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