@a2a-exposed/control-core 0.8.18 → 0.8.20
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/README.md
CHANGED
|
@@ -10,7 +10,11 @@ Agents that can call out over HTTP and cannot run a public server: hosted assist
|
|
|
10
10
|
|
|
11
11
|
## Quick start
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
No Cloudflare account, or no preference? Start with a [hosted agent](docs/getting-started.md#hosted-inbox) at `https://<name>.a2a.exposed`. It is in open beta with a Free plan; see [pricing](https://a2a.exposed/pricing).
|
|
14
|
+
|
|
15
|
+
Claude Code cloud sessions use the [hosted connector](docs/mcp.md#hosted-connector), with no skill install or CLI. Skip project installs in shared cloud checkouts; optional skills use `npx -y skills add telegraphic-dev/a2a-exposed -g`.
|
|
16
|
+
|
|
17
|
+
Self-hosting requires Node 22.18+. The skills are installed into the project the agent works in. They do not install the CLI. Every command runs through npx.
|
|
14
18
|
|
|
15
19
|
1. **Add the skills to the project**
|
|
16
20
|
|
|
@@ -20,6 +24,8 @@ Requires Node 22.18+. The skills are installed into the project the agent works
|
|
|
20
24
|
|
|
21
25
|
2. **Deploy an A2A endpoint** in your Cloudflare account. [Self-hosting](docs/self-hosting.md) covers the login: `cf auth login`, a named profile (`--cf-profile`), an API token, and `--account-id` when that token can see more than one account.
|
|
22
26
|
|
|
27
|
+
Before running `init` or another Cloudflare change, the agent shows you the account name/id from `npx cf auth whoami` and the plan: Worker name, D1 database, hostname and any workers.dev registration or custom-domain DNS record, and owner/wake secrets by purpose without values. It waits for your explicit yes; a login is not consent. This also applies to `deploy`, `tunnel create`, façade tunnel/Access steps, and `uninstall`. See the [consent checklist](skills/a2a-exposed-setup/references/deploy.md#before-cloudflare-changes). Ask before anything billable or DNS changes on a zone that already serves something.
|
|
28
|
+
|
|
23
29
|
```bash
|
|
24
30
|
npx -y a2a-exposed@latest init --hostname agent.example.com --agent-name "My Agent"
|
|
25
31
|
```
|
|
@@ -35,11 +41,11 @@ Requires Node 22.18+. The skills are installed into the project the agent works
|
|
|
35
41
|
|
|
36
42
|
`status` is read-only and prints the next step. Open the one-time link (15 minutes by default, `--ttl` from 1 to 60) and choose the approval password yourself. Another agent then runs `npx -y a2a-exposed@latest connect https://agent.example.com` (the code lasts 60 minutes). You approve that request on the `/device` page, or with `npx -y a2a-exposed@latest pair approve <code>`. Removing the A2A endpoint later is `npx -y a2a-exposed@latest uninstall` ([Self-hosting](docs/self-hosting.md#remove-an-inbox)).
|
|
37
43
|
|
|
38
|
-
After the skill is in the project, you can ask the agent to set this up. It follows the setup skill.
|
|
44
|
+
After the skill is in the project, you can ask the agent to set this up. It follows the setup skill. Claude Code cloud sessions follow the [hosted checklist](docs/mcp.md#hosted-connector); self-hosted deployment runs on a machine you control. Claude Code: `/plugin marketplace add telegraphic-dev/a2a-exposed`, then `/plugin install a2a-exposed@a2a-exposed`. [Getting started](docs/getting-started.md) has Node, the client table, and what to do if setup stops halfway.
|
|
39
45
|
|
|
40
46
|
## Hosted
|
|
41
47
|
|
|
42
|
-
[a2a.exposed](https://a2a.exposed) hosts the A2A endpoint and is open to everyone. Pricing is at [a2a.exposed/pricing](https://a2a.exposed/pricing). When the CLI cannot run, connect https://a2a.exposed/mcp. Sign-in is the owner identity. The owner token stays on the A2A endpoint. Claude and Codex are untested on this address. When to use the hosted endpoint, and the connect commands, are in [Getting started](docs/getting-started.md#hosted-inbox).
|
|
48
|
+
[a2a.exposed](https://a2a.exposed/hosted) hosts the A2A endpoint and is open to everyone. Pricing is at [a2a.exposed/pricing](https://a2a.exposed/pricing). Sign in at https://a2a.exposed/app to create an agent; no Cloudflare account is needed. When the CLI cannot run, connect https://a2a.exposed/mcp. Sign-in is the owner identity. The owner token stays on the A2A endpoint. Claude Code cloud sessions with the hosted connector and a routine wake are tested end to end. Other Claude clients and Codex are untested on this address. When to use the hosted endpoint, and the connect commands, are in [Getting started](docs/getting-started.md#hosted-inbox).
|
|
43
49
|
|
|
44
50
|
## Docs
|
|
45
51
|
|
package/dist/index.js
CHANGED
|
@@ -249,8 +249,8 @@ async function flushOutbox(db, plane, now = Date.now()) {
|
|
|
249
249
|
}
|
|
250
250
|
try {
|
|
251
251
|
if (await deliverOutbox(db, plane, row)) delivered += 1;
|
|
252
|
-
} catch
|
|
253
|
-
console.log(JSON.stringify({ ts: new Date(now).toISOString(), event: "outbox_push_failed", tenant: row.id, error:
|
|
252
|
+
} catch {
|
|
253
|
+
console.log(JSON.stringify({ ts: new Date(now).toISOString(), event: "outbox_push_failed", tenant: row.id, error: "config_delivery_failed" }));
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
const left = await db.prepare("SELECT COUNT(*) AS n FROM config_outbox").first();
|
|
@@ -498,14 +498,16 @@ async function listTenants(db, accountId, domain) {
|
|
|
498
498
|
}
|
|
499
499
|
|
|
500
500
|
// src/tenants/wake.ts
|
|
501
|
-
var WAKE_PRESETS = ["grok-bot", "claude-code", "openclaw-wake", "openclaw-agent", "hermes", "generic"];
|
|
501
|
+
var WAKE_PRESETS = ["grok-bot", "claude-code", "openclaw-wake", "openclaw-agent", "hermes", "openbot", "generic"];
|
|
502
502
|
var PLAIN_HEADERS = /* @__PURE__ */ new Set([
|
|
503
503
|
"content-type",
|
|
504
504
|
"user-agent",
|
|
505
505
|
"x-request-id",
|
|
506
506
|
"idempotency-key",
|
|
507
507
|
"anthropic-version",
|
|
508
|
-
"x-webhook-timestamp"
|
|
508
|
+
"x-webhook-timestamp",
|
|
509
|
+
"x-openbot-timestamp",
|
|
510
|
+
"x-openbot-delivery-id"
|
|
509
511
|
]);
|
|
510
512
|
var HEADER_NAME = /^[A-Za-z0-9-]{1,64}$/;
|
|
511
513
|
function configFields(text) {
|
|
@@ -552,15 +554,17 @@ async function tenantForAccount(db, accountId, name) {
|
|
|
552
554
|
}
|
|
553
555
|
async function saveWakeSettings(db, plane, secretsKey, input) {
|
|
554
556
|
const row = await tenantForAccount(db, input.accountId, input.name);
|
|
555
|
-
if (!row) return { ok: false, error: "conflict" };
|
|
557
|
+
if (!row || input.expectedVersion !== void 0 && input.expectedVersion !== row.config_version) return { ok: false, error: "conflict" };
|
|
556
558
|
const url = input.url.trim();
|
|
557
559
|
const preset = input.preset.trim();
|
|
558
560
|
const keyHeader = input.keyHeader.trim();
|
|
559
561
|
const key = input.key.trim();
|
|
562
|
+
const hmacSecret = (input.hmacSecret ?? "").trim();
|
|
560
563
|
if (wakeUrlProblem(url)) return { ok: false, error: "url" };
|
|
561
564
|
if (!WAKE_PRESETS.includes(preset)) return { ok: false, error: "preset" };
|
|
562
565
|
if (wakeHeaderProblem(keyHeader)) return { ok: false, error: "header" };
|
|
563
|
-
if (key.length > 4096 || /[\u0000-\u001f]/.test(
|
|
566
|
+
if ([key, hmacSecret].some((value) => value.length > 4096 || /[\u0000-\u001f]/.test(value))) return { ok: false, error: "key" };
|
|
567
|
+
if (url && preset === "openbot" && (key || keyHeader)) return { ok: false, error: keyHeader ? "header" : "key" };
|
|
564
568
|
const config = configFields(row.config_json);
|
|
565
569
|
const plaintext = {};
|
|
566
570
|
for (const secret of WAKE_SECRET_KEYS) {
|
|
@@ -568,23 +572,32 @@ async function saveWakeSettings(db, plane, secretsKey, input) {
|
|
|
568
572
|
if (value) plaintext[secret] = value;
|
|
569
573
|
delete config[secret];
|
|
570
574
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
config.WAKE_PRESET = preset;
|
|
574
|
-
if (keyHeader) config.WAKE_KEY_HEADER = keyHeader;
|
|
575
|
-
else delete config.WAKE_KEY_HEADER;
|
|
575
|
+
const same = config.WAKE_WEBHOOK_URL === url && config.WAKE_PRESET === preset;
|
|
576
|
+
const sameKey = same && (config.WAKE_KEY_HEADER ?? "") === keyHeader;
|
|
576
577
|
let secretsEnc = row.secrets_enc;
|
|
577
|
-
const
|
|
578
|
-
if (key ||
|
|
578
|
+
const hasSecrets = !!secretsEnc || Object.keys(plaintext).length > 0;
|
|
579
|
+
if (hasSecrets || key || hmacSecret || url && preset === "openbot") {
|
|
579
580
|
const ikm = secretsKey.trim();
|
|
580
581
|
if (!ikm) return { ok: false, error: "unavailable" };
|
|
581
582
|
const current = secretsEnc ? await openWakeSecrets(ikm, row.id, secretsEnc) : { ...plaintext };
|
|
582
583
|
if (!current) return { ok: false, error: "unreadable" };
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (!
|
|
586
|
-
|
|
584
|
+
const before = JSON.stringify(current);
|
|
585
|
+
if (!url || !sameKey || preset === "openbot") delete current.WAKE_WEBHOOK_KEY;
|
|
586
|
+
if (!url || !same) delete current.WAKE_HMAC_SECRET;
|
|
587
|
+
if (url && key && preset !== "openbot") current.WAKE_WEBHOOK_KEY = key;
|
|
588
|
+
if (url && hmacSecret) current.WAKE_HMAC_SECRET = hmacSecret;
|
|
589
|
+
if (url && preset === "openbot" && !current.WAKE_HMAC_SECRET) return { ok: false, error: "key" };
|
|
590
|
+
if (!secretsEnc || JSON.stringify(current) !== before) {
|
|
591
|
+
const sealed = await sealWakeSecrets(ikm, row.id, current);
|
|
592
|
+
if (!sealed) return { ok: false, error: "unavailable" };
|
|
593
|
+
secretsEnc = sealed;
|
|
594
|
+
}
|
|
587
595
|
}
|
|
596
|
+
if (url) config.WAKE_WEBHOOK_URL = url;
|
|
597
|
+
else delete config.WAKE_WEBHOOK_URL;
|
|
598
|
+
config.WAKE_PRESET = preset;
|
|
599
|
+
if (keyHeader && url) config.WAKE_KEY_HEADER = keyHeader;
|
|
600
|
+
else delete config.WAKE_KEY_HEADER;
|
|
588
601
|
const version = row.config_version + 1;
|
|
589
602
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
590
603
|
const configJson = JSON.stringify(config);
|
|
@@ -615,8 +628,8 @@ async function saveWakeSettings(db, plane, secretsKey, input) {
|
|
|
615
628
|
console.log(JSON.stringify({ ts: now, event: "wake_settings", tenant: row.id, accountId: input.accountId, preset }));
|
|
616
629
|
try {
|
|
617
630
|
await deliverOutbox(db, plane, outbox);
|
|
618
|
-
} catch
|
|
619
|
-
console.log(JSON.stringify({ ts: now, event: "outbox_push_failed", tenant: row.id, error:
|
|
631
|
+
} catch {
|
|
632
|
+
console.log(JSON.stringify({ ts: now, event: "outbox_push_failed", tenant: row.id, error: "wake_delivery_failed" }));
|
|
620
633
|
}
|
|
621
634
|
return { ok: true };
|
|
622
635
|
}
|
|
@@ -3019,6 +3032,9 @@ var conversation_owner_default = "-- One owner per context id. The first peer to
|
|
|
3019
3032
|
// sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/worker/migrations/0012_conversation_peer_owner.sql
|
|
3020
3033
|
var conversation_peer_owner_default = "-- One row per peer. The same context id can be a separate conversation for each peer.\n-- 0011's single-column primary key made a second peer's insert fail, which is how a caller\n-- could tell a fresh id from one another peer already held.\nCREATE TABLE conversation_owners_peer (\n context_id TEXT NOT NULL,\n peer TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (context_id, peer)\n);\nINSERT INTO conversation_owners_peer (context_id, peer, created_at)\nSELECT context_id, peer, created_at FROM conversation_owners;\nDROP TABLE conversation_owners;\nALTER TABLE conversation_owners_peer RENAME TO conversation_owners;\n";
|
|
3021
3034
|
|
|
3035
|
+
// sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/worker/migrations/0013_reply_delivery.sql
|
|
3036
|
+
var reply_delivery_default = "-- One delivery outcome for one logical owner reply. The key includes the inbound turn and reply content,\n-- so an HTTP/MCP retry is idempotent while a later peer message permits the same words to be sent again.\nCREATE TABLE IF NOT EXISTS reply_deliveries (\n delivery_key TEXT PRIMARY KEY,\n task_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n result_json TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS reply_deliveries_task ON reply_deliveries(task_id, created_at);\n";
|
|
3037
|
+
|
|
3022
3038
|
// src/provision/inbox-migrations.ts
|
|
3023
3039
|
var INBOX_MIGRATIONS = [
|
|
3024
3040
|
{ name: "0001_init.sql", sql: init_default },
|
|
@@ -3032,7 +3048,8 @@ var INBOX_MIGRATIONS = [
|
|
|
3032
3048
|
{ name: "0009_outbound_seen.sql", sql: outbound_seen_default },
|
|
3033
3049
|
{ name: "0010_conversation_peer.sql", sql: conversation_peer_default },
|
|
3034
3050
|
{ name: "0011_conversation_owner.sql", sql: conversation_owner_default },
|
|
3035
|
-
{ name: "0012_conversation_peer_owner.sql", sql: conversation_peer_owner_default }
|
|
3051
|
+
{ name: "0012_conversation_peer_owner.sql", sql: conversation_peer_owner_default },
|
|
3052
|
+
{ name: "0013_reply_delivery.sql", sql: reply_delivery_default }
|
|
3036
3053
|
];
|
|
3037
3054
|
|
|
3038
3055
|
// sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/control/migrations/0001_auth.sql
|
|
@@ -5,7 +5,7 @@ export type { DeleteResult } from "../tenants/delete.js";
|
|
|
5
5
|
export { rotateOwnerToken } from "../tenants/rotate.js";
|
|
6
6
|
export type { RotateResult } from "../tenants/rotate.js";
|
|
7
7
|
export { WAKE_PRESETS, configFields, saveWakeSettings, tenantForAccount, wakeHeaderProblem, wakeSettingsOf, wakeUrlProblem, } from "../tenants/wake.js";
|
|
8
|
-
export type { WakeSave, WakeSettings } from "../tenants/wake.js";
|
|
8
|
+
export type { WakeSave, WakeSaveInput, WakeSettings } from "../tenants/wake.js";
|
|
9
9
|
export { MAX_WAKE_PLAIN, MAX_WAKE_SEALED, MAX_WAKE_VALUE, WAKE_SECRET_KEYS, openWakeSecrets, sealWakeSecrets, } from "../tenants/wake-secrets.js";
|
|
10
10
|
export type { WakeSecretKey, WakeSecrets } from "../tenants/wake-secrets.js";
|
|
11
11
|
export { RESERVED_NAMES, TENANT_NAME, tenantNameProblem } from "../tenants/reserved.js";
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import type { Sql } from "../core/sql.js";
|
|
2
2
|
import { type DataPlane } from "./push.js";
|
|
3
3
|
/** The same list the Worker renders. Claude Code is one of them. */
|
|
4
|
-
export declare const WAKE_PRESETS: readonly ["grok-bot", "claude-code", "openclaw-wake", "openclaw-agent", "hermes", "generic"];
|
|
4
|
+
export declare const WAKE_PRESETS: readonly ["grok-bot", "claude-code", "openclaw-wake", "openclaw-agent", "hermes", "openbot", "generic"];
|
|
5
5
|
export interface WakeSettings {
|
|
6
6
|
url: string;
|
|
7
7
|
preset: string;
|
|
8
8
|
keyHeader: string;
|
|
9
9
|
}
|
|
10
|
+
/** Blank credentials retain a saved value only for the same URL and authentication mode.
|
|
11
|
+
* Callers validate public DNS before saving; the Worker enforces its delivery policy separately. */
|
|
12
|
+
export interface WakeSaveInput {
|
|
13
|
+
accountId: string;
|
|
14
|
+
name: string;
|
|
15
|
+
url: string;
|
|
16
|
+
preset: string;
|
|
17
|
+
keyHeader: string;
|
|
18
|
+
key: string;
|
|
19
|
+
hmacSecret?: string;
|
|
20
|
+
/** Version of the settings the caller tested. A stale version makes no write. */
|
|
21
|
+
expectedVersion?: number;
|
|
22
|
+
}
|
|
10
23
|
export type WakeSave = {
|
|
11
24
|
ok: true;
|
|
12
25
|
} | {
|
|
@@ -32,12 +45,5 @@ export declare function wakeHeaderProblem(value: string): string;
|
|
|
32
45
|
export declare function wakeSettingsOf(config: Record<string, string>): WakeSettings;
|
|
33
46
|
/** The signed-in account's agent, or null. Another account's agent is the same result. */
|
|
34
47
|
export declare function tenantForAccount(db: Sql, accountId: string, name: string): Promise<TenantRow | null>;
|
|
35
|
-
export declare function saveWakeSettings(db: Sql, plane: DataPlane, secretsKey: string, input:
|
|
36
|
-
accountId: string;
|
|
37
|
-
name: string;
|
|
38
|
-
url: string;
|
|
39
|
-
preset: string;
|
|
40
|
-
keyHeader: string;
|
|
41
|
-
key: string;
|
|
42
|
-
}): Promise<WakeSave>;
|
|
48
|
+
export declare function saveWakeSettings(db: Sql, plane: DataPlane, secretsKey: string, input: WakeSaveInput): Promise<WakeSave>;
|
|
43
49
|
export {};
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a2a-exposed/control-core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.20",
|
|
4
4
|
"description": "A2A endpoint provisioning, routing, and D1 migrations for a2a-exposed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"lint": "node scripts/check-repo.mjs lint",
|
|
8
8
|
"format:check": "node scripts/check-repo.mjs format",
|
|
9
|
-
"test:checks": "node --test scripts
|
|
9
|
+
"test:checks": "node --test scripts/*.test.mjs",
|
|
10
|
+
"version:set": "node scripts/set-version.mjs",
|
|
10
11
|
"prepare": "node control/scripts/build-core.mjs",
|
|
11
12
|
"prepack": "node control/scripts/build-core.mjs"
|
|
12
13
|
},
|