a2a-exposed 0.8.20 → 0.8.21
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/deploy.mjs +9 -1
- package/lib/main.mjs +3 -2
- package/lib/status.mjs +23 -1
- package/lib/update.mjs +1 -1
- package/package.json +1 -1
- package/worker/src/index.ts +2 -1
- package/worker/src/version.ts +1 -1
- package/worker/test/hosted.test.ts +5 -0
- package/worker/test/tenancy.test.ts +2 -1
package/lib/deploy.mjs
CHANGED
|
@@ -11,6 +11,14 @@ import { readHidden } from "./pair.mjs";
|
|
|
11
11
|
import * as WD from "./workersdev.mjs";
|
|
12
12
|
import { printStar } from "./feedback.mjs";
|
|
13
13
|
|
|
14
|
+
export const VERSION = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
15
|
+
|
|
16
|
+
/** Arguments shared by deploy attempts, including workers.dev registration retries. */
|
|
17
|
+
export function deployArgs(version, isoDate) {
|
|
18
|
+
if (!/^[A-Za-z0-9._-]+$/.test(version)) throw new Error("invalid Worker version tag");
|
|
19
|
+
return ["deploy", "--message", `a2a-exposed ${version} ${isoDate}`, "--tag", `a2a-exposed-${version}`];
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
23
|
/** Each Claude Code routine fire is a new cloud session: a 2xx only means a session started. */
|
|
16
24
|
export const CLAUDE_CODE_NOTE = [
|
|
@@ -392,7 +400,7 @@ function runTee(cmd, args, { cwd, env }) {
|
|
|
392
400
|
/** cf deploy (secrets via a temporary chmod-600 file). If the account has no workers.dev subdomain yet and
|
|
393
401
|
* `register` names one, retry through cf's own registration prompt. Returns cf's output. */
|
|
394
402
|
async function cfDeploy(dir, secrets, { register } = {}) {
|
|
395
|
-
const base =
|
|
403
|
+
const base = deployArgs(VERSION, new Date().toISOString());
|
|
396
404
|
const go = async (file) => {
|
|
397
405
|
const args = cfArgs(file ? [...base, "--secrets-file", file] : base);
|
|
398
406
|
let r = await runTee(cfBin(dir), args, { cwd: dir, env: deployEnv() });
|
package/lib/main.mjs
CHANGED
|
@@ -88,7 +88,7 @@ https://github.com/telegraphic-dev/a2a-exposed/blob/main/docs/getting-started.md
|
|
|
88
88
|
e.g. "node /path/to/repo/cli/bin/a2a-exposed.mjs")
|
|
89
89
|
--worker-dir where the Worker project (template copy) lives (default
|
|
90
90
|
<config dir>/worker). The config dir is --config-dir / A2A_CONFIG_DIR
|
|
91
|
-
deploy [same flags] redeploy with saved settings (secrets persist)
|
|
91
|
+
deploy [same flags] redeploy with saved settings (secrets persist); tag the Worker version as a2a-exposed-<version>
|
|
92
92
|
wake set [--preset P] [--agent-id A] [--key-header H] [--key-prefix X] [--body-template JSON]
|
|
93
93
|
[--cli-command CMD] [--debounce S] [--max-per-hour N]
|
|
94
94
|
save wake settings, upload wake secrets from env, and redeploy (one step)
|
|
@@ -145,11 +145,12 @@ Remove
|
|
|
145
145
|
--dir and --worker-dir are the Worker project directory (default <config dir>/worker)
|
|
146
146
|
|
|
147
147
|
Status
|
|
148
|
-
status [--json] deployment, base URL, agent card check (fetched by the CLI
|
|
148
|
+
status [--json] deployment, Worker version, base URL, agent card check (fetched by the CLI), wake
|
|
149
149
|
mode (webhook / tunnel / none = polling), tunnel state, proxy mode (--upstream):
|
|
150
150
|
upstream card, Access credential and upstream bearer configured (separately), the
|
|
151
151
|
live upstream check, the last failed peer call, card-leak check; warnings for peer
|
|
152
152
|
token variables shadowing saved tokens; and the next step to run.
|
|
153
|
+
Older Workers need deploy (exit 1); npm latest hints use the local update cache.
|
|
153
154
|
When GET /owner/mcp reports enabled and the agent card is reachable, prints
|
|
154
155
|
\`claude mcp add --transport http a2a-exposed <base URL>/mcp\`.
|
|
155
156
|
A failed probe, an unreachable card, or a Worker that reports MCP off leaves that line out.
|
package/lib/status.mjs
CHANGED
|
@@ -8,6 +8,17 @@ import { CLI, cardUrlProblem, cfErrorCode, describeHttp, die, fingerprint, httpJ
|
|
|
8
8
|
import { baseUrl, owner, ownerTry } from "./commands.mjs";
|
|
9
9
|
import * as D from "./deploy.mjs";
|
|
10
10
|
import * as T from "./tunnel.mjs";
|
|
11
|
+
import { cacheFile, readCache, newer } from "./update.mjs";
|
|
12
|
+
|
|
13
|
+
/** Public health probe. Only a valid health answer establishes missing version reporting. */
|
|
14
|
+
export async function checkHealth(base) {
|
|
15
|
+
try {
|
|
16
|
+
const { status, data } = await httpJson(`${base}/health`, { timeout: 15000 });
|
|
17
|
+
if (status === 200 && data?.ok === true)
|
|
18
|
+
return { ok: true, version: typeof data.version === "string" && data.version ? data.version : null };
|
|
19
|
+
} catch { /* unreachable: version stays unknown */ }
|
|
20
|
+
return { ok: false, version: null };
|
|
21
|
+
}
|
|
11
22
|
|
|
12
23
|
/** Presets whose webhook usually listens only locally: wakes need the tunnel (or another reverse proxy). */
|
|
13
24
|
export const LOCAL_PRESETS = ["openclaw-wake", "openclaw-agent", "hermes"];
|
|
@@ -111,6 +122,9 @@ export function secretsPropagating(tunnel, now = Date.now()) {
|
|
|
111
122
|
* as separate lines (so the next step stays one short instruction). */
|
|
112
123
|
export function nextStep(s) {
|
|
113
124
|
const n = nextStepCore(s);
|
|
125
|
+
const version = s.cliVersion || D.VERSION;
|
|
126
|
+
if (s.workerVersion && s.latestVersion && newer(s.latestVersion, s.workerVersion) && !newer(s.workerVersion, version))
|
|
127
|
+
n.also.push(`a2a-exposed ${s.latestVersion} is available: run npx -y a2a-exposed@latest deploy`);
|
|
114
128
|
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`);
|
|
115
129
|
return sh.length ? { ...n, also: [...n.also, ...sh] } : n;
|
|
116
130
|
}
|
|
@@ -133,6 +147,11 @@ function nextStepCore(s) {
|
|
|
133
147
|
return fail(/HTTP 401/.test(s.ownerApi.error)
|
|
134
148
|
? `the owner token in ${s.configFile} does not match the Worker: run \`${CLI} init --rotate-owner-token\` from the machine that owns this deployment`
|
|
135
149
|
: `the owner API failed (${s.ownerApi.error}): run \`${CLI} deploy\``);
|
|
150
|
+
const version = s.cliVersion || D.VERSION;
|
|
151
|
+
if (s.workerVersion && newer(version, s.workerVersion))
|
|
152
|
+
return fail(`the deployed Worker is ${s.workerVersion}, older than this CLI (${version}): run \`${CLI} deploy\``);
|
|
153
|
+
if (s.health?.ok && s.health.version === null)
|
|
154
|
+
return fail(`the deployed Worker predates version reporting (older than ${version}): run \`${CLI} deploy\``);
|
|
136
155
|
// proxy / expose mode: the upstream answers peers, so wakes are optional (pairing notifications only)
|
|
137
156
|
const fc = s.facade;
|
|
138
157
|
if (fc) {
|
|
@@ -220,12 +239,14 @@ export async function collect() {
|
|
|
220
239
|
upstream: C.get("A2A_UPSTREAM_URL") || null, facade: null, upstreamVerify: null, upstreamVerifyError: "", facadePeerAuth: null,
|
|
221
240
|
upstreamNoToken: C.get("A2A_UPSTREAM_NO_TOKEN") === "1", localUpstreamToken: fingerprint(process.env.UPSTREAM_TOKEN),
|
|
222
241
|
peerTokenShadowed: shadowedPeerTokens(),
|
|
223
|
-
mcp: "unknown",
|
|
242
|
+
mcp: "unknown", health: null, workerVersion: null, cliVersion: D.VERSION, latestVersion: readCache(cacheFile())?.latest || null,
|
|
224
243
|
};
|
|
225
244
|
const envBase = (process.env.A2A_BASE_URL || "").replace(/\/$/, "");
|
|
226
245
|
if (envBase && envBase !== s.baseUrlSaved) s.baseUrlEnv = envBase;
|
|
227
246
|
if (!s.deployed || !s.baseUrl) return s;
|
|
228
247
|
s.card = await checkCard(s.baseUrl);
|
|
248
|
+
s.health = await checkHealth(s.baseUrl);
|
|
249
|
+
s.workerVersion = s.health.version;
|
|
229
250
|
if (s.hasOwnerToken) {
|
|
230
251
|
try {
|
|
231
252
|
const p = await owner("GET", "/owner/wake/preview");
|
|
@@ -335,6 +356,7 @@ export async function status(o) {
|
|
|
335
356
|
if (s.deployed && s.baseUrl) {
|
|
336
357
|
rows.push(["agent card", !s.card.ok ? `FAILED: ${s.card.error}` : s.card.urlProblem ? `WRONG URL: "${s.card.name}", but ${s.card.urlProblem}`
|
|
337
358
|
: `OK: "${s.card.name}" (A2A ${s.card.versions.join(", ") || "?"})`]);
|
|
359
|
+
rows.push(["worker version", `${s.workerVersion || "unknown"} (CLI ${s.cliVersion})`]);
|
|
338
360
|
rows.push(["owner API", s.ownerApi.ok ? "OK" : `FAILED: ${s.ownerApi.error}`]);
|
|
339
361
|
if (s.upstream) {
|
|
340
362
|
const f = s.facade || {};
|
package/lib/update.mjs
CHANGED
|
@@ -36,7 +36,7 @@ export function notice(current, cache) {
|
|
|
36
36
|
|
|
37
37
|
export const due = (cache, now = Date.now()) => !cache || !(now - (cache.checkedAt || 0) < CHECK_INTERVAL_MS);
|
|
38
38
|
|
|
39
|
-
function readCache(file) {
|
|
39
|
+
export function readCache(file) {
|
|
40
40
|
try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; }
|
|
41
41
|
}
|
|
42
42
|
|
package/package.json
CHANGED
package/worker/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Worker serves a rewritten agent card and device-flow pairing, and forwards authenticated JSON-RPC to the upstream.
|
|
5
5
|
// Handlers take a TenantContext (tenancy.ts) built from the Worker bindings. With the hosted gates unset, that
|
|
6
6
|
// context is the single self-host tenant and behaviour matches a Worker that read env.* directly.
|
|
7
|
+
import { VERSION } from "./version.ts";
|
|
7
8
|
import * as A from "./a2a.ts";
|
|
8
9
|
import * as F from "./facade.ts";
|
|
9
10
|
import { renderWake, redact, cloudflareErrorHint, wakeUrlIsTunnelHost, defaultDebounceSeconds, defaultMaxPerHour, type WakeConfig, type WakeEvent } from "./wake.ts";
|
|
@@ -3404,7 +3405,7 @@ async function handle(req: Request, ctx: TenantContext, ectx: ExecutionContext,
|
|
|
3404
3405
|
// legacy discovery path (A2A 0.2): the 0.3-shaped card that clients of that era parse
|
|
3405
3406
|
if (req.method === "GET" && path === "/.well-known/agent.json")
|
|
3406
3407
|
return json(agentCard03(ctx, await agentCard(ctx)), 200, { "access-control-allow-origin": "*", "cache-control": cardCacheControl(ctx) });
|
|
3407
|
-
if (req.method === "GET" && path === "/health") return json({ ok: true, publicUrl: ctx.PUBLIC_URL, mode: proxyMode(ctx) ? "proxy" : "inbox", mcp: mcpOn(ctx) });
|
|
3408
|
+
if (req.method === "GET" && path === "/health") return json({ ok: true, version: VERSION, publicUrl: ctx.PUBLIC_URL, mode: proxyMode(ctx) ? "proxy" : "inbox", mcp: mcpOn(ctx) });
|
|
3408
3409
|
if (path === "/mcp") return mcpOn(ctx) ? await handleMcp(req, ctx, ectx, url) : json({ error: "not_found", error_description: "the MCP server is turned off on this A2A endpoint (MCP=off)" }, 404);
|
|
3409
3410
|
if (mcpOn(ctx)) {
|
|
3410
3411
|
if (req.method === "GET" && path === "/.well-known/oauth-protected-resource/mcp") return json(mcpResourceMetadata(ctx), 200, { "access-control-allow-origin": "*" });
|
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.21";
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { Miniflare, convertV4MiniflareOptions } from "miniflare";
|
|
10
|
+
import { VERSION } from "../src/version.ts";
|
|
10
11
|
import worker from "../src/index.ts";
|
|
11
12
|
import { sha256 } from "../src/a2a.ts";
|
|
12
13
|
import { sealWakeSecrets } from "../src/wake-secrets.ts";
|
|
@@ -81,6 +82,7 @@ test("gates unset: the hosted entry matches the self-host handler, including TEN
|
|
|
81
82
|
const base = "https://agent.example.com";
|
|
82
83
|
const nodeEnv = { DB: d1(MIGRATIONS), PUBLIC_URL: base, OWNER_TOKEN: "owner-secret" };
|
|
83
84
|
const nodeHealth = await text(await worker.fetch(new Request(base + "/health"), nodeEnv as never, ectx as never));
|
|
85
|
+
assert.deepEqual(JSON.parse(nodeHealth), { ok: true, version: VERSION, publicUrl: base, mode: "inbox", mcp: true });
|
|
84
86
|
const nodeDenied = await text(await worker.fetch(new Request(base + "/", {
|
|
85
87
|
method: "POST", headers: { "content-type": "application/json" },
|
|
86
88
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "message/send", params: {} }),
|
|
@@ -180,6 +182,9 @@ test("per-tenant SQLite: directory, config push, and isolation", async (t) => {
|
|
|
180
182
|
assert.equal(bobPush.result.applied, true, JSON.stringify(bobPush.result));
|
|
181
183
|
await kv.put("tenant:bobby", JSON.stringify({ id: "id-bobby", status: "active", region: "", version: 1 }));
|
|
182
184
|
|
|
185
|
+
assert.deepEqual(await (await mf.dispatchFetch("https://alice.example.com/health")).json(),
|
|
186
|
+
{ ok: true, version: VERSION, publicUrl: "https://alice.example.com", mode: "inbox", mcp: true });
|
|
187
|
+
|
|
183
188
|
const card = async (name: string, headers?: HeadersInit) => (await mf.dispatchFetch(`https://${name}.example.com/.well-known/agent-card.json`, { headers })).json() as Promise<{ name: string }>;
|
|
184
189
|
const aliceCard = await mf.dispatchFetch("https://alice.example.com/.well-known/agent-card.json");
|
|
185
190
|
assert.equal(aliceCard.headers.get("cache-control"), "public, max-age=60");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Tenant context: gates default off, self-host owner auth and peer-token key stay compatible, hosted uses a hash
|
|
2
2
|
// and TENANT_SECRETS_KEY. Run: npm test
|
|
3
|
+
import { VERSION } from "../src/version.ts";
|
|
3
4
|
import { test } from "node:test";
|
|
4
5
|
import assert from "node:assert/strict";
|
|
5
6
|
import worker from "../src/index.ts";
|
|
@@ -258,7 +259,7 @@ test("fetch path: gates set or unset, the self-host Worker still answers from OW
|
|
|
258
259
|
const plain = await call(base, "/health");
|
|
259
260
|
const reserved = await call({ ...base, ...gates }, "/health");
|
|
260
261
|
const empty = await call({ ...base, TENANCY: "", QUOTAS: "", DATA_REGION: "", SIGNUP_URL: "" }, "/health");
|
|
261
|
-
assert.deepEqual(plain.data, { ok: true, publicUrl: BASE, mode: "inbox", mcp: true });
|
|
262
|
+
assert.deepEqual(plain.data, { ok: true, version: VERSION, publicUrl: BASE, mode: "inbox", mcp: true });
|
|
262
263
|
assert.deepEqual(reserved.data, plain.data);
|
|
263
264
|
assert.deepEqual(empty.data, plain.data);
|
|
264
265
|
|