Skip to content

feat!: solutions run only through the host authority interface - #75

Merged
antoinetoussaint-byte merged 13 commits into
mainfrom
authority/through-the-interface
Oct 25, 2026
Merged

antoinetoussaint-byte merged 13 commits into
mainfrom
authority/through-the-interface

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

The passthrough relays each module call by that module's own procedure, through the hop, with one child Work Context per call. Saas mode, the transcoder and saas-sdk-go are deleted. Every route publishes a policy. Dispatch is method-aware: the operation checked is the operation run (security fix F1). Issuance reuses hop.Outbound.

This is part of moving the platform from saas permissions to the host authority interface: one interface, implemented by saas accounts and by the local authority. Core #835 (merged) is the base. Land in dependency order: core → cli → agents → authority → saas → modules → solution runtime → core-solutions → platform-obin.

Merge with a merge commit, never a squash. Other repos pin this branch's commits by SHA.

Every call goes through a Codefly-generated client on one Connect face behind the authority hop.

🤖 Generated with Claude Code

AntoineToussaint and others added 13 commits October 23, 2026 18:26
A page's runtime could only run in front of the saas host: it refused to boot
without the host gateway, read its viewer from headers the gateway stamped and
minted at the host's StartTask route. Under the authority interface
(codefly-dev/authority) none of that exists, and a page that cannot boot cannot
be developed against it.

When the composition names the authority (authority/endpoint):

- Boot needs no gateway and no broker credential.
- The viewer is the authority's answer about the request's bearer
  (session.Current); headers the request wrote are ignored, and an unknown
  bearer is answered 401 before the handler runs.
- ForModule's Work Context is issued by authority.Issue for the person, with the
  viewer's bearer, and held until due for renewal.
- ForBinding (new, authority only): the module's own Work Context through one of
  its declared bindings, in the viewer's organization, with the workload
  credential the run attests.
- WorkContext (new): the token a gateway acts under, for a caller reaching a
  module itself over gRPC through the hop.
- Gateway passthrough has no gateway under the authority and is refused at boot
  by name.

Without authority/endpoint nothing changes, so saas still runs locally.

Tests: viewer from the session with forged headers ignored, unknown bearer
refused (sabotage-checked), viewer and binding issues and their caching, a
refused issue, config without a gateway, ForBinding refused without the
authority. Existing suite unchanged and green. README: "Under the authority
interface".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ForBinding asks for a binding's own Work Context and names no scopes: the
authority issues the binding's declared headless scopes and nothing else. The
published-ceiling check every mint passes refused it as "a capability with no
scopes at all" whenever a contract was resolved, which is every real boot; the
test missed it because its gateway carried no contract. The check now applies
to scoped asks only, and the test carries ceilings.

Found by running the directory page against the authority gateway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unded by the authority

A module page consumes nothing through passthrough, so it has no ceiling it can
publish here, and every ForModule it made was refused locally as "no binding for
that audience" before the authority was asked. Under the authority the viewer's
Work Context is bounded by the authority itself: what the viewer holds, cut to
what the audience serves. A solution that publishes no ceiling is now held to
that alone; an ask for nothing is still refused before it leaves, and a
solution that publishes ceilings is still held to them. Saas mode is unchanged.

Known gap: the authority should hold a page's declared bindings as its ceiling,
so the bound is the module's declaration rather than the viewer's holdings.

Found by running the directory page against the authority gateway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ever a session bearer

Under the authority interface the page is reached through the authority's edge,
which signs the person in and sends each request on with the person's Work
Context addressed to this module. The runtime now takes that context as the
request's identity — the authority's Check of it says who the viewer is — and
issues every call a person makes to a module as a child of it, through one of
the module's declared bindings (ForParent: parent token, the workload as actor,
the audience derived by the authority from the binding). ForBinding is
unchanged; ForModule is the saas route and is refused under the authority,
naming ForParent. No session bearer reaches the runtime, and session.Current is
no longer asked.

Breaking for a page under the authority: call ForParent with a binding where it
called ForModule with an audience. The saas path is unchanged.

Tests: the viewer is the edge's context (a forged one, none, or a bearer alone
is 401; sabotage-checked), ForParent's issue shape and holding, a refusal, and
ForModule refused under the authority. README updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sals keep their reason

Under the authority interface the passthrough was refused at boot, so every
solution that consumed a module carried its own relay: decode the page's
Connect call, issue the viewer's child Work Context through its binding, call
the module over gRPC through the hop, pin and mask as declared. The wiki and
proposals held labelled copies of the same code.

