@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
@@ -1,7 +1,7 @@
1
1
  # A Repo is a slot on the `workspace()` and a cache on the node
2
2
 
3
3
  ADR-0049 moved Agents and the Sandbox Image into the Machine and left one dependency outside it: the Repo, catalogued in
4
- `jr2.config.ts` and named by string from a `workspace()` spec, with the name typed back through a `Register` interface
4
+ `jr2.config.ts` and named by string from a `workspace()`, with the name typed back through a `Register` interface
5
5
  (ADR-0050). The grill of 2026-09-13 found two faults. A packaged Machine still could not say which repository it works
6
6
  on, nor could a consumer bind one, except by the consumer's config holding the right NAME and the door threading it —
7
7
  the same encapsulation gap ADR-0049 closed for Agents. And the catalog's shape was dictated by storage: one read-only
@@ -20,28 +20,32 @@ decision: **what a Machine says about a repository, and what the cluster does wi
20
20
  target: open, // the consumer's — must be bound
21
21
  docs: { url: "https://github.com/acme/handbook.git", ref: "v3" }, // the package's own
22
22
  },
23
- spec: ({ input }) => ({ branch: input.branch }),
24
23
  });
25
24
  ```
26
25
 
27
26
  A slot is in one of three states, and the walk tells them apart without evaluating anything: **bound** (a url, or
28
27
  `{ url, ref? }`), **open** (the `open` sentinel — someone downstream binds it), or **per-run** (a mapper over the
29
28
  wrapper's door: `feature: ({ input }) => ({ url: input.feature.repo, ref: input.feature.baseRef })`). `ref` is the
30
- base the branch worktree is cut from; absent, the repository's own default branch. The spec no longer carries repos —
31
- it is `{ branch, workGroup?, reviewSha? }`, or `{ workGroup? }` for a Workspace with no slot (below). The body's
32
- handles are keyed by slot (`workspace.repos.target`), and so are the in-pod paths (`/work/<slot>/<branch>`): a slot
33
- key is collision-free by construction and the same in every Instance that consumes the package, so a prompt can name a
34
- path and be right everywhere.
29
+ Binding's base: `default/` stays on it, and a new branch starts from it. A branch is attached, and a tag or a commit
30
+ is detached. Absent, the base is the repository's own default branch. The body's handles are keyed by slot
31
+ (`workspace.repos.target` is `/work/target`), and so are the in-pod paths (`/work/<slot>/<name>`): a slot key is
32
+ collision-free by construction and the same in every Instance that consumes the package, so a prompt can name a path
33
+ and be right everywhere.
35
34
 
36
- - **The handles are the slot map and the branch — no slot is the `workdir`.** `workspace.repos` keeps the slots in
37
- declaration order, and jr2 reads nothing into that order. Which checkout an Agent works in is a fact about that
38
- Agent's Turn, not about the Workspace: a body with a `backend` and a `frontend` slot frames one Agent with the first
39
- path and another with the second, and either may read the other's tree. A body that names no slot may give the order a
40
- meaning of its own and document it (`task`: the first slot is the one the coder edits). The order is the kit's promise
41
- — a slot key starts with a letter, because JS reads an integer-like key first wherever it was written — and the
42
- meaning is the Machine's. Rejected: a `workdir` handle, the first declared slot's worktree — it made a singular
43
- "primary repo" a property of every Workspace, which is wrong for a body whose Agents work in different slots or in
44
- more than one, and it hid a rule in key order that the body never chose.
35
+ - **The handles are the slot map — no slot is the `workdir`.** `workspace.repos` keeps the slots in declaration order,
36
+ and jr2 reads nothing into that order. A handle is the slot's folder and not a Worktree: the Workspace only clones,
37
+ and a Machine cuts each Worktree at `/work/<slot>/<name>`
38
+ ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)). Which checkout an Agent works in is a
39
+ fact about that Agent's Turn, not about the Workspace: a body with a `backend` and a `frontend` slot frames one Agent
40
+ with a Worktree of the first and another with a Worktree of the second, and either may read the other's tree. An
41
+ Effect names its Worktree as a Turn does, in its input: a `command` and an `http` take no `slot`, and a `push` keeps
42
+ its static `slot`, which says the Repo Slot whose credential it spends. A slot that is only read needs no Worktree: an
43
+ Agent reads `/work/<slot>/default`. A body that names no slot may give the order a meaning of its own and document it
44
+ (`task`: the first slot is the one the coder edits). The order is the kit's promise — a slot key starts with a letter,
45
+ because JS reads an integer-like key first wherever it was written — and the meaning is the Machine's. Rejected: a
46
+ `workdir` handle, the first declared slot's worktree — it made a singular "primary repo" a property of every
47
+ Workspace, which is wrong for a body whose Agents work in different slots or in more than one, and it hid a rule in
48
+ key order that the body never chose.
45
49
 
46
50
  - **A Machine whose body names no slot declares the whole map Open: `repos: open`.** A body that enumerates whatever is
47
51
  attached — `@jr2/machines`'s `task` — knows nothing a slot name could say, and a `target: open` on it would claim "one
@@ -61,21 +65,18 @@ decision: **what a Machine says about a repository, and what the cluster does wi
61
65
  have a second Agent edit it, upload it. Its steps share `/work`, as the steps of one GitHub Actions job share that
62
66
  job's folder; two Workspaces are two jobs and share nothing. So `workspace(body, { input })` with no `repos` is a
63
67
  Workspace with no Repo Slot: the attach is skipped (there is nothing to clone, and the pod's `fsGroup` already makes
64
- `/work` the work group's), the body's handles are `{ repos: {} }` with no branch, typed `Workspaced<…, never>`, and a
65
- Turn that frames no `cwd` and a `command` both work in `/work`. The spec has nothing to cut a branch in: `spec` is
66
- optional, gives only `workGroup`, and a `branch` or a `reviewSha` in it is refused — at the type, and when the run
67
- enters, since a value jr2 silently ignores is the failure the refusals exist for. `repos: {}` is refused with the
68
- pointer to leaving it out, so "none" has one spelling and an empty map never reads as one somebody forgot to fill. An
69
- Open map still asks `customize` for at least one slot: Open says the composer names the slots, and a body that
70
- enumerates its checkouts (`task`) has nothing to do with none. Two Effects need a slot and are refused there, by
71
- `jr2 up` and at invoke, with one reason: a `push` publishes a slot's branch, and a `command` with an Effect secret
72
- runs against one commit of it (ADR-0067). A file-body `http` reads `/work` and stays. `/work` is an `emptyDir`, so a
73
- Lost Sandbox takes everything with it — with a repository, pushed commits outlive the pod; here only what an Effect
74
- sent out does, and the body hears `workspace.lost` as any body does. Storage that outlives the pod, the job's
68
+ `/work` the work group's), the body's handles are `{ repos: {} }`, typed `Workspaced<…, never>`, and a Turn that
69
+ frames no `cwd` and a `command` both work in `/work`. `repos: {}` is refused with the pointer to leaving it out, so
70
+ "none" has one spelling and an empty map never reads as one somebody forgot to fill. An Open map still asks
71
+ `customize` for at least one slot: Open says the composer names the slots, and a body that enumerates its checkouts
72
+ (`task`) has nothing to do with none. Three Effects need a slot and are refused there, by `jr2 up` and at invoke, for
73
+ one reason: a `worktree()` cuts in a slot's clone, a `push` publishes a slot's branch, and a `command` with an Effect
74
+ secret runs against one commit of it (ADR-0067). A file-body `http` reads `/work` and stays. `/work` is an `emptyDir`,
75
+ so a Lost Sandbox takes everything with it — with a repository, pushed commits outlive the pod; here only what an
76
+ Effect sent out does, and the body hears `workspace.lost` as any body does. Storage that outlives the pod, the job's
75
77
  "artifacts", is a decision of its own. Rejected: a placeholder repository bound to satisfy the rule — a clone nobody
76
- uses, a branch that means nothing, a Worktree that becomes every Agent's default `cwd`, and a cache entry to warm.
77
- Rejected: a new term for the repository-less Sandbox — the Glossary's Workspace never named a repository, so none was
78
- needed.
78
+ uses, a Worktree that means nothing, a `cwd` that each Turn must then state, and a cache entry to warm. Rejected: a
79
+ new term for the repository-less Sandbox — the Glossary's Workspace never named a repository, so none was needed.
79
80
 
80
81
  - **The consumer binds with `customize`**, in the same call as Agents and the image, nested through `actors` like every
81
82
  other part: `customize(codeReview, { repos: { target: "git@github.com:ourorg/app.git" } })`. Any of the three forms is
@@ -89,6 +90,14 @@ decision: **what a Machine says about a repository, and what the cluster does wi
89
90
  is the thing, not a reference into someone else's file. Two spellings of one repository (`https://`, `git@…:`,
