Skip to content

feat(accounts): a person's parent names its module's operation bindings - #1219

Closed
lukazhupa wants to merge 2 commits into
mainfrom
start-task-operation-bindings
Closed

lukazhupa wants to merge 2 commits into
mainfrom
start-task-operation-bindings

Conversation

@lukazhupa

Copy link
Copy Markdown
Contributor

What

StartTask takes operation_bindings: binding ids of the audience module. The host
resolves each one the way the module's delegated exchange will
(ModuleOperationAudience: the binding as installed at the organization, narrowed
by that installation). It adds the binding's invoke scopes beside authority_scopes,
keeps each resource only if the person holds it, and refuses by name a binding the
person holds nothing under.

Why

A module that exchanges through a source binding must today name the source ids in
the parent it requests for the person. Those ids are per organization and live in
the installation, so a consumer has to learn them first, and a static configuration
value works for one organization only. With this change the consumer names the
binding, and the ids come from the installation. The parent then carries exactly
what the exchange will redeem.

Classification

Fix at the owner. The installation already holds the narrowing and the exchange
already resolves the binding; the parent's mint now uses that same resolution
instead of each consumer re-deriving it.

Reuse

  • Service.ModuleOperationAudience: the delegated exchange's binding resolution, unchanged.
  • resolveAuthority and workContextScopes: the per-permission check every mint makes.
  • The rest of StartTask runs unchanged on the expanded scopes.

Deliberately not added: a discovery read for consumers to learn source ids, or a new
slot mode in core or the CLI.

Limits

  • Direct human-owned mints only. An actor's mint and a resource-confined mint refuse
    operation_bindings, because their ceilings are decided on caller-named scopes.
  • Invoke scopes only. A lookup-only parent still names its scopes.
  • One authority read per resource id, once per parent.

Verification

  • New adapter tests cover:

    • the parent carries the installed scopes, and the exchange redeems them;
    • a scope named twice is carried once;
    • refusals for an uninstalled binding, for an installation narrowed away from the
      binding's sources, for a person holding nothing under it (by name), and for an
      actor's mint.

    Each fails with the change reversed. Disabling the expansion fails 4 tests;
    disabling deduplication gives a duplicate-scope refusal.

  • go test ./pkg/business/... ./pkg/adapters/... ./pkg/cataloggen/... passes.

  • Regenerated with the documented pipeline, with the unchanged tree regenerated
    clean first:

    • codefly generate proto (CLI 0.1.185);
    • go generate;
    • codefly generate contracts and runnables, both clean under --check.
  • The naming gate and scripts/check-boundaries.sh are clean.

Not verified

  • No composed run, and no consumer sets the field yet. The published Go client
    needs regenerating first.
  • codefly ci run was not run locally; CI covers it.

🤖 Generated with Claude Code

https://claude.ai/code/session_012qqzbsVZ4vDaPMecQ4FT8w

StartTask takes operation_bindings: binding ids of the audience module. The
host resolves each one the way the module's delegated exchange will
(ModuleOperationAudience: the binding as installed at the organization,
narrowed by that installation), adds its invoke scopes beside
authority_scopes, keeps each resource only if the person holds it, and
refuses by name a binding the person holds nothing under. So a consumer
names the binding and never its resource ids, which come from the
installation. Direct human-owned mints only.
…tract

npm run generate -- --force with codefly 0.1.185, so library.codefly.yaml
records the contract digest the package now publishes.
@lukazhupa
lukazhupa marked this pull request as draft October 22, 2026 20:10
@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Independent review (fresh context, read at 8d83e9d6e against main, nothing run). Verdict: safe, do not merge as is.

No authority widening. The binding's scopes join authorityScopes and pass through the ordinary workContextScopes and the final resolveAuthority over every permission, so a binding never adds more than the caller could have named explicitly; resource ids come from the installation, never the request; a revision moving between the pre-filter and the final check refuses rather than widens. Actor and resource-confined mints are refused before any resolution; API keys, impersonation and forwarded sessions reach the same checks as before.

Major, fix before merge. start_task_operation_bindings.go:73, 107-118 against delegated_read_audience.go:219: a parent narrowed to part of a binding cannot be exchanged. The exchange asks for the binding's full invoke scopes and attenuateEffective narrows only a scope naming no ids, so a binding over sources [A, B] held at A only mints a parent with [A] and every exchange then fails as "audience exchange widens authority". Same when the person holds one of a binding's two scopes. Fails closed, but the PR's promise that the parent carries exactly what the exchange will redeem holds only for "holds all of it", and the error surfaces at the wrong step. Either refuse unless the person holds the whole binding, or make the exchange intersect with the parent; held == 0 matches neither.

Major/minor. :69: two scopes of one resource kind break the exchange, since de-duplication is exact proto.Equal and core's ScopeContained decides on the first scope of that kind; a caller scope source_operation:read [X] plus the binding's invoke,read [Y] mints and then refuses on exchange.

