@inflexa-ai/harness 0.46.0 → 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.
Files changed (280) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/conversation-agent.d.ts +3 -8
  3. package/dist/agents/conversation-agent.js +24 -4
  4. package/dist/agents/report-session-agent.d.ts +11 -14
  5. package/dist/agents/report-session-agent.js +30 -17
  6. package/dist/agents/sandbox/shared.d.ts +18 -13
  7. package/dist/agents/sandbox/shared.js +42 -27
  8. package/dist/agents/sandbox/types.d.ts +1 -1
  9. package/dist/agents/sandbox-catalog.d.ts +2 -0
  10. package/dist/agents/sandbox-catalog.js +30 -1
  11. package/dist/app/chat-turn-workflow.js +5 -6
  12. package/dist/app/synthesize-run.d.ts +10 -6
  13. package/dist/app/synthesize-run.js +4 -2
  14. package/dist/contracts/analogy-report.d.ts +46 -2
  15. package/dist/contracts/analogy-report.js +31 -25
  16. package/dist/contracts/chat-parts.d.ts +3 -14
  17. package/dist/contracts/report-reference.d.ts +2 -1
  18. package/dist/contracts/report-reference.js +2 -1
  19. package/dist/contracts/schemas/chat-parts.d.ts +0 -27
  20. package/dist/contracts/schemas/chat-parts.js +1 -3
  21. package/dist/execution/run-canceler.d.ts +2 -0
  22. package/dist/execution/run-canceler.js +9 -0
  23. package/dist/execution/run-synthesis.d.ts +36 -8
  24. package/dist/execution/run-synthesis.js +114 -32
  25. package/dist/index.d.ts +2 -5
  26. package/dist/index.js +3 -6
  27. package/dist/input-scan/enrich.d.ts +5 -10
  28. package/dist/input-scan/enrich.js +12 -8
  29. package/dist/lib/hooks.d.ts +9 -0
  30. package/dist/lib/hooks.js +6 -1
  31. package/dist/lib/markdown-pdf.d.ts +27 -0
  32. package/dist/lib/markdown-pdf.js +236 -0
  33. package/dist/lib/metrics.d.ts +31 -0
  34. package/dist/lib/metrics.js +64 -0
  35. package/dist/lib/pdf-text.d.ts +23 -0
  36. package/dist/lib/pdf-text.js +34 -0
  37. package/dist/lib/regulatory-corpus/fetch-fda-guidance.js +3 -5
  38. package/dist/lib/regulatory-corpus/fetch-ich.js +3 -5
  39. package/dist/loop/run-loop.d.ts +5 -0
  40. package/dist/loop/run-loop.js +30 -2
  41. package/dist/loop/sub-agent.js +5 -6
  42. package/dist/loop/tool-name-repair.d.ts +11 -0
  43. package/dist/loop/tool-name-repair.js +23 -0
  44. package/dist/loop/turn-credentials.d.ts +5 -0
  45. package/dist/loop/turn-credentials.js +17 -0
  46. package/dist/loop/types.d.ts +14 -0
  47. package/dist/memory/card-builders.d.ts +1 -1
  48. package/dist/prompts/SOUL.d.ts +6 -6
  49. package/dist/prompts/SOUL.js +22 -121
  50. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  51. package/dist/prompts/analogical-reasoner.js +35 -101
  52. package/dist/prompts/briefing.d.ts +20 -11
  53. package/dist/prompts/briefing.js +36 -19
  54. package/dist/prompts/conversation.d.ts +1 -1
  55. package/dist/prompts/conversation.js +103 -383
  56. package/dist/prompts/planner.d.ts +1 -5
  57. package/dist/prompts/planner.js +45 -128
  58. package/dist/prompts/report-session.d.ts +1 -1
  59. package/dist/prompts/report-session.js +26 -57
  60. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  61. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -3
  62. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  63. package/dist/prompts/sandbox/cheminformatics-agent.js +12 -47
  64. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  65. package/dist/prompts/sandbox/chromatin-agent.js +4 -5
  66. package/dist/prompts/sandbox/data-profiler.js +11 -34
  67. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  68. package/dist/prompts/sandbox/dna-methylation-agent.js +3 -1
  69. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  70. package/dist/prompts/sandbox/drug-repurposing-agent.js +23 -73
  71. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  72. package/dist/prompts/sandbox/enrichment-agent.js +3 -1
  73. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  74. package/dist/prompts/sandbox/genomic-variant-agent.js +3 -4
  75. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  76. package/dist/prompts/sandbox/immune-profiling-agent.js +5 -4
  77. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  78. package/dist/prompts/sandbox/metabolomics-agent.js +3 -1
  79. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  80. package/dist/prompts/sandbox/microbiome-agent.js +3 -1
  81. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  82. package/dist/prompts/sandbox/multi-omics-integration-agent.js +3 -1
  83. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  84. package/dist/prompts/sandbox/multimodal-sc-agent.js +3 -1
  85. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  86. package/dist/prompts/sandbox/network-agent.js +3 -1
  87. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  88. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +4 -7
  89. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  90. package/dist/prompts/sandbox/proteomics-agent.js +3 -1
  91. package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
  92. package/dist/prompts/sandbox/scientific-executor.js +0 -12
  93. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/single-cell-agent.js +3 -1
  95. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  96. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -1
  97. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  98. package/dist/prompts/sandbox/statistical-modeling-agent.js +4 -4
  99. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  100. package/dist/prompts/sandbox/translational-safety-agent.js +10 -29
  101. package/dist/prompts/sandbox-standards.d.ts +15 -18
  102. package/dist/prompts/sandbox-standards.js +96 -305
  103. package/dist/prompts/synthesis-agent.d.ts +1 -1
  104. package/dist/prompts/synthesis-agent.js +24 -90
  105. package/dist/provenance/collector.d.ts +2 -5
  106. package/dist/provenance/collector.js +2 -5
  107. package/dist/provenance/seam.d.ts +36 -1
  108. package/dist/provenance/workspace-reads.d.ts +36 -0
  109. package/dist/provenance/workspace-reads.js +33 -0
  110. package/dist/providers/request-headers.js +12 -3
  111. package/dist/report-model/production-resolver.d.ts +22 -52
  112. package/dist/report-model/production-resolver.js +43 -112
  113. package/dist/report-render/chart-export.d.ts +9 -0
  114. package/dist/report-render/chart-export.js +1 -1
  115. package/dist/report-render/views/chart-view.d.ts +1 -0
  116. package/dist/report-render/views/chart-view.js +1 -0
  117. package/dist/report-render/views/lineage.d.ts +1 -0
  118. package/dist/report-render/views/page-view.d.ts +1 -0
  119. package/dist/report-render/views/page-view.js +1 -0
  120. package/dist/report-render/views/prose.d.ts +1 -0
  121. package/dist/report-render/views/prose.js +1 -0
  122. package/dist/report-render/views/references-view.d.ts +1 -0
  123. package/dist/report-render/views/values.d.ts +1 -0
  124. package/dist/report-render/views/values.js +1 -0
  125. package/dist/runtime/assemble.d.ts +20 -9
  126. package/dist/runtime/assemble.js +47 -38
  127. package/dist/sandbox/client.d.ts +5 -3
  128. package/dist/sandbox/create-sandbox.d.ts +7 -12
  129. package/dist/sandbox/create-sandbox.js +15 -37
  130. package/dist/sandbox/docker-client.d.ts +1 -3
  131. package/dist/sandbox/docker-client.js +3 -5
  132. package/dist/sandbox/exec-outcome.d.ts +11 -11
  133. package/dist/sandbox/exec-outcome.js +22 -22
  134. package/dist/sandbox/exec.d.ts +5 -0
  135. package/dist/sandbox/exec.js +9 -1
  136. package/dist/sandbox/identity.d.ts +4 -16
  137. package/dist/sandbox/identity.js +5 -21
  138. package/dist/sandbox/k8s-client.d.ts +0 -1
  139. package/dist/sandbox/k8s-client.js +8 -13
  140. package/dist/sandbox/lease-names.d.ts +6 -0
  141. package/dist/sandbox/lease-names.js +12 -0
  142. package/dist/sandbox/leases.d.ts +105 -0
  143. package/dist/sandbox/leases.js +519 -0
  144. package/dist/sandbox/liveness.d.ts +5 -2
  145. package/dist/sandbox/mount-plan.d.ts +10 -1
  146. package/dist/sandbox/mount-plan.js +7 -1
  147. package/dist/sandbox/reconciler.d.ts +58 -0
  148. package/dist/sandbox/reconciler.js +153 -0
  149. package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
  150. package/dist/sandbox/types.d.ts +25 -18
  151. package/dist/sandbox/types.js +10 -4
  152. package/dist/schemas/plan-schemas.d.ts +0 -21
  153. package/dist/schemas/plan-schemas.js +2 -3
  154. package/dist/schemas/run-synthesis.d.ts +2 -2
  155. package/dist/schemas/run-synthesis.js +3 -3
  156. package/dist/schemas/validate-plan.d.ts +1 -14
  157. package/dist/schemas/validate-plan.js +5 -24
  158. package/dist/schemas/workflow-state.d.ts +3 -32
  159. package/dist/schemas/workflow-state.js +4 -13
  160. package/dist/state/index.d.ts +0 -1
  161. package/dist/state/index.js +0 -1
  162. package/dist/state/migrations/20261030120000_lease_the_sandboxes.d.ts +7 -0
  163. package/dist/state/migrations/20261030120000_lease_the_sandboxes.js +34 -0
  164. package/dist/state/migrations/20261030120100_drop_the_report_derivations.d.ts +5 -0
  165. package/dist/state/migrations/20261030120100_drop_the_report_derivations.js +9 -0
  166. package/dist/state/migrations/20261030120200_keep_the_report_derivations.d.ts +6 -0
  167. package/dist/state/migrations/20261030120200_keep_the_report_derivations.js +10 -0
  168. package/dist/state/migrations/index.js +6 -0
  169. package/dist/state/purge-analysis.d.ts +4 -2
  170. package/dist/state/purge-analysis.js +22 -11
  171. package/dist/state/sandbox-leases.d.ts +81 -0
  172. package/dist/state/sandbox-leases.js +247 -0
  173. package/dist/state/schema.d.ts +1 -13
  174. package/dist/state/schema.js +1 -6
  175. package/dist/state/step-executions.d.ts +0 -2
  176. package/dist/state/step-executions.js +1 -4
  177. package/dist/tasks/data-profile.d.ts +40 -2
  178. package/dist/tasks/data-profile.js +89 -87
  179. package/dist/testing.d.ts +2 -0
  180. package/dist/testing.js +6 -1
  181. package/dist/tools/ad-hoc-router.d.ts +1 -6
  182. package/dist/tools/ad-hoc-router.js +5 -48
  183. package/dist/tools/bio/comptox.js +1 -1
  184. package/dist/tools/bio/drug-gene-interactions.js +1 -1
  185. package/dist/tools/bio/gene-disease-evidence.js +1 -1
  186. package/dist/tools/bio/keys.d.ts +1 -1
  187. package/dist/tools/bio/pubmed.js +31 -0
  188. package/dist/tools/bio/search-gene.js +17 -0
  189. package/dist/tools/bio/search-geo-datasets.js +13 -0
  190. package/dist/tools/bio/search-regulatory-approvals.js +1 -1
  191. package/dist/tools/define-tool.d.ts +25 -2
  192. package/dist/tools/define-tool.js +11 -1
  193. package/dist/tools/display/show-user.js +1 -1
  194. package/dist/tools/execute-analysis.d.ts +1 -3
  195. package/dist/tools/execute-analysis.js +5 -33
  196. package/dist/tools/report-session/index.d.ts +0 -1
  197. package/dist/tools/report-session/index.js +0 -1
  198. package/dist/tools/report-session/script-tools.d.ts +30 -0
  199. package/dist/tools/report-session/script-tools.js +177 -0
  200. package/dist/tools/research/generate-analogy-report.d.ts +16 -9
  201. package/dist/tools/research/generate-analogy-report.js +111 -49
  202. package/dist/tools/research/generate-plan.d.ts +102 -7
  203. package/dist/tools/research/generate-plan.js +74 -43
  204. package/dist/tools/research/index.d.ts +4 -3
  205. package/dist/tools/research/index.js +4 -3
  206. package/dist/tools/research/inspect-run.d.ts +0 -1
  207. package/dist/tools/research/inspect-run.js +5 -10
  208. package/dist/tools/research/search-arxiv.js +15 -0
  209. package/dist/tools/research/search-github-repos.js +13 -0
  210. package/dist/tools/research/search-semantic-scholar.js +16 -0
  211. package/dist/tools/research/synthesis-lookups.d.ts +21 -0
  212. package/dist/tools/research/synthesis-lookups.js +29 -0
  213. package/dist/tools/research/web-fetch.d.ts +1 -1
  214. package/dist/tools/research/web-fetch.js +14 -12
  215. package/dist/tools/sandbox/lease-tools.d.ts +32 -0
  216. package/dist/tools/sandbox/lease-tools.js +60 -0
  217. package/dist/tools/script/codemode-worker.d.ts +5 -0
  218. package/dist/tools/script/codemode-worker.js +5 -0
  219. package/dist/tools/script/file-tools.d.ts +100 -0
  220. package/dist/tools/script/file-tools.js +377 -0
  221. package/dist/tools/script/metrics.d.ts +21 -0
  222. package/dist/tools/script/metrics.js +38 -0
  223. package/dist/tools/script/quickjs.wasm +0 -0
  224. package/dist/tools/script/run-script.d.ts +76 -0
  225. package/dist/tools/script/run-script.js +326 -0
  226. package/dist/tools/script/scratch.d.ts +81 -0
  227. package/dist/tools/script/scratch.js +142 -0
  228. package/dist/tools/start-report-session.js +1 -1
  229. package/dist/tools/workspace/edit-file.js +2 -1
  230. package/dist/tools/workspace/execute-command.d.ts +9 -13
  231. package/dist/tools/workspace/execute-command.js +23 -19
  232. package/dist/tools/workspace/file-stat.js +1 -1
  233. package/dist/tools/workspace/grep.d.ts +5 -2
  234. package/dist/tools/workspace/grep.js +19 -10
  235. package/dist/tools/workspace/mutator.d.ts +48 -4
  236. package/dist/tools/workspace/mutator.js +43 -12
  237. package/dist/tools/workspace/read-file.d.ts +5 -2
  238. package/dist/tools/workspace/read-file.js +15 -11
  239. package/dist/tools/workspace/read-surface.d.ts +17 -0
  240. package/dist/tools/workspace/read-surface.js +17 -0
  241. package/dist/tools/workspace/result-bounds.d.ts +1 -0
  242. package/dist/tools/workspace/run-exec.d.ts +12 -13
  243. package/dist/tools/workspace/run-exec.js +13 -13
  244. package/dist/tools/workspace/scan-inputs.d.ts +2 -5
  245. package/dist/tools/workspace/scan-inputs.js +2 -3
  246. package/dist/tools/workspace/show-file.js +1 -1
  247. package/dist/tools/workspace/show-plan.js +4 -3
  248. package/dist/tools/workspace/write-file.js +2 -1
  249. package/dist/workflows/execute-analysis-scheduler.d.ts +0 -26
  250. package/dist/workflows/execute-analysis-scheduler.js +12 -32
  251. package/dist/workflows/execute-analysis.d.ts +17 -35
  252. package/dist/workflows/execute-analysis.js +13 -107
  253. package/dist/workflows/pause.js +29 -22
  254. package/dist/workflows/sandbox-step.d.ts +16 -31
  255. package/dist/workflows/sandbox-step.js +36 -71
  256. package/dist/workflows/test/sql-writes.d.ts +0 -1
  257. package/dist/workflows/test/sql-writes.js +0 -2
  258. package/dist/workspace/filesystem.d.ts +2 -2
  259. package/dist/workspace/filesystem.js +10 -5
  260. package/dist/workspace/paths.d.ts +19 -1
  261. package/dist/workspace/paths.js +22 -1
  262. package/package.json +6 -3
  263. package/dist/prompts/literature-reviewer.d.ts +0 -1
  264. package/dist/prompts/literature-reviewer.js +0 -114
  265. package/dist/sandbox/reaper.d.ts +0 -81
  266. package/dist/sandbox/reaper.js +0 -161
  267. package/dist/sandbox/step-sandbox.d.ts +0 -47
  268. package/dist/sandbox/step-sandbox.js +0 -53
  269. package/dist/state/active-sandboxes.d.ts +0 -33
  270. package/dist/state/active-sandboxes.js +0 -65
  271. package/dist/tasks/extract-values-script.d.ts +0 -64
  272. package/dist/tasks/extract-values-script.js +0 -180
  273. package/dist/tasks/extract-values.d.ts +0 -77
  274. package/dist/tasks/extract-values.js +0 -146
  275. package/dist/tools/report-session/derive-table.d.ts +0 -164
  276. package/dist/tools/report-session/derive-table.js +0 -419
  277. package/dist/tools/research/literature-reviewer.d.ts +0 -42
  278. package/dist/tools/research/literature-reviewer.js +0 -107
  279. package/dist/workflows/metrics.d.ts +0 -23
  280. package/dist/workflows/metrics.js +0 -38
