@jr2/orchestrator 0.4.1 → 0.5.1

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 (327) hide show
  1. package/README.md +20 -2
  2. package/console/components/app.ts +2 -0
  3. package/console/components/fleet.ts +14 -1
  4. package/console/components/machine-pane.ts +41 -2
  5. package/console/main.ts +13 -1
  6. package/console/store.ts +102 -6
  7. package/console/style.css +21 -0
  8. package/docs/agents-block.md +5 -0
  9. package/docs/concepts/agents-and-turns.md +442 -0
  10. package/docs/concepts/architecture.md +230 -0
  11. package/docs/concepts/charts.md +143 -0
  12. package/docs/concepts/composition.md +357 -0
  13. package/docs/concepts/converge.md +254 -0
  14. package/docs/concepts/durable-runs.md +189 -0
  15. package/docs/concepts/effects.md +603 -0
  16. package/docs/concepts/gates.md +208 -0
  17. package/docs/concepts/machines-and-workflows.md +289 -0
  18. package/docs/concepts/pools-and-sources.md +230 -0
  19. package/docs/concepts/secrets-and-trust.md +222 -0
  20. package/docs/concepts/the-menu.md +378 -0
  21. package/docs/concepts/workspaces.md +678 -0
  22. package/docs/get-started/a-model-decides.md +563 -0
  23. package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
  24. package/docs/get-started/connect-a-model.md +248 -0
  25. package/docs/get-started/create-an-instance.md +194 -0
  26. package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
  27. package/docs/get-started/install.md +126 -0
  28. package/docs/guides/add-tools-to-the-sandbox.md +295 -0
  29. package/docs/guides/answer-a-gate.md +304 -0
  30. package/docs/guides/bind-open-parts.md +223 -0
  31. package/docs/guides/cancel-a-run.md +146 -0
  32. package/docs/guides/clean-up.md +206 -0
  33. package/docs/guides/fix-a-failed-converge.md +361 -0
  34. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  35. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  36. package/docs/guides/let-an-agent-write-a-chart.md +226 -0
  37. package/docs/guides/look-inside-a-sandbox.md +174 -0
  38. package/docs/guides/mirror-the-kit-images.md +176 -0
  39. package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
  40. package/docs/guides/open-the-console.md +91 -0
  41. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  42. package/docs/guides/push-and-sign-a-branch.md +486 -0
  43. package/docs/guides/run-a-chart-by-name.md +270 -0
  44. package/docs/guides/run-a-command-or-call-a-url.md +544 -0
  45. package/docs/guides/run-one-worker-per-item.md +321 -0
  46. package/docs/guides/set-an-agents-access-and-dials.md +375 -0
  47. package/docs/guides/size-a-workspace.md +204 -0
  48. package/docs/guides/start-a-run-from-a-run.md +237 -0
  49. package/docs/guides/start-a-run.md +207 -0
  50. package/docs/guides/upgrade-the-kit-version.md +240 -0
  51. package/docs/guides/use-a-hosted-provider.md +208 -0
  52. package/docs/guides/use-a-local-model.md +313 -0
  53. package/docs/guides/watch-a-run.md +326 -0
  54. package/docs/guides/write-a-workflow.md +318 -0
  55. package/docs/index.md +44 -0
  56. package/docs/llms.txt +216 -0
  57. package/docs/reference/authoring-surface/agent.md +128 -0
  58. package/docs/reference/authoring-surface/answer.md +140 -0
  59. package/docs/reference/authoring-surface/arg.md +60 -0
  60. package/docs/reference/authoring-surface/basic.md +62 -0
  61. package/docs/reference/authoring-surface/bearer.md +53 -0
  62. package/docs/reference/authoring-surface/chartschema.md +23 -0
  63. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  64. package/docs/reference/authoring-surface/command.md +191 -0
  65. package/docs/reference/authoring-surface/customize.md +64 -0
  66. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  67. package/docs/reference/authoring-surface/defineevent.md +114 -0
  68. package/docs/reference/authoring-surface/dispatch.md +126 -0
  69. package/docs/reference/authoring-surface/fail.md +83 -0
  70. package/docs/reference/authoring-surface/file.md +122 -0
  71. package/docs/reference/authoring-surface/frompick.md +277 -0
  72. package/docs/reference/authoring-surface/http.md +159 -0
  73. package/docs/reference/authoring-surface/isfailure.md +58 -0
  74. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  75. package/docs/reference/authoring-surface/open.md +74 -0
  76. package/docs/reference/authoring-surface/openbody.md +86 -0
  77. package/docs/reference/authoring-surface/paramsof.md +125 -0
  78. package/docs/reference/authoring-surface/pool.md +186 -0
  79. package/docs/reference/authoring-surface/push.md +112 -0
  80. package/docs/reference/authoring-surface/secret.md +118 -0
  81. package/docs/reference/authoring-surface/sentby.md +83 -0
  82. package/docs/reference/authoring-surface/source.md +51 -0
  83. package/docs/reference/authoring-surface/text.md +62 -0
  84. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  85. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  86. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  87. package/docs/reference/authoring-surface/type-agentslots.md +12 -0
  88. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  89. package/docs/reference/authoring-surface/type-arg.md +19 -0
  90. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  91. package/docs/reference/authoring-surface/type-binding.md +62 -0
  92. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  93. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  94. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  95. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  96. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  97. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  98. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  99. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  100. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  101. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  102. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  103. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  104. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  105. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  106. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  107. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  108. package/docs/reference/authoring-surface/type-childat.md +12 -0
  109. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  110. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  111. package/docs/reference/authoring-surface/type-commandinput.md +19 -0
  112. package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
  113. package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
  114. package/docs/reference/authoring-surface/type-customize.md +109 -0
  115. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  116. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  117. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  118. package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
  119. package/docs/reference/authoring-surface/type-duration.md +15 -0
  120. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  121. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  122. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  123. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  124. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  125. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  126. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  127. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  128. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  129. package/docs/reference/authoring-surface/type-failure.md +19 -0
  130. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  131. package/docs/reference/authoring-surface/type-filebody.md +41 -0
  132. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  133. package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
  134. package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
  135. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  136. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  137. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  138. package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
  139. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  140. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  141. package/docs/reference/authoring-surface/type-lends.md +38 -0
  142. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  143. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  144. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  145. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  146. package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
  147. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
  148. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  149. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  150. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  151. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  152. package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
  153. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  154. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  155. package/docs/reference/authoring-surface/type-openof.md +12 -0
  156. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  157. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  158. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
  159. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
  160. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  161. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  162. package/docs/reference/authoring-surface/type-pushinput.md +15 -0
  163. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  164. package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
  165. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  166. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  167. package/docs/reference/authoring-surface/type-reached.md +12 -0
  168. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  169. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  170. package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
  171. package/docs/reference/authoring-surface/type-secret.md +25 -0
  172. package/docs/reference/authoring-surface/type-sender.md +19 -0
  173. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  174. package/docs/reference/authoring-surface/type-size.md +85 -0
  175. package/docs/reference/authoring-surface/type-slothandles.md +32 -0
  176. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  177. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  178. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  179. package/docs/reference/authoring-surface/type-text.md +19 -0
  180. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  181. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  182. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  183. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  184. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  185. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  186. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  187. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  188. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  189. package/docs/reference/authoring-surface/type-workspaced.md +94 -0
  190. package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
  191. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  192. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  193. package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
  194. package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
  195. package/docs/reference/authoring-surface/workspace.md +259 -0
  196. package/docs/reference/authoring-surface.md +197 -0
  197. package/docs/reference/cli.md +433 -0
  198. package/docs/reference/config.md +756 -0
  199. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
  200. package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
  201. package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
  202. package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
  203. package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
  204. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  205. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  206. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  207. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  208. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  209. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  210. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
  211. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  212. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
  213. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
  214. package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
  215. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  216. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
  217. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  218. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
  219. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  220. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
  221. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
  222. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  223. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  224. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
  225. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
  226. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  227. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  228. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
  229. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  230. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
  231. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  232. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  233. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  234. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  235. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  236. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  237. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  238. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  239. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  240. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  241. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  242. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  243. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  244. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  245. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  246. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  247. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
  248. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
  249. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
  250. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  251. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  252. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  253. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  254. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
  255. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
  256. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  257. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  258. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
  259. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
  260. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  261. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  262. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +550 -0
  263. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
  264. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
  265. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  266. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
  267. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  268. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  269. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  270. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  271. package/docs/reference/decisions.md +83 -0
  272. package/docs/reference/glossary.md +648 -0
  273. package/docs/reference/http-api.md +1348 -0
  274. package/docs/reference/machines.md +190 -0
  275. package/docs/reference/mechanism-events.md +72 -0
  276. package/docs/why.md +206 -0
  277. package/package.json +5 -3
  278. package/src/actor.ts +541 -79
  279. package/src/agent.ts +114 -38
  280. package/src/ambient.ts +2 -1
  281. package/src/assembly-checks.ts +189 -7
  282. package/src/cause.ts +234 -0
  283. package/src/chart.ts +169 -37
  284. package/src/charts.ts +34 -6
  285. package/src/config.ts +430 -178
  286. package/src/custodian.ts +74 -22
  287. package/src/customize.ts +188 -46
  288. package/src/dispatch-effect.ts +945 -0
  289. package/src/door-compat.ts +217 -0
  290. package/src/durability.ts +11 -4
  291. package/src/effect.ts +341 -44
  292. package/src/effector-wire.ts +160 -11
  293. package/src/fail.ts +209 -0
  294. package/src/harness-client.ts +541 -47
  295. package/src/http-effect.ts +495 -53
  296. package/src/http.ts +427 -16
  297. package/src/index.ts +60 -82
  298. package/src/instance.ts +68 -8
  299. package/src/internal.ts +84 -0
  300. package/src/machine-doc.ts +21 -3
  301. package/src/mechanism-routes.ts +168 -0
  302. package/src/model-keys.generated.ts +53 -0
  303. package/src/model-keys.ts +18 -58
  304. package/src/notices.ts +66 -13
  305. package/src/open-body.ts +159 -23
  306. package/src/open-effect.ts +81 -8
  307. package/src/open.ts +59 -6
  308. package/src/parts.ts +184 -13
  309. package/src/party.ts +68 -2
  310. package/src/pick.ts +463 -11
  311. package/src/pool.ts +247 -36
  312. package/src/registration.ts +83 -4
  313. package/src/repos.ts +2 -1
  314. package/src/run-host.ts +565 -31
  315. package/src/run-output.ts +38 -0
  316. package/src/sandbox-effect.ts +273 -38
  317. package/src/sandbox-kube.ts +3 -1
  318. package/src/secret-placement.ts +52 -17
  319. package/src/server.ts +3 -0
  320. package/src/setup.ts +239 -56
  321. package/src/size.ts +121 -9
  322. package/src/snapshot-store.ts +54 -10
  323. package/src/stub-harness.ts +44 -16
  324. package/src/timers.ts +14 -1
  325. package/src/vocabulary.ts +69 -14
  326. package/src/wire.ts +90 -16
  327. package/src/workspace.ts +697 -175
@@ -0,0 +1,160 @@
1
+ # BDD acceptance tests via Cucumber.js (black-box, scenario-isolated)
2
+
3
+ jr2's per-package tests are `node --test` suites that run in-process and socket-free (e.g. the CLI's `cli.test.ts` calls
4
+ `main(argv, io)` with `fetch` bound to `app.request`). Those are fast and cover unit and integration behavior, but
5
+ nothing exercises the _system_ the way a user does — the real `jr2` binary, a real HTTP socket, exit codes, signals.
6
+ This ADR adds a second, complementary tier: **black-box end-to-end acceptance tests, written in Gherkin and run by
7
+ Cucumber.js**, living in a top-level `./features/` workspace package (`@jr2/e2e`).
8
+
9
+ ## What we decided
10
+
11
+ - **Two tiers, not one.** In-process `node --test` stays the unit/integration tier (fast, no sockets, per package).
12
+ Cucumber.js is the **e2e** tier: each step spawns the real `jr2` binary as a child process against a real orchestrator
13
+ server on an ephemeral port, and asserts only on what a shell sees — captured stdout (the one machine-readable
14
+ result), stderr (human activity), and the exit code. The e2e tier does not re-test what the in-process tier already
15
+ covers.
16
+ - **The per-scenario orchestrator is the image entrypoint, not a CLI verb.** There is no host dev mode (ADR-0019), but
17
+ the Orchestrator's server entrypoint is an ordinary Node process — the test fixture boots exactly that process on the
18
+ host, with `--url`/token env pointing the spawned `jr2` binary at it. Same real server the cluster runs, no cluster,
19
+ no user-facing verb. The wire-compatible **stub Harness** (ADR-0011) is a fixture owned by this tier and served the
20
+ same way.
21
+ - **`Feature` → `Rule` → `Scenario` maps to criteria.** A `Rule` is one acceptance criterion; a `Scenario` is an example
22
+ of it. This is organizational only — `Rule`s carry no shared state.
23
+ - **`Scenario` is the isolation unit — and the environment is part of it.** Each scenario gets its own scaffolded temp
24
+ instance (via the real `jr2 init`), its own server process + sqlite store, torn down in an `After` hook. It also gets
25
+ its own **environment**: the World spawns nothing from the ambient one, because a tier that inherits the developer's
26
+ shell has made that shell a silent participant in every scenario. Two classes are sealed. Every `JR2_*` variable is
27
+ scrubbed, since they say where to talk and as whom — an exported `JR2_URL` would point a whole suite at someone's
28
+ deployed instance, and `JR2_NAMESPACE` reaches the entrypoint's data-plane switch
29
+ (`composesSandbox && namespace !== undefined`), which would turn `workspace.feature`'s "no cluster, so it faults
30
+ pointedly" into a real Sandbox provisioned on whatever cluster was configured. Each tier adds back exactly what it
31
+ means to set, and the variables a scenario's own config reads arrive through the instance's `.env`, a file the step
32
+ wrote. `KUBECONFIG` is pinned to `/dev/null` wherever there is no cluster in the story, so a run-verb falling through
33
+ to the kube path finds nothing rather than the last cluster a human used; `@kind` and `@dist` own a namespace on a
34
+ real cluster and keep the real one. (The leak this closes was not theoretical: `cli-contract.feature`'s exit-code
35
+ scenario hung for its full 120s timeout against an unreachable home cluster, where it should fail in milliseconds —
36
+ and it hung only on the machine that had one.) Nothing is shared across scenarios, so the suite is safe under
37
+ `cucumber-js --parallel`.
38
+ - **An opt-in `@kind` tier for the data plane** (added once the `workspace()` slice landed — ADR-0012). Workspaces are
39
+ always real Sandboxes, so the only way to test them is against a real cluster: the `@kind` scenarios drive the
40
+ operator's Sandbox CR, a pod, the node's read-only Repo cache, an in-pod git worktree, and the Harness endpoint —
41
+ faking only the LLM. (Originally the Sandbox ran a **dev Harness image**, a wire-compatible stub in a container with
42
+ `git`; [ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md) retired it. The pod runs the STOCK Harness now — real
43
+ pi, the real Menu through the Custodian, real Working tools — and the substitution moved to the provider: a scripted
44
+ OpenAI-compatible endpoint the World serves from the host. Which is what makes this tier a second pi canary.) They are
45
+ tagged `@kind` and **excluded from the default profile**, so the everyday suite needs no docker. Bring-up is the
46
+ product's own path (ADR-0019): one shared VANILLA kind cluster and nothing else, then **`jr2 up` per scenario into a
47
+ fresh namespace** — which since ADR-0038 builds and loads every image it deploys, so no image is pre-loaded by hand.
48
+ Namespace-as-identity makes the scenario the isolation unit here too, so nothing is instance-bound to the cluster.
49
+ **The host address the pods dial back on is discovered, not assumed.** The scripted provider runs host-side, so the
50
+ tier needs the one address that routes from a pod back to this process — and it is not the same on every machine. On
51
+ Linux the daemon IS the host, so the kind bridge's gateway is the answer. Under a VM-backed daemon (colima, Docker
52
+ Desktop) that gateway is the VM: measured on colima, `172.18.0.1` reaches nothing while `host.docker.internal`,
53
+ `host.lima.internal` and `192.168.5.2` all answer — and on Linux those names mean nothing. So the candidates are
54
+ ordered by platform and the winner is VERIFIED, by `curl`ing the provider that is already listening from inside a kind
55
+ node (a pod's egress routes through it), for the price of one `docker exec` memoized per worker process. Taking the
56
+ bridge network's first IPAM entry is what broke: docker lists the IPv6 subnet first on some hosts and it carries no
57
+ gateway at all, so the tier now reads every entry that HAS one, IPv4 first. Failing to find any address is one error
58
+ naming every candidate tried, not an assertion in each scenario's `Before` hook. Measured: **22/22 on macOS/colima,
59
+ ~3m20 warm at degree 4** against the ~1m35 Linux figure below — the VM is slower, not different. **The tier runs
60
+ parallel, and three properties are what make that safe.** Scenario isolation (own namespace, own scripted model port);
61
+ no shared mutable image name between concurrent converges of one checkout — a Sandbox Image builds straight to its
62
+ content tag, with no intermediate, because the Harness arrives by volume at pod time
63
+ ([ADR-0037](0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md)) — while
64
+ [ADR-0041](0041-a-build-the-host-already-holds-is-not-spent-again.md) makes a warm scenario's converge build nothing
65
+ (~13s fresh-namespace converge, every build disk-skipped); and connection retries at the two seats that dial a Sandbox
66
+ before anything else ever has — [ADR-0042](0042-ready-is-not-routable-an-admission-retries-its-connection.md) has the
67
+ diagnosis (a Sandbox CR at `phase: Ready` is not yet ROUTABLE, so an unretried first dial is a lost turn; the Menu
68
+ read was a second instance of the same defect). Parallelism does not create those windows, it only samples them more
69
+ often — which is why any tier flake that vanishes at `--parallel 1` should be read as a routability-class defect
70
+ first, not as an isolation bug. Measured: **degree 4 → 8 consecutive runs, 88/88 scenarios, 1m30–1m43s each** on a
71
+ sealed tree (serial: ~5m). **The wired default is now `--parallel 4`** (in the `kind` profile, so a direct
72
+ `npx cucumber-js --profile kind` gets it too); pass `--parallel 1` to bisect a suspected isolation bug. Three tier
73
+ changes came out of the hunt and stay. A FAILED `@kind` scenario dumps its evidence (the provider's request count,
74
+ `jr2 status`, the Harness's `?view=history`, every pod log with timestamps, events, EndpointSlices) to
75
+ `features/.tmp/kind-failures/` before its namespace is deleted. The tier's own workflow ROUTES `agent.fault` instead
76
+ of ignoring it — an ignored terminal fault is an invisible one, and a tier that hangs where it could name the reason
77
+ is a tier that costs a session per defect. And **every** scenario, passing or not, is now scraped for what ADR-0042's
78
+ retries COST (`jr2.routability seat=… attempts=… ms=… last=…`), against a budget of 30s or 8 attempts judged per
79
+ worker in `AfterAll` — two meters because a REJECT spends attempts and a dropped SYN spends time, and the tier
80
+ measured one of each in its first two runs (2 attempts / 10667ms, then 3 attempts / 502ms / `ECONNREFUSED`). That last
81
+ one is the tier watching the fix rather than merely enjoying it: the retry is absorbed by design, so without it a
82
+ cluster drifting toward the 90s window would keep passing — slightly slower, silently — until the day it did not.
83
+ - **An opt-in `@console` tier for the Console's UX** (added with
84
+ [ADR-0032](0032-the-console-unlocks-with-the-instance-token.md)). The Console's risky behavior is interaction —
85
+ token-mode switching, the frame-triggered gate inbox, selection vs. folding — which no reducer test sees and no CLI
86
+ step drives. These scenarios hold a **Playwright page inside ordinary Cucumber steps** (Playwright the _library_, not
87
+ the `@playwright/test` runner): the browser is a second driver of the same black box, reusing the per-scenario
88
+ orchestrator fixture unchanged, and a scenario may drive both (the CLI starts a run; the browser sees it park). Tagged
89
+ `@console` and **excluded from the default profile** exactly as `@kind` is — the everyday suite must not require a
90
+ Chromium install — but they need no docker: a host-booted orchestrator serves the Console fine. Scope discipline: they
91
+ assert what a user does and sees, never pixels or layout; what the page _believes_ stays in `console-store.test.ts`.
92
+ - **Harness conformance, in `@jr2/harness`, in the default gate**
93
+ ([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)). The three tiers above all reach the
94
+ Harness through the **stub Harness**, which is a hand-written model of the _wire_ — it holds no conversation, so it
95
+ can report a turn settled `ended` or `aborted` without ever having had a turn to settle. That is the right fixture for
96
+ testing jr2, and it is blind by construction to what the real runtime keeps, drops, or sends onward. Those claims are
97
+ covered by the conformance suite in `packages/harness/test/`, driven through the real turn loop — pi at the exact pin,
98
+ the Menu over a real socket to the Custodian's address, a **scripted provider** — so a turn's shape (where it stalls,
99
+ what it half-emits) is chosen by the test rather than by a model. The witness is the message array the provider
100
+ receives on the _next_ turn, which is the only place these claims are visible. Not a separate tier: it runs as part of
101
+ `pnpm -r test` (no docker, no cluster), and it is the canary for pi bumps — **run it before bumping the pin**. (A
102
+ predecessor tier that ran the retired foreign harness runtime at its pin dissolved into this suite — ADR-0027.)
103
+ - **The Custodian suite, opt-in, against the real engine**
104
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). The Custodian's claims — the swap on
105
+ the wire, the strip, the refusals, the dial guard, TLS verification upstream, SSE timing, abort, and a real SDK
106
+ through `HTTPS_PROXY` — are Envoy's behaviour under jr2's configuration, so only the pinned Envoy image can prove
107
+ them. `just custodian-test` runs `node --test` (`packages/cli/test/custodian.docker.ts`) against it under docker, on
108
+ the host network with free ports, with local HTTPS upstreams under their own CA; about 23 seconds. Opt-in for the
109
+ `@kind` reason — it needs docker — and NOT part of `pnpm -r test`. What the default gate keeps is the rendering:
110
+ `custodian.test.ts` pins the bootstrap and script a pod's maker writes, socket-free. The `@kind` tier then proves the
111
+ Custodian in a real pod, in both placements, against a model provider that refuses any request without the key.
112
+ - **An opt-in `@model` tier, against a real model**
113
+ ([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)). Every tier above
114
+ fakes the model, so none can see a change in what a model DOES with what jr2 shows it — and ADR-0029/0062 measured
115
+ that such changes are large. This tier is `@kind` with the last fake removed: the same cluster and bring-up, `jr2 up`
116
+ per scenario into a fresh namespace, and behind the Custodian a real endpoint named by a profile
117
+ (`features/model-endpoints/<name>.json`, selected by `JR2_MODEL_ENDPOINT`). Its scenarios are CLAIMS — an ADR's
118
+ measured claim as N trials with a floor, scored into `features/.tmp/model-scorecard.json` against a committed baseline
119
+ — and THE RUN, one workflow from a failing test to a commit verified in the pod. It runs the endpoint profile's
120
+ `workers` scenarios at once, and `workers` × the profile's `parallel` runs must stay within what the endpoint serves
121
+ and what the host holds. Not in CI and not in `pnpm -r test`: `just e2e-model` probes the endpoint and says `skipped:`
122
+ when nothing answers, and whoever changes what the model reads runs it before calling the change done. A third pi
123
+ canary.
124
+ - **Shared cluster state is the one thing a scenario cannot own, and the pool is PROCESSES.** `--parallel` runs cucumber
125
+ workers as separate processes, so a fixture memoized "once per process" runs once per WORKER — four times,
126
+ concurrently, against one cluster. The tier has exactly one such fixture: the git seed (`steps/seed.ts`) it serves
127
+ in-cluster, which is cluster-scoped where everything else is namespaced per scenario. `kubectl apply` creates with
128
+ GET-then-POST, so the workers that lose that race are told `AlreadyExists` — which an `ensure` treats as success,
129
+ because the object being there is the whole ask. The rollout wait that follows is the real rendezvous, and it is
130
+ idempotent for every worker. (The same property is why the `@dist` profile is serial: its publish would otherwise
131
+ stand up one verdaccio per worker on a single port.)
132
+ - **A workspace package, not a bare folder.** `./features/` is `@jr2/e2e` so it owns its own `xstate` (+ cucumber)
133
+ dependency: a scaffolded instance's `workflows/*.ts` `import "xstate"`, resolved by walking up from the temp dir, and
134
+ the repo root has no `xstate`. The package's `node_modules` satisfies it. It is top-level (not under `packages/cli/`)
135
+ because an e2e test exercises most packages at once — the CLI is the common entry point, not the only conceivable one.
136
+ - **Zero-build, like the rest of the repo.** Cucumber.js v13 runs `.ts` ESM step definitions directly under Node 24's
137
+ native type-stripping — no `ts-node`, no transpile step. (Verified before adopting it.)
138
+
139
+ ## Considered options
140
+
141
+ - **Express BDD as a convention in `node --test`** (a Given/When/Then helper, no `.feature` files): keeps zero deps, but
142
+ loses the plain-English acceptance specs that are the point of writing them at the user boundary. Rejected — we want
143
+ the `.feature` files as readable, reviewable criteria.
144
+ - **In-process e2e** (drive `main(argv, io)` instead of a subprocess): faster, but it is integration, not e2e — it can't
145
+ catch bin/shebang, exit-code, signal, or attach bugs. That's already the job of the in-process tier; the e2e tier
146
+ exists precisely to cover the subprocess boundary.
147
+ - **Requiring a cluster for all e2e** (every scenario through `jr2 up`): honest but slow, and it gates the everyday
148
+ suite on docker. Rejected — the control-plane surface is fully exercisable against the bare server process; only the
149
+ data plane needs kind, and it has its own tier.
150
+
151
+ ## Consequences
152
+
153
+ - `@cucumber/cucumber` is the **first test-framework dependency** in a repo that otherwise uses only `node --test` — a
154
+ deliberate deviation, scoped to `@jr2/e2e` (it does not leak into the kit packages). `playwright` joins it there, same
155
+ scoping, browser binaries installed only where `@console` runs (CI, or `npx playwright install chromium` locally).
156
+ - The suite is run with `pnpm --filter @jr2/e2e test:e2e`; `@jr2/e2e` exposes **no** `test` script, so `pnpm -r test`
157
+ stays fast and unit-only.
158
+ - Human-in-the-loop delivery is covered at the gates API (`features/agent-and-gates.feature` — the agent played over
159
+ `/agents/<iid>/*`, the human over `/runs/:id/gates/:gate/events`); the `jr2 send --gate` CLI verb is covered at the
160
+ in-process tier. A CLI-driven e2e gate scenario remains a candidate follow-up `Rule`.
@@ -0,0 +1,141 @@
1
+ # Control events are workflow-defined and delivered by closure, not routed
2
+
3
+ Two decisions, out of the jr-parity design exercise: **jr2 ships the event mechanism and zero events**, and **an event
4
+ binds to a Machine state through the actor that exposes it** — delivery is a closure created at invoke time, so no
5
+ routing layer exists anywhere.
6
+
7
+ ## `defineEvent`: mechanism, not policy
8
+
9
+ `defineEvent({ name, input, audience?, semantics? })` produces a pure-data definition: a name, a zod input schema (flat
10
+ tagged objects, never `oneOf` — the ADR-0006 encoding rule), an optional audience tag (`agent` | `external` | `any`;
11
+ unset, each writing of the event belongs to the party its state, or the nearest state above it, invokes — ADR-0015), and
12
+ an optional semantics tag (`ack`, or the reserved `poll`; a def declares no output — ADR-0006). It is a **pure factory —
13
+ no import-time side effects, no global registry**. Because actor **inputs are serializable** (ADR-0007) they carry event
14
+ _names_, so resolution needs a name→def scope; that scope is **per-Machine**: `jr2Setup` takes the defs as values and
15
+ attaches the vocabulary to the machine (ADR-0015 — discovery reads it there; the module contract is
16
+ `export const machine` alone), and `gate`/`agentRun` resolve names against **the Machine that invoked them** —
17
+ `self._parent.logic`, public xstate API — never a run-wide set. Names are local to their Machine: `coding`'s `approve`
18
+ and `release`'s `approve` may differ, and one run may hold both, because a Machine nested inside another resolves
19
+ against its own defs and no map is ever merged. That is what makes a Machine composable by plain `invoke` (ADR-0049):
20
+ the importing Machine neither re-declares nor sees the nested one's events, and `workspace()`/`pool()` propagate
21
+ nothing. An unlisted name fails at invoke time with an error naming the Machine and its declared set. The attachment is
22
+ keyed on the machine's `config`, which xstate's `.provide()` passes through unchanged, so the host's per-run provide and
23
+ a test's `.provide()` both keep it. We rejected a global name registry (import = registration): it couples every
24
+ workflow in an instance — and any future npm-distributed one — through one namespace, and attribution can't be inferred
25
+ at import time. We also retired the run-scoped set this ADR first specified (`RunBinding.events`, the root's
26
+ vocabulary): it made every nested Machine's events the root's problem to re-declare, and turned a
27
+ same-name-different-payload pair into a collision that the per-Machine defs had already avoided.
28
+
29
+ `defineEvent` also carries the TypeScript side: `jr2Setup` derives the machine's event union from the defs — one source
30
+ of truth, no schema/type drift, and the machine stays fully typed for xstate tooling.
31
+
32
+ A def's `input` is what its sender supplies, and nothing the Machine adds. Where a pick starts work,
33
+ `fromPick(def, fill)` carries those params into the invoked Effect's input by name, and `fill` adds the Machine's part;
34
+ the load walk refuses a param nothing reads and a `fill` that overwrites one (ADR-0067, ADR-0015).
35
+
36
+ There is no jr2-blessed event vocabulary. `request_review`, `review_verdict`, `approve` are the _coding workflow's_
37
+ words; another workflow defines others. jr2 owns only definition, transport, validation, and delivery.
38
+
39
+ ## Agent side: invoke-scoped registration, delivery via `sendBack`
40
+
41
+ On start, the `agentRun` actor registers, for its instance id, a toolset — the menu derived from its invoking state's
42
+ transitions (ADR-0015) — whose handlers **close over that invocation's `sendBack`**; on stop it deregisters. A tool call
43
+ → validate against the named schema → `sendBack` a typed event, plus its sender → the event lands on **the state that
44
+ invoked the agent** (bubbling to ancestor states per statechart semantics — a machine can handle a
45
+ `report_blocked`-style event once at its root). The binding _is_ the closure.
46
+
47
+ - **No routing.** There is no instance→actor table and no workflow-side forwarding boilerplate; delivery works at any
48
+ nesting depth (workspace wrappers, pool children) with zero authored code.
49
+ - **State-scoped menus for free.** A transition swaps actors, which swaps registrations; the surface an Agent sees is
50
+ exactly the live registration.
51
+ - **One catch point.** A call for an unregistered iid (settled run, exited state) is rejected at the demux.
52
+
53
+ **Transport: the Agent's registration is read from the Sandbox** (ADR-0013). The Orchestrator speaks no MCP: it serves
54
+ `GET /agents/:iid/surface` + `POST /agents/:iid/events` under the Sandbox token, and the pod's Harness reads them
55
+ through the Custodian on `localhost`, which holds the token, and presents the surface to its model as the Menu (the
56
+ Harness re-reads per Submission, so the iid in the path is all the addressing needed). Lookup, validation, delivery, and
57
+ lifecycle stay implemented once, in the registration table.
58
+
59
+ ## External side: the same primitive over HTTP (`gate`), and each gate is a resource
60
+
61
+ `gate` is the symmetric actor for **every non-agent caller** — humans (`jr2 send`, a UI), webhook translators, CI, other
62
+ systems. "Human" is policy, not mechanism, so the actor is not named for one caller. Each invocation is an addressable
63
+ **gate**: input is `{ gate?, meta? }` — `gate` an optional authored id (the id is derived when absent — below), `meta`
64
+ serializable caller/integration context (PR URL, title); its accepted set derives from the gated state's transitions,
65
+ its ancestors' and its plain children's (ADR-0015). Registration is per-gate: `GET /runs/:runId` lists open gates
66
+ (`{ gate, accepts (names + schemas), acceptsNow, meta }` — `acceptsNow` the names the run would take from this Gate now,
67
+ read the way an Agent's Allowed picks are, ADR-0029), `POST /runs/:runId/gates/:gate/events` validates the body against
68
+ the named schema and delivers via `sendBack` into the gated state — or refuses a send the run would not take (ADR-0013)
69
+ — and leaving the state destroys the gate. Gate ids are **run-scoped by mechanism** (the same `system`→run mapping that
70
+ scopes event resolution), so `gate: "F-12"` in two concurrent runs cannot collide.
71
+
72
+ Gate-as-resource is forced by concurrency: several children park simultaneously (three features in `humanReview` is the
73
+ _normal_ case), all accepting `approve` — a run-level POST is ambiguous. And every real external surface already thinks
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).
80
+
81
+ **The gate id is derived; authoring one is the exception.** Absent an authored `gate`, the id is the gate actor's own
82
+ path below the run root, and jr2Setup's menu-derivation walk names an unnamed gate invoke with its state key path so the
83
+ leaf segment is readable — `F-12.body.humanReview`, not xstate's `0.body.humanReview` default. This is unique wherever
84
+ concurrently live siblings have distinct actor ids — the invariant any correct fan-out already maintains, because xstate
85
+ keys children by id (a duplicate silently shadows the children-map entry and breaks `xstate.done.actor.<id>`
86
+ correlation). Authored ids exist for meaningful flat names (jr's feature id) and collision on them is a loud invoke-time
87
+ error. Derivation runs at actor start, not the input mapper — deterministic from structure, so recomputation on every
88
+ (re)start is restore-stable, and one mechanism serves jr2Setup and plain-`setup` machines alike (the walk contributes id
89
+ _quality_ only, never correctness). Rejected: a workflow-computed id as the default (`gate: context.feature.id`) — it
90
+ quietly violated the address doctrine (Consequences below), making the gate id the one caller-facing address a workflow
91
+ computed by hand, and a machine with a static id (`"humanReview"`) was correct standalone but a latent, timing-dependent
92
+ collision once composed under a pool, firing only when two children park concurrently. Also rejected: deriving only in
93
+ the walk (a second dialect and an error case for plain-setup callers), and xstate's default invoke ids as the name
94
+ (invoke-index noise no caller cares about).
95
+
96
+ The full symmetry: **agents deliver through their per-iid registration (via the Custodian); everything else delivers
97
+ over HTTP (per-gate registration).** Two dialects, one primitive, zero routing. Internally that is literal structure:
98
+ one registration table (`address → { accepted event defs, deliver closure, meta, sender }`) that `agentRun` and `gate`
99
+ register into, with the agent surface and the gates HTTP API as dialect adapters over it — a future caller class (a
100
+ Slack bridge, an email reply) is another adapter, not a third mechanism. The table is implementation structure, not
101
+ vocabulary — workflows speak only `defineEvent` / `agentRun` / `gate`.
102
+
103
+ **Every delivered event carries its sender.** The table writes `sender: { party: "agent" | "gate", id }` on each event
104
+ 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.
110
+
111
+ `CANCEL` stays reserved as the run-level infra interrupt. jr's "exit 3 and let the human drive the bash script" becomes
112
+ "park in a durable state that advertises its human surface."
113
+
114
+ ## Static imports; live objects constructed per-invocation from input
115
+
116
+ Everything above is reachable by plain `import`. We rejected two alternatives:
117
+
118
+ - **Injected closures / a `defineWorkflow(kit)` wrapper** — xstate's `provide()` fills only the machine it is called on;
119
+ it cannot reach actors inside child machines, so host-side injection cannot compose past one level (the finding that
120
+ retired the templates-with-injected-providers model — see ADR-0015).
121
+ - **Adapter singletons** — a module-global "current adapters" holder cross-contaminates in-process multi-instance use
122
+ and turns mocking into global state. (The demux registry above is not this: it is rendezvous keyed by globally unique
123
+ ids, not a swappable adapter.)
124
+
125
+ The doctrine that makes static import work, sharpening ADR-0007: **actor logic is code; everything live is constructed
126
+ per-invocation from serializable input** — the Harness client from an endpoint, the kube client from ambient config.
127
+ Tests mock by `machine.provide()` at the layer under test.
128
+
129
+ ## Consequences
130
+
131
+ - Addresses are computed by jr2, never by the workflow — iids always (ADR-0016): fresh per invocation by default (the
132
+ lossy handoff), or `<run>/<machine actor path>/<agent>` plus its epoch under `continue: true` (ADR-0057); gate ids
133
+ derived from the gate's actor path by default, authored only to give external callers a meaningful name. Durable
134
+ handles live in the host ledger (ADR-0016), not in machine contexts.
135
+ - **Test stubbing happens at the wire, not in the actor.** The e2e tier (ADR-0010) hosts a wire-compatible stub Harness
136
+ beside its orchestrator fixture (admits the agent, holds the stream open, never acts). An endpoint is just a URL, so
137
+ `agentRun` keeps a single code path and cannot tell it is talking to a fake. Scope: **workspace-less test workflows
138
+ only**, which pass `endpoint` directly in run input — e2e drives their Machines by playing the Harness's Menu against
139
+ `/agents/<iid>/*` with no cluster. Workflows that invoke `workspace()` always get real Sandboxes (the data plane is
140
+ never faked — ADR-0009); their e2e tier requires kind.
141
+ - Unsolicited mid-turn steering remains the residual hard case (ADR-0002/0006).
@@ -0,0 +1,103 @@
1
+ # `workspace()` wraps a body Machine and owns only Sandbox lifecycle
2
+
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.
8
+
9
+ ```
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 } }
13
+ teardown: destroy the CR
14
+ done: final; workspace output = body output
15
+ ```
16
+
17
+ ## Why a wrapper Machine and not actors in the workflow
18
+
19
+ Teardown forces the shape. In xstate v5 a stop is synchronous — a stopped machine cannot run async work or transition
20
+ through cleanup states — so reliable multi-step teardown must be **internal states the machine reaches by its own
21
+ transitions**. The thing that provisions must therefore also observe the body's completion, which means it wraps the
22
+ body. The persisted `teardown` state is also **durable intent**: a crash mid-teardown restores into it and re-runs the
23
+ idempotent destroy, which host-side lifecycle could only replicate by rebuilding the same ledger the state machine
24
+ already is. When the guarantee still fails (`kill -9`, hard cancel), the operator's heartbeat-lease GC is the backstop
25
+ (ADR-0001) — the reason ADR-0001 made the CR the durable desired state. Author-assembled provisioning/cleanup states
26
+ (the obvious alternative) put the guarantee in every workflow's hands to forget, on every error path.
27
+
28
+ ## Boundary: the spec speaks workspace vocabulary only
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
34
+ (`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.
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
51
+ ([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)).
52
+
53
+ ## An Effect runs in its Workspace: the Harness container or the Sandbox Effector
54
+
55
+ An Effect in the body (ADR-0067) resolves the same ambient handles an Agent does. A `command` that names no secret runs
56
+ in the Harness container of the enclosing Workspace's Sandbox, as the Agent's user. A `push`, an `http` whose body is a
57
+ Worktree file and a `command` that names a secret run in that Workspace's Sandbox Effector, the pod's `effector`
58
+ container ([ADR-0005](0005-sandbox-pod-composition.md),
59
+ [ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), which the provision adds only when an
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
63
+ push whose published commit is not the Agent's own — signed copies, or one squashed commit, signed or not — the
64
+ Workspace's Harness moves the Agent's branch to the published commits (the sync, ADR-0067). An Effect of these kinds
65
+ outside any `workspace()` is refused at invoke, as an error, never as output. A `command` that names a secret runs
66
+ against a pinned commit in a scratch of the Effector's own, and its child is confined with Landlock from the Effector's
67
+ key, the secrets it does not name, the push credentials and the Worktree (ADR-0067, ADR-0068).
68
+
69
+ ## There is no retain policy: parking is retention
70
+
71
+ Teardown fires **only when the body reaches a final state or fails**. A Failure from the body
72
+ ([ADR-0073](0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md)) goes to a `failing` state,
73
+ which runs the same `destroy` and then sends the Failure up unchanged; a defect goes up with no `onError`, and its
74
+ Sandbox stays as evidence. A body that needs its Sandbox kept alive — escalation, a human inspecting the worktree, a
75
+ human-review gate — simply parks in a non-final state (typically holding a `gate`, ADR-0011). We considered and rejected
76
+ an `onSettled → retain | destroy` policy knob: parking expresses retention with no new API, and keeps the run visibly
77
+ "waiting" in `jr2 runs`. A parked run's Sandbox stays alive as long as the run does — each live workspace renews its own
78
+ lease, and the operator reaps only Sandboxes whose lease has lapsed (abandonment, not parking — ADR-0001).
79
+
80
+ ## Commits leave via the workflow, never the workspace
81
+
82
+ Getting commits out of the pod-local clone (push, PR) is **body flow**, decided by the workflow — a coding workflow
83
+ pushes the branch / opens a PR before its human gate, so review is PR-based and the forge owns merging. The workspace
84
+ attaches and destroys; it never touches remotes. A body that finishes without publishing loses its commits by design —
85
+ the same contract as a temp directory.
86
+
87
+ ## Consequences
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).
93
+ - **Lost workspace: the wrapper emits, the body decides.** While the body runs, a lease actor beside it renews the
94
+ Sandbox's keepalive and follows the Sandbox watch for whether the workspace it attached to is still there (ADR-0021,
95
+ ADR-0063) — continuously, and on snapshot restore as the first tick of that same loop rather than a separate
96
+ restore-time path. Intact → carry on (same endpoint — deterministic per Sandbox name; in-flight turns re-attach from
97
+ the host ledger, ADR-0016). Lost — reaped, deleted, or a replacement pod after an eviction or node loss — → the
98
+ pod-local clone and any unpushed commits are gone, so silently re-provisioning would resume into an inconsistent world
99
+ (tickets closed, commits vanished); instead the wrapper delivers a **`workspace.lost`** event into the body — same
100
+ channel as `agent.fault` — and the body's policy decides (route it to escalation, fail the run, or choose to
101
+ re-provision and restart). The body must route it: `workspace()` refuses a body with a non-final state that has no
102
+ route on itself or an ancestor, and an event that no transition takes faults the run
103
+ ([ADR-0074](0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md)).
@@ -0,0 +1,157 @@
1
+ # The Agent reaches its Machine through a container it cannot read
2
+
3
+ An earlier cut put the Agent's tool surface on the Orchestrator (`/mcp/:instanceId`), which meant an Agent must dial
4
+ **into** the control plane to drive its Machine. Validating the workspace slice on kind (2026-07-12) showed that leg did
5
+ not exist and could not be safely built as specified: nothing passed a callback URL into a Sandbox, `jr2 dev` bound
6
+ loopback, and — the real problem — an Agent that can reach the delivery API can deliver to **any** registration in its
7
+ run, including its own human-review Gate. The fix is not to hide the API from the Agent. Working tools give the Agent
8
+ code execution in the Harness container, which shares the pod's network namespace, so it can reach any address the pod
9
+ can. The fix is where the credential lives: in a container of the same pod that the Agent executes nothing in — the
10
+ **Custodian** ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). The Orchestrator speaks
11
+ no MCP, and keeps one HTTP surface over the registration table it already has
12
+ ([ADR-0011](0011-workflow-defined-events.md)).
13
+
14
+ ## The enabling fact: the Harness reads its Menu per Submission
15
+
16
+ Each Submission, the Harness reads `GET /agents/<iid>/surface`: the Agent's Menu, which it presents to its model as
17
+ tools named `mcp__jr2__<event>` (shipped instructions and the printer's prefix-stripping depend on that name;
18
+ [ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md) states it as explicit jr2 code, never
19
+ written by users). The Harness names the iid itself, so nothing has to _learn_ which turn is live — no push channel, no
20
+ long-poll, no `sandbox → active turn` index, no second inbound port on the pod. The same read carries the Turn's Allowed
21
+ picks, which the Harness states in the Briefing; the Menu itself is fixed for a conversation
22
+ ([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)), so no `list_changed` push
23
+ is needed either (ADR-0006).
24
+
25
+ ## Decision
26
+
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:
33
+
34
+ ```
35
+ GET /agents/<iid>/surface the Menu → the Orchestrator
36
+ POST /agents/<iid>/events a pick → the Orchestrator
37
+ GET /agents/<iid>/deliveries/<id> an outstanding pick's answer → the Orchestrator
38
+ POST /fetch the ask (ADR-0053), for THIS pod's Sandbox → POST /sandboxes/<name>/fetch
39
+ anything else 404
40
+ ```
41
+
42
+ So the Agent's sole control-plane peer is a container it can reach but whose credential it cannot read. That is what
43
+ makes ADR-0006's "the Agent never steers the workflow" **enforced** rather than advertised.
44
+
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
49
+ ([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)), and only the first pick of a model
50
+ answer runs. It imports nothing from pi: `menu-tools.ts` gives each item to pi as a tool. A harness that only speaks
51
+ MCP would get a second presenter over the same `menu.ts`, not a second Menu. The receipt comes back as prose
52
+ ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)). A 404 on the surface is a Turn that is
53
+ over, and it is never prompted ([ADR-0026](0026-a-turn-that-is-over-is-never-prompted.md)).
54
+ - **The Orchestrator hosts no MCP, and judges every pick.** The registration table stays the one internal primitive; it
55
+ keeps two thin HTTP surfaces, neither of them MCP:
56
+
57
+ ```
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]
61
+
62
+ # 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]
66
+ ```
67
+
68
+ The receipt is `{ delivered, event, moved, turnComplete, allowed, deliveryId }`. A pick the workflow is still running
69
+ when the POST answers — it moved the workflow to work inside the Agent's state
70
+ ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)) — is `outstanding`, and `deliveryId`
71
+ addresses its answer: `GET /agents/:iid/deliveries/:id` holds up to about 25 s and answers either `outstanding` (ask
72
+ again) or the receipt with `answered`, the Allowed picks read after the route, and the answer. A child that replied
73
+ inside the pick's own send and left the workflow at Rest makes the POST receipt itself the `answered` one. A stopping
74
+ or parked Orchestrator answers the poll 503, never 404, which says the Turn is over; the delivery is scoped to the
75
+ Sandbox its registration had when the pick was made, because the answer outlives a registration that a route out of
76
+ the Agent's state ends. A pick while one is outstanding, or while the Agent's state is busy, is answered
77
+ `409 { refused: "outstanding", outstanding?, allowed: [] }` and nothing is delivered. Lookup, validation, delivery and
78
+ lifecycle are implemented once, in the table. The token's scope, the live registration, and the event's name and
79
+ payload are judged here and nowhere else; the Custodian forwards and decides nothing about a pick. Neither surface
80
+ takes a body that carries `sender`: the table writes it on every delivered event (ADR-0011), so a client that posts it
81
+ — the Harness, `jr2 send --input` — is answered 400. The gates surface answers `{ ok: true }` when the run took the
82
+ send, and only then; a send the run would not take now — its selection holds another party's writing, or no writing
83
+ would move on this payload (ADR-0015, ADR-0029) — is answered `409 { error, acceptsNow }` and nothing is delivered.
84
+ `jr2 send --gate` prints that error, which names what the Gate takes now, and exits 1.
85
+
86
+ - **Bearer tokens, because the boundary is otherwise theater.** The Harness container shares the pod's network
87
+ namespace, so an Agent can `curl` the Orchestrator directly; a per-pod NetworkPolicy cannot distinguish it from the
88
+ 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.
94
+ - **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).
96
+ - **The Instance-token door.** The Instance token may also read any Agent's surface and deliver to it —
97
+ `GET /agents/:iid/surface`, `POST /agents/:iid/events` — because it is the operator. That is the operator speaking
98
+ 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
+ Agent's sender (ADR-0011) — and no audience filter applies, so it can send a pick no Gate's caller may send
100
+ ([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
102
+ it for a registration that is not its own.
103
+ - An agent registration records its Sandbox **ambiently**: `agentRun` resolves the enclosing `workspace()` and records
104
+ the same deterministic name the token was minted for (ADR-0016), so forgetting to pass it is unrepresentable (the
105
+ pre-ambient version shipped exactly that footgun: `sandbox` omitted, every tool call 403'd — fail-closed but
106
+ 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.
108
+ - **Reachability, narrowed to one caller.** The Orchestrator is always in-cluster (ADR-0019), so the route home is
109
+ simply **Service DNS**, which the pod's maker writes into the Custodian's configuration — the Orchestrator at a
110
+ Sandbox's provision, `jr2 up` for the Instance Harness. The Harness is told the Custodian's loopback address
111
+ (`JR2_CUSTODIAN_URL`) and nothing else.
112
+ - **A Machine-answered tool result is the held answer, for every pick that runs work.** No def declares it: a pick whose
113
+ target runs something inside the Agent's state is outstanding until the workflow is back at Rest, and the Harness
114
+ polls its delivery inside the one tool call (ADR-0002, ADR-0024). The gates surface answers `{ ok: true }` for a send
115
+ the run took and 409 for one it would not — external callers need no receipt. `deferred` is retired (ADR-0006). `poll`
116
+ is still in the model and the surface listing still ships each def's `semantics`; registering a `poll` def **fails
117
+ loudly as unimplemented** — a silent downgrade to `ack` would be a lying tool contract — and the Harness refuses a
118
+ turn whose surface offers one.
119
+
120
+ ## Considered options
121
+
122
+ - **Agent talks to the Orchestrator directly** (the original cut, plus a callback URL). Rejected: the credential lands
123
+ in a container where the Agent has code execution, so the Agent can deliver `approve` to its own human-review Gate —
124
+ it merges its own PR. The tool menu is advisory once you hold the key to the API.
125
+ - **A jr2-owned Adapter container that serves the Menu over MCP on loopback and holds the token.** It meets the claim,
126
+ but a whole container, image and package exist to translate two HTTP calls into MCP and back. Model keys need a
127
+ container the Agent executes nothing in anyway (ADR-0059); one container can hold every credential, and the
128
+ translation then has no reason to live outside the Harness.
129
+ - **Translation inside the Harness, with the credential there too.** Rejected — same container, same code execution,
130
+ same credential. The translation is inside the Harness now; the credential is not.
131
+ - **The Orchestrator serves MCP, and the Custodian forwards it.** Rejected: it keeps the MCP transport and its session
132
+ handling in the control plane and on both ends, to carry two plain HTTP calls.
133
+ - **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
135
+ decisions) in exchange for aesthetic symmetry.
136
+
137
+ ## Consequences
138
+
139
+ - **The operator needs no change.** ADR-0001 made sidecars generic container fragments the operator schedules without
140
+ understanding; the Custodian is exactly that. `kubeSandbox` composes it (plus the token Secret, written right after
141
+ 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.
145
+ - **The `@kind` e2e tier owns the pod → Orchestrator leg** — the pod's Harness actually calls its tool through the
146
+ Custodian, so a broken leg is a red test, and a Harness container that posts a pick straight to the Orchestrator with
147
+ the Stand-in it holds is refused. The mechanics tier plays `/agents/:iid/*` from the host, which is honestly
148
+ simulating the Harness's Menu, not an Agent.
149
+ - **NetworkPolicy is a second layer, not the control.** It bounds where a pod may talk; only the token bounds what it
150
+ may _do_. `jr2 up` converges ingress policies that admit only the Orchestrator to a Harness pod
151
+ ([ADR-0058](0058-a-harness-answers-the-orchestrator-alone.md)); the Orchestrator itself is left reachable, because a
152
+ Custodian in every Harness pod must reach it and no policy can tell the Custodian's packets from the Agent's.
153
+ - **The Harness wire is authenticated in the other direction.** The Orchestrator bears a token derived for the placement
154
+ 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).