@jr2/orchestrator 0.2.1 → 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.
Files changed (75) hide show
  1. package/bin/server.ts +7 -4
  2. package/console/canvas.ts +27 -14
  3. package/console/components/app.ts +10 -1
  4. package/console/components/fleet.ts +39 -6
  5. package/console/components/machine-pane.ts +89 -2
  6. package/console/components/nav.ts +1 -1
  7. package/console/components/schema-form.ts +12 -4
  8. package/console/json-field.ts +23 -0
  9. package/console/main.ts +110 -39
  10. package/console/store.ts +221 -17
  11. package/console/style.css +90 -0
  12. package/package.json +4 -5
  13. package/src/actor.ts +507 -54
  14. package/src/agent.ts +2 -2
  15. package/src/ambient.ts +2 -0
  16. package/src/assembly-checks.ts +165 -0
  17. package/src/assembly-tar.ts +494 -0
  18. package/src/assembly.ts +721 -0
  19. package/src/chart.ts +2050 -0
  20. package/src/charts.ts +720 -0
  21. package/src/config.ts +233 -22
  22. package/src/content-hash.ts +105 -0
  23. package/src/custodian.lua +313 -0
  24. package/src/custodian.ts +951 -0
  25. package/src/customize.ts +216 -25
  26. package/src/effect-checks.ts +557 -0
  27. package/src/effect.ts +905 -0
  28. package/src/effector-wire.ts +525 -0
  29. package/src/fingerprint.ts +4 -3
  30. package/src/gate.ts +8 -0
  31. package/src/git-clone.ts +65 -0
  32. package/src/git-push.ts +336 -0
  33. package/src/harness-client.ts +247 -25
  34. package/src/held-secrets.ts +643 -0
  35. package/src/holds.ts +156 -0
  36. package/src/http-effect.ts +981 -0
  37. package/src/http.ts +514 -119
  38. package/src/images.ts +58 -31
  39. package/src/index.ts +49 -4
  40. package/src/instance.ts +445 -59
  41. package/src/kube-client.ts +333 -0
  42. package/src/machine-doc.ts +88 -6
  43. package/src/model-keys.ts +92 -0
  44. package/src/mount-digest.ts +53 -0
  45. package/src/names.ts +89 -7
  46. package/src/notices.ts +88 -0
  47. package/src/open-body.ts +1444 -0
  48. package/src/open-effect.ts +144 -0
  49. package/src/open.ts +4 -4
  50. package/src/parts.ts +530 -33
  51. package/src/party.ts +468 -0
  52. package/src/pick.ts +309 -0
  53. package/src/pool.ts +1 -1
  54. package/src/registration.ts +301 -17
  55. package/src/repo-fetch.ts +61 -86
  56. package/src/repos.ts +63 -74
  57. package/src/rest.ts +133 -0
  58. package/src/run-host.ts +1841 -105
  59. package/src/sandbox-effect.ts +891 -0
  60. package/src/sandbox-effector.ts +111 -0
  61. package/src/sandbox-kube.ts +1719 -0
  62. package/src/sandbox-watch.ts +260 -0
  63. package/src/secret-placement.ts +494 -0
  64. package/src/server.ts +285 -89
  65. package/src/setup.ts +589 -70
  66. package/src/size.ts +342 -0
  67. package/src/snapshot-store.ts +64 -13
  68. package/src/stub-harness.ts +5 -0
  69. package/src/sync-client.ts +54 -0
  70. package/src/timers.ts +178 -0
  71. package/src/tokens.ts +44 -12
  72. package/src/vocabulary.ts +66 -1
  73. package/src/wire.ts +156 -2
  74. package/src/workspace.ts +529 -194
  75. package/src/sandbox-kubectl.ts +0 -1144
package/src/images.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  // The resolved key→ref image map, from the READ side (ADR-0037/0038/0049). `jr2 up` builds every
2
2
  // image it deploys and writes this map into the `jr2-images` ConfigMap; the Sandbox port consults it
3
3
  // when it creates a pod. This module is the shape both sides agree on — deliberately its own file,
4
- // not folded into sandbox-kubectl.ts, because the CLI needs the type without dragging in the
4
+ // not folded into sandbox-kube.ts, because the CLI needs the type without dragging in the
5
5
  // kubectl port.
6
6
  //
7
- // The map is NESTED, never flat. The kit's own `harness`/`adapter` refs sit beside a `sandbox`
7
+ // The map is NESTED, never flat. The kit's own `harness` and the `custodian` sit beside a `sandbox`
8
8
  // sub-map, so a user image can never shadow them — and `default` is the one reserved key in that
9
9
  // sub-map, because ADR-0037's fallback chain is built on it.
10
10
  //
11
11
  // A user image's key is its build context's CONTENT DIGEST (ADR-0049). That is what lets the two
12
12
  // sides agree without a path table: a Machine names its context as a `file:` URL, whose absolute
13
- // path differs between the host at `jr2 up` and the baked Orchestrator, while the FOLDER is the same
14
- // folder — the instance's `node_modules` holds the very tree the converge hashed. Digest in, digest
13
+ // path differs between the host at `jr2 up` and the Orchestrator Kit image, while the FOLDER is the
14
+ // same folder — the published Assembly's `node_modules` holds the very tree the converge hashed. Digest in, digest
15
15
  // out, no dirname registry in between.
16
16
  //
17
17
  // There is NO cache and NO memo for the map itself. The whole reason it arrives as a mounted
@@ -24,10 +24,12 @@ import { createHash } from "node:crypto";
24
24
  import { readdir, readFile } from "node:fs/promises";
25
25
  import { join, relative } from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
+ import { byteOrder } from "./content-hash.ts";
27
28
 
28
29
  /**
29
- * Every image ref one converge resolved. `harness`/`adapter` are the kit's own (built from a kit
30
- * checkout, or the published `<kitversion>` tags); `sandbox` is the instance's own docker-context
30
+ * Every image ref one converge resolved. `harness` is the kit's own (built from a kit checkout, or
31
+ * the published `<kitversion>` tag); `custodian` is the pinned Envoy the kit deploys and never
32
+ * builds (ADR-0038, ADR-0059); `sandbox` is the instance's own docker-context
31
33
  * builds — one `docker build` straight to a content tag, carrying no jr2 layers at all, because the
32
34
  * Harness arrives at POD time on the `/opt/jr2` volume (ADR-0037).
33
35
  *
@@ -38,9 +40,10 @@ import { fileURLToPath } from "node:url";
38
40
  export type ImageRefs = {
39
41
  /** The stock Harness image — also the last leg of the Sandbox Image fallback chain. */
40
42
  harness: string;
41
- /** The Adapter image (ADR-0013). An Agent with no Adapter cannot drive its Machine at all, so
43
+ /** The Custodian's image (ADR-0059) — upstream Envoy, pinned by digest, re-homed onto a
44
+ * `kitRegistry` when one is set. Every Harness pod runs one (it carries the Menu, ADR-0013), so
42
45
  * this is required: a map without it fails the provision rather than shipping a mute pod. */
43
- adapter: string;
46
+ custodian: string;
44
47
  /** Built Sandbox Images by their build context's CONTENT DIGEST (ADR-0049), plus the reserved
45
48
  * key `default` — the Instance's `images/default`, ADR-0037's middle leg. Empty when the
46
49
  * instance ships no context and scaffolded no default. */
@@ -99,10 +102,9 @@ export async function readImageRefs(path: string): Promise<ImageRefs> {
99
102
  return value as string;
100
103
  };
101
104
  const harness = ref("harness");
