a2a-exposed 0.8.39 → 0.8.40

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/hosted.mjs CHANGED
@@ -4,7 +4,7 @@ import * as C from "./config.mjs";
4
4
  import { controlOrigin, deviceLogin } from "./account.mjs";
5
5
  import { CliError, upstreamUrlProblem } from "./a2a.mjs";
6
6
 
7
- export const SETUP_SCOPES = ["agents:read", "agents:create", "agents:owner-token", "wake:write", "wake:test"];
7
+ export const SETUP_SCOPES = ["agents:read", "agents:create", "agents:owner-token", "wake:write", "wake:test", "upstream:write", "upstream:test"];
8
8
  const SETUP_UNSUPPORTED = "This service does not expose scoped hosted setup capabilities. Check the --control-url service origin. Use the service dashboard to create an agent and configure Wake settings, or hosted MCP create_agent/set_wake with human confirmation and secret entry. Do not run init or deploy to repair a hosted address; no setup approval was requested.";
9
9
  const NEXT = "Run hosted capabilities --json to list scopes and presets; login --machine NAME --scopes SCOPES to approve setup.";
10
10
  const fail = (code, next) => { const error = new CliError(next); error.result = { ok: false, code, next }; throw error; };
@@ -62,6 +62,7 @@ async function request(origin, path, { token = "", method = "GET", body, fetchIm
62
62
  const code = known.includes(data.error) ? data.error : `http_${response.status}`;
63
63
  const next = code === "taken" || code === "reserved_name" ? "Choose another --name using hosted names --name NAME --json before creating."
64
64
  : code === "limit" ? "The account is at its agent limit. Read hosted capacity --json and review account plans."
65
+ : code === "conflict" && path.includes("/upstream") ? "Upstream settings changed. Run hosted upstream preview --agent NAME --json, then repeat with its explicit --version."
65
66
  : code === "conflict" ? "Wake settings changed. Run wake preview --json, then test the draft again."
66
67
  : 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."
67
68
  : code === "invalid_grant" || code === "insufficient_scope" ? NEXT
@@ -74,7 +75,7 @@ async function request(origin, path, { token = "", method = "GET", body, fetchIm
74
75
  export async function capabilities(o = {}) {
75
76
  const data = await request(originFor(o), "/api/v1/setup/capabilities");
76
77
  if (!Array.isArray(data.scopes) || !Array.isArray(data.wake?.presets)) fail("unsupported", SETUP_UNSUPPORTED);
77
- 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)), preflight: data.preflight === true, secret_reference: data.wake.secret_reference === true, wake_removal: data.wake.removal === true, secure_link_create: data.wake.secure_link_create === true };
78
+ 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)), upstream: data.upstream?.configuration === true && data.upstream?.verify === true, preflight: data.preflight === true, secret_reference: data.wake.secret_reference === true, wake_removal: data.wake.removal === true, secure_link_create: data.wake.secure_link_create === true };
78
79
  }
79
80
 
80
81
  export async function login(o = {}) {
@@ -283,6 +284,34 @@ export async function wake(sub, o = {}) {
283
284
  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." : `Saved settings were not changed. ${recovery[code] || "Check the receiver, credentials and draft, then test again."}` };
284
285
  }
285
286
 
287
+ /** Hosted upstream configuration uses only scoped setup or this agent's owner credential. */
288
+ export async function upstream(sub, o = {}) {
289
+ if (!["preview", "set", "clear", "verify"].includes(sub)) fail("usage", "Use hosted upstream preview|set|clear|verify --agent NAME --json. Select NAME with hosted list --json and scopes with hosted capabilities --json.");
290
+ const allowed = new Set(["agent", "json", "control-url", ...(sub === "preview" ? [] : ["version"]), ...(sub === "set" ? ["url", "upstream-token-file", "upstream-token-env", "access-client-id-file", "access-client-id-env", "access-client-secret-file", "access-client-secret-env"] : [])]);
291
+ if (Object.keys(o).some(key => !allowed.has(key))) fail("usage", "Use only the options for hosted upstream preview|set|clear|verify shown in --help; secret inputs belong only to set.");
292
+ if (sub !== "preview" && (!/^(0|[1-9][0-9]*)$/.test(String(o.version ?? "")) || !Number.isSafeInteger(Number(o.version)))) fail("version", "Pass --version N from hosted upstream preview --agent NAME --json; no version is selected automatically.");
293
+ let body = { version: Number(o.version) };
294
+ if (sub === "set") {
295
+ const url = String(o.url || "");
296
+ if (!url || upstreamUrlProblem(url)) fail("url", "Pass --url with a public HTTPS A2A address; use hosted upstream clear to remove it.");
297
+ body = { ...body, url, token: secret(o, "upstream-token", "UPSTREAM_TOKEN"), accessClientId: secret(o, "access-client-id", "UPSTREAM_ACCESS_CLIENT_ID"), accessClientSecret: secret(o, "access-client-secret", "UPSTREAM_ACCESS_CLIENT_SECRET") };
298
+ if (Boolean(body.accessClientId) !== Boolean(body.accessClientSecret)) fail("access", "Supply both Access credentials through protected files or named environment variables. A new address requires both.");
299
+ }
300
+ const { origin, token, name } = wakeCredential(o, true);
301
+ if (!(await capabilities({ "control-url": origin })).upstream) fail("unsupported", "This service does not expose upstream configuration. Run hosted capabilities --json before selecting an operation.");
302
+ const path = `/api/v1/agents/${encodeURIComponent(name)}/upstream`;
303
+ const current = await request(origin, path, { token });
304
+ if (!Number.isSafeInteger(current.version) || typeof current.configured !== "boolean") fail("invalid_response", "No valid upstream version was returned. Run hosted upstream preview --agent NAME --json before retrying.");
305
+ if (sub === "preview") return { ok: true, code: "upstream", version: current.version, configured: current.configured, next: "Review this version, then pass --version N to set, clear or verify. Addresses and credentials are never printed." };
306
+
307
+ if (Number(o.version) !== current.version) fail("conflict", "Upstream settings changed. Run hosted upstream preview --agent NAME --json, review the version and repeat the explicit operation.");
308
+
309
+ const result = await request(origin, sub === "verify" ? `${path}/verify` : path, { token, method: sub === "verify" ? "POST" : sub === "clear" ? "DELETE" : "PUT", body });
310
+ const reasons = ["reachable", "access_credentials_missing", "access_rejected", "upstream_auth_missing", "upstream_auth_rejected", "tunnel_down", "upstream_unavailable", "network", "unexpected_response", "misconfigured"];
311
+ const ok = result.ok === true && (sub === "verify" ? result.reason === "reachable" : sub === "clear" ? result.removed === true : result.saved === true);
312
+ return { ok, code: sub === "verify" ? reasons.includes(result.reason) ? result.reason : "unexpected_response" : ok ? sub === "clear" ? "removed" : "saved" : "failed", ...(Number.isSafeInteger(result.version) ? { version: result.version } : {}), next: sub === "verify" ? ok ? "The saved upstream is reachable. You may pair and send a message." : "Check the connection layer named by code, then verify again. Keep Access and agent authentication enabled." : "Run hosted upstream preview --json, then verify with its explicit version before pairing." };
313
+ }
314
+
286
315
  export async function output(run, o = {}) {
287
316
  try {
288
317
  const result = await run();
package/lib/main.mjs CHANGED
@@ -36,9 +36,19 @@ Hosted setup:
36
36
  check one name anonymously before device approval; never lists other agents
37
37
  hosted capacity --json read account room with agents:read (or agents:create); reserves nothing
38
38
  hosted list --json list hosted agents with agents:read or agents:create before choosing an agent
39
+ hosted upstream preview|set|clear|verify --agent NAME --json
40
+ list scopes with hosted capabilities and agents with hosted list first
41
+ preview returns version; set/clear/verify require --version N
42
+ set takes --url URL and --upstream-token-file FILE|--upstream-token-env VAR,
43
+ --access-client-id-file FILE|--access-client-id-env VAR and
44
+ --access-client-secret-file FILE|--access-client-secret-env VAR
45
+ files require mode 600; defaults UPSTREAM_TOKEN/UPSTREAM_ACCESS_CLIENT_ID/
46
+ UPSTREAM_ACCESS_CLIENT_SECRET; never pass secret values as arguments
39
47
  hosted create --name NAME --json
40
48
  create and save the owner credential in this config directory;
41
49
  use a separate --config-dir for each agent
50
+ manual fallback: human creates the address in a signed-in dashboard session,
51
+ then saves A2A_BASE_URL + A2A_OWNER_TOKEN in protected config or routine secrets
42
52
  wake preview|test|set --agent NAME [--url URL] [--preset P] --json
43
53
  hosted settings: read, test a draft, or test then save it
44
54
  secrets: --key-file, --key-env, --key-stdin, --hmac-secret-file, --hmac-secret-env,
@@ -400,7 +410,7 @@ const deployOpts = {
400
410
  "workers-dev": B, "workers-dev-subdomain": S, "upstream-token-stdin": B, "no-upstream-token": B, "event-secret-stdin": B,
401
411
  };
402
412
  const SPEC = {
403
- 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 },
413
+ hosted: { url: S, version: S, "upstream-token-file": S, "upstream-token-env": S, "access-client-id-file": S, "access-client-id-env": S, "access-client-secret-file": S, "access-client-secret-env": S, 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 },
404
414
  init: { ...deployOpts, ...tunnelOpts, tunnel: B }, deploy: deployOpts,
405
415
  wake: { ...deployOpts, json: B, url: S, "reference-file": S, "reference-env": S, clear: B, version: 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,
406
416
  inbox: { context: S, from: S, all: B, json: B },
@@ -526,6 +536,7 @@ export async function main(argv) {
526
536
  throw new CliError(`unknown peer action ${sub} (announce|forward)`);
527
537
  }
528
538
  case "hosted": {
539
+ if (p[0] === "upstream" && p.length === 2) return hosted.output(() => hosted.upstream(p[1], o), o);
529
540
  if (p.length === 1 && ["capabilities", "list", "create", "names", "capacity"].includes(p[0])) return hosted.output(() => hosted[p[0]](o), o);
530
541
  if (p.length !== 2 || p[0] !== "link" || !["finish", "create"].includes(p[1])) return linkOutput(linkUsage, o);
531
542
  return linkOutput(() => (p[1] === "create" ? createLink : finishLink)({ ...o, "control-url": o["control-url"] || C.get("CONTROL_URL") }), o);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2a-exposed",
3
- "version": "0.8.39",
3
+ "version": "0.8.40",
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": {
@@ -1,2 +1,2 @@
1
1
  /** Inbox build. Keep this equal to `version` in cli/package.json. */
2
- export const VERSION = "0.8.39";
2
+ export const VERSION = "0.8.40";