Skip to content

Prepare MCP acquisition checks and bound delegated token expiry - #53

Merged
antoinetoussaint-byte merged 3 commits into
mainfrom
issue-52-an-agent-client-cannot-obtain-a-work-context
Oct 28, 2026
Merged

antoinetoussaint-byte merged 3 commits into
mainfrom
issue-52-an-agent-client-cannot-obtain-a-work-context

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Collaborator

Refs #52. Draft: acquisition is still unavailable to an external MCP client; this PR must not close the issue or be treated as its acceptance proof.

A capability issued shortly before a delegation ends currently receives another local MaxTTL (15 minutes). This change caps the signed capability expiry at the delegation end, rechecks that bound after signing, and adds the assertion to the shared conformance suite. Core’s existing 30-second verifier tolerance and already-issued tokens are unchanged.

The independent protocol work is in libraries/authority-core/go/oauthwire: protected-resource and authorization-server metadata from explicitly supplied public URLs, the initial 401 challenge value, S256 PKCE checks, exact resource comparison, and expiry reporting. These are stateless helpers with no production caller yet. They do not create a client registry, code/token store, sign-in path, grant resolver or minter.

README.md documents the changed signed-expiry behavior. docs/mcp-acquisition.md records the reuse audit, source-linked owner questions, scope/lifetime/refresh rules, and the pending acceptance recipe.

Decisions and implementation still required

  • External-client grantee identity and whether to serve RFC 7591 registration. Delegate requires an installed composed module; delegated Issue requires its attested workload and binding. An OAuth client ID alone cannot exercise that arm. Accounts already has an OAuth flow, but its code redemption mints Accounts sessions through its own transactional business/store path; host-owner reconciliation is still required.
  • Consent for a background client. The feat(authority): a page delegates a run through the edge, and a delegation can expire (authority 0.3.5) #50 proposal’s no-consent decision expressly concerns work initiated in the person’s page.
  • Who declares/projects each solution’s public MCP resource URL and the authority issuer URL through SDK resolution. No route is guessed from request headers or historical frontend paths.

After those decisions: connect discovery and authorization endpoints to shared host code/storage and existing SessionService/Delegate/Issue, implement edge admission and rotating refresh, and run a real URL-only MCP client through sign-in and all five tools, with cross-solution refusal. No new issues or tickets were filed; the remaining work stays in this PR and #52.

Validation

Passed locally:

  • Shared authority core: GOWORK=off go test -race -count=1 ./... and go vet ./..., plus the signing-race regression after it was added.
  • Local adapter: race tests and vet for ./pkg/business/issuer ./plugins/authority ./plugins/registry_test. This runs core’s Work Context conformance kit and the shared authority conformance suite through the local adapter.
  • Conformance Go module: race test/build and vet.
  • Package and no-proxy gates: race tests and vet.

The signed-expiry regression was observed failing before the fix (a default-TTL capability outlasted a delegation with 30 seconds remaining by 870 seconds).

Not proven: Accounts provider parity, an HTTP OAuth flow, a live composed MCP client, the five tool answers through acquired credentials, or cross-solution refusal at the real edge. oauthwire unit tests do not establish any of these.

@antoinetoussaint-byte
antoinetoussaint-byte marked this pull request as ready for review October 28, 2026 21:43
@antoinetoussaint-byte

Copy link
Copy Markdown
Collaborator Author

Merging this on the owner's instruction, having reviewed the diff. Recording what the review established, because the PR body is careful to say this is not the acceptance proof and that remains true.

It grants no new authority, and that is structural rather than a matter of care. A client delegation sets d.Module = "" (delegation.go), and IssueForDelegation refuses unless am.Name == d.Module — a condition no module name can satisfy, since am comes from tx.ModuleNamed(actor.Module). So a client-grantee row can be stored and nothing can mint from it. The minting path is simply not built yet, and the arm fails closed until it is.

The expiry change is a strict tightening. IssueForDelegation now caps the signed capability's TTL at the delegation's remaining lifetime instead of granting a fresh MaxTTL, refuses outright once the remaining lifetime is non-positive, and re-checks after signing that signed.GetExpiresAtUnix() does not exceed the delegation's end — which covers both time advancing during signing and the store shortening the grant. Core's 30-second verifier tolerance and already-issued tokens are untouched.

delegableScopes is a behaviour-preserving extraction of the binding loop that was inline in Delegate, now with one ceiling for both callers.

oauthwire has no production caller. Protected-resource and authorization-server metadata from explicitly supplied public URLs, the 401 challenge value, S256 PKCE checks, exact resource comparison and expiry reporting — stateless helpers, with their own tests.

Verified on the merged tree (7142769b, origin/main merged in rather than force-pushed): go build ./..., go vet ./... and go test ./... across libraries/authority-core/go all pass.

Not verified, and #52 stays open for it: nothing here has been exercised against a running edge. The first MCP 401 still carries no OAuth discovery, and DCR, the consent step, the code exchange and refresh are unwired, so no external MCP client can yet acquire a Work Context. The remaining order is on #52, along with the owner's three decisions.

One non-blocking finding for whoever continues: in delegate, a tx.Client(clientID) error is collapsed into domain.Refuse("unknown or disabled client"), so a store failure reads as a refusal rather than an error. It fails closed, which is the right direction, but the two cases are worth distinguishing.

@antoinetoussaint-byte
antoinetoussaint-byte merged commit 7954a62 into main Oct 28, 2026
17 checks passed
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