@jr2/orchestrator 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/src/http.ts CHANGED
@@ -2,15 +2,16 @@
2
2
  // carries NO domain logic of its own: every handler delegates to a single `RunHost` method, so the
3
3
  // wire shape and the in-process API stay one behavior.
4
4
  //
5
- // TWO dialects, one primitive (ADR-0013). The Orchestrator does not speak MCP — that moved into the
6
- // Sandbox, where the Adapter serves it to the Agent over localhost. What is left here are two thin
7
- // adapters over the same registration table:
5
+ // TWO dialects, one primitive (ADR-0013). The Orchestrator does not speak MCP — nothing does: the
6
+ // Harness presents the Menu to its model itself, and reaches these routes through the Custodian in
7
+ // its pod, which holds the Sandbox token. What is left here are two thin adapters over the same
8
+ // registration table:
8
9
  //
9
10
  // # human / webhook / CI — the Gate resource of ADR-0011
10
11
  // GET /runs/:id open gates: accepts + schemas + meta [Instance token]
11
12
  // POST /runs/:id/gates/:gate/events validate + deliver [Instance token]
12
13
  //
13
- // # the Agent's Adapter, and nothing else
14
+ // # the Agent's Harness, through its Custodian, and nothing else
14
15
  // GET /agents/:iid/surface accepts + schemas + semantics [Sandbox token]
15
16
  // POST /agents/:iid/events validate + deliver → the turn receipt [Sandbox token]
16
17
  // POST /sandboxes/:name/fetch ask the node cache to fetch one Repo [Sandbox token]
@@ -27,9 +28,10 @@
27
28
  // control.
28
29
  // authenticated any principal we minted a token for. The Agent's surface lives here, scoped
29
30
  // further per-registration by `mayDeliverToAgent`.
30
- // instanceOnly the Instance token ALONE. Run control and full run state (`/runs*`): a Sandbox
31
- // token authenticates but is refused, because reading another feature's context or
32
- // cancelling a run is not on the Agent's surface any more than a Gate is.
31
+ // instanceOnly the Instance token ALONE. Run control — starting a run included (ADR-0058) — and
32
+ // full run state (`/runs*`): a Sandbox token authenticates but is refused, because
33
+ // starting work, reading another feature's context, or cancelling a run is not on
34
+ // the Agent's surface any more than a Gate is.
33
35
 
34
36
  import { readFile, stat } from "node:fs/promises";
35
37
  import { createRequire } from "node:module";
