@acnlabs/acn-cli 0.14.1 → 0.14.2

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.
Files changed (3) hide show
  1. package/README.md +8 -5
  2. package/dist/index.js +124 -40
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -91,11 +91,13 @@ the A2A reply. Dedupe is on by default (`--no-dedupe` to disable).
91
91
 
92
92
  **Chat writeback (Interfaze / Chat Gateway):** when the relayed message carries
93
93
  `metadata.agentplanet.chat_id` + `reply_path`, enable the CLI to complete a
94
- host reply and POST `agent-messages` (hosts only return `{"content":"..."}`):
94
+ host reply and POST `agent-messages` (hosts only return `{"content":"..."}`).
95
+ Auth uses an **ACN agent JWT** minted from your config `api_key` (no
96
+ AgentPlanet Internal Token):
95
97
 
96
98
  ```bash
97
- export AGENTPLANET_API_BASE=https://api.example.com
98
- export AGENTPLANET_INTERNAL_TOKEN=… # or AGENTPLANET_INTERNAL_API_TOKEN
99
+ export AGENTPLANET_API_BASE=https://api.agentplanet.org
100
+ # optional: ACN_CHAT_JWT_AUDIENCE=https://api.agentplanet.org
99
101
 
100
102
  acn listen --runtime http \
101
103
  --wake-url http://127.0.0.1:10122/hooks/agent \
@@ -104,8 +106,9 @@ acn listen --runtime http \
104
106
  # or: --chat-complete-exec 'python3 /path/to/complete.py'
105
107
  ```
106
108
 
107
- Task / Org wakes still use `--wake-url` / `--wake-exec`. Chat envelopes skip
108
- wake and use the complete → writeback path instead.
109
+ `--chat-token` is deprecated/ignored. Task / Org wakes still use `--wake-url` /
110
+ `--wake-exec`. Chat envelopes skip wake and use the complete → writeback path
111
+ instead.
109
112
 
110
113
  **Coverage:** only traffic that arrives over the Mode B relay. Open Task Pool
111
114
  rows that were never pushed as A2A still need `acn tasks list` / reconcile.
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "@acnlabs/acn-cli",
34
- version: "0.14.1",
34
+ version: "0.14.2",
35
35
  description: "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
36
36
  main: "dist/index.js",
