Skip to content

RFC-015 draft: delegated access - #33

Merged
antra-tess merged 5 commits into
mainfrom
rfc-015-delegated-access
Oct 25, 2026
Merged

antra-tess merged 5 commits into
mainfrom
rfc-015-delegated-access

Conversation

@antra-tess

Copy link
Copy Markdown
Contributor

Adds RFC-015, Delegated Access: a way for a server to ask its host for a short-lived credential for a named audience, acting as the connection's principal.

SPEC §19.6.1 says today that there is no host→server credential "inversion". Servers that act for the agent toward another service (a proxy, a server that mounts remote services, a server that is itself a host) therefore carry static credentials in their own config. Those credentials are long-lived, give one identity to every principal the server serves, and expire silently.

What it adds

  • access/token (Server → Host, Request): {audience, endpoint?, purpose?, fresh?} → {scheme: "bearer", credential, expiresAt, principal?}.
  • One capability path, access.token. It is denied by default and never granted by a wildcard.
  • Advertisement experimental.mcpl.access: {token: true} on both sides, and error -32040 Access refused.

Design choices

  • The grant is the trust decision. There is no per-audience allowlist. What the principal can obtain from its identity provider is the outer bound.
  • Short-lived. Credentials expire in about 15 minutes, are asked for again at every dial, are cached per configured server, and never reach model context.
  • Chaining is allowed: a server that is a host for an upstream may forward a request upward under its own grant.

Revision 2 includes what the first host implementation found. Related: RFC-014 (Tool Presence, Toolsets and Surfaces), whose out-of-host surface hosts use this.

Implementations: the mcpl-cc-bridge host (PR linked below); an archipelago-home change that lets POST /token ask for a shorter lifetime.

🤖 Generated with Claude Code

…ementation)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antra-tess

Copy link
Copy Markdown
Contributor Author

Related: RFC-014 #34 · host implementation anima-research/mcpl-cc-bridge#9 · identity-provider change anima-research/archipelago-home#2

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review

Recommendation: fix, then merge. 1 high severity finding should be fixed first.

Reviewed commit 76d785c with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.

Does this change belong in the project?

It fits the project. This draft RFC fits. It fills a gap the SPEC names itself: host→server credentials, the "inversion" in §19.6.1, which §18.3 leaves to "a future revision".

5 reasons with sources

It arrives the way this project takes protocol changes, as a numbered RFC file at the root that grants authority through the existing §5.4 capability grants.

  • The SPEC sets aside credential policy for a later identity mechanism, and this RFC supplies the host→server half of it. §18.3 says "Credential policy belongs to the host and to whatever identity mechanism a future revision defines." §19.6.1 says there is no inversion only because "the server holds no host credential under the current connection model". The RFC changes that model and lists exactly those passages as amended. (SPEC.md §18.3 Credentials)
  • It takes the form the project uses for protocol changes: a Draft RFC at the root that names the SPEC sections it amends. The README says SPEC.md is the authoritative integrated text and that "RFC files preserve proposal and review history". RFC-001 to RFC-013 sit at the repository root, several of them still drafts. This PR adds only RFC-015-delegated-access.md and does not edit SPEC.md directly. (README.md Status)
  • Authority goes through the existing capability-grant model, not a new parallel mechanism. It adds one path, access.token, to the §6.2 vocabulary. That path is denied by default and checked against the effective grant with the existing -32002 error. The wildcard exception it adds to §5.4's matching rule is a design choice the maintainers should confirm when the SPEC is amended. (README.md Features (Capability Grants); SPEC.md §5.4, §6.2)
  • Defining "the connection's principal" moves into identity, a topic the SPEC parked rather than ruled out, so that boundary needs a maintainer's decision. §18.8 lists "Identity — principals, enrollment, succession" as "deferred, so parked rather than lost". The RFC leaves minting to the host and the identity provider and defines only the request on the wire, which keeps it close to the §18.3 framing. Still, accepting it starts committing MCPL to a notion of a principal. (SPEC.md §18.8 Out of scope)
  • It is one coherent proposal, though it refers to RFC-014, which is not in the repository. The diff is one self-contained RFC document. The RFC-014 reference is only motivation (surfaces), and the mechanism itself does not depend on it. (README.md Status table)

Findings

10 findings of medium or high severity, see the inline comments:

6 more findings, less severe
  • Low severity, documentation, RFC-015-delegated-access.md:169-171. reason is used as a fixed value but has no defined vocabulary. Line 171 says to answer with reason "no identity", as if data.reason were a machine-readable value. §3.3 and §11 type it as a free string, and §6.4 has servers repeat it to users. Servers cannot reliably tell "no identity" apart from a provider refusal or a host policy refusal. Either define a small registry of reasons, or say that reason is free text and add a separate classification field.
  • Low severity, documentation, RFC-015-delegated-access.md:302-314. §10 amendment list leaves out Appendix B, Changelog and §6.5, which the header promises. The header (line 11) says the RFC amends Appendix B and the Changelog, but §10 gives no text for either. Appendix B.2's uses enum (SPEC.md ~3235) must gain access.token, or schema validators will reject it. SPEC §6.5 says server-initiated messages MUST include featureSet; access/token is a server-initiated request without one, and §6.5 is not amended either. Integrators following §10 will leave the SPEC inconsistent.
  • Low severity, test coverage, RFC-015-delegated-access.md:349-350. Vector 6 cannot check server log lines from a recorded session. The vector checks for credentials in the server's "log line" by a syntactic scan over a recorded session. A recorded protocol session contains wire messages, not the server's local logs, so this part can never fail. Either name the artifact to scan, such as captured server stderr, or drop the claim.
  • Low severity, documentation, RFC-015-delegated-access.md:286-289. Wrong cross-reference: open questions are §13, not §12. "left to a later revision (RFC §12)" points to the conformance vectors. Actor claims and proof-of-possession are listed in RFC §13, Open questions.
  • Low severity, documentation, RFC-015-delegated-access.md:363. Open question 3 contradicts §4.5 on how a server learns of revocation. It says a server learns of revocation only when its next request fails. §4.5 calls revocation a §6.7 grant reduction, and SPEC §6.7 requires grant reductions to be sent as a featureSets/update Request. So the server is told directly, and §4.5's "server that loses the grant SHOULD discard" depends on that.
  • Low severity, documentation, RFC-015-delegated-access.md:11. Depends on RFC-014, which is not in the repository. The header and §2 (line 76) cite RFC-014 (Surfaces) as the consumer and motivation. No RFC-014 file exists in the repository, so readers cannot check the use case that justifies the design, such as one server owning many engagements.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +234 to +242
