@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
package/src/config.ts CHANGED
@@ -21,11 +21,12 @@ import { join } from "node:path";
21
21
  import { fileURLToPath, pathToFileURL } from "node:url";
22
22
  import { z } from "zod";
23
23
  import { repoIdentity } from "./repo-identity.ts";
24
+ import { assertSize, type Size } from "./size.ts";
24
25
 
25
26
  // ------------------------------------------------------------------------------------------------
26
27
  // `git.credentials` (ADR-0051): how the cluster authenticates to a Repo, and the fence.
27
28
  //
28
- // A Repo is identified by its url and nothing in this file names one (CONTEXT.md "Repo"). What
29
+ // A Repo is identified by its url and nothing in this file names one (GLOSSARY.md "Repo"). What
29
30
  // the config declares is CREDENTIALS, matched by prefix on the identity — the Argo and Flux shape.
30
31
  // The Orchestrator resolves the entry when it creates a Repo CR and writes a `secretRef`; the
31
32
  // cache agent reads only that. The list is also the fence: a per-run url (run input, a ticket
@@ -33,19 +34,96 @@ import { repoIdentity } from "./repo-identity.ts";
33
34
  // credential against an arbitrary host. A bound url is code the Instance typechecked and deployed
34
35
  // — admitted without a match, cloned anonymously.
35
36
 
36
- /** One credentials entry. An entry may carry neither `token` nor `sshKey`: it then admits its
37
- * prefix through the fence and the clone is anonymous. */
37
+ // One credentials entry. An entry may carry neither `token` nor `sshKey`: it then admits its
38
+ // prefix through the fence and the clone is anonymous.
39
+ /** One entry of `git.credentials`: the credential that the cluster uses for the Repos whose
40
+ * identity starts with `match`. An entry with neither `token` nor `sshKey` admits its Repos
41
+ * through the fence, and they clone with no credential. */
38
42
  export type GitCredential = {
39
- /** A prefix on the identity (`github.com/ourorg/`), or `*` for everything. */
43
+ /** The Repos this entry is for: a prefix of a Repo's identity, such as `github.com/ourorg/`, or
44
+ * `*` for every Repo. The identity of a Repo is its host and path with no scheme, user or `.git`.
45
+ * When more than one entry matches, the longest `match` wins; on a tie, the first entry in the
46
+ * list wins. */
40
47
  match: string;
41
- /** Env var name holding an HTTPS token; `jr2 up` materializes its value into the Instance Secret. */
48
+ // The cache agent spends the clone Secret; the Orchestrator never holds it (ADR-0068).
49
+ /** For an `https://` url: the name of the env var that holds a token, read from the shell or
50
+ * `.env` when `jr2 up` runs. `jr2 up` writes the value into a Secret (`jr2-git-<hash>`) that only
51
+ * the Repo cache agent reads. The Agent never sees it. */
42
52
  token?: string;
43
- /** A Secret (Flux key names: `identity`, `identity.pub`, `known_hosts`) holding a deploy key. */
53
+ /** For an ssh url: the name of a Secret that you create. It holds a deploy key under the Flux
54
+ * key names `identity`, `identity.pub` and `known_hosts`. */
44
55
  sshKey?: string;
56
+ // The credential a `push()` spends (ADR-0051, ADR-0067). A token goes Basic through a credential
57
+ // helper only.
58
+ /** The credential that a `push()` Effect uses, when it is not the clone credential. Write
59
+ * `{ sshKey }` or `{ token }`. Default: the clone credential pushes. An entry with no credential
60
+ * admits a clone and never a push. When the push finds no usable credential, it is refused, for
61
+ * example: `no git.credentials entry matches "<identity>" — a push spends the entry its url
62
+ * matches`, or `git.credentials entry "<match>" holds no credential a push over <scheme> can
63
+ * spend`. */
64
+ push?:
65
+ | {
66
+ /** For an ssh url: the name of a Secret that you create, with the same keys as the
67
+ * entry's `sshKey`. */
68
+ sshKey: string;
69
+ }
70
+ | {
71
+ /** For an `https://` url: the name of the env var that holds the push token, read as for
72
+ * the entry's `token`. */
73
+ token: string;
74
+ };
75
+ // Explicit (ADR-0067): an auth key is never reused as a signing key.
76
+ /** The key that a `push({ sign: true })` signs with, and the committer it signs as:
77
+ * `{ sshKey, name, email }` or `{ gpgKey, name, email }`. `sshKey` names a Secret with
78
+ * `identity`. `gpgKey` names a Secret with `private.asc` and no passphrase. jr2 never signs with
79
+ * the clone or push key, so name the signing key here. The email must belong to the forge account
80
+ * that holds the key, or the forge shows the signature as unverified. A signed push without it is
81
+ * refused: `signed, and git.credentials entry "<match>" holds no sign field`. */
82
+ sign?:
83
+ | {
84
+ /** The name of a Secret that you create, which holds the ssh signing key under `identity`. */
85
+ sshKey: string;
86
+ /** The committer's name on the signed commit. */
87
+ name: string;
88
+ /** The committer's email. It must belong to the forge account that holds the key. */
89
+ email: string;
90
+ }
91
+ | {
92
+ /** The name of a Secret that you create, which holds the GPG signing key under
93
+ * `private.asc`, with no passphrase. */
94
+ gpgKey: string;
95
+ /** The committer's name on the signed commit. */
96
+ name: string;
97
+ /** The committer's email. It must belong to the forge account that holds the key. */
98
+ email: string;
99
+ };
100
+ // The squashed commit is new, and without `sign` no signer names its committer (ADR-0067).
101
+ /** Who commits an unsigned `push({ squash: true })`: `{ name, email }`. The squashed commit is
102
+ * new, so it needs a committer. Its date comes from the Effector's clock, and its author stays
103
+ * the Agent. A signed push is committed by the signer, so it does not read this field. A squashed
104
+ * push with neither field is refused: `squashed without sign, and git.credentials entry "<match>"
105
+ * holds neither sign nor committer`. */
106
+ committer?: {
107
+ /** The committer's name on the squashed commit. */
108
+ name: string;
109
+ /** The committer's email on the squashed commit. */
110
+ email: string;
111
+ };
45
112
  };
46
113
 
114
+ /** How the cluster authenticates to Repos. */
47
115
  export type GitConfig = {
48
- /** Matched by prefix on the Repo identity; the longest `match` wins, ties → first in the list. */
116
+ /** The credential entries, matched by prefix on the Repo identity. The longest `match` wins; on a
117
+ * tie, the first entry in the list wins.
118
+ *
119
+ * The list is also a fence. A per-run url, which comes from the run input, must match an entry,
120
+ * or the Sandbox refuses it when it attaches the Repo:
121
+ * `Sandbox "<name>" refuses the per-run repo <url> for slot "<slot>": no git.credentials entry matches "<identity>"`.
122
+ * Thus a run cannot spend this cluster's credentials against any host. A url that a Machine
123
+ * binds needs no entry. When no entry matches it, it clones with no credential.
124
+ *
125
+ * Each entry is checked when the config loads:
126
+ * `<file> at git.credentials[<i>]<path>: <problem>`, followed by the shape of an entry. */
49
127
  credentials?: readonly GitCredential[];
50
128
  };
51
129
 
@@ -63,7 +141,7 @@ export function matchCredential(identity: string, list: readonly GitCredential[]
63
141
  }
64
142
 
65
143
  /** The Secret a Repo CR's `secretRef` names for `url` under `entry`, picked by the url's scheme:
66
- * https/http spend a token (a Secret the Orchestrator derives from the env var), ssh spends the
144
+ * https/http spend a token (the clone Secret `jr2 up` writes from the env var), ssh spends the
67
145
  * named deploy-key Secret. `undefined` when no entry, no applicable field, or a scheme that carries
68
146
  * no credential (`git://`, a local path) — the clone is anonymous. */
69
147
  export function credentialSecretFor(
@@ -94,21 +172,41 @@ export function isSshUrl(url: string): boolean {
94
172
  }
95
173
  }
96
174
 
97
- /** An env var on the Harness container, in the CR's (corev1.EnvVar) shape — `value` or a
98
- * `valueFrom` secret/configmap reference, passed through to the operator verbatim. */
175
+ // An env var on the Harness container, in the CR's (corev1.EnvVar) shape — `value` or a
176
+ // `valueFrom` secret/configmap reference, passed through to the operator verbatim.
177
+ /** One env var of the Harness container, in the Kubernetes `EnvVar` shape. The Agent can read it,
178
+ * so it must not hold a model key: put a model key in `secrets`. */
99
179
  export type HarnessEnvVar = {
180
+ /** The env var's name. A known model key name, such as `ANTHROPIC_API_KEY`, is refused:
181
+ * `harness.env[<i>] sets <NAME>, a model key the Agent could read and take out of the pod — hold it instead`. */
100
182
  name: string;
183
+ /** The value, usually read from `process.env`. `jr2 up` writes it into a Secret of the Instance.
184
+ * Write `value` or `valueFrom`. */
101
185
  value?: string;
186
+ /** Or a Kubernetes `valueFrom` reference, such as `{ secretKeyRef: { name, key } }`, given to the
187
+ * pod as it is. A reference to a Secret of the kit is refused:
188
+ * `harness.env[<i>] (<NAME>) reads Secret "<name>", which is the kit's and never the Harness's`. */
102
189
  valueFrom?: Record<string, unknown>;
103
190
  };
104
191
 
105
- /** A whole-Secret/ConfigMap env injection (corev1.EnvFromSource) for the Harness container —
106
- * e.g. `{ secretRef: { name: "anthropic" } }` to hand a real Harness its model API key. */
192
+ // A whole-Secret/ConfigMap env injection (corev1.EnvFromSource) for the Harness container.
193
+ /** A Secret or a ConfigMap whose keys all become env vars of the Harness container, in the
194
+ * Kubernetes `EnvFromSource` shape. Write `secretRef` or `configMapRef`. The Agent can read every
195
+ * key, so `jr2 up` refuses one that holds a known model key:
196
+ * `harness.envFrom[<i>] loads <where>, which carries <KEY> — a model key the Agent could read`. */
107
197
  export type HarnessEnvFromSource = {
108
- /** Put before every key's name, as corev1 does. */
198
+ /** Text put before each key's name, as Kubernetes does. Default: none. */
109
199
  prefix?: string;
110
- secretRef?: { name: string };
111
- configMapRef?: { name: string };
200
+ /** A Secret that you manage, by name: `{ name }`. `jr2 up` checks that it exists. */
201
+ secretRef?: {
202
+ /** The name of the Secret. */
203
+ name: string;
204
+ };
205
+ /** A ConfigMap that you manage, by name: `{ name }`. */
206
+ configMapRef?: {
207
+ /** The name of the ConfigMap. */
208
+ name: string;
209
+ };
112
210
  };
113
211
 
114
212
  /** This kit's version — npm version == published image tag, one release train (ADR-0019). */
@@ -118,188 +216,405 @@ export const KIT_VERSION = (
118
216
  }
119
217
  ).version;
120
218
 
121
- /** Token limits for one model — the Harness's provider-registration options, keyed per model because limits are
122
- * properties of the MODEL, not the endpoint (agents pick models per definition, ADR-0018). */
219
+ // Token limits for one model — the Harness's provider-registration options, keyed per model
220
+ // because limits are properties of the MODEL, not the endpoint (agents pick models per definition,
221
+ // ADR-0018).
222
+ /** The token limits of one model of a custom provider. */
123
223
  export type HarnessProviderModel = {
124
- /** The model's context window, in tokens (vLLM: `max_model_len`). */
224
+ /** The model's context window, in tokens (vLLM: `max_model_len`). Default: the provider's
225
+ * `contextWindow`. */
125
226
  contextWindow?: number;
126
- /** The model's max output tokens per completion. */
227
+ /** The most output tokens in one completion. Default: the provider's `maxTokens`. */
127
228
  maxTokens?: number;
128
229
  };
129
230
 
130
- /** A custom model provider (ADR-0018) — what the stock Harness registers with pi's
131
- * `registerProvider(id, { api, baseUrl, … })` (ADR-0027). The vLLM/Ollama path: an OpenAI-compatible
132
- * endpoint under an instance-chosen provider id. */
231
+ /** The wire protocols the Harness speaks for a custom provider (`harness.provider.api`, ADR-0018) —
232
+ * `CUSTOM_APIS` in `@jr2/harness`, whose test holds the two lists equal. `jr2 up` refuses any other
233
+ * value before it starts anything, as the Harness would refuse it at boot. */
234
+ export const CUSTOM_PROVIDER_APIS: readonly string[] = ["openai-completions"];
235
+
236
+ // A custom model provider (ADR-0018) — what the stock Harness registers with pi's
237
+ // `registerProvider(id, { api, baseUrl, … })` (ADR-0027). The vLLM/Ollama path: an
238
+ // OpenAI-compatible endpoint under an instance-chosen provider id.
239
+ /** A custom model provider: an endpoint that is not in the model catalog, such as vLLM or Ollama,
240
+ * under a provider id that you choose. An Agent names one of its models as `<id>/<model>`. */
133
241
  export type HarnessProvider = {
134
- /** The provider id model specifiers use (`<id>/<model>`), e.g. `vllm`. */
242
+ /** The provider id that model names start with: `vllm` in `vllm/Qwen/Qwen3-32B`. It must not
243
+ * also be a key of `harness.catalog`:
244
+ * `harness.catalog.<id>: "<id>" is also harness.provider.id — a custom provider owns its own baseUrl`. */
135
245
  id: string;
136
- /** The wire protocol, e.g. `openai-completions` (most OpenAI-compatible endpoints). */
246
+ /** The wire protocol. The only value is `"openai-completions"`, which most OpenAI-compatible
247
+ * endpoints speak, such as vLLM and Ollama. `jr2 up` refuses any other value before it builds or
248
+ * applies anything:
249
+ * `refusing: harness.provider.api is "<api>" — the Harness speaks: openai-completions (ADR-0018)`. */
137
250
  api: string;
138
- /** The endpoint — reachable FROM PODS (`localhost` never is; a LAN address works on kind).
139
- * Deployment-varying → resolve from env (`.env`), never hardcode (ADR-0019). */
251
+ // Deployment-varying → resolve from env (`.env`), never hardcode (ADR-0019).
252
+ /** The endpoint's URL. The pods must reach it: `localhost` never works, and a LAN address works
253
+ * on kind. It changes between deployments, so read it from `process.env` (`.env`). `jr2 up`
254
+ * checks from inside the cluster that the endpoint answers. */
140
255
  baseUrl: string;
141
- /** API key, when the endpoint wants one. May read `process.env`. It is a held secret
142
- * (ADR-0059): `jr2 up` gives the value to the Custodian alone, bound to `baseUrl`'s host, and the
143
- * Harness container holds only its Stand-in. So `baseUrl` must be `https://` when this is set.
144
- * Genuinely optional: an unauthenticated endpoint needs none (the Harness sends the wire
145
- * library's placeholder, which vLLM/Ollama ignore). */
256
+ // A held secret (ADR-0059). Genuinely optional: an unauthenticated endpoint needs none (the
257
+ // Harness sends the wire library's placeholder, which vLLM/Ollama ignore).
258
+ /** The API key, when the endpoint needs one. Read it from `process.env`. It is a Held secret,
259
+ * `JR2_PROVIDER_API_KEY`: `jr2 up` gives the value to the Custodian only, bound to the host of
260
+ * `baseUrl`, and the Harness container holds only its Stand-in. Thus `baseUrl` must be
261
+ * `https://` when you set it:
262
+ * `harness.provider.apiKey is set, and harness.provider.baseUrl is not https://`. Default: none,
263
+ * for an endpoint that needs no key. */
146
264
  apiKey?: string;
147
- /** Endpoint-wide default token limits, for any model this provider serves. Flue resolves
148
- * per-model → provider-level → catalog → 0, and a CUSTOM provider id has no catalog entry —
149
- * unset limits resolve to 0, which leaves auto-compaction no context budget to reason about
150
- * (the wire request itself is fine: a 0 `maxTokens` is omitted, never sent). */
265
+ // Limits resolve per-model → provider-level → 0, and a CUSTOM provider id has no catalog entry —
266
+ // unset limits resolve to 0, which leaves auto-compaction no context budget to reason about. The
267
+ // wire request sends no limit for a 0 `maxTokens`: pi-ai omits it when the context window is
268
+ // known, and the Harness removes the 1 that pi-ai would send when neither is (`unlimitedPayload`).
269
+ /** The context window, in tokens, of each model of this provider that `models` does not set.
270
+ * Set it here or in `models`: a limit that nothing sets is 0, and then Compaction has no context
271
+ * budget. */
151
272
  contextWindow?: number;
273
+ /** The most output tokens in one completion, for each model of this provider that `models` does
274
+ * not set. Default: no limit is sent. */
152
275
  maxTokens?: number;
153
- /** Per-model limits, keyed by the model id AFTER the provider prefix — the map entry for
154
- * `vllm/Qwen/Qwen3-32B` is `"Qwen/Qwen3-32B"`. Committable model properties, not
155
- * deployment-varying: whichever model an Agent definition names resolves its own entry. */
276
+ // Committable model properties, not deployment-varying: whichever model an Agent definition names
277
+ // resolves its own entry.
278
+ /** Token limits for each model, keyed by the model id after the provider prefix: the key for
279
+ * `vllm/Qwen/Qwen3-32B` is `"Qwen/Qwen3-32B"`. A limit set here wins over the provider's. */
156
280
  models?: Record<string, HarnessProviderModel>;
157
281
  };
158
282
 
159
- /** Where one of pi's catalog providers sends its calls (ADR-0059) — reach, not a model choice
160
- * (ADR-0018). The catalog keeps every model fact (limits, cost, thinking levels); this changes only
161
- * the endpoint, for a gateway that serves the provider's own API (LiteLLM, for example). */
283
+ // Where one of pi's catalog providers sends its calls (ADR-0059) — reach, not a model choice
284
+ // (ADR-0018). The catalog keeps every model fact (limits, cost, thinking levels).
285
+ /** Where the calls of one catalog provider go: a gateway that serves that provider's own API, such
286
+ * as LiteLLM. Only the endpoint changes. The catalog keeps every fact about the models. */
162
287
  export type CatalogReach = {
163
- /** Replaces `model.baseUrl` for every model of this provider — the WHOLE base the provider's API
164
- * appends to (`https://litellm.corp.example` for `anthropic`, which appends `/v1/messages`;
165
- * `https://litellm.corp.example/v1` for `openai`). HTTPS only. Deployment-varying → `.env`. */
288
+ // Replaces `model.baseUrl` for every model of this provider.
289
+ /** The whole base URL that the provider's API adds its paths to: `https://litellm.corp.example`
290
+ * for `anthropic`, which adds `/v1/messages`; `https://litellm.corp.example/v1` for `openai`. It
291
+ * must be `https://`: `harness.catalog.<id>.baseUrl must be an https:// URL`. It changes between
292
+ * deployments, so read it from `process.env` (`.env`). */
166
293
  baseUrl: string;
167
294
  };
168
295
 
