@acnlabs/acn-cli 0.14.0 → 0.14.1
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 +18 -0
- package/dist/index.js +439 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,6 +89,24 @@ 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
|
+
|
|
96
|
+
```bash
|
|
97
|
+
export AGENTPLANET_API_BASE=https://api.example.com
|
|
98
|
+
export AGENTPLANET_INTERNAL_TOKEN=… # or AGENTPLANET_INTERNAL_API_TOKEN
|
|
99
|
+
|
|
100
|
+
acn listen --runtime http \
|
|
101
|
+
--wake-url http://127.0.0.1:10122/hooks/agent \
|
|
102
|
+
--chat-writeback \
|
|
103
|
+
--chat-complete-url http://127.0.0.1:10122/chat/complete
|
|
104
|
+
# or: --chat-complete-exec 'python3 /path/to/complete.py'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Task / Org wakes still use `--wake-url` / `--wake-exec`. Chat envelopes skip
|
|
108
|
+
wake and use the complete → writeback path instead.
|
|
109
|
+
|
|
92
110
|
**Coverage:** only traffic that arrives over the Mode B relay. Open Task Pool
|
|
93
111
|
rows that were never pushed as A2A still need `acn tasks list` / reconcile.
|
|
94
112
|
|
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.1",
|
|
35
35
|
description: "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
|
|
36
36
|
main: "dist/index.js",
|
|
37
37
|
bin: {
|
|
@@ -608,6 +608,8 @@ function formatTask(t) {
|
|
|
608
608
|
lines.push(` Reward : ${t.reward} ${t.reward_currency ?? ""}`);
|
|
609
609
|
}
|
|
610
610
|
if (t.subnet_slug) lines.push(` Subnet : ${t.subnet_slug}`);
|
|
611
|
+
const orgId = t.metadata?.org_id;
|
|
612
|
+
if (typeof orgId === "string" && orgId) lines.push(` Org : ${orgId}`);
|
|
611
613
|
if (t.description) lines.push(` Desc : ${t.description.slice(0, 120)}`);
|
|
612
614
|
if (t.created_at) lines.push(` Created : ${t.created_at}`);
|
|
613
615
|
if (t.deadline) lines.push(` Deadline : ${t.deadline}`);
|
|
@@ -697,7 +699,10 @@ ${formatTask(t)}`).join("\n\n")
|
|
|
697
699
|
handleError(err);
|
|
698
700
|
}
|
|
699
701
|
});
|
|
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)").
|
|
702
|
+
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(
|
|
703
|
+
"--org-id <org_id>",
|
|
704
|
+
"Attribute to an Org (metadata.org_id + org_publish; prefer `acn org publish-task`)"
|
|
705
|
+
).action(
|
|
701
706
|
async (opts) => {
|
|
702
707
|
const config = loadConfig();
|
|
703
708
|
if (!config.api_key) {
|
|
@@ -722,6 +727,9 @@ ${formatTask(t)}`).join("\n\n")
|
|
|
722
727
|
if (opts.subnet) {
|
|
723
728
|
body.subnet_slug = opts.subnet;
|
|
724
729
|
}
|
|
730
|
+
if (opts.orgId) {
|
|
731
|
+
body.metadata = { org_id: opts.orgId, org_publish: true };
|
|
732
|
+
}
|
|
725
733
|
try {
|
|
726
734
|
const task = await acnPost("/tasks/agent/create", body);
|
|
727
735
|
output(task, [`Task created!
|
|
@@ -1375,10 +1383,14 @@ ${formatPolicy(res)}`);
|
|
|
1375
1383
|
|
|
1376
1384
|
// src/commands/listen.ts
|
|
1377
1385
|
var import_commander10 = require("commander");
|
|
1378
|
-
var
|
|
1386
|
+
var import_child_process3 = require("child_process");
|
|
1379
1387
|
var import_ws = __toESM(require("ws"));
|
|
1380
1388
|
|
|
1389
|
+
// src/commands/chat-writeback.ts
|
|
1390
|
+
var import_child_process = require("child_process");
|
|
1391
|
+
|
|
1381
1392
|
// src/commands/normalize-event.ts
|
|
1393
|
+
var CHAT_REPLY_CHANNEL = "agentplanet.chat";
|
|
1382
1394
|
function asRecord(v) {
|
|
1383
1395
|
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
1384
1396
|
}
|
|
@@ -1430,6 +1442,44 @@ function extractFromAgent(message) {
|
|
|
1430
1442
|
if (!metadata) return null;
|
|
1431
1443
|
return asNonEmptyString(metadata.from_agent) ?? asNonEmptyString(metadata.fromAgent);
|
|
1432
1444
|
}
|
|
1445
|
+
function extractUserText(message) {
|
|
1446
|
+
const parts = message.parts;
|
|
1447
|
+
if (!Array.isArray(parts)) return null;
|
|
1448
|
+
const chunks = [];
|
|
1449
|
+
for (const part of parts) {
|
|
1450
|
+
const p = asRecord(part);
|
|
1451
|
+
if (!p || p.kind !== "text") continue;
|
|
1452
|
+
const t = asNonEmptyString(p.text);
|
|
1453
|
+
if (t) chunks.push(t);
|
|
1454
|
+
}
|
|
1455
|
+
return chunks.length > 0 ? chunks.join("\n") : null;
|
|
1456
|
+
}
|
|
1457
|
+
function isAllowedChatReplyPath(chatId, replyPath) {
|
|
1458
|
+
if (!chatId || !replyPath) return false;
|
|
1459
|
+
if (replyPath.includes("..") || replyPath.includes("?") || replyPath.includes("#") || replyPath.includes("//") || replyPath.includes("\\") || !replyPath.startsWith("/")) {
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
return replyPath === `/api/chats/${chatId}/agent-messages`;
|
|
1463
|
+
}
|
|
1464
|
+
function extractChatEnvelope(message) {
|
|
1465
|
+
const metadata = asRecord(message.metadata);
|
|
1466
|
+
if (!metadata) return null;
|
|
1467
|
+
const ap = asRecord(metadata.agentplanet);
|
|
1468
|
+
if (!ap) return null;
|
|
1469
|
+
const chatId = asNonEmptyString(ap.chat_id);
|
|
1470
|
+
const replyPath = asNonEmptyString(ap.reply_path);
|
|
1471
|
+
const replyChannel = asNonEmptyString(ap.reply_channel);
|
|
1472
|
+
if (!chatId || !replyPath) return null;
|
|
1473
|
+
if (replyChannel !== CHAT_REPLY_CHANNEL) return null;
|
|
1474
|
+
if (!isAllowedChatReplyPath(chatId, replyPath)) return null;
|
|
1475
|
+
return {
|
|
1476
|
+
chat_id: chatId,
|
|
1477
|
+
reply_path: replyPath,
|
|
1478
|
+
reply_channel: CHAT_REPLY_CHANNEL,
|
|
1479
|
+
gateway_message_id: asNonEmptyString(ap.message_id) ?? asNonEmptyString(ap.messageId),
|
|
1480
|
+
user_text: extractUserText(message)
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1433
1483
|
function normalizeEvent(body, opts = {}) {
|
|
1434
1484
|
const generateId = opts.generateId ?? (() => crypto.randomUUID());
|
|
1435
1485
|
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -1441,11 +1491,16 @@ function normalizeEvent(body, opts = {}) {
|
|
|
1441
1491
|
message_id: extractMessageId(message, generateId),
|
|
1442
1492
|
context_id: extractContextId(message),
|
|
1443
1493
|
from_agent: extractFromAgent(message),
|
|
1494
|
+
chat: extractChatEnvelope(message),
|
|
1444
1495
|
received_at: now().toISOString(),
|
|
1445
1496
|
raw: body
|
|
1446
1497
|
};
|
|
1447
1498
|
}
|
|
1448
1499
|
function dedupeKey(event) {
|
|
1500
|
+
if (event.chat) {
|
|
1501
|
+
const mid = event.chat.gateway_message_id ?? event.message_id;
|
|
1502
|
+
return `chat:${event.chat.chat_id}:${mid}`;
|
|
1503
|
+
}
|
|
1449
1504
|
return event.task_id ?? event.message_id;
|
|
1450
1505
|
}
|
|
1451
1506
|
var DedupeStore = class {
|
|
@@ -1481,11 +1536,223 @@ var DedupeStore = class {
|
|
|
1481
1536
|
}
|
|
1482
1537
|
};
|
|
1483
1538
|
|
|
1539
|
+
// src/commands/chat-writeback.ts
|
|
1540
|
+
var DEFAULT_COMPLETE_TIMEOUT_MS = 12e4;
|
|
1541
|
+
var DEFAULT_WRITEBACK_TIMEOUT_MS = 3e4;
|
|
1542
|
+
function asRecord2(v) {
|
|
1543
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
1544
|
+
}
|
|
1545
|
+
function validateChatWritebackOptions(opts) {
|
|
1546
|
+
if (!opts.chatWriteback) return null;
|
|
1547
|
+
if (!opts.agentId) {
|
|
1548
|
+
return "--chat-writeback requires a known agent id (join / --agent-id).";
|
|
1549
|
+
}
|
|
1550
|
+
if (!opts.chatApiBase?.trim()) {
|
|
1551
|
+
return "--chat-writeback requires --chat-api-base (or ACN_CHAT_API_BASE / AGENTPLANET_API_BASE).";
|
|
1552
|
+
}
|
|
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
|
+
const hasUrl = Boolean(opts.chatCompleteUrl?.trim());
|
|
1557
|
+
const hasExec = Boolean(opts.chatCompleteExec?.trim());
|
|
1558
|
+
if (hasUrl === hasExec) {
|
|
1559
|
+
return '--chat-writeback requires exactly one of --chat-complete-url or --chat-complete-exec (host returns JSON {"content":"..."}).';
|
|
1560
|
+
}
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1563
|
+
function buildChatWritebackOptions(opts) {
|
|
1564
|
+
if (!opts.chatWriteback) return void 0;
|
|
1565
|
+
return {
|
|
1566
|
+
enabled: true,
|
|
1567
|
+
apiBase: opts.chatApiBase.replace(/\/+$/, ""),
|
|
1568
|
+
token: opts.chatToken,
|
|
1569
|
+
agentId: opts.agentId,
|
|
1570
|
+
completeUrl: opts.chatCompleteUrl?.trim() || void 0,
|
|
1571
|
+
completeExec: opts.chatCompleteExec?.trim() || void 0,
|
|
1572
|
+
completeTimeoutMs: opts.chatCompleteTimeoutMs,
|
|
1573
|
+
writebackTimeoutMs: opts.chatWritebackTimeoutMs
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
function extractContent(payload) {
|
|
1577
|
+
const rec = asRecord2(payload);
|
|
1578
|
+
if (!rec) return null;
|
|
1579
|
+
for (const key of ["content", "reply", "text"]) {
|
|
1580
|
+
const v = rec[key];
|
|
1581
|
+
if (typeof v === "string" && v.trim()) {
|
|
1582
|
+
const t = v.trim();
|
|
1583
|
+
if (t.toLowerCase() === "accepted") continue;
|
|
1584
|
+
return t;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
async function completeViaHttp(event, opts, deps) {
|
|
1590
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1591
|
+
const timeoutMs = opts.completeTimeoutMs ?? DEFAULT_COMPLETE_TIMEOUT_MS;
|
|
1592
|
+
const controller = new AbortController();
|
|
1593
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1594
|
+
try {
|
|
1595
|
+
const res = await fetchFn(opts.completeUrl, {
|
|
1596
|
+
method: "POST",
|
|
1597
|
+
headers: { "content-type": "application/json" },
|
|
1598
|
+
body: JSON.stringify(event),
|
|
1599
|
+
signal: controller.signal
|
|
1600
|
+
});
|
|
1601
|
+
const text = await res.text();
|
|
1602
|
+
if (res.status < 200 || res.status >= 300) {
|
|
1603
|
+
return { ok: false, reason: `complete_http_${res.status}` };
|
|
1604
|
+
}
|
|
1605
|
+
let parsed;
|
|
1606
|
+
try {
|
|
1607
|
+
parsed = JSON.parse(text);
|
|
1608
|
+
} catch {
|
|
1609
|
+
return { ok: false, reason: "complete_invalid_json" };
|
|
1610
|
+
}
|
|
1611
|
+
const content = extractContent(parsed);
|
|
1612
|
+
if (!content) return { ok: false, reason: "complete_missing_content" };
|
|
1613
|
+
return { ok: true, content };
|
|
1614
|
+
} catch (err) {
|
|
1615
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1616
|
+
if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
|
|
1617
|
+
return { ok: false, reason: "complete_timeout" };
|
|
1618
|
+
}
|
|
1619
|
+
return { ok: false, reason: msg.slice(0, 200) };
|
|
1620
|
+
} finally {
|
|
1621
|
+
clearTimeout(timer);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
function completeViaExec(event, opts, deps) {
|
|
1625
|
+
const spawnFn = deps.spawnFn ?? import_child_process.spawn;
|
|
1626
|
+
const timeoutMs = opts.completeTimeoutMs ?? DEFAULT_COMPLETE_TIMEOUT_MS;
|
|
1627
|
+
const body = Buffer.from(JSON.stringify(event), "utf-8");
|
|
1628
|
+
return new Promise((resolve) => {
|
|
1629
|
+
let settled = false;
|
|
1630
|
+
const child = spawnFn(opts.completeExec, { shell: true });
|
|
1631
|
+
const stdout = [];
|
|
1632
|
+
const stderr = [];
|
|
1633
|
+
const finish = (result) => {
|
|
1634
|
+
if (settled) return;
|
|
1635
|
+
settled = true;
|
|
1636
|
+
clearTimeout(timer);
|
|
1637
|
+
resolve(result);
|
|
1638
|
+
};
|
|
1639
|
+
const timer = setTimeout(() => {
|
|
1640
|
+
child.kill("SIGTERM");
|
|
1641
|
+
finish({ ok: false, reason: "complete_timeout" });
|
|
1642
|
+
}, timeoutMs);
|
|
1643
|
+
child.stdout?.on("data", (d) => stdout.push(Buffer.from(d)));
|
|
1644
|
+
child.stderr?.on("data", (d) => stderr.push(Buffer.from(d)));
|
|
1645
|
+
child.on(
|
|
1646
|
+
"error",
|
|
1647
|
+
(e) => finish({ ok: false, reason: e.message.slice(0, 200) })
|
|
1648
|
+
);
|
|
1649
|
+
child.on("close", (code) => {
|
|
1650
|
+
if (code !== 0) {
|
|
1651
|
+
const detail = Buffer.concat(stderr).toString("utf-8").slice(0, 80);
|
|
1652
|
+
finish({
|
|
1653
|
+
ok: false,
|
|
1654
|
+
reason: detail ? `complete_exit_${code}:${detail}` : `complete_exit_${code}`
|
|
1655
|
+
});
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
const text = Buffer.concat(stdout).toString("utf-8").trim();
|
|
1659
|
+
try {
|
|
1660
|
+
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 });
|
|
1667
|
+
} catch {
|
|
1668
|
+
finish({ ok: false, reason: "complete_invalid_json" });
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
child.stdin?.end(body);
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
async function postWriteback(event, content, opts, deps) {
|
|
1675
|
+
const chat = event.chat;
|
|
1676
|
+
if (!chat) return { ok: false, reason: "no_chat_envelope" };
|
|
1677
|
+
if (!isAllowedChatReplyPath(chat.chat_id, chat.reply_path)) {
|
|
1678
|
+
return { ok: false, reason: "reply_path_rejected" };
|
|
1679
|
+
}
|
|
1680
|
+
if (chat.reply_channel !== "agentplanet.chat") {
|
|
1681
|
+
return { ok: false, reason: "reply_channel_rejected" };
|
|
1682
|
+
}
|
|
1683
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1684
|
+
const path = chat.reply_path;
|
|
1685
|
+
let url;
|
|
1686
|
+
let baseOrigin;
|
|
1687
|
+
try {
|
|
1688
|
+
baseOrigin = new URL(opts.apiBase).origin;
|
|
1689
|
+
url = new URL(`${opts.apiBase}${path}`);
|
|
1690
|
+
} catch {
|
|
1691
|
+
return { ok: false, reason: "invalid_api_base" };
|
|
1692
|
+
}
|
|
1693
|
+
if (url.origin !== baseOrigin) {
|
|
1694
|
+
return { ok: false, reason: "reply_url_origin_mismatch" };
|
|
1695
|
+
}
|
|
1696
|
+
url.searchParams.set("agent_id", opts.agentId);
|
|
1697
|
+
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
|
+
}
|
|
1713
|
+
try {
|
|
1714
|
+
await res.arrayBuffer();
|
|
1715
|
+
} catch {
|
|
1716
|
+
}
|
|
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" };
|
|
1722
|
+
}
|
|
1723
|
+
return { ok: false, reason: msg.slice(0, 200) };
|
|
1724
|
+
} finally {
|
|
1725
|
+
clearTimeout(timer);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
async function handleChatWriteback(event, opts, deps = {}) {
|
|
1729
|
+
const logFn = deps.logFn ?? ((line) => console.error(line));
|
|
1730
|
+
if (!event.chat) return { ok: false, reason: "no_chat_envelope" };
|
|
1731
|
+
const completed = opts.completeUrl ? await completeViaHttp(event, opts, deps) : await completeViaExec(event, opts, deps);
|
|
1732
|
+
if (!completed.ok) {
|
|
1733
|
+
logFn(
|
|
1734
|
+
`[acn listen] chat_complete_failed chat_id=${event.chat.chat_id} message_id=${event.message_id} reason=${completed.reason}`
|
|
1735
|
+
);
|
|
1736
|
+
return { ok: false, reason: completed.reason };
|
|
1737
|
+
}
|
|
1738
|
+
const written = await postWriteback(event, completed.content, opts, deps);
|
|
1739
|
+
if (!written.ok) {
|
|
1740
|
+
logFn(
|
|
1741
|
+
`[acn listen] chat_writeback_failed chat_id=${event.chat.chat_id} message_id=${event.message_id} reason=${written.reason}`
|
|
1742
|
+
);
|
|
1743
|
+
return written;
|
|
1744
|
+
}
|
|
1745
|
+
logFn(
|
|
1746
|
+
`[acn listen] chat_writeback_ok chat_id=${event.chat.chat_id} message_id=${event.message_id} http=${written.httpStatus}`
|
|
1747
|
+
);
|
|
1748
|
+
return written;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1484
1751
|
// src/commands/local-receiver.ts
|
|
1485
1752
|
var import_crypto = require("crypto");
|
|
1486
1753
|
|
|
1487
1754
|
// src/commands/runtime-adapter.ts
|
|
1488
|
-
var
|
|
1755
|
+
var import_child_process2 = require("child_process");
|
|
1489
1756
|
var DEFAULT_WAKE_TIMEOUT_MS = 5e3;
|
|
1490
1757
|
function parseWakeHeaders(raw) {
|
|
1491
1758
|
const out = {};
|
|
@@ -1554,7 +1821,7 @@ async function wakeHttp(event, opts, deps) {
|
|
|
1554
1821
|
}
|
|
1555
1822
|
}
|
|
1556
1823
|
function wakeCommand(event, opts, deps) {
|
|
1557
|
-
const spawnFn = deps.spawnFn ??
|
|
1824
|
+
const spawnFn = deps.spawnFn ?? import_child_process2.spawn;
|
|
1558
1825
|
const timeoutMs = opts.wakeTimeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS;
|
|
1559
1826
|
const body = Buffer.from(JSON.stringify(event), "utf-8");
|
|
1560
1827
|
return new Promise((resolve) => {
|
|
@@ -1702,6 +1969,18 @@ function dispatchLocalReceiver(correlationId, bodyText, opts, dedupeStore, send,
|
|
|
1702
1969
|
if (!result.shouldWake || !result.event) return;
|
|
1703
1970
|
const event = result.event;
|
|
1704
1971
|
const key = opts.dedupe ? dedupeKey(event) : null;
|
|
1972
|
+
if (event.chat && opts.chatWriteback?.enabled) {
|
|
1973
|
+
void handleChatWriteback(event, opts.chatWriteback, deps).then((written) => {
|
|
1974
|
+
if (!written.ok && key) dedupeStore.forget(key);
|
|
1975
|
+
}).catch((err) => {
|
|
1976
|
+
if (key) dedupeStore.forget(key);
|
|
1977
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1978
|
+
logFn(
|
|
1979
|
+
`[acn listen] chat_writeback_failed message_id=${event.message_id} reason=${msg.slice(0, 200)}`
|
|
1980
|
+
);
|
|
1981
|
+
});
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1705
1984
|
void wakeRuntime(event, opts, deps).then((wake) => {
|
|
1706
1985
|
if (!wake.ok) {
|
|
1707
1986
|
if (key) dedupeStore.forget(key);
|
|
@@ -1767,7 +2046,7 @@ async function dispatchA2aRequest(frame, opts, send, deps = {}) {
|
|
|
1767
2046
|
return;
|
|
1768
2047
|
}
|
|
1769
2048
|
if (opts.exec) {
|
|
1770
|
-
send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ??
|
|
2049
|
+
send(await runExec(frame, bodyBuf, opts.exec, deps.spawnFn ?? import_child_process3.spawn));
|
|
1771
2050
|
return;
|
|
1772
2051
|
}
|
|
1773
2052
|
send(errorResponse(frame.id, 500, "no handler configured"));
|
|
@@ -1974,6 +2253,25 @@ function listenCommand() {
|
|
|
1974
2253
|
).option(
|
|
1975
2254
|
"--exec <command>",
|
|
1976
2255
|
"Compat: shell per request; stdout must be a full A2A JSON-RPC response"
|
|
2256
|
+
).option(
|
|
2257
|
+
"--chat-writeback",
|
|
2258
|
+
"On Chat Gateway envelopes: complete host reply then POST agent-messages (requires --runtime)"
|
|
2259
|
+
).option(
|
|
2260
|
+
"--chat-api-base <url>",
|
|
2261
|
+
"Chat Gateway origin for writeback (env: ACN_CHAT_API_BASE or AGENTPLANET_API_BASE)"
|
|
2262
|
+
).option(
|
|
2263
|
+
"--chat-token <token>",
|
|
2264
|
+
"X-Internal-Token for agent-messages (env: ACN_CHAT_WRITEBACK_TOKEN, AGENTPLANET_INTERNAL_TOKEN, or AGENTPLANET_INTERNAL_API_TOKEN)"
|
|
2265
|
+
).option(
|
|
2266
|
+
"--chat-complete-url <url>",
|
|
2267
|
+
'POST NormalizedEvent \u2192 JSON {"content":"..."} (mutually exclusive with --chat-complete-exec)'
|
|
2268
|
+
).option(
|
|
2269
|
+
"--chat-complete-exec <cmd>",
|
|
2270
|
+
'Shell: event JSON on stdin \u2192 stdout JSON {"content":"..."}'
|
|
2271
|
+
).option(
|
|
2272
|
+
"--chat-complete-timeout <ms>",
|
|
2273
|
+
`Host complete timeout in ms (default ${DEFAULT_COMPLETE_TIMEOUT_MS})`,
|
|
2274
|
+
String(DEFAULT_COMPLETE_TIMEOUT_MS)
|
|
1977
2275
|
).option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(
|
|
1978
2276
|
(opts) => {
|
|
1979
2277
|
const config = loadConfig();
|
|
@@ -2002,6 +2300,26 @@ function listenCommand() {
|
|
|
2002
2300
|
console.error(flagErr);
|
|
2003
2301
|
process.exit(1);
|
|
2004
2302
|
}
|
|
2303
|
+
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
|
+
const chatCompleteUrl = opts.chatCompleteUrl?.trim() || process.env.ACN_CHAT_COMPLETE_URL?.trim();
|
|
2306
|
+
const chatCompleteExec = opts.chatCompleteExec?.trim();
|
|
2307
|
+
if (opts.chatWriteback && !opts.runtime) {
|
|
2308
|
+
console.error("--chat-writeback requires --runtime http|command|log.");
|
|
2309
|
+
process.exit(1);
|
|
2310
|
+
}
|
|
2311
|
+
const chatErr = validateChatWritebackOptions({
|
|
2312
|
+
chatWriteback: opts.chatWriteback,
|
|
2313
|
+
chatApiBase,
|
|
2314
|
+
chatToken,
|
|
2315
|
+
chatCompleteUrl,
|
|
2316
|
+
chatCompleteExec,
|
|
2317
|
+
agentId
|
|
2318
|
+
});
|
|
2319
|
+
if (chatErr) {
|
|
2320
|
+
console.error(chatErr);
|
|
2321
|
+
process.exit(1);
|
|
2322
|
+
}
|
|
2005
2323
|
let wakeHeaders;
|
|
2006
2324
|
if (opts.runtime === "http") {
|
|
2007
2325
|
try {
|
|
@@ -2013,6 +2331,10 @@ function listenCommand() {
|
|
|
2013
2331
|
}
|
|
2014
2332
|
const wakeTimeoutMs = Number.parseInt(opts.wakeTimeout ?? "", 10);
|
|
2015
2333
|
const dedupeTtlSec = Number.parseInt(opts.dedupeTtl ?? "", 10);
|
|
2334
|
+
const chatCompleteTimeoutMs = Number.parseInt(
|
|
2335
|
+
opts.chatCompleteTimeout ?? "",
|
|
2336
|
+
10
|
|
2337
|
+
);
|
|
2016
2338
|
if (opts.runtime && (!Number.isFinite(wakeTimeoutMs) || wakeTimeoutMs <= 0)) {
|
|
2017
2339
|
console.error("--wake-timeout must be a positive integer (ms).");
|
|
2018
2340
|
process.exit(1);
|
|
@@ -2021,6 +2343,19 @@ function listenCommand() {
|
|
|
2021
2343
|
console.error("--dedupe-ttl must be a positive integer (seconds).");
|
|
2022
2344
|
process.exit(1);
|
|
2023
2345
|
}
|
|
2346
|
+
if (opts.chatWriteback && (!Number.isFinite(chatCompleteTimeoutMs) || chatCompleteTimeoutMs <= 0)) {
|
|
2347
|
+
console.error("--chat-complete-timeout must be a positive integer (ms).");
|
|
2348
|
+
process.exit(1);
|
|
2349
|
+
}
|
|
2350
|
+
const chatWriteback = buildChatWritebackOptions({
|
|
2351
|
+
chatWriteback: opts.chatWriteback,
|
|
2352
|
+
chatApiBase,
|
|
2353
|
+
chatToken,
|
|
2354
|
+
chatCompleteUrl,
|
|
2355
|
+
chatCompleteExec,
|
|
2356
|
+
chatCompleteTimeoutMs: opts.chatWriteback ? chatCompleteTimeoutMs : void 0,
|
|
2357
|
+
agentId
|
|
2358
|
+
});
|
|
2024
2359
|
runListener({
|
|
2025
2360
|
agentId,
|
|
2026
2361
|
apiKey,
|
|
@@ -2035,7 +2370,8 @@ function listenCommand() {
|
|
|
2035
2370
|
wakeTimeoutMs,
|
|
2036
2371
|
// commander: --no-dedupe sets dedupe=false; default true
|
|
2037
2372
|
dedupe: opts.dedupe !== false,
|
|
2038
|
-
dedupeTtlSec
|
|
2373
|
+
dedupeTtlSec,
|
|
2374
|
+
chatWriteback
|
|
2039
2375
|
} : void 0
|
|
2040
2376
|
});
|
|
2041
2377
|
}
|
|
@@ -3146,6 +3482,102 @@ function orgCommand() {
|
|
|
3146
3482
|
handleError(err);
|
|
3147
3483
|
}
|
|
3148
3484
|
});
|
|
3485
|
+
cmd.command("publish-task").description(
|
|
3486
|
+
"Publish a Task Pool task attributed to an Org (network by default; not Org work)"
|
|
3487
|
+
).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(
|
|
3488
|
+
"--pay-from <source>",
|
|
3489
|
+
"Who pays: agent (default, attribution only) | org (Org wallet + credits escrow)",
|
|
3490
|
+
"agent"
|
|
3491
|
+
).option("--type <type>", "Task type", "general").option("--max-participants <n>", "Max participants", "1").option(
|
|
3492
|
+
"--fence",
|
|
3493
|
+
"Scope task to the Org subnet fence (may deliver task.* to Org harness)",
|
|
3494
|
+
false
|
|
3495
|
+
).option("--subnet <slug>", "Override subnet slug (implies fence; default: Org fence)").action(
|
|
3496
|
+
async (opts) => {
|
|
3497
|
+
const config = loadConfig();
|
|
3498
|
+
if (!config.api_key) {
|
|
3499
|
+
console.error(
|
|
3500
|
+
"No API key found. Run `acn join` first or `acn config set api-key <key>`."
|
|
3501
|
+
);
|
|
3502
|
+
process.exit(1);
|
|
3503
|
+
}
|
|
3504
|
+
const payFrom = (opts.payFrom ?? "agent").toLowerCase();
|
|
3505
|
+
if (payFrom !== "agent" && payFrom !== "org") {
|
|
3506
|
+
console.error('--pay-from must be "agent" or "org"');
|
|
3507
|
+
process.exit(1);
|
|
3508
|
+
}
|
|
3509
|
+
try {
|
|
3510
|
+
const body = {
|
|
3511
|
+
title: opts.title,
|
|
3512
|
+
description: opts.description,
|
|
3513
|
+
deadline_hours: parseInt(opts.deadline ?? "48", 10),
|
|
3514
|
+
required_tags: opts.tags.split(",").map((s) => s.trim()).filter(Boolean),
|
|
3515
|
+
reward: opts.reward ?? "0",
|
|
3516
|
+
task_type: opts.type ?? "general",
|
|
3517
|
+
max_participants: parseInt(opts.maxParticipants ?? "1", 10),
|
|
3518
|
+
pay_from_org: payFrom === "org",
|
|
3519
|
+
fence: Boolean(opts.fence || opts.subnet)
|
|
3520
|
+
};
|
|
3521
|
+
if (opts.subnet) {
|
|
3522
|
+
body.subnet_slug = opts.subnet;
|
|
3523
|
+
}
|
|
3524
|
+
const task = await acnPost(
|
|
3525
|
+
`/orgs/${opts.org}/publish-task`,
|
|
3526
|
+
body
|
|
3527
|
+
);
|
|
3528
|
+
const lines = [
|
|
3529
|
+
payFrom === "org" ? "Task published \u2014 Org-paid (credits escrow when reward > 0)" : "Task published for Org (Task Pool \u2014 not Org work)",
|
|
3530
|
+
` Task ID : ${task.task_id}`,
|
|
3531
|
+
` Org : ${opts.org}`,
|
|
3532
|
+
` Pay from : ${payFrom}`,
|
|
3533
|
+
` Creator : ${task.creator_type}/${task.creator_id}`,
|
|
3534
|
+
` Status : ${task.status}`,
|
|
3535
|
+
` Title : ${task.title}`,
|
|
3536
|
+
` Currency : ${task.reward_currency}`,
|
|
3537
|
+
` Escrow : ${String(task.use_escrow ?? false)}`,
|
|
3538
|
+
` Subnet : ${task.subnet_slug ?? "(network / unscoped)"}`,
|
|
3539
|
+
` metadata : org_id=${String(task.metadata?.org_id ?? opts.org)} org_publish=${String(task.metadata?.org_publish ?? true)}`
|
|
3540
|
+
];
|
|
3541
|
+
if (body.fence) {
|
|
3542
|
+
lines.push(
|
|
3543
|
+
" Note : fenced \u2014 task.* may hit the Org harness webhook"
|
|
3544
|
+
);
|
|
3545
|
+
}
|
|
3546
|
+
output(task, lines.join("\n"));
|
|
3547
|
+
} catch (err) {
|
|
3548
|
+
handleError(err);
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
);
|
|
3552
|
+
cmd.command("import-task").description(
|
|
3553
|
+
"Import a Task Pool task as Org work (governance only; links via task.metadata)"
|
|
3554
|
+
).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(
|
|
3555
|
+
async (opts) => {
|
|
3556
|
+
const config = loadConfig();
|
|
3557
|
+
if (!config.api_key) {
|
|
3558
|
+
console.error(
|
|
3559
|
+
"No API key found. Run `acn join` first or `acn config set api-key <key>`."
|
|
3560
|
+
);
|
|
3561
|
+
process.exit(1);
|
|
3562
|
+
}
|
|
3563
|
+
try {
|
|
3564
|
+
const body = { task_id: opts.task };
|
|
3565
|
+
if (opts.assignee) body.assignee_agent_id = opts.assignee;
|
|
3566
|
+
const res = await acnPost(`/orgs/${opts.org}/work/import-task`, body);
|
|
3567
|
+
const lines = [
|
|
3568
|
+
res.already_imported ? "Task already imported (idempotent)" : "Task imported as Org work",
|
|
3569
|
+
` Work ID : ${res.work_id}`,
|
|
3570
|
+
` Org : ${res.org_id}`,
|
|
3571
|
+
` Task ID : ${res.source_task_id ?? opts.task}`,
|
|
3572
|
+
` Status : ${res.status}`,
|
|
3573
|
+
` Title : ${res.title}`
|
|
3574
|
+
];
|
|
3575
|
+
output(res, lines.join("\n"));
|
|
3576
|
+
} catch (err) {
|
|
3577
|
+
handleError(err);
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
);
|
|
3149
3581
|
return cmd;
|
|
3150
3582
|
}
|
|
3151
3583
|
|