Skip to content
Like what we’re building? Star on GitHub

ADR-103: A run is addressed by its id, across the teams you may see ​

Status: Accepted (2026-09-25, the cloud control-plane epic #1407; the operator's arbitrations of 2026-09-25 recorded on the epic) Relates to: 048-org-team-hierarchy.md (the active-team stamp this amends), 078-config-editor-role-sso-team-scoped-access.md (the config_editor capability stays out of the run console), the run-by-id tickets under #1407 (#1847 the role ladder, #1848 the by-id status contract, #1849 the same rule for other id-addressed resources)

Context ​

ADR-048 made one ACTIVE team per identity the tenancy primitive: the auth middleware stamps it on the request context, and the store filters every query by it. That bounds what an id can address: GET /api/runs/{id} answers 404 whenever the run lives in another team, even for the run's own members — a Revi status link, a shared run URL or an operator's remote api GET /api/runs/<id> die on the wrong side of an active team. The pin is not a security boundary either: canViewTeam already authorizes by the USER's standing (membership, org-admin lineage, super-admin), so a by-id read grants nothing a team switch would not.

Decision ​

  1. The id suffices; the run's team is the team. The auth middleware classifies every request by the ROUTE THE MUX MATCHED (never by raw path prefixes — a static sibling of /api/runs/{id} carries no wildcard and stays outside the class). For the class — {id} directly under /api/runs or /api/ws/runs, and everything under those — the run's owning team is resolved team-blind (store.TeamBlind, positive results cached per process in a bounded LRU; a run's team never changes, so entries carry no TTL) and the request is stamped with the CALLER'S STANDING IN THAT TEAM: identity and store tenant agree on the run's team, and the handler cannot tell a re-scoped read from a same-team one. A run the caller has no standing in answers 404 — the same answer a missing run gives; run identifiers and their existence are NOT secrets, and authorization protects contents and actions, so the lookup-versus-miss timing difference is accepted on that basis (not on unguessability: ids may be caller-supplied).
  2. The ladder rides the same choke point (#1847): reads (GET/HEAD, WebSocket upgrades included) take a viewer of the run's team; every action the route performs (cancel, resume, rewind, answer, upload, delete) takes a member; a super-admin keeps full power. A config_editor membership is not a rung (ADR-078) and sees no runs — closing the gap where the tenant filter alone let one read and act. The ladder lives in ONE function fed by ONE standing computation (identityInTeam): membership gives its role; org-admin lineage reads as a viewer (a grant the pinned-token path refused before — an org admin's PAT pinned elsewhere was unusable, now it reads; documented as a change, not as old behaviour); a store failure is a 500, never a silent degrade. The re-scoped identity records how its team was resolved in auth.Identity.Via for audit and tests.
  3. Lists are scoped explicitly. A collection route does not guess a team from an id: GET /api/runs takes ?team_id= over the X-Iterion-Team header over the active team, through the same resolveTenantScope the /api/v1/* insights use — an unauthorised scope is a 403 naming the parameter, never an accepted-and-dropped one.
  4. Actions follow the run they belong to. An action a caller can ARM through a re-scoped route must be stoppable the same way: stopping an assistant watch gained its run-addressed spelling (DELETE /api/runs/{id}/assistant-watches/{watchID}); the fixed sibling stays for compatibility.
  5. The resolver capability is required, not optional. A cloud replica without a run-tenant resolver fails at startup — a silent half-replica answering 404 to its own members is the one failure this ADR cannot tolerate. Mixed-replica windows degrade the old way (an old replica answers 404 as before); no durable state changes and no migration is needed.

What is not promised ​

  • Existence confidentiality. If a deployment needs "you cannot even learn whether the id exists", this design does not provide it; the 404 equality is for callers with and without standing on the by-id routes.
  • Other id-addressed resources. Schedules, kanban issues and authored bot sources keep their current rules until #1849 carries the pattern to them, one resource per change.
  • Timing equalization. The authorized path costs a lookup the unauthorized one avoids; ADR-103 declares existence non-secret and leaves it there.