## 7. Chains

A server that is itself the host of an upstream server (it dialed it) may receive
`access/token` from that upstream. It **MAY** satisfy the request by making its own
`access/token` request to its host, **only if** its own effective grant includes
`access.token` **and** it granted `access.token` to the upstream. The principal does not
change along the chain; each hop applies its own grant; the credential returns down the same
path. A hop **MUST NOT** answer from a credential it obtained for a different request's
audience.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High severity. Chaining cannot happen: a forwarding hop is forbidden to advertise access.

RFC §4.2 (line 164) forbids a server from sending access/token to a host that did not advertise access. Line 169 forbids a host with "no identity to mint from" from advertising access. A chaining hop holds no identity of its own; it only forwards upward under §7. Read literally, it must not advertise access to its upstream, so the upstream may never send the request §7 says the hop MAY satisfy. Chaining is one of the three design choices the PR lists. §4.2 needs a rule that lets a hop whose own grant includes access.token advertise access as a forwarder.

Comment thread RFC-015-delegated-access.md Outdated
(syntactic scan over a recorded session).
7. Two principals on one server: each request is answered with that principal's credential.
8. Grant revoked → next request `-32002`.
9. Chain: a hop without the grant refuses its upstream's request with `-32002`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Vector 9 returns -32002 to an upstream that does hold the grant.

§3.3 defines -32002 as "access.token is not in the effective grant" of the requester, with data.capability = "access.token". In vector 9, the hop granted access.token to the upstream; it is the hop's own grant from its host that is missing. Answering -32002 tells the upstream it lacks a capability it actually holds. Under SPEC §6.6 and RFC-013's mapping, that is a not_permitted authority verdict on the upstream's own grant. This case is the hop being unable to mint, which §3.3 assigns to -32040. Vector 9 and §7 should say -32040 with a reason.

Comment on lines +141 to +143
explicitly. **No default policy, preset or wildcard grants it**: although `access.*` would
match it under §5.4's one-segment rule, a host MUST NOT let a wildcard confer this path, and
SHOULD warn when an operator's wildcard would otherwise have matched it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, documentation. Wildcard exception overrides SPEC §5.4 but §5.4 is not listed as amended.

SPEC §5.4 requires a generic recursive walk in which * matches exactly one segment, so access.* and *.token grant access.token. RFC §4.1 adds a path-specific exception: no wildcard may confer this path. Neither the header's amendment list (lines 7-11) nor §10 (lines 302-314) names §5.4. Implementers working from the integrated SPEC will grant access.token through access.*. That defeats the deny-by-default rule revision 2 says it fixed.

Comment on lines +321 to +326
interface AccessTokenParams {
audience: string; // 1..128
endpoint?: string;
purpose?: string; // ≤ 256
fresh?: boolean;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Schema omits _meta and the endpoint bound that §3.1 requires.

§3.1 allows an optional _meta object and requires every other member to be rejected with -32602. AccessTokenParams in §11 has no _meta. An implementation that validates strictly against this schema will reject MCP's _meta, which is the exact bug revision 2 says it fixed. The schema also leaves out the 2048-code-point bound and absolute-URI requirement on endpoint, and the minimum length of 1 on audience appears only in a comment.

Comment thread RFC-015-delegated-access.md Outdated
|---|---|---|---|
| `scheme` | `string` | Yes | `"bearer"` in this revision |
| `credential` | `string` | Yes | The credential, opaque to MCPL |
| `expiresAt` | `string` (ISO 8601) | Yes | When it stops being valid. A host that cannot state this MUST NOT answer |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. expiresAt as bare ISO 8601 allows timestamps with no time zone.

ISO 8601 allows local date-times with no UTC offset (e.g. 2026-10-25T04:15:00) and many other forms such as week dates. A server enforcing "MUST NOT use a credential after its expiresAt" (line 207) cannot tell when a credential without an offset expires. Off by the zone difference, it may keep using an expired credential or drop a valid one. The field should require an RFC 3339 / UTC Z timestamp. The same applies to the schema comment at line 330.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +196 to +199
Revoking `access.token` (a grant reduction, §6.7) takes effect at once for new requests.
Credentials already issued remain valid until `expiresAt` — they are bearer credentials,
which is why lifetimes are short (RFC §5). A server that loses the grant **SHOULD** discard
cached credentials.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Discarding credentials after revocation is only a SHOULD.

After the host revokes access.token through a §6.7 grant reduction, the server is only told it SHOULD discard cached credentials. A conforming server may keep acting as the principal for the remaining lifetime (up to 15 minutes, or longer where the provider cannot shorten lifetimes), after the operator has withdrawn that authority. SPEC §6.7 says reductions must be respected. Discarding is entirely under the server's control, so this should be a MUST.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +259 to +260
The host **SHOULD** record each request: server, audience, endpoint, purpose, outcome,
`expiresAt`. It **MUST NOT** record the credential.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Audit logs record endpoint verbatim, which can contain secrets.

Hosts SHOULD record the server-supplied endpoint in the audit log. SPEC §4.2.6 notes that existing deployments put a token in the upgrade URL's query, so endpoint may well be something like wss://svc/mcpl?token=…. Anyone with access to the audit log can then read that downstream service's static credential, which they should not have. The RFC should require stripping userinfo, query and fragment from endpoint before it is recorded, or forbid credentials in endpoint.

Comment on lines +175 to +192
A server holding `access.token` may obtain, **for the connection's principal**, a credential
for **any audience the principal can obtain**. The protocol defines no narrowing. A host MAY
refuse particular audiences by its own policy, and says so with `-32040`; it is not required
to keep, and this RFC does not define, a list.

The rationale is the threat model. A server trusted to act as the agent at one service is
already trusted with the agent's actions, data and spend there; a list does not protect
against a compromised server, which would simply request what the list allows, and it costs
an operator a piece of configuration per service per server, out of date the day a service is
added. The bound that matters — what the agent may do at all — is enforced where it already
lives, by the identity provider.

### 4.4 One principal per connection

The host mints only for the principal of the connection the request arrived on. A server
serving several principals over several connections receives each principal's credential
over that principal's connection and **MUST NOT** use a credential obtained on one
connection on behalf of another principal.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, documentation. "The connection's principal" is never defined, and SPEC puts identity out of scope.

§4.3, §4.4, §5 and §8.1 all depend on "the principal of the connection". The SPEC has no such concept: §18.8 explicitly defers identity (principals, enrollment). The RFC never says how a host decides a connection's principal, or how one host serves several principals to one server (vector 7). Two hosts could reasonably disagree, and that would break the one-principal-per-connection guarantee.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +340 to +353
## 12. Conformance vectors

Freeze before Draft→Accepted (RFC-005 §11):

1. Request without the grant → `-32002`, `data.capability = "access.token"`.
2. Request to a host that did not advertise `access` → the server does not send it.
3. Granted request → result with `scheme`, `credential`, `expiresAt` in the future.
4. Identity provider refuses → `-32040` with `audience` and `reason`; no credential.
5. Unknown params member → `-32602`.
6. A credential never appears in any `push/event`, tool result, or log line of the server
(syntactic scan over a recorded session).
7. Two principals on one server: each request is answered with that principal's credential.
8. Grant revoked → next request `-32002`.
9. Chain: a hop without the grant refuses its upstream's request with `-32002`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, test coverage. No conformance vector covers the wildcard exclusion.

Revision 2's main security fix is that no wildcard (access.*, *.token) confers access.token (RFC §4.1). No vector checks that a host given only access.* answers -32002. That is the kind of divergence SPEC §5.4 notes between implementations, and it fails open.

Comment on lines +124 to +126
**Order of checks.** The grant first (`-32002`), then the parameters (`-32602`), then minting
(`-32040`): a server without the grant learns nothing about what a well-formed request would
have produced.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, test coverage. Order of checks, _meta and the no-identity case have no conformance vectors.

Revision 2 fixes the check order: grant, then params, then mint. No vector sends a malformed request without the grant and expects -32002 rather than -32602. There is also no vector for _meta being accepted while other unknown members are rejected. And none for a granted host with no identity answering -32040 with reason "no identity" (line 171). All three are revision-2 changes that hosts could get wrong.

…339, discard on revocation, audit, principal, schema, vectors)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review