90
91
  `ssh://`, with or without `.git`) resolve to one identity — host plus path, scheme and user dropped — and therefore
91
92
  one cache. A door that offers a menu of repositories is the Instance's own `z.enum` of urls.
93
+ - **The cache key names one identity.** The key is the Repo CR's name, the hostPath folder, the volume name `repo-<key>`
94
+ and the mount `/repos/<key>`: a slug of the identity's last segment, at most 16 characters, a dash, and the first 32
95
+ hex digits (128 bits) of the identity's sha256, at most 49 characters in all. No one can find two identities with one
96
+ key, and a per-run url is the Agent's text, so a shorter hash is a way into another Repo's cache: with 32 bits, a
97
+ search found a url of an attacker's repository with the key of a private one in seconds. Each Repo CR states its
98
+ identity (`jr2.dev/identity`). The Orchestrator refuses to write a CR whose stated identity is not the one that it
99
+ attaches or binds, and names both urls: no url, no `secretRef` and no clock of one repository is written on a CR of
100
+ another.
92
101
  - **The cluster keeps a bare cache per node, not a volume.** Each repository is a `Repo` custom resource the operator
93
102
  reconciles (spec: url, refresh interval; status: per node — synced, generation, last error). A cache agent runs as a
94
103
  DaemonSet per Instance over its Sandbox nodes (ADR-0052) with a hostPath directory: it clones a repository onto its
