@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
@@ -43,7 +43,7 @@
43
43
  // containers:
44
44
  //
45
45
  // volume runtime the kit's Harness image, as an `image` volume → /opt/jr2
46
- // volume conversations an emptyDir → /conversations, the Harness container's alone
46
+ // volume conversations an emptyDir → /run/jr2/conversations, the Harness container's alone
47
47
  // (ADR-0031: a Harness restart inside the pod keeps its conversations)
48
48
  // initContainer preflight the USER'S image + that volume → ADR-0037's probe, the thing that
49
49
  // proves a registry ref, whose first appearance is this provision
@@ -83,11 +83,14 @@ import {
83
83
  custodianBootstrap,
84
84
  custodianComposition,
85
85
  custodianScript,
86
+ CREDENTIAL_KEYS,
87
+ heldStandInKey,
86
88
  CUSTODIAN_BOOTSTRAP_KEY,
87
89
  CUSTODIAN_SCRIPT_KEY,
88
90
  type CustodianComposition,
89
91
  } from "./custodian.ts";
90
- import { readHeldManifest, workspaceHeldPod, type HeldManifest } from "./held-secrets.ts";
92
+ import { heldStandIn, readHeldManifest, workspaceHeldPod, type HeldManifest } from "./held-secrets.ts";
93
+ import { markHarnessEnv } from "./harness-env.ts";
91
94
  import {
92
95
  effectorManifestKey,
93
96
  readSandboxEffectorManifest,
@@ -133,10 +136,14 @@ import {
133
136
  type SandboxObject,
134
137
  type SandboxWatch,
135
138
  } from "./sandbox-watch.ts";
136
- import { effectorKey, harnessToken, harnessTokenDigest, sandboxToken } from "./tokens.ts";
139
+ import { controlStandIn, controlToken, effectorKey, harnessToken, harnessTokenDigest, sandboxToken } from "./tokens.ts";
137
140
  import type { AttachError, AttachRequest, AttachResponse } from "./wire.ts";
138
141
  import type { Continuity, MemoryKill, PlacingWait, ProvisionedRepo, SandboxLoss, SandboxPort } from "./workspace.ts";
139
142
 
143
+ /** A Secret's `data`: each value base64'd. */
144
+ const base64Values = (values: Record<string, string>): Record<string, string> =>
145
+ Object.fromEntries(Object.entries(values).map(([k, v]) => [k, Buffer.from(v).toString("base64")]));
146
+
140
147
  /** Where jr2's runtime lands in every container that gets it (ADR-0037). `/opt/jr2` and not `/app`
141
148
  * because a stranger's base may already use `/app`, and one layout must serve both the stock
142
149
  * Harness image and an arbitrary Sandbox Image. It is a PUBLISHED surface: `bin/` beside `lib/`
@@ -156,11 +163,21 @@ const RUNTIME_VOLUME_MOUNT = { name: RUNTIME_VOLUME, mountPath: RUNTIME_MOUNT, s
156
163
  /** The primary container's command (ADR-0037). Absolute, so it never depends on the image's
157
164
  * `WORKDIR`, and identical to the stock Harness image's own `CMD` — one runtime, two placements.
158
165
  * `tini` is PID 1 (ADR-0061): the Agent's processes are the Harness's children, and a tree the
159
- * memory guard kills must leave no zombies behind. */
166
+ * memory guard kills must leave no zombies behind. The Harness holds the Stand-ins in its memory,
167
+ * and below Landlock ABI 6 a confined child can still signal it (ADR-0076): `effector nocore`
168
+ * starts it with no core file, so a dumping signal writes nothing a child reads, and
169
+ * `--disable-sigusr1` keeps SIGUSR1 from opening Node's inspector, which would run a child's code
170
+ * in the Harness. `--harness-env` gives the Harness process only the names jr2 composed, less each
171
+ * name that loads code from a path, and the domain the whole start env (`harness-env.ts`). */
160
172
  const HARNESS_COMMAND = [
161
173
  `${RUNTIME_MOUNT}/bin/tini`,
162
174
  "--",
175
+ `${RUNTIME_MOUNT}/bin/effector`,
176
+ "nocore",
177
+ "--harness-env",
178
+ "--",
163
179
  `${RUNTIME_MOUNT}/bin/node`,
180
+ "--disable-sigusr1",
164
181
  `${RUNTIME_MOUNT}/src/main.ts`,
165
182
  ];
166
183
 
@@ -834,8 +851,14 @@ export function kubeSandbox(opts: KubeSandboxOptions = {}): SandboxPort {
834
851
  // Never empty: the Custodian's address is unconditional. The seat's own vars (the fallback
835
852
  // `HOME`) come FIRST, so the instance's `harness.env` can still override them the way it
836
853
  // overrides anything the image set.
837
- env: [...seat.env, ...harnessEnv(req.name, custodian, cpuHintEnv())],
838
- ...(cfg().envFrom?.length ? { envFrom: cfg().envFrom } : {}),
854
+ // Marked (`harness-env.ts`, ADR-0076): the Harness's start tells jr2's names from the image's.
855
+ ...(() => {
856
+ const marked = markHarnessEnv(
857
+ [...seat.env, ...harnessEnv(req.name, custodian, cpuHintEnv())],
858
+ cfg().envFrom ?? [],
859
+ );
860
+ return { env: marked.env, ...(marked.envFrom.length ? { envFrom: marked.envFrom } : {}) };
861
+ })(),
839
862
  // What the AGENT gets: an address on its own loopback and a Stand-in for every credential,
840
863
  // and no credential anywhere. The Custodian beside it holds them (ADR-0013, ADR-0059).
841
864
  sidecars,
@@ -902,7 +925,7 @@ export function kubeSandbox(opts: KubeSandboxOptions = {}): SandboxPort {
902
925
  * `data`, not `stringData`: a server-side apply owns the fields it names, and `stringData` is
903
926
  * write-only — it never reads back as the field applied.
904
927
  */
905
- const applyTokenSecret = async (name: string, owner: KubeObject): Promise<void> => {
928
+ const applyTokenSecret = async (name: string, owner: KubeObject, manifest: HeldManifest): Promise<void> => {
906
929
  if (!opts.signingKey) throw new Error("kubeSandbox: a Custodian needs a signingKey to mint its Sandbox token");
907
930
  const uid = owner.metadata?.uid;
908
931
  if (!uid) throw new Error(`Sandbox "${name}": the CR's apply answered with no uid to own its token Secret`);
@@ -925,7 +948,19 @@ export function kubeSandbox(opts: KubeSandboxOptions = {}): SandboxPort {
925
948
  ],
926
949
  },
927
950
  type: "Opaque",
928
- data: { JR2_SANDBOX_TOKEN: Buffer.from(sandboxToken(opts.signingKey, name)).toString("base64") },
951
+ // The pod's credentials (ADR-0076, C1, C1b): its fetch token and its control token, which the
952
+ // Custodian alone mounts; its secret control Stand-in; and one secret Stand-in per held secret
953
+ // the pod's manifest names, which the Custodian (for its swap) and the Harness container (as
954
+ // files under `/run/jr2/`) mount. Every one a signed name, so a re-provision re-applies the
955
+ // same values. Never a ConfigMap, the CR, an env, an event or a log line.
956
+ data: base64Values({
957
+ [CREDENTIAL_KEYS.fetchToken]: sandboxToken(opts.signingKey, name),
958
+ [CREDENTIAL_KEYS.controlToken]: controlToken(opts.signingKey, name),
959
+ [CREDENTIAL_KEYS.controlStandIn]: controlStandIn(opts.signingKey, name),
960
+ ...Object.fromEntries(
961
+ manifest.secrets.map((s) => [heldStandInKey(s.name), heldStandIn(opts.signingKey!, name, s.name)]),
962
+ ),
963
+ }),
929
964
  });
930
965
  };
931
966
 
@@ -1030,7 +1065,7 @@ export function kubeSandbox(opts: KubeSandboxOptions = {}): SandboxPort {
1030
1065
  },
1031
1066
  data: {
1032
1067
  [CUSTODIAN_BOOTSTRAP_KEY]: JSON.stringify(custodianBootstrap(manifest, orchestrator)),
1033
- [CUSTODIAN_SCRIPT_KEY]: custodianScript(manifest),
1068
+ [CUSTODIAN_SCRIPT_KEY]: custodianScript(manifest, { fetch: true }),
1034
1069
  },
1035
1070
  });