Minor. :88 pre-filters with mintContentReads where the mint uses the wider eligible, so a content-read binding held through a grant or share is refused as "holds nothing" although the mint would admit it. :107-115 runs one resolveAuthority read per resource id inside the authority span (bound ~32,000 reads per request). :52 builds an unauthenticated ModuleCaller, so module-caller refusal texts (another tenant's id, raw database errors, "not a registered module principal" for a solution audience) now reach people. Dropped ids and scopes are dropped silently and refusals in withOperationBindingScopes are not logged, unlike the exchange path. saas-sdk/generated/library.codefly.yaml:55 moves generated-by.codefly from 0.1.191 back to 0.1.185: regenerate with the newer CLI.

Tests missing. Partial hold of ids and of scopes (the fixture binding has one id, which hid the major), a resource-confined mint refused, another module's binding name, a same-kind non-equal caller scope, a headless-only binding, service == nil.

Interface consequence (from the authority work): the person shape of the interface's Issue would need the same operation_bindings field, additively, or the mini gateway and the host drift the day this lands.

antoinetoussaint-byte pushed a commit that referenced this pull request Oct 23, 2026
…tates the hop rule

The copied interface protos gain a solution's roles on the sketch manifest;
digests updated; regenerated with codefly generate proto. The suite dependency
moves to the head that states the hop rule (a module hops only on a person's
token addressed to it and names the binding it exercises) and the permission
matrix (skipped here until this fixture states one). The harness names the
workload's module. Suite: 5 of 11 lines green, one skipped; the hop line is red
until childHop enforces the parent's audience and names the binding, which
follows the person-issue half of #1219.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
antoinetoussaint-byte added a commit that referenced this pull request Oct 26, 2026
…tates the hop rule

The copied interface protos gain a solution's roles on the sketch manifest;
digests updated; regenerated with codefly generate proto. The suite dependency
moves to the head that states the hop rule (a module hops only on a person's
token addressed to it and names the binding it exercises) and the permission
matrix (skipped here until this fixture states one). The harness names the
workload's module. Suite: 5 of 11 lines green, one skipped; the hop line is red
until childHop enforces the parent's audience and names the binding, which
follows the person-issue half of #1219.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
crisfiorini-oss pushed a commit to crisfiorini-oss/module-saas-starter that referenced this pull request Oct 26, 2026
…g 1, with the gap list (codefly-dev#1217)

* fix(accounts): register installation REST handlers

* style(accounts): apply canonical test import grouping

* fix(accounts): fence invocation against the admitted source declaration

* fix(accounts): export the optional declaration fence to consumers

* feat(accounts): serve the published host interfaces as adapters; the policy sidecar; the gap list

accounts declares it implements the four published host interfaces (authority,
composition, installation, session at 0.1.0), carries their protos under
proto/ beside its own (bindings via codefly generate proto), and serves them as
adapters over what it already does: Keys and Check over the Work Context
authority; Issue by owner shape over StartTask, StartInstallationTask,
StartChildSession and ExchangeAudience; List over ListInstallations; SignIn over
fixture Authenticate. Where no path exists the adapter refuses by name:
workload-owned Issue (gateway workload endpoint), ApplyManifest (signed
delivery), Install and GrantToTeam (target and role resolvers).

The interface protos carry no saas.policy option on purpose; the host's policy
for them is business.InterfaceMethodPolicies, read by RPCPolicies when a
descriptor carries none, with the catalog descriptions beside it. The
solution-side InstallableService is not served.

Known red, by design of this branch (HOST_INTERFACE.md): the service catalog
assumes one proto package and Connect bindings are keyed by short service name,
so cmd/service-catalog refuses the four packages and the registrations cannot be
regenerated; the drift tests fail until both are generalised. go build and the
policy tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(accounts): the host interfaces under host-owned names

The published interfaces now carry the host's names (publisher codefly.dev,
packages host.*.v1 under proto/host/), so this repository carries no consumer
name in the copied protos or in anything generated from them. Policy sidecar,
adapters, bindings and the design note follow the rename. go build and the
policy tests pass; the catalog gaps of HOST_INTERFACE.md are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(accounts): the last consumer names in comments and the module declaration

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(accounts): regenerate the authorization matrix under the host-owned names; two comments

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(accounts): gap 7 closed in the host interface note

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(accounts): the catalog carries the implemented interface packages; registrations resolve by full name

The service catalog has one api_package, the service's own, and now lists the
interface_packages it implements beside it (catalog.proto field 9). The
compiler accepts a procedure of either; the validator likewise; the frontend
catalog, the gateway route aliases and the mesh gates read the API package
only, since an implemented interface is not a frontend plugin's client and has
no compatibility alias. cmd/connect-registration resolves bindings by full
service name first (two services may share a short name across packages) and
imports each implemented package's generated Go and Connect packages under
derived aliases. The authorization matrix counts services by full name.

The adapters now enforce the floors their sidecar policies declare (org
member for a person-owned Issue and for List; org admin for Install and
GrantToTeam), which the lockstep test requires. The team grant names its
organisation, as the interface now does.

The naming gate skips generated/service-catalog.json, which crossed its 512 KiB
scan cap at 40 services: every string in it is a scanned proto identifier or a
scanned Go description. Same reasoning as its descriptor skip; a tool fix,
named for review.

Catalog, registrations, authz catalog, routes, matrix, topology and frontend
catalog regenerated. go build, the business, adapters and cataloggen tests and
the naming gate pass. HOST_INTERFACE.md: gaps 1, 2 and 9 closed; 3 to 6 and 8
stand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(accounts): ApplyManifest declares no audit event; the delivery path emits host_binding_applied itself

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(accounts): the person shape is the caller's own session; protos follow the published interface

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(accounts): the harness, step by step, from the integration tests this package already has

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(accounts): the rung 1 harness — the interface conformance suite against accounts; the digest check on the copied protos

The harness boots the test store, seeds the dev-admin fixture, wires the sign-in
path as the business tests do, configures the Work Context authority against the
store, serves the Connect listener on an ephemeral port and runs the published
interface suite (github.com/codefly-dev/authority/libraries/conformance/go,
descriptor-driven, linking no generated package) over gRPC. Opt-in through
HOST_INTERFACE_CONFORMANCE=1 because it boots a store.

Green on the first run: Keys; sign-in; an unknown fixture token refused by name.
Red, recorded as the gap list in HOST_INTERFACE.md: a module must be installed
per organisation before it is an audience; the host has no effective-permissions
read for an empty ask; an unknown audience is refused PermissionDenied where the
contract says NotFound; workload Issue, ApplyManifest, Install, GrantToTeam as
before. Adapters: a fresh task id per person-owned Issue; the session returns the
access token as the bearer; List is an administrator's read over
ListOrganizationInstallations; an unknown fixture person is NotFound.

The copied interface protos carry DIGESTS.sha256 and a test that fails by file
name when either side drifts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(accounts): the person line of the host interface suite is green; composed modules are installed everywhere; a hidden refusal names its reason

Composition installs a module at every organisation (contract decision of
2026-10-22): business.ComposedModuleInstallations answers the live module
authority and the seal store for a composed module's installation, held by
every member, and wraps the Postgres store for everything else. A control-plane
membership read, WorkContextOrgMember, sits beside the other seal reads. The
harness wires it; production keeps its reads until the registry derives these
rows itself.

An uncomposed audience is refused NotFound by name before any mint, through
ModulePrincipalID against the derived registry. Check is authenticated by the
token it checks and is public like Keys. mapWorkContextError takes the context
and logs the one case the caller learns nothing, "cannot issue Work Context",
which until now had its reason recorded nowhere.

Suite: 4 of 10 green (Keys, sign-in, the whole person line, every refusal with a
reason). The six red lines are each a host change of their own and are the gap
list in HOST_INTERFACE.md, with one new gap: the host authenticates a child hop
by the owner's bearer where the contract authenticates the holder of the parent
token.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(accounts): the counted tripwires move for a public Check

Check is authenticated by the token it checks, so it leaves the
impersonation-restricted set (62 to 61) and joins the public routes (56 to 57).
Each baseline says why in its comment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(accounts): a workload is issued behind the interface from its credential alone; an operation context names its binding

ModulePrefixOfWorkload names the one composed module a workload credential
authenticates, through the execution reviewer and the existing reverse lookup
from a reviewed workload to the principal declaring it; no caller types a module
name. The interface's Issue, for a workload, is the host's operation context to
an installed audience when a binding is named, and the module's own context to
this surface otherwise; an undeclared binding is NotFound by name.

The interface's Issue is authenticated by the request's shape (a person by the
bearer, a workload by its credential, a child hop by the parent token and the
actor's credential), so both interceptors let a bearer-less call of that one
method reach the adapter, which refuses a bearer-less person by name;
a bearer, when present, is verified as on every other method.

An operation context now names the binding it exercises: the seal source
resolves module-operation/<principal>/<binding> from the installed grant at the
revision of its terms, and the operation-task mint seals it. Until now a module
operation context carried no binding, which is what left every runtime hop
"stale" at the revision check.

A composed installation's scope is the declared grant itself, since the manifest
is the ceiling. Suite: 5 of 10 green (Keys, sign-in, the person line, every
refusal with a reason, the workload line).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(accounts): a child hop behind the interface, by the parent token and the actor's credential

childHop verifies the parent as issued by this host, names the acting module
from its credential, re-reads the owner's authority as the owner (the parent
token is what proves the owner), refuses a widening hop as PermissionDenied and
derives the hop with core; the host's own child session is authorized by the
owner's bearer, which the module acting does not hold. The hop names no binding:
core keeps the owner's seal installation on a hop and requires a named binding to
be granted within it, and a module's binding is granted within its own
installation, an open decision recorded on the interface repository.

The bearer-less pass-through on the interface's Issue applies only to a call
carrying no credential at all; a forwarded identity is admitted as before. The
operation-task mint names a binding only when the authority has one.

The harness composes documents, org-directory (the page module, one workload,
one binding to documents) and model. Suite: 6 of 10 green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* copy the composition interface's signed-carrier messages into the host

ApplyManifestRequest carries the delivery as signed: the manifest document,
the DSSE envelope over it, and the delivering workload's credential.
Regenerated with codefly generate proto over the template's saas and host
inputs; DIGESTS.sha256 updated. The adapter behind it is the next change.

* feat(accounts): a person's parent names its module's operation bindings

StartTask takes operation_bindings: binding ids of the audience module. The
host resolves each one the way the module's delegated exchange will
(ModuleOperationAudience: the binding as installed at the organization,
narrowed by that installation), adds its invoke scopes beside
authority_scopes, keeps each resource only if the person holds it, and
refuses by name a binding the person holds nothing under. So a consumer
names the binding and never its resource ids, which come from the
installation. Direct human-owned mints only.

* chore(saas-sdk): regenerate the library from the accounts/connect contract

npm run generate -- --force with codefly 0.1.185, so library.codefly.yaml
records the contract digest the package now publishes.

* chore(accounts): the interface protos at fdb7ea6 and the suite that states the hop rule

The copied interface protos gain a solution's roles on the sketch manifest;
digests updated; regenerated with codefly generate proto. The suite dependency
moves to the head that states the hop rule (a module hops only on a person's
token addressed to it and names the binding it exercises) and the permission
matrix (skipped here until this fixture states one). The harness names the
workload's module. Suite: 5 of 11 lines green, one skipped; the hop line is red
until childHop enforces the parent's audience and names the binding, which
follows the person-issue half of #1219.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(accounts): run the interface suite at 13 lines, the hop rule and delegate chains

The suite dependency moves to the head that states the hop rule and delegate
chains. Accounts passes 5 lines, skips 3 this harness does not yet state (matrix,
chain, installed solution) and fails 5; HOST_INTERFACE.md records the person
half of the hop and the re-derivation the hop needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: bind authority child issuance to its authenticated actor

* feat(accounts): expose read-only fixture identity discovery

* feat(accounts): serve what the authority's edge and hop call

The host interface protos at codefly-dev/authority 06d0d3b, copied
unchanged for the four declared interfaces and regenerated, and the
adapters the edge and the hop need behind them:

- implements moves to accounts/connect, the listener that mounts the four
  services; accounts/authority admits only the internal module-authority
  procedures, so every interface method was refused there. A test holds
  the declaration to the endpoint whose contract surface serves it.
- Issue for a person with an empty ask is everything they hold at the
  audience (gap 11): candidates from the audience's declared content and
  every action the host names on it, each kept by the check every mint
  makes, then decided again by StartTask.
- SessionService Current (the perimeter-verified session, no bearer back,
  refused once revoked) and ListFixturePeople (the development
  validator's ordered allowlist through the read-only identity lookup;
  none where fixture sign-in is off).
- Check applies a named method: no module has a delivered method policy
  here (gap 4), so it is refused by name, or reported as the advisory
  refusal beside the verified capability for a hop in warn mode.
- ListResources and the delegation methods are classified and answer
  Unimplemented by name.
- The dev-admin fixture carries the authority fixture's people, tokens,
  organisations and teams; fixture role assignments may name a person by
  email, so a composing workspace's fixture assigns its module roles.
- An implemented interface's REST mapping is not on this host's surface:
  the public OpenAPI renderer drops it, as the gateway route compiler
  already did.

The conformance suite at 06d0d3b: 9 pass, 4 fail, 6 skip (was 5/7/7),
table in HOST_INTERFACE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(accounts): a composing workspace's fixture grants, and List naming its target

Composed run on the host interfaces (edge and hop against accounts):

- Installation List answered the presence's row id as an installation's
  target, so a frontend binding to the installed solution by name found
  none. It is the presence's route alias now, the name Install takes;
  a row whose presence is gone keeps the id.
- The fixture people's roles at composed modules cannot be named here,
  and the composing workspace had no way to state them: the fixture
  directory is found from the service's own checkout. A workspace now
  states them in a fixture-grants.json document (roles and assignments
  in the fixture's own shape), which the seed adds to the selected
  fixture and validates with it. The module ships an empty default, and
  the shipped-default gate counts a JSON document as one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): admit declared hosts at a meshed cell's egress addresses on every guarded egress path

On a cell whose ambient mesh carries egress, the mesh answers DNS for each
declared egress host with an address it allocated (Istio's auto-allocated
ServiceEntry addresses, 240.240.0.0/16 by default). That is reserved space, so
every guarded egress path refused it: InvokeSourceOperation failed on staging,
and webhooks, the Slack notifier, the crawler, object storage and Client ID
Metadata Document fetches would fail the same way.

One rule, auth.AdmitsDestination, now answers for every guarded path: the
resolved address is public (auth.IsPublicDestination, unchanged), or the
deployment asserts its mesh (internal-transport/mesh-protected=true) AND the
client asked for a hostname declared in internal-transport/mesh-egress-hosts
(on its port) AND the address lies in internal-transport/mesh-egress-range
(240.240.0.0/16 when empty; bounded to 240.0.0.0/4 or 2001:2::/48). An IP
literal, an undeclared host, and every loopback, link-local, metadata or
private address stay refused. Hosts or a range without the mesh assertion, or a
malformed entry, refuse startup.

It is decided on the address actually dialled: auth.GuardedDialContext resolves
the requested host and dials each answer as a literal, with the Control hook
judging that literal against the host:port the client asked for. The callable
source client (operation calls and OAuth token exchanges), the crawler, the
object-store connector and the metadata fetch use it; the webhook policy (and
the Slack notifier sharing its client) keeps its resolve-and-pin dial and asks
the same rule.

Reused: the shared classifier, the mesh assertion (meshtransport.Protected),
the per-package guard seams and egresstest. Not added: a second classifier or a
second allowance mechanism; the declared hosts mirror the environment's egress
declaration by hand because Codefly does not deliver it to the service yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): the fixture-grants document is YAML, as Codefly reads documents

