@foldrun/core 0.4.0 → 0.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/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/src/account.d.ts +45 -0
- package/dist/src/account.js +130 -0
- package/dist/src/api-tools.d.ts +8 -0
- package/dist/src/api-tools.js +22 -4
- package/dist/src/browser-pod.d.ts +129 -0
- package/dist/src/browser-pod.js +284 -0
- package/dist/src/catalog.js +2 -1
- package/dist/src/clock.d.ts +49 -0
- package/dist/src/clock.js +121 -0
- package/dist/src/completions.js +17 -8
- package/dist/src/confine.d.ts +22 -0
- package/dist/src/confine.js +218 -3
- package/dist/src/deploy.d.ts +33 -0
- package/dist/src/deploy.js +145 -6
- package/dist/src/egress.d.ts +7 -0
- package/dist/src/egress.js +6 -0
- package/dist/src/eol.d.ts +2 -0
- package/dist/src/eol.js +7 -0
- package/dist/src/evals.d.ts +5 -0
- package/dist/src/evals.js +6 -2
- package/dist/src/flow-lint.d.ts +8 -0
- package/dist/src/flow-lint.js +223 -1
- package/dist/src/flow-patterns.d.ts +141 -0
- package/dist/src/flow-patterns.js +568 -0
- package/dist/src/gitrepo.d.ts +18 -0
- package/dist/src/gitrepo.js +49 -0
- package/dist/src/kinds.js +1 -1
- package/dist/src/language.d.ts +17 -0
- package/dist/src/language.js +55 -0
- package/dist/src/layout.d.ts +69 -0
- package/dist/src/layout.js +200 -0
- package/dist/src/library.d.ts +9 -0
- package/dist/src/library.js +30 -3
- package/dist/src/limits.d.ts +84 -0
- package/dist/src/limits.js +287 -0
- package/dist/src/locale.d.ts +47 -0
- package/dist/src/locale.js +177 -0
- package/dist/src/mail.d.ts +88 -0
- package/dist/src/mail.js +245 -0
- package/dist/src/model-probe.d.ts +3 -0
- package/dist/src/model-probe.js +1 -0
- package/dist/src/notify.d.ts +53 -7
- package/dist/src/notify.js +194 -126
- package/dist/src/openapi.js +5 -3
- package/dist/src/operator.d.ts +201 -0
- package/dist/src/operator.js +251 -0
- package/dist/src/paths.d.ts +28 -0
- package/dist/src/paths.js +55 -0
- package/dist/src/platform.d.ts +43 -3
- package/dist/src/platform.js +9 -1
- package/dist/src/provider-health.js +3 -2
- package/dist/src/providers.d.ts +481 -0
- package/dist/src/providers.js +1105 -6
- package/dist/src/refusal.d.ts +83 -0
- package/dist/src/refusal.js +281 -0
- package/dist/src/run-container.d.ts +149 -13
- package/dist/src/run-container.js +403 -42
- package/dist/src/runner.d.ts +142 -9
- package/dist/src/runner.js +1027 -122
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +493 -58
- package/dist/src/script-tools.d.ts +3 -2
- package/dist/src/script-tools.js +35 -3
- package/dist/src/secret-files.d.ts +6 -5
- package/dist/src/secret-files.js +24 -11
- package/dist/src/secret-health.d.ts +30 -6
- package/dist/src/secret-health.js +42 -20
- package/dist/src/secrets.d.ts +19 -0
- package/dist/src/secrets.js +155 -86
- package/dist/src/starter.d.ts +9 -0
- package/dist/src/starter.js +92 -11
- package/dist/src/step-exec.d.ts +61 -2
- package/dist/src/step-exec.js +414 -36
- package/dist/src/step-removal.d.ts +15 -0
- package/dist/src/step-removal.js +49 -0
- package/dist/src/storage.d.ts +23 -4
- package/dist/src/storage.js +125 -23
- package/dist/src/store.d.ts +218 -37
- package/dist/src/store.js +347 -60
- package/dist/src/subagents.d.ts +75 -0
- package/dist/src/subagents.js +149 -0
- package/dist/src/tool-names.d.ts +13 -1
- package/dist/src/tool-names.js +38 -11
- package/dist/src/tool-programs.d.ts +26 -0
- package/dist/src/tool-programs.js +172 -0
- package/dist/src/tool-test.js +104 -12
- package/dist/src/translator.d.ts +8 -2
- package/dist/src/translator.js +69 -23
- package/dist/src/trigger-log.d.ts +1 -1
- package/dist/src/triggers.d.ts +3 -0
- package/dist/src/triggers.js +10 -2
- package/dist/src/untrusted-fetch.d.ts +11 -0
- package/dist/src/untrusted-fetch.js +77 -0
- package/dist/src/web-actions.d.ts +91 -0
- package/dist/src/web-actions.js +199 -0
- package/dist/src/webhook.d.ts +4 -0
- package/dist/src/webhook.js +39 -0
- package/index.ts +3 -0
- package/package.json +3 -3
- package/src/account.ts +149 -0
- package/src/api-tools.ts +32 -4
- package/src/browser-pod.ts +313 -0
- package/src/catalog.ts +2 -1
- package/src/clock.ts +149 -0
- package/src/completions.ts +17 -8
- package/src/confine.ts +218 -3
- package/src/deploy.ts +137 -6
- package/src/egress.ts +13 -1
- package/src/eol.ts +7 -0
- package/src/evals.ts +11 -2
- package/src/flow-lint.ts +253 -1
- package/src/flow-patterns.ts +597 -0
- package/src/gitrepo.ts +48 -0
- package/src/kinds.ts +1 -1
- package/src/language.ts +62 -0
- package/src/layout.ts +228 -0
- package/src/library.ts +37 -3
- package/src/limits.ts +290 -0
- package/src/locale.ts +217 -0
- package/src/mail.ts +316 -0
- package/src/model-probe.ts +4 -0
- package/src/notify.ts +189 -103
- package/src/openapi.ts +5 -3
- package/src/operator.ts +300 -0
- package/src/paths.ts +54 -0
- package/src/platform.ts +52 -3
- package/src/provider-health.ts +3 -2
- package/src/providers.ts +1368 -6
- package/src/refusal.ts +320 -0
- package/src/run-container.ts +439 -43
- package/src/runner.ts +1050 -122
- package/src/runtime.ts +487 -55
- package/src/script-tools.ts +38 -5
- package/src/secret-files.ts +22 -10
- package/src/secret-health.ts +64 -19
- package/src/secrets.ts +149 -87
- package/src/starter.ts +93 -11
- package/src/step-exec.ts +429 -37
- package/src/step-removal.ts +59 -0
- package/src/storage.ts +141 -22
- package/src/store.ts +500 -68
- package/src/subagents.ts +189 -0
- package/src/tool-names.ts +40 -11
- package/src/tool-programs.ts +179 -0
- package/src/tool-test.ts +110 -12
- package/src/translator.ts +72 -22
- package/src/trigger-log.ts +1 -0
- package/src/triggers.ts +10 -2
- package/src/untrusted-fetch.ts +88 -0
- package/src/web-actions.ts +237 -0
- package/src/webhook.ts +41 -0
- package/templates/hello/agents/curator/agent.md +1 -1
- package/templates/hello/agents/notetaker/agent.md +1 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// Slim browsing: a step whose only reason for the full runner image is that
|
|
2
|
+
// it browses may run on the slim image and drive the account's browser pod
|
|
3
|
+
// over Playwright's protocol. The pod can go away mid-step — a node drained,
|
|
4
|
+
// the idle reaper, an OOM — and a slim step has no browser of its own to fall
|
|
5
|
+
// back on. So:
|
|
6
|
+
//
|
|
7
|
+
// 1. The web tool reconnects first: a few tries, short backoff (the tool's
|
|
8
|
+
// run.mjs; each try is an event in FOLDRUN_BROWSER_POD_EVENTS).
|
|
9
|
+
// 2. Still gone: the step stops, and what it has done so far decides.
|
|
10
|
+
// Only reads — re-run the whole step from the start on the full image,
|
|
11
|
+
// and say so on the run ("browser pod lost; re-ran on full").
|
|
12
|
+
// 3. Anything written — fail the step: "browser pod died after the step had
|
|
13
|
+
// written <what>". Doing it again could send, post or charge twice, so
|
|
14
|
+
// retry:, on-fail: or a person decides.
|
|
15
|
+
//
|
|
16
|
+
// Read or write is decided from the calls the step made, as the model asked
|
|
17
|
+
// for them (step-exec.ts keeps the ledger). When a call could be either it
|
|
18
|
+
// counts as a write: the cost of being wrong that way is a failed step a
|
|
19
|
+
// retry can redo; the other way it is a duplicate email.
|
|
20
|
+
//
|
|
21
|
+
// Steps that act outward (a tool marked `outward: true`) never run slim; nor
|
|
22
|
+
// does anything the step start can already see needs a browser in the step
|
|
23
|
+
// (lightpanda, obscura, live sessions, extensions) — slimBrowseBlocker.
|
|
24
|
+
/** Set by the platform on a step it put on the slim image to browse
|
|
25
|
+
* through the pod. The web tool then never launches a browser of its own. */
|
|
26
|
+
export const POD_ONLY_ENV = "FOLDRUN_BROWSER_POD_ONLY";
|
|
27
|
+
/** Where the web tool appends one JSON line per reconnect, and the line
|
|
28
|
+
* that says the pod is lost or the call needs the full image. */
|
|
29
|
+
export const POD_EVENTS_ENV = "FOLDRUN_BROWSER_POD_EVENTS";
|
|
30
|
+
/** The file, inside the step's sandbox. */
|
|
31
|
+
export const POD_EVENTS_FILE = "/tmp/foldrun-browser-pod.jsonl";
|
|
32
|
+
/** The note the run carries when the fallback was taken. Worded once. */
|
|
33
|
+
export const RERAN_ON_FULL = "browser pod lost; re-ran on full";
|
|
34
|
+
/**
|
|
35
|
+
* Browse actions that can change something beyond the page in front of the
|
|
36
|
+
* model: anything that types, picks, submits, signs in, uploads, runs script
|
|
37
|
+
* or pays. A click is here because a click can submit and nothing before
|
|
38
|
+
* the click can tell. The web tool's run.mjs carries a copy (it decides
|
|
39
|
+
* whether a dropped call may be repeated); the gallery test holds the two
|
|
40
|
+
* equal.
|
|
41
|
+
*/
|
|
42
|
+
export const BROWSE_WRITE_ACTIONS = [
|
|
43
|
+
"click", "dblclick", "rightclick", "tap", "swipe", "mouse", "drag",
|
|
44
|
+
"fill", "type", "insert", "paste", "press", "keyboard", "keydown", "keyup", "clear",
|
|
45
|
+
"select", "check", "uncheck", "upload", "dialog", "login", "eval", "webmcp", "solve",
|
|
46
|
+
];
|
|
47
|
+
/** Call arguments of web action=browse that are writes whatever the actions:
|
|
48
|
+
* page script, a paid captcha solver, a confirmation of a credential step. */
|
|
49
|
+
const BROWSE_WRITE_ARGS = ["js", "captcha", "captcha_solver", "confirm"];
|
|
50
|
+
/** Web actions that only read — when foldrun's own answers them. A paid
|
|
51
|
+
* provider answering one (FOLDRUN_WEB_<ACTION>_VIA) makes it a write: doing
|
|
52
|
+
* it twice is paying twice. `monitor` keeps state, so it always writes. */
|
|
53
|
+
const WEB_READ_ACTIONS = new Set(["search", "fetch", "browse", "crawl", "map", "extract", "answer"]);
|
|
54
|
+
/** Built-ins that read and change nothing. */
|
|
55
|
+
const READ_BUILTINS = new Set(["Read", "Glob", "Grep", "WebSearch", "WebFetch", "TodoWrite", "LS", "NotebookRead"]);
|
|
56
|
+
/** The platform's own tool groups that only read. Consults are a toolless
|
|
57
|
+
* answer; a delegation's own calls come through on their own. */
|
|
58
|
+
const READ_SERVERS = new Set(["foldrun_search", "foldrun_history", "foldrun_desks", "foldrun_agents"]);
|
|
59
|
+
const DELEGATE = new Set(["Agent", "Task"]);
|
|
60
|
+
/** The browse actions a call asked for, nested `if` branches included. */
|
|
61
|
+
export function browseActionNames(actions) {
|
|
62
|
+
let steps = actions;
|
|
63
|
+
if (typeof steps === "string") {
|
|
64
|
+
try {
|
|
65
|
+
steps = JSON.parse(steps);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return ["(unreadable actions)"];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const out = [];
|
|
72
|
+
const walk = (v, depth) => {
|
|
73
|
+
if (depth > 8 || v === null || typeof v !== "object")
|
|
74
|
+
return;
|
|
75
|
+
if (Array.isArray(v)) {
|
|
76
|
+
for (const x of v)
|
|
77
|
+
walk(x, depth + 1);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const [k, x] of Object.entries(v)) {
|
|
81
|
+
out.push(k);
|
|
82
|
+
walk(x, depth + 1);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
walk(steps, 0);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/** What a browse call's arguments would write, or null when it only reads. */
|
|
89
|
+
export function browseWrites(input) {
|
|
90
|
+
const names = browseActionNames(input.actions);
|
|
91
|
+
if (names.includes("(unreadable actions)"))
|
|
92
|
+
return "browse actions it could not read";
|
|
93
|
+
const writes = [...new Set(names.filter((n) => BROWSE_WRITE_ACTIONS.includes(n)))];
|
|
94
|
+
if (writes.length)
|
|
95
|
+
return `browse ${writes.join("/")}`;
|
|
96
|
+
const arg = BROWSE_WRITE_ARGS.find((a) => input[a] !== undefined && input[a] !== null && input[a] !== "" && input[a] !== false);
|
|
97
|
+
if (arg)
|
|
98
|
+
return `browse ${arg}=`;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether one tool call can have changed anything outside the model's head,
|
|
103
|
+
* and if so, a few words saying what. Pure.
|
|
104
|
+
*/
|
|
105
|
+
export function classifyCall(tool, input, ctx = {}) {
|
|
106
|
+
const args = input ?? {};
|
|
107
|
+
const write = (what) => ({ write: true, what });
|
|
108
|
+
if (READ_BUILTINS.has(tool) || DELEGATE.has(tool))
|
|
109
|
+
return { write: false };
|
|
110
|
+
if (tool === "Write" || tool === "Edit" || tool === "MultiEdit" || tool === "NotebookEdit") {
|
|
111
|
+
const raw = String(args.file_path ?? args.notebook_path ?? args.path ?? "");
|
|
112
|
+
const file = raw.startsWith("/workspace/") ? raw.slice("/workspace/".length) : raw.split("/").slice(-3).join("/");
|
|
113
|
+
return write(file ? `${file} (${tool})` : `a file (${tool})`);
|
|
114
|
+
}
|
|
115
|
+
if (tool === "Bash")
|
|
116
|
+
return write("a shell command");
|
|
117
|
+
const mcp = /^mcp__(.+?)__(.+)$/.exec(tool);
|
|
118
|
+
if (!mcp)
|
|
119
|
+
return write(tool); // unknown: a write until shown otherwise
|
|
120
|
+
const [, server, name] = mcp;
|
|
121
|
+
if (READ_SERVERS.has(server))
|
|
122
|
+
return { write: false };
|
|
123
|
+
if (server === "foldrun_ask")
|
|
124
|
+
return write("a question to a person");
|
|
125
|
+
if (server === "foldrun_scripts") {
|
|
126
|
+
if (name !== "web")
|
|
127
|
+
return write(`script ${name}`);
|
|
128
|
+
const action = String(args.action ?? "").trim().toLowerCase();
|
|
129
|
+
if (!WEB_READ_ACTIONS.has(action))
|
|
130
|
+
return write(action === "monitor" ? "web monitor state" : `web ${action || "call"}`);
|
|
131
|
+
const paid = ctx.paidWeb?.[action];
|
|
132
|
+
if (paid)
|
|
133
|
+
return write(`a paid ${paid} ${action}`);
|
|
134
|
+
if (action === "browse") {
|
|
135
|
+
const w = browseWrites(args);
|
|
136
|
+
if (w)
|
|
137
|
+
return write(w);
|
|
138
|
+
}
|
|
139
|
+
return { write: false };
|
|
140
|
+
}
|
|
141
|
+
if (server === "foldrun_apis") {
|
|
142
|
+
const method = String(name.startsWith("call_") ? (args.method ?? "GET") : (ctx.methods?.[tool] ?? "")).toUpperCase();
|
|
143
|
+
const api = name.replace(/^call_/, "");
|
|
144
|
+
if (method === "GET" || method === "HEAD")
|
|
145
|
+
return { write: false };
|
|
146
|
+
return write(`${api} ${method || "call"}`);
|
|
147
|
+
}
|
|
148
|
+
// Any other MCP server: its tools could do anything.
|
|
149
|
+
return write(`${server} ${name}`);
|
|
150
|
+
}
|
|
151
|
+
/** Paid web actions, from a step's env (FOLDRUN_WEB_<ACTION>_VIA=provider). */
|
|
152
|
+
export function paidWebActions(env) {
|
|
153
|
+
const out = {};
|
|
154
|
+
for (const [k, v] of Object.entries(env)) {
|
|
155
|
+
const m = /^FOLDRUN_WEB_([A-Z]+)_VIA$/.exec(k);
|
|
156
|
+
if (m && v)
|
|
157
|
+
out[m[1].toLowerCase()] = v;
|
|
158
|
+
}
|
|
159
|
+
if (env.FOLDRUN_BROWSER_VENDOR)
|
|
160
|
+
out.browse = env.FOLDRUN_BROWSER_VENDOR;
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/** ", at 10:15:15Z" from a pod log line's `at`; empty when it has none. */
|
|
164
|
+
export function podAt(at) {
|
|
165
|
+
if (typeof at !== "string")
|
|
166
|
+
return "";
|
|
167
|
+
const t = Date.parse(at);
|
|
168
|
+
return Number.isNaN(t) ? "" : `, at ${new Date(t).toISOString().slice(11, 19)}Z`;
|
|
169
|
+
}
|
|
170
|
+
/** Lines of the pod log from byte `offset` on, and where the next read
|
|
171
|
+
* starts. A torn last line waits for the next read. */
|
|
172
|
+
export function readPodEvents(text, offset) {
|
|
173
|
+
const chunk = text.slice(offset);
|
|
174
|
+
const end = chunk.lastIndexOf("\n");
|
|
175
|
+
if (end < 0)
|
|
176
|
+
return { events: [], next: offset };
|
|
177
|
+
const events = [];
|
|
178
|
+
for (const line of chunk.slice(0, end).split("\n")) {
|
|
179
|
+
try {
|
|
180
|
+
const e = JSON.parse(line);
|
|
181
|
+
if (e && (e.kind === "reconnect" || e.kind === "lost" || e.kind === "needs-full"))
|
|
182
|
+
events.push(e);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// not ours
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { events, next: offset + end + 1 };
|
|
189
|
+
}
|
|
190
|
+
/** "a.md (Write), crm POST and 2 more". */
|
|
191
|
+
export function describeWrites(writes) {
|
|
192
|
+
const uniq = [...new Set(writes)];
|
|
193
|
+
if (uniq.length > 3)
|
|
194
|
+
return `${uniq.slice(0, 3).join(", ")} and ${uniq.length - 3} more`;
|
|
195
|
+
if (uniq.length <= 1)
|
|
196
|
+
return uniq[0] ?? "something";
|
|
197
|
+
return `${uniq.slice(0, -1).join(", ")} and ${uniq.at(-1)}`;
|
|
198
|
+
}
|
|
199
|
+
/** The decision, once the pod is gone: re-run on full, or fail and say why. */
|
|
200
|
+
export function podLossDecision(pod) {
|
|
201
|
+
const writes = pod.writes ?? [];
|
|
202
|
+
const cause = pod.lost?.cause === "needs-full" ? `a call needed a browser in the step (${pod.lost.detail})` : `browser pod lost (${pod.lost?.detail ?? "connection closed"})`;
|
|
203
|
+
if (!writes.length)
|
|
204
|
+
return { rerun: true, note: `${RERAN_ON_FULL} — ${cause}; the step had only read, so it ran again from the start on the full image` };
|
|
205
|
+
const head = pod.lost?.cause === "needs-full"
|
|
206
|
+
? `a call needed a browser in the step after the step had written ${describeWrites(writes)}`
|
|
207
|
+
: `browser pod died after the step had written ${describeWrites(writes)}`;
|
|
208
|
+
return { rerun: false, message: `${head} — not re-run, since doing it again could repeat that; retry:, on-fail: or a person decides (${pod.lost?.detail ?? "connection closed"})` };
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Why a browsing step must have a browser in its own sandbox, from what the
|
|
212
|
+
* step start can see (the agent's `web.browse` block, as env), or null when
|
|
213
|
+
* the account's pod can serve every call it could make by default. A call
|
|
214
|
+
* that picks an in-step engine itself is caught at the call (needs-full).
|
|
215
|
+
*/
|
|
216
|
+
export function slimBrowseBlocker(env) {
|
|
217
|
+
if (env.FOLDRUN_BROWSER_VENDOR)
|
|
218
|
+
return `web.browse names ${env.FOLDRUN_BROWSER_VENDOR}`;
|
|
219
|
+
if (env.FOLDRUN_BROWSER_LIVE === "1")
|
|
220
|
+
return "web.browse live: sessions run in the step";
|
|
221
|
+
const engine = (env.FOLDRUN_BROWSER_ENGINE ?? "").trim().toLowerCase();
|
|
222
|
+
if (engine === "lightpanda" || engine === "obscura")
|
|
223
|
+
return `engine ${engine} runs in the step`;
|
|
224
|
+
if (env.FOLDRUN_BROWSER_EXTENSIONS)
|
|
225
|
+
return "extensions load into a browser in the step";
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
/** The outcome as it crosses back from the sandbox, checked field by field
|
|
229
|
+
* (the done line is the pod's word); null when absent or not this shape. */
|
|
230
|
+
export function parseBrowserPod(v) {
|
|
231
|
+
if (!v || typeof v !== "object")
|
|
232
|
+
return null;
|
|
233
|
+
const o = v;
|
|
234
|
+
const n = (x) => (typeof x === "number" && Number.isFinite(x) && x >= 0 ? Math.floor(x) : 0);
|
|
235
|
+
const out = { reconnects: n(o.reconnects), reconnected: n(o.reconnected) };
|
|
236
|
+
if (n(o.closedAgain))
|
|
237
|
+
out.closedAgain = n(o.closedAgain);
|
|
238
|
+
const lost = o.lost;
|
|
239
|
+
if (lost && typeof lost === "object" && (lost.cause === "lost" || lost.cause === "needs-full")) {
|
|
240
|
+
out.lost = { cause: lost.cause, detail: String(lost.detail ?? "").slice(0, 300) };
|
|
241
|
+
out.writes = Array.isArray(o.writes) ? o.writes.slice(0, 50).map((w) => String(w).slice(0, 120)) : [];
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
/** One line for a person: "2 reconnects (2 got through)", "browser pod
|
|
246
|
+
* lost; re-ran on full", or the failure. What the run page, `foldrun
|
|
247
|
+
* report` and an attempt row say. A pod that was lost in the end is never
|
|
248
|
+
* said to have let a reconnect through — "1 reconnect (1 got through); pod
|
|
249
|
+
* lost" read live as a contradiction — and a reconnect that reached the pod
|
|
250
|
+
* only for the call to drop again says that. */
|
|
251
|
+
export function browserPodLine(p) {
|
|
252
|
+
const count = `${p.reconnects} reconnect${p.reconnects === 1 ? "" : "s"}`;
|
|
253
|
+
const tries = !p.reconnects
|
|
254
|
+
? "no reconnects"
|
|
255
|
+
: !p.lost
|
|
256
|
+
? `${count} (${p.reconnected} got through)`
|
|
257
|
+
: p.closedAgain
|
|
258
|
+
? `${count}, ${p.reconnects === 1 ? "which" : "the last"} reached the pod but it closed again`
|
|
259
|
+
: count;
|
|
260
|
+
if (p.failure)
|
|
261
|
+
return `${tries}; ${p.failure}`;
|
|
262
|
+
if (p.fallback)
|
|
263
|
+
return `${tries}; ${p.fallback}${p.lost ? ` (${p.lost.detail})` : ""}`;
|
|
264
|
+
if (p.lost)
|
|
265
|
+
return `${tries}; ${p.lost.cause === "needs-full" ? "needed a browser in the step" : "pod lost"}: ${p.lost.detail}`;
|
|
266
|
+
return tries;
|
|
267
|
+
}
|
|
268
|
+
/** A step's tries in one line when the browser pod was lost under one of
|
|
269
|
+
* them — "slim · lost · $0.0040 → full · completed · $0.0060" — so the run
|
|
270
|
+
* page and `foldrun report` show the slim go and the full re-run apart.
|
|
271
|
+
* `#n` leads each try when the step had more than one attempt. Null when
|
|
272
|
+
* no try was lost. */
|
|
273
|
+
export function podTriesLine(tries) {
|
|
274
|
+
if (!tries?.some((t) => t.status === "lost"))
|
|
275
|
+
return null;
|
|
276
|
+
const many = new Set(tries.map((t) => t.n)).size > 1;
|
|
277
|
+
return tries
|
|
278
|
+
.map((t) => [
|
|
279
|
+
`${many ? `#${t.n} ` : ""}${t.image ?? "?"}`,
|
|
280
|
+
t.status,
|
|
281
|
+
typeof t.costUsd === "number" ? `$${t.costUsd.toFixed(4)}` : null,
|
|
282
|
+
].filter(Boolean).join(" · "))
|
|
283
|
+
.join(" → ");
|
|
284
|
+
}
|
package/dist/src/catalog.js
CHANGED
|
@@ -22,6 +22,7 @@ import path from "node:path";
|
|
|
22
22
|
import { dataRoot } from "./paths.js";
|
|
23
23
|
import { resolveModel, resolveTier, MODEL_TIERS, EFFORT_LEVELS } from "./store.js";
|
|
24
24
|
import { trimSlashes } from "./paths.js";
|
|
25
|
+
import { fetchUntrusted } from "./untrusted-fetch.js";
|
|
25
26
|
/** A day. Model lists move weekly, not hourly, and a stale price in an
|
|
26
27
|
* autocomplete hint is a smaller failure than a fetch on every keystroke. */
|
|
27
28
|
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -84,7 +85,7 @@ export async function loadCatalog(baseUrl) {
|
|
|
84
85
|
if (cached && Date.now() - Date.parse(cached.fetchedAt) < TTL_MS)
|
|
85
86
|
return cached;
|
|
86
87
|
try {
|
|
87
|
-
const res = await
|
|
88
|
+
const res = await fetchUntrusted(`${trimSlashes(baseUrl)}/v1/models`, {
|
|
88
89
|
signal: AbortSignal.timeout(10_000),
|
|
89
90
|
});
|
|
90
91
|
if (!res.ok)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** A zone the runtime can actually use: the name Intl understands, and the
|
|
2
|
+
* value to put in `TZ` for the shell and Node inside the sandbox. They are
|
|
3
|
+
* the same for an IANA name and differ for a fixed offset. */
|
|
4
|
+
export interface Zone {
|
|
5
|
+
/** What `Intl.DateTimeFormat({ timeZone })` is given. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** What `TZ` is set to. */
|
|
8
|
+
tz: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read a written timezone into something usable, or null if nothing can be.
|
|
12
|
+
*
|
|
13
|
+
* Two shapes are accepted, because both are what people type: an IANA name
|
|
14
|
+
* (`Australia/Sydney`) and a plain fixed offset (`UTC+10`, `+10:00`,
|
|
15
|
+
* `-05:30`, `GMT+5`, or `UTC` on its own).
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeZone(raw: unknown): Zone | null;
|
|
18
|
+
/** Is this a value `timezone:` may carry? Used by the lint, so `foldrun
|
|
19
|
+
* check` refuses `Sydney/Australia` and `UTC+99` before a run does. */
|
|
20
|
+
export declare function isTimezone(raw: unknown): boolean;
|
|
21
|
+
/** The lint's sentence for a `timezone:` nobody can read. */
|
|
22
|
+
export declare function timezoneProblem(raw: unknown): string | null;
|
|
23
|
+
/** One level of the cascade: where the value was written, and what it said. */
|
|
24
|
+
export interface ZoneLevel {
|
|
25
|
+
/** The word the trace uses: "agent", "flow", "workspace", "account". */
|
|
26
|
+
level: string;
|
|
27
|
+
value: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface ClockChoice {
|
|
30
|
+
/** The zone, ready for Intl. */
|
|
31
|
+
timezone: string;
|
|
32
|
+
/** The value for `TZ`. */
|
|
33
|
+
tz: string;
|
|
34
|
+
/** Which level set it — or "FOLDRUN_TIMEZONE", or "default". */
|
|
35
|
+
source: string;
|
|
36
|
+
/** One sentence per unusable value found on the way down. Reported as a
|
|
37
|
+
* step event; never thrown, because a mistyped zone must not stop work. */
|
|
38
|
+
problems: string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Walk the levels, nearest first, and take the first zone that works.
|
|
42
|
+
*
|
|
43
|
+
* `FOLDRUN_TIMEZONE` is appended by the caller's level list or read here as
|
|
44
|
+
* the last written level; UTC is what remains when nothing said anything —
|
|
45
|
+
* the clock the pods actually run on.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveClock(levels: ZoneLevel[], env?: string | undefined): ClockChoice;
|
|
48
|
+
/** YYYY-MM-DD in a timezone — the date the step believes it is. */
|
|
49
|
+
export declare function localDate(timeZone: string, now?: Date): string;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// The clock an agent works to.
|
|
2
|
+
//
|
|
3
|
+
// A step used to run on whatever the pod's clock said, which is UTC, while
|
|
4
|
+
// the flow that fired it was scheduled in Australia/Sydney. A run at 08:00
|
|
5
|
+
// Sydney on the 16th wrote files stamped the 15th and the next step, looking
|
|
6
|
+
// for today's files, found none. The schedule and the work disagreed about
|
|
7
|
+
// what day it was.
|
|
8
|
+
//
|
|
9
|
+
// So the calendar is resolvable at every level, nearest wins:
|
|
10
|
+
//
|
|
11
|
+
// agent frontmatter → flow frontmatter → workspace AGENTS.md →
|
|
12
|
+
// account AGENTS.md → FOLDRUN_TIMEZONE → UTC
|
|
13
|
+
//
|
|
14
|
+
// Leaving `timezone:` out inherits the next level up; setting it anywhere
|
|
15
|
+
// wins for that level and everything under it. A value nothing can read
|
|
16
|
+
// never fails a step — it falls through to the next level and says so once.
|
|
17
|
+
/** `UTC+10`, `GMT-3:30`, `+10:00`, `-0530`, or a bare `UTC`/`GMT`. */
|
|
18
|
+
const OFFSET_RE = /^(?:UTC|GMT)?\s*(?:([+-])\s*(\d{1,2})(?::?(\d{2}))?)?$/i;
|
|
19
|
+
/**
|
|
20
|
+
* Read a written timezone into something usable, or null if nothing can be.
|
|
21
|
+
*
|
|
22
|
+
* Two shapes are accepted, because both are what people type: an IANA name
|
|
23
|
+
* (`Australia/Sydney`) and a plain fixed offset (`UTC+10`, `+10:00`,
|
|
24
|
+
* `-05:30`, `GMT+5`, or `UTC` on its own).
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeZone(raw) {
|
|
27
|
+
const text = typeof raw === "string" ? raw.trim() : "";
|
|
28
|
+
if (!text)
|
|
29
|
+
return null;
|
|
30
|
+
const offset = text.match(OFFSET_RE);
|
|
31
|
+
if (offset) {
|
|
32
|
+
const [, sign, hh, mm] = offset;
|
|
33
|
+
// A bare UTC/GMT, or the empty match — no offset at all.
|
|
34
|
+
if (!sign)
|
|
35
|
+
return { name: "UTC", tz: "UTC" };
|
|
36
|
+
const hours = Number(hh);
|
|
37
|
+
const minutes = Number(mm ?? "0");
|
|
38
|
+
// The real range of civil offsets. `UTC+99` is a typo, not a zone.
|
|
39
|
+
if (hours > 14 || minutes > 59)
|
|
40
|
+
return null;
|
|
41
|
+
if (hours === 14 && minutes > 0)
|
|
42
|
+
return null;
|
|
43
|
+
const total = hours * 60 + minutes;
|
|
44
|
+
if (total === 0)
|
|
45
|
+
return { name: "UTC", tz: "UTC" };
|
|
46
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
47
|
+
const name = `${sign}${pad(hours)}:${pad(minutes)}`; // Intl reads this form
|
|
48
|
+
if (minutes === 0) {
|
|
49
|
+
// Etc/GMT zones have their sign INVERTED — POSIX counts degrees west of
|
|
50
|
+
// Greenwich as positive, so Etc/GMT-10 is ten hours AHEAD of UTC. It is
|
|
51
|
+
// worth the confusion: `Etc/GMT-10` is understood by ICU (so Node in
|
|
52
|
+
// the sandbox honours it) and by the shell's date, which the offset
|
|
53
|
+
// form `+10:00` is not.
|
|
54
|
+
return { name, tz: `Etc/GMT${sign === "+" ? "-" : "+"}${hours}` };
|
|
55
|
+
}
|
|
56
|
+
// No Etc zone exists for a half-hour offset, so `TZ` gets the POSIX
|
|
57
|
+
// spec — which the shell reads, and whose sign is inverted for the same
|
|
58
|
+
// reason. FOLDRUN_DATE is computed from `name` either way, so the date
|
|
59
|
+
// an agent is told is right whatever TZ a program makes of it.
|
|
60
|
+
const posix = `${sign === "+" ? "-" : "+"}${pad(hours)}:${pad(minutes)}`;
|
|
61
|
+
return { name, tz: `<${sign}${pad(hours)}${pad(minutes)}>${posix}` };
|
|
62
|
+
}
|
|
63
|
+
// Not an offset: an IANA name, if the platform knows it.
|
|
64
|
+
try {
|
|
65
|
+
new Intl.DateTimeFormat("en-CA", { timeZone: text });
|
|
66
|
+
return { name: text, tz: text };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Is this a value `timezone:` may carry? Used by the lint, so `foldrun
|
|
73
|
+
* check` refuses `Sydney/Australia` and `UTC+99` before a run does. */
|
|
74
|
+
export function isTimezone(raw) {
|
|
75
|
+
return normalizeZone(raw) !== null;
|
|
76
|
+
}
|
|
77
|
+
/** The lint's sentence for a `timezone:` nobody can read. */
|
|
78
|
+
export function timezoneProblem(raw) {
|
|
79
|
+
if (raw === undefined || raw === null || raw === "")
|
|
80
|
+
return null;
|
|
81
|
+
if (isTimezone(raw))
|
|
82
|
+
return null;
|
|
83
|
+
return (`timezone: ${String(raw)} — not a zone; use an IANA name (Australia/Sydney) ` +
|
|
84
|
+
`or a fixed offset (UTC+10, +10:00, -05:30)`);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Walk the levels, nearest first, and take the first zone that works.
|
|
88
|
+
*
|
|
89
|
+
* `FOLDRUN_TIMEZONE` is appended by the caller's level list or read here as
|
|
90
|
+
* the last written level; UTC is what remains when nothing said anything —
|
|
91
|
+
* the clock the pods actually run on.
|
|
92
|
+
*/
|
|
93
|
+
export function resolveClock(levels, env = process.env.FOLDRUN_TIMEZONE) {
|
|
94
|
+
const problems = [];
|
|
95
|
+
const bad = [];
|
|
96
|
+
const all = [...levels, { level: "FOLDRUN_TIMEZONE", value: env }];
|
|
97
|
+
for (const { level, value } of all) {
|
|
98
|
+
const written = typeof value === "string" ? value.trim() : "";
|
|
99
|
+
if (!written)
|
|
100
|
+
continue;
|
|
101
|
+
const zone = normalizeZone(written);
|
|
102
|
+
if (zone) {
|
|
103
|
+
for (const b of bad)
|
|
104
|
+
problems.push(`${b} — using ${zone.name} (${level}) instead`);
|
|
105
|
+
return { timezone: zone.name, tz: zone.tz, source: level, problems };
|
|
106
|
+
}
|
|
107
|
+
bad.push(`timezone: "${written}" (${level}) is not a zone this runtime knows`);
|
|
108
|
+
}
|
|
109
|
+
for (const b of bad)
|
|
110
|
+
problems.push(`${b} — using UTC instead`);
|
|
111
|
+
return { timezone: "UTC", tz: "UTC", source: "default", problems };
|
|
112
|
+
}
|
|
113
|
+
/** YYYY-MM-DD in a timezone — the date the step believes it is. */
|
|
114
|
+
export function localDate(timeZone, now = new Date()) {
|
|
115
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
116
|
+
timeZone,
|
|
117
|
+
year: "numeric",
|
|
118
|
+
month: "2-digit",
|
|
119
|
+
day: "2-digit",
|
|
120
|
+
}).format(now);
|
|
121
|
+
}
|
package/dist/src/completions.js
CHANGED
|
@@ -28,11 +28,14 @@ const EFFORTS = [
|
|
|
28
28
|
// survive vendor renames — then the exact SDK names, accepted so a Claude Code
|
|
29
29
|
// subagent runs here unchanged.
|
|
30
30
|
const TOOL_GROUPS = [
|
|
31
|
+
// `files`, `bash` and `fetch` are retired (tool-names.ts) and not offered.
|
|
32
|
+
// `web` is our gallery tool, offered here because it is the one way onto
|
|
33
|
+
// the web.
|
|
31
34
|
{ label: "read", hint: "Read, Glob, Grep — may look, never write" },
|
|
32
|
-
{ label: "
|
|
33
|
-
{ label: "
|
|
34
|
-
{ label: "
|
|
35
|
-
{ label: "
|
|
35
|
+
{ label: "write", hint: "Read, Write, Edit, Glob, Grep" },
|
|
36
|
+
{ label: "code", hint: "Bash — runs anything in the sandbox" },
|
|
37
|
+
{ label: "web", hint: "search, fetch, browse, crawl, map, extract, answer, monitor — providers under web:" },
|
|
38
|
+
{ label: "ask", hint: "ask_person — ask the person running the desk mid-step, and wait for the answer" },
|
|
36
39
|
];
|
|
37
40
|
const SDK_TOOL_NAMES = [
|
|
38
41
|
{ label: "Read", hint: "exact SDK name" },
|
|
@@ -41,8 +44,6 @@ const SDK_TOOL_NAMES = [
|
|
|
41
44
|
{ label: "Glob", hint: "exact SDK name" },
|
|
42
45
|
{ label: "Grep", hint: "exact SDK name" },
|
|
43
46
|
{ label: "Bash", hint: "exact SDK name" },
|
|
44
|
-
{ label: "WebSearch", hint: "exact SDK name" },
|
|
45
|
-
{ label: "WebFetch", hint: "exact SDK name" },
|
|
46
47
|
];
|
|
47
48
|
const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"].map((m) => ({ label: m }));
|
|
48
49
|
const TRIGGERS = [
|
|
@@ -85,14 +86,17 @@ const FIELDS = {
|
|
|
85
86
|
{ label: "description", insert: "description: ", hint: "required — what it does, and when" },
|
|
86
87
|
{ label: "model", insert: "model: default" },
|
|
87
88
|
{ label: "effort", insert: "effort: high", hint: "how hard it thinks" },
|
|
88
|
-
{ label: "tools", insert: "tools:\n -
|
|
89
|
+
{ label: "tools", insert: "tools:\n - write", hint: "what the runtime gives it" },
|
|
89
90
|
{ label: "secrets", insert: "secrets:\n - ", hint: "credentials it may spend" },
|
|
90
91
|
{ label: "skills", insert: "skills:\n - ", hint: "allowlist; omit to inherit all" },
|
|
91
92
|
{ label: "scripts", insert: "scripts:\n - name: \n run: \n description: " },
|
|
92
93
|
{ label: "runtime", insert: "runtime:\n python: \"3\"\n packages: []" },
|
|
93
94
|
{ label: "mcpServers", insert: "mcpServers:\n " },
|
|
95
|
+
{ label: "ask", insert: "ask:\n timeout: 30m", hint: "tools: [ask] — how long ask_person waits for a person (max 24h)" },
|
|
94
96
|
{ label: "permissionMode", insert: "permissionMode: plan", hint: "plan = read-only" },
|
|
95
97
|
{ label: "disallowedTools", insert: "disallowedTools:\n - " },
|
|
98
|
+
{ label: "subagents", insert: "subagents:\n - ", hint: "colleagues it may delegate a job to — own context, own tools, no wider than its own" },
|
|
99
|
+
{ label: "limits", insert: "limits:\n web.search: 40\n calls: 300", hint: "per step: a call past a limit is refused, never run" },
|
|
96
100
|
{
|
|
97
101
|
label: "provider",
|
|
98
102
|
insert: "provider:\n name: \n token: ${PROVIDER_TOKEN}",
|
|
@@ -151,6 +155,8 @@ const FIELDS = {
|
|
|
151
155
|
{ label: "trigger", insert: "trigger: manual" },
|
|
152
156
|
{ label: "schedule", insert: "schedule: \"0 6 * * MON\"" },
|
|
153
157
|
{ label: "timezone", insert: "timezone: Australia/Sydney" },
|
|
158
|
+
{ label: "language", insert: "language: en-AU" },
|
|
159
|
+
{ label: "region", insert: "region: au", hint: "currency, units, calendar, weekend and text direction follow" },
|
|
154
160
|
{ label: "model", insert: "model: fast", hint: "every step, unless the step says" },
|
|
155
161
|
{ label: "effort", insert: "effort: high", hint: "every step, unless the step says" },
|
|
156
162
|
],
|
|
@@ -475,7 +481,7 @@ export function completionsAt(path, text, cursor, vocab) {
|
|
|
475
481
|
each: [
|
|
476
482
|
{ label: "lines", hint: "one instance per line of the previous result" },
|
|
477
483
|
{ label: "items", hint: "one per element of the previous output: json array" },
|
|
478
|
-
{ label: "rows of
|
|
484
|
+
{ label: "rows of workspace/storage/x.csv", hint: "one per CSV row" },
|
|
479
485
|
],
|
|
480
486
|
};
|
|
481
487
|
const items = byKey[key];
|
|
@@ -500,6 +506,9 @@ export function completionsAt(path, text, cursor, vocab) {
|
|
|
500
506
|
],
|
|
501
507
|
secrets: vocab.secrets.map((s) => ({ label: s, hint: "secret" })),
|
|
502
508
|
skills: vocab.skills.map((s) => ({ label: s, hint: "skill" })),
|
|
509
|
+
// Colleagues: a consult (agents:) or a delegate (subagents:).
|
|
510
|
+
agents: vocab.agents.map((a) => ({ label: a, hint: "consult — answers, no tools" })),
|
|
511
|
+
subagents: vocab.agents.map((a) => ({ label: a, hint: "delegate — own context and tools" })),
|
|
503
512
|
disallowedTools: [...TOOL_GROUPS, { label: "Bash" }, { label: "Write" }],
|
|
504
513
|
when: [{ label: "schema" }, { label: "urgent" }],
|
|
505
514
|
expect: ASSERTIONS.map((a) => ({ ...a, insert: `${a.label}: ` })),
|
package/dist/src/confine.d.ts
CHANGED
|
@@ -22,6 +22,16 @@ export declare const isFilesystemTool: (toolName: string) => boolean;
|
|
|
22
22
|
* enforced; it is advice in the error text.
|
|
23
23
|
*/
|
|
24
24
|
export declare function suggestPath(raw: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* A path an author wrote in a flow or an agent wrote in a tool call, as an
|
|
27
|
+
* absolute path: `workspace/…` from the workspace root, anything else from
|
|
28
|
+
* the agent's own folder (so the older `../../storage/x` lands in the same
|
|
29
|
+
* place as `workspace/storage/x`). Confinement is the caller's: this only
|
|
30
|
+
* says where the path points.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveAgentPath(workspaceRoot: string, agentDir: string, raw: string): string;
|
|
33
|
+
/** True when `abs` is `root` or under it. */
|
|
34
|
+
export declare function isWithin(root: string, abs: string): boolean;
|
|
25
35
|
/** Expand a virtual prefix to a real absolute path, or null if it has none. */
|
|
26
36
|
export declare function expandVirtual(raw: string, roots: Roots): {
|
|
27
37
|
abs: string;
|
|
@@ -29,3 +39,15 @@ export declare function expandVirtual(raw: string, roots: Roots): {
|
|
|
29
39
|
} | null;
|
|
30
40
|
export declare function checkPaths(toolName: string, input: Record<string, unknown>, roots: Roots): ConfineVerdict;
|
|
31
41
|
export declare function checkBash(command: string): ConfineVerdict;
|
|
42
|
+
export declare function linkWorkspace(agentDir: string, workspaceRoot: string): {
|
|
43
|
+
release: () => void;
|
|
44
|
+
note: string | null;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* `env` plus what makes `node workspace/…` its own main module (above):
|
|
48
|
+
* the link's path(s) in FOLDRUN_WORKSPACE_LINK, the preload appended to
|
|
49
|
+
* NODE_OPTIONS — after what it held, or the host's when it held nothing.
|
|
50
|
+
* Idempotent, and harmless where no link exists: the preload matches only
|
|
51
|
+
* a path under it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function workspaceLinkEnv<T extends Record<string, string | undefined>>(env: T, agentDir: string): T;
|