@jr2/orchestrator 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/docs/agents-block.md +2 -2
  2. package/docs/concepts/agents-and-turns.md +56 -20
  3. package/docs/concepts/architecture.md +30 -17
  4. package/docs/concepts/charts.md +6 -3
  5. package/docs/concepts/converge.md +3 -0
  6. package/docs/concepts/effects.md +86 -31
  7. package/docs/concepts/machines-and-workflows.md +9 -0
  8. package/docs/concepts/pools-and-sources.md +6 -6
  9. package/docs/concepts/secrets-and-trust.md +126 -42
  10. package/docs/concepts/the-menu.md +3 -0
  11. package/docs/concepts/workspaces.md +129 -51
  12. package/docs/get-started/an-agent-in-a-sandbox.md +21 -19
  13. package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
  14. package/docs/get-started/install.md +4 -0
  15. package/docs/guides/add-tools-to-the-sandbox.md +17 -4
  16. package/docs/guides/bind-open-parts.md +5 -4
  17. package/docs/guides/cut-a-worktree.md +560 -0
  18. package/docs/guides/fix-a-failed-converge.md +1 -1
  19. package/docs/guides/let-an-agent-write-a-chart.md +5 -1
  20. package/docs/guides/look-inside-a-sandbox.md +18 -15
  21. package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
  22. package/docs/guides/push-and-sign-a-branch.md +67 -21
  23. package/docs/guides/run-a-command-or-call-a-url.md +84 -41
  24. package/docs/guides/run-one-worker-per-item.md +19 -18
  25. package/docs/guides/set-an-agents-access-and-dials.md +67 -22
  26. package/docs/guides/size-a-workspace.md +19 -6
  27. package/docs/guides/use-a-hosted-provider.md +6 -0
  28. package/docs/guides/watch-a-run.md +47 -24
  29. package/docs/guides/write-a-workflow.md +1 -1
  30. package/docs/llms.txt +33 -12
  31. package/docs/reference/authoring-surface/agent.md +34 -11
  32. package/docs/reference/authoring-surface/answer.md +19 -3
  33. package/docs/reference/authoring-surface/arg.md +27 -5
  34. package/docs/reference/authoring-surface/branchname.md +15 -0
  35. package/docs/reference/authoring-surface/command.md +36 -21
  36. package/docs/reference/authoring-surface/fail.md +17 -4
  37. package/docs/reference/authoring-surface/file.md +36 -16
  38. package/docs/reference/authoring-surface/http.md +108 -8
  39. package/docs/reference/authoring-surface/isready.md +14 -0
  40. package/docs/reference/authoring-surface/jr2setup.md +1 -1
  41. package/docs/reference/authoring-surface/open.md +36 -5
  42. package/docs/reference/authoring-surface/paramsof.md +22 -4
  43. package/docs/reference/authoring-surface/pool.md +4 -4
  44. package/docs/reference/authoring-surface/push.md +30 -12
  45. package/docs/reference/authoring-surface/readyof.md +21 -0
  46. package/docs/reference/authoring-surface/secret.md +25 -4
  47. package/docs/reference/authoring-surface/source.md +3 -3
  48. package/docs/reference/authoring-surface/type-agentdefinition.md +2 -2
  49. package/docs/reference/authoring-surface/type-agentslots.md +4 -1
  50. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  51. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  52. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  53. package/docs/reference/authoring-surface/type-binding.md +5 -6
  54. package/docs/reference/authoring-surface/type-commandinput.md +4 -3
  55. package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
  56. package/docs/reference/authoring-surface/type-commandoutput.md +22 -4
  57. package/docs/reference/authoring-surface/type-customize.md +4 -2
  58. package/docs/reference/authoring-surface/type-dispatchoutput.md +8 -1
  59. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  60. package/docs/reference/authoring-surface/type-filebody.md +4 -8
  61. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  62. package/docs/reference/authoring-surface/type-hostinjectedinput.md +2 -4
  63. package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
  64. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  65. package/docs/reference/authoring-surface/type-httpoutput.md +7 -1
  66. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  67. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  68. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
  69. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  70. package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
  71. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
  72. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
  73. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  74. package/docs/reference/authoring-surface/type-poolspec.md +2 -2
  75. package/docs/reference/authoring-surface/type-pushinput.md +22 -3
  76. package/docs/reference/authoring-surface/type-pushoutput.md +32 -7
  77. package/docs/reference/authoring-surface/type-reposlot.md +5 -5
  78. package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
  79. package/docs/reference/authoring-surface/type-slothandles.md +5 -13
  80. package/docs/reference/authoring-surface/type-sourcespec.md +10 -10
  81. package/docs/reference/authoring-surface/type-workspaced.md +29 -8
  82. package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
  83. package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
  84. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  85. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  86. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  87. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  88. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  89. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  90. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  91. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  92. package/docs/reference/authoring-surface/workspace.md +65 -37
  93. package/docs/reference/authoring-surface/worktree.md +71 -0
  94. package/docs/reference/authoring-surface.md +30 -10
  95. package/docs/reference/cli.md +1 -1
  96. package/docs/reference/config.md +13 -4
  97. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
  98. package/docs/reference/decisions/0002-duplex-streaming-actor.md +17 -10
  99. package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
  100. package/docs/reference/decisions/0005-sandbox-pod-composition.md +29 -19
  101. package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
  102. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +30 -30
  103. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +35 -17
  104. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +8 -2
  105. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
  106. package/docs/reference/decisions/0017-pool-over-a-source-port.md +6 -4
  107. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +21 -9
  108. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +2 -1
  109. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +8 -9
  110. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +62 -65
  111. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +9 -1
  112. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +13 -12
  113. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +4 -1
  114. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +18 -19
  115. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +8 -8
  116. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +4 -4
  117. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
  118. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
  119. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
  120. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +24 -24
  121. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +5 -3
  122. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +110 -67
  123. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +101 -74
  124. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +5 -6
  125. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +2 -0
  126. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +181 -153
  127. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +74 -63
  128. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +31 -20
  129. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
  130. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
  131. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
  132. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  133. package/docs/reference/decisions.md +3 -0
  134. package/docs/reference/glossary.md +51 -12
  135. package/docs/reference/http-api.md +45 -7
  136. package/docs/reference/machines.md +0 -1
  137. package/docs/why.md +5 -4
  138. package/package.json +3 -3
  139. package/src/actor.ts +161 -35
  140. package/src/agent.ts +6 -6
  141. package/src/ambient.ts +3 -5
  142. package/src/chart.ts +186 -19
  143. package/src/charts.ts +9 -2
  144. package/src/config.ts +16 -6
  145. package/src/custodian.lua +17 -5
  146. package/src/custodian.ts +251 -77
  147. package/src/customize.ts +11 -3
  148. package/src/effect-checks.ts +50 -3
  149. package/src/effect.ts +343 -127
  150. package/src/effector-wire.ts +6 -3
  151. package/src/fail.ts +17 -4
  152. package/src/harness-client.ts +74 -31
  153. package/src/harness-env.ts +33 -0
  154. package/src/held-secrets.ts +125 -16
  155. package/src/holds.ts +3 -0
  156. package/src/http-effect.ts +88 -32
  157. package/src/http.ts +56 -17
  158. package/src/index.ts +15 -1
  159. package/src/internal.ts +1 -0
  160. package/src/kube-client.ts +6 -3
  161. package/src/machine-doc.ts +3 -4
  162. package/src/model-keys.ts +24 -0
  163. package/src/names.ts +19 -6
  164. package/src/notices.ts +443 -54
  165. package/src/open-body.ts +85 -19
  166. package/src/open.ts +36 -5
  167. package/src/parts.ts +40 -28
  168. package/src/party.ts +33 -0
  169. package/src/pick.ts +41 -7
  170. package/src/pool.ts +44 -23
  171. package/src/registration.ts +53 -11
  172. package/src/repo-identity.ts +7 -3
  173. package/src/repos.ts +20 -2
  174. package/src/run-host.ts +106 -20
  175. package/src/sandbox-effect.ts +217 -66
  176. package/src/sandbox-kube.ts +63 -16
  177. package/src/secret-placement.ts +9 -2
  178. package/src/setup.ts +86 -6
  179. package/src/tokens.ts +107 -32
  180. package/src/vocabulary.ts +23 -4
  181. package/src/wire.ts +10 -10
  182. package/src/workspace.ts +264 -253
  183. package/src/worktree.ts +249 -0
  184. package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
  185. package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
