@jr2/orchestrator 0.3.0 → 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 (354) hide show
  1. package/README.md +20 -2
  2. package/bin/server.ts +7 -4
  3. package/console/canvas.ts +27 -14
  4. package/console/components/app.ts +12 -1
  5. package/console/components/fleet.ts +52 -6
  6. package/console/components/machine-pane.ts +128 -2
  7. package/console/components/nav.ts +1 -1
  8. package/console/components/schema-form.ts +12 -4
  9. package/console/json-field.ts +23 -0
  10. package/console/main.ts +122 -39
  11. package/console/store.ts +321 -21
  12. package/console/style.css +111 -0
  13. package/docs/agents-block.md +5 -0
  14. package/docs/concepts/agents-and-turns.md +442 -0
  15. package/docs/concepts/architecture.md +230 -0
  16. package/docs/concepts/charts.md +143 -0
  17. package/docs/concepts/composition.md +357 -0
  18. package/docs/concepts/converge.md +254 -0
  19. package/docs/concepts/durable-runs.md +189 -0
  20. package/docs/concepts/effects.md +603 -0
  21. package/docs/concepts/gates.md +208 -0
  22. package/docs/concepts/machines-and-workflows.md +289 -0
  23. package/docs/concepts/pools-and-sources.md +230 -0
  24. package/docs/concepts/secrets-and-trust.md +222 -0
  25. package/docs/concepts/the-menu.md +378 -0
  26. package/docs/concepts/workspaces.md +678 -0
  27. package/docs/get-started/a-model-decides.md +563 -0
  28. package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
  29. package/docs/get-started/connect-a-model.md +248 -0
  30. package/docs/get-started/create-an-instance.md +194 -0
  31. package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
  32. package/docs/get-started/install.md +126 -0
  33. package/docs/guides/add-tools-to-the-sandbox.md +295 -0
  34. package/docs/guides/answer-a-gate.md +304 -0
  35. package/docs/guides/bind-open-parts.md +223 -0
  36. package/docs/guides/cancel-a-run.md +146 -0
  37. package/docs/guides/clean-up.md +206 -0
  38. package/docs/guides/fix-a-failed-converge.md +361 -0
  39. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  40. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  41. package/docs/guides/let-an-agent-write-a-chart.md +226 -0
  42. package/docs/guides/look-inside-a-sandbox.md +174 -0
  43. package/docs/guides/mirror-the-kit-images.md +176 -0
  44. package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
  45. package/docs/guides/open-the-console.md +91 -0
  46. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  47. package/docs/guides/push-and-sign-a-branch.md +486 -0
  48. package/docs/guides/run-a-chart-by-name.md +270 -0
  49. package/docs/guides/run-a-command-or-call-a-url.md +544 -0
  50. package/docs/guides/run-one-worker-per-item.md +321 -0
  51. package/docs/guides/set-an-agents-access-and-dials.md +375 -0
  52. package/docs/guides/size-a-workspace.md +204 -0
  53. package/docs/guides/start-a-run-from-a-run.md +237 -0
  54. package/docs/guides/start-a-run.md +207 -0
  55. package/docs/guides/upgrade-the-kit-version.md +240 -0
  56. package/docs/guides/use-a-hosted-provider.md +208 -0
  57. package/docs/guides/use-a-local-model.md +313 -0
  58. package/docs/guides/watch-a-run.md +326 -0
  59. package/docs/guides/write-a-workflow.md +318 -0
  60. package/docs/index.md +44 -0
  61. package/docs/llms.txt +216 -0
  62. package/docs/reference/authoring-surface/agent.md +128 -0
  63. package/docs/reference/authoring-surface/answer.md +140 -0
  64. package/docs/reference/authoring-surface/arg.md +60 -0
  65. package/docs/reference/authoring-surface/basic.md +62 -0
  66. package/docs/reference/authoring-surface/bearer.md +53 -0
  67. package/docs/reference/authoring-surface/chartschema.md +23 -0
  68. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  69. package/docs/reference/authoring-surface/command.md +191 -0
  70. package/docs/reference/authoring-surface/customize.md +64 -0
  71. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  72. package/docs/reference/authoring-surface/defineevent.md +114 -0
  73. package/docs/reference/authoring-surface/dispatch.md +126 -0
  74. package/docs/reference/authoring-surface/fail.md +83 -0
  75. package/docs/reference/authoring-surface/file.md +122 -0
  76. package/docs/reference/authoring-surface/frompick.md +277 -0
  77. package/docs/reference/authoring-surface/http.md +159 -0
  78. package/docs/reference/authoring-surface/isfailure.md +58 -0
  79. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  80. package/docs/reference/authoring-surface/open.md +74 -0
  81. package/docs/reference/authoring-surface/openbody.md +86 -0
  82. package/docs/reference/authoring-surface/paramsof.md +125 -0
  83. package/docs/reference/authoring-surface/pool.md +186 -0
  84. package/docs/reference/authoring-surface/push.md +112 -0
  85. package/docs/reference/authoring-surface/secret.md +118 -0
  86. package/docs/reference/authoring-surface/sentby.md +83 -0
  87. package/docs/reference/authoring-surface/source.md +51 -0
  88. package/docs/reference/authoring-surface/text.md +62 -0
  89. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  90. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  91. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  92. package/docs/reference/authoring-surface/type-agentslots.md +12 -0
  93. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  94. package/docs/reference/authoring-surface/type-arg.md +19 -0
  95. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  96. package/docs/reference/authoring-surface/type-binding.md +62 -0
  97. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  98. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  99. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  100. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  101. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  102. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  103. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  104. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  105. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  106. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  107. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  108. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  109. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  110. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  111. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  112. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  113. package/docs/reference/authoring-surface/type-childat.md +12 -0
  114. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  115. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  116. package/docs/reference/authoring-surface/type-commandinput.md +19 -0
  117. package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
  118. package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
  119. package/docs/reference/authoring-surface/type-customize.md +109 -0
  120. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  121. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  122. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  123. package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
  124. package/docs/reference/authoring-surface/type-duration.md +15 -0
  125. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  126. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  127. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  128. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  129. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  130. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  131. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  132. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  133. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  134. package/docs/reference/authoring-surface/type-failure.md +19 -0
  135. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  136. package/docs/reference/authoring-surface/type-filebody.md +41 -0
  137. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  138. package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
  139. package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
  140. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  141. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  142. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  143. package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
  144. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  145. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  146. package/docs/reference/authoring-surface/type-lends.md +38 -0
  147. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  148. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  149. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  150. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  151. package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
  152. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
  153. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  154. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  155. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  156. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  157. package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
  158. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  159. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  160. package/docs/reference/authoring-surface/type-openof.md +12 -0
  161. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  162. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  163. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
  164. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
  165. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  166. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  167. package/docs/reference/authoring-surface/type-pushinput.md +15 -0
  168. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  169. package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
  170. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  171. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  172. package/docs/reference/authoring-surface/type-reached.md +12 -0
  173. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  174. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  175. package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
  176. package/docs/reference/authoring-surface/type-secret.md +25 -0
  177. package/docs/reference/authoring-surface/type-sender.md +19 -0
  178. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  179. package/docs/reference/authoring-surface/type-size.md +85 -0
  180. package/docs/reference/authoring-surface/type-slothandles.md +32 -0
  181. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  182. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  183. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  184. package/docs/reference/authoring-surface/type-text.md +19 -0
  185. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  186. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  187. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  188. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  189. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  190. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  191. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  192. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  193. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  194. package/docs/reference/authoring-surface/type-workspaced.md +94 -0
  195. package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
  196. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  197. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  198. package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
  199. package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
  200. package/docs/reference/authoring-surface/workspace.md +259 -0
  201. package/docs/reference/authoring-surface.md +197 -0
  202. package/docs/reference/cli.md +433 -0
  203. package/docs/reference/config.md +756 -0
  204. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
  205. package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
  206. package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
  207. package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
  208. package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
  209. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  210. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  211. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  212. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  213. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  214. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  215. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
  216. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  217. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
  218. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
  219. package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
  220. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  221. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
  222. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  223. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
  224. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  225. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
  226. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
  227. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  228. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  229. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
  230. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
  231. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  232. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  233. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
  234. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  235. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
  236. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  237. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  238. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  239. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  240. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  241. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  242. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  243. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  244. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  245. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  246. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  247. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  248. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  249. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  250. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  251. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  252. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
  253. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
  254. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
  255. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  256. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  257. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  258. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  259. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
  260. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
  261. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  262. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  263. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
  264. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
  265. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  266. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  267. 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
  268. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
  269. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
  270. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  271. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
  272. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  273. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  274. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  275. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  276. package/docs/reference/decisions.md +83 -0
  277. package/docs/reference/glossary.md +648 -0
  278. package/docs/reference/http-api.md +1348 -0
  279. package/docs/reference/machines.md +190 -0
  280. package/docs/reference/mechanism-events.md +72 -0
  281. package/docs/why.md +206 -0
  282. package/package.json +6 -4
  283. package/src/actor.ts +976 -61
  284. package/src/agent.ts +114 -38
  285. package/src/ambient.ts +4 -1
  286. package/src/assembly-checks.ts +347 -0
  287. package/src/assembly-tar.ts +494 -0
  288. package/src/assembly.ts +721 -0
  289. package/src/cause.ts +234 -0
  290. package/src/chart.ts +2182 -0
  291. package/src/charts.ts +748 -0
  292. package/src/config.ts +520 -162
  293. package/src/content-hash.ts +105 -0
  294. package/src/custodian.lua +6 -5
  295. package/src/custodian.ts +143 -39
  296. package/src/customize.ts +388 -55
  297. package/src/dispatch-effect.ts +945 -0
  298. package/src/door-compat.ts +217 -0
  299. package/src/durability.ts +11 -4
  300. package/src/effect-checks.ts +557 -0
  301. package/src/effect.ts +1202 -0
  302. package/src/effector-wire.ts +674 -0
  303. package/src/fail.ts +209 -0
  304. package/src/fingerprint.ts +4 -3
  305. package/src/gate.ts +8 -0
  306. package/src/git-clone.ts +65 -0
  307. package/src/git-push.ts +336 -0
  308. package/src/harness-client.ts +764 -48
  309. package/src/held-secrets.ts +82 -26
  310. package/src/holds.ts +156 -0
  311. package/src/http-effect.ts +1423 -0
  312. package/src/http.ts +916 -113
  313. package/src/images.ts +47 -21
  314. package/src/index.ts +62 -43
  315. package/src/instance.ts +505 -59
  316. package/src/internal.ts +84 -0
  317. package/src/kube-client.ts +333 -0
  318. package/src/machine-doc.ts +106 -6
  319. package/src/mechanism-routes.ts +168 -0
  320. package/src/model-keys.generated.ts +53 -0
  321. package/src/model-keys.ts +18 -58
  322. package/src/mount-digest.ts +53 -0
  323. package/src/names.ts +66 -8
  324. package/src/notices.ts +141 -0
  325. package/src/open-body.ts +1580 -0
  326. package/src/open-effect.ts +217 -0
  327. package/src/open.ts +59 -6
  328. package/src/parts.ts +709 -41
  329. package/src/party.ts +534 -0
  330. package/src/pick.ts +761 -0
  331. package/src/pool.ts +248 -37
  332. package/src/registration.ts +382 -19
  333. package/src/repo-fetch.ts +60 -85
  334. package/src/repos.ts +65 -75
  335. package/src/rest.ts +133 -0
  336. package/src/run-host.ts +2383 -115
  337. package/src/run-output.ts +38 -0
  338. package/src/sandbox-effect.ts +1126 -0
  339. package/src/sandbox-effector.ts +111 -0
  340. package/src/sandbox-kube.ts +1721 -0
  341. package/src/sandbox-watch.ts +260 -0
  342. package/src/secret-placement.ts +529 -0
  343. package/src/server.ts +284 -84
  344. package/src/setup.ts +819 -117
  345. package/src/size.ts +454 -0
  346. package/src/snapshot-store.ts +115 -20
  347. package/src/stub-harness.ts +49 -16
  348. package/src/sync-client.ts +54 -0
  349. package/src/timers.ts +191 -0
  350. package/src/tokens.ts +35 -4
  351. package/src/vocabulary.ts +135 -15
  352. package/src/wire.ts +238 -10
  353. package/src/workspace.ts +1195 -338
  354. package/src/sandbox-kubectl.ts +0 -1108
@@ -0,0 +1,113 @@
1
+ # Sandboxes are provisioned by a Kubernetes operator via a generic CRD
2
+
3
+ A `Sandbox` custom resource describes infrastructure only — a primary container, a generic list of additional sidecar
4
+ container specs, volumes, resources, secrets, idle timeout — and a custom operator reconciles it into one Pod — created
5
+ once, never recreated: a pod that is gone or terminal makes the Sandbox `Lost` (ADR-0021) — plus a Service, reporting a
6
+ `status.endpoint` the Orchestrator uses to reach the Harness. The CRD names the Repos a Sandbox needs by identity —
7
+ enough for the operator to place the pod and mount each node cache read-only (ADR-0051) — and knows nothing about
8
+ clones, worktrees, or Agents; those are layered on by the Orchestrator after the Sandbox reaches `Ready`. The operator
9
+ also publishes what the Orchestrator needs to know about the pod onto the Sandbox's status — `podUID`, a scheduling
10
+ condition with the scheduler's message, the Harness container's restarts and last terminated reason, and the same for
11
+ the Sandbox Effector's container — because the Orchestrator watches Sandboxes and never reads a Pod
12
+ ([ADR-0063](0063-the-orchestrator-watches-the-cluster.md)).
13
+
14
+ We chose the operator over the Orchestrator calling the Kubernetes API directly because the custom resource _is_ the
15
+ durable desired state: Sandboxes survive an Orchestrator restart, and garbage collection / readiness / retries live in
16
+ one reconciler instead of being threaded through the Machine. We kept the CRD generic (rather than coding/worktree-aware
17
+ with an init-container clone) so the operator stays workflow-agnostic — matching jr2's thesis that the framework is a
18
+ grab-bag of composable pieces, with git/worktree being just one piece a coding Machine bolts on. The operator is
19
+ git-aware to exactly the extent of the `Repo` CRD and the cache agent that keeps a bare checkout per node (ADR-0051),
20
+ and of `gitCredentials`, the push Secrets a Sandbox names, which it projects into the Sandbox Effector (the `sidecars`
21
+ entry named `effector`) alone (ADR-0051, ADR-0068): a fetch is infrastructure, shared by every Sandbox on the node; a
22
+ worktree is a Machine's. The cost is an extra runtime step (worktree setup as its own state that can fail independently)
23
+ and an operator to build and maintain.
24
+
25
+ ## The Harness is the primary container; sidecars are opaque fragments
26
+
27
+ The CR's `spec.image` is the **Harness** — jr2's own server hosting the instance's Agents (ADR-0005/0018/0027).
28
+ Everything else in the pod rides `spec.sidecars`: plain Kubernetes `Container` fragments (image, ports, env,
29
+ volumeMounts) the operator schedules **without understanding them** — exactly as a Deployment's pod template carries
30
+ arbitrary containers without the controller knowing their roles. That is how the Custodian (ADR-0013, ADR-0059) and the
31
+ User Container (ADR-0005) land in the pod with zero operator awareness: `kubectlSandbox` compiles them into the sidecar
32
+ list when it builds the CR. Containers cannot be added to a live pod (native containers, that is), so everything a
33
+ Sandbox will run must be in the spec at spin-up.
34
+
35
+ One Harness **image** serves many **Agents** — the Harness resolves model, instructions, and its Menu at runtime without
36
+ a rebuild, reading the definition each admission carries (ADR-0018, ADR-0049) and reading the Menu through the Custodian
37
+ per turn (ADR-0013). So the Orchestrator injects a fixed Harness image plus **config** (env + the prepared worktree),
38
+ never a per-Agent image build. The HTTP prompt body itself carries only `{message, images}`, so anything persona-shaping
39
+ is set at provision time, not per request.
40
+
41
+ ## Idle GC: a renewed lease, not a TTL
42
+
43
+ The operator reaps **abandoned** Sandboxes — ones whose Orchestrator is gone — as the backstop behind the Orchestrator's
44
+ own teardown (ADR-0012). "Abandoned" is defined by a lease: each live workspace renews a keepalive annotation
45
+ (`jr2.dev/keepalive: <timestamp>`) on its CR, and the operator deletes a Sandbox only once `spec.idleTimeout` (default
46
+ `30m`) has elapsed since **max(creation, last keepalive)**. A run parked on a Gate for hours keeps its Sandbox — its
47
+ lease is still renewing — while a `kill -9`'d Orchestrator's Sandboxes reap one idle-timeout later. An Orchestrator that
48
+ restarts within the timeout re-attaches and resumes renewing; one that stays down longer finds the CR gone and delivers
49
+ `workspace.lost` to the restored body. Creation counts as the initial lease, so a CR whose run faults before it ever
50
+ renews is still reaped on schedule. The lease is renewed from the CR's write, so a Sandbox that waits for a node or for
51
+ quota is never taken for abandoned (ADR-0064).
52
+
53
+ There are no ownerReferences in this scheme — nothing in the cluster represents a run, so liveness has to be asserted,
54
+ not referenced. The lease only asserts; the Orchestrator learns from its watch of the Sandboxes (ADR-0021, ADR-0063).
55
+
56
+ ## The operator scales with Sandboxes, not with the cluster
57
+
58
+ The operator is deployed once per cluster (ADR-0008), so its cost must follow what jr2 runs, not what the cluster runs.
59
+ The scaling review (R7, 2026-09-30) found it fine at 40 Sandboxes and predicted the breaks at hundreds, or on a busy
60
+ cluster; these are the rules.
61
+
62
+ - **The operator caches only what it labels.** controller-runtime keeps an in-memory copy of every object a controller
63
+ watches; the Pod and Service informers take the selector `app.kubernetes.io/managed-by=jr2-operator`, which every
64
+ object the operator creates carries, so memory and watch traffic are O(Sandboxes) whatever cluster the operator lands
65
+ in. Sandboxes and Repos are jr2's own kinds and are cached whole. The one read that must not trust the cache — the
66
+ confirmation before a pod is declared lost (ADR-0021) — goes past it through the API reader.
67
+ - **Reconciles run in parallel, at fixed numbers.** controller-runtime never reconciles one Sandbox twice at once and a
68
+ reconcile holds no shared state, so the Sandbox controller runs 16 workers, the Repo controller 4, and the client's
69
+ rate limit is 100 QPS with a burst of 200, so a burst of CRs is placed in a fraction of a second instead of one API
70
+ round trip at a time. Kit values, not config: nobody has a reading to tune them by, and the API server's own priority
71
+ and fairness still governs.
72
+ - **A Repo event wakes only the Sandboxes it changes.** A Sandbox reads only its own node's entry of each Repo it names
73
+ (ADR-0051, ADR-0053), so the Repo watch diffs `status.nodes` and enqueues the Sandboxes naming the Repo whose
74
+ `status.node` changed; a spec change wakes them all. Without this a cache agent's per-fetch status write woke every
75
+ Sandbox naming the Repo on every node, S×N per interval and S² on a burst of asks.
76
+ - **The token Secret is owned from birth.** The Orchestrator writes the CR first, then the Secret with the CR as its
77
+ owner, so no path — an ownerRef patch that fails, an Orchestrator that dies between the two writes — leaves a token
78
+ behind on the cluster. The pod is created the moment the CR is seen and may reach its mount before the Secret lands;
79
+ kubelet retries the mount, and the Secret arrives one API round trip after the CR, well before the scheduler and
80
+ kubelet get there.
81
+
82
+ Not taken (R15): a parked Workspace releasing its pod. A run parked on a Gate for hours holds a pod for hours; that is
83
+ the problem Agent Substrate exists for, and upstream Kubernetes has no production checkpoint/restore to build it on. The
84
+ one constraint kept: the Sandbox CRD stays close to `agent-sandbox`'s shape, so a backend swap stays possible.
85
+
86
+ Not taken (R14): a headless Service. A ClusterIP Service programs kube-proxy rules on every node per Sandbox; a headless
87
+ one keeps the name and the endpoint and costs no node anything. Deferred until a cluster with many nodes measures it.
88
+
89
+ ## Known limitations
90
+
91
+ These are accepted gaps in the current operator, recorded so they aren't silently forgotten. Each is a deliberate
92
+ deferral.
93
+
94
+ - **Egress is not restricted.** The pod is hardened at the host boundary — `automountServiceAccountToken: false`,
95
+ pod/container `securityContext` (`runAsNonRoot`, `allowPrivilegeEscalation: false`, drop `ALL`, seccomp
96
+ `RuntimeDefault`) — and its _ingress_ is the Orchestrator's alone: `jr2 up` converges a NetworkPolicy that admits no
97
+ other pod to a Sandbox ([ADR-0058](0058-a-harness-answers-the-orchestrator-alone.md)), which kind's `kindnet`
98
+ enforces. Nothing yet restricts _egress_: an untrusted Agent can still reach the cluster's API server IP (it holds no
99
+ credential there) and the internet. A default-deny egress policy with an allowlist is the next layer, and ADR-0058
100
+ records it open. The ADR-0013 boundary does not depend on either: only the token bounds what a pod may _do_;
101
+ NetworkPolicy bounds where it may _talk_.
102
+ - **Sidecars are plain containers, not native sidecars.** They are scheduled as ordinary `containers`, not Kubernetes
103
+ ≥1.29 native sidecars (`initContainers` with `restartPolicy: Always`). There is no start-ordering or
104
+ termination-ordering guarantee between the Harness and its sidecars — the Harness may begin serving before the
105
+ Custodian is up, or outlive it during shutdown. Start-up is covered without ordering: the Custodian's readiness probe
106
+ holds the pod's Ready, which holds the Sandbox's, so no Turn is admitted before it serves (ADR-0059). Revisit if
107
+ ordering becomes load-bearing.
108
+ - **`reconcileStatus` writes status only when it changed.** Every status write is a watch event at the Orchestrator
109
+ (ADR-0063), so an unconditional `Status().Update` per reconcile would wake it for nothing.
110
+ - **Consumers must gate on `phase: Ready`, not endpoint presence.** `status.endpoint` is populated as soon as the
111
+ Service exists, before the Harness is reachable; only `phase: Ready` (backed by a readiness probe) means "serving".
112
+ The `Terminating` phase is best-effort — there is no finalizer, so a fast delete may GC the Pod/Service before the
113
+ phase is observed.
@@ -0,0 +1,110 @@
1
+ # The agent actor is a duplex channel; its control plane is Menu tool calls
2
+
3
+ The `agentRun` actor drives an Agent run as a long-lived duplex channel, not a request/response call: it admits the run
4
+ with `POST /agents/:name/:id`, persists the durable handle host-side, receives the Agent's decisions as events, and can
5
+ feed later turns down to the same conversation. We rejected the simpler `fromPromise`-over-synchronous-`POST` model
6
+ because a synchronous call returns exactly once, at the end — it cannot express human-in-the-loop (mid-run approval,
7
+ steering), and a held hour-long cross-pod connection is lost on any blip or Orchestrator restart.
8
+
9
+ ## Control plane: Menu tool calls, not stream interpretation
10
+
11
+ The Agent emits the domain events the Machine cares about by **calling tools** — its Menu, derived from the invoking
12
+ state's transitions (ADR-0015), read by the Harness through the pod's Custodian and presented to the model as tools
13
+ (ADR-0013). Events are Agent-authored and semantic rather than inferred from a generic event stream. This was proven
14
+ end-to-end against a real Harness + vLLM (PoC #5): the Agent called its tool, the Machine transitioned, and a gated
15
+ action observably occurred only post-approval.
16
+
17
+ The split-channel model:
18
+
19
+ | Direction | Kind | Mechanism |
20
+ | --------- | --------------------------------------------------------- | ---------------------------------------- |
21
+ | up | control / decision (the workflow's events, ADR-0011) | Menu tool call → Custodian → delivery |
22
+ | up | progress / telemetry (tokens, attempts, "still working") | Harness stream (lossy, observation-only) |
23
+ | down | the next turn's prompt (feedback, steering at a boundary) | a new submission on the same iid |
24
+ | down | the Turn part, restated (the Allowed picks changed) | the Harness's read at each step boundary |
25
+ | down | interrupts (deliberate terminal cancel) | `abort` on the wire |
26
+
27
+ Tool calls are Agent-initiated, so the Machine speaks inside a Turn only as an answer. The one exception is the
28
+ **restated Turn part**: when something other than the Agent's own pick changes its Allowed picks — a Gate's answer, an
29
+ outside event — the Harness learns it by reading the surface at each step boundary and appends the new list
30
+ ([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)). It is a pull, not a push:
31
+ nothing new reaches the Harness, and what is restated is fixed text plus Menu names. The residual hard case is
32
+ **unsolicited mid-turn steering** — free text from the Machine to an Agent that has not asked. That remains unsolved,
33
+ and it is the only behavior in that bucket, not a dependency of the whole control plane.
34
+
35
+ ## Durability: the admission is the handle
36
+
37
+ `send` returns an **admission** — `{ streamUrl, offset, submissionId }` — and that is the durable handle: the host
38
+ persists an `iid → admission` ledger beside the run snapshot in the same save (ADR-0016), never in Machine context.
39
+ `wait(admission)` reconnects from the offset, which is how a restarted Orchestrator re-attaches to an in-flight turn
40
+ instead of restarting it (proven by dropping the actor mid-run and re-attaching; the run completed).
41
+
42
+ **Stopping the actor is a local abandon, deliberately.** The wire's `abort` (`POST /agents/:name/:id/abort`) is a
43
+ first-class terminal outcome, but actor STOP does not call it: a host shutdown stops every actor, and those runs must
44
+ stay alive server-side for restore to re-attach. Remote abort is reserved for a deliberate terminal act, not a stop side
45
+ effect.
46
+
47
+ ## A held tool result is a poll, never a held socket
48
+
49
+ A pick that moves the workflow to work inside the Agent's state is answered when the workflow is back at Rest there
50
+ ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)) — seconds or hours later. One request cannot
51
+ carry that wait: the Harness's HTTP client (Node's undici) cuts a request at 300 s with no response headers, and at 300
52
+ s between body chunks, and its error then told the Agent "the pick never reached the Orchestrator — try it again" for a
53
+ pick that WAS delivered (measured: cut at 302 s, both timers, and through the real turn loop at 301 s). An Orchestrator
54
+ restart mid-request is a reset after the request was sent, which the Custodian rightly does not retry and which read as
55
+ "the Custodian could not reach it". A model told either goes round again.
56
+
57
+ So the answer is **poll-with-progress**. The pick's `POST` answers at once: `outstanding`, with a delivery id. Inside
58
+ the one tool call, the Harness long-polls `GET /agents/:iid/deliveries/:id` (ADR-0013), about 25 s per request — the
59
+ same register as the Harness's own stream long-poll — until the answer comes. Every timer on the path sees short
60
+ requests; a GET is safe to repeat, so a reset, a 503 from a stopping Orchestrator and a park cost a retry, never the
61
+ pick; and an Orchestrator restart is a longer wait in the same Turn, because the hold is saved with the snapshot and the
62
+ restored host answers the same poll. There is no default time bound on the wait: pi awaits a tool's `execute` with no
63
+ timer of its own, and the Runaway guard counts calls, not time
64
+ ([ADR-0035](0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md)).
65
+
66
+ The same rule holds for the `POST` itself. The Harness says "never reached" only when it knows that the Orchestrator did
67
+ not take the pick. A pick that leaves the Agent's state ends the Turn. The Orchestrator's abort of that Turn can land
68
+ before the pick's receipt does, and the abort then cuts a request that the Orchestrator already took. Real models showed
69
+ this on 2026-10-22 (Haiku 5.5 and Qwen, the `@model` claim `noticed`). The run moved on the pick, and the model read
70
+ "the pick never reached the Orchestrator (This operation was aborted) — try it again". In its next Turn, the model said
71
+ that the call failed.
72
+
73
+ So a pick's `POST` has three cases when it gets no receipt. They give five texts: one for the first case, and two for
74
+ each of the others. The Harness returns the second and the third case as the call's answer, not as an error, because an
75
+ error reads as a failed pick and invites a second one:
76
+
77
+ - **The request never left the Harness.** Then the call fails, and the error is "the pick never reached the Orchestrator
78
+ (<detail>) — try it again". Two kinds of failure prove this. The first kind is a transport failure in which no
79
+ connection was made, so no byte of the request was written. These are a refused connection (`ECONNREFUSED`), a failed
80
+ name lookup (`ENOTFOUND`, `EAI_AGAIN`), undici's connect timeout (`UND_ERR_CONNECT_TIMEOUT`), and an unreachable
81
+ network or host (`ENETUNREACH`, `EHOSTUNREACH`) from the `connect` call. The second kind is the Custodian's 502 with
82
+ `x-jr2-custodian: unreachable`, when all of its Envoy response flags say that it did not forward the request: `UF`,
83
+ `UH`, `DF` or `URX` ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). `URX`, the retry
84
+ limit, is in this set for one reason only. The `events` route retries only on `reset-before-request,connect-failure`,
85
+ so its last attempt failed before the request was sent. A test of `custodian.ts` holds that route to those two
86
+ conditions. The Custodian's text for these is "the Custodian could not reach it (<detail>)", in the place of
87
+ `<detail>`. After an abort of the call, no failure proves this, because the call was the Turn's and the Turn is over.
88
+ - **The Harness does not know whether the Orchestrator took the pick.** Every other failure can come after the request
89
+ left: a reset, a socket that the other side closed, a headers timeout, an abort of the call, or a Custodian 502 with
90
+ another flag. These flags are an upstream connection that closed (`UC`), a stream reset (`UMSDR`), an upstream
91
+ protocol error (`UPE`), a remote or a local reset (`UR`, `LR`), and a downstream protocol error (`DPE`). A `DPE` means
92
+ that the Custodian did not forward a whole request, but a body framing error can come after the headers went upstream.
93
+ The Custodian says `UPE`, `UR` and `LR` as its 502 too, so that Envoy's own 503 for them does not read as a stopping
94
+ Orchestrator's 503. After an abort of the call, the answer is "It is not known whether this pick reached the workflow:
95
+ your turn ended while the call to `<pick>` waited for the workflow's answer (<detail>). A pick that the workflow takes
96
+ can end the turn in this way. Do not call `<pick>` again before you read the next message: it tells you where the
97
+ workflow is now." Otherwise the Turn goes on, and the answer is "It is not known whether this pick reached the
98
+ workflow: the call to `<pick>` failed before the workflow answered (<detail>). A second call to `<pick>` can run the
99
+ pick twice." It gives no order to call again, because a pick with no target runs again when it is sent again. And it
100
+ does not say how the model can find out: a guard back, a self-transition and an unchanged list of Allowed picks all
101
+ look the same as a pick that did not arrive.
102
+ - **The Orchestrator took the pick, and its answer was not read.** This is an outstanding pick whose poll an abort of
103
+ the call cuts, or a 2xx whose body did not arrive. A 2xx is the Orchestrator's own word that it took the pick. The
104
+ answer is "This pick reached the workflow, and your turn ended before you read its answer. Do not call `<pick>` again
105
+ before you read the next message: it tells you where the workflow is now." When the call's own abort was not the
106
+ cause, "your turn ended before you read its answer" is "its answer did not reach you (<detail>)".
107
+
108
+ Each other answer of the Orchestrator keeps its own text, because it is a receipt or a refusal and not a lost request: a
109
+ 503 from a stopping Orchestrator (the pick was not taken — try it again, ADR-0042), a 409 while a pick is outstanding
110
+ (ADR-0024), and a 404 when the Turn is over (ADR-0026).
@@ -0,0 +1,124 @@
1
+ # A Sandbox's workspace is a private local clone of a read-only cache, not a shared worktree
2
+
3
+ Each Sandbox gets its **own** `.git` by running a local `git clone --shared` from a canonical bare checkout that is
4
+ mounted **read-only**, rather than a `git worktree add` against a single shared `.git`. The clone borrows objects from
5
+ the checkout (via alternates) for fast, disk-cheap setup but owns its refs, index, HEAD, config, and — critically — its
6
+ **lock files**, so a Sandbox contends only with itself. The checkout it borrows from is a **node-local cache** the
7
+ operator keeps (ADR-0051), one per repository per node, so a Sandbox can be scheduled on any node.
8
+
9
+ ## Why
10
+
11
+ jr gave every feature a `git worktree`, but all worktrees resolved through **one** `default/.git`. Git serializes access
12
+ to that shared state with lock files, so concurrent commit/fetch/rebase/ref updates across worktrees blocked each other
13
+ _through the filesystem_. That contention is the bottleneck the whole per-pod isolation design exists to kill — so the
14
+ worktree model had to change, not just move into pods. A private `.git` removes the shared lock surface **by
15
+ construction**; that is the load-bearing claim, and it does not depend on a measurement. A PoC corroborated it directly
16
+ (same concurrent churn: lock-retries present with a shared `.git`, zero with private clones) and confirmed setup stays
17
+ worktree-cheap (`--shared` `.git` ~7M vs a worktree's ~7M, vs a full copy ~33M).
18
+
19
+ The isolation is the point, not the speed: the read-only source plus a private pod-local clone is what confines an
20
+ Agent's blast radius to its own pod (it physically cannot write — or `gc` — the canonical source or any sibling's work),
21
+ which is the Sandbox thesis (GLOSSARY.md) applied to git.
22
+
23
+ We keep jr's worktree _ergonomics_ — fast spin-up, a working dir that looks like a totally normal checkout, one
24
+ canonical checkout everything branches from — because borrowing objects sets up about as cheaply as a worktree while
25
+ giving an independent `.git`.
26
+
27
+ ## The `--shared` invariant, and how it is enforced
28
+
29
+ A `--shared` clone reads the cache's object store via `objects/info/alternates`; if an object it borrows is ever deleted
30
+ from the cache, the clone corrupts (a PoC reproduced this by `gc`-ing a writable checkout under a live clone). So the
31
+ one invariant: **objects in a cache are never deleted while clones borrow them.** Deletion only happens via
32
+ `git gc`/`prune` — including the `gc --auto` that porcelain commands (`fetch` among them) run past a loose-object
33
+ threshold, which prunes unreachable objects (e.g. commits a force-push orphaned that a long-lived clone still borrows).
34
+ Enforcement, by side:
35
+
36
+ - **Sandboxes cannot violate it**: the cache is mounted read-only, and gc is a write. Structural.
37
+ - **The cache agent is the one writer per node, and it is config-pinned**: it sets `gc.auto=0`, `gc.pruneExpire=never`,
38
+ and `maintenance.auto=false` on every checkout it clones **or adopts**, before any fetch — so neither its own fetches
39
+ nor a human running git inside the checkout can trigger object deletion. Only a deliberate `git gc --prune=now`
40
+ defeats it, which is `rm -rf` territory.
41
+ - **Maintenance that deletes no object is allowed, and it is the agent's** (scaling review R13, 2026-09-30). A cache
42
+ that is only ever fetched accumulates a pack per fetch until every object lookup in every borrowing clone opens
43
+ thousands of packs. So the agent consolidates on its interval, once loose objects or packs pass a count:
44
+ `git repack -a -d -k` (one pack holding EVERY object, unreachable ones included — `-k` is the invariant, spelled as a
45
+ flag: a force-pushed branch's commits stay for the clone that borrows them), `git commit-graph write` and
46
+ `git pack-refs --all`. Git is built for readers under a repack — an open pack stays readable after its unlink, and a
47
+ missed object re-scans the pack list — so a borrowing clone never sees an absent object. The rule for anyone who edits
48
+ that command: no `--prune`, no `gc`, and `gc.pruneExpire=never` stays.
49
+ - **Eviction is deletion of the whole checkout**, never of objects inside one, and only once no pod on the node mounts
50
+ it (ADR-0051). A live clone never loses what it borrows.
51
+
52
+ The fallback where borrowing can't be made safe (a source jr2 doesn't control) is a **full copy** — fully independent,
53
+ gc-proof, at full disk and the slowest setup. An immutable generation-swap scheme (refresh = a new snapshot, old
54
+ generations released as their Sandboxes drain) was considered again as OCI image volumes in ADR-0051 and rejected there:
55
+ it buys nothing gc pinning does not, and a repack costs a full re-upload.
56
+
57
+ ## Refresh is fetch-in-place; the `Repo` resource is the catalog
58
+
59
+ The catalog is the set of `Repo` custom resources in the Instance's namespace (ADR-0051): the Orchestrator creates one
60
+ per repository its Machines bind, at boot from the walk and at first attach for a per-run url, and the operator
61
+ reconciles each onto the nodes that need it. A cache is cloned when missing and fetched when present — on the resource's
62
+ interval, on demand before every attach, so a Workspace starts from the remote's now, and on demand for every fetch run
63
+ inside a pod — the worktrees' `origin` asks the cache, and the cache asks the remote (ADR-0053). `fetch` only **adds**
64
+ objects — safe under the invariant by nature. A repository that fails to sync degrades that repository, never the
65
+ Instance: a cold clone that fails fails the one provision that needed it, pointedly; a fetch that fails on a warm cache
66
+ lets the attach proceed on what the cache holds, announced as stale
67
+ ([ADR-0048](0048-the-orchestrator-boots-without-its-repos.md)). The cache agent runs in-cluster, so every repository
68
+ must be **fetchable from the cluster** (an HTTPS token or deploy-key Secret for private ones — ADR-0019, ADR-0047); a
69
+ working copy that exists only on someone's host is not a valid source. Local-path urls are cloned like any other url,
70
+ never used in place. A checkout found in a node's cache directory with no `Repo` resource is adopted and pinned but not
71
+ refreshed, and is evicted like any other once nothing mounts it.
72
+
73
+ A repository is identified by its url — host plus path, with scheme, user, a trailing `/` and `.git` dropped — so every
74
+ spelling a Machine writes for one repository lands on ONE cache per node, and the expensive clone happens once per
75
+ repository per node. The directory name is derived from that identity, printed beside the url by `jr2 status`, and never
76
+ chosen by a human; there is no name to derive twice or to orphan.
77
+
78
+ ## Storage shape: a cache per node
79
+
80
+ The contract every Sandbox needs is narrow — _a read-only directory per repository it attaches, on the node it runs on_
81
+ — and the writable side is always pod-local (`emptyDir`), which scales with agent count by construction. The backing is
82
+ a **hostPath directory per node** that the Instance's cache agent (a DaemonSet) populates and the operator mounts
83
+ read-only into Sandboxes, with a soft affinity toward nodes already holding the repositories a Sandbox names. A node
84
+ that lacks one clones it on first need — the image-pull model, and the reason the cluster's node count is not a limit on
85
+ Sandbox placement. A single-node cluster (kind) is the one-node case of the same model; there is no separate volume path
86
+ to keep.
87
+
88
+ Read-only-ness is load-bearing twice over: nobody can write the shared thing (no write contention), and nobody can `gc`
89
+ it (the structural half of the invariant). Shell-in (`kubectl exec`) needs none of this — the cache exists purely for
90
+ fast setup.
91
+
92
+ ## Where setup runs
93
+
94
+ Worktree provisioning is a post-`Ready` step owned by the Orchestrator: the Sandbox CRD names repositories by identity
95
+ so the operator can place and mount them, but it knows nothing about clones or worktrees (ADR-0001, ADR-0051).
96
+ `workspace()`'s attach is one idempotent in-pod script the Harness runs on `POST /attach` (ADR-0012,
97
+ [ADR-0063](0063-the-orchestrator-watches-the-cluster.md)), once `Ready` says every cache is present and fetched.
98
+
99
+ ## In-Sandbox layout: worktrees off the per-Sandbox clone (gwtmux convention)
100
+
101
+ "Not a shared worktree" above means _not a worktree off a fleet-shared `.git`_. **Inside** a Sandbox, worktrees are the
102
+ intended ergonomic — taken off the Sandbox's **own** clone. The per-Sandbox clone _is_ the `default/`, and branch
103
+ working trees are its siblings, matching the layout [gwtmux](https://github.com/snapwich/gwtmux) already expects, under
104
+ the slot the Machine gave the repository (ADR-0051):
105
+
106
+ ```
107
+ /work/<slot>/default/ the per-Sandbox `git clone --shared` (holds the pod-local .git; the default branch checked out)
108
+ /work/<slot>/<branch>/ `git worktree add` siblings, one per branch worked
109
+ ```
110
+
111
+ Because the layout is identical to the one used outside the cluster, existing worktree tooling works **unchanged** when
112
+ you exec into a Sandbox. Object resolution falls through: a branch worktree shares the per-Sandbox `default/.git`, whose
113
+ alternates point at the read-only cache — existing objects are read from the cache, new commits land in the pod-local
114
+ `.git`. `default/` is a checkout of the Repo's default branch, as gwtmux's `default/` is outside the cluster: work
115
+ happens in the branch worktrees, and `default/` is the reference tree a human or a tool finds where it expects one. Not
116
+ `--no-checkout`: an empty working tree reads as every tracked file deleted to `git status`, which misleads an Agent that
117
+ looks. The branch name `default` is refused by the spec guard: the branch Worktree is a sibling of that directory, never
118
+ it. `origin`'s push url is the binding's own spelling, so a Machine that bound over ssh pushes over ssh even when the
119
+ cache was cloned over https.
120
+
121
+ This does **not** reintroduce jr's contention: the shared `.git` here is _per-Sandbox_, and it is accessed serially
122
+ under the standing invariant that **one Agent runs at a time per Workspace** — structural since ADR-0012 (one sequential
123
+ body per workspace). A Machine that runs concurrent Agents in one Sandbox, or an Agent that spawns parallel git-touching
124
+ subagents, reintroduces contention (pod-scoped, not fleet-wide) and must opt into separate Sandboxes instead.
@@ -0,0 +1,230 @@
1
+ # The Sandbox pod's containers: Harness, Custodian, an optional User Container, and an optional Sandbox Effector
2
+
3
+ A Sandbox pod composes up to four containers around one shared worktree volume (`/work`):
4
+
5
+ - **Harness container** — the CR's primary container: the Sandbox Image (user-built or user-brought,
6
+ [ADR-0037](0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md)) with jr2's runtime mounted at
7
+ `/opt/jr2` and the command overridden to start the Harness. The Agent's toolchain lives here because the Working tools
8
+ (read/write/edit/bash) execute here — the tools the Agent can reach are this image's. It is also where a human
9
+ `kubectl exec`s to inspect a run with the agent's exact tools, worktrees, and filesystem.
10
+ - **Custodian container** — jr2-composed sidecar, upstream Envoy configured at provision by the Orchestrator
11
+ ([ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md),
12
+ [ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)): the pod's credential holder. It holds
13
+ the Sandbox token and the held secrets its pod names — model API keys above all — as files, and carries the Harness's
14
+ traffic out: its Menu and the ask to the Orchestrator, and its outbound HTTPS. It exists as a separate container
15
+ precisely _because_ the Working tools give the Agent code execution in the Harness container — the credentials live
16
+ where the Agent cannot read them, and the Harness container's env holds only their Stand-ins.
17
+ - **User Container** — opt-in third seat, composed only when the `workspace()` names its image (a `file:` docker context
18
+ the Machine ships or a registry ref, the same resolution as the Sandbox Image, no default —
19
+ [ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)). The authoring surface is that string —
20
+ `user?: string` among `workspace()`'s options, beside `image` — or the object it widens to,
21
+ `{ image, resources?: { limits: { memory?, cpu? } } }`, whose only other key is the seat's split of the Workspace's
22
+ Size ([ADR-0060](0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md)): a budget is the pod's business, not
23
+ the process's, so it puts nothing in the container. Env and ports are not forwarded, because every key jr2 forwarded
24
+ into the process would be a crack in "jr2 puts nothing in it"; a routable port (the follow-up below) is the known
25
+ candidate for the next key. In the pod it is the container named `user` (`kubectl exec -c user`). It runs its **own
26
+ entrypoint, untouched**: jr2 injects nothing into its process — no env, no command, no probe — and overrides nothing;
27
+ it mounts only what the checkouts need to be checkouts (below). The zero-contract seat, which is exactly why it
28
+ exists. Two jobs no other seat can do:
29
+ - **Unattended services.** A container's one command belongs to the Harness in the primary seat (ADR-0037), so an
30
+ image's own services — an sshd for managed access, an IDE server, a metrics agent — need a container whose command
31
+ jr2 deliberately does not own. A system built _on_ jr2 that hands people access to their Sandboxes automates through
32
+ this seat.
33
+ - **Credential visibility.** A session's secrets — a forwarded ssh agent socket above all — live in this container's
34
+ private mount namespace, where the Agent executes nothing. The human sshs in with agent forwarding, works, and the
35
+ socket is gone on disconnect; at no point does it share a filesystem with code the Agent runs.
36
+ - **Sandbox Effector** — a jr2-composed container, added at provision only when an Effect below the `workspace()` is
37
+ placed there — a `push`, an `http` whose body is a Worktree file, or a credentialed `command`
38
+ ([ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md),
39
+ [ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), which `jr2 up` reads off the Machines
40
+ and declares in the pod's manifest beside its Held one. In the pod it is the container named `effector`. Its image is
41
+ the Sandbox Image unless the `workspace()` names `effector: { image }` — a `file:` context or a registry ref, resolved
42
+ like the User Container's — for the tools and the credentials kept out of the Agent's image. Its command is jr2's
43
+ binary on the runtime volume (`/opt/jr2/bin/effector`); it runs as its image's own `USER` (ADR-0037's 1000 fallback
44
+ included), hardened like the Harness. The Agent executes nothing in it: it holds the secrets its Effects name and the
45
+ credentials its pushes spend, which the Agent never reads. With the default image it runs as the Agent's uid, and that
46
+ is the rule, not a gap: the containers share no mount namespace and no `/proc`, so the barrier is the container and
47
+ the read-only mount, not the uid. Inside it, a credentialed command's child — the author's argv, often running the
48
+ Agent's code — shares the Effector's uid, so the Effector confines it with Landlock to the image's system paths, its
49
+ own scratch (its `TMPDIR` included; never `/tmp`) and the secret files it names, and ends every process it left before
50
+ its Effect settles (ADR-0068). No Custodian, no `harness.env`, and no env beyond where its mounts are and its pod's
51
+ name, address and node.
52
+
53
+ The Harness and User containers mount `/work` read-write, **`/repos` read-only** — the node's Repo cache, one bare
54
+ checkout per Repo the Sandbox names (ADR-0004, ADR-0051) — **and `/opt/jr2` read-only**, the runtime volume — so human
55
+ and agent see identical files. Neither `/repos` nor `/opt/jr2` is a second exception; each is half of the first. The
56
+ worktrees are `--shared` clones whose alternates resolve objects from `/repos/<key>`, so a seat holding `/work` alone
57
+ holds checkouts whose every borrowed object is missing — git in the User Container dies on "unable to normalize
58
+ alternate object path". And their `origin` fetches through a program on `/opt/jr2` (ADR-0053), so a seat without that
59
+ volume holds checkouts whose `git fetch` dies. The Custodian mounts none of the three — the pod's credential holder has
60
+ no business in the working tree.
61
+
62
+ What each container mounts of the credentials, stated once:
63
+
64
+ | Container | Mounts |
65
+ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
66
+ | `harness` | `jr2-ca` at `/etc/jr2/ca` (the trust bundles). None of the Custodian's volumes. |
67
+ | `custodian` | its values (the Sandbox token, its pod's `jr2-held-secrets-<pod>`, each user `secretKeyRef` its pod names) at `/etc/jr2/custodian/values`; its leaves (`jr2-held-tls`); its pod's config (its own `<sandbox>-custodian`, and `program.lua` from `jr2-held`); `upstream.crt`. |
68
+ | `user` | the checkouts' three volumes, and nothing else. |
69
+ | `effector` | `/work`, `/repos/<key>` and `/opt/jr2` read-only; at `/run/jr2/secrets` (0400) the secrets its Effects name — its pod's `jr2-effector-<pod>` literals, each user `secretKeyRef` by reference — and at `@git/<name>/` each `git.credentials` entry's push Secret the Sandbox CR names (`gitCredentials`), which the operator projects from that entry's Secret by reference into this container alone (`@` is no character of a secret's name, so no secret's file takes that directory); at `/run/jr2/effector` its key (its own `<sandbox>-effector`) and `effector.json`; an emptyDir at `/run/jr2/scratch` as `$HOME`; at `/etc/jr2/effector-ca`, read-only, the instance's caBundle ConfigMap when the instance ships one (ADR-0020). |
70
+
71
+ One git wall stays the image's own: git's dubious-ownership guard fires in the User Container whenever its uid differs
72
+ from the Harness's (the attach created the trees), and `safe.directory` is honored only from system/global config —
73
+ files jr2 does not own in this seat. Env-form config (`GIT_CONFIG_*` on the sidecar) was considered and rejected: it
74
+ would crack "jr2 puts nothing in it", and it does not even cover the seat's flagship access path — ssh login sessions
75
+ scrub container env — so the image would still owe a line for sshd while carrying jr2's env for exec sessions, two
76
+ mechanisms where one honest one serves. A User Container image whose sessions run git carries its own
77
+ `git config --global --add safe.directory '*'` (or ships it in `/etc/gitconfig`).
78
+
79
+ The worktrees' remotes encode which hop each seat can make: `git fetch origin` asks the node cache, and the cache asks
80
+ the remote with the cluster's credential (ADR-0053 — the hop the pod can make, from any seat, with no credential of its
81
+ own), while origin's **push url** is the real remote in the Binding's own spelling — the attach sets it from the slot
82
+ the Machine bound (ADR-0051), so a Machine that bound over ssh pushes over ssh even when the cache was cloned over
83
+ https, and nothing else plumbs it. A push from the Harness or the User Container therefore succeeds exactly when a
84
+ caller supplies the credential — a human's forwarded agent in the User Container — and never for the Agent, because no
85
+ container it executes in holds one (the credential-visibility story above, unchanged). An UNATTENDED publish is the
86
+ Machine's `push()` Effect (ADR-0067): it runs in the Sandbox Effector, which holds the push credential of the
87
+ `git.credentials` entry the Repo Slot's url falls under — the provision names that entry's Secret on the Sandbox CR and
88
+ the operator projects it (ADR-0051) — and pushes to that url, never to origin's push url from the Agent's config. A
89
+ `git.credentials` entry's credentials never enter the Orchestrator: `jr2 up` writes the push credential — a
90
+ `push: { token }` or `push: { sshKey }`, or the clone credential where the entry has no `push` field — into that entry's
91
+ own push Secret (`jr2-git-push-<hash of match>`), which only the Effector is given, and the clone token into the clone
92
+ Secret `jr2-git-<hash of match>`, which only the cache agent reads (ADR-0051, ADR-0068). The Orchestrator names both and
93
+ holds neither. Opening a pull request is an `http` Effect the author writes; jr2 has no forge client.
94
+
95
+ ## Sharing `/work` across uids
96
+
97
+ jr2 sets `runAsUser` nowhere — each image's own `USER` decides its seat's uid — so the two writing seats may disagree,
98
+ and plain POSIX modes would then make the other seat's files read-only. jr2 closes that with two mechanisms it happens
99
+ to own, unconditionally (both are inert when the uids already match):
100
+
101
+ - **The pod carries `fsGroup` — the work group, default `2000`.** The default is convention; the one override lives on
102
+ the `WorkspaceSpec` (`workGroup?: number`, beside `image` and `user` — pod composition is the spec's business,
103
+ ADR-0037), for the image whose session users already hold a gid of their own. Never a config key. Any value is safe
104
+ for the jr2 seats: Kubernetes grants the fsGroup as a supplemental group to every container process, so the Harness
105
+ writes `/work` whatever the number, and no image's `/etc/group` needs to know it. fsGroup puts a setgid group on
106
+ `/work`'s root, and setgid propagates down, so everything either seat creates is group-_owned_ by the work group.
107
+ - **The attach stamps a default ACL on every repo root it creates** — `u::rwx,g::rwx,o::r-x`, set by `work-acl`, a
108
+ static helper on the runtime volume (part of ADR-0037's published `/opt/jr2` surface, static for the same reason `rg`
109
+ is: it executes on a libc jr2 does not control). A default ACL is inherited by everything created beneath it, and
110
+ POSIX ignores the process umask where one is present — so every file either seat writes under a repo tree lands
111
+ group-writable (664/775), with **zero lines in any image**: no umask in a login shell, no jr2 knowledge in a brought
112
+ User Container. Ownership without writability was the trap — fsGroup alone leaves the other seat group-_read_. On a
113
+ filesystem without POSIX ACL support the helper warns and changes nothing, and sharing degrades to the umask fallback
114
+ below.
115
+
116
+ Group _membership_ is the one thing an image can still owe. `kubectl exec` sessions and a non-root sshd's logins hold
117
+ the work group automatically — both inherit the container's supplemental groups, where Kubernetes put the fsGroup. A
118
+ **root** sshd is the exception: the logins it setuids rebuild their groups from `/etc/group`, so that image puts its
119
+ session user in the work group itself (`groupadd -g 2000 work && usermod -aG work <user>`) — or the spec points
120
+ `workGroup` at a gid its users already hold.
121
+
122
+ **The promise is scoped to repo trees.** An ad-hoc path at `/work`'s top level — either seat's `mkdir /work/scratch` —
123
+ carries no default ACL, so files there fall back to the writer's umask. The Harness runs at `umask 002` (itself and
124
+ every tool child) and the attach script opens with the same line, so jr2's own writes are group-writable even there;
125
+ what a User Container session writes outside a repo tree is its own business. A Workspace with no Repo Slot (ADR-0051)
126
+ has no repo tree: its steps share `/work` itself, every Agent and `command` of it in the one Harness container as one
127
+ uid, so they need no ACL; a User Container's writes there have this paragraph's limit, until a user of that seat needs
128
+ more.
129
+
130
+ ## Constraints on the seat
131
+
132
+ The seat is zero-_contract_, not zero-_physics_. What an image brought here must live with:
133
+
134
+ - **The entrypoint must block.** The pod's restart policy restarts an exited container, so a `CMD` that finishes
135
+ crash-loops. Run a long-lived process — an sshd, an IDE server, `sleep infinity` at minimum.
136
+ - **The ports of jr2's seats are taken** — one network namespace: `:8080` (the Harness), the Custodian's
137
+ `127.0.0.1:8081` (control), `127.0.0.1:15001` (egress) and `:15021` (health), and `:8082` (the Sandbox Effector, in a
138
+ pod that has one). Everything else is the image's. A process that binds `:8082` while the Sandbox Effector is down
139
+ gets signed requests it can neither forge nor answer (ADR-0068): the Effect fails closed.
140
+ - **Root is allowed.** The hardened security context (non-root, all capabilities dropped, no escalation) applies to the
141
+ seats jr2 owns — Harness, Custodian and Sandbox Effector, per container — and deliberately not here: the seat's
142
+ identity is "what jr2 does not own", and hardening it is an opinion. A root sshd that binds `:22` and setuids sessions
143
+ down to its login user — the standard shape for managed access — runs unmodified; a platform that wants this seat
144
+ hardened hardens its own image or its namespace's Pod Security profile. The credential-visibility boundary never
145
+ depended on this seat being unprivileged, only on the Agent executing nothing in it.
146
+
147
+ ## Why separate containers, not one image
148
+
149
+ - **Image ownership** — the Sandbox Image is the user's (ADR-0037), the Custodian is a pinned upstream image, and the
150
+ User Container is whatever its owner wants, contract-free because jr2 puts nothing in it. None bloats or constrains
151
+ the others.
152
+ - **Independent resource limits** — the Workspace's Size is one ceiling for the pod, and a Machine may split it so the
153
+ User Container gets a limit of its own (ADR-0060); then a runaway build there can't starve the agent (and vice-versa).
154
+ Without a split the User Container shares the pod budget.
155
+ - **Lifecycle independence** — a Harness crash restarts only the Harness; an interactive session in the User Container
156
+ survives, and vice-versa.
157
+
158
+ ## What the split is — and is not — a security boundary
159
+
160
+ The **pod** is the isolation unit (one per Workspace, the k8s north-star): its containers share a kernel, network
161
+ namespace, node, and the worktree volume, and are one trust domain in every isolation sense. The one property a
162
+ container edge does provide is **credential visibility** — the asymmetry ADR-0013 builds on: the Agent executes code in
163
+ the Harness container and none in the others, so a secret that exists only in another container's mount namespace is out
164
+ of its reach. That is why the Custodian holds the Orchestrator credential and the model keys, and it is what the User
165
+ Container offers a human session — with its edges stated plainly:
166
+
167
+ - It holds only for what stays in the private filesystem or session. Anything on `/work` or listening on `localhost` is
168
+ shared with the Agent — the pod has one network namespace. The Custodian's listeners are on `localhost` too; what they
169
+ hand out is a key's USE toward its bound host, never the key (ADR-0059).
170
+ - A live forwarded agent socket is usable by anything that executes as that session's user in that namespace — so a
171
+ session holding one should not blindly execute what it finds on `/work` (the worktree is agent-authored; sourcing its
172
+ `direnv`, running its scripts, or trusting its `justfile` volunteers the boundary away).
173
+ - **It is not port isolation.** The User Container's sshd and the Agent's dev server contend for the same ports.
174
+ - **It is not a seat for parallel authorship.** Two writers on one checkout collide as files, whatever the container
175
+ layout; parallel work belongs on a second checkout — fetch the branch out
176
+ (`git fetch "ext::kubectl exec -i <pod> -c harness -- git upload-pack /work/<slot>/default"` needs no credential in
177
+ the pod at all) and push from where the keys already live.
178
+
179
+ ## Considered options
180
+
181
+ - **No third seat; start services by hand** (`kubectl exec -c harness -- sshd`). Works for a person, rejected as the
182
+ mechanism: a service a managed system relies on cannot depend on a human's exec, and it plants the session's
183
+ credentials in the Agent's own mount namespace.
184
+ - **Ephemeral containers as the attach path.** Right for one-off, creds-free debugging (`kubectl debug --target=harness`
185
+ with a plain image), rejected as the standing mechanism: they cannot run from pod birth, accumulate per attach, and
186
+ are spent on exit.
187
+ - **A truly minimal Harness** (agent loop only, tools executed in a sibling container) — would need a cross-container
188
+ execution transport jr2 does not have. Not blocking: the agent toolchain lives in the Sandbox Image; revisit only if
189
+ that ever becomes a real cost (ADR-0037 records the same rejection from the image side).
190
+ - **umask discipline as the sharing mechanism** (fsGroup + `umask 002` in every writer). Rejected as what the promise
191
+ rests on: the Harness's half is one startup line jr2 controls, but the User Container's half is a line every image
192
+ must remember (`[ -d /work ] && umask 002` in a login shell) — a gotcha, and a silent one: forgetting it costs nothing
193
+ until the Agent cannot edit a human's file, and the failure reads as the Agent's. It also cuts the other way — a
194
+ session's deliberate `umask 077` under a repo tree would break the sharing the pod exists for; under a default ACL,
195
+ `/work`'s repo trees are definitionally shared. The umask survives as defence in depth, not contract.
196
+ - **A root init container stamping the ACL on `/work`'s own root** — complete coverage (no repo-tree scoping), owner
197
+ rights with every capability dropped, kit code only, the standard volume-permissions idiom. Rejected: it puts a
198
+ `runAsNonRoot: false` container in _every_ pod, so no jr2 pod could satisfy the Pod Security "restricted" profile —
199
+ today that is forfeited only by a User Container that chooses root, and that choice belongs to the user, not to the
200
+ composition. The attach already owns the only place repo roots are born, so the narrower stamp costs one script line.
201
+ - **Unifying uids across seats** (document "make both images run the same `USER`"). Rejected: it makes the uid a
202
+ cross-image contract — every pairing of Sandbox Image and User Container must agree forever, which is exactly the
203
+ coupling the zero-contract seat exists to remove.
204
+
205
+ ## Consequences
206
+
207
+ - The generic `Sandbox` CRD composes all of this as plain container specs
208
+ ([ADR-0001](0001-operator-owned-generic-sandbox.md) holds — the operator stays agent-agnostic); the Orchestrator's
209
+ `kubectlSandbox` supplies the images. The pod runs two containers, plus `user` when the spec names one and `effector`
210
+ when an Effect is placed there.
211
+ - Every container but the Sandbox Effector carries a limit from the Workspace's Size (ADR-0060): the Custodian a fixed
212
+ kit slice, the User Container its split if stated, the Harness the rest. The Sandbox Effector carries no limit of its
213
+ own: like a User Container with no split, it shares the pod's Size ceiling — the whole Size, as pod-level resources —
214
+ with the containers beside it, and what share of the Size it gets is not decided. The Harness container alone mounts a
215
+ memory-backed `/dev/shm` (`sizeLimit` 25% of its share); the User Container keeps the pod's default.
216
+ - jr2 does not gate readiness on the User Container and never restarts the pod for it; it lives and dies by the pod's
217
+ own policy.
218
+ - **Operator**: `runAsNonRoot` moves from the pod level to the jr2-owned containers — the Harness, the Custodian and the
219
+ Sandbox Effector (the Custodian states its own context: non-root, read-only root filesystem, no capabilities); the
220
+ hardened-by-default rule for sidecar specs exempts the `user` container; the pod gains `fsGroup`
221
+ (`spec.workGroup ?? 2000`). **Orchestrator**: the attach script stamps the default ACL on each repo root _before_ the
222
+ clone that fills it — inheritance happens at creation, never retroactively. **Harness image**: vendors the static
223
+ `work-acl` into `/opt/jr2/bin` (ADR-0037's surface). **Harness**: sets `umask 002` at startup. `shareProcessNamespace`
224
+ stays off — ever — because the credential-visibility boundary depends on it.
225
+ - The Harness container's `/work` writes being group-writable widens nothing: every process that could abuse group
226
+ access already runs in the same trust domain, and the work group exists only inside this pod.
227
+ - **Routable access is a follow-up, deliberately untaken**: the operator's Service serves the Harness at `:8080`, and a
228
+ managed system wanting cluster-routed ssh to the User Container needs a declared port there. `kubectl port-forward`
229
+ reaches any listener in the pod without one, which covers the human case day one.
230
+ - **GLOSSARY.md**: **User Container** is a glossary term.