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.
- package/README.md +43 -2
- package/dist/abis.d.ts +1 -0
- package/dist/abis.d.ts.map +1 -1
- package/dist/abis.js +29 -1
- package/dist/abis.js.map +1 -1
- package/dist/commands/backup.d.ts +3 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +106 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/compute.d.ts +14 -0
- package/dist/commands/compute.d.ts.map +1 -0
- package/dist/commands/compute.js +466 -0
- package/dist/commands/compute.js.map +1 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +11 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +67 -0
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/discover.d.ts.map +1 -1
- package/dist/commands/discover.js +60 -15
- package/dist/commands/discover.js.map +1 -1
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +205 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/tunnel.d.ts +3 -0
- package/dist/commands/tunnel.d.ts.map +1 -0
- package/dist/commands/tunnel.js +281 -0
- package/dist/commands/tunnel.js.map +1 -0
- package/dist/commands/wallet.d.ts.map +1 -1
- package/dist/commands/wallet.js +299 -65
- package/dist/commands/wallet.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +146 -9
- package/dist/commands/watch.js.map +1 -1
- package/dist/commands/workroom.d.ts.map +1 -1
- package/dist/commands/workroom.js +112 -6
- package/dist/commands/workroom.js.map +1 -1
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -4
- package/dist/config.js.map +1 -1
- package/dist/daemon/compute-metering.d.ts +61 -0
- package/dist/daemon/compute-metering.d.ts.map +1 -0
- package/dist/daemon/compute-metering.js +299 -0
- package/dist/daemon/compute-metering.js.map +1 -0
- package/dist/daemon/compute-session.d.ts +100 -0
- package/dist/daemon/compute-session.d.ts.map +1 -0
- package/dist/daemon/compute-session.js +231 -0
- package/dist/daemon/compute-session.js.map +1 -0
- package/dist/daemon/config.d.ts +33 -1
- package/dist/daemon/config.d.ts.map +1 -1
- package/dist/daemon/config.js +69 -0
- package/dist/daemon/config.js.map +1 -1
- package/dist/daemon/credentials.d.ts +24 -0
- package/dist/daemon/credentials.d.ts.map +1 -0
- package/dist/daemon/credentials.js +80 -0
- package/dist/daemon/credentials.js.map +1 -0
- package/dist/daemon/delivery-client.d.ts +35 -0
- package/dist/daemon/delivery-client.d.ts.map +1 -0
- package/dist/daemon/delivery-client.js +231 -0
- package/dist/daemon/delivery-client.js.map +1 -0
- package/dist/daemon/file-delivery.d.ts +98 -0
- package/dist/daemon/file-delivery.d.ts.map +1 -0
- package/dist/daemon/file-delivery.js +461 -0
- package/dist/daemon/file-delivery.js.map +1 -0
- package/dist/daemon/index.d.ts +1 -0
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +793 -227
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/notify.d.ts +35 -6
- package/dist/daemon/notify.d.ts.map +1 -1
- package/dist/daemon/notify.js +176 -48
- package/dist/daemon/notify.js.map +1 -1
- package/dist/daemon/worker-executor.d.ts +71 -0
- package/dist/daemon/worker-executor.d.ts.map +1 -0
- package/dist/daemon/worker-executor.js +382 -0
- package/dist/daemon/worker-executor.js.map +1 -0
- package/dist/drain-v4.js +2 -2
- package/dist/drain-v4.js.map +1 -1
- package/dist/endpoint-notify.d.ts +9 -1
- package/dist/endpoint-notify.d.ts.map +1 -1
- package/dist/endpoint-notify.js +116 -3
- package/dist/endpoint-notify.js.map +1 -1
- package/dist/index.js +81 -1
- package/dist/index.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +8 -0
- package/dist/program.js.map +1 -1
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +69 -486
- package/dist/repl.js.map +1 -1
- package/dist/tui/App.d.ts +12 -0
- package/dist/tui/App.d.ts.map +1 -0
- package/dist/tui/App.js +154 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/Footer.d.ts +11 -0
- package/dist/tui/Footer.d.ts.map +1 -0
- package/dist/tui/Footer.js +13 -0
- package/dist/tui/Footer.js.map +1 -0
- package/dist/tui/Header.d.ts +14 -0
- package/dist/tui/Header.d.ts.map +1 -0
- package/dist/tui/Header.js +19 -0
- package/dist/tui/Header.js.map +1 -0
- package/dist/tui/InputLine.d.ts +11 -0
- package/dist/tui/InputLine.d.ts.map +1 -0
- package/dist/tui/InputLine.js +145 -0
- package/dist/tui/InputLine.js.map +1 -0
- package/dist/tui/Viewport.d.ts +14 -0
- package/dist/tui/Viewport.d.ts.map +1 -0
- package/dist/tui/Viewport.js +48 -0
- package/dist/tui/Viewport.js.map +1 -0
- package/dist/tui/WalletConnectPairing.d.ts +23 -0
- package/dist/tui/WalletConnectPairing.d.ts.map +1 -0
- package/dist/tui/WalletConnectPairing.js +61 -0
- package/dist/tui/WalletConnectPairing.js.map +1 -0
- package/dist/tui/components/Button.d.ts +7 -0
- package/dist/tui/components/Button.d.ts.map +1 -0
- package/dist/tui/components/Button.js +21 -0
- package/dist/tui/components/Button.js.map +1 -0
- package/dist/tui/components/CeremonyView.d.ts +13 -0
- package/dist/tui/components/CeremonyView.d.ts.map +1 -0
- package/dist/tui/components/CeremonyView.js +10 -0
- package/dist/tui/components/CeremonyView.js.map +1 -0
- package/dist/tui/components/CompletionDropdown.d.ts +7 -0
- package/dist/tui/components/CompletionDropdown.d.ts.map +1 -0
- package/dist/tui/components/CompletionDropdown.js +23 -0
- package/dist/tui/components/CompletionDropdown.js.map +1 -0
- package/dist/tui/components/ConfirmPrompt.d.ts +9 -0
- package/dist/tui/components/ConfirmPrompt.d.ts.map +1 -0
- package/dist/tui/components/ConfirmPrompt.js +10 -0
- package/dist/tui/components/ConfirmPrompt.js.map +1 -0
- package/dist/tui/components/CustomTextInput.d.ts +15 -0
- package/dist/tui/components/CustomTextInput.d.ts.map +1 -0
- package/dist/tui/components/CustomTextInput.js +99 -0
- package/dist/tui/components/CustomTextInput.js.map +1 -0
- package/dist/tui/components/InteractiveTable.d.ts +14 -0
- package/dist/tui/components/InteractiveTable.d.ts.map +1 -0
- package/dist/tui/components/InteractiveTable.js +61 -0
- package/dist/tui/components/InteractiveTable.js.map +1 -0
- package/dist/tui/components/StepSpinner.d.ts +11 -0
- package/dist/tui/components/StepSpinner.d.ts.map +1 -0
- package/dist/tui/components/StepSpinner.js +32 -0
- package/dist/tui/components/StepSpinner.js.map +1 -0
- package/dist/tui/components/Toast.d.ts +18 -0
- package/dist/tui/components/Toast.d.ts.map +1 -0
- package/dist/tui/components/Toast.js +29 -0
- package/dist/tui/components/Toast.js.map +1 -0
- package/dist/tui/index.d.ts +2 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +55 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/useChat.d.ts +11 -0
- package/dist/tui/useChat.d.ts.map +1 -0
- package/dist/tui/useChat.js +91 -0
- package/dist/tui/useChat.js.map +1 -0
- package/dist/tui/useCommand.d.ts +12 -0
- package/dist/tui/useCommand.d.ts.map +1 -0
- package/dist/tui/useCommand.js +137 -0
- package/dist/tui/useCommand.js.map +1 -0
- package/dist/tui/useNotifications.d.ts +9 -0
- package/dist/tui/useNotifications.d.ts.map +1 -0
- package/dist/tui/useNotifications.js +17 -0
- package/dist/tui/useNotifications.js.map +1 -0
- package/dist/tui/useScroll.d.ts +17 -0
- package/dist/tui/useScroll.d.ts.map +1 -0
- package/dist/tui/useScroll.js +46 -0
- package/dist/tui/useScroll.js.map +1 -0
- package/dist/ui/format.d.ts.map +1 -1
- package/dist/ui/format.js +2 -0
- package/dist/ui/format.js.map +1 -1
- package/dist/ui/qr-render.d.ts +25 -0
- package/dist/ui/qr-render.d.ts.map +1 -0
- package/dist/ui/qr-render.js +90 -0
- package/dist/ui/qr-render.js.map +1 -0
- package/dist/ui/rpc-fallback.d.ts +11 -0
- package/dist/ui/rpc-fallback.d.ts.map +1 -0
- package/dist/ui/rpc-fallback.js +58 -0
- package/dist/ui/rpc-fallback.js.map +1 -0
- package/dist/walletconnect.d.ts +4 -0
- package/dist/walletconnect.d.ts.map +1 -1
- package/dist/walletconnect.js.map +1 -1
- package/package.json +11 -3
- package/scripts/authorize-machine-key.ts +0 -43
- package/scripts/drain-wallet.ts +0 -149
- package/scripts/execute-spend-only.ts +0 -81
- package/scripts/register-agent-userop.ts +0 -186
- package/src/abis.ts +0 -187
- package/src/bundler.ts +0 -235
- package/src/client.ts +0 -36
- package/src/coinbase-smart-wallet.ts +0 -51
- package/src/commands/accept.ts +0 -64
- package/src/commands/agent-handshake.ts +0 -72
- package/src/commands/agent.ts +0 -691
- package/src/commands/agreements.ts +0 -350
- package/src/commands/arbitrator.ts +0 -180
- package/src/commands/arena-handshake.ts +0 -257
- package/src/commands/arena.ts +0 -122
- package/src/commands/cancel.ts +0 -35
- package/src/commands/channel.ts +0 -218
- package/src/commands/coldstart.ts +0 -165
- package/src/commands/config.ts +0 -58
- package/src/commands/contract-interaction.ts +0 -166
- package/src/commands/daemon.ts +0 -978
- package/src/commands/deliver.ts +0 -148
- package/src/commands/discover.ts +0 -297
- package/src/commands/dispute.ts +0 -375
- package/src/commands/endpoint.ts +0 -620
- package/src/commands/feed.ts +0 -229
- package/src/commands/hire.ts +0 -245
- package/src/commands/migrate.ts +0 -177
- package/src/commands/negotiate.ts +0 -271
- package/src/commands/openshell.ts +0 -1055
- package/src/commands/owner.ts +0 -35
- package/src/commands/policy.ts +0 -263
- package/src/commands/relay.ts +0 -273
- package/src/commands/remediate.ts +0 -24
- package/src/commands/reputation.ts +0 -79
- package/src/commands/setup.ts +0 -343
- package/src/commands/trust.ts +0 -27
- package/src/commands/verify.ts +0 -91
- package/src/commands/wallet.ts +0 -3280
- package/src/commands/watch.ts +0 -23
- package/src/commands/watchtower.ts +0 -248
- package/src/commands/workroom.ts +0 -959
- package/src/config.ts +0 -174
- package/src/daemon/config.ts +0 -308
- package/src/daemon/hire-listener.ts +0 -226
- package/src/daemon/index.ts +0 -955
- package/src/daemon/job-lifecycle.ts +0 -215
- package/src/daemon/notify.ts +0 -157
- package/src/daemon/token-metering.ts +0 -183
- package/src/daemon/userops.ts +0 -119
- package/src/daemon/wallet-monitor.ts +0 -90
- package/src/drain-v4.ts +0 -159
- package/src/endpoint-config.ts +0 -83
- package/src/endpoint-notify.ts +0 -46
- package/src/index.ts +0 -26
- package/src/openshell-runtime.ts +0 -277
- package/src/program.ts +0 -83
- package/src/repl.ts +0 -680
- package/src/signing.ts +0 -28
- package/src/telegram-notify.ts +0 -88
- package/src/ui/banner.ts +0 -51
- package/src/ui/colors.ts +0 -30
- package/src/ui/format.ts +0 -77
- package/src/ui/spinner.ts +0 -56
- package/src/ui/tree.ts +0 -16
- package/src/utils/format.ts +0 -48
- package/src/utils/hash.ts +0 -5
- package/src/utils/time.ts +0 -15
- package/src/wallet-router.ts +0 -178
- package/src/walletconnect-session.ts +0 -27
- package/src/walletconnect.ts +0 -294
- package/test/time.test.js +0 -11
- package/tsconfig.json +0 -19
package/dist/daemon/index.js
CHANGED
|
@@ -52,12 +52,18 @@ const net = __importStar(require("net"));
|
|
|
52
52
|
const http = __importStar(require("http"));
|
|
53
53
|
const ethers_1 = require("ethers");
|
|
54
54
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
55
|
+
const crypto = __importStar(require("crypto"));
|
|
55
56
|
const config_1 = require("./config");
|
|
57
|
+
const compute_metering_1 = require("./compute-metering");
|
|
58
|
+
const compute_session_1 = require("./compute-session");
|
|
56
59
|
const wallet_monitor_1 = require("./wallet-monitor");
|
|
57
60
|
const notify_1 = require("./notify");
|
|
58
61
|
const hire_listener_1 = require("./hire-listener");
|
|
59
62
|
const userops_1 = require("./userops");
|
|
60
63
|
const job_lifecycle_1 = require("./job-lifecycle");
|
|
64
|
+
const file_delivery_1 = require("./file-delivery");
|
|
65
|
+
const delivery_client_1 = require("./delivery-client");
|
|
66
|
+
const abis_1 = require("../abis");
|
|
61
67
|
function openStateDB(dbPath) {
|
|
62
68
|
const db = new better_sqlite3_1.default(dbPath);
|
|
63
69
|
db.pragma("journal_mode = WAL");
|
|
@@ -114,6 +120,7 @@ function openStateDB(dbPath) {
|
|
|
114
120
|
(@id, @agreement_id, @hirer_address, @capability, @price_eth, @deadline_unix, @spec_hash, @status, @created_at, @updated_at, @reject_reason)
|
|
115
121
|
`);
|
|
116
122
|
const getHireRequest = db.prepare(`SELECT * FROM hire_requests WHERE id = ?`);
|
|
123
|
+
const getHireRequestByAgreementId = db.prepare(`SELECT * FROM hire_requests WHERE agreement_id = ? ORDER BY created_at DESC LIMIT 1`);
|
|
117
124
|
const updateStatus = db.prepare(`UPDATE hire_requests SET status = ?, reject_reason = ?, updated_at = ? WHERE id = ?`);
|
|
118
125
|
const listPending = db.prepare(`SELECT * FROM hire_requests WHERE status = 'pending_approval' ORDER BY created_at ASC`);
|
|
119
126
|
const listActive = db.prepare(`SELECT * FROM hire_requests WHERE status IN ('accepted', 'delivered') ORDER BY created_at ASC`);
|
|
@@ -126,6 +133,9 @@ function openStateDB(dbPath) {
|
|
|
126
133
|
getHireRequest(id) {
|
|
127
134
|
return getHireRequest.get(id);
|
|
128
135
|
},
|
|
136
|
+
getHireRequestByAgreementId(agreementId) {
|
|
137
|
+
return getHireRequestByAgreementId.get(agreementId);
|
|
138
|
+
},
|
|
129
139
|
updateHireRequestStatus(id, status, rejectReason) {
|
|
130
140
|
updateStatus.run(status, rejectReason ?? null, Date.now(), id);
|
|
131
141
|
},
|
|
@@ -144,6 +154,40 @@ function openStateDB(dbPath) {
|
|
|
144
154
|
},
|
|
145
155
|
};
|
|
146
156
|
}
|
|
157
|
+
// ─── Auth token ───────────────────────────────────────────────────────────────
|
|
158
|
+
const DAEMON_TOKEN_FILE = path.join(path.dirname(config_1.DAEMON_SOCK), "daemon.token");
|
|
159
|
+
function generateApiToken() {
|
|
160
|
+
return crypto.randomBytes(32).toString("hex");
|
|
161
|
+
}
|
|
162
|
+
function saveApiToken(token) {
|
|
163
|
+
fs.mkdirSync(path.dirname(DAEMON_TOKEN_FILE), { recursive: true, mode: 0o700 });
|
|
164
|
+
fs.writeFileSync(DAEMON_TOKEN_FILE, token, { mode: 0o600 });
|
|
165
|
+
}
|
|
166
|
+
function loadApiToken() {
|
|
167
|
+
try {
|
|
168
|
+
return fs.readFileSync(DAEMON_TOKEN_FILE, "utf-8").trim();
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const rateLimitMap = new Map();
|
|
175
|
+
const RATE_LIMIT = 30;
|
|
176
|
+
const RATE_WINDOW_MS = 60000;
|
|
177
|
+
function checkRateLimit(ip) {
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
let bucket = rateLimitMap.get(ip);
|
|
180
|
+
if (!bucket || now >= bucket.resetTime) {
|
|
181
|
+
bucket = { count: 0, resetTime: now + RATE_WINDOW_MS };
|
|
182
|
+
rateLimitMap.set(ip, bucket);
|
|
183
|
+
}
|
|
184
|
+
bucket.count++;
|
|
185
|
+
return bucket.count <= RATE_LIMIT;
|
|
186
|
+
}
|
|
187
|
+
// Cleanup stale rate limit entries every 5 minutes to prevent unbounded growth
|
|
188
|
+
let rateLimitCleanupInterval = null;
|
|
189
|
+
// ─── Body size limit ──────────────────────────────────────────────────────────
|
|
190
|
+
const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
|
|
147
191
|
// ─── Logger ───────────────────────────────────────────────────────────────────
|
|
148
192
|
function openLogger(logPath, foreground) {
|
|
149
193
|
let stream = null;
|
|
@@ -161,13 +205,14 @@ function openLogger(logPath, foreground) {
|
|
|
161
205
|
}
|
|
162
206
|
};
|
|
163
207
|
}
|
|
164
|
-
function startIpcServer(ctx, log) {
|
|
208
|
+
function startIpcServer(ctx, log, apiToken) {
|
|
165
209
|
// Remove stale socket
|
|
166
210
|
if (fs.existsSync(config_1.DAEMON_SOCK)) {
|
|
167
211
|
fs.unlinkSync(config_1.DAEMON_SOCK);
|
|
168
212
|
}
|
|
169
213
|
const server = net.createServer((socket) => {
|
|
170
214
|
let buf = "";
|
|
215
|
+
let authenticated = false;
|
|
171
216
|
socket.on("data", (data) => {
|
|
172
217
|
buf += data.toString();
|
|
173
218
|
const lines = buf.split("\n");
|
|
@@ -183,6 +228,19 @@ function startIpcServer(ctx, log) {
|
|
|
183
228
|
socket.write(JSON.stringify({ ok: false, error: "invalid_json" }) + "\n");
|
|
184
229
|
continue;
|
|
185
230
|
}
|
|
231
|
+
// First message must be auth
|
|
232
|
+
if (!authenticated) {
|
|
233
|
+
if (cmd.auth === apiToken) {
|
|
234
|
+
authenticated = true;
|
|
235
|
+
socket.write(JSON.stringify({ ok: true, authenticated: true }) + "\n");
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
log({ event: "ipc_auth_failed" });
|
|
239
|
+
socket.write(JSON.stringify({ ok: false, error: "unauthorized" }) + "\n");
|
|
240
|
+
socket.destroy();
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
186
244
|
const response = handleIpcCommand(cmd, ctx, log);
|
|
187
245
|
socket.write(JSON.stringify(response) + "\n");
|
|
188
246
|
}
|
|
@@ -404,15 +462,34 @@ async function runDaemon(foreground = false) {
|
|
|
404
462
|
process.exit(1);
|
|
405
463
|
}
|
|
406
464
|
// ── Setup notifier ───────────────────────────────────────────────────────
|
|
407
|
-
const notifier =
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
dispute: config.notifications.notify_on_dispute,
|
|
413
|
-
channel_challenge: config.notifications.notify_on_channel_challenge,
|
|
414
|
-
low_balance: config.notifications.notify_on_low_balance,
|
|
465
|
+
const notifier = (0, notify_1.buildNotifier)(config);
|
|
466
|
+
// ── File delivery subsystem ──────────────────────────────────────────────
|
|
467
|
+
const fileDelivery = new file_delivery_1.FileDeliveryManager({
|
|
468
|
+
maxFileSizeMb: config.delivery.max_file_size_mb,
|
|
469
|
+
maxJobSizeMb: config.delivery.max_job_size_mb,
|
|
415
470
|
});
|
|
471
|
+
fileDelivery.setPartyResolver((agreementId) => {
|
|
472
|
+
const row = db.getHireRequestByAgreementId(agreementId);
|
|
473
|
+
if (!row)
|
|
474
|
+
return null;
|
|
475
|
+
return {
|
|
476
|
+
hirerAddress: row.hirer_address,
|
|
477
|
+
providerAddress: config.wallet.contract_address,
|
|
478
|
+
};
|
|
479
|
+
});
|
|
480
|
+
const deliveryClient = new delivery_client_1.DeliveryClient({ autoDownload: config.delivery.auto_download });
|
|
481
|
+
deliveryClient.log = log;
|
|
482
|
+
log({ event: "file_delivery_ready", serve_files: config.delivery.serve_files, auto_download: config.delivery.auto_download });
|
|
483
|
+
// ── Compute rental subsystem ─────────────────────────────────────────────
|
|
484
|
+
let computeMetering = null;
|
|
485
|
+
let computeSessions = null;
|
|
486
|
+
// Machine key signer — used for on-chain compute contract calls
|
|
487
|
+
const machineKeySigner = new ethers_1.ethers.Wallet(machinePrivateKey, provider);
|
|
488
|
+
if (config.compute.enabled) {
|
|
489
|
+
computeMetering = new compute_metering_1.ComputeMetering(machinePrivateKey, config.network.chain_id, config.compute.compute_agreement_address || config.wallet.contract_address, config.compute.metering_interval_seconds, config.compute.report_interval_minutes, config.compute.compute_agreement_address ? provider : undefined);
|
|
490
|
+
computeSessions = new compute_session_1.ComputeSessionManager(computeMetering);
|
|
491
|
+
log({ event: "compute_enabled", gpu_spec: config.compute.gpu_spec, rate_wei: config.compute.rate_per_hour_wei });
|
|
492
|
+
}
|
|
416
493
|
// ── Step 10: Start relay listener ───────────────────────────────────────
|
|
417
494
|
const hireListener = new hire_listener_1.HireListener(config, db, notifier, config.wallet.contract_address);
|
|
418
495
|
const ipcCtx = {
|
|
@@ -464,6 +541,14 @@ async function runDaemon(foreground = false) {
|
|
|
464
541
|
}
|
|
465
542
|
}
|
|
466
543
|
}, 30000);
|
|
544
|
+
// Rate limit map cleanup — every 5 minutes (prevents unbounded growth)
|
|
545
|
+
rateLimitCleanupInterval = setInterval(() => {
|
|
546
|
+
const now = Date.now();
|
|
547
|
+
for (const [ip, bucket] of rateLimitMap) {
|
|
548
|
+
if (bucket.resetTime < now)
|
|
549
|
+
rateLimitMap.delete(ip);
|
|
550
|
+
}
|
|
551
|
+
}, 5 * 60 * 1000);
|
|
467
552
|
// Balance monitor — every 5 minutes
|
|
468
553
|
const balanceInterval = setInterval(async () => {
|
|
469
554
|
try {
|
|
@@ -481,15 +566,76 @@ async function runDaemon(foreground = false) {
|
|
|
481
566
|
fs.writeFileSync(config_1.DAEMON_PID, String(process.pid), { mode: 0o600 });
|
|
482
567
|
log({ event: "pid_written", pid: process.pid, path: config_1.DAEMON_PID });
|
|
483
568
|
}
|
|
569
|
+
// ── Generate and save API token ──────────────────────────────────────────
|
|
570
|
+
const apiToken = generateApiToken();
|
|
571
|
+
saveApiToken(apiToken);
|
|
572
|
+
log({ event: "auth_token_saved", path: DAEMON_TOKEN_FILE });
|
|
484
573
|
// ── Start IPC socket ─────────────────────────────────────────────────────
|
|
485
|
-
const ipcServer = startIpcServer(ipcCtx, log);
|
|
574
|
+
const ipcServer = startIpcServer(ipcCtx, log, apiToken);
|
|
486
575
|
// ── Start HTTP relay server (public endpoint) ────────────────────────────
|
|
487
576
|
const httpPort = config.relay.listen_port ?? 4402;
|
|
577
|
+
/**
|
|
578
|
+
* Optionally verifies X-ARC402-Signature against the request body.
|
|
579
|
+
* Logs the result but never rejects — unsigned requests are accepted for backwards compat.
|
|
580
|
+
*/
|
|
581
|
+
function verifyRequestSignature(body, req) {
|
|
582
|
+
const sig = req.headers["x-arc402-signature"];
|
|
583
|
+
if (!sig)
|
|
584
|
+
return;
|
|
585
|
+
const claimedSigner = req.headers["x-arc402-signer"];
|
|
586
|
+
try {
|
|
587
|
+
const recovered = ethers_1.ethers.verifyMessage(body, sig);
|
|
588
|
+
if (claimedSigner && recovered.toLowerCase() !== claimedSigner.toLowerCase()) {
|
|
589
|
+
log({ event: "sig_mismatch", claimed: claimedSigner, recovered });
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
log({ event: "sig_verified", signer: recovered });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
log({ event: "sig_invalid" });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Read request body with a size cap. Destroys the request and sends 413
|
|
601
|
+
* if the body exceeds MAX_BODY_SIZE. Returns null in that case.
|
|
602
|
+
*/
|
|
603
|
+
function readBody(req, res) {
|
|
604
|
+
return new Promise((resolve) => {
|
|
605
|
+
let body = "";
|
|
606
|
+
let size = 0;
|
|
607
|
+
req.on("data", (chunk) => {
|
|
608
|
+
size += chunk.length;
|
|
609
|
+
if (size > MAX_BODY_SIZE) {
|
|
610
|
+
req.destroy();
|
|
611
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
612
|
+
res.end(JSON.stringify({ error: "payload_too_large" }));
|
|
613
|
+
resolve(null);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
body += chunk.toString();
|
|
617
|
+
});
|
|
618
|
+
req.on("end", () => { resolve(body); });
|
|
619
|
+
req.on("error", () => { resolve(null); });
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
const PUBLIC_GET_PATHS = new Set(["/", "/health", "/agent", "/capabilities", "/status"]);
|
|
623
|
+
// CORS whitelist — localhost for local tooling, arc402.xyz for the web app
|
|
624
|
+
const CORS_WHITELIST = new Set(["localhost", "127.0.0.1", "arc402.xyz", "app.arc402.xyz"]);
|
|
488
625
|
const httpServer = http.createServer(async (req, res) => {
|
|
489
|
-
// CORS
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
626
|
+
// CORS — only reflect origin header if it's in the whitelist
|
|
627
|
+
const origin = (req.headers["origin"] ?? "");
|
|
628
|
+
if (origin) {
|
|
629
|
+
try {
|
|
630
|
+
const { hostname } = new URL(origin);
|
|
631
|
+
if (CORS_WHITELIST.has(hostname)) {
|
|
632
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
633
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
634
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
catch { /* ignore invalid origin */ }
|
|
638
|
+
}
|
|
493
639
|
if (req.method === "OPTIONS") {
|
|
494
640
|
res.writeHead(204);
|
|
495
641
|
res.end();
|
|
@@ -497,6 +643,26 @@ async function runDaemon(foreground = false) {
|
|
|
497
643
|
}
|
|
498
644
|
const url = new URL(req.url || "/", `http://localhost:${httpPort}`);
|
|
499
645
|
const pathname = url.pathname;
|
|
646
|
+
// Rate limiting (all endpoints)
|
|
647
|
+
const clientIp = (req.socket.remoteAddress ?? "unknown").replace(/^::ffff:/, "");
|
|
648
|
+
if (!checkRateLimit(clientIp)) {
|
|
649
|
+
log({ event: "rate_limited", ip: clientIp, path: pathname });
|
|
650
|
+
res.writeHead(429, { "Content-Type": "application/json" });
|
|
651
|
+
res.end(JSON.stringify({ error: "too_many_requests" }));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
// Auth required on all POST endpoints (GET public paths are open).
|
|
655
|
+
// /job/* GET routes use party auth (verifyPartyAccess) instead of daemon bearer token.
|
|
656
|
+
if (req.method === "POST" || (req.method === "GET" && !PUBLIC_GET_PATHS.has(pathname) && !pathname.startsWith("/job/"))) {
|
|
657
|
+
const authHeader = req.headers["authorization"] ?? "";
|
|
658
|
+
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
659
|
+
if (token !== apiToken) {
|
|
660
|
+
log({ event: "http_unauthorized", ip: clientIp, path: pathname });
|
|
661
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
662
|
+
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
500
666
|
// Health / info
|
|
501
667
|
if (pathname === "/" || pathname === "/health") {
|
|
502
668
|
const info = {
|
|
@@ -526,51 +692,36 @@ async function runDaemon(foreground = false) {
|
|
|
526
692
|
}
|
|
527
693
|
// Receive hire proposal
|
|
528
694
|
if (pathname === "/hire" && req.method === "POST") {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
agreement_id: proposal.agreementId ?? null,
|
|
560
|
-
hirer_address: proposal.hirerAddress,
|
|
561
|
-
capability: proposal.capability,
|
|
562
|
-
price_eth: proposal.priceEth,
|
|
563
|
-
deadline_unix: proposal.deadlineUnix,
|
|
564
|
-
spec_hash: proposal.specHash,
|
|
565
|
-
status: "rejected",
|
|
566
|
-
reject_reason: policyResult.reason ?? "policy_violation",
|
|
567
|
-
});
|
|
568
|
-
log({ event: "http_hire_rejected", id: proposal.messageId, reason: policyResult.reason });
|
|
569
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
570
|
-
res.end(JSON.stringify({ status: "rejected", reason: policyResult.reason, id: proposal.messageId }));
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
const status = config.policy.auto_accept ? "accepted" : "pending_approval";
|
|
695
|
+
const body = await readBody(req, res);
|
|
696
|
+
if (body === null)
|
|
697
|
+
return;
|
|
698
|
+
verifyRequestSignature(body, req);
|
|
699
|
+
try {
|
|
700
|
+
const msg = JSON.parse(body);
|
|
701
|
+
// Feed into the hire listener's message handler
|
|
702
|
+
const proposal = {
|
|
703
|
+
messageId: String(msg.messageId ?? msg.id ?? `http_${Date.now()}`),
|
|
704
|
+
hirerAddress: String(msg.hirerAddress ?? msg.hirer_address ?? msg.from ?? ""),
|
|
705
|
+
capability: String(msg.capability ?? ""),
|
|
706
|
+
priceEth: String(msg.priceEth ?? msg.price_eth ?? "0"),
|
|
707
|
+
deadlineUnix: Number(msg.deadlineUnix ?? msg.deadline ?? 0),
|
|
708
|
+
specHash: String(msg.specHash ?? msg.spec_hash ?? ""),
|
|
709
|
+
agreementId: msg.agreementId ? String(msg.agreementId) : undefined,
|
|
710
|
+
signature: msg.signature ? String(msg.signature) : undefined,
|
|
711
|
+
};
|
|
712
|
+
// Dedup
|
|
713
|
+
const existing = db.getHireRequest(proposal.messageId);
|
|
714
|
+
if (existing) {
|
|
715
|
+
log({ event: "hire_duplicate", id: proposal.messageId, status: existing.status });
|
|
716
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
717
|
+
res.end(JSON.stringify({ status: existing.status, id: proposal.messageId }));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
// Policy check
|
|
721
|
+
const { evaluatePolicy } = await Promise.resolve().then(() => __importStar(require("./hire-listener")));
|
|
722
|
+
const activeCount = db.countActiveHireRequests();
|
|
723
|
+
const policyResult = evaluatePolicy(proposal, config, activeCount);
|
|
724
|
+
if (!policyResult.allowed) {
|
|
574
725
|
db.insertHireRequest({
|
|
575
726
|
id: proposal.messageId,
|
|
576
727
|
agreement_id: proposal.agreementId ?? null,
|
|
@@ -579,206 +730,227 @@ async function runDaemon(foreground = false) {
|
|
|
579
730
|
price_eth: proposal.priceEth,
|
|
580
731
|
deadline_unix: proposal.deadlineUnix,
|
|
581
732
|
spec_hash: proposal.specHash,
|
|
582
|
-
status,
|
|
583
|
-
reject_reason:
|
|
733
|
+
status: "rejected",
|
|
734
|
+
reject_reason: policyResult.reason ?? "policy_violation",
|
|
584
735
|
});
|
|
585
|
-
log({ event: "
|
|
586
|
-
if (config.notifications.notify_on_hire_request) {
|
|
587
|
-
await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
|
|
588
|
-
}
|
|
736
|
+
log({ event: "http_hire_rejected", id: proposal.messageId, reason: policyResult.reason });
|
|
589
737
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
590
|
-
res.end(JSON.stringify({ status, id: proposal.messageId }));
|
|
738
|
+
res.end(JSON.stringify({ status: "rejected", reason: policyResult.reason, id: proposal.messageId }));
|
|
739
|
+
return;
|
|
591
740
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
741
|
+
const status = config.policy.auto_accept ? "accepted" : "pending_approval";
|
|
742
|
+
db.insertHireRequest({
|
|
743
|
+
id: proposal.messageId,
|
|
744
|
+
agreement_id: proposal.agreementId ?? null,
|
|
745
|
+
hirer_address: proposal.hirerAddress,
|
|
746
|
+
capability: proposal.capability,
|
|
747
|
+
price_eth: proposal.priceEth,
|
|
748
|
+
deadline_unix: proposal.deadlineUnix,
|
|
749
|
+
spec_hash: proposal.specHash,
|
|
750
|
+
status,
|
|
751
|
+
reject_reason: null,
|
|
752
|
+
});
|
|
753
|
+
log({ event: "http_hire_received", id: proposal.messageId, hirer: proposal.hirerAddress, status });
|
|
754
|
+
if (config.notifications.notify_on_hire_request) {
|
|
755
|
+
await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
|
|
596
756
|
}
|
|
597
|
-
|
|
757
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
758
|
+
res.end(JSON.stringify({ status, id: proposal.messageId }));
|
|
759
|
+
}
|
|
760
|
+
catch (err) {
|
|
761
|
+
log({ event: "http_hire_error", error: String(err) });
|
|
762
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
763
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
764
|
+
}
|
|
598
765
|
return;
|
|
599
766
|
}
|
|
600
767
|
// Handshake acknowledgment endpoint
|
|
601
768
|
if (pathname === "/handshake" && req.method === "POST") {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
}
|
|
769
|
+
const body = await readBody(req, res);
|
|
770
|
+
if (body === null)
|
|
771
|
+
return;
|
|
772
|
+
verifyRequestSignature(body, req);
|
|
773
|
+
try {
|
|
774
|
+
const msg = JSON.parse(body);
|
|
775
|
+
log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
|
|
776
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
777
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
781
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
782
|
+
}
|
|
616
783
|
return;
|
|
617
784
|
}
|
|
618
785
|
// POST /hire/accepted — provider accepted, client notified
|
|
619
786
|
if (pathname === "/hire/accepted" && req.method === "POST") {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
}
|
|
631
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
632
|
-
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
787
|
+
const body = await readBody(req, res);
|
|
788
|
+
if (body === null)
|
|
789
|
+
return;
|
|
790
|
+
try {
|
|
791
|
+
const msg = JSON.parse(body);
|
|
792
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
793
|
+
const from = String(msg.from ?? "");
|
|
794
|
+
log({ event: "hire_accepted_inbound", agreementId, from });
|
|
795
|
+
if (config.notifications.notify_on_hire_accepted) {
|
|
796
|
+
await notifier.notifyHireAccepted(agreementId, agreementId);
|
|
633
797
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
798
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
799
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
803
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
804
|
+
}
|
|
639
805
|
return;
|
|
640
806
|
}
|
|
641
807
|
// POST /message — off-chain negotiation message
|
|
642
808
|
if (pathname === "/message" && req.method === "POST") {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
}
|
|
809
|
+
const body = await readBody(req, res);
|
|
810
|
+
if (body === null)
|
|
811
|
+
return;
|
|
812
|
+
verifyRequestSignature(body, req);
|
|
813
|
+
try {
|
|
814
|
+
const msg = JSON.parse(body);
|
|
815
|
+
const from = String(msg.from ?? "");
|
|
816
|
+
const to = String(msg.to ?? "");
|
|
817
|
+
const content = String(msg.content ?? "");
|
|
818
|
+
const timestamp = Number(msg.timestamp ?? Date.now());
|
|
819
|
+
log({ event: "message_received", from, to, timestamp, content_len: content.length });
|
|
820
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
821
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
825
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
826
|
+
}
|
|
661
827
|
return;
|
|
662
828
|
}
|
|
663
829
|
// POST /delivery — provider committed a deliverable
|
|
664
830
|
if (pathname === "/delivery" && req.method === "POST") {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
831
|
+
const body = await readBody(req, res);
|
|
832
|
+
if (body === null)
|
|
833
|
+
return;
|
|
834
|
+
verifyRequestSignature(body, req);
|
|
835
|
+
try {
|
|
836
|
+
const msg = JSON.parse(body);
|
|
837
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
838
|
+
const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
|
|
839
|
+
const filesUrl = String(msg.files_url ?? msg.filesUrl ?? "");
|
|
840
|
+
const from = String(msg.from ?? "");
|
|
841
|
+
log({ event: "delivery_received", agreementId, deliverableHash, from, has_files_url: !!filesUrl });
|
|
842
|
+
// Update DB: mark delivered
|
|
843
|
+
const active = db.listActiveHireRequests();
|
|
844
|
+
const found = active.find(r => r.agreement_id === agreementId);
|
|
845
|
+
if (found)
|
|
846
|
+
db.updateHireRequestStatus(found.id, "delivered");
|
|
847
|
+
if (config.notifications.notify_on_delivery) {
|
|
848
|
+
await notifier.notifyDelivery(agreementId, deliverableHash, "");
|
|
684
849
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
850
|
+
// Auto-download and verify if files_url provided and auto_download enabled
|
|
851
|
+
if (filesUrl && config.delivery.auto_download) {
|
|
852
|
+
void deliveryClient.handleDeliveryNotification({ agreementId, deliverableHash, filesUrl })
|
|
853
|
+
.then(result => {
|
|
854
|
+
if (result)
|
|
855
|
+
log({ event: "delivery_auto_downloaded", agreementId, ok: result.ok, root_hash_match: result.rootHashMatch, files: result.fileResults.length });
|
|
856
|
+
})
|
|
857
|
+
.catch(err => log({ event: "delivery_download_error", agreementId, error: String(err) }));
|
|
688
858
|
}
|
|
689
|
-
|
|
859
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
860
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
864
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
865
|
+
}
|
|
690
866
|
return;
|
|
691
867
|
}
|
|
692
868
|
// POST /delivery/accepted — client accepted delivery, payment releasing
|
|
693
869
|
if (pathname === "/delivery/accepted" && req.method === "POST") {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
});
|
|
870
|
+
const body = await readBody(req, res);
|
|
871
|
+
if (body === null)
|
|
872
|
+
return;
|
|
873
|
+
try {
|
|
874
|
+
const msg = JSON.parse(body);
|
|
875
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
876
|
+
const from = String(msg.from ?? "");
|
|
877
|
+
log({ event: "delivery_accepted_inbound", agreementId, from });
|
|
878
|
+
// Update DB: mark complete
|
|
879
|
+
const all = db.listActiveHireRequests();
|
|
880
|
+
const found = all.find(r => r.agreement_id === agreementId);
|
|
881
|
+
if (found)
|
|
882
|
+
db.updateHireRequestStatus(found.id, "complete");
|
|
883
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
884
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
885
|
+
}
|
|
886
|
+
catch {
|
|
887
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
888
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
889
|
+
}
|
|
715
890
|
return;
|
|
716
891
|
}
|
|
717
892
|
// POST /dispute — dispute raised against this agent
|
|
718
893
|
if (pathname === "/dispute" && req.method === "POST") {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
732
|
-
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
894
|
+
const body = await readBody(req, res);
|
|
895
|
+
if (body === null)
|
|
896
|
+
return;
|
|
897
|
+
try {
|
|
898
|
+
const msg = JSON.parse(body);
|
|
899
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
900
|
+
const reason = String(msg.reason ?? "");
|
|
901
|
+
const from = String(msg.from ?? "");
|
|
902
|
+
log({ event: "dispute_received", agreementId, reason, from });
|
|
903
|
+
if (config.notifications.notify_on_dispute) {
|
|
904
|
+
await notifier.notifyDispute(agreementId, from);
|
|
733
905
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
906
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
907
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
911
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
912
|
+
}
|
|
739
913
|
return;
|
|
740
914
|
}
|
|
741
915
|
// POST /dispute/resolved — dispute resolved by arbitrator
|
|
742
916
|
if (pathname === "/dispute/resolved" && req.method === "POST") {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
});
|
|
917
|
+
const body = await readBody(req, res);
|
|
918
|
+
if (body === null)
|
|
919
|
+
return;
|
|
920
|
+
try {
|
|
921
|
+
const msg = JSON.parse(body);
|
|
922
|
+
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
923
|
+
const outcome = String(msg.outcome ?? "");
|
|
924
|
+
const from = String(msg.from ?? "");
|
|
925
|
+
log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
|
|
926
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
927
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
928
|
+
}
|
|
929
|
+
catch {
|
|
930
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
931
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
932
|
+
}
|
|
760
933
|
return;
|
|
761
934
|
}
|
|
762
935
|
// POST /workroom/status — workroom lifecycle events
|
|
763
936
|
if (pathname === "/workroom/status" && req.method === "POST") {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
});
|
|
937
|
+
const body = await readBody(req, res);
|
|
938
|
+
if (body === null)
|
|
939
|
+
return;
|
|
940
|
+
try {
|
|
941
|
+
const msg = JSON.parse(body);
|
|
942
|
+
const event = String(msg.event ?? "");
|
|
943
|
+
const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
|
|
944
|
+
const jobId = msg.jobId ? String(msg.jobId) : undefined;
|
|
945
|
+
const timestamp = Number(msg.timestamp ?? Date.now());
|
|
946
|
+
log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
|
|
947
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
948
|
+
res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
952
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
953
|
+
}
|
|
782
954
|
return;
|
|
783
955
|
}
|
|
784
956
|
// GET /capabilities — agent capabilities from config
|
|
@@ -792,23 +964,415 @@ async function runDaemon(foreground = false) {
|
|
|
792
964
|
}));
|
|
793
965
|
return;
|
|
794
966
|
}
|
|
795
|
-
// GET /status — health with active agreement count
|
|
967
|
+
// GET /status — health with active agreement count (sensitive counts only for authenticated)
|
|
796
968
|
if (pathname === "/status" && req.method === "GET") {
|
|
797
|
-
const
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
969
|
+
const statusAuth = (req.headers["authorization"] ?? "");
|
|
970
|
+
const statusToken = statusAuth.startsWith("Bearer ") ? statusAuth.slice(7) : "";
|
|
971
|
+
const statusAuthed = statusToken === apiToken;
|
|
972
|
+
const statusPayload = {
|
|
801
973
|
protocol: "arc-402",
|
|
802
974
|
version: "0.3.0",
|
|
803
975
|
agent: config.wallet.contract_address,
|
|
804
976
|
status: "online",
|
|
805
977
|
uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
|
|
806
|
-
active_agreements: activeList.length,
|
|
807
|
-
pending_approval: pendingList.length,
|
|
808
978
|
capabilities: config.policy.allowed_capabilities,
|
|
809
|
-
}
|
|
979
|
+
};
|
|
980
|
+
if (statusAuthed) {
|
|
981
|
+
statusPayload.active_agreements = db.listActiveHireRequests().length;
|
|
982
|
+
statusPayload.pending_approval = db.listPendingHireRequests().length;
|
|
983
|
+
if (config.delivery.serve_files) {
|
|
984
|
+
const deliveriesDir = path.join(config_1.DAEMON_DIR, "deliveries");
|
|
985
|
+
let totalDeliveries = 0, totalFiles = 0;
|
|
986
|
+
if (fs.existsSync(deliveriesDir)) {
|
|
987
|
+
const entries = fs.readdirSync(deliveriesDir);
|
|
988
|
+
totalDeliveries = entries.length;
|
|
989
|
+
for (const entry of entries) {
|
|
990
|
+
const entryPath = path.join(deliveriesDir, entry);
|
|
991
|
+
if (fs.statSync(entryPath).isDirectory()) {
|
|
992
|
+
totalFiles += fs.readdirSync(entryPath).filter(f => f !== "_manifest.json").length;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
statusPayload.file_delivery = { total_deliveries: totalDeliveries, total_files: totalFiles };
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1000
|
+
res.end(JSON.stringify(statusPayload));
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
// ── Compute rental routes ─────────────────────────────────────────────
|
|
1004
|
+
// POST /compute/propose — client proposes a compute session
|
|
1005
|
+
if (pathname === "/compute/propose" && req.method === "POST") {
|
|
1006
|
+
const body = await readBody(req, res);
|
|
1007
|
+
if (body === null)
|
|
1008
|
+
return;
|
|
1009
|
+
try {
|
|
1010
|
+
const msg = JSON.parse(body);
|
|
1011
|
+
if (!computeSessions) {
|
|
1012
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1013
|
+
res.end(JSON.stringify({ error: "compute_disabled" }));
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
const proposal = {
|
|
1017
|
+
sessionId: String(msg.sessionId ?? ""),
|
|
1018
|
+
clientAddress: String(msg.clientAddress ?? msg.client ?? ""),
|
|
1019
|
+
providerAddress: config.wallet.contract_address,
|
|
1020
|
+
ratePerHourWei: config.compute.rate_per_hour_wei,
|
|
1021
|
+
maxHours: Number(msg.maxHours ?? 1),
|
|
1022
|
+
gpuSpecHash: String(msg.gpuSpecHash ?? "0x0000000000000000000000000000000000000000000000000000000000000000"),
|
|
1023
|
+
workloadDescription: String(msg.workloadDescription ?? ""),
|
|
1024
|
+
depositAmount: String(msg.depositAmount ?? "0"),
|
|
1025
|
+
proposedAt: Math.floor(Date.now() / 1000),
|
|
1026
|
+
};
|
|
1027
|
+
if (!proposal.sessionId) {
|
|
1028
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1029
|
+
res.end(JSON.stringify({ error: "sessionId required" }));
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
// Check capacity
|
|
1033
|
+
const activeSessions = computeSessions.countByStatus("active");
|
|
1034
|
+
if (activeSessions >= config.compute.max_concurrent_sessions) {
|
|
1035
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1036
|
+
res.end(JSON.stringify({ status: "rejected", reason: "at_capacity" }));
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const result = computeSessions.handleProposal(proposal);
|
|
1040
|
+
if (!result.ok) {
|
|
1041
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1042
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
log({ event: "compute_proposed", sessionId: proposal.sessionId, client: proposal.clientAddress });
|
|
1046
|
+
// Auto-accept if configured
|
|
1047
|
+
if (config.compute.auto_accept_compute) {
|
|
1048
|
+
computeSessions.acceptSession(proposal.sessionId);
|
|
1049
|
+
log({ event: "compute_auto_accepted", sessionId: proposal.sessionId });
|
|
1050
|
+
// Wire to on-chain: provider calls acceptSession
|
|
1051
|
+
if (config.compute.compute_agreement_address) {
|
|
1052
|
+
try {
|
|
1053
|
+
const caContract = new ethers_1.ethers.Contract(config.compute.compute_agreement_address, abis_1.COMPUTE_AGREEMENT_ABI, machineKeySigner);
|
|
1054
|
+
const tx = await caContract.acceptSession(proposal.sessionId);
|
|
1055
|
+
await tx.wait();
|
|
1056
|
+
log({ event: "compute_accept_onchain", sessionId: proposal.sessionId, txHash: tx.hash });
|
|
1057
|
+
}
|
|
1058
|
+
catch (onchainErr) {
|
|
1059
|
+
log({ event: "compute_accept_onchain_error", sessionId: proposal.sessionId, error: String(onchainErr) });
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const status = config.compute.auto_accept_compute ? "accepted" : "proposed";
|
|
1064
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1065
|
+
res.end(JSON.stringify({ status, sessionId: proposal.sessionId }));
|
|
1066
|
+
}
|
|
1067
|
+
catch (err) {
|
|
1068
|
+
log({ event: "compute_propose_error", error: String(err) });
|
|
1069
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1070
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
1071
|
+
}
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
// POST /compute/accept — provider accepts a proposed session
|
|
1075
|
+
if (pathname === "/compute/accept" && req.method === "POST") {
|
|
1076
|
+
const body = await readBody(req, res);
|
|
1077
|
+
if (body === null)
|
|
1078
|
+
return;
|
|
1079
|
+
try {
|
|
1080
|
+
const msg = JSON.parse(body);
|
|
1081
|
+
const sessionId = String(msg.sessionId ?? "");
|
|
1082
|
+
if (!computeSessions) {
|
|
1083
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1084
|
+
res.end(JSON.stringify({ error: "compute_disabled" }));
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
const result = computeSessions.acceptSession(sessionId);
|
|
1088
|
+
if (!result.ok) {
|
|
1089
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1090
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
log({ event: "compute_accepted", sessionId });
|
|
1094
|
+
// Wire to on-chain: provider calls acceptSession
|
|
1095
|
+
if (config.compute.compute_agreement_address) {
|
|
1096
|
+
try {
|
|
1097
|
+
const caContract = new ethers_1.ethers.Contract(config.compute.compute_agreement_address, abis_1.COMPUTE_AGREEMENT_ABI, machineKeySigner);
|
|
1098
|
+
const tx = await caContract.acceptSession(sessionId);
|
|
1099
|
+
await tx.wait();
|
|
1100
|
+
log({ event: "compute_accept_onchain", sessionId, txHash: tx.hash });
|
|
1101
|
+
}
|
|
1102
|
+
catch (onchainErr) {
|
|
1103
|
+
log({ event: "compute_accept_onchain_error", sessionId, error: String(onchainErr) });
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1107
|
+
res.end(JSON.stringify({ status: "accepted", sessionId }));
|
|
1108
|
+
}
|
|
1109
|
+
catch (err) {
|
|
1110
|
+
log({ event: "compute_accept_error", error: String(err) });
|
|
1111
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1112
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
1113
|
+
}
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
// POST /compute/started — provider marks session as started
|
|
1117
|
+
if (pathname === "/compute/started" && req.method === "POST") {
|
|
1118
|
+
const body = await readBody(req, res);
|
|
1119
|
+
if (body === null)
|
|
1120
|
+
return;
|
|
1121
|
+
try {
|
|
1122
|
+
const msg = JSON.parse(body);
|
|
1123
|
+
const sessionId = String(msg.sessionId ?? "");
|
|
1124
|
+
const accessEndpoint = msg.accessEndpoint ? String(msg.accessEndpoint) : undefined;
|
|
1125
|
+
if (!computeSessions) {
|
|
1126
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1127
|
+
res.end(JSON.stringify({ error: "compute_disabled" }));
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const result = computeSessions.startSession(sessionId, accessEndpoint);
|
|
1131
|
+
if (!result.ok) {
|
|
1132
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1133
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
log({ event: "compute_started", sessionId });
|
|
1137
|
+
// Wire to on-chain: provider calls startSession
|
|
1138
|
+
if (config.compute.compute_agreement_address) {
|
|
1139
|
+
try {
|
|
1140
|
+
const caContract = new ethers_1.ethers.Contract(config.compute.compute_agreement_address, abis_1.COMPUTE_AGREEMENT_ABI, machineKeySigner);
|
|
1141
|
+
const tx = await caContract.startSession(sessionId);
|
|
1142
|
+
await tx.wait();
|
|
1143
|
+
log({ event: "compute_start_onchain", sessionId, txHash: tx.hash });
|
|
1144
|
+
}
|
|
1145
|
+
catch (onchainErr) {
|
|
1146
|
+
log({ event: "compute_start_onchain_error", sessionId, error: String(onchainErr) });
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1150
|
+
res.end(JSON.stringify({ status: "active", sessionId }));
|
|
1151
|
+
}
|
|
1152
|
+
catch (err) {
|
|
1153
|
+
log({ event: "compute_started_error", error: String(err) });
|
|
1154
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1155
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
1156
|
+
}
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
// POST /compute/metrics — get current metrics (polling endpoint)
|
|
1160
|
+
if (pathname === "/compute/metrics" && req.method === "POST") {
|
|
1161
|
+
const body = await readBody(req, res);
|
|
1162
|
+
if (body === null)
|
|
1163
|
+
return;
|
|
1164
|
+
try {
|
|
1165
|
+
const msg = JSON.parse(body);
|
|
1166
|
+
const sessionId = String(msg.sessionId ?? "");
|
|
1167
|
+
if (!computeMetering || !computeSessions) {
|
|
1168
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1169
|
+
res.end(JSON.stringify({ error: "compute_disabled" }));
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const session = computeSessions.getSession(sessionId);
|
|
1173
|
+
if (!session) {
|
|
1174
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1175
|
+
res.end(JSON.stringify({ error: "session_not_found" }));
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
const current = computeMetering.getCurrentMetrics(sessionId);
|
|
1179
|
+
const reports = computeMetering.getUsageReports(sessionId);
|
|
1180
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1181
|
+
res.end(JSON.stringify({ sessionId, current, reports, consumedMinutes: session.consumedMinutes }));
|
|
1182
|
+
}
|
|
1183
|
+
catch (err) {
|
|
1184
|
+
log({ event: "compute_metrics_error", error: String(err) });
|
|
1185
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1186
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
1187
|
+
}
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
// POST /compute/end — either party ends the session
|
|
1191
|
+
if (pathname === "/compute/end" && req.method === "POST") {
|
|
1192
|
+
const body = await readBody(req, res);
|
|
1193
|
+
if (body === null)
|
|
1194
|
+
return;
|
|
1195
|
+
try {
|
|
1196
|
+
const msg = JSON.parse(body);
|
|
1197
|
+
const sessionId = String(msg.sessionId ?? "");
|
|
1198
|
+
if (!computeSessions) {
|
|
1199
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1200
|
+
res.end(JSON.stringify({ error: "compute_disabled" }));
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
const endResult = await computeSessions.endSession(sessionId);
|
|
1204
|
+
if (!endResult.ok) {
|
|
1205
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1206
|
+
res.end(JSON.stringify({ error: endResult.error }));
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
const r = endResult.result;
|
|
1210
|
+
log({
|
|
1211
|
+
event: "compute_ended",
|
|
1212
|
+
sessionId,
|
|
1213
|
+
consumedMinutes: r.consumedMinutes,
|
|
1214
|
+
costWei: r.costWei.toString(),
|
|
1215
|
+
refundWei: r.refundWei.toString(),
|
|
1216
|
+
});
|
|
1217
|
+
// Wire to on-chain: call endSession
|
|
1218
|
+
if (config.compute.compute_agreement_address) {
|
|
1219
|
+
try {
|
|
1220
|
+
const caContract = new ethers_1.ethers.Contract(config.compute.compute_agreement_address, abis_1.COMPUTE_AGREEMENT_ABI, machineKeySigner);
|
|
1221
|
+
const tx = await caContract.endSession(sessionId);
|
|
1222
|
+
await tx.wait();
|
|
1223
|
+
log({ event: "compute_end_onchain", sessionId, txHash: tx.hash });
|
|
1224
|
+
}
|
|
1225
|
+
catch (onchainErr) {
|
|
1226
|
+
log({ event: "compute_end_onchain_error", sessionId, error: String(onchainErr) });
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1230
|
+
res.end(JSON.stringify({
|
|
1231
|
+
status: "completed",
|
|
1232
|
+
sessionId,
|
|
1233
|
+
consumedMinutes: r.consumedMinutes,
|
|
1234
|
+
costWei: r.costWei.toString(),
|
|
1235
|
+
refundWei: r.refundWei.toString(),
|
|
1236
|
+
reports: r.reports,
|
|
1237
|
+
}));
|
|
1238
|
+
}
|
|
1239
|
+
catch (err) {
|
|
1240
|
+
log({ event: "compute_end_error", error: String(err) });
|
|
1241
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1242
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
1243
|
+
}
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
// POST /compute/dispute — client disputes a session
|
|
1247
|
+
if (pathname === "/compute/dispute" && req.method === "POST") {
|
|
1248
|
+
const body = await readBody(req, res);
|
|
1249
|
+
if (body === null)
|
|
1250
|
+
return;
|
|
1251
|
+
try {
|
|
1252
|
+
const msg = JSON.parse(body);
|
|
1253
|
+
const sessionId = String(msg.sessionId ?? "");
|
|
1254
|
+
if (!computeSessions) {
|
|
1255
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1256
|
+
res.end(JSON.stringify({ error: "compute_disabled" }));
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
const result = computeSessions.disputeSession(sessionId);
|
|
1260
|
+
if (!result.ok) {
|
|
1261
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1262
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
log({ event: "compute_disputed", sessionId });
|
|
1266
|
+
if (config.notifications.notify_on_dispute) {
|
|
1267
|
+
await notifier.notifyDispute(sessionId, String(msg.from ?? "client"));
|
|
1268
|
+
}
|
|
1269
|
+
// Wire to on-chain: call disputeSession
|
|
1270
|
+
if (config.compute.compute_agreement_address) {
|
|
1271
|
+
try {
|
|
1272
|
+
const caContract = new ethers_1.ethers.Contract(config.compute.compute_agreement_address, abis_1.COMPUTE_AGREEMENT_ABI, machineKeySigner);
|
|
1273
|
+
const tx = await caContract.disputeSession(sessionId);
|
|
1274
|
+
await tx.wait();
|
|
1275
|
+
log({ event: "compute_dispute_onchain", sessionId, txHash: tx.hash });
|
|
1276
|
+
}
|
|
1277
|
+
catch (onchainErr) {
|
|
1278
|
+
log({ event: "compute_dispute_onchain_error", sessionId, error: String(onchainErr) });
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1282
|
+
res.end(JSON.stringify({ status: "disputed", sessionId }));
|
|
1283
|
+
}
|
|
1284
|
+
catch (err) {
|
|
1285
|
+
log({ event: "compute_dispute_error", error: String(err) });
|
|
1286
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1287
|
+
res.end(JSON.stringify({ error: "invalid_request" }));
|
|
1288
|
+
}
|
|
810
1289
|
return;
|
|
811
1290
|
}
|
|
1291
|
+
// GET /compute/session/:id — session details
|
|
1292
|
+
if (pathname.startsWith("/compute/session/") && req.method === "GET") {
|
|
1293
|
+
const sessionId = pathname.slice("/compute/session/".length);
|
|
1294
|
+
if (!computeSessions) {
|
|
1295
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1296
|
+
res.end(JSON.stringify({ error: "compute_disabled" }));
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const session = computeSessions.getSession(sessionId);
|
|
1300
|
+
if (!session) {
|
|
1301
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1302
|
+
res.end(JSON.stringify({ error: "session_not_found" }));
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
const current = computeMetering ? computeMetering.getCurrentMetrics(sessionId) : null;
|
|
1306
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1307
|
+
res.end(JSON.stringify({ session, current }));
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
// GET /compute/sessions — list all sessions
|
|
1311
|
+
if (pathname === "/compute/sessions" && req.method === "GET") {
|
|
1312
|
+
if (!computeSessions) {
|
|
1313
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1314
|
+
res.end(JSON.stringify({ error: "compute_disabled" }));
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const sessions = computeSessions.listSessions();
|
|
1318
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1319
|
+
res.end(JSON.stringify({ sessions, count: sessions.length }));
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
// ── File delivery routes ──────────────────────────────────────────────
|
|
1323
|
+
// POST /job/:id/upload — store a file (daemon auth required via global gate above)
|
|
1324
|
+
if (pathname.startsWith("/job/") && req.method === "POST") {
|
|
1325
|
+
const parts = pathname.split("/");
|
|
1326
|
+
// /job/<id>/upload → ["", "job", "<id>", "upload"]
|
|
1327
|
+
if (parts.length === 4 && parts[3] === "upload") {
|
|
1328
|
+
const agreementId = parts[2];
|
|
1329
|
+
if (!config.delivery.serve_files) {
|
|
1330
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1331
|
+
res.end(JSON.stringify({ error: "file_delivery_disabled" }));
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
await fileDelivery.handleUpload(req, res, agreementId, log);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
// GET /job/:id/files, GET /job/:id/files/:name, GET /job/:id/manifest — party-gated
|
|
1339
|
+
if (pathname.startsWith("/job/") && req.method === "GET") {
|
|
1340
|
+
const parts = pathname.split("/");
|
|
1341
|
+
// /job/<id>/files/<name> → ["", "job", "<id>", "files", "<name>"]
|
|
1342
|
+
if (parts.length === 5 && parts[3] === "files") {
|
|
1343
|
+
const agreementId = parts[2];
|
|
1344
|
+
const filename = decodeURIComponent(parts[4]);
|
|
1345
|
+
if (!config.delivery.serve_files) {
|
|
1346
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1347
|
+
res.end(JSON.stringify({ error: "file_delivery_disabled" }));
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
fileDelivery.handleDownloadFile(req, res, agreementId, filename, apiToken, log);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
// /job/<id>/files → ["", "job", "<id>", "files"]
|
|
1354
|
+
if (parts.length === 4 && parts[3] === "files") {
|
|
1355
|
+
const agreementId = parts[2];
|
|
1356
|
+
if (!config.delivery.serve_files) {
|
|
1357
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1358
|
+
res.end(JSON.stringify({ error: "file_delivery_disabled" }));
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
fileDelivery.handleListFiles(req, res, agreementId, apiToken, log);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
// /job/<id>/manifest → ["", "job", "<id>", "manifest"]
|
|
1365
|
+
if (parts.length === 4 && parts[3] === "manifest") {
|
|
1366
|
+
const agreementId = parts[2];
|
|
1367
|
+
if (!config.delivery.serve_files) {
|
|
1368
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1369
|
+
res.end(JSON.stringify({ error: "file_delivery_disabled" }));
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
fileDelivery.handleManifest(req, res, agreementId, apiToken, log);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
812
1376
|
// 404
|
|
813
1377
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
814
1378
|
res.end(JSON.stringify({ error: "not_found" }));
|
|
@@ -838,6 +1402,8 @@ async function runDaemon(foreground = false) {
|
|
|
838
1402
|
clearInterval(relayInterval);
|
|
839
1403
|
clearInterval(timeoutInterval);
|
|
840
1404
|
clearInterval(balanceInterval);
|
|
1405
|
+
if (rateLimitCleanupInterval)
|
|
1406
|
+
clearInterval(rateLimitCleanupInterval);
|
|
841
1407
|
// Close HTTP + IPC
|
|
842
1408
|
httpServer.close();
|
|
843
1409
|
ipcServer.close();
|