Repository navigation
feat!: solutions run only through the host authority interface - #75
Merged
Merged
Conversation
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
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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