@jr2/cli 0.4.1 → 0.6.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/README.md +3 -2
- package/package.json +3 -3
- package/src/agents-md.ts +149 -0
- package/src/assembly.ts +1 -1
- package/src/build.ts +24 -7
- package/src/cli.ts +20 -45
- package/src/client.ts +22 -1
- package/src/command-table.ts +641 -0
- package/src/commands/check.ts +37 -0
- package/src/commands/down.ts +7 -17
- package/src/commands/gc.ts +2 -11
- package/src/commands/init.ts +73 -17
- package/src/commands/kit.ts +6 -11
- package/src/commands/logs.ts +4 -9
- package/src/commands/run.ts +14 -15
- package/src/commands/runs.ts +13 -28
- package/src/commands/send.ts +4 -18
- package/src/commands/status.ts +63 -11
- package/src/commands/up.ts +905 -384
- package/src/commands/version.ts +9 -13
- package/src/deploy.ts +394 -48
- package/src/index.ts +26 -0
- package/src/instance-check.ts +313 -0
- package/src/instance.ts +3 -9
- package/src/kit-version.ts +27 -17
- package/src/kube.ts +334 -67
- package/src/landlock.ts +129 -0
- package/src/nodes.ts +7 -1
- package/src/output.ts +1 -1
- package/src/repo-sweep.ts +1 -1
- package/src/sweep.ts +6 -5
package/src/commands/version.ts
CHANGED
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
// A REPORT verb, not a result verb: the human table goes to stdout, `--json` swaps in the one object.
|
|
14
14
|
|
|
15
15
|
import { basename } from "node:path";
|
|
16
|
-
import {
|
|
17
|
-
import { IMAGES_CONFIGMAP, IMAGES_KEY, INSTANCE_SECRET, loadConfig, type ImageRefs } from "@jr2/orchestrator";
|
|
16
|
+
import { IMAGES_CONFIGMAP, IMAGES_KEY, INSTANCE_SECRET, loadConfig, type ImageRefs } from "@jr2/orchestrator/internal";
|
|
18
17
|
import { detectKitCheckout, KIT_IMAGE_HOME } from "../build.ts";
|
|
19
18
|
import type { InstanceIdentity } from "../client.ts";
|
|
20
19
|
import { compareVersions, LABEL_VERSION, OPERATOR_DEPLOYMENT, OPERATOR_NAMESPACE } from "../deploy.ts";
|
|
21
20
|
import { handedOffFrom } from "../handoff.ts";
|
|
22
|
-
import {
|
|
21
|
+
import { targetOptions, type TargetOptions } from "../instance.ts";
|
|
23
22
|
import { checkKitVersion, CLI_ROOT, CLI_VERSION } from "../kit-version.ts";
|
|
24
23
|
import { kubectlAdmin, kubectlKube, ORCHESTRATOR_PORT, ORCHESTRATOR_SERVICE, type KubeObject } from "../kube.ts";
|
|
25
24
|
import { activity, result, type Io } from "../output.ts";
|
|
26
25
|
import { findRoot } from "../root.ts";
|
|
26
|
+
import { parseCommand } from "../command-table.ts";
|
|
27
27
|
|
|
28
28
|
/** A package as the report names it: the number and the real path, so the path says WHICH copy. */
|
|
29
29
|
export type Copy = { version: string; path: string };
|
|
@@ -79,12 +79,7 @@ export type VersionReport = {
|
|
|
79
79
|
type Orchestrator = { version?: string; hash?: string; image?: string } | { error: string };
|
|
80
80
|
|
|
81
81
|
export async function version(args: string[], io: Io): Promise<number> {
|
|
82
|
-
const { values } =
|
|
83
|
-
args,
|
|
84
|
-
allowPositionals: true,
|
|
85
|
-
strict: false,
|
|
86
|
-
options: { ...TARGET_ARGS, local: { type: "boolean" }, json: { type: "boolean" } },
|
|
87
|
-
});
|
|
82
|
+
const { values } = parseCommand("version", args);
|
|
88
83
|
const opts = targetOptions(values);
|
|
89
84
|
const report = await versionReport(io, opts, { local: values.local === true });
|
|
90
85
|
if (values.json === true) result(io, report);
|
|
@@ -258,7 +253,7 @@ const copy = (c: Copy): string => `${c.version.padEnd(8)} ${c.path}`;
|
|
|
258
253
|
export function render(r: VersionReport): string {
|
|
259
254
|
const rows: Row[] = [["cli", copy(r.cli)]];
|
|
260
255
|
if (r.global) rows.push(["global", copy(r.global)]);
|
|
261
|
-
if (r.kit) rows.push(["kit", renderKit(r.kit)]);
|
|
256
|
+
if (r.kit) rows.push(["kit", renderKit(r.kit, r.cli)]);
|
|
262
257
|
if (r.instance) {
|
|
263
258
|
const i = r.instance;
|
|
264
259
|
const mode = i.mode === "checkout" ? `kit checkout at ${i.kitCheckout}` : `installed kit → ${i.kitRegistry}`;
|
|
@@ -273,14 +268,15 @@ export function render(r: VersionReport): string {
|
|
|
273
268
|
return rows.map(([label, value]) => (label ? `${`${label}:`.padEnd(width)}${value}` : "")).join("\n") + "\n";
|
|
274
269
|
}
|
|
275
270
|
|
|
276
|
-
function renderKit(k: NonNullable<VersionReport["kit"]
|
|
271
|
+
function renderKit(k: NonNullable<VersionReport["kit"]>, cli: Copy): string {
|
|
277
272
|
const pinned = k.pinned !== undefined ? `; package.json pins ${k.pinned} — reinstall` : "";
|
|
278
273
|
if (k.check === "unresolved") return `not installed${pinned}`;
|
|
279
274
|
const head = copy({ version: k.version!, path: k.path! });
|
|
280
275
|
if (k.check === "ok") return `${head} ok${pinned}`;
|
|
276
|
+
// The CLI's own number is named: one copy at another version is a mismatch too (ADR-0056).
|
|
281
277
|
const against = k.against
|
|
282
|
-
? `this jr2 runs against ${k.against.version} (${k.against.path})`
|
|
283
|
-
:
|
|
278
|
+
? `this jr2 (${cli.version}) runs against ${k.against.version} (${k.against.path})`
|
|
279
|
+
: `this jr2 (${cli.version}) has no @jr2/orchestrator beside it`;
|
|
284
280
|
return `${head} MISMATCH — ${against}${pinned}`;
|
|
285
281
|
}
|
|
286
282
|
|
package/src/deploy.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// namespace are constants (namespace is the identity); every object carries the instance label so
|
|
4
4
|
// ownership is derivable from the cluster — there is no local target state.
|
|
5
5
|
|
|
6
|
-
import { createHmac } from "node:crypto";
|
|
6
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import {
|
|
@@ -19,6 +19,11 @@ import {
|
|
|
19
19
|
CUSTODIAN_PROGRAM,
|
|
20
20
|
CUSTODIAN_PROGRAM_KEY,
|
|
21
21
|
CUSTODIAN_SCRIPT_KEY,
|
|
22
|
+
CREDENTIAL_KEYS,
|
|
23
|
+
controlStandIn,
|
|
24
|
+
controlToken,
|
|
25
|
+
heldStandIn,
|
|
26
|
+
heldStandInKey,
|
|
22
27
|
INSTANCE_CUSTODIAN_CONFIGMAP,
|
|
23
28
|
GIT_SSH_SECRET,
|
|
24
29
|
HELD_CA_SECRET,
|
|
@@ -60,9 +65,10 @@ import {
|
|
|
60
65
|
type GitPushManifest,
|
|
61
66
|
type HarnessConfig,
|
|
62
67
|
type HeldManifest,
|
|
68
|
+
markHarnessEnv,
|
|
63
69
|
type PriorityClasses,
|
|
64
70
|
type SandboxPlacement,
|
|
65
|
-
} from "@jr2/orchestrator";
|
|
71
|
+
} from "@jr2/orchestrator/internal";
|
|
66
72
|
import type { Pem } from "./held-pki.ts";
|
|
67
73
|
|
|
68
74
|
export {
|
|
@@ -93,6 +99,39 @@ export const ANNOTATION_HELD_DIGEST = "jr2.dev/held-digest";
|
|
|
93
99
|
* same reason: a group every container is granted, which the Custodian's values are readable by. */
|
|
94
100
|
const INSTANCE_HARNESS_FS_GROUP = 2000;
|
|
95
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The Instance Harness's credentials Secret (ADR-0076, ADR-0059): what a Sandbox's `<sandbox>-token`
|
|
104
|
+
* is to a Sandbox. It holds the pod's control token (`controlToken`, for the placement's name), its
|
|
105
|
+
* secret control Stand-in and one secret Stand-in per held secret of the pod, under
|
|
106
|
+
* `CREDENTIAL_KEYS` and `heldStandInKey`. `jr2 up` mints it, as the pod's maker. The Custodian
|
|
107
|
+
* mounts the token and the Stand-ins; the Harness container mounts the Stand-ins alone, as files
|
|
108
|
+
* under `/run/jr2/` (`custodianComposition`). A Secret of its own, never `jr2-instance`: the
|
|
109
|
+
* Instance token and the signing key share no Secret with anything the Harness container mounts.
|
|
110
|
+
*/
|
|
111
|
+
export const INSTANCE_HARNESS_CREDENTIALS = `${INSTANCE_HARNESS_SERVICE}-token`;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* One pod's credentials Secret data (ADR-0076, ADR-0059), minted by `jr2 up` as that pod's maker:
|
|
115
|
+
* the control token, the secret control Stand-in, and one secret Stand-in per held secret of
|
|
116
|
+
* `manifest` (`heldStandIn`, under the pod key `heldPod`). The Instance Harness's control token is
|
|
117
|
+
* signed for the placement's name, so it speaks only for the Turns hosted there; a check pod's
|
|
118
|
+
* (`controlToken: "unsigned"`) is a value the Orchestrator never signed — it reads no Menu
|
|
119
|
+
* (ADR-0019). No pod here serves a fetch, so neither holds a fetch token. Never logged: the values
|
|
120
|
+
* reach a Secret and nothing else.
|
|
121
|
+
*/
|
|
122
|
+
export function podCredentials(
|
|
123
|
+
key: Buffer,
|
|
124
|
+
opts: { placement: string; heldPod: string; manifest: HeldManifest; controlToken?: "signed" | "unsigned" },
|
|
125
|
+
): Record<string, string> {
|
|
126
|
+
const data: Record<string, string> = {
|
|
127
|
+
[CREDENTIAL_KEYS.controlToken]:
|
|
128
|
+
opts.controlToken === "unsigned" ? randomBytes(24).toString("hex") : controlToken(key, opts.placement),
|
|
129
|
+
[CREDENTIAL_KEYS.controlStandIn]: controlStandIn(key, opts.placement),
|
|
130
|
+
};
|
|
131
|
+
for (const s of opts.manifest.secrets) data[heldStandInKey(s.name)] = heldStandIn(key, opts.heldPod, s.name);
|
|
132
|
+
return data;
|
|
133
|
+
}
|
|
134
|
+
|
|
96
135
|
/** How long the Instance Harness pod is given to stop (ADR-0031's drain): on SIGTERM it stops
|
|
97
136
|
* admitting and finishes the Turns it holds, so the grace is a Turn's worst case. `jr2 up` waits
|
|
98
137
|
* out a rollout that long, and no longer. */
|
|
@@ -110,6 +149,48 @@ export const INSTANCE_HARNESS_PODS_SERVICE = `${INSTANCE_HARNESS_SERVICE}-pods`;
|
|
|
110
149
|
/** The two ingress NetworkPolicies (ADR-0058) — see `harnessIngressPolicies`. */
|
|
111
150
|
export const SANDBOX_INGRESS_POLICY = "jr2-sandbox-ingress";
|
|
112
151
|
export const INSTANCE_HARNESS_INGRESS_POLICY = "jr2-instance-harness-ingress";
|
|
152
|
+
/** The two egress NetworkPolicies (ADR-0076) — see `metadataEgress`. */
|
|
153
|
+
export const SANDBOX_EGRESS_POLICY = "jr2-sandbox-egress";
|
|
154
|
+
export const INSTANCE_HARNESS_EGRESS_POLICY = "jr2-instance-harness-egress";
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The cloud metadata addresses that a Harness pod does not reach (ADR-0076): where a node's
|
|
158
|
+
* metadata service answers pods, it gives the node's own cloud credential, which no `workspace()`
|
|
159
|
+
* grants. AWS's IMDS (IPv4 and IPv6), ECS's and EKS Pod Identity's credential agents, the
|
|
160
|
+
* metadata service of GCP and Azure (the same 169.254.169.254), and Alibaba's. Each is a single
|
|
161
|
+
* address: all of 169.254.0.0/16 would cut NodeLocal DNSCache (169.254.20.10). The list is not
|
|
162
|
+
* complete for every provider, so the shared-cluster guide tells the admin to check theirs.
|
|
163
|
+
*/
|
|
164
|
+
export const METADATA_ADDRESSES_V4 = [
|
|
165
|
+
"169.254.169.254/32",
|
|
166
|
+
"169.254.170.2/32",
|
|
167
|
+
"169.254.170.23/32",
|
|
168
|
+
"100.100.100.200/32",
|
|
169
|
+
] as const;
|
|
170
|
+
export const METADATA_ADDRESSES_V6 = ["fd00:ec2::254/128"] as const;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The egress of a Harness pod (ADR-0076): everything, except the cloud metadata addresses.
|
|
174
|
+
* NetworkPolicy has no deny rule, so the rule allows every address but those, and every pod of the
|
|
175
|
+
* cluster as a peer of its own. A CNI that matches an `ipBlock` against a pod's address too (Cilium)
|
|
176
|
+
* would otherwise cut DNS, the Orchestrator and each Service. The policy needs a CNI that enforces
|
|
177
|
+
* NetworkPolicy; on one that does not, nothing is blocked.
|
|
178
|
+
*/
|
|
179
|
+
export function metadataEgress(podSelector: Record<string, unknown>): Record<string, unknown> {
|
|
180
|
+
return {
|
|
181
|
+
podSelector,
|
|
182
|
+
policyTypes: ["Egress"],
|
|
183
|
+
egress: [
|
|
184
|
+
{
|
|
185
|
+
to: [
|
|
186
|
+
{ ipBlock: { cidr: "0.0.0.0/0", except: [...METADATA_ADDRESSES_V4] } },
|
|
187
|
+
{ ipBlock: { cidr: "::/0", except: [...METADATA_ADDRESSES_V6] } },
|
|
188
|
+
{ namespaceSelector: {} },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
113
194
|
|
|
114
195
|
/** The operator's install location — per-cluster, shared by every instance (ADR-0019). */
|
|
115
196
|
export const OPERATOR_NAMESPACE = "jr2-system";
|
|
@@ -163,6 +244,8 @@ export const KIT_POD_SIZES = {
|
|
|
163
244
|
repoCache: { requests: { cpu: "50m", memory: "128Mi" }, limits: { memory: "1Gi" } },
|
|
164
245
|
/** The Instance Effector (ADR-0068): one http call at a time per Effect, its text body bounded. */
|
|
165
246
|
instanceEffector: { requests: { cpu: "50m", memory: "128Mi" }, limits: { memory: "128Mi" } },
|
|
247
|
+
/** The provider preflight's check container (ADR-0019): one Node.js process, pi-ai, one call at a time. */
|
|
248
|
+
checkContainer: { requests: { cpu: "100m", memory: "512Mi" }, limits: { memory: "512Mi" } },
|
|
166
249
|
} as const;
|
|
167
250
|
|
|
168
251
|
/** A fresh copy, so no manifest shares an object with another (or with the constant). */
|
|
@@ -366,28 +449,7 @@ export function instanceObjects(opts: {
|
|
|
366
449
|
kind: "ConfigMap",
|
|
367
450
|
metadata: meta(HARNESS_CONFIGMAP),
|
|
368
451
|
data: {
|
|
369
|
-
[HARNESS_CONFIG_KEY]:
|
|
370
|
-
{
|
|
371
|
-
// apiKey is deliberately dropped: it is a held secret (ADR-0059) — the Custodian holds
|
|
372
|
-
// it, the Harness's env holds its Stand-in, and a ConfigMap is no place for either.
|
|
373
|
-
provider: opts.harness?.provider
|
|
374
|
-
? {
|
|
375
|
-
id: opts.harness.provider.id,
|
|
376
|
-
api: opts.harness.provider.api,
|
|
377
|
-
baseUrl: opts.harness.provider.baseUrl,
|
|
378
|
-
// Token limits are model properties, not credentials — they ride the ConfigMap
|
|
379
|
-
// so the Harness can register the provider with them.
|
|
380
|
-
contextWindow: opts.harness.provider.contextWindow,
|
|
381
|
-
maxTokens: opts.harness.provider.maxTokens,
|
|
382
|
-
models: opts.harness.provider.models,
|
|
383
|
-
}
|
|
384
|
-
: undefined,
|
|
385
|
-
// Where pi's catalog providers send their calls (ADR-0059): endpoints, not keys.
|
|
386
|
-
catalog: opts.harness?.catalog,
|
|
387
|
-
},
|
|
388
|
-
null,
|
|
389
|
-
2,
|
|
390
|
-
),
|
|
452
|
+
[HARNESS_CONFIG_KEY]: harnessConfigJson(opts.harness),
|
|
391
453
|
},
|
|
392
454
|
},
|
|
393
455
|
{
|
|
@@ -598,6 +660,7 @@ export function instanceObjects(opts: {
|
|
|
598
660
|
},
|
|
599
661
|
},
|
|
600
662
|
...harnessIngressPolicies(opts.name, meta),
|
|
663
|
+
...harnessEgressPolicies(meta),
|
|
601
664
|
...(opts.repoCache
|
|
602
665
|
? repoCacheObjects({
|
|
603
666
|
...opts.repoCache,
|
|
@@ -629,7 +692,7 @@ const SANDBOX_POD_LABEL = "sandbox.jr2.dev/name";
|
|
|
629
692
|
*
|
|
630
693
|
* - The Orchestrator is not selected. Every route there is token-gated, and a Custodian in any
|
|
631
694
|
* Harness pod must reach it; a policy could not tell the Custodian's packets from the Agent's.
|
|
632
|
-
* - No egress.
|
|
695
|
+
* - No egress. That is `harnessEgressPolicies`: open, except the cloud metadata addresses (ADR-0076).
|
|
633
696
|
* - No port. The Orchestrator speaks only the Harness wire to these pods, and a port here would
|
|
634
697
|
* have to track the CR's `port`; `kubectl port-forward` (the CLI, a human's shell into the User
|
|
635
698
|
* Container — ADR-0005) and kubelet probes never cross a policy at all.
|
|
@@ -650,6 +713,26 @@ function harnessIngressPolicies(instance: string, meta: (name: string) => KubeMa
|
|
|
650
713
|
];
|
|
651
714
|
}
|
|
652
715
|
|
|
716
|
+
/**
|
|
717
|
+
* No Harness pod reaches a cloud metadata address (ADR-0076, `metadataEgress`). A Sandbox runs
|
|
718
|
+
* Workspace code, and the node's credential is one that no `workspace()` grants. The Instance
|
|
719
|
+
* Harness runs no Workspace code, but it gets the same rule: a check pod of the provider preflight
|
|
720
|
+
* stands for every Harness pod (ADR-0019), so a model that only the node's credential reaches must
|
|
721
|
+
* fail the check, not a Turn. Converged unconditionally, for the reason the ingress policies are.
|
|
722
|
+
*/
|
|
723
|
+
function harnessEgressPolicies(meta: (name: string) => KubeManifest): KubeManifest[] {
|
|
724
|
+
const policy = (name: string, podSelector: Record<string, unknown>): KubeManifest => ({
|
|
725
|
+
apiVersion: "networking.k8s.io/v1",
|
|
726
|
+
kind: "NetworkPolicy",
|
|
727
|
+
metadata: meta(name),
|
|
728
|
+
spec: metadataEgress(podSelector),
|
|
729
|
+
});
|
|
730
|
+
return [
|
|
731
|
+
policy(SANDBOX_EGRESS_POLICY, { matchExpressions: [{ key: SANDBOX_POD_LABEL, operator: "Exists" }] }),
|
|
732
|
+
policy(INSTANCE_HARNESS_EGRESS_POLICY, { matchLabels: { app: INSTANCE_HARNESS_SERVICE } }),
|
|
733
|
+
];
|
|
734
|
+
}
|
|
735
|
+
|
|
653
736
|
/** Where the cache agent's pod sees the node's directory: `--cache-dir`'s default. */
|
|
654
737
|
const REPO_CACHE_MOUNT = "/cache";
|
|
655
738
|
/** The agent's `$HOME` — an emptyDir, where it writes a deploy key for the life of the pod. */
|
|
@@ -670,6 +753,10 @@ const REPO_CACHE_HOME = "/home/jr2";
|
|
|
670
753
|
* (ADR-0052), so an agent lands on exactly the nodes a Sandbox can be placed on. The ServiceAccount token IS mounted: the agent is a client of the Repo resources and
|
|
671
754
|
* of the pods on its node (never of Sandboxes — demand is a pod's mount), unlike a Sandbox, whose
|
|
672
755
|
* north star is never reaching the API.
|
|
756
|
+
*
|
|
757
|
+
* Because it lands on exactly the Sandbox nodes, it is also the Landlock detector (ADR-0077): it
|
|
758
|
+
* probes at start and, on a node without Landlock, exits with the reason, and `jr2 up` refuses to
|
|
759
|
+
* converge (`landlock.ts`).
|
|
673
760
|
*/
|
|
674
761
|
function repoCacheObjects(opts: {
|
|
675
762
|
image: string;
|
|
@@ -732,6 +819,11 @@ function repoCacheObjects(opts: {
|
|
|
732
819
|
image,
|
|
733
820
|
imagePullPolicy: "IfNotPresent",
|
|
734
821
|
command: ["/manager", "repo-cache"],
|
|
822
|
+
// On a node without Landlock the agent exits at start with a message that names
|
|
823
|
+
// Landlock, the node and the reason (ADR-0077). The kubelet keeps it in the
|
|
824
|
+
// container's status, where `jr2 up` reads it; the log line serves if the file
|
|
825
|
+
// write fails.
|
|
826
|
+
terminationMessagePolicy: "FallbackToLogsOnError",
|
|
735
827
|
env: [
|
|
736
828
|
// The downward API names the node this pod is the writer for, and the namespace
|
|
737
829
|
// whose Repos and pods it watches (the agent's `--node` / `--namespace`).
|
|
@@ -872,6 +964,9 @@ export function instanceHarnessObjects(opts: {
|
|
|
872
964
|
* Custodian's config), keyed with the signing key so it reveals nothing — on the pod template, so
|
|
873
965
|
* a change to any of them rolls this pod (ADR-0059). */
|
|
874
966
|
heldDigest: string;
|
|
967
|
+
/** The pod's credentials Secret data (`podCredentials`): its control token and its secret
|
|
968
|
+
* Stand-ins, applied here as {@link INSTANCE_HARNESS_CREDENTIALS} with the pod they are for. */
|
|
969
|
+
credentials: Record<string, string>;
|
|
875
970
|
harness?: HarnessConfig;
|
|
876
971
|
/** The instance ships a private-CA bundle (ADR-0020). */
|
|
877
972
|
caBundle?: boolean;
|
|
@@ -899,15 +994,16 @@ export function instanceHarnessObjects(opts: {
|
|
|
899
994
|
seccompProfile: { type: "RuntimeDefault" },
|
|
900
995
|
});
|
|
901
996
|
|
|
902
|
-
// The Custodian (ADR-0013, ADR-0059): the placement's
|
|
903
|
-
// placement's name
|
|
904
|
-
//
|
|
905
|
-
// the
|
|
997
|
+
// The Custodian (ADR-0013, ADR-0059, ADR-0076): the placement's control token — signed for this
|
|
998
|
+
// placement's name, so it speaks only for the Turns hosted HERE (tokens.ts) — and the held secrets
|
|
999
|
+
// placed here, from the pod's own credentials Secret (`podCredentials`). The tokens are mounted
|
|
1000
|
+
// into the Custodian alone, the Stand-ins into the Custodian and, as files, the Harness; the
|
|
1001
|
+
// Instance token itself never enters this pod. The pod serves no fetch: no Repo is mounted here.
|
|
906
1002
|
const custodian = custodianComposition(opts.held, {
|
|
907
1003
|
image: opts.custodianImage,
|
|
908
1004
|
pod: INSTANCE_HARNESS_HELD_POD,
|
|
909
1005
|
config: INSTANCE_CUSTODIAN_CONFIGMAP,
|
|
910
|
-
|
|
1006
|
+
credentials: { secret: INSTANCE_HARNESS_CREDENTIALS },
|
|
911
1007
|
caBundle: opts.caBundle === true,
|
|
912
1008
|
resources: sized(KIT_POD_SIZES.instanceCustodian),
|
|
913
1009
|
});
|
|
@@ -920,24 +1016,28 @@ export function instanceHarnessObjects(opts: {
|
|
|
920
1016
|
// The same asymmetry the Sandbox pod builds (ADR-0013/0020/0059): the mounted harness config
|
|
921
1017
|
// and the instance's valueFrom entries ride `env` (literal values live in the jr2-harness-env
|
|
922
1018
|
// Secret), then the Custodian's address and every Stand-in, and no credential ever does.
|
|
923
|
-
env:
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1019
|
+
// Marked (ADR-0076, `harness-env.ts`): the stock image's command starts the Harness with
|
|
1020
|
+
// `effector nocore --harness-env`, which gives the Harness process jr2's names alone.
|
|
1021
|
+
...markHarnessEnv(
|
|
1022
|
+
[
|
|
1023
|
+
{
|
|
1024
|
+
name: "JR2_HARNESS_JSON",
|
|
1025
|
+
valueFrom: { configMapKeyRef: { name: HARNESS_CONFIGMAP, key: HARNESS_CONFIG_KEY } },
|
|
1026
|
+
},
|
|
1027
|
+
// The placement gate (ADR-0031): every admission carries its own definition (ADR-0049), so
|
|
1028
|
+
// the Harness itself refuses any admission whose definition declares Workspace access —
|
|
1029
|
+
// Menu-only Agents alone run here, which is what makes "no code execution in this pod" true
|
|
1030
|
+
// rather than asserted. The bearer (below) narrows who may try; this decides what.
|
|
1031
|
+
{ name: "JR2_MENU_ONLY", value: "1" },
|
|
1032
|
+
...(opts.harness?.env ?? []).filter((v) => v.valueFrom !== undefined),
|
|
1033
|
+
...custodian.harnessEnv,
|
|
1034
|
+
// Where the conversations persist (ADR-0031): the claim below. Mechanism, as on a Sandbox.
|
|
1035
|
+
{ name: CONVERSATIONS_DIR_ENV, value: CONVERSATIONS_DIR },
|
|
1036
|
+
// The wire's gate (ADR-0058), last — as on a Sandbox — so no `harness.env` entry chooses it.
|
|
1037
|
+
{ name: "JR2_HARNESS_TOKEN_SHA256", value: opts.bearerSha256 },
|
|
1038
|
+
],
|
|
1039
|
+
[{ secretRef: { name: HARNESS_ENV_SECRET } }, ...(opts.harness?.envFrom ?? [])],
|
|
1040
|
+
),
|
|
941
1041
|
// The Harness container's alone: the Custodian has no business with an Agent's history.
|
|
942
1042
|
volumeMounts: [{ name: CONVERSATIONS_VOLUME, mountPath: CONVERSATIONS_DIR }, ...custodian.harnessMounts],
|
|
943
1043
|
// The operator probes a Sandbox's Harness the same way: serving = the socket accepts.
|
|
@@ -957,6 +1057,14 @@ export function instanceHarnessObjects(opts: {
|
|
|
957
1057
|
};
|
|
958
1058
|
|
|
959
1059
|
const items: KubeManifest[] = [
|
|
1060
|
+
// The pod's credentials (ADR-0076): a Secret, never a ConfigMap, env or the pod template.
|
|
1061
|
+
{
|
|
1062
|
+
apiVersion: "v1",
|
|
1063
|
+
kind: "Secret",
|
|
1064
|
+
metadata: { ...meta(), name: INSTANCE_HARNESS_CREDENTIALS },
|
|
1065
|
+
type: "Opaque",
|
|
1066
|
+
stringData: opts.credentials,
|
|
1067
|
+
},
|
|
960
1068
|
// The one pod `jr2 up` makes, so the one whose Custodian files it renders: its bootstrap and
|
|
961
1069
|
// its HELD table, from its own manifest, beside the program its volume projects from `jr2-held`.
|
|
962
1070
|
{
|
|
@@ -1062,6 +1170,244 @@ export function instanceHarnessObjects(opts: {
|
|
|
1062
1170
|
return JSON.stringify({ apiVersion: "v1", kind: "List", items });
|
|
1063
1171
|
}
|
|
1064
1172
|
|
|
1173
|
+
/** What the Instance can REACH (ADR-0018), as the `jr2-harness` ConfigMap carries it to every
|
|
1174
|
+
* Harness container: the custom provider without its key and the catalog gateways. The check pod
|
|
1175
|
+
* carries the same text in its own ConfigMap (ADR-0019). */
|
|
1176
|
+
export function harnessConfigJson(harness: HarnessConfig | undefined): string {
|
|
1177
|
+
return JSON.stringify(
|
|
1178
|
+
{
|
|
1179
|
+
// apiKey is deliberately dropped: it is a held secret (ADR-0059) — the Custodian holds
|
|
1180
|
+
// it, the Harness's env holds its Stand-in, and a ConfigMap is no place for either.
|
|
1181
|
+
provider: harness?.provider
|
|
1182
|
+
? {
|
|
1183
|
+
id: harness.provider.id,
|
|
1184
|
+
api: harness.provider.api,
|
|
1185
|
+
baseUrl: harness.provider.baseUrl,
|
|
1186
|
+
// Token limits are model properties, not credentials — they ride the ConfigMap
|
|
1187
|
+
// so the Harness can register the provider with them.
|
|
1188
|
+
contextWindow: harness.provider.contextWindow,
|
|
1189
|
+
maxTokens: harness.provider.maxTokens,
|
|
1190
|
+
models: harness.provider.models,
|
|
1191
|
+
}
|
|
1192
|
+
: undefined,
|
|
1193
|
+
// Where pi's catalog providers send their calls (ADR-0059): endpoints, not keys.
|
|
1194
|
+
catalog: harness?.catalog,
|
|
1195
|
+
},
|
|
1196
|
+
null,
|
|
1197
|
+
2,
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/** The provider preflight's objects (ADR-0019): a check pod and its own copies of what it reads,
|
|
1202
|
+
* each named `jr2-provider-preflight-<run>-<n>` or that name and a suffix, unique to one `jr2 up`. */
|
|
1203
|
+
export const PROVIDER_PREFLIGHT = "jr2-provider-preflight";
|
|
1204
|
+
/** The label every one of them carries, its value the `jr2 up` run: how a later run finds the
|
|
1205
|
+
* leftovers of a run that did not end, and leaves another run's objects alone. */
|
|
1206
|
+
export const LABEL_PROVIDER_PREFLIGHT = "jr2.dev/provider-preflight";
|
|
1207
|
+
/** The check container in that pod, whose output `jr2 up` reads. An older `jr2 up` named it `probe`. */
|
|
1208
|
+
export const CHECK_CONTAINER = "check";
|
|
1209
|
+
|
|
1210
|
+
/** Every object of one check pod (`checkPodObjects`), for its deletion. */
|
|
1211
|
+
export function checkPodObjectNames(object: string): Array<{ kind: string; name: string }> {
|
|
1212
|
+
return [
|
|
1213
|
+
{ kind: "pod", name: object },
|
|
1214
|
+
{ kind: "networkpolicy", name: object },
|
|
1215
|
+
{ kind: "configmap", name: object },
|
|
1216
|
+
{ kind: "configmap", name: `${object}-ca` },
|
|
1217
|
+
{ kind: "secret", name: object },
|
|
1218
|
+
{ kind: "secret", name: `${object}-tls` },
|
|
1219
|
+
{ kind: "secret", name: `${object}-env` },
|
|
1220
|
+
];
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* One check pod of the provider preflight (ADR-0019, ADR-0059): a Harness pod with no Harness in
|
|
1225
|
+
* it, and its own copies of every object a Harness pod reads. So a check pod writes nothing that a
|
|
1226
|
+
* running pod of the Instance reads, and a check that fails leaves the Instance as it was.
|
|
1227
|
+
*
|
|
1228
|
+
* - `<object>` (ConfigMap): the Custodian's bootstrap and script, the program and the harness config
|
|
1229
|
+
* (`harnessConfigJson`).
|
|
1230
|
+
* - `<object>-ca` (ConfigMap): the trust bundles, as `jr2-ca` would carry them.
|
|
1231
|
+
* - `<object>` (Secret): the Custodian's control token, a value the Orchestrator never signed (the
|
|
1232
|
+
* check pod reads no Menu), the check pod's own secret Stand-ins (`podCredentials`, ADR-0059), and
|
|
1233
|
+
* the LITERAL values of the keys it holds, from the host. A `valueFrom` key
|
|
1234
|
+
* stays a reference to the user's Secret. The values are in this Secret for the check pod's life.
|
|
1235
|
+
* - `<object>-tls` (Secret): the leaves of the bound hosts. `<object>-env` (Secret): the literal
|
|
1236
|
+
* `harness.env` values, which `jr2-harness-env` would carry.
|
|
1237
|
+
*
|
|
1238
|
+
* The Custodian is composed by the function every Harness pod's is (`custodianComposition`). With
|
|
1239
|
+
* no binding, it holds nothing and the check pod's HTTPS goes direct, as in a Harness pod that holds no
|
|
1240
|
+
* secret. The check container runs the stock Harness image's `provider-preflight.ts` with the Harness
|
|
1241
|
+
* container's env, so the call is the one a Turn makes, and the key never enters that container,
|
|
1242
|
+
* its argv or its log. The argv is the model specifiers alone.
|
|
1243
|
+
*
|
|
1244
|
+
* - `<object>` (NetworkPolicy): the egress of a Harness pod (`metadataEgress`, ADR-0076), so the
|
|
1245
|
+
* check reaches a provider only as a Turn does. It is in the List before the pod.
|
|
1246
|
+
*
|
|
1247
|
+
* The pod has the placement and the priority class of the Harness pods it stands for (ADR-0052,
|
|
1248
|
+
* ADR-0060). Its labels are its own and match no Harness pod's: the Instance Harness Service
|
|
1249
|
+
* selects by label, and would send Turns to it. The Custodian is a native sidecar with a startup
|
|
1250
|
+
* probe, so the check starts only once Envoy serves and the pod ends when the check does.
|
|
1251
|
+
*/
|
|
1252
|
+
export function checkPodObjects(opts: {
|
|
1253
|
+
name: string;
|
|
1254
|
+
namespace: string;
|
|
1255
|
+
/** This check pod's object name (`jr2-provider-preflight-<run>-<n>`) and its run, for the label. */
|
|
1256
|
+
object: string;
|
|
1257
|
+
run: string;
|
|
1258
|
+
/** The resolved stock Harness ref: the image a Turn's Harness runs, with pi-ai at the pin. */
|
|
1259
|
+
harnessImage: string;
|
|
1260
|
+
custodianImage: string;
|
|
1261
|
+
/** The bindings of the keys this check pod holds: each a part of a held pod's manifest. */
|
|
1262
|
+
held: HeldManifest;
|
|
1263
|
+
/** The literal values of those keys, by name, from the host. */
|
|
1264
|
+
values: Record<string, string>;
|
|
1265
|
+
/** The leaves of their bound hosts, by file stem. */
|
|
1266
|
+
leaves: Record<string, Pem>;
|
|
1267
|
+
/** The trust bundles (`ca.crt`, `extra.crt`, `bundle.crt`, `upstream.crt`), as `jr2-ca` has them. */
|
|
1268
|
+
trust: Record<string, string>;
|
|
1269
|
+
/** The Custodian's two files, rendered from `held`. */
|
|
1270
|
+
custodian: CustodianFiles;
|
|
1271
|
+
/** The check pod's credentials (`podCredentials`, ADR-0076): a control token the Orchestrator
|
|
1272
|
+
* never signed, and its own secret Stand-ins, minted for this pod alone. */
|
|
1273
|
+
credentials: Record<string, string>;
|
|
1274
|
+
/** The literal `harness.env` values. */
|
|
1275
|
+
harnessEnvData: Record<string, string>;
|
|
1276
|
+
/** The `<provider>/<model>` specifiers to check, in order. */
|
|
1277
|
+
models: readonly string[];
|
|
1278
|
+
/** The node predicate of the Harness pods this check pod stands for. */
|
|
1279
|
+
placement?: SandboxPlacement;
|
|
1280
|
+
priorityClassName: string;
|
|
1281
|
+
harness?: HarnessConfig;
|
|
1282
|
+
caBundle?: boolean;
|
|
1283
|
+
/** The pod's whole budget: the kubelet ends it at this deadline, even when `jr2 up` is gone. */
|
|
1284
|
+
deadlineSeconds: number;
|
|
1285
|
+
}): string {
|
|
1286
|
+
const labels = {
|
|
1287
|
+
[LABEL_INSTANCE]: opts.name,
|
|
1288
|
+
"app.kubernetes.io/managed-by": "jr2",
|
|
1289
|
+
[LABEL_PROVIDER_PREFLIGHT]: opts.run,
|
|
1290
|
+
};
|
|
1291
|
+
const meta = (name: string) => ({ name, namespace: opts.namespace, labels });
|
|
1292
|
+
const o = opts.object;
|
|
1293
|
+
const custodian = custodianComposition(opts.held, {
|
|
1294
|
+
image: opts.custodianImage,
|
|
1295
|
+
pod: o,
|
|
1296
|
+
reads: { literals: o, tls: `${o}-tls`, trust: `${o}-ca`, program: o },
|
|
1297
|
+
config: o,
|
|
1298
|
+
credentials: { secret: o },
|
|
1299
|
+
caBundle: opts.caBundle === true,
|
|
1300
|
+
resources: sized(KIT_POD_SIZES.instanceCustodian),
|
|
1301
|
+
});
|
|
1302
|
+
const ready = custodian.custodianContainer.readinessProbe as { httpGet: unknown };
|
|
1303
|
+
const pod = {
|
|
1304
|
+
apiVersion: "v1",
|
|
1305
|
+
kind: "Pod",
|
|
1306
|
+
metadata: { ...meta(o), labels: { ...labels, app: PROVIDER_PREFLIGHT } },
|
|
1307
|
+
spec: {
|
|
1308
|
+
restartPolicy: "Never",
|
|
1309
|
+
activeDeadlineSeconds: opts.deadlineSeconds,
|
|
1310
|
+
automountServiceAccountToken: false,
|
|
1311
|
+
priorityClassName: opts.priorityClassName,
|
|
1312
|
+
...(opts.placement?.nodeSelector ? { nodeSelector: opts.placement.nodeSelector } : {}),
|
|
1313
|
+
...(opts.placement?.tolerations?.length ? { tolerations: opts.placement.tolerations } : {}),
|
|
1314
|
+
// The Instance Harness pod's baseline, and its `fsGroup` for the same reason: the
|
|
1315
|
+
// Custodian's values are group-readable, and Envoy runs as its own uid.
|
|
1316
|
+
securityContext: {
|
|
1317
|
+
runAsNonRoot: true,
|
|
1318
|
+
seccompProfile: { type: "RuntimeDefault" },
|
|
1319
|
+
fsGroup: INSTANCE_HARNESS_FS_GROUP,
|
|
1320
|
+
},
|
|
1321
|
+
initContainers: [
|
|
1322
|
+
{
|
|
1323
|
+
...custodian.custodianContainer,
|
|
1324
|
+
restartPolicy: "Always",
|
|
1325
|
+
startupProbe: { httpGet: ready.httpGet, periodSeconds: 1, failureThreshold: 60 },
|
|
1326
|
+
},
|
|
1327
|
+
],
|
|
1328
|
+
containers: [
|
|
1329
|
+
{
|
|
1330
|
+
name: CHECK_CONTAINER,
|
|
1331
|
+
image: opts.harnessImage,
|
|
1332
|
+
imagePullPolicy: "IfNotPresent",
|
|
1333
|
+
// Under tini, as the Harness runs (ADR-0061): PID 1 reaps, and forwards a stop.
|
|
1334
|
+
command: [
|
|
1335
|
+
"/opt/jr2/bin/tini",
|
|
1336
|
+
"--",
|
|
1337
|
+
"/opt/jr2/bin/node",
|
|
1338
|
+
"/opt/jr2/src/provider-preflight.ts",
|
|
1339
|
+
...opts.models,
|
|
1340
|
+
],
|
|
1341
|
+
// The Harness container's env, in its order (`instanceHarnessObjects`): no credential. And
|
|
1342
|
+
// the check pod's own Stand-ins (ADR-0019): it runs no Workspace code, so it may hold them
|
|
1343
|
+
// in env, where pi reads a provider's key; each is a `secretKeyRef`, never a value here.
|
|
1344
|
+
env: [
|
|
1345
|
+
{ name: "JR2_HARNESS_JSON", valueFrom: { configMapKeyRef: { name: o, key: HARNESS_CONFIG_KEY } } },
|
|
1346
|
+
...(opts.harness?.env ?? []).filter((v) => v.valueFrom !== undefined),
|
|
1347
|
+
...custodian.harnessEnv,
|
|
1348
|
+
...opts.held.secrets.map((s) => ({
|
|
1349
|
+
name: s.name,
|
|
1350
|
+
valueFrom: { secretKeyRef: { name: o, key: heldStandInKey(s.name) } },
|
|
1351
|
+
})),
|
|
1352
|
+
],
|
|
1353
|
+
envFrom: [{ secretRef: { name: `${o}-env` } }, ...(opts.harness?.envFrom ?? [])],
|
|
1354
|
+
volumeMounts: custodian.harnessMounts,
|
|
1355
|
+
resources: sized(KIT_POD_SIZES.checkContainer),
|
|
1356
|
+
securityContext: {
|
|
1357
|
+
runAsNonRoot: true,
|
|
1358
|
+
allowPrivilegeEscalation: false,
|
|
1359
|
+
capabilities: { drop: ["ALL"] },
|
|
1360
|
+
seccompProfile: { type: "RuntimeDefault" },
|
|
1361
|
+
},
|
|
1362
|
+
},
|
|
1363
|
+
],
|
|
1364
|
+
volumes: custodian.volumes,
|
|
1365
|
+
},
|
|
1366
|
+
};
|
|
1367
|
+
const secret = (name: string, stringData: Record<string, string>): KubeManifest => ({
|
|
1368
|
+
apiVersion: "v1",
|
|
1369
|
+
kind: "Secret",
|
|
1370
|
+
metadata: meta(name),
|
|
1371
|
+
type: "Opaque",
|
|
1372
|
+
stringData,
|
|
1373
|
+
});
|
|
1374
|
+
const leaves: Record<string, string> = {};
|
|
1375
|
+
for (const stem of new Set(opts.held.secrets.flatMap((s) => s.hosts.map((h) => h.leaf)))) {
|
|
1376
|
+
const pem = opts.leaves[stem];
|
|
1377
|
+
if (!pem) throw new Error(`no leaf for a bound host of the provider preflight (${stem}) — a jr2 defect`);
|
|
1378
|
+
leaves[`${stem}.crt`] = pem.cert;
|
|
1379
|
+
leaves[`${stem}.key`] = pem.key;
|
|
1380
|
+
}
|
|
1381
|
+
const literals = Object.fromEntries(
|
|
1382
|
+
opts.held.secrets.filter((s) => s.source.kind === "literal").map((s) => [s.name, opts.values[s.name] ?? ""]),
|
|
1383
|
+
);
|
|
1384
|
+
const items: KubeManifest[] = [
|
|
1385
|
+
{
|
|
1386
|
+
apiVersion: "v1",
|
|
1387
|
+
kind: "ConfigMap",
|
|
1388
|
+
metadata: meta(o),
|
|
1389
|
+
data: {
|
|
1390
|
+
[CUSTODIAN_BOOTSTRAP_KEY]: opts.custodian.bootstrap,
|
|
1391
|
+
[CUSTODIAN_SCRIPT_KEY]: opts.custodian.script,
|
|
1392
|
+
[CUSTODIAN_PROGRAM_KEY]: CUSTODIAN_PROGRAM,
|
|
1393
|
+
[HARNESS_CONFIG_KEY]: harnessConfigJson(opts.harness),
|
|
1394
|
+
},
|
|
1395
|
+
},
|
|
1396
|
+
{ apiVersion: "v1", kind: "ConfigMap", metadata: meta(`${o}-ca`), data: opts.trust },
|
|
1397
|
+
secret(o, { ...opts.credentials, ...literals }),
|
|
1398
|
+
secret(`${o}-tls`, leaves),
|
|
1399
|
+
secret(`${o}-env`, opts.harnessEnvData),
|
|
1400
|
+
{
|
|
1401
|
+
apiVersion: "networking.k8s.io/v1",
|
|
1402
|
+
kind: "NetworkPolicy",
|
|
1403
|
+
metadata: meta(o),
|
|
1404
|
+
spec: metadataEgress({ matchLabels: { app: PROVIDER_PREFLIGHT, [LABEL_PROVIDER_PREFLIGHT]: opts.run } }),
|
|
1405
|
+
},
|
|
1406
|
+
pod,
|
|
1407
|
+
];
|
|
1408
|
+
return JSON.stringify({ apiVersion: "v1", kind: "List", items });
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1065
1411
|
/** The Orchestrator's pod template digest of the Instance Secret: it rolls the pod when a value
|
|
1066
1412
|
* the process reads through `envFrom` changes (ADR-0069). */
|
|
1067
1413
|
export const ANNOTATION_SECRET_DIGEST = "jr2.dev/secret-digest";
|
package/src/index.ts
CHANGED
|
@@ -14,3 +14,29 @@ export type { KubePort } from "./kube.ts";
|
|
|
14
14
|
// test can issue its own fixtures the same way (the @kind tier's HTTPS model provider).
|
|
15
15
|
export { issueCa, issueLeaf } from "./held-pki.ts";
|
|
16
16
|
export type { Pem } from "./held-pki.ts";
|
|
17
|
+
// The command table (ADR-0072): every verb as data, and the renderers `--help` prints with — what the
|
|
18
|
+
// docs' CLI Reference is generated from, so the help and the site cannot disagree.
|
|
19
|
+
export {
|
|
20
|
+
COMMANDS,
|
|
21
|
+
GLOBAL,
|
|
22
|
+
GLOBAL_OPTIONS,
|
|
23
|
+
HELP_FLAG,
|
|
24
|
+
SECTIONS,
|
|
25
|
+
EXIT_CODES,
|
|
26
|
+
nameOf,
|
|
27
|
+
synopsis,
|
|
28
|
+
renderHelp,
|
|
29
|
+
renderCommand,
|
|
30
|
+
} from "./command-table.ts";
|
|
31
|
+
export type {
|
|
32
|
+
Command,
|
|
33
|
+
CommandName,
|
|
34
|
+
Flag,
|
|
35
|
+
Arg,
|
|
36
|
+
EnvVar,
|
|
37
|
+
Form,
|
|
38
|
+
Output,
|
|
39
|
+
OutputKind,
|
|
40
|
+
GlobalOption,
|
|
41
|
+
Section,
|
|
42
|
+
} from "./command-table.ts";
|