@jr2/orchestrator 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.
@@ -40,21 +40,22 @@
40
40
  // initContainer preflight the USER'S image + that volume → ADR-0037's probe, the thing that
41
41
  // proves a registry ref, whose first appearance is this provision
42
42
  // container harness the Sandbox Image, command overridden, /work + /opt/jr2 mounted
43
- // container adapter jr2-owned, the pod's only credential holder (below)
43
+ // container custodian jr2-composed, the pod's only credential holder (below)
44
44
  // container user optional, the image's own entrypoint, the checkouts (/work, plus
45
45
  // /repos and /opt/jr2 read-only) and NOTHING else
46
46
  //
47
- // This is also where the ADAPTER is injected (ADR-0013). The operator needs no change to carry it:
48
- // ADR-0001 made `Sidecars` generic container fragments it schedules WITHOUT understanding, so the
49
- // Adapter is exactly that — a container with an image, an env, and a Secret. What this module
50
- // builds is the pod's asymmetry:
47
+ // This is also where the CUSTODIAN is composed (ADR-0013, ADR-0059). The operator needs no change
48
+ // to carry it: ADR-0001 made `Sidecars` generic container fragments it schedules WITHOUT
49
+ // understanding, so the Custodian is exactly that — a container with an image, its mounts, and a
50
+ // readiness probe the pod's Ready waits on. What this module builds is the pod's asymmetry:
51
51
  //
52
- // harness container JR2_ADAPTER_URL=http://127.0.0.1:8081 (an address, no credential)
53
- // adapter container JR2_ORCHESTRATOR_URL + JR2_SANDBOX_TOKEN (the credential, via envFrom)
52
+ // harness container JR2_CUSTODIAN_URL + the Stand-in of every credential (no credential)
53
+ // custodian container the Sandbox token and every held secret, as files (the credentials)
54
54
  //
55
55
  // The Agent has code execution in the first and none in the second. The token is minted here — a
56
56
  // signed Sandbox name (see tokens.ts), so re-provisioning after a restart yields the SAME token and
57
- // the Secret re-applies as a no-op.
57
+ // the Secret re-applies as a no-op. What else the Custodian holds is `jr2 up`'s resolution, read
58
+ // per provision from the `jr2-held` mount (custodian.ts builds both placements' pods alike).
58
59
 
59
60
  import { execFile } from "node:child_process";
60
61
  import { join } from "node:path";
@@ -65,8 +66,10 @@ import {
65
66
  type HarnessEnvVar,
66
67
  type SandboxPlacement,
67
68
  } from "./config.ts";
69
+ import { custodianComposition, type CustodianComposition } from "./custodian.ts";
70
+ import { readHeldManifest } from "./held-secrets.ts";
68
71
  import { readImageRefs, resolveSandboxImage, resolveUserImage, type ImageRefs } from "./images.ts";
69
- import { CA_CONFIGMAP, IMAGES_KEY, IMAGES_MOUNT, REPOS_MOUNT } from "./names.ts";
72
+ import { HELD_KEY, HELD_MOUNT, IMAGES_KEY, IMAGES_MOUNT, REPOS_MOUNT } from "./names.ts";
70
73
  import { repoIdentity } from "./repo-identity.ts";
71
74
  import type { RepoResources } from "./repos.ts";
72
75
  import { harnessTokenDigest, sandboxToken } from "./tokens.ts";
@@ -75,9 +78,6 @@ import type { ProvisionedRepo, SandboxPort, WorkspaceSpec } from "./workspace.ts
75
78
  /** Run one kubectl invocation to completion. `input` is piped to stdin (`apply -f -`). */
76
79
  export type KubectlExec = (args: string[], opts?: { input?: string }) => Promise<{ stdout: string; stderr: string }>;
77
80
 
