arc402-cli 1.0.0-rc.1 → 1.1.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 (257) hide show
  1. package/README.md +43 -2
  2. package/dist/abis.d.ts +1 -0
  3. package/dist/abis.d.ts.map +1 -1
  4. package/dist/abis.js +29 -1
  5. package/dist/abis.js.map +1 -1
  6. package/dist/commands/backup.d.ts +3 -0
  7. package/dist/commands/backup.d.ts.map +1 -0
  8. package/dist/commands/backup.js +106 -0
  9. package/dist/commands/backup.js.map +1 -0
  10. package/dist/commands/compute.d.ts +14 -0
  11. package/dist/commands/compute.d.ts.map +1 -0
  12. package/dist/commands/compute.js +466 -0
  13. package/dist/commands/compute.js.map +1 -0
  14. package/dist/commands/config.d.ts.map +1 -1
  15. package/dist/commands/config.js +11 -1
  16. package/dist/commands/config.js.map +1 -1
  17. package/dist/commands/daemon.d.ts.map +1 -1
  18. package/dist/commands/daemon.js +67 -0
  19. package/dist/commands/daemon.js.map +1 -1
  20. package/dist/commands/discover.d.ts.map +1 -1
  21. package/dist/commands/discover.js +60 -15
  22. package/dist/commands/discover.js.map +1 -1
  23. package/dist/commands/doctor.d.ts +3 -0
  24. package/dist/commands/doctor.d.ts.map +1 -0
  25. package/dist/commands/doctor.js +205 -0
  26. package/dist/commands/doctor.js.map +1 -0
  27. package/dist/commands/tunnel.d.ts +3 -0
  28. package/dist/commands/tunnel.d.ts.map +1 -0
  29. package/dist/commands/tunnel.js +281 -0
  30. package/dist/commands/tunnel.js.map +1 -0
  31. package/dist/commands/wallet.d.ts.map +1 -1
  32. package/dist/commands/wallet.js +299 -65
  33. package/dist/commands/wallet.js.map +1 -1
  34. package/dist/commands/watch.d.ts.map +1 -1
  35. package/dist/commands/watch.js +146 -9
  36. package/dist/commands/watch.js.map +1 -1
  37. package/dist/commands/workroom.d.ts.map +1 -1
  38. package/dist/commands/workroom.js +112 -6
  39. package/dist/commands/workroom.js.map +1 -1
  40. package/dist/config.d.ts +13 -0
  41. package/dist/config.d.ts.map +1 -1
  42. package/dist/config.js +41 -4
  43. package/dist/config.js.map +1 -1
  44. package/dist/daemon/compute-metering.d.ts +61 -0
  45. package/dist/daemon/compute-metering.d.ts.map +1 -0
  46. package/dist/daemon/compute-metering.js +299 -0
  47. package/dist/daemon/compute-metering.js.map +1 -0
  48. package/dist/daemon/compute-session.d.ts +100 -0
  49. package/dist/daemon/compute-session.d.ts.map +1 -0
  50. package/dist/daemon/compute-session.js +231 -0
  51. package/dist/daemon/compute-session.js.map +1 -0
  52. package/dist/daemon/config.d.ts +33 -1
  53. package/dist/daemon/config.d.ts.map +1 -1
  54. package/dist/daemon/config.js +69 -0
  55. package/dist/daemon/config.js.map +1 -1
  56. package/dist/daemon/credentials.d.ts +24 -0
  57. package/dist/daemon/credentials.d.ts.map +1 -0
  58. package/dist/daemon/credentials.js +80 -0
  59. package/dist/daemon/credentials.js.map +1 -0
  60. package/dist/daemon/delivery-client.d.ts +35 -0
  61. package/dist/daemon/delivery-client.d.ts.map +1 -0
  62. package/dist/daemon/delivery-client.js +231 -0
  63. package/dist/daemon/delivery-client.js.map +1 -0
  64. package/dist/daemon/file-delivery.d.ts +98 -0
  65. package/dist/daemon/file-delivery.d.ts.map +1 -0
  66. package/dist/daemon/file-delivery.js +461 -0
  67. package/dist/daemon/file-delivery.js.map +1 -0
  68. package/dist/daemon/index.d.ts +1 -0
  69. package/dist/daemon/index.d.ts.map +1 -1
  70. package/dist/daemon/index.js +793 -227
  71. package/dist/daemon/index.js.map +1 -1
  72. package/dist/daemon/notify.d.ts +35 -6
  73. package/dist/daemon/notify.d.ts.map +1 -1
  74. package/dist/daemon/notify.js +176 -48
  75. package/dist/daemon/notify.js.map +1 -1
  76. package/dist/daemon/worker-executor.d.ts +71 -0
  77. package/dist/daemon/worker-executor.d.ts.map +1 -0
  78. package/dist/daemon/worker-executor.js +382 -0
  79. package/dist/daemon/worker-executor.js.map +1 -0
  80. package/dist/drain-v4.js +2 -2
  81. package/dist/drain-v4.js.map +1 -1
  82. package/dist/endpoint-notify.d.ts +9 -1
  83. package/dist/endpoint-notify.d.ts.map +1 -1
  84. package/dist/endpoint-notify.js +116 -3
  85. package/dist/endpoint-notify.js.map +1 -1
  86. package/dist/index.js +81 -1
  87. package/dist/index.js.map +1 -1
  88. package/dist/program.d.ts.map +1 -1
  89. package/dist/program.js +8 -0
  90. package/dist/program.js.map +1 -1
  91. package/dist/repl.d.ts.map +1 -1
  92. package/dist/repl.js +69 -486
  93. package/dist/repl.js.map +1 -1
  94. package/dist/tui/App.d.ts +12 -0
  95. package/dist/tui/App.d.ts.map +1 -0
  96. package/dist/tui/App.js +154 -0
  97. package/dist/tui/App.js.map +1 -0
  98. package/dist/tui/Footer.d.ts +11 -0
  99. package/dist/tui/Footer.d.ts.map +1 -0
  100. package/dist/tui/Footer.js +13 -0
  101. package/dist/tui/Footer.js.map +1 -0
  102. package/dist/tui/Header.d.ts +14 -0
  103. package/dist/tui/Header.d.ts.map +1 -0
  104. package/dist/tui/Header.js +19 -0
  105. package/dist/tui/Header.js.map +1 -0
  106. package/dist/tui/InputLine.d.ts +11 -0
  107. package/dist/tui/InputLine.d.ts.map +1 -0
  108. package/dist/tui/InputLine.js +145 -0
  109. package/dist/tui/InputLine.js.map +1 -0
  110. package/dist/tui/Viewport.d.ts +14 -0
  111. package/dist/tui/Viewport.d.ts.map +1 -0
  112. package/dist/tui/Viewport.js +48 -0
  113. package/dist/tui/Viewport.js.map +1 -0
  114. package/dist/tui/WalletConnectPairing.d.ts +23 -0
  115. package/dist/tui/WalletConnectPairing.d.ts.map +1 -0
  116. package/dist/tui/WalletConnectPairing.js +61 -0
  117. package/dist/tui/WalletConnectPairing.js.map +1 -0
  118. package/dist/tui/components/Button.d.ts +7 -0
  119. package/dist/tui/components/Button.d.ts.map +1 -0
  120. package/dist/tui/components/Button.js +21 -0
  121. package/dist/tui/components/Button.js.map +1 -0
  122. package/dist/tui/components/CeremonyView.d.ts +13 -0
  123. package/dist/tui/components/CeremonyView.d.ts.map +1 -0
  124. package/dist/tui/components/CeremonyView.js +10 -0
  125. package/dist/tui/components/CeremonyView.js.map +1 -0
  126. package/dist/tui/components/CompletionDropdown.d.ts +7 -0
  127. package/dist/tui/components/CompletionDropdown.d.ts.map +1 -0
  128. package/dist/tui/components/CompletionDropdown.js +23 -0
  129. package/dist/tui/components/CompletionDropdown.js.map +1 -0
  130. package/dist/tui/components/ConfirmPrompt.d.ts +9 -0
  131. package/dist/tui/components/ConfirmPrompt.d.ts.map +1 -0
  132. package/dist/tui/components/ConfirmPrompt.js +10 -0
  133. package/dist/tui/components/ConfirmPrompt.js.map +1 -0
  134. package/dist/tui/components/CustomTextInput.d.ts +15 -0
  135. package/dist/tui/components/CustomTextInput.d.ts.map +1 -0
  136. package/dist/tui/components/CustomTextInput.js +99 -0
  137. package/dist/tui/components/CustomTextInput.js.map +1 -0
  138. package/dist/tui/components/InteractiveTable.d.ts +14 -0
  139. package/dist/tui/components/InteractiveTable.d.ts.map +1 -0
  140. package/dist/tui/components/InteractiveTable.js +61 -0
  141. package/dist/tui/components/InteractiveTable.js.map +1 -0
  142. package/dist/tui/components/StepSpinner.d.ts +11 -0
  143. package/dist/tui/components/StepSpinner.d.ts.map +1 -0
  144. package/dist/tui/components/StepSpinner.js +32 -0
  145. package/dist/tui/components/StepSpinner.js.map +1 -0
  146. package/dist/tui/components/Toast.d.ts +18 -0
  147. package/dist/tui/components/Toast.d.ts.map +1 -0
  148. package/dist/tui/components/Toast.js +29 -0
  149. package/dist/tui/components/Toast.js.map +1 -0
  150. package/dist/tui/index.d.ts +2 -0
  151. package/dist/tui/index.d.ts.map +1 -0
  152. package/dist/tui/index.js +55 -0
  153. package/dist/tui/index.js.map +1 -0
  154. package/dist/tui/useChat.d.ts +11 -0
  155. package/dist/tui/useChat.d.ts.map +1 -0
  156. package/dist/tui/useChat.js +91 -0
  157. package/dist/tui/useChat.js.map +1 -0
  158. package/dist/tui/useCommand.d.ts +12 -0
  159. package/dist/tui/useCommand.d.ts.map +1 -0
  160. package/dist/tui/useCommand.js +137 -0
  161. package/dist/tui/useCommand.js.map +1 -0
  162. package/dist/tui/useNotifications.d.ts +9 -0
  163. package/dist/tui/useNotifications.d.ts.map +1 -0
  164. package/dist/tui/useNotifications.js +17 -0
  165. package/dist/tui/useNotifications.js.map +1 -0
  166. package/dist/tui/useScroll.d.ts +17 -0
  167. package/dist/tui/useScroll.d.ts.map +1 -0
  168. package/dist/tui/useScroll.js +46 -0
  169. package/dist/tui/useScroll.js.map +1 -0
  170. package/dist/ui/format.d.ts.map +1 -1
  171. package/dist/ui/format.js +2 -0
  172. package/dist/ui/format.js.map +1 -1
  173. package/dist/ui/qr-render.d.ts +25 -0
  174. package/dist/ui/qr-render.d.ts.map +1 -0
  175. package/dist/ui/qr-render.js +90 -0
  176. package/dist/ui/qr-render.js.map +1 -0
  177. package/dist/ui/rpc-fallback.d.ts +11 -0
  178. package/dist/ui/rpc-fallback.d.ts.map +1 -0
  179. package/dist/ui/rpc-fallback.js +58 -0
  180. package/dist/ui/rpc-fallback.js.map +1 -0
  181. package/dist/walletconnect.d.ts +4 -0
  182. package/dist/walletconnect.d.ts.map +1 -1
  183. package/dist/walletconnect.js.map +1 -1
  184. package/package.json +11 -3
  185. package/scripts/authorize-machine-key.ts +0 -43
  186. package/scripts/drain-wallet.ts +0 -149
  187. package/scripts/execute-spend-only.ts +0 -81
  188. package/scripts/register-agent-userop.ts +0 -186
  189. package/src/abis.ts +0 -187
  190. package/src/bundler.ts +0 -235
  191. package/src/client.ts +0 -36
  192. package/src/coinbase-smart-wallet.ts +0 -51
  193. package/src/commands/accept.ts +0 -64
  194. package/src/commands/agent-handshake.ts +0 -72
  195. package/src/commands/agent.ts +0 -691
  196. package/src/commands/agreements.ts +0 -350
  197. package/src/commands/arbitrator.ts +0 -180
  198. package/src/commands/arena-handshake.ts +0 -257
  199. package/src/commands/arena.ts +0 -122
  200. package/src/commands/cancel.ts +0 -35
  201. package/src/commands/channel.ts +0 -218
  202. package/src/commands/coldstart.ts +0 -165
  203. package/src/commands/config.ts +0 -58
  204. package/src/commands/contract-interaction.ts +0 -166
  205. package/src/commands/daemon.ts +0 -978
  206. package/src/commands/deliver.ts +0 -148
  207. package/src/commands/discover.ts +0 -297
  208. package/src/commands/dispute.ts +0 -375
  209. package/src/commands/endpoint.ts +0 -620
  210. package/src/commands/feed.ts +0 -229
  211. package/src/commands/hire.ts +0 -245
  212. package/src/commands/migrate.ts +0 -177
  213. package/src/commands/negotiate.ts +0 -271
  214. package/src/commands/openshell.ts +0 -1055
  215. package/src/commands/owner.ts +0 -35
  216. package/src/commands/policy.ts +0 -263
  217. package/src/commands/relay.ts +0 -273
  218. package/src/commands/remediate.ts +0 -24
  219. package/src/commands/reputation.ts +0 -79
  220. package/src/commands/setup.ts +0 -343
  221. package/src/commands/trust.ts +0 -27
  222. package/src/commands/verify.ts +0 -91
  223. package/src/commands/wallet.ts +0 -3280
  224. package/src/commands/watch.ts +0 -23
  225. package/src/commands/watchtower.ts +0 -248
  226. package/src/commands/workroom.ts +0 -959
  227. package/src/config.ts +0 -174
  228. package/src/daemon/config.ts +0 -308
  229. package/src/daemon/hire-listener.ts +0 -226
  230. package/src/daemon/index.ts +0 -955
  231. package/src/daemon/job-lifecycle.ts +0 -215
  232. package/src/daemon/notify.ts +0 -157
  233. package/src/daemon/token-metering.ts +0 -183
  234. package/src/daemon/userops.ts +0 -119
  235. package/src/daemon/wallet-monitor.ts +0 -90
  236. package/src/drain-v4.ts +0 -159
  237. package/src/endpoint-config.ts +0 -83
  238. package/src/endpoint-notify.ts +0 -46
  239. package/src/index.ts +0 -26
  240. package/src/openshell-runtime.ts +0 -277
  241. package/src/program.ts +0 -83
  242. package/src/repl.ts +0 -680
  243. package/src/signing.ts +0 -28
  244. package/src/telegram-notify.ts +0 -88
  245. package/src/ui/banner.ts +0 -51
  246. package/src/ui/colors.ts +0 -30
  247. package/src/ui/format.ts +0 -77
  248. package/src/ui/spinner.ts +0 -56
  249. package/src/ui/tree.ts +0 -16
  250. package/src/utils/format.ts +0 -48
  251. package/src/utils/hash.ts +0 -5
  252. package/src/utils/time.ts +0 -15
  253. package/src/wallet-router.ts +0 -178
  254. package/src/walletconnect-session.ts +0 -27
  255. package/src/walletconnect.ts +0 -294
  256. package/test/time.test.js +0 -11
  257. package/tsconfig.json +0 -19