102
- // Required, not optional: with the ref living in the map there is no "no adapter configured"
103
- // branch left to gate on, and an Agent whose pod has no Adapter parks forever on a tool call it
104
- // cannot make (ADR-0013). Failing the provision is the only honest outcome.
105
- const adapter = ref("adapter");
105
+ // Required, not optional: an Agent whose pod has no Custodian parks forever on a Menu it cannot
106
+ // read (ADR-0013). Failing the provision is the only honest outcome.
107
+ const custodian = ref("custodian");
106
108
  const sandboxRaw = map["sandbox"];
107
109
  if (sandboxRaw !== undefined && (typeof sandboxRaw !== "object" || sandboxRaw === null || Array.isArray(sandboxRaw)))
108
110
  bad("`sandbox` must be an object of name → ref");
@@ -121,7 +123,7 @@ export async function readImageRefs(path: string): Promise<ImageRefs> {
121
123
  if (typeof value !== "string") bad(`\`sandboxUser.${name}\` is not a USER (got ${JSON.stringify(value)})`);
122
124
  sandboxUser[name] = value as string;
123
125
  }
124
- return { harness, adapter, sandbox, sandboxUser };
126
+ return { harness, custodian, sandbox, sandboxUser };
125
127
  }
126
128
 
127
129
  /**
@@ -142,7 +144,8 @@ export function isImageContext(image: string): boolean {
142
144
  * nothing else (ADR-0037/0038).
143
145
  *
144
146
  * It lives HERE, not in the CLI's build module, because BOTH sides compute it: `jr2 up` keys the
145
- * map it publishes by this digest, and the baked Orchestrator computes it again at provision to
147
+ * map it publishes by this digest, and the Orchestrator computes it again at provision, over the
148
+ * published Assembly, to
146
149
  * look the ref up. One implementation is what makes "the host and the pod agree without a path
147
150
  * table" true rather than hopeful (ADR-0049).
148
151
  *
@@ -155,12 +158,11 @@ export function isImageContext(image: string): boolean {
155
158
  export async function imageContextDigest(dir: string): Promise<string> {
156
159
  const h = createHash("sha256");
157
160
  // A domain separator, and the whole of it: a Sandbox Image's build is `docker build` of the
158
- // user's own directory with no generated text anywhere in it, so — unlike the instance image,
159
- // whose generated Dockerfile is image content the context never holds — there is nothing to salt
160
- // WITH. The constant only keeps this hash's domain apart from the bundle's.
161
+ // user's own directory with no generated text anywhere in it, so there is nothing to salt WITH.
162
+ // The constant only keeps this hash's domain apart from the Assembly's.
161
163
  h.update("salt:sandbox\n");
162
164
  const walk = async (root: string, d: string): Promise<void> => {
163
- const entries = (await readdir(d, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
165
+ const entries = (await readdir(d, { withFileTypes: true })).sort((a, b) => byteOrder(a.name, b.name));
164
166
  for (const e of entries) {
165
167
  const p = join(d, e.name);
166
168
  if (e.isDirectory()) await walk(root, p);
@@ -181,7 +183,7 @@ export async function imageContextDigest(dir: string): Promise<string> {
181
183
  * own key and is never looked up. Absent is the reserved `default` key — ADR-0037's middle leg.
182
184
  *
183
185
  * A context whose folder is unreadable fails HERE, before a Secret or a CR exists, naming the path:
184
- * in a deployed Orchestrator that means the module shipped a context the instance bundle does not
186
+ * in a deployed Orchestrator that means the module shipped a context the current Assembly does not
185
187
  * hold, which is a packaging bug and not a cluster one.
186
188
  */
187
189
  async function imageKey(image: string): Promise<string> {
@@ -193,7 +195,7 @@ async function imageKey(image: string): Promise<string> {
193
195
  throw new Error(
194
196
  `the Sandbox Image context ${image} cannot be read (${(err as NodeJS.ErrnoException).code ?? "read failed"}) — ` +
195
197
  "a `file:` image names a docker context the Machine's own module ships (ADR-0037), so it must travel " +
196
- `with the module: check that ${dir} exists in the instance's node_modules.`,
198
+ `with the module: check that ${dir} exists in the current Assembly.`,
197
199
  );
198
200
  }
199
201
  }
