@inflexa-ai/harness 0.46.2 → 0.47.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 -1
- package/dist/agents/conversation-agent.d.ts +3 -8
- package/dist/agents/conversation-agent.js +24 -4
- package/dist/agents/report-session-agent.d.ts +11 -14
- package/dist/agents/report-session-agent.js +30 -17
- package/dist/agents/sandbox/shared.d.ts +18 -13
- package/dist/agents/sandbox/shared.js +42 -27
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/agents/sandbox-catalog.d.ts +2 -0
- package/dist/agents/sandbox-catalog.js +30 -1
- package/dist/app/synthesize-run.d.ts +10 -6
- package/dist/app/synthesize-run.js +4 -2
- package/dist/contracts/analogy-report.d.ts +46 -2
- package/dist/contracts/analogy-report.js +31 -25
- package/dist/contracts/chat-parts.d.ts +3 -14
- package/dist/contracts/report-reference.d.ts +2 -1
- package/dist/contracts/report-reference.js +2 -1
- package/dist/contracts/schemas/chat-parts.d.ts +0 -27
- package/dist/contracts/schemas/chat-parts.js +1 -3
- package/dist/execution/run-canceler.d.ts +2 -0
- package/dist/execution/run-canceler.js +9 -0
- package/dist/execution/run-synthesis.d.ts +36 -8
- package/dist/execution/run-synthesis.js +114 -32
- package/dist/index.d.ts +2 -5
- package/dist/index.js +3 -6
- package/dist/input-scan/enrich.d.ts +5 -10
- package/dist/input-scan/enrich.js +12 -8
- package/dist/lib/markdown-pdf.d.ts +27 -0
- package/dist/lib/markdown-pdf.js +236 -0
- package/dist/lib/metrics.d.ts +31 -0
- package/dist/lib/metrics.js +64 -0
- package/dist/lib/pdf-text.d.ts +23 -0
- package/dist/lib/pdf-text.js +34 -0
- package/dist/lib/regulatory-corpus/fetch-fda-guidance.js +3 -5
- package/dist/lib/regulatory-corpus/fetch-ich.js +3 -5
- package/dist/loop/run-loop.d.ts +5 -0
- package/dist/loop/run-loop.js +30 -2
- package/dist/loop/tool-name-repair.d.ts +11 -0
- package/dist/loop/tool-name-repair.js +23 -0
- package/dist/loop/types.d.ts +14 -0
- package/dist/memory/card-builders.d.ts +1 -1
- package/dist/prompts/SOUL.d.ts +6 -6
- package/dist/prompts/SOUL.js +22 -121
- package/dist/prompts/analogical-reasoner.d.ts +1 -1
- package/dist/prompts/analogical-reasoner.js +35 -101
- package/dist/prompts/briefing.d.ts +20 -11
- package/dist/prompts/briefing.js +36 -19
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +103 -383
- package/dist/prompts/planner.d.ts +1 -5
- package/dist/prompts/planner.js +45 -128
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +26 -57
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -3
- package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/cheminformatics-agent.js +12 -47
- package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
- package/dist/prompts/sandbox/chromatin-agent.js +4 -5
- package/dist/prompts/sandbox/data-profiler.js +11 -34
- package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
- package/dist/prompts/sandbox/dna-methylation-agent.js +3 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +23 -73
- package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
- package/dist/prompts/sandbox/enrichment-agent.js +3 -1
- package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
- package/dist/prompts/sandbox/genomic-variant-agent.js +3 -4
- package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/immune-profiling-agent.js +5 -4
- package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/metabolomics-agent.js +3 -1
- package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
- package/dist/prompts/sandbox/microbiome-agent.js +3 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +3 -1
- package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multimodal-sc-agent.js +3 -1
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +3 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +4 -7
- package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/proteomics-agent.js +3 -1
- package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
- package/dist/prompts/sandbox/scientific-executor.js +0 -12
- package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
- package/dist/prompts/sandbox/single-cell-agent.js +3 -1
- package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/spatial-omics-agent.js +3 -1
- package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/statistical-modeling-agent.js +4 -4
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +10 -29
- package/dist/prompts/sandbox-standards.d.ts +15 -18
- package/dist/prompts/sandbox-standards.js +96 -305
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +24 -90
- package/dist/provenance/collector.d.ts +2 -5
- package/dist/provenance/collector.js +2 -5
- package/dist/provenance/seam.d.ts +36 -1
- package/dist/provenance/workspace-reads.d.ts +36 -0
- package/dist/provenance/workspace-reads.js +33 -0
- package/dist/report-model/production-resolver.d.ts +22 -52
- package/dist/report-model/production-resolver.js +43 -112
- package/dist/report-render/chart-export.d.ts +9 -0
- package/dist/report-render/chart-export.js +1 -1
- package/dist/report-render/views/chart-view.d.ts +1 -0
- package/dist/report-render/views/chart-view.js +1 -0
- package/dist/report-render/views/lineage.d.ts +1 -0
- package/dist/report-render/views/page-view.d.ts +1 -0
- package/dist/report-render/views/page-view.js +1 -0
- package/dist/report-render/views/prose.d.ts +1 -0
- package/dist/report-render/views/prose.js +1 -0
- package/dist/report-render/views/references-view.d.ts +1 -0
- package/dist/report-render/views/values.d.ts +1 -0
- package/dist/report-render/views/values.js +1 -0
- package/dist/runtime/assemble.d.ts +20 -9
- package/dist/runtime/assemble.js +47 -38
- package/dist/sandbox/client.d.ts +5 -3
- package/dist/sandbox/create-sandbox.d.ts +7 -12
- package/dist/sandbox/create-sandbox.js +15 -37
- package/dist/sandbox/docker-client.d.ts +1 -3
- package/dist/sandbox/docker-client.js +3 -5
- package/dist/sandbox/exec-outcome.d.ts +11 -11
- package/dist/sandbox/exec-outcome.js +22 -22
- package/dist/sandbox/exec.d.ts +5 -0
- package/dist/sandbox/exec.js +9 -1
- package/dist/sandbox/identity.d.ts +4 -16
- package/dist/sandbox/identity.js +5 -21
- package/dist/sandbox/k8s-client.d.ts +0 -1
- package/dist/sandbox/k8s-client.js +8 -13
- package/dist/sandbox/lease-names.d.ts +6 -0
- package/dist/sandbox/lease-names.js +12 -0
- package/dist/sandbox/leases.d.ts +105 -0
- package/dist/sandbox/leases.js +519 -0
- package/dist/sandbox/liveness.d.ts +5 -2
- package/dist/sandbox/mount-plan.d.ts +10 -1
- package/dist/sandbox/mount-plan.js +7 -1
- package/dist/sandbox/reconciler.d.ts +58 -0
- package/dist/sandbox/reconciler.js +153 -0
- package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
- package/dist/sandbox/types.d.ts +25 -18
- package/dist/sandbox/types.js +10 -4
- package/dist/schemas/plan-schemas.d.ts +0 -21
- package/dist/schemas/plan-schemas.js +2 -3
- package/dist/schemas/run-synthesis.d.ts +2 -2
- package/dist/schemas/run-synthesis.js +3 -3
- package/dist/schemas/validate-plan.d.ts +1 -14
- package/dist/schemas/validate-plan.js +5 -24
- package/dist/schemas/workflow-state.d.ts +3 -32
- package/dist/schemas/workflow-state.js +4 -13
- package/dist/state/index.d.ts +0 -1
- package/dist/state/index.js +0 -1
- package/dist/state/migrations/20261030120000_lease_the_sandboxes.d.ts +7 -0
- package/dist/state/migrations/20261030120000_lease_the_sandboxes.js +34 -0
- package/dist/state/migrations/20261030120100_drop_the_report_derivations.d.ts +5 -0
- package/dist/state/migrations/20261030120100_drop_the_report_derivations.js +9 -0
- package/dist/state/migrations/20261030120200_keep_the_report_derivations.d.ts +6 -0
- package/dist/state/migrations/20261030120200_keep_the_report_derivations.js +10 -0
- package/dist/state/migrations/index.js +6 -0
- package/dist/state/purge-analysis.d.ts +4 -2
- package/dist/state/purge-analysis.js +22 -11
- package/dist/state/sandbox-leases.d.ts +81 -0
- package/dist/state/sandbox-leases.js +247 -0
- package/dist/state/schema.d.ts +1 -13
- package/dist/state/schema.js +1 -6
- package/dist/state/step-executions.d.ts +0 -2
- package/dist/state/step-executions.js +1 -4
- package/dist/tasks/data-profile.d.ts +40 -2
- package/dist/tasks/data-profile.js +89 -87
- package/dist/tools/ad-hoc-router.d.ts +1 -6
- package/dist/tools/ad-hoc-router.js +5 -48
- package/dist/tools/bio/comptox.js +1 -1
- package/dist/tools/bio/drug-gene-interactions.js +1 -1
- package/dist/tools/bio/gene-disease-evidence.js +1 -1
- package/dist/tools/bio/keys.d.ts +1 -1
- package/dist/tools/bio/pubmed.js +31 -0
- package/dist/tools/bio/search-gene.js +17 -0
- package/dist/tools/bio/search-geo-datasets.js +13 -0
- package/dist/tools/bio/search-regulatory-approvals.js +1 -1
- package/dist/tools/define-tool.d.ts +25 -2
- package/dist/tools/define-tool.js +11 -1
- package/dist/tools/display/show-user.js +1 -1
- package/dist/tools/execute-analysis.d.ts +1 -3
- package/dist/tools/execute-analysis.js +5 -33
- package/dist/tools/report-session/index.d.ts +0 -1
- package/dist/tools/report-session/index.js +0 -1
- package/dist/tools/report-session/script-tools.d.ts +30 -0
- package/dist/tools/report-session/script-tools.js +177 -0
- package/dist/tools/research/generate-analogy-report.d.ts +16 -9
- package/dist/tools/research/generate-analogy-report.js +111 -49
- package/dist/tools/research/generate-plan.d.ts +102 -7
- package/dist/tools/research/generate-plan.js +74 -43
- package/dist/tools/research/index.d.ts +4 -3
- package/dist/tools/research/index.js +4 -3
- package/dist/tools/research/inspect-run.d.ts +0 -1
- package/dist/tools/research/inspect-run.js +5 -10
- package/dist/tools/research/search-arxiv.js +15 -0
- package/dist/tools/research/search-github-repos.js +13 -0
- package/dist/tools/research/search-semantic-scholar.js +16 -0
- package/dist/tools/research/synthesis-lookups.d.ts +21 -0
- package/dist/tools/research/synthesis-lookups.js +29 -0
- package/dist/tools/research/web-fetch.d.ts +1 -1
- package/dist/tools/research/web-fetch.js +14 -12
- package/dist/tools/sandbox/lease-tools.d.ts +32 -0
- package/dist/tools/sandbox/lease-tools.js +60 -0
- package/dist/tools/script/codemode-worker.d.ts +5 -0
- package/dist/tools/script/codemode-worker.js +5 -0
- package/dist/tools/script/file-tools.d.ts +100 -0
- package/dist/tools/script/file-tools.js +377 -0
- package/dist/tools/script/metrics.d.ts +21 -0
- package/dist/tools/script/metrics.js +38 -0
- package/dist/tools/script/quickjs.wasm +0 -0
- package/dist/tools/script/run-script.d.ts +76 -0
- package/dist/tools/script/run-script.js +326 -0
- package/dist/tools/script/scratch.d.ts +81 -0
- package/dist/tools/script/scratch.js +142 -0
- package/dist/tools/start-report-session.js +1 -1
- package/dist/tools/workspace/edit-file.js +2 -1
- package/dist/tools/workspace/execute-command.d.ts +9 -13
- package/dist/tools/workspace/execute-command.js +23 -19
- package/dist/tools/workspace/file-stat.js +1 -1
- package/dist/tools/workspace/grep.d.ts +5 -2
- package/dist/tools/workspace/grep.js +19 -10
- package/dist/tools/workspace/mutator.d.ts +48 -4
- package/dist/tools/workspace/mutator.js +43 -12
- package/dist/tools/workspace/read-file.d.ts +5 -2
- package/dist/tools/workspace/read-file.js +15 -11
- package/dist/tools/workspace/read-surface.d.ts +17 -0
- package/dist/tools/workspace/read-surface.js +17 -0
- package/dist/tools/workspace/result-bounds.d.ts +1 -0
- package/dist/tools/workspace/run-exec.d.ts +12 -13
- package/dist/tools/workspace/run-exec.js +13 -13
- package/dist/tools/workspace/scan-inputs.d.ts +2 -5
- package/dist/tools/workspace/scan-inputs.js +2 -3
- package/dist/tools/workspace/show-file.js +1 -1
- package/dist/tools/workspace/show-plan.js +4 -3
- package/dist/tools/workspace/write-file.js +2 -1
- package/dist/workflows/execute-analysis-scheduler.d.ts +0 -26
- package/dist/workflows/execute-analysis-scheduler.js +12 -32
- package/dist/workflows/execute-analysis.d.ts +17 -35
- package/dist/workflows/execute-analysis.js +13 -107
- package/dist/workflows/pause.js +29 -22
- package/dist/workflows/sandbox-step.d.ts +16 -31
- package/dist/workflows/sandbox-step.js +36 -71
- package/dist/workflows/test/sql-writes.d.ts +0 -1
- package/dist/workflows/test/sql-writes.js +0 -2
- package/dist/workspace/filesystem.d.ts +2 -2
- package/dist/workspace/filesystem.js +10 -5
- package/dist/workspace/paths.d.ts +19 -1
- package/dist/workspace/paths.js +22 -1
- package/package.json +6 -3
- package/dist/prompts/literature-reviewer.d.ts +0 -1
- package/dist/prompts/literature-reviewer.js +0 -114
- package/dist/sandbox/reaper.d.ts +0 -81
- package/dist/sandbox/reaper.js +0 -161
- package/dist/sandbox/step-sandbox.d.ts +0 -47
- package/dist/sandbox/step-sandbox.js +0 -53
- package/dist/state/active-sandboxes.d.ts +0 -33
- package/dist/state/active-sandboxes.js +0 -65
- package/dist/tasks/extract-values-script.d.ts +0 -64
- package/dist/tasks/extract-values-script.js +0 -180
- package/dist/tasks/extract-values.d.ts +0 -77
- package/dist/tasks/extract-values.js +0 -146
- package/dist/tools/report-session/derive-table.d.ts +0 -164
- package/dist/tools/report-session/derive-table.js +0 -419
- package/dist/tools/research/literature-reviewer.d.ts +0 -42
- package/dist/tools/research/literature-reviewer.js +0 -107
- package/dist/workflows/metrics.d.ts +0 -23
- package/dist/workflows/metrics.js +0 -38
package/dist/sandbox/client.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { ResultAsync } from "neverthrow";
|
|
21
21
|
import type { SpawnSession } from "../auth/types.js";
|
|
22
|
+
import type { ResourceLimits } from "../config/resource-limits.js";
|
|
22
23
|
import type { SandboxError } from "./sandbox-error.js";
|
|
23
24
|
import type { ExecEmit, ExecRequest, ExecResult, ManagedSandbox, SandboxIdentity, SandboxLiveness, SandboxRef, SandboxSpec, ToolchainSource } from "./types.js";
|
|
24
25
|
export type SandboxClient = {
|
|
@@ -31,6 +32,8 @@ export type SandboxClient = {
|
|
|
31
32
|
* optional field would carry two meanings, undeclared and unknown.
|
|
32
33
|
*/
|
|
33
34
|
readonly toolchainSource: ToolchainSource;
|
|
35
|
+
/** The per-machine ceilings that the client clamps each spawn to. The sandbox module clamps a lease request to the same values. */
|
|
36
|
+
readonly resourceLimits: ResourceLimits;
|
|
34
37
|
/**
|
|
35
38
|
* DBOS step (`sandbox.create`) — the spawn half of the two-step create
|
|
36
39
|
* (see the harness-sandbox-exec spec). Launches the sandbox-base container/Job under the
|
|
@@ -59,9 +62,8 @@ export type SandboxClient = {
|
|
|
59
62
|
* same step gets the same id, and the sandbox gives it the result of the
|
|
60
63
|
* first. Outside a workflow it throws.
|
|
61
64
|
*
|
|
62
|
-
* `execId` replaces that id.
|
|
63
|
-
*
|
|
64
|
-
* (`sandbox/step-sandbox.ts`).
|
|
65
|
+
* `execId` replaces that id. The sandbox leases give an id under the salt
|
|
66
|
+
* of the lease (`sandbox/leases.ts`).
|
|
65
67
|
*
|
|
66
68
|
* A recovered step submits again under the same id. The sandbox dedups the
|
|
67
69
|
* submit on the id, thus the poll attaches to the exec that already ran or
|
|
@@ -2,17 +2,11 @@
|
|
|
2
2
|
* Backend-selected `SandboxClient` factory.
|
|
3
3
|
*
|
|
4
4
|
* Docker for local dev, K8s for production — the single place the
|
|
5
|
-
* `SANDBOX_BACKEND` env decision lives.
|
|
6
|
-
*
|
|
7
|
-
* backend module directly.
|
|
8
|
-
*
|
|
9
|
-
* Registry write/clear callbacks are wired here so the per-backend
|
|
10
|
-
* `createSandbox` / `teardown` implementations stay free of state-layer
|
|
11
|
-
* coupling (they just call the closures we hand them). `exec` is
|
|
5
|
+
* `SANDBOX_BACKEND` env decision lives. The embedder makes the client and gives
|
|
6
|
+
* it to `assembleCoreRuntime`, and only the sandbox module calls it. `exec` is
|
|
12
7
|
* backend-agnostic — it only needs the SandboxRef.
|
|
13
8
|
*/
|
|
14
9
|
import type { V1Toleration } from "@kubernetes/client-node";
|
|
15
|
-
import type { Querier } from "../state/db.js";
|
|
16
10
|
import type { Logger } from "../lib/logger.js";
|
|
17
11
|
import { type ResourceLimits } from "../config/resource-limits.js";
|
|
18
12
|
import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
@@ -30,8 +24,6 @@ export type SandboxBackendConfig = {
|
|
|
30
24
|
readonly namespace: string;
|
|
31
25
|
};
|
|
32
26
|
export type CreateSandboxClientConfig = {
|
|
33
|
-
/** App pool — used for registry writes inside the create/teardown steps. */
|
|
34
|
-
pool: Querier;
|
|
35
27
|
/** Backend default + namespace fallback source. */
|
|
36
28
|
env: SandboxBackendConfig;
|
|
37
29
|
/** Default sandbox-base image when the workflow doesn't override per-step. */
|
|
@@ -174,6 +166,9 @@ export declare function precreateStepTree(deps: {
|
|
|
174
166
|
resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
175
167
|
stepTreeAccess?: "world-writable";
|
|
176
168
|
}, coords: MountPlanCoords): Promise<void>;
|
|
169
|
+
/** Pre-create the directory of the exec records of the lease, which each machine of the lease mounts read-write. */
|
|
170
|
+
export declare function precreateRecordDir(deps: {
|
|
171
|
+
resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
172
|
+
stepTreeAccess?: "world-writable";
|
|
173
|
+
}, coords: MountPlanCoords): Promise<void>;
|
|
177
174
|
export declare function createSandboxClient(config: CreateSandboxClientConfig): SandboxClient;
|
|
178
|
-
/** Re-exports so consumers import once from this module. */
|
|
179
|
-
export { clearSandboxRef } from "../state/index.js";
|
|
@@ -2,13 +2,8 @@
|
|
|
2
2
|
* Backend-selected `SandboxClient` factory.
|
|
3
3
|
*
|
|
4
4
|
* Docker for local dev, K8s for production — the single place the
|
|
5
|
-
* `SANDBOX_BACKEND` env decision lives.
|
|
6
|
-
*
|
|
7
|
-
* backend module directly.
|
|
8
|
-
*
|
|
9
|
-
* Registry write/clear callbacks are wired here so the per-backend
|
|
10
|
-
* `createSandbox` / `teardown` implementations stay free of state-layer
|
|
11
|
-
* coupling (they just call the closures we hand them). `exec` is
|
|
5
|
+
* `SANDBOX_BACKEND` env decision lives. The embedder makes the client and gives
|
|
6
|
+
* it to `assembleCoreRuntime`, and only the sandbox module calls it. `exec` is
|
|
12
7
|
* backend-agnostic — it only needs the SandboxRef.
|
|
13
8
|
*/
|
|
14
9
|
import { chmod, mkdir } from "node:fs/promises";
|
|
@@ -20,13 +15,11 @@ import { recordSandboxExec } from "../lib/metrics.js";
|
|
|
20
15
|
import { ATTR_INFLEXA_EXEC_ID, stableSpan } from "../lib/otel-spans.js";
|
|
21
16
|
import { clampResources } from "../config/resource-limits.js";
|
|
22
17
|
import { tailWritePrefix } from "../workspace/paths.js";
|
|
23
|
-
import { tryMutation } from "../lib/db-result.js";
|
|
24
18
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
25
|
-
import { setSandboxRef } from "../state/index.js";
|
|
26
19
|
import { capExecStreams, EXEC_STREAM_BYTE_CAP } from "../tools/workspace/result-bounds.js";
|
|
27
20
|
import { createDockerSandboxOps } from "./docker-client.js";
|
|
28
21
|
import { runExec, stepExecId } from "./exec.js";
|
|
29
|
-
import {
|
|
22
|
+
import { sandboxExecOutcomeOf } from "./exec-outcome.js";
|
|
30
23
|
import { createK8sSandboxOps } from "./k8s-client.js";
|
|
31
24
|
import { mountCoordsOf, sandboxWriteTail } from "./mount-plan.js";
|
|
32
25
|
import { SandboxFailure } from "./sandbox-error.js";
|
|
@@ -65,6 +58,13 @@ export async function precreateStepTree(deps, coords) {
|
|
|
65
58
|
await Promise.all(write.subdirs.map((sub) => chmod(join(writeDir, sub), 0o777)));
|
|
66
59
|
}
|
|
67
60
|
}
|
|
61
|
+
/** Pre-create the directory of the exec records of the lease, which each machine of the lease mounts read-write. */
|
|
62
|
+
export async function precreateRecordDir(deps, coords) {
|
|
63
|
+
const recordDir = tailWritePrefix({ workspaceRoot: await deps.resolveWorkspaceRoot(coords.analysisId), tail: coords.recordTail });
|
|
64
|
+
await mkdir(recordDir, { recursive: true });
|
|
65
|
+
if (deps.stepTreeAccess === "world-writable")
|
|
66
|
+
await chmod(recordDir, 0o777);
|
|
67
|
+
}
|
|
68
68
|
export function createSandboxClient(config) {
|
|
69
69
|
const backend = config.backend ?? config.env.backend;
|
|
70
70
|
// Normalized once, here, and handed to the backends and to the client
|
|
@@ -100,9 +100,6 @@ export function createSandboxClient(config) {
|
|
|
100
100
|
* unwraps below stay on the plain bridge: they carry a `DbError`.
|
|
101
101
|
*/
|
|
102
102
|
const failing = (e) => new SandboxFailure(e);
|
|
103
|
-
const registerSandbox = async (session, spec, ref) => {
|
|
104
|
-
unwrapOrThrow(await setSandboxRef(config.pool, session.runFrame.runId, session.runFrame.stepId, ref));
|
|
105
|
-
};
|
|
106
103
|
const ops = backend === "k8s"
|
|
107
104
|
? createK8sSandboxOps({
|
|
108
105
|
image: config.image,
|
|
@@ -120,7 +117,6 @@ export function createSandboxClient(config) {
|
|
|
120
117
|
tolerations: config.tolerations,
|
|
121
118
|
runtimeClassName: config.runtimeClassName,
|
|
122
119
|
logger: config.logger,
|
|
123
|
-
registerSandbox,
|
|
124
120
|
})
|
|
125
121
|
: createDockerSandboxOps({
|
|
126
122
|
image: config.image,
|
|
@@ -133,27 +129,9 @@ export function createSandboxClient(config) {
|
|
|
133
129
|
platform: config.platform,
|
|
134
130
|
engineSocketPath: config.engineSocketPath,
|
|
135
131
|
logger: config.logger,
|
|
136
|
-
registerSandbox,
|
|
137
132
|
});
|
|
138
|
-
// `teardown` needs the registry-clear closure but the SandboxRef alone
|
|
139
|
-
// doesn't carry runId/stepId. Wrap the backend impl so callers pass the
|
|
140
|
-
// ref + step coordinates and the wrapper does the clear after the
|
|
141
|
-
// backend-level removal.
|
|
142
133
|
const teardown = async (ref) => {
|
|
143
134
|
unwrapOrThrow((await ops.teardown(ref)).mapErr(failing));
|
|
144
|
-
// Sandboxes are 1-per-step (see `sandbox-step.ts` body — each child
|
|
145
|
-
// workflow calls `createSandbox` once then `teardown` once), so the
|
|
146
|
-
// broad WHERE clause matches exactly one row. If that invariant ever
|
|
147
|
-
// changes (e.g. a step reuses a sibling's sandbox), this clear must
|
|
148
|
-
// be scoped to (run, step) to avoid wiping prior steps' provenance.
|
|
149
|
-
unwrapOrThrow(await tryMutation("createSandbox.teardownClearSandboxRef", async () => {
|
|
150
|
-
await config.pool.query({
|
|
151
|
-
text: `UPDATE cortex_step_executions
|
|
152
|
-
SET sandbox_ref = NULL
|
|
153
|
-
WHERE sandbox_ref->>'sandboxId' = $1`,
|
|
154
|
-
values: [ref.sandboxId],
|
|
155
|
-
});
|
|
156
|
-
}));
|
|
157
135
|
};
|
|
158
136
|
// One arbiter for both surfaces: the client's `isAlive` method and the
|
|
159
137
|
// poll loop's escalation probe are the same backend inspect.
|
|
@@ -185,10 +163,12 @@ export function createSandboxClient(config) {
|
|
|
185
163
|
// The config states an engine fact (binds preserve host ownership);
|
|
186
164
|
// which remediation that fact demands — today, a world-writable step
|
|
187
165
|
// write tree — is harness-owned and chosen here, not by the embedder.
|
|
188
|
-
const
|
|
166
|
+
const treeDeps = {
|
|
189
167
|
resolveWorkspaceRoot: config.resolveWorkspaceRoot,
|
|
190
168
|
stepTreeAccess: config.engineBindOwnership === "host-preserved" ? "world-writable" : undefined,
|
|
191
|
-
}
|
|
169
|
+
};
|
|
170
|
+
const coords = mountCoordsOf(session, spec);
|
|
171
|
+
const precreated = await ResultAsync.fromPromise(Promise.all([precreateStepTree(treeDeps, coords), precreateRecordDir(treeDeps, coords)]), createFailed("createSandbox.precreateStepTree"));
|
|
192
172
|
if (precreated.isErr())
|
|
193
173
|
return err(precreated.error);
|
|
194
174
|
// Clamp to cluster ceilings so the pod is always quota-admissible.
|
|
@@ -197,6 +177,7 @@ export function createSandboxClient(config) {
|
|
|
197
177
|
};
|
|
198
178
|
return {
|
|
199
179
|
toolchainSource,
|
|
180
|
+
resourceLimits: config.resourceLimits,
|
|
200
181
|
createSandbox: (session, spec, identity) => new ResultAsync(createSandbox(session, spec, identity)),
|
|
201
182
|
// The one place an ExecResult crosses from the wire into the process, so
|
|
202
183
|
// the cap lands here rather than at each consumer. Every downstream use —
|
|
@@ -223,7 +204,6 @@ export function createSandboxClient(config) {
|
|
|
223
204
|
peakMemoryBytes: result.usage?.peakMemoryBytes,
|
|
224
205
|
cpuMillis: result.usage?.cpuMillis,
|
|
225
206
|
});
|
|
226
|
-
noteExecOutcome(ref.sandboxId, summarizeExec(result));
|
|
227
207
|
return result;
|
|
228
208
|
}, { name: "sandbox.exec" }),
|
|
229
209
|
isAlive,
|
|
@@ -233,5 +213,3 @@ export function createSandboxClient(config) {
|
|
|
233
213
|
listManagedSandboxes: async () => unwrapOrThrow((await ops.listManagedSandboxes()).mapErr(failing)),
|
|
234
214
|
};
|
|
235
215
|
}
|
|
236
|
-
/** Re-exports so consumers import once from this module. */
|
|
237
|
-
export { clearSandboxRef } from "../state/index.js";
|
|
@@ -143,11 +143,9 @@ export type DockerClientConfig = {
|
|
|
143
143
|
/**
|
|
144
144
|
* Optional logger so a store degradation (a resolved farm whose `inflexa.lock`
|
|
145
145
|
* is absent or invalid by sandbox-create time) is observable instead of a
|
|
146
|
-
* silent libs-mount drop.
|
|
146
|
+
* silent libs-mount drop.
|
|
147
147
|
*/
|
|
148
148
|
logger?: Logger;
|
|
149
|
-
/** Hook called after the registry row is written. */
|
|
150
|
-
registerSandbox: (session: SpawnSession, spec: SandboxSpec, ref: SandboxRef) => Promise<void>;
|
|
151
149
|
};
|
|
152
150
|
/**
|
|
153
151
|
* dockerode connection options for a configured engine socket, or `undefined`
|
|
@@ -319,6 +319,7 @@ export function createDockerSandboxOps(config) {
|
|
|
319
319
|
...(libsMounted && farm && plan.farmPath ? [`${farm.farmPath}:${plan.farmPath}:ro`] : []),
|
|
320
320
|
...(libsMounted && cacheBindable && farm?.cachePath && plan.cachePath ? [`${farm.cachePath}:${plan.cachePath}:rw`] : []),
|
|
321
321
|
...(refsMounted && config.refStorePath ? [`${config.refStorePath}:${String(plan.refsPath)}:ro`] : []),
|
|
322
|
+
`${tailWritePrefix({ workspaceRoot: hostTreePath, tail: plan.recordTail })}:${plan.recordsPath}:rw`,
|
|
322
323
|
...cpuBinds,
|
|
323
324
|
];
|
|
324
325
|
const createFailed = (status, cause) => ({
|
|
@@ -348,7 +349,7 @@ export function createDockerSandboxOps(config) {
|
|
|
348
349
|
Env: env,
|
|
349
350
|
User: SANDBOX_ROOT_USER,
|
|
350
351
|
WorkingDir: plan.workingDir,
|
|
351
|
-
Labels: mergeLabels(hostLabels, { ...harnessLabels(session, sandboxId), [OWNER_WORKFLOW_LABEL]: spec.
|
|
352
|
+
Labels: mergeLabels(hostLabels, { ...harnessLabels(session, sandboxId), [OWNER_WORKFLOW_LABEL]: spec.ownerWorkflowId }),
|
|
352
353
|
ExposedPorts: { [`${SANDBOX_SERVER_PORT}/tcp`]: {} },
|
|
353
354
|
HostConfig: {
|
|
354
355
|
Binds: binds,
|
|
@@ -378,7 +379,7 @@ export function createDockerSandboxOps(config) {
|
|
|
378
379
|
};
|
|
379
380
|
if (config.platform !== undefined)
|
|
380
381
|
createOpts.platform = config.platform;
|
|
381
|
-
const sandbox = await createOrAdopt(docker, createOpts, sandboxId, spec.
|
|
382
|
+
const sandbox = await createOrAdopt(docker, createOpts, sandboxId, spec.ownerWorkflowId, createFailed, logger);
|
|
382
383
|
if (sandbox.isErr())
|
|
383
384
|
return err(sandbox.error);
|
|
384
385
|
if (!sandbox.value.alreadyRunning) {
|
|
@@ -407,9 +408,6 @@ export function createDockerSandboxOps(config) {
|
|
|
407
408
|
port,
|
|
408
409
|
backend: "docker",
|
|
409
410
|
};
|
|
410
|
-
const registered = await trySandbox(() => config.registerSandbox(session, spec, ref), createFailed);
|
|
411
|
-
if (registered.isErr())
|
|
412
|
-
return err(registered.error);
|
|
413
411
|
return ok(ref);
|
|
414
412
|
})());
|
|
415
413
|
},
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* keeps that text on the sandbox side of the boundary. A summary gives the
|
|
9
9
|
* SIZE of stderr and never a byte of it.
|
|
10
10
|
*
|
|
11
|
-
* The summary of the last exec of a
|
|
12
|
-
* keyed by
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* re-runs the await loop and writes the cell again, thus
|
|
16
|
-
* reads the summary of the same exec. The map is capped, so a
|
|
17
|
-
* step never reaches the read site cannot hold memory.
|
|
11
|
+
* The summary of the last exec of a lease lives in one process-local cell,
|
|
12
|
+
* keyed by the lease key. A step holds one lease key, thus the cell is
|
|
13
|
+
* step-scoped in effect and the step body reads it without a new dependency
|
|
14
|
+
* through the agent factory. The cell keeps the summary across a machine
|
|
15
|
+
* replacement. A replay re-runs the await loop and writes the cell again, thus
|
|
16
|
+
* a recovered body reads the summary of the same exec. The map is capped, so a
|
|
17
|
+
* lease whose step never reaches the read site cannot hold memory.
|
|
18
18
|
*/
|
|
19
19
|
import type { SandboxExecOutcome } from "../lib/metrics.js";
|
|
20
20
|
import type { ExecResult } from "./types.js";
|
|
@@ -39,7 +39,7 @@ export type ExecOutcomeSummary = {
|
|
|
39
39
|
};
|
|
40
40
|
/** Reduce a result to its summary. */
|
|
41
41
|
export declare function summarizeExec(result: ExecResult): ExecOutcomeSummary;
|
|
42
|
-
/** Keep `summary` as the last known exec of `
|
|
43
|
-
export declare function noteExecOutcome(
|
|
44
|
-
/** The last known exec of `
|
|
45
|
-
export declare function lastExecOutcome(
|
|
42
|
+
/** Keep `summary` as the last known exec of the lease `leaseKey`. */
|
|
43
|
+
export declare function noteExecOutcome(leaseKey: string, summary: ExecOutcomeSummary): void;
|
|
44
|
+
/** The last known exec of the lease `leaseKey`, or `undefined` when it ran none. */
|
|
45
|
+
export declare function lastExecOutcome(leaseKey: string): ExecOutcomeSummary | undefined;
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* keeps that text on the sandbox side of the boundary. A summary gives the
|
|
9
9
|
* SIZE of stderr and never a byte of it.
|
|
10
10
|
*
|
|
11
|
-
* The summary of the last exec of a
|
|
12
|
-
* keyed by
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* re-runs the await loop and writes the cell again, thus
|
|
16
|
-
* reads the summary of the same exec. The map is capped, so a
|
|
17
|
-
* step never reaches the read site cannot hold memory.
|
|
11
|
+
* The summary of the last exec of a lease lives in one process-local cell,
|
|
12
|
+
* keyed by the lease key. A step holds one lease key, thus the cell is
|
|
13
|
+
* step-scoped in effect and the step body reads it without a new dependency
|
|
14
|
+
* through the agent factory. The cell keeps the summary across a machine
|
|
15
|
+
* replacement. A replay re-runs the await loop and writes the cell again, thus
|
|
16
|
+
* a recovered body reads the summary of the same exec. The map is capped, so a
|
|
17
|
+
* lease whose step never reaches the read site cannot hold memory.
|
|
18
18
|
*/
|
|
19
19
|
/** The reason that names a machine the kernel killed for its memory limit. */
|
|
20
20
|
const OOM_KILLED = "sandbox-oom-killed";
|
|
@@ -41,25 +41,25 @@ export function summarizeExec(result) {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
* How many
|
|
45
|
-
*
|
|
44
|
+
* How many leases keep a summary. A host holds far fewer leases at once than
|
|
45
|
+
* this, thus the cap only drops the summary of a step that ended long ago.
|
|
46
46
|
*/
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
/** Keep `summary` as the last known exec of `
|
|
50
|
-
export function noteExecOutcome(
|
|
47
|
+
const MAX_TRACKED_LEASES = 256;
|
|
48
|
+
const lastExecByLease = new Map();
|
|
49
|
+
/** Keep `summary` as the last known exec of the lease `leaseKey`. */
|
|
50
|
+
export function noteExecOutcome(leaseKey, summary) {
|
|
51
51
|
// Delete before set so the key moves to the end of the insertion order and
|
|
52
|
-
// the eviction below drops the least recently written
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
while (
|
|
56
|
-
const oldest =
|
|
52
|
+
// the eviction below drops the least recently written lease.
|
|
53
|
+
lastExecByLease.delete(leaseKey);
|
|
54
|
+
lastExecByLease.set(leaseKey, summary);
|
|
55
|
+
while (lastExecByLease.size > MAX_TRACKED_LEASES) {
|
|
56
|
+
const oldest = lastExecByLease.keys().next();
|
|
57
57
|
if (oldest.done)
|
|
58
58
|
break;
|
|
59
|
-
|
|
59
|
+
lastExecByLease.delete(oldest.value);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
/** The last known exec of `
|
|
63
|
-
export function lastExecOutcome(
|
|
64
|
-
return
|
|
62
|
+
/** The last known exec of the lease `leaseKey`, or `undefined` when it ran none. */
|
|
63
|
+
export function lastExecOutcome(leaseKey) {
|
|
64
|
+
return lastExecByLease.get(leaseKey);
|
|
65
65
|
}
|
package/dist/sandbox/exec.d.ts
CHANGED
|
@@ -52,6 +52,11 @@ export type ExecDeps = {
|
|
|
52
52
|
* makes the pair unique, thus no caller mints an exec id.
|
|
53
53
|
*/
|
|
54
54
|
export declare function stepExecId(): string;
|
|
55
|
+
/**
|
|
56
|
+
* The exec id of a step under a salt that no machine receives. The id is the same on each run of the step, but the
|
|
57
|
+
* ids that a command can read in the exec records do not give the id of a later exec.
|
|
58
|
+
*/
|
|
59
|
+
export declare function saltedStepExecId(salt: string): string;
|
|
55
60
|
/**
|
|
56
61
|
* Run one exec to its terminal result.
|
|
57
62
|
*
|
package/dist/sandbox/exec.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { errorMessageOf } from "../lib/thrown.js";
|
|
16
16
|
import { textOf } from "../lib/unknown-text.js";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
17
18
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
18
19
|
import { DBOS, Error as DBOSErrors, StatusString } from "@dbos-inc/dbos-sdk";
|
|
19
20
|
import { untracedFetch } from "../lib/otel-spans.js";
|
|
@@ -62,6 +63,13 @@ export function stepExecId() {
|
|
|
62
63
|
}
|
|
63
64
|
return `${workflowId}:${stepId}`;
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The exec id of a step under a salt that no machine receives. The id is the same on each run of the step, but the
|
|
68
|
+
* ids that a command can read in the exec records do not give the id of a later exec.
|
|
69
|
+
*/
|
|
70
|
+
export function saltedStepExecId(salt) {
|
|
71
|
+
return createHash("sha256").update(`${salt}:${stepExecId()}`).digest("hex");
|
|
72
|
+
}
|
|
65
73
|
/**
|
|
66
74
|
* Run one exec to its terminal result.
|
|
67
75
|
*
|
|
@@ -177,7 +185,7 @@ async function pollExec(ref, execId, emit, deadlineMs, deps) {
|
|
|
177
185
|
if (resp.cursor > cursor)
|
|
178
186
|
cursor = resp.cursor;
|
|
179
187
|
if (resp.result !== undefined)
|
|
180
|
-
return resp.result;
|
|
188
|
+
return resp.result.lost === undefined ? resp.result : syntheticFailureResult(execId, "sandbox-replaced");
|
|
181
189
|
}
|
|
182
190
|
if (deps.isAlive && escalation.onPoll(polled.kind === "ok" ? "ok" : "unavailable")) {
|
|
183
191
|
const verdict = await probeLiveness(deps.isAlive, ref);
|
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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.
|
|
2
|
+
* The name of a sandbox machine. It comes from a key: the id of the lease workflow and the machine generation. Thus
|
|
3
|
+
* a rerun of the spawn step adopts the machine of the earlier attempt, and no step checkpoints a name before a spawn.
|
|
8
4
|
*/
|
|
9
5
|
import type { SandboxIdentity } from "./types.js";
|
|
10
|
-
/** `sbx-{run8}-{rand8}` — run prefix aids `kubectl` triage; the 8-hex random
|
|
11
|
-
* suffix makes within-run collisions across a run's concurrent steps
|
|
12
|
-
* negligible. Names are NOT the idempotency mechanism (the checkpoint is), and
|
|
13
|
-
* adoption is owner-label-guarded, but a unique name keeps a collision from
|
|
14
|
-
* ever reaching that guard. */
|
|
15
|
-
export declare function mintSandboxIdentity(runId: string): SandboxIdentity;
|
|
16
6
|
/**
|
|
17
|
-
* `sbx-{run8}-{hash12}`
|
|
18
|
-
*
|
|
19
|
-
* of a step adopts the machine of the earlier attempt. The key is hashed,
|
|
20
|
-
* because a workflow id can hold characters that a container name refuses.
|
|
7
|
+
* `sbx-{run8}-{hash12}`. The run prefix aids `kubectl` triage. The same key gives the same name, and the key is
|
|
8
|
+
* hashed, because a workflow id can hold characters that a container name refuses.
|
|
21
9
|
*/
|
|
22
10
|
export declare function keyedSandboxIdentity(runId: string, key: string): SandboxIdentity;
|
package/dist/sandbox/identity.js
CHANGED
|
@@ -1,27 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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.
|
|
2
|
+
* The name of a sandbox machine. It comes from a key: the id of the lease workflow and the machine generation. Thus
|
|
3
|
+
* a rerun of the spawn step adopts the machine of the earlier attempt, and no step checkpoints a name before a spawn.
|
|
8
4
|
*/
|
|
9
|
-
import { createHash
|
|
10
|
-
/** `sbx-{run8}-{rand8}` — run prefix aids `kubectl` triage; the 8-hex random
|
|
11
|
-
* suffix makes within-run collisions across a run's concurrent steps
|
|
12
|
-
* negligible. Names are NOT the idempotency mechanism (the checkpoint is), and
|
|
13
|
-
* adoption is owner-label-guarded, but a unique name keeps a collision from
|
|
14
|
-
* ever reaching that guard. */
|
|
15
|
-
export function mintSandboxIdentity(runId) {
|
|
16
|
-
const run8 = runId.replaceAll("-", "").slice(0, 8);
|
|
17
|
-
const suffix = randomUUID().replaceAll("-", "").slice(0, 8);
|
|
18
|
-
return { sandboxId: `sbx-${run8}-${suffix}` };
|
|
19
|
-
}
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
20
6
|
/**
|
|
21
|
-
* `sbx-{run8}-{hash12}`
|
|
22
|
-
*
|
|
23
|
-
* of a step adopts the machine of the earlier attempt. The key is hashed,
|
|
24
|
-
* because a workflow id can hold characters that a container name refuses.
|
|
7
|
+
* `sbx-{run8}-{hash12}`. The run prefix aids `kubectl` triage. The same key gives the same name, and the key is
|
|
8
|
+
* hashed, because a workflow id can hold characters that a container name refuses.
|
|
25
9
|
*/
|
|
26
10
|
export function keyedSandboxIdentity(runId, key) {
|
|
27
11
|
const run8 = runId.replaceAll("-", "").slice(0, 8);
|
|
@@ -105,7 +105,6 @@ export type K8sClientConfig = {
|
|
|
105
105
|
/** Injected for tests. */
|
|
106
106
|
batchApi?: BatchJobApi;
|
|
107
107
|
coreApi?: CorePodApi;
|
|
108
|
-
registerSandbox: (session: SpawnSession, spec: SandboxSpec, ref: SandboxRef) => Promise<void>;
|
|
109
108
|
};
|
|
110
109
|
/** Clock and sleep seams, so a test can drive the slow-pod warning without waiting a minute. */
|
|
111
110
|
export type WaitForPodReadyDeps = {
|
|
@@ -171,6 +171,12 @@ async function buildJobSpec(session, sandboxSpec, hostLabels, config, identity,
|
|
|
171
171
|
readOnly: false,
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
|
+
volumeMounts.push({
|
|
175
|
+
name: SESSION_VOLUME_NAME,
|
|
176
|
+
mountPath: plan.recordsPath,
|
|
177
|
+
subPath: subPaths.records,
|
|
178
|
+
readOnly: false,
|
|
179
|
+
});
|
|
174
180
|
}
|
|
175
181
|
if (config.libStorePvc && plan.libsPath) {
|
|
176
182
|
const cacheSubPath = farm?.cachePath !== undefined && plan.cachePath ? pvcSubPath(farm.cachePath, "the farm cache") : undefined;
|
|
@@ -263,7 +269,7 @@ async function buildJobSpec(session, sandboxSpec, hostLabels, config, identity,
|
|
|
263
269
|
name: sandboxId,
|
|
264
270
|
namespace: config.namespace,
|
|
265
271
|
annotations: {
|
|
266
|
-
[OWNER_WORKFLOW_ANNOTATION]: sandboxSpec.
|
|
272
|
+
[OWNER_WORKFLOW_ANNOTATION]: sandboxSpec.ownerWorkflowId,
|
|
267
273
|
},
|
|
268
274
|
labels,
|
|
269
275
|
},
|
|
@@ -493,7 +499,7 @@ export function createK8sSandboxOps(config) {
|
|
|
493
499
|
}
|
|
494
500
|
}
|
|
495
501
|
const job = await buildJobSpec(session, spec, hostLabels, config, identity, farm, libsMounted);
|
|
496
|
-
const adopted = await createOrAdoptJob(batchApi, coreApi, config.namespace, sandboxId, spec.
|
|
502
|
+
const adopted = await createOrAdoptJob(batchApi, coreApi, config.namespace, sandboxId, spec.ownerWorkflowId, job);
|
|
497
503
|
if (adopted.isErr())
|
|
498
504
|
return err(adopted.error);
|
|
499
505
|
// A Job whose pod never schedules (quota rejection, no nodes, spot
|
|
@@ -510,17 +516,6 @@ export function createK8sSandboxOps(config) {
|
|
|
510
516
|
port: SANDBOX_SERVER_PORT,
|
|
511
517
|
backend: "k8s",
|
|
512
518
|
};
|
|
513
|
-
const registered = await trySandbox(() => config.registerSandbox(session, spec, ref), (status, cause) => ({
|
|
514
|
-
type: "container_create_failed",
|
|
515
|
-
op: "k8s.registerSandbox",
|
|
516
|
-
sandboxId,
|
|
517
|
-
status,
|
|
518
|
-
cause,
|
|
519
|
-
}));
|
|
520
|
-
if (registered.isErr()) {
|
|
521
|
-
await cleanupFailedJob(batchApi, config.namespace, sandboxId, config.logger ?? createNoopLogger());
|
|
522
|
-
return err(registered.error);
|
|
523
|
-
}
|
|
524
519
|
return ok(ref);
|
|
525
520
|
}
|
|
526
521
|
await cleanupFailedJob(batchApi, config.namespace, sandboxId, config.logger ?? createNoopLogger());
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The names that the lease module, the reconciler, and the purge share. */
|
|
2
|
+
/** The prefix of each lease workflow id. A purge finds the lease workflows of an analysis by it. */
|
|
3
|
+
export declare const LEASE_WORKFLOW_PREFIX = "sandboxlease";
|
|
4
|
+
export declare function leaseWorkflowIdOf(analysisId: string, leaseKey: string, generation: number): string;
|
|
5
|
+
/** The workspace-relative records directory of a lease key. Each generation of the key uses it. */
|
|
6
|
+
export declare function leaseRecordTail(leaseKey: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The names that the lease module, the reconciler, and the purge share. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { HARNESS_PRIVATE_DIR } from "../workspace/paths.js";
|
|
4
|
+
/** The prefix of each lease workflow id. A purge finds the lease workflows of an analysis by it. */
|
|
5
|
+
export const LEASE_WORKFLOW_PREFIX = "sandboxlease";
|
|
6
|
+
export function leaseWorkflowIdOf(analysisId, leaseKey, generation) {
|
|
7
|
+
return `${LEASE_WORKFLOW_PREFIX}:${analysisId}:${leaseKey}:${generation}`;
|
|
8
|
+
}
|
|
9
|
+
/** The workspace-relative records directory of a lease key. Each generation of the key uses it. */
|
|
10
|
+
export function leaseRecordTail(leaseKey) {
|
|
11
|
+
return `${HARNESS_PRIVATE_DIR}/exec-records/${createHash("sha256").update(leaseKey, "utf8").digest("hex").slice(0, 16)}`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sandbox module (sandbox-leases spec). A consumer holds a sandbox through a lease, and the module owns each
|
|
3
|
+
* machine: the admission, the spawn, the replacement, the idle removal, and the teardown.
|
|
4
|
+
*
|
|
5
|
+
* Each generation of a lease has one lease workflow. A consumer enqueues it through a `DBOSClient`, thus the lease
|
|
6
|
+
* workflow is not a child of the consumer: a cancel, a pause, or a crash of the consumer does not stop it, and its
|
|
7
|
+
* teardown step can always run. The consumer waits for the events of the lease workflow with the same client,
|
|
8
|
+
* because DBOS refuses `recv` and `getEvent` inside a step, and each tool call is a step.
|
|
9
|
+
*
|
|
10
|
+
* A machine of a lease is replaceable. The state of a lease is its files, the exec records in its records
|
|
11
|
+
* directory, and the checkpoints of its consumer. Thus the module removes the machine of an idle lease, and the
|
|
12
|
+
* next exec gets a new one.
|
|
13
|
+
*/
|
|
14
|
+
import { type DBOSClient } from "@dbos-inc/dbos-sdk";
|
|
15
|
+
import { type Result } from "neverthrow";
|
|
16
|
+
import type { SpawnSession } from "../auth/types.js";
|
|
17
|
+
import { type ResourcePolicy, type ResourceSpec } from "../config/resource-limits.js";
|
|
18
|
+
import type { PausePart, WorkKind } from "../contracts/chat-parts.js";
|
|
19
|
+
import type { RunAuthorizer } from "../execution/run-authorizer.js";
|
|
20
|
+
import type { Connectable } from "../lib/db-result.js";
|
|
21
|
+
import type { Logger } from "../lib/logger.js";
|
|
22
|
+
import type { Querier } from "../state/db.js";
|
|
23
|
+
import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
24
|
+
import type { SandboxClient } from "./client.js";
|
|
25
|
+
import type { ExecEmit, ExecRequest, ExecResult, ToolchainSource } from "./types.js";
|
|
26
|
+
export declare const LEASE_WORKFLOW = "sandbox-lease";
|
|
27
|
+
export declare const LEASE_QUEUE = "sandbox-leases";
|
|
28
|
+
export declare const DEFAULT_IDLE_PERIOD_MS: number;
|
|
29
|
+
/** The timeout of a command that gives none. A step has no deadline of its own, thus each command has one. */
|
|
30
|
+
export declare const DEFAULT_COMMAND_TIMEOUT_SECONDS = 3600;
|
|
31
|
+
/** The spawn settings of a consumer, beside the size. */
|
|
32
|
+
export type LeaseSpawnSpec = {
|
|
33
|
+
readonly image?: string;
|
|
34
|
+
readonly extraEnv?: Readonly<Record<string, string>>;
|
|
35
|
+
readonly readOnly?: boolean;
|
|
36
|
+
readonly writableTail?: string;
|
|
37
|
+
};
|
|
38
|
+
/** Who holds a lease, and where its pause and its parts go. */
|
|
39
|
+
export type LeaseBinding = {
|
|
40
|
+
/** The key of the lease. A tool gives the id of its workflow. */
|
|
41
|
+
readonly leaseKey: string;
|
|
42
|
+
/** The spawn session of a run step or of a data profile. It goes into the input of the lease workflow. */
|
|
43
|
+
readonly session: SpawnSession;
|
|
44
|
+
/** The work of the consumer. A pause of the lease workflow belongs to it. */
|
|
45
|
+
readonly work: {
|
|
46
|
+
readonly kind: WorkKind;
|
|
47
|
+
readonly id: string;
|
|
48
|
+
};
|
|
49
|
+
readonly spawn?: LeaseSpawnSpec;
|
|
50
|
+
/** Writes a part to the stream of the consumer. A wait mirrors the pause of the lease workflow through it. */
|
|
51
|
+
readonly emit?: (part: PausePart) => Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
export type AcquireOutcome = {
|
|
54
|
+
readonly kind: "granted";
|
|
55
|
+
readonly size: ResourceSpec;
|
|
56
|
+
} | {
|
|
57
|
+
readonly kind: "refused";
|
|
58
|
+
readonly reason: string;
|
|
59
|
+
} | {
|
|
60
|
+
readonly kind: "failed";
|
|
61
|
+
readonly detail: string;
|
|
62
|
+
};
|
|
63
|
+
/** An exec found no lease that can run it. `detail` says why, for a lease that ended. */
|
|
64
|
+
export type NoLease = {
|
|
65
|
+
readonly kind: "no_lease";
|
|
66
|
+
readonly detail?: string;
|
|
67
|
+
};
|
|
68
|
+
/** A lease bound to its consumer. A tool holds one, and gives the size and the command only. */
|
|
69
|
+
export type BoundLease = {
|
|
70
|
+
acquire(size: ResourceSpec): Promise<AcquireOutcome>;
|
|
71
|
+
release(): Promise<void>;
|
|
72
|
+
exec(request: ExecRequest, emit: ExecEmit): Promise<Result<ExecResult, NoLease>>;
|
|
73
|
+
};
|
|
74
|
+
export type SandboxLeases = {
|
|
75
|
+
readonly toolchainSource: ToolchainSource;
|
|
76
|
+
acquire(binding: LeaseBinding, size: ResourceSpec): Promise<AcquireOutcome>;
|
|
77
|
+
release(leaseKey: string): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* End each lease of the given owner workflows, and do not wait for the lease workflows. A host that cancels
|
|
80
|
+
* work calls it, thus the machines go now and not at the next tick of each lease workflow.
|
|
81
|
+
*/
|
|
82
|
+
releaseOwnedBy(ownerWorkflowIds: readonly string[]): Promise<void>;
|
|
83
|
+
exec(leaseKey: string, request: ExecRequest, emit: ExecEmit, mirror?: LeaseBinding["emit"]): Promise<Result<ExecResult, NoLease>>;
|
|
84
|
+
bind(binding: LeaseBinding): BoundLease;
|
|
85
|
+
};
|
|
86
|
+
export type SandboxLeasesConfig = {
|
|
87
|
+
/** The application pool, which holds the lease table and the pause rows. */
|
|
88
|
+
readonly pool: Querier & Connectable;
|
|
89
|
+
readonly client: SandboxClient;
|
|
90
|
+
/** The budget of the grant. Absent: each lease gets its grant at once. */
|
|
91
|
+
readonly resourcePolicy?: ResourcePolicy;
|
|
92
|
+
readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
|
|
93
|
+
readonly runAuthorizer: RunAuthorizer;
|
|
94
|
+
/** Default {@link DEFAULT_IDLE_PERIOD_MS}. */
|
|
95
|
+
readonly idlePeriodMs?: number;
|
|
96
|
+
readonly pauseRetryDelaysMs?: readonly number[];
|
|
97
|
+
readonly logger?: Logger;
|
|
98
|
+
/** The DBOS client of the enqueue, the sends, and the waits of a consumer. */
|
|
99
|
+
readonly dbosClient: () => Promise<DBOSClient>;
|
|
100
|
+
/** The fetch of the health probe. Defaults to `globalThis.fetch`. */
|
|
101
|
+
readonly fetch?: (url: string, init?: RequestInit) => Promise<Response>;
|
|
102
|
+
/** The tick of a lease workflow, in seconds. Default 60. */
|
|
103
|
+
readonly tickSeconds?: number;
|
|
104
|
+
};
|
|
105
|
+
export declare function createSandboxLeases(config: SandboxLeasesConfig): SandboxLeases;
|