@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.
Files changed (149) hide show
  1. package/dist/src/agent-activity.d.ts +5 -0
  2. package/dist/src/agent-activity.js +31 -0
  3. package/dist/src/agent-import.js +2 -2
  4. package/dist/src/agent-tools.js +2 -2
  5. package/dist/src/agents-md.d.ts +5 -1
  6. package/dist/src/agents-md.js +42 -18
  7. package/dist/src/alerts.d.ts +60 -0
  8. package/dist/src/alerts.js +161 -0
  9. package/dist/src/browser-pod.js +4 -0
  10. package/dist/src/capture.d.ts +23 -0
  11. package/dist/src/capture.js +80 -0
  12. package/dist/src/compare.d.ts +57 -0
  13. package/dist/src/compare.js +116 -0
  14. package/dist/src/completions.js +26 -3
  15. package/dist/src/deploy.d.ts +36 -1
  16. package/dist/src/deploy.js +237 -13
  17. package/dist/src/egress.d.ts +16 -1
  18. package/dist/src/evals.d.ts +27 -2
  19. package/dist/src/evals.js +62 -19
  20. package/dist/src/flow-lint.d.ts +14 -0
  21. package/dist/src/flow-lint.js +35 -0
  22. package/dist/src/flow-patterns.js +29 -14
  23. package/dist/src/frontmatter-fields.d.ts +27 -0
  24. package/dist/src/frontmatter-fields.js +60 -3
  25. package/dist/src/frontmatter-guard.d.ts +19 -0
  26. package/dist/src/frontmatter-guard.js +67 -0
  27. package/dist/src/frontmatter-schema.d.ts +2 -2
  28. package/dist/src/frontmatter-schema.js +33 -2
  29. package/dist/src/frontmatter-text.d.ts +31 -0
  30. package/dist/src/frontmatter-text.js +102 -0
  31. package/dist/src/history.d.ts +4 -0
  32. package/dist/src/history.js +15 -0
  33. package/dist/src/hooks.js +1 -1
  34. package/dist/src/insights.d.ts +30 -0
  35. package/dist/src/insights.js +152 -0
  36. package/dist/src/issues.d.ts +140 -0
  37. package/dist/src/issues.js +302 -0
  38. package/dist/src/judge-verdict.d.ts +19 -0
  39. package/dist/src/judge-verdict.js +54 -0
  40. package/dist/src/library.js +1 -1
  41. package/dist/src/limits.d.ts +8 -0
  42. package/dist/src/limits.js +36 -5
  43. package/dist/src/linkable.js +1 -1
  44. package/dist/src/matter.d.ts +42 -0
  45. package/dist/src/matter.js +132 -0
  46. package/dist/src/model-credential.d.ts +14 -0
  47. package/dist/src/model-credential.js +40 -0
  48. package/dist/src/network.d.ts +12 -1
  49. package/dist/src/network.js +35 -8
  50. package/dist/src/notify.d.ts +16 -0
  51. package/dist/src/notify.js +17 -7
  52. package/dist/src/okf.js +1 -1
  53. package/dist/src/online-checks.d.ts +76 -0
  54. package/dist/src/online-checks.js +432 -0
  55. package/dist/src/operator.d.ts +277 -0
  56. package/dist/src/operator.js +176 -2
  57. package/dist/src/otel-config.d.ts +18 -0
  58. package/dist/src/otel-config.js +69 -0
  59. package/dist/src/otel.d.ts +38 -0
  60. package/dist/src/otel.js +85 -0
  61. package/dist/src/outcome.d.ts +40 -0
  62. package/dist/src/outcome.js +77 -0
  63. package/dist/src/package.js +9 -2
  64. package/dist/src/platform.d.ts +1 -1
  65. package/dist/src/prompt-layers.d.ts +30 -0
  66. package/dist/src/prompt-layers.js +77 -0
  67. package/dist/src/provider-health.js +2 -2
  68. package/dist/src/providers.d.ts +5 -0
  69. package/dist/src/providers.js +2 -2
  70. package/dist/src/run-container.d.ts +25 -2
  71. package/dist/src/run-container.js +61 -6
  72. package/dist/src/runner.d.ts +53 -2
  73. package/dist/src/runner.js +471 -102
  74. package/dist/src/runs-filter.d.ts +48 -0
  75. package/dist/src/runs-filter.js +73 -0
  76. package/dist/src/scan.d.ts +30 -1
  77. package/dist/src/scan.js +145 -14
  78. package/dist/src/starter.js +27 -2
  79. package/dist/src/step-exec.d.ts +39 -3
  80. package/dist/src/step-exec.js +118 -15
  81. package/dist/src/store.d.ts +144 -3
  82. package/dist/src/store.js +150 -7
  83. package/dist/src/tokens.d.ts +38 -0
  84. package/dist/src/tokens.js +57 -0
  85. package/dist/src/tool-names.js +1 -1
  86. package/dist/src/tool-programs.js +1 -1
  87. package/dist/src/trace-tree.d.ts +40 -0
  88. package/dist/src/trace-tree.js +156 -0
  89. package/dist/src/translator.d.ts +8 -0
  90. package/dist/src/translator.js +7 -2
  91. package/dist/src/trigger-log.d.ts +1 -1
  92. package/dist/src/workspace-check.d.ts +18 -0
  93. package/dist/src/workspace-check.js +34 -0
  94. package/package.json +2 -2
  95. package/src/agent-activity.ts +32 -0
  96. package/src/agent-import.ts +2 -2
  97. package/src/agent-tools.ts +2 -2
  98. package/src/agents-md.ts +39 -14
  99. package/src/alerts.ts +190 -0
  100. package/src/browser-pod.ts +3 -0
  101. package/src/capture.ts +88 -0
  102. package/src/compare.ts +146 -0
  103. package/src/completions.ts +27 -3
  104. package/src/deploy.ts +238 -12
  105. package/src/egress.ts +12 -2
  106. package/src/evals.ts +64 -19
  107. package/src/flow-lint.ts +36 -0
  108. package/src/flow-patterns.ts +26 -13
  109. package/src/frontmatter-fields.ts +83 -3
  110. package/src/frontmatter-guard.ts +70 -0
  111. package/src/frontmatter-schema.ts +33 -4
  112. package/src/frontmatter-text.ts +109 -0
  113. package/src/history.ts +13 -0
  114. package/src/hooks.ts +1 -1
  115. package/src/insights.ts +174 -0
  116. package/src/issues.ts +396 -0
  117. package/src/judge-verdict.ts +52 -0
  118. package/src/library.ts +1 -1
  119. package/src/limits.ts +38 -5
  120. package/src/linkable.ts +1 -1
  121. package/src/matter.ts +145 -0
  122. package/src/model-credential.ts +45 -0
  123. package/src/network.ts +35 -7
  124. package/src/notify.ts +22 -7
  125. package/src/okf.ts +1 -1
  126. package/src/online-checks.ts +450 -0
  127. package/src/operator.ts +213 -3
  128. package/src/otel-config.ts +67 -0
  129. package/src/otel.ts +100 -0
  130. package/src/outcome.ts +90 -0
  131. package/src/package.ts +8 -2
  132. package/src/platform.ts +1 -1
  133. package/src/prompt-layers.ts +90 -0
  134. package/src/provider-health.ts +2 -2
  135. package/src/providers.ts +7 -2
  136. package/src/run-container.ts +77 -8
  137. package/src/runner.ts +477 -79
  138. package/src/runs-filter.ts +88 -0
  139. package/src/scan.ts +153 -14
  140. package/src/starter.ts +27 -2
  141. package/src/step-exec.ts +168 -17
  142. package/src/store.ts +268 -9
  143. package/src/tokens.ts +68 -0
  144. package/src/tool-names.ts +1 -1
  145. package/src/tool-programs.ts +1 -1
  146. package/src/trace-tree.ts +187 -0
  147. package/src/translator.ts +13 -2
  148. package/src/trigger-log.ts +1 -0
  149. package/src/workspace-check.ts +41 -0
@@ -0,0 +1,132 @@
1
+ // gray-matter, without its cache.
2
+ //
3
+ // gray-matter keeps every input it is handed in a module-level cache keyed by
4
+ // the text, and it stores the entry BEFORE it parses. When the YAML does not
5
+ // parse, the first call throws — and the entry stays, unparsed. The next call
6
+ // with the same text returns it: no error, `data` empty. So in any process
7
+ // that read a file twice, a broken frontmatter was an error once and valid
8
+ // forever after.
9
+ //
10
+ // That is how one agent file took the worker down on 20 Oct 2026. The source
11
+ // API's save check parsed it first (and threw, caught), the write's own
12
+ // "reject files whose frontmatter doesn't parse" then hit the cache and let it
13
+ // through, and the worker, parsing it fresh at boot, threw on it out of
14
+ // listWorkspaces and never came up — for every account on the box.
15
+ //
16
+ // Passing an options object skips the cache entirely (gray-matter only reads
17
+ // and writes it when there are none), so every call here parses the text it
18
+ // was given. The cost is a parse per call, which is what every caller already
19
+ // believed it was paying. Import this, never "gray-matter" itself: a test in
20
+ // tests/matter.test.ts and foldrun-web's consistency test hold every repo to
21
+ // it.
22
+ import grayMatter from "gray-matter";
23
+ import { JOBS, FOLDABLE } from "./frontmatter-fields.js";
24
+ /** Where the four job blocks put things, on the parsed data — out of sight
25
+ * of every reader that spreads it or writes it out as JSON. */
26
+ const FOLD = Symbol.for("foldrun.jobBlocks");
27
+ /** What the fold did to this data, or null when it had no blocks. */
28
+ export function jobFold(data) {
29
+ return data && typeof data === "object" ? (data[FOLD] ?? null) : null;
30
+ }
31
+ /**
32
+ * The four job blocks (`governance:`, `guardrail:`, `observability:`,
33
+ * `evaluation:`) read as the flat keys they hold (frontmatter-fields.ts
34
+ * JOB_FIELDS). Only a key some config kind lets that block hold is lifted;
35
+ * anything else stays in the block for check to report. A key set both ways
36
+ * keeps its top-level value and is recorded. Nothing parsed is changed: new
37
+ * objects are built (YAML aliases share nodes — deleting from one would
38
+ * delete from the other).
39
+ */
40
+ function foldJobs(data) {
41
+ let fold = null;
42
+ let out = null;
43
+ for (const { name } of JOBS) {
44
+ const block = data[name];
45
+ if (!block || typeof block !== "object" || Array.isArray(block) || block instanceof Date)
46
+ continue;
47
+ const inner = block;
48
+ const lift = Object.keys(inner).filter((k) => FOLDABLE[name].has(k));
49
+ if (!lift.length)
50
+ continue;
51
+ out ??= { ...data };
52
+ fold ??= { folded: new Map(), conflicts: new Map() };
53
+ const rest = {};
54
+ for (const [k, v] of Object.entries(inner))
55
+ if (!lift.includes(k))
56
+ rest[k] = v;
57
+ for (const k of lift) {
58
+ if (Object.prototype.hasOwnProperty.call(data, k))
59
+ fold.conflicts.set(k, name);
60
+ else {
61
+ out[k] = inner[k];
62
+ fold.folded.set(k, name);
63
+ }
64
+ }
65
+ if (Object.keys(rest).length)
66
+ out[name] = rest;
67
+ else
68
+ delete out[name];
69
+ }
70
+ if (!out || !fold)
71
+ return data;
72
+ Object.defineProperty(out, FOLD, { value: fold, enumerable: false });
73
+ return out;
74
+ }
75
+ function matter(input, options) {
76
+ return grayMatter(input, options ?? {});
77
+ }
78
+ matter.stringify = grayMatter.stringify;
79
+ matter.test = grayMatter.test;
80
+ /**
81
+ * matter(), for the three files that hold configuration — an agent, a flow,
82
+ * AGENTS.md — with the four job blocks read as flat keys. Every reader of
83
+ * those files goes through this (tests/jobs.test.ts holds them to it);
84
+ * knowledge, memory, skills and every other document keep matter(), so a
85
+ * note's own `governance:` is its own (Codex debate, 21 Oct 2026).
86
+ */
87
+ export function configMatter(input, options) {
88
+ const file = grayMatter(input, options ?? {});
89
+ if (file.data && typeof file.data === "object" && !Array.isArray(file.data)) {
90
+ file.data = foldJobs(file.data);
91
+ }
92
+ return file;
93
+ }
94
+ export default matter;
95
+ /** The first line of a YAML parse error, with where it is: what a person
96
+ * needs to fix the file, without the parser's excerpt. */
97
+ export function frontmatterErrorText(err) {
98
+ const msg = err instanceof Error ? err.message : String(err);
99
+ return `frontmatter does not parse — ${msg.split("\n")[0].trim().replace(/:$/, "")}. Quote a value that contains a colon followed by a space.`;
100
+ }
101
+ /** The 1-based file line a YAML parse error points at, when it says. The
102
+ * text gray-matter hands the parser starts with the newline after the
103
+ * opening `---`, so the parser's 0-based line is the file's 1-based one. */
104
+ export function frontmatterErrorLine(err) {
105
+ const line = err?.mark?.line;
106
+ return typeof line === "number" ? line + 1 : undefined;
107
+ }
108
+ /** Frontmatter as the runtime reads a skill: strictly first, then once more
109
+ * with every unquoted value that holds a colon quoted (the commonest way a
110
+ * hand-written SKILL.md breaks, and one other skills clients forgive), and
111
+ * empty when even that fails. `lenient` says the second read was needed. */
112
+ export function parseLenient(raw) {
113
+ try {
114
+ return { data: matter(raw).data, lenient: false, failed: false };
115
+ }
116
+ catch {
117
+ const fm = raw.match(/^\uFEFF?---\r?\n([\s\S]*?)\r?\n---/);
118
+ if (fm) {
119
+ const fixed = fm[1].replace(/^([A-Za-z0-9_-]+):[ \t]+(?!["'>|])(.*:.*)$/gm, (_m, k, v) => `${k}: ${JSON.stringify(v)}`);
120
+ try {
121
+ // A function replacer, not a string: a fixed block containing `$&` or
122
+ // `$$` (a value with those in it) would otherwise be treated as a
123
+ // replacement pattern and splice the whole frontmatter into itself.
124
+ return { data: matter(raw.replace(fm[1], () => fixed)).data, lenient: true, failed: false };
125
+ }
126
+ catch {
127
+ /* fall through */
128
+ }
129
+ }
130
+ return { data: {}, lenient: false, failed: true };
131
+ }
132
+ }
@@ -28,3 +28,17 @@ export declare function accountModelBlock(read: (name: string) => string | null)
28
28
  * claude.ai logins, so on a hosted platform only the operator's own
29
29
  * accounts may; a customer's key must be an API key. */
30
30
  export declare function isClaudeLoginToken(value: string | null | undefined): boolean;
31
+ /**
32
+ * The variables a step's own model call is made with: the credential, the
33
+ * endpoint, the tier names a provider renames and its headers, and the
34
+ * egress proxy's name for the key. A declared secret with one of these
35
+ * names would replace the step's model settings in the sandbox, so `check`
36
+ * warns about it and the runner leaves it out (FOL-19).
37
+ */
38
+ export declare const MODEL_ENV_NAMES: readonly string[];
39
+ /** How such a secret crosses into the sandbox instead; the driver maps it
40
+ * back for the agent's own tools (run-container.ts). */
41
+ export declare const DECLARED_ENV_PREFIX = "FOLDRUN_DECLARED_";
42
+ export declare function isModelEnvName(name: string): boolean;
43
+ /** Why a `secrets:` list names a model variable, or null. */
44
+ export declare function modelSecretCollision(names: unknown): string | null;
@@ -96,3 +96,43 @@ export function accountModelBlock(read) {
96
96
  export function isClaudeLoginToken(value) {
97
97
  return typeof value === "string" && /^sk-ant-oat\d*-/.test(value.trim());
98
98
  }
99
+ /**
100
+ * The variables a step's own model call is made with: the credential, the
101
+ * endpoint, the tier names a provider renames and its headers, and the
102
+ * egress proxy's name for the key. A declared secret with one of these
103
+ * names would replace the step's model settings in the sandbox, so `check`
104
+ * warns about it and the runner leaves it out (FOL-19).
105
+ */
106
+ export const MODEL_ENV_NAMES = [
107
+ "ANTHROPIC_API_KEY",
108
+ "ANTHROPIC_AUTH_TOKEN",
109
+ "ANTHROPIC_BASE_URL",
110
+ "ANTHROPIC_CUSTOM_HEADERS",
111
+ "ANTHROPIC_MODEL",
112
+ "ANTHROPIC_SMALL_FAST_MODEL",
113
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
114
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
115
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
116
+ "CLAUDE_CODE_OAUTH_TOKEN",
117
+ "FOLDRUN_MODEL_KEY",
118
+ "FOLDRUN_MODEL_KEY_FALLBACK",
119
+ ];
120
+ /** How such a secret crosses into the sandbox instead; the driver maps it
121
+ * back for the agent's own tools (run-container.ts). */
122
+ export const DECLARED_ENV_PREFIX = "FOLDRUN_DECLARED_";
123
+ export function isModelEnvName(name) {
124
+ return MODEL_ENV_NAMES.includes(name.trim());
125
+ }
126
+ /** Why a `secrets:` list names a model variable, or null. */
127
+ export function modelSecretCollision(names) {
128
+ const list = (Array.isArray(names) ? names : typeof names === "string" ? names.split(",") : [])
129
+ .filter((n) => typeof n === "string")
130
+ .map((n) => n.trim())
131
+ .filter(isModelEnvName);
132
+ if (!list.length)
133
+ return null;
134
+ const which = list.map((n) => `\`${n}\``).join(", ");
135
+ return (`${which} ${list.length === 1 ? "is a name" : "are names"} the step's own model call uses, so the model keeps its own value: ` +
136
+ "its script and API tools and `verify:` get the declared one, but its own shell sees the model's — " +
137
+ "give the secret another name (MY_ANTHROPIC_KEY), or run the model on it with `provider:` or `foldrun account model set`");
138
+ }
@@ -1,4 +1,3 @@
1
- /** How wide a step's network is. */
2
1
  export type NetworkLevel = "open" | "trusted" | "none";
3
2
  export declare const NETWORK_LEVELS: readonly NetworkLevel[];
4
3
  /** One level's `network:` line, read. Absent fields inherit. */
@@ -74,6 +73,18 @@ export declare function resolveNetwork(args: {
74
73
  implied?: Iterable<string>;
75
74
  }): NetworkPolicy;
76
75
  /** One line for the run's trace. */
76
+ /** The sites a step reached, read from the egress lines on its trail
77
+ * (egress-proxy.ts formatDrain): `egress: CONNECT host:port …` and
78
+ * `egress: GET host/path → 200 …`. Lower-cased, port dropped, sorted. */
79
+ export declare function hostsInTrail(events: {
80
+ type?: string;
81
+ text?: string;
82
+ }[]): string[];
83
+ /** `network_enforce: true` in the account's or the workspace's AGENTS.md:
84
+ * the steps here have a site off their list refused rather than only named.
85
+ * Either level switches it on; neither can switch off what the other, or the
86
+ * install (FOLDRUN_NETWORK_ENFORCE=1), switched on. */
87
+ export declare function networkEnforceWanted(account: unknown, workspace: unknown): boolean;
77
88
  export declare function networkLine(policy: NetworkPolicy, enforced: boolean | null): string;
78
89
  /** What the proxy says when it refuses a site. */
79
90
  export declare function refusalText(policy: Pick<NetworkPolicy, "level">, host: string): string;
@@ -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
@@ -233,12 +235,36 @@ export function resolveNetwork(args) {
233
235
  return { level, hosts, from, defaults, added, implied };
234
236
  }
235
237
  /** One line for the run's trace. */
238
+ /** The sites a step reached, read from the egress lines on its trail
239
+ * (egress-proxy.ts formatDrain): `egress: CONNECT host:port …` and
240
+ * `egress: GET host/path → 200 …`. Lower-cased, port dropped, sorted. */
241
+ export function hostsInTrail(events) {
242
+ const hosts = new Set();
243
+ for (const e of events) {
244
+ // A refused or unreachable call reached nothing: the proxy says so on
245
+ // the same line (`… refused — …`, `… → unreachable: …`).
246
+ if (typeof e.text === "string" && /^egress: \S+ \S+ (?:refused|unreachable|→ unreachable)\b/.test(e.text))
247
+ continue;
248
+ const m = typeof e.text === "string" ? /^egress: (?:CONNECT ([^\s:]+)|[A-Z]+ ([^\s/:]+))/.exec(e.text) : null;
249
+ const host = (m?.[1] ?? m?.[2])?.toLowerCase();
250
+ if (host)
251
+ hosts.add(host);
252
+ }
253
+ return [...hosts].sort();
254
+ }
255
+ /** `network_enforce: true` in the account's or the workspace's AGENTS.md:
256
+ * the steps here have a site off their list refused rather than only named.
257
+ * Either level switches it on; neither can switch off what the other, or the
258
+ * install (FOLDRUN_NETWORK_ENFORCE=1), switched on. */
259
+ export function networkEnforceWanted(account, workspace) {
260
+ return account === true || workspace === true;
261
+ }
236
262
  export function networkLine(policy, enforced) {
237
263
  const how = enforced === null
238
264
  ? " — not enforced here (no sandbox network)"
239
265
  : enforced
240
266
  ? ""
241
- : " — report only on this install: a site off the list is allowed and named in the trace";
267
+ : " — report only: a site off the list is allowed and named in the trace (network_enforce: true in AGENTS.md refuses it)";
242
268
  if (policy.level === "open")
243
269
  return `network: open (${policy.from}) — any public site${how}`;
244
270
  const parts = [];
@@ -346,15 +372,16 @@ export function setNetworkSetting(raw, setting) {
346
372
  const close = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
347
373
  if (close < 0)
348
374
  return raw;
349
- const at = lines.findIndex((l, i) => i > 0 && i < close && /^network\s*:/.test(l));
350
- if (at < 0) {
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) {
351
379
  if (line)
352
380
  lines.splice(close, 0, line);
353
381
  return lines.join(eol);
354
382
  }
355
- let end = at + 1;
356
- while (end < close && (/^\s+\S/.test(lines[end]) || /^\s*-\s/.test(lines[end])))
357
- end++;
358
- lines.splice(at, end - at, ...(line ? [line] : []));
359
- 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);
360
387
  }
@@ -171,6 +171,22 @@ export declare function sendPlainNotification(tenant: string, workspace: string,
171
171
  flow?: string;
172
172
  runId?: string;
173
173
  }): Promise<boolean>;
174
+ /**
175
+ * The same send, saying what became of it: `ok` when it arrived, `retrying`
176
+ * when a webhook's first attempt failed and the delivery log holds it for
177
+ * another (FOL-58 — a caller that would retry on its own must not start a
178
+ * second chain).
179
+ */
180
+ export declare function sendPlainNotificationOutcome(tenant: string, workspace: string, msg: {
181
+ event: string;
182
+ headline: string;
183
+ detail: string;
184
+ flow?: string;
185
+ runId?: string;
186
+ }): Promise<{
187
+ ok: boolean;
188
+ retrying: boolean;
189
+ }>;
174
190
  /**
175
191
  * Prove the notification path works, now, while someone is looking.
176
192
  *
@@ -305,16 +305,26 @@ export function isQuietFlow(flow) {
305
305
  * of it. They are on by default: an alert nobody opted into is the point.
306
306
  */
307
307
  export async function sendPlainNotification(tenant, workspace, msg) {
308
+ return (await sendPlainNotificationOutcome(tenant, workspace, msg)).ok;
309
+ }
310
+ /**
311
+ * The same send, saying what became of it: `ok` when it arrived, `retrying`
312
+ * when a webhook's first attempt failed and the delivery log holds it for
313
+ * another (FOL-58 — a caller that would retry on its own must not start a
314
+ * second chain).
315
+ */
316
+ export async function sendPlainNotificationOutcome(tenant, workspace, msg) {
317
+ const no = { ok: false, retrying: false };
308
318
  const config = notifyConfig(tenant, workspace);
309
319
  if (!config)
310
- return false;
320
+ return no;
311
321
  // These follow `failed`. Each one IS a failure — a flow that stopped
312
322
  // running, a run that has stalled, a budget about to refuse work — that
313
323
  // happens to produce no failed run for the normal path to report. A
314
324
  // destination hearing about failures hears about these; naming one
315
325
  // explicitly also turns it on; leaving `failed` out silences them.
316
326
  if (!config.events.includes(msg.event) && !config.events.includes("failed"))
317
- return false;
327
+ return no;
318
328
  const body = {
319
329
  text: `${msg.headline} — ${msg.detail} · ${workspace}`,
320
330
  workspace,
@@ -327,7 +337,7 @@ export async function sendPlainNotification(tenant, workspace, msg) {
327
337
  if (config.email) {
328
338
  const mail = notifyMail(tenant);
329
339
  if (!mail)
330
- return false;
340
+ return no;
331
341
  const r = await sendMail({
332
342
  tenant,
333
343
  mail,
@@ -337,16 +347,16 @@ export async function sendPlainNotification(tenant, workspace, msg) {
337
347
  subject: `${msg.headline}`.slice(0, 160),
338
348
  text: `${msg.headline}\n\n${msg.detail}\n\nworkspace: ${workspace}\n${msg.flow ? `flow: ${msg.flow}\n` : ""}${msg.runId ? `run: ${msg.runId}\n` : ""}`,
339
349
  });
340
- return r.ok;
350
+ return { ok: r.ok, retrying: false };
341
351
  }
342
352
  if (!webhookTarget(tenant, workspace, config).url)
343
- return false;
353
+ return no;
344
354
  const r = await platform.deliverWebhook(tenant, workspace, { event: msg.event, body: JSON.stringify(body), retry: true });
345
- return r.ok;
355
+ return { ok: r.ok, retrying: r.retrying === true };
346
356
  }
347
357
  catch (err) {
348
358
  console.error(`[foldrun] notify (${msg.event}): ${tenant}/${workspace} →`, err instanceof Error ? err.message : err);
349
- return false;
359
+ return no;
350
360
  }
351
361
  }
352
362
  /**
package/dist/src/okf.js CHANGED
@@ -21,7 +21,7 @@
21
21
  // and `description:` stay exactly where they are.
22
22
  import fs from "node:fs";
23
23
  import path from "node:path";
24
- import matter from "gray-matter";
24
+ import matter from "./matter.js";
25
25
  export const OKF_VERSION = "0.2";
26
26
  /** This producer's actor string, in the spec's `<producer>/<version>` form. */
27
27
  export const PRODUCER = "foldrun/0.1.0";
@@ -0,0 +1,76 @@
1
+ import { type ScanFile, type ScanFinding } from "./scan.ts";
2
+ export declare const OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch";
3
+ export interface Pin {
4
+ ecosystem: "PyPI" | "npm";
5
+ name: string;
6
+ version: string;
7
+ /** As written, e.g. requests==2.19.0. */
8
+ spec: string;
9
+ file: string;
10
+ line?: number;
11
+ }
12
+ /** One source's outcome: checked (its findings are complete), or not. */
13
+ export interface SourceResult {
14
+ /** "osv", or "mcp:<declaring file>#<server>" — two files may name a server alike. */
15
+ source: string;
16
+ /** True only when its findings are complete. */
17
+ checked: boolean;
18
+ /** Not complete, but what it did read is real (a cap, a paged answer). */
19
+ partial?: boolean;
20
+ /** Not reached because the caller's deadline ran out first. */
21
+ skipped?: boolean;
22
+ /** Why not, or what was incomplete. */
23
+ problem?: string;
24
+ }
25
+ /** An online finding, carrying the source it came from — the rescan's identity
26
+ * for it, never parsed back out of the message (Codex review). */
27
+ export type OnlineFinding = ScanFinding & {
28
+ source: string;
29
+ };
30
+ export interface OnlineResult {
31
+ findings: OnlineFinding[];
32
+ /** What could not be checked, in words. */
33
+ problems: string[];
34
+ sources: SourceResult[];
35
+ }
36
+ type Fetch = (url: string, init?: RequestInit) => Promise<Response>;
37
+ /** An exact pin: pip `name==1.2.3` (extras allowed), npm `name@1.2.3` or `@scope/name@1.2.3`. */
38
+ export declare function parsePin(spec: string): Omit<Pin, "file" | "line" | "spec"> | null;
39
+ /** Every exact pin in the workspace's runtime: blocks (agents, AGENTS.md, tools). */
40
+ export declare function exactPins(files: ScanFile[]): Pin[];
41
+ /** Ask OSV about every exact pin, once each, in one batch; findings fan out to every file that declares it. */
42
+ export declare function vulnerabilityFindings(files: ScanFile[], fetchImpl?: Fetch, deadline?: number): Promise<OnlineResult>;
43
+ export interface RemoteMcp {
44
+ name: string;
45
+ url: string;
46
+ /** Configured headers or a URL that may carry a credential: never probed. */
47
+ needsCredential: boolean;
48
+ file: string;
49
+ line?: number;
50
+ }
51
+ /** MCP servers declared by URL — in an agent's mcpServers: or a tool with transport: mcp. */
52
+ export declare function remoteMcpServers(files: ScanFile[]): RemoteMcp[];
53
+ /** initialize → notifications/initialized → tools/list (every page, within limits), anonymously. */
54
+ export declare function mcpTools(url: string, fetchImpl: Fetch, deadline?: number): Promise<{
55
+ tools: {
56
+ name: string;
57
+ text: string;
58
+ }[];
59
+ incomplete?: string;
60
+ } | {
61
+ problem: string;
62
+ }>;
63
+ /** Every URL-reached MCP server's tool text, through the content rules as untrusted text. */
64
+ export declare function mcpDescriptionFindings(files: ScanFile[], opts?: {
65
+ fetch?: Fetch;
66
+ deadline?: number;
67
+ order?: string[];
68
+ }): Promise<OnlineResult>;
69
+ /** Both, as one answer. */
70
+ export declare function onlineFindings(files: ScanFile[], opts?: {
71
+ fetch?: Fetch;
72
+ osvFetch?: Fetch;
73
+ deadline?: number;
74
+ order?: string[];
75
+ }): Promise<OnlineResult>;
76
+ export {};