@@ -1,955 +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 path from "path";
12
- import * as net from "net";
13
- import * as http from "http";
14
- import { ethers } from "ethers";
15
- import Database from "better-sqlite3";
16
-
17
- import {
18
- loadDaemonConfig,
19
- loadMachineKey,
20
- DAEMON_DIR,
21
- DAEMON_PID,
22
- DAEMON_LOG,
23
- DAEMON_DB,
24
- DAEMON_SOCK,
25
- type DaemonConfig,
26
- } from "./config";
27
- import { verifyWallet, getWalletBalance } from "./wallet-monitor";
28
- import { Notifier } from "./notify";
29
- import { HireListener } from "./hire-listener";
30
- import { UserOpsManager, buildAcceptCalldata } from "./userops";
31
- import { generateReceipt, extractLearnings, createJobDirectory, cleanJobDirectory } from "./job-lifecycle";
32
-
33
- // ─── State DB ─────────────────────────────────────────────────────────────────
34
-
35
- export interface HireRequestRow {
36
- id: string;
37
- agreement_id: string | null;
38
- hirer_address: string;
39
- capability: string;
40
- price_eth: string;
41
- deadline_unix: number;
42
- spec_hash: string;
43
- status: string; // pending_approval | accepted | rejected | delivered | complete
44
- created_at: number;
45
- updated_at: number;
46
- reject_reason: string | null;
47
- }
48
-
49
- export interface DaemonDB {
50
- insertHireRequest(row: Omit<HireRequestRow, "created_at" | "updated_at">): void;
51
- getHireRequest(id: string): HireRequestRow | undefined;
52
- updateHireRequestStatus(id: string, status: string, rejectReason?: string): void;
53
- listPendingHireRequests(): HireRequestRow[];
54
- listActiveHireRequests(): HireRequestRow[];
55
- countActiveHireRequests(): number;
56
- close(): void;
57
- }
58
-
59
- function openStateDB(dbPath: string): DaemonDB {
60
- const db = new Database(dbPath);
61
- db.pragma("journal_mode = WAL");
62
-
63
- db.exec(`
64
- CREATE TABLE IF NOT EXISTS hire_requests (
65
- id TEXT PRIMARY KEY,
66
- agreement_id TEXT,
67
- hirer_address TEXT NOT NULL,
68
- capability TEXT,
69
- price_eth TEXT,
70
- deadline_unix INTEGER,
71
- spec_hash TEXT,
72
- status TEXT NOT NULL,
73
- created_at INTEGER,
74
- updated_at INTEGER,
75
- reject_reason TEXT
76
- );
77
-
78
- CREATE TABLE IF NOT EXISTS userop_queue (
79
- id TEXT PRIMARY KEY,
80
- hire_request_id TEXT,
81
- call_data TEXT NOT NULL,
82
- user_op_hash TEXT,
83
- status TEXT NOT NULL,
84
- submitted_at INTEGER,
85
- included_at INTEGER,
86
- retry_count INTEGER DEFAULT 0,
87
- last_error TEXT
88
- );
89
-
90
- CREATE TABLE IF NOT EXISTS session_channels (
91
- channel_id TEXT PRIMARY KEY,
92
- counterparty TEXT NOT NULL,
93
- token_address TEXT,
94
- latest_state_seq INTEGER,
95
- latest_state_bytes BLOB,
96
- status TEXT NOT NULL,
97
- challenge_deadline_unix INTEGER,
98
- external_watcher_id TEXT
99
- );
100
-
101
- CREATE TABLE IF NOT EXISTS notifications (
102
- id INTEGER PRIMARY KEY AUTOINCREMENT,
103
- type TEXT NOT NULL,
104
- payload TEXT,
105
- sent_at INTEGER,
106
- status TEXT
107
- );
108
- `);
109
-
110
- const insertHireRequest = db.prepare(`
111
- INSERT OR IGNORE INTO hire_requests
112
- (id, agreement_id, hirer_address, capability, price_eth, deadline_unix, spec_hash, status, created_at, updated_at, reject_reason)
113
- VALUES
114
- (@id, @agreement_id, @hirer_address, @capability, @price_eth, @deadline_unix, @spec_hash, @status, @created_at, @updated_at, @reject_reason)
115
- `);
116
-
117
- const getHireRequest = db.prepare(`SELECT * FROM hire_requests WHERE id = ?`);
118
- const updateStatus = db.prepare(`UPDATE hire_requests SET status = ?, reject_reason = ?, updated_at = ? WHERE id = ?`);
119
- const listPending = db.prepare(`SELECT * FROM hire_requests WHERE status = 'pending_approval' ORDER BY created_at ASC`);
120
- const listActive = db.prepare(`SELECT * FROM hire_requests WHERE status IN ('accepted', 'delivered') ORDER BY created_at ASC`);
121
- const countActive = db.prepare(`SELECT COUNT(*) as n FROM hire_requests WHERE status IN ('accepted', 'delivered')`);
122
-
123
- return {
124
- insertHireRequest(row) {
125
- const now = Date.now();
126
- insertHireRequest.run({ ...row, created_at: now, updated_at: now });
127
- },
128
- getHireRequest(id) {
129
- return getHireRequest.get(id) as HireRequestRow | undefined;
130
- },
131
- updateHireRequestStatus(id, status, rejectReason) {
132
- updateStatus.run(status, rejectReason ?? null, Date.now(), id);
133
- },
134
- listPendingHireRequests() {
135
- return listPending.all() as HireRequestRow[];
136
- },
137
- listActiveHireRequests() {
138
- return listActive.all() as HireRequestRow[];
139
- },
140
- countActiveHireRequests() {
141
- const row = countActive.get() as { n: number };
142
- return row.n;
143
- },
144
- close() {
145
- db.close();
146
- },
147
- };
148
- }
149
-
150
- // ─── Logger ───────────────────────────────────────────────────────────────────
151
-
152
- function openLogger(logPath: string, foreground: boolean): (entry: Record<string, unknown>) => void {
153
- let stream: fs.WriteStream | null = null;
154
- if (!foreground) {
155
- fs.mkdirSync(path.dirname(logPath), { recursive: true });
156
- stream = fs.createWriteStream(logPath, { flags: "a" });
157
- }
158
-
159
- return (entry: Record<string, unknown>) => {
160
- const line = JSON.stringify({ ...entry, ts: new Date().toISOString() });
161
- if (foreground) {
162
- process.stdout.write(line + "\n");
163
- } else if (stream) {
164
- stream.write(line + "\n");
165
- }
166
- };
167
- }
168
-
169
- // ─── IPC Socket ───────────────────────────────────────────────────────────────
170
-
171
- interface IpcContext {
172
- db: DaemonDB;
173
- config: DaemonConfig;
174
- startTime: number;
175
- walletAddress: string;
176
- machineKeyAddress: string;
177
- hireListener: HireListener | null;
178
- userOps: UserOpsManager | null;
179
- activeAgreements: number;
180
- bundlerMode: string;
181
- bundlerEndpoint: string;
182
- }
183
-
184
- function startIpcServer(ctx: IpcContext, log: ReturnType<typeof openLogger>): net.Server {
185
- // Remove stale socket
186
- if (fs.existsSync(DAEMON_SOCK)) {
187
- fs.unlinkSync(DAEMON_SOCK);
188
- }
189
-
190
- const server = net.createServer((socket) => {
191
- let buf = "";
192
- socket.on("data", (data) => {
193
- buf += data.toString();
194
- const lines = buf.split("\n");
195
- buf = lines.pop() ?? "";
196
- for (const line of lines) {
197
- if (!line.trim()) continue;
198
- let cmd: { command: string; id?: string; reason?: string };
199
- try {
200
- cmd = JSON.parse(line) as { command: string; id?: string; reason?: string };
201
- } catch {
202
- socket.write(JSON.stringify({ ok: false, error: "invalid_json" }) + "\n");
203
- continue;
204
- }
205
-
206
- const response = handleIpcCommand(cmd, ctx, log);
207
- socket.write(JSON.stringify(response) + "\n");
208
- }
209
- });
210
- socket.on("error", () => { /* client disconnected */ });
211
- });
212
-
213
- server.listen(DAEMON_SOCK, () => {
214
- fs.chmodSync(DAEMON_SOCK, 0o600);
215
- log({ event: "ipc_ready", socket: DAEMON_SOCK });
216
- });
217
-
218
- return server;
219
- }
220
-
221
- function handleIpcCommand(
222
- cmd: { command: string; id?: string; reason?: string },
223
- ctx: IpcContext,
224
- log: ReturnType<typeof openLogger>
225
- ): { ok: boolean; data?: unknown; error?: string } {
226
- const uptimeSeconds = Math.floor((Date.now() - ctx.startTime) / 1000);
227
- const uptimeStr = formatUptime(uptimeSeconds);
228
-
229
- switch (cmd.command) {
230
- case "status": {
231
- const pending = ctx.db.listPendingHireRequests();
232
- const active = ctx.db.listActiveHireRequests();
233
- return {
234
- ok: true,
235
- data: {
236
- state: "running",
237
- pid: process.pid,
238
- uptime: uptimeStr,
239
- wallet: ctx.walletAddress,
240
- machine_key_address: ctx.machineKeyAddress,
241
- relay_enabled: ctx.config.relay.enabled,
242
- relay_url: ctx.config.relay.relay_url,
243
- relay_poll_seconds: ctx.config.relay.poll_interval_seconds,
244
- watchtower_enabled: ctx.config.watchtower.enabled,
245
- bundler_mode: ctx.bundlerMode,
246
- bundler_endpoint: ctx.bundlerEndpoint,
247
- active_agreements: active.length,
248
- pending_approval: pending.length,
249
- },
250
- };
251
- }
252
-
253
- case "pending": {
254
- return { ok: true, data: { requests: ctx.db.listPendingHireRequests() } };
255
- }
256
-
257
- case "agreements": {
258
- return { ok: true, data: { agreements: ctx.db.listActiveHireRequests() } };
259
- }
260
-
261
- case "agreement": {
262
- if (!cmd.id) return { ok: false, error: "id required" };
263
- const agreement = ctx.db.getHireRequest(cmd.id);
264
- if (!agreement) return { ok: false, error: "agreement not found" };
265
- return { ok: true, data: { agreement } };
266
- }
267
-
268
- case "approve": {
269
- if (!cmd.id) return { ok: false, error: "id required" };
270
- const hire = ctx.db.getHireRequest(cmd.id);
271
- if (!hire) return { ok: false, error: "hire request not found" };
272
- if (hire.status !== "pending_approval") {
273
- return { ok: false, error: `hire request status is '${hire.status}', not pending_approval` };
274
- }
275
- ctx.db.updateHireRequestStatus(cmd.id, "accepted");
276
- log({ event: "hire_approved", id: cmd.id });
277
-
278
- // Trigger accept UserOp (fire and forget)
279
- if (ctx.userOps && ctx.config.serviceAgreementAddress) {
280
- const callData = buildAcceptCalldata(
281
- ctx.config.serviceAgreementAddress,
282
- hire.agreement_id ?? cmd.id,
283
- ctx.walletAddress
284
- );
285
- ctx.userOps.submit(callData, ctx.walletAddress).then((hash) => {
286
- log({ event: "userop_submitted", id: cmd.id, hash });
287
- }).catch((err: unknown) => {
288
- log({ event: "userop_error", id: cmd.id, error: String(err) });
289
- });
290
- }
291
-
292
- return { ok: true, data: { approved: true, id: cmd.id } };
293
- }
294
-
295
- case "reject": {
296
- if (!cmd.id) return { ok: false, error: "id required" };
297
- const hire = ctx.db.getHireRequest(cmd.id);
298
- if (!hire) return { ok: false, error: "hire request not found" };
299
- if (hire.status !== "pending_approval") {
300
- return { ok: false, error: `hire request status is '${hire.status}', not pending_approval` };
301
- }
302
- const reason = cmd.reason ?? "operator_rejected";
303
- ctx.db.updateHireRequestStatus(cmd.id, "rejected", reason);
304
- log({ event: "hire_rejected", id: cmd.id, reason });
305
- return { ok: true, data: { rejected: true, id: cmd.id, reason } };
306
- }
307
-
308
- case "complete": {
309
- // Called after a job is delivered and accepted. Triggers post-job lifecycle:
310
- // receipt generation, learning extraction, worker memory update.
311
- if (!cmd.id) return { ok: false, error: "id required" };
312
- const hire = ctx.db.getHireRequest(cmd.id);
313
- if (!hire) return { ok: false, error: "hire request not found" };
314
-
315
- const now = new Date().toISOString();
316
- const startedAt = new Date(hire.created_at).toISOString();
317
-
318
- // Generate execution receipt
319
- const receipt = generateReceipt({
320
- agreementId: hire.agreement_id ?? cmd.id,
321
- deliverableHash: hire.spec_hash ?? "0x0",
322
- walletAddress: ctx.walletAddress,
323
- startedAt,
324
- completedAt: now,
325
- });
326
- log({ event: "receipt_generated", id: cmd.id, receipt_hash: receipt.receipt_hash });
327
-
328
- // Extract learnings
329
- extractLearnings({
330
- agreementId: hire.agreement_id ?? cmd.id,
331
- taskDescription: hire.capability ?? "unknown",
332
- deliverableHash: hire.spec_hash ?? "0x0",
333
- priceEth: hire.price_eth ?? "0",
334
- capability: hire.capability ?? "general",
335
- wallClockSeconds: receipt.metrics.wall_clock_seconds,
336
- success: true,
337
- });
338
- log({ event: "learnings_extracted", id: cmd.id });
339
-
340
- // Update status to complete
341
- ctx.db.updateHireRequestStatus(cmd.id, "complete");
342
-
343
- // Clean job directory (keep receipt + memory)
344
- cleanJobDirectory(hire.agreement_id ?? cmd.id);
345
-
346
- return {
347
- ok: true,
348
- data: {
349
- completed: true,
350
- id: cmd.id,
351
- receipt_hash: receipt.receipt_hash,
352
- },
353
- };
354
- }
355
-
356
- default:
357
- return { ok: false, error: `unknown command: ${cmd.command}` };
358
- }
359
- }
360
-
361
- function formatUptime(seconds: number): string {
362
- const h = Math.floor(seconds / 3600);
363
- const m = Math.floor((seconds % 3600) / 60);
364
- const s = seconds % 60;
365
- if (h > 0) return `${h}h ${m}m`;
366
- if (m > 0) return `${m}m ${s}s`;
367
- return `${s}s`;
368
- }
369
-
370
- // ─── Daemon main ──────────────────────────────────────────────────────────────
371
-
372
- // Extend config with serviceAgreementAddress (loaded from CLI config if available)
373
- declare module "./config" {
374
- interface DaemonConfig {
375
- serviceAgreementAddress?: string;
376
- }
377
- }
378
-
379
- export async function runDaemon(foreground = false): Promise<void> {
380
- fs.mkdirSync(DAEMON_DIR, { recursive: true, mode: 0o700 });
381
- const log = openLogger(DAEMON_LOG, foreground);
382
-
383
- log({ event: "daemon_starting" });
384
-
385
- // ── Step 1: Load config ──────────────────────────────────────────────────
386
- let config: DaemonConfig;
387
- try {
388
- config = loadDaemonConfig();
389
- log({ event: "config_loaded", path: require("path").join(require("os").homedir(), ".arc402", "daemon.toml") });
390
- } catch (err) {
391
- process.stderr.write(`Config error: ${err}\n`);
392
- process.exit(1);
393
- }
394
-
395
- // ── Step 2: Load machine key ─────────────────────────────────────────────
396
- let machineKeyAddress: string;
397
- let machinePrivateKey: string;
398
- try {
399
- const mk = loadMachineKey(config);
400
- machinePrivateKey = mk.privateKey;
401
- machineKeyAddress = mk.address;
402
- log({ event: "machine_key_loaded", address: machineKeyAddress });
403
- } catch (err) {
404
- process.stderr.write(`Machine key error: ${err}\n`);
405
- process.exit(1);
406
- }
407
-
408
- // ── Step 3: Connect to RPC ───────────────────────────────────────────────
409
- const provider = new ethers.JsonRpcProvider(config.network.rpc_url);
410
- try {
411
- const chainId = (await provider.getNetwork()).chainId;
412
- if (Number(chainId) !== config.network.chain_id) {
413
- process.stderr.write(
414
- `RPC chain ID ${chainId} does not match config ${config.network.chain_id}\n`
415
- );
416
- process.exit(1);
417
- }
418
- const networkName = config.network.chain_id === 8453 ? "Base Mainnet" : `Chain ${config.network.chain_id}`;
419
- log({ event: "rpc_connected", chain_id: config.network.chain_id, network: networkName });
420
- } catch (err) {
421
- process.stderr.write(`RPC connection failed: ${err}\n`);
422
- process.exit(1);
423
- }
424
-
425
- // ── Step 4+5: Verify wallet ──────────────────────────────────────────────
426
- try {
427
- const walletStatus = await verifyWallet(config, provider, machineKeyAddress);
428
- log({
429
- event: "wallet_verified",
430
- address: walletStatus.contractAddress,
431
- owner: walletStatus.ownerAddress,
432
- balance_eth: walletStatus.ethBalance,
433
- machine_key_authorized: walletStatus.machineKeyAuthorized,
434
- });
435
- } catch (err) {
436
- process.stderr.write(`Wallet verification failed: ${err}\n`);
437
- process.exit(1);
438
- }
439
-
440
- // ── Step 6: Connect bundler ──────────────────────────────────────────────
441
- const userOps = new UserOpsManager(config, provider);
442
- const bundlerOk = await userOps.pingBundler();
443
- if (!bundlerOk) {
444
- log({ event: "bundler_warn", msg: "Bundler endpoint unreachable — will retry on demand" });
445
- }
446
- const bundlerEndpoint = config.bundler.endpoint || "https://api.pimlico.io/v2/base/rpc";
447
- log({ event: "bundler_configured", mode: config.bundler.mode, endpoint: bundlerEndpoint });
448
-
449
- // ── Step 7: Open state DB ────────────────────────────────────────────────
450
- let db: DaemonDB;
451
- try {
452
- db = openStateDB(DAEMON_DB);
453
- log({ event: "state_db_opened", path: DAEMON_DB });
454
- } catch (err) {
455
- process.stderr.write(`State DB error: ${err}\n`);
456
- process.exit(1);
457
- }
458
-
459
- // ── Setup notifier ───────────────────────────────────────────────────────
460
- const notifier = new Notifier(
461
- config.notifications.telegram_bot_token,
462
- config.notifications.telegram_chat_id,
463
- {
464
- hire_request: config.notifications.notify_on_hire_request,
465
- hire_accepted: config.notifications.notify_on_hire_accepted,
466
- hire_rejected: config.notifications.notify_on_hire_rejected,
467
- delivery: config.notifications.notify_on_delivery,
468
- dispute: config.notifications.notify_on_dispute,
469
- channel_challenge: config.notifications.notify_on_channel_challenge,
470
- low_balance: config.notifications.notify_on_low_balance,
471
- }
472
- );
473
-
474
- // ── Step 10: Start relay listener ───────────────────────────────────────
475
- const hireListener = new HireListener(config, db, notifier, config.wallet.contract_address);
476
-
477
- const ipcCtx: IpcContext = {
478
- db,
479
- config,
480
- startTime: Date.now(),
481
- walletAddress: config.wallet.contract_address,
482
- machineKeyAddress,
483
- hireListener,
484
- userOps,
485
- activeAgreements: 0,
486
- bundlerMode: config.bundler.mode,
487
- bundlerEndpoint,
488
- };
489
-
490
- // Wire approve callback — submits UserOp when hire is auto-accepted
491
- hireListener.setApproveCallback(async (hireId) => {
492
- const hire = db.getHireRequest(hireId);
493
- if (!hire || !hire.agreement_id || !config.serviceAgreementAddress) return;
494
- try {
495
- const callData = buildAcceptCalldata(
496
- config.serviceAgreementAddress,
497
- hire.agreement_id,
498
- config.wallet.contract_address
499
- );
500
- const hash = await userOps.submit(callData, config.wallet.contract_address);
501
- log({ event: "hire_auto_accepted", id: hireId, userop_hash: hash });
502
- if (config.notifications.notify_on_hire_accepted) {
503
- await notifier.notifyHireAccepted(hireId, hire.agreement_id);
504
- }
505
- } catch (err) {
506
- log({ event: "accept_userop_error", id: hireId, error: String(err) });
507
- }
508
- });
509
-
510
- // Relay poll interval
511
- let relayInterval: ReturnType<typeof setInterval> | null = null;
512
- if (config.relay.enabled && config.relay.relay_url) {
513
- const pollMs = config.relay.poll_interval_seconds * 1000;
514
- relayInterval = setInterval(() => { void hireListener.poll(); }, pollMs);
515
- void hireListener.poll(); // immediate first poll
516
- log({ event: "relay_started", url: config.relay.relay_url, poll_seconds: config.relay.poll_interval_seconds });
517
- }
518
-
519
- // Hire timeout checker — reject stale pending approvals
520
- const timeoutInterval = setInterval(() => {
521
- const pending = db.listPendingHireRequests();
522
- const now = Math.floor(Date.now() / 1000);
523
- for (const req of pending) {
524
- const minLead = config.policy.min_hire_lead_time_seconds;
525
- if (req.deadline_unix > 0 && req.deadline_unix < now + minLead) {
526
- db.updateHireRequestStatus(req.id, "rejected", "approval_timeout");
527
- log({ event: "hire_timeout_rejected", id: req.id });
528
- }
529
- }
530
- }, 30_000);
531
-
532
- // Balance monitor — every 5 minutes
533
- const balanceInterval = setInterval(async () => {
534
- try {
535
- const balance = await getWalletBalance(config.wallet.contract_address, provider);
536
- const threshold = parseFloat(config.notifications.low_balance_threshold_eth);
537
- if (parseFloat(balance) < threshold) {
538
- log({ event: "low_balance", balance_eth: balance, threshold_eth: config.notifications.low_balance_threshold_eth });
539
- await notifier.notifyLowBalance(balance, config.notifications.low_balance_threshold_eth);
540
- }
541
- } catch { /* non-fatal */ }
542
- }, 5 * 60 * 1000);
543
-
544
- // ── Step 11: Write PID file (if not foreground) ──────────────────────────
545
- if (!foreground) {
546
- fs.writeFileSync(DAEMON_PID, String(process.pid), { mode: 0o600 });
547
- log({ event: "pid_written", pid: process.pid, path: DAEMON_PID });
548
- }
549
-
550
- // ── Start IPC socket ─────────────────────────────────────────────────────
551
- const ipcServer = startIpcServer(ipcCtx, log);
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
-
897
- // ── Step 12: Startup complete ────────────────────────────────────────────
898
- const subsystems = [];
899
- if (config.relay.enabled) subsystems.push("relay");
900
- if (config.watchtower.enabled) subsystems.push("watchtower");
901
- subsystems.push(`bundler(${config.bundler.mode})`);
902
-
903
- log({
904
- event: "daemon_started",
905
- wallet: config.wallet.contract_address,
906
- subsystems,
907
- pid: process.pid,
908
- });
909
-
910
- await notifier.notifyStarted(config.wallet.contract_address, subsystems);
911
-
912
- // ── Graceful shutdown ────────────────────────────────────────────────────
913
- const shutdown = async (signal: string) => {
914
- log({ event: "daemon_stopping", signal });
915
-
916
- // Stop accepting new hire requests
917
- if (relayInterval) clearInterval(relayInterval);
918
- clearInterval(timeoutInterval);
919
- clearInterval(balanceInterval);
920
-
921
- // Close HTTP + IPC
922
- httpServer.close();
923
- ipcServer.close();
924
- if (fs.existsSync(DAEMON_SOCK)) fs.unlinkSync(DAEMON_SOCK);
925
-
926
- await notifier.notifyStopped();
927
- log({ event: "daemon_stopped" });
928
-
929
- // Clean up PID file
930
- if (!foreground && fs.existsSync(DAEMON_PID)) {
931
- fs.unlinkSync(DAEMON_PID);
932
- }
933
-
934
- db.close();
935
- process.exit(0);
936
- };
937
-
938
- process.on("SIGTERM", () => { void shutdown("SIGTERM"); });
939
- process.on("SIGINT", () => { void shutdown("SIGINT"); });
940
-
941
- // Keep process alive
942
- process.stdin.resume();
943
- }
944
-
945
- // ─── Entry point ──────────────────────────────────────────────────────────────
946
- // Run when spawned directly (node dist/daemon/index.js [--foreground])
947
-
948
- if (require.main === module || process.env.ARC402_DAEMON_PROCESS === "1") {
949
- const foreground = process.argv.includes("--foreground") ||
950
- process.env.ARC402_DAEMON_FOREGROUND === "1";
951
- runDaemon(foreground).catch((err) => {
952
- process.stderr.write(`Daemon fatal error: ${err}\n`);
953
- process.exit(1);
954
- });
955
- }