The runtime now serves the same Consumes declaration under the authority. A
consumed module whose new ConsumedModule.Authority names its configuration
slots (the module's gRPC address and the binding id) is relayed over gRPC as
the viewer: a child of the edge's Work Context issued through that binding
with the method's declared scopes (ForParent), the declared pin merged in, the
response allowlist applied, at /v1/<id>/modules/<as>/<procedure>, where the
edge forwards the solution's requests unchanged. The hop's address is admitted
by the SDK's one rule for a credential's destination (AdmitEndpoint). A module
naming no slots is not relayed and its routes stay the solution's; a missing
slot, a ViewerBearer or a server-streaming relayed method refuses the boot.
Reused: resolvePassthrough, the Connect handler and its protocol handling,
ForParent, Apply, codeForStatus, relayedError's 4xx/5xx line; the pin merge
and the viewer check were extracted rather than restated.

WorkContextRefusal.Reason keeps the authority's reason for refusing an issue
(not a transport failure's text; the saas broker's body is still never kept),
and handlers and the passthrough relay it. A refusal from a solution's own
route reaches a Connect or gRPC client as a Connect error with the authority's
code instead of {"error": ...}, which Connect clients ignore; a plain REST
caller still gets the JSON envelope.

Tests: relay as the viewer (child issue, pin, mask, unrelayed module 404, no
Work Context 401), the authority's and the module's refusals keep code and
reason, a module failure leaks no text, boot refusals naming the slot and a
cleartext off-machine address, refusals in the caller's protocol, the reason
on WorkContextRefusal. Sabotage-checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y endpoint's is

dialAuthority strips grpc:// from authority/endpoint; a relay slot holding the
same spelling was refused as a scheme that is not https. It is now read as
cleartext and admitted by the same SDK rule. The relay test dials grpc://.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d policy; a module's tool call acts for the person

The authority's Check of a request's Work Context named no method, so any
context addressed to the solution, a module's child issued for a tool call
among them, reached every route. The Check now names the route,
"<VERB> <path>" as the solution serves it, the spelling the authority
matches against the "<VERB> <template>" rows the CLI derives from the
published contract, and a route the call does not hold is answered 403 with
the authority's reason (ErrRequestRefused); an unchecked context stays 401.
Under hop/ENFORCEMENT=warn the check is the authority's advisory one, as a
permissive hop's is: the route goes through and the refusal is logged.

A module's tool call is admitted like the edge's context, and the handler's
gateway says who it is for from the Check: WorkContextPrincipals names the
person as owner and the module as current actor; Gateway.ActorChain() the
whole chain.

The authority client moves to 06d0d3b for CheckRequest.advisory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ten where the CLI reads it

Under the authority the runtime's Check names every route a request calls,
"<VERB> <path>" as served, and the authority holds it to the row the CLI
derives from the published contract's x-codefly-authority-policy. Nothing
wrote one: Operation had no policy and InterfaceDocument no extension, so no
solution route could be delivered a policy.

Operation.Policy is what a call to the route must hold — Requires(kind,
actions...) or AnyCheckedCaller(), the shape of authority.hop.v1.Policy — and
InterfaceDocument writes it on each operation. An operation that declares
none, an empty one, or both is refused at generation by its route.

The document is the routes as served: an empty BasePath is omitted (an
authority-served backend's operations already carry /v1/<id>), a subtree
pattern ending in "/" is written ".../{path=**}", a method is lowercased, and
two methods on one path are both kept (the second used to replace the first).
The passthrough's documented routes carry no policy: the module they relay to
applies its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
operationID spelled only the path, so two methods on one path (which the
document keeps since the policy change) shared one operationId, and an
operationId names exactly one operation. It is now
<prefix>_<Method>_<Path...> (GET /sources is Notes_Get_Sources), and
InterfaceDocument refuses two operations the derivation would spell alike
rather than writing an ambiguous document.

TestInterfaceDocumentGivesEveryOperationItsOwnID renders six methods on
one path and fails if any two share an id (it fails on the previous
code: six operations, one id), and checks that a colliding pair is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
operationID spelled op.Path, where "/x" and the subtree pattern "/x/" are
the same segments, so the two routes (the wiki's /v1/wiki/mcp and
/v1/wiki/mcp/{path=**}) still shared an id: the guard added in e5b3354
refused the wiki's artifact. The id is now derived from the route as
served, a wildcard segment contributing its name ("/x/{path=**}" ends
"_X_Path"). TestInterfaceDocumentGivesEveryOperationItsOwnID covers the
pair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ule's own procedure

The modules now serve one Connect-family face behind the authority's hop and
carry no google.api.http bindings. resolvePassthrough still required a binding
under /v1/<as> for every declared method, authority or not, so a solution
pinned to those clients refused to boot.

The passthrough now relays every declared method over gRPC to the module's own
procedure, the one mechanism the documents relay already used: a child of the
edge's Work Context issued through the module's binding, the pin merged in,
the response masked. A server-streaming method is relayed as a gRPC stream
under the same bounds. No binding is read.

The saas-host passthrough is deleted, with its configuration, tests and docs:
forwarding over a REST binding through the gateway, the NDJSON stream reader
and Gateway.TranscodedStream, ConsumedModule.ViewerBearer, the
CODEFLY__API_CONSUMES projection check (the projection is no longer read), the
plain-text gateway refusal relay, GatewayError's refusal body, and the
saas-host test seam (passthroughtest, internal/seam).

ConsumedModule.Authority is required on every consumed module. A solution
that declares consumed modules and has no authority/endpoint refuses at boot,
naming it, before any gateway check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ode is deleted

a009bc2 left the passthrough relaying only through the authority, which made
every other part of the saas-host mode unreachable for a consuming solution.
Legacy means delete, so it goes with its tests and docs:

- Gateway.ForModule and its published-ceiling check, the accounts StartTask
  mint, and the gateway-stamped identity it rooted in (InstallationID,
  x-org-id, x-session-id, x-codefly-installation-id).
- The broker execution credential: Server.Credential, CredentialSource,
  openAuthority and the module-authority group, credential.go,
  broker_credential.go, scoped_credential.go, broker_owner.go,
  mint_errors.go, and the saas-sdk-go dependency.
- Gateway.Transcoded, Unary, Gateway.HTTPClient/BaseURL and gateway
  resolution by role (GATEWAY_URL, CODEFLY_HOST_*).
- The install step (OnInstall, Installation, RefuseInstall,
  POST /_codefly/installations), which only the saas host called.
- The MCP surface's saas wiring: the stamped-identity check, the OAuth
  Protected Resource Metadata document and challenge, MCPEnvironment, the
  mcp group and PUBLIC_URL.

Kept, each with the caller that reaches it under the authority:

- ServeMCP/ViewerFromContext and the result sanitizer: the MCP transport,
  now at /v1/<id>/mcp and admitted by the authority's Check naming
  POST /v1/<id>/mcp, a tool reaching modules through ForParent. It is the
  transport an authority-based MCP route (robin calling a solution's tools)
  needs.
- workContextCache, delegation, surfaced: ForParent and ForBinding cache,
  single-flight and sanitize through them (authority.go).
- Contract/checkContract: start() resolves it for the passthrough's asks.
- GatewayError, WorkContextRefusal, ClientError: handlerErrorResponse and
  relayedError map handler and authority refusals through them.
- cleanPrefix, methodDescriptor: resolvePassthrough (moved out of the
  deleted transcode.go).

A composition that names no authority/endpoint is refused at boot, consumed
modules or not, and so is one with no projected workload credential path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…publishes its policy

The independent review of 2026-10-25 (F1-F4, F10, F11):

- F1 (security): a handler was picked by path alone, so a GET checked against
  a read-only row ran the POST (write) handler on the same path. A request is
  now served by the operation declared for its method on its path, which is
  the operation the authority's Check names; an undeclared method is 405 by
  name before the authority is asked or any handler runs. The boot refuses an
  operation with no policy, undocumented, declared twice, or with a method
  that is not an operation's. Handle and HandleRequest (no method, no
  policy) are deleted: Operations is the one way to serve a route.
  TestTheOperationCheckedIsTheOperationRun is the reviewer's reproduction;
  it failed before ("GET answered 200 {\"ran\":\"write-handler\"}").
- F2: ForParent and ForBinding issue through the authority's hop library
  (hop.Outbound.IssueForParent / IssueForModule, hop.FileCredential), pinned
  at codefly-dev/authority 7d00337; the runtime builds no IssueRequest.
- F3: ServeMCP takes the policy for POST /v1/<id>/mcp, and the interface
  artifact publishes it; only POST is served.
- F4: relayed /v1/<id>/modules/... routes publish any_checked_caller and the
  passthrough names them in its Check; the gate is the per-call child through
  the solution's binding and the module's own method policy (README).
- Server.InterfaceArtifact renders the artifact from what the server serves;
  the free InterfaceArtifact is deleted.
  TestUnderEnforcementEveryServedRouteIsAdmittedByItsPublishedRow boots an
  operation, the MCP route and two relayed routes against an enforcing
  stand-in holding only the published rows, and shows each row is what
  admits its route.
- F10: an authority outage is 503 by name, never a 401 sign-in.
- F11: the authority endpoint passes the SDK's credential-destination rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 00f2acb into main Oct 25, 2026
1 of 2 checks passed
antoinetoussaint-byte added a commit that referenced this pull request Oct 25, 2026
fix!: the authority is asked about the operation dispatched, never the decoded path (#75 review)
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.

2 participants