@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.
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/bin/atel.mjs +2692 -0
- package/bin/tunnel-manager.mjs +171 -0
- package/dist/anchor/base.d.ts +21 -0
- package/dist/anchor/base.js +26 -0
- package/dist/anchor/bsc.d.ts +20 -0
- package/dist/anchor/bsc.js +25 -0
- package/dist/anchor/evm.d.ts +99 -0
- package/dist/anchor/evm.js +262 -0
- package/dist/anchor/index.d.ts +173 -0
- package/dist/anchor/index.js +165 -0
- package/dist/anchor/mock.d.ts +43 -0
- package/dist/anchor/mock.js +100 -0
- package/dist/anchor/solana.d.ts +95 -0
- package/dist/anchor/solana.js +298 -0
- package/dist/auditor/index.d.ts +54 -0
- package/dist/auditor/index.js +141 -0
- package/dist/collaboration/index.d.ts +146 -0
- package/dist/collaboration/index.js +237 -0
- package/dist/crypto/index.d.ts +162 -0
- package/dist/crypto/index.js +231 -0
- package/dist/endpoint/index.d.ts +147 -0
- package/dist/endpoint/index.js +390 -0
- package/dist/envelope/index.d.ts +104 -0
- package/dist/envelope/index.js +156 -0
- package/dist/executor/index.d.ts +71 -0
- package/dist/executor/index.js +398 -0
- package/dist/gateway/index.d.ts +278 -0
- package/dist/gateway/index.js +520 -0
- package/dist/graph/index.d.ts +215 -0
- package/dist/graph/index.js +524 -0
- package/dist/handshake/index.d.ts +166 -0
- package/dist/handshake/index.js +287 -0
- package/dist/identity/index.d.ts +155 -0
- package/dist/identity/index.js +250 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +28 -0
- package/dist/negotiation/index.d.ts +133 -0
- package/dist/negotiation/index.js +160 -0
- package/dist/network/index.d.ts +78 -0
- package/dist/network/index.js +207 -0
- package/dist/orchestrator/index.d.ts +190 -0
- package/dist/orchestrator/index.js +297 -0
- package/dist/policy/index.d.ts +100 -0
- package/dist/policy/index.js +206 -0
- package/dist/proof/index.d.ts +220 -0
- package/dist/proof/index.js +541 -0
- package/dist/registry/index.d.ts +98 -0
- package/dist/registry/index.js +129 -0
- package/dist/rollback/index.d.ts +76 -0
- package/dist/rollback/index.js +91 -0
- package/dist/schema/capability-schema.json +52 -0
- package/dist/schema/index.d.ts +128 -0
- package/dist/schema/index.js +163 -0
- package/dist/schema/task-schema.json +69 -0
- package/dist/score/index.d.ts +174 -0
- package/dist/score/index.js +275 -0
- package/dist/service/index.d.ts +34 -0
- package/dist/service/index.js +273 -0
- package/dist/service/server.d.ts +7 -0
- package/dist/service/server.js +22 -0
- package/dist/trace/index.d.ts +217 -0
- package/dist/trace/index.js +446 -0
- package/dist/trust/index.d.ts +84 -0
- package/dist/trust/index.js +107 -0
- package/dist/trust-sync/index.d.ts +30 -0
- package/dist/trust-sync/index.js +57 -0
- package/package.json +71 -0
- package/skill/SKILL.md +363 -0
- package/skill/references/commercial.md +184 -0
- package/skill/references/executor.md +356 -0
- package/skill/references/networking.md +64 -0
- package/skill/references/onchain.md +73 -0
- 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); });
|