@foldrun/core 11.0.0 → 11.1.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 +9 -0
- package/dist/src/a2a-client.js +29 -0
- package/dist/src/runner.js +3 -1
- package/package.json +1 -1
- package/src/a2a-client.ts +26 -0
- package/src/runner.ts +4 -1
package/dist/src/a2a-client.d.ts
CHANGED
|
@@ -112,6 +112,15 @@ export declare function stripHidden(text: string): {
|
|
|
112
112
|
/** The address a request to `target` goes to: the egress door when the
|
|
113
113
|
* token is a placeholder only the proxy can fill, else the target. */
|
|
114
114
|
export declare function routeFor(conn: Pick<A2AConnection, "token" | "egress">, target: string): string;
|
|
115
|
+
/** Why the egress proxy refused a request, or null for an answer that is
|
|
116
|
+
* the remote's. The diagnostic is fixed text of ours; the body's own words
|
|
117
|
+
* go back as `said`, for the untrusted fence — the proxy passes a remote's
|
|
118
|
+
* 403 through as it came, so a remote can send this exact shape (Codex
|
|
119
|
+
* review, 2 Nov 2026). */
|
|
120
|
+
export declare function egressRefusal(status: number, text: string): {
|
|
121
|
+
reason: string;
|
|
122
|
+
said: string | null;
|
|
123
|
+
} | null;
|
|
115
124
|
/**
|
|
116
125
|
* The endpoint a card names, if it names one we may call: a JSON-RPC
|
|
117
126
|
* interface of protocol 1.x at the card's own origin. Pure — the card in,
|
package/dist/src/a2a-client.js
CHANGED
|
@@ -129,6 +129,30 @@ export function routeFor(conn, target) {
|
|
|
129
129
|
}
|
|
130
130
|
return target;
|
|
131
131
|
}
|
|
132
|
+
/** Why the egress proxy refused a request, or null for an answer that is
|
|
133
|
+
* the remote's. The diagnostic is fixed text of ours; the body's own words
|
|
134
|
+
* go back as `said`, for the untrusted fence — the proxy passes a remote's
|
|
135
|
+
* 403 through as it came, so a remote can send this exact shape (Codex
|
|
136
|
+
* review, 2 Nov 2026). */
|
|
137
|
+
export function egressRefusal(status, text) {
|
|
138
|
+
if (status !== 403)
|
|
139
|
+
return null;
|
|
140
|
+
let body;
|
|
141
|
+
try {
|
|
142
|
+
body = JSON.parse(text);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const err = body?.error;
|
|
148
|
+
if (!err || err.type !== "egress")
|
|
149
|
+
return null;
|
|
150
|
+
const said = typeof err.message === "string" ? short(err.message.replace(/^egress:\s*/, ""), 200) : "";
|
|
151
|
+
const reason = /private|internal/i.test(said)
|
|
152
|
+
? "its address is not allowed — this platform does not call private or internal addresses"
|
|
153
|
+
: "the platform's egress refused it";
|
|
154
|
+
return { reason, said: said || null };
|
|
155
|
+
}
|
|
132
156
|
/**
|
|
133
157
|
* One HTTP exchange, read as JSON, to a limit, with no redirect followed.
|
|
134
158
|
*
|
|
@@ -178,6 +202,11 @@ async function exchange(conn, target, init, maxBytes) {
|
|
|
178
202
|
}
|
|
179
203
|
throw new A2AError(`${conn.remote}: the answer could not be read: ${short(err?.message ?? err, 200)}`);
|
|
180
204
|
}
|
|
205
|
+
// The egress proxy's own refusal ({error: {type: "egress"}}) is ours, not
|
|
206
|
+
// the remote's: the address is not allowed, and no secret would change it.
|
|
207
|
+
const egress = egressRefusal(res.status, text);
|
|
208
|
+
if (egress)
|
|
209
|
+
throw new A2AError(`${conn.remote} cannot be called from here: ${egress.reason}`, null, true, egress.said);
|
|
181
210
|
if (res.status === 401 || res.status === 403) {
|
|
182
211
|
throw new A2AError(`${conn.remote} refused the credential (HTTP ${res.status})${conn.token ? "" : " — this remote has no secret: set one"}`, null, true);
|
|
183
212
|
}
|
package/dist/src/runner.js
CHANGED
|
@@ -2211,8 +2211,10 @@ mayDefer) {
|
|
|
2211
2211
|
const cardProblems = new Map();
|
|
2212
2212
|
for (const r of remotes) {
|
|
2213
2213
|
const why = await platform.checkUntrustedUrl(r.card);
|
|
2214
|
-
if (why)
|
|
2214
|
+
if (why) {
|
|
2215
2215
|
cardProblems.set(r.name, why);
|
|
2216
|
+
console.warn(`[a2a] remotes: ${r.name} refused for ${tenant}/${path.basename(workspaceRootOf(agentDir))} (${step.agent}): ${why}`);
|
|
2217
|
+
}
|
|
2216
2218
|
}
|
|
2217
2219
|
const stepRemotes = remotes.map((r) => {
|
|
2218
2220
|
const refused = cardProblems.get(r.name);
|
package/package.json
CHANGED
package/src/a2a-client.ts
CHANGED
|
@@ -200,6 +200,28 @@ export function routeFor(conn: Pick<A2AConnection, "token" | "egress">, target:
|
|
|
200
200
|
return target;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/** Why the egress proxy refused a request, or null for an answer that is
|
|
204
|
+
* the remote's. The diagnostic is fixed text of ours; the body's own words
|
|
205
|
+
* go back as `said`, for the untrusted fence — the proxy passes a remote's
|
|
206
|
+
* 403 through as it came, so a remote can send this exact shape (Codex
|
|
207
|
+
* review, 2 Nov 2026). */
|
|
208
|
+
export function egressRefusal(status: number, text: string): { reason: string; said: string | null } | null {
|
|
209
|
+
if (status !== 403) return null;
|
|
210
|
+
let body: unknown;
|
|
211
|
+
try {
|
|
212
|
+
body = JSON.parse(text);
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const err = (body as { error?: { type?: unknown; message?: unknown } } | null)?.error;
|
|
217
|
+
if (!err || err.type !== "egress") return null;
|
|
218
|
+
const said = typeof err.message === "string" ? short(err.message.replace(/^egress:\s*/, ""), 200) : "";
|
|
219
|
+
const reason = /private|internal/i.test(said)
|
|
220
|
+
? "its address is not allowed — this platform does not call private or internal addresses"
|
|
221
|
+
: "the platform's egress refused it";
|
|
222
|
+
return { reason, said: said || null };
|
|
223
|
+
}
|
|
224
|
+
|
|
203
225
|
/**
|
|
204
226
|
* One HTTP exchange, read as JSON, to a limit, with no redirect followed.
|
|
205
227
|
*
|
|
@@ -251,6 +273,10 @@ async function exchange(
|
|
|
251
273
|
}
|
|
252
274
|
throw new A2AError(`${conn.remote}: the answer could not be read: ${short((err as Error)?.message ?? err, 200)}`);
|
|
253
275
|
}
|
|
276
|
+
// The egress proxy's own refusal ({error: {type: "egress"}}) is ours, not
|
|
277
|
+
// the remote's: the address is not allowed, and no secret would change it.
|
|
278
|
+
const egress = egressRefusal(res.status, text);
|
|
279
|
+
if (egress) throw new A2AError(`${conn.remote} cannot be called from here: ${egress.reason}`, null, true, egress.said);
|
|
254
280
|
if (res.status === 401 || res.status === 403) {
|
|
255
281
|
throw new A2AError(`${conn.remote} refused the credential (HTTP ${res.status})${conn.token ? "" : " — this remote has no secret: set one"}`, null, true);
|
|
256
282
|
}
|
package/src/runner.ts
CHANGED
|
@@ -2407,7 +2407,10 @@ async function runStep(
|
|
|
2407
2407
|
const cardProblems = new Map<string, string>();
|
|
2408
2408
|
for (const r of remotes) {
|
|
2409
2409
|
const why = await platform.checkUntrustedUrl(r.card);
|
|
2410
|
-
if (why)
|
|
2410
|
+
if (why) {
|
|
2411
|
+
cardProblems.set(r.name, why);
|
|
2412
|
+
console.warn(`[a2a] remotes: ${r.name} refused for ${tenant}/${path.basename(workspaceRootOf(agentDir))} (${step.agent}): ${why}`);
|
|
2413
|
+
}
|
|
2411
2414
|
}
|
|
2412
2415
|
const stepRemotes: RemoteSpec[] = remotes.map((r) => {
|
|
2413
2416
|
const refused = cardProblems.get(r.name);
|