fix(attestation): fail closed on subagent per-spawn attestation MISS (lr-2a8653) - #26
Conversation
…icy (lr-2a8653) Reuses lr-2ca216's DomainResolver substrate for the local (GitHub) mint path: a per-spawn attestation MISS on a subagent invocation must fail closed like DomainA2A already does, not fall through to a lower-priority provider. DomainLocal (lead/director session, no per-spawn source by design) is unaffected.
…nLocal (lr-2a8653) Service gains DomainResolver (preferred) alongside the legacy AttestationResolver field. MintForDomain resolves identity under the caller-supplied Domain's MISS policy; Mint is now MintForDomain scoped to DomainLocal, so every existing caller keeps today's exact behavior. Adds the T+/T- regression pair at the Service level: a subagent per-spawn MISS refuses and never mints under the parent's identity; a lead/director per-spawn MISS still resolves via the session sidecar unchanged.
…r's own env var (lr-2a8653) runMint now builds a DomainResolver with PerSpawn scoped to attestation.sidecars[0] (the deployment's per-spawn namespace, per the documented spawn-first convention) and picks DomainLocalSubagent when that entry's session_id_env is set in the process environment — the same signal sidecarProvider.Resolve itself checks for its own MISS. No new config or CLI flag: presence of the per-spawn harness's own env var IS the "a per-spawn source was expected here" signal. Closes the confused-deputy hole: a subagent spawn whose per-spawn sidecar file is missing (env set, file absent) now refuses instead of silently resolving to the parent lead's identity via the session-keyed sidecar adapter. A lead/director session (per-spawn env unset by design) is unaffected — still resolves via the session sidecar exactly as before (lr-86779f). Adds the CLI-level T+/T- regression pair mirroring the deployed two-sidecar config shape (subagent-/CLAGENTIC_SUBAGENT_ID then lore-agent-name-/CLAUDE_CODE_SESSION_ID). Test status: go build ./... and go test ./... both pass, all packages, including the pre-existing DomainA2A substrate tests (lr-2ca216) unchanged. REF lr-2ca216, lr-86779f, lr-3028e8, clagentic-loadout lr-1e16a4/PR #125.
…lr-2a8653) Section 5 previously stated the domain-aware policy shipped as substrate only, unwired into gatekeeper mint. That's no longer accurate for the local-subagent case now that cmd/gatekeeper constructs a DomainResolver and selects DomainLocalSubagent from the per-spawn sidecar's own env var.
|
PEACHES — clean (0 findings) Review of PR #26 (fix/lr-2a8653-gatekeeper-per-spawn-fail-closed) against AMoS code-craft baseline and project rules. Summary: Checked:
|
|
Reviewed PR #26 (fix/lr-2a8653-gatekeeper-per-spawn-fail-closed) against RULEBOOK.md. Focus: confirm no residual fall-through from a subagent per-spawn attestation MISS to the session/parent identity. Trace: internal/attestation/domain_policy.go DomainResolver.Resolve (lines 149-168) — for DomainA2A and DomainLocalSubagent (Domain.requiresPerSpawn()==true), resolution delegates ONLY to d.PerSpawn.Resolve; d.Chain is never consulted on this branch. PerSpawn is built in cmd/gatekeeper/main.go (lines 161-171) from chainSidecars[0] alone (the per-spawn namespace) via an independent Resolver, so it is structurally incapable of reading the session sidecar entry even when present. A per-spawn MISS (ErrNoIdentity) or nil PerSpawn returns ErrPerSpawnRequired — a hard refusal, never a fallthrough to Chain's remaining providers. Any hard error from PerSpawn also propagates unchanged (no soft-downgrade). internal/mint/mint.go MintForDomain (lines 472-502) always resolves via a DomainResolver (wrapping legacy AttestationResolver callers at DomainLocal semantics for back-compat) and never calls the broker/entitlement path before this resolution succeeds. Both directions are test-covered end to end through runMint itself (cmd/gatekeeper/main_test.go) and at the mint.Service boundary (internal/mint/mint_test.go): subagent per-spawn MISS refuses and never reaches MintFunc; lead/director session per-spawn-absent still resolves via session sidecar unchanged (no lr-86779f regression). Discriminator (main.go lines 187-192): mintDomain is selected by reading the SAME session-id env var (chainSidecars[0].SessionIDEnv) that sidecarProvider.Resolve itself already reads to decide its own MISS. This is not a new trust boundary — an actor able to spoof/unset that env var to force DomainLocal already controls the same process environment the pre-existing sidecar-provider read trusts; no new bypass surface is introduced by reusing it as the domain discriminator. No secret/token exposure: gitleaks (full-tree, no-git mode) found 0 leaks. No SAST hits: semgrep --config=auto over the three changed Go files (mint.go, domain_policy.go, main.go), 131 rules, 0 findings. No SQL/command/deserialization/template/path-traversal patterns in the diff. sidecarProvider's existing Lstat-not-Stat symlink guard and requireContained path-containment check are untouched by this PR. Dependencies: go.mod is not touched by this diff. osv-scanner against go.mod reports pre-existing Go stdlib advisories tied to the toolchain floor (1.22.99) already present on main, unrelated to and not introduced by this PR -- out of scope per bobbie.dep.1/2 (no new dependency). Config/docs surface: docs/SETUP.md and docs/SIDECAR-READ-CONTRACT.md updates are documentation-only and consistent with the code change; no new config keys, no new CLI flags. No blocking or nit findings. Clean diff. |
|
Merged via clagentic-loadout v0.1.0
|
What changed
Closes a live confused-deputy hole in gatekeeper attestation resolver chain, P1 lr-2a8653: a subagent spawn whose per-spawn sidecar file misses was falling through to the session-keyed sidecar adapter and silently minting a token under its PARENT session identity, e.g. NAOMI-as-holden.
Why
MILLER diagnosis, confidence 0.9, verdict B live and uncovered, confirmed lr-2ca216 built the domain-aware fail-closed substrate but wired it to nothing. Every mint gatekeeper serves used the raw, domain-blind resolver. Deployed config has two sidecar adapters in order: a per-spawn one keyed on CLAGENTIC_SUBAGENT_ID, then a session one keyed on CLAUDE_CODE_SESSION_ID. Inside a subagent, CLAUDE_CODE_SESSION_ID is always the PARENT session id, so a per-spawn read MISS meant the resolver walked to the session adapter and attested the subagent as its parent lead. This is the same mechanism that converted lr-5a077d from refuse into mint-holden, retro 718.
Per lr-86779f, a blanket fail-closed-on-per-spawn-MISS would be wrong: a lead or director session legitimately has no per-spawn sidecar and must keep resolving via the session sidecar. This PR discriminator is whether a per-spawn source was expected for this invocation, reusing the existing DomainResolver and PerSpawn substrate rather than inventing a parallel mechanism.
Task
lr-2a8653, P1 bug. REF lr-2ca216 PR 23, lr-86779f, lr-3028e8, clagentic-loadout lr-1e16a4 PR 125.
Tests
Added regression tests at both layers, both directions:
tests_passed: true
Task: lr-2a8653