@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/api-tools.ts
CHANGED
|
@@ -20,7 +20,12 @@ import { createSdkMcpServer, tool, type SdkMcpToolDefinition } from "@anthropic-
|
|
|
20
20
|
import type { ApiSpec } from "./store.ts";
|
|
21
21
|
import { previewOrKeep, RESULT_KEEP_CHARS, type KeepIn } from "./tool-output.ts";
|
|
22
22
|
import type { OperationSpec } from "./openapi.ts";
|
|
23
|
-
import { resolveSecrets,
|
|
23
|
+
import { resolveSecrets, materializeCurrent } from "./secrets.ts";
|
|
24
|
+
|
|
25
|
+
/** Whose vault the secrets came from, on a host step: the account (and
|
|
26
|
+
* workspace) they are made live in. Null inside a sandbox, where the values
|
|
27
|
+
* crossed already resolved and there is no vault. */
|
|
28
|
+
type VaultScope = { tenant: string; workspace?: string } | null;
|
|
24
29
|
|
|
25
30
|
// Collected whole up to this; the model sees a preview and where the rest is
|
|
26
31
|
// kept (tool-output.ts), not a cut with the rest gone.
|
|
@@ -145,6 +150,7 @@ async function performRequest(
|
|
|
145
150
|
req: Request,
|
|
146
151
|
/** Where a long body is kept, when the agent can read it back (tool-output.ts). */
|
|
147
152
|
keepIn?: KeepIn,
|
|
153
|
+
scope: VaultScope = null,
|
|
148
154
|
): Promise<ToolReply> {
|
|
149
155
|
const started = Date.now();
|
|
150
156
|
try {
|
|
@@ -153,8 +159,11 @@ async function performRequest(
|
|
|
153
159
|
// Per call, not per build: an oauth2 secret's recipe becomes a
|
|
154
160
|
// live access token here, so a step that runs for an hour keeps
|
|
155
161
|
// getting fresh tokens (the exchange is cached until near expiry)
|
|
156
|
-
// — and a recipe can never end up inside a header.
|
|
157
|
-
|
|
162
|
+
// — and a recipe can never end up inside a header. On a host, the
|
|
163
|
+
// vault's recipe as it is now, made live in the account's scope: a
|
|
164
|
+
// rotating refresh token the runner (or an earlier call) spent is never
|
|
165
|
+
// sent again (secrets.ts materializeCurrent).
|
|
166
|
+
const live = await materializeCurrent(env, scope);
|
|
158
167
|
const path = req.path.startsWith("/") ? req.path : `/${req.path}`;
|
|
159
168
|
const url = new URL(api.base + path);
|
|
160
169
|
// Base-URL confinement: a path can't escape the declared host.
|
|
@@ -243,6 +252,7 @@ function typedTool(
|
|
|
243
252
|
bucket: TokenBucket | null,
|
|
244
253
|
log: string[],
|
|
245
254
|
keepIn?: KeepIn,
|
|
255
|
+
scope: VaultScope = null,
|
|
246
256
|
) {
|
|
247
257
|
const keys = argKeys(op);
|
|
248
258
|
const shape: Record<string, z.ZodTypeAny> = {};
|
|
@@ -292,11 +302,11 @@ function typedTool(
|
|
|
292
302
|
query,
|
|
293
303
|
headers,
|
|
294
304
|
body: typeof values.body === "string" ? values.body : undefined,
|
|
295
|
-
}, keepIn);
|
|
305
|
+
}, keepIn, scope);
|
|
296
306
|
});
|
|
297
307
|
}
|
|
298
308
|
|
|
299
|
-
function genericTool(api: ApiSpec, env: Record<string, string>, bucket: TokenBucket | null, log: string[], keepIn?: KeepIn) {
|
|
309
|
+
function genericTool(api: ApiSpec, env: Record<string, string>, bucket: TokenBucket | null, log: string[], keepIn?: KeepIn, scope: VaultScope = null) {
|
|
300
310
|
const typed = api.resolvedOperations?.length
|
|
301
311
|
? ` Prefer the typed ${api.name}_* tools; this one is the escape hatch for operations they do not cover.`
|
|
302
312
|
: "";
|
|
@@ -321,7 +331,7 @@ function genericTool(api: ApiSpec, env: Record<string, string>, bucket: TokenBuc
|
|
|
321
331
|
query: args.query ?? {},
|
|
322
332
|
headers: {},
|
|
323
333
|
body: args.body,
|
|
324
|
-
}, keepIn),
|
|
334
|
+
}, keepIn, scope),
|
|
325
335
|
);
|
|
326
336
|
}
|
|
327
337
|
|
|
@@ -351,6 +361,8 @@ export function buildApiTools(
|
|
|
351
361
|
missing: resolved.missing,
|
|
352
362
|
}
|
|
353
363
|
: resolveSecrets(tenant, neededSecrets, workspace);
|
|
364
|
+
// Read from the vault here: made live per call in its account's scope.
|
|
365
|
+
const scope: VaultScope = !resolved && tenant ? { tenant, ...(workspace ? { workspace } : {}) } : null;
|
|
354
366
|
const log: string[] = [];
|
|
355
367
|
|
|
356
368
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- the SDK types its own list this way
|
|
@@ -393,11 +405,11 @@ export function buildApiTools(
|
|
|
393
405
|
|
|
394
406
|
const typedNames: string[] = [];
|
|
395
407
|
for (const op of ops) {
|
|
396
|
-
tools.push(typedTool(api, op, env, bucket, log, keepIn));
|
|
408
|
+
tools.push(typedTool(api, op, env, bucket, log, keepIn, scope));
|
|
397
409
|
typedNames.push(typedToolName(api.name, op.id));
|
|
398
410
|
methods[`mcp__foldrun_apis__${typedToolName(api.name, op.id)}`] = String(op.method ?? "").toUpperCase();
|
|
399
411
|
}
|
|
400
|
-
if (withGeneric) tools.push(genericTool(api, env, bucket, log, keepIn));
|
|
412
|
+
if (withGeneric) tools.push(genericTool(api, env, bucket, log, keepIn, scope));
|
|
401
413
|
|
|
402
414
|
const names = [...typedNames, ...(withGeneric ? [`call_${api.name}`] : [])];
|
|
403
415
|
for (const n of names) {
|
package/src/browser-pod.ts
CHANGED
|
@@ -162,6 +162,9 @@ export function classifyCall(tool: string, input: Record<string, unknown> | unde
|
|
|
162
162
|
// tools: [peers] — reading the roster changes nothing; a message to a
|
|
163
163
|
// colleague is said, and a re-run would say it twice.
|
|
164
164
|
if (server === "foldrun_peers") return name === "peers_list" ? { write: false } : write("a message to a colleague");
|
|
165
|
+
// remotes: — reading where a request got to changes nothing; a send, a
|
|
166
|
+
// reply or a cancel acts on an outside agent's system.
|
|
167
|
+
if (server === "foldrun_a2a") return name === "a2a_status" ? { write: false } : write(`${name.replace(/^a2a_/, "")} to an outside agent`);
|
|
165
168
|
if (server === "foldrun_scripts") {
|
|
166
169
|
if (name !== "web") return write(`script ${name}`);
|
|
167
170
|
const action = String(args.action ?? "").trim().toLowerCase();
|
package/src/completions.ts
CHANGED
|
@@ -394,9 +394,9 @@ export function completionsAt(
|
|
|
394
394
|
// that field's files and nothing else. `[[search]]` in tools: is how a
|
|
395
395
|
// tool named after a runtime group gets granted; offering the group
|
|
396
396
|
// here would defeat the point of the brackets.
|
|
397
|
-
const inlineOwner = line.match(/^\s*([a-zA-Z_]
|
|
397
|
+
const inlineOwner = line.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*):\s*(?:\[.*)?\[\[[^\]]*$/)?.[1];
|
|
398
398
|
const blockOwner = /^\s+-\s*\[\[/.test(line)
|
|
399
|
-
? before.slice(0, lineStart).split("\n").reverse().find((l) => /^\s*[a-zA-Z_]
|
|
399
|
+
? before.slice(0, lineStart).split("\n").reverse().find((l) => /^\s*[a-zA-Z_][a-zA-Z0-9_]*:\s*$/.test(l))?.trim().replace(":", "")
|
|
400
400
|
: undefined;
|
|
401
401
|
const owner = inlineOwner ?? blockOwner;
|
|
402
402
|
const byField: Record<string, Completion[]> = {
|
|
@@ -460,7 +460,7 @@ export function completionsAt(
|
|
|
460
460
|
// been chosen yet, one at a time. Fixed combinations were the wrong
|
|
461
461
|
// model: nobody wants a menu of every permutation, they want to add the
|
|
462
462
|
// next verb.
|
|
463
|
-
const inlineList = line.match(/^(\s*)([a-zA-Z_]
|
|
463
|
+
const inlineList = line.match(/^(\s*)([a-zA-Z_][a-zA-Z0-9_]*):\s*\[([^\]]*)$/);
|
|
464
464
|
if (inlineList) {
|
|
465
465
|
const [, , key, inside] = inlineList;
|
|
466
466
|
// The inline and block forms of a list are the same field — `tools: [a, b]`
|
|
@@ -505,7 +505,7 @@ export function completionsAt(
|
|
|
505
505
|
}
|
|
506
506
|
|
|
507
507
|
// 4. `key: value` — the value side of a known field.
|
|
508
|
-
const kv = line.match(/^(\s*)([a-zA-Z_]
|
|
508
|
+
const kv = line.match(/^(\s*)([a-zA-Z_][a-zA-Z0-9_]*):\s*(\S*)$/);
|
|
509
509
|
if (kv) {
|
|
510
510
|
const [, , key, partial] = kv;
|
|
511
511
|
const from = cursor - partial.length;
|
|
@@ -587,7 +587,7 @@ export function completionsAt(
|
|
|
587
587
|
const partial = listItem[2];
|
|
588
588
|
const from = cursor - partial.length;
|
|
589
589
|
const priorLines = before.slice(0, lineStart).split("\n").reverse();
|
|
590
|
-
const owner = priorLines.find((l) => /^\s*[a-zA-Z_]
|
|
590
|
+
const owner = priorLines.find((l) => /^\s*[a-zA-Z_][a-zA-Z0-9_]*:\s*$/.test(l))?.trim().replace(":", "");
|
|
591
591
|
const byOwner: Record<string, Completion[]> = {
|
|
592
592
|
// One list now: built-ins and your own tools are granted the same way.
|
|
593
593
|
// Groups first (they cover most needs), then your own tools — the ones
|
|
@@ -629,7 +629,7 @@ export function completionsAt(
|
|
|
629
629
|
const partial = mapping[2] ?? "";
|
|
630
630
|
const priorLines = before.slice(0, lineStart).split("\n").reverse();
|
|
631
631
|
const owner = priorLines
|
|
632
|
-
.find((l) => /^\s*[a-zA-Z_]
|
|
632
|
+
.find((l) => /^\s*[a-zA-Z_][a-zA-Z0-9_]*:\s*$/.test(l))
|
|
633
633
|
?.trim()
|
|
634
634
|
.replace(":", "");
|
|
635
635
|
const byOwner: Record<string, Completion[]> = {
|
|
@@ -666,7 +666,7 @@ export function completionsAt(
|
|
|
666
666
|
// kind of file — the block takes nothing else.
|
|
667
667
|
const fmClose = text.indexOf("\n---", text.indexOf("---") + 3);
|
|
668
668
|
const insideFrontmatter = text.startsWith("---") && (fmClose === -1 || cursor <= fmClose);
|
|
669
|
-
const nested = line.match(/^(\s+)([a-zA-Z_]
|
|
669
|
+
const nested = line.match(/^(\s+)([a-zA-Z_][a-zA-Z0-9_]*|)$/);
|
|
670
670
|
if (insideFrontmatter && nested) {
|
|
671
671
|
const header = before.slice(0, lineStart).split("\n").reverse().find((l) => /^\S/.test(l));
|
|
672
672
|
const job = JOBS.find((j) => new RegExp(`^${j.name}\\s*:\\s*$`).test(header ?? ""));
|
|
@@ -699,7 +699,7 @@ export function completionsAt(
|
|
|
699
699
|
// this kind of file. This is where you learn the format by writing it.
|
|
700
700
|
const fmEnd = text.indexOf("\n---", text.indexOf("---") + 3);
|
|
701
701
|
const inFrontmatter = text.startsWith("---") && (fmEnd === -1 || cursor <= fmEnd);
|
|
702
|
-
if (inFrontmatter && /^[a-zA-Z_][a-zA-
|
|
702
|
+
if (inFrontmatter && /^[a-zA-Z_][a-zA-Z0-9_-]*$|^$/.test(line)) {
|
|
703
703
|
const from = cursor - line.length;
|
|
704
704
|
return { from, query: line, items: filter(fieldsFor(path, kind), line), title: `${kind} fields` };
|
|
705
705
|
}
|
|
@@ -88,6 +88,7 @@ const AGENT: FieldSpec[] = [
|
|
|
88
88
|
{ key: "scripts", type: "list", help: "programs in scripts/, each a tool" },
|
|
89
89
|
{ key: "apis", type: "list", help: "an HTTP API as a tool" },
|
|
90
90
|
{ key: "mcpServers", type: "object", help: "MCP servers, name → {command | url, args, env, headers}" },
|
|
91
|
+
{ key: "remotes", type: "list", help: "outside A2A agents it may call — [{name, card: the https URL of its agent card, secret?: a vault NAME sent as a bearer token, description?}]" },
|
|
91
92
|
{ key: "agents", type: ["list", "string"], help: "colleagues it may consult" },
|
|
92
93
|
{ key: "subagents", type: ["list", "string"], help: "colleagues it may delegate to" },
|
|
93
94
|
{ key: "sandbox", type: "string", values: ["own", "parent"], help: "when delegated to as a sub-agent: own = its own sandbox, tools, secrets and network; parent (default) = inside the delegating step's" },
|
|
@@ -174,6 +175,7 @@ const FLOW: FieldSpec[] = [
|
|
|
174
175
|
{ key: "budget", type: ["number", "string"], help: "USD per run" },
|
|
175
176
|
{ key: "compute", type: "object", help: "{vcpu, memory, disk} one run's steps may hold together at once — only narrows the workspace's and account's" },
|
|
176
177
|
{ key: "live", type: "boolean", help: "true: steps may act for real in a test run" },
|
|
178
|
+
{ key: "a2a", type: "object", help: "serve this flow to outside agents over A2A 1.0 — {publish: true, name?, description?, examples?, public_card?, input?}; changing it needs workspace:manage" },
|
|
177
179
|
];
|
|
178
180
|
|
|
179
181
|
const TOOL: FieldSpec[] = [
|
|
@@ -10,6 +10,8 @@ import { cronProblem, parseCron, cronCanFire } from "./cron.ts";
|
|
|
10
10
|
import { SECRET_NAME } from "./secrets.ts";
|
|
11
11
|
import { modelSecretCollision } from "./model-credential.ts";
|
|
12
12
|
import { resourcesProblem, computeProblem } from "./resources.ts";
|
|
13
|
+
import { remotesProblem } from "./a2a-remotes.ts";
|
|
14
|
+
import { a2aPublishProblem } from "./a2a-publish.ts";
|
|
13
15
|
|
|
14
16
|
export { FRONTMATTER, schemaKindOf, type FieldSpec, type FieldType, type SchemaKind } from "./frontmatter-fields.ts";
|
|
15
17
|
|
|
@@ -33,6 +35,7 @@ const CHECKS: Partial<Record<SchemaKind, Record<string, (value: unknown) => stri
|
|
|
33
35
|
"agent": {
|
|
34
36
|
effort: (v) => (resolveEffort(v) ? null : `"${String(v)}" is not an effort — low, medium, high, xhigh or max; the step runs at the model's default`),
|
|
35
37
|
apis: apisProblem,
|
|
38
|
+
remotes: remotesProblem,
|
|
36
39
|
budget: (v) => budgetProblem(v, ["run"]),
|
|
37
40
|
ask: (v) => askProblem(v),
|
|
38
41
|
resources: resourcesProblem,
|
|
@@ -57,6 +60,7 @@ const CHECKS: Partial<Record<SchemaKind, Record<string, (value: unknown) => stri
|
|
|
57
60
|
pause_when: (v) => (String(v).length <= 60 ? null : "is longer than 60 characters"),
|
|
58
61
|
budget: (v) => budgetProblem(v, ["run"]),
|
|
59
62
|
compute: computeProblem,
|
|
63
|
+
a2a: a2aPublishProblem,
|
|
60
64
|
},
|
|
61
65
|
"tool": {
|
|
62
66
|
transport: (v) => (["http", "script", "mcp"].includes(String(v).toLowerCase()) ? null : `"${String(v)}" is not a transport — http, script or mcp`),
|
package/src/limits.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// web.search: 40 # one action of the web tool
|
|
6
6
|
// web: 150 # every web action together
|
|
7
7
|
// crm: 20 # an API, script or MCP tool, by the name it is granted as
|
|
8
|
+
// a2a: 10 # messages to outside agents (a2a_send and a2a_reply)
|
|
9
|
+
// a2a.status: 30 # polls of them, apart from the sends
|
|
8
10
|
// calls: 300 # every tool call in the step, whatever the tool
|
|
9
11
|
// repeat: 5 # the same call — one tool, the same input — at most this often
|
|
10
12
|
//
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
import crypto from "node:crypto";
|
|
23
25
|
import { WEB_ACTIONS } from "./providers.ts";
|
|
24
26
|
import { DELEGATE_TOOLS } from "./subagents.ts";
|
|
27
|
+
import { A2A_SERVER, A2A_LIMIT_KEY, A2A_LIMIT_ACTIONS } from "./a2a-remotes.ts";
|
|
25
28
|
|
|
26
29
|
export type Limits = Record<string, number>;
|
|
27
30
|
|
|
@@ -103,8 +106,13 @@ export function readLimits(raw: unknown, where = "limits"): { limits: Limits; pr
|
|
|
103
106
|
}
|
|
104
107
|
if (k.includes(".")) {
|
|
105
108
|
const [head, action] = k.split(".");
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
const web = head === "web" && (WEB_ACTIONS as readonly string[]).includes(action);
|
|
110
|
+
const a2a = head === A2A_LIMIT_KEY && (A2A_LIMIT_ACTIONS as readonly string[]).includes(action);
|
|
111
|
+
if (!web && !a2a) {
|
|
112
|
+
problems.push(
|
|
113
|
+
`${where}: "${k}" is not a key — the dotted keys are web.<action>: ${WEB_ACTIONS.map((a) => `web.${a}`).join(", ")}, ` +
|
|
114
|
+
`and ${A2A_LIMIT_ACTIONS.map((a) => `${A2A_LIMIT_KEY}.${a}`).join(", ")}.`,
|
|
115
|
+
);
|
|
108
116
|
continue;
|
|
109
117
|
}
|
|
110
118
|
}
|
|
@@ -166,6 +174,15 @@ export function limitKeysFor(toolName: string, input: Record<string, unknown> |
|
|
|
166
174
|
if (toolName === "WebSearch") { webAction("search"); return keys; }
|
|
167
175
|
if (toolName === "WebFetch") { webAction("fetch"); return keys; }
|
|
168
176
|
if ((DELEGATE_TOOLS as readonly string[]).includes(toolName)) return keys;
|
|
177
|
+
// remotes: — a message to an outside agent (a2a_send, a2a_reply) counts
|
|
178
|
+
// under `a2a`; every tool also under its own `a2a.<action>`, so polling
|
|
179
|
+
// (`a2a.status`) is capped apart from sending.
|
|
180
|
+
const a2a = new RegExp(`^mcp__${A2A_SERVER}__a2a_([a-z]+)$`).exec(toolName);
|
|
181
|
+
if (a2a) {
|
|
182
|
+
if (a2a[1] === "send" || a2a[1] === "reply") keys.push(A2A_LIMIT_KEY);
|
|
183
|
+
keys.push(`${A2A_LIMIT_KEY}.${a2a[1]}`);
|
|
184
|
+
return keys;
|
|
185
|
+
}
|
|
169
186
|
const owner = owners[toolName];
|
|
170
187
|
const mcp = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
171
188
|
const name = owner
|
|
@@ -274,7 +291,7 @@ export function limitKeyProblems(
|
|
|
274
291
|
ctx: { known: Iterable<string>; granted: Iterable<string> },
|
|
275
292
|
): { level: "error" | "warn"; message: string }[] {
|
|
276
293
|
const out: { level: "error" | "warn"; message: string }[] = [];
|
|
277
|
-
const builtinKeys = ["read", "write", "code", ...Object.keys(BUILTIN_GROUP), "TodoWrite", "search", "history", "desks", "ask", "web"];
|
|
294
|
+
const builtinKeys = ["read", "write", "code", ...Object.keys(BUILTIN_GROUP), "TodoWrite", "search", "history", "desks", "ask", "web", A2A_LIMIT_KEY];
|
|
278
295
|
const known = new Set([...builtinKeys, ...ctx.known].map(normKey));
|
|
279
296
|
const granted = new Set([...ctx.granted].flatMap((g) => {
|
|
280
297
|
const n = normKey(g);
|
|
@@ -287,10 +304,11 @@ export function limitKeyProblems(
|
|
|
287
304
|
}).map(normKey));
|
|
288
305
|
for (const key of Object.keys(limits)) {
|
|
289
306
|
if (key === TOTAL_KEY || key === REPEAT_KEY) continue;
|
|
290
|
-
const base = key.startsWith("web.") ? "web" : key;
|
|
307
|
+
const base = key.startsWith("web.") ? "web" : key.startsWith(`${A2A_LIMIT_KEY}.`) ? A2A_LIMIT_KEY : key;
|
|
291
308
|
const n = normKey(base);
|
|
292
309
|
if (!known.has(n)) {
|
|
293
310
|
const valid = [TOTAL_KEY, "web", ...WEB_ACTIONS.map((a) => `web.${a}`), ...[...new Set([...ctx.granted].map(String))].filter((g) => g !== "web")];
|
|
311
|
+
if ([...ctx.granted].map(String).includes(A2A_LIMIT_KEY)) valid.push(...A2A_LIMIT_ACTIONS.map((a) => `${A2A_LIMIT_KEY}.${a}`));
|
|
294
312
|
out.push({ level: "error", message: `limits: ${key} — not a tool this agent could call. The keys: ${[...new Set(valid)].join(", ")}.` });
|
|
295
313
|
} else if (!granted.has(n)) {
|
|
296
314
|
out.push({ level: "warn", message: `limits: ${key} — this agent is not granted ${base}, so the limit never applies` });
|
package/src/run-container.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { isOperatorEvent, type OperatorEvent } from "./operator.ts";
|
|
|
44
44
|
import type { SearchRoot, RunDigest } from "./context-tools.ts";
|
|
45
45
|
import type { TranslatorSpec } from "./translator.ts";
|
|
46
46
|
import { parseBrowserPod, type BrowserPodOutcome } from "./browser-pod.ts";
|
|
47
|
+
import type { RemoteSpec } from "./a2a-remotes.ts";
|
|
47
48
|
|
|
48
49
|
/** What crosses the boundary. Everything in here is values — pre-resolved
|
|
49
50
|
* secrets in headers, assembled prompt, serializable MCP configs. */
|
|
@@ -61,6 +62,10 @@ export interface ContainerStepInput {
|
|
|
61
62
|
/** http/stdio configs only — in-process servers are rebuilt inside. */
|
|
62
63
|
mcpServers: Record<string, McpServerConfig>;
|
|
63
64
|
apis: ApiSpec[]; // headers pre-substituted from the vault, host-side
|
|
65
|
+
/** `remotes:` — outside A2A agents, each spec's `token` filled host-side
|
|
66
|
+
* (`${NAME}` for the egress proxy, or the value with no proxy), and the
|
|
67
|
+
* run and step its handles are kept under (a2a-tools.ts). */
|
|
68
|
+
a2a?: { remotes: RemoteSpec[]; runId: string | null; step: number | null };
|
|
64
69
|
scripts: ScriptSpec[];
|
|
65
70
|
/** The agent's `runtime:` declaration — the driver builds the venv/npm
|
|
66
71
|
* prefix inside the container, where the network is. Without this,
|
|
@@ -749,6 +754,7 @@ try {
|
|
|
749
754
|
process.env.FOLDRUN_DATA = "/home/agent/.foldrun";
|
|
750
755
|
const { executeStep } = await import("@foldrun/core/step-exec");
|
|
751
756
|
const { buildApiTools } = await import("@foldrun/core/api-tools");
|
|
757
|
+
const { buildA2ATools } = await import("@foldrun/core/a2a-tools");
|
|
752
758
|
const { buildScriptTools } = await import("@foldrun/core/script-tools");
|
|
753
759
|
const { buildConsultTools } = await import("@foldrun/core/agent-tools");
|
|
754
760
|
const { buildSearchTools, buildHistoryTools, buildDeskTools } = await import("@foldrun/core/context-tools");
|
|
@@ -825,6 +831,20 @@ try {
|
|
|
825
831
|
const toolEnv = { ...env };
|
|
826
832
|
for (const [k, v] of Object.entries(env)) if (k.startsWith("FOLDRUN_DECLARED_")) toolEnv[k.slice("FOLDRUN_DECLARED_".length)] = v;
|
|
827
833
|
const api = buildApiTools("", input.apis, undefined, { env: toolEnv, missing: [] }, keepIn);
|
|
834
|
+
// remotes: — the four a2a tools, rebuilt from values. Each token crossed in
|
|
835
|
+
// its spec (a placeholder the proxy fills for the card's origin alone), so
|
|
836
|
+
// nothing here asks a vault; handles go under the workspace's state/.
|
|
837
|
+
const a2a = input.a2a && Array.isArray(input.a2a.remotes) && input.a2a.remotes.length
|
|
838
|
+
? buildA2ATools("", input.a2a.remotes, undefined, { env: {}, missing: [] }, {
|
|
839
|
+
stateDir: "/workspace/state/a2a/" + input.agentRel.split("/").pop(),
|
|
840
|
+
runId: input.a2a.runId,
|
|
841
|
+
step: input.a2a.step,
|
|
842
|
+
egress: env.FOLDRUN_EGRESS || null,
|
|
843
|
+
test: env.FOLDRUN_TEST_MODE === "1",
|
|
844
|
+
keepIn,
|
|
845
|
+
emit,
|
|
846
|
+
})
|
|
847
|
+
: { server: null, drainLog: () => [] };
|
|
828
848
|
const script = buildScriptTools(
|
|
829
849
|
agentDir,
|
|
830
850
|
input.scripts,
|
|
@@ -901,6 +921,7 @@ try {
|
|
|
901
921
|
mcpNames: input.mcpNames,
|
|
902
922
|
mcpServers: {
|
|
903
923
|
...(api.server ? { foldrun_apis: api.server } : {}),
|
|
924
|
+
...(a2a.server ? { foldrun_a2a: a2a.server } : {}),
|
|
904
925
|
...(script.server ? { foldrun_scripts: script.server } : {}),
|
|
905
926
|
...(consult.server ? { foldrun_agents: consult.server } : {}),
|
|
906
927
|
...(search.server ? { foldrun_search: search.server } : {}),
|
|
@@ -940,6 +961,7 @@ try {
|
|
|
940
961
|
emit,
|
|
941
962
|
});
|
|
942
963
|
for (const line of api.drainLog()) emit("info", "api: " + line);
|
|
964
|
+
for (const line of a2a.drainLog()) emit("info", "a2a: " + line);
|
|
943
965
|
for (const line of script.drainLog()) emit("info", "script: " + line);
|
|
944
966
|
if (translator) {
|
|
945
967
|
for (const line of translator.drainLog()) emit("info", line.startsWith("translator") ? line : "translator: " + line);
|