Codefly reads a configuration document from <group>.yaml (or .secret.yaml);
a .json file in a configuration directory is not a group, so a composed run
refused accounts with "no configuration found for fixture-grants". The
shipped default and the gate's document default are YAML, which also keeps
the fixture's own shape and allows comments.

HOST_INTERFACE.md's "not verified" note named a composition; it now says
"a composition that runs saas as the authority host", as the naming gate
requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): a workload's issue through a binding is addressed to the binding's audience

A hop's ForModule issues as the module's workload through a binding it
declares and names no audience: the binding carries it. accounts refused
every such issue with "an issue names its audience", so a module's own
work through its binding (a vocabulary install at start, say) never got
a Work Context. The audience is now the declared binding's; every other
shape still names its audience, and an undeclared binding is NotFound by
name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): admit the environment's generated egress hosts, inert without the mesh

Codefly now generates the workspace group egress/HOSTS from the
environment's egress declaration and refuses an authored one, so accounts
declares the group and reads its hosts from it instead of a hand-kept copy
in internal-transport/mesh-egress-hosts, which is deleted.

Every environment declares its egress whether or not its deployment is
meshed, so declared hosts without internal-transport/mesh-protected=true
no longer refuse startup: they are inert, and a declared host resolving
into the mesh range is refused there like any reserved address. A
hand-set mesh-egress-range without the mesh assertion still refuses
startup. The mesh cases now install the declared host on unmeshed
deployments too, so every guarded path proves the inert case end to end.

