@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
@@ -58,7 +58,7 @@ lifecycle stay implemented once, in the registration table.
58
58
 
59
59
  ## External side: the same primitive over HTTP (`gate`), and each gate is a resource
60
60
 
61
- `gate` is the symmetric actor for **every non-agent caller** — humans (`jr2 send`, a UI), webhook translators, CI, other
61
+ `gate` is the symmetric actor for **every non-agent caller** — humans (`jr2 send`, the Console), a Hook, CI, other
62
62
  systems. "Human" is policy, not mechanism, so the actor is not named for one caller. Each invocation is an addressable
63
63
  **gate**: input is `{ gate?, meta? }` — `gate` an optional authored id (the id is derived when absent — below), `meta`
64
64
  serializable caller/integration context (PR URL, title); its accepted set derives from the gated state's transitions,
@@ -72,11 +72,29 @@ scopes event resolution), so `gate: "F-12"` in two concurrent runs cannot collid
72
72
  Gate-as-resource is forced by concurrency: several children park simultaneously (three features in `humanReview` is the
73
73
  _normal_ case), all accepting `approve` — a run-level POST is ambiguous. And every real external surface already thinks
74
74
  in gates: a CLI listing shows pending decisions and acts on one; a UI inbox renders one card per gate (schemas drive the
75
- forms, `meta` drives the copy); a forge webhook translator finds its gate by matching `meta` (e.g. `prUrl`), which is
76
- why `meta` exists. Generalizing beyond humans also closes a hole hard-coded event types left: a push seam like "a
77
- work-source webhook wakes discovery" is just an idle state holding a gate accepting `work_ready` (the `wake` seam of
78
- ADR-0017). Two obvious later additions, both deferred: a cross-run inbox (`GET /gates`), and CLI segment-matching over
79
- derived gate ids (`--gate F-12` resolving an unambiguous segment, git-style like run-id prefixes).
75
+ forms, `meta` drives the copy); a hooked Gate's own handler matches a forge's delivery against its `meta` (e.g. `pr`,
76
+ below), which is why `meta` exists. Generalizing beyond humans also closes a hole hard-coded event types left: a push
77
+ seam like "a Hook brings an item" is a Pool's `source` Gate ([ADR-0017](0017-pool-over-a-source-port.md)). The cross-run
78
+ inbox is `GET /gates`: the open Gates of every live run that the Caller may answer, oldest first, with a `workflow`
79
+ filter, each entry `{ runId, workflow, gate, path, accepts, acceptsNow, meta, openedAt, hook? }` (`hook` names the Hook
80
+ kinds that the Gate hears, when it hears one) and never the run's `context`
81
+ ([ADR-0014](0014-observation-is-open-run-state-is-not.md)). A Gate that takes no event from a Caller, as a Pool's
82
+ `source` Gate that hears only Hooks, is not in it: no person can answer it. `jr2 gates [--workflow]` prints it, and the
83
+ Console's Gate cards read it. A stream of it is a later addition. CLI segment-matching over derived gate ids
84
+ (`--gate F-12` resolving an unambiguous segment, git-style like run-id prefixes) is deferred.
85
+
86
+ **The `answer` scope can name single Gates**, in the form of
87
+ [ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md). It names a Gate by its invoke
88
+ id, the one static name a Gate has in the Machine that invokes it, and the id that its events carry as their sender
89
+ (below): the invoke `id` that the author wrote, or else the key path of its gated state below the root of that Machine
90
+ (`body.humanReview`), with `.0`, `.1` after it when that state invokes more than one unnamed Gate. So `body.humanReview`
91
+ names that Gate in every child of a Pool, and two gated states with one key at two paths of one Machine have two names.
92
+ The name is not unique across the Workflow: a Workflow composes Machines by invoke
93
+ ([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)), and two composed Machines that each gate a
94
+ state `approve` give that name to two Gates. One per-Gate name covers every Gate of the Workflow that has that invoke
95
+ id, and `jr2 up` refuses a per-Gate name that matches Gates in more than one Machine of the Workflow, so a scope never
96
+ opens a Gate that its author did not see; the author writes an invoke `id` on one of them to tell them apart. It is not
97
+ the caller-facing gate id, which is different in each child and which a workflow may compute.
80
98
 
81
99
  **The gate id is derived; authoring one is the exception.** Absent an authored `gate`, the id is the gate actor's own
82
100
  path below the run root, and jr2Setup's menu-derivation walk names an unnamed gate invoke with its state key path so the
@@ -102,15 +120,202 @@ vocabulary — workflows speak only `defineEvent` / `agentRun` / `gate`.
102
120
 
103
121
  **Every delivered event carries its sender.** The table writes `sender: { party: "agent" | "gate", id }` on each event
104
122
  it delivers, after the schema parse and from the registration alone: `id` is the Agent's slot, or the Gate's invoke id
105
- (static in the Machine, the same after a restore — not the caller-facing gate id, which a workflow may compute). It is
106
- plain data on the event, so it rides the mailbox, a delayed event the run host persists beside the snapshot, and a
107
- restore; a restored registration writes it again from the persisted input and the invoke id. The Machine's own events
108
- (`raise`, `sendTo` itself) carry none. `defineEvent` refuses an input that declares `sender`, and both surfaces refuse a
109
- body that carries it (ADR-0013): no caller and no def can write it. What an author does with it is ADR-0015's.
123
+ (static in the Machine, the same after a restore — not the caller-facing gate id, which a workflow may compute). A
124
+ Gate's event also carries the **Caller** that answered it
125
+ ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)): `via`, `name` and
126
+ `onBehalfOf`, with `checked` naming the fields that jr2 verified. `via` and `name` are always checked, because an
127
+ authenticator made them; `onBehalfOf` is checked only for a Hook, whose signature covers the payload it comes from. The
128
+ run records the same Caller as the answer's `answeredBy`. An event that a Gate's `look` gives (below) has the Gate as
129
+ its Sender and no Caller. The Sender is plain data on the event, so it rides the mailbox, a delayed event the run host
130
+ persists beside the snapshot, and a restore; a restored registration writes it again from the persisted input and the
131
+ invoke id. The Machine's own events (`raise`, `sendTo` itself) carry none. `defineEvent` refuses an input that declares
132
+ `sender`, and both surfaces refuse a body that carries it (ADR-0013): no caller and no def can write it. What an author
133
+ does with it is ADR-0015's.
110
134
 
111
135
  `CANCEL` stays reserved as the run-level infra interrupt. jr's "exit 3 and let the human drive the bash script" becomes
112
136
  "park in a durable state that advertises its human surface."
113
137
 
138
+ ## A Gate hears a Hook, and looks before it waits
139
+
140
+ A Hook ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)) is a door for one
141
+ outside service. Its delivery is not an event of any Machine: a Gate makes one of it, or ignores it. So a hooked Gate is
142
+ an extension of the Gate, not a second concept. It has the same derived id, accepted set, Sender, Console card and
143
+ `jr2 send` path as any Gate. The Hook checks the delivery and decides nothing; only a Gate's handler or its look makes
144
+ an event for a run.
145
+
146
+ - **A Gate slot declares what it hears.** `actors: { review: gate({ meta, delivers, hooks, look, reply }) }`, and the
147
+ state invokes it by its key:
148
+ `invoke: { src: "review", input: ({ context }) => ({ meta: { pr: context.pr, sha: context.sha } }) }`. A plain Gate
149
+ stays `src: "gate"`. `meta` is a zod schema of the meta that the invoking state sets. The Gate checks the meta when it
150
+ opens, and the handlers and the look read it typed. It is a schema and not a type argument, because TypeScript does
151
+ not infer `delivers` when an author names one type argument.
152
+ - **`delivers` lists the events that the handlers and the look can give.** The load walk refuses one that is not in the
153
+ Gate's derived accepted set. The Chart door refuses one that is not in the Chart's Vocabulary
154
+ ([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)). An event in `delivers` is `audience: "external"` unless
155
+ its def sets an audience, so an Agent below the Gate never has it on its Menu, and the author does not have to know
156
+ this ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)). This default can change what a Menu shows, so the
157
+ change that builds it runs the `@model` tier
158
+ ([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)).
159
+ - **A handler for each Hook kind and event key.** `hooks: { github: { "pull_request_review.submitted": handler } }`. The
160
+ key is the kind's id and the event key that the kind reads from the delivery (ADR-0078), never a Hook's name: two
161
+ Hooks of one kind (two organizations, two secrets) reach the same handlers. The table is typed at compile time: each
162
+ kind module that the program imports adds its id and its payload types to `interface HookKinds` by declaration
163
+ merging, and a key that no kind in scope gives does not compile. There is no registry at run time: the door matches a
164
+ delivery by the kind's id and the event key, exactly. A kind's schema key can end in `.*` (`"block_actions.*"`) to
165
+ type each key with that prefix, but a handler names the full key (`"block_actions.approve"`): a handler key that holds
166
+ `*` does not compile, and `gate()` and `source()` refuse it, so the load walk refuses its Workflow. No handler hears a
167
+ handshake. A handler reads `{ payload, meta, caller }`, where `payload` is the decoded body after the kind's schema
168
+ parsed it (below). Its type is the schema's output, so a handler reads only the fields that the schema names. It is
169
+ pure and synchronous, as a guard is: no I/O, no Promise, no clock. It matches the payload against the Gate's `meta`
170
+ (for a review, the PR's url and the pushed commit) and gives one plain event of `delivers`, such as
171
+ `{ type: "approve", by }`, typed by that list, or `undefined` to ignore the delivery. The Orchestrator parses the
172
+ event with its def's schema and asks the run whether it takes it from this Gate now, as it does for `jr2 send`
173
+ (ADR-0013). There are no correlation keys: each delivery calls every open handler of its kind and event key.
174
+ - **A look reads the fact that the Gate waits for.** `look: { read, input, every?, until }`. `read` is an `http` Effect
175
+ marked `idempotent: true`, or a promise actor, which a Machine can invoke anyway; `gate()` refuses another Effect
176
+ ([ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)). The promise is for
177
+ the default test tier, which has no Instance Effector, and for a fact inside the Instance. A read of a service outside
178
+ the Instance is an `http` read, because the Orchestrator makes no call out of the Instance for a run. The read is a
179
+ child of the Gate in the run's actor system, so an `http` read goes through the run's Instance Effector and its
180
+ ledger, and `jr2 up` places it as it places an Effect slot. The look runs once after the Gate registers: a change
181
+ before the read is in its answer, and a change after the Gate registered reaches the Gate as a delivery. It runs again
182
+ after each restore, and every `every` (10 minutes by default) while the Gate is open. `until` gives an event of
183
+ `delivers`, or `undefined` for "not yet". So a delivery that came before the Gate opened, a delivery lost while the
184
+ Orchestrator was down, and a restart all come to the same read, and the author writes no idiom for them. The run takes
185
+ a looked event only when it takes it from this Gate now, as for `jr2 send`. A read that fails, an `until` that throws,
186
+ and an event that the run does not take now are each a line in the Orchestrator's log, and the look reads again at the
187
+ next `every`: a look never faults the run. A looked event has the Gate as its Sender and no `answeredBy`: the look is
188
+ the Machine's own read, not a Caller. The per-run feed marks it `looked`, behind the token as a Turn marker is, before
189
+ the delivery, because a delivery can end the run in its own send. The look stops after the run takes its event, until
190
+ the Gate opens again, because a look reads a level: a Gate that stays open after its event would get the same event
191
+ every `every`. So a state that goes back to its own Gate must change the `meta` (a new `sha`), or it loops. A restore
192
+ opens the Gate again, so a Gate that stayed open after its event looks one more time after a restore.
193
+ - **`look: "none"` for a Gate that hears a command.** A `/retry` comment is an edge, not a level, so there is no fact to
194
+ read. A lost delivery of a command stays lost.
195
+ - **`jr2 up` warns about a hooked Gate with no look and no `after` on its state**, and does not refuse: a delivery lost
196
+ in an outage then stops the run until a person answers the Gate. The fix is one line, a look or `look: "none"`.
197
+ - **A bad delivery marks the feed and never faults the run.** A handler that throws, an event that is not in `delivers`,
198
+ and an event that the run does not take now each put a `hook-refused` mark on the run's feed and a line in the
199
+ answer's `refused`. The run goes on. A refusal that the author wants the person to read is a throw in the handler,
200
+ with the reason as its message.
201
+ - **`reply` gives the text that the person who acted reads**, for a kind whose answer a person reads (`slack`,
202
+ `discord`; not `github`, and not a Slack event). `reply(outcome, { meta, caller })` is pure and synchronous, as a
203
+ handler is. `outcome` is that Gate's own: `delivered` with the event, or `refused` with the reason. It gives plain
204
+ text or `undefined`, never a service's JSON, and the kind escapes the text and puts it in its envelope (below). It
205
+ runs after the run answers, because only then is `refused` known, so the text is not in the handler's return. A Gate
206
+ whose handler gave `undefined` was not a target, and its `reply` does not run: a click on one message does not make
207
+ the other open Gates of that key speak. A throw gives no text, and the hook log keeps the error. Only the person who
208
+ acted sees the text. A message that everyone sees is a change of shared state, so it is an Effect of the Machine. So
209
+ is the reply after the work: a plain `http` Effect with a bot token, such as Slack's `chat.postMessage`, never the
210
+ delivery's `response_url`, which takes 5 posts in 30 minutes.
211
+ - **A packaged Machine writes its handlers and leaves its look Open.** A forge's payload is public, so a package writes
212
+ the handlers for each forge that it supports, and handlers are never Open. It imports the kinds that it hears, so
213
+ their payload types are in scope where it is compiled. A look needs a url and a token, so a package leaves
214
+ `look: open` ([ADR-0054](0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md)). The composer
215
+ binds it with `customize(m, { gates: { review: { look, hooks } } })`, which also adds or replaces a handler by kind
216
+ and event key; `null` removes one. An Open look that nobody binds gets the warning above, not a refusal, so a person
217
+ can still answer the Gate with `jr2 send`. A forge part gives the handlers and the look for one fact (a review
218
+ verdict, CI on a commit). It takes event defs and secrets, never a Hook's name, and each verdict compares the commit,
219
+ not only the PR. It types its handlers by the kind's schemas, so a field that it reads is a field that the kind
220
+ parses.
221
+ - **A parked run keeps the handlers, the look and the `reply` of the Assembly that it holds**
222
+ ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)): they are code in the
223
+ Assembly, and the Gate's input stays data, so a restore registers the Gate again as before.
224
+
225
+ ## The Hook's door
226
+
227
+ - **`POST /hooks/:name`.** Each Hook that `jr2.config.ts` declares has one stable address, which a service is set up
228
+ with once. The Hook's verifier authenticates the route, outside the bands of ADR-0014 (ADR-0078). A delivery that
229
+ fails it, or whose signed timestamp is more than 300 seconds from the door's clock, gets 401, and nothing else
230
+ happens. The window is jr2's, the same for every kind. A name that the current config does not declare gets 404. With
231
+ no current Assembly the door answers 503 before the verifier, because no config says which Hooks exist. A body above 2
232
+ MiB gets 413, after the name and before the verifier. The door reads no more of the body than the limit, because this
233
+ is the one route with no bearer token that takes a body, and a `mac` check sends the body to the Instance Effector as
234
+ base64, which its wire limits to 4 MiB. Without the limit, a large delivery that is correctly signed would get a
235
+ false 503. The hook log keeps a `too-large` line, which claims no delivery id. A `mac` check that the Instance
236
+ Effector cannot answer (none is deployed, or no replica answers) is 503 too, so the service can send again: the
237
+ Orchestrator holds no secret to check with
238
+ ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)).
239
+ - **The kind's handshake comes after the verifier.** A Slack `url_verification` gets `{ "challenge": … }`, and a Discord
240
+ `PING` gets `{ "type": 1 }`. A handshake reaches no handler, and the hook log keeps it as `handshake`. A request that
241
+ the kind cannot read (its `read` throws, or gives no id or no event key) gets 400, and the hook log keeps the error as
242
+ `unreadable`.
243
+ - **The door parses the payload before the fan-out.** A Hook kind gives a zod schema for each event key, and the door
244
+ parses the decoded body with the schema of the delivery's key. Each object in a schema is a loose object, and a schema
245
+ names only the fields that handlers need. So a field that the service adds breaks nothing, and a named field that is
246
+ missing or of the wrong type fails the parse. A failed parse reaches no handler. The hook log keeps a
247
+ `payload-refused` line with zod's issues, and the answer names the kind and the event key in
248
+ `payloadRefused: { kind, key, issues }`. The status is 200, because a second try fails the same way. A key that the
249
+ kind gives no schema for is not parsed, and it reaches nothing, because no handler can hear it.
250
+ - **A repeat is found by the delivery id AND the body's digest.** A delivery whose id or digest the hook log holds gets
251
+ 200 with `seen: true`, and nothing is delivered. The check is for each Hook, because two Hooks can get one body (an
252
+ organization's webhook and a repository's). Only a delivery that the door claimed counts: a handshake, a
253
+ `payload-refused` line, an `unreadable` line, a `hold-full` line (a 429, below) and a `no-live-run` line (a 503,
254
+ below) never make a repeat, so a refused payload that comes again is parsed again, and a delivery that every Source
255
+ was too full to hold is heard again. The check and the claim are one synchronous step, so of two copies that come at
256
+ once, one is delivered. A service may or may not send a delivery again; the design depends on neither, because the
257
+ look finds what a lost delivery said.
258
+ - **The fan-in reaches open Gates only.** A delivery goes to every open Gate, in every live run of a Workflow that the
259
+ Hook may answer, whose handlers hear its kind and event key, and to nothing else. A Pool's `source` Gate is one of
260
+ them ([ADR-0017](0017-pool-over-a-source-port.md)). So a Hook answers a Gate of a run, or brings items to a Source. It
261
+ never starts a run, and it never names one.
262
+ - **The answer is the outcome.** 200 with
263
+ `{ delivery, seen, delivered: [{ run, workflow, gate, event }], refused: [{ run, gate, event?, reason }], ignored, sources, payloadRefused? }`:
264
+ `ignored` counts the open Gates whose handler gave nothing, and `sources` holds each Source's verdict (ADR-0017). A
265
+ delivery that reached nothing gets 200 with `delivered: []`. After the verifier, the fan-out always runs first. Then
266
+ the answer is 429 when every Source's verdict is `hold-full`, and 503 only in two cases: a standing Pool that hears
267
+ this kind and event key has no live run and is not paused, or the delivery followed a standing Pool's hand-overs to
268
+ the new run's Source 3 times and the Pool handed its run over again (ADR-0017). In the second case the Pool can have a
269
+ live run: the delivery stops following it, so that it cannot follow without end. Neither 503 claims the delivery's id,
270
+ so the service's send again is heard. The 503s that come before the verifier are in the door's rule above. So a Pool
271
+ in back-off does not stop the review Gates of other runs. This is the answer of a kind whose answer no person reads: a
272
+ forge, or a Slack event, which keeps its 429 and 503 so that Slack sends it again.
273
+ - **A person reads the answer to a chat command or click.** For a Slack command or click and a Discord interaction, the
274
+ kind's envelope takes the place of the outcome, and the hook log keeps the outcome. The text joins the `reply` of
275
+ every target: Sources first (what the act brought), then Gates (what the act answered), and in each by Workflow name,
276
+ then by run id. Empty texts and exact repeats are dropped. A text from a `skipped` verdict shows only when no other
277
+ target gave a text, because a skip says "not for me". Every target speaks, and not one winner, because the outcome is
278
+ already a list and the load walk cannot see which Gates will be open. When no target gives a text, the door writes one
279
+ from the outcome: a repeat, a delivery that reached nothing, a payload that the parse refused, or what the fan-in did
280
+ (a Gate that took an event, a Pool that is paused). For a 429 or a 503 of the fan-in, the door adds a line with the
281
+ cause after the targets' texts, so the text always says why. This text is the same for every service, so the kind
282
+ never sees the outcome. A paused Pool has no run, so no `reply` of it runs: the door's text says that it is paused.
283
+ The 503s and the 404 that come before the verifier stay as they are, because the door cannot know who reads the answer
284
+ before the kind reads a verified request. The door cuts the text to the limit that the kind gives (Discord: 2000
285
+ characters), and the kind puts it in its envelope. The envelope shows the text to the person who acted, and to nobody
286
+ else: Slack `{ "response_type": "ephemeral", "text": … }`, Discord
287
+ `{ "type": 4, "data": { "content": …, "flags": 64 } }`. The text is plain: no Block Kit blocks, no Discord embeds and
288
+ no `in_channel` in v1. A rich reply adds later without a break.
289
+ - **Such an answer is 200.** Where the rules above give 429 or 503, a delivery whose answer a person reads gets 200, and
290
+ its text says why. A non-200 shows the person the service's own error, which says nothing about what jr2 did. The hook
291
+ log keeps the status that the rules give. A failed verifier stays 401.
292
+ - **The reply to a Slack click goes to its `response_url`.** Slack shows no text from the answer to a Block Kit click:
293
+ that answer can only be an empty 200. So the `slack` kind's envelope for a click is an empty 200 and a follow-up, as
294
+ data: a post of the text to the click's `response_url`. The door answers the empty 200, and then makes the post
295
+ through the Instance Effector's `http` call
296
+ ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)): to a host that the kind declares
297
+ (`hooks.slack.com`), with no secret (the url is in the verified payload), one try. The door refuses a url that does
298
+ not start with one of the kind's `posts`, and posts nothing. A failed or refused post is a `follow-up` line in the
299
+ hook log. A `reply` that threw is in its delivery's line, as `replyErrors`. It is the answer by another road, with no
300
+ slot, no ledger row and no run. No Machine can post it, because a click that reaches nothing has no run.
301
+ - **No deferred answer.** The text is a verdict or an outcome, which is ready when the answer leaves. The work never is.
302
+ Discord's deferred answer (type 5) would make the run's result the answer, but its token lives 15 minutes, a run is
303
+ often longer, and a deferred answer that nobody edits stays "thinking". When the door misses the service's 3 seconds,
304
+ the person sees the service's error, the hook log keeps what jr2 did, and a held item still runs.
305
+ - **The hook log keeps each delivery for `runs.retain`**
306
+ ([ADR-0065](0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md)): its Hook, id, digest, outcome and
307
+ the status that the rules give, a `payload-refused` delivery with zod's issues, a Source's skip with its reason, a
308
+ `reply` that threw, and a failed follow-up post. `jr2 hooks <name>` prints it, newest first, from `GET /hooks/:name`,
309
+ which takes the Instance token alone ([ADR-0014](0014-observation-is-open-run-state-is-not.md)): a line can hold a
310
+ payload's fields and a handler's words. A run's blob keeps its `(delivery, gate)` pairs as the `delivery` of each
311
+ answer that a Hook gave (the run's last 100 answers, ADR-0078), so a restore never gives one delivery twice to one
312
+ Gate.
313
+ - **Tests.** The default tier drives `POST /hooks/:name` with a `token` and an `ed25519` verifier against hooked Gates
314
+ and a Pool's Source, with the handshakes, the parse, the join of the replies and the envelopes. It has no Instance
315
+ Effector, so the `@kind` tier drives a `mac` verifier through the Instance Effector, and a look whose `http` read runs
316
+ there ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md) lists the facts of
317
+ the services that the build must check).
318
+
114
319
  ## Static imports; live objects constructed per-invocation from input
115
320
 
116
321
  Everything above is reachable by plain `import`. We rejected two alternatives:
@@ -126,6 +331,42 @@ The doctrine that makes static import work, sharpening ADR-0007: **actor logic i
126
331
  per-invocation from serializable input** — the Harness client from an endpoint, the kube client from ambient config.
127
332
  Tests mock by `machine.provide()` at the layer under test.
128
333
 
334
+ ## Considered options for a Hook's delivery
335
+
336
+ - **Correlation keys**: a Gate states keys (`{ pr }`, `{ sha }`), a Hook event carries them, and the door routes by
337
+ them. Not now: the fan-out calls every open handler of the kind and event key, and keys can come later as an index,
338
+ without a break, when a measurement shows the need.
339
+ - **A kept buffer**: the door keeps each Hook event for a time and gives it to a Gate that opens later. Rejected: it
340
+ gives edges again in the order that they arrived, so an old verdict can come after a newer one. The look reads the
341
+ level instead.
342
+ - **A Listener**: an actor at a Machine's root, `listen(kind, { delivers, on, hold })`, that turns each delivery into
343
+ one of its own events and holds it until the Machine takes it. Rejected: it is a second concept beside the Gate, and a
344
+ third party of Sender, outside the position rule of ADR-0015.
345
+ - **A Lookout**: a delivery only wakes an Effect slot, which reads the fact with an idempotent read and settles on it,
346
+ so the payload never decides. Rejected as the only form: it is the soundest, and the hardest to write. Each wait needs
347
+ a read and a schema, and a service with nothing to read needs a second form. Its read is the Gate's `look`.
348
+ - **Payloads typed only**: a kind gives a TypeScript type for each event key, and the door parses nothing. Rejected: the
349
+ type then promises what nothing checks, and a fault in it shows in the handler of every Workflow, as a `hook-refused`
350
+ mark far from its cause, instead of at the kind.
351
+ - **Handlers as a list of `on(kind, handlers)` calls**, which needs no `HookKinds`. Rejected: TypeScript lost the Gate's
352
+ `meta` type through the nested generic call, and the handler's `meta` was `never`.
353
+ - **A brand on a handler's return** (`deliver(approve, { by })`). Rejected: a plain event typed by `delivers` gets the
354
+ same checks from the compiler and the walk, with one name less.
355
+ - **The reply text in the handler's return.** Rejected: the handler runs before the run answers, so it cannot know
356
+ `refused`.
357
+ - **`reply` in the Hook's config**, a function of the whole outcome. Rejected: the config is not the Assembly, it knows
358
+ no Workflow, and a publish does not hand it over.
359
+ - **The author's envelope** (`{ response_type, text }`, a Discord type), or a `reply` for each kind. Rejected: every
360
+ author learns two services for one text, and a message that the channel sees is shared state, which is an Effect.
361
+ - **A reply that changes the message** (Discord type 7, Slack `replace_original`). Rejected: a click that counts for
362
+ nothing would remove the buttons for everyone. The Machine's update of the message follows its state.
363
+ - **One winner** among the targets' texts (the first target, or a load-walk refusal of two). Rejected: it hides what
364
+ happened at the other targets, and the walk cannot see which Gates will be open.
365
+ - **No text for a Slack click**, only the Machine's update. Rejected: a click that reaches nothing, or that the run
366
+ refuses, then shows nothing at all.
367
+ - **A deadline fallback** (a deferred answer at 2.5 s, and the text posted later). Not now: the door is fast by design,
368
+ and the fallback adds a timer and a second road for a rare case.
369
+
129
370
  ## Consequences
130
371
 
131
372
  - Addresses are computed by jr2, never by the workflow — iids always (ADR-0016): fresh per invocation by default (the
@@ -138,4 +379,7 @@ Tests mock by `machine.provide()` at the layer under test.
138
379
  only**, which pass `endpoint` directly in run input — e2e drives their Machines by playing the Harness's Menu against
139
380
  `/agents/<iid>/*` with no cluster. Workflows that invoke `workspace()` always get real Sandboxes (the data plane is
140
381
  never faked — ADR-0009); their e2e tier requires kind.
141
- - Unsolicited mid-turn steering remains the residual hard case (ADR-0002/0006).
382
+ - A Caller's free text to an Agent is a Steering message ([ADR-0002](0002-duplex-streaming-actor.md)), never a workflow
383
+ event: it is in no Vocabulary, and no Gate takes it.
384
+ - Not set yet: a time budget for the handlers, which run in the Orchestrator's event loop for each open Gate of their
385
+ kind and event key.
@@ -1,15 +1,15 @@
1
1
  # `workspace()` wraps a body Machine and owns only Sandbox lifecycle
2
2
 
3
3
  jr2 provides Workspaces as a helper so workflows never provision Sandboxes themselves:
4
- `workspace(body, { input, repos, spec })` is a statically-imported factory (per
5
- [ADR-0011](0011-workflow-defined-events.md)'s import doctrine) returning a Machine that provisions a Sandbox and its
6
- worktrees, runs the author's **body** Machine inside it, and tears the Sandbox down when the body finishes. This makes
7
- GLOSSARY.md's "Workspace = child Machine bound to a unit of work" concrete.
4
+ `workspace(body, { input, repos })` is a statically-imported factory (per [ADR-0011](0011-workflow-defined-events.md)'s
5
+ import doctrine) returning a Machine that provisions a Sandbox and clones its Repos, runs the author's **body** Machine
6
+ inside it, and tears the Sandbox down when the body finishes. This makes GLOSSARY.md's "Workspace = child Machine bound
7
+ to a unit of work" concrete.
8
8
 
9
9
  ```
10
10
  placing: create Sandbox CR → await the pod scheduled, no deadline (ADR-0064)
11
- provisioning: await phase: Ready (watched, ADR-0063) → Harness /attach repos/worktrees (ADR-0004)
12
- running: invoke body; input = parent input + { workspace: { repos, branch } }
11
+ provisioning: await phase: Ready (watched, ADR-0063) → Harness /attach repos (ADR-0004)
12
+ running: invoke body; input = parent input + { workspace: { repos } }
13
13
  teardown: destroy the CR
14
14
  done: final; workspace output = body output
15
15
  ```
@@ -25,29 +25,29 @@ already is. When the guarantee still fails (`kill -9`, hard cancel), the operato
25
25
  (ADR-0001) — the reason ADR-0001 made the CR the durable desired state. Author-assembled provisioning/cleanup states
26
26
  (the obvious alternative) put the guarantee in every workflow's hands to forget, on every error path.
27
27
 
28
- ## Boundary: the spec speaks workspace vocabulary only
28
+ ## Boundary: the options speak workspace vocabulary only
29
29
 
30
- The spec — `{ branch, workGroup?, reviewSha? }` — is mapped from the parent's context by the author, but its _shape_ is
31
- workspace-domain: the branch to cut, and what the attach needs to cut it. The branch itself is the author's: its name
32
- and its granularity (per feature, per task, from a ticket, from run input) are the Machine's policy, not jr2's; the
33
- workspace only cuts it. Which Repos to attach is not per-run at all: they are slots on the wrapper's options
30
+ The wrapper's options are workspace-domain. Which Repos to attach is not per-run at all: they are slots on the options
34
31
  (`repos: { target: open, docs: { url, ref } }`, ADR-0051), static so `jr2 up` can see them, each bound by the Machine,
35
- by a composer's `customize`, or by a mapper over the door. Workflow configuration (review rounds, budgets, ticket data)
36
- passes through to the body untouched; the workspace never sees it. In return the body gets what it needs to operate
37
- inside the Sandbox: `{ repos: Record<slot, path>, branch }`, the handles keyed by the slots the wrapper declared, in
38
- declaration order. No handle names a primary checkout: which tree an Agent works in is a fact about that Agent's Turn,
39
- which the body states (ADR-0051). The Harness `endpoint` and the Sandbox name are mechanism-facing, not body-facing —
40
- `agentRun` resolves them **ambiently** from the enclosing wrapper (a registrar actor co-invoked in `running`, ADR-0016),
41
- so the body cannot mis-thread them. Keeping this boundary is what lets one helper serve any workflow rather than
42
- coupling to coding-shaped ones.
32
+ by a composer's `customize`, or by a mapper over the door. The Workspace only clones. A branch and a Worktree are the
33
+ body's: their names and their granularity (per feature, per task, from a ticket, from run input) are the Machine's
34
+ policy, not jr2's, and the body cuts each Worktree with `worktree()`
35
+ ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)). Workflow configuration (review rounds,
36
+ budgets, ticket data) passes through to the body untouched; the workspace never sees it. In return the body gets what it
37
+ needs to operate inside the Sandbox: `{ repos: Record<slot, path> }`, the handles keyed by the slots the wrapper
38
+ declared, in declaration order. A handle is the slot's folder, `/work/<slot>`, and not a Worktree. No handle names a
39
+ primary checkout: which tree an Agent works in is a fact about that Agent's Turn, which the body states (ADR-0051). The
40
+ Harness `endpoint` and the Sandbox name are mechanism-facing, not body-facing — `agentRun` resolves them **ambiently**
41
+ from the enclosing wrapper (a registrar actor co-invoked in `running`, ADR-0016), so the body cannot mis-thread them.
42
+ Keeping this boundary is what lets one helper serve any workflow rather than coupling to coding-shaped ones.
43
43
 
44
- The wrapper's `input` beside it is the door the spec maps FROM — the wrapper's own declared run-input schema, which also
45
- types the mapping ([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)). It sits on the wrapper and not on
46
- the body precisely because of the injection above: the body's input is the door plus `workspace`, which no caller sends.
47
- That same composition is what the door checks the body against, one way only — the body may not demand more than the
48
- wrapper hands it; demanding less is safe — so the injection is a compile-time contract, not just a comment. The host's
49
- own injection into a root machine's input (`instanceId`) counts as handed over for that check, because whether it
50
- actually reaches this body depends on where the wrapper sits and no type can see that
44
+ The wrapper's `input` is its door — the wrapper's own declared run-input schema, which also types a per-run slot's
45
+ mapper ([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)). It sits on the wrapper and not on the body
46
+ precisely because of the injection above: the body's input is the door plus `workspace`, which no caller sends. That
47
+ same composition is what the door checks the body against, one way only — the body may not demand more than the wrapper
48
+ hands it; demanding less is safe — so the injection is a compile-time contract, not just a comment. The host's own
49
+ injection into a root machine's input (`instanceId`) counts as handed over for that check, because whether it actually
50
+ reaches this body depends on where the wrapper sits and no type can see that
51
51
  ([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)).
52
52
 
53
53
  ## An Effect runs in its Workspace: the Harness container or the Sandbox Effector
@@ -58,8 +58,8 @@ Worktree file and a `command` that names a secret run in that Workspace's Sandbo
58
58
  container ([ADR-0005](0005-sandbox-pod-composition.md),
59
59
  [ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), which the provision adds only when an
60
60
  Effect below the `workspace()` is placed there; its image is the Sandbox Image unless the `workspace()` names
61
- `effector: { image }`. Each one's Worktree follows the Turn's rule (ADR-0057): the one Repo Slot's, or the slot it names
62
- with `slot`; a Workspace with several and none named refuses it. A push goes to that slot's Binding, and after every
61
+ `effector: { image }`. Each one's Worktree follows the Turn's rule (ADR-0057): its input names it, and under Repo Slots
62
+ it must. A push goes to the Binding of its Repo Slot, the one slot or the one its static `slot` names, and after every
63
63
  push whose published commit is not the Agent's own — signed copies, or one squashed commit, signed or not — the
64
64
  Workspace's Harness moves the Agent's branch to the published commits (the sync, ADR-0067). An Effect of these kinds
65
65
  outside any `workspace()` is refused at invoke, as an error, never as output. A `command` that names a secret runs
@@ -86,10 +86,17 @@ the same contract as a temp directory.
86
86
 
87
87
  ## Consequences
88
88
 
89
- - ADR-0004's single-active-Agent-per-Workspace invariant becomes structural: one sequential body per workspace, no lock
90
- machinery.
91
- - The workspace labels the CR with its run (`jr2.dev/run`, `jr2.dev/workflow`), which is what `jr2 ls` groups by
92
- (ADR-0009).
89
+ - The body chooses how many Agents work in its Workspace (ADR-0004, ADR-0075). The wrapper has no lock machinery for
90
+ them.
91
+ - The workspace labels the CR with its run, its Workflow and its path (`jr2.dev/run`, `jr2.dev/workflow`,
92
+ `jr2.dev/workspace`), and writes the exact path in the annotation `jr2.dev/workspace-path`. The path is the workspace
93
+ actor's path in the run's Machine: actor ids joined by `/`, and a Pool child's id is its item's id, which can hold any
94
+ character. A label value holds at most 63 characters from `[A-Za-z0-9._-]`, so the label holds a form of the path for
95
+ a selector only: each other character becomes `-`, leading and trailing characters that are not alphanumeric are
96
+ removed (a label value must start and end with one), and a value above 63 characters is cut and ends with a hash, as
97
+ the CR's name does. A person selects by it with `kubectl -l`; no jr2 reader does. The `jr2 workspace` verbs list the
98
+ CRs by `jr2.dev/run` and compare a handle, `<run-prefix>/<workspace path>`, with the annotation, with no Orchestrator
99
+ (ADR-0009). They do not select by a run prefix, because a label selector matches only a whole value.
93
100
  - **Lost workspace: the wrapper emits, the body decides.** While the body runs, a lease actor beside it renews the
94
101
  Sandbox's keepalive and follows the Sandbox watch for whether the workspace it attached to is still there (ADR-0021,
95
102
  ADR-0063) — continuously, and on snapshot restore as the first tick of that same loop rather than a separate
@@ -25,16 +25,22 @@ is needed either (ADR-0006).
25
25
  ## Decision
26
26
 
27
27
  - **The Custodian carries the Agent's Menu, and holds the credential that makes it work.** It is a jr2-composed
28
- container in every Harness pod ([ADR-0005](0005-sandbox-pod-composition.md)). The Sandbox token is mounted into it as
29
- a file, and into no other container. The Harness container holds the token's Stand-in,
30
- `JR2_SANDBOX_TOKEN=jr2-held-JR2_SANDBOX_TOKEN`, and reaches the Orchestrator only through the Custodian's control
31
- listener on `127.0.0.1:8081`. The Custodian forwards four routes, with the token put in place of the Stand-in, and
32
- answers every other route itself:
28
+ container in every Harness pod ([ADR-0005](0005-sandbox-pod-composition.md)). A Sandbox has two tokens, its **Sandbox
29
+ tokens**: the **control token** and the **fetch token**, mounted into the Custodian as files, and into no other
30
+ container. The Harness process holds the control token's Stand-in, which is secret: a file that no process it confines
31
+ can read, in no child's env
32
+ ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)). The fetch token's Stand-in
33
+ is public, `jr2-held-JR2_SANDBOX_TOKEN`, and `jr2-upload-pack` holds it as a constant. The Harness reaches the
34
+ Orchestrator only through the Custodian: the agent routes on its control socket, `/run/jr2/custodian/control.sock`,
35
+ which no confined program can replace (ADR-0076), and the fetch on `127.0.0.1:8081`. The Custodian forwards six
36
+ routes, with each route's own token put in place of its Stand-in, and answers every other route itself:
33
37
 
34
38
  ```
35
39
  GET /agents/<iid>/surface the Menu → the Orchestrator
36
40
  POST /agents/<iid>/events a pick → the Orchestrator
37
41
  GET /agents/<iid>/deliveries/<id> an outstanding pick's answer → the Orchestrator
42
+ POST /agents/<iid>/notices a step boundary's notices heard → the Orchestrator
43
+ POST /agents/<iid>/conversation a batch of the conversation's entries (ADR-0023) → the Orchestrator
38
44
  POST /fetch the ask (ADR-0053), for THIS pod's Sandbox → POST /sandboxes/<name>/fetch
39
45
  anything else 404
40
46
  ```
@@ -42,10 +48,10 @@ is needed either (ADR-0006).
42
48
  So the Agent's sole control-plane peer is a container it can reach but whose credential it cannot read. That is what
43
49
  makes ADR-0006's "the Agent never steers the workflow" **enforced** rather than advertised.
44
50
 
45
- - **The Harness presents the Menu to its model itself.** There is no MCP server and no MCP client. `menu.ts` turns each
46
- accepted event into a Menu item, with the event's JSON Schema as its parameters, and turns a pick into one
47
- `POST …/events` — and, for an outstanding pick, the polls of its delivery, inside the same tool call. The engine runs
48
- one model answer's calls in the model's order
51
+ - **The Harness presents the Menu to its model itself.** No MCP server serves the Menu, and no MCP client reads it.
52
+ `menu.ts` turns each accepted event into a Menu item, with the event's JSON Schema as its parameters, and turns a pick
53
+ into one `POST …/events` — and, for an outstanding pick, the polls of its delivery, inside the same tool call. The
54
+ engine runs one model answer's calls in the model's order
49
55
  ([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)), and only the first pick of a model
50
56
  answer runs. It imports nothing from pi: `menu-tools.ts` gives each item to pi as a tool. A harness that only speaks
51
57
  MCP would get a second presenter over the same `menu.ts`, not a second Menu. The receipt comes back as prose
@@ -55,14 +61,16 @@ is needed either (ADR-0006).
55
61
  keeps two thin HTTP surfaces, neither of them MCP:
56
62
 
57
63
  ```
58
- # human / webhook / CI — the Gate resource of ADR-0011, authenticated
59
- GET /runs/:id → open gates: accepts + schemas + acceptsNow + meta [Instance token]
60
- POST /runs/:id/gates/:gate/events → validate + deliver, or 409 [Instance token]
64
+ # human / Hook / CI — the Gate resource of ADR-0011, a Caller's (ADR-0078)
65
+ GET /runs/:id → open gates: accepts + schemas + acceptsNow + meta [answer]
66
+ POST /runs/:id/gates/:gate/events → validate + deliver, or 409 [answer]
61
67
 
62
68
  # the Agent's, through its pod's Custodian
63
- GET /agents/:iid/surface → Menu + Allowed picks + schemas + semantics [Sandbox token]
64
- POST /agents/:iid/events → validate + deliver → the receipt, or 409 [Sandbox token]
65
- GET /agents/:iid/deliveries/:id → an outstanding pick's answer, long-polled [Sandbox token]
69
+ GET /agents/:iid/surface → Menu + Allowed picks + schemas + semantics [control token]
70
+ POST /agents/:iid/events → validate + deliver → the receipt, or 409 [control token]
71
+ GET /agents/:iid/deliveries/:id → an outstanding pick's answer, long-polled [control token]
72
+ POST /agents/:iid/notices → the notices a step boundary appended, heard (ADR-0062) [control token]
73
+ POST /agents/:iid/conversation → a batch of the conversation's entries (ADR-0023) [control token]
66
74
  ```
67
75
 
68
76
  The receipt is `{ delivered, event, moved, turnComplete, allowed, deliveryId }`. A pick the workflow is still running
@@ -86,25 +94,34 @@ is needed either (ADR-0006).
86
94
  - **Bearer tokens, because the boundary is otherwise theater.** The Harness container shares the pod's network
87
95
  namespace, so an Agent can `curl` the Orchestrator directly; a per-pod NetworkPolicy cannot distinguish it from the
88
96
  Custodian. Only authentication closes this.
89
- - **Sandbox token** — minted per Sandbox at provision (a signed Sandbox name, so re-provision after a restart yields
90
- the same token), delivered as a Secret mounted into the **Custodian container only**. User `harness.env`/`envFrom`
91
- land on the Harness container, never the Custodian. Authorizes exactly: deliver to agent registrations **whose
92
- Sandbox is this one**, and ask for this Sandbox's fetch. Never a Gate (they are not on that surface at all), never
93
- another Sandbox. The Stand-in the Harness holds is refused on every route.
97
+ - **Control token and fetch token** — minted per Sandbox at provision (signed names, so re-provision after a restart
98
+ yields the same tokens), delivered as a Secret mounted into the **Custodian container only**. User
99
+ `harness.env`/`envFrom` land on the Harness container, never the Custodian. The control token authorizes exactly:
100
+ read and deliver to agent registrations **whose Sandbox is this one** and whose slot is the slot the Harness states
101
+ for the conversation (ADR-0076). The fetch token authorizes exactly: ask for this Sandbox's fetch. The Orchestrator
102
+ accepts each token only on its own routes. Neither reaches a Gate: a Gate refuses every principal of an Agent.
103
+ Neither reaches another Sandbox. A Stand-in is refused on every route.
94
104
  - **Instance token** — the human/CLI credential for Gates and run control. Minted into an in-cluster Secret at
95
- `jr2 up`; the CLI reads it over the kube API (RBAC is the gate) while port-forwarding the Service (ADR-0019).
105
+ `jr2 up`; the CLI reads it over the kube API (RBAC is the gate) while port-forwarding the Service (ADR-0019). It is
106
+ one Caller among others: a named Caller token, a JWT and a Hook reach Gates within their scopes
107
+ ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)). No Caller is a
108
+ principal of an Agent.
96
109
  - **The Instance-token door.** The Instance token may also read any Agent's surface and deliver to it —
97
110
  `GET /agents/:iid/surface`, `POST /agents/:iid/events` — because it is the operator. That is the operator speaking
98
111
  AS the Agent: it reads the Agent's Menu and Allowed picks and its picks are judged as the Agent's — they carry the
99
112
  Agent's sender (ADR-0011) — and no audience filter applies, so it can send a pick no Gate's caller may send
100
113
  ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)). It is deliberate — it is how the e2e tier stands in
101
- for an Agent — and it adds no party: neither `jr2 send` nor the Console offers it, and a Sandbox token never opens
114
+ for an Agent — and it adds no party: neither `jr2 send` nor the Console offers it, and a control token never opens
102
115
  it for a registration that is not its own.
103
116
  - An agent registration records its Sandbox **ambiently**: `agentRun` resolves the enclosing `workspace()` and records
104
117
  the same deterministic name the token was minted for (ADR-0016), so forgetting to pass it is unrepresentable (the
105
118
  pre-ambient version shipped exactly that footgun: `sandbox` omitted, every tool call 403'd — fail-closed but
106
119
  silent). A run-scoped token instead of a Sandbox-scoped one would be genuinely exploitable: derivable iids plus
107
- readable ticket ids would let one feature's coder inject a `review_verdict` into another feature's reviewer.
120
+ readable ticket ids would let one feature's coder inject a `review_verdict` into another feature's reviewer. That
121
+ holds across Sandboxes, where another feature's Agents are in another pod. Within one Sandbox several Agents can
122
+ share it ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)); there the Sandbox scope is
123
+ not enough, and ADR-0076 keeps the control credential where no Workspace code runs, so a coder's code cannot act as
124
+ the reviewer beside it.
108
125
  - **Reachability, narrowed to one caller.** The Orchestrator is always in-cluster (ADR-0019), so the route home is
109
126
  simply **Service DNS**, which the pod's maker writes into the Custodian's configuration — the Orchestrator at a
110
127
  Sandbox's provision, `jr2 up` for the Instance Harness. The Harness is told the Custodian's loopback address
@@ -131,7 +148,7 @@ is needed either (ADR-0006).
131
148
  - **The Orchestrator serves MCP, and the Custodian forwards it.** Rejected: it keeps the MCP transport and its session
132
149
  handling in the control plane and on both ends, to carry two plain HTTP calls.
133
150
  - **A uniform `/registrations/:address` API** for every caller. Rejected: it dissolves the run-scoped, discoverable Gate
134
- resource ADR-0011 designed on purpose (a webhook translator finds its Gate by `meta.prUrl`; `GET /runs/:id` lists open
151
+ resource ADR-0011 designed on purpose (a hooked Gate's handler matches its own `meta.pr`; `GET /runs/:id` lists open
135
152
  decisions) in exchange for aesthetic symmetry.
136
153
 
137
154
  ## Consequences
@@ -139,19 +156,26 @@ is needed either (ADR-0006).
139
156
  - **The operator needs no change.** ADR-0001 made sidecars generic container fragments the operator schedules without
140
157
  understanding; the Custodian is exactly that. `kubeSandbox` composes it (plus the token Secret, written right after
141
158
  the CR with the CR as its owner from birth, so Kubernetes reaps it with the Sandbox and no path leaks it — ADR-0001).
142
- - **Nothing in jr2 speaks MCP.** The Orchestrator, the Harness and the CLI carry no MCP dependency.
143
- - **One container holds the Sandbox token and every model key.** A fault in the Custodian's engine is a fault in the one
144
- container that holds both. ADR-0059 records the trade.
159
+ - **The control plane speaks no MCP.** The Orchestrator and the CLI carry no MCP dependency, and no MCP carries the
160
+ Menu. The Harness and the Instance Effector speak MCP only as clients of an author's servers
161
+ ([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)).
162
+ - **One container holds the Sandbox's tokens and every model key.** A fault in the Custodian's engine is a fault in the
163
+ one container that holds both. ADR-0059 records the trade.
145
164
  - **The `@kind` e2e tier owns the pod → Orchestrator leg** — the pod's Harness actually calls its tool through the
146
165
  Custodian, so a broken leg is a red test, and a Harness container that posts a pick straight to the Orchestrator with
147
166
  the Stand-in it holds is refused. The mechanics tier plays `/agents/:iid/*` from the host, which is honestly
148
167
  simulating the Harness's Menu, not an Agent.
168
+ - **Per-agent identity within one Sandbox is ADR-0076.** A Sandbox-scoped credential that an Agent's code could read
169
+ would let one Agent in a shared Sandbox deliver a pick as another, and ADR-0075 makes a shared Sandbox normal. Only
170
+ the Harness process holds the control credential, and each process it starts in `/work` runs confined; the Harness
171
+ states each conversation's slot, and the Orchestrator checks it on the whole `/agents/:iid/*` set. The `@kind` tier
172
+ gains the sibling cases: a coder's `bash` finds no control credential, and code it plants that the reviewer's own
173
+ `bash` runs cannot deliver a pick as the reviewer.
149
174
  - **NetworkPolicy is a second layer, not the control.** It bounds where a pod may talk; only the token bounds what it
150
175
  may _do_. `jr2 up` converges ingress policies that admit only the Orchestrator to a Harness pod
151
176
  ([ADR-0058](0058-a-harness-answers-the-orchestrator-alone.md)); the Orchestrator itself is left reachable, because a
152
177
  Custodian in every Harness pod must reach it and no policy can tell the Custodian's packets from the Agent's.
153
178
  - **The Harness wire is authenticated in the other direction.** The Orchestrator bears a token derived for the placement
154
179
  on every Harness call, and the Harness holds only its digest (ADR-0058). Same signed-name idea, reversed caller.
155
- - **Open: authn for non-kube callers.** The Instance token rides kube RBAC for anyone with cluster creds; what an
156
- ingress-exposed jr2 Application uses for human/webhook callers without them (`--url` mode) is out of scope here
157
- (ADR-0014 inherits the same question).
180
+ - **Callers without kube credentials are ADR-0078's.** The Instance token rides kube RBAC for anyone with cluster creds;
181
+ a Caller without them (`--url` mode, a browser, a Hook) holds a named token, a JWT or a Hook's signature.