Skip to content

feat(identity): populate agentic-identity fields on PDP + audit (#444 item 2) - #446

Merged
initializ-mk merged 2 commits into
mainfrom
feat/agentic-identity-fields
Sep 7, 2026
Merged

feat(identity): populate agentic-identity fields on PDP + audit (#444 item 2)#446
initializ-mk merged 2 commits into
mainfrom
feat/agentic-identity-fields

Conversation

@initializ-mk

@initializ-mk initializ-mk commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Part of #444 (agent identity L1–L4). Item 2 — populate the security-next#41 promoted identity columns on audit events + the PDP request.

Depends on item 1 (#445), now merged to main. This PR is rebased onto main; the diff is item-2-only. AttestationLevelForMode() uses the WORKLOAD_IDENTITY_MODE constants item 1 added (now present in main).

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_sub without 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_levelattested:placement when WORKLOAD_IDENTITY_MODE=k8s_sa, else omitted (SPIRE attested:workload is item 6).
  • delegation_modeagent_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 under agent_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's ChainContext (X-Agent-Chain-Token).

Implementation

  • forge-core/runtime/agent_identity.go (new): 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. pdp_decision and all other events inherit it via the existing Emit stamp — no per-emitter changes.
  • pdpCaller gains the identity fields; pdpRequest gains chain_id/chain_hop; Resolve stamps the three now-known values.

Wire-shape note for the reviewer

I mapped the identity fields onto caller and chain_id/chain_hop onto the request root, per the epic's phrasing ("caller: principal_sub, … + chain_id, chain_hop on the request"). Please sanity-check against security-next agent_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 under caller, 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-lint clean; runtime + cli-runtime suites pass.

#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.
@initializ-mk
initializ-mk force-pushed the feat/agentic-identity-fields branch from 8812939 to bbc7f5b Compare September 6, 2026 20:46
@initializ-mk
initializ-mk changed the base branch from feat/workload-token-presentation to main September 6, 2026 20:46

@initializ-mk initializ-mk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 against develop.

Verified correct against develop

  • Wire nesting matches exactlyPDPCaller carries all eight identity fields; PDPDecideRequest has chain_id/chain_hop at 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.go struct + knownTopLevelKeys), so forge's top-level emission maps straight to columns.
  • del enum values match exactly (chained / connected_account:user / connected_account:workspace / mandate / agent_own / none).
  • agent_own + empty principal_sub is the PDP's correctly-allowed pathDelegationHasHumanPrincipal(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; AttestationAtLeast ranks empty == asserted (0), so forge behaves correctly under floors.

Changes requested

  1. actor_agent_id value-format discrepancy (verified against develop). See inline. security-next develop documents actor_agent_id as urn:agent:<slug>; forge emits the bare p.agentID/agentID on 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 the urn:agent: shape will not match forge's rows. Emit urn:agent:<slug>, or confirm bare ids are accepted and fix the security-next doc.
  2. Attestation floor × non-k8s_sa (rollout note). A policy floor of attested:placement+ will DENY a forge deployed without WORKLOAD_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 of asserted does not deny — empty ranks equal to asserted.)
  3. Phantom-principal hardening (minor). The live guard only checks the missing-principal direction, so agent_own + principal_sub is not actively penalized today; forge's omission is correct. A defensive clear-under-agent_own at the emit + pdpCaller layer is nice-to-have insurance for when items 3 / L2 start populating principal_sub.
  4. CI hasn't run — "no checks reported on the branch"; needs to trigger and go green before merge.
  5. Minor doc nit — the AttestationPlacement constant 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.

Comment thread forge-cli/runtime/pdp_resolver.go Outdated
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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
initializ-mk added a commit that referenced this pull request Sep 7, 2026
)

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
@initializ-mk

Copy link
Copy Markdown
Contributor Author

Addressed in b8d39fd (verified against security-next develop, thanks for re-checking the right branch):

1. actor_agent_id → urn:agent:<slug> — new AgentURN() helper applied at both the PDP caller and the audit stamp, so L4 report / foreign-agent joins match. caller.subject (agent:<id>), audit entity_id, and pdpRequest.agent keep the bare id (their existing forms).

2. Attestation floor rollout note — added to tenancy.md: a bind-strength floor of attested:placement+ denies a forge deployed without WORKLOAD_IDENTITY_MODE=k8s_sa (empty ranks below placement); an asserted floor does not.

3. Phantom-principal hardening — the audit emitter AND the PDP request now defensively clear principal_sub/principal_iss whenever delegation_mode == agent_own. Insurance for when items 3 / L2 start populating a delegated principal; harmless today. New test covers it.

5. Doc nit — fixed the AttestationPlacement comment (item 6 is the SPIRE attested:workload phase).

4. CI — this push should trigger the checks; will confirm green.

golangci-lint clean; runtime + cli-runtime suites pass. The confirmed wire-nesting / del enum / agent_own path are unchanged.

@initializ-mk

Copy link
Copy Markdown
Contributor Author

Confirmed the field placement against security-next models/agent_policy_model.go on develop (post-#44) — #446 has it right:

  • Identity fields under caller (PDPCaller): principal_sub, principal_iss, delegation_mode, actor_agent_id, actor_workload_id, attestation_level, mandate_id, grant_ref — all omitempty.
  • chain_id + chain_hop at the request root (PDPDecideRequest), alongside invocation_id/context.

Two specifics worth matching exactly:

  1. chain_hop is *int on our side (json:"chain_hop,omitempty"), deliberately a pointer so hop 0 survives — an omitted/null chain_hop means "no chain", but hop 0 is the real ingress hop. So when a chain exists, send the integer including 0; only omit it when there's genuinely no chain. (Same on the audit column.)

  2. Audit events use a FLAT shape, not the request's split. On /pdp/decide the identity is under caller, but on emitted audit events all ten are promoted top-level columns (security-next#41): chain_id, chain_hop, mandate_id, grant_ref, principal_sub, principal_iss, delegation_mode, actor_agent_id, actor_workload_id, attestation_level at the event root — not nested under a caller object. Worth confirming feat(identity): populate agentic-identity fields on PDP + audit (#444 item 2) #446 stamps them flat there.

Also 👍 on the delegation_mode = agent_own + leaving principal_sub empty: that's exactly correct against the merged phantom-principal guard (security-next#42) — it denies a user-delegated mode (chained/connected_account:user) with no resolved principal_sub; agent_own has no human principal, so an empty principal_sub there is the intended, guard-safe posture. The del enum + attestation constants match security-next.

@initializ-mk initializ-mk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All requested changes resolved — verified against the branch source + security-next develop. ✅ All 10 CI checks now green (previously unreported).

#1actor_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.

@initializ-mk
initializ-mk force-pushed the feat/agentic-identity-fields branch from b8d39fd to 1a6ddad Compare September 7, 2026 18:33
@initializ-mk
initializ-mk merged commit b834553 into main Sep 7, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant