@lawrenceliang-btc/atel-sdk 0.8.7

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +221 -0
  3. package/bin/atel.mjs +2692 -0
  4. package/bin/tunnel-manager.mjs +171 -0
  5. package/dist/anchor/base.d.ts +21 -0
  6. package/dist/anchor/base.js +26 -0
  7. package/dist/anchor/bsc.d.ts +20 -0
  8. package/dist/anchor/bsc.js +25 -0
  9. package/dist/anchor/evm.d.ts +99 -0
  10. package/dist/anchor/evm.js +262 -0
  11. package/dist/anchor/index.d.ts +173 -0
  12. package/dist/anchor/index.js +165 -0
  13. package/dist/anchor/mock.d.ts +43 -0
  14. package/dist/anchor/mock.js +100 -0
  15. package/dist/anchor/solana.d.ts +95 -0
  16. package/dist/anchor/solana.js +298 -0
  17. package/dist/auditor/index.d.ts +54 -0
  18. package/dist/auditor/index.js +141 -0
  19. package/dist/collaboration/index.d.ts +146 -0
  20. package/dist/collaboration/index.js +237 -0
  21. package/dist/crypto/index.d.ts +162 -0
  22. package/dist/crypto/index.js +231 -0
  23. package/dist/endpoint/index.d.ts +147 -0
  24. package/dist/endpoint/index.js +390 -0
  25. package/dist/envelope/index.d.ts +104 -0
  26. package/dist/envelope/index.js +156 -0
  27. package/dist/executor/index.d.ts +71 -0
  28. package/dist/executor/index.js +398 -0
  29. package/dist/gateway/index.d.ts +278 -0
  30. package/dist/gateway/index.js +520 -0
  31. package/dist/graph/index.d.ts +215 -0
  32. package/dist/graph/index.js +524 -0
  33. package/dist/handshake/index.d.ts +166 -0
  34. package/dist/handshake/index.js +287 -0
  35. package/dist/identity/index.d.ts +155 -0
  36. package/dist/identity/index.js +250 -0
  37. package/dist/index.d.ts +23 -0
  38. package/dist/index.js +28 -0
  39. package/dist/negotiation/index.d.ts +133 -0
  40. package/dist/negotiation/index.js +160 -0
  41. package/dist/network/index.d.ts +78 -0
  42. package/dist/network/index.js +207 -0
  43. package/dist/orchestrator/index.d.ts +190 -0
  44. package/dist/orchestrator/index.js +297 -0
  45. package/dist/policy/index.d.ts +100 -0
  46. package/dist/policy/index.js +206 -0
  47. package/dist/proof/index.d.ts +220 -0
  48. package/dist/proof/index.js +541 -0
  49. package/dist/registry/index.d.ts +98 -0
  50. package/dist/registry/index.js +129 -0
  51. package/dist/rollback/index.d.ts +76 -0
  52. package/dist/rollback/index.js +91 -0
  53. package/dist/schema/capability-schema.json +52 -0
  54. package/dist/schema/index.d.ts +128 -0
  55. package/dist/schema/index.js +163 -0
  56. package/dist/schema/task-schema.json +69 -0
  57. package/dist/score/index.d.ts +174 -0
  58. package/dist/score/index.js +275 -0
  59. package/dist/service/index.d.ts +34 -0
  60. package/dist/service/index.js +273 -0
  61. package/dist/service/server.d.ts +7 -0
  62. package/dist/service/server.js +22 -0
  63. package/dist/trace/index.d.ts +217 -0
  64. package/dist/trace/index.js +446 -0
  65. package/dist/trust/index.d.ts +84 -0
  66. package/dist/trust/index.js +107 -0
  67. package/dist/trust-sync/index.d.ts +30 -0
  68. package/dist/trust-sync/index.js +57 -0
  69. package/package.json +71 -0
  70. package/skill/SKILL.md +363 -0
  71. package/skill/references/commercial.md +184 -0
  72. package/skill/references/executor.md +356 -0
  73. package/skill/references/networking.md +64 -0
  74. package/skill/references/onchain.md +73 -0
  75. package/skill/references/security.md +96 -0
