@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
@@ -45,11 +45,13 @@ loudly narrated, safe to re-run:
45
45
  ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). Referenced-but-unmanaged Secrets
46
46
  (`envFrom` refs, git credentials) are **preflighted**: `up` fails naming the missing Secret with the exact creation
47
47
  hint — converting the `CreateContainerConfigError` hang into an immediate error. Sealed Secrets/External Secrets ride
48
- this seam untouched: jr2's contract is "a Secret named X exists", however it got there. A credential the Harness uses
49
- is a held secret ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)): `up` writes a
50
- literal value into `jr2-held-secrets-<pod>` for each pod that names it, which only that pod's Custodian mounts, and
51
- checks that a referenced Secret and key exist. Like the signing key, the held-secret CA (`jr2-held-ca`) is kept across
52
- converges — minted on first need, never rotated by `up` — because a live pod trusts it.
48
+ this seam untouched: jr2's contract is "a Secret named X exists", however it got there. `up` also refuses a held
49
+ secret's name that is a key of an `envFrom` Secret, and names the Secret, the key and the held secret (ADR-0059, R14).
50
+ A credential the Harness uses is a held secret
51
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)): `up` writes a literal value into
52
+ `jr2-held-secrets-<pod>` for each pod that names it, which only that pod's Custodian mounts, and checks that a
53
+ referenced Secret and key exist. Like the signing key, the held-secret CA (`jr2-held-ca`) is kept across converges —
54
+ minted on first need, never rotated by `up` — because a live pod trusts it.
53
55
  - **Repos**: `up`'s walk of the registered Machines collects every bound Repo Slot (ADR-0049, ADR-0051) — to refuse an
54
56
  open slot nobody bound, and to see which urls need a key. The Orchestrator creates one `Repo` resource per url at
55
57
  boot, and the operator's cache agent clones each onto the nodes that need it (ADR-0004). There is no `repos` config
@@ -59,6 +61,11 @@ loudly narrated, safe to re-run:
59
61
  in-cluster deploy keypair (the recommended default, public key printed to register), a local key, or one pasted on
60
62
  stdin ([ADR-0047](0047-the-git-ssh-key-source-is-the-users-choice.md)) — declining bails. A personal key never enters
61
63
  a cluster silently: only by that explicit, warned choice.
64
+ - **Landlock**: `up` refuses to converge while a Sandbox node lacks Landlock. It reads the pods of the Repo cache
65
+ agent's DaemonSet, which probe Landlock on each Sandbox node, within the rollout wait it already makes for the cache.
66
+ It is an ordinary converge failure: it names each node whose agent reports none, the probe's reason, and the fix (keep
67
+ the node out with `sandbox.nodeSelector` or a taint, ADR-0052, or enable Landlock); a run after the fix converges
68
+ ([ADR-0077](0077-jr2-requires-landlock-on-every-sandbox-node.md)).
62
69
  - **Provider preflight**: `up` checks each provider that a carried model names. It checks from inside the cluster, with
63
70
  one trivial tool-call completion. An endpoint that the pods cannot reach, a wrong or missing key, or a vLLM without
64
71
  `--enable-auto-tool-choice` then fails at converge time, not as `agent.fault` in a run. Reachability is the user's
@@ -75,14 +82,17 @@ loudly narrated, safe to re-run:
75
82
  the config, each model's result says why, in words.
76
83
  - **The check pod.** It is a Harness pod with no Harness in it. It has a Custodian and the stock Harness image's
77
84
  `provider-preflight.ts`. The function that composes the Custodian of every Harness pod composes this Custodian too.
78
- Its token is a value that the Orchestrator never signed. `provider-preflight.ts` gets the env of the Harness
79
- container: the harness config, `harness.env`, `harness.envFrom`, the Stand-ins, the proxy and the trust. pi resolves
85
+ Its token is a value that the Orchestrator never signed. Its Stand-ins are secret and its own: `jr2 up` mints them
86
+ with the signing key, as it does for the Instance Harness
87
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). The check pod runs no Workspace
88
+ code, so it may hold them in env. `provider-preflight.ts` gets the env of the Harness container, with the Stand-ins
89
+ in it: the harness config, `harness.env`, `harness.envFrom`, the Stand-ins, the proxy and the trust. pi resolves
80
90
  each model and uses its api, as in a Turn. The `harness.catalog` gateway applies. The key never enters the check
81
91
  container, its argv or its log.
82
92
  - **Its own objects.** The check pod reads no object of the Instance. `up` writes copies from the values on the host:
83
93
  - a ConfigMap with the Custodian's files, the program and the harness config;
84
94
  - a ConfigMap with the trust bundles;
85
- - a Secret with the token and the literal values of the held secrets;
95
+ - a Secret with the token, the check pod's Stand-ins and the literal values of the held secrets;
86
96
  - a Secret with the leaves;
87
97
  - a Secret with the literal `harness.env` values.
88
98
 
@@ -194,7 +204,9 @@ loudly narrated, safe to re-run:
194
204
  - **A limit.** The check pod has labels of its own. It does not get the labels of a Harness pod: the Instance Harness
195
205
  Service selects its pods by label, and would send Turns to the check pod. So a NetworkPolicy that selects Harness
196
206
  pods by label does not select the check pod. When such a policy closes the egress of the Harness pods, the check can
197
- pass while a Turn fails.
207
+ pass while a Turn fails. jr2's own egress policy, which closes the cloud metadata addresses
208
+ ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)), gives the check pod one
209
+ of its own, so it does not have this limit.
198
210
 
199
211
  **`jr2 down`** removes the instance from the cluster. It asks for confirmation unless `--yes` is given (for CI). `--all`
200
212
  takes the operator too.
@@ -31,7 +31,8 @@ end. And a restarted Harness answered a `continue` with no memory of the convers
31
31
  Sandbox goes to the terminal phase **`Lost`**, with the pod's own reason and message (for a deleted pod, the last it
32
32
  saw). It never returns to `Pending` and is never given a second pod: `work` is an `emptyDir`, so a new pod could
33
33
  continue nothing, and it would hold a whole Size for nobody while hiding the loss. The CR stays, so `jr2 status` can
34
- say why, until teardown or the idle GC.
34
+ say why, until teardown or the idle GC. A Sandbox delete waits for its pod, because a retry can take the name of a pod
35
+ that is still going away.
35
36
  - **The operator judges identity, because it knows which pod it created.** It still publishes `status.podUID`; the
36
37
  Orchestrator reads `Lost`, not a UID comparison.
37
38
  - **The watch answers; the lease asserts.** The Orchestrator watches its Sandboxes
@@ -122,10 +122,10 @@ replayable from any offset. The gap was a read decision, not a missing capabilit
122
122
 
123
123
  - **Per-`agentRun` containers, or a pod per Agent.** Rejected on lifetime before merit: the container list is fixed when
124
124
  the CR is created, before the body has run, while `agentRun` is a per-turn invocation. Pre-declaring per _Agent_ is
125
- possible but pays N resident copies of a Harness image that deliberately carries the agent toolchain (ADR-0005) to run
126
- one at a time. It buys no isolation — containers in a pod share the network namespace, so it does not even fix the
127
- port contention that motivates it (ADR-0005:31) — and the git contention it would address is prohibited anyway
128
- (ADR-0004:95). What it actually buys is `kubectl logs -c <agent>`: a container, to avoid a prefix.
125
+ possible but pays N resident copies of a Harness image that deliberately carries the agent toolchain (ADR-0005). It
126
+ buys no isolation — containers in a pod share the network namespace, so it does not even fix the port contention that
127
+ motivates it (ADR-0005:31) — and it does not remove the git contention, because the containers share one `/work` and
128
+ one clone (ADR-0004). What it actually buys is `kubectl logs -c <agent>`: a container, to avoid a prefix.
129
129
  - **A trace from the Custodian.** Tempting, and wrong on the facts. The Custodian carries the Menu's reads and the
130
130
  Agent's picks — a handful of deliveries per turn — and the model calls, and it logs names only, never a body
131
131
  ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). The Agent's working tools execute in
@@ -159,11 +159,10 @@ replayable from any offset. The gap was a read decision, not a missing capabilit
159
159
  what it happened to read; what jr2 told it includes an Effect's output, so a `command` Effect's output and a Held
160
160
  value echoed into it reach the log as they reach the model, and so does an Effect secret in a form an Effector's scrub
161
161
  does not catch (a transformed secret, a partial echo; ADR-0068). Fine on kind; a real decision on a shared cluster.
162
- - **It depends on an invariant that is documented but not enforced.** One Agent at a time per Workspace (ADR-0004:95,
163
- "structural since ADR-0012") is what makes two conversations never interleave on one container's stdout. Nothing
164
- refuses a body that invokes concurrent `agentRun`s; such a body corrupts a shared `default/.git` before it garbles a
165
- log, so enforcement is owed on its own merits — the ambient registrar (ADR-0012:32, ADR-0016) is the one thing that
166
- sees every Agent in a Workspace, and the place to refuse a second concurrent admission.
162
+ - **The conversations of concurrent Agents interleave in one log.** The author chooses how many Agents work in a
163
+ Workspace ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)), so two conversations can
164
+ print at one time on one container's stdout. The prefix keeps them apart: every line carries its label and its iid, so
165
+ a reader keeps one conversation's lines by its iid.
167
166
  - **Truncation is load-bearing, not cosmetic.** Kubelet rotates container logs at 10Mi by default and drops the
168
167
  remainder. jr's filter capped `Bash` at 100 chars and unknown tools at 150 and reduced `Write`/`Edit` to a path; that
169
168
  was readability, and it is now also what keeps a long turn inside the window.
@@ -34,8 +34,7 @@ state. A prompt fix cannot be relied on and cannot even be shown to have worked.
34
34
  Workspace; the Machine's states are sequential, but the Agents are not. Throughout `reviewing`, the orphaned coder was
35
35
  still live in the worktree the reviewer was reviewing — and still live at `humanReview`, the park that deliberately
36
36
  retains the Sandbox so a human can exec in and inspect the diff. In the runs we watched, the orphan only read. Nothing
37
- prevented it writing. An un-ended turn is an unaccounted-for concurrent writer in a Workspace the Machine believes is
38
- single-threaded.
37
+ prevented it writing. An un-ended turn is a concurrent writer in a Workspace that the Machine did not put there.
39
38
 
40
39
  ## Decision
41
40
 
@@ -190,69 +189,67 @@ single-threaded.
190
189
  is local to its Machine: two Machines of one tree, and the workers of a pool, which run one body, may each have a
