@acnlabs/acn-cli 0.14.1 → 1.0.0

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 +176 -53
  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: "1.0.0",
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,26 +1551,30 @@ 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) {
1559
- return '--chat-writeback requires exactly one of --chat-complete-url or --chat-complete-exec (host returns JSON {"content":"..."}).';
1563
+ return '--chat-writeback requires exactly one of --chat-complete-url or --chat-complete-exec (host returns JSON {"content":"..."} and optional usage).';
1560
1564
  }
1561
1565
  return null;
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,84 @@ function extractContent(payload) {
1586
1594
  }
1587
1595
  return null;
1588
1596
  }
1597
+ function asNonNegInt(v) {
1598
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0) {
1599
+ return Math.floor(v);
1600
+ }
1601
+ if (typeof v === "string" && v.trim() !== "") {
1602
+ const n = Number(v);
1603
+ if (Number.isFinite(n) && n >= 0) return Math.floor(n);
1604
+ }
1605
+ return null;
1606
+ }
1607
+ function extractUsage(payload) {
1608
+ const rec = asRecord2(payload);
1609
+ if (!rec) return void 0;
1610
+ const usageRec = asRecord2(rec.usage) ?? rec;
1611
+ const input = asNonNegInt(usageRec.input_tokens) ?? asNonNegInt(usageRec.prompt_tokens);
1612
+ const output2 = asNonNegInt(usageRec.output_tokens) ?? asNonNegInt(usageRec.completion_tokens);
1613
+ if (input === null && output2 === null) return void 0;
1614
+ const out = {
1615
+ input_tokens: input ?? 0,
1616
+ output_tokens: output2 ?? 0
1617
+ };
1618
+ const ms = usageRec.meter_source;
1619
+ if (ms === "peer_self" || ms === "gateway" || ms === "runtime_attested" || ms === "protocol") {
1620
+ out.meter_source = ms;
1621
+ }
1622
+ return out;
1623
+ }
1624
+ function parseCompletePayload(payload) {
1625
+ const content = extractContent(payload);
1626
+ if (!content) return { ok: false, reason: "complete_missing_content" };
1627
+ const usage = extractUsage(payload);
1628
+ return { ok: true, result: usage ? { content, usage } : { content } };
1629
+ }
1630
+ async function mintAgentJwt(opts, fetchFn = fetch) {
1631
+ const now = Math.floor(Date.now() / 1e3);
1632
+ if (cachedJwt && cachedJwt.agentId === opts.agentId && cachedJwt.expEpochSec > now + 60) {
1633
+ return { ok: true, token: cachedJwt.token };
1634
+ }
1635
+ const url = `${opts.acnBaseUrl.replace(/\/+$/, "")}/oauth/token`;
1636
+ try {
1637
+ const res = await fetchFn(url, {
1638
+ method: "POST",
1639
+ headers: { "content-type": "application/json" },
1640
+ body: JSON.stringify({
1641
+ grant_type: "client_credentials",
1642
+ client_id: opts.agentId,
1643
+ client_secret: opts.apiKey,
1644
+ audience: opts.audience
1645
+ })
1646
+ });
1647
+ const text = await res.text();
1648
+ if (res.status < 200 || res.status >= 300) {
1649
+ return { ok: false, reason: `oauth_http_${res.status}` };
1650
+ }
1651
+ let parsed;
1652
+ try {
1653
+ parsed = JSON.parse(text);
1654
+ } catch {
1655
+ return { ok: false, reason: "oauth_invalid_json" };
1656
+ }
1657
+ const rec = asRecord2(parsed);
1658
+ const token = typeof rec?.access_token === "string" ? rec.access_token.trim() : "";
1659
+ if (!token) return { ok: false, reason: "oauth_missing_access_token" };
1660
+ const expiresIn = typeof rec?.expires_in === "number" && rec.expires_in > 0 ? rec.expires_in : 1800;
1661
+ cachedJwt = {
1662
+ token,
1663
+ agentId: opts.agentId,
1664
+ expEpochSec: now + expiresIn
1665
+ };
1666
+ return { ok: true, token };
1667
+ } catch (err) {
1668
+ const msg = err instanceof Error ? err.message : String(err);
1669
+ return { ok: false, reason: msg.slice(0, 200) };
1670
+ }
1671
+ }
1672
+ function clearAgentJwtCache() {
1673
+ cachedJwt = null;
1674
+ }
1589
1675
  async function completeViaHttp(event, opts, deps) {
1590
1676
  const fetchFn = deps.fetchFn ?? fetch;
1591
1677
  const timeoutMs = opts.completeTimeoutMs ?? DEFAULT_COMPLETE_TIMEOUT_MS;
@@ -1608,9 +1694,7 @@ async function completeViaHttp(event, opts, deps) {
1608
1694
  } catch {
1609
1695
  return { ok: false, reason: "complete_invalid_json" };
1610
1696
  }
1611
- const content = extractContent(parsed);
1612
- if (!content) return { ok: false, reason: "complete_missing_content" };
1613
- return { ok: true, content };
1697
+ return parseCompletePayload(parsed);
1614
1698
  } catch (err) {
1615
1699
  const msg = err instanceof Error ? err.message : String(err);
1616
1700
  if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
@@ -1658,12 +1742,7 @@ function completeViaExec(event, opts, deps) {
1658
1742
  const text = Buffer.concat(stdout).toString("utf-8").trim();
1659
1743
  try {
1660
1744
  const parsed = JSON.parse(text);
1661
- const content = extractContent(parsed);
1662
- if (!content) {
1663
- finish({ ok: false, reason: "complete_missing_content" });
1664
- return;
1665
- }
1666
- finish({ ok: true, content });
1745
+ finish(parseCompletePayload(parsed));
1667
1746
  } catch {
1668
1747
  finish({ ok: false, reason: "complete_invalid_json" });
1669
1748
  }
@@ -1671,7 +1750,7 @@ function completeViaExec(event, opts, deps) {
1671
1750
  child.stdin?.end(body);
1672
1751
  });
1673
1752
  }
