feat(identity): populate agentic-identity fields on PDP + audit (#444 item 2) - #446
Conversation
#444 item 2) Adds the security-next#41 promoted identity columns to every audit event and the PDP request, populating what forge can source correctly today and plumbing the rest for items 3 / L2. Populated now (process-static, no phantom-principal risk): - actor_agent_id — the agent's own id. - attestation_level — attested:placement in WORKLOAD_IDENTITY_MODE=k8s_sa, else omitted (SPIRE attested:workload is item 6). - delegation_mode — agent_own: the agent acts as its own principal, which is forge's actual PDP posture (caller.subject = agent:<id>, no end-user subject threaded into the decision). Plumb-only (declared, omitempty, populated as sources land): - principal_sub / principal_iss — accompany a delegated delegation_mode (items 3 / L2); MUST stay empty under agent_own or the platform's phantom-principal guard trips. - actor_workload_id — needs the SA-ref source (not parsed from the token). - mandate_id / grant_ref — L2 delegation flows. - chain_id / chain_hop — item 3's ChainContext (X-Agent-Chain-Token). Implementation: - New forge-core/runtime/agent_identity.go: the `del` enum + attestation constants (exact security-next strings) + AttestationLevelForMode(). - AuditEvent gains the 10 columns (omitempty); AuditLogger.WithAgentIdentity static-stamps actor_agent_id/attestation_level/delegation_mode on every event (mirrors WithEntity), wired at runner startup. - pdpCaller gains the identity fields, pdpRequest gains chain_id/chain_hop; Resolve stamps the three now-known values. Tests: attestation derivation, audit stamp on every event, unpopulated columns omitted (incl. no phantom principal_sub), explicit-event override, PDP caller shape. golangci-lint clean; runtime + cli-runtime suites pass. Stacked on #445 (item 1) — depends on its workload_token.go mode constants.
8812939 to
bbc7f5b
Compare
initializ-mk
left a comment
There was a problem hiding this comment.
Verdict: changes requested. I verified this end-to-end against security-next develop (your target branch) — the cross-repo contract is realized there and forge matches it, so this is close to approve-grade; the substantive ask is one value-format discrepancy.
Note: my first pass checked security-next
main(where the schema is not landed) and I flagged a nesting / promoted-columns mismatch — that was the wrong branch and is withdrawn. Everything below is againstdevelop.
Verified correct against develop
- Wire nesting matches exactly —
PDPCallercarries all eight identity fields;PDPDecideRequesthaschain_id/chain_hopat the request root — precisely the nesting this PR chose. (This is the sanity-check the PR asked for: confirmed.) - Audit fields are genuinely promoted top-level columns on develop (
audit_event_model.gostruct +knownTopLevelKeys), so forge's top-level emission maps straight to columns. delenum values match exactly (chained/connected_account:user/connected_account:workspace/mandate/agent_own/none).agent_own+ emptyprincipal_subis the PDP's correctly-allowed path —DelegationHasHumanPrincipal(agent_own)is false (models/delegation.go), so the subject-continuity check (helper/pdp_evaluator.go) is skipped. The design is exactly right for the live PDP.- Attestation values match;
AttestationAtLeastranks empty == asserted (0), so forge behaves correctly under floors.
Changes requested
actor_agent_idvalue-format discrepancy (verified against develop). See inline. security-next develop documentsactor_agent_idasurn:agent:<slug>; forge emits the barep.agentID/agentIDon both the PDP caller and the audit stamp. Fail-safe today (no evaluator parses the prefix), but any L4 report / foreign-agent join keyed on theurn:agent:shape will not match forge's rows. Emiturn:agent:<slug>, or confirm bare ids are accepted and fix the security-next doc.- Attestation floor × non-k8s_sa (rollout note). A policy floor of
attested:placement+ will DENY a forge deployed withoutWORKLOAD_IDENTITY_MODE=k8s_sa(attestation_level "" → rank 0) once enforcement is on. Correct-by-design, but surface it in rollout docs so self-hosted deployments with attestation floors are not surprised. (A floor ofasserteddoes not deny — empty ranks equal to asserted.) - Phantom-principal hardening (minor). The live guard only checks the missing-principal direction, so
agent_own+principal_subis not actively penalized today; forge's omission is correct. A defensive clear-under-agent_ownat the emit +pdpCallerlayer is nice-to-have insurance for when items 3 / L2 start populatingprincipal_sub. - CI hasn't run — "no checks reported on the branch"; needs to trigger and go green before merge.
- Minor doc nit — the
AttestationPlacementconstant comment cites "#444 item 6"; item 6 is SPIRE (attested:workload).
The implementation itself is correct and fail-safe against develop — nice work threading the phantom-principal invariant. #1 is the one that will actually bite an L4 report if left as a bare id.
| Subject: "agent:" + p.agentID, | ||
| // Agentic-identity (#444 item 2): the agent acts as its own | ||
| // principal, so no principal_sub accompanies agent_own. | ||
| ActorAgentID: p.agentID, |
There was a problem hiding this comment.
Change requested: value-format mismatch against security-next develop. Here (and in the audit stamp via WithAgentIdentity(agentID, …)) forge sends the BARE agent id. But security-next develop documents actor_agent_id as urn:agent:<slug> — models/agent_policy_model.go:247 (PDPCaller) and models/audit_event_model.go:58 (audit ingest column). No PDP evaluator parses the prefix today, so it is fail-safe, but the L4 actor-breakdown / foreign-agent reports key on that shape and a bare id will not join. Either format this as urn:agent:<slug> at both stamp sites, or confirm with the platform team that bare ids are accepted and correct the develop schema comment. Note it is distinct from caller.Subject, which you correctly build as agent:<id> — so there are three id shapes in play; worth making actor_agent_id match the documented one.
) Addresses the #446 review (verified against security-next develop): 1. actor_agent_id now uses the urn:agent:<slug> form the platform's L4 actor-breakdown / foreign-agent reports key on (new AgentURN helper), at both the PDP caller and the audit stamp. caller.subject, audit entity_id, and pdpRequest.agent keep the bare id (their existing forms). 2. Phantom-principal hardening: the audit emitter AND the PDP request now defensively clear principal_sub/iss whenever delegation_mode is agent_own — insurance for when items 3 / L2 begin populating a delegated principal. Harmless today (principal_sub is unset). 3. Rollout note (tenancy.md): a PDP bind-strength floor of attested:placement+ denies a forge deployed without WORKLOAD_IDENTITY_MODE=k8s_sa (empty attestation ranks below placement); an `asserted` floor does not. 4. Doc nit: fixed the AttestationPlacement constant comment (item 6 is the SPIRE attested:workload phase, not placement). Tests: AgentURN format + empty guard; agent_own clears an explicitly-set principal_sub; PDP caller asserts the urn:agent: actor id + bare subject. golangci-lint clean; runtime + cli-runtime suites pass.
) Addresses the #446 review (verified against security-next develop): 1. actor_agent_id now uses the urn:agent:<slug> form the platform's L4 actor-breakdown / foreign-agent reports key on (new AgentURN helper), at both the PDP caller and the audit stamp. caller.subject, audit entity_id, and pdpRequest.agent keep the bare id (their existing forms). 2. Phantom-principal hardening: the audit emitter AND the PDP request now defensively clear principal_sub/iss whenever delegation_mode is agent_own — insurance for when items 3 / L2 begin populating a delegated principal. Harmless today (principal_sub is unset). 3. Rollout note (tenancy.md): a PDP bind-strength floor of attested:placement+ denies a forge deployed without WORKLOAD_IDENTITY_MODE=k8s_sa (empty attestation ranks below placement); an `asserted` floor does not. 4. Doc nit: fixed the AttestationPlacement constant comment (item 6 is the SPIRE attested:workload phase, not placement). Tests: AgentURN format + empty guard; agent_own clears an explicitly-set principal_sub; PDP caller asserts the urn:agent: actor id + bare subject. golangci-lint clean; runtime + cli-runtime suites pass. Claude-Session: https://claude.ai/code/session_01Hkimw1PDJRY5Dh8BgNQxWJ
|
Addressed in b8d39fd (verified against security-next develop, thanks for re-checking the right branch): 1. actor_agent_id → 2. Attestation floor rollout note — added to 3. Phantom-principal hardening — the audit emitter AND the PDP request now defensively clear 5. Doc nit — fixed the 4. CI — this push should trigger the checks; will confirm green.
|
|
Confirmed the field placement against security-next
Two specifics worth matching exactly:
Also 👍 on the |
initializ-mk
left a comment
There was a problem hiding this comment.
All requested changes resolved — verified against the branch source + security-next develop. ✅ All 10 CI checks now green (previously unreported).
#1 — actor_agent_id format → resolved. New AgentURN(slug) helper emits urn:agent:<slug> — the exact form the develop L4 reports key on — at BOTH stamp sites (pdpCaller.ActorAgentID and WithAgentIdentity). Correctly scoped: caller.Subject stays the bare agent:<id>, and WithEntity/entity_id + pdpRequest.agent stay bare (only actor_agent_id needed the urn form). AgentURN("") returns "" so an empty slug omits rather than emitting a bare urn:agent: prefix. The test pins both shapes (urn:agent:member-service + bare agent:member-service), and TestAgentURN covers the empty edge.
#2 — attestation-floor rollout note → resolved. tenancy.md now documents that a attested:placement+ bind-strength floor will deny a non-k8s_sa forge once enforcement is on, and that an asserted floor does not (empty ranks equal to asserted). Accurate to develop’s AttestationRank.
#3 — phantom-principal hardening → resolved. A defensive guard clears principal_sub/principal_iss whenever delegation_mode == agent_own, at BOTH choke points — the audit Emit (after the static-stamp fill, so runtime events resolve to agent_own then clear) and the pdpCaller construction. Correctly scoped: a delegated mode (chained, etc.) keeps its principal. Test TestWithAgentIdentity_ClearsPhantomPrincipalUnderAgentOwn pins it. Good insurance for when items 3 / L2 start populating a principal.
#5 — doc nit → resolved. The AttestationPlacement comment now attributes it to item 2 (set now) and describes item 6 as the SPIRE alternative.
#4 — CI → green. All 10 checks pass.
No new issues. The core (nesting matches develop, del enum match, agent_own continuity-skip, promoted audit columns) was already correct and is unchanged. LGTM for #444 item 2 — clean, responsive iteration.
b8d39fd to
1a6ddad
Compare
Part of #444 (agent identity L1–L4). Item 2 — populate the security-next#41 promoted identity columns on audit events + the PDP request.
Approach: populate what's correct today, plumb the rest
I investigated each field's runtime source before writing anything (there's a per-field verdict behind this). The guiding constraint: populating
principal_subwithout a matching delegation mode would trip the platform's phantom-principal guard once enforcement is on — so I only populate what's unambiguous and guard-safe.Populated now (process-static):
actor_agent_id— the agent's own id.attestation_level—attested:placementwhenWORKLOAD_IDENTITY_MODE=k8s_sa, else omitted (SPIREattested:workloadis item 6).delegation_mode—agent_own: forge's actual PDP posture today (caller.subject = agent:<id>; no end-user subject is threaded into the tool-call decision — the code already documents this). This is accurate, not a guess, and it lights up the L4 agent-vs-human report correctly.Plumb-only (declared
omitempty, populated as sources land):principal_sub/principal_iss— accompany a delegated mode (items 3 / L2); MUST stay empty underagent_own(phantom-principal invariant).actor_workload_id— needs an SA-ref source (forge deliberately doesn't parse the SA token JWT).mandate_id/grant_ref— L2 delegation flows.chain_id/chain_hop— item 3'sChainContext(X-Agent-Chain-Token).Implementation
forge-core/runtime/agent_identity.go(new): thedelenum + attestation constants (exact security-next strings) +AttestationLevelForMode().AuditEventgains the 10 columns (omitempty);AuditLogger.WithAgentIdentity(...)static-stampsactor_agent_id/attestation_level/delegation_modeon every event (mirrorsWithEntity), wired at runner startup.pdp_decisionand all other events inherit it via the existingEmitstamp — no per-emitter changes.pdpCallergains the identity fields;pdpRequestgainschain_id/chain_hop;Resolvestamps the three now-known values.Wire-shape note for the reviewer
I mapped the identity fields onto
callerandchain_id/chain_hoponto the request root, per the epic's phrasing ("caller: principal_sub, … + chain_id, chain_hop on the request"). Please sanity-check against security-nextagent_policy_model.go— if the PDP expects a different nesting, it's a quick tag change (fail-safe until then: the PDP sees the fields undercaller, and unknown fields are ignored — no security regression, same as today's null baseline).Tests
Attestation derivation (k8s_sa / unset / other-mode), audit stamp on every event, unpopulated columns omitted (incl. no phantom
principal_sub), explicit-event value overrides the static stamp, PDP caller shape.golangci-lintclean;runtime+cli-runtimesuites pass.