@@ -226,13 +228,14 @@ export type ResolvedImage = {
226
228
  * cannot prove is non-root because it does not read the image's `/etc/passwd`, and uid 0
227
229
  * (`USER root`, `USER 0`), which plainly is root.
228
230
  *
229
- * Both surface as CreateContainerConfigError on the FIRST init step that runs the image — the
231
+ * Both surface as CreateContainerConfigError on the init step that runs the image — the
230
232
  * preflight — and that container never starts, so `kubectl logs -c preflight` prints nothing. The
231
233
  * record is the one place that can see it coming: `docker inspect` at converge already put the
232
234
  * string in the map, so the provision can fail BEFORE it applies anything. Only a BUILT image is
233
- * knowable here; a registry ref was never inspected, so it answers undefined and that pod is
234
- * caught later, from the cluster (`rootImageFault` in sandbox-kubectl.ts) — same fault, same fix,
235
- * one round trip more expensive.
235
+ * knowable here; a registry ref was never inspected, so it answers undefined and that pod runs
236
+ * out the provision's Ready budget, whose message names the numeric non-root `USER` among the
237
+ * floor's causes (sandbox-kube.ts). The Orchestrator reads no Pod (ADR-0063), so the kubelet's
238
+ * waiting reason is not visible to it.
236
239
  */
237
240
  refusedUser?: string;
238
241
  };
@@ -248,6 +251,14 @@ function judgeUser(recorded: string | undefined): Pick<ResolvedImage, "fallbackS
248
251
  return { fallbackSeat: false };
249
252
  }
250
253
 
254
+ /**
255
+ * The run a provision resolves images for, when it holds an Assembly that is not the current one
256
+ * (ADR-0069). The image map holds the current walk's contexts only, so a held run whose context the
257
+ * current Assembly dropped cannot provision, and `jr2 up` cannot fix it: the error must say which
258
+ * case it is.
259
+ */
260
+ export type HeldImageScope = { runId: string; assembly: string; current: string | undefined };
261
+
251
262
  /**
252
263
  * Look one built key up in the map this converge wrote — for the Sandbox Image and the User
253
264
  * Container alike, because ADR-0005 gives the User Container "the same resolution" deliberately.
@@ -255,15 +266,27 @@ function judgeUser(recorded: string | undefined): Pick<ResolvedImage, "fallbackS
255
266
  * An unknown key throws: a converge-time check is impossible for a ref and unnecessary for a
256
267
  * context (the walk built every one it found), so this fires when the Orchestrator's own
257
268
  * `node_modules` holds a context the LAST converge did not build — a stale deployment. The error
258
- * says exactly that, because "re-run `jr2 up`" is the whole fix.
269
+ * says exactly that, because "re-run `jr2 up`" is the fix. It also fires for a run under a held
270
+ * Assembly (ADR-0069) whose context the current walk no longer carries, which `jr2 up` cannot fix.
271
+ * With `held`, the error names that case alone: the run, the held Assembly and the current one.
259
272
  */
