@foldrun/core 0.6.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.
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/src/agent-import.d.ts +21 -0
- package/dist/src/agent-import.js +116 -0
- package/dist/src/api-tools.js +14 -2
- package/dist/src/approvals.d.ts +8 -1
- package/dist/src/approvals.js +10 -5
- package/dist/src/completions.js +1 -0
- package/dist/src/confine.d.ts +1 -0
- package/dist/src/confine.js +33 -4
- package/dist/src/deploy.d.ts +9 -0
- package/dist/src/deploy.js +31 -3
- package/dist/src/evals.js +22 -5
- package/dist/src/host-env.d.ts +14 -0
- package/dist/src/host-env.js +26 -0
- package/dist/src/install-helper.d.ts +17 -0
- package/dist/src/install-helper.js +134 -0
- package/dist/src/install-tool.d.ts +128 -0
- package/dist/src/install-tool.js +133 -0
- package/dist/src/model-credential.d.ts +16 -0
- package/dist/src/model-credential.js +40 -0
- package/dist/src/model-env.d.ts +17 -0
- package/dist/src/model-env.js +57 -0
- package/dist/src/package.d.ts +67 -0
- package/dist/src/package.js +594 -0
- package/dist/src/platform.d.ts +5 -0
- package/dist/src/platform.js +1 -0
- package/dist/src/provider-health.js +3 -0
- package/dist/src/run-container.d.ts +5 -0
- package/dist/src/run-container.js +66 -13
- package/dist/src/runner.d.ts +204 -0
- package/dist/src/runner.js +257 -36
- package/dist/src/runtime.d.ts +77 -0
- package/dist/src/runtime.js +230 -5
- package/dist/src/script-tools.d.ts +2 -0
- package/dist/src/script-tools.js +3 -1
- package/dist/src/secrets.d.ts +5 -0
- package/dist/src/secrets.js +37 -6
- package/dist/src/starter.d.ts +12 -0
- package/dist/src/starter.js +36 -4
- package/dist/src/step-exec.d.ts +11 -2
- package/dist/src/step-exec.js +52 -5
- package/dist/src/storage.d.ts +7 -1
- package/dist/src/storage.js +14 -1
- package/dist/src/store.d.ts +20 -0
- package/dist/src/store.js +89 -8
- package/dist/src/tool-names.d.ts +3 -1
- package/dist/src/tool-names.js +4 -2
- package/dist/src/tool-test.js +8 -1
- package/dist/src/zip.d.ts +17 -0
- package/dist/src/zip.js +184 -0
- package/index.ts +3 -0
- package/package.json +1 -1
- package/src/agent-import.ts +154 -0
- package/src/api-tools.ts +16 -1
- package/src/approvals.ts +10 -3
- package/src/completions.ts +1 -0
- package/src/confine.ts +36 -5
- package/src/deploy.ts +28 -3
- package/src/evals.ts +21 -3
- package/src/host-env.ts +27 -0
- package/src/install-helper.ts +143 -0
- package/src/install-tool.ts +154 -0
- package/src/model-credential.ts +41 -0
- package/src/model-env.ts +66 -0
- package/src/package.ts +664 -0
- package/src/platform.ts +6 -0
- package/src/provider-health.ts +3 -0
- package/src/run-container.ts +65 -12
- package/src/runner.ts +264 -36
- package/src/runtime.ts +235 -5
- package/src/script-tools.ts +5 -1
- package/src/secrets.ts +34 -6
- package/src/starter.ts +42 -4
- package/src/step-exec.ts +56 -8
- package/src/storage.ts +13 -0
- package/src/store.ts +85 -7
- package/src/tool-names.ts +4 -2
- package/src/tool-test.ts +8 -1
- package/src/zip.ts +189 -0
package/dist/src/storage.d.ts
CHANGED
|
@@ -128,6 +128,10 @@ export declare function uploadUrl(tenant: string, workspace: string, rel: string
|
|
|
128
128
|
* is worth writing when a workspace actually outgrows its quota, not before.
|
|
129
129
|
*/
|
|
130
130
|
export declare function materializeFiles(tenant: string, workspace: string): Promise<string[]>;
|
|
131
|
+
/** path → sha of every stored file: what `storage/` holds once
|
|
132
|
+
* materializeFiles has run. A run keeps it to tell its own writes from the
|
|
133
|
+
* files it was handed. */
|
|
134
|
+
export declare function storageBaseline(tenant: string, workspace: string): Record<string, string>;
|
|
131
135
|
/**
|
|
132
136
|
* Take what the run left in `storage/` back into the store, stamped with the
|
|
133
137
|
* run that wrote it.
|
|
@@ -136,7 +140,9 @@ export declare function materializeFiles(tenant: string, workspace: string): Pro
|
|
|
136
140
|
* derived from the index, so anything on disk that the index does not already
|
|
137
141
|
* describe with the same size and hash is, by construction, new work.
|
|
138
142
|
*/
|
|
139
|
-
export declare function harvestFiles(tenant: string, workspace: string, by: string
|
|
143
|
+
export declare function harvestFiles(tenant: string, workspace: string, by: string, opts?: {
|
|
144
|
+
changedFrom?: Record<string, string>;
|
|
145
|
+
}): Promise<{
|
|
140
146
|
saved: string[];
|
|
141
147
|
errors: string[];
|
|
142
148
|
}>;
|
package/dist/src/storage.js
CHANGED
|
@@ -686,6 +686,14 @@ export async function materializeFiles(tenant, workspace) {
|
|
|
686
686
|
fs.mkdirSync(root, { recursive: true });
|
|
687
687
|
return brought;
|
|
688
688
|
}
|
|
689
|
+
/** path → sha of every stored file: what `storage/` holds once
|
|
690
|
+
* materializeFiles has run. A run keeps it to tell its own writes from the
|
|
691
|
+
* files it was handed. */
|
|
692
|
+
export function storageBaseline(tenant, workspace) {
|
|
693
|
+
if (!fileStoreEnabled())
|
|
694
|
+
return {};
|
|
695
|
+
return Object.fromEntries(readIndex(tenant, workspace).files.map((f) => [f.path, f.sha]));
|
|
696
|
+
}
|
|
689
697
|
/**
|
|
690
698
|
* Take what the run left in `storage/` back into the store, stamped with the
|
|
691
699
|
* run that wrote it.
|
|
@@ -694,7 +702,7 @@ export async function materializeFiles(tenant, workspace) {
|
|
|
694
702
|
* derived from the index, so anything on disk that the index does not already
|
|
695
703
|
* describe with the same size and hash is, by construction, new work.
|
|
696
704
|
*/
|
|
697
|
-
export async function harvestFiles(tenant, workspace, by) {
|
|
705
|
+
export async function harvestFiles(tenant, workspace, by, opts = {}) {
|
|
698
706
|
if (!fileStoreEnabled())
|
|
699
707
|
return { saved: [], errors: [] };
|
|
700
708
|
adoptLegacyFilesDir(workspaceDir(tenant, workspace));
|
|
@@ -729,6 +737,11 @@ export async function harvestFiles(tenant, workspace, by) {
|
|
|
729
737
|
if (sha256(fs.readFileSync(abs)) === previous.sha)
|
|
730
738
|
continue;
|
|
731
739
|
}
|
|
740
|
+
// Only what the run itself changed: a file still holding the bytes it
|
|
741
|
+
// was handed is not this run's work, and the index may have moved on
|
|
742
|
+
// since (an upload while the run was going) — that must win.
|
|
743
|
+
if (opts.changedFrom && opts.changedFrom[norm] === sha256(fs.readFileSync(abs)))
|
|
744
|
+
continue;
|
|
732
745
|
await putFileQuietly(tenant, workspace, norm, fs.readFileSync(abs), by);
|
|
733
746
|
saved.push(norm);
|
|
734
747
|
}
|
package/dist/src/store.d.ts
CHANGED
|
@@ -188,6 +188,15 @@ export declare function fencedCodeBlock(body: string): {
|
|
|
188
188
|
* failure costs something that cannot be taken back. See `lintFlow`.
|
|
189
189
|
*/
|
|
190
190
|
export declare function toolIsOutward(data: Record<string, unknown>): boolean;
|
|
191
|
+
/** How much of a tool.md body rides to the model with the tool. */
|
|
192
|
+
export declare const TOOL_GUIDE_MAX = 6000;
|
|
193
|
+
/**
|
|
194
|
+
* The prose under a tool.md's frontmatter: how to call it, what it refuses,
|
|
195
|
+
* what its output means. Only `description:` used to reach the model, so
|
|
196
|
+
* agents invented what the body documented (5 Oct 2026). The fenced program
|
|
197
|
+
* of a single-file tool is code, not guidance, and stays out.
|
|
198
|
+
*/
|
|
199
|
+
export declare function toolGuide(body: string | undefined, inlineProgram?: boolean): string | undefined;
|
|
191
200
|
export declare function parseToolDef(data: Record<string, unknown>, fallbackName: string, body?: string): ToolDef | null;
|
|
192
201
|
/**
|
|
193
202
|
* Tools in a directory, in both shapes.
|
|
@@ -234,6 +243,8 @@ export interface ApiSpec {
|
|
|
234
243
|
name: string;
|
|
235
244
|
base: string;
|
|
236
245
|
description: string;
|
|
246
|
+
/** The tool.md body, for the model (see toolGuide). */
|
|
247
|
+
guide?: string;
|
|
237
248
|
headers: Record<string, string>;
|
|
238
249
|
query: Record<string, string>;
|
|
239
250
|
methods: string[];
|
|
@@ -846,6 +857,9 @@ export declare function setWorkspaceDescription(tenant: string, workspace: strin
|
|
|
846
857
|
* this copy still said `type: Agent` after the CLI's had moved to `kind:`,
|
|
847
858
|
* so every workspace made from the dashboard was born in the old format.
|
|
848
859
|
*/
|
|
860
|
+
/** What the dashboard's "Create workspace" makes: blank (blankWorkspaceFiles),
|
|
861
|
+
* minus the laptop-only files, for the same reason as templateFiles. */
|
|
862
|
+
export declare function blankTemplateFiles(workspace: string): DeployFile[];
|
|
849
863
|
export declare function templateFiles(workspace: string): DeployFile[];
|
|
850
864
|
export declare const AGENT_TEMPLATE: (name: string) => string;
|
|
851
865
|
export declare const FLOW_TEMPLATE: (name: string, firstAgent: string) => string;
|
|
@@ -1341,6 +1355,12 @@ export declare function runCost(run: RunRecord): number;
|
|
|
1341
1355
|
* fan-out template is a row in the trace rather than work — billing any of
|
|
1342
1356
|
* them would charge for steps the customer can see never executed, which is
|
|
1343
1357
|
* the fastest way to lose an argument about an invoice.
|
|
1358
|
+
*
|
|
1359
|
+
* Tokens count only for steps that ran on the platform's own model
|
|
1360
|
+
* credential. A step on the account's own key (`credential` "provider: …" —
|
|
1361
|
+
* a provider: block, or Settings → Model) was paid for at the provider, and
|
|
1362
|
+
* billing it again here charged a BYOK customer twice. And a carried step's
|
|
1363
|
+
* tokens were billed where it ran, like its compute.
|
|
1344
1364
|
*/
|
|
1345
1365
|
export declare function runMeter(run: RunRecord): {
|
|
1346
1366
|
tokenCostUsd: number;
|
package/dist/src/store.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// or an agent, and belongs in git.
|
|
20
20
|
//
|
|
21
21
|
// An account is a named directory (per OKF: tenancy is a hosting concern).
|
|
22
|
+
import { expandPlatformVars } from "./host-env.js";
|
|
22
23
|
import fs from "node:fs";
|
|
23
24
|
import path from "node:path";
|
|
24
25
|
import { recordRevision, registerTreeReader } from "./history.js";
|
|
@@ -50,7 +51,7 @@ import { localeProblems } from "./locale.js";
|
|
|
50
51
|
import { readBundle, syncIndex, appendLog, provenanceMarks, syncWorkspaceBundles, } from "./okf.js";
|
|
51
52
|
import { readTransport, KINDS } from "./kinds.js";
|
|
52
53
|
import { providerPreset, looksOpenAiShaped, PROTECTED_PARAMS } from "./providers.js";
|
|
53
|
-
import { starterFiles, accountFiles } from "./starter.js";
|
|
54
|
+
import { starterFiles, accountFiles, blankWorkspaceFiles } from "./starter.js";
|
|
54
55
|
import { trimSlashes, MAX_EDITABLE_FILE } from "./paths.js";
|
|
55
56
|
// Where workspaces live. The hosted app keeps many under data/; the CLI runs
|
|
56
57
|
// against one folder, which is what `foldrun run ./my-desk` has to mean.
|
|
@@ -288,6 +289,18 @@ export function assertCanonicalCase(rel) {
|
|
|
288
289
|
// trigger-log.jsonl: the scheduler's record of why a flow did or did not
|
|
289
290
|
// fire (trigger-log.ts). Before it was listed here every deploy erased it.
|
|
290
291
|
export const PLATFORM_FILES = ["secrets.json", "hooks.json", "hook-deliveries.jsonl", ".repo.json", "trigger-log.jsonl"];
|
|
292
|
+
// The agent engine's own home files. Each step's sandbox runs the Claude
|
|
293
|
+
// Agent SDK — the engine, whatever model answers — with its home in the
|
|
294
|
+
// agent's folder, and the engine writes its settings (.claude.json, its
|
|
295
|
+
// backups), plugin caches and the full transcript of every session
|
|
296
|
+
// (.claude/projects/*.jsonl) there. Nothing in foldrun reads them. Copied
|
|
297
|
+
// back after every step, they had piled up to 5,274 files and 19 MB in one
|
|
298
|
+
// agent's folder on the box by 6 Oct 2026, carried every past conversation
|
|
299
|
+
// into every sandbox, and their clashing rewrites were the "write-back
|
|
300
|
+
// conflict on .claude.json" lines (and the big merges) of that day. A
|
|
301
|
+
// workspace's own .claude/agents/*.md — Claude Code subagents an author
|
|
302
|
+
// writes, imported as agents — sits at the workspace root and is untouched.
|
|
303
|
+
const ENGINE_HOME_RE = /^agents\/[^/]+\/\.claude([./]|$)/;
|
|
291
304
|
/** True for anything the platform, not the author or the agent, writes. */
|
|
292
305
|
export function isPlatformPath(rel) {
|
|
293
306
|
const norm = rel.replaceAll("\\", "/");
|
|
@@ -299,6 +312,8 @@ export function isPlatformPath(rel) {
|
|
|
299
312
|
return true;
|
|
300
313
|
if (norm === "trigger-log.jsonl.tmp")
|
|
301
314
|
return true;
|
|
315
|
+
if (ENGINE_HOME_RE.test(norm))
|
|
316
|
+
return true;
|
|
302
317
|
return norm === "runs" || norm.startsWith("runs/") || norm === ".foldrun" || norm.startsWith(".foldrun/");
|
|
303
318
|
}
|
|
304
319
|
/** What a deploy keeps whatever it ships — the rule above saveWorkspace's
|
|
@@ -547,6 +562,28 @@ export function fencedCodeBlock(body) {
|
|
|
547
562
|
export function toolIsOutward(data) {
|
|
548
563
|
return data.outward === true;
|
|
549
564
|
}
|
|
565
|
+
/** How much of a tool.md body rides to the model with the tool. */
|
|
566
|
+
export const TOOL_GUIDE_MAX = 6000;
|
|
567
|
+
/**
|
|
568
|
+
* The prose under a tool.md's frontmatter: how to call it, what it refuses,
|
|
569
|
+
* what its output means. Only `description:` used to reach the model, so
|
|
570
|
+
* agents invented what the body documented (5 Oct 2026). The fenced program
|
|
571
|
+
* of a single-file tool is code, not guidance, and stays out.
|
|
572
|
+
*/
|
|
573
|
+
export function toolGuide(body, inlineProgram = false) {
|
|
574
|
+
if (!body)
|
|
575
|
+
return undefined;
|
|
576
|
+
let text = body.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
577
|
+
// Only a single-file tool's program is cut; in a `run:` tool every fenced
|
|
578
|
+
// block is an example of a call, which is exactly what the model needs.
|
|
579
|
+
const inline = inlineProgram ? fencedCodeBlock(text) : null;
|
|
580
|
+
if (inline)
|
|
581
|
+
text = text.slice(0, inline.start) + text.slice(inline.end);
|
|
582
|
+
text = text.trim();
|
|
583
|
+
if (!text)
|
|
584
|
+
return undefined;
|
|
585
|
+
return text.length > TOOL_GUIDE_MAX ? `${text.slice(0, TOOL_GUIDE_MAX)}\n\n[the rest of tool.md is cut here]` : text;
|
|
586
|
+
}
|
|
550
587
|
export function parseToolDef(data, fallbackName, body) {
|
|
551
588
|
const name = typeof data.name === "string" ? data.name : fallbackName;
|
|
552
589
|
// `transport:`, or a legacy `type: http|script|mcp` from before `type:` meant
|
|
@@ -580,18 +617,28 @@ export function parseToolDef(data, fallbackName, body) {
|
|
|
580
617
|
};
|
|
581
618
|
}
|
|
582
619
|
if (kind === "script") {
|
|
620
|
+
// `guide` is the body's, never the frontmatter's: an author's `guide:`
|
|
621
|
+
// would otherwise ride to the model in place of the manual.
|
|
622
|
+
if ("guide" in data) {
|
|
623
|
+
data = { ...data };
|
|
624
|
+
delete data.guide;
|
|
625
|
+
}
|
|
583
626
|
// The program is a run: path, or the file's own fenced code block — the
|
|
584
627
|
// single-file form, which is what lets a script tool read and edit like
|
|
585
628
|
// every other markdown document.
|
|
586
|
-
if (typeof data.run === "string")
|
|
587
|
-
|
|
629
|
+
if (typeof data.run === "string") {
|
|
630
|
+
const guide = toolGuide(body);
|
|
631
|
+
return { kind: "script", name, spec: { ...data, name, ...(guide ? { guide } : {}) } };
|
|
632
|
+
}
|
|
588
633
|
const inline = body ? fencedCode(body) : null;
|
|
589
634
|
if (!inline)
|
|
590
635
|
return null;
|
|
591
|
-
|
|
636
|
+
const guide = toolGuide(body, true);
|
|
637
|
+
return { kind: "script", name, spec: { ...data, name, code: inline.code, codeExt: inline.ext, ...(guide ? { guide } : {}) } };
|
|
592
638
|
}
|
|
593
639
|
const [spec] = parseApis([{ ...data, name }]);
|
|
594
|
-
|
|
640
|
+
const guide = toolGuide(body);
|
|
641
|
+
return spec ? { kind: "http", name, spec: guide ? { ...spec, guide } : spec } : null;
|
|
595
642
|
}
|
|
596
643
|
/**
|
|
597
644
|
* Tools in a directory, in both shapes.
|
|
@@ -955,7 +1002,9 @@ export function parseApis(raw) {
|
|
|
955
1002
|
out.push({
|
|
956
1003
|
name: name.replace(/[^a-zA-Z0-9_]/g, "_"),
|
|
957
1004
|
...(Number(e.timeout) > 0 ? { timeout: Number(e.timeout) } : {}),
|
|
958
|
-
|
|
1005
|
+
// `${FOLDRUN_API_URL}` and `${FOLDRUN_PUBLIC_URL}` are the install's own
|
|
1006
|
+
// addresses, filled here on the host so a desk never names one.
|
|
1007
|
+
base: trimSlashes(expandPlatformVars(base)),
|
|
959
1008
|
description: typeof e.description === "string" ? e.description : "",
|
|
960
1009
|
headers: asRecordLocal(e.headers),
|
|
961
1010
|
query: asRecordLocal(e.query),
|
|
@@ -2430,6 +2479,11 @@ export function setWorkspaceDescription(tenant, workspace, description) {
|
|
|
2430
2479
|
* this copy still said `type: Agent` after the CLI's had moved to `kind:`,
|
|
2431
2480
|
* so every workspace made from the dashboard was born in the old format.
|
|
2432
2481
|
*/
|
|
2482
|
+
/** What the dashboard's "Create workspace" makes: blank (blankWorkspaceFiles),
|
|
2483
|
+
* minus the laptop-only files, for the same reason as templateFiles. */
|
|
2484
|
+
export function blankTemplateFiles(workspace) {
|
|
2485
|
+
return blankWorkspaceFiles(workspace).filter((f) => f.path === "AGENTS.md" || IN_WORKSPACE_DIR.test(f.path));
|
|
2486
|
+
}
|
|
2433
2487
|
export function templateFiles(workspace) {
|
|
2434
2488
|
// The starter minus what only matters on a laptop's disk: .gitignore
|
|
2435
2489
|
// guards a local clone's secrets, but the hosted store never keeps
|
|
@@ -2579,7 +2633,25 @@ export function runOutputPath(tenant, workspace, runId, agent, rel) {
|
|
|
2579
2633
|
}
|
|
2580
2634
|
export function readRun(tenant, workspace, runId) {
|
|
2581
2635
|
const p = runFilePath(tenant, workspace, runId);
|
|
2582
|
-
|
|
2636
|
+
// EFS/NFS: writeRun() replaces this file by rename, so an open()+read()
|
|
2637
|
+
// here can land on the just-unlinked inode and come back ESTALE (errno
|
|
2638
|
+
// -116, surfaced by Node as "Unknown system error -116", syscall read).
|
|
2639
|
+
// That isn't corruption — the next open() by path sees the committed new
|
|
2640
|
+
// inode — so re-read a few times on ESTALE and rethrow anything else.
|
|
2641
|
+
// (On dev-aws this surfaced as a CLI `--wait` call returning HTTP 500
|
|
2642
|
+
// from waitForRun's poll while the run itself completed fine.)
|
|
2643
|
+
const attempts = 5;
|
|
2644
|
+
for (let i = 1;; i++) {
|
|
2645
|
+
try {
|
|
2646
|
+
return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf8")) : null;
|
|
2647
|
+
}
|
|
2648
|
+
catch (e) {
|
|
2649
|
+
const err = e;
|
|
2650
|
+
if ((err.code === "ESTALE" || err.errno === -116) && i < attempts)
|
|
2651
|
+
continue;
|
|
2652
|
+
throw e;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2583
2655
|
}
|
|
2584
2656
|
export function writeRun(tenant, workspace, run) {
|
|
2585
2657
|
const p = runFilePath(tenant, workspace, run.id);
|
|
@@ -2825,12 +2897,19 @@ export function runCost(run) {
|
|
|
2825
2897
|
* fan-out template is a row in the trace rather than work — billing any of
|
|
2826
2898
|
* them would charge for steps the customer can see never executed, which is
|
|
2827
2899
|
* the fastest way to lose an argument about an invoice.
|
|
2900
|
+
*
|
|
2901
|
+
* Tokens count only for steps that ran on the platform's own model
|
|
2902
|
+
* credential. A step on the account's own key (`credential` "provider: …" —
|
|
2903
|
+
* a provider: block, or Settings → Model) was paid for at the provider, and
|
|
2904
|
+
* billing it again here charged a BYOK customer twice. And a carried step's
|
|
2905
|
+
* tokens were billed where it ran, like its compute.
|
|
2828
2906
|
*/
|
|
2829
2907
|
export function runMeter(run) {
|
|
2830
2908
|
let steps = 0;
|
|
2831
2909
|
let computeSecs = 0;
|
|
2832
2910
|
let smallSecs = 0;
|
|
2833
2911
|
let netBytes = 0;
|
|
2912
|
+
let tokenCostUsd = 0;
|
|
2834
2913
|
for (const s of run.steps) {
|
|
2835
2914
|
if (s.status !== "completed" && s.status !== "failed")
|
|
2836
2915
|
continue;
|
|
@@ -2838,13 +2917,15 @@ export function runMeter(run) {
|
|
|
2838
2917
|
if (s.carriedFrom)
|
|
2839
2918
|
continue;
|
|
2840
2919
|
steps += 1;
|
|
2920
|
+
if (!(s.credential ?? "").startsWith("provider:"))
|
|
2921
|
+
tokenCostUsd += s.costUsd ?? 0;
|
|
2841
2922
|
const secs = s.computeSecs ?? 0;
|
|
2842
2923
|
computeSecs += secs;
|
|
2843
2924
|
if (s.size === "small")
|
|
2844
2925
|
smallSecs += secs;
|
|
2845
2926
|
netBytes += (s.actual?.rxBytes ?? 0) + (s.actual?.txBytes ?? 0);
|
|
2846
2927
|
}
|
|
2847
|
-
return { tokenCostUsd
|
|
2928
|
+
return { tokenCostUsd, steps, computeSecs, smallSecs, netBytes };
|
|
2848
2929
|
}
|
|
2849
2930
|
/**
|
|
2850
2931
|
* The compute meter for hold+work pricing: core-seconds and GiB-seconds
|
package/dist/src/tool-names.d.ts
CHANGED
|
@@ -12,7 +12,9 @@ export declare const RETIRED_TOOLS: Record<string, string>;
|
|
|
12
12
|
* and a model provider's own search (`web: {search: zai}`) is the runtime's
|
|
13
13
|
* to swap in, not the author's to name. */
|
|
14
14
|
export declare const BUILTIN_TOOLS: Set<string>;
|
|
15
|
-
/** The platform's own groups, served in-process rather than by the SDK.
|
|
15
|
+
/** The platform's own groups, served in-process rather than by the SDK.
|
|
16
|
+
* `install` is granted to every agent anyway (install-tool.ts); naming it is
|
|
17
|
+
* accepted so a file that says so is not flagged. */
|
|
16
18
|
export declare const PLATFORM_GROUPS: Set<string>;
|
|
17
19
|
/** Would this name resolve to something the runtime provides? Built-ins win a
|
|
18
20
|
* clash, so a tool file of the same name is shadowed rather than granted. */
|
package/dist/src/tool-names.js
CHANGED
|
@@ -44,8 +44,10 @@ export const BUILTIN_TOOLS = new Set([
|
|
|
44
44
|
"Read", "Write", "Edit", "MultiEdit", "Glob", "Grep", "Bash",
|
|
45
45
|
"NotebookEdit", "TodoWrite",
|
|
46
46
|
]);
|
|
47
|
-
/** The platform's own groups, served in-process rather than by the SDK.
|
|
48
|
-
|
|
47
|
+
/** The platform's own groups, served in-process rather than by the SDK.
|
|
48
|
+
* `install` is granted to every agent anyway (install-tool.ts); naming it is
|
|
49
|
+
* accepted so a file that says so is not flagged. */
|
|
50
|
+
export const PLATFORM_GROUPS = new Set(["search", "history", "desks", "ask", "install"]);
|
|
49
51
|
/** Would this name resolve to something the runtime provides? Built-ins win a
|
|
50
52
|
* clash, so a tool file of the same name is shadowed rather than granted. */
|
|
51
53
|
export function isRuntimeTool(name) {
|
package/dist/src/tool-test.js
CHANGED
|
@@ -229,7 +229,14 @@ export async function testTool(tenant, workspace, def, input = {}) {
|
|
|
229
229
|
detail: `looked for ${file}\n\nThis is the commonest way to get a tool that silently never loads.`,
|
|
230
230
|
});
|
|
231
231
|
}
|
|
232
|
-
|
|
232
|
+
// The tool's own secrets, and the calling agent's `secrets:` — a run
|
|
233
|
+
// hands a script both. Testing gbp_read (whose agent, not the tool,
|
|
234
|
+
// declares GBP_OAUTH) said "GBP_OAUTH is not set" while every run of the
|
|
235
|
+
// same call worked (9 Oct 2026). Only an agent that grants the tool: the
|
|
236
|
+
// agents[0] stand-in above is there for its depth, not its keys.
|
|
237
|
+
const own = Array.isArray(def.spec.secrets) ? def.spec.secrets.map(String) : [];
|
|
238
|
+
const granting = caller && caller.ownTools.includes(def.name) ? caller.secrets : [];
|
|
239
|
+
const names = [...new Set([...own, ...granting])];
|
|
233
240
|
const { env: stored, missing } = resolveSecrets(tenant, names, workspace);
|
|
234
241
|
const live = await liveSecrets(stored, { tenant, workspace });
|
|
235
242
|
if ("error" in live) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ZipEntry {
|
|
2
|
+
path: string;
|
|
3
|
+
data: Buffer;
|
|
4
|
+
}
|
|
5
|
+
export declare class ZipError extends Error {
|
|
6
|
+
}
|
|
7
|
+
export interface UnzipLimits {
|
|
8
|
+
/** Most entries the archive may hold. */
|
|
9
|
+
maxEntries?: number;
|
|
10
|
+
/** Largest one file, uncompressed. */
|
|
11
|
+
maxFileBytes?: number;
|
|
12
|
+
/** All files together, uncompressed. */
|
|
13
|
+
maxTotalBytes?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function crc32(buf: Buffer): number;
|
|
16
|
+
export declare function zip(entries: ZipEntry[], now?: Date): Buffer;
|
|
17
|
+
export declare function unzip(buf: Buffer, limits?: UnzipLimits): ZipEntry[];
|
package/dist/src/zip.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
import zlib from "node:zlib";
|
|
11
|
+
export class ZipError extends Error {
|
|
12
|
+
}
|
|
13
|
+
const CRC_TABLE = (() => {
|
|
14
|
+
const t = new Uint32Array(256);
|
|
15
|
+
for (let n = 0; n < 256; n++) {
|
|
16
|
+
let c = n;
|
|
17
|
+
for (let k = 0; k < 8; k++)
|
|
18
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
19
|
+
t[n] = c >>> 0;
|
|
20
|
+
}
|
|
21
|
+
return t;
|
|
22
|
+
})();
|
|
23
|
+
export function crc32(buf) {
|
|
24
|
+
let c = 0xffffffff;
|
|
25
|
+
for (let i = 0; i < buf.length; i++)
|
|
26
|
+
c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
27
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
28
|
+
}
|
|
29
|
+
/** DOS date and time for the entry headers. */
|
|
30
|
+
function dosTime(d) {
|
|
31
|
+
return {
|
|
32
|
+
time: (d.getHours() << 11) | (d.getMinutes() << 5) | Math.floor(d.getSeconds() / 2),
|
|
33
|
+
date: (Math.max(0, d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function zip(entries, now = new Date()) {
|
|
37
|
+
const { time, date } = dosTime(now);
|
|
38
|
+
const locals = [];
|
|
39
|
+
const centrals = [];
|
|
40
|
+
let offset = 0;
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
for (const e of entries) {
|
|
43
|
+
const name = safeEntryPath(e.path);
|
|
44
|
+
if (seen.has(name))
|
|
45
|
+
throw new ZipError(`duplicate entry: ${name}`);
|
|
46
|
+
seen.add(name);
|
|
47
|
+
const nameBuf = Buffer.from(name, "utf8");
|
|
48
|
+
const deflated = zlib.deflateRawSync(e.data);
|
|
49
|
+
const stored = deflated.length >= e.data.length;
|
|
50
|
+
const body = stored ? e.data : deflated;
|
|
51
|
+
const crc = crc32(e.data);
|
|
52
|
+
const local = Buffer.alloc(30);
|
|
53
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
54
|
+
local.writeUInt16LE(20, 4); // version needed
|
|
55
|
+
local.writeUInt16LE(0x0800, 6); // UTF-8 names
|
|
56
|
+
local.writeUInt16LE(stored ? 0 : 8, 8);
|
|
57
|
+
local.writeUInt16LE(time, 10);
|
|
58
|
+
local.writeUInt16LE(date, 12);
|
|
59
|
+
local.writeUInt32LE(crc, 14);
|
|
60
|
+
local.writeUInt32LE(body.length, 18);
|
|
61
|
+
local.writeUInt32LE(e.data.length, 22);
|
|
62
|
+
local.writeUInt16LE(nameBuf.length, 26);
|
|
63
|
+
local.writeUInt16LE(0, 28);
|
|
64
|
+
locals.push(local, nameBuf, body);
|
|
65
|
+
const central = Buffer.alloc(46);
|
|
66
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
67
|
+
central.writeUInt16LE(0x0314, 4); // made by: unix, 2.0
|
|
68
|
+
central.writeUInt16LE(20, 6);
|
|
69
|
+
central.writeUInt16LE(0x0800, 8);
|
|
70
|
+
central.writeUInt16LE(stored ? 0 : 8, 10);
|
|
71
|
+
central.writeUInt16LE(time, 12);
|
|
72
|
+
central.writeUInt16LE(date, 14);
|
|
73
|
+
central.writeUInt32LE(crc, 16);
|
|
74
|
+
central.writeUInt32LE(body.length, 20);
|
|
75
|
+
central.writeUInt32LE(e.data.length, 24);
|
|
76
|
+
central.writeUInt16LE(nameBuf.length, 28);
|
|
77
|
+
central.writeUInt32LE((0o100644 << 16) >>> 0, 38); // a regular file, rw-r--r--
|
|
78
|
+
central.writeUInt32LE(offset, 42);
|
|
79
|
+
centrals.push(central, nameBuf);
|
|
80
|
+
offset += local.length + nameBuf.length + body.length;
|
|
81
|
+
}
|
|
82
|
+
const cd = Buffer.concat(centrals);
|
|
83
|
+
const end = Buffer.alloc(22);
|
|
84
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
85
|
+
end.writeUInt16LE(entries.length, 8);
|
|
86
|
+
end.writeUInt16LE(entries.length, 10);
|
|
87
|
+
end.writeUInt32LE(cd.length, 12);
|
|
88
|
+
end.writeUInt32LE(offset, 16);
|
|
89
|
+
return Buffer.concat([...locals, cd, end]);
|
|
90
|
+
}
|
|
91
|
+
export function unzip(buf, limits = {}) {
|
|
92
|
+
const maxEntries = limits.maxEntries ?? 2000;
|
|
93
|
+
const maxFile = limits.maxFileBytes ?? 1024 * 1024;
|
|
94
|
+
const maxTotal = limits.maxTotalBytes ?? 50 * 1024 * 1024;
|
|
95
|
+
// The end-of-central-directory record: within the last 22 + 65535 bytes.
|
|
96
|
+
let eocd = -1;
|
|
97
|
+
for (let i = buf.length - 22; i >= Math.max(0, buf.length - 22 - 0xffff); i--) {
|
|
98
|
+
if (buf.readUInt32LE(i) === 0x06054b50) {
|
|
99
|
+
eocd = i;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (eocd < 0)
|
|
104
|
+
throw new ZipError("not a zip file");
|
|
105
|
+
const count = buf.readUInt16LE(eocd + 10);
|
|
106
|
+
const cdSize = buf.readUInt32LE(eocd + 12);
|
|
107
|
+
const cdOffset = buf.readUInt32LE(eocd + 16);
|
|
108
|
+
if (count === 0xffff || cdOffset === 0xffffffff)
|
|
109
|
+
throw new ZipError("zip64 archives are not supported");
|
|
110
|
+
if (count > maxEntries)
|
|
111
|
+
throw new ZipError(`too many files: ${count} (at most ${maxEntries})`);
|
|
112
|
+
if (cdOffset + cdSize > eocd)
|
|
113
|
+
throw new ZipError("corrupt zip: central directory out of range");
|
|
114
|
+
const out = [];
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
let total = 0;
|
|
117
|
+
let p = cdOffset;
|
|
118
|
+
for (let n = 0; n < count; n++) {
|
|
119
|
+
if (p + 46 > eocd || buf.readUInt32LE(p) !== 0x02014b50)
|
|
120
|
+
throw new ZipError("corrupt zip: bad central directory entry");
|
|
121
|
+
const flags = buf.readUInt16LE(p + 8);
|
|
122
|
+
const method = buf.readUInt16LE(p + 10);
|
|
123
|
+
const crc = buf.readUInt32LE(p + 16);
|
|
124
|
+
const csize = buf.readUInt32LE(p + 20);
|
|
125
|
+
const usize = buf.readUInt32LE(p + 24);
|
|
126
|
+
const nameLen = buf.readUInt16LE(p + 28);
|
|
127
|
+
const extraLen = buf.readUInt16LE(p + 30);
|
|
128
|
+
const commentLen = buf.readUInt16LE(p + 32);
|
|
129
|
+
const localAt = buf.readUInt32LE(p + 42);
|
|
130
|
+
if (p + 46 + nameLen > eocd)
|
|
131
|
+
throw new ZipError("corrupt zip: name out of range");
|
|
132
|
+
const rawName = buf.subarray(p + 46, p + 46 + nameLen).toString("utf8");
|
|
133
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
134
|
+
if (rawName.endsWith("/"))
|
|
135
|
+
continue; // a directory entry
|
|
136
|
+
// macOS "Compress" adds these beside every file; they are not content.
|
|
137
|
+
if (/(^|\/)__MACOSX\//.test(rawName) || /(^|\/)\.DS_Store$/.test(rawName))
|
|
138
|
+
continue;
|
|
139
|
+
if (flags & 0x1)
|
|
140
|
+
throw new ZipError(`${rawName}: encrypted entries are not supported`);
|
|
141
|
+
if (method !== 0 && method !== 8)
|
|
142
|
+
throw new ZipError(`${rawName}: compression method ${method} is not supported`);
|
|
143
|
+
const name = safeEntryPath(rawName);
|
|
144
|
+
if (seen.has(name))
|
|
145
|
+
throw new ZipError(`duplicate entry: ${name}`);
|
|
146
|
+
seen.add(name);
|
|
147
|
+
if (usize > maxFile)
|
|
148
|
+
throw new ZipError(`${name} is too large (${usize} bytes, at most ${maxFile})`);
|
|
149
|
+
total += usize;
|
|
150
|
+
if (total > maxTotal)
|
|
151
|
+
throw new ZipError(`the files are too large together (more than ${maxTotal} bytes)`);
|
|
152
|
+
if (localAt + 30 > buf.length || buf.readUInt32LE(localAt) !== 0x04034b50)
|
|
153
|
+
throw new ZipError(`${name}: corrupt local header`);
|
|
154
|
+
const start = localAt + 30 + buf.readUInt16LE(localAt + 26) + buf.readUInt16LE(localAt + 28);
|
|
155
|
+
if (start + csize > buf.length)
|
|
156
|
+
throw new ZipError(`${name}: data out of range`);
|
|
157
|
+
const raw = buf.subarray(start, start + csize);
|
|
158
|
+
let data;
|
|
159
|
+
try {
|
|
160
|
+
// maxOutputLength: the header's size is the attacker's to choose, so the
|
|
161
|
+
// inflate itself is capped too — a bomb stops here, not in memory.
|
|
162
|
+
data = method === 0 ? Buffer.from(raw) : zlib.inflateRawSync(raw, { maxOutputLength: maxFile + 1 });
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new ZipError(`${name}: could not be decompressed`);
|
|
166
|
+
}
|
|
167
|
+
if (data.length !== usize)
|
|
168
|
+
throw new ZipError(`${name}: size does not match its header`);
|
|
169
|
+
if (crc32(data) !== crc)
|
|
170
|
+
throw new ZipError(`${name}: checksum mismatch`);
|
|
171
|
+
out.push({ path: name, data });
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
/** A relative, forward-slash path that stays inside the archive's root. */
|
|
176
|
+
function safeEntryPath(raw) {
|
|
177
|
+
const p = raw.replaceAll("\\", "/");
|
|
178
|
+
if (p.startsWith("/") || /^[a-zA-Z]:/.test(p) || p.includes("\0"))
|
|
179
|
+
throw new ZipError(`unsafe path: ${raw}`);
|
|
180
|
+
const parts = p.split("/").filter((s) => s !== "" && s !== ".");
|
|
181
|
+
if (parts.length === 0 || parts.some((s) => s === ".."))
|
|
182
|
+
throw new ZipError(`unsafe path: ${raw}`);
|
|
183
|
+
return parts.join("/");
|
|
184
|
+
}
|
package/index.ts
CHANGED
|
@@ -34,6 +34,8 @@ export * from "./src/linkable.ts";
|
|
|
34
34
|
export * from "./src/refs.ts";
|
|
35
35
|
export * from "./src/tool-names.ts";
|
|
36
36
|
export * from "./src/history.ts";
|
|
37
|
+
export * from "./src/zip.ts";
|
|
38
|
+
export * from "./src/package.ts";
|
|
37
39
|
export * from "./src/gitrepo.ts";
|
|
38
40
|
export * from "./src/storage.ts";
|
|
39
41
|
export * from "./src/secrets.ts";
|
|
@@ -43,6 +45,7 @@ export * from "./src/notify.ts";
|
|
|
43
45
|
export * from "./src/webhook.ts";
|
|
44
46
|
export * from "./src/approvals.ts";
|
|
45
47
|
export * from "./src/deploy.ts";
|
|
48
|
+
export * from "./src/agent-import.ts";
|
|
46
49
|
export * from "./src/account.ts";
|
|
47
50
|
export * from "./src/tar.ts";
|
|
48
51
|
export * from "./src/git.ts";
|