1036
1071
  };
@@ -1257,8 +1292,9 @@ export function kubeSandbox(opts: KubeSandboxOptions = {}): SandboxPort {
1257
1292
  image: refs.custodian,
1258
1293
  pod,
1259
1294
  config: sandboxCustodianConfigMap(req.name),
1260
- token: { secret: secretName(req.name), key: "JR2_SANDBOX_TOKEN" },
1295
+ credentials: { secret: secretName(req.name) },
1261
1296
  sandbox: req.name,
1297
+ ...(req.secrets?.length ? { granted: req.secrets } : {}),
1262
1298
  caBundle: cfg().caBundle === true,
1263
1299
  resources: split.custodian,
1264
1300
  });
@@ -1287,7 +1323,7 @@ export function kubeSandbox(opts: KubeSandboxOptions = {}): SandboxPort {
1287
1323
  sandboxes();
1288
1324
  const applied = await kube().apply(SANDBOXES, ns, cr).catch(noOperator(req.name, "Sandbox"));
1289
1325
  // After the CR: its uid owns both, so they go when it goes.
1290
- await applyTokenSecret(req.name, applied);
1326
+ await applyTokenSecret(req.name, applied, manifest);
1291
1327
  await applyCustodianConfig(req.name, applied, manifest);
1292
1328
  if (effectorManifest) {
1293
1329
  await applyEffectorKey(req.name, applied);
@@ -1403,12 +1439,10 @@ export function kubeSandbox(opts: KubeSandboxOptions = {}): SandboxPort {
1403
1439
  const { identity, key } = repoIdentity(r.url);
1404
1440
  return { slot: r.slot, url: r.url, identity, key, ...(r.ref !== undefined ? { ref: r.ref } : {}) };
1405
1441
  }),
1406
- branch: req.spec.branch,
1407
- ...(req.spec.reviewSha ? { reviewSha: req.spec.reviewSha } : {}),
1408
1442
  };
