@jr2/orchestrator 0.5.1 → 0.7.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 (350) hide show
  1. package/console/components/app.ts +55 -15
  2. package/console/components/bottom-drawer.ts +259 -0
  3. package/console/components/drawer.ts +14 -6
  4. package/console/components/fleet.ts +36 -13
  5. package/console/components/machine-pane.ts +61 -5
  6. package/console/components/nav.ts +54 -8
  7. package/console/conversation.ts +161 -0
  8. package/console/login.ts +123 -0
  9. package/console/main.ts +600 -60
  10. package/console/store.ts +611 -64
  11. package/console/style.css +231 -0
  12. package/docs/agents-block.md +2 -2
  13. package/docs/concepts/agents-and-turns.md +142 -29
  14. package/docs/concepts/architecture.md +60 -34
  15. package/docs/concepts/callers-and-hooks.md +205 -0
  16. package/docs/concepts/charts.md +13 -8
  17. package/docs/concepts/composition.md +10 -1
  18. package/docs/concepts/converge.md +7 -2
  19. package/docs/concepts/durable-runs.md +43 -16
  20. package/docs/concepts/effects.md +133 -50
  21. package/docs/concepts/gates.md +95 -25
  22. package/docs/concepts/machines-and-workflows.md +14 -4
  23. package/docs/concepts/pools-and-sources.md +193 -17
  24. package/docs/concepts/secrets-and-trust.md +186 -62
  25. package/docs/concepts/the-menu.md +14 -0
  26. package/docs/concepts/workspaces.md +135 -56
  27. package/docs/get-started/a-model-decides.md +14 -13
  28. package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
  29. package/docs/get-started/connect-a-model.md +1 -1
  30. package/docs/get-started/create-an-instance.md +17 -14
  31. package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
  32. package/docs/get-started/install.md +4 -0
  33. package/docs/guides/add-tools-to-the-sandbox.md +72 -15
  34. package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
  35. package/docs/guides/answer-a-gate.md +74 -37
  36. package/docs/guides/bind-open-parts.md +29 -12
  37. package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
  38. package/docs/guides/cancel-a-run.md +26 -16
  39. package/docs/guides/cut-a-worktree.md +560 -0
  40. package/docs/guides/fix-a-failed-converge.md +19 -7
  41. package/docs/guides/give-a-caller-a-scope.md +203 -0
  42. package/docs/guides/give-an-agent-mcp-tools.md +422 -0
  43. package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
  44. package/docs/guides/let-an-agent-write-a-chart.md +14 -7
  45. package/docs/guides/look-inside-a-sandbox.md +115 -42
  46. package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
  47. package/docs/guides/open-the-console.md +63 -14
  48. package/docs/guides/push-and-sign-a-branch.md +67 -21
  49. package/docs/guides/read-a-conversation.md +225 -0
  50. package/docs/guides/run-a-chart-by-name.md +17 -16
  51. package/docs/guides/run-a-command-or-call-a-url.md +84 -41
  52. package/docs/guides/run-one-worker-per-item.md +34 -27
  53. package/docs/guides/set-an-agents-access-and-dials.md +69 -22
  54. package/docs/guides/size-a-workspace.md +19 -6
  55. package/docs/guides/start-a-run-for-each-alert.md +290 -0
  56. package/docs/guides/start-a-run-from-a-run.md +18 -16
  57. package/docs/guides/start-a-run.md +24 -18
  58. package/docs/guides/steer-an-agent.md +332 -0
  59. package/docs/guides/use-a-hosted-provider.md +7 -1
  60. package/docs/guides/watch-a-run.md +146 -117
  61. package/docs/guides/write-a-hook-kind.md +238 -0
  62. package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
  63. package/docs/guides/write-a-workflow.md +18 -8
  64. package/docs/llms.txt +110 -23
  65. package/docs/reference/authoring-surface/agent.md +35 -12
  66. package/docs/reference/authoring-surface/answer.md +20 -4
  67. package/docs/reference/authoring-surface/arg.md +28 -6
  68. package/docs/reference/authoring-surface/basic.md +1 -1
  69. package/docs/reference/authoring-surface/bearer.md +1 -1
  70. package/docs/reference/authoring-surface/branchname.md +15 -0
  71. package/docs/reference/authoring-surface/carry.md +23 -0
  72. package/docs/reference/authoring-surface/chartschema.md +1 -1
  73. package/docs/reference/authoring-surface/chartschemafor.md +1 -1
  74. package/docs/reference/authoring-surface/command.md +37 -22
  75. package/docs/reference/authoring-surface/customize.md +4 -1
  76. package/docs/reference/authoring-surface/defineconfig.md +1 -1
  77. package/docs/reference/authoring-surface/defineevent.md +1 -1
  78. package/docs/reference/authoring-surface/dispatch.md +5 -4
  79. package/docs/reference/authoring-surface/fail.md +18 -5
  80. package/docs/reference/authoring-surface/file.md +37 -17
  81. package/docs/reference/authoring-surface/frompick.md +1 -1
  82. package/docs/reference/authoring-surface/gate.md +118 -0
  83. package/docs/reference/authoring-surface/group-agents.md +15 -0
  84. package/docs/reference/authoring-surface/group-charts.md +16 -0
  85. package/docs/reference/authoring-surface/group-composition.md +25 -0
  86. package/docs/reference/authoring-surface/group-config.md +17 -0
  87. package/docs/reference/authoring-surface/group-effects.md +48 -0
  88. package/docs/reference/authoring-surface/group-failure.md +13 -0
  89. package/docs/reference/authoring-surface/group-gates.md +16 -0
  90. package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
  91. package/docs/reference/authoring-surface/group-steering.md +12 -0
  92. package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
  93. package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
  94. package/docs/reference/authoring-surface/http.md +109 -9
  95. package/docs/reference/authoring-surface/isfailure.md +1 -1
  96. package/docs/reference/authoring-surface/isready.md +14 -0
  97. package/docs/reference/authoring-surface/jr2setup.md +14 -4
  98. package/docs/reference/authoring-surface/mcp.md +107 -0
  99. package/docs/reference/authoring-surface/open.md +41 -8
  100. package/docs/reference/authoring-surface/openbody.md +1 -1
  101. package/docs/reference/authoring-surface/paramsof.md +23 -5
  102. package/docs/reference/authoring-surface/pool.md +79 -11
  103. package/docs/reference/authoring-surface/push.md +31 -13
  104. package/docs/reference/authoring-surface/readyof.md +21 -0
  105. package/docs/reference/authoring-surface/reread.md +19 -0
  106. package/docs/reference/authoring-surface/secret.md +26 -5
  107. package/docs/reference/authoring-surface/sentby.md +1 -1
  108. package/docs/reference/authoring-surface/skip.md +22 -0
  109. package/docs/reference/authoring-surface/source.md +59 -4
  110. package/docs/reference/authoring-surface/steer.md +93 -0
  111. package/docs/reference/authoring-surface/text.md +1 -1
  112. package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
  113. package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
  114. package/docs/reference/authoring-surface/type-agentslots.md +4 -1
  115. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  116. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  117. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  118. package/docs/reference/authoring-surface/type-binding.md +6 -7
  119. package/docs/reference/authoring-surface/type-caller.md +40 -0
  120. package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
  121. package/docs/reference/authoring-surface/type-carry.md +19 -0
  122. package/docs/reference/authoring-surface/type-chainlink.md +3 -3
  123. package/docs/reference/authoring-surface/type-commandinput.md +4 -3
  124. package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
  125. package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
  126. package/docs/reference/authoring-surface/type-customize.md +20 -3
  127. package/docs/reference/authoring-surface/type-delivered.md +17 -0
  128. package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
  129. package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
  130. package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
  131. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  132. package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
  133. package/docs/reference/authoring-surface/type-filebody.md +4 -8
  134. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  135. package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
  136. package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
  137. package/docs/reference/authoring-surface/type-gatelook.md +37 -0
  138. package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
  139. package/docs/reference/authoring-surface/type-gatereply.md +19 -0
  140. package/docs/reference/authoring-surface/type-gateslot.md +21 -0
  141. package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
  142. package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
  143. package/docs/reference/authoring-surface/type-gateslots.md +12 -0
  144. package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
  145. package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
  146. package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
  147. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  148. package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
  149. package/docs/reference/authoring-surface/type-jr2config.md +37 -2
  150. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  151. package/docs/reference/authoring-surface/type-lends.md +5 -1
  152. package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
  153. package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
  154. package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
  155. package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
  156. package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
  157. package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
  158. package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
  159. package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
  160. package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
  161. package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
  162. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  163. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
  164. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  165. package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
  166. package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
  167. package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
  168. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
  169. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
  170. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  171. package/docs/reference/authoring-surface/type-poolspec.md +11 -6
  172. package/docs/reference/authoring-surface/type-pushinput.md +22 -3
  173. package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
  174. package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
  175. package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
  176. package/docs/reference/authoring-surface/type-reposlot.md +6 -6
  177. package/docs/reference/authoring-surface/type-reread.md +18 -0
  178. package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
  179. package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
  180. package/docs/reference/authoring-surface/type-sender.md +26 -1
  181. package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
  182. package/docs/reference/authoring-surface/type-size.md +1 -1
  183. package/docs/reference/authoring-surface/type-skip.md +19 -0
  184. package/docs/reference/authoring-surface/type-slothandles.md +5 -13
  185. package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
  186. package/docs/reference/authoring-surface/type-sourcein.md +25 -0
  187. package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
  188. package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
  189. package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
  190. package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
  191. package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
  192. package/docs/reference/authoring-surface/type-steerargs.md +20 -0
  193. package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
  194. package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
  195. package/docs/reference/authoring-surface/type-workspaced.md +30 -9
  196. package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
  197. package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
  198. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  199. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  200. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  201. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  202. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  203. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  204. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  205. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  206. package/docs/reference/authoring-surface/workspace.md +66 -38
  207. package/docs/reference/authoring-surface/worktree.md +71 -0
  208. package/docs/reference/authoring-surface.md +88 -147
  209. package/docs/reference/cli.md +413 -74
  210. package/docs/reference/config.md +248 -6
  211. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
  212. package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
  213. package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
  214. package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
  215. package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
  216. package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
  217. package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
  218. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
  219. package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
  220. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
  221. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
  222. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
  223. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
  224. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
  225. package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
  226. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
  227. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
  228. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
  229. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
  230. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
  231. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
  232. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
  233. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
  234. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
  235. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
  236. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
  237. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
  238. package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
  239. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
  240. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
  241. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
  242. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
  243. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
  244. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
  245. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
  246. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
  247. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
  248. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
  249. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
  250. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
  251. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
  252. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
  253. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
  254. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
  255. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
  256. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
  257. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
  258. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
  259. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
  260. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
  261. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
  262. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
  263. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
  264. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
  265. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
  266. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  267. package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
  268. package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
  269. package/docs/reference/decisions.md +9 -4
  270. package/docs/reference/glossary.md +138 -45
  271. package/docs/reference/hooks.md +1077 -0
  272. package/docs/reference/http-api.md +988 -123
  273. package/docs/reference/machines.md +0 -1
  274. package/docs/why.md +7 -5
  275. package/package.json +10 -3
  276. package/src/actor.ts +194 -42
  277. package/src/agent.ts +56 -6
  278. package/src/ambient.ts +3 -5
  279. package/src/assembly-checks.ts +530 -4
  280. package/src/callers.ts +245 -0
  281. package/src/chart.ts +238 -26
  282. package/src/charts.ts +9 -2
  283. package/src/config.ts +566 -7
  284. package/src/conversations.ts +453 -0
  285. package/src/custodian.lua +17 -5
  286. package/src/custodian.ts +257 -77
  287. package/src/customize.ts +154 -7
  288. package/src/dispatch-effect.ts +142 -26
  289. package/src/durability.ts +2 -0
  290. package/src/duration.ts +20 -0
  291. package/src/effect-checks.ts +52 -4
  292. package/src/effect.ts +401 -132
  293. package/src/effector-wire.ts +278 -30
  294. package/src/fail.ts +17 -4
  295. package/src/gate-brand.ts +16 -0
  296. package/src/gate.ts +634 -48
  297. package/src/harness-client.ts +84 -35
  298. package/src/harness-env.ts +33 -0
  299. package/src/held-secrets.ts +197 -39
  300. package/src/holds.ts +5 -2
  301. package/src/hook-kinds/discord.ts +92 -0
  302. package/src/hook-kinds/door.ts +107 -0
  303. package/src/hook-kinds/github.ts +89 -0
  304. package/src/hook-kinds/gitlab.ts +72 -0
  305. package/src/hook-kinds/hook.ts +7 -0
  306. package/src/hook-kinds/index.ts +465 -0
  307. package/src/hook-kinds/slack.ts +151 -0
  308. package/src/hook-kinds/webhook.ts +113 -0
  309. package/src/hooks.ts +506 -0
  310. package/src/http-effect.ts +164 -70
  311. package/src/http.ts +933 -99
  312. package/src/index.ts +30 -5
  313. package/src/instance.ts +196 -26
  314. package/src/internal.ts +23 -3
  315. package/src/kube-client.ts +6 -3
  316. package/src/machine-doc.ts +29 -5
  317. package/src/mcp-effect.ts +424 -0
  318. package/src/model-keys.ts +24 -0
  319. package/src/mount-digest.ts +41 -7
  320. package/src/names.ts +82 -7
  321. package/src/notices.ts +576 -59
  322. package/src/open-body.ts +110 -30
  323. package/src/open-effect.ts +17 -4
  324. package/src/open.ts +40 -7
  325. package/src/parts.ts +125 -35
  326. package/src/party.ts +35 -1
  327. package/src/pick.ts +43 -9
  328. package/src/pool.ts +1172 -172
  329. package/src/registration.ts +249 -17
  330. package/src/repo-identity.ts +7 -3
  331. package/src/repos.ts +20 -2
  332. package/src/run-host.ts +1614 -190
  333. package/src/run-list.ts +191 -0
  334. package/src/sandbox-effect.ts +217 -66
  335. package/src/sandbox-kube.ts +116 -21
  336. package/src/secret-placement.ts +248 -35
  337. package/src/server.ts +53 -4
  338. package/src/setup.ts +158 -38
  339. package/src/snapshot-store.ts +565 -48
  340. package/src/steer.ts +111 -0
  341. package/src/stub-harness.ts +6 -2
  342. package/src/tokens.ts +362 -46
  343. package/src/vocabulary.ts +23 -4
  344. package/src/wire.ts +27 -10
  345. package/src/workspace.ts +314 -259
  346. package/src/worktree.ts +249 -0
  347. package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
  348. package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
  349. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
  350. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