191
190
  child of one id, and one never reads or changes the other's entry. A child started on an event that names no Agent
192
191
  anywhere else is no Agent's. The other Agent's pick keeps its default answer, and when the Agent the event names has
193
- no pick being answered, it sets nothing. When one Agent slot is invoked in two regions, it shapes the pick at the same
194
- invoke as the pick that sent the event or started the child, and nothing otherwise: with both regions' picks being
195
- answered, the done event of a child shapes the pick whose invoke started it; a pick in its own send is shaped while
196
- the slot's other pick is outstanding; and the done event of a child that one region's pick started after that pick's
197
- Turn ended (its child runs on) shapes no pick, while the other region's pick keeps its default answer. When the event
198
- names no Agent (a timer, a Gate's answer, an outside send, the done event of a child no Agent's pick started), it
199
- shapes the pick in its own send; outside one, it shapes the one such pick outstanding only while one Agent's state is
200
- active. Outside a pick's own send, while two Agents' states are active, it sets nothing, whatever is outstanding: the
201
- event cannot say whose pick it is. On an entry or an exit below no Agent's state it sets nothing, whatever is
202
- outstanding, and the load walk warns, naming the Agent states it could be moved under: an entry or exit there is no
203
- transition an Agent's event takes. One action object written in two places runs from each by that place's rule alone,
204
- never by their sum: written below two Agents' states, from A's state it shapes only A's pick and from B's only B's;
205
- written below A's state and on a transition below none, from A's state it shapes only A's pick and from the transition
206
- the pick of the Agent the event names; written on an entry or exit below no Agent's state as well, it sets nothing as
207
- that entry or exit. An `answer` that an `enqueueActions` enqueues, at any depth, runs from the place the enqueuing
208
- action is written, by that place's rule: enqueued on an entry or exit below no Agent's state it sets nothing, and
209
- enqueued below an Agent's state it shapes only that Agent's pick. The load walk cannot see inside an `enqueueActions`,
210
- so it does not warn on one enqueued on an entry or exit below no Agent's state. An `answer` that runs while no pick of
211
- that Agent is being answered (a state's first entry, a later Turn's re-entry, another Agent's region of a parallel
212
- state) sets nothing. A reply comes from a child invoked on an active state below the Agent's state, or from a party
213
- whose Gate invoke id or Agent slot is invoked there (its sender, ADR-0015). The events the Machine sent itself (a
214
- `raise`, a `sendTo(self)`, a timer) are no reply, and neither is an event from outside that set (a Gate in a sibling
215
- region, a root Gate, an outside send), nor a child's error: every outcome of work is its output, and an error is a jr2
216
- or config defect, so by default its message never reaches the model. The operator reads it: while a pick is
217
- outstanding, an error from a child below the Agent's state goes on the run's feed (`child-error`: the Agent, its pick,
218
- the child and the message; Instance-token band, as `fault` is), and `jr2 run` and `jr2 logs` print it. An author who
219
- wants the Agent told passes it explicitly: `answer(...)` on the `onError` route (ADR-0067), or an `onError` route to a
220
- state whose prompt carries it. With none, the answer carries no reply, and its words say only that ("No reply came
221
- back for it"): never that nothing ran, because a timer or an outside event may have stopped work that did run. The
222
- pick's `POST` answers at once with `outstanding` and a delivery id, and the Harness long-polls the answer inside the
223
- one tool call (ADR-0002, ADR-0013). A child that settles inside the pick's own send — a decision Machine that is final
224
- as it starts — replies before the `POST` answers: when that reply leaves the workflow at Rest, or out of the Agent's
225
- state, the `POST` itself is the answer, with `answered` and nothing to poll. So is a guard straight back: a pick that
226
- enters a state and leaves it again inside its own send (an `always` back), with no reply, leaves the state value as it
227
- was, and is answered with `straightBack` — the one answer that says "it moved the workflow, and nothing ran". An
228
- outstanding pick's answer never carries it. A pick that changes the state value moved to a mode, and gets the plain
229
- receipt with its Allowed picks; a pick with no target went nowhere, and gets the same, as does a pick whose target is
230
- the state it was taken in (it enters nothing it leaves). Every receipt that leaves the Turn open (the plain receipt, a
231
- guard straight back, and an answered pick back at Rest) ends its state part with the same words, "Your turn is not
232
- over.": the plain receipt once said "the workflow is still in the state that asked for this turn, so it is not over
233
- yet", and real Haiku 5.5 runs stopped after it, because "it" can be the workflow or the Turn. The refusals and the
234
- notices of a cut or unsent pick keep their own words. A route that leaves the Agent's state ends the Turn like any
235
- other: the poll reads `turnComplete`, the next state's prompt is the author's, and the answer is a `pick-answer`
236
- notice to the Agent that picked: the next Turn of the conversation the picking Turn continued (`continue: true`,
237
- ADR-0062). A FRESH picking Turn's conversation is never heard again, and a later continued Turn of the same slot never
238
- made the pick, so its answer goes nowhere: the ledger keeps no notice for it. The route of a reply includes the events
239
- the Machine sends itself AT ONCE while it runs (a `raise`, or a `sendTo(self)` with no delay, in `onDone` or in the
240
- next state's entry): a reply whose route leaves the Agent's state that way is the answer, never stopped. Something
241
- other than the route can leave the Agent's state while the pick's work still runs below it — a timer the Machine set
242
- (a Turn timeout `after`, a named delay, a delayed `raise` or `sendTo(self)`), an event from outside the states below,
243
- a Gate's event, the Agent's own end. The exit stops that work (an invoke is stopped when its state exits), and the
244
- answer says so (`stopped`): the poll reads "The workflow left the state that asked for this turn while it ran your
245
- pick, and that stopped the pick's work before it finished: your turn is over.", and the next continued Turn's notice
246
- reads "Your pick `X` did not finish: the workflow left the state that asked for your last turn while it ran, and that
247
- stopped it." Both keep the last reply an earlier link gave, if any, and state it: an earlier link's reply is never
248
- dropped because a later link was stopped. The run host tells a fired timer from an event sent at once by the
249
- scheduler's own table: it watches what the actor system arms, cancels and fires (`timers.ts`, the same xstate
250
- internals as the timer table, ADR-0007). Back at Rest INSIDE the Agent's state — a per-step timeout `after` on the
251
- child state that exits the work — the answer is the last reply since the pick, with the Allowed picks; with no reply
252
- since the pick, the answer carries none. A restored hold is the Turn of the first registration of its address in the
253
- restored tree: a restore that ends that Turn and starts another at the same address before anything settles answers
254
- the pick with `turnComplete`. There is no default time bound on a held Turn; the Console shows each outstanding pick
255
- "held since".
192
+ no pick being answered, it sets nothing. One Agent slot can be invoked by two states that are not active together (two
193
+ states one after the other). Then it shapes the pick at the same invoke as the pick that sent the event or started the
194
+ child, and nothing otherwise: the done event of a child that the earlier state's pick started, after that pick's Turn
195
+ ended (its child runs on), shapes no pick, and the later Turn's pick keeps its default answer. When the event names no
196
+ Agent (a timer, a Gate's answer, an outside send, the done event of a child no Agent's pick started), it shapes the
197
+ pick in its own send; outside one, it shapes the one such pick outstanding only while one Agent's state is active.
198
+ Outside a pick's own send, while two Agents' states are active, it sets nothing, whatever is outstanding: the event
199
+ cannot say whose pick it is. On an entry or an exit below no Agent's state it sets nothing, whatever is outstanding,
200
+ and the load walk warns, naming the Agent states it could be moved under: an entry or exit there is no transition an
201
+ Agent's event takes. One action object written in two places runs from each by that place's rule alone, never by their
202
+ sum: written below two Agents' states, from A's state it shapes only A's pick and from B's only B's; written below A's
203
+ state and on a transition below none, from A's state it shapes only A's pick and from the transition the pick of the
204
+ Agent the event names; written on an entry or exit below no Agent's state as well, it sets nothing as that entry or
205
+ exit. An `answer` that an `enqueueActions` enqueues, at any depth, runs from the place the enqueuing action is
206
+ written, by that place's rule: enqueued on an entry or exit below no Agent's state it sets nothing, and enqueued below
207
+ an Agent's state it shapes only that Agent's pick. The load walk cannot see inside an `enqueueActions`, so it does not
208
+ warn on one enqueued on an entry or exit below no Agent's state. An `answer` that runs while no pick of that Agent is
209
+ being answered (a state's first entry, a later Turn's re-entry, another Agent's region of a parallel state) sets
210
+ nothing. A reply comes from a child invoked on an active state below the Agent's state, or from a party whose Gate
211
+ invoke id or Agent slot is invoked there (its sender, ADR-0015). The events the Machine sent itself (a `raise`, a
212
+ `sendTo(self)`, a timer) are no reply, and neither is an event from outside that set (a Gate in a sibling region, a
213
+ root Gate, an outside send), nor a child's error: every outcome of work is its output, and an error is a jr2 or config
214
+ defect, so by default its message never reaches the model. The operator reads it: while a pick is outstanding, an
215
+ error from a child below the Agent's state goes on the run's feed (`child-error`: the Agent, its pick, the child and
216
+ the message; Instance-token band, as `fault` is), and `jr2 run` and `jr2 logs` print it. An author who wants the Agent
217
+ told passes it explicitly: `answer(...)` on the `onError` route (ADR-0067), or an `onError` route to a state whose
218
+ prompt carries it. With none, the answer carries no reply, and its words say only that ("No reply came back for it"):
219
+ never that nothing ran, because a timer or an outside event may have stopped work that did run. The pick's `POST`
220
+ answers at once with `outstanding` and a delivery id, and the Harness long-polls the answer inside the one tool call
221
+ (ADR-0002, ADR-0013). A child that settles inside the pick's own send — a decision Machine that is final as it starts
222
+ — replies before the `POST` answers: when that reply leaves the workflow at Rest, or out of the Agent's state, the
223
+ `POST` itself is the answer, with `answered` and nothing to poll. So is a guard straight back: a pick that enters a
224
+ state and leaves it again inside its own send (an `always` back), with no reply, leaves the state value as it was, and
225
+ is answered with `straightBack` — the one answer that says "it moved the workflow, and nothing ran". An outstanding
226
+ pick's answer never carries it. A pick that changes the state value moved to a mode, and gets the plain receipt with
227
+ its Allowed picks; a pick with no target went nowhere, and gets the same, as does a pick whose target is the state it
228
+ was taken in (it enters nothing it leaves). Every receipt that leaves the Turn open (the plain receipt, a guard
229
+ straight back, and an answered pick back at Rest) ends its state part with the same words, "Your turn is not over.":
230
+ the plain receipt once said "the workflow is still in the state that asked for this turn, so it is not over yet", and
231
+ real Haiku 5.5 runs stopped after it, because "it" can be the workflow or the Turn. The refusals and the notices of a
232
+ cut or unsent pick keep their own words. A route that leaves the Agent's state ends the Turn like any other: the poll
233
+ reads `turnComplete`, the next state's prompt is the author's, and the answer is a `pick-answer` notice to the Agent
234
+ that picked: the next Turn of the conversation the picking Turn continued (`continue: true`, ADR-0062). A FRESH
235
+ picking Turn's conversation is never heard again, and a later continued Turn of the same slot never made the pick, so
236
+ its answer goes nowhere: the ledger keeps no notice for it. The route of a reply includes the events the Machine sends
237
+ itself AT ONCE while it runs (a `raise`, or a `sendTo(self)` with no delay, in `onDone` or in the next state's entry):
238
+ a reply whose route leaves the Agent's state that way is the answer, never stopped. Something other than the route can
239
+ leave the Agent's state while the pick's work still runs below it — a timer the Machine set (a Turn timeout `after`, a
240
+ named delay, a delayed `raise` or `sendTo(self)`), an event from outside the states below, a Gate's event, the Agent's
241
+ own end. The exit stops that work (an invoke is stopped when its state exits), and the answer says so (`stopped`): the
242
+ poll reads "The workflow left the state that asked for this turn while it ran your pick, and that stopped the pick's
243
+ work before it finished: your turn is over.", and the next continued Turn's notice reads "Your pick `X` did not
244
+ finish: the workflow left the state that asked for your last turn while it ran, and that stopped it." Both keep the
245
+ last reply an earlier link gave, if any, and state it: an earlier link's reply is never dropped because a later link
246
+ was stopped. The run host tells a fired timer from an event sent at once by the scheduler's own table: it watches what
247
+ the actor system arms, cancels and fires (`timers.ts`, the same xstate internals as the timer table, ADR-0007). Back
248
+ at Rest INSIDE the Agent's state — a per-step timeout `after` on the child state that exits the work — the answer is
249
+ the last reply since the pick, with the Allowed picks; with no reply since the pick, the answer carries none. A
250
+ restored hold is the Turn of the first registration of its address in the restored tree: a restore that ends that Turn
251
+ and starts another at the same address before anything settles answers the pick with `turnComplete`. There is no
252
+ default time bound on a held Turn; the Console shows each outstanding pick "held since".
256
253
  - **The one exception is the Orchestrator ending it for its own reasons**, which is ADR-0007's requirement and exactly
257
254
  one place: `RunHost.stop()`. Process shutdown never stops actors at all (`instance.ts` calls `host.close()`, which
258
255
  ends observation feeds and nothing else), and restore is a fresh process. So "the host did this" is a flag it sets,
@@ -106,6 +106,13 @@ does not exist for a caller that owns tool assembly. jr2 owns the rest: the engi
106
106
  no adapter: a test rig can pass another (`TurnDeps.engineAdapter`). So a second engine changes `engine-choice.ts`, not
107
107
  the Turn. The Working tools, the printer, the transcript and Compaction do not change.
108
108
 
109
+ - **The Working tools run as confined children beside the engine.** The engine runs in the Harness process, which runs
110
+ jr2's code only and holds the control credential
111
+ ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)). Each process that a
112
+ Working tool starts — each `bash` call and what it runs, and the search tools' `rg` or `grep` — runs confined by the
113
+ Effector's Landlock confiner ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), with an
114
+ explicit env. The file tools (read, write, edit) run in the Harness process, and resolve each path beneath `/work`,
115
+ with no escape by `..` or by a symbolic link. A second engine keeps this rule: it is the Harness's, not pi's.
109
116
  - **Assembly is runtime construction, not codegen.** `main.ts` constructs the server and listens; each admission carries
110
117
  its Agent's definition (ADR-0049), validated loudly and re-read per Submission. `boot.mjs`, the generated shims, the
111
118
  boot-time `flue build`, and the readiness lag it caused are deleted, and so — later — was the `JR2_AGENTS_JSON` roster
@@ -219,7 +226,8 @@ does not exist for a caller that owns tool assembly. jr2 owns the rest: the engi
219
226
  - **Kept verbatim**: the wire, the stub as the mechanics fixture, `AgentRunPort` and the admission ledger, ADR-0024's
220
227
  rule, receipt, and ordering, and the container split — restated in jr2 terms: the pod's credentials live in a separate
221
228
  container (the Custodian, ADR-0059) _because working tools execute in the Harness container_, so the
222
- credential-isolation premise survives while `local()` leaves the vocabulary.
229
+ credential-isolation premise survives while `local()` leaves the vocabulary. The one credential that the Harness
230
+ process holds, the control Stand-in, is out of reach of the Working tools' children, which run confined (ADR-0076).
223
231
  - **The `flue-contract` tier dissolves into the conformance suite.** Its rig — scripted OpenAI-compatible provider, the
224
232
  Menu over a real socket to a killable fake Orchestrator, real server — moves into `packages/harness/test/` and runs in
225
233
  the default `test` gate: the opt-in-ness existed only because the tier owned a foreign pin and build. Its claims
@@ -13,8 +13,8 @@ instructions that say "Do not modify the code yourself".
13
13
  [ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md) already established what that means: the same
14
14
  prompt, model, and state produce compliance on one run and not the next, "which is what rules out fixing this with
15
15
  words." The prose ban is a persona, not a boundary. ADR-0024 also named the stakes — an Agent with write access is a
16
- concurrent writer in a Workspace the Machine believes is single-threaded — and
17
- [ADR-0012](0012-workspace-wrapper-machine.md) puts coder and reviewer in that one Workspace on purpose.
16
+ writer that the Machine did not plan for — and [ADR-0012](0012-workspace-wrapper-machine.md) puts coder and reviewer in
17
+ one Workspace on purpose.
18
18
 
19
19
  Under the retired flue runtime this had no seat — its tool list was take-it-or-replace-it, and the only recorded out was
20
20
  ejecting to a foreign harness project. [ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)
@@ -41,19 +41,20 @@ definition can state.
41
41
  tools (the tool set that the Harness gives the loop for each Submission). `bash` stays — the reviewer's own
42
42
  instructions require running the tests — and the ADR says plainly what that means: a shell can write, so the tool
43
43
  layer states intent and stops the honest path. It is known leaky, and it is not the guarantee.
44
- - **The worktree layer contains the blast radius: a reviewer works in a detached review worktree.** The attach step —
45
- already jr2-owned (`sandbox-kubectl.ts`'s `attachScript`) — adds a worktree at the sha under review with a detached
46
- HEAD, sibling to the branch worktree per the Project layout. A write there cannot move the branch or dirty the coder's
47
- worktree; a rogue commit lands on a detached HEAD and evaporates with the checkout. This is not a permission boundary
48
- — it is containment, the property ADR-0024 bought for turns, bought for the filesystem.
44
+ - **The worktree layer contains the blast radius: a reviewer works in a detached review Worktree.** The Machine cuts it
45
+ with `worktree()`, `{ path, detach: sha }`, at the sha under review, with a detached HEAD, sibling to the coder's
46
+ Worktree per the Project layout ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)). A write
47
+ there cannot move the branch or dirty the coder's Worktree; a rogue commit lands on a detached HEAD and goes when the
48
+ next cut forces the folder. This is not a permission boundary — it is containment, the property ADR-0024 bought for
49
+ turns, bought for the filesystem.
49
50
  - **Two layers, one structure: the tool list is the hint, the worktree is the guarantee** — the same shape as ADR-0024's
50
51
  receipt (hint) and abort (guarantee). Neither layer alone: tools without the worktree leak through bash; the worktree
51
52
  without tools invites the model to fight its checkout instead of declining politely.
52
- - **The reviewing state requests the review worktree per round.** The sha under review moves between rounds, so the
53
- detached worktree is attached (idempotently, by sha) on entry to the reviewing state, and its path frames the
54
- reviewer's turn — cwd and prompt — instead of the branch worktree. The exact Workspace-port verb is settled at
55
- implementation; what is decided here is the seat: worktree mechanics stay in the attach step and the Workspace
56
- wrapper, never in Agent instructions.
53
+ - **A state before the reviewing state cuts the review Worktree on each round.** The sha under review moves between
54
+ rounds, so each round cuts the one folder again at the new sha. The cut forces the folder to the commit and cleans it
55
+ with `-ffd`. Ignored files stay, because the reviewer runs the tests. The folder's path frames the reviewer's turn —
56
+ cwd and prompt — instead of the coder's Worktree. What is decided here is the seat: worktree mechanics stay in
57
+ `worktree()` and the Machine's states, never in Agent instructions.
57
58
 
58
59
  ## Considered options
59
60
 
@@ -42,7 +42,10 @@ built.
42
42
  carries the Instance Harness's model calls the same way. It holds only the model keys of the `"none"` Agents it hosts
43
43
  — the keys their providers read — and none of the held secrets a `workspace()` lists: a `"none"` Agent cannot execute
44
44
  code, so it had no way to use the rest. Its held objects are its own (`held-instance.json`,
45
- `jr2-held-secrets-instance`). Defense-in-depth is the bonus, not the reason.
45
+ `jr2-held-secrets-instance`). Its held secrets get secret Stand-ins, as in every Harness pod: `jr2 up` mints them with
46
+ the signing key when it writes that pod's Secrets, as it does for the Instance Effector's key
47
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). It starts no confined child, so this
48
+ is for consistency, not a boundary. Defense-in-depth is the bonus, not the reason.
46
49
  - **The Instance Effector is the second Instance-level pod, beside it**
47
50
  ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), on the same convention: `jr2 up`
48
51
  converges it when a registered Machine places an Effect there — an `http` Effect — and deletes it otherwise. It runs
@@ -24,9 +24,9 @@ machine is the one piece of its vocabulary that pattern missed.
24
24
  Thus a `workspace()` or `pool()` wrapper with its own door passes. A `.provide()` clone keeps the config of its
25
25
  original, so the walk gets the same result for the clone. A Machine with no `input`, or with an `input` that is not
26
26
  zod, can use `setup()`. The `ping` Workflow that `jr2 init` writes uses it. The walk checks the registered roots only,
27
- because the `input` of a nested Machine is never the door (see below). `workspace()` refuses a body with a zod `input`
28
- when you call it. It reads the config and the attached door, so it also refuses a body that `setup()` made. Considered
29
- and rejected:
27
+ because a run starts at a root, and a door under a parent is parsed at invoke (see below). `workspace()` refuses a
28
+ body with a zod `input` when you call it. It reads the config and the attached door, so it also refuses a body that
29
+ `setup()` made. Considered and rejected:
30
30
  - Refuse all `setup()` Machines. This adds work to the first Workflow and gives nothing.
31
31
  - Refuse at boot. Then an Assembly that is already published stops the Orchestrator. The walk refuses only the next
32
32
  publish.
@@ -34,7 +34,7 @@ machine is the one piece of its vocabulary that pattern missed.
34
34
  two ways to declare one door. Also, the zod schema stays in `machine.config`, and a schema must not go into the
35
35
  fingerprint and serialization paths that read the config. And that Machine does not get the Vocabulary checks of
36
36
  `jr2Setup`.
37
- - **A wrapper declares its own door; it never borrows its child's.** `workspace(body, { input, spec })` and
37
+ - **A wrapper declares its own door; it never borrows its child's.** `workspace(body, { input })` and
38
38
  `pool(worker, { input, … })` each take an `input` of their own, and neither propagates one upward. The reason is the
