@foldrun/core 2.5.0 → 2.6.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/dist/src/agent-import.js +2 -2
- package/dist/src/agent-tools.js +2 -2
- package/dist/src/agents-md.d.ts +5 -1
- package/dist/src/agents-md.js +42 -18
- package/dist/src/completions.js +25 -3
- package/dist/src/deploy.js +72 -7
- package/dist/src/evals.d.ts +22 -1
- package/dist/src/evals.js +43 -14
- package/dist/src/flow-lint.d.ts +14 -0
- package/dist/src/flow-lint.js +35 -0
- package/dist/src/flow-patterns.js +29 -14
- package/dist/src/frontmatter-fields.d.ts +27 -0
- package/dist/src/frontmatter-fields.js +51 -3
- package/dist/src/frontmatter-guard.d.ts +19 -0
- package/dist/src/frontmatter-guard.js +67 -0
- package/dist/src/frontmatter-schema.d.ts +2 -2
- package/dist/src/frontmatter-schema.js +30 -2
- package/dist/src/frontmatter-text.d.ts +31 -0
- package/dist/src/frontmatter-text.js +102 -0
- package/dist/src/judge-verdict.d.ts +19 -0
- package/dist/src/judge-verdict.js +54 -0
- package/dist/src/matter.d.ts +34 -0
- package/dist/src/matter.js +104 -0
- package/dist/src/network.d.ts +0 -1
- package/dist/src/network.js +10 -7
- package/dist/src/package.js +9 -2
- package/dist/src/provider-health.js +2 -2
- package/dist/src/run-container.d.ts +9 -0
- package/dist/src/run-container.js +4 -1
- package/dist/src/runner.js +119 -36
- package/dist/src/scan.d.ts +11 -0
- package/dist/src/scan.js +86 -8
- package/dist/src/starter.js +23 -0
- package/dist/src/step-exec.d.ts +10 -2
- package/dist/src/step-exec.js +12 -9
- package/dist/src/store.d.ts +17 -3
- package/dist/src/store.js +14 -10
- package/package.json +1 -1
- package/src/agent-import.ts +2 -2
- package/src/agent-tools.ts +2 -2
- package/src/agents-md.ts +39 -14
- package/src/completions.ts +26 -3
- package/src/deploy.ts +59 -7
- package/src/evals.ts +45 -13
- package/src/flow-lint.ts +36 -0
- package/src/flow-patterns.ts +26 -13
- package/src/frontmatter-fields.ts +74 -3
- package/src/frontmatter-guard.ts +70 -0
- package/src/frontmatter-schema.ts +30 -4
- package/src/frontmatter-text.ts +109 -0
- package/src/judge-verdict.ts +52 -0
- package/src/matter.ts +112 -0
- package/src/network.ts +10 -6
- package/src/package.ts +8 -2
- package/src/provider-health.ts +2 -2
- package/src/run-container.ts +11 -1
- package/src/runner.ts +118 -37
- package/src/scan.ts +86 -8
- package/src/starter.ts +23 -0
- package/src/step-exec.ts +21 -8
- package/src/store.ts +23 -10
package/dist/src/network.js
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
// proxy and DNS and nothing else, so a program that ignores the proxy
|
|
27
27
|
// variables fails instead of going round. This file is the vocabulary; the
|
|
28
28
|
// proxy (foldrun-platform egress-proxy.ts) holds the rule per request.
|
|
29
|
+
/** How wide a step's network is. */
|
|
30
|
+
import { keySpan, dropEmptyBlock } from "./frontmatter-text.js";
|
|
29
31
|
export const NETWORK_LEVELS = ["open", "trusted", "none"];
|
|
30
32
|
/**
|
|
31
33
|
* The trusted list: the domains Anthropic's Claude Code cloud environments
|
|
@@ -370,15 +372,16 @@ export function setNetworkSetting(raw, setting) {
|
|
|
370
372
|
const close = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
371
373
|
if (close < 0)
|
|
372
374
|
return raw;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
+
// Where network: is written — at the top or inside `guardrail:` — is
|
|
376
|
+
// where it is set (frontmatter-text.ts); written nowhere, it goes last.
|
|
377
|
+
const where = keySpan(lines.slice(1, close), "network");
|
|
378
|
+
if (!where) {
|
|
375
379
|
if (line)
|
|
376
380
|
lines.splice(close, 0, line);
|
|
377
381
|
return lines.join(eol);
|
|
378
382
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
lines
|
|
383
|
-
return lines.join(eol);
|
|
383
|
+
const front = lines.slice(1, close);
|
|
384
|
+
front.splice(where.at, where.end - where.at, ...(line ? [`${where.indent}${line}`] : []));
|
|
385
|
+
const edited = !line && where.block !== null ? dropEmptyBlock(front, where.block) : front;
|
|
386
|
+
return [lines[0], ...edited, ...lines.slice(close)].join(eol);
|
|
384
387
|
}
|
package/dist/src/package.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// claim, the files are what will run.
|
|
26
26
|
import fs from "node:fs";
|
|
27
27
|
import path from "node:path";
|
|
28
|
-
import matter from "./matter.js";
|
|
28
|
+
import matter, { configMatter } from "./matter.js";
|
|
29
29
|
import { assertSafeName, isEditablePath, listWorkspaceFiles, listWorkspaces, listFlows, parseFlow, readWorkspaceFile, saveWorkspace, blankTemplateFiles, syncBundleFor, notifyWorkspaceChanged, workspaceDir, } from "./store.js";
|
|
30
30
|
import { recordRevision } from "./history.js";
|
|
31
31
|
import { libraryDir, libraryTools } from "./library.js";
|
|
@@ -522,7 +522,14 @@ function safeFront(content) {
|
|
|
522
522
|
}
|
|
523
523
|
}
|
|
524
524
|
function frontOf(content) {
|
|
525
|
-
|
|
525
|
+
// An agent's secrets: and tools: may sit in its guardrail: block.
|
|
526
|
+
let d;
|
|
527
|
+
try {
|
|
528
|
+
d = configMatter(content).data;
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
d = {};
|
|
532
|
+
}
|
|
526
533
|
const apis = Array.isArray(d.apis) ? d.apis.map((a) => (a && typeof a === "object" ? String(a.name ?? "") : "")) : [];
|
|
527
534
|
const mcp = d.mcpServers && typeof d.mcpServers === "object" ? Object.keys(d.mcpServers) : [];
|
|
528
535
|
const scripts = list(d.scripts);
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { accountModelBlock } from "./model-credential.js";
|
|
21
21
|
import fs from "node:fs";
|
|
22
22
|
import path from "node:path";
|
|
23
|
-
import
|
|
23
|
+
import { configMatter } from "./matter.js";
|
|
24
24
|
import { getSecret } from "./secrets.js";
|
|
25
25
|
import { noteSecretUse, healthKey } from "./secret-health.js";
|
|
26
26
|
import { accountDir, workspaceDir, listWorkspaces, listAgents, parseProvider } from "./store.js";
|
|
@@ -168,7 +168,7 @@ function cheapestModel(spec) {
|
|
|
168
168
|
}
|
|
169
169
|
function frontmatterOf(file) {
|
|
170
170
|
try {
|
|
171
|
-
return (
|
|
171
|
+
return (configMatter(fs.readFileSync(file, "utf8")).data ?? {});
|
|
172
172
|
}
|
|
173
173
|
catch {
|
|
174
174
|
return {};
|
|
@@ -45,6 +45,9 @@ export interface ContainerStepInput {
|
|
|
45
45
|
budgetNote?: string;
|
|
46
46
|
price?: TokenPrice | null;
|
|
47
47
|
verify?: string;
|
|
48
|
+
/** The model `verify: judge:` grades with (the workspace's `judge:`). */
|
|
49
|
+
judgeModel?: string;
|
|
50
|
+
judgeEffort?: string;
|
|
48
51
|
/** `output: json` — checked inside, where the reply is. */
|
|
49
52
|
output?: "json";
|
|
50
53
|
/** `schema:` and `max_turns:`, resolved host-side — see ExecOptions. */
|
|
@@ -285,6 +288,8 @@ export declare function runtimeCacheMount(tenant: string | undefined): {
|
|
|
285
288
|
source: string;
|
|
286
289
|
target: string;
|
|
287
290
|
} | null;
|
|
291
|
+
/** The runner container's entrypoint, as JS text: written into the image as driver.mjs, so it must be plain JS — a TypeScript cast here broke every step on v2026.10.21.5 (tests/run-container.test.ts checks it). */
|
|
292
|
+
export declare const DRIVER = "// The runner container's entrypoint. Reads one step, executes it with the\n// same core the server uses, speaks JSONL on stdout, exits.\nimport fs from \"node:fs\";\nconst input = JSON.parse(fs.readFileSync(\"/opt/runner/job/input.json\", \"utf8\"));\n// Start-up marks (run-container.ts#STARTUP_PHASES): which phase just ended,\n// on the sandbox's clock. A worker that does not know a mark skips the line.\nconst mark = (name) => process.stdout.write(JSON.stringify({ e: \"mark\", name, at: Date.now() }) + \"\\n\");\nmark(\"driver\");\nlet modelSpoke = false;\nconst emit = (type, text, extra) => {\n // The model's first text or tool call is where start-up ends and the\n // step's own work begins \u2014 runtime and tool lines before it are set-up.\n if (!modelSpoke && (type === \"text\" || type === \"tool\")) { modelSpoke = true; mark(\"model\"); }\n process.stdout.write(JSON.stringify({ e: \"event\", type, text, ...extra }) + \"\\n\");\n};\n// What this sandbox actually touched, read at the last moment from the\n// kernel's own books: cgroup v2 for CPU busy-time and peak memory,\n// /proc/net/dev for bytes on the wire. Every read is best-effort \u2014 gVisor\n// exposes some of these files and not others depending on version, and a\n// metric we cannot read is null, never guessed. Reservation is what is\n// billed; these exist so the reservation can be right-sized.\nfunction readActuals() {\n // fs from the module's own import \u2014 the driver is ESM, and a require()\n // here threw on every call, silently nulling every metric it guarded.\n const read = (p) => { try { return fs.readFileSync(p, \"utf8\"); } catch { return null; } };\n // Two cgroup dialects: v2 (cpu.stat, microseconds; memory.peak) on a\n // plain runtime, v1-style controllers under gVisor (cpuacct.usage,\n // NANOseconds; memory.max_usage_in_bytes) \u2014 probed on the real cluster,\n // not assumed. Whichever answers wins; both silent means null.\n const cpuStat = read(\"/sys/fs/cgroup/cpu.stat\");\n const usage = cpuStat?.match(/^usage_usec (\\d+)/m);\n const cpuacctNs = usage ? NaN : Number(read(\"/sys/fs/cgroup/cpuacct/cpuacct.usage\")?.trim());\n const peak =\n read(\"/sys/fs/cgroup/memory.peak\") ??\n read(\"/sys/fs/cgroup/memory/memory.max_usage_in_bytes\") ??\n read(\"/sys/fs/cgroup/memory/memory.usage_in_bytes\");\n let rx = 0, tx = 0, sawNet = false;\n const net = read(\"/proc/net/dev\");\n if (net) {\n for (const line of net.split(\"\\n\").slice(2)) {\n const m = line.trim().match(/^(\\S+):\\s+(\\d+)(?:\\s+\\d+){7}\\s+(\\d+)/);\n if (m && m[1] !== \"lo\") { rx += Number(m[2]); tx += Number(m[3]); sawNet = true; }\n }\n }\n return {\n busyCpuSecs: usage\n ? Number(usage[1]) / 1e6\n : Number.isFinite(cpuacctNs) && cpuacctNs > 0\n ? cpuacctNs / 1e9\n : null,\n peakMemBytes: peak ? Number(peak.trim()) || null : null,\n rxBytes: sawNet ? rx : null,\n txBytes: sawNet ? tx : null,\n };\n}\n\ntry {\n // Runtime state (venvs, npm prefixes) lands in the agent user's real home\n // \u2014 writable, inside the container, gone with it.\n process.env.FOLDRUN_DATA = \"/home/agent/.foldrun\";\n const { executeStep } = await import(\"@foldrun/core/step-exec\");\n const { buildApiTools } = await import(\"@foldrun/core/api-tools\");\n const { buildScriptTools } = await import(\"@foldrun/core/script-tools\");\n const { buildConsultTools } = await import(\"@foldrun/core/agent-tools\");\n const { buildSearchTools, buildHistoryTools, buildDeskTools } = await import(\"@foldrun/core/context-tools\");\n const { httpChannel, buildAskTool, buildPeerTools, inboxReader } = await import(\"@foldrun/core/operator\");\n const { startTranslator } = await import(\"@foldrun/core/translator\");\n const { prepareRuntime } = await import(\"@foldrun/core/runtime\");\n const { materializeFileSecrets } = await import(\"@foldrun/core/secret-files\");\n const { paidWebActions } = await import(\"@foldrun/core/browser-pod\");\n mark(\"imports\");\n\n const agentDir = \"/workspace/\" + input.agentRel;\n // HOME is where a bare relative path lands when anything resolves one\n // outside cwd \u2014 and root's /root is outside the workspace, so such a read\n // was refused by confinement with a path the agent never typed. Its own\n // directory is both the truthful answer and the safe one.\n // @file secrets crossed as content; materialise them to 0600 paths inside\n // the container (a host path from the other side would not exist here).\n const env = materializeFileSecrets(agentDir, { ...process.env, HOME: agentDir }).env;\n // Caches go to the sandbox user's own home, not to HOME: the Claude CLI's\n // logs, fontconfig, Mesa and gstreamer all follow XDG_CACHE_HOME, and under\n // agents/<name>/.cache they were copied back after every step \u2014 3,855 files\n // across 179 agent folders on the box by 17 Oct 2026, read by nothing.\n env.XDG_CACHE_HOME = \"/home/agent/.cache\";\n\n // The declared runtime, built in here \u2014 pip and npm have the network, which\n // the host side may not. It lands in .foldrun/runner/.runtimes, which the\n // executors mount from a per-tenant directory (run-container.ts#RUNTIME_CACHE):\n // the first step for a given fingerprint installs, every later one finds\n // .ready and skips. With no mount it still works, just fresh every time \u2014\n // which is what this used to be unconditionally. The log says which happened.\n const runtime = input.runtime\n ? prepareRuntime(\"runner\", input.runtime)\n : { interpreters: {}, env: {}, log: [], error: null };\n for (const line of runtime.log) emit(\"info\", \"runtime: \" + line);\n if (runtime.error) emit(\"error\", \"runtime: \" + runtime.error);\n // The step's own environment gets it too, not only the script tools: an\n // agent that runs \"python3 x.py\" or a shebang script through Bash should\n // meet the packages its runtime declared, not the image's bare python.\n Object.assign(env, runtime.env);\n\n // install: what the agent finds it needs mid-step. apt goes through the\n // root start-up's helper (install-helper.ts); pip and npm land in a\n // directory of this container's own, on the paths from the first turn.\n const { buildInstallTool, INSTALL_TOOL } = await import(\"@foldrun/core/install-tool\");\n const { stepInstallEnv, INSTALL_SOCKET } = await import(\"@foldrun/core/runtime\");\n const installDir = \"/home/agent/.foldrun/step\";\n const installing = (input.allowed ?? []).includes(INSTALL_TOOL);\n if (installing) Object.assign(env, stepInstallEnv(installDir, env));\n const install = installing\n ? buildInstallTool({\n dir: installDir,\n socket: env.FOLDRUN_INSTALL_SOCKET || INSTALL_SOCKET,\n cacheRoot: \"/home/agent/.foldrun/runner/.runtimes\",\n python: env.VIRTUAL_ENV ? env.VIRTUAL_ENV + \"/bin/python\" : \"python3\",\n env,\n emit,\n })\n : { server: null };\n\n // API and script tools, rebuilt in here from their specs. Secrets were\n // substituted into API headers before the input crossed the boundary, so\n // buildApiTools gets an already-resolved environment. verify: gets the\n // container's own environment, whole: it is the boundary, assembled on\n // the host from an allowlist, and the declared secrets are in it.\n // A long result is kept in a file only when the agent can read it back.\n // (tool-output.ts canReadBack, inlined: the driver imports core by subpath.)\n const keepIn = { dir: agentDir, readable: () => (input.allowed ?? []).some((t) => t === \"Read\" || t === \"Grep\") };\n // A declared secret named like a model variable crosses as\n // FOLDRUN_DECLARED_<NAME> (runner.ts, FOL-19): the model keeps its own\n // settings, and the agent's API and script tools and verify: get the value\n // back under the name it declared. Built here, after the runtime and any\n // mid-step install have added their paths to env.\n const toolEnv = { ...env };\n for (const [k, v] of Object.entries(env)) if (k.startsWith(\"FOLDRUN_DECLARED_\")) toolEnv[k.slice(\"FOLDRUN_DECLARED_\".length)] = v;\n const api = buildApiTools(\"\", input.apis, undefined, { env: toolEnv, missing: [] }, keepIn);\n const script = buildScriptTools(\n agentDir,\n input.scripts,\n toolEnv,\n \"/library/scripts\",\n runtime.interpreters,\n null,\n keepIn.readable,\n );\n // The translator, when the provider speaks Chat Completions: a loopback\n // server for the length of this step. Its env replaces the endpoint the\n // host assembled; the provider's real URL and key stay inside it.\n const translator = input.translator ? await startTranslator(input.translator) : null;\n if (translator) {\n Object.assign(env, translator.env);\n emit(\"info\", \"translator: listening on \" + translator.baseUrl + \" for \" + (input.translator.label ?? input.translator.upstreamBase));\n }\n const consult = buildConsultTools(input.consults ?? [], env, emit);\n // The platform's own two groups, rebuilt from values like everything else.\n const search = buildSearchTools(input.search ?? []);\n const history = input.history?.length ? buildHistoryTools(input.history) : { server: null };\n const desks = input.desks?.length ? buildDeskTools(input.desks) : { server: null };\n // A person in the loop, over the proxy this step's lease already names:\n // ask_person when tools: [ask], and the run's inbox after every tool call.\n const channel = httpChannel(env.FOLDRUN_EGRESS);\n const ask = input.ask ? buildAskTool({ channel, timeoutSec: input.ask.timeoutSec, emit }) : { server: null };\n const peers = input.peers ? buildPeerTools({ channel, self: input.peers.self, siblings: input.peers.siblings, emit }) : { server: null };\n const inbox = channel ? inboxReader(channel, emit) : undefined;\n\n mark(\"tools\");\n const outcome = await executeStep({\n agentDir,\n workspaceRoot: \"/workspace\",\n libraryRoot: \"/library\",\n prompt: input.prompt,\n model: input.model,\n effort: input.effort ?? null,\n systemPrompt: input.systemPrompt,\n allowed: input.allowed,\n mcpNames: input.mcpNames,\n mcpServers: {\n ...(api.server ? { foldrun_apis: api.server } : {}),\n ...(script.server ? { foldrun_scripts: script.server } : {}),\n ...(consult.server ? { foldrun_agents: consult.server } : {}),\n ...(search.server ? { foldrun_search: search.server } : {}),\n ...(history.server ? { foldrun_history: history.server } : {}),\n ...(desks.server ? { foldrun_desks: desks.server } : {}),\n ...(ask.server ? { foldrun_ask: ask.server } : {}),\n ...(peers.server ? { foldrun_peers: peers.server } : {}),\n ...(install.server ? { foldrun_install: install.server } : {}),\n ...input.mcpServers,\n },\n ...(inbox ? { inbox } : {}),\n ...(input.subagents?.length ? { subagents: input.subagents } : {}),\n env,\n timeoutSec: input.timeoutSec,\n budgetUsd: input.budgetUsd,\n budgetNote: input.budgetNote,\n price: input.price,\n verify: input.verify,\n verifyEnv: toolEnv,\n ...(typeof input.judgeModel === \"string\" ? { judgeModel: input.judgeModel } : {}),\n ...(typeof input.judgeEffort === \"string\" ? { judgeEffort: input.judgeEffort } : {}),\n output: input.output,\n schema: input.schema,\n maxTurns: input.maxTurns,\n ...(input.capture === \"metadata\" || input.capture === \"full\" ? { capture: input.capture } : {}),\n ...(input.limits && Object.keys(input.limits).length ? { limits: input.limits, toolOwners: input.toolOwners ?? {} } : {}),\n ...(input.hookNames?.length ? { hookNames: input.hookNames, hookBase: input.hookBase ?? {} } : {}),\n // Slim browsing: the platform put this step on the slim image to browse\n // through the account's pod, and named the file the web tool logs its\n // reconnects to. A lost pod stops the step; what it wrote decides what\n // the runner does next (browser-pod.ts).\n ...(env.FOLDRUN_BROWSER_POD_EVENTS ? { browserPod: { events: env.FOLDRUN_BROWSER_POD_EVENTS, methods: api.methods ?? {}, paidWeb: paidWebActions(env), ...(env.FOLDRUN_BROWSER_INIT ? { browseInit: env.FOLDRUN_BROWSER_INIT } : {}) } } : {}),\n // The container is the boundary; the SDK's own bash sandbox here would\n // only block declared network use (SSH, curl) for no added safety.\n sandboxBash: false,\n emit,\n });\n for (const line of api.drainLog()) emit(\"info\", \"api: \" + line);\n for (const line of script.drainLog()) emit(\"info\", \"script: \" + line);\n if (translator) {\n for (const line of translator.drainLog()) emit(\"info\", line.startsWith(\"translator\") ? line : \"translator: \" + line);\n await translator.close();\n }\n // A consult's spend belongs to the step that asked.\n const consultCost = consult.drainCost();\n if (consultCost > 0) outcome.costUsd = (outcome.costUsd ?? 0) + consultCost;\n if (consultCost > 0 && typeof outcome.turnsCostUsd === \"number\") outcome.turnsCostUsd += consultCost;\n process.stdout.write(JSON.stringify({ e: \"done\", ...outcome, res: readActuals() }) + \"\\n\");\n process.exit(0);\n} catch (err) {\n emit(\"error\", err instanceof Error ? err.message : String(err));\n // The failure path reports actuals too \u2014 a step that died eleven minutes\n // in still held its slice for eleven minutes, and the failed runs are\n // exactly the ones whose resource story gets read.\n let res = null;\n try { res = readActuals(); } catch { /* the reading must never mask the error */ }\n process.stdout.write(JSON.stringify({ e: \"done\", status: \"failed\", result: null, costUsd: null, res }) + \"\\n\");\n process.exit(0);\n}\n";
|
|
288
293
|
/** The uv release copied into the image. Pinned: a floating uv is a floating
|
|
289
294
|
* resolver, and the runtime cache is keyed on the declaration, not on it. */
|
|
290
295
|
export declare const UV_VERSION = "0.11.25";
|
|
@@ -415,6 +420,10 @@ export interface RunInContainerArgs {
|
|
|
415
420
|
* without it, "stop" could only mean "stop after this step", and a
|
|
416
421
|
* browser step has fifteen minutes left to spend. */
|
|
417
422
|
runId?: string;
|
|
423
|
+
/** Whether a person has stopped the run, read from the record. A step that
|
|
424
|
+
* waits for room before its sandbox exists asks this, since there is no
|
|
425
|
+
* sandbox yet for the stop to destroy. */
|
|
426
|
+
stopRequested?: () => boolean;
|
|
418
427
|
/** The reservation class — which limits this step's sandbox holds. */
|
|
419
428
|
size?: "small" | "large" | "heavy";
|
|
420
429
|
/** Re-attach to a sandbox a previous driver started, instead of creating
|
|
@@ -410,7 +410,8 @@ export function runtimeCacheMount(tenant) {
|
|
|
410
410
|
return { source, target: RUNTIME_CACHE };
|
|
411
411
|
}
|
|
412
412
|
// ---------- the runner image ----------
|
|
413
|
-
|
|
413
|
+
/** The runner container's entrypoint, as JS text: written into the image as driver.mjs, so it must be plain JS — a TypeScript cast here broke every step on v2026.10.21.5 (tests/run-container.test.ts checks it). */
|
|
414
|
+
export const DRIVER = `// The runner container's entrypoint. Reads one step, executes it with the
|
|
414
415
|
// same core the server uses, speaks JSONL on stdout, exits.
|
|
415
416
|
import fs from "node:fs";
|
|
416
417
|
const input = JSON.parse(fs.readFileSync("/opt/runner/job/input.json", "utf8"));
|
|
@@ -608,6 +609,8 @@ try {
|
|
|
608
609
|
price: input.price,
|
|
609
610
|
verify: input.verify,
|
|
610
611
|
verifyEnv: toolEnv,
|
|
612
|
+
...(typeof input.judgeModel === "string" ? { judgeModel: input.judgeModel } : {}),
|
|
613
|
+
...(typeof input.judgeEffort === "string" ? { judgeEffort: input.judgeEffort } : {}),
|
|
611
614
|
output: input.output,
|
|
612
615
|
schema: input.schema,
|
|
613
616
|
maxTurns: input.maxTurns,
|
package/dist/src/runner.js
CHANGED
|
@@ -9,7 +9,7 @@ import { hookNames } from "./hooks.js";
|
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
import os from "node:os";
|
|
11
11
|
import path from "node:path";
|
|
12
|
-
import matter from "./matter.js";
|
|
12
|
+
import matter, { parseLenient, configMatter } from "./matter.js";
|
|
13
13
|
import { knownPrice, executeStep, extractJson, stepCeiling, stepCeilingFor } from "./step-exec.js";
|
|
14
14
|
import { cascadeLimits, toolOwners } from "./limits.js";
|
|
15
15
|
import { resolveAgentPath } from "./confine.js";
|
|
@@ -18,6 +18,7 @@ import { eventUrl } from "./webhook.js";
|
|
|
18
18
|
import { runStepInContainer, sizeLimits, killRunSandboxes } from "./run-container.js";
|
|
19
19
|
import { hostSafeEnv, platformApiUrl } from "./host-env.js";
|
|
20
20
|
import { resolveModelCredential, credentialLine, accountModelBlock, isClaudeLoginToken, isModelEnvName, DECLARED_ENV_PREFIX } from "./model-credential.js";
|
|
21
|
+
import { parseJudge } from "./judge-verdict.js";
|
|
21
22
|
import { validateSchema, describeSchemaErrors, looksLikeSchema } from "./json-schema.js";
|
|
22
23
|
import { EGRESS_ENV, MODEL_KEY_NAME, addGrant, hostOf, placeholderNames, proxyModelEnv, unsubstitute } from "./egress.js";
|
|
23
24
|
import { explainRefusal, isRefusalStatus, refusalFromLine, supplyNote } from "./refusal.js";
|
|
@@ -36,7 +37,7 @@ import { ASK_TOOL, PEER_TOOLS, applyOperatorEvent, askPromptLine, askTimeoutSec,
|
|
|
36
37
|
import { sendPlainNotification } from "./notify.js";
|
|
37
38
|
import { TOOL_MAP, BUILTIN_TOOLS, ownToolNames, toolRefs, legacyUseNames, legacyUseError, retiredToolNames, retiredToolError } from "./tool-names.js";
|
|
38
39
|
import { refNames } from "./refs.js";
|
|
39
|
-
import { accountDir, workspaceDir, buildMemoryIndex, workspaceMemoryIndex, knowledgeIndex, workspaceKnowledgeIndex, workspaceTools, brokenToolReport, listWorkspaces, listTenants, listRuns, liveRuns, syncBundleFor, parseToolDef, readFlow, markerPresent, whenRowsPath, parseApis, resolveModel, parseProvider, providerEnvFor, resolveEffort, isEffortWord, EFFORT_LEVELS, checkFormatVersion, readRun, writeRun, runSummary, verdictOf, sumAttempts, tokensOf, parseSchemaText, STORAGE_DIR, adoptLegacyVersionKey, listAgents, } from "./store.js";
|
|
40
|
+
import { accountDir, workspaceDir, buildMemoryIndex, workspaceMemoryIndex, knowledgeIndex, workspaceKnowledgeIndex, workspaceTools, brokenToolReport, listWorkspaces, listTenants, listRuns, liveRuns, syncBundleFor, parseToolDef, readFlow, markerPresent, whenRowsPath, parseApis, resolveModel, parseProvider, providerEnvFor, resolveEffort, isEffortWord, EFFORT_LEVELS, checkFormatVersion, readRun, writeRun, isRescueRecord, runSummary, verdictOf, sumAttempts, tokensOf, parseSchemaText, STORAGE_DIR, adoptLegacyVersionKey, listAgents, } from "./store.js";
|
|
40
41
|
import { canReadBack } from "./tool-output.js";
|
|
41
42
|
import { PromptLayers, promptCacheKey } from "./prompt-layers.js";
|
|
42
43
|
import { loadCatalog, checkModel, clampEffort, catalogCost, findModel, tokenPriceOf } from "./catalog.js";
|
|
@@ -266,27 +267,9 @@ const toTags = (v) => Array.isArray(v) ? v.map(String) : typeof v === "string" &
|
|
|
266
267
|
// rather than dropping the skill, so a cross-client skill still loads here.
|
|
267
268
|
function safeMatter(raw) {
|
|
268
269
|
// gray-matter caches a *failed* parse as a hollow success; ./matter.ts
|
|
269
|
-
// parses every call, so the
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
return matter(raw);
|
|
273
|
-
}
|
|
274
|
-
catch {
|
|
275
|
-
const fm = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
276
|
-
if (fm) {
|
|
277
|
-
const fixed = fm[1].replace(/^([A-Za-z0-9_-]+):[ \t]+(?!["'>|])(.*:.*)$/gm, (_m, k, v) => `${k}: ${JSON.stringify(v)}`);
|
|
278
|
-
try {
|
|
279
|
-
// A function replacer, not a string: a fixed block containing `$&` or
|
|
280
|
-
// `$$` (a value with those in it) would otherwise be treated as a
|
|
281
|
-
// replacement pattern and splice the whole frontmatter into itself.
|
|
282
|
-
return matter(raw.replace(fm[1], () => fixed));
|
|
283
|
-
}
|
|
284
|
-
catch {
|
|
285
|
-
/* fall through */
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return { data: {} };
|
|
289
|
-
}
|
|
270
|
+
// parses every call, so the strict read throws every time and the lenient
|
|
271
|
+
// one (shared with check, which warns about it) fires every time.
|
|
272
|
+
return { data: parseLenient(raw).data };
|
|
290
273
|
}
|
|
291
274
|
// Both layouts: <root>/<name>/SKILL.md (Agent Skills standard) and the flat
|
|
292
275
|
// <root>/<name>.md convenience form. `subdir` is the skills root under the
|
|
@@ -385,7 +368,7 @@ export function readAgentsMd(dir) {
|
|
|
385
368
|
if (!fs.existsSync(file))
|
|
386
369
|
continue;
|
|
387
370
|
try {
|
|
388
|
-
const parsed =
|
|
371
|
+
const parsed = configMatter(fs.readFileSync(file, "utf8"));
|
|
389
372
|
return { data: parsed.data, body: withoutAgentRules(parsed.content).trim() };
|
|
390
373
|
}
|
|
391
374
|
catch {
|
|
@@ -753,7 +736,7 @@ grantsOnly = false) {
|
|
|
753
736
|
// agentDir is <data>/<tenant>/projects/<workspace>/agents/<agent>, so the
|
|
754
737
|
// workspace a secret should resolve against is two levels up.
|
|
755
738
|
const workspace = path.basename(path.resolve(agentDir, "..", ".."));
|
|
756
|
-
const { data: front, content: body } =
|
|
739
|
+
const { data: front, content: body } = configMatter(fs.readFileSync(path.join(agentDir, "agent.md"), "utf8"));
|
|
757
740
|
// The prompt's sections, by name and by how often they change. Added in
|
|
758
741
|
// the order they are computed; assembled stable first, so the model's
|
|
759
742
|
// cache keeps the agent's half across steps and runs (prompt-layers.ts).
|
|
@@ -1915,6 +1898,15 @@ peerBus) {
|
|
|
1915
1898
|
// AGENTS.md — every model call always; each tool call's input and output
|
|
1916
1899
|
// unless it says `metadata`.
|
|
1917
1900
|
const capture = workspaceFrontmatter(agentDir, tenant).capture === "metadata" ? "metadata" : "full";
|
|
1901
|
+
// `judge:` (AGENTS.md): the model and effort every `verify: judge:` here
|
|
1902
|
+
// grades with — field by field, the workspace's else the account's.
|
|
1903
|
+
const { judgeModel, judgeEffort } = (() => {
|
|
1904
|
+
const ws = parseJudge(readAgentsMd(workspaceRootOf(agentDir))?.data?.judge);
|
|
1905
|
+
const account = parseJudge(readAgentsMd(accountDir(tenant))?.data?.judge);
|
|
1906
|
+
const model = ws.model ?? account.model;
|
|
1907
|
+
const effort = resolveEffort(ws.effort ?? account.effort ?? "");
|
|
1908
|
+
return { judgeModel: model ? resolveModel(model) : undefined, judgeEffort: effort ?? undefined };
|
|
1909
|
+
})();
|
|
1918
1910
|
// A step that pours out content stops being recorded past this — its
|
|
1919
1911
|
// calls keep their names, sizes and times (FOL-54 step 4).
|
|
1920
1912
|
let capturedChars = 0;
|
|
@@ -2651,6 +2643,8 @@ peerBus) {
|
|
|
2651
2643
|
budgetNote: stepBudgetNote,
|
|
2652
2644
|
price: tokenPrice,
|
|
2653
2645
|
verify: step.verify,
|
|
2646
|
+
...(judgeModel ? { judgeModel } : {}),
|
|
2647
|
+
...(judgeEffort ? { judgeEffort } : {}),
|
|
2654
2648
|
output: step.output,
|
|
2655
2649
|
schema: resolvedSchema ?? undefined,
|
|
2656
2650
|
maxTurns: step.maxTurns,
|
|
@@ -2705,6 +2699,9 @@ peerBus) {
|
|
|
2705
2699
|
emit: pushWatching,
|
|
2706
2700
|
// Stamps the sandbox, so stopping this run can destroy it.
|
|
2707
2701
|
runId,
|
|
2702
|
+
// A step still waiting for room has no sandbox to destroy; the
|
|
2703
|
+
// executor asks this before it creates one (pod-room.ts).
|
|
2704
|
+
...(stopRequested ? { stopRequested } : {}),
|
|
2708
2705
|
size,
|
|
2709
2706
|
// The pod's network, for the executor's own wall (the k8s tier's
|
|
2710
2707
|
// network policy): a filtered pod reaches the proxy and DNS only.
|
|
@@ -3040,6 +3037,8 @@ peerBus) {
|
|
|
3040
3037
|
budgetNote: stepBudgetNote,
|
|
3041
3038
|
price: tokenPrice,
|
|
3042
3039
|
verify: step.verify,
|
|
3040
|
+
...(judgeModel ? { judgeModel } : {}),
|
|
3041
|
+
...(judgeEffort ? { judgeEffort } : {}),
|
|
3043
3042
|
output: step.output,
|
|
3044
3043
|
schema: resolvedSchema ?? undefined,
|
|
3045
3044
|
maxTurns: step.maxTurns,
|
|
@@ -3247,7 +3246,7 @@ export function copyTreeBytes(from, to) {
|
|
|
3247
3246
|
function openApiSources(agentDir, tenant) {
|
|
3248
3247
|
const out = new Set();
|
|
3249
3248
|
try {
|
|
3250
|
-
const { data } =
|
|
3249
|
+
const { data } = configMatter(fs.readFileSync(path.join(agentDir, "agent.md"), "utf8"));
|
|
3251
3250
|
const front = data;
|
|
3252
3251
|
for (const api of parseApis(front.apis))
|
|
3253
3252
|
if (api.openapi?.startsWith("https://"))
|
|
@@ -3839,10 +3838,31 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
3839
3838
|
*
|
|
3840
3839
|
* The flag is one-way: nothing here clears it, so the read-modify-write can
|
|
3841
3840
|
* only ever turn a stop on, never off.
|
|
3841
|
+
*
|
|
3842
|
+
* And once it is on, this copy may not undo what stopRun wrote: the run
|
|
3843
|
+
* stays failed, and steps that had not started stay skipped. Before
|
|
3844
|
+
* 21 Oct 2026 only the flag came across, so the next save put the record
|
|
3845
|
+
* back to `running` with its steps pending; a second stop then answered
|
|
3846
|
+
* "failed" instead of "already failed", and the group kept starting pods
|
|
3847
|
+
* (medium-desk run-mvho48wp-99e1). A step running in THIS process is left
|
|
3848
|
+
* as it is: its sandbox is gone, it ends on its own, and records how.
|
|
3842
3849
|
*/
|
|
3843
3850
|
const save = () => {
|
|
3844
|
-
|
|
3851
|
+
const disk = run.stopRequested ? null : readRun(tenant, workspace, run.id);
|
|
3852
|
+
if (disk?.stopRequested)
|
|
3845
3853
|
run.stopRequested = true;
|
|
3854
|
+
if (run.stopRequested) {
|
|
3855
|
+
if (run.status !== "completed" && run.status !== "failed") {
|
|
3856
|
+
run.status = "failed";
|
|
3857
|
+
run.finishedAt ??= disk?.finishedAt ?? new Date().toISOString();
|
|
3858
|
+
}
|
|
3859
|
+
run.parkedAt = null;
|
|
3860
|
+
for (const s of run.steps) {
|
|
3861
|
+
if (s.status === "pending" || s.status === "awaiting-approval") {
|
|
3862
|
+
s.status = "skipped";
|
|
3863
|
+
s.skipReason ??= "run stopped";
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3846
3866
|
}
|
|
3847
3867
|
writeRun(tenant, workspace, run);
|
|
3848
3868
|
};
|
|
@@ -4063,7 +4083,13 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4063
4083
|
return groupData[i];
|
|
4064
4084
|
return undefined;
|
|
4065
4085
|
};
|
|
4066
|
-
|
|
4086
|
+
// A rescue's result already stands on the step it took over, so the
|
|
4087
|
+
// three handoffs below read the flow's steps only: counting the rescue
|
|
4088
|
+
// too handed the next group the same output twice, or a stale one
|
|
4089
|
+
// from an evaluator loop's earlier cycle.
|
|
4090
|
+
const flowStepsOf = (group) => group.filter((s) => !isRescueRecord(s));
|
|
4091
|
+
const dataOf = (all) => {
|
|
4092
|
+
const group = flowStepsOf(all);
|
|
4067
4093
|
const carrying = group.filter((s) => s.status === "completed" && s.data !== undefined);
|
|
4068
4094
|
if (carrying.length === 0)
|
|
4069
4095
|
return undefined;
|
|
@@ -4071,7 +4097,8 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4071
4097
|
return carrying[0].data;
|
|
4072
4098
|
return carrying.map((s) => ({ agent: s.agent, ...(s.item ? { item: s.item } : {}), data: s.data }));
|
|
4073
4099
|
};
|
|
4074
|
-
const resultsOf = (
|
|
4100
|
+
const resultsOf = (all) => {
|
|
4101
|
+
const group = flowStepsOf(all);
|
|
4075
4102
|
const results = group
|
|
4076
4103
|
.filter((s) => s.status === "completed" && s.result)
|
|
4077
4104
|
.map((s) => s.item
|
|
@@ -4083,8 +4110,8 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4083
4110
|
};
|
|
4084
4111
|
// What each step concluded — its final turn — for the handoffs that
|
|
4085
4112
|
// should read a conclusion rather than a transcript (`each: lines`).
|
|
4086
|
-
const conclusionsOf = (
|
|
4087
|
-
const out =
|
|
4113
|
+
const conclusionsOf = (all) => {
|
|
4114
|
+
const out = flowStepsOf(all)
|
|
4088
4115
|
.filter((s) => s.status === "completed" && (s.conclusion || s.result))
|
|
4089
4116
|
.map((s) => s.conclusion || s.result);
|
|
4090
4117
|
return out.length ? out.join("\n") : null;
|
|
@@ -4459,7 +4486,7 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4459
4486
|
save();
|
|
4460
4487
|
if (freshGroup.every((s) => s.status !== "pending")) {
|
|
4461
4488
|
// everything in this group was rejected
|
|
4462
|
-
if (freshGroup.some((s) => s.status === "failed" && !s.optional && !s.carriedFrom)) {
|
|
4489
|
+
if (freshGroup.some((s) => s.status === "failed" && !s.optional && !s.carriedFrom && !isRescueRecord(s))) {
|
|
4463
4490
|
run.status = "failed";
|
|
4464
4491
|
break;
|
|
4465
4492
|
}
|
|
@@ -4472,7 +4499,11 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4472
4499
|
// itself mid-turn at its share, so a fan-out of twenty cannot end
|
|
4473
4500
|
// twenty steps over — the shares sum to what was left. Between
|
|
4474
4501
|
// groups the check above still refuses to start the next one.
|
|
4475
|
-
|
|
4502
|
+
// A rescue record is the on-fail of a step in this group, not a step
|
|
4503
|
+
// of the flow: one left pending by a drive that died is picked up by
|
|
4504
|
+
// rescueStep below, never launched alongside the group (21 Oct 2026:
|
|
4505
|
+
// a resumed run launched 77 leftover fixers at once).
|
|
4506
|
+
const launching = freshGroup.filter((s) => s.status === "pending" && !isRescueRecord(s));
|
|
4476
4507
|
// tools: [peers] — what each step of the group is told about the
|
|
4477
4508
|
// others, and the bus they share when they run in this process.
|
|
4478
4509
|
const peerBus = localPeerBus();
|
|
@@ -4531,7 +4562,7 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4531
4562
|
// lead's reply or for a slot — must not start it: the
|
|
4532
4563
|
// between-groups check runs only once this group is over,
|
|
4533
4564
|
// and save() is what carries the flag in from the record.
|
|
4534
|
-
|
|
4565
|
+
{
|
|
4535
4566
|
save();
|
|
4536
4567
|
if (run.stopRequested) {
|
|
4537
4568
|
step.status = "skipped";
|
|
@@ -4694,6 +4725,13 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4694
4725
|
// runs out of cycles below, which used to fail the run without ever
|
|
4695
4726
|
// reaching the step's on-fail (medium-desk's claim-trimmer was
|
|
4696
4727
|
// written for exactly that case and could never run).
|
|
4728
|
+
// A rescue hears a stop the way a group step does: its model loop and
|
|
4729
|
+
// its executor's wait for room both poll this (no sandbox exists yet
|
|
4730
|
+
// for a stop to destroy while it waits).
|
|
4731
|
+
const stopPoll = () => {
|
|
4732
|
+
save();
|
|
4733
|
+
return run.stopRequested === true;
|
|
4734
|
+
};
|
|
4697
4735
|
const rescueStep = async (step) => {
|
|
4698
4736
|
if (!step.onFail)
|
|
4699
4737
|
return;
|
|
@@ -4706,6 +4744,38 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4706
4744
|
});
|
|
4707
4745
|
return;
|
|
4708
4746
|
}
|
|
4747
|
+
// One rescue per failure, across drives. A worker that died
|
|
4748
|
+
// mid-rescue left an unfinished record behind it; the drive that
|
|
4749
|
+
// resumes picks that one up instead of splicing another — a crash
|
|
4750
|
+
// loop used to add one per restart (blog-desk run-mvh1zra7-ae4k,
|
|
4751
|
+
// ~80). The leftover carries the same instruction, built from the
|
|
4752
|
+
// same failure, and a running one may still hold a sandbox to
|
|
4753
|
+
// re-attach to, so it is THE rescue; any further unfinished copies
|
|
4754
|
+
// are superseded. A FINISHED rescue is history, never reused: it
|
|
4755
|
+
// answered an earlier failure — an evaluator loop's previous cycle
|
|
4756
|
+
// — not this one.
|
|
4757
|
+
const leftovers = [];
|
|
4758
|
+
for (let j = run.steps.indexOf(step) + 1; j < run.steps.length && isRescueRecord(run.steps[j]); j++) {
|
|
4759
|
+
const r = run.steps[j];
|
|
4760
|
+
if (r.agent === step.onFail && (r.rescueOf === undefined || r.rescueOf === step.agent) && (r.status === "pending" || r.status === "running")) {
|
|
4761
|
+
leftovers.push(r);
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
const resumed = leftovers[0];
|
|
4765
|
+
for (const r of leftovers.slice(1)) {
|
|
4766
|
+
r.status = "skipped";
|
|
4767
|
+
r.skipReason = "superseded on resume — one rescue per failure";
|
|
4768
|
+
}
|
|
4769
|
+
if (resumed) {
|
|
4770
|
+
step.events.push({ t: new Date().toISOString(), type: "info", text: `resuming the rescue by [[${step.onFail}]] that an earlier drive started` });
|
|
4771
|
+
resumed.rescueOf ??= step.agent;
|
|
4772
|
+
resumed.attempts = Math.max(1, resumed.attempts ?? 0);
|
|
4773
|
+
resumed.status = "running";
|
|
4774
|
+
save();
|
|
4775
|
+
await runStep(rescuerDir, tenant, resumed, ctx, save, modelOverride, tags, effortOverride, run.id, ctxData, null, undefined, run.test === true, stopPoll, flowTimezone, run.steps.indexOf(resumed));
|
|
4776
|
+
settleRescue(step, resumed);
|
|
4777
|
+
return;
|
|
4778
|
+
}
|
|
4709
4779
|
step.events.push({
|
|
4710
4780
|
t: new Date().toISOString(),
|
|
4711
4781
|
type: "info",
|
|
@@ -4722,6 +4792,7 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4722
4792
|
const owesMarker = step.until ? `\nThe step's loop ended without its marker: your reply must end with a line saying ${step.until}.` : "";
|
|
4723
4793
|
const rescue = {
|
|
4724
4794
|
agent: step.onFail,
|
|
4795
|
+
rescueOf: step.agent,
|
|
4725
4796
|
instruction: `${step.instruction}\n\n<rescue>\nYou are taking this step over: [[${step.agent}]] attempted it and failed. ` +
|
|
4726
4797
|
`Its failure, for whatever it is worth:\n${failure}${approved}${owesMarker}\n</rescue>`,
|
|
4727
4798
|
...(step.ask ? { ask: step.ask } : {}),
|
|
@@ -4751,7 +4822,11 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4751
4822
|
rescue.attempts = 1;
|
|
4752
4823
|
rescue.status = "running";
|
|
4753
4824
|
save();
|
|
4754
|
-
await runStep(rescuerDir, tenant, rescue, ctx, save, modelOverride, tags, effortOverride, run.id, ctxData, null, undefined, run.test === true,
|
|
4825
|
+
await runStep(rescuerDir, tenant, rescue, ctx, save, modelOverride, tags, effortOverride, run.id, ctxData, null, undefined, run.test === true, stopPoll, flowTimezone, run.steps.indexOf(rescue));
|
|
4826
|
+
settleRescue(step, rescue);
|
|
4827
|
+
};
|
|
4828
|
+
// What a rescue's end means for the step it took over.
|
|
4829
|
+
const settleRescue = (step, rescue) => {
|
|
4755
4830
|
// runStep mutates rescue.status; widen past TS's "running" narrowing.
|
|
4756
4831
|
const rescueOutcome = rescue.status;
|
|
4757
4832
|
if (rescueOutcome === "completed") {
|
|
@@ -4786,7 +4861,10 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4786
4861
|
// here: the person chose to start after it. Counting it stopped
|
|
4787
4862
|
// fix-desk's "re-run from the verifier" (run-muwfi5jn-4217) in under
|
|
4788
4863
|
// a second, before the one step it was asked to run.
|
|
4789
|
-
|
|
4864
|
+
// A rescue record's own failure is already on the step it took over
|
|
4865
|
+
// ("also failed"); counted here it would fail a group whose step a
|
|
4866
|
+
// later rescue completed.
|
|
4867
|
+
const requiredFailed = freshGroup.some((s) => s.status === "failed" && !s.optional && !s.carriedFrom && !isRescueRecord(s));
|
|
4790
4868
|
if (requiredFailed) {
|
|
4791
4869
|
run.status = "failed";
|
|
4792
4870
|
break;
|
|
@@ -4862,6 +4940,11 @@ function driveRunInner(tenant, workspace, run, modelOverride, tags, opts) {
|
|
|
4862
4940
|
for (const s of [...ordered[gi - 1], ...freshGroup]) {
|
|
4863
4941
|
if (s.status === "skipped" && s.each)
|
|
4864
4942
|
continue; // an expanded template stays expanded
|
|
4943
|
+
// A rescue answered its own failure and stays as it ended: reset
|
|
4944
|
+
// to pending it read as an interrupted rescue, and the next
|
|
4945
|
+
// cycle's failure was handed the old one's instructions.
|
|
4946
|
+
if (isRescueRecord(s))
|
|
4947
|
+
continue;
|
|
4865
4948
|
s.status = "pending";
|
|
4866
4949
|
s.result = null;
|
|
4867
4950
|
}
|
package/dist/src/scan.d.ts
CHANGED
|
@@ -83,6 +83,17 @@ export declare function untrustedTextFindings(text: string): {
|
|
|
83
83
|
}[];
|
|
84
84
|
/** The known field a key was probably meant to be, or null. */
|
|
85
85
|
export declare function nearestField(key: string, known: readonly string[]): string | null;
|
|
86
|
+
/** What a run leaves in an agent's folder — the Claude CLI's `.cache/` logs,
|
|
87
|
+
* package caches — and copies back with it. Nobody wrote these, so they are
|
|
88
|
+
* not checked as prompts or code; they are still searched for credentials,
|
|
89
|
+
* since a key in a log here is a key in the workspace (seen live on
|
|
90
|
+
* 17 Oct 2026: zero-width characters in a CLI MCP log, reported as hidden
|
|
91
|
+
* text). */
|
|
92
|
+
/** The definition files a deploy refuses outright when their frontmatter does
|
|
93
|
+
* not parse (deploy.ts deployIssues): the workspace's own, by these paths.
|
|
94
|
+
* Everything else the platform reads frontmatter from — skills, and the
|
|
95
|
+
* account library's agents, tools and hooks — is the scanner's to report. */
|
|
96
|
+
export declare const STRICT_FRONTMATTER_PATH: RegExp;
|
|
86
97
|
/** How many times one rule is listed for one file before the rest are counted. */
|
|
87
98
|
export declare const PER_FILE_RULE = 100;
|
|
88
99
|
/**
|