arc402-cli 0.2.0 → 0.3.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.
Files changed (45) hide show
  1. package/README.md +2 -0
  2. package/dist/commands/accept.d.ts.map +1 -1
  3. package/dist/commands/accept.js +26 -1
  4. package/dist/commands/accept.js.map +1 -1
  5. package/dist/commands/agent.d.ts.map +1 -1
  6. package/dist/commands/agent.js +31 -0
  7. package/dist/commands/agent.js.map +1 -1
  8. package/dist/commands/arena-handshake.d.ts.map +1 -1
  9. package/dist/commands/arena-handshake.js +22 -0
  10. package/dist/commands/arena-handshake.js.map +1 -1
  11. package/dist/commands/deliver.d.ts.map +1 -1
  12. package/dist/commands/deliver.js +19 -0
  13. package/dist/commands/deliver.js.map +1 -1
  14. package/dist/commands/hire.d.ts.map +1 -1
  15. package/dist/commands/hire.js +29 -0
  16. package/dist/commands/hire.js.map +1 -1
  17. package/dist/commands/workroom.d.ts.map +1 -1
  18. package/dist/commands/workroom.js +37 -0
  19. package/dist/commands/workroom.js.map +1 -1
  20. package/dist/config.d.ts +1 -1
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +1 -1
  23. package/dist/config.js.map +1 -1
  24. package/dist/daemon/config.d.ts +1 -1
  25. package/dist/daemon/config.d.ts.map +1 -1
  26. package/dist/daemon/config.js +2 -2
  27. package/dist/daemon/config.js.map +1 -1
  28. package/dist/daemon/index.d.ts.map +1 -1
  29. package/dist/daemon/index.js +336 -1
  30. package/dist/daemon/index.js.map +1 -1
  31. package/dist/endpoint-notify.d.ts +18 -0
  32. package/dist/endpoint-notify.d.ts.map +1 -0
  33. package/dist/endpoint-notify.js +43 -0
  34. package/dist/endpoint-notify.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/commands/accept.ts +31 -1
  37. package/src/commands/agent.ts +37 -0
  38. package/src/commands/arena-handshake.ts +33 -0
  39. package/src/commands/deliver.ts +19 -0
  40. package/src/commands/hire.ts +31 -1
  41. package/src/commands/workroom.ts +44 -0
  42. package/src/config.ts +1 -1
  43. package/src/daemon/config.ts +2 -2
  44. package/src/daemon/index.ts +347 -1
  45. package/src/endpoint-notify.ts +46 -0
@@ -181,6 +181,43 @@ export function registerAgentCommands(program: Command): void {
181
181
  await claimSubdomain(subdomain, walletAddress, opts.tunnelTarget);
182
182
  });
183
183
 
184
+ // ─── transfer-subdomain ──────────────────────────────────────────────────────
185
+
186
+ agent
187
+ .command("transfer-subdomain <subdomain>")
188
+ .description("Transfer a subdomain to a new wallet. Both wallets must share the same master key (owner EOA). Used during wallet migration.")
189
+ .requiredOption("--new-wallet <address>", "New wallet address to transfer the subdomain to")
190
+ .action(async (subdomain, opts) => {
191
+ const normalized = subdomain.toLowerCase();
192
+ let newWallet: string;
193
+ try {
194
+ newWallet = ethers.getAddress(opts.newWallet);
195
+ } catch {
196
+ console.error(chalk.red(`Invalid address: ${opts.newWallet}`));
197
+ process.exit(1);
198
+ }
199
+
200
+ console.log(`\nTransferring subdomain: ${normalized}.arc402.xyz`);
201
+ console.log(` New wallet: ${newWallet}`);
202
+ console.log(` Verifying master key ownership onchain...`);
203
+
204
+ const res = await fetch("https://api.arc402.xyz/transfer", {
205
+ method: "POST",
206
+ headers: { "Content-Type": "application/json" },
207
+ body: JSON.stringify({ subdomain: normalized, newWalletAddress: newWallet }),
208
+ });
209
+
210
+ const body = await res.json() as Record<string, unknown>;
211
+
212
+ if (!res.ok) {
213
+ console.error(chalk.red(`\n✗ Transfer failed (${res.status}): ${body["error"] ?? JSON.stringify(body)}`));
214
+ process.exit(1);
215
+ }
216
+
217
+ console.log(chalk.green(`\n✓ Subdomain transferred: ${body["subdomain"]}`));
218
+ console.log(` New owner: ${body["newWalletAddress"]}`);
219
+ });
220
+
184
221
  // ─── set-metadata ───────────────────────────────────────────────────────────
185
222
 
186
223
  agent
@@ -2,6 +2,26 @@ import { Command } from "commander";
2
2
  import { ethers } from "ethers";
3
3
  import { loadConfig, getUsdcAddress } from "../config";
4
4
  import { requireSigner } from "../client";
5
+ import { AGENT_REGISTRY_ABI } from "../abis";
6
+
7
+ const DEFAULT_REGISTRY_ADDRESS = "0xD5c2851B00090c92Ba7F4723FB548bb30C9B6865";
8
+
9
+ async function pingHandshakeEndpoint(
10
+ agentAddress: string,
11
+ payload: Record<string, unknown>,
12
+ registryAddress: string,
13
+ provider: ethers.Provider
14
+ ): Promise<void> {
15
+ const registry = new ethers.Contract(registryAddress, AGENT_REGISTRY_ABI, provider);
16
+ const agentData = await registry.getAgent(agentAddress);
17
+ const endpoint = agentData.endpoint as string;
18
+ if (!endpoint) return;
19
+ await fetch(`${endpoint}/handshake`, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json" },
22
+ body: JSON.stringify(payload),
23
+ });
24
+ }
5
25
 
6
26
  // ─── Handshake Contract ABI (from Handshake.sol) ─────────────────────────────
7
27
 
@@ -114,6 +134,19 @@ export function registerArenaHandshakeCommands(program: Command): void {
114
134
  tx = await handshake.sendHandshake(agentAddress, hsType, opts.note, { value });
115
135
  }
116
136
 
137
+ // Notify recipient's HTTP endpoint (non-blocking)
138
+ const registryAddress = config.agentRegistryV2Address ?? config.agentRegistryAddress ?? DEFAULT_REGISTRY_ADDRESS;
139
+ try {
140
+ await pingHandshakeEndpoint(
141
+ agentAddress,
142
+ { from: myAddress, type: opts.type, note: opts.note, txHash: tx.hash },
143
+ registryAddress,
144
+ provider
145
+ );
146
+ } catch (err) {
147
+ console.warn(`Warning: could not notify recipient endpoint: ${err instanceof Error ? err.message : String(err)}`);
148
+ }
149
+
117
150
  if (opts.json) {
118
151
  console.log(JSON.stringify({ tx: tx.hash, from: myAddress, to: agentAddress, type: opts.type, note: opts.note }));
119
152
  } else {
@@ -8,6 +8,7 @@ import { printSenderInfo, executeContractWriteViaWallet } from "../wallet-router
8
8
  import { SERVICE_AGREEMENT_ABI } from "../abis";
9
9
  import { readFile } from "fs/promises";
10
10
  import prompts from "prompts";
11
+ import { resolveAgentEndpoint, notifyAgent, DEFAULT_REGISTRY_ADDRESS } from "../endpoint-notify";
11
12
 
12
13
  export function registerDeliverCommand(program: Command): void {
13
14
  program
@@ -24,6 +25,7 @@ export function registerDeliverCommand(program: Command): void {
24
25
  printSenderInfo(config);
25
26
 
26
27
  // Pre-flight: check deadline and legacyFulfillEnabled (J3-01, J3-02)
28
+ let clientAddress = "";
27
29
  {
28
30
  const { provider: prefProvider } = await getClient(config);
29
31
  const saAbi = [
@@ -42,6 +44,7 @@ export function registerDeliverCommand(program: Command): void {
42
44
  console.error(`Contact the client to open a new agreement.`);
43
45
  process.exit(1);
44
46
  }
47
+ clientAddress = String(ag.client ?? "");
45
48
  } catch (e) {
46
49
  // If it's a contract read failure, skip the check (let the tx reveal the error)
47
50
  if (e instanceof Error && !e.message.includes("CALL_EXCEPTION") && !e.message.includes("could not decode")) throw e;
@@ -112,5 +115,21 @@ export function registerDeliverCommand(program: Command): void {
112
115
  if (opts.fulfill) await client.fulfill(BigInt(id), hash); else await client.commitDeliverable(BigInt(id), hash);
113
116
  }
114
117
  console.log(`${opts.fulfill ? 'fulfilled' : 'committed'} ${id} hash=${hash}`);
118
+
119
+ // Notify client's HTTP endpoint (non-blocking)
120
+ if (clientAddress) {
121
+ try {
122
+ const notifyProvider = new ethers.JsonRpcProvider(config.rpcUrl);
123
+ const registryAddress = config.agentRegistryV2Address ?? config.agentRegistryAddress ?? DEFAULT_REGISTRY_ADDRESS;
124
+ const endpoint = await resolveAgentEndpoint(clientAddress, notifyProvider, registryAddress);
125
+ await notifyAgent(endpoint, "/delivery", {
126
+ agreementId: id,
127
+ deliverableHash: hash,
128
+ from: signerAddress,
129
+ });
130
+ } catch (err) {
131
+ console.warn(`Warning: could not notify client endpoint: ${err instanceof Error ? err.message : String(err)}`);
132
+ }
133
+ }
115
134
  });
116
135
  }
@@ -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
  }
@@ -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/config.ts CHANGED
@@ -71,7 +71,7 @@ export const configExists = () => fs.existsSync(CONFIG_PATH);
71
71
 
72
72
  // Public Base RPC — stale state, do not use for production. Alchemy recommended.
73
73
  export const PUBLIC_BASE_RPC = "https://mainnet.base.org";
74
- export const ALCHEMY_BASE_RPC = "https://base-mainnet.g.alchemy.com/v2/YIA2uRCsFI-j5pqH-aRzflrACSlV1Qrs";
74
+ export const ALCHEMY_BASE_RPC = "https://mainnet.base.org";
75
75
 
76
76
  /**
77
77
  * Warn at runtime if the configured RPC is the public Base endpoint.
@@ -123,7 +123,7 @@ function withDefaults(raw: Record<string, unknown>): DaemonConfig {
123
123
  machine_key: str(w.machine_key, "env:ARC402_MACHINE_KEY"),
124
124
  },
125
125
  network: {
126
- rpc_url: str(n.rpc_url, "https://base-mainnet.g.alchemy.com/v2/YIA2uRCsFI-j5pqH-aRzflrACSlV1Qrs"),
126
+ rpc_url: str(n.rpc_url, "https://mainnet.base.org"),
127
127
  chain_id: num(n.chain_id, 8453),
128
128
  entry_point: str(n.entry_point, "0x0000000071727De22E5E9d8BAf0edAc6f37da032"),
129
129
  },
@@ -252,7 +252,7 @@ owner_address = "" # Owner EOA address — for display and verificatio
252
252
  machine_key = "env:ARC402_MACHINE_KEY" # Machine key loaded from environment. NEVER hardcode here.
253
253
 
254
254
  [network]
255
- rpc_url = "https://base-mainnet.g.alchemy.com/v2/YIA2uRCsFI-j5pqH-aRzflrACSlV1Qrs" # Alchemy Base RPC (recommended)
255
+ rpc_url = "https://mainnet.base.org" # Public Base RPC (default)
256
256
  chain_id = 8453 # Base mainnet. Use 84532 for Base Sepolia.
257
257
  entry_point = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" # ERC-4337 EntryPoint v0.7
258
258
 
@@ -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