@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.
@@ -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,
@@ -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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldrun/core",
3
- "version": "11.0.0",
3
+ "version": "11.1.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",
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) cardProblems.set(r.name, 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);