39
39
  same in both, at different strengths: a wrapper does not feed its child what a caller sent. `pool` is the sharp case —
40
40
  a worker is fed per-item, never the run body, so propagating its schema would demand fields the pool never passes and
@@ -55,10 +55,10 @@ machine is the one piece of its vocabulary that pattern missed.
55
55
  it was rejected because the author then chooses between two names for one statement, and finds the second only after
56
56
  the opaque error. A body that _still_ declares a schema is refused by `workspace()` with a message naming the fix:
57
57
  nothing would ever serve or validate it, and a contract nobody enforces is worse than none.
58
- - **The door types the mapping, and constrains the body.** `WorkspaceOptions.spec` reads its `input` from the declared
59
- schema (`z.infer`), so a workflow states its shape once and the workspace mapping is checked against it — the drift
60
- this ADR exists to close, closed in the one place that used to re-declare the shape by hand. The same schema checks
61
- the _body_, in **one direction only**: the body may not demand more than the wrapper will hand it, which is
58
+ - **The door types the mapping, and constrains the body.** A per-run Repo Slot's mapper reads its `input` from the
59
+ declared schema (`z.infer`), so a workflow states its shape once and the workspace mapping is checked against it — the
60
+ drift this ADR exists to close, closed in the one place that used to re-declare the shape by hand. The same schema
61
+ checks the _body_, in **one direction only**: the body may not demand more than the wrapper will hand it, which is
62
62
  `Workspaced<door, slots>`; demanding _less_ is safe, because it is fed a superset. So a body reading a field the door
63
63
  never carries is a build error at the `workspace()` call instead of an `undefined` mid-run. The guard counts the raw
64
64
  door as provided beside `Workspaced<door, slots>`: for the door `z.object({})` the two differ, and a body that writes
@@ -75,22 +75,21 @@ machine is the one piece of its vocabulary that pattern missed.
75
75
  a parent invoking a nested `workspace()` has its input mapper checked against that wrapper's door, and gets the body's
76
76
  output typed back. With no schema declared the door is permissive, so there is nothing to infer and the mapper's
77
77
  argument is `unknown` — an honest "jr2 does not know", not `any`; a wrapper fed by something other than a caller (a
78
- pool worker) states what it is fed by annotating the parameter, as `PoolSpec.cap`/`itemInput` do. `spec` takes the
79
- same permissive answer as the guard: its `input` is the door plus `HostInjectedInput`, so a `spec` gives each run a
80
- branch from `instanceId` with no cast, and a nested wrapper whose `spec` reads it needs a parent that gives it.
81
- `Partial<HostInjectedInput>` was the alternative; it was rejected because each root `spec` would then write a fallback
82
- for a value that is always there. The input of the wrapper stays the door alone, so no parent must send `instanceId`.
83
- For the door `z.object({})` that input is `{}`, the door without its `Record<string, never>`, so a parent can give
84
- `instanceId` to a nested `spec` that reads it; with the raw `z.infer` it could not, and the nested Workspace then
85
- pushed each run to `fix/undefined`. `z.infer<…> & Partial<HostInjectedInput>` was the alternative; it was rejected
86
- because the index signature makes `instanceId` `never` there too. A per-run Repo Slot's mapper keeps the door alone,
87
- because a url does not come from a run's id.
78
+ pool worker) states what it is fed by annotating the parameter, as `PoolSpec.cap`/`itemInput` do. The input of the
79
+ wrapper stays the door alone, so no parent must send `instanceId`, and a nested body that reads it needs a parent that
80
+ gives it. For the door `z.object({})` that input is `{}`, the door without its `Record<string, never>`, so a parent
81
+ can give `instanceId` to such a body; with the raw `z.infer` it could not. A per-run Repo Slot's mapper keeps the door
82
+ alone, because a url does not come from a run's id.
88
83
  - **The schema is structure, so it is open.** Workflow-detail JSON (`GET /workflows/:name`, the address ADR-0032's
89
84
  negotiation kept) serves it as JSON Schema — same band as the Machine doc, and what drives the Console's start form
