@acnlabs/acn-cli 0.14.0 → 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.
- package/README.md +21 -0
- package/dist/index.js +524 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,6 +89,27 @@ Semantics: CLI answers `message/send` / `message/stream` with a valid A2A
|
|
|
89
89
|
wake the host. Wake failure is logged (`wake_failed`) and does **not** fail
|
|
90
90
|
the A2A reply. Dedupe is on by default (`--no-dedupe` to disable).
|
|
91
91
|
|
|
92
|
+
**Chat writeback (Interfaze / Chat Gateway):** when the relayed message carries
|
|
93
|
+
`metadata.agentplanet.chat_id` + `reply_path`, enable the CLI to complete a
|
|
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):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
export AGENTPLANET_API_BASE=https://api.agentplanet.org
|
|
100
|
+
# optional: ACN_CHAT_JWT_AUDIENCE=https://api.agentplanet.org
|
|
101
|
+
|
|
102
|
+
acn listen --runtime http \
|
|
103
|
+
--wake-url http://127.0.0.1:10122/hooks/agent \
|
|
104
|
+
--chat-writeback \
|
|
105
|
+
--chat-complete-url http://127.0.0.1:10122/chat/complete
|
|
106
|
+
# or: --chat-complete-exec 'python3 /path/to/complete.py'
|
|
107
|
+
```
|
|
108
|
+
|
|
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.
|
|
112
|
+
|
|
92
113
|
**Coverage:** only traffic that arrives over the Mode B relay. Open Task Pool
|
|
93
114
|
rows that were never pushed as A2A still need `acn tasks list` / reconcile.
|
|
94
115
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
|
@@ -608,6 +610,8 @@ function formatTask(t) {
|
|
|
608
610
|
lines.push(` Reward : ${t.reward} ${t.reward_currency ?? ""}`);
|
|
609
611
|
}
|
|
610
612
|
if (t.subnet_slug) lines.push(` Subnet : ${t.subnet_slug}`);
|
|
613
|
+
const orgId = t.metadata?.org_id;
|
|
614
|
+
if (typeof orgId === "string" && orgId) lines.push(` Org : ${orgId}`);
|
|
611
615
|
if (t.description) lines.push(` Desc : ${t.description.slice(0, 120)}`);
|
|
612
616
|
if (t.created_at) lines.push(` Created : ${t.created_at}`);
|
|
613
617
|
if (t.deadline) lines.push(` Deadline : ${t.deadline}`);
|
|
@@ -697,7 +701,10 @@ ${formatTask(t)}`).join("\n\n")
|
|
|
697
701
|
handleError(err);
|
|
698
702
|
}
|
|
699
703
|
});
|
|
700
|
-
cmd.command("create").description("Create a new task (as agent)").requiredOption("-t, --title <title>", "Task title (min 3 chars)").requiredOption("-d, --description <text>", "Task description (min 10 chars)").requiredOption("--tags <tags>", "Required skill tags, comma-separated (e.g. coding,review)").option("--deadline <hours>", "Deadline in hours (default: 48)", "48").option("--reward <amount>", "Reward amount (default: 0)", "0").option("--currency <currency>", "Reward currency (e.g. USD, USDC, ap_points)", "ap_points").option("--type <type>", "Task type (e.g. coding, general)", "general").option("--max-participants <n>", "Max participants (default: 1)", "1").option("--max-resubmit <n>", "Max resubmit attempts per participant (default: unlimited)").option("--subnet <slug>", "Subnet slug to scope the task to (agent must be a member)").
|
|
704
|
+
cmd.command("create").description("Create a new task (as agent)").requiredOption("-t, --title <title>", "Task title (min 3 chars)").requiredOption("-d, --description <text>", "Task description (min 10 chars)").requiredOption("--tags <tags>", "Required skill tags, comma-separated (e.g. coding,review)").option("--deadline <hours>", "Deadline in hours (default: 48)", "48").option("--reward <amount>", "Reward amount (default: 0)", "0").option("--currency <currency>", "Reward currency (e.g. USD, USDC, ap_points)", "ap_points").option("--type <type>", "Task type (e.g. coding, general)", "general").option("--max-participants <n>", "Max participants (default: 1)", "1").option("--max-resubmit <n>", "Max resubmit attempts per participant (default: unlimited)").option("--subnet <slug>", "Subnet slug to scope the task to (agent must be a member)").option(
|
|
705
|
+
"--org-id <org_id>",
|
|
706
|
+
"Attribute to an Org (metadata.org_id + org_publish; prefer `acn org publish-task`)"
|
|
707
|
+
).action(
|
|
701
708
|
async (opts) => {
|
|
702
709
|
const config = loadConfig();
|
|
703
710
|
if (!config.api_key) {
|
|
@@ -722,6 +729,9 @@ ${formatTask(t)}`).join("\n\n")
|
|
|
722
729
|
if (opts.subnet) {
|
|
723
730
|
body.subnet_slug = opts.subnet;
|
|
724
731
|
}
|
|
732
|
+
if (opts.orgId) {
|
|
733
|
+
body.metadata = { org_id: opts.orgId, org_publish: true };
|
|
734
|
+
}
|
|
725
735
|
try {
|
|
726
736
|
const task = await acnPost("/tasks/agent/create", body);
|
|
727
737
|
output(task, [`Task created!
|
|
@@ -1375,10 +1385,14 @@ ${formatPolicy(res)}`);
|
|
|
1375
1385
|
|
|
1376
1386
|
// src/commands/listen.ts
|
|
1377
1387
|
var import_commander10 = require("commander");
|
|
1378
|
-
var
|
|
1388
|
+
var import_child_process3 = require("child_process");
|
|
1379
1389
|
var import_ws = __toESM(require("ws"));
|
|
1380
1390
|
|
|
1391
|
+
// src/commands/chat-writeback.ts
|
|
1392
|
+
var import_child_process = require("child_process");
|
|
1393
|
+
|
|
1381
1394
|
// src/commands/normalize-event.ts
|
|
1395
|
+
var CHAT_REPLY_CHANNEL = "agentplanet.chat";
|
|
1382
1396
|
function asRecord(v) {
|
|
1383
1397
|
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
1384
1398
|
}
|
|
@@ -1430,6 +1444,44 @@ function extractFromAgent(message) {
|
|
|
1430
1444
|
if (!metadata) return null;
|
|
1431
1445
|
return asNonEmptyString(metadata.from_agent) ?? asNonEmptyString(metadata.fromAgent);
|
|
1432
1446
|
}
|
|
1447
|
+
function extractUserText(message) {
|
|
1448
|
+
const parts = message.parts;
|
|
1449
|
+
if (!Array.isArray(parts)) return null;
|
|
1450
|
+
const chunks = [];
|
|
1451
|
+
for (const part of parts) {
|
|
1452
|
+
const p = asRecord(part);
|
|
1453
|
+
if (!p || p.kind !== "text") continue;
|
|
1454
|
+
const t = asNonEmptyString(p.text);
|
|
1455
|
+
if (t) chunks.push(t);
|
|
1456
|
+
}
|
|
1457
|
+
return chunks.length > 0 ? chunks.join("\n") : null;
|
|
1458
|
+
}
|
|
1459
|
+
function isAllowedChatReplyPath(chatId, replyPath) {
|
|
1460
|
+
if (!chatId || !replyPath) return false;
|
|
1461
|
+
if (replyPath.includes("..") || replyPath.includes("?") || replyPath.includes("#") || replyPath.includes("//") || replyPath.includes("\\") || !replyPath.startsWith("/")) {
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
return replyPath === `/api/chats/${chatId}/agent-messages`;
|
|
1465
|
+
}
|
|
1466
|
+
function extractChatEnvelope(message) {
|
|
1467
|
+
const metadata = asRecord(message.metadata);
|
|
1468
|
+
if (!metadata) return null;
|
|
1469
|
+
const ap = asRecord(metadata.agentplanet);
|
|
1470
|
+
if (!ap) return null;
|
|
1471
|
+
const chatId = asNonEmptyString(ap.chat_id);
|
|
1472
|
+
const replyPath = asNonEmptyString(ap.reply_path);
|
|
1473
|
+
const replyChannel = asNonEmptyString(ap.reply_channel);
|
|
1474
|
+
if (!chatId || !replyPath) return null;
|
|
1475
|
+
if (replyChannel !== CHAT_REPLY_CHANNEL) return null;
|
|
1476
|
+
if (!isAllowedChatReplyPath(chatId, replyPath)) return null;
|
|
1477
|
+
return {
|
|
1478
|
+
chat_id: chatId,
|
|
1479
|
+
reply_path: replyPath,
|
|
1480
|
+
reply_channel: CHAT_REPLY_CHANNEL,
|
|
1481
|
+
gateway_message_id: asNonEmptyString(ap.message_id) ?? asNonEmptyString(ap.messageId),
|
|
1482
|
+
user_text: extractUserText(message)
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1433
1485
|
function normalizeEvent(body, opts = {}) {
|
|
1434
1486
|
const generateId = opts.generateId ?? (() => crypto.randomUUID());
|
|
1435
1487
|
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -1441,11 +1493,16 @@ function normalizeEvent(body, opts = {}) {
|
|
|
1441
1493
|
message_id: extractMessageId(message, generateId),
|
|
1442
1494
|
context_id: extractContextId(message),
|
|
1443
1495
|
from_agent: extractFromAgent(message),
|
|
1496
|
+
chat: extractChatEnvelope(message),
|
|
1444
1497
|
received_at: now().toISOString(),
|
|
1445
1498
|
raw: body
|
|
1446
1499
|
};
|
|
1447
1500
|
}
|
|
1448
1501
|
function dedupeKey(event) {
|
|
1502
|
+
if (event.chat) {
|
|
1503
|
+
const mid = event.chat.gateway_message_id ?? event.message_id;
|
|
1504
|
+
return `chat:${event.chat.chat_id}:${mid}`;
|
|
1505
|
+
}
|
|
1449
1506
|
return event.task_id ?? event.message_id;
|
|
1450
1507
|
}
|
|
1451
1508
|
var DedupeStore = class {
|
|
@@ -1481,11 +1538,299 @@ var DedupeStore = class {
|
|
|
1481
1538
|
}
|
|
1482
1539
|
};
|
|
1483
1540
|
|
|
1541
|
+
// src/commands/chat-writeback.ts
|
|
1542
|
+
var DEFAULT_COMPLETE_TIMEOUT_MS = 12e4;
|
|
1543
|
+
var DEFAULT_WRITEBACK_TIMEOUT_MS = 3e4;
|
|
1544
|
+
var DEFAULT_CHAT_JWT_AUDIENCE = "https://api.agentplanet.org";
|
|
1545
|
+
var cachedJwt = null;
|
|
1546
|
+
function asRecord2(v) {
|
|
1547
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
1548
|
+
}
|
|
1549
|
+
function validateChatWritebackOptions(opts) {
|
|
1550
|
+
if (!opts.chatWriteback) return null;
|
|
1551
|
+
if (!opts.agentId) {
|
|
1552
|
+
return "--chat-writeback requires a known agent id (join / --agent-id).";
|
|
1553
|
+
}
|
|
1554
|
+
if (!opts.apiKey?.trim()) {
|
|
1555
|
+
return "--chat-writeback requires an ACN API key (acn join / config set api-key).";
|
|
1556
|
+
}
|
|
1557
|
+
if (!opts.chatApiBase?.trim()) {
|
|
1558
|
+
return "--chat-writeback requires --chat-api-base (or ACN_CHAT_API_BASE / AGENTPLANET_API_BASE).";
|
|
1559
|
+
}
|
|
1560
|
+
const hasUrl = Boolean(opts.chatCompleteUrl?.trim());
|
|
1561
|
+
const hasExec = Boolean(opts.chatCompleteExec?.trim());
|
|
1562
|
+
if (hasUrl === hasExec) {
|
|
1563
|
+
return '--chat-writeback requires exactly one of --chat-complete-url or --chat-complete-exec (host returns JSON {"content":"..."}).';
|
|
1564
|
+
}
|
|
1565
|
+
return null;
|
|
1566
|
+
}
|
|
1567
|
+
function buildChatWritebackOptions(opts) {
|
|
1568
|
+
if (!opts.chatWriteback) return void 0;
|
|
1569
|
+
const apiBase = opts.chatApiBase.replace(/\/+$/, "");
|
|
1570
|
+
const audience = opts.audience?.trim() || DEFAULT_CHAT_JWT_AUDIENCE;
|
|
1571
|
+
return {
|
|
1572
|
+
enabled: true,
|
|
1573
|
+
apiBase,
|
|
1574
|
+
acnBaseUrl: opts.acnBaseUrl.replace(/\/+$/, ""),
|
|
1575
|
+
apiKey: opts.apiKey,
|
|
1576
|
+
agentId: opts.agentId,
|
|
1577
|
+
audience,
|
|
1578
|
+
completeUrl: opts.chatCompleteUrl?.trim() || void 0,
|
|
1579
|
+
completeExec: opts.chatCompleteExec?.trim() || void 0,
|
|
1580
|
+
completeTimeoutMs: opts.chatCompleteTimeoutMs,
|
|
1581
|
+
writebackTimeoutMs: opts.chatWritebackTimeoutMs
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
function extractContent(payload) {
|
|
1585
|
+
const rec = asRecord2(payload);
|
|
1586
|
+
if (!rec) return null;
|
|
1587
|
+
for (const key of ["content", "reply", "text"]) {
|
|
1588
|
+
const v = rec[key];
|
|
1589
|
+
if (typeof v === "string" && v.trim()) {
|
|
1590
|
+
const t = v.trim();
|
|
1591
|
+
if (t.toLowerCase() === "accepted") continue;
|
|
1592
|
+
return t;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
return null;
|
|
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
|
+
}
|
|
1642
|
+
async function completeViaHttp(event, opts, deps) {
|
|
1643
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1644
|
+
const timeoutMs = opts.completeTimeoutMs ?? DEFAULT_COMPLETE_TIMEOUT_MS;
|
|
1645
|
+
const controller = new AbortController();
|
|
1646
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1647
|
+
try {
|
|
1648
|
+
const res = await fetchFn(opts.completeUrl, {
|
|
1649
|
+
method: "POST",
|
|
1650
|
+
headers: { "content-type": "application/json" },
|
|
1651
|
+
body: JSON.stringify(event),
|
|
1652
|
+
signal: controller.signal
|
|
1653
|
+
});
|
|
1654
|
+
const text = await res.text();
|
|
1655
|
+
if (res.status < 200 || res.status >= 300) {
|
|
1656
|
+
return { ok: false, reason: `complete_http_${res.status}` };
|
|
1657
|
+
}
|
|
1658
|
+
let parsed;
|
|
1659
|
+
try {
|
|
1660
|
+
parsed = JSON.parse(text);
|
|
1661
|
+
} catch {
|
|
1662
|
+
return { ok: false, reason: "complete_invalid_json" };
|
|
1663
|
+
}
|
|
1664
|
+
const content = extractContent(parsed);
|
|
1665
|
+
if (!content) return { ok: false, reason: "complete_missing_content" };
|
|
1666
|
+
return { ok: true, content };
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1669
|
+
if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
|
|
1670
|
+
return { ok: false, reason: "complete_timeout" };
|
|
1671
|
+
}
|
|
1672
|
+
return { ok: false, reason: msg.slice(0, 200) };
|
|
1673
|
+
} finally {
|
|
1674
|
+
clearTimeout(timer);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
function completeViaExec(event, opts, deps) {
|
|
1678
|
+
const spawnFn = deps.spawnFn ?? import_child_process.spawn;
|
|
1679
|
+
const timeoutMs = opts.completeTimeoutMs ?? DEFAULT_COMPLETE_TIMEOUT_MS;
|
|
1680
|
+
const body = Buffer.from(JSON.stringify(event), "utf-8");
|
|
1681
|
+
return new Promise((resolve) => {
|
|
1682
|
+
let settled = false;
|
|
1683
|
+
const child = spawnFn(opts.completeExec, { shell: true });
|
|
1684
|
+
const stdout = [];
|
|
1685
|
+
const stderr = [];
|
|
1686
|
+
const finish = (result) => {
|
|
1687
|
+
if (settled) return;
|
|
1688
|
+
settled = true;
|
|
1689
|
+
clearTimeout(timer);
|
|
1690
|
+
resolve(result);
|
|
1691
|
+
};
|
|
1692
|
+
const timer = setTimeout(() => {
|
|
1693
|
+
child.kill("SIGTERM");
|
|
1694
|
+
finish({ ok: false, reason: "complete_timeout" });
|
|
1695
|
+
}, timeoutMs);
|
|
1696
|
+
child.stdout?.on("data", (d) => stdout.push(Buffer.from(d)));
|
|
1697
|
+
child.stderr?.on("data", (d) => stderr.push(Buffer.from(d)));
|
|
1698
|
+
child.on(
|
|
1699
|
+
"error",
|
|
1700
|
+
(e) => finish({ ok: false, reason: e.message.slice(0, 200) })
|
|
1701
|
+
);
|
|
1702
|
+
child.on("close", (code) => {
|
|
1703
|
+
if (code !== 0) {
|
|
1704
|
+
const detail = Buffer.concat(stderr).toString("utf-8").slice(0, 80);
|
|
1705
|
+
finish({
|
|
1706
|
+
ok: false,
|
|
1707
|
+
reason: detail ? `complete_exit_${code}:${detail}` : `complete_exit_${code}`
|
|
1708
|
+
});
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
const text = Buffer.concat(stdout).toString("utf-8").trim();
|
|
1712
|
+
try {
|
|
1713
|
+
const parsed = JSON.parse(text);
|
|
1714
|
+
const content = extractContent(parsed);
|
|
1715
|
+
if (!content) {
|
|
1716
|
+
finish({ ok: false, reason: "complete_missing_content" });
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
finish({ ok: true, content });
|
|
1720
|
+
} catch {
|
|
1721
|
+
finish({ ok: false, reason: "complete_invalid_json" });
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
child.stdin?.end(body);
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
async function postWriteback(event, content, opts, deps) {
|
|
1728
|
+
const chat = event.chat;
|
|
1729
|
+
if (!chat) return { ok: false, reason: "no_chat_envelope" };
|
|
1730
|
+
if (!isAllowedChatReplyPath(chat.chat_id, chat.reply_path)) {
|
|
1731
|
+
return { ok: false, reason: "reply_path_rejected" };
|
|
1732
|
+
}
|
|
1733
|
+
if (chat.reply_channel !== "agentplanet.chat") {
|
|
1734
|
+
return { ok: false, reason: "reply_channel_rejected" };
|
|
1735
|
+
}
|
|
1736
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1737
|
+
const path = chat.reply_path;
|
|
1738
|
+
let url;
|
|
1739
|
+
let baseOrigin;
|
|
1740
|
+
try {
|
|
1741
|
+
baseOrigin = new URL(opts.apiBase).origin;
|
|
1742
|
+
url = new URL(`${opts.apiBase}${path}`);
|
|
1743
|
+
} catch {
|
|
1744
|
+
return { ok: false, reason: "invalid_api_base" };
|
|
1745
|
+
}
|
|
1746
|
+
if (url.origin !== baseOrigin) {
|
|
1747
|
+
return { ok: false, reason: "reply_url_origin_mismatch" };
|
|
1748
|
+
}
|
|
1749
|
+
const timeoutMs = opts.writebackTimeoutMs ?? DEFAULT_WRITEBACK_TIMEOUT_MS;
|
|
1750
|
+
const postOnce = async (token) => {
|
|
1751
|
+
const controller = new AbortController();
|
|
1752
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1753
|
+
try {
|
|
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);
|
|
1783
|
+
}
|
|
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 };
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
return { ok: false, reason: result.reason };
|
|
1805
|
+
}
|
|
1806
|
+
async function handleChatWriteback(event, opts, deps = {}) {
|
|
1807
|
+
const logFn = deps.logFn ?? ((line) => console.error(line));
|
|
1808
|
+
if (!event.chat) return { ok: false, reason: "no_chat_envelope" };
|
|
1809
|
+
const completed = opts.completeUrl ? await completeViaHttp(event, opts, deps) : await completeViaExec(event, opts, deps);
|
|
1810
|
+
if (!completed.ok) {
|
|
1811
|
+
logFn(
|
|
1812
|
+
`[acn listen] chat_complete_failed chat_id=${event.chat.chat_id} message_id=${event.message_id} reason=${completed.reason}`
|
|
1813
|
+
);
|
|
1814
|
+
return { ok: false, reason: completed.reason };
|
|
1815
|
+
}
|
|
1816
|
+
const written = await postWriteback(event, completed.content, opts, deps);
|
|
1817
|
+
if (!written.ok) {
|
|
1818
|
+
logFn(
|
|
1819
|
+
`[acn listen] chat_writeback_failed chat_id=${event.chat.chat_id} message_id=${event.message_id} reason=${written.reason}`
|
|
1820
|
+
);
|
|
1821
|
+
return written;
|
|
1822
|
+
}
|
|
1823
|
+
logFn(
|
|
1824
|
+
`[acn listen] chat_writeback_ok chat_id=${event.chat.chat_id} message_id=${event.message_id} http=${written.httpStatus}`
|
|
1825
|
+
);
|
|
1826
|
+
return written;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1484
1829
|
// src/commands/local-receiver.ts
|
|
1485
1830
|
var import_crypto = require("crypto");
|
|
1486
1831
|
|
|
1487
1832
|
// src/commands/runtime-adapter.ts
|
|
1488
|
-
var
|
|
1833
|
+
var import_child_process2 = require("child_process");
|
|
1489
1834
|
var DEFAULT_WAKE_TIMEOUT_MS = 5e3;
|
|
1490
1835
|
function parseWakeHeaders(raw) {
|
|
1491
1836
|
const out = {};
|
|
@@ -1554,7 +1899,7 @@ async function wakeHttp(event, opts, deps) {
|
|
|
1554
1899
|
}
|
|
1555
1900
|
}
|
|
1556
1901
|
function wakeCommand(event, opts, deps) {
|
|
1557
|
-
const spawnFn = deps.spawnFn ??
|
|
1902
|
+
const spawnFn = deps.spawnFn ?? import_child_process2.spawn;
|
|
1558
1903
|
const timeoutMs = opts.wakeTimeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS;
|
|
1559
1904
|
const body = Buffer.from(JSON.stringify(event), "utf-8");
|
|
1560
1905
|
return new Promise((resolve) => {
|
|
@@ -1702,6 +2047,18 @@ function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send,
|
|
|
1702
2047
|
if (!result.shouldWake || !result.event) return;
|
|
1703
2048
|
const event = result.event;
|
|
1704
2049
|
const key = opts.dedupe ? dedupeKey(event) : null;
|
|
2050
|
+
if (event.chat && opts.chatWriteback?.enabled) {
|
|
2051
|
+
void handleChatWriteback(event, opts.chatWriteback, deps).then((written) => {
|
|
2052
|
+
if (!written.ok && key) dedupeStore.forget(key);
|
|
2053
|
+
}).catch((err) => {
|
|
2054
|
+
if (key) dedupeStore.forget(key);
|
|
2055
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2056
|
+
logFn(
|
|
2057
|
+
`[acn listen] chat_writeback_failed message_id=${event.message_id} reason=${msg.slice(0, 200)}`
|
|
2058
|
+
);
|
|
2059
|
+
});
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
1705
2062
|
void wakeRuntime(event, opts, deps).then((wake) => {
|
|
1706
2063
|
if (!wake.ok) {
|
|
1707
2064
|
if (key) dedupeStore.forget(key);
|
|
@@ -1767,7 +2124,7 @@ async function dispatchA2aRequest(frame, opts, send, deps = {}) {
|
|
|
1767
2124
|
return;
|
|
1768
2125
|
}
|
|
1769
2126
|
if (opts.exec) {
|
|
1770
|
-
send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ??
|
|
2127
|
+
send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process3.spawn));
|
|
1771
2128
|
return;
|
|
1772
2129
|
}
|
|
1773
2130
|
send(errorResponse(frame.id, 500, "no handler configured"));
|
|
@@ -1974,6 +2331,25 @@ function listenCommand() {
|
|
|
1974
2331
|
).option(
|
|
1975
2332
|
"--exec <command>",
|
|
1976
2333
|
"Compat: shell per request; stdout must be a full A2A JSON-RPC response"
|
|
2334
|
+
).option(
|
|
2335
|
+
"--chat-writeback",
|
|
2336
|
+
"On Chat Gateway envelopes: complete host reply then POST agent-messages with ACN agent JWT (requires --runtime; uses config api-key)"
|
|
2337
|
+
).option(
|
|
2338
|
+
"--chat-api-base <url>",
|
|
2339
|
+
"Chat Gateway origin for writeback (env: ACN_CHAT_API_BASE or AGENTPLANET_API_BASE)"
|
|
2340
|
+
).option(
|
|
2341
|
+
"--chat-token <token>",
|
|
2342
|
+
"Deprecated/ignored: writeback now mints ACN agent JWT from api-key"
|
|
2343
|
+
).option(
|
|
2344
|
+
"--chat-complete-url <url>",
|
|
2345
|
+
'POST NormalizedEvent \u2192 JSON {"content":"..."} (mutually exclusive with --chat-complete-exec)'
|
|
2346
|
+
).option(
|
|
2347
|
+
"--chat-complete-exec <cmd>",
|
|
2348
|
+
'Shell: event JSON on stdin \u2192 stdout JSON {"content":"..."}'
|
|
2349
|
+
).option(
|
|
2350
|
+
"--chat-complete-timeout <ms>",
|
|
2351
|
+
`Host complete timeout in ms (default ${DEFAULT_COMPLETE_TIMEOUT_MS})`,
|
|
2352
|
+
String(DEFAULT_COMPLETE_TIMEOUT_MS)
|
|
1977
2353
|
).option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(
|
|
1978
2354
|
(opts) => {
|
|
1979
2355
|
const config = loadConfig();
|
|
@@ -2002,6 +2378,30 @@ function listenCommand() {
|
|
|
2002
2378
|
console.error(flagErr);
|
|
2003
2379
|
process.exit(1);
|
|
2004
2380
|
}
|
|
2381
|
+
const chatApiBase = opts.chatApiBase?.trim() || process.env.ACN_CHAT_API_BASE?.trim() || process.env.AGENTPLANET_API_BASE?.trim();
|
|
2382
|
+
const chatCompleteUrl = opts.chatCompleteUrl?.trim() || process.env.ACN_CHAT_COMPLETE_URL?.trim();
|
|
2383
|
+
const chatCompleteExec = opts.chatCompleteExec?.trim();
|
|
2384
|
+
if (opts.chatWriteback && !opts.runtime) {
|
|
2385
|
+
console.error("--chat-writeback requires --runtime http|command|log.");
|
|
2386
|
+
process.exit(1);
|
|
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
|
+
}
|
|
2393
|
+
const chatErr = validateChatWritebackOptions({
|
|
2394
|
+
chatWriteback: opts.chatWriteback,
|
|
2395
|
+
chatApiBase,
|
|
2396
|
+
chatCompleteUrl,
|
|
2397
|
+
chatCompleteExec,
|
|
2398
|
+
agentId,
|
|
2399
|
+
apiKey
|
|
2400
|
+
});
|
|
2401
|
+
if (chatErr) {
|
|
2402
|
+
console.error(chatErr);
|
|
2403
|
+
process.exit(1);
|
|
2404
|
+
}
|
|
2005
2405
|
let wakeHeaders;
|
|
2006
2406
|
if (opts.runtime === "http") {
|
|
2007
2407
|
try {
|
|
@@ -2013,6 +2413,10 @@ function listenCommand() {
|
|
|
2013
2413
|
}
|
|
2014
2414
|
const wakeTimeoutMs = Number.parseInt(opts.wakeTimeout ?? "", 10);
|
|
2015
2415
|
const dedupeTtlSec = Number.parseInt(opts.dedupeTtl ?? "", 10);
|
|
2416
|
+
const chatCompleteTimeoutMs = Number.parseInt(
|
|
2417
|
+
opts.chatCompleteTimeout ?? "",
|
|
2418
|
+
10
|
|
2419
|
+
);
|
|
2016
2420
|
if (opts.runtime && (!Number.isFinite(wakeTimeoutMs) || wakeTimeoutMs <= 0)) {
|
|
2017
2421
|
console.error("--wake-timeout must be a positive integer (ms).");
|
|
2018
2422
|
process.exit(1);
|
|
@@ -2021,6 +2425,21 @@ function listenCommand() {
|
|
|
2021
2425
|
console.error("--dedupe-ttl must be a positive integer (seconds).");
|
|
2022
2426
|
process.exit(1);
|
|
2023
2427
|
}
|
|
2428
|
+
if (opts.chatWriteback && (!Number.isFinite(chatCompleteTimeoutMs) || chatCompleteTimeoutMs <= 0)) {
|
|
2429
|
+
console.error("--chat-complete-timeout must be a positive integer (ms).");
|
|
2430
|
+
process.exit(1);
|
|
2431
|
+
}
|
|
2432
|
+
const chatWriteback = buildChatWritebackOptions({
|
|
2433
|
+
chatWriteback: opts.chatWriteback,
|
|
2434
|
+
chatApiBase,
|
|
2435
|
+
acnBaseUrl: config.base_url,
|
|
2436
|
+
apiKey,
|
|
2437
|
+
chatCompleteUrl,
|
|
2438
|
+
chatCompleteExec,
|
|
2439
|
+
chatCompleteTimeoutMs: opts.chatWriteback ? chatCompleteTimeoutMs : void 0,
|
|
2440
|
+
agentId,
|
|
2441
|
+
audience: process.env.ACN_CHAT_JWT_AUDIENCE?.trim() || process.env.AGENTPLANET_JWT_AUDIENCE?.trim()
|
|
2442
|
+
});
|
|
2024
2443
|
runListener({
|
|
2025
2444
|
agentId,
|
|
2026
2445
|
apiKey,
|
|
@@ -2035,7 +2454,8 @@ function listenCommand() {
|
|
|
2035
2454
|
wakeTimeoutMs,
|
|
2036
2455
|
// commander: --no-dedupe sets dedupe=false; default true
|
|
2037
2456
|
dedupe: opts.dedupe !== false,
|
|
2038
|
-
dedupeTtlSec
|
|
2457
|
+
dedupeTtlSec,
|
|
2458
|
+
chatWriteback
|
|
2039
2459
|
} : void 0
|
|
2040
2460
|
});
|
|
2041
2461
|
}
|
|
@@ -3146,6 +3566,102 @@ function orgCommand() {
|
|
|
3146
3566
|
handleError(err);
|
|
3147
3567
|
}
|
|
3148
3568
|
});
|
|
3569
|
+
cmd.command("publish-task").description(
|
|
3570
|
+
"Publish a Task Pool task attributed to an Org (network by default; not Org work)"
|
|
3571
|
+
).requiredOption("--org <org_id>", "Org id (stored as metadata.org_id)").requiredOption("-t, --title <title>", "Task title (min 3 chars)").requiredOption("-d, --description <text>", "Task description (min 10 chars)").requiredOption("--tags <tags>", "Required skill tags, comma-separated").option("--deadline <hours>", "Deadline in hours (default: 48)", "48").option("--reward <amount>", "Reward amount (default: 0)", "0").option(
|
|
3572
|
+
"--pay-from <source>",
|
|
3573
|
+
"Who pays: agent (default, attribution only) | org (Org wallet + credits escrow)",
|
|
3574
|
+
"agent"
|
|
3575
|
+
).option("--type <type>", "Task type", "general").option("--max-participants <n>", "Max participants", "1").option(
|
|
3576
|
+
"--fence",
|
|
3577
|
+
"Scope task to the Org subnet fence (may deliver task.* to Org harness)",
|
|
3578
|
+
false
|
|
3579
|
+
).option("--subnet <slug>", "Override subnet slug (implies fence; default: Org fence)").action(
|
|
3580
|
+
async (opts) => {
|
|
3581
|
+
const config = loadConfig();
|
|
3582
|
+
if (!config.api_key) {
|
|
3583
|
+
console.error(
|
|
3584
|
+
"No API key found. Run `acn join` first or `acn config set api-key <key>`."
|
|
3585
|
+
);
|
|
3586
|
+
process.exit(1);
|
|
3587
|
+
}
|
|
3588
|
+
const payFrom = (opts.payFrom ?? "agent").toLowerCase();
|
|
3589
|
+
if (payFrom !== "agent" && payFrom !== "org") {
|
|
3590
|
+
console.error('--pay-from must be "agent" or "org"');
|
|
3591
|
+
process.exit(1);
|
|
3592
|
+
}
|
|
3593
|
+
try {
|
|
3594
|
+
const body = {
|
|
3595
|
+
title: opts.title,
|
|
3596
|
+
description: opts.description,
|
|
3597
|
+
deadline_hours: parseInt(opts.deadline ?? "48", 10),
|
|
3598
|
+
required_tags: opts.tags.split(",").map((s) => s.trim()).filter(Boolean),
|
|
3599
|
+
reward: opts.reward ?? "0",
|
|
3600
|
+
task_type: opts.type ?? "general",
|
|
3601
|
+
max_participants: parseInt(opts.maxParticipants ?? "1", 10),
|
|
3602
|
+
pay_from_org: payFrom === "org",
|
|
3603
|
+
fence: Boolean(opts.fence || opts.subnet)
|
|
3604
|
+
};
|
|
3605
|
+
if (opts.subnet) {
|
|
3606
|
+
body.subnet_slug = opts.subnet;
|
|
3607
|
+
}
|
|
3608
|
+
const task = await acnPost(
|
|
3609
|
+
`/orgs/${opts.org}/publish-task`,
|
|
3610
|
+
body
|
|
3611
|
+
);
|
|
3612
|
+
const lines = [
|
|
3613
|
+
payFrom === "org" ? "Task published \u2014 Org-paid (credits escrow when reward > 0)" : "Task published for Org (Task Pool \u2014 not Org work)",
|
|
3614
|
+
` Task ID : ${task.task_id}`,
|
|
3615
|
+
` Org : ${opts.org}`,
|
|
3616
|
+
` Pay from : ${payFrom}`,
|
|
3617
|
+
` Creator : ${task.creator_type}/${task.creator_id}`,
|
|
3618
|
+
` Status : ${task.status}`,
|
|
3619
|
+
` Title : ${task.title}`,
|
|
3620
|
+
` Currency : ${task.reward_currency}`,
|
|
3621
|
+
` Escrow : ${String(task.use_escrow ?? false)}`,
|
|
3622
|
+
` Subnet : ${task.subnet_slug ?? "(network / unscoped)"}`,
|
|
3623
|
+
` metadata : org_id=${String(task.metadata?.org_id ?? opts.org)} org_publish=${String(task.metadata?.org_publish ?? true)}`
|
|
3624
|
+
];
|
|
3625
|
+
if (body.fence) {
|
|
3626
|
+
lines.push(
|
|
3627
|
+
" Note : fenced \u2014 task.* may hit the Org harness webhook"
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3630
|
+
output(task, lines.join("\n"));
|
|
3631
|
+
} catch (err) {
|
|
3632
|
+
handleError(err);
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
);
|
|
3636
|
+
cmd.command("import-task").description(
|
|
3637
|
+
"Import a Task Pool task as Org work (governance only; links via task.metadata)"
|
|
3638
|
+
).requiredOption("--org <org_id>", "Org id").requiredOption("--task <task_id>", "Task id to import").option("--assignee <agent_id>", "Optional assignee for the work item").action(
|
|
3639
|
+
async (opts) => {
|
|
3640
|
+
const config = loadConfig();
|
|
3641
|
+
if (!config.api_key) {
|
|
3642
|
+
console.error(
|
|
3643
|
+
"No API key found. Run `acn join` first or `acn config set api-key <key>`."
|
|
3644
|
+
);
|
|
3645
|
+
process.exit(1);
|
|
3646
|
+
}
|
|
3647
|
+
try {
|
|
3648
|
+
const body = { task_id: opts.task };
|
|
3649
|
+
if (opts.assignee) body.assignee_agent_id = opts.assignee;
|
|
3650
|
+
const res = await acnPost(`/orgs/${opts.org}/work/import-task`, body);
|
|
3651
|
+
const lines = [
|
|
3652
|
+
res.already_imported ? "Task already imported (idempotent)" : "Task imported as Org work",
|
|
3653
|
+
` Work ID : ${res.work_id}`,
|
|
3654
|
+
` Org : ${res.org_id}`,
|
|
3655
|
+
` Task ID : ${res.source_task_id ?? opts.task}`,
|
|
3656
|
+
` Status : ${res.status}`,
|
|
3657
|
+
` Title : ${res.title}`
|
|
3658
|
+
];
|
|
3659
|
+
output(res, lines.join("\n"));
|
|
3660
|
+
} catch (err) {
|
|
3661
|
+
handleError(err);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
);
|
|
3149
3665
|
return cmd;
|
|
3150
3666
|
}
|
|
3151
3667
|
|