arc402-cli 0.3.0 → 0.3.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/dist/commands/accept.d.ts.map +1 -1
- package/dist/commands/accept.js +26 -1
- package/dist/commands/accept.js.map +1 -1
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +166 -0
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/arena-handshake.d.ts.map +1 -1
- package/dist/commands/arena-handshake.js +22 -0
- package/dist/commands/arena-handshake.js.map +1 -1
- package/dist/commands/arena.d.ts +3 -0
- package/dist/commands/arena.d.ts.map +1 -0
- package/dist/commands/arena.js +122 -0
- package/dist/commands/arena.js.map +1 -0
- package/dist/commands/deliver.d.ts.map +1 -1
- package/dist/commands/deliver.js +19 -0
- package/dist/commands/deliver.js.map +1 -1
- package/dist/commands/feed.d.ts +10 -0
- package/dist/commands/feed.d.ts.map +1 -0
- package/dist/commands/feed.js +203 -0
- package/dist/commands/feed.js.map +1 -0
- package/dist/commands/hire.d.ts.map +1 -1
- package/dist/commands/hire.js +29 -0
- package/dist/commands/hire.js.map +1 -1
- package/dist/commands/workroom.d.ts.map +1 -1
- package/dist/commands/workroom.js +37 -0
- package/dist/commands/workroom.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +336 -1
- package/dist/daemon/index.js.map +1 -1
- package/dist/endpoint-notify.d.ts +18 -0
- package/dist/endpoint-notify.d.ts.map +1 -0
- package/dist/endpoint-notify.js +43 -0
- package/dist/endpoint-notify.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/accept.ts +31 -1
- package/src/commands/agent.ts +187 -0
- package/src/commands/arena-handshake.ts +33 -0
- package/src/commands/arena.ts +121 -0
- package/src/commands/deliver.ts +19 -0
- package/src/commands/feed.ts +228 -0
- package/src/commands/hire.ts +31 -1
- package/src/commands/workroom.ts +44 -0
- package/src/daemon/index.ts +347 -1
- package/src/endpoint-notify.ts +46 -0
- package/src/index.ts +4 -0
package/src/commands/hire.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { requireSigner } from "../client";
|
|
|
6
6
|
import { hashFile, hashString } from "../utils/hash";
|
|
7
7
|
import { parseDuration } from "../utils/time";
|
|
8
8
|
import { printSenderInfo, executeContractWriteViaWallet } from "../wallet-router";
|
|
9
|
-
import { SERVICE_AGREEMENT_ABI } from "../abis";
|
|
9
|
+
import { AGENT_REGISTRY_ABI, SERVICE_AGREEMENT_ABI } from "../abis";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_REGISTRY_ADDRESS = "0xD5c2851B00090c92Ba7F4723FB548bb30C9B6865";
|
|
10
12
|
|
|
11
13
|
const sessionManager = new SessionManager();
|
|
12
14
|
|
|
@@ -183,6 +185,34 @@ export function registerHireCommand(program: Command): void {
|
|
|
183
185
|
agreementId = result.agreementId;
|
|
184
186
|
}
|
|
185
187
|
|
|
188
|
+
// Notify provider's HTTP endpoint (non-blocking)
|
|
189
|
+
const hireRegistryAddress = config.agentRegistryV2Address ?? config.agentRegistryAddress ?? DEFAULT_REGISTRY_ADDRESS;
|
|
190
|
+
try {
|
|
191
|
+
const hireProvider = new ethers.JsonRpcProvider(config.rpcUrl);
|
|
192
|
+
const hireRegistry = new ethers.Contract(hireRegistryAddress, AGENT_REGISTRY_ABI, hireProvider);
|
|
193
|
+
const agentData = await hireRegistry.getAgent(opts.agent);
|
|
194
|
+
const endpoint = agentData.endpoint as string;
|
|
195
|
+
if (endpoint) {
|
|
196
|
+
await fetch(`${endpoint}/hire`, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: { "Content-Type": "application/json" },
|
|
199
|
+
body: JSON.stringify({
|
|
200
|
+
agreementId: agreementId!.toString(),
|
|
201
|
+
from: address,
|
|
202
|
+
provider: opts.agent,
|
|
203
|
+
serviceType: opts.serviceType,
|
|
204
|
+
task: opts.task,
|
|
205
|
+
price: price.toString(),
|
|
206
|
+
token,
|
|
207
|
+
deadline: deadlineSeconds,
|
|
208
|
+
deliverablesHash,
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.warn(`Warning: could not notify provider endpoint: ${err instanceof Error ? err.message : String(err)}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
186
216
|
if (opts.session) {
|
|
187
217
|
sessionManager.setOnChainId(opts.session, agreementId!.toString());
|
|
188
218
|
}
|
package/src/commands/workroom.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import * as os from "os";
|
|
5
|
+
import * as http from "http";
|
|
5
6
|
import { spawnSync, execSync } from "child_process";
|
|
6
7
|
import {
|
|
7
8
|
ARC402_DIR,
|
|
@@ -9,6 +10,45 @@ import {
|
|
|
9
10
|
} from "../openshell-runtime";
|
|
10
11
|
import { DAEMON_LOG, DAEMON_TOML } from "../daemon/config";
|
|
11
12
|
|
|
13
|
+
// ─── Daemon lifecycle notify ──────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
function notifyDaemonWorkroomStatus(
|
|
16
|
+
event: "entered" | "exited" | "job_started" | "job_completed",
|
|
17
|
+
agentAddress?: string,
|
|
18
|
+
jobId?: string,
|
|
19
|
+
port = 4402
|
|
20
|
+
): void {
|
|
21
|
+
try {
|
|
22
|
+
// Try to read port from daemon config
|
|
23
|
+
let daemonPort = port;
|
|
24
|
+
if (fs.existsSync(DAEMON_TOML)) {
|
|
25
|
+
try {
|
|
26
|
+
const { loadDaemonConfig } = require("../daemon/config") as typeof import("../daemon/config");
|
|
27
|
+
const cfg = loadDaemonConfig();
|
|
28
|
+
daemonPort = cfg.relay?.listen_port ?? port;
|
|
29
|
+
} catch { /* use default */ }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const payload = JSON.stringify({
|
|
33
|
+
event,
|
|
34
|
+
agentAddress: agentAddress ?? "",
|
|
35
|
+
jobId,
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const req = http.request({
|
|
40
|
+
hostname: "127.0.0.1",
|
|
41
|
+
port: daemonPort,
|
|
42
|
+
path: "/workroom/status",
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) },
|
|
45
|
+
});
|
|
46
|
+
req.on("error", () => { /* non-fatal */ });
|
|
47
|
+
req.write(payload);
|
|
48
|
+
req.end();
|
|
49
|
+
} catch { /* non-fatal */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
12
52
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
13
53
|
|
|
14
54
|
const WORKROOM_IMAGE = "arc402-workroom";
|
|
@@ -232,6 +272,8 @@ export function registerWorkroomCommands(program: Command): void {
|
|
|
232
272
|
console.log(` Policy hash: ${getPolicyHash()}`);
|
|
233
273
|
console.log(` Relay port: 4402`);
|
|
234
274
|
console.log(` Logs: arc402 workroom logs`);
|
|
275
|
+
// Notify local daemon of workroom entry
|
|
276
|
+
notifyDaemonWorkroomStatus("entered");
|
|
235
277
|
} else {
|
|
236
278
|
console.error("Workroom started but exited immediately. Check logs:");
|
|
237
279
|
console.error(" docker logs arc402-workroom");
|
|
@@ -249,6 +291,8 @@ export function registerWorkroomCommands(program: Command): void {
|
|
|
249
291
|
return;
|
|
250
292
|
}
|
|
251
293
|
console.log("Stopping ARC-402 Workroom...");
|
|
294
|
+
// Notify daemon before stopping (daemon may be inside container)
|
|
295
|
+
notifyDaemonWorkroomStatus("exited");
|
|
252
296
|
runCmd("docker", ["stop", WORKROOM_CONTAINER]);
|
|
253
297
|
console.log("✓ Workroom stopped");
|
|
254
298
|
});
|
package/src/daemon/index.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import * as fs from "fs";
|
|
11
11
|
import * as path from "path";
|
|
12
12
|
import * as net from "net";
|
|
13
|
+
import * as http from "http";
|
|
13
14
|
import { ethers } from "ethers";
|
|
14
15
|
import Database from "better-sqlite3";
|
|
15
16
|
|
|
@@ -549,6 +550,350 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
549
550
|
// ── Start IPC socket ─────────────────────────────────────────────────────
|
|
550
551
|
const ipcServer = startIpcServer(ipcCtx, log);
|
|
551
552
|
|
|
553
|
+
// ── Start HTTP relay server (public endpoint) ────────────────────────────
|
|
554
|
+
const httpPort = config.relay.listen_port ?? 4402;
|
|
555
|
+
|
|
556
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
557
|
+
// CORS headers
|
|
558
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
559
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
560
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
561
|
+
if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
|
|
562
|
+
|
|
563
|
+
const url = new URL(req.url || "/", `http://localhost:${httpPort}`);
|
|
564
|
+
const pathname = url.pathname;
|
|
565
|
+
|
|
566
|
+
// Health / info
|
|
567
|
+
if (pathname === "/" || pathname === "/health") {
|
|
568
|
+
const info = {
|
|
569
|
+
protocol: "arc-402",
|
|
570
|
+
version: "0.3.0",
|
|
571
|
+
agent: config.wallet.contract_address,
|
|
572
|
+
status: "online",
|
|
573
|
+
capabilities: config.policy.allowed_capabilities,
|
|
574
|
+
uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
|
|
575
|
+
};
|
|
576
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
577
|
+
res.end(JSON.stringify(info));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Agent info
|
|
582
|
+
if (pathname === "/agent") {
|
|
583
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
584
|
+
res.end(JSON.stringify({
|
|
585
|
+
wallet: config.wallet.contract_address,
|
|
586
|
+
owner: config.wallet.owner_address,
|
|
587
|
+
machineKey: machineKeyAddress,
|
|
588
|
+
chainId: config.network.chain_id,
|
|
589
|
+
bundlerMode: config.bundler.mode,
|
|
590
|
+
relay: config.relay.enabled,
|
|
591
|
+
}));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Receive hire proposal
|
|
596
|
+
if (pathname === "/hire" && req.method === "POST") {
|
|
597
|
+
let body = "";
|
|
598
|
+
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
599
|
+
req.on("end", async () => {
|
|
600
|
+
try {
|
|
601
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
602
|
+
|
|
603
|
+
// Feed into the hire listener's message handler
|
|
604
|
+
const proposal = {
|
|
605
|
+
messageId: String(msg.messageId ?? msg.id ?? `http_${Date.now()}`),
|
|
606
|
+
hirerAddress: String(msg.hirerAddress ?? msg.hirer_address ?? msg.from ?? ""),
|
|
607
|
+
capability: String(msg.capability ?? ""),
|
|
608
|
+
priceEth: String(msg.priceEth ?? msg.price_eth ?? "0"),
|
|
609
|
+
deadlineUnix: Number(msg.deadlineUnix ?? msg.deadline ?? 0),
|
|
610
|
+
specHash: String(msg.specHash ?? msg.spec_hash ?? ""),
|
|
611
|
+
agreementId: msg.agreementId ? String(msg.agreementId) : undefined,
|
|
612
|
+
signature: msg.signature ? String(msg.signature) : undefined,
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Dedup
|
|
616
|
+
const existing = db.getHireRequest(proposal.messageId);
|
|
617
|
+
if (existing) {
|
|
618
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
619
|
+
res.end(JSON.stringify({ status: existing.status, id: proposal.messageId }));
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Policy check
|
|
624
|
+
const { evaluatePolicy } = await import("./hire-listener");
|
|
625
|
+
const activeCount = db.countActiveHireRequests();
|
|
626
|
+
const policyResult = evaluatePolicy(proposal, config, activeCount);
|
|
627
|
+
|
|
628
|
+
if (!policyResult.allowed) {
|
|
629
|
+
db.insertHireRequest({
|
|
630
|
+
id: proposal.messageId,
|
|
631
|
+
agreement_id: proposal.agreementId ?? null,
|
|
632
|
+
hirer_address: proposal.hirerAddress,
|
|
633
|
+
capability: proposal.capability,
|
|
634
|
+
price_eth: proposal.priceEth,
|
|
635
|
+
deadline_unix: proposal.deadlineUnix,
|
|
636
|
+
spec_hash: proposal.specHash,
|
|
637
|
+
status: "rejected",
|
|
638
|
+
reject_reason: policyResult.reason ?? "policy_violation",
|
|
639
|
+
});
|
|
640
|
+
log({ event: "http_hire_rejected", id: proposal.messageId, reason: policyResult.reason });
|
|
641
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
642
|
+
res.end(JSON.stringify({ status: "rejected", reason: policyResult.reason, id: proposal.messageId }));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const status = config.policy.auto_accept ? "accepted" : "pending_approval";
|
|
647
|
+
db.insertHireRequest({
|
|
648
|
+
id: proposal.messageId,
|
|
649
|
+
agreement_id: proposal.agreementId ?? null,
|
|
650
|
+
hirer_address: proposal.hirerAddress,
|
|
651
|
+
capability: proposal.capability,
|
|
652
|
+
price_eth: proposal.priceEth,
|
|
653
|
+
deadline_unix: proposal.deadlineUnix,
|
|
654
|
+
spec_hash: proposal.specHash,
|
|
655
|
+
status,
|
|
656
|
+
reject_reason: null,
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
log({ event: "http_hire_received", id: proposal.messageId, hirer: proposal.hirerAddress, status });
|
|
660
|
+
|
|
661
|
+
if (config.notifications.notify_on_hire_request) {
|
|
662
|
+
await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
666
|
+
res.end(JSON.stringify({ status, id: proposal.messageId }));
|
|
667
|
+
} catch (err) {
|
|
668
|
+
log({ event: "http_hire_error", error: String(err) });
|
|
669
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
670
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Handshake acknowledgment endpoint
|
|
677
|
+
if (pathname === "/handshake" && req.method === "POST") {
|
|
678
|
+
let body = "";
|
|
679
|
+
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
680
|
+
req.on("end", () => {
|
|
681
|
+
try {
|
|
682
|
+
const msg = JSON.parse(body);
|
|
683
|
+
log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
|
|
684
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
685
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
686
|
+
} catch {
|
|
687
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
688
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// POST /hire/accepted — provider accepted, client notified
|
|
695
|
+
if (pathname === "/hire/accepted" && req.method === "POST") {
|
|
696
|
+
let body = "";
|
|
697
|
+
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
698
|
+
req.on("end", async () => {
|
|
699
|
+
try {
|
|
700
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
701
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
702
|
+
const from = String(msg.from ?? "");
|
|
703
|
+
log({ event: "hire_accepted_inbound", agreementId, from });
|
|
704
|
+
if (config.notifications.notify_on_hire_accepted) {
|
|
705
|
+
await notifier.notifyHireAccepted(agreementId, agreementId);
|
|
706
|
+
}
|
|
707
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
708
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
709
|
+
} catch {
|
|
710
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
711
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// POST /message — off-chain negotiation message
|
|
718
|
+
if (pathname === "/message" && req.method === "POST") {
|
|
719
|
+
let body = "";
|
|
720
|
+
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
721
|
+
req.on("end", () => {
|
|
722
|
+
try {
|
|
723
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
724
|
+
const from = String(msg.from ?? "");
|
|
725
|
+
const to = String(msg.to ?? "");
|
|
726
|
+
const content = String(msg.content ?? "");
|
|
727
|
+
const timestamp = Number(msg.timestamp ?? Date.now());
|
|
728
|
+
log({ event: "message_received", from, to, timestamp, content_len: content.length });
|
|
729
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
730
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
731
|
+
} catch {
|
|
732
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
733
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// POST /delivery — provider committed a deliverable
|
|
740
|
+
if (pathname === "/delivery" && req.method === "POST") {
|
|
741
|
+
let body = "";
|
|
742
|
+
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
743
|
+
req.on("end", async () => {
|
|
744
|
+
try {
|
|
745
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
746
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
747
|
+
const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
|
|
748
|
+
const from = String(msg.from ?? "");
|
|
749
|
+
log({ event: "delivery_received", agreementId, deliverableHash, from });
|
|
750
|
+
// Update DB: mark delivered
|
|
751
|
+
const active = db.listActiveHireRequests();
|
|
752
|
+
const found = active.find(r => r.agreement_id === agreementId);
|
|
753
|
+
if (found) db.updateHireRequestStatus(found.id, "delivered");
|
|
754
|
+
if (config.notifications.notify_on_delivery) {
|
|
755
|
+
await notifier.notifyDelivery(agreementId, deliverableHash, "");
|
|
756
|
+
}
|
|
757
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
758
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
759
|
+
} catch {
|
|
760
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
761
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// POST /delivery/accepted — client accepted delivery, payment releasing
|
|
768
|
+
if (pathname === "/delivery/accepted" && req.method === "POST") {
|
|
769
|
+
let body = "";
|
|
770
|
+
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
771
|
+
req.on("end", () => {
|
|
772
|
+
try {
|
|
773
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
774
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
775
|
+
const from = String(msg.from ?? "");
|
|
776
|
+
log({ event: "delivery_accepted_inbound", agreementId, from });
|
|
777
|
+
// Update DB: mark complete
|
|
778
|
+
const all = db.listActiveHireRequests();
|
|
779
|
+
const found = all.find(r => r.agreement_id === agreementId);
|
|
780
|
+
if (found) db.updateHireRequestStatus(found.id, "complete");
|
|
781
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
782
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
783
|
+
} catch {
|
|
784
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
785
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// POST /dispute — dispute raised against this agent
|
|
792
|
+
if (pathname === "/dispute" && req.method === "POST") {
|
|
793
|
+
let body = "";
|
|
794
|
+
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
795
|
+
req.on("end", async () => {
|
|
796
|
+
try {
|
|
797
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
798
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
799
|
+
const reason = String(msg.reason ?? "");
|
|
800
|
+
const from = String(msg.from ?? "");
|
|
801
|
+
log({ event: "dispute_received", agreementId, reason, from });
|
|
802
|
+
if (config.notifications.notify_on_dispute) {
|
|
803
|
+
await notifier.notifyDispute(agreementId, from);
|
|
804
|
+
}
|
|
805
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
806
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
807
|
+
} catch {
|
|
808
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
809
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// POST /dispute/resolved — dispute resolved by arbitrator
|
|
816
|
+
if (pathname === "/dispute/resolved" && req.method === "POST") {
|
|
817
|
+
let body = "";
|
|
818
|
+
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
819
|
+
req.on("end", () => {
|
|
820
|
+
try {
|
|
821
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
822
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
823
|
+
const outcome = String(msg.outcome ?? "");
|
|
824
|
+
const from = String(msg.from ?? "");
|
|
825
|
+
log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
|
|
826
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
827
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
828
|
+
} catch {
|
|
829
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
830
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// POST /workroom/status — workroom lifecycle events
|
|
837
|
+
if (pathname === "/workroom/status" && req.method === "POST") {
|
|
838
|
+
let body = "";
|
|
839
|
+
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
840
|
+
req.on("end", () => {
|
|
841
|
+
try {
|
|
842
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
843
|
+
const event = String(msg.event ?? "");
|
|
844
|
+
const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
|
|
845
|
+
const jobId = msg.jobId ? String(msg.jobId) : undefined;
|
|
846
|
+
const timestamp = Number(msg.timestamp ?? Date.now());
|
|
847
|
+
log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
|
|
848
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
849
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
850
|
+
} catch {
|
|
851
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
852
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// GET /capabilities — agent capabilities from config
|
|
859
|
+
if (pathname === "/capabilities" && req.method === "GET") {
|
|
860
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
861
|
+
res.end(JSON.stringify({
|
|
862
|
+
capabilities: config.policy.allowed_capabilities,
|
|
863
|
+
max_price_eth: config.policy.max_price_eth,
|
|
864
|
+
auto_accept: config.policy.auto_accept,
|
|
865
|
+
max_concurrent_agreements: config.relay.max_concurrent_agreements,
|
|
866
|
+
}));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// GET /status — health with active agreement count
|
|
871
|
+
if (pathname === "/status" && req.method === "GET") {
|
|
872
|
+
const activeList = db.listActiveHireRequests();
|
|
873
|
+
const pendingList = db.listPendingHireRequests();
|
|
874
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
875
|
+
res.end(JSON.stringify({
|
|
876
|
+
protocol: "arc-402",
|
|
877
|
+
version: "0.3.0",
|
|
878
|
+
agent: config.wallet.contract_address,
|
|
879
|
+
status: "online",
|
|
880
|
+
uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
|
|
881
|
+
active_agreements: activeList.length,
|
|
882
|
+
pending_approval: pendingList.length,
|
|
883
|
+
capabilities: config.policy.allowed_capabilities,
|
|
884
|
+
}));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// 404
|
|
889
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
890
|
+
res.end(JSON.stringify({ error: "not_found" }));
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
httpServer.listen(httpPort, "0.0.0.0", () => {
|
|
894
|
+
log({ event: "http_server_started", port: httpPort });
|
|
895
|
+
});
|
|
896
|
+
|
|
552
897
|
// ── Step 12: Startup complete ────────────────────────────────────────────
|
|
553
898
|
const subsystems = [];
|
|
554
899
|
if (config.relay.enabled) subsystems.push("relay");
|
|
@@ -573,7 +918,8 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
573
918
|
clearInterval(timeoutInterval);
|
|
574
919
|
clearInterval(balanceInterval);
|
|
575
920
|
|
|
576
|
-
// Close IPC
|
|
921
|
+
// Close HTTP + IPC
|
|
922
|
+
httpServer.close();
|
|
577
923
|
ipcServer.close();
|
|
578
924
|
if (fs.existsSync(DAEMON_SOCK)) fs.unlinkSync(DAEMON_SOCK);
|
|
579
925
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared endpoint-notify helper for CLI commands.
|
|
3
|
+
* Resolves an agent's registered HTTP endpoint from AgentRegistry
|
|
4
|
+
* and POSTs lifecycle events after onchain transactions.
|
|
5
|
+
*/
|
|
6
|
+
import { ethers } from "ethers";
|
|
7
|
+
import { AGENT_REGISTRY_ABI } from "./abis";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_REGISTRY_ADDRESS = "0xD5c2851B00090c92Ba7F4723FB548bb30C9B6865";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reads an agent's public HTTP endpoint from AgentRegistry.
|
|
13
|
+
* Returns empty string if not registered or no endpoint.
|
|
14
|
+
*/
|
|
15
|
+
export async function resolveAgentEndpoint(
|
|
16
|
+
address: string,
|
|
17
|
+
provider: ethers.Provider,
|
|
18
|
+
registryAddress = DEFAULT_REGISTRY_ADDRESS
|
|
19
|
+
): Promise<string> {
|
|
20
|
+
const registry = new ethers.Contract(registryAddress, AGENT_REGISTRY_ABI, provider);
|
|
21
|
+
const agentData = await registry.getAgent(address);
|
|
22
|
+
return (agentData.endpoint as string) ?? "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* POSTs JSON payload to {endpoint}{path}. Returns true on success.
|
|
27
|
+
* Never throws — logs a warning on failure.
|
|
28
|
+
*/
|
|
29
|
+
export async function notifyAgent(
|
|
30
|
+
endpoint: string,
|
|
31
|
+
path: string,
|
|
32
|
+
payload: Record<string, unknown>
|
|
33
|
+
): Promise<boolean> {
|
|
34
|
+
if (!endpoint) return false;
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`${endpoint}${path}`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
body: JSON.stringify(payload),
|
|
40
|
+
});
|
|
41
|
+
return res.ok;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.warn(`Warning: endpoint notify failed (${endpoint}${path}): ${err instanceof Error ? err.message : String(err)}`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,8 @@ import { registerContractInteractionCommands } from "./commands/contract-interac
|
|
|
30
30
|
import { registerWatchtowerCommands } from "./commands/watchtower";
|
|
31
31
|
import { registerColdStartCommands } from "./commands/coldstart";
|
|
32
32
|
import { registerMigrateCommands } from "./commands/migrate";
|
|
33
|
+
import { registerFeedCommand } from "./commands/feed";
|
|
34
|
+
import { registerArenaCommands } from "./commands/arena";
|
|
33
35
|
import reputation from "./commands/reputation.js";
|
|
34
36
|
import policy from "./commands/policy.js";
|
|
35
37
|
|
|
@@ -70,6 +72,8 @@ registerContractInteractionCommands(program);
|
|
|
70
72
|
registerWatchtowerCommands(program);
|
|
71
73
|
registerColdStartCommands(program);
|
|
72
74
|
registerMigrateCommands(program);
|
|
75
|
+
registerFeedCommand(program);
|
|
76
|
+
registerArenaCommands(program);
|
|
73
77
|
program.addCommand(reputation);
|
|
74
78
|
program.addCommand(policy);
|
|
75
79
|
program.parse(process.argv);
|