@@ -33,16 +33,21 @@ carved out for "the Orchestrator ending a run for its own reasons" was being spe
33
33
  - **`stop()` stays public with no production caller, and is deliberately not on the wire.** It is correct, it is what
34
34
  the durability suite drives, and it is what a future graceful shutdown should call. What nobody asked for is a
35
35
  user-facing "park this run" verb.
36
+ - **A cancel of a standing run is also a pause.** The run ends `cancelled` as any run does, and jr2 does not start its
37
+ Workflow again until `jr2 run <workflow>` resumes it, with the items it held
38
+ ([ADR-0017](0017-pool-over-a-source-port.md)). Without the pause, jr2 would start the standing run again at the next
39
+ boot or publish, and a person could not stop it. The runs that it dispatched go on, as for any dispatch.
36
40
  - **A cancelled run's Workspaces are destroyed, unless the cancel says `keep`.** The run is over, so nothing can use
37
41
  them, and each holds its whole Size (ADR-0060) — a placed pod left to the idle GC is capacity for nobody. The
38
42
  wrapper's `teardown` is a STATE, entered when the body reaches final, and an actor stop skips it; so the host destroys
39
43
  every Sandbox the run owns after the stop, the same idempotent delete `teardown` runs.
40
44
  `POST /runs/:id/events { type: "CANCEL", keep: true }` — `jr2 send <run> --event CANCEL --keep` — leaves them for
41
- inspection instead: with no Lease they age out at the operator's idle timeout, and `jr2 ssh` reaches them until then.
42
- Whether to keep is a fact of the moment of the cancel, never a config key. A **faulted** run keeps its Workspaces as
43
- before ([ADR-0012](0012-workspace-wrapper-machine.md)'s destroy-less terminal, ADR-0021): a fault is the case nobody
44
- chose, and the pod is the evidence. A **failed** run deletes them, as a `done` run does: a Failure is an end that the
45
- Machine chose ([ADR-0073](0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md)).
45
+ inspection instead: with no Lease they age out at the operator's idle timeout, and `jr2 workspace shell` reaches them
46
+ until then; `jr2 workspace rm` removes one sooner. Whether to keep is a fact of the moment of the cancel, never a
47
+ config key. A **faulted** run keeps its Workspaces as before ([ADR-0012](0012-workspace-wrapper-machine.md)'s
48
+ destroy-less terminal, ADR-0021): a fault is the case nobody chose, and the pod is the evidence. A **failed** run
49
+ deletes them, as a `done` run does: a Failure is an end that the Machine chose
50
+ ([ADR-0073](0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md)).
46
51
 
47
52
  ## Considered options
48
53
 
@@ -57,8 +57,8 @@ does not exist for a caller that owns tool assembly. jr2 owns the rest: the engi
57
57
  ([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)). The model sent
58
58
  `bash` with `git add … && git commit …` and the pick `publish` in one model answer. In 3 of 10 trials, `publish`
59
59
  pushed before the commit existed. A `write` and a `bash` test run in one model answer had the same race. The rule is
60
- for all tools, the Working tools and the Menu. The cost is that independent reads of one model answer do not run at
61
- the same time. Four facts follow:
60
+ for all tools: the Working tools, the MCP tools and the Menu. The cost is that independent reads of one model answer
61
+ do not run at the same time. Four facts follow:
62
62
  - Only the first pick of a model answer runs
63
63
  ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)). The model wrote a later one before it
64
64
  read the first one's answer. The seam states two more facts, so that the Turn can find the first pick by itself. The
@@ -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
@@ -170,10 +177,11 @@ does not exist for a caller that owns tool assembly. jr2 owns the rest: the engi
170
177
  long as its run** (ADR-0031). The Harness persists each conversation to a directory as it goes and rebuilds it on
171
178
  boot, so a Harness restart inside a living pod keeps it; a Submission in flight at the restart settles `failed`.
172
179
  What is durable across a lost pod is Orchestrator-side — the admission ledger and snapshot
173
- ([ADR-0007](0007-durable-machine-state.md)), and `wait`'s reconnect-from-offset. A conversation that is gone
174
- surfaces as 404 → fault → workflow policy, the same family as `workspace.lost`. Provider-stream retry moves inside
175
- the turn: the Harness states 2 retries, and pi-ai runs them (`maxRetries`) — the seat ADR-0016 delegated to flue's
176
- `durability{}`.
180
+ ([ADR-0007](0007-durable-machine-state.md)), `wait`'s reconnect-from-offset, and the kept conversation, which a
181
+ person reads and no Turn continues from ([ADR-0023](0023-the-harness-prints-the-conversation.md)). A conversation
182
+ that is gone surfaces as 404 → fault → workflow policy, the same family as `workspace.lost`. Provider-stream retry
183
+ moves inside the turn: the Harness states 2 retries, and pi-ai runs them (`maxRetries`) — the seat ADR-0016
184
+ delegated to flue's `durability{}`.
177
185
  - **[ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md)'s enabling fact is reproduced
178
186
  as explicit code.** Each Submission reads `$JR2_CUSTODIAN_URL/agents/<iid>/surface` afresh and presents it to pi as
179
187
  the Menu, so the menu is per-turn with no push channel, no turn index, no second port. Menu tools keep the
@@ -219,7 +227,8 @@ does not exist for a caller that owns tool assembly. jr2 owns the rest: the engi
219
227
  - **Kept verbatim**: the wire, the stub as the mechanics fixture, `AgentRunPort` and the admission ledger, ADR-0024's
220
228
  rule, receipt, and ordering, and the container split — restated in jr2 terms: the pod's credentials live in a separate
221
229
  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.
230
+ credential-isolation premise survives while `local()` leaves the vocabulary. The one credential that the Harness
231
+ process holds, the control Stand-in, is out of reach of the Working tools' children, which run confined (ADR-0076).
223
232
  - **The `flue-contract` tier dissolves into the conformance suite.** Its rig — scripted OpenAI-compatible provider, the
224
233
  Menu over a real socket to a killable fake Orchestrator, real server — moves into `packages/harness/test/` and runs in
225
234
  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)
@@ -31,9 +31,12 @@ definition can state.
31
31
  Workspace (it is this ADR's title). A string field, JSON-serializable, riding the existing ConfigMap channel unchanged
32
32
  (ADR-0018's constraint holds).
33
33
  - **`"none"` is the Menu-only Agent — and the value also names the Harness that hosts the Agent.** `"none"` withholds
34
- the entire Working toolset: the Agent converses and picks from its Menu, nothing else — the decisioning persona (read
35
- inputs, pick the next event). Because such an Agent touches no Workspace, it does not run in one: `"write"`/`"read"`
36
- place a Turn on the enclosing Workspace's Harness; `"none"` places it on the
34
+ the entire Working toolset: the Agent converses, picks from its Menu and calls the MCP tools of the remote servers it
35
+ names
36
+ ([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)), and
37
+ runs no code — the decisioning persona (read inputs, pick the next event). "Menu-only" means "no Working tools": such
38
+ an Agent can also hold MCP tools. Because such an Agent touches no Workspace, it does not run in one:
39
+ `"write"`/`"read"` place a Turn on the enclosing Workspace's Harness; `"none"` places it on the
37
40
  [Instance Harness](0031-menu-only-agents-run-on-the-instance-harness.md), always — even when a `workspace()` encloses
38
41
  the invocation. Placement is definitional, which is this ADR's thesis extended one step: what an Agent may do to the
39
42
  Workspace decides where it runs. (`cwd` is moot for `"none"` — only Working tools consume it.)
@@ -41,19 +44,32 @@ definition can state.
41
44
  tools (the tool set that the Harness gives the loop for each Submission). `bash` stays — the reviewer's own
42
45
  instructions require running the tests — and the ADR says plainly what that means: a shell can write, so the tool
43
46
  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.
47
+ - **The worktree layer contains the blast radius: a reviewer works in a detached review Worktree.** The Machine cuts it
48
+ with `worktree()`, `{ path, detach: sha }`, at the sha under review, with a detached HEAD, sibling to the coder's
49
+ Worktree per the Project layout ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)). A write
50
+ there cannot move the branch or dirty the coder's Worktree; a rogue commit lands on a detached HEAD and goes when the
51
+ next cut forces the folder. This is not a permission boundary — it is containment, the property ADR-0024 bought for
52
+ turns, bought for the filesystem.
49
53
  - **Two layers, one structure: the tool list is the hint, the worktree is the guarantee** — the same shape as ADR-0024's
50
54
  receipt (hint) and abort (guarantee). Neither layer alone: tools without the worktree leak through bash; the worktree
51
55
  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.
56
+ - **An MCP allowlist is a guarantee about the credential, and jr2 offers it only where it can keep it.** An Agent's MCP
57
+ tools ([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md))
58
+ spend the server's token, so the allowlist guards that token, not the Workspace. A remote server needs a list (tool
59
+ names, or `"*"`): the Harness offers and runs only those tools. Where Workspace code of the Sandbox in which the
60
+ Agent's Turns run can spend that secret, Workspace code can call every tool, so `jr2 up` refuses a list and takes only
61
+ `"*"`. That is the case when the Sandbox's `workspace()` grants the secret, when a credentialed `command` of that
62
+ Workspace names it, or when an `http` Effect of that Workspace whose body is a Worktree file names it: the Agent
63
+ writes that body ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)). A `"none"` Agent's
64
+ Turns run on the Instance Harness, where no Workspace code runs, so it keeps its list. A stdio server runs as
65
+ Workspace code and gets no list. The token's own scopes are the outer limit. This is not the shape of
66
+ `workspace: "read"` above: there the tool list is the hint, because `bash` can write; here the list is the guarantee,
67
+ because no Workspace code can spend the secret.
68
+ - **A state before the reviewing state cuts the review Worktree on each round.** The sha under review moves between
69
+ rounds, so each round cuts the one folder again at the new sha. The cut forces the folder to the commit and cleans it
70
+ with `-ffd`. Ignored files stay, because the reviewer runs the tests. The folder's path frames the reviewer's turn —
71
+ cwd and prompt — instead of the coder's Worktree. What is decided here is the seat: worktree mechanics stay in
72
+ `worktree()` and the Machine's states, never in Agent instructions.
57
73
 
58
74
  ## Considered options
59
75
 
@@ -78,10 +94,11 @@ definition can state.
78
94
  - **The reviewer definition becomes `workspace: "read"`**, and the prose ban stays in its instructions as intent — the
79
95
  half of the contract a model reads — no longer as the mechanism. A decisioner becomes `workspace: "none"`.
80
96
  - **The definition stays plain data.** No code enters the ConfigMap channel; composition by re-export/spread (ADR-0018)