@@ -100,9 +109,8 @@ decision: **what a Machine says about a repository, and what the cluster does wi
100
109
  on-demand fetch; every cache speaks git protocol v2, so a fetch advertises only the refs it asks for; and the refresh
101
110
  interval carries ±20% jitter, like the Lease, so R Repos on N nodes do not hit a remote on one beat (scaling review
102
111
  R13, 2026-09-30). The agent also consolidates a cache's packs without deleting an object (ADR-0004). The
103
- Orchestrator's attach then runs unchanged: `git clone --shared` off the mount, worktree beside it. A Sandbox that
104
- lands on a cold node pays one clone there, once — the same economics as an image pull, and the reason a Sandbox can
105
- run on any node.
112
+ Orchestrator's attach then runs unchanged: `git clone --shared` off the mount. A Sandbox that lands on a cold node
113
+ pays one clone there, once — the same economics as an image pull, and the reason a Sandbox can run on any node.
106
114
  - **The Orchestrator creates `Repo` CRs; it does not sync them.** At boot it walks its registered Machines and creates
107
115
  one CR per bound identity, so a statically known repository is KNOWN before a run can ask: the cache agent on every
108
116
  node probes it (`git ls-remote`) as soon as the CR exists, so a wrong url or a credential that does not reach shows in
@@ -123,10 +131,13 @@ decision: **what a Machine says about a repository, and what the cluster does wi
123
131
  whose clone Secret `git-clone.json` does not list gets none, and the clone is anonymous; the cache agent reads only
124
132
  that Secret, and the operator matches nothing. The Orchestrator writes and reads no git credential (ADR-0068). The
125
133
  Secret uses Flux's key names (`username`/`password`; `identity`, `identity.pub`, `known_hosts`), so a Flux or Argo
