@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.
- package/package.json +3 -3
- package/src/actor.ts +157 -62
- package/src/agent.ts +8 -7
- package/src/gate.ts +1 -1
- package/src/harness-client.ts +61 -14
- package/src/http.ts +8 -5
- package/src/instance.ts +13 -10
- package/src/registration.ts +48 -2
- package/src/run-host.ts +54 -11
- package/src/sandbox-kubectl.ts +19 -11
- package/src/server.ts +1 -12
- package/src/setup.ts +110 -35
- package/src/stub-harness.ts +21 -7
- package/src/tokens.ts +31 -1
- package/src/vocabulary.ts +2 -1
- package/src/workspace.ts +1 -1
package/src/stub-harness.ts
CHANGED
|
@@ -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,
|
|
36
|
-
*
|
|
37
|
-
* mechanics-tier test can assert which Agent a
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
| {
|
|
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
|
|
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();
|