a2a-exposed 0.8.32 → 0.8.33
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 +78 -8
- package/lib/main.mjs +13 -4
- package/package.json +1 -1
- package/worker/src/version.ts +1 -1
package/lib/hosted.mjs
CHANGED
|
@@ -4,11 +4,23 @@ import * as C from "./config.mjs";
|
|
|
4
4
|
import { controlOrigin, deviceLogin } from "./account.mjs";
|
|
5
5
|
import { CliError, upstreamUrlProblem } from "./a2a.mjs";
|
|
6
6
|
|
|
7
|
-
export const SETUP_SCOPES = ["agents:create", "agents:owner-token", "wake:write", "wake:test"];
|
|
7
|
+
export const SETUP_SCOPES = ["agents:read", "agents:create", "agents:owner-token", "wake:write", "wake:test"];
|
|
8
8
|
const NEXT = "Run hosted capabilities --json to list scopes and presets; login --machine NAME --scopes SCOPES to approve setup.";
|
|
9
9
|
const fail = (code, next) => { const error = new CliError(next); error.result = { ok: false, code, next }; throw error; };
|
|
10
10
|
const printable = value => typeof value === "string" && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value);
|
|
11
11
|
|
|
12
|
+
function retryAfter(response, data) {
|
|
13
|
+
const header = response.headers.get("retry-after");
|
|
14
|
+
let seconds;
|
|
15
|
+
if (header && /^\d+$/.test(header)) seconds = Number(header);
|
|
16
|
+
else if (header && /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(header)) {
|
|
17
|
+
const date = Date.parse(header);
|
|
18
|
+
if (Number.isFinite(date)) seconds = Math.max(0, Math.ceil((date - Date.now()) / 1000));
|
|
19
|
+
}
|
|
20
|
+
if (Number.isSafeInteger(seconds) && seconds >= 0) return seconds;
|
|
21
|
+
return Number.isSafeInteger(data.retry_after) && data.retry_after >= 0 ? data.retry_after : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
export function setupSession(env = process.env, saved = C.fileConfig()) {
|
|
13
25
|
const source = env.HOSTED_SETUP_URL || env.HOSTED_SETUP_TOKEN ? env : saved;
|
|
14
26
|
return { origin: controlOrigin(source.HOSTED_SETUP_URL), token: source.HOSTED_SETUP_TOKEN || "" };
|
|
@@ -32,10 +44,21 @@ async function request(origin, path, { token = "", method = "GET", body, fetchIm
|
|
|
32
44
|
} 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."); }
|
|
33
45
|
if (!data || typeof data !== "object" || Array.isArray(data)) fail("invalid_response", "The hosted service returned an invalid setup response.");
|
|
34
46
|
if (!response.ok) {
|
|
35
|
-
|
|
47
|
+
if (response.status === 429 && data.error === "too_many_references") {
|
|
48
|
+
const seconds = retryAfter(response, data);
|
|
49
|
+
const next = seconds === undefined
|
|
50
|
+
? "Wait for an unused reference to expire before running wake prepare again; no automatic retry was made."
|
|
51
|
+
: `Wait ${seconds} seconds before running wake prepare again with a new --reference-file; no automatic retry was made.`;
|
|
52
|
+
// Retain the capacity message, but never reflect arbitrary service text or credentials.
|
|
53
|
+
const message = typeof data.message === "string" && /^This setup grant has \d{1,10} unused references\. Wait \d{1,10} seconds before uploading another draft\.$/.test(data.message) ? data.message : undefined;
|
|
54
|
+
const error = new CliError(next);
|
|
55
|
+
error.result = { ok: false, code: "too_many_references", next, ...(message ? { message } : {}), ...(seconds === undefined ? {} : { retry_after: seconds }) };
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
const known = ["taken", "limit", "invalid_name", "reserved_name", "invalid_grant", "insufficient_scope", "conflict", "unavailable", "not_found", "test", "signing", "unreadable", "preset", "url", "header", "access", "invalid_reference", "body"];
|
|
36
59
|
const code = known.includes(data.error) ? data.error : `http_${response.status}`;
|
|
37
|
-
const next = code === "taken" || code === "reserved_name" ? "Choose another --name
|
|
38
|
-
: code === "limit" ? "The account is at its agent limit.
|
|
60
|
+
const next = code === "taken" || code === "reserved_name" ? "Choose another --name using hosted names --name NAME --json before creating."
|
|
61
|
+
: code === "limit" ? "The account is at its agent limit. Read hosted capacity --json and review account plans."
|
|
39
62
|
: code === "conflict" ? "Wake settings changed. Run wake preview --json, then test the draft again."
|
|
40
63
|
: code === "invalid_reference" ? "The wake reference was already used, expired or does not match this draft. Run wake prepare again with the draft inputs and a new --reference-file."
|
|
41
64
|
: code === "invalid_grant" || code === "insufficient_scope" ? NEXT
|
|
@@ -48,7 +71,7 @@ async function request(origin, path, { token = "", method = "GET", body, fetchIm
|
|
|
48
71
|
export async function capabilities(o = {}) {
|
|
49
72
|
const data = await request(originFor(o), "/api/v1/setup/capabilities");
|
|
50
73
|
if (!Array.isArray(data.scopes) || !Array.isArray(data.wake?.presets)) fail("unsupported", "This service does not expose hosted setup capabilities yet.");
|
|
51
|
-
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)), secret_reference: data.wake.secret_reference === true, secure_link_create: data.wake.secure_link_create === true };
|
|
74
|
+
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 };
|
|
52
75
|
}
|
|
53
76
|
|
|
54
77
|
export async function login(o = {}) {
|
|
@@ -75,12 +98,37 @@ export async function list(o = {}) {
|
|
|
75
98
|
return { ok: true, code: "agents", next: "This listing contains hosted agents only; Cloudflare-linked Workers remain in dashboard Your agents. Select an agent, or run hosted create --name NAME --json.", agents: data.tenants.map(row => ({ name: String(row.name || ""), status: String(row.status || ""), version: row.version })) };
|
|
76
99
|
}
|
|
77
100
|
|
|
101
|
+
export async function names(o = {}) {
|
|
102
|
+
const name = String(o.name || "").trim().toLowerCase();
|
|
103
|
+
if (!/^[a-z0-9][a-z0-9-]{2,30}[a-z0-9]$/.test(name)) fail("invalid_name", "Pass one explicit --name of 4–32 letters, numbers or hyphens, starting and ending with a letter or number. Use hosted names --name NAME --json before approving setup.");
|
|
104
|
+
const origin = originFor(o);
|
|
105
|
+
if (!(await capabilities(o)).preflight) fail("unsupported", "This service does not expose name preflight. Use hosted capabilities --json or the website.");
|
|
106
|
+
const data = await request(origin, `/api/v1/setup/name?name=${encodeURIComponent(name)}`);
|
|
107
|
+
if (data.name !== name || typeof data.available !== "boolean" || !["available", "invalid_name", "reserved_name", "taken"].includes(data.reason) || data.available !== (data.reason === "available")) fail("invalid_response", "The service did not return a valid name availability result. No creation was attempted.");
|
|
108
|
+
return { ok: true, code: "name_availability", name, available: data.available, reason: data.reason, next: data.available ? "This name is available now, without a reservation. Run hosted capacity --json before hosted create --name NAME --json." : "Choose another --name and repeat hosted names --name NAME --json before requesting creation access." };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function capacity(o = {}) {
|
|
112
|
+
const { origin, token } = accountCredential(o);
|
|
113
|
+
if (!(await capabilities(o)).preflight) fail("unsupported", "This service does not expose capacity preflight. Use hosted capabilities --json or the website.");
|
|
114
|
+
const data = await request(origin, "/api/v1/setup/capacity", { token });
|
|
115
|
+
const count = value => Number.isSafeInteger(value) && value >= 0;
|
|
116
|
+
if (!(data.limit === null || count(data.limit)) || !count(data.used) || !(data.remaining === null || count(data.remaining)) || data.remaining !== (data.limit === null ? null : Math.max(0, data.limit - data.used)) || data.can_create !== (data.remaining === null || data.remaining > 0)) fail("invalid_response", "The service did not return valid account capacity. No creation was attempted.");
|
|
117
|
+
return { ok: true, code: "capacity", limit: data.limit, used: data.used, remaining: data.remaining, can_create: data.can_create, next: data.can_create ? "This is a read-only snapshot. Check hosted names --name NAME --json, then explicitly approve creation scopes if needed and run hosted create --name NAME --json." : "The account has no creation room. Review its agents and plan before another creation attempt." };
|
|
118
|
+
}
|
|
119
|
+
|
|
78
120
|
export async function create(o = {}) {
|
|
79
121
|
const name = String(o.name || "").trim().toLowerCase();
|
|
80
122
|
if (!/^[a-z0-9][a-z0-9-]{2,30}[a-z0-9]$/.test(name)) fail("invalid_name", "Pass --name with 4–32 letters, numbers or hyphens, starting and ending with a letter or number. Run hosted list --json first.");
|
|
81
123
|
// Never replace an existing deployment or an environment-selected credential.
|
|
82
124
|
if (C.get("A2A_BASE_URL") || C.get("A2A_OWNER_TOKEN") || C.get("A2A_WORKER_NAME") || C.get("A2A_D1_ID")) fail("config_in_use", "Use --config-dir with a separate directory for the new agent; keep the existing deployment configuration.");
|
|
83
125
|
const { origin, token } = accountCredential(o);
|
|
126
|
+
const service = await capabilities(o);
|
|
127
|
+
if (service.preflight) {
|
|
128
|
+
const available = await names(o);
|
|
129
|
+
if (!available.available) fail(available.reason, "Choose a name with hosted names --name NAME --json before creating.");
|
|
130
|
+
if (!(await capacity(o)).can_create) fail("limit", "The account is at its agent limit. Read hosted capacity --json and review its agents or plan.");
|
|
131
|
+
}
|
|
84
132
|
const before = await list(o);
|
|
85
133
|
if (before.agents.some(row => row.name === name && row.status !== "deleted")) fail("taken", "That name already belongs to this account. Select it from hosted list --json or choose another --name.");
|
|
86
134
|
const data = await request(origin, "/api/v1/tenants", { token, method: "POST", body: { name } });
|
|
@@ -146,14 +194,20 @@ export async function status(o = {}) {
|
|
|
146
194
|
|
|
147
195
|
export async function wake(sub, o = {}) {
|
|
148
196
|
if (!["preview", "test", "set", "prepare", "remove", "unset"].includes(sub)) fail("usage", "Use wake preview|test|set|prepare|remove. List presets with hosted capabilities --json.");
|
|
149
|
-
|
|
197
|
+
const removing = sub === "remove" || sub === "unset";
|
|
198
|
+
const preparingRemoval = sub === "prepare" && o.clear === true;
|
|
199
|
+
if (o.clear && !preparingRemoval) fail("usage", "Use wake prepare --clear for an MCP removal reference, or wake remove --version N. Run wake preview --json to list the current version.");
|
|
200
|
+
if (removing || preparingRemoval) {
|
|
201
|
+
if (o.url || o.preset || Object.keys(o).some(key => /^(?:key|hmac-secret|access-client)/.test(key))) fail("removal_inputs", "Omit draft settings and secrets when removing a wake. Run wake preview --json first.");
|
|
202
|
+
if (!/^(0|[1-9][0-9]*)$/.test(String(o.version ?? "")) || !Number.isSafeInteger(Number(o.version))) fail("version", "Pass --version N selected from wake preview --agent NAME --json. Removal never selects a version automatically.");
|
|
203
|
+
}
|
|
150
204
|
if (sub === "prepare") {
|
|
151
205
|
if (!o["reference-file"]) fail("reference_file", "Pass a new --reference-file FILE. The reference is saved mode 600 and never printed.");
|
|
152
206
|
try { fs.lstatSync(o["reference-file"]); fail("reference_file", "Choose a new --reference-file path; existing files are never replaced."); }
|
|
153
207
|
catch (error) { if (error.result) throw error; if (error.code !== "ENOENT") fail("reference_file", "Cannot inspect --reference-file; check its path and permissions."); }
|
|
154
208
|
}
|
|
155
209
|
let reference;
|
|
156
|
-
if (sub === "set" && (o["reference-file"] || o["reference-env"])) {
|
|
210
|
+
if ((sub === "set" || removing) && (o["reference-file"] || o["reference-env"])) {
|
|
157
211
|
if (o.url || o.preset || o["key-file"] || o["key-env"] || o["key-stdin"] || o["hmac-secret-file"] || o["hmac-secret-env"]) fail("reference_inputs", "Use a reference without draft overrides; it saves exactly the uploaded settings.");
|
|
158
212
|
const next = "Run wake prepare --agent NAME with the draft inputs and a new --reference-file FILE, then use that protected reference with wake set. Existing files are never replaced.";
|
|
159
213
|
try { reference = secret(o, "reference", ""); }
|
|
@@ -167,6 +221,22 @@ export async function wake(sub, o = {}) {
|
|
|
167
221
|
const current = await request(origin, path, { token });
|
|
168
222
|
if (!Number.isSafeInteger(current.version)) fail("invalid_response", "The service did not return a valid wake version. Saved settings were not changed.");
|
|
169
223
|
if (sub === "preview") return { ok: true, code: "preview", next: "Test an explicit draft before saving.", version: current.version, preset: current.preset, configured: Boolean(current.url), key_header: ["hermes", "openbot"].includes(current.preset) ? "" : ["grok-bot", "claude-code", "openclaw-wake", "openclaw-agent"].includes(current.preset) ? "Authorization" : current.keyHeader || "Authorization", url: previewUrl(current.url) };
|
|
224
|
+
if (removing || preparingRemoval) {
|
|
225
|
+
const version = Number(o.version);
|
|
226
|
+
if (version !== current.version) fail("conflict", "Wake settings changed. Run wake preview --agent NAME --json, review them, then repeat removal with its explicit --version.");
|
|
227
|
+
const available = await capabilities({ "control-url": origin });
|
|
228
|
+
if (!available.wake_removal || (preparingRemoval && !available.secret_reference)) fail("unsupported", "This service does not support this removal operation. Use the agent's Wake settings on the website.");
|
|
229
|
+
if (preparingRemoval) {
|
|
230
|
+
const prepared = await request(origin, `${path}/reference`, { token, method: "POST", body: { version, clear: true } });
|
|
231
|
+
if (!/^a2awr_[a-f0-9]{64}$/.test(prepared.reference || "")) fail("invalid_response", "The service did not return a valid removal reference. No wake was removed.");
|
|
232
|
+
try { fs.writeFileSync(o["reference-file"], prepared.reference + "\n", { mode: 0o600, flag: "wx" }); }
|
|
233
|
+
catch { fail("reference_file", "Cannot create --reference-file. Choose a new path in a private directory; existing files are never replaced."); }
|
|
234
|
+
return { ok: true, code: "prepared", version, next: "Within ten minutes, use MCP set_wake with clear: true and secretReference, or wake remove --version N --reference-file FILE. The reference is single-use and never printed." };
|
|
235
|
+
}
|
|
236
|
+
const result = await request(origin, path, { token, method: "DELETE", body: { version, ...(reference ? { secretReference: reference } : {}) } });
|
|
237
|
+
const ok = result.ok === true && (result.removed === true || result.already_removed === true);
|
|
238
|
+
return { ok, code: ok ? result.already_removed === true ? "already_removed" : "removed" : "failed", removed: result.removed === true, already_removed: result.already_removed === true, version: result.version, next: ok ? "Use polling to read messages." : "Read wake preview --json before retrying removal with an explicit --version." };
|
|
239
|
+
}
|
|
170
240
|
if (reference) {
|
|
171
241
|
const result = await request(origin, path, { token, method: "PUT", body: { version: current.version, secretReference: reference } });
|
|
172
242
|
return { ok: result.ok === true && result.saved === true, code: result.saved === true ? "saved" : "failed", saved: result.saved === true, next: result.saved === true ? "Check the receiver for the test wake." : "The reference is spent. Check saved settings before preparing another draft." };
|
|
@@ -212,7 +282,7 @@ export async function output(run, o = {}) {
|
|
|
212
282
|
if (!result.ok) process.exitCode = 1;
|
|
213
283
|
} catch (error) {
|
|
214
284
|
const result = error.result || { ok: false, code: "unavailable", next: "Hosted setup did not finish. Check hosted list --json before retrying." };
|
|
215
|
-
console.log(o.json ? JSON.stringify(result) : `${result.code}: ${result.next}`);
|
|
285
|
+
console.log(o.json ? JSON.stringify(result) : `${result.code}: ${result.next}${result.message ? `\nmessage: ${result.message}` : ""}`);
|
|
216
286
|
process.exitCode = 1;
|
|
217
287
|
}
|
|
218
288
|
}
|
package/lib/main.mjs
CHANGED
|
@@ -31,7 +31,10 @@ Hosted setup:
|
|
|
31
31
|
login --machine NAME --scopes SCOPES [--control-url URL] --json
|
|
32
32
|
approve the explicit scopes using the printed website link once;
|
|
33
33
|
save the scoped grant locally (mode 600), never print it
|
|
34
|
-
hosted
|
|
34
|
+
hosted names --name NAME --json
|
|
35
|
+
check one name anonymously before device approval; never lists other agents
|
|
36
|
+
hosted capacity --json read account room with agents:read (or agents:create); reserves nothing
|
|
37
|
+
hosted list --json list hosted agents with agents:read or agents:create before choosing an agent
|
|
35
38
|
hosted create --name NAME --json
|
|
36
39
|
create and save the owner credential in this config directory;
|
|
37
40
|
use a separate --config-dir for each agent
|
|
@@ -45,7 +48,13 @@ Hosted setup:
|
|
|
45
48
|
wake set --agent NAME --reference-file FILE --json
|
|
46
49
|
test and save exactly that reference's draft; --reference-env VAR also works
|
|
47
50
|
MCP set_wake accepts secretReference on services listing that capability
|
|
48
|
-
Hosted
|
|
51
|
+
Hosted reference capacity failures report too_many_references and a retry wait; never auto-retry.
|
|
52
|
+
wake remove --agent NAME --version N --json
|
|
53
|
+
read wake preview --json first; remove only the version explicitly selected
|
|
54
|
+
wake unset is an alias; credentials remain in protected storage
|
|
55
|
+
wake prepare --agent NAME --clear --version N --reference-file FILE --json
|
|
56
|
+
prepare a removal reference for MCP set_wake clear: true, secretReference;
|
|
57
|
+
without that reference MCP keeps browser confirmation
|
|
49
58
|
Listings and previews show result fields; --json provides structured output.
|
|
50
59
|
|
|
51
60
|
Setup (needs Node 22.18+, e.g. \`mise exec node@22 -- npx -y a2a-exposed@latest ...\`, and a Cloudflare login:
|
|
@@ -379,7 +388,7 @@ const deployOpts = {
|
|
|
379
388
|
const SPEC = {
|
|
380
389
|
hosted: { name: S, json: B, agent: S, hostname: S, origin: S, path: S, preset: S, "control-url": S, "secret-file": S, "secret-env": S, "secret-from": S, "hermes-home": S, route: S, "token-file": S, "no-run": B },
|
|
381
390
|
init: { ...deployOpts, ...tunnelOpts, tunnel: B }, deploy: deployOpts,
|
|
382
|
-
wake: { ...deployOpts, json: B, url: S, "reference-file": S, "reference-env": S, agent: S, "control-url": S, "key-header": S, "key-file": S, "key-env": S, "key-stdin": B, "hmac-secret-file": S, "hmac-secret-env": S, "access-client-id-file": S, "access-client-id-env": S, "access-client-secret-file": S, "access-client-secret-env": S }, tunnel: tunnelOpts,
|
|
391
|
+
wake: { ...deployOpts, json: B, url: S, "reference-file": S, "reference-env": S, clear: B, version: S, agent: S, "control-url": S, "key-header": S, "key-file": S, "key-env": S, "key-stdin": B, "hmac-secret-file": S, "hmac-secret-env": S, "access-client-id-file": S, "access-client-id-env": S, "access-client-secret-file": S, "access-client-secret-env": S }, tunnel: tunnelOpts,
|
|
383
392
|
inbox: { context: S, from: S, all: B, json: B },
|
|
384
393
|
reply: { text: S, stdin: B, state: { type: "string", default: "completed" }, artifact: B, "artifact-name": { type: "string", default: "response" }, force: B },
|
|
385
394
|
history: { from: S, n: { type: "string", short: "n", default: "50" }, json: B },
|
|
@@ -502,7 +511,7 @@ export async function main(argv) {
|
|
|
502
511
|
throw new CliError(`unknown peer action ${sub} (announce|forward)`);
|
|
503
512
|
}
|
|
504
513
|
case "hosted": {
|
|
505
|
-
if (p.length === 1 && ["capabilities", "list", "create"].includes(p[0])) return hosted.output(() => hosted[p[0]](o), o);
|
|
514
|
+
if (p.length === 1 && ["capabilities", "list", "create", "names", "capacity"].includes(p[0])) return hosted.output(() => hosted[p[0]](o), o);
|
|
506
515
|
if (p.length !== 2 || p[0] !== "link" || !["finish", "create"].includes(p[1])) throw new CliError("usage: hosted link create --agent NAME --hostname HOST --origin URL --path PATH --preset ID (--secret-file FILE | --secret-env VAR | --secret-from hermes); or hosted link finish --agent NAME (--secret-file FILE | --secret-env VAR | --secret-from hermes)");
|
|
507
516
|
return (p[1] === "create" ? createLink : finishLink)({ ...o, "control-url": o["control-url"] || C.get("CONTROL_URL") });
|
|
508
517
|
}
|
package/package.json
CHANGED
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.33";
|