@@ -16,21 +16,28 @@ action observably occurred only post-approval.
16
16
 
17
17
  The split-channel model:
18
18
 
19
- | Direction | Kind | Mechanism |
20
- | --------- | --------------------------------------------------------- | ---------------------------------------- |
21
- | up | control / decision (the workflow's events, ADR-0011) | Menu tool call → Custodian → delivery |
22
- | up | progress / telemetry (tokens, attempts, "still working") | Harness stream (lossy, observation-only) |
23
- | down | the next turn's prompt (feedback, steering at a boundary) | a new submission on the same iid |
24
- | down | the Turn part, restated (the Allowed picks changed) | the Harness's read at each step boundary |
25
- | down | interrupts (deliberate terminal cancel) | `abort` on the wire |
19
+ | Direction | Kind | Mechanism |
20
+ | --------- | -------------------------------------------------------------- | --------------------------------------------- |
21
+ | up | control / decision (the workflow's events, ADR-0011) | Menu tool call → Custodian → delivery |
22
+ | up | progress / telemetry (tokens, attempts, "still working") | Harness stream (lossy, observation-only) |
23
+ | down | the next turn's prompt (feedback, steering at a boundary) | a new submission on the same iid |
24
+ | down | the Turn part, restated (the Allowed picks changed) | the Harness's read at each step boundary |
25
+ | down | a jr2 notice (a memory kill, ADR-0062), typed, to its audience | the same step-boundary read, per conversation |
26
+ | down | interrupts (deliberate terminal cancel) | `abort` on the wire |
26
27
 
27
28
  Tool calls are Agent-initiated, so the Machine speaks inside a Turn only as an answer. The one exception is the
28
29
  **restated Turn part**: when something other than the Agent's own pick changes its Allowed picks — a Gate's answer, an
29
30
  outside event — the Harness learns it by reading the surface at each step boundary and appends the new list
30
31
  ([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)). It is a pull, not a push:
31
- nothing new reaches the Harness, and what is restated is fixed text plus Menu names. The residual hard case is
32
- **unsolicited mid-turn steering** — free text from the Machine to an Agent that has not asked. That remains unsolved,
33
- and it is the only behavior in that bucket, not a dependency of the whole control plane.
32
+ the Harness asks at a step boundary, and nothing reaches it that it did not ask for. What is restated is fixed text plus
33
+ Menu names. jr2's own **typed notices** ride that same step-boundary read: the read of an open Turn returns its
34
+ conversation's pending notices, the Harness appends them, and its acknowledgment of the append delivers them. So a
35
+ running Agent hears a fact the mechanism knows — a memory kill above all — without an interrupt. A notice is typed data,
36
+ and the Harness words it ([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)). The residual case is
37
+ **free-text human steering** — arbitrary text from a human Caller to an Agent that has not asked. jr2 reserves it for v1
38
+ on this same channel, authenticated as a Caller ([ADR-0011](0011-workflow-defined-events.md)) rather than by a
39
+ Sandbox-scoped credential; its sender and authorization are designed with the Caller. It is one behavior, not a
40
+ dependency of the whole control plane.
34
41
 
35
42
  ## Durability: the admission is the handle
36
43
 
@@ -91,34 +91,40 @@ fast setup.
91
91
 
92
92
  ## Where setup runs
93
93
 
94
- Worktree provisioning is a post-`Ready` step owned by the Orchestrator: the Sandbox CRD names repositories by identity
95
- so the operator can place and mount them, but it knows nothing about clones or worktrees (ADR-0001, ADR-0051).
96
- `workspace()`'s attach is one idempotent in-pod script the Harness runs on `POST /attach` (ADR-0012,
97
- [ADR-0063](0063-the-orchestrator-watches-the-cluster.md)), once `Ready` says every cache is present and fetched.
94
+ The clone is a post-`Ready` step owned by the Orchestrator: the Sandbox CRD names repositories by identity so the
95
+ operator can place and mount them, but it knows nothing about clones or worktrees (ADR-0001, ADR-0051). `workspace()`'s
96
+ attach is one idempotent in-pod script the Harness runs on `POST /attach` (ADR-0012,
97
+ [ADR-0063](0063-the-orchestrator-watches-the-cluster.md)), once `Ready` says every cache is present and fetched. The
98
+ attach only clones. A Machine cuts each Worktree with the kit Effect `worktree()`
99
+ ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)).
98
100
 
99
101
  ## In-Sandbox layout: worktrees off the per-Sandbox clone (gwtmux convention)
100
102
 
101
103
  "Not a shared worktree" above means _not a worktree off a fleet-shared `.git`_. **Inside** a Sandbox, worktrees are the