81
- is preserved. Custom tool _implementations_ remain out of the contract — this decision adds a restriction vocabulary,
82
- not an extension one. Growing extension tools is a separate, later decision.
83
- - **GLOSSARY.md arbitrates the three senses of "tools"**: the Menu (control plane — what an Agent may say) and Working
84
- tools (data plane — what it may do) become glossary entries; unqualified "tools" joins the Avoid lists.
97
+ is preserved. Tool _implementations_ stay out of the contract — this decision adds a restriction vocabulary, not an
98
+ extension one. The extension is MCP: an Agent names servers, and a server holds the code (ADR-0079).
99
+ - **GLOSSARY.md arbitrates the senses of "tools"**: the Menu (control plane — what an Agent may say), Working tools
100
+ (data plane — what it may do) and MCP tools (what it may do through a service, ADR-0079) are glossary entries;
101
+ unqualified "tools" joins the Avoid lists.
85
102
  - **The `@kind` tier gains the containment scenario**: from the review worktree, a write probe followed by a commit must
86
103
  leave the branch ref and the coder's worktree untouched.
87
104
  - **The zero-support guard stays available to workflows**: a workflow can still have both Agents report the sha and gate
@@ -21,12 +21,19 @@ whole history again. It also left the history calling tools the request no longe
21
21
 
22
22
  ## Decision
23
23
 
24
- - **The Menu is the whole of what an Agent can ever be offered in its Machine, and it is the tools block.** It is the
25
- union of the derived sets of every state that invokes an Agent of that name — the same scope as its continued
26
- conversation's id (ADR-0057). `jr2Setup.createMachine` derives it in ADR-0015's walk, in a fixed order, so it is
27
- byte-stable for every Turn of the conversation and for every fresh conversation of that Agent. The descriptions are
28
- the event defs' own and do not vary by state, so a def's description must hold at every writing of it the Agent can
29
- reach; the load warns where its writings on one path differ (ADR-0015).
24
+ - **The Menu is the whole of what an Agent can ever be offered in its Machine, and it is the Menu's part of the tools
25
+ block.** It is the union of the derived sets of every state that invokes an Agent of that name — the same scope as its
26
+ continued conversation's id (ADR-0057). `jr2Setup.createMachine` derives it in ADR-0015's walk, in a fixed order, so
27
+ it is byte-stable for every Turn of the conversation and for every fresh conversation of that Agent. The descriptions
28
+ are the event defs' own and do not vary by state, so a def's description must hold at every writing of it the Agent
29
+ can reach; the load warns where its writings on one path differ (ADR-0015).
30
+ - **An Agent's MCP tools are fixed for the conversation too.** The Harness reads a server's tools at the conversation's
31
+ first Turn, keeps the allowed ones sorted by name, records them with the conversation, and never reads them again for
32
+ it, across a park and a restart
33
+ ([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)). The
34
+ tools block is the Working tools, then the MCP tools, then the Menu, and it is the same bytes for every Turn of the
35
+ conversation. Any fixed order keeps the cache. A tool that the server took away stays in the block, and its call fails
36
+ with jr2's text.
30
37
  - **What one Turn may pick is its Allowed picks**: the invoking state's derived set (its own, its ancestors' and its
31
38
  modes' picks — ADR-0015), narrowed to what the live Machine would currently accept **from this Agent**: xstate's
32
39
  selection for an event that carries the Agent's sender, a guard that reads the payload counted as passing, must move
@@ -117,7 +124,8 @@ block was not perfect either: once the model invented a tool it had never been g
117
124
  cross-state `continue`.
118
125
  - **The Machine's whole agent-audience vocabulary as the Menu.** Rejected: it offers picks only another Agent's states
119
126
  handle.
120
- - **Put the Menu tools last.** Rejected: measured, a reordered tools block misses as completely as a changed one.
127
+ - **Narrow the Menu per Turn, with the Menu tools last so that only the tail of the block changes.** Rejected: measured,
128
+ a change at the tail of the tools block misses as completely as one at its head.
121
129
  - **Mask at decode time** (OpenAI `allowed_tools`, logit masking). Deferred: it is provider-specific — Anthropic has no
122
130
  subset form, and changing `tool_choice` there drops the messages cache; vLLM's Chat Completions refuses it. The design
123
131
  must hold without it; where it is cache-safe, it can be added in the presenter.
@@ -141,9 +149,9 @@ block was not perfect either: once the model invented a tool it had never been g
141
149
 
142
150
  ## Consequences
143
151
 
144
- - **One state can allow different picks as its context changes or its active mode changes; the Menu does not change.**
145
- The conformance tier asserts it: two Submissions with different Allowed picks send a byte-identical tools block
146
- (ADR-0027).
152
+ - **One state can allow different picks as its context changes or its active mode changes; the Menu and the MCP tools do
153
+ not change.** The conformance tier asserts it: two Submissions with different Allowed picks send a byte-identical
154
+ tools block (ADR-0027).
147
155
  - **A restatement is appended, never rewritten.** Conformance asserts that each request of a Submission is a byte prefix
148
156
  of the next, with the tools and the system prompt unchanged, and that the restatement sits right after the model
149
157
  answer's last tool result.
@@ -89,8 +89,9 @@ to ask about.
89
89
  ([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)) is the run's data, not its Machine's shape: the Chart's
90
90
  text and its hash ride the body's own snapshot, restore admits the text again and compares the hash, and a mismatch or
91
91
  a refusal is drift naming the body slot and the reason, after the stamp check and in the same words.
92
- - **A drifted run is not in `jr2 runs`**, which lists only the live registry. It is reachable through the announce line
93
- and `jr2 status <id>`. If stranded runs become common enough to browse, the listing is where that belongs.
92
+ - **A drifted run is in `jr2 runs --status drifted`** and in the Console's list of ended runs, with its reason
93
+ ([ADR-0009](0009-cli-and-instance-interface.md)), and `jr2 status <id>` reads it whole. It is not in the default
94
+ listing, which is the live runs.
94
95
  - **The digest is only as complete as the walk.** A state running `enqueueActions` can spawn children no static analysis
95
96
  recovers (`machine-doc.ts`'s `opaqueStates`); the opacity is folded into the hash, but what it hides is not. Drift
96
97
  inside such a subtree can go unnoticed.
@@ -39,16 +39,23 @@ built.
39
39
  the Harness has exactly one route to its Menu — the Custodian on `localhost` — and forking that path for one pod buys
40
40
  a divergence in the component this ADR keeps deliberately uniform. The pod is composed by the same function a
41
41
  Sandbox's is ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)), so its Custodian
42
- carries the Instance Harness's model calls the same way. It holds only the model keys of the `"none"` Agents it hosts
43
- — the keys their providers read — and none of the held secrets a `workspace()` lists: a `"none"` Agent cannot execute
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.
42
+ carries the Instance Harness's model calls the same way. It holds what the `"none"` Agents it hosts name — the keys
43
+ their providers read, and the secrets of the remote MCP servers they name
44
+ ([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)) — and
45
+ none of the held secrets a `workspace()` lists: a `"none"` Agent cannot execute code, so it had no way to use the
46
+ rest. A `"none"` Agent names no stdio MCP server, because the Instance Harness has no Landlock domain to start one in.
47
+ Its held objects are its own (`held-instance.json`, `jr2-held-secrets-instance`). Its held secrets get secret
48
+ Stand-ins, as in every Harness pod: `jr2 up` mints them with the signing key when it writes that pod's Secrets, as it
49
+ does for the Instance Effector's key ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)).
50
+ It starts no confined child, so this is for consistency, not a boundary. Defense-in-depth is the bonus, not the
51
+ reason.
46
52
  - **The Instance Effector is the second Instance-level pod, beside it**
47
53
  ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), on the same convention: `jr2 up`
48
- converges it when a registered Machine places an Effect there — an `http` Effect — and deletes it otherwise. It runs
49
- the same `jr2-harness` image with `/opt/jr2/bin/effector` as its command, holds no conversation and runs no Agent, and
50
- is a Deployment, not a StatefulSet: it keeps nothing, and a rollout must run the new pod beside the old one. No config
51
- key names, sizes, addresses, or enables it.
54
+ converges it when a registered Machine places an Effect there — an `http`, an `mcp` or a targeted `dispatch` Effect —
55
+ or a Hook needs it (a `mac` verifier, or a kind that declares `posts`), and deletes it otherwise. It runs the same
56
+ `jr2-harness` image with `/opt/jr2/bin/effector` as its command, holds no conversation and runs no Agent, and is a
57
+ Deployment, not a StatefulSet: it keeps nothing, and a rollout must run the new pod beside the old one. No config key
58
+ names, sizes, addresses, or enables it.
52
59
  - **No image config at all.** The Harness image was once configured as `sandbox.image` because the Sandbox pod was the
53
60
  only place a Harness ran — a misnomer once the Instance Harness exists, and config is the wrong seat regardless:
54
61
  `jr2 up` builds and resolves every image it deploys ([ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md)), and
@@ -68,13 +75,14 @@ pointers only; memory snapshots are tied to one template version, so the state w
68
75
  change is a volume the actor reloads on a cold boot. These decisions take that shape.
69
76
 
70
77
  - **A conversation is persisted to a directory, and rebuilt from it on boot.** The Harness writes each conversation as
