@foldrun/core 2.4.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-activity.d.ts +5 -0
- package/dist/src/agent-activity.js +31 -0
- 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/alerts.d.ts +60 -0
- package/dist/src/alerts.js +161 -0
- package/dist/src/browser-pod.js +4 -0
- package/dist/src/capture.d.ts +23 -0
- package/dist/src/capture.js +80 -0
- package/dist/src/compare.d.ts +57 -0
- package/dist/src/compare.js +116 -0
- package/dist/src/completions.js +26 -3
- package/dist/src/deploy.d.ts +36 -1
- package/dist/src/deploy.js +237 -13
- package/dist/src/egress.d.ts +16 -1
- package/dist/src/evals.d.ts +27 -2
- package/dist/src/evals.js +62 -19
- 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 +60 -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 +33 -2
- package/dist/src/frontmatter-text.d.ts +31 -0
- package/dist/src/frontmatter-text.js +102 -0
- package/dist/src/history.d.ts +4 -0
- package/dist/src/history.js +15 -0
- package/dist/src/hooks.js +1 -1
- package/dist/src/insights.d.ts +30 -0
- package/dist/src/insights.js +152 -0
- package/dist/src/issues.d.ts +140 -0
- package/dist/src/issues.js +302 -0
- package/dist/src/judge-verdict.d.ts +19 -0
- package/dist/src/judge-verdict.js +54 -0
- package/dist/src/library.js +1 -1
- package/dist/src/limits.d.ts +8 -0
- package/dist/src/limits.js +36 -5
- package/dist/src/linkable.js +1 -1
- package/dist/src/matter.d.ts +42 -0
- package/dist/src/matter.js +132 -0
- package/dist/src/model-credential.d.ts +14 -0
- package/dist/src/model-credential.js +40 -0
- package/dist/src/network.d.ts +12 -1
- package/dist/src/network.js +35 -8
- package/dist/src/notify.d.ts +16 -0
- package/dist/src/notify.js +17 -7
- package/dist/src/okf.js +1 -1
- package/dist/src/online-checks.d.ts +76 -0
- package/dist/src/online-checks.js +432 -0
- package/dist/src/operator.d.ts +277 -0
- package/dist/src/operator.js +176 -2
- package/dist/src/otel-config.d.ts +18 -0
- package/dist/src/otel-config.js +69 -0
- package/dist/src/otel.d.ts +38 -0
- package/dist/src/otel.js +85 -0
- package/dist/src/outcome.d.ts +40 -0
- package/dist/src/outcome.js +77 -0
- package/dist/src/package.js +9 -2
- package/dist/src/platform.d.ts +1 -1
- package/dist/src/prompt-layers.d.ts +30 -0
- package/dist/src/prompt-layers.js +77 -0
- package/dist/src/provider-health.js +2 -2
- package/dist/src/providers.d.ts +5 -0
- package/dist/src/providers.js +2 -2
- package/dist/src/run-container.d.ts +25 -2
- package/dist/src/run-container.js +61 -6
- package/dist/src/runner.d.ts +53 -2
- package/dist/src/runner.js +471 -102
- package/dist/src/runs-filter.d.ts +48 -0
- package/dist/src/runs-filter.js +73 -0
- package/dist/src/scan.d.ts +30 -1
- package/dist/src/scan.js +145 -14
- package/dist/src/starter.js +27 -2
- package/dist/src/step-exec.d.ts +39 -3
- package/dist/src/step-exec.js +118 -15
- package/dist/src/store.d.ts +144 -3
- package/dist/src/store.js +150 -7
- package/dist/src/tokens.d.ts +38 -0
- package/dist/src/tokens.js +57 -0
- package/dist/src/tool-names.js +1 -1
- package/dist/src/tool-programs.js +1 -1
- package/dist/src/trace-tree.d.ts +40 -0
- package/dist/src/trace-tree.js +156 -0
- package/dist/src/translator.d.ts +8 -0
- package/dist/src/translator.js +7 -2
- package/dist/src/trigger-log.d.ts +1 -1
- package/dist/src/workspace-check.d.ts +18 -0
- package/dist/src/workspace-check.js +34 -0
- package/package.json +2 -2
- package/src/agent-activity.ts +32 -0
- package/src/agent-import.ts +2 -2
- package/src/agent-tools.ts +2 -2
- package/src/agents-md.ts +39 -14
- package/src/alerts.ts +190 -0
- package/src/browser-pod.ts +3 -0
- package/src/capture.ts +88 -0
- package/src/compare.ts +146 -0
- package/src/completions.ts +27 -3
- package/src/deploy.ts +238 -12
- package/src/egress.ts +12 -2
- package/src/evals.ts +64 -19
- package/src/flow-lint.ts +36 -0
- package/src/flow-patterns.ts +26 -13
- package/src/frontmatter-fields.ts +83 -3
- package/src/frontmatter-guard.ts +70 -0
- package/src/frontmatter-schema.ts +33 -4
- package/src/frontmatter-text.ts +109 -0
- package/src/history.ts +13 -0
- package/src/hooks.ts +1 -1
- package/src/insights.ts +174 -0
- package/src/issues.ts +396 -0
- package/src/judge-verdict.ts +52 -0
- package/src/library.ts +1 -1
- package/src/limits.ts +38 -5
- package/src/linkable.ts +1 -1
- package/src/matter.ts +145 -0
- package/src/model-credential.ts +45 -0
- package/src/network.ts +35 -7
- package/src/notify.ts +22 -7
- package/src/okf.ts +1 -1
- package/src/online-checks.ts +450 -0
- package/src/operator.ts +213 -3
- package/src/otel-config.ts +67 -0
- package/src/otel.ts +100 -0
- package/src/outcome.ts +90 -0
- package/src/package.ts +8 -2
- package/src/platform.ts +1 -1
- package/src/prompt-layers.ts +90 -0
- package/src/provider-health.ts +2 -2
- package/src/providers.ts +7 -2
- package/src/run-container.ts +77 -8
- package/src/runner.ts +477 -79
- package/src/runs-filter.ts +88 -0
- package/src/scan.ts +153 -14
- package/src/starter.ts +27 -2
- package/src/step-exec.ts +168 -17
- package/src/store.ts +268 -9
- package/src/tokens.ts +68 -0
- package/src/tool-names.ts +1 -1
- package/src/tool-programs.ts +1 -1
- package/src/trace-tree.ts +187 -0
- package/src/translator.ts +13 -2
- package/src/trigger-log.ts +1 -0
- package/src/workspace-check.ts +41 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface RunFilter {
|
|
2
|
+
/** A step called a tool whose name contains this (`web`, `crm_export`). */
|
|
3
|
+
tool?: string;
|
|
4
|
+
/** A tool call errored. */
|
|
5
|
+
toolError?: boolean;
|
|
6
|
+
/** A step was this agent. */
|
|
7
|
+
agent?: string;
|
|
8
|
+
/** A step ran a model whose name contains this (`haiku`, `opus`). */
|
|
9
|
+
model?: string;
|
|
10
|
+
/** The run read at least this many input tokens, all steps together. */
|
|
11
|
+
minTokens?: number;
|
|
12
|
+
/** The run took at least this many seconds. */
|
|
13
|
+
minSecs?: number;
|
|
14
|
+
}
|
|
15
|
+
/** The query parameters a filter comes from: tool, tool_error, agent,
|
|
16
|
+
* model, min_tokens, min_secs. Unreadable numbers are refused in words. */
|
|
17
|
+
export declare function parseRunFilter(q: {
|
|
18
|
+
get(name: string): string | null;
|
|
19
|
+
}): {
|
|
20
|
+
filter: RunFilter;
|
|
21
|
+
problems: string[];
|
|
22
|
+
};
|
|
23
|
+
export declare const isEmptyFilter: (f: RunFilter) => boolean;
|
|
24
|
+
type FilterableStep = {
|
|
25
|
+
agent: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
tools?: string[];
|
|
28
|
+
toolErrors?: number;
|
|
29
|
+
/** A whole record has events instead of the index's tools/toolErrors. */
|
|
30
|
+
events?: {
|
|
31
|
+
type: string;
|
|
32
|
+
text: string;
|
|
33
|
+
ms?: number;
|
|
34
|
+
err?: boolean;
|
|
35
|
+
}[];
|
|
36
|
+
tokens?: {
|
|
37
|
+
input?: number;
|
|
38
|
+
} | null;
|
|
39
|
+
startedAt?: string;
|
|
40
|
+
finishedAt?: string;
|
|
41
|
+
};
|
|
42
|
+
type FilterableRun = {
|
|
43
|
+
startedAt: string;
|
|
44
|
+
finishedAt?: string | null;
|
|
45
|
+
steps: FilterableStep[];
|
|
46
|
+
};
|
|
47
|
+
export declare function matchesRunFilter(run: FilterableRun, f: RunFilter, now?: number): boolean;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Search over runs (FOL-72 step 9): one filter for the runs API, the runs
|
|
2
|
+
// pages and `foldrun runs`, read off the run index — the tools each step
|
|
3
|
+
// called, its tool errors, its agent and model, tokens and how long it took.
|
|
4
|
+
// Pure.
|
|
5
|
+
/** The query parameters a filter comes from: tool, tool_error, agent,
|
|
6
|
+
* model, min_tokens, min_secs. Unreadable numbers are refused in words. */
|
|
7
|
+
export function parseRunFilter(q) {
|
|
8
|
+
const filter = {};
|
|
9
|
+
const problems = [];
|
|
10
|
+
const text = (k) => q.get(k)?.trim() || undefined;
|
|
11
|
+
const tool = text("tool");
|
|
12
|
+
if (tool)
|
|
13
|
+
filter.tool = tool.toLowerCase();
|
|
14
|
+
const te = text("tool_error");
|
|
15
|
+
if (te)
|
|
16
|
+
filter.toolError = te === "1" || te === "true" || te === "yes";
|
|
17
|
+
const agent = text("agent");
|
|
18
|
+
if (agent)
|
|
19
|
+
filter.agent = agent;
|
|
20
|
+
const model = text("model");
|
|
21
|
+
if (model)
|
|
22
|
+
filter.model = model.toLowerCase();
|
|
23
|
+
for (const [k, field] of [["min_tokens", "minTokens"], ["min_secs", "minSecs"]]) {
|
|
24
|
+
const v = text(k);
|
|
25
|
+
if (v === undefined)
|
|
26
|
+
continue;
|
|
27
|
+
// "50k" and "1.5k" are thousands (Codex review: "1.5k" read as 1.5).
|
|
28
|
+
const kilo = /^(\d+(?:\.\d+)?)k$/i.exec(v);
|
|
29
|
+
const n = kilo ? Number(kilo[1]) * 1000 : Number(v);
|
|
30
|
+
if (!Number.isFinite(n) || n < 0)
|
|
31
|
+
problems.push(`${k} is a number (${k === "min_tokens" ? "50000 or 50k" : "seconds"}), not ${v}`);
|
|
32
|
+
else
|
|
33
|
+
filter[field] = n;
|
|
34
|
+
}
|
|
35
|
+
return { filter, problems };
|
|
36
|
+
}
|
|
37
|
+
export const isEmptyFilter = (f) => Object.keys(f).length === 0;
|
|
38
|
+
/** The index's facts, or worked out from a whole record's events (Codex
|
|
39
|
+
* review: the workspace runs page reads whole records). */
|
|
40
|
+
function factsOf(s) {
|
|
41
|
+
if (s.tools !== undefined || s.toolErrors !== undefined || !s.events)
|
|
42
|
+
return { tools: s.tools ?? [], toolErrors: s.toolErrors ?? 0 };
|
|
43
|
+
const tools = new Set();
|
|
44
|
+
let errs = 0;
|
|
45
|
+
for (const e of s.events) {
|
|
46
|
+
if (e.type !== "tool")
|
|
47
|
+
continue;
|
|
48
|
+
if (e.ms == null)
|
|
49
|
+
tools.add(e.text);
|
|
50
|
+
else if (e.err)
|
|
51
|
+
errs++;
|
|
52
|
+
}
|
|
53
|
+
return { tools: [...tools], toolErrors: errs };
|
|
54
|
+
}
|
|
55
|
+
export function matchesRunFilter(run, f, now = Date.now()) {
|
|
56
|
+
const steps = (run.steps ?? []).map((s) => ({ ...s, ...factsOf(s) }));
|
|
57
|
+
if (f.tool && !steps.some((s) => (s.tools ?? []).some((t) => t.toLowerCase().includes(f.tool))))
|
|
58
|
+
return false;
|
|
59
|
+
if (f.toolError !== undefined && steps.some((s) => (s.toolErrors ?? 0) > 0) !== f.toolError)
|
|
60
|
+
return false;
|
|
61
|
+
if (f.agent && !steps.some((s) => s.agent === f.agent))
|
|
62
|
+
return false;
|
|
63
|
+
if (f.model && !steps.some((s) => (s.model ?? "").toLowerCase().includes(f.model)))
|
|
64
|
+
return false;
|
|
65
|
+
if (f.minTokens !== undefined && steps.reduce((n, s) => n + (s.tokens?.input ?? 0), 0) < f.minTokens)
|
|
66
|
+
return false;
|
|
67
|
+
if (f.minSecs !== undefined) {
|
|
68
|
+
const secs = ((run.finishedAt ? Date.parse(run.finishedAt) : now) - Date.parse(run.startedAt)) / 1000;
|
|
69
|
+
if (!(secs >= f.minSecs))
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
package/dist/src/scan.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ export interface ScanFinding {
|
|
|
25
25
|
message: string;
|
|
26
26
|
/** Stable across edits elsewhere in the file: rule + file + line text. */
|
|
27
27
|
fingerprint: string;
|
|
28
|
+
/** This rule had more to say in this file than PER_FILE_RULE: this
|
|
29
|
+
* finding counts the rest, so what is not listed is not resolved. Its
|
|
30
|
+
* fingerprint is of everything it counts, so a change among them is new. */
|
|
31
|
+
truncated?: true;
|
|
28
32
|
}
|
|
29
33
|
export interface ScanOptions {
|
|
30
34
|
/** Tool names whose definition says `outward: true` — the CLI and the
|
|
@@ -66,11 +70,36 @@ export declare function redactSecret(secret: string): string;
|
|
|
66
70
|
/** The text with every credential in it redacted — for a finding that
|
|
67
71
|
* quotes source, so no message ever carries a key whole. */
|
|
68
72
|
export declare function redactText(text: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* Text from outside the workspace — what a remote MCP server says about its
|
|
75
|
+
* tools — through the content rules with nothing the text itself can turn
|
|
76
|
+
* off: no foldrun-ignore, no binary or vendored-path skip, no fences
|
|
77
|
+
* (FOL-58, Codex review: each of those let a server silence its own finding).
|
|
78
|
+
*/
|
|
79
|
+
export declare function untrustedTextFindings(text: string): {
|
|
80
|
+
rule: string;
|
|
81
|
+
line: number;
|
|
82
|
+
message: string;
|
|
83
|
+
}[];
|
|
69
84
|
/** The known field a key was probably meant to be, or null. */
|
|
70
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;
|
|
97
|
+
/** How many times one rule is listed for one file before the rest are counted. */
|
|
98
|
+
export declare const PER_FILE_RULE = 100;
|
|
71
99
|
/**
|
|
72
100
|
* Every finding in these files, ignores applied. Findings are sorted by file
|
|
73
|
-
* then line, and one rule says one thing once per line
|
|
101
|
+
* then line, and one rule says one thing once per line, and at most
|
|
102
|
+
* PER_FILE_RULE times in one file.
|
|
74
103
|
*/
|
|
75
104
|
export declare function scanFiles(files: ScanFile[], options?: ScanOptions): ScanFinding[];
|
|
76
105
|
/**
|
package/dist/src/scan.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// The reason is required: an ignore without one ignores nothing and is itself
|
|
27
27
|
// reported, because "why is this allowed?" is the question a reviewer asks.
|
|
28
28
|
import crypto from "node:crypto";
|
|
29
|
-
import matter from "
|
|
29
|
+
import matter, { frontmatterErrorLine, frontmatterErrorText, parseLenient, configMatter } from "./matter.js";
|
|
30
30
|
import { refNames } from "./refs.js";
|
|
31
31
|
import { resolveNetwork } from "./network.js";
|
|
32
32
|
import { frontmatterProblems, schemaKindOf } from "./frontmatter-schema.js";
|
|
@@ -117,6 +117,24 @@ export const SCAN_RULES = [
|
|
|
117
117
|
"so a run tomorrow uses what was tested today.",
|
|
118
118
|
owasp: ["ASI04"],
|
|
119
119
|
},
|
|
120
|
+
{
|
|
121
|
+
id: "supply-chain/known-vulnerability",
|
|
122
|
+
category: "supply-chain",
|
|
123
|
+
level: "warn",
|
|
124
|
+
title: "A pinned runtime package with known vulnerabilities",
|
|
125
|
+
help: "OSV.dev lists vulnerabilities for this exact version. Upgrade to a fixed one. An online check: `foldrun check --to <ws> --online`, " +
|
|
126
|
+
"Checks → Check online, and the daily rescan.",
|
|
127
|
+
owasp: ["ASI04"],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "content/mcp-tool-text",
|
|
131
|
+
category: "content",
|
|
132
|
+
level: "warn",
|
|
133
|
+
title: "An MCP server's tool text carries hidden or injected instructions",
|
|
134
|
+
help: "What a remote MCP server says about its tools reaches the model as instructions. Hidden characters or an order in a tool " +
|
|
135
|
+
"description is a server steering your agent. An online check, for servers reached by URL; a command server is never run to check it.",
|
|
136
|
+
owasp: ["ASI01", "ASI04"],
|
|
137
|
+
},
|
|
120
138
|
{
|
|
121
139
|
id: "config/unknown-field",
|
|
122
140
|
category: "config",
|
|
@@ -141,6 +159,32 @@ export const SCAN_RULES = [
|
|
|
141
159
|
help: "`effort: extreme`, `size: giant`, `trigger: sometimes`, a cron the scheduler cannot read: each is ignored or falls back, " +
|
|
142
160
|
"so the agent or flow quietly does something else. The message lists what the field takes.",
|
|
143
161
|
},
|
|
162
|
+
{
|
|
163
|
+
id: "config/conflict",
|
|
164
|
+
category: "config",
|
|
165
|
+
level: "error",
|
|
166
|
+
title: "A field set twice: at the top and inside its job block",
|
|
167
|
+
help: "`limits:` at the top and `limits:` inside `guardrail:` cannot both be meant. The top one would be read and the other " +
|
|
168
|
+
"quietly ignored, so the file is refused until one is removed.",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "config/misplaced",
|
|
172
|
+
category: "config",
|
|
173
|
+
level: "error",
|
|
174
|
+
title: "A field inside the wrong job block",
|
|
175
|
+
help: "Each of the four blocks (governance, guardrail, observability, evaluation) holds its own fields. A setting written in " +
|
|
176
|
+
"another block is not read — a guardrail under governance: guards nothing — so the file is refused until it is moved " +
|
|
177
|
+
"to the block the message names, or to the top. A word foldrun does not read at all is only a note.",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "config/invalid-frontmatter",
|
|
181
|
+
category: "config",
|
|
182
|
+
level: "warn",
|
|
183
|
+
title: "Frontmatter that YAML cannot read",
|
|
184
|
+
help: "A value with a colon followed by a space (`description: the format (Markdown, ::: directives)`) ends the YAML. " +
|
|
185
|
+
"foldrun reads a skill like that by quoting the value, but the dashboard editor and the source API refuse to save it, " +
|
|
186
|
+
"and other tools that read the file may not. Quote the value: `description: \"…\"`.",
|
|
187
|
+
},
|
|
144
188
|
{
|
|
145
189
|
id: "config/missing-field",
|
|
146
190
|
category: "config",
|
|
@@ -399,13 +443,15 @@ const INJECTION = [
|
|
|
399
443
|
/\b(?:send|post|upload|exfiltrate)\s+(?:all\s+|the\s+|every\s+)?(?:secrets?|credentials|api\s+keys?|tokens?|env(?:ironment)?\s+variables)\s+to\b/i,
|
|
400
444
|
];
|
|
401
445
|
const BLOB = /[A-Za-z0-9+/]{300,}={0,2}/g;
|
|
402
|
-
function scanContent(file, lines, push) {
|
|
403
|
-
|
|
404
|
-
|
|
446
|
+
function scanContent(file, lines, push, untrusted = false) {
|
|
447
|
+
// Untrusted text (a remote server's) is read as data, every line: it
|
|
448
|
+
// cannot fence an order off as an "example" either.
|
|
449
|
+
const md = untrusted || /\.md$/i.test(file.path);
|
|
450
|
+
const data = untrusted || isDataDoc(file.path);
|
|
405
451
|
let fenced = false;
|
|
406
452
|
for (let i = 0; i < lines.length; i++) {
|
|
407
453
|
const line = i === 0 ? lines[i].replace(/^/, "") : lines[i];
|
|
408
|
-
if (/^\s*(?:```|~~~)/.test(line))
|
|
454
|
+
if (!untrusted && /^\s*(?:```|~~~)/.test(line))
|
|
409
455
|
fenced = !fenced;
|
|
410
456
|
HIDDEN.lastIndex = 0;
|
|
411
457
|
if (HIDDEN.test(line)) {
|
|
@@ -431,6 +477,17 @@ function scanContent(file, lines, push) {
|
|
|
431
477
|
}
|
|
432
478
|
}
|
|
433
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* Text from outside the workspace — what a remote MCP server says about its
|
|
482
|
+
* tools — through the content rules with nothing the text itself can turn
|
|
483
|
+
* off: no foldrun-ignore, no binary or vendored-path skip, no fences
|
|
484
|
+
* (FOL-58, Codex review: each of those let a server silence its own finding).
|
|
485
|
+
*/
|
|
486
|
+
export function untrustedTextFindings(text) {
|
|
487
|
+
const out = [];
|
|
488
|
+
scanContent({ path: "untrusted", content: text }, text.split(/\r?\n/), (rule, _file, line, _text, message) => out.push({ rule, line: line ?? 0, message }), true);
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
434
491
|
/** The text hidden in tag characters: each is U+E0000 + an ASCII code. */
|
|
435
492
|
function decodeTags(s) {
|
|
436
493
|
let out = "";
|
|
@@ -513,17 +570,35 @@ export function nearestField(key, known) {
|
|
|
513
570
|
return bestD <= allowed ? best : null;
|
|
514
571
|
}
|
|
515
572
|
function keyLine(lines, key) {
|
|
516
|
-
const
|
|
517
|
-
const
|
|
518
|
-
|
|
573
|
+
const k = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
574
|
+
const top = new RegExp(`^${k}\\s*:`);
|
|
575
|
+
const i = lines.findIndex((l) => top.test(l));
|
|
576
|
+
if (i >= 0)
|
|
577
|
+
return i + 1;
|
|
578
|
+
// A key inside one of the four job blocks: indented, under its header.
|
|
579
|
+
const inner = new RegExp(`^\\s+${k}\\s*:`);
|
|
580
|
+
let inBlock = false;
|
|
581
|
+
for (let n = 0; n < lines.length; n++) {
|
|
582
|
+
const l = lines[n];
|
|
583
|
+
if (/^(governance|guardrail|observability|evaluation)\s*:\s*(#.*)?$/.test(l))
|
|
584
|
+
inBlock = true;
|
|
585
|
+
else if (/^\S/.test(l))
|
|
586
|
+
inBlock = false;
|
|
587
|
+
else if (inBlock && inner.test(l))
|
|
588
|
+
return n + 1;
|
|
589
|
+
}
|
|
590
|
+
return undefined;
|
|
519
591
|
}
|
|
520
592
|
/** Read-only: gray-matter caches by content, so two files with the same
|
|
521
593
|
* text share one object, and writing to it would leak between them. */
|
|
522
|
-
function frontOf(content) {
|
|
594
|
+
function frontOf(content, filePath) {
|
|
523
595
|
if (!/^?---\r?\n/.test(content))
|
|
524
596
|
return null;
|
|
525
597
|
try {
|
|
526
|
-
|
|
598
|
+
// An agent, a flow or AGENTS.md reads its job blocks as flat keys, as
|
|
599
|
+
// the runtime does; every other document is read as written.
|
|
600
|
+
const kind = filePath ? schemaKindOf(filePath) : null;
|
|
601
|
+
return (kind === "agent" || kind === "flow" || kind === "agents-md" ? configMatter(content) : matter(content)).data;
|
|
527
602
|
}
|
|
528
603
|
catch {
|
|
529
604
|
return null; // a frontmatter YAML cannot read is check's finding, not ours
|
|
@@ -652,14 +727,25 @@ const VENDORED = /(?:^|\/)(?:node_modules|\.venv|venv|site-packages|__pycache__|
|
|
|
652
727
|
* since a key in a log here is a key in the workspace (seen live on
|
|
653
728
|
* 17 Oct 2026: zero-width characters in a CLI MCP log, reported as hidden
|
|
654
729
|
* text). */
|
|
730
|
+
/** The definition files a deploy refuses outright when their frontmatter does
|
|
731
|
+
* not parse (deploy.ts deployIssues): the workspace's own, by these paths.
|
|
732
|
+
* Everything else the platform reads frontmatter from — skills, and the
|
|
733
|
+
* account library's agents, tools and hooks — is the scanner's to report. */
|
|
734
|
+
export const STRICT_FRONTMATTER_PATH = /^(AGENTS\.md|agents\/[^/]+\/agent\.md|flows\/[^/]+\.md|tools\/[^/]+\.md|tools\/[^/]+\/tool\.md|evals\/[^/]+\.md|hooks\/[^/]+\/hook\.md|\.claude\/agents\/[^/]+\.md)$/;
|
|
735
|
+
/** A skill file in either layout the runtime discovers: <name>/SKILL.md, or
|
|
736
|
+
* the flat <name>.md, under skills/ or .agents/skills/, in the library too. */
|
|
737
|
+
const SKILL_FILE = /(?:^|\/)skills\/(?:[^/]+\/SKILL\.md|[^/]+\.md)$/;
|
|
655
738
|
const RUNTIME_WRITTEN = /(?:^|\/)(?:\.cache|\.npm|\.local|\.pnpm-store|\.yarn|\.foldrun)\//;
|
|
656
739
|
/** Text files only — a PNG read as UTF-8 is noise to every rule here. */
|
|
657
740
|
function isText(f) {
|
|
658
741
|
return !VENDORED.test(f.path) && !f.content.slice(0, 8000).includes("\u0000") && !/\.(?:png|jpe?g|gif|webp|ico|pdf|zip|gz|woff2?|ttf|mp[34]|mov|wasm)$/i.test(f.path);
|
|
659
742
|
}
|
|
743
|
+
/** How many times one rule is listed for one file before the rest are counted. */
|
|
744
|
+
export const PER_FILE_RULE = 100;
|
|
660
745
|
/**
|
|
661
746
|
* Every finding in these files, ignores applied. Findings are sorted by file
|
|
662
|
-
* then line, and one rule says one thing once per line
|
|
747
|
+
* then line, and one rule says one thing once per line, and at most
|
|
748
|
+
* PER_FILE_RULE times in one file.
|
|
663
749
|
*/
|
|
664
750
|
export function scanFiles(files, options = {}) {
|
|
665
751
|
const want = options.only?.length ? new Set(options.only) : null;
|
|
@@ -667,6 +753,11 @@ export function scanFiles(files, options = {}) {
|
|
|
667
753
|
const findings = [];
|
|
668
754
|
const seen = new Set();
|
|
669
755
|
const ignoresByFile = new Map();
|
|
756
|
+
// One rule in one file says it PER_FILE_RULE times, then counts the rest:
|
|
757
|
+
// a million lines of hidden characters is one problem, not a million
|
|
758
|
+
// findings held in memory (Codex review of the 20 Oct rescan outage).
|
|
759
|
+
const perFileRule = new Map();
|
|
760
|
+
const more = new Map();
|
|
670
761
|
const push = (rule, file, line, text, message, level) => {
|
|
671
762
|
if (!enabled(rule))
|
|
672
763
|
return;
|
|
@@ -676,6 +767,16 @@ export function scanFiles(files, options = {}) {
|
|
|
676
767
|
const key = `${rule}\0${file}\0${line ?? ""}\0${message}`;
|
|
677
768
|
if (seen.has(key))
|
|
678
769
|
return;
|
|
770
|
+
const rf = `${rule}\0${file}`;
|
|
771
|
+
const said = perFileRule.get(rf) ?? 0;
|
|
772
|
+
if (said >= PER_FILE_RULE) {
|
|
773
|
+
const m = more.get(rf) ?? { rule, file, level, n: 0, hash: crypto.createHash("sha256") };
|
|
774
|
+
m.n++;
|
|
775
|
+
m.hash.update(`${findingFingerprint(rule, file, text)}\0`);
|
|
776
|
+
more.set(rf, m);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
perFileRule.set(rf, said + 1);
|
|
679
780
|
seen.add(key);
|
|
680
781
|
findings.push({ rule, level: level ?? scanRule(rule)?.level ?? "warn", file, ...(line ? { line } : {}), message, fingerprint: findingFingerprint(rule, file, text) });
|
|
681
782
|
};
|
|
@@ -691,9 +792,35 @@ export function scanFiles(files, options = {}) {
|
|
|
691
792
|
scanContent(f, lines, push);
|
|
692
793
|
scanSupplyChain(f, lines, push);
|
|
693
794
|
const schema = schemaKindOf(f.path);
|
|
795
|
+
// Frontmatter YAML cannot read. Until 2026-10-21 nothing reported it for
|
|
796
|
+
// a skill or a library file: frontOf below reads it as "no frontmatter"
|
|
797
|
+
// and moves on, and the deploy's own refusal covers only the workspace's
|
|
798
|
+
// definition files. A vaza.ai skill with `description: … (Markdown,
|
|
799
|
+
// frontmatter, ::: directives) …` deployed clean, ran (the runtime quotes
|
|
800
|
+
// the value and retries), and was refused by the source API on its next
|
|
801
|
+
// save.
|
|
802
|
+
if ((schema || SKILL_FILE.test(f.path)) && !STRICT_FRONTMATTER_PATH.test(f.path) && /^\uFEFF?---\r?\n/.test(f.content)) {
|
|
803
|
+
let err = null;
|
|
804
|
+
try {
|
|
805
|
+
matter(f.content);
|
|
806
|
+
}
|
|
807
|
+
catch (e) {
|
|
808
|
+
err = e;
|
|
809
|
+
}
|
|
810
|
+
if (err) {
|
|
811
|
+
const isSkill = schema === "skill" || SKILL_FILE.test(f.path);
|
|
812
|
+
const read = parseLenient(f.content);
|
|
813
|
+
const what = isSkill && !read.failed
|
|
814
|
+
? "foldrun reads this skill anyway by quoting the value, but the dashboard editor and the source API refuse to save it, and other skills clients may not load it"
|
|
815
|
+
: isSkill
|
|
816
|
+
? "the runtime reads it as empty, so the skill has no name or description and is never loaded"
|
|
817
|
+
: "the runtime cannot read this file";
|
|
818
|
+
push("config/invalid-frontmatter", f.path, frontmatterErrorLine(err), `config/invalid-frontmatter ${frontmatterErrorText(err)}`, `${frontmatterErrorText(err)} ${what === "the runtime cannot read this file" ? "The runtime cannot read this file" : what.replace(/^foldrun/, "foldrun").replace(/^the/, "The")}.`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
694
821
|
// A skill with no frontmatter at all is the clearest one that never
|
|
695
822
|
// loads: read it as empty, so its required fields are reported.
|
|
696
|
-
const data = schema || docKind(f.path) ? (frontOf(f.content) ?? (schema === "skill" && !/^\uFEFF?---\r?\n/.test(f.content) ? {} : null)) : null;
|
|
823
|
+
const data = schema || docKind(f.path) ? (frontOf(f.content, f.path) ?? (schema === "skill" && !/^\uFEFF?---\r?\n/.test(f.content) ? {} : null)) : null;
|
|
697
824
|
if (!data)
|
|
698
825
|
continue;
|
|
699
826
|
if (schema) {
|
|
@@ -719,8 +846,12 @@ export function scanFiles(files, options = {}) {
|
|
|
719
846
|
toolDefs.set(f.path, { data, lines });
|
|
720
847
|
}
|
|
721
848
|
}
|
|
722
|
-
|
|
723
|
-
|
|
849
|
+
// project.md is the older name the runtime still reads (Codex review, FOL-58).
|
|
850
|
+
const workspaceMd = files.find((f) => f.path === "AGENTS.md") ?? files.find((f) => f.path === "project.md");
|
|
851
|
+
scanAgents(agents, outward, toolDefs, push, { account: options.accountNetwork, workspace: workspaceMd ? frontOf(workspaceMd.content, "AGENTS.md")?.network : undefined });
|
|
852
|
+
for (const m of more.values()) {
|
|
853
|
+
findings.push({ rule: m.rule, level: m.level ?? scanRule(m.rule)?.level ?? "warn", file: m.file, message: `and ${m.n} more like this in this file, not listed`, fingerprint: m.hash.digest("hex").slice(0, 16), truncated: true });
|
|
854
|
+
}
|
|
724
855
|
return findings.sort((a, b) => a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0) || a.rule.localeCompare(b.rule));
|
|
725
856
|
}
|
|
726
857
|
/**
|
package/dist/src/starter.js
CHANGED
|
@@ -55,6 +55,22 @@ foldrun_version: "0.1"
|
|
|
55
55
|
#
|
|
56
56
|
# The token is sent as a bearer credential. A gateway that wants it as a key
|
|
57
57
|
# header asks for that by name: headers: { x-api-key: \${THE_SECRET} }.
|
|
58
|
+
#
|
|
59
|
+
# The four jobs of running agents, each a block — or write the same keys at
|
|
60
|
+
# the top; one place or the other, never both:
|
|
61
|
+
#
|
|
62
|
+
# governance: # who answers for it, what it may spend and ship
|
|
63
|
+
# owner: you@example.com
|
|
64
|
+
# budget: 60/month
|
|
65
|
+
# deploy_block: secrets
|
|
66
|
+
# guardrail: # what agents may reach and call
|
|
67
|
+
# limits: {calls: 300, repeat: 5}
|
|
68
|
+
# network: trusted
|
|
69
|
+
# observability: # what is recorded, and who is told
|
|
70
|
+
# notify: {email: you@example.com, events: [failed]}
|
|
71
|
+
# alerts: {failure_rate: 30%}
|
|
72
|
+
# evaluation: # how work is judged
|
|
73
|
+
# judge: {model: fast}
|
|
58
74
|
---
|
|
59
75
|
|
|
60
76
|
# ${account}
|
|
@@ -357,7 +373,7 @@ All of it, \`/api/workspaces/<workspace>\` unless noted:
|
|
|
357
373
|
| \`/assistant\` | POST | ask the assistant, or have it write agents, flows, tools and skills — from the docs only; proposals come back, nothing is written |
|
|
358
374
|
|
|
359
375
|
Account-level, outside \`/workspaces\`: \`/api/secrets\`, \`/api/keys\`, \`/api/schedule\`,
|
|
360
|
-
\`/api/approvals\`, \`/api/usage\`, \`/api/library/<kind>\`, \`/api/team\`, \`/api/billing\`,
|
|
376
|
+
\`/api/approvals\`, \`/api/issues\` (what went wrong, open until resolved), \`/api/usage\`, \`/api/library/<kind>\`, \`/api/team\`, \`/api/billing\`,
|
|
361
377
|
\`/api/me/notifications\` (what mail you get; a key acts for whoever minted it),
|
|
362
378
|
\`/api/account/backups\` (snapshots, restore requests), \`/api/me/onboarding\` (getting started),
|
|
363
379
|
\`/api/healthz\` and \`/api/version\` (open), \`/api/changelog\` (release notes)
|
|
@@ -391,6 +407,7 @@ subflow), \`foldrun flow rm-step\` removes a step, and \`foldrun agent link
|
|
|
391
407
|
trigger: manual # manual | schedule | webhook
|
|
392
408
|
schedule: "0 9 * * 1" # cron, when trigger: schedule
|
|
393
409
|
timezone: Australia/Sydney
|
|
410
|
+
owner: ana@example.com # who answers for it; else the workspace's, else the account's
|
|
394
411
|
model: default # a default for every step here
|
|
395
412
|
effort: low
|
|
396
413
|
overlap: skip # skip | queue — what a second run does while one is live
|
|
@@ -448,10 +465,11 @@ by a model: what this role is for, what it must not do, what good looks like.
|
|
|
448
465
|
---
|
|
449
466
|
name: researcher
|
|
450
467
|
description: Finds and summarises sources.
|
|
468
|
+
owner: ana@example.com # who answers for it; else the workspace's, else the account's
|
|
451
469
|
model: default # fast | default | max
|
|
452
470
|
effort: low # how hard to think about it
|
|
453
471
|
size: large # small | large | heavy — the sandbox it rents
|
|
454
|
-
tools: [web, read, ask, my-folder-tool] # built-ins and your own tools/, one list; \`ask\` lets it ask you mid-step
|
|
472
|
+
tools: [web, read, ask, peers, my-folder-tool] # built-ins and your own tools/, one list; \`ask\` lets it ask you mid-step, \`peers\` lets it message the steps running beside it
|
|
455
473
|
disallowedTools: [Bash] # subtract from what it would otherwise have
|
|
456
474
|
skills: [house-style] # from skills/
|
|
457
475
|
hooks: [no-force-push] # from hooks/ or the library: code at every tool call
|
|
@@ -474,6 +492,13 @@ runtime: # only if scripts need packages
|
|
|
474
492
|
You research a topic and report what you found...
|
|
475
493
|
\`\`\`
|
|
476
494
|
|
|
495
|
+
The fields that do one of the four jobs of running agents may be grouped
|
|
496
|
+
under it — \`governance:\` (owner, budget), \`guardrail:\` (limits, network,
|
|
497
|
+
hooks, disallowedTools, permissionMode, secrets), and in AGENTS.md also
|
|
498
|
+
\`observability:\` (alerts, telemetry, notify, capture) and \`evaluation:\`
|
|
499
|
+
(judge). A block reads exactly as the same keys at the top; a key written
|
|
500
|
+
both ways is refused (\`config/conflict\`). \`foldrun docs four-jobs\`.
|
|
501
|
+
|
|
477
502
|
Only \`name\` and the prose are required. Every other field widens or narrows
|
|
478
503
|
what the agent can reach, and the default is narrow.
|
|
479
504
|
|
package/dist/src/step-exec.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type HookDef, type HookRecord } from "./hooks.ts";
|
|
2
2
|
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
-
import type
|
|
3
|
+
import { type Effort, type ModelCall } from "./store.ts";
|
|
4
4
|
import { type TokenPrice } from "./catalog.ts";
|
|
5
5
|
import type { TestEffect } from "./test-mode.ts";
|
|
6
6
|
import type { OperatorEvent } from "./operator.ts";
|
|
@@ -57,6 +57,11 @@ export interface ExecOutcome {
|
|
|
57
57
|
browserPod?: BrowserPodOutcome;
|
|
58
58
|
}
|
|
59
59
|
export interface ExecOptions {
|
|
60
|
+
/** What the step reports beyond its events (FOL-54 step 4): `metadata`
|
|
61
|
+
* sends each model call (`turn`); `full` also sends each tool call's input
|
|
62
|
+
* and output (`io`). Unset sends neither — the consult, the probe and any
|
|
63
|
+
* caller that does not store them. */
|
|
64
|
+
capture?: "metadata" | "full";
|
|
60
65
|
/** `hooks:` resolved host-side (hooks.ts): run at this step's own dispatch,
|
|
61
66
|
* inside its sandbox. `hookBase` is what every hook event carries. */
|
|
62
67
|
hooks?: HookDef[];
|
|
@@ -99,6 +104,11 @@ export interface ExecOptions {
|
|
|
99
104
|
* `matches: re`, `file: path`, `judge: sentence`) — see checkVerify. */
|
|
100
105
|
verify?: string;
|
|
101
106
|
verifyEnv?: Record<string, string>;
|
|
107
|
+
/** The model `verify: judge:` grades with — the workspace's `judge:` from
|
|
108
|
+
* AGENTS.md, resolved; absent, the fast tier. */
|
|
109
|
+
judgeModel?: string;
|
|
110
|
+
/** And how long it thinks — `judge: {effort}`; absent, the SDK's default. */
|
|
111
|
+
judgeEffort?: Effort;
|
|
102
112
|
/** `output: json` — the reply must carry one JSON value; extracting it is
|
|
103
113
|
* part of finishing the step, and failing to is failing the step. */
|
|
104
114
|
output?: "json";
|
|
@@ -175,7 +185,30 @@ export type EventExtra = {
|
|
|
175
185
|
subagent?: string;
|
|
176
186
|
check?: boolean;
|
|
177
187
|
hook?: HookRecord;
|
|
188
|
+
/** A tool call's content, on its way to the runner (FOL-54 step 4): the
|
|
189
|
+
* runner masks it, stores it beside the run and keeps only `captured` on
|
|
190
|
+
* the event. Sent only under `capture: "full"`. */
|
|
191
|
+
io?: CallIo;
|
|
192
|
+
/** One model call, on its way to the runner's `step.calls` — not an event. */
|
|
193
|
+
turn?: ModelCall;
|
|
194
|
+
/** The model's first response of the step has begun streaming — the
|
|
195
|
+
* moment the prompt cache entry its request wrote becomes readable. Sent
|
|
196
|
+
* once; the runner's fan-out gate reads it. Not an event. */
|
|
197
|
+
responding?: true;
|
|
198
|
+
/** Set by the runner once it stored `io` (RunEvent.captured). */
|
|
199
|
+
captured?: boolean;
|
|
200
|
+
/** On a tool call: the model call (its message id) that asked for it. */
|
|
201
|
+
inCall?: string;
|
|
178
202
|
};
|
|
203
|
+
/** A tool call's input (on the call) or output (on its completion), capped. */
|
|
204
|
+
export interface CallIo {
|
|
205
|
+
input?: string;
|
|
206
|
+
output?: string;
|
|
207
|
+
/** The whole input's / output's length, in characters, before any cut. */
|
|
208
|
+
inputChars?: number;
|
|
209
|
+
outputChars?: number;
|
|
210
|
+
truncated?: boolean;
|
|
211
|
+
}
|
|
179
212
|
/** Conservative per-token rates for a model nothing else can price — an
|
|
180
213
|
* unknown id on a gateway with no catalogue. Opus-class, so a ceiling
|
|
181
214
|
* still means something. Only ever reached when `knownPrice` below does
|
|
@@ -273,14 +306,17 @@ export declare function checkVerify(agentDir: string, verify: string, ctx: {
|
|
|
273
306
|
/** The step's own model environment, for `judge:` — it grades on the
|
|
274
307
|
* fast tier through the same credential the step rode. */
|
|
275
308
|
modelEnv?: Record<string, string | undefined>;
|
|
309
|
+
/** The judge's model and effort (the workspace's `judge:`). */
|
|
310
|
+
judgeModel?: string;
|
|
311
|
+
judgeEffort?: Effort;
|
|
276
312
|
/** Ends a shell check or a judge mid-way: the step's clock ran out, or a
|
|
277
313
|
* person asked the run to stop. */
|
|
278
314
|
signal?: AbortSignal;
|
|
279
315
|
}): Promise<VerifyVerdict>;
|
|
280
316
|
/**
|
|
281
317
|
* A toolless, fast-tier grading call: does the reply satisfy the sentence?
|
|
282
|
-
* Same shape as the eval judge
|
|
283
|
-
*
|
|
318
|
+
* Same shape as the eval judge: reasoning first, then the verdict alone on
|
|
319
|
+
* the last line (judge-verdict.ts says why).
|
|
284
320
|
*/
|
|
285
321
|
export declare function judgePrompt(rubric: string, reply: string, work: string): string;
|
|
286
322
|
/**
|