102
- intended ergonomic — taken off the Sandbox's **own** clone. The per-Sandbox clone _is_ the `default/`, and branch
103
- working trees are its siblings, matching the layout [gwtmux](https://github.com/snapwich/gwtmux) already expects, under
104
- the slot the Machine gave the repository (ADR-0051):
104
+ intended ergonomic — taken off the Sandbox's **own** clone. The per-Sandbox clone _is_ the `default/`, and the Worktrees
105
+ are its siblings, matching the layout [gwtmux](https://github.com/snapwich/gwtmux) already expects, under the slot the
106
+ Machine gave the repository (ADR-0051):
105
107
 
106
108
  ```
107
- /work/<slot>/default/ the per-Sandbox `git clone --shared` (holds the pod-local .git; the default branch checked out)
108
- /work/<slot>/<branch>/ `git worktree add` siblings, one per branch worked
109
+ /work/<slot>/default/ the per-Sandbox `git clone --shared` (holds the pod-local .git; stays on the Binding's base)
110
+ /work/<slot>/<name>/ `git worktree add` siblings: folders a Machine cuts, each with one branch or one commit
109
111
  ```
110
112
 
111
113
  Because the layout is identical to the one used outside the cluster, existing worktree tooling works **unchanged** when
112
- you exec into a Sandbox. Object resolution falls through: a branch worktree shares the per-Sandbox `default/.git`, whose
114
+ you exec into a Sandbox. Object resolution falls through: a Worktree shares the per-Sandbox `default/.git`, whose
113
115
  alternates point at the read-only cache — existing objects are read from the cache, new commits land in the pod-local
114
- `.git`. `default/` is a checkout of the Repo's default branch, as gwtmux's `default/` is outside the cluster: work
115
- happens in the branch worktrees, and `default/` is the reference tree a human or a tool finds where it expects one. Not
116
- `--no-checkout`: an empty working tree reads as every tracked file deleted to `git status`, which misleads an Agent that
117
- looks. The branch name `default` is refused by the spec guard: the branch Worktree is a sibling of that directory, never
118
- it. `origin`'s push url is the binding's own spelling, so a Machine that bound over ssh pushes over ssh even when the
119
- cache was cloned over https.
120
-
121
- This does **not** reintroduce jr's contention: the shared `.git` here is _per-Sandbox_, and it is accessed serially
122
- under the standing invariant that **one Agent runs at a time per Workspace** — structural since ADR-0012 (one sequential
123
- body per workspace). A Machine that runs concurrent Agents in one Sandbox, or an Agent that spawns parallel git-touching
124
- subagents, reintroduces contention (pod-scoped, not fleet-wide) and must opt into separate Sandboxes instead.
116
+ `.git`. `default/` stays on the Binding's base: a branch is attached, a tag or a commit is detached, and with no base it
117
+ is the Repo's default branch, as gwtmux's `default/` is outside the cluster. No work happens in it: work happens in the
118
+ Worktrees, and `default/` is the reference tree that a human, a tool or an Agent that only reads finds where it expects
119
+ one. Not `--no-checkout`: an empty working tree reads as every tracked file deleted to `git status`, which misleads an
120
+ Agent that looks. A Worktree's folder cannot be named `default`, and a cut of the branch that `default/` holds is
121
+ `refused`, with a reason that names `/work/<slot>/default`. `origin`'s push url is the binding's own spelling, so a
122
+ Machine that bound over ssh pushes over ssh even when the cache was cloned over https.
123
+
124
+ This does **not** reintroduce jr's contention: the shared `.git` here is _per-Sandbox_, so what contends stays in one
125
+ pod and never crosses the fleet. Inside the pod it is the author's choice. A Machine that runs concurrent Agents in
126
+ Worktrees of one clone, or an Agent that spawns parallel git-touching subagents, takes that contention, and a Machine
127
+ that does not want it uses separate Sandboxes. Two things keep one clone safe to share. The attach sets `gc.auto=0` and
128
+ `maintenance.auto=false` in the clone, because a `gc` beside a cut or a commit can prune what another Worktree needs.
129
+ And all Worktrees of a clone share `refs/remotes/origin/*`, so `jr2-upload-pack` holds a per-clone lock for each fetch
130
+ (ADR-0053, ADR-0075).
@@ -6,14 +6,20 @@ A Sandbox pod composes up to four containers around one shared worktree volume (
6
6
  [ADR-0037](0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md)) with jr2's runtime mounted at
7
7
  `/opt/jr2` and the command overridden to start the Harness. The Agent's toolchain lives here because the Working tools
8
8
  (read/write/edit/bash) execute here — the tools the Agent can reach are this image's. It is also where a human
9
- `kubectl exec`s to inspect a run with the agent's exact tools, worktrees, and filesystem.
9
+ `kubectl exec`s to inspect a run with the agent's exact tools, worktrees, and filesystem. One Harness container serves
10
+ every workspace-ful Agent of the Sandbox. Each process that the Harness starts in `/work` — a Working tool's shell, a
11
+ test, git, a `command` Effect — runs as a confined child of the Harness process, so no code from `/work` reaches the
12
+ control credential that the Harness process holds
13
+ ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)).
10
14
  - **Custodian container** — jr2-composed sidecar, upstream Envoy configured at provision by the Orchestrator
11
15
  ([ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md),
12
16
  [ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)): the pod's credential holder. It holds
13
17
  the Sandbox token and the held secrets its pod names — model API keys above all — as files, and carries the Harness's
14
18
  traffic out: its Menu and the ask to the Orchestrator, and its outbound HTTPS. It exists as a separate container
15
19
  precisely _because_ the Working tools give the Agent code execution in the Harness container — the credentials live
16
- where the Agent cannot read them, and the Harness container's env holds only their Stand-ins.
20
+ where the Agent cannot read them, and the Harness container holds only their Stand-ins, which are secret: only the
21
+ Harness process holds them, as files, and a process it confines gets only those that its `workspace()` grants
22
+ (ADR-0059, ADR-0076).
17
23
  - **User Container** — opt-in third seat, composed only when the `workspace()` names its image (a `file:` docker context
18
24
  the Machine ships or a registry ref, the same resolution as the Sandbox Image, no default —
19
25
  [ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)). The authoring surface is that string —
@@ -63,8 +69,8 @@ What each container mounts of the credentials, stated once:
63
69
 
64
70
  | Container | Mounts |
65
71
  | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
