@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.
- 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 -5
- package/src/actor.ts +507 -54
- package/src/agent.ts +2 -2
- 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 +233 -22
- package/src/content-hash.ts +105 -0
- package/src/custodian.lua +313 -0
- package/src/custodian.ts +951 -0
- 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 +247 -25
- package/src/held-secrets.ts +643 -0
- package/src/holds.ts +156 -0
- package/src/http-effect.ts +981 -0
- package/src/http.ts +514 -119
- package/src/images.ts +58 -31
- package/src/index.ts +49 -4
- package/src/instance.ts +445 -59
- package/src/kube-client.ts +333 -0
- package/src/machine-doc.ts +88 -6
- package/src/model-keys.ts +92 -0
- package/src/mount-digest.ts +53 -0
- package/src/names.ts +89 -7
- 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 +61 -86
- package/src/repos.ts +63 -74
- package/src/rest.ts +133 -0
- package/src/run-host.ts +1841 -105
- 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 +285 -89
- 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 +44 -12
- package/src/vocabulary.ts +66 -1
- package/src/wire.ts +156 -2
- package/src/workspace.ts +529 -194
- 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
|
|
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
|
|
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
|
|
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
|
|
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 —
|
|
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
|
|
45
|
-
*
|
|
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
|
+
}
|