71
- it goes: jr2's own record — the stream log, the Settlements, the history messages, in the wire's shapes — plus the
72
- engine part, in a folder the engine names (`engine/<name>`), which the record does not read. Today it is jr2's
73
- transcript (ADR-0027): `engine/pi/transcript.jsonl`, the pi-ai messages the model read and each Compaction. Rebuilding
74
- reads jr2's record first; an engine that cannot read the engine part (another engine, another version) continues from
75
- the history messages. The record is the contract; the engine part is not. A Submission in flight at the restart
76
- settles `failed` (the typed error `harness_restarted`, which the Agent's Actor reads as "Turn lost") on the rebuilt
77
- stream, so the Orchestrator reads a Settlement, not a 404, and the conversation's next Turn continues it.
78
+ it goes: jr2's own record — the stream log, the Settlements, the history messages, the conversation's MCP tools, in
79
+ the wire's shapes — plus the engine part, in a folder the engine names (`engine/<name>`), which the record does not
80
+ read. Today it is jr2's transcript (ADR-0027): `engine/pi/transcript.jsonl`, the pi-ai messages the model read and
81
+ each Compaction. Rebuilding reads jr2's record first; an engine that cannot read the engine part (another engine,
82
+ another version) continues from the history messages. The record is the contract; the engine part is not. A Submission
83
+ in flight at the restart settles `failed` (the typed error `harness_restarted`, which the Agent's Actor reads as "Turn
84
+ lost") on the rebuilt stream, so the Orchestrator reads a Settlement, not a 404, and the conversation's next Turn
85
+ continues it.
78
86
  - **A restart while the Agent's state is busy cuts the Turn and is no fault.** When the Agent's state is not at Rest —
79
87
  its pick is outstanding, or work it started in runs below it — or its pick's answer was read and never reached the
80
88
  model, the Agent's Actor raises no `agent.fault` for a restart: the Machine would route it and stop the work the Turn
@@ -0,0 +1,101 @@
1
+ # The Console unlocks with a Caller's credential, and shows what its scopes allow
2
+
3
+ [ADR-0014](0014-observation-is-open-run-state-is-not.md) built the HTTP surface on one premise: the visualizer page is a
4
+ browser, and a browser holds no token — so it got a projection instead of a credential, and "give the page a token" was
5
+ explicitly rejected. That page is now the **Console**: it starts runs, answers Gates and cancels runs, which are writes
6
+ on the guarded surface. This ADR records how a browser earns those writes without moving the open band. The person
7
+ presents a credential; the page never ships with one.
8
+
9
+ ## Decision
10
+
11
+ - **The person presents a Caller's credential; the page never ships with one.** The Console's nav carries a password
12
+ field, and a login button for each JWT issuer that the Instance declares with `login: true`
13
+ ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)). Empty, the page is
14
+ precisely ADR-0014's observer: open structure and observation, no control rendered. With a credential, the page sends
15
+ `Authorization: Bearer` on the guarded surface. The credential is any Caller's: the Instance token, a named Caller
16
+ token, or a JWT. The login gets a JWT from the issuer with OIDC (the authorization code flow with PKCE, as a public
17
+ client), and the page then uses it as it uses a typed token. The page as _served_ stays open and inert; ADR-0014's
18
+ rejection stands, because what it rejected was baking the credential into the page.
19
+ - **The login's facts come from an open route, and its client id is the issuer's `audience`.** Before a login, the page
20
+ reads `GET /console/config`: `{ login: [{ issuer, clientId }] }`, from the issuers of the current Assembly that have
21
+ `login: true`. The route is in the open band (ADR-0014), because the page reads it before it holds a credential, and
22
+ both values are public: a public client has no secret, and the issuer's URL is in every JWT that it signs. The client
23
+ id is the issuer entry's `audience`, because the JWT that the login gives names its client as its audience, and that
24
+ is what the JWT authenticator checks. A login is opt-in for each issuer, because an issuer for machines (a CI
25
+ provider's tokens) has no login page. The page reads the issuer's discovery document, asks for `openid`, `profile`,
26
+ `email` and `groups` where the issuer lists each, and sends the ID token as the bearer: an access token's audience and
27
+ form differ between providers. The return address is always the Console's root (`/`), so the client registers one
28
+ redirect URI; the page then puts the address back to where the login began. The verifier, the state and that address
29
+ wait in `sessionStorage` until the return, and a return whose state does not match is refused. PKCE needs
30
+ `crypto.subtle`, so a login needs the Console on https or on localhost, and the issuer must let the Console's origin
31
+ call its discovery document and its token endpoint (CORS).
32
+ - **The page shows only what the Caller's scopes allow.** On entry the page asks `GET /whoami` for the Caller record:
33
+ its name, the name that jr2 shows, and its scopes for each Workflow. It renders a control only where a scope opens its
34
+ route: the start form with `start` for that Workflow, Gate cards with `answer`, cancel with `cancel`, a run's full
35
+ state with `read`, and the Steering box with `steer`. The page re-fetches a run on a frame only for a Caller with
36
+ `answer` or `read` on its Workflow. The nav shows who the page acts as: the name that jr2 shows, else `<via>/<name>`,
37
+ with the scopes in its title, and a sign-out button in place of the field. The server still checks each request
38
+ (ADR-0014); the page hides a control so that a person does not meet a 403, not as a guard.
39
+ - **The credential lives in `sessionStorage`** — survives a reload, dies with the tab, never a cookie (no ambient
40
+ credential, no CSRF surface). A typed token and a login's JWT live there alike. A later 401 (an expired JWT, a rotated
41
+ token) drops the UI back to observer mode rather than failing per widget.
42
+ - **Gate discovery is a frame-triggered guarded read, not an open-band extension.** A Gate exists exactly while its
43
+ invoking state is entered ([ADR-0011](0011-workflow-defined-events.md)), and every entry/exit lands a status frame on
44
+ the open workflow feed (ADR-0022) — so a Console whose Caller has `answer` reads `GET /gates` on entry and again on
45
+ each frame, and folds the Gates into its store. `GET /gates` lists only the Gates that the Caller may answer, across
46
+ every Workflow (ADR-0078), so the Gate cards are the person's inbox and never show a Gate the person cannot answer.
47
+ Synchronized, no polling loop, and the observation projection is untouched. A delivery's success is likewise confirmed
48
+ by the next frame emptying the card, never by local bookkeeping.
49
+ - **The rail lists ended runs too.** It lists the live runs and, under them, the ended runs of the selected Workflow
50
+ from `GET /runs?status=ended&workflow=<name>` (ADR-0009), each with its status, and a `drifted` run with its reason
51
+ (ADR-0030). An ended run opens as a live one does: its graph, its last status and its conversations, for as long as
52
+ jr2 keeps the run (ADR-0065). A run on an older Assembly carries the mark "older Assembly · <hash>" and is drawn on
53
+ its own Machine, live or ended: an ended run's row names the Machine it ran on
54
+ ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)). A `drifted` run has no
55
+ Machine to draw, so it shows its drift reason in place of the graph. The Console shows each outstanding pick as
56
+ "outstanding since".
57
+ - **A conversation opens in a bottom drawer.** A full-width drawer across the bottom of the page shows the run's
58
+ conversations ([ADR-0023](0023-the-harness-prints-the-conversation.md)). A person can change its height, and the page
59
+ keeps the height for that viewer. Closed, it is a bar of tabs, one for each Agent slot, with a dot on a slot whose
60
+ Turn is live. Open, it shows the selected slot's conversation and follows a live Turn, with the Steering box beside it
61
+ for a Caller with `steer`. It reads the conversation routes, so it needs `read`. The right drawer keeps Attention and
62
+ Activity.
63
+ - **Page addresses content-negotiate; JSON is the default dialect.** `/` and `/workflows/:name` serve the Console shell
64
+ to `Accept: text/html` and JSON to everything else — so the workflow-detail resource keeps its natural address (the
65
+ run-input schema's home, [ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)) and no future page has to
66
+ squat on a mangled sibling path. Nothing existing moves: EventSource asks `text/event-stream`, and the Harness's Menu
67
+ and the CLI never ask for HTML. `/viz/*` is deleted (greenfield, no redirects); assets move to `/assets/*`, which also
68
+ retires the "a workflow named `assets` is shadowed" hazard.
69
+
70
+ ## Considered options
71
+
72
+ - **`jr2 ui` — the CLI opens the browser through an authenticated local proxy.** A convenience layer worth building _on
73
+ top of_ the token field later, not a substitute: it has no answer for the deployed jr2 Application behind an ingress,
74
+ where the browser talks to the Service directly and the CLI is not in the path.
75
+ - **The Instance token alone.** Rejected (ADR-0078): a person who must answer Gates and holds no cluster RBAC would then
76
+ hold every act on every run. A scoped Caller's credential is what that person holds.
77
+ - **The conversation in the right drawer.** Rejected: a conversation needs the page's width, and the right drawer keeps
78
+ Attention and Activity.
79
+ - **Announce gates on the open observation feed** (even just `{runId, gate}`). Rejected: it widens the projection for a
80
+ convenience the credential already solves, and the open band has an unresolved exposure question as it stands (spawn
81
+ ids — the ADR-0014 open call). The projection stays the one place the line is drawn.
82
+ - **A `/ui` path prefix, or moving the API under `/api`.** Rejected for negotiation: the first is ceremony on the
83
+ address a human types, the second churns the Harness's Menu, the Custodian's route allowlist, the CLI, and the
84
+ ADR-0009/0013 route tables for cosmetics.
85
+
86
+ ## Consequences
87
+
88
+ - **No new band.** The server-side diff of the Console itself is: negotiate two addresses, serve the shell, retire
89
+ `/viz/*`, and serve `GET /console/config` in the open band. The scopes, `GET /whoami` and the login's issuers are
90
+ ADR-0078's.
91
+ - **Possession follows the credential.** The Instance token stays one `kubectl get secret` away for whoever holds
92
+ cluster RBAC; a named token is what its holder was given; a login is what the identity provider's groups grant.
93
+ - **The XSS caveat is the real one and is bounded by what the page is:** a credential in `sessionStorage` is readable by
94
+ scripts on the page, so the Console must keep serving only its own assets — no CDN, no third-party script, exactly the
95
+ constraint the viz page already lives under.
96
+ - **GLOSSARY.md**: **Console** is a glossary term (visualizer, dashboard, UI, viz on its Avoid list).
97
+ - **The Console's UX gets a browser tier** — `@console`-tagged Cucumber scenarios holding a Playwright page, in
98
+ `@jr2/e2e`, excluded from the default profile ([ADR-0010](0010-bdd-acceptance-tests.md) as amended). What the page
99
+ _believes_ stays unit-tested in `console-store.test.ts`, and the login's pure half in `console-login.test.ts`; the
100
+ browser tier asserts what a user does and sees, and the login against Dex in a container (ADR-0078). The login
101
+ scenario is also tagged `@dex`, because it needs docker, which the rest of the tier does not.
@@ -3,7 +3,7 @@
3
3
  `POST /workflows/:name/runs` takes a JSON body and hands it to the machine as xstate `input` — unvalidated, and
4
4
  undescribed: the module contract is `export const machine` alone (ADR-0011/0015), so nothing at runtime says what a run
5
5
  of a given workflow should be started _with_. The Console's start form
6
- ([ADR-0032](0032-the-console-unlocks-with-the-instance-token.md)) forced the question, but the gap predates it —
6
+ ([ADR-0032](0032-the-console-unlocks-with-a-callers-credential.md)) forced the question, but the gap predates it —
7
7
  `jr2 run` has the same blindness, and a typo'd input surfaces as a confusing mid-run failure instead of a refusal at the
8
8
  door. Meanwhile every event a machine _accepts_ already carries a zod schema, attached to the machine itself
9
9
  ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)'s vocabulary-on-the-machine). The input that starts a
@@ -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,27 +75,27 @@ 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
97
- start whose answer was lost is sent once where it may have arrived (ADR-0067) and makes one run. `jr2 run` and the
98
- Console send none.
96
+ start whose answer was lost is sent once where it may have arrived (ADR-0067) and makes one run. With a second header,
97
+ a start is a key-only ask: the door answers the run under the key, or that no run has it, and starts nothing.
98
+ `jr2 run` and the Console send none.
99
99
 
100
100
  ## Considered options
101
101
 
@@ -1,11 +1,11 @@
1
1
  # The Console renders with Preact, and the server erases the types
2
2
 
3
- The Console ([ADR-0032](0032-the-console-unlocks-with-the-instance-token.md)) outgrew its framework-free form: `main.js`
4
- reached ~1,500 lines, much of it hand-rolled DOM reconciliation (`memo()`, `paint()`, manual list diffing) — work a view
5
- library exists to own. Three standing constraints shaped the replacement: the repo is zero-build (Node 24 strips types;
6
- nothing compiles), the Console serves only its own assets (ADR-0032), and `@jr2/orchestrator` is a library that serves
7
- its files from disk wherever the process starts. A fourth arrived with this decision: Console source is TypeScript like
8
- everything else.
3
+ The Console ([ADR-0032](0032-the-console-unlocks-with-a-callers-credential.md)) outgrew its framework-free form:
4
+ `main.js` reached ~1,500 lines, much of it hand-rolled DOM reconciliation (`memo()`, `paint()`, manual list diffing) —
5
+ work a view library exists to own. Three standing constraints shaped the replacement: the repo is zero-build (Node 24
6
+ strips types; nothing compiles), the Console serves only its own assets (ADR-0032), and `@jr2/orchestrator` is a library
7
+ that serves its files from disk wherever the process starts. A fourth arrived with this decision: Console source is
8
+ TypeScript like everything else.
9
9
 
10
10
  ## Decision
11
11
 
@@ -81,10 +81,11 @@ every build) and promotes incidental image properties into contract items.
81
81
  because jr2's file supplies one; such an image sets `GIT_CONFIG_SYSTEM=/etc/gitconfig` itself, and jr2 then adds
82
82
  nothing. With the variable set, `git config --system <key> <value>` writes jr2's file on the read-only volume and
83
83
  fails (exit 4); without it, an Agent that runs as root wrote `/etc/gitconfig`. A `--global` or repo-local write works
84
- as before. A human's `kubectl exec` shell does not get the variable, as it does not get the PATH append. jr2's other
85
- git is out of scope: the sync runs under `GIT_CONFIG_NOSYSTEM` (ADR-0067); the Sandbox Effector is another container
86
- with its own committer rules (ADR-0068), and a credentialed `command` Effect runs there with `PATH`, `HOME` and
87
- `TMPDIR` alone, so it gets no default identity.
84
+ as before. A raw `kubectl exec` shell does not get the variable, as it does not get the PATH append;
85
+ `jr2 workspace shell` starts through the spawner, as an Agent's `bash` does (ADR-0009). jr2's other git is out of
86
+ scope: the sync runs under `GIT_CONFIG_NOSYSTEM` (ADR-0067); the Sandbox Effector is another container with its own
87
+ committer rules (ADR-0068), and a credentialed `command` Effect runs there with `PATH`, `HOME` and `TMPDIR` alone, so
88
+ it gets no default identity.
88
89
 
89
90
  - **The contract is the floor, and the preflight proves it.** What is inherent to "jr2's runtime in your filesystem" and
90
91
  nothing more: a **glibc** base no older than the one jr2's node was built against (musl is out entirely), **`git`** on
@@ -101,14 +102,14 @@ every build) and promotes incidental image properties into contract items.
101
102
  its first provision, in the `preflight` init container's log, with an error that names the fix (because "node did not
102
103
  execute" is not actionable) — never as a mid-turn tool failure, and never as a converge refusal for a contract the
103
104
  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.
105
+ - **`workspace()` names the image as a STATIC OPTION, never from run input** — what the Sandbox is _made of_ is the
106
+ 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
107
+ it by walking the registered Machines ([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)), and no
108
+ walk can evaluate a function of run input. The User Container rides the same rule and the same resolution as one more
109
+ option beside `image` (`user?: string`, [ADR-0005](0005-sandbox-pod-composition.md)). Neither is ever persisted: the
110
+ option rides the Machine (a parts map keyed on `machine.config`, as the vocabulary is) and the provisioning state
111
+ re-reads it at invoke time, so a restore provisions what the Machine carries now — and a resolved content-addressed
112
+ tag, which would outlive the image it names, never leaves the port's side of the seam at all.
112
113
  - **A Menu-only Agent never gets a Sandbox Image.** `workspace: "none"` withholds the entire Working toolset (ADR-0028),
113
114
  so there is no tooling to carry; the Instance Harness (ADR-0031) runs the stock image permanently. The feature has
114
115
  exactly one seat, and it is the Workspace.