66
- | `harness` | `jr2-ca` at `/etc/jr2/ca` (the trust bundles). None of the Custodian's volumes. |
67
- | `custodian` | its values (the Sandbox token, its pod's `jr2-held-secrets-<pod>`, each user `secretKeyRef` its pod names) at `/etc/jr2/custodian/values`; its leaves (`jr2-held-tls`); its pod's config (its own `<sandbox>-custodian`, and `program.lua` from `jr2-held`); `upstream.crt`. |
72
+ | `harness` | `jr2-ca` at `/etc/jr2/ca` (the trust bundles); the Stand-ins — the control one and each held secret's — as files under `/run/jr2/` that only the Harness process reads and no process it confines can (ADR-0059, ADR-0076); no Stand-in is in its env. None of the Custodian's volumes. |
73
+ | `custodian` | its values (the Sandbox's control token and fetch token, its pod's `jr2-held-secrets-<pod>`, each user `secretKeyRef` its pod names) at `/etc/jr2/custodian/values`; its leaves (`jr2-held-tls`); its pod's config (its own `<sandbox>-custodian`, and `program.lua` from `jr2-held`); `upstream.crt`. |
68
74
  | `user` | the checkouts' three volumes, and nothing else. |
69
75
  | `effector` | `/work`, `/repos/<key>` and `/opt/jr2` read-only; at `/run/jr2/secrets` (0400) the secrets its Effects name — its pod's `jr2-effector-<pod>` literals, each user `secretKeyRef` by reference — and at `@git/<name>/` each `git.credentials` entry's push Secret the Sandbox CR names (`gitCredentials`), which the operator projects from that entry's Secret by reference into this container alone (`@` is no character of a secret's name, so no secret's file takes that directory); at `/run/jr2/effector` its key (its own `<sandbox>-effector`) and `effector.json`; an emptyDir at `/run/jr2/scratch` as `$HOME`; at `/etc/jr2/effector-ca`, read-only, the instance's caBundle ConfigMap when the instance ships one (ADR-0020). |
70
76
 
@@ -98,12 +104,13 @@ jr2 sets `runAsUser` nowhere — each image's own `USER` decides its seat's uid
98
104
  and plain POSIX modes would then make the other seat's files read-only. jr2 closes that with two mechanisms it happens
99
105
  to own, unconditionally (both are inert when the uids already match):
100
106
 
101
- - **The pod carries `fsGroup` — the work group, default `2000`.** The default is convention; the one override lives on
102
- the `WorkspaceSpec` (`workGroup?: number`, beside `image` and `user` — pod composition is the spec's business,
103
- ADR-0037), for the image whose session users already hold a gid of their own. Never a config key. Any value is safe
104
- for the jr2 seats: Kubernetes grants the fsGroup as a supplemental group to every container process, so the Harness
105
- writes `/work` whatever the number, and no image's `/etc/group` needs to know it. fsGroup puts a setgid group on
106
- `/work`'s root, and setgid propagates down, so everything either seat creates is group-_owned_ by the work group.
107
+ - **The pod carries `fsGroup` — the work group, default `2000`.** The default is convention; the one override is an
108
+ option of `workspace()` (`workspace(body, { workGroup: 2000 })`, beside `image` and `user` — pod composition is the
109
+ wrapper's business, ADR-0037), for the image whose session users already hold a gid of their own. Never a config key.
110
+ Any value is safe for the jr2 seats: Kubernetes grants the fsGroup as a supplemental group to every container process,
111
+ so the Harness writes `/work` whatever the number, and no image's `/etc/group` needs to know it. fsGroup puts a setgid
112
+ group on `/work`'s root, and setgid propagates down, so everything either seat creates is group-_owned_ by the work
113
+ group.
107
114
  - **The attach stamps a default ACL on every repo root it creates** — `u::rwx,g::rwx,o::r-x`, set by `work-acl`, a
108
115
  static helper on the runtime volume (part of ADR-0037's published `/opt/jr2` surface, static for the same reason `rg`
109
116
  is: it executes on a libc jr2 does not control). A default ACL is inherited by everything created beneath it, and
@@ -116,16 +123,16 @@ to own, unconditionally (both are inert when the uids already match):
116
123
  Group _membership_ is the one thing an image can still owe. `kubectl exec` sessions and a non-root sshd's logins hold
117
124
  the work group automatically — both inherit the container's supplemental groups, where Kubernetes put the fsGroup. A
118
125
  **root** sshd is the exception: the logins it setuids rebuild their groups from `/etc/group`, so that image puts its
119
- session user in the work group itself (`groupadd -g 2000 work && usermod -aG work <user>`) — or the spec points
126
+ session user in the work group itself (`groupadd -g 2000 work && usermod -aG work <user>`) — or the `workspace()` points
120
127
  `workGroup` at a gid its users already hold.
121
128
 
122
129
  **The promise is scoped to repo trees.** An ad-hoc path at `/work`'s top level — either seat's `mkdir /work/scratch` —
123
130
  carries no default ACL, so files there fall back to the writer's umask. The Harness runs at `umask 002` (itself and
124
131
  every tool child) and the attach script opens with the same line, so jr2's own writes are group-writable even there;
125
132
  what a User Container session writes outside a repo tree is its own business. A Workspace with no Repo Slot (ADR-0051)
126
- has no repo tree: its steps share `/work` itself, every Agent and `command` of it in the one Harness container as one
127
- uid, so they need no ACL; a User Container's writes there have this paragraph's limit, until a user of that seat needs
128
- more.
133
+ has no repo tree: its steps share `/work` itself, every Agent and `command` of it at the one uid in the one Harness
134
+ container, so they need no ACL; a User Container's writes there have this paragraph's limit, until a user of that seat
135
+ needs more.
129
136
 
130
137
  ## Constraints on the seat
131
138
 
@@ -134,8 +141,8 @@ The seat is zero-_contract_, not zero-_physics_. What an image brought here must
134
141
  - **The entrypoint must block.** The pod's restart policy restarts an exited container, so a `CMD` that finishes
135
142
  crash-loops. Run a long-lived process — an sshd, an IDE server, `sleep infinity` at minimum.
136
143
  - **The ports of jr2's seats are taken** — one network namespace: `:8080` (the Harness), the Custodian's
137
- `127.0.0.1:8081` (control), `127.0.0.1:15001` (egress) and `:15021` (health), and `:8082` (the Sandbox Effector, in a
138
- pod that has one). Everything else is the image's. A process that binds `:8082` while the Sandbox Effector is down
144
+ `127.0.0.1:8081` (the fetch), `127.0.0.1:15001` (egress) and `:15021` (health), and `:8082` (the Sandbox Effector, in
145
+ a pod that has one). Everything else is the image's. A process that binds `:8082` while the Sandbox Effector is down
139
146
  gets signed requests it can neither forge nor answer (ADR-0068): the Effect fails closed.
140
147
  - **Root is allowed.** The hardened security context (non-root, all capabilities dropped, no escalation) applies to the
141
148
  seats jr2 owns — Harness, Custodian and Sandbox Effector, per container — and deliberately not here: the seat's
@@ -162,7 +169,10 @@ namespace, node, and the worktree volume, and are one trust domain in every isol
162
169
  container edge does provide is **credential visibility** — the asymmetry ADR-0013 builds on: the Agent executes code in
163
170
  the Harness container and none in the others, so a secret that exists only in another container's mount namespace is out
164
171
  of its reach. That is why the Custodian holds the Orchestrator credential and the model keys, and it is what the User
165
- Container offers a human session — with its edges stated plainly:
172
+ Container offers a human session. Inside the Harness container, a second edge holds the one credential the Harness keeps
173
+ itself, the control Stand-in: the Harness process is jr2's code, and each process it starts in `/work` is confined from
174
+ that Stand-in by Landlock (ADR-0076, [ADR-0077](0077-jr2-requires-landlock-on-every-sandbox-node.md)). The edges, stated
175
+ plainly:
166
176
 
167
177
  - It holds only for what stays in the private filesystem or session. Anything on `/work` or listening on `localhost` is
168
178
  shared with the Agent — the pod has one network namespace. The Custodian's listeners are on `localhost` too; what they
@@ -206,8 +216,8 @@ Container offers a human session — with its edges stated plainly:
206
216
 
207
217
  - The generic `Sandbox` CRD composes all of this as plain container specs
208
218
  ([ADR-0001](0001-operator-owned-generic-sandbox.md) holds — the operator stays agent-agnostic); the Orchestrator's
209
- `kubectlSandbox` supplies the images. The pod runs two containers, plus `user` when the spec names one and `effector`
210
- when an Effect is placed there.
219
+ `kubectlSandbox` supplies the images. The pod runs two containers, plus `user` when the `workspace()` names one and
220
+ `effector` when an Effect is placed there.
211
221
  - Every container but the Sandbox Effector carries a limit from the Workspace's Size (ADR-0060): the Custodian a fixed
212
222
  kit slice, the User Container its split if stated, the Harness the rest. The Sandbox Effector carries no limit of its
213
223
  own: like a User Container with no split, it shares the pod's Size ceiling — the whole Size, as pod-level resources —
@@ -42,6 +42,9 @@ is the only structured channel. Consequences:
42
42
 
43
43
  ## Residual
44
44
 
45
- Unsolicited mid-turn steering remains the hard case (ADR-0002): the menu only opens when the Agent reaches a checkpoint
46
- or a turn boundary. Evidence for the findings above: `poc/actor/POC-5B-FINDINGS.md`, `poc/actor/POC-5C-FINDINGS.md`
47
- (`poc/` is gitignored; conclusions live here).
45
+ jr2's own typed notices now reach a running Agent through the step-boundary read (ADR-0002,
46
+ [ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)); the Menu still opens only when the Agent
47
+ reaches a checkpoint or a turn boundary. Free-text human steering is the remaining case, reserved for v1 as a
48
+ Caller-authenticated message on the same channel (ADR-0002, [ADR-0011](0011-workflow-defined-events.md)). Evidence for
49
+ the findings above: `poc/actor/POC-5B-FINDINGS.md`, `poc/actor/POC-5C-FINDINGS.md` (`poc/` is gitignored; conclusions
50
+ live here).
@@ -1,15 +1,15 @@
1
1
  # `workspace()` wraps a body Machine and owns only Sandbox lifecycle
