Skip to content

Docs: say that key-discovery "operator" is the signing agent's operator, never account.operator #1426

Description

@bokelley

Problem

The signing docs (docs/agent-resolution-33.md, docs/request-signing-migration.md, and the BrandJsonJwksResolver docstrings) use "operator" to mean the signing agent's operator: whoever publishes the brand.json that lists the agent and its keys. They never say this is a different thing from the operator in a request's account (account.operator, the natural key's agency or seat holder). The spec makes the distinction explicitly (security.mdx, "Discovering an agent's signing keys via brand_json_url": "It isn't the operator in an account's natural key"). The SDK docs don't repeat it.

A seller we work with built its verifier as BrandJsonJwksResolver(f"https://{account.operator}/.well-known/brand.json", ...). It then resolved our signing key from the buyer's agency domain and rejected every request with request_signature_jwks_untrusted. The example BrandJsonJwksResolver("https://operator.example.com/brand.json", ...) makes that reading natural. It fails for every real buyer, because agency domains don't publish the buying agent's keys.

Proposal

  1. Add a short callout where "operator" first appears in the signing docs, and in the BrandJsonJwksResolver docstring:

    The operator here is the signing agent's operator, discovered from the agent (its get_adcp_capabilities identity.brand_json_url) or configured at onboarding. It is never the operator (or brand) in the request's account. Do not derive key discovery from request-body fields.

  2. Lead the verifier docs with the agent-first paths: verify_from_agent_url / async_resolve_agent, and JwksUriSignerKeys / StaticSignerKeys keyed by agent URL for onboarded counterparties. Present direct BrandJsonJwksResolver construction as the lower-level option.
  3. Optionally, warn in debug logging when the brand.json host equals the request's account.operator host, since that pairing is almost always this mistake.

Activity

  1. added
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 6, 2026
  2. bokelley commented on Oct 6, 2026

    @bokelley
    ContributorAuthor

    Triage

    Classification: docs
    Bucket(s): docs, signing
    Status: ready-to-implement
    Milestone: (omitted — no version target specified)

    What the experts said:

    • docs-expert: Proposal 1 is a blocker — BrandJsonJwksResolver docstring and agent-resolution-33.md example both omit the source of brand_json_url; two targeted sentences fix both entry points. Proposal 2 is a nit (migration doc already leads with JwksUriSignerKeys; BrandJsonJwksResolver doesn't appear there). Proposal 3 is out of scope — constructor has no access to account.operator, false-positive risk is real.
    • dx-expert: Confirms blocker severity — the naming collision (account.operator vs. signing-agent operator) is structurally set up by the example; a coding agent or new integrator will reach for account.operator as the only operator-named symbol in scope. Minimum fix: one sentence in the class docstring + one comment line above the constructor call in the doc example.

    My take: The bug report is precise and confirmed: a seller misread account.operator for the brand.json URL source — a natural reading of the current docs — and request_signature_jwks_untrusted gave no field hint. Two surgical edits close both reading entry points (IDE hover on the class + linear doc read). Proposals 2 and 3 from the issue are deferred as noted.

    • Edit src/adcp/signing/brand_jwks.py BrandJsonJwksResolver class docstring (~line 368): add a sentence to the opening paragraph: "Obtain brand_json_url from the signing agent's identity.brand_json_url capabilities field — this is the signing agent's operator (whoever publishes the brand.json). Do not use account.operator from the inbound request; that is the request's account-scoping field, unrelated to signing key discovery."
    • Edit docs/agent-resolution-33.md constructor example (~line 21): add an inline comment or parenthetical to "https://operator.example.com/brand.json" clarifying that operator.example.com is the signing agent's operator domain (from identity.brand_json_url), not the request's account.operator field.
    • No changes needed to docs/request-signing-migration.md — that doc already leads with JwksUriSignerKeys keyed by agent URL and does not use BrandJsonJwksResolver.
    • Non-breaking: adds clarifying wording to a docstring and a doc example. No API surface, type, or behaviour changes. pytest, ruff, mypy required before PR; fast-subset only (no integration tier needed for a doc-only diff).

    Triaged by Claude Code. Session: https://claude.ai/code/session_016ztEeMWKstfGshSRaXGsJg


    Generated by Claude Code

  3. added and removed
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 6, 2026
  4. bokelley commented on Oct 6, 2026

    @bokelley
    ContributorAuthor

    A second misreading from the same integration: the seller expected keyid to carry the JWKS location (keyid="https://…/jwks.json#kid", as in some other HTTP Signatures ecosystems) and rejected our signatures because it was a bare kid. In the AdCP profile, keyid only matches a kid in the signer's published JWKS (security.mdx, profile table). The verifier finds that JWKS from the signing agent it already knows, through onboarding, a registry, or identity.brand_json_url discovery. The spec deliberately gives the signer no wire field to assert its own key location (see the agent_url derivation note), because trusting a URL from the request is self-attestation.

    Could the verifier docs say this next to the operator callout? Something like: keyid is a bare kid; never fetch a JWKS location taken from the request. Map the counterparty agent to its JWKS with JwksUriSignerKeys/StaticSignerKeys, or discover it with verify_from_agent_url.

  5. added
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 6, 2026
  6. bokelley commented on Oct 6, 2026

    @bokelley
    ContributorAuthor

    Triage (updated)

    Classification: docs
    Bucket(s): docs, signing
    Status: ready-to-implement
    Milestone: (omitted)

    What the experts said:

    • docs-expert: The keyid-as-JWKS-URL misreading is the same class of problem as the account.operator one — a developer mapping a familiar pattern from another HTTP Signatures ecosystem. The spec deliberately omits a wire field for the signer's JWKS location to prevent self-attestation; that design decision needs one sentence of explanation near both the operator callout and the keyid field.
    • dx-expert: Confirmed — the two misreadings occur at the same verifier integration point. Pairing both callouts in the same doc section (agent-resolution-33.md near the BrandJsonJwksResolver example) is more efficient than a separate note; a new integrator reading that block gets both corrections at once.

    My take: The new comment from @bokelley adds a third, well-specified doc edit to this issue's existing scope. It is non-breaking, directly addresses a confirmed real-world misreading, and naturally co-locates with the two changes already in the brief. Expanding the brief accordingly.

    Updated implementation scope (3 edits, all non-breaking doc additions):

    • src/adcp/signing/brand_jwks.py — BrandJsonJwksResolver docstring (~line 368): add to the opening paragraph: "Obtain brand_json_url from the signing agent's identity.brand_json_url capabilities field — this is the signing agent's operator (whoever publishes the brand.json). It is never account.operator from the inbound request; that field is the request's account-scoping seat holder, unrelated to key discovery."
    • docs/agent-resolution-33.md — BrandJsonJwksResolver constructor example (~line 22–26): add a comment or parenthetical clarifying that operator.example.com is the signing agent's operator domain (from identity.brand_json_url), not the request's account.operator. Immediately after, or in the same callout block, add: "keyid in the AdCP HTTP Signatures profile is a bare kid; it never carries a JWKS URL. The verifier locates the signer's JWKS through onboarding (JwksUriSignerKeys/StaticSignerKeys) or discovery (verify_from_agent_url) — never by fetching a URL from the request itself, which would be self-attestation."
    • docs/agent-resolution-33.md — verifier guidance block (lines 49–55, agent-first paths): ensure the verify_from_agent_url/async_resolve_agent block mentions that identity.brand_json_url is the JWKS source and is never derived from request fields. (Likely a one-sentence addition to the existing paragraph.)

    No changes to docs/request-signing-migration.md (already leads with agent-first paths). No API, type, or behavior changes. Required checks before PR: pytest (fast subset), ruff check ., black --check ..


    Triaged by Claude Code. Session: https://claude.ai/code/session_01UKt3gk9aK6zusQWosbhGCX


    Generated by Claude Code

  7. removed
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions