@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,88 @@
|
|
|
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
|
+
|
|
6
|
+
export interface RunFilter {
|
|
7
|
+
/** A step called a tool whose name contains this (`web`, `crm_export`). */
|
|
8
|
+
tool?: string;
|
|
9
|
+
/** A tool call errored. */
|
|
10
|
+
toolError?: boolean;
|
|
11
|
+
/** A step was this agent. */
|
|
12
|
+
agent?: string;
|
|
13
|
+
/** A step ran a model whose name contains this (`haiku`, `opus`). */
|
|
14
|
+
model?: string;
|
|
15
|
+
/** The run read at least this many input tokens, all steps together. */
|
|
16
|
+
minTokens?: number;
|
|
17
|
+
/** The run took at least this many seconds. */
|
|
18
|
+
minSecs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The query parameters a filter comes from: tool, tool_error, agent,
|
|
22
|
+
* model, min_tokens, min_secs. Unreadable numbers are refused in words. */
|
|
23
|
+
export function parseRunFilter(q: { get(name: string): string | null }): { filter: RunFilter; problems: string[] } {
|
|
24
|
+
const filter: RunFilter = {};
|
|
25
|
+
const problems: string[] = [];
|
|
26
|
+
const text = (k: string) => q.get(k)?.trim() || undefined;
|
|
27
|
+
const tool = text("tool");
|
|
28
|
+
if (tool) filter.tool = tool.toLowerCase();
|
|
29
|
+
const te = text("tool_error");
|
|
30
|
+
if (te) filter.toolError = te === "1" || te === "true" || te === "yes";
|
|
31
|
+
const agent = text("agent");
|
|
32
|
+
if (agent) filter.agent = agent;
|
|
33
|
+
const model = text("model");
|
|
34
|
+
if (model) filter.model = model.toLowerCase();
|
|
35
|
+
for (const [k, field] of [["min_tokens", "minTokens"], ["min_secs", "minSecs"]] as const) {
|
|
36
|
+
const v = text(k);
|
|
37
|
+
if (v === undefined) continue;
|
|
38
|
+
// "50k" and "1.5k" are thousands (Codex review: "1.5k" read as 1.5).
|
|
39
|
+
const kilo = /^(\d+(?:\.\d+)?)k$/i.exec(v);
|
|
40
|
+
const n = kilo ? Number(kilo[1]) * 1000 : Number(v);
|
|
41
|
+
if (!Number.isFinite(n) || n < 0) problems.push(`${k} is a number (${k === "min_tokens" ? "50000 or 50k" : "seconds"}), not ${v}`);
|
|
42
|
+
else filter[field] = n;
|
|
43
|
+
}
|
|
44
|
+
return { filter, problems };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const isEmptyFilter = (f: RunFilter) => Object.keys(f).length === 0;
|
|
48
|
+
|
|
49
|
+
type FilterableStep = {
|
|
50
|
+
agent: string;
|
|
51
|
+
model?: string;
|
|
52
|
+
tools?: string[];
|
|
53
|
+
toolErrors?: number;
|
|
54
|
+
/** A whole record has events instead of the index's tools/toolErrors. */
|
|
55
|
+
events?: { type: string; text: string; ms?: number; err?: boolean }[];
|
|
56
|
+
tokens?: { input?: number } | null;
|
|
57
|
+
startedAt?: string;
|
|
58
|
+
finishedAt?: string;
|
|
59
|
+
};
|
|
60
|
+
type FilterableRun = { startedAt: string; finishedAt?: string | null; steps: FilterableStep[] };
|
|
61
|
+
|
|
62
|
+
/** The index's facts, or worked out from a whole record's events (Codex
|
|
63
|
+
* review: the workspace runs page reads whole records). */
|
|
64
|
+
function factsOf(s: FilterableStep): { tools: string[]; toolErrors: number } {
|
|
65
|
+
if (s.tools !== undefined || s.toolErrors !== undefined || !s.events) return { tools: s.tools ?? [], toolErrors: s.toolErrors ?? 0 };
|
|
66
|
+
const tools = new Set<string>();
|
|
67
|
+
let errs = 0;
|
|
68
|
+
for (const e of s.events) {
|
|
69
|
+
if (e.type !== "tool") continue;
|
|
70
|
+
if (e.ms == null) tools.add(e.text);
|
|
71
|
+
else if (e.err) errs++;
|
|
72
|
+
}
|
|
73
|
+
return { tools: [...tools], toolErrors: errs };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function matchesRunFilter(run: FilterableRun, f: RunFilter, now = Date.now()): boolean {
|
|
77
|
+
const steps = (run.steps ?? []).map((s) => ({ ...s, ...factsOf(s) }));
|
|
78
|
+
if (f.tool && !steps.some((s) => (s.tools ?? []).some((t) => t.toLowerCase().includes(f.tool!)))) return false;
|
|
79
|
+
if (f.toolError !== undefined && steps.some((s) => (s.toolErrors ?? 0) > 0) !== f.toolError) return false;
|
|
80
|
+
if (f.agent && !steps.some((s) => s.agent === f.agent)) return false;
|
|
81
|
+
if (f.model && !steps.some((s) => (s.model ?? "").toLowerCase().includes(f.model!))) return false;
|
|
82
|
+
if (f.minTokens !== undefined && steps.reduce((n, s) => n + (s.tokens?.input ?? 0), 0) < f.minTokens) return false;
|
|
83
|
+
if (f.minSecs !== undefined) {
|
|
84
|
+
const secs = ((run.finishedAt ? Date.parse(run.finishedAt) : now) - Date.parse(run.startedAt)) / 1000;
|
|
85
|
+
if (!(secs >= f.minSecs)) return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
package/src/scan.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// reported, because "why is this allowed?" is the question a reviewer asks.
|
|
28
28
|
|
|
29
29
|
import crypto from "node:crypto";
|
|
30
|
-
import matter from "
|
|
30
|
+
import matter, { frontmatterErrorLine, frontmatterErrorText, parseLenient, configMatter } from "./matter.ts";
|
|
31
31
|
import { refNames } from "./refs.ts";
|
|
32
32
|
import { resolveNetwork } from "./network.ts";
|
|
33
33
|
import { frontmatterProblems, schemaKindOf } from "./frontmatter-schema.ts";
|
|
@@ -62,6 +62,10 @@ export interface ScanFinding {
|
|
|
62
62
|
message: string;
|
|
63
63
|
/** Stable across edits elsewhere in the file: rule + file + line text. */
|
|
64
64
|
fingerprint: string;
|
|
65
|
+
/** This rule had more to say in this file than PER_FILE_RULE: this
|
|
66
|
+
* finding counts the rest, so what is not listed is not resolved. Its
|
|
67
|
+
* fingerprint is of everything it counts, so a change among them is new. */
|
|
68
|
+
truncated?: true;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
export interface ScanOptions {
|
|
@@ -173,6 +177,26 @@ export const SCAN_RULES: readonly ScanRule[] = [
|
|
|
173
177
|
"so a run tomorrow uses what was tested today.",
|
|
174
178
|
owasp: ["ASI04"],
|
|
175
179
|
},
|
|
180
|
+
{
|
|
181
|
+
id: "supply-chain/known-vulnerability",
|
|
182
|
+
category: "supply-chain",
|
|
183
|
+
level: "warn",
|
|
184
|
+
title: "A pinned runtime package with known vulnerabilities",
|
|
185
|
+
help:
|
|
186
|
+
"OSV.dev lists vulnerabilities for this exact version. Upgrade to a fixed one. An online check: `foldrun check --to <ws> --online`, " +
|
|
187
|
+
"Checks → Check online, and the daily rescan.",
|
|
188
|
+
owasp: ["ASI04"],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "content/mcp-tool-text",
|
|
192
|
+
category: "content",
|
|
193
|
+
level: "warn",
|
|
194
|
+
title: "An MCP server's tool text carries hidden or injected instructions",
|
|
195
|
+
help:
|
|
196
|
+
"What a remote MCP server says about its tools reaches the model as instructions. Hidden characters or an order in a tool " +
|
|
197
|
+
"description is a server steering your agent. An online check, for servers reached by URL; a command server is never run to check it.",
|
|
198
|
+
owasp: ["ASI01", "ASI04"],
|
|
199
|
+
},
|
|
176
200
|
{
|
|
177
201
|
id: "config/unknown-field",
|
|
178
202
|
category: "config",
|
|
@@ -199,6 +223,35 @@ export const SCAN_RULES: readonly ScanRule[] = [
|
|
|
199
223
|
"`effort: extreme`, `size: giant`, `trigger: sometimes`, a cron the scheduler cannot read: each is ignored or falls back, " +
|
|
200
224
|
"so the agent or flow quietly does something else. The message lists what the field takes.",
|
|
201
225
|
},
|
|
226
|
+
{
|
|
227
|
+
id: "config/conflict",
|
|
228
|
+
category: "config",
|
|
229
|
+
level: "error",
|
|
230
|
+
title: "A field set twice: at the top and inside its job block",
|
|
231
|
+
help:
|
|
232
|
+
"`limits:` at the top and `limits:` inside `guardrail:` cannot both be meant. The top one would be read and the other " +
|
|
233
|
+
"quietly ignored, so the file is refused until one is removed.",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "config/misplaced",
|
|
237
|
+
category: "config",
|
|
238
|
+
level: "error",
|
|
239
|
+
title: "A field inside the wrong job block",
|
|
240
|
+
help:
|
|
241
|
+
"Each of the four blocks (governance, guardrail, observability, evaluation) holds its own fields. A setting written in " +
|
|
242
|
+
"another block is not read — a guardrail under governance: guards nothing — so the file is refused until it is moved " +
|
|
243
|
+
"to the block the message names, or to the top. A word foldrun does not read at all is only a note.",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "config/invalid-frontmatter",
|
|
247
|
+
category: "config",
|
|
248
|
+
level: "warn",
|
|
249
|
+
title: "Frontmatter that YAML cannot read",
|
|
250
|
+
help:
|
|
251
|
+
"A value with a colon followed by a space (`description: the format (Markdown, ::: directives)`) ends the YAML. " +
|
|
252
|
+
"foldrun reads a skill like that by quoting the value, but the dashboard editor and the source API refuse to save it, " +
|
|
253
|
+
"and other tools that read the file may not. Quote the value: `description: \"…\"`.",
|
|
254
|
+
},
|
|
202
255
|
{
|
|
203
256
|
id: "config/missing-field",
|
|
204
257
|
category: "config",
|
|
@@ -451,13 +504,15 @@ const INJECTION: RegExp[] = [
|
|
|
451
504
|
|
|
452
505
|
const BLOB = /[A-Za-z0-9+/]{300,}={0,2}/g;
|
|
453
506
|
|
|
454
|
-
function scanContent(file: ScanFile, lines: string[], push: Push) {
|
|
455
|
-
|
|
456
|
-
|
|
507
|
+
function scanContent(file: ScanFile, lines: string[], push: Push, untrusted = false) {
|
|
508
|
+
// Untrusted text (a remote server's) is read as data, every line: it
|
|
509
|
+
// cannot fence an order off as an "example" either.
|
|
510
|
+
const md = untrusted || /\.md$/i.test(file.path);
|
|
511
|
+
const data = untrusted || isDataDoc(file.path);
|
|
457
512
|
let fenced = false;
|
|
458
513
|
for (let i = 0; i < lines.length; i++) {
|
|
459
514
|
const line = i === 0 ? lines[i]!.replace(/^/, "") : lines[i]!;
|
|
460
|
-
if (/^\s*(?:```|~~~)/.test(line)) fenced = !fenced;
|
|
515
|
+
if (!untrusted && /^\s*(?:```|~~~)/.test(line)) fenced = !fenced;
|
|
461
516
|
HIDDEN.lastIndex = 0;
|
|
462
517
|
if (HIDDEN.test(line)) {
|
|
463
518
|
const kind = HIDDEN_NAMES.find(([re]) => re.test(line))?.[1] ?? "invisible characters";
|
|
@@ -481,6 +536,18 @@ function scanContent(file: ScanFile, lines: string[], push: Push) {
|
|
|
481
536
|
}
|
|
482
537
|
}
|
|
483
538
|
|
|
539
|
+
/**
|
|
540
|
+
* Text from outside the workspace — what a remote MCP server says about its
|
|
541
|
+
* tools — through the content rules with nothing the text itself can turn
|
|
542
|
+
* off: no foldrun-ignore, no binary or vendored-path skip, no fences
|
|
543
|
+
* (FOL-58, Codex review: each of those let a server silence its own finding).
|
|
544
|
+
*/
|
|
545
|
+
export function untrustedTextFindings(text: string): { rule: string; line: number; message: string }[] {
|
|
546
|
+
const out: { rule: string; line: number; message: string }[] = [];
|
|
547
|
+
scanContent({ path: "untrusted", content: text }, text.split(/\r?\n/), (rule, _file, line, _text, message) => out.push({ rule, line: line ?? 0, message }), true);
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
|
|
484
551
|
/** The text hidden in tag characters: each is U+E0000 + an ASCII code. */
|
|
485
552
|
function decodeTags(s: string): string {
|
|
486
553
|
let out = "";
|
|
@@ -563,17 +630,31 @@ export function nearestField(key: string, known: readonly string[]): string | nu
|
|
|
563
630
|
}
|
|
564
631
|
|
|
565
632
|
function keyLine(lines: string[], key: string): number | undefined {
|
|
566
|
-
const
|
|
567
|
-
const
|
|
568
|
-
|
|
633
|
+
const k = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
634
|
+
const top = new RegExp(`^${k}\\s*:`);
|
|
635
|
+
const i = lines.findIndex((l) => top.test(l));
|
|
636
|
+
if (i >= 0) return i + 1;
|
|
637
|
+
// A key inside one of the four job blocks: indented, under its header.
|
|
638
|
+
const inner = new RegExp(`^\\s+${k}\\s*:`);
|
|
639
|
+
let inBlock = false;
|
|
640
|
+
for (let n = 0; n < lines.length; n++) {
|
|
641
|
+
const l = lines[n]!;
|
|
642
|
+
if (/^(governance|guardrail|observability|evaluation)\s*:\s*(#.*)?$/.test(l)) inBlock = true;
|
|
643
|
+
else if (/^\S/.test(l)) inBlock = false;
|
|
644
|
+
else if (inBlock && inner.test(l)) return n + 1;
|
|
645
|
+
}
|
|
646
|
+
return undefined;
|
|
569
647
|
}
|
|
570
648
|
|
|
571
649
|
/** Read-only: gray-matter caches by content, so two files with the same
|
|
572
650
|
* text share one object, and writing to it would leak between them. */
|
|
573
|
-
function frontOf(content: string): Record<string, unknown> | null {
|
|
651
|
+
function frontOf(content: string, filePath?: string): Record<string, unknown> | null {
|
|
574
652
|
if (!/^?---\r?\n/.test(content)) return null;
|
|
575
653
|
try {
|
|
576
|
-
|
|
654
|
+
// An agent, a flow or AGENTS.md reads its job blocks as flat keys, as
|
|
655
|
+
// the runtime does; every other document is read as written.
|
|
656
|
+
const kind = filePath ? schemaKindOf(filePath) : null;
|
|
657
|
+
return (kind === "agent" || kind === "flow" || kind === "agents-md" ? configMatter(content) : matter(content)).data as Record<string, unknown>;
|
|
577
658
|
} catch {
|
|
578
659
|
return null; // a frontmatter YAML cannot read is check's finding, not ours
|
|
579
660
|
}
|
|
@@ -735,6 +816,16 @@ const VENDORED = /(?:^|\/)(?:node_modules|\.venv|venv|site-packages|__pycache__|
|
|
|
735
816
|
* since a key in a log here is a key in the workspace (seen live on
|
|
736
817
|
* 17 Oct 2026: zero-width characters in a CLI MCP log, reported as hidden
|
|
737
818
|
* text). */
|
|
819
|
+
/** The definition files a deploy refuses outright when their frontmatter does
|
|
820
|
+
* not parse (deploy.ts deployIssues): the workspace's own, by these paths.
|
|
821
|
+
* Everything else the platform reads frontmatter from — skills, and the
|
|
822
|
+
* account library's agents, tools and hooks — is the scanner's to report. */
|
|
823
|
+
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)$/;
|
|
824
|
+
|
|
825
|
+
/** A skill file in either layout the runtime discovers: <name>/SKILL.md, or
|
|
826
|
+
* the flat <name>.md, under skills/ or .agents/skills/, in the library too. */
|
|
827
|
+
const SKILL_FILE = /(?:^|\/)skills\/(?:[^/]+\/SKILL\.md|[^/]+\.md)$/;
|
|
828
|
+
|
|
738
829
|
const RUNTIME_WRITTEN = /(?:^|\/)(?:\.cache|\.npm|\.local|\.pnpm-store|\.yarn|\.foldrun)\//;
|
|
739
830
|
|
|
740
831
|
/** Text files only — a PNG read as UTF-8 is noise to every rule here. */
|
|
@@ -742,9 +833,13 @@ function isText(f: ScanFile): boolean {
|
|
|
742
833
|
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);
|
|
743
834
|
}
|
|
744
835
|
|
|
836
|
+
/** How many times one rule is listed for one file before the rest are counted. */
|
|
837
|
+
export const PER_FILE_RULE = 100;
|
|
838
|
+
|
|
745
839
|
/**
|
|
746
840
|
* Every finding in these files, ignores applied. Findings are sorted by file
|
|
747
|
-
* then line, and one rule says one thing once per line
|
|
841
|
+
* then line, and one rule says one thing once per line, and at most
|
|
842
|
+
* PER_FILE_RULE times in one file.
|
|
748
843
|
*/
|
|
749
844
|
export function scanFiles(files: ScanFile[], options: ScanOptions = {}): ScanFinding[] {
|
|
750
845
|
const want = options.only?.length ? new Set(options.only) : null;
|
|
@@ -752,6 +847,11 @@ export function scanFiles(files: ScanFile[], options: ScanOptions = {}): ScanFin
|
|
|
752
847
|
const findings: ScanFinding[] = [];
|
|
753
848
|
const seen = new Set<string>();
|
|
754
849
|
const ignoresByFile = new Map<string, Ignores>();
|
|
850
|
+
// One rule in one file says it PER_FILE_RULE times, then counts the rest:
|
|
851
|
+
// a million lines of hidden characters is one problem, not a million
|
|
852
|
+
// findings held in memory (Codex review of the 20 Oct rescan outage).
|
|
853
|
+
const perFileRule = new Map<string, number>();
|
|
854
|
+
const more = new Map<string, { rule: string; file: string; level?: ScanFinding["level"]; n: number; hash: crypto.Hash }>();
|
|
755
855
|
|
|
756
856
|
const push: Push = (rule, file, line, text, message, level) => {
|
|
757
857
|
if (!enabled(rule)) return;
|
|
@@ -759,6 +859,16 @@ export function scanFiles(files: ScanFile[], options: ScanOptions = {}): ScanFin
|
|
|
759
859
|
if (ig && ignored(ig, rule, line)) return;
|
|
760
860
|
const key = `${rule}\0${file}\0${line ?? ""}\0${message}`;
|
|
761
861
|
if (seen.has(key)) return;
|
|
862
|
+
const rf = `${rule}\0${file}`;
|
|
863
|
+
const said = perFileRule.get(rf) ?? 0;
|
|
864
|
+
if (said >= PER_FILE_RULE) {
|
|
865
|
+
const m = more.get(rf) ?? { rule, file, level, n: 0, hash: crypto.createHash("sha256") };
|
|
866
|
+
m.n++;
|
|
867
|
+
m.hash.update(`${findingFingerprint(rule, file, text)}\0`);
|
|
868
|
+
more.set(rf, m);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
perFileRule.set(rf, said + 1);
|
|
762
872
|
seen.add(key);
|
|
763
873
|
findings.push({ rule, level: level ?? scanRule(rule)?.level ?? "warn", file, ...(line ? { line } : {}), message, fingerprint: findingFingerprint(rule, file, text) });
|
|
764
874
|
};
|
|
@@ -776,9 +886,34 @@ export function scanFiles(files: ScanFile[], options: ScanOptions = {}): ScanFin
|
|
|
776
886
|
scanSupplyChain(f, lines, push);
|
|
777
887
|
|
|
778
888
|
const schema = schemaKindOf(f.path);
|
|
889
|
+
// Frontmatter YAML cannot read. Until 2026-10-21 nothing reported it for
|
|
890
|
+
// a skill or a library file: frontOf below reads it as "no frontmatter"
|
|
891
|
+
// and moves on, and the deploy's own refusal covers only the workspace's
|
|
892
|
+
// definition files. A vaza.ai skill with `description: … (Markdown,
|
|
893
|
+
// frontmatter, ::: directives) …` deployed clean, ran (the runtime quotes
|
|
894
|
+
// the value and retries), and was refused by the source API on its next
|
|
895
|
+
// save.
|
|
896
|
+
if ((schema || SKILL_FILE.test(f.path)) && !STRICT_FRONTMATTER_PATH.test(f.path) && /^\uFEFF?---\r?\n/.test(f.content)) {
|
|
897
|
+
let err: unknown = null;
|
|
898
|
+
try {
|
|
899
|
+
matter(f.content);
|
|
900
|
+
} catch (e) {
|
|
901
|
+
err = e;
|
|
902
|
+
}
|
|
903
|
+
if (err) {
|
|
904
|
+
const isSkill = schema === "skill" || SKILL_FILE.test(f.path);
|
|
905
|
+
const read = parseLenient(f.content);
|
|
906
|
+
const what = isSkill && !read.failed
|
|
907
|
+
? "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"
|
|
908
|
+
: isSkill
|
|
909
|
+
? "the runtime reads it as empty, so the skill has no name or description and is never loaded"
|
|
910
|
+
: "the runtime cannot read this file";
|
|
911
|
+
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")}.`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
779
914
|
// A skill with no frontmatter at all is the clearest one that never
|
|
780
915
|
// loads: read it as empty, so its required fields are reported.
|
|
781
|
-
const data = schema || docKind(f.path) ? (frontOf(f.content) ?? (schema === "skill" && !/^\uFEFF?---\r?\n/.test(f.content) ? {} : null)) : null;
|
|
916
|
+
const data = schema || docKind(f.path) ? (frontOf(f.content, f.path) ?? (schema === "skill" && !/^\uFEFF?---\r?\n/.test(f.content) ? {} : null)) : null;
|
|
782
917
|
if (!data) continue;
|
|
783
918
|
if (schema) {
|
|
784
919
|
for (const p of frontmatterProblems(schema, data, nearestField)) {
|
|
@@ -800,8 +935,12 @@ export function scanFiles(files: ScanFile[], options: ScanOptions = {}): ScanFin
|
|
|
800
935
|
toolDefs.set(f.path, { data, lines });
|
|
801
936
|
}
|
|
802
937
|
}
|
|
803
|
-
|
|
804
|
-
|
|
938
|
+
// project.md is the older name the runtime still reads (Codex review, FOL-58).
|
|
939
|
+
const workspaceMd = files.find((f) => f.path === "AGENTS.md") ?? files.find((f) => f.path === "project.md");
|
|
940
|
+
scanAgents(agents, outward, toolDefs, push, { account: options.accountNetwork, workspace: workspaceMd ? frontOf(workspaceMd.content, "AGENTS.md")?.network : undefined });
|
|
941
|
+
for (const m of more.values()) {
|
|
942
|
+
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 });
|
|
943
|
+
}
|
|
805
944
|
|
|
806
945
|
return findings.sort((a, b) => a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0) || a.rule.localeCompare(b.rule));
|
|
807
946
|
}
|
package/src/starter.ts
CHANGED
|
@@ -62,6 +62,22 @@ foldrun_version: "0.1"
|
|
|
62
62
|
#
|
|
63
63
|
# The token is sent as a bearer credential. A gateway that wants it as a key
|
|
64
64
|
# header asks for that by name: headers: { x-api-key: \${THE_SECRET} }.
|
|
65
|
+
#
|
|
66
|
+
# The four jobs of running agents, each a block — or write the same keys at
|
|
67
|
+
# the top; one place or the other, never both:
|
|
68
|
+
#
|
|
69
|
+
# governance: # who answers for it, what it may spend and ship
|
|
70
|
+
# owner: you@example.com
|
|
71
|
+
# budget: 60/month
|
|
72
|
+
# deploy_block: secrets
|
|
73
|
+
# guardrail: # what agents may reach and call
|
|
74
|
+
# limits: {calls: 300, repeat: 5}
|
|
75
|
+
# network: trusted
|
|
76
|
+
# observability: # what is recorded, and who is told
|
|
77
|
+
# notify: {email: you@example.com, events: [failed]}
|
|
78
|
+
# alerts: {failure_rate: 30%}
|
|
79
|
+
# evaluation: # how work is judged
|
|
80
|
+
# judge: {model: fast}
|
|
65
81
|
---
|
|
66
82
|
|
|
67
83
|
# ${account}
|
|
@@ -373,7 +389,7 @@ All of it, \`/api/workspaces/<workspace>\` unless noted:
|
|
|
373
389
|
| \`/assistant\` | POST | ask the assistant, or have it write agents, flows, tools and skills — from the docs only; proposals come back, nothing is written |
|
|
374
390
|
|
|
375
391
|
Account-level, outside \`/workspaces\`: \`/api/secrets\`, \`/api/keys\`, \`/api/schedule\`,
|
|
376
|
-
\`/api/approvals\`, \`/api/usage\`, \`/api/library/<kind>\`, \`/api/team\`, \`/api/billing\`,
|
|
392
|
+
\`/api/approvals\`, \`/api/issues\` (what went wrong, open until resolved), \`/api/usage\`, \`/api/library/<kind>\`, \`/api/team\`, \`/api/billing\`,
|
|
377
393
|
\`/api/me/notifications\` (what mail you get; a key acts for whoever minted it),
|
|
378
394
|
\`/api/account/backups\` (snapshots, restore requests), \`/api/me/onboarding\` (getting started),
|
|
379
395
|
\`/api/healthz\` and \`/api/version\` (open), \`/api/changelog\` (release notes)
|
|
@@ -407,6 +423,7 @@ subflow), \`foldrun flow rm-step\` removes a step, and \`foldrun agent link
|
|
|
407
423
|
trigger: manual # manual | schedule | webhook
|
|
408
424
|
schedule: "0 9 * * 1" # cron, when trigger: schedule
|
|
409
425
|
timezone: Australia/Sydney
|
|
426
|
+
owner: ana@example.com # who answers for it; else the workspace's, else the account's
|
|
410
427
|
model: default # a default for every step here
|
|
411
428
|
effort: low
|
|
412
429
|
overlap: skip # skip | queue — what a second run does while one is live
|
|
@@ -464,10 +481,11 @@ by a model: what this role is for, what it must not do, what good looks like.
|
|
|
464
481
|
---
|
|
465
482
|
name: researcher
|
|
466
483
|
description: Finds and summarises sources.
|
|
484
|
+
owner: ana@example.com # who answers for it; else the workspace's, else the account's
|
|
467
485
|
model: default # fast | default | max
|
|
468
486
|
effort: low # how hard to think about it
|
|
469
487
|
size: large # small | large | heavy — the sandbox it rents
|
|
470
|
-
tools: [web, read, ask, my-folder-tool] # built-ins and your own tools/, one list; \`ask\` lets it ask you mid-step
|
|
488
|
+
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
|
|
471
489
|
disallowedTools: [Bash] # subtract from what it would otherwise have
|
|
472
490
|
skills: [house-style] # from skills/
|
|
473
491
|
hooks: [no-force-push] # from hooks/ or the library: code at every tool call
|
|
@@ -490,6 +508,13 @@ runtime: # only if scripts need packages
|
|
|
490
508
|
You research a topic and report what you found...
|
|
491
509
|
\`\`\`
|
|
492
510
|
|
|
511
|
+
The fields that do one of the four jobs of running agents may be grouped
|
|
512
|
+
under it — \`governance:\` (owner, budget), \`guardrail:\` (limits, network,
|
|
513
|
+
hooks, disallowedTools, permissionMode, secrets), and in AGENTS.md also
|
|
514
|
+
\`observability:\` (alerts, telemetry, notify, capture) and \`evaluation:\`
|
|
515
|
+
(judge). A block reads exactly as the same keys at the top; a key written
|
|
516
|
+
both ways is refused (\`config/conflict\`). \`foldrun docs four-jobs\`.
|
|
517
|
+
|
|
493
518
|
Only \`name\` and the prose are required. Every other field widens or narrows
|
|
494
519
|
what the agent can reach, and the default is narrow.
|
|
495
520
|
|