@nylorun/runtime 0.15.1-beta → 0.16.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 +82 -0
- package/README.md +33 -6
- package/dist/adapters/execution/restate.d.ts +9 -1
- package/dist/adapters/execution/restate.js +64 -5
- package/dist/adapters/replication/pgoutput.js +3 -1
- package/dist/adapters/sandbox/local.d.ts +10 -0
- package/dist/adapters/sandbox/local.js +195 -0
- package/dist/adapters/sandbox/virtual.js +36 -1
- package/dist/admin-openapi.json +37 -3
- package/dist/api/a2a/tasks.js +6 -1
- package/dist/api/ag-ui/history.js +6 -1
- package/dist/api/ag-ui/routes.js +5 -1
- package/dist/api/components.d.ts +2673 -655
- package/dist/api/components.js +17 -1
- package/dist/api/gate/keys.d.ts +4 -3
- package/dist/api/gate/routes.d.ts +19 -3
- package/dist/api/gate/routes.js +162 -53
- package/dist/api/http/app.js +4 -0
- package/dist/api/http/define.d.ts +10 -0
- package/dist/api/http/define.js +2 -1
- package/dist/api/http/routes/artifacts.d.ts +11 -0
- package/dist/api/http/routes/artifacts.js +518 -0
- package/dist/api/http/routes/sandboxes.d.ts +9 -0
- package/dist/api/http/routes/sandboxes.js +152 -0
- package/dist/api/http/routes/sessions.js +3 -1
- package/dist/api/http/routes/tenant.js +36 -2
- package/dist/artifacts/config.d.ts +23 -0
- package/dist/artifacts/config.js +27 -0
- package/dist/artifacts/export.d.ts +8 -0
- package/dist/artifacts/export.js +205 -0
- package/dist/artifacts/files.d.ts +36 -0
- package/dist/artifacts/files.js +45 -0
- package/dist/artifacts/folders.d.ts +60 -0
- package/dist/artifacts/folders.js +209 -0
- package/dist/artifacts/links.d.ts +28 -0
- package/dist/artifacts/links.js +92 -0
- package/dist/artifacts/media-types.d.ts +13 -0
- package/dist/artifacts/media-types.js +80 -0
- package/dist/artifacts/parts.d.ts +34 -0
- package/dist/artifacts/parts.js +38 -0
- package/dist/artifacts/service.d.ts +74 -0
- package/dist/artifacts/service.js +301 -0
- package/dist/artifacts/workspace.d.ts +60 -0
- package/dist/artifacts/workspace.js +57 -0
- package/dist/artifacts/zip.d.ts +10 -0
- package/dist/artifacts/zip.js +126 -0
- package/dist/blob/fs.d.ts +13 -0
- package/dist/blob/fs.js +194 -0
- package/dist/blob/index.d.ts +9 -0
- package/dist/blob/index.js +9 -0
- package/dist/blob/s3.d.ts +42 -0
- package/dist/blob/s3.js +366 -0
- package/dist/blob/types.d.ts +108 -0
- package/dist/blob/types.js +85 -0
- package/dist/core/sandbox-routes.d.ts +2 -2
- package/dist/core/sandbox-routes.js +4 -3
- package/dist/execution/memory.d.ts +11 -1
- package/dist/execution/memory.js +77 -3
- package/dist/execution/types.d.ts +54 -1
- package/dist/execution/types.js +31 -0
- package/dist/gates/contract.d.ts +14 -13
- package/dist/gates/contract.js +13 -12
- package/dist/gates/egress.d.ts +72 -0
- package/dist/gates/egress.js +319 -0
- package/dist/gates/handler.d.ts +3 -0
- package/dist/gates/handler.js +3 -2
- package/dist/gates/http-client.d.ts +3 -2
- package/dist/gates/http-client.js +17 -6
- package/dist/gates/in-process.d.ts +13 -1
- package/dist/gates/in-process.js +11 -0
- package/dist/gates/inflight.d.ts +23 -4
- package/dist/gates/inflight.js +18 -4
- package/dist/gates/model-gate.d.ts +6 -2
- package/dist/gates/tool-calls.d.ts +9 -5
- package/dist/gates/tool-calls.js +6 -6
- package/dist/gates/tool-client.d.ts +9 -2
- package/dist/gates/tool-client.js +56 -22
- package/dist/gates/tool-contract.d.ts +12 -7
- package/dist/gates/tool-contract.js +10 -4
- package/dist/gates/tool-gate.d.ts +1 -0
- package/dist/harness/calls.d.ts +52 -0
- package/dist/harness/calls.js +113 -0
- package/dist/harness/executors.d.ts +22 -0
- package/dist/harness/executors.js +96 -0
- package/dist/harness/main.d.ts +6 -0
- package/dist/harness/main.js +103 -0
- package/dist/harness/pod.d.ts +21 -0
- package/dist/harness/pod.js +119 -0
- package/dist/harness/run-tokens.d.ts +18 -0
- package/dist/harness/run-tokens.js +15 -0
- package/dist/harness/service.d.ts +46 -0
- package/dist/harness/service.js +179 -0
- package/dist/harness/ws-client.d.ts +43 -0
- package/dist/harness/ws-client.js +122 -0
- package/dist/harness/ws-port.d.ts +22 -0
- package/dist/harness/ws-port.js +78 -0
- package/dist/harness-api/in-process.d.ts +24 -0
- package/dist/harness-api/in-process.js +119 -0
- package/dist/harness-api/record.d.ts +44 -0
- package/dist/harness-api/record.js +250 -0
- package/dist/harness-api/renew.d.ts +37 -0
- package/dist/harness-api/renew.js +77 -0
- package/dist/harness-api/server.d.ts +104 -0
- package/dist/harness-api/server.js +408 -0
- package/dist/harness-api/start.d.ts +36 -0
- package/dist/harness-api/start.js +118 -0
- package/dist/harness-api/workspace.d.ts +53 -0
- package/dist/harness-api/workspace.js +201 -0
- package/dist/harness-api/ws-server.d.ts +34 -0
- package/dist/harness-api/ws-server.js +227 -0
- package/dist/host/app.js +23 -2
- package/dist/host/gates.d.ts +27 -2
- package/dist/host/gates.js +46 -1
- package/dist/host/http.d.ts +4 -1
- package/dist/host/http.js +6 -1
- package/dist/host/main.js +135 -27
- package/dist/host/stack-config.d.ts +101 -1
- package/dist/host/stack-config.js +199 -8
- package/dist/mcp/snapshot.d.ts +8 -1
- package/dist/mcp/snapshot.js +28 -0
- package/dist/media.d.ts +5 -24
- package/dist/media.js +5 -0
- package/dist/model/pi-model.d.ts +6 -2
- package/dist/model/pi-model.js +39 -22
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/openapi.json +15245 -8850
- package/dist/record/index.d.ts +1 -0
- package/dist/record/index.js +1 -0
- package/dist/record/sandbox.d.ts +25 -0
- package/dist/record/sandbox.js +43 -0
- package/dist/sandbox/capability.d.ts +3 -0
- package/dist/sandbox/capability.js +10 -0
- package/dist/sandbox/egress-token.d.ts +26 -0
- package/dist/sandbox/egress-token.js +8 -0
- package/dist/sandbox/join.d.ts +37 -0
- package/dist/sandbox/join.js +165 -0
- package/dist/sandbox/manager.d.ts +99 -34
- package/dist/sandbox/manager.js +216 -84
- package/dist/sandbox/placement.d.ts +27 -0
- package/dist/sandbox/placement.js +44 -0
- package/dist/sandbox/pods/client.d.ts +90 -0
- package/dist/sandbox/pods/client.js +79 -0
- package/dist/sandbox/pods/lifecycle.d.ts +91 -0
- package/dist/sandbox/pods/lifecycle.js +178 -0
- package/dist/sandbox/pods/name.d.ts +1 -0
- package/dist/sandbox/pods/name.js +28 -0
- package/dist/sandbox/pods/network-gate.d.ts +25 -0
- package/dist/sandbox/pods/network-gate.js +71 -0
- package/dist/sandbox/pods/reconcile.d.ts +11 -0
- package/dist/sandbox/pods/reconcile.js +152 -0
- package/dist/sandbox/pods/spec.d.ts +45 -0
- package/dist/sandbox/pods/spec.js +62 -0
- package/dist/sandbox/records.d.ts +29 -0
- package/dist/sandbox/records.js +79 -0
- package/dist/sandbox/resolve.d.ts +5 -0
- package/dist/sandbox/resolve.js +3 -2
- package/dist/sandbox/session-sandbox.js +7 -5
- package/dist/sandbox/share.d.ts +11 -0
- package/dist/sandbox/share.js +10 -0
- package/dist/sandbox/tenant-config.d.ts +16 -1
- package/dist/sandbox/tenant-config.js +9 -0
- package/dist/sandbox/types.d.ts +29 -1
- package/dist/sandbox/types.js +13 -1
- package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
- package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
- package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
- package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/record-writer.d.ts +6 -1
- package/dist/store/postgres/record-writer.js +20 -2
- package/dist/store/postgres/schema.d.ts +894 -0
- package/dist/store/postgres/schema.js +130 -2
- package/dist/store/postgres/store.js +289 -4
- package/dist/store/types.d.ts +143 -6
- package/dist/streams/control.d.ts +2 -0
- package/dist/streams/control.js +5 -0
- package/dist/streams/types.d.ts +10 -1
- package/dist/tenant/advance.js +218 -160
- package/dist/tenant/artifact-tool.d.ts +5 -0
- package/dist/tenant/artifact-tool.js +119 -0
- package/dist/tenant/commands.js +30 -5
- package/dist/tenant/context.d.ts +64 -5
- package/dist/tenant/effects.d.ts +19 -13
- package/dist/tenant/effects.js +5 -382
- package/dist/tenant/history.d.ts +13 -23
- package/dist/tenant/history.js +14 -62
- package/dist/tenant/host-token.d.ts +92 -0
- package/dist/tenant/host-token.js +179 -0
- package/dist/tenant/module.js +1 -0
- package/dist/tenant/paths.d.ts +2 -1
- package/dist/tenant/paths.js +3 -1
- package/dist/tenant/reset.d.ts +5 -2
- package/dist/tenant/reset.js +22 -2
- package/dist/tenant/run-grants.d.ts +49 -0
- package/dist/tenant/run-grants.js +62 -0
- package/dist/tenant/run-token.d.ts +70 -0
- package/dist/tenant/run-token.js +138 -0
- package/dist/tenant/runtime.d.ts +38 -0
- package/dist/tenant/runtime.js +120 -32
- package/dist/tenant/sandboxes.d.ts +119 -0
- package/dist/tenant/sandboxes.js +551 -0
- package/dist/tenant/session.d.ts +2 -13
- package/dist/tenant/session.js +4 -28
- package/dist/tenant/sessions.d.ts +5 -0
- package/dist/tenant/sessions.js +28 -1
- package/dist/tenant/status.d.ts +8 -3
- package/dist/tenant/status.js +5 -0
- package/dist/tenant/streams.d.ts +7 -0
- package/dist/tenant/streams.js +24 -1
- package/dist/tenant/sweep.js +37 -4
- package/dist/tenant/tokens.js +9 -2
- package/dist/tenant/types.d.ts +18 -1
- package/dist/tenant/worker.d.ts +11 -5
- package/dist/tenant/worker.js +12 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -10
- package/dist/adapters/media.d.ts +0 -24
- package/dist/adapters/media.js +0 -111
package/dist/sandbox/manager.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { SANDBOX_WORKSPACE, } from "@nylorun/core/define";
|
|
2
|
+
import { sandboxWorkspaceKey, sessionWorkspaceKey, workspacePrefix, } from "./records.js";
|
|
3
3
|
import { DEFAULT_SANDBOX_CPUS, DEFAULT_SANDBOX_IMAGE, describeNetwork, idleMsOf, memoryMiBOf, resolveNetwork, } from "./policy.js";
|
|
4
4
|
import { parseSandboxPreference, reportSelection, selectSandboxBackend, } from "./select.js";
|
|
5
|
-
import { runSandboxTool } from "./tools.js";
|
|
5
|
+
import { resolveSandboxPath, runSandboxTool, } from "./tools.js";
|
|
6
|
+
import { SandboxFileTooLargeError, } from "./types.js";
|
|
7
|
+
export { sandboxCapabilityOf } from "./capability.js";
|
|
6
8
|
const READ_TOOLS = new Set(["read", "grep", "glob"]);
|
|
7
|
-
/** The sandbox capability that owns this tool call, when the call is a built-in sandbox tool. */
|
|
8
|
-
export function sandboxCapabilityOf(manifest, capabilityId, toolName) {
|
|
9
|
-
if (!manifest || !capabilityId || !toolName || !isSandboxToolName(toolName))
|
|
10
|
-
return undefined;
|
|
11
|
-
const capability = manifest.capabilities.find((item) => item.id === capabilityId);
|
|
12
|
-
return capability?.sandbox && capability.tools?.some((tool) => tool.name === toolName)
|
|
13
|
-
? capability
|
|
14
|
-
: undefined;
|
|
15
|
-
}
|
|
16
9
|
export class SandboxManager {
|
|
17
10
|
options;
|
|
18
11
|
selection;
|
|
@@ -22,21 +15,33 @@ export class SandboxManager {
|
|
|
22
15
|
reconciled = false;
|
|
23
16
|
constructor(options) {
|
|
24
17
|
this.options = options;
|
|
25
|
-
this.prefix =
|
|
18
|
+
this.prefix = workspacePrefix(options.scope);
|
|
26
19
|
}
|
|
27
20
|
/**
|
|
28
|
-
* One
|
|
29
|
-
*
|
|
30
|
-
* call reattaches);
|
|
31
|
-
* - marks records whose compute this process does not hold as stopped (compute is gone
|
|
32
|
-
* after a Runtime restart);
|
|
33
|
-
* - on the first pass of this process, removes sandboxes whose session no longer exists
|
|
34
|
-
* (skipped for ephemeral Runtimes, which delete theirs on close).
|
|
21
|
+
* One sweep pass of core's process: `stopIdle`, then, on the first pass of this process,
|
|
22
|
+
* `reconcile` (skipped for ephemeral Runtimes, which delete theirs on close).
|
|
35
23
|
*/
|
|
36
24
|
async sweep(options) {
|
|
37
25
|
if (this.closing)
|
|
38
26
|
return;
|
|
39
|
-
|
|
27
|
+
await this.stopIdle(options.now);
|
|
28
|
+
if (this.reconciled || this.options.ephemeral)
|
|
29
|
+
return;
|
|
30
|
+
this.reconciled = true;
|
|
31
|
+
if (!(await this.hasRecords()))
|
|
32
|
+
return;
|
|
33
|
+
await this.reconcile(async (sessionId) => [...this.live.values()].some((live) => live.sessionId === sessionId) ||
|
|
34
|
+
(await options.sessionExists(sessionId)), options.sandboxExists ?? (() => true));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Stops sandboxes idle for at least their idle timeout (their files remain; the next call
|
|
38
|
+
* reattaches), and marks records whose compute this process does not hold as stopped
|
|
39
|
+
* (compute is gone after a restart).
|
|
40
|
+
*/
|
|
41
|
+
async stopIdle(at) {
|
|
42
|
+
if (this.closing)
|
|
43
|
+
return;
|
|
44
|
+
const now = at ?? Date.now();
|
|
40
45
|
const idle = (live) => !!live.handle && live.active === 0 && now - live.lastUsedAt >= live.idleMs;
|
|
41
46
|
const stops = [];
|
|
42
47
|
for (const [key, live] of this.live) {
|
|
@@ -49,23 +54,18 @@ export class SandboxManager {
|
|
|
49
54
|
stops.push(live.tail);
|
|
50
55
|
}
|
|
51
56
|
await Promise.all(stops);
|
|
52
|
-
await this.options.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.reconciled = true;
|
|
65
|
-
if (!(await this.hasRecords()))
|
|
66
|
-
return;
|
|
67
|
-
await this.reconcile(async (sessionId) => [...this.live.values()].some((live) => live.sessionId === sessionId) ||
|
|
68
|
-
(await options.sessionExists(sessionId)));
|
|
57
|
+
for (const record of await this.options.records.list()) {
|
|
58
|
+
if (record.state === "stopped")
|
|
59
|
+
continue;
|
|
60
|
+
const live = this.live.get(record.key);
|
|
61
|
+
if (live && (live.handle || live.active > 0))
|
|
62
|
+
continue;
|
|
63
|
+
await this.options.records.put({ ...record, state: "stopped" });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Every workspace record this manager keeps. */
|
|
67
|
+
workspaces() {
|
|
68
|
+
return this.options.records.list();
|
|
69
69
|
}
|
|
70
70
|
/** Backends are probed once, on first use, and the choice holds for the life of the Runtime. */
|
|
71
71
|
get ready() {
|
|
@@ -83,15 +83,126 @@ export class SandboxManager {
|
|
|
83
83
|
}
|
|
84
84
|
/** Whether any sandbox was ever recorded, so startup can skip probing for agents without one. */
|
|
85
85
|
async hasRecords() {
|
|
86
|
-
return (await this.options.
|
|
86
|
+
return (await this.options.records.count()) > 0;
|
|
87
87
|
}
|
|
88
88
|
keyOf(sessionId) {
|
|
89
|
-
return this.
|
|
89
|
+
return sessionWorkspaceKey(this.options.scope, sessionId);
|
|
90
|
+
}
|
|
91
|
+
/** The workspace key of sandbox resource `sandboxId` (`sandboxWorkspaceKey`). */
|
|
92
|
+
sandboxKeyOf(sandboxId) {
|
|
93
|
+
return sandboxWorkspaceKey(this.options.scope, sandboxId);
|
|
94
|
+
}
|
|
95
|
+
keyFor(session) {
|
|
96
|
+
return session.sandboxId === undefined
|
|
97
|
+
? this.keyOf(session.id)
|
|
98
|
+
: this.sandboxKeyOf(session.sandboxId);
|
|
90
99
|
}
|
|
91
100
|
async report() {
|
|
92
101
|
return { ...reportSelection(await this.ready), defaultImage: DEFAULT_SANDBOX_IMAGE };
|
|
93
102
|
}
|
|
94
103
|
async run(session, capability, toolName, input, signal) {
|
|
104
|
+
const prepared = await this.prepare(session, capability);
|
|
105
|
+
if ("kind" in prepared)
|
|
106
|
+
return prepared;
|
|
107
|
+
const { live: entry, spec } = prepared;
|
|
108
|
+
const task = entry.tail.then(() => this.execute(entry, session, spec, toolName, input, signal));
|
|
109
|
+
entry.tail = task.catch(() => undefined);
|
|
110
|
+
return task;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The bytes of one file in the session's sandbox, for `save_artifact` (F8.1), in the same queue
|
|
114
|
+
* as its tool calls: `missing` when there is no such file, a failed outcome when the sandbox
|
|
115
|
+
* cannot open or the file is larger than `maxBytes`. Relative paths resolve in the workspace.
|
|
116
|
+
*/
|
|
117
|
+
async readBytes(session, capability, path, maxBytes, signal) {
|
|
118
|
+
return this.withHandle(session, capability, signal, async (handle, live) => {
|
|
119
|
+
const resolved = resolveSandboxPath(path, handle.workspace ?? SANDBOX_WORKSPACE);
|
|
120
|
+
if (!handle.readBytes)
|
|
121
|
+
return {
|
|
122
|
+
kind: "failed",
|
|
123
|
+
code: "sandbox.unsupported",
|
|
124
|
+
message: `The ${live.backend.name} backend cannot read files as bytes`,
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
const bytes = await handle.readBytes(resolved, maxBytes);
|
|
128
|
+
return bytes === undefined
|
|
129
|
+
? { kind: "missing", path: resolved }
|
|
130
|
+
: { kind: "read", path: resolved, bytes };
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error instanceof SandboxFileTooLargeError)
|
|
134
|
+
return {
|
|
135
|
+
kind: "failed",
|
|
136
|
+
code: "artifact.too_large",
|
|
137
|
+
message: `${resolved} is ${error.size} bytes; an artifact may hold at most ${maxBytes}`,
|
|
138
|
+
};
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The files under directory `dir` of the session's sandbox, for the turn-end export (F8.2), in
|
|
145
|
+
* the same queue as its tool calls. `missing` when the sandbox was never created (nothing is
|
|
146
|
+
* started to look) or `dir` is not a directory. Relative paths resolve in the workspace.
|
|
147
|
+
*/
|
|
148
|
+
async listFiles(session, capability, dir, maxEntries, signal) {
|
|
149
|
+
const key = this.keyFor(session);
|
|
150
|
+
if (!this.live.get(key)?.handle) {
|
|
151
|
+
const record = await this.options.records.get(key);
|
|
152
|
+
if (!record)
|
|
153
|
+
return { kind: "missing", path: dir };
|
|
154
|
+
}
|
|
155
|
+
return this.withHandle(session, capability, signal, async (handle, live) => {
|
|
156
|
+
const resolved = resolveSandboxPath(dir, handle.workspace ?? SANDBOX_WORKSPACE);
|
|
157
|
+
if (!handle.listFiles)
|
|
158
|
+
return {
|
|
159
|
+
kind: "failed",
|
|
160
|
+
code: "sandbox.unsupported",
|
|
161
|
+
message: `The ${live.backend.name} backend cannot list files`,
|
|
162
|
+
};
|
|
163
|
+
const listing = await handle.listFiles(resolved, maxEntries);
|
|
164
|
+
return listing === undefined
|
|
165
|
+
? { kind: "missing", path: resolved }
|
|
166
|
+
: { kind: "listed", path: resolved, listing };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/** Runs `use` on the session's sandbox in its queue, starting it first when it is stopped. */
|
|
170
|
+
async withHandle(session, capability, signal, use) {
|
|
171
|
+
const prepared = await this.prepare(session, capability);
|
|
172
|
+
if ("kind" in prepared)
|
|
173
|
+
return prepared;
|
|
174
|
+
const { live, spec } = prepared;
|
|
175
|
+
const task = live.tail.then(async () => {
|
|
176
|
+
if (signal.aborted)
|
|
177
|
+
throw new Error("Turn cancelled");
|
|
178
|
+
if (this.closing)
|
|
179
|
+
throw new Error("Runtime is shutting down");
|
|
180
|
+
live.active += 1;
|
|
181
|
+
try {
|
|
182
|
+
if (!live.handle) {
|
|
183
|
+
try {
|
|
184
|
+
live.handle = await this.open(live, session, spec);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
return {
|
|
188
|
+
kind: "failed",
|
|
189
|
+
code: "sandbox.start_failed",
|
|
190
|
+
message: `The sandbox could not start: ${error instanceof Error ? error.message : String(error)}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return await use(live.handle, live);
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
live.active -= 1;
|
|
198
|
+
live.lastUsedAt = Date.now();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
live.tail = task.catch(() => undefined);
|
|
202
|
+
return task;
|
|
203
|
+
}
|
|
204
|
+
/** The session's live sandbox entry and spec, or why it cannot run here. */
|
|
205
|
+
async prepare(session, capability) {
|
|
95
206
|
const selection = await this.ready;
|
|
96
207
|
const backend = selection.backend;
|
|
97
208
|
if (!backend)
|
|
@@ -100,7 +211,7 @@ export class SandboxManager {
|
|
|
100
211
|
code: "sandbox.unavailable",
|
|
101
212
|
message: `No sandbox is available: ${selection.reason}. Run \`npx nylorun doctor sandbox\` for options.`,
|
|
102
213
|
};
|
|
103
|
-
const key = this.
|
|
214
|
+
const key = this.keyFor(session);
|
|
104
215
|
const spec = {
|
|
105
216
|
key,
|
|
106
217
|
image: capability.sandbox?.image ?? DEFAULT_SANDBOX_IMAGE,
|
|
@@ -119,6 +230,7 @@ export class SandboxManager {
|
|
|
119
230
|
if (!live) {
|
|
120
231
|
live = {
|
|
121
232
|
sessionId: session.id,
|
|
233
|
+
...(session.sandboxId === undefined ? {} : { sandboxId: session.sandboxId }),
|
|
122
234
|
backend,
|
|
123
235
|
idleMs: idleMsOf(capability.sandbox),
|
|
124
236
|
active: 0,
|
|
@@ -127,10 +239,7 @@ export class SandboxManager {
|
|
|
127
239
|
};
|
|
128
240
|
this.live.set(key, live);
|
|
129
241
|
}
|
|
130
|
-
|
|
131
|
-
const task = entry.tail.then(() => this.execute(entry, session, spec, toolName, input, signal));
|
|
132
|
-
entry.tail = task.catch(() => undefined);
|
|
133
|
-
return task;
|
|
242
|
+
return { live, spec };
|
|
134
243
|
}
|
|
135
244
|
async execute(live, session, spec, toolName, input, signal) {
|
|
136
245
|
if (signal.aborted)
|
|
@@ -153,12 +262,8 @@ export class SandboxManager {
|
|
|
153
262
|
}
|
|
154
263
|
catch (error) {
|
|
155
264
|
const message = error instanceof Error ? error.message : String(error);
|
|
156
|
-
await this.record(spec.key, session.id,
|
|
157
|
-
await this.options.emit(session.id, session.activeTurnId, "sandbox.state", {
|
|
158
|
-
state: "stopped",
|
|
159
|
-
backend: live.backend.name,
|
|
160
|
-
error: message,
|
|
161
|
-
});
|
|
265
|
+
const record = await this.record(spec.key, live, session.id, spec.image, "stopped");
|
|
266
|
+
await this.options.emit(session.id, session.activeTurnId, "sandbox.state", { state: "stopped", backend: live.backend.name, error: message }, { record, ...claimOf(session) });
|
|
162
267
|
// Nothing ran yet, so a failed start is a definite outcome, not an uncertain one.
|
|
163
268
|
return {
|
|
164
269
|
kind: "failed",
|
|
@@ -180,7 +285,7 @@ export class SandboxManager {
|
|
|
180
285
|
outcome: outcome.kind,
|
|
181
286
|
...(outcome.kind === "failed" ? { code: outcome.code } : {}),
|
|
182
287
|
durationMs: Date.now() - started,
|
|
183
|
-
});
|
|
288
|
+
}, claimOf(session));
|
|
184
289
|
return outcome;
|
|
185
290
|
}
|
|
186
291
|
catch (error) {
|
|
@@ -196,28 +301,24 @@ export class SandboxManager {
|
|
|
196
301
|
}
|
|
197
302
|
}
|
|
198
303
|
async open(live, session, spec) {
|
|
199
|
-
const existing = await this.options.
|
|
304
|
+
const existing = await this.options.records.get(spec.key);
|
|
200
305
|
const payload = {
|
|
201
306
|
backend: live.backend.name,
|
|
202
307
|
isolation: live.backend.isolation,
|
|
203
308
|
image: spec.image,
|
|
204
309
|
network: describeNetwork(spec.network),
|
|
205
310
|
};
|
|
206
|
-
await this.record(spec.key, session.id,
|
|
207
|
-
await this.options.emit(session.id, session.activeTurnId, "sandbox.state", {
|
|
208
|
-
state: "creating",
|
|
209
|
-
...payload,
|
|
210
|
-
...(existing ? { reattach: true } : {}),
|
|
211
|
-
});
|
|
311
|
+
const creating = await this.record(spec.key, live, session.id, spec.image, "creating", existing);
|
|
312
|
+
await this.options.emit(session.id, session.activeTurnId, "sandbox.state", { state: "creating", ...payload, ...(existing ? { reattach: true } : {}) }, { record: creating, ...claimOf(session) });
|
|
212
313
|
const handle = await live.backend.open(spec);
|
|
213
|
-
await this.record(spec.key, session.id,
|
|
314
|
+
const running = await this.record(spec.key, live, session.id, spec.image, "running", existing);
|
|
214
315
|
await this.options.emit(session.id, session.activeTurnId, "sandbox.state", {
|
|
215
316
|
state: "running",
|
|
216
317
|
...payload,
|
|
217
318
|
...(existing && handle.created
|
|
218
319
|
? { lost: true, note: "The sandbox was gone and was created again; its earlier files are lost." }
|
|
219
320
|
: {}),
|
|
220
|
-
});
|
|
321
|
+
}, { record: running, ...claimOf(session) });
|
|
221
322
|
return handle;
|
|
222
323
|
}
|
|
223
324
|
async stop(key, live) {
|
|
@@ -229,40 +330,68 @@ export class SandboxManager {
|
|
|
229
330
|
await handle.stop();
|
|
230
331
|
}
|
|
231
332
|
finally {
|
|
232
|
-
await this.record(key, live
|
|
233
|
-
await this.options.emit(live.sessionId, null, "sandbox.state", { state: "stopped", backend: live.backend.name });
|
|
333
|
+
const record = await this.record(key, live, live.sessionId, undefined, "stopped");
|
|
334
|
+
await this.options.emit(live.sessionId, null, "sandbox.state", { state: "stopped", backend: live.backend.name }, { record });
|
|
234
335
|
}
|
|
235
336
|
}
|
|
236
|
-
async record(key,
|
|
337
|
+
async record(key, live, sessionId, image, state, known) {
|
|
237
338
|
const now = new Date().toISOString();
|
|
238
|
-
await this.options.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
339
|
+
const existing = known ?? (await this.options.records.get(key));
|
|
340
|
+
const record = {
|
|
341
|
+
key,
|
|
342
|
+
sessionId,
|
|
343
|
+
...(live.sandboxId === undefined ? {} : { sandboxId: live.sandboxId }),
|
|
344
|
+
backend: live.backend.name,
|
|
345
|
+
image: image ?? existing?.image ?? "",
|
|
346
|
+
state,
|
|
347
|
+
createdAt: existing?.createdAt ?? now,
|
|
348
|
+
updatedAt: now,
|
|
349
|
+
};
|
|
350
|
+
await this.options.records.put(record);
|
|
351
|
+
return record;
|
|
250
352
|
}
|
|
251
353
|
/** The sandbox record for a session, if one was ever created. */
|
|
252
354
|
status(sessionId) {
|
|
253
|
-
return this.options.
|
|
355
|
+
return this.options.records.get(this.keyOf(sessionId));
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Deletes sandbox resource `sandboxId`'s workspace, its files and its compute record, after
|
|
359
|
+
* any command queued on it. Called once the resource is deleted.
|
|
360
|
+
*/
|
|
361
|
+
async removeSandbox(sandboxId) {
|
|
362
|
+
await this.remove(this.sandboxKeyOf(sandboxId));
|
|
363
|
+
}
|
|
364
|
+
/** Deletes the workspace `key`, its files and its record, after any command queued on it. */
|
|
365
|
+
async remove(key) {
|
|
366
|
+
const live = this.live.get(key);
|
|
367
|
+
if (live) {
|
|
368
|
+
await live.tail.catch(() => undefined);
|
|
369
|
+
live.handle = undefined;
|
|
370
|
+
this.live.delete(key);
|
|
371
|
+
}
|
|
372
|
+
const { backend } = await this.ready;
|
|
373
|
+
if (backend)
|
|
374
|
+
await backend.remove(key);
|
|
375
|
+
await this.options.records.delete(key);
|
|
254
376
|
}
|
|
255
|
-
/**
|
|
256
|
-
|
|
377
|
+
/**
|
|
378
|
+
* Delete workspaces this Runtime owns whose owner is gone: a session's when `sessionExists`
|
|
379
|
+
* says the session is gone, a sandbox resource's when `sandboxExists` says the resource is.
|
|
380
|
+
*/
|
|
381
|
+
async reconcile(sessionExists, sandboxExists) {
|
|
257
382
|
const { backend } = await this.ready;
|
|
258
383
|
if (!backend)
|
|
259
384
|
return;
|
|
260
385
|
for (const key of await backend.list(this.prefix)) {
|
|
261
|
-
const record = await this.options.
|
|
262
|
-
if (record
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
386
|
+
const record = await this.options.records.get(key);
|
|
387
|
+
if (record) {
|
|
388
|
+
const kept = record.sandboxId === undefined
|
|
389
|
+
? await sessionExists(record.sessionId)
|
|
390
|
+
: await sandboxExists(record.sandboxId);
|
|
391
|
+
if (kept)
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
await this.remove(key);
|
|
266
395
|
}
|
|
267
396
|
}
|
|
268
397
|
async close() {
|
|
@@ -282,3 +411,6 @@ export class SandboxManager {
|
|
|
282
411
|
this.live.clear();
|
|
283
412
|
}
|
|
284
413
|
}
|
|
414
|
+
function claimOf(session) {
|
|
415
|
+
return session.claim === undefined ? {} : { claim: session.claim };
|
|
416
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placement (F7.2, D38): where a session's harness runs, checked when the session opens and
|
|
3
|
+
* never downgraded afterwards. The Tenant maps harness ids (`nylorun`, or `*` for the rest) to
|
|
4
|
+
* the hosts they may run on (`sandbox.config` `placement`): `harness-container` (the Runtime's
|
|
5
|
+
* harness: in process, or the harness container) and `sandbox` (the engine in a pod sandbox).
|
|
6
|
+
* Default: both.
|
|
7
|
+
*
|
|
8
|
+
* A session attached to a pod sandbox runs on `sandbox`; every other session on
|
|
9
|
+
* `harness-container`. A host the Tenant does not allow is `409 placement_refused`. A pod
|
|
10
|
+
* sandbox needs sandbox pods: without a cluster (`nylorun sandbox enable`), or with the
|
|
11
|
+
* sandboxes service not ready, it is `409 sandbox_unavailable`.
|
|
12
|
+
*/
|
|
13
|
+
import type { SandboxPlacementHost, TenantSandboxConfig } from "@nylorun/core/contracts";
|
|
14
|
+
import type { TenantPods } from "../tenant/context.js";
|
|
15
|
+
/** The harness every session of this Runtime runs (blueprint D38). */
|
|
16
|
+
export declare const HARNESS_ID = "nylorun";
|
|
17
|
+
export declare const DEFAULT_PLACEMENT: readonly SandboxPlacementHost[];
|
|
18
|
+
/** The hosts harness `harnessId` may run on. */
|
|
19
|
+
export declare function placementOf(config: TenantSandboxConfig, harnessId?: string): readonly SandboxPlacementHost[];
|
|
20
|
+
/** How to enable sandbox pods, for the messages that need them. */
|
|
21
|
+
export declare const ENABLE_PODS = "run `nylorun sandbox enable --context <name>` to give this Tenant a cluster";
|
|
22
|
+
/** Refuses a session whose sandbox needs a host the Tenant does not allow. */
|
|
23
|
+
export declare function checkPlacement(config: TenantSandboxConfig, kind: "virtual" | "pod" | undefined): void;
|
|
24
|
+
/** Whether the sandboxes service is up, cached for a few seconds. */
|
|
25
|
+
export declare function podsReady(pods: TenantPods): Promise<boolean>;
|
|
26
|
+
/** Refuses a pod sandbox without sandbox pods, or with the sandboxes service down. */
|
|
27
|
+
export declare function requirePods(pods: TenantPods | undefined, what: string): Promise<TenantPods>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { fail } from "../tenant/http.js";
|
|
2
|
+
/** The harness every session of this Runtime runs (blueprint D38). */
|
|
3
|
+
export const HARNESS_ID = "nylorun";
|
|
4
|
+
export const DEFAULT_PLACEMENT = ["harness-container", "sandbox"];
|
|
5
|
+
/** The hosts harness `harnessId` may run on. */
|
|
6
|
+
export function placementOf(config, harnessId = HARNESS_ID) {
|
|
7
|
+
const placement = config.placement;
|
|
8
|
+
return placement?.[harnessId]?.hosts ?? placement?.["*"]?.hosts ?? DEFAULT_PLACEMENT;
|
|
9
|
+
}
|
|
10
|
+
/** How to enable sandbox pods, for the messages that need them. */
|
|
11
|
+
export const ENABLE_PODS = "run `nylorun sandbox enable --context <name>` to give this Tenant a cluster";
|
|
12
|
+
/** Refuses a session whose sandbox needs a host the Tenant does not allow. */
|
|
13
|
+
export function checkPlacement(config, kind) {
|
|
14
|
+
const host = kind === "pod" ? "sandbox" : "harness-container";
|
|
15
|
+
const allowed = placementOf(config);
|
|
16
|
+
if (!allowed.includes(host))
|
|
17
|
+
fail(409, kind === "pod"
|
|
18
|
+
? `This Tenant does not let the ${HARNESS_ID} harness run in a sandbox pod (placement allows ${allowed.join(", ")}).`
|
|
19
|
+
: `This Tenant runs the ${HARNESS_ID} harness only in sandbox pods (placement allows ${allowed.join(", ")}): attach a pod sandbox with sandbox: { id }.`, { code: "placement_refused", details: { harness: HARNESS_ID, host, allowed: [...allowed] } });
|
|
20
|
+
}
|
|
21
|
+
const READY_CACHE_MS = 5_000;
|
|
22
|
+
const ready = new WeakMap();
|
|
23
|
+
/** Whether the sandboxes service is up, cached for a few seconds. */
|
|
24
|
+
export async function podsReady(pods) {
|
|
25
|
+
const cached = ready.get(pods);
|
|
26
|
+
if (cached && Date.now() - cached.at < READY_CACHE_MS)
|
|
27
|
+
return cached.ready;
|
|
28
|
+
const answer = await pods.client.ready();
|
|
29
|
+
ready.set(pods, { at: Date.now(), ready: answer });
|
|
30
|
+
return answer;
|
|
31
|
+
}
|
|
32
|
+
/** Refuses a pod sandbox without sandbox pods, or with the sandboxes service down. */
|
|
33
|
+
export async function requirePods(pods, what) {
|
|
34
|
+
if (!pods)
|
|
35
|
+
return fail(409, `${what} needs sandbox pods, and this Runtime has no cluster: ${ENABLE_PODS}.`, {
|
|
36
|
+
code: "sandbox_unavailable",
|
|
37
|
+
});
|
|
38
|
+
if (!(await podsReady(pods)))
|
|
39
|
+
fail(409, `${what} needs sandbox pods, and the sandboxes service is not ready. Check \`nylorun sandbox status\`.`, {
|
|
40
|
+
code: "sandbox_unavailable",
|
|
41
|
+
details: { retryAfterSeconds: 5 },
|
|
42
|
+
}, { "retry-after": "5" });
|
|
43
|
+
return pods;
|
|
44
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sandboxes service's client (F7.2, D32): the Go service (`sandboxes/`) that alone holds
|
|
3
|
+
* the cluster credentials and drives agent-sandbox Sandboxes in the Tenant's namespace. Core
|
|
4
|
+
* reaches it at `NYLORUN_SANDBOXES_URL` with `NYLORUN_SANDBOXES_TOKEN`; nothing else does.
|
|
5
|
+
*
|
|
6
|
+
* Pull, not push: the service never calls core. Core reads a Sandbox's status when it
|
|
7
|
+
* reconciles (`GET /v1/pods/{name}`, optionally blocking with `?wait=`), and every change it
|
|
8
|
+
* asks for carries an operation id, so a retried `PUT` is a no-op.
|
|
9
|
+
*/
|
|
10
|
+
/** `PUT /v1/pods/{name}`: the Sandbox the service applies (`driver.Spec` in Go). */
|
|
11
|
+
export interface PodSpec {
|
|
12
|
+
readonly opId: string;
|
|
13
|
+
readonly mode: "Running" | "Suspended";
|
|
14
|
+
/** The workload image; the service defaults it to `python:3.13-slim`. */
|
|
15
|
+
readonly image?: string;
|
|
16
|
+
/** The Runtime image the engine is copied from. */
|
|
17
|
+
readonly harnessImage?: string;
|
|
18
|
+
/** Replaces the engine command (diagnostics). */
|
|
19
|
+
readonly command?: readonly string[];
|
|
20
|
+
readonly cpus?: number;
|
|
21
|
+
readonly memoryMiB?: number;
|
|
22
|
+
readonly storageGiB?: number;
|
|
23
|
+
readonly stopGraceSeconds?: number;
|
|
24
|
+
/** RFC 3339: the Sandbox expires then (agent-sandbox `shutdownTime`). */
|
|
25
|
+
readonly shutdownTime?: string;
|
|
26
|
+
readonly shutdownPolicy?: "Retain" | "Delete";
|
|
27
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
28
|
+
/** Written to the Sandbox's join Secret before it is applied. */
|
|
29
|
+
readonly joinToken?: string;
|
|
30
|
+
}
|
|
31
|
+
/** A Sandbox as the service's informers see it (`driver.Status` in Go). */
|
|
32
|
+
export interface PodStatus {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly exists: boolean;
|
|
35
|
+
readonly deleting: boolean;
|
|
36
|
+
readonly mode?: string;
|
|
37
|
+
readonly ready: boolean;
|
|
38
|
+
readonly suspended: boolean;
|
|
39
|
+
readonly expired: boolean;
|
|
40
|
+
readonly podUID?: string;
|
|
41
|
+
readonly podPhase?: string;
|
|
42
|
+
readonly volume: "present" | "missing";
|
|
43
|
+
readonly opId?: string;
|
|
44
|
+
readonly shutdownTime?: string;
|
|
45
|
+
readonly reason?: string;
|
|
46
|
+
readonly message?: string;
|
|
47
|
+
/** With `?wait=`: whether the state was reached before the timeout. */
|
|
48
|
+
readonly met?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** `GET /v1/info`: the cluster `nylorun sandbox enable` recorded. */
|
|
51
|
+
export interface ClusterInfo {
|
|
52
|
+
readonly namespace: string;
|
|
53
|
+
readonly context: string;
|
|
54
|
+
readonly controllerVersion?: string;
|
|
55
|
+
readonly apiVersion?: string;
|
|
56
|
+
readonly hostAddress?: string;
|
|
57
|
+
readonly ports?: Readonly<Record<string, number>>;
|
|
58
|
+
readonly networkPolicy?: {
|
|
59
|
+
readonly enforced: boolean;
|
|
60
|
+
readonly probedAt?: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export type PodWait = "ready" | "suspended" | "expired" | "gone";
|
|
64
|
+
export interface SandboxesClient {
|
|
65
|
+
/** Whether the service is up and its informers synced (`GET /ready`). Never throws. */
|
|
66
|
+
ready(signal?: AbortSignal): Promise<boolean>;
|
|
67
|
+
info(signal?: AbortSignal): Promise<ClusterInfo>;
|
|
68
|
+
put(name: string, spec: PodSpec, signal?: AbortSignal): Promise<PodStatus>;
|
|
69
|
+
status(name: string, options?: {
|
|
70
|
+
wait?: PodWait;
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
}): Promise<PodStatus>;
|
|
74
|
+
delete(name: string, opId: string, signal?: AbortSignal): Promise<PodStatus>;
|
|
75
|
+
}
|
|
76
|
+
/** The service refused a request, or did not answer. */
|
|
77
|
+
export declare class SandboxesError extends Error {
|
|
78
|
+
readonly status: number;
|
|
79
|
+
readonly code: string;
|
|
80
|
+
readonly name = "SandboxesError";
|
|
81
|
+
constructor(status: number, code: string, message: string);
|
|
82
|
+
}
|
|
83
|
+
/** The sandboxes service over HTTP. */
|
|
84
|
+
export declare function httpSandboxesClient(options: {
|
|
85
|
+
readonly url: string;
|
|
86
|
+
readonly token: string;
|
|
87
|
+
readonly fetch?: typeof fetch;
|
|
88
|
+
/** Per request, unless the request waits longer. Default 20 s. */
|
|
89
|
+
readonly timeoutMs?: number;
|
|
90
|
+
}): SandboxesClient;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sandboxes service's client (F7.2, D32): the Go service (`sandboxes/`) that alone holds
|
|
3
|
+
* the cluster credentials and drives agent-sandbox Sandboxes in the Tenant's namespace. Core
|
|
4
|
+
* reaches it at `NYLORUN_SANDBOXES_URL` with `NYLORUN_SANDBOXES_TOKEN`; nothing else does.
|
|
5
|
+
*
|
|
6
|
+
* Pull, not push: the service never calls core. Core reads a Sandbox's status when it
|
|
7
|
+
* reconciles (`GET /v1/pods/{name}`, optionally blocking with `?wait=`), and every change it
|
|
8
|
+
* asks for carries an operation id, so a retried `PUT` is a no-op.
|
|
9
|
+
*/
|
|
10
|
+
/** The service refused a request, or did not answer. */
|
|
11
|
+
export class SandboxesError extends Error {
|
|
12
|
+
status;
|
|
13
|
+
code;
|
|
14
|
+
name = "SandboxesError";
|
|
15
|
+
constructor(status, code, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** The sandboxes service over HTTP. */
|
|
22
|
+
export function httpSandboxesClient(options) {
|
|
23
|
+
const base = options.url.replace(/\/+$/, "");
|
|
24
|
+
const call = options.fetch ?? fetch;
|
|
25
|
+
const request = async (method, path, init = {}) => {
|
|
26
|
+
const timeout = AbortSignal.timeout(init.timeoutMs ?? options.timeoutMs ?? 20_000);
|
|
27
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
28
|
+
let response;
|
|
29
|
+
try {
|
|
30
|
+
response = await call(`${base}${path}`, {
|
|
31
|
+
method,
|
|
32
|
+
headers: {
|
|
33
|
+
...(init.auth === false ? {} : { authorization: `Bearer ${options.token}` }),
|
|
34
|
+
...(init.body === undefined ? {} : { "content-type": "application/json" }),
|
|
35
|
+
},
|
|
36
|
+
...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
|
|
37
|
+
signal,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new SandboxesError(0, "unreachable", `The sandboxes service did not answer: ${error instanceof Error ? error.message : String(error)}`);
|
|
42
|
+
}
|
|
43
|
+
const text = await response.text();
|
|
44
|
+
let body;
|
|
45
|
+
try {
|
|
46
|
+
body = text === "" ? undefined : JSON.parse(text);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
body = undefined;
|
|
50
|
+
}
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const error = body?.error;
|
|
53
|
+
throw new SandboxesError(response.status, error?.code ?? "error", error?.message ?? `The sandboxes service answered ${response.status}`);
|
|
54
|
+
}
|
|
55
|
+
return body;
|
|
56
|
+
};
|
|
57
|
+
const pod = (name) => `/v1/pods/${encodeURIComponent(name)}`;
|
|
58
|
+
return {
|
|
59
|
+
async ready(signal) {
|
|
60
|
+
try {
|
|
61
|
+
await request("GET", "/ready", { auth: false, timeoutMs: 3_000, ...(signal ? { signal } : {}) });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
info: (signal) => request("GET", "/v1/info", signal ? { signal } : {}),
|
|
69
|
+
put: (name, spec, signal) => request("PUT", pod(name), { body: spec, ...(signal ? { signal } : {}) }),
|
|
70
|
+
status(name, { wait, timeoutMs, signal } = {}) {
|
|
71
|
+
const query = wait ? `?wait=${wait}&timeoutMs=${Math.min(timeoutMs ?? 30_000, 30_000)}` : "";
|
|
72
|
+
return request("GET", `${pod(name)}${query}`, {
|
|
73
|
+
...(signal ? { signal } : {}),
|
|
74
|
+
...(wait ? { timeoutMs: Math.min(timeoutMs ?? 30_000, 30_000) + 10_000 } : {}),
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
delete: (name, opId, signal) => request("DELETE", `${pod(name)}?opId=${encodeURIComponent(opId)}`, signal ? { signal } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|