78
- /** Where the Harness container sees the instance's CA bundle (ADR-0020). */
79
- const CA_MOUNT = "/etc/jr2/ca";
80
-
81
81
  /** Where jr2's runtime lands in every container that gets it (ADR-0037). `/opt/jr2` and not `/app`
82
82
  * because a stranger's base may already use `/app`, and one layout must serve both the stock
83
83
  * Harness image and an arbitrary Sandbox Image. It is a PUBLISHED surface: `bin/` beside `lib/`
@@ -97,10 +97,6 @@ const HARNESS_COMMAND = [`${RUNTIME_MOUNT}/bin/node`, `${RUNTIME_MOUNT}/src/main
97
97
  * checkouts must hold this volume — which is why the User Container mounts it (ADR-0005). */
98
98
  const UPLOAD_PACK = `${RUNTIME_MOUNT}/bin/jr2-upload-pack`;
99
99
 
100
- /** The Adapter's port on the pod's loopback. The program defaults to this address too, so the
101
- * fetch url names it only when the composition moved it (attachScript). */
102
- const DEFAULT_ADAPTER_PORT = 8081;
103
-
104
100
  /** ADR-0005's default work group. Convention, not config: the pod's `fsGroup` is granted to every
105
101
  * container as a supplemental group, so the Harness writes `/work` whatever the number and no
106
102
  * image's `/etc/group` needs to know it. The one override is the spec's `workGroup`. */
