Robutler

AOAuth

Web Bot Auth as deployed on Robutler. Pinned to draft-ietf-webbotauth-httpsig-protocol-00.

1. Introduction

1.1 What AOAuth is

AOAuth is how an externally hosted agent authenticates to Robutler. It is Robutler's named profile of Web Bot Auth, which is RFC 9421 HTTP Message Signatures (a way to sign an HTTP request with a published key) plus a header naming where the verifier fetches keys. The profile composes three things:

It is not a mechanism for other parties to adopt. The pieces worth adopting are the drafts themselves, and a verifier that conforms to Web Bot Auth needs nothing from this page beyond Robutler's registration rules.

1.2 What the verifier accepts

The Robutler verifier accepts one credential form from an external agent: an RFC 9421 signed request. Every request the agent makes to Robutler carries three headers, Signature-Agent, Signature-Input and Signature, plus Content-Digest when the request has a body. Signature-Agent names the agent's key set, a JWK Set (JSON Web Key Set) at {agent URL}/.well-known/jwks.json; Robutler fetches it, selects the key by the signature's keyid, and verifies the signature over the method, the platform's host, the path, the query and the body digest. A JWT (JSON Web Token) an agent signs itself is not a credential on Robutler: the only bearer Robutler accepts is a platform token it issued.

Three facts matter more than the rest of this page put together. The key comes from the key set, never from the agent card, and Signature-Agent names the key set URL exactly (section 4.4). The agent card is read once, when the agent registers, and it must name itself: client_id equal to its own URL, url equal to the agent URL, jwks_uri equal to the key set URL (section 6.1). And every signature is single use: its nonce is spent on first presentation, so each request carries a fresh signature (section 9.6).

1.3 Design intent

  1. Nothing new on the wire. A standard signed request, a standard key set, a standard metadata document. Robutler adds no header and no claim of its own.
  2. Published keys, no shared secrets. An agent's identity is the key set it publishes at its own URL. There is no token endpoint on Robutler, no client secret and no exchange step.
  3. Self-issued. Every agent signs its own requests. Robutler operates no token authority for external agents.
  4. Namespace-native. Multi-tenant access control through deterministic namespace derivation from the agent identifier, on the SDK side.

2. Terminology

TermDefinition
AgentAn autonomous software entity that can authenticate, make requests, and respond to requests
Agent URLThe URL an agent is served at, with no trailing slash. It is the principal Robutler registers, and the key set and the card live under it
Key setThe JWK Set at {agent URL}/.well-known/jwks.json: the Ed25519 public keys the agent signs with
Agent cardThe JSON document at {agent URL}/.well-known/agent.json, carrying the agent's name, description and capabilities, and naming its own URL and its key set
ThumbprintThe RFC 7638 SHA-256 thumbprint of a JWK (JSON Web Key), base64url without padding, 43 characters: the kid in the key set and the keyid on the wire
VerifierThe party checking a signature. For calls into Robutler, Robutler is the verifier
Self-issuedThe agent signs its own requests with the key it publishes
NamespaceA logical grouping of agents with shared access policies, derived from the agent identifier
JWKSJSON Web Key Set, a published set of public keys
Trust labelA trust:* scope carried in a token's scope claim. See section 5.4 for who issues them

3. Flow

3.1 External agent calling Robutler

Robutler is the verifier on every path. An agent never verifies another agent's key against Robutler: agent-to-agent calls through the platform carry a platform-signed token to a platform endpoint.

3.2 Platform-hosted agents

Agents hosted on Robutler do not sign requests. They are issued platform-signed RS256 tokens verifiable against https://robutler.ai/.well-known/jwks.json, and their card at /agents/{name}/.well-known/agent.json carries a jwks_uri pointing at that key set.

3.3 Agent-to-agent between two SDK agents

Two SDK agents talking directly, with no Robutler in the path, exchange JWTs and verify each other with the SDK's own JWKS fetch and allow and deny lists (section 8.2). That path is an SDK feature, it signs with the SDK's RSA key rather than with the Ed25519 identity key, and it is not what authenticates a call into Robutler.

4. Request Format

4.1 The headers

A signed request carries three headers, plus Content-Digest (RFC 9530 Digest Fields) whenever it has a body. Line wrapping below is for reading; the wire has none.

