@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/names.ts CHANGED
@@ -9,16 +9,29 @@ export const ORCHESTRATOR_PORT = 4000;
9
9
  /** The instance-owned Secret: Instance token + signing key (+ orchestrator-side creds). */
10
10
  export const INSTANCE_SECRET = "jr2-instance";
11
11
 
12
- /** The Instance Harness's Deployment + Service name (ADR-0031): converged by `jr2 up` whenever any
12
+ /** The Instance Harness's StatefulSet + Service name (ADR-0031): converged by `jr2 up` whenever any
13
13
  * Agent definition declares `workspace: "none"`, and the deterministic Service DNS the Agent actor
14
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
15
+ * name the placement's Sandbox token is signed for (tokens.ts, ADR-0013), which its Custodian holds. The port is the
16
16
  * Harness's own listen port (`PORT` default). */
17
17
  export const INSTANCE_HARNESS_SERVICE = "jr2-instance-harness";
18
18
  export const INSTANCE_HARNESS_PORT = 8080;
19
19
 
20
+ /** The Instance Effector (ADR-0068): its Deployment, headless Service, NetworkPolicy and Secret all
21
+ * carry this name. `jr2 up` converges them when a registered Machine places an Effect there. The
22
+ * port is the Effector's own (`JR2_EFFECTOR_PORT` default) — none of the Harness pod's ports. */
23
+ export const INSTANCE_EFFECTOR = "jr2-instance-effector";
24
+ export const EFFECTOR_PORT = 8082;
25
+
26
+ /** Where a Harness persists its conversations, and the env var that tells it so (ADR-0031): one
27
+ * fixed path in both placements — the Instance Harness's PersistentVolumeClaim (deploy.ts) and a
28
+ * Sandbox's emptyDir (sandbox-kube.ts), each mounted into the Harness container alone. */
29
+ export const CONVERSATIONS_DIR = "/conversations";
30
+ export const CONVERSATIONS_DIR_ENV = "JR2_CONVERSATIONS_DIR";
31
+ export const CONVERSATIONS_VOLUME = "conversations";
32
+
20
33
  /** 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
34
+ * REACH — the custom model provider minus its key, and the catalog providers' gateways (ADR-0059). No Agents ride it: a Machine carries its own
22
35
  * (ADR-0049), and the definition rides each admission, so this holds deployment facts alone
23
36
  * (ADR-0050). */
24
37
  export const HARNESS_CONFIGMAP = "jr2-harness";
@@ -26,7 +39,7 @@ export const HARNESS_CONFIGMAP = "jr2-harness";
26
39
  export const HARNESS_CONFIG_KEY = "harness.json";
27
40
 
28
41
  /** 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
42
+ * the Harness, the Custodian, and every Sandbox Image context the registered Machines carry — keyed
30
43
  * by content digest, plus the reserved `default` (ADR-0049).
31
44
  *
32
45
  * MOUNTED, never projected into env — the load-bearing part. A mount updates in place through
@@ -38,13 +51,60 @@ export const IMAGES_MOUNT = "/etc/jr2/images";
38
51
  /** The ConfigMap key, hence the filename under the mount — the two-sided contract's other half. */
39
52
  export const IMAGES_KEY = "images.json";
40
53
 
41
- /** The HARNESS containers' env Secret — Agent creds only, never the Instance token (ADR-0013). */
54
+ /** The HARNESS containers' env Secret — the literal `harness.env` values, never the Instance token
55
+ * (ADR-0013) and never a model key (ADR-0059: those are held). */
42
56
  export const HARNESS_ENV_SECRET = "jr2-harness-env";
43
57
 
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. */
58
+ /** The instance's trust ConfigMap (ADR-0020, ADR-0059): the user's `harness.caBundle` as `ca.crt`
59
+ * and, while a held secret exists, the three bundles the held-secret path needs — `extra.crt` and
60
+ * `bundle.crt` for the Harness, `upstream.crt` for the Custodian. Public data by nature. */
46
61
  export const CA_CONFIGMAP = "jr2-ca";
47
62
 
