@jr2/orchestrator 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) 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 +478 -0
  10. package/docs/concepts/architecture.md +243 -0
  11. package/docs/concepts/charts.md +146 -0
  12. package/docs/concepts/composition.md +357 -0
  13. package/docs/concepts/converge.md +257 -0
  14. package/docs/concepts/durable-runs.md +189 -0
  15. package/docs/concepts/effects.md +658 -0
  16. package/docs/concepts/gates.md +208 -0
  17. package/docs/concepts/machines-and-workflows.md +298 -0
  18. package/docs/concepts/pools-and-sources.md +230 -0
  19. package/docs/concepts/secrets-and-trust.md +306 -0
  20. package/docs/concepts/the-menu.md +381 -0
  21. package/docs/concepts/workspaces.md +756 -0
  22. package/docs/get-started/a-model-decides.md +563 -0
  23. package/docs/get-started/an-agent-in-a-sandbox.md +383 -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 +993 -0
  27. package/docs/get-started/install.md +130 -0
  28. package/docs/guides/add-tools-to-the-sandbox.md +308 -0
  29. package/docs/guides/answer-a-gate.md +304 -0
  30. package/docs/guides/bind-open-parts.md +224 -0
  31. package/docs/guides/cancel-a-run.md +146 -0
  32. package/docs/guides/clean-up.md +206 -0
  33. package/docs/guides/cut-a-worktree.md +560 -0
  34. package/docs/guides/fix-a-failed-converge.md +361 -0
  35. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  36. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  37. package/docs/guides/let-an-agent-write-a-chart.md +230 -0
  38. package/docs/guides/look-inside-a-sandbox.md +177 -0
  39. package/docs/guides/mirror-the-kit-images.md +176 -0
  40. package/docs/guides/move-from-kind-to-a-shared-cluster.md +318 -0
  41. package/docs/guides/open-the-console.md +91 -0
  42. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  43. package/docs/guides/push-and-sign-a-branch.md +532 -0
  44. package/docs/guides/run-a-chart-by-name.md +270 -0
  45. package/docs/guides/run-a-command-or-call-a-url.md +587 -0
  46. package/docs/guides/run-one-worker-per-item.md +322 -0
  47. package/docs/guides/set-an-agents-access-and-dials.md +420 -0
  48. package/docs/guides/size-a-workspace.md +217 -0
  49. package/docs/guides/start-a-run-from-a-run.md +237 -0
  50. package/docs/guides/start-a-run.md +207 -0
  51. package/docs/guides/upgrade-the-kit-version.md +240 -0
  52. package/docs/guides/use-a-hosted-provider.md +214 -0
  53. package/docs/guides/use-a-local-model.md +313 -0
  54. package/docs/guides/watch-a-run.md +349 -0
  55. package/docs/guides/write-a-workflow.md +318 -0
  56. package/docs/index.md +44 -0
  57. package/docs/llms.txt +237 -0
  58. package/docs/reference/authoring-surface/agent.md +151 -0
  59. package/docs/reference/authoring-surface/answer.md +156 -0
  60. package/docs/reference/authoring-surface/arg.md +82 -0
  61. package/docs/reference/authoring-surface/basic.md +62 -0
  62. package/docs/reference/authoring-surface/bearer.md +53 -0
  63. package/docs/reference/authoring-surface/branchname.md +15 -0
  64. package/docs/reference/authoring-surface/chartschema.md +23 -0
  65. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  66. package/docs/reference/authoring-surface/command.md +206 -0
  67. package/docs/reference/authoring-surface/customize.md +64 -0
  68. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  69. package/docs/reference/authoring-surface/defineevent.md +114 -0
  70. package/docs/reference/authoring-surface/dispatch.md +126 -0
  71. package/docs/reference/authoring-surface/fail.md +96 -0
  72. package/docs/reference/authoring-surface/file.md +142 -0
  73. package/docs/reference/authoring-surface/frompick.md +277 -0
  74. package/docs/reference/authoring-surface/http.md +259 -0
  75. package/docs/reference/authoring-surface/isfailure.md +58 -0
  76. package/docs/reference/authoring-surface/isready.md +14 -0
  77. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  78. package/docs/reference/authoring-surface/open.md +105 -0
  79. package/docs/reference/authoring-surface/openbody.md +86 -0
  80. package/docs/reference/authoring-surface/paramsof.md +143 -0
  81. package/docs/reference/authoring-surface/pool.md +186 -0
  82. package/docs/reference/authoring-surface/push.md +130 -0
  83. package/docs/reference/authoring-surface/readyof.md +21 -0
  84. package/docs/reference/authoring-surface/secret.md +139 -0
  85. package/docs/reference/authoring-surface/sentby.md +83 -0
  86. package/docs/reference/authoring-surface/source.md +51 -0
  87. package/docs/reference/authoring-surface/text.md +62 -0
  88. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  89. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  90. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  91. package/docs/reference/authoring-surface/type-agentslots.md +15 -0
  92. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  93. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  94. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  95. package/docs/reference/authoring-surface/type-arg.md +19 -0
  96. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  97. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  98. package/docs/reference/authoring-surface/type-binding.md +61 -0
  99. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  100. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  101. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  102. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  103. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  104. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  105. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  106. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  107. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  108. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  109. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  110. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  111. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  112. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  113. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  114. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  115. package/docs/reference/authoring-surface/type-childat.md +12 -0
  116. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  117. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  118. package/docs/reference/authoring-surface/type-commandinput.md +20 -0
  119. package/docs/reference/authoring-surface/type-commandoptions.md +56 -0
  120. package/docs/reference/authoring-surface/type-commandoutput.md +122 -0
  121. package/docs/reference/authoring-surface/type-customize.md +111 -0
  122. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  123. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  124. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  125. package/docs/reference/authoring-surface/type-dispatchoutput.md +72 -0
  126. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  127. package/docs/reference/authoring-surface/type-duration.md +15 -0
  128. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  129. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  130. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  131. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  132. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  133. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  134. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  135. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  136. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  137. package/docs/reference/authoring-surface/type-failure.md +19 -0
  138. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  139. package/docs/reference/authoring-surface/type-filebody.md +37 -0
  140. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  141. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  142. package/docs/reference/authoring-surface/type-hostinjectedinput.md +56 -0
  143. package/docs/reference/authoring-surface/type-httpdefinition.md +104 -0
  144. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  145. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  146. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  147. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  148. package/docs/reference/authoring-surface/type-httpoutput.md +95 -0
  149. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  150. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  151. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  152. package/docs/reference/authoring-surface/type-lends.md +38 -0
  153. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  154. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  155. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  156. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  157. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  158. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +27 -0
  159. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  160. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  161. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  162. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  163. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  164. package/docs/reference/authoring-surface/type-opencommandinput.md +22 -0
  165. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  166. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  167. package/docs/reference/authoring-surface/type-openof.md +12 -0
  168. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  169. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  170. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +26 -0
  171. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +27 -0
  172. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  173. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  174. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  175. package/docs/reference/authoring-surface/type-pushinput.md +34 -0
  176. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  177. package/docs/reference/authoring-surface/type-pushoutput.md +149 -0
  178. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  179. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  180. package/docs/reference/authoring-surface/type-reached.md +12 -0
  181. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  182. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  183. package/docs/reference/authoring-surface/type-sandboxoptions.md +84 -0
  184. package/docs/reference/authoring-surface/type-secret.md +25 -0
  185. package/docs/reference/authoring-surface/type-sender.md +19 -0
  186. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  187. package/docs/reference/authoring-surface/type-size.md +85 -0
  188. package/docs/reference/authoring-surface/type-slothandles.md +24 -0
  189. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  190. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  191. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  192. package/docs/reference/authoring-surface/type-text.md +19 -0
  193. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  194. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  195. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  196. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  197. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  198. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  199. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  200. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  201. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  202. package/docs/reference/authoring-surface/type-workspaced.md +115 -0
  203. package/docs/reference/authoring-surface/type-workspacehandles.md +22 -0
  204. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  205. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  206. package/docs/reference/authoring-surface/type-workspaceoptions.md +27 -0
  207. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  208. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  209. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  210. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  211. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  212. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  213. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  214. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  215. package/docs/reference/authoring-surface/workspace.md +287 -0
  216. package/docs/reference/authoring-surface/worktree.md +71 -0
  217. package/docs/reference/authoring-surface.md +217 -0
  218. package/docs/reference/cli.md +433 -0
  219. package/docs/reference/config.md +765 -0
  220. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +114 -0
  221. package/docs/reference/decisions/0002-duplex-streaming-actor.md +117 -0
  222. package/docs/reference/decisions/0004-worktree-provisioning-model.md +130 -0
  223. package/docs/reference/decisions/0005-sandbox-pod-composition.md +240 -0
  224. package/docs/reference/decisions/0006-agent-control-surface.md +50 -0
  225. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  226. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  227. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  228. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  229. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  230. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  231. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +175 -0
  232. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  233. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +271 -0
  234. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +126 -0
  235. package/docs/reference/decisions/0017-pool-over-a-source-port.md +71 -0
  236. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  237. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +287 -0
  238. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  239. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +101 -0
  240. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  241. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +175 -0
  242. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +436 -0
  243. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  244. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  245. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +245 -0
  246. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +90 -0
  247. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  248. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  249. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +193 -0
  250. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  251. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +126 -0
  252. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  253. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  254. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  255. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  256. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  257. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  258. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  259. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  260. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  261. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  262. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  263. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  264. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  265. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  266. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  267. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  268. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +238 -0
  269. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +86 -0
  270. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +123 -0
  271. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  272. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  273. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  274. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  275. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +108 -0
  276. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +427 -0
  277. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  278. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  279. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +239 -0
  280. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +82 -0
  281. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  282. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  283. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +552 -0
  284. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +458 -0
  285. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +475 -0
  286. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  287. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +334 -0
  288. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  289. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  290. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  291. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  292. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
  293. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
  294. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  295. package/docs/reference/decisions.md +86 -0
  296. package/docs/reference/glossary.md +687 -0
  297. package/docs/reference/http-api.md +1386 -0
  298. package/docs/reference/machines.md +189 -0
  299. package/docs/reference/mechanism-events.md +72 -0
  300. package/docs/why.md +207 -0
  301. package/package.json +5 -3
  302. package/src/actor.ts +694 -106
  303. package/src/agent.ts +114 -38
  304. package/src/ambient.ts +3 -4
  305. package/src/assembly-checks.ts +189 -7
  306. package/src/cause.ts +234 -0
  307. package/src/chart.ts +345 -46
  308. package/src/charts.ts +43 -8
  309. package/src/config.ts +440 -178
  310. package/src/custodian.lua +17 -5
  311. package/src/custodian.ts +325 -99
  312. package/src/customize.ts +199 -49
  313. package/src/dispatch-effect.ts +945 -0
  314. package/src/door-compat.ts +217 -0
  315. package/src/durability.ts +11 -4
  316. package/src/effect-checks.ts +50 -3
  317. package/src/effect.ts +641 -128
  318. package/src/effector-wire.ts +165 -13
  319. package/src/fail.ts +222 -0
  320. package/src/harness-client.ts +587 -50
  321. package/src/harness-env.ts +33 -0
  322. package/src/held-secrets.ts +125 -16
  323. package/src/holds.ts +3 -0
  324. package/src/http-effect.ts +556 -58
  325. package/src/http.ts +480 -30
  326. package/src/index.ts +74 -82
  327. package/src/instance.ts +68 -8
  328. package/src/internal.ts +85 -0
  329. package/src/kube-client.ts +6 -3
  330. package/src/machine-doc.ts +24 -7
  331. package/src/mechanism-routes.ts +168 -0
  332. package/src/model-keys.generated.ts +53 -0
  333. package/src/model-keys.ts +42 -58
  334. package/src/names.ts +19 -6
  335. package/src/notices.ts +483 -41
  336. package/src/open-body.ts +242 -40
  337. package/src/open-effect.ts +81 -8
  338. package/src/open.ts +90 -6
  339. package/src/parts.ts +213 -30
  340. package/src/party.ts +101 -2
  341. package/src/pick.ts +497 -11
  342. package/src/pool.ts +282 -50
  343. package/src/registration.ts +133 -12
  344. package/src/repo-identity.ts +7 -3
  345. package/src/repos.ts +22 -3
  346. package/src/run-host.ts +665 -45
  347. package/src/run-output.ts +38 -0
  348. package/src/sandbox-effect.ts +457 -71
  349. package/src/sandbox-kube.ts +66 -17
  350. package/src/secret-placement.ts +59 -17
  351. package/src/server.ts +3 -0
  352. package/src/setup.ts +325 -62
  353. package/src/size.ts +121 -9
  354. package/src/snapshot-store.ts +54 -10
  355. package/src/stub-harness.ts +44 -16
  356. package/src/timers.ts +14 -1
  357. package/src/tokens.ts +107 -32
  358. package/src/vocabulary.ts +88 -14
  359. package/src/wire.ts +100 -26
  360. package/src/workspace.ts +771 -238
  361. package/src/worktree.ts +249 -0
@@ -0,0 +1,427 @@
1
+ # A Harness holds stand-ins; the Custodian holds the keys
2
+
3
+ [ADR-0058](0058-a-harness-answers-the-orchestrator-alone.md) recorded an open gap: model API keys were in the Harness
4
+ container. `JR2_PROVIDER_API_KEY` and literal `harness.env` values (for example `ANTHROPIC_API_KEY`) came from the
5
+ `jr2-harness-env` Secret by `envFrom`. Working tools give the Agent code execution in that container, and every tool
6
+ child inherits the env. So the Agent could read a key and send it out of the pod. Dropping pi's catalog was not an
7
+ option: the user's real setup is Bedrock through a LiteLLM gateway, with a fixed `ANTHROPIC_API_KEY` and `anthropic/…`
8
+ models, and the catalog holds the limits, costs and thinking levels of those models.
9
+
10
+ The same problem had one solution already, for one credential. The Adapter held the Sandbox token in a container the
11
+ Agent executes nothing in ([ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md)). This
12
+ decision generalizes that container. It holds every credential the Harness in its pod uses, and it replaces the Adapter.
13
+
14
+ ## Decision
15
+
16
+ - **A held secret is a credential the Harness uses and never holds.** The Harness process holds its **Stand-in**. It
17
+ means something only to the Custodian, and only toward a host the secret is bound to. It is unique per name, it is a
18
+ valid header and env value, and it never contains `sk-ant-oat` (pi reads such a key as an Anthropic OAuth token). The
19
+ word is not "placeholder", because that is the word of an Open part.
20
+ - **Every Stand-in is secret, except the fetch Stand-in.** The pod's maker mints each Stand-in for its pod with the
21
+ signing key, and no process can guess it: the Orchestrator for a Sandbox, and `jr2 up` for the Instance Harness, as it
22
+ writes that pod's other Secrets (the Instance Effector's key too), and for a check pod
23
+ ([ADR-0019](0019-one-converging-command-against-the-current-context.md)). Only the Harness process holds the
24
+ Stand-ins. It holds each one, the control Stand-in included, as a file under `/run/jr2/` that no process it confines
25
+ can read, and never in the Harness container's env. It gives a model key's Stand-in to the engine in-process. A
26
+ process that the Harness confines gets in its env only the Stand-ins of the secrets that its `workspace()`'s `secrets`
27
+ list grants ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)). So the
28
+ Stand-in of a model key is in reach of the Harness process alone, unless the author grants that key. Secrecy matters
29
+ because confinement makes it hold: a Stand-in that Workspace code can guess lets that code spend the secret. A call
30
+ that code in `/work` makes to the model directly is outside the Turn's Settlement, so it escapes the usage that jr2
31
+ records ([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)) and each
32
+ ceiling on spend. A project whose own code needs a model gets a key that the author grants on purpose. The fetch
33
+ Stand-in stays public (below): it can only ask for a fetch, for its own Sandbox.
34
+ - **The Custodian holds them.** It is a container in every Harness pod — every Sandbox and the Instance Harness — beside
35
+ the Harness, and it holds the held secrets its own pod names (below). The Agent executes no code in it, and
36
+ `shareProcessNamespace` stays off ([ADR-0005](0005-sandbox-pod-composition.md)). So the Agent cannot read its env, its
37
+ files or its `/proc`. Its engine is Envoy: the upstream distroless image, pinned by digest (`CUSTODIAN_IMAGE` in
38
+ `@jr2/orchestrator`). The pod's maker writes Envoy's whole configuration — the Orchestrator a Sandbox's at provision,
39
+ `jr2 up` the Instance Harness's — from the manifest `jr2 up` declares (see "Objects, per pod"). There is no admin
40
+ listener and no xDS, so nothing in the pod can change the configuration or read it back.
41
+ - **The Custodian replaces the Adapter.** The `@jr2/adapter` package and the `jr2-adapter` image are deleted. A
42
+ Sandbox's two tokens are held secrets of the kit's own (ADR-0013): the Custodian holds the control token and the fetch
43
+ token as files from the `<sandbox>-token` Secret (the Instance Harness: its control token, from
44
+ `jr2-instance-harness-token`, which `jr2 up` mints as that pod's maker; it serves no fetch). No other container mounts
45
+ the tokens, and the real tokens never leave the Custodian. The same Secret holds the pod's Stand-ins, which the
46
+ Harness container mounts as files; so it is never `jr2-instance`, which holds the Instance token and the signing key.
47
+ - The **control Stand-in** is secret, as every Stand-in but one. The Harness process alone holds it, as a file under
48
+ `/run/jr2/` that no process it confines can read, and no `secrets` list can grant it: it is in no child's env
49
+ (ADR-0076). The Custodian puts the control token in its place on the agent routes. There is one control Stand-in for
50
+ each Sandbox, whatever the count of its Agents.
51
+ - The **fetch Stand-in** is public, `jr2-held-JR2_SANDBOX_TOKEN`. `jr2-upload-pack` holds it as a constant. The
52
+ Custodian puts the fetch token in its place on the fetch route alone, and refuses it on every agent route.
53
+ - **The Harness reads its Menu itself.** There is no MCP. For each Submission, `menu.ts` reads
54
+ `GET /agents/<iid>/surface` and delivers a pick with `POST /agents/<iid>/events`, both to the Custodian's control
55
+ socket, `/run/jr2/custodian/control.sock`, with the control Stand-in as the bearer and the slot it states for the
56
+ conversation (ADR-0076). `menu-tools.ts` gives pi each accepted event as a tool. The tool names stay
57
+ `mcp__jr2__<event>`, because shipped instructions and the printer depend on them; the name is no longer a transport.
58
+ `menu-tools.ts` adds the prefix, and `menu.ts` keeps the bare name, because an MCP client adds `mcp__<server>__`
59
+ itself. The Harness keeps what the Adapter did: a turn that is over is never prompted
60
+ ([ADR-0026](0026-a-turn-that-is-over-is-never-prompted.md)), a receipt is prose
61
+ ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md),
62
+ [ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)), the surface read retries
63
+ on the ladder with the `jr2.routability` line
64
+ ([ADR-0042](0042-ready-is-not-routable-an-admission-retries-its-connection.md)), and a `poll` event refuses the turn
65
+ (`deferred` is retired, ADR-0006). The event's own JSON Schema is the tool's parameters, so the model sees the
66
+ contract the Orchestrator validates against.
67
+ - **The control listener forwards five routes and no other.** Plain HTTP, to the Orchestrator's Service
68
+ (`jr2-orchestrator.<ns>.svc:4000`, written into the pod's bootstrap by whoever makes the pod — see "Objects, per
69
+ pod"):
70
+
71
+ ```
72
+ GET /agents/<iid>/surface → the same path the Menu
73
+ POST /agents/<iid>/events → the same path a pick
74
+ GET /agents/<iid>/deliveries/<id> → the same path an outstanding pick's answer (ADR-0024)
75
+ POST /agents/<iid>/notices → the same path a step boundary's notices heard (ADR-0062, ADR-0076)
76
+ POST /fetch → /sandboxes/<this pod's Sandbox>/fetch the ask (ADR-0053)
77
+ anything else → 404, from the Custodian
78
+ ```
79
+
80
+ The four agent routes are on a Unix socket, `/run/jr2/custodian/control.sock`, in a folder that the Harness and the
81
+ Custodian share and that no confined program can write (ADR-0076). The fetch is on `127.0.0.1:8081`, because
82
+ `jr2-upload-pack` runs in the domain, and the fetch Stand-in is public. Each listener answers 404 to the other's
83
+ routes. Landlock does not limit a connect to a named socket, so a confined program can connect to the control socket.
84
+ It holds no control Stand-in, so the Custodian refuses each agent route that it asks for there, with the fetch
85
+ Stand-in or with none, and the control socket answers 404 to the fetch route.
86
+
87
+ A read — the surface, a delivery's poll — and an acknowledgment of notices, which marks them heard however often it
88
+ lands, are retried on any reset; a pick and the ask only on a reset before they were sent, so one the Orchestrator may
89
+ have read is not sent twice. A poll holds up to about 25 s, far below the listener's 3600 s idle limit, so an
90
+ outstanding pick never rides one long request (ADR-0002).
91
+
92
+ On each forwarded request, the Custodian swaps `Authorization: Bearer <Stand-in>` for the token of that route — the
93
+ control token on an agent route, the fetch token on the fetch route — and removes every other credential header. A
94
+ request without the Stand-in of its route gets 403. The ask's path comes from the Custodian (`JR2_SANDBOX` in its env,
95
+ set per pod); on the Instance Harness there is none, and the ask gets 404. The route allowlist is the second layer.
96
+ The control stays in the Orchestrator: the token's scope, the live registration, and the event's name and payload are
97
+ judged there, as before (ADR-0013). The Orchestrator also accepts each token on its own routes only, and checks the
98
+ slot that the Harness states against the registration's (ADR-0076).
99
+
100
+ - **Model keys go the same way, over HTTPS.** When its pod holds a held secret, the Harness container also gets:
101
+
102
+ ```sh
103
+ HTTPS_PROXY / https_proxy=http://127.0.0.1:15001
104
+ NO_PROXY / no_proxy=localhost,127.0.0.1,::1
105
+ NODE_USE_ENV_PROXY=1
106
+ NODE_EXTRA_CA_CERTS=/etc/jr2/ca/extra.crt
107
+ SSL_CERT_FILE / REQUESTS_CA_BUNDLE / GIT_SSL_CAINFO=/etc/jr2/ca/bundle.crt
108
+ ```
109
+
110
+ These ride `env`, after `harness.env` and before the wire's digest. This is the Harness container's env, and it holds
111
+ no Stand-in: the Stand-ins are files (above). A process that the Harness confines gets an explicit env instead, which
112
+ holds only the Stand-ins that its `workspace()` grants (ADR-0076). There is no `HTTP_PROXY`: the Custodian carries TLS
113
+ only, and plain HTTP (the Custodian's control listener, an in-cluster seed repository) goes direct. `NO_PROXY` is
114
+ loopback only, so an in-cluster gateway that a secret is bound to still goes through the Custodian.
115
+
116
+ - **The egress listener: CONNECT only.** `127.0.0.1:15001`. The CONNECT target decides everything (lowercased, without a
117
+ trailing dot) — never the SNI, never the `Host` header. A target that a held secret is bound to goes to an internal
118
+ listener for that target alone. That listener ends TLS with a leaf `jr2 up` issued (ALPN `http/1.1` only) and, per
119
+ request:
120
+ - The method must be GET, HEAD, POST, PUT, PATCH, DELETE or OPTIONS, else 405. A server that answers TRACE echoes the
121
+ request, the value among it, back to the Agent. A header name with `_` gives 400: `x_api_key` is not `x-api-key` to
122
+ the strip, but some servers read it as one.
123
+ - `Host` must name the target, else 421. An encoded `/`, `.` or `\` in the path gives 400. A path outside the secret's
124
+ `paths` gives 403. It is checked after Envoy removes dot segments and reads `\` as `/` and `..;` as `..`, and the
125
+ upstream gets that path. So IIS and Tomcat cannot read a different path. A credential header sent twice gives 400.
126
+ - The swap: in each header the secret names, the Stand-in alone, or one auth scheme and the Stand-in
127
+ (`Bearer <Stand-in>`), becomes the value. Nothing else is rewritten.
128
+ - Every credential header that is not the product of a swap is removed. A request with no swap gets 403 and never
129
+ leaves the pod: `jr2 custodian: <target> needs the stand-in of <NAME> in <headers>`.
130
+ - Envoy opens its own TLS to the target, verifies the chain against `upstream.crt` and the name against the target's
131
+ SAN (DNS or IP), and adds no header either way (`server`, `x-envoy-*`, `x-request-id` and `x-forwarded-*` are off).
132
+ A failure gives 502 and a body that names the target and the reason. Redirects pass back unchanged.
133
+ - When the target gives no answer, the Custodian answers 502 with a body that names the target, says what jr2 knows
134
+ about the request, and ends with Envoy's reason. It replaces Envoy's own answer for ten Envoy response flags, in two
135
+ mappers. One reply can carry several flags, and Envoy uses the first mapper that matches, in order. The first mapper
136
+ is for `UF`, `UH` and `DF`, so a reply with one of them gets the first text whatever else it carries:
137
+ - `UF` (the connection to the target failed), `UH` (no healthy host) and `DF` (the DNS lookup failed) say that Envoy
138
+ did not send the request. The body is `jr2 custodian: <target> could not be reached: <details> <reason>`.
139
+ - `UC` (the target closed the connection), `DPE` (a downstream protocol error, which a body framing error can cause
140
+ after the headers went), `UPE` (an upstream protocol error), `UR` (the target reset the stream) and `LR` (Envoy
141
+ reset it) can come after the request went to the target. The body is
142
+ `jr2 custodian: <target> did not answer; the request may have reached it: <details> <reason>`. `UMSDR` (the
143
+ maximum stream duration) is in this mapper: it ends a stream that was sent. `URX` (the retry limit) is in it too:
144
+ Envoy sets it with the last attempt's own flag, so `UF,URX` gets the first text and `UC,URX` the second.
145
+ - Envoy does not set `URX` or `UMSDR` on these routes, because they set no retry policy and no
146
+ `max_stream_duration`. The two flags are in the mappers only to keep their union equal to the Orchestrator route's
147
+ list.
148
+
149
+ The second text is there because the first would be false after those flags. The target may have the request, so a
150
+ `curl -X POST` or a `gh api` call to a bound host can write twice when the Agent sends it again. "Could not be
151
+ reached" would tell the Agent that the first call went nowhere. The status stays 502 for both, the status of a
152
+ gateway whose target gave no valid answer. No code reads the words. The Harness's provider adapter reads the status,
153
+ and no model reads its own provider's 502. The Agent's own tools show the body to the model.
154
+
155
+ Without the mappers, Envoy answers each of the ten flags itself: 503 for most of them, and 502 for `UPE`. That body
156
+ names no target, so a caller can read it as the target's own answer. With them, every caller sees that the Custodian
157
+ gave the answer, and for which target. A reset after the target's answer started is not replaced: the caller sees
158
+ its stream end.
159
+
160
+ The tunnel (below) keeps one mapper for all ten flags, with the first text. Envoy answers a `CONNECT` with 200 only
161
+ after its TCP connection to the target is up, and no byte of the Agent's request goes before that 200. So a 502 on
162
+ the tunnel always comes before the request reached the target, and "could not be reached" is true for each flag.
163
+
164
+ The Orchestrator route is different ([ADR-0002](0002-duplex-streaming-actor.md)). Its mapper adds the header
165
+ `x-jr2-custodian: unreachable` and the flags, and the Harness reads them to decide whether a pick may be sent again
166
+ (`menu.ts`). It says "never reached" only when each flag of the reply is `UF`, `UH`, `DF` or `URX`, and "not known"
167
+ when any other flag is there.
168
+
169
+ - Bodies stream both ways. A route has no total timeout; an idle stream has an hour; a client that goes away resets
170
+ the upstream request.
171
+
172
+ Every other target is tunneled, byte for byte, after the dial guard (below).
173
+
174
+ - **The engine choices.** The values and every credential decision are in a Lua filter. Its program is fixed
175
+ (`program.lua`); each pod's own `custodian.lua` is a generated `HELD` table — names, headers, paths and targets, never
176
+ a value — that ends with `dofile` of the program, so the program runs after the table it reads. At start the program
177
+ reads each value from `/etc/jr2/custodian/values/<NAME>`, takes off one trailing `\n` or `\r\n`, and stops the
178
+ start-up with a line that names the secret when a value is missing, empty, or has a byte outside `0x20`–`0x7E`. Each
179
+ Stand-in but the fetch Stand-in is a value too: the Custodian reads it from its values, never from the table. Envoy's
180
+ `credential_injector` is not used: it is alpha, it breaks on a secret file that ends in a newline (1.39.1), and it
181
+ replaces a whole header, where the swap must see the Stand-in. Values never enter a ConfigMap. Envoy runs one worker,
182
+ with hot restart off, and flushes its access log twice a second.
183
+ - **The log is a contract.** One line per decision on stdout, with the prefix `jr2.custodian` and `key=value` pairs:
184
+ `ready secrets=… hosts=N` (at the first probe), `action=intercept target=… secret=… method=… status=…`,
185
+ `action=tunnel target=…`, `action=refuse target=… reason=…`. Never a header value, a body, a path, a query, a value or
186
+ a Stand-in.
187
+ - **The dial guard, in two halves.** A tunnel never reaches a loopback, link-local (the cloud metadata address included,
188
+ and AWS's `fd00:ec2::254`) or unspecified address, or an IPv4-mapped form of one; it gets 403 and a `reason=guard`
189
+ line. The script refuses a target that SPELLS one, and `localhost` or a `.localhost` name, before anything is
190
+ resolved. The dynamic forward proxy filter saves the address it resolved a name to (`save_upstream_address`), and an
191
+ RBAC filter after it refuses that address when it is in `GUARDED_RANGES` — so a name that resolves to one is refused
192
+ like the literal. The address judged is the address dialed: both come from the one DNS cache entry.
193
+ - **Health.** `0.0.0.0:15021`, `GET /healthz`. The kubelet cannot reach a loopback listener, and a distroless image has
194
+ no shell for an exec probe. The Custodian's readiness probe is on it. Pod Ready needs every container ready, and the
195
+ operator holds a Sandbox's Ready on pod Ready, so no Turn is admitted before the Custodian serves. The Instance
196
+ Harness's rollout wait covers it the same way. It is the one listener a peer can reach, where the ADR-0058
197
+ NetworkPolicy is not enforced. So it is outside Envoy's global connection limit, holds 16 connections at most, and
198
+ closes a connection idle for 5 seconds: a peer's idle connections cannot stop the Menu or the egress.
199
+ - **The config surface** (`jr2.config.ts`,
200
+ [ADR-0018](0018-instance-agents-are-definitions-jr2-assembles-the-harness.md)): the top-level `secrets` list, whose
201
+ entry is
202
+ `{ name; value | valueFrom.secretKeyRef; hosts?; headers?, default `authorization`, `x-api-key`, `x-goog-api-key`, `api-key`; paths? }`,
203
+ and `harness.catalog` (a catalog provider id → `{ baseUrl }`). The list serves both sides
204
+ ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)). `harness.provider.apiKey` is an entry
205
+ too, implicitly: `JR2_PROVIDER_API_KEY`, bound to its `baseUrl`'s host. `loadConfig` checks the shape in both worlds.
206
+ - **Naming is placement: a pod gets the held secrets something in it names.** Placement is read, never declared
207
+ (`secretPlacementsOf`), and an entry's `hosts`, `headers` and `paths` are its binding — where the Custodian may put
208
+ the value — never its placement:
209
+
210
+ | Pod | The held secrets its Custodian holds |
211
+ | --------------------- | ------------------------------------------------------------------------------------------ |
212
+ | A Workspace's Sandbox | the names its `workspace()` lists under `secrets: [...]`, and the model keys of its Agents |
213
+ | The Instance Harness | the model keys of the `"none"` Agents it hosts, and nothing else |
214
+
215
+ A model key is claimed by the PROVIDER: an Agent names its model, and jr2 knows which keys that provider reads
216
+ (`MODEL_KEYS`; the custom provider's id for `JR2_PROVIDER_API_KEY`). An Agent's Turns run in the Workspace below which
217
+ it is declared, down to the next `workspace()`, unless it is `"none"`, whose Turns run at the Instance Harness
218
+ ([ADR-0031](0031-menu-only-agents-run-on-the-instance-harness.md)). A `"none"` Agent cannot execute code, so the
219
+ Instance Harness loses nothing by holding only model keys. Listing a model key under a `workspace()`'s `secrets` is
220
+ allowed, and `jr2 up` reports it as redundant. A name a `workspace()` lists that the list does not hold is refused,
221
+ with the line to add. An entry nothing names — no `workspace()`, no provider, no Effect — goes nowhere, and `jr2 up`
222
+ warns. `jr2 up` reports per pod:
223
+ `Workspace of workflow "coding": Custodian holds ANTHROPIC_API_KEY (provider), JIRA_TOKEN`.
224
+
225
+ `jr2 up` alone checks the claimed entries (`heldSecretsOf`), host-side, because `.env` values are absent in-cluster.
226
+ An entry no pod claims is not checked as a held secret:
227
+
228
+ | # | Refused |
229
+ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
230
+ | R1 | A known model key (`MODEL_KEYS`) in `harness.env`, or as a key of a Secret or ConfigMap `harness.envFrom` names. |
231
+ | R2 | A held-secret name that is `JR2_*` or a proxy or trust variable. |
232
+ | R3 | One name held twice, or held and set by `harness.env`. |
233
+ | R4 | Not exactly one of `value`/`valueFrom`; an empty value; a byte outside `0x20`–`0x7E`; leading or trailing space. |
234
+ | R5 | A claimed entry with no `hosts` where no default exists; a wildcard, userinfo, a non-`https` scheme, a bare-host path, a bad port; two secrets on one host in one header. |
235
+ | R6 | A `headers` entry that is not a lowercase token, or that frames or routes the request; a duplicate. |
236
+ | R7 | `provider.apiKey` with a non-`https` `baseUrl`; a non-`https` catalog `baseUrl`. |
237
+ | R8 | A catalog key that is not a known provider, or that is the custom provider's id. |
238
+ | R9 | Explicit `hosts` on a known model key that leave out where a carried Agent's provider sends its calls. |
239
+ | R10 | A `secretKeyRef` whose Secret or key does not exist. |
240
+ | R11 | While a secret is held: `harness.env` sets a proxy or trust variable. |
241
+ | R12 | A `paths` entry that does not start with `/`, or holds `..`, `;`, `\`, `%2e`, `%2f` or `%5c`. |
242
+ | R13 | A held value that also reaches the Harness: its Secret loaded by `harness.envFrom`, its key read by a `harness.env` `secretKeyRef`, or its literal set by `harness.env`. A kit Secret (`jr2-instance`, `jr2-held-*`, `jr2-instance-effector`, `jr2-effector-*`, `<sandbox>-effector`, `jr2-git-*`, and each pod's credentials, `jr2-instance-harness-token` and `<sandbox>-token`) named by either. |
243
+ | R14 | A held secret's name that is also a key of a Secret that `harness.envFrom` names. The message names the Secret, the key and the held secret. |
244
+
245
+ R14 exists because the Stand-ins are files, not env: no Stand-in hides such a key, so its real value would reach the
246
+ Harness's env, and the Harness uses a held secret but never holds it. `up` already reads each `envFrom` Secret to
247
+ check that it exists (ADR-0019).
248
+
249
+ Each message names the config path, never a value, and says what to write instead. A SigV4 or ADC credential in
250
+ `harness.env` is warned about, not refused. A known model key with no `hosts` is bound to the host its provider's
251
+ calls go to: the host of `harness.catalog.<id>.baseUrl` when set, else pi's catalog host. The key and the calls then
252
+ read one config and cannot drift. `MODEL_KEYS` is pi's data at the pin, written as plain data by `just model-keys` (a
253
+ script in `@jr2/harness`) at each pi bump, so the Orchestrator has no pi dependency. A test in `@jr2/harness` checks
254
+ the table against pi, and checks that the file is the script's exact text. So a pi bump that moves a provider, or a
255
+ hand edit of the file, fails the default gate until the recipe runs.
256
+
257
+ - **A catalog provider can move to a gateway.** pi has no env var for a catalog provider's base URL. It hands the
258
+ provider the model it is given. So `resolveModel` returns a copy of the catalog model with `baseUrl` replaced, for
259
+ every model of that provider. The catalog's other facts ride along. The model id sent upstream is the catalog id, so
260
+ the gateway must serve that name (a LiteLLM `model_name` alias).
261
+ - **Certificates.** `jr2 up` keeps a per-Instance CA in the `jr2-held-ca` Secret, which no pod mounts; it reads the CA
262
+ over the kube API. It issues one leaf per bound `host:port` into `jr2-held-tls` before a pod starts, so the CA's key
263
+ never enters a Harness pod. Keys are ECDSA P-256, written by a small DER writer in `@jr2/cli` over `node:crypto`: no
264
+ dependency, and no `openssl` on the host. `jr2 up` never rotates the CA; a human deletes the Secret to rotate it. A
265
+ leaf is reused while its SAN is the same, it chains to the current CA, and more than 30 days remain.
266
+ - **Trust.** The `jr2-ca` ConfigMap carries `ca.crt` (the user's `caBundle`), and while a secret is held: `extra.crt`
267
+ (`caBundle` + the jr2 CA, for `NODE_EXTRA_CA_CERTS`), `bundle.crt` (the roots + both, for the tools whose variable
268
+ replaces their store), and `upstream.crt` (the roots + `caBundle`, and never the jr2 CA, for the Custodian). "Roots"
269
+ are the Mozilla roots of the Node that runs `jr2 up`. The ConfigMap is applied server-side, because two copies of the
270
+ roots are more than client-side apply's annotation can hold.
271
+ - **Every Secret is applied server-side.** A client-side apply copies the whole object into its
272
+ `kubectl.kubernetes.io/last-applied-configuration` annotation, so the values of a Secret are then in plaintext for
273
+ each reader of its metadata. So `jr2 up` applies each Secret that it writes server-side: `jr2-instance`,
274
+ `jr2-harness-env`, `jr2-held-ca`, `jr2-held-tls`, `jr2-held-secrets-<pod>`, the Effector Secrets, the git Secrets, the
275
+ deploy key and the check pod's Secrets. The field manager is `jr2`, with `--force-conflicts`, because jr2 is the only
276
+ writer of these Secrets. A `stringData` goes as `data`, because a server-side apply owns the fields that it names, and
277
+ `stringData` is write-only. On a Secret that an older `jr2 up` applied client-side, the first server-side apply
278
+ migrates it: kubectl (1.26 and later) moves the fields of the client-side manager to `jr2`, so that apply removes the
279
+ annotation and each key that it does not name, and `jr2` is then the only manager (measured on kind with kubectl
280
+ 1.35.9). When that migration fails, kubectl only writes a warning on stderr. So after each apply of Secrets, a
281
+ fallback removes the annotation (`kubectl annotate … last-applied-configuration-`). The removal of an annotation that
282
+ is not there changes nothing, so the fallback needs no record of what was removed. When the fallback fails, `jr2 up`
283
+ writes one warning that names the Secrets, and the converge goes on. The rule is in the apply seam
284
+ (`kubectlAdmin.apply`), so a Secret that a later change adds is applied in the same way. The values go on stdin, never
285
+ in argv. No ConfigMap holds a value.
286
+ - **Objects, per pod.** The held manifest, the Custodian's files and the literal values are each a pod's own, from the
287
+ one derivation that places Effect secrets. A pod is keyed `instance` for the Instance Harness, and `ws-<digest>` for a
288
+ Workspace, the digest of its registered workflow and the placement route to its `workspace()` (the pod key below) —
289
+ the route the walk reads statically and the run host reads off the live actor at provision, so both name one pod
290
+ alike. Nothing rendered per pod lands in a shared object: `jr2 up` declares, and the pod's maker renders. `jr2-held`
291
+ (always) holds `held-<pod>.json` for each pod (what it holds: names, bindings, headers, never a value) and
292
+ `program.lua` once per Instance, so it grows by one manifest per Workspace. A pod's two rendered files, Envoy's
293
+ `envoy.json` and its `custodian.lua`, are in a ConfigMap of that pod's own: the Orchestrator renders a Sandbox's at
294
+ provision, from the manifest it reads, into `<sandbox>-custodian`, applied after the CR and owned by it like the
295
+ `<sandbox>-token` Secret, so it goes with the pod (the Orchestrator's Role may create and patch a ConfigMap, and no
296
+ more); `jr2 up` renders the Instance Harness's into `jr2-held-custodian`, applied with its StatefulSet and deleted
297
+ with it. A pod's `custodian-config` volume projects its own ConfigMap and `program.lua` from `jr2-held`. The reason:
298
+ every Kubernetes object stops at 1 MiB, and client-side apply's annotation at 256 KiB; one copy of Envoy's files per
299
+ pod in `jr2-held` passed that limit at nine Workspaces, and a dedupe or a server-side apply only moves the ceiling.
300
+ `jr2-held-secrets-<pod>` holds one pod's literal values, labeled `jr2.dev/held-pod`, and `jr2 up` deletes the one a
301
+ pod no longer needs. `jr2-held-tls` (every bound target's leaf) and `jr2-held-ca` are the Instance's. The Orchestrator
302
+ mounts `jr2-held` at `/etc/jr2/held` and reads the manifest of the Workspace it provisions, per provision — the
303
+ `jr2-images` pattern ([ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md)); a Workspace `jr2 up` never walked has
304
+ none, and its provision is refused, naming `jr2 up`. A pod key is the workflow and the Workspace's placement route:
305
+ the `actors` slot keys down to its `workspace()`, a wrapper's body skipped, where a plain Machine invoked inline adds
306
+ xstate's generated invoke src for that hop (`xstate.invoke.<index>.<state id>`), held so it can never equal a slot
307
+ key. `jr2 up` computes it from the state node, and the provision reads it off the live actor tree, so both derive one
308
+ key. A key reached by slot keys alone is what it was. A wrapper's body adds no hop, so a `workspace()` reached through
309
+ another `workspace()`'s body (`workspace(workspace(...))`, `workspace(pool(workspace(...)))`) has the outer one's key:
310
+ `jr2 up` refuses that Machine, with or without a secret, since the two pods' secrets could not be kept apart, and
311
+ names the fix (the inner `workspace()` under a key of a plain Machine's `actors`, which has a route of its own); every
312
+ other Workspace has a key of its own. A Workspace reached through a Machine object handed to `spawnChild`, which no
313
+ walk can see, fails at provision with the slot form. One function, `custodianComposition`, builds what a pod gains
314
+ from its pod's manifest; `kubeSandbox` and `instanceHarnessObjects` both call it, so the two pod shapes cannot drift.
315
+ The Instance Harness's pod template carries `jr2.dev/held-digest`, an HMAC over its pod's held-secret inputs keyed
316
+ with the signing key, so a held-secret edit rolls it at the next `jr2 up`. Its Custodian reads each value and its
317
+ files once at start, so the inputs are its two rendered files, the program, a literal's value, a `valueFrom`'s
318
+ reference, and a digest of the value in the user's Secret, keyed with the signing key: `jr2 up` reads that Secret to
319
+ check the key exists, and keeps the digest in place of the value, so a value edited there rolls the pod too. A live
320
+ Sandbox keeps what its Custodian read at start.
321
+
322
+ ## Considered options
323
+
324
+ - **jr2's own proxy in Node, in a second container of the Adapter's image** (branch `held-secrets/node`). No new image
325
+ and no second language; jr2 then owns TLS-parsing code at the trust boundary, and needs its own tests for every edge
326
+ Envoy already handles. It keeps the Adapter.
327
+ - **Envoy in a container beside the Adapter** (branch `held-secrets/envoy-sidecar`). The same Envoy configuration, and
328
+ three containers per pod. Two containers hold two credentials, so a fault in one engine does not reach the other's
329
+ credential. This branch trades that split for one container fewer and one component fewer: the Custodian holds both.
330
+ - **The Orchestrator serves MCP, and Envoy forwards `/mcp/<iid>`.** Rejected. It reverses ADR-0013's "the Orchestrator
331
+ hosts no MCP", puts a transport and session handling back in the control plane, and keeps the MCP SDK on both ends —
332
+ to carry two plain HTTP calls. What ADR-0013 rejected about translation inside the Harness was the credential in that
333
+ container. The Custodian now holds it, so that reason is gone.
334
+ - **The Adapter relays model calls** (ADR-0058's first direction). Rejected: every provider needs its `baseUrl`
335
+ rewritten to the relay, and pi's catalog has no env var for a base URL, so the catalog could not follow.
336
+ - **Drop pi's catalog and name every model as a custom provider.** Rejected by the user: the catalog holds the model
337
+ facts the user would have to copy by hand.
338
+ - **iron-proxy.** It intercepts every host and cannot pass one through, and it makes leaves at runtime, so the CA's key
339
+ would be in the pod. The Sandbox Image is the user's, and its tools may not trust a jr2 CA.
340
+ - **mitmproxy, Squid.** mitmproxy streams SSE only if the addon asks, and its swap is code jr2 writes anyway; Squid
341
+ brings the GPL and a poor CVE record. Both would be a fourth image.
342
+ - **Envoy's `credential_injector`, or the value in a bootstrap Secret with `request_headers_to_add`.** Rejected: the
343
+ first is alpha and fails on a trailing newline; both replace a header without seeing the Stand-in, and the second
344
+ cannot hold a `secretKeyRef` value, which `jr2 up` never copies.
345
+ - **Intercept every host.** Rejected for the iron-proxy reason: a tool that does not trust the jr2 CA would then fail on
346
+ every host, not only on bound ones.
347
+
348
+ ## Consequences
349
+
350
+ - **What this closes, and what it does not.** The Agent cannot take a model key or a Sandbox's token out of the pod. The
351
+ Harness spends the model keys for the Turns. Workspace code can still **spend** a key through the Custodian while its
352
+ pod lives, on any path `paths` allows, when the `workspace()` grants that key. A key with admin rights can mint a new
353
+ key, and a minted key is one the Agent can take away: use a key with no admin rights, and narrow `paths`. The User
354
+ Container shares the pod's loopback, so a human in it can spend a key the same way. That human cannot read one either.
355
+ - **Response echo.** Responses pass unchanged. An upstream that echoes the full credential in a response hands it to the
356
+ Agent. Providers mask keys in errors; jr2 does not scrub responses.
357
+ - **Bypass.** A client that ignores `HTTPS_PROXY` goes direct and sends the Stand-in, which is worth nothing. It is not
358
+ blocked: egress control stays the cluster's decision (ADR-0058's open egress gap), except for the cloud metadata
359
+ addresses, which no Harness pod reaches
360
+ ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)). The same holds for the
361
+ control plane: a caller that skips the Custodian and sends the Stand-in to the Orchestrator gets 401 on every route
362
+ (`auth.test.ts`).
363
+ - **Clients that do not trust the jr2 CA** (Java, a pinned client) fail TLS to a bound host. They fail closed and send
364
+ nothing. Unbound hosts still work for them.
365
+ - **`SSL_CERT_FILE` replaces an OpenSSL tool's store.** A private CA baked into the Sandbox Image, and not put in
366
+ `caBundle`, stops being trusted by curl, git and Python in a pod with a held secret. The fix is `caBundle`. The gain:
367
+ `caBundle` now reaches curl, git and Python, which [ADR-0020](0020-private-ca-trust-is-a-named-concept.md) recorded as
368
+ missing.
369
+ - **Signing credentials cannot be held.** A SigV4 or ADC credential signs the request with the secret, and a Stand-in
370
+ cannot be swapped into a signature. `jr2 up` warns. The fix is the Effector
371
+ ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)): an Effect secret is the real value, in
372
+ a container the Agent executes nothing in. Its `http` kind sends a bearer, a Basic pair or a header the author writes,
373
+ and the credentialed `command`
374
+ ([ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)) runs the signing tool
375
+ there (`terraform`, `aws`, `gcloud`), its child confined with Landlock from the Effector's key and every secret it
376
+ does not name. `ANTHROPIC_OAUTH_TOKEN` is held like a key, but pi sends a Stand-in as `x-api-key`, which an OAuth
377
+ token cannot ride; hold it as `ANTHROPIC_AUTH_TOKEN` (`Authorization: Bearer`) instead.
378
+ - **Rotation of a value** reaches a live Sandbox only when its pod is replaced.
379
+ - **The walk reads definitions, not Turns.** A pod's model keys come from the models its Agents' definitions name. A
380
+ Turn whose invocation overrides the model with another provider's (`AgentTurnInput.model`) finds no key for that
381
+ provider in its pod: the Harness process holds a Stand-in only for the names in its own pod's manifest, so that
382
+ provider's key variable is not set at all. The call then has no key (a catalog provider) or sends the custom
383
+ provider's placeholder `unused`, and it fails. This holds in a Workspace and in the Instance Harness alike. The fix is
384
+ a definition that names the provider; in a Workspace, the key under the `workspace()`'s `secrets` also works. The
385
+ Instance Harness has no such list: its pod holds only the model keys of the `"none"` Agents it hosts.
386
+ - **TLS-only bindings.** A key for a plain-HTTP endpoint is refused (R7). A LAN `llama-server --api-key` needs TLS and
387
+ its CA in `caBundle`, or no key.
388
+ - **No proxy chaining.** The Custodian cannot forward to a corporate proxy. R11 says so rather than fail silently.
389
+ - **Two Harness env shapes.** With no held secret in it, a pod gets the Custodian's address and no proxy, and its HTTPS
390
+ goes direct. The Harness process holds the control Stand-in in both shapes. The Custodian runs in both shapes, because
391
+ it carries the Menu.
392
+ - **The dial guard adds no confinement while egress is open.** The Agent can dial those addresses itself, except the
393
+ cloud metadata addresses, which jr2's egress policy closes on a CNI that enforces it (ADR-0076). The guard matters
394
+ when a cluster closes egress to everything but the Custodian: then the Custodian is not a way around it.
395
+ - **One container holds two kinds of credential.** A fault in Envoy's TLS or HTTP parsing is a fault in the container
396
+ that holds both the Sandbox's tokens and the model keys. The branch with a separate Adapter keeps them apart.
397
+ - **The Harness process speaks the Orchestrator's agent routes itself.** Nothing is judged in the pod. No process that
398
+ the Harness confines holds the control Stand-in, so none can make those calls (ADR-0076).
399
+ - **`jr2-upload-pack` carries the fetch Stand-in as a constant.** The User Container gets no env (ADR-0005), and asks
400
+ like any other seat. A test pins the Go literal to `standIn("JR2_SANDBOX_TOKEN")`. The Custodian swaps it on the fetch
401
+ route alone, so the constant reaches no agent route.
402
+ - **The Custodian's image is a Pinned image.** It is deployed and never built, which
403
+ [ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md) now states. `kitRegistry` re-homes it and `jr2 kit push`
404
+ mirrors it ([ADR-0044](0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md)). The image sweep never
405
+ takes it, because jr2 never stamped it ([ADR-0039](0039-image-garbage-collects-by-reachability.md)). A kind node pulls
406
+ it from its registry, so the `@kind` tier needs network to `docker.io`. `just kit-push` does not seed it.
407
+ - **Where the Custodian suite runs.** `just custodian-test`: node's test runner against the pinned image under docker,
408
+ on the host network with free ports, with local HTTPS upstreams under their own CA. It is opt-in, like `@kind`,
409
+ because it needs docker ([ADR-0010](0010-bdd-acceptance-tests.md)), and the release job runs it on every tag
410
+ ([ADR-0055](0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md)): the image moves only with its
411
+ pin, but the configuration jr2 renders moves with any release. About 35 seconds. It checks the swap, the strip, 403
412
+ with no Stand-in, 405 on a method, 400 on an underscore header, a header `Connection` names, two `Host` headers, idle
413
+ connections on the health listener, 421, 400 and 403 on paths, the tunnel, the dial guard by every spelling and by
414
+ resolved name, 502 on an untrusted upstream, SSE timing, abort, a 302, the start-up refusal, the log, and the
415
+ Anthropic SDK through `HTTPS_PROXY`. The first SSE event arrives through the Custodian about half a millisecond later
416
+ than direct.
417
+ - **The `@kind` tier's model wants a key.** Its fake provider serves HTTPS under the tier's CA and refuses a request
418
+ without the key, which is `harness.provider.apiKey` — so every turn in the tier crosses a Custodian, in both
419
+ placements. A Rule proves the key is in no env and no file of either Harness container.
420
+ - **The provider preflight crosses a Custodian.** A provider's key is held. So `jr2 up` checks the provider from a pod
421
+ with a Custodian that holds its keys, and the stock Harness's `provider-preflight.ts`
422
+ ([ADR-0019](0019-one-converging-command-against-the-current-context.md)). The check pod reads its own copies of the
423
+ held objects. The check runs before `jr2 up` writes an object that the Instance or the operator reads. A literal key
424
+ is in the check pod's own Secret while the pod lives. A wrong or missing key, no egress to the bound host, and a
425
+ gateway that does not answer fail the converge, not a Turn.
426
+ - **The CA has no name constraints.** Constraints would force a new CA whenever the bound hosts change, and that breaks
427
+ a live pod whose Custodian restarts.
@@ -0,0 +1,111 @@
1
+ # A Workspace states its Size, and jr2 reserves all of it
2
+
3
+ No jr2 pod asked Kubernetes for anything. `Sandbox.spec.resources` existed and nothing filled it; the Custodian, the
4
+ Instance Harness and the Orchestrator set nothing either. Every pod was BestEffort, so the scheduler packed Sandboxes
5
+ without limit, the cluster autoscaler saw no demand, and a full cluster showed up as OOMKill and eviction instead of
6
+ Pending — an eviction takes `/work` with it (ADR-0021). The scaling review (2026-09-27) and a lab of real workloads on
7
+ the home cluster (2026-09-28: Playwright, `tsc` + vitest on hono, `cargo build` of ripgrep; one run each) measured what
8
+ a Sandbox uses. The owner plans to move Workspaces onto Agent Substrate later, so the shape is chosen to move there
9
+ unchanged.
10
+
11
+ ## Decision
12
+
13
+ - **A Workspace states one Size: a ceiling for its whole Sandbox, cpu and memory, nothing else.** The shape is Agent
14
+ Substrate's `ActorTemplate.resources`: a Kubernetes `ResourceRequirements` of which only `limits.cpu` and
15
+ `limits.memory` are accepted; `requests` and any other key are refused where they are read.
16
+ `workspace(body, { resources: { limits: { memory: "3Gi", cpu: "2" } } })`.
17
+ - **The Size is a fact of the Machine**, like its Sandbox Image (ADR-0037, ADR-0049): the author knows the toolchain,
18
+ the runner, the worker count. It is never per run. The resolution chain, most specific first:
19
+ `customize(machine, { resources })` (the composer, whose Repo may be bigger than the author's) → the `workspace()` →
20
+ `sandbox.resources` in `jr2.config.ts` (the Instance's default, applied **only** to a Workspace that states none —
21
+ never an override of a stated Size, so each fact has one override path) → the kit default.
22
+ - **jr2 reserves the whole Size.** Memory request = limit and cpu request = limit: the Sandbox pod is Guaranteed. Memory
23
+ pressure never picks a Workspace, and placement accounts the way Substrate's does. Density is paid for here and
24
+ recovered later by suspend (the scaling review's R15), not by overcommit.
25
+ - **The CPU limit is enforced.** Nothing reads a CPU request (the cgroup shows `cpu.max = max`), while Node's
26
+ `availableParallelism`, Go 1.25, Rust and cargo, the JVM, .NET and every Substrate runtime read the limit. Throttling
27
+ is the accepted cost.
28
+ - **Tools that read neither the limit nor affinity get hints**, from the Downward API (`limits.cpu`, divisor 1):
29
+ `JR2_CPUS`, `OMP_NUM_THREADS` (which also makes `nproc` answer N), `PYTHON_CPU_COUNT`, `GOMAXPROCS`. `os.cpus()` still
30
+ reports the node, and Playwright sizes its workers from it alone, so a Machine passes `--workers=$JR2_CPUS`; the Agent
31
+ is told its CPU count by the Briefing ([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)), never
32
+ by the Frame.
33
+ - **jr2 splits the Size inside the pod.** The Custodian takes a fixed kit slice (64Mi, 50m). The User Container takes
34
+ its split if the Machine states one — `user` widens from a string to
35
+ `{ image, resources?: { limits: { memory?, cpu? } } }` (ADR-0005 foresaw the widening) — and otherwise shares the pod
36
+ budget with no limit of its own. The Sandbox Effector, in a pod that has one
37
+ ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), has no split and no limit of its own
38
+ and shares the pod budget the same way. The Harness container gets the rest as its own limit, so an OOM stays per
39
+ container where it can. Pod-level resources (KEP-2837) hold the whole-pod ceiling. A server with the PodLevelResources
40
+ gate off drops them without a word, so `jr2 up` dry-runs a pod that states them and refuses the cluster when they are
41
+ gone. `jr2 up` refuses a split that leaves the Harness below its floor (256Mi) and names the line.
42
+ - **Kit numbers.** Default Size: memory 2Gi, cpu 1. Node gives each process a V8 heap of about 55% of its container
43
+ limit, and hono's `tsc` aborted with a heap error below about 1.1Gi, so 1Gi fails a common task. 2Gi covers `tsc` +
44
+ vitest, `cargo build` and Playwright at 2 workers; Playwright at 4 workers and large monorepos state a Size. No
45
+ `NODE_OPTIONS` heap raise: a V8 abort is a tool error the Agent reads, while a bigger heap turns it into a kernel
46
+ kill.
47
+ - **`/dev/shm` is a memory-backed emptyDir in the Harness container only**, `sizeLimit` = 25% of the Harness share
48
+ (minimum 64Mi). It reserves nothing extra (shm is charged inside the Harness limit), and a full shm is ENOSPC or
49
+ SIGBUS — a tool error, never an OOM (lab). The Harness clears it at start: shm files outlive a container restart and
50
+ stay charged to the pod, which crash-looped the restarted container in the lab. There is no `shm` field; a Machine
51
+ that needs more raises its Size.
52
+ - **`/work` disk is a kit-fixed ephemeral-storage request of 10Gi on the Harness container, with no cap.** The request
53
+ places the pod and ranks it last under node disk pressure; no `sizeLimit` and no limit, because passing one evicts the
54
+ pod and loses `/work` (T12). Disk is not part of the Size: Substrate's template size is cpu and memory only.
55
+ - **Priority.** `jr2 up` creates two cluster-scoped PriorityClasses beside the CRDs: `jr2-control` (100000,
56
+ PreemptLowerPriority) for the Orchestrator, the operator and the Repo cache agent — small, fixed and needed by every
57
+ Sandbox — and the Instance Effector ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)),
58
+ small and fixed; `jr2-sandbox` (1000, preemptionPolicy `Never`) for Sandboxes and the Instance Harness — a pod of
59
+ ordinary priority cannot preempt a live Workspace, and a waiting Sandbox evicts nobody. Both stay far below the system
60
+ classes. A cluster with its own scheme names its classes in `priorityClasses: { control?, sandbox? }`.
61
+ - **Voluntary disruption.** A Sandbox pod and the Instance Harness pod carry
62
+ `cluster-autoscaler.kubernetes.io/safe-to-evict: "false"` and `karpenter.sh/do-not-disrupt: "true"`: moving either
63
+ loses work (`/work`; live conversations). The Orchestrator, the operator, the Repo cache agent and the Instance
64
+ Effector carry neither — a restarted Orchestrator re-attaches (ADR-0007), and a stopping Effector finishes its Effects
65
+ first. No PodDisruptionBudget: `maxUnavailable: 0` blocks node upgrades without end. A drain ends in `workspace.lost`,
66
+ and the body's policy decides (ADR-0021).
67
+ - **jr2's own pods are kit-sized, with no config key**: memory request = limit, a CPU request, no CPU limit (a
68
+ provisioning burst needs CPU, and no Substrate move applies to them). Orchestrator 500m / 1Gi. Instance Harness 250m /
69
+ 1Gi (it frees every conversation no live run holds — ADR-0031). Operator 50m / 256Mi (it caches every Pod in the
70
+ cluster). Instance Effector 50m / 128Mi. Repo cache agent 50m, memory request 128Mi and limit 1Gi — the one exception,
71
+ since `git index-pack` spikes and its OOM costs only a refetch.
72
+
73
+ ## Considered options
74
+
75
+ - **Sizes as named classes in `jr2.config.ts`, which the Machine names** (`workspace(body, { class: "browser" })`, the
76
+ config maps the class to resources). Rejected: it breaks packaging. Two packages each name `browser` and mean
77
+ different sizes, and the consumer must supply numbers only the package's author knew. ADR-0052's named class stays
78
+ placement only — node labels are the cluster's facts; a memory need is the Machine's.
79
+ - **Raw `ResourceRequirements`, all of it.** Rejected: the author could set a request below the limit, leave out the
80
+ memory limit, or reach fields Substrate refuses, and a Machine would depend on something the move drops.
81
+ - **A jr2 shape** (`{ memory, memoryLimit?, cpu }`). Rejected for the same reason in the other direction: one more
82
+ translation at the move, for no gain the Substrate subset does not give.
83
+ - **Burstable: reserve the working peak, allow bursts to the limit.** About twice the density. Rejected: with no
84
+ suspend, several Workspaces bursting at once is node pressure, and pressure evicts a whole pod with its `/work`.
85
+ - **Reserve CPU, never throttle.** Chosen first, then reversed: no mechanism tells a tool its CPU request (the lab tried
86
+ affinity, CPU Manager, LXCFS, gVisor, Kata and `LD_PRELOAD` shims; all but affinity need a limit, and affinity misses
87
+ `os.cpus()` and lets pods pin the same cores), while every container-aware runtime reads the limit.
88
+ - **An `shm` field, or a `/work` disk field, on the Size.** Deferred: Substrate has neither. A jr2 field beside the Size
89
+ can come when a real Machine needs one.
90
+ - **`/dev/shm` without a `sizeLimit`, or the container's 64Mi default.** Rejected: unbounded, a full shm OOM-kills the
91
+ container and poisons its restart; 64Mi crashed Chromium renderers when Playwright's `--disable-dev-shm-usage` was
92
+ off.
93
+
94
+ ## Consequences
95
+
96
+ - ADR-0005 is rewritten in place: containers now carry limits from the Size, the User Container may take a split, and
97
+ `/dev/shm` joins the Harness container's mounts.
98
+ - ADR-0052 gains `sandbox.resources` beside `nodeSelector` and `tolerations`; its reserved named class stays placement
99
+ only, so its `node:` name stays.
100
+ - An OOM in the Harness container still kills the Harness with the Agent's processes (cgroup v2 group kill). Containing
101
+ it is ADR-0061.
102
+ - A Workspace parked on a Gate holds its whole Size. That is the density problem R15 exists for.
103
+ - On a move to Substrate, a Workspace with a User Container split needs a microVM pool: Substrate accepts per-container
104
+ limits on microVM runtimes only, and gVisor refuses them.
105
+ - `jr2 up` warns — never refuses — when no current Sandbox node could hold a Workspace's Size (the ADR-0052 stance: the
106
+ node set moves).
107
+ - GLOSSARY.md gains **Size**.
108
+ - The kit-sized control pods ask a node for about 900m a scenario before any Sandbox, so the `@kind` tier on a 4-vCPU
109
+ hosted runner runs two workers, not the profile's four (`just e2e-kind 2` in the release job). v0.4.0's release run
110
+ measured the alternative: four workers left every Sandbox Pending on `Insufficient cpu`. The requests are the kit's
111
+ and are not lowered for a runner.