@jr2/orchestrator 0.2.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jr2/orchestrator",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "The jr2 Orchestrator: xstate runtime, HTTP API and Console, and the kit pieces a Workflow imports.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -42,11 +42,11 @@
42
42
  "ts-blank-space": "^0.9.0",
43
43
  "xstate": "^5.18.0",
44
44
  "zod": "^4.4.3",
45
- "@jr2/agent-protocol": "0.2.0"
45
+ "@jr2/agent-protocol": "0.2.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^26.0.1",
49
- "@jr2/harness": "0.2.0"
49
+ "@jr2/harness": "0.2.1"
50
50
  },
51
51
  "files": [
52
52
  "bin",
package/src/actor.ts CHANGED
@@ -257,8 +257,10 @@ export interface AgentRunPort {
257
257
  * Machine edit that changed it. */
258
258
  export type AgentAdmitOptions = { definition: AgentDefinition; signal?: AbortSignal };
259
259
 
260
- /** Build a port for one invocation from its serializable input (ADR-0011 static-import doctrine). */
261
- export type AgentRunPortFactory = (endpoint: string) => AgentRunPort;
260
+ /** Build a port for one invocation from its serializable input (ADR-0011 static-import doctrine),
261
+ * plus the Harness bearer for the placement that input resolved to (ADR-0058) — absent where no
262
+ * placement is named (the stub path) or the host derives none. */
263
+ export type AgentRunPortFactory = (endpoint: string, bearer?: string) => AgentRunPort;
262
264
 
263
265
  /** Absorbed-turn-mechanics knobs (ADR-0016): defaulted, never Machine context. */
264
266
  export type AgentRunOptions = {
@@ -504,7 +506,10 @@ export function agentActorWith(
504
506
  );
505
507
  registerSurface(currentIid);
506
508
 
507
- const client = portFactory(endpoint);
509
+ // The Harness bearer (ADR-0058) is derived from the SAME name the registration records as its
510
+ // delivery scope: the pod hosting this Turn. One name, both directions — the Adapter there may
511
+ // speak for these picks, and only this bearer may drive the conversation there.
512
+ const client = portFactory(endpoint, sandbox === undefined ? undefined : binding.harnessBearer?.(sandbox));
508
513
  const controller = new AbortController();
509
514
  // Shared per run, created on demand so the ordering guarantee holds for any binding.
510
515
  const pendingAborts = (binding.pendingAborts ??= new Map<string, Promise<void>>());
@@ -80,6 +80,10 @@ export type HarnessClient = {
80
80
  export type HarnessClientOptions = {
81
81
  /** The Harness base URL — what a workspace publishes, or a stub's `url`. */
82
82
  baseUrl: string;
83
+ /** The Harness bearer for this placement (ADR-0058; `harnessToken` in tokens.ts) — sent on
84
+ * every call, the long-poll included. Absent only where no placement exists: an explicit-
85
+ * `endpoint` run against the stub Harness, which checks nothing. */
86
+ token?: string;
83
87
  /** Injectable for socket-free tests. Default: global `fetch`. */
84
88
  fetch?: typeof fetch;
85
89
  /** Reconnect backoff floor/ceiling (capped exponential, jittered per rung — see `jittered`;
@@ -125,6 +129,7 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
125
129
  const backoffMaxMs = options.backoffMaxMs ?? 5_000;
126
130
  const admitWindowMs = options.admitWindowMs ?? 90_000;
127
131
  const log = options.log ?? ((line: string) => console.warn(line));
132
+ const authorization: Record<string, string> = options.token ? { authorization: `Bearer ${options.token}` } : {};
128
133
 
129
134
  const conversationUrl = (agentName: string, instanceId: string) =>
130
135
  new URL(`/agents/${encodeURIComponent(agentName)}/${encodeURIComponent(instanceId)}`, baseUrl).toString();
@@ -192,7 +197,7 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
192
197
  url,
193
198
  {
194
199
  method: "POST",
195
- headers: { "content-type": "application/json" },
200
+ headers: { "content-type": "application/json", ...authorization },
196
201
  body: JSON.stringify({
197
202
  message: sendOptions.message,
198
203
  // The other half of the Frame (ADR-0057): where this Turn works. Omitted only when
@@ -238,13 +243,21 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
238
243
  let events: StreamEvent[];
239
244
  let nextOffset: string | null;
240
245
  try {
241
- const res = await fetchImpl(url.toString(), { signal });
246
+ const res = await fetchImpl(url.toString(), { headers: authorization, signal });
242
247
  if (res.status === 404) {
243
248
  throw new SettlementFault(
244
249
  `conversation lost: the harness answered 404 for submission "${admission.submissionId}" — ` +
245
250
  `a conversation lives as long as its Harness process (ADR-0027)`,
246
251
  );
247
252
  }
253
+ // Refused, not unreachable: the bearer is derived, so a Harness that rejects it now
254
+ // rejects it on every retry — reconnecting would park the Turn forever, silently.
255
+ if (res.status === 401 || res.status === 403) {
256
+ throw new SettlementFault(
257
+ `harness refused the stream read (${res.status}) for submission "${admission.submissionId}": ` +
258
+ `${await errorDetail(res)} — the bearer is not this placement's (ADR-0058)`,
259
+ );
260
+ }
248
261
  if (!res.ok && res.status !== 204) {
249
262
  throw new ReconnectableError(`harness stream read failed (${res.status})`);
250
263
  }
@@ -274,6 +287,7 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
274
287
  async abort(agentName, instanceId, opts) {
275
288
  const res = await fetchImpl(`${conversationUrl(agentName, instanceId)}/abort`, {
276
289
  method: "POST",
290
+ headers: authorization,
277
291
  signal: opts?.signal,
278
292
  });
279
293
  if (!res.ok) {
@@ -334,16 +348,16 @@ export function createHarnessAgentRunClient(options: HarnessClientOptions): Agen
334
348
 
335
349
  /**
336
350
  * The run-narrative echo push (ADR-0023): `POST /echo { events }` against one Workspace Harness,
337
- * bearing the Instance token (the endpoint is instance-token-gated — the Harness verifies the
338
- * bearer against the token's sha-256, never holding the token itself). The payload is the
351
+ * bearing that placement's Harness bearer (ADR-0058 — the Harness verifies it against a sha-256,
352
+ * never holding it at rest). The payload is the
339
353
  * STRUCTURED feed events; the Harness renders. A non-OK answer rejects, and the CALLER treats
340
354
  * that as log-and-continue — fire-and-forget lives in the tee (run-host.ts), not here, so a test
341
355
  * can still assert a push failed.
342
356
  */
343
357
  export function createEchoPush(options: {
344
358
  baseUrl: string;
345
- /** The Instance token — the echo bearer. */
346
- token: string;
359
+ /** The Workspace's Harness bearer (ADR-0058). Absent only against the stub Harness. */
360
+ token?: string;
347
361
  /** Injectable for socket-free tests. Default: global `fetch`. */
348
362
  fetch?: typeof fetch;
349
363
  }): (events: EchoEvent[]) => Promise<void> {
@@ -356,7 +370,10 @@ export function createEchoPush(options: {
356
370
  // `fetch failed` in the pod log is what let that condition hide.
357
371
  const res = await fetchImpl(url, {
358
372
  method: "POST",
359
- headers: { "content-type": "application/json", authorization: `Bearer ${options.token}` },
373
+ headers: {
374
+ "content-type": "application/json",
375
+ ...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
376
+ },
360
377
  body: JSON.stringify({ events }),
361
378
  }).catch((err: unknown) => {
362
379
  throw new Error(`harness echo to ${url} failed: ${transportDetail(err)}`, { cause: err });
@@ -386,7 +403,10 @@ export function createEchoPush(options: {
386
403
  * still an `AgentDefinition`: the actor narrows on start and refuses an Open one there.
387
404
  */
388
405
  export function agent(declaration: AgentDeclaration): AgentLogic {
389
- return agentActorWith((endpoint) => createHarnessAgentRunClient({ baseUrl: endpoint }), declaration);
406
+ return agentActorWith(
407
+ (endpoint, bearer) => createHarnessAgentRunClient({ baseUrl: endpoint, ...(bearer ? { token: bearer } : {}) }),
408
+ declaration,
409
+ );
390
410
  }
391
411
 
392
412
  /** A stream read worth retrying (server hiccup) — internal to the reconnect loop, never thrown out. */
package/src/http.ts CHANGED
@@ -27,9 +27,10 @@
27
27
  // control.
28
28
  // authenticated any principal we minted a token for. The Agent's surface lives here, scoped
29
29
  // 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.
30
+ // instanceOnly the Instance token ALONE. Run control — starting a run included (ADR-0058) — and
31
+ // full run state (`/runs*`): a Sandbox token authenticates but is refused, because
32
+ // starting work, reading another feature's context, or cancelling a run is not on
33
+ // the Agent's surface any more than a Gate is.
33
34
 
34
35
  import { readFile, stat } from "node:fs/promises";
35
36
  import { createRequire } from "node:module";
@@ -356,8 +357,10 @@ export function createApp(host: RunHost, auth?: Authenticator, opts: CreateAppOp
356
357
  // Push work: start a run of a registered workflow. Unknown workflow → host.start throws → 404.
357
358
  // A body failing the machine's declared input schema (ADR-0033) → 400 naming the accepted
358
359
  // 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) => {
360
+ // schema (`POST /runs/:id/gates/:gate/events` below). Instance-band (ADR-0058): starting work is
361
+ // run control, and a Sandbox token's scope is its own agent surface — no Adapter forwarding this
362
+ // route is what keeps an Agent from starting runs today, and that is not a control.
363
+ app.post("/workflows/:name/runs", instanceOnly, async (c) => {
361
364
  const name = c.req.param("name");
362
365
  const input = await readJson(c.req.text());
363
366
  try {
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
+ // Adapters still bear 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).
@@ -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/run-host.ts CHANGED
@@ -312,13 +312,18 @@ export type RunHostOptions = {
312
312
  sandbox?: SandboxPort;
313
313
  /** The Instance Harness base URL (ADR-0031) — where `workspace: "none"` Turns are admitted. */
314
314
  instanceHarness?: string;
315
+ /** The Harness bearer for a placement (ADR-0058) — `startInstance` closes `harnessToken` over the
316
+ * signing key. Handed to every run's binding, and to the echo pusher below. Absent, no Harness
317
+ * call bears one: only the stub Harness answers such a host. */
318
+ harnessBearer?: (placement: string) => string;
315
319
  /**
316
320
  * 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.
321
+ * echo server rides in tests; `startInstance` binds the real wire push (harness-client.ts).
322
+ * `bearer` is that Workspace's Harness bearer (ADR-0058), when the host derives one. Absent = no
323
+ * echo: a host without it runs identically, because the echo is a courtesy view of the feed,
324
+ * never a dependency of the run.
320
325
  */
321
- echo?: (endpoint: string) => (events: EchoEvent[]) => Promise<void>;
326
+ echo?: (endpoint: string, bearer: string | undefined) => (events: EchoEvent[]) => Promise<void>;
322
327
  };
323
328
 
324
329
  /** What we persist per run: the machine snapshot wrapped with the run metadata restore needs.
@@ -383,7 +388,11 @@ export class RunHost {
383
388
  private readonly onRestoreError?: (runId: string, err: unknown) => void;
384
389
  private readonly sandbox?: SandboxPort;
385
390
  private readonly instanceHarness?: string;
386
- private readonly echoFactory?: (endpoint: string) => (events: EchoEvent[]) => Promise<void>;
391
+ private readonly harnessBearer?: (placement: string) => string;
392
+ private readonly echoFactory?: (
393
+ endpoint: string,
394
+ bearer: string | undefined,
395
+ ) => (events: EchoEvent[]) => Promise<void>;
387
396
  private readonly workflowDefs = new Map<string, WorkflowDef>();
388
397
  private readonly runs = new Map<string, LiveRun>();
389
398
  /**
@@ -401,6 +410,7 @@ export class RunHost {
401
410
  this.onRestoreError = opts.onRestoreError;
402
411
  this.sandbox = opts.sandbox;
403
412
  this.instanceHarness = opts.instanceHarness;
413
+ this.harnessBearer = opts.harnessBearer;
404
414
  this.echoFactory = opts.echo;
405
415
  }
406
416
 
@@ -883,11 +893,11 @@ export class RunHost {
883
893
  * Scoped to the OWNING run's lineage by construction: it reads one run's buffer and listeners
884
894
  * and nothing else — a sibling run's events cannot reach this endpoint through here.
885
895
  */
886
- private attachEcho(runId: string, endpoint: string): () => void {
896
+ private attachEcho(runId: string, endpoint: string, placement: string): () => void {
887
897
  const run = this.runs.get(runId);
888
898
  const factory = this.echoFactory;
889
899
  if (!run || !factory) return () => {};
890
- const push = factory(endpoint);
900
+ const push = factory(endpoint, this.harnessBearer?.(placement));
891
901
  let chain = Promise.resolve();
892
902
  let reported = false;
893
903
  const enqueue = (events: EchoEvent[]): void => {
@@ -965,6 +975,7 @@ export class RunHost {
965
975
  table: this.table,
966
976
  sandbox: this.sandbox,
967
977
  instanceHarness: this.instanceHarness,
978
+ ...(this.harnessBearer ? { harnessBearer: this.harnessBearer } : {}),
968
979
  // The admission ledger's write half (ADR-0016): the Agent actor reports the durable handle the
969
980
  // moment the Harness admits it, and the ledger hits the store in the same RunBlob save. An
970
981
  // admission arriving around stop/untrack still lands in `agents` but skips the save,
@@ -999,7 +1010,7 @@ export class RunHost {
999
1010
  if (run && run.binding === binding) this.feed(run, event);
1000
1011
  },
1001
1012
  // The run-narrative echo attach (ADR-0023), called from `workspace()`'s registrar.
1002
- echo: (endpoint) => this.attachEcho(record.runId, endpoint),
1013
+ echo: (endpoint, placement) => this.attachEcho(record.runId, endpoint, placement),
1003
1014
  };
1004
1015
  let bound = false;
1005
1016
  const actor = createActor(machine, {
@@ -69,7 +69,7 @@ import { readImageRefs, resolveSandboxImage, resolveUserImage, type ImageRefs }
69
69
  import { CA_CONFIGMAP, IMAGES_KEY, IMAGES_MOUNT, REPOS_MOUNT } from "./names.ts";
70
70
  import { repoIdentity } from "./repo-identity.ts";
71
71
  import type { RepoResources } from "./repos.ts";
72
- import { sandboxToken } from "./tokens.ts";
72
+ import { harnessTokenDigest, sandboxToken } from "./tokens.ts";
73
73
  import type { ProvisionedRepo, SandboxPort, WorkspaceSpec } from "./workspace.ts";
74
74
 
75
75
  /** Run one kubectl invocation to completion. `input` is piped to stdin (`apply -f -`). */
@@ -423,15 +423,23 @@ export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
423
423
  ];
424
424
 
425
425
  // 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), which
427
- // win on collision. Note the asymmetry stands (ADR-0013): user env/envFrom land on the HARNESS
428
- // container only — never on the Adapter, whose env is minted here and carries the pod's only
429
- // credential.
430
- const harnessEnv = (): HarnessEnvVar[] => [
431
- ...(opts.env ?? []),
432
- { name: "JR2_ADAPTER_URL", value: `http://127.0.0.1:${adapterPort}` },
433
- ...(opts.caBundle ? [{ name: "NODE_EXTRA_CA_CERTS", value: `${CA_MOUNT}/ca.crt` }] : []),
434
- ];
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.
430
+ //
431
+ // The gate rides LAST (ADR-0058): the digest of the bearer the Orchestrator derives for THIS
432
+ // Sandbox. A digest because the Agent reads this env; last because a `harness.env` entry of the
433
+ // same name must not be able to choose the Harness's credential.
434
+ const harnessEnv = (name: string): HarnessEnvVar[] => {
435
+ if (!opts.signingKey) throw new Error("kubectlSandbox: a Harness needs a signingKey to check its bearer");
436
+ return [
437
+ ...(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` }] : []),
440
+ { name: "JR2_HARNESS_TOKEN_SHA256", value: harnessTokenDigest(opts.signingKey, name) },
441
+ ];
442
+ };
435
443
 
436
444
  /**
437
445
  * The two init steps, in order (ADR-0037). They are plain container fragments the operator
@@ -573,7 +581,7 @@ export function kubectlSandbox(opts: KubectlSandboxOptions = {}): SandboxPort {
573
581
  // this used to carry was unreachable. The seat's own vars (the fallback `HOME`) come
574
582
  // FIRST, so the instance's `harness.env` can still override them the way it overrides
575
583
  // anything the image set.
576
- env: [...seat.env, ...harnessEnv()],
584
+ env: [...seat.env, ...harnessEnv(req.name)],
577
585
  ...(opts.envFrom?.length ? { envFrom: opts.envFrom } : {}),
578
586
  // What the AGENT gets: an address on its own loopback, and no credential anywhere. This is
579
587
  // the only thing in the pod that tells it how to reach its Machine (ADR-0013).
package/src/server.ts CHANGED
@@ -25,7 +25,6 @@
25
25
  // previous deploy bound — an Instance that drops its last `workspace()` leaves `jr2 gc` able to
26
26
  // collect what it stopped using.
27
27
 
28
- import { createHash } from "node:crypto";
29
28
  import { join } from "node:path";
30
29
  import { loadConfig } from "./config.ts";
31
30
  import { loadWorkflows, startInstance, type RunningInstance } from "./instance.ts";
@@ -70,10 +69,7 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
70
69
  // by nature, which is what lets live Sandboxes outlive orchestrator restarts (ADR-0013).
71
70
  const namespace = env.JR2_NAMESPACE;
72
71
 
73
- // Resolved HERE, not left for startInstance to mint: every Sandbox Harness gets the token's
74
- // sha-256 as its echo gate (ADR-0023, below), so the token must exist before the first
75
- // provision. From the instance Secret when deployed; per-boot for a host-booted fixture,
76
- // exactly as before.
72
+ // From the instance Secret when deployed; per-boot for a host-booted fixture.
77
73
  const instanceToken = env.JR2_INSTANCE_TOKEN ?? mintInstanceToken();
78
74
 
79
75
  // The data-plane switch (ADR-0012/0031/0051): a registered Machine COMPOSES a Sandbox — read off
@@ -124,13 +120,6 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
124
120
  name: "JR2_HARNESS_JSON",
125
121
  valueFrom: { configMapKeyRef: { name: HARNESS_CONFIGMAP, key: HARNESS_CONFIG_KEY } },
126
122
  },
127
- // The echo gate (ADR-0023): the Harness verifies echo bearers against this sha-256. The
128
- // digest, never the token — the Agent executes code in the Harness container, and a
129
- // digest inverts to nothing (the Instance token itself never enters a Sandbox, ADR-0013).
130
- {
131
- name: "JR2_ECHO_TOKEN_SHA256",
132
- value: createHash("sha256").update(instanceToken).digest("base64url"),
133
- },
134
123
  ...(config?.harness?.env ?? []).filter((v) => v.valueFrom !== undefined),
135
124
  ],
136
125
  envFrom: [{ secretRef: { name: HARNESS_ENV_SECRET } }, ...(config?.harness?.envFrom ?? [])],
package/src/tokens.ts CHANGED
@@ -21,13 +21,20 @@
21
21
  // Harness's Adapter bears one signed for that placement's Service name
22
22
  // (ADR-0031), scoping it to the Menu-only registrations placed there.
23
23
  //
24
+ // And one bearer the other way (ADR-0058), which is no principal here because this server never
25
+ // checks it:
26
+ //
27
+ // Harness bearer what the Orchestrator presents to the Harness in one pod — admit, stream,
28
+ // abort, echo. Derived per placement (`harnessToken`); the Harness holds only
29
+ // its sha-256 (`harnessTokenDigest`), so it verifies and mints nothing.
30
+ //
24
31
  // The Sandbox token is a SIGNED NAME, not a random string in a table: `<sandbox>.<hmac(key, name)>`,
25
32
  // verified by recomputing. Three things fall out that a token table would have to work for — the
26
33
  // Orchestrator holds no per-Sandbox state, `provision` stays idempotent (re-minting yields the same
27
34
  // token, so a re-applied Secret is a no-op), and a token minted before a restart still verifies
28
35
  // after one, which is exactly what ADR-0012's re-attach promises the still-running Adapter.
29
36
 
30
- import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
37
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
31
38
  import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
32
39
  import { join } from "node:path";
33
40
 
@@ -46,6 +53,29 @@ export function sandboxToken(key: Buffer, sandbox: string): string {
46
53
  return `${sandbox}.${sign(key, sandbox)}`;
47
54
  }
48
55
 
56
+ /**
57
+ * The Harness bearer for one placement (ADR-0058): what the Orchestrator presents on every Harness
58
+ * wire call — admit, stream, abort, echo — to the Harness in the pod `placement` names (a
59
+ * Sandbox, or `jr2-instance-harness`). The same signed-name idea as the Sandbox token, turned the
60
+ * other way: here the Orchestrator is the CALLER. Deterministic for the same reason — a restarted
61
+ * Orchestrator re-derives the bearer a live Harness already checks against.
62
+ *
63
+ * Domain-separated from `sandboxToken` by the `harness:` prefix, which no DNS-label name can
64
+ * produce, so no Sandbox token ever doubles as a Harness bearer or the reverse.
65
+ */
66
+ export function harnessToken(key: Buffer, placement: string): string {
67
+ return sign(key, `harness:${placement}`);
68
+ }
69
+
70
+ /**
71
+ * What the Harness in `placement` holds to CHECK its bearer: the sha-256 of it, never the bearer
72
+ * (ADR-0058). The Agent executes code in that container, and a digest inverts to nothing — so the
73
+ * Harness can verify its own bearer and can mint none, for itself or for any other pod.
74
+ */
75
+ export function harnessTokenDigest(key: Buffer, placement: string): string {
76
+ return createHash("sha256").update(harnessToken(key, placement)).digest("base64url");
77
+ }
78
+
49
79
  /** Mint the per-boot Instance token (opaque and random — it names nothing, it just IS the trust). */
50
80
  export function mintInstanceToken(): string {
51
81
  return randomBytes(32).toString("base64url");
package/src/workspace.ts CHANGED
@@ -688,7 +688,7 @@ function buildWorkspaceMachine(body: AnyStateMachine, spec: (args: { input: any
688
688
  const wrapperRef = self._parent;
689
689
  if (!wrapperRef) return;
690
690
  const disposeHandles = registerAmbientHandles(wrapperRef, input.handles);
691
- const detachEcho = runBindingOf(system as AnyActorSystem).echo?.(input.handles.endpoint);
691
+ const detachEcho = runBindingOf(system as AnyActorSystem).echo?.(input.handles.endpoint, input.handles.sandbox);
692
692
  return () => {
693
693
  detachEcho?.();
694
694
  disposeHandles();