The configuration gates treat egress as CLI-owned, like artifact-cache:
no authored default, and its keys are the CLI's to name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): the composed-installation stand-in is a test package; the defaults gate knows document groups

Two gates red on this branch, each fixed where it points:

- TestEveryModuleCapabilityPathReReadsLiveAuthority flagged
  ComposedModuleInstallations (LiveModuleAuthority, composedModuleOf)
  for reading the declared registry directly. It is no capability path:
  it stands in for the live module authority in the host-interface
  conformance harness, and only that test wires it. It moves to
  pkg/business/compositiontest beside executiontest and
  installationtest, reading the declaration through the service's
  accessors that exist to stand in for it; production reads its
  installations from the seal store, unchanged.
- TestEveryDeclaredGroupShipsALocalDefault knew only .env and
  .secret.env, while Codefly reads a configuration document from
  <group>.yaml. It now counts a YAML document stating at least one key,
  and fixture-grants.yaml gets its repo-root symlink like every default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): an unwitnessed envelope narrowing is refused by name, not as an internal error

A module envelope write that changes what an existing principal is
granted is a narrowing, and with no policy log the host refuses it
(ErrPolicyLogUnreachable). PutModuleEnvelope answered that as Internal
"cannot write the module envelope", so a composed run that added one
binding to a principal retried the write for ever with no reason. It is
FailedPrecondition now, saying the policy log's witness is what the
write needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): a child issue through the actor's binding is addressed to the binding's audience

A hop's ForChild names the parent, the acting workload and the binding,
and no audience; accounts refused it with "an issue names its audience"
before the child path could answer. The audience is now the actor's
declared binding's, as for a workload's own issue, so the child path
answers what it holds today: a hop naming its binding is Unimplemented,
waiting on where a cross-module binding is held (gap 14), by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(accounts): a module's hop through its binding inherits its owner's current grants

Issue with a parent token, the acting module's workload and the binding it
exercises was refused as Unimplemented ("waits on core deciding where a
cross-module binding is held"), which blocked every module calling a
module on a person's behalf through the host interface (gap 14).

Per the owner's decision (child issuance A), the child is the owner's
current grants at the binding's audience within the binding's declared
scopes, with the parent's actor chain replayed and the acting module as
the last hop naming the binding. The binding is the one this host already
holds: the module principal registry as installed in the organisation
(ModuleOperationAudience). The child is sealed to the installation that
binding is granted within, so core's binding-within-installation rule
holds by construction. It descends from the parent's session and never
outlives the parent.

A narrowed parent stays narrowed: for every kind the parent carries, in
its authority scopes or any hop's grant, the child is held to the least of
them, and carries those narrowings in its own authority scopes for the
next hop. A kind the parent never carried is no narrowing, which is how a
chain crosses audiences.

Refused by name: an undeclared binding (NotFound) and a parent addressed
to another module (PermissionDenied), both before the parent is verified
so a single-use parent is not spent; an audience other than the binding's;
an owner holding none of the binding; an ask beyond the owner's grants;
an ask beyond the parent's narrowing; headless; a non-person owner. The
revision check accepts the binding spelling this host seals; a module hop
replayed ahead of it stands while its module is installed.

The conformance harness states a delegate chain and a lookup case: 11
pass, 4 fail, 4 skip at authority 06d0d3b (was 9/4/6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): an envelope entry that only gains is a grant, by the installation-consent rule

The envelope write skips the policy-log witness only for a pure grant,
and no deployment has a log transport, so whatever it classifies as a
narrowing cannot be written at all. It compared a kept principal's JSON
member by member and required every array to be identical, so a module
that declared one more content kind (documents' resources gaining
documents.collection and documents.ingest), or one more action on a
binding's scope, read as a narrowing and could not be granted; a local
run had to wipe its database to move its own composition's ceiling.

A kept principal is now compared decoded, by the rule this host already
holds an installation's consent to its ceiling with
(InstallationScopeWithin over FullInstallationScope): allowlists and
scope actions may grow, resource ids may widen or drop, artifact
approvals and audiences must hold. The tenant must be unchanged, a
cross-tenant grant kept, and every binding's delegation mode kept, as
before. A removal, a retarget or resource ids appearing on an
unbounded scope still needs the witness. The JSON member walk is
deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(accounts): a fixture declares its installations, performed by name once delivered

A fresh local run came up with nothing installed: each organisation's
administrator installed the wiki and every module by hand through the
install API. A fixture now declares `installations` (org, target, role,
owner), in the fixture file or in the composing workspace's
fixture-grants document, since a target's name and a module's role are
the composition's words. The seed refuses, by name, an owner the
fixture does not make an owner or admin of the organisation.

Nothing is written directly. business.Service.InstallTargetByName
resolves the target from the organisation's catalogue
(ListAvailableSolutions, the one an administrator installs from) and
the role from its role catalogue, then installs through InstallSolution
with every check it makes; a held installation (any status but revoked)
is returned unchanged. The host interface's Install now uses it (gap 5;
GrantToTeam stays Unimplemented).

Targets arrive by delivery after the seed, so the declared
installations run after every delivery pass
(SolutionHostBindingReconciler.AfterEachPass), as their owner: an
undelivered target waits and says so once, any other refusal is
reported with the pass, naming the installation. A host with no
delivery surface says it installs nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): one sealed binding spelling and one module hop rule, as the interface states

The host interface's conformance suite names the binding a child exercises
<organisation>/<module>/<binding>. accounts sealed module-operation/
<principal>/<binding>. That spelling is deleted rather than accepted beside
the new one: ModuleOperationBindingID, the seal source, the operation
context's shape check and ConfirmModuleHopBinding read only
<organisation>/<module>/<binding>, for a workload's operation context and a
hop's child alike, and an id naming another organisation confirms nothing.
The organisation is a canonical UUID, which tells the id apart from a
source delegation's and an installed binding's. The operation-context mint
now carries the module's prefix, and refuses a binding whose prefix does not
name its principal.

