@inflexa-ai/harness 0.42.1 → 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.
- package/README.md +2 -2
- package/dist/agents/conversation-agent.js +5 -1
- package/dist/agents/sandbox/drug-repurposing-agent.js +1 -0
- package/dist/agents/sandbox/shared.d.ts +1 -6
- package/dist/agents/sandbox/shared.js +3 -16
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/app/chat-turn.js +13 -1
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +9 -1
- package/dist/contracts/chat-frame.d.ts +3 -3
- package/dist/contracts/chat-frame.js +8 -3
- package/dist/contracts/chat-parts.d.ts +4 -1
- package/dist/contracts/data-profile.d.ts +4 -6
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/report-blocks.d.ts +1364 -75
- package/dist/contracts/report-blocks.js +221 -29
- package/dist/contracts/report-reference.js +1 -1
- package/dist/contracts/schemas/chat-events.d.ts +13 -0
- package/dist/contracts/schemas/chat-events.js +5 -0
- package/dist/contracts/schemas/chat-parts.d.ts +2 -2
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/execution/input-signature.d.ts +7 -13
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/run-canceler.d.ts +3 -2
- package/dist/execution/run-canceler.js +3 -2
- package/dist/execution/run-event-stream.d.ts +16 -2
- package/dist/execution/run-event-stream.js +54 -3
- package/dist/index.d.ts +1 -8
- package/dist/index.js +5 -16
- package/dist/input-scan/enrich.d.ts +0 -2
- package/dist/input-scan/enrich.js +0 -1
- package/dist/lib/metrics.js +6 -7
- package/dist/lib/otel-spans.d.ts +4 -9
- package/dist/lib/otel-spans.js +3 -8
- package/dist/loop/run-agent.d.ts +4 -2
- package/dist/loop/run-agent.js +38 -23
- package/dist/memory/thread-history.d.ts +1 -1
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +17 -0
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +7 -0
- package/dist/provenance/exec-frame.d.ts +1 -1
- package/dist/report-model/authoring-grammar.d.ts +416 -17
- package/dist/report-model/authoring-grammar.js +25 -4
- package/dist/report-model/block-walk.d.ts +20 -4
- package/dist/report-model/block-walk.js +64 -7
- package/dist/report-model/draft-finish.d.ts +6 -2
- package/dist/report-model/draft-finish.js +6 -1
- package/dist/report-model/production-resolver.d.ts +8 -0
- package/dist/report-model/production-resolver.js +1 -1
- package/dist/report-model/validate.d.ts +20 -10
- package/dist/report-model/validate.js +29 -13
- package/dist/report-render/chart-export.d.ts +52 -0
- package/dist/report-render/chart-export.js +192 -0
- package/dist/report-render/chart-presets.d.ts +46 -47
- package/dist/report-render/chart-presets.js +31 -101
- package/dist/report-render/chart-renderers.d.ts +163 -0
- package/dist/report-render/chart-renderers.js +1055 -0
- package/dist/report-render/chart-toolbox.d.ts +124 -0
- package/dist/report-render/chart-toolbox.js +595 -0
- package/dist/report-render/chart.d.ts +60 -27
- package/dist/report-render/chart.js +2323 -524
- package/dist/report-render/design.d.ts +476 -16
- package/dist/report-render/design.js +390 -56
- package/dist/report-render/figures/common.d.ts +321 -0
- package/dist/report-render/figures/common.js +759 -0
- package/dist/report-render/figures/dendrogram.d.ts +36 -0
- package/dist/report-render/figures/dendrogram.js +130 -0
- package/dist/report-render/figures/dense.d.ts +111 -0
- package/dist/report-render/figures/dense.js +204 -0
- package/dist/report-render/figures/dotplot.d.ts +28 -0
- package/dist/report-render/figures/dotplot.js +234 -0
- package/dist/report-render/figures/embedding.d.ts +43 -0
- package/dist/report-render/figures/embedding.js +360 -0
- package/dist/report-render/figures/equal-units.d.ts +101 -0
- package/dist/report-render/figures/equal-units.js +215 -0
- package/dist/report-render/figures/forest.d.ts +20 -0
- package/dist/report-render/figures/forest.js +398 -0
- package/dist/report-render/figures/gsea.d.ts +30 -0
- package/dist/report-render/figures/gsea.js +391 -0
- package/dist/report-render/figures/heatmap.d.ts +40 -0
- package/dist/report-render/figures/heatmap.js +395 -0
- package/dist/report-render/figures/index.d.ts +135 -0
- package/dist/report-render/figures/index.js +100 -0
- package/dist/report-render/figures/km.d.ts +16 -0
- package/dist/report-render/figures/km.js +406 -0
- package/dist/report-render/figures/label-room.d.ts +130 -0
- package/dist/report-render/figures/label-room.js +341 -0
- package/dist/report-render/figures/locuszoom.d.ts +50 -0
- package/dist/report-render/figures/locuszoom.js +675 -0
- package/dist/report-render/figures/lollipop.d.ts +16 -0
- package/dist/report-render/figures/lollipop.js +247 -0
- package/dist/report-render/figures/ma.d.ts +20 -0
- package/dist/report-render/figures/ma.js +114 -0
- package/dist/report-render/figures/manhattan.d.ts +60 -0
- package/dist/report-render/figures/manhattan.js +343 -0
- package/dist/report-render/figures/oncoprint.d.ts +41 -0
- package/dist/report-render/figures/oncoprint.js +420 -0
- package/dist/report-render/figures/pca.d.ts +37 -0
- package/dist/report-render/figures/pca.js +241 -0
- package/dist/report-render/figures/qq.d.ts +30 -0
- package/dist/report-render/figures/qq.js +182 -0
- package/dist/report-render/figures/roc.d.ts +17 -0
- package/dist/report-render/figures/roc.js +116 -0
- package/dist/report-render/figures/sankey.d.ts +48 -0
- package/dist/report-render/figures/sankey.js +353 -0
- package/dist/report-render/figures/upset.d.ts +60 -0
- package/dist/report-render/figures/upset.js +316 -0
- package/dist/report-render/figures/volcano.d.ts +25 -0
- package/dist/report-render/figures/volcano.js +166 -0
- package/dist/report-render/hybrid-svg.d.ts +75 -0
- package/dist/report-render/hybrid-svg.js +504 -0
- package/dist/report-render/number-format.d.ts +19 -0
- package/dist/report-render/number-format.js +25 -1
- package/dist/report-render/page.d.ts +41 -4
- package/dist/report-render/page.js +247 -12
- package/dist/report-render/render.d.ts +13 -4
- package/dist/report-render/render.js +37 -6
- package/dist/report-render/types.d.ts +23 -2
- package/dist/report-render/value-bridge.d.ts +38 -3
- package/dist/report-render/value-bridge.js +137 -6
- package/dist/report-render/views/chart-view.d.ts +42 -6
- package/dist/report-render/views/chart-view.js +70 -9
- package/dist/report-render/views/page-view.js +9 -10
- package/dist/runtime/assemble.js +2 -2
- package/dist/sandbox/client.d.ts +23 -32
- package/dist/sandbox/client.js +3 -11
- package/dist/sandbox/create-sandbox.d.ts +9 -24
- package/dist/sandbox/create-sandbox.js +15 -30
- package/dist/sandbox/docker-client.d.ts +12 -22
- package/dist/sandbox/docker-client.js +29 -43
- package/dist/sandbox/exec.d.ts +58 -0
- package/dist/sandbox/exec.js +197 -0
- package/dist/sandbox/identity.d.ts +5 -5
- package/dist/sandbox/identity.js +7 -10
- package/dist/sandbox/k8s-client.d.ts +4 -10
- package/dist/sandbox/k8s-client.js +5 -12
- package/dist/sandbox/liveness.d.ts +5 -12
- package/dist/sandbox/liveness.js +5 -12
- package/dist/sandbox/notification-sweep.d.ts +4 -7
- package/dist/sandbox/notification-sweep.js +4 -7
- package/dist/sandbox/reaper.d.ts +3 -3
- package/dist/sandbox/reaper.js +3 -3
- package/dist/sandbox/sandbox-error.d.ts +10 -11
- package/dist/sandbox/sandbox-error.js +10 -11
- package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
- package/dist/sandbox/sandbox-step-translate.js +1 -1
- package/dist/sandbox/types.d.ts +31 -99
- package/dist/sandbox/types.js +17 -84
- package/dist/state/active-sandboxes.d.ts +7 -32
- package/dist/state/active-sandboxes.js +11 -46
- package/dist/state/index.d.ts +1 -2
- package/dist/state/index.js +1 -1
- package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.d.ts +7 -0
- package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.js +12 -0
- package/dist/state/migrations/index.js +2 -0
- package/dist/state/schema.d.ts +1 -5
- package/dist/state/schema.js +1 -6
- package/dist/state/step-executions.js +1 -2
- package/dist/tasks/data-profile.js +7 -20
- package/dist/tasks/derive-table-exec.d.ts +4 -14
- package/dist/tasks/derive-table-exec.js +9 -24
- package/dist/tasks/extract-values.d.ts +2 -2
- package/dist/tasks/extract-values.js +4 -7
- package/dist/tools/bio/index.d.ts +2 -0
- package/dist/tools/bio/index.js +3 -0
- package/dist/tools/bio/keys.d.ts +11 -0
- package/dist/tools/bio/keys.js +15 -0
- package/dist/tools/bio/search-clinical-trials.d.ts +1 -1
- package/dist/tools/bio/search-patents.d.ts +46 -0
- package/dist/tools/bio/search-patents.js +134 -0
- package/dist/tools/bio/search-regulatory-approvals.d.ts +49 -0
- package/dist/tools/bio/search-regulatory-approvals.js +158 -0
- package/dist/tools/define-tool.d.ts +56 -13
- package/dist/tools/define-tool.js +28 -1
- package/dist/tools/execute-analysis.js +3 -3
- package/dist/tools/lib/amass-client.d.ts +586 -0
- package/dist/tools/lib/amass-client.js +492 -0
- package/dist/tools/lib/amass-config.d.ts +2 -0
- package/dist/tools/lib/amass-config.js +10 -0
- package/dist/tools/report-session/derive-table.d.ts +8 -9
- package/dist/tools/report-session/derive-table.js +3 -4
- package/dist/tools/report-session/preview-report.js +4 -49
- package/dist/tools/sandbox/list-available-packages.d.ts +1 -1
- package/dist/tools/sandbox/list-available-packages.js +5 -4
- package/dist/tools/workspace/edit-file.d.ts +5 -4
- package/dist/tools/workspace/edit-file.js +10 -13
- package/dist/tools/workspace/execute-command.d.ts +27 -42
- package/dist/tools/workspace/execute-command.js +42 -38
- package/dist/tools/workspace/index.d.ts +5 -6
- package/dist/tools/workspace/index.js +5 -6
- package/dist/tools/workspace/mutator.d.ts +43 -8
- package/dist/tools/workspace/mutator.js +45 -34
- package/dist/tools/workspace/run-exec.d.ts +3 -12
- package/dist/tools/workspace/run-exec.js +7 -24
- package/dist/tools/workspace/scan-inputs.d.ts +0 -3
- package/dist/tools/workspace/scan-inputs.js +0 -1
- package/dist/tools/workspace/write-file.d.ts +12 -3
- package/dist/tools/workspace/write-file.js +17 -13
- package/dist/workflows/sandbox-step.d.ts +7 -11
- package/dist/workflows/sandbox-step.js +24 -27
- package/package.json +3 -1
- package/dist/sandbox/await-exec.d.ts +0 -143
- package/dist/sandbox/await-exec.js +0 -417
- package/dist/sandbox/deliver-exec-event.d.ts +0 -25
- package/dist/sandbox/deliver-exec-event.js +0 -29
- package/dist/sandbox/digest.d.ts +0 -14
- package/dist/sandbox/digest.js +0 -17
- package/dist/sandbox/exec-id.d.ts +0 -12
- package/dist/sandbox/exec-id.js +0 -20
- package/dist/sandbox/hmac.d.ts +0 -62
- package/dist/sandbox/hmac.js +0 -72
- package/dist/sandbox/submit-exec.d.ts +0 -38
- package/dist/sandbox/submit-exec.js +0 -80
- package/dist/sandbox/watchdog.d.ts +0 -71
- package/dist/sandbox/watchdog.js +0 -161
package/dist/sandbox/hmac.js
DELETED
|
@@ -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;
|
package/dist/sandbox/watchdog.js
DELETED
|
@@ -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
|
-
}
|