@foldrun/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +7 -4
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.js +4 -0
  4. package/dist/src/agent-import.d.ts +21 -0
  5. package/dist/src/agent-import.js +116 -0
  6. package/dist/src/api-tools.js +14 -2
  7. package/dist/src/approvals.d.ts +8 -1
  8. package/dist/src/approvals.js +10 -5
  9. package/dist/src/browser-pod.d.ts +20 -1
  10. package/dist/src/browser-pod.js +58 -9
  11. package/dist/src/completions.js +1 -0
  12. package/dist/src/confine.d.ts +1 -0
  13. package/dist/src/confine.js +33 -4
  14. package/dist/src/container.d.ts +9 -0
  15. package/dist/src/container.js +115 -5
  16. package/dist/src/context-tools.d.ts +1 -1
  17. package/dist/src/deploy.d.ts +9 -0
  18. package/dist/src/deploy.js +31 -3
  19. package/dist/src/evals.js +24 -4
  20. package/dist/src/host-env.d.ts +14 -0
  21. package/dist/src/host-env.js +31 -0
  22. package/dist/src/install-helper.d.ts +17 -0
  23. package/dist/src/install-helper.js +134 -0
  24. package/dist/src/install-tool.d.ts +128 -0
  25. package/dist/src/install-tool.js +133 -0
  26. package/dist/src/mail.js +3 -1
  27. package/dist/src/model-credential.d.ts +30 -0
  28. package/dist/src/model-credential.js +98 -0
  29. package/dist/src/model-env.d.ts +17 -0
  30. package/dist/src/model-env.js +57 -0
  31. package/dist/src/package.d.ts +67 -0
  32. package/dist/src/package.js +594 -0
  33. package/dist/src/platform.d.ts +5 -0
  34. package/dist/src/platform.js +1 -0
  35. package/dist/src/provider-health.js +3 -0
  36. package/dist/src/run-container.d.ts +6 -1
  37. package/dist/src/run-container.js +93 -20
  38. package/dist/src/runner.d.ts +239 -0
  39. package/dist/src/runner.js +380 -72
  40. package/dist/src/runtime.d.ts +77 -0
  41. package/dist/src/runtime.js +230 -5
  42. package/dist/src/script-tools.d.ts +16 -0
  43. package/dist/src/script-tools.js +50 -8
  44. package/dist/src/secrets.d.ts +5 -0
  45. package/dist/src/secrets.js +37 -6
  46. package/dist/src/starter.d.ts +12 -0
  47. package/dist/src/starter.js +36 -4
  48. package/dist/src/step-exec.d.ts +22 -2
  49. package/dist/src/step-exec.js +70 -7
  50. package/dist/src/storage.d.ts +7 -1
  51. package/dist/src/storage.js +14 -1
  52. package/dist/src/store.d.ts +24 -0
  53. package/dist/src/store.js +91 -10
  54. package/dist/src/subagents.js +1 -1
  55. package/dist/src/test-mode.d.ts +17 -2
  56. package/dist/src/test-mode.js +53 -13
  57. package/dist/src/tool-names.d.ts +3 -1
  58. package/dist/src/tool-names.js +4 -2
  59. package/dist/src/tool-test.js +8 -1
  60. package/dist/src/zip.d.ts +17 -0
  61. package/dist/src/zip.js +184 -0
  62. package/index.ts +4 -0
  63. package/package.json +1 -1
  64. package/src/agent-import.ts +154 -0
  65. package/src/api-tools.ts +16 -1
  66. package/src/approvals.ts +10 -3
  67. package/src/browser-pod.ts +46 -9
  68. package/src/completions.ts +1 -0
  69. package/src/confine.ts +36 -5
  70. package/src/container.ts +112 -5
  71. package/src/context-tools.ts +1 -1
  72. package/src/deploy.ts +28 -3
  73. package/src/evals.ts +23 -2
  74. package/src/host-env.ts +32 -0
  75. package/src/install-helper.ts +143 -0
  76. package/src/install-tool.ts +154 -0
  77. package/src/mail.ts +3 -1
  78. package/src/model-credential.ts +112 -0
  79. package/src/model-env.ts +66 -0
  80. package/src/package.ts +664 -0
  81. package/src/platform.ts +6 -0
  82. package/src/provider-health.ts +3 -0
  83. package/src/run-container.ts +92 -19
  84. package/src/runner.ts +385 -72
  85. package/src/runtime.ts +235 -5
  86. package/src/script-tools.ts +48 -6
  87. package/src/secrets.ts +34 -6
  88. package/src/starter.ts +42 -4
  89. package/src/step-exec.ts +75 -10
  90. package/src/storage.ts +13 -0
  91. package/src/store.ts +91 -9
  92. package/src/subagents.ts +1 -1
  93. package/src/test-mode.ts +51 -11
  94. package/src/tool-names.ts +4 -2
  95. package/src/tool-test.ts +8 -1
  96. package/src/zip.ts +189 -0
  97. package/templates/blog-desk/agents/writer/agent.md +1 -1
  98. package/templates/hello/agents/curator/agent.md +3 -3
  99. package/templates/hello/agents/notetaker/agent.md +2 -2
package/src/zip.ts ADDED
@@ -0,0 +1,189 @@
1
+ // A small zip reader and writer — enough for an export package, no dependency.
2
+ //
3
+ // What a person downloads from "Export" has to open with a double-click on
4
+ // any machine, which is a zip, not a tarball. The format here is the plain
5
+ // one: deflate or stored entries, a central directory, no zip64, no
6
+ // encryption. The reader refuses anything outside that rather than guessing,
7
+ // and it is written to be handed untrusted bytes: every offset is bounds
8
+ // checked, sizes are capped before anything is inflated, and a path that
9
+ // climbs out (`..`, absolute, a drive letter) is an error, not a skip.
10
+
11
+ import zlib from "node:zlib";
12
+
13
+ export interface ZipEntry {
14
+ path: string;
15
+ data: Buffer;
16
+ }
17
+
18
+ export class ZipError extends Error {}
19
+
20
+ export interface UnzipLimits {
21
+ /** Most entries the archive may hold. */
22
+ maxEntries?: number;
23
+ /** Largest one file, uncompressed. */
24
+ maxFileBytes?: number;
25
+ /** All files together, uncompressed. */
26
+ maxTotalBytes?: number;
27
+ }
28
+
29
+ const CRC_TABLE = (() => {
30
+ const t = new Uint32Array(256);
31
+ for (let n = 0; n < 256; n++) {
32
+ let c = n;
33
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
34
+ t[n] = c >>> 0;
35
+ }
36
+ return t;
37
+ })();
38
+
39
+ export function crc32(buf: Buffer): number {
40
+ let c = 0xffffffff;
41
+ for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]!) & 0xff]! ^ (c >>> 8);
42
+ return (c ^ 0xffffffff) >>> 0;
43
+ }
44
+
45
+ /** DOS date and time for the entry headers. */
46
+ function dosTime(d: Date): { time: number; date: number } {
47
+ return {
48
+ time: (d.getHours() << 11) | (d.getMinutes() << 5) | Math.floor(d.getSeconds() / 2),
49
+ date: (Math.max(0, d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(),
50
+ };
51
+ }
52
+
53
+ export function zip(entries: ZipEntry[], now = new Date()): Buffer {
54
+ const { time, date } = dosTime(now);
55
+ const locals: Buffer[] = [];
56
+ const centrals: Buffer[] = [];
57
+ let offset = 0;
58
+ const seen = new Set<string>();
59
+ for (const e of entries) {
60
+ const name = safeEntryPath(e.path);
61
+ if (seen.has(name)) throw new ZipError(`duplicate entry: ${name}`);
62
+ seen.add(name);
63
+ const nameBuf = Buffer.from(name, "utf8");
64
+ const deflated = zlib.deflateRawSync(e.data);
65
+ const stored = deflated.length >= e.data.length;
66
+ const body = stored ? e.data : deflated;
67
+ const crc = crc32(e.data);
68
+
69
+ const local = Buffer.alloc(30);
70
+ local.writeUInt32LE(0x04034b50, 0);
71
+ local.writeUInt16LE(20, 4); // version needed
72
+ local.writeUInt16LE(0x0800, 6); // UTF-8 names
73
+ local.writeUInt16LE(stored ? 0 : 8, 8);
74
+ local.writeUInt16LE(time, 10);
75
+ local.writeUInt16LE(date, 12);
76
+ local.writeUInt32LE(crc, 14);
77
+ local.writeUInt32LE(body.length, 18);
78
+ local.writeUInt32LE(e.data.length, 22);
79
+ local.writeUInt16LE(nameBuf.length, 26);
80
+ local.writeUInt16LE(0, 28);
81
+ locals.push(local, nameBuf, body);
82
+
83
+ const central = Buffer.alloc(46);
84
+ central.writeUInt32LE(0x02014b50, 0);
85
+ central.writeUInt16LE(0x0314, 4); // made by: unix, 2.0
86
+ central.writeUInt16LE(20, 6);
87
+ central.writeUInt16LE(0x0800, 8);
88
+ central.writeUInt16LE(stored ? 0 : 8, 10);
89
+ central.writeUInt16LE(time, 12);
90
+ central.writeUInt16LE(date, 14);
91
+ central.writeUInt32LE(crc, 16);
92
+ central.writeUInt32LE(body.length, 20);
93
+ central.writeUInt32LE(e.data.length, 24);
94
+ central.writeUInt16LE(nameBuf.length, 28);
95
+ central.writeUInt32LE((0o100644 << 16) >>> 0, 38); // a regular file, rw-r--r--
96
+ central.writeUInt32LE(offset, 42);
97
+ centrals.push(central, nameBuf);
98
+
99
+ offset += local.length + nameBuf.length + body.length;
100
+ }
101
+ const cd = Buffer.concat(centrals);
102
+ const end = Buffer.alloc(22);
103
+ end.writeUInt32LE(0x06054b50, 0);
104
+ end.writeUInt16LE(entries.length, 8);
105
+ end.writeUInt16LE(entries.length, 10);
106
+ end.writeUInt32LE(cd.length, 12);
107
+ end.writeUInt32LE(offset, 16);
108
+ return Buffer.concat([...locals, cd, end]);
109
+ }
110
+
111
+ export function unzip(buf: Buffer, limits: UnzipLimits = {}): ZipEntry[] {
112
+ const maxEntries = limits.maxEntries ?? 2000;
113
+ const maxFile = limits.maxFileBytes ?? 1024 * 1024;
114
+ const maxTotal = limits.maxTotalBytes ?? 50 * 1024 * 1024;
115
+
116
+ // The end-of-central-directory record: within the last 22 + 65535 bytes.
117
+ let eocd = -1;
118
+ for (let i = buf.length - 22; i >= Math.max(0, buf.length - 22 - 0xffff); i--) {
119
+ if (buf.readUInt32LE(i) === 0x06054b50) {
120
+ eocd = i;
121
+ break;
122
+ }
123
+ }
124
+ if (eocd < 0) throw new ZipError("not a zip file");
125
+ const count = buf.readUInt16LE(eocd + 10);
126
+ const cdSize = buf.readUInt32LE(eocd + 12);
127
+ const cdOffset = buf.readUInt32LE(eocd + 16);
128
+ if (count === 0xffff || cdOffset === 0xffffffff) throw new ZipError("zip64 archives are not supported");
129
+ if (count > maxEntries) throw new ZipError(`too many files: ${count} (at most ${maxEntries})`);
130
+ if (cdOffset + cdSize > eocd) throw new ZipError("corrupt zip: central directory out of range");
131
+
132
+ const out: ZipEntry[] = [];
133
+ const seen = new Set<string>();
134
+ let total = 0;
135
+ let p = cdOffset;
136
+ for (let n = 0; n < count; n++) {
137
+ if (p + 46 > eocd || buf.readUInt32LE(p) !== 0x02014b50) throw new ZipError("corrupt zip: bad central directory entry");
138
+ const flags = buf.readUInt16LE(p + 8);
139
+ const method = buf.readUInt16LE(p + 10);
140
+ const crc = buf.readUInt32LE(p + 16);
141
+ const csize = buf.readUInt32LE(p + 20);
142
+ const usize = buf.readUInt32LE(p + 24);
143
+ const nameLen = buf.readUInt16LE(p + 28);
144
+ const extraLen = buf.readUInt16LE(p + 30);
145
+ const commentLen = buf.readUInt16LE(p + 32);
146
+ const localAt = buf.readUInt32LE(p + 42);
147
+ if (p + 46 + nameLen > eocd) throw new ZipError("corrupt zip: name out of range");
148
+ const rawName = buf.subarray(p + 46, p + 46 + nameLen).toString("utf8");
149
+ p += 46 + nameLen + extraLen + commentLen;
150
+
151
+ if (rawName.endsWith("/")) continue; // a directory entry
152
+ // macOS "Compress" adds these beside every file; they are not content.
153
+ if (/(^|\/)__MACOSX\//.test(rawName) || /(^|\/)\.DS_Store$/.test(rawName)) continue;
154
+ if (flags & 0x1) throw new ZipError(`${rawName}: encrypted entries are not supported`);
155
+ if (method !== 0 && method !== 8) throw new ZipError(`${rawName}: compression method ${method} is not supported`);
156
+ const name = safeEntryPath(rawName);
157
+ if (seen.has(name)) throw new ZipError(`duplicate entry: ${name}`);
158
+ seen.add(name);
159
+ if (usize > maxFile) throw new ZipError(`${name} is too large (${usize} bytes, at most ${maxFile})`);
160
+ total += usize;
161
+ if (total > maxTotal) throw new ZipError(`the files are too large together (more than ${maxTotal} bytes)`);
162
+
163
+ if (localAt + 30 > buf.length || buf.readUInt32LE(localAt) !== 0x04034b50) throw new ZipError(`${name}: corrupt local header`);
164
+ const start = localAt + 30 + buf.readUInt16LE(localAt + 26) + buf.readUInt16LE(localAt + 28);
165
+ if (start + csize > buf.length) throw new ZipError(`${name}: data out of range`);
166
+ const raw = buf.subarray(start, start + csize);
167
+ let data: Buffer;
168
+ try {
169
+ // maxOutputLength: the header's size is the attacker's to choose, so the
170
+ // inflate itself is capped too — a bomb stops here, not in memory.
171
+ data = method === 0 ? Buffer.from(raw) : zlib.inflateRawSync(raw, { maxOutputLength: maxFile + 1 });
172
+ } catch {
173
+ throw new ZipError(`${name}: could not be decompressed`);
174
+ }
175
+ if (data.length !== usize) throw new ZipError(`${name}: size does not match its header`);
176
+ if (crc32(data) !== crc) throw new ZipError(`${name}: checksum mismatch`);
177
+ out.push({ path: name, data });
178
+ }
179
+ return out;
180
+ }
181
+
182
+ /** A relative, forward-slash path that stays inside the archive's root. */
183
+ function safeEntryPath(raw: string): string {
184
+ const p = raw.replaceAll("\\", "/");
185
+ if (p.startsWith("/") || /^[a-zA-Z]:/.test(p) || p.includes("\0")) throw new ZipError(`unsafe path: ${raw}`);
186
+ const parts = p.split("/").filter((s) => s !== "" && s !== ".");
187
+ if (parts.length === 0 || parts.some((s) => s === "..")) throw new ZipError(`unsafe path: ${raw}`);
188
+ return parts.join("/");
189
+ }
@@ -2,7 +2,7 @@
2
2
  name: writer
3
3
  description: Turns a brief into a short draft.
4
4
  tools:
5
- - files
5
+ - write
6
6
  ---
7
7
 
8
8
  Write a short draft from the brief you were given. Save it to outputs/draft.md.
@@ -3,12 +3,12 @@ name: curator
3
3
  description: Reviews what the workspace has remembered — stale, duplicated or contradicting facts — and tidies memory without inventing anything.
4
4
  model: default
5
5
  tools:
6
- - files
6
+ - write
7
7
  - search
8
8
  - history
9
9
  ---
10
10
 
11
- You look after this workspace's memory: the files under `../../memory/` and
11
+ You look after this workspace's memory: the files under `workspace/memory/` and
12
12
  under each agent's `memory/`. Agents write there one fact per file, and nobody
13
13
  prunes it, so over months it gains duplicates, facts that later runs
14
14
  contradicted, and notes that stopped being true.
@@ -27,6 +27,6 @@ Do exactly this:
27
27
  move it to `workspace/state/` if it is still useful, else delete it.
28
28
  - Anything you are not sure about: leave it, and list it in your report.
29
29
  3. Never write a fact you did not find in a file or a run. Never touch
30
- `knowledge/` — that is what people gave the agents, and it is theirs.
30
+ `workspace/knowledge/` — that is what people gave the agents, and it is theirs.
31
31
  4. Reply with a one-line headline (how many files reviewed, merged, corrected,
32
32
  removed), then the list of what you changed and what you left for a person.
@@ -3,7 +3,7 @@ name: notetaker
3
3
  description: Reads the house style, writes one short note, checks its length, and records what it learned.
4
4
  model: fast
5
5
  tools:
6
- - files
6
+ - write
7
7
  - wordcount
8
8
  ---
9
9
 
@@ -16,7 +16,7 @@ Do exactly this, in order:
16
16
  2. Draft two sentences about rain gauges, then call the `wordcount` tool on
17
17
  your draft. If it reports more than 2 sentences, cut it down and check
18
18
  again. Save the final text to `outputs/note.md`.
19
- 3. Write one thing you learned to `../../memory/` as a new file — one fact,
19
+ 3. Write one thing you learned to `workspace/memory/` as a new file — one fact,
20
20
  plain markdown, a sentence or two. The platform records who wrote it.
21
21
  4. Update `workspace/state/publishing.md`: increase the note count by one and set
22
22
  the last topic to what you wrote about. Keep it as markdown.