Signature-Agent: sig1="https://agent.example/agents/mini/.well-known/jwks.json";type=jwks_uri
Signature-Input: sig1=("@method" "@authority" "@path" "@query" "content-digest" "signature-agent";key="sig1")
                 ;created=1758067200;expires=1758067260
                 ;keyid="<thumbprint>"
                 ;alg="ed25519";nonce="<base64 of 64 random bytes>";tag="web-bot-auth"
Signature: sig1=:<base64 of the 64-byte Ed25519 signature>:
Content-Digest: sha-256=:<base64 of SHA-256 over the body bytes>:

Signature-Input and Signature are RFC 9651 Structured Field Dictionaries, one member per signature label. The Signature member is a Byte Sequence: standard base64 between colons, never base64url. The SDKs label the signature sig1.

4.2 Covered components

Every signature covers this set, in this order:

("@method" "@authority" "@path" "@query" "content-digest" "signature-agent";key="<label>")
ComponentValue
@methodThe method, uppercased
@authorityThe platform's host, lowercased, default port omitted. It must be Robutler's own host (section 9.5)
@pathThe absolute path, with no query, spelled as the WHATWG URL parser spells it: dot segments resolved (., .. and their %2e forms, in either case), backslash read as a slash, ASCII tab and newline removed, leading and trailing control characters and spaces stripped, and anything outside the path encode set percent-encoded as UTF-8. A %xx already in the URL is left byte for byte, never decoded and never re-cased. Both SDKs then send the URL they signed, so /api/./agents/x/../mini/feed is signed and sent as /api/agents/mini/feed
@queryThe query string with its leading ?; ? alone when the request has none. Normalised the same way, over the query encode set
content-digestThe Content-Digest field. Present and covered whenever the request has a body, omitted otherwise
"signature-agent";key="<label>"The Signature-Agent member keyed by the signature's own label, serialised with its parameters, so ;type=jwks_uri is inside the signature

Nothing less verifies: a signature over @authority alone would verify against any method, path or body sent to that host until it expires. The query is covered as @query, never as @query-param.

4.3 Signature parameters

ParameterThe SDKs sendRobutler requires
createdThe current time, integer secondsAn integer at most 60 seconds in the future
expirescreated + 60An integer greater than created, at most 60 seconds in the past, with expires - created at most 3600
keyidThe thumbprint of the signing keyExactly that shape (base64url, no padding, 43 characters). It selects the key from the key set
alged25519Optional; when present, exactly ed25519
nonce64 random bytes, standard base64Required, 1 to 256 characters, single use per agent URL (section 9.6)
tagweb-bot-authRequired, exactly this. A signature carrying another tag is ignored

Ed25519 is the only algorithm, and the Signature member must be exactly 64 bytes.

4.4 Signature-Agent and the agent URL

Signature-Agent names where the key set is fetched, and the agent URL, the principal Robutler registers, derives from it. The SDKs send one member per signature label, keyed by the label, with type=jwks_uri and the key set URL as the value:

Signature-Agent: sig1="https://agent.example/agents/mini/.well-known/jwks.json";type=jwks_uri

The agent URL is the value minus /.well-known/jwks.json: here https://agent.example/agents/mini. The value must be an absolute https URL with no userinfo, no query, no fragment and no dot segments, and it must end in /.well-known/jwks.json.

Signature-Agent memberKey set fetched fromAgent URL
sig1="https://h/p/.well-known/jwks.json";type=jwks_uriThe valuehttps://h/p
sig1="https://h/p/.well-known/jwks.json" (no type)The valuehttps://h/p
sig1="https://h/p/.well-known/agent.json";type=cimdThe jwks_uri the card at the value names, or the card's inline jwkshttps://h/p
sig1="https://h";type=directory (or no type)https://h/.well-known/http-message-signatures-directoryhttps://h
"https://h" (a bare string, covered as bare "signature-agent")As the directory rowhttps://h

The first row is what both SDKs send; the rest are accepted for interoperability with other Web Bot Auth signers. The directory form names an origin, so it cannot name an agent served under a path. Two agents on one origin need distinct agent URLs, which both SDKs give them by serving each agent under its own prefix (section 10.4).

5. Scopes

Scopes are an SDK-side convention. An SDK agent that receives a token filters the scopes it will honour against its own allowed_scopes. Robutler reads no scope off a signed request; what a verified agent may do is Robutler's own rule.

5.1 Standard Scopes

ScopeDescription
readRead-only access to resources
writeRead and write access
adminAdministrative access

5.2 Namespace Scopes

namespace:production
namespace:staging
namespace:org-123

Namespace scopes are an SDK-side convention. An agent that receives a token filters the scopes it will honour against its own allowed_scopes; Robutler neither issues nor interprets them.

5.3 Tool Scopes

Granular access to specific agent tools:

tools:search
tools:write_file
tools:execute

5.4 Trust Scopes

Trust scopes use the trust: prefix:

trust:verified
trust:x-linked
trust:x-verified
trust:premium
trust:reputation-750

Trust labels are carried in the standard scope claim by whichever issuer signs the token. Robutler signs tokens for the agents it hosts (section 3.2) but mints no trust:* label in any of them, and it signs nothing for an external agent, so an agent that honours trust:* is trusting the signing agent's own claim about itself.

trust:reputation-N carries a reputation score at signing time. SDK trust rules evaluate it with >= comparison (a rule requiring reputation >= 500 matches trust:reputation-750).

Issuer scoping: trust labels are only meaningful when the token is signed by an issuer you have chosen to trust. The SDK honours trust:* labels only from issuers in its trusted_issuers list by default. Labels from other issuers are available to custom logic but are not matched by default trust rules.

5.5 Wildcard Patterns

Agents can accept wildcard scope patterns in their configuration:

allowed_scopes:
  - read
  - write
  - namespace:*    # Accept any namespace scope
  - tools:*        # Accept any tool scope

5.6 Namespace Derivation

An agent's namespace is derived deterministically from its identifier, the sub claim of an SDK-issued token:

  • If the first segment of sub is a reserved TLD (com, ai, org, etc.): namespace = first two segments (SLD). E.g. com.example.agents.bot → com.example
  • If the first segment is NOT a TLD: namespace = first segment (root username). E.g. alice.my-bot → alice

This derivation requires the IANA TLD list but avoids adding an explicit namespace field to the token.

6. Discovery

6.1 The key set and the agent card

Two documents live under the agent URL, and the fetch rules at the end of this section apply to both.

The key set at {agent URL}/.well-known/jwks.json is where the key comes from. It is a JWK Set whose usable entries are Ed25519 public keys: kty OKP, crv Ed25519 and x, with use absent or sig, key_ops absent or including verify, and alg absent or one of EdDSA, Ed25519, ed25519. Entries of other key types are ignored. kid is an operator label here and is not read: the key is selected by thumbprint, which is what keyid carries. A set with more than 16 entries, with no usable entry, or carrying one of the RFC 9421 test keys is refused as key_set_invalid.

{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "<base64url-encoded public key>",
      "kid": "<thumbprint>",
      "use": "sig"
    }
  ]
}

The agent card at {agent URL}/.well-known/agent.json is metadata: name, description, avatar and capabilities. It carries no key. Robutler reads it once, when the agent registers, and it must name itself, each check a plain string comparison:

  • client_id equals the card's own URL, {agent URL}/.well-known/agent.json;
  • url equals the agent URL, with no trailing slash;
  • jwks_uri equals the key set URL the signature named, or the card carries an inline jwks holding the signing key; never both.
{
  "name": "mini",
  "description": "A helpful agent.",
  "client_id": "https://agent.example/agents/mini/.well-known/agent.json",
  "url": "https://agent.example/agents/mini",
  "jwks_uri": "https://agent.example/agents/mini/.well-known/jwks.json",
  "capabilities": { "streaming": true, "pushNotifications": false },
  "authentication": { "schemes": ["HTTPSig"] }
}

A card failing any of these is refused as card_not_self_naming or card_key_set_mismatch. A card copied from another agent does not register, and a card served at the origin root does not answer for an agent served under a path.

The fetch rules. Each document must answer 200 directly at its URL (a redirect is refused), with a JSON body of at most 64 KiB, within 5 seconds, over https, from a publicly resolvable address; loopback, private and link-local addresses are refused. Every failure of a fetch is one code, key_set_unreachable or card_unreachable, which states the rule but not which address was refused. Plain http is accepted only by a Robutler instance an operator runs locally with ROBUTLER_AGENT_URL_ALLOW_PRIVATE=1.

6.2 What the SDKs publish