126
- user reuses the Secret they have. **The list is also the fence.** A per-run url — a run input, a ticket field — is
127
- otherwise a way to spend the cluster's credential against any host: one that matches no entry is refused at attach,
128
- naming the list. A static binding is code the Instance typechecked and deployed, so it is admitted without a match and
129
- cloned anonymously; a private one then fails at the cache with git's error. `jr2 init` scaffolds one wildcard entry —
134
+ user reuses the Secret they have. With a token, the cache agent's helper answers for the Repo's own origin alone
135
+ (scheme, host and port), and its clone, probe and fetch follow no redirect, so the token never reaches an origin
136
+ outside the entry. A repository that moved, and that its old url redirects to, is cloned by its new url. **The list is
137
+ also the fence.** A per-run url — a run input, a ticket field — is otherwise a way to spend the cluster's credential
138
+ against any host: one that matches no entry is refused at attach, naming the list. A static binding is code the
139
+ Instance typechecked and deployed, so it is admitted without a match and cloned anonymously; a private one then fails
140
+ at the cache with git's error. `jr2 init` scaffolds one wildcard entry —
130
141
  `{ match: "*", token: "JR2_GIT_TOKEN", sshKey: "jr2-git-ssh" }`, today's two implicit defaults made visible, with a
131
142
  comment saying to narrow it before anything untrusted can start a run, and that it also pushes: a `push()` whose Repo
132
143
  Slot url it matches, a per-run url included, spends the same token or key — so the open fence is in the user's file
@@ -198,15 +209,15 @@ decision: **what a Machine says about a repository, and what the cluster does wi
198
209
  the kit's first cluster requirement a storage class most small clusters lack.
199
210
  - **A Flux-style `GitRepository` serving an artifact over HTTP.** Rejected: an artifact download is a clone per Sandbox
200
211
  — the cost the cache exists to avoid.
201
- - **Urls kept in the spec, not the slot** (the shape before this decision, with a url in place of a name). Rejected: the
202
- walk cannot see inside the spec, so a bound repository would be invisible to `jr2 up` and to prewarm, and `customize`
203
- would have nothing to bind.
212
+ - **Urls from a per-run function of the `workspace()`, not the slot** (the shape before this decision, with a url in
213
+ place of a name). Rejected: the walk cannot see inside a function, so a bound repository would be invisible to
214
+ `jr2 up` and to prewarm, and `customize` would have nothing to bind.
204
215
 
205
216
  ## Consequences
206
217
 
207
218
  - The operator becomes git-aware, which ADR-0001 kept it from being: the `Repo` CRD and the cache agent are its. The
208
- Sandbox CRD stays free of git semantics beyond a list of identities; clone and worktree remain the Orchestrator's
209
- post-`Ready` step (ADR-0004).
219
+ Sandbox CRD stays free of git semantics beyond a list of identities; the clone remains the Orchestrator's post-`Ready`
220
+ step, and a Worktree is a Machine's cut (ADR-0004).
210
221
  - `workspace()`'s `repos` names at least one slot, or is `open` whole — in which case the `customize` that names the map
211
222
  is held to the same rule — or is left out, for a Workspace with no Repo Slot; a Machine that composes no Sandbox has
212
223
  no repos to customize, and the data-plane switch ("does this Instance need Sandboxes") is "a registered Machine
@@ -214,12 +225,12 @@ decision: **what a Machine says about a repository, and what the cluster does wi
214
225
  Sandboxes, the cache agent and the Sandbox Images; a deployed Orchestrator wires its kube client whatever its current
215
226
  Assembly composes, because a publish can bring the first `workspace()` without a roll
216
227
  ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)).
217
- - Every slot gets a branch worktree cut at the run's branch, a reference checkout included. A slot has no read-only
218
- mode; that is a decision of its own if it is ever wanted.
228
+ - A slot has no read-only mode, and needs none: a slot in which a Machine cuts nothing holds only `default/`, on the
229
+ Binding's base.
219
230
  - The Console's start form loses the catalog dropdown: a per-run repository is a url field unless the Machine's door
220
231
  enumerates its own.
