@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
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `awaitExec` — awaits a submitted exec's terminal result, forwarding progress
|
|
3
|
-
* events, under one of two transports selected by `options.transport`:
|
|
4
|
-
*
|
|
5
|
-
* - `poll` (default): {@link awaitExecPoll} loops durable pull steps against
|
|
6
|
-
* `GET /exec/{execId}?since={cursor}`. The sandbox initiates nothing; the
|
|
7
|
-
* host asks. Inherently restart-proof — a recovered workflow just resumes
|
|
8
|
-
* polling from its current identity — so the recovery wedge (#41) does not
|
|
9
|
-
* apply. A machine that dies mid-exec fast-fails in-loop: sustained
|
|
10
|
-
* `unavailable` polls escalate to a backend-inspect probe (`liveness.ts`)
|
|
11
|
-
* and an observably dead machine returns the synthetic-failure result
|
|
12
|
-
* instead of waiting out the deadline.
|
|
13
|
-
* - `callback`: {@link awaitExecCallback} is the recv loop below, with the
|
|
14
|
-
* pull as its recovery backstop.
|
|
15
|
-
*
|
|
16
|
-
* Both run in the workflow body (NOT a step) — `DBOS.recv`, `DBOS.writeStream`,
|
|
17
|
-
* and `DBOS.sleepms` are body-only — and both verify every result against the
|
|
18
|
-
* per-sandbox HMAC, treating a bad/stale signature as a hard cancel.
|
|
19
|
-
*
|
|
20
|
-
* ## Callback mode: the recv loop
|
|
21
|
-
*
|
|
22
|
-
* On each received envelope:
|
|
23
|
-
*
|
|
24
|
-
* 1. If it carries `signature: null` and the payload is a
|
|
25
|
-
* `synthetic-failure` done-marker, accept it — the watchdog is an
|
|
26
|
-
* in-process trusted sender. Any other null-signature message
|
|
27
|
-
* hard-cancels.
|
|
28
|
-
* 2. Otherwise HMAC-verify against the per-sandbox `callbackSecret`.
|
|
29
|
-
* A bad or stale signature throws `HardCancelError`, which DBOS
|
|
30
|
-
* treats as a fatal workflow error (no retry).
|
|
31
|
-
* 3. If the verified payload is a done-marker, return its `result`.
|
|
32
|
-
* 4. Otherwise forward the payload via `emit` and continue.
|
|
33
|
-
*
|
|
34
|
-
* `deadline` is an absolute unix-ms timestamp — the durable backstop
|
|
35
|
-
* (typically `step.timeout`). `T` is liveness-agnostic recv pacing: we
|
|
36
|
-
* use `min(5s, remaining)` so the loop returns often enough to honour
|
|
37
|
-
* the deadline.
|
|
38
|
-
*
|
|
39
|
-
* ## Recv is not the only way home
|
|
40
|
-
*
|
|
41
|
-
* The completion callback is a *push* to an address baked into the container at
|
|
42
|
-
* creation. A host that dies mid-exec comes back with a different ingress port,
|
|
43
|
-
* so the push can never land — the sandbox retries into a void while the
|
|
44
|
-
* recovered recv waits for a message that will never arrive. That is the
|
|
45
|
-
* recovery wedge (#41), and it is silent: a `running` row that never advances.
|
|
46
|
-
*
|
|
47
|
-
* So whenever the topic falls quiet, this loop stops waiting and *asks*:
|
|
48
|
-
* `GET /exec/{execId}` on the sandbox itself returns the terminal result,
|
|
49
|
-
* signed fresh at request time. The bytes are the ones the callback would have
|
|
50
|
-
* carried, so the provenance frame survives the recovery path intact and one
|
|
51
|
-
* verification path serves both. Push remains the fast path; pull is what makes
|
|
52
|
-
* the protocol durable rather than merely optimistic.
|
|
53
|
-
*
|
|
54
|
-
* The pull is a DBOS step, not a bare `fetch`: a body-level call whose result
|
|
55
|
-
* varied between replays would desynchronise the recorded function-ID sequence.
|
|
56
|
-
*/
|
|
57
|
-
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
58
|
-
import { ATTR_INFLEXA_ATTEMPT, ATTR_INFLEXA_EXEC_ID, stableSpan, untracedFetch } from "../lib/otel-spans.js";
|
|
59
|
-
import { digestBody } from "./digest.js";
|
|
60
|
-
import { signCallback, verifyCallback } from "./hmac.js";
|
|
61
|
-
import { createEscalationPolicy, probeLiveness, syntheticFailureReason, syntheticFailureResult } from "./liveness.js";
|
|
62
|
-
import { ExecResultSchema, isDoneMarker, isSyntheticFailure, PollResponseSchema, } from "./types.js";
|
|
63
|
-
const DEFAULT_FRESHNESS_SECONDS = 300;
|
|
64
|
-
const MAX_RECV_TIMEOUT_SECONDS = 5;
|
|
65
|
-
/**
|
|
66
|
-
* Two-phase poll cadence. Every poll writes a durable step row (plus a durable
|
|
67
|
-
* sleep), so the interval is a direct multiplier on DBOS system-table growth
|
|
68
|
-
* for the life of the exec: 1.5s for the first 40 attempts (~the exec's first
|
|
69
|
-
* minute) keeps short commands snappy; 10s thereafter keeps an hours-long
|
|
70
|
-
* exec's row growth bounded, at a terminal-latency cost that is noise at that
|
|
71
|
-
* runtime. Attempt-derived, so the schedule is a pure function of the
|
|
72
|
-
* checkpointed step sequence and replays identically.
|
|
73
|
-
*/
|
|
74
|
-
const DEFAULT_POLL_INTERVAL_MS = 1_500;
|
|
75
|
-
const DEFAULT_FAST_POLL_ATTEMPTS = 40;
|
|
76
|
-
const DEFAULT_SLOW_POLL_INTERVAL_MS = 10_000;
|
|
77
|
-
/**
|
|
78
|
-
* Silent recv slices to tolerate before pulling. At `MAX_RECV_TIMEOUT_SECONDS`
|
|
79
|
-
* per slice this is ~30s of silence — long enough that a chatty exec never
|
|
80
|
-
* pulls at all, short enough that a recovered workflow converges quickly. Each
|
|
81
|
-
* pull is a durable step, so polling every slice would write a step row every
|
|
82
|
-
* five seconds for the life of a quiet three-hour exec.
|
|
83
|
-
*/
|
|
84
|
-
const PULL_AFTER_SILENT_SLICES = 6;
|
|
85
|
-
const PULL_HTTP_TIMEOUT_MS = 10_000;
|
|
86
|
-
/**
|
|
87
|
-
* Fetch the terminal result for `execId` directly from sandbox-server.
|
|
88
|
-
*
|
|
89
|
-
* Never throws: every failure mode collapses to `unavailable`, because a failed
|
|
90
|
-
* pull is not a failed exec. Returning an error here would fail the enclosing
|
|
91
|
-
* DBOS step and take the workflow with it, when the correct response is simply
|
|
92
|
-
* to keep waiting for the push.
|
|
93
|
-
*
|
|
94
|
-
* A terminal response is distinguished by the presence of the signature header
|
|
95
|
-
* — sandbox-server leaves a still-running exec unsigned, having nothing to
|
|
96
|
-
* attest to yet.
|
|
97
|
-
*/
|
|
98
|
-
async function pullExecResult(fetchImpl, ref, execId) {
|
|
99
|
-
const controller = new AbortController();
|
|
100
|
-
const timer = setTimeout(() => controller.abort(), PULL_HTTP_TIMEOUT_MS);
|
|
101
|
-
try {
|
|
102
|
-
// The result discloses the command's output, so sandbox-server requires a
|
|
103
|
-
// signed request. Sign an empty body over the same construction as the
|
|
104
|
-
// callbacks. `Date.now()` is safe: this runs inside a `DBOS.runStep` whose
|
|
105
|
-
// cached result is replayed without re-executing the body.
|
|
106
|
-
const reqTimestamp = Math.floor(Date.now() / 1000);
|
|
107
|
-
const reqSignature = signCallback({ execId, body: "", timestamp: reqTimestamp, secret: ref.callbackSecret });
|
|
108
|
-
const res = await untracedFetch(fetchImpl, `http://${ref.host}:${ref.port}/exec/${encodeURIComponent(execId)}`, {
|
|
109
|
-
method: "GET",
|
|
110
|
-
headers: {
|
|
111
|
-
"x-sandbox-signature": reqSignature,
|
|
112
|
-
"x-sandbox-timestamp": String(reqTimestamp),
|
|
113
|
-
},
|
|
114
|
-
signal: controller.signal,
|
|
115
|
-
});
|
|
116
|
-
if (!res.ok) {
|
|
117
|
-
await res.text().catch(() => "");
|
|
118
|
-
return { kind: "unavailable", detail: `status ${res.status}` };
|
|
119
|
-
}
|
|
120
|
-
const signature = res.headers.get("x-sandbox-signature");
|
|
121
|
-
const raw = await res.text();
|
|
122
|
-
if (signature === null)
|
|
123
|
-
return { kind: "running" };
|
|
124
|
-
const tsHeader = res.headers.get("x-sandbox-timestamp");
|
|
125
|
-
const parsedTs = tsHeader === null ? null : Number.parseInt(tsHeader, 10);
|
|
126
|
-
return {
|
|
127
|
-
kind: "completed",
|
|
128
|
-
raw,
|
|
129
|
-
signature,
|
|
130
|
-
// An unparseable timestamp forwards as null so `verifyCallback`
|
|
131
|
-
// reports `missing` rather than us guessing a wall-clock.
|
|
132
|
-
timestamp: parsedTs === null || Number.isNaN(parsedTs) ? null : parsedTs,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
catch (cause) {
|
|
136
|
-
return { kind: "unavailable", detail: cause instanceof Error ? cause.message : String(cause) };
|
|
137
|
-
}
|
|
138
|
-
finally {
|
|
139
|
-
clearTimeout(timer);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
export class HardCancelError extends Error {
|
|
143
|
-
execId;
|
|
144
|
-
reason;
|
|
145
|
-
constructor(execId, reason, detail) {
|
|
146
|
-
super(`awaitExec[${execId}]: hard cancel — ${reason}${detail ? ` (${detail})` : ""}`);
|
|
147
|
-
this.execId = execId;
|
|
148
|
-
this.reason = reason;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
export class ExecTimeoutError extends Error {
|
|
152
|
-
execId;
|
|
153
|
-
constructor(execId) {
|
|
154
|
-
super(`awaitExec[${execId}]: deadline exceeded before done-marker`);
|
|
155
|
-
this.execId = execId;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Await a submitted exec's terminal result. Dispatches on transport — `poll`
|
|
160
|
-
* (default) or `callback`. Both verify every result against the per-sandbox
|
|
161
|
-
* HMAC and are bounded by `deadlineMs`.
|
|
162
|
-
*/
|
|
163
|
-
export async function awaitExec(ref, execId, emit, deadlineMs, options = {}) {
|
|
164
|
-
const transport = options.transport ?? "poll";
|
|
165
|
-
return transport === "callback" ? awaitExecCallback(ref, execId, emit, deadlineMs, options) : awaitExecPoll(ref, execId, emit, deadlineMs, options);
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Body of the recv loop (callback transport). Hoisted so it's testable without
|
|
169
|
-
* spinning up DBOS — `recv` is injectable and `now` is overridable.
|
|
170
|
-
*/
|
|
171
|
-
export async function awaitExecCallback(ref, execId, emit, deadlineMs, options = {}) {
|
|
172
|
-
const topic = `exec-event:${execId}`;
|
|
173
|
-
const callbackSecret = ref.callbackSecret;
|
|
174
|
-
const now = options.now ?? (() => DBOS.now());
|
|
175
|
-
const recv = options.recv ?? (async (t, sec) => DBOS.recv(t, sec));
|
|
176
|
-
const fetchImpl = options.fetch ?? fetch;
|
|
177
|
-
const runStep = options.runStep ?? ((fn, c) => DBOS.runStep(fn, c));
|
|
178
|
-
// Both counters are pure functions of the checkpointed recv sequence, so a
|
|
179
|
-
// replay issues exactly the same pulls in exactly the same order — which is
|
|
180
|
-
// what keeps the DBOS function-ID sequence stable.
|
|
181
|
-
let silentSlices = 0;
|
|
182
|
-
let pullAttempt = 0;
|
|
183
|
-
/** Ask the sandbox directly. Returns null when the exec has not finished. */
|
|
184
|
-
const pull = async () => {
|
|
185
|
-
pullAttempt += 1;
|
|
186
|
-
const pullStepName = `sandbox.pull-exec-result.${execId}.${pullAttempt}`;
|
|
187
|
-
const pulled = await runStep(() => {
|
|
188
|
-
stableSpan(pullStepName, "sandbox.pull-exec-result", { [ATTR_INFLEXA_EXEC_ID]: execId, [ATTR_INFLEXA_ATTEMPT]: pullAttempt });
|
|
189
|
-
return pullExecResult(fetchImpl, ref, execId);
|
|
190
|
-
}, { name: pullStepName });
|
|
191
|
-
if (pulled.kind !== "completed")
|
|
192
|
-
return null;
|
|
193
|
-
// Identical treatment to a pushed completion: the bytes were signed by
|
|
194
|
-
// the same secret, so a forgery is as fatal here as it is there.
|
|
195
|
-
const verified = verifyCallback({
|
|
196
|
-
execId,
|
|
197
|
-
body: Buffer.from(pulled.raw, "utf8"),
|
|
198
|
-
signature: pulled.signature,
|
|
199
|
-
timestamp: pulled.timestamp,
|
|
200
|
-
secret: callbackSecret,
|
|
201
|
-
nowSec: Math.floor((await now()) / 1000),
|
|
202
|
-
freshnessSec: DEFAULT_FRESHNESS_SECONDS,
|
|
203
|
-
});
|
|
204
|
-
if (!verified.valid) {
|
|
205
|
-
throw new HardCancelError(execId, verified.reason, "pulled result");
|
|
206
|
-
}
|
|
207
|
-
return ExecResultSchema.parse(JSON.parse(pulled.raw));
|
|
208
|
-
};
|
|
209
|
-
while (true) {
|
|
210
|
-
const remainingMs = deadlineMs - (await now());
|
|
211
|
-
if (remainingMs <= 0) {
|
|
212
|
-
// A lost callback is not a slow command. Ask once before declaring a
|
|
213
|
-
// timeout — the result may have been sitting in the sandbox the whole
|
|
214
|
-
// time, and reporting a timeout would discard a completed analysis.
|
|
215
|
-
const pulled = await pull();
|
|
216
|
-
if (pulled !== null)
|
|
217
|
-
return pulled;
|
|
218
|
-
throw new ExecTimeoutError(execId);
|
|
219
|
-
}
|
|
220
|
-
const timeoutSec = Math.min(MAX_RECV_TIMEOUT_SECONDS, Math.max(1, Math.ceil(remainingMs / 1000)));
|
|
221
|
-
const msg = await recv(topic, timeoutSec);
|
|
222
|
-
if (msg === null) {
|
|
223
|
-
silentSlices += 1;
|
|
224
|
-
if (silentSlices >= PULL_AFTER_SILENT_SLICES) {
|
|
225
|
-
silentSlices = 0;
|
|
226
|
-
const pulled = await pull();
|
|
227
|
-
if (pulled !== null)
|
|
228
|
-
return pulled;
|
|
229
|
-
}
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
silentSlices = 0;
|
|
233
|
-
if (msg.signature === null) {
|
|
234
|
-
if (isSyntheticFailure(msg.payload)) {
|
|
235
|
-
return ExecResultSchema.parse(msg.payload.result);
|
|
236
|
-
}
|
|
237
|
-
throw new HardCancelError(execId, "synthetic-without-marker");
|
|
238
|
-
}
|
|
239
|
-
// Verify against the exact bytes sandbox-server signed. The route
|
|
240
|
-
// reduces them to `payloadDigest` at ingress — the signature covers that
|
|
241
|
-
// digest, so it is all verification needs and the message never carries
|
|
242
|
-
// a second copy of the body. `payloadRaw` is the superseded form, and
|
|
243
|
-
// the JSON.stringify fallback covers messages persisted before either
|
|
244
|
-
// field existed — re-serializing diverges from Go's HTML-escaping
|
|
245
|
-
// encoder, so it is a last resort, not a normal path.
|
|
246
|
-
const bodyDigest = msg.payloadDigest ?? digestBody(Buffer.from(msg.payloadRaw ?? JSON.stringify(msg.payload), "utf8"));
|
|
247
|
-
const result = verifyCallback({
|
|
248
|
-
execId,
|
|
249
|
-
bodyDigest,
|
|
250
|
-
signature: msg.signature,
|
|
251
|
-
timestamp: msg.timestamp,
|
|
252
|
-
secret: callbackSecret,
|
|
253
|
-
nowSec: Math.floor((await now()) / 1000),
|
|
254
|
-
freshnessSec: DEFAULT_FRESHNESS_SECONDS,
|
|
255
|
-
});
|
|
256
|
-
if (!result.valid) {
|
|
257
|
-
throw new HardCancelError(execId, result.reason);
|
|
258
|
-
}
|
|
259
|
-
if (isDoneMarker(msg.payload)) {
|
|
260
|
-
return ExecResultSchema.parse(msg.payload.result);
|
|
261
|
-
}
|
|
262
|
-
await emit(msg.payload);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
async function pollExecOnce(fetchImpl, ref, execId, cursor) {
|
|
266
|
-
const controller = new AbortController();
|
|
267
|
-
const timer = setTimeout(() => controller.abort(), PULL_HTTP_TIMEOUT_MS);
|
|
268
|
-
try {
|
|
269
|
-
// The response discloses the command's output, so the request is signed
|
|
270
|
-
// over an empty body — the same construction as the callbacks. `Date.now()`
|
|
271
|
-
// is safe: this runs inside a `DBOS.runStep` whose cached result is
|
|
272
|
-
// replayed without re-executing the body.
|
|
273
|
-
const reqTimestamp = Math.floor(Date.now() / 1000);
|
|
274
|
-
const reqSignature = signCallback({ execId, body: "", timestamp: reqTimestamp, secret: ref.callbackSecret });
|
|
275
|
-
const res = await untracedFetch(fetchImpl, `http://${ref.host}:${ref.port}/exec/${encodeURIComponent(execId)}?since=${cursor}`, {
|
|
276
|
-
method: "GET",
|
|
277
|
-
headers: {
|
|
278
|
-
"x-sandbox-signature": reqSignature,
|
|
279
|
-
"x-sandbox-timestamp": String(reqTimestamp),
|
|
280
|
-
},
|
|
281
|
-
signal: controller.signal,
|
|
282
|
-
});
|
|
283
|
-
if (!res.ok) {
|
|
284
|
-
await res.text().catch(() => "");
|
|
285
|
-
return { kind: "unavailable", detail: `status ${res.status}` };
|
|
286
|
-
}
|
|
287
|
-
const signature = res.headers.get("x-sandbox-signature");
|
|
288
|
-
const raw = await res.text();
|
|
289
|
-
if (signature === null)
|
|
290
|
-
return { kind: "unavailable", detail: "poll response missing signature" };
|
|
291
|
-
const tsHeader = res.headers.get("x-sandbox-timestamp");
|
|
292
|
-
const parsedTs = tsHeader === null ? null : Number.parseInt(tsHeader, 10);
|
|
293
|
-
return {
|
|
294
|
-
kind: "ok",
|
|
295
|
-
raw,
|
|
296
|
-
signature,
|
|
297
|
-
timestamp: parsedTs === null || Number.isNaN(parsedTs) ? null : parsedTs,
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
catch (cause) {
|
|
301
|
-
return { kind: "unavailable", detail: cause instanceof Error ? cause.message : String(cause) };
|
|
302
|
-
}
|
|
303
|
-
finally {
|
|
304
|
-
clearTimeout(timer);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Body of the poll loop (poll transport). No `DBOS.recv`, no per-exec topic, no
|
|
309
|
-
* callback handler: a sequence of durable pull steps named
|
|
310
|
-
* `sandbox.poll-exec-result.${execId}.${n}`, each fetching the signed
|
|
311
|
-
* `{ status, events, cursor, result? }`, verifying it exactly as a pushed
|
|
312
|
-
* completion is verified, forwarding new events via `emit`, advancing the
|
|
313
|
-
* cursor, and returning `result` once terminal. `sleep`, `now`, `fetch`,
|
|
314
|
-
* `runStep`, and `isAlive` are injectable so the loop is testable without DBOS.
|
|
315
|
-
*
|
|
316
|
-
* Liveness: when the `isAlive` seam is wired (the client always wires it),
|
|
317
|
-
* consecutive `unavailable` polls past the module threshold escalate to a
|
|
318
|
-
* durable probe step `sandbox.probe-liveness.${execId}.${k}`; a dead verdict
|
|
319
|
-
* returns the synthetic-failure result in-loop — the poll-mode analogue of the
|
|
320
|
-
* watchdog's synthetic-complete, minus the topic and the race gate, since the
|
|
321
|
-
* verdict is produced and consumed by the same workflow in sequence.
|
|
322
|
-
*
|
|
323
|
-
* The `cursor`, `pollAttempt`, and `probeAttempt` counters are pure functions
|
|
324
|
-
* of the cached step sequence, so a replay issues the same polls and probes in
|
|
325
|
-
* the same order and the DBOS function-ID sequence stays stable.
|
|
326
|
-
*/
|
|
327
|
-
export async function awaitExecPoll(ref, execId, emit, deadlineMs, options = {}) {
|
|
328
|
-
const callbackSecret = ref.callbackSecret;
|
|
329
|
-
const now = options.now ?? (() => DBOS.now());
|
|
330
|
-
const fetchImpl = options.fetch ?? fetch;
|
|
331
|
-
const runStep = options.runStep ?? ((fn, c) => DBOS.runStep(fn, c));
|
|
332
|
-
const sleep = options.sleep ?? ((ms) => DBOS.sleepms(ms));
|
|
333
|
-
const warn = options.warn ?? ((message) => DBOS.logger.warn(message));
|
|
334
|
-
const isAlive = options.isAlive;
|
|
335
|
-
let cursor = 0;
|
|
336
|
-
let pollAttempt = 0;
|
|
337
|
-
// Sustained-unavailability escalation (`liveness.ts`): consecutive
|
|
338
|
-
// `unavailable` polls arm a backend-inspect probe; poll failures alone
|
|
339
|
-
// never fail the exec (`unavailable` conflates unreachable with unknown
|
|
340
|
-
// execId / non-200). Policy state and the probe counter are pure functions
|
|
341
|
-
// of the checkpointed poll outcomes, so replays walk the identical
|
|
342
|
-
// poll/probe step sequence.
|
|
343
|
-
const escalation = createEscalationPolicy();
|
|
344
|
-
let probeAttempt = 0;
|
|
345
|
-
while (true) {
|
|
346
|
-
pollAttempt += 1;
|
|
347
|
-
const pollStepName = `sandbox.poll-exec-result.${execId}.${pollAttempt}`;
|
|
348
|
-
const polled = await runStep(() => {
|
|
349
|
-
stableSpan(pollStepName, "sandbox.poll-exec-result", { [ATTR_INFLEXA_EXEC_ID]: execId, [ATTR_INFLEXA_ATTEMPT]: pollAttempt });
|
|
350
|
-
return pollExecOnce(fetchImpl, ref, execId, cursor);
|
|
351
|
-
}, { name: pollStepName });
|
|
352
|
-
if (polled.kind === "ok") {
|
|
353
|
-
// Identical treatment to a pushed completion: the bytes were signed by
|
|
354
|
-
// the same secret, so a forgery is as fatal here as it is there.
|
|
355
|
-
const verified = verifyCallback({
|
|
356
|
-
execId,
|
|
357
|
-
body: Buffer.from(polled.raw, "utf8"),
|
|
358
|
-
signature: polled.signature,
|
|
359
|
-
timestamp: polled.timestamp,
|
|
360
|
-
secret: callbackSecret,
|
|
361
|
-
nowSec: Math.floor((await now()) / 1000),
|
|
362
|
-
freshnessSec: DEFAULT_FRESHNESS_SECONDS,
|
|
363
|
-
});
|
|
364
|
-
if (!verified.valid) {
|
|
365
|
-
throw new HardCancelError(execId, verified.reason, "poll response");
|
|
366
|
-
}
|
|
367
|
-
const resp = PollResponseSchema.parse(JSON.parse(polled.raw));
|
|
368
|
-
// Forward only events past the local cursor, before returning, so
|
|
369
|
-
// trailing progress lands ahead of the terminal result. The filter is
|
|
370
|
-
// load-bearing: the signature covers the body but not the request's
|
|
371
|
-
// `since`, so any validly-signed snapshot verifies against any poll —
|
|
372
|
-
// a replayed or crossed response must not re-emit delivered events.
|
|
373
|
-
const newEvents = resp.events.filter((ev) => ev.seq > cursor);
|
|
374
|
-
// Sequence numbers are per-exec contiguous, so a hole between the
|
|
375
|
-
// local cursor and the next delivered (or high-water) seq is exact
|
|
376
|
-
// proof the ring shed events before the host saw them. Advisory: the
|
|
377
|
-
// terminal result, not the event stream, is the authoritative outcome.
|
|
378
|
-
const nextSeq = newEvents.length > 0 ? newEvents[0].seq : resp.cursor > cursor ? resp.cursor + 1 : cursor + 1;
|
|
379
|
-
const lost = nextSeq - cursor - 1;
|
|
380
|
-
if (lost > 0) {
|
|
381
|
-
warn(`awaitExecPoll[${execId}]: ${lost} progress event(s) (seq ${cursor + 1}-${cursor + lost}) shed by the sandbox ring before delivery; the terminal result is unaffected`);
|
|
382
|
-
}
|
|
383
|
-
for (const ev of newEvents) {
|
|
384
|
-
await emit(ev.payload);
|
|
385
|
-
}
|
|
386
|
-
if (resp.cursor > cursor)
|
|
387
|
-
cursor = resp.cursor;
|
|
388
|
-
if (resp.result !== undefined)
|
|
389
|
-
return resp.result;
|
|
390
|
-
}
|
|
391
|
-
if (isAlive && escalation.onPoll(polled.kind === "ok" ? "ok" : "unavailable")) {
|
|
392
|
-
probeAttempt += 1;
|
|
393
|
-
const probeStepName = `sandbox.probe-liveness.${execId}.${probeAttempt}`;
|
|
394
|
-
const verdict = await runStep(() => {
|
|
395
|
-
stableSpan(probeStepName, "sandbox.probe-liveness", { [ATTR_INFLEXA_EXEC_ID]: execId, [ATTR_INFLEXA_ATTEMPT]: probeAttempt });
|
|
396
|
-
return probeLiveness(isAlive, ref);
|
|
397
|
-
}, { name: probeStepName });
|
|
398
|
-
// Only an observably dead machine fails the exec — the result lives
|
|
399
|
-
// in that machine and nowhere else, so it is unrecoverable and the
|
|
400
|
-
// synthetic failure races nothing. `alive` (live-but-slow exec,
|
|
401
|
-
// evicted execId) and `inconclusive` (backend API error) both
|
|
402
|
-
// resume polling, bounded by the deadline.
|
|
403
|
-
if (verdict.kind === "dead") {
|
|
404
|
-
return syntheticFailureResult(execId, syntheticFailureReason({ oomKilled: verdict.oomKilled }));
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
// The deadline gate sits AFTER the poll so the loop always asks once more
|
|
408
|
-
// before declaring a timeout — same reasoning as the callback loop's
|
|
409
|
-
// deadline pull: a lost poll window is not a slow command, and reporting
|
|
410
|
-
// a timeout would discard a completed analysis.
|
|
411
|
-
const remainingMs = deadlineMs - (await now());
|
|
412
|
-
if (remainingMs <= 0)
|
|
413
|
-
throw new ExecTimeoutError(execId);
|
|
414
|
-
const intervalMs = pollAttempt <= DEFAULT_FAST_POLL_ATTEMPTS ? DEFAULT_POLL_INTERVAL_MS : DEFAULT_SLOW_POLL_INTERVAL_MS;
|
|
415
|
-
await sleep(Math.min(intervalMs, remainingMs));
|
|
416
|
-
}
|
|
417
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ingress-side delivery of a sandbox-server callback onto the per-exec DBOS
|
|
3
|
-
* topic (`awaitExec`'s recv loop, `await-exec.ts`).
|
|
4
|
-
*
|
|
5
|
-
* Exists so an embedder hosting the HTTP callback endpoint never touches the
|
|
6
|
-
* DBOS SDK itself: `DBOS` is module-singleton state, and an embedder with its
|
|
7
|
-
* own `node_modules` would import a SECOND, un-launched SDK instance whose
|
|
8
|
-
* `send` writes nowhere. Routing delivery through the harness pins the call to
|
|
9
|
-
* the same instance `launchDbos` initialized — and keeps the topic-name format
|
|
10
|
-
* beside its consumer instead of duplicated in every host.
|
|
11
|
-
*
|
|
12
|
-
* HMAC verification is deliberately NOT done here: `awaitExec` verifies inside
|
|
13
|
-
* the workflow body against the per-sandbox `callbackSecret`, so the ingress
|
|
14
|
-
* stays secret-free and a forged POST is rejected where the secret lives.
|
|
15
|
-
*/
|
|
16
|
-
import type { ExecEventMessage } from "./types.js";
|
|
17
|
-
/** Topic `awaitExec` listens on for one exec's events and done-marker. */
|
|
18
|
-
export declare function execEventTopic(execId: string): string;
|
|
19
|
-
/**
|
|
20
|
-
* Send one callback envelope (an event, or a `DoneMarker`-shaped completion
|
|
21
|
-
* payload) to the workflow awaiting `execId`. The caller derives
|
|
22
|
-
* `workflowId` via `workflowIdFromExec` and owns HTTP status mapping:
|
|
23
|
-
* a rejection here is retryable (5xx), a malformed execId is not (4xx).
|
|
24
|
-
*/
|
|
25
|
-
export declare function deliverExecEvent(workflowId: string, execId: string, message: ExecEventMessage): Promise<void>;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ingress-side delivery of a sandbox-server callback onto the per-exec DBOS
|
|
3
|
-
* topic (`awaitExec`'s recv loop, `await-exec.ts`).
|
|
4
|
-
*
|
|
5
|
-
* Exists so an embedder hosting the HTTP callback endpoint never touches the
|
|
6
|
-
* DBOS SDK itself: `DBOS` is module-singleton state, and an embedder with its
|
|
7
|
-
* own `node_modules` would import a SECOND, un-launched SDK instance whose
|
|
8
|
-
* `send` writes nowhere. Routing delivery through the harness pins the call to
|
|
9
|
-
* the same instance `launchDbos` initialized — and keeps the topic-name format
|
|
10
|
-
* beside its consumer instead of duplicated in every host.
|
|
11
|
-
*
|
|
12
|
-
* HMAC verification is deliberately NOT done here: `awaitExec` verifies inside
|
|
13
|
-
* the workflow body against the per-sandbox `callbackSecret`, so the ingress
|
|
14
|
-
* stays secret-free and a forged POST is rejected where the secret lives.
|
|
15
|
-
*/
|
|
16
|
-
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
17
|
-
/** Topic `awaitExec` listens on for one exec's events and done-marker. */
|
|
18
|
-
export function execEventTopic(execId) {
|
|
19
|
-
return `exec-event:${execId}`;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Send one callback envelope (an event, or a `DoneMarker`-shaped completion
|
|
23
|
-
* payload) to the workflow awaiting `execId`. The caller derives
|
|
24
|
-
* `workflowId` via `workflowIdFromExec` and owns HTTP status mapping:
|
|
25
|
-
* a rejection here is retryable (5xx), a malformed execId is not (4xx).
|
|
26
|
-
*/
|
|
27
|
-
export async function deliverExecEvent(workflowId, execId, message) {
|
|
28
|
-
await DBOS.send(workflowId, message, execEventTopic(execId));
|
|
29
|
-
}
|
package/dist/sandbox/digest.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Body digest for sandbox callbacks.
|
|
3
|
-
*
|
|
4
|
-
* The callback signature is taken over the hex SHA-256 of the POSTed body
|
|
5
|
-
* rather than over the body itself, so the digest is a value the ingress can
|
|
6
|
-
* compute and forward while the bytes themselves are dropped — verification
|
|
7
|
-
* downstream needs nothing more.
|
|
8
|
-
*
|
|
9
|
-
* This lives apart from `hmac.ts` on purpose. Hashing carries no secret and
|
|
10
|
-
* decides nothing, so the route that only forwards can depend on it without
|
|
11
|
-
* taking on the verification machinery it is required to stay clear of.
|
|
12
|
-
*/
|
|
13
|
-
/** Hex SHA-256 of the POSTed bytes — the value the callback signature covers. */
|
|
14
|
-
export declare function digestBody(input: Buffer | string): string;
|
package/dist/sandbox/digest.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Body digest for sandbox callbacks.
|
|
3
|
-
*
|
|
4
|
-
* The callback signature is taken over the hex SHA-256 of the POSTed body
|
|
5
|
-
* rather than over the body itself, so the digest is a value the ingress can
|
|
6
|
-
* compute and forward while the bytes themselves are dropped — verification
|
|
7
|
-
* downstream needs nothing more.
|
|
8
|
-
*
|
|
9
|
-
* This lives apart from `hmac.ts` on purpose. Hashing carries no secret and
|
|
10
|
-
* decides nothing, so the route that only forwards can depend on it without
|
|
11
|
-
* taking on the verification machinery it is required to stay clear of.
|
|
12
|
-
*/
|
|
13
|
-
import { createHash } from "node:crypto";
|
|
14
|
-
/** Hex SHA-256 of the POSTed bytes — the value the callback signature covers. */
|
|
15
|
-
export function digestBody(input) {
|
|
16
|
-
return createHash("sha256").update(input).digest("hex");
|
|
17
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `execId` parsing — execId is `${workflowId}:${stepId}:${fnId}` where the
|
|
3
|
-
* workflowId portion itself contains exactly one colon (child workflow ids
|
|
4
|
-
* are `${analysisId}:${runId}-${stepIdx}`, parent ids are
|
|
5
|
-
* `${analysisId}:${runId}`). `indexOf(":")` / `split(":")[0]` over an
|
|
6
|
-
* execId would stop INSIDE the workflowId and return only the analysisId,
|
|
7
|
-
* which `DBOS.send` would treat as a wrong-workflow target. Strip the last
|
|
8
|
-
* two colon-delimited segments (`:${stepId}:${fnId}`) to recover the
|
|
9
|
-
* workflowId. `stepId` is a plan-defined slug and `fnId` is `fn-${n.toString(36)}`,
|
|
10
|
-
* neither of which can contain colons.
|
|
11
|
-
*/
|
|
12
|
-
export declare function workflowIdFromExec(execId: string): string | null;
|
package/dist/sandbox/exec-id.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `execId` parsing — execId is `${workflowId}:${stepId}:${fnId}` where the
|
|
3
|
-
* workflowId portion itself contains exactly one colon (child workflow ids
|
|
4
|
-
* are `${analysisId}:${runId}-${stepIdx}`, parent ids are
|
|
5
|
-
* `${analysisId}:${runId}`). `indexOf(":")` / `split(":")[0]` over an
|
|
6
|
-
* execId would stop INSIDE the workflowId and return only the analysisId,
|
|
7
|
-
* which `DBOS.send` would treat as a wrong-workflow target. Strip the last
|
|
8
|
-
* two colon-delimited segments (`:${stepId}:${fnId}`) to recover the
|
|
9
|
-
* workflowId. `stepId` is a plan-defined slug and `fnId` is `fn-${n.toString(36)}`,
|
|
10
|
-
* neither of which can contain colons.
|
|
11
|
-
*/
|
|
12
|
-
export function workflowIdFromExec(execId) {
|
|
13
|
-
const lastColon = execId.lastIndexOf(":");
|
|
14
|
-
if (lastColon <= 0)
|
|
15
|
-
return null;
|
|
16
|
-
const secondLastColon = execId.lastIndexOf(":", lastColon - 1);
|
|
17
|
-
if (secondLastColon <= 0)
|
|
18
|
-
return null;
|
|
19
|
-
return execId.slice(0, secondLastColon);
|
|
20
|
-
}
|
package/dist/sandbox/hmac.d.ts
DELETED
|
@@ -1,62 +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
|
-
export interface VerifyCallbackInput {
|
|
18
|
-
execId: string;
|
|
19
|
-
/**
|
|
20
|
-
* The bytes the sandbox-server POSTed. Supply this or `bodyDigest`.
|
|
21
|
-
*
|
|
22
|
-
* The signature covers the hex SHA-256 of the body, never the body itself,
|
|
23
|
-
* so a caller that has already hashed the bytes can verify from the digest
|
|
24
|
-
* alone — which is what keeps a large payload from being carried twice.
|
|
25
|
-
*/
|
|
26
|
-
body?: Buffer | string;
|
|
27
|
-
/** Hex SHA-256 of the POSTed bytes. Takes precedence over `body`. */
|
|
28
|
-
bodyDigest?: string;
|
|
29
|
-
/** `X-Sandbox-Signature` value (lowercase hex). */
|
|
30
|
-
signature: string | null;
|
|
31
|
-
/** `X-Sandbox-Timestamp` value (unix seconds). */
|
|
32
|
-
timestamp: number | null;
|
|
33
|
-
/** Per-sandbox secret from the cached `createSandbox` step output. */
|
|
34
|
-
secret: string;
|
|
35
|
-
/** Current unix-second wall-clock — injectable for tests. */
|
|
36
|
-
nowSec: number;
|
|
37
|
-
/** Symmetric freshness window in seconds. */
|
|
38
|
-
freshnessSec: number;
|
|
39
|
-
}
|
|
40
|
-
export type VerifyResult = {
|
|
41
|
-
valid: true;
|
|
42
|
-
} | {
|
|
43
|
-
valid: false;
|
|
44
|
-
reason: "bad-signature" | "stale-timestamp" | "missing";
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Recompute the HMAC and compare in constant time, then check the
|
|
48
|
-
* timestamp falls within a symmetric `freshnessSec` window of `nowSec`.
|
|
49
|
-
*
|
|
50
|
-
* Order: `missing` (one of signature/timestamp is null) is reported
|
|
51
|
-
* separately from `bad-signature` so the caller can log it precisely.
|
|
52
|
-
* Freshness is checked AFTER signature, so a stale-but-valid signature
|
|
53
|
-
* is distinguishable from a forgery.
|
|
54
|
-
*/
|
|
55
|
-
export declare function verifyCallback(input: VerifyCallbackInput): VerifyResult;
|
|
56
|
-
/** Convenience for the Cortex side to compute a signature for tests. */
|
|
57
|
-
export declare function signCallback({ execId, body, timestamp, secret }: {
|
|
58
|
-
execId: string;
|
|
59
|
-
body: Buffer | string;
|
|
60
|
-
timestamp: number;
|
|
61
|
-
secret: string;
|
|
62
|
-
}): string;
|