@jr2/orchestrator 0.1.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/LICENSE +21 -0
- package/README.md +23 -0
- package/bin/server.ts +23 -0
- package/console/canvas.ts +843 -0
- package/console/components/app.ts +79 -0
- package/console/components/drawer.ts +131 -0
- package/console/components/fleet.ts +117 -0
- package/console/components/machine-pane.ts +85 -0
- package/console/components/nav.ts +81 -0
- package/console/components/schema-form.ts +137 -0
- package/console/main.ts +383 -0
- package/console/page.html +28 -0
- package/console/store.ts +336 -0
- package/console/style.css +700 -0
- package/console/tsconfig.json +18 -0
- package/package.json +61 -0
- package/src/actor.ts +562 -0
- package/src/agent.ts +124 -0
- package/src/ambient.ts +50 -0
- package/src/config.ts +297 -0
- package/src/customize.ts +348 -0
- package/src/durability.ts +135 -0
- package/src/fingerprint.ts +92 -0
- package/src/gate.ts +76 -0
- package/src/harness-client.ts +503 -0
- package/src/http.ts +753 -0
- package/src/images.ts +303 -0
- package/src/index.ts +40 -0
- package/src/instance.ts +294 -0
- package/src/machine-doc.ts +334 -0
- package/src/names.ts +78 -0
- package/src/open.ts +17 -0
- package/src/parts.ts +500 -0
- package/src/pool.ts +284 -0
- package/src/registration.ts +340 -0
- package/src/repo-fetch.ts +259 -0
- package/src/repo-identity.ts +145 -0
- package/src/repos.ts +330 -0
- package/src/run-host.ts +1095 -0
- package/src/sandbox-kubectl.ts +1136 -0
- package/src/server.ts +220 -0
- package/src/setup.ts +360 -0
- package/src/snapshot-store.ts +150 -0
- package/src/stub-harness.ts +217 -0
- package/src/tokens.ts +126 -0
- package/src/vocabulary.ts +99 -0
- package/src/wire.ts +103 -0
- package/src/workspace.ts +874 -0
- package/tsconfig.instance.json +26 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// Serialize a workflow's Machine for the Console (`GET /workflows/:name/machine`). Walks the
|
|
2
|
+
// live StateNode tree (`machine.root`) rather than `machine.definition`/`toJSON()` — those carry
|
|
3
|
+
// entry/exit actions and output mappers as functions, which don't survive JSON. The DTO here is
|
|
4
|
+
// pure data: the nested state tree drives the renderer's containment, the flat transition list its
|
|
5
|
+
// edges. Structure doesn't depend on which actors fill the named slots, so the registered Machine
|
|
6
|
+
// (before any test-seam `.provide()` — ADR-0015) is the right thing to serialize.
|
|
7
|
+
//
|
|
8
|
+
// A workflow's real work usually happens in CHILD machines (`coding`'s whole feature pipeline is a
|
|
9
|
+
// `spawnChild`), so the walk descends into them too — one `MachineBodyDoc` per child, attached to
|
|
10
|
+
// the state that runs it. Each carries the `src` a live child actor reports, which is how the page
|
|
11
|
+
// hangs run state under the right subgraph.
|
|
12
|
+
//
|
|
13
|
+
// Vocabulary is attributed the same way: each `MachineBodyDoc` carries the events ITS Machine
|
|
14
|
+
// declares (ADR-0011, ADR-0049), never one flattened root list. Two Machines in one doc may
|
|
15
|
+
// declare the same name with different payloads — that is legal, and only a per-Machine listing
|
|
16
|
+
// can report it honestly.
|
|
17
|
+
|
|
18
|
+
import type { AnyStateMachine, StateNode, TransitionDefinition } from "xstate";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import type { EventAudience } from "@jr2/agent-protocol";
|
|
21
|
+
import { vocabularyOf } from "./vocabulary.ts";
|
|
22
|
+
|
|
23
|
+
/** One transition of the Machine, id-addressed at both ends. */
|
|
24
|
+
export type MachineTransitionDoc = {
|
|
25
|
+
/** Source state id. */
|
|
26
|
+
source: string;
|
|
27
|
+
/** Target state ids; empty = targetless (self/internal) transition. */
|
|
28
|
+
targets: string[];
|
|
29
|
+
/** The raw event descriptor (`""` for always/eventless). */
|
|
30
|
+
event: string;
|
|
31
|
+
/** Display label, precomputed here so the renderer stays dumb. */
|
|
32
|
+
label: string;
|
|
33
|
+
/** Normalized guard name, if guarded. */
|
|
34
|
+
guard?: string;
|
|
35
|
+
kind: "event" | "always" | "after" | "done" | "error";
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** One state of the Machine; `states` nests in document order. */
|
|
39
|
+
export type MachineStateDoc = {
|
|
40
|
+
/** Unique state id (custom `id:` respected). */
|
|
41
|
+
id: string;
|
|
42
|
+
/** Relative key — the segment that appears in a run's `status.value`. */
|
|
43
|
+
key: string;
|
|
44
|
+
type: "atomic" | "compound" | "parallel" | "final" | "history";
|
|
45
|
+
/** Initial child state id (compound only). */
|
|
46
|
+
initial?: string;
|
|
47
|
+
invoke: Array<{ id: string; src: string }>;
|
|
48
|
+
tags: string[];
|
|
49
|
+
description?: string;
|
|
50
|
+
states: MachineStateDoc[];
|
|
51
|
+
/** The child MACHINES this state runs — invoked ones (a subset of `invoke`, which stays the
|
|
52
|
+
* complete invocation list) plus spawned ones (which appear nowhere else). See
|
|
53
|
+
* {@link ChildMachineDoc}. A promise/callback actor has no state tree, so it is never here. */
|
|
54
|
+
children: ChildMachineDoc[];
|
|
55
|
+
/**
|
|
56
|
+
* This state runs an `enqueueActions` closure, so THIS STATE'S `children` MAY BE INCOMPLETE.
|
|
57
|
+
*
|
|
58
|
+
* Spawning is an action, and `spawnChild` is the only static trace it leaves (see
|
|
59
|
+
* {@link spawnedSrcs}). A spawn issued as `enqueue.spawnChild(...)` inside a closure resolves at
|
|
60
|
+
* runtime — it can be conditional, looped, or have a computed `src` — so no amount of static
|
|
61
|
+
* analysis recovers it, and the subgraph beneath it would simply be absent from the diagram while
|
|
62
|
+
* the machine ran correctly. That silence is the hazard, not the omission: it once erased the
|
|
63
|
+
* whole feature pipeline of a multi-stage Machine with no signal at all. So the doc carries the
|
|
64
|
+
* fact it cannot see, and callers say so out loud (`opaqueStates`, surfaced on the page).
|
|
65
|
+
*/
|
|
66
|
+
opaqueActions?: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A child Machine reached from a state — the pipeline a workflow delegates to, which is most of
|
|
71
|
+
* what it DOES. Attached to the state that runs it, so the renderer nests it where it belongs.
|
|
72
|
+
*
|
|
73
|
+
* `src` is the JOIN KEY: it is exactly the `src` a live child actor reports (`RunChild.src`), for
|
|
74
|
+
* both kinds — a named actor keeps its name (`"body"`, `"worker"`, `"featureWorkspace"`), an inline
|
|
75
|
+
* machine object gets xstate's generated key (`"xstate.invoke.0.wrapper.running"`) on both sides.
|
|
76
|
+
* That is what lets the page hang a run's live child state under the right subgraph without the two
|
|
77
|
+
* halves agreeing on anything but this string. Since ADR-0049 every kit wrapper names its child, so
|
|
78
|
+
* the generated key is an author's shape only.
|
|
79
|
+
*/
|
|
80
|
+
export type ChildMachineDoc = {
|
|
81
|
+
/** The join key — matches a live `RunChild.src` exactly. */
|
|
82
|
+
src: string;
|
|
83
|
+
/** Display name: the invoke id (`"body"`) or the spawned actor's name (`"featureWorkspace"`). */
|
|
84
|
+
label: string;
|
|
85
|
+
/** How the state reaches it: `invoke` binds it to the state's lifetime, `spawn` outlives it. */
|
|
86
|
+
via: "invoke" | "spawn";
|
|
87
|
+
/** The child's own structure. Absent iff `recursive`. */
|
|
88
|
+
machine?: MachineBodyDoc;
|
|
89
|
+
/** The machine is already an ancestor of this point — the body is up there, not repeated here. */
|
|
90
|
+
recursive?: true;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* One event a Machine declares — its Vocabulary entry (ADR-0011), as the doc carries it. Same
|
|
95
|
+
* shape a Gate's `accepts` serves, because it is the same thing seen statically: the name, what a
|
|
96
|
+
* caller sends (JSON Schema), and who may send it.
|
|
97
|
+
*/
|
|
98
|
+
export type MachineEventDoc = {
|
|
99
|
+
name: string;
|
|
100
|
+
description?: string;
|
|
101
|
+
audience: EventAudience;
|
|
102
|
+
/** The def's input schema as JSON Schema — what a form or a `jr2 send` prompt generates from. */
|
|
103
|
+
input: unknown;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** One Machine's structure, independent of what NAMES it (a `workflow` at the root, a `src` below). */
|
|
107
|
+
export type MachineBodyDoc = {
|
|
108
|
+
/** Machine id. */
|
|
109
|
+
id: string;
|
|
110
|
+
root: MachineStateDoc;
|
|
111
|
+
transitions: MachineTransitionDoc[];
|
|
112
|
+
/** The events THIS Machine declares, and only this one (ADR-0049): a nested Machine's ride its
|
|
113
|
+
* own `MachineBodyDoc`. Empty for a Machine not built by `jr2Setup`. */
|
|
114
|
+
events: MachineEventDoc[];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** The serialized structure of a workflow's Machine. */
|
|
118
|
+
export type MachineDoc = MachineBodyDoc & {
|
|
119
|
+
workflow: string;
|
|
120
|
+
/** States whose child list may be incomplete — see {@link opaqueStates}. Computed once here, on
|
|
121
|
+
* the server, so the notice reaches whoever renders the doc without every consumer re-walking it
|
|
122
|
+
* to discover the diagram is lying. Absent on the nested `MachineBodyDoc`s: it is a whole-doc
|
|
123
|
+
* question, and the root's walk already covers them. */
|
|
124
|
+
opaqueStates?: string[];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Normalize a guard to a display name: setup() name, parameterized type, or `"inline"`. */
|
|
128
|
+
function guardName(guard: unknown): string | undefined {
|
|
129
|
+
if (guard === undefined) return undefined;
|
|
130
|
+
if (typeof guard === "string") return guard;
|
|
131
|
+
// An inline arrow gets the property-inferred name "guard" — meaningless, so report "inline";
|
|
132
|
+
// a deliberately named function (`guard: function isValid() {…}`) keeps its name.
|
|
133
|
+
if (typeof guard === "function") return guard.name && guard.name !== "guard" ? guard.name : "inline";
|
|
134
|
+
return (guard as { type?: string }).type ?? "inline";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Normalize an invoke `src` to a display name: setup() actor name or `"inline"`. */
|
|
138
|
+
function srcName(src: unknown): string {
|
|
139
|
+
if (typeof src === "string") return src;
|
|
140
|
+
return ((src as { id?: string })?.id ?? "inline") as string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The tail of a done/error descriptor's id. xstate names an ANONYMOUS invoke after its own position
|
|
145
|
+
* (`0.body.working.claimTask`), so the raw descriptor spells out the whole path down to the state
|
|
146
|
+
* the edge already leaves from. Only the last segment says anything new — and a label is not free:
|
|
147
|
+
* in a layered graph an edge label's WIDTH becomes spacing between layers, so the path is paid for
|
|
148
|
+
* in diagram width. The full descriptor stays on `event`, so nothing is lost.
|
|
149
|
+
*/
|
|
150
|
+
const shortId = (id = "") => id.slice(id.lastIndexOf(".") + 1);
|
|
151
|
+
|
|
152
|
+
/** Classify an event descriptor and derive its display label. */
|
|
153
|
+
function eventLabel(eventType: string): { kind: MachineTransitionDoc["kind"]; label: string } {
|
|
154
|
+
if (eventType === "") return { kind: "always", label: "always" };
|
|
155
|
+
const done = /^xstate\.done\.(?:actor|state)\.(.*)$/.exec(eventType);
|
|
156
|
+
if (done) return { kind: "done", label: `done: ${shortId(done[1])}` };
|
|
157
|
+
const error = /^xstate\.error\.actor\.(.*)$/.exec(eventType);
|
|
158
|
+
if (error) return { kind: "error", label: `error: ${shortId(error[1])}` };
|
|
159
|
+
const after = /^xstate\.after\.([^.]+)\./.exec(eventType);
|
|
160
|
+
if (after) return { kind: "after", label: `after ${after[1]}` };
|
|
161
|
+
return { kind: "event", label: eventType };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function serializeTransition(t: TransitionDefinition<any, any>): MachineTransitionDoc {
|
|
165
|
+
const { kind, label } = eventLabel(t.eventType);
|
|
166
|
+
const guard = guardName(t.guard);
|
|
167
|
+
const doc: MachineTransitionDoc = {
|
|
168
|
+
source: t.source.id,
|
|
169
|
+
targets: (t.target ?? []).map((s) => s.id),
|
|
170
|
+
event: t.eventType,
|
|
171
|
+
label,
|
|
172
|
+
kind,
|
|
173
|
+
};
|
|
174
|
+
if (guard !== undefined) doc.guard = guard;
|
|
175
|
+
return doc;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The registry a `src` name resolves against, plus the cycle guard. One per machine level. */
|
|
179
|
+
type Scope = {
|
|
180
|
+
/** The machine's `setup({ actors })` registry — where a named `src` is bound. */
|
|
181
|
+
actors: Record<string, unknown>;
|
|
182
|
+
/** The machines on the path from the root down to here. A machine already on it does not recurse. */
|
|
183
|
+
path: Set<AnyStateMachine>;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** A machine actor, told apart from a promise/callback/observable one by having a state tree. */
|
|
187
|
+
function asMachine(logic: unknown): AnyStateMachine | undefined {
|
|
188
|
+
return (logic as AnyStateMachine | undefined)?.root ? (logic as AnyStateMachine) : undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The machine behind `node.invoke[index]`, if it is one. Named actors resolve through the registry;
|
|
192
|
+
* an INLINE machine object survives only on the raw config node — xstate rewrites
|
|
193
|
+
* `StateNode.invoke[].src` to a generated key (which is exactly the key the live child reports, so
|
|
194
|
+
* the doc keeps it as the join `src` and looks the logic up here instead). */
|
|
195
|
+
function invokedMachine(node: StateNode<any, any>, index: number, scope: Scope): AnyStateMachine | undefined {
|
|
196
|
+
const src = node.invoke[index]?.src;
|
|
197
|
+
if (typeof src === "string") {
|
|
198
|
+
const named = asMachine(scope.actors[src]);
|
|
199
|
+
if (named) return named;
|
|
200
|
+
}
|
|
201
|
+
const config = [node.config.invoke ?? []].flat()[index] as { src?: unknown } | undefined;
|
|
202
|
+
return asMachine(config?.src);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Walk every action list a state can carry: its entry, and all of its transitions'. */
|
|
206
|
+
function eachAction(node: StateNode<any, any>, visit: (action: { type?: unknown; src?: unknown }) => void): void {
|
|
207
|
+
const scan = (actions: readonly unknown[] | undefined) => {
|
|
208
|
+
for (const action of actions ?? []) visit(action as { type?: unknown; src?: unknown });
|
|
209
|
+
};
|
|
210
|
+
scan(node.entry);
|
|
211
|
+
for (const defs of node.transitions.values()) for (const t of defs) scan(t.actions);
|
|
212
|
+
for (const t of node.always ?? []) scan(t.actions);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Does this state run an `enqueueActions` closure? If so its {@link MachineStateDoc.children} may be
|
|
217
|
+
* incomplete — the closure is opaque, and a spawn inside it leaves no static trace at all.
|
|
218
|
+
*
|
|
219
|
+
* We cannot see through it (the body resolves at runtime against a live context and event), so the
|
|
220
|
+
* honest move is to REPORT that we cannot, rather than emit a confidently wrong diagram. xstate
|
|
221
|
+
* gives us exactly enough to do that: the action's `type` is `"xstate.enqueueActions"`, visible even
|
|
222
|
+
* though its contents are not.
|
|
223
|
+
*/
|
|
224
|
+
function hasOpaqueActions(node: StateNode<any, any>): boolean {
|
|
225
|
+
let opaque = false;
|
|
226
|
+
eachAction(node, (a) => {
|
|
227
|
+
if (a?.type === "xstate.enqueueActions") opaque = true;
|
|
228
|
+
});
|
|
229
|
+
return opaque;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Every actor name this state `spawnChild`s — from its entry actions or any of its transitions'.
|
|
233
|
+
* A spawned child is an ACTION, so it appears nowhere in the state tree; this is its only trace.
|
|
234
|
+
* Only a TOP-LEVEL `spawnChild` is visible; one issued inside an `enqueueActions` closure is not,
|
|
235
|
+
* which is what {@link hasOpaqueActions} exists to flag. */
|
|
236
|
+
function spawnedSrcs(node: StateNode<any, any>): string[] {
|
|
237
|
+
const srcs = new Set<string>();
|
|
238
|
+
eachAction(node, (a) => {
|
|
239
|
+
if (a?.type === "xstate.spawnChild" && typeof a.src === "string") srcs.add(a.src);
|
|
240
|
+
});
|
|
241
|
+
return [...srcs];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The child machines a state runs, invoked and spawned. */
|
|
245
|
+
function childMachines(node: StateNode<any, any>, scope: Scope): ChildMachineDoc[] {
|
|
246
|
+
const docs: ChildMachineDoc[] = [];
|
|
247
|
+
const attach = (src: string, label: string, via: ChildMachineDoc["via"], machine: AnyStateMachine) => {
|
|
248
|
+
// Recursion is legal (a machine that spawns itself); serializing it is not. Name it and stop.
|
|
249
|
+
if (scope.path.has(machine)) docs.push({ src, label, via, recursive: true });
|
|
250
|
+
// A fresh path per branch, not a shared visited-set: two SIBLINGS may run the same machine, and
|
|
251
|
+
// both should carry its body — only an ANCESTOR is a cycle.
|
|
252
|
+
else docs.push({ src, label, via, machine: serializeBody(machine, new Set(scope.path)) });
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
node.invoke.forEach((inv, i) => {
|
|
256
|
+
const machine = invokedMachine(node, i, scope);
|
|
257
|
+
if (machine) attach(srcName(inv.src), inv.id, "invoke", machine);
|
|
258
|
+
});
|
|
259
|
+
for (const src of spawnedSrcs(node)) {
|
|
260
|
+
const machine = asMachine(scope.actors[src]);
|
|
261
|
+
if (machine) attach(src, src, "spawn", machine);
|
|
262
|
+
}
|
|
263
|
+
return docs;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function serializeState(node: StateNode<any, any>, into: MachineTransitionDoc[], scope: Scope): MachineStateDoc {
|
|
267
|
+
// `node.always` entries are already in the transitions map under the eventless descriptor, so the
|
|
268
|
+
// map alone is the complete edge set; a reference-set dedupe guards against either representation.
|
|
269
|
+
const seen = new Set<TransitionDefinition<any, any>>();
|
|
270
|
+
for (const defs of node.transitions.values()) {
|
|
271
|
+
for (const t of defs) {
|
|
272
|
+
if (!seen.has(t)) {
|
|
273
|
+
seen.add(t);
|
|
274
|
+
into.push(serializeTransition(t));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const t of node.always ?? []) {
|
|
279
|
+
if (!seen.has(t)) {
|
|
280
|
+
seen.add(t);
|
|
281
|
+
into.push(serializeTransition(t));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const children = Object.values(node.states as Record<string, StateNode<any, any>>).sort((a, b) => a.order - b.order);
|
|
286
|
+
const doc: MachineStateDoc = {
|
|
287
|
+
id: node.id,
|
|
288
|
+
key: node.key,
|
|
289
|
+
type: node.type,
|
|
290
|
+
invoke: node.invoke.map((inv) => ({ id: inv.id, src: srcName(inv.src) })),
|
|
291
|
+
tags: [...node.tags],
|
|
292
|
+
states: children.map((child) => serializeState(child, into, scope)),
|
|
293
|
+
children: childMachines(node, scope),
|
|
294
|
+
};
|
|
295
|
+
if (node.type === "compound") doc.initial = node.initial.target[0]?.id;
|
|
296
|
+
if (node.description !== undefined) doc.description = node.description;
|
|
297
|
+
if (hasOpaqueActions(node)) doc.opaqueActions = true;
|
|
298
|
+
return doc;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Every state whose `children` may be incomplete, by id — what a caller warns about. Walks the
|
|
302
|
+
* whole doc, child machines included, since an opaque spawn hides just as well one level down. */
|
|
303
|
+
export function opaqueStates(doc: MachineDoc): string[] {
|
|
304
|
+
const ids: string[] = [];
|
|
305
|
+
const walk = (state: MachineStateDoc): void => {
|
|
306
|
+
if (state.opaqueActions) ids.push(state.id);
|
|
307
|
+
for (const child of state.children) if (child.machine) walk(child.machine.root);
|
|
308
|
+
for (const nested of state.states) walk(nested);
|
|
309
|
+
};
|
|
310
|
+
walk(doc.root);
|
|
311
|
+
return ids;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Serialize one Machine — its own state tree and its own transitions. Each level carries its own
|
|
315
|
+
* actor registry (a child machine resolves `src` names against ITS `setup`, not its parent's). */
|
|
316
|
+
function serializeBody(machine: AnyStateMachine, path: Set<AnyStateMachine>): MachineBodyDoc {
|
|
317
|
+
path.add(machine);
|
|
318
|
+
const transitions: MachineTransitionDoc[] = [];
|
|
319
|
+
const scope: Scope = { actors: machine.implementations.actors as Record<string, unknown>, path };
|
|
320
|
+
const root = serializeState(machine.root, transitions, scope);
|
|
321
|
+
const events = [...(vocabularyOf(machine)?.values() ?? [])].map((def) => ({
|
|
322
|
+
name: def.name,
|
|
323
|
+
...(def.description !== undefined ? { description: def.description } : {}),
|
|
324
|
+
audience: def.audience,
|
|
325
|
+
input: z.toJSONSchema(def.input),
|
|
326
|
+
}));
|
|
327
|
+
return { id: machine.id, root, transitions, events };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Serialize a workflow's template Machine into the Console's DTO, child machines and all. */
|
|
331
|
+
export function serializeMachine(workflow: string, machine: AnyStateMachine): MachineDoc {
|
|
332
|
+
const doc: MachineDoc = { workflow, ...serializeBody(machine, new Set()) };
|
|
333
|
+
return { ...doc, opaqueStates: opaqueStates(doc) };
|
|
334
|
+
}
|
package/src/names.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The fixed in-namespace object names of a deployed instance (ADR-0019). The namespace is the
|
|
2
|
+
// instance's IDENTITY, so names inside it are constants — shared by `jr2 up` (which creates them),
|
|
3
|
+
// the CLI transport (which dials them), and the server entrypoint (which consumes them).
|
|
4
|
+
|
|
5
|
+
/** The orchestrator's Deployment + Service name; the Service targets `ORCHESTRATOR_PORT`. */
|
|
6
|
+
export const ORCHESTRATOR_SERVICE = "jr2-orchestrator";
|
|
7
|
+
export const ORCHESTRATOR_PORT = 4000;
|
|
8
|
+
|
|
9
|
+
/** The instance-owned Secret: Instance token + signing key (+ orchestrator-side creds). */
|
|
10
|
+
export const INSTANCE_SECRET = "jr2-instance";
|
|
11
|
+
|
|
12
|
+
/** The Instance Harness's Deployment + Service name (ADR-0031): converged by `jr2 up` whenever any
|
|
13
|
+
* Agent definition declares `workspace: "none"`, and the deterministic Service DNS the Agent actor
|
|
14
|
+
* resolves such a Turn to. Doubles as the delivery scope a Menu-only registration records — the
|
|
15
|
+
* name the placement's Adapter token is signed for (tokens.ts, ADR-0013). The port is the
|
|
16
|
+
* Harness's own listen port (`PORT` default). */
|
|
17
|
+
export const INSTANCE_HARNESS_SERVICE = "jr2-instance-harness";
|
|
18
|
+
export const INSTANCE_HARNESS_PORT = 8080;
|
|
19
|
+
|
|
20
|
+
/** The harness config ConfigMap the stock Harness boots from (ADR-0018): what this instance can
|
|
21
|
+
* REACH — the custom model provider, minus its key. No Agents ride it: a Machine carries its own
|
|
22
|
+
* (ADR-0049), and the definition rides each admission, so this holds deployment facts alone
|
|
23
|
+
* (ADR-0050). */
|
|
24
|
+
export const HARNESS_CONFIGMAP = "jr2-harness";
|
|
25
|
+
/** Its one key, hence the env var's value — `JR2_HARNESS_JSON` on every Harness container. */
|
|
26
|
+
export const HARNESS_CONFIG_KEY = "harness.json";
|
|
27
|
+
|
|
28
|
+
/** The resolved image map `jr2 up` writes and every provision reads (ADR-0037/0038): key → ref for
|
|
29
|
+
* the Harness, the Adapter, and every Sandbox Image context the registered Machines carry — keyed
|
|
30
|
+
* by content digest, plus the reserved `default` (ADR-0049).
|
|
31
|
+
*
|
|
32
|
+
* MOUNTED, never projected into env — the load-bearing part. A mount updates in place through
|
|
33
|
+
* kubelet propagation, so adding a CLI to a Dockerfile costs one propagation window; the same map
|
|
34
|
+
* as Deployment env would be a pod-template change, rolling the Orchestrator and putting every
|
|
35
|
+
* live run through snapshot restore (ADR-0007) for a change that affects only FUTURE Sandboxes. */
|
|
36
|
+
export const IMAGES_CONFIGMAP = "jr2-images";
|
|
37
|
+
export const IMAGES_MOUNT = "/etc/jr2/images";
|
|
38
|
+
/** The ConfigMap key, hence the filename under the mount — the two-sided contract's other half. */
|
|
39
|
+
export const IMAGES_KEY = "images.json";
|
|
40
|
+
|
|
41
|
+
/** The HARNESS containers' env Secret — Agent creds only, never the Instance token (ADR-0013). */
|
|
42
|
+
export const HARNESS_ENV_SECRET = "jr2-harness-env";
|
|
43
|
+
|
|
44
|
+
/** The instance's private-CA bundle ConfigMap (`harness.caBundle`, ADR-0020) — mounted into the
|
|
45
|
+
* Harness container (and only it) so Agent egress trusts an internal CA. Public data by nature. */
|
|
46
|
+
export const CA_CONFIGMAP = "jr2-ca";
|
|
47
|
+
|
|
48
|
+
/** The scaffold's default deploy-key Secret name (ADR-0047/0051): what `jr2 init`'s wildcard
|
|
49
|
+
* `git.credentials` entry names as its `sshKey`, and what `jr2 up`'s ssh offer generates into when
|
|
50
|
+
* an entry names it. Only a default — an entry may name any Secret. */
|
|
51
|
+
export const GIT_SSH_SECRET = "jr2-git-ssh";
|
|
52
|
+
|
|
53
|
+
/** The snapshot store's PVC (sqlite lives on it — ADR-0019). */
|
|
54
|
+
export const STATE_PVC = "jr2-state";
|
|
55
|
+
|
|
56
|
+
/** The cache agent (ADR-0051): the per-Instance DaemonSet that clones each Repo onto its node and
|
|
57
|
+
* fetches it in place, plus its ServiceAccount/Role/RoleBinding, all by this name. */
|
|
58
|
+
export const REPO_CACHE = "jr2-repo-cache";
|
|
59
|
+
/** The node directory the cache agent owns — `<REPO_CACHE_HOSTPATH>/<namespace>/repos/<key>` is one
|
|
60
|
+
* Repo's bare clone on one node, mounted read-only into every Sandbox there that names it. */
|
|
61
|
+
export const REPO_CACHE_HOSTPATH = "/var/lib/jr2";
|
|
62
|
+
/** Where a Sandbox sees the node's caches: `/repos/<key>`, one mount per Repo the CR names. */
|
|
63
|
+
export const REPOS_MOUNT = "/repos";
|
|
64
|
+
|
|
65
|
+
/** Metadata the Orchestrator writes on a `Repo` resource (ADR-0051). `bound` is set when a
|
|
66
|
+
* registered Machine binds the Repo, so `jr2 gc` never evicts it; the identity is what every
|
|
67
|
+
* spelling of the url normalizes to (repo-identity.ts); `last-attached` is the eviction clock for
|
|
68
|
+
* a Repo nothing binds. */
|
|
69
|
+
export const LABEL_REPO_BOUND = "jr2.dev/bound";
|
|
70
|
+
export const ANNOTATION_REPO_IDENTITY = "jr2.dev/identity";
|
|
71
|
+
export const ANNOTATION_REPO_LAST_ATTACHED = "jr2.dev/last-attached";
|
|
72
|
+
|
|
73
|
+
/** The mark one ask leaves on a Sandbox CR, per Repo key (ADR-0053): a timestamp the Orchestrator
|
|
74
|
+
* writes when something inside the pod asks for a fetch. The operator copies it onto the pod, and
|
|
75
|
+
* the node's cache agent takes `asked` as the later of the pod's creation and this — so a fetch
|
|
76
|
+
* that started before the ask does not satisfy it. The Lease's shape: an annotation, written by
|
|
77
|
+
* the Orchestrator, read by the operator. */
|
|
78
|
+
export const askedAnnotation = (key: string): string => `jr2.dev/asked-${key}`;
|
package/src/open.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The OPEN sentinel (ADR-0051, widened by ADR-0054) — one value, its own module.
|
|
2
|
+
//
|
|
3
|
+
// It lives alone here because both of the parts it marks must read it and they sit on opposite
|
|
4
|
+
// sides of the package: `agent()`'s declaration (agent.ts, a pure leaf that pulls no wire client)
|
|
5
|
+
// and a `workspace()`'s Repo Slots (parts.ts, which already imports agent.ts). A module with
|
|
6
|
+
// nothing but the sentinel in it is what lets both import it and neither import the other.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Open (CONTEXT.md): this part is a composer's to bind, with `customize()` — a Repo Slot with no
|
|
10
|
+
* url, an Agent with no model. The shape a packaged Machine ships in, because a package cannot
|
|
11
|
+
* know the repository or pay for the model. `jr2 up` refuses an Open part nobody bound and names
|
|
12
|
+
* the line that binds it; a run never sees one.
|
|
13
|
+
*
|
|
14
|
+
* `Symbol.for`, so an Instance's own copy of this module and the CLI's walk agree on it — a
|
|
15
|
+
* packaged Machine may be built against one and walked by the other.
|
|
16
|
+
*/
|
|
17
|
+
export const open: unique symbol = Symbol.for("jr2.open");
|