@jr2/orchestrator 0.5.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 (185) hide show
  1. package/docs/agents-block.md +2 -2
  2. package/docs/concepts/agents-and-turns.md +56 -20
  3. package/docs/concepts/architecture.md +30 -17
  4. package/docs/concepts/charts.md +6 -3
  5. package/docs/concepts/converge.md +3 -0
  6. package/docs/concepts/effects.md +86 -31
  7. package/docs/concepts/machines-and-workflows.md +9 -0
  8. package/docs/concepts/pools-and-sources.md +6 -6
  9. package/docs/concepts/secrets-and-trust.md +126 -42
  10. package/docs/concepts/the-menu.md +3 -0
  11. package/docs/concepts/workspaces.md +129 -51
  12. package/docs/get-started/an-agent-in-a-sandbox.md +21 -19
  13. package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
  14. package/docs/get-started/install.md +4 -0
  15. package/docs/guides/add-tools-to-the-sandbox.md +17 -4
  16. package/docs/guides/bind-open-parts.md +5 -4
  17. package/docs/guides/cut-a-worktree.md +560 -0
  18. package/docs/guides/fix-a-failed-converge.md +1 -1
  19. package/docs/guides/let-an-agent-write-a-chart.md +5 -1
  20. package/docs/guides/look-inside-a-sandbox.md +18 -15
  21. package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
  22. package/docs/guides/push-and-sign-a-branch.md +67 -21
  23. package/docs/guides/run-a-command-or-call-a-url.md +84 -41
  24. package/docs/guides/run-one-worker-per-item.md +19 -18
  25. package/docs/guides/set-an-agents-access-and-dials.md +67 -22
  26. package/docs/guides/size-a-workspace.md +19 -6
  27. package/docs/guides/use-a-hosted-provider.md +6 -0
  28. package/docs/guides/watch-a-run.md +47 -24
  29. package/docs/guides/write-a-workflow.md +1 -1
  30. package/docs/llms.txt +33 -12
  31. package/docs/reference/authoring-surface/agent.md +34 -11
  32. package/docs/reference/authoring-surface/answer.md +19 -3
  33. package/docs/reference/authoring-surface/arg.md +27 -5
  34. package/docs/reference/authoring-surface/branchname.md +15 -0
  35. package/docs/reference/authoring-surface/command.md +36 -21
  36. package/docs/reference/authoring-surface/fail.md +17 -4
  37. package/docs/reference/authoring-surface/file.md +36 -16
  38. package/docs/reference/authoring-surface/http.md +108 -8
  39. package/docs/reference/authoring-surface/isready.md +14 -0
  40. package/docs/reference/authoring-surface/jr2setup.md +1 -1
  41. package/docs/reference/authoring-surface/open.md +36 -5
  42. package/docs/reference/authoring-surface/paramsof.md +22 -4
  43. package/docs/reference/authoring-surface/pool.md +4 -4
  44. package/docs/reference/authoring-surface/push.md +30 -12
  45. package/docs/reference/authoring-surface/readyof.md +21 -0
  46. package/docs/reference/authoring-surface/secret.md +25 -4
  47. package/docs/reference/authoring-surface/source.md +3 -3
  48. package/docs/reference/authoring-surface/type-agentdefinition.md +2 -2
  49. package/docs/reference/authoring-surface/type-agentslots.md +4 -1
  50. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  51. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  52. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  53. package/docs/reference/authoring-surface/type-binding.md +5 -6
  54. package/docs/reference/authoring-surface/type-commandinput.md +4 -3
  55. package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
  56. package/docs/reference/authoring-surface/type-commandoutput.md +22 -4
  57. package/docs/reference/authoring-surface/type-customize.md +4 -2
  58. package/docs/reference/authoring-surface/type-dispatchoutput.md +8 -1
  59. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  60. package/docs/reference/authoring-surface/type-filebody.md +4 -8
  61. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  62. package/docs/reference/authoring-surface/type-hostinjectedinput.md +2 -4
  63. package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
  64. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  65. package/docs/reference/authoring-surface/type-httpoutput.md +7 -1
  66. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  67. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  68. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
  69. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  70. package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
  71. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
  72. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
  73. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  74. package/docs/reference/authoring-surface/type-poolspec.md +2 -2
  75. package/docs/reference/authoring-surface/type-pushinput.md +22 -3
  76. package/docs/reference/authoring-surface/type-pushoutput.md +32 -7
  77. package/docs/reference/authoring-surface/type-reposlot.md +5 -5
  78. package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
  79. package/docs/reference/authoring-surface/type-slothandles.md +5 -13
  80. package/docs/reference/authoring-surface/type-sourcespec.md +10 -10
  81. package/docs/reference/authoring-surface/type-workspaced.md +29 -8
  82. package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
  83. package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
  84. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  85. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  86. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  87. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  88. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  89. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  90. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  91. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  92. package/docs/reference/authoring-surface/workspace.md +65 -37
  93. package/docs/reference/authoring-surface/worktree.md +71 -0
  94. package/docs/reference/authoring-surface.md +30 -10
  95. package/docs/reference/cli.md +1 -1
  96. package/docs/reference/config.md +13 -4
  97. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
  98. package/docs/reference/decisions/0002-duplex-streaming-actor.md +17 -10
  99. package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
  100. package/docs/reference/decisions/0005-sandbox-pod-composition.md +29 -19
  101. package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
  102. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +30 -30
  103. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +35 -17
  104. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +8 -2
  105. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
  106. package/docs/reference/decisions/0017-pool-over-a-source-port.md +6 -4
  107. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +21 -9
  108. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +2 -1
  109. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +8 -9
  110. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +62 -65
  111. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +9 -1
  112. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +13 -12
  113. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +4 -1
  114. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +18 -19
  115. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +8 -8
  116. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +4 -4
  117. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
  118. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
  119. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
  120. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +24 -24
  121. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +5 -3
  122. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +110 -67
  123. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +101 -74
  124. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +5 -6
  125. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +2 -0
  126. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +181 -153
  127. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +74 -63
  128. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +31 -20
  129. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
  130. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
  131. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
  132. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  133. package/docs/reference/decisions.md +3 -0
  134. package/docs/reference/glossary.md +51 -12
  135. package/docs/reference/http-api.md +45 -7
  136. package/docs/reference/machines.md +0 -1
  137. package/docs/why.md +5 -4
  138. package/package.json +3 -3
  139. package/src/actor.ts +161 -35
  140. package/src/agent.ts +6 -6
  141. package/src/ambient.ts +3 -5
  142. package/src/chart.ts +186 -19
  143. package/src/charts.ts +9 -2
  144. package/src/config.ts +16 -6
  145. package/src/custodian.lua +17 -5
  146. package/src/custodian.ts +251 -77
  147. package/src/customize.ts +11 -3
  148. package/src/effect-checks.ts +50 -3
  149. package/src/effect.ts +343 -127
  150. package/src/effector-wire.ts +6 -3
  151. package/src/fail.ts +17 -4
  152. package/src/harness-client.ts +74 -31
  153. package/src/harness-env.ts +33 -0
  154. package/src/held-secrets.ts +125 -16
  155. package/src/holds.ts +3 -0
  156. package/src/http-effect.ts +88 -32
  157. package/src/http.ts +56 -17
  158. package/src/index.ts +15 -1
  159. package/src/internal.ts +1 -0
  160. package/src/kube-client.ts +6 -3
  161. package/src/machine-doc.ts +3 -4
  162. package/src/model-keys.ts +24 -0
  163. package/src/names.ts +19 -6
  164. package/src/notices.ts +443 -54
  165. package/src/open-body.ts +85 -19
  166. package/src/open.ts +36 -5
  167. package/src/parts.ts +40 -28
  168. package/src/party.ts +33 -0
  169. package/src/pick.ts +41 -7
  170. package/src/pool.ts +44 -23
  171. package/src/registration.ts +53 -11
  172. package/src/repo-identity.ts +7 -3
  173. package/src/repos.ts +20 -2
  174. package/src/run-host.ts +106 -20
  175. package/src/sandbox-effect.ts +217 -66
  176. package/src/sandbox-kube.ts +63 -16
  177. package/src/secret-placement.ts +9 -2
  178. package/src/setup.ts +86 -6
  179. package/src/tokens.ts +107 -32
  180. package/src/vocabulary.ts +23 -4
  181. package/src/wire.ts +10 -10
  182. package/src/workspace.ts +264 -253
  183. package/src/worktree.ts +249 -0
  184. package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
  185. package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
@@ -77,6 +77,7 @@ import {
77
77
  type WireBody,
78
78
  } from "./effector-wire.ts";
79
79
  import { boundRun, type AnyActorSystem } from "./registration.ts";
80
+ import type { AnyWorktreePath } from "./worktree.ts";
80
81
  import { open } from "./open.ts";
81
82
  import { effectBindingShape, openEffectLogic, openPartsOf, type OpenOf } from "./open-effect.ts";
82
83
  import { httpFileWith, sandboxEffectorPort } from "./sandbox-effect.ts";
@@ -169,13 +170,25 @@ const openSecretOf = new WeakMap<object, object>();
169
170
  *
170
171
  * @example
171
172
  * ```ts
172
- * import { command, fail, jr2Setup, secret, workspace, type Workspaced } from "@jr2/orchestrator";
173
+ * import {
174
+ * command,
175
+ * fail,
176
+ * isReady,
177
+ * jr2Setup,
178
+ * secret,
179
+ * workspace,
180
+ * worktree,
181
+ * type Workspaced,
182
+ * } from "@jr2/orchestrator";
173
183
  *
174
184
  * type Input = Workspaced<{}, "app">;
175
185
  *
186
+ * const deployTree = (context: Input) => `${context.workspace.repos.app}/deploy` as const;
187
+ *
176
188
  * const body = jr2Setup({
177
189
  * types: {} as { context: Input; input: Input },
178
190
  * actors: {
191
+ * cut: worktree(),
179
192
  * // The command reads the file at $KUBECONFIG. The Agent never reads the secret.
180
193
  * deploy: command(["kubectl", "apply", "-f", "k8s/"], {
181
194
  * env: { KUBECONFIG: secret("kube").path },
@@ -186,17 +199,26 @@ const openSecretOf = new WeakMap<object, object>();
186
199
  * id: "deploy",
187
200
  * context: ({ input }) => input,
188
201
  * on: { "workspace.lost": ".lost" },
189
- * initial: "deploying",
202
+ * initial: "cutting",
190
203
  * states: {
191
- * deploying: { invoke: { src: "deploy", onDone: "done" } },
204
+ * cutting: {
205
+ * invoke: {
206
+ * src: "cut",
207
+ * input: ({ context }) => ({ path: deployTree(context), branch: "deploy" }),
208
+ * onDone: [{ guard: ({ event }) => isReady(event.output), target: "deploying" }, { target: "notCut" }],
209
+ * },
210
+ * },
211
+ * deploying: {
212
+ * invoke: { src: "deploy", input: ({ context }) => ({ worktree: deployTree(context) }), onDone: "done" },
213
+ * },
192
214
  * done: { type: "final" },
215
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
193
216
  * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
194
217
  * },
195
218
  * });
196
219
  *
197
220
  * export const machine = workspace(body, {
198
221
  * repos: { app: { url: "https://github.com/you/app.git" } },
199
- * spec: () => ({ branch: "deploy" }),
200
222
  * });
201
223
  * ```
202
224
  *
@@ -501,15 +523,12 @@ function asText(v: string | Secret | Text): Text {
501
523
  /** A value of a file body's part: as written, or from the Effect's input. */
502
524
  export type FromInput<S, T> = T | ((input: S extends z.ZodType ? z.infer<S> : void) => T);
503
525
 
504
- /** `file(path, { slot?, segments?, contentType? })`: a Worktree file as an `http` body. */
526
+ /** `file(path, { segments?, contentType? })`: a Worktree file as an `http` body. */
505
527
  export type FileBody<S extends z.ZodType | undefined = undefined> = {
506
528
  readonly file: {
507
- /** The file, relative to the Repo Slot's Worktree, or to `/work` in a Workspace with no Repo
508
- * Slot. */
529
+ /** The file, relative to the Worktree that `worktree` of the Effect's input names, or to `/work`
530
+ * in a Workspace with no Repo Slot. */
509
531
  readonly path: FromInput<S, string>;
510
- /** The Repo Slot whose Worktree holds it. Write it only when the Workspace has more than one.
511
- */
512
- readonly slot?: string;
513
532
  /** Path segments that jr2 adds to the end of the url, as `path` of `request` does for a json
514
533
  * or text body. */
515
534
  readonly segments?: FromInput<S, readonly string[]>;
@@ -521,14 +540,18 @@ export type FileBody<S extends z.ZodType | undefined = undefined> = {
521
540
 
522
541
  /**
523
542
  * Make a Worktree file the body of an `http` Effect: `body: file("dist/report.pdf")`. The Effect
524
- * then runs in the Sandbox Effector of the Workspace whose body invokes it. That Effector opens the
525
- * file in the Worktree and sends it. In a Workspace with no Repo Slot, the path is relative to
543
+ * then runs in the Sandbox Effector of the Workspace whose body invokes it. Under a Workspace with
544
+ * Repo Slots, the invoke's input names the Worktree: `input: { worktree: "/work/app/fix" }`. It is a
545
+ * Worktree that a cut of this run made. That Effector opens the file in that Worktree and sends it.
546
+ * In a Workspace with no Repo Slot, the input names no `worktree`, and the path is relative to
526
547
  * `/work`, the folder that its steps share. Use it with `PUT`, `POST` or `PATCH`. `path` and
527
548
  * `segments` can be functions of the input of the Effect.
528
549
  *
529
- * Some paths and files give the output `refused`, and then nothing is sent. The path has a symlink
530
- * in it, has `..`, or is absolute. Or the file is a FIFO, a directory, or a file with more than one
531
- * link.
550
+ * Some inputs, paths and files give the output `refused`, and then nothing is sent. The `worktree`
551
+ * has no cut in this Sandbox: `no cut made <path> in this Sandbox: cut it with worktree(); nothing
552
+ * was sent`. Or the input names a `worktree` in a Workspace with no Repo Slot. Or the path has a
553
+ * symlink in it, has `..`, or is absolute. Or the file is a FIFO, a directory, or a file with more
554
+ * than one link.
532
555
  *
533
556
  * It refuses, when you call it:
534
557
  * - a path that is not a string or a function: `file(path): the path is a string, or a function of
@@ -548,8 +571,13 @@ export type FileBody<S extends z.ZodType | undefined = undefined> = {
548
571
  * there is no Worktree to read; invoke it from a workspace()'s body, or send a json or text body
549
572
  * (ADR-0068)`.
550
573
  *
551
- * @param path - The file, relative to the Worktree of the Repo Slot, or to `/work` in a Workspace
552
- * with no Repo Slot: a string, or a function of the input of the Effect that gives one.
574
+ * And when it starts, under a Workspace with Repo Slots, an input with no `worktree`: ``http
575
+ * "<slot>" reads a Worktree file, under a Workspace with Repo Slots, and its input names no
576
+ * `worktree` ``.
577
+ *
578
+ * @param path - The file, relative to the Worktree that `worktree` of the input names, or to
579
+ * `/work` in a Workspace with no Repo Slot: a string, or a function of the input of the Effect
580
+ * that gives one.
553
581
  * @param options - Optional. Each field says what it does.
554
582
  * @returns The `body` of an `http` Effect.
555
583
  *
@@ -561,17 +589,22 @@ export type FileBody<S extends z.ZodType | undefined = undefined> = {
561
589
  * fail,
562
590
  * file,
563
591
  * http,
592
+ * isReady,
564
593
  * jr2Setup,
565
594
  * secret,
566
595
  * workspace,
596
+ * worktree,
567
597
  * type Workspaced,
568
598
  * } from "@jr2/orchestrator";
569
599
  *
570
600
  * type Input = Workspaced<{}, "app">;
571
601
  *
602
+ * const reportTree = (context: Input) => `${context.workspace.repos.app}/report` as const;
603
+ *
572
604
  * const body = jr2Setup({
573
605
  * types: {} as { context: Input; input: Input },
574
606
  * actors: {
607
+ * cut: worktree(),
575
608
  * build: command(["npm", "run", "report"]),
576
609
  * upload: http({
577
610
  * method: "PUT",
@@ -588,18 +621,29 @@ export type FileBody<S extends z.ZodType | undefined = undefined> = {
588
621
  * id: "report",
589
622
  * context: ({ input }) => input,
590
623
  * on: { "workspace.lost": ".lost" },
591
- * initial: "building",
624
+ * initial: "cutting",
592
625
  * states: {
593
- * building: { invoke: { src: "build", onDone: "uploading" } },
594
- * uploading: { invoke: { src: "upload", onDone: "done" } },
626
+ * cutting: {
627
+ * invoke: {
628
+ * src: "cut",
629
+ * input: ({ context }) => ({ path: reportTree(context), branch: "report" }),
630
+ * onDone: [{ guard: ({ event }) => isReady(event.output), target: "building" }, { target: "notCut" }],
631
+ * },
632
+ * },
633
+ * building: {
634
+ * invoke: { src: "build", input: ({ context }) => ({ worktree: reportTree(context) }), onDone: "uploading" },
635
+ * },
636
+ * uploading: {
637
+ * invoke: { src: "upload", input: ({ context }) => ({ worktree: reportTree(context) }), onDone: "done" },
638
+ * },
595
639
  * done: { type: "final" },
640
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
596
641
  * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
597
642
  * },
598
643
  * });
599
644
  *
600
645
  * export const machine = workspace(body, {
601
646
  * repos: { app: { url: "https://github.com/you/app.git" } },
602
- * spec: () => ({ branch: "report" }),
603
647
  * });
604
648
  * ```
605
649
  *
@@ -610,9 +654,6 @@ export type FileBody<S extends z.ZodType | undefined = undefined> = {
610
654
  export function file<S extends z.ZodType | undefined = undefined>(
611
655
  path: FromInput<S, string>,
612
656
  options: {
613
- /** The Repo Slot whose Worktree holds the file. Write it only when the Workspace has more than
614
- * one. */
615
- slot?: string;
616
657
  /**
617
658
  * Path segments that jr2 adds to the end of the url, as `path` does in `request` for a json or
618
659
  * text body. A list of strings, or a function of the input of the Effect that gives one.
@@ -633,7 +674,6 @@ export function file<S extends z.ZodType | undefined = undefined>(
633
674
  return {
634
675
  file: {
635
676
  path,
636
- ...(options.slot !== undefined ? { slot: options.slot } : {}),
637
677
  ...(options.segments !== undefined ? { segments: options.segments } : {}),
638
678
  ...(options.contentType !== undefined ? { contentType: options.contentType } : {}),
639
679
  },
@@ -723,10 +763,13 @@ export type HttpDefinition<S extends z.ZodType | undefined = undefined> = {
723
763
  request?: (input: S extends z.ZodType ? z.infer<S> : void) => HttpRequestParts;
724
764
  // A file body is an ADR-0067 Effect: the Sandbox Effector of the Workspace sends it.
725
765
  /**
726
- * A Worktree file as the body: `file(path, { slot?, segments?, contentType? })`. The Effect then
727
- * runs in the Sandbox Effector of its Workspace. Do not write `request` beside it: the `segments`
766
+ * A Worktree file as the body: `file(path, { segments?, contentType? })`. The Effect then
767
+ * runs in the Sandbox Effector of its Workspace. Under a Workspace with Repo Slots, the invoke's
768
+ * input names a Worktree that a cut of this run made, in `worktree`; with none, it refuses when
769
+ * it starts: ``http "<slot>" reads a Worktree file, under a Workspace with Repo Slots, and its
770
+ * input names no `worktree` ``. Do not write `request` beside it: the `segments`
728
771
  * of `file` are the path. It refuses another body: ``http({ body }): a body is file(path, {
729
- * slot?, segments?, contentType? }) — json and text come from `request` ``.
772
+ * segments?, contentType? }) — json and text come from `request` ``.
730
773
  */
731
774
  body?: FileBody<S>;
732
775
  /**
@@ -755,6 +798,11 @@ export type HttpDefinition<S extends z.ZodType | undefined = undefined> = {
755
798
  /** @internal */
756
799
  export type HttpInput<S> = S extends z.ZodType ? z.infer<S> : void;
757
800
 
801
+ /** The input of an `http` Effect with a Worktree file as its body. Under a Workspace with Repo
802
+ * Slots, `worktree` names the Worktree. In a Workspace with no Repo Slot, the input has no
803
+ * `worktree`, and the path is relative to `/work`. */
804
+ export type HttpFileInput<S> = (S extends z.ZodType ? z.infer<S> : unknown) & { worktree?: AnyWorktreePath };
805
+
758
806
  const DEFAULT_TIMEOUT_MS = 30_000;
759
807
  const DEFAULT_MAX_BODY = 65_536;
760
808
  /** How long a re-asked request waits, at most, after a transport failure. */
@@ -830,7 +878,7 @@ export function httpParts<S extends z.ZodType | undefined>(def: HttpDefinition<S
830
878
  const body = def.body?.file;
831
879
  if (def.body !== undefined && (typeof body !== "object" || body === null)) {
832
880
  throw new Error(
833
- "http({ body }): a body is file(path, { slot?, segments?, contentType? }) — json and text come from `request`",
881
+ "http({ body }): a body is file(path, { segments?, contentType? }) — json and text come from `request`",
834
882
  );
835
883
  }
836
884
  if (body && !FILE_METHODS.includes(def.method)) {
@@ -866,7 +914,6 @@ export function httpParts<S extends z.ZodType | undefined>(def: HttpDefinition<S
866
914
  ...(body
867
915
  ? {
868
916
  file: {
869
- ...(body.slot !== undefined ? { slot: body.slot } : {}),
870
917
  ...(body.contentType !== undefined ? { contentType: body.contentType } : {}),
871
918
  },
872
919
  }
@@ -1347,8 +1394,9 @@ function parseDuration(d: Duration): number {
1347
1394
  * src: "comment" }`.
1348
1395
  *
1349
1396
  * It runs in the Instance Effector, which `jr2 up` deploys when a Machine has an `http` Effect.
1350
- * With `body: file(...)`, it runs in the Sandbox Effector of the Workspace whose body invokes it.
1351
- * The Effector makes the call. The Orchestrator holds no secret.
1397
+ * With `body: file(...)`, it runs in the Sandbox Effector of the Workspace whose body invokes it,
1398
+ * and under Repo Slots its input names the Worktree in `worktree`. The Effector makes the call. The
1399
+ * Orchestrator holds no secret.
1352
1400
  *
1353
1401
  * Every outcome is output (`HttpOutput`): `response`, `unreachable`, `refused` or `unknown`. A 4xx
1354
1402
  * or 5xx status is output too. jr2 sends the call at most one time, unless you mark it
@@ -1402,6 +1450,14 @@ function parseDuration(d: Duration): number {
1402
1450
  * @see docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md
1403
1451
  * @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
1404
1452
  */
1453
+ export function http<
1454
+ S extends z.ZodType | undefined = undefined,
1455
+ const U extends HttpDefinition<S>["url"] = string,
1456
+ const A extends HttpDefinition<S>["auth"] = undefined,
1457
+ const H extends HttpDefinition<S>["headers"] = undefined,
1458
+ >(
1459
+ def: HttpDefinition<S> & { url: U; auth?: A; headers?: H; body: FileBody<S> },
1460
+ ): EffectLogic<HttpFileInput<S>, HttpOutput> & OpenOf<HttpOpenParts<U, A, H>>;
1405
1461
  export function http<
1406
1462
  S extends z.ZodType | undefined = undefined,
1407
1463
  const U extends HttpDefinition<S>["url"] = string,
package/src/http.ts CHANGED
@@ -61,7 +61,14 @@ import {
61
61
  } from "./registration.ts";
62
62
  import type { RepoStatus } from "./repos.ts";
63
63
  import { UnmountedRepoError, type FetchAnswer } from "./repo-fetch.ts";
64
- import { mayAskForSandbox, mayDeliverToAgent, type Authenticator, type Principal } from "./tokens.ts";
64
+ import {
65
+ isAgentPrincipal,
66
+ mayAskForSandbox,
67
+ mayDeliverToAgent,
68
+ SLOT_HEADER,
69
+ type Authenticator,
70
+ type Principal,
71
+ } from "./tokens.ts";
65
72
  import {
66
73
  DispatchKeyConflictError,
67
74
  keyAnswer,
@@ -387,8 +394,9 @@ export const HTTP_ROUTES: readonly HttpRoute[] = [
387
394
  method: "GET",
388
395
  path: "/agents/:instanceId/surface",
389
396
  band: "authenticated",
390
- summary: "An Agent's Menu and this Turn's Allowed picks. A Sandbox token reads only its own Sandbox's.",
391
- response: "AgentSurfaceView",
397
+ summary:
398
+ "An Agent's Menu, this Turn's Allowed picks and the conversation's pending notices. A control token reads only its own pod's, for the slot it states.",
399
+ response: "AgentSurfaceView & { notices: Array<{ id, notice }> }",
392
400
  stream: false,
393
401
  adrs: ["0013", "0029"],
394
402
  },
@@ -411,11 +419,21 @@ export const HTTP_ROUTES: readonly HttpRoute[] = [
411
419
  stream: false,
412
420
  adrs: ["0013", "0024"],
413
421
  },
422
+ {
423
+ method: "POST",
424
+ path: "/agents/:instanceId/notices",
425
+ band: "authenticated",
426
+ summary: "Acknowledge the notices a step boundary appended to the Turn: they are heard in its conversation.",
427
+ body: "{ heard: string[] }",
428
+ response: "{ heard: number }",
429
+ stream: false,
430
+ adrs: ["0062", "0076"],
431
+ },
414
432
  {
415
433
  method: "POST",
416
434
  path: "/sandboxes/:name/fetch",
417
435
  band: "authenticated",
418
- summary: "Ask the node cache to fetch one Repo that this Sandbox mounts. A Sandbox token asks only for its own.",
436
+ summary: "Ask the node cache to fetch one Repo that this Sandbox mounts. A fetch token asks only for its own.",
419
437
  body: "{ identity }",
420
438
  response: "FetchAnswer: { fetched } | { stale, asOf }",
421
439
  stream: false,
@@ -828,7 +846,8 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
828
846
  * hand every Agent in the cluster a delivery API, which is the hole this ADR exists to close. */
829
847
  const authenticated: MiddlewareHandler<JR2Env> = async (c, next) => {
830
848
  if (!auth) return next(); // no authenticator configured: tests only (see the doc comment)
831
- const principal = auth(bearerOf(c));
849
+ // The slot a control request states (ADR-0076): a control token without one resolves to nothing.
850
+ const principal = auth(bearerOf(c), c.req.header(SLOT_HEADER));
832
851
  if (!principal) return c.json({ error: "unauthorized" }, 401);
833
852
  if (principal.kind === "publisher") return c.json({ error: PUBLISHER_ONLY }, 403);
834
853
  c.set("principal", principal);
@@ -847,7 +866,7 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
847
866
  */
848
867
  const instanceOnly: MiddlewareHandler<JR2Env> = async (c, next) => {
849
868
  if (!auth) return next(); // no authenticator configured: tests only (see the doc comment)
850
- const principal = auth(bearerOf(c));
869
+ const principal = auth(bearerOf(c), c.req.header(SLOT_HEADER));
851
870
  if (!principal) return c.json({ error: "unauthorized" }, 401);
852
871
  if (principal.kind === "publisher") return c.json({ error: PUBLISHER_ONLY }, 403);
853
872
  if (principal.kind !== "instance") {
@@ -1288,11 +1307,11 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
1288
1307
  // the gated state. Unknown gate (never opened, state exited, run settled) → 404; a name the gate
1289
1308
  // doesn't accept, or a payload failing its schema → 400 naming what IS accepted.
1290
1309
  //
1291
- // A Gate is a HUMAN's decision (or a webhook's, or CI's). An Agent holding a Sandbox token is
1292
- // refused here unconditionally — this is the exact line between "the Agent reports an outcome"
1293
- // and "the Agent approves its own PR" (ADR-0013).
1310
+ // A Gate is a HUMAN's decision (or a webhook's, or CI's). Every Agent principal — a control
1311
+ // token or a fetch token — is refused here unconditionally: this is the exact line between "the
1312
+ // Agent reports an outcome" and "the Agent approves its own PR" (ADR-0013, ADR-0076).
1294
1313
  app.post("/runs/:runId/gates/:gate/events", authenticated, async (c) => {
1295
- if (principalOf(c).kind === "sandbox") {
1314
+ if (isAgentPrincipal(principalOf(c))) {
1296
1315
  return c.json({ error: "a Sandbox token cannot deliver to a gate — gates are not on the Agent's surface" }, 403);
1297
1316
  }
1298
1317
  const body = await readJson(c.req.text());
@@ -1419,7 +1438,9 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
1419
1438
  // and delivery are implemented once, in `registration.ts`.
1420
1439
 
1421
1440
  /** Guard: the surface must exist, and this principal must be allowed to speak for it. */
1422
- const agentRegistration = (c: Context<JR2Env, "/agents/:instanceId/surface" | "/agents/:instanceId/events">) => {
1441
+ const agentRegistration = (
1442
+ c: Context<JR2Env, "/agents/:instanceId/surface" | "/agents/:instanceId/events" | "/agents/:instanceId/notices">,
1443
+ ) => {
1423
1444
  const instanceId = c.req.param("instanceId");
1424
1445
  // Only a Turn that is over answers 404 (ADR-0026). While the host is stopping its runs are
1425
1446
  // parked for restore, not over, so every `/agents/*` call answers 503: the Harness re-asks a
@@ -1442,9 +1463,10 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
1442
1463
  // iid) → there is no surface to serve. 404 BEFORE the scope check — a caller with a valid
1443
1464
  // token learns nothing from it that it did not already know.
1444
1465
  if (!surface) return { error: c.json({ error: `no live agent surface for instance "${instanceId}"` }, 404) };
1445
- if (!mayDeliverToAgent(principalOf(c), surface.sandbox)) {
1446
- // A Sandbox token for a DIFFERENT Sandbox (or for a workspace-less run, which no Sandbox
1447
- // owns). This is the check that keeps one feature's coder out of another's reviewer.
1466
+ if (!mayDeliverToAgent(principalOf(c), surface)) {
1467
+ // A control token for a DIFFERENT pod (or for a workspace-less run, which no pod owns), a
1468
+ // stated slot that is not this registration's (ADR-0076), or a fetch token. The first is the
1469
+ // check that keeps one feature's coder out of another's reviewer.
1448
1470
  return { error: c.json({ error: `this token cannot speak for instance "${instanceId}"` }, 403) };
1449
1471
  }
1450
1472
  return { surface };
@@ -1454,11 +1476,28 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
1454
1476
  // semantics, fixed for the conversation — and this Turn's Allowed picks (ADR-0029). A transition
1455
1477
  // swaps the registration, which swaps the Allowed picks, never the Menu; the Harness reads both
1456
1478
  // per Submission and needs no `list_changed`.
1457
- app.get("/agents/:instanceId/surface", authenticated, (c) => {
1479
+ // The same read at a step boundary carries the conversation's pending notices in its audience
1480
+ // (ADR-0062), reserved for this read: the Harness appends them and acknowledges them below.
1481
+ app.get("/agents/:instanceId/surface", authenticated, async (c) => {
1458
1482
  const { surface, error } = agentRegistration(c);
1459
1483
  if (error) return error;
1460
1484
  host.agentSurfaceServed(c.req.param("instanceId"));
1461
- return c.json(surface);
1485
+ const notices = await host.boundaryNotices(c.req.param("instanceId"));
1486
+ return c.json({ ...surface, notices });
1487
+ });
1488
+
1489
+ // The Harness appended a step boundary's notices to the Turn (ADR-0062, ADR-0076): they are heard
1490
+ // in that conversation, ledgered in one save. An id this conversation did not reserve is ignored,
1491
+ // so the call says nothing a caller did not already know.
1492
+ app.post("/agents/:instanceId/notices", authenticated, async (c) => {
1493
+ const { error } = agentRegistration(c);
1494
+ if (error) return error;
1495
+ const body = await readJson(c.req.text());
1496
+ const heard = (body as { heard?: unknown }).heard;
1497
+ if (!Array.isArray(heard) || heard.some((id) => typeof id !== "string")) {
1498
+ return c.json({ error: "an acknowledgment names the notices heard: { heard: string[] }" }, 400);
1499
+ }
1500
+ return c.json({ heard: host.noticesHeard(c.req.param("instanceId"), heard as string[]) });
1462
1501
  });
1463
1502
 
1464
1503
  // The Agent's pick, delivered into the state that invoked it. The receipt describes itself
@@ -1514,7 +1553,7 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
1514
1553
  return c.json({ error: restorePendingMessage(`delivery "${deliveryId}"`) }, 503);
1515
1554
  }
1516
1555
  if (!scope) return c.json({ error: `no delivery "${deliveryId}" for instance "${instanceId}"` }, 404);
1517
- if (!mayDeliverToAgent(principalOf(c), scope.sandbox)) {
1556
+ if (!mayDeliverToAgent(principalOf(c), scope)) {
1518
1557
  return c.json({ error: `this token cannot speak for instance "${instanceId}"` }, 403);
1519
1558
  }
1520
1559
  const receipt = await host.awaitDelivery(instanceId, deliveryId, opts.deliveryWaitMs, c.req.raw.signal);
package/src/index.ts CHANGED
@@ -33,12 +33,25 @@ export type { Binding, RepoSlot } from "./parts.ts";
33
33
  // Effects: work a Machine does outside itself, as an actor slot (ADR-0067, ADR-0068, ADR-0071),
34
34
  // and what an author writes between a pick and that work.
35
35
  export { arg } from "./effect.ts";
36
- export type { CommandOutput } from "./effect.ts";
36
+ export type { CommandOutput, LaterOutcome } from "./effect.ts";
37
37
  export { command } from "./harness-client.ts";
38
38
  export { basic, bearer, file, http, secret, text } from "./http-effect.ts";
39
39
  export type { HttpOutput } from "./effector-wire.ts";
40
40
  export { push } from "./sandbox-effect.ts";
41
41
  export type { PushOutput } from "./sandbox-effect.ts";
42
+ export { branchName, isReady, readyOf, worktree } from "./worktree.ts";
43
+ export type {
44
+ AnyFramePath,
45
+ AnyWorktreePath,
46
+ FramePath,
47
+ WorktreeFound,
48
+ WorktreeInput,
49
+ WorktreeOptions,
50
+ WorktreeOutput,
51
+ WorktreePath,
52
+ WorktreeReady,
53
+ WorktreeRefused,
54
+ } from "./worktree.ts";
42
55
  export { dispatch } from "./dispatch-effect.ts";
43
56
  export type { DispatchOutput } from "./effect.ts";
44
57
  export { answer, fromPick, paramsOf } from "./pick.ts";
@@ -51,6 +64,7 @@ export type { Customize } from "./customize.ts";
51
64
  export type { EffectBinding } from "./open-effect.ts";
52
65
  export type { Size, UserContainer } from "./size.ts";
53
66
  export { pool, source } from "./pool.ts";
67
+ export type { PoolNextInput } from "./pool.ts";
54
68
 
55
69
  // Charts: the Open body a Chart wrapper places in `actors`, and the grammar a planner's pick
56
70
  // declares, whole or narrowed to one wrapper (ADR-0070).
package/src/internal.ts CHANGED
@@ -59,6 +59,7 @@ export * from "./secret-placement.ts";
59
59
  export * from "./git-push.ts";
60
60
  export * from "./git-clone.ts";
61
61
  export * from "./custodian.ts";
62
+ export * from "./harness-env.ts";
62
63
  export * from "./sandbox-kube.ts";
63
64
  export {
64
65
  effectorManifestKey,
@@ -108,7 +108,7 @@ export interface KubeClient {
108
108
  /** Create; an existing object is a {@link KubeError} with status 409. */
109
109
  create<T extends KubeObject>(kind: KubeKind, namespace: string, body: T): Promise<T & KubeObject>;
110
110
  /** Delete; true when there was one to delete, false when it was already gone. */
111
- delete(kind: KubeKind, namespace: string, name: string): Promise<boolean>;
111
+ delete(kind: KubeKind, namespace: string, name: string, opts?: { foreground?: boolean }): Promise<boolean>;
112
112
  /** A JSON merge patch — `null` removes a key. `status` patches the status subresource. Answers
113
113
  * the object as it stands after the patch. */
114
114
  patch<T extends KubeObject>(
@@ -216,9 +216,12 @@ export function kubeClient(opts: KubeClientOptions = {}): KubeClient {
216
216
  });
217
217
  },
218
218
 
219
- delete(kind: KubeKind, ns: string, name: string) {
219
+ delete(kind: KubeKind, ns: string, name: string, opts: { foreground?: boolean } = {}) {
220
220
  return writes(async () => {
221
- const res = await send("DELETE", pathOf(kind, ns, name));
221
+ // Foreground: the object stays until its dependents, a Sandbox's pod, are gone.
222
+ const res = await send("DELETE", pathOf(kind, ns, name), {
223
+ ...(opts.foreground ? { query: { propagationPolicy: "Foreground" } } : {}),
224
+ });
222
225
  if (res.status === 404) {
223
226
  await res.body?.cancel();
224
227
  return false;
@@ -42,9 +42,9 @@ export type MachineTransitionDoc = {
42
42
  };
43
43
 
44
44
  /** An Effect slot's parts, as the doc carries them (ADR-0067): a command's argv (an argv slot reads
45
- * `<name>`), its timeout, and the Repo Slot whose Worktree is its cwd, when it names one. */
45
+ * `<name>`) and its timeout. Its input names its Worktree, so the doc does not (ADR-0075). */
46
46
  export type EffectDoc =
47
- | { kind: "command"; argv: string[]; timeoutMs: number; slot?: string; secrets?: string[]; pin?: true }
47
+ | { kind: "command"; argv: string[]; timeoutMs: number; secrets?: string[]; pin?: true }
48
48
  /** An http Effect (ADR-0068): its method, its fixed target, the secrets it names — never a value
49
49
  * — and `file` when its body is a Worktree file (ADR-0067). */
50
50
  | {
@@ -422,12 +422,11 @@ function effectDoc(parts: EffectParts): EffectDoc {
422
422
  timeoutMs,
423
423
  };
424
424
  }
425
- const { argv, timeoutMs, slot, secrets, pin } = parts;
425
+ const { argv, timeoutMs, secrets, pin } = parts;
426
426
  return {
427
427
  kind: "command",
428
428
  argv: argvText(argv),
429
429
  timeoutMs,
430
- ...(slot ? { slot } : {}),
431
430
  ...(secrets?.length ? { secrets: [...secrets] } : {}),
432
431
  ...(pin ? { pin } : {}),
433
432
  };
package/src/model-keys.ts CHANGED
@@ -50,3 +50,27 @@ export const SIGNING_CREDENTIALS: readonly string[] = [
50
50
  "AWS_PROFILE",
51
51
  "GOOGLE_APPLICATION_CREDENTIALS",
52
52
  ];
53
+
54
+ /**
55
+ * Names whose value is the path of a credential or trust file that the Harness PROCESS reads (the
56
+ * AWS SDK, Google's library), when `harness.env` or `harness.envFrom` sets one (ADR-0076). The
57
+ * Harness runs with the control Stand-in in its memory, so such a file must be one that the Agent's
58
+ * code cannot write: `~/.aws/config` can name a `credential_process`, which the SDK runs. `jr2 up`
59
+ * refuses a path that the domain writes for certain (`DOMAIN_WRITABLE_ROOTS`), and warns on any
60
+ * other, because whether the Sandbox's uid can write a path of the image is the image's, which jr2
61
+ * cannot read. An image's own `ENV` never reaches the Harness process.
62
+ */
63
+ export const CREDENTIAL_FILES: readonly string[] = [
64
+ "AWS_CONFIG_FILE",
65
+ "AWS_SHARED_CREDENTIALS_FILE",
66
+ "AWS_WEB_IDENTITY_TOKEN_FILE",
67
+ "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
68
+ "AWS_CA_BUNDLE",
69
+ "GOOGLE_APPLICATION_CREDENTIALS",
70
+ "GOOGLE_API_CERTIFICATE_CONFIG",
71
+ "CLOUDSDK_CONFIG",
72
+ ];
73
+
74
+ /** Folders of a Sandbox's Harness container that the domain writes in every pod: the Worktree root,
75
+ * the temporary folders, `/dev/shm`, and the HOME that jr2 gives an image with no `USER`. */
76
+ export const DOMAIN_WRITABLE_ROOTS: readonly string[] = ["/work", "/tmp", "/var/tmp", "/dev/shm", "/home/jr2"];
package/src/names.ts CHANGED
@@ -25,8 +25,10 @@ export const EFFECTOR_PORT = 8082;
25
25
 
26
26
  /** Where a Harness persists its conversations, and the env var that tells it so (ADR-0031): one
27
27
  * fixed path in both placements — the Instance Harness's PersistentVolumeClaim (deploy.ts) and a
28
- * Sandbox's emptyDir (sandbox-kube.ts), each mounted into the Harness container alone. */
29
- export const CONVERSATIONS_DIR = "/conversations";
28
+ * Sandbox's emptyDir (sandbox-kube.ts), each mounted into the Harness container alone. It is under
29
+ * `/run/jr2`, as each path that the Harness holds alone is, so no process that the Harness confines
30
+ * can read an Agent's history or write into it (ADR-0076). */
31
+ export const CONVERSATIONS_DIR = "/run/jr2/conversations";
30
32
  export const CONVERSATIONS_DIR_ENV = "JR2_CONVERSATIONS_DIR";
31
33
  export const CONVERSATIONS_VOLUME = "conversations";
32
34
 
@@ -98,12 +100,23 @@ export const sandboxCustodianConfigMap = (sandbox: string): string => `${sandbox
98
100
  * (`workspaceHeldPod`), so the two cannot collide. */
99
101
  export const INSTANCE_HARNESS_HELD_POD = "instance";
100
102
 
101
- /** The Custodian's listeners (ADR-0059), all taken on every Harness pod (ADR-0005): the Harness's
102
- * HTTPS proxy on loopback, the kubelet's probe, and the control listener the Menu and the ask ride
103
- * to the Orchestrator, at the address `jr2-upload-pack` defaults to (ADR-0053). */
103
+ /** The Custodian's ports (ADR-0059), all taken on every Harness pod (ADR-0005): the Harness's
104
+ * HTTPS proxy on loopback, the kubelet's probe, and the fetch listener that the ask rides to the
105
+ * Orchestrator, at the address `jr2-upload-pack` defaults to (ADR-0053). The agent routes are not
106
+ * on a port: they are on the control socket ({@link CUSTODIAN_CONTROL_SOCKET}, ADR-0076). */
104
107
  export const CUSTODIAN_PORT = 15001;
105
108
  export const CUSTODIAN_HEALTH_PORT = 15021;
106
- export const CUSTODIAN_CONTROL_PORT = 8081;
109
+ export const CUSTODIAN_FETCH_PORT = 8081;
110
+
111
+ /** The folder of the Custodian's control socket (ADR-0076): an emptyDir that the Harness container
112
+ * and the Custodian container alone mount, at this path in both. It is under `/run/jr2`, so no
113
+ * process that the Harness confines can make a socket here, and none can take the control
114
+ * listener's place while the Custodian restarts. The Custodian writes it; the Harness mounts it
115
+ * read-only, which still lets it connect to the socket. */
116
+ export const CUSTODIAN_SOCKET_DIR = "/run/jr2/custodian";
117
+ /** The control socket: the Custodian's listener for the four agent routes, which carry the control
118
+ * Stand-in (ADR-0059, ADR-0076). */
119
+ export const CUSTODIAN_CONTROL_SOCKET = `${CUSTODIAN_SOCKET_DIR}/control.sock`;
107
120
 
108
121
  /** The scaffold's default deploy-key Secret name (ADR-0047/0051): what `jr2 init`'s wildcard
109
122
  * `git.credentials` entry names as its `sshKey`, and what `jr2 up`'s ssh offer generates into when