221
- - The volume paths change: the in-pod source mount is `/repos/<key>` where `<key>` is derived from the identity, and the
222
- pod-local layout is `/work/<slot>/{default,<branch>}`. Existing checkouts on a pre-0051 PVC are not migrated.
232
+ - The volume paths change: the in-pod source mount is `/repos/<key>` where `<key>` is the identity's cache key, and the
233
+ pod-local layout is `/work/<slot>/{default,<name>}`. Existing checkouts on a pre-0051 PVC are not migrated.
223
234
  - ADR-0047's deploy-key prompt reads ssh urls off the walk. A per-run ssh url first seen at attach was never prompted
224
235
  for; its first attach fails with the key hint, as any unregistered key does.
225
236
  - A `customize` of a per-run slot with a static url is legal and makes it bound; a Machine that wants a slot to stay the
@@ -27,6 +27,11 @@ use. **Which nodes are an Instance's Sandbox nodes, and who follows that set** i
27
27
  Sandbox node, so a cache is only ever present where a Sandbox can land, and the affinity it feeds only ever names a
28
28
  usable node. The build set (ADR-0045) is the union of the nodes an ordinary pod lands on — the Orchestrator
29
29
  Deployment's own placement — and the Sandbox nodes, intersected with the supported set as before.
30
+ - **The cache agent also reports Landlock for its node.** It probes Landlock when it starts, and on a node without it
31
+ the agent is NotReady, with the reason. `jr2 up` reads the DaemonSet's pods and refuses to converge while a Sandbox
32
+ node lacks Landlock, naming each one ([ADR-0077](0077-jr2-requires-landlock-on-every-sandbox-node.md)). This is the
33
+ one refusal on the node set: Landlock is a fact that each Sandbox node must have, not a set that can be empty. The
34
+ admin keeps a bad node out with `sandbox.nodeSelector` or a taint.
30
35
  - **The Sandbox CR carries the placement.** `Sandbox.spec` gains `nodeSelector` and `tolerations` beside `resources`,
31
36
  `initContainers`, and `volumes`; the Orchestrator writes them from its baked config at provision and the operator
32
37
  copies them onto the pod, merging nothing. The operator stays config-blind (ADR-0001), and every Instance's placement
@@ -29,8 +29,8 @@ a fetch inside a pod reaches the remote, and who may ask** is one decision.
29
29
  Binding's own spelling, the caller's own credential, never the Agent's (ADR-0005).
30
30
 
31
31
  - **The ask rides the Sandbox CR and reaches the agent through the pod.** The program asks the Custodian on `localhost`
32
- (`POST /fetch`, with the Sandbox token's Stand-in — a constant, because the User Container gets no env); the Custodian
33
- puts the token it holds in place of the Stand-in and asks the Orchestrator for THIS pod's Sandbox
32
+ (`POST /fetch`, with the fetch token's Stand-in — a constant, because the User Container gets no env); the Custodian
33
+ puts the fetch token in place of the Stand-in, on this route alone and asks the Orchestrator for THIS pod's Sandbox
34
34
  (`POST /sandboxes/<name>/fetch` — the program cannot name another,
35
35
  [ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)); the Orchestrator patches one
36
36
  annotation on the Sandbox CR, per Repo key, timestamp value — the Lease's shape, an annotation the Orchestrator
@@ -45,16 +45,28 @@ a fetch inside a pod reaches the remote, and who may ask** is one decision.
45
45
  sticky, once per pod life. The Orchestrator's route waits on that entry — a Sandbox watch event, never a poll
46
46
  ([ADR-0063](0063-the-orchestrator-watches-the-cluster.md)) — then answers the Custodian, which answers the program.
47
47
  One wait, and it is the wait an attach already makes.
48
- - **The scope is the Sandbox token's scope, and no seat gains a credential.** A Sandbox may ask for the caches it mounts
49
- and nothing else; the remote is reached by the cache agent with the Repo CR's `secretRef`, the credential that cloned
50
- the cache. The pod holds none before and none after. The Menu is untouched — a fetch is not something the Agent
51
- _says_, it is something it _does_ — and no Machine declares anything: the ability is ambient in every Workspace pod,
52
- the way Working tools are, and absent on the Instance Harness, which mounts no Repo.
48
+ - **The scope is the fetch token's scope, and no seat gains a credential.** The fetch token is valid on the fetch route
49
+ alone, and the control token is not valid there
50
+ ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)). A Sandbox may ask for the
51
+ caches it mounts and nothing else; the remote is reached by the cache agent with the Repo CR's `secretRef`, the
52
+ credential that cloned the cache. The pod holds none before and none after. The Menu is untouched — a fetch is not
53
+ something the Agent _says_, it is something it _does_ — and no Machine declares anything: the ability is ambient in
54
+ every Workspace pod, the way Working tools are, and absent on the Instance Harness, which mounts no Repo.
53
55
  - **Freshness degrades, absence does not, and the caller is told.** A remote fetch that fails or outruns the on-demand
54
56
  budget lets the program fall through to the objects the cache holds — ADR-0051's stance for attach — and writes one
55
57
  line to stderr, which git passes through verbatim:
56
58
  `warning: jr2: remote fetch failed (<git's error>); serving the cache as of <lastFetched>`. The budget is the cache
57
- agent's one on-demand number; the program's wait is that plus watch slack, and nothing else owns a timeout.
59
+ agent's one on-demand number; the program's wait is that plus watch slack, and nothing else owns a timeout. A cut
60
+ reads that line too: its output carries the line in `stale`, and the cut uses what the clone holds. A branch name that
61
+ the clone does not hold is then `refused`, because the remote cannot confirm that it is new
62
+ ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)).
63
+ - **One fetch at a time in a clone.** All Worktrees of a clone share `refs/remotes/origin/*`, and git updates a ref only
64
+ if it still holds the value that the fetch read. So two fetches that overlap on a moved remote can fail with
65
+ `cannot lock ref`. The failure does no harm, but it costs a Turn. So the program takes a per-clone lock after the ask,
66
+ so a dead remote does not make coders wait in line. `git upload-pack` can end before the fetch writes its refs: that
67
+ happens when a pack moves. In a shared clone, git often keeps the connection open to the end. The lock must last to
68
+ the end of the fetch in both cases, so a small child process that the program leaves watches the fetch and holds the
69
+ lock. ADR-0075 holds the measurements.
58
70
  - **Every seat that holds the checkouts holds the program.** The fetch url lives in the shared `default/.git/config`, so
59
71
  a seat without the program has checkouts whose `git fetch` dies. That is ADR-0005's own argument for `/repos` — half
60
72
  of one exception, not a second — applied once more: the pod's `/opt/jr2` volume is mounted read-only into the User
@@ -22,24 +22,24 @@ ADR-0018 and GLOSSARY.md said `cwd` is identity, never per Turn. The decided thi
22
22
  default: `agent({ model, instructions, workspace?, thinkingLevel?, description? })`.
23
23
 
24
24
  - **The Frame is `prompt` and `cwd`: what this Turn is about and where it works.** Neither identity nor a Dial. A
25
- worktree path is per run by nature — `/work/<slot>/<branch>` exists only once a run has a branch — so no definition
26
- can name it, and a directory is not a property of a persona any more than a prompt is. `cwd` leaves `AgentDefinition`.
27
- The Harness roots the Working tools at the Frame's `cwd`, which rides the admit body beside the prompt and the Dials.
25
+ worktree path is per run by nature — `/work/<slot>/<name>` exists only once a run cuts it — so no definition can name
26
+ it, and a directory is not a property of a persona any more than a prompt is. `cwd` leaves `AgentDefinition`. The
27
+ Harness roots the Working tools at the Frame's `cwd`, which rides the admit body beside the prompt and the Dials.
28
28
 
29
29
  - **The Briefing is not the Frame.** jr2's Turn part
30
30
  ([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)) follows the prompt — the resolved working
31
31
  directory, the Allowed picks and any notices — in a block of its own. The prompt stays the Machine state's words
32
32
  alone; jr2 never writes into it.
33
33
 
34
- - **Under a Workspace, an absent `cwd` resolves to the only Repo Slot's Worktree, and is refused when there is more than
35
- one.** With one slot there is no choice, so no convention is being smuggled in (ADR-0051: the kit gives no slot a
36
- meaning). With two or more, the state must say which — `cwd: context.workspace.repos.target` — and the actor refuses
37
- the Turn before admission, naming the slots and the line to add. The trap is accepted with open eyes: a composer who
38
- adds a second slot on the `customize` line can make a Machine that ran yesterday refuse today, in a file the Machine's
39
- author never sees. The refusal is explicit, at the first Turn, before any pod is spent; `jr2 up` cannot see it because
40
- the invoke's input is a function. A `workspace: "none"` Agent has no Working tools and no `cwd` (ADR-0028); a
41
- stub-Harness run with no Workspace keeps `/work`. `task` always passes its `cwd`, because "the first slot is the one
42
- the coder edits" is its convention and a convention is stated, not defaulted.
34
+ - **Under Repo Slots, `cwd` is required.** A Workspace that only clones has no Worktree for a default to name
35
+ ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)), and `/work` is the wrong default: an
36
+ Agent's search hits `default/`, and a relative write lands outside each Worktree, which is the incident above. So the
37
+ state says where — `cwd: "/work/target/fix"` — and the actor refuses the Turn before admission, naming the slots and
38
+ the line to add; `jr2 up` cannot see it because the invoke's input is a function. A Turn that wants `/work` states it.
39
+ A Workspace with no Repo Slot needs no `cwd`: its Turns work in `/work` (ADR-0051). A `workspace: "none"` Agent has no
40
+ Working tools and no `cwd` (ADR-0028); a stub-Harness run with no Workspace keeps `/work`. `task` passes its `cwd` as
41
+ any Machine does: "the first slot is the one the coder edits" is its convention, and a convention is stated, not
42
+ defaulted.
43
43
 
44
44
  - **`continue: true` is the whole continuation surface.** Absent, every invocation is a fresh conversation — jr's lossy
45
45
  handoff, ADR-0016's default, unchanged. Present, the Turn lands on **this Agent's one conversation in this Machine
@@ -112,11 +112,11 @@ ADR-0018 and GLOSSARY.md said `cwd` is identity, never per Turn. The decided thi
112
112
  that is wrong under every `workspace()` is not a default.
113
113
  - **`cwd` as a third Dial.** Rejected on the word: Dials say how hard to run. A directory says where, which is the same
114
114
  kind of fact as the prompt — hence Frame.
115
- - **Default `cwd` to the first Repo Slot.** Rejected: reverses ADR-0051 ("there is no `workdir`"). The single-slot
116
- default is not the same thing — with one slot there is nothing to privilege.
117
- - **Refuse an absent `cwd` under every Workspace.** The safer twin of the single-slot default: one line per invoke, no
118
- composition trap. Not taken: the trap's failure is explicit and early, and the line is noise for the common
119
- single-repo Machine.
115
+ - **Default `cwd` to the first Repo Slot.** Rejected: reverses ADR-0051 ("there is no `workdir`").
116
+ - **Default `cwd` to the only Repo Slot's Worktree.** Rejected: a Workspace that only clones has no such Worktree. And a
117
+ refusal under every Workspace with Repo Slots has no composition trap: with a default, a composer who adds a second
118
+ slot on the `customize` line makes a Machine that ran yesterday refuse today, in a file the Machine's author never
119
+ sees. The cost is one line per invoke.
120
120
  - **A caller-chosen conversation key (`conversation: "coder"`).** Rejected: it either restates the structure or
121
121
  contradicts it. Path-scoping the key was proposed and made the key redundant — once the id is
122
122
  `<run>/<machine path>/<agent>`, the author has nothing left to name.
@@ -133,19 +133,19 @@ ADR-0018 and GLOSSARY.md said `cwd` is identity, never per Turn. The decided thi
133
133
  - `AgentTurnInput` is `{ prompt, cwd?, continue?, model?, thinkingLevel? }`; `AgentDefinition` loses `cwd`. The Harness
134
134
  admit body gains the Frame's `cwd`; `ResolvedDefinition.cwd` resolves from it, not the definition; the Harness stops
135
135
  validating `description`, which the Orchestrator strips before admission (it is Console material).
136
- - **A Frame's `cwd` is absolute or the admission is refused**, naming the Frame. Absent is the one silent case, and it
137
- says `/work` — a stated default. A relative path has no such answer: it would resolve against whatever directory the
138
- Harness process sits in, which is the silent wrong directory this ADR was written for, one level further from the
139
- author. The refusal is a 400 at admission, before a pod is spent.
140
- - `task` passes `cwd: worktreeOf(context).path` and `continue: true`, and drops `generation` (and its `scope`). It keeps
141
- `turns`, reset on `agent.fault`, because re-briefing is prompt policy.
136
+ - **A Frame's `cwd` is absolute or the admission is refused**, naming the Frame. Absent is the one silent case, it
137
+ reaches a Harness only where no Repo Slot is, and it says `/work` — a stated default. A relative path has no such
138
+ answer: it would resolve against whatever directory the Harness process sits in, which is the silent wrong directory
139
+ this ADR was written for, one level further from the author. The refusal is a 400 at admission, before a pod is spent.
140
+ - `task` passes the path of the Worktree it cuts as `cwd`, and `continue: true`, and drops `generation` (and its
141
+ `scope`). It keeps `turns`, reset on `agent.fault`, because re-briefing is prompt policy.
142
142
  - The ledger record for a continued conversation carries its epoch; the input mapper reads it at mint time, so the
143
143
  minted id persists with the input and restore re-attaches the same conversation (ADR-0016's mapper rule holds — the
144
144
  fresh suffix is still the random component).
145
145
  - The `@kind` fixtures drop `tools:` and `instanceId:`; the steps compute the continued id from the actor path, as they
146
146
  already computed today's pin.
147
147
  - A regression scenario for the silent success: a `task` whose model uses relative paths must produce its file inside
148
- the worktree, and a multi-slot Machine with no `cwd` must be refused with the slots named.
148
+ the worktree, and a Machine under Repo Slots with no `cwd` must be refused with the slots named.
149
149
  - GLOSSARY.md: **Frame** enters; **Dials** loses `cwd`; **Instance ID** states the `continue` scope and that a faulted
150
150
  conversation is not continued. ADR-0016 (continuation), ADR-0018 (identity line), ADR-0031 (placement scenario),
151
151
  ADR-0035 (reroll gate wording), ADR-0049 (persona hazard), and ADR-0054 (`task`'s continuation) are edited to state
@@ -93,9 +93,11 @@ and we add an ingress NetworkPolicy that admits only the Orchestrator to a Harne
93
93
  `harness.env` or in an `envFrom` Secret.
94
94
  - **Open gaps, recorded here and not fixed:**
95
95
  - **Sandbox egress.** An Agent with bash can reach the internet and send out anything it can read — which no longer
96
- includes a held key: a client that goes around the Custodian carries only Stand-ins. A default-deny egress policy
97
- needs an allowlist: DNS, the Orchestrator, the model provider, and whatever the work needs (package registries, for
98
- example). That needs a config surface and cluster CIDRs, so it is a separate decision.
96
+ includes a held key: a client that goes around the Custodian carries only Stand-ins. One exception: no Harness pod
97
+ reaches a cloud metadata address
98
+ ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)). A default-deny egress
99
+ policy needs an allowlist: DNS, the Orchestrator, the model provider, and whatever the work needs (package
100
+ registries, for example). That needs a config surface and cluster CIDRs, so it is a separate decision.
99
101
  - **A held key can be spent.** The Agent can use a key through the Custodian while its pod lives, and a SigV4 or ADC
100
102
  credential cannot be held at all ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)).
101
103
  - **The Orchestrator's Secret writes are namespace-wide.** Its Role may create and patch every Secret, and may not get