@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/customize.ts
CHANGED
|
@@ -9,24 +9,31 @@
|
|
|
9
9
|
// `.provide()`, nothing to learn (ADR-0015's rule, restated by ADR-0049 — a `.with()` method and
|
|
10
10
|
// a callable-machine hybrid were both rejected there).
|
|
11
11
|
//
|
|
12
|
-
//
|
|
12
|
+
// Four moves, and each is the smallest one that works:
|
|
13
13
|
//
|
|
14
14
|
// - An AGENT override is xstate's own `provide`: `machine.provide({ actors: { coder:
|
|
15
15
|
// agent({ ...stock, ...override }) } })`. The slot already IS the Agent (ADR-0049), so
|
|
16
16
|
// retuning one is substituting a slot's logic, which is what `provide` is for.
|
|
17
|
+
// - An EFFECT binding is the same substitution (ADR-0054): an Effect whose url or argv the
|
|
18
|
+
// package left `open`, or whose auth, header or env names `secret(open)`, carries a binder,
|
|
19
|
+
// which builds the Effect again with the composer's parts, so the bound slot is checked as a
|
|
20
|
+
// written one is. Once — a bound part is fixed from then on.
|
|
17
21
|
// - A CHILD override is the same call one level down, recursing. Each level is exactly the
|
|
18
22
|
// one-level reach ADR-0015 found `provide` has — held by the composer who owns the child
|
|
19
23
|
// object, never host-side injection into somebody else's Machine.
|
|
20
|
-
// - The IMAGE and the
|
|
24
|
+
// - The IMAGE, the REPOS and the SIZE are the parts `provide` cannot carry: xstate copies implementations
|
|
21
25
|
// and passes the CONFIG through by reference, and every part a Machine carries is keyed on
|
|
22
26
|
// that config (parts.ts, vocabulary.ts). A new image or a bound slot therefore needs a new key
|
|
23
27
|
// — so those fields clone the wrapper's config and rebuild it with the same implementations,
|
|
24
|
-
// re-attaching what the original carried.
|
|
28
|
+
// re-attaching what the original carried. The load warnings of a bound argv are the same
|
|
29
|
+
// kind of part: the Machine the author wrote is rebuilt the same way, carrying the rechecked
|
|
30
|
+
// warnings in place of the Open Machine's.
|
|
25
31
|
//
|
|
26
32
|
// jr2's wrappers are TRANSPARENT: `agents`/`actors` route through `workspace()`'s `body` and
|
|
27
33
|
// `pool()`'s `worker`, so a consumer customizing a Workspace-rooted workflow never writes `body`
|
|
28
|
-
// and never has to know that jr2 wrapped anything. `image`/`user`/`repos` travel the same
|
|
29
|
-
// the other direction — down to the `workspace()` that owns the seats and the
|
|
34
|
+
// and never has to know that jr2 wrapped anything. `image`/`user`/`repos`/`resources` travel the same
|
|
35
|
+
// chain in the other direction — down to the `workspace()` that owns the seats, the slots and the
|
|
36
|
+
// Size.
|
|
30
37
|
//
|
|
31
38
|
// Both halves route on the wrapper's own RECORD of being one, never on a slot's spelling: the
|
|
32
39
|
// runtime reads `wrapperBodyOf` and the types read `JR2Wrapper` (parts.ts), which the wrappers
|
|
@@ -38,7 +45,7 @@
|
|
|
38
45
|
// its slot MAP Open (`repos: open`, ADR-0051) declared exactly this: every slot is the composer's
|
|
39
46
|
// to name, so the map they write is the declaration, not an addition to one.
|
|
40
47
|
|
|
41
|
-
import { StateMachine, type AnyStateMachine, type InputFrom, type ProvidedActor } from "xstate";
|
|
48
|
+
import { StateMachine, type AnyStateMachine, type InputFrom, type ProvidedActor, type UnknownActorLogic } from "xstate";
|
|
42
49
|
import type { AgentLogic } from "./actor.ts";
|
|
43
50
|
import { isAgent, type AgentDefinition } from "./agent.ts";
|
|
44
51
|
import { agent } from "./harness-client.ts";
|
|
@@ -56,7 +63,30 @@ import {
|
|
|
56
63
|
type RepoSlot,
|
|
57
64
|
type SandboxParts,
|
|
58
65
|
} from "./parts.ts";
|
|
59
|
-
import {
|
|
66
|
+
import { assertSize, assertUserSeat, layerSize, type Size, type UserContainer } from "./size.ts";
|
|
67
|
+
import {
|
|
68
|
+
attachInputSchema,
|
|
69
|
+
attachLoadWarnings,
|
|
70
|
+
attachSeats,
|
|
71
|
+
attachVocabulary,
|
|
72
|
+
inputSchemaOf,
|
|
73
|
+
loadWarningsOf,
|
|
74
|
+
seatsOf,
|
|
75
|
+
vocabularyOf,
|
|
76
|
+
} from "./vocabulary.ts";
|
|
77
|
+
import { isEffect, type Arg } from "./effect.ts";
|
|
78
|
+
import { attachLent, copyOpenBody, lentIn, openBodiesOf, openBodyDeclarationOf } from "./open-body.ts";
|
|
79
|
+
import { effectChecks } from "./effect-checks.ts";
|
|
80
|
+
import type { Text } from "./http-effect.ts";
|
|
81
|
+
import {
|
|
82
|
+
binderOf,
|
|
83
|
+
effectBindingShape,
|
|
84
|
+
openPartsOf,
|
|
85
|
+
openPartsText,
|
|
86
|
+
type EffectBinding,
|
|
87
|
+
type OpenEffect,
|
|
88
|
+
type OpenEffectPart,
|
|
89
|
+
} from "./open-effect.ts";
|
|
60
90
|
|
|
61
91
|
// --- What a Machine declares, read at the TYPE level ---------------------------------------------
|
|
62
92
|
// Everything below reads xstate's own `TActor` parameter — the `{ src, logic, id }` union
|
|
@@ -92,6 +122,26 @@ type LogicAt<M extends AnyStateMachine, K extends string> = Extract<SlotsOf<M>,
|
|
|
92
122
|
/** The Agent slots — told from every other slot by their logic, which no other actor kind has. */
|
|
93
123
|
type AgentSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: AgentLogic }>["src"];
|
|
94
124
|
|
|
125
|
+
/** The Effect slots with an Open part — told by the phantom `OpenEffect` their constructor's
|
|
126
|
+
* overload states for an `open` url or argv, or a `secret(open)` (open-effect.ts). */
|
|
127
|
+
type OpenEffectSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: OpenEffect<OpenEffectPart> }>["src"];
|
|
128
|
+
|
|
129
|
+
/** What one Open part binds to: a url, an argv, a secret's name. */
|
|
130
|
+
type PartBinding<P> = P extends "url"
|
|
131
|
+
? { url: string | Text }
|
|
132
|
+
: P extends "argv"
|
|
133
|
+
? { argv: readonly (string | Arg)[] }
|
|
134
|
+
: P extends "secret"
|
|
135
|
+
? { secret: string }
|
|
136
|
+
: never;
|
|
137
|
+
|
|
138
|
+
/** A union of object types as their intersection: one binding object with every key. */
|
|
139
|
+
type Intersected<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
|
|
140
|
+
|
|
141
|
+
/** What one Open Effect binds to: its url, its argv or its secret — for an Effect with two Open
|
|
142
|
+
* parts, one object with both keys, the intersection of their bindings (ADR-0054). */
|
|
143
|
+
type BindingOf<L> = L extends OpenEffect<infer P> ? Intersected<PartBinding<P>> : never;
|
|
144
|
+
|
|
95
145
|
/** The child-Machine slots: the slots composition reaches (ADR-0049 — a nested Machine is an
|
|
96
146
|
* ordinary `invoke` of an ordinary slot). */
|
|
97
147
|
type ChildSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: AnyStateMachine }>["src"];
|
|
@@ -150,6 +200,15 @@ export type Customize<M extends AnyStateMachine> = {
|
|
|
150
200
|
agents?: [AgentSlots<Reached<M>>] extends [never]
|
|
151
201
|
? never
|
|
152
202
|
: { [K in AgentSlots<Reached<M>>]?: Partial<AgentDefinition> };
|
|
203
|
+
/** Bind an Effect's Open parts (ADR-0054): `{ url }` for an http Effect (whatever its body) whose url
|
|
204
|
+
* the package left Open, `{ argv }` for a command whose argv it left Open, `{ secret }` for an
|
|
205
|
+
* Effect that names `secret(open)` — one object with both keys for an Effect with two, so a binding
|
|
206
|
+
* that leaves one out does not compile. Bound once, here — a value, never a mapper over the door,
|
|
207
|
+
* so the host is fixed before any run. Only an Effect with an Open part is offered: a part its
|
|
208
|
+
* author fixed stays fixed. A Machine with none takes `never`, as `agents` does. */
|
|
209
|
+
effects?: [OpenEffectSlots<Reached<M>>] extends [never]
|
|
210
|
+
? never
|
|
211
|
+
: { [K in OpenEffectSlots<Reached<M>>]?: BindingOf<LogicAt<Reached<M>, K>> };
|
|
153
212
|
/** Retune a Machine this one composes — the same shape, one level down. */
|
|
154
213
|
actors?: [ChildSlots<Reached<M>>] extends [never]
|
|
155
214
|
? never
|
|
@@ -157,8 +216,12 @@ export type Customize<M extends AnyStateMachine> = {
|
|
|
157
216
|
/** The Sandbox Image (ADR-0037): a `file:` URL to a docker context this module ships, or a
|
|
158
217
|
* registry ref. */
|
|
159
218
|
image?: string;
|
|
160
|
-
/** The User Container
|
|
161
|
-
|
|
219
|
+
/** The User Container (ADR-0005): its image in the same two shapes, or `{ image, resources }`
|
|
220
|
+
* with its split of the Size (ADR-0060). Replaces the seat whole — split included. */
|
|
221
|
+
user?: string | UserContainer;
|
|
222
|
+
/** The Size (ADR-0060), layered field by field over the `workspace()`'s own: the composer's
|
|
223
|
+
* Repo may be bigger than the author's. `limits.memory` and `limits.cpu`, nothing else. */
|
|
224
|
+
resources?: Size;
|
|
162
225
|
/** Bind the Repo Slots the reached `workspace()` declared (ADR-0051) — an open slot to a url,
|
|
163
226
|
* or a bound or per-run one to a different Binding; any of the three forms, so a consumer can
|
|
164
227
|
* also bind a mapper over the wrapper's door. A slot the Machine does not declare is a compile
|
|
@@ -171,10 +234,12 @@ export type Customize<M extends AnyStateMachine> = {
|
|
|
171
234
|
/** The same shape with the types erased — what the implementation walks. */
|
|
172
235
|
type LooseParts = {
|
|
173
236
|
agents?: Record<string, Partial<AgentDefinition> | undefined>;
|
|
237
|
+
effects?: Record<string, EffectBinding | undefined>;
|
|
174
238
|
actors?: Record<string, LooseParts | undefined>;
|
|
175
239
|
image?: string;
|
|
176
|
-
user?: string;
|
|
240
|
+
user?: string | UserContainer;
|
|
177
241
|
repos?: Record<string, RepoSlot | undefined>;
|
|
242
|
+
resources?: Size;
|
|
178
243
|
};
|
|
179
244
|
|
|
180
245
|
/**
|
|
@@ -183,23 +248,32 @@ type LooseParts = {
|
|
|
183
248
|
* `deep`/`quick`), and a run of either carries what it was given.
|
|
184
249
|
*/
|
|
185
250
|
export function customize<M extends AnyStateMachine>(machine: M, parts: Customize<M>): M {
|
|
186
|
-
const { agents, actors, image, user, repos } = parts as LooseParts;
|
|
251
|
+
const { agents, effects, actors, image, user, repos, resources } = parts as LooseParts;
|
|
187
252
|
let out: AnyStateMachine = machine;
|
|
188
253
|
// Order matters in one direction only: the Sandbox seats and slots REBUILD the wrapper, so they
|
|
189
254
|
// go last and carry the retuned implementations with them.
|
|
190
|
-
if (agents || actors) out = retune(out, { agents, actors });
|
|
255
|
+
if (agents || effects || actors) out = retune(out, { agents, effects, actors });
|
|
191
256
|
const seats: Seats = {
|
|
192
257
|
...(image !== undefined ? { image } : {}),
|
|
193
258
|
...(user !== undefined ? { user } : {}),
|
|
194
259
|
...(repos !== undefined ? { repos } : {}),
|
|
260
|
+
...(resources !== undefined ? { resources } : {}),
|
|
195
261
|
};
|
|
196
|
-
|
|
262
|
+
// Refused where the composer wrote them, by field (ADR-0060), before any wrapper is rebuilt.
|
|
263
|
+
if (seats.user !== undefined) assertUserSeat("customize()", seats.user);
|
|
264
|
+
if (seats.resources !== undefined) assertSize("customize(): resources", seats.resources);
|
|
265
|
+
if (Object.keys(seats).length > 0) out = reseat(out, seats);
|
|
197
266
|
return out as M;
|
|
198
267
|
}
|
|
199
268
|
|
|
200
|
-
/** What `reseat` carries down the chain: the two image seats
|
|
201
|
-
* only when the composer named it. */
|
|
202
|
-
type Seats = {
|
|
269
|
+
/** What `reseat` carries down the chain: the two image seats, the slot overrides and the Size, each
|
|
270
|
+
* present only when the composer named it. */
|
|
271
|
+
type Seats = {
|
|
272
|
+
image?: string;
|
|
273
|
+
user?: string | UserContainer;
|
|
274
|
+
repos?: Record<string, RepoSlot | undefined>;
|
|
275
|
+
resources?: Size;
|
|
276
|
+
};
|
|
203
277
|
|
|
204
278
|
/** The slot keys whose logic answers `pred` — what an error names, so the message is the
|
|
205
279
|
* Machine's own declaration rather than advice. */
|
|
@@ -246,6 +320,8 @@ function retune(machine: AnyStateMachine, parts: LooseParts): AnyStateMachine {
|
|
|
246
320
|
// walk reports it again (ADR-0054).
|
|
247
321
|
actors[name] = agent({ ...logic.definition, ...override });
|
|
248
322
|
}
|
|
323
|
+
const bound = Object.entries(parts.effects ?? {}).filter(([, b]) => b !== undefined);
|
|
324
|
+
for (const [name, binding] of bound) actors[name] = bindEffect(machine, name, binding!);
|
|
249
325
|
for (const [name, childParts] of Object.entries(parts.actors ?? {})) {
|
|
250
326
|
if (!childParts) continue;
|
|
251
327
|
const child = asMachine((machine.implementations.actors as Record<string, unknown>)[name]);
|
|
@@ -257,15 +333,114 @@ function retune(machine: AnyStateMachine, parts: LooseParts): AnyStateMachine {
|
|
|
257
333
|
}
|
|
258
334
|
actors[name] = customize(child, childParts as Customize<AnyStateMachine>);
|
|
259
335
|
}
|
|
260
|
-
|
|
336
|
+
// An Open body lends by slot name (ADR-0070), and its stamped lent set must be the retuned slots,
|
|
337
|
+
// not the import's: each body is copied (the import keeps its own stamp) and stamped again over
|
|
338
|
+
// the retuned actors.
|
|
339
|
+
if (Object.keys(actors).length > 0) {
|
|
340
|
+
const impl = machine.implementations as Parameters<typeof lentIn>[0];
|
|
341
|
+
const merged = { ...impl, actors: { ...(impl.actors ?? {}), ...actors } };
|
|
342
|
+
for (const { slot, logic } of openBodiesOf(machine)) {
|
|
343
|
+
const copy = copyOpenBody(logic);
|
|
344
|
+
attachLent(copy, lentIn(merged, openBodyDeclarationOf(logic)));
|
|
345
|
+
actors[slot] = copy;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const out = substitute(machine, actors);
|
|
349
|
+
if (bound.length === 0) return out;
|
|
350
|
+
// The bound Machine's warnings differ from the Open one's, and `provide` kept the Open one's
|
|
351
|
+
// config, which is what every attachment keys on (ADR-0049) — so it gets a config of its own,
|
|
352
|
+
// carrying what the original carried and the rechecked warnings, and any later `provide()` of
|
|
353
|
+
// it (a run's, ADR-0015) keeps them.
|
|
354
|
+
const warnings = recheck(machine, out);
|
|
355
|
+
const rebuilt = cloneConfig(out);
|
|
356
|
+
attachLoadWarnings(rebuilt, warnings);
|
|
357
|
+
return rebuilt;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Bind one Effect's Open parts (ADR-0054): the slot must be an Effect, each key of the binding one
|
|
362
|
+
* of its Open parts, and each value the value that part takes — a url (a string, or a `text` with a
|
|
363
|
+
* secret in its path), an argv, or a secret's name. A key left out leaves that part Open. The binder
|
|
364
|
+
* builds the Effect again, whole, so the bound one meets every check its constructor runs: the url's
|
|
365
|
+
* literal host, the argv's entries, the secret's name. The compiler's check of the
|
|
366
|
+
* invoke's input against the argv's slots does not reach it (an Open argv's input is one string per
|
|
367
|
+
* key): a slot the invoke does not supply faults when the Effect starts (ADR-0054).
|
|
368
|
+
*/
|
|
369
|
+
function bindEffect(machine: AnyStateMachine, name: string, binding: EffectBinding): unknown {
|
|
370
|
+
const logic = (machine.implementations.actors as Record<string, unknown>)[name];
|
|
371
|
+
if (!isEffect(logic)) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`customize(): machine "${machine.id}" carries no Effect slot "${name}" — its Effects are: ` +
|
|
374
|
+
`${listed(slotsWhere(machine, isEffect))} (ADR-0067). Only declared parts can be bound.`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const parts = openPartsOf(logic.effect);
|
|
378
|
+
const binder = binderOf(logic);
|
|
379
|
+
if (parts.length === 0 || binder === undefined) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`customize(): the ${logic.effect.kind} Effect "${name}" of machine "${machine.id}" leaves nothing Open — its ` +
|
|
382
|
+
"parts are fixed in its definition, and only a part a package left `open` is bound, once (ADR-0054).",
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
// One object binds them, each key an Open part; a key it leaves out leaves that part Open, and
|
|
386
|
+
// the walk reports it again (ADR-0054).
|
|
387
|
+
const given = typeof binding === "object" && binding !== null ? (binding as Record<string, unknown>) : {};
|
|
388
|
+
const keys = Object.keys(given).filter((k) => given[k] !== undefined);
|
|
389
|
+
if (keys.length === 0 || keys.some((k) => !parts.includes(k as OpenEffectPart))) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`customize(): the ${logic.effect.kind} Effect "${name}" leaves its ${openPartsText(parts)} Open — bind it ` +
|
|
392
|
+
`with ${effectBindingShape(parts)} (ADR-0054)`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
for (const part of keys) {
|
|
396
|
+
const value = given[part];
|
|
397
|
+
if (value === open || typeof value === "function") {
|
|
398
|
+
throw new Error(
|
|
399
|
+
`customize(): the ${logic.effect.kind} Effect "${name}"'s ${part} is bound once, to a value — not ` +
|
|
400
|
+
`${value === open ? "`open` again" : "a function of the run"}: no run chooses an Effect's ${part} (ADR-0054)`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (part === "secret" && typeof value !== "string") {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`customize(): the ${logic.effect.kind} Effect "${name}"'s secret is bound to a secret's name, a string — ` +
|
|
406
|
+
`the file the Effector reads it from: { secret: "<name>" } (ADR-0054, ADR-0068)`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return binder(binding);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The load walk's Effect checks (ADR-0067), again, on a Machine whose Open argv this `customize()`
|
|
415
|
+
* bound: at build its argv slots were unknown and every pick param counted as read; now the bound
|
|
416
|
+
* argv names them. A param it does not read is refused as at build, and a warning the walk gives
|
|
417
|
+
* for the bound argv (a param only an action the probe does not run may read) replaces the
|
|
418
|
+
* findings it gave for the Open one: the result is the warnings the same Machine written with that
|
|
419
|
+
* argv carries, which the caller attaches to the bound Machine's own config.
|
|
420
|
+
*/
|
|
421
|
+
function recheck(before: AnyStateMachine, machine: AnyStateMachine): string[] {
|
|
422
|
+
const defs = vocabularyOf(machine);
|
|
423
|
+
if (!defs) return loadWarningsOf(before);
|
|
424
|
+
const checks = (m: AnyStateMachine) =>
|
|
425
|
+
effectChecks(m, m.implementations.actors as Record<string, UnknownActorLogic>, defs);
|
|
426
|
+
const { errors, warnings } = checks(machine);
|
|
427
|
+
if (errors.length) throw new Error(`customize(): machine "${machine.id}": ${errors.join("; ")}`);
|
|
428
|
+
const unbound = new Set(checks(before).warnings);
|
|
429
|
+
const kept = loadWarningsOf(before).filter((warning) => !unbound.has(warning));
|
|
430
|
+
return [...new Set([...kept, ...warnings])];
|
|
261
431
|
}
|
|
262
432
|
|
|
263
433
|
/** The Sandbox seats and slots, applied to the `workspace()` the chain reaches. */
|
|
264
434
|
function reseat(machine: AnyStateMachine, seats: Seats): AnyStateMachine {
|
|
265
435
|
if (composesSandbox(machine)) {
|
|
266
|
-
const { repos: override, ...images } = seats;
|
|
436
|
+
const { repos: override, resources, ...images } = seats;
|
|
267
437
|
const parts = sandboxPartsOf(machine);
|
|
268
|
-
|
|
438
|
+
// The Size layers over the author's, field by field — a composer who raises memory keeps the
|
|
439
|
+
// author's cpu (ADR-0060).
|
|
440
|
+
const size = layerSize(parts.resources, resources);
|
|
441
|
+
const sized = size !== undefined ? { resources: size } : {};
|
|
442
|
+
if (parts.repos === open)
|
|
443
|
+
return rebuild(machine, { ...parts, ...images, ...sized, repos: nameSlots(machine, override) });
|
|
269
444
|
const repos = { ...parts.repos };
|
|
270
445
|
// Only DECLARED slots can be bound (ADR-0051): the Machine's own word for each Repo is the
|
|
271
446
|
// key, and a key it never declared would attach a repository the body has no handle for.
|
|
@@ -285,15 +460,16 @@ function reseat(machine: AnyStateMachine, seats: Seats): AnyStateMachine {
|
|
|
285
460
|
assertRepoSlot("customize()", slot, value);
|
|
286
461
|
repos[slot] = value;
|
|
287
462
|
}
|
|
288
|
-
return rebuild(machine, { ...parts, ...images, repos });
|
|
463
|
+
return rebuild(machine, { ...parts, ...images, ...sized, repos });
|
|
289
464
|
}
|
|
290
465
|
const inner = bodyOf(machine);
|
|
291
466
|
if (!inner) {
|
|
292
|
-
const named =
|
|
467
|
+
const named =
|
|
468
|
+
seats.repos !== undefined ? "`repos`" : seats.resources !== undefined ? "`resources`" : "`image`/`user`";
|
|
293
469
|
throw new Error(
|
|
294
|
-
`customize(): ${named} say what a SANDBOX is made of and which Repos it attaches, and
|
|
295
|
-
`"${machine.id}" composes none — only a workspace() wrapper carries those seats
|
|
296
|
-
"(ADR-0049, ADR-0037, ADR-0051).",
|
|
470
|
+
`customize(): ${named} say what a SANDBOX is made of, how big it is and which Repos it attaches, and ` +
|
|
471
|
+
`machine "${machine.id}" composes none — only a workspace() wrapper carries those seats, that Size ` +
|
|
472
|
+
"and those slots (ADR-0049, ADR-0037, ADR-0051, ADR-0060).",
|
|
297
473
|
);
|
|
298
474
|
}
|
|
299
475
|
return substitute(machine, { [inner.slot]: reseat(inner.body, seats) });
|
|
@@ -336,13 +512,28 @@ function nameSlots(machine: AnyStateMachine, override: Seats["repos"]): SandboxP
|
|
|
336
512
|
* fingerprint a restore compares (ADR-0030) — is bit-identical to the original's.
|
|
337
513
|
*/
|
|
338
514
|
function rebuild(machine: AnyStateMachine, seats: SandboxParts): AnyStateMachine {
|
|
515
|
+
const clone = cloneConfig(machine);
|
|
516
|
+
attachSandboxParts(clone, seats);
|
|
517
|
+
return clone;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* The Machine around a NEW config object — a shallow clone, so the shape and the fingerprint are
|
|
522
|
+
* the original's — with the same implementations, and every attachment the original carried
|
|
523
|
+
* re-attached under the new key (ADR-0049): the vocabulary, the door, the seats, the load
|
|
524
|
+
* warnings and a wrapper's body. What the caller attaches after is the clone's alone.
|
|
525
|
+
*/
|
|
526
|
+
function cloneConfig(machine: AnyStateMachine): AnyStateMachine {
|
|
339
527
|
const clone = new StateMachine({ ...machine.config }, machine.implementations) as AnyStateMachine;
|
|
340
528
|
const vocabulary = vocabularyOf(machine);
|
|
341
529
|
if (vocabulary) attachVocabulary(clone, vocabulary);
|
|
342
530
|
const door = inputSchemaOf(machine);
|
|
343
531
|
if (door) attachInputSchema(clone, door);
|
|
532
|
+
const seats = seatsOf(machine);
|
|
533
|
+
if (seats.length > 0) attachSeats(clone, seats);
|
|
534
|
+
const warnings = loadWarningsOf(machine);
|
|
535
|
+
if (warnings.length > 0) attachLoadWarnings(clone, warnings);
|
|
344
536
|
const slot = wrapperBodyOf(machine);
|
|
345
537
|
if (slot) attachWrapperBody(clone, slot);
|
|
346
|
-
attachSandboxParts(clone, seats);
|
|
347
538
|
return clone;
|
|
348
539
|
}
|