@foldrun/core 0.2.0 → 0.4.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 +31 -10
- package/dist/src/approvals.d.ts +104 -1
- package/dist/src/approvals.js +192 -2
- package/dist/src/budget.d.ts +32 -0
- package/dist/src/budget.js +177 -0
- package/dist/src/catalog.js +2 -1
- package/dist/src/confine.js +6 -1
- package/dist/src/deploy.js +4 -2
- package/dist/src/egress.d.ts +19 -2
- package/dist/src/egress.js +2 -1
- package/dist/src/evals.d.ts +4 -0
- package/dist/src/evals.js +20 -11
- package/dist/src/flow-lint.d.ts +4 -0
- package/dist/src/flow-lint.js +108 -0
- package/dist/src/host-env.d.ts +8 -0
- package/dist/src/host-env.js +48 -0
- package/dist/src/json-schema.d.ts +12 -0
- package/dist/src/json-schema.js +202 -0
- package/dist/src/library.js +13 -1
- package/dist/src/notify.d.ts +79 -1
- package/dist/src/notify.js +243 -9
- package/dist/src/paths.d.ts +5 -0
- package/dist/src/paths.js +32 -0
- package/dist/src/platform.d.ts +112 -2
- package/dist/src/platform.js +20 -2
- package/dist/src/provider-health.d.ts +55 -0
- package/dist/src/provider-health.js +232 -0
- package/dist/src/run-container.d.ts +80 -4
- package/dist/src/run-container.js +176 -37
- package/dist/src/runner.d.ts +41 -5
- package/dist/src/runner.js +513 -81
- package/dist/src/script-tools.d.ts +12 -0
- package/dist/src/script-tools.js +10 -2
- package/dist/src/secret-health.d.ts +59 -0
- package/dist/src/secret-health.js +155 -0
- package/dist/src/secrets.d.ts +19 -1
- package/dist/src/secrets.js +44 -12
- package/dist/src/starter.js +4 -0
- package/dist/src/step-exec.d.ts +62 -6
- package/dist/src/step-exec.js +199 -68
- package/dist/src/store.d.ts +300 -3
- package/dist/src/store.js +574 -26
- package/dist/src/tar.js +2 -1
- package/dist/src/test-mode.d.ts +111 -0
- package/dist/src/test-mode.js +285 -0
- package/dist/src/tool-test.js +48 -21
- package/dist/src/translator.js +2 -1
- package/dist/src/trigger-log.d.ts +43 -0
- package/dist/src/trigger-log.js +122 -0
- package/dist/src/triggers.d.ts +20 -0
- package/dist/src/triggers.js +91 -10
- package/dist/src/webhook.d.ts +15 -3
- package/dist/src/webhook.js +11 -11
- package/package.json +6 -5
- package/src/approvals.ts +253 -2
- package/src/budget.ts +178 -0
- package/src/catalog.ts +2 -1
- package/src/confine.ts +6 -1
- package/src/deploy.ts +4 -1
- package/src/egress.ts +20 -4
- package/src/evals.ts +25 -9
- package/src/flow-lint.ts +118 -0
- package/src/host-env.ts +51 -0
- package/src/json-schema.ts +184 -0
- package/src/library.ts +13 -1
- package/src/notify.ts +272 -11
- package/src/paths.ts +29 -0
- package/src/platform.ts +119 -3
- package/src/provider-health.ts +274 -0
- package/src/run-container.ts +215 -35
- package/src/runner.ts +525 -73
- package/src/script-tools.ts +18 -2
- package/src/secret-health.ts +185 -0
- package/src/secrets.ts +44 -10
- package/src/starter.ts +4 -0
- package/src/step-exec.ts +176 -21
- package/src/storage.ts +8 -2
- package/src/store.ts +785 -25
- package/src/tar.ts +2 -1
- package/src/test-mode.ts +311 -0
- package/src/tool-test.ts +49 -20
- package/src/translator.ts +2 -1
- package/src/trigger-log.ts +159 -0
- package/src/triggers.ts +92 -10
- package/src/webhook.ts +17 -11
package/src/step-exec.ts
CHANGED
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
import fs from "node:fs";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
14
|
-
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
14
|
+
import type { McpServerConfig, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
15
15
|
import type { Effort } from "./store.ts";
|
|
16
|
+
import type { TestEffect } from "./test-mode.ts";
|
|
16
17
|
import { spawn } from "node:child_process";
|
|
17
18
|
import { checkPaths, checkBash, isFilesystemTool } from "./confine.ts";
|
|
19
|
+
import { hostSafeEnv } from "./host-env.ts";
|
|
20
|
+
import { validateSchema, describeSchemaErrors, looksLikeSchema } from "./json-schema.ts";
|
|
18
21
|
|
|
19
22
|
export interface ExecOutcome {
|
|
20
23
|
status: "completed" | "failed";
|
|
@@ -57,6 +60,9 @@ export interface ExecOptions {
|
|
|
57
60
|
* The runner derives it from the flow's `budget:` and what the run has
|
|
58
61
|
* already spent; absent means no ceiling on this step. */
|
|
59
62
|
budgetUsd?: number | null;
|
|
63
|
+
/** Which line set the ceiling, for the error that names it — "budget: in
|
|
64
|
+
* the flow file" or "budget: on the <agent> agent". */
|
|
65
|
+
budgetNote?: string;
|
|
60
66
|
/** Price per token for the model, when the catalogue knows it, so spend
|
|
61
67
|
* can be counted turn by turn instead of learned at the end. Without it
|
|
62
68
|
* the step prices its turns from the SDK's own running total. */
|
|
@@ -69,30 +75,93 @@ export interface ExecOptions {
|
|
|
69
75
|
/** `output: json` — the reply must carry one JSON value; extracting it is
|
|
70
76
|
* part of finishing the step, and failing to is failing the step. */
|
|
71
77
|
output?: "json";
|
|
78
|
+
/** `schema:` — what that value must look like; a value that does not fit
|
|
79
|
+
* fails the step naming the field. See json-schema.ts. */
|
|
80
|
+
schema?: Record<string, unknown> | boolean;
|
|
81
|
+
/** `max_turns:` — the most model turns before the step is stopped. The
|
|
82
|
+
* SDK enforces it; the step reads the result's reason and says so. */
|
|
83
|
+
maxTurns?: number;
|
|
72
84
|
/** false when the caller is already an isolation boundary (a run
|
|
73
85
|
* container): the SDK's bash sandbox is then redundant and would block
|
|
74
86
|
* declared network use. Default (undefined/true) keeps it on. */
|
|
75
87
|
sandboxBash?: boolean;
|
|
88
|
+
/** Has a person asked for the run to stop? Polled every couple of
|
|
89
|
+
* seconds while the model loop runs; true interrupts it. The in-process
|
|
90
|
+
* runner answers from the run record (stopRun writes there from another
|
|
91
|
+
* process); a container has a sandbox that is destroyed instead. */
|
|
92
|
+
stopRequested?: () => boolean;
|
|
76
93
|
emit: (type: "text" | "tool" | "info" | "error", text: string, extra?: EventExtra) => void;
|
|
77
94
|
}
|
|
78
95
|
|
|
96
|
+
/** How often the stop flag is read while a step runs. */
|
|
97
|
+
export const STOP_POLL_MS = 2000;
|
|
98
|
+
/** After an interrupt is asked for, how long the model loop gets to wind
|
|
99
|
+
* down on its own before the query is aborted outright. */
|
|
100
|
+
export const INTERRUPT_GRACE_MS = 10_000;
|
|
101
|
+
|
|
102
|
+
/** The model loop, as executeStep needs it — the SDK's `query`, or a test's
|
|
103
|
+
* stand-in. Only the shape the loop reads. */
|
|
104
|
+
export type QueryLike = AsyncIterable<unknown> & { interrupt(): Promise<unknown> };
|
|
105
|
+
export type QueryFn = (args: { prompt: string; options: Record<string, unknown> }) => QueryLike;
|
|
106
|
+
|
|
79
107
|
/** The pairing fields on a tool event — see RunEvent in store.ts. */
|
|
80
|
-
export type EventExtra = { call?: string; ms?: number; err?: boolean };
|
|
108
|
+
export type EventExtra = { call?: string; ms?: number; err?: boolean; effect?: TestEffect };
|
|
81
109
|
|
|
82
|
-
|
|
83
|
-
|
|
110
|
+
|
|
111
|
+
/** Conservative per-token rates for a model nothing else can price — an
|
|
112
|
+
* unknown id on a gateway with no catalogue. Opus-class, so a ceiling
|
|
113
|
+
* still means something. Only ever reached when `knownPrice` below does
|
|
114
|
+
* not recognise the model either. */
|
|
84
115
|
export const FALLBACK_PRICE = { input: 15e-6, output: 75e-6 };
|
|
85
116
|
|
|
117
|
+
/** The rates we know without asking anyone: the tiers this runtime resolves
|
|
118
|
+
* (`fast`/`default`/`max` → haiku/sonnet/opus) and the model ids that carry
|
|
119
|
+
* those words. A gateway with a catalogue still wins — that is the
|
|
120
|
+
* authority — but the platform's own Anthropic key has no catalogue, and
|
|
121
|
+
* pricing its steps at Opus rates made every mid-turn ceiling read up to
|
|
122
|
+
* fifteen times high: an indexing run was stopped at "$1.17" having
|
|
123
|
+
* actually spent $0.15 (2026-09-08). Per token, input/output. */
|
|
124
|
+
const KNOWN_PRICES: [RegExp, { input: number; output: number }][] = [
|
|
125
|
+
[/haiku/i, { input: 1e-6, output: 5e-6 }],
|
|
126
|
+
[/sonnet/i, { input: 2e-6, output: 10e-6 }],
|
|
127
|
+
[/\bopus\b/i, { input: 5e-6, output: 25e-6 }],
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
/** What a token costs on this model, when we can say without a catalogue. */
|
|
131
|
+
export function knownPrice(model: string | undefined): { input: number; output: number } | null {
|
|
132
|
+
if (!model) return null;
|
|
133
|
+
return KNOWN_PRICES.find(([re]) => re.test(model))?.[1] ?? null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Anthropic's cache multipliers, and the shape every gateway that bills
|
|
137
|
+
* for caching at all has copied: a write costs a quarter more than fresh
|
|
138
|
+
* input, a read a tenth of it. Counting both at the full input rate — as
|
|
139
|
+
* this did — makes a long cached prompt read about ten times its price,
|
|
140
|
+
* which on a ceiling is not "the honest direction to be wrong in", it is
|
|
141
|
+
* a step killed for spending money it never spent. */
|
|
142
|
+
const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
143
|
+
const CACHE_READ_MULTIPLIER = 0.1;
|
|
144
|
+
|
|
86
145
|
/**
|
|
87
|
-
* What one assistant turn cost
|
|
88
|
-
*
|
|
89
|
-
*
|
|
146
|
+
* What one assistant turn cost, from the usage the SDK reports. Pure.
|
|
147
|
+
*
|
|
148
|
+
* A gateway whose cache is priced differently is over- or under-counted
|
|
149
|
+
* here by that difference alone; the run record's own cost is repriced
|
|
150
|
+
* from the catalogue afterwards and stays the authority on the bill. This
|
|
151
|
+
* number exists to stop a runaway step mid-turn, and wants to be close.
|
|
90
152
|
*/
|
|
91
153
|
export function priceTurn(u: Record<string, number | undefined>, price: { input: number; output: number } | null): number {
|
|
92
|
-
const inTok = (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0);
|
|
93
|
-
const outTok = u.output_tokens ?? 0;
|
|
94
154
|
const p = price ?? FALLBACK_PRICE;
|
|
95
|
-
|
|
155
|
+
const fresh = u.input_tokens ?? 0;
|
|
156
|
+
const written = u.cache_creation_input_tokens ?? 0;
|
|
157
|
+
const read = u.cache_read_input_tokens ?? 0;
|
|
158
|
+
const outTok = u.output_tokens ?? 0;
|
|
159
|
+
return (
|
|
160
|
+
fresh * p.input +
|
|
161
|
+
written * p.input * CACHE_WRITE_MULTIPLIER +
|
|
162
|
+
read * p.input * CACHE_READ_MULTIPLIER +
|
|
163
|
+
outTok * p.output
|
|
164
|
+
);
|
|
96
165
|
}
|
|
97
166
|
|
|
98
167
|
/**
|
|
@@ -105,7 +174,34 @@ export function stepCeiling(budgetUsd: number | null | undefined, spentUsd: numb
|
|
|
105
174
|
return Math.max(0, budgetUsd - spentUsd) / Math.max(1, launching);
|
|
106
175
|
}
|
|
107
176
|
|
|
108
|
-
|
|
177
|
+
/**
|
|
178
|
+
* The ceiling a step actually runs under, and which line set it.
|
|
179
|
+
*
|
|
180
|
+
* Two caps can apply to one step: the flow's `budget:` gives it an equal
|
|
181
|
+
* share of what the run has left; the agent's own `budget:` is the most that
|
|
182
|
+
* agent may spend in one run, less what its earlier steps in this run cost.
|
|
183
|
+
* The tighter one wins, and the note says which, because "over budget" with
|
|
184
|
+
* no line to go and raise is a message that sends someone to the wrong file.
|
|
185
|
+
*/
|
|
186
|
+
export function stepCeilingFor(
|
|
187
|
+
flowShareUsd: number | null,
|
|
188
|
+
agentBudgetUsd: number | null | undefined,
|
|
189
|
+
agentSpentUsd: number,
|
|
190
|
+
agent: string,
|
|
191
|
+
): { ceilingUsd: number | null; note: string } {
|
|
192
|
+
const flow = { ceilingUsd: flowShareUsd, note: "budget: in the flow file" };
|
|
193
|
+
if (!agentBudgetUsd || agentBudgetUsd <= 0) return flow;
|
|
194
|
+
const left = Math.max(0, agentBudgetUsd - agentSpentUsd);
|
|
195
|
+
if (flowShareUsd !== null && flowShareUsd <= left) return flow;
|
|
196
|
+
return { ceilingUsd: left, note: `budget: on the ${agent} agent` };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function executeStep(
|
|
200
|
+
opts: ExecOptions,
|
|
201
|
+
/** The model loop. Injected so the clock and the stop can be tested
|
|
202
|
+
* without a model; production passes nothing and gets the SDK. */
|
|
203
|
+
runQuery: QueryFn = query as unknown as QueryFn,
|
|
204
|
+
): Promise<ExecOutcome> {
|
|
109
205
|
const { agentDir, workspaceRoot, libraryRoot, emit } = opts;
|
|
110
206
|
let status: "running" | "completed" | "failed" = "running";
|
|
111
207
|
let costUsd: number | null = null;
|
|
@@ -115,16 +211,23 @@ export async function executeStep(opts: ExecOptions): Promise<ExecOutcome> {
|
|
|
115
211
|
fs.mkdirSync(path.join(agentDir, "outputs"), { recursive: true });
|
|
116
212
|
fs.mkdirSync(path.join(agentDir, "memory"), { recursive: true });
|
|
117
213
|
|
|
118
|
-
|
|
214
|
+
// The one handle that can end the loop from outside: the timeout and a
|
|
215
|
+
// stop both ask the query to interrupt, and abort it outright if it has
|
|
216
|
+
// not wound down within the grace period.
|
|
217
|
+
const abort = new AbortController();
|
|
218
|
+
|
|
219
|
+
const q = runQuery({
|
|
119
220
|
prompt: opts.prompt,
|
|
120
221
|
options: {
|
|
121
222
|
cwd: agentDir,
|
|
223
|
+
abortController: abort,
|
|
122
224
|
model: opts.model,
|
|
123
225
|
// Omitted, not passed as undefined-with-a-default: an unset effort
|
|
124
226
|
// should mean "whatever this model does normally", which is not a
|
|
125
227
|
// level we can name — it moves as models ship.
|
|
126
228
|
...(opts.effort ? { effort: opts.effort } : {}),
|
|
127
229
|
systemPrompt: opts.systemPrompt,
|
|
230
|
+
...(opts.maxTurns ? { maxTurns: opts.maxTurns } : {}),
|
|
128
231
|
// Restrict the toolset itself, not just approval: an agent that
|
|
129
232
|
// declares no tools gets none, instead of seeing the full Claude
|
|
130
233
|
// Code toolset and burning turns on denied calls.
|
|
@@ -178,7 +281,34 @@ export async function executeStep(opts: ExecOptions): Promise<ExecOutcome> {
|
|
|
178
281
|
},
|
|
179
282
|
});
|
|
180
283
|
|
|
181
|
-
|
|
284
|
+
// Why the loop was ended from outside, if it was. The deadline used to be
|
|
285
|
+
// checked only when a message arrived, so a step waiting on one long tool
|
|
286
|
+
// call — a crawl, a build — sailed past its timeout: with no message there
|
|
287
|
+
// was nothing to check it against. The backstop timer fires regardless,
|
|
288
|
+
// and a stop is read on a clock rather than between groups.
|
|
289
|
+
let ended: "timeout" | "stopped" | null = null;
|
|
290
|
+
const timers: NodeJS.Timeout[] = [];
|
|
291
|
+
const endWith = (why: "timeout" | "stopped") => {
|
|
292
|
+
if (ended) return;
|
|
293
|
+
ended = why;
|
|
294
|
+
void q.interrupt().catch(() => {});
|
|
295
|
+
timers.push(setTimeout(() => abort.abort(), INTERRUPT_GRACE_MS));
|
|
296
|
+
};
|
|
297
|
+
if (opts.timeoutSec) timers.push(setTimeout(() => endWith("timeout"), opts.timeoutSec * 1000));
|
|
298
|
+
if (opts.stopRequested) {
|
|
299
|
+
timers.push(setInterval(() => {
|
|
300
|
+
try {
|
|
301
|
+
if (opts.stopRequested!()) endWith("stopped");
|
|
302
|
+
} catch {
|
|
303
|
+
// an unreadable record is not a stop
|
|
304
|
+
}
|
|
305
|
+
}, STOP_POLL_MS));
|
|
306
|
+
}
|
|
307
|
+
// The clocks stay referenced on purpose: a step hanging in a tool call may
|
|
308
|
+
// be the only thing keeping the loop alive, and an unreferenced timer
|
|
309
|
+
// would let the process exit around it instead of ending it. They are all
|
|
310
|
+
// cleared when the step ends, so nothing outlives the step.
|
|
311
|
+
|
|
182
312
|
// The hard cap. Spend is counted as each assistant turn arrives — its
|
|
183
313
|
// usage is in the message — so the step can stop at the ceiling rather
|
|
184
314
|
// than learn afterwards that it crossed it. Cache traffic is counted as
|
|
@@ -191,18 +321,15 @@ export async function executeStep(opts: ExecOptions): Promise<ExecOutcome> {
|
|
|
191
321
|
// its call and the trace can say how long each tool ran.
|
|
192
322
|
const openCalls = new Map<string, { name: string; at: number }>();
|
|
193
323
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
status = "failed";
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
324
|
+
try {
|
|
325
|
+
for await (const message of q as AsyncIterable<SDKMessage>) {
|
|
326
|
+
if (ended) break;
|
|
200
327
|
if (message.type === "assistant") {
|
|
201
328
|
const u = (message.message as unknown as { usage?: Record<string, number | undefined> }).usage;
|
|
202
329
|
if (ceiling && u) {
|
|
203
330
|
spentUsd += priceTurn(u, opts.price ?? null);
|
|
204
331
|
if (spentUsd >= ceiling) {
|
|
205
|
-
emit("error", `over budget — this step reached $${spentUsd.toFixed(4)} of its $${ceiling.toFixed(4)} ceiling mid-turn and was stopped (budget: in the flow file)`);
|
|
332
|
+
emit("error", `over budget — this step reached $${spentUsd.toFixed(4)} of its $${ceiling.toFixed(4)} ceiling mid-turn and was stopped (${opts.budgetNote ?? "budget: in the flow file"})`);
|
|
206
333
|
status = "failed";
|
|
207
334
|
break;
|
|
208
335
|
}
|
|
@@ -235,6 +362,9 @@ export async function executeStep(opts: ExecOptions): Promise<ExecOutcome> {
|
|
|
235
362
|
}
|
|
236
363
|
} else if (message.type === "result") {
|
|
237
364
|
status = message.subtype === "success" ? "completed" : "failed";
|
|
365
|
+
if (message.subtype === "error_max_turns") {
|
|
366
|
+
emit("error", `stopped after ${opts.maxTurns ?? "its"} turns (max_turns: in the flow file) — the step did not finish`);
|
|
367
|
+
}
|
|
238
368
|
costUsd = "total_cost_usd" in message ? (message.total_cost_usd ?? null) : null;
|
|
239
369
|
if ("usage" in message && message.usage) {
|
|
240
370
|
const u = message.usage as unknown as Record<string, number | undefined>;
|
|
@@ -251,6 +381,19 @@ export async function executeStep(opts: ExecOptions): Promise<ExecOutcome> {
|
|
|
251
381
|
}
|
|
252
382
|
}
|
|
253
383
|
}
|
|
384
|
+
} catch (err) {
|
|
385
|
+
// An abort we asked for is not an error of the step's; anything else is.
|
|
386
|
+
if (!ended) throw err;
|
|
387
|
+
} finally {
|
|
388
|
+
for (const t of timers) clearTimeout(t);
|
|
389
|
+
}
|
|
390
|
+
if (ended === "timeout") {
|
|
391
|
+
emit("error", `timed out after ${opts.timeoutSec}s (timeout: in the flow file) — the step was stopped with the files it wrote so far kept`);
|
|
392
|
+
status = "failed";
|
|
393
|
+
} else if (ended === "stopped") {
|
|
394
|
+
emit("error", "stopped by a person mid-step");
|
|
395
|
+
status = "failed";
|
|
396
|
+
}
|
|
254
397
|
// The SDK ends a healthy run with a `result` message. A stream that just
|
|
255
398
|
// stops — subprocess OOM-killed, crashed, or torn down — used to fall
|
|
256
399
|
// through as "completed", which reported a run that produced nothing as a
|
|
@@ -279,6 +422,14 @@ export async function executeStep(opts: ExecOptions): Promise<ExecOutcome> {
|
|
|
279
422
|
if (extracted.ok) {
|
|
280
423
|
data = extracted.value;
|
|
281
424
|
emit("info", `output: json — ${describeJson(data)}`);
|
|
425
|
+
// The declared shape, checked here beside the extraction: a value
|
|
426
|
+
// that parses but is not what the next step was promised is the same
|
|
427
|
+
// failure as no value, and the message names the field.
|
|
428
|
+
const errors = opts.schema !== undefined && looksLikeSchema(opts.schema) ? validateSchema(data, opts.schema) : [];
|
|
429
|
+
if (errors.length) {
|
|
430
|
+
emit("error", `schema: the value does not fit — ${describeSchemaErrors(errors)}`);
|
|
431
|
+
status = "failed";
|
|
432
|
+
}
|
|
282
433
|
} else {
|
|
283
434
|
emit("error", `output: json — ${extracted.reason}`);
|
|
284
435
|
status = "failed";
|
|
@@ -475,9 +626,13 @@ function runVerify(
|
|
|
475
626
|
return new Promise((resolve) => {
|
|
476
627
|
// No clock of the platform's: a verify like `npm run build` takes what
|
|
477
628
|
// it takes, and the step's own `timeout:` is the bound if the flow set one.
|
|
629
|
+
// The allowlisted host base plus what the caller passed: the step's
|
|
630
|
+
// secrets and identifiers on the in-process path (runner.ts), the
|
|
631
|
+
// container's own environment on the isolated one (run-container.ts).
|
|
632
|
+
// Never process.env whole — see host-env.ts.
|
|
478
633
|
const child = spawn("bash", ["-lc", command], {
|
|
479
634
|
cwd: agentDir,
|
|
480
|
-
env: { ...
|
|
635
|
+
env: { ...hostSafeEnv(), ...env },
|
|
481
636
|
stdio: ["pipe", "pipe", "pipe"],
|
|
482
637
|
});
|
|
483
638
|
// An `output: json` step's data arrives on stdin, so a check can be
|
package/src/storage.ts
CHANGED
|
@@ -302,8 +302,14 @@ export function s3Config(): S3Config | null {
|
|
|
302
302
|
};
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
// Narrower than crypto.BinaryLike on purpose. BinaryLike also covers views
|
|
306
|
+
// over a SharedArrayBuffer, which .update() has never accepted and which
|
|
307
|
+
// @types/node 26 finally says so about — the old typings let this compile by
|
|
308
|
+
// being vaguer than the runtime. Every caller here passes a string or a
|
|
309
|
+
// Buffer, so this is the honest signature, not a workaround.
|
|
310
|
+
type Hashable = string | Uint8Array;
|
|
311
|
+
const sha256 = (b: Hashable) => crypto.createHash("sha256").update(b).digest("hex");
|
|
312
|
+
const hmac = (key: Hashable, data: string) =>
|
|
307
313
|
crypto.createHmac("sha256", key).update(data, "utf8").digest();
|
|
308
314
|
|
|
309
315
|
/** RFC 3986. encodeURIComponent leaves !'()* alone and SigV4 does not. */
|