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/src/daemon/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ import * as http from "http";
|
|
|
14
14
|
import { ethers } from "ethers";
|
|
15
15
|
import Database from "better-sqlite3";
|
|
16
16
|
|
|
17
|
+
import * as crypto from "crypto";
|
|
18
|
+
|
|
17
19
|
import {
|
|
18
20
|
loadDaemonConfig,
|
|
19
21
|
loadMachineKey,
|
|
@@ -147,6 +149,52 @@ function openStateDB(dbPath: string): DaemonDB {
|
|
|
147
149
|
};
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
// ─── Auth token ───────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
const DAEMON_TOKEN_FILE = path.join(path.dirname(DAEMON_SOCK), "daemon.token");
|
|
155
|
+
|
|
156
|
+
function generateApiToken(): string {
|
|
157
|
+
return crypto.randomBytes(32).toString("hex");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function saveApiToken(token: string): void {
|
|
161
|
+
fs.mkdirSync(path.dirname(DAEMON_TOKEN_FILE), { recursive: true, mode: 0o700 });
|
|
162
|
+
fs.writeFileSync(DAEMON_TOKEN_FILE, token, { mode: 0o600 });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function loadApiToken(): string | null {
|
|
166
|
+
try {
|
|
167
|
+
return fs.readFileSync(DAEMON_TOKEN_FILE, "utf-8").trim();
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Rate limiter ─────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
interface RateBucket { count: number; resetTime: number }
|
|
176
|
+
const rateLimitMap = new Map<string, RateBucket>();
|
|
177
|
+
const RATE_LIMIT = 30;
|
|
178
|
+
const RATE_WINDOW_MS = 60_000;
|
|
179
|
+
|
|
180
|
+
function checkRateLimit(ip: string): boolean {
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
let bucket = rateLimitMap.get(ip);
|
|
183
|
+
if (!bucket || now >= bucket.resetTime) {
|
|
184
|
+
bucket = { count: 0, resetTime: now + RATE_WINDOW_MS };
|
|
185
|
+
rateLimitMap.set(ip, bucket);
|
|
186
|
+
}
|
|
187
|
+
bucket.count++;
|
|
188
|
+
return bucket.count <= RATE_LIMIT;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Cleanup stale rate limit entries every 5 minutes to prevent unbounded growth
|
|
192
|
+
let rateLimitCleanupInterval: ReturnType<typeof setInterval> | null = null;
|
|
193
|
+
|
|
194
|
+
// ─── Body size limit ──────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
|
|
197
|
+
|
|
150
198
|
// ─── Logger ───────────────────────────────────────────────────────────────────
|
|
151
199
|
|
|
152
200
|
function openLogger(logPath: string, foreground: boolean): (entry: Record<string, unknown>) => void {
|
|
@@ -181,7 +229,7 @@ interface IpcContext {
|
|
|
181
229
|
bundlerEndpoint: string;
|
|
182
230
|
}
|
|
183
231
|
|
|
184
|
-
function startIpcServer(ctx: IpcContext, log: ReturnType<typeof openLogger
|
|
232
|
+
function startIpcServer(ctx: IpcContext, log: ReturnType<typeof openLogger>, apiToken: string): net.Server {
|
|
185
233
|
// Remove stale socket
|
|
186
234
|
if (fs.existsSync(DAEMON_SOCK)) {
|
|
187
235
|
fs.unlinkSync(DAEMON_SOCK);
|
|
@@ -189,20 +237,34 @@ function startIpcServer(ctx: IpcContext, log: ReturnType<typeof openLogger>): ne
|
|
|
189
237
|
|
|
190
238
|
const server = net.createServer((socket) => {
|
|
191
239
|
let buf = "";
|
|
240
|
+
let authenticated = false;
|
|
192
241
|
socket.on("data", (data) => {
|
|
193
242
|
buf += data.toString();
|
|
194
243
|
const lines = buf.split("\n");
|
|
195
244
|
buf = lines.pop() ?? "";
|
|
196
245
|
for (const line of lines) {
|
|
197
246
|
if (!line.trim()) continue;
|
|
198
|
-
let cmd: { command: string; id?: string; reason?: string };
|
|
247
|
+
let cmd: { command: string; id?: string; reason?: string; auth?: string };
|
|
199
248
|
try {
|
|
200
|
-
cmd = JSON.parse(line) as { command: string; id?: string; reason?: string };
|
|
249
|
+
cmd = JSON.parse(line) as { command: string; id?: string; reason?: string; auth?: string };
|
|
201
250
|
} catch {
|
|
202
251
|
socket.write(JSON.stringify({ ok: false, error: "invalid_json" }) + "\n");
|
|
203
252
|
continue;
|
|
204
253
|
}
|
|
205
254
|
|
|
255
|
+
// First message must be auth
|
|
256
|
+
if (!authenticated) {
|
|
257
|
+
if (cmd.auth === apiToken) {
|
|
258
|
+
authenticated = true;
|
|
259
|
+
socket.write(JSON.stringify({ ok: true, authenticated: true }) + "\n");
|
|
260
|
+
} else {
|
|
261
|
+
log({ event: "ipc_auth_failed" });
|
|
262
|
+
socket.write(JSON.stringify({ ok: false, error: "unauthorized" }) + "\n");
|
|
263
|
+
socket.destroy();
|
|
264
|
+
}
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
206
268
|
const response = handleIpcCommand(cmd, ctx, log);
|
|
207
269
|
socket.write(JSON.stringify(response) + "\n");
|
|
208
270
|
}
|
|
@@ -529,6 +591,14 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
529
591
|
}
|
|
530
592
|
}, 30_000);
|
|
531
593
|
|
|
594
|
+
// Rate limit map cleanup — every 5 minutes (prevents unbounded growth)
|
|
595
|
+
rateLimitCleanupInterval = setInterval(() => {
|
|
596
|
+
const now = Date.now();
|
|
597
|
+
for (const [ip, bucket] of rateLimitMap) {
|
|
598
|
+
if (bucket.resetTime < now) rateLimitMap.delete(ip);
|
|
599
|
+
}
|
|
600
|
+
}, 5 * 60 * 1000);
|
|
601
|
+
|
|
532
602
|
// Balance monitor — every 5 minutes
|
|
533
603
|
const balanceInterval = setInterval(async () => {
|
|
534
604
|
try {
|
|
@@ -547,22 +617,105 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
547
617
|
log({ event: "pid_written", pid: process.pid, path: DAEMON_PID });
|
|
548
618
|
}
|
|
549
619
|
|
|
620
|
+
// ── Generate and save API token ──────────────────────────────────────────
|
|
621
|
+
const apiToken = generateApiToken();
|
|
622
|
+
saveApiToken(apiToken);
|
|
623
|
+
log({ event: "auth_token_saved", path: DAEMON_TOKEN_FILE });
|
|
624
|
+
|
|
550
625
|
// ── Start IPC socket ─────────────────────────────────────────────────────
|
|
551
|
-
const ipcServer = startIpcServer(ipcCtx, log);
|
|
626
|
+
const ipcServer = startIpcServer(ipcCtx, log, apiToken);
|
|
552
627
|
|
|
553
628
|
// ── Start HTTP relay server (public endpoint) ────────────────────────────
|
|
554
629
|
const httpPort = config.relay.listen_port ?? 4402;
|
|
555
630
|
|
|
631
|
+
/**
|
|
632
|
+
* Optionally verifies X-ARC402-Signature against the request body.
|
|
633
|
+
* Logs the result but never rejects — unsigned requests are accepted for backwards compat.
|
|
634
|
+
*/
|
|
635
|
+
function verifyRequestSignature(body: string, req: http.IncomingMessage): void {
|
|
636
|
+
const sig = req.headers["x-arc402-signature"] as string | undefined;
|
|
637
|
+
if (!sig) return;
|
|
638
|
+
const claimedSigner = req.headers["x-arc402-signer"] as string | undefined;
|
|
639
|
+
try {
|
|
640
|
+
const recovered = ethers.verifyMessage(body, sig);
|
|
641
|
+
if (claimedSigner && recovered.toLowerCase() !== claimedSigner.toLowerCase()) {
|
|
642
|
+
log({ event: "sig_mismatch", claimed: claimedSigner, recovered });
|
|
643
|
+
} else {
|
|
644
|
+
log({ event: "sig_verified", signer: recovered });
|
|
645
|
+
}
|
|
646
|
+
} catch {
|
|
647
|
+
log({ event: "sig_invalid" });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Read request body with a size cap. Destroys the request and sends 413
|
|
653
|
+
* if the body exceeds MAX_BODY_SIZE. Returns null in that case.
|
|
654
|
+
*/
|
|
655
|
+
function readBody(req: http.IncomingMessage, res: http.ServerResponse): Promise<string | null> {
|
|
656
|
+
return new Promise((resolve) => {
|
|
657
|
+
let body = "";
|
|
658
|
+
let size = 0;
|
|
659
|
+
req.on("data", (chunk: Buffer) => {
|
|
660
|
+
size += chunk.length;
|
|
661
|
+
if (size > MAX_BODY_SIZE) {
|
|
662
|
+
req.destroy();
|
|
663
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
664
|
+
res.end(JSON.stringify({ error: "payload_too_large" }));
|
|
665
|
+
resolve(null);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
body += chunk.toString();
|
|
669
|
+
});
|
|
670
|
+
req.on("end", () => { resolve(body); });
|
|
671
|
+
req.on("error", () => { resolve(null); });
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const PUBLIC_GET_PATHS = new Set(["/", "/health", "/agent", "/capabilities", "/status"]);
|
|
676
|
+
|
|
677
|
+
// CORS whitelist — localhost for local tooling, arc402.xyz for the web app
|
|
678
|
+
const CORS_WHITELIST = new Set(["localhost", "127.0.0.1", "arc402.xyz", "app.arc402.xyz"]);
|
|
679
|
+
|
|
556
680
|
const httpServer = http.createServer(async (req, res) => {
|
|
557
|
-
// CORS
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
681
|
+
// CORS — only reflect origin header if it's in the whitelist
|
|
682
|
+
const origin = (req.headers["origin"] ?? "") as string;
|
|
683
|
+
if (origin) {
|
|
684
|
+
try {
|
|
685
|
+
const { hostname } = new URL(origin);
|
|
686
|
+
if (CORS_WHITELIST.has(hostname)) {
|
|
687
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
688
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
689
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
690
|
+
}
|
|
691
|
+
} catch { /* ignore invalid origin */ }
|
|
692
|
+
}
|
|
561
693
|
if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
|
|
562
694
|
|
|
563
695
|
const url = new URL(req.url || "/", `http://localhost:${httpPort}`);
|
|
564
696
|
const pathname = url.pathname;
|
|
565
697
|
|
|
698
|
+
// Rate limiting (all endpoints)
|
|
699
|
+
const clientIp = (req.socket.remoteAddress ?? "unknown").replace(/^::ffff:/, "");
|
|
700
|
+
if (!checkRateLimit(clientIp)) {
|
|
701
|
+
log({ event: "rate_limited", ip: clientIp, path: pathname });
|
|
702
|
+
res.writeHead(429, { "Content-Type": "application/json" });
|
|
703
|
+
res.end(JSON.stringify({ error: "too_many_requests" }));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Auth required on all POST endpoints (GET public paths are open)
|
|
708
|
+
if (req.method === "POST" || (req.method === "GET" && !PUBLIC_GET_PATHS.has(pathname))) {
|
|
709
|
+
const authHeader = req.headers["authorization"] ?? "";
|
|
710
|
+
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
711
|
+
if (token !== apiToken) {
|
|
712
|
+
log({ event: "http_unauthorized", ip: clientIp, path: pathname });
|
|
713
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
714
|
+
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
566
719
|
// Health / info
|
|
567
720
|
if (pathname === "/" || pathname === "/health") {
|
|
568
721
|
const info = {
|
|
@@ -594,10 +747,10 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
594
747
|
|
|
595
748
|
// Receive hire proposal
|
|
596
749
|
if (pathname === "/hire" && req.method === "POST") {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
750
|
+
const body = await readBody(req, res);
|
|
751
|
+
if (body === null) return;
|
|
752
|
+
verifyRequestSignature(body, req);
|
|
753
|
+
try {
|
|
601
754
|
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
602
755
|
|
|
603
756
|
// Feed into the hire listener's message handler
|
|
@@ -615,6 +768,7 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
615
768
|
// Dedup
|
|
616
769
|
const existing = db.getHireRequest(proposal.messageId);
|
|
617
770
|
if (existing) {
|
|
771
|
+
log({ event: "hire_duplicate", id: proposal.messageId, status: existing.status });
|
|
618
772
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
619
773
|
res.end(JSON.stringify({ status: existing.status, id: proposal.messageId }));
|
|
620
774
|
return;
|
|
@@ -669,16 +823,15 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
669
823
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
670
824
|
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
671
825
|
}
|
|
672
|
-
});
|
|
673
826
|
return;
|
|
674
827
|
}
|
|
675
828
|
|
|
676
829
|
// Handshake acknowledgment endpoint
|
|
677
830
|
if (pathname === "/handshake" && req.method === "POST") {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
831
|
+
const body = await readBody(req, res);
|
|
832
|
+
if (body === null) return;
|
|
833
|
+
verifyRequestSignature(body, req);
|
|
834
|
+
try {
|
|
682
835
|
const msg = JSON.parse(body);
|
|
683
836
|
log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
|
|
684
837
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -687,171 +840,158 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
687
840
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
688
841
|
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
689
842
|
}
|
|
690
|
-
});
|
|
691
843
|
return;
|
|
692
844
|
}
|
|
693
845
|
|
|
694
846
|
// POST /hire/accepted — provider accepted, client notified
|
|
695
847
|
if (pathname === "/hire/accepted" && req.method === "POST") {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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" }));
|
|
848
|
+
const body = await readBody(req, res);
|
|
849
|
+
if (body === null) return;
|
|
850
|
+
try {
|
|
851
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
852
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
853
|
+
const from = String(msg.from ?? "");
|
|
854
|
+
log({ event: "hire_accepted_inbound", agreementId, from });
|
|
855
|
+
if (config.notifications.notify_on_hire_accepted) {
|
|
856
|
+
await notifier.notifyHireAccepted(agreementId, agreementId);
|
|
712
857
|
}
|
|
713
|
-
|
|
858
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
859
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
860
|
+
} catch {
|
|
861
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
862
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
863
|
+
}
|
|
714
864
|
return;
|
|
715
865
|
}
|
|
716
866
|
|
|
717
867
|
// POST /message — off-chain negotiation message
|
|
718
868
|
if (pathname === "/message" && req.method === "POST") {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
});
|
|
869
|
+
const body = await readBody(req, res);
|
|
870
|
+
if (body === null) return;
|
|
871
|
+
verifyRequestSignature(body, req);
|
|
872
|
+
try {
|
|
873
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
874
|
+
const from = String(msg.from ?? "");
|
|
875
|
+
const to = String(msg.to ?? "");
|
|
876
|
+
const content = String(msg.content ?? "");
|
|
877
|
+
const timestamp = Number(msg.timestamp ?? Date.now());
|
|
878
|
+
log({ event: "message_received", from, to, timestamp, content_len: content.length });
|
|
879
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
880
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
881
|
+
} catch {
|
|
882
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
883
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
884
|
+
}
|
|
736
885
|
return;
|
|
737
886
|
}
|
|
738
887
|
|
|
739
888
|
// POST /delivery — provider committed a deliverable
|
|
740
889
|
if (pathname === "/delivery" && req.method === "POST") {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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" }));
|
|
890
|
+
const body = await readBody(req, res);
|
|
891
|
+
if (body === null) return;
|
|
892
|
+
verifyRequestSignature(body, req);
|
|
893
|
+
try {
|
|
894
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
895
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
896
|
+
const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
|
|
897
|
+
const from = String(msg.from ?? "");
|
|
898
|
+
log({ event: "delivery_received", agreementId, deliverableHash, from });
|
|
899
|
+
// Update DB: mark delivered
|
|
900
|
+
const active = db.listActiveHireRequests();
|
|
901
|
+
const found = active.find(r => r.agreement_id === agreementId);
|
|
902
|
+
if (found) db.updateHireRequestStatus(found.id, "delivered");
|
|
903
|
+
if (config.notifications.notify_on_delivery) {
|
|
904
|
+
await notifier.notifyDelivery(agreementId, deliverableHash, "");
|
|
762
905
|
}
|
|
763
|
-
|
|
906
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
907
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
908
|
+
} catch {
|
|
909
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
910
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
911
|
+
}
|
|
764
912
|
return;
|
|
765
913
|
}
|
|
766
914
|
|
|
767
915
|
// POST /delivery/accepted — client accepted delivery, payment releasing
|
|
768
916
|
if (pathname === "/delivery/accepted" && req.method === "POST") {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
787
|
-
});
|
|
917
|
+
const body = await readBody(req, res);
|
|
918
|
+
if (body === null) return;
|
|
919
|
+
try {
|
|
920
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
921
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
922
|
+
const from = String(msg.from ?? "");
|
|
923
|
+
log({ event: "delivery_accepted_inbound", agreementId, from });
|
|
924
|
+
// Update DB: mark complete
|
|
925
|
+
const all = db.listActiveHireRequests();
|
|
926
|
+
const found = all.find(r => r.agreement_id === agreementId);
|
|
927
|
+
if (found) db.updateHireRequestStatus(found.id, "complete");
|
|
928
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
929
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
930
|
+
} catch {
|
|
931
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
932
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
933
|
+
}
|
|
788
934
|
return;
|
|
789
935
|
}
|
|
790
936
|
|
|
791
937
|
// POST /dispute — dispute raised against this agent
|
|
792
938
|
if (pathname === "/dispute" && req.method === "POST") {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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" }));
|
|
939
|
+
const body = await readBody(req, res);
|
|
940
|
+
if (body === null) return;
|
|
941
|
+
try {
|
|
942
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
943
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
944
|
+
const reason = String(msg.reason ?? "");
|
|
945
|
+
const from = String(msg.from ?? "");
|
|
946
|
+
log({ event: "dispute_received", agreementId, reason, from });
|
|
947
|
+
if (config.notifications.notify_on_dispute) {
|
|
948
|
+
await notifier.notifyDispute(agreementId, from);
|
|
810
949
|
}
|
|
811
|
-
|
|
950
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
951
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
952
|
+
} catch {
|
|
953
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
954
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
955
|
+
}
|
|
812
956
|
return;
|
|
813
957
|
}
|
|
814
958
|
|
|
815
959
|
// POST /dispute/resolved — dispute resolved by arbitrator
|
|
816
960
|
if (pathname === "/dispute/resolved" && req.method === "POST") {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
});
|
|
961
|
+
const body = await readBody(req, res);
|
|
962
|
+
if (body === null) return;
|
|
963
|
+
try {
|
|
964
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
965
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
966
|
+
const outcome = String(msg.outcome ?? "");
|
|
967
|
+
const from = String(msg.from ?? "");
|
|
968
|
+
log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
|
|
969
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
970
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
971
|
+
} catch {
|
|
972
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
973
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
974
|
+
}
|
|
833
975
|
return;
|
|
834
976
|
}
|
|
835
977
|
|
|
836
978
|
// POST /workroom/status — workroom lifecycle events
|
|
837
979
|
if (pathname === "/workroom/status" && req.method === "POST") {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
}
|
|
854
|
-
});
|
|
980
|
+
const body = await readBody(req, res);
|
|
981
|
+
if (body === null) return;
|
|
982
|
+
try {
|
|
983
|
+
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
984
|
+
const event = String(msg.event ?? "");
|
|
985
|
+
const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
|
|
986
|
+
const jobId = msg.jobId ? String(msg.jobId) : undefined;
|
|
987
|
+
const timestamp = Number(msg.timestamp ?? Date.now());
|
|
988
|
+
log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
|
|
989
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
990
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
991
|
+
} catch {
|
|
992
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
993
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
994
|
+
}
|
|
855
995
|
return;
|
|
856
996
|
}
|
|
857
997
|
|
|
@@ -867,21 +1007,25 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
867
1007
|
return;
|
|
868
1008
|
}
|
|
869
1009
|
|
|
870
|
-
// GET /status — health with active agreement count
|
|
1010
|
+
// GET /status — health with active agreement count (sensitive counts only for authenticated)
|
|
871
1011
|
if (pathname === "/status" && req.method === "GET") {
|
|
872
|
-
const
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
1012
|
+
const statusAuth = (req.headers["authorization"] ?? "") as string;
|
|
1013
|
+
const statusToken = statusAuth.startsWith("Bearer ") ? statusAuth.slice(7) : "";
|
|
1014
|
+
const statusAuthed = statusToken === apiToken;
|
|
1015
|
+
const statusPayload: Record<string, unknown> = {
|
|
876
1016
|
protocol: "arc-402",
|
|
877
1017
|
version: "0.3.0",
|
|
878
1018
|
agent: config.wallet.contract_address,
|
|
879
1019
|
status: "online",
|
|
880
1020
|
uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
|
|
881
|
-
active_agreements: activeList.length,
|
|
882
|
-
pending_approval: pendingList.length,
|
|
883
1021
|
capabilities: config.policy.allowed_capabilities,
|
|
884
|
-
}
|
|
1022
|
+
};
|
|
1023
|
+
if (statusAuthed) {
|
|
1024
|
+
statusPayload.active_agreements = db.listActiveHireRequests().length;
|
|
1025
|
+
statusPayload.pending_approval = db.listPendingHireRequests().length;
|
|
1026
|
+
}
|
|
1027
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1028
|
+
res.end(JSON.stringify(statusPayload));
|
|
885
1029
|
return;
|
|
886
1030
|
}
|
|
887
1031
|
|
|
@@ -917,6 +1061,7 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
917
1061
|
if (relayInterval) clearInterval(relayInterval);
|
|
918
1062
|
clearInterval(timeoutInterval);
|
|
919
1063
|
clearInterval(balanceInterval);
|
|
1064
|
+
if (rateLimitCleanupInterval) clearInterval(rateLimitCleanupInterval);
|
|
920
1065
|
|
|
921
1066
|
// Close HTTP + IPC
|
|
922
1067
|
httpServer.close();
|