@inflexa-ai/harness 0.37.0 → 0.38.0
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 +1 -2
- package/dist/auth/local-run-authorizer.js +5 -4
- package/dist/auth/types.d.ts +28 -9
- package/dist/auth/types.js +5 -3
- package/dist/billing/noop-run-charge.js +3 -2
- package/dist/billing/noop-usage-recorder.js +2 -1
- package/dist/billing/run-charge.d.ts +18 -5
- package/dist/billing/run-charge.js +5 -1
- package/dist/billing/usage-recorder.d.ts +11 -9
- package/dist/billing/usage-recorder.js +2 -3
- package/dist/contracts/chat-events.d.ts +4 -1
- package/dist/contracts/chat-parts.d.ts +1 -1
- package/dist/contracts/schemas/chat-parts.d.ts +4 -4
- package/dist/execution/artifact-registration.d.ts +6 -1
- package/dist/execution/artifact-registration.js +12 -4
- package/dist/execution/artifact-registry.d.ts +10 -3
- package/dist/execution/noop-artifact-registry.js +3 -4
- package/dist/execution/post-step-pipeline.d.ts +14 -3
- package/dist/execution/post-step-pipeline.js +21 -8
- package/dist/execution/run-authorizer.d.ts +5 -3
- package/dist/execution/run-canceler.js +4 -20
- package/dist/index.d.ts +13 -7
- package/dist/index.js +12 -6
- package/dist/lib/hooks.d.ts +49 -0
- package/dist/lib/hooks.js +39 -0
- package/dist/lib/result.d.ts +22 -11
- package/dist/lib/result.js +24 -14
- package/dist/loop/run-agent.d.ts +2 -2
- package/dist/loop/run-agent.js +5 -4
- package/dist/provenance/seam.d.ts +1 -1
- package/dist/providers/ai-sdk.d.ts +43 -5
- package/dist/providers/ai-sdk.js +55 -52
- package/dist/providers/anthropic.d.ts +2 -3
- package/dist/providers/anthropic.js +2 -2
- package/dist/providers/embedding.d.ts +13 -7
- package/dist/providers/embedding.js +26 -6
- package/dist/providers/errors.d.ts +67 -37
- package/dist/providers/errors.js +57 -58
- package/dist/providers/request-headers.d.ts +18 -0
- package/dist/providers/request-headers.js +22 -0
- package/dist/report-model/production-resolver.d.ts +5 -2
- package/dist/report-model/production-resolver.js +12 -2
- package/dist/runtime/assemble.d.ts +3 -2
- package/dist/runtime/dbos.d.ts +4 -3
- package/dist/runtime/dbos.js +18 -3
- package/dist/runtime/result-serialization.d.ts +29 -0
- package/dist/runtime/result-serialization.js +45 -0
- package/dist/sandbox/client.d.ts +11 -3
- package/dist/sandbox/create-sandbox.d.ts +5 -2
- package/dist/sandbox/create-sandbox.js +51 -25
- package/dist/sandbox/docker-client.d.ts +4 -3
- package/dist/sandbox/docker-client.js +23 -31
- package/dist/sandbox/k8s-client.d.ts +4 -13
- package/dist/sandbox/k8s-client.js +28 -61
- package/dist/sandbox/labels.d.ts +34 -0
- package/dist/sandbox/labels.js +47 -0
- package/dist/sandbox/mount-plan.d.ts +3 -1
- package/dist/sandbox/mount-plan.js +9 -0
- package/dist/sandbox/sandbox-error.d.ts +36 -13
- package/dist/sandbox/sandbox-error.js +32 -17
- package/dist/sandbox/types.d.ts +13 -9
- package/dist/schemas/validate-plan.js +13 -0
- package/dist/schemas/workflow-state.d.ts +4 -4
- package/dist/state/analyses.d.ts +4 -4
- package/dist/state/analyses.js +4 -4
- package/dist/state/schema.d.ts +5 -5
- package/dist/tasks/data-profile.d.ts +5 -8
- package/dist/tasks/data-profile.js +88 -43
- package/dist/tasks/derive-table-exec.d.ts +10 -6
- package/dist/tasks/derive-table-exec.js +17 -12
- package/dist/tasks/extract-values.d.ts +16 -10
- package/dist/tasks/extract-values.js +34 -18
- package/dist/tools/execute-analysis.js +22 -15
- package/dist/tools/report-session/derive-table.d.ts +9 -1
- package/dist/tools/report-session/derive-table.js +25 -22
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +25 -16
- package/dist/workflows/execute-analysis.js +223 -216
- package/dist/workflows/metrics.d.ts +4 -3
- package/dist/workflows/metrics.js +4 -3
- package/dist/workflows/sandbox-step.d.ts +20 -29
- package/dist/workflows/sandbox-step.js +71 -127
- package/dist/workflows/suspension.d.ts +32 -0
- package/dist/workflows/suspension.js +38 -0
- package/package.json +1 -1
- package/dist/billing/noop-resolver.d.ts +0 -6
- package/dist/billing/noop-resolver.js +0 -7
- package/dist/billing/resolver.d.ts +0 -64
- package/dist/billing/resolver.js +0 -24
- package/dist/loop/budget-exceeded.d.ts +0 -38
- package/dist/loop/budget-exceeded.js +0 -88
|
@@ -40,10 +40,11 @@
|
|
|
40
40
|
*/
|
|
41
41
|
import Docker from "dockerode";
|
|
42
42
|
import { ResultAsync } from "neverthrow";
|
|
43
|
+
import type { SpawnSession } from "../auth/types.js";
|
|
43
44
|
import type { Logger } from "../lib/logger.js";
|
|
44
45
|
import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
45
46
|
import { type SandboxError } from "./sandbox-error.js";
|
|
46
|
-
import type {
|
|
47
|
+
import type { FarmSource, ManagedSandbox, SandboxIdentity, SandboxLabels, SandboxLiveness, SandboxRef, SandboxSpec, SandboxTransport, ToolchainSource } from "./types.js";
|
|
47
48
|
export interface DockerClientConfig {
|
|
48
49
|
image: string;
|
|
49
50
|
/** Cortex base URL injected into the sandbox env in callback mode so callbacks land here. Unused in poll mode. */
|
|
@@ -112,7 +113,7 @@ export interface DockerClientConfig {
|
|
|
112
113
|
*/
|
|
113
114
|
logger?: Logger;
|
|
114
115
|
/** Hook called after the registry row is written. */
|
|
115
|
-
registerSandbox: (
|
|
116
|
+
registerSandbox: (session: SpawnSession, spec: SandboxSpec, ref: SandboxRef) => Promise<void>;
|
|
116
117
|
}
|
|
117
118
|
/**
|
|
118
119
|
* dockerode connection options for a configured engine socket, or `undefined`
|
|
@@ -123,7 +124,7 @@ export interface DockerClientConfig {
|
|
|
123
124
|
*/
|
|
124
125
|
export declare function engineConnectionOptions(engineSocketPath: string | undefined): Docker.DockerOptions | undefined;
|
|
125
126
|
export declare function createDockerSandboxOps(config: DockerClientConfig): {
|
|
126
|
-
createSandbox(
|
|
127
|
+
createSandbox(session: SpawnSession, spec: SandboxSpec, identity: SandboxIdentity, hostLabels: SandboxLabels): ResultAsync<SandboxRef, SandboxError>;
|
|
127
128
|
teardown(ref: SandboxRef): ResultAsync<void, SandboxError>;
|
|
128
129
|
teardownById(sandboxId: string): ResultAsync<void, SandboxError>;
|
|
129
130
|
isAlive(ref: SandboxRef): ResultAsync<SandboxLiveness, SandboxError>;
|
|
@@ -47,7 +47,8 @@ import { createNoopLogger } from "../lib/console-logger.js";
|
|
|
47
47
|
import { tailWritePrefix } from "../workspace/paths.js";
|
|
48
48
|
import { trySandbox } from "./sandbox-error.js";
|
|
49
49
|
import { readFarmLock, resolveFarmSource } from "./farm.js";
|
|
50
|
-
import {
|
|
50
|
+
import { harnessLabels, MANAGED_BY_LABEL, MANAGED_BY_VALUE, mergeLabels, OWNER_WORKFLOW_KEY, SANDBOX_ID_LABEL } from "./labels.js";
|
|
51
|
+
import { buildMountPlan, mountCoordsOf, sandboxWriteTail } from "./mount-plan.js";
|
|
51
52
|
import { CPU_ONLINE_PATH, CPUINFO_PATH, cpuFiles, readHostCpuinfoOnce } from "./cpu-files.js";
|
|
52
53
|
import { threadLimitEnv } from "./thread-env.js";
|
|
53
54
|
/** Read the originating HTTP status off any `SandboxError` variant that carries one. */
|
|
@@ -69,11 +70,7 @@ const SANDBOX_USER = "1000:1000";
|
|
|
69
70
|
*/
|
|
70
71
|
const SANDBOX_ROOT_USER = "0:0";
|
|
71
72
|
const HEALTH_POLL_MS = 250;
|
|
72
|
-
const
|
|
73
|
-
const MANAGED_BY_VALUE = "cortex";
|
|
74
|
-
const OWNER_WORKFLOW_LABEL = "cortex/owner-workflow-id";
|
|
75
|
-
const RUN_ID_LABEL = "cortex/run-id";
|
|
76
|
-
const STEP_ID_LABEL = "cortex/step-id";
|
|
73
|
+
const OWNER_WORKFLOW_LABEL = OWNER_WORKFLOW_KEY;
|
|
77
74
|
/** The directory next to the workspace root that holds the cpu files of each sandbox. */
|
|
78
75
|
const CPU_FILES_DIR = ".cpu";
|
|
79
76
|
/**
|
|
@@ -232,15 +229,17 @@ export function createDockerSandboxOps(config) {
|
|
|
232
229
|
/** The cpu directory of each sandbox that this process wrote, for `teardown`. */
|
|
233
230
|
const cpuDirs = new Map();
|
|
234
231
|
return {
|
|
235
|
-
createSandbox(
|
|
232
|
+
createSandbox(session, spec, identity, hostLabels) {
|
|
236
233
|
return new ResultAsync((async () => {
|
|
237
234
|
const { sandboxId, callbackSecret } = identity;
|
|
235
|
+
const analysisId = session.scope.analysisId;
|
|
236
|
+
const coords = mountCoordsOf(session, spec);
|
|
238
237
|
// Farm resolution comes before any container work: a resolver
|
|
239
238
|
// refusal and a resolver throw refuse the whole call, and no
|
|
240
239
|
// container is made. The reason of the embedder rides in the error.
|
|
241
240
|
let farm;
|
|
242
241
|
if (config.libStorePath) {
|
|
243
|
-
const resolved = await resolveFarmSource(config.farmSource,
|
|
242
|
+
const resolved = await resolveFarmSource(config.farmSource, analysisId, "docker.createSandbox");
|
|
244
243
|
if (resolved.isErr())
|
|
245
244
|
return err(resolved.error);
|
|
246
245
|
farm = resolved.value;
|
|
@@ -262,7 +261,7 @@ export function createDockerSandboxOps(config) {
|
|
|
262
261
|
return err({
|
|
263
262
|
type: "farm_unusable",
|
|
264
263
|
op: "docker.createSandbox",
|
|
265
|
-
analysisId
|
|
264
|
+
analysisId,
|
|
266
265
|
farmPath: farm.farmPath,
|
|
267
266
|
lockPath: lock.error.lockPath,
|
|
268
267
|
lockError: lock.error.type,
|
|
@@ -292,20 +291,20 @@ export function createDockerSandboxOps(config) {
|
|
|
292
291
|
// not installed reference data yet — and warning on every create would be
|
|
293
292
|
// noise. A store that appears mid-session is picked up by the next create.
|
|
294
293
|
const refsMounted = !!config.refStorePath && refStoreUsable(config.refStorePath);
|
|
295
|
-
const plan = buildMountPlan(
|
|
294
|
+
const plan = buildMountPlan(coords, {
|
|
296
295
|
libs: libsMounted,
|
|
297
296
|
refs: refsMounted,
|
|
298
297
|
toolchainSource: config.toolchainSource,
|
|
299
298
|
cache: cacheBindable,
|
|
300
299
|
});
|
|
301
|
-
const hostTreePath = config.resolveWorkspaceRoot(
|
|
300
|
+
const hostTreePath = config.resolveWorkspaceRoot(analysisId);
|
|
302
301
|
// `tailWritePrefix` (not a raw `join`) so the RW bind source runs through
|
|
303
302
|
// the same validation as the pre-created write tree — a crafted stepId or
|
|
304
303
|
// a crafted tail cannot escape the resolved root into the container. The
|
|
305
304
|
// tail is the step directory, or the one the caller declared.
|
|
306
|
-
const write = sandboxWriteTail(
|
|
305
|
+
const write = sandboxWriteTail(coords);
|
|
307
306
|
// The same floor as `threadLimitEnv`: a fractional cpu request is one cpu.
|
|
308
|
-
const threads = Math.max(1, Math.floor(
|
|
307
|
+
const threads = Math.max(1, Math.floor(spec.resources.cpu));
|
|
309
308
|
const cpuinfo = await readHostCpuinfo();
|
|
310
309
|
if (cpuinfo === undefined) {
|
|
311
310
|
logger.debug("no host /proc/cpuinfo — the sandbox gets the online file only", { sandboxId });
|
|
@@ -336,8 +335,8 @@ export function createDockerSandboxOps(config) {
|
|
|
336
335
|
status,
|
|
337
336
|
cause,
|
|
338
337
|
});
|
|
339
|
-
const image =
|
|
340
|
-
const
|
|
338
|
+
const image = spec.image ?? config.image;
|
|
339
|
+
const limits = spec.resources;
|
|
341
340
|
// Poll mode never dials out and carries no CORTEX_BASE_URL; it sets the
|
|
342
341
|
// firewall flag so the root entrypoint installs the egress block before
|
|
343
342
|
// dropping to uid 1000. Callback mode is the inverse.
|
|
@@ -349,9 +348,9 @@ export function createDockerSandboxOps(config) {
|
|
|
349
348
|
SANDBOX_TRANSPORT: transport,
|
|
350
349
|
SANDBOX_CALLBACK_SECRET: callbackSecret,
|
|
351
350
|
...(pollMode ? { SANDBOX_EGRESS_FIREWALL: "1" } : { CORTEX_BASE_URL: config.cortexBaseUrl }),
|
|
352
|
-
...threadLimitEnv(
|
|
351
|
+
...threadLimitEnv(limits),
|
|
353
352
|
...plan.env,
|
|
354
|
-
...(
|
|
353
|
+
...(spec.extraEnv ?? {}),
|
|
355
354
|
}).map(([k, v]) => `${k}=${v}`);
|
|
356
355
|
const createOpts = {
|
|
357
356
|
name: sandboxId,
|
|
@@ -360,14 +359,7 @@ export function createDockerSandboxOps(config) {
|
|
|
360
359
|
Env: env,
|
|
361
360
|
User: pollMode ? SANDBOX_ROOT_USER : SANDBOX_USER,
|
|
362
361
|
WorkingDir: plan.workingDir,
|
|
363
|
-
Labels: {
|
|
364
|
-
[MANAGED_BY_LABEL]: MANAGED_BY_VALUE,
|
|
365
|
-
[OWNER_WORKFLOW_LABEL]: meta.childWorkflowId,
|
|
366
|
-
[RUN_ID_LABEL]: meta.runId,
|
|
367
|
-
[STEP_ID_LABEL]: meta.stepId,
|
|
368
|
-
role: "sandbox",
|
|
369
|
-
"cortex/sandbox-id": sandboxId,
|
|
370
|
-
},
|
|
362
|
+
Labels: mergeLabels(hostLabels, { ...harnessLabels(session, sandboxId), [OWNER_WORKFLOW_LABEL]: spec.childWorkflowId }),
|
|
371
363
|
ExposedPorts: { [`${SANDBOX_SERVER_PORT}/tcp`]: {} },
|
|
372
364
|
HostConfig: {
|
|
373
365
|
Binds: binds,
|
|
@@ -385,17 +377,17 @@ export function createDockerSandboxOps(config) {
|
|
|
385
377
|
// — and `no-new-privileges` prevents regaining any.
|
|
386
378
|
...(pollMode ? { CapAdd: ["NET_ADMIN", "SETUID", "SETGID", "SETPCAP"] } : {}),
|
|
387
379
|
SecurityOpt: ["no-new-privileges"],
|
|
388
|
-
NanoCpus: Math.round(
|
|
389
|
-
Memory:
|
|
380
|
+
NanoCpus: Math.round(limits.cpu * 1e9),
|
|
381
|
+
Memory: limits.memoryGb * 1024 ** 3,
|
|
390
382
|
// `Memory` alone lets the container swap as much again. A fork
|
|
391
383
|
// storm then thrashes for minutes, and sandbox-server stops to
|
|
392
384
|
// answer. With no swap the OOM killer removes the largest fork in
|
|
393
385
|
// seconds, and sandbox-server survives. K8s runs with no swap.
|
|
394
|
-
MemorySwap:
|
|
386
|
+
MemorySwap: limits.memoryGb * 1024 ** 3,
|
|
395
387
|
AutoRemove: false,
|
|
396
388
|
},
|
|
397
389
|
};
|
|
398
|
-
const sandbox = await createOrAdopt(docker, createOpts, sandboxId,
|
|
390
|
+
const sandbox = await createOrAdopt(docker, createOpts, sandboxId, spec.childWorkflowId, createFailed, logger);
|
|
399
391
|
if (sandbox.isErr())
|
|
400
392
|
return err(sandbox.error);
|
|
401
393
|
if (!sandbox.value.alreadyRunning) {
|
|
@@ -425,7 +417,7 @@ export function createDockerSandboxOps(config) {
|
|
|
425
417
|
backend: "docker",
|
|
426
418
|
callbackSecret,
|
|
427
419
|
};
|
|
428
|
-
const registered = await trySandbox(() => config.registerSandbox(
|
|
420
|
+
const registered = await trySandbox(() => config.registerSandbox(session, spec, ref), createFailed);
|
|
429
421
|
if (registered.isErr())
|
|
430
422
|
return err(registered.error);
|
|
431
423
|
return ok(ref);
|
|
@@ -450,7 +442,7 @@ export function createDockerSandboxOps(config) {
|
|
|
450
442
|
.map((c) => {
|
|
451
443
|
const labels = c.Labels ?? {};
|
|
452
444
|
return {
|
|
453
|
-
sandboxId: labels[
|
|
445
|
+
sandboxId: labels[SANDBOX_ID_LABEL] ?? "",
|
|
454
446
|
// Docker label values are unconstrained, so the id is stored verbatim.
|
|
455
447
|
ownerWorkflowId: labels[OWNER_WORKFLOW_LABEL] ?? null,
|
|
456
448
|
// Docker reports `Created` as unix seconds.
|
|
@@ -16,18 +16,9 @@ import { BatchV1Api, CoreV1Api, type V1Toleration } from "@kubernetes/client-nod
|
|
|
16
16
|
import { ResultAsync } from "neverthrow";
|
|
17
17
|
import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
18
18
|
import { type SandboxError } from "./sandbox-error.js";
|
|
19
|
-
import type {
|
|
19
|
+
import type { SpawnSession } from "../auth/types.js";
|
|
20
|
+
import type { FarmSource, ManagedSandbox, SandboxIdentity, SandboxLabels, SandboxLiveness, SandboxRef, SandboxSpec, SandboxTransport, ToolchainSource } from "./types.js";
|
|
20
21
|
import type { Logger } from "../lib/logger.js";
|
|
21
|
-
/**
|
|
22
|
-
* Coerce an arbitrary identifier into a valid K8s label value:
|
|
23
|
-
* ≤63 chars, `[A-Za-z0-9._-]` only, starting and ending alphanumeric.
|
|
24
|
-
* Without it one malformed value gets the whole Job rejected at admission.
|
|
25
|
-
*
|
|
26
|
-
* Lossy by construction: `:` becomes `-` and anything past 63 chars is dropped,
|
|
27
|
-
* neither of which is recoverable. No label value may therefore be used as a
|
|
28
|
-
* lookup key — see {@link OWNER_WORKFLOW_ANNOTATION}.
|
|
29
|
-
*/
|
|
30
|
-
export declare function sanitizeLabelValue(value: string): string;
|
|
31
22
|
export interface K8sClientConfig {
|
|
32
23
|
/** Operational logging seam; omitted falls back to no-op. */
|
|
33
24
|
readonly logger?: Logger;
|
|
@@ -93,7 +84,7 @@ export interface K8sClientConfig {
|
|
|
93
84
|
/** Injected for tests. */
|
|
94
85
|
batchApi?: BatchV1Api;
|
|
95
86
|
coreApi?: CoreV1Api;
|
|
96
|
-
registerSandbox: (
|
|
87
|
+
registerSandbox: (session: SpawnSession, spec: SandboxSpec, ref: SandboxRef) => Promise<void>;
|
|
97
88
|
}
|
|
98
89
|
/** Clock and sleep seams, so a test can drive the slow-pod warning without waiting a minute. */
|
|
99
90
|
export interface WaitForPodReadyDeps {
|
|
@@ -106,7 +97,7 @@ export declare function waitForPodReady(coreApi: CoreV1Api, namespace: string, s
|
|
|
106
97
|
podName: string;
|
|
107
98
|
}, SandboxError>;
|
|
108
99
|
export declare function createK8sSandboxOps(config: K8sClientConfig): {
|
|
109
|
-
createSandbox(
|
|
100
|
+
createSandbox(session: SpawnSession, spec: SandboxSpec, identity: SandboxIdentity, hostLabels: SandboxLabels): ResultAsync<SandboxRef, SandboxError>;
|
|
110
101
|
teardown(ref: SandboxRef): ResultAsync<void, SandboxError>;
|
|
111
102
|
teardownById(sandboxId: string): ResultAsync<void, SandboxError>;
|
|
112
103
|
isAlive(ref: SandboxRef): ResultAsync<SandboxLiveness, SandboxError>;
|
|
@@ -17,7 +17,8 @@ import { BatchV1Api, CoreV1Api, KubeConfig, } from "@kubernetes/client-node";
|
|
|
17
17
|
import { ResultAsync, err, ok } from "neverthrow";
|
|
18
18
|
import { trySandbox } from "./sandbox-error.js";
|
|
19
19
|
import { readFarmLock, resolveFarmSource } from "./farm.js";
|
|
20
|
-
import {
|
|
20
|
+
import { harnessLabels, MANAGED_BY_LABEL, MANAGED_BY_VALUE, mergeLabels, OWNER_WORKFLOW_KEY, SANDBOX_ID_LABEL } from "./labels.js";
|
|
21
|
+
import { buildMountPlan, buildSessionSubPaths, mountCoordsOf } from "./mount-plan.js";
|
|
21
22
|
import { threadLimitEnv } from "./thread-env.js";
|
|
22
23
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
23
24
|
/** Read the originating HTTP status off any `SandboxError` variant that carries one. */
|
|
@@ -37,34 +38,14 @@ const POD_POLL_INTERVAL_MS = 1_000;
|
|
|
37
38
|
* and the run pays that minute before any command executes.
|
|
38
39
|
*/
|
|
39
40
|
const POD_READY_SLOW_MS = 60_000;
|
|
40
|
-
const MANAGED_BY_LABEL = "app.kubernetes.io/managed-by";
|
|
41
|
-
const MANAGED_BY_VALUE = "cortex";
|
|
42
41
|
/**
|
|
43
42
|
* Annotation key of the owning DBOS workflow id. An annotation value has neither
|
|
44
43
|
* a length cap nor a charset restriction, so the id round-trips verbatim and can
|
|
45
44
|
* be handed back to `DBOS.getWorkflowStatus`. Deliberately not a label: nothing
|
|
46
|
-
* selects on it, and a
|
|
45
|
+
* selects on it, and a DBOS workflow id can be longer than 63 characters and can
|
|
46
|
+
* hold `:`, thus it is not a valid label value.
|
|
47
47
|
*/
|
|
48
|
-
const OWNER_WORKFLOW_ANNOTATION =
|
|
49
|
-
const RUN_ID_LABEL = "cortex/run-id";
|
|
50
|
-
const STEP_ID_LABEL = "cortex/step-id";
|
|
51
|
-
const ANALYSIS_ID_LABEL = "cortex/analysis-id";
|
|
52
|
-
/**
|
|
53
|
-
* Coerce an arbitrary identifier into a valid K8s label value:
|
|
54
|
-
* ≤63 chars, `[A-Za-z0-9._-]` only, starting and ending alphanumeric.
|
|
55
|
-
* Without it one malformed value gets the whole Job rejected at admission.
|
|
56
|
-
*
|
|
57
|
-
* Lossy by construction: `:` becomes `-` and anything past 63 chars is dropped,
|
|
58
|
-
* neither of which is recoverable. No label value may therefore be used as a
|
|
59
|
-
* lookup key — see {@link OWNER_WORKFLOW_ANNOTATION}.
|
|
60
|
-
*/
|
|
61
|
-
export function sanitizeLabelValue(value) {
|
|
62
|
-
return value
|
|
63
|
-
.replace(/[^A-Za-z0-9._-]/g, "-")
|
|
64
|
-
.replace(/^[^A-Za-z0-9]+/, "")
|
|
65
|
-
.slice(0, 63)
|
|
66
|
-
.replace(/[^A-Za-z0-9]+$/, "");
|
|
67
|
-
}
|
|
48
|
+
const OWNER_WORKFLOW_ANNOTATION = OWNER_WORKFLOW_KEY;
|
|
68
49
|
/** The owner workflow id recorded on a Job, or null if it records none. */
|
|
69
50
|
function readOwnerWorkflowId(metadata) {
|
|
70
51
|
return metadata?.annotations?.[OWNER_WORKFLOW_ANNOTATION] ?? null;
|
|
@@ -129,21 +110,22 @@ function pvcSubPath(value, what) {
|
|
|
129
110
|
}
|
|
130
111
|
return value;
|
|
131
112
|
}
|
|
132
|
-
function buildJobSpec(
|
|
113
|
+
function buildJobSpec(session, sandboxSpec, hostLabels, config, identity, farm, libsMounted) {
|
|
133
114
|
const { sandboxId } = identity;
|
|
134
115
|
// The lock gate of the package-store spec: `farmPath` is PVC-relative,
|
|
135
116
|
// thus this host can prove the lock only through `libStorePvcRoot`. The
|
|
136
117
|
// caller runs that proof and hands the verdict in `libsMounted`. Without
|
|
137
118
|
// the root, the proof duty stays whole on the farm resolver — see
|
|
138
119
|
// {@link ResolveAnalysisFarm}.
|
|
139
|
-
const
|
|
120
|
+
const coords = mountCoordsOf(session, sandboxSpec);
|
|
121
|
+
const plan = buildMountPlan(coords, {
|
|
140
122
|
libs: libsMounted,
|
|
141
123
|
refs: !!config.refStorePvc,
|
|
142
124
|
toolchainSource: config.toolchainSource,
|
|
143
125
|
cache: libsMounted && farm?.cachePath !== undefined,
|
|
144
126
|
});
|
|
145
127
|
const transport = config.transport ?? "poll";
|
|
146
|
-
const spec =
|
|
128
|
+
const spec = sandboxSpec.resources;
|
|
147
129
|
// Composed as one record, not as a list of `{name, value}`. A duplicate name
|
|
148
130
|
// in the env of a container resolves at the kubelet, thus the later spread
|
|
149
131
|
// must win here, not there.
|
|
@@ -156,7 +138,7 @@ function buildJobSpec(meta, config, identity, farm, libsMounted) {
|
|
|
156
138
|
SANDBOX_CALLBACK_SECRET: identity.callbackSecret,
|
|
157
139
|
...threadLimitEnv(spec),
|
|
158
140
|
...plan.env,
|
|
159
|
-
...(
|
|
141
|
+
...(sandboxSpec.extraEnv ?? {}),
|
|
160
142
|
}).map(([name, value]) => ({ name, value }));
|
|
161
143
|
// requests == limits → Guaranteed QoS and a predictable OOM bound; also
|
|
162
144
|
// satisfies the namespace ResourceQuota, which rejects any pod that omits
|
|
@@ -172,7 +154,7 @@ function buildJobSpec(meta, config, identity, farm, libsMounted) {
|
|
|
172
154
|
const volumes = [];
|
|
173
155
|
const volumeMounts = [];
|
|
174
156
|
if (config.sessionPvc) {
|
|
175
|
-
const subPaths = buildSessionSubPaths(
|
|
157
|
+
const subPaths = buildSessionSubPaths(coords, workspaceSubPathFor(config, session.scope.analysisId));
|
|
176
158
|
volumes.push({
|
|
177
159
|
name: SESSION_VOLUME_NAME,
|
|
178
160
|
persistentVolumeClaim: { claimName: config.sessionPvc },
|
|
@@ -244,7 +226,7 @@ function buildJobSpec(meta, config, identity, farm, libsMounted) {
|
|
|
244
226
|
containers: [
|
|
245
227
|
{
|
|
246
228
|
name: "sandbox",
|
|
247
|
-
image:
|
|
229
|
+
image: sandboxSpec.image ?? config.image,
|
|
248
230
|
imagePullPolicy: "IfNotPresent",
|
|
249
231
|
env,
|
|
250
232
|
resources,
|
|
@@ -277,14 +259,8 @@ function buildJobSpec(meta, config, identity, farm, libsMounted) {
|
|
|
277
259
|
if (config.runtimeClassName)
|
|
278
260
|
podSpec.runtimeClassName = config.runtimeClassName;
|
|
279
261
|
// Pod-template placement is load-bearing: a cost reconciler allocates by pod
|
|
280
|
-
// labels, thus a Job-only label is invisible to it.
|
|
281
|
-
|
|
282
|
-
// malformed value makes the API server reject the whole Job at admission.
|
|
283
|
-
const attributionLabels = {
|
|
284
|
-
[ANALYSIS_ID_LABEL]: sanitizeLabelValue(meta.analysisId),
|
|
285
|
-
[RUN_ID_LABEL]: sanitizeLabelValue(meta.runId),
|
|
286
|
-
...Object.fromEntries(Object.entries(meta.podLabels ?? {}).map(([key, value]) => [key, sanitizeLabelValue(value)])),
|
|
287
|
-
};
|
|
262
|
+
// labels, thus a Job-only label is invisible to it.
|
|
263
|
+
const labels = mergeLabels(hostLabels, harnessLabels(session, sandboxId));
|
|
288
264
|
return {
|
|
289
265
|
apiVersion: "batch/v1",
|
|
290
266
|
kind: "Job",
|
|
@@ -292,26 +268,16 @@ function buildJobSpec(meta, config, identity, farm, libsMounted) {
|
|
|
292
268
|
name: sandboxId,
|
|
293
269
|
namespace: config.namespace,
|
|
294
270
|
annotations: {
|
|
295
|
-
[OWNER_WORKFLOW_ANNOTATION]:
|
|
296
|
-
},
|
|
297
|
-
labels: {
|
|
298
|
-
[MANAGED_BY_LABEL]: MANAGED_BY_VALUE,
|
|
299
|
-
role: "sandbox",
|
|
300
|
-
"cortex/sandbox-id": sandboxId,
|
|
301
|
-
[STEP_ID_LABEL]: sanitizeLabelValue(meta.stepId),
|
|
302
|
-
...attributionLabels,
|
|
271
|
+
[OWNER_WORKFLOW_ANNOTATION]: sandboxSpec.childWorkflowId,
|
|
303
272
|
},
|
|
273
|
+
labels,
|
|
304
274
|
},
|
|
305
275
|
spec: {
|
|
306
276
|
backoffLimit: 0,
|
|
307
277
|
ttlSecondsAfterFinished: 60,
|
|
308
278
|
template: {
|
|
309
279
|
metadata: {
|
|
310
|
-
labels
|
|
311
|
-
role: "sandbox",
|
|
312
|
-
"cortex/sandbox-id": sandboxId,
|
|
313
|
-
...attributionLabels,
|
|
314
|
-
},
|
|
280
|
+
labels,
|
|
315
281
|
},
|
|
316
282
|
spec: podSpec,
|
|
317
283
|
},
|
|
@@ -345,7 +311,7 @@ export function waitForPodReady(coreApi, namespace, sandboxId, deps = {}) {
|
|
|
345
311
|
while (now() < deadline) {
|
|
346
312
|
const listed = await trySandbox(() => coreApi.listNamespacedPod({
|
|
347
313
|
namespace,
|
|
348
|
-
labelSelector:
|
|
314
|
+
labelSelector: `${SANDBOX_ID_LABEL}=${sandboxId}`,
|
|
349
315
|
}), createFailed);
|
|
350
316
|
if (listed.isErr())
|
|
351
317
|
return err(listed.error);
|
|
@@ -386,7 +352,7 @@ export function waitForPodReady(coreApi, namespace, sandboxId, deps = {}) {
|
|
|
386
352
|
function existingPodIsTerminal(coreApi, namespace, sandboxId) {
|
|
387
353
|
return trySandbox(() => coreApi.listNamespacedPod({
|
|
388
354
|
namespace,
|
|
389
|
-
labelSelector:
|
|
355
|
+
labelSelector: `${SANDBOX_ID_LABEL}=${sandboxId}`,
|
|
390
356
|
}), (status, cause) => ({
|
|
391
357
|
type: "container_create_failed",
|
|
392
358
|
op: "k8s.existingPodIsTerminal",
|
|
@@ -484,15 +450,16 @@ function createOrAdoptJob(batchApi, coreApi, namespace, sandboxId, ownerWorkflow
|
|
|
484
450
|
export function createK8sSandboxOps(config) {
|
|
485
451
|
const { batchApi, coreApi } = config.batchApi && config.coreApi ? { batchApi: config.batchApi, coreApi: config.coreApi } : buildKubeApi();
|
|
486
452
|
return {
|
|
487
|
-
createSandbox(
|
|
453
|
+
createSandbox(session, spec, identity, hostLabels) {
|
|
488
454
|
return new ResultAsync((async () => {
|
|
489
455
|
const { sandboxId } = identity;
|
|
456
|
+
const analysisId = session.scope.analysisId;
|
|
490
457
|
// Farm resolution comes before any cluster work: a resolver
|
|
491
458
|
// refusal and a resolver throw refuse the whole call, and no
|
|
492
459
|
// Job is made.
|
|
493
460
|
let farm;
|
|
494
461
|
if (config.libStorePvc) {
|
|
495
|
-
const resolved = await resolveFarmSource(config.farmSource,
|
|
462
|
+
const resolved = await resolveFarmSource(config.farmSource, analysisId, "k8s.createSandbox");
|
|
496
463
|
if (resolved.isErr())
|
|
497
464
|
return err(resolved.error);
|
|
498
465
|
farm = resolved.value;
|
|
@@ -517,7 +484,7 @@ export function createK8sSandboxOps(config) {
|
|
|
517
484
|
return err({
|
|
518
485
|
type: "farm_unusable",
|
|
519
486
|
op: "k8s.createSandbox",
|
|
520
|
-
analysisId
|
|
487
|
+
analysisId,
|
|
521
488
|
farmPath: farm.farmPath,
|
|
522
489
|
lockPath: lock.error.lockPath,
|
|
523
490
|
lockError: lock.error.type,
|
|
@@ -530,8 +497,8 @@ export function createK8sSandboxOps(config) {
|
|
|
530
497
|
.warn("the farm of the analysis has no usable inflexa.lock at sandbox creation — mounting no package store (sandbox degrades to available:false)", { libStorePvc: config.libStorePvc, farmPath: farm.farmPath, lockError: lock.error.type, sandboxId });
|
|
531
498
|
}
|
|
532
499
|
}
|
|
533
|
-
const job = buildJobSpec(
|
|
534
|
-
const adopted = await createOrAdoptJob(batchApi, coreApi, config.namespace, sandboxId,
|
|
500
|
+
const job = buildJobSpec(session, spec, hostLabels, config, identity, farm, libsMounted);
|
|
501
|
+
const adopted = await createOrAdoptJob(batchApi, coreApi, config.namespace, sandboxId, spec.childWorkflowId, job);
|
|
535
502
|
if (adopted.isErr())
|
|
536
503
|
return err(adopted.error);
|
|
537
504
|
// A Job whose pod never schedules (quota rejection, no nodes, spot
|
|
@@ -549,7 +516,7 @@ export function createK8sSandboxOps(config) {
|
|
|
549
516
|
backend: "k8s",
|
|
550
517
|
callbackSecret: identity.callbackSecret,
|
|
551
518
|
};
|
|
552
|
-
const registered = await trySandbox(() => config.registerSandbox(
|
|
519
|
+
const registered = await trySandbox(() => config.registerSandbox(session, spec, ref), (status, cause) => ({
|
|
553
520
|
type: "container_create_failed",
|
|
554
521
|
op: "k8s.registerSandbox",
|
|
555
522
|
sandboxId,
|
|
@@ -586,7 +553,7 @@ export function createK8sSandboxOps(config) {
|
|
|
586
553
|
const labels = j.metadata?.labels ?? {};
|
|
587
554
|
const ts = j.metadata?.creationTimestamp;
|
|
588
555
|
return {
|
|
589
|
-
sandboxId: labels[
|
|
556
|
+
sandboxId: labels[SANDBOX_ID_LABEL] ?? j.metadata?.name ?? "",
|
|
590
557
|
ownerWorkflowId: readOwnerWorkflowId(j.metadata),
|
|
591
558
|
createdAtMs: ts ? new Date(ts).getTime() : null,
|
|
592
559
|
};
|
|
@@ -601,7 +568,7 @@ export function createK8sSandboxOps(config) {
|
|
|
601
568
|
function isAliveById(sandboxId) {
|
|
602
569
|
return trySandbox(() => coreApi.listNamespacedPod({
|
|
603
570
|
namespace: config.namespace,
|
|
604
|
-
labelSelector:
|
|
571
|
+
labelSelector: `${SANDBOX_ID_LABEL}=${sandboxId}`,
|
|
605
572
|
}), (status, cause) => ({
|
|
606
573
|
type: "liveness_failed",
|
|
607
574
|
op: "k8s.isAlive",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The labels of a sandbox, the same on the Docker backend and on the K8s
|
|
3
|
+
* backend (sandbox-labels spec).
|
|
4
|
+
*
|
|
5
|
+
* Each harness value is a valid K8s label value by construction. A run id is a
|
|
6
|
+
* UUID or a short literal of the harness, a step id obeys the step-id rule of
|
|
7
|
+
* the plan validator, and the harness makes the sandbox id. The analysis id
|
|
8
|
+
* comes from the host, thus on K8s the host must give one that is a valid
|
|
9
|
+
* label value.
|
|
10
|
+
*
|
|
11
|
+
* The host labels merge first and the harness labels last, thus a harness key
|
|
12
|
+
* wins a clash. The harness stamps each host value as the host gives it and
|
|
13
|
+
* changes no value: a host reconciler uses a label value as a lookup key, and
|
|
14
|
+
* a changed value cannot find its record. On K8s the API server does a check of
|
|
15
|
+
* each value at admission, and it is the authority.
|
|
16
|
+
*/
|
|
17
|
+
import type { SpawnSession } from "../auth/types.js";
|
|
18
|
+
import type { SandboxLabels } from "./types.js";
|
|
19
|
+
export declare const MANAGED_BY_LABEL = "app.kubernetes.io/managed-by";
|
|
20
|
+
export declare const MANAGED_BY_VALUE = "cortex";
|
|
21
|
+
export declare const SANDBOX_ID_LABEL = "cortex/sandbox-id";
|
|
22
|
+
export declare const ANALYSIS_ID_LABEL = "cortex/analysis-id";
|
|
23
|
+
export declare const RUN_ID_LABEL = "cortex/run-id";
|
|
24
|
+
export declare const STEP_ID_LABEL = "cortex/step-id";
|
|
25
|
+
/**
|
|
26
|
+
* A Docker label value has no limit, thus Docker keeps the owner workflow id as
|
|
27
|
+
* a label. A DBOS workflow id can be longer than 63 characters and can hold
|
|
28
|
+
* `:`, thus K8s keeps it as a Job annotation.
|
|
29
|
+
*/
|
|
30
|
+
export declare const OWNER_WORKFLOW_KEY = "cortex/owner-workflow-id";
|
|
31
|
+
/** The harness never applies it to a host value. */
|
|
32
|
+
export declare function isLabelValue(value: string): boolean;
|
|
33
|
+
export declare function harnessLabels(session: SpawnSession, sandboxId: string): Record<string, string>;
|
|
34
|
+
export declare function mergeLabels(hostLabels: SandboxLabels, harness: Readonly<Record<string, string>>): Record<string, string>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The labels of a sandbox, the same on the Docker backend and on the K8s
|
|
3
|
+
* backend (sandbox-labels spec).
|
|
4
|
+
*
|
|
5
|
+
* Each harness value is a valid K8s label value by construction. A run id is a
|
|
6
|
+
* UUID or a short literal of the harness, a step id obeys the step-id rule of
|
|
7
|
+
* the plan validator, and the harness makes the sandbox id. The analysis id
|
|
8
|
+
* comes from the host, thus on K8s the host must give one that is a valid
|
|
9
|
+
* label value.
|
|
10
|
+
*
|
|
11
|
+
* The host labels merge first and the harness labels last, thus a harness key
|
|
12
|
+
* wins a clash. The harness stamps each host value as the host gives it and
|
|
13
|
+
* changes no value: a host reconciler uses a label value as a lookup key, and
|
|
14
|
+
* a changed value cannot find its record. On K8s the API server does a check of
|
|
15
|
+
* each value at admission, and it is the authority.
|
|
16
|
+
*/
|
|
17
|
+
export const MANAGED_BY_LABEL = "app.kubernetes.io/managed-by";
|
|
18
|
+
export const MANAGED_BY_VALUE = "cortex";
|
|
19
|
+
export const SANDBOX_ID_LABEL = "cortex/sandbox-id";
|
|
20
|
+
export const ANALYSIS_ID_LABEL = "cortex/analysis-id";
|
|
21
|
+
export const RUN_ID_LABEL = "cortex/run-id";
|
|
22
|
+
export const STEP_ID_LABEL = "cortex/step-id";
|
|
23
|
+
/**
|
|
24
|
+
* A Docker label value has no limit, thus Docker keeps the owner workflow id as
|
|
25
|
+
* a label. A DBOS workflow id can be longer than 63 characters and can hold
|
|
26
|
+
* `:`, thus K8s keeps it as a Job annotation.
|
|
27
|
+
*/
|
|
28
|
+
export const OWNER_WORKFLOW_KEY = "cortex/owner-workflow-id";
|
|
29
|
+
/** A valid K8s label value: 1 to 63 characters, `[A-Za-z0-9._-]` only, a letter or a digit at each end. */
|
|
30
|
+
const LABEL_VALUE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,61}[A-Za-z0-9])?$/;
|
|
31
|
+
/** The harness never applies it to a host value. */
|
|
32
|
+
export function isLabelValue(value) {
|
|
33
|
+
return LABEL_VALUE.test(value);
|
|
34
|
+
}
|
|
35
|
+
export function harnessLabels(session, sandboxId) {
|
|
36
|
+
return {
|
|
37
|
+
[MANAGED_BY_LABEL]: MANAGED_BY_VALUE,
|
|
38
|
+
role: "sandbox",
|
|
39
|
+
[SANDBOX_ID_LABEL]: sandboxId,
|
|
40
|
+
[ANALYSIS_ID_LABEL]: session.scope.analysisId,
|
|
41
|
+
[RUN_ID_LABEL]: session.runFrame.runId,
|
|
42
|
+
[STEP_ID_LABEL]: session.runFrame.stepId,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function mergeLabels(hostLabels, harness) {
|
|
46
|
+
return { ...hostLabels, ...harness };
|
|
47
|
+
}
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* resolved root directly, K8s addresses it as a `subPath` into the session PVC
|
|
28
28
|
* (see {@link buildSessionSubPaths}).
|
|
29
29
|
*/
|
|
30
|
-
import type {
|
|
30
|
+
import type { SpawnSession } from "../auth/types.js";
|
|
31
|
+
import type { SandboxSpec, ToolchainSource } from "./types.js";
|
|
31
32
|
export declare const STEP_SUBDIRS: readonly ["output", "scripts", "figures", "logs", "notebooks"];
|
|
32
33
|
/**
|
|
33
34
|
* Container mountpoints of the two read-only stores. Exported because they are
|
|
@@ -64,6 +65,7 @@ export interface MountPlanCoords {
|
|
|
64
65
|
*/
|
|
65
66
|
writableTail?: string;
|
|
66
67
|
}
|
|
68
|
+
export declare function mountCoordsOf(session: SpawnSession, spec: Pick<SandboxSpec, "readOnly" | "writableTail">): MountPlanCoords;
|
|
67
69
|
export interface MountPlanStores {
|
|
68
70
|
/** Lib store is mounted at `/mnt/libs`, with the farm nested inside it, and its env is emitted. */
|
|
69
71
|
libs: boolean;
|
|
@@ -47,6 +47,15 @@ export const FARM_CACHE_CONTAINER_PATH = "/mnt/libs/cache";
|
|
|
47
47
|
export function farmContainerPath(toolchainSource) {
|
|
48
48
|
return toolchainSource === "image" ? `${LIBS_CONTAINER_PATH}/farm` : `${LIBS_CONTAINER_PATH}/current`;
|
|
49
49
|
}
|
|
50
|
+
export function mountCoordsOf(session, spec) {
|
|
51
|
+
return {
|
|
52
|
+
analysisId: session.scope.analysisId,
|
|
53
|
+
runId: session.runFrame.runId,
|
|
54
|
+
stepId: session.runFrame.stepId,
|
|
55
|
+
...(spec.readOnly !== undefined ? { readOnly: spec.readOnly } : {}),
|
|
56
|
+
...(spec.writableTail !== undefined ? { writableTail: spec.writableTail } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
50
59
|
/**
|
|
51
60
|
* The step tail beneath an analysis's workspace root, in both container and
|
|
52
61
|
* PVC-relative space. The harness owns this layout (workspace-layout spec).
|
|
@@ -25,21 +25,21 @@
|
|
|
25
25
|
* propagate untouched. `await-exec.ts` is NOT converted; `trySandbox` only
|
|
26
26
|
* ever wraps the single SDK/HTTP call handed to it.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* untouched by this wave.
|
|
28
|
+
* `createSandbox` of the public `SandboxClient` gives each `SandboxError` as
|
|
29
|
+
* an `err` value, because a refusal of the label hook must reach the spawn path
|
|
30
|
+
* as a value. A spawn path splits the result with `keepSuspendingRefusal`: a
|
|
31
|
+
* refusal with the suspend flag stays a value, and each other failure throws
|
|
32
|
+
* as a `SandboxFailure`.
|
|
33
|
+
* Each other op keeps a `Promise<T>` signature. `create-sandbox.ts` is the
|
|
34
|
+
* composition seam: it builds the backend ops, maps each `err` of such an op to
|
|
35
|
+
* a `SandboxFailure`, and `unwrapOrThrow`s it, so the seam throws the described
|
|
36
|
+
* failure with the variant on `.cause` and on `.error`. A `catch` never sees
|
|
37
|
+
* the raw variant. (`submitExec` is the exception — it is already its own DBOS
|
|
38
|
+
* step and throws directly, so the seam forwards its `Promise<void>` as-is.)
|
|
40
39
|
*/
|
|
41
|
-
import { ResultAsync } from "neverthrow";
|
|
40
|
+
import { ResultAsync, type Result } from "neverthrow";
|
|
42
41
|
import type { FarmLockError } from "./farm.js";
|
|
42
|
+
import type { SandboxRef } from "./types.js";
|
|
43
43
|
/**
|
|
44
44
|
* A sandbox backend failure. Absence / idempotent "already gone" is NOT
|
|
45
45
|
* modelled — those ride the OK channel (`ok(false)` for a dead/missing machine,
|
|
@@ -82,6 +82,18 @@ export type SandboxError = {
|
|
|
82
82
|
readonly sandboxId?: string;
|
|
83
83
|
readonly status?: number;
|
|
84
84
|
readonly cause: unknown;
|
|
85
|
+
} | {
|
|
86
|
+
/**
|
|
87
|
+
* The label hook of the host refused the spawn. The client made no
|
|
88
|
+
* step tree and called no backend. `reason` is the text of the host,
|
|
89
|
+
* and `suspend` asks the caller to suspend the work in place of a
|
|
90
|
+
* failure (see `lib/hooks.ts`).
|
|
91
|
+
*/
|
|
92
|
+
readonly type: "labels_refused";
|
|
93
|
+
readonly op: string;
|
|
94
|
+
readonly reason: string;
|
|
95
|
+
readonly suspend: boolean;
|
|
96
|
+
readonly cause?: unknown;
|
|
85
97
|
} | {
|
|
86
98
|
/**
|
|
87
99
|
* The farm source refused the analysis: the resolver returned
|
|
@@ -137,6 +149,17 @@ export declare class SandboxFailure extends Error {
|
|
|
137
149
|
readonly error: SandboxError;
|
|
138
150
|
constructor(error: SandboxError);
|
|
139
151
|
}
|
|
152
|
+
export type SuspendingRefusal = Extract<SandboxError, {
|
|
153
|
+
readonly type: "labels_refused";
|
|
154
|
+
}> & {
|
|
155
|
+
readonly suspend: true;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Split the result of a spawn at a DBOS boundary. Call it inside the step of
|
|
159
|
+
* the spawn, before the checkpoint: the cause of a backend failure does not
|
|
160
|
+
* survive the serialization of a `Result`.
|
|
161
|
+
*/
|
|
162
|
+
export declare function keepSuspendingRefusal(result: Result<SandboxRef, SandboxError>): Result<SandboxRef, SuspendingRefusal>;
|
|
140
163
|
/**
|
|
141
164
|
* Wrap a single sandbox backend call (Docker SDK op, K8s API op, or the
|
|
142
165
|
* `/exec` POST). A throw becomes `err(toError(status, cause))`; the caller
|