@jr2/cli 0.2.0 → 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/manifests/operator.yaml +1 -1
- package/package.json +6 -4
- package/src/build.ts +22 -23
- package/src/commands/kit.ts +24 -12
- package/src/commands/up.ts +167 -26
- package/src/commands/version.ts +1 -1
- package/src/deploy.ts +217 -80
- package/src/held-pki.ts +306 -0
- package/src/index.ts +4 -0
- package/src/kube.ts +8 -4
- package/src/sweep.ts +5 -5
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,17 @@ 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
|
+
|
|
69
|
+
/** The two ingress NetworkPolicies (ADR-0058) — see `harnessIngressPolicies`. */
|
|
70
|
+
export const SANDBOX_INGRESS_POLICY = "jr2-sandbox-ingress";
|
|
71
|
+
export const INSTANCE_HARNESS_INGRESS_POLICY = "jr2-instance-harness-ingress";
|
|
72
|
+
|
|
51
73
|
/** The operator's install location — per-cluster, shared by every instance (ADR-0019). */
|
|
52
74
|
export const OPERATOR_NAMESPACE = "jr2-system";
|
|
53
75
|
export const OPERATOR_DEPLOYMENT = "jr2-controller-manager";
|
|
@@ -77,6 +99,24 @@ export function compareVersions(a: string, b: string): number {
|
|
|
77
99
|
|
|
78
100
|
type KubeManifest = Record<string, unknown>;
|
|
79
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
|
+
|
|
80
120
|
/** Everything `jr2 up` converges inside the instance's namespace, as one apply-able List. */
|
|
81
121
|
export function instanceObjects(opts: {
|
|
82
122
|
name: string;
|
|
@@ -85,14 +125,15 @@ export function instanceObjects(opts: {
|
|
|
85
125
|
hash: string;
|
|
86
126
|
/** Secret data (token, signing key, harness env literals) — written stringData, kube encodes. */
|
|
87
127
|
secretData: Record<string, string>;
|
|
88
|
-
/** The HARNESS containers' env values
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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). */
|
|
91
131
|
harnessEnvData: Record<string, string>;
|
|
92
132
|
harness?: HarnessConfig;
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
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?,
|
|
96
137
|
* sandbox: { <key>: ref }, sandboxUser: { <key>: user } }`, where a key is a build context's
|
|
97
138
|
* content digest or the reserved `default`. It lands twice, deliberately as one JSON so the
|
|
98
139
|
* record `up` diffs and the map pods read can never disagree: as the `jr2-images` ConfigMap the
|
|
@@ -161,9 +202,8 @@ export function instanceObjects(opts: {
|
|
|
161
202
|
data: {
|
|
162
203
|
[HARNESS_CONFIG_KEY]: JSON.stringify(
|
|
163
204
|
{
|
|
164
|
-
// apiKey is deliberately dropped: it
|
|
165
|
-
//
|
|
166
|
-
// 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.
|
|
167
207
|
provider: opts.harness?.provider
|
|
168
208
|
? {
|
|
169
209
|
id: opts.harness.provider.id,
|
|
@@ -176,6 +216,8 @@ export function instanceObjects(opts: {
|
|
|
176
216
|
models: opts.harness.provider.models,
|
|
177
217
|
}
|
|
178
218
|
: undefined,
|
|
219
|
+
// Where pi's catalog providers send their calls (ADR-0059): endpoints, not keys.
|
|
220
|
+
catalog: opts.harness?.catalog,
|
|
179
221
|
},
|
|
180
222
|
null,
|
|
181
223
|
2,
|
|
@@ -193,18 +235,20 @@ export function instanceObjects(opts: {
|
|
|
193
235
|
metadata: meta(IMAGES_CONFIGMAP),
|
|
194
236
|
data: { [IMAGES_KEY]: imagesJson },
|
|
195
237
|
},
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
]
|
|
207
|
-
|
|
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),
|
|
208
252
|
{
|
|
209
253
|
apiVersion: "v1",
|
|
210
254
|
kind: "Secret",
|
|
@@ -265,6 +309,9 @@ export function instanceObjects(opts: {
|
|
|
265
309
|
// The image map, read per provision (ADR-0038). A mount, so `jr2 up` rewriting
|
|
266
310
|
// it costs one kubelet propagation window instead of a rollout.
|
|
267
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 },
|
|
268
315
|
],
|
|
269
316
|
// The period, not the boot, is what `up`'s rollout wait measures. Measured: the
|
|
270
317
|
// container answers `/healthz` 1.1s after it starts, and the default 10s period
|
|
@@ -289,6 +336,7 @@ export function instanceObjects(opts: {
|
|
|
289
336
|
volumes: [
|
|
290
337
|
{ name: "state", persistentVolumeClaim: { claimName: STATE_PVC } },
|
|
291
338
|
{ name: "images", configMap: { name: IMAGES_CONFIGMAP } },
|
|
339
|
+
{ name: "held", configMap: { name: HELD_CONFIGMAP, items: [{ key: HELD_KEY, path: HELD_KEY }] } },
|
|
292
340
|
],
|
|
293
341
|
},
|
|
294
342
|
},
|
|
@@ -303,12 +351,51 @@ export function instanceObjects(opts: {
|
|
|
303
351
|
ports: [{ port: ORCHESTRATOR_PORT, targetPort: ORCHESTRATOR_PORT }],
|
|
304
352
|
},
|
|
305
353
|
},
|
|
354
|
+
...harnessIngressPolicies(opts.name, meta),
|
|
306
355
|
...(opts.repoCache ? repoCacheObjects({ ...opts.repoCache, namespace: opts.namespace, labels, meta }) : []),
|
|
307
356
|
];
|
|
308
357
|
|
|
309
358
|
return JSON.stringify({ apiVersion: "v1", kind: "List", items });
|
|
310
359
|
}
|
|
311
360
|
|
|
361
|
+
/** The operator's label on every Sandbox pod (operator/internal/controller/helpers.go) — and on no
|
|
362
|
+
* other pod, which is what lets one selector mean "every Sandbox, now and later". */
|
|
363
|
+
const SANDBOX_POD_LABEL = "sandbox.jr2.dev/name";
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Ingress to every Harness pod is the Orchestrator's alone (ADR-0058) — the NetworkPolicy half of
|
|
367
|
+
* the wire's defense; the bearer is the other, and the control (ADR-0013: policy bounds where a pod
|
|
368
|
+
* may talk, only a token bounds what it may do). One policy per placement: every Sandbox pod, by
|
|
369
|
+
* the label the operator stamps on each, and the Instance Harness.
|
|
370
|
+
*
|
|
371
|
+
* Converged UNCONDITIONALLY, not only when a Machine composes a Sandbox or declares a `"none"`
|
|
372
|
+
* Agent: a policy that selects nothing costs nothing, and a pod born before its policy would be a
|
|
373
|
+
* window. The peer is this instance's Orchestrator and no other — a `podSelector` never crosses the
|
|
374
|
+
* namespace, and the instance label pins it within one. What is NOT here, on purpose:
|
|
375
|
+
*
|
|
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.
|
|
378
|
+
* - No egress. What a Sandbox may reach is its own decision (ADR-0058 records it open).
|
|
379
|
+
* - No port. The Orchestrator speaks only the Harness wire to these pods, and a port here would
|
|
380
|
+
* have to track the CR's `port`; `kubectl port-forward` (the CLI, a human's shell into the User
|
|
381
|
+
* Container — ADR-0005) and kubelet probes never cross a policy at all.
|
|
382
|
+
*/
|
|
383
|
+
function harnessIngressPolicies(instance: string, meta: (name: string) => KubeManifest): KubeManifest[] {
|
|
384
|
+
const fromOrchestrator = [
|
|
385
|
+
{ from: [{ podSelector: { matchLabels: { app: ORCHESTRATOR_SERVICE, [LABEL_INSTANCE]: instance } } }] },
|
|
386
|
+
];
|
|
387
|
+
const policy = (name: string, podSelector: Record<string, unknown>): KubeManifest => ({
|
|
388
|
+
apiVersion: "networking.k8s.io/v1",
|
|
389
|
+
kind: "NetworkPolicy",
|
|
390
|
+
metadata: meta(name),
|
|
391
|
+
spec: { podSelector, policyTypes: ["Ingress"], ingress: fromOrchestrator },
|
|
392
|
+
});
|
|
393
|
+
return [
|
|
394
|
+
policy(SANDBOX_INGRESS_POLICY, { matchExpressions: [{ key: SANDBOX_POD_LABEL, operator: "Exists" }] }),
|
|
395
|
+
policy(INSTANCE_HARNESS_INGRESS_POLICY, { matchLabels: { app: INSTANCE_HARNESS_SERVICE } }),
|
|
396
|
+
];
|
|
397
|
+
}
|
|
398
|
+
|
|
312
399
|
/** Where the cache agent's pod sees the node's directory: `--cache-dir`'s default. */
|
|
313
400
|
const REPO_CACHE_MOUNT = "/cache";
|
|
314
401
|
/** The agent's `$HOME` — an emptyDir, where it writes a deploy key for the life of the pod. */
|
|
@@ -426,23 +513,73 @@ function repoCacheObjects(opts: {
|
|
|
426
513
|
];
|
|
427
514
|
}
|
|
428
515
|
|
|
429
|
-
/**
|
|
430
|
-
*
|
|
431
|
-
|
|
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
|
+
}
|
|
432
541
|
|
|
433
|
-
/**
|
|
434
|
-
|
|
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
|
+
}
|
|
435
571
|
|
|
436
572
|
/**
|
|
437
573
|
* The Instance Harness (ADR-0031): the per-instance Harness Deployment + Service `jr2 up`
|
|
438
574
|
* converges whenever an Agent a registered Machine CARRIES declares `workspace: "none"`
|
|
439
575
|
* (ADR-0049's walk) — the placement for every Menu-only Agent's Turn, regardless of any enclosing
|
|
440
|
-
* Workspace. The one Harness shape, minus the Workspace: the stock Harness image plus the
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* (ADR-
|
|
445
|
-
* entire
|
|
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.
|
|
446
583
|
*/
|
|
447
584
|
export function instanceHarnessObjects(opts: {
|
|
448
585
|
name: string;
|
|
@@ -455,16 +592,24 @@ export function instanceHarnessObjects(opts: {
|
|
|
455
592
|
* a Sandbox's refs travel through the `jr2-images` ConfigMap. Both are right for what they are —
|
|
456
593
|
* this Deployment is supposed to roll when its image moves; the Orchestrator is not. */
|
|
457
594
|
harnessImage: string;
|
|
458
|
-
/** The resolved
|
|
595
|
+
/** The resolved Custodian ref: the Harness's one route to its Menu, kept even though a `"none"`
|
|
459
596
|
* Agent cannot execute code — forking the path for one pod buys a divergence ADR-0031 declines. */
|
|
460
|
-
|
|
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;
|
|
461
605
|
harness?: HarnessConfig;
|
|
462
|
-
/** The instance ships a private-CA bundle (ADR-0020)
|
|
606
|
+
/** The instance ships a private-CA bundle (ADR-0020). */
|
|
463
607
|
caBundle?: boolean;
|
|
464
|
-
/** The
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
|
|
608
|
+
/** The digest of this placement's Harness bearer (ADR-0058; `harnessTokenDigest(key,
|
|
609
|
+
* "jr2-instance-harness")`) — the wire's gate. The digest, never the bearer: the same shape
|
|
610
|
+
* every Sandbox Harness container gets, so this pod can verify the Orchestrator and mint
|
|
611
|
+
* nothing. */
|
|
612
|
+
bearerSha256: string;
|
|
468
613
|
}): string {
|
|
469
614
|
const labels = { [LABEL_INSTANCE]: opts.name, "app.kubernetes.io/managed-by": "jr2" };
|
|
470
615
|
const meta = (): KubeManifest => ({
|
|
@@ -482,31 +627,41 @@ export function instanceHarnessObjects(opts: {
|
|
|
482
627
|
seccompProfile: { type: "RuntimeDefault" },
|
|
483
628
|
});
|
|
484
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
|
+
|
|
485
640
|
const harnessContainer = {
|
|
486
641
|
name: "harness",
|
|
487
642
|
image: opts.harnessImage,
|
|
488
643
|
imagePullPolicy: "IfNotPresent",
|
|
489
644
|
ports: [{ containerPort: INSTANCE_HARNESS_PORT }],
|
|
490
|
-
// The same asymmetry the Sandbox pod builds (ADR-0013/0020): the mounted harness config
|
|
491
|
-
// the instance's valueFrom entries ride `env` (literal values live in the jr2-harness-env
|
|
492
|
-
// Secret), the
|
|
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.
|
|
493
648
|
env: [
|
|
494
649
|
{
|
|
495
650
|
name: "JR2_HARNESS_JSON",
|
|
496
651
|
valueFrom: { configMapKeyRef: { name: HARNESS_CONFIGMAP, key: HARNESS_CONFIG_KEY } },
|
|
497
652
|
},
|
|
498
|
-
// The placement gate (ADR-0031): every admission carries its own definition (ADR-0049)
|
|
499
|
-
// the
|
|
500
|
-
//
|
|
501
|
-
//
|
|
653
|
+
// The placement gate (ADR-0031): every admission carries its own definition (ADR-0049), so
|
|
654
|
+
// the Harness itself refuses any admission whose definition declares Workspace access —
|
|
655
|
+
// Menu-only Agents alone run here, which is what makes "no code execution in this pod" true
|
|
656
|
+
// rather than asserted. The bearer (below) narrows who may try; this decides what.
|
|
502
657
|
{ name: "JR2_MENU_ONLY", value: "1" },
|
|
503
|
-
...(opts.echoTokenSha256 ? [{ name: "JR2_ECHO_TOKEN_SHA256", value: opts.echoTokenSha256 }] : []),
|
|
504
658
|
...(opts.harness?.env ?? []).filter((v) => v.valueFrom !== undefined),
|
|
505
|
-
|
|
506
|
-
|
|
659
|
+
...custodian.harnessEnv,
|
|
660
|
+
// The wire's gate (ADR-0058), last — as on a Sandbox — so no `harness.env` entry chooses it.
|
|
661
|
+
{ name: "JR2_HARNESS_TOKEN_SHA256", value: opts.bearerSha256 },
|
|
507
662
|
],
|
|
508
663
|
envFrom: [{ secretRef: { name: HARNESS_ENV_SECRET } }, ...(opts.harness?.envFrom ?? [])],
|
|
509
|
-
...(
|
|
664
|
+
...(custodian.harnessMounts.length ? { volumeMounts: custodian.harnessMounts } : {}),
|
|
510
665
|
// The operator probes a Sandbox's Harness the same way: serving = the socket accepts.
|
|
511
666
|
// Period and threshold as reasoned on the Orchestrator above: the default 10s period is the
|
|
512
667
|
// rollout wait rather than the boot, and the threshold then has to carry the stall tolerance
|
|
@@ -520,32 +675,6 @@ export function instanceHarnessObjects(opts: {
|
|
|
520
675
|
securityContext: hardenedContainerSecurityContext(),
|
|
521
676
|
};
|
|
522
677
|
|
|
523
|
-
const adapterContainer = {
|
|
524
|
-
name: "adapter",
|
|
525
|
-
image: opts.adapterImage,
|
|
526
|
-
imagePullPolicy: "IfNotPresent",
|
|
527
|
-
securityContext: hardenedContainerSecurityContext(),
|
|
528
|
-
env: [
|
|
529
|
-
{
|
|
530
|
-
name: "JR2_ORCHESTRATOR_URL",
|
|
531
|
-
value: `http://${ORCHESTRATOR_SERVICE}.${opts.namespace}.svc:${ORCHESTRATOR_PORT}`,
|
|
532
|
-
},
|
|
533
|
-
{ name: "JR2_ADAPTER_PORT", value: String(ADAPTER_PORT) },
|
|
534
|
-
// The Adapter's bearer env, carrying a sandbox-style token SIGNED FOR THIS PLACEMENT's
|
|
535
|
-
// name (`up.ts` mints it into the instance Secret): ADR-0013's delivery doctrine, extended
|
|
536
|
-
// to the second placement — the token speaks only for registrations that record the
|
|
537
|
-
// Instance Harness as the pod hosting their Turn (tokens.ts), never a Workspace's, and the
|
|
538
|
-
// Instance token itself never enters this pod. The credential lives in this container,
|
|
539
|
-
// where no Agent can read it — and `JR2_MENU_ONLY` above is what keeps that true: only
|
|
540
|
-
// Menu-only Agents run here, so nothing in this pod executes code (ADR-0031's
|
|
541
|
-
// defense-in-depth bonus).
|
|
542
|
-
{
|
|
543
|
-
name: "JR2_SANDBOX_TOKEN",
|
|
544
|
-
valueFrom: { secretKeyRef: { name: INSTANCE_SECRET, key: "JR2_INSTANCE_HARNESS_TOKEN" } },
|
|
545
|
-
},
|
|
546
|
-
],
|
|
547
|
-
};
|
|
548
|
-
|
|
549
678
|
const items: KubeManifest[] = [
|
|
550
679
|
{
|
|
551
680
|
apiVersion: "apps/v1",
|
|
@@ -561,18 +690,26 @@ export function instanceHarnessObjects(opts: {
|
|
|
561
690
|
strategy: { type: "Recreate" },
|
|
562
691
|
selector: { matchLabels: { app: INSTANCE_HARNESS_SERVICE } },
|
|
563
692
|
template: {
|
|
564
|
-
metadata: {
|
|
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
|
+
},
|
|
565
699
|
spec: {
|
|
566
|
-
containers: [harnessContainer,
|
|
700
|
+
containers: [harnessContainer, custodian.custodianContainer],
|
|
567
701
|
// The operator's isolation baseline (sandbox_controller.go), mirrored: same Harness
|
|
568
702
|
// image, same "never reach the Kubernetes API" north star — JR2_MENU_ONLY makes code
|
|
569
|
-
// 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.
|
|
570
706
|
automountServiceAccountToken: false,
|
|
571
707
|
securityContext: {
|
|
572
708
|
runAsNonRoot: true,
|
|
573
709
|
seccompProfile: { type: "RuntimeDefault" },
|
|
710
|
+
fsGroup: INSTANCE_HARNESS_FS_GROUP,
|
|
574
711
|
},
|
|
575
|
-
|
|
712
|
+
volumes: custodian.volumes,
|
|
576
713
|
},
|
|
577
714
|
},
|
|
578
715
|
},
|