@jr2/orchestrator 0.3.0 → 0.4.1
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/bin/server.ts +7 -4
- package/console/canvas.ts +27 -14
- package/console/components/app.ts +10 -1
- package/console/components/fleet.ts +39 -6
- package/console/components/machine-pane.ts +89 -2
- package/console/components/nav.ts +1 -1
- package/console/components/schema-form.ts +12 -4
- package/console/json-field.ts +23 -0
- package/console/main.ts +110 -39
- package/console/store.ts +221 -17
- package/console/style.css +90 -0
- package/package.json +4 -4
- package/src/actor.ts +502 -49
- package/src/ambient.ts +2 -0
- package/src/assembly-checks.ts +165 -0
- package/src/assembly-tar.ts +494 -0
- package/src/assembly.ts +721 -0
- package/src/chart.ts +2050 -0
- package/src/charts.ts +720 -0
- package/src/config.ts +152 -46
- package/src/content-hash.ts +105 -0
- package/src/custodian.lua +6 -5
- package/src/custodian.ts +71 -19
- package/src/customize.ts +216 -25
- package/src/effect-checks.ts +557 -0
- package/src/effect.ts +905 -0
- package/src/effector-wire.ts +525 -0
- package/src/fingerprint.ts +4 -3
- package/src/gate.ts +8 -0
- package/src/git-clone.ts +65 -0
- package/src/git-push.ts +336 -0
- package/src/harness-client.ts +245 -23
- package/src/held-secrets.ts +82 -26
- package/src/holds.ts +156 -0
- package/src/http-effect.ts +981 -0
- package/src/http.ts +496 -104
- package/src/images.ts +47 -21
- package/src/index.ts +43 -2
- package/src/instance.ts +444 -58
- package/src/kube-client.ts +333 -0
- package/src/machine-doc.ts +88 -6
- package/src/mount-digest.ts +53 -0
- package/src/names.ts +66 -8
- package/src/notices.ts +88 -0
- package/src/open-body.ts +1444 -0
- package/src/open-effect.ts +144 -0
- package/src/open.ts +4 -4
- package/src/parts.ts +530 -33
- package/src/party.ts +468 -0
- package/src/pick.ts +309 -0
- package/src/pool.ts +1 -1
- package/src/registration.ts +301 -17
- package/src/repo-fetch.ts +60 -85
- package/src/repos.ts +63 -74
- package/src/rest.ts +133 -0
- package/src/run-host.ts +1832 -98
- package/src/sandbox-effect.ts +891 -0
- package/src/sandbox-effector.ts +111 -0
- package/src/sandbox-kube.ts +1719 -0
- package/src/sandbox-watch.ts +260 -0
- package/src/secret-placement.ts +494 -0
- package/src/server.ts +281 -84
- package/src/setup.ts +589 -70
- package/src/size.ts +342 -0
- package/src/snapshot-store.ts +64 -13
- package/src/stub-harness.ts +5 -0
- package/src/sync-client.ts +54 -0
- package/src/timers.ts +178 -0
- package/src/tokens.ts +35 -4
- package/src/vocabulary.ts +66 -1
- package/src/wire.ts +156 -2
- package/src/workspace.ts +529 -194
- package/src/sandbox-kubectl.ts +0 -1108
package/src/parts.ts
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
// Instance Harness converges when any of them declares `workspace: "none"`, every `file:`
|
|
17
17
|
// image context a Machine ships is built and content-tagged, every BOUND Repo is known before
|
|
18
18
|
// a run can ask for it, an OPEN part — a Repo Slot with no url, an Agent with no model
|
|
19
|
-
// (ADR-0054) — is refused before anything is built,
|
|
20
|
-
//
|
|
19
|
+
// (ADR-0054) — is refused before anything is built, every Size a `workspace()` states is split
|
|
20
|
+
// and held to the Harness floor (ADR-0060), and whether any registered Machine composes a
|
|
21
|
+
// Sandbox at all is the data-plane switch. None of these can read
|
|
21
22
|
// an invoke's `input` (it is a function — dials are not statically recoverable), and none
|
|
22
23
|
// needs to: identity lives in the definition, the image and the slots are options, and all are
|
|
23
24
|
// values ON the Machine. A per-run slot is a function too, and the walk reports nothing for
|
|
@@ -33,6 +34,13 @@
|
|
|
33
34
|
// xstate rewrites to a generated key and keeps only on the raw config node. Nothing jr2 owns
|
|
34
35
|
// arrives this way any more; the walk keeps it because an AUTHOR may still write one.
|
|
35
36
|
//
|
|
37
|
+
// A PART written inline is refused, not walked (`refuseInlineParts`): an `agent()`, an Effect
|
|
38
|
+
// (ADR-0067) or a `workspace()` on an `invoke.src` has no author-written name. xstate's generated
|
|
39
|
+
// key (`xstate.invoke.<n>.<state id>`) moves when a state is renamed, so the ledger, the Console doc
|
|
40
|
+
// and `jr2 up`'s report would name it by a number, and `customize()` — xstate's `provide`, keyed on
|
|
41
|
+
// `actors` — could not reach it at all. A slot key is the one name every reader shares (ADR-0049),
|
|
42
|
+
// so a Part is a key of `actors`, and the refusal says how to write it.
|
|
43
|
+
//
|
|
36
44
|
// Deliberately NOT a `provide()`-aware read: the walk runs on the registered Machine, which is the
|
|
37
45
|
// object the run will start, so a `customize()` clone is walked as itself and a test seam's fake
|
|
38
46
|
// (unbranded) contributes nothing.
|
|
@@ -41,9 +49,13 @@ import { basename } from "node:path";
|
|
|
41
49
|
import { fileURLToPath } from "node:url";
|
|
42
50
|
import type { AnyActorRef, AnyStateMachine, StateNode, UnknownActorLogic } from "xstate";
|
|
43
51
|
import { isAgent, isOpenAgent, type AgentDeclaration } from "./agent.ts";
|
|
52
|
+
import { argvText, isEffect, placementOf, type EffectParts, type Placement } from "./effect.ts";
|
|
53
|
+
import { effectBindingLine, openPartsOf, type OpenEffectPart } from "./open-effect.ts";
|
|
44
54
|
import { isImageContext } from "./images.ts";
|
|
55
|
+
import { isOpenBody, openBodyDeclarationOf, type OpenBodyDeclaration } from "./open-body.ts";
|
|
45
56
|
import { open } from "./open.ts";
|
|
46
57
|
import { repoIdentity } from "./repo-identity.ts";
|
|
58
|
+
import { userSeatOf, type Size, type UserContainer } from "./size.ts";
|
|
47
59
|
|
|
48
60
|
// --- Repo Slots (ADR-0051) ---------------------------------------------------------------------
|
|
49
61
|
// A `workspace()` names each Repo it attaches under a SLOT — the Machine's own word for it, the
|
|
@@ -51,7 +63,7 @@ import { repoIdentity } from "./repo-identity.ts";
|
|
|
51
63
|
// is one of three states, and the walk tells them apart without evaluating anything. A Machine
|
|
52
64
|
// whose body names no slot — it enumerates whatever is attached — declares the whole MAP Open
|
|
53
65
|
// instead (`repos: open`): the composer names every slot, in an order the Machine may give a
|
|
54
|
-
// meaning to. Open at the map is the same word as Open at a slot, one level up (
|
|
66
|
+
// meaning to. Open at the map is the same word as Open at a slot, one level up (ADR-0051).
|
|
55
67
|
|
|
56
68
|
// The sentinel itself lives in open.ts, a module with nothing else in it: since ADR-0054 it marks
|
|
57
69
|
// an Agent's model too, and agent.ts must read it without importing this file (the walk imports
|
|
@@ -59,7 +71,7 @@ import { repoIdentity } from "./repo-identity.ts";
|
|
|
59
71
|
// meets it first, and `import { open } from "@jr2/orchestrator"` is the only spelling anyone writes.
|
|
60
72
|
export { open } from "./open.ts";
|
|
61
73
|
|
|
62
|
-
/** What a Repo Slot resolves to (
|
|
74
|
+
/** What a Repo Slot resolves to (GLOSSARY.md "Binding"): the url, and the base the branch
|
|
63
75
|
* Worktree is cut from — absent, the Repo's own default branch. */
|
|
64
76
|
export type Binding = { url: string; ref?: string };
|
|
65
77
|
|
|
@@ -146,11 +158,20 @@ export function assertRepoSlot(where: string, slot: string, value: unknown): ass
|
|
|
146
158
|
export type SandboxParts = {
|
|
147
159
|
/** The Sandbox Image. Absent → the Instance's `images/default`, then the stock Harness. */
|
|
148
160
|
image?: string;
|
|
149
|
-
/** The User Container
|
|
150
|
-
|
|
161
|
+
/** The User Container (ADR-0005): its image, or `{ image, resources }` when the Machine states
|
|
162
|
+
* its split of the Size (ADR-0060). Absent → the pod has no third container. */
|
|
163
|
+
user?: string | UserContainer;
|
|
151
164
|
/** The Repo Slots, in declaration order — which the handles keep (ADR-0051) — or
|
|
152
165
|
* {@link open} for a map the composer fills whole. */
|
|
153
166
|
repos: Record<string, RepoSlot> | typeof open;
|
|
167
|
+
/** The Size the Machine states (ADR-0060) — the `workspace()`'s, with any `customize()` layered
|
|
168
|
+
* over it. Absent → the Instance's `sandbox.resources`, then the kit default. */
|
|
169
|
+
resources?: Size;
|
|
170
|
+
/** The Held secrets this Workspace's pod gets beside its Agents' model keys (ADR-0059): names of
|
|
171
|
+
* the `secrets` list. Naming is placement — no other pod gets a name for being listed here. */
|
|
172
|
+
secrets?: string[];
|
|
173
|
+
/** The Sandbox Effector's image (ADR-0068). Absent → the Sandbox Image. */
|
|
174
|
+
effector?: { image: string };
|
|
154
175
|
};
|
|
155
176
|
|
|
156
177
|
// The stamps below are written ON `machine.config` under `Symbol.for` keys, never held in a
|
|
@@ -180,9 +201,10 @@ export function sandboxPartsOf(machine: AnyStateMachine | undefined): SandboxPar
|
|
|
180
201
|
* `sandboxPartsOf(m)` naming no image: a wrapper that names neither image still owns the two
|
|
181
202
|
* seats, which is what `customize({ image })` retunes (ADR-0049) — and it always carries at least
|
|
182
203
|
* one Repo Slot (ADR-0051). Also the data-plane switch, read off the walk: an Instance needs
|
|
183
|
-
* Sandboxes exactly when a registered Machine composes one.
|
|
204
|
+
* Sandboxes exactly when a registered Machine composes one. A logic with no `config` (an Effect's,
|
|
205
|
+
* ADR-0067) composes none. */
|
|
184
206
|
export function composesSandbox(machine: AnyStateMachine): boolean {
|
|
185
|
-
return (machine.config as Stamped)[SANDBOX_PARTS] !== undefined;
|
|
207
|
+
return (machine.config as Stamped | undefined)?.[SANDBOX_PARTS] !== undefined;
|
|
186
208
|
}
|
|
187
209
|
|
|
188
210
|
// The slot a jr2 WRAPPER is transparent to (ADR-0049): `workspace()`'s `body`, `pool()`'s
|
|
@@ -295,8 +317,43 @@ export type CarriedRepo = { url: string; ref?: string; identity: string; key: st
|
|
|
295
317
|
*/
|
|
296
318
|
export type OpenSlot = { slot: string | undefined; path: readonly string[] | undefined };
|
|
297
319
|
|
|
298
|
-
/**
|
|
299
|
-
|
|
320
|
+
/**
|
|
321
|
+
* One hop of a placement route (ADR-0059, ADR-0068): the `actors` slot key a child is invoked by,
|
|
322
|
+
* or — for a plain child Machine written INLINE on a state's invoke — xstate's generated invoke
|
|
323
|
+
* src for that hop (`xstate.invoke.<index>.<state id>`), which the static walk computes from the
|
|
324
|
+
* state node and the run reads off the live child. An inline hop is an object, so it can never
|
|
325
|
+
* equal a slot key.
|
|
326
|
+
*/
|
|
327
|
+
export type RouteHop = string | { readonly inline: string };
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Where a placement sits: the chain of hops from the registered root down to it, a wrapper's
|
|
331
|
+
* transparent body skipped as {@link OpenSlot}'s `path` skips it. Unlike `path`, a route goes on
|
|
332
|
+
* past an inline Machine, so every placement — a Workspace's pod, an Effect — has a route of its
|
|
333
|
+
* own, and the pod keys of two Workspaces never collide. Where `path` is defined the route is that
|
|
334
|
+
* same list of slot keys, so a pod key reached by slot keys alone is what it was.
|
|
335
|
+
*/
|
|
336
|
+
export type PlacementRoute = readonly RouteHop[];
|
|
337
|
+
|
|
338
|
+
/** The hop xstate gives the `index`-th invoke of state node `stateId` when its `src` is inline. */
|
|
339
|
+
export const inlineHop = (index: number, stateId: string): RouteHop => ({
|
|
340
|
+
inline: `xstate.invoke.${index}.${stateId}`,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
/** A route as a human reads it, for a label: ` → "review"`, ` → invoked inline in state "w.a"`. */
|
|
344
|
+
export function routeText(route: PlacementRoute): string {
|
|
345
|
+
return route
|
|
346
|
+
.map((hop) =>
|
|
347
|
+
typeof hop === "string"
|
|
348
|
+
? ` → "${hop}"`
|
|
349
|
+
: ` → invoked inline in state "${hop.inline.replace(/^xstate\.invoke\.\d+\./, "")}"`,
|
|
350
|
+
)
|
|
351
|
+
.join("");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Which kind of Open part a fix line binds — a Repo Slot (`repos`, ADR-0051), an Agent's model
|
|
355
|
+
* (`agents`, ADR-0054), or an Effect's url, argv or secret (`effects`, ADR-0054). */
|
|
356
|
+
export type OpenPart = "repo" | "agent" | OpenEffectPart;
|
|
300
357
|
|
|
301
358
|
/**
|
|
302
359
|
* The `customize` line that binds an Open part, given the identifier the composer holds the
|
|
@@ -305,22 +362,26 @@ export type OpenPart = "repo" | "agent";
|
|
|
305
362
|
* `customize(top, { actors: { review: { repos: { target: "<url>" } } } })`. The same nesting
|
|
306
363
|
* `customize()` accepts, so the line pastes.
|
|
307
364
|
*
|
|
308
|
-
* An Agent binds through `agents` instead, and the
|
|
365
|
+
* An Agent binds through `agents` instead, and the sample value is the model spelling ADR-0018
|
|
309
366
|
* demands — `<provider>/<model>`, not a bare model id, since the prefix is what picks the
|
|
310
|
-
* endpoint.
|
|
311
|
-
*
|
|
367
|
+
* endpoint. An Effect binds through `effects`: its url, its argv, its secret, or one object with two
|
|
368
|
+
* of them for an Effect with two Open parts (open-effect.ts). `repo` is the
|
|
369
|
+
* default because a Repo Slot was the first Open part and reads as the
|
|
370
|
+
* unmarked case. An Open MAP has no slot to print (`slot` undefined): the sample `<slot>`
|
|
312
371
|
* stands where the composer's own word goes, since choosing it is the composer's half of the line.
|
|
313
372
|
*/
|
|
314
373
|
export function customizeLine(
|
|
315
374
|
machine: string,
|
|
316
375
|
path: readonly string[],
|
|
317
376
|
slot: string | undefined,
|
|
318
|
-
part: OpenPart = "repo",
|
|
377
|
+
part: OpenPart | readonly OpenEffectPart[] = "repo",
|
|
319
378
|
): string {
|
|
320
379
|
const binds =
|
|
321
380
|
part === "agent"
|
|
322
381
|
? `{ agents: { ${slot}: { model: "<provider>/<model>" } } }`
|
|
323
|
-
:
|
|
382
|
+
: part === "repo"
|
|
383
|
+
? `{ repos: { ${slot ?? "<slot>"}: "<url>" } }`
|
|
384
|
+
: effectBindingLine(slot ?? "<slot>", part);
|
|
324
385
|
const inner = path.reduceRight((parts, key) => `{ actors: { ${key}: ${parts} } }`, binds);
|
|
325
386
|
return `customize(${machine}, ${inner})`;
|
|
326
387
|
}
|
|
@@ -329,8 +390,10 @@ export function customizeLine(
|
|
|
329
390
|
* The runtime twin of the walk's `path`: the actor-slot chain from a run's root actor down to
|
|
330
391
|
* `actor` (its own `src` included), read off the live actor tree — each `src` is the slot key it
|
|
331
392
|
* was invoked or spawned as, and a wrapper's transparent body is skipped by the same record
|
|
332
|
-
* ({@link wrapperBodyOf}) the walk skips it by. `undefined` past an inline-invoked actor,
|
|
333
|
-
* `
|
|
393
|
+
* ({@link wrapperBodyOf}) the walk skips it by. `undefined` past an inline-invoked actor, as the
|
|
394
|
+
* walk's `path` is: such an actor names no slot. xstate gives an inline `invoke.src` a generated
|
|
395
|
+
* string (`xstate.invoke.<n>.<state id>`) that is no key of its parent's `actors`, so the test is
|
|
396
|
+
* that registry, not the type of `src`; inline logic passed to `spawn` keeps the logic as `src`.
|
|
334
397
|
*/
|
|
335
398
|
export function actorSlotPath(actor: AnyActorRef | undefined): string[] | undefined {
|
|
336
399
|
const path: string[] = [];
|
|
@@ -338,14 +401,105 @@ export function actorSlotPath(actor: AnyActorRef | undefined): string[] | undefi
|
|
|
338
401
|
const src = (node as { src?: unknown }).src;
|
|
339
402
|
if (typeof src !== "string") return undefined;
|
|
340
403
|
const parent = asMachine((node._parent as { logic?: unknown }).logic);
|
|
404
|
+
if (parent && !Object.hasOwn(parent.implementations.actors, src)) return undefined;
|
|
341
405
|
if (!parent || wrapperBodyOf(parent) !== src) path.unshift(src);
|
|
342
406
|
}
|
|
343
407
|
return path;
|
|
344
408
|
}
|
|
345
409
|
|
|
410
|
+
/**
|
|
411
|
+
* The runtime twin of the walk's `route` (ADR-0059): the placement route from a run's root actor
|
|
412
|
+
* down to `actor`, read off the live actor tree. A child invoked by a slot key adds that key, as
|
|
413
|
+
* {@link actorSlotPath} does; a plain Machine invoked inline adds xstate's generated src as an
|
|
414
|
+
* inline hop, which the walk computes from the state node — so `jr2 up` and the provision derive
|
|
415
|
+
* one pod key. Never undefined: an actor the walk cannot see — a Machine object handed to
|
|
416
|
+
* `spawnChild`, whose `src` is the logic itself — is refused with the slot form.
|
|
417
|
+
*/
|
|
418
|
+
export function actorRoute(actor: AnyActorRef | undefined): RouteHop[] {
|
|
419
|
+
const route: RouteHop[] = [];
|
|
420
|
+
for (let node = actor; node?._parent; node = node._parent) {
|
|
421
|
+
const src = (node as { src?: unknown }).src;
|
|
422
|
+
const parent = asMachine((node._parent as { logic?: unknown }).logic);
|
|
423
|
+
if (typeof src === "string" && (!parent || Object.hasOwn(parent.implementations.actors, src))) {
|
|
424
|
+
if (!parent || wrapperBodyOf(parent) !== src) route.unshift(src);
|
|
425
|
+
} else if (typeof src === "string" && /^xstate\.invoke\.\d+\./.test(src)) {
|
|
426
|
+
route.unshift({ inline: src });
|
|
427
|
+
} else {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`actor "${node.id}" was spawned from a logic object, which no walk can see, so jr2 up placed nothing ` +
|
|
430
|
+
"for what it composes — a Part is reached through keys of actors, so it has a name that the ledger, " +
|
|
431
|
+
'the report and customize() share: actors: { <name>: <machine> }, then spawnChild("<name>") (ADR-0059)',
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return route;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* One `workspace()` a Machine composes, and the Size it states (ADR-0060) — what `jr2 up` resolves
|
|
440
|
+
* against the Instance default and splits, so a split that leaves the Harness below its floor is
|
|
441
|
+
* refused at the converge rather than at the first provision, and a Size no Sandbox node could hold
|
|
442
|
+
* is warned about. Located like an {@link OpenSlot}: `path` is the `customize()` route to it.
|
|
443
|
+
*/
|
|
444
|
+
export type CarriedSize = {
|
|
445
|
+
path: readonly string[] | undefined;
|
|
446
|
+
/** The placement route to it (ADR-0059). */
|
|
447
|
+
route: PlacementRoute;
|
|
448
|
+
/** The Machine's Size, as stated; absent when it states none. */
|
|
449
|
+
resources?: Size;
|
|
450
|
+
/** The User Container's split, as stated; absent when it states none. */
|
|
451
|
+
user?: Size;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/** One Effect slot a Machine carries (ADR-0067), by its slot key and the `customize()` route to its
|
|
455
|
+
* Machine, with the plain data `jr2 up` prints: the kind, where it runs, a command's argv (an argv
|
|
456
|
+
* slot reads `<name>`), its timeout, and the Repo Slot whose Worktree is its cwd, when it names one.
|
|
457
|
+
* `open` names the parts a package left Open (ADR-0054) — an http url, which then has no target
|
|
458
|
+
* yet, or a command's argv, which reads `<open>`, and a secret its auth, a header or its env names
|
|
459
|
+
* as `secret(open)`, which names no secret yet — and the walk reports them for `jr2 up` to refuse;
|
|
460
|
+
* once `customize()` binds them, the slot carries the bound Effect and `open` is absent. */
|
|
461
|
+
export type CarriedEffect = {
|
|
462
|
+
name: string;
|
|
463
|
+
path: readonly string[] | undefined;
|
|
464
|
+
/** The placement route to its Machine (ADR-0068): what its placement key reads, never `path`. */
|
|
465
|
+
route: PlacementRoute;
|
|
466
|
+
placement: Placement;
|
|
467
|
+
timeoutMs: number;
|
|
468
|
+
} &
|
|
469
|
+
/** A command; one whose env names a secret runs in the Sandbox Effector (ADR-0067). */
|
|
470
|
+
(| {
|
|
471
|
+
kind: "command";
|
|
472
|
+
argv: string[];
|
|
473
|
+
slot?: string;
|
|
474
|
+
secrets?: string[];
|
|
475
|
+
pin?: true;
|
|
476
|
+
open?: Array<"argv" | "secret">;
|
|
477
|
+
}
|
|
478
|
+
/** An http Effect (ADR-0068): its method, its fixed target, and the secrets it names — what
|
|
479
|
+
* places each secret, and what its Effector's config allows. `file`: its body is a Worktree
|
|
480
|
+
* file (ADR-0067), so it runs in its Workspace's Sandbox Effector, on Repo Slot `slot` when named.
|
|
481
|
+
* An Open url has no target, and names only the secrets its headers and auth name. */
|
|
482
|
+
| {
|
|
483
|
+
kind: "http";
|
|
484
|
+
method: string;
|
|
485
|
+
target?: { scheme: "http" | "https"; host: string; port: number };
|
|
486
|
+
secrets: string[];
|
|
487
|
+
idempotent: boolean;
|
|
488
|
+
file?: true;
|
|
489
|
+
slot?: string;
|
|
490
|
+
open?: Array<"url" | "secret">;
|
|
491
|
+
}
|
|
492
|
+
/** A push (ADR-0067, ADR-0051): the Workspace's branch to its Repo Slot's Binding. It names no
|
|
493
|
+
* secret: `jr2 up` resolves its credential through `git.credentials`. */
|
|
494
|
+
| { kind: "push"; slot?: string; sign: boolean; squash: boolean; force: boolean; pin: boolean; secrets: string[] }
|
|
495
|
+
);
|
|
496
|
+
|
|
346
497
|
/** Everything the registered Machines carry that a converge or a boot must act on. */
|
|
347
498
|
export type CarriedParts = {
|
|
348
499
|
agents: CarriedAgent[];
|
|
500
|
+
/** Every Effect slot, in walk order — one per Machine and slot key, at the first route the walk
|
|
501
|
+
* reached its Machine by. The placement reads {@link placedEffectsOf}: one per route. */
|
|
502
|
+
effects: CarriedEffect[];
|
|
349
503
|
images: CarriedImage[];
|
|
350
504
|
/** Every bound Repo, deduped by identity, in walk order. */
|
|
351
505
|
repos: CarriedRepo[];
|
|
@@ -355,8 +509,32 @@ export type CarriedParts = {
|
|
|
355
509
|
/** Every Agent whose model is still Open (ADR-0054), in walk order — the same refusal, by the
|
|
356
510
|
* same route, and the reason these are two lists rather than one: the fix lines differ. */
|
|
357
511
|
openAgents: OpenSlot[];
|
|
512
|
+
/** Every Effect whose url, argv or secret is still Open (ADR-0054), in walk order — the same
|
|
513
|
+
* refusal, by the same route; `parts` says which line binds it: one object for both of two. */
|
|
514
|
+
openEffects: Array<OpenSlot & { parts: OpenEffectPart[] }>;
|
|
515
|
+
/** Every `workspace()` reached, with the Size it states, in walk order (ADR-0060). */
|
|
516
|
+
sizes: CarriedSize[];
|
|
358
517
|
/** Whether any Machine reached, at any depth, composes a Sandbox — the data-plane switch. */
|
|
359
518
|
composesSandbox: boolean;
|
|
519
|
+
/**
|
|
520
|
+
* Every Open body slot (ADR-0070), in walk order, with its declaration: what it lends, what a Chart
|
|
521
|
+
* may define, its ceilings. The one Open part a run binds, not a composer, so it is never in an
|
|
522
|
+
* Open list: an Open body whose lent names did not resolve never reaches the walk (createMachine
|
|
523
|
+
* refused it).
|
|
524
|
+
*/
|
|
525
|
+
openBodies: Array<{ slot: string; declaration: OpenBodyDeclaration; path: OpenSlot["path"] }>;
|
|
526
|
+
/**
|
|
527
|
+
* Every model a key is placed and a provider preflighted for: each bound Agent's, and each model
|
|
528
|
+
* an Open body allows a Chart's own Agents (ADR-0070: a minted model is a door refusal, never a
|
|
529
|
+
* 401 mid-run). Deduped, sorted.
|
|
530
|
+
*/
|
|
531
|
+
models: string[];
|
|
532
|
+
/**
|
|
533
|
+
* The Instance Harness is needed (ADR-0031): an Agent declares `workspace: "none"`, or an Open
|
|
534
|
+
* body allows it. `models` are the models its Turns may run: those Agents', and those Open bodies'
|
|
535
|
+
* allowed ones. Absent when nothing needs it.
|
|
536
|
+
*/
|
|
537
|
+
instanceHarness?: { models: string[] };
|
|
360
538
|
};
|
|
361
539
|
|
|
362
540
|
/** A machine actor, told apart from a promise/callback/observable one by having a state tree.
|
|
@@ -366,16 +544,48 @@ export function asMachine(logic: unknown): AnyStateMachine | undefined {
|
|
|
366
544
|
return (logic as AnyStateMachine | undefined)?.root ? (logic as AnyStateMachine) : undefined;
|
|
367
545
|
}
|
|
368
546
|
|
|
369
|
-
/** Every machine object written INLINE on one state's invokes
|
|
547
|
+
/** Every machine object written INLINE on one state's invokes, with the hop xstate gives it (its
|
|
548
|
+
* generated src, from the invoke's index on the state). A named `src` resolves through
|
|
370
549
|
* `implementations.actors` instead and is walked there. */
|
|
371
|
-
function inlineMachines(node: StateNode<any, any>): AnyStateMachine
|
|
550
|
+
function inlineMachines(node: StateNode<any, any>): Array<{ machine: AnyStateMachine; hop: RouteHop }> {
|
|
372
551
|
const config = [node.config.invoke ?? []].flat() as Array<{ src?: unknown }>;
|
|
373
|
-
return config.flatMap((inv) => {
|
|
552
|
+
return config.flatMap((inv, index) => {
|
|
374
553
|
const machine = asMachine(inv?.src);
|
|
375
|
-
return machine ? [machine] : [];
|
|
554
|
+
return machine ? [{ machine, hop: inlineHop(index, node.id) }] : [];
|
|
376
555
|
});
|
|
377
556
|
}
|
|
378
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Refuse a Part written inline on an `invoke.src` — an `agent()`, an Effect or a `workspace()` that
|
|
560
|
+
* is no key of the Machine's `actors` (see the module comment). Thrown at build by
|
|
561
|
+
* `jr2Setup.createMachine` and at load by {@link partsOf}, so a Machine built with xstate's own
|
|
562
|
+
* `createMachine` is refused where `jr2 up` and the Orchestrator read it. A plain child Machine
|
|
563
|
+
* written inline is not a Part: it stays walked, with no `customize()` route (its `path` is
|
|
564
|
+
* `undefined`).
|
|
565
|
+
*/
|
|
566
|
+
export function refuseInlineParts(machine: AnyStateMachine): void {
|
|
567
|
+
const partOf = (src: unknown): { part: string; written: string } | undefined => {
|
|
568
|
+
if (isAgent(src)) return { part: "an Agent", written: "agent(...)" };
|
|
569
|
+
if (isEffect(src)) return { part: "an Effect", written: `${src.effect.kind}(...)` };
|
|
570
|
+
const child = asMachine(src);
|
|
571
|
+
if (child && composesSandbox(child)) return { part: "a Workspace", written: "workspace(...)" };
|
|
572
|
+
return undefined;
|
|
573
|
+
};
|
|
574
|
+
const visit = (node: StateNode<any, any>): void => {
|
|
575
|
+
for (const inv of [node.config.invoke ?? []].flat() as Array<{ src?: unknown }>) {
|
|
576
|
+
const found = partOf(inv?.src);
|
|
577
|
+
if (found === undefined) continue;
|
|
578
|
+
throw new Error(
|
|
579
|
+
`machine "${machine.id}": state "${node.id}" invokes ${found.part} inline — a Part is a key of actors, so ` +
|
|
580
|
+
`it has a name that the ledger, the report and customize() share: actors: { <name>: ${found.written} }, ` +
|
|
581
|
+
`then invoke: { src: "<name>" }`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
for (const child of Object.values(node.states as Record<string, StateNode<any, any>>)) visit(child);
|
|
585
|
+
};
|
|
586
|
+
visit(machine.root);
|
|
587
|
+
}
|
|
588
|
+
|
|
379
589
|
/** `JSON.stringify` with object keys sorted at every depth — a value's identity, not its spelling.
|
|
380
590
|
* Symbols are spelled out rather than dropped: `JSON.stringify` silently omits a symbol-valued
|
|
381
591
|
* key, which would make an Open Agent (ADR-0054) collapse into a bound one that differs in nothing
|
|
@@ -405,10 +615,16 @@ function canonical(value: unknown): string {
|
|
|
405
615
|
*/
|
|
406
616
|
export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
|
|
407
617
|
const agents: CarriedAgent[] = [];
|
|
618
|
+
const effects: CarriedEffect[] = [];
|
|
408
619
|
const images: CarriedImage[] = [];
|
|
409
620
|
const repos: CarriedRepo[] = [];
|
|
410
621
|
const openSlots: OpenSlot[] = [];
|
|
411
622
|
const openAgents: OpenSlot[] = [];
|
|
623
|
+
const openEffects: CarriedParts["openEffects"] = [];
|
|
624
|
+
const sizes: CarriedSize[] = [];
|
|
625
|
+
const openBodies: CarriedParts["openBodies"] = [];
|
|
626
|
+
// The Machine that declares each Open body: where its lent Agents are read (not carried data).
|
|
627
|
+
const bodyWrappers = new Map<CarriedParts["openBodies"][number], AnyStateMachine>();
|
|
412
628
|
let sandboxed = false;
|
|
413
629
|
const seen = new Set<string>();
|
|
414
630
|
// Cycle guard AND work saver: a Machine reached twice carries the same parts both times, and a
|
|
@@ -466,35 +682,316 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
|
|
|
466
682
|
}
|
|
467
683
|
};
|
|
468
684
|
|
|
469
|
-
const walkStates = (node: StateNode<any, any>, visit: (machine: AnyStateMachine) => void): void => {
|
|
470
|
-
for (const machine of inlineMachines(node)) visit(machine);
|
|
471
|
-
for (const child of Object.values(node.states as Record<string, StateNode<any, any>>)) walkStates(child, visit);
|
|
472
|
-
};
|
|
473
|
-
|
|
474
685
|
// `path` is the `customize()` route from the registered root to `machine` (see `OpenSlot`):
|
|
475
686
|
// a named child slot extends it, a wrapper's transparent body does not, and an inline invoke
|
|
476
687
|
// ends it — nothing downstream of an actor without a slot key can be named by a composer.
|
|
477
|
-
|
|
688
|
+
// `route` is the placement route, which an inline invoke extends by its hop (`PlacementRoute`).
|
|
689
|
+
const walk = (machine: AnyStateMachine, path: OpenSlot["path"], route: PlacementRoute): void => {
|
|
478
690
|
if (walked.has(machine)) return;
|
|
479
691
|
walked.add(machine);
|
|
692
|
+
refuseInlineParts(machine);
|
|
480
693
|
const parts = sandboxPartsOf(machine);
|
|
694
|
+
const user = userSeatOf(parts.user);
|
|
481
695
|
collectImage(parts.image);
|
|
482
|
-
collectImage(
|
|
696
|
+
collectImage(user.image);
|
|
697
|
+
collectImage(parts.effector?.image);
|
|
483
698
|
if (composesSandbox(machine)) {
|
|
484
699
|
sandboxed = true;
|
|
485
700
|
collectRepos(path, parts.repos);
|
|
701
|
+
sizes.push({
|
|
702
|
+
path,
|
|
703
|
+
route,
|
|
704
|
+
...(parts.resources !== undefined ? { resources: parts.resources } : {}),
|
|
705
|
+
...(user.resources !== undefined ? { user: user.resources } : {}),
|
|
706
|
+
});
|
|
486
707
|
}
|
|
487
708
|
const body = wrapperBodyOf(machine);
|
|
488
709
|
for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
|
|
489
710
|
if (isAgent(logic)) collectAgent(path, name, logic.definition);
|
|
490
|
-
else {
|
|
711
|
+
else if (isOpenBody(logic)) {
|
|
712
|
+
const body = { slot: name, declaration: openBodyDeclarationOf(logic), path };
|
|
713
|
+
openBodies.push(body);
|
|
714
|
+
bodyWrappers.set(body, machine);
|
|
715
|
+
} else if (isEffect(logic)) {
|
|
716
|
+
const effect = carriedEffectOf(name, path, route, logic.effect);
|
|
717
|
+
effects.push(effect);
|
|
718
|
+
if (effect.kind !== "push" && effect.open !== undefined)
|
|
719
|
+
openEffects.push({ slot: name, path, parts: [...effect.open] });
|
|
720
|
+
} else {
|
|
491
721
|
const child = asMachine(logic);
|
|
492
|
-
if (child) walk(child, path
|
|
722
|
+
if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name]);
|
|
493
723
|
}
|
|
494
724
|
}
|
|
495
|
-
walkStates(machine.root, (inline) => walk(inline, undefined));
|
|
725
|
+
walkStates(machine.root, (inline, hop) => walk(inline, undefined, [...route, hop]));
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
for (const machine of machines) walk(machine, [], []);
|
|
729
|
+
const bound = (m: unknown): m is string => typeof m === "string";
|
|
730
|
+
const models = new Set<string>(agents.map((a) => a.definition.model).filter(bound));
|
|
731
|
+
for (const body of openBodies) for (const m of body.declaration.allows.models) models.add(m);
|
|
732
|
+
const menuOnly = agents.filter((a) => a.definition.workspace === "none");
|
|
733
|
+
const noneBodies = openBodies.filter((b) => bodyTurnsRunIn(bodyWrappers.get(b)!, b.declaration, "instance"));
|
|
734
|
+
const harnessModels = new Set<string>(menuOnly.map((a) => a.definition.model).filter(bound));
|
|
735
|
+
for (const body of noneBodies) for (const m of body.declaration.allows.models) harnessModels.add(m);
|
|
736
|
+
return {
|
|
737
|
+
agents,
|
|
738
|
+
effects,
|
|
739
|
+
images,
|
|
740
|
+
repos,
|
|
741
|
+
openSlots,
|
|
742
|
+
openAgents,
|
|
743
|
+
openEffects,
|
|
744
|
+
sizes,
|
|
745
|
+
composesSandbox: sandboxed,
|
|
746
|
+
openBodies,
|
|
747
|
+
models: [...models].sort(),
|
|
748
|
+
...(menuOnly.length || noneBodies.length ? { instanceHarness: { models: [...harnessModels].sort() } } : {}),
|
|
496
749
|
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Can a Turn under an Open body of `wrapper` run where `where` says, with a model the body allows
|
|
754
|
+
* (ADR-0070: a minted model needs a key the Assembly placed, a door refusal and never a 401)? A
|
|
755
|
+
* Chart's own Agent runs where its Workspace access says, so a body that allows "write" or "read"
|
|
756
|
+
* runs Turns in a pod, and one that allows "none" at the Instance Harness. A lent Agent runs where
|
|
757
|
+
* its own access says, and a Chart may set its `model` dial to an allowed model: a lent Agent that
|
|
758
|
+
* is not "none" is a pod Turn, and a "none" one an Instance Harness Turn.
|
|
759
|
+
*/
|
|
760
|
+
function bodyTurnsRunIn(wrapper: AnyStateMachine, decl: OpenBodyDeclaration, where: "pod" | "instance"): boolean {
|
|
761
|
+
const atInstance = (w: string | undefined) => w === "none";
|
|
762
|
+
if (decl.allows.workspace.some((w) => (where === "instance" ? atInstance(w) : !atInstance(w)))) return true;
|
|
763
|
+
const actors = wrapper.implementations.actors as Record<string, unknown>;
|
|
764
|
+
return (decl.lends.agents ?? []).some((slot) => {
|
|
765
|
+
const logic = actors[slot];
|
|
766
|
+
if (!isAgent(logic)) return false;
|
|
767
|
+
const access = (logic.definition as { workspace?: string }).workspace;
|
|
768
|
+
return where === "instance" ? atInstance(access) : !atInstance(access);
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** The `customize()` route to a child of slot `name`: a wrapper's body does not extend it. */
|
|
773
|
+
function slotPath(path: OpenSlot["path"], name: string, body: string | undefined): OpenSlot["path"] {
|
|
774
|
+
return path === undefined ? undefined : name === body ? path : [...path, name];
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Every Effect the registered Machine `root` places, at every route it is reached by — one entry
|
|
779
|
+
* per route, as {@link workspacePodsOf} lists pods: a Machine invoked under two slot keys places
|
|
780
|
+
* its Effects twice, and each is checked where it is. What the placement walk reads (ADR-0068),
|
|
781
|
+
* never {@link partsOf}'s list, which holds one entry per Machine. A cycle (a recursive pool
|
|
782
|
+
* worker) stops at the Machine on its own route.
|
|
783
|
+
*/
|
|
784
|
+
export function placedEffectsOf(root: AnyStateMachine): CarriedEffect[] {
|
|
785
|
+
const effects: CarriedEffect[] = [];
|
|
786
|
+
const walk = (
|
|
787
|
+
machine: AnyStateMachine,
|
|
788
|
+
path: OpenSlot["path"],
|
|
789
|
+
route: PlacementRoute,
|
|
790
|
+
on: Set<AnyStateMachine>,
|
|
791
|
+
): void => {
|
|
792
|
+
if (on.has(machine)) return;
|
|
793
|
+
on.add(machine);
|
|
794
|
+
const body = wrapperBodyOf(machine);
|
|
795
|
+
for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
|
|
796
|
+
if (isAgent(logic)) continue;
|
|
797
|
+
if (isEffect(logic)) {
|
|
798
|
+
effects.push(carriedEffectOf(name, path, route, logic.effect));
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const child = asMachine(logic);
|
|
802
|
+
if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name], on);
|
|
803
|
+
}
|
|
804
|
+
walkStates(machine.root, (inline, hop) => walk(inline, undefined, [...route, hop], on));
|
|
805
|
+
on.delete(machine);
|
|
806
|
+
};
|
|
807
|
+
walk(root, [], [], new Set());
|
|
808
|
+
return effects;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** One Effect slot's plain data, as `jr2 up` and the placement read it. */
|
|
812
|
+
export function carriedEffectOf(
|
|
813
|
+
name: string,
|
|
814
|
+
path: OpenSlot["path"],
|
|
815
|
+
route: PlacementRoute,
|
|
816
|
+
parts: EffectParts,
|
|
817
|
+
): CarriedEffect {
|
|
818
|
+
const placement = placementOf(parts);
|
|
819
|
+
const openParts = openPartsOf(parts);
|
|
820
|
+
if (parts.kind === "http") {
|
|
821
|
+
return {
|
|
822
|
+
name,
|
|
823
|
+
path,
|
|
824
|
+
route,
|
|
825
|
+
kind: "http",
|
|
826
|
+
placement,
|
|
827
|
+
method: parts.method,
|
|
828
|
+
...(parts.target === open ? {} : { target: { ...parts.target } }),
|
|
829
|
+
...(openParts.length ? { open: openParts as Array<"url" | "secret"> } : {}),
|
|
830
|
+
secrets: [...parts.secrets],
|
|
831
|
+
idempotent: parts.idempotent,
|
|
832
|
+
timeoutMs: parts.timeoutMs,
|
|
833
|
+
...(parts.file ? { file: true as const } : {}),
|
|
834
|
+
...(parts.file?.slot !== undefined ? { slot: parts.file.slot } : {}),
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
if (parts.kind === "push") {
|
|
838
|
+
const { sign, squash, force, pin, timeoutMs, slot } = parts;
|
|
839
|
+
return {
|
|
840
|
+
name,
|
|
841
|
+
path,
|
|
842
|
+
route,
|
|
843
|
+
kind: "push",
|
|
844
|
+
placement,
|
|
845
|
+
sign,
|
|
846
|
+
squash,
|
|
847
|
+
force,
|
|
848
|
+
pin,
|
|
849
|
+
secrets: [],
|
|
850
|
+
timeoutMs,
|
|
851
|
+
...(slot !== undefined ? { slot } : {}),
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
const { argv, timeoutMs, slot, secrets, pin } = parts;
|
|
855
|
+
return {
|
|
856
|
+
name,
|
|
857
|
+
path,
|
|
858
|
+
route,
|
|
859
|
+
kind: "command",
|
|
860
|
+
placement,
|
|
861
|
+
argv: argvText(argv),
|
|
862
|
+
timeoutMs,
|
|
863
|
+
...(slot !== undefined ? { slot } : {}),
|
|
864
|
+
...(secrets?.length ? { secrets: [...secrets] } : {}),
|
|
865
|
+
...(pin ? { pin } : {}),
|
|
866
|
+
...(openParts.length ? { open: openParts as Array<"argv" | "secret"> } : {}),
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* One Workspace a Machine composes, as its pod's Held secrets are placed (ADR-0059): what the pod
|
|
872
|
+
* names. Located like a {@link CarriedSize} — `path` is the `customize()` route to its
|
|
873
|
+
* `workspace()`, and `route` its placement route, the same route the run host reads off the live
|
|
874
|
+
* actor at provision (`actorRoute`), so `jr2 up` and the provision name one pod alike.
|
|
875
|
+
*/
|
|
876
|
+
export type WorkspacePod = {
|
|
877
|
+
path: readonly string[] | undefined;
|
|
878
|
+
/** The pod's placement route: what its pod key reads (`workspaceHeldPod`). */
|
|
879
|
+
route: PlacementRoute;
|
|
880
|
+
/** The `workspace()`'s own `secrets: [...]`, as written. */
|
|
881
|
+
secrets: string[];
|
|
882
|
+
/** The model of every bound Agent whose Turn runs in this pod: each Agent below the
|
|
883
|
+
* `workspace()` that is not `workspace: "none"` (those run at the Instance Harness, ADR-0031),
|
|
884
|
+
* down to the next `workspace()`, which is a pod of its own, and every model an Open body below
|
|
885
|
+
* it allows a Chart's own Agents when it allows "write" or "read" (ADR-0070). Sorted, deduped. */
|
|
886
|
+
models: string[];
|
|
887
|
+
/** The Effects placed in this pod's Sandbox Effector (ADR-0068): every `push`, file-body `http` and
|
|
888
|
+
* credentialed `command` slot below the `workspace()`, down to the next `workspace()` — read off
|
|
889
|
+
* their plain data, statically. Empty → the pod gets no Sandbox Effector and no Secret. */
|
|
890
|
+
effects: CarriedEffect[];
|
|
891
|
+
/** The `workspace()`'s `effector: { image }`, as written. Absent → the Sandbox Image. */
|
|
892
|
+
effector?: { image: string };
|
|
893
|
+
/** The `workspace()`'s own `image`, as written — its Sandbox Image. Absent → `images/default`,
|
|
894
|
+
* then the stock Harness (ADR-0037). */
|
|
895
|
+
image?: string;
|
|
896
|
+
/** Each Repo Slot's Binding url as the Machine binds it, by slot, in order; undefined for an open
|
|
897
|
+
* or per-run slot, whose url `jr2 up` cannot read. A push resolves its credential through it. */
|
|
898
|
+
bindings: Record<string, string | undefined>;
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Every Workspace a Machine composes, at every route it is reached by — one entry per route, so a
|
|
903
|
+
* `workspace()` composed under two slot keys is two pods. Not deduped like {@link partsOf}: a pod
|
|
904
|
+
* is a route, not a Machine. A cycle (a recursive pool worker) stops at the Machine on its own
|
|
905
|
+
* route.
|
|
906
|
+
*/
|
|
907
|
+
export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
|
|
908
|
+
const pods: WorkspacePod[] = [];
|
|
909
|
+
/** The bound models of the Agents in `machine` and the Machines it composes, short of a
|
|
910
|
+
* `workspace()`. */
|
|
911
|
+
const modelsIn = (
|
|
912
|
+
machine: AnyStateMachine,
|
|
913
|
+
models: Set<string>,
|
|
914
|
+
on: Set<AnyStateMachine>,
|
|
915
|
+
effects: Map<string, CarriedEffect>,
|
|
916
|
+
path: OpenSlot["path"],
|
|
917
|
+
route: PlacementRoute,
|
|
918
|
+
): void => {
|
|
919
|
+
if (on.has(machine)) return;
|
|
920
|
+
on.add(machine);
|
|
921
|
+
const body = wrapperBodyOf(machine);
|
|
922
|
+
const visit = (child: AnyStateMachine, at: OpenSlot["path"], to: PlacementRoute) => {
|
|
923
|
+
if (!composesSandbox(child)) modelsIn(child, models, on, effects, at, to);
|
|
924
|
+
};
|
|
925
|
+
for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
|
|
926
|
+
if (isAgent(logic)) {
|
|
927
|
+
const { model, workspace } = logic.definition;
|
|
928
|
+
if (workspace !== "none" && typeof model === "string") models.add(model);
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
// A Turn under an Open body may run in this pod with any model the body allows (ADR-0070):
|
|
932
|
+
// a Chart's own Agent, when the body allows a Workspace access other than "none", and a lent
|
|
933
|
+
// Agent that is not "none", whose `model` dial a Chart may set to an allowed model.
|
|
934
|
+
if (isOpenBody(logic)) {
|
|
935
|
+
const decl = openBodyDeclarationOf(logic);
|
|
936
|
+
if (bodyTurnsRunIn(machine, decl, "pod")) for (const m of decl.allows.models) models.add(m);
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
if (isEffect(logic)) {
|
|
940
|
+
const effect = carriedEffectOf(name, path, route, logic.effect);
|
|
941
|
+
if (effect.placement === "sandbox") effects.set(JSON.stringify([route, name]), effect);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const child = asMachine(logic);
|
|
945
|
+
if (child) visit(child, slotPath(path, name, body), name === body ? route : [...route, name]);
|
|
946
|
+
}
|
|
947
|
+
walkStates(machine.root, (inline, hop) => visit(inline, undefined, [...route, hop]));
|
|
948
|
+
on.delete(machine);
|
|
949
|
+
};
|
|
950
|
+
const walk = (
|
|
951
|
+
machine: AnyStateMachine,
|
|
952
|
+
path: OpenSlot["path"],
|
|
953
|
+
route: PlacementRoute,
|
|
954
|
+
on: Set<AnyStateMachine>,
|
|
955
|
+
): void => {
|
|
956
|
+
if (on.has(machine)) return;
|
|
957
|
+
on.add(machine);
|
|
958
|
+
if (composesSandbox(machine)) {
|
|
959
|
+
const models = new Set<string>();
|
|
960
|
+
const effects = new Map<string, CarriedEffect>();
|
|
961
|
+
modelsIn(machine, models, new Set(), effects, path, route);
|
|
962
|
+
const parts = sandboxPartsOf(machine);
|
|
963
|
+
const bindings: Record<string, string | undefined> = {};
|
|
964
|
+
if (parts.repos !== open) {
|
|
965
|
+
for (const [slot, value] of Object.entries(parts.repos)) {
|
|
966
|
+
const state = repoSlotState(value);
|
|
967
|
+
bindings[slot] = state.kind === "bound" ? state.binding.url : undefined;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
pods.push({
|
|
971
|
+
path,
|
|
972
|
+
route,
|
|
973
|
+
secrets: [...(parts.secrets ?? [])],
|
|
974
|
+
models: [...models].sort(),
|
|
975
|
+
effects: [...effects.values()],
|
|
976
|
+
...(parts.effector ? { effector: { ...parts.effector } } : {}),
|
|
977
|
+
...(parts.image !== undefined ? { image: parts.image } : {}),
|
|
978
|
+
bindings,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
const body = wrapperBodyOf(machine);
|
|
982
|
+
for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
|
|
983
|
+
const child = isAgent(logic) || isEffect(logic) ? undefined : asMachine(logic);
|
|
984
|
+
if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name], on);
|
|
985
|
+
}
|
|
986
|
+
walkStates(machine.root, (inline, hop) => walk(inline, undefined, [...route, hop], on));
|
|
987
|
+
on.delete(machine);
|
|
988
|
+
};
|
|
989
|
+
walk(root, [], [], new Set());
|
|
990
|
+
return pods;
|
|
991
|
+
}
|
|
497
992
|
|
|
498
|
-
|
|
499
|
-
|
|
993
|
+
/** Every machine object written inline below `node`, at any depth of its state tree, with its hop. */
|
|
994
|
+
function walkStates(node: StateNode<any, any>, visit: (machine: AnyStateMachine, hop: RouteHop) => void): void {
|
|
995
|
+
for (const { machine, hop } of inlineMachines(node)) visit(machine, hop);
|
|
996
|
+
for (const child of Object.values(node.states as Record<string, StateNode<any, any>>)) walkStates(child, visit);
|
|
500
997
|
}
|