@jr2/cli 0.1.4 → 0.2.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/package.json +3 -3
- package/src/build.ts +2 -2
- package/src/commands/up.ts +4 -3
- package/src/deploy.ts +54 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jr2/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "The jr2 CLI: init, up, run — the interface to a jr2 Instance.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
"ts-blank-space": "^0.9.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@jr2/orchestrator": "0.1
|
|
42
|
+
"@jr2/orchestrator": "0.2.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^26.0.1",
|
|
46
46
|
"xstate": "^5.18.0",
|
|
47
47
|
"zod": "^4.4.3",
|
|
48
|
-
"@jr2/orchestrator": "0.1
|
|
48
|
+
"@jr2/orchestrator": "0.2.1"
|
|
49
49
|
},
|
|
50
50
|
"files": [
|
|
51
51
|
"bin",
|
package/src/build.ts
CHANGED
|
@@ -1004,8 +1004,8 @@ const execCommand: RunCommand = async (command, args, cwd) => {
|
|
|
1004
1004
|
* dependency graph, wherever the folder sits — so it is staged as a copy
|
|
1005
1005
|
* ({@link BUNDLE_STAGE_EXCLUDE}) and installed frozen and production-only by the lockfile's own
|
|
1006
1006
|
* package manager ({@link lockfileInstall}). This is how a standalone instance committed INSIDE
|
|
1007
|
-
* a pnpm monorepo bundles:
|
|
1008
|
-
*
|
|
1007
|
+
* a pnpm monorepo bundles: it sits under a `pnpm-workspace.yaml` that does not list it, so the
|
|
1008
|
+
* workspace above it says nothing about how it installs.
|
|
1009
1009
|
* 2. A `pnpm-workspace.yaml` above it. A workspace member carries no lockfile of its own — the
|
|
1010
1010
|
* workspace root holds it — so it is the ABSENCE of one that sends the instance to `pnpm deploy
|
|
1011
1011
|
* --legacy`, unchanged. pnpm is a contributor prerequisite, like go for the operator, never a
|
package/src/commands/up.ts
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
loadWorkflows,
|
|
60
60
|
matchCredential,
|
|
61
61
|
partsOf,
|
|
62
|
+
harnessTokenDigest,
|
|
62
63
|
sandboxToken,
|
|
63
64
|
type CarriedAgent,
|
|
64
65
|
type CarriedImage,
|
|
@@ -750,9 +751,9 @@ export async function up(args: string[], io: Io): Promise<number> {
|
|
|
750
751
|
adapterImage: refs.adapter,
|
|
751
752
|
harness: config.harness,
|
|
752
753
|
caBundle: caPem !== undefined,
|
|
753
|
-
// The
|
|
754
|
-
// the
|
|
755
|
-
|
|
754
|
+
// The wire's gate (ADR-0058): the digest of the bearer the Orchestrator derives for this
|
|
755
|
+
// placement from the kept key — the same shape it stamps onto every Sandbox at provision.
|
|
756
|
+
bearerSha256: harnessTokenDigest(Buffer.from(signingKey, "base64"), INSTANCE_HARNESS_SERVICE),
|
|
756
757
|
}),
|
|
757
758
|
...ctx,
|
|
758
759
|
});
|
package/src/deploy.ts
CHANGED
|
@@ -48,6 +48,10 @@ export const ANNOTATION_IMAGES = "jr2.dev/images";
|
|
|
48
48
|
|
|
49
49
|
export const ORCHESTRATOR_SA = "jr2-orchestrator";
|
|
50
50
|
|
|
51
|
+
/** The two ingress NetworkPolicies (ADR-0058) — see `harnessIngressPolicies`. */
|
|
52
|
+
export const SANDBOX_INGRESS_POLICY = "jr2-sandbox-ingress";
|
|
53
|
+
export const INSTANCE_HARNESS_INGRESS_POLICY = "jr2-instance-harness-ingress";
|
|
54
|
+
|
|
51
55
|
/** The operator's install location — per-cluster, shared by every instance (ADR-0019). */
|
|
52
56
|
export const OPERATOR_NAMESPACE = "jr2-system";
|
|
53
57
|
export const OPERATOR_DEPLOYMENT = "jr2-controller-manager";
|
|
@@ -303,12 +307,51 @@ export function instanceObjects(opts: {
|
|
|
303
307
|
ports: [{ port: ORCHESTRATOR_PORT, targetPort: ORCHESTRATOR_PORT }],
|
|
304
308
|
},
|
|
305
309
|
},
|
|
310
|
+
...harnessIngressPolicies(opts.name, meta),
|
|
306
311
|
...(opts.repoCache ? repoCacheObjects({ ...opts.repoCache, namespace: opts.namespace, labels, meta }) : []),
|
|
307
312
|
];
|
|
308
313
|
|
|
309
314
|
return JSON.stringify({ apiVersion: "v1", kind: "List", items });
|
|
310
315
|
}
|
|
311
316
|
|
|
317
|
+
/** The operator's label on every Sandbox pod (operator/internal/controller/helpers.go) — and on no
|
|
318
|
+
* other pod, which is what lets one selector mean "every Sandbox, now and later". */
|
|
319
|
+
const SANDBOX_POD_LABEL = "sandbox.jr2.dev/name";
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Ingress to every Harness pod is the Orchestrator's alone (ADR-0058) — the NetworkPolicy half of
|
|
323
|
+
* the wire's defense; the bearer is the other, and the control (ADR-0013: policy bounds where a pod
|
|
324
|
+
* may talk, only a token bounds what it may do). One policy per placement: every Sandbox pod, by
|
|
325
|
+
* the label the operator stamps on each, and the Instance Harness.
|
|
326
|
+
*
|
|
327
|
+
* Converged UNCONDITIONALLY, not only when a Machine composes a Sandbox or declares a `"none"`
|
|
328
|
+
* Agent: a policy that selects nothing costs nothing, and a pod born before its policy would be a
|
|
329
|
+
* window. The peer is this instance's Orchestrator and no other — a `podSelector` never crosses the
|
|
330
|
+
* namespace, and the instance label pins it within one. What is NOT here, on purpose:
|
|
331
|
+
*
|
|
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.
|
|
334
|
+
* - No egress. What a Sandbox may reach is its own decision (ADR-0058 records it open).
|
|
335
|
+
* - No port. The Orchestrator speaks only the Harness wire to these pods, and a port here would
|
|
336
|
+
* have to track the CR's `port`; `kubectl port-forward` (the CLI, a human's shell into the User
|
|
337
|
+
* Container — ADR-0005) and kubelet probes never cross a policy at all.
|
|
338
|
+
*/
|
|
339
|
+
function harnessIngressPolicies(instance: string, meta: (name: string) => KubeManifest): KubeManifest[] {
|
|
340
|
+
const fromOrchestrator = [
|
|
341
|
+
{ from: [{ podSelector: { matchLabels: { app: ORCHESTRATOR_SERVICE, [LABEL_INSTANCE]: instance } } }] },
|
|
342
|
+
];
|
|
343
|
+
const policy = (name: string, podSelector: Record<string, unknown>): KubeManifest => ({
|
|
344
|
+
apiVersion: "networking.k8s.io/v1",
|
|
345
|
+
kind: "NetworkPolicy",
|
|
346
|
+
metadata: meta(name),
|
|
347
|
+
spec: { podSelector, policyTypes: ["Ingress"], ingress: fromOrchestrator },
|
|
348
|
+
});
|
|
349
|
+
return [
|
|
350
|
+
policy(SANDBOX_INGRESS_POLICY, { matchExpressions: [{ key: SANDBOX_POD_LABEL, operator: "Exists" }] }),
|
|
351
|
+
policy(INSTANCE_HARNESS_INGRESS_POLICY, { matchLabels: { app: INSTANCE_HARNESS_SERVICE } }),
|
|
352
|
+
];
|
|
353
|
+
}
|
|
354
|
+
|
|
312
355
|
/** Where the cache agent's pod sees the node's directory: `--cache-dir`'s default. */
|
|
313
356
|
const REPO_CACHE_MOUNT = "/cache";
|
|
314
357
|
/** The agent's `$HOME` — an emptyDir, where it writes a deploy key for the life of the pod. */
|
|
@@ -461,10 +504,11 @@ export function instanceHarnessObjects(opts: {
|
|
|
461
504
|
harness?: HarnessConfig;
|
|
462
505
|
/** The instance ships a private-CA bundle (ADR-0020): mount `jr2-ca` into the Harness container. */
|
|
463
506
|
caBundle?: boolean;
|
|
464
|
-
/** The
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
|
|
507
|
+
/** The digest of this placement's Harness bearer (ADR-0058; `harnessTokenDigest(key,
|
|
508
|
+
* "jr2-instance-harness")`) — the wire's gate. The digest, never the bearer: the same shape
|
|
509
|
+
* every Sandbox Harness container gets, so this pod can verify the Orchestrator and mint
|
|
510
|
+
* nothing. */
|
|
511
|
+
bearerSha256: string;
|
|
468
512
|
}): string {
|
|
469
513
|
const labels = { [LABEL_INSTANCE]: opts.name, "app.kubernetes.io/managed-by": "jr2" };
|
|
470
514
|
const meta = (): KubeManifest => ({
|
|
@@ -495,15 +539,16 @@ export function instanceHarnessObjects(opts: {
|
|
|
495
539
|
name: "JR2_HARNESS_JSON",
|
|
496
540
|
valueFrom: { configMapKeyRef: { name: HARNESS_CONFIGMAP, key: HARNESS_CONFIG_KEY } },
|
|
497
541
|
},
|
|
498
|
-
// The placement gate (ADR-0031): every admission carries its own definition (ADR-0049)
|
|
499
|
-
// the
|
|
500
|
-
//
|
|
501
|
-
//
|
|
542
|
+
// The placement gate (ADR-0031): every admission carries its own definition (ADR-0049), so
|
|
543
|
+
// the Harness itself refuses any admission whose definition declares Workspace access —
|
|
544
|
+
// Menu-only Agents alone run here, which is what makes "no code execution in this pod" true
|
|
545
|
+
// rather than asserted. The bearer (below) narrows who may try; this decides what.
|
|
502
546
|
{ name: "JR2_MENU_ONLY", value: "1" },
|
|
503
|
-
...(opts.echoTokenSha256 ? [{ name: "JR2_ECHO_TOKEN_SHA256", value: opts.echoTokenSha256 }] : []),
|
|
504
547
|
...(opts.harness?.env ?? []).filter((v) => v.valueFrom !== undefined),
|
|
505
548
|
{ name: "JR2_ADAPTER_URL", value: `http://127.0.0.1:${ADAPTER_PORT}` },
|
|
506
549
|
...(opts.caBundle ? [{ name: "NODE_EXTRA_CA_CERTS", value: `${CA_MOUNT}/ca.crt` }] : []),
|
|
550
|
+
// The wire's gate (ADR-0058), last — as on a Sandbox — so no `harness.env` entry chooses it.
|
|
551
|
+
{ name: "JR2_HARNESS_TOKEN_SHA256", value: opts.bearerSha256 },
|
|
507
552
|
],
|
|
508
553
|
envFrom: [{ secretRef: { name: HARNESS_ENV_SECRET } }, ...(opts.harness?.envFrom ?? [])],
|
|
509
554
|
...(opts.caBundle ? { volumeMounts: [{ name: "ca", mountPath: CA_MOUNT, readOnly: true }] } : {}),
|