Skip to content

M2-14 Acting-for binding (SR9)

No Date Ruling Reason
D-M2-14-0 2026-10-22 Built on the old integration line with M2-08 and M2-09 stacked first, and ported to main after M2-09 landed: only the ticket's own commits were brought over. The spec commit that ported the Scope sentence of 07 section 7.4 rule 1 is dropped, because main already carries it; the gofmt commit of the messaging world is dropped, because main restructured that file; the additions to the German chapter files of the old base go to their English files on main; the wiring moved from serve.go into the module files (module_runs.go binds the acting-for source at the start of step sessions, module_authz.go installs the Scope observer), PL/28. The rules of the landing replace the integration branches; main is the reference.
D-M2-14-1 2026-10-21 The header is Invocation-Token with the compact JWS exactly as delivered in EventEnvelope.invocation_token; declared once as components.parameters.InvocationToken and named in the descriptions of bearerAuth and dpopAuth, not as a parameter of each operation; the DPoP proof does not cover it. 07 SR9 leaves the name to 06, which had none; no product name (02 6) and no X- prefix, like Idempotency-Key; like Authorization it is an input of step 7 of 02 9.1 for every route, and it needs no binding of its own because it never adds a right.
D-M2-14-2 2026-10-21 Claims without run_id (trigger_kind = autocomplete, and command.invoked without an active run of the command's owner in the thread) give the status invalid. SR9 binds a run, and "run active" cannot hold without one (05 5, R-AZ-25).
D-M2-14-3 2026-10-21 The binding check verifies with IgnoreExpiry and does not check the currency of the jti: signature, aud and an active run of this agent, triggered by sub, in the claimed thread. 07 SR9 names signature, aud and an active run; a run outlives agent.invocation_token_ttl; every token of a run names the same person (runs.triggered_by); the token never adds a right, so a leaked or older token gives its holder nothing the agent token alone would not give.
D-M2-14-4 2026-10-21 identity declares the source port ActingForSource, runs implements it in adapters/actingfor with only kernel types in the signature, and internal/wire holds the compile-time assertion; no new module edge in 02 4.4 or modules.yaml. The authenticator owns the actor (02 4.3); a consumer outside runs gets a port with its own DTOs (M2-06 note 1).
D-M2-14-5 2026-10-21 A failed binding check is a failed token check: status invalid denies every action and Scope, status run_inactive every action but run.start and run.token.renew, both with forbidden and reason invocation_token_invalid, Detail and problem reason invalid or the new run_inactive; this includes the exceptions (finishRun after the run ended answers 403 with run_inactive with the header; without it M2-09's answers stay). 07 SR9: "the token is checked on every request"; the existing problem code with a new reason tells the adapter that the run ended.
D-M2-14-6 2026-10-21 The triggering person's snapshot is the re-check snapshot of R-AZ-29 (D-M2-08-1): the marker is also allowed for snapshots of an ActingFor person, besides the actions it already admits, and the snapshot lives under its own cache key, so it never equals a token principal of the same person. Without owner status no run of an owner (AZ-M19, every personal and host agent) could read its own context; a shared key would give a personal token the owner snapshot.
D-M2-14-7 2026-10-21 Closed mode table in authz/domain: own exceptions message.create, run.update, run.finish (also awaitRunInput), interaction.respond and agent_session.report, only on the own thread and, for a resource of kind run, the own run; unbound run.start (also resumeRun) and run.token.renew; every other action bound, a new action until its ticket classifies it (fail closed). 07 SR9 names the exceptions partly in prose and gives no closed list; the two unbound actions neither read content nor write into a channel and check their own token in the body (09 4.5, 4.5.1).
D-M2-14-8 2026-10-21 In delivery mode (authorize.Delivery(ctx)) decisions and Scope ignore ActingFor; Filter(ctx) stays bound. 07 8.1 row "Agent consumers": like people, by their own grants; the consumer must not be narrowed by the header. The REST poll reads its feed in filter mode (D-M2-07-7), so pollAgentEvents removes the binding from the actor of the feed after agent.events.consume checked it (D-M2-14-15).
D-M2-14-9 2026-10-21 A denial whose principal carries ActingFor has the audit details acting_for_user_id and run_id (absent for invalid) and, for invocation_token_invalid, token_verdict with the Detail code; allows and filters write nothing new. The audit must name whose rights bound the agent without the token (07 4.2, R-AZ-04, SEC-AUD-5, AZ-P13).
D-M2-14-10 2026-10-21 SR9 binds REST requests only; gateway connections carry no ActingFor. The gateway has no request header, and an agent's gateway ops (run.update of the own run, ack, autocomplete answers) read no content in a run's name.
D-M2-14-11 2026-10-21 The pure rules of authz/domain take domain types: ActingForModeOf(action string), ActingForApplies(mode, ActingForFacts{RunID, ThreadID}, Path, ResourceFacts, delivery) and CheckActingFor(status, mode); authz reads the binding through the optional interface authorize.ActingForBinder (authorize.ActingForOf), which actor.Actor implements with plain values. A principal without the binder whose ActingForPerson is set, and an ActingFor with a person but without a status, count as bound. Place: authz/domain/actingfor.go, kernel/authorize/authorizer.go, kernel/actor/actor.go. authz-domain may depend on nothing but itself and kernel-core, and kernel ports never import each other (.go-arch-lint.yml), so the brief's signatures with actor.ActingFor and authorize.Resource cannot compile there; a narrowing is never lost (fail closed).
D-M2-14-12 2026-10-21 When the requirement fails for the agent alone, the reason is the one the agent's own rights give, not the one of the intersected rights (they differ for file.upload: the intersection may fail on the level while the agent lacks upload). Place: authz/domain/decide.go decideRequirement. Brief design note 4f: "If the agent alone already fails, the agent's own reason stays"; the SR9 dimension of the AZ-P8 reference found the difference.
D-M2-14-13 2026-10-21 The binding read is its own port runs/app.RunBindings (RunBinding, query GetRunBinding, always on the pool, never in the ambient transaction), which adapters/postgres.Repository implements next to Repository. Place: runs/app/ports.go, runs/adapters/postgres/repository.go. It runs during authentication, before any unit of work and without a lock (design note 3g), and reads runs without the visible-channel predicate (note 3f, listed in tools/lint/sql_visibility.yaml).
D-M2-14-14 2026-10-21 TestSR9_EveryScopedList observes the Scope calls through the test seam authzapp.Authorizer.SetScopeObserver, passed as wire.Deps.ScopeObserver and harness.Options.ScopeObserver; nil in production, and it never changes a decision. The harness starts the real wire graph, whose modules hold the Authorizer directly, so a decorator around the port cannot be put in front of them without rewiring every module.
D-M2-14-15 2026-10-21 agents/app.ConsumerService.Poll hands the feed of pollAgentEvents an actor without the Invocation-Token binding (withoutActingFor), after authorizeConsume decided agent.events.consume with it, so an invalid header or an ended run still refuses the poll. Place: agents/app/consumers.go. Found by the code review: the feed reads in filter mode with the request actor, so a bound header narrowed the consumer and moved its cursor over the hidden events for good (07 8.1, design note 6).
D-M2-14-16 2026-10-21 A catch-up (listEvents) of a request bound to a run leaves out every personal event of the agent with receive action self (access.changed, access.expiring, sidebar.updated, read_state.updated and the rest of that class; the cursor passes them); the agent consumer still gets them. Place: sync/app/catchup.go ownSelfEventInRun. These events are judged by the agent's own rights alone: access.changed and access.expiring name the targets of the agent's grants, the others its spaces and threads, so they would name channels the triggering person may not read (07 7.4 rule 1, requirement 14); filtering the whole class at one place keeps a new self event from reopening the leak; the consumer is judged by the agent's own grants.
D-M2-14-17 2026-10-21 The integration tests of test/integration/actingfor run on the shared harness (test/harness, the real wire graph, the M1 matrix world of BuildMatrixWorld): the harness owner plays the brief's owner Robin, Alex's run is opened in a new thread of Support/Allgemein (the world thread holds the owner's run), three servers carry the cases as subtests, TestSR9_RevocationImmediate uses two api nodes on one database, the expired-token case uses an offset clock (DPoP proofs need real time), the autocomplete token is signed with the server's invocation key, resumeRun is compared with the consumed start token (nothing produces agent.followup yet), unread_threads is empty for agents with and without the header, and listMyMentions is refused for agents (subject_kind_not_allowed). The harness is the one place with the real authenticator, runs source, REST poll and run lifecycle; the brief's world of M2-09 replaces the login and has no agent consumers.