package/bin/atel.mjs ADDED
@@ -0,0 +1,2692 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ATEL CLI — Command-line interface for ATEL SDK
5
+ *
6
+ * Protocol Commands:
7
+ * atel init [name] Create agent identity + default policy
8
+ * atel info Show identity, capabilities, policy, network
9
+ * atel start [port] Start endpoint (auto network + auto register)
10
+ * atel setup [port] Network setup only (detect IP, UPnP, verify)
11
+ * atel verify Verify port reachability
12
+ * atel inbox [count] Show received messages
13
+ * atel register [name] [caps] [url] Register on public registry
14
+ * atel search <capability> Search registry for agents
15
+ * atel handshake <endpoint> [did] Handshake with a remote agent
16
+ * atel task <endpoint> <json> Delegate a task to a remote agent
17
+ * atel result <taskId> <json> Submit execution result (from executor)
18
+ *
19
+ * Account Commands:
20
+ * atel balance Show platform account balance
21
+ * atel deposit <amount> [channel] Deposit funds
22
+ * atel withdraw <amount> [channel] Withdraw funds
23
+ * atel transactions List payment history
24
+ *
25
+ * Trade Commands:
26
+ * atel order <did> <cap> <price> Create a trade order
27
+ * atel accept <orderId> Accept an order (executor)
28
+ * atel reject <orderId> Reject an order (executor)
29
+ * atel escrow <orderId> Freeze funds for order (requester)
30
+ * atel complete <orderId> [taskId] Mark order complete (executor)
31
+ * atel confirm <orderId> Confirm delivery + settle (requester)
32
+ * atel rate <orderId> <1-5> [comment] Rate the other party
33
+ * atel orders [role] [status] List orders
34
+ *
35
+ * Dispute Commands:
36
+ * atel dispute <orderId> <reason> Open a dispute
37
+ * atel evidence <disputeId> <json> Submit dispute evidence
38
+ * atel disputes List your disputes
39
+ *
40
+ * Certification & Boost Commands:
41
+ * atel cert-apply [level] Apply for certification
42
+ * atel cert-status [did] Check certification status
43
+ * atel boost <tier> <weeks> Purchase boost
44
+ * atel boost-status [did] Check boost status
45
+ */
46
+
47
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
48
+ import { resolve, join } from 'node:path';
49
+ import {
50
+ AgentIdentity, AgentEndpoint, AgentClient, HandshakeManager,
51
+ createMessage, RegistryClient, ExecutionTrace, ProofGenerator,
52
+ SolanaAnchorProvider, BaseAnchorProvider, BSCAnchorProvider,
53
+ autoNetworkSetup, collectCandidates, connectToAgent,
54
+ discoverPublicIP, checkReachable, ContentAuditor, TrustScoreClient,
55
+ RollbackManager, rotateKey, verifyKeyRotation, ToolGateway, PolicyEngine, mintConsentToken, sign,
56
+ TrustGraph, calculateTaskWeight,
57
+ } from '@lawrenceliang-btc/atel-sdk';
58
+ import { TunnelManager, HeartbeatManager } from './tunnel-manager.mjs';
59
+
60
+ const ATEL_DIR = resolve(process.env.ATEL_DIR || '.atel');
61
+ const IDENTITY_FILE = resolve(ATEL_DIR, 'identity.json');
62
+ const REGISTRY_URL = process.env.ATEL_REGISTRY || 'https://api.atelai.org';
63
+ const ATEL_PLATFORM = process.env.ATEL_PLATFORM || 'https://api.atelai.org';
64
+ const ATEL_NOTIFY_GATEWAY = process.env.ATEL_NOTIFY_GATEWAY || process.env.OPENCLAW_GATEWAY_URL || '';
65
+ const ATEL_NOTIFY_TARGET = process.env.ATEL_NOTIFY_TARGET || '';
66
+ let EXECUTOR_URL = process.env.ATEL_EXECUTOR_URL || '';
67
+ const INBOX_FILE = resolve(ATEL_DIR, 'inbox.jsonl');
68
+ const POLICY_FILE = resolve(ATEL_DIR, 'policy.json');
69
+ const TASKS_FILE = resolve(ATEL_DIR, 'tasks.json');
70
+ const SCORE_FILE = resolve(ATEL_DIR, 'trust-scores.json');
71
+ const GRAPH_FILE = resolve(ATEL_DIR, 'trust-graph.json');
72
+ const NETWORK_FILE = resolve(ATEL_DIR, 'network.json');
73
+ const TRACES_DIR = resolve(ATEL_DIR, 'traces');
74
+ const PENDING_FILE = resolve(ATEL_DIR, 'pending-tasks.json');
75
+
76
+ const DEFAULT_POLICY = { rateLimit: 60, maxPayloadBytes: 1048576, maxConcurrent: 10, allowedDIDs: [], blockedDIDs: [], taskMode: 'auto', autoAcceptPlatform: true, autoAcceptP2P: true, trustPolicy: { minScore: 0, newAgentPolicy: 'allow_low_risk', riskThresholds: { low: 0, medium: 50, high: 75, critical: 90 } } };
77
+
78
+ // ─── Helpers ─────────────────────────────────────────────────────
79
+
80
+ function ensureDir() { if (!existsSync(ATEL_DIR)) mkdirSync(ATEL_DIR, { recursive: true }); }
81
+ function log(event) { ensureDir(); appendFileSync(INBOX_FILE, JSON.stringify(event) + '\n'); console.log(JSON.stringify(event)); }
82
+
83
+ function saveIdentity(id) { ensureDir(); writeFileSync(IDENTITY_FILE, JSON.stringify({ agent_id: id.agent_id, did: id.did, publicKey: Buffer.from(id.publicKey).toString('hex'), secretKey: Buffer.from(id.secretKey).toString('hex') }, null, 2)); }
84
+ function loadIdentity() { if (!existsSync(IDENTITY_FILE)) return null; const d = JSON.parse(readFileSync(IDENTITY_FILE, 'utf-8')); return new AgentIdentity({ agent_id: d.agent_id, publicKey: Uint8Array.from(Buffer.from(d.publicKey, 'hex')), secretKey: Uint8Array.from(Buffer.from(d.secretKey, 'hex')) }); }
85
+ function requireIdentity() { const id = loadIdentity(); if (!id) { console.error('No identity. Run: atel init'); process.exit(1); } return id; }
86
+
87
+ function loadCapabilities() { const f = resolve(ATEL_DIR, 'capabilities.json'); if (!existsSync(f)) return []; try { return JSON.parse(readFileSync(f, 'utf-8')); } catch { return []; } }
88
+ function saveCapabilities(c) { ensureDir(); writeFileSync(resolve(ATEL_DIR, 'capabilities.json'), JSON.stringify(c, null, 2)); }
89
+ function loadPolicy() { if (!existsSync(POLICY_FILE)) return { ...DEFAULT_POLICY }; try { return { ...DEFAULT_POLICY, ...JSON.parse(readFileSync(POLICY_FILE, 'utf-8')) }; } catch { return { ...DEFAULT_POLICY }; } }
90
+ function savePolicy(p) { ensureDir(); writeFileSync(POLICY_FILE, JSON.stringify(p, null, 2)); }
91
+ function loadTasks() { if (!existsSync(TASKS_FILE)) return {}; try { return JSON.parse(readFileSync(TASKS_FILE, 'utf-8')); } catch { return {}; } }
92
+ function saveTasks(t) { ensureDir(); writeFileSync(TASKS_FILE, JSON.stringify(t, null, 2)); }
93
+ function loadNetwork() { if (!existsSync(NETWORK_FILE)) return null; try { return JSON.parse(readFileSync(NETWORK_FILE, 'utf-8')); } catch { return null; } }
94
+ function saveNetwork(n) { ensureDir(); writeFileSync(NETWORK_FILE, JSON.stringify(n, null, 2)); }
95
+ function saveTrace(taskId, trace) { if (!existsSync(TRACES_DIR)) mkdirSync(TRACES_DIR, { recursive: true }); writeFileSync(resolve(TRACES_DIR, `${taskId}.jsonl`), trace.export()); }
96
+ function loadTrace(taskId) { const f = resolve(TRACES_DIR, `${taskId}.jsonl`); if (!existsSync(f)) return null; return readFileSync(f, 'utf-8'); }
97
+ function loadPending() { if (!existsSync(PENDING_FILE)) return {}; try { return JSON.parse(readFileSync(PENDING_FILE, 'utf-8')); } catch { return {}; } }
98
+ function savePending(p) { ensureDir(); writeFileSync(PENDING_FILE, JSON.stringify(p, null, 2)); }
99
+
100
+ // Derive wallet addresses from env private keys
101
+ async function getWalletAddresses() {
102
+ const wallets = {};
103
+ // Solana: base58 private key → public key
104
+ const solKey = process.env.ATEL_SOLANA_PRIVATE_KEY;
105
+ if (solKey) {
106
+ try {
107
+ const { Keypair } = await import('@solana/web3.js');
108
+ const bs58 = (await import('bs58')).default;
109
+ const kp = Keypair.fromSecretKey(bs58.decode(solKey));
110
+ wallets.solana = kp.publicKey.toBase58();
111
+ } catch {}
112
+ }
113
+ // Base: hex private key → address
114
+ const baseKey = process.env.ATEL_BASE_PRIVATE_KEY;
115
+ if (baseKey) {
116
+ try {
117
+ const { ethers } = await import('ethers');
118
+ wallets.base = new ethers.Wallet(baseKey).address;
119
+ } catch {}
120
+ }
121
+ // BSC: hex private key → address
122
+ const bscKey = process.env.ATEL_BSC_PRIVATE_KEY;
123
+ if (bscKey) {
124
+ try {
125
+ const { ethers } = await import('ethers');
126
+ wallets.bsc = new ethers.Wallet(bscKey).address;
127
+ } catch {}
128
+ }
129
+ return Object.keys(wallets).length > 0 ? wallets : undefined;
130
+ }
131
+
132
+ // Detect preferred chain based on configured private keys
133
+ function detectPreferredChain() {
134
+ if (process.env.ATEL_SOLANA_PRIVATE_KEY) return 'solana';
135
+ if (process.env.ATEL_BASE_PRIVATE_KEY) return 'base';
136
+ if (process.env.ATEL_BSC_PRIVATE_KEY) return 'bsc';
137
+ return null;
138
+ }
139
+
140
+ // ─── Unified Trust Score & Level System ──────────────────────────
141
+ // Single source of truth: computeTrustScore() calculates score,
142
+ // trustLevel is derived from score. No independent logic.
143
+ //
144
+ // DUAL MODE:
145
+ // - Local mode (default): Only uses local trust-history.json
146
+ // Passive, only knows about direct interactions. Fast, no network.
147
+ // - Chain-verified mode: Verifies anchor_tx on-chain + accepts peer-provided proofs
148
+ // Active, can assess agents never interacted with. Requires RPC access.
149
+ //
150
+ // Score formula (0-100):
151
+ // - Success rate: successRate * 40 (max 40, baseline competence)
152
+ // - Task volume: min(tasks/30, 1) * 30 (max 30, needs 30 tasks for full credit)
153
+ // - Verified proofs: verifiedRatio * 20 * sqrt(volFactor) (max 20, scales with experience)
154
+ // - Chain bonus: +10 if 5+ verified proofs (sustained chain participation)
155
+ //
156
+ // Level mapping (derived from score):
157
+ // Level 0 (zero_trust): score < 30 → max risk: low
158
+ // Level 1 (basic_trust): score 30-64 → max risk: medium
159
+ // Level 2 (verified_trust): score 65-89 → max risk: high
160
+ // Level 3 (enterprise_trust): score >= 90 → max risk: critical
161
+ //
162
+ // Upgrade path (best case, 100% success + all verified):
163
+ // 1 task → 44 pts → L1 | 8 tasks → 68 pts → L2
164
+ // 25 tasks → 93 pts → L3 | No proofs → capped at ~50 pts (L1)
165
+
166
+ function computeTrustScore(agentHistory) {
167
+ if (!agentHistory || agentHistory.tasks === 0) return 0;
168
+ const successRate = agentHistory.successes / agentHistory.tasks;
169
+ const volFactor = Math.min(agentHistory.tasks / 30, 1);
170
+ const successScore = successRate * 40;
171
+ const volumeScore = volFactor * 30;
172
+ const verifiedProofs = agentHistory.proofs ? agentHistory.proofs.filter(p => p.verified).length : 0;
173
+ const verifiedRatio = agentHistory.proofs?.length > 0 ? verifiedProofs / agentHistory.proofs.length : 0;
174
+ const proofScore = verifiedRatio * 20 * Math.sqrt(volFactor);
175
+ const chainBonus = verifiedProofs >= 5 ? 10 : 0;
176
+ return Math.min(100, Math.round((successScore + volumeScore + proofScore + chainBonus) * 100) / 100);
177
+ }
178
+
179
+ function computeTrustLevel(score) {
180
+ if (score >= 90) return { level: 3, name: 'enterprise_trust', maxRisk: 'critical' };
181
+ if (score >= 65) return { level: 2, name: 'verified_trust', maxRisk: 'high' };
182
+ if (score >= 30) return { level: 1, name: 'basic_trust', maxRisk: 'medium' };
183
+ return { level: 0, name: 'zero_trust', maxRisk: 'low' };
184
+ }
185
+
186
+ const RISK_ORDER = { low: 0, medium: 1, high: 2, critical: 3 };
187
+ function riskAllowed(maxRisk, requestedRisk) { return (RISK_ORDER[requestedRisk] || 0) <= (RISK_ORDER[maxRisk] || 0); }
188
+
189
+ // Verify anchor_tx list on-chain, return count of valid proofs
190
+ async function verifyAnchorTxList(anchorTxList, targetDid) {
191
+ if (!anchorTxList || anchorTxList.length === 0) return { verified: 0, total: 0, proofs: [] };
192
+ const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
193
+ const provider = new SolanaAnchorProvider({ rpcUrl });
194
+ let verified = 0;
195
+ const proofs = [];
196
+ for (const tx of anchorTxList) {
197
+ try {
198
+ const result = await provider.verify(tx.trace_root || '', tx.txHash || tx.anchor_tx || '');
199
+ if (result.valid) {
200
+ verified++;
201
+ proofs.push({ proof_id: tx.proof_id || tx.txHash, trace_root: tx.trace_root, verified: true, anchor_tx: tx.txHash || tx.anchor_tx, timestamp: new Date().toISOString() });
202
+ }
203
+ } catch {}
204
+ }
205
+ return { verified, total: anchorTxList.length, proofs };
206
+ }
207
+
208
+ // Unified trust check — single function used by all code paths
209
+ function checkTrust(remoteDid, risk, policy, force) {
210
+ if (force) return { passed: true };
211
+ const tp = policy.trustPolicy || DEFAULT_POLICY.trustPolicy;
212
+ const localHistoryFile = resolve(ATEL_DIR, 'trust-history.json');
213
+ let history = {};
214
+ try { history = JSON.parse(readFileSync(localHistoryFile, 'utf-8')); } catch {}
215
+ const agentHistory = history[remoteDid] || { tasks: 0, successes: 0, failures: 0, proofs: [] };
216
+ const isNewAgent = agentHistory.tasks === 0;
217
+
218
+ // New agent policy
219
+ if (isNewAgent) {
220
+ if (tp.newAgentPolicy === 'deny') return { passed: false, reason: 'Trust policy denies unknown agents', did: remoteDid, risk };
221
+ if (tp.newAgentPolicy === 'allow_low_risk' && (risk === 'high' || risk === 'critical')) return { passed: false, reason: `New agent, policy only allows low risk (requested: ${risk})`, did: remoteDid };
222
+ }
223
+
224
+ // Compute score and level
225
+ const score = computeTrustScore(agentHistory);
226
+ const trustLevel = computeTrustLevel(score);
227
+ const threshold = tp.riskThresholds?.[risk] ?? 0;
228
+
229
+ // Check score threshold
230
+ if (!isNewAgent && threshold > 0 && score < threshold) {
231
+ return { passed: false, reason: `Score ${score} below threshold ${threshold} for ${risk} risk`, did: remoteDid, score, threshold, risk, level: trustLevel.level };
232
+ }
233
+
234
+ // Check level-based risk cap
235
+ if (!riskAllowed(trustLevel.maxRisk, risk)) {
236
+ return { passed: false, reason: `Trust level ${trustLevel.level} (${trustLevel.name}) only allows up to ${trustLevel.maxRisk} risk, requested ${risk}`, did: remoteDid, level: trustLevel.level, maxRisk: trustLevel.maxRisk };
237
+ }
238
+
239
+ return { passed: true, score, level: trustLevel.level, levelName: trustLevel.name, threshold };
240
+ }
241
+
242
+ // ─── Policy Enforcer ─────────────────────────────────────────────
243
+
244
+ class PolicyEnforcer {
245
+ constructor(policy) { this.policy = policy; this.requestLog = []; this.activeTasks = 0; }
246
+ check(message) {
247
+ const p = this.policy, from = message.from, size = JSON.stringify(message.payload || {}).length, now = Date.now();
248
+ if (p.blockedDIDs.length > 0 && p.blockedDIDs.includes(from)) return { allowed: false, reason: `DID blocked` };
249
+ if (p.allowedDIDs.length > 0 && !p.allowedDIDs.includes(from)) return { allowed: false, reason: `DID not in allowlist` };
250
+ this.requestLog = this.requestLog.filter(t => now - t < 60000);
251
+ if (this.requestLog.length >= p.rateLimit) return { allowed: false, reason: `Rate limit (${p.rateLimit}/min)` };
252
+ if (size > p.maxPayloadBytes) return { allowed: false, reason: `Payload too large (${size} > ${p.maxPayloadBytes})` };
253
+ if (this.activeTasks >= p.maxConcurrent) return { allowed: false, reason: `Max concurrent (${p.maxConcurrent})` };
254
+ this.requestLog.push(now);
255
+ return { allowed: true };
256
+ }
257
+ taskStarted() { this.activeTasks++; }
258
+ taskFinished() { this.activeTasks = Math.max(0, this.activeTasks - 1); }
259
+ }
260
+
261
+ // ─── On-chain Anchoring ──────────────────────────────────────────
262
+
263
+ async function anchorOnChain(traceRoot, metadata, preferredChain) {
264
+ const chain = preferredChain || detectPreferredChain();
265
+ if (!chain) return null;
266
+
267
+ try {
268
+ let provider;
269
+ if (chain === 'solana') {
270
+ const key = process.env.ATEL_SOLANA_PRIVATE_KEY;
271
+ if (!key) return null;
272
+ provider = new SolanaAnchorProvider({
273
+ rpcUrl: process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com',
274
+ privateKey: key
275
+ });
276
+ } else if (chain === 'base') {
277
+ const key = process.env.ATEL_BASE_PRIVATE_KEY;
278
+ if (!key) return null;
279
+ provider = new BaseAnchorProvider({
280
+ rpcUrl: process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org',
281
+ privateKey: key
282
+ });
283
+ } else if (chain === 'bsc') {
284
+ const key = process.env.ATEL_BSC_PRIVATE_KEY;
285
+ if (!key) return null;
286
+ provider = new BSCAnchorProvider({
287
+ rpcUrl: process.env.ATEL_BSC_RPC_URL || 'https://bsc-dataseed.binance.org',
288
+ privateKey: key
289
+ });
290
+ } else {
291
+ return null;
292
+ }
293
+
294
+ const r = await provider.anchor(traceRoot, {
295
+ executorDid: metadata?.executorDid,
296
+ requesterDid: metadata?.requesterDid || metadata?.task_from,
297
+ taskId: metadata?.taskId,
298
+ ...metadata,
299
+ });
300
+ log({ event: 'proof_anchored', chain, txHash: r.txHash, block: r.blockNumber, trace_root: traceRoot });
301
+ return { ...r, chain };
302
+ } catch (e) {
303
+ log({ event: 'anchor_failed', chain, error: e.message });
304
+ return null;
305
+ }
306
+ }
307
+
308
+ // ─── Commands ────────────────────────────────────────────────────
309
+
310
+ async function cmdInit(agentId) {
311
+ const name = agentId || `agent-${Date.now()}`;
312
+ const identity = new AgentIdentity({ agent_id: name });
313
+ saveIdentity(identity);
314
+ savePolicy(DEFAULT_POLICY);
315
+ // Create default agent-context.md for built-in executor
316
+ const ctxFile = resolve(ATEL_DIR, 'agent-context.md');
317
+ if (!existsSync(ctxFile)) {
318
+ writeFileSync(ctxFile, `# Agent Context\n\nYou are an ATEL agent (${name}) processing tasks from other agents via the ATEL protocol.\n\n## Guidelines\n- Complete the task accurately and concisely\n- Return only the requested result, no extra commentary\n- If the task is unclear, do your best interpretation\n- Do not access private files or sensitive data\n- Do not make external network requests unless the task requires it\n`);
319
+ }
320
+ console.log(JSON.stringify({ status: 'created', agent_id: identity.agent_id, did: identity.did, policy: POLICY_FILE, next: 'Run: atel start [port] — auto-configures network and registers' }, null, 2));
321
+ }
322
+
323
+ async function cmdInfo() {
324
+ const id = requireIdentity();
325
+ console.log(JSON.stringify({ agent_id: id.agent_id, did: id.did, capabilities: loadCapabilities(), policy: loadPolicy(), network: loadNetwork(), executor: EXECUTOR_URL || 'not configured' }, null, 2));
326
+ }
327
+
328
+ async function cmdSetup(port) {
329
+ const p = parseInt(port || '3100');
330
+ console.log(JSON.stringify({ event: 'network_setup', port: p }));
331
+ const net = await autoNetworkSetup(p);
332
+ for (const step of net.steps) console.log(JSON.stringify({ event: 'step', message: step }));
333
+ if (net.endpoint) {
334
+ saveNetwork({ publicIP: net.publicIP, port: p, endpoint: net.endpoint, upnp: net.upnpSuccess, reachable: net.reachable, configuredAt: new Date().toISOString() });
335
+ console.log(JSON.stringify({ status: 'ready', endpoint: net.endpoint }));
336
+ } else if (net.publicIP) {
337
+ const ep = `http://${net.publicIP}:${p}`;
338
+ saveNetwork({ publicIP: net.publicIP, port: p, endpoint: ep, upnp: false, reachable: false, needsManualPortForward: true, configuredAt: new Date().toISOString() });
339
+ console.log(JSON.stringify({ status: 'needs_port_forward', publicIP: net.publicIP, port: p, instruction: `Forward external TCP port ${p} to this machine's port ${p} on your router. Then run: atel verify` }));
340
+ } else {
341
+ console.log(JSON.stringify({ status: 'failed', error: 'Could not determine public IP' }));
342
+ }
343
+ }
344
+
345
+ async function cmdVerify() {
346
+ const net = loadNetwork();
347
+ if (!net) { console.error('No network config. Run: atel setup'); process.exit(1); }
348
+ console.log(JSON.stringify({ event: 'verifying', ip: net.publicIP, port: net.port }));
349
+ const result = await verifyPortReachable(net.publicIP, net.port);
350
+ console.log(JSON.stringify({ status: result.reachable ? 'reachable' : 'not_reachable', detail: result.detail }));
351
+ if (result.reachable) { net.reachable = true; net.needsManualPortForward = false; saveNetwork(net); }
352
+ }
353
+
354
+ /**
355
+ * Start ToolGateway proxy server for verifiable execution.
356
+ * All executor tool calls must go through this proxy to be recorded in trace.
357
+ */
358
+ async function startToolGatewayProxy(port, identity, policy) {
359
+ const { default: express } = await import('express');
360
+ const app = express();
361
+ app.use(express.json({ limit: '10mb' }));
362
+
363
+ // Each task has its own gateway + trace
364
+ const taskGateways = new Map();
365
+
366
+ // POST /init - Initialize task gateway
367
+ app.post('/init', async (req, res) => {
368
+ const { taskId } = req.body;
369
+ if (!taskId) { res.status(400).json({ error: 'taskId required' }); return; }
370
+
371
+ const trace = new ExecutionTrace(taskId, identity);
372
+
373
+ // Create a permissive policy adapter (allow all tools)
374
+ const permissivePolicy = {
375
+ evaluate: (action, context) => ({ decision: 'allow' }),
376
+ };
377
+
378
+ const gateway = new ToolGateway(permissivePolicy, { trace });
379
+
380
+ taskGateways.set(taskId, { gateway, trace, tools: new Map() });
381
+ res.json({ status: 'initialized', taskId, proxyUrl: `http://127.0.0.1:${port}` });
382
+ });
383
+
384
+ // POST /register - Register tool endpoint
385
+ app.post('/register', async (req, res) => {
386
+ const { taskId, tool, endpoint } = req.body;
387
+ if (!taskId || !tool || !endpoint) {
388
+ res.status(400).json({ error: 'taskId, tool, endpoint required' });
389
+ return;
390
+ }
391
+
392
+ const ctx = taskGateways.get(taskId);
393
+ if (!ctx) { res.status(404).json({ error: 'Task not initialized' }); return; }
394
+
395
+ // Register tool: calls executor-provided endpoint
396
+ ctx.gateway.registerTool(tool, async (input) => {
397
+ const resp = await fetch(endpoint, {
398
+ method: 'POST',
399
+ headers: { 'Content-Type': 'application/json' },
400
+ body: JSON.stringify({ tool, input }),
401
+ signal: AbortSignal.timeout(180000),
402
+ });
403
+ if (!resp.ok) throw new Error(`Tool ${tool} failed: ${resp.status}`);
404
+ return await resp.json();
405
+ });
406
+
407
+ ctx.tools.set(tool, endpoint);
408
+ res.json({ status: 'registered', tool });
409
+ });
410
+
411
+ // POST /call - Call tool through gateway
412
+ app.post('/call', async (req, res) => {
413
+ const { taskId, tool, input, risk_level, data_scope } = req.body;
414
+ if (!taskId || !tool) {
415
+ res.status(400).json({ error: 'taskId and tool required' });
416
+ return;
417
+ }
418
+
419
+ const ctx = taskGateways.get(taskId);
420
+ if (!ctx) { res.status(404).json({ error: 'Task not initialized' }); return; }
421
+
422
+ try {
423
+ const result = await ctx.gateway.callTool({ tool, input, risk_level, data_scope });
424
+ res.json(result);
425
+ } catch (e) {
426
+ res.status(400).json({ error: e.message, type: e.name });
427
+ }
428
+ });
429
+
430
+ // POST /finalize - Finalize task, return trace
431
+ app.post('/finalize', (req, res) => {
432
+ const { taskId, success, result } = req.body;
433
+ if (!taskId) { res.status(400).json({ error: 'taskId required' }); return; }
434
+
435
+ const ctx = taskGateways.get(taskId);
436
+ if (!ctx) { res.status(404).json({ error: 'Task not initialized' }); return; }
437
+
438
+ if (success) {
439
+ ctx.trace.finalize(result || {});
440
+ } else {
441
+ ctx.trace.fail(new Error(result?.error || 'Task failed'));
442
+ }
443
+
444
+ // Return trace as object with events array
445
+ const traceObj = {
446
+ events: ctx.trace.events,
447
+ taskId: ctx.trace.taskId,
448
+ executor: ctx.trace.identity.did,
449
+ };
450
+ taskGateways.delete(taskId);
451
+
452
+ res.json({ status: 'finalized', trace: traceObj });
453
+ });
454
+
455
+ // GET /trace/:taskId - Get current trace
456
+ app.get('/trace/:taskId', (req, res) => {
457
+ const ctx = taskGateways.get(req.params.taskId);
458
+ if (!ctx) { res.status(404).json({ error: 'Task not found' }); return; }
459
+ res.json({ trace: ctx.trace.export() });
460
+ });
461
+
462
+ return new Promise((resolve) => {
463
+ const server = app.listen(port, '127.0.0.1', () => {
464
+ resolve({ server, port, taskGateways });
465
+ });
466
+ });
467
+ }
468
+
469
+ async function cmdStart(port) {
470
+ const id = requireIdentity();
471
+ const p = parseInt(port || '3100');
472
+ const caps = loadCapabilities();
473
+ const capTypes = caps.map(c => c.type || c);
474
+ const policy = loadPolicy();
475
+ const enforcer = new PolicyEnforcer(policy);
476
+ const pendingTasks = loadTasks();
477
+
478
+ // ── Network: collect candidates ──
479
+ let networkConfig = loadNetwork();
480
+ if (!networkConfig) {
481
+ log({ event: 'network_setup', status: 'auto-detecting' });
482
+ networkConfig = await autoNetworkSetup(p);
483
+ for (const step of networkConfig.steps) log({ event: 'network_step', message: step });
484
+ delete networkConfig.steps;
485
+ saveNetwork(networkConfig);
486
+ } else {
487
+ log({ event: 'network_loaded', candidates: networkConfig.candidates.length });
488
+ }
489
+
490
+ // ── Start endpoint ──
491
+ const endpoint = new AgentEndpoint(id, { port: p, host: '0.0.0.0' });
492
+
493
+ // ── Start ToolGateway Proxy Server ──
494
+ const toolProxyPort = p + 1;
495
+ const toolGatewayServer = await startToolGatewayProxy(toolProxyPort, id, policy);
496
+ log({ event: 'tool_gateway_started', port: toolProxyPort });
497
+
498
+ // ── Built-in Executor (auto-start if no external ATEL_EXECUTOR_URL) ──
499
+ let builtinExecutor = null;
500
+ if (!EXECUTOR_URL) {
501
+ const executorPort = p + 2;
502
+ try {
503
+ const { BuiltinExecutor } = await import('../dist/executor/index.js');
504
+ builtinExecutor = new BuiltinExecutor({
505
+ port: executorPort,
506
+ callbackUrl: `http://127.0.0.1:${p}/atel/v1/result`,
507
+ gatewayUrl: process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789',
508
+ contextPath: join(ATEL_DIR, 'agent-context.md'),
509
+ log,
510
+ });
511
+ await builtinExecutor.start();
512
+ EXECUTOR_URL = `http://127.0.0.1:${executorPort}`;
513
+ log({ event: 'builtin_executor_started', port: executorPort, url: EXECUTOR_URL });
514
+ } catch (e) {
515
+ log({ event: 'builtin_executor_failed', error: e.message, note: 'Falling back to echo mode. Set ATEL_EXECUTOR_URL for external executor.' });
516
+ }
517
+ }
518
+
519
+ // ── Trust Score Client (persistent) ──
520
+ const trustScoreClient = new TrustScoreClient();
521
+ // Load saved proof records
522
+ try {
523
+ const saved = JSON.parse(readFileSync(SCORE_FILE, 'utf-8'));
524
+ if (saved.proofRecords) {
525
+ for (const r of saved.proofRecords) trustScoreClient.addProofRecord(r);
526
+ }
527
+ if (saved.summaries) {
528
+ for (const s of saved.summaries) trustScoreClient.submitExecutionSummary(s);
529
+ }
530
+ log({ event: 'trust_scores_loaded', records: (saved.proofRecords || []).length, summaries: (saved.summaries || []).length });
531
+ } catch {}
532
+ // Accumulated records for persistence
533
+ const _proofRecords = [];
534
+ const _summaries = [];
535
+ try {
536
+ const saved = JSON.parse(readFileSync(SCORE_FILE, 'utf-8'));
537
+ if (saved.proofRecords) _proofRecords.push(...saved.proofRecords);
538
+ if (saved.summaries) _summaries.push(...saved.summaries);
539
+ } catch {}
540
+ function saveTrustScores() {
541
+ try { writeFileSync(SCORE_FILE, JSON.stringify({ proofRecords: _proofRecords, summaries: _summaries }, null, 2)); } catch {}
542
+ }
543
+
544
+ // ── Trust Graph (persistent) ──
545
+ const trustGraph = new TrustGraph();
546
+ const _interactions = [];
547
+ try {
548
+ const saved = JSON.parse(readFileSync(GRAPH_FILE, 'utf-8'));
549
+ if (saved.interactions) {
550
+ for (const i of saved.interactions) {
551
+ trustGraph.recordInteraction(i);
552
+ }
553
+ _interactions.push(...saved.interactions);
554
+ }
555
+ log({ event: 'trust_graph_loaded', interactions: _interactions.length, stats: trustGraph.getStats() });
556
+ } catch {}
557
+ function saveTrustGraph() {
558
+ try { writeFileSync(GRAPH_FILE, JSON.stringify({ interactions: _interactions, exported: trustGraph.exportGraph() }, null, 2)); } catch {}
559
+ }
560
+
561
+ // ── Nonce Store (anti-replay) ──
562
+ const nonceFile = join(ATEL_DIR, 'nonces.json');
563
+ const usedNonces = new Set((() => { try { return JSON.parse(readFileSync(nonceFile, 'utf8')); } catch { return []; } })());
564
+ const saveNonces = () => { try { writeFileSync(nonceFile, JSON.stringify([...usedNonces].slice(-10000))); } catch {} };
565
+
566
+ // ── Helper: generate rejection Proof (local only, no on-chain) ──
567
+ function generateRejectionProof(from, action, reason, stage) {
568
+ const rejectId = `reject-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
569
+ const trace = new ExecutionTrace(rejectId, id);
570
+ trace.append('TASK_RECEIVED', { from, action });
571
+ trace.append(stage, { result: 'rejected', reason });
572
+ trace.fail(new Error(reason));
573
+ const proofGen = new ProofGenerator(trace, id);
574
+ const proof = proofGen.generate(capTypes.join(',') || 'no-policy', `rejected-from-${from}`, reason);
575
+ log({ event: 'rejection_proof', rejectId, from, action, stage, reason, proof_id: proof.proof_id, trace_root: proof.trace_root, timestamp: new Date().toISOString() });
576
+ return { proof_id: proof.proof_id, trace_root: proof.trace_root };
577
+ }
578
+
579
+ // ── Trace endpoint (for audit requests from other agents) ──
580
+ endpoint.app?.get?.('/atel/v1/trace/:taskId', (req, res) => {
581
+ const taskId = req.params.taskId;
582
+ const traceData = loadTrace(taskId);
583
+ if (!traceData) { res.status(404).json({ error: 'Trace not found' }); return; }
584
+ const events = traceData.split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
585
+ res.json({ taskId, events, agent: id.did });
586
+ });
587
+
588
+ // Approve pending task: POST /atel/v1/approve (CLI calls this)
589
+ endpoint.app?.post?.('/atel/v1/approve', async (req, res) => {
590
+ const { taskId } = req.body || {};
591
+ if (!taskId) { res.status(400).json({ error: 'taskId required' }); return; }
592
+ const pending = loadPending();
593
+ const task = pending[taskId];
594
+ if (!task) { res.status(404).json({ error: 'Task not found in pending queue' }); return; }
595
+ if (task.status !== 'pending_confirm') { res.status(400).json({ error: `Task not pending (status: ${task.status})` }); return; }
596
+
597
+ if (!EXECUTOR_URL) {
598
+ res.status(500).json({ error: 'No executor configured' });
599
+ return;
600
+ }
601
+
602
+ // Forward to executor
603
+ try {
604
+ log({ event: 'task_approved', taskId, from: task.from, action: task.action, timestamp: new Date().toISOString() });
605
+
606
+ // Register in active tasks
607
+ pendingTasks[taskId] = { from: task.from, action: task.action, payload: task.payload, encrypted: task.encrypted || false, acceptedAt: new Date().toISOString() };
608
+ saveTasks(pendingTasks);
609
+ enforcer.taskStarted();
610
+
611
+ const execResp = await fetch(EXECUTOR_URL, {
612
+ method: 'POST',
613
+ headers: { 'Content-Type': 'application/json' },
614
+ body: JSON.stringify({
615
+ taskId,
616
+ from: task.from,
617
+ action: task.action,
618
+ payload: task.payload,
619
+ encrypted: task.encrypted || false,
620
+ toolProxy: `http://127.0.0.1:${toolProxyPort}`,
621
+ }),
622
+ signal: AbortSignal.timeout(600000),
623
+ });
624
+
625
+ if (execResp.ok) {
626
+ task.status = 'approved';
627
+ savePending(pending);
628
+ res.json({ status: 'approved', taskId, forwarded: true });
629
+ } else {
630
+ const err = await execResp.text();
631
+ res.status(500).json({ error: 'Executor error: ' + err });
632
+ }
633
+ } catch (e) {
634
+ res.status(500).json({ error: 'Forward failed: ' + e.message });
635
+ }
636
+ });
637
+
638
+ // Webhook notification: POST /atel/v1/notify (platform calls this for order events)
639
+ endpoint.app?.post?.('/atel/v1/notify', async (req, res) => {
640
+ const { event, payload } = req.body || {};
641
+ if (!event || !payload) {
642
+ res.status(400).json({ error: 'event and payload required' });
643
+ return;
644
+ }
645
+
646
+ log({ event: 'webhook_received', type: event, payload });
647
+
648
+ if (event === 'order_created') {
649
+ // New order notification - decide whether to accept
650
+ const { orderId, requesterDid, capabilityType, priceAmount, description } = payload;
651
+
652
+ // Check capability match
653
+ if (!capTypes.includes(capabilityType)) {
654
+ log({ event: 'order_rejected', orderId, reason: 'capability_mismatch', required: capabilityType, available: capTypes });
655
+ res.json({ status: 'rejected', reason: 'capability not supported' });
656
+ return;
657
+ }
658
+
659
+ // ── Task Mode Check ──
660
+ const currentPolicy = loadPolicy();
661
+ const taskMode = currentPolicy.taskMode || 'auto';
662
+
663
+ if (taskMode === 'off') {
664
+ log({ event: 'order_rejected_mode_off', orderId, requesterDid, capabilityType });
665
+ // Reject via Platform API
666
+ try {
667
+ const timestamp = new Date().toISOString();
668
+ const rejectPayload = { reason: 'Agent task mode is off' };
669
+ const signPayload = { did: id.did, timestamp, payload: rejectPayload };
670
+ const signature = sign(signPayload, id.secretKey);
671
+ await fetch(`${ATEL_PLATFORM}/trade/v1/order/${orderId}/reject`, {
672
+ method: 'POST',
673
+ headers: { 'Content-Type': 'application/json' },
674
+ body: JSON.stringify({ did: id.did, timestamp, signature, payload: rejectPayload }),
675
+ signal: AbortSignal.timeout(10000),
676
+ });
677
+ } catch (e) { log({ event: 'order_reject_api_error', orderId, error: e.message }); }
678
+ res.json({ status: 'rejected', reason: 'task_mode_off' });
679
+ return;
680
+ }
681
+
682
+ if (taskMode === 'confirm' || currentPolicy.autoAcceptPlatform === false) {
683
+ // Queue for manual approval
684
+ const pending = loadPending();
685
+ pending[orderId] = {
686
+ source: 'platform',
687
+ from: requesterDid,
688
+ action: capabilityType,
689
+ payload: { orderId, priceAmount, description },
690
+ price: priceAmount || 0,
691
+ status: 'pending_confirm',
692
+ receivedAt: new Date().toISOString(),
693
+ orderId,
694
+ };
695
+ savePending(pending);
696
+ log({ event: 'order_queued', orderId, requesterDid, capabilityType, reason: taskMode === 'confirm' ? 'task_mode_confirm' : 'autoAcceptPlatform_off' });
697
+ res.json({ status: 'queued', orderId, message: 'Awaiting manual confirmation. Use: atel approve ' + orderId });
698
+ return;
699
+ }
700
+
701
+ // Auto-accept (default)
702
+ log({ event: 'order_auto_accept', orderId, requesterDid, capabilityType });
703
+
704
+ // Call platform API to accept
705
+ try {
706
+ const timestamp = new Date().toISOString(); // RFC3339 format
707
+ const payload = {}; // Empty payload for accept
708
+ const signPayload = { did: id.did, timestamp, payload };
709
+ const signature = sign(signPayload, id.secretKey);
710
+
711
+ const signedRequest = {
712
+ did: id.did,
713
+ timestamp,
714
+ signature,
715
+ payload
716
+ };
717
+
718
+ log({ event: 'order_accept_calling_api', orderId, platform: ATEL_PLATFORM });
719
+
720
+ const acceptResp = await fetch(`${ATEL_PLATFORM}/trade/v1/order/${orderId}/accept`, {
721
+ method: 'POST',
722
+ headers: { 'Content-Type': 'application/json' },
723
+ body: JSON.stringify(signedRequest),
724
+ signal: AbortSignal.timeout(10000), // 10秒超时
725
+ });
726
+
727
+ log({ event: 'order_accept_response', orderId, status: acceptResp.status, ok: acceptResp.ok });
728
+
729
+ if (acceptResp.ok) {
730
+ log({ event: 'order_accepted', orderId });
731
+ res.json({ status: 'accepted', orderId });
732
+ } else {
733
+ const error = await acceptResp.text();
734
+ log({ event: 'order_accept_failed', orderId, error, status: acceptResp.status });
735
+ res.status(500).json({ error: 'accept failed: ' + error });
736
+ }
737
+ } catch (err) {
738
+ log({ event: 'order_accept_error', orderId, error: err.message, stack: err.stack });
739
+ console.error('[ERROR] Order accept failed:', err);
740
+ res.status(500).json({ error: err.message });
741
+ }
742
+ return;
743
+ }
744
+
745
+ if (event === 'task_start') {
746
+ // Task execution notification - forward to executor
747
+ const { orderId, requesterDid, capabilityType, priceAmount, chain, description } = payload;
748
+
749
+ log({ event: 'task_start_received', orderId, requesterDid, chain });
750
+
751
+ // Forward to executor
752
+ if (!EXECUTOR_URL) {
753
+ log({ event: 'task_start_no_executor', orderId });
754
+ res.status(500).json({ error: 'no executor configured' });
755
+ return;
756
+ }
757
+
758
+ // Register task in pendingTasks
759
+ pendingTasks[orderId] = {
760
+ from: requesterDid,
761
+ action: capabilityType,
762
+ chain: chain, // 保存 chain
763
+ acceptedAt: new Date().toISOString(),
764
+ encrypted: false
765
+ };
766
+ saveTasks(pendingTasks);
767
+
768
+ // Respond immediately to relay, then forward to executor async
769
+ res.json({ status: 'accepted', orderId });
770
+
771
+ // Async: forward to executor (no timeout pressure from relay)
772
+ (async () => {
773
+ try {
774
+ log({ event: 'task_forward_calling_executor', orderId, executor: EXECUTOR_URL });
775
+
776
+ const execResp = await fetch(EXECUTOR_URL, {
777
+ method: 'POST',
778
+ headers: { 'Content-Type': 'application/json' },
779
+ body: JSON.stringify({
780
+ taskId: orderId,
781
+ from: requesterDid,
782
+ action: capabilityType,
783
+ payload: { orderId, priceAmount, text: description || '' },
784
+ toolProxy: `http://127.0.0.1:${toolProxyPort}`,
785
+ callbackUrl: `http://127.0.0.1:${p}/atel/v1/result`
786
+ }),
787
+ signal: AbortSignal.timeout(600000), // 10 min timeout
788
+ });
789
+
790
+ log({ event: 'task_forward_response', orderId, status: execResp.status, ok: execResp.ok });
791
+
792
+ if (!execResp.ok) {
793
+ const error = await execResp.text();
794
+ log({ event: 'task_forward_failed', orderId, error, status: execResp.status });
795
+ } else {
796
+ log({ event: 'task_forwarded_to_executor', orderId });
797
+ }
798
+ } catch (err) {
799
+ log({ event: 'task_forward_error', orderId, error: err.message, stack: err.stack });
800
+ console.error('[ERROR] Task forward failed:', err);
801
+ }
802
+ })();
803
+ return;
804
+ }
805
+
806
+ // Order completed notification (requester side)
807
+ if (event === 'order_completed') {
808
+ const { orderId, executorDid, capabilityType, priceAmount, description, traceRoot, anchorTx } = payload;
809
+ log({ event: 'order_completed_notification', orderId, executorDid, capabilityType, priceAmount });
810
+
811
+ // Notify owner
812
+ if (ATEL_NOTIFY_GATEWAY && ATEL_NOTIFY_TARGET) {
813
+ try {
814
+ const desc = description ? description.slice(0, 150) : 'N/A';
815
+ const msg = `📦 Order ${orderId} completed!\nExecutor: ${(executorDid || '').slice(-12)}\nType: ${capabilityType}\nPrice: $${priceAmount || 0}\nTask: ${desc}\nTrace: ${(traceRoot || '').slice(0, 16)}...${anchorTx ? '\n⛓️ On-chain: ' + anchorTx : ''}`;
816
+ const token = (() => { try { return JSON.parse(readFileSync(join(process.env.HOME || '', '.openclaw/openclaw.json'), 'utf-8')).gateway?.auth?.token || ''; } catch { return ''; } })();
817
+ if (token) {
818
+ fetch(`${ATEL_NOTIFY_GATEWAY}/tools/invoke`, {
819
+ method: 'POST',
820
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
821
+ body: JSON.stringify({ tool: 'message', args: { action: 'send', message: msg, target: ATEL_NOTIFY_TARGET } }),
822
+ signal: AbortSignal.timeout(5000),
823
+ }).then(() => log({ event: 'order_notify_sent', orderId })).catch(e => log({ event: 'order_notify_failed', orderId, error: e.message }));
824
+ }
825
+ } catch (e) { log({ event: 'order_notify_error', orderId, error: e.message }); }
826
+ }
827
+
828
+ res.json({ status: 'received', orderId });
829
+ return;
830
+ }
831
+
832
+ // Unknown event type
833
+ res.json({ status: 'ignored', event });
834
+ });
835
+
836
+ // Result callback: POST /atel/v1/result (executor calls this when done)
837
+ endpoint.app?.post?.('/atel/v1/result', async (req, res) => {
838
+ const { taskId, result, success, trace: executorTrace } = req.body || {};
839
+ if (!taskId || !pendingTasks[taskId]) { res.status(404).json({ error: 'Unknown taskId' }); return; }
840
+ const task = pendingTasks[taskId];
841
+ const startTime = new Date(task.acceptedAt).getTime();
842
+ const durationMs = Date.now() - startTime;
843
+ enforcer.taskFinished();
844
+
845
+ // ── Execution Trace ──
846
+ let trace;
847
+ if (executorTrace && executorTrace.events && Array.isArray(executorTrace.events)) {
848
+ // Use executor-provided trace (from ToolGateway)
849
+ trace = new ExecutionTrace(taskId, id);
850
+ // Import events from executor trace
851
+ for (const event of executorTrace.events) {
852
+ trace.events.push(event);
853
+ }
854
+ log({ event: 'trace_imported', taskId, event_count: executorTrace.events.length, has_tool_calls: executorTrace.events.some(e => e.type === 'TOOL_CALL') });
855
+ } else {
856
+ // Fallback: simple trace (for executors without ToolGateway integration)
857
+ trace = new ExecutionTrace(taskId, id);
858
+ trace.append('TASK_RECEIVED', { from: task.from, action: task.action, encrypted: task.encrypted });
859
+ trace.append('POLICY_CHECK', { rateLimit: policy.rateLimit, maxConcurrent: policy.maxConcurrent, result: 'allowed' });
860
+ trace.append('CAPABILITY_CHECK', { action: task.action, capabilities: capTypes, result: 'allowed' });
861
+ trace.append('CONTENT_AUDIT', { result: 'passed' });
862
+ trace.append('TASK_FORWARDED', { executor_url: EXECUTOR_URL, timestamp: task.acceptedAt });
863
+ trace.append('EXECUTOR_RESULT', { success: success !== false, duration_ms: durationMs, result_size: JSON.stringify(result).length });
864
+ log({ event: 'trace_fallback', taskId, warning: 'Executor did not provide trace, using simple trace' });
865
+ }
866
+
867
+ // ── Rollback on failure ──
868
+ let rollbackReport = null;
869
+ if (success === false) {
870
+ trace.append('TASK_FAILED', { error: result?.error || 'Execution failed' });
871
+ const rollback = new RollbackManager();
872
+ // Register compensation: notify sender of failure
873
+ rollback.registerCompensation('Notify sender of task failure', async () => {
874
+ log({ event: 'rollback_notify', taskId, to: task.from, message: 'Task failed, compensating' });
875
+ });
876
+ // If executor reported side effects that need rollback
877
+ if (result?.sideEffects && Array.isArray(result.sideEffects)) {
878
+ for (const effect of result.sideEffects) {
879
+ rollback.registerCompensation(effect.description || 'Undo side effect', async () => {
880
+ if (effect.compensateUrl) {
881
+ await fetch(effect.compensateUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(effect.compensatePayload || {}), signal: AbortSignal.timeout(10000) });
882
+ }
883
+ });
884
+ }
885
+ }
886
+ rollbackReport = await rollback.rollback();
887
+ trace.append('ROLLBACK', { total: rollbackReport.total, succeeded: rollbackReport.succeeded, failed: rollbackReport.failed });
888
+ trace.fail(new Error(result?.error || 'Execution failed'));
889
+ log({ event: 'rollback_executed', taskId, total: rollbackReport.total, succeeded: rollbackReport.succeeded, failed: rollbackReport.failed });
890
+ } else {
891
+ trace.finalize(typeof result === 'object' ? result : { result });
892
+ }
893
+
894
+ // ── Save Trace (for audit requests) ──
895
+ saveTrace(taskId, trace);
896
+
897
+ // ── Proof Generation ──
898
+ const proofGen = new ProofGenerator(trace, id);
899
+ const proof = proofGen.generate(capTypes.join(',') || 'no-policy', `task-from-${task.from}`, JSON.stringify(result));
900
+
901
+ // ── On-chain Anchoring ──
902
+ const anchor = await anchorOnChain(proof.trace_root, { proof_id: proof.proof_id, executorDid: id.did, requesterDid: task.from, taskId, action: task.action }, task.chain);
903
+
904
+ // ── Trust Score Update (always, with or without anchor) ──
905
+ try {
906
+ if (anchor?.txHash) {
907
+ const proofRecord = {
908
+ traceRoot: proof.trace_root,
909
+ txHash: anchor.txHash,
910
+ chain: anchor.chain || 'solana',
911
+ executor: id.did,
912
+ taskFrom: task.from,
913
+ action: task.action,
914
+ success: success !== false,
915
+ durationMs,
916
+ riskLevel: 'low',
917
+ policyViolations: 0,
918
+ proofId: proof.proof_id,
919
+ timestamp: new Date().toISOString(),
920
+ verified: true,
921
+ };
922
+ trustScoreClient.addProofRecord(proofRecord);
923
+ _proofRecords.push(proofRecord);
924
+ } else {
925
+ // No anchor — still record as legacy summary so score accumulates
926
+ const summary = {
927
+ executor: id.did,
928
+ task_id: taskId,
929
+ task_type: task.action || 'general',
930
+ risk_level: 'low',
931
+ success: success !== false,
932
+ duration_ms: durationMs,
933
+ tool_calls: trace.events.filter(e => e.type === 'TOOL_CALL').length,
934
+ policy_violations: 0,
935
+ proof_id: proof.proof_id,
936
+ timestamp: new Date().toISOString(),
937
+ };
938
+ trustScoreClient.submitExecutionSummary(summary);
939
+ _summaries.push(summary);
940
+ log({ event: 'trust_score_updated_no_anchor', reason: 'No on-chain anchor — recorded as unverified. Set ATEL_SOLANA_PRIVATE_KEY for verified proofs.' });
941
+ }
942
+ const scoreReport = trustScoreClient.getAgentScore(id.did);
943
+ log({ event: 'trust_score_updated', did: id.did, score: scoreReport.trust_score, total_tasks: scoreReport.total_tasks, success_rate: scoreReport.success_rate, verified_count: scoreReport.verified_count });
944
+ saveTrustScores();
945
+
946
+ // Update score on Registry
947
+ try {
948
+ const { serializePayload } = await import('@lawrenceliang-btc/atel-sdk');
949
+ const ts = new Date().toISOString();
950
+ const scorePayload = { did: id.did, trustScore: scoreReport.trust_score };
951
+ const signable = serializePayload({ payload: scorePayload, did: id.did, timestamp: ts });
952
+ const { default: nacl } = await import('tweetnacl');
953
+ const sig = Buffer.from(nacl.sign.detached(Buffer.from(signable), id.secretKey)).toString('base64');
954
+ await fetch(`${REGISTRY_URL}/registry/v1/score/update`, {
955
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
956
+ body: JSON.stringify({ payload: scorePayload, did: id.did, timestamp: ts, signature: sig }),
957
+ signal: AbortSignal.timeout(5000),
958
+ });
959
+ } catch (e) { log({ event: 'score_registry_update_failed', error: e.message }); }
960
+ } catch (e) { log({ event: 'trust_score_error', error: e.message }); }
961
+
962
+ // ── Trust Graph Update ──
963
+ try {
964
+ const interaction = {
965
+ from: task.from,
966
+ to: id.did,
967
+ scene: task.action || 'general',
968
+ success: success !== false,
969
+ task_weight: calculateTaskWeight({
970
+ tool_calls: trace.events.filter(e => e.type === 'TOOL_CALL').length,
971
+ duration_ms: durationMs,
972
+ max_cost: 1,
973
+ risk_level: 'low',
974
+ similar_task_count: _interactions.filter(i => i.from === task.from && i.scene === (task.action || 'general')).length,
975
+ }),
976
+ duration_ms: durationMs,
977
+ };
978
+ trustGraph.recordInteraction(interaction);
979
+ _interactions.push(interaction);
980
+ saveTrustGraph();
981
+ const graphStats = trustGraph.getStats();
982
+ log({ event: 'trust_graph_updated', from: task.from, to: id.did, scene: task.action, nodes: graphStats.total_nodes, edges: graphStats.total_edges, interactions: graphStats.total_interactions });
983
+ } catch (e) { log({ event: 'trust_graph_error', error: e.message }); }
984
+
985
+ // ── Anchoring Warning ──
986
+ if (!anchor) {
987
+ log({ event: 'anchor_missing', taskId, warning: 'Proof not anchored on-chain. Set ATEL_SOLANA_PRIVATE_KEY for verifiable trust.', timestamp: new Date().toISOString() });
988
+ }
989
+
990
+ // ── Platform Complete (async, don't block) ──
991
+ if (ATEL_PLATFORM && taskId.startsWith('ord-')) {
992
+ (async () => {
993
+ try {
994
+ log({ event: 'platform_complete_starting', orderId: taskId, hasAnchor: !!anchor, chain: anchor?.chain });
995
+
996
+ const timestamp = new Date().toISOString();
997
+ const payload = {
998
+ proofBundle: proof,
999
+ traceRoot: proof.trace_root,
1000
+ anchorTx: anchor?.txHash || null,
1001
+ chain: anchor?.chain || null,
1002
+ traceEvents: trace.events, // Include trace events for verification
1003
+ };
1004
+ const signPayload = { did: id.did, timestamp, payload };
1005
+ const signature = sign(signPayload, id.secretKey);
1006
+
1007
+ const completeResp = await fetch(`${ATEL_PLATFORM}/trade/v1/order/${taskId}/complete`, {
1008
+ method: 'POST',
1009
+ headers: { 'Content-Type': 'application/json' },
1010
+ body: JSON.stringify({ did: id.did, timestamp, signature, payload }),
1011
+ signal: AbortSignal.timeout(10000),
1012
+ });
1013
+
1014
+ log({ event: 'platform_complete_response', orderId: taskId, status: completeResp.status, ok: completeResp.ok });
1015
+
1016
+ if (completeResp.ok) {
1017
+ log({ event: 'platform_complete_success', orderId: taskId });
1018
+ } else {
1019
+ const error = await completeResp.text();
1020
+ log({ event: 'platform_complete_failed', orderId: taskId, error, status: completeResp.status });
1021
+ console.error('[ERROR] Platform complete failed:', error);
1022
+ }
1023
+ } catch (err) {
1024
+ log({ event: 'platform_complete_error', orderId: taskId, error: err.message, stack: err.stack });
1025
+ console.error('[ERROR] Platform complete exception:', err);
1026
+ }
1027
+ })();
1028
+ }
1029
+
1030
+ log({ event: 'task_completed', taskId, from: task.from, action: task.action, success: success !== false, proof_id: proof.proof_id, trace_root: proof.trace_root, anchor_tx: anchor?.txHash || null, duration_ms: durationMs, timestamp: new Date().toISOString() });
1031
+
1032
+ // ── Notify owner (optional) ──
1033
+ if (ATEL_NOTIFY_GATEWAY && ATEL_NOTIFY_TARGET) {
1034
+ try {
1035
+ const resultText = typeof result === 'object' ? (result.response || JSON.stringify(result)).toString().slice(0, 300) : String(result).slice(0, 300);
1036
+ const status = success !== false ? '✅' : '❌';
1037
+ const msg = `${status} ATEL Task ${taskId}\nFrom: ${task.from.slice(-12)}\nAction: ${task.action}\nDuration: ${(durationMs/1000).toFixed(1)}s\nResult: ${resultText}`;
1038
+ const token = (() => { try { return JSON.parse(readFileSync(join(process.env.HOME || '', '.openclaw/openclaw.json'), 'utf-8')).gateway?.auth?.token || ''; } catch { return ''; } })();
1039
+ if (token) {
1040
+ fetch(`${ATEL_NOTIFY_GATEWAY}/tools/invoke`, {
1041
+ method: 'POST',
1042
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
1043
+ body: JSON.stringify({ tool: 'message', args: { action: 'send', message: msg, target: ATEL_NOTIFY_TARGET } }),
1044
+ signal: AbortSignal.timeout(5000),
1045
+ }).then(() => log({ event: 'notify_sent', taskId })).catch(e => log({ event: 'notify_failed', taskId, error: e.message }));
1046
+ }
1047
+ } catch (e) { log({ event: 'notify_error', taskId, error: e.message }); }
1048
+ }
1049
+
1050
+ log({ event: 'result_push_starting', taskId, hasSenderEndpoint: !!task.senderEndpoint, hasSenderCandidates: !!(task.senderCandidates?.length) });
1051
+
1052
+ // Push result back to sender
1053
+ // Re-lookup sender if we don't have their endpoint (e.g., lookup failed at accept time)
1054
+ try {
1055
+ if (!task.senderCandidates && !task.senderEndpoint) {
1056
+ try {
1057
+ const r = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(task.from)}`, { signal: AbortSignal.timeout(10000) });
1058
+ if (r.ok) {
1059
+ const data = await r.json();
1060
+ task.senderEndpoint = data.endpoint;
1061
+ task.senderCandidates = data.candidates;
1062
+ log({ event: 'sender_relookup_ok', taskId, endpoint: data.endpoint, candidates: data.candidates?.length || 0 });
1063
+ }
1064
+ } catch (e) { log({ event: 'sender_relookup_failed', taskId, error: e.message }); }
1065
+ }
1066
+
1067
+ if (task.senderCandidates || task.senderEndpoint) {
1068
+ try {
1069
+ // Determine connection type and target
1070
+ let targetUrl = task.senderEndpoint;
1071
+ let isRelay = false;
1072
+
1073
+ if (task.senderCandidates && task.senderCandidates.length > 0) {
1074
+ const conn = await connectToAgent(task.senderCandidates, task.from);
1075
+ if (conn) {
1076
+ targetUrl = conn.url;
1077
+ isRelay = conn.candidateType === 'relay';
1078
+ }
1079
+ }
1080
+ if (!targetUrl) throw new Error('No reachable endpoint');
1081
+
1082
+ const resultPayload = {
1083
+ taskId,
1084
+ status: success !== false ? 'completed' : 'failed',
1085
+ result,
1086
+ proof: { proof_id: proof.proof_id, trace_root: proof.trace_root, events_count: trace.events.length },
1087
+ anchor: anchor ? { chain: 'solana', txHash: anchor.txHash, block: anchor.blockNumber } : null,
1088
+ execution: { duration_ms: durationMs, encrypted: task.encrypted },
1089
+ rollback: rollbackReport ? { total: rollbackReport.total, succeeded: rollbackReport.succeeded, failed: rollbackReport.failed } : null,
1090
+ };
1091
+
1092
+ if (isRelay) {
1093
+ // Relay mode: use relay send API
1094
+ const relaySend = async (path, body) => {
1095
+ const resp = await fetch(targetUrl, {
1096
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1097
+ body: JSON.stringify({ method: 'POST', path, body, from: id.did }),
1098
+ signal: AbortSignal.timeout(60000),
1099
+ });
1100
+ if (!resp.ok) throw new Error(`Relay ${path} failed: ${resp.status}`);
1101
+ return resp.json();
1102
+ };
1103
+
1104
+ // Handshake via relay
1105
+ const hsManager = new HandshakeManager(id);
1106
+ const initMsg = hsManager.createInit(task.from);
1107
+ const ackMsg = await relaySend('/atel/v1/handshake', initMsg);
1108
+ const { confirm } = hsManager.processAck(ackMsg);
1109
+ await relaySend('/atel/v1/handshake', confirm);
1110
+
1111
+ // Send result via relay
1112
+ const msg = createMessage({ type: 'task-result', from: id.did, to: task.from, payload: resultPayload, secretKey: id.secretKey });
1113
+ await relaySend('/atel/v1/task', msg);
1114
+ } else {
1115
+ // Direct mode
1116
+ const client = new AgentClient(id);
1117
+ const hsManager = new HandshakeManager(id);
1118
+ await client.handshake(targetUrl, hsManager, task.from);
1119
+ const msg = createMessage({ type: 'task-result', from: id.did, to: task.from, payload: resultPayload, secretKey: id.secretKey });
1120
+ await client.sendTask(targetUrl, msg, hsManager);
1121
+ }
1122
+
1123
+ log({ event: 'result_pushed', taskId, to: task.from, via: targetUrl, relay: isRelay });
1124
+ } catch (e) { log({ event: 'result_push_failed', taskId, error: e.message }); }
1125
+ } else {
1126
+ log({ event: 'result_push_skipped', taskId, to: task.from, reason: 'No sender endpoint or candidates found — sender may not be reachable' });
1127
+ }
1128
+ } catch (pushErr) { log({ event: 'result_push_outer_error', taskId, error: pushErr.message, stack: pushErr.stack?.split('\n')[1]?.trim() }); }
1129
+
1130
+ delete pendingTasks[taskId]; saveTasks(pendingTasks);
1131
+ res.json({ status: 'ok', proof_id: proof.proof_id, anchor_tx: anchor?.txHash || null });
1132
+ });
1133
+
1134
+ // Task handler
1135
+ endpoint.onTask(async (message, session) => {
1136
+ const payload = message.payload || {};
1137
+
1138
+ // Ignore task-result messages (these are responses, not new tasks)
1139
+ if (message.type === 'task-result' || payload.status === 'completed' || payload.status === 'failed') {
1140
+ log({ event: 'result_received', type: 'task-result', from: message.from, taskId: payload.taskId, status: payload.status, proof: payload.proof || null, anchor: payload.anchor || null, execution: payload.execution || null, result: payload.result || null, timestamp: new Date().toISOString() });
1141
+ return { status: 'ok', message: 'Result received' };
1142
+ }
1143
+
1144
+ const action = payload.action || payload.type || 'unknown';
1145
+
1146
+ // ── Nonce anti-replay check ──
1147
+ const nonce = payload.nonce || message.nonce;
1148
+ if (nonce) {
1149
+ if (usedNonces.has(nonce)) {
1150
+ const rp = generateRejectionProof(message.from, action, 'Replay detected: nonce already used', 'REPLAY_REJECTED');
1151
+ log({ event: 'task_rejected', from: message.from, action, reason: 'Replay: duplicate nonce', nonce, timestamp: new Date().toISOString() });
1152
+ return { status: 'rejected', error: 'Replay detected: nonce already used', proof: rp };
1153
+ }
1154
+ usedNonces.add(nonce);
1155
+ saveNonces();
1156
+ }
1157
+
1158
+ // ── Protocol-level content audit (SDK layer) ──
1159
+ const auditor = new ContentAuditor();
1160
+ const auditResult = auditor.audit(payload, { action, from: message.from });
1161
+ if (!auditResult.safe) {
1162
+ const rp = generateRejectionProof(message.from, action, `Content audit: ${auditResult.reason}`, 'CONTENT_AUDIT_FAILED');
1163
+ log({ event: 'task_rejected', from: message.from, action, reason: `Content audit: ${auditResult.reason}`, severity: auditResult.severity, pattern: auditResult.pattern, timestamp: new Date().toISOString() });
1164
+ return { status: 'rejected', error: `Security: ${auditResult.reason}`, severity: auditResult.severity, proof: rp };
1165
+ }
1166
+
1167
+ // ── Policy check ──
1168
+ const pc = enforcer.check(message);
1169
+ if (!pc.allowed) {
1170
+ const rp = generateRejectionProof(message.from, action, pc.reason, 'POLICY_VIOLATION');
1171
+ log({ event: 'task_rejected', from: message.from, action, reason: pc.reason, timestamp: new Date().toISOString() });
1172
+ return { status: 'rejected', error: pc.reason, proof: rp };
1173
+ }
1174
+
1175
+ // ── Capability check (strict matching, no wildcards) ──
1176
+ if (capTypes.length > 0 && !capTypes.includes(action)) {
1177
+ const reason = `Outside capability: [${capTypes.join(',')}]`;
1178
+ const rp = generateRejectionProof(message.from, action, reason, 'CAPABILITY_REJECTED');
1179
+ log({ event: 'task_rejected', from: message.from, action, reason, timestamp: new Date().toISOString() });
1180
+ return { status: 'rejected', error: `Action "${action}" outside capability boundary`, capabilities: capTypes, proof: rp };
1181
+ }
1182
+
1183
+ // ── Task Mode Check (P2P) ──
1184
+ const currentPolicy = loadPolicy();
1185
+ const taskMode = currentPolicy.taskMode || 'auto';
1186
+
1187
+ if (taskMode === 'off') {
1188
+ const reason = 'Agent task mode is off — not accepting tasks';
1189
+ const rp = generateRejectionProof(message.from, action, reason, 'TASK_MODE_OFF');
1190
+ log({ event: 'task_mode_rejected', from: message.from, action, reason: 'task_mode_off', timestamp: new Date().toISOString() });
1191
+ return { status: 'rejected', error: reason, proof: rp };
1192
+ }
1193
+
1194
+ if (taskMode === 'confirm' || currentPolicy.autoAcceptP2P === false) {
1195
+ const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1196
+ // Queue for manual approval
1197
+ const pending = loadPending();
1198
+ pending[taskId] = {
1199
+ source: 'p2p',
1200
+ from: message.from,
1201
+ action,
1202
+ payload,
1203
+ price: 0,
1204
+ status: 'pending_confirm',
1205
+ receivedAt: new Date().toISOString(),
1206
+ encrypted: !!session?.encrypted,
1207
+ };
1208
+ savePending(pending);
1209
+ log({ event: 'task_queued', taskId, from: message.from, action, reason: taskMode === 'confirm' ? 'task_mode_confirm' : 'autoAcceptP2P_off', timestamp: new Date().toISOString() });
1210
+ return { status: 'queued', taskId, message: 'Task queued for manual confirmation. Use: atel approve ' + taskId };
1211
+ }
1212
+
1213
+ const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1214
+ enforcer.taskStarted();
1215
+
1216
+ // Lookup sender endpoint/candidates for result push-back
1217
+ let senderEndpoint = null;
1218
+ let senderCandidates = null;
1219
+ try {
1220
+ const r = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(message.from)}`);
1221
+ if (r.ok) {
1222
+ const data = await r.json();
1223
+ senderEndpoint = data.endpoint;
1224
+ senderCandidates = data.candidates;
1225
+ }
1226
+ } catch {}
1227
+
1228
+ pendingTasks[taskId] = { from: message.from, action, payload, senderEndpoint, senderCandidates, encrypted: !!session?.encrypted, acceptedAt: new Date().toISOString() };
1229
+ saveTasks(pendingTasks);
1230
+ log({ event: 'task_accepted', taskId, from: message.from, action, encrypted: !!session?.encrypted, timestamp: new Date().toISOString() });
1231
+
1232
+ // Forward to executor or echo
1233
+ if (EXECUTOR_URL) {
1234
+ fetch(EXECUTOR_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId, from: message.from, action, payload, encrypted: !!session?.encrypted, toolProxy: `http://127.0.0.1:${toolProxyPort}` }) }).catch(e => log({ event: 'executor_forward_failed', taskId, error: e.message }));
1235
+ return { status: 'accepted', taskId, message: 'Task accepted. Result will be pushed when ready.' };
1236
+ } else {
1237
+ // Echo mode
1238
+ enforcer.taskFinished();
1239
+ const trace = new ExecutionTrace(taskId, id);
1240
+ trace.append('TASK_ACCEPTED', { from: message.from, action, payload });
1241
+ const result = { status: 'no_executor', agent: id.agent_id, action, received_payload: payload };
1242
+ trace.append('TASK_ECHO', { result }); trace.finalize(result);
1243
+ saveTrace(taskId, trace);
1244
+ const proofGen = new ProofGenerator(trace, id);
1245
+ const proof = proofGen.generate(capTypes.join(',') || 'no-policy', `task-from-${message.from}`, JSON.stringify(result));
1246
+ const anchor = await anchorOnChain(proof.trace_root, { proof_id: proof.proof_id, task_from: message.from, action, taskId });
1247
+ const echoAcceptedAt = pendingTasks[taskId]?.acceptedAt;
1248
+ delete pendingTasks[taskId]; saveTasks(pendingTasks);
1249
+
1250
+ // ── Trust Score + Graph Update (echo mode) ──
1251
+ try {
1252
+ const echoSuccess = true;
1253
+ const echoDurationMs = Date.now() - new Date(echoAcceptedAt || Date.now()).getTime();
1254
+ if (anchor?.txHash) {
1255
+ const proofRecord = {
1256
+ traceRoot: proof.trace_root, txHash: anchor.txHash, chain: anchor.chain || 'solana',
1257
+ executor: id.did, taskFrom: message.from, action, success: echoSuccess,
1258
+ durationMs: echoDurationMs, riskLevel: 'low', policyViolations: 0,
1259
+ proofId: proof.proof_id, timestamp: new Date().toISOString(), verified: true,
1260
+ };
1261
+ trustScoreClient.addProofRecord(proofRecord);
1262
+ _proofRecords.push(proofRecord);
1263
+ } else {
1264
+ const summary = {
1265
+ executor: id.did, task_id: taskId, task_type: action || 'general',
1266
+ risk_level: 'low', success: echoSuccess, duration_ms: echoDurationMs,
1267
+ tool_calls: 0, policy_violations: 0, proof_id: proof.proof_id,
1268
+ timestamp: new Date().toISOString(),
1269
+ };
1270
+ trustScoreClient.submitExecutionSummary(summary);
1271
+ _summaries.push(summary);
1272
+ }
1273
+ saveTrustScores();
1274
+ const interaction = {
1275
+ from: message.from, to: id.did, scene: action || 'general',
1276
+ success: echoSuccess, task_weight: 0.1, duration_ms: echoDurationMs,
1277
+ };
1278
+ trustGraph.recordInteraction(interaction);
1279
+ _interactions.push(interaction);
1280
+ saveTrustGraph();
1281
+ } catch (e) { log({ event: 'trust_update_error_echo', error: e.message }); }
1282
+
1283
+ log({ event: 'task_completed', taskId, from: message.from, action, mode: 'echo', proof_id: proof.proof_id, anchor_tx: anchor?.txHash || null, timestamp: new Date().toISOString() });
1284
+ return { status: 'completed', taskId, result, proof, anchor };
1285
+ }
1286
+ });
1287
+
1288
+ endpoint.onProof(async (message) => { log({ event: 'proof_received', from: message.from, payload: message.payload, timestamp: new Date().toISOString() }); });
1289
+
1290
+ await endpoint.start();
1291
+
1292
+ // Auto-register to Registry with candidates
1293
+ if (capTypes.length > 0 && networkConfig.candidates.length > 0) {
1294
+ try {
1295
+ const regClient = new RegistryClient({ registryUrl: REGISTRY_URL });
1296
+ const bestDirect = networkConfig.candidates.find(c => c.type !== 'relay') || networkConfig.candidates[0];
1297
+ const discoverable = policy.discoverable !== false;
1298
+ const wallets = await getWalletAddresses();
1299
+ const preferredChain = detectPreferredChain();
1300
+ await regClient.register({
1301
+ name: id.agent_id,
1302
+ capabilities: caps,
1303
+ endpoint: bestDirect.url,
1304
+ candidates: networkConfig.candidates,
1305
+ discoverable,
1306
+ wallets,
1307
+ metadata: { preferredChain }
1308
+ }, id);
1309
+ log({ event: 'auto_registered', registry: REGISTRY_URL, candidates: networkConfig.candidates.length, discoverable, wallets: wallets ? Object.keys(wallets) : [], preferredChain });
1310
+ } catch (e) { log({ event: 'auto_register_failed', error: e.message }); }
1311
+ }
1312
+
1313
+ // Register with relay server and start polling for relayed requests
1314
+ const relayCandidate = networkConfig.candidates.find(c => c.type === 'relay');
1315
+ if (relayCandidate) {
1316
+ const relayUrl = relayCandidate.url;
1317
+
1318
+ // Register
1319
+ try {
1320
+ const resp = await fetch(`${relayUrl}/relay/v1/register`, {
1321
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1322
+ body: JSON.stringify({ did: id.did }), signal: AbortSignal.timeout(5000),
1323
+ });
1324
+ if (resp.ok) log({ event: 'relay_registered', relay: relayUrl });
1325
+ else log({ event: 'relay_register_failed', error: await resp.text() });
1326
+ } catch (e) { log({ event: 'relay_register_failed', error: e.message }); }
1327
+
1328
+ // Poll loop: check relay for incoming requests, forward to local endpoint
1329
+ const pollRelay = async () => {
1330
+ try {
1331
+ const resp = await fetch(`${relayUrl}/relay/v1/poll`, {
1332
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1333
+ body: JSON.stringify({ did: id.did }), signal: AbortSignal.timeout(5000),
1334
+ });
1335
+ if (!resp.ok) return;
1336
+ const { requests } = await resp.json();
1337
+ for (const req of requests) {
1338
+ // Forward to local endpoint
1339
+ try {
1340
+ const method = req.method || 'POST';
1341
+ const fetchOpts = {
1342
+ method,
1343
+ headers: { 'Content-Type': 'application/json' },
1344
+ signal: AbortSignal.timeout(30000),
1345
+ };
1346
+ if (method !== 'GET' && method !== 'HEAD') fetchOpts.body = JSON.stringify(req.body);
1347
+ const localResp = await fetch(`http://127.0.0.1:${p}${req.path}`, fetchOpts);
1348
+ const body = await localResp.json();
1349
+ // Send response back to relay
1350
+ await fetch(`${relayUrl}/relay/v1/respond`, {
1351
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1352
+ body: JSON.stringify({ requestId: req.requestId, status: localResp.status, body }),
1353
+ signal: AbortSignal.timeout(5000),
1354
+ });
1355
+ } catch (e) {
1356
+ // Send error response
1357
+ await fetch(`${relayUrl}/relay/v1/respond`, {
1358
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1359
+ body: JSON.stringify({ requestId: req.requestId, status: 500, body: { error: e.message } }),
1360
+ signal: AbortSignal.timeout(5000),
1361
+ }).catch(() => {});
1362
+ }
1363
+ }
1364
+ } catch {}
1365
+ };
1366
+
1367
+ // Poll every 2 seconds + re-register every 2 minutes
1368
+ setInterval(pollRelay, 2000);
1369
+ setInterval(async () => {
1370
+ try {
1371
+ await fetch(`${relayUrl}/relay/v1/register`, {
1372
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1373
+ body: JSON.stringify({ did: id.did }), signal: AbortSignal.timeout(5000),
1374
+ });
1375
+ } catch {}
1376
+ }, 120000);
1377
+ }
1378
+
1379
+ console.log(JSON.stringify({
1380
+ status: 'listening', agent_id: id.agent_id, did: id.did,
1381
+ port: p, candidates: networkConfig.candidates, capabilities: capTypes,
1382
+ policy: { rateLimit: policy.rateLimit, maxPayloadBytes: policy.maxPayloadBytes, maxConcurrent: policy.maxConcurrent, allowedDIDs: policy.allowedDIDs.length, blockedDIDs: policy.blockedDIDs.length },
1383
+ executor: EXECUTOR_URL || 'echo mode', inbox: INBOX_FILE,
1384
+ }, null, 2));
1385
+
1386
+ // ── Tunnel (optional) ──
1387
+ let tunnelManager = null;
1388
+ const tunnelType = process.env.ATEL_TUNNEL; // 'localtunnel' or 'ngrok'
1389
+ if (tunnelType) {
1390
+ const regClient = new RegistryClient({ registryUrl: REGISTRY_URL });
1391
+ tunnelManager = new TunnelManager(tunnelType, p, regClient, id);
1392
+ await tunnelManager.start();
1393
+ }
1394
+
1395
+ // ── Heartbeat ──
1396
+ const heartbeat = new HeartbeatManager(REGISTRY_URL, id);
1397
+ heartbeat.start();
1398
+
1399
+ process.on('SIGINT', async () => {
1400
+ heartbeat.stop();
1401
+ if (tunnelManager) await tunnelManager.stop();
1402
+ if (builtinExecutor) await builtinExecutor.stop();
1403
+ await endpoint.stop();
1404
+ process.exit(0);
1405
+ });
1406
+ process.on('SIGTERM', async () => {
1407
+ heartbeat.stop();
1408
+ if (tunnelManager) await tunnelManager.stop();
1409
+ if (builtinExecutor) await builtinExecutor.stop();
1410
+ await endpoint.stop();
1411
+ process.exit(0);
1412
+ });
1413
+
1414
+ // Global error handlers
1415
+ process.on('uncaughtException', (err) => {
1416
+ log({ event: 'uncaught_exception', error: err.message, stack: err.stack });
1417
+ console.error('[FATAL] Uncaught exception:', err);
1418
+ // Don't exit immediately, give time to log
1419
+ setTimeout(() => process.exit(1), 1000);
1420
+ });
1421
+
1422
+ process.on('unhandledRejection', (reason, promise) => {
1423
+ log({ event: 'unhandled_rejection', reason: String(reason), promise: String(promise) });
1424
+ console.error('[FATAL] Unhandled rejection:', reason);
1425
+ // Don't exit immediately, give time to log
1426
+ setTimeout(() => process.exit(1), 1000);
1427
+ });
1428
+ }
1429
+
1430
+ async function cmdInbox(count) {
1431
+ const n = parseInt(count || '20');
1432
+ if (!existsSync(INBOX_FILE)) { console.log(JSON.stringify({ messages: [], count: 0 })); return; }
1433
+ const lines = readFileSync(INBOX_FILE, 'utf-8').trim().split('\n').filter(Boolean);
1434
+ const messages = lines.slice(-n).map(l => JSON.parse(l));
1435
+ console.log(JSON.stringify({ messages, count: messages.length, total: lines.length }, null, 2));
1436
+ }
1437
+
1438
+ async function cmdRegister(name, capabilities, endpointUrl) {
1439
+ const id = requireIdentity();
1440
+ const policy = loadPolicy();
1441
+ const caps = (capabilities || 'general').split(',').map(c => ({ type: c.trim(), description: c.trim() }));
1442
+ saveCapabilities(caps);
1443
+ let ep = endpointUrl;
1444
+ if (!ep) { const net = loadNetwork(); ep = net?.endpoint || 'http://localhost:3100'; }
1445
+ const discoverable = policy.discoverable !== false;
1446
+ const client = new RegistryClient({ registryUrl: REGISTRY_URL });
1447
+ const entry = await client.register({ name: name || id.agent_id, capabilities: caps, endpoint: ep, discoverable }, id);
1448
+ console.log(JSON.stringify({ status: 'registered', did: entry.did, name: entry.name, capabilities: caps.map(c => c.type), endpoint: ep, discoverable, registry: REGISTRY_URL }, null, 2));
1449
+ }
1450
+
1451
+ async function cmdSearch(capability) {
1452
+ const client = new RegistryClient({ registryUrl: REGISTRY_URL });
1453
+ const result = await client.search({ type: capability, limit: 10 });
1454
+ console.log(JSON.stringify(result, null, 2));
1455
+ }
1456
+
1457
+ async function cmdHandshake(remoteEndpoint, remoteDid) {
1458
+ const id = requireIdentity();
1459
+ const client = new AgentClient(id);
1460
+ const hsManager = new HandshakeManager(id);
1461
+ const wallets = await getWalletAddresses();
1462
+ let did = remoteDid;
1463
+ if (!did) { const h = await client.health(remoteEndpoint); did = h.did; }
1464
+ const session = await client.handshake(remoteEndpoint, hsManager, did, wallets);
1465
+ console.log(JSON.stringify({ status: 'handshake_complete', sessionId: session.sessionId, remoteDid: did, encrypted: session.encrypted, remoteWalletsVerified: session.remoteWalletsVerified, remoteWallets: session.remoteWallets }, null, 2));
1466
+ const sf = resolve(ATEL_DIR, 'sessions.json');
1467
+ let sessions = {}; if (existsSync(sf)) sessions = JSON.parse(readFileSync(sf, 'utf-8'));
1468
+ sessions[remoteEndpoint] = { did, sessionId: session.sessionId, encrypted: session.encrypted };
1469
+ writeFileSync(sf, JSON.stringify(sessions, null, 2));
1470
+ }
1471
+
1472
+ async function cmdTask(target, taskJson) {
1473
+ const id = requireIdentity();
1474
+ const policy = loadPolicy();
1475
+ const tp = policy.trustPolicy || DEFAULT_POLICY.trustPolicy;
1476
+
1477
+ // Parse task payload and extract risk level
1478
+ const payload = typeof taskJson === 'string' ? JSON.parse(taskJson) : taskJson;
1479
+ const risk = payload._risk || 'low';
1480
+ delete payload._risk;
1481
+ const force = payload._force || false;
1482
+ delete payload._force;
1483
+
1484
+ let remoteEndpoint = target;
1485
+ let remoteDid;
1486
+ let connectionType = 'direct';
1487
+
1488
+ // If target looks like a DID or name, search Registry and try candidates
1489
+ if (!target.startsWith('http')) {
1490
+ const regClient = new RegistryClient({ registryUrl: REGISTRY_URL });
1491
+ let entry;
1492
+ try {
1493
+ const resp = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(target)}`);
1494
+ if (resp.ok) entry = await resp.json();
1495
+ } catch {}
1496
+ if (!entry) {
1497
+ const results = await regClient.search({ type: target, limit: 5 });
1498
+ if (results.length > 0) entry = results[0];
1499
+ }
1500
+ if (!entry) { console.error(`Agent not found: ${target}`); process.exit(1); }
1501
+
1502
+ remoteDid = entry.did;
1503
+
1504
+ // ── Pre-task trust check (unified) ──
1505
+ const trustResult = checkTrust(remoteDid, risk, policy, force);
1506
+ if (!trustResult.passed) {
1507
+ console.log(JSON.stringify({ status: 'blocked', ...trustResult }));
1508
+ process.exit(1);
1509
+ }
1510
+ if (!force) {
1511
+ console.log(JSON.stringify({ event: 'trust_check_passed', did: remoteDid, risk, score: trustResult.score, level: trustResult.level, level_name: trustResult.levelName }));
1512
+ }
1513
+
1514
+ // Try candidates if available
1515
+ if (entry.candidates && entry.candidates.length > 0) {
1516
+ console.log(JSON.stringify({ event: 'connecting', did: remoteDid, candidates: entry.candidates.length }));
1517
+ const conn = await connectToAgent(entry.candidates, remoteDid);
1518
+ if (conn) {
1519
+ remoteEndpoint = conn.url;
1520
+ connectionType = conn.candidateType;
1521
+ console.log(JSON.stringify({ event: 'connected', type: conn.candidateType, url: conn.url, latencyMs: conn.latencyMs }));
1522
+ } else {
1523
+ console.error('All candidates unreachable'); process.exit(1);
1524
+ }
1525
+ } else {
1526
+ remoteEndpoint = entry.endpoint;
1527
+ }
1528
+
1529
+ // Try candidates if available
1530
+ if (entry.candidates && entry.candidates.length > 0) {
1531
+ console.log(JSON.stringify({ event: 'connecting', did: remoteDid, candidates: entry.candidates.length }));
1532
+ const conn = await connectToAgent(entry.candidates, remoteDid);
1533
+ if (conn) {
1534
+ remoteEndpoint = conn.url;
1535
+ connectionType = conn.candidateType;
1536
+ console.log(JSON.stringify({ event: 'connected', type: conn.candidateType, url: conn.url, latencyMs: conn.latencyMs }));
1537
+ } else {
1538
+ console.error('All candidates unreachable'); process.exit(1);
1539
+ }
1540
+ } else {
1541
+ remoteEndpoint = entry.endpoint;
1542
+ }
1543
+ }
1544
+
1545
+ // ── Helper: update local trust history after task ──
1546
+ function updateTrustHistory(did, success, proofInfo) {
1547
+ const localHistoryFile = resolve(ATEL_DIR, 'trust-history.json');
1548
+ let history = {};
1549
+ try { history = JSON.parse(readFileSync(localHistoryFile, 'utf-8')); } catch {}
1550
+ if (!history[did]) history[did] = { tasks: 0, successes: 0, failures: 0, lastSeen: null, proofs: [] };
1551
+ history[did].tasks++;
1552
+ if (success) history[did].successes++; else history[did].failures++;
1553
+ history[did].lastSeen = new Date().toISOString();
1554
+ if (proofInfo) history[did].proofs.push(proofInfo);
1555
+ writeFileSync(localHistoryFile, JSON.stringify(history, null, 2));
1556
+ }
1557
+
1558
+ if (connectionType === 'relay') {
1559
+ // Relay mode: all requests go through relay's /relay/v1/send/:did API
1560
+ const relayUrl = remoteEndpoint; // e.g. http://47.251.8.19:9000/relay/v1/send/did:atel:xxx
1561
+
1562
+ async function relaySend(path, body) {
1563
+ const resp = await fetch(relayUrl, {
1564
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1565
+ body: JSON.stringify({ method: 'POST', path, body, from: id.did }),
1566
+ signal: AbortSignal.timeout(60000),
1567
+ });
1568
+ if (!resp.ok) {
1569
+ const err = await resp.json().catch(() => ({}));
1570
+ throw new Error(`Relay request to ${path} failed: ${resp.status} ${JSON.stringify(err)}`);
1571
+ }
1572
+ return resp.json();
1573
+ }
1574
+
1575
+ // Step 1: handshake init
1576
+ const hsManager = new HandshakeManager(id);
1577
+ const initMsg = hsManager.createInit(remoteDid);
1578
+ const ackMsg = await relaySend('/atel/v1/handshake', initMsg);
1579
+
1580
+ // Step 2: handshake confirm
1581
+ const { confirm } = hsManager.processAck(ackMsg);
1582
+ await relaySend('/atel/v1/handshake', confirm);
1583
+
1584
+ // Step 3: send task
1585
+ const msg = createMessage({ type: 'task', from: id.did, to: remoteDid, payload, secretKey: id.secretKey });
1586
+ const relayAck = await relaySend('/atel/v1/task', msg);
1587
+
1588
+ console.log(JSON.stringify({ status: 'task_sent', remoteDid, via: 'relay', relay_ack: relayAck, note: 'Relay mode is async. Waiting for result (up to 120s)...' }));
1589
+
1590
+ // Wait for result to arrive in inbox (poll for task-result)
1591
+ // Extract taskId from relay ack (assigned by remote agent), fallback to msg fields
1592
+ const taskId = relayAck?.result?.taskId || msg.id || msg.payload?.taskId;
1593
+ let result = null;
1594
+ const waitStart = Date.now();
1595
+ const WAIT_TIMEOUT = 120000; // 2 minutes
1596
+ while (Date.now() - waitStart < WAIT_TIMEOUT) {
1597
+ await new Promise(r => setTimeout(r, 3000)); // poll every 3s
1598
+ if (existsSync(INBOX_FILE)) {
1599
+ const lines = readFileSync(INBOX_FILE, 'utf-8').split('\n').filter(l => l.trim());
1600
+ for (let i = lines.length - 1; i >= 0; i--) {
1601
+ try {
1602
+ const entry = JSON.parse(lines[i]);
1603
+ // Match by taskId first (precise), fallback to from+event (legacy)
1604
+ if (entry.event === 'result_received' && entry.from === remoteDid && (!taskId || entry.taskId === taskId)) {
1605
+ result = { taskId: entry.taskId, status: entry.status, result: entry.result, proof: entry.proof, anchor: entry.anchor, execution: entry.execution };
1606
+ break;
1607
+ }
1608
+ } catch {}
1609
+ }
1610
+ if (result) break;
1611
+ }
1612
+ }
1613
+
1614
+ if (result) {
1615
+ console.log(JSON.stringify({ status: 'task_completed', remoteDid, via: 'relay', result }, null, 2));
1616
+ } else {
1617
+ console.log(JSON.stringify({ status: 'task_sent_no_result', remoteDid, via: 'relay', note: 'Result not received within timeout. Check: atel inbox' }, null, 2));
1618
+ }
1619
+
1620
+ // Update local trust history
1621
+ const success = result?.status === 'completed' || (result && result?.status !== 'rejected' && result?.status !== 'failed');
1622
+ const proofInfo = result?.proof ? { proof_id: result.proof.proof_id, trace_root: result.proof.trace_root, verified: !!result?.anchor?.txHash, anchor_tx: result?.anchor?.txHash || null, timestamp: new Date().toISOString() } : null;
1623
+ if (remoteDid) updateTrustHistory(remoteDid, success, proofInfo);
1624
+ } else {
1625
+ // Direct mode: standard handshake + task
1626
+ const client = new AgentClient(id);
1627
+ const hsManager = new HandshakeManager(id);
1628
+ const sf = resolve(ATEL_DIR, 'sessions.json');
1629
+ if (!remoteDid) {
1630
+ const h = await client.health(remoteEndpoint); remoteDid = h.did;
1631
+ }
1632
+
1633
+ // Trust check for direct mode too (unified)
1634
+ if (!force && remoteDid) {
1635
+ const trustResult = checkTrust(remoteDid, risk, policy, false);
1636
+ if (!trustResult.passed) {
1637
+ console.log(JSON.stringify({ status: 'blocked', ...trustResult }));
1638
+ process.exit(1);
1639
+ }
1640
+ }
1641
+ await client.handshake(remoteEndpoint, hsManager, remoteDid);
1642
+ let sessions = {}; if (existsSync(sf)) sessions = JSON.parse(readFileSync(sf, 'utf-8'));
1643
+ sessions[remoteEndpoint] = { did: remoteDid };
1644
+ writeFileSync(sf, JSON.stringify(sessions, null, 2));
1645
+
1646
+ const msg = createMessage({ type: 'task', from: id.did, to: remoteDid, payload, secretKey: id.secretKey });
1647
+ const result = await client.sendTask(remoteEndpoint, msg, hsManager);
1648
+ console.log(JSON.stringify({ status: 'task_sent', remoteDid, via: remoteEndpoint, result }, null, 2));
1649
+
1650
+ // Update local trust history
1651
+ const success = result?.status !== 'rejected' && result?.status !== 'failed';
1652
+ const proofInfo = result?.proof ? { proof_id: result.proof.proof_id, trace_root: result.proof.trace_root, verified: !!result?.anchor?.txHash, anchor_tx: result?.anchor?.txHash || null, timestamp: new Date().toISOString() } : null;
1653
+ if (remoteDid) updateTrustHistory(remoteDid, success, proofInfo);
1654
+ }
1655
+ }
1656
+
1657
+ async function cmdResult(taskId, resultJson) {
1658
+ const result = typeof resultJson === 'string' ? JSON.parse(resultJson) : resultJson;
1659
+ const resp = await fetch(`http://localhost:${process.env.ATEL_PORT || '3100'}/atel/v1/result`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId, result, success: true }) });
1660
+ console.log(JSON.stringify(await resp.json(), null, 2));
1661
+ }
1662
+
1663
+ // ─── Trust Verification Commands ─────────────────────────────────
1664
+
1665
+ async function cmdCheck(targetDid, riskLevel, options) {
1666
+ const risk = riskLevel || 'low';
1667
+ const chainMode = options?.chain || !!process.env.ATEL_SOLANA_RPC_URL;
1668
+ const policy = loadPolicy();
1669
+ const tp = policy.trustPolicy || DEFAULT_POLICY.trustPolicy;
1670
+
1671
+ console.log(JSON.stringify({ event: 'checking_trust', did: targetDid, risk, mode: chainMode ? 'chain-verified' : 'local-only' }));
1672
+
1673
+ // 1. Get Registry info (reference only, includes wallets)
1674
+ let registryScore = null;
1675
+ let agentName = null;
1676
+ let peerWallets = null;
1677
+ try {
1678
+ const r = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(targetDid)}`, { signal: AbortSignal.timeout(5000) });
1679
+ if (r.ok) {
1680
+ const d = await r.json();
1681
+ registryScore = d.trustScore;
1682
+ agentName = d.name;
1683
+ if (d.wallets) peerWallets = d.wallets;
1684
+ }
1685
+ } catch {}
1686
+
1687
+ // 2. Local interaction history
1688
+ const localHistoryFile = resolve(ATEL_DIR, 'trust-history.json');
1689
+ let history = {};
1690
+ try { history = JSON.parse(readFileSync(localHistoryFile, 'utf-8')); } catch {}
1691
+ const agentHistory = history[targetDid] || { tasks: 0, successes: 0, failures: 0, lastSeen: null, proofs: [] };
1692
+
1693
+ // 3. Chain-verified mode: query all three chains by wallet address
1694
+ let chainVerification = null;
1695
+ if (chainMode) {
1696
+ const chainResults = { solana: null, base: null, bsc: null, totalRecords: 0, matchingDid: 0 };
1697
+
1698
+ // 3a. Verify unverified local proofs on-chain
1699
+ const unverifiedProofs = agentHistory.proofs.filter(p => !p.verified && p.anchor_tx);
1700
+ if (unverifiedProofs.length > 0) {
1701
+ console.log(JSON.stringify({ event: 'verifying_local_proofs', count: unverifiedProofs.length }));
1702
+ const result = await verifyAnchorTxList(unverifiedProofs, targetDid);
1703
+ for (const vp of result.proofs) {
1704
+ const existing = agentHistory.proofs.find(p => p.anchor_tx === vp.anchor_tx);
1705
+ if (existing) existing.verified = true;
1706
+ }
1707
+ history[targetDid] = agentHistory;
1708
+ try { writeFileSync(localHistoryFile, JSON.stringify(history, null, 2)); } catch {}
1709
+ }
1710
+
1711
+ // 3b. Query peer's wallet addresses on all three chains
1712
+ if (peerWallets) {
1713
+ console.log(JSON.stringify({ event: 'querying_chain_history', wallets: peerWallets }));
1714
+
1715
+ // Solana
1716
+ if (peerWallets.solana) {
1717
+ try {
1718
+ const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
1719
+ const provider = new SolanaAnchorProvider({ rpcUrl });
1720
+ const records = await provider.queryByWallet(peerWallets.solana, { limit: 100, filterDid: targetDid });
1721
+ chainResults.solana = { wallet: peerWallets.solana, records: records.length, asExecutor: records.filter(r => r.executorDid === targetDid).length, asRequester: records.filter(r => r.requesterDid === targetDid).length };
1722
+ chainResults.totalRecords += records.length;
1723
+ chainResults.matchingDid += records.length;
1724
+ } catch (e) { chainResults.solana = { error: e.message }; }
1725
+ }
1726
+
1727
+ // Base
1728
+ if (peerWallets.base) {
1729
+ try {
1730
+ const { BaseAnchorProvider } = await import('@lawrenceliang-btc/atel-sdk');
1731
+ const baseRpc = process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org';
1732
+ const provider = new BaseAnchorProvider({ rpcUrl: baseRpc });
1733
+ const explorerApi = process.env.ATEL_BASE_EXPLORER_API || 'https://api.basescan.org/api';
1734
+ const apiKey = process.env.ATEL_BASE_EXPLORER_KEY;
1735
+ const records = await provider.queryByWallet(peerWallets.base, explorerApi, apiKey, { limit: 100, filterDid: targetDid });
1736
+ chainResults.base = { wallet: peerWallets.base, records: records.length, asExecutor: records.filter(r => r.executorDid === targetDid).length, asRequester: records.filter(r => r.requesterDid === targetDid).length };
1737
+ chainResults.totalRecords += records.length;
1738
+ chainResults.matchingDid += records.length;
1739
+ } catch (e) { chainResults.base = { error: e.message }; }
1740
+ }
1741
+
1742
+ // BSC
1743
+ if (peerWallets.bsc) {
1744
+ try {
1745
+ const { BSCAnchorProvider } = await import('@lawrenceliang-btc/atel-sdk');
1746
+ const bscRpc = process.env.ATEL_BSC_RPC_URL || 'https://bsc-dataseed.binance.org';
1747
+ const provider = new BSCAnchorProvider({ rpcUrl: bscRpc });
1748
+ const explorerApi = process.env.ATEL_BSC_EXPLORER_API || 'https://api.bscscan.com/api';
1749
+ const apiKey = process.env.ATEL_BSC_EXPLORER_KEY;
1750
+ const records = await provider.queryByWallet(peerWallets.bsc, explorerApi, apiKey, { limit: 100, filterDid: targetDid });
1751
+ chainResults.bsc = { wallet: peerWallets.bsc, records: records.length, asExecutor: records.filter(r => r.executorDid === targetDid).length, asRequester: records.filter(r => r.requesterDid === targetDid).length };
1752
+ chainResults.totalRecords += records.length;
1753
+ chainResults.matchingDid += records.length;
1754
+ } catch (e) { chainResults.bsc = { error: e.message }; }
1755
+ }
1756
+ }
1757
+
1758
+ chainVerification = chainResults;
1759
+ }
1760
+
1761
+ // 4. Compute unified trust score and level
1762
+ const computedScore = computeTrustScore(agentHistory);
1763
+ const trustLevel = computeTrustLevel(computedScore);
1764
+
1765
+ // 5. Apply trust policy
1766
+ const threshold = tp.riskThresholds?.[risk] ?? 0;
1767
+ const effectiveScore = computedScore > 0 ? computedScore : (registryScore || 0);
1768
+ const isNewAgent = agentHistory.tasks === 0;
1769
+ let decision = 'allow';
1770
+ let reason = '';
1771
+
1772
+ if (isNewAgent) {
1773
+ if (tp.newAgentPolicy === 'deny') { decision = 'deny'; reason = 'New agent, policy denies unknown agents'; }
1774
+ else if (tp.newAgentPolicy === 'allow_low_risk' && (risk === 'high' || risk === 'critical')) { decision = 'deny'; reason = `New agent, policy only allows low risk (requested: ${risk})`; }
1775
+ else { decision = 'allow'; reason = `New agent, policy: ${tp.newAgentPolicy}`; }
1776
+ } else if (effectiveScore < threshold) {
1777
+ decision = 'deny';
1778
+ reason = `Score ${effectiveScore} below threshold ${threshold} for ${risk} risk`;
1779
+ } else if (!riskAllowed(trustLevel.maxRisk, risk)) {
1780
+ decision = 'deny';
1781
+ reason = `Trust level ${trustLevel.level} (${trustLevel.name}) only allows up to ${trustLevel.maxRisk} risk, requested ${risk}`;
1782
+ } else {
1783
+ decision = 'allow';
1784
+ reason = `Score ${effectiveScore} meets threshold ${threshold} for ${risk} risk`;
1785
+ }
1786
+
1787
+ const output = {
1788
+ did: targetDid,
1789
+ name: agentName,
1790
+ mode: chainMode ? 'chain-verified' : 'local-only',
1791
+ trust: {
1792
+ computed_score: computedScore,
1793
+ registry_score: registryScore,
1794
+ effective_score: effectiveScore,
1795
+ level: trustLevel.level,
1796
+ level_name: trustLevel.name,
1797
+ max_risk: trustLevel.maxRisk,
1798
+ total_tasks: agentHistory.tasks,
1799
+ successes: agentHistory.successes,
1800
+ failures: agentHistory.failures,
1801
+ verified_proofs: agentHistory.proofs.filter(p => p.verified).length,
1802
+ total_proofs: agentHistory.proofs.length,
1803
+ },
1804
+ policy: { risk, threshold, decision, reason },
1805
+ };
1806
+ if (chainVerification) output.chain_verification = chainVerification;
1807
+ if (!chainMode) output.note = 'Local-only mode: score based on direct interaction history only. Set ATEL_SOLANA_RPC_URL or use --chain for on-chain verification.';
1808
+
1809
+ console.log(JSON.stringify(output, null, 2));
1810
+ }
1811
+
1812
+ async function cmdVerifyProof(anchorTx, traceRoot) {
1813
+ if (!anchorTx || !traceRoot) { console.error('Usage: atel verify-proof <anchor_tx> <trace_root>'); process.exit(1); }
1814
+
1815
+ console.log(JSON.stringify({ event: 'verifying_proof', anchor_tx: anchorTx, trace_root: traceRoot }));
1816
+
1817
+ const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
1818
+ try {
1819
+ const provider = new SolanaAnchorProvider({ rpcUrl });
1820
+ const result = await provider.verify(traceRoot, anchorTx);
1821
+ console.log(JSON.stringify({
1822
+ verified: result.valid,
1823
+ chain: 'solana',
1824
+ anchor_tx: anchorTx,
1825
+ trace_root: traceRoot,
1826
+ detail: result.detail || (result.valid ? 'Memo matches trace_root' : 'Memo does not match'),
1827
+ block: result.blockNumber,
1828
+ timestamp: result.timestamp,
1829
+ }, null, 2));
1830
+ } catch (e) {
1831
+ console.log(JSON.stringify({ verified: false, error: e.message }));
1832
+ }
1833
+ }
1834
+
1835
+ async function cmdAudit(targetDidOrUrl, taskId) {
1836
+ if (!targetDidOrUrl || !taskId) { console.error('Usage: atel audit <did_or_endpoint> <taskId>'); process.exit(1); }
1837
+
1838
+ const id = requireIdentity();
1839
+
1840
+ // Resolve endpoint
1841
+ let endpoint = targetDidOrUrl;
1842
+ let connectionType = 'direct';
1843
+ if (targetDidOrUrl.startsWith('did:')) {
1844
+ try {
1845
+ const r = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(targetDidOrUrl)}`, { signal: AbortSignal.timeout(5000) });
1846
+ if (r.ok) {
1847
+ const d = await r.json();
1848
+ if (d.candidates && d.candidates.length > 0) {
1849
+ const conn = await connectToAgent(d.candidates, targetDidOrUrl);
1850
+ if (conn) { endpoint = conn.url; connectionType = conn.candidateType; }
1851
+ }
1852
+ if (endpoint === targetDidOrUrl && d.endpoint) endpoint = d.endpoint;
1853
+ }
1854
+ } catch {}
1855
+ }
1856
+
1857
+ if (endpoint.startsWith('did:')) { console.error('Could not resolve endpoint for DID'); process.exit(1); }
1858
+
1859
+ console.log(JSON.stringify({ event: 'auditing', target: endpoint, taskId, via: connectionType }));
1860
+
1861
+ try {
1862
+ let traceData;
1863
+ if (connectionType === 'relay') {
1864
+ // Relay mode: send GET-like request through relay
1865
+ const resp = await fetch(endpoint, {
1866
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1867
+ body: JSON.stringify({ method: 'GET', path: `/atel/v1/trace/${taskId}`, from: id.did }),
1868
+ signal: AbortSignal.timeout(30000),
1869
+ });
1870
+ if (!resp.ok) { console.log(JSON.stringify({ audit: 'failed', error: `Relay trace fetch failed: ${resp.status}` })); return; }
1871
+ traceData = await resp.json();
1872
+ } else {
1873
+ // Direct mode
1874
+ const traceUrl = endpoint.replace(/\/$/, '') + `/atel/v1/trace/${taskId}`;
1875
+ const resp = await fetch(traceUrl, { signal: AbortSignal.timeout(10000) });
1876
+ if (!resp.ok) { console.log(JSON.stringify({ audit: 'failed', error: `Trace fetch failed: ${resp.status}` })); return; }
1877
+ traceData = await resp.json();
1878
+ }
1879
+
1880
+ // Verify hash chain
1881
+ const events = traceData.events || [];
1882
+ let chainValid = true;
1883
+ const chainErrors = [];
1884
+ for (let i = 0; i < events.length; i++) {
1885
+ const e = events[i];
1886
+ const expectedPrev = i === 0 ? '0x00' : events[i - 1].hash;
1887
+ if (e.prev !== expectedPrev) {
1888
+ chainValid = false;
1889
+ chainErrors.push(`Event #${e.seq}: prev mismatch (expected ${expectedPrev}, got ${e.prev})`);
1890
+ }
1891
+ }
1892
+
1893
+ // Recompute merkle root
1894
+ const { createHash } = await import('node:crypto');
1895
+ const hashes = events.map(e => e.hash);
1896
+ let level = [...hashes];
1897
+ while (level.length > 1) {
1898
+ const next = [];
1899
+ for (let i = 0; i < level.length; i += 2) {
1900
+ const left = level[i];
1901
+ const right = i + 1 < level.length ? level[i + 1] : left;
1902
+ next.push(createHash('sha256').update(left + right).digest('hex'));
1903
+ }
1904
+ level = next;
1905
+ }
1906
+ const computedRoot = level[0] || '';
1907
+
1908
+ console.log(JSON.stringify({
1909
+ audit: 'complete',
1910
+ taskId,
1911
+ agent: traceData.agent,
1912
+ events_count: events.length,
1913
+ hash_chain_valid: chainValid,
1914
+ chain_errors: chainErrors,
1915
+ computed_merkle_root: computedRoot,
1916
+ event_types: events.map(e => e.type),
1917
+ }, null, 2));
1918
+ } catch (e) {
1919
+ console.log(JSON.stringify({ audit: 'failed', error: e.message }));
1920
+ }
1921
+ }
1922
+
1923
+ // ─── Key Rotation ────────────────────────────────────────────────
1924
+
1925
+ async function cmdRotate() {
1926
+ const oldId = requireIdentity();
1927
+ const oldDid = oldId.did;
1928
+
1929
+ // Backup old identity
1930
+ const backupFile = resolve(ATEL_DIR, `identity.backup.${Date.now()}.json`);
1931
+ writeFileSync(backupFile, readFileSync(IDENTITY_FILE, 'utf-8'));
1932
+
1933
+ // Rotate
1934
+ const { newIdentity, proof } = rotateKey(oldId);
1935
+ saveIdentity(newIdentity);
1936
+
1937
+ // Save rotation proof
1938
+ const proofsDir = resolve(ATEL_DIR, 'rotation-proofs');
1939
+ if (!existsSync(proofsDir)) mkdirSync(proofsDir, { recursive: true });
1940
+ writeFileSync(resolve(proofsDir, `${Date.now()}.json`), JSON.stringify(proof, null, 2));
1941
+
1942
+ // Anchor rotation on-chain if possible
1943
+ let anchor = null;
1944
+ const key = process.env.ATEL_SOLANA_PRIVATE_KEY;
1945
+ if (key) {
1946
+ try {
1947
+ const s = new SolanaAnchorProvider({ rpcUrl: process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com', privateKey: key });
1948
+ const { createHash } = await import('node:crypto');
1949
+ const rotationHash = createHash('sha256').update(JSON.stringify(proof)).digest('hex');
1950
+ anchor = await s.anchor(`rotation:${rotationHash}`, { oldDid, newDid: newIdentity.did, type: 'key_rotation' });
1951
+ } catch (e) { console.log(JSON.stringify({ warning: 'On-chain anchor failed', error: e.message })); }
1952
+ }
1953
+
1954
+ // Update Registry
1955
+ try {
1956
+ const regClient = new RegistryClient({ registryUrl: REGISTRY_URL });
1957
+ const caps = loadCapabilities();
1958
+ const net = loadNetwork();
1959
+ const policy = loadPolicy();
1960
+ const ep = net?.endpoint || 'http://localhost:3100';
1961
+ const discoverable = policy.discoverable !== false;
1962
+ await regClient.register({ name: newIdentity.agent_id, capabilities: caps, endpoint: ep, candidates: net?.candidates || [], discoverable }, newIdentity);
1963
+ console.log(JSON.stringify({ event: 'registry_updated', newDid: newIdentity.did }));
1964
+ } catch (e) { console.log(JSON.stringify({ warning: 'Registry update failed', error: e.message })); }
1965
+
1966
+ console.log(JSON.stringify({
1967
+ status: 'rotated',
1968
+ oldDid,
1969
+ newDid: newIdentity.did,
1970
+ backup: backupFile,
1971
+ proof_valid: verifyKeyRotation(proof),
1972
+ anchor: anchor ? { chain: 'solana', txHash: anchor.txHash } : null,
1973
+ next: 'Restart endpoint: atel start [port]',
1974
+ }, null, 2));
1975
+ }
1976
+
1977
+ // ─── Platform API Helpers ────────────────────────────────────────
1978
+
1979
+ const PLATFORM_URL = process.env.ATEL_PLATFORM || process.env.ATEL_REGISTRY || 'https://api.atelai.org';
1980
+
1981
+ async function signedFetch(method, path, payload = {}) {
1982
+ const id = requireIdentity();
1983
+ const { default: nacl } = await import('tweetnacl');
1984
+ const { serializePayload } = await import('@lawrenceliang-btc/atel-sdk');
1985
+ const ts = new Date().toISOString();
1986
+ const signable = serializePayload({ payload, did: id.did, timestamp: ts });
1987
+ const sig = Buffer.from(nacl.sign.detached(Buffer.from(signable), id.secretKey)).toString('base64');
1988
+ const body = JSON.stringify({ did: id.did, payload, timestamp: ts, signature: sig });
1989
+ // Always use POST for signed requests (DIDAuth reads body, GET cannot have body)
1990
+ const res = await fetch(`${PLATFORM_URL}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body });
1991
+ const data = await res.json();
1992
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
1993
+ return data;
1994
+ }
1995
+
1996
+ // ─── Account Commands ────────────────────────────────────────────
1997
+
1998
+ async function cmdBalance() {
1999
+ const data = await signedFetch('GET', '/account/v1/balance');
2000
+ console.log(JSON.stringify(data, null, 2));
2001
+ }
2002
+
2003
+ async function cmdDeposit(amount, channel) {
2004
+ if (!amount || isNaN(amount)) { console.error('Usage: atel deposit <amount> [channel]'); process.exit(1); }
2005
+ const data = await signedFetch('POST', '/account/v1/deposit', { amount: parseFloat(amount), channel: channel || 'manual' });
2006
+ console.log(JSON.stringify(data, null, 2));
2007
+ }
2008
+
2009
+ async function cmdWithdraw(amount, channel, address) {
2010
+ if (!amount || isNaN(amount)) { console.error('Usage: atel withdraw <amount> [channel] [address]'); process.exit(1); }
2011
+ if (channel && channel.startsWith('crypto_') && !address) {
2012
+ console.error('Error: recipient wallet address required for crypto withdrawal');
2013
+ console.error('Usage: atel withdraw <amount> crypto_base <your_wallet_address>');
2014
+ process.exit(1);
2015
+ }
2016
+ const data = await signedFetch('POST', '/account/v1/withdraw', { amount: parseFloat(amount), channel: channel || 'manual', address: address || '' });
2017
+ console.log(JSON.stringify(data, null, 2));
2018
+ }
2019
+
2020
+ async function cmdTransactions() {
2021
+ const data = await signedFetch('GET', '/account/v1/transactions');
2022
+ console.log(JSON.stringify(data, null, 2));
2023
+ }
2024
+
2025
+ // ─── Trade Commands ──────────────────────────────────────────────
2026
+
2027
+ // ─── Trade Task: High-level one-shot command ────────────────────
2028
+ async function cmdTradeTask(capability, description) {
2029
+ if (!capability) { console.error('Usage: atel trade-task <capability> <description> [--budget N] [--executor DID] [--timeout 300]'); process.exit(1); }
2030
+ const id = requireIdentity();
2031
+ const budget = parseFloat(rawArgs.find((a, i) => rawArgs[i-1] === '--budget') || '0');
2032
+ const executorArg = rawArgs.find((a, i) => rawArgs[i-1] === '--executor') || '';
2033
+ const timeout = parseInt(rawArgs.find((a, i) => rawArgs[i-1] === '--timeout') || '300') * 1000;
2034
+ const desc = description || capability;
2035
+
2036
+ // Step 1: Find executor
2037
+ let executorDid = executorArg;
2038
+ if (!executorDid) {
2039
+ console.error(`[trade-task] Searching for executor with capability: ${capability}...`);
2040
+ const regClient = new RegistryClient({ registryUrl: REGISTRY_URL });
2041
+ const results = await regClient.search({ type: capability, limit: 5 });
2042
+ if (results.length === 0) { console.error('[trade-task] No executor found for capability: ' + capability); process.exit(1); }
2043
+ // Pick best by trust score (if available), exclude self
2044
+ const candidates = results.filter(r => r.did !== id.did);
2045
+ if (candidates.length === 0) { console.error('[trade-task] No other executor found (only self)'); process.exit(1); }
2046
+ const best = candidates[0]; // Registry returns sorted by score
2047
+ executorDid = best.did;
2048
+ console.error(`[trade-task] Found executor: ${best.name || best.did} (score: ${best.trustScore || 'N/A'})`);
2049
+ }
2050
+
2051
+ // Step 2: Create order
2052
+ console.error(`[trade-task] Creating order: ${capability}, budget: $${budget}...`);
2053
+ const orderData = await signedFetch('POST', '/trade/v1/order', {
2054
+ executorDid, capabilityType: capability, priceAmount: budget, priceCurrency: 'USD', pricingModel: 'per_task',
2055
+ });
2056
+ const orderId = orderData.orderId;
2057
+ console.error(`[trade-task] Order created: ${orderId}`);
2058
+
2059
+ // Step 3: Poll for status changes (executor accepts → auto-escrow → executes → completes)
2060
+ console.error(`[trade-task] Waiting for executor to accept and complete (timeout: ${timeout/1000}s)...`);
2061
+ const startTime = Date.now();
2062
+ let lastStatus = 'created';
2063
+ while (Date.now() - startTime < timeout) {
2064
+ await new Promise(r => setTimeout(r, 3000));
2065
+ try {
2066
+ const info = await signedFetch('GET', `/trade/v1/order/${orderId}`);
2067
+ if (info.status !== lastStatus) {
2068
+ console.error(`[trade-task] Status: ${lastStatus} → ${info.status}`);
2069
+ lastStatus = info.status;
2070
+ }
2071
+ if (info.status === 'completed' || info.status === 'settled') {
2072
+ console.error(`[trade-task] Task completed! Confirming delivery...`);
2073
+ // Auto-confirm if still completed (not yet settled)
2074
+ if (info.status === 'completed') {
2075
+ try {
2076
+ await signedFetch('POST', `/trade/v1/order/${orderId}/confirm`);
2077
+ console.error(`[trade-task] Delivery confirmed and settled.`);
2078
+ } catch (e) {
2079
+ console.error(`[trade-task] Auto-confirm skipped: ${e.message}`);
2080
+ }
2081
+ }
2082
+ // Output final order info
2083
+ const final = await signedFetch('GET', `/trade/v1/order/${orderId}`);
2084
+ console.log(JSON.stringify(final, null, 2));
2085
+ return;
2086
+ }
2087
+ if (info.status === 'rejected' || info.status === 'cancelled') {
2088
+ console.error(`[trade-task] Order ${info.status}. Aborting.`);
2089
+ process.exit(1);
2090
+ }
2091
+ } catch (e) {
2092
+ console.error(`[trade-task] Poll error: ${e.message}`);
2093
+ }
2094
+ }
2095
+ console.error(`[trade-task] Timeout waiting for completion. Order: ${orderId}, last status: ${lastStatus}`);
2096
+ process.exit(1);
2097
+ }
2098
+
2099
+ async function cmdOrder(executorDid, capType, price) {
2100
+ if (!executorDid || !capType || !price) { console.error('Usage: atel order <executorDid> <capabilityType> <price> [--desc "task description"]'); process.exit(1); }
2101
+ const description = rawArgs.find((a, i) => rawArgs[i-1] === '--desc') || '';
2102
+ const data = await signedFetch('POST', '/trade/v1/order', {
2103
+ executorDid, capabilityType: capType, priceAmount: parseFloat(price), priceCurrency: 'USD', pricingModel: 'per_task', description,
2104
+ });
2105
+ console.log(JSON.stringify(data, null, 2));
2106
+ }
2107
+
2108
+ async function cmdOrderInfo(orderId) {
2109
+ if (!orderId) { console.error('Usage: atel order-info <orderId>'); process.exit(1); }
2110
+ const res = await fetch(`${PLATFORM_URL}/trade/v1/order/${orderId}`);
2111
+ const data = await res.json();
2112
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
2113
+ console.log(JSON.stringify(data, null, 2));
2114
+ }
2115
+
2116
+ async function cmdAccept(orderId) {
2117
+ if (!orderId) { console.error('Usage: atel accept <orderId>'); process.exit(1); }
2118
+ const data = await signedFetch('POST', `/trade/v1/order/${orderId}/accept`);
2119
+ console.log(JSON.stringify(data, null, 2));
2120
+ }
2121
+
2122
+ async function cmdReject(orderId) {
2123
+ if (!orderId) { console.error('Usage: atel reject <orderId>'); process.exit(1); }
2124
+ const data = await signedFetch('POST', `/trade/v1/order/${orderId}/reject`);
2125
+ console.log(JSON.stringify(data, null, 2));
2126
+ }
2127
+
2128
+ async function cmdEscrow(orderId) {
2129
+ console.error('⚠️ DEPRECATED: Escrow is now automatic on accept. No action needed.');
2130
+ if (!orderId) { process.exit(0); }
2131
+ const data = await signedFetch('POST', `/trade/v1/order/${orderId}/escrow`);
2132
+ console.log(JSON.stringify(data, null, 2));
2133
+ }
2134
+
2135
+ async function cmdComplete(orderId, taskId) {
2136
+ if (!orderId) { console.error('Usage: atel complete <orderId> [taskId] [--proof]'); process.exit(1); }
2137
+ const id = requireIdentity();
2138
+ const policy = loadPolicy();
2139
+ const effectiveTaskId = taskId || orderId;
2140
+ const payload = {};
2141
+ if (taskId) payload.taskId = taskId;
2142
+
2143
+ // Fetch order info for context
2144
+ let requesterDid = 'unknown';
2145
+ let orderInfo = null;
2146
+ try {
2147
+ orderInfo = await signedFetch('GET', `/trade/v1/order/${orderId}`);
2148
+ requesterDid = orderInfo.requesterDid || 'unknown';
2149
+ } catch (e) { console.error(`[complete] Warning: could not fetch order info: ${e.message}`); }
2150
+
2151
+ // ── ContentAuditor: audit the completion context ──
2152
+ const auditor = new ContentAuditor();
2153
+ const auditResult = auditor.audit({ orderId, taskId: effectiveTaskId, action: 'complete' }, { action: 'complete', from: requesterDid });
2154
+ if (auditResult.blocked) {
2155
+ console.error(`[complete] BLOCKED by ContentAuditor: ${auditResult.reason}`);
2156
+ process.exit(1);
2157
+ }
2158
+ if (auditResult.warnings?.length > 0) {
2159
+ console.error(`[complete] ContentAuditor warnings: ${auditResult.warnings.join(', ')}`);
2160
+ }
2161
+
2162
+ // ── PolicyEnforcer: check policy compliance ──
2163
+ const enforcer = new PolicyEnforcer(policy);
2164
+ const policyCheck = enforcer.check({ from: requesterDid, action: 'complete', payload: { orderId } });
2165
+ if (policyCheck && !policyCheck.allowed) {
2166
+ console.error(`[complete] BLOCKED by PolicyEnforcer: ${policyCheck.reason}`);
2167
+ process.exit(1);
2168
+ }
2169
+
2170
+ // Try to load existing trace, otherwise generate fresh proof + anchor
2171
+ let proof = null;
2172
+ let anchor = null;
2173
+ let trace = null;
2174
+ const traceData = loadTrace(effectiveTaskId);
2175
+ if (traceData) {
2176
+ try {
2177
+ const lines = traceData.trim().split('\n').map(l => JSON.parse(l));
2178
+ const proofLine = lines.find(l => l.proof_id);
2179
+ if (proofLine) proof = proofLine;
2180
+ const anchorLine = lines.find(l => l.anchor_tx);
2181
+ if (anchorLine) anchor = { txHash: anchorLine.anchor_tx, trace_root: anchorLine.trace_root };
2182
+ } catch {}
2183
+ }
2184
+
2185
+ // Generate fresh proof if none found
2186
+ if (!proof) {
2187
+ console.error('[complete] Generating execution trace + proof...');
2188
+ trace = new ExecutionTrace(effectiveTaskId, id);
2189
+ trace.append('TASK_RECEIVED', { orderId, taskId: effectiveTaskId, requesterDid });
2190
+ trace.append('CONTENT_AUDIT', { result: auditResult.blocked ? 'blocked' : 'passed', warnings: auditResult.warnings || [] });
2191
+ trace.append('POLICY_CHECK', { result: 'passed' });
2192
+ trace.append('EXECUTION', { mode: 'cli-complete', orderId });
2193
+ trace.finalize({ orderId, status: 'completed' });
2194
+ saveTrace(effectiveTaskId, trace);
2195
+ const proofGen = new ProofGenerator(trace, id);
2196
+ proof = proofGen.generate('cli-complete', `order-${orderId}`, JSON.stringify({ orderId, status: 'completed' }));
2197
+ console.error(`[complete] Proof generated: ${proof.proof_id}, trace_root: ${proof.trace_root}`);
2198
+ }
2199
+
2200
+ // ── On-chain Anchoring ──
2201
+ if (!anchor) {
2202
+ console.error('[complete] Anchoring proof on-chain...');
2203
+ anchor = await anchorOnChain(proof.trace_root, { proof_id: proof.proof_id, executorDid: id.did, requesterDid, taskId: effectiveTaskId, action: 'cli-complete' });
2204
+ if (anchor) {
2205
+ console.error(`[complete] Anchored on Solana: ${anchor.txHash}`);
2206
+ } else {
2207
+ console.error('[complete] WARNING: On-chain anchoring failed. Set ATEL_SOLANA_PRIVATE_KEY for verifiable trust.');
2208
+ }
2209
+ }
2210
+
2211
+ // ── Trust Score Update (persistent, with or without anchor) ──
2212
+ try {
2213
+ const trustScoreClient = new TrustScoreClient();
2214
+ // Load existing records
2215
+ try {
2216
+ const saved = JSON.parse(readFileSync(SCORE_FILE, 'utf-8'));
2217
+ if (saved.proofRecords) for (const r of saved.proofRecords) trustScoreClient.addProofRecord(r);
2218
+ if (saved.summaries) for (const s of saved.summaries) trustScoreClient.submitExecutionSummary(s);
2219
+ } catch {}
2220
+ const _pr = []; const _sm = [];
2221
+ try { const saved = JSON.parse(readFileSync(SCORE_FILE, 'utf-8')); if (saved.proofRecords) _pr.push(...saved.proofRecords); if (saved.summaries) _sm.push(...saved.summaries); } catch {}
2222
+
2223
+ if (anchor?.txHash) {
2224
+ const proofRecord = {
2225
+ traceRoot: proof.trace_root, txHash: anchor.txHash, chain: anchor.chain || 'solana',
2226
+ executor: id.did, taskFrom: requesterDid, action: 'cli-complete',
2227
+ success: true, durationMs: 0, riskLevel: 'low', policyViolations: 0,
2228
+ proofId: proof.proof_id, timestamp: new Date().toISOString(), verified: true,
2229
+ };
2230
+ trustScoreClient.addProofRecord(proofRecord);
2231
+ _pr.push(proofRecord);
2232
+ } else {
2233
+ const summary = {
2234
+ executor: id.did, task_id: effectiveTaskId, task_type: 'cli-complete',
2235
+ risk_level: 'low', success: true, duration_ms: 0, tool_calls: 0,
2236
+ policy_violations: 0, proof_id: proof.proof_id, timestamp: new Date().toISOString(),
2237
+ };
2238
+ trustScoreClient.submitExecutionSummary(summary);
2239
+ _sm.push(summary);
2240
+ console.error('[complete] Trust score updated (unverified — no on-chain anchor)');
2241
+ }
2242
+ try { writeFileSync(SCORE_FILE, JSON.stringify({ proofRecords: _pr, summaries: _sm }, null, 2)); } catch {}
2243
+
2244
+ const scoreReport = trustScoreClient.getAgentScore(id.did);
2245
+ console.error(`[complete] Trust score: ${scoreReport.trust_score} (tasks: ${scoreReport.total_tasks}, verified: ${scoreReport.verified_count})`);
2246
+
2247
+ // ── Push score to Registry ──
2248
+ try {
2249
+ const { serializePayload } = await import('@lawrenceliang-btc/atel-sdk');
2250
+ const ts = new Date().toISOString();
2251
+ const scorePayload = { did: id.did, trustScore: scoreReport.trust_score };
2252
+ const signable = serializePayload({ payload: scorePayload, did: id.did, timestamp: ts });
2253
+ const { default: nacl } = await import('tweetnacl');
2254
+ const sig = Buffer.from(nacl.sign.detached(Buffer.from(signable), id.secretKey)).toString('base64');
2255
+ await fetch(`${REGISTRY_URL}/registry/v1/score/update`, {
2256
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
2257
+ body: JSON.stringify({ payload: scorePayload, did: id.did, timestamp: ts, signature: sig }),
2258
+ signal: AbortSignal.timeout(5000),
2259
+ });
2260
+ console.error(`[complete] Score pushed to Registry: ${scoreReport.trust_score}`);
2261
+ } catch (e) { console.error(`[complete] Registry score push failed: ${e.message}`); }
2262
+ } catch (e) { console.error(`[complete] Trust score error: ${e.message}`); }
2263
+
2264
+ // Attach proof + anchor to payload
2265
+ payload.proofBundle = proof;
2266
+ payload.traceRoot = proof.trace_root;
2267
+ if (anchor?.txHash) payload.anchorTx = anchor.txHash;
2268
+
2269
+ const data = await signedFetch('POST', `/trade/v1/order/${orderId}/complete`, payload);
2270
+ console.log(JSON.stringify(data, null, 2));
2271
+ }
2272
+
2273
+ async function cmdConfirm(orderId) {
2274
+ if (!orderId) { console.error('Usage: atel confirm <orderId>'); process.exit(1); }
2275
+ const data = await signedFetch('POST', `/trade/v1/order/${orderId}/confirm`);
2276
+ console.log(JSON.stringify(data, null, 2));
2277
+ }
2278
+
2279
+ async function cmdRate(orderId, rating, comment) {
2280
+ if (!orderId || !rating) { console.error('Usage: atel rate <orderId> <1-5> [comment]'); process.exit(1); }
2281
+ const r = parseInt(rating);
2282
+ if (r < 1 || r > 5) { console.error('Rating must be 1-5'); process.exit(1); }
2283
+ const data = await signedFetch('POST', `/trade/v1/order/${orderId}/rate`, { rating: r, comment: comment || '' });
2284
+ console.log(JSON.stringify(data, null, 2));
2285
+ }
2286
+
2287
+ async function cmdOrders(role, status) {
2288
+ const params = new URLSearchParams();
2289
+ if (role) params.set('role', role);
2290
+ if (status) params.set('status', status);
2291
+ const qs = params.toString() ? '?' + params.toString() : '';
2292
+ const data = await signedFetch('GET', `/trade/v1/orders${qs}`);
2293
+ console.log(JSON.stringify(data, null, 2));
2294
+ }
2295
+
2296
+ // ─── Dispute Commands ────────────────────────────────────────────
2297
+
2298
+ async function cmdDispute(orderId, reason, description) {
2299
+ if (!orderId || !reason) { console.error('Usage: atel dispute <orderId> <reason> [description]\nReasons: quality, incomplete, timeout, fraud, malicious, other'); process.exit(1); }
2300
+ const data = await signedFetch('POST', '/dispute/v1/open', { orderId, reason, description: description || '' });
2301
+ console.log(JSON.stringify(data, null, 2));
2302
+ }
2303
+
2304
+ async function cmdEvidence(disputeId, evidenceJson) {
2305
+ if (!disputeId || !evidenceJson) { console.error('Usage: atel evidence <disputeId> <json>'); process.exit(1); }
2306
+ let evidence;
2307
+ try { evidence = JSON.parse(evidenceJson); } catch { console.error('Invalid JSON'); process.exit(1); }
2308
+ const data = await signedFetch('POST', `/dispute/v1/${disputeId}/evidence`, { evidence });
2309
+ console.log(JSON.stringify(data, null, 2));
2310
+ }
2311
+
2312
+ async function cmdDisputes() {
2313
+ const data = await signedFetch('GET', '/dispute/v1/list');
2314
+ console.log(JSON.stringify(data, null, 2));
2315
+ }
2316
+
2317
+ async function cmdDisputeInfo(disputeId) {
2318
+ if (!disputeId) { console.error('Usage: atel dispute-info <disputeId>'); process.exit(1); }
2319
+ const res = await fetch(`${PLATFORM_URL}/dispute/v1/${disputeId}`);
2320
+ const data = await res.json();
2321
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
2322
+ console.log(JSON.stringify(data, null, 2));
2323
+ }
2324
+
2325
+ // ─── Cert Commands ───────────────────────────────────────────────
2326
+
2327
+ async function cmdCertApply(level) {
2328
+ const data = await signedFetch('POST', '/cert/v1/apply', { level: level || 'certified' });
2329
+ console.log(JSON.stringify(data, null, 2));
2330
+ }
2331
+
2332
+ async function cmdCertStatus(did) {
2333
+ const targetDid = did || requireIdentity().did;
2334
+ const res = await fetch(`${PLATFORM_URL}/cert/v1/status/${encodeURIComponent(targetDid)}`);
2335
+ const data = await res.json();
2336
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
2337
+ console.log(JSON.stringify(data, null, 2));
2338
+ }
2339
+
2340
+ async function cmdCertRenew(level) {
2341
+ const data = await signedFetch('POST', '/cert/v1/renew', { level: level || 'certified' });
2342
+ console.log(JSON.stringify(data, null, 2));
2343
+ }
2344
+
2345
+ // ─── Boost Commands ──────────────────────────────────────────────
2346
+
2347
+ async function cmdBoost(tier, weeks) {
2348
+ if (!tier || !weeks) { console.error('Usage: atel boost <tier> <weeks>\nTiers: basic ($10/wk), premium ($30/wk), featured ($100/wk)'); process.exit(1); }
2349
+ const data = await signedFetch('POST', '/boost/v1/purchase', { tier, weeks: parseInt(weeks) });
2350
+ console.log(JSON.stringify(data, null, 2));
2351
+ }
2352
+
2353
+ async function cmdBoostStatus(did) {
2354
+ const targetDid = did || requireIdentity().did;
2355
+ const res = await fetch(`${PLATFORM_URL}/boost/v1/status/${encodeURIComponent(targetDid)}`);
2356
+ const data = await res.json();
2357
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
2358
+ console.log(JSON.stringify(data, null, 2));
2359
+ }
2360
+
2361
+ async function cmdBoostCancel(boostId) {
2362
+ if (!boostId) { console.error('Usage: atel boost-cancel <boostId>'); process.exit(1); }
2363
+ const data = await signedFetch('DELETE', `/boost/v1/cancel/${boostId}`);
2364
+ console.log(JSON.stringify(data, null, 2));
2365
+ }
2366
+
2367
+ // ─── Offer Commands ──────────────────────────────────────────────
2368
+
2369
+ async function cmdOfferCreate(cap, price) {
2370
+ if (!cap) { console.error('Usage: atel offer <capability> <price> [--title "..."] [--desc "..."]'); process.exit(1); }
2371
+ const titleIdx = rawArgs.indexOf('--title');
2372
+ const descIdx = rawArgs.indexOf('--desc');
2373
+ const title = titleIdx >= 0 ? rawArgs[titleIdx + 1] : undefined;
2374
+ const desc = descIdx >= 0 ? rawArgs[descIdx + 1] : undefined;
2375
+ const body = { capabilityType: cap, priceAmount: parseFloat(price) || 0 };
2376
+ if (title) body.title = title;
2377
+ if (desc) body.description = desc;
2378
+ const data = await signedFetch('POST', '/trade/v1/offer', body);
2379
+ console.log(JSON.stringify(data, null, 2));
2380
+ }
2381
+
2382
+ async function cmdOfferList(did) {
2383
+ const params = new URLSearchParams();
2384
+ if (did) params.set('did', did);
2385
+ const capIdx = rawArgs.indexOf('--capability');
2386
+ if (capIdx >= 0) params.set('capability', rawArgs[capIdx + 1]);
2387
+ const url = `/trade/v1/offers${params.toString() ? '?' + params : ''}`;
2388
+ const resp = await fetch(`${PLATFORM_URL}${url}`);
2389
+ const data = await resp.json();
2390
+ if (data.offers && data.offers.length > 0) {
2391
+ console.log(`\n Found ${data.count} offer(s):\n`);
2392
+ for (const o of data.offers) {
2393
+ console.log(` ${o.offerId} ${o.executorName || o.executorDid.slice(0,20)+'...'} ${o.capabilityType} $${o.priceAmount} orders:${o.totalOrders} completed:${o.totalCompleted} ${o.title || ''}`);
2394
+ }
2395
+ console.log('');
2396
+ } else {
2397
+ console.log(' No active offers found.');
2398
+ }
2399
+ }
2400
+
2401
+ async function cmdOfferUpdate(offerId) {
2402
+ if (!offerId) { console.error('Usage: atel offer-update <offerId> [--price N] [--title "..."] [--desc "..."] [--status active|paused]'); process.exit(1); }
2403
+ const body = {};
2404
+ const priceIdx = rawArgs.indexOf('--price');
2405
+ const titleIdx = rawArgs.indexOf('--title');
2406
+ const descIdx = rawArgs.indexOf('--desc');
2407
+ const statusIdx = rawArgs.indexOf('--status');
2408
+ if (priceIdx >= 0) body.priceAmount = parseFloat(rawArgs[priceIdx + 1]);
2409
+ if (titleIdx >= 0) body.title = rawArgs[titleIdx + 1];
2410
+ if (descIdx >= 0) body.description = rawArgs[descIdx + 1];
2411
+ if (statusIdx >= 0) body.status = rawArgs[statusIdx + 1];
2412
+ const data = await signedFetch('POST', `/trade/v1/offer/${offerId}/update`, body);
2413
+ console.log(JSON.stringify(data, null, 2));
2414
+ }
2415
+
2416
+ async function cmdOfferClose(offerId) {
2417
+ if (!offerId) { console.error('Usage: atel offer-close <offerId>'); process.exit(1); }
2418
+ const data = await signedFetch('POST', `/trade/v1/offer/${offerId}/close`);
2419
+ console.log(JSON.stringify(data, null, 2));
2420
+ }
2421
+
2422
+ async function cmdOfferBuy(offerId, desc) {
2423
+ if (!offerId) { console.error('Usage: atel offer-buy <offerId> [description]'); process.exit(1); }
2424
+ const body = {};
2425
+ if (desc) body.description = rawArgs.slice(rawArgs.indexOf(offerId) + 1).join(' ');
2426
+ const data = await signedFetch('POST', `/trade/v1/offer/${offerId}/buy`, body);
2427
+ console.log(JSON.stringify(data, null, 2));
2428
+ }
2429
+
2430
+ // ─── Task Mode Commands ──────────────────────────────────────────
2431
+
2432
+ async function cmdMode(newMode) {
2433
+ const policy = loadPolicy();
2434
+ if (!newMode) {
2435
+ console.log(JSON.stringify({
2436
+ taskMode: policy.taskMode || 'auto',
2437
+ autoAcceptPlatform: policy.autoAcceptPlatform !== false,
2438
+ autoAcceptP2P: policy.autoAcceptP2P !== false,
2439
+ }, null, 2));
2440
+ return;
2441
+ }
2442
+ if (!['auto', 'confirm', 'off'].includes(newMode)) {
2443
+ console.error('Usage: atel mode [auto|confirm|off]');
2444
+ console.error(' auto - Accept and execute all tasks automatically (default)');
2445
+ console.error(' confirm - Queue tasks for manual approval');
2446
+ console.error(' off - Reject all incoming tasks');
2447
+ process.exit(1);
2448
+ }
2449
+ policy.taskMode = newMode;
2450
+ savePolicy(policy);
2451
+ console.log(JSON.stringify({ status: 'ok', taskMode: newMode, message: `Task mode set to "${newMode}"` }));
2452
+ }
2453
+
2454
+ async function cmdPending() {
2455
+ const pending = loadPending();
2456
+ const entries = Object.entries(pending);
2457
+ if (entries.length === 0) {
2458
+ console.log('No pending tasks.');
2459
+ return;
2460
+ }
2461
+ console.log(`Pending tasks (${entries.length}):\n`);
2462
+ for (const [taskId, t] of entries) {
2463
+ console.log(` ${taskId}`);
2464
+ console.log(` Source: ${t.source}`);
2465
+ console.log(` From: ${t.from}`);
2466
+ console.log(` Action: ${t.action}`);
2467
+ console.log(` Price: $${t.price || 0}`);
2468
+ console.log(` Status: ${t.status}`);
2469
+ console.log(` Time: ${t.receivedAt}`);
2470
+ if (t.orderId) console.log(` OrderId: ${t.orderId}`);
2471
+ console.log('');
2472
+ }
2473
+ }
2474
+
2475
+ async function cmdApprove(taskId) {
2476
+ if (!taskId) { console.error('Usage: atel approve <taskId|orderId>'); process.exit(1); }
2477
+ const pending = loadPending();
2478
+ const task = pending[taskId];
2479
+ if (!task) { console.error(`Task "${taskId}" not found in pending queue.`); process.exit(1); }
2480
+ if (task.status !== 'pending_confirm') { console.error(`Task "${taskId}" is not pending (status: ${task.status}).`); process.exit(1); }
2481
+
2482
+ if (task.source === 'platform') {
2483
+ // Accept via Platform API
2484
+ const data = await signedFetch('POST', `/trade/v1/order/${task.orderId}/accept`);
2485
+ task.status = 'approved';
2486
+ savePending(pending);
2487
+ console.log(JSON.stringify({ status: 'approved', taskId, orderId: task.orderId, platform: data }));
2488
+ } else {
2489
+ // P2P: notify the running agent to process this task
2490
+ // Try the agent's local approve endpoint first
2491
+ const agentPort = process.env.ATEL_PORT || '3100';
2492
+ try {
2493
+ const resp = await fetch(`http://127.0.0.1:${agentPort}/atel/v1/approve`, {
2494
+ method: 'POST',
2495
+ headers: { 'Content-Type': 'application/json' },
2496
+ body: JSON.stringify({ taskId }),
2497
+ signal: AbortSignal.timeout(10000),
2498
+ });
2499
+ const data = await resp.json();
2500
+ if (resp.ok) {
2501
+ task.status = 'approved';
2502
+ savePending(pending);
2503
+ console.log(JSON.stringify({ status: 'approved', taskId, ...data }));
2504
+ } else {
2505
+ console.error(`Agent error: ${JSON.stringify(data)}`);
2506
+ }
2507
+ } catch (e) {
2508
+ console.error(`Cannot reach agent at port ${agentPort}. Is 'atel start' running?`);
2509
+ console.error(`Error: ${e.message}`);
2510
+ process.exit(1);
2511
+ }
2512
+ }
2513
+ }
2514
+
2515
+ // Extend reject to handle pending tasks too
2516
+ const _origCmdReject = async (orderId) => {
2517
+ if (!orderId) { console.error('Usage: atel reject <orderId|taskId> [reason]'); process.exit(1); }
2518
+ // Check if it's a pending task first
2519
+ const pending = loadPending();
2520
+ if (pending[orderId] && pending[orderId].status === 'pending_confirm') {
2521
+ const task = pending[orderId];
2522
+ const reason = rawArgs.slice(1).join(' ') || 'Manually rejected';
2523
+ if (task.source === 'platform') {
2524
+ const data = await signedFetch('POST', `/trade/v1/order/${task.orderId}/reject`);
2525
+ task.status = 'rejected';
2526
+ task.rejectReason = reason;
2527
+ savePending(pending);
2528
+ console.log(JSON.stringify({ status: 'rejected', taskId: orderId, orderId: task.orderId, reason, platform: data }));
2529
+ } else {
2530
+ task.status = 'rejected';
2531
+ task.rejectReason = reason;
2532
+ savePending(pending);
2533
+ console.log(JSON.stringify({ status: 'rejected', taskId: orderId, reason }));
2534
+ }
2535
+ return;
2536
+ }
2537
+ // Fall through to Platform order reject
2538
+ const data = await signedFetch('POST', `/trade/v1/order/${orderId}/reject`);
2539
+ console.log(JSON.stringify(data, null, 2));
2540
+ };
2541
+
2542
+ // ─── Main ────────────────────────────────────────────────────────
2543
+
2544
+ const [,, cmd, ...rawArgs] = process.argv;
2545
+ const args = rawArgs.filter(a => !a.startsWith('--'));
2546
+ const commands = {
2547
+ init: () => cmdInit(args[0]),
2548
+ info: () => cmdInfo(),
2549
+ setup: () => cmdSetup(args[0]),
2550
+ verify: () => cmdVerify(),
2551
+ start: () => cmdStart(args[0]),
2552
+ inbox: () => cmdInbox(args[0]),
2553
+ register: () => cmdRegister(args[0], args[1], args[2]),
2554
+ search: () => cmdSearch(args[0]),
2555
+ handshake: () => cmdHandshake(args[0], args[1]),
2556
+ task: () => cmdTask(args[0], args[1]),
2557
+ result: () => cmdResult(args[0], args[1]),
2558
+ check: () => cmdCheck(args[0], args[1], { chain: rawArgs.includes('--chain') }),
2559
+ 'verify-proof': () => cmdVerifyProof(args[0], args[1]),
2560
+ audit: () => cmdAudit(args[0], args[1]),
2561
+ rotate: () => cmdRotate(),
2562
+ // Account
2563
+ balance: () => cmdBalance(),
2564
+ deposit: () => cmdDeposit(args[0], args[1]),
2565
+ withdraw: () => cmdWithdraw(args[0], args[1], args[2]),
2566
+ transactions: () => cmdTransactions(),
2567
+ // Trade
2568
+ 'trade-task': () => cmdTradeTask(args[0], args.slice(1).join(' ')),
2569
+ order: () => cmdOrder(args[0], args[1], args[2]),
2570
+ 'order-info': () => cmdOrderInfo(args[0]),
2571
+ accept: () => cmdAccept(args[0]),
2572
+ reject: () => _origCmdReject(args[0]),
2573
+ escrow: () => cmdEscrow(args[0]),
2574
+ complete: () => cmdComplete(args[0], args[1]),
2575
+ confirm: () => cmdConfirm(args[0]),
2576
+ rate: () => cmdRate(args[0], args[1], args[2]),
2577
+ orders: () => cmdOrders(args[0], args[1]),
2578
+ // Dispute
2579
+ dispute: () => cmdDispute(args[0], args[1], args[2]),
2580
+ evidence: () => cmdEvidence(args[0], args[1]),
2581
+ disputes: () => cmdDisputes(),
2582
+ 'dispute-info': () => cmdDisputeInfo(args[0]),
2583
+ // Cert
2584
+ 'cert-apply': () => cmdCertApply(args[0]),
2585
+ 'cert-status': () => cmdCertStatus(args[0]),
2586
+ 'cert-renew': () => cmdCertRenew(args[0]),
2587
+ // Boost
2588
+ boost: () => cmdBoost(args[0], args[1]),
2589
+ 'boost-status': () => cmdBoostStatus(args[0]),
2590
+ 'boost-cancel': () => cmdBoostCancel(args[0]),
2591
+ // Offers
2592
+ offer: () => cmdOfferCreate(args[0], args[1]),
2593
+ offers: () => cmdOfferList(args[0]),
2594
+ 'offer-info': () => cmdOfferList(args[0]), // alias — single offer uses GET /offer/:id
2595
+ 'offer-update': () => cmdOfferUpdate(args[0]),
2596
+ 'offer-close': () => cmdOfferClose(args[0]),
2597
+ 'offer-buy': () => cmdOfferBuy(args[0], args[1]),
2598
+ // Task Mode
2599
+ mode: () => cmdMode(args[0]),
2600
+ pending: () => cmdPending(),
2601
+ approve: () => cmdApprove(args[0]),
2602
+ };
2603
+
2604
+ if (!cmd || !commands[cmd]) {
2605
+ console.log(`ATEL CLI - Agent Trust & Exchange Layer
2606
+
2607
+ Usage: atel <command> [args]
2608
+
2609
+ Protocol Commands:
2610
+ init [name] Create agent identity + security policy
2611
+ info Show identity, capabilities, network, policy
2612
+ setup [port] Configure network (detect IP, UPnP, verify)
2613
+ verify Verify port reachability
2614
+ start [port] Start endpoint (auto network + auto register)
2615
+ inbox [count] Show received messages (default: 20)
2616
+ register [name] [caps] [endpoint] Register on public registry
2617
+ search <capability> Search registry for agents
2618
+ handshake <endpoint> [did] Handshake with remote agent
2619
+ task <target> <json> Delegate task (auto trust check)
2620
+ result <taskId> <json> Submit execution result (from executor)
2621
+ check <did> [risk] Check agent trust (risk: low|medium|high|critical)
2622
+ verify-proof <anchor_tx> <root> Verify on-chain proof
2623
+ audit <did_or_url> <taskId> Deep audit: fetch trace + verify hash chain
2624
+ rotate Rotate identity key pair (backup + on-chain anchor)
2625
+
2626
+ Account Commands:
2627
+ balance Show platform account balance
2628
+ deposit <amount> [channel] Deposit funds (channel: manual|crypto_sol|stripe|alipay)
2629
+ withdraw <amount> [channel] [address] Withdraw funds (address required for crypto)
2630
+ transactions List payment history
2631
+
2632
+ Trade Commands:
2633
+ trade-task <cap> <desc> [--budget N] One-shot: search → order → wait → confirm (requester)
2634
+ order <executorDid> <cap> <price> Create a trade order
2635
+ order-info <orderId> Get order details
2636
+ accept <orderId> Accept an order (auto-escrow for paid orders)
2637
+ reject <orderId> Reject an order (executor)
2638
+ escrow <orderId> [DEPRECATED] Escrow is now automatic on accept
2639
+ complete <orderId> [taskId] Mark order complete + attach proof (executor)
2640
+ confirm <orderId> Confirm delivery + settle (requester)
2641
+ rate <orderId> <1-5> [comment] Rate the other party
2642
+ orders [role] [status] List orders (role: requester|executor|all)
2643
+
2644
+ Dispute Commands:
2645
+ dispute <orderId> <reason> [desc] Open a dispute (reason: quality|incomplete|timeout|fraud|malicious|other)
2646
+ evidence <disputeId> <json> Submit dispute evidence
2647
+ disputes List your disputes
2648
+ dispute-info <disputeId> Get dispute details
2649
+
2650
+ Certification Commands:
2651
+ cert-apply [level] Apply for certification (level: certified|enterprise)
2652
+ cert-status [did] Check certification status
2653
+ cert-renew [level] Renew certification
2654
+
2655
+ Boost Commands:
2656
+ boost <tier> <weeks> Purchase boost (tier: basic|premium|featured)
2657
+ boost-status [did] Check boost status
2658
+ boost-cancel <boostId> Cancel a boost
2659
+
2660
+ Offer Commands (Seller Listings):
2661
+ offer <capability> <price> Publish a service offer (--title, --desc)
2662
+ offers [did] Browse active offers (--capability filter)
2663
+ offer-update <offerId> Update offer (--price, --title, --desc, --status)
2664
+ offer-close <offerId> Close an offer
2665
+ offer-buy <offerId> [description] Buy from an offer (creates order automatically)
2666
+
2667
+ Task Mode Commands:
2668
+ mode [auto|confirm|off] Get or set task acceptance mode
2669
+ pending List tasks awaiting manual confirmation
2670
+ approve <taskId|orderId> Approve a pending task (forward to executor)
2671
+
2672
+ Environment:
2673
+ ATEL_DIR Identity directory (default: .atel)
2674
+ ATEL_REGISTRY Registry URL (default: https://api.atelai.org)
2675
+ ATEL_PLATFORM Platform URL (default: ATEL_REGISTRY value)
2676
+ ATEL_EXECUTOR_URL Local executor HTTP endpoint
2677
+ ATEL_SOLANA_PRIVATE_KEY Solana key for on-chain anchoring
2678
+ ATEL_SOLANA_RPC_URL Solana RPC (default: mainnet-beta)
2679
+ ATEL_BASE_PRIVATE_KEY Base chain key for on-chain anchoring
2680
+ ATEL_BSC_PRIVATE_KEY BSC chain key for on-chain anchoring
2681
+
2682
+ Trust Policy: Configure .atel/policy.json trustPolicy for automatic
2683
+ pre-task trust evaluation. Use _risk in payload or --risk flag.
2684
+
2685
+ Task Mode: Configure .atel/policy.json taskMode (auto|confirm|off).
2686
+ auto - Accept all tasks automatically (default)
2687
+ confirm - Queue tasks for manual approval (atel pending / atel approve)
2688
+ off - Reject all incoming tasks (communication still works)`);
2689
+ process.exit(cmd ? 1 : 0);
2690
+ }
2691
+
2692
+ commands[cmd]().catch(err => { console.error(JSON.stringify({ error: err.message })); process.exit(1); });