a2a-exposed 0.8.36 → 0.8.37
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/lib/hosted.mjs +5 -2
- package/lib/main.mjs +1 -0
- package/package.json +1 -1
- package/worker/src/index.ts +5 -3
- package/worker/src/version.ts +1 -1
- package/worker/test/reply-delivery.test.ts +33 -0
package/lib/hosted.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { controlOrigin, deviceLogin } from "./account.mjs";
|
|
|
5
5
|
import { CliError, upstreamUrlProblem } from "./a2a.mjs";
|
|
6
6
|
|
|
7
7
|
export const SETUP_SCOPES = ["agents:read", "agents:create", "agents:owner-token", "wake:write", "wake:test"];
|
|
8
|
+
const SETUP_UNSUPPORTED = "This service does not expose scoped hosted setup capabilities. Check the --control-url service origin. Use the service dashboard to create an agent and configure Wake settings, or hosted MCP create_agent/set_wake with human confirmation and secret entry. Do not run init or deploy to repair a hosted address; no setup approval was requested.";
|
|
8
9
|
const NEXT = "Run hosted capabilities --json to list scopes and presets; login --machine NAME --scopes SCOPES to approve setup.";
|
|
9
10
|
const fail = (code, next) => { const error = new CliError(next); error.result = { ok: false, code, next }; throw error; };
|
|
10
11
|
const printable = value => typeof value === "string" && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value);
|
|
@@ -34,14 +35,16 @@ function originFor(o = {}) {
|
|
|
34
35
|
|
|
35
36
|
async function request(origin, path, { token = "", method = "GET", body, fetchImpl = fetch } = {}) {
|
|
36
37
|
let response, data;
|
|
38
|
+
const discovery = method === "GET" && path === "/api/v1/setup/capabilities";
|
|
37
39
|
try {
|
|
38
40
|
response = await fetchImpl(`${origin}${path}`, {
|
|
39
41
|
method, redirect: "error", signal: AbortSignal.timeout(30_000),
|
|
40
42
|
headers: { accept: "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}), ...(body === undefined ? {} : { "content-type": "application/json" }) },
|
|
41
43
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
42
44
|
});
|
|
43
|
-
data = await response.json();
|
|
45
|
+
if (!(discovery && response.status === 404)) data = await response.json();
|
|
44
46
|
} catch { fail("unavailable", "Hosted setup did not answer with JSON. Check the service and try again. Do not repeat a create request until hosted list confirms its result."); }
|
|
47
|
+
if (discovery && response.status === 404) fail("unsupported", SETUP_UNSUPPORTED);
|
|
45
48
|
if (!data || typeof data !== "object" || Array.isArray(data)) fail("invalid_response", "The hosted service returned an invalid setup response.");
|
|
46
49
|
if (!response.ok) {
|
|
47
50
|
if (response.status === 429 && data.error === "too_many_references") {
|
|
@@ -70,7 +73,7 @@ async function request(origin, path, { token = "", method = "GET", body, fetchIm
|
|
|
70
73
|
|
|
71
74
|
export async function capabilities(o = {}) {
|
|
72
75
|
const data = await request(originFor(o), "/api/v1/setup/capabilities");
|
|
73
|
-
if (!Array.isArray(data.scopes) || !Array.isArray(data.wake?.presets)) fail("unsupported",
|
|
76
|
+
if (!Array.isArray(data.scopes) || !Array.isArray(data.wake?.presets)) fail("unsupported", SETUP_UNSUPPORTED);
|
|
74
77
|
return { ok: true, code: "capabilities", next: "Select explicit scopes and a wake preset from this listing.", scopes: data.scopes.filter(s => SETUP_SCOPES.includes(s)), presets: data.wake.presets.filter(s => /^[a-z-]+$/.test(s)), preflight: data.preflight === true, secret_reference: data.wake.secret_reference === true, wake_removal: data.wake.removal === true, secure_link_create: data.wake.secure_link_create === true };
|
|
75
78
|
}
|
|
76
79
|
|
package/lib/main.mjs
CHANGED
|
@@ -28,6 +28,7 @@ Setup options: --skip-install skips dependency installation when dependencies al
|
|
|
28
28
|
|
|
29
29
|
Hosted setup:
|
|
30
30
|
hosted capabilities --json list setup scopes, wake presets and service capabilities
|
|
31
|
+
unsupported/404: check --control-url; use dashboard or hosted MCP with human confirmation
|
|
31
32
|
login --machine NAME --scopes SCOPES [--control-url URL] --json
|
|
32
33
|
approve the explicit scopes using the printed website link once;
|
|
33
34
|
save the scoped grant locally (mode 600), never print it
|
package/package.json
CHANGED
package/worker/src/index.ts
CHANGED
|
@@ -1147,7 +1147,7 @@ async function checkedPeerFetch(ctx: TenantContext, ectx: ExecutionContext, base
|
|
|
1147
1147
|
try { baseUrl = new URL(base); target = new URL(url); } catch { throw new ToolError("peer endpoint is not a URL"); }
|
|
1148
1148
|
if (target.origin !== baseUrl.origin) throw new ToolError("peer card endpoint refused: it is on a different origin");
|
|
1149
1149
|
const why = peerUrlProblem(target.href);
|
|
1150
|
-
if (why) throw new ToolError(`peer endpoint ${target.
|
|
1150
|
+
if (why) throw new ToolError(`peer endpoint ${target.host} refused: ${why}`);
|
|
1151
1151
|
const internal = await fetchTenant(ctx, target, { ...init, redirect: "manual" });
|
|
1152
1152
|
if (internal) {
|
|
1153
1153
|
if (internal.status >= 300 && internal.status < 400) throw new ToolError("peer endpoint refused: redirects are not allowed");
|
|
@@ -1563,10 +1563,12 @@ async function deliverReplyMessage(ctx: TenantContext, ectx: ExecutionContext, t
|
|
|
1563
1563
|
const pushed = await notifyPeer(ctx, ectx, task, signal);
|
|
1564
1564
|
const failed = pushed.find((x: Json) => !x.ok);
|
|
1565
1565
|
const target = safeTarget(undefined, String((failed || pushed[0])?.url || ""));
|
|
1566
|
-
const
|
|
1566
|
+
const info = String((failed || pushed[0])?.info || "");
|
|
1567
|
+
const http = /^HTTP (\d{3})$/.exec(info);
|
|
1568
|
+
const httpStatus = http && http[0] === info ? Number(http[1]) : undefined;
|
|
1567
1569
|
return {
|
|
1568
1570
|
status: failed ? "delivery-failed" : "delivered", method: "push", taskId: task.id, state,
|
|
1569
|
-
...(target ? { target } : {}), ...(
|
|
1571
|
+
...(target ? { target } : {}), ...(httpStatus !== undefined && httpStatus >= 100 && httpStatus <= 599 ? { httpStatus } : {}),
|
|
1570
1572
|
detail: failed ? "A task push failed; the reply remains stored on the task." : "Delivered as the task's existing push update.",
|
|
1571
1573
|
};
|
|
1572
1574
|
}
|
package/worker/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Inbox build. Keep this equal to `version` in cli/package.json. */
|
|
2
|
-
export const VERSION = "0.8.
|
|
2
|
+
export const VERSION = "0.8.37";
|
|
@@ -259,3 +259,36 @@ for (const version of ["0.3", "1.0"]) {
|
|
|
259
259
|
});
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
test("push status only accepts an exact HTTP result in the output schema range", async (t) => {
|
|
265
|
+
const s = await fixture(t as never);
|
|
266
|
+
const originalFetch = globalThis.fetch;
|
|
267
|
+
t.after(() => { globalThis.fetch = originalFetch; });
|
|
268
|
+
for (const value of [200, 307, 401, 599, 99, 600, 999, "HTTP 999", "prefix HTTP 401 suffix", "HTTP 401\n"]) {
|
|
269
|
+
globalThis.fetch = (async () => {
|
|
270
|
+
if (typeof value === "string") throw value;
|
|
271
|
+
const response = new Response(null);
|
|
272
|
+
Object.defineProperty(response, "status", { value });
|
|
273
|
+
return response;
|
|
274
|
+
}) as typeof fetch;
|
|
275
|
+
const taskId = await s.sendToBob("status " + value, crypto.randomUUID(), { id: "cfg", url: "https://push.example.net/update" });
|
|
276
|
+
const result = await s.owner(s.b, "acct-bob", "POST", "/owner/mcp/reply", { taskId, text: "stored answer" });
|
|
277
|
+
const delivery = (result.body as any).structuredContent as Delivery;
|
|
278
|
+
assert.equal(delivery.httpStatus, typeof value === "number" && value >= 100 && value <= 599 ? value : undefined, String(value));
|
|
279
|
+
assert.equal(delivery.status, value === 200 ? "delivered" : "delivery-failed");
|
|
280
|
+
const stored = await s.dbB.prepare("SELECT state FROM tasks WHERE id = ?").bind(taskId).first() as { state: string };
|
|
281
|
+
assert.equal(stored.state, "completed");
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("direct reply refusal reports only the endpoint host", async (t) => {
|
|
286
|
+
const s = await fixture(t as never);
|
|
287
|
+
await s.dbB.prepare("UPDATE outbound_peers SET url = 'http://alice.example.com/private-path?private-query=yes' WHERE alias = 'alice'").run();
|
|
288
|
+
const taskId = await s.sendToBob("refused endpoint", "ctx-refused-url");
|
|
289
|
+
const result = await s.owner(s.b, "acct-bob", "POST", "/owner/mcp/reply", { taskId, text: "stored answer" });
|
|
290
|
+
const delivery = (result.body as any).structuredContent;
|
|
291
|
+
assert.equal(delivery.status, "delivery-failed");
|
|
292
|
+
assert.match(delivery.detail, /peer endpoint alice\.example\.com refused/);
|
|
293
|
+
assert.doesNotMatch(JSON.stringify(result.body), /private-path|private-query|http:\/\//);
|
|
294
|
+
});
|