1674
- async function postWriteback(event, content, opts, deps) {
1753
+ async function postWriteback(event, complete, opts, deps) {
1675
1754
  const chat = event.chat;
1676
1755
  if (!chat) return { ok: false, reason: "no_chat_envelope" };
1677
1756
  if (!isAllowedChatReplyPath(chat.chat_id, chat.reply_path)) {
@@ -1693,37 +1772,74 @@ async function postWriteback(event, content, opts, deps) {
1693
1772
  if (url.origin !== baseOrigin) {
1694
1773
  return { ok: false, reason: "reply_url_origin_mismatch" };
1695
1774
  }
1696
- url.searchParams.set("agent_id", opts.agentId);
1697
1775
  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
- }
1776
+ const replyToId = chat.gateway_message_id ?? event.message_id;
1777
+ const body = {
1778
+ content: complete.content,
1779
+ reply_to_id: replyToId
1780
+ };
1781
+ if (complete.usage) {
1782
+ body.usage = {
1783
+ input_tokens: complete.usage.input_tokens,
1784
+ output_tokens: complete.usage.output_tokens,
1785
+ meter_source: complete.usage.meter_source ?? "peer_self"
1786
+ };
1787
+ }
1788
+ const postOnce = async (token) => {
1789
+ const controller = new AbortController();
1790
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1713
1791
  try {
1714
- await res.arrayBuffer();
1715
- } catch {
1792
+ const res = await fetchFn(url.toString(), {
1793
+ method: "POST",
1794
+ headers: {
1795
+ "content-type": "application/json",
1796
+ Authorization: `Bearer ${token}`
1797
+ },
1798
+ body: JSON.stringify(body),
1799
+ signal: controller.signal
1800
+ });
1801
+ if (res.status === 200 || res.status === 201) {
1802
+ return { ok: true, status: res.status };
1803
+ }
1804
+ try {
1805
+ await res.arrayBuffer();
1806
+ } catch {
1807
+ }
1808
+ return {
1809
+ ok: false,
1810
+ status: res.status,
1811
+ reason: `writeback_http_${res.status}`
1812
+ };
1813
+ } catch (err) {
1814
+ const msg = err instanceof Error ? err.message : String(err);
1815
+ if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
1816
+ return { ok: false, status: 0, reason: "writeback_timeout" };
1817
+ }
1818
+ return { ok: false, status: 0, reason: msg.slice(0, 200) };
1819
+ } finally {
1820
+ clearTimeout(timer);
1716
1821
  }
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" };
1822
+ };
1823
+ const minted = await mintAgentJwt(opts, fetchFn);
1824
+ if (!minted.ok) {
1825
+ return { ok: false, reason: minted.reason };
1826
+ }
1827
+ let result = await postOnce(minted.token);
1828
+ if (result.ok) {
1829
+ return { ok: true, httpStatus: result.status };
1830
+ }
1831
+ if (result.status === 401) {
1832
+ clearAgentJwtCache();
1833
+ const reminted = await mintAgentJwt(opts, fetchFn);
1834
+ if (!reminted.ok) {
1835
+ return { ok: false, reason: reminted.reason };
1836
+ }
1837
+ result = await postOnce(reminted.token);
1838
+ if (result.ok) {
1839
+ return { ok: true, httpStatus: result.status };
1722
1840
  }
1723
- return { ok: false, reason: msg.slice(0, 200) };
1724
- } finally {
1725
- clearTimeout(timer);
1726
1841
  }
1842
+ return { ok: false, reason: result.reason };
1727
1843
  }
1728
1844
  async function handleChatWriteback(event, opts, deps = {}) {
1729
1845
  const logFn = deps.logFn ?? ((line) => console.error(line));
@@ -1735,15 +1851,16 @@ async function handleChatWriteback(event, opts, deps = {}) {
1735
1851
  );
1736
1852
  return { ok: false, reason: completed.reason };
1737
1853
  }
1738
- const written = await postWriteback(event, completed.content, opts, deps);
1854
+ const written = await postWriteback(event, completed.result, opts, deps);
1739
1855
  if (!written.ok) {
1740
1856
  logFn(
1741
1857
  `[acn listen] chat_writeback_failed chat_id=${event.chat.chat_id} message_id=${event.message_id} reason=${written.reason}`
1742
1858
  );
1743
1859
  return written;
1744
1860
  }
1861
+ const usageNote = completed.result.usage ? ` usage_in=${completed.result.usage.input_tokens} usage_out=${completed.result.usage.output_tokens}` : "";
1745
1862
  logFn(
1746
- `[acn listen] chat_writeback_ok chat_id=${event.chat.chat_id} message_id=${event.message_id} http=${written.httpStatus}`
1863
+ `[acn listen] chat_writeback_ok chat_id=${event.chat.chat_id} message_id=${event.message_id} http=${written.httpStatus}${usageNote}`
1747
1864
  );
1748
1865
  return written;
1749
1866
  }
@@ -2255,13 +2372,13 @@ function listenCommand() {
2255
2372
  "Compat: shell per request; stdout must be a full A2A JSON-RPC response"
2256
2373
  ).option(
2257
2374
  "--chat-writeback",
2258
- "On Chat Gateway envelopes: complete host reply then POST agent-messages (requires --runtime)"
2375
+ "On Chat Gateway envelopes: complete host reply then POST agent-messages with ACN agent JWT (requires --runtime; uses config api-key)"
2259
2376
  ).option(
2260
2377
  "--chat-api-base <url>",
2261
2378
  "Chat Gateway origin for writeback (env: ACN_CHAT_API_BASE or AGENTPLANET_API_BASE)"
2262
2379
  ).option(
2263
2380
  "--chat-token <token>",
2264
- "X-Internal-Token for agent-messages (env: ACN_CHAT_WRITEBACK_TOKEN, AGENTPLANET_INTERNAL_TOKEN, or AGENTPLANET_INTERNAL_API_TOKEN)"
2381
+ "Deprecated/ignored: writeback now mints ACN agent JWT from api-key"
2265
2382
  ).option(
2266
2383
  "--chat-complete-url <url>",
2267
2384
  'POST NormalizedEvent \u2192 JSON {"content":"..."} (mutually exclusive with --chat-complete-exec)'
@@ -2301,20 +2418,24 @@ function listenCommand() {
2301
2418
  process.exit(1);
2302
2419
  }
2303
2420
  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
2421
  const chatCompleteUrl = opts.chatCompleteUrl?.trim() || process.env.ACN_CHAT_COMPLETE_URL?.trim();
2306
2422
  const chatCompleteExec = opts.chatCompleteExec?.trim();
2307
2423
  if (opts.chatWriteback && !opts.runtime) {
2308
2424
  console.error("--chat-writeback requires --runtime http|command|log.");
2309
2425
  process.exit(1);
2310
2426
  }
2427
+ 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())) {
2428
+ console.error(
2429
+ "[acn listen] warning: --chat-token / AGENTPLANET_INTERNAL_TOKEN is ignored; writeback authenticates with ACN agent JWT minted from your api-key."
2430
+ );
2431
+ }
2311
2432
  const chatErr = validateChatWritebackOptions({
2312
2433
  chatWriteback: opts.chatWriteback,
2313
2434
  chatApiBase,
2314
- chatToken,
2315
2435
  chatCompleteUrl,
2316
2436
  chatCompleteExec,
2317
- agentId
2437
+ agentId,
2438
+ apiKey
2318
2439
  });
2319
2440
  if (chatErr) {
2320
2441
  console.error(chatErr);
@@ -2350,11 +2471,13 @@ function listenCommand() {
2350
2471
  const chatWriteback = buildChatWritebackOptions({
2351
2472
  chatWriteback: opts.chatWriteback,
2352
2473
  chatApiBase,
2353
- chatToken,
2474
+ acnBaseUrl: config.base_url,
2475
+ apiKey,
2354
2476
  chatCompleteUrl,
2355
2477
  chatCompleteExec,
2356
2478
  chatCompleteTimeoutMs: opts.chatWriteback ? chatCompleteTimeoutMs : void 0,
2357
- agentId
2479
+ agentId,
2480
+ audience: process.env.ACN_CHAT_JWT_AUDIENCE?.trim() || process.env.AGENTPLANET_JWT_AUDIENCE?.trim()
2358
2481
  });
2359
2482
  runListener({
2360
2483
  agentId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acnlabs/acn-cli",
3
- "version": "0.14.1",
3
+ "version": "1.0.0",
4
4
  "description": "Official CLI for ACN (Agent Collaboration Network) — zero-integration agent access",
5
5
  "main": "dist/index.js",
6
6
  "bin": {