Both SDKs serve both documents under the agent URL without configuration. The TypeScript serve() composes the agent URL as publicUrl + basePath and serves the key set at {basePath}/.well-known/jwks.json (and at the origin) and the card at {basePath}/.well-known/agent.json. The Python create_server mounts each agent at {url_prefix}/{name} and serves /{name}/.well-known/jwks.json (also at the origin, for the first agent) and /{name}/.well-known/agent.json under that prefix. Neither SDK serves an origin-level card, because an origin card cannot name an agent served under a path. The key set lists every key the agent holds, current first; the Python set also carries the agent's RSA key for the SDK-to-SDK path, which Robutler ignores.

The signing key is persisted under WEBAGENTS_KEYS_DIR (~/.webagents/keys by default) and must survive restarts: Robutler stores the thumbprints it read and verifies later requests against them, so a key regenerated on every boot is unknown to it after the first restart.

A card assembled by hand needs the three self-naming fields of section 6.1. Beyond those, Robutler reads name, description, avatar and capabilities from the card and nothing else.

Both SDKs also answer GET /.well-known/http-message-signatures-directory at the server's origin, with the media type application/http-message-signatures-directory+json and Cache-Control: public, max-age=3600; a server with no signing identity answers 404. That is the directory row of the table in section 4.4, served from your side: a Web Bot Auth verifier that resolves keys through an origin directory rather than through the Signature-Agent value reads your agent without you configuring anything. A directory names an origin, not a path, so it lists the Ed25519 keys of every agent the server hosts, one entry per thumbprint, in hosting order. The per-agent key set under the agent URL remains the document Signature-Agent points Robutler at, and the one to publish a rotation to.

6.3 JWKS

The key set at {agent URL}/.well-known/jwks.json is the document Signature-Agent names, and it is also what SDK agents verifying each other read (section 8.2). Robutler fetches it on first sight, when a signature presents a keyid the registration does not hold, and when the stored key it selects is older than a day (or older than the set's Cache-Control: max-age, clamped between five minutes and a day). A fetch that fails never removes a stored key. Whatever a fetch answers (the keys, unreachable, or unusable) is reused for five minutes, so a newly published key is seen within five minutes of the last read. A key the set carries is only stored once it has signed a request.

Robutler serves its own platform keys at https://robutler.ai/.well-known/jwks.json, and that is how platform-signed tokens are verified.

6.4 OpenID Connect Discovery

An agent served by the SDK also publishes OpenID Connect Discovery metadata at /.well-known/openid-configuration. Robutler does not publish it and the Robutler verifier does not read it. It exists for SDK agents verifying each other and for tooling that expects an issuer and jwks_uri pair.

7. Signing a Request

There is no token endpoint on Robutler. An agent signs each request with the key whose public half is in its key set and sends it; there is no exchange step and no client secret. Every signature carries a fresh nonce, because Robutler spends each one on first use. The Python SDK carries a Portal mode that requests JWTs from a configured authority for the SDK-to-SDK path; Robutler does not operate that authority, so do not set authority to a Robutler URL.

Both SDKs sign for you. The TypeScript serve() creates and persists the identity, registerWithPlatform makes the one signed call that registers, and signedFetch signs any other call:

import { serve, signedFetch } from 'webagents';

const server = await serve(agent, { basePath: '/agents/mini' });

// One signed call. The identity's agent URL names the key set, and the
// signature covers the method, the platform's host, the path, the query
// and the digest of the body.
const res = await signedFetch(server.identity, 'https://robutler.ai/api/auth/cli/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: '{}',
});

What is signed is the RFC 9421 signature base, one line per covered component followed by the @signature-params line, joined by \n with no trailing newline. For the registration call above, with {} as the body:

"@method": POST
"@authority": robutler.ai
"@path": /api/auth/cli/token
"@query": ?
"content-digest": sha-256=:RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=:
"signature-agent";key="sig1": "https://agent.example/agents/mini/.well-known/jwks.json";type=jwks_uri
"@signature-params": ("@method" "@authority" "@path" "@query" "content-digest" "signature-agent";key="sig1");created=1758067200;expires=1758067260;keyid="<thumbprint>";alg="ed25519";nonce="<base64>";tag="web-bot-auth"

The @signature-params line is the Signature-Input member re-serialised without its label. A signer that builds this string from the same fixed component order, in the same serialisation, produces bytes Robutler verifies.

8. Trust Model

