@nylorun/runtime 0.23.0-beta → 0.25.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 +252 -0
- package/README.md +87 -28
- package/dist/adapters/execution/restate.js +1 -1
- package/dist/adapters/replication/pgoutput.d.ts +2 -0
- package/dist/adapters/replication/pgoutput.js +6 -3
- package/dist/adapters/sandbox/local.d.ts +9 -1
- package/dist/adapters/sandbox/local.js +16 -4
- package/dist/adapters/sandbox/virtual.js +4 -1
- package/dist/api/a2a/card.d.ts +11 -1
- package/dist/api/a2a/card.js +26 -2
- package/dist/api/a2a/endpoint.js +9 -3
- package/dist/api/a2a/protocol.d.ts +25 -2
- package/dist/api/a2a/protocol.js +20 -3
- package/dist/api/a2a/routes.d.ts +6 -1
- package/dist/api/a2a/routes.js +67 -9
- package/dist/api/a2a/tasks.js +2 -1
- package/dist/api/ag-ui/endpoint.js +8 -3
- package/dist/api/ag-ui/history.d.ts +6 -1
- package/dist/api/ag-ui/history.js +39 -3
- package/dist/api/ag-ui/routes.d.ts +4 -1
- package/dist/api/ag-ui/routes.js +63 -20
- package/dist/api/ag-ui/translate.js +3 -2
- package/dist/api/components.d.ts +5327 -1091
- package/dist/api/components.js +40 -16
- package/dist/api/gate/keys.d.ts +2 -1
- package/dist/api/gate/keys.js +14 -1
- package/dist/api/gate/routes.d.ts +6 -0
- package/dist/api/gate/routes.js +52 -3
- package/dist/api/http/app.js +17 -6
- package/dist/api/http/body.d.ts +3 -3
- package/dist/api/http/body.js +7 -7
- package/dist/api/http/define.d.ts +6 -0
- package/dist/api/http/define.js +13 -1
- package/dist/api/http/respond.js +1 -1
- package/dist/api/http/routes/artifacts.js +35 -8
- package/dist/api/http/routes/coverage.d.ts +9 -0
- package/dist/api/http/routes/coverage.js +34 -0
- package/dist/api/http/routes/environment-manifests.d.ts +9 -0
- package/dist/api/http/routes/environment-manifests.js +118 -0
- package/dist/api/http/routes/environments.d.ts +11 -0
- package/dist/api/http/routes/environments.js +332 -0
- package/dist/api/http/routes/mcp.d.ts +1 -1
- package/dist/api/http/routes/mcp.js +2 -2
- package/dist/api/http/routes/me.js +1 -1
- package/dist/api/http/routes/reads.d.ts +94 -9
- package/dist/api/http/routes/reads.js +54 -5
- package/dist/api/http/routes/sessions.js +24 -9
- package/dist/api/http/routes/tenant.js +65 -15
- package/dist/api/inbound-files.d.ts +65 -0
- package/dist/api/inbound-files.js +115 -0
- package/dist/api/openapi.js +48 -28
- package/dist/artifacts/disk.d.ts +59 -0
- package/dist/artifacts/disk.js +569 -0
- package/dist/artifacts/export.d.ts +1 -1
- package/dist/artifacts/export.js +2 -3
- package/dist/artifacts/folders.d.ts +6 -3
- package/dist/artifacts/folders.js +17 -12
- package/dist/artifacts/media-types.d.ts +15 -0
- package/dist/artifacts/media-types.js +56 -4
- package/dist/artifacts/parts.d.ts +12 -3
- package/dist/artifacts/parts.js +15 -4
- package/dist/artifacts/service.d.ts +2 -1
- package/dist/artifacts/service.js +6 -1
- package/dist/artifacts/workspace.d.ts +5 -5
- package/dist/artifacts/workspace.js +7 -19
- package/dist/core/flow-host.d.ts +3 -4
- package/dist/core/flow-host.js +5 -2
- package/dist/core/provider.d.ts +3 -2
- package/dist/core/provider.js +5 -37
- package/dist/core/sandbox-routes.d.ts +2 -21
- package/dist/core/sandbox-routes.js +11 -44
- package/dist/core/turn-manifest.d.ts +3 -2
- package/dist/core/turn-manifest.js +29 -5
- package/dist/execution/types.d.ts +1 -1
- package/dist/gates/contract.d.ts +3 -24
- package/dist/gates/contract.js +1 -36
- package/dist/gates/egress-credentials.d.ts +9 -0
- package/dist/gates/egress-credentials.js +11 -4
- package/dist/gates/egress.d.ts +22 -1
- package/dist/gates/egress.js +116 -10
- package/dist/gates/fixture-model.d.ts +28 -0
- package/dist/gates/fixture-model.js +92 -0
- package/dist/gates/handler.js +4 -4
- package/dist/gates/http-tool.d.ts +9 -6
- package/dist/gates/http-tool.js +11 -17
- package/dist/gates/in-process.d.ts +5 -0
- package/dist/gates/in-process.js +12 -1
- package/dist/gates/journal.d.ts +30 -0
- package/dist/gates/journal.js +51 -0
- package/dist/gates/mcp-handler.d.ts +4 -0
- package/dist/gates/mcp-handler.js +7 -4
- package/dist/gates/meter.d.ts +10 -2
- package/dist/gates/meter.js +20 -5
- package/dist/gates/policy.d.ts +49 -0
- package/dist/gates/policy.js +132 -0
- package/dist/gates/tenant-vaults.d.ts +7 -7
- package/dist/gates/tenant-vaults.js +5 -5
- package/dist/gates/tool-calls.d.ts +10 -5
- package/dist/gates/tool-calls.js +28 -10
- package/dist/gates/tool-client.d.ts +8 -0
- package/dist/gates/tool-client.js +27 -11
- package/dist/gates/tool-contract.d.ts +11 -69
- package/dist/gates/tool-contract.js +1 -85
- package/dist/gates/tool-gate.d.ts +1 -1
- package/dist/harness/calls.d.ts +14 -7
- package/dist/harness/calls.js +30 -24
- package/dist/harness/executors.js +12 -9
- package/dist/harness/main.js +24 -5
- package/dist/harness/pod.d.ts +39 -0
- package/dist/harness/pod.js +46 -0
- package/dist/harness/service.d.ts +0 -1
- package/dist/harness/service.js +17 -9
- package/dist/harness/setup.d.ts +29 -0
- package/dist/harness/setup.js +174 -0
- package/dist/harness/ws-client.js +3 -2
- package/dist/harness/ws-port.d.ts +1 -5
- package/dist/harness/ws-port.js +1 -5
- package/dist/harness-api/in-process.js +3 -3
- package/dist/harness-api/record.d.ts +15 -1
- package/dist/harness-api/record.js +180 -50
- package/dist/harness-api/redact.d.ts +3 -0
- package/dist/harness-api/redact.js +45 -0
- package/dist/harness-api/server.d.ts +35 -4
- package/dist/harness-api/server.js +130 -17
- package/dist/harness-api/start.d.ts +10 -3
- package/dist/harness-api/start.js +40 -10
- package/dist/harness-api/tool-journal.d.ts +105 -0
- package/dist/harness-api/tool-journal.js +132 -0
- package/dist/harness-api/tool-status.d.ts +13 -0
- package/dist/harness-api/tool-status.js +26 -0
- package/dist/harness-api/workspace.d.ts +12 -7
- package/dist/harness-api/workspace.js +47 -25
- package/dist/harness-api/ws-server.d.ts +11 -3
- package/dist/harness-api/ws-server.js +94 -15
- package/dist/host/gates.d.ts +2 -1
- package/dist/host/gates.js +10 -2
- package/dist/host/main.js +54 -5
- package/dist/host/operate.js +67 -5
- package/dist/host/stack-config.d.ts +64 -0
- package/dist/host/stack-config.js +62 -2
- package/dist/keys/client.js +4 -1
- package/dist/keys/keys.d.ts +13 -6
- package/dist/keys/keys.js +9 -8
- package/dist/management-openapi.json +1264 -2171
- package/dist/mcp/connect.d.ts +26 -22
- package/dist/mcp/connect.js +30 -34
- package/dist/mcp/pool.d.ts +3 -6
- package/dist/mcp/pool.js +14 -28
- package/dist/mcp/preview.d.ts +3 -3
- package/dist/mcp/preview.js +10 -21
- package/dist/mcp/snapshot.d.ts +16 -24
- package/dist/mcp/snapshot.js +33 -45
- package/dist/model/capabilities.d.ts +26 -0
- package/dist/model/capabilities.js +44 -0
- package/dist/model/catalog.d.ts +34 -3
- package/dist/model/catalog.js +38 -2
- package/dist/model/models.d.ts +2 -0
- package/dist/model/models.js +1 -1
- package/dist/model/native-blocks.d.ts +35 -0
- package/dist/model/native-blocks.js +170 -0
- package/dist/model/pi-model.js +114 -94
- package/dist/openapi.json +12822 -3943
- package/dist/reads/types.d.ts +16 -3
- package/dist/record/append.d.ts +3 -1
- package/dist/record/append.js +1 -0
- package/dist/record/envelope.d.ts +5 -1
- package/dist/record/envelope.js +7 -3
- package/dist/record/environment.d.ts +25 -0
- package/dist/record/environment.js +44 -0
- package/dist/record/index.d.ts +1 -1
- package/dist/record/index.js +1 -1
- package/dist/sandbox/disk-fs.d.ts +95 -0
- package/dist/sandbox/disk-fs.js +206 -0
- package/dist/sandbox/join.d.ts +17 -18
- package/dist/sandbox/join.js +46 -15
- package/dist/sandbox/manager.d.ts +47 -21
- package/dist/sandbox/manager.js +162 -107
- package/dist/sandbox/placement.d.ts +14 -13
- package/dist/sandbox/placement.js +31 -14
- package/dist/sandbox/pods/client.d.ts +9 -2
- package/dist/sandbox/pods/lifecycle.d.ts +36 -27
- package/dist/sandbox/pods/lifecycle.js +29 -26
- package/dist/sandbox/pods/reconcile.js +18 -12
- package/dist/sandbox/pods/spec.d.ts +15 -13
- package/dist/sandbox/pods/spec.js +17 -20
- package/dist/sandbox/records.d.ts +2 -2
- package/dist/sandbox/records.js +3 -3
- package/dist/sandbox/resolve.d.ts +37 -27
- package/dist/sandbox/resolve.js +73 -41
- package/dist/sandbox/runtime-tools.d.ts +9 -0
- package/dist/sandbox/runtime-tools.js +16 -0
- package/dist/sandbox/session-sandbox.d.ts +4 -6
- package/dist/sandbox/session-sandbox.js +47 -94
- package/dist/sandbox/setup.d.ts +64 -0
- package/dist/sandbox/setup.js +160 -0
- package/dist/sandbox/share.d.ts +21 -26
- package/dist/sandbox/share.js +36 -63
- package/dist/sandbox/skills.d.ts +8 -6
- package/dist/sandbox/skills.js +24 -23
- package/dist/sandbox/tenant-config.d.ts +12 -14
- package/dist/sandbox/tenant-config.js +12 -17
- package/dist/sandbox/types.d.ts +6 -0
- package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
- package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
- package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
- package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
- package/dist/store/postgres/drizzle/0024_flat_manifest.sql +7 -0
- package/dist/store/postgres/drizzle/0025_environment_manifests.sql +14 -0
- package/dist/store/postgres/drizzle/0026_environment_forks.sql +1 -0
- package/dist/store/postgres/drizzle/0027_environment_image_digest.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +56 -0
- package/dist/store/postgres/reads.js +123 -17
- package/dist/store/postgres/record-writer.d.ts +4 -4
- package/dist/store/postgres/record-writer.js +9 -9
- package/dist/store/postgres/record.d.ts +7 -0
- package/dist/store/postgres/record.js +23 -2
- package/dist/store/postgres/schema.d.ts +1126 -736
- package/dist/store/postgres/schema.js +136 -73
- package/dist/store/postgres/store.js +195 -67
- package/dist/store/types.d.ts +165 -50
- package/dist/tenant/advance.d.ts +7 -6
- package/dist/tenant/advance.js +118 -42
- package/dist/tenant/artifact-tool.d.ts +1 -1
- package/dist/tenant/artifact-tool.js +46 -26
- package/dist/tenant/commands.d.ts +15 -2
- package/dist/tenant/commands.js +115 -14
- package/dist/tenant/context.d.ts +56 -24
- package/dist/tenant/context.js +4 -15
- package/dist/tenant/effects.d.ts +1 -1
- package/dist/tenant/effects.js +44 -29
- package/dist/tenant/environment-manifests.d.ts +57 -0
- package/dist/tenant/environment-manifests.js +200 -0
- package/dist/tenant/environments.d.ts +174 -0
- package/dist/tenant/environments.js +1044 -0
- package/dist/tenant/harness-tokens.d.ts +21 -0
- package/dist/tenant/harness-tokens.js +61 -0
- package/dist/tenant/host-token.d.ts +1 -1
- package/dist/tenant/host-token.js +2 -4
- package/dist/tenant/identity-file.d.ts +4 -4
- package/dist/tenant/identity-file.js +17 -8
- package/dist/tenant/issuers.js +3 -3
- package/dist/tenant/model-setting.d.ts +14 -0
- package/dist/tenant/model-setting.js +51 -0
- package/dist/tenant/principals.js +1 -1
- package/dist/tenant/reset.js +3 -3
- package/dist/tenant/runtime.d.ts +6 -1
- package/dist/tenant/runtime.js +7 -1
- package/dist/tenant/session.d.ts +3 -3
- package/dist/tenant/session.js +0 -14
- package/dist/tenant/sessions.d.ts +18 -14
- package/dist/tenant/sessions.js +99 -143
- package/dist/tenant/signing-keys.d.ts +5 -1
- package/dist/tenant/skill-tool.d.ts +3 -3
- package/dist/tenant/skill-tool.js +6 -11
- package/dist/tenant/status.js +3 -3
- package/dist/tenant/sweep.js +13 -7
- package/dist/tenant/tool-results.js +1 -1
- package/dist/tenant/tool-search.d.ts +7 -11
- package/dist/tenant/tool-search.js +8 -5
- package/dist/tenant/transcript.d.ts +245 -36
- package/dist/tenant/transcript.js +64 -12
- package/dist/tenant/turn-timing.d.ts +43 -0
- package/dist/tenant/turn-timing.js +39 -0
- package/dist/tenant/types.d.ts +2 -0
- package/dist/traces/ids.d.ts +41 -0
- package/dist/traces/ids.js +81 -0
- package/dist/traces/key.d.ts +6 -0
- package/dist/traces/key.js +37 -0
- package/dist/traces/map.d.ts +113 -0
- package/dist/traces/map.js +289 -0
- package/dist/traces/options.d.ts +7 -0
- package/dist/traces/options.js +1 -0
- package/dist/traces/otlp.d.ts +42 -0
- package/dist/traces/otlp.js +84 -0
- package/dist/traces/projector.d.ts +58 -0
- package/dist/traces/projector.js +200 -0
- package/dist/traces/read.d.ts +23 -0
- package/dist/traces/read.js +16 -0
- package/dist/vault/coverage.d.ts +26 -0
- package/dist/vault/coverage.js +62 -0
- package/dist/vault/error.d.ts +11 -1
- package/dist/vault/error.js +5 -1
- package/dist/vault/host-model.d.ts +4 -1
- package/dist/vault/host-model.js +10 -2
- package/dist/vault/service.d.ts +92 -48
- package/dist/vault/service.js +462 -376
- package/dist/vault/sources.d.ts +5 -16
- package/dist/vault/sources.js +6 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/package.json +13 -13
- package/dist/api/http/routes/sandboxes.d.ts +0 -9
- package/dist/api/http/routes/sandboxes.js +0 -153
- package/dist/api/http/routes/vaults.d.ts +0 -10
- package/dist/api/http/routes/vaults.js +0 -139
- package/dist/record/sandbox.d.ts +0 -25
- package/dist/record/sandbox.js +0 -43
- package/dist/sandbox/capability.d.ts +0 -3
- package/dist/sandbox/capability.js +0 -10
- package/dist/tenant/sandboxes.d.ts +0 -123
- package/dist/tenant/sandboxes.js +0 -566
package/dist/sandbox/manager.js
CHANGED
|
@@ -2,10 +2,11 @@ import { SANDBOX_WORKSPACE, } from "@nylorun/core/define";
|
|
|
2
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 { mountSkill, sandboxSkillsOf
|
|
5
|
+
import { mountSkill, sandboxSkillsOf } from "./skills.js";
|
|
6
|
+
import { runDiskOp } from "./disk-fs.js";
|
|
6
7
|
import { resolveSandboxPath, runSandboxTool, } from "./tools.js";
|
|
7
8
|
import { SandboxFileTooLargeError, } from "./types.js";
|
|
8
|
-
export {
|
|
9
|
+
export { sandboxToolSpec } from "./runtime-tools.js";
|
|
9
10
|
const READ_TOOLS = new Set(["read", "grep", "glob"]);
|
|
10
11
|
export class SandboxManager {
|
|
11
12
|
options;
|
|
@@ -43,23 +44,20 @@ export class SandboxManager {
|
|
|
43
44
|
if (this.closing)
|
|
44
45
|
return;
|
|
45
46
|
const now = at ?? Date.now();
|
|
46
|
-
const idle = (live) => !!live.handle && live.
|
|
47
|
+
const idle = (live) => !!live.handle && live.running.size === 0 && now - live.lastUsedAt >= live.idleMs;
|
|
47
48
|
const stops = [];
|
|
48
49
|
for (const [key, live] of this.live) {
|
|
49
50
|
if (!idle(live))
|
|
50
51
|
continue;
|
|
51
|
-
//
|
|
52
|
-
live
|
|
53
|
-
.then(() => (idle(live) ? this.stop(key, live) : undefined))
|
|
54
|
-
.catch(() => undefined);
|
|
55
|
-
stops.push(live.tail);
|
|
52
|
+
// After the calls running; checked again when its turn comes.
|
|
53
|
+
stops.push(this.alone(live, () => (idle(live) ? this.stop(key, live) : undefined)).catch(() => undefined));
|
|
56
54
|
}
|
|
57
55
|
await Promise.all(stops);
|
|
58
56
|
for (const record of await this.options.records.list()) {
|
|
59
57
|
if (record.state === "stopped")
|
|
60
58
|
continue;
|
|
61
59
|
const live = this.live.get(record.key);
|
|
62
|
-
if (live && (live.handle || live.
|
|
60
|
+
if (live && (live.handle || live.running.size > 0))
|
|
63
61
|
continue;
|
|
64
62
|
await this.options.records.put({ ...record, state: "stopped" });
|
|
65
63
|
}
|
|
@@ -89,34 +87,84 @@ export class SandboxManager {
|
|
|
89
87
|
keyOf(sessionId) {
|
|
90
88
|
return sessionWorkspaceKey(this.options.scope, sessionId);
|
|
91
89
|
}
|
|
92
|
-
/** The workspace key of sandbox resource `
|
|
93
|
-
sandboxKeyOf(
|
|
94
|
-
return sandboxWorkspaceKey(this.options.scope,
|
|
90
|
+
/** The workspace key of sandbox resource `environmentId` (`sandboxWorkspaceKey`). */
|
|
91
|
+
sandboxKeyOf(environmentId) {
|
|
92
|
+
return sandboxWorkspaceKey(this.options.scope, environmentId);
|
|
95
93
|
}
|
|
96
94
|
keyFor(session) {
|
|
97
|
-
return session.
|
|
95
|
+
return session.environmentId === undefined
|
|
98
96
|
? this.keyOf(session.id)
|
|
99
|
-
: this.sandboxKeyOf(session.
|
|
97
|
+
: this.sandboxKeyOf(session.environmentId);
|
|
100
98
|
}
|
|
101
99
|
async report() {
|
|
102
100
|
return { ...reportSelection(await this.ready), defaultImage: DEFAULT_SANDBOX_IMAGE };
|
|
103
101
|
}
|
|
104
|
-
async run(session,
|
|
105
|
-
const prepared = await this.prepare(session,
|
|
102
|
+
async run(session, sandbox, toolName, input, signal) {
|
|
103
|
+
const prepared = await this.prepare(session, sandbox);
|
|
106
104
|
if ("kind" in prepared)
|
|
107
105
|
return prepared;
|
|
108
106
|
const { live: entry, spec } = prepared;
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
return this.inLane(entry, session.id, () => this.execute(entry, session, spec, toolName, input, signal));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Runs `call` in session `sessionId`'s queue on `live`: after its earlier calls, and after any
|
|
111
|
+
* stop or removal pending, beside other sessions' calls (D66).
|
|
112
|
+
*/
|
|
113
|
+
inLane(live, sessionId, call) {
|
|
114
|
+
const task = (live.lanes.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
115
|
+
// A stop queued while this waited goes first.
|
|
116
|
+
for (let gate = live.gate;; gate = live.gate) {
|
|
117
|
+
await gate;
|
|
118
|
+
if (gate === live.gate)
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
const running = call();
|
|
122
|
+
live.running.add(running);
|
|
123
|
+
try {
|
|
124
|
+
return await running;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
live.running.delete(running);
|
|
128
|
+
live.lastUsedAt = Date.now();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const queued = task.catch(() => undefined);
|
|
132
|
+
live.lanes.set(sessionId, queued);
|
|
133
|
+
void queued.then(() => {
|
|
134
|
+
if (live.lanes.get(sessionId) === queued)
|
|
135
|
+
live.lanes.delete(sessionId);
|
|
136
|
+
});
|
|
137
|
+
return task;
|
|
138
|
+
}
|
|
139
|
+
/** Runs `step` on `live` alone: after the calls running, before the calls that come after it. */
|
|
140
|
+
alone(live, step) {
|
|
141
|
+
const task = live.gate.then(async () => {
|
|
142
|
+
while (live.running.size > 0)
|
|
143
|
+
await Promise.allSettled([...live.running]);
|
|
144
|
+
return step();
|
|
145
|
+
});
|
|
146
|
+
live.gate = task.catch(() => undefined);
|
|
111
147
|
return task;
|
|
112
148
|
}
|
|
149
|
+
/** The workspace's handle, opened once for the calls that need it at the same time. */
|
|
150
|
+
async handleOf(live, session, spec) {
|
|
151
|
+
if (live.handle)
|
|
152
|
+
return live.handle;
|
|
153
|
+
const opening = (live.opening ??= this.open(live, session, spec).finally(() => {
|
|
154
|
+
if (live.opening === opening)
|
|
155
|
+
live.opening = undefined;
|
|
156
|
+
}));
|
|
157
|
+
const handle = await opening;
|
|
158
|
+
live.handle ??= handle;
|
|
159
|
+
return handle;
|
|
160
|
+
}
|
|
113
161
|
/**
|
|
114
162
|
* The bytes of one file in the session's sandbox, for `save_artifact` (F8.1), in the same queue
|
|
115
163
|
* as its tool calls: `missing` when there is no such file, a failed outcome when the sandbox
|
|
116
164
|
* cannot open or the file is larger than `maxBytes`. Relative paths resolve in the workspace.
|
|
117
165
|
*/
|
|
118
|
-
async readBytes(session,
|
|
119
|
-
return this.withHandle(session,
|
|
166
|
+
async readBytes(session, sandbox, path, maxBytes, signal) {
|
|
167
|
+
return this.withHandle(session, sandbox, signal, async (handle, live) => {
|
|
120
168
|
const resolved = resolveSandboxPath(path, handle.workspace ?? SANDBOX_WORKSPACE);
|
|
121
169
|
if (!handle.readBytes)
|
|
122
170
|
return {
|
|
@@ -146,14 +194,14 @@ export class SandboxManager {
|
|
|
146
194
|
* the same queue as its tool calls. `missing` when the sandbox was never created (nothing is
|
|
147
195
|
* started to look) or `dir` is not a directory. Relative paths resolve in the workspace.
|
|
148
196
|
*/
|
|
149
|
-
async listFiles(session,
|
|
197
|
+
async listFiles(session, sandbox, dir, maxEntries, signal) {
|
|
150
198
|
const key = this.keyFor(session);
|
|
151
199
|
if (!this.live.get(key)?.handle) {
|
|
152
200
|
const record = await this.options.records.get(key);
|
|
153
201
|
if (!record)
|
|
154
202
|
return { kind: "missing", path: dir };
|
|
155
203
|
}
|
|
156
|
-
return this.withHandle(session,
|
|
204
|
+
return this.withHandle(session, sandbox, signal, async (handle, live) => {
|
|
157
205
|
const resolved = resolveSandboxPath(dir, handle.workspace ?? SANDBOX_WORKSPACE);
|
|
158
206
|
if (!handle.listFiles)
|
|
159
207
|
return {
|
|
@@ -167,46 +215,51 @@ export class SandboxManager {
|
|
|
167
215
|
: { kind: "listed", path: resolved, listing };
|
|
168
216
|
});
|
|
169
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* One operation on the disk of the session's sandbox (R2d step 3, `sandbox/disk-fs.ts`), in the
|
|
220
|
+
* same queue as its tool calls, starting it first when it is stopped.
|
|
221
|
+
*/
|
|
222
|
+
async disk(session, sandbox, op, signal) {
|
|
223
|
+
return this.withHandle(session, sandbox, signal, async (handle, live) => {
|
|
224
|
+
if (!handle.diskRoots)
|
|
225
|
+
return {
|
|
226
|
+
kind: "failed",
|
|
227
|
+
code: "sandbox.unsupported",
|
|
228
|
+
message: `The ${live.backend.name} backend has no disk to version`,
|
|
229
|
+
};
|
|
230
|
+
return runDiskOp(handle.diskRoots, op);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
170
233
|
/** Runs `use` on the session's sandbox in its queue, starting it first when it is stopped. */
|
|
171
|
-
async withHandle(session,
|
|
172
|
-
const prepared = await this.prepare(session,
|
|
234
|
+
async withHandle(session, sandbox, signal, use) {
|
|
235
|
+
const prepared = await this.prepare(session, sandbox);
|
|
173
236
|
if ("kind" in prepared)
|
|
174
237
|
return prepared;
|
|
175
238
|
const { live, spec } = prepared;
|
|
176
|
-
|
|
239
|
+
return this.inLane(live, session.id, async () => {
|
|
177
240
|
if (signal.aborted)
|
|
178
241
|
throw new Error("Turn cancelled");
|
|
179
242
|
if (this.closing)
|
|
180
243
|
throw new Error("Runtime is shutting down");
|
|
181
|
-
|
|
244
|
+
let handle;
|
|
182
245
|
try {
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
live.handle = await this.open(live, session, spec);
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
return {
|
|
189
|
-
kind: "failed",
|
|
190
|
-
code: "sandbox.start_failed",
|
|
191
|
-
message: `The sandbox could not start: ${error instanceof Error ? error.message : String(error)}`,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
const unmounted = await this.mountSkills(live, live.handle, session, signal);
|
|
196
|
-
if (unmounted)
|
|
197
|
-
return unmounted;
|
|
198
|
-
return await use(live.handle, live);
|
|
246
|
+
handle = await this.handleOf(live, session, spec);
|
|
199
247
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
248
|
+
catch (error) {
|
|
249
|
+
return {
|
|
250
|
+
kind: "failed",
|
|
251
|
+
code: "sandbox.start_failed",
|
|
252
|
+
message: `The sandbox could not start: ${error instanceof Error ? error.message : String(error)}`,
|
|
253
|
+
};
|
|
203
254
|
}
|
|
255
|
+
const unmounted = await this.mountSkills(live, handle, session, signal);
|
|
256
|
+
if (unmounted)
|
|
257
|
+
return unmounted;
|
|
258
|
+
return await use(handle, live);
|
|
204
259
|
});
|
|
205
|
-
live.tail = task.catch(() => undefined);
|
|
206
|
-
return task;
|
|
207
260
|
}
|
|
208
261
|
/** The session's live sandbox entry and spec, or why it cannot run here. */
|
|
209
|
-
async prepare(session,
|
|
262
|
+
async prepare(session, sandbox) {
|
|
210
263
|
const selection = await this.ready;
|
|
211
264
|
const backend = selection.backend;
|
|
212
265
|
if (!backend)
|
|
@@ -218,10 +271,10 @@ export class SandboxManager {
|
|
|
218
271
|
const key = this.keyFor(session);
|
|
219
272
|
const spec = {
|
|
220
273
|
key,
|
|
221
|
-
image:
|
|
222
|
-
cpus:
|
|
223
|
-
memoryMiB: memoryMiBOf(
|
|
224
|
-
network: resolveNetwork(
|
|
274
|
+
image: sandbox?.image ?? DEFAULT_SANDBOX_IMAGE,
|
|
275
|
+
cpus: sandbox?.resources?.cpus ?? DEFAULT_SANDBOX_CPUS,
|
|
276
|
+
memoryMiB: memoryMiBOf(sandbox),
|
|
277
|
+
network: resolveNetwork(sandbox),
|
|
225
278
|
};
|
|
226
279
|
const unmet = backend.unmet(spec);
|
|
227
280
|
if (unmet)
|
|
@@ -234,12 +287,13 @@ export class SandboxManager {
|
|
|
234
287
|
if (!live) {
|
|
235
288
|
live = {
|
|
236
289
|
sessionId: session.id,
|
|
237
|
-
...(session.
|
|
290
|
+
...(session.environmentId === undefined ? {} : { environmentId: session.environmentId }),
|
|
238
291
|
backend,
|
|
239
|
-
idleMs: idleMsOf(
|
|
240
|
-
|
|
292
|
+
idleMs: idleMsOf(sandbox),
|
|
293
|
+
running: new Set(),
|
|
241
294
|
lastUsedAt: Date.now(),
|
|
242
|
-
|
|
295
|
+
lanes: new Map(),
|
|
296
|
+
gate: Promise.resolve(),
|
|
243
297
|
skills: new Map(),
|
|
244
298
|
};
|
|
245
299
|
this.live.set(key, live);
|
|
@@ -251,42 +305,34 @@ export class SandboxManager {
|
|
|
251
305
|
throw new Error("Turn cancelled");
|
|
252
306
|
if (this.closing)
|
|
253
307
|
throw new Error("Runtime is shutting down");
|
|
254
|
-
live
|
|
255
|
-
try {
|
|
256
|
-
return await this.executeOpen(live, session, spec, toolName, input, signal);
|
|
257
|
-
}
|
|
258
|
-
finally {
|
|
259
|
-
live.active -= 1;
|
|
260
|
-
live.lastUsedAt = Date.now();
|
|
261
|
-
}
|
|
308
|
+
return this.executeOpen(live, session, spec, toolName, input, signal);
|
|
262
309
|
}
|
|
263
310
|
async executeOpen(live, session, spec, toolName, input, signal) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
catch (error) {
|
|
269
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
270
|
-
const record = await this.record(spec.key, live, session.id, spec.image, "stopped");
|
|
271
|
-
await this.options.emit(session.id, session.activeTurnId, "sandbox.state", { state: "stopped", backend: live.backend.name, error: message }, { record, ...claimOf(session) });
|
|
272
|
-
// Nothing ran yet, so a failed start is a definite outcome, not an uncertain one.
|
|
273
|
-
return {
|
|
274
|
-
kind: "failed",
|
|
275
|
-
code: "sandbox.start_failed",
|
|
276
|
-
message: `The sandbox could not start on ${live.backend.name} (image ${spec.image ?? "the backend's default"}): ${message}`,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
311
|
+
let handle;
|
|
312
|
+
try {
|
|
313
|
+
handle = await this.handleOf(live, session, spec);
|
|
279
314
|
}
|
|
280
|
-
|
|
315
|
+
catch (error) {
|
|
316
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
317
|
+
const record = await this.record(spec.key, live, session.id, spec.image, "stopped");
|
|
318
|
+
await this.options.emit(session.id, session.activeTurnId, "environment.state", { state: "stopped", backend: live.backend.name, error: message }, { record, ...claimOf(session) });
|
|
319
|
+
// Nothing ran yet, so a failed start is a definite outcome, not an uncertain one.
|
|
320
|
+
return {
|
|
321
|
+
kind: "failed",
|
|
322
|
+
code: "sandbox.start_failed",
|
|
323
|
+
message: `The sandbox could not start on ${live.backend.name} (image ${spec.image ?? "the backend's default"}): ${message}`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const unmounted = await this.mountSkills(live, handle, session, signal);
|
|
281
327
|
if (unmounted)
|
|
282
328
|
return unmounted;
|
|
283
329
|
const started = Date.now();
|
|
284
330
|
let report = {};
|
|
285
331
|
try {
|
|
286
|
-
const outcome = await runSandboxTool(
|
|
332
|
+
const outcome = await runSandboxTool(handle, toolName, (input ?? {}), signal, (value) => {
|
|
287
333
|
report = value;
|
|
288
334
|
}, session.environment);
|
|
289
|
-
await this.options.emit(session.id, session.activeTurnId, "
|
|
335
|
+
await this.options.emit(session.id, session.activeTurnId, "environment.exec", {
|
|
290
336
|
tool: toolName,
|
|
291
337
|
...report,
|
|
292
338
|
...(report.command ? { command: report.command.slice(0, 500) } : {}),
|
|
@@ -297,8 +343,9 @@ export class SandboxManager {
|
|
|
297
343
|
return outcome;
|
|
298
344
|
}
|
|
299
345
|
catch (error) {
|
|
300
|
-
// The backend itself failed; reopen on the next call.
|
|
301
|
-
live.handle
|
|
346
|
+
// The backend itself failed; reopen on the next call, unless another call already did.
|
|
347
|
+
if (live.handle === handle)
|
|
348
|
+
live.handle = undefined;
|
|
302
349
|
if (signal.aborted || !READ_TOOLS.has(toolName))
|
|
303
350
|
throw error;
|
|
304
351
|
return {
|
|
@@ -309,32 +356,38 @@ export class SandboxManager {
|
|
|
309
356
|
}
|
|
310
357
|
}
|
|
311
358
|
/**
|
|
312
|
-
* Mounts the session
|
|
313
|
-
* all of them, later only
|
|
314
|
-
* one
|
|
359
|
+
* Mounts the skill folders the session needs that the workspace does not hold (D66): on a
|
|
360
|
+
* handle just opened all of them, later only versions another session has not mounted. Calls
|
|
361
|
+
* that need one folder at once share its mount. A failed outcome when one cannot be mounted;
|
|
362
|
+
* the folder is not marked mounted then, so the next call tries again.
|
|
315
363
|
*/
|
|
316
364
|
async mountSkills(live, handle, session, signal) {
|
|
317
365
|
const read = this.options.definitionFile;
|
|
318
366
|
if (!read)
|
|
319
367
|
return undefined;
|
|
320
|
-
for (const [
|
|
321
|
-
|
|
322
|
-
if (
|
|
323
|
-
|
|
368
|
+
for (const [folder, files] of sandboxSkillsOf(session.manifest)) {
|
|
369
|
+
let mounting = live.skills.get(folder);
|
|
370
|
+
if (!mounting) {
|
|
371
|
+
const mount = mountSkill(handle, folder, files, (sha256) => read(sha256, session), signal);
|
|
372
|
+
live.skills.set(folder, mount);
|
|
373
|
+
void mount.catch(() => {
|
|
374
|
+
if (live.skills.get(folder) === mount)
|
|
375
|
+
live.skills.delete(folder);
|
|
376
|
+
});
|
|
377
|
+
mounting = mount;
|
|
378
|
+
}
|
|
324
379
|
try {
|
|
325
|
-
await
|
|
380
|
+
await mounting;
|
|
326
381
|
}
|
|
327
382
|
catch (error) {
|
|
328
383
|
if (signal.aborted)
|
|
329
384
|
throw error;
|
|
330
|
-
live.skills.delete(name);
|
|
331
385
|
return {
|
|
332
386
|
kind: "failed",
|
|
333
387
|
code: "sandbox.skills_unavailable",
|
|
334
|
-
message: `The files of skill
|
|
388
|
+
message: `The files of skill folder ${folder} could not be put in the sandbox: ${error instanceof Error ? error.message : String(error)}`,
|
|
335
389
|
};
|
|
336
390
|
}
|
|
337
|
-
live.skills.set(name, signature);
|
|
338
391
|
}
|
|
339
392
|
return undefined;
|
|
340
393
|
}
|
|
@@ -347,12 +400,12 @@ export class SandboxManager {
|
|
|
347
400
|
network: describeNetwork(spec.network),
|
|
348
401
|
};
|
|
349
402
|
const creating = await this.record(spec.key, live, session.id, spec.image, "creating", existing);
|
|
350
|
-
await this.options.emit(session.id, session.activeTurnId, "
|
|
403
|
+
await this.options.emit(session.id, session.activeTurnId, "environment.state", { state: "creating", ...payload, ...(existing ? { reattach: true } : {}) }, { record: creating, ...claimOf(session) });
|
|
351
404
|
const handle = await live.backend.open(spec);
|
|
352
405
|
// A handle opened afresh holds no skills this manager knows of.
|
|
353
406
|
live.skills.clear();
|
|
354
407
|
const running = await this.record(spec.key, live, session.id, spec.image, "running", existing);
|
|
355
|
-
await this.options.emit(session.id, session.activeTurnId, "
|
|
408
|
+
await this.options.emit(session.id, session.activeTurnId, "environment.state", {
|
|
356
409
|
state: "running",
|
|
357
410
|
...payload,
|
|
358
411
|
...(existing && handle.created
|
|
@@ -371,7 +424,7 @@ export class SandboxManager {
|
|
|
371
424
|
}
|
|
372
425
|
finally {
|
|
373
426
|
const record = await this.record(key, live, live.sessionId, undefined, "stopped");
|
|
374
|
-
await this.options.emit(live.sessionId, null, "
|
|
427
|
+
await this.options.emit(live.sessionId, null, "environment.state", { state: "stopped", backend: live.backend.name }, { record });
|
|
375
428
|
}
|
|
376
429
|
}
|
|
377
430
|
async record(key, live, sessionId, image, state, known) {
|
|
@@ -380,7 +433,7 @@ export class SandboxManager {
|
|
|
380
433
|
const record = {
|
|
381
434
|
key,
|
|
382
435
|
sessionId,
|
|
383
|
-
...(live.
|
|
436
|
+
...(live.environmentId === undefined ? {} : { environmentId: live.environmentId }),
|
|
384
437
|
backend: live.backend.name,
|
|
385
438
|
image: image ?? existing?.image ?? "",
|
|
386
439
|
state,
|
|
@@ -395,18 +448,19 @@ export class SandboxManager {
|
|
|
395
448
|
return this.options.records.get(this.keyOf(sessionId));
|
|
396
449
|
}
|
|
397
450
|
/**
|
|
398
|
-
* Deletes sandbox resource `
|
|
451
|
+
* Deletes sandbox resource `environmentId`'s workspace, its files and its compute record, after
|
|
399
452
|
* any command queued on it. Called once the resource is deleted.
|
|
400
453
|
*/
|
|
401
|
-
async removeSandbox(
|
|
402
|
-
await this.remove(this.sandboxKeyOf(
|
|
454
|
+
async removeSandbox(environmentId) {
|
|
455
|
+
await this.remove(this.sandboxKeyOf(environmentId));
|
|
403
456
|
}
|
|
404
457
|
/** Deletes the workspace `key`, its files and its record, after any command queued on it. */
|
|
405
458
|
async remove(key) {
|
|
406
459
|
const live = this.live.get(key);
|
|
407
460
|
if (live) {
|
|
408
|
-
await
|
|
409
|
-
|
|
461
|
+
await this.alone(live, () => {
|
|
462
|
+
live.handle = undefined;
|
|
463
|
+
}).catch(() => undefined);
|
|
410
464
|
this.live.delete(key);
|
|
411
465
|
}
|
|
412
466
|
const { backend } = await this.ready;
|
|
@@ -425,9 +479,9 @@ export class SandboxManager {
|
|
|
425
479
|
for (const key of await backend.list(this.prefix)) {
|
|
426
480
|
const record = await this.options.records.get(key);
|
|
427
481
|
if (record) {
|
|
428
|
-
const kept = record.
|
|
482
|
+
const kept = record.environmentId === undefined
|
|
429
483
|
? await sessionExists(record.sessionId)
|
|
430
|
-
: await sandboxExists(record.
|
|
484
|
+
: await sandboxExists(record.environmentId);
|
|
431
485
|
if (kept)
|
|
432
486
|
continue;
|
|
433
487
|
}
|
|
@@ -439,7 +493,8 @@ export class SandboxManager {
|
|
|
439
493
|
if (!this.selection)
|
|
440
494
|
return;
|
|
441
495
|
const tasks = [...this.live.entries()].map(async ([key, live]) => {
|
|
442
|
-
await live.
|
|
496
|
+
await Promise.allSettled([...live.lanes.values(), live.gate]);
|
|
497
|
+
await Promise.allSettled([...live.running]);
|
|
443
498
|
if (this.options.ephemeral) {
|
|
444
499
|
live.handle = undefined;
|
|
445
500
|
await live.backend.remove(key);
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Placement (F7.2, D38): where a session's harness runs, checked when the session opens and
|
|
3
3
|
* never downgraded afterwards. The Tenant maps harness ids (`nylorun`, or `*` for the rest) to
|
|
4
|
-
* the hosts they may run on (`
|
|
5
|
-
* harness: in process, or the harness container) and `sandbox` (the engine in a pod
|
|
6
|
-
* Default: both.
|
|
4
|
+
* the hosts they may run on (`environments.config` `placement`): `harness-container` (the
|
|
5
|
+
* Runtime's harness: in process, or the harness container) and `sandbox` (the engine in a pod).
|
|
6
|
+
* Default: both. `"*": { hosts: [] }` refuses every harness the map does not name (R4.1).
|
|
7
7
|
*
|
|
8
|
-
* A session
|
|
9
|
-
* `harness-container
|
|
10
|
-
*
|
|
11
|
-
* sandboxes service not ready, it is `409
|
|
8
|
+
* A session on a pod Environment runs on `sandbox`; every other session (a virtual or `none`
|
|
9
|
+
* Environment, or none) on `harness-container` (D52). A host the Tenant does not allow is
|
|
10
|
+
* `409 placement_refused`. A pod Environment needs sandbox pods: without a cluster (`nylorun
|
|
11
|
+
* sandbox enable`), or with the sandboxes service not ready, it is `409 environment_unavailable`.
|
|
12
12
|
*/
|
|
13
|
-
import type
|
|
13
|
+
import { type EnvironmentKind, type PlacementHost as SandboxPlacementHost } from "@nylorun/core/contracts";
|
|
14
|
+
import type { TenantSandboxConfig } from "./tenant-config.js";
|
|
14
15
|
import type { TenantPods } from "../tenant/context.js";
|
|
15
|
-
/**
|
|
16
|
+
/** Our harness's id (blueprint D38): what a session runs unless its Environment names another. */
|
|
16
17
|
export declare const HARNESS_ID = "nylorun";
|
|
17
18
|
export declare const DEFAULT_PLACEMENT: readonly SandboxPlacementHost[];
|
|
18
19
|
/** The hosts harness `harnessId` may run on. */
|
|
19
20
|
export declare function placementOf(config: TenantSandboxConfig, harnessId?: string): readonly SandboxPlacementHost[];
|
|
20
21
|
/** 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
|
|
23
|
-
export declare function checkPlacement(config: TenantSandboxConfig, kind:
|
|
22
|
+
export declare const ENABLE_PODS = "run `nylorun local sandbox enable --context <name>` to give this Tenant a cluster";
|
|
23
|
+
/** Refuses a session whose harness, on the host its Environment needs, the Tenant does not allow. */
|
|
24
|
+
export declare function checkPlacement(config: TenantSandboxConfig, kind: EnvironmentKind | undefined, harness?: string): void;
|
|
24
25
|
/** Whether the sandboxes service is up, cached for a few seconds. */
|
|
25
26
|
export declare function podsReady(pods: TenantPods): Promise<boolean>;
|
|
26
|
-
/** Refuses a pod
|
|
27
|
+
/** Refuses a pod Environment without sandbox pods, or with the sandboxes service down. */
|
|
27
28
|
export declare function requirePods(pods: TenantPods | undefined, what: string): Promise<TenantPods>;
|
|
@@ -1,6 +1,19 @@
|
|
|
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 (`environments.config` `placement`): `harness-container` (the
|
|
5
|
+
* Runtime's harness: in process, or the harness container) and `sandbox` (the engine in a pod).
|
|
6
|
+
* Default: both. `"*": { hosts: [] }` refuses every harness the map does not name (R4.1).
|
|
7
|
+
*
|
|
8
|
+
* A session on a pod Environment runs on `sandbox`; every other session (a virtual or `none`
|
|
9
|
+
* Environment, or none) on `harness-container` (D52). A host the Tenant does not allow is
|
|
10
|
+
* `409 placement_refused`. A pod Environment needs sandbox pods: without a cluster (`nylorun
|
|
11
|
+
* sandbox enable`), or with the sandboxes service not ready, it is `409 environment_unavailable`.
|
|
12
|
+
*/
|
|
13
|
+
import { DEFAULT_HARNESS, } from "@nylorun/core/contracts";
|
|
1
14
|
import { fail } from "../tenant/http.js";
|
|
2
|
-
/**
|
|
3
|
-
export const HARNESS_ID =
|
|
15
|
+
/** Our harness's id (blueprint D38): what a session runs unless its Environment names another. */
|
|
16
|
+
export const HARNESS_ID = DEFAULT_HARNESS;
|
|
4
17
|
export const DEFAULT_PLACEMENT = ["harness-container", "sandbox"];
|
|
5
18
|
/** The hosts harness `harnessId` may run on. */
|
|
6
19
|
export function placementOf(config, harnessId = HARNESS_ID) {
|
|
@@ -8,15 +21,19 @@ export function placementOf(config, harnessId = HARNESS_ID) {
|
|
|
8
21
|
return placement?.[harnessId]?.hosts ?? placement?.["*"]?.hosts ?? DEFAULT_PLACEMENT;
|
|
9
22
|
}
|
|
10
23
|
/** 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
|
|
13
|
-
export function checkPlacement(config, kind) {
|
|
24
|
+
export const ENABLE_PODS = "run `nylorun local sandbox enable --context <name>` to give this Tenant a cluster";
|
|
25
|
+
/** Refuses a session whose harness, on the host its Environment needs, the Tenant does not allow. */
|
|
26
|
+
export function checkPlacement(config, kind, harness = HARNESS_ID) {
|
|
14
27
|
const host = kind === "pod" ? "sandbox" : "harness-container";
|
|
15
|
-
const allowed = placementOf(config);
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
const allowed = placementOf(config, harness);
|
|
29
|
+
if (allowed.includes(host))
|
|
30
|
+
return;
|
|
31
|
+
const where = allowed.length === 0 ? "it may run nowhere" : `placement allows ${allowed.join(", ")}`;
|
|
32
|
+
fail(409, allowed.length === 0
|
|
33
|
+
? `This Tenant does not run the ${harness} harness (${where}).`
|
|
34
|
+
: kind === "pod"
|
|
35
|
+
? `This Tenant does not let the ${harness} harness run in a sandbox pod (${where}).`
|
|
36
|
+
: `This Tenant runs the ${harness} harness only in sandbox pods (${where}): open the session on a pod Environment.`, { code: "placement_refused", details: { harness, host, allowed: [...allowed] } });
|
|
20
37
|
}
|
|
21
38
|
const READY_CACHE_MS = 5_000;
|
|
22
39
|
const ready = new WeakMap();
|
|
@@ -29,15 +46,15 @@ export async function podsReady(pods) {
|
|
|
29
46
|
ready.set(pods, { at: Date.now(), ready: answer });
|
|
30
47
|
return answer;
|
|
31
48
|
}
|
|
32
|
-
/** Refuses a pod
|
|
49
|
+
/** Refuses a pod Environment without sandbox pods, or with the sandboxes service down. */
|
|
33
50
|
export async function requirePods(pods, what) {
|
|
34
51
|
if (!pods)
|
|
35
52
|
return fail(409, `${what} needs sandbox pods, and this Runtime has no cluster: ${ENABLE_PODS}.`, {
|
|
36
|
-
code: "
|
|
53
|
+
code: "environment_unavailable",
|
|
37
54
|
});
|
|
38
55
|
if (!(await podsReady(pods)))
|
|
39
|
-
fail(409, `${what} needs sandbox pods, and the sandboxes service is not ready. Check \`nylorun sandbox status\`.`, {
|
|
40
|
-
code: "
|
|
56
|
+
fail(409, `${what} needs sandbox pods, and the sandboxes service is not ready. Check \`nylorun local sandbox status\`.`, {
|
|
57
|
+
code: "environment_unavailable",
|
|
41
58
|
details: { retryAfterSeconds: 5 },
|
|
42
59
|
}, { "retry-after": "5" });
|
|
43
60
|
return pods;
|
|
@@ -13,7 +13,12 @@ export interface PodSpec {
|
|
|
13
13
|
readonly mode: "Running" | "Suspended";
|
|
14
14
|
/** The workload image; the service defaults it to `python:3.13-slim`. */
|
|
15
15
|
readonly image?: string;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* What the workload runs: `engine` (default), our engine copied from `harnessImage`, or
|
|
18
|
+
* `image`, the image's own entrypoint, for a custom harness built into it (R4.4).
|
|
19
|
+
*/
|
|
20
|
+
readonly entrypoint?: "engine" | "image";
|
|
21
|
+
/** The Runtime image the engine is copied from; not with `entrypoint: "image"`. */
|
|
17
22
|
readonly harnessImage?: string;
|
|
18
23
|
/** Replaces the engine command (diagnostics). */
|
|
19
24
|
readonly command?: readonly string[];
|
|
@@ -39,6 +44,8 @@ export interface PodStatus {
|
|
|
39
44
|
readonly expired: boolean;
|
|
40
45
|
readonly podUID?: string;
|
|
41
46
|
readonly podPhase?: string;
|
|
47
|
+
/** The workload container's image as the node pulled it (`repo@sha256:…`), once it runs. */
|
|
48
|
+
readonly imageID?: string;
|
|
42
49
|
readonly volume: "present" | "missing";
|
|
43
50
|
readonly opId?: string;
|
|
44
51
|
readonly shutdownTime?: string;
|
|
@@ -47,7 +54,7 @@ export interface PodStatus {
|
|
|
47
54
|
/** With `?wait=`: whether the state was reached before the timeout. */
|
|
48
55
|
readonly met?: boolean;
|
|
49
56
|
}
|
|
50
|
-
/** `GET /v1/info`: the cluster `nylorun sandbox enable` recorded. */
|
|
57
|
+
/** `GET /v1/info`: the cluster `nylorun local sandbox enable` recorded. */
|
|
51
58
|
export interface ClusterInfo {
|
|
52
59
|
readonly namespace: string;
|
|
53
60
|
readonly context: string;
|