@@ -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. A caller that runs more than one exec in one
63
- * step, or an exec outside a workflow, gives its own id
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. Injected at the composition root
6
- * as a construction-time dependency (see the harness-durable-runtime spec); callers do NOT import a
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. Injected at the composition root
6
- * as a construction-time dependency (see the harness-durable-runtime spec); callers do NOT import a
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 { noteExecOutcome, sandboxExecOutcomeOf, summarizeExec } from "./exec-outcome.js";
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 precreated = await ResultAsync.fromPromise(precreateStepTree({
166
+ const treeDeps = {
189
167
  resolveWorkspaceRoot: config.resolveWorkspaceRoot,
190
168
  stepTreeAccess: config.engineBindOwnership === "host-preserved" ? "world-writable" : undefined,
191
- }, mountCoordsOf(session, spec)), createFailed("createSandbox.precreateStepTree"));
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. Matches the `reaper`/`watchdog` logger seam.
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.childWorkflowId }),
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.childWorkflowId, createFailed, logger);
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 sandbox lives in one process-local cell,
12
- * keyed by sandbox id. A sandbox belongs to one step (see the teardown of
13
- * `create-sandbox.ts`), thus the cell is step-scoped in effect and the step
14
- * body reads it without a new dependency through the agent factory. A replay
15
- * re-runs the await loop and writes the cell again, thus a recovered body
16
- * reads the summary of the same exec. The map is capped, so a sandbox whose
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 `sandboxId`. */
43
- export declare function noteExecOutcome(sandboxId: string, summary: ExecOutcomeSummary): void;
44
- /** The last known exec of `sandboxId`, or `undefined` when it ran none. */
45
- export declare function lastExecOutcome(sandboxId: string): ExecOutcomeSummary | undefined;
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 sandbox lives in one process-local cell,
12
- * keyed by sandbox id. A sandbox belongs to one step (see the teardown of
13
- * `create-sandbox.ts`), thus the cell is step-scoped in effect and the step
14
- * body reads it without a new dependency through the agent factory. A replay
15
- * re-runs the await loop and writes the cell again, thus a recovered body
16
- * reads the summary of the same exec. The map is capped, so a sandbox whose
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 sandboxes keep a summary. A host runs far fewer sandboxes at once
45
- * than this, thus the cap only drops the summary of a step that ended long ago.
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 MAX_TRACKED_SANDBOXES = 256;
48
- const lastExecBySandbox = new Map();
49
- /** Keep `summary` as the last known exec of `sandboxId`. */
50
- export function noteExecOutcome(sandboxId, summary) {
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 sandbox.
53
- lastExecBySandbox.delete(sandboxId);
54
- lastExecBySandbox.set(sandboxId, summary);
55
- while (lastExecBySandbox.size > MAX_TRACKED_SANDBOXES) {
56
- const oldest = lastExecBySandbox.keys().next();
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
- lastExecBySandbox.delete(oldest.value);
59
+ lastExecByLease.delete(oldest.value);
60
60
  }
61
61
  }
62
- /** The last known exec of `sandboxId`, or `undefined` when it ran none. */
63
- export function lastExecOutcome(sandboxId) {
64
- return lastExecBySandbox.get(sandboxId);
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
  }
@@ -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
  *
@@ -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
- * Sandbox identity minting — the durable half of the two-step create (ADR
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.
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}` — the name of a sandbox whose identity comes from a
18
- * key, not from a checkpoint. The same key gives the same name, thus a rerun
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;
@@ -1,27 +1,11 @@
1
1
  /**
2
- * Sandbox identity minting — the durable half of the two-step create (ADR
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.
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, randomUUID } from "node:crypto";
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}` — the name of a sandbox whose identity comes from a
22
- * key, not from a checkpoint. The same key gives the same name, thus a rerun
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.childWorkflowId,
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.childWorkflowId, job);
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;