2
2
 
3
3
  jr2 provides Workspaces as a helper so workflows never provision Sandboxes themselves:
4
- `workspace(body, { input, repos, spec })` is a statically-imported factory (per
5
- [ADR-0011](0011-workflow-defined-events.md)'s import doctrine) returning a Machine that provisions a Sandbox and its
6
- worktrees, runs the author's **body** Machine inside it, and tears the Sandbox down when the body finishes. This makes
7
- GLOSSARY.md's "Workspace = child Machine bound to a unit of work" concrete.
4
+ `workspace(body, { input, repos })` is a statically-imported factory (per [ADR-0011](0011-workflow-defined-events.md)'s
5
+ import doctrine) returning a Machine that provisions a Sandbox and clones its Repos, runs the author's **body** Machine
6
+ inside it, and tears the Sandbox down when the body finishes. This makes GLOSSARY.md's "Workspace = child Machine bound
7
+ to a unit of work" concrete.
8
8
 
9
9
  ```
10
10
  placing: create Sandbox CR → await the pod scheduled, no deadline (ADR-0064)
11
- provisioning: await phase: Ready (watched, ADR-0063) → Harness /attach repos/worktrees (ADR-0004)
12
- running: invoke body; input = parent input + { workspace: { repos, branch } }
11
+ provisioning: await phase: Ready (watched, ADR-0063) → Harness /attach repos (ADR-0004)
12
+ running: invoke body; input = parent input + { workspace: { repos } }
13
13
  teardown: destroy the CR
14
14
  done: final; workspace output = body output
15
15
  ```
@@ -25,29 +25,29 @@ already is. When the guarantee still fails (`kill -9`, hard cancel), the operato
25
25
  (ADR-0001) — the reason ADR-0001 made the CR the durable desired state. Author-assembled provisioning/cleanup states
26
26
  (the obvious alternative) put the guarantee in every workflow's hands to forget, on every error path.
27
27
 
28
- ## Boundary: the spec speaks workspace vocabulary only
28
+ ## Boundary: the options speak workspace vocabulary only
29
29
 
30
- The spec — `{ branch, workGroup?, reviewSha? }` — is mapped from the parent's context by the author, but its _shape_ is
31
- workspace-domain: the branch to cut, and what the attach needs to cut it. The branch itself is the author's: its name
32
- and its granularity (per feature, per task, from a ticket, from run input) are the Machine's policy, not jr2's; the
33
- workspace only cuts it. Which Repos to attach is not per-run at all: they are slots on the wrapper's options
30
+ The wrapper's options are workspace-domain. Which Repos to attach is not per-run at all: they are slots on the options
34
31
  (`repos: { target: open, docs: { url, ref } }`, ADR-0051), static so `jr2 up` can see them, each bound by the Machine,
35
- by a composer's `customize`, or by a mapper over the door. Workflow configuration (review rounds, budgets, ticket data)
36
- passes through to the body untouched; the workspace never sees it. In return the body gets what it needs to operate
37
- inside the Sandbox: `{ repos: Record<slot, path>, branch }`, the handles keyed by the slots the wrapper declared, in
38
- declaration order. No handle names a primary checkout: which tree an Agent works in is a fact about that Agent's Turn,
39
- which the body states (ADR-0051). The Harness `endpoint` and the Sandbox name are mechanism-facing, not body-facing —
40
- `agentRun` resolves them **ambiently** from the enclosing wrapper (a registrar actor co-invoked in `running`, ADR-0016),
41
- so the body cannot mis-thread them. Keeping this boundary is what lets one helper serve any workflow rather than
42
- coupling to coding-shaped ones.
32
+ by a composer's `customize`, or by a mapper over the door. The Workspace only clones. A branch and a Worktree are the
33
+ body's: their names and their granularity (per feature, per task, from a ticket, from run input) are the Machine's
34
+ policy, not jr2's, and the body cuts each Worktree with `worktree()`
35
+ ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)). Workflow configuration (review rounds,
36
+ budgets, ticket data) passes through to the body untouched; the workspace never sees it. In return the body gets what it
37
+ needs to operate inside the Sandbox: `{ repos: Record<slot, path> }`, the handles keyed by the slots the wrapper
38
+ declared, in declaration order. A handle is the slot's folder, `/work/<slot>`, and not a Worktree. No handle names a
39
+ primary checkout: which tree an Agent works in is a fact about that Agent's Turn, which the body states (ADR-0051). The
40
+ Harness `endpoint` and the Sandbox name are mechanism-facing, not body-facing — `agentRun` resolves them **ambiently**
41
+ from the enclosing wrapper (a registrar actor co-invoked in `running`, ADR-0016), so the body cannot mis-thread them.
42
+ Keeping this boundary is what lets one helper serve any workflow rather than coupling to coding-shaped ones.
43
43
 
