@a2a-exposed/control-core 0.8.19 → 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/README.md
CHANGED
|
@@ -10,6 +10,8 @@ Agents that can call out over HTTP and cannot run a public server: hosted assist
|
|
|
10
10
|
|
|
11
11
|
## Quick start
|
|
12
12
|
|
|
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
|
+
|
|
13
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`.
|
|
14
16
|
|
|
15
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.
|
|
@@ -22,6 +24,8 @@ Self-hosting requires Node 22.18+. The skills are installed into the project the
|
|
|
22
24
|
|
|
23
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.
|
|
24
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
|
+
|
|
25
29
|
```bash
|
|
26
30
|
npx -y a2a-exposed@latest init --hostname agent.example.com --agent-name "My Agent"
|
|
27
31
|
```
|
|
@@ -41,7 +45,7 @@ After the skill is in the project, you can ask the agent to set this up. It foll
|
|
|
41
45
|
|
|
42
46
|
## Hosted
|
|
43
47
|
|
|
44
|
-
[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 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).
|
|
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).
|
|
45
49
|
|
|
46
50
|
## Docs
|
|
47
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();
|
|
@@ -554,15 +554,17 @@ async function tenantForAccount(db, accountId, name) {
|
|
|
554
554
|
}
|
|
555
555
|
async function saveWakeSettings(db, plane, secretsKey, input) {
|
|
556
556
|
const row = await tenantForAccount(db, input.accountId, input.name);
|
|
557
|
-
if (!row) return { ok: false, error: "conflict" };
|
|
557
|
+
if (!row || input.expectedVersion !== void 0 && input.expectedVersion !== row.config_version) return { ok: false, error: "conflict" };
|
|
558
558
|
const url = input.url.trim();
|
|
559
559
|
const preset = input.preset.trim();
|
|
560
560
|
const keyHeader = input.keyHeader.trim();
|
|
561
561
|
const key = input.key.trim();
|
|
562
|
+
const hmacSecret = (input.hmacSecret ?? "").trim();
|
|
562
563
|
if (wakeUrlProblem(url)) return { ok: false, error: "url" };
|
|
563
564
|
if (!WAKE_PRESETS.includes(preset)) return { ok: false, error: "preset" };
|
|
564
565
|
if (wakeHeaderProblem(keyHeader)) return { ok: false, error: "header" };
|
|
565
|
-
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" };
|
|
566
568
|
const config = configFields(row.config_json);
|
|
567
569
|
const plaintext = {};
|
|
568
570
|
for (const secret of WAKE_SECRET_KEYS) {
|
|
@@ -570,23 +572,32 @@ async function saveWakeSettings(db, plane, secretsKey, input) {
|
|
|
570
572
|
if (value) plaintext[secret] = value;
|
|
571
573
|
delete config[secret];
|
|
572
574
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
config.WAKE_PRESET = preset;
|
|
576
|
-
if (keyHeader) config.WAKE_KEY_HEADER = keyHeader;
|
|
577
|
-
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;
|
|
578
577
|
let secretsEnc = row.secrets_enc;
|
|
579
|
-
const
|
|
580
|
-
if (key ||
|
|
578
|
+
const hasSecrets = !!secretsEnc || Object.keys(plaintext).length > 0;
|
|
579
|
+
if (hasSecrets || key || hmacSecret || url && preset === "openbot") {
|
|
581
580
|
const ikm = secretsKey.trim();
|
|
582
581
|
if (!ikm) return { ok: false, error: "unavailable" };
|
|
583
582
|
const current = secretsEnc ? await openWakeSecrets(ikm, row.id, secretsEnc) : { ...plaintext };
|
|
584
583
|
if (!current) return { ok: false, error: "unreadable" };
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (!
|
|
588
|
-
|
|
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
|
+
}
|
|
589
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;
|
|
590
601
|
const version = row.config_version + 1;
|
|
591
602
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
592
603
|
const configJson = JSON.stringify(config);
|
|
@@ -617,8 +628,8 @@ async function saveWakeSettings(db, plane, secretsKey, input) {
|
|
|
617
628
|
console.log(JSON.stringify({ ts: now, event: "wake_settings", tenant: row.id, accountId: input.accountId, preset }));
|
|
618
629
|
try {
|
|
619
630
|
await deliverOutbox(db, plane, outbox);
|
|
620
|
-
} catch
|
|
621
|
-
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" }));
|
|
622
633
|
}
|
|
623
634
|
return { ok: true };
|
|
624
635
|
}
|
|
@@ -3021,6 +3032,9 @@ var conversation_owner_default = "-- One owner per context id. The first peer to
|
|
|
3021
3032
|
// sql-raw:/home/runner/work/a2a-exposed/a2a-exposed/worker/migrations/0012_conversation_peer_owner.sql
|
|
3022
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";
|
|
3023
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
|
+
|
|
3024
3038
|
// src/provision/inbox-migrations.ts
|
|
3025
3039
|
var INBOX_MIGRATIONS = [
|
|
3026
3040
|
{ name: "0001_init.sql", sql: init_default },
|
|
@@ -3034,7 +3048,8 @@ var INBOX_MIGRATIONS = [
|
|
|
3034
3048
|
{ name: "0009_outbound_seen.sql", sql: outbound_seen_default },
|
|
3035
3049
|
{ name: "0010_conversation_peer.sql", sql: conversation_peer_default },
|
|
3036
3050
|
{ name: "0011_conversation_owner.sql", sql: conversation_owner_default },
|
|
3037
|
-
{ 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 }
|
|
3038
3053
|
];
|
|
3039
3054
|
|
|
3040
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";
|
|
@@ -7,6 +7,19 @@ export interface WakeSettings {
|
|
|
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 {};
|