@jr2/cli 0.2.1 → 0.3.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/src/deploy.ts CHANGED
@@ -7,7 +7,16 @@ import { readFile } from "node:fs/promises";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import {
9
9
  CA_CONFIGMAP,
10
+ custodianComposition,
11
+ CUSTODIAN_BOOTSTRAP_KEY,
12
+ CUSTODIAN_SCRIPT_KEY,
10
13
  GIT_SSH_SECRET,
14
+ HELD_CA_SECRET,
15
+ HELD_CONFIGMAP,
16
+ HELD_KEY,
17
+ HELD_MOUNT,
18
+ HELD_SECRETS_SECRET,
19
+ HELD_TLS_SECRET,
11
20
  HARNESS_ENV_SECRET,
12
21
  IMAGES_CONFIGMAP,
13
22
  IMAGES_KEY,
@@ -24,8 +33,10 @@ import {
24
33
  REPO_CACHE_HOSTPATH,
25
34
  STATE_PVC,
26
35
  type HarnessConfig,
36
+ type HeldManifest,
27
37
  type SandboxPlacement,
28
38
  } from "@jr2/orchestrator";
39
+ import type { Pem } from "./held-pki.ts";
29
40
 
30
41
  export {
31
42
  GIT_SSH_SECRET,
@@ -48,6 +59,13 @@ export const ANNOTATION_IMAGES = "jr2.dev/images";
48
59
 
49
60
  export const ORCHESTRATOR_SA = "jr2-orchestrator";
50
61
 
62
+ /** The HMAC over the held-secret inputs, on the Instance Harness's pod template (ADR-0059). */
63
+ export const ANNOTATION_HELD_DIGEST = "jr2.dev/held-digest";
64
+
65
+ /** The Instance Harness pod's `fsGroup` — a Sandbox pod's default work group (ADR-0005), for the
66
+ * same reason: a group every container is granted, which the Custodian's values are readable by. */
67
+ const INSTANCE_HARNESS_FS_GROUP = 2000;
68
+
51
69
  /** The two ingress NetworkPolicies (ADR-0058) — see `harnessIngressPolicies`. */
52
70
  export const SANDBOX_INGRESS_POLICY = "jr2-sandbox-ingress";
53
71
  export const INSTANCE_HARNESS_INGRESS_POLICY = "jr2-instance-harness-ingress";
@@ -81,6 +99,24 @@ export function compareVersions(a: string, b: string): number {
81
99
 
82
100
  type KubeManifest = Record<string, unknown>;
83
101
 
102
+ /**
103
+ * What `jr2 up` resolved about held secrets (ADR-0059), ready to become objects. Values appear in
104
+ * two places only: `values` (the literals, for the `jr2-held-secrets` Secret) and the CA's and the
105
+ * leaves' keys — each lands in a Secret no Harness container mounts.
106
+ */
107
+ export type HeldObjects = {
108
+ manifest: HeldManifest;
109
+ /** The literal values, by name — never in the manifest, never in a ConfigMap. */
110
+ values: Record<string, string>;
111
+ /** The per-Instance CA — kept across converges; present whenever a secret is held. */
112
+ ca?: Pem;
113
+ /** One leaf per bound target, by file stem (`leafStem`). */
114
+ leaves: Record<string, Pem>;
115
+ /** The Custodian's rendered bootstrap (JSON) and script. */
116
+ bootstrap: string;
117
+ script: string;
118
+ };
119
+
84
120
  /** Everything `jr2 up` converges inside the instance's namespace, as one apply-able List. */
85
121
  export function instanceObjects(opts: {
86
122
  name: string;
@@ -89,14 +125,15 @@ export function instanceObjects(opts: {
89
125
  hash: string;
90
126
  /** Secret data (token, signing key, harness env literals) — written stringData, kube encodes. */
91
127
  secretData: Record<string, string>;
92
- /** The HARNESS containers' env values (creds, provider key) — a SEPARATE Secret from
93
- * `secretData` by doctrine (ADR-0013): Agent code executes where this lands, so the Instance
94
- * token and signing key must never share a Secret with it. */
128
+ /** The HARNESS containers' literal `harness.env` values — a SEPARATE Secret from `secretData` by
129
+ * doctrine (ADR-0013): Agent code executes where this lands, so the Instance token and signing
130
+ * key must never share a Secret with it, and no model key rides it either (ADR-0059). */
95
131
  harnessEnvData: Record<string, string>;
96
132
  harness?: HarnessConfig;
97
- /** The private-CA PEM bundle (`harness.caBundle` file contents, read by `up` — ADR-0020). */
98
- caBundle?: string;
99
- /** Every image ref THIS converge resolved (ADR-0037/0038/0049): `{ harness, adapter, operator?,
133
+ /** Held secrets (ADR-0059): the Custodian's objects. The `jr2-held` ConfigMap is applied always —
134
+ * every Harness pod runs a Custodian — and the rest only while a secret is held. */
135
+ held: HeldObjects;
136
+ /** Every image ref THIS converge resolved (ADR-0037/0038/0049): `{ harness, custodian, operator?,
100
137
  * sandbox: { <key>: ref }, sandboxUser: { <key>: user } }`, where a key is a build context's
101
138
  * content digest or the reserved `default`. It lands twice, deliberately as one JSON so the
102
139
  * record `up` diffs and the map pods read can never disagree: as the `jr2-images` ConfigMap the
@@ -165,9 +202,8 @@ export function instanceObjects(opts: {
165
202
  data: {
166
203
  [HARNESS_CONFIG_KEY]: JSON.stringify(
167
204
  {
168
- // apiKey is deliberately dropped: it materializes into the Secret as
169
- // JR2_PROVIDER_API_KEY (`up`), and the Harness reads it from env — a ConfigMap is not
170
- // a place for a credential.
205
+ // apiKey is deliberately dropped: it is a held secret (ADR-0059) — the Custodian holds
206
+ // it, the Harness's env holds its Stand-in, and a ConfigMap is no place for either.
171
207
  provider: opts.harness?.provider
172
208
  ? {
173
209
  id: opts.harness.provider.id,
@@ -180,6 +216,8 @@ export function instanceObjects(opts: {
180
216
  models: opts.harness.provider.models,
181
217
  }
182
218
  : undefined,
219
+ // Where pi's catalog providers send their calls (ADR-0059): endpoints, not keys.
220
+ catalog: opts.harness?.catalog,
183
221
  },
184
222
  null,
185
223
  2,
@@ -197,18 +235,20 @@ export function instanceObjects(opts: {
197
235
  metadata: meta(IMAGES_CONFIGMAP),
198
236
  data: { [IMAGES_KEY]: imagesJson },
199
237
  },
200
- // The private-CA bundle (ADR-0020) — a ConfigMap, not a Secret: CA certs are public data.
201
- // kubectlSandbox mounts it into the Harness container and points NODE_EXTRA_CA_CERTS at it.
202
- ...(opts.caBundle
203
- ? [
204
- {
205
- apiVersion: "v1",
206
- kind: "ConfigMap",
207
- metadata: meta(CA_CONFIGMAP),
208
- data: { "ca.crt": opts.caBundle },
209
- },
210
- ]
211
- : []),
238
+ // Held secrets (ADR-0059). `held.json` is what every composition reads — the Orchestrator per
239
+ // provision from the mount below, `jr2 up` for the Instance Harness — beside the Custodian's
240
+ // bootstrap and script, which Envoy reads at start. Names, hosts, headers: never a value.
241
+ {
242
+ apiVersion: "v1",
243
+ kind: "ConfigMap",
244
+ metadata: meta(HELD_CONFIGMAP),
245
+ data: {
246
+ [HELD_KEY]: JSON.stringify(opts.held.manifest, null, 2),
247
+ [CUSTODIAN_BOOTSTRAP_KEY]: opts.held.bootstrap,
248
+ [CUSTODIAN_SCRIPT_KEY]: opts.held.script,
249
+ },
250
+ },
251
+ ...heldSecretObjects(opts.held, meta),
212
252
  {
213
253
  apiVersion: "v1",
214
254
  kind: "Secret",
@@ -269,6 +309,9 @@ export function instanceObjects(opts: {
269
309
  // The image map, read per provision (ADR-0038). A mount, so `jr2 up` rewriting
270
310
  // it costs one kubelet propagation window instead of a rollout.
271
311
  { name: "images", mountPath: IMAGES_MOUNT, readOnly: true },
312
+ // What the Custodian holds, read per provision the same way (ADR-0059): names
313
+ // and hosts, which this process could not derive — its `.env` values are absent.
314
+ { name: "held", mountPath: HELD_MOUNT, readOnly: true },
272
315
  ],
273
316
  // The period, not the boot, is what `up`'s rollout wait measures. Measured: the
274
317
  // container answers `/healthz` 1.1s after it starts, and the default 10s period
@@ -293,6 +336,7 @@ export function instanceObjects(opts: {
293
336
  volumes: [
294
337
  { name: "state", persistentVolumeClaim: { claimName: STATE_PVC } },
295
338
  { name: "images", configMap: { name: IMAGES_CONFIGMAP } },
339
+ { name: "held", configMap: { name: HELD_CONFIGMAP, items: [{ key: HELD_KEY, path: HELD_KEY }] } },
296
340
  ],
297
341
  },
298
342
  },
@@ -329,8 +373,8 @@ const SANDBOX_POD_LABEL = "sandbox.jr2.dev/name";
329
373
  * window. The peer is this instance's Orchestrator and no other — a `podSelector` never crosses the
330
374
  * namespace, and the instance label pins it within one. What is NOT here, on purpose:
331
375
  *
332
- * - The Orchestrator is not selected. Every route there is token-gated, and an Adapter in any
333
- * Harness pod must reach it; a policy could not tell the Adapter's packets from the Agent's.
376
+ * - The Orchestrator is not selected. Every route there is token-gated, and a Custodian in any
377
+ * Harness pod must reach it; a policy could not tell the Custodian's packets from the Agent's.
334
378
  * - No egress. What a Sandbox may reach is its own decision (ADR-0058 records it open).
335
379
  * - No port. The Orchestrator speaks only the Harness wire to these pods, and a port here would
336
380
  * have to track the CR's `port`; `kubectl port-forward` (the CLI, a human's shell into the User
@@ -469,23 +513,73 @@ function repoCacheObjects(opts: {
469
513
  ];
470
514
  }
471
515
 
472
- /** Where the Instance Harness's Harness container sees the CA bundle — the same path
473
- * `kubectlSandbox` mounts it at in a Sandbox pod (ADR-0020). */
474
- const CA_MOUNT = "/etc/jr2/ca";
516
+ /**
517
+ * The held-secret Secrets (ADR-0059), present only while a secret is held: the CA (which NO pod
518
+ * mounts — `jr2 up` alone reads it back), the literal values and the leaves (the Custodian's alone).
519
+ * `stringData` as written: no trailing newline is added to a value.
520
+ */
521
+ function heldSecretObjects(held: HeldObjects, meta: (name: string) => KubeManifest): KubeManifest[] {
522
+ if (held.manifest.secrets.length === 0 || !held.ca) return [];
523
+ const secret = (name: string, stringData: Record<string, string>): KubeManifest => ({
524
+ apiVersion: "v1",
525
+ kind: "Secret",
526
+ metadata: meta(name),
527
+ type: "Opaque",
528
+ stringData,
529
+ });
530
+ const leaves: Record<string, string> = {};
531
+ for (const [stem, pem] of Object.entries(held.leaves)) {
532
+ leaves[`${stem}.crt`] = pem.cert;
533
+ leaves[`${stem}.key`] = pem.key;
534
+ }
535
+ return [
536
+ secret(HELD_CA_SECRET, { "ca.crt": held.ca.cert, "ca.key": held.ca.key }),
537
+ ...(Object.keys(held.values).length ? [secret(HELD_SECRETS_SECRET, held.values)] : []),
538
+ secret(HELD_TLS_SECRET, leaves),
539
+ ];
540
+ }
475
541
 
476
- /** The Adapter's port on the pod's loopback — the same default the Sandbox pod uses. */
477
- const ADAPTER_PORT = 8081;
542
+ /**
543
+ * The trust ConfigMap (ADR-0020, ADR-0059): the user's `caBundle` as `ca.crt`, and while a secret
544
+ * is held, the three bundles its path needs. Its own manifest, apart from the List, because the
545
+ * bundles carry every root certificate — past what client-side apply's annotation can hold — so
546
+ * `jr2 up` applies it server-side. Undefined when there is nothing to trust.
547
+ */
548
+ export function trustObject(opts: {
549
+ name: string;
550
+ namespace: string;
551
+ caBundle?: string;
552
+ bundles?: { extra: string; bundle: string; upstream: string };
553
+ }): string | undefined {
554
+ if (opts.caBundle === undefined && opts.bundles === undefined) return undefined;
555
+ return JSON.stringify({
556
+ apiVersion: "v1",
557
+ kind: "ConfigMap",
558
+ metadata: {
559
+ name: CA_CONFIGMAP,
560
+ namespace: opts.namespace,
561
+ labels: { [LABEL_INSTANCE]: opts.name, "app.kubernetes.io/managed-by": "jr2" },
562
+ },
563
+ data: {
564
+ ...(opts.caBundle !== undefined ? { "ca.crt": opts.caBundle } : {}),
565
+ ...(opts.bundles
566
+ ? { "extra.crt": opts.bundles.extra, "bundle.crt": opts.bundles.bundle, "upstream.crt": opts.bundles.upstream }
567
+ : {}),
568
+ },
569
+ });
570
+ }
478
571
 
479
572
  /**
480
573
  * The Instance Harness (ADR-0031): the per-instance Harness Deployment + Service `jr2 up`
481
574
  * converges whenever an Agent a registered Machine CARRIES declares `workspace: "none"`
482
575
  * (ADR-0049's walk) — the placement for every Menu-only Agent's Turn, regardless of any enclosing
483
- * Workspace. The one Harness shape, minus the Workspace: the stock Harness image plus the Adapter
484
- * sidecar, the same harness-config ConfigMap and env/envFrom/CA wiring a Sandbox's Harness
485
- * container gets — and NO `/work` volume, no attach step. It runs the STOCK image permanently: `workspace: "none"` withholds the whole
486
- * Working toolset (ADR-0028), so there are no tools to carry and no Sandbox Image to resolve
487
- * (ADR-0037). No config key names, sizes, addresses, or enables it: the Machine walk is the
488
- * entire surface.
576
+ * Workspace. The one Harness shape, minus the Workspace: the stock Harness image plus the Custodian,
577
+ * composed by the same function a Sandbox's is (custodian.ts, ADR-0059), the same harness-config
578
+ * ConfigMap and env/envFrom/trust wiring a Sandbox's Harness container gets — and NO `/work`
579
+ * volume, no attach step. It runs the STOCK image permanently: `workspace: "none"` withholds the
580
+ * whole Working toolset (ADR-0028), so there are no tools to carry and no Sandbox Image to resolve
581
+ * (ADR-0037). No config key names, sizes, addresses, or enables it: the Machine walk is the entire
582
+ * surface.
489
583
  */
490
584
  export function instanceHarnessObjects(opts: {
491
585
  name: string;
@@ -498,11 +592,18 @@ export function instanceHarnessObjects(opts: {
498
592
  * a Sandbox's refs travel through the `jr2-images` ConfigMap. Both are right for what they are —
499
593
  * this Deployment is supposed to roll when its image moves; the Orchestrator is not. */
500
594
  harnessImage: string;
501
- /** The resolved Adapter ref: the Harness's one menu-delivery path, kept even though a `"none"`
595
+ /** The resolved Custodian ref: the Harness's one route to its Menu, kept even though a `"none"`
502
596
  * Agent cannot execute code — forking the path for one pod buys a divergence ADR-0031 declines. */
503
- adapterImage: string;
597
+ custodianImage: string;
598
+ /** What this converge resolved about held secrets (ADR-0059) — the same manifest the
599
+ * Orchestrator composes every Sandbox's Custodian from. */
600
+ held: HeldManifest;
601
+ /** An HMAC over every held-secret input (`held.json`, the leaves, the literal values, the
602
+ * Custodian's config), keyed with the signing key so it reveals nothing — on the pod template, so
603
+ * a change to any of them rolls this pod (ADR-0059). */
604
+ heldDigest: string;
504
605
  harness?: HarnessConfig;
505
- /** The instance ships a private-CA bundle (ADR-0020): mount `jr2-ca` into the Harness container. */
606
+ /** The instance ships a private-CA bundle (ADR-0020). */
506
607
  caBundle?: boolean;
507
608
  /** The digest of this placement's Harness bearer (ADR-0058; `harnessTokenDigest(key,
508
609
  * "jr2-instance-harness")`) — the wire's gate. The digest, never the bearer: the same shape
@@ -526,14 +627,24 @@ export function instanceHarnessObjects(opts: {
526
627
  seccompProfile: { type: "RuntimeDefault" },
527
628
  });
528
629
 
630
+ // The Custodian (ADR-0013, ADR-0059): the placement's Sandbox token — signed for this
631
+ // placement's name (`up.ts` mints it into the instance Secret), so it speaks only for the Turns
632
+ // hosted HERE (tokens.ts) — plus every held secret. Mounted into the Custodian alone; the Instance
633
+ // token itself never enters this pod.
634
+ const custodian = custodianComposition(opts.held, {
635
+ image: opts.custodianImage,
636
+ token: { secret: INSTANCE_SECRET, key: "JR2_INSTANCE_HARNESS_TOKEN" },
637
+ caBundle: opts.caBundle === true,
638
+ });
639
+
529
640
  const harnessContainer = {
530
641
  name: "harness",
531
642
  image: opts.harnessImage,
532
643
  imagePullPolicy: "IfNotPresent",
533
644
  ports: [{ containerPort: INSTANCE_HARNESS_PORT }],
534
- // The same asymmetry the Sandbox pod builds (ADR-0013/0020): the mounted harness config and
535
- // the instance's valueFrom entries ride `env` (literal values live in the jr2-harness-env
536
- // Secret), the CA trust lands here and nowhere else, and no credential ever does.
645
+ // The same asymmetry the Sandbox pod builds (ADR-0013/0020/0059): the mounted harness config
646
+ // and the instance's valueFrom entries ride `env` (literal values live in the jr2-harness-env
647
+ // Secret), then the Custodian's address and every Stand-in, and no credential ever does.
537
648
  env: [
538
649
  {
539
650
  name: "JR2_HARNESS_JSON",
@@ -545,13 +656,12 @@ export function instanceHarnessObjects(opts: {
545
656
  // rather than asserted. The bearer (below) narrows who may try; this decides what.
546
657
  { name: "JR2_MENU_ONLY", value: "1" },
547
658
  ...(opts.harness?.env ?? []).filter((v) => v.valueFrom !== undefined),
548
- { name: "JR2_ADAPTER_URL", value: `http://127.0.0.1:${ADAPTER_PORT}` },
549
- ...(opts.caBundle ? [{ name: "NODE_EXTRA_CA_CERTS", value: `${CA_MOUNT}/ca.crt` }] : []),
659
+ ...custodian.harnessEnv,
550
660
  // The wire's gate (ADR-0058), last — as on a Sandbox — so no `harness.env` entry chooses it.
551
661
  { name: "JR2_HARNESS_TOKEN_SHA256", value: opts.bearerSha256 },
552
662
  ],
553
663
  envFrom: [{ secretRef: { name: HARNESS_ENV_SECRET } }, ...(opts.harness?.envFrom ?? [])],
554
- ...(opts.caBundle ? { volumeMounts: [{ name: "ca", mountPath: CA_MOUNT, readOnly: true }] } : {}),
664
+ ...(custodian.harnessMounts.length ? { volumeMounts: custodian.harnessMounts } : {}),
555
665
  // The operator probes a Sandbox's Harness the same way: serving = the socket accepts.
556
666
  // Period and threshold as reasoned on the Orchestrator above: the default 10s period is the
557
667
  // rollout wait rather than the boot, and the threshold then has to carry the stall tolerance
@@ -565,32 +675,6 @@ export function instanceHarnessObjects(opts: {
565
675
  securityContext: hardenedContainerSecurityContext(),
566
676
  };
567
677
 
568
- const adapterContainer = {
569
- name: "adapter",
570
- image: opts.adapterImage,
571
- imagePullPolicy: "IfNotPresent",
572
- securityContext: hardenedContainerSecurityContext(),
573
- env: [
574
- {
575
- name: "JR2_ORCHESTRATOR_URL",
576
- value: `http://${ORCHESTRATOR_SERVICE}.${opts.namespace}.svc:${ORCHESTRATOR_PORT}`,
577
- },
578
- { name: "JR2_ADAPTER_PORT", value: String(ADAPTER_PORT) },
579
- // The Adapter's bearer env, carrying a sandbox-style token SIGNED FOR THIS PLACEMENT's
580
- // name (`up.ts` mints it into the instance Secret): ADR-0013's delivery doctrine, extended
581
- // to the second placement — the token speaks only for registrations that record the
582
- // Instance Harness as the pod hosting their Turn (tokens.ts), never a Workspace's, and the
583
- // Instance token itself never enters this pod. The credential lives in this container,
584
- // where no Agent can read it — and `JR2_MENU_ONLY` above is what keeps that true: only
585
- // Menu-only Agents run here, so nothing in this pod executes code (ADR-0031's
586
- // defense-in-depth bonus).
587
- {
588
- name: "JR2_SANDBOX_TOKEN",
589
- valueFrom: { secretKeyRef: { name: INSTANCE_SECRET, key: "JR2_INSTANCE_HARNESS_TOKEN" } },
590
- },
591
- ],
592
- };
593
-
594
678
  const items: KubeManifest[] = [
595
679
  {
596
680
  apiVersion: "apps/v1",
@@ -606,18 +690,26 @@ export function instanceHarnessObjects(opts: {
606
690
  strategy: { type: "Recreate" },
607
691
  selector: { matchLabels: { app: INSTANCE_HARNESS_SERVICE } },
608
692
  template: {
609
- metadata: { labels: { ...labels, app: INSTANCE_HARNESS_SERVICE } },
693
+ metadata: {
694
+ labels: { ...labels, app: INSTANCE_HARNESS_SERVICE },
695
+ // A held-secret edit rolls this pod; a live Sandbox keeps what its Custodian read at
696
+ // start (ADR-0059, the ADR-0037 stance).
697
+ annotations: { [ANNOTATION_HELD_DIGEST]: opts.heldDigest },
698
+ },
610
699
  spec: {
611
- containers: [harnessContainer, adapterContainer],
700
+ containers: [harnessContainer, custodian.custodianContainer],
612
701
  // The operator's isolation baseline (sandbox_controller.go), mirrored: same Harness
613
702
  // image, same "never reach the Kubernetes API" north star — JR2_MENU_ONLY makes code
614
- // execution here unlikely, not unimaginable.
703
+ // execution here unlikely, not unimaginable. No `shareProcessNamespace`: the Harness
704
+ // container must not read the Custodian's `/proc`. `fsGroup` makes the Custodian's
705
+ // group-readable values readable by Envoy's own uid, as a Sandbox pod's does.
615
706
  automountServiceAccountToken: false,
616
707
  securityContext: {
617
708
  runAsNonRoot: true,
618
709
  seccompProfile: { type: "RuntimeDefault" },
710
+ fsGroup: INSTANCE_HARNESS_FS_GROUP,
619
711
  },
620
- ...(opts.caBundle ? { volumes: [{ name: "ca", configMap: { name: CA_CONFIGMAP } }] } : {}),
712
+ volumes: custodian.volumes,
621
713
  },
622
714
  },
623
715
  },