@inflexa-ai/harness 0.42.2 → 0.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +5 -3
  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
@@ -1,72 +0,0 @@
1
- /**
2
- * HMAC verification for sandbox callbacks (see the harness-sandbox-exec spec).
3
- *
4
- * sandbox-server signs every outbound event/completion POST with
5
- * `X-Sandbox-Signature = hex(HMAC-SHA256(callbackSecret,
6
- * "${execId}:${timestamp}:${sha256Hex(body)}"))` plus a
7
- * `X-Sandbox-Timestamp` header. The Cortex-side callback endpoint is
8
- * **dumb** — it forwards the headers verbatim onto the per-exec DBOS
9
- * topic. Verification happens here, in the workflow-body recv loop,
10
- * because that body holds the `callbackSecret` from the cached
11
- * `createSandbox` step output.
12
- *
13
- * A bad or stale signature triggers a hard cancel of the run — under
14
- * NetworkPolicy isolation a forged event implies a bug or a breach,
15
- * neither of which should keep a 3-hour sandbox burning.
16
- */
17
- import { createHmac, timingSafeEqual } from "node:crypto";
18
- import { digestBody } from "./digest.js";
19
- /**
20
- * Decode the per-sandbox secret. sandbox-server accepts both raw UTF-8
21
- * and `base64:` prefixed values (change 4 spec); we mirror the same
22
- * convention here so the Cortex and Go sides interpret the secret
23
- * identically.
24
- */
25
- function decodeSecret(secret) {
26
- if (secret.startsWith("base64:")) {
27
- return Buffer.from(secret.slice("base64:".length), "base64");
28
- }
29
- return Buffer.from(secret, "utf8");
30
- }
31
- function sha256Hex(input) {
32
- return digestBody(input);
33
- }
34
- /**
35
- * Recompute the HMAC and compare in constant time, then check the
36
- * timestamp falls within a symmetric `freshnessSec` window of `nowSec`.
37
- *
38
- * Order: `missing` (one of signature/timestamp is null) is reported
39
- * separately from `bad-signature` so the caller can log it precisely.
40
- * Freshness is checked AFTER signature, so a stale-but-valid signature
41
- * is distinguishable from a forgery.
42
- */
43
- export function verifyCallback(input) {
44
- if (input.signature === null || input.timestamp === null) {
45
- return { valid: false, reason: "missing" };
46
- }
47
- const digest = input.bodyDigest ?? (input.body !== undefined ? sha256Hex(input.body) : undefined);
48
- if (digest === undefined) {
49
- return { valid: false, reason: "missing" };
50
- }
51
- const secretBytes = decodeSecret(input.secret);
52
- const message = `${input.execId}:${input.timestamp}:${digest}`;
53
- const expected = createHmac("sha256", secretBytes).update(message).digest("hex");
54
- const provided = input.signature;
55
- if (provided.length !== expected.length) {
56
- return { valid: false, reason: "bad-signature" };
57
- }
58
- const matches = timingSafeEqual(Buffer.from(provided, "utf8"), Buffer.from(expected, "utf8"));
59
- if (!matches) {
60
- return { valid: false, reason: "bad-signature" };
61
- }
62
- if (Math.abs(input.nowSec - input.timestamp) > input.freshnessSec) {
63
- return { valid: false, reason: "stale-timestamp" };
64
- }
65
- return { valid: true };
66
- }
67
- /** Convenience for the Cortex side to compute a signature for tests. */
68
- export function signCallback({ execId, body, timestamp, secret }) {
69
- const secretBytes = decodeSecret(secret);
70
- const message = `${execId}:${timestamp}:${sha256Hex(body)}`;
71
- return createHmac("sha256", secretBytes).update(message).digest("hex");
72
- }
@@ -1,38 +0,0 @@
1
- /**
2
- * `submitExec` — backend-agnostic DBOS step that POSTs a command to
3
- * sandbox-server's `/exec` and returns after the HTTP 202 ack (ADR
4
- * 0002). `submitExec` wraps its own POST in a `DBOS.runStep` keyed
5
- * `sandbox.submit-exec.${execId}`.
6
- *
7
- * Step name: `sandbox.submit-exec.${execId}` — `execId` is already unique
8
- * across the workflow (`${workflowId}:${stepId}:${functionId}`), so it
9
- * doubles as the DBOS step replay cache key. On replay the cached step
10
- * output is returned without re-POSTing; any duplicate POST that
11
- * actually reaches sandbox-server during a narrow in-flight window is
12
- * deduped server-side (PR #3 change 4) on the same `execId`.
13
- *
14
- * A non-202 status throws — the step boundary is throw-based by design
15
- * (the `resultStep` principle): durability records a step as failed only
16
- * on a thrown exception, and a throw lets DBOS cancellation propagate
17
- * untouched.
18
- */
19
- import type { SandboxRef, SubmitExecBody } from "./types.js";
20
- export interface SubmitExecDeps {
21
- /** Injected for tests. Defaults to `globalThis.fetch`. */
22
- fetch?: typeof fetch;
23
- /** Injected for tests. Defaults to `DBOS.runStep`. */
24
- runStep?: <T>(fn: () => Promise<T>, config: {
25
- name: string;
26
- }) => Promise<T>;
27
- /**
28
- * Per-stream retention budget to send when the body does not carry one.
29
- * Defaults to `EXEC_STREAM_BYTE_CAP`, the maximum of a kept tool output.
30
- */
31
- execStreamByteCap?: number;
32
- }
33
- /**
34
- * Submit an exec as a DBOS step. The step name is unique per execId, so
35
- * a workflow that has already submitted the exec replays without issuing
36
- * another POST.
37
- */
38
- export declare function submitExec(ref: SandboxRef, body: SubmitExecBody, deps?: SubmitExecDeps): Promise<void>;
@@ -1,80 +0,0 @@
1
- /**
2
- * `submitExec` — backend-agnostic DBOS step that POSTs a command to
3
- * sandbox-server's `/exec` and returns after the HTTP 202 ack (ADR
4
- * 0002). `submitExec` wraps its own POST in a `DBOS.runStep` keyed
5
- * `sandbox.submit-exec.${execId}`.
6
- *
7
- * Step name: `sandbox.submit-exec.${execId}` — `execId` is already unique
8
- * across the workflow (`${workflowId}:${stepId}:${functionId}`), so it
9
- * doubles as the DBOS step replay cache key. On replay the cached step
10
- * output is returned without re-POSTing; any duplicate POST that
11
- * actually reaches sandbox-server during a narrow in-flight window is
12
- * deduped server-side (PR #3 change 4) on the same `execId`.
13
- *
14
- * A non-202 status throws — the step boundary is throw-based by design
15
- * (the `resultStep` principle): durability records a step as failed only
16
- * on a thrown exception, and a throw lets DBOS cancellation propagate
17
- * untouched.
18
- */
19
- import { DBOS } from "@dbos-inc/dbos-sdk";
20
- import { ATTR_INFLEXA_EXEC_ID, stableSpan, untracedFetch } from "../lib/otel-spans.js";
21
- import { EXEC_STREAM_BYTE_CAP } from "../tools/workspace/result-bounds.js";
22
- import { signCallback } from "./hmac.js";
23
- const SUBMIT_HTTP_TIMEOUT_MS = 30_000;
24
- async function postExec(fetchImpl, ref, body) {
25
- const controller = new AbortController();
26
- const timer = setTimeout(() => controller.abort(), SUBMIT_HTTP_TIMEOUT_MS);
27
- try {
28
- // Sign the exact bytes we send, over the same HMAC construction the
29
- // sandbox uses for its callbacks — sandbox-server authenticates every
30
- // inbound request. `Date.now()` is safe here: this whole POST runs inside
31
- // a `DBOS.runStep` whose cached result is replayed without re-executing
32
- // the body, so the timestamp never re-computes on recovery.
33
- const raw = JSON.stringify(body);
34
- const timestamp = Math.floor(Date.now() / 1000);
35
- const signature = signCallback({ execId: body.execId, body: raw, timestamp, secret: ref.callbackSecret });
36
- const res = await untracedFetch(fetchImpl, `http://${ref.host}:${ref.port}/exec`, {
37
- method: "POST",
38
- headers: {
39
- "content-type": "application/json",
40
- "x-sandbox-signature": signature,
41
- "x-sandbox-timestamp": String(timestamp),
42
- },
43
- body: raw,
44
- signal: controller.signal,
45
- });
46
- if (res.status !== 202) {
47
- const text = await res.text().catch(() => "");
48
- throw Object.assign(new Error(`submitExec: sandbox-server returned ${res.status} for execId=${body.execId}: ${text}`), { statusCode: res.status });
49
- }
50
- // The 202 body carries either `{ status: "started" }` (fresh) or a
51
- // dedup-hit summary. Both are success — read and discard so the
52
- // socket is released.
53
- await res.text().catch(() => "");
54
- }
55
- finally {
56
- clearTimeout(timer);
57
- }
58
- }
59
- /**
60
- * Submit an exec as a DBOS step. The step name is unique per execId, so
61
- * a workflow that has already submitted the exec replays without issuing
62
- * another POST.
63
- */
64
- export async function submitExec(ref, body, deps = {}) {
65
- const fetchImpl = deps.fetch ?? fetch;
66
- const runStep = deps.runStep ?? ((fn, c) => DBOS.runStep(fn, c));
67
- // Every exec in the process funnels through here, so this is where the
68
- // retention budget is attached rather than at each of the callers that build
69
- // a body. Applied before signing, since the signature covers these bytes.
70
- const capped = {
71
- ...body,
72
- stdoutByteCap: body.stdoutByteCap ?? deps.execStreamByteCap ?? EXEC_STREAM_BYTE_CAP,
73
- stderrByteCap: body.stderrByteCap ?? deps.execStreamByteCap ?? EXEC_STREAM_BYTE_CAP,
74
- };
75
- const stepName = `sandbox.submit-exec.${body.execId}`;
76
- await runStep(() => {
77
- stableSpan(stepName, "sandbox.submit-exec", { [ATTR_INFLEXA_EXEC_ID]: body.execId });
78
- return postExec(fetchImpl, ref, capped);
79
- }, { name: stepName });
80
- }
@@ -1,71 +0,0 @@
1
- /**
2
- * Per-sandbox-machine liveness watchdog (CONTEXT.md "Sandbox exec").
3
- *
4
- * A `@DBOS.scheduled` parent fires every minute, lists the active-sandbox
5
- * registry, shards by `hash(sandboxId) % SHARD_COUNT`, and `startWorkflow`s
6
- * one child check workflow per non-empty shard. The fan-out invariant —
7
- * "no single invocation polls every sandbox" — is the user's explicit
8
- * constraint.
9
- *
10
- * Each child workflow walks its shard, calls `isAlive`, and on `false`
11
- * gates on `DBOS.getWorkflowStatus`: if the owning workflow is still
12
- * `PENDING` or `ENQUEUED`, the child sends a `synthetic-failure`
13
- * done-marker on `exec-event:${execId}` to unblock `awaitExec`. The
14
- * marker carries `signature: null` and `kind: "synthetic-failure"` so the
15
- * recv loop accepts it from the trusted in-process sender (see the harness-sandbox-exec spec).
16
- *
17
- * The pure sharding and check logic is hoisted so it can be unit-tested
18
- * without spinning a DBOS runtime; `registerWatchdog` is the thin
19
- * production wiring.
20
- */
21
- import type { ResultAsync } from "neverthrow";
22
- import type { Logger } from "../lib/logger.js";
23
- import type { DbError } from "../lib/db-result.js";
24
- import type { ActiveSandboxRow } from "../state/index.js";
25
- import type { SandboxClient } from "./client.js";
26
- import type { ExecEventMessage, ExecResult, SandboxLiveness, SandboxRef } from "./types.js";
27
- export declare const SHARD_COUNT = 8;
28
- /** Stable hash → shard mapping, exported for tests. */
29
- export declare function shardIndex(sandboxId: string, shardCount?: number): number;
30
- /** Partition the active-sandbox set into `shardCount` lists. */
31
- export declare function shardActiveSandboxes(rows: ActiveSandboxRow[], shardCount?: number): ActiveSandboxRow[][];
32
- export interface CheckShardDeps {
33
- isAlive: (ref: SandboxRef) => Promise<SandboxLiveness>;
34
- getStatus: (workflowId: string) => Promise<{
35
- status: string;
36
- } | null>;
37
- /**
38
- * Send a synthetic-failure marker on the per-exec topic. In production
39
- * this is `DBOS.send`; injected here for testability.
40
- */
41
- sendSynthetic: (workflowId: string, execId: string, failure: ExecResult, reason: string) => Promise<void>;
42
- logger?: Logger;
43
- }
44
- export interface CheckShardSummary {
45
- activeCount: number;
46
- deadCount: number;
47
- syntheticSends: number;
48
- liveWorkflowsSkipped: number;
49
- }
50
- /**
51
- * Pure check logic over a shard: enumerate, call `isAlive`, and on
52
- * dead-and-still-in-flight send a synthetic-failure. The in-flight guard
53
- * is critical — without it a real `complete` arriving microseconds before
54
- * the watchdog would race with the synthetic and produce a duplicate
55
- * notification.
56
- */
57
- export declare function checkShard(rows: ActiveSandboxRow[], deps: CheckShardDeps): Promise<CheckShardSummary>;
58
- /** Encodes the synthetic-failure marker on the per-exec topic envelope. */
59
- export declare function syntheticFailureMessage(execId: string, failure: ExecResult, reason: string, nowSec: number): ExecEventMessage;
60
- export interface WatchdogDeps {
61
- queryActiveSandboxes: () => ResultAsync<ActiveSandboxRow[], DbError>;
62
- sandboxClient: SandboxClient;
63
- shardCount?: number;
64
- logger?: Logger;
65
- }
66
- /**
67
- * Production registration. Builds the parent + child registered workflows
68
- * and the scheduled trigger. The parent fans out via `startWorkflow` so
69
- * each child is durable in its own right (and recovers independently).
70
- */
71
- export declare function registerWatchdog(deps: WatchdogDeps): void;
@@ -1,161 +0,0 @@
1
- /**
2
- * Per-sandbox-machine liveness watchdog (CONTEXT.md "Sandbox exec").
3
- *
4
- * A `@DBOS.scheduled` parent fires every minute, lists the active-sandbox
5
- * registry, shards by `hash(sandboxId) % SHARD_COUNT`, and `startWorkflow`s
6
- * one child check workflow per non-empty shard. The fan-out invariant —
7
- * "no single invocation polls every sandbox" — is the user's explicit
8
- * constraint.
9
- *
10
- * Each child workflow walks its shard, calls `isAlive`, and on `false`
11
- * gates on `DBOS.getWorkflowStatus`: if the owning workflow is still
12
- * `PENDING` or `ENQUEUED`, the child sends a `synthetic-failure`
13
- * done-marker on `exec-event:${execId}` to unblock `awaitExec`. The
14
- * marker carries `signature: null` and `kind: "synthetic-failure"` so the
15
- * recv loop accepts it from the trusted in-process sender (see the harness-sandbox-exec spec).
16
- *
17
- * The pure sharding and check logic is hoisted so it can be unit-tested
18
- * without spinning a DBOS runtime; `registerWatchdog` is the thin
19
- * production wiring.
20
- */
21
- import { createHash } from "node:crypto";
22
- import { DBOS } from "@dbos-inc/dbos-sdk";
23
- import { createNoopLogger } from "../lib/console-logger.js";
24
- import { untracedWorkflow } from "../lib/otel-spans.js";
25
- import { unwrapOrThrow } from "../lib/result.js";
26
- import { workflowIdFromExec } from "./exec-id.js";
27
- import { syntheticFailureReason, syntheticFailureResult } from "./liveness.js";
28
- export const SHARD_COUNT = 8;
29
- const WATCHDOG_CRON = "*/60 * * * * *";
30
- const WATCHDOG_PARENT_WORKFLOW = "sandbox-watchdog-parent";
31
- const WATCHDOG_SHARD_WORKFLOW = "sandbox-watchdog-shard";
32
- /** Stable hash → shard mapping, exported for tests. */
33
- export function shardIndex(sandboxId, shardCount = SHARD_COUNT) {
34
- const digest = createHash("sha1").update(sandboxId).digest();
35
- const slice = digest.readUInt32BE(0);
36
- return slice % shardCount;
37
- }
38
- /** Partition the active-sandbox set into `shardCount` lists. */
39
- export function shardActiveSandboxes(rows, shardCount = SHARD_COUNT) {
40
- const shards = Array.from({ length: shardCount }, () => []);
41
- for (const row of rows) {
42
- shards[shardIndex(row.sandboxRef.sandboxId, shardCount)].push(row);
43
- }
44
- return shards;
45
- }
46
- const ACTIVE_WORKFLOW_STATUSES = new Set(["PENDING", "ENQUEUED"]);
47
- /**
48
- * Pure check logic over a shard: enumerate, call `isAlive`, and on
49
- * dead-and-still-in-flight send a synthetic-failure. The in-flight guard
50
- * is critical — without it a real `complete` arriving microseconds before
51
- * the watchdog would race with the synthetic and produce a duplicate
52
- * notification.
53
- */
54
- export async function checkShard(rows, deps) {
55
- const logger = (deps.logger ?? createNoopLogger()).named("sandbox-watchdog");
56
- let deadCount = 0;
57
- let syntheticSends = 0;
58
- let liveWorkflowsSkipped = 0;
59
- for (const row of rows) {
60
- // We don't know the secret from here; reconstruct a minimal in-memory
61
- // SandboxRef from the persisted shape. `isAlive` does not consult
62
- // the secret, so this is safe.
63
- const ref = {
64
- ...row.sandboxRef,
65
- callbackSecret: "",
66
- };
67
- let liveness;
68
- try {
69
- liveness = await deps.isAlive(ref);
70
- }
71
- catch (err) {
72
- logger.warn("isAlive threw — skipping this round", {
73
- sandboxId: row.sandboxRef.sandboxId,
74
- ...logger.errorFields(err),
75
- });
76
- continue;
77
- }
78
- if (liveness.alive)
79
- continue;
80
- deadCount++;
81
- const workflowId = row.execId ? workflowIdFromExec(row.execId) : null;
82
- if (!workflowId || !row.execId) {
83
- // No exec was in flight when this row was tagged; nothing to
84
- // unblock. Future change: clear the registry row separately.
85
- continue;
86
- }
87
- const status = await deps.getStatus(workflowId);
88
- if (!status || !ACTIVE_WORKFLOW_STATUSES.has(status.status)) {
89
- liveWorkflowsSkipped++;
90
- continue;
91
- }
92
- const reason = syntheticFailureReason(liveness);
93
- const failure = syntheticFailureResult(row.execId, reason);
94
- await deps.sendSynthetic(workflowId, row.execId, failure, reason);
95
- syntheticSends++;
96
- }
97
- const summary = {
98
- activeCount: rows.length,
99
- deadCount,
100
- syntheticSends,
101
- liveWorkflowsSkipped,
102
- };
103
- // Spread: an interface has no implicit index signature, so it needs widening to `LogFields`.
104
- logger.info("shard check completed", { ...summary });
105
- return summary;
106
- }
107
- /** Encodes the synthetic-failure marker on the per-exec topic envelope. */
108
- export function syntheticFailureMessage(execId, failure, reason, nowSec) {
109
- return {
110
- payload: {
111
- done: true,
112
- result: failure,
113
- kind: "synthetic-failure",
114
- reason,
115
- },
116
- signature: null,
117
- timestamp: nowSec,
118
- };
119
- }
120
- /**
121
- * Production registration. Builds the parent + child registered workflows
122
- * and the scheduled trigger. The parent fans out via `startWorkflow` so
123
- * each child is durable in its own right (and recovers independently).
124
- */
125
- export function registerWatchdog(deps) {
126
- const shardCount = deps.shardCount ?? SHARD_COUNT;
127
- const childCheckShard = DBOS.registerWorkflow(async (rows) => {
128
- return checkShard(rows, {
129
- isAlive: (ref) => deps.sandboxClient.isAlive(ref),
130
- getStatus: async (workflowId) => (await DBOS.getWorkflowStatus(workflowId)),
131
- sendSynthetic: async (workflowId, execId, failure, reason) => {
132
- const msg = syntheticFailureMessage(execId, failure, reason, Math.floor((await DBOS.now()) / 1000));
133
- await DBOS.send(workflowId, msg, `exec-event:${execId}`);
134
- },
135
- logger: deps.logger,
136
- });
137
- }, { name: WATCHDOG_SHARD_WORKFLOW });
138
- untracedWorkflow(WATCHDOG_SHARD_WORKFLOW);
139
- // The scheduled target must itself be a registered workflow — DBOS's
140
- // scheduler loop skips (and errors on) any `@scheduled` function that lacks
141
- // a workflow registration. Register, then schedule the same callable.
142
- const watchdogParent = DBOS.registerWorkflow(async () => {
143
- // Checkpoint the live registry read: the row set gates how many
144
- // `startWorkflow` calls fire below, so a differing set on replay would
145
- // drift the recorded function-ID sequence (see the harness-durable-runtime spec).
146
- const rows = await DBOS.runStep(async () => unwrapOrThrow(await deps.queryActiveSandboxes()), { name: "query-active-sandboxes" });
147
- if (rows.length === 0)
148
- return;
149
- const shards = shardActiveSandboxes(rows, shardCount);
150
- for (const shard of shards) {
151
- if (shard.length === 0)
152
- continue;
153
- await DBOS.startWorkflow(childCheckShard)(shard);
154
- }
155
- }, { name: WATCHDOG_PARENT_WORKFLOW });
156
- untracedWorkflow(WATCHDOG_PARENT_WORKFLOW);
157
- DBOS.registerScheduled(watchdogParent, {
158
- name: WATCHDOG_PARENT_WORKFLOW,
159
- crontab: WATCHDOG_CRON,
160
- });
161
- }