@jr2/orchestrator 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,8 @@
6
6
  // silent real Harness, and e2e drives it by playing the agent against `/mcp/<iid>` instead.
7
7
  //
8
8
  // Wire (ADR-0027 — the normative model of the five endpoints; `@jr2/harness` serves it for real):
9
- // POST /agents/:name/:id {message, definition, model?, thinkingLevel?} → 200 { streamUrl, offset, submissionId }
9
+ // POST /agents/:name/:id {message, cwd?, definition, model?, thinkingLevel?} → 200 { streamUrl, offset, submissionId }
10
+ // (`message` and `cwd` are the Turn's Frame — what it is about and where it works, ADR-0057)
10
11
  // GET /agents/:name/:id?offset=…[&view=updates] → 200 `[]` + Stream-Next-Offset/Up-To-Date
11
12
  // GET /agents/:name/:id?…&live=long-poll → parked; 204 + same headers on timeout
12
13
  // POST /agents/:name/:id/abort → 200 { aborted }
@@ -32,15 +33,19 @@
32
33
  import { createServer } from "node:http";
33
34
  import type { AddressInfo, Socket } from "node:net";
34
35
 
35
- /** One admission of an Agent: which slot, which durable exchange, the prompt, the DEFINITION the
36
- * Machine's slot carried here (ADR-0049), and this Submission's dials (ADR-0018) — captured so a
37
- * mechanics-tier test can assert which Agent a state ran and at what settings, exactly as it
38
- * asserts the prompt. The stub is inert and never READS the definition: a real Harness runs it,
39
- * this one only proves it rode the wire. */
36
+ /** One admission of an Agent: which slot, which durable exchange, this Turn's FRAME (the prompt
37
+ * and where it works — ADR-0057), the DEFINITION the Machine's slot carried here (ADR-0049), and
38
+ * this Submission's dials (ADR-0018) — captured so a mechanics-tier test can assert which Agent a
39
+ * state ran, at what settings and in which directory, exactly as it asserts the prompt. The stub
40
+ * is inert and never READS any of it: a real Harness runs the Turn, this one only proves each
41
+ * piece rode the wire. */
40
42
  export type Admission = {
41
43
  agentName: string;
42
44
  instanceId: string;
43
45
  message?: string;
46
+ /** Where the Working tools would be rooted (ADR-0057). Absent when the Turn carried no `cwd`,
47
+ * which is the workspace-less run a real Harness answers with `/work`. */
48
+ cwd?: string;
44
49
  definition?: Record<string, unknown>;
45
50
  model?: string;
46
51
  thinkingLevel?: string;
@@ -122,12 +127,21 @@ export async function startStubHarness(opts: StubHarnessOptions = {}): Promise<R
122
127
  req.on("data", (chunk: Buffer) => (body += chunk));
123
128
  req.on("end", () => {
124
129
  const sent = safeParse(body) as
125
- | { message?: string; definition?: Record<string, unknown>; model?: string; thinkingLevel?: string }
130
+ | {
131
+ message?: string;
132
+ cwd?: string;
133
+ definition?: Record<string, unknown>;
134
+ model?: string;
135
+ thinkingLevel?: string;
136
+ }
126
137
  | undefined;
127
138
  const admission: Admission = {
128
139
  agentName: decodeURIComponent(agentName),
129
140
  instanceId: decodeURIComponent(instanceId),
130
141
  message: sent?.message,
142
+ // Recorded only when the Frame carried one, so an admission without a `cwd` reads as
143
+ // the absence it is rather than a directory the state never named (ADR-0057).
144
+ ...(sent?.cwd === undefined ? {} : { cwd: sent.cwd }),
131
145
  ...(sent?.definition ? { definition: sent.definition } : {}),
132
146
  ...(sent?.model ? { model: sent.model } : {}),
133
147
  ...(sent?.thinkingLevel ? { thinkingLevel: sent.thinkingLevel } : {}),
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/vocabulary.ts CHANGED
@@ -82,7 +82,8 @@ export function inputSchemaOf(machine: AnyStateMachine): z.ZodObject | undefined
82
82
  /**
83
83
  * What the HOST adds to the input of the machine a run STARTS with, beside the door
84
84
  * (`RunHost.start`). One field today: the run's seed Instance ID, minted with the run and
85
- * reported by `jr2 status`, so an external caller can address the run's first conversation.
85
+ * reported by `jr2 status`. It names the RUN, never a conversation — jr2 mints every Agent
86
+ * Instance ID in the input mapper (ADR-0016, ADR-0057), and a Machine cannot pass one in.
86
87
  *
87
88
  * It is deliberately NOT door material (ADR-0033): no caller sends it — `start` overwrites
88
89
  * whatever arrived, after the parse — and it is never served as JSON Schema, so putting it in a
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();