chore(audit): add the management audit seam with actor capture
Why
The remote audit stream has no actor anywhere, and Phase 6's management surface needs audit payloads that carry one. This MR lands the three payloads (repository writes with the previous and new URL on a URL change, upstream association changes, and the connection test with a passed/failed/unavailable outcome), the Actor field on the existing credential event, the structured-log rendering, and the managementapi AuditSink seam with the actor resolver, which is the seam the later handler steps consume. No management handler emits yet: consumers arrive with the connection-test, settings-PATCH, create-enablement, URL-change, and association steps. Real actor population arrives when the bootstrap identity stub is replaced. Until then the field renders present-and-empty.
Plan: 2026-08-13-s17-phase6-virtual-remote-repositories.md (Step 3). Spec: S17-rest-management-api.md.
What (non-obvious only)
internal/managementapi/handler.gois a fifth file beyond the plan step's list. TheDeps.Auditfield is the receiving end ofwire_management's "pass the sink". Nothing reads it yet, and therequireDepsguard lands with the first handler step that does.RecordCredentialWrite's signature is unchanged: existing store emitters keep compiling with an empty actor, and the per-format store steps thread the real one when their inputs gain it.RepositoryWriteAction,UpstreamAssociationAction, andConnectionTestOutcomeare exported discriminators because the spec names their values. Each is consumed by its payload.- The URL pair renders when either side is set, exposing a contract-violating half-set pair rather than hiding it (mutation-pinned test).
docs/dev/observability.mdis updated because this diff falsified its audit-stream section (the no-actor claim, the credential field list, and the record catalog).- The boot suppression breadcrumb now names the whole remote audit trail, keeping the greppable "audit trail suppressed" stem.
Diff size
1216 reviewable LOC, past the 500 guardrail. Justification: the payloads, renderer, seam, and actor resolver are one contract, and 775 of the 1216 are the tests that pin it. By file group: tests 775 (sink rendering 587, actor resolver 186, wiring-test comment 2), production 423 (payload types 140, renderer 157, seam plus resolver 101, Deps field 11, breadcrumb 11, wiring 3), docs 18.
Test plan
gofmt, build, and vet clean. golangci-lint 2.12 (plain and --build-tags=integration, with --max-same-issues=0 --max-issues-per-linter=0): 0 new findings. Full unit sweep green. Integration suites for internal/remote, internal/managementapi, and cmd/artifact-registry green. Sink rendering tests cover every enumerated action/outcome value as a positive hit, the URL half-set case, the userinfo masking and unparseable-value arms, and per-method empty actors. The actor resolver has 7 cases through the real auth middleware chain, including empty-actor and fail-closed invalid-origin arms. No e2e scenario affected: the catalogs are protocol-client driven and this is management-surface plumbing with no served behavior change.
Spec coverage
| # | Criterion (slice) | Tests |
|---|---|---|
| AC-79 (remote CRUD) | Create, update, and delete of a remote repository each emit one audit event naming the actor | TestRemoteAuditLogSink_RepositoryWritePayload (all three actions as positive hits; audit_actor asserted per case, including present-but-empty) |
| AC-79 (url change) | The update event for a url change records the previous and new URL |
TestRemoteAuditLogSink_RepositoryWritePayload/update_with_url_change (both keys present, together, with the event's values); /create, /update_without_url_change, /delete_with_empty_actor (both keys absent, not merely empty); half-set arm pins either-field rendering |
| AC-79 (credential event) | Every credential write emits the credential event | Pre-existing TestRemoteAuditLogSink_EmitsStructuredPayload and _DropsAboveItsLevel kept whole; actor attribute pinned by _EmitsStructuredPayload (empty renders) and _RendersCredentialActor (non-empty renders). Store-side emission with a real actor is the store steps' to wire. |
| AC-79 (associations) | Each association mutation (associate, reposition, dissociate) emits one naming the actor | TestRemoteAuditLogSink_UpstreamAssociationPayload (all three actions; virtual and upstream repository ids, position, actor) |
| AC-85 (audit slice) | The connection test emits its audit event naming the actor on every request | TestRemoteAuditLogSink_ConnectionTestPayload (all three outcomes as positive hits); the endpoint's emission lands with the connection-test handler step |
| AC-87 (audit slice) | The audit entry records outcome unavailable when the probe ran but the health write failed |
TestRemoteAuditLogSink_ConnectionTestPayload/unavailable; remote.ConnectionTestUnavailable's doc pins it as audit-entry-only, never a stored health status |
| Error case: connection test, health write failed | Audit entry still names the actor, outcome unavailable |
TestRemoteAuditLogSink_ConnectionTestPayload/unavailable; the 500 envelope is the handler step's |
| Security: credential values never in logs | No audit payload carries a credential field, and the renderer masks a userinfo component in URL values (fixed redacted marker, invalid-url for a value that fails URL parsing) with tests asserting the secret bytes never reach the rendered line |
|
| Security: SSRF attribution (url + association changes audited with actor, url event records previous/new) | TestRemoteAuditLogSink_RepositoryWritePayload (URL arms, actor), TestRemoteAuditLogSink_UpstreamAssociationPayload (actor on all three mutations) |
|
| Step acceptance: each payload renders as one structured log line with actor, namespace, and repository attributes | the three per-payload tests (single-JSON-line parse, level Info, per-payload attribute set); shared-level ridership: TestRemoteAuditLogSink_DropsAboveItsLevel (info emits, warn/error drop, per method) |
|
| Emitter helper incl. empty-actor case | TestAuditActor: no holder, holder without identity, anonymous, ids-under-OriginUnspecified → ""; organization:/gitlab_federated: renderings; out-of-range Origin → fail-closed invalid_origin: |
Related to #314