Skip to content

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 into
codefly-dev:mainfrom
saurabhmaheshwari-obin:s6/b2-main
Oct 25, 2026
Merged

saurabhmaheshwari-obin merged 17 commits into
codefly-dev:mainfrom
saurabhmaheshwari-obin:s6/b2-main

Conversation

@saurabhmaheshwari-obin

Copy link
Copy Markdown
Contributor

What

A declared module route /v1/<alias>/* admits a host-minted headless operation context with no bearer, on the gateway's workload endpoint 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 context StartModuleOperationTask mints.

Also in this PR:

  • accounts: a person's capability always stays sealed to its audience's installation. StartChildSession used 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:

  • no Authorization header and no Cookie;
  • the request arrived on the workload listener, plus the XFCC presenter on a cell;
  • exactly one capability header, and no Connection option names it;
  • core's inspection passes for the route's alias, and the alias is never module-capabilities.

Every other request takes the existing path unchanged.

On the arm:

  • Strip first: identity headers, Authorization, Proxy-Authorization and XFCC are removed before anything else runs.
  • Rate limit: keyed workload-operation:<tenant>. It fails closed and runs before the accounts call.
  • Admission: one call to AdmitWorkloadOperation{work_context, alias, target_id}, bounded at 2 s.
    • Admitted: forward.
    • TARGET_NOT_INSTALLED: 403.
    • Any other refusal, or an invalid argument: 401.
    • An error, a timeout, no client, or an answer naming a different tenant or seal than the one inspected: 503.
  • Forwarding: no bearer, and no installation header stamped. The callee verifies the capability itself at full strength. An upstream failure gets a static 502.
  • Logging: every admit and refusal is logged with the issuer's reason, never the token.

What accounts admits

AdmitWorkloadOperation is internal-only, with the same policy block as ListInstallations. It checks the internal credential before it validates the request.

  • It reads the capability back through core's issuer read-back (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.
  • It admits only the StartModuleOperationTask shape:
    • a service owner that is a declared module principal, and not the alias's own;
    • sealed to that principal's live, active installation;
    • no actor chain, grant hop, operation binding or parent session;
    • idempotent, with a window no longer than the 60 s TTL;
    • at least one scope.
  • It also answers the target: the target must be installed (ACTIVE) in the tenant, and the seal must not be one of the target's rows.
  • Refusals: NOT_INSPECTED, OWNER_NOT_SERVICE, NOT_OPERATION_CONTEXT, OWNER_IS_TARGET, SEAL_NOT_OWNERS, TARGET_NOT_INSTALLED, SEAL_ON_TARGET and NOT_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

  • New accounts-owned saas/accounts/v1/workload_operation_admission.proto holds the request, the response and the refusal enum. work_contexts.proto imports it, and it adds WorkContextService/AdmitWorkloadOperation. The gateway vendors the file byte-identical, as it does its other accounts protos.
  • Additive only; buf breaking against main passes.
  • @codefly-dev/saas-sdk goes to 0.3.36 for the new generated messages, because the kit-version gate requires the bump.
  • Naming-gate size cap (module/tools/naming-gate.mjs), its own commit:
    • The new RPC grows generated/service-catalog.json to 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.
    • Alternatives: allowlisting the file, which would stop scanning it entirely, or splitting the generated catalog, which is a structural call for maintainers.
    • The commit stands alone, so it can be reverted or replaced without touching the rest.

Tests

  • accounts:
    • TestEveryMintHasAWorkloadOperationVerdict: every token-minting RPC and every binding mode, each minted through a real path, gets an asserted verdict. Only StartModuleOperationTask admits.
    • TestWorkloadOperationTableNamesEveryMintAndOwnerKind: fails when any of these appears without a row in that table:
      • a token-returning accounts RPC, detected transitively through the response;
      • a signing call on *workcontext.Authority, detected by type, including method values and package-level vars;
      • a principal kind.
    • TestEveryRemintOfAServicePrincipalsParentHasAVerdict, plus TestADelegatedExchangeRefusesAModuleOperationContextAsItsParent in pkg/infra against 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.
    • TestAPersonsCapabilityIsSealedToItsAudiencesInstallation and TestStartChildSessionRefusesAPersonsChildForAnotherAudiencesInstallation.
  • auth-gateway: TestModuleRoute_TheBearerLessArmAdmitsOnlyWhatTheIssuerAdmits and 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.
  • Gates run locally:
    • go vet and go test on the changed packages: accounts pkg/adapters, the pkg/infra installation and exchange tests, and auth-gateway;
    • buf lint and buf breaking, and codefly generate contracts --check;
    • kit-version, release-gates, interface-docs, naming gate, authz coverage and 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

  • An admitted context is a bearer capability for its window (60 s or less), pinned to one audience.
  • The arm does not bind the presenter's workload identity to the owner. On a cell the presenter must still be a mesh workload.
  • The admission rule copies the shape StartModuleOperationTask signs. The durable form is core-side: core's Inspected would 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

  • Codex at xhigh, twice, plus security, architecture and an executing bug-hunter.
  • They found, and this PR fixes:
    • an earlier seal-based classification that admitted person-owned shapes, replaced by the issuer allow-list;
    • a hand-built inspection, replaced by core's issuer read-back;
    • an admission split across two RPCs;
    • a Connection option naming the capability header;
    • credential-before-validate ordering;
    • 503 versus 401 classification for uninstalled modules;
    • gaps in the guard tests' mint detection.
  • Antoine: host code. Review requested.

Live

Local run with CLI 0.1.207 and this head (3b16ae2d3) as the host. A solution's release publisher mints at /modules/_operation-context and posts its bundle to /v1/<alias>/definitions:publish on the workload endpoint, with no bearer.

  • Admitted: the gateway logged reason=workload_operation_admit naming the alias, the tenant, the task, the solution's own installation as seal, and presenter local. The module created 5 definitions.
  • Replay after a full reboot: nothing created; same digest.
  • A bundle naming another repository: the gateway admitted it and the module refused it with 403. Authorization stays the callee's.
  • Target not yet installed in the tenant: 403 target_not_installed, then admitted once it was installed.
  • Accounts paused: 503 authority_unavailable at the 2 s bound. 200 once it resumed.
  • Refusals through the running gateway, all 401:
    • the operation context on the public rest endpoint;
    • a person's StartTask capability presented with no bearer on workload (owner_not_service);
    • a context minted for another audience;
    • two carrier headers (capability_not_single);
    • no carrier;
    • a Connection option naming the carrier (connection_names_capability).
  • The person bearer path with a capability: unchanged. 200 once the person is entitled.

Not verified live (unit-tested):

  • the remaining refusal reasons;
  • a 429 from the limiter;
  • cell mode (presenter through the mesh);
  • a second declared module.

Second consumer

Any other solution with a release-declared headless operation uses the same arm and copies nothing. Any other edge would call AdmitWorkloadOperation rather than reimplement it. Modules keep calling each other directly in the mesh.

🤖 Generated with Claude Code

saurabhmaheshwari-obin and others added 17 commits October 24, 2026 17:23
…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>
@saurabhmaheshwari-obin
saurabhmaheshwari-obin added this pull request to the merge queue Oct 25, 2026
Merged via the queue into codefly-dev:main with commit 90cd1f4 Oct 25, 2026
24 checks passed
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>
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