a2a-exposed 0.8.18 → 0.8.19

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 CHANGED
@@ -25,7 +25,7 @@ export const CLAUDE_CODE_NOTE = [
25
25
  " Then open the new session from the routine's runs and confirm it ran the inbox command without errors.",
26
26
  ].join("\n");
27
27
 
28
- const PRESETS = ["grok-bot", "claude-code", "openclaw-wake", "openclaw-agent", "hermes", "generic"];
28
+ const PRESETS = ["grok-bot", "claude-code", "openclaw-wake", "openclaw-agent", "hermes", "openbot", "generic"];
29
29
  // WAKE_ACCESS_*: Cloudflare Access service token for a wake URL behind Access (set by `tunnel create`, or exported
30
30
  // by hand for your own Access-protected endpoint)
31
31
  export const WAKE_SECRETS = ["WAKE_WEBHOOK_URL", "WAKE_WEBHOOK_KEY", "WAKE_HMAC_SECRET", "WAKE_ACCESS_CLIENT_ID", "WAKE_ACCESS_CLIENT_SECRET"];
package/lib/main.mjs CHANGED
@@ -92,9 +92,11 @@ https://github.com/telegraphic-dev/a2a-exposed/blob/main/docs/getting-started.md
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)
95
+ openbot: human supplies WAKE_HMAC_SECRET via protected env/secret storage, never chat or argv
95
96
  --max-per-hour hourly cap; 0 or unset is unlimited, except Claude Code,
96
97
  which defaults to 25 when this is unset
97
98
  wake preview rendered wake request (a credential is Bearer •••• or ••••, none of the secret)
99
+ openbot includes a sample delivery ID and canonical signing-input description
98
100
  + sha256 fingerprints of the uploaded URL/key/HMAC secret; compared with
99
101
  local env values when WAKE_WEBHOOK_URL, WAKE_WEBHOOK_KEY, and WAKE_HMAC_SECRET
100
102
  are exported in this shell. match: the exported value is the one on the Worker.
@@ -116,7 +118,7 @@ anywhere on the account: the A2A endpoint itself can be on workers.dev or a cust
116
118
  creates nothing when you run it again. A partial tunnel stops and asks for tunnel rm.
117
119
  The tunnel, the Access app, and the service token are named "a2a-exposed wake <worker>".
118
120
  The DNS host is wake-<word>-<word>-<4 hex>.<zone> unless --tunnel-hostname is set.
119
- Export WAKE_WEBHOOK_KEY (hermes: WAKE_HMAC_SECRET) so tunnel create uploads it.
121
+ Export WAKE_WEBHOOK_KEY (hermes/openbot: WAKE_HMAC_SECRET) so tunnel create uploads it.
120
122
  The connector machine needs cloudflared installed
121
123
  (https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/).
122
124
  Defaults: openclaw-* -> http://127.0.0.1:18789 /hooks/wake|agent, hermes -> :8644
@@ -267,7 +269,7 @@ Feedback
267
269
  polling), and hosted or self-hosted. --connection overrides
268
270
  that detection. Remove tokens from the message and from --logs.
269
271
 
270
- Presets: grok-bot | claude-code | openclaw-wake | openclaw-agent | hermes | generic
272
+ Presets: grok-bot | claude-code | openclaw-wake | openclaw-agent | hermes | openbot | generic
271
273
  Which clients are tested end to end is in the client table:
272
274
  https://github.com/telegraphic-dev/a2a-exposed/blob/main/docs/getting-started.md#clients
273
275
  Config: ${C.CONFIG_FILE} (override dir with --config-dir DIR or A2A_CONFIG_DIR; env vars override file values)
package/lib/status.mjs CHANGED
@@ -171,8 +171,8 @@ function nextStepCore(s) {
171
171
  if (t && !t.complete) return fail(`a previous \`tunnel create\` did not finish: run \`${CLI} tunnel rm\`, then \`${CLI} tunnel create\``);
172
172
  if (t && !t.tokenFileOk)
173
173
  return fail(`the tunnel's connector token file (${t.tokenFile}) is missing or empty: run \`${CLI} tunnel create\` again (it downloads the token again; nothing new is created)`);
174
- const agentSecret = s.wake.preset === "hermes" ? "WAKE_HMAC_SECRET" : "WAKE_WEBHOOK_KEY";
175
- const needsAgentSecret = LOCAL_PRESETS.includes(s.wake.preset) && !s.wake.hasKey && !s.wake.hasHmacSecret;
174
+ const agentSecret = ["hermes", "openbot"].includes(s.wake.preset) ? "WAKE_HMAC_SECRET" : "WAKE_WEBHOOK_KEY";
175
+ const needsAgentSecret = s.wake.preset === "openbot" ? !s.wake.hasHmacSecret : LOCAL_PRESETS.includes(s.wake.preset) && !s.wake.hasKey && !s.wake.hasHmacSecret;
176
176
  const now = typeof s.now === "number" ? s.now : Date.now();
177
177
  if (t && !(s.wake.hasAccessServiceToken && t.workerUrlMatches)) {
178
178
  if (secretsPropagating(t, now) && (t.connections === 0 || t.connections == null))
@@ -184,7 +184,7 @@ function nextStepCore(s) {
184
184
  if (t && t.connections === 0)
185
185
  return fail(`the tunnel has ${t.state === "inactive" ? "never connected" : "no running connector"}: on the agent's machine run \`cloudflared tunnel run --token-file ${t.tokenFile}\` (outbound port 7844 must be open), then \`${CLI} wake test\``);
186
186
  if (s.wake.configured && needsAgentSecret)
187
- return fail(`the Worker sends no webhook auth, so the ${s.wake.preset} webhook will reject wakes: export ${agentSecret} (${s.wake.preset === "hermes" ? "the route secret" : "the hooks token"}), then run \`${CLI} ${t ? "tunnel create" : "wake set"}\``);
187
+ return fail(`the Worker sends no webhook auth, so the ${s.wake.preset} webhook will reject wakes: export ${agentSecret} (${["hermes", "openbot"].includes(s.wake.preset) ? "the route secret" : "the hooks token"}), then run \`${CLI} ${t ? "tunnel create" : "wake set"}\``);
188
188
  // not broken, but peers can't pair until the human sets the approval password
189
189
  const also = [];
190
190
  if (s.pairing && s.pairing.mode === "human" && !s.pairing.passwordSet)
@@ -195,7 +195,7 @@ function nextStepCore(s) {
195
195
  if (!s.wake.configured) {
196
196
  const poll = "the agent must check the A2A endpoint on a schedule (Hermes and OpenClaw have copy-paste commands in the setup references)";
197
197
  if (s.wake.preset && !LOCAL_PRESETS.includes(s.wake.preset) && s.wake.preset !== "generic")
198
- return done2(`no wake webhook: export WAKE_WEBHOOK_URL and WAKE_WEBHOOK_KEY, then run \`${CLI} wake set --preset ${s.wake.preset}\`; or ${poll}`);
198
+ return done2(`no wake webhook: export WAKE_WEBHOOK_URL and ${agentSecret}, then run \`${CLI} wake set --preset ${s.wake.preset}\`; or ${poll}`);
199
199
  const z = s.zones;
200
200
  const tunnel = !z ? `a local-only webhook can use \`${CLI} tunnel create\` (needs a zone on the account)`
201
201
  : !z.length ? "the account has no domain (zone), so the secure tunnel is not available"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2a-exposed",
3
- "version": "0.8.18",
3
+ "version": "0.8.19",
4
4
  "description": "Give any AI agent a public A2A (Agent2Agent) endpoint: a Cloudflare Worker, a wake webhook, or scheduled checks",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@ const plain: Record<string, string> = {
28
28
  PROVIDER_ORGANIZATION: v("A2A_PROVIDER_ORGANIZATION"),
29
29
  PROVIDER_URL: v("A2A_PROVIDER_URL"),
30
30
  DOCUMENTATION_URL: v("A2A_DOCUMENTATION_URL"),
31
- WAKE_PRESET: v("WAKE_PRESET") || "generic", // grok-bot | claude-code | openclaw-wake | openclaw-agent | hermes | generic
31
+ WAKE_PRESET: v("WAKE_PRESET") || "generic", // grok-bot | claude-code | openclaw-wake | openclaw-agent | hermes | openbot | generic
32
32
  WAKE_AGENT_ID: v("WAKE_AGENT_ID"), // openclaw presets (default "main")
33
33
  WAKE_KEY_HEADER: v("WAKE_KEY_HEADER"), // generic preset (default "authorization")
34
34
  WAKE_KEY_PREFIX: e.WAKE_KEY_PREFIX ?? "", // generic preset; unset = "Bearer "
@@ -1,4 +1,4 @@
1
- // Write worker/src/version.ts from cli/package.json. Publish runs `npm version` in cli/
1
+ // Write worker/src/version.ts from cli/package.json. Release preparation runs `npm run version:set`
2
2
  // before the Worker tests and build, and prepack bundles this tree into the npm package.
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
@@ -1619,6 +1619,7 @@ async function handleOwner(req: Request, ctx: TenantContext, ectx: ExecutionCont
1619
1619
  fingerprints: { url: await A.fingerprint(ctx.WAKE_WEBHOOK_URL), key: await A.fingerprint(ctx.WAKE_WEBHOOK_KEY),
1620
1620
  hmacSecret: await A.fingerprint(ctx.WAKE_HMAC_SECRET),
1621
1621
  accessClientId: await A.fingerprint(ctx.WAKE_ACCESS_CLIENT_ID), accessClientSecret: await A.fingerprint(ctx.WAKE_ACCESS_CLIENT_SECRET) },
1622
+ ...(preset(ctx) === "openbot" ? { signingInput: "<timestamp>.<delivery-id>.<exact-body-bytes>" } : {}),
1622
1623
  request: req ? redact(req) : null });
1623
1624
  }
1624
1625
  if (seg[0] === "wake" && seg[1] === "test" && m === "POST") {
@@ -1,2 +1,2 @@
1
1
  /** Inbox build. Keep this equal to `version` in cli/package.json. */
2
- export const VERSION = "0.8.18";
2
+ export const VERSION = "0.8.19";
@@ -46,7 +46,7 @@ export type WakeConfig = {
46
46
 
47
47
  export type WakeRequest = { url: string; method: "POST"; headers: Record<string, string>; body: string };
48
48
 
49
- export const PRESETS = ["grok-bot", "claude-code", "openclaw-wake", "openclaw-agent", "hermes", "generic"] as const;
49
+ export const PRESETS = ["grok-bot", "claude-code", "openclaw-wake", "openclaw-agent", "hermes", "openbot", "generic"] as const;
50
50
 
51
51
  /** Default debounce window per preset (seconds), used when WAKE_DEBOUNCE_SECONDS is unset. */
52
52
  export function defaultDebounceSeconds(preset: string): number {
@@ -207,6 +207,16 @@ export async function renderWake(cfg: WakeConfig, ev: WakeEvent, opts: { nowMs?:
207
207
  // `name` is the hook name OpenClaw filters match. It stays "A2A inbox". The wake text itself starts with "A2A endpoint:".
208
208
  body = JSON.stringify({ message: wakeSummary(ev, cli), name: "A2A inbox", agentId: cfg.agentId || "main", sessionMode: "isolated", deliver: false });
209
209
  break;
210
+ case "openbot": {
211
+ const ts = String(Math.floor(nowMs / 1000));
212
+ headers["x-openbot-timestamp"] = ts;
213
+ headers["x-openbot-delivery-id"] = requestId;
214
+ // Serialize once: the signature covers exactly the UTF-8 bytes sent to OpenBot.
215
+ body = JSON.stringify({ type: "a2a_wake", occurredAt: new Date(nowMs).toISOString(), data: wakePayload(ev, cli) });
216
+ if (cfg.hmacSecret)
217
+ headers["x-openbot-signature"] = `sha256=${await hmacSha256Hex(cfg.hmacSecret, `${ts}.${requestId}.${body}`)}`;
218
+ break;
219
+ }
210
220
  case "hermes":
211
221
  headers["x-request-id"] = requestId;
212
222
  body = JSON.stringify(wakePayload(ev, cli));
@@ -268,7 +278,7 @@ export function cloudflareErrorHint(status: number, body: string, opts?: { tunne
268
278
 
269
279
  /** Headers a wake preview may show in full. Every other header carries a credential, whatever its name. */
270
280
  export const PREVIEW_PLAIN_HEADERS = new Set([
271
- "content-type", "user-agent", "x-request-id", "idempotency-key", "anthropic-version", "x-webhook-timestamp",
281
+ "content-type", "user-agent", "x-request-id", "idempotency-key", "anthropic-version", "x-webhook-timestamp", "x-openbot-timestamp", "x-openbot-delivery-id",
272
282
  ]);
273
283
 
274
284
  export function previewHeaderPlain(name: string): boolean {
@@ -72,3 +72,31 @@ test("a 429 is due at Retry-After when the debounce is longer", async (t) => {
72
72
  const at = await nextCronAt(resolveTenant(env), Date.now());
73
73
  assert.ok(at != null && at >= before + 25_000 && at <= before + 35_000, `alarm ${at} is Retry-After, not the 120s debounce`);
74
74
  });
75
+
76
+ test("OpenBot preview masks signing credentials and wake test accepts HTTP 202", async (t) => {
77
+ const env: any = { DB: d1(new URL("../migrations/", import.meta.url)), PUBLIC_URL: BASE, OWNER_TOKEN: "owner-secret",
78
+ WAKE_PRESET: "openbot", WAKE_WEBHOOK_URL: "https://hooks.example.com/a2a", WAKE_HMAC_SECRET: "dummy-openbot-secret" };
79
+ const pending: Promise<unknown>[] = [];
80
+ const call = (method: string, path: string) => worker.fetch(new Request(BASE + path, { method, headers: { authorization: "Bearer owner-secret" } }) as any,
81
+ env, { waitUntil: (p: Promise<unknown>) => pending.push(p), passThroughOnException() {} } as any);
82
+ const preview = await (await call("GET", "/owner/wake/preview")).json() as any;
83
+ assert.equal(preview.preset, "openbot");
84
+ assert.equal(preview.signingInput, "<timestamp>.<delivery-id>.<exact-body-bytes>");
85
+ assert.equal(preview.request.headers["x-openbot-delivery-id"], "preview");
86
+ assert.equal(preview.request.headers["x-openbot-signature"], "••••");
87
+ assert.ok(!JSON.stringify(preview).includes(env.WAKE_HMAC_SECRET));
88
+ const original = globalThis.fetch;
89
+ let sent = false;
90
+ globalThis.fetch = (async (_url: any, init: any) => {
91
+ sent = true;
92
+ assert.equal(JSON.parse(init.body).type, "a2a_wake");
93
+ assert.match(init.headers["x-openbot-signature"], /^sha256=[0-9a-f]{64}$/);
94
+ assert.ok(init.headers["x-openbot-delivery-id"]);
95
+ return new Response("{}", { status: 202 });
96
+ }) as any;
97
+ t.after(() => { globalThis.fetch = original; });
98
+ const result = await (await call("POST", "/owner/wake/test")).json() as any;
99
+ assert.equal(result.status, 202);
100
+ assert.equal(sent, true);
101
+ await Promise.allSettled(pending);
102
+ });
@@ -2,7 +2,7 @@
2
2
  import { test } from "node:test";
3
3
  import assert from "node:assert/strict";
4
4
  import { createHmac } from "node:crypto";
5
- import { renderWake, renderTemplate, wakeSummary, redact, addAccessHeaders, cloudflareErrorHint, PRESETS, type WakeEvent } from "../src/wake.ts";
5
+ import { renderWake, renderTemplate, wakeSummary, redact, addAccessHeaders, cloudflareErrorHint, PRESETS, wakePayload, type WakeEvent } from "../src/wake.ts";
6
6
 
7
7
  const ev: WakeEvent = {
8
8
  contextId: "ctx-1", taskId: "t-2", taskIds: ["t-1", "t-2"], from: "peer-a",
@@ -144,6 +144,7 @@ test("Access service-token headers are added for every preset, next to the prese
144
144
  assert.equal(r.headers["CF-Access-Client-Id"], "abc123.access", preset);
145
145
  assert.equal(r.headers["CF-Access-Client-Secret"], "s3cr3t-value-0123456789", preset);
146
146
  if (preset === "hermes") assert.ok(r.headers["X-Webhook-Signature-V2"], "hermes keeps its HMAC");
147
+ else if (preset === "openbot") assert.ok(r.headers["x-openbot-signature"], "openbot keeps its HMAC");
147
148
  else assert.ok(r.headers["authorization"], `${preset} keeps its bearer`);
148
149
  }
149
150
  });
@@ -180,3 +181,24 @@ test("cloudflareErrorHint explains tunnel / Access failures", () => {
180
181
  assert.match(cloudflareErrorHint(530, "error code: 1016", { tunnel: true }), /tunnel hostname has no matching DNS record/);
181
182
  });
182
183
 
184
+
185
+ // Fixed vector independently generated with Python hashlib/hmac.
186
+ test("openbot: envelope and signature cover the exact body and delivery ID", async () => {
187
+ assert.ok(PRESETS.includes("openbot"));
188
+ const req = (await renderWake({ preset: "openbot", url: "https://hooks.example.com/a2a", hmacSecret: "openbot-test-secret", key: "unused-key" }, ev,
189
+ { nowMs: 1760000000000, requestId: "delivery-1" }))!;
190
+ assert.deepEqual(JSON.parse(req.body), { type: "a2a_wake", occurredAt: "2025-10-09T08:53:20.000Z", data: wakePayload(ev, "npx -y a2a-exposed@latest") });
191
+ assert.equal(req.headers["x-openbot-timestamp"], "1760000000");
192
+ assert.equal(req.headers["x-openbot-delivery-id"], "delivery-1");
193
+ assert.equal(req.headers["x-openbot-signature"], "sha256=8d28056bd4d5f700fd49865a0cc240bd769462d6e75aac4aa15fc7051c3ecc26");
194
+ assert.equal(req.headers["authorization"], undefined);
195
+ assert.equal(req.headers["X-Webhook-Signature-V2"], undefined);
196
+ const preview = redact(req);
197
+ assert.equal(preview.headers["x-openbot-delivery-id"], "delivery-1");
198
+ assert.equal(preview.headers["x-openbot-timestamp"], "1760000000");
199
+ assert.equal(preview.headers["x-openbot-signature"], "••••");
200
+ assert.ok(!JSON.stringify(preview).includes("openbot-test-secret"));
201
+ const unicode = (await renderWake({ preset: "openbot", url: req.url, hmacSecret: "s" }, { ...ev, preview: '雪 "quoted"\ntext' },
202
+ { nowMs: 1760000000000, requestId: "delivery-2" }))!;
203
+ assert.equal(unicode.headers["x-openbot-signature"], "sha256=" + createHmac("sha256", "s").update(`1760000000.delivery-2.${unicode.body}`).digest("hex"));
204
+ });