Recommendation: merge and fast-follow. 9 medium severity findings can be fixed after the merge.

Reviewed commit bc10701 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.

Does this change belong in the project?

It fits the project. This draft RFC belongs here. The project grows by RFC files, and the SPEC itself names a future identity mechanism as the place for host-side credential policy.

5 reasons with sources

The RFC extends the existing grant and feature-set model and does not set up a parallel one.

  • The repository develops its protocol through numbered RFC drafts, and this change adds one in the same form. The README says SPEC.md is the authoritative integrated text and that RFC files preserve proposal and review history. The repo root already holds RFC-001 to RFC-008 and RFC-013. The new file is a draft with a status, the SPEC version it targets, a spec-amendment list and conformance vectors, the same shape as RFC-005. (README.md, Status)
  • The SPEC leaves room for a later revision to define host-side credential policy, and this RFC fills that space. §18.3 says credential policy 'belongs to the host and to whatever identity mechanism a future revision defines'. §19.6.1 says there is no host→server credential inversion only because the connection model has none; it does not say the protocol must never have one. The RFC proposes to amend both passages. (SPEC.md §18.3 Credentials)
  • It extends existing concepts and adds no new mechanism alongside them. The RFC adds one capability path, access.token, which is denied by default. It uses the existing advertisement and grant model, and grant reductions go through featureSets/update as §6.7 already defines. This matches the 'Capability Grants — host-controlled authorization' feature the README lists. (README.md, Features; SPEC.md §5.4, §6)
  • Identity is deferred in the SPEC, but that deferral covers URIs, and the RFC explicitly adopts no identity model. §18.8 lists 'Identity — principals, enrollment, succession' as deferred, within the endpoint-URI section. RFC §4.4 cites that line and only requires one principal per connection, leaving the identity model to host policy. Maintainers may still want to confirm that 'connection's principal' does not quietly create the identity layer they deferred. (SPEC.md §18.8 Out of scope)
  • It is a single, coherent proposal. The diff adds only the RFC file. It does not edit SPEC.md or the README status table, which matches how drafts are handled before they are accepted and incorporated. RFC-014, which this draft refers to, is not in the repository, but RFC-015 does not depend on it. (RFC-015-delegated-access.md)

Findings

9 findings of medium or high severity, see the inline comments:

4 more findings, less severe
  • Low severity, maintainability, RFC-015-delegated-access.md:135-137. Closed kind vocabulary gives no rule for values a server does not know. kind "is one of" six values, and servers are told to branch on it. The RFC does not say whether the set is open, or how a server treats a value it does not recognize. RFC-013's RefusalKind, by contrast, is declared open with a fallback rule. A later revision that adds a kind, such as one for proof-of-possession failures, would leave older servers with undefined behavior. The RFC should declare the set open and say servers treat unknown kinds as a final refusal.
  • Low severity, documentation, RFC-015-delegated-access.md:135-137. upstream_refused uses "upstream" in the opposite sense from RFC §7. In RFC §7 the "upstream" is the server the hop dialed, the one sending the request. Yet upstream_refused means that the hop's own host refused, which is the other direction. Readers implementing chains will misread which party refused. A name like forwarder_refused or host_refused, or consistent terminology in §7, would remove the ambiguity.
  • Low severity, documentation, RFC-015-delegated-access.md:403. Conformance vector 10 says "A host granted only access.*" but grants are given to servers. In SPEC §5.4 the host computes an effective grant for a server; hosts are not granted capabilities. The vector should read "A server granted only access.* (or *.token) receives -32002 from the host". As written, it is unclear which side is under test.
  • Low severity, documentation, RFC-015-delegated-access.md:336-353. Amendment list misses the §4 method overview and §13.2 audit fields. The SPEC §4 overview diagram (SPEC.md lines 143-159) lists every MCPL extension method and also needs access/token; the amendment list names only the message-flow table. SPEC §13.2 says hosts log MCPL operations with featureSet, but access/token carries none, and RFC §8.2 defines a different audit record. §13.2 should be listed as amended so the two do not conflict.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +135 to +137
