@foldrun/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -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 +148 -0
- package/dist/src/browser-pod.js +333 -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/container.d.ts +9 -0
- package/dist/src/container.js +115 -5
- package/dist/src/context-tools.d.ts +1 -1
- 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 +9 -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/host-env.js +5 -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 +247 -0
- package/dist/src/model-credential.d.ts +14 -0
- package/dist/src/model-credential.js +58 -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 +424 -43
- package/dist/src/runner.d.ts +177 -9
- package/dist/src/runner.js +1146 -154
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +493 -58
- package/dist/src/script-tools.d.ts +17 -2
- package/dist/src/script-tools.js +82 -10
- 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 +72 -2
- package/dist/src/step-exec.js +431 -37
- 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 +222 -37
- package/dist/src/store.js +349 -62
- package/dist/src/subagents.d.ts +75 -0
- package/dist/src/subagents.js +149 -0
- package/dist/src/test-mode.d.ts +17 -2
- package/dist/src/test-mode.js +53 -13
- 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 +4 -0
- package/package.json +3 -3
- package/src/account.ts +149 -0
- package/src/api-tools.ts +32 -4
- package/src/browser-pod.ts +350 -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/container.ts +112 -5
- package/src/context-tools.ts +1 -1
- package/src/deploy.ts +137 -6
- package/src/egress.ts +13 -1
- package/src/eol.ts +7 -0
- package/src/evals.ts +14 -2
- package/src/flow-lint.ts +253 -1
- package/src/flow-patterns.ts +597 -0
- package/src/gitrepo.ts +48 -0
- package/src/host-env.ts +5 -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 +318 -0
- package/src/model-credential.ts +71 -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 +460 -44
- package/src/runner.ts +1168 -155
- package/src/runtime.ts +487 -55
- package/src/script-tools.ts +81 -10
- 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 +447 -38
- package/src/step-removal.ts +59 -0
- package/src/storage.ts +141 -22
- package/src/store.ts +506 -70
- package/src/subagents.ts +189 -0
- package/src/test-mode.ts +51 -11
- 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/blog-desk/agents/writer/agent.md +1 -1
- package/templates/hello/agents/curator/agent.md +4 -4
- package/templates/hello/agents/notetaker/agent.md +3 -3
package/dist/src/secrets.js
CHANGED
|
@@ -22,6 +22,7 @@ import fs from "node:fs";
|
|
|
22
22
|
import path from "node:path";
|
|
23
23
|
import { dataRoot } from "./paths.js";
|
|
24
24
|
import { platform } from "./platform.js";
|
|
25
|
+
import { fetchUntrusted } from "./untrusted-fetch.js";
|
|
25
26
|
/**
|
|
26
27
|
* GCM's auth tag, pinned to its full 16 bytes at every cipher call. Without
|
|
27
28
|
* `authTagLength`, Node accepts whatever length `setAuthTag` is handed — 4, 8,
|
|
@@ -75,20 +76,83 @@ function secretsFile(tenant, workspace) {
|
|
|
75
76
|
return path.join(base, "secrets.json");
|
|
76
77
|
}
|
|
77
78
|
function read(tenant, workspace) {
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
return readFile(secretsFile(tenant, workspace));
|
|
80
|
+
}
|
|
81
|
+
// ------------------------------------------------------------ safe writes
|
|
82
|
+
//
|
|
83
|
+
// A vault file is read, changed and written back. It was written in place,
|
|
84
|
+
// with no lock: two saves at once kept one; a crash mid-write left half a
|
|
85
|
+
// file, which is an account that cannot authenticate anything; and the boot
|
|
86
|
+
// re-wrap ran in every pod at once through ONE shared temp name, so two pods
|
|
87
|
+
// could interleave into it and rename a corrupt file into place.
|
|
88
|
+
//
|
|
89
|
+
// Now every change goes through mutateVaultFile: an exclusive lock file
|
|
90
|
+
// (O_EXCL, which NFS 4.1 and EFS honour, so it holds across pods), the read
|
|
91
|
+
// and the change under it, a temp file unique to this writer, and a rename —
|
|
92
|
+
// atomic on the same filesystem, so a reader sees the old file or the new
|
|
93
|
+
// one, never half. A lock older than LOCK_STALE_MS is a crashed holder.
|
|
94
|
+
const LOCK_STALE_MS = 30_000;
|
|
95
|
+
const LOCK_WAIT_MS = 10_000;
|
|
96
|
+
const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
97
|
+
function readFile(file) {
|
|
98
|
+
if (!fs.existsSync(file))
|
|
80
99
|
return {};
|
|
81
100
|
try {
|
|
82
|
-
return JSON.parse(fs.readFileSync(
|
|
101
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
83
102
|
}
|
|
84
103
|
catch {
|
|
85
104
|
return {};
|
|
86
105
|
}
|
|
87
106
|
}
|
|
88
|
-
function
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
107
|
+
function lockVault(file) {
|
|
108
|
+
const lock = `${file}.lock`;
|
|
109
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
110
|
+
for (;;) {
|
|
111
|
+
try {
|
|
112
|
+
const fd = fs.openSync(lock, "wx", 0o600);
|
|
113
|
+
fs.writeSync(fd, `${process.pid}@${new Date().toISOString()}`);
|
|
114
|
+
fs.closeSync(fd);
|
|
115
|
+
return () => fs.rmSync(lock, { force: true });
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (err.code !== "EEXIST")
|
|
119
|
+
throw err;
|
|
120
|
+
try {
|
|
121
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
122
|
+
fs.rmSync(lock, { force: true }); // a holder that died; take it over
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
continue; // released between the open and the stat
|
|
128
|
+
}
|
|
129
|
+
if (Date.now() > deadline)
|
|
130
|
+
throw new Error("the vault is busy — another change is being saved; try again");
|
|
131
|
+
sleep(25);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Change one vault file safely: `change` gets what is stored now and returns
|
|
137
|
+
* the new contents, or null to leave the file untouched. Returns what is
|
|
138
|
+
* stored after.
|
|
139
|
+
*/
|
|
140
|
+
export function mutateVaultFile(file, change) {
|
|
141
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
142
|
+
const release = lockVault(file);
|
|
143
|
+
try {
|
|
144
|
+
const current = readFile(file);
|
|
145
|
+
const next = change(structuredClone(current));
|
|
146
|
+
if (next === null)
|
|
147
|
+
return current;
|
|
148
|
+
const tmp = `${file}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
|
|
149
|
+
fs.writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
150
|
+
fs.renameSync(tmp, file);
|
|
151
|
+
return next;
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
release();
|
|
155
|
+
}
|
|
92
156
|
}
|
|
93
157
|
export const SECRET_NAME = /^[A-Z][A-Z0-9_]{1,63}$/;
|
|
94
158
|
export function setSecret(tenant, name, value, workspace, kind) {
|
|
@@ -98,13 +162,14 @@ export function setSecret(tenant, name, value, workspace, kind) {
|
|
|
98
162
|
if (value.length > 8192)
|
|
99
163
|
throw new Error("secret too long");
|
|
100
164
|
const sealed = encryptValue(tenant, value);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
165
|
+
mutateVaultFile(secretsFile(tenant, workspace), (all) => {
|
|
166
|
+
all[name] = {
|
|
167
|
+
...sealed,
|
|
168
|
+
updatedAt: new Date().toISOString(),
|
|
169
|
+
...(kind ? { kind } : {}),
|
|
170
|
+
};
|
|
171
|
+
return all;
|
|
172
|
+
});
|
|
108
173
|
}
|
|
109
174
|
/**
|
|
110
175
|
* Which key writes this account's records.
|
|
@@ -190,9 +255,12 @@ export function getSecret(tenant, name, workspace) {
|
|
|
190
255
|
return shared === null ? null : { value: shared, scope: "account" };
|
|
191
256
|
}
|
|
192
257
|
export function deleteSecret(tenant, name, workspace) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
258
|
+
mutateVaultFile(secretsFile(tenant, workspace), (all) => {
|
|
259
|
+
if (!(name in all))
|
|
260
|
+
return null;
|
|
261
|
+
delete all[name];
|
|
262
|
+
return all;
|
|
263
|
+
});
|
|
196
264
|
}
|
|
197
265
|
// Metadata only — never values. Without a workspace this lists the account
|
|
198
266
|
// store; with one it lists what that workspace actually resolves, marking
|
|
@@ -288,47 +356,47 @@ export function rotateMasterKey(tenant, from, to) {
|
|
|
288
356
|
const rewritten = [];
|
|
289
357
|
const unreadable = [];
|
|
290
358
|
for (const { file } of secretFiles(tenant)) {
|
|
291
|
-
const all = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
292
|
-
const next = {};
|
|
293
359
|
let count = 0;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
360
|
+
mutateVaultFile(file, (all) => {
|
|
361
|
+
const next = {};
|
|
362
|
+
for (const [name, rec] of Object.entries(all)) {
|
|
363
|
+
// A record under the ACCOUNT's key is not under the install key, so
|
|
364
|
+
// rotating the install key must not touch it — re-encrypting it here
|
|
365
|
+
// with a key it was never sealed with is how a rotation would destroy a
|
|
366
|
+
// vault it was meant to protect. Rotating the install key now re-wraps
|
|
367
|
+
// the account keys instead, which is a handful of small values rather
|
|
368
|
+
// than every secret on the box.
|
|
369
|
+
if (rec.k === "tenant") {
|
|
370
|
+
next[name] = rec;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
let plain;
|
|
374
|
+
try {
|
|
375
|
+
const d = crypto.createDecipheriv("aes-256-gcm", oldKey, Buffer.from(rec.iv, "base64"), GCM);
|
|
376
|
+
d.setAuthTag(Buffer.from(rec.tag, "base64"));
|
|
377
|
+
plain = Buffer.concat([d.update(Buffer.from(rec.data, "base64")), d.final()]).toString("utf8");
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
unreadable.push(name);
|
|
381
|
+
next[name] = rec; // untouched
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const iv = crypto.randomBytes(12);
|
|
385
|
+
const c = crypto.createCipheriv("aes-256-gcm", newKey, iv, GCM);
|
|
386
|
+
const enc = Buffer.concat([c.update(plain, "utf8"), c.final()]);
|
|
387
|
+
next[name] = {
|
|
388
|
+
iv: iv.toString("base64"),
|
|
389
|
+
tag: c.getAuthTag().toString("base64"),
|
|
390
|
+
data: enc.toString("base64"),
|
|
391
|
+
updatedAt: rec.updatedAt, // rotation is not an edit
|
|
392
|
+
// Kept: without it an oauth2 secret stops being refreshed after a
|
|
393
|
+
// rotation, and a file/ssh/api secret stops being materialised.
|
|
394
|
+
...(rec.kind ? { kind: rec.kind } : {}),
|
|
395
|
+
};
|
|
396
|
+
count++;
|
|
304
397
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const d = crypto.createDecipheriv("aes-256-gcm", oldKey, Buffer.from(rec.iv, "base64"), GCM);
|
|
308
|
-
d.setAuthTag(Buffer.from(rec.tag, "base64"));
|
|
309
|
-
plain = Buffer.concat([d.update(Buffer.from(rec.data, "base64")), d.final()]).toString("utf8");
|
|
310
|
-
}
|
|
311
|
-
catch {
|
|
312
|
-
unreadable.push(name);
|
|
313
|
-
next[name] = rec; // untouched
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
const iv = crypto.randomBytes(12);
|
|
317
|
-
const c = crypto.createCipheriv("aes-256-gcm", newKey, iv, GCM);
|
|
318
|
-
const enc = Buffer.concat([c.update(plain, "utf8"), c.final()]);
|
|
319
|
-
next[name] = {
|
|
320
|
-
iv: iv.toString("base64"),
|
|
321
|
-
tag: c.getAuthTag().toString("base64"),
|
|
322
|
-
data: enc.toString("base64"),
|
|
323
|
-
updatedAt: rec.updatedAt, // rotation is not an edit
|
|
324
|
-
};
|
|
325
|
-
count++;
|
|
326
|
-
}
|
|
327
|
-
// Write via a temp file in the same directory: a half-written secrets.json
|
|
328
|
-
// is an account that cannot authenticate anything.
|
|
329
|
-
const tmp = `${file}.rotating`;
|
|
330
|
-
fs.writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
331
|
-
fs.renameSync(tmp, file);
|
|
398
|
+
return next;
|
|
399
|
+
});
|
|
332
400
|
rewritten.push({ file, secrets: count });
|
|
333
401
|
}
|
|
334
402
|
return { rewritten, unreadable };
|
|
@@ -372,12 +440,12 @@ async function exchange(config, cacheKey) {
|
|
|
372
440
|
client_secret: config.client_secret,
|
|
373
441
|
...(config.extra ?? {}),
|
|
374
442
|
});
|
|
375
|
-
const res = await
|
|
443
|
+
const res = await fetchUntrusted(config.token_url, {
|
|
376
444
|
method: "POST",
|
|
377
445
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
378
446
|
body: body.toString(),
|
|
379
447
|
signal: AbortSignal.timeout(15_000),
|
|
380
|
-
});
|
|
448
|
+
}, { sameOrigin: true });
|
|
381
449
|
const payload = (await res.json().catch(() => ({})));
|
|
382
450
|
if (!res.ok || !payload.access_token) {
|
|
383
451
|
throw new Error(`token refresh failed (${res.status}): ${payload.error_description ?? payload.error ?? "no access_token in the reply"}`);
|
|
@@ -499,7 +567,7 @@ async function exchangeServiceAccount(config, cacheKey) {
|
|
|
499
567
|
signer.update(`${header}.${claim}`);
|
|
500
568
|
const signature = base64url(signer.sign(config.private_key));
|
|
501
569
|
const jwt = `${header}.${claim}.${signature}`;
|
|
502
|
-
const res = await
|
|
570
|
+
const res = await fetchUntrusted(config.token_url, {
|
|
503
571
|
method: "POST",
|
|
504
572
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
505
573
|
body: new URLSearchParams({
|
|
@@ -507,7 +575,7 @@ async function exchangeServiceAccount(config, cacheKey) {
|
|
|
507
575
|
assertion: jwt,
|
|
508
576
|
}).toString(),
|
|
509
577
|
signal: AbortSignal.timeout(15_000),
|
|
510
|
-
});
|
|
578
|
+
}, { sameOrigin: true });
|
|
511
579
|
const payload = (await res.json().catch(() => ({})));
|
|
512
580
|
if (!res.ok || !payload.access_token) {
|
|
513
581
|
throw new Error(`service account exchange failed (${res.status}): ${payload.error_description ?? payload.error ?? "no access_token"}`);
|
|
@@ -626,33 +694,34 @@ export function rewrapTenantSecrets(tenant) {
|
|
|
626
694
|
let moved = 0;
|
|
627
695
|
const unreadable = [];
|
|
628
696
|
for (const { file } of secretFiles(tenant)) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
697
|
+
// Under the vault's lock: every pod runs this at boot, and a save made
|
|
698
|
+
// between our read and our write must not be lost.
|
|
699
|
+
mutateVaultFile(file, (all) => {
|
|
700
|
+
const plain = new Map();
|
|
701
|
+
const unreadableHere = [];
|
|
702
|
+
for (const [name, rec] of Object.entries(all)) {
|
|
703
|
+
if (rec.k === "tenant")
|
|
704
|
+
continue; // already moved
|
|
705
|
+
const value = decrypt(tenant, rec);
|
|
706
|
+
if (value === null) {
|
|
707
|
+
unreadableHere.push(name);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
plain.set(name, value);
|
|
639
711
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
fs.writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
654
|
-
fs.renameSync(tmp, file);
|
|
655
|
-
moved += pending;
|
|
712
|
+
unreadable.push(...unreadableHere);
|
|
713
|
+
// Nothing to do, or something in THIS file could not be read — leave it
|
|
714
|
+
// alone. A partial conversion is worse than none, because the half that
|
|
715
|
+
// moved is no longer openable by the key the other half still needs.
|
|
716
|
+
if (plain.size === 0 || unreadableHere.length)
|
|
717
|
+
return null;
|
|
718
|
+
const next = { ...all };
|
|
719
|
+
for (const [name, value] of plain) {
|
|
720
|
+
next[name] = { ...encryptValue(tenant, value), updatedAt: all[name].updatedAt, ...(all[name].kind ? { kind: all[name].kind } : {}) };
|
|
721
|
+
}
|
|
722
|
+
moved += plain.size;
|
|
723
|
+
return next;
|
|
724
|
+
});
|
|
656
725
|
}
|
|
657
726
|
return { moved, unreadable };
|
|
658
727
|
}
|
package/dist/src/starter.d.ts
CHANGED
|
@@ -18,4 +18,13 @@ export interface StarterFile {
|
|
|
18
18
|
* workspace somewhere would be worse than no starter.
|
|
19
19
|
*/
|
|
20
20
|
export declare function accountFiles(account: string): StarterFile[];
|
|
21
|
+
/**
|
|
22
|
+
* What an account folder ignores, at its own root.
|
|
23
|
+
*
|
|
24
|
+
* The workspace scaffold has always shipped one of these; an account folder is
|
|
25
|
+
* the thing that gets committed now, and the key that decrypts its secrets
|
|
26
|
+
* lives at ITS root — `<account>/.foldrun/` — so the guard has to be here too
|
|
27
|
+
* or the first `git add .` commits the vault key.
|
|
28
|
+
*/
|
|
29
|
+
export declare function accountGitignore(): StarterFile;
|
|
21
30
|
export declare function starterFiles(workspace: string): StarterFile[];
|
package/dist/src/starter.js
CHANGED
|
@@ -64,6 +64,36 @@ dropped further down.
|
|
|
64
64
|
},
|
|
65
65
|
];
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* What an account folder ignores, at its own root.
|
|
69
|
+
*
|
|
70
|
+
* The workspace scaffold has always shipped one of these; an account folder is
|
|
71
|
+
* the thing that gets committed now, and the key that decrypts its secrets
|
|
72
|
+
* lives at ITS root — `<account>/.foldrun/` — so the guard has to be here too
|
|
73
|
+
* or the first `git add .` commits the vault key.
|
|
74
|
+
*/
|
|
75
|
+
export function accountGitignore() {
|
|
76
|
+
return {
|
|
77
|
+
path: ".gitignore",
|
|
78
|
+
content: `# The key that decrypts every secret in this account, plus the local run
|
|
79
|
+
# store. Never commit these.
|
|
80
|
+
.foldrun/
|
|
81
|
+
|
|
82
|
+
# Written by runs, not by you.
|
|
83
|
+
workspaces/*/runs/
|
|
84
|
+
workspaces/*/outputs/
|
|
85
|
+
state/
|
|
86
|
+
storage/
|
|
87
|
+
# The link a running step keeps in its agent folder (workspace/ for the
|
|
88
|
+
# shell); removed when the step ends, ignored in case one is left behind.
|
|
89
|
+
workspaces/*/agents/*/workspace
|
|
90
|
+
|
|
91
|
+
.env
|
|
92
|
+
.env.local
|
|
93
|
+
.DS_Store
|
|
94
|
+
`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
67
97
|
export function starterFiles(workspace) {
|
|
68
98
|
return [
|
|
69
99
|
{
|
|
@@ -98,6 +128,9 @@ Context every agent here shares. Prices, rules, anything they should all know.
|
|
|
98
128
|
# Written by runs, not by you.
|
|
99
129
|
runs/
|
|
100
130
|
outputs/
|
|
131
|
+
# The link a running step keeps in its agent folder (workspace/ for the
|
|
132
|
+
# shell); removed when the step ends, ignored in case one is left behind.
|
|
133
|
+
agents/*/workspace
|
|
101
134
|
|
|
102
135
|
.env
|
|
103
136
|
.env.local
|
|
@@ -114,6 +147,8 @@ effort: high
|
|
|
114
147
|
tools:
|
|
115
148
|
- web
|
|
116
149
|
- read
|
|
150
|
+
web:
|
|
151
|
+
actions: [search, fetch]
|
|
117
152
|
---
|
|
118
153
|
|
|
119
154
|
Pick exactly one topic, in a short paragraph, and say who it helps and why now.
|
|
@@ -126,7 +161,7 @@ Do not write the article.
|
|
|
126
161
|
name: writer
|
|
127
162
|
description: Turns a brief into a short draft.
|
|
128
163
|
tools:
|
|
129
|
-
-
|
|
164
|
+
- write
|
|
130
165
|
---
|
|
131
166
|
|
|
132
167
|
Write a short draft from the brief you were given. Save it to outputs/draft.md.
|
|
@@ -209,6 +244,10 @@ foldrun deploy --url <server> --token $FOLDRUN_TOKEN
|
|
|
209
244
|
\`check\` is cheap and catches a broken flow before a schedule fires it at 3am.
|
|
210
245
|
Run it after edits, the way you would run a typecheck.
|
|
211
246
|
|
|
247
|
+
A deploy that would delete files this folder no longer has lists them and
|
|
248
|
+
stops: with no terminal it is refused. Tell the person what it would delete; add \`--yes\`
|
|
249
|
+
only when they asked for exactly that deletion.
|
|
250
|
+
|
|
212
251
|
## Where things go
|
|
213
252
|
|
|
214
253
|
| Path | What |
|
|
@@ -231,10 +270,23 @@ Regenerated every run means a file; accumulated across runs means \`state/\`.
|
|
|
231
270
|
That last row is worth knowing: a subagent you write in Claude Code is imported
|
|
232
271
|
as \`agents/<name>/agent.md\` at deploy, no conversion. Write it either way.
|
|
233
272
|
|
|
273
|
+
## Paths inside a workspace
|
|
274
|
+
|
|
275
|
+
An agent's working directory is \`agents/<name>/\`. Write every path to the
|
|
276
|
+
workspace's own folders as \`workspace/…\` — \`workspace/storage/report.csv\`,
|
|
277
|
+
\`workspace/state/cursor.json\` — in an agent's prose, in flow options
|
|
278
|
+
(\`each: rows of\`, \`when: rows of\`, \`verify:\`, \`preview:\`, \`schema:\`) and in
|
|
279
|
+
script arguments. It resolves the same in the file tools, the shell, scripts
|
|
280
|
+
and checks. The older \`../../storage/…\` reaches the same place and still
|
|
281
|
+
works; \`foldrun check\` suggests the new spelling where it sees it.
|
|
282
|
+
A Node script started as \`node workspace/tools/x/run.mjs\` is its own main
|
|
283
|
+
module there, so its \`import.meta.url\` entry check runs (it did not before
|
|
284
|
+
2 Oct 2026 and exited 0 doing nothing).
|
|
285
|
+
|
|
234
286
|
## What a deploy does NOT touch
|
|
235
287
|
|
|
236
|
-
\`runs/\`, \`state/\`, \`secrets.json\`,
|
|
237
|
-
push does not mention. Those belong to the platform, not to git — a deploy that
|
|
288
|
+
\`runs/\`, \`state/\`, \`storage/\`, \`secrets.json\`, the trigger log, and any
|
|
289
|
+
memory an agent wrote that your push does not mention. Those belong to the platform, not to git — a deploy that
|
|
238
290
|
reverted what an agent learned would make every run a little dumber.
|
|
239
291
|
|
|
240
292
|
Secrets never go in these files. \`foldrun secrets\` puts them in the vault, and
|
|
@@ -270,19 +322,32 @@ All of it, \`/api/workspaces/<workspace>\` unless noted:
|
|
|
270
322
|
| \`/runs/<id>/stop\` | POST | kill it — destroys the sandbox too |
|
|
271
323
|
| \`/runs/<id>/rerun\` | POST | again, or from a chosen step |
|
|
272
324
|
| \`/runs/<id>/approve\` | POST | release a run waiting on a human |
|
|
325
|
+
| \`/runs/<id>/answer\` | POST | answer a question an agent is asking mid-step (\`tools: [ask]\`) |
|
|
326
|
+
| \`/runs/<id>/message\` | POST | tell a running step something; \`step\` picks one of a parallel group |
|
|
273
327
|
| \`/flows\` · \`/flows/<f>\` | GET POST · POST DELETE PATCH | list, create, edit |
|
|
274
328
|
| \`/flows/<f>/run\` | POST | start a flow |
|
|
275
329
|
| \`/agents\` · \`/agents/<a>/run\` | GET POST · POST | list agents, run one |
|
|
276
330
|
| \`/evals\` · \`/evals/<e>/run\` | GET POST · POST | list evals, run one |
|
|
277
331
|
| \`/tools/<t>/test\` | POST | exercise one tool alone |
|
|
278
|
-
| \`/storage\` · \`/storage/download\` · \`/storage/upload-url\` | GET POST PUT DELETE · GET · POST | workspace files |
|
|
332
|
+
| \`/storage\` · \`/storage/download\` · \`/storage/upload-url\` · \`/storage/preview\` | GET POST PUT DELETE · GET · POST · GET | workspace files, any type; \`preview\` returns one parsed for reading |
|
|
279
333
|
| \`/assets\` | POST | upload an asset |
|
|
280
334
|
| \`/hooks/<f>/rotate\` | POST | new webhook token for a flow |
|
|
335
|
+
| \`/notify/deliveries\` · \`/notify/deliveries/<id>/redeliver\` | GET · POST | every notify: webhook sent and each attempt; send one again |
|
|
336
|
+
| \`/restore\` | POST | the workspace's source back to a point in its history — \`dryRun\` first, then \`confirm\` |
|
|
281
337
|
| \`/vocabulary\` | GET | what this workspace's documents may say |
|
|
282
338
|
|
|
283
339
|
Account-level, outside \`/workspaces\`: \`/api/secrets\`, \`/api/keys\`, \`/api/schedule\`,
|
|
284
340
|
\`/api/approvals\`, \`/api/usage\`, \`/api/library/<kind>\`, \`/api/team\`, \`/api/billing\`,
|
|
285
|
-
\`/api/
|
|
341
|
+
\`/api/me/notifications\` (what mail you get; a key acts for whoever minted it),
|
|
342
|
+
\`/api/account/backups\` (snapshots, restore requests), \`/api/me/onboarding\` (getting started),
|
|
343
|
+
\`/api/healthz\` and \`/api/version\` (open), \`/api/changelog\` (release notes)
|
|
344
|
+
and \`/api/metrics\` (Prometheus).
|
|
345
|
+
|
|
346
|
+
Every call answers — and the CLI sends — \`Foldrun-Version: YYYY-MM-DD\` (dated API
|
|
347
|
+
versions; an account pin in Settings → API), \`X-RateLimit-*\` headers (a \`429\`
|
|
348
|
+
carries \`Retry-After\`), and writes that start work take an \`Idempotency-Key\`
|
|
349
|
+
(a retry with the same key replays the first answer). The whole surface as
|
|
350
|
+
OpenAPI 3.1: \`/api/openapi.json\` (open), or \`foldrun api spec --out openapi.json\`.
|
|
286
351
|
|
|
287
352
|
Debugging a failed run is usually: \`runs\` to find it, \`runs/<id>\` to read which
|
|
288
353
|
step failed and what it cost, fix the markdown, \`deploy\`, then \`rerun\`.
|
|
@@ -295,6 +360,11 @@ git push endpoint.
|
|
|
295
360
|
|
|
296
361
|
A numbered list of steps. The number is the GROUP: same number means those
|
|
297
362
|
steps run in parallel, and the flow waits for all of them before the next.
|
|
363
|
+
The dashboard's canvas and the CLI edit the same file: \`foldrun flow show
|
|
364
|
+
<flow>\` draws it, \`foldrun flow add <flow> <pattern>\` inserts a pattern
|
|
365
|
+
(chain, parallel, router, fan-out, loop, approval, ask, wait, rescue,
|
|
366
|
+
subflow), \`foldrun flow rm-step\` removes a step, and \`foldrun agent link
|
|
367
|
+
<agent> --subagent|--consult|--can-ask\` wires agents together.
|
|
298
368
|
|
|
299
369
|
\`\`\`markdown
|
|
300
370
|
---
|
|
@@ -310,7 +380,7 @@ priority: normal # high | normal | low — where its runs stand in the q
|
|
|
310
380
|
1. [[researcher]] — Find three sources on {{topic}}.
|
|
311
381
|
2. [[writer]] — Draft from what the researcher found.
|
|
312
382
|
model: max
|
|
313
|
-
verify: the draft cites every source
|
|
383
|
+
verify: judge: the draft cites every source
|
|
314
384
|
3?. [[editor]] — Tighten it.
|
|
315
385
|
4!. [[publisher]] — Publish it.
|
|
316
386
|
\`\`\`
|
|
@@ -323,11 +393,14 @@ Indented under a step, any of:
|
|
|
323
393
|
| | |
|
|
324
394
|
|---|---|
|
|
325
395
|
| \`approve: true\` | stop for a human (\`!\` is shorthand for this) |
|
|
326
|
-
| \`when: <
|
|
396
|
+
| \`when: <MARKER>\` | run only if a line of the previous result begins with it |
|
|
397
|
+
| \`when: rows of <path>\` | run only if that CSV has a data row — for the steps after an \`each:\` |
|
|
327
398
|
| \`case: <value>\` / \`else: true\` | branch on the previous step's result |
|
|
328
399
|
| \`retry: 2\` | re-run on failure, up to 5 |
|
|
329
400
|
| \`timeout: 900\` | seconds before the step is cut off |
|
|
330
|
-
| \`verify: <
|
|
401
|
+
| \`verify: <shell>\` | the step fails unless the command exits 0; the reply is in \`$FOLDRUN_REPLY_FILE\` |
|
|
402
|
+
| \`verify: judge: <claim>\` | a model grades the claim against the output |
|
|
403
|
+
| \`verify: contains:\` / \`not-contains:\` / \`matches:\` / \`file:\` | the cheap checks, no shell |
|
|
331
404
|
| \`model:\` / \`effort:\` | override the flow's default for this step |
|
|
332
405
|
| \`loop: 3\` / \`until: <condition>\` | repeat until it holds, at most 5 |
|
|
333
406
|
| \`each: lines\` | run once per line of the previous result |
|
|
@@ -344,6 +417,7 @@ Indented under a step, any of:
|
|
|
344
417
|
| \`each: items\` | run once per element of the previous step's JSON array |
|
|
345
418
|
| \`parallel: 3\` | how many \`each:\` instances run at once — default all |
|
|
346
419
|
| \`max_turns: 20\` | the most model turns before the step is stopped, beside \`budget:\` and \`timeout:\` |
|
|
420
|
+
| \`limits: {web.search: 10}\` | this step's call limits, over the agent's key by key |
|
|
347
421
|
|
|
348
422
|
## Writing an agent
|
|
349
423
|
|
|
@@ -357,14 +431,17 @@ description: Finds and summarises sources.
|
|
|
357
431
|
model: default # fast | default | max
|
|
358
432
|
effort: low # how hard to think about it
|
|
359
433
|
size: large # small | large | heavy — the sandbox it rents
|
|
360
|
-
tools: [
|
|
434
|
+
tools: [web, read, ask, my-folder-tool] # built-ins and your own tools/, one list; \`ask\` lets it ask you mid-step
|
|
361
435
|
disallowedTools: [Bash] # subtract from what it would otherwise have
|
|
362
436
|
skills: [house-style] # from skills/
|
|
363
437
|
scripts: [summarise.py] # from scripts/, each becomes a callable tool
|
|
364
438
|
apis: [{ name: crm }] # an HTTP API, as a tool
|
|
365
439
|
mcpServers: {} # an MCP server, as a tool
|
|
366
|
-
agents: [writer] # colleagues this one may consult
|
|
367
|
-
|
|
440
|
+
agents: [writer] # colleagues this one may consult (an answer, no tools)
|
|
441
|
+
subagents: [researcher] # colleagues it may delegate a job to (own context, own tools ≤ its own)
|
|
442
|
+
secrets: [CRM_TOKEN] # keys the agent uses itself; a tool's own come with the tool
|
|
443
|
+
limits: {web.search: 40, crm: 20, calls: 300} # per step; a call past a limit is refused, never run
|
|
444
|
+
timezone: Australia/Sydney # the calendar it works to — IANA name or UTC+10
|
|
368
445
|
provider: {} # BYOK: your own model credential
|
|
369
446
|
permissionMode: plan # plan first, act once approved
|
|
370
447
|
runtime: # only if scripts need packages
|
|
@@ -377,6 +454,10 @@ You research a topic and report what you found...
|
|
|
377
454
|
Only \`name\` and the prose are required. Every other field widens or narrows
|
|
378
455
|
what the agent can reach, and the default is narrow.
|
|
379
456
|
|
|
457
|
+
\`timezone:\` is nearest-wins all the way down: the agent's own, else its
|
|
458
|
+
flow's, else the workspace's AGENTS.md, else the account's, else UTC. It sets
|
|
459
|
+
\`TZ\` and the date the agent is told, so a Sydney desk stamps the Sydney date.
|
|
460
|
+
|
|
380
461
|
\`runtime:\` installs pip/npm packages in the sandbox each agent runs in. Pin
|
|
381
462
|
versions the normal way (\`pandas>=2\`, \`lodash@^4\`). Python and Node are
|
|
382
463
|
supported; anything else should ship as a committed binary.
|