44
- The wrapper's `input` beside it is the door the spec maps FROM — the wrapper's own declared run-input schema, which also
45
- types the mapping ([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)). It sits on the wrapper and not on
46
- the body precisely because of the injection above: the body's input is the door plus `workspace`, which no caller sends.
47
- That same composition is what the door checks the body against, one way only — the body may not demand more than the
48
- wrapper hands it; demanding less is safe — so the injection is a compile-time contract, not just a comment. The host's
49
- own injection into a root machine's input (`instanceId`) counts as handed over for that check, because whether it
50
- actually reaches this body depends on where the wrapper sits and no type can see that
44
+ The wrapper's `input` is its door — the wrapper's own declared run-input schema, which also types a per-run slot's
45
+ mapper ([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)). It sits on the wrapper and not on the body
46
+ precisely because of the injection above: the body's input is the door plus `workspace`, which no caller sends. That
47
+ same composition is what the door checks the body against, one way only — the body may not demand more than the wrapper
48
+ hands it; demanding less is safe — so the injection is a compile-time contract, not just a comment. The host's own
49
+ injection into a root machine's input (`instanceId`) counts as handed over for that check, because whether it actually
50
+ reaches this body depends on where the wrapper sits and no type can see that
51
51
  ([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)).
52
52
 
53
53
  ## An Effect runs in its Workspace: the Harness container or the Sandbox Effector
@@ -58,8 +58,8 @@ Worktree file and a `command` that names a secret run in that Workspace's Sandbo
58
58
  container ([ADR-0005](0005-sandbox-pod-composition.md),
59
59
  [ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), which the provision adds only when an
60
60
  Effect below the `workspace()` is placed there; its image is the Sandbox Image unless the `workspace()` names
61
- `effector: { image }`. Each one's Worktree follows the Turn's rule (ADR-0057): the one Repo Slot's, or the slot it names
62
- with `slot`; a Workspace with several and none named refuses it. A push goes to that slot's Binding, and after every
61
+ `effector: { image }`. Each one's Worktree follows the Turn's rule (ADR-0057): its input names it, and under Repo Slots
62
+ it must. A push goes to the Binding of its Repo Slot, the one slot or the one its static `slot` names, and after every
63
63
  push whose published commit is not the Agent's own — signed copies, or one squashed commit, signed or not — the
64
64
  Workspace's Harness moves the Agent's branch to the published commits (the sync, ADR-0067). An Effect of these kinds
65
65
  outside any `workspace()` is refused at invoke, as an error, never as output. A `command` that names a secret runs
@@ -86,8 +86,8 @@ the same contract as a temp directory.
86
86
 
87
87
  ## Consequences
88
88
 
89
- - ADR-0004's single-active-Agent-per-Workspace invariant becomes structural: one sequential body per workspace, no lock
90
- machinery.
89
+ - The body chooses how many Agents work in its Workspace (ADR-0004, ADR-0075). The wrapper has no lock machinery for
90
+ them.
91
91
  - The workspace labels the CR with its run (`jr2.dev/run`, `jr2.dev/workflow`), which is what `jr2 ls` groups by
92
92
  (ADR-0009).
93
93
  - **Lost workspace: the wrapper emits, the body decides.** While the body runs, a lease actor beside it renews the
@@ -25,16 +25,21 @@ is needed either (ADR-0006).
25
25
  ## Decision
26
26
 
27
27
  - **The Custodian carries the Agent's Menu, and holds the credential that makes it work.** It is a jr2-composed
28
- container in every Harness pod ([ADR-0005](0005-sandbox-pod-composition.md)). The Sandbox token is mounted into it as
29
- a file, and into no other container. The Harness container holds the token's Stand-in,
30
- `JR2_SANDBOX_TOKEN=jr2-held-JR2_SANDBOX_TOKEN`, and reaches the Orchestrator only through the Custodian's control
31
- listener on `127.0.0.1:8081`. The Custodian forwards four routes, with the token put in place of the Stand-in, and
32
- answers every other route itself:
28
+ container in every Harness pod ([ADR-0005](0005-sandbox-pod-composition.md)). A Sandbox has two tokens, its **Sandbox
29
+ tokens**: the **control token** and the **fetch token**, mounted into the Custodian as files, and into no other
30
+ container. The Harness process holds the control token's Stand-in, which is secret: a file that no process it confines
31
+ can read, in no child's env
32
+ ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)). The fetch token's Stand-in
33
+ is public, `jr2-held-JR2_SANDBOX_TOKEN`, and `jr2-upload-pack` holds it as a constant. The Harness reaches the
34
+ Orchestrator only through the Custodian: the agent routes on its control socket, `/run/jr2/custodian/control.sock`,
35
+ which no confined program can replace (ADR-0076), and the fetch on `127.0.0.1:8081`. The Custodian forwards five
36
+ routes, with each route's own token put in place of its Stand-in, and answers every other route itself:
33
37
 
34
38
  ```
35
39
  GET /agents/<iid>/surface the Menu → the Orchestrator
36
40
  POST /agents/<iid>/events a pick → the Orchestrator
37
41
  GET /agents/<iid>/deliveries/<id> an outstanding pick's answer → the Orchestrator
42
+ POST /agents/<iid>/notices a step boundary's notices heard → the Orchestrator
38
43
  POST /fetch the ask (ADR-0053), for THIS pod's Sandbox → POST /sandboxes/<name>/fetch
39
44
  anything else 404
40
45
  ```