63
+ /** Held secrets (ADR-0059). The per-Instance CA — its key included — that `jr2 up` alone reads over
64
+ * the kube API, and that NO pod mounts: every leaf is issued before a pod starts. */
65
+ export const HELD_CA_SECRET = "jr2-held-ca";
66
+ /** The prefix of each pod's literal held values (`heldSecretsSecret`): one Secret per pod, one key
67
+ * per name it holds — mounted into that pod's Custodian and nothing else. */
68
+ export const HELD_SECRETS_SECRET = "jr2-held-secrets";
69
+ /** The literal held values of one pod (ADR-0059): `jr2-held-secrets-<pod>`. */
70
+ export function heldSecretsSecret(pod: string): string {
71
+ return `${HELD_SECRETS_SECRET}-${pod}`;
72
+ }
73
+ /** One leaf per bound `host:port` (`<stem>.crt`, `<stem>.key`) — the Custodians' alone. */
74
+ export const HELD_TLS_SECRET = "jr2-held-tls";
75
+ /** What `jr2 up` DECLARES about held secrets: each pod's held manifest (`heldManifestKey` — names,
76
+ * bindings, headers; never a value) and the Custodian's program once (`CUSTODIAN_PROGRAM_KEY`).
77
+ * Nothing rendered per pod lives here: a shared object stops at 1 MiB, and Envoy's files are a
78
+ * pod's own, rendered where the pod is made (`sandboxCustodianConfigMap`,
79
+ * `INSTANCE_CUSTODIAN_CONFIGMAP`). Always applied, possibly holding no secret: every Harness pod
80
+ * runs a Custodian. */
81
+ export const HELD_CONFIGMAP = "jr2-held";
82
+ /** Where the Orchestrator reads a Workspace's held manifest, per provision — a mount, so a
83
+ * held-secret edit reaches future Sandboxes without rolling the Orchestrator (the `jr2-images`
84
+ * pattern, ADR-0038). */
85
+ export const HELD_MOUNT = "/etc/jr2/held";
86
+ /** The `jr2-held` key of one pod's manifest. */
87
+ export const heldManifestKey = (pod: string): string => `held-${pod}.json`;
88
+ /** The `jr2-held` key of the Custodian's fixed Lua — `custodian.lua` as written, once per Instance;
89
+ * each pod's own script loads it by this name from its config mount. */
90
+ export const CUSTODIAN_PROGRAM_KEY = "program.lua";
91
+ /** The ConfigMap with the Instance Harness's Custodian's two rendered files (`envoy.json`,
92
+ * `custodian.lua`), written by `jr2 up`: the one pod it makes. */
93
+ export const INSTANCE_CUSTODIAN_CONFIGMAP = "jr2-held-custodian";
94
+ /** The ConfigMap with one Sandbox's Custodian's two rendered files, written by the Orchestrator at
95
+ * provision and owned by the Sandbox CR — the `<name>-token` family: it goes with the pod. */
96
+ export const sandboxCustodianConfigMap = (sandbox: string): string => `${sandbox}-custodian`;
97
+ /** The Instance Harness's pod, as its held objects are keyed. A Workspace's is `ws-<digest>`
98
+ * (`workspaceHeldPod`), so the two cannot collide. */
99
+ export const INSTANCE_HARNESS_HELD_POD = "instance";
100
+
101
+ /** The Custodian's listeners (ADR-0059), all taken on every Harness pod (ADR-0005): the Harness's
102
+ * HTTPS proxy on loopback, the kubelet's probe, and the control listener the Menu and the ask ride
103
+ * to the Orchestrator, at the address `jr2-upload-pack` defaults to (ADR-0053). */
104
+ export const CUSTODIAN_PORT = 15001;
105
+ export const CUSTODIAN_HEALTH_PORT = 15021;
106
+ export const CUSTODIAN_CONTROL_PORT = 8081;
107
+
48
108
  /** The scaffold's default deploy-key Secret name (ADR-0047/0051): what `jr2 init`'s wildcard
49
109
  * `git.credentials` entry names as its `sshKey`, and what `jr2 up`'s ssh offer generates into when
50
110
  * an entry names it. Only a default — an entry may name any Secret. */
@@ -76,3 +136,25 @@ export const ANNOTATION_REPO_LAST_ATTACHED = "jr2.dev/last-attached";
76
136
  * that started before the ask does not satisfy it. The Lease's shape: an annotation, written by
77
137
  * the Orchestrator, read by the operator. */
78
138
  export const askedAnnotation = (key: string): string => `jr2.dev/asked-${key}`;