37
37
  bin: {
@@ -469,7 +469,9 @@ function rotateKeyCommand() {
469
469
  }
470
470
  if (!config.api_key) {
471
471
  console.error(
472
- "No API key found in ~/.acn/config.json. The CLI rotates with the current agent key; if you have lost the key, recover via the Labs web UI (Auth0-authorised owner-side rotation)."
472
+ `No API key found in ~/.acn/config.json. The CLI rotates with the current agent key.
473
+ If you have lost the key, sign in to Labs as the agent owner, open /agents/${agentId}, click "Reset API Key", then run:
474
+ acn config set api_key <new>`
473
475
  );
474
476
  process.exit(1);
475
477
  }
@@ -1539,6 +1541,8 @@ var DedupeStore = class {
1539
1541
  // src/commands/chat-writeback.ts
1540
1542
  var DEFAULT_COMPLETE_TIMEOUT_MS = 12e4;
1541
1543
  var DEFAULT_WRITEBACK_TIMEOUT_MS = 3e4;
1544
+ var DEFAULT_CHAT_JWT_AUDIENCE = "https://api.agentplanet.org";
1545
+ var cachedJwt = null;
1542
1546
  function asRecord2(v) {
1543
1547
  return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
1544
1548
  }
@@ -1547,12 +1551,12 @@ function validateChatWritebackOptions(opts) {
1547
1551
  if (!opts.agentId) {
1548
1552
  return "--chat-writeback requires a known agent id (join / --agent-id).";
1549
1553
  }
1554
+ if (!opts.apiKey?.trim()) {
1555
+ return "--chat-writeback requires an ACN API key (acn join / config set api-key).";
1556
+ }
1550
1557
  if (!opts.chatApiBase?.trim()) {
1551
1558
  return "--chat-writeback requires --chat-api-base (or ACN_CHAT_API_BASE / AGENTPLANET_API_BASE).";
1552
1559
  }
1553
- if (!opts.chatToken?.trim()) {
1554
- return "--chat-writeback requires --chat-token (or ACN_CHAT_WRITEBACK_TOKEN / AGENTPLANET_INTERNAL_TOKEN / AGENTPLANET_INTERNAL_API_TOKEN).";
1555
- }
1556
1560
  const hasUrl = Boolean(opts.chatCompleteUrl?.trim());
1557
1561
  const hasExec = Boolean(opts.chatCompleteExec?.trim());
1558
1562
  if (hasUrl === hasExec) {
@@ -1562,11 +1566,15 @@ function validateChatWritebackOptions(opts) {
1562
1566
  }
1563
1567
  function buildChatWritebackOptions(opts) {
1564
1568
  if (!opts.chatWriteback) return void 0;
1569
+ const apiBase = opts.chatApiBase.replace(/\/+$/, "");
1570
+ const audience = opts.audience?.trim() || DEFAULT_CHAT_JWT_AUDIENCE;
1565
1571
  return {
1566
1572
  enabled: true,
1567
- apiBase: opts.chatApiBase.replace(/\/+$/, ""),
1568
- token: opts.chatToken,
1573
+ apiBase,
1574
+ acnBaseUrl: opts.acnBaseUrl.replace(/\/+$/, ""),
1575
+ apiKey: opts.apiKey,
1569
1576
  agentId: opts.agentId,
1577
+ audience,
1570
1578
  completeUrl: opts.chatCompleteUrl?.trim() || void 0,
1571
1579
  completeExec: opts.chatCompleteExec?.trim() || void 0,
1572
1580
  completeTimeoutMs: opts.chatCompleteTimeoutMs,
@@ -1586,6 +1594,51 @@ function extractContent(payload) {
1586
1594
  }
1587
1595
  return null;
1588
1596
  }
1597
+ async function mintAgentJwt(opts, fetchFn = fetch) {
1598
+ const now = Math.floor(Date.now() / 1e3);
1599
+ if (cachedJwt && cachedJwt.agentId === opts.agentId && cachedJwt.expEpochSec > now + 60) {
1600
+ return { ok: true, token: cachedJwt.token };
1601
+ }
1602
+ const url = `${opts.acnBaseUrl.replace(/\/+$/, "")}/oauth/token`;
1603
+ try {
1604
+ const res = await fetchFn(url, {
1605
+ method: "POST",
1606
+ headers: { "content-type": "application/json" },
1607
+ body: JSON.stringify({
1608
+ grant_type: "client_credentials",
1609
+ client_id: opts.agentId,
1610
+ client_secret: opts.apiKey,
1611
+ audience: opts.audience
1612
+ })
1613
+ });
1614
+ const text = await res.text();
1615
+ if (res.status < 200 || res.status >= 300) {
1616
+ return { ok: false, reason: `oauth_http_${res.status}` };
1617
+ }
1618
+ let parsed;
1619
+ try {
1620
+ parsed = JSON.parse(text);
1621
+ } catch {
1622
+ return { ok: false, reason: "oauth_invalid_json" };
1623
+ }
1624
+ const rec = asRecord2(parsed);
1625
+ const token = typeof rec?.access_token === "string" ? rec.access_token.trim() : "";
1626
+ if (!token) return { ok: false, reason: "oauth_missing_access_token" };
1627
+ const expiresIn = typeof rec?.expires_in === "number" && rec.expires_in > 0 ? rec.expires_in : 1800;
1628
+ cachedJwt = {
1629
+ token,
1630
+ agentId: opts.agentId,
1631
+ expEpochSec: now + expiresIn
1632
+ };
1633
+ return { ok: true, token };
1634
+ } catch (err) {
1635
+ const msg = err instanceof Error ? err.message : String(err);
1636
+ return { ok: false, reason: msg.slice(0, 200) };
1637
+ }
1638
+ }
1639
+ function clearAgentJwtCache() {
1640
+ cachedJwt = null;
1641
+ }
1589
1642
  async function completeViaHttp(event, opts, deps) {
1590
1643
  const fetchFn = deps.fetchFn ?? fetch;
1591
1644
  const timeoutMs = opts.completeTimeoutMs ?? DEFAULT_COMPLETE_TIMEOUT_MS;
@@ -1693,37 +1746,62 @@ async function postWriteback(event, content, opts, deps) {
1693
1746
  if (url.origin !== baseOrigin) {
1694
1747
  return { ok: false, reason: "reply_url_origin_mismatch" };
1695
1748
  }
1696
- url.searchParams.set("agent_id", opts.agentId);
1697
1749
  const timeoutMs = opts.writebackTimeoutMs ?? DEFAULT_WRITEBACK_TIMEOUT_MS;
1698
- const controller = new AbortController();
1699
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1700
- try {
1701
- const res = await fetchFn(url.toString(), {
1702
- method: "POST",
1703
- headers: {
1704
- "content-type": "application/json",
1705
- "X-Internal-Token": opts.token
1706
- },
1707
- body: JSON.stringify({ content }),
1708
- signal: controller.signal
1709
- });
1710
- if (res.status === 200 || res.status === 201) {
1711
- return { ok: true, httpStatus: res.status };
1712
- }
1750
+ const postOnce = async (token) => {
1751
+ const controller = new AbortController();
1752
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1713
1753
  try {
1714
- await res.arrayBuffer();
1715
- } catch {
1754
+ const res = await fetchFn(url.toString(), {
1755
+ method: "POST",
1756
+ headers: {
1757
+ "content-type": "application/json",
1758
+ Authorization: `Bearer ${token}`
1759
+ },
1760
+ body: JSON.stringify({ content }),
1761
+ signal: controller.signal
1762
+ });
1763
+ if (res.status === 200 || res.status === 201) {
1764
+ return { ok: true, status: res.status };
1765
+ }
1766
+ try {
1767
+ await res.arrayBuffer();
1768
+ } catch {
1769
+ }
1770
+ return {
1771
+ ok: false,
1772
+ status: res.status,
1773
+ reason: `writeback_http_${res.status}`
1774
+ };
1775
+ } catch (err) {
1776
+ const msg = err instanceof Error ? err.message : String(err);
1777
+ if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
1778
+ return { ok: false, status: 0, reason: "writeback_timeout" };
1779
+ }
1780
+ return { ok: false, status: 0, reason: msg.slice(0, 200) };
1781
+ } finally {
1782
+ clearTimeout(timer);
1716
1783
  }
1717
- return { ok: false, reason: `writeback_http_${res.status}` };
1718
- } catch (err) {
1719
- const msg = err instanceof Error ? err.message : String(err);
1720
- if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
1721
- return { ok: false, reason: "writeback_timeout" };
1784
+ };
1785
+ const minted = await mintAgentJwt(opts, fetchFn);
1786
+ if (!minted.ok) {
1787
+ return { ok: false, reason: minted.reason };
1788
+ }
1789
+ let result = await postOnce(minted.token);
1790
+ if (result.ok) {
1791
+ return { ok: true, httpStatus: result.status };
1792
+ }
1793
+ if (result.status === 401) {
1794
+ clearAgentJwtCache();
1795
+ const reminted = await mintAgentJwt(opts, fetchFn);
1796
+ if (!reminted.ok) {
1797
+ return { ok: false, reason: reminted.reason };
1798
+ }
1799
+ result = await postOnce(reminted.token);
1800
+ if (result.ok) {
1801
+ return { ok: true, httpStatus: result.status };
1722
1802
  }
1723
- return { ok: false, reason: msg.slice(0, 200) };
1724
- } finally {
1725
- clearTimeout(timer);
1726
1803
  }
1804
+ return { ok: false, reason: result.reason };
1727
1805
  }
1728
1806
  async function handleChatWriteback(event, opts, deps = {}) {
1729
1807
  const logFn = deps.logFn ?? ((line) => console.error(line));
@@ -2255,13 +2333,13 @@ function listenCommand() {
2255
2333
  "Compat: shell per request; stdout must be a full A2A JSON-RPC response"
2256
2334
  ).option(
2257
2335
  "--chat-writeback",
2258
- "On Chat Gateway envelopes: complete host reply then POST agent-messages (requires --runtime)"
2336
+ "On Chat Gateway envelopes: complete host reply then POST agent-messages with ACN agent JWT (requires --runtime; uses config api-key)"
2259
2337
  ).option(
2260
2338
  "--chat-api-base <url>",
2261
2339
  "Chat Gateway origin for writeback (env: ACN_CHAT_API_BASE or AGENTPLANET_API_BASE)"
2262
2340
  ).option(
2263
2341
  "--chat-token <token>",
2264
- "X-Internal-Token for agent-messages (env: ACN_CHAT_WRITEBACK_TOKEN, AGENTPLANET_INTERNAL_TOKEN, or AGENTPLANET_INTERNAL_API_TOKEN)"
2342
+ "Deprecated/ignored: writeback now mints ACN agent JWT from api-key"
2265
2343
  ).option(
2266
2344
  "--chat-complete-url <url>",
2267
2345
  'POST NormalizedEvent \u2192 JSON {"content":"..."} (mutually exclusive with --chat-complete-exec)'
@@ -2301,20 +2379,24 @@ function listenCommand() {
2301
2379
  process.exit(1);
2302
2380
  }
2303
2381
  const chatApiBase = opts.chatApiBase?.trim() || process.env.ACN_CHAT_API_BASE?.trim() || process.env.AGENTPLANET_API_BASE?.trim();
2304
- const chatToken = opts.chatToken?.trim() || process.env.ACN_CHAT_WRITEBACK_TOKEN?.trim() || process.env.AGENTPLANET_INTERNAL_TOKEN?.trim() || process.env.AGENTPLANET_INTERNAL_API_TOKEN?.trim();
2305
2382
  const chatCompleteUrl = opts.chatCompleteUrl?.trim() || process.env.ACN_CHAT_COMPLETE_URL?.trim();
2306
2383
  const chatCompleteExec = opts.chatCompleteExec?.trim();
2307
2384
  if (opts.chatWriteback && !opts.runtime) {
2308
2385
  console.error("--chat-writeback requires --runtime http|command|log.");
2309
2386
  process.exit(1);
2310
2387
  }
2388
+ if (opts.chatWriteback && (opts.chatToken?.trim() || process.env.ACN_CHAT_WRITEBACK_TOKEN?.trim() || process.env.AGENTPLANET_INTERNAL_TOKEN?.trim() || process.env.AGENTPLANET_INTERNAL_API_TOKEN?.trim())) {
2389
+ console.error(
2390
+ "[acn listen] warning: --chat-token / AGENTPLANET_INTERNAL_TOKEN is ignored; writeback authenticates with ACN agent JWT minted from your api-key."
2391
+ );
2392
+ }
2311
2393
  const chatErr = validateChatWritebackOptions({
2312
2394
  chatWriteback: opts.chatWriteback,
2313
2395
  chatApiBase,
2314
- chatToken,
2315
2396
  chatCompleteUrl,
2316
2397
  chatCompleteExec,
2317
- agentId
2398
+ agentId,
2399
+ apiKey
2318
2400
  });
2319
2401
  if (chatErr) {
2320
2402
  console.error(chatErr);
@@ -2350,11 +2432,13 @@ function listenCommand() {
2350
2432
  const chatWriteback = buildChatWritebackOptions({
2351
2433
  chatWriteback: opts.chatWriteback,
2352
2434
  chatApiBase,
2353
- chatToken,
2435
+ acnBaseUrl: config.base_url,
2436
+ apiKey,
2354
2437
  chatCompleteUrl,
2355
2438
  chatCompleteExec,
2356
2439
  chatCompleteTimeoutMs: opts.chatWriteback ? chatCompleteTimeoutMs : void 0,
2357
- agentId
2440
+ agentId,
2441
+ audience: process.env.ACN_CHAT_JWT_AUDIENCE?.trim() || process.env.AGENTPLANET_JWT_AUDIENCE?.trim()
2358
2442
  });
2359
2443
  runListener({
2360
2444
  agentId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acnlabs/acn-cli",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
4
4
  "description": "Official CLI for ACN (Agent Collaboration Network) — zero-integration agent access",
5
5
  "main": "dist/index.js",
6
6
  "bin": {