arc402-cli 0.6.0 → 0.7.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/dist/commands/backup.d.ts +3 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +106 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +11 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/discover.d.ts.map +1 -1
- package/dist/commands/discover.js +60 -15
- package/dist/commands/discover.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 +192 -58
- package/dist/commands/wallet.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +146 -9
- package/dist/commands/watch.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +35 -3
- package/dist/config.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +359 -220
- package/dist/daemon/index.js.map +1 -1
- package/dist/endpoint-notify.d.ts +9 -1
- package/dist/endpoint-notify.d.ts.map +1 -1
- package/dist/endpoint-notify.js +116 -3
- package/dist/endpoint-notify.js.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +4 -0
- package/dist/program.js.map +1 -1
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +45 -34
- package/dist/repl.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/package.json +1 -1
- package/src/commands/backup.ts +117 -0
- package/src/commands/config.ts +12 -2
- package/src/commands/discover.ts +74 -21
- package/src/commands/doctor.ts +172 -0
- package/src/commands/wallet.ts +194 -57
- package/src/commands/watch.ts +207 -10
- package/src/config.ts +48 -2
- package/src/daemon/index.ts +297 -152
- package/src/endpoint-notify.ts +86 -3
- package/src/index.ts +26 -0
- package/src/program.ts +4 -0
- package/src/repl.ts +53 -42
- package/src/ui/format.ts +1 -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,40 @@ 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
|
+
// Cleanup stale rate limit entries every 5 minutes to prevent unbounded growth
|
|
179
|
+
let rateLimitCleanupInterval = null;
|
|
180
|
+
// ─── Body size limit ──────────────────────────────────────────────────────────
|
|
181
|
+
const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
|
|
147
182
|
// ─── Logger ───────────────────────────────────────────────────────────────────
|
|
148
183
|
function openLogger(logPath, foreground) {
|
|
149
184
|
let stream = null;
|
|
@@ -161,13 +196,14 @@ function openLogger(logPath, foreground) {
|
|
|
161
196
|
}
|
|
162
197
|
};
|
|
163
198
|
}
|
|
164
|
-
function startIpcServer(ctx, log) {
|
|
199
|
+
function startIpcServer(ctx, log, apiToken) {
|
|
165
200
|
// Remove stale socket
|
|
166
201
|
if (fs.existsSync(config_1.DAEMON_SOCK)) {
|
|
167
202
|
fs.unlinkSync(config_1.DAEMON_SOCK);
|
|
168
203
|
}
|
|
169
204
|
const server = net.createServer((socket) => {
|
|
170
205
|
let buf = "";
|
|
206
|
+
let authenticated = false;
|
|
171
207
|
socket.on("data", (data) => {
|
|
172
208
|
buf += data.toString();
|
|
173
209
|
const lines = buf.split("\n");
|
|
@@ -183,6 +219,19 @@ function startIpcServer(ctx, log) {
|
|
|
183
219
|
socket.write(JSON.stringify({ ok: false, error: "invalid_json" }) + "\n");
|
|
184
220
|
continue;
|
|
185
221
|
}
|
|
222
|
+
// First message must be auth
|
|
223
|
+
if (!authenticated) {
|
|
224
|
+
if (cmd.auth === apiToken) {
|
|
225
|
+
authenticated = true;
|
|
226
|
+
socket.write(JSON.stringify({ ok: true, authenticated: true }) + "\n");
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
log({ event: "ipc_auth_failed" });
|
|
230
|
+
socket.write(JSON.stringify({ ok: false, error: "unauthorized" }) + "\n");
|
|
231
|
+
socket.destroy();
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
186
235
|
const response = handleIpcCommand(cmd, ctx, log);
|
|
187
236
|
socket.write(JSON.stringify(response) + "\n");
|
|
188
237
|
}
|
|
@@ -464,6 +513,14 @@ async function runDaemon(foreground = false) {
|
|
|
464
513
|
}
|
|
465
514
|
}
|
|
466
515
|
}, 30000);
|
|
516
|
+
// Rate limit map cleanup — every 5 minutes (prevents unbounded growth)
|
|
517
|
+
rateLimitCleanupInterval = setInterval(() => {
|
|
518
|
+
const now = Date.now();
|
|
519
|
+
for (const [ip, bucket] of rateLimitMap) {
|
|
520
|
+
if (bucket.resetTime < now)
|
|
521
|
+
rateLimitMap.delete(ip);
|
|
522
|
+
}
|
|
523
|
+
}, 5 * 60 * 1000);
|
|
467
524
|
// Balance monitor — every 5 minutes
|
|
468
525
|
const balanceInterval = setInterval(async () => {
|
|
469
526
|
try {
|
|
@@ -481,15 +538,76 @@ async function runDaemon(foreground = false) {
|
|
|
481
538
|
fs.writeFileSync(config_1.DAEMON_PID, String(process.pid), { mode: 0o600 });
|
|
482
539
|
log({ event: "pid_written", pid: process.pid, path: config_1.DAEMON_PID });
|
|
483
540
|
}
|
|
541
|
+
// ── Generate and save API token ──────────────────────────────────────────
|
|
542
|
+
const apiToken = generateApiToken();
|
|
543
|
+
saveApiToken(apiToken);
|
|
544
|
+
log({ event: "auth_token_saved", path: DAEMON_TOKEN_FILE });
|
|
484
545
|
// ── Start IPC socket ─────────────────────────────────────────────────────
|
|
485
|
-
const ipcServer = startIpcServer(ipcCtx, log);
|
|
546
|
+
const ipcServer = startIpcServer(ipcCtx, log, apiToken);
|
|
486
547
|
// ── Start HTTP relay server (public endpoint) ────────────────────────────
|
|
487
548
|
const httpPort = config.relay.listen_port ?? 4402;
|
|
549
|
+
/**
|
|
550
|
+
* Optionally verifies X-ARC402-Signature against the request body.
|
|
551
|
+
* Logs the result but never rejects — unsigned requests are accepted for backwards compat.
|
|
552
|
+
*/
|
|
553
|
+
function verifyRequestSignature(body, req) {
|
|
554
|
+
const sig = req.headers["x-arc402-signature"];
|
|
555
|
+
if (!sig)
|
|
556
|
+
return;
|
|
557
|
+
const claimedSigner = req.headers["x-arc402-signer"];
|
|
558
|
+
try {
|
|
559
|
+
const recovered = ethers_1.ethers.verifyMessage(body, sig);
|
|
560
|
+
if (claimedSigner && recovered.toLowerCase() !== claimedSigner.toLowerCase()) {
|
|
561
|
+
log({ event: "sig_mismatch", claimed: claimedSigner, recovered });
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
log({ event: "sig_verified", signer: recovered });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
log({ event: "sig_invalid" });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Read request body with a size cap. Destroys the request and sends 413
|
|
573
|
+
* if the body exceeds MAX_BODY_SIZE. Returns null in that case.
|
|
574
|
+
*/
|
|
575
|
+
function readBody(req, res) {
|
|
576
|
+
return new Promise((resolve) => {
|
|
577
|
+
let body = "";
|
|
578
|
+
let size = 0;
|
|
579
|
+
req.on("data", (chunk) => {
|
|
580
|
+
size += chunk.length;
|
|
581
|
+
if (size > MAX_BODY_SIZE) {
|
|
582
|
+
req.destroy();
|
|
583
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
584
|
+
res.end(JSON.stringify({ error: "payload_too_large" }));
|
|
585
|
+
resolve(null);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
body += chunk.toString();
|
|
589
|
+
});
|
|
590
|
+
req.on("end", () => { resolve(body); });
|
|
591
|
+
req.on("error", () => { resolve(null); });
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
const PUBLIC_GET_PATHS = new Set(["/", "/health", "/agent", "/capabilities", "/status"]);
|
|
595
|
+
// CORS whitelist — localhost for local tooling, arc402.xyz for the web app
|
|
596
|
+
const CORS_WHITELIST = new Set(["localhost", "127.0.0.1", "arc402.xyz", "app.arc402.xyz"]);
|
|
488
597
|
const httpServer = http.createServer(async (req, res) => {
|
|
489
|
-
// CORS
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
598
|
+
// CORS — only reflect origin header if it's in the whitelist
|
|
599
|
+
const origin = (req.headers["origin"] ?? "");
|
|
600
|
+
if (origin) {
|
|
601
|
+
try {
|
|
602
|
+
const { hostname } = new URL(origin);
|
|
603
|
+
if (CORS_WHITELIST.has(hostname)) {
|
|
604
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
605
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
606
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
catch { /* ignore invalid origin */ }
|
|
610
|
+
}
|
|
493
611
|
if (req.method === "OPTIONS") {
|
|
494
612
|
res.writeHead(204);
|
|
495
613
|
res.end();
|
|
@@ -497,6 +615,25 @@ async function runDaemon(foreground = false) {
|
|
|
497
615
|
}
|
|
498
616
|
const url = new URL(req.url || "/", `http://localhost:${httpPort}`);
|
|
499
617
|
const pathname = url.pathname;
|
|
618
|
+
// Rate limiting (all endpoints)
|
|
619
|
+
const clientIp = (req.socket.remoteAddress ?? "unknown").replace(/^::ffff:/, "");
|
|
620
|
+
if (!checkRateLimit(clientIp)) {
|
|
621
|
+
log({ event: "rate_limited", ip: clientIp, path: pathname });
|
|
622
|
+
res.writeHead(429, { "Content-Type": "application/json" });
|
|
623
|
+
res.end(JSON.stringify({ error: "too_many_requests" }));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
// Auth required on all POST endpoints (GET public paths are open)
|
|
627
|
+
if (req.method === "POST" || (req.method === "GET" && !PUBLIC_GET_PATHS.has(pathname))) {
|
|
628
|
+
const authHeader = req.headers["authorization"] ?? "";
|
|
629
|
+
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
630
|
+
if (token !== apiToken) {
|
|
631
|
+
log({ event: "http_unauthorized", ip: clientIp, path: pathname });
|
|
632
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
633
|
+
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
500
637
|
// Health / info
|
|
501
638
|
if (pathname === "/" || pathname === "/health") {
|
|
502
639
|
const info = {
|
|
@@ -526,51 +663,36 @@ async function runDaemon(foreground = false) {
|
|
|
526
663
|
}
|
|
527
664
|
// Receive hire proposal
|
|
528
665
|
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
|
-
|
|
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";
|
|
666
|
+
const body = await readBody(req, res);
|
|
667
|
+
if (body === null)
|
|
668
|
+
return;
|
|
669
|
+
verifyRequestSignature(body, req);
|
|
670
|
+
try {
|
|
671
|
+
const msg = JSON.parse(body);
|
|
672
|
+
// Feed into the hire listener's message handler
|
|
673
|
+
const proposal = {
|
|
674
|
+
messageId: String(msg.messageId ?? msg.id ?? `http_${Date.now()}`),
|
|
675
|
+
hirerAddress: String(msg.hirerAddress ?? msg.hirer_address ?? msg.from ?? ""),
|
|
676
|
+
capability: String(msg.capability ?? ""),
|
|
677
|
+
priceEth: String(msg.priceEth ?? msg.price_eth ?? "0"),
|
|
678
|
+
deadlineUnix: Number(msg.deadlineUnix ?? msg.deadline ?? 0),
|
|
679
|
+
specHash: String(msg.specHash ?? msg.spec_hash ?? ""),
|
|
680
|
+
agreementId: msg.agreementId ? String(msg.agreementId) : undefined,
|
|
681
|
+
signature: msg.signature ? String(msg.signature) : undefined,
|
|
682
|
+
};
|
|
683
|
+
// Dedup
|
|
684
|
+
const existing = db.getHireRequest(proposal.messageId);
|
|
685
|
+
if (existing) {
|
|
686
|
+
log({ event: "hire_duplicate", id: proposal.messageId, status: existing.status });
|
|
687
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
688
|
+
res.end(JSON.stringify({ status: existing.status, id: proposal.messageId }));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
// Policy check
|
|
692
|
+
const { evaluatePolicy } = await Promise.resolve().then(() => __importStar(require("./hire-listener")));
|
|
693
|
+
const activeCount = db.countActiveHireRequests();
|
|
694
|
+
const policyResult = evaluatePolicy(proposal, config, activeCount);
|
|
695
|
+
if (!policyResult.allowed) {
|
|
574
696
|
db.insertHireRequest({
|
|
575
697
|
id: proposal.messageId,
|
|
576
698
|
agreement_id: proposal.agreementId ?? null,
|
|
@@ -579,206 +701,217 @@ async function runDaemon(foreground = false) {
|
|
|
579
701
|
price_eth: proposal.priceEth,
|
|
580
702
|
deadline_unix: proposal.deadlineUnix,
|
|
581
703
|
spec_hash: proposal.specHash,
|
|
582
|
-
status,
|
|
583
|
-
reject_reason:
|
|
704
|
+
status: "rejected",
|
|
705
|
+
reject_reason: policyResult.reason ?? "policy_violation",
|
|
584
706
|
});
|
|
585
|
-
log({ event: "
|
|
586
|
-
if (config.notifications.notify_on_hire_request) {
|
|
587
|
-
await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
|
|
588
|
-
}
|
|
707
|
+
log({ event: "http_hire_rejected", id: proposal.messageId, reason: policyResult.reason });
|
|
589
708
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
590
|
-
res.end(JSON.stringify({ status, id: proposal.messageId }));
|
|
709
|
+
res.end(JSON.stringify({ status: "rejected", reason: policyResult.reason, id: proposal.messageId }));
|
|
710
|
+
return;
|
|
591
711
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
712
|
+
const status = config.policy.auto_accept ? "accepted" : "pending_approval";
|
|
713
|
+
db.insertHireRequest({
|
|
714
|
+
id: proposal.messageId,
|
|
715
|
+
agreement_id: proposal.agreementId ?? null,
|
|
716
|
+
hirer_address: proposal.hirerAddress,
|
|
717
|
+
capability: proposal.capability,
|
|
718
|
+
price_eth: proposal.priceEth,
|
|
719
|
+
deadline_unix: proposal.deadlineUnix,
|
|
720
|
+
spec_hash: proposal.specHash,
|
|
721
|
+
status,
|
|
722
|
+
reject_reason: null,
|
|
723
|
+
});
|
|
724
|
+
log({ event: "http_hire_received", id: proposal.messageId, hirer: proposal.hirerAddress, status });
|
|
725
|
+
if (config.notifications.notify_on_hire_request) {
|
|
726
|
+
await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
|
|
596
727
|
}
|
|
597
|
-
|
|
728
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
729
|
+
res.end(JSON.stringify({ status, id: proposal.messageId }));
|
|
730
|
+
}
|
|
731
|
+
catch (err) {
|
|
732
|
+
log({ event: "http_hire_error", error: String(err) });
|
|
733
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
734
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
735
|
+
}
|
|
598
736
|
return;
|
|
599
737
|
}
|
|
600
738
|
// Handshake acknowledgment endpoint
|
|
601
739
|
if (pathname === "/handshake" && req.method === "POST") {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
}
|
|
740
|
+
const body = await readBody(req, res);
|
|
741
|
+
if (body === null)
|
|
742
|
+
return;
|
|
743
|
+
verifyRequestSignature(body, req);
|
|
744
|
+
try {
|
|
745
|
+
const msg = JSON.parse(body);
|
|
746
|
+
log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
|
|
747
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
748
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
752
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
753
|
+
}
|
|
616
754
|
return;
|
|
617
755
|
}
|
|
618
756
|
// POST /hire/accepted — provider accepted, client notified
|
|
619
757
|
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 }));
|
|
758
|
+
const body = await readBody(req, res);
|
|
759
|
+
if (body === null)
|
|
760
|
+
return;
|
|
761
|
+
try {
|
|
762
|
+
const msg = JSON.parse(body);
|
|
763
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
764
|
+
const from = String(msg.from ?? "");
|
|
765
|
+
log({ event: "hire_accepted_inbound", agreementId, from });
|
|
766
|
+
if (config.notifications.notify_on_hire_accepted) {
|
|
767
|
+
await notifier.notifyHireAccepted(agreementId, agreementId);
|
|
633
768
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
769
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
770
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
774
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
775
|
+
}
|
|
639
776
|
return;
|
|
640
777
|
}
|
|
641
778
|
// POST /message — off-chain negotiation message
|
|
642
779
|
if (pathname === "/message" && req.method === "POST") {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
}
|
|
780
|
+
const body = await readBody(req, res);
|
|
781
|
+
if (body === null)
|
|
782
|
+
return;
|
|
783
|
+
verifyRequestSignature(body, req);
|
|
784
|
+
try {
|
|
785
|
+
const msg = JSON.parse(body);
|
|
786
|
+
const from = String(msg.from ?? "");
|
|
787
|
+
const to = String(msg.to ?? "");
|
|
788
|
+
const content = String(msg.content ?? "");
|
|
789
|
+
const timestamp = Number(msg.timestamp ?? Date.now());
|
|
790
|
+
log({ event: "message_received", from, to, timestamp, content_len: content.length });
|
|
791
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
792
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
793
|
+
}
|
|
794
|
+
catch {
|
|
795
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
796
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
797
|
+
}
|
|
661
798
|
return;
|
|
662
799
|
}
|
|
663
800
|
// POST /delivery — provider committed a deliverable
|
|
664
801
|
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" }));
|
|
802
|
+
const body = await readBody(req, res);
|
|
803
|
+
if (body === null)
|
|
804
|
+
return;
|
|
805
|
+
verifyRequestSignature(body, req);
|
|
806
|
+
try {
|
|
807
|
+
const msg = JSON.parse(body);
|
|
808
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
809
|
+
const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
|
|
810
|
+
const from = String(msg.from ?? "");
|
|
811
|
+
log({ event: "delivery_received", agreementId, deliverableHash, from });
|
|
812
|
+
// Update DB: mark delivered
|
|
813
|
+
const active = db.listActiveHireRequests();
|
|
814
|
+
const found = active.find(r => r.agreement_id === agreementId);
|
|
815
|
+
if (found)
|
|
816
|
+
db.updateHireRequestStatus(found.id, "delivered");
|
|
817
|
+
if (config.notifications.notify_on_delivery) {
|
|
818
|
+
await notifier.notifyDelivery(agreementId, deliverableHash, "");
|
|
688
819
|
}
|
|
689
|
-
|
|
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
|
+
}
|
|
690
827
|
return;
|
|
691
828
|
}
|
|
692
829
|
// POST /delivery/accepted — client accepted delivery, payment releasing
|
|
693
830
|
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
|
-
});
|
|
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 from = String(msg.from ?? "");
|
|
838
|
+
log({ event: "delivery_accepted_inbound", agreementId, from });
|
|
839
|
+
// Update DB: mark complete
|
|
840
|
+
const all = db.listActiveHireRequests();
|
|
841
|
+
const found = all.find(r => r.agreement_id === agreementId);
|
|
842
|
+
if (found)
|
|
843
|
+
db.updateHireRequestStatus(found.id, "complete");
|
|
844
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
845
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
849
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
850
|
+
}
|
|
715
851
|
return;
|
|
716
852
|
}
|
|
717
853
|
// POST /dispute — dispute raised against this agent
|
|
718
854
|
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 }));
|
|
733
|
-
}
|
|
734
|
-
catch {
|
|
735
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
736
|
-
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
855
|
+
const body = await readBody(req, res);
|
|
856
|
+
if (body === null)
|
|
857
|
+
return;
|
|
858
|
+
try {
|
|
859
|
+
const msg = JSON.parse(body);
|
|
860
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
861
|
+
const reason = String(msg.reason ?? "");
|
|
862
|
+
const from = String(msg.from ?? "");
|
|
863
|
+
log({ event: "dispute_received", agreementId, reason, from });
|
|
864
|
+
if (config.notifications.notify_on_dispute) {
|
|
865
|
+
await notifier.notifyDispute(agreementId, from);
|
|
737
866
|
}
|
|
738
|
-
|
|
867
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
868
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
872
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
873
|
+
}
|
|
739
874
|
return;
|
|
740
875
|
}
|
|
741
876
|
// POST /dispute/resolved — dispute resolved by arbitrator
|
|
742
877
|
if (pathname === "/dispute/resolved" && req.method === "POST") {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
});
|
|
878
|
+
const body = await readBody(req, res);
|
|
879
|
+
if (body === null)
|
|
880
|
+
return;
|
|
881
|
+
try {
|
|
882
|
+
const msg = JSON.parse(body);
|
|
883
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
884
|
+
const outcome = String(msg.outcome ?? "");
|
|
885
|
+
const from = String(msg.from ?? "");
|
|
886
|
+
log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
|
|
887
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
888
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
892
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
893
|
+
}
|
|
760
894
|
return;
|
|
761
895
|
}
|
|
762
896
|
// POST /workroom/status — workroom lifecycle events
|
|
763
897
|
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
|
-
});
|
|
898
|
+
const body = await readBody(req, res);
|
|
899
|
+
if (body === null)
|
|
900
|
+
return;
|
|
901
|
+
try {
|
|
902
|
+
const msg = JSON.parse(body);
|
|
903
|
+
const event = String(msg.event ?? "");
|
|
904
|
+
const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
|
|
905
|
+
const jobId = msg.jobId ? String(msg.jobId) : undefined;
|
|
906
|
+
const timestamp = Number(msg.timestamp ?? Date.now());
|
|
907
|
+
log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
|
|
908
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
909
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
913
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
914
|
+
}
|
|
782
915
|
return;
|
|
783
916
|
}
|
|
784
917
|
// GET /capabilities — agent capabilities from config
|
|
@@ -792,21 +925,25 @@ async function runDaemon(foreground = false) {
|
|
|
792
925
|
}));
|
|
793
926
|
return;
|
|
794
927
|
}
|
|
795
|
-
// GET /status — health with active agreement count
|
|
928
|
+
// GET /status — health with active agreement count (sensitive counts only for authenticated)
|
|
796
929
|
if (pathname === "/status" && req.method === "GET") {
|
|
797
|
-
const
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
930
|
+
const statusAuth = (req.headers["authorization"] ?? "");
|
|
931
|
+
const statusToken = statusAuth.startsWith("Bearer ") ? statusAuth.slice(7) : "";
|
|
932
|
+
const statusAuthed = statusToken === apiToken;
|
|
933
|
+
const statusPayload = {
|
|
801
934
|
protocol: "arc-402",
|
|
802
935
|
version: "0.3.0",
|
|
803
936
|
agent: config.wallet.contract_address,
|
|
804
937
|
status: "online",
|
|
805
938
|
uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
|
|
806
|
-
active_agreements: activeList.length,
|
|
807
|
-
pending_approval: pendingList.length,
|
|
808
939
|
capabilities: config.policy.allowed_capabilities,
|
|
809
|
-
}
|
|
940
|
+
};
|
|
941
|
+
if (statusAuthed) {
|
|
942
|
+
statusPayload.active_agreements = db.listActiveHireRequests().length;
|
|
943
|
+
statusPayload.pending_approval = db.listPendingHireRequests().length;
|
|
944
|
+
}
|
|
945
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
946
|
+
res.end(JSON.stringify(statusPayload));
|
|
810
947
|
return;
|
|
811
948
|
}
|
|
812
949
|
// 404
|
|
@@ -838,6 +975,8 @@ async function runDaemon(foreground = false) {
|
|
|
838
975
|
clearInterval(relayInterval);
|
|
839
976
|
clearInterval(timeoutInterval);
|
|
840
977
|
clearInterval(balanceInterval);
|
|
978
|
+
if (rateLimitCleanupInterval)
|
|
979
|
+
clearInterval(rateLimitCleanupInterval);
|
|
841
980
|
// Close HTTP + IPC
|
|
842
981
|
httpServer.close();
|
|
843
982
|
ipcServer.close();
|