`kind` is one of `no_identity`, `host_policy`, `provider_refused`, `rate_limited`,
`upstream_refused` (a chain hop's own host refused, RFC §7) or `unavailable` (no answer in
time); `reason` is free text for people. Servers branch on `kind`, never on `reason`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. unavailable ("no answer in time") depends on a timeout the RFC never defines.

The host mints by calling an external identity provider (RFC §8.1), or forwards up a chain (RFC §7). Yet the RFC sets no deadline for the host's provider call and no request timeout for the server. Hosts and hops can wait without bound, while the server dialing a remote is blocked on access/token. In a chain, each hop's timeout can also fire before or after the one above it, and nothing says which side reports unavailable. The RFC should recommend timeouts, as SPEC §10.6 does for hooks, and say which party emits unavailable.

Comment thread RFC-015-delegated-access.md Outdated
|---|---|---|---|
| `scheme` | `string` | Yes | `"bearer"` in this revision |
| `credential` | `string` | Yes | The credential, opaque to MCPL |
| `expiresAt` | `string` (RFC 3339, UTC, `Z` suffix) | Yes | When it stops being valid. A host that cannot state this MUST NOT answer |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. "A host that cannot state expiresAt MUST NOT answer" requires silence, which SPEC §6.6 forbids.

Read literally, the result table tells a host with no expiry for the minted credential not to respond at all. SPEC §6.6 says "A method that will never be answered MUST return an error", and RFC §4.2 repeats "never silence". A host that follows this line leaves the server's request hanging. The rule should say the host MUST NOT return a result and must instead refuse with -32040, either with an existing kind (e.g. host_policy) or a new one. The error table at lines 129-137 should list this case.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +188 to +190
**A hop in a chain** (RFC §7) holds no identity of its own but may obtain credentials from its
host. Such a hop **MAY** advertise `access` to the servers it dials, as a forwarder, if and only
if its own effective grant includes `access.token`; it stops advertising when it loses the grant.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. A forwarding hop cannot "stop advertising" access mid-connection, and may not know its grant when it advertises.

Host capabilities are sent once, in initialize (SPEC §5.2, §4.2.3). Nothing in the SPEC lets a host withdraw an advertisement during a connection; §17 manifest changes cover only servers. So "it stops advertising when it loses the grant" cannot be carried out on a live connection. The "if and only if its own effective grant includes access.token" condition also assumes the hop already knows its grant when it initializes upstream. In practice the hop may dial upstream before its own host's featureSets/update arrives (SPEC §5.3). The RFC should say what the hop does on loss of the grant, for example reduce the upstream's grant through featureSets/update, and how it treats an advertisement it made before its grant was known.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +222 to +227
Revoking `access.token` (a grant reduction, §6.7) takes effect at once for new requests.
Credentials already issued remain valid at their services until `expiresAt` — they are bearer
credentials, which is why lifetimes are short (RFC §5). The server learns of the reduction
from the `featureSets/update` Request §6.7 requires, and **MUST** then discard every credential
it obtained through this method on that connection and stop using it, as §6.7 requires
reductions to be respected at once.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Revocation does not propagate down a chain, so upstream servers keep forwarded credentials.

RFC §4.5 tells only the server whose own grant was reduced to discard its credentials. In a chain (RFC §7), the hop's host revokes the hop's access.token, but nothing requires the hop to reduce the upstream server's grant or tell it to discard. The upstream keeps using credentials it received through the hop until they expire, and it keeps sending requests that now come back -32040 upstream_refused. This undercuts the "revocation is quick" principle. The hop should be required to issue a grant reduction (featureSets/update removing access.token) to the servers it forwarded for.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +268 to +270
RFC §4.2). If its own host refuses, or it no longer holds the grant, it answers the upstream
with `-32040` and `kind: "upstream_refused"` — never `-32002`, which would tell the upstream it
lacks a grant that it holds. The principal does not

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. A chain hop collapses every refusal from its host into upstream_refused, so the downstream server cannot branch on kind.

RFC §3.3 says servers branch on kind. RFC §7, however, requires a hop to answer upstream_refused whenever its own host refuses. A rate_limited, provider_refused, no_identity or host_policy refusal from the top of the chain therefore reaches the requesting server as upstream_refused. That server cannot tell throttling, where it should back off, from a permanent refusal. The RFC should have the hop pass through the original kind, or add a member such as upstreamKind that carries it.

Comment thread RFC-015-delegated-access.md Outdated

| Field | Type | Required | Description |
|---|---|---|---|
| `scheme` | `string` | Yes | `"bearer"` in this revision |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. No scheme negotiation: a future host can return a scheme an older server cannot use.

The result's scheme is said to "leave room" for proof-of-possession and other schemes (RFC §9, §13). The request, however, gives the server no way to say which schemes it accepts, and the advertisement {token: true} carries no scheme list. The RFC also does not say what a server does when it receives an unknown scheme. Once a later revision adds a scheme, a host could return it to a server that only understands bearer, and that server would fail or misuse the credential. The RFC should add a schemes request member or advertisement field, or require the host to return bearer unless the server asked for another scheme.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +290 to +293
The host **SHOULD** record each request: server, audience, endpoint, purpose, outcome,
`expiresAt`. It **MUST NOT** record the credential, and records `endpoint` only as scheme,
host and path — without userinfo, query or fragment, where some deployments carry tokens
(§4.2.6). A server **MUST NOT** put a credential in `endpoint`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Audit form of endpoint drops the port, and purpose is logged unconstrained.

The host records endpoint "only as scheme, host and path", which drops the port. Two services on the same host but different ports become indistinguishable in the audit trail, even though RFC §8.4 says the field exists to make audits legible. The free-text purpose (up to 256 code points, no character restrictions in RFC §3.1) is also written to the same audit record. A granted server could include newlines or control characters in it to forge or obscure audit entries an operator reads. The RFC should keep the port and require control characters in purpose to be escaped or rejected.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +384 to +408

## 12. Conformance vectors

Freeze before Draft→Accepted (RFC-005 §11):

