Repository navigation
feat(auth-gateway): a declared module route admits a host-minted operation context with no bearer, decided by accounts - #1251
Merged
saurabhmaheshwari-obin merged 17 commits intoOct 25, 2026
Conversation
…nstallation A person's capability is sealed to the organization's installation of the audience it addresses, and a module or installation principal's to its own. StartTask resolves that seal and remint re-resolves it for a root session, an audience exchange, a renewal and a module's exchange through its operation binding. StartChildSession did not: core's Child keeps the parent's seal, and the request names a free audience, so a member could derive a child for another module that stayed sealed to the first module's installation. StartChildSession now refuses a person's child unless the child's audience resolves to the parent's seal (PermissionDenied; an audience the organization never installed keeps audienceInstallation's FailedPrecondition). A principal that is not a person is sealed to its own installation and is unaffected. Existing callers derive same-audience children only. The invariant becomes load-bearing for the gateway's bearer-less module arm, which reads "sealed to an installation other than the target's" as unattended work. TestAPersonsCapabilityIsSealedToItsAudiencesInstallation pins it over every person mint (StartTask, StartRootSession, ExchangeAudience, RenewWorkContext, StartChildSession, and a module's exchange through its operation binding): each either seals to its audience's installation or is refused, with re-derived parents sealed to a stale installation first so a copied seal fails. TestStartChildSessionRefusesAPersonsChildForAnotherAudiencesInstallation is the cross-installation case alone. Both AGENTS.md files name the test, and WORK_CONTEXTS.md states the rule in its seal contract. The ListInstallations contract comment in installations.proto is not changed here. Editing it changes the accounts descriptors (the exported contracts, their digests, the generated Go and the raw OpenAPI), and a no-change regeneration of the accounts tree with the available toolchain did not come back clean, so the second caller is recorded in accounts' AGENTS.md instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on context with no bearer A composed workload with no person present (a solution's release publisher, for example) mints an operation context at /modules/_operation-context, bound to the target module's own audience, and had no way to present it at the one door: /v1/<alias>/* ran ext_authz, which denies every request without a bearer. The declared module route now has two arms. A request with no Authorization and no Cookie header, on the workload endpoint, presenting an edge-inspected capability addressed to the alias, takes the bearer-less arm (gateway_workload_operation.go); everything else takes the person path unchanged. On the arm: - a deployed gateway requires the waypoint presenter again (the listener already does); - the signed tenant and seal must be non-empty (401 invalid work context); - every identity header is stripped and Authorization / Proxy-Authorization dropped BEFORE the limiter; - the limiter keys on workload-operation:<signed tenant>, a namespaced branch of Middleware read from a private context value only the arm sets, and fails closed; - inside the limiter, accounts' InstallationService/ListInstallations (ACTIVE, uncached) must show an installation of the route's target (else 403) and the seal as a DIFFERENT active installation of that tenant (else 401 authentication required). A seal equal to the target's installation is a person's capability, which accounts now guarantees by invariant. An RPC error, an unterminated cursor or no client wired is 503; - the only stamped header is x-codefly-installation-id = the seal; no bearer, cookie or identity header is forwarded; an upstream failure is a static 502; - every admit and refusal is logged with alias, tenant, task, seal and presenter, never the token or an authority error. The ListInstallations messages are vendored as proto/saas/accounts/v1/installation_listing.proto: installations.proto imports the policy options and google.api the gateway does not vendor, so the file carries the six definitions the RPC uses, copied verbatim, and TestVendoredInstallationListingMatchesAccounts holds each byte-identical to accounts'. The client is accountsInstallations, in the shape of accountsSolutionEntitlements. Generated with `codefly generate proto` and `codefly generate contracts saas-starter` (CLI v0.1.207, whose no-change runs came back clean on both): the new .pb.go, the gateway contract copy, its contract.binpb and the digest in module.package.codefly.yaml and catalog.codefly.json. Tests: the admitted case, every refusal with one fact broken, paging, logging, the static 502, the edge refusals (expired, forged, foreign issuer), the empty-tenant guard, caller-asserted headers on the arm, a bearer on the workload listener taking the person path, grpc-metadata stripping, a Basic credential leaving the arm, Proxy-Authorization, the tenant-keyed bucket, limiter fail-closed, and an in-process mint-then-publish round trip. Existing tests pass unedited; test helpers gained an installation and issuer on edgeCapability, a call counter on fakeUpstream, and a token override on the fake operation-context mint. Docs: GATEWAY_ROUTES.md (the two arms), WORK_CONTEXTS.md (how a minted operation context is presented), the auth-gateway AGENTS.md (the arm, SP-GW-07's positive half met on this path, the fourth workload-only surface), and the comments on removePersonsSessionCredential, installationConfinementHeader (its meaning on each arm), gateway_module_capability.go and handleDeclaredModule. Second consumer: any solution with a release-declared headless operation uses this arm as is. Modules calling peers keep their in-mesh seam. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s every target row Review fixes on the declared module route's bearer-less arm: - Exactly one capability header. Core's Inspector verifies the first value only, so a request carrying two is refused with 401 invalid work context before anything is stripped or asked. The person path is unchanged. - A seal equal to ANY active installation of the route's target is read as a person's. The admission read kept only the last target row it saw, so with two active rows a seal equal to the first was admitted. Every page is now read before deciding. - X-Forwarded-Client-Cert is removed before forwarding. The authenticated listener removes it on a cell; natively nothing did. - The log reason for a request that never reached the admission read is limited_or_origin, since the cross-origin binding refuses there too. - installationConfinementHeader's comment and the AGENTS.md line say the header equals the presented seal whenever a capability is presented. - The AGENTS.md release note: deploy accounts before the gateway, since an older accounts can mint a cross-sealed child valid for up to 900 seconds. - Test fixtures use a neutral path, /v1/documents/items:write. Tests: two capability headers, a seal equal to the first of two target rows, and the header absent upstream on an admitted request. Each fails on the previous arm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…child owner kinds The seal rule's documentation said source-operation contexts and installation tasks are minted for a module or an installation. Both are owned by a person and sealed elsewhere, by design, and the bearer-less module arm admits them: - a source-operation context (startSourceOperation) is sealed to the installation of the module using the person's source delegation, and only that module's identity exercising the delegation mints one; - an installation task (StartInstallationTask) is owned by the installation's owner of record, sealed to that installation, and reached only with an EXPOSURE_INTERNAL credential. WORK_CONTEXTS.md, both AGENTS.md files and the arm's header comment now name them as the two person-owned kinds sealed elsewhere, with their gates, and the arm's admitted family as module operation contexts plus these two. TestEveryWorkContextMintIsClassifiedBySeal enumerates every saas.accounts.v1 RPC whose response carries a token from the generated descriptors (eleven today) and fails on one listed neither as person-sealed, naming the sealPinCases row that holds it, nor as sealed elsewhere, with a one-line reason. A listed name that no longer exists fails too. The seal pin table gains the module read exchange (ExchangeDelegatedReadAudience), which re-mints a person's capability like the operation exchange does. StartChildSession's guard is an allow-list: a person's child is held to its audience's seal, a service principal's passes, and any other owner kind (agent, empty, unknown) is refused with PermissionDenied. TestStartChildSessionGuardAllowsOnlyKnownOwnerKinds pins it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vendors it verbatim The gateway's installation_listing.proto declared, in accounts' package, a file accounts does not have. Each published contract loaded alone, but loading accounts' and the gateway's into one descriptor pool failed with a name conflict over saas.accounts.v1.Installation. The six definitions ListInstallations takes and answers (InstallationStatus, InstallationHealth, Installation, ListInstallationsRequest, InstallationSummary, ListInstallationsResponse) move, with their comments, from installations.proto into an accounts-owned saas/accounts/v1/installation_listing.proto with minimal imports, the pattern module_registration.proto and solution_entitlements.proto follow. installations.proto and platform_admin.proto import it. Same package, same fully-qualified names, same wire format; every RPC stays where it was. buf.yaml gains scoped MESSAGE_NO_DELETE and ENUM_NO_DELETE waivers for installations.proto, as for the earlier moves; buf breaking against the previous tree and buf lint pass. The gateway's copy is now that file byte for byte. The regex drift test is replaced by TestVendoredAccountsProtosAreAccounts, which holds every vendored accounts proto in the gateway byte-identical to accounts' file of the same name (five files today). The ListInstallations contract comment, the ListOrganizationInstallations comment and the ListInstallations handler comment now name the gateway's second read: its bearer-less module arm, whose tenant is the one an edge-verified Work Context names. The accounts AGENTS.md note that deferred this to the next regeneration is replaced. Regenerated with the CI's Codefly source candidate 5234ce24 (proto companion 0.0.16): codefly generate proto --path saas for accounts (a no-change run with --path saas was clean first; without it the CLI also generates the vendored codefly/ tree and reorders the raw OpenAPI), codefly generate proto for the gateway, go generate for accounts' business, adapters and cataloggen, codefly generate contracts saas-starter, and the saas-sdk library through its own scripts/generate.mjs --force. The frontend's imports of the moved types point at installation_listing_pb (biome check clean), and ui-inventory.json records the new import. Both published contracts, and all three together, now load into one pool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d from accounts A module's route alias and its principal prefix are independent names. Accounts seals a person's capability for audience X to the active installation whose registry principal is ModulePrincipalID(X), while the bearer-less arm found "the target's installation" by the route alias. With module A (alias storage, principal documents) and module B (alias documents), a person's StartTask(audience="documents") is sealed to A's installation, /v1/documents routes to B, and the arm read A's seal as unattended work and admitted it without the person's bearer. A withdrawn binding's row left ACTIVE under the principal had the same effect. Installation gains registry_principal_id = 14 in the accounts-owned installation_listing.proto: the principal the installation was installed under, written at install and never changed. The store's canonical installation columns carry it, so ListInstallations, GetInstallation, the catalogue listing and every RETURNING row report it consistently. The gateway's copy stays byte identical. The arm now also refuses (401 authentication required) a seal whose active row has registry_principal_id equal to modulePrincipalID(alias), or empty: an unknown principal never admits. modulePrincipalID restates accounts' ModulePrincipalID (neither core nor the sdk exports it, and the gateway cannot import accounts); TestModulePrincipalIDMatchesAccounts in the gateway and TestModulePrincipalIDIsPinnedForTheGateway in accounts pin the same values. Tests: the alias/principal mismatch, a stale row under the alias's principal and a seal row with no principal are refused, and each is admitted without the new check; the existing admit case still admits (its fixture rows now carry their principals). TestInstallationReadsCarryTheRegistryPrincipal (Postgres) holds InstallSolution, GetInstallation and ListInstallations to the column. Regenerated with Codefly source candidate 5234ce24 (--path saas for accounts), go generate, contracts and the saas-sdk library. buf breaking and lint pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd pin a solution's The registry_principal_id comment said a solution's installation carries none. It does: InstallSolution and a module's install both resolve the declared binding's registry principal in prepareInstallation and refuse without one. The column is empty only on rows written before migration 32 added it (left NULL by design, no backfill infers a principal) or written by the store directly with none. The comment now says so. TestInstallationReadsCarryTheRegistryPrincipal now names its row as a solution's installation and checks that LiveModuleAuthority resolves that solution's principal to the same row: the seal a solution's module identity mints is a row carrying its principal, which the bearer-less arm admits. Regenerated with Codefly source candidate 5234ce24 (--path saas), go generate, contracts and the saas-sdk library. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generated accounts types moved to installation_listing_pb, so the published package changes. saas-ui's peer floor stays 0.3.35: it uses none of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he bearer-less module arm
The gateway's bearer-less arm of /v1/<alias>/* classified a capability by its
seal: one sealed to an active installation other than the target's read as
unattended work. Several person-owned capabilities carry exactly that seal by
design: a source-operation context, an installation task or install step, and
the installed binding children of ExchangeDelegatedOperationAudience
(content_read and module_actor), which keep the exchanging module's seal while
addressing another module. The arm would admit each of them without the
person's bearer. Core's Inspected withholds the owner, its kind and the chain,
so the gateway cannot tell; the issuer can.
WorkContextService/AdmitWorkloadOperation{work_context, alias} is internal-tier
(EXPOSURE_INTERNAL, the internal credential, the tier ListInstallations sits
in). Accounts runs core's Inspector (signature under this issuer, window,
audience equal to the alias), decodes its own claims, and admits only what
StartModuleOperationTask signs: a service principal's capability with no actor
chain, grant hop, operation binding, parent session, owner agent, workspace,
project or attribution teams, with authority scopes, an idempotent replay policy
and at most the operation-context window, whose owner is a declared module
principal other than ModulePrincipalID(alias) and whose seal is that
principal's ACTIVE installation in the tenant, read live through
WorkContextSeal. A refusal is an answer carrying a WorkloadOperationRefusal; an
error means it could not decide. It never reaches verifyFor, verifyIssued or
the replay store, so it consumes nothing.
The messages live in an accounts-owned, import-minimal
workload_operation_admission.proto, which work_contexts.proto imports, so the
gateway can vendor it byte for byte as it does installation_listing.proto.
Tests (pkg/adapters/workload_operation_admission_test.go):
- TestEveryMintHasAWorkloadOperationVerdict mints through every token-answering
RPC (StartTask, StartRootSession, ExchangeAudience, RenewWorkContext,
StartChildSession, StartInstallationTask, MintModuleWorkContext,
MintModuleOperationContext, MintSourceOperationContext, and both delegated
exchanges in every binding mode: plain, content_read, module_actor, source
delegation reference and parent), the install step, the installer token
route, and crafted variants of the operation-context shape (single-use, longer
window, no scope, an actor hop, the alias's own module, another
installation's seal, an undeclared service principal, agent, human and
unnamed owner kinds). Exactly one row is admitted.
- TestWorkloadOperationTableNamesEveryMintAndOwnerKind fails when a
saas.accounts.v1 RPC answers a token, an adapters function calls Start,
Child, mint or remint, or business declares a PrincipalKind constant, that the
table has no row for.
- TestAdmitWorkloadOperationConsumesNothing: a single-use capability shown three
times still verifies once at its callee and is then replayed; an admitted
operation context still verifies at its callee.
- TestAdmitWorkloadOperationIsInternalAndFailsClosed.
The seal rule's documentation (accounts AGENTS.md, the seal pin test, the child
guard, the ListInstallations contract and handler comments) no longer says the
arm depends on it, and names the installed binding child among the kinds sealed
elsewhere. The child guard and the seal pin test are unchanged in behaviour and
pass: exchangeInstalledBinding derives through core's Child directly, not
StartChildSession, so the guard never sees it.
Regenerated with Codefly source candidate 5234ce24: codefly generate proto
--path saas for accounts (a no-change run was clean first), go generate for
business, adapters and cataloggen, codefly generate contracts saas-starter, and
the saas-sdk library through scripts/generate.mjs --force. The internal method
count moves 54 to 55. buf lint and buf breaking against the base pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…admits, not what the seal suggests
The arm admitted a capability with no bearer when its seal named an active
installation that was not the target's and was not installed under the alias's
module principal. A source-operation context, an installation task or install
step, and an installed binding's child (content_read, module_actor) are each a
person's capability sealed exactly that way, so each was admitted without the
person's bearer.
The arm now asks accounts' WorkContextService/AdmitWorkloadOperation, the
issuer's allow-list of the one shape it is for (StartModuleOperationTask), and
decides nothing from the seal. Selection, the identity strip (with
Authorization, Proxy-Authorization and X-Forwarded-Client-Cert) before the
limiter, the fail-closed workload-operation:<tenant> budget, the static 502 and
the admit/refusal log are unchanged. Behind the limiter:
- AdmitWorkloadOperation{work_context, alias}, by method name over the existing
accounts connection with the internal credential, bounded at 2 seconds (the
on-request lookup bound the client registry uses). A refusal is 401
"authentication required", logged with the issuer's reason; an error, a
timeout, no client wired, or an answer naming another tenant or seal than the
edge inspected is 503. Nothing else is asked after a refusal.
- ListInstallations for the signed tenant, as before, for the target's rows
only: not installed is 403, a seal on one of the target's rows is 401.
x-codefly-installation-id is set once, to the seal accounts admitted. The
gateway vendors workload_operation_admission.proto byte for byte
(TestVendoredAccountsProtosAreAccounts covers it). modulePrincipalID and its pin
are gone: the alias's own principal is refused by the issuer now.
Tests: TestModuleRoute_TheBearerLessArmAdmitsOnlyWhatTheIssuerAdmits holds every
WorkloadOperationRefusal to 401 with no installation read, and fails against the
seal-based arm (each case answered 200 there). The refusal table gains an
erroring issuer, no issuer wired and admissions naming another seal or tenant;
TestAccountsWorkloadAdmissionAsksTheIssuerWithinItsBound runs the production
client against a gRPC accounts double for the method and internal credential,
and against one that never answers for a bounded 503. The limiter tests assert
a limited or limiter-down request never reaches the issuer, and the admit test
that the header carries one value, the admitted seal, with no bearer forwarded.
The registry-principal cases leave with the check they tested.
The gateway AGENTS.md, GATEWAY_ROUTES.md and WORK_CONTEXTS.md say
the arm asks the issuer, name the guard tests, and change the release note: an
older accounts does not serve AdmitWorkloadOperation, so the arm answers 503
and admits nothing until accounts is rolled.
Regenerated with Codefly source candidate 5234ce24: codefly generate proto for
the gateway and codefly generate contracts saas-starter. buf lint passes for
the gateway's proto tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… reads now registry_principal_id = 14 was added to the accounts-owned installation_listing.proto so the gateway's bearer-less arm could refuse a seal on the alias's module principal. The arm now asks AdmitWorkloadOperation, which reads the owner itself, and no longer reads the field; nothing else does. This restores, byte for byte, the state before the two commits that added it: installation_listing.proto (accounts' file and the gateway's vendored copy), the installation read columns in postgres_installations.go, and their tests (TestInstallationReadsCarryTheRegistryPrincipal and TestModulePrincipalIDIsPinnedForTheGateway). The column itself predates this branch (migration 32) and is untouched. The field never shipped in a release, so buf breaking against the base sees no change to Installation. Regenerated with Codefly source candidate 5234ce24 (proto for accounts and the gateway, go generate, contracts, the saas-sdk library); every generated file the field touched is identical to its state before it was added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The accounts service catalog (generated/service-catalog.json) is generated text, and each RPC adds about 1.7 KB to it. With AdmitWorkloadOperation it grew to 524,580 bytes, past the gate's 512 KiB cap, so the gate reported it as too large to scan and failed. The gate's own remedies were to split a generated file or to allowlist it, and an allowlisted catalog would stop being read at all while every RPC description keeps landing in it. The cap is raised to 1 MiB instead, above the largest generated text in the tree, so the file is scanned rather than exempted. A file over the cap is still reported, never skipped; the test that pins that builds a file over the new cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nswers the target's installation too Review follow-up on the bearer-less arm's issuer answer. - The capability is read back, not inspected and decoded: readBackFor, beside readBack, is core's Authority.ReadBack with the alias as the audience and noApprovalGrants as the grant source, so the answer also holds the live revision, seal and epochs, and still never touches the replay store. Read-back failures map to refusals: an invalid or foreign token is NOT_INSPECTED; a superseded revision, a seal the owner no longer holds (core reports it as revoked), an uninstalled module or a module whose grant now names another tenant is the new NOT_CURRENT; a binding or a grant hop is NOT_OPERATION_CONTEXT. Any other failure is a live read that could not answer, Unavailable. The shape and owner checks run on the read-back claims. The explicit WorkContextSeal call is gone: the read-back's seal check is the same answer. - One call answers the gateway: the request gains target_id (uuid), and accounts refuses TARGET_NOT_INSTALLED when the tenant has no ACTIVE installation of it and SEAL_ON_TARGET when the seal is one of its rows. business.ActiveTargetInstallations pages ListInstallations for those rows; TestActiveTargetInstallationsAnswersEveryActiveRowOfTheTarget (Postgres) pins it against a revoked row, another target and another organization. - The internal credential is checked before the request is validated. Guards: - signingFunctions type-checks the package (export data from go list) and flags any declaration referencing a method of core's *workcontext.Authority other than ReadBack, or this server's own mint or remint, by type: calls, method values, method expressions, and package-level variables. Entries are keyed by receiver type and name, so another type's mint is its own entry. Throwaway files calling Authority.Grant, binding Authority.Start to a variable, declaring a package-level method expression of it, and declaring a signing mint on another type each failed TestWorkloadOperationTableNamesEveryMintAndOwnerKind; all were deleted. - The RPC guards read the response and request messages transitively: an RPC answers a capability when IssuedWorkContext, or an accounts string field named token or work_context, appears at any depth, and re-mints when it also takes parent_work_context_token at any depth. TestCapabilityDetectionIsTransitive holds that against synthetic nested messages (a walker limited to the top level fails it). - TestEveryRemintOfAServicePrincipalsParentHasAVerdict presents a service principal's capability to every re-mint, and Guard B fails on a re-mint RPC without such a row. Every re-mint refuses it at the mint: the owner a re-mint resolves current authority for needs an organization membership and an authorization revision, which a declared module principal does not have. TestADelegatedExchangeRefusesAModuleOperationContextAsItsParent (pkg/infra) runs both delegated exchanges against PostgreSQL with a module operation context minted through StartModuleOperationTask, and Guard B requires that file to cover every module re-mint. workContextAuthorityFake now refuses a declared module owner as the store does; no other test's verdict changed. - New verdict rows: target not installed, seal on a target row, a module reinstalled since the mint, a module uninstalled since the mint, a module whose tenant changed, and a person's capability whose revision moved (all NOT_CURRENT but the first two). Regenerated with Codefly source candidate 5234ce24 (accounts and gateway proto, go generate, contracts, saas-sdk). The gateway's vendored copy follows the accounts file; the gateway does not send target_id until the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d installation header Review follow-up on the bearer-less module arm. - One authority call: the arm sends the route's target_id with the capability and alias to AdmitWorkloadOperation, which now answers the installation question too. TARGET_NOT_INSTALLED (and a route that resolves no target, decided before any call) is 403; every other refusal is 401. The ListInstallations loop, its client, its page bounds and the gateway's installations field are gone from the arm. - An InvalidArgument answer is 401 "authentication required" (the request's shape is the capability's or the alias's), not 503. - A Connection option may not name the capability header: the arm answers 401 "invalid work context" before anything is stripped or asked. - The arm stamps no x-codefly-installation-id. No consumer of a module route reads it (the only reader in this repository is accounts' own interceptor, for requests carrying the gateway token on accounts' routes; neither sdk-go nor saas-sdk-go reads it), a caller's value is stripped as before, and core says a hop stamps nothing a callee would read in place of the token. The header's comment and the docs say so. Tests: the refusal table and harness lose the installation fakes; the issuer fake records target_id; every WorkloadOperationRefusal value is mapped (403 for TARGET_NOT_INSTALLED, 401 otherwise); the admit test asserts one call carrying the capability, alias and target and no installation header upstream. Docs: the gateway AGENTS.md, GATEWAY_ROUTES.md and WORK_CONTEXTS.md describe the single call, the Connection rule and the absent header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tions.proto The bearer-less arm no longer calls ListInstallations (accounts answers the target's installation inside AdmitWorkloadOperation), and nothing else in the gateway did on the base either, so the gateway needs neither the ListInstallations messages nor a vendored copy of them. This undoes the extraction e94688afd made for it, back to the base byte for byte: - accounts: installations.proto, platform_admin.proto and proto/buf.yaml (with its MESSAGE_NO_DELETE and ENUM_NO_DELETE waivers for installations.proto) return to the base; installation_listing.proto is deleted. The ListInstallations contract comment and handler comment return to the base wording, which never named the arm. - gateway: its vendored installation_listing.proto and generated Go are gone. TestVendoredAccountsProtosAreAccounts stays and now guards workload_operation_admission.proto with the vendored files the base had. - frontend: the five solution-installations imports and ui-inventory.json return to installations_pb. Every regenerated installation file (Go, TypeScript, raw and public OpenAPI, contract protos and buf.yaml) is identical to the base. The saas-sdk keeps 0.3.36: its content still differs from the last release (the admission messages), and kit-version check requires the bump. Regenerated with Codefly source candidate 5234ce24 (accounts and gateway proto, go generate, contracts). The saas-sdk library was regenerated with released CLI 0.1.191, the version the base's last regeneration of it records: its output is byte-identical to the candidate's except the generated-by stamp, which now reads 0.1.191 again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…generated text A file over the cap was told to split or be allowlisted. For generated text that grew, the remedy this tree took is raising MAX_BYTES, so the message now names it beside the other two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WorkloadOperationRefusal's comments follow the read-back mapping: a seal on an installation the owner no longer holds, an uninstalled module and a module whose grant now names another tenant are NOT_CURRENT (core reports a seal that is no longer held as revoked), and SEAL_NOT_OWNERS is the owner no module declares. The gateway's copy follows byte for byte. Regenerated with Codefly source candidate 5234ce24 (accounts and gateway proto, go generate, contracts) and the saas-sdk library with released CLI 0.1.191. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
antoinetoussaint-byte
pushed a commit
that referenced
this pull request
Oct 25, 2026
Main's bearer-less module arm (AdmitWorkloadOperation, #1251) admits one shape: a module operation context with no operation binding. This branch seals that context to the module's own installed operation binding (module-operation/<principal>/<binding>), so the shape now admits that binding and only it (business.IsModuleOperationBindingOf). The interface's child hop signs a capability, so it gets its admission row (a person's, refused as OWNER_NOT_SERVICE). Counts and the authorization matrix are read off the regenerated catalogs; the host interface harness passes main's plan policy. The infra active-target test now carries the !pure constraint its database fixture needs (it broke the pure build on main). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
antoinetoussaint-byte
added a commit
that referenced
this pull request
Oct 26, 2026
Main's bearer-less module arm (AdmitWorkloadOperation, #1251) admits one shape: a module operation context with no operation binding. This branch seals that context to the module's own installed operation binding (module-operation/<principal>/<binding>), so the shape now admits that binding and only it (business.IsModuleOperationBindingOf). The interface's child hop signs a capability, so it gets its admission row (a person's, refused as OWNER_NOT_SERVICE). Counts and the authorization matrix are read off the regenerated catalogs; the host interface harness passes main's plan policy. The infra active-target test now carries the !pure constraint its database fixture needs (it broke the pure build on main). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
What
A declared module route
/v1/<alias>/*admits a host-minted headless operation context with no bearer, on the gateway'sworkloadendpoint only.Today a composed workload with a headless binding to a declared module can mint its operation context at
/modules/_operation-context, but the route then refuses it, because ext_authz requires a person's bearer. That leaves a solution's release-declared headless operation with no host path at all (for example, publishing its own definitions to a module at release), since a solution may reach a module only through the gateway.Accounts, the capability's issuer, makes the admission decision through a new internal RPC,
AdmitWorkloadOperation. It is an allow-list of exactly one shape: the contextStartModuleOperationTaskmints.Also in this PR:
StartChildSessionused to let a child for another module's audience keep the parent's seal.This replaces #1243, which targeted a patch line off v0.0.105. That line has no consumer, so this PR targets main.
How the arm decides
The gateway selects the arm only when all of these hold:
Authorizationheader and noCookie;Connectionoption names it;module-capabilities.Every other request takes the existing path unchanged.
On the arm:
Authorization,Proxy-Authorizationand XFCC are removed before anything else runs.workload-operation:<tenant>. It fails closed and runs before the accounts call.AdmitWorkloadOperation{work_context, alias, target_id}, bounded at 2 s.TARGET_NOT_INSTALLED: 403.What accounts admits
AdmitWorkloadOperationis internal-only, with the same policy block asListInstallations. It checks the internal credential before it validates the request.Authority.ReadBack, audience = alias). The read-back checks signature, window, revision and the live seal, and it never touches the replay store, so no nonce is spent.StartModuleOperationTaskshape:NOT_INSPECTED,OWNER_NOT_SERVICE,NOT_OPERATION_CONTEXT,OWNER_IS_TARGET,SEAL_NOT_OWNERS,TARGET_NOT_INSTALLED,SEAL_ON_TARGETandNOT_CURRENT(an uninstalled module, a changed tenant, or a revoked seal).Person-owned capabilities are all refused, whatever their seal. That includes source-operation contexts, installation tasks, and installed content-read and module_actor children.
Contract changes
saas/accounts/v1/workload_operation_admission.protoholds the request, the response and the refusal enum.work_contexts.protoimports it, and it addsWorkContextService/AdmitWorkloadOperation. The gateway vendors the file byte-identical, as it does its other accounts protos.buf breakingagainst main passes.@codefly-dev/saas-sdkgoes to 0.3.36 for the new generated messages, because the kit-version gate requires the bump.module/tools/naming-gate.mjs), its own commit:generated/service-catalog.jsonto 524,580 bytes, past the gate's 512 KiB cap, so the gate refused to scan it. This PR raises the cap to 1 MiB, so the file is still scanned.Tests
TestEveryMintHasAWorkloadOperationVerdict: every token-minting RPC and every binding mode, each minted through a real path, gets an asserted verdict. OnlyStartModuleOperationTaskadmits.TestWorkloadOperationTableNamesEveryMintAndOwnerKind: fails when any of these appears without a row in that table:*workcontext.Authority, detected by type, including method values and package-level vars;TestEveryRemintOfAServicePrincipalsParentHasAVerdict, plusTestADelegatedExchangeRefusesAModuleOperationContextAsItsParentinpkg/infraagainst Postgres: a module operation context presented as a parent to every re-mint path is refused.TestAdmitWorkloadOperationConsumesNothing: a single-use capability admitted three times still verifies once at the callee.TestAdmitWorkloadOperationIsInternalAndFailsClosed.TestAPersonsCapabilityIsSealedToItsAudiencesInstallationandTestStartChildSessionRefusesAPersonsChildForAnotherAudiencesInstallation.TestModuleRoute_TheBearerLessArmAdmitsOnlyWhatTheIssuerAdmitsand the arm's selection, strip order, limiter-before-call, 401/403/503 mapping,Connection, two-carrier, 502 and nil-client cases. Each refusal case fails against the code before its fix.pkg/adapters, thepkg/infrainstallation and exchange tests, and auth-gateway;buf lintandbuf breaking, andcodefly generate contracts --check;check-boundaries.sh.Rollout
Deploy accounts before the gateway. A gateway that calls an accounts without the RPC answers 503 and admits nothing, so it fails closed.
Known limits
StartModuleOperationTasksigns. The durable form is core-side: core'sInspectedwould expose the owner kind (or a mint-stamped marker) so a hop can check it directly. That reverses core's "a hop never reads the principal", so it is left for a design discussion.Review
Connectionoption naming the capability header;Live
Local run with CLI 0.1.207 and this head (
3b16ae2d3) as the host. A solution's release publisher mints at/modules/_operation-contextand posts its bundle to/v1/<alias>/definitions:publishon the workload endpoint, with no bearer.reason=workload_operation_admitnaming the alias, the tenant, the task, the solution's own installation as seal, and presenterlocal. The module created 5 definitions.target_not_installed, then admitted once it was installed.authority_unavailableat the 2 s bound. 200 once it resumed.restendpoint;owner_not_service);capability_not_single);Connectionoption naming the carrier (connection_names_capability).Not verified live (unit-tested):
Second consumer
Any other solution with a release-declared headless operation uses the same arm and copies nothing. Any other edge would call
AdmitWorkloadOperationrather than reimplement it. Modules keep calling each other directly in the mesh.🤖 Generated with Claude Code