@nylorun/runtime 0.22.0-beta → 0.24.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 +161 -0
- package/README.md +95 -30
- package/dist/adapters/execution/restate.d.ts +15 -3
- package/dist/adapters/execution/restate.js +78 -84
- 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/adapters/streams/s2.js +1 -13
- package/dist/api/a2a/endpoint.js +5 -1
- package/dist/api/a2a/routes.d.ts +2 -0
- package/dist/api/a2a/routes.js +2 -2
- package/dist/api/a2a/tasks.js +2 -1
- package/dist/api/ag-ui/endpoint.js +4 -2
- package/dist/api/ag-ui/history.js +4 -1
- package/dist/api/ag-ui/routes.d.ts +4 -1
- package/dist/api/ag-ui/routes.js +11 -10
- package/dist/api/ag-ui/translate.js +3 -2
- package/dist/api/components.d.ts +4953 -1204
- package/dist/api/components.js +25 -16
- package/dist/api/gate/keys.js +7 -1
- package/dist/api/gate/routes.d.ts +6 -0
- package/dist/api/gate/routes.js +39 -3
- package/dist/api/http/app.js +4 -4
- 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/environments.d.ts +11 -0
- package/dist/api/http/routes/environments.js +283 -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 +45 -9
- package/dist/api/http/routes/reads.js +54 -5
- package/dist/api/http/routes/sessions.js +11 -6
- package/dist/api/http/routes/tenant.js +32 -10
- package/dist/api/openapi.js +31 -26
- package/dist/artifacts/disk.d.ts +59 -0
- package/dist/artifacts/disk.js +563 -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/service.d.ts +2 -1
- package/dist/artifacts/service.js +6 -1
- package/dist/artifacts/workspace.d.ts +1 -1
- package/dist/artifacts/workspace.js +1 -1
- package/dist/core/flow-host.d.ts +3 -22
- package/dist/core/flow-host.js +11 -37
- 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 +10 -43
- package/dist/execution/types.d.ts +11 -9
- package/dist/execution/types.js +5 -7
- 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 +28 -1
- package/dist/gates/egress.js +115 -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 +6 -2
- package/dist/gates/http-tool.js +3 -5
- 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 +52 -0
- package/dist/gates/mcp-handler.d.ts +4 -0
- package/dist/gates/mcp-handler.js +5 -1
- 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 +134 -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 +10 -68
- package/dist/gates/tool-contract.js +1 -85
- package/dist/gates/tool-gate.d.ts +1 -1
- package/dist/harness/calls.d.ts +12 -5
- package/dist/harness/calls.js +21 -12
- package/dist/harness/executors.js +12 -9
- package/dist/harness/main.js +18 -5
- package/dist/harness/pod.d.ts +34 -0
- package/dist/harness/pod.js +41 -0
- package/dist/harness/service.d.ts +0 -1
- package/dist/harness/service.js +13 -5
- package/dist/harness/setup.d.ts +27 -0
- package/dist/harness/setup.js +150 -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 +174 -44
- package/dist/harness-api/redact.d.ts +3 -0
- package/dist/harness-api/redact.js +45 -0
- package/dist/harness-api/server.d.ts +8 -0
- package/dist/harness-api/server.js +93 -13
- 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 +96 -0
- package/dist/harness-api/tool-journal.js +102 -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 +6 -3
- package/dist/harness-api/workspace.js +27 -11
- package/dist/harness-api/ws-server.d.ts +11 -3
- package/dist/harness-api/ws-server.js +94 -15
- package/dist/host/execution.d.ts +8 -6
- package/dist/host/gates.d.ts +2 -1
- package/dist/host/gates.js +10 -2
- package/dist/host/main.js +64 -21
- package/dist/host/operate.js +67 -5
- package/dist/host/stack-config.d.ts +70 -0
- package/dist/host/stack-config.js +67 -1
- package/dist/infra/execution.d.ts +18 -1
- package/dist/infra/execution.js +28 -4
- package/dist/infra/index.d.ts +1 -1
- package/dist/keys/client.js +4 -1
- package/dist/keys/keys.d.ts +5 -5
- package/dist/keys/keys.js +5 -7
- package/dist/management-openapi.json +999 -2211
- package/dist/mcp/connect.d.ts +25 -20
- package/dist/mcp/connect.js +29 -30
- package/dist/mcp/pool.d.ts +2 -2
- package/dist/mcp/pool.js +1 -1
- package/dist/mcp/preview.d.ts +3 -3
- package/dist/mcp/preview.js +10 -20
- package/dist/mcp/snapshot.d.ts +5 -11
- package/dist/mcp/snapshot.js +6 -7
- package/dist/model/pi-model.js +8 -0
- package/dist/openapi.json +6955 -2526
- 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/retry.d.ts +21 -0
- package/dist/retry.js +34 -0
- 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 -20
- package/dist/sandbox/manager.d.ts +16 -8
- package/dist/sandbox/manager.js +33 -17
- package/dist/sandbox/placement.d.ts +13 -12
- package/dist/sandbox/placement.js +29 -12
- package/dist/sandbox/pods/client.d.ts +6 -1
- package/dist/sandbox/pods/lifecycle.d.ts +28 -27
- package/dist/sandbox/pods/lifecycle.js +14 -26
- package/dist/sandbox/pods/reconcile.js +21 -15
- package/dist/sandbox/pods/spec.d.ts +15 -13
- package/dist/sandbox/pods/spec.js +16 -20
- package/dist/sandbox/records.d.ts +2 -2
- package/dist/sandbox/records.js +3 -3
- package/dist/sandbox/resolve.d.ts +35 -27
- package/dist/sandbox/resolve.js +66 -41
- package/dist/sandbox/session-sandbox.js +1 -1
- package/dist/sandbox/setup.d.ts +61 -0
- package/dist/sandbox/setup.js +123 -0
- package/dist/sandbox/share.d.ts +11 -21
- package/dist/sandbox/share.js +12 -41
- 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/control.js +8 -1
- package/dist/store/postgres/drizzle/0018_wake_outbox_retries.sql +3 -0
- package/dist/store/postgres/drizzle/0019_sandbox_signal_outbox.sql +18 -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/meta/_journal.json +42 -0
- package/dist/store/postgres/reads.js +117 -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 +1213 -779
- package/dist/store/postgres/schema.js +136 -76
- package/dist/store/postgres/store.js +283 -117
- package/dist/store/types.d.ts +206 -88
- package/dist/streams/relay/core.js +1 -13
- package/dist/tenant/advance.d.ts +7 -6
- package/dist/tenant/advance.js +121 -49
- package/dist/tenant/artifact-tool.js +3 -4
- package/dist/tenant/commands.d.ts +15 -2
- package/dist/tenant/commands.js +94 -14
- package/dist/tenant/context.d.ts +67 -33
- package/dist/tenant/context.js +4 -15
- package/dist/tenant/control.d.ts +8 -1
- package/dist/tenant/control.js +2 -0
- package/dist/tenant/effects.d.ts +1 -1
- package/dist/tenant/effects.js +37 -20
- package/dist/tenant/environments.d.ts +167 -0
- package/dist/tenant/environments.js +823 -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/principals.js +1 -1
- package/dist/tenant/reset.js +3 -3
- package/dist/tenant/runtime.d.ts +6 -1
- package/dist/tenant/runtime.js +30 -4
- package/dist/tenant/scheduler.d.ts +7 -0
- package/dist/tenant/scheduler.js +38 -7
- package/dist/tenant/session-streams.d.ts +0 -1
- package/dist/tenant/session-streams.js +1 -13
- package/dist/tenant/session.d.ts +3 -3
- package/dist/tenant/session.js +0 -14
- package/dist/tenant/sessions.d.ts +13 -13
- package/dist/tenant/sessions.js +61 -138
- package/dist/tenant/signing-keys.d.ts +5 -1
- package/dist/tenant/status.js +3 -3
- package/dist/tenant/streams.js +14 -29
- package/dist/tenant/sweep.d.ts +39 -12
- package/dist/tenant/sweep.js +168 -63
- package/dist/tenant/transcript.d.ts +210 -20
- package/dist/tenant/transcript.js +62 -11
- 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 +42 -0
- package/dist/traces/ids.js +85 -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 +317 -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 +64 -0
- package/dist/vault/error.d.ts +11 -1
- package/dist/vault/error.js +5 -1
- package/dist/vault/service.d.ts +47 -48
- package/dist/vault/service.js +321 -382
- 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 +14 -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/tenant/sandboxes.d.ts +0 -122
- package/dist/tenant/sandboxes.js +0 -562
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** A setup failure's `reason` starts with this (`sandbox/setup.ts`); it is sticky. */
|
|
2
|
+
export const SETUP_FAILED = "setup:";
|
|
1
3
|
/** A pod not ready this long after it was asked to run has failed. */
|
|
2
4
|
export const READY_DEADLINE_MS = 5 * 60_000;
|
|
3
5
|
/** How soon to look again while the pod changes. */
|
|
@@ -67,24 +69,6 @@ export function decide(facts) {
|
|
|
67
69
|
again(POLL_MS);
|
|
68
70
|
return done();
|
|
69
71
|
}
|
|
70
|
-
const expiresAt = pod.expiresAt === undefined ? undefined : Date.parse(pod.expiresAt);
|
|
71
|
-
if (expiresAt !== undefined && now >= expiresAt) {
|
|
72
|
-
if (pod.observed !== "expired") {
|
|
73
|
-
patch.observed = "expired";
|
|
74
|
-
patch.hostEpoch = pod.hostEpoch + 1;
|
|
75
|
-
events.push(event("sandbox.expired", { onExpiry: facts.config.onExpiry }));
|
|
76
|
-
}
|
|
77
|
-
if (facts.config.onExpiry === "delete") {
|
|
78
|
-
patch.desired = "deleted";
|
|
79
|
-
patch.rev = pod.rev + 1;
|
|
80
|
-
if (status.exists && !status.deleting)
|
|
81
|
-
deletes.push(pod.k8sName);
|
|
82
|
-
again(POLL_MS);
|
|
83
|
-
}
|
|
84
|
-
return done();
|
|
85
|
-
}
|
|
86
|
-
if (expiresAt !== undefined)
|
|
87
|
-
arm.push({ timer: "ttl", at: expiresAt });
|
|
88
72
|
let desired = pod.desired;
|
|
89
73
|
let rev = pod.rev;
|
|
90
74
|
if (facts.trigger === "idle" && desired === "running" && !facts.busy) {
|
|
@@ -96,7 +80,7 @@ export function decide(facts) {
|
|
|
96
80
|
patch.desired = desired;
|
|
97
81
|
patch.rev = rev;
|
|
98
82
|
patch.hostEpoch = pod.hostEpoch + 1;
|
|
99
|
-
events.push(event("
|
|
83
|
+
events.push(event("environment.suspended", { reason: "idle" }));
|
|
100
84
|
}
|
|
101
85
|
else
|
|
102
86
|
arm.push({ timer: "idle", at: due });
|
|
@@ -104,14 +88,14 @@ export function decide(facts) {
|
|
|
104
88
|
// An incarnation a pod has joined, whose Sandbox or volume is gone: lost.
|
|
105
89
|
if (pod.podUid !== undefined && (!status.exists || status.deleting || status.volume === "missing")) {
|
|
106
90
|
const reason = !status.exists
|
|
107
|
-
? "The
|
|
91
|
+
? "The Environment's pod and volume were deleted outside the Runtime"
|
|
108
92
|
: status.deleting
|
|
109
|
-
? "The
|
|
110
|
-
: "The
|
|
93
|
+
? "The Environment's pod is being deleted outside the Runtime"
|
|
94
|
+
: "The Environment's volume is gone";
|
|
111
95
|
patch.observed = "lost";
|
|
112
96
|
patch.reason = reason;
|
|
113
97
|
patch.hostEpoch = (patch.hostEpoch ?? pod.hostEpoch) + 1;
|
|
114
|
-
events.push(event("
|
|
98
|
+
events.push(event("environment.lost", { reason }));
|
|
115
99
|
if (status.exists && !status.deleting)
|
|
116
100
|
deletes.push(pod.k8sName);
|
|
117
101
|
if (status.exists)
|
|
@@ -119,7 +103,7 @@ export function decide(facts) {
|
|
|
119
103
|
return done();
|
|
120
104
|
}
|
|
121
105
|
if (status.deleting) {
|
|
122
|
-
// The previous incarnation of this name is still going (
|
|
106
|
+
// The previous incarnation of this name is still going (an Environment deleted and created again).
|
|
123
107
|
again(POLL_MS);
|
|
124
108
|
return done();
|
|
125
109
|
}
|
|
@@ -135,7 +119,7 @@ export function decide(facts) {
|
|
|
135
119
|
patch.startedAt = new Date(now).toISOString();
|
|
136
120
|
patch.reason = null;
|
|
137
121
|
}
|
|
138
|
-
// A pod started by this apply (a create, a resume) is the
|
|
122
|
+
// A pod started by this apply (a create, a resume) is the Environment's first, not a relaunch.
|
|
139
123
|
if (desired === "running" && pod.podUid !== undefined)
|
|
140
124
|
patch.podUid = null;
|
|
141
125
|
again(POLL_MS);
|
|
@@ -144,6 +128,10 @@ export function decide(facts) {
|
|
|
144
128
|
});
|
|
145
129
|
}
|
|
146
130
|
if (desired === "running") {
|
|
131
|
+
// A failed setup (R2d step 2) stays failed until a turn start asks the pod to run again,
|
|
132
|
+
// though the pod itself is ready: its engine takes no turn meanwhile.
|
|
133
|
+
if (pod.observed === "failed" && pod.reason?.startsWith(SETUP_FAILED))
|
|
134
|
+
return done();
|
|
147
135
|
if (status.ready) {
|
|
148
136
|
if (pod.observed !== "running") {
|
|
149
137
|
patch.observed = "running";
|
|
@@ -159,7 +147,7 @@ export function decide(facts) {
|
|
|
159
147
|
`The pod was not ready within ${READY_DEADLINE_MS / 60_000} minutes`;
|
|
160
148
|
patch.observed = "failed";
|
|
161
149
|
patch.reason = reason;
|
|
162
|
-
events.push(event("
|
|
150
|
+
events.push(event("environment.failed", { reason }));
|
|
163
151
|
return done();
|
|
164
152
|
}
|
|
165
153
|
if (pod.observed !== "creating")
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
* infrastructure errors throw; Restate retries those.
|
|
16
16
|
*/
|
|
17
17
|
import { randomBytes } from "node:crypto";
|
|
18
|
-
import { readSandboxConfig } from "../tenant-config.js";
|
|
18
|
+
import { effectiveSandboxConfig, readSandboxConfig } from "../tenant-config.js";
|
|
19
|
+
import { podSandboxSpecOf } from "../resolve.js";
|
|
19
20
|
import { sha256Hex } from "../join.js";
|
|
21
|
+
import { recoverLostVolume } from "../../artifacts/disk.js";
|
|
20
22
|
import { decide } from "./lifecycle.js";
|
|
21
23
|
import { podLifecycleConfig, podSpecOf } from "./spec.js";
|
|
22
24
|
export async function reconcileSandbox(ctx, sandboxId, trigger) {
|
|
@@ -24,11 +26,11 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
|
|
|
24
26
|
if (!pods || ctx.closed)
|
|
25
27
|
return {};
|
|
26
28
|
const read = await ctx.store.tx(async (t) => {
|
|
27
|
-
const row = await t.
|
|
29
|
+
const row = await t.environment(sandboxId);
|
|
28
30
|
if (!row || row.kind !== "pod" || !row.pod)
|
|
29
31
|
return undefined;
|
|
30
32
|
const config = await readSandboxConfig(t);
|
|
31
|
-
const busy = (await t.
|
|
33
|
+
const busy = (await t.sessionsOnEnvironment(sandboxId)).some((session) => session.activeTurnId != null);
|
|
32
34
|
return { row: row, config, busy };
|
|
33
35
|
});
|
|
34
36
|
if (!read)
|
|
@@ -47,7 +49,7 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
|
|
|
47
49
|
return undefined;
|
|
48
50
|
}
|
|
49
51
|
};
|
|
50
|
-
const lifecycle = podLifecycleConfig(config);
|
|
52
|
+
const lifecycle = podLifecycleConfig(config, row.recipe);
|
|
51
53
|
const decision = decide({
|
|
52
54
|
row,
|
|
53
55
|
config: lifecycle,
|
|
@@ -69,10 +71,10 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
|
|
|
69
71
|
joinToken = randomBytes(32).toString("base64url");
|
|
70
72
|
rev += 1;
|
|
71
73
|
const recorded = await ctx.store.tx(async (t) => {
|
|
72
|
-
const current = await t.
|
|
74
|
+
const current = await t.environment(sandboxId, { lock: true });
|
|
73
75
|
if (!unchanged(current?.pod))
|
|
74
76
|
return false;
|
|
75
|
-
await t.
|
|
77
|
+
await t.updateEnvironmentPod(sandboxId, { joinTokenHash: sha256Hex(joinToken), rev }, new Date().toISOString());
|
|
76
78
|
return true;
|
|
77
79
|
});
|
|
78
80
|
if (!recorded)
|
|
@@ -95,12 +97,13 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
|
|
|
95
97
|
}
|
|
96
98
|
if (decision.apply) {
|
|
97
99
|
const spec = podSpecOf({
|
|
98
|
-
sandboxId,
|
|
99
|
-
spec: row.
|
|
100
|
+
environmentId: sandboxId,
|
|
101
|
+
spec: podSandboxSpecOf(row.applied, effectiveSandboxConfig(config).limits.idle),
|
|
100
102
|
pod: { ...row.pod, rev },
|
|
101
103
|
config,
|
|
102
104
|
mode: decision.apply.mode,
|
|
103
105
|
harnessImage: pods.harnessImage,
|
|
106
|
+
harness: row.applied.harness,
|
|
104
107
|
...(joinToken ? { joinToken } : {}),
|
|
105
108
|
});
|
|
106
109
|
try {
|
|
@@ -117,13 +120,13 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
|
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
const written = await ctx.store.tx(async (t) => {
|
|
120
|
-
const current = await t.
|
|
123
|
+
const current = await t.environment(sandboxId, { lock: true });
|
|
121
124
|
if (!current?.pod)
|
|
122
125
|
return { kind: "gone" };
|
|
123
126
|
if (!unchanged(current.pod))
|
|
124
127
|
return { kind: "changed" };
|
|
125
128
|
if (decision.removeRow) {
|
|
126
|
-
await t.
|
|
129
|
+
await t.deleteEnvironment(sandboxId);
|
|
127
130
|
return { kind: "removed" };
|
|
128
131
|
}
|
|
129
132
|
const epochMoved = patch.hostEpoch !== undefined && patch.hostEpoch !== null;
|
|
@@ -132,10 +135,12 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
|
|
|
132
135
|
...(epochMoved ? { hostEpoch: current.pod.hostEpoch + 1 } : {}),
|
|
133
136
|
};
|
|
134
137
|
if (Object.keys(finalPatch).length > 0)
|
|
135
|
-
await t.
|
|
138
|
+
await t.updateEnvironmentPod(sandboxId, finalPatch, new Date().toISOString());
|
|
136
139
|
for (const item of decision.events)
|
|
137
|
-
await t.
|
|
138
|
-
|
|
140
|
+
await t.environmentEvent(sandboxId, item.type, item.payload);
|
|
141
|
+
if (epochMoved)
|
|
142
|
+
await t.signal({ type: "host.revoked", sandboxId, epoch: finalPatch.hostEpoch });
|
|
143
|
+
return { kind: "written" };
|
|
139
144
|
});
|
|
140
145
|
if (written.kind === "changed")
|
|
141
146
|
return { retryAfterMs: 0 };
|
|
@@ -143,8 +148,9 @@ export async function reconcileSandbox(ctx, sandboxId, trigger) {
|
|
|
143
148
|
await ctx.sandbox.removeSandbox(sandboxId).catch(() => undefined);
|
|
144
149
|
return {};
|
|
145
150
|
}
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
// A lost volume with a disk version starts again from the latest one (R2d step 3).
|
|
152
|
+
if (patch.observed === "lost" && (await recoverLostVolume(ctx, sandboxId).catch(() => false)))
|
|
153
|
+
return { retryAfterMs: 0 };
|
|
148
154
|
return {
|
|
149
155
|
...(decision.retryAfterMs === undefined ? {} : { retryAfterMs: decision.retryAfterMs }),
|
|
150
156
|
...(decision.arm.length > 0 ? { arm: decision.arm } : {}),
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `PUT /v1/pods/{name}` body of a pod
|
|
2
|
+
* The `PUT /v1/pods/{name}` body of a pod Environment: its applied recipe, the Tenant's lifecycle
|
|
3
3
|
* settings (D36, read at every apply) and the incarnation's fences. The pod shape itself is
|
|
4
4
|
* rendered only by the sandboxes service.
|
|
5
5
|
*/
|
|
6
6
|
import { type SandboxManifest } from "@nylorun/core/define";
|
|
7
|
-
import type
|
|
7
|
+
import { type EnvironmentRecipe } from "@nylorun/core/contracts";
|
|
8
|
+
import type { TenantSandboxConfig } from "../tenant-config.js";
|
|
8
9
|
import type { SandboxPodState } from "../../store/types.js";
|
|
9
10
|
import type { PodSpec } from "./client.js";
|
|
10
11
|
import { type PodLifecycleConfig } from "./lifecycle.js";
|
|
@@ -14,32 +15,33 @@ export declare const DEFAULT_STOP_GRACE_SECONDS = 10;
|
|
|
14
15
|
export declare const DEFAULT_STORAGE_GIB = 5;
|
|
15
16
|
/** Where the engine in the pod keeps its records, plugin data and home (on the volume). */
|
|
16
17
|
export declare const POD_HARNESS_ROOT = "/harness";
|
|
17
|
-
/** What a pod
|
|
18
|
+
/** What a pod's spec holds beyond a virtual sandbox's. */
|
|
18
19
|
export type PodSandboxSpec = SandboxManifest & {
|
|
19
20
|
readonly storage?: string;
|
|
20
|
-
readonly lifecycle?: {
|
|
21
|
-
readonly ttl?: string;
|
|
22
|
-
};
|
|
23
21
|
};
|
|
24
|
-
/**
|
|
25
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The lifecycle settings with their defaults: the Tenant's (D36), and the Environment's
|
|
24
|
+
* `lifecycle.idle` within the Tenant's `limits.idle` when `recipe` is given.
|
|
25
|
+
*/
|
|
26
|
+
export declare function podLifecycleConfig(config: TenantSandboxConfig, recipe?: Pick<EnvironmentRecipe, "lifecycle">): PodLifecycleConfig & {
|
|
26
27
|
readonly stopGraceSeconds: number;
|
|
27
28
|
};
|
|
28
29
|
/** The volume's size in GiB. */
|
|
29
30
|
export declare function storageGiBOf(spec: PodSandboxSpec): number;
|
|
30
|
-
/** The Tenant's default image, when its default sandbox names one. */
|
|
31
|
-
export declare function tenantDefaultImage(config: TenantSandboxConfig): string | undefined;
|
|
32
31
|
export interface PodSpecInput {
|
|
33
|
-
readonly
|
|
32
|
+
readonly environmentId: string;
|
|
34
33
|
readonly spec: PodSandboxSpec;
|
|
35
34
|
readonly pod: SandboxPodState;
|
|
36
35
|
readonly config: TenantSandboxConfig;
|
|
37
36
|
readonly mode: "Running" | "Suspended";
|
|
38
37
|
/** The Runtime image the engine is copied from. */
|
|
39
38
|
readonly harnessImage: string;
|
|
39
|
+
/**
|
|
40
|
+
* The harness the pod runs (R4.4): ours, copied in from `harnessImage`, or a custom one, which
|
|
41
|
+
* is the workload image itself and runs as its entrypoint.
|
|
42
|
+
*/
|
|
43
|
+
readonly harness?: string;
|
|
40
44
|
/** The join token, when this apply rotates it. */
|
|
41
45
|
readonly joinToken?: string;
|
|
42
46
|
}
|
|
43
47
|
export declare function podSpecOf(input: PodSpecInput): PodSpec;
|
|
44
|
-
/** When a sandbox with this TTL, created at `createdAt`, expires. */
|
|
45
|
-
export declare function expiresAtOf(createdAt: string, ttl: string | undefined): string | undefined;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `PUT /v1/pods/{name}` body of a pod
|
|
2
|
+
* The `PUT /v1/pods/{name}` body of a pod Environment: its applied recipe, the Tenant's lifecycle
|
|
3
3
|
* settings (D36, read at every apply) and the incarnation's fences. The pod shape itself is
|
|
4
4
|
* rendered only by the sandboxes service.
|
|
5
5
|
*/
|
|
6
6
|
import { parseSandboxDuration, parseSandboxSize } from "@nylorun/core/define";
|
|
7
|
+
import { DEFAULT_HARNESS } from "@nylorun/core/contracts";
|
|
7
8
|
import { memoryMiBOf } from "../policy.js";
|
|
8
9
|
import { opIdOf } from "./lifecycle.js";
|
|
9
10
|
/** The pod's grace period to stop when the Tenant sets none (D34). */
|
|
@@ -12,12 +13,16 @@ export const DEFAULT_STOP_GRACE_SECONDS = 10;
|
|
|
12
13
|
export const DEFAULT_STORAGE_GIB = 5;
|
|
13
14
|
/** Where the engine in the pod keeps its records, plugin data and home (on the volume). */
|
|
14
15
|
export const POD_HARNESS_ROOT = "/harness";
|
|
15
|
-
/**
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The lifecycle settings with their defaults: the Tenant's (D36), and the Environment's
|
|
18
|
+
* `lifecycle.idle` within the Tenant's `limits.idle` when `recipe` is given.
|
|
19
|
+
*/
|
|
20
|
+
export function podLifecycleConfig(config, recipe) {
|
|
17
21
|
const stopGrace = config.lifecycle?.stopGrace;
|
|
22
|
+
const tenantIdle = parseSandboxDuration(config.limits?.idle ?? "15m") ?? 15 * 60_000;
|
|
23
|
+
const own = recipe?.lifecycle.idle === undefined ? undefined : parseSandboxDuration(recipe.lifecycle.idle);
|
|
18
24
|
return {
|
|
19
|
-
|
|
20
|
-
idleMs: parseSandboxDuration(config.limits?.idle ?? "15m") ?? 15 * 60_000,
|
|
25
|
+
idleMs: own === undefined ? tenantIdle : Math.min(own, tenantIdle),
|
|
21
26
|
stopGraceSeconds: stopGrace === undefined
|
|
22
27
|
? DEFAULT_STOP_GRACE_SECONDS
|
|
23
28
|
: Math.max(1, Math.min(30, Math.round((parseSandboxDuration(stopGrace) ?? 10_000) / 1000))),
|
|
@@ -28,35 +33,26 @@ export function storageGiBOf(spec) {
|
|
|
28
33
|
const bytes = spec.storage === undefined ? undefined : parseSandboxSize(spec.storage);
|
|
29
34
|
return bytes === undefined ? DEFAULT_STORAGE_GIB : Math.max(1, Math.ceil(bytes / 1024 ** 3));
|
|
30
35
|
}
|
|
31
|
-
/** The Tenant's default image, when its default sandbox names one. */
|
|
32
|
-
export function tenantDefaultImage(config) {
|
|
33
|
-
const fallback = config.default;
|
|
34
|
-
return typeof fallback === "object" ? fallback.image : undefined;
|
|
35
|
-
}
|
|
36
36
|
export function podSpecOf(input) {
|
|
37
37
|
const { spec, pod, config } = input;
|
|
38
38
|
const lifecycle = podLifecycleConfig(config);
|
|
39
|
-
const image = spec.image
|
|
39
|
+
const image = spec.image;
|
|
40
|
+
const ours = (input.harness ?? DEFAULT_HARNESS) === DEFAULT_HARNESS;
|
|
40
41
|
return {
|
|
41
42
|
opId: opIdOf(pod),
|
|
42
43
|
mode: input.mode,
|
|
43
44
|
...(image ? { image } : {}),
|
|
44
|
-
|
|
45
|
+
// A custom harness is its image: the pod runs the image's entrypoint, nothing copied in.
|
|
46
|
+
...(ours ? { harnessImage: input.harnessImage } : { entrypoint: "image" }),
|
|
45
47
|
cpus: spec.resources?.cpus ?? 1,
|
|
46
48
|
memoryMiB: memoryMiBOf(spec),
|
|
47
49
|
storageGiB: storageGiBOf(spec),
|
|
48
50
|
stopGraceSeconds: lifecycle.stopGraceSeconds,
|
|
49
|
-
|
|
50
|
-
shutdownPolicy: lifecycle.onExpiry === "delete" ? "Delete" : "Retain",
|
|
51
|
+
shutdownPolicy: "Retain",
|
|
51
52
|
env: {
|
|
52
|
-
NYLORUN_SANDBOX_ID: input.
|
|
53
|
+
NYLORUN_SANDBOX_ID: input.environmentId,
|
|
53
54
|
NYLORUN_HARNESS_ROOT: POD_HARNESS_ROOT,
|
|
54
55
|
},
|
|
55
56
|
...(input.joinToken ? { joinToken: input.joinToken } : {}),
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
|
-
/** When a sandbox with this TTL, created at `createdAt`, expires. */
|
|
59
|
-
export function expiresAtOf(createdAt, ttl) {
|
|
60
|
-
const ms = ttl === undefined ? undefined : parseSandboxDuration(ttl);
|
|
61
|
-
return ms === undefined ? undefined : new Date(Date.parse(createdAt) + ms).toISOString();
|
|
62
|
-
}
|
|
@@ -16,10 +16,10 @@ export declare function workspacePrefix(scope: string): string;
|
|
|
16
16
|
/** The key of session `sessionId`'s own workspace. */
|
|
17
17
|
export declare function sessionWorkspaceKey(scope: string, sessionId: string): string;
|
|
18
18
|
/**
|
|
19
|
-
* The key of sandbox resource `
|
|
19
|
+
* The key of sandbox resource `environmentId`'s workspace: apart from every session key (16 hex
|
|
20
20
|
* characters after the prefix). The virtual backend keeps it in `<sandboxes>/<key>/workspace`.
|
|
21
21
|
*/
|
|
22
|
-
export declare function sandboxWorkspaceKey(scope: string,
|
|
22
|
+
export declare function sandboxWorkspaceKey(scope: string, environmentId: string): string;
|
|
23
23
|
/** The records in the Tenant's `sandboxes` table. */
|
|
24
24
|
export declare function storeSandboxRecords(store: SessionStore): SandboxRecords;
|
|
25
25
|
/**
|
package/dist/sandbox/records.js
CHANGED
|
@@ -21,11 +21,11 @@ export function sessionWorkspaceKey(scope, sessionId) {
|
|
|
21
21
|
return workspacePrefix(scope) + createHash("sha256").update(sessionId).digest("hex").slice(0, 16);
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* The key of sandbox resource `
|
|
24
|
+
* The key of sandbox resource `environmentId`'s workspace: apart from every session key (16 hex
|
|
25
25
|
* characters after the prefix). The virtual backend keeps it in `<sandboxes>/<key>/workspace`.
|
|
26
26
|
*/
|
|
27
|
-
export function sandboxWorkspaceKey(scope,
|
|
28
|
-
return `${workspacePrefix(scope)}sbx-${createHash("sha256").update(
|
|
27
|
+
export function sandboxWorkspaceKey(scope, environmentId) {
|
|
28
|
+
return `${workspacePrefix(scope)}sbx-${createHash("sha256").update(environmentId).digest("hex").slice(0, 16)}`;
|
|
29
29
|
}
|
|
30
30
|
/** The records in the Tenant's `sandboxes` table. */
|
|
31
31
|
export function storeSandboxRecords(store) {
|
|
@@ -1,37 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resolves
|
|
3
|
-
*
|
|
4
|
-
* reported, not only the first, so one
|
|
2
|
+
* Resolves an Environment's recipe (`PUT /v1/environments/{id}`) against its Tenant's limits,
|
|
3
|
+
* and the sandbox spec a recipe gives the session's sandbox capability and the pod. Pure: the
|
|
4
|
+
* caller supplies the configuration. Every problem is reported, not only the first, so one
|
|
5
|
+
* response lists all of them.
|
|
5
6
|
*/
|
|
6
|
-
import type {
|
|
7
|
-
import type
|
|
7
|
+
import type { EnvironmentFile, EnvironmentKind, EnvironmentLifecycle, EnvironmentRecipe, EnvironmentSetupCommand } from "@nylorun/core/contracts";
|
|
8
|
+
import { type SandboxManifest } from "@nylorun/core/define";
|
|
9
|
+
import type { PodSandboxSpec } from "./pods/spec.js";
|
|
8
10
|
import { type EffectiveSandboxConfig } from "./tenant-config.js";
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
readonly
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
/** What a `PUT` asks of the recipe: the request's fields, over the existing recipe's. */
|
|
12
|
+
export interface RecipeRequest {
|
|
13
|
+
readonly image?: string;
|
|
14
|
+
readonly resources?: {
|
|
15
|
+
readonly cpus?: number;
|
|
16
|
+
readonly memory?: string;
|
|
17
|
+
};
|
|
18
|
+
readonly storage?: string;
|
|
19
|
+
readonly network?: {
|
|
20
|
+
readonly allowedHosts?: readonly string[];
|
|
21
|
+
};
|
|
22
|
+
readonly harness?: string;
|
|
23
|
+
readonly lifecycle?: EnvironmentLifecycle;
|
|
24
|
+
readonly setup?: readonly EnvironmentSetupCommand[];
|
|
25
|
+
readonly files?: readonly EnvironmentFile[];
|
|
26
|
+
}
|
|
27
|
+
export type RecipeResolution = {
|
|
28
|
+
readonly kind: "recipe";
|
|
29
|
+
readonly recipe: EnvironmentRecipe;
|
|
16
30
|
} | {
|
|
17
31
|
readonly kind: "error";
|
|
18
|
-
readonly status: 400 | 403;
|
|
19
32
|
readonly errors: readonly string[];
|
|
20
33
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*/
|
|
31
|
-
readonly kind?: "virtual" | "pod";
|
|
32
|
-
}
|
|
33
|
-
export declare const SUBJECT_INLINE_MESSAGE = "Defining a sandbox needs an application key. Use the Tenant default, or open the session from the application backend.";
|
|
34
|
-
export declare function resolveSandbox(input: ResolveSandboxInput): SandboxResolution;
|
|
34
|
+
/** The recipe `request` asks for an Environment of `kind`, or every reason it cannot have it. */
|
|
35
|
+
export declare function resolveRecipe(kind: EnvironmentKind, request: RecipeRequest, config: EffectiveSandboxConfig): RecipeResolution;
|
|
36
|
+
/**
|
|
37
|
+
* The sandbox spec of a recipe: what the session's sandbox capability pins and the virtual
|
|
38
|
+
* workspace runs. Idle is the Environment's `lifecycle.idle`, else the Tenant's.
|
|
39
|
+
*/
|
|
40
|
+
export declare function sandboxSpecOf(recipe: EnvironmentRecipe, tenantIdle: string): SandboxManifest;
|
|
41
|
+
/** The pod's spec of a recipe: its sandbox spec and its volume's size. */
|
|
42
|
+
export declare function podSandboxSpecOf(recipe: EnvironmentRecipe, tenantIdle: string): PodSandboxSpec;
|
|
35
43
|
/** Whether `host` (a name or `*.suffix` pattern) is inside one of the ceiling's patterns. */
|
|
36
44
|
export declare function withinCeiling(host: string, ceiling: readonly string[]): boolean;
|
|
37
45
|
/** Problems with a Tenant configuration, checked when an operator saves it. */
|
package/dist/sandbox/resolve.js
CHANGED
|
@@ -1,50 +1,84 @@
|
|
|
1
|
+
import { DEFAULT_HARNESS } from "@nylorun/core/contracts";
|
|
2
|
+
import { parseSandboxDuration } from "@nylorun/core/define";
|
|
1
3
|
import { memoryMiB } from "./tenant-config.js";
|
|
2
|
-
|
|
3
|
-
export function
|
|
4
|
-
const {
|
|
5
|
-
if (request === false)
|
|
6
|
-
return { kind: "none" };
|
|
7
|
-
if (request === undefined) {
|
|
8
|
-
const fallback = config.default;
|
|
9
|
-
if (fallback === "none")
|
|
10
|
-
return { kind: "none" };
|
|
11
|
-
return build(fallback === "virtual" ? {} : fallback, "default", input);
|
|
12
|
-
}
|
|
13
|
-
if (input.actingForSubject)
|
|
14
|
-
return { kind: "error", status: 403, errors: [SUBJECT_INLINE_MESSAGE] };
|
|
15
|
-
return build(request, "inline", input);
|
|
16
|
-
}
|
|
17
|
-
function build(request, source, input) {
|
|
18
|
-
const { limits } = input.config;
|
|
4
|
+
/** The recipe `request` asks for an Environment of `kind`, or every reason it cannot have it. */
|
|
5
|
+
export function resolveRecipe(kind, request, config) {
|
|
6
|
+
const { limits } = config;
|
|
19
7
|
const errors = [];
|
|
20
|
-
const pod =
|
|
8
|
+
const pod = kind === "pod";
|
|
9
|
+
// Any harness id (R4.1): placement decides where it may run, when a session opens.
|
|
10
|
+
const harness = request.harness ?? DEFAULT_HARNESS;
|
|
11
|
+
if (pod && harness !== DEFAULT_HARNESS && request.image === undefined)
|
|
12
|
+
errors.push(`harness ${harness} needs image: a pod Environment runs its image as the harness.`);
|
|
13
|
+
const lifecycle = request.lifecycle ?? {};
|
|
14
|
+
const idle = lifecycle.idle === undefined ? undefined : parseSandboxDuration(lifecycle.idle);
|
|
15
|
+
if (idle !== undefined && idle > (parseSandboxDuration(limits.idle) ?? Infinity))
|
|
16
|
+
errors.push(`lifecycle.idle is ${lifecycle.idle}; this Tenant allows at most ${limits.idle}.`);
|
|
17
|
+
if (kind === "none") {
|
|
18
|
+
for (const field of ["image", "resources", "storage", "network", "setup", "files"])
|
|
19
|
+
if (request[field] !== undefined)
|
|
20
|
+
errors.push(`${field} is for an Environment with a sandbox: kind none has none.`);
|
|
21
|
+
if (errors.length > 0)
|
|
22
|
+
return { kind: "error", errors };
|
|
23
|
+
return { kind: "recipe", recipe: { harness, lifecycle: { ...lifecycle } } };
|
|
24
|
+
}
|
|
21
25
|
if (request.image !== undefined && !pod)
|
|
22
|
-
errors.push("
|
|
23
|
-
|
|
26
|
+
errors.push("image is for kind pod: a virtual Environment runs no images.");
|
|
27
|
+
if (request.storage !== undefined && !pod)
|
|
28
|
+
errors.push("storage is for kind pod: a virtual Environment has no volume.");
|
|
29
|
+
for (const field of ["setup", "files"])
|
|
30
|
+
if (request[field] !== undefined && request[field].length > 0 && !pod)
|
|
31
|
+
errors.push(`${field} is for kind pod: it runs when a pod's disk is made or reset.`);
|
|
32
|
+
const allow = (request.network?.allowedHosts ?? []).map((host) => host.toLowerCase());
|
|
24
33
|
for (const host of allow) {
|
|
25
34
|
if (!withinCeiling(host, limits.network))
|
|
26
|
-
errors.push(`
|
|
35
|
+
errors.push(`network.allowedHosts includes ${host}, which this Tenant does not allow.`);
|
|
27
36
|
else if (host.startsWith("*.") && !pod)
|
|
28
|
-
errors.push(`
|
|
37
|
+
errors.push(`network.allowedHosts includes ${host}; a virtual Environment allows exact host names only, such as ${host.slice(2)}.`);
|
|
29
38
|
}
|
|
30
39
|
const cpus = request.resources?.cpus ?? limits.defaultResources.cpus;
|
|
31
40
|
const memory = request.resources?.memory === undefined
|
|
32
41
|
? limits.defaultResources.memoryMiB
|
|
33
42
|
: memoryMiB(request.resources.memory);
|
|
34
43
|
if (cpus > limits.resources.cpus)
|
|
35
|
-
errors.push(`
|
|
44
|
+
errors.push(`resources.cpus asks for ${cpus}; this Tenant allows at most ${limits.resources.cpus}.`);
|
|
36
45
|
if (memory > limits.resources.memoryMiB)
|
|
37
|
-
errors.push(`
|
|
46
|
+
errors.push(`resources.memory asks for ${memory}MiB; this Tenant allows at most ${limits.resources.memoryMiB}MiB.`);
|
|
38
47
|
if (errors.length > 0)
|
|
39
|
-
return { kind: "error",
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
return { kind: "error", errors };
|
|
49
|
+
return {
|
|
50
|
+
kind: "recipe",
|
|
51
|
+
recipe: {
|
|
52
|
+
...(request.image === undefined ? {} : { image: request.image }),
|
|
53
|
+
resources: { cpus, memory: `${memory}MiB` },
|
|
54
|
+
...(request.storage === undefined ? {} : { storage: request.storage }),
|
|
55
|
+
network: { allowedHosts: [...new Set(allow)].sort() },
|
|
56
|
+
harness,
|
|
57
|
+
lifecycle: { ...lifecycle },
|
|
58
|
+
...(pod && request.setup?.length ? { setup: request.setup.map((command) => ({ ...command })) } : {}),
|
|
59
|
+
...(pod && request.files?.length ? { files: request.files.map((file) => ({ ...file })) } : {}),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The sandbox spec of a recipe: what the session's sandbox capability pins and the virtual
|
|
65
|
+
* workspace runs. Idle is the Environment's `lifecycle.idle`, else the Tenant's.
|
|
66
|
+
*/
|
|
67
|
+
export function sandboxSpecOf(recipe, tenantIdle) {
|
|
68
|
+
const allow = recipe.network?.allowedHosts ?? [];
|
|
69
|
+
return {
|
|
70
|
+
...(recipe.image === undefined ? {} : { image: recipe.image }),
|
|
71
|
+
network: { preset: "none", ...(allow.length > 0 ? { allow: [...allow] } : {}) },
|
|
72
|
+
...(recipe.resources === undefined ? {} : { resources: { ...recipe.resources } }),
|
|
73
|
+
idle: recipe.lifecycle.idle ?? tenantIdle,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** The pod's spec of a recipe: its sandbox spec and its volume's size. */
|
|
77
|
+
export function podSandboxSpecOf(recipe, tenantIdle) {
|
|
78
|
+
return {
|
|
79
|
+
...sandboxSpecOf(recipe, tenantIdle),
|
|
80
|
+
...(recipe.storage === undefined ? {} : { storage: recipe.storage }),
|
|
46
81
|
};
|
|
47
|
-
return { kind: "sandbox", spec, source };
|
|
48
82
|
}
|
|
49
83
|
/** Whether `host` (a name or `*.suffix` pattern) is inside one of the ceiling's patterns. */
|
|
50
84
|
export function withinCeiling(host, ceiling) {
|
|
@@ -69,14 +103,5 @@ export function sandboxConfigErrors(config) {
|
|
|
69
103
|
errors.push("limits.defaultResources.cpus is above limits.resources.cpus.");
|
|
70
104
|
if (limits.defaultResources.memoryMiB > limits.resources.memoryMiB)
|
|
71
105
|
errors.push("limits.defaultResources.memory is above limits.resources.memory.");
|
|
72
|
-
if (config.default !== "none" && config.default !== "virtual") {
|
|
73
|
-
const resolved = resolveSandbox({
|
|
74
|
-
request: undefined,
|
|
75
|
-
config,
|
|
76
|
-
actingForSubject: false,
|
|
77
|
-
});
|
|
78
|
-
if (resolved.kind === "error")
|
|
79
|
-
errors.push(...resolved.errors.map((message) => `default: ${message}`));
|
|
80
|
-
}
|
|
81
106
|
return errors;
|
|
82
107
|
}
|
|
@@ -102,7 +102,7 @@ function checked(original, sandboxed) {
|
|
|
102
102
|
return {
|
|
103
103
|
ok: false,
|
|
104
104
|
message: clash
|
|
105
|
-
? `'${original.id}' declares a tool named like a sandbox tool (bash, read, write, edit, grep, glob, save_artifact). Rename it, or open the session
|
|
105
|
+
? `'${original.id}' declares a tool named like a sandbox tool (bash, read, write, edit, grep, glob, save_artifact). Rename it, or open the session on no Environment or one of kind none.`
|
|
106
106
|
: `'${original.id}' cannot take this sandbox: ${[...new Set(issues)].join("; ")}`,
|
|
107
107
|
};
|
|
108
108
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Environment, Tx } from "../store/types.js";
|
|
2
|
+
import type { TenantContext } from "../tenant/context.js";
|
|
3
|
+
import { SETUP_FAILED } from "./pods/lifecycle.js";
|
|
4
|
+
export { SETUP_FAILED };
|
|
5
|
+
/** What the engine runs on a new disk. */
|
|
6
|
+
export interface SetupPlan {
|
|
7
|
+
/** The applied recipe's version: written into the volume's marker. */
|
|
8
|
+
readonly recipeVersion: number;
|
|
9
|
+
readonly commands: readonly {
|
|
10
|
+
readonly command: string;
|
|
11
|
+
readonly cwd: string;
|
|
12
|
+
readonly timeoutMs: number;
|
|
13
|
+
}[];
|
|
14
|
+
/** Each input file by its index into the recipe's `files`, and where it goes. */
|
|
15
|
+
readonly files: readonly {
|
|
16
|
+
readonly index: number;
|
|
17
|
+
readonly path: string;
|
|
18
|
+
}[];
|
|
19
|
+
/** Variables every command gets: secrets as the sentinel, plain values (R2c). */
|
|
20
|
+
readonly environment: Readonly<Record<string, string>>;
|
|
21
|
+
}
|
|
22
|
+
/** The setup of `recipe`, or undefined when it has neither commands nor files. */
|
|
23
|
+
export declare function setupPlanOf(ctx: TenantContext, t: Tx, environment: Environment): Promise<SetupPlan | undefined>;
|
|
24
|
+
/** What the engine reports after running a setup. */
|
|
25
|
+
export interface SetupReport {
|
|
26
|
+
readonly status: "ok" | "failed";
|
|
27
|
+
/** The failed command's index into the plan's commands; absent for an input file. */
|
|
28
|
+
readonly index?: number;
|
|
29
|
+
readonly exitCode?: number;
|
|
30
|
+
/** The end of the failed command's output, or why an input file could not be copied. */
|
|
31
|
+
readonly output?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function parseSetupReport(value: unknown): SetupReport | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Records a failed setup on Environment `id` held at host epoch `epoch` (the reporting engine's):
|
|
36
|
+
* the pod is `failed` with a reason that starts `setup:`, and `environment.failed` says which
|
|
37
|
+
* command failed and how. A report from an older host, or a success, changes nothing.
|
|
38
|
+
*/
|
|
39
|
+
export declare function recordSetup(ctx: TenantContext, id: string, epoch: number, report: SetupReport): Promise<void>;
|
|
40
|
+
/** Whether the engine of `id` should run its setup again: the pod is no longer `failed`. */
|
|
41
|
+
export declare function setupState(ctx: TenantContext, id: string): Promise<{
|
|
42
|
+
retry: boolean;
|
|
43
|
+
setup?: SetupPlan;
|
|
44
|
+
}>;
|
|
45
|
+
/** One input file's bytes, or a folder's listing (`path` absent). */
|
|
46
|
+
export type SetupFile = {
|
|
47
|
+
readonly kind: "file";
|
|
48
|
+
readonly contentType: string;
|
|
49
|
+
readonly body: ReadableStream<Uint8Array>;
|
|
50
|
+
} | {
|
|
51
|
+
readonly kind: "folder";
|
|
52
|
+
readonly entries: readonly {
|
|
53
|
+
readonly path: string;
|
|
54
|
+
readonly size: number;
|
|
55
|
+
}[];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Input file `index` of Environment `id`'s applied recipe: a file artifact's bytes, a folder's
|
|
59
|
+
* listing, or (with `path`) one of the folder's files. Undefined when there is no such file.
|
|
60
|
+
*/
|
|
61
|
+
export declare function setupFile(ctx: TenantContext, id: string, index: number, path: string | undefined): Promise<SetupFile | undefined>;
|