@foldrun/core 9.0.0 → 9.2.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/src/a2a-client.d.ts +165 -0
- package/dist/src/a2a-client.js +440 -0
- package/dist/src/a2a-publish.d.ts +37 -0
- package/dist/src/a2a-publish.js +116 -0
- package/dist/src/a2a-remotes.d.ts +73 -0
- package/dist/src/a2a-remotes.js +192 -0
- package/dist/src/a2a-tools.d.ts +148 -0
- package/dist/src/a2a-tools.js +833 -0
- package/dist/src/api-tools.js +15 -10
- package/dist/src/approvals.d.ts +18 -10
- package/dist/src/approvals.js +51 -1
- package/dist/src/browser-pod.js +4 -0
- package/dist/src/catalog.d.ts +14 -0
- package/dist/src/catalog.js +28 -0
- package/dist/src/completions.js +8 -8
- package/dist/src/cron.d.ts +9 -0
- package/dist/src/cron.js +20 -0
- package/dist/src/frontmatter-fields.js +3 -1
- package/dist/src/frontmatter-schema.d.ts +5 -1
- package/dist/src/frontmatter-schema.js +44 -1
- package/dist/src/limits.js +22 -4
- package/dist/src/notify.d.ts +6 -3
- package/dist/src/notify.js +25 -12
- package/dist/src/package.d.ts +5 -1
- package/dist/src/package.js +5 -3
- package/dist/src/run-container.d.ts +10 -1
- package/dist/src/run-container.js +17 -0
- package/dist/src/runner.d.ts +10 -0
- package/dist/src/runner.js +163 -16
- package/dist/src/scan.js +123 -5
- package/dist/src/secrets.d.ts +122 -10
- package/dist/src/secrets.js +291 -27
- package/dist/src/starter.js +5 -1
- package/dist/src/store.d.ts +34 -0
- package/dist/src/store.js +28 -7
- package/dist/src/subagents.d.ts +16 -0
- package/dist/src/subagents.js +23 -2
- package/dist/src/triggers.js +2 -2
- package/dist/src/webhook.d.ts +19 -3
- package/dist/src/webhook.js +14 -5
- package/package.json +4 -1
- package/src/a2a-client.ts +517 -0
- package/src/a2a-publish.ts +120 -0
- package/src/a2a-remotes.ts +225 -0
- package/src/a2a-tools.ts +919 -0
- package/src/api-tools.ts +20 -8
- package/src/approvals.ts +55 -1
- package/src/browser-pod.ts +3 -0
- package/src/catalog.ts +27 -0
- package/src/completions.ts +8 -8
- package/src/cron.ts +19 -0
- package/src/frontmatter-fields.ts +3 -1
- package/src/frontmatter-schema.ts +42 -2
- package/src/limits.ts +22 -4
- package/src/notify.ts +23 -12
- package/src/package.ts +5 -3
- package/src/run-container.ts +22 -0
- package/src/runner.ts +157 -13
- package/src/scan.ts +146 -6
- package/src/secrets.ts +319 -33
- package/src/starter.ts +5 -1
- package/src/store.ts +47 -2
- package/src/subagents.ts +29 -2
- package/src/triggers.ts +2 -2
- package/src/webhook.ts +22 -5
package/dist/src/store.js
CHANGED
|
@@ -45,6 +45,8 @@ export function readFrontmatter(file) {
|
|
|
45
45
|
import { ownToolNames, legacyUseNames, retiredToolNames } from "./tool-names.js";
|
|
46
46
|
import { refNames } from "./refs.js";
|
|
47
47
|
import { checkFilter } from "./trigger-filter.js";
|
|
48
|
+
import { parseA2aPublish } from "./a2a-publish.js";
|
|
49
|
+
import { parseRemotes, remoteNetworkHosts, A2A_LIMIT_KEY } from "./a2a-remotes.js";
|
|
48
50
|
import { parseBudget, budgetProblem } from "./budget.js";
|
|
49
51
|
import { timezoneProblem } from "./clock.js";
|
|
50
52
|
import { parseStepLimits, readLimits } from "./limits.js";
|
|
@@ -1115,6 +1117,9 @@ function inlineToolNames(data) {
|
|
|
1115
1117
|
}
|
|
1116
1118
|
if (data.mcpServers && typeof data.mcpServers === "object")
|
|
1117
1119
|
out.push(...Object.keys(data.mcpServers));
|
|
1120
|
+
// remotes: grants the a2a tools, counted under `limits: {a2a: N}`.
|
|
1121
|
+
if (parseRemotes(data.remotes).length)
|
|
1122
|
+
out.push(A2A_LIMIT_KEY);
|
|
1118
1123
|
return out;
|
|
1119
1124
|
}
|
|
1120
1125
|
export function listAgents(tenant, workspace) {
|
|
@@ -1158,12 +1163,16 @@ export function listAgents(tenant, workspace) {
|
|
|
1158
1163
|
workspace: workspaceFront.network,
|
|
1159
1164
|
agent: data.network,
|
|
1160
1165
|
readsWeb,
|
|
1161
|
-
implied:
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1166
|
+
implied: [
|
|
1167
|
+
...parseApis(data.apis).map((a) => { try {
|
|
1168
|
+
return new URL(a.base).host;
|
|
1169
|
+
}
|
|
1170
|
+
catch {
|
|
1171
|
+
return "";
|
|
1172
|
+
} }),
|
|
1173
|
+
// remotes: — each card's own host, and nothing its card says.
|
|
1174
|
+
...remoteNetworkHosts(parseRemotes(data.remotes)),
|
|
1175
|
+
],
|
|
1167
1176
|
}),
|
|
1168
1177
|
readsWeb,
|
|
1169
1178
|
};
|
|
@@ -1176,6 +1185,7 @@ export function listAgents(tenant, workspace) {
|
|
|
1176
1185
|
effort: data.effort ?? null,
|
|
1177
1186
|
tools,
|
|
1178
1187
|
apis: parseApis(data.apis),
|
|
1188
|
+
remotes: parseRemotes(data.remotes),
|
|
1179
1189
|
ownTools: ownToolNames(data),
|
|
1180
1190
|
legacyUse: legacyUseNames(data),
|
|
1181
1191
|
retiredTools: retiredToolNames(data),
|
|
@@ -1689,6 +1699,7 @@ export function parseFlow(file, raw) {
|
|
|
1689
1699
|
approveWithin: durationOf(data.approve_within),
|
|
1690
1700
|
description: typeof data.description === "string" ? data.description.trim() || null : null,
|
|
1691
1701
|
...resolveOwner(data.owner, undefined, undefined, "flow"),
|
|
1702
|
+
a2a: parseA2aPublish(data.a2a),
|
|
1692
1703
|
frontProblems,
|
|
1693
1704
|
steps,
|
|
1694
1705
|
};
|
|
@@ -2505,7 +2516,9 @@ export function renameWorkspaceFile(tenant, workspace, from, to, meta = {}) {
|
|
|
2505
2516
|
// Refuse with the reason before any path validation: "not an editable
|
|
2506
2517
|
// path" is true of AGENTS.md too, and the generic sentence buries the
|
|
2507
2518
|
// specific one worth reading.
|
|
2508
|
-
|
|
2519
|
+
// Read as the paths the rename will use: `./AGENTS.md` is AGENTS.md
|
|
2520
|
+
// (1 Nov 2026 — the raw spelling slipped past this and still moved it).
|
|
2521
|
+
const why = anchoredReason(path.normalize(from)) ?? anchoredReason(path.normalize(to));
|
|
2509
2522
|
if (why)
|
|
2510
2523
|
throw new Error(why);
|
|
2511
2524
|
const dir = workspaceDir(tenant, workspace);
|
|
@@ -3014,6 +3027,8 @@ export function summarizeRun(run) {
|
|
|
3014
3027
|
...(run.startedBy ? { startedBy: run.startedBy } : {}),
|
|
3015
3028
|
...(run.parkedAt ? { parkedAt: run.parkedAt } : {}),
|
|
3016
3029
|
...(run.revision ? { revision: run.revision } : {}),
|
|
3030
|
+
...(run.trigger ? { trigger: run.trigger } : {}),
|
|
3031
|
+
...(run.a2a ? { a2a: run.a2a } : {}),
|
|
3017
3032
|
awaitingApproval: run.steps.some((s) => s.status === "awaiting-approval" && s.waitFor !== "event"),
|
|
3018
3033
|
steps: run.steps.map((s) => {
|
|
3019
3034
|
const error = s.status === "failed" ? s.events.filter((e) => e.type === "error").at(-1)?.text : undefined;
|
|
@@ -3212,6 +3227,12 @@ export function listAllRunSummaries(tenant) {
|
|
|
3212
3227
|
.flatMap((p) => listRunSummaries(tenant, p.name).map((s) => ({ ...s, workspace: p.name })))
|
|
3213
3228
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
3214
3229
|
}
|
|
3230
|
+
/** Who stopped a run, for a trail line: "a person" unless stopRun was told
|
|
3231
|
+
* otherwise (RunRecord.stoppedBy). */
|
|
3232
|
+
export function stoppedByText(run) {
|
|
3233
|
+
const by = run?.stoppedBy?.trim();
|
|
3234
|
+
return by ? by : "a person";
|
|
3235
|
+
}
|
|
3215
3236
|
/**
|
|
3216
3237
|
* The status a person should see. "failed" and "someone stopped it" are
|
|
3217
3238
|
* different facts — the record keeps them apart (stopRequested), and the
|
package/dist/src/subagents.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The a2a tools (remotes:) are the parent's: one server, built for the
|
|
3
|
+
* PARENT's remotes with the parent's credentials, under the same four names
|
|
4
|
+
* whatever remotes a sub-agent declares. Intersecting names would hand a
|
|
5
|
+
* shared-sandbox sub-agent every remote its parent may call. So they are
|
|
6
|
+
* withheld from it entirely — not in its list, and refused if it calls one.
|
|
7
|
+
* A `sandbox: own` sub-agent runs as a step of its own, from its own file,
|
|
8
|
+
* and gets its OWN remotes there (runner.ts delegateFor → runStep).
|
|
9
|
+
*/
|
|
10
|
+
export declare function isA2aTool(tool: string): boolean;
|
|
11
|
+
/** What a shared-sandbox sub-agent is told, and what its refused call says. */
|
|
12
|
+
export declare const SUBAGENT_NO_REMOTES = "remotes: are not available to shared-sandbox sub-agents yet \u2014 give the sub-agent `sandbox: own`, or call the remote from the parent";
|
|
1
13
|
/** Plain JSON: this crosses into the run container. */
|
|
2
14
|
export interface SubagentSpec {
|
|
3
15
|
name: string;
|
|
@@ -50,6 +62,10 @@ export declare function subagentPrompt(o: {
|
|
|
50
62
|
parent: string;
|
|
51
63
|
workspace: string;
|
|
52
64
|
shared?: string | null;
|
|
65
|
+
}, extra?: {
|
|
66
|
+
/** It declares remotes:, which it does not get here. A second argument,
|
|
67
|
+
* so the signature only widened. */
|
|
68
|
+
remotesWithheld?: boolean;
|
|
53
69
|
}): string;
|
|
54
70
|
/** SDK definitions. `tools` is always set — an absent list inherits every
|
|
55
71
|
* tool the parent has, which is the one thing this module exists to stop. */
|
package/dist/src/subagents.js
CHANGED
|
@@ -27,6 +27,21 @@ import fs from "node:fs";
|
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import { refNames } from "./refs.js";
|
|
29
29
|
import { TOOL_MAP } from "./tool-names.js";
|
|
30
|
+
import { A2A_SERVER } from "./a2a-remotes.js";
|
|
31
|
+
/**
|
|
32
|
+
* The a2a tools (remotes:) are the parent's: one server, built for the
|
|
33
|
+
* PARENT's remotes with the parent's credentials, under the same four names
|
|
34
|
+
* whatever remotes a sub-agent declares. Intersecting names would hand a
|
|
35
|
+
* shared-sandbox sub-agent every remote its parent may call. So they are
|
|
36
|
+
* withheld from it entirely — not in its list, and refused if it calls one.
|
|
37
|
+
* A `sandbox: own` sub-agent runs as a step of its own, from its own file,
|
|
38
|
+
* and gets its OWN remotes there (runner.ts delegateFor → runStep).
|
|
39
|
+
*/
|
|
40
|
+
export function isA2aTool(tool) {
|
|
41
|
+
return tool === `mcp__${A2A_SERVER}` || tool.startsWith(`mcp__${A2A_SERVER}__`);
|
|
42
|
+
}
|
|
43
|
+
/** What a shared-sandbox sub-agent is told, and what its refused call says. */
|
|
44
|
+
export const SUBAGENT_NO_REMOTES = "remotes: are not available to shared-sandbox sub-agents yet — give the sub-agent `sandbox: own`, or call the remote from the parent";
|
|
30
45
|
/** Does this agent run in its own sandbox when delegated to? */
|
|
31
46
|
export function ownSandbox(front) {
|
|
32
47
|
return front.sandbox === "own";
|
|
@@ -63,7 +78,7 @@ export function subagentTools(own, parent) {
|
|
|
63
78
|
const servers = new Set(parent.filter((t) => /^mcp__[^_]/.test(t)).map((t) => t.replace(/__\*$/, "")).filter((t) => !t.slice(5).includes("__")));
|
|
64
79
|
const out = [];
|
|
65
80
|
for (const t of own) {
|
|
66
|
-
if (DELEGATE_TOOLS.includes(t) || out.includes(t))
|
|
81
|
+
if (DELEGATE_TOOLS.includes(t) || isA2aTool(t) || out.includes(t))
|
|
67
82
|
continue;
|
|
68
83
|
const server = /^mcp__([^_].*?)__/.exec(t)?.[1];
|
|
69
84
|
if (held.has(t) || (server && servers.has(`mcp__${server}`)))
|
|
@@ -73,9 +88,12 @@ export function subagentTools(own, parent) {
|
|
|
73
88
|
}
|
|
74
89
|
/** The sub-agent's system prompt: its own prose, told where it stands. It
|
|
75
90
|
* works in the PARENT's directory — the SDK shares the session's cwd. */
|
|
76
|
-
export function subagentPrompt(o) {
|
|
91
|
+
export function subagentPrompt(o, extra = {}) {
|
|
77
92
|
return [
|
|
78
93
|
o.body.trim(),
|
|
94
|
+
...(extra.remotesWithheld
|
|
95
|
+
? [`# Outside agents\n\nYour remotes: are not available to you as ${o.parent}'s sub-agent: ${SUBAGENT_NO_REMOTES}. If the job needs an outside agent, say so in your reply and ${o.parent} can call it.`]
|
|
96
|
+
: []),
|
|
79
97
|
`# Where you are\n\n` +
|
|
80
98
|
`You are ${o.name}, a sub-agent started by ${o.parent} to do one job and report back. ` +
|
|
81
99
|
`You work in ${o.parent}'s directory, \`agents/${o.parent}/\` in the \`${o.workspace}\` workspace: ` +
|
|
@@ -131,6 +149,9 @@ export function subagentGuard(specs) {
|
|
|
131
149
|
if (isDelegate(tool)) {
|
|
132
150
|
return `${s.name} is a sub-agent and cannot start another — do the work with your own tools`;
|
|
133
151
|
}
|
|
152
|
+
// The parent's remotes, with the parent's credentials: never a sub-agent's.
|
|
153
|
+
if (isA2aTool(tool))
|
|
154
|
+
return `${tool} is not available to ${s.name}: ${SUBAGENT_NO_REMOTES}`;
|
|
134
155
|
if (s.disallowedTools.includes(tool))
|
|
135
156
|
return `${tool} is disallowed for ${s.name}`;
|
|
136
157
|
if (subagentTools([tool], s.tools).length === 0)
|
package/dist/src/triggers.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import crypto from "node:crypto";
|
|
15
15
|
import { platform } from "./platform.js";
|
|
16
16
|
import { listFlows, readRun, runVerdict } from "./store.js";
|
|
17
|
-
import { getSecret } from "./secrets.js";
|
|
17
|
+
import { getSecret, signingSecretShown } from "./secrets.js";
|
|
18
18
|
/** The last step's result — the same reading runner.ts makes, kept local so
|
|
19
19
|
* this module never imports the runner (the queue imports both). */
|
|
20
20
|
function runResult(run) {
|
|
@@ -251,7 +251,7 @@ export function verifyWebhookSignature(tenant, workspace, flow, rawBody, header,
|
|
|
251
251
|
return { ok: false, reason: `signature: ${flow.signature} needs signing_secret: \${NAME}` };
|
|
252
252
|
const secret = getSecret(tenant, flow.signingSecret, workspace);
|
|
253
253
|
if (!secret)
|
|
254
|
-
return { ok: false, reason: `signing secret ${flow.signingSecret} is not set` };
|
|
254
|
+
return { ok: false, reason: `signing secret ${signingSecretShown(flow.signingSecret)} is not set` };
|
|
255
255
|
const hmac = (data) => crypto.createHmac("sha256", secret.value).update(data).digest("hex");
|
|
256
256
|
const fresh = (ts) => Math.abs(now / 1000 - ts) <= 300;
|
|
257
257
|
switch (flow.signature) {
|
package/dist/src/webhook.d.ts
CHANGED
|
@@ -27,14 +27,30 @@ export declare function webhookPath(tenant: string, workspace: string, flow: str
|
|
|
27
27
|
* working hook token.
|
|
28
28
|
*/
|
|
29
29
|
export declare function approveToken(tenant: string, workspace: string, runId: string): string;
|
|
30
|
+
/** One parked `wait: event` gate: the step's index in the run record and
|
|
31
|
+
* the moment it parked (the `t` of its "waiting for an external event"
|
|
32
|
+
* trace line). */
|
|
33
|
+
export interface EventGate {
|
|
34
|
+
step: number;
|
|
35
|
+
at: string;
|
|
36
|
+
}
|
|
30
37
|
/**
|
|
31
38
|
* The token that releases a run parked on `wait: event`. Its own HMAC
|
|
32
39
|
* family, so an approval link cannot double as an event URL or the reverse.
|
|
40
|
+
*
|
|
41
|
+
* With `gate`, the token is for that one park of that one step (30 Oct
|
|
42
|
+
* 2026). The per-run token released every later `wait: event` of the run,
|
|
43
|
+
* so a URL handed to a third party for the first gate also opened the
|
|
44
|
+
* second, and never expired. A gate's token dies with its park: the step
|
|
45
|
+
* released is no longer waiting, and parking again (a loop) stamps a new
|
|
46
|
+
* `at`. The per-run form, without `gate`, is what URLs printed before then
|
|
47
|
+
* carry — the events route still takes it for a step parked under it.
|
|
33
48
|
*/
|
|
34
|
-
export declare function eventToken(tenant: string, workspace: string, runId: string): string;
|
|
49
|
+
export declare function eventToken(tenant: string, workspace: string, runId: string, gate?: EventGate): string;
|
|
35
50
|
/** The URL a `wait: event` step is released at — absolute when the install
|
|
36
|
-
* knows its origin, a path otherwise.
|
|
37
|
-
|
|
51
|
+
* knows its origin, a path otherwise. With `gate`, that step's own URL
|
|
52
|
+
* (`&step=<index>`); the route finds the park's moment on the record. */
|
|
53
|
+
export declare function eventUrl(tenant: string, workspace: string, runId: string, gate?: EventGate): string;
|
|
38
54
|
/**
|
|
39
55
|
* Where this install can be reached from outside, or null. The platform has
|
|
40
56
|
* never needed to know its own address — every URL it hands out has been a
|
package/dist/src/webhook.js
CHANGED
|
@@ -80,18 +80,27 @@ export function approveToken(tenant, workspace, runId) {
|
|
|
80
80
|
/**
|
|
81
81
|
* The token that releases a run parked on `wait: event`. Its own HMAC
|
|
82
82
|
* family, so an approval link cannot double as an event URL or the reverse.
|
|
83
|
+
*
|
|
84
|
+
* With `gate`, the token is for that one park of that one step (30 Oct
|
|
85
|
+
* 2026). The per-run token released every later `wait: event` of the run,
|
|
86
|
+
* so a URL handed to a third party for the first gate also opened the
|
|
87
|
+
* second, and never expired. A gate's token dies with its park: the step
|
|
88
|
+
* released is no longer waiting, and parking again (a loop) stamps a new
|
|
89
|
+
* `at`. The per-run form, without `gate`, is what URLs printed before then
|
|
90
|
+
* carry — the events route still takes it for a step parked under it.
|
|
83
91
|
*/
|
|
84
|
-
export function eventToken(tenant, workspace, runId) {
|
|
92
|
+
export function eventToken(tenant, workspace, runId, gate) {
|
|
85
93
|
return crypto
|
|
86
94
|
.createHmac("sha256", installKey())
|
|
87
|
-
.update(`event:${tenant}/${workspace}/${runId}`)
|
|
95
|
+
.update(gate ? `event:${tenant}/${workspace}/${runId}#${gate.step}@${gate.at}` : `event:${tenant}/${workspace}/${runId}`)
|
|
88
96
|
.digest("hex")
|
|
89
97
|
.slice(0, 32);
|
|
90
98
|
}
|
|
91
99
|
/** The URL a `wait: event` step is released at — absolute when the install
|
|
92
|
-
* knows its origin, a path otherwise.
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
* knows its origin, a path otherwise. With `gate`, that step's own URL
|
|
101
|
+
* (`&step=<index>`); the route finds the park's moment on the record. */
|
|
102
|
+
export function eventUrl(tenant, workspace, runId, gate) {
|
|
103
|
+
const p = `/api/events/${tenant}/${workspace}/${runId}?token=${eventToken(tenant, workspace, runId, gate)}${gate ? `&step=${gate.step}` : ""}`;
|
|
95
104
|
const base = publicUrl();
|
|
96
105
|
return base ? `${base}${p}` : p;
|
|
97
106
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foldrun/core",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.2.0",
|
|
4
4
|
"description": "Agents are just folders. The format, the runner, the checks.",
|
|
5
5
|
"license": "FSL-1.1-ALv2",
|
|
6
6
|
"type": "module",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"foldrun": {
|
|
18
18
|
"publicApi": [
|
|
19
19
|
".",
|
|
20
|
+
"./a2a-client",
|
|
21
|
+
"./a2a-publish",
|
|
22
|
+
"./a2a-remotes",
|
|
20
23
|
"./agents-md",
|
|
21
24
|
"./browser-pod",
|
|
22
25
|
"./flow-patterns",
|