@inflexa-ai/harness 0.42.2 → 0.43.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/agents/sandbox/shared.d.ts +1 -6
- package/dist/agents/sandbox/shared.js +0 -15
- package/dist/app/chat-turn.js +13 -1
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +9 -1
- package/dist/contracts/chat-frame.d.ts +3 -3
- package/dist/contracts/chat-frame.js +8 -3
- package/dist/contracts/chat-parts.d.ts +4 -1
- package/dist/contracts/data-profile.d.ts +4 -6
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/report-blocks.d.ts +1364 -75
- package/dist/contracts/report-blocks.js +221 -29
- package/dist/contracts/report-reference.js +1 -1
- package/dist/contracts/schemas/chat-events.d.ts +13 -0
- package/dist/contracts/schemas/chat-events.js +5 -0
- package/dist/contracts/schemas/chat-parts.d.ts +2 -2
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/execution/input-signature.d.ts +7 -13
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/run-canceler.d.ts +3 -2
- package/dist/execution/run-canceler.js +3 -2
- package/dist/execution/run-event-stream.d.ts +16 -2
- package/dist/execution/run-event-stream.js +54 -3
- package/dist/index.d.ts +1 -8
- package/dist/index.js +5 -16
- package/dist/input-scan/enrich.d.ts +0 -2
- package/dist/input-scan/enrich.js +0 -1
- package/dist/lib/metrics.js +6 -7
- package/dist/lib/otel-spans.d.ts +4 -9
- package/dist/lib/otel-spans.js +3 -8
- package/dist/loop/run-agent.d.ts +4 -2
- package/dist/loop/run-agent.js +38 -23
- package/dist/memory/thread-history.d.ts +1 -1
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +17 -0
- package/dist/provenance/exec-frame.d.ts +1 -1
- package/dist/report-model/authoring-grammar.d.ts +416 -17
- package/dist/report-model/authoring-grammar.js +25 -4
- package/dist/report-model/block-walk.d.ts +20 -4
- package/dist/report-model/block-walk.js +64 -7
- package/dist/report-model/draft-finish.d.ts +6 -2
- package/dist/report-model/draft-finish.js +6 -1
- package/dist/report-model/production-resolver.d.ts +8 -0
- package/dist/report-model/production-resolver.js +1 -1
- package/dist/report-model/validate.d.ts +20 -10
- package/dist/report-model/validate.js +29 -13
- package/dist/report-render/chart-export.d.ts +52 -0
- package/dist/report-render/chart-export.js +192 -0
- package/dist/report-render/chart-presets.d.ts +46 -47
- package/dist/report-render/chart-presets.js +31 -101
- package/dist/report-render/chart-renderers.d.ts +163 -0
- package/dist/report-render/chart-renderers.js +1055 -0
- package/dist/report-render/chart-toolbox.d.ts +124 -0
- package/dist/report-render/chart-toolbox.js +595 -0
- package/dist/report-render/chart.d.ts +60 -27
- package/dist/report-render/chart.js +2323 -524
- package/dist/report-render/design.d.ts +476 -16
- package/dist/report-render/design.js +390 -56
- package/dist/report-render/figures/common.d.ts +321 -0
- package/dist/report-render/figures/common.js +759 -0
- package/dist/report-render/figures/dendrogram.d.ts +36 -0
- package/dist/report-render/figures/dendrogram.js +130 -0
- package/dist/report-render/figures/dense.d.ts +111 -0
- package/dist/report-render/figures/dense.js +204 -0
- package/dist/report-render/figures/dotplot.d.ts +28 -0
- package/dist/report-render/figures/dotplot.js +234 -0
- package/dist/report-render/figures/embedding.d.ts +43 -0
- package/dist/report-render/figures/embedding.js +360 -0
- package/dist/report-render/figures/equal-units.d.ts +101 -0
- package/dist/report-render/figures/equal-units.js +215 -0
- package/dist/report-render/figures/forest.d.ts +20 -0
- package/dist/report-render/figures/forest.js +398 -0
- package/dist/report-render/figures/gsea.d.ts +30 -0
- package/dist/report-render/figures/gsea.js +391 -0
- package/dist/report-render/figures/heatmap.d.ts +40 -0
- package/dist/report-render/figures/heatmap.js +395 -0
- package/dist/report-render/figures/index.d.ts +135 -0
- package/dist/report-render/figures/index.js +100 -0
- package/dist/report-render/figures/km.d.ts +16 -0
- package/dist/report-render/figures/km.js +406 -0
- package/dist/report-render/figures/label-room.d.ts +130 -0
- package/dist/report-render/figures/label-room.js +341 -0
- package/dist/report-render/figures/locuszoom.d.ts +50 -0
- package/dist/report-render/figures/locuszoom.js +675 -0
- package/dist/report-render/figures/lollipop.d.ts +16 -0
- package/dist/report-render/figures/lollipop.js +247 -0
- package/dist/report-render/figures/ma.d.ts +20 -0
- package/dist/report-render/figures/ma.js +114 -0
- package/dist/report-render/figures/manhattan.d.ts +60 -0
- package/dist/report-render/figures/manhattan.js +343 -0
- package/dist/report-render/figures/oncoprint.d.ts +41 -0
- package/dist/report-render/figures/oncoprint.js +420 -0
- package/dist/report-render/figures/pca.d.ts +37 -0
- package/dist/report-render/figures/pca.js +241 -0
- package/dist/report-render/figures/qq.d.ts +30 -0
- package/dist/report-render/figures/qq.js +182 -0
- package/dist/report-render/figures/roc.d.ts +17 -0
- package/dist/report-render/figures/roc.js +116 -0
- package/dist/report-render/figures/sankey.d.ts +48 -0
- package/dist/report-render/figures/sankey.js +353 -0
- package/dist/report-render/figures/upset.d.ts +60 -0
- package/dist/report-render/figures/upset.js +316 -0
- package/dist/report-render/figures/volcano.d.ts +25 -0
- package/dist/report-render/figures/volcano.js +166 -0
- package/dist/report-render/hybrid-svg.d.ts +75 -0
- package/dist/report-render/hybrid-svg.js +504 -0
- package/dist/report-render/number-format.d.ts +19 -0
- package/dist/report-render/number-format.js +25 -1
- package/dist/report-render/page.d.ts +41 -4
- package/dist/report-render/page.js +247 -12
- package/dist/report-render/render.d.ts +13 -4
- package/dist/report-render/render.js +37 -6
- package/dist/report-render/types.d.ts +23 -2
- package/dist/report-render/value-bridge.d.ts +38 -3
- package/dist/report-render/value-bridge.js +137 -6
- package/dist/report-render/views/chart-view.d.ts +42 -6
- package/dist/report-render/views/chart-view.js +70 -9
- package/dist/report-render/views/page-view.js +9 -10
- package/dist/runtime/assemble.js +2 -2
- package/dist/sandbox/client.d.ts +23 -32
- package/dist/sandbox/client.js +3 -11
- package/dist/sandbox/create-sandbox.d.ts +9 -24
- package/dist/sandbox/create-sandbox.js +15 -30
- package/dist/sandbox/docker-client.d.ts +12 -22
- package/dist/sandbox/docker-client.js +29 -43
- package/dist/sandbox/exec.d.ts +58 -0
- package/dist/sandbox/exec.js +197 -0
- package/dist/sandbox/identity.d.ts +5 -5
- package/dist/sandbox/identity.js +7 -10
- package/dist/sandbox/k8s-client.d.ts +4 -10
- package/dist/sandbox/k8s-client.js +5 -12
- package/dist/sandbox/liveness.d.ts +5 -12
- package/dist/sandbox/liveness.js +5 -12
- package/dist/sandbox/notification-sweep.d.ts +4 -7
- package/dist/sandbox/notification-sweep.js +4 -7
- package/dist/sandbox/reaper.d.ts +3 -3
- package/dist/sandbox/reaper.js +3 -3
- package/dist/sandbox/sandbox-error.d.ts +10 -11
- package/dist/sandbox/sandbox-error.js +10 -11
- package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
- package/dist/sandbox/sandbox-step-translate.js +1 -1
- package/dist/sandbox/types.d.ts +31 -99
- package/dist/sandbox/types.js +17 -84
- package/dist/state/active-sandboxes.d.ts +7 -32
- package/dist/state/active-sandboxes.js +11 -46
- package/dist/state/index.d.ts +1 -2
- package/dist/state/index.js +1 -1
- package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.d.ts +7 -0
- package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.js +12 -0
- package/dist/state/migrations/index.js +2 -0
- package/dist/state/schema.d.ts +1 -5
- package/dist/state/schema.js +1 -6
- package/dist/state/step-executions.js +1 -2
- package/dist/tasks/data-profile.js +7 -20
- package/dist/tasks/derive-table-exec.d.ts +4 -14
- package/dist/tasks/derive-table-exec.js +9 -24
- package/dist/tasks/extract-values.d.ts +2 -2
- package/dist/tasks/extract-values.js +4 -7
- package/dist/tools/define-tool.d.ts +56 -13
- package/dist/tools/define-tool.js +28 -1
- package/dist/tools/execute-analysis.js +3 -3
- package/dist/tools/report-session/derive-table.d.ts +8 -9
- package/dist/tools/report-session/derive-table.js +3 -4
- package/dist/tools/report-session/preview-report.js +4 -49
- package/dist/tools/sandbox/list-available-packages.d.ts +1 -1
- package/dist/tools/sandbox/list-available-packages.js +5 -4
- package/dist/tools/workspace/edit-file.d.ts +5 -4
- package/dist/tools/workspace/edit-file.js +10 -13
- package/dist/tools/workspace/execute-command.d.ts +27 -42
- package/dist/tools/workspace/execute-command.js +42 -38
- package/dist/tools/workspace/index.d.ts +5 -6
- package/dist/tools/workspace/index.js +5 -6
- package/dist/tools/workspace/mutator.d.ts +43 -8
- package/dist/tools/workspace/mutator.js +45 -34
- package/dist/tools/workspace/run-exec.d.ts +3 -12
- package/dist/tools/workspace/run-exec.js +7 -24
- package/dist/tools/workspace/scan-inputs.d.ts +0 -3
- package/dist/tools/workspace/scan-inputs.js +0 -1
- package/dist/tools/workspace/write-file.d.ts +12 -3
- package/dist/tools/workspace/write-file.js +17 -13
- package/dist/workflows/sandbox-step.d.ts +7 -11
- package/dist/workflows/sandbox-step.js +24 -27
- package/package.json +5 -3
- package/dist/sandbox/await-exec.d.ts +0 -143
- package/dist/sandbox/await-exec.js +0 -417
- package/dist/sandbox/deliver-exec-event.d.ts +0 -25
- package/dist/sandbox/deliver-exec-event.js +0 -29
- package/dist/sandbox/digest.d.ts +0 -14
- package/dist/sandbox/digest.js +0 -17
- package/dist/sandbox/exec-id.d.ts +0 -12
- package/dist/sandbox/exec-id.js +0 -20
- package/dist/sandbox/hmac.d.ts +0 -62
- package/dist/sandbox/hmac.js +0 -72
- package/dist/sandbox/submit-exec.d.ts +0 -38
- package/dist/sandbox/submit-exec.js +0 -80
- package/dist/sandbox/watchdog.d.ts +0 -71
- package/dist/sandbox/watchdog.js +0 -161
|
@@ -8,38 +8,28 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Registry write/clear callbacks are wired here so the per-backend
|
|
10
10
|
* `createSandbox` / `teardown` implementations stay free of state-layer
|
|
11
|
-
* coupling (they just call the closures we hand them). `
|
|
12
|
-
*
|
|
11
|
+
* coupling (they just call the closures we hand them). `exec` is
|
|
12
|
+
* backend-agnostic — it only needs the SandboxRef.
|
|
13
13
|
*/
|
|
14
14
|
import { chmod, mkdir } from "node:fs/promises";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
+
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
16
17
|
import { ResultAsync, err, okAsync } from "neverthrow";
|
|
17
18
|
import { passGate } from "../lib/hooks.js";
|
|
18
19
|
import { recordSandboxExec } from "../lib/metrics.js";
|
|
20
|
+
import { ATTR_INFLEXA_EXEC_ID, stableSpan } from "../lib/otel-spans.js";
|
|
19
21
|
import { clampResources } from "../config/resource-limits.js";
|
|
20
22
|
import { tailWritePrefix } from "../workspace/paths.js";
|
|
21
23
|
import { tryMutation } from "../lib/db-result.js";
|
|
22
24
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
23
25
|
import { clearSandboxRef, setSandboxRef } from "../state/index.js";
|
|
24
26
|
import { capExecStreams, EXEC_STREAM_BYTE_CAP } from "../tools/workspace/result-bounds.js";
|
|
25
|
-
import { awaitExec } from "./await-exec.js";
|
|
26
27
|
import { createDockerSandboxOps } from "./docker-client.js";
|
|
28
|
+
import { runExec, stepExecId } from "./exec.js";
|
|
27
29
|
import { noteExecOutcome, sandboxExecOutcomeOf, summarizeExec } from "./exec-outcome.js";
|
|
28
30
|
import { createK8sSandboxOps } from "./k8s-client.js";
|
|
29
31
|
import { mountCoordsOf, sandboxWriteTail } from "./mount-plan.js";
|
|
30
32
|
import { SandboxFailure } from "./sandbox-error.js";
|
|
31
|
-
import { submitExec } from "./submit-exec.js";
|
|
32
|
-
import { toPersistedRef, } from "./types.js";
|
|
33
|
-
/**
|
|
34
|
-
* Assemble `awaitExec`'s options: the liveness probe self-wires from the
|
|
35
|
-
* backend ops (the poll loop's escalation always has its arbiter under the
|
|
36
|
-
* client), explicit seam injections in `base` win over the self-wired probe,
|
|
37
|
-
* and the transport is client-owned — never overridable through the seam bag.
|
|
38
|
-
* Exported for tests.
|
|
39
|
-
*/
|
|
40
|
-
export function composeAwaitOptions(base, transport, isAlive) {
|
|
41
|
-
return { isAlive, ...base, transport };
|
|
42
|
-
}
|
|
43
33
|
/**
|
|
44
34
|
* Backend-agnostic pre-creation of the writable tree under the analysis's
|
|
45
35
|
* resolved workspace root. On Docker that root is the host dir the container
|
|
@@ -77,7 +67,6 @@ export async function precreateStepTree(deps, coords) {
|
|
|
77
67
|
}
|
|
78
68
|
export function createSandboxClient(config) {
|
|
79
69
|
const backend = config.backend ?? config.env.backend;
|
|
80
|
-
const transport = config.transport ?? "poll";
|
|
81
70
|
// Normalized once, here, and handed to the backends and to the client
|
|
82
71
|
// alike: the mount plan and the orient-core prompt then key on one value,
|
|
83
72
|
// and an absent declaration means "store" on both sides.
|
|
@@ -112,13 +101,11 @@ export function createSandboxClient(config) {
|
|
|
112
101
|
*/
|
|
113
102
|
const failing = (e) => new SandboxFailure(e);
|
|
114
103
|
const registerSandbox = async (session, spec, ref) => {
|
|
115
|
-
unwrapOrThrow(await setSandboxRef(config.pool, session.runFrame.runId, session.runFrame.stepId,
|
|
104
|
+
unwrapOrThrow(await setSandboxRef(config.pool, session.runFrame.runId, session.runFrame.stepId, ref));
|
|
116
105
|
};
|
|
117
106
|
const ops = backend === "k8s"
|
|
118
107
|
? createK8sSandboxOps({
|
|
119
108
|
image: config.image,
|
|
120
|
-
cortexBaseUrl: config.cortexBaseUrl,
|
|
121
|
-
transport,
|
|
122
109
|
namespace: config.namespace ?? config.env.namespace,
|
|
123
110
|
sessionPvc: config.sessionPvc,
|
|
124
111
|
sessionPvcRoot: config.sessionPvcRoot,
|
|
@@ -137,8 +124,6 @@ export function createSandboxClient(config) {
|
|
|
137
124
|
})
|
|
138
125
|
: createDockerSandboxOps({
|
|
139
126
|
image: config.image,
|
|
140
|
-
cortexBaseUrl: config.cortexBaseUrl,
|
|
141
|
-
transport,
|
|
142
127
|
resolveWorkspaceRoot: config.resolveWorkspaceRoot,
|
|
143
128
|
libStorePath: config.libStorePath,
|
|
144
129
|
farmSource: config.farmSource,
|
|
@@ -164,7 +149,7 @@ export function createSandboxClient(config) {
|
|
|
164
149
|
unwrapOrThrow(await tryMutation("createSandbox.teardownClearSandboxRef", async () => {
|
|
165
150
|
await config.pool.query({
|
|
166
151
|
text: `UPDATE cortex_step_executions
|
|
167
|
-
SET sandbox_ref = NULL
|
|
152
|
+
SET sandbox_ref = NULL
|
|
168
153
|
WHERE sandbox_ref->>'sandboxId' = $1`,
|
|
169
154
|
values: [ref.sandboxId],
|
|
170
155
|
});
|
|
@@ -173,6 +158,8 @@ export function createSandboxClient(config) {
|
|
|
173
158
|
// One arbiter for both surfaces: the client's `isAlive` method and the
|
|
174
159
|
// poll loop's escalation probe are the same backend inspect.
|
|
175
160
|
const isAlive = async (ref) => unwrapOrThrow((await ops.isAlive(ref)).mapErr(failing));
|
|
161
|
+
const streamCap = config.execStreamByteCap ?? EXEC_STREAM_BYTE_CAP;
|
|
162
|
+
const execDeps = { isAlive, ...config.execDeps, execStreamByteCap: streamCap };
|
|
176
163
|
const hostLabelsFor = (session) => {
|
|
177
164
|
const hook = config.resolveSandboxLabels;
|
|
178
165
|
if (hook === undefined)
|
|
@@ -217,10 +204,6 @@ export function createSandboxClient(config) {
|
|
|
217
204
|
return {
|
|
218
205
|
toolchainSource,
|
|
219
206
|
createSandbox: (session, spec, identity) => new ResultAsync(createSandbox(session, spec, identity)),
|
|
220
|
-
submitExec: async (ref, body) => submitExec(ref, body, {
|
|
221
|
-
...config.submitDeps,
|
|
222
|
-
execStreamByteCap: config.submitDeps?.execStreamByteCap ?? config.execStreamByteCap,
|
|
223
|
-
}),
|
|
224
207
|
// The one place an ExecResult crosses from the wire into the process, so
|
|
225
208
|
// the cap lands here rather than at each consumer. Every downstream use —
|
|
226
209
|
// tool results, workflow return values, durable step outputs — is bounded
|
|
@@ -232,11 +215,13 @@ export function createSandboxClient(config) {
|
|
|
232
215
|
// and it sees the outcome the caller then folds into a tool result, so
|
|
233
216
|
// one record here counts what a consumer would each have to count for
|
|
234
217
|
// itself. `recordSandboxExec` is idempotent over the exec id, because a
|
|
235
|
-
//
|
|
218
|
+
// recovered step reaches this line again. `capExecStreams` cuts the
|
|
236
219
|
// streams and keeps every other field, thus the kernel accounting the
|
|
237
220
|
// sandbox reported reaches the record intact.
|
|
238
|
-
|
|
239
|
-
const
|
|
221
|
+
exec: (ref, request, emit, deadline) => DBOS.runStep(async () => {
|
|
222
|
+
const execId = stepExecId();
|
|
223
|
+
stableSpan("sandbox.exec", "sandbox.exec", { [ATTR_INFLEXA_EXEC_ID]: execId });
|
|
224
|
+
const result = capExecStreams(await runExec(ref, execId, request, emit, deadline, execDeps), streamCap);
|
|
240
225
|
recordSandboxExec({
|
|
241
226
|
execId,
|
|
242
227
|
outcome: sandboxExecOutcomeOf(result),
|
|
@@ -246,7 +231,7 @@ export function createSandboxClient(config) {
|
|
|
246
231
|
});
|
|
247
232
|
noteExecOutcome(ref.sandboxId, summarizeExec(result));
|
|
248
233
|
return result;
|
|
249
|
-
},
|
|
234
|
+
}, { name: "sandbox.exec" }),
|
|
250
235
|
isAlive,
|
|
251
236
|
isAliveById: async (sandboxId) => unwrapOrThrow((await ops.isAliveById(sandboxId)).mapErr(failing)),
|
|
252
237
|
teardown,
|
|
@@ -17,26 +17,20 @@
|
|
|
17
17
|
* that the sandbox sees. `teardown` removes the directory when this process
|
|
18
18
|
* wrote it. A directory from an earlier process stays.
|
|
19
19
|
*
|
|
20
|
-
* ##
|
|
20
|
+
* ## Confinement
|
|
21
21
|
*
|
|
22
22
|
* The container joins the default bridge and publishes sandbox-server's port to
|
|
23
|
-
* `127.0.0.1` only, so the host can reach `/exec` but the LAN cannot.
|
|
24
|
-
* sandbox
|
|
23
|
+
* `127.0.0.1` only, so the host can reach `/exec` but the LAN cannot. The
|
|
24
|
+
* sandbox initiates nothing, so it gets no egress. The container is created as
|
|
25
|
+
* root with `CAP_NET_ADMIN` and the `SANDBOX_EGRESS_FIREWALL` flag; the image's
|
|
26
|
+
* root entrypoint installs `iptables -P OUTPUT DROP` (allowing loopback and
|
|
27
|
+
* established return traffic) and then `setpriv`-drops to the uid-1000
|
|
28
|
+
* workload, which can neither reach the network nor flush the rules. The host
|
|
29
|
+
* polls `GET /exec/{execId}?since={cursor}` over the published port; the reply
|
|
30
|
+
* rides the established connection, so polling works with egress hard-blocked.
|
|
25
31
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `SANDBOX_EGRESS_FIREWALL` flag; the image's root entrypoint installs
|
|
29
|
-
* `iptables -P OUTPUT DROP` (allowing loopback and established return
|
|
30
|
-
* traffic) and then `setpriv`-drops to the uid-1000 workload, which can
|
|
31
|
-
* neither reach the network nor flush the rules. The host polls
|
|
32
|
-
* `GET /exec/{execId}?since={cursor}` over the published port; the reply
|
|
33
|
-
* rides the established connection, so polling works with egress hard-blocked.
|
|
34
|
-
* - **callback**: the sandbox is *allowed* egress and POSTs signed callbacks to
|
|
35
|
-
* `CORTEX_BASE_URL`. It runs as uid 1000 throughout with no `NET_ADMIN` and
|
|
36
|
-
* no firewall.
|
|
37
|
-
*
|
|
38
|
-
* Either way the workload ends as uid 1000 with `no-new-privileges` and no
|
|
39
|
-
* effective capabilities.
|
|
32
|
+
* The workload ends as uid 1000 with `no-new-privileges` and no effective
|
|
33
|
+
* capabilities.
|
|
40
34
|
*/
|
|
41
35
|
import Docker from "dockerode";
|
|
42
36
|
import { ResultAsync } from "neverthrow";
|
|
@@ -44,13 +38,9 @@ import type { SpawnSession } from "../auth/types.js";
|
|
|
44
38
|
import type { Logger } from "../lib/logger.js";
|
|
45
39
|
import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
46
40
|
import { type SandboxError } from "./sandbox-error.js";
|
|
47
|
-
import type { FarmSource, ManagedSandbox, SandboxIdentity, SandboxLabels, SandboxLiveness, SandboxRef, SandboxSpec,
|
|
41
|
+
import type { FarmSource, ManagedSandbox, SandboxIdentity, SandboxLabels, SandboxLiveness, SandboxRef, SandboxSpec, ToolchainSource } from "./types.js";
|
|
48
42
|
export interface DockerClientConfig {
|
|
49
43
|
image: string;
|
|
50
|
-
/** Cortex base URL injected into the sandbox env in callback mode so callbacks land here. Unused in poll mode. */
|
|
51
|
-
cortexBaseUrl: string;
|
|
52
|
-
/** Result transport. `poll` (default) confines the sandbox with the egress firewall; `callback` permits egress. */
|
|
53
|
-
transport?: SandboxTransport;
|
|
54
44
|
/** Workspace-root resolution seam; each analysis's resolved root is the bind source for its tree mounts. */
|
|
55
45
|
resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
56
46
|
/** Host lib store; bind-mounted read-only at `/mnt/libs` when set. */
|
|
@@ -17,26 +17,20 @@
|
|
|
17
17
|
* that the sandbox sees. `teardown` removes the directory when this process
|
|
18
18
|
* wrote it. A directory from an earlier process stays.
|
|
19
19
|
*
|
|
20
|
-
* ##
|
|
20
|
+
* ## Confinement
|
|
21
21
|
*
|
|
22
22
|
* The container joins the default bridge and publishes sandbox-server's port to
|
|
23
|
-
* `127.0.0.1` only, so the host can reach `/exec` but the LAN cannot.
|
|
24
|
-
* sandbox
|
|
23
|
+
* `127.0.0.1` only, so the host can reach `/exec` but the LAN cannot. The
|
|
24
|
+
* sandbox initiates nothing, so it gets no egress. The container is created as
|
|
25
|
+
* root with `CAP_NET_ADMIN` and the `SANDBOX_EGRESS_FIREWALL` flag; the image's
|
|
26
|
+
* root entrypoint installs `iptables -P OUTPUT DROP` (allowing loopback and
|
|
27
|
+
* established return traffic) and then `setpriv`-drops to the uid-1000
|
|
28
|
+
* workload, which can neither reach the network nor flush the rules. The host
|
|
29
|
+
* polls `GET /exec/{execId}?since={cursor}` over the published port; the reply
|
|
30
|
+
* rides the established connection, so polling works with egress hard-blocked.
|
|
25
31
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `SANDBOX_EGRESS_FIREWALL` flag; the image's root entrypoint installs
|
|
29
|
-
* `iptables -P OUTPUT DROP` (allowing loopback and established return
|
|
30
|
-
* traffic) and then `setpriv`-drops to the uid-1000 workload, which can
|
|
31
|
-
* neither reach the network nor flush the rules. The host polls
|
|
32
|
-
* `GET /exec/{execId}?since={cursor}` over the published port; the reply
|
|
33
|
-
* rides the established connection, so polling works with egress hard-blocked.
|
|
34
|
-
* - **callback**: the sandbox is *allowed* egress and POSTs signed callbacks to
|
|
35
|
-
* `CORTEX_BASE_URL`. It runs as uid 1000 throughout with no `NET_ADMIN` and
|
|
36
|
-
* no firewall.
|
|
37
|
-
*
|
|
38
|
-
* Either way the workload ends as uid 1000 with `no-new-privileges` and no
|
|
39
|
-
* effective capabilities.
|
|
32
|
+
* The workload ends as uid 1000 with `no-new-privileges` and no effective
|
|
33
|
+
* capabilities.
|
|
40
34
|
*/
|
|
41
35
|
import { lstatSync } from "node:fs";
|
|
42
36
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
@@ -59,14 +53,12 @@ const SANDBOX_SERVER_PORT = 8765;
|
|
|
59
53
|
const HEALTH_TIMEOUT_MS = 30_000;
|
|
60
54
|
/** Only this process dials the published port; binding it on every host interface would expose `/exec` to the LAN. */
|
|
61
55
|
const SANDBOX_PORT_HOST_IP = "127.0.0.1";
|
|
62
|
-
/** Matches `USER sandbox` (uid/gid 1000) in the sandbox images — the callback-mode workload user. */
|
|
63
|
-
const SANDBOX_USER = "1000:1000";
|
|
64
56
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
57
|
+
* The container starts as root so the entrypoint can install the egress
|
|
58
|
+
* firewall; the entrypoint then `setpriv`-drops to uid 1000, so the workload
|
|
59
|
+
* still runs unprivileged. sandbox-server refuses to start as root when the
|
|
60
|
+
* firewall flag is set, so an image whose entrypoint skips the drop fails at
|
|
61
|
+
* create time rather than running privileged and unconfined.
|
|
70
62
|
*/
|
|
71
63
|
const SANDBOX_ROOT_USER = "0:0";
|
|
72
64
|
const HEALTH_POLL_MS = 250;
|
|
@@ -223,15 +215,13 @@ export function createDockerSandboxOps(config) {
|
|
|
223
215
|
const docker = config.docker ?? (connection ? new Docker(connection) : new Docker());
|
|
224
216
|
const logger = (config.logger ?? createNoopLogger()).named("docker-client");
|
|
225
217
|
const fetchImpl = config.fetch ?? fetch;
|
|
226
|
-
const transport = config.transport ?? "poll";
|
|
227
|
-
const pollMode = transport === "poll";
|
|
228
218
|
const readHostCpuinfo = config.readHostCpuinfo ?? readHostCpuinfoOnce;
|
|
229
219
|
/** The cpu directory of each sandbox that this process wrote, for `teardown`. */
|
|
230
220
|
const cpuDirs = new Map();
|
|
231
221
|
return {
|
|
232
222
|
createSandbox(session, spec, identity, hostLabels) {
|
|
233
223
|
return new ResultAsync((async () => {
|
|
234
|
-
const { sandboxId
|
|
224
|
+
const { sandboxId } = identity;
|
|
235
225
|
const analysisId = session.scope.analysisId;
|
|
236
226
|
const coords = mountCoordsOf(session, spec);
|
|
237
227
|
// Farm resolution comes before any container work: a resolver
|
|
@@ -337,17 +327,14 @@ export function createDockerSandboxOps(config) {
|
|
|
337
327
|
});
|
|
338
328
|
const image = spec.image ?? config.image;
|
|
339
329
|
const limits = spec.resources;
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
// dropping to uid 1000. Callback mode is the inverse.
|
|
330
|
+
// The firewall flag makes the root entrypoint install the egress block
|
|
331
|
+
// before it drops to uid 1000.
|
|
343
332
|
//
|
|
344
333
|
// Composed as one record, not as a list of `K=V`. A duplicate key in
|
|
345
334
|
// the Env array resolves at the libc that scans `environ`, thus the
|
|
346
335
|
// later spread must win here, not there.
|
|
347
336
|
const env = Object.entries({
|
|
348
|
-
|
|
349
|
-
SANDBOX_CALLBACK_SECRET: callbackSecret,
|
|
350
|
-
...(pollMode ? { SANDBOX_EGRESS_FIREWALL: "1" } : { CORTEX_BASE_URL: config.cortexBaseUrl }),
|
|
337
|
+
SANDBOX_EGRESS_FIREWALL: "1",
|
|
351
338
|
...threadLimitEnv(limits),
|
|
352
339
|
...plan.env,
|
|
353
340
|
...(spec.extraEnv ?? {}),
|
|
@@ -357,7 +344,7 @@ export function createDockerSandboxOps(config) {
|
|
|
357
344
|
...(config.platform !== undefined && { platform: config.platform }),
|
|
358
345
|
Image: image,
|
|
359
346
|
Env: env,
|
|
360
|
-
User:
|
|
347
|
+
User: SANDBOX_ROOT_USER,
|
|
361
348
|
WorkingDir: plan.workingDir,
|
|
362
349
|
Labels: mergeLabels(hostLabels, { ...harnessLabels(session, sandboxId), [OWNER_WORKFLOW_LABEL]: spec.childWorkflowId }),
|
|
363
350
|
ExposedPorts: { [`${SANDBOX_SERVER_PORT}/tcp`]: {} },
|
|
@@ -368,14 +355,14 @@ export function createDockerSandboxOps(config) {
|
|
|
368
355
|
[`${SANDBOX_SERVER_PORT}/tcp`]: [{ HostIp: SANDBOX_PORT_HOST_IP, HostPort: "0" }],
|
|
369
356
|
},
|
|
370
357
|
CapDrop: ["ALL"],
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
|
|
358
|
+
// The ROOT entrypoint gets exactly what its privileged setup needs:
|
|
359
|
+
// NET_ADMIN to install the egress iptables rules, SETUID/SETGID for
|
|
360
|
+
// the setpriv uid/gid drop to the workload user (setresuid/setresgid
|
|
361
|
+
// fail EPERM without them), and SETPCAP to apply setpriv's
|
|
362
|
+
// `--bounding-set=-all`. The entrypoint drops all of them before the
|
|
363
|
+
// workload runs — the workload's capability sets end empty — and
|
|
364
|
+
// `no-new-privileges` prevents regaining any.
|
|
365
|
+
CapAdd: ["NET_ADMIN", "SETUID", "SETGID", "SETPCAP"],
|
|
379
366
|
SecurityOpt: ["no-new-privileges"],
|
|
380
367
|
NanoCpus: Math.round(limits.cpu * 1e9),
|
|
381
368
|
Memory: limits.memoryGb * 1024 ** 3,
|
|
@@ -415,7 +402,6 @@ export function createDockerSandboxOps(config) {
|
|
|
415
402
|
host,
|
|
416
403
|
port,
|
|
417
404
|
backend: "docker",
|
|
418
|
-
callbackSecret,
|
|
419
405
|
};
|
|
420
406
|
const registered = await trySandbox(() => config.registerSandbox(session, spec, ref), createFailed);
|
|
421
407
|
if (registered.isErr())
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One sandbox exec: submit the command, then poll until it is terminal.
|
|
3
|
+
*
|
|
4
|
+
* The whole exec runs inside ONE durable step (`SandboxClient.exec` wraps it),
|
|
5
|
+
* thus nothing here touches DBOS but the cancellation read. The polls, the
|
|
6
|
+
* sleeps, and the liveness probes are plain calls. A replay returns the cached
|
|
7
|
+
* result of the step and never reaches this module.
|
|
8
|
+
*
|
|
9
|
+
* A recovered step runs this module again from the start. The sandbox makes
|
|
10
|
+
* that safe: the submit is idempotent on `execId`, and the sandbox keeps the
|
|
11
|
+
* record of each exec it ran, thus a second submit attaches to the running or
|
|
12
|
+
* finished exec instead of a second run. The poll then starts at cursor 0, and
|
|
13
|
+
* the caller gets the events of the exec again.
|
|
14
|
+
*/
|
|
15
|
+
import { type ExecEmit, type ExecRequest, type ExecResult, type SandboxLiveness, type SandboxRef } from "./types.js";
|
|
16
|
+
export declare class ExecTimeoutError extends Error {
|
|
17
|
+
readonly execId: string;
|
|
18
|
+
constructor(execId: string);
|
|
19
|
+
}
|
|
20
|
+
/** The effects of one exec. Each member has a default, and a test replaces it. */
|
|
21
|
+
export interface ExecDeps {
|
|
22
|
+
/** Defaults to `globalThis.fetch`. */
|
|
23
|
+
readonly fetch?: typeof fetch;
|
|
24
|
+
/** The clock of the deadline and of the cancel reads. Defaults to `Date.now`. */
|
|
25
|
+
readonly now?: () => number;
|
|
26
|
+
/** The pause between two polls. Defaults to a timer. */
|
|
27
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Sink for advisory warnings: events that the sandbox ring shed. Defaults to
|
|
30
|
+
* `DBOS.logger.warn`. The terminal result, not the event stream, is the
|
|
31
|
+
* outcome of the exec.
|
|
32
|
+
*/
|
|
33
|
+
readonly warn?: (message: string) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Backend inspect for the liveness escalation (`liveness.ts`). Absent, the
|
|
36
|
+
* exec never probes and only the deadline bounds it. A throw is an
|
|
37
|
+
* inconclusive probe, never a failed exec.
|
|
38
|
+
*/
|
|
39
|
+
readonly isAlive?: (ref: SandboxRef) => Promise<SandboxLiveness>;
|
|
40
|
+
/** True once the owning workflow is cancelled. Defaults to a read of the DBOS workflow status. */
|
|
41
|
+
readonly isCancelled?: () => Promise<boolean>;
|
|
42
|
+
/** Per-stream retention budget sent with the submit. Defaults to `EXEC_STREAM_BYTE_CAP`. */
|
|
43
|
+
readonly execStreamByteCap?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The id of the exec that runs in the current durable step: the workflow id
|
|
47
|
+
* and the function id of the step. Both are stable across a replay, and DBOS
|
|
48
|
+
* makes the pair unique, thus no caller mints an exec id.
|
|
49
|
+
*/
|
|
50
|
+
export declare function stepExecId(): string;
|
|
51
|
+
/**
|
|
52
|
+
* Run one exec to its terminal result.
|
|
53
|
+
*
|
|
54
|
+
* `deadlineMs` is an absolute unix-ms timestamp. A machine that the liveness
|
|
55
|
+
* probe finds dead ends the exec with a synthetic-failure result. A cancel of
|
|
56
|
+
* the owning workflow throws `DBOSWorkflowCancelledError`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function runExec(ref: SandboxRef, execId: string, request: ExecRequest, emit: ExecEmit, deadlineMs: number, deps?: ExecDeps): Promise<ExecResult>;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One sandbox exec: submit the command, then poll until it is terminal.
|
|
3
|
+
*
|
|
4
|
+
* The whole exec runs inside ONE durable step (`SandboxClient.exec` wraps it),
|
|
5
|
+
* thus nothing here touches DBOS but the cancellation read. The polls, the
|
|
6
|
+
* sleeps, and the liveness probes are plain calls. A replay returns the cached
|
|
7
|
+
* result of the step and never reaches this module.
|
|
8
|
+
*
|
|
9
|
+
* A recovered step runs this module again from the start. The sandbox makes
|
|
10
|
+
* that safe: the submit is idempotent on `execId`, and the sandbox keeps the
|
|
11
|
+
* record of each exec it ran, thus a second submit attaches to the running or
|
|
12
|
+
* finished exec instead of a second run. The poll then starts at cursor 0, and
|
|
13
|
+
* the caller gets the events of the exec again.
|
|
14
|
+
*/
|
|
15
|
+
import { DBOS, Error as DBOSErrors, StatusString } from "@dbos-inc/dbos-sdk";
|
|
16
|
+
import { untracedFetch } from "../lib/otel-spans.js";
|
|
17
|
+
import { EXEC_STREAM_BYTE_CAP } from "../tools/workspace/result-bounds.js";
|
|
18
|
+
import { createEscalationPolicy, probeLiveness, syntheticFailureReason, syntheticFailureResult } from "./liveness.js";
|
|
19
|
+
import { PollResponseSchema } from "./types.js";
|
|
20
|
+
const SUBMIT_HTTP_TIMEOUT_MS = 30_000;
|
|
21
|
+
const POLL_HTTP_TIMEOUT_MS = 10_000;
|
|
22
|
+
/**
|
|
23
|
+
* Two-phase poll cadence: 1.5 s for the first 40 polls (about the first minute
|
|
24
|
+
* of the exec) keeps a short command prompt, and 10 s after that bounds the
|
|
25
|
+
* request rate of an exec that runs for hours.
|
|
26
|
+
*/
|
|
27
|
+
const FAST_POLL_INTERVAL_MS = 1_500;
|
|
28
|
+
const FAST_POLL_ATTEMPTS = 40;
|
|
29
|
+
const SLOW_POLL_INTERVAL_MS = 10_000;
|
|
30
|
+
/**
|
|
31
|
+
* The minimum gap between two reads of the workflow status. A step does not see
|
|
32
|
+
* a cancel on its own, thus the exec reads the status of its workflow and stops
|
|
33
|
+
* when the workflow is cancelled.
|
|
34
|
+
*/
|
|
35
|
+
const CANCEL_CHECK_INTERVAL_MS = 10_000;
|
|
36
|
+
export class ExecTimeoutError extends Error {
|
|
37
|
+
execId;
|
|
38
|
+
constructor(execId) {
|
|
39
|
+
super(`exec[${execId}]: deadline exceeded before a terminal result`);
|
|
40
|
+
this.execId = execId;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The id of the exec that runs in the current durable step: the workflow id
|
|
45
|
+
* and the function id of the step. Both are stable across a replay, and DBOS
|
|
46
|
+
* makes the pair unique, thus no caller mints an exec id.
|
|
47
|
+
*/
|
|
48
|
+
export function stepExecId() {
|
|
49
|
+
const workflowId = DBOS.workflowID;
|
|
50
|
+
const stepId = DBOS.stepID;
|
|
51
|
+
if (workflowId === undefined || stepId === undefined) {
|
|
52
|
+
throw new Error("a sandbox exec runs inside a DBOS workflow, and none is active");
|
|
53
|
+
}
|
|
54
|
+
return `${workflowId}:${stepId}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Run one exec to its terminal result.
|
|
58
|
+
*
|
|
59
|
+
* `deadlineMs` is an absolute unix-ms timestamp. A machine that the liveness
|
|
60
|
+
* probe finds dead ends the exec with a synthetic-failure result. A cancel of
|
|
61
|
+
* the owning workflow throws `DBOSWorkflowCancelledError`.
|
|
62
|
+
*/
|
|
63
|
+
export async function runExec(ref, execId, request, emit, deadlineMs, deps = {}) {
|
|
64
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
65
|
+
const cap = deps.execStreamByteCap ?? EXEC_STREAM_BYTE_CAP;
|
|
66
|
+
await submitExec(fetchImpl, ref, {
|
|
67
|
+
command: [...request.command],
|
|
68
|
+
execId,
|
|
69
|
+
...(request.cwd === undefined ? {} : { cwd: request.cwd }),
|
|
70
|
+
...(request.env === undefined ? {} : { env: { ...request.env } }),
|
|
71
|
+
...(request.timeoutSeconds === undefined ? {} : { timeoutSeconds: request.timeoutSeconds }),
|
|
72
|
+
stdoutByteCap: cap,
|
|
73
|
+
stderrByteCap: cap,
|
|
74
|
+
});
|
|
75
|
+
return pollExec(ref, execId, emit, deadlineMs, deps);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* POST the command to `/exec` and return after the 202 ack. A non-202 throws.
|
|
79
|
+
* The server dedups on `execId`: a submit of an exec that it already holds
|
|
80
|
+
* returns 202 with the stored status and runs nothing.
|
|
81
|
+
*/
|
|
82
|
+
async function submitExec(fetchImpl, ref, body) {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timer = setTimeout(() => controller.abort(), SUBMIT_HTTP_TIMEOUT_MS);
|
|
85
|
+
try {
|
|
86
|
+
const res = await untracedFetch(fetchImpl, `http://${ref.host}:${ref.port}/exec`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "content-type": "application/json" },
|
|
89
|
+
body: JSON.stringify(body),
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
});
|
|
92
|
+
if (res.status !== 202) {
|
|
93
|
+
const text = await res.text().catch(() => "");
|
|
94
|
+
throw Object.assign(new Error(`submitExec: sandbox-server returned ${res.status} for execId=${body.execId}: ${text}`), { statusCode: res.status });
|
|
95
|
+
}
|
|
96
|
+
// Read and discard the ack, so the socket is released.
|
|
97
|
+
await res.text().catch(() => "");
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function pollOnce(fetchImpl, ref, execId, cursor) {
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timer = setTimeout(() => controller.abort(), POLL_HTTP_TIMEOUT_MS);
|
|
106
|
+
try {
|
|
107
|
+
const res = await untracedFetch(fetchImpl, `http://${ref.host}:${ref.port}/exec/${encodeURIComponent(execId)}?since=${cursor}`, {
|
|
108
|
+
method: "GET",
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
await res.text().catch(() => "");
|
|
113
|
+
return { kind: "unavailable", detail: `status ${res.status}` };
|
|
114
|
+
}
|
|
115
|
+
return { kind: "ok", raw: await res.text() };
|
|
116
|
+
}
|
|
117
|
+
catch (cause) {
|
|
118
|
+
return { kind: "unavailable", detail: cause instanceof Error ? cause.message : String(cause) };
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Poll until the exec is terminal. Each poll reads
|
|
126
|
+
* `{ status, events, cursor, result? }`, forwards the events past the local
|
|
127
|
+
* cursor through `emit`, and returns `result` once it is there.
|
|
128
|
+
*
|
|
129
|
+
* Consecutive `unavailable` polls arm a backend probe (`liveness.ts`). Only an
|
|
130
|
+
* observably dead machine fails the exec, because the result lives in that
|
|
131
|
+
* machine and nowhere else.
|
|
132
|
+
*/
|
|
133
|
+
async function pollExec(ref, execId, emit, deadlineMs, deps) {
|
|
134
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
135
|
+
const now = deps.now ?? Date.now;
|
|
136
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
137
|
+
const warn = deps.warn ?? ((message) => DBOS.logger.warn(message));
|
|
138
|
+
const isCancelled = deps.isCancelled ?? workflowCancelled;
|
|
139
|
+
let cursor = 0;
|
|
140
|
+
let pollAttempt = 0;
|
|
141
|
+
let lastCancelCheckMs = now();
|
|
142
|
+
const escalation = createEscalationPolicy();
|
|
143
|
+
while (true) {
|
|
144
|
+
pollAttempt += 1;
|
|
145
|
+
const polled = await pollOnce(fetchImpl, ref, execId, cursor);
|
|
146
|
+
if (polled.kind === "ok") {
|
|
147
|
+
const resp = PollResponseSchema.parse(JSON.parse(polled.raw));
|
|
148
|
+
// Each event is emitted at most once, whatever `since` the server applied.
|
|
149
|
+
const newEvents = resp.events.filter((ev) => ev.seq > cursor);
|
|
150
|
+
// Sequence numbers are contiguous per exec, thus a hole between the local
|
|
151
|
+
// cursor and the next delivered (or high-water) seq proves that the ring
|
|
152
|
+
// shed events before the host saw them.
|
|
153
|
+
const nextSeq = newEvents.length > 0 ? newEvents[0].seq : resp.cursor > cursor ? resp.cursor + 1 : cursor + 1;
|
|
154
|
+
const lost = nextSeq - cursor - 1;
|
|
155
|
+
if (lost > 0) {
|
|
156
|
+
warn(`exec[${execId}]: ${lost} progress event(s) (seq ${cursor + 1}-${cursor + lost}) shed by the sandbox ring before delivery; the terminal result is unaffected`);
|
|
157
|
+
}
|
|
158
|
+
// Forwarded before the return, thus trailing progress lands ahead of the result.
|
|
159
|
+
for (const ev of newEvents) {
|
|
160
|
+
await emit(ev.payload);
|
|
161
|
+
}
|
|
162
|
+
if (resp.cursor > cursor)
|
|
163
|
+
cursor = resp.cursor;
|
|
164
|
+
if (resp.result !== undefined)
|
|
165
|
+
return resp.result;
|
|
166
|
+
}
|
|
167
|
+
if (deps.isAlive && escalation.onPoll(polled.kind === "ok" ? "ok" : "unavailable")) {
|
|
168
|
+
const verdict = await probeLiveness(deps.isAlive, ref);
|
|
169
|
+
// `alive` (a slow exec) and `inconclusive` (a backend
|
|
170
|
+
// API error) both resume the poll, bounded by the deadline.
|
|
171
|
+
if (verdict.kind === "dead") {
|
|
172
|
+
return syntheticFailureResult(execId, syntheticFailureReason({ oomKilled: verdict.oomKilled }));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// The deadline gate sits AFTER the poll, thus the loop always asks once more
|
|
176
|
+
// before it declares a timeout: a missed poll window is not a slow command.
|
|
177
|
+
const nowMs = now();
|
|
178
|
+
const remainingMs = deadlineMs - nowMs;
|
|
179
|
+
if (remainingMs <= 0)
|
|
180
|
+
throw new ExecTimeoutError(execId);
|
|
181
|
+
if (nowMs - lastCancelCheckMs >= CANCEL_CHECK_INTERVAL_MS) {
|
|
182
|
+
lastCancelCheckMs = nowMs;
|
|
183
|
+
if (await isCancelled())
|
|
184
|
+
throw new DBOSErrors.DBOSWorkflowCancelledError(DBOS.workflowID ?? execId);
|
|
185
|
+
}
|
|
186
|
+
const intervalMs = pollAttempt <= FAST_POLL_ATTEMPTS ? FAST_POLL_INTERVAL_MS : SLOW_POLL_INTERVAL_MS;
|
|
187
|
+
await sleep(Math.min(intervalMs, remainingMs));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Read the status of the workflow that runs this exec. Outside a workflow, nothing can cancel it. */
|
|
191
|
+
async function workflowCancelled() {
|
|
192
|
+
const workflowId = DBOS.workflowID;
|
|
193
|
+
if (workflowId === undefined)
|
|
194
|
+
return false;
|
|
195
|
+
const status = await DBOS.getWorkflowStatus(workflowId);
|
|
196
|
+
return status?.status === StatusString.CANCELLED;
|
|
197
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sandbox identity minting — the durable half of the two-step create (ADR
|
|
3
|
-
* 0016). Backend-agnostic: the `sandboxId`
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* 0016). Backend-agnostic: the `sandboxId` is minted here, checkpointed by the
|
|
4
|
+
* `sandbox.mint` DBOS step, then handed to the backend's spawn step. Because the
|
|
5
|
+
* identity is durable before any machine exists, a crash between spawn and the
|
|
6
|
+
* spawn-step checkpoint is recoverable — the re-run adopts the existing machine
|
|
7
|
+
* under the same name.
|
|
8
8
|
*/
|
|
9
9
|
import type { SandboxIdentity } from "./types.js";
|
|
10
10
|
/** `sbx-{run8}-{rand8}` — run prefix aids `kubectl` triage; the 8-hex random
|
package/dist/sandbox/identity.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sandbox identity minting — the durable half of the two-step create (ADR
|
|
3
|
-
* 0016). Backend-agnostic: the `sandboxId`
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* 0016). Backend-agnostic: the `sandboxId` is minted here, checkpointed by the
|
|
4
|
+
* `sandbox.mint` DBOS step, then handed to the backend's spawn step. Because the
|
|
5
|
+
* identity is durable before any machine exists, a crash between spawn and the
|
|
6
|
+
* spawn-step checkpoint is recoverable — the re-run adopts the existing machine
|
|
7
|
+
* under the same name.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
10
|
/** `sbx-{run8}-{rand8}` — run prefix aids `kubectl` triage; the 8-hex random
|
|
11
11
|
* suffix makes within-run collisions across a run's concurrent steps
|
|
12
12
|
* negligible. Names are NOT the idempotency mechanism (the checkpoint is), and
|
|
@@ -15,8 +15,5 @@ import { randomBytes, randomUUID } from "node:crypto";
|
|
|
15
15
|
export function mintSandboxIdentity(runId) {
|
|
16
16
|
const run8 = runId.replace(/-/g, "").slice(0, 8);
|
|
17
17
|
const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
18
|
-
return {
|
|
19
|
-
sandboxId: `sbx-${run8}-${suffix}`,
|
|
20
|
-
callbackSecret: "base64:" + randomBytes(32).toString("base64"),
|
|
21
|
-
};
|
|
18
|
+
return { sandboxId: `sbx-${run8}-${suffix}` };
|
|
22
19
|
}
|