@nylorun/runtime 0.52.0-beta → 0.53.0-beta
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/CHANGELOG.md +22 -0
- package/dist/adapters/sandbox/computer.d.ts +28 -0
- package/dist/adapters/sandbox/computer.js +184 -0
- package/dist/artifacts/disk.js +10 -2
- package/dist/harness/computer.d.ts +86 -0
- package/dist/harness/computer.js +273 -0
- package/dist/harness/executors.d.ts +5 -0
- package/dist/harness/executors.js +13 -5
- package/dist/harness/pod.d.ts +2 -0
- package/dist/harness/pod.js +1 -1
- package/dist/harness/service.js +58 -22
- package/dist/harness-api/server.js +17 -0
- package/dist/harness-api/start.js +2 -0
- package/dist/harness-api/workspace.d.ts +5 -0
- package/dist/harness-api/workspace.js +9 -5
- package/dist/host/harness-entry.js +7 -2
- package/dist/host/main.js +13 -0
- package/dist/host/stack-config.d.ts +13 -1
- package/dist/host/stack-config.js +42 -2
- package/dist/management-openapi.json +1 -1
- package/dist/openapi.json +1 -1
- package/dist/sandbox/computer.d.ts +53 -0
- package/dist/sandbox/computer.js +271 -0
- package/dist/sandbox/placement.d.ts +5 -2
- package/dist/sandbox/placement.js +6 -3
- package/dist/sandbox/pods/reconcile.js +1 -0
- package/dist/sandbox/pods/spec.d.ts +5 -0
- package/dist/sandbox/pods/spec.js +1 -1
- package/dist/sandbox/records.d.ts +2 -0
- package/dist/sandbox/records.js +11 -0
- package/dist/sandbox/types.d.ts +5 -2
- package/dist/tenant/advance.js +6 -3
- package/dist/tenant/context.d.ts +3 -0
- package/dist/tenant/runtime.js +34 -0
- package/dist/tenant/sessions.js +2 -1
- package/dist/tenant/sweep.js +2 -1
- package/dist/tenant/transcript.d.ts +8 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -14,6 +14,14 @@ import { serversOf } from "../mcp/pool.js";
|
|
|
14
14
|
import { isHttpToolCall } from "../gates/http-tool.js";
|
|
15
15
|
import { abortOn, callHttpTool, callMcpTool, callSandboxTool, invokeModel, isRemoteMcpCall, } from "./calls.js";
|
|
16
16
|
export function harnessExecutors(options) {
|
|
17
|
+
/** The run's SandboxManager: its computer's, or the harness's own. */
|
|
18
|
+
const managerOf = (run) => {
|
|
19
|
+
if (run.start.routing.sandbox?.computer !== true)
|
|
20
|
+
return options.sandbox();
|
|
21
|
+
if (!options.computer)
|
|
22
|
+
throw new Error("This harness can't reach computers: the run's sandbox is one");
|
|
23
|
+
return options.computer(run);
|
|
24
|
+
};
|
|
17
25
|
const recoversTools = options.toolGate.recovers === true;
|
|
18
26
|
const sandboxTools = sandboxToolLimitsOf(options.sandboxTools);
|
|
19
27
|
const atGate = (effect, run) => {
|
|
@@ -36,7 +44,7 @@ export function harnessExecutors(options) {
|
|
|
36
44
|
return callHttpTool(options.toolGate, effect, abortOn(signal, kinds), traceparent);
|
|
37
45
|
// A sandbox command joins its call's span (R7.4): a CLI that reads TRACEPARENT traces under it.
|
|
38
46
|
if (effect.target?.kind === "sandbox")
|
|
39
|
-
return callSandboxTool(
|
|
47
|
+
return callSandboxTool(managerOf(run), traceparent ? { ...routing, environment: { ...routing.environment, TRACEPARENT: traceparent } } : routing, effect, signal, sandboxTools);
|
|
40
48
|
if (effect.target?.kind !== "mcp")
|
|
41
49
|
throw new Error(`This harness runs no ${effect.target?.kind ?? "untargeted"} call of '${effect.toolName ?? ""}'`);
|
|
42
50
|
return callMcpTool(options.mcp, routing, effect, abortOn(signal, kinds), run.start.limits.toolAnswerBytes, traceparent);
|
|
@@ -58,17 +66,17 @@ export function harnessExecutors(options) {
|
|
|
58
66
|
},
|
|
59
67
|
prepare: (run, record) => prepareMcp(options.mcp, run, record),
|
|
60
68
|
files: {
|
|
61
|
-
read: (run, path, maxBytes, signal) => withSandbox(run, (session, spec) =>
|
|
62
|
-
write: (run, path, body, signal) => withSandbox(run, (session, spec) =>
|
|
69
|
+
read: (run, path, maxBytes, signal) => withSandbox(run, (session, spec) => managerOf(run).readBytes(session, spec, path, maxBytes, signal)),
|
|
70
|
+
write: (run, path, body, signal) => withSandbox(run, (session, spec) => managerOf(run).writeBytes(session, spec, path, body, signal)),
|
|
63
71
|
list: (run, dir, maxEntries, signal) => withSandbox(run, async (session, spec) => {
|
|
64
|
-
const listed = await
|
|
72
|
+
const listed = await managerOf(run).listFiles(session, spec, dir, maxEntries, signal);
|
|
65
73
|
return listed.kind === "listed"
|
|
66
74
|
? { kind: "listed", entries: listed.listing.entries, truncated: listed.listing.truncated }
|
|
67
75
|
: listed.kind === "missing"
|
|
68
76
|
? { kind: "missing" }
|
|
69
77
|
: listed;
|
|
70
78
|
}),
|
|
71
|
-
mount: (run, folder, files, read, signal) => withSandbox(run, (session, spec) =>
|
|
79
|
+
mount: (run, folder, files, read, signal) => withSandbox(run, (session, spec) => managerOf(run).mountFolder(session, spec, folder, files, read, signal)),
|
|
72
80
|
},
|
|
73
81
|
};
|
|
74
82
|
}
|
package/dist/harness/pod.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface DiskSetupReport {
|
|
|
22
22
|
readonly exitCode?: number;
|
|
23
23
|
readonly output?: string;
|
|
24
24
|
}
|
|
25
|
+
/** The variables CLIs read their trust bundle from: OpenSSL, Node, Python, curl, git, AWS. */
|
|
26
|
+
export declare const TRUST_VARIABLES: readonly ["SSL_CERT_FILE", "NODE_EXTRA_CA_CERTS", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "GIT_SSL_CAINFO", "AWS_CA_BUNDLE"];
|
|
25
27
|
export interface PodHost {
|
|
26
28
|
/**
|
|
27
29
|
* A current host token, for the next connection. Throws when neither renewal nor join works,
|
package/dist/harness/pod.js
CHANGED
|
@@ -19,7 +19,7 @@ import { hostTuning } from "../tuning.js";
|
|
|
19
19
|
import { RUNTIME_VERSION } from "../version.js";
|
|
20
20
|
import { IMAGE_ENGINE_HEADER, isBuiltinEngine } from "./image-engine.js";
|
|
21
21
|
/** The variables CLIs read their trust bundle from: OpenSSL, Node, Python, curl, git, AWS. */
|
|
22
|
-
const TRUST_VARIABLES = [
|
|
22
|
+
export const TRUST_VARIABLES = [
|
|
23
23
|
"SSL_CERT_FILE",
|
|
24
24
|
"NODE_EXTRA_CA_CERTS",
|
|
25
25
|
"REQUESTS_CA_BUNDLE",
|
package/dist/harness/service.js
CHANGED
|
@@ -15,7 +15,8 @@ import { DEFAULT_HARNESS, HarnessApiError, } from "@nylorun/core/harness-api";
|
|
|
15
15
|
import { openMcpThrough } from "../gates/tool-gate.js";
|
|
16
16
|
import { McpPool } from "../mcp/pool.js";
|
|
17
17
|
import { SandboxManager } from "../sandbox/manager.js";
|
|
18
|
-
import {
|
|
18
|
+
import { computerBackend } from "../adapters/sandbox/computer.js";
|
|
19
|
+
import { fileSandboxRecords, memorySandboxRecords, workspacePrefix } from "../sandbox/records.js";
|
|
19
20
|
import { defaultSandboxBackends } from "../sandbox/select.js";
|
|
20
21
|
import { harnessExecutors } from "./executors.js";
|
|
21
22
|
import { connectHarness } from "./ws-client.js";
|
|
@@ -57,6 +58,60 @@ export function startHarnessService(options) {
|
|
|
57
58
|
throw new HarnessApiError("unavailable", "The harness has not said hello yet");
|
|
58
59
|
return manager;
|
|
59
60
|
};
|
|
61
|
+
// A claim core refuses (its run ended, the session is gone) is dropped, as core's own
|
|
62
|
+
// SandboxManager drops an event it cannot write.
|
|
63
|
+
const emit = async (sessionId, turnId, type, payload, meta) => {
|
|
64
|
+
const channel = client.channel();
|
|
65
|
+
if (!channel)
|
|
66
|
+
return;
|
|
67
|
+
await channel
|
|
68
|
+
.request("event", {
|
|
69
|
+
...(meta.claim === undefined ? {} : { runId: meta.claim }),
|
|
70
|
+
sessionId,
|
|
71
|
+
turnId,
|
|
72
|
+
type,
|
|
73
|
+
payload,
|
|
74
|
+
...(meta.record ? { record: meta.record } : {}),
|
|
75
|
+
})
|
|
76
|
+
.catch((error) => logger.warn("harness sandbox event not claimed", {
|
|
77
|
+
sessionId,
|
|
78
|
+
type,
|
|
79
|
+
message: error instanceof Error ? error.message : String(error),
|
|
80
|
+
}));
|
|
81
|
+
};
|
|
82
|
+
// Computers (ADR-149): one manager per computer, on the computer backend, opened with
|
|
83
|
+
// `computer.open` for the latest run that used it.
|
|
84
|
+
const computers = new Map();
|
|
85
|
+
const computerOf = (run) => {
|
|
86
|
+
const environmentId = run.start.routing.sandbox?.environmentId;
|
|
87
|
+
if (environmentId === undefined)
|
|
88
|
+
throw new HarnessApiError("invalid", "A computer's run names its Environment");
|
|
89
|
+
const known = computers.get(environmentId);
|
|
90
|
+
if (known) {
|
|
91
|
+
known.runId = run.runId;
|
|
92
|
+
return known.manager;
|
|
93
|
+
}
|
|
94
|
+
const entry = { runId: run.runId };
|
|
95
|
+
entry.manager = new SandboxManager({
|
|
96
|
+
scope: tenantId,
|
|
97
|
+
records: memorySandboxRecords(),
|
|
98
|
+
backends: [
|
|
99
|
+
computerBackend({
|
|
100
|
+
open: async (signal) => {
|
|
101
|
+
const channel = client.channel();
|
|
102
|
+
if (!channel)
|
|
103
|
+
throw new HarnessApiError("unavailable", "Not connected to core");
|
|
104
|
+
return channel.request("computer.open", { runId: entry.runId, environmentId }, signal);
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
],
|
|
108
|
+
preference: "auto",
|
|
109
|
+
ephemeral: false,
|
|
110
|
+
emit,
|
|
111
|
+
});
|
|
112
|
+
computers.set(environmentId, entry);
|
|
113
|
+
return entry.manager;
|
|
114
|
+
};
|
|
60
115
|
const pool = new McpPool({ openRemote: openMcpThrough(options.toolGate) });
|
|
61
116
|
// How often the harness closes MCP connections idle past their timeout.
|
|
62
117
|
const sweep = setInterval(() => void pool.sweep().catch(() => undefined), hostTuning().harness.mcpSweepMs);
|
|
@@ -75,6 +130,7 @@ export function startHarnessService(options) {
|
|
|
75
130
|
toolGate: options.toolGate,
|
|
76
131
|
mcp: pool,
|
|
77
132
|
sandbox,
|
|
133
|
+
computer: (run) => computerOf(run),
|
|
78
134
|
...(sandboxTools ? { sandboxTools } : {}),
|
|
79
135
|
});
|
|
80
136
|
const serve = async (method, params, signal) => {
|
|
@@ -142,27 +198,7 @@ export function startHarnessService(options) {
|
|
|
142
198
|
backends,
|
|
143
199
|
preference: answer.sandbox.backend ?? "auto",
|
|
144
200
|
ephemeral: options.ephemeral === true,
|
|
145
|
-
|
|
146
|
-
// SandboxManager drops an event it cannot write.
|
|
147
|
-
emit: async (sessionId, turnId, type, payload, meta) => {
|
|
148
|
-
const channel = client.channel();
|
|
149
|
-
if (!channel)
|
|
150
|
-
return;
|
|
151
|
-
await channel
|
|
152
|
-
.request("event", {
|
|
153
|
-
...(meta.claim === undefined ? {} : { runId: meta.claim }),
|
|
154
|
-
sessionId,
|
|
155
|
-
turnId,
|
|
156
|
-
type,
|
|
157
|
-
payload,
|
|
158
|
-
...(meta.record ? { record: meta.record } : {}),
|
|
159
|
-
})
|
|
160
|
-
.catch((error) => logger.warn("harness sandbox event not claimed", {
|
|
161
|
-
sessionId,
|
|
162
|
-
type,
|
|
163
|
-
message: error instanceof Error ? error.message : String(error),
|
|
164
|
-
}));
|
|
165
|
-
},
|
|
201
|
+
emit,
|
|
166
202
|
});
|
|
167
203
|
},
|
|
168
204
|
...(options.onGrant ? { onGrant: options.onGrant } : {}),
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
* the sandbox's (`revokeHost`: a new join, a stop, a loss) is closed; its runs end as
|
|
28
28
|
* `connection.lost`.
|
|
29
29
|
*/
|
|
30
|
+
import { openComputer } from "../sandbox/computer.js";
|
|
31
|
+
import { HttpError } from "../tenant/http.js";
|
|
30
32
|
import { randomUUID } from "node:crypto";
|
|
31
33
|
import { DEFAULT_HARNESS, HARNESS_API_VERSION, HARNESS_CLAIMS, isHarnessId, HarnessApiError, } from "@nylorun/core/harness-api";
|
|
32
34
|
import { definitionFilesOf, limitExceededMessage } from "@nylorun/core/contracts";
|
|
@@ -186,6 +188,21 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
186
188
|
return recordMcp(run, p);
|
|
187
189
|
case "definition.file":
|
|
188
190
|
return definitionFile(run, p, signal);
|
|
191
|
+
case "computer.open": {
|
|
192
|
+
// Only the computer of the run's own session (ADR-149).
|
|
193
|
+
const { environmentId } = p;
|
|
194
|
+
const sandbox = run.offer.start.routing.sandbox;
|
|
195
|
+
if (sandbox?.computer !== true || sandbox.environmentId !== environmentId)
|
|
196
|
+
throw new HarnessApiError("invalid", "This run has no such computer");
|
|
197
|
+
try {
|
|
198
|
+
return await openComputer(ctx, environmentId, signal);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (error instanceof HttpError)
|
|
202
|
+
throw new HarnessApiError(error.status === 409 || error.status === 503 ? "unavailable" : "invalid", error.message);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
189
206
|
case "artifact.read":
|
|
190
207
|
return artifacts.read(artifactScope(run), p, signal);
|
|
191
208
|
case "artifact.write":
|
|
@@ -74,6 +74,8 @@ options = {}) {
|
|
|
74
74
|
...workspace,
|
|
75
75
|
...(Object.keys(environment).length > 0 ? { environment } : {}),
|
|
76
76
|
...(current.sandbox === undefined ? {} : { spec: current.sandbox }),
|
|
77
|
+
// A pod Agent's sandbox is a computer when the Tenant's are (ADR-149).
|
|
78
|
+
...(resource?.kind === "pod" && ctx.pods?.computer ? { computer: true } : {}),
|
|
77
79
|
};
|
|
78
80
|
const turnId = current.activeTurnId;
|
|
79
81
|
const noted = current.memoryIndex;
|
|
@@ -50,5 +50,10 @@ export declare function remoteWorkspace(options: RemoteWorkspaceOptions): Worksp
|
|
|
50
50
|
*/
|
|
51
51
|
export declare function withPodWorkspaces(base: WorkspacePort, options: Omit<RemoteWorkspaceOptions, "pod"> & {
|
|
52
52
|
readonly isPod: (environmentId: string) => Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Computers (ADR-149): a pod Agent's workspace, reached through the sandbox gateway rather
|
|
55
|
+
* than a pod's host connection.
|
|
56
|
+
*/
|
|
57
|
+
readonly computer?: (environmentId: string) => WorkspacePort;
|
|
53
58
|
}): WorkspacePort;
|
|
54
59
|
export {};
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* stop idle workspaces and list them (`workspace.sweep`), decides which owners are gone, and
|
|
13
13
|
* removes theirs (`workspace.remove`); it also keeps the `sandboxes` table in step with the
|
|
14
14
|
* list.
|
|
15
|
+
* - **Computers** (ADR-149): a pod Agent's workspace when the Tenant's pod Agents are computers,
|
|
16
|
+
* through a SandboxManager on the computer backend (`sandbox/computer.ts`).
|
|
15
17
|
* - **Pod sandboxes** (F7.2, `withPodWorkspaces`): a session attached to a pod sandbox has its
|
|
16
18
|
* workspace in the pod, served by the sandbox's engine (its host connection). Its tool calls
|
|
17
19
|
* and disk operations go there whatever serves the other workspaces; with the pod stopped or
|
|
@@ -165,11 +167,13 @@ export function remoteWorkspace(options) {
|
|
|
165
167
|
*/
|
|
166
168
|
export function withPodWorkspaces(base, options) {
|
|
167
169
|
const routed = async (session) => session.environmentId !== undefined && (await options.isPod(session.environmentId))
|
|
168
|
-
?
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
? options.computer
|
|
171
|
+
? options.computer(session.environmentId)
|
|
172
|
+
: remoteWorkspace({
|
|
173
|
+
...options,
|
|
174
|
+
pod: session.environmentId,
|
|
175
|
+
...(session.hostEpoch === undefined ? {} : { minEpoch: session.hostEpoch }),
|
|
176
|
+
})
|
|
173
177
|
: base;
|
|
174
178
|
return {
|
|
175
179
|
...base,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pod's engine (`--service harness` in a sandbox pod), on its own: what `main()` does for that
|
|
2
|
+
* The pod's engine (`--service harness` in a sandbox pod, or `--service computer` in a computer), on its own: what `main()` does for that
|
|
3
3
|
* service, without importing the rest of the Host. `scripts/bundle-harness.mjs` bundles this file
|
|
4
4
|
* into one script, which the Runtime image ships as `/engine/harness.mjs` and the sandboxes service
|
|
5
5
|
* copies into each pod instead of `/app` (ADR-92 §5: 12 files instead of about 30,000).
|
|
@@ -7,13 +7,18 @@
|
|
|
7
7
|
import { baselineEnvironment } from "./environment.js";
|
|
8
8
|
import { parseStackConfig } from "./stack-config.js";
|
|
9
9
|
import { runHarness } from "../harness/main.js";
|
|
10
|
+
import { runComputer } from "../harness/computer.js";
|
|
10
11
|
import { useHostTuning } from "../tuning.js";
|
|
11
12
|
import { useHostIdentity } from "../vault/host-identity.js";
|
|
12
13
|
import { useWorkloadToken } from "../vault/workload-token.js";
|
|
13
14
|
async function start() {
|
|
14
15
|
const stack = parseStackConfig(process.env, process.argv.slice(2));
|
|
16
|
+
if (stack.computer) {
|
|
17
|
+
await runComputer(stack.computer, baselineEnvironment(process.env));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
15
20
|
if (!stack.services.has("harness"))
|
|
16
|
-
throw new Error("This entry runs only --service harness");
|
|
21
|
+
throw new Error("This entry runs only --service harness or computer");
|
|
17
22
|
if (stack.tuning)
|
|
18
23
|
useHostTuning(stack.tuning.values, stack.tuning.overrides);
|
|
19
24
|
useWorkloadToken(stack.workloadToken ?? {});
|
package/dist/host/main.js
CHANGED
|
@@ -61,6 +61,8 @@ import { httpModelGate } from "../gates/http-client.js";
|
|
|
61
61
|
import { createS3BlobStore } from "../blob/index.js";
|
|
62
62
|
import { createRunGrants } from "../tenant/run-grants.js";
|
|
63
63
|
import { runHarness } from "../harness/main.js";
|
|
64
|
+
import { runComputer } from "../harness/computer.js";
|
|
65
|
+
import { loadTicketKey, TICKET_TTL_MS } from "../sandbox/computer.js";
|
|
64
66
|
import { startHarnessListener } from "../harness-api/ws-server.js";
|
|
65
67
|
import { httpSandboxesClient } from "../sandbox/pods/client.js";
|
|
66
68
|
import { createCredentialProvider } from "../gates/credential-provider.js";
|
|
@@ -234,6 +236,8 @@ export async function main() {
|
|
|
234
236
|
return runGates(stack);
|
|
235
237
|
if (stack.services.has("harness"))
|
|
236
238
|
return runHarness(stack, baselineEnvironment(process.env));
|
|
239
|
+
if (stack.computer)
|
|
240
|
+
return runComputer(stack.computer, baselineEnvironment(process.env));
|
|
237
241
|
const hostRoot = resolveHostRoot();
|
|
238
242
|
const paths = hostPaths(hostRoot);
|
|
239
243
|
mkdirSync(paths.home, { recursive: true });
|
|
@@ -385,6 +389,15 @@ export async function main() {
|
|
|
385
389
|
? {
|
|
386
390
|
client: httpSandboxesClient({ url: stack.sandboxes.url, token: stack.sandboxes.token }),
|
|
387
391
|
harnessImage: stack.sandboxes.harnessImage,
|
|
392
|
+
...(stack.sandboxes.computer
|
|
393
|
+
? {
|
|
394
|
+
computer: {
|
|
395
|
+
gatewayUrl: stack.sandboxes.computer.gatewayUrl,
|
|
396
|
+
ticketKey: loadTicketKey(stack.sandboxes.computer.ticketKeyFile),
|
|
397
|
+
ttlMs: TICKET_TTL_MS,
|
|
398
|
+
},
|
|
399
|
+
}
|
|
400
|
+
: {}),
|
|
388
401
|
}
|
|
389
402
|
: undefined;
|
|
390
403
|
// The outside credential provider: each pod Agent segment's variables it names (CP-E).
|
|
@@ -50,8 +50,9 @@ import { type TraceResource } from "../traces/otlp.js";
|
|
|
50
50
|
import { type TraceContentKind } from "../traces/map.js";
|
|
51
51
|
import { type BlockedAddress } from "../sandbox/pods/network-gate.js";
|
|
52
52
|
import { type HostTuning } from "../tuning.js";
|
|
53
|
+
import type { ComputerServiceConfig } from "../harness/computer.js";
|
|
53
54
|
/** A Runtime service this release has. */
|
|
54
|
-
export type RuntimeService = "core" | "loop" | "gates" | "keys" | "egress" | "harness";
|
|
55
|
+
export type RuntimeService = "core" | "loop" | "gates" | "keys" | "egress" | "harness" | "computer";
|
|
55
56
|
export type RuntimeServices = ReadonlySet<RuntimeService>;
|
|
56
57
|
export declare const RUNTIME_SERVICES: readonly RuntimeService[];
|
|
57
58
|
/** Where the gates service listens by default. */
|
|
@@ -148,6 +149,15 @@ export interface SandboxesConfig {
|
|
|
148
149
|
token: string;
|
|
149
150
|
/** `NYLORUN_SANDBOX_HARNESS_IMAGE` (the stack sets it to the Runtime image). */
|
|
150
151
|
harnessImage: string;
|
|
152
|
+
/**
|
|
153
|
+
* Computers (ADR-149): `NYLORUN_SANDBOX_GATEWAY_URL`, the sandbox gateway, and
|
|
154
|
+
* `NYLORUN_COMPUTER_TICKET_KEY_FILE`, the key core signs tickets with. With them, every pod
|
|
155
|
+
* Agent is a computer: its runs go to the harness container.
|
|
156
|
+
*/
|
|
157
|
+
computer?: {
|
|
158
|
+
gatewayUrl: string;
|
|
159
|
+
ticketKeyFile: string;
|
|
160
|
+
};
|
|
151
161
|
}
|
|
152
162
|
/**
|
|
153
163
|
* The engine in a pod sandbox (`--service harness` with `NYLORUN_SANDBOX_KIND=pod`, F7.2): no
|
|
@@ -226,6 +236,8 @@ export interface StackConfig {
|
|
|
226
236
|
egress?: EgressConfig;
|
|
227
237
|
/** Present when the process runs the harness service (`--service harness`). */
|
|
228
238
|
harness?: HarnessServiceConfig;
|
|
239
|
+
/** `--service computer` (ADR-149): the engine in a computer. */
|
|
240
|
+
computer?: ComputerServiceConfig;
|
|
229
241
|
/**
|
|
230
242
|
* `NYLORUN_HARNESS` (`in-process` or `remote`; absent means `in-process`), for a process running
|
|
231
243
|
* core or loop: whether the Tenant runs its own harness, or harnesses connect to
|
|
@@ -53,7 +53,7 @@ import { LOOPBACK_HOSTS } from "@nylorun/core/contracts";
|
|
|
53
53
|
import { blockedAddresses } from "../sandbox/pods/network-gate.js";
|
|
54
54
|
import { HostTuningError, parseHostTuning } from "../tuning.js";
|
|
55
55
|
import { HOST_IDENTITY_PROVIDERS } from "../vault/host-identity.js";
|
|
56
|
-
export const RUNTIME_SERVICES = ["core", "loop", "gates", "keys", "egress", "harness"];
|
|
56
|
+
export const RUNTIME_SERVICES = ["core", "loop", "gates", "keys", "egress", "harness", "computer"];
|
|
57
57
|
/**
|
|
58
58
|
* Services that may share a process (D12): they hold the same secrets and parse the same
|
|
59
59
|
* trust class of input. keys joins gates (F4.2), and egress joins them (F7.2). The harness
|
|
@@ -63,6 +63,7 @@ const SERVICE_GROUPS = [
|
|
|
63
63
|
["core", "loop"],
|
|
64
64
|
["gates", "keys", "egress"],
|
|
65
65
|
["harness"],
|
|
66
|
+
["computer"],
|
|
66
67
|
];
|
|
67
68
|
/** Where the gates service listens by default. */
|
|
68
69
|
export const DEFAULT_GATES_LISTEN_PORT = 4100;
|
|
@@ -264,6 +265,8 @@ export function parseStackConfig(env, argv) {
|
|
|
264
265
|
const tuning = parseTuning(env);
|
|
265
266
|
if (services.has("harness"))
|
|
266
267
|
return { services, harness: parseHarnessService(env), endpoints: {}, ...(tuning ? { tuning } : {}) };
|
|
268
|
+
if (services.has("computer"))
|
|
269
|
+
return { services, computer: parseComputerService(env), endpoints: {}, ...(tuning ? { tuning } : {}) };
|
|
267
270
|
// The image sets NYLORUN_LISTEN_*: only the API's processes read them.
|
|
268
271
|
const servesApi = services.has("core") || services.has("loop");
|
|
269
272
|
const listen = servesApi ? parseListen(env) : undefined;
|
|
@@ -567,7 +570,16 @@ function parseSandboxes(env) {
|
|
|
567
570
|
const harnessImage = read(env, "NYLORUN_SANDBOX_HARNESS_IMAGE");
|
|
568
571
|
if (harnessImage === undefined || /\s/.test(harnessImage))
|
|
569
572
|
throw new StackConfigError("NYLORUN_SANDBOX_HARNESS_IMAGE is required with NYLORUN_SANDBOXES_URL: the Runtime image sandbox pods copy the engine from (`nylorun start` sets it)");
|
|
570
|
-
|
|
573
|
+
const gatewayUrl = parseUrl(env, "NYLORUN_SANDBOX_GATEWAY_URL", ["http:", "https:"]);
|
|
574
|
+
const ticketKeyFile = read(env, "NYLORUN_COMPUTER_TICKET_KEY_FILE");
|
|
575
|
+
if ((gatewayUrl === undefined) !== (ticketKeyFile === undefined))
|
|
576
|
+
throw new StackConfigError("NYLORUN_SANDBOX_GATEWAY_URL and NYLORUN_COMPUTER_TICKET_KEY_FILE go together: computers need the sandbox gateway and the key core signs their tickets with");
|
|
577
|
+
return {
|
|
578
|
+
url: url.replace(/\/+$/, ""),
|
|
579
|
+
token,
|
|
580
|
+
harnessImage,
|
|
581
|
+
...(gatewayUrl && ticketKeyFile ? { computer: { gatewayUrl: gatewayUrl.replace(/\/+$/, ""), ticketKeyFile } } : {}),
|
|
582
|
+
};
|
|
571
583
|
}
|
|
572
584
|
/**
|
|
573
585
|
* `StackConfig.harnessListener` from `NYLORUN_HARNESS_LISTEN_*` and `NYLORUN_HARNESS_TOKEN`
|
|
@@ -585,6 +597,34 @@ function parseHarnessListener(env, tokenRequired = true) {
|
|
|
585
597
|
}
|
|
586
598
|
/** What a harness process must not hold: it runs next to agent code. */
|
|
587
599
|
const HARNESS_REFUSED = ["NYLORUN_DATABASE_URL", "NYLORUN_GATES_TOKEN", "NYLORUN_KEYS_URL"];
|
|
600
|
+
/**
|
|
601
|
+
* The engine in a computer (ADR-149), from the variables its template carries: the port, the
|
|
602
|
+
* ticket key's public half and the file with the actor's name. It holds no Runtime credential.
|
|
603
|
+
*/
|
|
604
|
+
function parseComputerService(env) {
|
|
605
|
+
const held = [...HARNESS_REFUSED, "NYLORUN_HARNESS_TOKEN", "NYLORUN_JOIN_TOKEN"].filter((name) => read(env, name) !== undefined);
|
|
606
|
+
if (held.length > 0)
|
|
607
|
+
throw new StackConfigError(`--service computer refuses to start with ${held.join(", ")} set: a computer holds no Runtime credential`);
|
|
608
|
+
const ticketKey = read(env, "NYLORUN_COMPUTER_TICKET_KEY");
|
|
609
|
+
if (ticketKey === undefined || !ticketKey.includes("PUBLIC KEY"))
|
|
610
|
+
throw new StackConfigError("NYLORUN_COMPUTER_TICKET_KEY is required for --service computer: the PEM public key tickets are checked with");
|
|
611
|
+
const nameFile = read(env, "NYLORUN_COMPUTER_NAME_FILE");
|
|
612
|
+
if (nameFile === undefined)
|
|
613
|
+
throw new StackConfigError("NYLORUN_COMPUTER_NAME_FILE is required for --service computer: the file holding the actor's name");
|
|
614
|
+
const rawPort = read(env, "NYLORUN_COMPUTER_PORT");
|
|
615
|
+
const root = read(env, "NYLORUN_HARNESS_ROOT") ?? "/harness";
|
|
616
|
+
if (!root.startsWith("/"))
|
|
617
|
+
throw new StackConfigError(`NYLORUN_HARNESS_ROOT must be an absolute path, not ${root}`);
|
|
618
|
+
const egressCaFile = read(env, "NYLORUN_SANDBOX_EGRESS_CA_FILE");
|
|
619
|
+
return {
|
|
620
|
+
port: rawPort === undefined ? 8080 : parsePort("NYLORUN_COMPUTER_PORT", rawPort),
|
|
621
|
+
ticketKey,
|
|
622
|
+
nameFile,
|
|
623
|
+
root,
|
|
624
|
+
...(egressCaFile ? { egressCaFile } : {}),
|
|
625
|
+
substrate: read(env, "NYLORUN_SANDBOX_SUBSTRATE") === "1",
|
|
626
|
+
};
|
|
627
|
+
}
|
|
588
628
|
/** `StackConfig.harness` from `NYLORUN_HARNESS_*` and `NYLORUN_GATES_URL`. */
|
|
589
629
|
function parseHarnessService(env) {
|
|
590
630
|
const held = [
|
package/dist/openapi.json
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computers in core (ADR-149, ADR-150): a pod Agent on an Agent Substrate cluster whose sandboxes
|
|
3
|
+
* service renders computers (`NYLORUN_SANDBOX_GATEWAY_URL` set). Its engine runs no loop: its
|
|
4
|
+
* sessions' runs go to the harness container, and its commands and files go through the sandbox
|
|
5
|
+
* gateway with a ticket core signs here (`computer-ticket.key`, Ed25519; ADR-148).
|
|
6
|
+
*
|
|
7
|
+
* - `computerAccess` wakes the computer when it isn't running (as a turn start does), waits until
|
|
8
|
+
* the sandboxes service says it is ready (its wake-up probe answered), records its first contact
|
|
9
|
+
* (`agent.running` or `agent.relaunched`, the pod's UID for the lost check), and answers where it
|
|
10
|
+
* is and a ticket. Core's own uses (the sandbox route, disk versions, artifacts) open it so.
|
|
11
|
+
* - `openComputer` is what a harness gets with `computer.open`: the same, once a seed being copied
|
|
12
|
+
* onto a new volume is written (R2d step 3), with the disk's setup run through the computer when
|
|
13
|
+
* its marker isn't the recipe's (R2d step 2; `prepareComputerDisk`), and a version's start check
|
|
14
|
+
* passed (T3).
|
|
15
|
+
*/
|
|
16
|
+
import { type KeyObject } from "node:crypto";
|
|
17
|
+
import type { TenantContext } from "../tenant/context.js";
|
|
18
|
+
import { type ComputerAccess } from "../adapters/sandbox/computer.js";
|
|
19
|
+
import { SandboxManager } from "./manager.js";
|
|
20
|
+
import type { SandboxHandle } from "./types.js";
|
|
21
|
+
/** How a Tenant reaches its computers. */
|
|
22
|
+
export interface TenantComputers {
|
|
23
|
+
/** The sandbox gateway's base URL (`http://sandbox-gateway:4400`). */
|
|
24
|
+
readonly gatewayUrl: string;
|
|
25
|
+
/** The ticket key's private half. */
|
|
26
|
+
readonly ticketKey: KeyObject;
|
|
27
|
+
/** How long a ticket holds (10 minutes). */
|
|
28
|
+
readonly ttlMs: number;
|
|
29
|
+
}
|
|
30
|
+
export declare const TICKET_TTL_MS: number;
|
|
31
|
+
/** Reads `NYLORUN_COMPUTER_TICKET_KEY_FILE`'s key (PKCS #8 PEM, Ed25519). */
|
|
32
|
+
export declare function loadTicketKey(path: string): KeyObject;
|
|
33
|
+
/** A computer ticket for sandbox `subject`, signed with `key`: a compact JWS (EdDSA). */
|
|
34
|
+
export declare function signTicket(key: KeyObject, subject: string, expiresAt: number): string;
|
|
35
|
+
/** Whether pod Environments of this Tenant are computers. */
|
|
36
|
+
export declare function computersOf(ctx: Pick<TenantContext, "pods">): TenantComputers | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Wakes computer `id` if needed, waits until it is ready, and answers where it is with a ticket.
|
|
39
|
+
* Refused for a lost, deleted or failed one.
|
|
40
|
+
*/
|
|
41
|
+
export declare function computerAccess(ctx: TenantContext, id: string, signal?: AbortSignal): Promise<ComputerAccess>;
|
|
42
|
+
/**
|
|
43
|
+
* What `computer.open` answers a harness: access to computer `id`, once a seed is written, its
|
|
44
|
+
* setup ran, and a version's start check passed. Concurrent opens of one computer share one.
|
|
45
|
+
*/
|
|
46
|
+
export declare function openComputer(ctx: TenantContext, id: string, signal?: AbortSignal): Promise<ComputerAccess>;
|
|
47
|
+
/** The disk's setup, run through the computer when its marker isn't the recipe's (R2d step 2). */
|
|
48
|
+
export declare function prepareComputerDisk(ctx: TenantContext, id: string, handle: SandboxHandle, signal?: AbortSignal): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* A SandboxManager per computer, for core's own uses: its backend opens the computer with
|
|
51
|
+
* `computerAccess` (no setup run: the harness's `computer.open` does that before a turn's tools).
|
|
52
|
+
*/
|
|
53
|
+
export declare function computerManagers(ctx: () => TenantContext, emit: ConstructorParameters<typeof SandboxManager>[0]["emit"]): (environmentId: string) => SandboxManager;
|