0007. The scope is a plain argument on every action call
0007. The scope is a plain argument on every action call
Status
Accepted
Date
2026-10-04
Deciders
lead; the operator may overrule. Supersedes ADR-0008.
Context
An action needs to know who is calling (the actor) and may need extra data for the call (the context). Together this value is the scope. Ash, the Elixir framework Mesh is modelled on, takes actor:, tenant: and context: options on each call, and offers Ash.Scope to bundle them into one value (Ash features, section 6.9, and “Implications for Mesh”, item 3). Mesh needs its own answer.
The operator’s only statement on the subject is that application-specific concerns must not enter Mesh’s architecture. The rest of the earlier design, including “scope = actor and context”, was the lead’s own (rulings of 2026-10-04, Review note, first bullet).
Decision
Decided by the lead, as recorded in rulings of 2026-10-04, section “Lead decisions of the architecture-docs brief”, first bullet:
Scope is a plain argument. The caller passes
{ actor, context }on every action call, as Ash’sactor:option. Drop the actor-resolver contract, theactor-devpackage, the action registry and the transport contract from v1. N4 is moot.
The scope is never ambient. In the roadmap, the scope is the second argument of every generated function, createPost(input, scope), required on every call; “a call without a scope is a type error” (roadmap, M2). Tenancy is not in this type; where it lives is open (ADR-0009). The operator may overrule.
Options considered
Option A: A plain argument (chosen)
| Dimension | Assessment |
|---|---|
| Complexity | Lowest: a typed parameter |
| Cost | Callers pass it everywhere; generated code threads it |
| Visibility | Best: every call shows who it runs as |
| Reversibility | Medium: a changed scope type changes every generated signature |
Pros: Matches the direction Ash took. Ash 3.0 removed Ash.set_*, which stored actor, tenant and context in the process dictionary, because “There were fundamental issues with this pattern that manifested in subtle bugs” (Ash strengths and weaknesses, section 4). The research recommends: “Pass context explicitly, always” (same file, “Implications for Mesh”, item 8, the researcher’s opinion). Works the same in a test, a script or a daemon.
Cons: Boilerplate; every internal call must forward the scope. The research notes that without a bundling value “generated handlers would otherwise thread actor/tenant by hand” (Ash features, “Implications for Mesh”, item 3, the researcher’s opinion), which is why the scope is one object.
Option B: An actor-resolver adapter
| Dimension | Assessment |
|---|---|
| Complexity | Higher: a contract, an adapter package, a development implementation |
| Cost | Extra package (actor-dev) to build and maintain |
| Fit with Ruling 8 | Needs a transport to resolve from |
| Reversibility | Medium |
Pros: Callers need not build a scope by hand; each transport can map its own request to a scope.
Cons: It only makes sense when a transport exists, and none is in v1 (ADR-0005). It was the lead’s invention, not an operator ruling (rulings of 2026-10-04, Review note). See ADR-0008.
Option C: Ambient scope
| Dimension | Assessment |
|---|---|
| Complexity | Low at call sites |
| Cost | Low to write |
| Risk | High: hidden state |
| Reversibility | Low |
Pros: No threading; one place to set it.
Cons: Ash removed it. In Bun, AsyncLocalStorage (Node’s mechanism for async context) is not propagated into Worker or MessagePort events (research synthesis, section 12, risk 4).
Trade-off analysis
The plain argument costs typing and buys one rule everywhere: the caller states who is calling. Option B adds machinery for a transport that does not exist. Option C was tried by Ash and reversed.
Consequences
- Easier: tests, scripts and daemons call the same functions; authorization checks in M8 have a defined input.
- Harder: nested calls must forward the scope; adding a field to the scope later changes every generated signature, though regeneration handles it.
- Revisit: ADR-0009 (tenant); the first transport will need its own way to build a scope. How an application types its actor is decided in M2:
runtimecannot know the application’s actor type, soScopeneeds a generic or anunknownactor.
Action items
- M2: define the
Scopetype inruntime, including how the actor is typed, as a required second argument. - M2: test that a call without a scope is a type error.
- M8: policies read the actor and context from the scope.