@foldrun/core 2.3.0 → 2.5.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 +1 -1
- package/dist/src/agent-tools.js +1 -1
- package/dist/src/agents-md.js +1 -1
- 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 +1 -0
- package/dist/src/deploy.d.ts +36 -1
- package/dist/src/deploy.js +170 -11
- package/dist/src/egress.d.ts +16 -1
- package/dist/src/evals.d.ts +5 -1
- package/dist/src/evals.js +26 -18
- package/dist/src/frontmatter-fields.js +9 -0
- package/dist/src/frontmatter-schema.js +3 -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/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 +8 -0
- package/dist/src/matter.js +28 -0
- package/dist/src/model-credential.d.ts +14 -0
- package/dist/src/model-credential.js +40 -0
- package/dist/src/model-env.d.ts +14 -2
- package/dist/src/model-env.js +34 -9
- package/dist/src/network.d.ts +12 -0
- package/dist/src/network.js +25 -1
- 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 +1 -1
- 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 +1 -1
- package/dist/src/providers.d.ts +5 -0
- package/dist/src/providers.js +2 -2
- package/dist/src/run-container.d.ts +16 -2
- package/dist/src/run-container.js +57 -5
- package/dist/src/runner.d.ts +53 -2
- package/dist/src/runner.js +356 -70
- package/dist/src/runs-filter.d.ts +48 -0
- package/dist/src/runs-filter.js +73 -0
- package/dist/src/scan.d.ts +19 -1
- package/dist/src/scan.js +60 -7
- package/dist/src/starter.js +4 -2
- package/dist/src/step-exec.d.ts +29 -1
- package/dist/src/step-exec.js +106 -6
- package/dist/src/store.d.ts +130 -3
- package/dist/src/store.js +145 -6
- 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 +1 -1
- package/src/agent-tools.ts +1 -1
- package/src/agents-md.ts +1 -1
- 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 +1 -0
- package/src/deploy.ts +184 -10
- package/src/egress.ts +12 -2
- package/src/evals.ts +26 -18
- package/src/frontmatter-fields.ts +9 -0
- package/src/frontmatter-schema.ts +3 -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/library.ts +1 -1
- package/src/limits.ts +38 -5
- package/src/linkable.ts +1 -1
- package/src/matter.ts +33 -0
- package/src/model-credential.ts +45 -0
- package/src/model-env.ts +42 -10
- package/src/network.ts +25 -1
- 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 +1 -1
- package/src/platform.ts +1 -1
- package/src/prompt-layers.ts +90 -0
- package/src/provider-health.ts +1 -1
- package/src/providers.ts +7 -2
- package/src/run-container.ts +66 -7
- package/src/runner.ts +363 -46
- package/src/runs-filter.ts +88 -0
- package/src/scan.ts +68 -7
- package/src/starter.ts +4 -2
- package/src/step-exec.ts +147 -9
- package/src/store.ts +254 -8
- 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
package/dist/src/limits.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// web: 150 # every web action together
|
|
7
7
|
// crm: 20 # an API, script or MCP tool, by the name it is granted as
|
|
8
8
|
// calls: 300 # every tool call in the step, whatever the tool
|
|
9
|
+
// repeat: 5 # the same call — one tool, the same input — at most this often
|
|
9
10
|
//
|
|
10
11
|
// Counted per step and enforced in step-exec.ts's PreToolUse hook — the one
|
|
11
12
|
// place every call passes, the step's own and its sub-agents', on the
|
|
@@ -17,10 +18,27 @@
|
|
|
17
18
|
//
|
|
18
19
|
// Nearest wins per key: the account's AGENTS.md, then the workspace's, then
|
|
19
20
|
// the agent's own `limits:`, then a flow step's `limits:` option.
|
|
21
|
+
import crypto from "node:crypto";
|
|
20
22
|
import { WEB_ACTIONS } from "./providers.js";
|
|
21
23
|
import { DELEGATE_TOOLS } from "./subagents.js";
|
|
22
24
|
/** The key that caps every call in the step. */
|
|
23
25
|
export const TOTAL_KEY = "calls";
|
|
26
|
+
/** The key that caps one identical call: a model asking the same tool the
|
|
27
|
+
* same thing over and over is stuck, and every repeat is spend. Each call
|
|
28
|
+
* also counts under `repeat:<hash of tool and input>`; `repeat: N` caps
|
|
29
|
+
* every one of those. */
|
|
30
|
+
export const REPEAT_KEY = "repeat";
|
|
31
|
+
/** JSON with its keys sorted, so `{a,b}` and `{b,a}` are the same call. */
|
|
32
|
+
function stable(v) {
|
|
33
|
+
if (Array.isArray(v))
|
|
34
|
+
return `[${v.map(stable).join(",")}]`;
|
|
35
|
+
if (v && typeof v === "object")
|
|
36
|
+
return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${stable(v[k])}`).join(",")}}`;
|
|
37
|
+
return JSON.stringify(v) ?? "null";
|
|
38
|
+
}
|
|
39
|
+
function repeatKey(toolName, input) {
|
|
40
|
+
return `${REPEAT_KEY}:${crypto.createHash("sha1").update(`${toolName}\0${stable(input ?? {})}`).digest("hex").slice(0, 16)}`;
|
|
41
|
+
}
|
|
24
42
|
/** SDK built-ins, counted under the foldrun group that grants them — and
|
|
25
43
|
* under their own SDK name, so `Bash: 5` works for a Claude Code file. */
|
|
26
44
|
const BUILTIN_GROUP = {
|
|
@@ -36,6 +54,7 @@ const PLATFORM_SERVERS = {
|
|
|
36
54
|
foldrun_history: "history",
|
|
37
55
|
foldrun_desks: "desks",
|
|
38
56
|
foldrun_ask: "ask",
|
|
57
|
+
foldrun_peers: "peers",
|
|
39
58
|
};
|
|
40
59
|
/** One spelling for comparing: `desk-email` and `desk_email` are the same
|
|
41
60
|
* tool (a script tool's SDK name has its dashes made underscores). */
|
|
@@ -72,7 +91,7 @@ export function readLimits(raw, where = "limits") {
|
|
|
72
91
|
for (const [key, value] of flat) {
|
|
73
92
|
const k = key.trim();
|
|
74
93
|
if (!KEY_RE.test(k)) {
|
|
75
|
-
problems.push(`${where}: "${k}" is not a key — a tool's name, web.<action>, or
|
|
94
|
+
problems.push(`${where}: "${k}" is not a key — a tool's name, web.<action>, calls, or repeat.`);
|
|
76
95
|
continue;
|
|
77
96
|
}
|
|
78
97
|
if (k.includes(".")) {
|
|
@@ -169,6 +188,11 @@ export function limitKeysFor(toolName, input, owners = {}) {
|
|
|
169
188
|
keys.push(toolName);
|
|
170
189
|
return keys;
|
|
171
190
|
}
|
|
191
|
+
/** What step-exec counts one call under: limitKeysFor's keys and the
|
|
192
|
+
* call's own `repeat:` key, for `limits: repeat`. */
|
|
193
|
+
export function callKeysFor(toolName, input, owners = {}) {
|
|
194
|
+
return [...limitKeysFor(toolName, input, owners), repeatKey(toolName, input)];
|
|
195
|
+
}
|
|
172
196
|
/**
|
|
173
197
|
* One step's counts. `take` checks every key the call counts under and
|
|
174
198
|
* either refuses — counting nothing — or counts it under all of them.
|
|
@@ -187,7 +211,8 @@ export class CallCounter {
|
|
|
187
211
|
take(keys) {
|
|
188
212
|
const norm = [...new Set(keys.map(normKey))];
|
|
189
213
|
for (const k of norm) {
|
|
190
|
-
const
|
|
214
|
+
const repeat = k.startsWith(`${REPEAT_KEY}:`);
|
|
215
|
+
const l = this.limits.get(repeat ? REPEAT_KEY : k);
|
|
191
216
|
if (!l)
|
|
192
217
|
continue;
|
|
193
218
|
const used = this.used.get(k) ?? 0;
|
|
@@ -195,7 +220,9 @@ export class CallCounter {
|
|
|
195
220
|
this.refused++;
|
|
196
221
|
return {
|
|
197
222
|
key: l.key, used, limit: l.limit,
|
|
198
|
-
message:
|
|
223
|
+
message: repeat
|
|
224
|
+
? `limit reached: this exact call (same tool, same input) ${used} of ${l.limit} times in this step — repeating it will not change the answer; try something different, or say what is stuck`
|
|
225
|
+
: `limit reached: ${l.key} ${used} of ${l.limit} in this step — work with what you have, or say what more you would need`,
|
|
199
226
|
};
|
|
200
227
|
}
|
|
201
228
|
}
|
|
@@ -218,7 +245,11 @@ export class CallCounter {
|
|
|
218
245
|
summary() {
|
|
219
246
|
if (!this.active)
|
|
220
247
|
return null;
|
|
221
|
-
|
|
248
|
+
// repeat: the most any one call was made, against its cap.
|
|
249
|
+
const most = (k) => k === REPEAT_KEY
|
|
250
|
+
? Math.max(0, ...[...this.used.entries()].filter(([u]) => u.startsWith(`${REPEAT_KEY}:`)).map(([, n]) => n))
|
|
251
|
+
: this.used.get(k) ?? 0;
|
|
252
|
+
const parts = [...this.limits.entries()].map(([k, l]) => `${l.key} ${most(k)}/${l.limit}`);
|
|
222
253
|
return `limits: ${parts.join(", ")}${this.refused ? ` — ${this.refused} call${this.refused === 1 ? "" : "s"} refused` : ""}`;
|
|
223
254
|
}
|
|
224
255
|
}
|
|
@@ -248,7 +279,7 @@ export function limitKeyProblems(limits, ctx) {
|
|
|
248
279
|
return [n];
|
|
249
280
|
}).map(normKey));
|
|
250
281
|
for (const key of Object.keys(limits)) {
|
|
251
|
-
if (key === TOTAL_KEY)
|
|
282
|
+
if (key === TOTAL_KEY || key === REPEAT_KEY)
|
|
252
283
|
continue;
|
|
253
284
|
const base = key.startsWith("web.") ? "web" : key;
|
|
254
285
|
const n = normKey(base);
|
package/dist/src/linkable.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// real workspace, so a future rename fails a test instead of a run.
|
|
14
14
|
import fs from "node:fs";
|
|
15
15
|
import path from "node:path";
|
|
16
|
-
import matter from "
|
|
16
|
+
import matter from "./matter.js";
|
|
17
17
|
import { STORAGE_DIR } from "./store.js";
|
|
18
18
|
/** The directories a bare [[name]] may point at, matching the resolver. */
|
|
19
19
|
export const LINKABLE_DIRS = [STORAGE_DIR, "state", "knowledge", "memory", "skills", "outputs"];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import grayMatter from "gray-matter";
|
|
2
|
+
type Options = NonNullable<Parameters<typeof grayMatter>[1]>;
|
|
3
|
+
declare function matter(input: string, options?: Options): grayMatter.GrayMatterFile<string>;
|
|
4
|
+
declare namespace matter {
|
|
5
|
+
var stringify: typeof grayMatter.stringify;
|
|
6
|
+
var test: typeof grayMatter.test;
|
|
7
|
+
}
|
|
8
|
+
export default matter;
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
function matter(input, options) {
|
|
24
|
+
return grayMatter(input, options ?? {});
|
|
25
|
+
}
|
|
26
|
+
matter.stringify = grayMatter.stringify;
|
|
27
|
+
matter.test = grayMatter.test;
|
|
28
|
+
export default matter;
|
|
@@ -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
|
+
}
|
package/dist/src/model-env.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TranslatorSpec } from "./translator.ts";
|
|
1
2
|
/** Refused for want of a usable key; `status` is what an HTTP layer answers. */
|
|
2
3
|
export declare class ModelKeyError extends Error {
|
|
3
4
|
readonly status: number;
|
|
@@ -7,11 +8,22 @@ export declare const NO_MODEL_KEY: string;
|
|
|
7
8
|
/**
|
|
8
9
|
* `platform` is a function: the caller's own way to build the platform's env
|
|
9
10
|
* (a step's, or the SDK's host env), called only when this account may use
|
|
10
|
-
* it. `format` says what the endpoint speaks
|
|
11
|
-
*
|
|
11
|
+
* it. `format` says what the endpoint speaks; `translator` is set when it is
|
|
12
|
+
* not Anthropic's, and `withModelEnv` puts the step's translator in front of
|
|
13
|
+
* it, so a judge or a draft works on any key a run works on (FOL-16).
|
|
12
14
|
*/
|
|
13
15
|
export declare function accountModelEnv(tenant: string, workspace: string, platformEnv: () => Record<string, string | undefined>): {
|
|
14
16
|
env: Record<string, string | undefined>;
|
|
15
17
|
supply: "provider" | "platform";
|
|
16
18
|
format: string;
|
|
19
|
+
translator: TranslatorSpec | null;
|
|
17
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Run `fn` with the model env, through a translator on this host when the
|
|
23
|
+
* provider speaks Chat Completions or Responses — the same one a step uses
|
|
24
|
+
* (runner.ts), started for this call and closed after it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function withModelEnv<T>(m: {
|
|
27
|
+
env: Record<string, string | undefined>;
|
|
28
|
+
translator?: TranslatorSpec | null;
|
|
29
|
+
}, fn: (env: Record<string, string | undefined>) => Promise<T>): Promise<T>;
|
package/dist/src/model-env.js
CHANGED
|
@@ -10,6 +10,8 @@ import { readAgentsMd } from "./runner.js";
|
|
|
10
10
|
import { getSecret } from "./secrets.js";
|
|
11
11
|
import { platform } from "./platform.js";
|
|
12
12
|
import { accountModelBlock, isClaudeLoginToken } from "./model-credential.js";
|
|
13
|
+
import { providerPreset } from "./providers.js";
|
|
14
|
+
import { startTranslator, translatorSpecFor } from "./translator.js";
|
|
13
15
|
/** Refused for want of a usable key; `status` is what an HTTP layer answers. */
|
|
14
16
|
export class ModelKeyError extends Error {
|
|
15
17
|
status;
|
|
@@ -23,8 +25,9 @@ export const NO_MODEL_KEY = "no model key: this account runs on its own API key,
|
|
|
23
25
|
/**
|
|
24
26
|
* `platform` is a function: the caller's own way to build the platform's env
|
|
25
27
|
* (a step's, or the SDK's host env), called only when this account may use
|
|
26
|
-
* it. `format` says what the endpoint speaks
|
|
27
|
-
*
|
|
28
|
+
* it. `format` says what the endpoint speaks; `translator` is set when it is
|
|
29
|
+
* not Anthropic's, and `withModelEnv` puts the step's translator in front of
|
|
30
|
+
* it, so a judge or a draft works on any key a run works on (FOL-16).
|
|
28
31
|
*/
|
|
29
32
|
export function accountModelEnv(tenant, workspace, platformEnv) {
|
|
30
33
|
const raw = readAgentsMd(workspaceDir(tenant, workspace))?.data?.provider ??
|
|
@@ -34,7 +37,7 @@ export function accountModelEnv(tenant, workspace, platformEnv) {
|
|
|
34
37
|
if (!spec?.baseUrl) {
|
|
35
38
|
if (!platform.platformKeyAllowed(tenant))
|
|
36
39
|
throw new ModelKeyError(NO_MODEL_KEY);
|
|
37
|
-
return { env: platformEnv(), supply: "platform", format: "anthropic" };
|
|
40
|
+
return { env: platformEnv(), supply: "platform", format: "anthropic", translator: null };
|
|
38
41
|
}
|
|
39
42
|
const sub = (text) => text.replace(/\$\{([A-Z][A-Z0-9_]*)\}/g, (whole, name) => getSecret(tenant, name, workspace)?.value ?? whole);
|
|
40
43
|
const token = sub(spec.token);
|
|
@@ -43,15 +46,37 @@ export function accountModelEnv(tenant, workspace, platformEnv) {
|
|
|
43
46
|
if (!platform.platformKeyAllowed(tenant) && isClaudeLoginToken(token)) {
|
|
44
47
|
throw new ModelKeyError("model key: a Claude login token (sk-ant-oat…) cannot be used here — use an API key from your provider", 422);
|
|
45
48
|
}
|
|
49
|
+
const headers = Object.fromEntries(Object.entries(spec.headers).map(([k, v]) => [k, sub(v)]));
|
|
50
|
+
const preset = providerPreset(spec.name);
|
|
46
51
|
return {
|
|
47
|
-
env: providerEnvFor({
|
|
52
|
+
env: providerEnvFor({ baseUrl: spec.baseUrl, token, auth: spec.auth, models: spec.models, headers }),
|
|
53
|
+
supply: "provider",
|
|
54
|
+
format: spec.format,
|
|
55
|
+
translator: translatorSpecFor({
|
|
56
|
+
format: spec.format,
|
|
48
57
|
baseUrl: spec.baseUrl,
|
|
49
58
|
token,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
headers,
|
|
60
|
+
params: spec.params,
|
|
61
|
+
name: spec.name,
|
|
62
|
+
maxTokensParam: preset?.maxTokensParam,
|
|
63
|
+
reasoningEffort: preset?.reasoningEffort,
|
|
53
64
|
}),
|
|
54
|
-
supply: "provider",
|
|
55
|
-
format: spec.format,
|
|
56
65
|
};
|
|
57
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Run `fn` with the model env, through a translator on this host when the
|
|
69
|
+
* provider speaks Chat Completions or Responses — the same one a step uses
|
|
70
|
+
* (runner.ts), started for this call and closed after it.
|
|
71
|
+
*/
|
|
72
|
+
export async function withModelEnv(m, fn) {
|
|
73
|
+
if (!m.translator)
|
|
74
|
+
return fn(m.env);
|
|
75
|
+
const t = await startTranslator(m.translator);
|
|
76
|
+
try {
|
|
77
|
+
return await fn({ ...m.env, ...t.env });
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await t.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/src/network.d.ts
CHANGED
|
@@ -74,6 +74,18 @@ export declare function resolveNetwork(args: {
|
|
|
74
74
|
implied?: Iterable<string>;
|
|
75
75
|
}): NetworkPolicy;
|
|
76
76
|
/** One line for the run's trace. */
|
|
77
|
+
/** The sites a step reached, read from the egress lines on its trail
|
|
78
|
+
* (egress-proxy.ts formatDrain): `egress: CONNECT host:port …` and
|
|
79
|
+
* `egress: GET host/path → 200 …`. Lower-cased, port dropped, sorted. */
|
|
80
|
+
export declare function hostsInTrail(events: {
|
|
81
|
+
type?: string;
|
|
82
|
+
text?: string;
|
|
83
|
+
}[]): string[];
|
|
84
|
+
/** `network_enforce: true` in the account's or the workspace's AGENTS.md:
|
|
85
|
+
* the steps here have a site off their list refused rather than only named.
|
|
86
|
+
* Either level switches it on; neither can switch off what the other, or the
|
|
87
|
+
* install (FOLDRUN_NETWORK_ENFORCE=1), switched on. */
|
|
88
|
+
export declare function networkEnforceWanted(account: unknown, workspace: unknown): boolean;
|
|
77
89
|
export declare function networkLine(policy: NetworkPolicy, enforced: boolean | null): string;
|
|
78
90
|
/** What the proxy says when it refuses a site. */
|
|
79
91
|
export declare function refusalText(policy: Pick<NetworkPolicy, "level">, host: string): string;
|
package/dist/src/network.js
CHANGED
|
@@ -233,12 +233,36 @@ export function resolveNetwork(args) {
|
|
|
233
233
|
return { level, hosts, from, defaults, added, implied };
|
|
234
234
|
}
|
|
235
235
|
/** One line for the run's trace. */
|
|
236
|
+
/** The sites a step reached, read from the egress lines on its trail
|
|
237
|
+
* (egress-proxy.ts formatDrain): `egress: CONNECT host:port …` and
|
|
238
|
+
* `egress: GET host/path → 200 …`. Lower-cased, port dropped, sorted. */
|
|
239
|
+
export function hostsInTrail(events) {
|
|
240
|
+
const hosts = new Set();
|
|
241
|
+
for (const e of events) {
|
|
242
|
+
// A refused or unreachable call reached nothing: the proxy says so on
|
|
243
|
+
// the same line (`… refused — …`, `… → unreachable: …`).
|
|
244
|
+
if (typeof e.text === "string" && /^egress: \S+ \S+ (?:refused|unreachable|→ unreachable)\b/.test(e.text))
|
|
245
|
+
continue;
|
|
246
|
+
const m = typeof e.text === "string" ? /^egress: (?:CONNECT ([^\s:]+)|[A-Z]+ ([^\s/:]+))/.exec(e.text) : null;
|
|
247
|
+
const host = (m?.[1] ?? m?.[2])?.toLowerCase();
|
|
248
|
+
if (host)
|
|
249
|
+
hosts.add(host);
|
|
250
|
+
}
|
|
251
|
+
return [...hosts].sort();
|
|
252
|
+
}
|
|
253
|
+
/** `network_enforce: true` in the account's or the workspace's AGENTS.md:
|
|
254
|
+
* the steps here have a site off their list refused rather than only named.
|
|
255
|
+
* Either level switches it on; neither can switch off what the other, or the
|
|
256
|
+
* install (FOLDRUN_NETWORK_ENFORCE=1), switched on. */
|
|
257
|
+
export function networkEnforceWanted(account, workspace) {
|
|
258
|
+
return account === true || workspace === true;
|
|
259
|
+
}
|
|
236
260
|
export function networkLine(policy, enforced) {
|
|
237
261
|
const how = enforced === null
|
|
238
262
|
? " — not enforced here (no sandbox network)"
|
|
239
263
|
: enforced
|
|
240
264
|
? ""
|
|
241
|
-
: " — report only
|
|
265
|
+
: " — report only: a site off the list is allowed and named in the trace (network_enforce: true in AGENTS.md refuses it)";
|
|
242
266
|
if (policy.level === "open")
|
|
243
267
|
return `network: open (${policy.from}) — any public site${how}`;
|
|
244
268
|
const parts = [];
|
package/dist/src/notify.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/src/notify.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 "
|
|
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 {};
|