A hop through the interface that names no binding is refused by name before
the parent is verified ("a module's hop names the binding it exercises"),
as the local authority refuses it. The old shrink-from-parent child is
deleted: a module hop has one rule.

ExchangeDelegatedOperationAudience stays: composed modules still call it
through the Go SDK's module-authority client. HOST_INTERFACE.md records it.

Conformance unchanged at 11 pass, 4 fail, 4 skip (authority 06d0d3b).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): persist and enforce signed method-policy delivery

* fix(accounts): enforce delivered solution HTTP route policies

* fix(accounts): an installation's issue verifies the installation it names

The interface's installation Issue never read the installation id: it called
the headless mint with no organisation, installation or task, behind a
comment claiming the id was checked after the mint. An installation id is
not a credential. The issue now requires the actor's workload credential,
resolves the module it proves, holds the organisation to that solution's
pinned tenant, and refuses, by name and before any mint, an installation
that is not that solution's own active installation in the organisation.
The mint is the existing headless installation mint, past its internal
credential check; an empty ask is what the installation's authority admits
at the audience.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): a solution's delivered roles decide what a person holds through it

Accounts signed and stored a solution's delivered roles and scope ceilings
and read neither, so a person reached a solution, and a module through it,
only by organisation roles: the wiki's viewer and editor meant nothing here,
and a person holding no organisation grant at documents was refused every
documents call the wiki made for them.

What a person holds through a solution is now its roles, read from the
applied signed authority document (business.PersonSolutionRoles): installer
at the installation they own, and the installation's standing role through
each team it is exposed to that they belong to, bounded by the delivered
ceiling on the solution's own kinds and by its bindings on the kinds it
consumes. A person's issue at such a solution is that grant on its own
kinds; a hop through a solution on the path (rule A) is those roles within
the binding, with a solution audience's own grant met in; a parent issued
at a solution stands while its roles still grant what it carries.
Organisation roles answer only where no solution on the path declares
roles, and keep administering the organisation.

Install takes the solution's own role names: the installation's standing
role is a role this host derives per organisation, solution and role (the
existing derived-role seam, outside the catalogue), and List reads the
solution's role name back. A fixture installation names the teams it is
exposed to, granted once the installation admits it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(contracts): export the complete host interface with its owning CLI

* fix(accounts): ListResources answers which resources a capability covers

ListResources answered Unimplemented, while modules are told to ask it which
resources of a kind a person may act on (the token carries the kind alone).
It now reads the capability back for the asking audience without spending
it, requires the capability to hold the action on the kind, and answers from
its owner's grants now: a person's solution roles where a solution on the
path declares roles; otherwise every resource for an organisation-wide
grant, by the check every mint makes, or the ids of their grants limited to
one resource (a new read of role assignments). A capability naming ids
covers only those the grants cover; any other owner is bounded by the
capability alone, as the local authority answers.

The procedure's catalog description is regenerated through go generate, and
the control-plane call-site inventory through its test generator (the
applied-delivery read moved into its own function in the previous commit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): a solution's standing role is granted at its authority root

The previous commit made an installation of a role-declaring solution hold a
host-derived role, and GrantScope refused it: every derived role was one
the module-access derivation confers with a named cause, never by hand. So
installing the wiki with one of its own roles failed at the agent's standing
grant, as the conformance install case showed.

A derived role whose provenance is a solution's role is now granted only at
a solution's authority-root node in the organisation it was derived for: the
installation's standing grant and the teams it is exposed to. Every other
derived role keeps its rule, and a solution role is still never assigned
organisation-wide or shared. A database test proves the root grant and the
refusal elsewhere, and fails without the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(accounts): solution roles, ListResources and installation issuance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): pass person bindings through the host authority adapter

* fix(accounts): a team is exposed with the solution's own role

An installation made before solution roles were read holds a catalogue role
named after one of the solution's (the built-in viewer) as its standing role.
Exposing it to a team granted that role, which does not permit using a
solution, so every fixture reconcile pass on an existing store failed with
"role does not permit using a solution". The solution's own role did not
carry it either, so a fresh install would have failed the same way.

A solution's role now permits using the solution, which is what holding one
of its roles means. ExposeHostInstallation grants a team, for a solution that
declares roles, the solution's own role of the standing role's name, through
the existing grant path and its root-node rule; the standing grant is not
rewritten and no database wipe is needed. A database test starts from an
installation holding a catalogue "viewer" and ends with the team exposed and
its member holding the solution's viewer grant; it fails without either half
of the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): one exposure rule, and its refusal names why

c86c750d put the solution-role rule in ExposeHostInstallation, beside the
grant rather than in it. GrantInstallationToTeam naming no role now resolves
the installation's role itself (solutionExposureRole), so every exposure
naming no role, the fixture installer's and the interface's alike, takes the
solution's own role of the standing role's name, and the helper beside it is
deleted. When the solution's role cannot be used, the standing role is
granted as before and the refusal says why: no signed delivery, an applied
generation delivered without its authority document, no solution roles, or
no role of the standing role's name.

The test goes through the fixture installer against the real signed
delivery path: an installation holding the built-in viewer, refused with the
reason while the solution's delivery carries no authority document, then
exposed to its team once the solution delivers its roles, its member holding
the solution's viewer. It fails without the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): declare host audit and documentation ownership

* fix(accounts): derive fresh-store authority from signed manifests

* fix(accounts): isolate composition delivery outcomes by binding

* fix(ci): track registry publication and format accounts imports

* fix(accounts): the authority interface holds each issue and check to its caller

Review r12 of #1217. Check held a named method to the owner's authority
scopes, so an actor delegated read passed a method requiring write; it now
holds it to the call's effective scopes, the last hop's grant.

Issue authenticates each shape before it answers for the composition, so a
bearer-less caller learns nothing of which modules are composed and a
workload's own context (module-capabilities) is reachable. A workload's issue
honours its scopes, operation and lifetime within the installed binding's
headless grant and refuses anything beyond it, or a mismatched audience, by
name. An exchange forwards the organisation and reads an empty ask as the
parent's effective scopes; at the parent's own audience the issue is a
renewal. A delegation names its gap, a refused credential names its cause.

The hop refuses a parent narrowed to nothing of a kind and runs the audience
guards every mint runs; no binding may be addressed to module-capabilities.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): verify an applied authority document once, list resources of the asked kind

Review r12 of #1217. Every Check naming a method and every solution-role read
re-read the inbox and re-verified two signatures; the verified outcome is now
kept per binding for the applied generation and digest it was verified at, so
applying another generation is what makes the next read verify again. An
unparsable applied row on another binding no longer fails every answer.

A resource question no longer reports a registered scope node of another kind
as an id of the asked kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(accounts): the host interface after review r12

