From 352d21fe4fd348adfbb858b968ea2ab255adce0d Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 20 Aug 2026 03:10:10 -0400 Subject: [PATCH] pdp: send invocation_id so the platform verdict attributes to the turn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PDP resolver didn't send the turn's correlation id, so the platform-written tool_call_decided event (security-next) had no invocation_id and the console's invocation timeline dropped it into "unattributed events" — while this agent's own pdp_decision for the same call was attributed. Send InvocationID = hctx.CorrelationID (the SAME source emitPDPDecision uses for the pdp_decision event), so the platform verdict and the agent-side record carry an identical invocation_id and group together. Test: assert the decide request carries invocation_id + session. --- forge-cli/runtime/pdp_resolver.go | 13 +++++++++++-- forge-cli/runtime/pdp_resolver_test.go | 9 +++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/forge-cli/runtime/pdp_resolver.go b/forge-cli/runtime/pdp_resolver.go index 7a9b3ab..1df3f19 100644 --- a/forge-cli/runtime/pdp_resolver.go +++ b/forge-cli/runtime/pdp_resolver.go @@ -69,7 +69,12 @@ type pdpRequest struct { Caller pdpCaller `json:"caller"` Agent string `json:"agent"` Session string `json:"session,omitempty"` - Context map[string]any `json:"context,omitempty"` + // InvocationID is the turn's correlation id. Sent so the platform-written + // tool_call_decided event carries the same invocation_id as this agent's + // pdp_decision / task_deferred events and groups with them in the console's + // invocation timeline (otherwise the platform verdict is "unattributed"). + InvocationID string `json:"invocation_id,omitempty"` + Context map[string]any `json:"context,omitempty"` } type pdpCaller struct { @@ -154,7 +159,11 @@ func (p *pdpResolver) Resolve(ctx context.Context, hctx *coreruntime.HookContext Caller: pdpCaller{Subject: "agent:" + p.agentID}, Agent: p.agentID, Session: hctx.TaskID, - Context: map[string]any{}, + // Same source as the sibling pdp_decision event (emitPDPDecision uses + // hctx.CorrelationID) so the platform tool_call_decided and this agent's + // pdp_decision carry an IDENTICAL invocation_id and group together. + InvocationID: hctx.CorrelationID, + Context: map[string]any{}, } body, err := json.Marshal(reqBody) if err != nil { diff --git a/forge-cli/runtime/pdp_resolver_test.go b/forge-cli/runtime/pdp_resolver_test.go index b107768..621f72e 100644 --- a/forge-cli/runtime/pdp_resolver_test.go +++ b/forge-cli/runtime/pdp_resolver_test.go @@ -58,6 +58,15 @@ func TestPDPResolver_Allow(t *testing.T) { if req.Caller.Subject != "agent:member-service" || req.Caller.EntitledAccounts != nil { t.Errorf("caller = %+v, want subject agent:member-service, no entitled_accounts", req.Caller) } + // The turn's correlation id + task id must be sent so the platform-written + // tool_call_decided event attributes to the same invocation as this agent's + // pdp_decision (else it lands in "unattributed events"). + if req.InvocationID != "corr-1" { + t.Errorf("invocation_id = %q, want corr-1 (the hook correlation id)", req.InvocationID) + } + if req.Session != "task-1" { + t.Errorf("session = %q, want task-1", req.Session) + } writeEnvelope(w, `{"decision":"allow","reason":"within policy","policy_version":7}`) })) defer srv.Close()