@foldrun/core 9.1.0 → 9.2.1
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/browser-pod.js +4 -0
- package/dist/src/completions.js +8 -8
- package/dist/src/frontmatter-fields.js +2 -0
- package/dist/src/frontmatter-schema.js +4 -0
- package/dist/src/limits.js +22 -4
- 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 +158 -14
- package/dist/src/scan.js +102 -5
- package/dist/src/secrets.d.ts +30 -0
- package/dist/src/secrets.js +41 -1
- package/dist/src/starter.js +6 -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/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/browser-pod.ts +3 -0
- package/src/completions.ts +8 -8
- package/src/frontmatter-fields.ts +2 -0
- package/src/frontmatter-schema.ts +4 -0
- package/src/limits.ts +22 -4
- package/src/run-container.ts +22 -0
- package/src/runner.ts +152 -11
- package/src/scan.ts +123 -6
- package/src/secrets.ts +43 -1
- package/src/starter.ts +6 -1
- package/src/store.ts +47 -2
- package/src/subagents.ts +29 -2
package/src/store.ts
CHANGED
|
@@ -50,6 +50,8 @@ export function readFrontmatter(file: string): Record<string, unknown> {
|
|
|
50
50
|
import { ownToolNames, legacyUseNames, retiredToolNames } from "./tool-names.ts";
|
|
51
51
|
import { refNames } from "./refs.ts";
|
|
52
52
|
import { checkFilter } from "./trigger-filter.ts";
|
|
53
|
+
import { parseA2aPublish, type A2aPublication } from "./a2a-publish.ts";
|
|
54
|
+
import { parseRemotes, remoteNetworkHosts, A2A_LIMIT_KEY, type RemoteSpec } from "./a2a-remotes.ts";
|
|
53
55
|
import { parseBudget, budgetProblem } from "./budget.ts";
|
|
54
56
|
import { timezoneProblem } from "./clock.ts";
|
|
55
57
|
import { parseStepLimits, readLimits } from "./limits.ts";
|
|
@@ -984,6 +986,9 @@ export interface AgentInfo {
|
|
|
984
986
|
effort: string | null;
|
|
985
987
|
tools: string[];
|
|
986
988
|
apis: ApiSpec[];
|
|
989
|
+
/** `remotes:` — the outside A2A agents it may call, the entries that read
|
|
990
|
+
* (a2a-remotes.ts). Never a token: names, card URLs, secret NAMES. */
|
|
991
|
+
remotes?: RemoteSpec[];
|
|
987
992
|
/** The author's own tools named in `tools:` — the subset that is not a
|
|
988
993
|
* built-in. Same list as `tools`, minus what the runtime provides. */
|
|
989
994
|
ownTools: string[];
|
|
@@ -1518,6 +1523,11 @@ export interface FlowInfo {
|
|
|
1518
1523
|
/** Which of the three `owner` came from. */
|
|
1519
1524
|
ownerFrom: OwnerSource | null;
|
|
1520
1525
|
|
|
1526
|
+
/** `a2a:` — whether outside agents may call this flow over A2A, and what
|
|
1527
|
+
* its agent card says (a2a-publish.ts). Null when absent or unreadable:
|
|
1528
|
+
* a block with a problem publishes nothing. */
|
|
1529
|
+
a2a?: A2aPublication | null;
|
|
1530
|
+
|
|
1521
1531
|
/** Flow-level frontmatter values the parser could not read — a bad
|
|
1522
1532
|
* `overlap:`/`priority:`/`catchup:`/`on:`/`signature:`, or a step line
|
|
1523
1533
|
* whose `[[target]]` is malformed. Errors, the way a bad step option is,
|
|
@@ -1604,6 +1614,8 @@ function inlineToolNames(data: Record<string, unknown>): string[] {
|
|
|
1604
1614
|
}
|
|
1605
1615
|
}
|
|
1606
1616
|
if (data.mcpServers && typeof data.mcpServers === "object") out.push(...Object.keys(data.mcpServers));
|
|
1617
|
+
// remotes: grants the a2a tools, counted under `limits: {a2a: N}`.
|
|
1618
|
+
if (parseRemotes(data.remotes).length) out.push(A2A_LIMIT_KEY);
|
|
1607
1619
|
return out;
|
|
1608
1620
|
}
|
|
1609
1621
|
|
|
@@ -1645,7 +1657,11 @@ export function listAgents(tenant: string, workspace: string): AgentInfo[] {
|
|
|
1645
1657
|
workspace: workspaceFront.network,
|
|
1646
1658
|
agent: data.network,
|
|
1647
1659
|
readsWeb,
|
|
1648
|
-
implied:
|
|
1660
|
+
implied: [
|
|
1661
|
+
...parseApis(data.apis).map((a) => { try { return new URL(a.base).host; } catch { return ""; } }),
|
|
1662
|
+
// remotes: — each card's own host, and nothing its card says.
|
|
1663
|
+
...remoteNetworkHosts(parseRemotes(data.remotes)),
|
|
1664
|
+
],
|
|
1649
1665
|
}),
|
|
1650
1666
|
readsWeb,
|
|
1651
1667
|
};
|
|
@@ -1658,6 +1674,7 @@ export function listAgents(tenant: string, workspace: string): AgentInfo[] {
|
|
|
1658
1674
|
effort: data.effort ?? null,
|
|
1659
1675
|
tools,
|
|
1660
1676
|
apis: parseApis(data.apis),
|
|
1677
|
+
remotes: parseRemotes(data.remotes),
|
|
1661
1678
|
ownTools: ownToolNames(data),
|
|
1662
1679
|
legacyUse: legacyUseNames(data),
|
|
1663
1680
|
retiredTools: retiredToolNames(data),
|
|
@@ -2138,6 +2155,7 @@ export function parseFlow(file: string, raw: string): FlowInfo {
|
|
|
2138
2155
|
approveWithin: durationOf(data.approve_within),
|
|
2139
2156
|
description: typeof data.description === "string" ? data.description.trim() || null : null,
|
|
2140
2157
|
...resolveOwner(data.owner, undefined, undefined, "flow"),
|
|
2158
|
+
a2a: parseA2aPublish(data.a2a),
|
|
2141
2159
|
frontProblems,
|
|
2142
2160
|
steps,
|
|
2143
2161
|
};
|
|
@@ -2985,7 +3003,9 @@ export function renameWorkspaceFile(tenant: string, workspace: string, from: str
|
|
|
2985
3003
|
// Refuse with the reason before any path validation: "not an editable
|
|
2986
3004
|
// path" is true of AGENTS.md too, and the generic sentence buries the
|
|
2987
3005
|
// specific one worth reading.
|
|
2988
|
-
|
|
3006
|
+
// Read as the paths the rename will use: `./AGENTS.md` is AGENTS.md
|
|
3007
|
+
// (1 Nov 2026 — the raw spelling slipped past this and still moved it).
|
|
3008
|
+
const why = anchoredReason(path.normalize(from)) ?? anchoredReason(path.normalize(to));
|
|
2989
3009
|
if (why) throw new Error(why);
|
|
2990
3010
|
const dir = workspaceDir(tenant, workspace);
|
|
2991
3011
|
const src = path.join(dir, assertEditablePath(from));
|
|
@@ -3740,6 +3760,11 @@ export interface RunRecord {
|
|
|
3740
3760
|
* expressed only as a status, because "failed" and "someone stopped it"
|
|
3741
3761
|
* are different facts and the trace should not conflate them. */
|
|
3742
3762
|
stopRequested?: boolean;
|
|
3763
|
+
/** Who stopped it, as the trail says it, when that was not a person
|
|
3764
|
+
* stopping it from the dashboard, the CLI or the API: "A2A caller
|
|
3765
|
+
* <key label>" for an A2A CancelTask (1 Nov 2026). Absent: a person.
|
|
3766
|
+
* Written by stopRun beside stopRequested, carried the same way. */
|
|
3767
|
+
stoppedBy?: string;
|
|
3743
3768
|
/**
|
|
3744
3769
|
* What this run concluded, in one line — the first line of the last step
|
|
3745
3770
|
* that produced a result, captured when the run finishes.
|
|
@@ -3810,6 +3835,14 @@ export interface RunRecord {
|
|
|
3810
3835
|
* step of this run — never the tokens. A link is good once; the second
|
|
3811
3836
|
* click is a 409, not a second approval. Written by approvals.ts. */
|
|
3812
3837
|
approvalLinksUsed?: string[];
|
|
3838
|
+
/** What started it, when that is not a person, a schedule or an event the
|
|
3839
|
+
* flow's own trigger names: "a2a" — an outside agent, over A2A (1 Nov
|
|
3840
|
+
* 2026). Absent for every other run. */
|
|
3841
|
+
trigger?: "a2a";
|
|
3842
|
+
/** For an A2A run: the A2A task it serves and the id of the A2A key (its
|
|
3843
|
+
* first id, kept across rotations) that asked. A correlation only — the
|
|
3844
|
+
* protocol's own state lives in the platform's a2a_tasks. */
|
|
3845
|
+
a2a?: { task: string; key: string };
|
|
3813
3846
|
steps: StepRecord[];
|
|
3814
3847
|
}
|
|
3815
3848
|
|
|
@@ -4089,6 +4122,9 @@ export interface RunSummary {
|
|
|
4089
4122
|
revision?: string | null;
|
|
4090
4123
|
/** A step is parked for a person — what the approvals banner counts. */
|
|
4091
4124
|
awaitingApproval: boolean;
|
|
4125
|
+
/** RunRecord.trigger and .a2a: an outside agent started it. */
|
|
4126
|
+
trigger?: "a2a";
|
|
4127
|
+
a2a?: { task: string; key: string };
|
|
4092
4128
|
steps: RunSummaryStep[];
|
|
4093
4129
|
}
|
|
4094
4130
|
|
|
@@ -4131,6 +4167,8 @@ export function summarizeRun(run: RunRecord): RunSummary {
|
|
|
4131
4167
|
...(run.startedBy ? { startedBy: run.startedBy } : {}),
|
|
4132
4168
|
...(run.parkedAt ? { parkedAt: run.parkedAt } : {}),
|
|
4133
4169
|
...(run.revision ? { revision: run.revision } : {}),
|
|
4170
|
+
...(run.trigger ? { trigger: run.trigger } : {}),
|
|
4171
|
+
...(run.a2a ? { a2a: run.a2a } : {}),
|
|
4134
4172
|
awaitingApproval: run.steps.some((s) => s.status === "awaiting-approval" && s.waitFor !== "event"),
|
|
4135
4173
|
steps: run.steps.map((s) => {
|
|
4136
4174
|
const error = s.status === "failed" ? s.events.filter((e) => e.type === "error").at(-1)?.text : undefined;
|
|
@@ -4319,6 +4357,13 @@ export function listAllRunSummaries(tenant: string): (RunSummary & { workspace:
|
|
|
4319
4357
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
4320
4358
|
}
|
|
4321
4359
|
|
|
4360
|
+
/** Who stopped a run, for a trail line: "a person" unless stopRun was told
|
|
4361
|
+
* otherwise (RunRecord.stoppedBy). */
|
|
4362
|
+
export function stoppedByText(run: Pick<RunRecord, "stoppedBy"> | null | undefined): string {
|
|
4363
|
+
const by = run?.stoppedBy?.trim();
|
|
4364
|
+
return by ? by : "a person";
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4322
4367
|
/**
|
|
4323
4368
|
* The status a person should see. "failed" and "someone stopped it" are
|
|
4324
4369
|
* different facts — the record keeps them apart (stopRequested), and the
|
package/src/subagents.ts
CHANGED
|
@@ -28,6 +28,24 @@ import fs from "node:fs";
|
|
|
28
28
|
import path from "node:path";
|
|
29
29
|
import { refNames } from "./refs.ts";
|
|
30
30
|
import { TOOL_MAP } from "./tool-names.ts";
|
|
31
|
+
import { A2A_SERVER } from "./a2a-remotes.ts";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The a2a tools (remotes:) are the parent's: one server, built for the
|
|
35
|
+
* PARENT's remotes with the parent's credentials, under the same four names
|
|
36
|
+
* whatever remotes a sub-agent declares. Intersecting names would hand a
|
|
37
|
+
* shared-sandbox sub-agent every remote its parent may call. So they are
|
|
38
|
+
* withheld from it entirely — not in its list, and refused if it calls one.
|
|
39
|
+
* A `sandbox: own` sub-agent runs as a step of its own, from its own file,
|
|
40
|
+
* and gets its OWN remotes there (runner.ts delegateFor → runStep).
|
|
41
|
+
*/
|
|
42
|
+
export function isA2aTool(tool: string): boolean {
|
|
43
|
+
return tool === `mcp__${A2A_SERVER}` || tool.startsWith(`mcp__${A2A_SERVER}__`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What a shared-sandbox sub-agent is told, and what its refused call says. */
|
|
47
|
+
export const SUBAGENT_NO_REMOTES =
|
|
48
|
+
"remotes: are not available to shared-sandbox sub-agents yet — give the sub-agent `sandbox: own`, or call the remote from the parent";
|
|
31
49
|
|
|
32
50
|
/** Plain JSON: this crosses into the run container. */
|
|
33
51
|
export interface SubagentSpec {
|
|
@@ -95,7 +113,7 @@ export function subagentTools(own: string[], parent: string[]): string[] {
|
|
|
95
113
|
);
|
|
96
114
|
const out: string[] = [];
|
|
97
115
|
for (const t of own) {
|
|
98
|
-
if ((DELEGATE_TOOLS as readonly string[]).includes(t) || out.includes(t)) continue;
|
|
116
|
+
if ((DELEGATE_TOOLS as readonly string[]).includes(t) || isA2aTool(t) || out.includes(t)) continue;
|
|
99
117
|
const server = /^mcp__([^_].*?)__/.exec(t)?.[1];
|
|
100
118
|
if (held.has(t) || (server && servers.has(`mcp__${server}`))) out.push(t);
|
|
101
119
|
}
|
|
@@ -110,9 +128,16 @@ export function subagentPrompt(o: {
|
|
|
110
128
|
parent: string;
|
|
111
129
|
workspace: string;
|
|
112
130
|
shared?: string | null;
|
|
113
|
-
}
|
|
131
|
+
}, extra: {
|
|
132
|
+
/** It declares remotes:, which it does not get here. A second argument,
|
|
133
|
+
* so the signature only widened. */
|
|
134
|
+
remotesWithheld?: boolean;
|
|
135
|
+
} = {}): string {
|
|
114
136
|
return [
|
|
115
137
|
o.body.trim(),
|
|
138
|
+
...(extra.remotesWithheld
|
|
139
|
+
? [`# 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.`]
|
|
140
|
+
: []),
|
|
116
141
|
`# Where you are\n\n` +
|
|
117
142
|
`You are ${o.name}, a sub-agent started by ${o.parent} to do one job and report back. ` +
|
|
118
143
|
`You work in ${o.parent}'s directory, \`agents/${o.parent}/\` in the \`${o.workspace}\` workspace: ` +
|
|
@@ -183,6 +208,8 @@ export function subagentGuard(specs: SubagentSpec[]): (call: GuardedCall) => str
|
|
|
183
208
|
if (isDelegate(tool)) {
|
|
184
209
|
return `${s.name} is a sub-agent and cannot start another — do the work with your own tools`;
|
|
185
210
|
}
|
|
211
|
+
// The parent's remotes, with the parent's credentials: never a sub-agent's.
|
|
212
|
+
if (isA2aTool(tool)) return `${tool} is not available to ${s.name}: ${SUBAGENT_NO_REMOTES}`;
|
|
186
213
|
if (s.disallowedTools.includes(tool)) return `${tool} is disallowed for ${s.name}`;
|
|
187
214
|
if (subagentTools([tool], s.tools).length === 0) return `${tool} is not one of ${s.name}'s tools`;
|
|
188
215
|
return null;
|