Gap 3 is closed (workload Issue is served), and the Issue mapping states every
shape as it now is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): admit resource-limited method scopes unchanged

* fix(accounts): project person holdings at each binding target

* fix(accounts): refuse envelope authority absent from manifests

* test(accounts): give the conformance harness every fixture the shared suite needs

The suite at authority aa487dff fails a case outright when its fixture is
missing. The harness now supplies each one, built through this host's own
paths:

- a conformance fixture (testdata) with the dev-admin people plus an Acme
  owner who is not an administrator, a member with no role or team, a
  resource-limited record grant, and the roles a module's users hold;
- three solutions delivered as the CLI delivers them (signed carrier plus the
  authority's signed document through ApplyManifest), installed through the
  fixture's declared installations, acknowledged by the host's own install-step
  claim and finish, then exposed to their teams;
- the installed solution, its workload credential and audience; signed
  solution roles; a module-to-solution tool binding; a delegation; child-scope
  and parent-limit chains; an installation-owned chain; resource cases;
  ambiguous and absent installing organisations; install acknowledgement and
  revocation (the host's uninstall path).

External sources stand in at their seams only: the cluster, the composition's
module authority (one module withdrawn for the absent case) and the policy log.

The suite dependency moves to aa487dff, which takes core to e5014589 (a
descendant of the pinned 0a8d16c5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(accounts): the conformance harness supplies every fixture the suite asks for

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): hold delivered solution roles to the manifest ceiling

* fix(accounts): the module tier proves the calling module, never a shared token

ModuleCapabilitiesService on the `authority` endpoint asked every caller for
the cluster-internal token (CODEFLY_INTERNAL_TOKEN) on top of the Work Context
each method already verifies. A static token shared by every workload names no
module and adds nothing a forged caller lacks, so the endpoint no longer asks
it of the capability surface: each method proves its caller itself — from the
module's Work Context (issued by the authority from the workload credential),
or for MintSourceOperationContext from the pod's workload credential, reviewed
and bound to its build — and its refusal names what was not proven. The
handlers' own requireInternalCredential calls (ListReadableSourceCollections,
CheckWorkContextRecordAccess, the delegated exchanges, the module-parent
authentication) are deleted with it; no flag, no fallback.

MintSourceOperationContext is served on the endpoint, as a declared workload
mint: datasources are the host's and never the authority interface's, so the
module tier is the one place a module can ask for a source's Work Context.
The private listener is unchanged, and the two read-only oracles, which name
no caller, keep the perimeter credential until each is bound to its caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(source-read-sdk): regenerate with codefly generate client; delete the local pipeline

The library was the output of cmd/generate, a local pipeline that ran
`codefly generate client` and then overwrote its bindings with `buf generate`,
pointing policy, jobs and Work Context descriptors at saas-sdk-go while
keeping its own saas/events/v1 copy. A binary linking it beside saas-sdk-go
registered saas/events/v1 twice and panicked at init, and the curated tree
had gone stale against the contract.

It is now plain `codefly generate client --from saas-starter/accounts/authority
--services ModuleCapabilitiesService` output: self-contained, importing no
other SDK, with every datasource seam (ClaimJobs, HeartbeatJob, AckJob,
NackJob, FetchDatasourceBlob, FetchDatasourceFiles, MintSourceOperationContext,
ListReadableSourceCollections). cmd/generate is deleted. The pinned CLI could
not generate from this service at all (it compiled buf.yaml's excluded
`codefly/` tree and named vendored files `vendor/...`); the fix is
codefly-dev/cli 2c6225ad, which generated this tree. The authored transport
exposes InternalGateway for the streaming calls the facade skips, and its
tests send no internal token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(frontend): regenerate collision-free TypeScript SDK exports

Regenerate with CLI df447bbc6cb83b844f871393c284960056fdbeae, the owner fix in codefly-dev/cli#1162. Every generated module has its own namespace; the published SaaS facade and direct imports retain their API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(contracts): export MintSourceOperationContext on the accounts authority endpoint

`codefly generate contracts saas-starter` output after 98ee9e7a served the
source-delegation mint on the module tier; the exported catalog lists the
endpoint's served procedures, so it moves with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(accounts): the host interface protos at authority 0.2.0

The four interface protos re-copied unchanged from codefly-dev/authority at
aa487dff (authority and session 0.2.0: Session gains person_display_name = 7
and organization_display_name = 8; composition and installation change
comments only), DIGESTS.sha256 re-recorded, and everything they generate
regenerated through Codefly with the CI's CLI source pin (8de0f667): the Go
and TypeScript bindings, the raw OpenAPI, the exported contracts and the SDK
client. module.codefly.yaml advertises authority@0.2.0 and session@0.2.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): the host interface behaviour the shared suite reaches

- Session: SignIn and Current name the person (profile name, else email) and
  the organisation for display; a non-member session names no organisation.
- Issue for a person refuses an uninstalled audience as FailedPrecondition by
  name before solution roles read an installation that is not there.
- A delivered solution named by its alias resolves to its own installation,
  never to a composed module's.
- An installation's issue proves its actor before reading the ask, then reads
  the named installation for whose it is: NotFound when unknown, and another
  solution's workload, another organisation, or a pending or revoked
  installation refused by name. Its empty ask considers the actions its
  solution's roles name.
- A person at a solution through one of its bindings carries what the
  solution's roles grant within the binding (was Unimplemented).
- An empty ask carries resource-limited grants on exactly their resources.
- GrantToTeam refuses a non-active installation naming pending or revoked.
- Refusal texts name the interface's reasons.
- The harness's HTTP-route subtest installs its solution before issuing there.

Each fix carries a unit test. Shared suite at authority aa487dff: 37 of 45
pass (was 21 with the fixtures, 13 before them); the 8 still red are listed
with their owner in HOST_INTERFACE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style(accounts): goimports the host interface tests

Standard library, third-party and accounts imports in their own groups, as
the lint phase's goimports requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): a person's ask meets their holdings; a dropped kind stays dropped

Two interface rules the shared suite holds the host to.

An ask is met with what the person holds, action by action: an id they do
not hold is dropped and the rest issued, a kind-wide ask they hold only on
named resources keeps those, and an asked action they hold on none of what it
names is refused by name ("asked beyond what the owner holds"). Resources are
narrowed, never actions, and nothing comes back empty.

Rule A, never recovered: a person's explicit ask records each audience kind it
left out as a denial (a scope naming the kind and no action), and a hop
through a binding denies each kind the owner holds through it that the child
was not granted. The chain's limits are read per kind and per action, so two
actions on different resources each keep their own; a kind narrowed to
nothing is carried as a denial instead of refusing every later hop. Core
already signs and attenuates these rows.

Conformance at authority aa487dff: 40 of 47 direct cases (was 39); parent
per-kind limits and resources by grant are green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(ci): pin codefly CLI 0.1.213

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(accounts): the staleness probe exposes a real team with no member

