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