Repository navigation
[Proposal] Clarify request_id_ref semantics and ACS-Core HKDF parameters #118
Description
The problem
The v0.1.0 specification leaves two interoperability-related behaviors underspecified.
First, steps/toolCallResult.payload.request_id_ref is described as linking a result to its originating toolCallRequest, but the specification does not define the expected behavior when the reference cannot be resolved against the session's recorded toolCallRequests.
During testing of the AGT reference implementation, I submitted an otherwise valid steps/toolCallResult with a request_id_ref UUID that had never appeared as a toolCallRequest in the session. The reference Guardian accepted it in the same way as the equivalent result without request_id_ref.
Second, Section 10 specifies that ACS-Core uses HMAC-SHA256 with a per-session key derived using HKDF from deployment-provided input keying material and session_id, but does not specify the derivation parameters needed for deterministic interoperability.
The reference implementation currently uses HKDF-SHA256 with an empty salt, UTF-8 session_id as info, and a 32-byte output. Without normative parameters, independent implementations can derive different keys from the same input keying material and therefore be unable to verify each other's ACS-Core signatures.
Why the wire has to carry it
No new wire fields are proposed.
For request_id_ref, the existing field carries the correlation between a tool result and its originating toolCallRequest. Defining how that reference should be resolved and how an unresolvable reference must be recorded gives independent implementations consistent semantics for an existing wire field.
For ACS-Core signatures, the existing cryptographic contract requires a per-session HMAC key. Normative HKDF parameters are necessary so independent implementations can derive the same key from the same deployment-provided input keying material and session_id. Without deterministic derivation parameters, the existing HMAC-SHA256 wire contract cannot provide reliable cross-implementation interoperability.
Which constituencies this affects
- Observed Agent implementers
- Guardian implementers
- Policy authors
- Auditors and incident responders
- Platform and harness vendors
- Enterprise deployers
Alternatives considered
For request_id_ref, one alternative is to leave the field entirely informational. This would avoid defining resolution behavior, but would leave the existing "Links to the originating toolCallRequest" description without consistent semantics when the reference is present but cannot be resolved.
Another alternative is to require every unresolvable request_id_ref to result in denial. This would make the correlation rule unnecessarily prescriptive and couple the ACS wire contract to a particular authorization policy. Keeping the final disposition vendor-defined preserves that separation while still requiring the condition to be recorded.
For HKDF, one alternative is to leave the derivation parameters deployment-defined. This provides flexibility, but independent implementations could derive different per-session keys from the same input keying material and therefore fail to interoperate.
Another alternative is to document the reference implementation's current HKDF behavior without making it normative. This would describe the current implementation but would not establish a deterministic cryptographic interoperability profile for independent ACS-Core implementations.
No new wire fields are proposed by this change.
Discussion link
Current Priority Scope
Feeds conformance evidence
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsIn progress
Activity
rocklambros commentedon Sep 17, 2026
Recording two things that were settled in discussion and never written down here.
request_id_refresolution. The agreed wording is that a Guardian SHOULD resolverequest_id_refagainst thetoolCallRequestrecords it holds for the session, and MUST record the reference when it cannot resolve it. Whether an unresolvable reference is grounds to deny stays vendor policy. The point of the MUST is that an unresolvable reference is evidence and has to survive in the record, not that it dictates a disposition.That matches what you observed in the reference Guardian. It accepted a
toolCallResultwhoserequest_id_refhad never appeared in the session and treated it the same as a result with no reference at all. Under this wording the acceptance is fine and the silence is not.HKDF parameters. This is a real specification gap rather than an implementation detail, and it should be closed in the specification rather than by documenting what the reference implementation happens to do.
Until it is closed, the reference Guardian's choices are the de facto interoperability profile: HKDF-SHA256, empty salt, UTF-8
session_idas info, 32-byte output. Worth saying plainly, because it is the part that should be uncomfortable: a second implementer who wants to verify our ACS-Core signatures today has to match our code rather than our specification.docs/spec/conformance.mdputs the baseline HMAC signature in the ACS-Core mandatory list, so this is a mandatory requirement that two independent implementations can both satisfy and still fail to interoperate.rocklambros commentedon Oct 13, 2026
Recording one more decision for the §10 HMAC details, this time on what
key_idmeans. Ariel settled it in Slack today.The spec only defines
key_idas the identifier "resolving to the public key", and HMAC has no public key. The reference implementations each filled that gap differently: one ties it tosession_id, one hardcodes"default", one shipsdefaultas its example, and one defaults to"conformance". If you pair the TypeScript Guardian with the Python adapters, every request gets-32004.The decision is that
key_idis an opaque label the deployment chooses, naming one configured secret. The spec reserves no values. Ariel's reasoning on"default"is that it suggests there will only ever be one secret, invites itself as a fallback, and ends up hardcoded. The proposed text:The example in the text becomes a version label such as
"2026-10", and"default"comes out. That also gives rotation a hook: a Guardian can hold two secrets while a rotation is in progress, and the §17.1 recovery for-32004("re-resolvekey_id") then means something.What this means for the open implementations. Every one of them should read
key_idfrom config, with no default:key_idtosession_id(server.ts:700,:915, and the host check at:1345). Sign with the configured label, and refuse a request whosekey_idisn't the configured one. HKDF still binds the derived key to the session, so nothing is lost.acs_common.py:284defaultskey_idto"default". Please take it from config instead.guardian.yaml:24fromdefaultto something like2026-10, and confirm a request with an unknownkey_idis refused, not verified with the configured secret.server.py:76defaults to"conformance", and the Guardian should refuse akey_idit doesn't have configured.The spec text lands with the PR for this issue.
Answer the review: close the ping oracle and the deep-nesting fail-op…
dtduc-git commentedon Oct 14, 2026
Done in #183:
key_idnow comes from configuration with no default, and a request whose signature names a label this deployment does not hold never resolves a key — a step within the depth limit is refused -32004, unsigned; a ping is still answered, unsigned; a body past the 128-level limit gets -32600, unsigned. Nothing is ever verified with the configured secret under another label, no code path defaults to aconformancelabel anymore (the string survives only as a test fixture and in prose), and the example label is2026-10.Answer the 12 October review of the Go Guardian