The shared suite at authority PR #8 (3222e991) grants the installed solution
to a team the host names (Fixture.UnexposedTeam) instead of a made-up id. The
harness fixture seeds "Unexposed", an Acme team with no member, through the
host's own seed, so the exposure moves the organisation's authority and
grants nobody anything. The conformance dependency is pinned at that head.

Conformance: 42 of 47 direct cases (40 of 45 shared); "a callee refuses what
does not verify, by name" is green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(accounts): the host interface suite at 40 of 45, and what remains

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): the Work Context read oracles prove the calling module, never a shared token

CheckAuthorizationRevision and AuthorizeEvidenceRead were the last methods on
the module `authority` endpoint that asked for CODEFLY_INTERNAL_TOKEN instead
of identifying their caller: a static token every workload holds, which names
no module. Each now authenticates the calling module with the proof the module
capability surface uses since 98ee9e7a — the module's own Work Context, issued
by the authority from its workload credential, verified and held to its current
grant and installation (service.AuthorizeModuleCapability). The internal token
is not accepted in its place on either listener.

Refusals are named: no single module Work Context, or the token alone, is
Unauthenticated ("the calling module is not identified ..."); a context that
does not verify is Unauthenticated; one naming an undeclared or uninstalled
module is PermissionDenied; a host with no module authority is Unavailable.
With no method left that names no caller, the endpoint interceptor asks no
perimeter credential at all and ModuleAuthorityAuthenticatesCaller is deleted.

Callers must present their module Work Context: annotations already does;
document-store, runtime (main), model-gateway (main, via saas-sdk-python) and
robin (tool-owner, chat replay host) send the token alone today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(auth-gateway): the admission fake records under a lock

TestAccountsWorkloadAdmissionAsksTheIssuerWithinItsBound read what the fake
accounts handler recorded while that handler, on the gRPC server's goroutine,
had written it with no happens-before edge; -race failed the 503 case. The fake
now records under a mutex and the test reads a copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): a mirror fetch joins its size watchdog and reads its bound once

The watchdog goroutine guarding a git fetch was signalled but never joined, so
it could still be measuring the mirror — and reading the package-level
maxMirrorBytes — after the fetch returned and while the mirror was discarded.
-race caught it in TestMirrorSizeBoundFailsClosed, which lowers the bound after
an earlier fetch. The bound is now read once per fetch on the caller's
goroutine, and the fetch waits for its watchdog before deciding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): an installation's agent stands in its organisation while its installation does

The seal reads answered only for members, so a capability an installation's
agent owns could not be sealed or carry a revision. An agent now stands while
its installation is active, the agent is live and an owner of record is still
an admin: the conditions the headless mint resolves. Adds the read of the one
active installation an agent acts for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(accounts): an installation acting for itself owns its capability as its agent

No person is acting, so the installation's issue is owned by its agent, kind
agent, with no actor hop and no person's teams. Check and ListResources
re-verify the installation it stands on, since a narrowed standing grant
moves no revision. A hop from an agent-owned parent inherits the
installation's standing grants within the binding (rule A), and stays the
agent's. A removed owner is a PermissionDenied decision, not Internal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(accounts): the installation the suite refuses elsewhere is unrelated to it

conformance-pages' viewer role grants example-records:write, so documents is
not unrelated to it; the suite's refusal case now uses conformance-proposals,
whose roles name its own kind alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(accounts): an installation owns its capability as its agent; the suite at 41 of 45

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): resolve a module principal by its name; the binding is granted to the name

A hop's actor is the module's name (the host interface's decision). The
seal, epoch and revision answers and the revision check resolve a principal
to the declared module by name or registry id; a module named as a UUID is
refused so the two never collide. A module's installed operation binding is
answered as granted to the module's name, with its registry id beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): a hop names its actor by the module's name

The hop through a binding carries the module's name as its actor, as the
binding id already names it; the journal stores the registry id. The seal
source answers a module's own headless context, still owned by its registry
id for the consumer contract, in its owner's spelling, and every other
context by the name, so no module exercises another's binding. Tests cover
each comparison against an impostor module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(accounts): an installation-owned hop names its actor by the module's name

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(accounts): a hop's actor is the module's name; the suite at 43 of 45

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(accounts): module delegations, revoked once and sealed to the grantor's revision

Migration 48 records a person's delegation to a module: tenant scoped,
forced RLS, the grant immutable, revoked at most once and never deleted.
The revocation stamp advances the grantor's authorization revision in
the same statement. Revoking runs under the policy log like every other
narrowing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(accounts): Delegate, GetDelegation, RevokeDelegation and Issue from a delegation

Delegate holds each action to what the person holds now at the module or
an audience its bindings reach. Issue from a delegation proves the
actor is the delegation's module and issues its one hop for the grantor,
the grantor's current grants through the binding cut to the delegation,
with the delegation's id on the hop. Every later hop is cut to each
stored delegation the chain names, re-read live. GetDelegation, like
Issue, is authenticated by the request's shape. Catalog regenerated with
the repository's go generate steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(accounts): a delegation is bounded by its grantor and ends with its revocation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(accounts): delegations behind the host interface, and what still holds the case red

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(accounts): the hop fixture answers no module delegation; the delegation lookup is a reviewed control-plane site

A module's issue from a delegation names no organisation, so its lookup by
id runs under the control plane, as the source delegation mint's does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(accounts): the suite at 41 of 45 with delegations and the agent-owned installation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(accounts): the suite at 45 of 45 with delegations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(store): renumber this branch's migrations above main's 48

main merged 48_retention_index_gaps (#1254), so 48_module_delegations
duplicated its version and 46/47 fell at or below the merge reference's
frontier: the reference gate refuses them, and golang-migrate never applies a
version below a database's current one, so a database already at 48 would have
skipped both silently. 46_authority_delivery, 47_installation_exposure_revision
and 48_module_delegations become 49, 50 and 51; every prose and code reference
to them follows. The SQL is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): every hop of a chain is held to the binding it went through

A capability seals only its last hop's binding, so a child kept verifying
while an upstream binding narrowed or was withdrawn. ConfirmModuleChain
re-reads each module hop against its module's binding to the next hop
(or to the audience), on Check, ListResources, a hop's parent and the
consumer revision check. A module actor named by its name reaches storage
as its registry id (record access, readable sources, the journal).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): a pinned principal is never issued outside its pin

The installation issue let a tenant-pinned solution with cross_tenant be
issued in another organisation while the module mint refused. Both now
read ModulePrincipalGrant.IssuedIn; cross_tenant still lets installed
authority serve other organisations' installations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): with no envelope, grant from the verified manifest bytes only

The registry pass derives the grant from the manifest the signature
covers rather than the applied record that matches its digest, and a test
holds every unverified case (unsigned, no carrier, no pinned keys, an
applied document the signature does not cover) to granting nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(accounts): review r93 repairs; resolve the committed merge markers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts)!: the viewer oracles authenticate the calling module, the viewer travels in the request