1409
- const { repos, review } = await postAttach(req.name, endpoint, body);
1443
+ const { repos, bases } = await postAttach(req.name, endpoint, body);
1410
1444
  const stale = staleSlots(req.repos, staleByName.get(req.name));
1411
- return { repos, ...(review ? { review } : {}), ...(stale ? { stale } : {}) };
1445
+ return { repos, bases, ...(stale ? { stale } : {}) };
1412
1446
  },
1413
1447
 
1414
1448
  leaseIntervalMs,
@@ -1458,7 +1492,20 @@ export function kubeSandbox(opts: KubeSandboxOptions = {}): SandboxPort {
1458
1492
  async destroy(name) {
1459
1493
  staleByName.delete(name);
1460
1494
  // The token Secret is the CR's child from birth, so deleting the CR reaps it (ADR-0001).
1461
- await kube().delete(SANDBOXES, ns, name);
1495
+ await kube().delete(SANDBOXES, ns, name, { foreground: true });
1496
+ // A retry of the same invoke id takes the same name, and the pod takes it too: a new Sandbox
1497
+ // must not find the old pod on its way out, and read `Lost` (ADR-0075). So the wait is
1498
+ // for the pod as well: foreground keeps the CR until the pod is gone. Bounded: a stuck finalizer
1499
+ // is the operator's to report, and the run does not wait on it for ever.
1500
+ for (let i = 0; i < 120; i++) {
1501
+ if (
1502
+ (await kube()
1503
+ .get<SandboxObject>(SANDBOXES, ns, name)
1504
+ .catch(() => undefined)) === undefined
1505
+ )
1506
+ return;
1507
+ await new Promise((resolve) => setTimeout(resolve, 500));
1508
+ }
1462
1509
  },
1463
1510
  };
1464
1511
  }
@@ -151,8 +151,8 @@ export function refuseEffectSecrets(config: JR2Config, walk: PlacementWalk): voi
151
151
  /**
152
152
  * A Workspace with no Repo Slot (ADR-0051) has no branch and no commit, so the two Sandbox Effector kinds that
153
153
  * run on a commit of a slot's branch have none: a `push`, and a `command` with an Effect secret
154
- * (ADR-0067). Refused here, before anything is built, as the invoke would refuse them mid-run. A
155
- * file-body `http` reads `/work` and stays.
154
+ * (ADR-0067). A `worktree()` has no clone to cut from (ADR-0075). Refused here, before anything is
155
+ * built, as the invoke would refuse them mid-run. A file-body `http` reads `/work` and stays.
156
156
  */
157
157
  function refuseWithNoSlot(ws: PlacedWorkspace): void {
158
158
  if (!ws.pod.noSlot) return;
@@ -165,6 +165,13 @@ function refuseWithNoSlot(ws: PlacedWorkspace): void {
165
165
  `Slot: ${why} (ADR-0051)`,
166
166
  );
167
167
  }
168
+ const [cut] = ws.pod.cuts ?? [];
169
+ if (cut !== undefined) {
170
+ throw new Error(
171
+ `workflow "${ws.workflow}": worktree "${cut}" runs in ${workspaceLabel(ws)}, which has no Repo Slot: ` +
172
+ `${needsASlot("worktree")} (ADR-0075)`,
173
+ );
174
+ }
168
175
  }
169
176
 
170
177
  /**
package/src/setup.ts CHANGED
@@ -29,6 +29,7 @@ import { randomUUID } from "node:crypto";
29
29
  import {
30
30
  setup,
31
31
  type ActionFunction,
32
+ type ActorLogic,
32
33
  type AnyActorRef,
33
34
  type AnyStateMachine,
34
35
  type DelayConfig,
@@ -39,6 +40,7 @@ import {
39
40
  type NonReducibleUnknown,
40
41
  type ParameterizedObject,
41
42
  type SetupReturn,
43
+ type Snapshot,
42
44
  type UnknownActorLogic,
43
45
  } from "xstate";
44
46
  import type { z } from "zod";
@@ -46,11 +48,12 @@ import { eventMap, type EventDef, type EventFrom, type Sender } from "@jr2/agent
46
48
  import type { AgentRunInput, AgentTurnInput, FaultTelemetry } from "./actor.ts";
47
49
  import { isAgent } from "./agent.ts";
48
50
  import { isEffect } from "./effect.ts";
49
- import { effectChecks } from "./effect-checks.ts";
51
+ import type { AnyFramePath, FramePath, WorktreePath, WorktreeReady } from "./worktree.ts";
52
+ import { effectChecks, refuseUnroutedOutcome } from "./effect-checks.ts";
50
53
  import { gate } from "./gate.ts";
51
54
  import { attachLent, isOpenBody, openBodyDeclarationOf, type LentParts } from "./open-body.ts";
52
55
  import { refuseInlineParts } from "./parts.ts";
53
- import { partyWarnings, refuseUnknownSentBy, SENT_BY, sentByGuard } from "./party.ts";
56
+ import { partyWarnings, refuseSlotActiveTwice, refuseUnknownSentBy, SENT_BY, sentByGuard } from "./party.ts";
54
57
  import { refuseLostOutput } from "./run-output.ts";
55
58
  import { refuseUnroutedAgentFault } from "./mechanism-routes.ts";
56
59
  import { boundEpoch, boundRunId, continuedIid } from "./registration.ts";
@@ -130,6 +133,74 @@ type ToProvidedActor<TActors extends Record<string, UnknownActorLogic>> = {
130
133
  [K in keyof TActors & string]: { src: K; logic: TActors[K]; id: string | undefined };
131
134
  }[keyof TActors & string];
132
135
 
136
+ // The types below carry the body's Repo Slots to every Worktree input.
137
+ /** @internal */
138
+ type NoHandles = { readonly noHandles: true };
139
+ /** `S`, the slots of the body, or {@link NoHandles} when its input and context hold no handles.
140
+ * @internal */
141
+ type RepoSlotsOf<TInput, TContext> = TInput extends { workspace: { repos: infer R } }
142
+ ? keyof R & string
143
+ : TContext extends { workspace: { repos: infer R } }
144
+ ? keyof R & string
145
+ : NoHandles;
146
+ /** The Worktrees of the slots `S` that this key takes: a `push({ slot })` keeps to its own slot.
147
+ * @internal */
148
+ type PathIn<W, S> = S extends string ? (`/work/${S}/x` extends NonNullable<W> ? WorktreePath<S> : never) : never;
149
+ /** One arm of an input. A key that names a Worktree or a Frame is REQUIRED under a body with Repo
150
+ * Slots, so that no mix of `cwd`, `worktree` and `path` lands in `/work`. Under a body with none,
151
+ * `worktree` goes: there is no Worktree. A Turn's arm that carries no kit path (the run's own form)
152
+ * and every other key stay.
153
+ * @internal */
154
+ type BindArm<I, S> = I extends object
155
+ ? "worktree" extends keyof I
156
+ ? [S] extends [never]
157
+ ? Omit<I, "worktree"> & { worktree?: never }
158
+ : Omit<I, "worktree"> & { worktree: PathIn<I["worktree"], S> }
159
+ : "cwd" extends keyof I
160
+ ? [NonNullable<I["cwd"]>] extends [AnyFramePath]
161
+ ? [S] extends [never]
162
+ ? I
163
+ : Omit<I, "cwd"> & { cwd: FramePath<S & string> }
164
+ : I
165
+ : "path" extends keyof I
166
+ ? [S] extends [never]
167
+ ? I
168
+ : Omit<I, "path"> & { path: PathIn<I["path"], S> }
169
+ : I
170
+ : I;
171
+ /** Does this input name a Worktree? @internal */
172
+ type NamesWorktree<I> = I extends object ? ("worktree" extends keyof I ? true : never) : never;
173
+ /** @internal */
174
+ type BindInput<I, S> = [NamesWorktree<I>] extends [never]
175
+ ? BindArm<I, S>
176
+ : [S] extends [never]
177
+ ? BindArm<I, S>
178
+ : Exclude<BindArm<I, S>, void | undefined>;
179
+ /** A cut's `ready` output with the body's slots. @internal */
180
+ type BindOutput<O, S> = O extends WorktreeReady ? WorktreeReady<S & string> : O;
181
+ /** @internal */
182
+ type BindSnapshot<Snap, S> = Snap extends { output: infer O }
183
+ ? [O] extends [BindOutput<O, S>]
184
+ ? Snap
185
+ : Omit<Snap, "output"> & { output: BindOutput<O, S> }
186
+ : Snap;
187
+ /** @internal */
188
+ type BindLogic<L extends UnknownActorLogic, S> = L extends AnyStateMachine
189
+ ? L
190
+ : ([S] extends [never] ? false : [S] extends [NoHandles] ? true : false) extends true
191
+ ? L
192
+ : L extends { readonly definition: { readonly workspace: "none" } }
193
+ ? L
194
+ : L extends ActorLogic<infer Snap, infer Ev, infer I, infer Sys, infer Em>
195
+ ? BindSnapshot<Snap, S> extends infer B extends Snapshot<unknown>
196
+ ? Omit<L, keyof ActorLogic<Snap, Ev, I, Sys, Em>> & ActorLogic<B, Ev, BindInput<I, S>, any, Em>
197
+ : never
198
+ : L;
199
+ /** @internal */
200
+ type BindActors<A extends Record<string, UnknownActorLogic>, TInput, TContext> = {
201
+ [K in keyof A]: BindLogic<A[K], RepoSlotsOf<TInput, TContext>>;
202
+ };
203
+
133
204
  /**
134
205
  * Set up a Machine that takes workflow events: the jr2 form of xstate's `setup()`. Write a Machine
135
206
  * with it when an Agent or a Gate sends events into the Machine, or when the Machine declares a
@@ -321,7 +392,7 @@ export function jr2Setup<
321
392
  ): SetupReturn<
322
393
  TContext,
323
394
  WorkflowEvent<TDefs>,
324
- MergedActors<TActors>,
395
+ MergedActors<BindActors<TActors, TInput, TContext>>,
325
396
  {},
326
397
  TActions,
327
398
  TGuards & SentByGuards,
@@ -411,13 +482,20 @@ export function jr2Setup<
411
482
  // (ADR-0074): the run would wait for ever. Not in a compiled Chart: there the route is optional,
412
483
  // and the Open body sends the event that no transition takes to the Machine that invoked it (ADR-0070).
413
484
  if (compiledChart !== true) refuseUnroutedAgentFault(machine);
485
+ // An Effect's `onDone` with no last arm that always matches (ADR-0075): the run would stay in the
486
+ // state and show no fault. A compiled Chart comes here too, so the door refuses it.
487
+ refuseUnroutedOutcome(machine);
414
488
  refuseParallelBelowAgents(machine, def.actors ?? {});
489
+ // One slot, two Turns at once (ADR-0015): one sender and one conversation cannot hold both.
490
+ refuseSlotActiveTwice(machine);
415
491
  refuseUnknownSentBy(machine);
416
492
  // Before the Effect checks: they ask who may send a writing (`mayTake`), which reads its audience.
417
493
  attachVocabulary(machine, defs);
418
494
  // The Effect checks (ADR-0067): a pick param nothing reads, a `fill` that overwrites one, a
419
495
  // `fromPick` of another pick — refused; a mapper the probe cannot run — warned.
420
- const effects = effectChecks(machine, def.actors ?? {}, defs);
496
+ // A Chart's door reads each reference itself (rule 7), and it refuses every load warning: the
497
+ // walk's "I could not run this" says nothing of a Chart's `{{...}}` mapper or its `set`.
498
+ const effects = effectChecks(machine, def.actors ?? {}, defs, compiledChart === true);
421
499
  if (effects.errors.length) throw new Error(`machine "${machine.id}": ${effects.errors.join("; ")}`);
422
500
 
423
501
  const idOf = (path: readonly string[]): string => stateAt(machine, path).id;
@@ -441,7 +519,7 @@ export function jr2Setup<
441
519
  return { ...inner, createMachine } as unknown as SetupReturn<
442
520
  TContext,
443
521
  WorkflowEvent<TDefs>,
444
- MergedActors<TActors>,
522
+ MergedActors<BindActors<TActors, TInput, TContext>>,
445
523
  {},
446
524
  TActions,
447
525
  TGuards & SentByGuards,
@@ -1066,7 +1144,7 @@ function refuseUnknownKeys(consumer: object, agentName: string): void {
1066
1144
  /** Wrap an Agent slot's invoke input: name the Agent (the slot key), append the state's derived set
1067
1145
  * and the Agent's Menu, and finalize the mechanism fields. */
1068
1146
  function wrapAgentInput(orig: unknown, derived: string[], agentName: string, menuOf: (agentName: string) => string[]) {
1069
- return (args: InputArgs): AgentRunInput => {
1147
+ const wrapped = (args: InputArgs): AgentRunInput => {
1070
1148
  const consumer = resolveInput(orig, args) as Partial<AgentTurnInput & AgentRunInput>;
1071
1149
  refuseUnknownKeys(consumer, agentName);
1072
1150
  return {
@@ -1097,6 +1175,8 @@ function wrapAgentInput(orig: unknown, derived: string[], agentName: string, men
1097
1175
  menu: menuOf(agentName),
1098
1176
  };
1099
1177
  };
1178
+ // The walk reads what the author wrote: `workspace()`, for a literal `cwd`.
1179
+ return Object.assign(wrapped, { consumerInput: orig });
1100
1180
  }
1101
1181
 
1102
1182
  /** Wrap a gate invoke input: append the derived accepted set. */
package/src/tokens.ts CHANGED
@@ -17,17 +17,20 @@
17
17
  // (`JR2_PUBLISH_TOKEN`), read by `jr2 up` through the kube API only. Every other
18
18
  // route answers it 403: it is a known token, but not one that route accepts.
19
19
  //
20
- // Sandbox token the Custodian's credential. Authorizes exactly: deliver to `kind: "agent"`
21
- // registrations whose Sandbox is THIS one. Never a Gate (a compromised Agent
22
- // must not approve its own review), never another Sandbox (`coding.ts`'s iids
23
- // are derivable and feature ids are readable from the Work Source, so a merely
24
- // run-scoped token would let one feature's coder inject a verdict into another
25
- // feature's reviewer). Delivered as a Secret mounted into the Custodian
26
- // container ALONE, which the Agent cannot read; the Harness holds its
27
- // Stand-in (ADR-0059).
28
- // The signed name is a POD hosting Turns, not a Sandbox CR per se: the Instance
29
- // Harness's Custodian holds one signed for that placement's Service name
30
- // (ADR-0031), scoping it to the Menu-only registrations placed there.
20
+ // Sandbox token the Custodian's credentials, two of them, each valid on its own routes alone
21
+ // (ADR-0076). Both are signed names of a POD hosting Turns — a Sandbox, or the
22
+ // Instance Harness's Service name (ADR-0031) — and both live in the Custodian
23
+ // container ALONE, which no Workspace code reaches (ADR-0059):
24
+ // control token `agent:<pod>.<sig>`. Delivers to `kind: "agent"` registrations whose pod is
25
+ // THIS one, and only for the slot the Harness states (`X-Jr2-Slot`), on the
26
+ // four agent routes. The Harness process alone holds its secret Stand-in.
27
+ // fetch token `<pod>.<sig>`. Asks for a fetch for THIS Sandbox (ADR-0053), on
28
+ // `POST /sandboxes/<pod>/fetch` alone. Its Stand-in is public: it can do
29
+ // nothing else.
30
+ // Neither is ever a Gate's (a compromised Agent must not approve its own review),
31
+ // and neither reaches another pod (`coding.ts`'s iids are derivable and feature
32
+ // ids are readable from the Work Source, so a merely run-scoped token would let
33
+ // one feature's coder inject a verdict into another feature's reviewer).
31
34
  //
32
35
  // And one bearer the other way (ADR-0058), which is no principal here because this server never
33
36
  // checks it:
@@ -53,17 +56,53 @@ export type Principal =
53
56
  /** `jr2 up`, publishing an Assembly (ADR-0069). Only `PUT /assembly` accepts it: it is not an
54
57
  * Instance principal, so it reads no run and starts none. */
55
58
  | { kind: "publisher" }
56
- /** One Sandbox's Custodian, speaking for the Agent in that pod — and for no one else. */
57
- | { kind: "sandbox"; sandbox: string };
59
+ /** One Sandbox's fetch token (ADR-0053, ADR-0076): asks for a fetch for that Sandbox, and does
60
+ * nothing else. */
61
+ | { kind: "sandbox"; sandbox: string }
62
+ /** One pod's control token (ADR-0076), with the slot the Harness stated for the conversation
63
+ * (`X-Jr2-Slot`): speaks on the agent routes for that slot's registrations in that pod, and for no
64
+ * other. */
65
+ | { kind: "agent"; sandbox: string; slot: string };
58
66
 
59
- /** Resolve a bearer token to a principal; undefined = not a token we minted (→ 401). */
60
- export type Authenticator = (bearer: string | undefined) => Principal | undefined;
67
+ /** The header the Harness states a control request's slot in (ADR-0076): the Agent's actor path
68
+ * and agent name, its continued conversation's Instance ID without the run id. */
69
+ export const SLOT_HEADER = "x-jr2-slot";
61
70
 
62
- /** The Sandbox token for one Sandbox: its name, signed. Same name + key → same token, always. */
71
+ /** Resolve a bearer token, and the slot a control request states, to a principal; undefined = not a
72
+ * token we minted, or a control token with no slot (→ 401). */
73
+ export type Authenticator = (bearer: string | undefined, slot?: string) => Principal | undefined;
74
+
75
+ /** The fetch token for one Sandbox: its name, signed. Same name + key → same token, always. The
76
+ * bare form of ADR-0013's Sandbox token; since ADR-0076 it opens the fetch route alone. */
63
77
  export function sandboxToken(key: Buffer, sandbox: string): string {
64
78
  return `${sandbox}.${sign(key, sandbox)}`;
65
79
  }
66
80
 
81
+ /** The domain of a control token's signed name (ADR-0076). No DNS label holds a `:`, so a control
82
+ * token is never read as a fetch token, nor the reverse. */
83
+ const CONTROL_PREFIX = "agent:";
84
+
85
+ /**
86
+ * The control token for one pod (ADR-0076): what the Custodian puts in place of the secret control
87
+ * Stand-in on the agent routes. A signed name like the fetch token — same key, same deterministic
88
+ * re-mint — in its own `agent:` domain. No slot is inside it: the Harness states the slot on each
89
+ * request, and the Orchestrator checks it against the registration's.
90
+ */
91
+ export function controlToken(key: Buffer, pod: string): string {
92
+ const name = `${CONTROL_PREFIX}${pod}`;
93
+ return `${name}.${sign(key, name)}`;
94
+ }
95
+
96
+ /**
97
+ * The secret control Stand-in for one pod (ADR-0076, ADR-0059): the bearer the Harness process
98
+ * alone holds, as a file under `/run/jr2/` that no confined child can read, and that the Custodian
99
+ * swaps for {@link controlToken} on the agent routes. A 43-character base64url HMAC, so no
100
+ * Workspace code can guess it. Its own `control-standin:` domain keeps it apart from every token.
101
+ */
102
+ export function controlStandIn(key: Buffer, pod: string): string {
103
+ return sign(key, `control-standin:${pod}`);
104
+ }
105
+
67
106
  /**
68
107
  * The Harness bearer for one placement (ADR-0058): what the Orchestrator presents on every Harness
69
108
  * wire call — admit, stream, abort, echo — to the Harness in the pod `placement` names (a
@@ -138,37 +177,65 @@ export function createAuthenticator(opts: {
138
177
  /** The publish token (ADR-0069). Absent → no bearer resolves to the publisher. */
139
178
  publishToken?: string;
140
179
  }): Authenticator {
141
- return (bearer) => {
180
+ return (bearer, slot) => {
142
181
  if (!bearer) return undefined;
143
182
  if (constantTimeEqual(bearer, opts.instanceToken)) return { kind: "instance" };
144
183
  if (opts.publishToken !== undefined && constantTimeEqual(bearer, opts.publishToken)) return { kind: "publisher" };
145
184
  const cut = bearer.lastIndexOf(".");
146
185
  if (cut <= 0) return undefined;
147
- const sandbox = bearer.slice(0, cut);
148
- if (!constantTimeEqual(bearer.slice(cut + 1), sign(opts.signingKey, sandbox))) return undefined;
149
- return { kind: "sandbox", sandbox };
186
+ const name = bearer.slice(0, cut);
187
+ if (!constantTimeEqual(bearer.slice(cut + 1), sign(opts.signingKey, name))) return undefined;
188
+ // A control token (ADR-0076), read BEFORE the bare form. A control request states its slot; one
189
+ // that states none is refused like an unknown token, so no route ever sees a control principal
190
+ // without a slot.
191
+ if (name.startsWith(CONTROL_PREFIX)) {
192
+ const pod = name.slice(CONTROL_PREFIX.length);
193
+ if (!isPodName(pod) || !slot) return undefined;
194
+ return { kind: "agent", sandbox: pod, slot };
195
+ }
196
+ // The bare fetch token, in its own domain alone: a name in any other domain (`harness:`,
197
+ // `control-standin:`, `held-standin:` …) is not a Sandbox's, whatever key signed it.
198
+ if (!isPodName(name)) return undefined;
199
+ return { kind: "sandbox", sandbox: name };
150
200
  };
151
201
  }
152
202
 
203
+ /** A pod's name as a token signs it: a DNS-1123 name, so it holds no `:` and no `/` — the marks of
204
+ * every other signed domain (ADR-0076). */
205
+ function isPodName(name: string): boolean {
206
+ return /^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$/.test(name);
207
+ }
208
+
153
209
  /**
154
- * May this principal deliver to this agent registration? The Instance token may (it is the
155
- * operator). A Sandbox token may only when the registration records ITS name — which is why
156
- * an Agent registration carries `sandbox` at all (ADR-0013). The recorded name is the pod hosting the Turn:
157
- * a Workspace's Sandbox, or `jr2-instance-harness` for a Menu-only registration (ADR-0031). An
158
- * agent registration with NO name at all is an explicit-`endpoint` run (the stub Harness on the
159
- * host, in no pod): no Sandbox token can claim it.
210
+ * May this principal speak on this agent registration's routes — its surface, a pick, a delivery's
211
+ * poll, a notice's acknowledgment? The Instance token may (it is the operator). A control token may
212
+ * only when the registration records ITS pod — which is why an Agent registration carries `sandbox`
213
+ * at all (ADR-0013) — AND the slot the Harness stated is the registration's (ADR-0076). The recorded
214
+ * pod is the one hosting the Turn: a Workspace's Sandbox, or `jr2-instance-harness` for a Menu-only
215
+ * registration (ADR-0031). An agent registration with NO pod at all is an explicit-`endpoint` run
216
+ * (the stub Harness on the host, in no pod): no pod's token can claim it. A fetch token never may:
217
+ * it opens the fetch route alone.
218
+ *
219
+ * The slot check finds a routing fault. It is not the boundary: only the Harness process holds the
220
+ * control Stand-in, so only the Harness can state a slot at all (ADR-0076).
160
221
  */
161
- export function mayDeliverToAgent(principal: Principal, registrationSandbox: string | undefined): boolean {
222
+ export function mayDeliverToAgent(principal: Principal, registration: { sandbox?: string; slot?: string }): boolean {
162
223
  if (principal.kind === "instance") return true;
163
- if (principal.kind !== "sandbox") return false;
164
- return registrationSandbox !== undefined && registrationSandbox === principal.sandbox;
224
+ if (principal.kind !== "agent") return false;
225
+ return (
226
+ registration.sandbox !== undefined &&
227
+ registration.sandbox === principal.sandbox &&
228
+ registration.slot !== undefined &&
229
+ registration.slot === principal.slot
230
+ );
165
231
  }
166
232
 
167
233
  /**
168
- * May this principal ask for a fetch on this Sandbox (ADR-0053)? A Sandbox token may ask for its
234
+ * May this principal ask for a fetch on this Sandbox (ADR-0053)? A fetch token may ask for its
169
235
  * OWN pod and no other — the scope is the caches that pod mounts, and the route refuses a Repo it
170
236
  * does not. The Instance token may, on the same grounds it may deliver to any agent surface: it is
171
- * the operator. The publish token may not: it opens the publish door and no other (ADR-0069).
237
+ * the operator. A control token may not: it opens the agent routes alone (ADR-0076). The publish
238
+ * token may not: it opens the publish door and no other (ADR-0069).
172
239
  */
173
240
  export function mayAskForSandbox(principal: Principal, sandbox: string): boolean {
174
241
  if (principal.kind === "instance") return true;
@@ -176,7 +243,15 @@ export function mayAskForSandbox(principal: Principal, sandbox: string): boolean
176
243
  return principal.sandbox === sandbox;
177
244
  }
178
245
 
179
- function sign(key: Buffer, value: string): string {
246
+ /** Whether a principal is an Agent's — a control token or a fetch token. A Gate refuses every one
247
+ * (ADR-0013, ADR-0076): an Agent never reaches a Gate. */
248
+ export function isAgentPrincipal(principal: Principal): boolean {
249
+ return principal.kind === "agent" || principal.kind === "sandbox";
250
+ }
251
+
252
+ /** An HMAC of `value` under the signing key, base64url (43 characters). Exported so every signed
253
+ * name and Stand-in derives one way. */
254
+ export function sign(key: Buffer, value: string): string {
180
255
  return createHmac("sha256", key).update(value).digest("base64url");
181
256
  }
182
257
 
package/src/vocabulary.ts CHANGED
@@ -101,6 +101,27 @@ export function inputSchemaOf(machine: AnyStateMachine): z.ZodObject | undefined
101
101
  return inputSchemas.get(machine.config);
102
102
  }
103
103
 
104
+ /**
105
+ * The door of a wrapper under a parent (ADR-0033). No other door reads a parent's input, so the
106
+ * wrapper parses it, and the parsed shape is what it uses: defaults are applied and unknown keys
107
+ * go. A key in `beside` is no one's to send, so it stays out of the parse and rides beside the
108
+ * result. A value that is wrong is a defect of the parent's Machine: a throw, and its `onError`.
109
+ *
110
+ * @param refusal - the start of the error, which says what the refusal saved
111
+ * @internal
112
+ */
113
+ export function parsedNestedDoor(
114
+ door: z.ZodObject,
115
+ value: Record<string, unknown>,
116
+ beside: readonly string[],
117
+ refusal: string,
118
+ ): Record<string, unknown> {
119
+ const kept = Object.entries(value).filter(([key, v]) => beside.includes(key) && v !== undefined);
120
+ const parsed = door.safeParse(Object.fromEntries(Object.entries(value).filter(([key]) => !beside.includes(key))));
121
+ if (!parsed.success) throw new Error(`${refusal}: ${parsed.error.message}`);
122
+ return { ...(parsed.data as Record<string, unknown>), ...Object.fromEntries(kept) };
123
+ }
124
+
104
125
  /** A zod schema, by its shape: the Instance can resolve a zod of its own, so `instanceof` does not
105
126
  * tell. */
106
127
  export function isZodSchema(value: unknown): boolean {
@@ -187,10 +208,8 @@ export function loadWarningsOf(machine: AnyStateMachine): string[] {
187
208
  *
188
209
  * Only the root Machine of a run gets it. A Machine that another Machine invokes gets the input
189
210
  * that its parent gives it. A `workspace()` passes its input to its body, so the body of a root
190
- * `workspace()` gets it too: `Workspaced<HostInjectedInput & RunInput, "app">`.
191
- *
192
- * The `spec` of a root `workspace()` also gets it, and the type of its input shows it:
193
- * ``spec: ({ input }) => ({ branch: `fix/${input.instanceId}` })``.
211
+ * `workspace()` gets it too: `Workspaced<HostInjectedInput & RunInput, "app">`. For example, a cut
212
+ * can name a branch for each run: `` branch: `fix/${context.instanceId}` ``.
194
213
  *
195
214
  * It is not the run id. Use it, for example, to give each run a branch of its own.
196
215
  *
package/src/wire.ts CHANGED
@@ -267,18 +267,18 @@ export type EchoRequest = { events: EchoEvent[] };
267
267
  /** One Repo Slot to attach. The Orchestrator resolves the Repo's `identity` and cache `key`
268
268
  * (repo-identity.ts) — the Harness never re-derives them, so the cache it clones from and the node
269
269
  * cache the operator mounted agree by construction (ADR-0051). `url` is the Binding's own spelling,
270
- * the push url (ADR-0005); `ref` is the base the branch starts from, omitted for the Repo's default
271
- * branch. */
270
+ * the push url (ADR-0005); `ref` is the base that `default/` stays on and a new branch starts from,
271
+ * omitted for the Repo's default branch. */
272
272
  export type AttachSlot = { slot: string; url: string; identity: string; key: string; ref?: string };
273
273
 
274
- /** What `POST /attach` accepts: the slots in declaration order, the Workspace's branch, and — for a
275
- * reviewer's seat — the sha the detached review worktree is forced to (ADR-0028). */
276
- export type AttachRequest = { slots: AttachSlot[]; branch: string; reviewSha?: string };
274
+ /** What `POST /attach` accepts: the slots in declaration order. The Workspace only clones, so the
275
+ * request names no branch and no commit (ADR-0075). */
276
+ export type AttachRequest = { slots: AttachSlot[] };
277
277
 
278
- /** What `POST /attach` answers (200): the branch worktree per slot, and the review worktree per
279
- * slot when the request named a `reviewSha`. Freshness is the operator's verdict on the Sandbox,
280
- * which the Orchestrator reads itself — never the Harness's to report. */
281
- export type AttachResponse = { repos: Record<string, string>; review?: Record<string, string> };
278
+ /** What `POST /attach` answers (200): the folder of each slot, and the base that each slot's
279
+ * `default/` holds. Freshness is the operator's verdict on the Sandbox, which the Orchestrator
280
+ * reads itself — never the Harness's to report. */
281
+ export type AttachResponse = { repos: Record<string, string>; bases: Record<string, string> };
282
282
 
283
283
  /** What a failed attach answers: 400 for a body that is not an `AttachRequest`, 500 when a step
284
284
  * failed — `slot` names the Repo Slot, and `error` carries the step and git's own stderr. */
@@ -312,7 +312,7 @@ export type SyncAnswer =
312
312
 
313
313
  /** What `POST /exec/:id` accepts: argv (never shell text), an absolute `cwd` or none, the timeout,
314
314
  * and how many characters of the stream's end the output keeps. */
315
- export type ExecRequest = { argv: string[]; cwd?: string; timeoutMs: number; tail: number };
315
+ export type ExecRequest = { argv: string[]; cwd?: string; root?: true; timeoutMs: number; tail: number };
316
316
 
317
317
  /** How an exec ended. Every one is output; none is a fault. */
318
318
  export type ExecOutcome =