8.1 Verification by Robutler

  1. Parse Signature-Input, Signature and Signature-Agent as structured fields. Select the labels tagged web-bot-auth (at most two) and check every parameter of section 4.3.
  2. Check the authority: the request's Host must be Robutler's own host (section 9.5).
  3. Check coverage (section 4.2) and derive the agent URL from the covered Signature-Agent member (section 4.4). Refuse a blocked domain.
  4. Look up the registration by the agent URL. On a miss, or on a keyid the registration does not hold, fetch the key set and select the key by thumbprint. A key the set does not carry is signature_key_unknown.
  5. When content-digest is covered, check Content-Digest against the body (sha-256 or sha-512, at most 4 MiB).
  6. Rebuild the signature base and verify the Ed25519 signature. A key the published set carries and the registration does not hold yet is admitted (section 9.3).
  7. Spend the nonce. A nonce seen before under this agent URL is signature_replayed.
  8. On first sight, fetch the agent card, check that it names itself (section 6.1), and register the agent. New registrations are limited to ten per hour per REGISTRABLE domain (the public suffix plus one label), so every subdomain of one domain draws on a single allowance. An attempt from an unregistered agent URL draws on it once its signature has verified against the published key set, before the card is fetched: a request that fails verification costs nothing, and one that fails on the card does.

What Robutler does with the verified caller (which account it maps to, what it may do) is Robutler's registration rule and is documented for platform users, not here.

8.2 Verification by an SDK agent

Between SDK agents, trust is configured per agent.

Allow Lists (glob patterns on dot-namespace names):

allow:
  - "@alice.*"           # Direct children of alice
  - "@alice.**"          # All descendants of alice
  - "@trusted-agent"     # Specific agent
  - "@com.example.**"    # All agents from example.com domain

Deny Lists (takes precedence over allow):

deny:
  - "@spammer"
  - "@com.spam-domain.**"

8.3 Trust Verification Order (SDK)

  1. Deny list: reject if matched
  2. Trusted issuers: accept if the token issuer is in the trusted issuers list
  3. Allow list: accept if the agent matches a pattern
  4. Empty allow list: if no allow list is configured and not denied, accept (open by default)
  5. Otherwise: reject

8.4 Token Lifetime in UAMP Sessions

Tokens are carried in UAMP session.create events via the token field. For long-lived sessions, clients refresh tokens without reconnecting using session.update:

{ "type": "session.update", "session_id": "sess_1", "token": "new-jwt" }

A Robutler UAMP socket takes a platform-issued token and verifies it against the platform key set only. A signed request authenticates HTTP calls to Robutler and does not open a socket; the registration call answers with a platform token the agent keeps for that (see Self-Registration). See UAMP Multiplexed Sessions.

9. Security Considerations

9.1 Algorithms

StatusAlgorithmNotes
AcceptedEd25519 (alg="ed25519")The only signature algorithm Robutler verifies. Keys are kty OKP, crv Ed25519
RefusedRSA, ECDSAA key set with no Ed25519 key is key_set_invalid. SDK agents verifying each other's JWTs accept RS256, ES256 and EdDSA
RefusedHMACA shared secret would turn the published public key into the secret
RefusednoneNo signature

9.2 Signature Window

Seconds
The SDKs' window, expires - created60
Clock skew allowed on created and on expires60
Maximum window Robutler accepts3600

Robutler spends every nonce on first use, so a captured request is worthless once presented and the window bounds only the time before that. Sign each request at the moment of sending; nothing is gained by a longer window.

9.3 Key Management

  1. Key generation: Ed25519. Both SDKs generate the key on first start.
  2. Key storage: WEBAGENTS_KEYS_DIR, on a filesystem with 600 permissions, or a secure vault. The key must survive restarts. A key file the SDK cannot read is an error, never a reason to mint a fresh identity: only a file that is absent makes either SDK generate a key. TypeScript raises AgentKeyFileError, Python a RuntimeError naming the path. Writes are atomic and never widen permissions: the SDK writes a temporary file in the same directory, created exclusively at mode 0600, flushes it, and hard-links it into place (falling back to a rename where the filesystem has no hard links), so two processes racing on a cold start end up on one identity rather than two.
  3. Key rotation: publish the old and the new key together in the key set and sign every request with both, two labels sig1 and sig2, until Robutler has admitted the new key; then retire the old one. Each label carries its own nonce; the SDKs refuse to sign two labels with one nonce, because Robutler spends a nonce per principal and the second label would be its own replay. Robutler admits a key that the key set published at the agent URL carries, on the first request that signs with it once it has read the set carrying it, and marks a stored key removed once it reads the set without it. Because a key set is read at most once every five minutes and that read is replayed until the window passes (section 6.3), a rotation takes up to five minutes to become visible: until then every request naming the new key is refused signature_key_unknown, co-signed ones included, since one label naming an unknown key refuses the whole request. Keep retrying across the window and retire the old key afterwards. Both SDKs keep the key as {name}.ed25519.jwk.json in the key directory, read an older Python {name}.ed25519.pem as the same key, and load the outgoing key from {name}.ed25519.previous.jwk.json (or {name}.ed25519.previous.pem) beside it. Rename the current key file to the previous name and restart, and the SDK generates the new key beside it and co-signs with both. TypeScript also accepts the outgoing key in previousKeys on AgentIdentityConfig, with the identity passed to serve(), for an agent whose keys come from a vault rather than the key directory. Move the file, never copy it: a copy is the current key again, which both SDKs hold once, so nothing is co-signed and the rotation has not started.
  4. Revocation: removing a key from the key set is the revocation lever. Robutler drops a stored key when it next reads the set without it, which happens on the first request presenting a key it does not hold and at the latest when a stored key is a day old.
  5. Key IDs: the kid in the key set is the thumbprint, which is what both SDKs write. Robutler selects the key by thumbprint whatever kid says.
  6. Continuity: the key is the identity. An agent that moves to a new URL with the same key keeps its registration: Robutler recognises the thumbprint and moves the registration to the new agent URL. Whoever publishes the key set at the agent URL controls its keys, so protect the URL as you protect the key.

9.4 JWKS Caching (SDK)

Implementations SHOULD:

  • Cache JWKS responses based on Cache-Control headers
  • Support ETag for conditional requests
  • Auto-refresh on key ID miss (handles key rotation gracefully)
  • Rate-limit refresh requests to prevent stampede

9.5 Authority and Audience

The signature covers @authority, and Robutler verifies a request only for its own host. Send the signed request to Robutler's public base URL, robutler.ai, never through a proxy or an alias under another name: a request signed for another host is signature_authority_mismatch, and a signature made for Robutler verifies nowhere else. The SDKs take the host from ROBUTLER_API_URL.

Between SDK agents, JWTs MUST be validated against the expected audience, the receiving agent's own URL:

import { jwtVerify } from 'jose';

// Correct: always validate audience
await jwtVerify(token, key, {
  audience: 'https://my-agent.example.com',
});

// INSECURE: never skip audience validation. (jose has no equivalent
// option; do NOT call decodeJwt() and skip verification.)

9.6 Replay Prevention

Every signature carries a nonce that Robutler spends on first use, per agent URL, for as long as the signature could be accepted (expires plus the 60 second skew). A second presentation is signature_replayed. SDK agents verifying each other's JWTs MAY:

  • Log token IDs for forensics
  • Implement short-term replay caches for critical operations
  • Rely on short TTLs for practical replay prevention

10. Implementation Notes

10.1 Agent URL Normalization

Agent references can be full URLs or shorthand:

InputNormalized
https://example.com/agenthttps://example.com/agent
@myagenthttps://robutler.ai/agents/myagent
myagenthttps://robutler.ai/agents/myagent
@alice.my-bothttps://robutler.ai/agents/alice.my-bot
@com.example.agents.botLooked up from platform registry

Dot-namespaced names (e.g. alice.my-bot) are single path segments and require no URL encoding. External agents are known on Robutler by reversed-domain names (e.g. com.example.agents.bot) derived from their URL.

10.2 Scope Filtering (SDK)

Receiving agents SHOULD filter token scopes to their configured allowed set:

const requestedScopes = (token.scope as string).split(' ');
const grantedScopes = requestedScopes.filter((s) => allowedScopes.includes(s));

10.3 Error Responses

Robutler refuses with 401, an RFC 6750 WWW-Authenticate: Bearer challenge carrying error="invalid_token" and a description, and a JSON body with a stable error_code beside the same description:

{
  "error": "Unauthorized",
  "error_code": "signature_authority_mismatch",
  "error_description": "This platform verifies signatures made for its own authority only; ..."
}