1. Request without the grant → `-32002`, `data.capability = "access.token"`.
2. Request to a host that did not advertise `access` → the server does not send it.
3. Granted request → result with `scheme`, `credential`, and an RFC 3339 UTC `expiresAt` in
the future.
4. Identity provider refuses → `-32040` with `audience`, `kind: "provider_refused"` and a
`reason`; no credential.
5. Unknown params member → `-32602`; a `_meta` object alongside valid params is accepted.
6. A credential never appears in any `push/event`, tool result or channel message of a recorded
session, nor in the server's captured log output (stderr) for it.
7. Two principals on one server: each request is answered with that principal's credential.
8. Grant revoked → the server discards its credentials, and its next request gets `-32002`.
9. Chain: a hop whose own host refuses answers its upstream `-32040` with
`kind: "upstream_refused"`; a hop without the grant does not advertise `access` to its
upstream.
10. A host granted only `access.*` (or `*.token`) answers `-32002`: no wildcard grants the path.
11. A malformed request from a server without the grant gets `-32002`, not `-32602` (order of
checks).
12. A host that includes `access.token` in a grant but has no identity answers `-32040` with
`kind: "no_identity"`.
13. The host's audit record of an `endpoint` with a query string holds no query string.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, test coverage. No conformance vectors for the parameter bounds or endpoint validation.

RFC §3.1 sets limits: audience 1-128 code points, endpoint an absolute URI of at most 2048 code points, purpose at most 256, fresh a boolean. Vector 5 covers only an unknown member and _meta. No vector checks an empty or over-long audience, a relative or over-long endpoint, an endpoint that carries a credential, or a non-boolean fresh. Implementations could disagree on code points versus UTF-16 units, as RFC revision 2 itself had to pin down, and no vector would catch it.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +384 to +408

## 12. Conformance vectors

Freeze before Draft→Accepted (RFC-005 §11):

1. Request without the grant → `-32002`, `data.capability = "access.token"`.
2. Request to a host that did not advertise `access` → the server does not send it.
3. Granted request → result with `scheme`, `credential`, and an RFC 3339 UTC `expiresAt` in
the future.
4. Identity provider refuses → `-32040` with `audience`, `kind: "provider_refused"` and a
`reason`; no credential.
5. Unknown params member → `-32602`; a `_meta` object alongside valid params is accepted.
6. A credential never appears in any `push/event`, tool result or channel message of a recorded
session, nor in the server's captured log output (stderr) for it.
7. Two principals on one server: each request is answered with that principal's credential.
8. Grant revoked → the server discards its credentials, and its next request gets `-32002`.
9. Chain: a hop whose own host refuses answers its upstream `-32040` with
`kind: "upstream_refused"`; a hop without the grant does not advertise `access` to its
upstream.
10. A host granted only `access.*` (or `*.token`) answers `-32002`: no wildcard grants the path.
11. A malformed request from a server without the grant gets `-32002`, not `-32602` (order of
checks).
12. A host that includes `access.token` in a grant but has no identity answers `-32040` with
`kind: "no_identity"`.
13. The host's audit record of an `endpoint` with a query string holds no query string.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, test coverage. No vectors for the cache, fresh: true, or a successful forward through a chain.

RFC §5's host caching (per configured server and audience, never across principals, bypassed by fresh: true) has no conformance vector. No vector checks that fresh: true returns a newly minted credential, or that cache entries are not shared between principals. RFC §7's chain has vectors only for refusal (vector 9). None checks that a hop holding both grants forwards the request, that the principal stays the same, or that a hop without the upstream's grant answers -32002. None checks that a host which cannot state expiresAt refuses rather than staying silent.

From review of revision 3:
- A hop advertises access as a forwarder at initialize and controls its upstream through the
  grant, reducing it at once when it loses its own; revocation travels up the chain.
- A hop passes its host's refusal on unchanged with forwarded: true; upstream_refused removed.
- The kind set is open; unknown kinds are final refusals.
- Servers name the schemes they can use; absent means bearer.
- A host that cannot state an expiry refuses with unavailable; recommended time bounds.
- Audit keeps the port; purpose may not carry control characters; endpoint may not carry
  userinfo; code points, not UTF-16 units.
- Amendments name the §4 method overview and §13.2; vectors 9, 10 reworded, 13-18 added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review

Recommendation: merge and fast-follow. 7 medium severity findings can be fixed after the merge.

Reviewed commit e7124f3 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.

Does this change belong in the project?

It fits the project. This draft RFC belongs here. It proposes a capability-gated protocol extension through the project's RFC process and fills a gap the SPEC set aside for a later revision.

5 reasons with sources
  • The SPEC sets aside credential policy for an identity mechanism that a future revision will define, and this RFC is such a proposal. §18.3 says "Credential policy belongs to the host and to whatever identity mechanism a future revision defines." §19.6.1 says there is no host→server inversion "under the current connection model", which describes the current state and does not forbid adding one. The RFC lists both passages as amendments. (SPEC.md §18.3 Credentials)
  • New protocol features enter as numbered RFC files at the repo root, which SPEC.md integrates once they are accepted, and this PR adds one Draft RFC in that form. The README calls SPEC.md the authoritative text and says "RFC files preserve proposal and review history". Several draft RFCs (006, 007, 008, 013) already sit at the root, and the README status table does not list them. The PR does not touch SPEC.md, so it is a proposal and not an integration. (README.md Status; repo root (RFC-001…RFC-013))
  • The feature is built on the project's existing capability-grant model and does not add a parallel authorization scheme. It adds one path, access.token, denied by default and authorized by the grant alone, the same way the channel methods are. Revocation uses the existing featureSets/update reduction. The one exception it carves out of the wildcard rule in §5.4 is listed explicitly as an amendment for maintainers to decide on. (README.md Features (Capability Grants); SPEC.md §5.4, §6.7)
  • It works around the SPEC's deferral of identity models and does not adopt one. §18.8 parks "Identity — principals, enrollment, succession". The RFC defines "the connection's principal" only as the identity the host acts as on a connection, and says it adopts no identity model. Whether even that minimal definition is acceptable is a call for maintainers to make during RFC review. (SPEC.md §18.8 Out of scope)
  • The change is coherent: one document covering one method, one capability path, one advertisement member and one error code. The PR adds this single file and nothing unrelated. The design defects the code review found, such as the cache key, chained time bounds and the conformance-vector gaps, are for the author to fix within the draft and do not affect whether the RFC belongs. (RFC-015-delegated-access.md (the whole file))

Findings

7 findings of medium or high severity, see the inline comments:

