Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
406 changes: 109 additions & 297 deletions AGENTS.md

Large diffs are not rendered by default.

179 changes: 179 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,184 @@
# Changelog

## Unreleased — the operation checked is the operation run; every served route publishes its policy (breaking)

From the independent review of 2026-10-25 (F1–F4, F10, F11):

- **Dispatch by method and path (F1, security).** The runtime picked a
handler by path alone, so with a GET (read policy) and a POST (write policy)
on one path, a GET checked against the read row ran the POST handler. A
request is now answered by the operation declared for its method on its
path, which is the operation the authority's `Check` names; a method the
path does not declare is answered 405, naming the methods it serves (and
`Allow`), before the authority is asked or any handler runs. The boot
refuses an operation that declares no policy, is undocumented, declares one
method twice on a path, or names a method that is not an operation's.
`Server.Handle` and `Server.HandleRequest`, which registered a route with
no method and no policy, are deleted: `Operations` is the one way a backend
serves a route of its own.
- **Issues go through the authority's hop library (F2).** `ForParent` and
`ForBinding` ask through `hop.Outbound.IssueForParent` / `IssueForModule`
(codefly-dev/authority `libraries/hop/go`, extended for it) with
`hop.FileCredential` at the projected path; the runtime builds no
`IssueRequest` and reads no credential file of its own.
- **The MCP route publishes its policy (F3).** `ServeMCP(name, version,
policy, register)` takes the policy a call to `POST /v1/<id>/mcp` must hold,
and the interface artifact publishes it there, so the CLI's rows include the
route and an enforcing authority admits it. Only POST is served.
- **Relayed routes are any checked caller's (F4).** Every relayed
`/v1/<id>/modules/<as>/...` route is published with
`{"any_checked_caller": true}` and the passthrough's `Check` names it as
served; the gate on a relayed call is the per-call child issued through the
solution's binding (the person's current grants ∩ the binding) and the
module's own method policy at its hop. The README says why. A route refusal
reaches the page as `permission_denied`.
- **`InterfaceArtifact` is the server's (breaking).** `Server.InterfaceArtifact(dir,
info)` renders the artifact from what the server serves — its operations,
its MCP route and the passthrough at the paths they are served at — so a
route cannot be served without a row. The free `InterfaceArtifact(dir, info,
ops, modules...)` is deleted.
- **An authority outage is named (F10).** A `Check` the authority did not
answer is 503 `ErrAuthorityUnavailable` (Connect `unavailable`), never a
401 that reads as "sign in".
- **The authority endpoint passes the SDK's credential-destination rule
(F11).** It is dialled through the same `workcontext.AdmitEndpoint` rule as
every relayed module.

## Unreleased — the runtime runs only under the authority; the saas-host mode is deleted (breaking)

Since the passthrough relays only through the authority, nothing a consuming
solution does reaches the saas host any more, and the rest of that mode is
deleted with its tests and docs:

- `Gateway.ForModule` and its published-ceiling check, the accounts
`StartTask` mint, and the identity headers it was rooted in
(`Gateway.InstallationID`, `x-org-id`, `x-session-id`,
`x-codefly-installation-id`).
- The broker execution credential: `Server.Credential`, `CredentialSource`,
the `module-authority` group (`AuthorityGroup` and its keys,
`INTERNAL_TOKEN`), the per-installation credential controller, and the
`saas-sdk-go` dependency.
- Calls through the saas gateway: `Gateway.Transcoded`, `MaxResponseBytes`,
`TranscodeOption`, `DefaultTranscodedResponseLimit` (now
`DefaultPassthroughResponseLimit`), `Unary`, `Gateway.HTTPClient`,
`Gateway.BaseURL`, gateway resolution by role (`GATEWAY_URL`,
`CODEFLY_HOST_MODULE`, `CODEFLY_HOST_GATEWAY`, `CODEFLY_HOST_FRONTEND`).
- The install step: `Server.OnInstall`, `Installation`, `RefuseInstall`,
`ErrInstallRefused` and `POST /_codefly/installations`.
- The MCP surface's saas wiring: the gateway-stamped identity it read, the
OAuth Protected Resource Metadata document and its challenge,
`ProtectedResourceMetadataPath`, `MCPEnvironment`, the `mcp` group
(`issuer-url`, `public-url`) and `PUBLIC_URL`.
- `ErrNotAttested`, `ErrCredentialRefused`, `ErrCredentialUnavailable`,
`WorkContextPrincipals` from an accounts mint, and the published
contract's `principal`.

Kept, because the authority path reaches it:

- `ServeMCP` and `ViewerFromContext`: the MCP transport, now served at
`/v1/<id>/mcp` and admitted exactly as a handler route is — the authority's
`Check` of the Work Context the request carries, naming `POST /v1/<id>/mcp`.
A tool reaches a module through `ForParent`. This is the transport an
authority-based MCP route (robin calling a solution's tools) builds on.
- `Contract`/`ModuleContract`/`ContractArtifact`: optional; a solution that
declares no ceiling is bounded by the authority alone.
- `GatewayError`, `ClientError`, `WorkContextRefusal`: a handler's own
refusals and the authority's.

A composition that names no `authority/endpoint` is now refused at boot,
whether or not the solution consumes modules, and so is one with no projected
workload credential path.

## Unreleased — the passthrough relays only through the authority, by each module's own procedure (breaking)

The passthrough relays every declared method over gRPC to the module's own
procedure — the Connect/gRPC face the module serves behind the authority's
hop — so a module whose protos carry no `google.api.http` binding is consumed
as it is. No binding is read: the boot no longer requires one under
`/v1/<as>` for every declared method, which refused every solution pinned to a
module that serves only behind the hop. A server-streaming method is relayed
as a gRPC stream, bounded and masked as before.

The saas-host passthrough is deleted, with its configuration, tests and docs:
forwarding a method over its REST binding through the saas gateway (`forward`,
the NDJSON stream reader and `Gateway.TranscodedStream`, `StreamError`,
`ErrStreamMessageTooLarge`, `StreamOption`), `ConsumedModule.ViewerBearer`,
the check of the declaration against the `CODEFLY__API_CONSUMES` projection
(which is no longer read), the plain-text gateway refusal relay, and the test
seam that faked the saas host (`passthroughtest`, `internal/seam`).

- `ConsumedModule.Authority` is required on every consumed module, naming all
three of its group, endpoint slot and binding slot.
- A solution that declares consumed modules and has no `authority/endpoint`
refuses to boot, naming it — before the gateway, which its page's calls
never reach. The `module-authority/INTERNAL_TOKEN` refusal it replaces is
gone, and so is the boot's broker credential for a consuming solution in
saas mode, which this made unreachable.
- `GatewayError` keeps only its status: the refusal body it carried was read
by the saas passthrough alone.

## Unreleased — every documented operation declares its authority policy (breaking)

`Operation.Policy` is what a call to the route must hold —
`Requires(kind, actions...)` or `AnyCheckedCaller()`, the shape of the
authority's `authority.hop.v1.Policy` — and `InterfaceDocument` writes it as
the operation's `x-codefly-authority-policy`, the extension the CLI derives
the solution's `"<VERB> <template>"` route rows from. An operation that
declares no policy, an empty one, or both, is refused at generation, by its
route: the runtime's `Check` names every route a request calls, so a route
with nothing to be held to would only ever be refused by the authority.

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

Each `operationId` is derived from the method and the route as served
(`<prefix>_<Method>_<Path…>`, GET `/sources` is `Notes_Get_Sources`, a
subtree `/x/` ends `_X_Path`), so the methods of one path, and a path and the
subtree under it, no longer share one; two operations the derivation would
spell alike are refused at generation.

## Unreleased — under the authority, a route is held to the solution's delivered policy; a module's tool call acts for the person

The authority's `Check` of the Work Context a request carries now names the
route it calls, `"<VERB> <path>"` as the solution serves it — the spelling the
authority matches against the `"<VERB> <template>"` rows the CLI derives from
the published contract — so the solution's delivered per-route policy is
applied. Before, any context addressed to the solution reached every route. A
route the call does not hold is answered 403 with the authority's reason
(`ErrRequestRefused`); a context the authority does not check is still 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. The
passthrough relay names no route; the module it relays to applies its own.

A module calling a solution route as a tool, with the child the authority
issued through its binding, is admitted like the edge's context, and the
handler's gateway says who it is for: `WorkContextPrincipals` answers the
person as owner and the module as current actor, and the new
`Gateway.ActorChain()` the whole chain, both from the authority's `Check`.

`dialAuthority` is unexported; the authority client moves to 06d0d3b for
`CheckRequest.advisory`.

## Unreleased — the passthrough under the authority; refusals keep their reason

Under the authority interface the passthrough is no longer refused at boot: a
consumed module whose new `ConsumedModule.Authority` names its configuration
slots (gRPC address, binding id) is relayed over gRPC as the viewer, a child
of the edge's Work Context issued through that binding, pinned and masked as
declared, at `/v1/<id>/modules/…`. The copies solutions carried of this relay
can be deleted.

`WorkContextRefusal.Reason` carries the authority interface's reason for a
refusal (a transport failure keeps none, and the saas broker's body is still
never retained); handlers and the passthrough relay it. A refusal from a
solution's own route reaches a Connect or gRPC client as a Connect error, code
and message, instead of `{"error": …}`, which Connect clients ignore; a plain
REST caller (no `Connect-Protocol-Version`) still gets `{"error": …}`.

## Unreleased — `Gateway.InstallationID` exposes the stamped installation

`Gateway` exposed `OrgID()` but not the installation beside it, so a handler
Expand Down
Loading
Loading