@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/chart.ts
CHANGED
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
// (open-body.ts), so that `jr2Setup`'s own refusals and load warnings are lines of the same report.
|
|
20
20
|
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
|
+
import { posix } from "node:path";
|
|
22
23
|
import { z } from "zod";
|
|
23
24
|
import { defineEvent, type EventAudience, type EventDef } from "@jr2/agent-protocol";
|
|
24
25
|
import type { AgentDefinition, ThinkingLevel, WorkspaceAccess } from "./agent.ts";
|
|
25
|
-
import { inputKeysOf, isEffect } from "./effect.ts";
|
|
26
|
+
import { inputKeysOf, inputShapeOf, isEffect, type InputShape } from "./effect.ts";
|
|
26
27
|
import { FAILURE_GUARD } from "./fail.ts";
|
|
27
28
|
import { compileChart, durationMs, type LentParts, type OpenBodyDeclaration } from "./open-body.ts";
|
|
28
29
|
import { SENT_BY } from "./party.ts";
|
|
@@ -140,6 +141,10 @@ const MECHANISM_EVENTS: Record<string, { fields: Record<string, boolean> }> = {
|
|
|
140
141
|
* placement; see {@link checkAgentInput}). */
|
|
141
142
|
const AGENT_INPUT_KEYS = ["prompt", "cwd", "continue", "model", "thinkingLevel"] as const;
|
|
142
143
|
|
|
144
|
+
/** The kit's test for a cut that settled `ready`, in every Chart beside `jr2.failure`: a cut's own
|
|
145
|
+
* outcomes are output, so a Chart routes on them, and a Chart has no other way to read one. */
|
|
146
|
+
export const READY_GUARD = "jr2.ready";
|
|
147
|
+
|
|
143
148
|
/**
|
|
144
149
|
* The Chart briefing (ADR-0070): jr2's text on how a Chart runs, for whoever writes one. It is the
|
|
145
150
|
* grammar's root description, so it rides `GET /workflows/<wrapper>/chart-schema` and a pick's tool
|
|
@@ -164,6 +169,16 @@ export function chartBriefing(narrow?: Narrow): string {
|
|
|
164
169
|
? ' `src: "fail"` with `input: { reason }` ends the run as a Failure: work that did not succeed, with ' +
|
|
165
170
|
`the reason for a person; an \`onError\` with the guard \`${FAILURE_GUARD}\` catches it. A final state means done.`
|
|
166
171
|
: "";
|
|
172
|
+
// A declaration is names alone, so this says "a lent Effect that cuts".
|
|
173
|
+
const cut =
|
|
174
|
+
narrow === undefined || narrow.lendsEffects
|
|
175
|
+
? ' In a Workspace with Repo Slots, work happens in a Worktree. A lent Effect that cuts one takes `input: { path: "/work/<slot>/<name>", ' +
|
|
176
|
+
'branch: "<branch>" }`: `<name>` is one folder name that you choose, and it is not the branch. The Open body sets the cut\'s `expect`, ' +
|
|
177
|
+
"so its input has no `expect`. Give its `onDone` two edges: the guard " +
|
|
178
|
+
`\`${READY_GUARD}\` on the one that goes on, and one with no guard for a refused cut. Then write that same literal path as \`cwd\` in an ` +
|
|
179
|
+
"Agent's `input` and as `worktree` in an Effect's `input`: each Turn and each Effect there must say where it works. " +
|
|
180
|
+
"The slot's folder `/work/<slot>` is not a Worktree."
|
|
181
|
+
: "";
|
|
167
182
|
const agents = mints
|
|
168
183
|
? `A state runs an Agent with \`invoke: { src, input: { prompt } }\`: \`src\` is a lent Agent's name${lent}. ` +
|
|
169
184
|
"This Chart mints no Agent of its own: there is no `agents`."
|
|
@@ -171,7 +186,7 @@ export function chartBriefing(narrow?: Narrow): string {
|
|
|
171
186
|
"a key of `agents`; a lent Agent needs no entry in `agents`, which mints new ones.";
|
|
172
187
|
return (
|
|
173
188
|
`A Chart is a workflow written as data: states, the events its Agents pick${mints ? "" : ", and the Agents it mints"}. ` +
|
|
174
|
-
`${agents}${gate}${fail} An Agent's Turn ends only when it ` +
|
|
189
|
+
`${agents}${gate}${fail}${cut} An Agent's Turn ends only when it ` +
|
|
175
190
|
"picks one of the Chart's `events`: declare each pick in `events` with an `input` schema " +
|
|
176
191
|
'(`{ "type": "object", "properties": { "<field>": { "type": "string" } }, "required": ["<field>"] }`; a field not in ' +
|
|
177
192
|
"`required` may be absent), and " +
|
|
@@ -238,6 +253,7 @@ export function pathSegments(path: string): Array<string | number> {
|
|
|
238
253
|
type Narrow = {
|
|
239
254
|
srcs: string[];
|
|
240
255
|
lentAgents: string[];
|
|
256
|
+
lendsEffects: boolean;
|
|
241
257
|
gate: boolean;
|
|
242
258
|
fail: boolean;
|
|
243
259
|
guards: string[];
|
|
@@ -363,16 +379,16 @@ function grammar(narrow?: Narrow): z.ZodType<ChartText> {
|
|
|
363
379
|
// `jr2.sentBy` is the kit's routing guard (ADR-0015), and `jr2.failure` its test for a Failure on
|
|
364
380
|
// an `onError` (ADR-0073): every Chart has both, and neither is lent.
|
|
365
381
|
const sentByArm = z.strictObject({ type: z.literal(SENT_BY), params: z.strictObject({ id: z.string() }) });
|
|
366
|
-
const failureArm = z.strictObject({ type: z.literal(FAILURE_GUARD) });
|
|
382
|
+
const failureArm = z.strictObject({ type: z.union([z.literal(FAILURE_GUARD), z.literal(READY_GUARD)]) });
|
|
367
383
|
const lentGuardName = narrow
|
|
368
|
-
? enumOf([...narrow.guards, FAILURE_GUARD])
|
|
384
|
+
? enumOf([...narrow.guards, FAILURE_GUARD, READY_GUARD])
|
|
369
385
|
: z.string().refine((t) => t !== SENT_BY, { message: `"${SENT_BY}" takes params { id }` });
|
|
370
386
|
const paramGuardName = narrow
|
|
371
387
|
? enumOf(narrow.guards)
|
|
372
388
|
: z
|
|
373
389
|
.string()
|
|
374
390
|
.refine((t) => t !== SENT_BY, { message: `"${SENT_BY}" takes params { id }` })
|
|
375
|
-
.refine((t) => t !== FAILURE_GUARD, { message:
|
|
391
|
+
.refine((t) => t !== FAILURE_GUARD && t !== READY_GUARD, { message: "that guard takes no params" });
|
|
376
392
|
const guard: z.ZodType<ChartGuard> = z
|
|
377
393
|
.union([
|
|
378
394
|
lentGuardName,
|
|
@@ -397,7 +413,7 @@ function grammar(narrow?: Narrow): z.ZodType<ChartText> {
|
|
|
397
413
|
])
|
|
398
414
|
.meta({
|
|
399
415
|
id: `${ID_PREFIX}ChartGuard`,
|
|
400
|
-
description: `A lent guard by name, \`jr2.sentBy\`, \`${FAILURE_GUARD}\`, or not/and/or of guards.`,
|
|
416
|
+
description: `A lent guard by name, \`jr2.sentBy\`, \`${FAILURE_GUARD}\`, \`${READY_GUARD}\`, or not/and/or of guards.`,
|
|
401
417
|
}) as never;
|
|
402
418
|
|
|
403
419
|
const lentActionName = narrow
|
|
@@ -681,6 +697,7 @@ function narrowOf(decl: OpenBodyDeclaration): Narrow {
|
|
|
681
697
|
return {
|
|
682
698
|
srcs: [...l.agents, ...l.effects, ...l.machines, ...(l.gate ? ["gate"] : []), ...(l.fail ? ["fail"] : [])],
|
|
683
699
|
lentAgents: l.agents,
|
|
700
|
+
lendsEffects: l.effects.length > 0,
|
|
684
701
|
gate: l.gate,
|
|
685
702
|
fail: l.fail,
|
|
686
703
|
guards: l.guards,
|
|
@@ -838,6 +855,17 @@ export type AdmittedChart = {
|
|
|
838
855
|
size: number;
|
|
839
856
|
};
|
|
840
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Why a Chart's `cwd` is not a place to work, or undefined when it can be: a Repo Slot's folder is no
|
|
860
|
+
* Worktree. There, an Agent's search hits `default/`, and a relative write lands outside each
|
|
861
|
+
* Worktree (ADR-0057, ADR-0075).
|
|
862
|
+
*/
|
|
863
|
+
export function slotFolderFault(cwd: string, slots: readonly string[]): string | undefined {
|
|
864
|
+
const slot = posix.normalize(cwd).match(/^\/work\/([^/]+)\/?$/)?.[1];
|
|
865
|
+
if (slot === undefined || !slots.includes(slot)) return undefined;
|
|
866
|
+
return `"${cwd}" is a Repo Slot's folder, not a Worktree; \`cwd\` is a Worktree that a cut made, "/work/${slot}/<name>", or "/work/${slot}/default" to read, or "/work"`;
|
|
867
|
+
}
|
|
868
|
+
|
|
841
869
|
/** What the door knows beyond the declaration and the lent set. */
|
|
842
870
|
export type AdmitOptions = {
|
|
843
871
|
/**
|
|
@@ -846,6 +874,11 @@ export type AdmitOptions = {
|
|
|
846
874
|
* `input.<key>` reference is checked against it; absent, `input` references are not checked.
|
|
847
875
|
*/
|
|
848
876
|
inputKeys?: readonly string[];
|
|
877
|
+
/** The Repo Slots of the Workspace around the Open body, where the door can read them. Given and
|
|
878
|
+
* not empty, a Turn's `cwd` and an Effect's `worktree` are required, a literal path's slot is
|
|
879
|
+
* checked, and a literal `cwd` cannot be a slot's folder. Absent: the start of a Turn or an Effect
|
|
880
|
+
* says the same of a missing `cwd`, of a Worktree path, and of a `cwd` that is a slot's folder. */
|
|
881
|
+
repoSlots?: readonly string[];
|
|
849
882
|
};
|
|
850
883
|
|
|
851
884
|
const MAX_STATES = 200;
|
|
@@ -991,6 +1024,13 @@ export function admitChart(
|
|
|
991
1024
|
return new Door(chart, decl, lent, options).admit(size);
|
|
992
1025
|
}
|
|
993
1026
|
|
|
1027
|
+
/** What an Effect's input takes, for a line of the door. */
|
|
1028
|
+
function shapeText(shape: InputShape): string {
|
|
1029
|
+
const need = [...shape.required, ...(shape.oneOf ? [`exactly one of ${shape.oneOf.join(", ")}`] : [])].join(", ");
|
|
1030
|
+
const may = shape.optional.length ? `${need ? ", and " : ""}optionally ${shape.optional.join(", ")}` : "";
|
|
1031
|
+
return need || may ? `${need}${may}` : "nothing";
|
|
1032
|
+
}
|
|
1033
|
+
|
|
994
1034
|
/** The checks from rule 3 on, over a Chart the grammar admitted. */
|
|
995
1035
|
class Door {
|
|
996
1036
|
private readonly lines: Line[] = [];
|
|
@@ -1171,8 +1211,8 @@ class Door {
|
|
|
1171
1211
|
this.sentBys.push({ id: (guard.params as { id: string }).id, path: `${path}.params.id` });
|
|
1172
1212
|
return;
|
|
1173
1213
|
}
|
|
1174
|
-
if (guard.type === FAILURE_GUARD) {
|
|
1175
|
-
if (guard.params !== undefined) this.fail(3, `${path}.params: "${
|
|
1214
|
+
if (guard.type === FAILURE_GUARD || guard.type === READY_GUARD) {
|
|
1215
|
+
if (guard.params !== undefined) this.fail(3, `${path}.params: "${guard.type}" takes no params`);
|
|
1176
1216
|
return;
|
|
1177
1217
|
}
|
|
1178
1218
|
this.checkGuardName(guard.type, `${path}.type`);
|
|
@@ -1180,7 +1220,7 @@ class Door {
|
|
|
1180
1220
|
}
|
|
1181
1221
|
|
|
1182
1222
|
private checkGuardName(name: string, path: string): void {
|
|
1183
|
-
if (name === FAILURE_GUARD) return;
|
|
1223
|
+
if (name === FAILURE_GUARD || name === READY_GUARD) return;
|
|
1184
1224
|
const lent = this.lends.guards.filter((g) => g in this.lent.guards);
|
|
1185
1225
|
if (this.lends.guards.includes(name) && !(name in this.lent.guards)) {
|
|
1186
1226
|
this.fail(
|
|
@@ -1190,7 +1230,7 @@ class Door {
|
|
|
1190
1230
|
} else if (!this.lends.guards.includes(name)) {
|
|
1191
1231
|
this.fail(
|
|
1192
1232
|
3,
|
|
1193
|
-
`${path}: "${name}" is not a lent guard; lent guards: ${names(lent)} (and ${SENT_BY} with params { id }, and ${
|
|
1233
|
+
`${path}: "${name}" is not a lent guard; lent guards: ${names(lent)} (and ${SENT_BY} with params { id }, ${FAILURE_GUARD} and ${READY_GUARD})`,
|
|
1194
1234
|
);
|
|
1195
1235
|
}
|
|
1196
1236
|
}
|
|
@@ -1680,6 +1720,7 @@ class Door {
|
|
|
1680
1720
|
private checkParties(): void {
|
|
1681
1721
|
const agents = new Set<string>();
|
|
1682
1722
|
const gates = new Set<string>();
|
|
1723
|
+
const slotStates = new Map<string, Node[]>();
|
|
1683
1724
|
for (const node of this.nodes) {
|
|
1684
1725
|
const invokes = listAt(node.state.invoke, `${node.doc}.invoke`);
|
|
1685
1726
|
// A Gate's invoke id, as jr2Setup names an unnamed one: the state's key path.
|
|
@@ -1694,6 +1735,7 @@ class Door {
|
|
|
1694
1735
|
if (kind === "gate") gates.add(inv.id ?? (unnamed > 1 ? `${key}.${ordinal++}` : key));
|
|
1695
1736
|
if (kind !== "agent" && kind !== "minted") continue;
|
|
1696
1737
|
agents.add(inv.src);
|
|
1738
|
+
slotStates.set(inv.src, [...(slotStates.get(inv.src) ?? []), node]);
|
|
1697
1739
|
if (inv.onDone !== undefined) {
|
|
1698
1740
|
this.fail(
|
|
1699
1741
|
6,
|
|
@@ -1715,6 +1757,25 @@ class Door {
|
|
|
1715
1757
|
}
|
|
1716
1758
|
}
|
|
1717
1759
|
}
|
|
1760
|
+
// One Agent, two Turns at once: one sender and one conversation cannot hold both.
|
|
1761
|
+
// One line for each Agent: every state of it that can be active with another of them.
|
|
1762
|
+
for (const [slot, states] of slotStates) {
|
|
1763
|
+
const clash = new Set<Node>();
|
|
1764
|
+
for (let i = 0; i < states.length; i++) {
|
|
1765
|
+
for (let j = i + 1; j < states.length; j++) {
|
|
1766
|
+
const [a, b] = [states[i]!, states[j]!];
|
|
1767
|
+
const together =
|
|
1768
|
+
a === b || isAncestorOrSelf(a, b) || isAncestorOrSelf(b, a) || nearestCommon(a, b)?.kind === "parallel";
|
|
1769
|
+
if (together) clash.add(a).add(b);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
if (clash.size === 0) continue;
|
|
1773
|
+
const docs = [...clash].map((n) => n.doc).join(", ");
|
|
1774
|
+
this.fail(
|
|
1775
|
+
6,
|
|
1776
|
+
`${docs}: Agent "${slot}" would run two Turns at once: these states invoke it and can be active together (in parallel regions, one inside the other, or one state twice); an Agent runs one Turn at a time: invoke another Agent in one of them`,
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1718
1779
|
for (const { id, path } of this.sentBys) {
|
|
1719
1780
|
if (id.includes("{{")) {
|
|
1720
1781
|
this.fail(3, `${path}: a sentBy id is a literal party name, never a reference`);
|
|
@@ -1933,8 +1994,9 @@ class Door {
|
|
|
1933
1994
|
}
|
|
1934
1995
|
if (kind === "effect") {
|
|
1935
1996
|
const logic = this.lent.actors[inv.src];
|
|
1936
|
-
const
|
|
1937
|
-
|
|
1997
|
+
const parts = isEffect(logic) ? logic.effect : undefined;
|
|
1998
|
+
const keys = parts ? inputKeysOf(parts) : undefined;
|
|
1999
|
+
if (keys === undefined && parts?.kind === "dispatch") {
|
|
1938
2000
|
// A dispatch with no input schema has no keys to hold the Chart to: its input is the run's
|
|
1939
2001
|
// body, and the callee's door is the check (ADR-0071). Its values are what any Effect's
|
|
1940
2002
|
// input slot takes.
|
|
@@ -1957,31 +2019,114 @@ class Door {
|
|
|
1957
2019
|
`${ipath}: the lent Effect "${inv.src}" has no input keys the door can read (an Open argv nobody bound)`,
|
|
1958
2020
|
);
|
|
1959
2021
|
}
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2022
|
+
const shape = this.shapeHere(parts!);
|
|
2023
|
+
// A cut's `expect` is static, and a Chart cannot state it: an unstated one would let a planner's
|
|
2024
|
+
// name land on someone else's branch.
|
|
2025
|
+
if (parts?.kind === "command" && parts.cut && parts.cut.expect === undefined) {
|
|
2026
|
+
const fresh = this.lentNewCuts();
|
|
2027
|
+
this.fail(
|
|
2028
|
+
8,
|
|
2029
|
+
`${ipath}: the lent cut "${inv.src}" does not state \`expect\`, so a Chart cannot use it; ` +
|
|
2030
|
+
(fresh.length
|
|
2031
|
+
? `use a lent cut with \`expect: "new"\`: ${names(fresh)}`
|
|
2032
|
+
: `the Open body must state it (\`worktree({ expect: "new" })\` would); ${this.lentText()}`),
|
|
2033
|
+
);
|
|
1963
2034
|
}
|
|
2035
|
+
// A push publishes the branch of a cut's row, which stays in the run's ledger, so a Chart that
|
|
2036
|
+
// cuts and a later Chart that pushes are the same pair: the test is what is lent, not what this
|
|
2037
|
+
// Chart invokes.
|
|
2038
|
+
const force = this.lentForcePush();
|
|
2039
|
+
if (parts?.kind === "command" && parts.cut?.expect === "existing" && force !== undefined) {
|
|
2040
|
+
const fresh = this.lentNewCuts();
|
|
2041
|
+
this.fail(
|
|
2042
|
+
8,
|
|
2043
|
+
`${ipath}: the lent cut "${inv.src}" states \`expect: "existing"\` beside the lent force push "${force}", and together they can force-push any existing branch; ` +
|
|
2044
|
+
(fresh.length
|
|
2045
|
+
? `use a lent cut with \`expect: "new"\`: ${names(fresh)}`
|
|
2046
|
+
: `this Open body lends no cut with \`expect: "new"\`, so a Chart cannot cut here (\`worktree({ expect: "new" })\` would); ${this.lentText()}`),
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
if (input === undefined && shape.required.length === 0 && !shape.oneOf) return;
|
|
2050
|
+
const takes = shapeText(shape);
|
|
2051
|
+
if (!isObject) return this.fail(8, `${ipath}: the lent Effect "${inv.src}" takes an object: ${takes}`);
|
|
1964
2052
|
const given = Object.keys(input);
|
|
1965
|
-
const missing =
|
|
2053
|
+
const missing = shape.required.filter((k) => !given.includes(k));
|
|
1966
2054
|
const extra = given.filter((k) => !keys.includes(k));
|
|
1967
|
-
|
|
2055
|
+
const ofOne = shape.oneOf?.filter((k) => given.includes(k)).length ?? 1;
|
|
2056
|
+
if (missing.length || extra.length || ofOne !== 1) {
|
|
1968
2057
|
this.fail(
|
|
1969
2058
|
8,
|
|
1970
|
-
`${ipath}: the lent Effect "${inv.src}" takes
|
|
2059
|
+
`${ipath}: the lent Effect "${inv.src}" takes ${takes}` +
|
|
1971
2060
|
(missing.length ? `; missing: ${missing.join(", ")}` : "") +
|
|
1972
|
-
(extra.length ? `; not its input: ${extra.join(", ")}` : "")
|
|
2061
|
+
(extra.length ? `; not its input: ${extra.join(", ")}` : "") +
|
|
2062
|
+
(ofOne !== 1 ? `; give one of ${shape.oneOf!.join(", ")}` : ""),
|
|
1973
2063
|
);
|
|
1974
2064
|
}
|
|
2065
|
+
if (parts?.kind === "command" && parts.cut && given.includes("detach") && given.includes("from")) {
|
|
2066
|
+
this.fail(8, `${ipath}: a detached cut takes no \`from\``);
|
|
2067
|
+
}
|
|
1975
2068
|
for (const k of given.filter((g) => keys.includes(g))) {
|
|
1976
2069
|
const v = (input as Record<string, unknown>)[k];
|
|
1977
2070
|
if (typeof v !== "string" && !isRef(v))
|
|
1978
2071
|
this.fail(8, `${ipath}.${k}: an Effect's input slot takes a string or a reference`);
|
|
2072
|
+
else if ((k === "worktree" || k === "path") && typeof v === "string") this.checkLiteralPath(v, `${ipath}.${k}`);
|
|
1979
2073
|
}
|
|
1980
2074
|
return;
|
|
1981
2075
|
}
|
|
1982
2076
|
this.checkAgentInput(inv.src, kind, input, isObject, ipath);
|
|
1983
2077
|
}
|
|
1984
2078
|
|
|
2079
|
+
/** An Effect's shape here: where the door knows the Workspace has Repo Slots, `worktree` is
|
|
2080
|
+
* required on what can name one. Elsewhere the start says so (`drive`). */
|
|
2081
|
+
private shapeHere(parts: Parameters<typeof inputShapeOf>[0]): InputShape {
|
|
2082
|
+
const shape = inputShapeOf(parts)!;
|
|
2083
|
+
if (!this.options.repoSlots?.length || !shape.optional.includes("worktree")) return shape;
|
|
2084
|
+
return {
|
|
2085
|
+
...shape,
|
|
2086
|
+
required: [...shape.required, "worktree"],
|
|
2087
|
+
optional: shape.optional.filter((k) => k !== "worktree"),
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
/** The lent Effects, each with its parts. */
|
|
2092
|
+
private lentEffects(): Array<{ name: string; parts: Parameters<typeof inputShapeOf>[0] }> {
|
|
2093
|
+
return this.lends.effects.flatMap((name) => {
|
|
2094
|
+
const logic = this.lent.actors[name];
|
|
2095
|
+
return isEffect(logic) ? [{ name, parts: logic.effect }] : [];
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
/** The name of a lent push that forces, if one is lent. */
|
|
2100
|
+
private lentForcePush(): string | undefined {
|
|
2101
|
+
return this.lentEffects().find(({ parts }) => parts.kind === "push" && parts.force)?.name;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
/** The names of the lent cuts that make only a new branch. */
|
|
2105
|
+
private lentNewCuts(): string[] {
|
|
2106
|
+
return this.lentEffects()
|
|
2107
|
+
.filter(({ parts }) => parts.kind === "command" && parts.cut?.expect === "new")
|
|
2108
|
+
.map(({ name }) => name);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
/** A literal Worktree path: a wrong slot costs no pod. A path with a reference waits for its start. */
|
|
2112
|
+
private checkLiteralPath(value: string, path: string): void {
|
|
2113
|
+
if (referencesOf(value).length) return;
|
|
2114
|
+
const match = value.match(/^\/work\/([^/]+)\/([^/.-][^/]*)$/);
|
|
2115
|
+
const slots = this.options.repoSlots;
|
|
2116
|
+
if (match === null) this.fail(8, `${path}: "${value}" is not a Worktree path: /work/<slot>/<name>`);
|
|
2117
|
+
else if (slots !== undefined && !slots.includes(match[1]!)) {
|
|
2118
|
+
this.fail(8, `${path}: "${value}" names no Repo Slot of this Workspace; its Repo Slots: ${names([...slots])}`);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
/** A literal `cwd`, see {@link slotFolderFault}. A `cwd` with a reference waits for its value: the
|
|
2123
|
+
* Open body asks the same question when the Turn starts. */
|
|
2124
|
+
private checkCwd(value: string, path: string): void {
|
|
2125
|
+
if (referencesOf(value).length) return;
|
|
2126
|
+
const fault = slotFolderFault(value, this.options.repoSlots ?? []);
|
|
2127
|
+
if (fault !== undefined) this.fail(8, `${path}: ${fault}`);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
1985
2130
|
/** The input of `fail` (ADR-0073): `{ reason, detail? }`, `reason` a text for a person (a string
|
|
1986
2131
|
* or a reference), `detail` any value. */
|
|
1987
2132
|
private checkFailInput(input: unknown, isObject: boolean, ipath: string): void {
|
|
@@ -2000,6 +2145,15 @@ class Door {
|
|
|
2000
2145
|
}
|
|
2001
2146
|
}
|
|
2002
2147
|
|
|
2148
|
+
/** Does this Agent read `/work`? A `workspace: "none"` one frames nothing. */
|
|
2149
|
+
private worksInWorkspace(src: string, kind: "agent" | "minted"): boolean {
|
|
2150
|
+
if (kind === "agent") {
|
|
2151
|
+
const logic = this.lent.actors[src] as { definition?: { workspace?: unknown } } | undefined;
|
|
2152
|
+
return logic?.definition?.workspace !== "none";
|
|
2153
|
+
}
|
|
2154
|
+
return (this.chart.agents?.[src] as { workspace?: unknown } | undefined)?.workspace !== "none";
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2003
2157
|
/**
|
|
2004
2158
|
* An Agent invoke's input is a Turn's Frame and Dials (ADR-0057): `prompt` (a string), and
|
|
2005
2159
|
* optionally `cwd`, `continue: true`, a `model` the Open body allows, and a `thinkingLevel`.
|
|
@@ -2033,6 +2187,13 @@ class Door {
|
|
|
2033
2187
|
else if (typeof obj.prompt !== "string")
|
|
2034
2188
|
this.fail(8, `${ipath}.prompt: a prompt is a string (placeholders inside it are fine)`);
|
|
2035
2189
|
if (obj.cwd !== undefined && typeof obj.cwd !== "string") this.fail(8, `${ipath}.cwd: a cwd is a string`);
|
|
2190
|
+
if (obj.cwd === undefined && this.options.repoSlots?.length && this.worksInWorkspace(src, kind)) {
|
|
2191
|
+
this.fail(
|
|
2192
|
+
8,
|
|
2193
|
+
`${ipath}: ${who} works under a Workspace with Repo Slots, and a Turn there says where: \`cwd\` is a Worktree, or "/work"`,
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
if (typeof obj.cwd === "string" && this.worksInWorkspace(src, kind)) this.checkCwd(obj.cwd, `${ipath}.cwd`);
|
|
2036
2197
|
if (obj.continue !== undefined && obj.continue !== true)
|
|
2037
2198
|
this.fail(8, `${ipath}.continue: \`continue\` is true or absent`);
|
|
2038
2199
|
if (obj.model !== undefined) {
|
|
@@ -2164,6 +2325,12 @@ function wholeRefOf(v: unknown): string | undefined {
|
|
|
2164
2325
|
return isRef(v) ? (v as { $ref: string }).$ref : undefined;
|
|
2165
2326
|
}
|
|
2166
2327
|
|
|
2328
|
+
/** The nearest state at or above both `a` and `b`. */
|
|
2329
|
+
function nearestCommon(a: Node, b: Node): Node | undefined {
|
|
2330
|
+
for (let n: Node | undefined = a; n; n = n.parent) if (isAncestorOrSelf(n, b)) return n;
|
|
2331
|
+
return undefined;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2167
2334
|
/** Is `a` the state `b` or a state above it? */
|
|
2168
2335
|
function isAncestorOrSelf(a: Node, b: Node): boolean {
|
|
2169
2336
|
for (let n: Node | undefined = b; n; n = n.parent) if (n === a) return true;
|
package/src/charts.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
isOpenBody,
|
|
26
26
|
lentIn,
|
|
27
27
|
openBodyDeclarationOf,
|
|
28
|
+
repoSlotsAround,
|
|
28
29
|
type LentParts,
|
|
29
30
|
type OpenBodyDeclaration,
|
|
30
31
|
type OpenBodyLogic,
|
|
@@ -305,6 +306,8 @@ export type ChartWrapper = {
|
|
|
305
306
|
chartFields: string[];
|
|
306
307
|
/** The door's other fields: the Chart's `input` keys the door can see (ADR-0070 rule 7). */
|
|
307
308
|
inputKeys: string[];
|
|
309
|
+
/** The Repo Slots of the `workspace()` around the wrapper, where the door can read them. */
|
|
310
|
+
repoSlots?: readonly string[];
|
|
308
311
|
};
|
|
309
312
|
|
|
310
313
|
/**
|
|
@@ -333,7 +336,8 @@ export function chartWrapperOf(machine: AnyStateMachine): ChartWrapper | undefin
|
|
|
333
336
|
const chartFields = Object.keys(shape).filter((k) => isChartSchema(shape[k]));
|
|
334
337
|
// The door's other fields, less the body's placement: a Chart reads no url (open-body.ts).
|
|
335
338
|
const inputKeys = Object.keys(chartInputOf(shape)).filter((k) => !chartFields.includes(k));
|
|
336
|
-
|
|
339
|
+
const repoSlots = repoSlotsAround(machine);
|
|
340
|
+
return { body: bodies[0]!.slot, bodies, chartFields, inputKeys, ...(repoSlots ? { repoSlots } : {}) };
|
|
337
341
|
}
|
|
338
342
|
|
|
339
343
|
/**
|
|
@@ -347,7 +351,10 @@ export function admitUnder(wrapper: ChartWrapper, text: unknown): AdmittedChart
|
|
|
347
351
|
const report: string[] = [];
|
|
348
352
|
for (const body of wrapper.bodies) {
|
|
349
353
|
try {
|
|
350
|
-
return admitBodyChart(text, body.declaration, body.lent, {
|
|
354
|
+
return admitBodyChart(text, body.declaration, body.lent, {
|
|
355
|
+
inputKeys: wrapper.inputKeys,
|
|
356
|
+
...(wrapper.repoSlots ? { repoSlots: wrapper.repoSlots } : {}),
|
|
357
|
+
});
|
|
351
358
|
} catch (err) {
|
|
352
359
|
if (!(err instanceof ChartRefusal)) throw err;
|
|
353
360
|
const prefix = wrapper.bodies.length > 1 ? `body "${body.slot}": ` : "";
|
package/src/config.ts
CHANGED
|
@@ -257,7 +257,7 @@ export type HarnessProvider = {
|
|
|
257
257
|
// Harness sends the wire library's placeholder, which vLLM/Ollama ignore).
|
|
258
258
|
/** The API key, when the endpoint needs one. Read it from `process.env`. It is a Held secret,
|
|
259
259
|
* `JR2_PROVIDER_API_KEY`: `jr2 up` gives the value to the Custodian only, bound to the host of
|
|
260
|
-
* `baseUrl`, and the Harness
|
|
260
|
+
* `baseUrl`, and the Harness process holds only its secret Stand-in. Thus `baseUrl` must be
|
|
261
261
|
* `https://` when you set it:
|
|
262
262
|
* `harness.provider.apiKey is set, and harness.provider.baseUrl is not https://`. Default: none,
|
|
263
263
|
* for an endpoint that needs no key. */
|
|
@@ -302,8 +302,9 @@ export type CatalogReach = {
|
|
|
302
302
|
*
|
|
303
303
|
* - It is a Held secret in each Harness pod that names it. A pod names it when its `workspace()`
|
|
304
304
|
* lists it under `secrets`, or when it is the model key of a provider that an Agent there uses. The Harness
|
|
305
|
-
*
|
|
306
|
-
* puts the value into requests toward `hosts` only.
|
|
305
|
+
* process then holds only its Stand-in, a secret value that starts `jr2-held-<name>.`, and the
|
|
306
|
+
* pod's Custodian puts the value into requests toward `hosts` only. The Agent's programs get the
|
|
307
|
+
* Stand-in, in their env under this name, only when the `workspace()` lists it under `secrets`.
|
|
307
308
|
* - It is an Effect secret where an Effect names it with `secret("name")`: a file in that
|
|
308
309
|
* Effect's Effector.
|
|
309
310
|
*
|
|
@@ -312,8 +313,9 @@ export type CatalogReach = {
|
|
|
312
313
|
* `secrets[<i>] (<NAME>) is named by nothing; goes nowhere`.
|
|
313
314
|
*/
|
|
314
315
|
export type SecretEntry = {
|
|
315
|
-
/** As a Held secret, the name of
|
|
316
|
-
*
|
|
316
|
+
/** As a Held secret, the name of its Stand-in: the file the Harness reads it from, the env var
|
|
317
|
+
* that the engine reads a model key from, and the env var of the Agent's programs when the
|
|
318
|
+
* `workspace()` lists it under `secrets`. As an Effect secret, the name that `secret("<name>")` names, and the name of the
|
|
317
319
|
* Effector's file. For a Held secret, it must match `[A-Z_][A-Z0-9_]*`:
|
|
318
320
|
* `secrets[<i>].name "<name>" is not an env var name ([A-Z_][A-Z0-9_]*)`. It must not start with
|
|
319
321
|
* `JR2_` and must not be a proxy or trust variable of the kit:
|
|
@@ -386,12 +388,20 @@ export type HarnessConfig = {
|
|
|
386
388
|
// `jr2 up`. A known model key here is refused (ADR-0059).
|
|
387
389
|
/** Env vars for the Harness container. The Agent can read every one of them, so do not put a
|
|
388
390
|
* credential here. A known model key is refused here. Put it in the top-level `secrets` list.
|
|
391
|
+
* The Harness process gets each one, except a name that loads code from a path, such as
|
|
392
|
+
* `NODE_OPTIONS` or `LD_PRELOAD`, and `HOME`, which is its own; the Agent's programs get each
|
|
393
|
+
* one. A name that starts with `JR2_` is jr2's own, and is refused. A credential file that the
|
|
394
|
+
* Harness process reads, such as `AWS_CONFIG_FILE`, is refused in a folder that the Agent's code
|
|
395
|
+
* writes (`/work`, `/tmp`, `/home/jr2`), and gives a warning anywhere else.
|
|
389
396
|
* See {@link HarnessEnvVar}. */
|
|
390
397
|
env?: readonly HarnessEnvVar[];
|
|
391
398
|
// `jr2 up` preflights that each referenced Secret exists (ADR-0019), and refuses one that
|
|
392
399
|
// carries a known model key (ADR-0059).
|
|
393
400
|
/** Secrets and ConfigMaps that you manage, whose keys all become env vars of the Harness
|
|
394
|
-
*
|
|
401
|
+
* process and of the Agent's programs, as `env` does. In the container's spec each key has the
|
|
402
|
+
* prefix `JR2_ENVFROM_`, so that the Harness can tell jr2's names from the image's; the Harness
|
|
403
|
+
* removes it. A key that starts with `JR2_` is refused: `jr2 up` refuses it, and the Harness does
|
|
404
|
+
* not start with one. See {@link HarnessEnvFromSource}. */
|
|
395
405
|
envFrom?: readonly HarnessEnvFromSource[];
|
|
396
406
|
// Only `jr2 up` reads it (host-side): it materializes the `jr2-ca` ConfigMap and runs the
|
|
397
407
|
// provider preflight with the same trust (ADR-0020, ADR-0059).
|
package/src/custodian.lua
CHANGED
|
@@ -10,15 +10,18 @@
|
|
|
10
10
|
--
|
|
11
11
|
-- Four roles, one per route family, read from the route's metadata:
|
|
12
12
|
--
|
|
13
|
-
-- control
|
|
13
|
+
-- control toward the Orchestrator, plain HTTP: the agent routes on the control socket, and
|
|
14
|
+
-- the ask on the loopback fetch listener (ADR-0076). Each listener has only its own
|
|
15
|
+
-- routes, so a route of the other one gets the deny route's 404
|
|
14
16
|
-- egress the CONNECT listener: dial guard, then tunnel or hand to an intercept listener
|
|
15
17
|
-- intercept one bound host, TLS already ended: swap the Stand-in, strip the rest
|
|
16
18
|
-- health the kubelet's probe
|
|
17
19
|
|
|
18
20
|
local VALUES = "/etc/jr2/custodian/values/"
|
|
19
21
|
|
|
20
|
-
-- The value of one held secret, read once. One trailing line end is taken off,
|
|
21
|
-
-- `kubectl create secret --from-file` writes one; anything else a header cannot carry stops
|
|
22
|
+
-- The value of one held secret, or its Stand-in, read once. One trailing line end is taken off,
|
|
23
|
+
-- because `kubectl create secret --from-file` writes one; anything else a header cannot carry stops
|
|
24
|
+
-- us. The line names the file, never what it holds.
|
|
22
25
|
local function load(name)
|
|
23
26
|
local f = io.open(VALUES .. name, "rb")
|
|
24
27
|
if f == nil then
|
|
@@ -36,13 +39,20 @@ local function load(name)
|
|
|
36
39
|
return v
|
|
37
40
|
end
|
|
38
41
|
|
|
42
|
+
-- Every Stand-in is secret but the fetch Stand-in (ADR-0059, ADR-0076): the pod's maker minted it,
|
|
43
|
+
-- and it is a value too, read from `<NAME>.standin` beside the secret's own. The one public Stand-in
|
|
44
|
+
-- rides the table (`standIn`), because `jr2-upload-pack` holds it as a constant.
|
|
39
45
|
local STAND_IN = {}
|
|
40
46
|
local VALUE = {}
|
|
41
47
|
for _, s in ipairs(HELD.secrets) do
|
|
42
|
-
STAND_IN[s.name] =
|
|
48
|
+
STAND_IN[s.name] = s.standIn or load(s.name .. ".standin")
|
|
43
49
|
VALUE[s.name] = load(s.name)
|
|
44
50
|
end
|
|
45
51
|
|
|
52
|
+
-- The slot the Harness states on a control request (ADR-0076). It goes to the Orchestrator on a
|
|
53
|
+
-- control route, unchanged, and to no other host.
|
|
54
|
+
local SLOT_HEADER = "x-jr2-slot"
|
|
55
|
+
|
|
46
56
|
-- The headers every bound target treats as a credential (`CREDENTIAL_HEADERS`, held-secrets.ts).
|
|
47
57
|
local CREDENTIAL = {}
|
|
48
58
|
for _, h in ipairs(HELD.credential) do CREDENTIAL[h] = true end
|
|
@@ -258,7 +268,7 @@ local function intercept(handle, headers, target)
|
|
|
258
268
|
if path:lower():find("%%2[ef]") then
|
|
259
269
|
return refuse(handle, 400, "path", "an encoded / or . in the path")
|
|
260
270
|
end
|
|
261
|
-
for _, h in ipairs({ "te", "trailer", "keep-alive", "proxy-connection" }) do headers:remove(h) end
|
|
271
|
+
for _, h in ipairs({ "te", "trailer", "keep-alive", "proxy-connection", SLOT_HEADER }) do headers:remove(h) end
|
|
262
272
|
return hold(handle, headers, HELD.targets[target], target, path)
|
|
263
273
|
end
|
|
264
274
|
|
|
@@ -273,6 +283,8 @@ local function control(handle, headers, route)
|
|
|
273
283
|
'{"error":"this pod serves no Sandbox, so it mounts no Repo to fetch (ADR-0053)"}')
|
|
274
284
|
end
|
|
275
285
|
headers:replace(":path", "/sandboxes/" .. sandbox .. "/fetch")
|
|
286
|
+
-- The fetch acts for the Sandbox, not for an Agent: no slot rides it.
|
|
287
|
+
headers:remove(SLOT_HEADER)
|
|
276
288
|
end
|
|
277
289
|
return hold(handle, headers, HELD.control, "orchestrator", headers:get(":path"))
|
|
278
290
|
end
|