260
- function builtRef(refs: ImageRefs, key: string, image: string, what: string): string {
273
+ function builtRef(refs: ImageRefs, key: string, image: string, what: string, held?: HeldImageScope): string {
261
274
  const ref = refs.sandbox[key];
262
275
  if (ref === undefined) {
276
+ if (held) {
277
+ throw new Error(
278
+ `no ${what} for ${image} (context digest ${key}) — run ${held.runId} is under the held Assembly ` +
279
+ `${held.assembly}, whose context ${key} the current Assembly ${held.current ?? "(none)"} does not ` +
280
+ "carry. The image map holds the current Assembly's contexts only, so `jr2 up` does not fix this: " +
281
+ "publish an Assembly that names the context again, or cancel the run (ADR-0069).",
282
+ );
283
+ }
263
284
  throw new Error(
264
285
  `no ${what} for ${image} (context digest ${key}) — this instance's last converge built ` +
265
286
  `${Object.keys(refs.sandbox).length} image(s), none of them this one. Re-run \`jr2 up\`: it walks the ` +
266
- "registered Machines and builds every `file:` context they carry (ADR-0037/0049).",
287
+ "registered Machines and builds every `file:` context they carry (ADR-0037/0049). A run under a held " +
288
+ "Assembly (ADR-0069) whose context the current Assembly no longer carries is the one case it does not " +
289
+ "fix: the map holds the current walk's contexts only.",
267
290
  );
268
291
  }
269
292
  return ref;
@@ -279,7 +302,11 @@ function builtRef(refs: ImageRefs, key: string, image: string, what: string): st
279
302
  * look up and nothing to say about its tag discipline — the cluster pulls it, and nothing was
280
303
  * inspected, so no seat fact is known.
281
304
  */
282
- export async function resolveSandboxImage(refs: ImageRefs, image?: string): Promise<ResolvedImage> {
305
+ export async function resolveSandboxImage(
306
+ refs: ImageRefs,
307
+ image?: string,
308
+ held?: HeldImageScope,
309
+ ): Promise<ResolvedImage> {
283
310
  if (image === undefined) {
284
311
  const ref = refs.sandbox["default"];
285
312
  if (ref === undefined) return { ref: refs.harness, fallbackSeat: false };
@@ -287,7 +314,7 @@ export async function resolveSandboxImage(refs: ImageRefs, image?: string): Prom
287
314
  }
288
315
  if (!isImageContext(image)) return { ref: image, fallbackSeat: false };
289
316
  const key = await imageKey(image);
290
- return { ref: builtRef(refs, key, image, "Sandbox Image"), ...judgeUser(refs.sandboxUser?.[key]) };
317
+ return { ref: builtRef(refs, key, image, "Sandbox Image", held), ...judgeUser(refs.sandboxUser?.[key]) };
291
318
  }
292
319
 
293
320
  /**
@@ -297,7 +324,7 @@ export async function resolveSandboxImage(refs: ImageRefs, image?: string): Prom
297
324
  * the one place ADR-0005 promises none. Its `USER` is not judged either: the seat carries no
298
325
  * `securityContext` at all, so root is allowed there.
299
326
  */
300
- export async function resolveUserImage(refs: ImageRefs, image: string): Promise<string> {
327
+ export async function resolveUserImage(refs: ImageRefs, image: string, held?: HeldImageScope): Promise<string> {
301
328
  if (!isImageContext(image)) return image;
302
- return builtRef(refs, await imageKey(image), image, "User Container image");
329
+ return builtRef(refs, await imageKey(image), image, "User Container image", held);
303
330
  }
package/src/index.ts CHANGED
@@ -2,20 +2,42 @@
2
2
  // delivery surfaces, the duplex Actor over the Harness wire, and durable snapshot persistence
3
3
  // (ADR-0002/0006/0007). Builds on the `@jr2/agent-protocol` wire contract.
4
4
  //
5
- // It does NOT speak MCP (ADR-0013): the Agent's MCP surface is hosted by the Adapter, in the
6
- // Sandbox. What lives here is the registration table and two thin HTTP adapters over it.
5
+ // It does NOT speak MCP (ADR-0013): the Harness presents the Menu to its model itself, and reaches
6
+ // the surface here through the Custodian in its pod. What lives here is the registration table and
7
+ // two thin HTTP adapters over it — plus what composes a Custodian (ADR-0059).
7
8
 
8
9
  // The wire contract, re-exported: a workflow authors against ONE package (`defineEvent`,
9
10
  // `jr2Setup`, `workspace`, … all import from "@jr2/orchestrator" — ADR-0015).
10
11
  export * from "@jr2/agent-protocol";
11
12
  export * from "./agent.ts";
13
+ // The Effect surface (ADR-0067): `command` itself is the wire-bound one, from harness-client.ts;
14
+ // `fromPick`, `paramsOf` and `answer` are what an author writes between a pick and its work.
15
+ export * from "./effect.ts";
16
+ // The http kind (ADR-0068): `http`, `secret`, `bearer`, `basic`, `text`, and the Effector's wire.
17
+ export * from "./http-effect.ts";
18
+ export * from "./sandbox-effect.ts";
19
+ // An Effect's Open parts (ADR-0054): the types `customize({ effects })` reads.
20
+ export type { EffectBinding, OpenEffect, OpenEffectPart } from "./open-effect.ts";
21
+ export { openPartsText } from "./open-effect.ts";
22
+ export * from "./effector-wire.ts";
23
+ export * from "./pick.ts";
24
+ // The Chart (ADR-0070): its grammar (`chartSchema`, which a Chart wrapper's door and a planner's pick
25
+ // import), the door that admits one, and the resolver of its references.
26
+ export * from "./chart.ts";
27
+ // The Open body a Chart binds (`openBody`, the slot constructor a Chart wrapper places in `actors`)
28
+ // and the named Charts of an Instance: Chart files and runtime Charts (ADR-0070).
29
+ export * from "./open-body.ts";
30
+ export * from "./charts.ts";
12
31
  export * from "./parts.ts";
32
+ export * from "./size.ts";
13
33
  export * from "./customize.ts";
14
34
  export * from "./ambient.ts";
15
35
  export * from "./config.ts";
16
36
  export * from "./repo-identity.ts";
17
37
  export * from "./pool.ts";
18
38
  export * from "./setup.ts";
39
+ // The author's routing guard (ADR-0015).
40
+ export { sentBy } from "./party.ts";
19
41
  export * from "./vocabulary.ts";
20
42
  export * from "./tokens.ts";
21
43
  export * from "./durability.ts";
@@ -24,12 +46,35 @@ export * from "./actor.ts";
24
46
  export * from "./gate.ts";
25
47
  export * from "./workspace.ts";
26
48
  export * from "./images.ts";
27
- export * from "./sandbox-kubectl.ts";
49
+ // The Assembly's content address and wire form (ADR-0069): `jr2 up` and the door share both.
50
+ export * from "./content-hash.ts";
51
+ export * from "./assembly-tar.ts";
52
+ // The current Assembly and the publish door (ADR-0069), and the walk's refusal listing that
53
+ // `jr2 up` and the door both run.
54
+ export * from "./assembly.ts";
55
+ export * from "./assembly-checks.ts";
56
+ export * from "./mount-digest.ts";
57
+ export * from "./model-keys.ts";
58
+ export * from "./held-secrets.ts";
59
+ export * from "./secret-placement.ts";
60
+ export * from "./git-push.ts";
61
+ export * from "./git-clone.ts";
62
+ export * from "./custodian.ts";
63
+ export * from "./sandbox-kube.ts";
64
+ export {
65
+ effectorManifestKey,
66
+ LABEL_EFFECTOR_POD,
67
+ readSandboxEffectorManifest,
68
+ sandboxEffectorKeySecret,
69
+ sandboxEffectorObjectsOf,
70
+ type ProjectedSecret,
71
+ type SandboxEffectorManifest,
72
+ } from "./sandbox-effector.ts";
28
73
  export * from "./repos.ts";
29
74
  export * from "./repo-fetch.ts";
30
75
  // Type-only: what a workflow event delivery looks like (the registration TABLE stays internal —
31
76
  // ADR-0011: workflows speak only defineEvent/agent/gate).
32
- export type { DeliveredEvent } from "./registration.ts";
77
+ export type { ChildErrorMarker, DeliveredEvent, PlacingMarker, RunMarker, TurnMarker } from "./registration.ts";
33
78
  export * from "./machine-doc.ts";
34
79
  export * from "./run-host.ts";
35
80
  export * from "./http.ts";