CheckWorkContextRecordAccess and ListReadableSourceCollections read a viewer
Work Context from the header and identified no caller: any workload the mesh
let reach the authority port, holding a viewer capability, could ask record
access and list readable collections for that viewer (r92 M2). They now
authenticate as the delegated exchanges do: the calling module's own Work
Context in x-codefly-work-context, held to its current grant and installation,
and the viewer in a new required viewer_work_context_token field, which must be
addressed to that module (its audience is the module's prefix). A viewer
addressed to another module is PermissionDenied ("the viewer Work Context is
not addressed to the calling module").

Breaking on the wire: a caller must send its module Work Context in the header
and the viewer in the body. The one consumer is the document store
(collection_reads.go, publication_access.go); it moves in lockstep.

Proto regenerated with codefly 0.1.213 (`generate proto`, `go generate`),
contracts with `codefly generate contracts saas-starter`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): the module authority endpoint denies by default

The endpoint's interceptor admitted every procedure with no credential and
relied on each handler to identify its caller, so a future method that forgot
to would be served open to the mesh with every gate green (r92 M3). It now
requires exactly one verified module Work Context before any handler runs, for
every procedure except the explicit allowlist of workload mints
(ModuleAuthorityAuthenticatesInRequest, today MintSourceOperationContext, which
proves the module from the projected token in its request). moduleCaller now
refuses more than one module Work Context instead of reading the first.

TestEveryModuleAuthorityProcedureRefusesAnUnidentifiedCaller builds the
endpoint's server (its interceptors, the catalog's registrations) and calls
every ModuleAuthorityProcedures entry with no credential and with the internal
token alone: each must answer Unauthenticated or PermissionDenied with nothing
written. With the interceptor's check removed, the test fails (handlers
validate input before identifying the caller and answer InvalidArgument).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(audit): a delegation to a module is granted and revoked on its own events

saas.module_delegation.granted and saas.module_delegation.revoked replace
saas.work_context.task_started as the audit events Delegate and
RevokeDelegation declare. The events contribution, event catalog,
AsyncAPI and communication documents are regenerated by the declared
composition generator; the RPC catalogs by the repository's go generate
steps. ListResources' policy comment no longer calls it unimplemented.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): a delegation fails closed, is the person's own, and ends only its own capabilities

r93 B2, M1 and m3:
- Delegate refuses, FailedPrecondition by name, on a host that cannot
  witness a revocation (no policy log), so no delegation is a standing
  grant nothing can end.
- Delegate and RevokeDelegation refuse a resource-confined credential,
  an identity acting on someone's behalf (X-Act), an impersonated session
  and any credential that is not an interactive session.
- Every hop of a chain under a delegation is sealed to
  <org>/<module>/<binding>@<delegation>; the binding answer reads the
  delegation live, so revoking it revokes exactly those capabilities at
  every verifier. Migration 48 no longer advances the grantor's whole
  authorization revision.
- An organisation administrator may revoke a member's delegation.
- A purpose past 1024 characters is InvalidArgument by name.
- Grant and revocation are emitted on the audit spine in the
  transaction that writes the row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(accounts): delegations refuse borrowed credentials, fail closed, and revoke only themselves

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(accounts): delegations as served after r93; remove the committed conflict markers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(accounts): the retained-binding integration asks as the module the child is addressed to

TestInstalledContentReadPostgresRetainedBinding presented the exchanged child
as the header credential. Under 7cab3a4f the target module asks with its own
identity (minted against its current installation) and the child travels in
viewer_work_context_token; while the target is uninstalled it has no identity
and is refused for that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(accounts): the journal records the binding and scope set a hop went through

Migration 49 adds operation_binding_id and operation (invoke, lookup or
headless; both or neither) to actor_chain_journal, folded into the hop's
content address when present, with a reader answering them by hop id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): hold each hop to the scope set it was issued under

A hop upstream in a chain was compared with the union of its binding's
sets, which is invoke, so a lookup hop outlived its binding's lookup. A
binding hop now journals its binding and operation, and the chain re-read
holds a recorded hop to exactly that set; the remote binding answer runs
the same re-read. The re-read is limited to chains sealed to a module
binding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(accounts): a hop is held to the scope set it was issued under

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): a module context the host does not admit is Unauthenticated, not a revocation

AuthorizeModuleCapability answered PermissionDenied when it refused the CALLING
MODULE itself — a principal no composition registers, installed nowhere for the
bound organization, a tenant its grant no longer names, an installation of
another registry binding — and FailedPrecondition when the host could not
re-read live authority at all. Consumers read PermissionDenied from the
revision oracle and the capability surface as "the caller's authority was
revoked", so they blamed the person and never re-minted their own context (r103
on document-store #585).

Each of those is now Unauthenticated, naming its reason, so the module drops its
context and mints a new one; PermissionDenied stays for an admitted module
asking beyond its grant (another tenant, an undeclared resource). A host with no
live authority read answers Unavailable under LIVE_AUTHORITY_UNAVAILABLE:
retryable, and a statement about nobody. The module mint's own 412 for a
workload installed nowhere is unchanged.

TestAModuleContextThisHostDoesNotAdmitIsUnauthenticated covers each case; the
existing assertions move to the new codes; NOTIFICATIONS.md and the accounts
AGENTS.md reason table say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(store): renumber the actor-chain hop binding migration above this branch's 51

The hop fixer's 49_actor_chain_hop_binding was numbered against the branch
before main's 48 was merged and this branch's migrations moved to 49-51, so it
duplicated 49_authority_delivery. It becomes 52; HOST_INTERFACE.md's two
references follow, and module_delegation.go's reference to the delegations
migration names 51. The SQL is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(ci): pin codefly 0.1.214; regenerate both generated libraries with it and gate their drift

v0.1.214 carries cli#1166 (buf-module client proto sources) and cli#1162
(namespaced TypeScript client barrel exports), so the two checked-in libraries
can finally be reproduced by a released CLI instead of a source build:

- module/services/frontend/code/packages/saas-sdk/generated: regenerated with
  `node scripts/generate.mjs --force`. It was stale against accounts/connect
  since 7cab3a4f moved the contract (the Immutable module package job's
  TestGeneratedLibraryContractDigestsMatchThePackage), and its barrel came from
  the unreleased source-df447bbc (r92 m1).
- libraries/source-read-sdk: regenerated with the README's command. It was
  stale against accounts/authority and generated by the unreleased
  source-2c6225ad (r92 B2, reproducibility half). Its narrowing to
  ModuleCapabilitiesService still waits on codefly-dev/cli#1173: `--services`
  narrows only the facade.

scripts/ci/check-generated-libraries.sh regenerates both with the pinned CLI
and fails on any difference; the SDK-boundary job runs it after the contracts
and runnables checks. Contracts, runnables and accounts proto generation are
unchanged under 0.1.214.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): with no envelope, a manifest widening waits for each organisation's consent

With no administrative envelope the …
@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Superseded by #1257 (3c53033): child issuance is rule A in the shared authority core, which accounts adopted, with the per-installation binding ceiling of codefly-dev/authority#36. Closed in the 2026-10-27 clean-up on the owner's instruction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants