a2a-exposed 0.8.30 → 0.8.32
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/account.mjs +15 -1
- package/lib/config.mjs +6 -0
- package/lib/hosted.mjs +218 -0
- package/lib/layers.mjs +5 -3
- package/lib/main.mjs +39 -6
- package/lib/status.mjs +23 -4
- package/lib/tunnel.mjs +7 -0
- package/lib/uninstall.mjs +1 -0
- package/package.json +1 -1
- package/worker/src/version.ts +1 -1
package/lib/account.mjs
CHANGED
|
@@ -38,6 +38,8 @@ async function readJson(response) {
|
|
|
38
38
|
export async function deviceLogin({
|
|
39
39
|
controlUrl,
|
|
40
40
|
intent = "login",
|
|
41
|
+
scope = "",
|
|
42
|
+
machine = "",
|
|
41
43
|
fetchImpl = fetch,
|
|
42
44
|
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
43
45
|
write = (line) => console.error(line),
|
|
@@ -48,7 +50,8 @@ export async function deviceLogin({
|
|
|
48
50
|
const codeResponse = await fetchImpl(`${origin}/api/auth/device/code`, {
|
|
49
51
|
method: "POST",
|
|
50
52
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
51
|
-
|
|
53
|
+
redirect: "error", signal: AbortSignal.timeout(30_000),
|
|
54
|
+
body: JSON.stringify({ client_id: CLI_CLIENT_ID, ...(scope ? { scope, machine } : {}) }),
|
|
52
55
|
});
|
|
53
56
|
const started = await readJson(codeResponse);
|
|
54
57
|
if (!codeResponse.ok || !started.device_code || !started.user_code || !started.verification_uri_complete) {
|
|
@@ -57,7 +60,11 @@ export async function deviceLogin({
|
|
|
57
60
|
? `login is not turned on at ${origin}`
|
|
58
61
|
: `could not start sign-in at ${origin} (${reason})`);
|
|
59
62
|
}
|
|
63
|
+
let verification;
|
|
64
|
+
try { verification = new URL(started.verification_uri_complete); } catch { die("invalid sign-in approval URL"); }
|
|
65
|
+
if (verification.origin !== origin || verification.username || verification.password || !/^[A-Za-z0-9-]{4,32}$/.test(started.user_code)) die("invalid sign-in approval URL or comparison code");
|
|
60
66
|
const verb = intent === "signup" ? "Create an account and approve" : "Sign in and approve";
|
|
67
|
+
if (scope) write(`Hosted setup access\nMachine: ${machine}\nPermissions: ${scope.split(" ").join(", ")}`);
|
|
61
68
|
write(`${verb} this code:\n${started.user_code}\n${started.verification_uri_complete}`);
|
|
62
69
|
const interval = Math.max(1, Number(started.interval) || 5);
|
|
63
70
|
const deadline = Date.now() + Math.max(1, Number(started.expires_in) || 600) * 1000;
|
|
@@ -66,11 +73,18 @@ export async function deviceLogin({
|
|
|
66
73
|
await sleep(wait * 1000 * scale());
|
|
67
74
|
const tokenResponse = await fetchImpl(`${origin}/api/auth/device/token`, {
|
|
68
75
|
method: "POST",
|
|
76
|
+
redirect: "error", signal: AbortSignal.timeout(30_000),
|
|
69
77
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
70
78
|
body: JSON.stringify({ grant_type: DEVICE_GRANT, device_code: started.device_code, client_id: CLI_CLIENT_ID }),
|
|
71
79
|
});
|
|
72
80
|
const body = await readJson(tokenResponse);
|
|
73
81
|
if (tokenResponse.ok && typeof body.access_token === "string" && body.access_token) {
|
|
82
|
+
if (scope) {
|
|
83
|
+
if (!/^a2asg_[a-f0-9]{64}$/.test(body.access_token) || body.scope !== scope) die("invalid hosted setup grant response; start approval again");
|
|
84
|
+
save({ HOSTED_SETUP_URL: origin, HOSTED_SETUP_TOKEN: body.access_token, HOSTED_SETUP_SCOPES: scope, HOSTED_SETUP_MACHINE: machine });
|
|
85
|
+
write("Hosted setup approved. Credential saved privately.");
|
|
86
|
+
return { controlUrl: origin, email: "" };
|
|
87
|
+
}
|
|
74
88
|
save({ CONTROL_URL: origin, CONTROL_TOKEN: body.access_token });
|
|
75
89
|
const sessionResponse = await fetchImpl(`${origin}/api/auth/get-session`, {
|
|
76
90
|
headers: { accept: "application/json", authorization: `Bearer ${body.access_token}` },
|
package/lib/config.mjs
CHANGED
|
@@ -54,6 +54,12 @@ export function get(key, dflt = "") {
|
|
|
54
54
|
return f !== undefined && f !== "" ? f : dflt;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** Config type, without network or Cloudflare calls. Partial self-hosted metadata stays self-hosted. */
|
|
58
|
+
export function deploymentKind() {
|
|
59
|
+
if (get("A2A_WORKER_NAME") || get("A2A_D1_ID")) return "self-hosted";
|
|
60
|
+
return get("A2A_BASE_URL") && get("A2A_OWNER_TOKEN") ? "hosted" : "none";
|
|
61
|
+
}
|
|
62
|
+
|
|
57
63
|
/** Resolve a control credential with the origin from the same source.
|
|
58
64
|
* A partial environment override must never borrow saved credentials.
|
|
59
65
|
*/
|
package/lib/hosted.mjs
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Hosted setup credentials never leave their origin or appear in command output.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import * as C from "./config.mjs";
|
|
4
|
+
import { controlOrigin, deviceLogin } from "./account.mjs";
|
|
5
|
+
import { CliError, upstreamUrlProblem } from "./a2a.mjs";
|
|
6
|
+
|
|
7
|
+
export const SETUP_SCOPES = ["agents:create", "agents:owner-token", "wake:write", "wake:test"];
|
|
8
|
+
const NEXT = "Run hosted capabilities --json to list scopes and presets; login --machine NAME --scopes SCOPES to approve setup.";
|
|
9
|
+
const fail = (code, next) => { const error = new CliError(next); error.result = { ok: false, code, next }; throw error; };
|
|
10
|
+
const printable = value => typeof value === "string" && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value);
|
|
11
|
+
|
|
12
|
+
export function setupSession(env = process.env, saved = C.fileConfig()) {
|
|
13
|
+
const source = env.HOSTED_SETUP_URL || env.HOSTED_SETUP_TOKEN ? env : saved;
|
|
14
|
+
return { origin: controlOrigin(source.HOSTED_SETUP_URL), token: source.HOSTED_SETUP_TOKEN || "" };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function originFor(o = {}) {
|
|
18
|
+
const origin = controlOrigin(o["control-url"] || setupSession().origin || C.controlSession().origin || "https://a2a.exposed");
|
|
19
|
+
if (!origin) fail("control_url", "--control-url must be an HTTPS origin without a path or credentials.");
|
|
20
|
+
return origin;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function request(origin, path, { token = "", method = "GET", body, fetchImpl = fetch } = {}) {
|
|
24
|
+
let response, data;
|
|
25
|
+
try {
|
|
26
|
+
response = await fetchImpl(`${origin}${path}`, {
|
|
27
|
+
method, redirect: "error", signal: AbortSignal.timeout(30_000),
|
|
28
|
+
headers: { accept: "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}), ...(body === undefined ? {} : { "content-type": "application/json" }) },
|
|
29
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
30
|
+
});
|
|
31
|
+
data = await response.json();
|
|
32
|
+
} 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
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) fail("invalid_response", "The hosted service returned an invalid setup response.");
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
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"];
|
|
36
|
+
const code = known.includes(data.error) ? data.error : `http_${response.status}`;
|
|
37
|
+
const next = code === "taken" || code === "reserved_name" ? "Choose another --name and run hosted list --json before creating."
|
|
38
|
+
: code === "limit" ? "The account is at its agent limit. Review hosted list --json and account plans."
|
|
39
|
+
: code === "conflict" ? "Wake settings changed. Run wake preview --json, then test the draft again."
|
|
40
|
+
: 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
|
+
: code === "invalid_grant" || code === "insufficient_scope" ? NEXT
|
|
42
|
+
: "Hosted setup refused the request. Run hosted capabilities --json and check the inputs; no automatic retry was made.";
|
|
43
|
+
fail(code, next);
|
|
44
|
+
}
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function capabilities(o = {}) {
|
|
49
|
+
const data = await request(originFor(o), "/api/v1/setup/capabilities");
|
|
50
|
+
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 };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function login(o = {}) {
|
|
55
|
+
const origin = originFor(o), machine = o.machine || "";
|
|
56
|
+
if (!machine.trim() || machine.length > 80 || !printable(machine)) fail("machine", "Pass --machine NAME (1–80 printable characters). " + NEXT);
|
|
57
|
+
const scopes = [...new Set(String(o.scopes || "").split(/[ ,]+/).filter(Boolean))];
|
|
58
|
+
if (!scopes.length || scopes.some(s => !SETUP_SCOPES.includes(s))) fail("scope", "Pass --scopes with choices from hosted capabilities --json.");
|
|
59
|
+
const available = await capabilities({ "control-url": origin });
|
|
60
|
+
if (scopes.some(s => !available.scopes.includes(s))) fail("unsupported_scope", NEXT);
|
|
61
|
+
await deviceLogin({ controlUrl: origin, scope: scopes.join(" "), machine: machine.trim(), save: updates => C.saveConfig(updates) });
|
|
62
|
+
return { ok: true, code: "approved", next: "Run hosted list --json, then hosted create --name NAME --json.", control_url: origin };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function accountCredential(o) {
|
|
66
|
+
const session = setupSession(), origin = originFor(o);
|
|
67
|
+
if (!session.token || session.origin !== origin) fail("login_required", NEXT);
|
|
68
|
+
return { origin, token: session.token };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function list(o = {}) {
|
|
72
|
+
const { origin, token } = accountCredential(o);
|
|
73
|
+
const data = await request(origin, "/api/v1/tenants", { token });
|
|
74
|
+
if (!Array.isArray(data.tenants)) fail("invalid_response", "The service did not return an agent listing.");
|
|
75
|
+
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
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function create(o = {}) {
|
|
79
|
+
const name = String(o.name || "").trim().toLowerCase();
|
|
80
|
+
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
|
+
// Never replace an existing deployment or an environment-selected credential.
|
|
82
|
+
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
|
+
const { origin, token } = accountCredential(o);
|
|
84
|
+
const before = await list(o);
|
|
85
|
+
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
|
+
const data = await request(origin, "/api/v1/tenants", { token, method: "POST", body: { name } });
|
|
87
|
+
const base = data.base_url || data.public_url;
|
|
88
|
+
if (data.name !== name || !/^a2aot_[A-Za-z0-9_-]{43}$/.test(data.owner_token || "") || !controlOrigin(base)) fail("invalid_response", "Creation returned an invalid credential response. Check hosted list --json before retrying; the agent may already exist.");
|
|
89
|
+
try { C.saveConfig({ A2A_BASE_URL: base, A2A_OWNER_TOKEN: data.owner_token, HOSTED_AGENT_NAME: name, HOSTED_SETUP_URL: origin }); }
|
|
90
|
+
catch { fail("credential_save", "The agent was created, but its one-time credential could not be saved. Do not create it again. Use the agent's account page to recover access."); }
|
|
91
|
+
return { ok: true, code: "created", next: "Run wake set with an explicit URL and preset, reading credentials from protected files or named environment variables.", name, base_url: base };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function secret(o, name, envName) {
|
|
95
|
+
const file = o[`${name}-file`], env = o[`${name}-env`], stdin = name === "key" && o["key-stdin"];
|
|
96
|
+
if ([file, env, stdin].filter(Boolean).length > 1) fail("secret_source", `Choose one --${name}-file or --${name}-env${name === "key" ? " or --key-stdin" : ""}.`);
|
|
97
|
+
let value = "";
|
|
98
|
+
try {
|
|
99
|
+
if (file) {
|
|
100
|
+
const stat = fs.lstatSync(file);
|
|
101
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) || stat.size > 4098) fail("secret_file", `--${name}-file needs a regular protected file (chmod 600), at most 4098 bytes.`);
|
|
102
|
+
value = fs.readFileSync(file, "utf8").replace(/\r?\n$/, "");
|
|
103
|
+
} else if (stdin) {
|
|
104
|
+
if (process.stdin.isTTY) fail("secret_stdin", "Pipe the key on stdin; terminal prompts are not supported.");
|
|
105
|
+
value = fs.readFileSync(0, "utf8").replace(/\r?\n$/, "");
|
|
106
|
+
} else value = process.env[env || envName] || "";
|
|
107
|
+
} catch (error) { if (error.result) throw error; fail("secret_file", `Could not read --${name}-file; check its path and permissions.`); }
|
|
108
|
+
if (!printable(value) || ((file || env || stdin) && !value)) fail("secret_input", `The ${name} credential must contain 1–4096 printable characters.`);
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function wakeCredential(o, preferOwner = false) {
|
|
113
|
+
const origin = originFor(o), session = setupSession();
|
|
114
|
+
let name = o.agent || C.get("HOSTED_AGENT_NAME");
|
|
115
|
+
// Deployment halves come from one source, independently of setup selection.
|
|
116
|
+
const saved = C.fileConfig();
|
|
117
|
+
const source = process.env.A2A_BASE_URL || process.env.A2A_OWNER_TOKEN ? process.env : saved;
|
|
118
|
+
const owner = source.A2A_OWNER_TOKEN || "", base = source.A2A_BASE_URL || "";
|
|
119
|
+
if (!name && base) { try { name = new URL(base).hostname.split(".")[0]; } catch {} }
|
|
120
|
+
if (!/^[a-z0-9][a-z0-9-]{2,30}[a-z0-9]$/.test(name || "")) fail("agent", "Pass --agent NAME selected from hosted list --json.");
|
|
121
|
+
// An explicit agent must not borrow a different agent's owner token.
|
|
122
|
+
let ownerOrigin = "";
|
|
123
|
+
if (controlOrigin(base)) {
|
|
124
|
+
const parsed = new URL(base);
|
|
125
|
+
if (parsed.hostname.startsWith(`${name}.`)) ownerOrigin = controlOrigin(`${parsed.protocol}//${parsed.host.slice(name.length + 1)}`);
|
|
126
|
+
}
|
|
127
|
+
const ownerMatches = owner && base && ownerOrigin === origin && controlOrigin(base) && new URL(base).hostname.split(".")[0] === name;
|
|
128
|
+
const token = preferOwner && ownerMatches ? owner : session.origin === origin && session.token ? session.token : ownerMatches ? owner : "";
|
|
129
|
+
if (!token) fail("login_required", NEXT);
|
|
130
|
+
return { origin, token, name };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// A webhook path or query may contain its credential. Show only the public destination.
|
|
134
|
+
function previewUrl(value) {
|
|
135
|
+
try { const url = new URL(value); return url.protocol === "https:" && !url.username && !url.password ? `${url.origin}${url.pathname === "/" ? "/" : "/…"}` : ""; }
|
|
136
|
+
catch { return ""; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function status(o = {}) {
|
|
140
|
+
if (!/^[a-z0-9][a-z0-9-]{2,30}[a-z0-9]$/.test(o.agent || "")) fail("agent", "Pass --agent NAME selected from hosted list --json.");
|
|
141
|
+
const preview = await wake("preview", o);
|
|
142
|
+
return { ...preview, code: "status", agent: o.agent, next: preview.configured
|
|
143
|
+
? "Wake settings are configured. Run wake test --agent NAME with explicit draft inputs to check delivery."
|
|
144
|
+
: "No webhook is configured. Select a preset from hosted capabilities --json, then prepare a wake draft, or use polling." };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function wake(sub, o = {}) {
|
|
148
|
+
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
|
+
if (sub === "remove" || sub === "unset") fail("unsupported", "This hosted API does not support wake removal yet. Remove it in the agent's Wake settings; saved settings were not changed.");
|
|
150
|
+
if (sub === "prepare") {
|
|
151
|
+
if (!o["reference-file"]) fail("reference_file", "Pass a new --reference-file FILE. The reference is saved mode 600 and never printed.");
|
|
152
|
+
try { fs.lstatSync(o["reference-file"]); fail("reference_file", "Choose a new --reference-file path; existing files are never replaced."); }
|
|
153
|
+
catch (error) { if (error.result) throw error; if (error.code !== "ENOENT") fail("reference_file", "Cannot inspect --reference-file; check its path and permissions."); }
|
|
154
|
+
}
|
|
155
|
+
let reference;
|
|
156
|
+
if (sub === "set" && (o["reference-file"] || o["reference-env"])) {
|
|
157
|
+
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
|
+
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
|
+
try { reference = secret(o, "reference", ""); }
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (error.result?.code === "secret_input") fail("invalid_reference", next);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
if (!/^a2awr_[a-f0-9]{64}$/.test(reference)) fail("invalid_reference", next);
|
|
165
|
+
}
|
|
166
|
+
const { origin, token, name } = wakeCredential(o, sub !== "prepare" && !o["reference-file"] && !o["reference-env"]), path = `/api/v1/agents/${encodeURIComponent(name)}/wake`;
|
|
167
|
+
const current = await request(origin, path, { token });
|
|
168
|
+
if (!Number.isSafeInteger(current.version)) fail("invalid_response", "The service did not return a valid wake version. Saved settings were not changed.");
|
|
169
|
+
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) };
|
|
170
|
+
if (reference) {
|
|
171
|
+
const result = await request(origin, path, { token, method: "PUT", body: { version: current.version, secretReference: reference } });
|
|
172
|
+
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." };
|
|
173
|
+
}
|
|
174
|
+
const url = o.url || process.env.WAKE_WEBHOOK_URL || current.url;
|
|
175
|
+
const preset = o.preset || current.preset || "generic";
|
|
176
|
+
const problem = upstreamUrlProblem(url);
|
|
177
|
+
if (problem) fail("url", "Pass --url with a public HTTPS webhook URL without credentials. Use polling if no public webhook is available.");
|
|
178
|
+
const available = await capabilities({ "control-url": origin });
|
|
179
|
+
if (!available.presets.includes(preset)) fail("preset", "Select --preset from hosted capabilities --json.");
|
|
180
|
+
const draft = { version: current.version, url, preset, keyHeader: o["key-header"] ?? current.keyHeader ?? "", key: secret(o, "key", "WAKE_WEBHOOK_KEY"), hmacSecret: secret(o, "hmac-secret", "WAKE_HMAC_SECRET"), accessClientId: secret(o, "access-client-id", "WAKE_ACCESS_CLIENT_ID"), accessClientSecret: secret(o, "access-client-secret", "WAKE_ACCESS_CLIENT_SECRET") };
|
|
181
|
+
if (Boolean(draft.accessClientId) !== Boolean(draft.accessClientSecret)) fail("access", "Supply both Access credentials through protected files or named environment variables.");
|
|
182
|
+
let body = draft;
|
|
183
|
+
if (sub === "prepare" || (sub === "set" && available.secret_reference && token.startsWith("a2asg_"))) {
|
|
184
|
+
if (!available.secret_reference) fail("unsupported", "This service does not support wake references. Use wake set or browser confirmation.");
|
|
185
|
+
if (sub === "prepare" && !o["reference-file"]) fail("reference_file", "Pass a new --reference-file FILE. The reference is saved mode 600 and never printed.");
|
|
186
|
+
const prepared = await request(origin, `${path}/reference`, { token, method: "POST", body: draft });
|
|
187
|
+
if (!/^a2awr_[a-f0-9]{64}$/.test(prepared.reference || "")) fail("invalid_response", "The service did not return a valid reference. No wake was saved.");
|
|
188
|
+
if (sub === "prepare") {
|
|
189
|
+
try { fs.writeFileSync(o["reference-file"], prepared.reference + "\n", { mode: 0o600, flag: "wx" }); }
|
|
190
|
+
catch { fail("reference_file", "Cannot create --reference-file. Choose a new path in a private directory; existing files are never replaced."); }
|
|
191
|
+
return { ok: true, code: "prepared", next: "Use the protected reference with wake set --reference-file FILE, or MCP set_wake secretReference and the same draft settings, within ten minutes. It is single-use even when a test fails." };
|
|
192
|
+
}
|
|
193
|
+
body = { version: current.version, secretReference: prepared.reference };
|
|
194
|
+
}
|
|
195
|
+
const result = await request(origin, sub === "test" ? `${path}/test` : path, { token, method: sub === "test" ? "POST" : "PUT", body });
|
|
196
|
+
// Do not print proofs, arbitrary service text, webhook URLs or unexpected fields.
|
|
197
|
+
const code = typeof result.code === "string" && /^(?:[1-5][0-9]{2}|access-40[13]|none|timeout|tls|private|dns|dns-missing|transport-delivery|transport-native|transport-socket|cloudflare-target-unsupported|connection|failed|unreadable|signing)$/.test(result.code) ? result.code : result.saved === true ? "saved" : "failed";
|
|
198
|
+
const ok = result.ok === true && (sub !== "set" || result.saved === true);
|
|
199
|
+
return { ok, code, saved: result.saved === true, next: ok ? sub === "set" ? "Check the receiver for the test wake." : "The draft passed. Run wake set with the same inputs to test and save." : code === "cloudflare-target-unsupported" ? "Use polling or a self-hosted Worker for this Cloudflare-proxied webhook." : "Saved settings were not changed. Check the receiver, credentials and draft, then test again." };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function output(run, o = {}) {
|
|
203
|
+
try {
|
|
204
|
+
const result = await run();
|
|
205
|
+
if (o.json) console.log(JSON.stringify(result));
|
|
206
|
+
else {
|
|
207
|
+
console.log(`${result.code}: ${result.next}`);
|
|
208
|
+
for (const [key, value] of Object.entries(result)) {
|
|
209
|
+
if (!["ok", "code", "next"].includes(key)) console.log(`${key}: ${JSON.stringify(value, null, 2)}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!result.ok) process.exitCode = 1;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
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}`);
|
|
216
|
+
process.exitCode = 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
package/lib/layers.mjs
CHANGED
|
@@ -61,15 +61,16 @@ function workerDnsLayer(f) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function workerLayer(f) {
|
|
64
|
+
const fix = f.deployment === "hosted" ? "check the hosted service and saved A2A_BASE_URL; run status again" : `${CLI} deploy`;
|
|
64
65
|
if (!f.baseUrl) return layer("Worker", "SKIP", "not checked (no base URL)");
|
|
65
66
|
const https = /^https:/i.test(f.baseUrl);
|
|
66
67
|
const card = f.card || { ok: false, error: "not checked" };
|
|
67
68
|
if (https && card.error && TLS_RE.test(card.error))
|
|
68
69
|
return layer("Worker", "FAIL", `Likely mismatched: TLS certificate for ${f.host}. ${card.error}`, `check the certificate for ${f.host} in Cloudflare (SSL/TLS)`);
|
|
69
70
|
if (!card.ok)
|
|
70
|
-
return layer("Worker", "FAIL", `Likely missing: deployed Worker. Agent card: ${card.error || "unreachable"}`,
|
|
71
|
+
return layer("Worker", "FAIL", `Likely missing: deployed Worker. Agent card: ${card.error || "unreachable"}`, fix);
|
|
71
72
|
if (card.urlProblem)
|
|
72
|
-
return layer("Worker", "FAIL", `Likely mismatched: agent card URL. ${card.urlProblem}`,
|
|
73
|
+
return layer("Worker", "FAIL", `Likely mismatched: agent card URL. ${card.urlProblem}`, fix);
|
|
73
74
|
const tls = https ? "TLS ok" : "TLS skipped (not https)";
|
|
74
75
|
const named = card.name ? ` ("${card.name}")` : "";
|
|
75
76
|
const cardBit = `${tls}; agent card HTTP ${card.httpStatus || 200}${named}`;
|
|
@@ -80,7 +81,7 @@ function workerLayer(f) {
|
|
|
80
81
|
: h.httpStatus < 200 || h.httpStatus >= 300
|
|
81
82
|
? `/health HTTP ${h.httpStatus}`
|
|
82
83
|
: `/health HTTP ${h.httpStatus} did not report ok`;
|
|
83
|
-
return layer("Worker", "FAIL", `Likely missing: Worker /health. ${cardBit}; ${problem}`,
|
|
84
|
+
return layer("Worker", "FAIL", `Likely missing: Worker /health. ${cardBit}; ${problem}`, fix);
|
|
84
85
|
}
|
|
85
86
|
return layer("Worker", "PASS", `${cardBit}; /health HTTP ${h.httpStatus || 200}`);
|
|
86
87
|
}
|
|
@@ -91,6 +92,7 @@ function ownerLayer(f) {
|
|
|
91
92
|
if (f.ownerApi && f.ownerApi.ok) return layer("Owner API", "PASS", "owner token accepted");
|
|
92
93
|
const err = (f.ownerApi && f.ownerApi.error) || "not checked";
|
|
93
94
|
if (err === "not checked") return layer("Owner API", "SKIP", "not checked");
|
|
95
|
+
if (f.deployment === "hosted") return layer("Owner API", "FAIL", `hosted owner credential check failed: ${err}`, "check hosted agent access with its owner");
|
|
94
96
|
if (/HTTP 401/.test(err)) return layer("Owner API", "FAIL", "Likely mismatched: owner token", `${CLI} init --rotate-owner-token`);
|
|
95
97
|
return layer("Owner API", "FAIL", `Likely missing: deployed Worker. ${err}`, `${CLI} deploy`);
|
|
96
98
|
}
|
package/lib/main.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import * as st from "./status.mjs";
|
|
|
9
9
|
import * as pair from "./pair.mjs";
|
|
10
10
|
import * as address from "./address.mjs";
|
|
11
11
|
import * as account from "./account.mjs";
|
|
12
|
+
import * as hosted from "./hosted.mjs";
|
|
12
13
|
import * as upd from "./update.mjs";
|
|
13
14
|
import * as fb from "./feedback.mjs";
|
|
14
15
|
import * as un from "./uninstall.mjs";
|
|
@@ -25,6 +26,28 @@ Usage: npx -y a2a-exposed@latest <command> [options]
|
|
|
25
26
|
|
|
26
27
|
Setup options: --skip-install skips dependency installation when dependencies already exist.
|
|
27
28
|
|
|
29
|
+
Hosted setup:
|
|
30
|
+
hosted capabilities --json list setup scopes, wake presets and service capabilities
|
|
31
|
+
login --machine NAME --scopes SCOPES [--control-url URL] --json
|
|
32
|
+
approve the explicit scopes using the printed website link once;
|
|
33
|
+
save the scoped grant locally (mode 600), never print it
|
|
34
|
+
hosted list --json list hosted agents before choosing --name or --agent
|
|
35
|
+
hosted create --name NAME --json
|
|
36
|
+
create and save the owner credential in this config directory;
|
|
37
|
+
use a separate --config-dir for each agent
|
|
38
|
+
wake preview|test|set --agent NAME [--url URL] [--preset P] --json
|
|
39
|
+
hosted settings: read, test a draft, or test then save it
|
|
40
|
+
secrets: --key-file, --key-env, --key-stdin, --hmac-secret-file, --hmac-secret-env,
|
|
41
|
+
--access-client-id-file, --access-client-id-env and --access-client-secret-file, --access-client-secret-env
|
|
42
|
+
wake prepare --agent NAME --url URL --preset P --reference-file FILE --json
|
|
43
|
+
when the service lists secret_reference, upload the draft and save a
|
|
44
|
+
ten-minute single-use reference in a new protected file; never print it
|
|
45
|
+
wake set --agent NAME --reference-file FILE --json
|
|
46
|
+
test and save exactly that reference's draft; --reference-env VAR also works
|
|
47
|
+
MCP set_wake accepts secretReference on services listing that capability
|
|
48
|
+
Hosted wake removal uses the agent's Wake settings on the website.
|
|
49
|
+
Listings and previews show result fields; --json provides structured output.
|
|
50
|
+
|
|
28
51
|
Setup (needs Node 22.18+, e.g. \`mise exec node@22 -- npx -y a2a-exposed@latest ...\`, and a Cloudflare login:
|
|
29
52
|
npx cf auth login --no-browser; on a VPS where it fails with HTTP 403 before showing a code, export
|
|
30
53
|
CLOUDFLARE_API_TOKEN instead: setup skill, Troubleshooting. a2a.exposed hosts the A2A endpoint and is open to everyone
|
|
@@ -155,6 +178,10 @@ Remove
|
|
|
155
178
|
--dir and --worker-dir are the Worker project directory (default <config dir>/worker)
|
|
156
179
|
|
|
157
180
|
Status
|
|
181
|
+
status --agent NAME [--control-url URL] [--json]
|
|
182
|
+
read selected hosted agent wake configuration; select NAME with hosted list --json.
|
|
183
|
+
Uses setup approval or that agent's matching owner credential; no delivery test.
|
|
184
|
+
Hosted-only configs report deployment: hosted and hosted recovery guidance.
|
|
158
185
|
status [--json] deployment, Worker version, base URL, agent card check (fetched by the CLI), wake
|
|
159
186
|
mode (webhook / tunnel / none = polling), tunnel state, proxy mode (--upstream):
|
|
160
187
|
upstream card, Access credential and upstream bearer configured (separately), the
|
|
@@ -308,6 +335,8 @@ Update notice: at most a daily background check of the npm registry; off with A2
|
|
|
308
335
|
|
|
309
336
|
const HELP_TUNNEL = `tunnel create|status|rm
|
|
310
337
|
|
|
338
|
+
Self-hosted only. Hosted configs use hosted webhook settings or polling. Check status --json first.
|
|
339
|
+
|
|
311
340
|
tunnel create [--tunnel-zone Z | --tunnel-hostname H] [--tunnel-origin URL] [--tunnel-path /p]
|
|
312
341
|
[--show-token] [--zero-trust-org TEAM] [--dir DIR | --worker-dir DIR]
|
|
313
342
|
Named Cloudflare Tunnel, proxied DNS, and an Access app that admits one service token.
|
|
@@ -327,6 +356,7 @@ const HELP_TUNNEL = `tunnel create|status|rm
|
|
|
327
356
|
|
|
328
357
|
const HELP_UNINSTALL = `uninstall [--yes] [--dry-run] [--dir DIR | --worker-dir DIR]
|
|
329
358
|
|
|
359
|
+
Self-hosted only. Hosted agents are deleted through hosted settings and confirmation.
|
|
330
360
|
Remove the Worker, the wake tunnel when the config has one, and the D1 database.
|
|
331
361
|
Prints the plan first: Worker name, D1 name and id, account, and the wake hostname
|
|
332
362
|
when a tunnel is saved. Removal is permanent and takes the messages and paired peers with it.
|
|
@@ -347,8 +377,9 @@ const deployOpts = {
|
|
|
347
377
|
"workers-dev": B, "workers-dev-subdomain": S, "upstream-token-stdin": B, "no-upstream-token": B, "event-secret-stdin": B,
|
|
348
378
|
};
|
|
349
379
|
const SPEC = {
|
|
350
|
-
hosted: { 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 },
|
|
351
|
-
init: { ...deployOpts, ...tunnelOpts, tunnel: B }, deploy: deployOpts,
|
|
380
|
+
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
|
+
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,
|
|
352
383
|
inbox: { context: S, from: S, all: B, json: B },
|
|
353
384
|
reply: { text: S, stdin: B, state: { type: "string", default: "completed" }, artifact: B, "artifact-name": { type: "string", default: "response" }, force: B },
|
|
354
385
|
history: { from: S, n: { type: "string", short: "n", default: "50" }, json: B },
|
|
@@ -357,13 +388,13 @@ const SPEC = {
|
|
|
357
388
|
peer: { to: S, all: B, reissue: B, hours: S, off: B, json: B },
|
|
358
389
|
send: { to: S, text: S, context: S, task: S, push: B, proto: S },
|
|
359
390
|
poll: { to: S, proto: S },
|
|
360
|
-
status: { json: B },
|
|
391
|
+
status: { json: B, agent: S, "control-url": S },
|
|
361
392
|
export: { sql: B },
|
|
362
393
|
upstream: { json: B },
|
|
363
394
|
connect: { alias: S, name: S, json: B, "no-wait": B, replace: B, force: B, "card-url": S },
|
|
364
395
|
// Internal hosted-service integration commands. Keep out of public help until the production flow is available.
|
|
365
396
|
signup: { "control-url": S, json: B },
|
|
366
|
-
login: { "control-url": S, json: B },
|
|
397
|
+
login: { "control-url": S, json: B, machine: S, scopes: S },
|
|
367
398
|
pair: { json: B, web: B, ttl: S, issuer: S, "client-id": S, subjects: S, methods: S, "secret-stdin": B },
|
|
368
399
|
feedback: { kind: S, contact: S, steps: S, expected: S, actual: S, logs: S, agent: S, environment: S, connection: S, yes: B, json: B },
|
|
369
400
|
uninstall: { yes: B, "dry-run": B, dir: S, "worker-dir": S },
|
|
@@ -434,13 +465,13 @@ export async function main(argv) {
|
|
|
434
465
|
if (p.length) throw new CliError(`uninstall takes no arguments (${p.join(" ")}). Run \`${CLI} uninstall --help\`.`);
|
|
435
466
|
return un.uninstall(o);
|
|
436
467
|
case "deploy": return dep.deploy(o);
|
|
437
|
-
case "status": return st.status(o);
|
|
468
|
+
case "status": return o.agent !== undefined || o["control-url"] !== undefined ? hosted.output(() => hosted.status(o), o) : st.status(o);
|
|
438
469
|
case "upstream": {
|
|
439
470
|
const sub = need(p[0], "upstream verify [--json]");
|
|
440
471
|
if (sub === "verify" || sub === "check") return st.upstreamVerify(o);
|
|
441
472
|
throw new CliError(`unknown upstream action ${sub} (verify)`);
|
|
442
473
|
}
|
|
443
|
-
case "wake": return dep.wake(p[0], o);
|
|
474
|
+
case "wake": return C.deploymentKind() === "hosted" || o.agent ? hosted.output(() => hosted.wake(p[0], o), o) : dep.wake(p[0], o);
|
|
444
475
|
case "url": {
|
|
445
476
|
const u = cmd.baseUrl();
|
|
446
477
|
if (!u) throw new CliError(`no base URL configured in ${C.CONFIG_FILE}: run \`${CLI} init\` (or pass --config-dir / A2A_CONFIG_DIR for another deployment)`);
|
|
@@ -471,11 +502,13 @@ export async function main(argv) {
|
|
|
471
502
|
throw new CliError(`unknown peer action ${sub} (announce|forward)`);
|
|
472
503
|
}
|
|
473
504
|
case "hosted": {
|
|
505
|
+
if (p.length === 1 && ["capabilities", "list", "create"].includes(p[0])) return hosted.output(() => hosted[p[0]](o), o);
|
|
474
506
|
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)");
|
|
475
507
|
return (p[1] === "create" ? createLink : finishLink)({ ...o, "control-url": o["control-url"] || C.get("CONTROL_URL") });
|
|
476
508
|
}
|
|
477
509
|
case "signup":
|
|
478
510
|
case "login": {
|
|
511
|
+
if (o.machine || o.scopes) return hosted.output(() => hosted.login(o), o);
|
|
479
512
|
const result = await account.deviceLogin({ controlUrl: o["control-url"] || C.get("CONTROL_URL"), intent: name });
|
|
480
513
|
if (o.json) console.log(JSON.stringify({ control_url: result.controlUrl, email: result.email }));
|
|
481
514
|
return;
|
package/lib/status.mjs
CHANGED
|
@@ -275,7 +275,7 @@ export function secretsPropagating(tunnel, now = Date.now()) {
|
|
|
275
275
|
export function nextStep(s) {
|
|
276
276
|
const n = nextStepCore(s);
|
|
277
277
|
const version = s.cliVersion || D.VERSION;
|
|
278
|
-
if (s.workerVersion && s.latestVersion && newer(s.latestVersion, s.workerVersion) && !newer(s.workerVersion, version))
|
|
278
|
+
if (s.deployment !== "hosted" && s.workerVersion && s.latestVersion && newer(s.latestVersion, s.workerVersion) && !newer(s.workerVersion, version))
|
|
279
279
|
n.also.push(`a2a-exposed ${s.latestVersion} is available: run npx -y a2a-exposed@latest deploy`);
|
|
280
280
|
const sh = (s.peerTokenShadowed || []).map((k) => `${k} is set in the environment and overrides the different peer token saved in ${s.configFile} (e.g. a stale value from before \`connect\` stored a new one): send/poll use the environment value. Unset ${k} where it is set to use the saved token`);
|
|
281
281
|
const addr = s.addresses;
|
|
@@ -289,6 +289,16 @@ export function nextStep(s) {
|
|
|
289
289
|
function nextStepCore(s) {
|
|
290
290
|
const fail = (text) => ({ ok: false, text, also: [] });
|
|
291
291
|
const done = (text, also = []) => ({ ok: true, text, also });
|
|
292
|
+
if (s.deployment === "hosted") {
|
|
293
|
+
if (!s.card.ok) return fail(`the hosted agent card is not reachable (${s.card.error}). Run \`${CLI} status\` again; if it still fails, check the hosted service and the saved A2A_BASE_URL.`);
|
|
294
|
+
if (s.card.urlProblem) return fail(`the hosted agent card has a different endpoint: ${s.card.urlProblem}. Check the address with the hosted service.`);
|
|
295
|
+
if (!s.health?.ok) return fail("the hosted health check failed. Check the hosted service and run status again.");
|
|
296
|
+
if (!s.ownerApi.ok) return fail("the hosted owner API refused or could not check the saved credential. Check the hosted agent access with its owner.");
|
|
297
|
+
if (!s.wake) return done("the hosted endpoint is reachable and its owner credential is accepted. Wake preview is unavailable; check wake delivery through the hosted service.");
|
|
298
|
+
return done(s.wake?.configured
|
|
299
|
+
? "none: the hosted endpoint is reachable and its owner credential is accepted. Check wake delivery through the hosted service."
|
|
300
|
+
: "the hosted endpoint is reachable. Configure a public HTTPS webhook through the hosted service, or use polling. Cloudflare Tunnel delivery may be unavailable; do not provision a self-hosted tunnel for this address.");
|
|
301
|
+
}
|
|
292
302
|
if (!s.deployed) return fail(`nothing is deployed from ${path.dirname(s.configFile)}. Run \`${CLI} init\` (see the quick start). Another deployment? Set A2A_CONFIG_DIR.`);
|
|
293
303
|
if (s.baseUrlEnv)
|
|
294
304
|
return fail(`A2A_BASE_URL is exported as ${s.baseUrlEnv}, but ${s.configFile} has ${s.baseUrlSaved || "(none)"}: CLI commands (and push URLs sent to peers) use the exported value. Unset A2A_BASE_URL (the A2A endpoint URL is always the deployment's own)`);
|
|
@@ -392,7 +402,7 @@ function hostOf(url) {
|
|
|
392
402
|
function layerFacts(s, extra) {
|
|
393
403
|
const t = s.tunnel;
|
|
394
404
|
return {
|
|
395
|
-
deployed: s.deployed, baseUrl: s.baseUrl || "", host: extra.host || "", platform: process.platform,
|
|
405
|
+
deployed: s.deployed, deployment: s.deployment, baseUrl: s.baseUrl || "", host: extra.host || "", platform: process.platform,
|
|
396
406
|
workerDns: extra.workerDns, card: s.card, health: s.health, hasOwnerToken: s.hasOwnerToken, ownerApi: s.ownerApi,
|
|
397
407
|
wakeHost: extra.wakeHost || "", wakeDns: extra.wakeDns, bare: extra.bare, authed: extra.authed,
|
|
398
408
|
serviceToken: extra.serviceToken, localServiceToken: extra.localServiceToken, pairing: s.pairing,
|
|
@@ -445,6 +455,7 @@ function finishLayers(s, extra) {
|
|
|
445
455
|
export async function collect(net = {}) {
|
|
446
456
|
const s = {
|
|
447
457
|
configFile: C.CONFIG_FILE,
|
|
458
|
+
deployment: C.deploymentKind(),
|
|
448
459
|
deployed: !!(C.get("A2A_D1_ID") && C.get("A2A_WORKER_NAME")),
|
|
449
460
|
worker: C.get("A2A_WORKER_NAME"), d1: C.get("A2A_D1_NAME"), account: C.get("CLOUDFLARE_ACCOUNT_ID"), cfProfile: C.get("CF_PROFILE"),
|
|
450
461
|
baseUrl: baseUrl(), urlKind: C.get("A2A_HOSTNAME") ? "custom domain" : "workers.dev", cron: C.get("A2A_ENABLE_CRON") === "1",
|
|
@@ -455,6 +466,7 @@ export async function collect(net = {}) {
|
|
|
455
466
|
peerTokenShadowed: shadowedPeerTokens(), addresses: null,
|
|
456
467
|
mcp: "unknown", health: null, workerVersion: null, cliVersion: D.VERSION, latestVersion: readCache(cacheFile())?.latest || null,
|
|
457
468
|
};
|
|
469
|
+
if (s.deployment === "hosted") { s.deployed = true; s.urlKind = "hosted"; }
|
|
458
470
|
const envBase = (process.env.A2A_BASE_URL || "").replace(/\/$/, "");
|
|
459
471
|
if (envBase && envBase !== s.baseUrlSaved) s.baseUrlEnv = envBase;
|
|
460
472
|
if (!s.deployed || !s.baseUrl) return finishLayers(s, { host: "", workerDns: { kind: "skip" }, wakeHost: "", wakeDns: { kind: "skip" }, bare: null, authed: null });
|
|
@@ -470,7 +482,13 @@ export async function collect(net = {}) {
|
|
|
470
482
|
s.ownerApi = { ok: true };
|
|
471
483
|
s.wake = { preset: p.preset, configured: !!p.configured, hasKey: !!p.hasKey, hasHmacSecret: !!p.hasHmacSecret,
|
|
472
484
|
hasAccessServiceToken: !!p.hasAccessServiceToken, urlFingerprint: p.fingerprints?.url || null };
|
|
473
|
-
} catch (e) {
|
|
485
|
+
} catch (e) {
|
|
486
|
+
s.ownerApi = { ok: false, error: e.message };
|
|
487
|
+
if (s.deployment === "hosted" && /HTTP (404|405)\b/.test(e.message)) {
|
|
488
|
+
try { await owner("GET", "/owner/summary"); s.ownerApi = { ok: true }; }
|
|
489
|
+
catch (summaryError) { s.ownerApi = { ok: false, error: summaryError.message }; }
|
|
490
|
+
}
|
|
491
|
+
}
|
|
474
492
|
if (s.ownerApi.ok) {
|
|
475
493
|
try {
|
|
476
494
|
const pr = await owner("GET", "/owner/pairing");
|
|
@@ -496,6 +514,7 @@ export async function collect(net = {}) {
|
|
|
496
514
|
} catch { /* probe failed: leave mcp unknown so status omits the add line */ }
|
|
497
515
|
}
|
|
498
516
|
}
|
|
517
|
+
if (s.deployment === "hosted") return;
|
|
499
518
|
const dir = D.workerDir({});
|
|
500
519
|
const haveCf = fs.existsSync(dir);
|
|
501
520
|
const tunnelHost = C.get("A2A_TUNNEL_HOSTNAME");
|
|
@@ -577,7 +596,7 @@ export async function status(o) {
|
|
|
577
596
|
else {
|
|
578
597
|
const rows = [
|
|
579
598
|
["config", s.configFile],
|
|
580
|
-
["deployment", s.deployed ? `Worker ${s.worker}, D1 ${s.d1 || s.worker}${s.account ? `, account ${s.account}` : ""}${s.cfProfile ? `, cf profile ${s.cfProfile}` : ""}${s.cron ? ", cron flush on" : ""}` : "none"],
|
|
599
|
+
["deployment", s.deployment === "hosted" ? "hosted" : s.deployed ? `Worker ${s.worker}, D1 ${s.d1 || s.worker}${s.account ? `, account ${s.account}` : ""}${s.cfProfile ? `, cf profile ${s.cfProfile}` : ""}${s.cron ? ", cron flush on" : ""}` : "none"],
|
|
581
600
|
["base URL", s.baseUrl ? `${s.baseUrl} (${s.baseUrlEnv ? `from the exported A2A_BASE_URL; ${s.configFile} has ${s.baseUrlSaved || "none"}` : s.urlKind})` : "(not saved)"],
|
|
582
601
|
];
|
|
583
602
|
if (s.deployed && s.baseUrl) {
|
package/lib/tunnel.mjs
CHANGED
|
@@ -254,7 +254,12 @@ export function cfWorkDir(preferred) {
|
|
|
254
254
|
return base;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
function refuseHosted() {
|
|
258
|
+
if (C.deploymentKind() === "hosted") die("this is a hosted config. tunnel provisions a self-hosted Worker only. Configure a public HTTPS webhook through the hosted service, or use polling if Cloudflare Tunnel delivery is unavailable. Check `status --json` first.");
|
|
259
|
+
}
|
|
260
|
+
|
|
257
261
|
function needDeployment(o) {
|
|
262
|
+
refuseHosted();
|
|
258
263
|
if (!C.get("A2A_D1_ID") || !C.get("A2A_WORKER_NAME")) die(`nothing is deployed from ${C.CONFIG_DIR}. Another deployment? Set A2A_CONFIG_DIR.`);
|
|
259
264
|
const dir = D.workerDir(o);
|
|
260
265
|
if (!fs.existsSync(dir)) die(`worker project not found at ${dir}; run \`${CLI} deploy\` once`);
|
|
@@ -263,6 +268,7 @@ function needDeployment(o) {
|
|
|
263
268
|
|
|
264
269
|
/** `cf` cwd for `tunnel rm` / `uninstall`. A missing Worker project uses a temp directory with a writable cache. */
|
|
265
270
|
function cfDirForRemove(o, { allowMissingProject = false } = {}) {
|
|
271
|
+
refuseHosted();
|
|
266
272
|
if (!C.get("A2A_D1_ID") || !C.get("A2A_WORKER_NAME")) die(`nothing is deployed from ${C.CONFIG_DIR}. Another deployment? Set A2A_CONFIG_DIR.`);
|
|
267
273
|
const dir = D.workerDir(o);
|
|
268
274
|
if (!fs.existsSync(dir) && !allowMissingProject) die(`worker project not found at ${dir}; run \`${CLI} deploy\` once`);
|
|
@@ -475,6 +481,7 @@ export function bareProbeNote(withoutToken) {
|
|
|
475
481
|
}
|
|
476
482
|
|
|
477
483
|
export async function remove(o, { quiet = false, allowMissingProject = false, skipSecrets = false, secretNames = undefined, rerun = "" } = {}) {
|
|
484
|
+
refuseHosted();
|
|
478
485
|
if (!TUNNEL_KEYS.some((k) => C.get(k))) {
|
|
479
486
|
if (!quiet) console.error("nothing to remove");
|
|
480
487
|
return;
|
package/lib/uninstall.mjs
CHANGED
|
@@ -78,6 +78,7 @@ function removalError(err, { removed, left, workerGone, workerStill }) {
|
|
|
78
78
|
|
|
79
79
|
/** One line per removal. A missing Worker or D1 counts as already gone. The config directory stays. */
|
|
80
80
|
export async function uninstall(o) {
|
|
81
|
+
if (C.deploymentKind() === "hosted") throw new CliError("this is a hosted config. uninstall removes self-hosted infrastructure only. To delete this hosted agent, use the hosted service agent settings and its deletion confirmation. The local config is unchanged.");
|
|
81
82
|
if (!deploymentSaved())
|
|
82
83
|
throw new CliError(`nothing is deployed from ${C.CONFIG_DIR}. Another deployment? Set A2A_CONFIG_DIR.`);
|
|
83
84
|
console.error(uninstallPlan());
|
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.32";
|