139
+
140
+ /** The two PriorityClasses `jr2 up` creates, cluster-scoped, beside the CRDs (ADR-0060) — unless
141
+ * `priorityClasses` in jr2.config.ts names existing ones. `jr2-control` (100000,
142
+ * PreemptLowerPriority) is for the Orchestrator, the operator and the Repo cache agent: small, fixed,
143
+ * and needed by every Sandbox. `jr2-sandbox` (1000, preemptionPolicy Never) is for Sandboxes and the
144
+ * Instance Harness: an ordinary pod cannot preempt a live Workspace, and a waiting Sandbox evicts
145
+ * nobody. Both stay far below the system classes. */
146
+ export const PRIORITY_CLASS_CONTROL = "jr2-control";
147
+ export const PRIORITY_CLASS_SANDBOX = "jr2-sandbox";
148
+
149
+ /** The voluntary-disruption opt-outs (ADR-0060) a Sandbox pod and the Instance Harness pod carry:
150
+ * moving either loses work (`/work`; live conversations). No PodDisruptionBudget — `maxUnavailable:
151
+ * 0` blocks node upgrades without end. */
152
+ export const NO_DISRUPT_ANNOTATIONS = {
153
+ "cluster-autoscaler.kubernetes.io/safe-to-evict": "false",
154
+ "karpenter.sh/do-not-disrupt": "true",
155
+ } as const;
156
+
157
+ /** The API server's CA as the kubelet mounts it with the Pod's ServiceAccount. The Orchestrator
158
+ * Deployment points `NODE_EXTRA_CA_CERTS` at it, so the Orchestrator's own Kubernetes client
159
+ * (kube-client.ts, ADR-0063) trusts the API server with built-in `fetch` and no TLS code. */
160
+ export const SERVICE_ACCOUNT_CA = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt";
package/src/notices.ts ADDED
@@ -0,0 +1,88 @@
1
+ // The Orchestrator's notices (ADR-0062): what happened that the next Turn cannot find out for
2
+ // itself — a memory kill in its Workspace, a conversation that is new — kept as plain data in the
3
+ // run's persisted state (ADR-0007), beside the admission and epoch ledgers, and handed to the next
4
+ // admission in scope. The Harness writes every word; this is only the keeping.
5
+ //
6
+ // Delivered ONCE, and "delivered" means LEDGERED: a notice leaves the pending set in the same save
7
+ // that records the admission it rode. A restart before that save finds it still pending and the
8
+ // restored Turn's admission carries it again; a restart after it does not. Between `take` and that
9
+ // save the notice is RESERVED, in memory only, so a second admission in the same scope cannot carry
10
+ // it too — and a restart forgets reservations along with everything else in memory.
11
+
12
+ import type { Notice } from "./wire.ts";
13
+
14
+ /** Where an admission may hear notices: its continued conversation's base Instance ID (ADR-0057)
15
+ * and its Workspace's Sandbox name. Either may be absent — a fresh Turn has no conversation to be
16
+ * told about, and a Menu-only Agent works in no Workspace. */
17
+ export type NoticeScope = { conversation?: string; workspace?: string };
18
+
19
+ /** One pending notice: the notice, and the conversation or Workspace it goes to. `source` names
20
+ * the event that raised it, so the same event raised twice — two Turns lost to one kernel kill, a
21
+ * stream read replayed after a restore — is one notice. */
22
+ export type PendingNotice = { id: string; notice: Notice; to: string; source?: string };
23
+
24
+ /** The persisted form (`RunBlob.notices`). `seen` is every source ever raised, so a delivered
25
+ * notice is not raised again by a late or replayed witness of the same event. */
26
+ export type NoticeLedgerState = { pending: PendingNotice[]; seen: string[] };
27
+
28
+ /** What one admission takes: the notices it carries, and their ids for the ledger write. */
29
+ export type TakenNotices = { ids: string[]; notices: Notice[] };
30
+
31
+ export class NoticeLedger {
32
+ readonly #pending: PendingNotice[];
33
+ readonly #seen: Set<string>;
34
+ readonly #reserved = new Set<string>();
35
+ readonly #newId: () => string;
36
+
37
+ constructor(state: NoticeLedgerState | undefined, newId: () => string) {
38
+ this.#pending = [...(state?.pending ?? [])];
39
+ this.#seen = new Set(state?.seen ?? []);
40
+ this.#newId = newId;
41
+ }
42
+
43
+ /** Raise a notice for `to` (a conversation's base id, or a Sandbox name — the notice's own
44
+ * `scope` says which). False when `source` was already raised, or the same notice is already
45
+ * pending for `to`. */
46
+ raise(notice: Notice, to: string, source?: string): boolean {
47
+ if (source !== undefined) {
48
+ if (this.#seen.has(source)) return false;
49
+ this.#seen.add(source);
50
+ }
51
+ // The same news, still unheard, is one notice: the guard writes one kill to the stream of
52
+ // every conversation running in the pod, and each Turn following one reads it.
53
+ const same = JSON.stringify(notice);
54
+ if (this.#pending.some((p) => p.to === to && JSON.stringify(p.notice) === same)) return false;
55
+ this.#pending.push({ id: this.#newId(), notice, to, ...(source === undefined ? {} : { source }) });
56
+ return true;
57
+ }
58
+
59
+ /** Reserve every unreserved notice in this admission's scope, in the order raised. */
60
+ take(scope: NoticeScope): TakenNotices {
61
+ const taken = this.#pending.filter(
62
+ (p) =>
63
+ !this.#reserved.has(p.id) &&
64
+ (p.notice.scope === "conversation" ? p.to === scope.conversation : p.to === scope.workspace),
65
+ );
66
+ for (const p of taken) this.#reserved.add(p.id);
67
+ return { ids: taken.map((p) => p.id), notices: taken.map((p) => p.notice) };
68
+ }
69
+
70
+ /** The admission that carried them was ledgered: they are delivered. */
71
+ delivered(ids: readonly string[]): void {
72
+ for (const id of ids) {
73
+ this.#reserved.delete(id);
74
+ const at = this.#pending.findIndex((p) => p.id === id);
75
+ if (at !== -1) this.#pending.splice(at, 1);
76
+ }
77
+ }
78
+
79
+ /** The admission that carried them failed: they are pending again, for the next one. */
80
+ release(ids: readonly string[]): void {
81
+ for (const id of ids) this.#reserved.delete(id);
82
+ }
83
+
84
+ /** The persisted form — reserved notices included, because reserved is not delivered. */
85
+ state(): NoticeLedgerState {
86
+ return { pending: [...this.#pending], seen: [...this.#seen] };
87
+ }
88
+ }