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()