@foldrun/core 0.3.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/README.md +27 -10
- 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/approvals.d.ts +104 -1
- package/dist/src/approvals.js +192 -2
- package/dist/src/browser-pod.d.ts +129 -0
- package/dist/src/browser-pod.js +284 -0
- package/dist/src/budget.d.ts +32 -0
- package/dist/src/budget.js +177 -0
- package/dist/src/catalog.js +3 -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 +224 -4
- package/dist/src/deploy.d.ts +33 -0
- package/dist/src/deploy.js +145 -6
- package/dist/src/egress.d.ts +26 -2
- package/dist/src/egress.js +8 -1
- package/dist/src/eol.d.ts +2 -0
- package/dist/src/eol.js +7 -0
- package/dist/src/evals.d.ts +9 -0
- package/dist/src/evals.js +26 -13
- package/dist/src/flow-lint.d.ts +12 -0
- package/dist/src/flow-lint.js +331 -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.d.ts +8 -0
- package/dist/src/host-env.js +48 -0
- package/dist/src/json-schema.d.ts +12 -0
- package/dist/src/json-schema.js +202 -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 +43 -4
- 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 +132 -8
- package/dist/src/notify.js +360 -58
- 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 +33 -0
- package/dist/src/paths.js +87 -0
- package/dist/src/platform.d.ts +144 -4
- package/dist/src/platform.js +27 -3
- package/dist/src/provider-health.d.ts +55 -0
- package/dist/src/provider-health.js +233 -0
- 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 +206 -17
- package/dist/src/run-container.js +547 -80
- package/dist/src/runner.d.ts +179 -10
- package/dist/src/runner.js +1423 -169
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +493 -58
- package/dist/src/script-tools.d.ts +15 -2
- package/dist/src/script-tools.js +44 -4
- package/dist/src/secret-files.d.ts +6 -5
- package/dist/src/secret-files.js +24 -11
- package/dist/src/secret-health.d.ts +83 -0
- package/dist/src/secret-health.js +177 -0
- package/dist/src/secrets.d.ts +38 -1
- package/dist/src/secrets.js +197 -96
- package/dist/src/starter.d.ts +9 -0
- package/dist/src/starter.js +95 -11
- package/dist/src/step-exec.d.ts +106 -1
- package/dist/src/step-exec.js +558 -85
- 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 +492 -38
- package/dist/src/store.js +882 -69
- package/dist/src/subagents.d.ts +75 -0
- package/dist/src/subagents.js +149 -0
- package/dist/src/tar.js +2 -1
- package/dist/src/test-mode.d.ts +111 -0
- package/dist/src/test-mode.js +285 -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 +152 -33
- package/dist/src/translator.d.ts +8 -2
- package/dist/src/translator.js +71 -24
- package/dist/src/trigger-log.d.ts +43 -0
- package/dist/src/trigger-log.js +122 -0
- package/dist/src/triggers.d.ts +23 -0
- package/dist/src/triggers.js +101 -12
- 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 +19 -3
- package/dist/src/webhook.js +50 -11
- package/index.ts +3 -0
- package/package.json +5 -5
- package/src/account.ts +149 -0
- package/src/api-tools.ts +32 -4
- package/src/approvals.ts +253 -2
- package/src/browser-pod.ts +313 -0
- package/src/budget.ts +178 -0
- package/src/catalog.ts +3 -1
- package/src/clock.ts +149 -0
- package/src/completions.ts +17 -8
- package/src/confine.ts +224 -4
- package/src/deploy.ts +137 -6
- package/src/egress.ts +32 -4
- package/src/eol.ts +7 -0
- package/src/evals.ts +36 -11
- package/src/flow-lint.ts +371 -1
- package/src/flow-patterns.ts +597 -0
- package/src/gitrepo.ts +48 -0
- package/src/host-env.ts +51 -0
- package/src/json-schema.ts +184 -0
- package/src/kinds.ts +1 -1
- package/src/language.ts +62 -0
- package/src/layout.ts +228 -0
- package/src/library.ts +50 -4
- 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 +391 -44
- package/src/openapi.ts +5 -3
- package/src/operator.ts +300 -0
- package/src/paths.ts +83 -0
- package/src/platform.ts +158 -5
- package/src/provider-health.ts +275 -0
- package/src/providers.ts +1368 -6
- package/src/refusal.ts +320 -0
- package/src/run-container.ts +611 -76
- package/src/runner.ts +1463 -165
- package/src/runtime.ts +487 -55
- package/src/script-tools.ts +55 -6
- package/src/secret-files.ts +22 -10
- package/src/secret-health.ts +230 -0
- package/src/secrets.ts +191 -95
- package/src/starter.ts +96 -11
- package/src/step-exec.ts +548 -41
- package/src/step-removal.ts +59 -0
- package/src/storage.ts +149 -24
- package/src/store.ts +1227 -74
- package/src/subagents.ts +189 -0
- package/src/tar.ts +2 -1
- package/src/test-mode.ts +311 -0
- package/src/tool-names.ts +40 -11
- package/src/tool-programs.ts +179 -0
- package/src/tool-test.ts +159 -32
- package/src/translator.ts +74 -23
- package/src/trigger-log.ts +160 -0
- package/src/triggers.ts +102 -12
- package/src/untrusted-fetch.ts +88 -0
- package/src/web-actions.ts +237 -0
- package/src/webhook.ts +58 -11
- package/templates/hello/agents/curator/agent.md +1 -1
- package/templates/hello/agents/notetaker/agent.md +1 -1
package/README.md
CHANGED
|
@@ -188,19 +188,36 @@ a hand-edited template cannot ship an index that disagrees with its own files.
|
|
|
188
188
|
## Hosting it
|
|
189
189
|
|
|
190
190
|
This package is the framework: everything needed to read a workspace and
|
|
191
|
-
run it on the machine you are on. The
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
191
|
+
run it on the machine you are on. The platform — the dashboard, a queue and
|
|
192
|
+
its worker, one sandbox per step, a vault, metering, a team — is built on
|
|
193
|
+
it, in a separate package that plugs in through `src/platform.ts`. Nothing
|
|
194
|
+
in this repository imports it, so what is here is exactly what is open.
|
|
195
|
+
|
|
196
|
+
**Run the platform yourself:** one container, one command —
|
|
197
|
+
[self-hosting/](self-hosting/). Sign up at `localhost:3900`, and
|
|
198
|
+
`foldrun deploy` moves a workspace from your machine to it unchanged. The
|
|
199
|
+
images are the same builds that run foldrun's own installation, published
|
|
200
|
+
under the same versions.
|
|
198
201
|
|
|
199
202
|
## Requirements
|
|
200
203
|
|
|
201
|
-
Node 22+. Docker or Podman if your agents run scripts.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
Node 22+. Docker or Podman if your agents run scripts. `foldrun check` needs
|
|
205
|
+
no credential at all.
|
|
206
|
+
|
|
207
|
+
To *run* an agent you need one of:
|
|
208
|
+
|
|
209
|
+
- **`ANTHROPIC_API_KEY`** — an API key from
|
|
210
|
+
[console.anthropic.com](https://console.anthropic.com).
|
|
211
|
+
- **Your own provider.** An agent that names a `provider:` needs no
|
|
212
|
+
Anthropic key: 29 endpoints are known by name, including `ollama`,
|
|
213
|
+
`lmstudio` and `vllm`, so a local model is a complete answer. Anything
|
|
214
|
+
not in that list works by spelling out `base_url:`, `format:` and
|
|
215
|
+
`auth:`. See [providers](https://docs.foldrun.io/providers).
|
|
216
|
+
|
|
217
|
+
A **Claude Code or claude.ai subscription login is not one of them**, and
|
|
218
|
+
the CLI will not look for one. Anthropic does not permit products built on
|
|
219
|
+
its Agent SDK to run on consumer subscriptions, so using that login here
|
|
220
|
+
would put you outside their terms — an API key is the supported path.
|
|
204
221
|
|
|
205
222
|
## Status
|
|
206
223
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from "./src/layout.ts";
|
|
1
2
|
export * from "./src/paths.ts";
|
|
2
3
|
export * from "./src/store.ts";
|
|
3
4
|
export * from "./src/starter.ts";
|
|
@@ -10,6 +11,7 @@ export * from "./src/model-probe.ts";
|
|
|
10
11
|
export * from "./src/providers.ts";
|
|
11
12
|
export * from "./src/translator.ts";
|
|
12
13
|
export * from "./src/arrange.ts";
|
|
14
|
+
export * from "./src/flow-patterns.ts";
|
|
13
15
|
export * from "./src/completions.ts";
|
|
14
16
|
export * from "./src/confine.ts";
|
|
15
17
|
export * from "./src/platform.ts";
|
|
@@ -28,5 +30,6 @@ export * from "./src/notify.ts";
|
|
|
28
30
|
export * from "./src/webhook.ts";
|
|
29
31
|
export * from "./src/approvals.ts";
|
|
30
32
|
export * from "./src/deploy.ts";
|
|
33
|
+
export * from "./src/account.ts";
|
|
31
34
|
export * from "./src/tar.ts";
|
|
32
35
|
export * from "./src/git.ts";
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// its worker, metering and billing, pods on a cluster, per-account keys in a
|
|
10
10
|
// database, previews, share links, tenancy — lives in its own private package
|
|
11
11
|
// and plugs in through platform.ts. Nothing here imports it.
|
|
12
|
+
export * from "./src/layout.js";
|
|
12
13
|
export * from "./src/paths.js";
|
|
13
14
|
export * from "./src/store.js";
|
|
14
15
|
export * from "./src/starter.js";
|
|
@@ -21,6 +22,7 @@ export * from "./src/model-probe.js";
|
|
|
21
22
|
export * from "./src/providers.js";
|
|
22
23
|
export * from "./src/translator.js";
|
|
23
24
|
export * from "./src/arrange.js";
|
|
25
|
+
export * from "./src/flow-patterns.js";
|
|
24
26
|
export * from "./src/completions.js";
|
|
25
27
|
export * from "./src/confine.js";
|
|
26
28
|
export * from "./src/platform.js";
|
|
@@ -39,5 +41,6 @@ export * from "./src/notify.js";
|
|
|
39
41
|
export * from "./src/webhook.js";
|
|
40
42
|
export * from "./src/approvals.js";
|
|
41
43
|
export * from "./src/deploy.js";
|
|
44
|
+
export * from "./src/account.js";
|
|
42
45
|
export * from "./src/tar.js";
|
|
43
46
|
export * from "./src/git.js";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type LibraryKind } from "./library.ts";
|
|
2
|
+
import { type Layout } from "./layout.ts";
|
|
3
|
+
import type { DeployFile } from "./store.ts";
|
|
4
|
+
export interface AccountWorkspace {
|
|
5
|
+
name: string;
|
|
6
|
+
dir: string;
|
|
7
|
+
files: DeployFile[];
|
|
8
|
+
}
|
|
9
|
+
export interface LibraryFile {
|
|
10
|
+
kind: LibraryKind;
|
|
11
|
+
/** Relative to the kind's directory — what the library API calls `path`. */
|
|
12
|
+
path: string;
|
|
13
|
+
content: string;
|
|
14
|
+
}
|
|
15
|
+
export interface AccountTree {
|
|
16
|
+
root: string;
|
|
17
|
+
/** The account-scope AGENTS.md, or null when there is none. */
|
|
18
|
+
agentsMd: string | null;
|
|
19
|
+
library: LibraryFile[];
|
|
20
|
+
workspaces: AccountWorkspace[];
|
|
21
|
+
}
|
|
22
|
+
/** The shared library under an account root, flattened to one list. */
|
|
23
|
+
export declare function readLibraryTree(accountRoot: string): LibraryFile[];
|
|
24
|
+
/**
|
|
25
|
+
* Everything an account folder would push: its AGENTS.md, its library, and
|
|
26
|
+
* every workspace under it.
|
|
27
|
+
*
|
|
28
|
+
* `only` narrows it to one workspace — `foldrun deploy <name>` — and still
|
|
29
|
+
* carries the account scope, because a workspace deployed without the library
|
|
30
|
+
* its agents name is a workspace that fails on its first run.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readAccountTree(accountRoot: string, only?: string): AccountTree;
|
|
33
|
+
/**
|
|
34
|
+
* The account tree for wherever you are standing — an account root, a
|
|
35
|
+
* workspace inside one, or a flat workspace.
|
|
36
|
+
*
|
|
37
|
+
* A flat workspace is reported as an account of one whose root is its parent,
|
|
38
|
+
* which is exactly what `accountDir` and `libraryDir` already believe about
|
|
39
|
+
* it. So the deploy path has no branch for the old layout: it pushes one
|
|
40
|
+
* workspace and whatever library sits beside it, which is what it did before.
|
|
41
|
+
*/
|
|
42
|
+
export declare function accountTreeFrom(from: string, only?: string): {
|
|
43
|
+
layout: Layout;
|
|
44
|
+
tree: AccountTree;
|
|
45
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// An account folder, read as the thing a deploy pushes.
|
|
2
|
+
//
|
|
3
|
+
// `readTree` answers the same question for one workspace: what, of the files
|
|
4
|
+
// on disk, is source? This is that question one scope up — the account's own
|
|
5
|
+
// AGENTS.md, the shared `library/`, and every workspace under `workspaces/` —
|
|
6
|
+
// so `foldrun deploy` at an account root has one list to walk and one shape to
|
|
7
|
+
// report, rather than three special cases written at the call site.
|
|
8
|
+
//
|
|
9
|
+
// Nothing here talks to a platform. It reads a directory and says what is in
|
|
10
|
+
// it; who gets sent where is the CLI's business, because the endpoints differ
|
|
11
|
+
// per part (a workspace deploys whole, a library file writes one at a time)
|
|
12
|
+
// and that asymmetry belongs next to the fetch calls, not in here.
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { readTree } from "./deploy.js";
|
|
16
|
+
import { LIBRARY_KINDS } from "./library.js";
|
|
17
|
+
import { detectLayout, listWorkspaceDirs } from "./layout.js";
|
|
18
|
+
/** Anything that is bookkeeping rather than source, at either scope. */
|
|
19
|
+
const NOT_SOURCE = new Set([".foldrun", ".git", "node_modules", "runs", "outputs", ".DS_Store"]);
|
|
20
|
+
function walkFiles(root, rel = "") {
|
|
21
|
+
const out = [];
|
|
22
|
+
let entries;
|
|
23
|
+
try {
|
|
24
|
+
entries = fs.readdirSync(root).sort();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (NOT_SOURCE.has(entry))
|
|
31
|
+
continue;
|
|
32
|
+
const full = path.join(root, entry);
|
|
33
|
+
const next = rel ? `${rel}/${entry}` : entry;
|
|
34
|
+
try {
|
|
35
|
+
if (fs.lstatSync(full).isSymbolicLink())
|
|
36
|
+
continue;
|
|
37
|
+
if (fs.statSync(full).isDirectory())
|
|
38
|
+
out.push(...walkFiles(full, next));
|
|
39
|
+
else
|
|
40
|
+
out.push({ path: next, content: fs.readFileSync(full, "utf8") });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* unreadable: not source */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/** The shared library under an account root, flattened to one list. */
|
|
49
|
+
export function readLibraryTree(accountRoot) {
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const kind of LIBRARY_KINDS) {
|
|
52
|
+
const dir = path.join(accountRoot, "library", kind);
|
|
53
|
+
for (const f of walkFiles(dir)) {
|
|
54
|
+
// `.gitkeep` and friends keep an empty library directory in git and are
|
|
55
|
+
// not library entries: the write endpoint refuses a path with no known
|
|
56
|
+
// extension, so shipping one would fail the deploy over a placeholder.
|
|
57
|
+
if (f.path.split("/").some((seg) => seg.startsWith(".")))
|
|
58
|
+
continue;
|
|
59
|
+
out.push({ kind, path: f.path, content: f.content });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Everything an account folder would push: its AGENTS.md, its library, and
|
|
66
|
+
* every workspace under it.
|
|
67
|
+
*
|
|
68
|
+
* `only` narrows it to one workspace — `foldrun deploy <name>` — and still
|
|
69
|
+
* carries the account scope, because a workspace deployed without the library
|
|
70
|
+
* its agents name is a workspace that fails on its first run.
|
|
71
|
+
*/
|
|
72
|
+
export function readAccountTree(accountRoot, only) {
|
|
73
|
+
const root = path.resolve(accountRoot);
|
|
74
|
+
const agentsMdFile = path.join(root, "AGENTS.md");
|
|
75
|
+
let agentsMd = null;
|
|
76
|
+
try {
|
|
77
|
+
agentsMd = fs.readFileSync(agentsMdFile, "utf8");
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* an account with no AGENTS.md pushes none */
|
|
81
|
+
}
|
|
82
|
+
const names = listWorkspaceDirs(root).filter((n) => (only ? n === only : true));
|
|
83
|
+
if (only && names.length === 0) {
|
|
84
|
+
throw new Error(`no workspace "${only}" in ${root} — have: ${listWorkspaceDirs(root).join(", ") || "none"}`);
|
|
85
|
+
}
|
|
86
|
+
const wsRoot = path.join(root, "workspaces");
|
|
87
|
+
const legacy = path.join(root, "projects");
|
|
88
|
+
const base = fs.existsSync(wsRoot) ? wsRoot : legacy;
|
|
89
|
+
return {
|
|
90
|
+
root,
|
|
91
|
+
agentsMd,
|
|
92
|
+
library: readLibraryTree(root),
|
|
93
|
+
workspaces: names.map((name) => {
|
|
94
|
+
const dir = path.join(base, name);
|
|
95
|
+
return { name, dir, files: readTree(dir) };
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The account tree for wherever you are standing — an account root, a
|
|
101
|
+
* workspace inside one, or a flat workspace.
|
|
102
|
+
*
|
|
103
|
+
* A flat workspace is reported as an account of one whose root is its parent,
|
|
104
|
+
* which is exactly what `accountDir` and `libraryDir` already believe about
|
|
105
|
+
* it. So the deploy path has no branch for the old layout: it pushes one
|
|
106
|
+
* workspace and whatever library sits beside it, which is what it did before.
|
|
107
|
+
*/
|
|
108
|
+
export function accountTreeFrom(from, only) {
|
|
109
|
+
const layout = detectLayout(from);
|
|
110
|
+
if (layout.kind === "flat" || layout.kind === "empty") {
|
|
111
|
+
const dir = layout.workspaceDir;
|
|
112
|
+
const name = path.basename(dir);
|
|
113
|
+
if (only && only !== name)
|
|
114
|
+
throw new Error(`${dir} is a single workspace called "${name}", not "${only}"`);
|
|
115
|
+
return {
|
|
116
|
+
layout,
|
|
117
|
+
tree: {
|
|
118
|
+
root: layout.accountRoot,
|
|
119
|
+
// A flat workspace's AGENTS.md belongs to the workspace, not the
|
|
120
|
+
// account — pushing it as account scope would apply one desk's rules
|
|
121
|
+
// to every other desk in the installation.
|
|
122
|
+
agentsMd: null,
|
|
123
|
+
library: readLibraryTree(layout.accountRoot),
|
|
124
|
+
workspaces: [{ name, dir, files: readTree(dir) }],
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const only2 = only ?? (layout.kind === "workspace-in-account" ? layout.workspace : undefined);
|
|
129
|
+
return { layout, tree: readAccountTree(layout.accountRoot, only2) };
|
|
130
|
+
}
|
package/dist/src/api-tools.d.ts
CHANGED
|
@@ -25,6 +25,14 @@ export interface ApiToolResult {
|
|
|
25
25
|
server: ReturnType<typeof createSdkMcpServer> | null;
|
|
26
26
|
/** Tool names to allow, e.g. mcp__foldrun_apis__call_google_ads. */
|
|
27
27
|
toolNames: string[];
|
|
28
|
+
/** SDK tool name → the API it was built for, recorded as each tool is
|
|
29
|
+
* made: `crm`'s operation `admin_list` is `crm_admin_list` beside an API
|
|
30
|
+
* named `crm_admin`, so the name alone cannot say whose it is. */
|
|
31
|
+
owners: Record<string, string>;
|
|
32
|
+
/** SDK tool name → the HTTP method of a typed operation. A generic
|
|
33
|
+
* call_<api> carries its method in each call's input instead. Read by the
|
|
34
|
+
* browser-pod fallback (browser-pod.ts) to tell a GET from a write. */
|
|
35
|
+
methods: Record<string, string>;
|
|
28
36
|
/** Secrets referenced but not set — surfaced as run warnings. */
|
|
29
37
|
missingSecrets: string[];
|
|
30
38
|
/** Human-readable lines describing each API, appended to the system prompt. */
|
package/dist/src/api-tools.js
CHANGED
|
@@ -258,7 +258,7 @@ export function buildApiTools(tenant, apis, workspace,
|
|
|
258
258
|
// crossed the boundary, and there is no vault in there to ask.
|
|
259
259
|
resolved) {
|
|
260
260
|
if (apis.length === 0) {
|
|
261
|
-
return { server: null, toolNames: [], missingSecrets: [], promptLines: [], drainLog: () => [] };
|
|
261
|
+
return { server: null, toolNames: [], owners: {}, methods: {}, missingSecrets: [], promptLines: [], drainLog: () => [] };
|
|
262
262
|
}
|
|
263
263
|
const neededSecrets = [...new Set(apis.flatMap(secretsUsedByApi))];
|
|
264
264
|
const { env, missing } = resolved
|
|
@@ -271,23 +271,39 @@ resolved) {
|
|
|
271
271
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- the SDK types its own list this way
|
|
272
272
|
const tools = [];
|
|
273
273
|
const toolNames = [];
|
|
274
|
+
const owners = {};
|
|
275
|
+
const methods = {};
|
|
274
276
|
const promptLines = [];
|
|
275
277
|
for (const api of apis) {
|
|
276
278
|
// One bucket per API per step: the limit is the vendor's, the scope is
|
|
277
279
|
// this process. Two steps on two workers each get their own.
|
|
278
280
|
const bucket = api.rate ? new TokenBucket(api.rate.count, api.rate.perSec) : null;
|
|
279
281
|
const ops = api.resolvedOperations ?? [];
|
|
280
|
-
// An explicit `operations:` list means "only these": no escape hatch
|
|
281
|
-
|
|
282
|
+
// An explicit `operations:` list means "only these": no escape hatch,
|
|
283
|
+
// ever. It fails closed. If the document did not load, or yielded none of
|
|
284
|
+
// the listed operations, the API gets no tools this run rather than the
|
|
285
|
+
// generic one: an allowlist that opens when a vendor renames an
|
|
286
|
+
// operation or a URL moves is not an allowlist.
|
|
287
|
+
const allowlisted = !!api.operations?.length;
|
|
288
|
+
const withGeneric = !allowlisted;
|
|
289
|
+
if (allowlisted && !ops.length) {
|
|
290
|
+
promptLines.push(`- **${api.name}** — unavailable this run: its \`operations:\` allowlist resolved no operations, ` +
|
|
291
|
+
`so no tool is exposed. Report it as blocked; do not reach this API another way.`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
282
294
|
const typedNames = [];
|
|
283
295
|
for (const op of ops) {
|
|
284
296
|
tools.push(typedTool(api, op, env, bucket, log));
|
|
285
297
|
typedNames.push(typedToolName(api.name, op.id));
|
|
298
|
+
methods[`mcp__foldrun_apis__${typedToolName(api.name, op.id)}`] = String(op.method ?? "").toUpperCase();
|
|
286
299
|
}
|
|
287
300
|
if (withGeneric)
|
|
288
301
|
tools.push(genericTool(api, env, bucket, log));
|
|
289
302
|
const names = [...typedNames, ...(withGeneric ? [`call_${api.name}`] : [])];
|
|
290
|
-
|
|
303
|
+
for (const n of names) {
|
|
304
|
+
toolNames.push(`mcp__foldrun_apis__${n}`);
|
|
305
|
+
owners[`mcp__foldrun_apis__${n}`] = api.name;
|
|
306
|
+
}
|
|
291
307
|
const rateLine = api.rate ? ` Rate limit ${api.rate.count}/${Math.round(api.rate.count / api.rate.perSec)}s — over it, calls wait.` : "";
|
|
292
308
|
if (ops.length) {
|
|
293
309
|
const listed = ops.slice(0, PROMPT_LIST_CAP).map((op) => ` - \`${typedToolName(api.name, op.id)}\` — ${op.summary}`);
|
|
@@ -306,6 +322,8 @@ resolved) {
|
|
|
306
322
|
return {
|
|
307
323
|
server,
|
|
308
324
|
toolNames,
|
|
325
|
+
owners,
|
|
326
|
+
methods,
|
|
309
327
|
missingSecrets: missing,
|
|
310
328
|
promptLines,
|
|
311
329
|
drainLog: () => log.splice(0, log.length),
|
package/dist/src/approvals.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RunRecord } from "./store.ts";
|
|
1
|
+
import { type FlowInfo, type RunRecord } from "./store.ts";
|
|
2
2
|
export interface ApprovalDecision {
|
|
3
3
|
decision: "approve" | "reject";
|
|
4
4
|
/** Guidance for the step being approved — rides the record into its prompt. */
|
|
@@ -9,7 +9,97 @@ export interface ApprovalDecision {
|
|
|
9
9
|
step?: number;
|
|
10
10
|
/** Who decided, as it reads after the verb: "by a human", "via emailed link". */
|
|
11
11
|
by: string;
|
|
12
|
+
/**
|
|
13
|
+
* The person behind the decision, when a session made it: their email and
|
|
14
|
+
* the role they hold. A link and an external event have neither, and are
|
|
15
|
+
* checked by their token instead — the token IS the authority there.
|
|
16
|
+
*
|
|
17
|
+
* Two rules read this. `approvers:` in the flow file names who may decide
|
|
18
|
+
* that flow's gates at all. And nobody approves a run they started
|
|
19
|
+
* themselves: a gate exists so that a second person looks, and a flow a
|
|
20
|
+
* person kicked off and then waved through is a gate that never happened.
|
|
21
|
+
*/
|
|
22
|
+
actor?: {
|
|
23
|
+
email?: string | null;
|
|
24
|
+
role?: string | null;
|
|
25
|
+
} | null;
|
|
26
|
+
/**
|
|
27
|
+
* The emailed link's token, when a link is deciding. Read here rather
|
|
28
|
+
* than trusted from the route, because what a link may decide is policy:
|
|
29
|
+
* the step it was minted for, before it expires, once, and never a gate
|
|
30
|
+
* whose flow names its approvers unless the link was minted for one of
|
|
31
|
+
* them. See readApproveLink.
|
|
32
|
+
*/
|
|
33
|
+
link?: string;
|
|
12
34
|
}
|
|
35
|
+
/** How long an emailed link decides for when the flow does not say. */
|
|
36
|
+
export declare const APPROVE_LINK_DEFAULT_TTL_MS: number;
|
|
37
|
+
/** What a link may decide: one step, until a moment, as somebody or as
|
|
38
|
+
* nobody. A `run` link is the older kind — bound to the run alone, and
|
|
39
|
+
* kept working, once, for runs that were already waiting when this
|
|
40
|
+
* changed. */
|
|
41
|
+
export type ApproveLink = {
|
|
42
|
+
kind: "step";
|
|
43
|
+
step: number;
|
|
44
|
+
expiresAt: string;
|
|
45
|
+
approver: string | null;
|
|
46
|
+
} | {
|
|
47
|
+
kind: "run";
|
|
48
|
+
};
|
|
49
|
+
/** The link's lifetime: the flow's `approve_within:` when it has one —
|
|
50
|
+
* after that the gate has expired on its own — else a week. */
|
|
51
|
+
export declare function approveLinkTtlMs(flow: Pick<FlowInfo, "approveWithin"> | null | undefined): number;
|
|
52
|
+
/**
|
|
53
|
+
* The token an emailed link carries. Bound to one step of one run and to
|
|
54
|
+
* a moment, and — when the notification went to a named approver — to
|
|
55
|
+
* that person, so the link decides as them. A link that was only bound
|
|
56
|
+
* to the run approved every later gate of that run for as long as it ran,
|
|
57
|
+
* and a forwarded one let anyone past `approvers:`.
|
|
58
|
+
*
|
|
59
|
+
* `step.expires.approver.signature`; the signature covers all of it plus
|
|
60
|
+
* the run's identity, under the install key, so rotating the key kills
|
|
61
|
+
* every link at once, the way it kills every hook.
|
|
62
|
+
*/
|
|
63
|
+
export declare function approveLinkToken(tenant: string, workspace: string, runId: string, link: {
|
|
64
|
+
step: number;
|
|
65
|
+
expiresAt: number;
|
|
66
|
+
approver?: string | null;
|
|
67
|
+
}): string;
|
|
68
|
+
/** The path an emailed link opens — absolute when the caller prefixes the
|
|
69
|
+
* install's public origin. `decision=reject` pre-selects; a GET never
|
|
70
|
+
* decides. */
|
|
71
|
+
export declare function approveLinkPath(tenant: string, workspace: string, runId: string, link: {
|
|
72
|
+
step: number;
|
|
73
|
+
expiresAt: number;
|
|
74
|
+
approver?: string | null;
|
|
75
|
+
}): string;
|
|
76
|
+
/**
|
|
77
|
+
* What a presented token is, or why it is nothing. 401 for a token that
|
|
78
|
+
* was never minted for this run, 410 for one that was and has expired —
|
|
79
|
+
* the second deserves a sentence, because the person holding it did
|
|
80
|
+
* nothing wrong.
|
|
81
|
+
*/
|
|
82
|
+
export declare function readApproveLink(tenant: string, workspace: string, runId: string, presented: string, now?: number): {
|
|
83
|
+
link: ApproveLink;
|
|
84
|
+
} | {
|
|
85
|
+
error: string;
|
|
86
|
+
status: 401 | 410;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Everything a link has to pass before it decides anything, answered the
|
|
90
|
+
* same way for the page that shows the gate and the POST that decides it.
|
|
91
|
+
* Returns which waiting steps the link may decide and who it decides as.
|
|
92
|
+
*/
|
|
93
|
+
export declare function checkApproveLink(tenant: string, workspace: string, run: RunRecord, presented: string, now?: number): {
|
|
94
|
+
ok: true;
|
|
95
|
+
link: ApproveLink;
|
|
96
|
+
steps: number[];
|
|
97
|
+
approver: string | null;
|
|
98
|
+
} | {
|
|
99
|
+
ok: false;
|
|
100
|
+
status: number;
|
|
101
|
+
message: string;
|
|
102
|
+
};
|
|
13
103
|
/**
|
|
14
104
|
* Apply a decision to every step the run is waiting on, and line the run up
|
|
15
105
|
* again if a worker had parked it. Returns the record as written and which
|
|
@@ -23,6 +113,19 @@ export declare function decideApproval(tenant: string, workspace: string, runId:
|
|
|
23
113
|
run: RunRecord;
|
|
24
114
|
steps: number[];
|
|
25
115
|
}>;
|
|
116
|
+
/**
|
|
117
|
+
* A gate that ran out of time. `approve_within:` in the flow file stamps a
|
|
118
|
+
* deadline when the run parks; past it, the gate is rejected and the run
|
|
119
|
+
* fails, exactly as if a person had said no.
|
|
120
|
+
*
|
|
121
|
+
* Rejection, not approval, is the only safe way for a clock to answer a
|
|
122
|
+
* question a person was asked. The whole reason the step is parked is that
|
|
123
|
+
* someone wanted to look at it first.
|
|
124
|
+
*
|
|
125
|
+
* Returns whether it acted. Called from the same sweep that closes abandoned
|
|
126
|
+
* runs, so an expiry needs nothing running of its own.
|
|
127
|
+
*/
|
|
128
|
+
export declare function expireStaleGate(tenant: string, workspace: string, run: RunRecord, now?: number): Promise<boolean>;
|
|
26
129
|
/**
|
|
27
130
|
* Release a run parked on `wait: event` with what arrived. The payload is
|
|
28
131
|
* stored on the step — not as an approval note, which is a person's
|