1 more finding, less severe
  • Low severity, documentation, RFC-015-delegated-access.md:313-315. The chain text describes a hop holding no grant 'not received yet', which RFC §4.2 rules out. RFC §4.2 says a hop includes access.token in an upstream's grant only while its own effective grant includes it. If it follows that rule, it cannot have granted the upstream before its own grant arrives. The 'not received yet' case in RFC §7 therefore cannot happen, or it means RFC §4.2 is not normative. The two sections should be made consistent.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +280 to +282
- Hosts **MAY** cache per (configured server, audience) — across reconnects of the same
configured server, never across principals — and **MAY** rate-limit; a server whose
requests are throttled degrades as for a refusal. `fresh: true` bypasses the cache.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. The host cache key leaves out the principal and the scheme it promises to respect.

RFC §5 lets hosts cache per (configured server, audience) and then says "never across principals". The key it names does not include the principal, and vector 7 has two principals on one server. A host built to the stated tuple would hand principal A's cached credential to principal B, which breaks RFC §4.4's one-principal rule. The key also leaves out the requested schemes, so a cached bearer credential could be returned to a request whose schemes excludes bearer, against RFC §3.2. The key should be stated as (configured server, principal, audience, scheme).

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +275 to +282
- A server **MUST NOT** use a credential after its `expiresAt`. It **SHOULD** request at each
dial and reconnect, and **MAY** reuse a credential across requests until shortly before
expiry (the reference implementation renews 60 s early).
- When the endpoint rejects a credential, the server **MAY** request once with
`fresh: true`; a second rejection is the endpoint's answer, not a reason to retry.
- Hosts **MAY** cache per (configured server, audience) — across reconnects of the same
configured server, never across principals — and **MAY** rate-limit; a server whose
requests are throttled degrades as for a refusal. `fresh: true` bypasses the cache.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Host caching can return a nearly expired credential, so the server's early renewal never gets a new one.

Servers MAY renew shortly before expiry; the reference renews 60 s early. Hosts MAY cache per audience without a required minimum remaining lifetime. When a server renews 60 s before expiresAt, a caching host can return the same cached credential. The server then still holds a credential inside its renewal window, asks again, and gets the same answer each time until it expires. The only way out is fresh: true, which RFC §5 reserves for an endpoint rejection. The RFC should require that cached credentials have a minimum remaining lifetime, or say how a server renews.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +166 to +176
### 3.4 Time bounds

Every party bounds its wait, and the party nearer the identity provider gives up first, so the
server hears a refusal rather than a timeout:

- A server **SHOULD** allow an `access/token` request at least **30 s** before abandoning it,
and treats an abandoned request as `unavailable`.
- A host **SHOULD** answer within **20 s**: it bounds its call to the identity provider and
answers `-32040`, `kind: "unavailable"`, when the bound passes.
- A hop (RFC §7) answers within the same 20 s, so it **SHOULD** give its own host no more than
**15 s** before answering `unavailable` itself.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Time bounds do not hold for chains of more than one hop.

RFC §3.4 says the party nearer the identity provider gives up first. It gives every host 20 s and every hop 15 s toward its own host. In a chain A→B→host, where both A and B are hops, A waits only 15 s for B, but B may take up to 20 s, giving its own host 15 s. A therefore times out and answers unavailable while B's real answer, possibly a credential or a final provider_refused, is still coming. The stated principle is broken, and the upstream gets unavailable (retry later) instead of the actual outcome. Chains are explicitly allowed (RFC §7), so the budget has to shrink at each hop, for example by carrying a deadline.