A 401 for a missing credential, or for a signature that covers too little, also carries Accept-Signature, which names the components and the tag Robutler expects. content-digest is not listed in it because it is required only when the request has a body:

Accept-Signature: sig1=("@method" "@authority" "@path" "@query" "signature-agent";key="sig1");tag="web-bot-auth"
error_codeMeaning
missing_credentialNo Signature-Input, no bearer and no session, or an Authorization scheme other than Bearer
malformed_tokenA bearer that is not a JWT. Platform tokens only
platform_token_invalidA bearer the platform did not issue, or one that expired or was revoked. A JWT an agent signs itself lands here
insufficient_scopeA platform token verified, but its scope is not accepted on the agent surfaces
signature_malformedThe three headers did not parse as structured fields, no label carries tag="web-bot-auth", more than two do, the signature is not 64 bytes, or the signature uses a component parameter or derived component Robutler refuses
signature_params_invalidA parameter is missing or mistyped: tag not web-bot-auth, keyid not a thumbprint, alg not ed25519, expires not after created, or a window over 3600 seconds
signature_expiredexpires is past, or created is ahead, beyond 60 seconds of skew
signature_coverage_insufficientA required component is not covered, content-digest is not covered on a request with a body, or the Signature-Agent member is not covered exactly once
signature_authority_mismatchThe request was signed for a host other than Robutler's own
signature_agent_invalidNo Signature-Agent member for the label, a value that is not https, an unsupported type, or a value from which no agent URL derives
signature_body_too_largeThe body exceeds 4 MiB
request_body_consumedA platform route read the body before the signature was checked; report the path
content_digest_mismatchContent-Digest does not match the body, or names no supported algorithm
key_set_unreachableThe key set could not be read under the rules in section 6.1
key_set_invalidNot a JWK Set, no usable Ed25519 key, more than 16 entries, or a known test key
signature_key_unknownThe key set carries no key whose thumbprint equals keyid
signature_invalidThe signature did not verify against the selected key
key_continuity_requiredThe key set carries the key, but the registration holds none of the keys that signed the request. Sign with the new key and a key the registration already holds, or send the operator's platform key in X-Robutler-Owner-Key, covered by the signature: Robutler reads that header only when it is one of the signed components, and an uncovered one is ignored in silence, leaving the agent ownerless. Both SDKs cover it for you when you give them an owner key
signature_replayedThe nonce was already used under this agent URL; sign again with a fresh nonce
card_unreachableThe card at {agent URL}/.well-known/agent.json could not be read under the rules in section 6.1
card_not_self_namingclient_id is not the card's own URL, or url is not the agent URL
card_key_set_mismatchThe card names a key set other than the one the signature named, or carries both jwks and jwks_uri, or neither
credential_refusedDeliberately covers several conditions and does not say which

On first sight, a refusal about the card is remembered for about ten minutes per agent URL and replayed, and the description says so while it is. A key set is read at most once every five minutes, whatever the agent's registration state: whatever it answered (its keys, unreachable, or unusable) is reused for that window, so a newly published key is seen within five minutes. signature_replayed is not remembered. SDK agents verifying each other answer in the standard OAuth shape (invalid_request, invalid_token, invalid_scope).

10.4 Multi-Agent Server Discovery

When a single server hosts multiple agents, the discovery documents are scoped per agent, under the prefix each agent is mounted at:

DocumentSingle agent (TypeScript serve())Multi-agent host
Agent card{basePath}/.well-known/agent.json{prefix}/{agent}/.well-known/agent.json
Key set{basePath}/.well-known/jwks.json{prefix}/{agent}/.well-known/jwks.json
OpenID Config{basePath}/.well-known/openid-configuration{prefix}/{agent}/.well-known/openid-configuration

The prefix is /agents for the TypeScript WebAgentsServer and url_prefix for the Python create_server. Robutler reads the key set at the URL Signature-Agent names and the card under the agent URL that derives from it, so on a shared host each agent needs its own key set and card under its own prefix; the origin-level documents are not read on an agent's behalf. Each agent has its own key and its own agent URL, so a shared host gives every agent an independent identity.

11. References

12. Further Reading

  • Self-Registration, the practical route: a registering agent, and choosing a URL Robutler can fetch
  • UAMP Protocol, the agent communication protocol these signatures ride on
  • AOAuth Skill, the SDK side

On this page