@@ -263,6 +259,10 @@ export type KubectlSandboxOptions = {
263
259
  * off it at invoke time, handed to `provision()` as a string — and resolved against this map at
264
260
  * provision. The per-run spec never names one (ADR-0051). */
265
261
  imagesPath?: string;
262
+ /** The held-secret manifest `jr2 up` resolved (ADR-0059) — read per provision like the image map,
263
+ * so a held-secret edit reaches future Sandboxes without rolling this process. Default: the
264
+ * `jr2-held` ConfigMap's mount. */
265
+ heldPath?: string;
266
266
  /** Extra env for the HARNESS container (`harness.env`) — merged ahead of the
267
267
  * mechanism-owned vars, which win on collision. */
268
268
  env?: HarnessEnvVar[];
@@ -273,20 +273,11 @@ export type KubectlSandboxOptions = {
273
273
  * `sandbox.tolerations`, written on the CR verbatim and copied onto the pod by the operator, which
274
274
  * merges nothing with them. Absent → wherever an ordinary pod lands. */
275
275
  placement?: SandboxPlacement;
276
- /** The instance ships a private-CA bundle (ADR-0020): mount the `jr2-ca` ConfigMap into the
277
- * HARNESS container and point NODE_EXTRA_CA_CERTS at it — never the Adapter, which speaks plain
278
- * HTTP to the Orchestrator's Service (the same asymmetry as env/envFrom above). */
276
+ /** The instance ships a private-CA bundle (ADR-0020): the HARNESS container trusts it, and the
277
+ * Custodian verifies a bound host with it (custodian.ts). */
279
278
  caBundle?: boolean;
280
- /**
281
- * Where the Adapter reaches the Orchestrator FROM INSIDE THE CLUSTER — the orchestrator's own
282
- * Service DNS (derived from JR2_NAMESPACE by the entrypoint). The Agent is never told it.
283
- * A thunk is still accepted for callers that resolve their address late.
284
- */
285
- orchestratorUrl?: string | (() => string | undefined);
286
279
  /** The key Sandbox tokens are signed with — from the instance Secret (ADR-0013/0019). */
287
280
  signingKey?: Buffer;
288
- /** The Adapter's port on the pod's loopback. Default 8081. */
289
- adapterPort?: number;
290
281
  /** Kube namespace for Sandbox CRs. Default `default`. */
291
282
  namespace?: string;
292
283
  /** kubectl `--context` override. Default: the current context (ADR-0009). */
@@ -339,6 +330,7 @@ export type KubectlSandboxOptions = {
339
330
  export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
340
331
  const ns = opts.namespace ?? "default";
341
332
  const imagesPath = opts.imagesPath ?? join(IMAGES_MOUNT, IMAGES_KEY);
333
+ const heldPath = opts.heldPath ?? join(HELD_MOUNT, HELD_KEY);
342
334
  const workRoot = opts.workRoot ?? "/work";
343
335
  const readyTimeoutMs = opts.readyTimeoutMs ?? 120_000;
344
336
  const repoTimeoutMs = opts.repoTimeoutMs ?? 25 * 60_000;
@@ -346,32 +338,12 @@ export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
346
338
  const exec = opts.exec ?? defaultKubectlExec;
347
339
  const credentials = opts.credentials ?? [];
348
340
 
349
- const adapterPort = opts.adapterPort ?? DEFAULT_ADAPTER_PORT;
350
341
  const leaseIntervalMs = opts.leaseIntervalMs ?? 5 * 60_000;
351
342
  const base = ["--namespace", ns, ...(opts.context ? ["--context", opts.context] : [])];
352
343
 
353
- /** The Sandbox's token Secret — read by the Adapter container, and by nothing else in the pod. */
344
+ /** The Sandbox's token Secret — mounted into the Custodian container, and nothing else in the pod. */
354
345
  const secretName = (name: string) => `${name}-token`;
355
346
 
356
- /** Resolved at provision time (see the option's doc): the Orchestrator's in-cluster address. */
357
- const orchestratorUrl = (): string | undefined =>
358
- typeof opts.orchestratorUrl === "function" ? opts.orchestratorUrl() : opts.orchestratorUrl;
359
-
360
- /** The Adapter, as the operator sees it: an opaque container fragment (ADR-0001). */
361
- const adapterSidecar = (name: string, refs: ImageRefs) => ({
362
- name: "adapter",
363
- image: refs.adapter,
364
- env: [
365
- { name: "JR2_ORCHESTRATOR_URL", value: orchestratorUrl() },
366
- { name: "JR2_SANDBOX", value: name },
367
- { name: "JR2_ADAPTER_PORT", value: String(adapterPort) },
368
- ],
369
- // The credential, and the reason this is a separate container: `local()` tools give the Agent
370
- // code execution in the HARNESS container, so anything mounted there is the Agent's. Here, it
371
- // is out of reach — different container, no shared process namespace.
372
- envFrom: [{ secretRef: { name: secretName(name) } }],
373
- });
374
-
375
347
  /**
376
348
  * The User Container (ADR-0005): the opt-in third seat, composed only when the wrapper's static
377
349
  * `user` option names an image (ADR-0049). The ZERO-CONTRACT seat — jr2 injects nothing, probes
@@ -395,8 +367,8 @@ export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
395
367
  * Agent, with no credential of their own. Nothing else follows it in: `ext::` names the program
396
368
  * by absolute path, so this seat still gets no env, no command, and no probe. The repo volumes
397
369
  * are the operator's — it defines `repo-<key>` for every key the CR names — so this seat mounts
398
- * them by name. The Adapter is deliberately not given any of the three: it reads no worktree, and it is the
399
- * container holding the pod's only credential, so it gets the narrowest mount set that works.
370
+ * them by name. The Custodian is deliberately not given any of the three: it reads no worktree, and it is the
371
+ * container holding the pod's credentials, so it gets the narrowest mount set that works.
400
372
  * It also carries no `securityContext`, which the operator reads as the exemption — root is
401
373
  * ALLOWED here, because hardening a seat whose identity is "what jr2 does not own" is an opinion,
402
374
  * and the standard managed-access shape (a root sshd that setuids sessions down) must run
@@ -413,30 +385,28 @@ export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
413
385
  });
414
386
 
415
387
  /** The pod's sidecar list (ADR-0001: opaque fragments the operator schedules verbatim). The
416
- * Adapter is ALWAYS here: with its ref in the image map there is no "no adapter configured"
417
- * state left to branch on, and a Sandbox without one is a pod that comes up Ready and then parks
418
- * its Machine forever on a tool call it cannot make (ADR-0013). A map with no `adapter` fails the
419
- * read instead (images.ts). The User Container joins it only when the spec named one. */
420
- const sidecarsFor = async (name: string, refs: ImageRefs, keys: string[], user?: string) => [
421
- adapterSidecar(name, refs),
388
+ * Custodian is ALWAYS here: a Sandbox without one is a pod that comes up Ready and then parks its
389
+ * Machine forever on a Menu it cannot read (ADR-0013). The User Container joins it only when the
390
+ * spec named one. */
391
+ const sidecarsFor = async (custodian: CustodianComposition, refs: ImageRefs, keys: string[], user?: string) => [
392
+ custodian.custodianContainer,
422
393
  ...(user !== undefined ? [await userSidecar(refs, user, keys)] : []),
423
394
  ];
424
395
 
425
396
  // The Harness container's env: the instance's passthrough (`harness.env` — e.g. model
426
- // config) first, then the mechanism-owned vars (the Adapter address, the CA trust path, the
427
- // wire's gate), which win on collision. Note the asymmetry stands (ADR-0013): user env/envFrom
428
- // land on the HARNESS container only — never on the Adapter, whose env is minted here and
429
- // carries the pod's only credential.
397
+ // config) first, then the mechanism-owned vars (the Custodian's address and every Stand-in, the
398
+ // proxy and the trust bundles — custodian.ts), which win on collision. Note the asymmetry stands
399
+ // (ADR-0013): user env/envFrom land on the HARNESS container only — never on the Custodian, whose
400
+ // mounts are composed here and carry the pod's credentials.
430
401
  //
431
402
  // The gate rides LAST (ADR-0058): the digest of the bearer the Orchestrator derives for THIS
432
403
  // Sandbox. A digest because the Agent reads this env; last because a `harness.env` entry of the
433
404
  // same name must not be able to choose the Harness's credential.
434
- const harnessEnv = (name: string): HarnessEnvVar[] => {
405
+ const harnessEnv = (name: string, custodian: CustodianComposition): HarnessEnvVar[] => {
435
406
  if (!opts.signingKey) throw new Error("kubectlSandbox: a Harness needs a signingKey to check its bearer");
436
407
  return [
437
408
  ...(opts.env ?? []),
438
- { name: "JR2_ADAPTER_URL", value: `http://127.0.0.1:${adapterPort}` },
439
- ...(opts.caBundle ? [{ name: "NODE_EXTRA_CA_CERTS", value: `${CA_MOUNT}/ca.crt` }] : []),
409
+ ...custodian.harnessEnv,
440
410
  { name: "JR2_HARNESS_TOKEN_SHA256", value: harnessTokenDigest(opts.signingKey, name) },
441
411
  ];
442
412
  };
@@ -531,10 +501,11 @@ export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
531
501
  req: { name: string; runId: string; workflow: string; image?: string; user?: string; workGroup?: number },
532
502
  refs: ImageRefs,
533
503
  repos: FencedRepo[],
504
+ custodian: CustodianComposition,
534
505
  ) => {
535
506
  const seat = await seatFor(refs, req.image);
536
507
  const sidecars = await sidecarsFor(
537
- req.name,
508
+ custodian,
538
509
  refs,
539
510
  repos.map((r) => r.key),
540
511
  req.user,
@@ -577,14 +548,13 @@ export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
577
548
  fsGroup: req.workGroup ?? DEFAULT_WORK_GROUP,
578
549
  // Ordered, and before any container starts: populate `/opt/jr2`, then prove the image on it.
579
550
  initContainers: initContainersFor(refs, seat),
580
- // Never empty any more: JR2_ADAPTER_URL is unconditional, so the "omit an empty env" branch
581
- // this used to carry was unreachable. The seat's own vars (the fallback `HOME`) come
582
- // FIRST, so the instance's `harness.env` can still override them the way it overrides
583
- // anything the image set.
584
- env: [...seat.env, ...harnessEnv(req.name)],
551
+ // Never empty: the Custodian's address is unconditional. The seat's own vars (the fallback
552
+ // `HOME`) come FIRST, so the instance's `harness.env` can still override them the way it
553
+ // overrides anything the image set.
554
+ env: [...seat.env, ...harnessEnv(req.name, custodian)],
585
555
  ...(opts.envFrom?.length ? { envFrom: opts.envFrom } : {}),
586
- // What the AGENT gets: an address on its own loopback, and no credential anywhere. This is
587
- // the only thing in the pod that tells it how to reach its Machine (ADR-0013).
556
+ // What the AGENT gets: an address on its own loopback and a Stand-in for every credential,
557
+ // and no credential anywhere. The Custodian beside it holds them (ADR-0013, ADR-0059).
588
558
  sidecars,
589
559
  // The Repos this Sandbox attaches, by cache key (ADR-0051). The operator does the rest: a
590
560
  // `repo-<key>` volume per entry — the node's cache, hostPath, read-only in the primary
@@ -607,18 +577,19 @@ export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
607
577
  { name: "runtime", emptyDir: {} },
608
578
  // Only for ADR-0037's fallback seat: uid 1000 on a stranger's base has no home at all.
609
579
  ...seat.homeVolume,
610
- ...(opts.caBundle ? [{ name: "ca", configMap: { name: CA_CONFIGMAP } }] : []),
580
+ // The Custodian's (its values, leaves and config), and the trust ConfigMap (ADR-0020).
581
+ ...custodian.volumes,
611
582
  ],
612
- // CR-level volumeMounts land on the HARNESS container only (the operator's contract) —
613
- // exactly the CA-trust asymmetry ADR-0020 wants: the Adapter never inherits it. The Repo
614
- // caches are not listed: the operator mounts each `repo-<key>` into this container itself.
583
+ // CR-level volumeMounts land on the HARNESS container only (the operator's contract): the
584
+ // trust bundles, and none of the Custodian's volumes. The Repo caches are not listed: the
585
+ // operator mounts each `repo-<key>` into this container itself.
615
586
  volumeMounts: [
616
587
  { name: "work", mountPath: workRoot },
617
588
  // Read-only: nothing writes under `/opt/jr2` at runtime, and the Agent has code execution
618
589
  // in this container — leaving its own runtime writable would let a turn edit it.
619
590
  { name: "runtime", mountPath: RUNTIME_MOUNT, readOnly: true },
620
591
  ...seat.homeMount,
621
- ...(opts.caBundle ? [{ name: "ca", mountPath: CA_MOUNT, readOnly: true }] : []),
592
+ ...custodian.harnessMounts,
622
593
  ],
623
594
  },
624
595
  };
@@ -676,23 +647,13 @@ export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
676
647
 
677
648
  /**
678
649
  * Mint this Sandbox's token into a Secret, BEFORE the CR exists — the operator creates the pod
679
- * the moment it sees the CR, and a pod whose `envFrom` names an absent Secret sits in
680
- * CreateContainerConfigError. Idempotent by construction: the token is the Sandbox's name, signed
681
- * (tokens.ts), so a re-provision after an orchestrator restart re-applies the SAME value, and the
682
- * Adapter that has been holding it all along stays valid.
650
+ * the moment it sees the CR, and a pod whose volume names an absent Secret never starts.
651
+ * Idempotent by construction: the token is the Sandbox's name, signed (tokens.ts), so a
652
+ * re-provision after an orchestrator restart re-applies the SAME value, and the Custodian that has
653
+ * been holding it all along stays valid.
683
654
  */
684
655
  const applyTokenSecret = async (name: string): Promise<void> => {
685
- if (!opts.signingKey) throw new Error("kubectlSandbox: an Adapter needs a signingKey to mint its Sandbox token");
686
- // Fail the provision rather than ship an Adapter that cannot reach the Orchestrator. A mute
687
- // Adapter is the worst possible outcome: the pod comes up Ready, the Agent is admitted, its
688
- // tool call dies on `localhost`, and the Machine simply parks forever — a hang with no error.
689
- if (!orchestratorUrl()) {
690
- throw new Error(
691
- "kubectlSandbox: the Adapter has no route to the Orchestrator (no orchestratorUrl — deployed " +
692
- "instances derive Service DNS from JR2_NAMESPACE). An Agent with no Adapter cannot drive its " +
693
- "Machine at all (ADR-0013).",
694
- );
695
- }
656
+ if (!opts.signingKey) throw new Error("kubectlSandbox: a Custodian needs a signingKey to mint its Sandbox token");
696
657
  const secret = {
697
658
  apiVersion: "v1",
698
659
  kind: "Secret",
@@ -783,7 +744,17 @@ export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
783
744
  // reaches future Sandboxes without rolling the Orchestrator. Reading before the Secret apply
784
745
  // also means an unknown image name costs nothing: no Secret, no CR, nothing to clean up.
785
746
  const refs = await readImageRefs(imagesPath);
786
- const cr = await crFor(req, refs, repos);
747
+ // The same stance for what the Custodian holds (ADR-0059): `jr2 up` resolved it host-side,
748
+ // where `.env` exists, and this process reads the result — never its own config, whose
749
+ // `.env` values are absent in-cluster.
750
+ const manifest = await readHeldManifest(heldPath);
751
+ const custodian = custodianComposition(manifest, {
752
+ image: refs.custodian,
753
+ token: { secret: secretName(req.name), key: "JR2_SANDBOX_TOKEN" },
754
+ sandbox: req.name,
755
+ caBundle: opts.caBundle === true,
756
+ });
757
+ const cr = await crFor(req, refs, repos, custodian);
787
758
 
788
759
  // The Repo resources, BEFORE the CR names them (ADR-0051): a bound one already exists from
789
760
  // the boot and only its eviction clock moves — this run's spelling never rewrites the spec
@@ -794,7 +765,7 @@ export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
794
765
  // a Sandbox whose Repo could not be recorded.
795
766
  for (const repo of repos) await opts.repos.ensure(repo);
796
767
 
797
- await applyTokenSecret(req.name); // before the CR: the pod's Adapter mounts it at start
768
+ await applyTokenSecret(req.name); // before the CR: the pod's Custodian mounts it at start
798
769
  await exec(["apply", ...base, "-f", "-"], { input: JSON.stringify(cr) });
799
770
 
800
771
  const applied = Date.now();
@@ -868,12 +839,7 @@ export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
868
839
  },
869
840
 
870
841
  async attach(req) {
871
- const { script, repos, review } = attachScript(req.spec, req.repos, {
872
- reposMount: REPOS_MOUNT,
873
- workRoot,
874
- // The fetch url names the Adapter only when it is somewhere unexpected (ADR-0053).
875
- ...(adapterPort !== DEFAULT_ADAPTER_PORT ? { adapterUrl: `http://127.0.0.1:${adapterPort}` } : {}),
876
- });
842
+ const { script, repos, review } = attachScript(req.spec, req.repos, { reposMount: REPOS_MOUNT, workRoot });
877
843
  // `-c harness` is unchanged and still correct after ADR-0037: the primary container runs the
878
844
  // Sandbox Image, so `git` here is the git the user chose. Never `-c user` — that seat is
879
845
  // zero-contract, may hold no git at all, and jr2 commands nothing in it (ADR-0005).
@@ -1002,7 +968,7 @@ function staleSlots(
1002
968
  export function attachScript(
1003
969
  spec: WorkspaceSpec,
1004
970
  repos: Array<{ slot: string; url: string; ref?: string }>,
1005
- paths: { reposMount: string; workRoot: string; adapterUrl?: string },
971
+ paths: { reposMount: string; workRoot: string },
1006
972
  ): { script: string; repos: Record<string, string>; review?: Record<string, string> } {
1007
973
  const worktrees: Record<string, string> = {};
1008
974
  const review: Record<string, string> = {};
@@ -1048,7 +1014,7 @@ export function attachScript(
1048
1014
  // (ADR-0051). Push still succeeds only with a caller-supplied credential (a forwarded agent
1049
1015
  // in the User Container); the pod itself holds none. `--` keeps the url an operand, never an
1050
1016
  // option.
1051
- `git -C ${sq(dflt)} remote set-url origin -- ${sq(fetchUrl(identity, paths.adapterUrl))}`,
1017
+ `git -C ${sq(dflt)} remote set-url origin -- ${sq(fetchUrl(identity))}`,
1052
1018
  `git -C ${sq(dflt)} remote set-url --push origin -- ${sq(repo.url)}`,
1053
1019
  // `ext` is on git's own "known scary" list, so its built-in default is `never` and the url
1054
1020
  // above would die with `fatal: transport 'ext' not allowed` before the program ever ran.
@@ -1086,13 +1052,11 @@ export function attachScript(
1086
1052
 
1087
1053
  /**
1088
1054
  * `origin`'s fetch url for one Repo (ADR-0053): the `ext::` transport, the program's absolute path
1089
- * on the runtime volume, the service git asks for, and the Repo's identity. The Adapter's address
1090
- * rides as a third argument only when the composition moved the Adapter off its default port —
1091
- * the program reads `$JR2_ADAPTER_URL` and then falls back to that same address, so spelling it out
1092
- * unconditionally would put a number in every `git remote -v` that says nothing.
1055
+ * on the runtime volume, the service git asks for, and the Repo's identity. No address: the
1056
+ * program asks the Custodian at its one loopback address, which every Harness pod has (ADR-0059).
1093
1057
  */
1094
- function fetchUrl(identity: string, adapterUrl?: string): string {
1095
- return `ext::${UPLOAD_PACK} %S ${extArg(identity)}${adapterUrl !== undefined ? ` ${extArg(adapterUrl)}` : ""}`;
1058
+ function fetchUrl(identity: string): string {
1059
+ return `ext::${UPLOAD_PACK} %S ${extArg(identity)}`;
1096
1060
  }
1097
1061
 
1098
1062
  /**
package/src/server.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  // JR2_SIGNING_KEY base64 key Sandbox tokens are signed with; from the Secret so live Sandboxes
12
12
  // survive a pod restart. Absent → minted into `<dir>/.jr2/secret` (dev-grade).
13
13
  // JR2_NAMESPACE the pod's own namespace (Deployment fieldRef) — presence = "deployed":
14
- // Sandboxes are driven in it, and the Adapters' route home is Service DNS.
14
+ // Sandboxes are driven in it, and the Custodians' route home is Service DNS.
15
15
  // <git.credentials[].token>
16
16
  // each token env var the config names rides the instance Secret (ADR-0051):
17
17
  // the Orchestrator materializes it into the Repo's credential Secret.
@@ -36,7 +36,6 @@ import {
36
36
  IMAGES_MOUNT,
37
37
  INSTANCE_HARNESS_PORT,
38
38
  INSTANCE_HARNESS_SERVICE,
39
- ORCHESTRATOR_SERVICE,
40
39
  } from "./names.ts";
41
40
  import { partsOf, type CarriedRepo } from "./parts.ts";
42
41
  import { kubectlRepoFetches, type RepoFetches } from "./repo-fetch.ts";
@@ -65,8 +64,9 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
65
64
  const signingKey =
66
65
  env.JR2_SIGNING_KEY !== undefined ? Buffer.from(env.JR2_SIGNING_KEY, "base64") : await loadSigningKey(opts.dir);
67
66
 
68
- // Deployed (JR2_NAMESPACE set): Adapters dial the orchestrator at its own Service DNS — stable
69
- // by nature, which is what lets live Sandboxes outlive orchestrator restarts (ADR-0013).
67
+ // Deployed (JR2_NAMESPACE set): Custodians dial the orchestrator at its own Service DNS — stable
68
+ // by nature, which is what lets live Sandboxes outlive orchestrator restarts (ADR-0013). `jr2 up`
69
+ // writes that address into the Custodian's bootstrap; nothing here passes it on.
70
70
  const namespace = env.JR2_NAMESPACE;
71
71
 
72
72
  // From the instance Secret when deployed; per-boot for a host-booted fixture.
@@ -128,7 +128,6 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
128
128
  caBundle: config?.harness?.caBundle !== undefined,
129
129
  // Which nodes are Sandbox nodes (ADR-0052) — the CR carries it, the operator reads no config.
130
130
  ...(config?.sandbox ? { placement: config.sandbox } : {}),
131
- orchestratorUrl: `http://${ORCHESTRATOR_SERVICE}.${namespace}.svc:${port}`,
132
131
  signingKey,
133
132
  namespace,
134
133
  ...(opts.exec ? { exec: opts.exec } : {}),
package/src/tokens.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Bearer tokens (ADR-0013), because the Sandbox boundary is otherwise theater. The Harness
2
2
  // container shares the pod's network namespace, so an Agent can reach the Orchestrator directly;
3
- // a per-pod NetworkPolicy cannot distinguish its packets from the Adapter's. Only authentication
3
+ // a per-pod NetworkPolicy cannot distinguish its packets from the Custodian's. Only authentication
4
4
  // closes this, and only the token bounds what a caller may DO (as opposed to where it may talk).
5
5
  //
6
6
  // Two principals, and the asymmetry between them is the whole design:
@@ -10,15 +10,16 @@
10
10
  // announced once on stdout (fixtures capture it). An Agent never has it — it
11
11
  // never enters a Sandbox.
12
12
  //
13
- // Sandbox token the Adapter's credential. Authorizes exactly: deliver to `kind: "agent"`
13
+ // Sandbox token the Custodian's credential. Authorizes exactly: deliver to `kind: "agent"`
14
14
  // registrations whose Sandbox is THIS one. Never a Gate (a compromised Agent
15
15
  // must not approve its own review), never another Sandbox (`coding.ts`'s iids
16
16
  // are derivable and feature ids are readable from the Work Source, so a merely
17
17
  // run-scoped token would let one feature's coder inject a verdict into another
18
- // feature's reviewer). Delivered as a Secret via the CR's `envFrom` into the
19
- // Adapter container ALONE, which is the one place the Agent cannot read.
18
+ // feature's reviewer). Delivered as a Secret mounted into the Custodian
19
+ // container ALONE, which the Agent cannot read; the Harness holds its
20
+ // Stand-in (ADR-0059).
20
21
  // The signed name is a POD hosting Turns, not a Sandbox CR per se: the Instance
21
- // Harness's Adapter bears one signed for that placement's Service name
22
+ // Harness's Custodian holds one signed for that placement's Service name
22
23
  // (ADR-0031), scoping it to the Menu-only registrations placed there.
23
24
  //
24
25
  // And one bearer the other way (ADR-0058), which is no principal here because this server never
@@ -32,7 +33,7 @@
32
33
  // verified by recomputing. Three things fall out that a token table would have to work for — the
33
34
  // Orchestrator holds no per-Sandbox state, `provision` stays idempotent (re-minting yields the same
34
35
  // token, so a re-applied Secret is a no-op), and a token minted before a restart still verifies
35
- // after one, which is exactly what ADR-0012's re-attach promises the still-running Adapter.
36
+ // after one, which is exactly what ADR-0012's re-attach promises the still-running Custodian.
36
37
 
37
38
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
38
39
  import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
@@ -42,7 +43,7 @@ import { join } from "node:path";
42
43
  export type Principal =
43
44
  /** The instance's own operator: the CLI, a human, a webhook translator holding the token. */
44
45
  | { kind: "instance" }
45
- /** One Sandbox's Adapter, speaking for the Agent in that pod — and for no one else. */
46
+ /** One Sandbox's Custodian, speaking for the Agent in that pod — and for no one else. */
46
47
  | { kind: "sandbox"; sandbox: string };
47
48
 
48
49
  /** Resolve a bearer token to a principal; undefined = not a token we minted (→ 401). */
@@ -85,7 +86,7 @@ export function mintInstanceToken(): string {
85
86
  * The instance's HMAC signing key, at `<dir>/.jr2/secret` (0600), created on first use.
86
87
  *
87
88
  * It MUST outlive the process: an Orchestrator restart leaves live Sandboxes running (ADR-0012 re-attach),
88
- * and their Adapters still hold tokens minted by the process that died. A fresh key would reject
89
+ * and their Custodians still hold tokens minted by the process that died. A fresh key would reject
89
90
  * every one of them — the Agent would silently lose its only route to its Machine.
90
91
  */
91
92
  export async function loadSigningKey(dir: string): Promise<Buffer> {
package/src/workspace.ts CHANGED
@@ -805,7 +805,7 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
805
805
  endpoint: ctx.endpoint!,
806
806
  // Derived, not remembered: the same function every port operation names the CR
807
807
  // with, so the Sandbox the registrar publishes — and the Agent actor records on its
808
- // registration — is the one the Adapter's token is scoped to, by construction
808
+ // registration — is the one the Custodian's token is scoped to, by construction
809
809
  // (ADR-0013).
810
810
  sandbox: workspaceName(runBindingOf(system as AnyActorSystem).runId, ctx.wsId),
811
811
  repos: out.repos,