Skip to content

[Proposal] Clarify request_id_ref semantics and ACS-Core HKDF parameters #118

Open

Description

@shivang-sec

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

#117

Current Priority Scope

Feeds conformance evidence

Activity

  1. rocklambros commented on Sep 17, 2026

    @rocklambros
    Collaborator

    Recording two things that were settled in discussion and never written down here.

    request_id_ref resolution. The agreed wording is that a Guardian SHOULD resolve request_id_ref against the toolCallRequest records 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 toolCallResult whose request_id_ref had 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_id as 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.md puts 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.

  2. rocklambros commented on Oct 13, 2026

    @rocklambros
    Collaborator

    Recording one more decision for the §10 HMAC details, this time on what key_id means. Ariel settled it in Slack today.

    The spec only defines key_id as the identifier "resolving to the public key", and HMAC has no public key. The reference implementations each filled that gap differently: one ties it to session_id, one hardcodes "default", one ships default as 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_id is 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:

    key_id is an opaque label, chosen by the deployment, that names one configured secret. A verifier MUST reject an unknown key_id and MUST NOT fall back to another secret. key_id MUST NOT be derived from the secret.

    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-resolve key_id") then means something.

    What this means for the open implementations. Every one of them should read key_id from config, with no default:

    The spec text lands with the PR for this issue.

  3. added
    status:acceptedIn the backlog. Maintainers only
    scope:in-focusFeeds the ninety-day committed outcome. Maintainers only
    workstream:specOwning workstream. Maintainers only
    and removed
    status:needs-triageNot yet triaged. Applied by the issue forms
    on Oct 13, 2026
  4. self-assigned this
    on Oct 13, 2026
  5. moved this from Needs triage to In progress in ACS Project Trackeron Oct 13, 2026
  6. dtduc-git commented on Oct 14, 2026

    @dtduc-git

    Done in #183: key_id now 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 a conformance label anymore (the string survives only as a test fixture and in prose), and the example label is 2026-10.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

priority:P1Maintainers onlyscope:in-focusFeeds the ninety-day committed outcome. Maintainers onlystatus:acceptedIn the backlog. Maintainers onlytype:proposalNew feature or requestworkstream:specOwning workstream. Maintainers only

Type

No type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions