@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.
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +56 -20
- package/docs/concepts/architecture.md +30 -17
- package/docs/concepts/charts.md +6 -3
- package/docs/concepts/converge.md +3 -0
- package/docs/concepts/effects.md +86 -31
- package/docs/concepts/machines-and-workflows.md +9 -0
- package/docs/concepts/pools-and-sources.md +6 -6
- package/docs/concepts/secrets-and-trust.md +126 -42
- package/docs/concepts/the-menu.md +3 -0
- package/docs/concepts/workspaces.md +129 -51
- package/docs/get-started/an-agent-in-a-sandbox.md +21 -19
- package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
- package/docs/get-started/install.md +4 -0
- package/docs/guides/add-tools-to-the-sandbox.md +17 -4
- package/docs/guides/bind-open-parts.md +5 -4
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +1 -1
- package/docs/guides/let-an-agent-write-a-chart.md +5 -1
- package/docs/guides/look-inside-a-sandbox.md +18 -15
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
- package/docs/guides/push-and-sign-a-branch.md +67 -21
- package/docs/guides/run-a-command-or-call-a-url.md +84 -41
- package/docs/guides/run-one-worker-per-item.md +19 -18
- package/docs/guides/set-an-agents-access-and-dials.md +67 -22
- package/docs/guides/size-a-workspace.md +19 -6
- package/docs/guides/use-a-hosted-provider.md +6 -0
- package/docs/guides/watch-a-run.md +47 -24
- package/docs/guides/write-a-workflow.md +1 -1
- package/docs/llms.txt +33 -12
- package/docs/reference/authoring-surface/agent.md +34 -11
- package/docs/reference/authoring-surface/answer.md +19 -3
- package/docs/reference/authoring-surface/arg.md +27 -5
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/command.md +36 -21
- package/docs/reference/authoring-surface/fail.md +17 -4
- package/docs/reference/authoring-surface/file.md +36 -16
- package/docs/reference/authoring-surface/http.md +108 -8
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +1 -1
- package/docs/reference/authoring-surface/open.md +36 -5
- package/docs/reference/authoring-surface/paramsof.md +22 -4
- package/docs/reference/authoring-surface/pool.md +4 -4
- package/docs/reference/authoring-surface/push.md +30 -12
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/secret.md +25 -4
- package/docs/reference/authoring-surface/source.md +3 -3
- package/docs/reference/authoring-surface/type-agentdefinition.md +2 -2
- package/docs/reference/authoring-surface/type-agentslots.md +4 -1
- package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
- package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
- package/docs/reference/authoring-surface/type-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +5 -6
- package/docs/reference/authoring-surface/type-commandinput.md +4 -3
- package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
- package/docs/reference/authoring-surface/type-commandoutput.md +22 -4
- package/docs/reference/authoring-surface/type-customize.md +4 -2
- package/docs/reference/authoring-surface/type-dispatchoutput.md +8 -1
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-filebody.md +4 -8
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +2 -4
- package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +7 -1
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +2 -2
- package/docs/reference/authoring-surface/type-pushinput.md +22 -3
- package/docs/reference/authoring-surface/type-pushoutput.md +32 -7
- package/docs/reference/authoring-surface/type-reposlot.md +5 -5
- package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
- package/docs/reference/authoring-surface/type-slothandles.md +5 -13
- package/docs/reference/authoring-surface/type-sourcespec.md +10 -10
- package/docs/reference/authoring-surface/type-workspaced.md +29 -8
- package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
- package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
- package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
- package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
- package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
- package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
- package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
- package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
- package/docs/reference/authoring-surface/workspace.md +65 -37
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +30 -10
- package/docs/reference/cli.md +1 -1
- package/docs/reference/config.md +13 -4
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +17 -10
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +29 -19
- package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +30 -30
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +35 -17
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +8 -2
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +6 -4
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +21 -9
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +2 -1
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +8 -9
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +62 -65
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +9 -1
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +13 -12
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +4 -1
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +18 -19
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +8 -8
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +4 -4
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +24 -24
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +5 -3
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +110 -67
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +101 -74
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +5 -6
- 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
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +181 -153
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +74 -63
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +31 -20
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions.md +3 -0
- package/docs/reference/glossary.md +51 -12
- package/docs/reference/http-api.md +45 -7
- package/docs/reference/machines.md +0 -1
- package/docs/why.md +5 -4
- package/package.json +3 -3
- package/src/actor.ts +161 -35
- package/src/agent.ts +6 -6
- package/src/ambient.ts +3 -5
- package/src/chart.ts +186 -19
- package/src/charts.ts +9 -2
- package/src/config.ts +16 -6
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +251 -77
- package/src/customize.ts +11 -3
- package/src/effect-checks.ts +50 -3
- package/src/effect.ts +343 -127
- package/src/effector-wire.ts +6 -3
- package/src/fail.ts +17 -4
- package/src/harness-client.ts +74 -31
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +125 -16
- package/src/holds.ts +3 -0
- package/src/http-effect.ts +88 -32
- package/src/http.ts +56 -17
- package/src/index.ts +15 -1
- package/src/internal.ts +1 -0
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +3 -4
- package/src/model-keys.ts +24 -0
- package/src/names.ts +19 -6
- package/src/notices.ts +443 -54
- package/src/open-body.ts +85 -19
- package/src/open.ts +36 -5
- package/src/parts.ts +40 -28
- package/src/party.ts +33 -0
- package/src/pick.ts +41 -7
- package/src/pool.ts +44 -23
- package/src/registration.ts +53 -11
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +20 -2
- package/src/run-host.ts +106 -20
- package/src/sandbox-effect.ts +217 -66
- package/src/sandbox-kube.ts +63 -16
- package/src/secret-placement.ts +9 -2
- package/src/setup.ts +86 -6
- package/src/tokens.ts +107 -32
- package/src/vocabulary.ts +23 -4
- package/src/wire.ts +10 -10
- package/src/workspace.ts +264 -253
- package/src/worktree.ts +249 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
- package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
|
@@ -16,21 +16,28 @@ action observably occurred only post-approval.
|
|
|
16
16
|
|
|
17
17
|
The split-channel model:
|
|
18
18
|
|
|
19
|
-
| Direction | Kind
|
|
20
|
-
| --------- |
|
|
21
|
-
| up | control / decision (the workflow's events, ADR-0011)
|
|
22
|
-
| up | progress / telemetry (tokens, attempts, "still working")
|
|
23
|
-
| down | the next turn's prompt (feedback, steering at a boundary)
|
|
24
|
-
| down | the Turn part, restated (the Allowed picks changed)
|
|
25
|
-
| down |
|
|
19
|
+
| Direction | Kind | Mechanism |
|
|
20
|
+
| --------- | -------------------------------------------------------------- | --------------------------------------------- |
|
|
21
|
+
| up | control / decision (the workflow's events, ADR-0011) | Menu tool call → Custodian → delivery |
|
|
22
|
+
| up | progress / telemetry (tokens, attempts, "still working") | Harness stream (lossy, observation-only) |
|
|
23
|
+
| down | the next turn's prompt (feedback, steering at a boundary) | a new submission on the same iid |
|
|
24
|
+
| down | the Turn part, restated (the Allowed picks changed) | the Harness's read at each step boundary |
|
|
25
|
+
| down | a jr2 notice (a memory kill, ADR-0062), typed, to its audience | the same step-boundary read, per conversation |
|
|
26
|
+
| down | interrupts (deliberate terminal cancel) | `abort` on the wire |
|
|
26
27
|
|
|
27
28
|
Tool calls are Agent-initiated, so the Machine speaks inside a Turn only as an answer. The one exception is the
|
|
28
29
|
**restated Turn part**: when something other than the Agent's own pick changes its Allowed picks — a Gate's answer, an
|
|
29
30
|
outside event — the Harness learns it by reading the surface at each step boundary and appends the new list
|
|
30
31
|
([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)). It is a pull, not a push:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
the Harness asks at a step boundary, and nothing reaches it that it did not ask for. What is restated is fixed text plus
|
|
33
|
+
Menu names. jr2's own **typed notices** ride that same step-boundary read: the read of an open Turn returns its
|
|
34
|
+
conversation's pending notices, the Harness appends them, and its acknowledgment of the append delivers them. So a
|
|
35
|
+
running Agent hears a fact the mechanism knows — a memory kill above all — without an interrupt. A notice is typed data,
|
|
36
|
+
and the Harness words it ([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)). The residual case is
|
|
37
|
+
**free-text human steering** — arbitrary text from a human Caller to an Agent that has not asked. jr2 reserves it for v1
|
|
38
|
+
on this same channel, authenticated as a Caller ([ADR-0011](0011-workflow-defined-events.md)) rather than by a
|
|
39
|
+
Sandbox-scoped credential; its sender and authorization are designed with the Caller. It is one behavior, not a
|
|
40
|
+
dependency of the whole control plane.
|
|
34
41
|
|
|
35
42
|
## Durability: the admission is the handle
|
|
36
43
|
|
|
@@ -91,34 +91,40 @@ fast setup.
|
|
|
91
91
|
|
|
92
92
|
## Where setup runs
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
[ADR-0063](0063-the-orchestrator-watches-the-cluster.md)), once `Ready` says every cache is present and fetched.
|
|
94
|
+
The clone is a post-`Ready` step owned by the Orchestrator: the Sandbox CRD names repositories by identity so the
|
|
95
|
+
operator can place and mount them, but it knows nothing about clones or worktrees (ADR-0001, ADR-0051). `workspace()`'s
|
|
96
|
+
attach is one idempotent in-pod script the Harness runs on `POST /attach` (ADR-0012,
|
|
97
|
+
[ADR-0063](0063-the-orchestrator-watches-the-cluster.md)), once `Ready` says every cache is present and fetched. The
|
|
98
|
+
attach only clones. A Machine cuts each Worktree with the kit Effect `worktree()`
|
|
99
|
+
([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)).
|
|
98
100
|
|
|
99
101
|
## In-Sandbox layout: worktrees off the per-Sandbox clone (gwtmux convention)
|
|
100
102
|
|
|
101
103
|
"Not a shared worktree" above means _not a worktree off a fleet-shared `.git`_. **Inside** a Sandbox, worktrees are the
|
|
102
|
-
intended ergonomic — taken off the Sandbox's **own** clone. The per-Sandbox clone _is_ the `default/`, and
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
intended ergonomic — taken off the Sandbox's **own** clone. The per-Sandbox clone _is_ the `default/`, and the Worktrees
|
|
105
|
+
are its siblings, matching the layout [gwtmux](https://github.com/snapwich/gwtmux) already expects, under the slot the
|
|
106
|
+
Machine gave the repository (ADR-0051):
|
|
105
107
|
|
|
106
108
|
```
|
|
107
|
-
/work/<slot>/default/ the per-Sandbox `git clone --shared` (holds the pod-local .git;
|
|
108
|
-
/work/<slot>/<
|
|
109
|
+
/work/<slot>/default/ the per-Sandbox `git clone --shared` (holds the pod-local .git; stays on the Binding's base)
|
|
110
|
+
/work/<slot>/<name>/ `git worktree add` siblings: folders a Machine cuts, each with one branch or one commit
|
|
109
111
|
```
|
|
110
112
|
|
|
111
113
|
Because the layout is identical to the one used outside the cluster, existing worktree tooling works **unchanged** when
|
|
112
|
-
you exec into a Sandbox. Object resolution falls through: a
|
|
114
|
+
you exec into a Sandbox. Object resolution falls through: a Worktree shares the per-Sandbox `default/.git`, whose
|
|
113
115
|
alternates point at the read-only cache — existing objects are read from the cache, new commits land in the pod-local
|
|
114
|
-
`.git`. `default/`
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
116
|
+
`.git`. `default/` stays on the Binding's base: a branch is attached, a tag or a commit is detached, and with no base it
|
|
117
|
+
is the Repo's default branch, as gwtmux's `default/` is outside the cluster. No work happens in it: work happens in the
|
|
118
|
+
Worktrees, and `default/` is the reference tree that a human, a tool or an Agent that only reads finds where it expects
|
|
119
|
+
one. Not `--no-checkout`: an empty working tree reads as every tracked file deleted to `git status`, which misleads an
|
|
120
|
+
Agent that looks. A Worktree's folder cannot be named `default`, and a cut of the branch that `default/` holds is
|
|
121
|
+
`refused`, with a reason that names `/work/<slot>/default`. `origin`'s push url is the binding's own spelling, so a
|
|
122
|
+
Machine that bound over ssh pushes over ssh even when the cache was cloned over https.
|
|
123
|
+
|
|
124
|
+
This does **not** reintroduce jr's contention: the shared `.git` here is _per-Sandbox_, so what contends stays in one
|
|
125
|
+
pod and never crosses the fleet. Inside the pod it is the author's choice. A Machine that runs concurrent Agents in
|
|
126
|
+
Worktrees of one clone, or an Agent that spawns parallel git-touching subagents, takes that contention, and a Machine
|
|
127
|
+
that does not want it uses separate Sandboxes. Two things keep one clone safe to share. The attach sets `gc.auto=0` and
|
|
128
|
+
`maintenance.auto=false` in the clone, because a `gc` beside a cut or a commit can prune what another Worktree needs.
|
|
129
|
+
And all Worktrees of a clone share `refs/remotes/origin/*`, so `jr2-upload-pack` holds a per-clone lock for each fetch
|
|
130
|
+
(ADR-0053, ADR-0075).
|
|
@@ -6,14 +6,20 @@ A Sandbox pod composes up to four containers around one shared worktree volume (
|
|
|
6
6
|
[ADR-0037](0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md)) with jr2's runtime mounted at
|
|
7
7
|
`/opt/jr2` and the command overridden to start the Harness. The Agent's toolchain lives here because the Working tools
|
|
8
8
|
(read/write/edit/bash) execute here — the tools the Agent can reach are this image's. It is also where a human
|
|
9
|
-
`kubectl exec`s to inspect a run with the agent's exact tools, worktrees, and filesystem.
|
|
9
|
+
`kubectl exec`s to inspect a run with the agent's exact tools, worktrees, and filesystem. One Harness container serves
|
|
10
|
+
every workspace-ful Agent of the Sandbox. Each process that the Harness starts in `/work` — a Working tool's shell, a
|
|
11
|
+
test, git, a `command` Effect — runs as a confined child of the Harness process, so no code from `/work` reaches the
|
|
12
|
+
control credential that the Harness process holds
|
|
13
|
+
([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)).
|
|
10
14
|
- **Custodian container** — jr2-composed sidecar, upstream Envoy configured at provision by the Orchestrator
|
|
11
15
|
([ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md),
|
|
12
16
|
[ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)): the pod's credential holder. It holds
|
|
13
17
|
the Sandbox token and the held secrets its pod names — model API keys above all — as files, and carries the Harness's
|
|
14
18
|
traffic out: its Menu and the ask to the Orchestrator, and its outbound HTTPS. It exists as a separate container
|
|
15
19
|
precisely _because_ the Working tools give the Agent code execution in the Harness container — the credentials live
|
|
16
|
-
where the Agent cannot read them, and the Harness container
|
|
20
|
+
where the Agent cannot read them, and the Harness container holds only their Stand-ins, which are secret: only the
|
|
21
|
+
Harness process holds them, as files, and a process it confines gets only those that its `workspace()` grants
|
|
22
|
+
(ADR-0059, ADR-0076).
|
|
17
23
|
- **User Container** — opt-in third seat, composed only when the `workspace()` names its image (a `file:` docker context
|
|
18
24
|
the Machine ships or a registry ref, the same resolution as the Sandbox Image, no default —
|
|
19
25
|
[ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)). The authoring surface is that string —
|
|
@@ -63,8 +69,8 @@ What each container mounts of the credentials, stated once:
|
|
|
63
69
|
|
|
64
70
|
| Container | Mounts |
|
|
65
71
|
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
66
|
-
| `harness` | `jr2-ca` at `/etc/jr2/ca` (the trust bundles). None of the Custodian's volumes.
|
|
67
|
-
| `custodian` | its values (the Sandbox token, its pod's `jr2-held-secrets-<pod>`, each user `secretKeyRef` its pod names) at `/etc/jr2/custodian/values`; its leaves (`jr2-held-tls`); its pod's config (its own `<sandbox>-custodian`, and `program.lua` from `jr2-held`); `upstream.crt`.
|
|
72
|
+
| `harness` | `jr2-ca` at `/etc/jr2/ca` (the trust bundles); the Stand-ins — the control one and each held secret's — as files under `/run/jr2/` that only the Harness process reads and no process it confines can (ADR-0059, ADR-0076); no Stand-in is in its env. None of the Custodian's volumes. |
|
|
73
|
+
| `custodian` | its values (the Sandbox's control token and fetch token, its pod's `jr2-held-secrets-<pod>`, each user `secretKeyRef` its pod names) at `/etc/jr2/custodian/values`; its leaves (`jr2-held-tls`); its pod's config (its own `<sandbox>-custodian`, and `program.lua` from `jr2-held`); `upstream.crt`. |
|
|
68
74
|
| `user` | the checkouts' three volumes, and nothing else. |
|
|
69
75
|
| `effector` | `/work`, `/repos/<key>` and `/opt/jr2` read-only; at `/run/jr2/secrets` (0400) the secrets its Effects name — its pod's `jr2-effector-<pod>` literals, each user `secretKeyRef` by reference — and at `@git/<name>/` each `git.credentials` entry's push Secret the Sandbox CR names (`gitCredentials`), which the operator projects from that entry's Secret by reference into this container alone (`@` is no character of a secret's name, so no secret's file takes that directory); at `/run/jr2/effector` its key (its own `<sandbox>-effector`) and `effector.json`; an emptyDir at `/run/jr2/scratch` as `$HOME`; at `/etc/jr2/effector-ca`, read-only, the instance's caBundle ConfigMap when the instance ships one (ADR-0020). |
|
|
70
76
|
|
|
@@ -98,12 +104,13 @@ jr2 sets `runAsUser` nowhere — each image's own `USER` decides its seat's uid
|
|
|
98
104
|
and plain POSIX modes would then make the other seat's files read-only. jr2 closes that with two mechanisms it happens
|
|
99
105
|
to own, unconditionally (both are inert when the uids already match):
|
|
100
106
|
|
|
101
|
-
- **The pod carries `fsGroup` — the work group, default `2000`.** The default is convention; the one override
|
|
102
|
-
|
|
103
|
-
ADR-0037), for the image whose session users already hold a gid of their own. Never a config key.
|
|
104
|
-
for the jr2 seats: Kubernetes grants the fsGroup as a supplemental group to every container process,
|
|
105
|
-
writes `/work` whatever the number, and no image's `/etc/group` needs to know it. fsGroup puts a setgid
|
|
106
|
-
`/work`'s root, and setgid propagates down, so everything either seat creates is group-_owned_ by the work
|
|
107
|
+
- **The pod carries `fsGroup` — the work group, default `2000`.** The default is convention; the one override is an
|
|
108
|
+
option of `workspace()` (`workspace(body, { workGroup: 2000 })`, beside `image` and `user` — pod composition is the
|
|
109
|
+
wrapper's business, ADR-0037), for the image whose session users already hold a gid of their own. Never a config key.
|
|
110
|
+
Any value is safe for the jr2 seats: Kubernetes grants the fsGroup as a supplemental group to every container process,
|
|
111
|
+
so the Harness writes `/work` whatever the number, and no image's `/etc/group` needs to know it. fsGroup puts a setgid
|
|
112
|
+
group on `/work`'s root, and setgid propagates down, so everything either seat creates is group-_owned_ by the work
|
|
113
|
+
group.
|
|
107
114
|
- **The attach stamps a default ACL on every repo root it creates** — `u::rwx,g::rwx,o::r-x`, set by `work-acl`, a
|
|
108
115
|
static helper on the runtime volume (part of ADR-0037's published `/opt/jr2` surface, static for the same reason `rg`
|
|
109
116
|
is: it executes on a libc jr2 does not control). A default ACL is inherited by everything created beneath it, and
|
|
@@ -116,16 +123,16 @@ to own, unconditionally (both are inert when the uids already match):
|
|
|
116
123
|
Group _membership_ is the one thing an image can still owe. `kubectl exec` sessions and a non-root sshd's logins hold
|
|
117
124
|
the work group automatically — both inherit the container's supplemental groups, where Kubernetes put the fsGroup. A
|
|
118
125
|
**root** sshd is the exception: the logins it setuids rebuild their groups from `/etc/group`, so that image puts its
|
|
119
|
-
session user in the work group itself (`groupadd -g 2000 work && usermod -aG work <user>`) — or the
|
|
126
|
+
session user in the work group itself (`groupadd -g 2000 work && usermod -aG work <user>`) — or the `workspace()` points
|
|
120
127
|
`workGroup` at a gid its users already hold.
|
|
121
128
|
|
|
122
129
|
**The promise is scoped to repo trees.** An ad-hoc path at `/work`'s top level — either seat's `mkdir /work/scratch` —
|
|
123
130
|
carries no default ACL, so files there fall back to the writer's umask. The Harness runs at `umask 002` (itself and
|
|
124
131
|
every tool child) and the attach script opens with the same line, so jr2's own writes are group-writable even there;
|
|
125
132
|
what a User Container session writes outside a repo tree is its own business. A Workspace with no Repo Slot (ADR-0051)
|
|
126
|
-
has no repo tree: its steps share `/work` itself, every Agent and `command` of it
|
|
127
|
-
|
|
128
|
-
more.
|
|
133
|
+
has no repo tree: its steps share `/work` itself, every Agent and `command` of it at the one uid in the one Harness
|
|
134
|
+
container, so they need no ACL; a User Container's writes there have this paragraph's limit, until a user of that seat
|
|
135
|
+
needs more.
|
|
129
136
|
|
|
130
137
|
## Constraints on the seat
|
|
131
138
|
|
|
@@ -134,8 +141,8 @@ The seat is zero-_contract_, not zero-_physics_. What an image brought here must
|
|
|
134
141
|
- **The entrypoint must block.** The pod's restart policy restarts an exited container, so a `CMD` that finishes
|
|
135
142
|
crash-loops. Run a long-lived process — an sshd, an IDE server, `sleep infinity` at minimum.
|
|
136
143
|
- **The ports of jr2's seats are taken** — one network namespace: `:8080` (the Harness), the Custodian's
|
|
137
|
-
`127.0.0.1:8081` (
|
|
138
|
-
pod that has one). Everything else is the image's. A process that binds `:8082` while the Sandbox Effector is down
|
|
144
|
+
`127.0.0.1:8081` (the fetch), `127.0.0.1:15001` (egress) and `:15021` (health), and `:8082` (the Sandbox Effector, in
|
|
145
|
+
a pod that has one). Everything else is the image's. A process that binds `:8082` while the Sandbox Effector is down
|
|
139
146
|
gets signed requests it can neither forge nor answer (ADR-0068): the Effect fails closed.
|
|
140
147
|
- **Root is allowed.** The hardened security context (non-root, all capabilities dropped, no escalation) applies to the
|
|
141
148
|
seats jr2 owns — Harness, Custodian and Sandbox Effector, per container — and deliberately not here: the seat's
|
|
@@ -162,7 +169,10 @@ namespace, node, and the worktree volume, and are one trust domain in every isol
|
|
|
162
169
|
container edge does provide is **credential visibility** — the asymmetry ADR-0013 builds on: the Agent executes code in
|
|
163
170
|
the Harness container and none in the others, so a secret that exists only in another container's mount namespace is out
|
|
164
171
|
of its reach. That is why the Custodian holds the Orchestrator credential and the model keys, and it is what the User
|
|
165
|
-
Container offers a human session
|
|
172
|
+
Container offers a human session. Inside the Harness container, a second edge holds the one credential the Harness keeps
|
|
173
|
+
itself, the control Stand-in: the Harness process is jr2's code, and each process it starts in `/work` is confined from
|
|
174
|
+
that Stand-in by Landlock (ADR-0076, [ADR-0077](0077-jr2-requires-landlock-on-every-sandbox-node.md)). The edges, stated
|
|
175
|
+
plainly:
|
|
166
176
|
|
|
167
177
|
- It holds only for what stays in the private filesystem or session. Anything on `/work` or listening on `localhost` is
|
|
168
178
|
shared with the Agent — the pod has one network namespace. The Custodian's listeners are on `localhost` too; what they
|
|
@@ -206,8 +216,8 @@ Container offers a human session — with its edges stated plainly:
|
|
|
206
216
|
|
|
207
217
|
- The generic `Sandbox` CRD composes all of this as plain container specs
|
|
208
218
|
([ADR-0001](0001-operator-owned-generic-sandbox.md) holds — the operator stays agent-agnostic); the Orchestrator's
|
|
209
|
-
`kubectlSandbox` supplies the images. The pod runs two containers, plus `user` when the
|
|
210
|
-
when an Effect is placed there.
|
|
219
|
+
`kubectlSandbox` supplies the images. The pod runs two containers, plus `user` when the `workspace()` names one and
|
|
220
|
+
`effector` when an Effect is placed there.
|
|
211
221
|
- Every container but the Sandbox Effector carries a limit from the Workspace's Size (ADR-0060): the Custodian a fixed
|
|
212
222
|
kit slice, the User Container its split if stated, the Harness the rest. The Sandbox Effector carries no limit of its
|
|
213
223
|
own: like a User Container with no split, it shares the pod's Size ceiling — the whole Size, as pod-level resources —
|
|
@@ -42,6 +42,9 @@ is the only structured channel. Consequences:
|
|
|
42
42
|
|
|
43
43
|
## Residual
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
jr2's own typed notices now reach a running Agent through the step-boundary read (ADR-0002,
|
|
46
|
+
[ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)); the Menu still opens only when the Agent
|
|
47
|
+
reaches a checkpoint or a turn boundary. Free-text human steering is the remaining case, reserved for v1 as a
|
|
48
|
+
Caller-authenticated message on the same channel (ADR-0002, [ADR-0011](0011-workflow-defined-events.md)). Evidence for
|
|
49
|
+
the findings above: `poc/actor/POC-5B-FINDINGS.md`, `poc/actor/POC-5C-FINDINGS.md` (`poc/` is gitignored; conclusions
|
|
50
|
+
live here).
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# `workspace()` wraps a body Machine and owns only Sandbox lifecycle
|
|
2
2
|
|
|
3
3
|
jr2 provides Workspaces as a helper so workflows never provision Sandboxes themselves:
|
|
4
|
-
`workspace(body, { input, repos
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
`workspace(body, { input, repos })` is a statically-imported factory (per [ADR-0011](0011-workflow-defined-events.md)'s
|
|
5
|
+
import doctrine) returning a Machine that provisions a Sandbox and clones its Repos, runs the author's **body** Machine
|
|
6
|
+
inside it, and tears the Sandbox down when the body finishes. This makes GLOSSARY.md's "Workspace = child Machine bound
|
|
7
|
+
to a unit of work" concrete.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
placing: create Sandbox CR → await the pod scheduled, no deadline (ADR-0064)
|
|
11
|
-
provisioning: await phase: Ready (watched, ADR-0063) → Harness /attach repos
|
|
12
|
-
running: invoke body; input = parent input + { workspace: { repos
|
|
11
|
+
provisioning: await phase: Ready (watched, ADR-0063) → Harness /attach repos (ADR-0004)
|
|
12
|
+
running: invoke body; input = parent input + { workspace: { repos } }
|
|
13
13
|
teardown: destroy the CR
|
|
14
14
|
done: final; workspace output = body output
|
|
15
15
|
```
|
|
@@ -25,29 +25,29 @@ already is. When the guarantee still fails (`kill -9`, hard cancel), the operato
|
|
|
25
25
|
(ADR-0001) — the reason ADR-0001 made the CR the durable desired state. Author-assembled provisioning/cleanup states
|
|
26
26
|
(the obvious alternative) put the guarantee in every workflow's hands to forget, on every error path.
|
|
27
27
|
|
|
28
|
-
## Boundary: the
|
|
28
|
+
## Boundary: the options speak workspace vocabulary only
|
|
29
29
|
|
|
30
|
-
The
|
|
31
|
-
workspace-domain: the branch to cut, and what the attach needs to cut it. The branch itself is the author's: its name
|
|
32
|
-
and its granularity (per feature, per task, from a ticket, from run input) are the Machine's policy, not jr2's; the
|
|
33
|
-
workspace only cuts it. Which Repos to attach is not per-run at all: they are slots on the wrapper's options
|
|
30
|
+
The wrapper's options are workspace-domain. Which Repos to attach is not per-run at all: they are slots on the options
|
|
34
31
|
(`repos: { target: open, docs: { url, ref } }`, ADR-0051), static so `jr2 up` can see them, each bound by the Machine,
|
|
35
|
-
by a composer's `customize`, or by a mapper over the door.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
`
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
by a composer's `customize`, or by a mapper over the door. The Workspace only clones. A branch and a Worktree are the
|
|
33
|
+
body's: their names and their granularity (per feature, per task, from a ticket, from run input) are the Machine's
|
|
34
|
+
policy, not jr2's, and the body cuts each Worktree with `worktree()`
|
|
35
|
+
([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)). Workflow configuration (review rounds,
|
|
36
|
+
budgets, ticket data) passes through to the body untouched; the workspace never sees it. In return the body gets what it
|
|
37
|
+
needs to operate inside the Sandbox: `{ repos: Record<slot, path> }`, the handles keyed by the slots the wrapper
|
|
38
|
+
declared, in declaration order. A handle is the slot's folder, `/work/<slot>`, and not a Worktree. No handle names a
|
|
39
|
+
primary checkout: which tree an Agent works in is a fact about that Agent's Turn, which the body states (ADR-0051). The
|
|
40
|
+
Harness `endpoint` and the Sandbox name are mechanism-facing, not body-facing — `agentRun` resolves them **ambiently**
|
|
41
|
+
from the enclosing wrapper (a registrar actor co-invoked in `running`, ADR-0016), so the body cannot mis-thread them.
|
|
42
|
+
Keeping this boundary is what lets one helper serve any workflow rather than coupling to coding-shaped ones.
|
|
43
43
|
|
|
44
|
-
The wrapper's `input`
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
The wrapper's `input` is its door — the wrapper's own declared run-input schema, which also types a per-run slot's
|
|
45
|
+
mapper ([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)). It sits on the wrapper and not on the body
|
|
46
|
+
precisely because of the injection above: the body's input is the door plus `workspace`, which no caller sends. That
|
|
47
|
+
same composition is what the door checks the body against, one way only — the body may not demand more than the wrapper
|
|
48
|
+
hands it; demanding less is safe — so the injection is a compile-time contract, not just a comment. The host's own
|
|
49
|
+
injection into a root machine's input (`instanceId`) counts as handed over for that check, because whether it actually
|
|
50
|
+
reaches this body depends on where the wrapper sits and no type can see that
|
|
51
51
|
([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)).
|
|
52
52
|
|
|
53
53
|
## An Effect runs in its Workspace: the Harness container or the Sandbox Effector
|
|
@@ -58,8 +58,8 @@ Worktree file and a `command` that names a secret run in that Workspace's Sandbo
|
|
|
58
58
|
container ([ADR-0005](0005-sandbox-pod-composition.md),
|
|
59
59
|
[ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), which the provision adds only when an
|
|
60
60
|
Effect below the `workspace()` is placed there; its image is the Sandbox Image unless the `workspace()` names
|
|
61
|
-
`effector: { image }`. Each one's Worktree follows the Turn's rule (ADR-0057):
|
|
62
|
-
|
|
61
|
+
`effector: { image }`. Each one's Worktree follows the Turn's rule (ADR-0057): its input names it, and under Repo Slots
|
|
62
|
+
it must. A push goes to the Binding of its Repo Slot, the one slot or the one its static `slot` names, and after every
|
|
63
63
|
push whose published commit is not the Agent's own — signed copies, or one squashed commit, signed or not — the
|
|
64
64
|
Workspace's Harness moves the Agent's branch to the published commits (the sync, ADR-0067). An Effect of these kinds
|
|
65
65
|
outside any `workspace()` is refused at invoke, as an error, never as output. A `command` that names a secret runs
|
|
@@ -86,8 +86,8 @@ the same contract as a temp directory.
|
|
|
86
86
|
|
|
87
87
|
## Consequences
|
|
88
88
|
|
|
89
|
-
-
|
|
90
|
-
|
|
89
|
+
- The body chooses how many Agents work in its Workspace (ADR-0004, ADR-0075). The wrapper has no lock machinery for
|
|
90
|
+
them.
|
|
91
91
|
- The workspace labels the CR with its run (`jr2.dev/run`, `jr2.dev/workflow`), which is what `jr2 ls` groups by
|
|
92
92
|
(ADR-0009).
|
|
93
93
|
- **Lost workspace: the wrapper emits, the body decides.** While the body runs, a lease actor beside it renews the
|
|
@@ -25,16 +25,21 @@ is needed either (ADR-0006).
|
|
|
25
25
|
## Decision
|
|
26
26
|
|
|
27
27
|
- **The Custodian carries the Agent's Menu, and holds the credential that makes it work.** It is a jr2-composed
|
|
28
|
-
container in every Harness pod ([ADR-0005](0005-sandbox-pod-composition.md)).
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
container in every Harness pod ([ADR-0005](0005-sandbox-pod-composition.md)). A Sandbox has two tokens, its **Sandbox
|
|
29
|
+
tokens**: the **control token** and the **fetch token**, mounted into the Custodian as files, and into no other
|
|
30
|
+
container. The Harness process holds the control token's Stand-in, which is secret: a file that no process it confines
|
|
31
|
+
can read, in no child's env
|
|
32
|
+
([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)). The fetch token's Stand-in
|
|
33
|
+
is public, `jr2-held-JR2_SANDBOX_TOKEN`, and `jr2-upload-pack` holds it as a constant. The Harness reaches the
|
|
34
|
+
Orchestrator only through the Custodian: the agent routes on its control socket, `/run/jr2/custodian/control.sock`,
|
|
35
|
+
which no confined program can replace (ADR-0076), and the fetch on `127.0.0.1:8081`. The Custodian forwards five
|
|
36
|
+
routes, with each route's own token put in place of its Stand-in, and answers every other route itself:
|
|
33
37
|
|
|
34
38
|
```
|
|
35
39
|
GET /agents/<iid>/surface the Menu → the Orchestrator
|
|
36
40
|
POST /agents/<iid>/events a pick → the Orchestrator
|
|
37
41
|
GET /agents/<iid>/deliveries/<id> an outstanding pick's answer → the Orchestrator
|
|
42
|
+
POST /agents/<iid>/notices a step boundary's notices heard → the Orchestrator
|
|
38
43
|
POST /fetch the ask (ADR-0053), for THIS pod's Sandbox → POST /sandboxes/<name>/fetch
|
|
39
44
|
anything else 404
|
|
40
45
|
```
|
|
@@ -60,9 +65,10 @@ is needed either (ADR-0006).
|
|
|
60
65
|
POST /runs/:id/gates/:gate/events → validate + deliver, or 409 [Instance token]
|
|
61
66
|
|
|
62
67
|
# the Agent's, through its pod's Custodian
|
|
63
|
-
GET /agents/:iid/surface → Menu + Allowed picks + schemas + semantics [
|
|
64
|
-
POST /agents/:iid/events → validate + deliver → the receipt, or 409 [
|
|
65
|
-
GET /agents/:iid/deliveries/:id → an outstanding pick's answer, long-polled [
|
|
68
|
+
GET /agents/:iid/surface → Menu + Allowed picks + schemas + semantics [control token]
|
|
69
|
+
POST /agents/:iid/events → validate + deliver → the receipt, or 409 [control token]
|
|
70
|
+
GET /agents/:iid/deliveries/:id → an outstanding pick's answer, long-polled [control token]
|
|
71
|
+
POST /agents/:iid/notices → the notices a step boundary appended, heard (ADR-0062) [control token]
|
|
66
72
|
```
|
|
67
73
|
|
|
68
74
|
The receipt is `{ delivered, event, moved, turnComplete, allowed, deliveryId }`. A pick the workflow is still running
|
|
@@ -86,11 +92,13 @@ is needed either (ADR-0006).
|
|
|
86
92
|
- **Bearer tokens, because the boundary is otherwise theater.** The Harness container shares the pod's network
|
|
87
93
|
namespace, so an Agent can `curl` the Orchestrator directly; a per-pod NetworkPolicy cannot distinguish it from the
|
|
88
94
|
Custodian. Only authentication closes this.
|
|
89
|
-
- **
|
|
90
|
-
the same
|
|
91
|
-
land on the Harness container, never the Custodian.
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
- **Control token and fetch token** — minted per Sandbox at provision (signed names, so re-provision after a restart
|
|
96
|
+
yields the same tokens), delivered as a Secret mounted into the **Custodian container only**. User
|
|
97
|
+
`harness.env`/`envFrom` land on the Harness container, never the Custodian. The control token authorizes exactly:
|
|
98
|
+
read and deliver to agent registrations **whose Sandbox is this one** and whose slot is the slot the Harness states
|
|
99
|
+
for the conversation (ADR-0076). The fetch token authorizes exactly: ask for this Sandbox's fetch. The Orchestrator
|
|
100
|
+
accepts each token only on its own routes. Neither reaches a Gate: a Gate refuses every principal of an Agent.
|
|
101
|
+
Neither reaches another Sandbox. A Stand-in is refused on every route.
|
|
94
102
|
- **Instance token** — the human/CLI credential for Gates and run control. Minted into an in-cluster Secret at
|
|
95
103
|
`jr2 up`; the CLI reads it over the kube API (RBAC is the gate) while port-forwarding the Service (ADR-0019).
|
|
96
104
|
- **The Instance-token door.** The Instance token may also read any Agent's surface and deliver to it —
|
|
@@ -98,13 +106,17 @@ is needed either (ADR-0006).
|
|
|
98
106
|
AS the Agent: it reads the Agent's Menu and Allowed picks and its picks are judged as the Agent's — they carry the
|
|
99
107
|
Agent's sender (ADR-0011) — and no audience filter applies, so it can send a pick no Gate's caller may send
|
|
100
108
|
([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)). It is deliberate — it is how the e2e tier stands in
|
|
101
|
-
for an Agent — and it adds no party: neither `jr2 send` nor the Console offers it, and a
|
|
109
|
+
for an Agent — and it adds no party: neither `jr2 send` nor the Console offers it, and a control token never opens
|
|
102
110
|
it for a registration that is not its own.
|
|
103
111
|
- An agent registration records its Sandbox **ambiently**: `agentRun` resolves the enclosing `workspace()` and records
|
|
104
112
|
the same deterministic name the token was minted for (ADR-0016), so forgetting to pass it is unrepresentable (the
|
|
105
113
|
pre-ambient version shipped exactly that footgun: `sandbox` omitted, every tool call 403'd — fail-closed but
|
|
106
114
|
silent). A run-scoped token instead of a Sandbox-scoped one would be genuinely exploitable: derivable iids plus
|
|
107
|
-
readable ticket ids would let one feature's coder inject a `review_verdict` into another feature's reviewer.
|
|
115
|
+
readable ticket ids would let one feature's coder inject a `review_verdict` into another feature's reviewer. That
|
|
116
|
+
holds across Sandboxes, where another feature's Agents are in another pod. Within one Sandbox several Agents can
|
|
117
|
+
share it ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)); there the Sandbox scope is
|
|
118
|
+
not enough, and ADR-0076 keeps the control credential where no Workspace code runs, so a coder's code cannot act as
|
|
119
|
+
the reviewer beside it.
|
|
108
120
|
- **Reachability, narrowed to one caller.** The Orchestrator is always in-cluster (ADR-0019), so the route home is
|
|
109
121
|
simply **Service DNS**, which the pod's maker writes into the Custodian's configuration — the Orchestrator at a
|
|
110
122
|
Sandbox's provision, `jr2 up` for the Instance Harness. The Harness is told the Custodian's loopback address
|
|
@@ -140,12 +152,18 @@ is needed either (ADR-0006).
|
|
|
140
152
|
understanding; the Custodian is exactly that. `kubeSandbox` composes it (plus the token Secret, written right after
|
|
141
153
|
the CR with the CR as its owner from birth, so Kubernetes reaps it with the Sandbox and no path leaks it — ADR-0001).
|
|
142
154
|
- **Nothing in jr2 speaks MCP.** The Orchestrator, the Harness and the CLI carry no MCP dependency.
|
|
143
|
-
- **One container holds the Sandbox
|
|
144
|
-
container that holds both. ADR-0059 records the trade.
|
|
155
|
+
- **One container holds the Sandbox's tokens and every model key.** A fault in the Custodian's engine is a fault in the
|
|
156
|
+
one container that holds both. ADR-0059 records the trade.
|
|
145
157
|
- **The `@kind` e2e tier owns the pod → Orchestrator leg** — the pod's Harness actually calls its tool through the
|
|
146
158
|
Custodian, so a broken leg is a red test, and a Harness container that posts a pick straight to the Orchestrator with
|
|
147
159
|
the Stand-in it holds is refused. The mechanics tier plays `/agents/:iid/*` from the host, which is honestly
|
|
148
160
|
simulating the Harness's Menu, not an Agent.
|
|
161
|
+
- **Per-agent identity within one Sandbox is ADR-0076.** A Sandbox-scoped credential that an Agent's code could read
|
|
162
|
+
would let one Agent in a shared Sandbox deliver a pick as another, and ADR-0075 makes a shared Sandbox normal. Only
|
|
163
|
+
the Harness process holds the control credential, and each process it starts in `/work` runs confined; the Harness
|
|
164
|
+
states each conversation's slot, and the Orchestrator checks it on the whole `/agents/:iid/*` set. The `@kind` tier
|
|
165
|
+
gains the sibling cases: a coder's `bash` finds no control credential, and code it plants that the reviewer's own
|
|
166
|
+
`bash` runs cannot deliver a pick as the reviewer.
|
|
149
167
|
- **NetworkPolicy is a second layer, not the control.** It bounds where a pod may talk; only the token bounds what it
|
|
150
168
|
may _do_. `jr2 up` converges ingress policies that admit only the Orchestrator to a Harness pod
|
|
151
169
|
([ADR-0058](0058-a-harness-answers-the-orchestrator-alone.md)); the Orchestrator itself is left reachable, because a
|
|
@@ -143,8 +143,14 @@ sends there, and every case with the Agent's state at Rest.
|
|
|
143
143
|
**Load checks on modes and audience.** `createMachine` refuses a registered event that no state handles: it is in no
|
|
144
144
|
derived set, so it is on no Menu and no Gate. It refuses a parallel region below a state that invokes an Agent — the
|
|
145
145
|
Agent's state itself being parallel included, since its regions are below it: the shape is reserved for picks the Agent
|
|
146
|
-
does not wait for, which jr2 does not build, and background work goes above the Agent's state. It
|
|
147
|
-
|
|
146
|
+
does not wait for, which jr2 does not build, and background work goes above the Agent's state. It refuses one Agent slot
|
|
147
|
+
invoked by two states that can be active together — two parallel regions, one state inside the other, or one state
|
|
148
|
+
twice: a slot is one Agent of the Machine, so its two Turns would have one sender, which `sentBy` cannot tell apart, and
|
|
149
|
+
one conversation, which `continue` cannot give to both. Two Agents at once are two slots (one definition can fill both),
|
|
150
|
+
a child Machine in each region, or a Pool ([ADR-0075](0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md)).
|
|
151
|
+
jr2 refuses this shape and does not support it as a special case, because jr2 can remove a refusal later without
|
|
152
|
+
breaking a Machine, but a refusal that it adds later breaks each Machine that loads now. It warns — the Machine stays
|
|
153
|
+
legal; the doc carries the warning, `jr2 up` prints it and the Console shows it with the seats — about a mode that
|
|
148
154
|
nothing enters (not initial, and no state the Machine can enter has a transition into it or a state inside it — so
|
|
149
155
|
neither the mode's own `initial` nor its transition into itself counts: its picks are on the Menu and never allowed; a
|
|
150
156
|
transition into a history state enters that history state's default target, shallow or deep, or its parent's initial
|
|
@@ -21,7 +21,7 @@ restore re-attaches to a Submission whose model is already fixed server-side and
|
|
|
21
21
|
`agentRun` walks `self._parent` to the nearest registered ancestor. Invoked-actor registration (not an entry action) is
|
|
22
22
|
what makes this restore-safe: invoked actors restart on snapshot restore, entry actions do not. Not under a workspace →
|
|
23
23
|
fail loudly, unless input carries an explicit `endpoint` (the dev-stub, workspace-less path). Body-facing workspace
|
|
24
|
-
handles are `{ repos
|
|
24
|
+
handles are `{ repos }`: each is a Repo Slot's folder ([ADR-0012](0012-workspace-wrapper-machine.md)).
|
|
25
25
|
|
|
26
26
|
**[ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md)'s token scoping survives and
|
|
27
27
|
strengthens**: the registration records the Sandbox resolved from the enclosing wrapper — the same deterministic
|
|
@@ -108,12 +108,14 @@ modes — `continue` governs only what a _new invocation_ means. The author name
|
|
|
108
108
|
conversation's identity is already on the page. (This corrects the jr-parity exercise's "same iid = jr's resume
|
|
109
109
|
machinery, free" — an inversion of jr's actual semantics.)
|
|
110
110
|
|
|
111
|
-
Two live invocations of one continued iid
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
Two live invocations of one continued iid are refused: one live surface per address. The load walk refuses the shape
|
|
112
|
+
that makes them, one Agent slot in two states that can be active together
|
|
113
|
+
([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)), and the registration table refuses a second registration
|
|
114
|
+
of one address as a second guard. A Submission that arrives behind a live one on the Harness queues: prompts for one
|
|
115
|
+
instance enter one per-instance queue in admission order, and a Submission is promoted only when it is the first
|
|
116
|
+
unsettled one for its conversation (ADR-0027 states this as jr2's own semantics). A queued prompt waits behind work that
|
|
117
|
+
may never finish, which is why [ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md) both ends the
|
|
118
|
+
turn and orders the abort ahead of the next `send`.
|
|
117
119
|
|
|
118
120
|
The minting doctrine — addresses are computed by jr2, never by the workflow — covers gate ids too: a gate with no
|
|
119
121
|
authored id derives one from its actor path below the run root, through the same walk `mintIid` uses (`actorPath` in
|
|
@@ -22,10 +22,12 @@ outgrowing the pool means hand-rolling without penalty, not fighting a framework
|
|
|
22
22
|
|
|
23
23
|
## The source port is generalized; Work Source is one adapter
|
|
24
24
|
|
|
25
|
-
The pool draws from a **Source** port — `next(active) → item | null`, plus an optional `wake` event
|
|
26
|
-
`jr2 send` push seam) and `pollEvery` (the set mutates underneath us) — a queue, a generator, or a
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
The pool draws from a **Source** port — `next({ active, settled, input }) → item | null`, plus an optional `wake` event
|
|
26
|
+
def (webhook / `jr2 send` push seam) and `pollEvery` (the set mutates underneath us) — a queue, a generator, or a
|
|
27
|
+
re-queried set. `input` is the Pool's input and `settled` holds the ids of the finished and failed items, so a Source
|
|
28
|
+
can fan out over a plan with no state of its own. tk's claim actor is one adapter; GLOSSARY.md's **Work Source**
|
|
29
|
+
reframes as _the Source adapter for tickets_, and the Ready-set doctrine (re-query, never materialize) survives as that
|
|
30
|
+
adapter's semantics, not the primitive's.
|
|
29
31
|
|
|
30
32
|
A Work Source — `tk`, GitHub issues, Jira, a task queue — is a behavior port, not a canonical data schema: verbs like
|
|
31
33
|
`claimNext`, `updateStatus` and `comment`, each gated by a capability the adapter advertises. It owns the dependency
|