arc402-cli 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +17 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +205 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/wallet.d.ts.map +1 -1
- package/dist/commands/wallet.js +467 -23
- package/dist/commands/wallet.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -2
- package/dist/config.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +294 -208
- package/dist/daemon/index.js.map +1 -1
- package/dist/endpoint-notify.d.ts +7 -0
- package/dist/endpoint-notify.d.ts.map +1 -1
- package/dist/endpoint-notify.js +104 -0
- package/dist/endpoint-notify.js.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +2 -0
- package/dist/program.js.map +1 -1
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +565 -162
- package/dist/repl.js.map +1 -1
- package/dist/ui/banner.d.ts +2 -0
- package/dist/ui/banner.d.ts.map +1 -1
- package/dist/ui/banner.js +27 -18
- package/dist/ui/banner.js.map +1 -1
- package/dist/ui/format.d.ts.map +1 -1
- package/dist/ui/format.js +2 -0
- package/dist/ui/format.js.map +1 -1
- package/dist/ui/spinner.d.ts.map +1 -1
- package/dist/ui/spinner.js +11 -0
- package/dist/ui/spinner.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/config.ts +18 -2
- package/src/commands/doctor.ts +172 -0
- package/src/commands/wallet.ts +512 -35
- package/src/config.ts +10 -1
- package/src/daemon/index.ts +234 -140
- package/src/endpoint-notify.ts +73 -0
- package/src/index.ts +15 -1
- package/src/program.ts +2 -0
- package/src/repl.ts +673 -197
- package/src/ui/banner.ts +26 -19
- package/src/ui/format.ts +1 -0
- package/src/ui/spinner.ts +10 -0
package/dist/daemon/index.js
CHANGED
|
@@ -52,6 +52,7 @@ const net = __importStar(require("net"));
|
|
|
52
52
|
const http = __importStar(require("http"));
|
|
53
53
|
const ethers_1 = require("ethers");
|
|
54
54
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
55
|
+
const crypto = __importStar(require("crypto"));
|
|
55
56
|
const config_1 = require("./config");
|
|
56
57
|
const wallet_monitor_1 = require("./wallet-monitor");
|
|
57
58
|
const notify_1 = require("./notify");
|
|
@@ -144,6 +145,38 @@ function openStateDB(dbPath) {
|
|
|
144
145
|
},
|
|
145
146
|
};
|
|
146
147
|
}
|
|
148
|
+
// ─── Auth token ───────────────────────────────────────────────────────────────
|
|
149
|
+
const DAEMON_TOKEN_FILE = path.join(path.dirname(config_1.DAEMON_SOCK), "daemon.token");
|
|
150
|
+
function generateApiToken() {
|
|
151
|
+
return crypto.randomBytes(32).toString("hex");
|
|
152
|
+
}
|
|
153
|
+
function saveApiToken(token) {
|
|
154
|
+
fs.mkdirSync(path.dirname(DAEMON_TOKEN_FILE), { recursive: true, mode: 0o700 });
|
|
155
|
+
fs.writeFileSync(DAEMON_TOKEN_FILE, token, { mode: 0o600 });
|
|
156
|
+
}
|
|
157
|
+
function loadApiToken() {
|
|
158
|
+
try {
|
|
159
|
+
return fs.readFileSync(DAEMON_TOKEN_FILE, "utf-8").trim();
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const rateLimitMap = new Map();
|
|
166
|
+
const RATE_LIMIT = 30;
|
|
167
|
+
const RATE_WINDOW_MS = 60000;
|
|
168
|
+
function checkRateLimit(ip) {
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
let bucket = rateLimitMap.get(ip);
|
|
171
|
+
if (!bucket || now >= bucket.resetTime) {
|
|
172
|
+
bucket = { count: 0, resetTime: now + RATE_WINDOW_MS };
|
|
173
|
+
rateLimitMap.set(ip, bucket);
|
|
174
|
+
}
|
|
175
|
+
bucket.count++;
|
|
176
|
+
return bucket.count <= RATE_LIMIT;
|
|
177
|
+
}
|
|
178
|
+
// ─── Body size limit ──────────────────────────────────────────────────────────
|
|
179
|
+
const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
|
|
147
180
|
// ─── Logger ───────────────────────────────────────────────────────────────────
|
|
148
181
|
function openLogger(logPath, foreground) {
|
|
149
182
|
let stream = null;
|
|
@@ -161,13 +194,14 @@ function openLogger(logPath, foreground) {
|
|
|
161
194
|
}
|
|
162
195
|
};
|
|
163
196
|
}
|
|
164
|
-
function startIpcServer(ctx, log) {
|
|
197
|
+
function startIpcServer(ctx, log, apiToken) {
|
|
165
198
|
// Remove stale socket
|
|
166
199
|
if (fs.existsSync(config_1.DAEMON_SOCK)) {
|
|
167
200
|
fs.unlinkSync(config_1.DAEMON_SOCK);
|
|
168
201
|
}
|
|
169
202
|
const server = net.createServer((socket) => {
|
|
170
203
|
let buf = "";
|
|
204
|
+
let authenticated = false;
|
|
171
205
|
socket.on("data", (data) => {
|
|
172
206
|
buf += data.toString();
|
|
173
207
|
const lines = buf.split("\n");
|
|
@@ -183,6 +217,19 @@ function startIpcServer(ctx, log) {
|
|
|
183
217
|
socket.write(JSON.stringify({ ok: false, error: "invalid_json" }) + "\n");
|
|
184
218
|
continue;
|
|
185
219
|
}
|
|
220
|
+
// First message must be auth
|
|
221
|
+
if (!authenticated) {
|
|
222
|
+
if (cmd.auth === apiToken) {
|
|
223
|
+
authenticated = true;
|
|
224
|
+
socket.write(JSON.stringify({ ok: true, authenticated: true }) + "\n");
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
log({ event: "ipc_auth_failed" });
|
|
228
|
+
socket.write(JSON.stringify({ ok: false, error: "unauthorized" }) + "\n");
|
|
229
|
+
socket.destroy();
|
|
230
|
+
}
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
186
233
|
const response = handleIpcCommand(cmd, ctx, log);
|
|
187
234
|
socket.write(JSON.stringify(response) + "\n");
|
|
188
235
|
}
|
|
@@ -481,10 +528,38 @@ async function runDaemon(foreground = false) {
|
|
|
481
528
|
fs.writeFileSync(config_1.DAEMON_PID, String(process.pid), { mode: 0o600 });
|
|
482
529
|
log({ event: "pid_written", pid: process.pid, path: config_1.DAEMON_PID });
|
|
483
530
|
}
|
|
531
|
+
// ── Generate and save API token ──────────────────────────────────────────
|
|
532
|
+
const apiToken = generateApiToken();
|
|
533
|
+
saveApiToken(apiToken);
|
|
534
|
+
log({ event: "auth_token_saved", path: DAEMON_TOKEN_FILE });
|
|
484
535
|
// ── Start IPC socket ─────────────────────────────────────────────────────
|
|
485
|
-
const ipcServer = startIpcServer(ipcCtx, log);
|
|
536
|
+
const ipcServer = startIpcServer(ipcCtx, log, apiToken);
|
|
486
537
|
// ── Start HTTP relay server (public endpoint) ────────────────────────────
|
|
487
538
|
const httpPort = config.relay.listen_port ?? 4402;
|
|
539
|
+
/**
|
|
540
|
+
* Read request body with a size cap. Destroys the request and sends 413
|
|
541
|
+
* if the body exceeds MAX_BODY_SIZE. Returns null in that case.
|
|
542
|
+
*/
|
|
543
|
+
function readBody(req, res) {
|
|
544
|
+
return new Promise((resolve) => {
|
|
545
|
+
let body = "";
|
|
546
|
+
let size = 0;
|
|
547
|
+
req.on("data", (chunk) => {
|
|
548
|
+
size += chunk.length;
|
|
549
|
+
if (size > MAX_BODY_SIZE) {
|
|
550
|
+
req.destroy();
|
|
551
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
552
|
+
res.end(JSON.stringify({ error: "payload_too_large" }));
|
|
553
|
+
resolve(null);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
body += chunk.toString();
|
|
557
|
+
});
|
|
558
|
+
req.on("end", () => { resolve(body); });
|
|
559
|
+
req.on("error", () => { resolve(null); });
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
const PUBLIC_GET_PATHS = new Set(["/", "/health", "/agent", "/capabilities", "/status"]);
|
|
488
563
|
const httpServer = http.createServer(async (req, res) => {
|
|
489
564
|
// CORS headers
|
|
490
565
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -497,6 +572,25 @@ async function runDaemon(foreground = false) {
|
|
|
497
572
|
}
|
|
498
573
|
const url = new URL(req.url || "/", `http://localhost:${httpPort}`);
|
|
499
574
|
const pathname = url.pathname;
|
|
575
|
+
// Rate limiting (all endpoints)
|
|
576
|
+
const clientIp = (req.socket.remoteAddress ?? "unknown").replace(/^::ffff:/, "");
|
|
577
|
+
if (!checkRateLimit(clientIp)) {
|
|
578
|
+
log({ event: "rate_limited", ip: clientIp, path: pathname });
|
|
579
|
+
res.writeHead(429, { "Content-Type": "application/json" });
|
|
580
|
+
res.end(JSON.stringify({ error: "too_many_requests" }));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
// Auth required on all POST endpoints (GET public paths are open)
|
|
584
|
+
if (req.method === "POST" || (req.method === "GET" && !PUBLIC_GET_PATHS.has(pathname))) {
|
|
585
|
+
const authHeader = req.headers["authorization"] ?? "";
|
|
586
|
+
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
587
|
+
if (token !== apiToken) {
|
|
588
|
+
log({ event: "http_unauthorized", ip: clientIp, path: pathname });
|
|
589
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
590
|
+
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
500
594
|
// Health / info
|
|
501
595
|
if (pathname === "/" || pathname === "/health") {
|
|
502
596
|
const info = {
|
|
@@ -526,51 +620,35 @@ async function runDaemon(foreground = false) {
|
|
|
526
620
|
}
|
|
527
621
|
// Receive hire proposal
|
|
528
622
|
if (pathname === "/hire" && req.method === "POST") {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
id: proposal.messageId,
|
|
559
|
-
agreement_id: proposal.agreementId ?? null,
|
|
560
|
-
hirer_address: proposal.hirerAddress,
|
|
561
|
-
capability: proposal.capability,
|
|
562
|
-
price_eth: proposal.priceEth,
|
|
563
|
-
deadline_unix: proposal.deadlineUnix,
|
|
564
|
-
spec_hash: proposal.specHash,
|
|
565
|
-
status: "rejected",
|
|
566
|
-
reject_reason: policyResult.reason ?? "policy_violation",
|
|
567
|
-
});
|
|
568
|
-
log({ event: "http_hire_rejected", id: proposal.messageId, reason: policyResult.reason });
|
|
569
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
570
|
-
res.end(JSON.stringify({ status: "rejected", reason: policyResult.reason, id: proposal.messageId }));
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
const status = config.policy.auto_accept ? "accepted" : "pending_approval";
|
|
623
|
+
const body = await readBody(req, res);
|
|
624
|
+
if (body === null)
|
|
625
|
+
return;
|
|
626
|
+
try {
|
|
627
|
+
const msg = JSON.parse(body);
|
|
628
|
+
// Feed into the hire listener's message handler
|
|
629
|
+
const proposal = {
|
|
630
|
+
messageId: String(msg.messageId ?? msg.id ?? `http_${Date.now()}`),
|
|
631
|
+
hirerAddress: String(msg.hirerAddress ?? msg.hirer_address ?? msg.from ?? ""),
|
|
632
|
+
capability: String(msg.capability ?? ""),
|
|
633
|
+
priceEth: String(msg.priceEth ?? msg.price_eth ?? "0"),
|
|
634
|
+
deadlineUnix: Number(msg.deadlineUnix ?? msg.deadline ?? 0),
|
|
635
|
+
specHash: String(msg.specHash ?? msg.spec_hash ?? ""),
|
|
636
|
+
agreementId: msg.agreementId ? String(msg.agreementId) : undefined,
|
|
637
|
+
signature: msg.signature ? String(msg.signature) : undefined,
|
|
638
|
+
};
|
|
639
|
+
// Dedup
|
|
640
|
+
const existing = db.getHireRequest(proposal.messageId);
|
|
641
|
+
if (existing) {
|
|
642
|
+
log({ event: "hire_duplicate", id: proposal.messageId, status: existing.status });
|
|
643
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
644
|
+
res.end(JSON.stringify({ status: existing.status, id: proposal.messageId }));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
// Policy check
|
|
648
|
+
const { evaluatePolicy } = await Promise.resolve().then(() => __importStar(require("./hire-listener")));
|
|
649
|
+
const activeCount = db.countActiveHireRequests();
|
|
650
|
+
const policyResult = evaluatePolicy(proposal, config, activeCount);
|
|
651
|
+
if (!policyResult.allowed) {
|
|
574
652
|
db.insertHireRequest({
|
|
575
653
|
id: proposal.messageId,
|
|
576
654
|
agreement_id: proposal.agreementId ?? null,
|
|
@@ -579,206 +657,214 @@ async function runDaemon(foreground = false) {
|
|
|
579
657
|
price_eth: proposal.priceEth,
|
|
580
658
|
deadline_unix: proposal.deadlineUnix,
|
|
581
659
|
spec_hash: proposal.specHash,
|
|
582
|
-
status,
|
|
583
|
-
reject_reason:
|
|
660
|
+
status: "rejected",
|
|
661
|
+
reject_reason: policyResult.reason ?? "policy_violation",
|
|
584
662
|
});
|
|
585
|
-
log({ event: "
|
|
586
|
-
if (config.notifications.notify_on_hire_request) {
|
|
587
|
-
await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
|
|
588
|
-
}
|
|
663
|
+
log({ event: "http_hire_rejected", id: proposal.messageId, reason: policyResult.reason });
|
|
589
664
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
590
|
-
res.end(JSON.stringify({ status, id: proposal.messageId }));
|
|
665
|
+
res.end(JSON.stringify({ status: "rejected", reason: policyResult.reason, id: proposal.messageId }));
|
|
666
|
+
return;
|
|
591
667
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
668
|
+
const status = config.policy.auto_accept ? "accepted" : "pending_approval";
|
|
669
|
+
db.insertHireRequest({
|
|
670
|
+
id: proposal.messageId,
|
|
671
|
+
agreement_id: proposal.agreementId ?? null,
|
|
672
|
+
hirer_address: proposal.hirerAddress,
|
|
673
|
+
capability: proposal.capability,
|
|
674
|
+
price_eth: proposal.priceEth,
|
|
675
|
+
deadline_unix: proposal.deadlineUnix,
|
|
676
|
+
spec_hash: proposal.specHash,
|
|
677
|
+
status,
|
|
678
|
+
reject_reason: null,
|
|
679
|
+
});
|
|
680
|
+
log({ event: "http_hire_received", id: proposal.messageId, hirer: proposal.hirerAddress, status });
|
|
681
|
+
if (config.notifications.notify_on_hire_request) {
|
|
682
|
+
await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
|
|
596
683
|
}
|
|
597
|
-
|
|
684
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
685
|
+
res.end(JSON.stringify({ status, id: proposal.messageId }));
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
log({ event: "http_hire_error", error: String(err) });
|
|
689
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
690
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
691
|
+
}
|
|
598
692
|
return;
|
|
599
693
|
}
|
|
600
694
|
// Handshake acknowledgment endpoint
|
|
601
695
|
if (pathname === "/handshake" && req.method === "POST") {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
});
|
|
696
|
+
const body = await readBody(req, res);
|
|
697
|
+
if (body === null)
|
|
698
|
+
return;
|
|
699
|
+
try {
|
|
700
|
+
const msg = JSON.parse(body);
|
|
701
|
+
log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
|
|
702
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
703
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
707
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
708
|
+
}
|
|
616
709
|
return;
|
|
617
710
|
}
|
|
618
711
|
// POST /hire/accepted — provider accepted, client notified
|
|
619
712
|
if (pathname === "/hire/accepted" && req.method === "POST") {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
}
|
|
631
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
632
|
-
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
713
|
+
const body = await readBody(req, res);
|
|
714
|
+
if (body === null)
|
|
715
|
+
return;
|
|
716
|
+
try {
|
|
717
|
+
const msg = JSON.parse(body);
|
|
718
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
719
|
+
const from = String(msg.from ?? "");
|
|
720
|
+
log({ event: "hire_accepted_inbound", agreementId, from });
|
|
721
|
+
if (config.notifications.notify_on_hire_accepted) {
|
|
722
|
+
await notifier.notifyHireAccepted(agreementId, agreementId);
|
|
633
723
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
724
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
725
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
729
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
730
|
+
}
|
|
639
731
|
return;
|
|
640
732
|
}
|
|
641
733
|
// POST /message — off-chain negotiation message
|
|
642
734
|
if (pathname === "/message" && req.method === "POST") {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
});
|
|
735
|
+
const body = await readBody(req, res);
|
|
736
|
+
if (body === null)
|
|
737
|
+
return;
|
|
738
|
+
try {
|
|
739
|
+
const msg = JSON.parse(body);
|
|
740
|
+
const from = String(msg.from ?? "");
|
|
741
|
+
const to = String(msg.to ?? "");
|
|
742
|
+
const content = String(msg.content ?? "");
|
|
743
|
+
const timestamp = Number(msg.timestamp ?? Date.now());
|
|
744
|
+
log({ event: "message_received", from, to, timestamp, content_len: content.length });
|
|
745
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
746
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
750
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
751
|
+
}
|
|
661
752
|
return;
|
|
662
753
|
}
|
|
663
754
|
// POST /delivery — provider committed a deliverable
|
|
664
755
|
if (pathname === "/delivery" && req.method === "POST") {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
683
|
-
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
684
|
-
}
|
|
685
|
-
catch {
|
|
686
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
687
|
-
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
756
|
+
const body = await readBody(req, res);
|
|
757
|
+
if (body === null)
|
|
758
|
+
return;
|
|
759
|
+
try {
|
|
760
|
+
const msg = JSON.parse(body);
|
|
761
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
762
|
+
const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
|
|
763
|
+
const from = String(msg.from ?? "");
|
|
764
|
+
log({ event: "delivery_received", agreementId, deliverableHash, from });
|
|
765
|
+
// Update DB: mark delivered
|
|
766
|
+
const active = db.listActiveHireRequests();
|
|
767
|
+
const found = active.find(r => r.agreement_id === agreementId);
|
|
768
|
+
if (found)
|
|
769
|
+
db.updateHireRequestStatus(found.id, "delivered");
|
|
770
|
+
if (config.notifications.notify_on_delivery) {
|
|
771
|
+
await notifier.notifyDelivery(agreementId, deliverableHash, "");
|
|
688
772
|
}
|
|
689
|
-
|
|
773
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
774
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
775
|
+
}
|
|
776
|
+
catch {
|
|
777
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
778
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
779
|
+
}
|
|
690
780
|
return;
|
|
691
781
|
}
|
|
692
782
|
// POST /delivery/accepted — client accepted delivery, payment releasing
|
|
693
783
|
if (pathname === "/delivery/accepted" && req.method === "POST") {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
});
|
|
784
|
+
const body = await readBody(req, res);
|
|
785
|
+
if (body === null)
|
|
786
|
+
return;
|
|
787
|
+
try {
|
|
788
|
+
const msg = JSON.parse(body);
|
|
789
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
790
|
+
const from = String(msg.from ?? "");
|
|
791
|
+
log({ event: "delivery_accepted_inbound", agreementId, from });
|
|
792
|
+
// Update DB: mark complete
|
|
793
|
+
const all = db.listActiveHireRequests();
|
|
794
|
+
const found = all.find(r => r.agreement_id === agreementId);
|
|
795
|
+
if (found)
|
|
796
|
+
db.updateHireRequestStatus(found.id, "complete");
|
|
797
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
798
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
799
|
+
}
|
|
800
|
+
catch {
|
|
801
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
802
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
803
|
+
}
|
|
715
804
|
return;
|
|
716
805
|
}
|
|
717
806
|
// POST /dispute — dispute raised against this agent
|
|
718
807
|
if (pathname === "/dispute" && req.method === "POST") {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
732
|
-
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
808
|
+
const body = await readBody(req, res);
|
|
809
|
+
if (body === null)
|
|
810
|
+
return;
|
|
811
|
+
try {
|
|
812
|
+
const msg = JSON.parse(body);
|
|
813
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
814
|
+
const reason = String(msg.reason ?? "");
|
|
815
|
+
const from = String(msg.from ?? "");
|
|
816
|
+
log({ event: "dispute_received", agreementId, reason, from });
|
|
817
|
+
if (config.notifications.notify_on_dispute) {
|
|
818
|
+
await notifier.notifyDispute(agreementId, from);
|
|
733
819
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
820
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
821
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
825
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
826
|
+
}
|
|
739
827
|
return;
|
|
740
828
|
}
|
|
741
829
|
// POST /dispute/resolved — dispute resolved by arbitrator
|
|
742
830
|
if (pathname === "/dispute/resolved" && req.method === "POST") {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
});
|
|
831
|
+
const body = await readBody(req, res);
|
|
832
|
+
if (body === null)
|
|
833
|
+
return;
|
|
834
|
+
try {
|
|
835
|
+
const msg = JSON.parse(body);
|
|
836
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
837
|
+
const outcome = String(msg.outcome ?? "");
|
|
838
|
+
const from = String(msg.from ?? "");
|
|
839
|
+
log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
|
|
840
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
841
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
845
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
846
|
+
}
|
|
760
847
|
return;
|
|
761
848
|
}
|
|
762
849
|
// POST /workroom/status — workroom lifecycle events
|
|
763
850
|
if (pathname === "/workroom/status" && req.method === "POST") {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
});
|
|
851
|
+
const body = await readBody(req, res);
|
|
852
|
+
if (body === null)
|
|
853
|
+
return;
|
|
854
|
+
try {
|
|
855
|
+
const msg = JSON.parse(body);
|
|
856
|
+
const event = String(msg.event ?? "");
|
|
857
|
+
const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
|
|
858
|
+
const jobId = msg.jobId ? String(msg.jobId) : undefined;
|
|
859
|
+
const timestamp = Number(msg.timestamp ?? Date.now());
|
|
860
|
+
log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
|
|
861
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
862
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
866
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
867
|
+
}
|
|
782
868
|
return;
|
|
783
869
|
}
|
|
784
870
|
// GET /capabilities — agent capabilities from config
|