Comment on lines +310 to +320
- If its own host refuses, the hop passes the refusal on **unchanged** — the same `kind` and
`reason` — adding `forwarded: true`, so the upstream can still tell throttling from a final
refusal.
- If at that moment it holds no grant of its own (not received yet, or lost before it reduced
the upstream's grant), it answers `-32040` with `kind: "host_policy"` — never `-32002`, which
would tell the upstream it lacks a grant that it holds.
- It bounds its wait as RFC §3.4 says.

The principal does not change along the chain; each hop applies its own grant; the credential
returns down the same path. A hop **MUST NOT** answer from a credential it obtained for a
different request's audience, and passes the upstream's `schemes` on unchanged.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Forwarding rules cover neither fresh/endpoint/purpose nor host errors other than -32040.

RFC §7 only says a hop passes schemes on unchanged. It does not say whether fresh, endpoint and purpose are forwarded. A hop that drops fresh: true lets its host's cache answer, so the recovery path in RFC §5 does nothing for upstream servers, and the audit at the end of the chain loses the endpoint and purpose. Refusals are also only defined for -32040. If the hop's own host answers -32002 (a reduction still in flight) or -32602 (stricter validation), the RFC does not say what the hop returns. Passing -32002 through is exactly what the following bullet forbids.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +340 to +342
carry tokens (§4.2.6). A server **MUST NOT** put a credential in `endpoint`. `purpose` reaches
the record as data: it cannot carry control characters (RFC §3.1), and a host escapes anything
else its log format treats specially. This record is the audit of `access/token`; §13.2's

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. audience reaches the audit log with no control-character restriction.

RFC §8.2 defends the audit record against injection by banning control characters in purpose only. audience (1–128 code points, any characters) is also recorded, and a server can supply any value. A newline in audience can forge or split audit lines, unless every host happens to escape it, which the RFC states only as a loose aside. A granted server could hide which audiences it requested. The control-character rule (and vector 14) should also cover audience.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +128 to +130
Any other member, a value out of these bounds, an `endpoint` with userinfo, or a `purpose`
containing a control character (U+0000–U+001F, U+007F–U+009F) **MUST** be rejected with
`-32602 Invalid params`. Lengths are counted in Unicode code points, not UTF-16 units.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Rejecting every unknown param member blocks the RFC's own evolution path.

Any member other than the listed ones MUST be rejected with -32602. Revision 4 has already added schemes, and RFC §13 expects more (actor claims, PoP). A revision-4 server that sends schemes to a host built to revision 3 now gets a hard -32602 instead of the default bearer behavior. Every later field will cause the same break, and hops on different revisions will reject each other's requests. This contrasts with kind, which is explicitly open. The RFC needs either an ignore-unknown rule or a version or feature negotiation for new members.

Comment on lines +440 to +476

Freeze before Draft→Accepted (RFC-005 §11):

1. Request without the grant → `-32002`, `data.capability = "access.token"`.
2. Request to a host that did not advertise `access` → the server does not send it.
3. Granted request → result with `scheme`, `credential`, and an RFC 3339 UTC `expiresAt` in
the future.
4. Identity provider refuses → `-32040` with `audience`, `kind: "provider_refused"` and a
`reason`; no credential.
5. Unknown params member → `-32602`; a `_meta` object alongside valid params is accepted.
6. A credential never appears in any `push/event`, tool result or channel message of a recorded
session, nor in the server's captured log output (stderr) for it.
7. Two principals on one server: each request is answered with that principal's credential.
8. Grant revoked → the server discards its credentials, and its next request gets `-32002`.
9. Chain: a hop holding both grants forwards a request and returns its host's credential, for
the same principal; a hop whose own host refuses passes the refusal on with the same `kind`
and `forwarded: true`; a hop that loses its grant reduces its upstream's grant, and the
upstream discards its credentials.
10. A server granted only `access.*` (or `*.token`) receives `-32002` from the host: no wildcard
grants the path.
11. A malformed request from a server without the grant gets `-32002`, not `-32602` (order of
checks).
12. A host that includes `access.token` in a grant but has no identity answers `-32040` with
`kind: "no_identity"`.
13. The host's audit record of an `endpoint` with a query string holds no query string, and
keeps a non-default port.
14. Bounds, each `-32602`: an empty audience; an audience of 129 code points; an endpoint that is
relative, has userinfo, or is 2049 code points long; a non-boolean `fresh`; a `purpose` with
a newline; an empty `schemes`. An audience of 128 astral-plane characters (256 UTF-16 units)
is accepted.
15. A host whose provider gives no expiry answers `-32040`, `kind: "unavailable"` — no result,
no silence.
16. Cache: two requests for one audience get the same credential; `fresh: true` gets a newly
minted one; two principals never share an entry.
17. `schemes: ["example-pop"]` to a host that has only bearer → `-32040`, `kind: "host_policy"`;
absent `schemes` → `scheme: "bearer"`.
18. A server receiving an unknown `kind` treats the refusal as final.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, test coverage. No conformance vectors for the time bounds, a hop without its own grant, or fresh through a hop.

Vectors 1–18 do not cover several normative rules: the RFC §3.4 time bounds (a host answering unavailable when its bound passes, a hop's 15 s bound); RFC §7's rule that a hop without its own grant answers -32040 host_policy rather than -32002; fresh: true passing through a hop; a result's scheme being one of the request's schemes; and a server not using a credential after expiresAt. Vector 13 checks the query string but not that the fragment is removed. These rules can be implemented incorrectly without any frozen vector catching it.

… full forwarding, cache key and freshness

From review of revision 4:
- Hosts ignore unknown members (new ones need an advertisement if ignoring them is unsafe);
  servers check the returned scheme.
- timeoutMs carries the requester's bound; each hop shrinks it, so bounds hold for any chain.
- A hop forwards every member, passes -32602 on, and maps other host errors to -32040.
- Cache key (configured server, principal, audience, scheme); never a cached credential with
  under two minutes left.
- audience may not carry control characters.
- Vectors 19-24; 5, 13, 14 updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review

Recommendation: merge and fast-follow. 6 medium severity findings can be fixed after the merge.

Reviewed commit 2060103 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.

Does this change belong in the project?

It fits the project. This draft RFC belongs in the project: it is proposed the way the project takes new protocol features, as a draft RFC file. It fills a gap that SPEC §18.3 leaves open for a future revision, and it stays outside identity models, which the SPEC keeps out of scope.

5 reasons with sources
  • The change is a proposal in the form the project uses: a draft RFC file at the root that amends SPEC.md. The README says SPEC.md is the authoritative integrated text and that RFC files keep proposal and review history. RFC-006, -007, -008 and -013 already sit beside the accepted RFCs as drafts. This PR adds only RFC-015-delegated-access.md and does not edit SPEC.md. It lists the SPEC amendments it would make, which matches how earlier RFCs moved to 'incorporated'. (README.md (Status))
  • The SPEC itself leaves room for a credential mechanism in a later revision, and this RFC proposes one. §18.3 says credential policy 'belongs to the host and to whatever identity mechanism a future revision defines'. §19.6.1 says there is no host→server credential inversion 'under the current connection model', which describes the current model rather than ruling the idea out. The RFC names both passages as the ones it amends. (SPEC.md §18.3 Credentials)
  • It does not adopt an identity model, so it stays clear of what the SPEC parks as out of scope. §18.8 defers 'Identity — principals, enrollment, succession.' RFC §4.4 cites that section and says the RFC adopts no identity model. It requires only that a host act as one principal per connection. Enrollment and minting are left to the host and its identity provider. (SPEC.md §18.8 Out of scope)
  • It builds on the project's existing authority model instead of adding a second one. Access is controlled by one new capability path, access.token, which is denied by default. Revocation uses the existing grant reduction (featureSets/update, §6.7). Advertisement goes under experimental.mcpl, as other features do. This follows RFC-002's host-controlled grants rather than setting up a separate trust mechanism. (README.md (Features: Capability Grants); SPEC.md §5.4, §6.7)
  • It is one coherent proposal with no unrelated work attached. The whole diff is one new RFC file covering a single method, its capability, its error code and its conformance vectors. The code review found defects in the spec text, such as the chaining trust boundary and the unknown-member error, but those concern how the RFC is written, not whether it belongs in the project. (RFC-015-delegated-access.md)

Findings

6 findings of medium or high severity, see the inline comments:

2 more findings, less severe
  • Low severity, maintainability, RFC-015-delegated-access.md:513-514. Vector 24 repeats vector 16's per-principal cache check. Vector 16 already asserts "two principals never share an entry". Vector 24 tests the same property again, for two principals on one configured server. Vectors are meant to be frozen before acceptance (line 468), so duplicates will drift apart. Merge them.
  • Low severity, test coverage, RFC-015-delegated-access.md:332-337. No conformance vector for a hop's mapping of its host's -32002, -32602 and undefined errors. §7 defines how a hop maps errors from its own host: -32602 passes through unchanged, -32002 becomes -32040 host_policy with forwarded: true, and any other error becomes -32040 unavailable with forwarded: true. Vector 9 covers only a refusal passed on with its kind, and vector 20 covers only a hop with no grant. No vector exercises these three mappings, so implementations could diverge on them.

Comment thread RFC-015-delegated-access.md Outdated
|---|---|---|
| `-32002` Capability denied | `access.token` is not in the effective grant | `{ capability: "access.token" }` |
| `-32040` Access refused | The host has no identity to mint from, its policy refuses, it can offer none of the server's `schemes`, the identity provider refused or did not answer in time, the provider's answer is not a usable credential (no expiry), or (RFC §7) a hop's own host refused | `{ audience, kind, reason, forwarded? }` |
| `-32602` Invalid params | Malformed request, unknown member | `{ field, reason }` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Error table says an unknown member gets -32602, but §3.1 says hosts must ignore unknown members.

The §3.3 errors table lists -32602 Invalid params for "Malformed request, unknown member". RFC §3.1 (lines 141-144), the revision 5 note, the schema comment (line 445) and vector 5 all say a host MUST ignore members it does not know. A host built from the error table rejects every request that carries schemes, timeoutMs or any later member. That breaks the forward-compatibility the revision exists to provide: for example, a revision-4 host would refuse a revision-5 server's timeoutMs. Remove "unknown member" from the table.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +186 to +194
- A server waits `timeoutMs` (its own choice; **SHOULD** be at least 30000) and treats an
abandoned request as `unavailable`.
- A host answers within `timeoutMs` − 2000 ms (and in any case **SHOULD** within 20 s): it bounds
its call to the identity provider and answers `-32040`, `kind: "unavailable"`, when the bound
passes.
- A hop (RFC §7) forwards with `timeoutMs` reduced by at least 5000 ms, waits that long, and
answers `unavailable` itself if it passes — so each hop's bound is shorter than the one
waiting on it, however long the chain. A hop whose remaining budget would fall below 1000 ms
answers `unavailable` without forwarding.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Time bounds cannot be met for small timeoutMs values the schema allows.

timeoutMs may be anywhere from 1000 to 120000 (line 134), and a hop forwards values as low as 1000 (line 193). A host, however, must answer within timeoutMs − 2000 ms (line 188). For any timeoutMs below 2000 that bound is zero or negative, so a conforming host cannot meet it, and vector 19 cannot pass. Either raise the minimum (for example to at least 3000) and the hop's forwarding floor with it, or define the host's bound for small budgets. A related inconsistency: line 186 says a requester SHOULD wait at least 30000, but every hop must forward at least 5000 less, so hops always break that SHOULD.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +330 to +331
- It forwards `audience`, `endpoint`, `purpose`, `fresh` and `schemes` unchanged, and
`timeoutMs` reduced (RFC §3.4).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, documentation. Forwarding rule lists five members, but the revision note says a hop forwards every member.

The revision 5 note (line 20) says "A hop forwards every member". The normative text in §7 forwards only audience, endpoint, purpose, fresh and schemes unchanged, plus a reduced timeoutMs. _meta and any future member are left out. The two readings behave differently. Forwarding every member would also break §3.1's rule that a member which is unsafe to ignore is sent only to a host that advertised it, because a hop cannot know whether its own host advertised a member the hop does not understand. Pick one rule and state how unknown or advertisement-gated members are handled at a hop.

Comment on lines +238 to +246
**A hop in a chain** (RFC §7) holds no identity of its own but may obtain credentials from its
host. A hop configured to forward **MAY** advertise `access` to the servers it dials, as a
forwarder. Advertisements are made once, at `initialize`, often before the hop's own host has
sent its grant, so the advertisement says only that the hop can forward. **What an upstream
may do is the hop's grant to it**: a hop includes `access.token` in an upstream's grant only
while its own effective grant includes it, and when it loses its own it **MUST** at once reduce
every upstream's grant to exclude `access.token` (a `featureSets/update` Request, §6.7), which
makes each discard what it holds (RFC §4.5). When it gains the grant it **MAY** widen theirs
the same way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Chaining lets a hop pass the root operator's grant to arbitrary upstreams without the root operator's knowledge.

RFC §4.1 says access.token is granted only when "the operator has named it for that server explicitly", and §9 justifies the grant as trust in "a process the operator chose to trust". The chaining rules undercut this. A granted hop may grant access.token to any server it dials, and "when it gains the grant it MAY widen theirs" automatically (line 245). Requests carry no marker that they were forwarded or which upstream sent them, and the root host's audit (§8.2) records only the hop as server. The root operator therefore cannot see, attribute or refuse credentials that end up in remote upstream servers they never vetted, and with no audience narrowing those credentials can be for any audience. Consider requiring explicit operator naming per upstream at each hop, and a forwarded/via indicator the root host can audit and refuse.

Comment thread RFC-015-delegated-access.md Outdated
Comment on lines +499 to +500
16. Cache: two requests for one audience get the same credential; `fresh: true` gets a newly
minted one; two principals never share an entry.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, test coverage. Conformance vector 16 requires caching, which the RFC only permits.

RFC §5 says hosts MAY cache. Vector 16 requires that "two requests for one audience get the same credential". A conforming host that never caches, or that is rate-limiting, would fail a frozen conformance vector. Make the vector conditional on the host caching, or test only that a cached answer, when one is given, follows the keying and two-minute rules.

Comment thread RFC-015-delegated-access.md Outdated
| `endpoint` | `string` (absolute URI without userinfo, ≤ 2048 code points) | No | Where the server will present it. For audit; see RFC §8.4 |
| `purpose` | `string` (≤ 256 code points, no control characters) | No | Why, in words an operator reading an audit log would understand |
| `fresh` | `boolean` | No | Do not return a cached credential (the endpoint refused the last one) |
| `schemes` | `string[]` (1–8 entries) | No | The credential schemes the server can use, in order of preference. Absent means `["bearer"]` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. schemes entries have no length or character bounds.

Every other string parameter is bounded in code points and, for audience and purpose, has control characters excluded. schemes bounds only the number of entries (1–8); each entry may be a string of any length and content. The bounds were added so that hosts reject oversized or malformed input with -32602, and schemes is a gap in that. Vector 14 tests only an empty array. Add a per-entry bound and character set, and vectors for nine entries and an over-long entry.

…ward every member, schemes grammar, forwarder consent

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antra-tess
antra-tess merged commit eeabb3b into main Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant