arc402-cli 0.9.19 → 0.10.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.
Files changed (359) hide show
  1. package/README.md +41 -2
  2. package/dist/abis.d.ts +1 -0
  3. package/dist/abis.d.ts.map +1 -1
  4. package/dist/abis.js +45 -14
  5. package/dist/abis.js.map +1 -1
  6. package/dist/bundler.d.ts +1 -1
  7. package/dist/bundler.d.ts.map +1 -1
  8. package/dist/bundler.js +61 -27
  9. package/dist/bundler.js.map +1 -1
  10. package/dist/client.d.ts +1 -1
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +9 -5
  13. package/dist/client.js.map +1 -1
  14. package/dist/coinbase-smart-wallet.js +4 -1
  15. package/dist/coinbase-smart-wallet.js.map +1 -1
  16. package/dist/commands/accept.js +28 -25
  17. package/dist/commands/accept.js.map +1 -1
  18. package/dist/commands/agent-handshake.js +18 -15
  19. package/dist/commands/agent-handshake.js.map +1 -1
  20. package/dist/commands/agent.js +104 -98
  21. package/dist/commands/agent.js.map +1 -1
  22. package/dist/commands/agreements.js +98 -62
  23. package/dist/commands/agreements.js.map +1 -1
  24. package/dist/commands/arbitrator.js +81 -45
  25. package/dist/commands/arbitrator.js.map +1 -1
  26. package/dist/commands/arena-handshake.d.ts.map +1 -1
  27. package/dist/commands/arena-handshake.js +35 -53
  28. package/dist/commands/arena-handshake.js.map +1 -1
  29. package/dist/commands/arena.js +18 -12
  30. package/dist/commands/arena.js.map +1 -1
  31. package/dist/commands/backup.js +36 -30
  32. package/dist/commands/backup.js.map +1 -1
  33. package/dist/commands/cancel.js +18 -15
  34. package/dist/commands/cancel.js.map +1 -1
  35. package/dist/commands/channel.js +81 -45
  36. package/dist/commands/channel.js.map +1 -1
  37. package/dist/commands/coldstart.js +34 -31
  38. package/dist/commands/coldstart.js.map +1 -1
  39. package/dist/commands/compute.d.ts +14 -0
  40. package/dist/commands/compute.d.ts.map +1 -0
  41. package/dist/commands/compute.js +466 -0
  42. package/dist/commands/compute.js.map +1 -0
  43. package/dist/commands/config.js +30 -24
  44. package/dist/commands/config.js.map +1 -1
  45. package/dist/commands/contract-interaction.js +15 -12
  46. package/dist/commands/contract-interaction.js.map +1 -1
  47. package/dist/commands/daemon.d.ts.map +1 -1
  48. package/dist/commands/daemon.js +135 -98
  49. package/dist/commands/daemon.js.map +1 -1
  50. package/dist/commands/deliver.js +76 -37
  51. package/dist/commands/deliver.js.map +1 -1
  52. package/dist/commands/discover.js +27 -24
  53. package/dist/commands/discover.js.map +1 -1
  54. package/dist/commands/dispute.js +110 -104
  55. package/dist/commands/dispute.js.map +1 -1
  56. package/dist/commands/doctor.js +55 -16
  57. package/dist/commands/doctor.js.map +1 -1
  58. package/dist/commands/endpoint.js +95 -56
  59. package/dist/commands/endpoint.js.map +1 -1
  60. package/dist/commands/feed.js +18 -11
  61. package/dist/commands/feed.js.map +1 -1
  62. package/dist/commands/hire.js +40 -37
  63. package/dist/commands/hire.js.map +1 -1
  64. package/dist/commands/migrate.js +33 -30
  65. package/dist/commands/migrate.js.map +1 -1
  66. package/dist/commands/negotiate.d.ts.map +1 -1
  67. package/dist/commands/negotiate.js +36 -34
  68. package/dist/commands/negotiate.js.map +1 -1
  69. package/dist/commands/openshell.js +104 -68
  70. package/dist/commands/openshell.js.map +1 -1
  71. package/dist/commands/owner.js +20 -17
  72. package/dist/commands/owner.js.map +1 -1
  73. package/dist/commands/policy.js +43 -41
  74. package/dist/commands/policy.js.map +1 -1
  75. package/dist/commands/relay.d.ts.map +1 -1
  76. package/dist/commands/relay.js +51 -18
  77. package/dist/commands/relay.js.map +1 -1
  78. package/dist/commands/remediate.js +23 -20
  79. package/dist/commands/remediate.js.map +1 -1
  80. package/dist/commands/reputation.js +27 -25
  81. package/dist/commands/reputation.js.map +1 -1
  82. package/dist/commands/setup.js +104 -65
  83. package/dist/commands/setup.js.map +1 -1
  84. package/dist/commands/trust.js +20 -17
  85. package/dist/commands/trust.js.map +1 -1
  86. package/dist/commands/verify.js +21 -18
  87. package/dist/commands/verify.js.map +1 -1
  88. package/dist/commands/wallet.d.ts.map +1 -1
  89. package/dist/commands/wallet.js +645 -679
  90. package/dist/commands/wallet.js.map +1 -1
  91. package/dist/commands/watch.js +36 -33
  92. package/dist/commands/watch.js.map +1 -1
  93. package/dist/commands/watchtower.js +73 -37
  94. package/dist/commands/watchtower.js.map +1 -1
  95. package/dist/commands/workroom.d.ts.map +1 -1
  96. package/dist/commands/workroom.js +282 -143
  97. package/dist/commands/workroom.js.map +1 -1
  98. package/dist/config.d.ts +3 -0
  99. package/dist/config.d.ts.map +1 -1
  100. package/dist/config.js +71 -22
  101. package/dist/config.js.map +1 -1
  102. package/dist/daemon/compute-metering.d.ts +61 -0
  103. package/dist/daemon/compute-metering.d.ts.map +1 -0
  104. package/dist/daemon/compute-metering.js +299 -0
  105. package/dist/daemon/compute-metering.js.map +1 -0
  106. package/dist/daemon/compute-session.d.ts +100 -0
  107. package/dist/daemon/compute-session.d.ts.map +1 -0
  108. package/dist/daemon/compute-session.js +231 -0
  109. package/dist/daemon/compute-session.js.map +1 -0
  110. package/dist/daemon/config.d.ts +19 -1
  111. package/dist/daemon/config.d.ts.map +1 -1
  112. package/dist/daemon/config.js +90 -16
  113. package/dist/daemon/config.js.map +1 -1
  114. package/dist/daemon/credentials.d.ts +24 -0
  115. package/dist/daemon/credentials.d.ts.map +1 -0
  116. package/dist/daemon/credentials.js +80 -0
  117. package/dist/daemon/credentials.js.map +1 -0
  118. package/dist/daemon/delivery-client.d.ts +35 -0
  119. package/dist/daemon/delivery-client.d.ts.map +1 -0
  120. package/dist/daemon/delivery-client.js +231 -0
  121. package/dist/daemon/delivery-client.js.map +1 -0
  122. package/dist/daemon/file-delivery.d.ts +98 -0
  123. package/dist/daemon/file-delivery.d.ts.map +1 -0
  124. package/dist/daemon/file-delivery.js +461 -0
  125. package/dist/daemon/file-delivery.js.map +1 -0
  126. package/dist/daemon/hire-listener.d.ts +3 -3
  127. package/dist/daemon/hire-listener.d.ts.map +1 -1
  128. package/dist/daemon/hire-listener.js +47 -13
  129. package/dist/daemon/hire-listener.js.map +1 -1
  130. package/dist/daemon/index.d.ts +2 -1
  131. package/dist/daemon/index.d.ts.map +1 -1
  132. package/dist/daemon/index.js +526 -53
  133. package/dist/daemon/index.js.map +1 -1
  134. package/dist/daemon/job-lifecycle.d.ts +1 -1
  135. package/dist/daemon/job-lifecycle.d.ts.map +1 -1
  136. package/dist/daemon/job-lifecycle.js +51 -11
  137. package/dist/daemon/job-lifecycle.js.map +1 -1
  138. package/dist/daemon/notify.d.ts +1 -1
  139. package/dist/daemon/notify.d.ts.map +1 -1
  140. package/dist/daemon/notify.js +53 -19
  141. package/dist/daemon/notify.js.map +1 -1
  142. package/dist/daemon/token-metering.js +47 -8
  143. package/dist/daemon/token-metering.js.map +1 -1
  144. package/dist/daemon/userops.d.ts +2 -2
  145. package/dist/daemon/userops.d.ts.map +1 -1
  146. package/dist/daemon/userops.js +27 -23
  147. package/dist/daemon/userops.js.map +1 -1
  148. package/dist/daemon/wallet-monitor.d.ts +1 -1
  149. package/dist/daemon/wallet-monitor.d.ts.map +1 -1
  150. package/dist/daemon/wallet-monitor.js +12 -8
  151. package/dist/daemon/wallet-monitor.js.map +1 -1
  152. package/dist/daemon/worker-executor.d.ts +71 -0
  153. package/dist/daemon/worker-executor.d.ts.map +1 -0
  154. package/dist/daemon/worker-executor.js +382 -0
  155. package/dist/daemon/worker-executor.js.map +1 -0
  156. package/dist/drain-v4.js +64 -26
  157. package/dist/drain-v4.js.map +1 -1
  158. package/dist/endpoint-config.js +63 -20
  159. package/dist/endpoint-config.js.map +1 -1
  160. package/dist/endpoint-notify.d.ts.map +1 -1
  161. package/dist/endpoint-notify.js +49 -15
  162. package/dist/endpoint-notify.js.map +1 -1
  163. package/dist/index.js +50 -18
  164. package/dist/index.js.map +1 -1
  165. package/dist/openshell-runtime.d.ts.map +1 -1
  166. package/dist/openshell-runtime.js +82 -38
  167. package/dist/openshell-runtime.js.map +1 -1
  168. package/dist/program.d.ts.map +1 -1
  169. package/dist/program.js +85 -78
  170. package/dist/program.js.map +1 -1
  171. package/dist/repl.js +31 -25
  172. package/dist/repl.js.map +1 -1
  173. package/dist/signing.js +6 -3
  174. package/dist/signing.js.map +1 -1
  175. package/dist/telegram-notify.js +40 -3
  176. package/dist/telegram-notify.js.map +1 -1
  177. package/dist/tui/App.d.ts.map +1 -1
  178. package/dist/tui/App.js +56 -89
  179. package/dist/tui/App.js.map +1 -1
  180. package/dist/tui/Footer.js +7 -4
  181. package/dist/tui/Footer.js.map +1 -1
  182. package/dist/tui/Header.d.ts +1 -1
  183. package/dist/tui/Header.d.ts.map +1 -1
  184. package/dist/tui/Header.js +14 -9
  185. package/dist/tui/Header.js.map +1 -1
  186. package/dist/tui/InputLine.d.ts +2 -1
  187. package/dist/tui/InputLine.d.ts.map +1 -1
  188. package/dist/tui/InputLine.js +47 -97
  189. package/dist/tui/InputLine.js.map +1 -1
  190. package/dist/tui/Viewport.d.ts +1 -2
  191. package/dist/tui/Viewport.d.ts.map +1 -1
  192. package/dist/tui/Viewport.js +26 -6
  193. package/dist/tui/Viewport.js.map +1 -1
  194. package/dist/tui/WalletConnectPairing.js +19 -16
  195. package/dist/tui/WalletConnectPairing.js.map +1 -1
  196. package/dist/tui/components/Button.js +9 -6
  197. package/dist/tui/components/Button.js.map +1 -1
  198. package/dist/tui/components/CeremonyView.js +8 -5
  199. package/dist/tui/components/CeremonyView.js.map +1 -1
  200. package/dist/tui/components/CompletionDropdown.js +9 -6
  201. package/dist/tui/components/CompletionDropdown.js.map +1 -1
  202. package/dist/tui/components/ConfirmPrompt.js +8 -5
  203. package/dist/tui/components/ConfirmPrompt.js.map +1 -1
  204. package/dist/tui/components/CustomTextInput.js +14 -11
  205. package/dist/tui/components/CustomTextInput.js.map +1 -1
  206. package/dist/tui/components/InteractiveTable.js +12 -9
  207. package/dist/tui/components/InteractiveTable.js.map +1 -1
  208. package/dist/tui/components/StepSpinner.js +13 -10
  209. package/dist/tui/components/StepSpinner.js.map +1 -1
  210. package/dist/tui/components/Toast.js +12 -8
  211. package/dist/tui/components/Toast.js.map +1 -1
  212. package/dist/tui/index.d.ts.map +1 -1
  213. package/dist/tui/index.js +21 -28
  214. package/dist/tui/index.js.map +1 -1
  215. package/dist/tui/useChat.js +19 -13
  216. package/dist/tui/useChat.js.map +1 -1
  217. package/dist/tui/useCommand.d.ts +2 -3
  218. package/dist/tui/useCommand.d.ts.map +1 -1
  219. package/dist/tui/useCommand.js +24 -100
  220. package/dist/tui/useCommand.js.map +1 -1
  221. package/dist/tui/useNotifications.js +8 -5
  222. package/dist/tui/useNotifications.js.map +1 -1
  223. package/dist/tui/useScroll.d.ts.map +1 -1
  224. package/dist/tui/useScroll.js +12 -15
  225. package/dist/tui/useScroll.js.map +1 -1
  226. package/dist/ui/banner.d.ts +0 -12
  227. package/dist/ui/banner.d.ts.map +1 -1
  228. package/dist/ui/banner.js +19 -35
  229. package/dist/ui/banner.js.map +1 -1
  230. package/dist/ui/colors.js +19 -13
  231. package/dist/ui/colors.js.map +1 -1
  232. package/dist/ui/format.js +14 -6
  233. package/dist/ui/format.js.map +1 -1
  234. package/dist/ui/qr-render.js +11 -4
  235. package/dist/ui/qr-render.js.map +1 -1
  236. package/dist/ui/rpc-fallback.js +11 -6
  237. package/dist/ui/rpc-fallback.js.map +1 -1
  238. package/dist/ui/spinner.js +12 -6
  239. package/dist/ui/spinner.js.map +1 -1
  240. package/dist/ui/tree.js +6 -3
  241. package/dist/ui/tree.js.map +1 -1
  242. package/dist/utils/format.js +41 -27
  243. package/dist/utils/format.js.map +1 -1
  244. package/dist/utils/hash.js +42 -4
  245. package/dist/utils/hash.js.map +1 -1
  246. package/dist/utils/time.js +6 -2
  247. package/dist/utils/time.js.map +1 -1
  248. package/dist/wallet-router.d.ts +1 -1
  249. package/dist/wallet-router.d.ts.map +1 -1
  250. package/dist/wallet-router.js +19 -12
  251. package/dist/wallet-router.js.map +1 -1
  252. package/dist/walletconnect-session.d.ts +1 -1
  253. package/dist/walletconnect-session.d.ts.map +1 -1
  254. package/dist/walletconnect-session.js +11 -6
  255. package/dist/walletconnect-session.js.map +1 -1
  256. package/dist/walletconnect.d.ts +5 -6
  257. package/dist/walletconnect.d.ts.map +1 -1
  258. package/dist/walletconnect.js +35 -32
  259. package/dist/walletconnect.js.map +1 -1
  260. package/package.json +11 -10
  261. package/INK6-UX-SPEC.md +0 -446
  262. package/MIGRATION-SPEC.md +0 -108
  263. package/TUI-SPEC.md +0 -214
  264. package/scripts/authorize-machine-key.ts +0 -43
  265. package/scripts/drain-wallet.ts +0 -149
  266. package/scripts/execute-spend-only.ts +0 -81
  267. package/scripts/register-agent-userop.ts +0 -186
  268. package/src/abis.ts +0 -187
  269. package/src/bundler.ts +0 -235
  270. package/src/client.ts +0 -36
  271. package/src/coinbase-smart-wallet.ts +0 -51
  272. package/src/commands/accept.ts +0 -64
  273. package/src/commands/agent-handshake.ts +0 -72
  274. package/src/commands/agent.ts +0 -691
  275. package/src/commands/agreements.ts +0 -350
  276. package/src/commands/arbitrator.ts +0 -180
  277. package/src/commands/arena-handshake.ts +0 -274
  278. package/src/commands/arena.ts +0 -122
  279. package/src/commands/backup.ts +0 -117
  280. package/src/commands/cancel.ts +0 -35
  281. package/src/commands/channel.ts +0 -218
  282. package/src/commands/coldstart.ts +0 -165
  283. package/src/commands/config.ts +0 -68
  284. package/src/commands/contract-interaction.ts +0 -166
  285. package/src/commands/daemon.ts +0 -1054
  286. package/src/commands/deliver.ts +0 -148
  287. package/src/commands/discover.ts +0 -350
  288. package/src/commands/dispute.ts +0 -375
  289. package/src/commands/doctor.ts +0 -172
  290. package/src/commands/endpoint.ts +0 -620
  291. package/src/commands/feed.ts +0 -229
  292. package/src/commands/hire.ts +0 -245
  293. package/src/commands/migrate.ts +0 -177
  294. package/src/commands/negotiate.ts +0 -272
  295. package/src/commands/openshell.ts +0 -1055
  296. package/src/commands/owner.ts +0 -35
  297. package/src/commands/policy.ts +0 -263
  298. package/src/commands/relay.ts +0 -277
  299. package/src/commands/remediate.ts +0 -24
  300. package/src/commands/reputation.ts +0 -79
  301. package/src/commands/setup.ts +0 -343
  302. package/src/commands/trust.ts +0 -27
  303. package/src/commands/verify.ts +0 -91
  304. package/src/commands/wallet.ts +0 -3548
  305. package/src/commands/watch.ts +0 -220
  306. package/src/commands/watchtower.ts +0 -248
  307. package/src/commands/workroom.ts +0 -963
  308. package/src/config.ts +0 -220
  309. package/src/daemon/config.ts +0 -344
  310. package/src/daemon/hire-listener.ts +0 -226
  311. package/src/daemon/index.ts +0 -1089
  312. package/src/daemon/job-lifecycle.ts +0 -215
  313. package/src/daemon/notify.ts +0 -297
  314. package/src/daemon/token-metering.ts +0 -183
  315. package/src/daemon/userops.ts +0 -119
  316. package/src/daemon/wallet-monitor.ts +0 -90
  317. package/src/drain-v4.ts +0 -159
  318. package/src/endpoint-config.ts +0 -83
  319. package/src/endpoint-notify.ts +0 -134
  320. package/src/index.ts +0 -74
  321. package/src/openshell-runtime.ts +0 -281
  322. package/src/program.ts +0 -88
  323. package/src/repl.ts +0 -178
  324. package/src/signing.ts +0 -28
  325. package/src/telegram-notify.ts +0 -88
  326. package/src/tui/App.tsx +0 -263
  327. package/src/tui/Footer.tsx +0 -18
  328. package/src/tui/Header.tsx +0 -45
  329. package/src/tui/InputLine.tsx +0 -243
  330. package/src/tui/Viewport.tsx +0 -51
  331. package/src/tui/WalletConnectPairing.tsx +0 -114
  332. package/src/tui/components/Button.tsx +0 -38
  333. package/src/tui/components/CeremonyView.tsx +0 -39
  334. package/src/tui/components/CompletionDropdown.tsx +0 -56
  335. package/src/tui/components/ConfirmPrompt.tsx +0 -36
  336. package/src/tui/components/CustomTextInput.tsx +0 -132
  337. package/src/tui/components/InteractiveTable.tsx +0 -106
  338. package/src/tui/components/StepSpinner.tsx +0 -84
  339. package/src/tui/components/Toast.tsx +0 -59
  340. package/src/tui/index.tsx +0 -90
  341. package/src/tui/useChat.ts +0 -103
  342. package/src/tui/useCommand.ts +0 -238
  343. package/src/tui/useNotifications.ts +0 -28
  344. package/src/tui/useScroll.ts +0 -69
  345. package/src/ui/banner.ts +0 -78
  346. package/src/ui/colors.ts +0 -30
  347. package/src/ui/format.ts +0 -78
  348. package/src/ui/qr-render.ts +0 -92
  349. package/src/ui/rpc-fallback.ts +0 -59
  350. package/src/ui/spinner.ts +0 -56
  351. package/src/ui/tree.ts +0 -16
  352. package/src/utils/format.ts +0 -48
  353. package/src/utils/hash.ts +0 -5
  354. package/src/utils/time.ts +0 -15
  355. package/src/wallet-router.ts +0 -178
  356. package/src/walletconnect-session.ts +0 -27
  357. package/src/walletconnect.ts +0 -309
  358. package/test/time.test.js +0 -11
  359. package/tsconfig.json +0 -33
@@ -1,1089 +0,0 @@
1
- /**
2
- * ARC-402 Daemon — main process entry point.
3
- *
4
- * Startup sequence per Spec 32 §3.
5
- * Runs when spawned by `arc402 daemon start` or invoked with --foreground.
6
- *
7
- * IPC: Unix socket at ~/.arc402/daemon.sock (JSON-lines protocol).
8
- * Signals: SIGTERM → graceful shutdown.
9
- */
10
- import * as fs from "fs";
11
- import * as os from "os";
12
- import * as path from "path";
13
- import * as net from "net";
14
- import * as http from "http";
15
- import { ethers } from "ethers";
16
- import Database from "better-sqlite3";
17
-
18
- import * as crypto from "crypto";
19
-
20
- import {
21
- loadDaemonConfig,
22
- loadMachineKey,
23
- DAEMON_DIR,
24
- DAEMON_PID,
25
- DAEMON_LOG,
26
- DAEMON_DB,
27
- DAEMON_SOCK,
28
- type DaemonConfig,
29
- } from "./config.js";
30
- import { verifyWallet, getWalletBalance } from "./wallet-monitor.js";
31
- import { buildNotifier } from "./notify.js";
32
- import { HireListener } from "./hire-listener.js";
33
- import { UserOpsManager, buildAcceptCalldata } from "./userops.js";
34
- import { generateReceipt, extractLearnings, createJobDirectory, cleanJobDirectory } from "./job-lifecycle.js";
35
-
36
- // ─── State DB ─────────────────────────────────────────────────────────────────
37
-
38
- export interface HireRequestRow {
39
- id: string;
40
- agreement_id: string | null;
41
- hirer_address: string;
42
- capability: string;
43
- price_eth: string;
44
- deadline_unix: number;
45
- spec_hash: string;
46
- status: string; // pending_approval | accepted | rejected | delivered | complete
47
- created_at: number;
48
- updated_at: number;
49
- reject_reason: string | null;
50
- }
51
-
52
- export interface DaemonDB {
53
- insertHireRequest(row: Omit<HireRequestRow, "created_at" | "updated_at">): void;
54
- getHireRequest(id: string): HireRequestRow | undefined;
55
- updateHireRequestStatus(id: string, status: string, rejectReason?: string): void;
56
- listPendingHireRequests(): HireRequestRow[];
57
- listActiveHireRequests(): HireRequestRow[];
58
- countActiveHireRequests(): number;
59
- close(): void;
60
- }
61
-
62
- function openStateDB(dbPath: string): DaemonDB {
63
- const db = new Database(dbPath);
64
- db.pragma("journal_mode = WAL");
65
-
66
- db.exec(`
67
- CREATE TABLE IF NOT EXISTS hire_requests (
68
- id TEXT PRIMARY KEY,
69
- agreement_id TEXT,
70
- hirer_address TEXT NOT NULL,
71
- capability TEXT,
72
- price_eth TEXT,
73
- deadline_unix INTEGER,
74
- spec_hash TEXT,
75
- status TEXT NOT NULL,
76
- created_at INTEGER,
77
- updated_at INTEGER,
78
- reject_reason TEXT
79
- );
80
-
81
- CREATE TABLE IF NOT EXISTS userop_queue (
82
- id TEXT PRIMARY KEY,
83
- hire_request_id TEXT,
84
- call_data TEXT NOT NULL,
85
- user_op_hash TEXT,
86
- status TEXT NOT NULL,
87
- submitted_at INTEGER,
88
- included_at INTEGER,
89
- retry_count INTEGER DEFAULT 0,
90
- last_error TEXT
91
- );
92
-
93
- CREATE TABLE IF NOT EXISTS session_channels (
94
- channel_id TEXT PRIMARY KEY,
95
- counterparty TEXT NOT NULL,
96
- token_address TEXT,
97
- latest_state_seq INTEGER,
98
- latest_state_bytes BLOB,
99
- status TEXT NOT NULL,
100
- challenge_deadline_unix INTEGER,
101
- external_watcher_id TEXT
102
- );
103
-
104
- CREATE TABLE IF NOT EXISTS notifications (
105
- id INTEGER PRIMARY KEY AUTOINCREMENT,
106
- type TEXT NOT NULL,
107
- payload TEXT,
108
- sent_at INTEGER,
109
- status TEXT
110
- );
111
- `);
112
-
113
- const insertHireRequest = db.prepare(`
114
- INSERT OR IGNORE INTO hire_requests
115
- (id, agreement_id, hirer_address, capability, price_eth, deadline_unix, spec_hash, status, created_at, updated_at, reject_reason)
116
- VALUES
117
- (@id, @agreement_id, @hirer_address, @capability, @price_eth, @deadline_unix, @spec_hash, @status, @created_at, @updated_at, @reject_reason)
118
- `);
119
-
120
- const getHireRequest = db.prepare(`SELECT * FROM hire_requests WHERE id = ?`);
121
- const updateStatus = db.prepare(`UPDATE hire_requests SET status = ?, reject_reason = ?, updated_at = ? WHERE id = ?`);
122
- const listPending = db.prepare(`SELECT * FROM hire_requests WHERE status = 'pending_approval' ORDER BY created_at ASC`);
123
- const listActive = db.prepare(`SELECT * FROM hire_requests WHERE status IN ('accepted', 'delivered') ORDER BY created_at ASC`);
124
- const countActive = db.prepare(`SELECT COUNT(*) as n FROM hire_requests WHERE status IN ('accepted', 'delivered')`);
125
-
126
- return {
127
- insertHireRequest(row) {
128
- const now = Date.now();
129
- insertHireRequest.run({ ...row, created_at: now, updated_at: now });
130
- },
131
- getHireRequest(id) {
132
- return getHireRequest.get(id) as HireRequestRow | undefined;
133
- },
134
- updateHireRequestStatus(id, status, rejectReason) {
135
- updateStatus.run(status, rejectReason ?? null, Date.now(), id);
136
- },
137
- listPendingHireRequests() {
138
- return listPending.all() as HireRequestRow[];
139
- },
140
- listActiveHireRequests() {
141
- return listActive.all() as HireRequestRow[];
142
- },
143
- countActiveHireRequests() {
144
- const row = countActive.get() as { n: number };
145
- return row.n;
146
- },
147
- close() {
148
- db.close();
149
- },
150
- };
151
- }
152
-
153
- // ─── Auth token ───────────────────────────────────────────────────────────────
154
-
155
- const DAEMON_TOKEN_FILE = path.join(path.dirname(DAEMON_SOCK), "daemon.token");
156
-
157
- function generateApiToken(): string {
158
- return crypto.randomBytes(32).toString("hex");
159
- }
160
-
161
- function saveApiToken(token: string): void {
162
- fs.mkdirSync(path.dirname(DAEMON_TOKEN_FILE), { recursive: true, mode: 0o700 });
163
- fs.writeFileSync(DAEMON_TOKEN_FILE, token, { mode: 0o600 });
164
- }
165
-
166
- function loadApiToken(): string | null {
167
- try {
168
- return fs.readFileSync(DAEMON_TOKEN_FILE, "utf-8").trim();
169
- } catch {
170
- return null;
171
- }
172
- }
173
-
174
- // ─── Rate limiter ─────────────────────────────────────────────────────────────
175
-
176
- interface RateBucket { count: number; resetTime: number }
177
- const rateLimitMap = new Map<string, RateBucket>();
178
- const RATE_LIMIT = 30;
179
- const RATE_WINDOW_MS = 60_000;
180
-
181
- function checkRateLimit(ip: string): boolean {
182
- const now = Date.now();
183
- let bucket = rateLimitMap.get(ip);
184
- if (!bucket || now >= bucket.resetTime) {
185
- bucket = { count: 0, resetTime: now + RATE_WINDOW_MS };
186
- rateLimitMap.set(ip, bucket);
187
- }
188
- bucket.count++;
189
- return bucket.count <= RATE_LIMIT;
190
- }
191
-
192
- // Cleanup stale rate limit entries every 5 minutes to prevent unbounded growth
193
- let rateLimitCleanupInterval: ReturnType<typeof setInterval> | null = null;
194
-
195
- // ─── Body size limit ──────────────────────────────────────────────────────────
196
-
197
- const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
198
-
199
- // ─── Logger ───────────────────────────────────────────────────────────────────
200
-
201
- function openLogger(logPath: string, foreground: boolean): (entry: Record<string, unknown>) => void {
202
- let stream: fs.WriteStream | null = null;
203
- if (!foreground) {
204
- fs.mkdirSync(path.dirname(logPath), { recursive: true });
205
- stream = fs.createWriteStream(logPath, { flags: "a" });
206
- }
207
-
208
- return (entry: Record<string, unknown>) => {
209
- const line = JSON.stringify({ ...entry, ts: new Date().toISOString() });
210
- if (foreground) {
211
- process.stdout.write(line + "\n");
212
- } else if (stream) {
213
- stream.write(line + "\n");
214
- }
215
- };
216
- }
217
-
218
- // ─── IPC Socket ───────────────────────────────────────────────────────────────
219
-
220
- interface IpcContext {
221
- db: DaemonDB;
222
- config: DaemonConfig;
223
- startTime: number;
224
- walletAddress: string;
225
- machineKeyAddress: string;
226
- hireListener: HireListener | null;
227
- userOps: UserOpsManager | null;
228
- activeAgreements: number;
229
- bundlerMode: string;
230
- bundlerEndpoint: string;
231
- }
232
-
233
- function startIpcServer(ctx: IpcContext, log: ReturnType<typeof openLogger>, apiToken: string): net.Server {
234
- // Remove stale socket
235
- if (fs.existsSync(DAEMON_SOCK)) {
236
- fs.unlinkSync(DAEMON_SOCK);
237
- }
238
-
239
- const server = net.createServer((socket) => {
240
- let buf = "";
241
- let authenticated = false;
242
- socket.on("data", (data) => {
243
- buf += data.toString();
244
- const lines = buf.split("\n");
245
- buf = lines.pop() ?? "";
246
- for (const line of lines) {
247
- if (!line.trim()) continue;
248
- let cmd: { command: string; id?: string; reason?: string; auth?: string };
249
- try {
250
- cmd = JSON.parse(line) as { command: string; id?: string; reason?: string; auth?: string };
251
- } catch {
252
- socket.write(JSON.stringify({ ok: false, error: "invalid_json" }) + "\n");
253
- continue;
254
- }
255
-
256
- // First message must be auth
257
- if (!authenticated) {
258
- if (cmd.auth === apiToken) {
259
- authenticated = true;
260
- socket.write(JSON.stringify({ ok: true, authenticated: true }) + "\n");
261
- } else {
262
- log({ event: "ipc_auth_failed" });
263
- socket.write(JSON.stringify({ ok: false, error: "unauthorized" }) + "\n");
264
- socket.destroy();
265
- }
266
- continue;
267
- }
268
-
269
- const response = handleIpcCommand(cmd, ctx, log);
270
- socket.write(JSON.stringify(response) + "\n");
271
- }
272
- });
273
- socket.on("error", () => { /* client disconnected */ });
274
- });
275
-
276
- server.listen(DAEMON_SOCK, () => {
277
- fs.chmodSync(DAEMON_SOCK, 0o600);
278
- log({ event: "ipc_ready", socket: DAEMON_SOCK });
279
- });
280
-
281
- return server;
282
- }
283
-
284
- function handleIpcCommand(
285
- cmd: { command: string; id?: string; reason?: string },
286
- ctx: IpcContext,
287
- log: ReturnType<typeof openLogger>
288
- ): { ok: boolean; data?: unknown; error?: string } {
289
- const uptimeSeconds = Math.floor((Date.now() - ctx.startTime) / 1000);
290
- const uptimeStr = formatUptime(uptimeSeconds);
291
-
292
- switch (cmd.command) {
293
- case "status": {
294
- const pending = ctx.db.listPendingHireRequests();
295
- const active = ctx.db.listActiveHireRequests();
296
- return {
297
- ok: true,
298
- data: {
299
- state: "running",
300
- pid: process.pid,
301
- uptime: uptimeStr,
302
- wallet: ctx.walletAddress,
303
- machine_key_address: ctx.machineKeyAddress,
304
- relay_enabled: ctx.config.relay.enabled,
305
- relay_url: ctx.config.relay.relay_url,
306
- relay_poll_seconds: ctx.config.relay.poll_interval_seconds,
307
- watchtower_enabled: ctx.config.watchtower.enabled,
308
- bundler_mode: ctx.bundlerMode,
309
- bundler_endpoint: ctx.bundlerEndpoint,
310
- active_agreements: active.length,
311
- pending_approval: pending.length,
312
- },
313
- };
314
- }
315
-
316
- case "pending": {
317
- return { ok: true, data: { requests: ctx.db.listPendingHireRequests() } };
318
- }
319
-
320
- case "agreements": {
321
- return { ok: true, data: { agreements: ctx.db.listActiveHireRequests() } };
322
- }
323
-
324
- case "agreement": {
325
- if (!cmd.id) return { ok: false, error: "id required" };
326
- const agreement = ctx.db.getHireRequest(cmd.id);
327
- if (!agreement) return { ok: false, error: "agreement not found" };
328
- return { ok: true, data: { agreement } };
329
- }
330
-
331
- case "approve": {
332
- if (!cmd.id) return { ok: false, error: "id required" };
333
- const hire = ctx.db.getHireRequest(cmd.id);
334
- if (!hire) return { ok: false, error: "hire request not found" };
335
- if (hire.status !== "pending_approval") {
336
- return { ok: false, error: `hire request status is '${hire.status}', not pending_approval` };
337
- }
338
- ctx.db.updateHireRequestStatus(cmd.id, "accepted");
339
- log({ event: "hire_approved", id: cmd.id });
340
-
341
- // Trigger accept UserOp (fire and forget)
342
- if (ctx.userOps && ctx.config.serviceAgreementAddress) {
343
- const callData = buildAcceptCalldata(
344
- ctx.config.serviceAgreementAddress,
345
- hire.agreement_id ?? cmd.id,
346
- ctx.walletAddress
347
- );
348
- ctx.userOps.submit(callData, ctx.walletAddress).then((hash) => {
349
- log({ event: "userop_submitted", id: cmd.id, hash });
350
- }).catch((err: unknown) => {
351
- log({ event: "userop_error", id: cmd.id, error: String(err) });
352
- });
353
- }
354
-
355
- return { ok: true, data: { approved: true, id: cmd.id } };
356
- }
357
-
358
- case "reject": {
359
- if (!cmd.id) return { ok: false, error: "id required" };
360
- const hire = ctx.db.getHireRequest(cmd.id);
361
- if (!hire) return { ok: false, error: "hire request not found" };
362
- if (hire.status !== "pending_approval") {
363
- return { ok: false, error: `hire request status is '${hire.status}', not pending_approval` };
364
- }
365
- const reason = cmd.reason ?? "operator_rejected";
366
- ctx.db.updateHireRequestStatus(cmd.id, "rejected", reason);
367
- log({ event: "hire_rejected", id: cmd.id, reason });
368
- return { ok: true, data: { rejected: true, id: cmd.id, reason } };
369
- }
370
-
371
- case "complete": {
372
- // Called after a job is delivered and accepted. Triggers post-job lifecycle:
373
- // receipt generation, learning extraction, worker memory update.
374
- if (!cmd.id) return { ok: false, error: "id required" };
375
- const hire = ctx.db.getHireRequest(cmd.id);
376
- if (!hire) return { ok: false, error: "hire request not found" };
377
-
378
- const now = new Date().toISOString();
379
- const startedAt = new Date(hire.created_at).toISOString();
380
-
381
- // Generate execution receipt
382
- const receipt = generateReceipt({
383
- agreementId: hire.agreement_id ?? cmd.id,
384
- deliverableHash: hire.spec_hash ?? "0x0",
385
- walletAddress: ctx.walletAddress,
386
- startedAt,
387
- completedAt: now,
388
- });
389
- log({ event: "receipt_generated", id: cmd.id, receipt_hash: receipt.receipt_hash });
390
-
391
- // Extract learnings
392
- extractLearnings({
393
- agreementId: hire.agreement_id ?? cmd.id,
394
- taskDescription: hire.capability ?? "unknown",
395
- deliverableHash: hire.spec_hash ?? "0x0",
396
- priceEth: hire.price_eth ?? "0",
397
- capability: hire.capability ?? "general",
398
- wallClockSeconds: receipt.metrics.wall_clock_seconds,
399
- success: true,
400
- });
401
- log({ event: "learnings_extracted", id: cmd.id });
402
-
403
- // Update status to complete
404
- ctx.db.updateHireRequestStatus(cmd.id, "complete");
405
-
406
- // Clean job directory (keep receipt + memory)
407
- cleanJobDirectory(hire.agreement_id ?? cmd.id);
408
-
409
- return {
410
- ok: true,
411
- data: {
412
- completed: true,
413
- id: cmd.id,
414
- receipt_hash: receipt.receipt_hash,
415
- },
416
- };
417
- }
418
-
419
- default:
420
- return { ok: false, error: `unknown command: ${cmd.command}` };
421
- }
422
- }
423
-
424
- function formatUptime(seconds: number): string {
425
- const h = Math.floor(seconds / 3600);
426
- const m = Math.floor((seconds % 3600) / 60);
427
- const s = seconds % 60;
428
- if (h > 0) return `${h}h ${m}m`;
429
- if (m > 0) return `${m}m ${s}s`;
430
- return `${s}s`;
431
- }
432
-
433
- // ─── Daemon main ──────────────────────────────────────────────────────────────
434
-
435
- // Extend config with serviceAgreementAddress (loaded from CLI config if available)
436
- declare module "./config.js" {
437
- interface DaemonConfig {
438
- serviceAgreementAddress?: string;
439
- }
440
- }
441
-
442
- export async function runDaemon(foreground = false): Promise<void> {
443
- fs.mkdirSync(DAEMON_DIR, { recursive: true, mode: 0o700 });
444
- const log = openLogger(DAEMON_LOG, foreground);
445
-
446
- log({ event: "daemon_starting" });
447
-
448
- // ── Step 1: Load config ──────────────────────────────────────────────────
449
- let config: DaemonConfig;
450
- try {
451
- config = loadDaemonConfig();
452
- log({ event: "config_loaded", path: path.join(os.homedir(), ".arc402", "daemon.toml") });
453
- } catch (err) {
454
- process.stderr.write(`Config error: ${err}\n`);
455
- process.exit(1);
456
- }
457
-
458
- // ── Step 2: Load machine key ─────────────────────────────────────────────
459
- let machineKeyAddress: string;
460
- let machinePrivateKey: string;
461
- try {
462
- const mk = loadMachineKey(config);
463
- machinePrivateKey = mk.privateKey;
464
- machineKeyAddress = mk.address;
465
- log({ event: "machine_key_loaded", address: machineKeyAddress });
466
- } catch (err) {
467
- process.stderr.write(`Machine key error: ${err}\n`);
468
- process.exit(1);
469
- }
470
-
471
- // ── Step 3: Connect to RPC ───────────────────────────────────────────────
472
- const provider = new ethers.JsonRpcProvider(config.network.rpc_url);
473
- try {
474
- const chainId = (await provider.getNetwork()).chainId;
475
- if (Number(chainId) !== config.network.chain_id) {
476
- process.stderr.write(
477
- `RPC chain ID ${chainId} does not match config ${config.network.chain_id}\n`
478
- );
479
- process.exit(1);
480
- }
481
- const networkName = config.network.chain_id === 8453 ? "Base Mainnet" : `Chain ${config.network.chain_id}`;
482
- log({ event: "rpc_connected", chain_id: config.network.chain_id, network: networkName });
483
- } catch (err) {
484
- process.stderr.write(`RPC connection failed: ${err}\n`);
485
- process.exit(1);
486
- }
487
-
488
- // ── Step 4+5: Verify wallet ──────────────────────────────────────────────
489
- try {
490
- const walletStatus = await verifyWallet(config, provider, machineKeyAddress);
491
- log({
492
- event: "wallet_verified",
493
- address: walletStatus.contractAddress,
494
- owner: walletStatus.ownerAddress,
495
- balance_eth: walletStatus.ethBalance,
496
- machine_key_authorized: walletStatus.machineKeyAuthorized,
497
- });
498
- } catch (err) {
499
- process.stderr.write(`Wallet verification failed: ${err}\n`);
500
- process.exit(1);
501
- }
502
-
503
- // ── Step 6: Connect bundler ──────────────────────────────────────────────
504
- const userOps = new UserOpsManager(config, provider);
505
- const bundlerOk = await userOps.pingBundler();
506
- if (!bundlerOk) {
507
- log({ event: "bundler_warn", msg: "Bundler endpoint unreachable — will retry on demand" });
508
- }
509
- const bundlerEndpoint = config.bundler.endpoint || "https://api.pimlico.io/v2/base/rpc";
510
- log({ event: "bundler_configured", mode: config.bundler.mode, endpoint: bundlerEndpoint });
511
-
512
- // ── Step 7: Open state DB ────────────────────────────────────────────────
513
- let db: DaemonDB;
514
- try {
515
- db = openStateDB(DAEMON_DB);
516
- log({ event: "state_db_opened", path: DAEMON_DB });
517
- } catch (err) {
518
- process.stderr.write(`State DB error: ${err}\n`);
519
- process.exit(1);
520
- }
521
-
522
- // ── Setup notifier ───────────────────────────────────────────────────────
523
- const notifier = buildNotifier(config);
524
-
525
- // ── Step 10: Start relay listener ───────────────────────────────────────
526
- const hireListener = new HireListener(config, db, notifier, config.wallet.contract_address);
527
-
528
- const ipcCtx: IpcContext = {
529
- db,
530
- config,
531
- startTime: Date.now(),
532
- walletAddress: config.wallet.contract_address,
533
- machineKeyAddress,
534
- hireListener,
535
- userOps,
536
- activeAgreements: 0,
537
- bundlerMode: config.bundler.mode,
538
- bundlerEndpoint,
539
- };
540
-
541
- // Wire approve callback — submits UserOp when hire is auto-accepted
542
- hireListener.setApproveCallback(async (hireId) => {
543
- const hire = db.getHireRequest(hireId);
544
- if (!hire || !hire.agreement_id || !config.serviceAgreementAddress) return;
545
- try {
546
- const callData = buildAcceptCalldata(
547
- config.serviceAgreementAddress,
548
- hire.agreement_id,
549
- config.wallet.contract_address
550
- );
551
- const hash = await userOps.submit(callData, config.wallet.contract_address);
552
- log({ event: "hire_auto_accepted", id: hireId, userop_hash: hash });
553
- if (config.notifications.notify_on_hire_accepted) {
554
- await notifier.notifyHireAccepted(hireId, hire.agreement_id);
555
- }
556
- } catch (err) {
557
- log({ event: "accept_userop_error", id: hireId, error: String(err) });
558
- }
559
- });
560
-
561
- // Relay poll interval
562
- let relayInterval: ReturnType<typeof setInterval> | null = null;
563
- if (config.relay.enabled && config.relay.relay_url) {
564
- const pollMs = config.relay.poll_interval_seconds * 1000;
565
- relayInterval = setInterval(() => { void hireListener.poll(); }, pollMs);
566
- void hireListener.poll(); // immediate first poll
567
- log({ event: "relay_started", url: config.relay.relay_url, poll_seconds: config.relay.poll_interval_seconds });
568
- }
569
-
570
- // Hire timeout checker — reject stale pending approvals
571
- const timeoutInterval = setInterval(() => {
572
- const pending = db.listPendingHireRequests();
573
- const now = Math.floor(Date.now() / 1000);
574
- for (const req of pending) {
575
- const minLead = config.policy.min_hire_lead_time_seconds;
576
- if (req.deadline_unix > 0 && req.deadline_unix < now + minLead) {
577
- db.updateHireRequestStatus(req.id, "rejected", "approval_timeout");
578
- log({ event: "hire_timeout_rejected", id: req.id });
579
- }
580
- }
581
- }, 30_000);
582
-
583
- // Rate limit map cleanup — every 5 minutes (prevents unbounded growth)
584
- rateLimitCleanupInterval = setInterval(() => {
585
- const now = Date.now();
586
- for (const [ip, bucket] of rateLimitMap) {
587
- if (bucket.resetTime < now) rateLimitMap.delete(ip);
588
- }
589
- }, 5 * 60 * 1000);
590
-
591
- // Balance monitor — every 5 minutes
592
- const balanceInterval = setInterval(async () => {
593
- try {
594
- const balance = await getWalletBalance(config.wallet.contract_address, provider);
595
- const threshold = parseFloat(config.notifications.low_balance_threshold_eth);
596
- if (parseFloat(balance) < threshold) {
597
- log({ event: "low_balance", balance_eth: balance, threshold_eth: config.notifications.low_balance_threshold_eth });
598
- await notifier.notifyLowBalance(balance, config.notifications.low_balance_threshold_eth);
599
- }
600
- } catch { /* non-fatal */ }
601
- }, 5 * 60 * 1000);
602
-
603
- // ── Step 11: Write PID file (if not foreground) ──────────────────────────
604
- if (!foreground) {
605
- fs.writeFileSync(DAEMON_PID, String(process.pid), { mode: 0o600 });
606
- log({ event: "pid_written", pid: process.pid, path: DAEMON_PID });
607
- }
608
-
609
- // ── Generate and save API token ──────────────────────────────────────────
610
- const apiToken = generateApiToken();
611
- saveApiToken(apiToken);
612
- log({ event: "auth_token_saved", path: DAEMON_TOKEN_FILE });
613
-
614
- // ── Start IPC socket ─────────────────────────────────────────────────────
615
- const ipcServer = startIpcServer(ipcCtx, log, apiToken);
616
-
617
- // ── Start HTTP relay server (public endpoint) ────────────────────────────
618
- const httpPort = config.relay.listen_port ?? 4402;
619
-
620
- /**
621
- * Optionally verifies X-ARC402-Signature against the request body.
622
- * Logs the result but never rejects — unsigned requests are accepted for backwards compat.
623
- */
624
- function verifyRequestSignature(body: string, req: http.IncomingMessage): void {
625
- const sig = req.headers["x-arc402-signature"] as string | undefined;
626
- if (!sig) return;
627
- const claimedSigner = req.headers["x-arc402-signer"] as string | undefined;
628
- try {
629
- const recovered = ethers.verifyMessage(body, sig);
630
- if (claimedSigner && recovered.toLowerCase() !== claimedSigner.toLowerCase()) {
631
- log({ event: "sig_mismatch", claimed: claimedSigner, recovered });
632
- } else {
633
- log({ event: "sig_verified", signer: recovered });
634
- }
635
- } catch {
636
- log({ event: "sig_invalid" });
637
- }
638
- }
639
-
640
- /**
641
- * Read request body with a size cap. Destroys the request and sends 413
642
- * if the body exceeds MAX_BODY_SIZE. Returns null in that case.
643
- */
644
- function readBody(req: http.IncomingMessage, res: http.ServerResponse): Promise<string | null> {
645
- return new Promise((resolve) => {
646
- let body = "";
647
- let size = 0;
648
- req.on("data", (chunk: Buffer) => {
649
- size += chunk.length;
650
- if (size > MAX_BODY_SIZE) {
651
- req.destroy();
652
- res.writeHead(413, { "Content-Type": "application/json" });
653
- res.end(JSON.stringify({ error: "payload_too_large" }));
654
- resolve(null);
655
- return;
656
- }
657
- body += chunk.toString();
658
- });
659
- req.on("end", () => { resolve(body); });
660
- req.on("error", () => { resolve(null); });
661
- });
662
- }
663
-
664
- const PUBLIC_GET_PATHS = new Set(["/", "/health", "/agent", "/capabilities", "/status"]);
665
-
666
- // CORS whitelist — localhost for local tooling, arc402.xyz for the web app
667
- const CORS_WHITELIST = new Set(["localhost", "127.0.0.1", "arc402.xyz", "app.arc402.xyz"]);
668
-
669
- const httpServer = http.createServer(async (req, res) => {
670
- // CORS — only reflect origin header if it's in the whitelist
671
- const origin = (req.headers["origin"] ?? "") as string;
672
- if (origin) {
673
- try {
674
- const { hostname } = new URL(origin);
675
- if (CORS_WHITELIST.has(hostname)) {
676
- res.setHeader("Access-Control-Allow-Origin", origin);
677
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
678
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
679
- }
680
- } catch { /* ignore invalid origin */ }
681
- }
682
- if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
683
-
684
- const url = new URL(req.url || "/", `http://localhost:${httpPort}`);
685
- const pathname = url.pathname;
686
-
687
- // Rate limiting (all endpoints)
688
- const clientIp = (req.socket.remoteAddress ?? "unknown").replace(/^::ffff:/, "");
689
- if (!checkRateLimit(clientIp)) {
690
- log({ event: "rate_limited", ip: clientIp, path: pathname });
691
- res.writeHead(429, { "Content-Type": "application/json" });
692
- res.end(JSON.stringify({ error: "too_many_requests" }));
693
- return;
694
- }
695
-
696
- // Auth required on all POST endpoints (GET public paths are open)
697
- if (req.method === "POST" || (req.method === "GET" && !PUBLIC_GET_PATHS.has(pathname))) {
698
- const authHeader = req.headers["authorization"] ?? "";
699
- const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
700
- if (token !== apiToken) {
701
- log({ event: "http_unauthorized", ip: clientIp, path: pathname });
702
- res.writeHead(401, { "Content-Type": "application/json" });
703
- res.end(JSON.stringify({ error: "unauthorized" }));
704
- return;
705
- }
706
- }
707
-
708
- // Health / info
709
- if (pathname === "/" || pathname === "/health") {
710
- const info = {
711
- protocol: "arc-402",
712
- version: "0.3.0",
713
- agent: config.wallet.contract_address,
714
- status: "online",
715
- capabilities: config.policy.allowed_capabilities,
716
- uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
717
- };
718
- res.writeHead(200, { "Content-Type": "application/json" });
719
- res.end(JSON.stringify(info));
720
- return;
721
- }
722
-
723
- // Agent info
724
- if (pathname === "/agent") {
725
- res.writeHead(200, { "Content-Type": "application/json" });
726
- res.end(JSON.stringify({
727
- wallet: config.wallet.contract_address,
728
- owner: config.wallet.owner_address,
729
- machineKey: machineKeyAddress,
730
- chainId: config.network.chain_id,
731
- bundlerMode: config.bundler.mode,
732
- relay: config.relay.enabled,
733
- }));
734
- return;
735
- }
736
-
737
- // Receive hire proposal
738
- if (pathname === "/hire" && req.method === "POST") {
739
- const body = await readBody(req, res);
740
- if (body === null) return;
741
- verifyRequestSignature(body, req);
742
- try {
743
- const msg = JSON.parse(body) as Record<string, unknown>;
744
-
745
- // Feed into the hire listener's message handler
746
- const proposal = {
747
- messageId: String(msg.messageId ?? msg.id ?? `http_${Date.now()}`),
748
- hirerAddress: String(msg.hirerAddress ?? msg.hirer_address ?? msg.from ?? ""),
749
- capability: String(msg.capability ?? ""),
750
- priceEth: String(msg.priceEth ?? msg.price_eth ?? "0"),
751
- deadlineUnix: Number(msg.deadlineUnix ?? msg.deadline ?? 0),
752
- specHash: String(msg.specHash ?? msg.spec_hash ?? ""),
753
- agreementId: msg.agreementId ? String(msg.agreementId) : undefined,
754
- signature: msg.signature ? String(msg.signature) : undefined,
755
- };
756
-
757
- // Dedup
758
- const existing = db.getHireRequest(proposal.messageId);
759
- if (existing) {
760
- log({ event: "hire_duplicate", id: proposal.messageId, status: existing.status });
761
- res.writeHead(200, { "Content-Type": "application/json" });
762
- res.end(JSON.stringify({ status: existing.status, id: proposal.messageId }));
763
- return;
764
- }
765
-
766
- // Policy check
767
- const { evaluatePolicy } = await import("./hire-listener.js");
768
- const activeCount = db.countActiveHireRequests();
769
- const policyResult = evaluatePolicy(proposal, config, activeCount);
770
-
771
- if (!policyResult.allowed) {
772
- db.insertHireRequest({
773
- id: proposal.messageId,
774
- agreement_id: proposal.agreementId ?? null,
775
- hirer_address: proposal.hirerAddress,
776
- capability: proposal.capability,
777
- price_eth: proposal.priceEth,
778
- deadline_unix: proposal.deadlineUnix,
779
- spec_hash: proposal.specHash,
780
- status: "rejected",
781
- reject_reason: policyResult.reason ?? "policy_violation",
782
- });
783
- log({ event: "http_hire_rejected", id: proposal.messageId, reason: policyResult.reason });
784
- res.writeHead(200, { "Content-Type": "application/json" });
785
- res.end(JSON.stringify({ status: "rejected", reason: policyResult.reason, id: proposal.messageId }));
786
- return;
787
- }
788
-
789
- const status = config.policy.auto_accept ? "accepted" : "pending_approval";
790
- db.insertHireRequest({
791
- id: proposal.messageId,
792
- agreement_id: proposal.agreementId ?? null,
793
- hirer_address: proposal.hirerAddress,
794
- capability: proposal.capability,
795
- price_eth: proposal.priceEth,
796
- deadline_unix: proposal.deadlineUnix,
797
- spec_hash: proposal.specHash,
798
- status,
799
- reject_reason: null,
800
- });
801
-
802
- log({ event: "http_hire_received", id: proposal.messageId, hirer: proposal.hirerAddress, status });
803
-
804
- if (config.notifications.notify_on_hire_request) {
805
- await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
806
- }
807
-
808
- res.writeHead(200, { "Content-Type": "application/json" });
809
- res.end(JSON.stringify({ status, id: proposal.messageId }));
810
- } catch (err) {
811
- log({ event: "http_hire_error", error: String(err) });
812
- res.writeHead(400, { "Content-Type": "application/json" });
813
- res.end(JSON.stringify({ error: "invalid_request" }));
814
- }
815
- return;
816
- }
817
-
818
- // Handshake acknowledgment endpoint
819
- if (pathname === "/handshake" && req.method === "POST") {
820
- const body = await readBody(req, res);
821
- if (body === null) return;
822
- verifyRequestSignature(body, req);
823
- try {
824
- const msg = JSON.parse(body);
825
- log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
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
- return;
833
- }
834
-
835
- // POST /hire/accepted — provider accepted, client notified
836
- if (pathname === "/hire/accepted" && req.method === "POST") {
837
- const body = await readBody(req, res);
838
- if (body === null) return;
839
- try {
840
- const msg = JSON.parse(body) as Record<string, unknown>;
841
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
842
- const from = String(msg.from ?? "");
843
- log({ event: "hire_accepted_inbound", agreementId, from });
844
- if (config.notifications.notify_on_hire_accepted) {
845
- await notifier.notifyHireAccepted(agreementId, agreementId);
846
- }
847
- res.writeHead(200, { "Content-Type": "application/json" });
848
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
849
- } catch {
850
- res.writeHead(400, { "Content-Type": "application/json" });
851
- res.end(JSON.stringify({ error: "invalid_request" }));
852
- }
853
- return;
854
- }
855
-
856
- // POST /message — off-chain negotiation message
857
- if (pathname === "/message" && req.method === "POST") {
858
- const body = await readBody(req, res);
859
- if (body === null) return;
860
- verifyRequestSignature(body, req);
861
- try {
862
- const msg = JSON.parse(body) as Record<string, unknown>;
863
- const from = String(msg.from ?? "");
864
- const to = String(msg.to ?? "");
865
- const content = String(msg.content ?? "");
866
- const timestamp = Number(msg.timestamp ?? Date.now());
867
- log({ event: "message_received", from, to, timestamp, content_len: content.length });
868
- res.writeHead(200, { "Content-Type": "application/json" });
869
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
870
- } catch {
871
- res.writeHead(400, { "Content-Type": "application/json" });
872
- res.end(JSON.stringify({ error: "invalid_request" }));
873
- }
874
- return;
875
- }
876
-
877
- // POST /delivery — provider committed a deliverable
878
- if (pathname === "/delivery" && req.method === "POST") {
879
- const body = await readBody(req, res);
880
- if (body === null) return;
881
- verifyRequestSignature(body, req);
882
- try {
883
- const msg = JSON.parse(body) as Record<string, unknown>;
884
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
885
- const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
886
- const from = String(msg.from ?? "");
887
- log({ event: "delivery_received", agreementId, deliverableHash, from });
888
- // Update DB: mark delivered
889
- const active = db.listActiveHireRequests();
890
- const found = active.find(r => r.agreement_id === agreementId);
891
- if (found) db.updateHireRequestStatus(found.id, "delivered");
892
- if (config.notifications.notify_on_delivery) {
893
- await notifier.notifyDelivery(agreementId, deliverableHash, "");
894
- }
895
- res.writeHead(200, { "Content-Type": "application/json" });
896
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
897
- } catch {
898
- res.writeHead(400, { "Content-Type": "application/json" });
899
- res.end(JSON.stringify({ error: "invalid_request" }));
900
- }
901
- return;
902
- }
903
-
904
- // POST /delivery/accepted — client accepted delivery, payment releasing
905
- if (pathname === "/delivery/accepted" && req.method === "POST") {
906
- const body = await readBody(req, res);
907
- if (body === null) return;
908
- try {
909
- const msg = JSON.parse(body) as Record<string, unknown>;
910
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
911
- const from = String(msg.from ?? "");
912
- log({ event: "delivery_accepted_inbound", agreementId, from });
913
- // Update DB: mark complete
914
- const all = db.listActiveHireRequests();
915
- const found = all.find(r => r.agreement_id === agreementId);
916
- if (found) db.updateHireRequestStatus(found.id, "complete");
917
- res.writeHead(200, { "Content-Type": "application/json" });
918
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
919
- } catch {
920
- res.writeHead(400, { "Content-Type": "application/json" });
921
- res.end(JSON.stringify({ error: "invalid_request" }));
922
- }
923
- return;
924
- }
925
-
926
- // POST /dispute — dispute raised against this agent
927
- if (pathname === "/dispute" && req.method === "POST") {
928
- const body = await readBody(req, res);
929
- if (body === null) return;
930
- try {
931
- const msg = JSON.parse(body) as Record<string, unknown>;
932
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
933
- const reason = String(msg.reason ?? "");
934
- const from = String(msg.from ?? "");
935
- log({ event: "dispute_received", agreementId, reason, from });
936
- if (config.notifications.notify_on_dispute) {
937
- await notifier.notifyDispute(agreementId, from);
938
- }
939
- res.writeHead(200, { "Content-Type": "application/json" });
940
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
941
- } catch {
942
- res.writeHead(400, { "Content-Type": "application/json" });
943
- res.end(JSON.stringify({ error: "invalid_request" }));
944
- }
945
- return;
946
- }
947
-
948
- // POST /dispute/resolved — dispute resolved by arbitrator
949
- if (pathname === "/dispute/resolved" && req.method === "POST") {
950
- const body = await readBody(req, res);
951
- if (body === null) return;
952
- try {
953
- const msg = JSON.parse(body) as Record<string, unknown>;
954
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
955
- const outcome = String(msg.outcome ?? "");
956
- const from = String(msg.from ?? "");
957
- log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
958
- res.writeHead(200, { "Content-Type": "application/json" });
959
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
960
- } catch {
961
- res.writeHead(400, { "Content-Type": "application/json" });
962
- res.end(JSON.stringify({ error: "invalid_request" }));
963
- }
964
- return;
965
- }
966
-
967
- // POST /workroom/status — workroom lifecycle events
968
- if (pathname === "/workroom/status" && req.method === "POST") {
969
- const body = await readBody(req, res);
970
- if (body === null) return;
971
- try {
972
- const msg = JSON.parse(body) as Record<string, unknown>;
973
- const event = String(msg.event ?? "");
974
- const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
975
- const jobId = msg.jobId ? String(msg.jobId) : undefined;
976
- const timestamp = Number(msg.timestamp ?? Date.now());
977
- log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
978
- res.writeHead(200, { "Content-Type": "application/json" });
979
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
980
- } catch {
981
- res.writeHead(400, { "Content-Type": "application/json" });
982
- res.end(JSON.stringify({ error: "invalid_request" }));
983
- }
984
- return;
985
- }
986
-
987
- // GET /capabilities — agent capabilities from config
988
- if (pathname === "/capabilities" && req.method === "GET") {
989
- res.writeHead(200, { "Content-Type": "application/json" });
990
- res.end(JSON.stringify({
991
- capabilities: config.policy.allowed_capabilities,
992
- max_price_eth: config.policy.max_price_eth,
993
- auto_accept: config.policy.auto_accept,
994
- max_concurrent_agreements: config.relay.max_concurrent_agreements,
995
- }));
996
- return;
997
- }
998
-
999
- // GET /status — health with active agreement count (sensitive counts only for authenticated)
1000
- if (pathname === "/status" && req.method === "GET") {
1001
- const statusAuth = (req.headers["authorization"] ?? "") as string;
1002
- const statusToken = statusAuth.startsWith("Bearer ") ? statusAuth.slice(7) : "";
1003
- const statusAuthed = statusToken === apiToken;
1004
- const statusPayload: Record<string, unknown> = {
1005
- protocol: "arc-402",
1006
- version: "0.3.0",
1007
- agent: config.wallet.contract_address,
1008
- status: "online",
1009
- uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
1010
- capabilities: config.policy.allowed_capabilities,
1011
- };
1012
- if (statusAuthed) {
1013
- statusPayload.active_agreements = db.listActiveHireRequests().length;
1014
- statusPayload.pending_approval = db.listPendingHireRequests().length;
1015
- }
1016
- res.writeHead(200, { "Content-Type": "application/json" });
1017
- res.end(JSON.stringify(statusPayload));
1018
- return;
1019
- }
1020
-
1021
- // 404
1022
- res.writeHead(404, { "Content-Type": "application/json" });
1023
- res.end(JSON.stringify({ error: "not_found" }));
1024
- });
1025
-
1026
- httpServer.listen(httpPort, "0.0.0.0", () => {
1027
- log({ event: "http_server_started", port: httpPort });
1028
- });
1029
-
1030
- // ── Step 12: Startup complete ────────────────────────────────────────────
1031
- const subsystems = [];
1032
- if (config.relay.enabled) subsystems.push("relay");
1033
- if (config.watchtower.enabled) subsystems.push("watchtower");
1034
- subsystems.push(`bundler(${config.bundler.mode})`);
1035
-
1036
- log({
1037
- event: "daemon_started",
1038
- wallet: config.wallet.contract_address,
1039
- subsystems,
1040
- pid: process.pid,
1041
- });
1042
-
1043
- await notifier.notifyStarted(config.wallet.contract_address, subsystems);
1044
-
1045
- // ── Graceful shutdown ────────────────────────────────────────────────────
1046
- const shutdown = async (signal: string) => {
1047
- log({ event: "daemon_stopping", signal });
1048
-
1049
- // Stop accepting new hire requests
1050
- if (relayInterval) clearInterval(relayInterval);
1051
- clearInterval(timeoutInterval);
1052
- clearInterval(balanceInterval);
1053
- if (rateLimitCleanupInterval) clearInterval(rateLimitCleanupInterval);
1054
-
1055
- // Close HTTP + IPC
1056
- httpServer.close();
1057
- ipcServer.close();
1058
- if (fs.existsSync(DAEMON_SOCK)) fs.unlinkSync(DAEMON_SOCK);
1059
-
1060
- await notifier.notifyStopped();
1061
- log({ event: "daemon_stopped" });
1062
-
1063
- // Clean up PID file
1064
- if (!foreground && fs.existsSync(DAEMON_PID)) {
1065
- fs.unlinkSync(DAEMON_PID);
1066
- }
1067
-
1068
- db.close();
1069
- process.exit(0);
1070
- };
1071
-
1072
- process.on("SIGTERM", () => { void shutdown("SIGTERM"); });
1073
- process.on("SIGINT", () => { void shutdown("SIGINT"); });
1074
-
1075
- // Keep process alive
1076
- process.stdin.resume();
1077
- }
1078
-
1079
- // ─── Entry point ──────────────────────────────────────────────────────────────
1080
- // Run when spawned directly (node dist/daemon/index.js [--foreground])
1081
-
1082
- if (require.main === module || process.env.ARC402_DAEMON_PROCESS === "1") {
1083
- const foreground = process.argv.includes("--foreground") ||
1084
- process.env.ARC402_DAEMON_FOREGROUND === "1";
1085
- runDaemon(foreground).catch((err) => {
1086
- process.stderr.write(`Daemon fatal error: ${err}\n`);
1087
- process.exit(1);
1088
- });
1089
- }