90
85
  before any token is entered. The _submit_ stays `authenticated`; schema open, trigger guarded.
91
86
  - **A declared schema is enforced at the door.** `POST /workflows/:name/runs` validates the body and 400s naming the
92
87
  accepted shape — parity with what Gates already do for deliveries. No schema declared → accept anything (today's
93
- behavior), and the Console falls back from a generated form to a raw-JSON textarea.
88
+ behavior), and the Console falls back from a generated form to a raw-JSON textarea. A wrapper's door is parsed under a
89
+ parent too, so a parent that passes less than a `workspace()` or a `pool()` requires fails at invoke, before any pod.
90
+ That is a defect of the parent's Machine, so it goes to the parent's `onError`, and a Pool's `itemInput` is held to
91
+ the door of a `workspace()` worker in the same way. A plain child Machine's door is a type: no wrapper reads it at run
92
+ time.
94
93
  - **The door takes an idempotency key, out of band.** A start may carry one in a header, never in the body (a body field
95
94
  would be one every door had to reserve); a repeat with the same key answers with the run the first one made. A
96
95
  `dispatch` Effect sends its ledger row's id ([ADR-0071](0071-a-run-starts-a-run-through-a-dispatch-effect.md)), so a
@@ -101,14 +101,14 @@ every build) and promotes incidental image properties into contract items.
101
101
  its first provision, in the `preflight` init container's log, with an error that names the fix (because "node did not
102
102
  execute" is not actionable) — never as a mid-turn tool failure, and never as a converge refusal for a contract the
103
103
  image was not under.
104
- - **`workspace()` names the image as a STATIC OPTION, never in the per-run spec** — what the Sandbox is _made of_ is the
105
- wrapper's business in the same way its worktrees are, but it is a fact about the MACHINE, not about a run: `jr2 up`
106
- finds it by walking the registered Machines ([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)),
107
- and a spec is a function of run input that no walk can evaluate. The User Container rides the same rule and the same
108
- resolution as one more option beside `image` (`user?: string`, [ADR-0005](0005-sandbox-pod-composition.md)). Neither
109
- is ever persisted: the option rides the Machine (a parts map keyed on `machine.config`, as the vocabulary is) and the
110
- provisioning state re-reads it at invoke time, so a restore provisions what the Machine carries now — and a resolved
111
- content-addressed tag, which would outlive the image it names, never leaves the port's side of the seam at all.
104
+ - **`workspace()` names the image as a STATIC OPTION, never from run input** — what the Sandbox is _made of_ is the
105
+ wrapper's business in the same way its clones are, and it is a fact about the MACHINE, not about a run: `jr2 up` finds
106
+ it by walking the registered Machines ([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)), and no
107
+ walk can evaluate a function of run input. The User Container rides the same rule and the same resolution as one more
108
+ option beside `image` (`user?: string`, [ADR-0005](0005-sandbox-pod-composition.md)). Neither is ever persisted: the
109
+ option rides the Machine (a parts map keyed on `machine.config`, as the vocabulary is) and the provisioning state
110
+ re-reads it at invoke time, so a restore provisions what the Machine carries now — and a resolved content-addressed
111
+ tag, which would outlive the image it names, never leaves the port's side of the seam at all.
112
112
  - **A Menu-only Agent never gets a Sandbox Image.** `workspace: "none"` withholds the entire Working toolset (ADR-0028),
113
113
  so there is no tooling to carry; the Instance Harness (ADR-0031) runs the stock image permanently. The feature has
114
114
  exactly one seat, and it is the Workspace.
@@ -31,11 +31,11 @@ because they are one problem: **what a Machine depends on that is not inside it.
31
31
  names of the secrets it names — or the `open` sentinel where a package left the url, the argv or a secret's name for
32
32
  its composer ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)). `jr2 up` reads them
33
33
  without running anything, and a secret named there is what places it.
34
- - **The Sandbox Image is a `workspace()` option**, static: `workspace(body, { input, image, repos, spec })` (`repos`
35
- is ADR-0051's, and follows the same rule). `image` is a string in one of two shapes — a `file:` URL
34
+ - **The Sandbox Image is a `workspace()` option**, static: `workspace(body, { input, image, repos, workGroup })`
35
+ (`repos` is ADR-0051's, and follows the same rule). `image` is a string in one of two shapes — a `file:` URL
36
36
  (`import.meta.resolve("./image")`, a docker context the module ships) or a registry ref — or absent, which keeps
37
- ADR-0037's fallback. It moved out of the per-run spec because `jr2 up` must find it statically. It is never
38
- persisted: the provisioning state re-reads it off the Machine on restore.
37
+ ADR-0037's fallback. It is static because `jr2 up` must find it with no run. It is never persisted: the provisioning
38
+ state re-reads it off the Machine on restore.
39
39
  - **The definition rides the Turn.** The admission body carries the slot's definition; the Harness runs what it was
40
40
  handed and re-reads it per Submission as before. The Instance roster (`agents/`, then `config.agents`), its ConfigMap,
41
41
  and `JR2_AGENTS_JSON` retire — a flat roster cannot hold two `researcher`s, and a Machine edit already needs `jr2 up`