Skip to content

feat(accounts): a module_actor binding names the exchanging module as the child's actor - #1215

Merged
lukazhupa merged 2 commits into
mainfrom
feat/exchange-module-hop
Oct 22, 2026
Merged

lukazhupa merged 2 commits into
mainfrom
feat/exchange-module-hop

Conversation

@lukazhupa

@lukazhupa lukazhupa commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1200: the explicit first-module-hop mode on the operation exchange, for a module, a solution: audience or the host's own source operations, plus the binding resolution Core's seal check needs for it.

It builds on the installed-binding child #1198 added.

What changes

An installed operation binding may set "delegation_mode": "module_actor" in the module envelope. A module that exchanges a person's parent through that binding (ExchangeDelegatedOperationAudience) gets Core's Child with one actor hop:

  • Principal: the calling module's authenticated principal (moduleCaller), never a value the request supplies.
  • Scopes: the binding's invoke scopes (or its lookup scopes), attenuated against the parent. The exchange refuses a scope the parent doesn't hold.
  • Binding: a namespaced id, installed-actor/…, committing to the caller's installation, presence and envelope facts, and to the audience's installation where it has one.
  • Journal: the hop is journaled, as fix: bind delegated content reads to installed authority #1198 journals content-read hops.

The audience must be one of three:

  • A module. It must serve every kind the binding names, the same check content_read makes.
  • A solution: audience. It's held by the organization's installation of that solution, so an uninstall or a reinstall retires issued children.
  • The host's own source operations (saas-datasource). Every scope names datasource.sources with explicit source ids and carries only invoke and read. The source surface takes the child as the person's call with the module acting, for those sources only.

Callee path (source operations). authenticateSourceOperationContext verifies the child, which runs Core's seal check against the installed binding. It then goes through requireCurrentAuthority → requireInstalledBinding, which re-resolves the binding, confirms the hop and rechecks the person's own authority. After a successful call, the same child is refused when:

  • the module's installation changes;
  • the binding is narrowed;
  • the person loses access to the source.

MintSourceOperationContext is unchanged: a connector-owned delegation keeps its meaning, and this path never borrows it.

Opt-in by installed policy: the mode belongs to the binding, never the request. These are unchanged:

  • a binding without a mode;
  • the chain-preserving exchange;
  • StartTask, and registered-agent actors.

It adds no request field, registry, permission vocabulary or store.

Reuse

  • fix: bind delegated content reads to installed authority #1198's installed-binding child: InstalledContentReadBinding delegates to InstalledBinding(mode), and the exchange is exchangeInstalledBinding for both modes.
  • Core Child and its seal rule: the hop's binding resolves through WorkContextOperationBinding.
  • The callee's revision check: ConfirmModuleHopBinding resolves installed-actor/ ids the same way it resolves installed-read/ ones.
  • The source surface's existing requireCurrentAuthority route to requireInstalledBinding.
  • Helpers: journalActorHop, attenuateEffective, WorkContextAudienceInstallation.

Deliberately not reused:

Classification

Owner fix. The host issues the first module hop at the exchange it owns, through its existing binding and seal machinery.

For the owner to confirm

  • Seal: the child keeps the parent's seal, which is the calling module's installation, as fix: bind delegated content reads to installed authority #1198's content-read child does. Withdrawing the audience retires it through the binding id's facts.
  • Solution served kinds: a solution: audience isn't held to served kinds, because the host records none for a solution target. It's held to the organization's installation of that solution.

Tests

From the accounts code root, GOWORK=off go test ./pkg/adapters ./pkg/business passes. In module/tools, TestModuleCapabilityEntryPointsHoldAuthorityThroughUse passes. go vet, gofmt, goimports and scripts/check-boundaries.sh are clean on every file this change touches.

New or extended tests:

  • TestModuleActorExchangeNamesTheExchangingModule: the hop equals the module Work Context's owner, scopes are attenuated, and owner, tenant, task and expiry come from the parent. The hop is journaled, and the revision check accepts the child when it's sent operation_binding_id.
  • TestModuleActorExchangeRefusals: each of these is refused:
    • a credential that isn't a module's;
    • a binding not granted to the caller;
    • a parent that already has a chain;
    • a kind the module audience doesn't serve;
    • authority broader than the parent.
  • TestModuleActorSolutionAudienceFollowsItsInstallation: reinstalling retires an issued child, and once uninstalled nothing is exchanged.
  • TestModuleActorSourceCallIsThePersonsWithTheModuleActing: for invoke and lookup, the child is the person's with the module as its one hop, narrowed to one source. The source surface accepts it for that source and refuses it for another.
  • TestModuleActorSourceChildRetiresWithItsStanding: after an accepted call, the same child is refused when the installation changes, when the binding narrows, and when the person loses access to the source.
  • TestModuleActorChildIsTerminal, TestInstalledModuleActorModeShape and TestSourceOperationActorScopeNamesItsSources.

Not run: the DB-backed pkg/infra suites, and codefly ci run.

Out of scope (the rest of #1200)

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ehgefxu5moNkjuHfTJofXM

… the child's actor

An installed operation binding marked `delegation_mode: module_actor`
exchanges a person's parent through Core's Child with the authenticated
calling module as its one actor hop, for any invoke scope and for a module
or `solution:` audience. It reuses the installed content-read child: the
binding id is namespaced (`installed-actor/`), commits to both
installations' lifetimes, resolves for Core's seal check and is confirmed
by the existing module-hop revision check. The hop is journaled.

Ordinary bindings, the chain-preserving exchange and StartTask are
unchanged.

Part of #1200.
… reaches named sources

A module_actor binding addressed to a module now names only kinds that
module serves, the check content_read already makes. It may also address
the host's own source operations: every scope names datasource.sources with
explicit source ids and carries only invoke and read. The source surface
then takes the child as the person's call with the module acting, for those
sources only. The existing callee path (requireCurrentAuthority, then the
installed binding) refuses it once the module's installation or the binding
changes, or the person loses access to the source.

Part of #1200.
@lukazhupa
lukazhupa force-pushed the feat/exchange-module-hop branch from 17e1a05 to e86bbc8 Compare October 22, 2026 16:13
@lukazhupa
lukazhupa marked this pull request as ready for review October 22, 2026 16:14
@lukazhupa
lukazhupa added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 0ad1b0c Oct 22, 2026
24 of 25 checks passed
@antoinetoussaint-byte
antoinetoussaint-byte deleted the feat/exchange-module-hop branch October 25, 2026 17:07
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