169
- /** A credential the Harness uses and never holds (ADR-0059). The Harness container's env carries
170
- * its Stand-in, `jr2-held-<name>`; the Custodian puts the value into requests toward `hosts` alone. */
171
- export type HeldSecret = {
172
- /** The env var the Harness container sees — `[A-Z_][A-Z0-9_]*`. It holds the Stand-in. */
296
+ // One secret of the Instance (ADR-0059's entry shape, ADR-0068's list). Its placement is READ,
297
+ // never declared, and naming is placement: a pod gets the secrets something in it names. Both is
298
+ // both, and `jr2 up` reports it per pod. `hosts`, `headers` and `paths` are the binding, never the
299
+ // placement.
300
+ /**
301
+ * One secret of the Instance. You do not say where it goes: it goes where something names it.
302
+ *
303
+ * - It is a Held secret in each Harness pod that names it. A pod names it when its `workspace()`
304
+ * lists it under `secrets`, or when it is the model key of a provider that an Agent there uses. The Harness
305
+ * container's env var then holds only its Stand-in, `jr2-held-<name>`, and the pod's Custodian
306
+ * puts the value into requests toward `hosts` only.
307
+ * - It is an Effect secret where an Effect names it with `secret("name")`: a file in that
308
+ * Effect's Effector.
309
+ *
310
+ * One secret can be both. Its `hosts`, `headers` and `paths` are its binding, never its placement:
311
+ * they say where the value may go, not which pod gets it. `jr2 up` warns about an entry that nothing names:
312
+ * `secrets[<i>] (<NAME>) is named by nothing; goes nowhere`.
313
+ */
314
+ export type SecretEntry = {
315
+ /** As a Held secret, the name of the env var that the Harness container sees, which holds the
316
+ * Stand-in. As an Effect secret, the name that `secret("<name>")` names, and the name of the
317
+ * Effector's file. For a Held secret, it must match `[A-Z_][A-Z0-9_]*`:
318
+ * `secrets[<i>].name "<name>" is not an env var name ([A-Z_][A-Z0-9_]*)`. It must not start with
319
+ * `JR2_` and must not be a proxy or trust variable of the kit:
320
+ * `secrets[<i>].name <NAME> is the kit's — JR2_ names and the proxy and trust variables belong to the mechanism`.
321
+ * One name, one entry: `secrets[<i>].name <NAME> is held twice`. */
173
322
  name: string;
174
- /** The value as a literal, usually read from `process.env`/`.env`. `jr2 up` writes it into the
175
- * `jr2-held-secrets` Secret, which only the Custodian mounts. Exactly one of `value`/`valueFrom`. */
323
+ // `jr2 up` writes it into the Secret of each placement that names it —
324
+ // `jr2-held-secrets-<pod>` (that pod's Custodian's), `jr2-instance-effector` — and nowhere else.
325
+ /** The value, usually read from `process.env` (`.env`). `jr2 up` writes it only into the Secrets
326
+ * of the containers that hold it. Write exactly one of `value` and `valueFrom`:
327
+ * `secrets[<i>] needs exactly one of value or valueFrom`. A `.env` variable that is not set reads
328
+ * as no value, and an empty one is refused: `secrets[<i>].value is empty — set <NAME> in .env`. */
176
329
  value?: string;
177
- /** Or a key of a Secret you manage (Sealed Secrets, External Secrets). `jr2 up` checks that the
178
- * Secret and key exist; the Custodian mounts that one key as a file, and no other container does. */
179
- valueFrom?: { secretKeyRef: { name: string; key: string } };
180
- /** Where the value may go. Each entry is `host`, `host:port`, or an `https://` URL (its host and
181
- * port are taken; the path is ignored). Exact DNS names or IP literals; no wildcards. Port
182
- * default 443. Optional ONLY for a known model-key name (`MODEL_KEYS`), where it defaults to the
183
- * one host that provider's calls go to. */
330
+ /** Or a key of a Secret that you manage (Sealed Secrets, External Secrets):
331
+ * `{ secretKeyRef: { name, key } }`. `jr2 up` checks that the Secret and the key exist. Each
332
+ * container that holds the secret mounts that one key as a file, and no other container does. */
333
+ valueFrom?: {
334
+ /** The Secret and the key in it that hold the value. */
335
+ secretKeyRef: {
336
+ /** The name of the Secret. */
337
+ name: string;
338
+ /** The key in the Secret that holds the value. */
339
+ key: string;
340
+ };
341
+ };
342
+ // A known model-key name is a key of `MODEL_KEYS` (model-keys.ts), which holds its default host.
343
+ /** Where the value may go as a Held secret. Each entry is `host`, `host:port`, or an `https://`
344
+ * URL, of which only the host and port count. Write exact DNS names or IP addresses, with no
345
+ * wildcards. The port default is 443. A known model key, such as `ANTHROPIC_API_KEY`, may leave
346
+ * it out: it then goes to the one host that its provider calls. Any other Held secret must name
347
+ * its hosts: `secrets[<i>].hosts is absent, and <NAME> has no single default host`. */
184
348
  hosts?: readonly string[];
185
- /** Request headers the Stand-in is swapped in, lowercase. Default
186
- * `["authorization", "x-api-key", "x-goog-api-key", "api-key"]`. */
349
+ /** The request headers in which the Custodian puts the value in place of the Stand-in,
350
+ * lowercase. Default: `["authorization", "x-api-key", "x-goog-api-key", "api-key"]`. A header
351
+ * that frames or routes the request is refused:
352
+ * `secrets[<i>].headers[<j>] "<header>" cannot carry a held secret`. */
187
353
  headers?: readonly string[];
188
- /** Optional path prefixes the value may be sent to (`/v1/messages`). Absent → every path. A
189
- * gateway key that can reach admin routes (a LiteLLM key that can mint keys) should narrow this:
190
- * a key minted through the Custodian is a key the Agent can take away. */
354
+ // A key minted through the Custodian is a key the Agent can take away.
355
+ /** The path prefixes to which the value may be sent, such as `/v1/messages`. Each starts with
356
+ * `/`. Default: every path. Narrow it for a gateway key that can reach admin routes, such as a
357
+ * LiteLLM key that can make new keys. The Agent could take away a key made that way. */
191
358
  paths?: readonly string[];
192
359
  };
193
360
 
194
- /** The agent-runtime section (ADR-0018): what the stock Harness image consumes alongside the
195
- * Agent definition each Turn hands it (ADR-0049). Moved out of `sandbox` deliberately — `sandbox`
196
- * is pod transport (it still CARRIES this env to the Harness container), but model concerns are
197
- * Harness semantics.
198
- *
199
- * It declares what this instance can REACH — endpoints, credentials, trust — and never WHICH
200
- * model to use (ADR-0018). This config holds no instance-wide `model` default and no Agent
201
- * roster: an Agent is an actor slot on ONE Machine, whose definition rides the Turn (ADR-0049).
202
- * One definition value may still be carried by several Machines, so the variation that matters is
203
- * per-definition and per-invocation, which one global default serves not at all. */
361
+ // The agent-runtime section (ADR-0018): what the stock Harness image consumes alongside the Agent
362
+ // definition each Turn hands it (ADR-0049). Moved out of `sandbox` deliberately — `sandbox` is pod
363
+ // transport (it still CARRIES this env to the Harness container), but model concerns are Harness
364
+ // semantics.
365
+ //
366
+ // It declares what this instance can REACH — endpoints, credentials, trust — and never WHICH
367
+ // model to use (ADR-0018). This config holds no instance-wide `model` default and no Agent
368
+ // roster: an Agent is an actor slot on ONE Machine, whose definition rides the Turn (ADR-0049).
369
+ // One definition value may still be carried by several Machines, so the variation that matters is
370
+ // per-definition and per-invocation, which one global default serves not at all.
371
+ /** What the Harness can reach: the model endpoints, their credentials, and the CAs it trusts. It
372
+ * never says which model an Agent uses: each Agent's definition names its model. */
204
373
  export type HarnessConfig = {
205
- /** Custom model provider, preflighted from inside the cluster by `jr2 up` (ADR-0019). Its
206
- * `apiKey` is a held secret, `JR2_PROVIDER_API_KEY`, bound to `baseUrl`'s host (ADR-0059). */
374
+ // Preflighted from inside the cluster by `jr2 up` (ADR-0019); its `apiKey` is held (ADR-0059).
375
+ /** A custom model provider, such as a vLLM or Ollama endpoint. See {@link HarnessProvider}.
376
+ * Default: none; the Agents use catalog providers only. */
207
377
  provider?: HarnessProvider;
208
- /** pi catalog providers whose calls go somewhere else, keyed by the catalog provider id
209
- * (`anthropic`, `openai`, ...) — a gateway that serves the provider's own API (ADR-0059). */
378
+ // A gateway that serves the provider's own API (ADR-0059).
379
+ /** Catalog providers whose calls go to another endpoint, such as a gateway, keyed by the catalog
380
+ * provider id: `anthropic`, `openai`, and so on. See {@link CatalogReach}. A key that is not a
381
+ * catalog provider with a known model key is refused:
382
+ * `harness.catalog.<id>: "<id>" is not a pi catalog provider jr2 knows a key for`. Default:
383
+ * each provider's own endpoint. */
210
384
  catalog?: Readonly<Record<string, CatalogReach>>;
211
- /** Credentials the Harness uses and never holds (ADR-0059): the Harness container sees a
212
- * Stand-in, and the Custodian puts the value into requests toward the bound hosts alone. A model
213
- * API key belongs here, never in `env`. */
214
- heldSecrets?: readonly HeldSecret[];
215
- /** Env vars for the Harness container. Values read from `process.env`/`.env` are materialized
216
- * into the instance-owned Secret by `jr2 up`. The Agent can read every one of them, so a known
217
- * model key here is refused — it goes in `heldSecrets` (ADR-0059). */
385
+ // Values read from `process.env`/`.env` are materialized into the instance-owned Secret by
386
+ // `jr2 up`. A known model key here is refused (ADR-0059).
387
+ /** Env vars for the Harness container. The Agent can read every one of them, so do not put a
388
+ * credential here. A known model key is refused here. Put it in the top-level `secrets` list.
389
+ * See {@link HarnessEnvVar}. */
218
390
  env?: readonly HarnessEnvVar[];
219
- /** Whole-Secret/ConfigMap env for the Harness container — `envFrom` refs to Secrets YOU manage
220
- * (Sealed Secrets etc.); `jr2 up` preflights that each referenced Secret exists (ADR-0019), and
221
- * refuses one that carries a known model key (ADR-0059). */
391
+ // `jr2 up` preflights that each referenced Secret exists (ADR-0019), and refuses one that
392
+ // carries a known model key (ADR-0059).
393
+ /** Secrets and ConfigMaps that you manage, whose keys all become env vars of the Harness
394
+ * container. See {@link HarnessEnvFromSource}. */
222
395
  envFrom?: readonly HarnessEnvFromSource[];
223
- /** Path to a PEM CA bundle, RELATIVE to the instance folder — commit the file (CA certs are
224
- * public; e.g. an internal CA in front of a LAN vLLM). Only `jr2 up` reads it (host-side): it
225
- * materializes the `jr2-ca` ConfigMap and runs the provider preflight with the same trust. The
226
- * Harness container trusts it, and so does the Custodian when it verifies a bound host
227
- * (ADR-0020, ADR-0059). */
396
+ // Only `jr2 up` reads it (host-side): it materializes the `jr2-ca` ConfigMap and runs the
397
+ // provider preflight with the same trust (ADR-0020, ADR-0059).
398
+ /** A path to a PEM CA bundle, relative to the Instance folder. An example is the CA of an
399
+ * internal endpoint in front of a LAN vLLM. Commit the file: CA certificates are public. `jr2 up`
400
+ * puts it in the `jr2-ca` ConfigMap and uses it when it checks the provider. The Harness
401
+ * container trusts it, and so does the Custodian when it verifies a host that a Held secret is
402
+ * bound to. */
228
403
  caBundle?: string;
229
404
  };
230
405
 
231
- /** One pod-spec toleration, verbatim (ADR-0052): `nodeSelector` and `tolerations` are the raw
232
- * Kubernetes shapes, the stance `HarnessEnvVar` takes for `EnvVar`. */
406
+ // One pod-spec toleration, verbatim (ADR-0052): `nodeSelector` and `tolerations` are the raw
407
+ // Kubernetes shapes, the stance `HarnessEnvVar` takes for `EnvVar`.
408
+ /** One toleration of a Sandbox pod, in the Kubernetes `Toleration` shape: a node taint that the
409
+ * pod tolerates. */
233
410
  export type Toleration = {
411
+ /** The taint key. Leave it out, with `operator: "Exists"`, to tolerate every taint. */
234
412
  key?: string;
413
+ /** `"Equal"` matches a taint whose value is `value`; `"Exists"` matches any value. Default:
414
+ * `"Equal"`. */
235
415
  operator?: "Exists" | "Equal";
416
+ /** The taint value, for `operator: "Equal"`. */
236
417
  value?: string;
418
+ /** The taint effect to tolerate: `"NoSchedule"`, `"PreferNoSchedule"` or `"NoExecute"`. Default:
419
+ * every effect. */
237
420
  effect?: "NoSchedule" | "PreferNoSchedule" | "NoExecute";
421
+ /** For `"NoExecute"`: how many seconds the pod stays on the node after the taint is added.
422
+ * Default: the pod stays. */
238
423
  tolerationSeconds?: number;
239
424
  };
240
425
 
241
- /**
242
- * Where an Instance's Sandboxes may land (ADR-0052). Absent, a Sandbox node is wherever an
243
- * ordinary pod lands — not cordoned, no taint — and no jr2 label is ever required. Both keys ride
244
- * the Sandbox pod verbatim, the Repo cache agent's DaemonSet takes the same two so a cache is only
245
- * ever where a Sandbox can reach it, and `jr2 up` reads the same predicate off the nodes to report
246
- * the set. A Machine says nothing about placement: a node label is a deployment fact (ADR-0050).
247
- * These two are the default class; a per-Machine `classes` map is the deferred extension.
248
- */
426
+ // Where an Instance's Sandboxes may land (ADR-0052). Both keys ride the Sandbox pod verbatim, the
427
+ // Repo cache agent's DaemonSet takes the same two so a cache is only ever where a Sandbox can reach
428
+ // it, and `jr2 up` reads the same predicate off the nodes to report the set. A Machine says nothing
429
+ // about placement: a node label is a deployment fact (ADR-0050). These two are the default class;
430
+ // a per-Machine `classes` map is the deferred extension.
431
+ /** Which nodes are Sandbox nodes: the nodes on which this Instance's Sandboxes may run. Absent, a
432
+ * Sandbox runs wherever an ordinary pod runs: on a node that is not cordoned and has no taint. No
433
+ * jr2 label is ever needed. The Repo cache agent runs on the same nodes, and `jr2 up` reports the
434
+ * set and warns when it is empty. */
249
435
  export type SandboxPlacement = {
250
- /** Pod `spec.nodeSelector`: a node must carry every label. */
436
+ /** The pod's `nodeSelector`: a Sandbox node must have every one of these labels. Default: none. */
251
437
  nodeSelector?: Readonly<Record<string, string>>;
252
- /** Pod `spec.tolerations`, verbatim — no `tolerationSeconds` is added, so a tolerated `NoExecute`
253
- * taint keeps a Sandbox through it for as long as its Lease is renewed. */
438
+ // No `tolerationSeconds` is added.
439
+ /** The pod's `tolerations`, as you write them. Default: none. jr2 adds no `tolerationSeconds`.
440
+ * Thus a tolerated `NoExecute` taint keeps a Sandbox on its node while its Lease is renewed. */
254
441
  tolerations?: readonly Toleration[];
255
442
  };
256
443
 
444
+ // The Instance's Sandbox defaults (ADR-0052, ADR-0060).
445
+ /** The Instance's Sandbox defaults: the Sandbox nodes (see {@link SandboxPlacement}), and the Size
446
+ * of a Workspace that states none. The config load refuses a `sandbox` that is not an object:
447
+ * `<file> at sandbox: expected an object — { nodeSelector?, tolerations?, resources? }`. A key that
448
+ * this type does not have fails the typecheck that `jr2 up` runs. */
449
+ export type SandboxConfig = SandboxPlacement & {
450
+ // NEVER an override of a Size a Machine states: a Size is a fact of the Machine, and its one
451
+ // override path is `customize(machine, { resources })` (ADR-0060).
452
+ /**
453
+ * The Size of a Workspace that states no Size at all: `{ limits: { memory, cpu } }` and nothing
454
+ * else. The kit default fills a field that it leaves out. It never changes a Size that a Machine
455
+ * states: to change that, use `customize(machine, { resources })`. Default: the kit default,
456
+ * memory `2Gi` and cpu `1`. jr2 reserves the whole Size and enforces the cpu limit. See
457
+ * {@link Size} for the refusals, such as
458
+ * `<file> at sandbox.resources.requests is not accepted`.
459
+ */
460
+ resources?: Size;
461
+ };
462
+
463
+ // The PriorityClasses jr2's pods run under (ADR-0060). Absent, `jr2 up` creates two,
464
+ // cluster-scoped, beside the CRDs.
465
+ /**
466
+ * The PriorityClasses that jr2's pods run under. Absent, `jr2 up` creates two: `jr2-control`
467
+ * (value 100000, `PreemptLowerPriority`) and `jr2-sandbox` (value 1000, `preemptionPolicy: Never`).
468
+ * Thus an ordinary pod cannot preempt a live Workspace, and a Sandbox that waits for a node evicts
469
+ * no pod. A cluster with its own scheme names its existing classes here, and `jr2 up` then creates
470
+ * none for that key. The config load refuses any other key:
471
+ * `<file> at priorityClasses<path>: <problem> — { control?, sandbox? }, each the name of an existing PriorityClass`.
472
+ */
473
+ export type PriorityClasses = {
474
+ /** An existing PriorityClass for the Orchestrator, the operator and the Repo cache agent.
475
+ * Default: `jr2-control`, which `jr2 up` creates. */
476
+ control?: string;
477
+ /** An existing PriorityClass for the Sandboxes and the Instance Harness. Default: `jr2-sandbox`,
478
+ * which `jr2 up` creates. */
479
+ sandbox?: string;
480
+ };
481
+
482
+ /**
483
+ * The shape of `jr2.config.ts`: the deployment facts of an Instance, which a Machine cannot carry.
484
+ * It holds the Instance's name, its secrets, how the cluster authenticates to Repos, what the
485
+ * Harness can reach, where Sandboxes run, and where images go. It names no Repo, no model and no
486
+ * Sandbox Image: a Machine names those itself.
487
+ *
488
+ * Every field is optional, and an Instance can run with no `jr2.config.ts`. Write the file as
489
+ * `export default defineConfig({ … })`. When the config loads, a file with no default export is
490
+ * refused: `` <file> has no default export (use `export default defineConfig({…})`) ``. The load also
491
+ * checks the shape of `git.credentials`, `secrets`, `harness.catalog` and `priorityClasses`. It
492
+ * checks that `sandbox` is an object, and checks its `resources`. Each refusal names the field.
493
+ * `jr2 up` checks the rest.
494
+ *
495
+ * @example
496
+ * ```ts
497
+ * import { defineConfig, type JR2Config } from "@jr2/orchestrator";
498
+ *
499
+ * const harness: JR2Config["harness"] = {
500
+ * provider: {
501
+ * id: "vllm",
502
+ * api: "openai-completions",
503
+ * baseUrl: process.env.VLLM_URL ?? "",
504
+ * models: { "Qwen/Qwen3.6-35B-A3B": { contextWindow: 131072, maxTokens: 16384 } },
505
+ * },
506
+ * };
507
+ *
508
+ * export default defineConfig({ name: "my-instance", harness });
509
+ * ```
510
+ *
511
+ * @see docs/reference/config.md
512
+ * @see docs/get-started/connect-a-model.md
513
+ * @see docs/concepts/secrets-and-trust.md
514
+ * @see docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md
515
+ */
257
516
  export type JR2Config = {
258
- /** The instance's identity (ADR-0019): its kube namespace defaults to this (`-n` overrides),
259
- * and `jr2 up` labels every object it owns with it. Default: the instance folder's name. */
517
+ // The instance's identity (ADR-0019).
518
+ /** The Instance's name. Its Kubernetes namespace is this name (`-n` overrides it), and
519
+ * `jr2 up` labels every object that it owns with it. Default: the name of the Instance folder. */
260
520
  name?: string;
261
- /** How the cluster authenticates to Repos, and the fence a per-run url must pass (ADR-0051).
262
- * NOT a list of Repos: a Machine names those itself, through its `workspace()`'s Repo Slots, and
263
- * whether the instance has a data plane at all is read off the registered Machines. */
521
+ // One list for both sides (ADR-0068). Naming is the placement; `hosts` is a Held secret's
522
+ // binding, never its placement: an entry nothing names goes nowhere.
523
+ /** Every secret of the Instance, in one list. A secret goes where something names it. A Held
524
+ * secret goes where a `workspace()` lists it under `secrets` or an Agent's provider reads it. An
525
+ * Effect secret goes where an Effect names it with `secret("name")`. Its `hosts` is a Held
526
+ * secret's binding, never its placement: an entry that nothing names goes nowhere. A model API
527
+ * key goes here, never in `harness.env`. See {@link SecretEntry}. Each entry is checked when the
528
+ * config loads: `<file> at secrets[<i>]<path>: <problem>`, followed by the shape of an entry. */
529
+ secrets?: readonly SecretEntry[];
530
+ // NOT a list of Repos (ADR-0051): a Machine names those itself, through its `workspace()`'s Repo
531
+ // Slots, and whether the instance has a data plane at all is read off the registered Machines.
532
+ /** How the cluster authenticates to Repos, and the fence that a per-run url must pass. It is not
533
+ * a list of Repos: a Machine names its Repos in the Repo Slots of its `workspace()`. See
534
+ * {@link GitConfig}. */
264
535
  git?: GitConfig;
265
- /** Agent-runtime config for the stock Harness (see `HarnessConfig`). */
536
+ /** What the Harness can reach: a custom model provider, gateways for catalog providers, env
537
+ * vars, and CAs. See {@link HarnessConfig}. */
266
538
  harness?: HarnessConfig;
267
- /** Which nodes are this Instance's Sandbox nodes (see `SandboxPlacement`, ADR-0052). Absent →
268
- * wherever an ordinary pod lands. */
269
- sandbox?: SandboxPlacement;
270
- /** Image registry prefix (deployment-varying — resolve from env). Absent → images are
271
- * `kind load`-ed; present → pushed. A non-kind cluster without one fails loudly (ADR-0019). */
539
+ // See `SandboxPlacement` (ADR-0052) and `SandboxConfig` (ADR-0060).
540
+ /** Which nodes are Sandbox nodes, and the Size of a Workspace that states none. Default: a
541
+ * Sandbox runs wherever an ordinary pod runs, at the kit's Size. See {@link SandboxConfig}. */
542
+ sandbox?: SandboxConfig;
543
+ // See `PriorityClasses` (ADR-0060).
544
+ /** Existing PriorityClasses to use in place of the two that `jr2 up` creates. See
545
+ * {@link PriorityClasses}. */
546
+ priorityClasses?: PriorityClasses;
547
+ // Deployment-varying — resolve from env (ADR-0019).
548
+ /** The image registry prefix to which `jr2 up` pushes the images that it builds, such as
549
+ * `registry.example.com/team`. It changes between deployments, so read it from `process.env`
550
+ * (`.env`). Default: none; `jr2 up` loads the images into the kind cluster. On a cluster that is
551
+ * not kind, `jr2 up` then refuses:
552
+ * `` context <context> is not a kind cluster and no `registry` is configured ``. */
272
553
  registry?: string;
273
- /** Where this cluster pulls the PUBLISHED Kit images from (deployment-varying — resolve from
274
- * env). Absent → the canonical home, `ghcr.io/snapwich/jr2-harness:<kitversion>` and friends;
275
- * present → the same tags re-homed to a self-hosted mirror, `<kitRegistry>/jr2-harness:<ver>`,
276
- * for a self-hosted, air-gapped, or mirror-only cluster (ADR-0044). Seeding that mirror is a
277
- * deliberate, instance-less act (`jr2 kit push`), never a side effect of `jr2 up`.
278
- *
279
- * Separate from `registry` on purpose: `registry` addresses images THIS converge builds,
280
- * `kitRegistry` addresses artifacts the kit already published. One key for both would make every
281
- * private-registry user mirror the Kit images they could have pulled from the home. */
554
+ // Deployment-varying — resolve from env. Seeding that mirror is a deliberate, instance-less act
555
+ // (`jr2 kit push`), never a side effect of `jr2 up` (ADR-0044).
556
+ //
557
+ // Separate from `registry` on purpose: `registry` addresses images THIS converge builds,
558
+ // `kitRegistry` addresses artifacts the kit already published. One key for both would make every
559
+ // private-registry user mirror the Kit images they could have pulled from the home.
560
+ /** The registry from which this cluster pulls the published Kit images: a mirror for a cluster
561
+ * that is self-hosted, air-gapped, or mirror-only. Default: none; the cluster pulls from the
562
+ * canonical home, `ghcr.io/snapwich/jr2-harness:<version>` and the others. Set, the cluster pulls
563
+ * the same tags from `<kitRegistry>/jr2-harness:<version>`. Fill the mirror with
564
+ * `jr2 kit push <registry>`; `jr2 up` never pushes to it. Read it from `process.env` (`.env`). */
282
565
  kitRegistry?: string;
283
- /** What `jr2 up` builds its images FOR — docker platform strings, e.g. `["linux/arm64"]`
284
- * (deployment-varying — resolve from env). Absent → derived from the cluster's schedulable nodes
285
- * and intersected with the platforms the kit releases for, which is the answer for every ordinary
286
- * cluster (ADR-0045). Present → ABSOLUTE: derivation is skipped and this is the build set (still
287
- * intersected, so an unpublished platform is a named error, never a silent build).
566
+ // Deployment-varying — resolve from env (ADR-0045). Present → ABSOLUTE: derivation is skipped
567
+ // (still intersected, so an unpublished platform is a named error, never a silent build).
568
+ //
569
+ // The escape hatch for the two cases derivation cannot see: a pool that autoscales from zero (no
570
+ // nodes to read yet), and a polluted set (an amd64 GPU pool beside arm64 workers, where the
571
+ // derived pair would cost a needless qemu cross-build). Not additive or subtractive.
572
+ /** The platforms for which `jr2 up` builds its images, as docker platform strings, such as
573
+ * `["linux/arm64"]`. Default: the platforms of the cluster's schedulable nodes that the kit
574
+ * releases images for. `jr2 up` reports a node platform that it skips.
288
575
  *
289
- * The escape hatch for the two cases derivation cannot see: a pool that autoscales from zero (no
290
- * nodes to read yet), and a polluted set (an amd64 GPU pool beside arm64 workers, where the
291
- * derived pair would cost a needless qemu cross-build). Not additive or subtractive. */
576
+ * Set it when the nodes cannot tell. One case is a node group that scales up from zero. Another
577
+ * is a set of nodes that need no image, such as an amd64 GPU group beside arm64 nodes. When you
578
+ * set it, it replaces the default. It adds to nothing and removes from nothing. The kit releases
579
+ * images for `linux/amd64` and `linux/arm64` only, so `jr2 up` refuses any other:
580
+ * `` `platforms` names <platforms> — the kit releases images for linux/amd64, linux/arm64 and nothing else ``. */
292
581
  platforms?: readonly string[];
293
- /** Operator-layer overrides — kit development territory (ADR-0019). */
582
+ // Kit development territory (ADR-0019).
583
+ /** Settings of the operator layer, for kit development. */
294
584
  operator?: {
295
- /** `false` = `jr2 up` skips the operator layer (run the controller loop yourself). */
585
+ /** `false`: `jr2 up` does not converge the operator, and you run its controller yourself.
586
+ * Default: `true`. */
296
587
  manage?: boolean;
297
588
  };
298
589
  };
299
590
 
300
- /** Identity passthrough that checks a config object against `JR2Config` where it is written —
301
- * excess keys included, at every depth, which is why the parameter is the type and not a generic
302
- * bound by it. */
591
+ // Identity passthrough that checks a config object against `JR2Config` where it is written —
592
+ // excess keys included, at every depth, which is why the parameter is the type and not a generic
593
+ // bound by it.
594
+ /**
595
+ * Declare the config of an Instance: the default export of `jr2.config.ts`. It gives back the
596
+ * object that you pass. Its only work is to check that object against {@link JR2Config} where you
597
+ * write it, at every depth, a misspelled key included. `jr2 up` typechecks the file before it
598
+ * converges.
599
+ *
600
+ * @param c - The Instance's config. See {@link JR2Config} for every field.
601
+ * @returns The same object.
602
+ *
603
+ * @example
604
+ * ```ts
605
+ * import { defineConfig } from "@jr2/orchestrator";
606
+ *
607
+ * export default defineConfig({
608
+ * name: "my-instance",
609
+ * secrets: [{ name: "ANTHROPIC_API_KEY", value: process.env.ANTHROPIC_API_KEY }],
610
+ * git: { credentials: [{ match: "github.com/you/", token: "GITHUB_TOKEN" }] },
611
+ * });
612
+ * ```
613
+ *
614
+ * @see docs/reference/config.md
615
+ * @see docs/get-started/create-an-instance.md
616
+ * @see docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md
617
+ */
303
618
  export function defineConfig(c: JR2Config): JR2Config {
304
619
  return c;
305
620
  }
@@ -308,9 +623,9 @@ export function defineConfig(c: JR2Config): JR2Config {
308
623
  * Load an instance's `jr2.config.ts` (default export). Absent file → undefined (an instance
309
624
  * can boot configless); a file that fails to IMPORT throws — a broken config must be loud,
310
625
  * never silently treated as "no config". The shape checks live here, on the path the host CLI
311
- * and the in-cluster Orchestrator share: `git.credentials`, `harness.heldSecrets` and
312
- * `harness.catalog` are checked at runtime, because an instance is zero-build and nothing
313
- * typechecks this file before Node strips its types and imports it. SHAPE only: a held secret's
626
+ * and the in-cluster Orchestrator share: `git.credentials`, `secrets` and `harness.catalog` are
627
+ * checked at runtime, because an instance is zero-build and nothing typechecks this file before
628
+ * Node strips its types and imports it. SHAPE only: a secret's
314
629
  * `value` reads `.env`, which the in-cluster evaluation does not have, so an absent value is not
315
630
  * this function's to refuse — `jr2 up` checks presence and every held-secret rule host-side
316
631
  * (`heldSecretsOf`, ADR-0059).
@@ -321,12 +636,29 @@ export async function loadConfig(dir: string): Promise<JR2Config | undefined> {
321
636
  const mod = (await import(pathToFileURL(file).href)) as { default?: JR2Config };
322
637
  if (!mod.default) throw new Error(`${file} has no default export (use \`export default defineConfig({…})\`)`);
323
638
  if (mod.default.git !== undefined) checkGit(mod.default.git, file);
324
- if (mod.default.harness !== undefined) checkHeld(mod.default.harness, file);
639
+ if (mod.default.secrets !== undefined) checkSecrets(mod.default.secrets, file);
640
+ if (mod.default.harness !== undefined) checkHarness(mod.default.harness, file);
641
+ if (mod.default.sandbox !== undefined) checkSandbox(mod.default.sandbox, file);
642
+ if (mod.default.priorityClasses !== undefined) checkPriorityClasses(mod.default.priorityClasses, file);
325
643
  return mod.default;
326
644
  }
327
645
 
328
646
  const NonEmpty = z.string().min(1);
329
- const Credential = z.object({ match: NonEmpty, token: NonEmpty.optional(), sshKey: NonEmpty.optional() }).strict();
647
+ const Credential = z
648
+ .object({
649
+ match: NonEmpty,
650
+ token: NonEmpty.optional(),
651
+ sshKey: NonEmpty.optional(),
652
+ push: z.union([z.object({ sshKey: NonEmpty }).strict(), z.object({ token: NonEmpty }).strict()]).optional(),
653
+ sign: z
654
+ .union([
655
+ z.object({ sshKey: NonEmpty, name: NonEmpty, email: NonEmpty }).strict(),
656
+ z.object({ gpgKey: NonEmpty, name: NonEmpty, email: NonEmpty }).strict(),
657
+ ])
658
+ .optional(),
659
+ committer: z.object({ name: NonEmpty, email: NonEmpty }).strict().optional(),
660
+ })
661
+ .strict();
330
662
 
331
663
  /** `git.credentials`' SHAPE, checked at load — by index and field, so a bad entry names itself
332
664
  * (`git.credentials[1].token`) rather than surfacing as a Secret that never matches. */
@@ -345,10 +677,12 @@ function checkGit(git: unknown, where: string): void {
345
677
  }
346
678
  }
347
679
 
348
- const GIT_HINT = "an entry is { match, token?, sshKey? } (ADR-0051)";
680
+ const GIT_HINT =
681
+ "an entry is { match, token?, sshKey?, push?: { sshKey } | { token }, sign?: { sshKey | gpgKey, name, email }, " +
682
+ "committer?: { name, email } } (ADR-0051)";
349
683
 
350
684
  const OptionalText = z.string().optional();
351
- const HeldSecretShape = z
685
+ const SecretShape = z
352
686
  .object({
353
687
  name: NonEmpty,
354
688
  // `.env` values are absent in-cluster, so `undefined` must pass here — presence is `jr2 up`'s.
@@ -364,25 +698,25 @@ const HeldSecretShape = z
364
698
  .strict();
365
699
  const CatalogReachShape = z.object({ baseUrl: z.string() }).strict();
366
700
 
367
- /** `harness.heldSecrets` and `harness.catalog`, SHAPE only (ADR-0059) — by index and field, the
368
- * `checkGit` style, so a bad entry names itself (`harness.heldSecrets[1].hosts`). */
369
- function checkHeld(harness: unknown, where: string): void {
701
+ /** `secrets`' SHAPE (ADR-0059's entry, ADR-0068's list) — by index and field, the `checkGit` style,
702
+ * so a bad entry names itself (`secrets[1].hosts`). */
703
+ function checkSecrets(secrets: unknown, where: string): void {
704
+ if (!Array.isArray(secrets)) throw new Error(`${where} at secrets: expected an array — ${SECRET_HINT}`);
705
+ for (const [i, entry] of secrets.entries()) {
706
+ const parsed = SecretShape.safeParse(entry);
707
+ if (parsed.success) continue;
708
+ const issue = parsed.error.issues[0];
709
+ const at = ["", ...(issue?.path ?? [])].map(String).join(".");
710
+ throw new Error(`${where} at secrets[${i}]${at}: ${issue?.message ?? "invalid"} — ${SECRET_HINT}`);
711
+ }
712
+ }
713
+
714
+ /** `harness.catalog`'s SHAPE (ADR-0059) — by provider id and field, the `checkGit` style. */
715
+ function checkHarness(harness: unknown, where: string): void {
370
716
  if (typeof harness !== "object" || harness === null || Array.isArray(harness)) {
371
717
  throw new Error(`${where} at harness: expected an object`);
372
718
  }
373
- const { heldSecrets, catalog } = harness as { heldSecrets?: unknown; catalog?: unknown };
374
- if (heldSecrets !== undefined) {
375
- if (!Array.isArray(heldSecrets)) {
376
- throw new Error(`${where} at harness.heldSecrets: expected an array — ${HELD_HINT}`);
377
- }
378
- for (const [i, entry] of heldSecrets.entries()) {
379
- const parsed = HeldSecretShape.safeParse(entry);
380
- if (parsed.success) continue;
381
- const issue = parsed.error.issues[0];
382
- const at = ["", ...(issue?.path ?? [])].map(String).join(".");
383
- throw new Error(`${where} at harness.heldSecrets[${i}]${at}: ${issue?.message ?? "invalid"} — ${HELD_HINT}`);
384
- }
385
- }
719
+ const { catalog } = harness as { catalog?: unknown };
386
720
  if (catalog !== undefined) {
387
721
  if (typeof catalog !== "object" || catalog === null || Array.isArray(catalog)) {
388
722
  throw new Error(`${where} at harness.catalog: expected an object keyed by catalog provider id — ${CATALOG_HINT}`);
@@ -397,6 +731,30 @@ function checkHeld(harness: unknown, where: string): void {
397
731
  }
398
732
  }
399
733
 
400
- const HELD_HINT =
401
- "an entry is { name, value? | valueFrom?: { secretKeyRef: { name, key } }, hosts?, headers?, paths? } (ADR-0059)";
734
+ const SECRET_HINT =
735
+ "an entry is { name, value? | valueFrom?: { secretKeyRef: { name, key } }, hosts?, headers?, paths? } (ADR-0059, ADR-0068)";
402
736
  const CATALOG_HINT = "an entry is <provider id>: { baseUrl } (ADR-0059)";
737
+
738
+ /** `sandbox.resources`' SHAPE (ADR-0060), checked at load by field — `requests`, or any key beside
739
+ * the two limits, is refused naming itself (`sandbox.resources.requests`). */
740
+ function checkSandbox(sandbox: unknown, where: string): void {
741
+ if (typeof sandbox !== "object" || sandbox === null || Array.isArray(sandbox)) {
742
+ throw new Error(`${where} at sandbox: expected an object — { nodeSelector?, tolerations?, resources? }`);
743
+ }
744
+ const { resources } = sandbox as { resources?: unknown };
745
+ if (resources !== undefined) assertSize(`${where} at sandbox.resources`, resources);
746
+ }
747
+
748
+ const PriorityClassesShape = z.object({ control: NonEmpty.optional(), sandbox: NonEmpty.optional() }).strict();
749
+
750
+ /** `priorityClasses`' SHAPE (ADR-0060): two optional class names, nothing else. */
751
+ function checkPriorityClasses(classes: unknown, where: string): void {
752
+ const parsed = PriorityClassesShape.safeParse(classes);
753
+ if (parsed.success) return;
754
+ const issue = parsed.error.issues[0];
755
+ const at = ["", ...(issue?.path ?? [])].map(String).join(".");
756
+ throw new Error(
757
+ `${where} at priorityClasses${at}: ${issue?.message ?? "invalid"} — { control?, sandbox? }, each the name of ` +
758
+ "an existing PriorityClass (ADR-0060)",
759
+ );
760
+ }