@@ -60,9 +65,10 @@ is needed either (ADR-0006).
60
65
  POST /runs/:id/gates/:gate/events → validate + deliver, or 409 [Instance token]
61
66
 
62
67
  # the Agent's, through its pod's Custodian
63
- GET /agents/:iid/surface → Menu + Allowed picks + schemas + semantics [Sandbox token]
64
- POST /agents/:iid/events → validate + deliver → the receipt, or 409 [Sandbox token]
65
- GET /agents/:iid/deliveries/:id → an outstanding pick's answer, long-polled [Sandbox token]
68
+ GET /agents/:iid/surface → Menu + Allowed picks + schemas + semantics [control token]
69
+ POST /agents/:iid/events → validate + deliver → the receipt, or 409 [control token]
70
+ GET /agents/:iid/deliveries/:id → an outstanding pick's answer, long-polled [control token]
71
+ POST /agents/:iid/notices → the notices a step boundary appended, heard (ADR-0062) [control token]
66
72
  ```
67
73
 
68
74
  The receipt is `{ delivered, event, moved, turnComplete, allowed, deliveryId }`. A pick the workflow is still running
@@ -86,11 +92,13 @@ is needed either (ADR-0006).
86
92
  - **Bearer tokens, because the boundary is otherwise theater.** The Harness container shares the pod's network
87
93
  namespace, so an Agent can `curl` the Orchestrator directly; a per-pod NetworkPolicy cannot distinguish it from the
88
94
  Custodian. Only authentication closes this.
89
- - **Sandbox token** — minted per Sandbox at provision (a signed Sandbox name, so re-provision after a restart yields
90
- the same token), delivered as a Secret mounted into the **Custodian container only**. User `harness.env`/`envFrom`
91
- land on the Harness container, never the Custodian. Authorizes exactly: deliver to agent registrations **whose
92
- Sandbox is this one**, and ask for this Sandbox's fetch. Never a Gate (they are not on that surface at all), never
93
- another Sandbox. The Stand-in the Harness holds is refused on every route.
95
+ - **Control token and fetch token** — minted per Sandbox at provision (signed names, so re-provision after a restart
96
+ yields the same tokens), delivered as a Secret mounted into the **Custodian container only**. User
97
+ `harness.env`/`envFrom` land on the Harness container, never the Custodian. The control token authorizes exactly:
98
+ read and deliver to agent registrations **whose Sandbox is this one** and whose slot is the slot the Harness states
99
+ for the conversation (ADR-0076). The fetch token authorizes exactly: ask for this Sandbox's fetch. The Orchestrator
100
+ accepts each token only on its own routes. Neither reaches a Gate: a Gate refuses every principal of an Agent.
101
+ Neither reaches another Sandbox. A Stand-in is refused on every route.
94
102
  - **Instance token** — the human/CLI credential for Gates and run control. Minted into an in-cluster Secret at
95
103
  `jr2 up`; the CLI reads it over the kube API (RBAC is the gate) while port-forwarding the Service (ADR-0019).
96
104
  - **The Instance-token door.** The Instance token may also read any Agent's surface and deliver to it —
@@ -98,13 +106,17 @@ is needed either (ADR-0006).
98
106
  AS the Agent: it reads the Agent's Menu and Allowed picks and its picks are judged as the Agent's — they carry the
99
107
  Agent's sender (ADR-0011) — and no audience filter applies, so it can send a pick no Gate's caller may send
100
108
  ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)). It is deliberate — it is how the e2e tier stands in
101
- for an Agent — and it adds no party: neither `jr2 send` nor the Console offers it, and a Sandbox token never opens
109
+ for an Agent — and it adds no party: neither `jr2 send` nor the Console offers it, and a control token never opens
102
110
  it for a registration that is not its own.
103
111
  - An agent registration records its Sandbox **ambiently**: `agentRun` resolves the enclosing `workspace()` and records
104
112
  the same deterministic name the token was minted for (ADR-0016), so forgetting to pass it is unrepresentable (the
105
113
  pre-ambient version shipped exactly that footgun: `sandbox` omitted, every tool call 403'd — fail-closed but
106
114
  silent). A run-scoped token instead of a Sandbox-scoped one would be genuinely exploitable: derivable iids plus
107
- readable ticket ids would let one feature's coder inject a `review_verdict` into another feature's reviewer.
115
+ readable ticket ids would let one feature's coder inject a `review_verdict` into another feature's reviewer. That
116
+ holds across Sandboxes, where another feature's Agents are in another pod. Within one Sandbox several Agents can
117
+ share it ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)); there the Sandbox scope is
118
+ not enough, and ADR-0076 keeps the control credential where no Workspace code runs, so a coder's code cannot act as
119
+ the reviewer beside it.
108
120
  - **Reachability, narrowed to one caller.** The Orchestrator is always in-cluster (ADR-0019), so the route home is
109
121
  simply **Service DNS**, which the pod's maker writes into the Custodian's configuration — the Orchestrator at a
110
122
  Sandbox's provision, `jr2 up` for the Instance Harness. The Harness is told the Custodian's loopback address
@@ -140,12 +152,18 @@ is needed either (ADR-0006).
140
152
  understanding; the Custodian is exactly that. `kubeSandbox` composes it (plus the token Secret, written right after
141
153
  the CR with the CR as its owner from birth, so Kubernetes reaps it with the Sandbox and no path leaks it — ADR-0001).
142
154
  - **Nothing in jr2 speaks MCP.** The Orchestrator, the Harness and the CLI carry no MCP dependency.
143
- - **One container holds the Sandbox token and every model key.** A fault in the Custodian's engine is a fault in the one
144
- container that holds both. ADR-0059 records the trade.
155
+ - **One container holds the Sandbox's tokens and every model key.** A fault in the Custodian's engine is a fault in the
156
+ one container that holds both. ADR-0059 records the trade.
145
157
  - **The `@kind` e2e tier owns the pod → Orchestrator leg** — the pod's Harness actually calls its tool through the
146
158
  Custodian, so a broken leg is a red test, and a Harness container that posts a pick straight to the Orchestrator with
147
159
  the Stand-in it holds is refused. The mechanics tier plays `/agents/:iid/*` from the host, which is honestly
148
160
  simulating the Harness's Menu, not an Agent.
161
+ - **Per-agent identity within one Sandbox is ADR-0076.** A Sandbox-scoped credential that an Agent's code could read
162
+ would let one Agent in a shared Sandbox deliver a pick as another, and ADR-0075 makes a shared Sandbox normal. Only
163
+ the Harness process holds the control credential, and each process it starts in `/work` runs confined; the Harness
164
+ states each conversation's slot, and the Orchestrator checks it on the whole `/agents/:iid/*` set. The `@kind` tier
165
+ gains the sibling cases: a coder's `bash` finds no control credential, and code it plants that the reviewer's own
166
+ `bash` runs cannot deliver a pick as the reviewer.
149
167
  - **NetworkPolicy is a second layer, not the control.** It bounds where a pod may talk; only the token bounds what it
150
168
  may _do_. `jr2 up` converges ingress policies that admit only the Orchestrator to a Harness pod
151
169
  ([ADR-0058](0058-a-harness-answers-the-orchestrator-alone.md)); the Orchestrator itself is left reachable, because a
@@ -143,8 +143,14 @@ sends there, and every case with the Agent's state at Rest.
143
143
  **Load checks on modes and audience.** `createMachine` refuses a registered event that no state handles: it is in no
144
144
  derived set, so it is on no Menu and no Gate. It refuses a parallel region below a state that invokes an Agent — the
145
145
  Agent's state itself being parallel included, since its regions are below it: the shape is reserved for picks the Agent
146
- does not wait for, which jr2 does not build, and background work goes above the Agent's state. It warns — the Machine
147
- stays legal; the doc carries the warning, `jr2 up` prints it and the Console shows it with the seats — about a mode that
146
+ does not wait for, which jr2 does not build, and background work goes above the Agent's state. It refuses one Agent slot
147
+ invoked by two states that can be active together — two parallel regions, one state inside the other, or one state
148
+ twice: a slot is one Agent of the Machine, so its two Turns would have one sender, which `sentBy` cannot tell apart, and
149
+ one conversation, which `continue` cannot give to both. Two Agents at once are two slots (one definition can fill both),
150
+ a child Machine in each region, or a Pool ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)).
151
+ jr2 refuses this shape and does not support it as a special case, because jr2 can remove a refusal later without
152
+ breaking a Machine, but a refusal that it adds later breaks each Machine that loads now. It warns — the Machine stays
153
+ legal; the doc carries the warning, `jr2 up` prints it and the Console shows it with the seats — about a mode that
148
154
  nothing enters (not initial, and no state the Machine can enter has a transition into it or a state inside it — so
149
155
  neither the mode's own `initial` nor its transition into itself counts: its picks are on the Menu and never allowed; a
150
156
  transition into a history state enters that history state's default target, shallow or deep, or its parent's initial
@@ -21,7 +21,7 @@ restore re-attaches to a Submission whose model is already fixed server-side and
21
21
  `agentRun` walks `self._parent` to the nearest registered ancestor. Invoked-actor registration (not an entry action) is
22
22
  what makes this restore-safe: invoked actors restart on snapshot restore, entry actions do not. Not under a workspace →
23
23
  fail loudly, unless input carries an explicit `endpoint` (the dev-stub, workspace-less path). Body-facing workspace
24
- handles are `{ repos, branch }` ([ADR-0012](0012-workspace-wrapper-machine.md)).
24
+ handles are `{ repos }`: each is a Repo Slot's folder ([ADR-0012](0012-workspace-wrapper-machine.md)).
25
25
 
26
26
  **[ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md)'s token scoping survives and
27
27
  strengthens**: the registration records the Sandbox resolved from the enclosing wrapper — the same deterministic
@@ -108,12 +108,14 @@ modes — `continue` governs only what a _new invocation_ means. The author name
108
108
  conversation's identity is already on the page. (This corrects the jr-parity exercise's "same iid = jr's resume
109
109
  machinery, free" — an inversion of jr's actual semantics.)
110
110
 
111
- Two live invocations of one continued iid — parallel states — are refused at the registration table: one live surface
112
- per address. A Submission that arrives behind a live one on the Harness queues: prompts for one instance enter one
113
- per-instance queue in admission order, and a Submission is promoted only when it is the first unsettled one for its
114
- conversation (ADR-0027 states this as jr2's own semantics). A queued prompt waits behind work that may never finish,
115
- which is why [ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md) both ends the turn and orders the
116
- abort ahead of the next `send`.
111
+ Two live invocations of one continued iid are refused: one live surface per address. The load walk refuses the shape
112
+ that makes them, one Agent slot in two states that can be active together
113
+ ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)), and the registration table refuses a second registration
114
+ of one address as a second guard. A Submission that arrives behind a live one on the Harness queues: prompts for one
115
+ instance enter one per-instance queue in admission order, and a Submission is promoted only when it is the first
116
+ unsettled one for its conversation (ADR-0027 states this as jr2's own semantics). A queued prompt waits behind work that
117
+ may never finish, which is why [ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md) both ends the
118
+ turn and orders the abort ahead of the next `send`.
117
119
 
118
120
  The minting doctrine — addresses are computed by jr2, never by the workflow — covers gate ids too: a gate with no
119
121
  authored id derives one from its actor path below the run root, through the same walk `mintIid` uses (`actorPath` in
@@ -22,10 +22,12 @@ outgrowing the pool means hand-rolling without penalty, not fighting a framework
22
22
 
23
23
  ## The source port is generalized; Work Source is one adapter
24
24
 
25
- The pool draws from a **Source** port — `next(active) → item | null`, plus an optional `wake` event def (webhook /
26
- `jr2 send` push seam) and `pollEvery` (the set mutates underneath us) — a queue, a generator, or a re-queried set. tk's
27
- claim actor is one adapter; GLOSSARY.md's **Work Source** reframes as _the Source adapter for tickets_, and the
28
- Ready-set doctrine (re-query, never materialize) survives as that adapter's semantics, not the primitive's.
25
+ The pool draws from a **Source** port — `next({ active, settled, input }) → item | null`, plus an optional `wake` event
26
+ def (webhook / `jr2 send` push seam) and `pollEvery` (the set mutates underneath us) — a queue, a generator, or a
27
+ re-queried set. `input` is the Pool's input and `settled` holds the ids of the finished and failed items, so a Source
28
+ can fan out over a plan with no state of its own. tk's claim actor is one adapter; GLOSSARY.md's **Work Source**
29
+ reframes as _the Source adapter for tickets_, and the Ready-set doctrine (re-query, never materialize) survives as that
30
+ adapter's semantics, not the primitive's.
29
31
 
30
32
  A Work Source — `tk`, GitHub issues, Jira, a task queue — is a behavior port, not a canonical data schema: verbs like
31
33
  `claimNext`, `updateStatus` and `comment`, each gated by a capability the adapter advertises. It owns the dependency