@@ -179,7 +181,7 @@ async function consoleTsAsset(file: string): Promise<Response> {
179
181
  // Content negotiation for the two page addresses (ADR-0032). JSON is the DEFAULT dialect: only a
180
182
  // request whose `Accept` prefers `text/html` — a browser's navigation — gets the Console shell.
181
183
  // No header, a bare wildcard, `application/json`, `text/event-stream` all fall through to JSON,
182
- // so the CLI, the Adapter and EventSource never see HTML they did not ask for.
184
+ // so the CLI, the Harness and EventSource never see HTML they did not ask for.
183
185
  function prefersHtml(c: Context): boolean {
184
186
  return (
185
187
  accepts(c, { header: "Accept", supports: ["application/json", "text/html"], default: "application/json" }) ===
@@ -300,8 +302,8 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
300
302
 
301
303
  /**
302
304
  * Authenticate, AND require the INSTANCE token (ADR-0014). A Sandbox token is a token we minted,
303
- * so `authenticated` alone lets it through — which on the run surface is too much: it would let an
304
- * Adapter's credential read every run's context (other features' branches, tickets, verdicts) and
305
+ * so `authenticated` alone lets it through — which on the run surface is too much: it would let a
306
+ * Custodian's credential read every run's context (other features' branches, tickets, verdicts) and
305
307
  * CANCEL any run. Neither is on the Agent's surface. Its scope is delivering to agent
306
308
  * registrations recorded against its OWN Sandbox (ADR-0013), and the gate route already says so in
307
309
  * the other direction.
@@ -356,8 +358,10 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
356
358
  // Push work: start a run of a registered workflow. Unknown workflow → host.start throws → 404.
357
359
  // A body failing the machine's declared input schema (ADR-0033) → 400 naming the accepted
358
360
  // shape — the same error class, and the same wire mapping, as a gate delivery failing its
359
- // schema (`POST /runs/:id/gates/:gate/events` below).
360
- app.post("/workflows/:name/runs", authenticated, async (c) => {
361
+ // schema (`POST /runs/:id/gates/:gate/events` below). Instance-band (ADR-0058): starting work is
362
+ // run control, and a Sandbox token's scope is its own agent surface — the Custodian's route
363
+ // allowlist not forwarding this route is a second layer, not the control (ADR-0059).
364
+ app.post("/workflows/:name/runs", instanceOnly, async (c) => {
361
365
  const name = c.req.param("name");
362
366
  const input = await readJson(c.req.text());
363
367
  try {
@@ -668,10 +672,12 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
668
672
  });
669
673
 
670
674
  // ---- The Agent's surface (`/agents/:iid/*` — ADR-0013) --------------------------------------
671
- // Served to ONE caller: the Adapter in the Agent's Sandbox. It renders `surface` as `tools/list`
672
- // and turns a `tools/call` into an `events` POST. The Orchestrator therefore keeps no MCP
673
- // dependency, no transport, no session handling — and the Agent keeps no route to this API
674
- // except through a process whose credential it cannot read.
675
+ // Served to ONE caller: the Agent's Harness, through the Custodian in its pod. The Harness renders
676
+ // `surface` as the Menu and turns a pick into an `events` POST. The Orchestrator therefore keeps
677
+ // no MCP dependency, no transport, no session handling — and the Agent keeps no route to this
678
+ // API except through a container whose credential it cannot read. Everything a pick is judged by
679
+ // is judged HERE: the token's scope, the live registration, the event's name and payload — the
680
+ // Custodian forwards and never decides (ADR-0059).
675
681
  //
676
682
  // Both routes are adapters over the SAME registration table the gates ride: lookup, validation
677
683
  // and delivery are implemented once, in `registration.ts`.
@@ -693,7 +699,7 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
693
699
  };
694
700
 
695
701
  // This turn's menu: the events the invoking state accepts, their input schemas, their semantics.
696
- // A transition swaps the registration, which swaps this — so the Adapter gets a state-scoped
702
+ // A transition swaps the registration, which swaps this — so the Harness gets a state-scoped
697
703
  // toolset for free, and needs no `list_changed` to know it (flue re-lists on every submission).
698
704
  app.get("/agents/:instanceId/surface", authenticated, (c) => {
699
705
  const { surface, error } = agentRegistration(c);
@@ -701,7 +707,7 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
701
707
  });
702
708
 
703
709
  // The Agent's pick, delivered into the state that invoked it. The receipt describes itself
704
- // (ADR-0024): what was delivered, and whether that ended the turn — which the Adapter renders as
710
+ // (ADR-0024): what was delivered, and whether that ended the turn — which the Harness renders as
705
711
  // prose. Its `deliveryId` still makes an outcome addressable after the fact, the room a deferred
706
712
  // result will need when it lands.
707
713
  app.post("/agents/:instanceId/events", authenticated, async (c) => {
@@ -718,7 +724,7 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
718
724
  });
719
725
 
720
726
  // The pod's one route out (ADR-0053). A `git fetch` inside a Sandbox runs a program on the
721
- // runtime volume, which asks the Adapter on localhost, which forwards to this. What lands here
727
+ // runtime volume, which asks the Custodian on localhost, which forwards to this. What lands here
722
728
  // is one identity; what goes back is the landing, or the cache as it stands with git's own words
723
729
  // — freshness degrades, absence does not, so this answers 200 either way and the program prints
724
730
  // the warning. The caller waits on the ask the way an attach waits on Ready: same status, same
package/src/images.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // not folded into sandbox-kubectl.ts, because the CLI needs the type without dragging in the
5
5
  // kubectl port.
6
6
  //
7
- // The map is NESTED, never flat. The kit's own `harness`/`adapter` refs sit beside a `sandbox`
7
+ // The map is NESTED, never flat. The kit's own `harness` and the `custodian` sit beside a `sandbox`
8
8
  // sub-map, so a user image can never shadow them — and `default` is the one reserved key in that
9
9
  // sub-map, because ADR-0037's fallback chain is built on it.
10
10
  //
@@ -26,8 +26,9 @@ import { join, relative } from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
27
 
28
28
  /**
29
- * Every image ref one converge resolved. `harness`/`adapter` are the kit's own (built from a kit
30
- * checkout, or the published `<kitversion>` tags); `sandbox` is the instance's own docker-context
29
+ * Every image ref one converge resolved. `harness` is the kit's own (built from a kit checkout, or
30
+ * the published `<kitversion>` tag); `custodian` is the pinned Envoy the kit deploys and never
31
+ * builds (ADR-0038, ADR-0059); `sandbox` is the instance's own docker-context
31
32
  * builds — one `docker build` straight to a content tag, carrying no jr2 layers at all, because the
32
33
  * Harness arrives at POD time on the `/opt/jr2` volume (ADR-0037).
33
34
  *
@@ -38,9 +39,10 @@ import { fileURLToPath } from "node:url";
38
39
  export type ImageRefs = {
39
40
  /** The stock Harness image — also the last leg of the Sandbox Image fallback chain. */
40
41
  harness: string;
41
- /** The Adapter image (ADR-0013). An Agent with no Adapter cannot drive its Machine at all, so
42
+ /** The Custodian's image (ADR-0059) — upstream Envoy, pinned by digest, re-homed onto a
43
+ * `kitRegistry` when one is set. Every Harness pod runs one (it carries the Menu, ADR-0013), so
42
44
  * this is required: a map without it fails the provision rather than shipping a mute pod. */
43
- adapter: string;
45
+ custodian: string;
44
46
  /** Built Sandbox Images by their build context's CONTENT DIGEST (ADR-0049), plus the reserved
45
47
  * key `default` — the Instance's `images/default`, ADR-0037's middle leg. Empty when the
46
48
  * instance ships no context and scaffolded no default. */
@@ -99,10 +101,9 @@ export async function readImageRefs(path: string): Promise<ImageRefs> {
99
101
  return value as string;
100
102
  };
101
103
  const harness = ref("harness");
102
- // Required, not optional: with the ref living in the map there is no "no adapter configured"
103
- // branch left to gate on, and an Agent whose pod has no Adapter parks forever on a tool call it
104
- // cannot make (ADR-0013). Failing the provision is the only honest outcome.
105
- const adapter = ref("adapter");
104
+ // Required, not optional: an Agent whose pod has no Custodian parks forever on a Menu it cannot
105
+ // read (ADR-0013). Failing the provision is the only honest outcome.
106
+ const custodian = ref("custodian");
106
107
  const sandboxRaw = map["sandbox"];
107
108
  if (sandboxRaw !== undefined && (typeof sandboxRaw !== "object" || sandboxRaw === null || Array.isArray(sandboxRaw)))
108
109
  bad("`sandbox` must be an object of name → ref");
@@ -121,7 +122,7 @@ export async function readImageRefs(path: string): Promise<ImageRefs> {
121
122
  if (typeof value !== "string") bad(`\`sandboxUser.${name}\` is not a USER (got ${JSON.stringify(value)})`);
122
123
  sandboxUser[name] = value as string;
123
124
  }
124
- return { harness, adapter, sandbox, sandboxUser };
125
+ return { harness, custodian, sandbox, sandboxUser };
125
126
  }
126
127
 
127
128
  /**
package/src/index.ts CHANGED
@@ -2,8 +2,9 @@
2
2
  // delivery surfaces, the duplex Actor over the Harness wire, and durable snapshot persistence
3
3
  // (ADR-0002/0006/0007). Builds on the `@jr2/agent-protocol` wire contract.
4
4
  //
5
- // It does NOT speak MCP (ADR-0013): the Agent's MCP surface is hosted by the Adapter, in the
6
- // Sandbox. What lives here is the registration table and two thin HTTP adapters over it.
5
+ // It does NOT speak MCP (ADR-0013): the Harness presents the Menu to its model itself, and reaches
6
+ // the surface here through the Custodian in its pod. What lives here is the registration table and
7
+ // two thin HTTP adapters over it — plus what composes a Custodian (ADR-0059).
7
8
 
8
9
  // The wire contract, re-exported: a workflow authors against ONE package (`defineEvent`,
9
10
  // `jr2Setup`, `workspace`, … all import from "@jr2/orchestrator" — ADR-0015).
@@ -24,6 +25,9 @@ export * from "./actor.ts";
24
25
  export * from "./gate.ts";
25
26
  export * from "./workspace.ts";
26
27
  export * from "./images.ts";
28
+ export * from "./model-keys.ts";
29
+ export * from "./held-secrets.ts";
30
+ export * from "./custodian.ts";
27
31
  export * from "./sandbox-kubectl.ts";
28
32
  export * from "./repos.ts";
29
33
  export * from "./repo-fetch.ts";
package/src/instance.ts CHANGED
@@ -29,7 +29,7 @@ import { RunHost } from "./run-host.ts";
29
29
  import type { RunRecord } from "./run-host.ts";
30
30
  import { SqliteSnapshotStore } from "./snapshot-store.ts";
31
31
  import type { SnapshotStore } from "./snapshot-store.ts";
32
- import { createAuthenticator, loadSigningKey, mintInstanceToken } from "./tokens.ts";
32
+ import { createAuthenticator, harnessToken, loadSigningKey, mintInstanceToken } from "./tokens.ts";
33
33
  import type { SandboxPort } from "./workspace.ts";
34
34
 
35
35
  export type InstanceOptions = {
@@ -113,19 +113,25 @@ export async function startInstance(opts: InstanceOptions): Promise<RunningInsta
113
113
  }
114
114
  await store.init();
115
115
 
116
- // Resolved BEFORE the host: the Instance token is also the echo bearer (ADR-0023), so the
117
- // host's echo pusher closes over it. Served under in step 4 below, unchanged.
116
+ // Resolved BEFORE the host, and the signing key with it: the host derives every Harness bearer
117
+ // from the key (ADR-0058) — admission, stream, abort, echo. The key is loaded from (or minted
118
+ // into) the instance folder, NOT generated per process: live Sandboxes outlive a restart, their
119
+ // Custodians still hold tokens this key signed, and their Harnesses still check bearers it derived.
120
+ // The Instance token is per-boot; the key is not. Both are served under in step 4 below.
118
121
  const instanceToken = opts.instanceToken ?? mintInstanceToken();
122
+ const signingKey = opts.signingKey ?? (await loadSigningKey(opts.dir));
119
123
 
120
124
  const host = new RunHost({
121
125
  store,
122
126
  reconcile: opts.reconcile,
123
127
  sandbox: opts.sandbox,
124
128
  instanceHarness: opts.instanceHarness,
129
+ harnessBearer: (placement) => harnessToken(signingKey, placement),
125
130
  // The run-narrative echo (ADR-0023): tee a run's feed to its enclosing Workspace's Harness,
126
- // authenticated as the instance. The wire push is here and the fire-and-forget is the
127
- // host's, so a Harness that refuses (or is gone) costs a log line at most.
128
- echo: (endpoint) => createEchoPush({ baseUrl: endpoint, token: instanceToken }),
131
+ // bearing that Workspace's Harness bearer (ADR-0058) — never the Instance token, which would
132
+ // otherwise pass through a process the Agent executes code beside. The wire push is here and
133
+ // the fire-and-forget is the host's, so a Harness that refuses (or is gone) costs a log line.
134
+ echo: (endpoint, bearer) => createEchoPush({ baseUrl: endpoint, ...(bearer ? { token: bearer } : {}) }),
129
135
  // A run left `live` to be retried is otherwise unexplained — the announce line names it, this
130
136
  // says why (ADR-0030). stderr, because it is a fault, not the boot's structured result.
131
137
  onRestoreError: (runId, err) =>
@@ -153,10 +159,7 @@ export async function startInstance(opts: InstanceOptions): Promise<RunningInsta
153
159
  // discoverable by asking after a specific run id nobody knows to ask about.
154
160
  const restored = await host.restore();
155
161
 
156
- // 4. Serve, authenticated (ADR-0013). The signing key is loaded from (or minted into) the
157
- // instance folder, NOT generated per process: live Sandboxes outlive a restart, and their
158
- // Adapters still bear tokens this key signed. The Instance token is per-boot; the key is not.
159
- const signingKey = opts.signingKey ?? (await loadSigningKey(opts.dir));
162
+ // 4. Serve, authenticated (ADR-0013) — the token and key resolved above.
160
163
  const auth = createAuthenticator({ instanceToken, signingKey });
161
164
  // The Repos are read PER REQUEST, never snapshotted here: a cache the agent cloned minutes
162
165
  // after boot must show as present the next time anyone asks (ADR-0048/0051).
@@ -0,0 +1,92 @@
1
+ // The known model keys (ADR-0059): every env var pi's catalog reads a provider's credential from,
2
+ // the catalog provider(s) that read it, and the one host those providers' calls go to by default.
3
+ //
4
+ // A COPY of pi's data at the exact pin `@jr2/harness` holds, because this package has no pi
5
+ // dependency and must not grow one: `jr2 up` reads this table host-side (R1, and the default
6
+ // `hosts` of a held secret), and the in-cluster Orchestrator never touches pi. The copy cannot
7
+ // drift silently — `@jr2/harness`'s `model-keys.test.ts` checks it against pi itself, env names per
8
+ // provider and every provider's default host, in the default gate. A pi bump that adds or moves a
9
+ // provider fails there first, the same canary role Harness conformance plays (ADR-0027).
10
+ //
11
+ // `host` is present only when every model of every provider that reads the name goes to ONE static
12
+ // host. It is absent when there is none: the base URL is a template (`{location}`, an account id),
13
+ // the providers disagree (MOONSHOT_API_KEY serves two regions), or the host is regional (Bedrock).
14
+ // A held secret with such a name must then say its `hosts` itself (§3.4 of the spec, R5).
15
+
16
+ /** One known model-key env name. */
17
+ export type ModelKey = {
18
+ /** The catalog provider ids that read this name. */
19
+ providers: readonly string[];
20
+ /** The host (port 443) those providers' calls go to by default, when there is exactly one. */
21
+ host?: string;
22
+ };
23
+
24
+ export const MODEL_KEYS: Readonly<Record<string, ModelKey>> = {
25
+ // anthropic reads three names. AUTH_TOKEN goes as `Authorization: Bearer`, the other two as
26
+ // `x-api-key` (an `sk-ant-oat` value is an OAuth token, which a Stand-in never is).
27
+ ANTHROPIC_API_KEY: { providers: ["anthropic"], host: "api.anthropic.com" },
28
+ ANTHROPIC_AUTH_TOKEN: { providers: ["anthropic"], host: "api.anthropic.com" },
29
+ ANTHROPIC_OAUTH_TOKEN: { providers: ["anthropic"], host: "api.anthropic.com" },
30
+ OPENAI_API_KEY: { providers: ["openai"], host: "api.openai.com" },
31
+ GEMINI_API_KEY: { providers: ["google"], host: "generativelanguage.googleapis.com" },
32
+ // `https://{location}-aiplatform.googleapis.com` — regional, so no default.
33
+ GOOGLE_CLOUD_API_KEY: { providers: ["google-vertex"] },
34
+ COPILOT_GITHUB_TOKEN: { providers: ["github-copilot"], host: "api.individual.githubcopilot.com" },
35
+ // The bearer form of Bedrock auth. Regional, so no default. (SigV4 credentials cannot be held at
36
+ // all — a signature is not a header a Stand-in can be swapped into — and `jr2 up` warns on them.)
37
+ AWS_BEARER_TOKEN_BEDROCK: { providers: ["amazon-bedrock"] },
38
+ ANT_LING_API_KEY: { providers: ["ant-ling"], host: "api.ant-ling.com" },
39
+ QWEN_TOKEN_PLAN_API_KEY: {
40
+ providers: ["qwen-token-plan"],
41
+ host: "token-plan.ap-southeast-1.maas.aliyuncs.com",
42
+ },
43
+ QWEN_TOKEN_PLAN_CN_API_KEY: { providers: ["qwen-token-plan-cn"], host: "token-plan.cn-beijing.maas.aliyuncs.com" },
44
+ // The catalog names no base URL: an Azure deployment is the user's own resource.
45
+ AZURE_OPENAI_API_KEY: { providers: ["azure-openai-responses"] },
46
+ NVIDIA_API_KEY: { providers: ["nvidia"], host: "integrate.api.nvidia.com" },
47
+ DEEPSEEK_API_KEY: { providers: ["deepseek"], host: "api.deepseek.com" },
48
+ GROQ_API_KEY: { providers: ["groq"], host: "api.groq.com" },
49
+ CEREBRAS_API_KEY: { providers: ["cerebras"], host: "api.cerebras.ai" },
50
+ XAI_API_KEY: { providers: ["xai"], host: "api.x.ai" },
51
+ // pi reads the name, and its catalog lists no model for the provider at this pin.
52
+ RADIUS_API_KEY: { providers: ["radius"] },
53
+ OPENROUTER_API_KEY: { providers: ["openrouter"], host: "openrouter.ai" },
54
+ AI_GATEWAY_API_KEY: { providers: ["vercel-ai-gateway"], host: "ai-gateway.vercel.sh" },
55
+ ZAI_API_KEY: { providers: ["zai"], host: "api.z.ai" },
56
+ ZAI_CODING_CN_API_KEY: { providers: ["zai-coding-cn"], host: "open.bigmodel.cn" },
57
+ MISTRAL_API_KEY: { providers: ["mistral"], host: "api.mistral.ai" },
58
+ MINIMAX_API_KEY: { providers: ["minimax"], host: "api.minimax.io" },
59
+ MINIMAX_CN_API_KEY: { providers: ["minimax-cn"], host: "api.minimaxi.com" },
60
+ // Two providers, two hosts (`.ai` and `.cn`): no single default.
61
+ MOONSHOT_API_KEY: { providers: ["moonshotai", "moonshotai-cn"] },
62
+ HF_TOKEN: { providers: ["huggingface"], host: "router.huggingface.co" },
63
+ FIREWORKS_API_KEY: { providers: ["fireworks"], host: "api.fireworks.ai" },
64
+ TOGETHER_API_KEY: { providers: ["together"], host: "api.together.ai" },
65
+ // Two providers, one host.
66
+ OPENCODE_API_KEY: { providers: ["opencode", "opencode-go"], host: "opencode.ai" },
67
+ KIMI_API_KEY: { providers: ["kimi-coding"], host: "api.kimi.com" },
68
+ // Account-templated base URLs.
69
+ CLOUDFLARE_API_KEY: { providers: ["cloudflare-workers-ai", "cloudflare-ai-gateway"] },
70
+ XIAOMI_API_KEY: { providers: ["xiaomi"], host: "api.xiaomimimo.com" },
71
+ XIAOMI_TOKEN_PLAN_CN_API_KEY: { providers: ["xiaomi-token-plan-cn"], host: "token-plan-cn.xiaomimimo.com" },
72
+ XIAOMI_TOKEN_PLAN_AMS_API_KEY: { providers: ["xiaomi-token-plan-ams"], host: "token-plan-ams.xiaomimimo.com" },
73
+ XIAOMI_TOKEN_PLAN_SGP_API_KEY: { providers: ["xiaomi-token-plan-sgp"], host: "token-plan-sgp.xiaomimimo.com" },
74
+ };
75
+
76
+ /** Every catalog provider id some known model key belongs to — the set `harness.catalog` may name. */
77
+ export const MODEL_KEY_PROVIDERS: ReadonlySet<string> = new Set(
78
+ Object.values(MODEL_KEYS).flatMap((key) => key.providers),
79
+ );
80
+
81
+ /**
82
+ * Credentials that sign a request instead of riding it in a header (ADR-0059). A Stand-in cannot be
83
+ * swapped into a signature, so these cannot be held — `jr2 up` warns when `harness.env` sets one,
84
+ * because the Agent can read it, and names a gateway as the way out.
85
+ */
86
+ export const SIGNING_CREDENTIALS: readonly string[] = [
87
+ "AWS_ACCESS_KEY_ID",
88
+ "AWS_SECRET_ACCESS_KEY",
89
+ "AWS_SESSION_TOKEN",
90
+ "AWS_PROFILE",
91
+ "GOOGLE_APPLICATION_CREDENTIALS",
92
+ ];
package/src/names.ts CHANGED
@@ -12,13 +12,13 @@ export const INSTANCE_SECRET = "jr2-instance";
12
12
  /** The Instance Harness's Deployment + Service name (ADR-0031): converged by `jr2 up` whenever any
13
13
  * Agent definition declares `workspace: "none"`, and the deterministic Service DNS the Agent actor
14
14
  * resolves such a Turn to. Doubles as the delivery scope a Menu-only registration records — the
15
- * name the placement's Adapter token is signed for (tokens.ts, ADR-0013). The port is the
15
+ * name the placement's Sandbox token is signed for (tokens.ts, ADR-0013), which its Custodian holds. The port is the
16
16
  * Harness's own listen port (`PORT` default). */
17
17
  export const INSTANCE_HARNESS_SERVICE = "jr2-instance-harness";
18
18
  export const INSTANCE_HARNESS_PORT = 8080;
19
19
 
20
20
  /** The harness config ConfigMap the stock Harness boots from (ADR-0018): what this instance can
21
- * REACH — the custom model provider, minus its key. No Agents ride it: a Machine carries its own
21
+ * REACH — the custom model provider minus its key, and the catalog providers' gateways (ADR-0059). No Agents ride it: a Machine carries its own
22
22
  * (ADR-0049), and the definition rides each admission, so this holds deployment facts alone
23
23
  * (ADR-0050). */
24
24
  export const HARNESS_CONFIGMAP = "jr2-harness";
@@ -26,7 +26,7 @@ export const HARNESS_CONFIGMAP = "jr2-harness";
26
26
  export const HARNESS_CONFIG_KEY = "harness.json";
27
27
 
28
28
  /** The resolved image map `jr2 up` writes and every provision reads (ADR-0037/0038): key → ref for
29
- * the Harness, the Adapter, and every Sandbox Image context the registered Machines carry — keyed
29
+ * the Harness, the Custodian, and every Sandbox Image context the registered Machines carry — keyed
30
30
  * by content digest, plus the reserved `default` (ADR-0049).
31
31
  *
32
32
  * MOUNTED, never projected into env — the load-bearing part. A mount updates in place through
@@ -38,13 +38,37 @@ export const IMAGES_MOUNT = "/etc/jr2/images";
38
38
  /** The ConfigMap key, hence the filename under the mount — the two-sided contract's other half. */
39
39
  export const IMAGES_KEY = "images.json";
40
40
 
41
- /** The HARNESS containers' env Secret — Agent creds only, never the Instance token (ADR-0013). */
41
+ /** The HARNESS containers' env Secret — the literal `harness.env` values, never the Instance token
42
+ * (ADR-0013) and never a model key (ADR-0059: those are held). */
42
43
  export const HARNESS_ENV_SECRET = "jr2-harness-env";
43
44
 
44
- /** The instance's private-CA bundle ConfigMap (`harness.caBundle`, ADR-0020) — mounted into the
45
- * Harness container (and only it) so Agent egress trusts an internal CA. Public data by nature. */
45
+ /** The instance's trust ConfigMap (ADR-0020, ADR-0059): the user's `harness.caBundle` as `ca.crt`
46
+ * and, while a held secret exists, the three bundles the held-secret path needs — `extra.crt` and
47
+ * `bundle.crt` for the Harness, `upstream.crt` for the Custodian. Public data by nature. */
46
48
  export const CA_CONFIGMAP = "jr2-ca";
47
49
 
50
+ /** Held secrets (ADR-0059). The per-Instance CA — its key included — that `jr2 up` alone reads over
51
+ * the kube API, and that NO pod mounts: every leaf is issued before a pod starts. */
52
+ export const HELD_CA_SECRET = "jr2-held-ca";
53
+ /** The literal held values, one key per name — mounted into the Custodian and nothing else. */
54
+ export const HELD_SECRETS_SECRET = "jr2-held-secrets";
55
+ /** One leaf per bound `host:port` (`<stem>.crt`, `<stem>.key`) — the Custodian's alone. */
56
+ export const HELD_TLS_SECRET = "jr2-held-tls";
57
+ /** `held.json` (what `jr2 up` resolved) plus the Custodian's bootstrap and script. Always applied,
58
+ * possibly holding no secret: every Harness pod runs a Custodian. */
59
+ export const HELD_CONFIGMAP = "jr2-held";
60
+ /** Where the Orchestrator reads `held.json`, per provision — a mount, so a held-secret edit reaches
61
+ * future Sandboxes without rolling the Orchestrator (the `jr2-images` pattern, ADR-0038). */
62
+ export const HELD_MOUNT = "/etc/jr2/held";
63
+ export const HELD_KEY = "held.json";
64
+
65
+ /** The Custodian's listeners (ADR-0059), all taken on every Harness pod (ADR-0005): the Harness's
66
+ * HTTPS proxy on loopback, the kubelet's probe, and the control listener the Menu and the ask ride
67
+ * to the Orchestrator, at the address `jr2-upload-pack` defaults to (ADR-0053). */
68
+ export const CUSTODIAN_PORT = 15001;
69
+ export const CUSTODIAN_HEALTH_PORT = 15021;
70
+ export const CUSTODIAN_CONTROL_PORT = 8081;
71
+
48
72
  /** The scaffold's default deploy-key Secret name (ADR-0047/0051): what `jr2 init`'s wildcard
49
73
  * `git.credentials` entry names as its `sshKey`, and what `jr2 up`'s ssh offer generates into when
50
74
  * an entry names it. Only a default — an entry may name any Secret. */
@@ -1,6 +1,6 @@
1
1
  // The internal registration table (ADR-0011): one structure behind both delivery dialects.
2
2
  // An Agent slot and `gate` register `address → { accepted event defs, deliver closure, meta }`;
3
- // the two HTTP surfaces (`/agents/:iid/*` for the Adapter, `/runs/:id/gates/*` for humans and
3
+ // the two HTTP surfaces (`/agents/:iid/*` for the Agent's Harness, `/runs/:id/gates/*` for humans and
4
4
  // webhooks) are adapters over it — lookup, schema validation, delivery, discovery, and lifecycle
5
5
  // are implemented ONCE. The table is implementation structure, not vocabulary: workflows speak
6
6
  // only `defineEvent` / `agent` / `gate`.
@@ -246,6 +246,13 @@ export type RunBinding = {
246
246
  * absent, a `"none"` Agent without an explicit `endpoint` fails loudly.
247
247
  */
248
248
  instanceHarness?: string;
249
+ /**
250
+ * The Harness bearer for a placement (ADR-0058): `harnessToken(signingKey, placement)`, closed
251
+ * over the key by the host. The Agent actor asks it for the pod its Turn resolved to; the echo
252
+ * attach asks it for the Workspace's. Absent (bare unit-test bindings, the stub path), no bearer
253
+ * is sent — which only a Harness that checks nothing accepts.
254
+ */
255
+ harnessBearer?: (placement: string) => string;
249
256
  /**
250
257
  * Record an Agent invocation's durable admission in the host ledger (ADR-0016): persisted
251
258
  * beside the snapshot in the same RunBlob save, keyed by iid (globally unique, so the map is
@@ -283,12 +290,13 @@ export type RunBinding = {
283
290
  marker?: (event: TurnMarker) => void;
284
291
  /**
285
292
  * Attach the run-narrative echo to a Workspace's Harness (ADR-0023): the host replays the
286
- * run's feed-so-far to `endpoint`, then tees live; returns the detach. Called by `workspace()`'s
293
+ * run's feed-so-far to `endpoint` — the Harness in `placement`'s pod, pushed with that
294
+ * placement's bearer (ADR-0058) — then tees live; returns the detach. Called by `workspace()`'s
287
295
  * registrar — the same restore-safe seat the ambient handles ride — and scoped to the OWNING
288
296
  * run by construction: the binding is per-run, so a sibling run's feed is unreachable.
289
297
  * Host-supplied; absent = no echo.
290
298
  */
291
- echo?: (endpoint: string) => () => void;
299
+ echo?: (endpoint: string, placement: string) => () => void;
292
300
  /**
293
301
  * The HOST is ending this run for its own reasons (ADR-0024). An Agent invocation ending
294
302
  * normally ends the Agent's turn — the state stopped waiting — but `RunHost.stop()` is the one
package/src/repo-fetch.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Nothing in the pod holds a git credential, and nothing in it may talk to a remote — the node's
4
4
  // cache agent is the only thing that fetches (ADR-0051). So a `git fetch` in a pod is a program on
5
- // the runtime volume (`jr2-upload-pack`, operator/cmd) asking the Adapter on localhost, the Adapter
5
+ // the runtime volume (`jr2-upload-pack`, operator/cmd) asking the Custodian on localhost, the Custodian
6
6
  // asking the Orchestrator with the Sandbox token it already holds, and this port marking the
7
7
  // Sandbox CR: one annotation per Repo key, timestamp value. The operator copies the mark onto the
8
8
  // pod, the agent reads its demand off pods alone, and the landing comes back on the Sandbox's
package/src/run-host.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  // closures over their own `sendBack` — so delivery lands on the invoking state at any
8
8
  // nesting depth and the host routes nothing.
9
9
  // - Two thin surfaces sit on that table, and NEITHER is MCP (ADR-0013 — the Orchestrator does
10
- // not speak it): `agentSurface` / `sendToAgent` serve the Agent's Adapter (`/agents/:iid/*`),
10
+ // not speak it): `agentSurface` / `sendToAgent` serve the Agent's Harness (`/agents/:iid/*`),
11
11
  // `gates` / `sendToGate` serve humans, webhooks and CI (`/runs/:id/gates/*`). Lookup,
12
12
  // validation, delivery and lifecycle stay implemented once, in the table.
13
13
  // - The run's Agent children report their durable admissions through the run binding into
@@ -169,13 +169,14 @@ export type GateView = {
169
169
  };
170
170
 
171
171
  /**
172
- * One live agent surface, as its Adapter reads it (`GET /agents/:iid/surface` — ADR-0013). The
173
- * Adapter renders this as `tools/list`: each accepted event becomes a tool, its `input` schema the
174
- * tool's input schema. `semantics` rides along so the Adapter can tell an awaiting tool from a
175
- * fire-and-forget one — the room ADR-0006's deferred results will land in, unbuilt today.
172
+ * One live agent surface, as the Harness reads it through its Custodian (`GET /agents/:iid/surface`
173
+ * — ADR-0013). The Harness presents it to its model as the Menu: each accepted event becomes a
174
+ * tool, its `input` schema the tool's parameters. `semantics` rides along so the Harness can tell
175
+ * an awaiting tool from a fire-and-forget one — the room ADR-0006's deferred results will land in,
176
+ * unbuilt today.
176
177
  *
177
- * `sandbox` is the Sandbox that may deliver here. It is not a secret from the Adapter (that pod IS
178
- * the sandbox), and serving it lets the Adapter fail loudly on a surface that is not its own.
178
+ * `sandbox` is the Sandbox that may deliver here. It is not a secret from that pod (the pod IS the
179
+ * sandbox), and serving it lets a caller fail loudly on a surface that is not its own.
179
180
  */
180
181
  export type AgentSurfaceView = {
181
182
  instanceId: string;
@@ -186,7 +187,7 @@ export type AgentSurfaceView = {
186
187
 
187
188
  /**
188
189
  * The answer to one Agent delivery (`POST /agents/:iid/events`) — a receipt that DESCRIBES ITSELF
189
- * (ADR-0024). The Adapter renders it as prose, because a bare `deliveryId` told the Agent nothing
190
+ * (ADR-0024). The Harness renders it as prose, because a bare `deliveryId` told the Agent nothing
190
191
  * about whether it was finished, and the model answered that silence by calling again.
191
192
  *
192
193
  * `turnComplete` is the HINT, never the guarantee (the abort on invocation end is): it is read off
@@ -312,13 +313,18 @@ export type RunHostOptions = {
312
313
  sandbox?: SandboxPort;
313
314
  /** The Instance Harness base URL (ADR-0031) — where `workspace: "none"` Turns are admitted. */
314
315
  instanceHarness?: string;
316
+ /** The Harness bearer for a placement (ADR-0058) — `startInstance` closes `harnessToken` over the
317
+ * signing key. Handed to every run's binding, and to the echo pusher below. Absent, no Harness
318
+ * call bears one: only the stub Harness answers such a host. */
319
+ harnessBearer?: (placement: string) => string;
315
320
  /**
316
321
  * Build the run-narrative echo pusher for one Workspace's Harness (ADR-0023) — the seam a fake
317
- * echo server rides in tests; `startInstance` binds the real wire push (harness-client.ts),
318
- * closed over the Instance token. Absent = no echo: a host without it runs identically, because
319
- * the echo is a courtesy view of the feed, never a dependency of the run.
322
+ * echo server rides in tests; `startInstance` binds the real wire push (harness-client.ts).
323
+ * `bearer` is that Workspace's Harness bearer (ADR-0058), when the host derives one. Absent = no
324
+ * echo: a host without it runs identically, because the echo is a courtesy view of the feed,
325
+ * never a dependency of the run.
320
326
  */
321
- echo?: (endpoint: string) => (events: EchoEvent[]) => Promise<void>;
327
+ echo?: (endpoint: string, bearer: string | undefined) => (events: EchoEvent[]) => Promise<void>;
322
328
  };
323
329
 
324
330
  /** What we persist per run: the machine snapshot wrapped with the run metadata restore needs.
@@ -383,7 +389,11 @@ export class RunHost {
383
389
  private readonly onRestoreError?: (runId: string, err: unknown) => void;
384
390
  private readonly sandbox?: SandboxPort;
385
391
  private readonly instanceHarness?: string;
386
- private readonly echoFactory?: (endpoint: string) => (events: EchoEvent[]) => Promise<void>;
392
+ private readonly harnessBearer?: (placement: string) => string;
393
+ private readonly echoFactory?: (
394
+ endpoint: string,
395
+ bearer: string | undefined,
396
+ ) => (events: EchoEvent[]) => Promise<void>;
387
397
  private readonly workflowDefs = new Map<string, WorkflowDef>();
388
398
  private readonly runs = new Map<string, LiveRun>();
389
399
  /**
@@ -401,6 +411,7 @@ export class RunHost {
401
411
  this.onRestoreError = opts.onRestoreError;
402
412
  this.sandbox = opts.sandbox;
403
413
  this.instanceHarness = opts.instanceHarness;
414
+ this.harnessBearer = opts.harnessBearer;
404
415
  this.echoFactory = opts.echo;
405
416
  }
406
417
 
@@ -569,15 +580,14 @@ export class RunHost {
569
580
  /**
570
581
  * An agent instance's LIVE surface (`GET /agents/:iid/surface` — ADR-0013): what the state that
571
582
  * invoked this Agent accepts, right now. Undefined once nothing is registered (the state exited,
572
- * the run settled, the iid is unknown) — the one catch point, and the reason the Adapter never
583
+ * the run settled, the iid is unknown) — the one catch point, and the reason the Harness never
573
584
  * has to learn which turn is live: it asks, per turn, and the answer IS the turn.
574
585
  *
575
586
  * "Right now" is load-bearing (ADR-0029): the registered defs are the state's VOCABULARY, derived
576
587
  * statically from its transitions, and the guards on those transitions are asked here — so an
577
588
  * event the Machine cannot currently accept is not offered. The pick has not happened yet, so the
578
589
  * question is `mayMove`, not `wouldMove`: a guard that would have judged the Agent's arguments is
579
- * left on the menu and settled at delivery. The Adapter rebuilds this per MCP connection and the
580
- * Harness re-lists per Submission, so the filter lands at turn boundaries and never moves under
590
+ * left on the menu and settled at delivery. The Harness reads this afresh per Submission, so the filter lands at turn boundaries and never moves under
581
591
  * an Agent mid-turn.
582
592
  */
583
593
  agentSurface(instanceId: string): AgentSurfaceView | undefined {
@@ -599,8 +609,10 @@ export class RunHost {
599
609
  }
600
610
 
601
611
  /**
602
- * Deliver one event from an Agent's Adapter (`POST /agents/:iid/events` — ADR-0013). Validation
603
- * and delivery are the table's; this only agent-scopes the address and mints the receipt.
612
+ * Deliver one event from an Agent's Harness (`POST /agents/:iid/events` — ADR-0013). Validation
613
+ * and delivery are the table's; this only agent-scopes the address and mints the receipt. (A
614
+ * `deferred` event never reaches here: `jr2Setup` refuses to build a Machine that declares one —
615
+ * ADR-0013.)
604
616
  */
605
617
  sendToAgent(instanceId: string, event: { type?: unknown } & Record<string, unknown>): AgentDeliveryReceipt {
606
618
  const { type, ...payload } = event;
@@ -883,11 +895,11 @@ export class RunHost {
883
895
  * Scoped to the OWNING run's lineage by construction: it reads one run's buffer and listeners
884
896
  * and nothing else — a sibling run's events cannot reach this endpoint through here.
885
897
  */
886
- private attachEcho(runId: string, endpoint: string): () => void {
898
+ private attachEcho(runId: string, endpoint: string, placement: string): () => void {
887
899
  const run = this.runs.get(runId);
888
900
  const factory = this.echoFactory;
889
901
  if (!run || !factory) return () => {};
890
- const push = factory(endpoint);
902
+ const push = factory(endpoint, this.harnessBearer?.(placement));
891
903
  let chain = Promise.resolve();
892
904
  let reported = false;
893
905
  const enqueue = (events: EchoEvent[]): void => {
@@ -965,6 +977,7 @@ export class RunHost {
965
977
  table: this.table,
966
978
  sandbox: this.sandbox,
967
979
  instanceHarness: this.instanceHarness,
980
+ ...(this.harnessBearer ? { harnessBearer: this.harnessBearer } : {}),
968
981
  // The admission ledger's write half (ADR-0016): the Agent actor reports the durable handle the
969
982
  // moment the Harness admits it, and the ledger hits the store in the same RunBlob save. An
970
983
  // admission arriving around stop/untrack still lands in `agents` but skips the save,
@@ -999,7 +1012,7 @@ export class RunHost {
999
1012
  if (run && run.binding === binding) this.feed(run, event);
1000
1013
  },
1001
1014
  // The run-narrative echo attach (ADR-0023), called from `workspace()`'s registrar.
1002
- echo: (endpoint) => this.attachEcho(record.runId, endpoint),
1015
+ echo: (endpoint, placement) => this.attachEcho(record.runId, endpoint, placement),
1003
1016
  };
1004
1017
  let bound = false;
1005
1018
  const actor = createActor(machine, {