@lawrenceliang-btc/atel-sdk 1.2.13 → 1.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ ATEL provides the cryptographic primitives and protocol building blocks that ena
10
10
  - **📋 Policy Enforcement** — Scoped consent tokens, call tracking, deterministic hashing
11
11
  - **🔍 Execution Tracing** — Tamper-evident, hash-chained audit logs with auto-checkpoints
12
12
  - **✅ Proof Generation** — Merkle-tree proof bundles with multi-check verification
13
- - **⚓ On-Chain Anchoring** — Multi-chain proof anchoring (Solana/Base/BSC)
13
+ - **⚓ On-Chain Anchoring** — Multi-chain proof anchoring (Base/BSC)
14
14
  - **📊 Trust Scoring** — Local trust computation based on execution history
15
15
  - **🔔 Notification & Callback Runtime** — Local notify, callback, inbox, and recovery flow
16
16
  - **👥 P2P Access Control** — Relationship-based friend system with temporary sessions
@@ -60,7 +60,7 @@ the `paymentTxHash` on Base, the `auditUrl` pointing at the CompletionProof (Int
60
60
  ### Trust & Verification
61
61
  - Tamper-evident execution traces
62
62
  - Merkle-tree proof generation
63
- - On-chain anchoring (Solana/Base/BSC)
63
+ - On-chain anchoring (Base/BSC)
64
64
  - Local trust score computation
65
65
  - Callback-driven execution and recovery
66
66
 
@@ -359,21 +359,21 @@ const report = ProofVerifier.verify(bundle, { trace });
359
359
  ### On-Chain Anchoring
360
360
 
361
361
  ```typescript
362
- import { SolanaAnchorProvider } from '@lawrenceliang-btc/atel-sdk';
362
+ import { BaseAnchorProvider } from '@lawrenceliang-btc/atel-sdk';
363
363
 
364
- const solana = new SolanaAnchorProvider({
365
- rpcUrl: 'https://api.mainnet-beta.solana.com',
366
- privateKey: process.env.ATEL_SOLANA_PRIVATE_KEY
364
+ const base = new BaseAnchorProvider({
365
+ rpcUrl: process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org',
366
+ privateKey: process.env.ATEL_BASE_PRIVATE_KEY
367
367
  });
368
368
 
369
- const result = await solana.anchor(traceRoot, {
369
+ const result = await base.anchor(traceRoot, {
370
370
  executorDid: 'did:atel:ed25519:...',
371
371
  requesterDid: 'did:atel:ed25519:...',
372
372
  taskId: 'task-123'
373
373
  });
374
374
  // result.txHash, result.blockNumber
375
375
 
376
- const verified = await solana.verify(traceRoot, txHash);
376
+ const verified = await base.verify(traceRoot, txHash);
377
377
  // verified.valid, verified.detail
378
378
  ```
379
379
 
@@ -433,8 +433,6 @@ Friend system data is stored in `.atel/`:
433
433
  ## Environment Variables
434
434
 
435
435
  **On-Chain Anchoring:**
436
- - `ATEL_SOLANA_PRIVATE_KEY` - Solana wallet private key (base58)
437
- - `ATEL_SOLANA_RPC_URL` - Solana RPC endpoint
438
436
  - `ATEL_BASE_PRIVATE_KEY` - Base chain private key (hex)
439
437
  - `ATEL_BASE_RPC_URL` - Base RPC endpoint
440
438
  - `ATEL_BSC_PRIVATE_KEY` - BSC private key (hex)
package/bin/atel.mjs CHANGED
@@ -59,14 +59,14 @@ import crypto from 'node:crypto';
59
59
  import {
60
60
  AgentIdentity, AgentEndpoint, AgentClient, HandshakeManager,
61
61
  createMessage, verifyMessage, parseDID, RegistryClient, ExecutionTrace, ProofGenerator,
62
- SolanaAnchorProvider, BaseAnchorProvider, BSCAnchorProvider,
62
+ BaseAnchorProvider, BSCAnchorProvider,
63
63
  autoNetworkSetup, collectCandidates, connectToAgent,
64
- discoverPublicIP, checkReachable, ContentAuditor, TrustScoreClient,
64
+ discoverPublicIP, checkReachable, verifyPortReachable, ContentAuditor, TrustScoreClient,
65
65
  RollbackManager, rotateKey, verifyKeyRotation, ToolGateway, PolicyEngine, mintConsentToken, sign,
66
66
  TrustGraph, calculateTaskWeight,
67
67
  } from '@lawrenceliang-btc/atel-sdk';
68
68
  import { TunnelManager, HeartbeatManager } from './tunnel-manager.mjs';
69
- import { buildAgentCallbackAction, getDirectExecutableActions, normalizeGatewayBind, shouldSkipAgentHook, shouldUseGatewaySession } from './notification-action-helpers.mjs';
69
+ import { buildAgentCallbackAction, explainDirectExecutionSkip, getDirectExecutableActions, normalizeGatewayBind, shouldSkipAgentHook, shouldUseGatewaySession } from './notification-action-helpers.mjs';
70
70
  import { parseOrderCancelArgs, preflightOrderCancel } from './order-cancel-helpers.mjs';
71
71
  // ollama-manager removed — SDK does not run local models
72
72
  const initializeOllama = async () => {};
@@ -107,7 +107,6 @@ function getEnvironmentProfile(url = '') {
107
107
  const host = String(parsed.hostname || '').toLowerCase();
108
108
  if (host === 'api.atelai.org') return { name: 'production', label: 'production', url: normalized };
109
109
  if (host === '127.0.0.1' || host === 'localhost') return { name: 'test', label: 'local-test', url: normalized };
110
- if (host === '43.160.230.129' || host === '43.160.211.180') return { name: 'test', label: 'host-test', url: normalized };
111
110
  return { name: 'custom', label: host || 'custom', url: normalized };
112
111
  } catch {
113
112
  return { name: 'custom', label: normalized, url: normalized };
@@ -1118,12 +1117,26 @@ async function pushTradeNotification(eventType, payload, body) {
1118
1117
  if (c === 'bsc') return ' (BSC)';
1119
1118
  return '';
1120
1119
  };
1120
+ const autoAcceptReasonText = (reason) => {
1121
+ if (reason === 'missing_recommended_actions') return '平台未提供可执行接单动作';
1122
+ if (reason === 'missing_accept_action') return '事件里没有 accept 动作';
1123
+ if (reason === 'task_mode_not_auto') return '当前 taskMode 不是 auto';
1124
+ if (reason === 'auto_accept_platform_disabled') return '当前未启用免费单自动接单';
1125
+ if (reason === 'paid_auto_accept_disabled') return '当前未启用付费单自动接单';
1126
+ if (reason === 'price_exceeds_accept_max') return '订单金额超过自动接单上限';
1127
+ return reason || '未说明';
1128
+ };
1129
+
1121
1130
  const templates = {
1122
1131
  'order_created': (p) => `📥 收到新订单
1123
1132
  订单: ${p.orderId || body?.orderId || '?'}
1124
1133
  金额: $${p.priceAmount ?? '?'} USDC
1125
1134
  来自: ${p.requesterDid || '未知请求方'}
1126
1135
  请审核后决定是否接单`,
1136
+ 'order_created_auto_accept_skipped': (p) => `⏸️ 未自动接单
1137
+ 订单: ${p.orderId || body?.orderId || '?'}
1138
+ 原因: ${autoAcceptReasonText(p.reasonCode)}
1139
+ 请人工判断是否接单`,
1127
1140
  'order_accepted': (p) => `📋 订单已被接单
1128
1141
  订单: ${p.orderId || body?.orderId || '?'}
1129
1142
  执行方已开始处理,进入里程碑阶段`,
@@ -1176,7 +1189,8 @@ async function pushTradeNotification(eventType, payload, body) {
1176
1189
  订单: ${p.orderId || body?.orderId || '?'}
1177
1190
  原因: ${p.reason || '未说明'}`,
1178
1191
  'order_expired': (p) => `⌛ 订单已过期
1179
- 订单: ${p.orderId || body?.orderId || '?'}
1192
+ 订单: ${p.orderId || p.order_id || body?.orderId || body?.order_id || '?'}
1193
+ 原因: ${p.reason || '未说明'}
1180
1194
  系统已自动结束该订单`,
1181
1195
  'dispute_created': (p) => `⚖️ 争议已创建
1182
1196
  订单: ${p.orderId || body?.orderId || '?'}
@@ -2147,7 +2161,6 @@ function getChainPrivateKey(chain) {
2147
2161
  return config.chains[chain].privateKey;
2148
2162
  }
2149
2163
  // 2. Fall back to environment variables (backward compatibility)
2150
- if (chain === 'solana') return process.env.ATEL_SOLANA_PRIVATE_KEY;
2151
2164
  if (chain === 'base') return process.env.ATEL_BASE_PRIVATE_KEY;
2152
2165
  if (chain === 'bsc') return process.env.ATEL_BSC_PRIVATE_KEY;
2153
2166
  return null;
@@ -2158,19 +2171,6 @@ async function getWalletAddresses() {
2158
2171
  const wallets = {};
2159
2172
  const config = loadAnchorConfig();
2160
2173
 
2161
- // Solana: base58 private key → public key
2162
- const solKey = getChainPrivateKey('solana');
2163
- if (solKey) {
2164
- try {
2165
- const { Keypair } = await import('@solana/web3.js');
2166
- const bs58 = (await import('bs58')).default;
2167
- const kp = Keypair.fromSecretKey(bs58.decode(solKey));
2168
- wallets.solana = kp.publicKey.toBase58();
2169
- } catch {}
2170
- } else if (config?.chains?.solana?.address) {
2171
- wallets.solana = config.chains.solana.address;
2172
- }
2173
-
2174
2174
  // Base: hex private key → address
2175
2175
  const baseKey = getChainPrivateKey('base');
2176
2176
  if (baseKey) {
@@ -2200,9 +2200,8 @@ async function getWalletAddresses() {
2200
2200
  function detectPreferredChain() {
2201
2201
  const config = loadAnchorConfig();
2202
2202
  if (config?.preferredChain) return config.preferredChain;
2203
- if (getChainPrivateKey('bsc')) return 'bsc';
2204
2203
  if (getChainPrivateKey('base')) return 'base';
2205
- if (getChainPrivateKey('solana')) return 'solana';
2204
+ if (getChainPrivateKey('bsc')) return 'bsc';
2206
2205
  return null;
2207
2206
  }
2208
2207
 
@@ -2312,6 +2311,12 @@ function addFriend(did, options = {}) {
2312
2311
 
2313
2312
  saveFriends(data);
2314
2313
  log({ event: 'friend_added', did, addedBy: options.addedBy });
2314
+ syncContactToPlatform(did, { alias: options.alias || '', notes: options.notes || '' }).catch(() => {});
2315
+ pushP2PNotification('p2p_contact_added', {
2316
+ peerDid: did,
2317
+ alias: options.alias || '',
2318
+ text: options.notes || ''
2319
+ }).catch((e) => log({ event: 'p2p_notify_error', kind: 'friend_added', error: e.message }));
2315
2320
  return true;
2316
2321
  }
2317
2322
 
@@ -2941,16 +2946,23 @@ function riskAllowed(maxRisk, requestedRisk) { return (RISK_ORDER[requestedRisk]
2941
2946
  // Verify anchor_tx list on-chain, return count of valid proofs
2942
2947
  async function verifyAnchorTxList(anchorTxList, targetDid) {
2943
2948
  if (!anchorTxList || anchorTxList.length === 0) return { verified: 0, total: 0, proofs: [] };
2944
- const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
2945
- const provider = new SolanaAnchorProvider({ rpcUrl });
2946
2949
  let verified = 0;
2947
2950
  const proofs = [];
2948
2951
  for (const tx of anchorTxList) {
2949
2952
  try {
2953
+ const chain = String(tx?.chain || detectPreferredChain() || 'base').toLowerCase();
2954
+ let provider;
2955
+ if (chain === 'base') {
2956
+ provider = new BaseAnchorProvider({ rpcUrl: process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org' });
2957
+ } else if (chain === 'bsc') {
2958
+ provider = new BSCAnchorProvider({ rpcUrl: process.env.ATEL_BSC_RPC_URL || 'https://bsc-dataseed.binance.org' });
2959
+ } else {
2960
+ continue;
2961
+ }
2950
2962
  const result = await provider.verify(tx.trace_root || '', tx.txHash || tx.anchor_tx || '');
2951
2963
  if (result.valid) {
2952
2964
  verified++;
2953
- 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() });
2965
+ proofs.push({ proof_id: tx.proof_id || tx.txHash, trace_root: tx.trace_root, verified: true, anchor_tx: tx.txHash || tx.anchor_tx, chain, timestamp: new Date().toISOString() });
2954
2966
  }
2955
2967
  } catch {}
2956
2968
  }
@@ -3021,12 +3033,7 @@ async function anchorOnChain(traceRoot, metadata, preferredChain) {
3021
3033
  if (!key) return null;
3022
3034
 
3023
3035
  let provider;
3024
- if (chain === 'solana') {
3025
- provider = new SolanaAnchorProvider({
3026
- rpcUrl: process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com',
3027
- privateKey: key
3028
- });
3029
- } else if (chain === 'base') {
3036
+ if (chain === 'base') {
3030
3037
  provider = new BaseAnchorProvider({
3031
3038
  rpcUrl: process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org',
3032
3039
  privateKey: key,
@@ -3125,7 +3132,7 @@ async function configureAnchor() {
3125
3132
  // 1. Select chain
3126
3133
  const chain = await promptChoice(
3127
3134
  'Select blockchain for anchoring:',
3128
- ['solana', 'bsc', 'base']
3135
+ ['base', 'bsc']
3129
3136
  );
3130
3137
 
3131
3138
  // 2. Input private key
@@ -3141,16 +3148,9 @@ async function configureAnchor() {
3141
3148
  // 3. Validate private key and derive address
3142
3149
  let address;
3143
3150
  try {
3144
- if (chain === 'solana') {
3145
- const { Keypair } = await import('@solana/web3.js');
3146
- const bs58 = (await import('bs58')).default;
3147
- const kp = Keypair.fromSecretKey(bs58.decode(privateKey));
3148
- address = kp.publicKey.toBase58();
3149
- } else {
3150
- const { ethers } = await import('ethers');
3151
- const wallet = new ethers.Wallet(privateKey);
3152
- address = wallet.address;
3153
- }
3151
+ const { ethers } = await import('ethers');
3152
+ const wallet = new ethers.Wallet(privateKey);
3153
+ address = wallet.address;
3154
3154
  } catch (e) {
3155
3155
  console.error(`❌ Invalid ${chain.toUpperCase()} private key: ${e.message}`);
3156
3156
  process.exit(1);
@@ -3667,6 +3667,15 @@ async function cmdStart(port) {
3667
3667
  log({ event: 'atel_skill_sync_error', error: e.message });
3668
3668
  }
3669
3669
 
3670
+ try {
3671
+ const syncedContacts = await syncAllFriendsToPlatform();
3672
+ if (syncedContacts.attempted > 0) {
3673
+ log({ event: 'contacts_backfill_sync_done', ...syncedContacts });
3674
+ }
3675
+ } catch (e) {
3676
+ log({ event: 'contacts_backfill_sync_error', error: e.message });
3677
+ }
3678
+
3670
3679
  // Initialize Ollama only if explicitly enabled (optional local AI audit)
3671
3680
  if (process.env.ATEL_OLLAMA_ENABLED === 'true') {
3672
3681
  await initializeOllama().catch(err => {
@@ -3705,14 +3714,6 @@ async function cmdStart(port) {
3705
3714
  if (!txHash || !traceRoot) return { checked: false, verified: false, reason: 'missing_anchor_or_root' };
3706
3715
  const c = (chain || 'base').toLowerCase();
3707
3716
 
3708
- if (c === 'solana') {
3709
- const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
3710
- if (process.env.ATEL_DEBUG) console.error('[DEBUG] Solana RPC URL:', rpcUrl);
3711
- const provider = new SolanaAnchorProvider({ rpcUrl });
3712
- const r = await provider.verify(traceRoot, txHash);
3713
- if (process.env.ATEL_DEBUG) console.error('[DEBUG] Solana verify result:', r);
3714
- return { checked: true, verified: !!r?.valid, chain: 'solana', detail: r?.detail };
3715
- }
3716
3717
  if (c === 'base') {
3717
3718
  const rpcUrl = process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org';
3718
3719
  if (process.env.ATEL_DEBUG) console.error('[DEBUG] Base RPC URL:', rpcUrl);
@@ -4024,6 +4025,22 @@ async function cmdStart(port) {
4024
4025
  const MAX_HOOK_CONCURRENCY = Math.max(1, Math.min(4, Number.parseInt(process.env.ATEL_HOOK_CONCURRENCY || '3', 10) || 3));
4025
4026
  const hookQueue = [];
4026
4027
  const activeRecoveryKeys = new Set();
4028
+ const recoveryKeyLogState = new Map();
4029
+
4030
+ function logRecoveryKeyActive(eventType, dedupeKey, recoveryKey) {
4031
+ if (!recoveryKey) return;
4032
+ const now = Date.now();
4033
+ const previous = recoveryKeyLogState.get(recoveryKey) || { lastLoggedAt: 0, suppressed: 0 };
4034
+ if (now - previous.lastLoggedAt < 15000) {
4035
+ previous.suppressed += 1;
4036
+ recoveryKeyLogState.set(recoveryKey, previous);
4037
+ return;
4038
+ }
4039
+ const payload = { event: 'agent_hook_not_queued', eventType, dedupeKey, reason: 'recovery_key_active', recoveryKey };
4040
+ if (previous.suppressed > 0) payload.suppressed = previous.suppressed;
4041
+ log(payload);
4042
+ recoveryKeyLogState.set(recoveryKey, { lastLoggedAt: now, suppressed: 0 });
4043
+ }
4027
4044
 
4028
4045
  endpoint.app?.post?.('/atel/v1/agent-callback', async (req, res) => {
4029
4046
  const body = req.body || {};
@@ -4258,6 +4275,15 @@ async function cmdStart(port) {
4258
4275
  'urllib.request.urlopen(req, timeout=10).read()',
4259
4276
  'PY',
4260
4277
  ].join('\n'),
4278
+ executor_milestone: [
4279
+ "python3 - <<'PY'",
4280
+ 'import json, urllib.request',
4281
+ `result = """Write the final deliverable here"""`,
4282
+ `body = {"dedupeKey": "${dedupeKey}", "status": "done", "eventType": "${eventType}", "orderId": "${payload?.orderId || ''}", "milestoneIndex": ${Number.isFinite(Number(payload?.currentMilestone)) ? Number(payload.currentMilestone) : Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0}, "result": result, "summary": result[:120]}`,
4283
+ `req = urllib.request.Request("${callbackUrl}", data=json.dumps(body).encode(), headers={"Content-Type": "application/json"})`,
4284
+ 'urllib.request.urlopen(req, timeout=10).read()',
4285
+ 'PY',
4286
+ ].join('\n'),
4261
4287
  default: [
4262
4288
  "python3 - <<'PY'",
4263
4289
  'import json, urllib.request',
@@ -4278,7 +4304,11 @@ async function cmdStart(port) {
4278
4304
  'PY',
4279
4305
  ].join('\n');
4280
4306
 
4281
- const callbackDone = eventType === 'milestone_submitted' ? callbackExamples.milestone_submitted : callbackExamples.default;
4307
+ const callbackDone = eventType === 'milestone_submitted'
4308
+ ? callbackExamples.milestone_submitted
4309
+ : (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)
4310
+ ? callbackExamples.executor_milestone
4311
+ : callbackExamples.default);
4282
4312
  const contextFile = join(cwd, 'ORDER_CONTEXT.md');
4283
4313
  const allowRepoAccess = shouldAllowRepoAccess(payload);
4284
4314
  const fileAccessRule = allowRepoAccess
@@ -4326,7 +4356,7 @@ ${callbackFailed}
4326
4356
  `;
4327
4357
  }
4328
4358
 
4329
- function buildLocalAgentPrompt(eventType, promptText) {
4359
+ function buildLocalAgentPrompt(eventType, promptText, payload = {}) {
4330
4360
  if (eventType === 'milestone_submitted') {
4331
4361
  return `${promptText}
4332
4362
 
@@ -4342,6 +4372,10 @@ For rejection:
4342
4372
  }
4343
4373
 
4344
4374
  if (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)) {
4375
+ const expectedIndex = eventType === 'milestone_plan_confirmed'
4376
+ ? (Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0)
4377
+ : (Number.isFinite(Number(payload?.currentMilestone)) ? Number(payload.currentMilestone) : Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0);
4378
+ const expectedOrderId = String(payload?.orderId || '').trim();
4345
4379
  return `${promptText}
4346
4380
 
4347
4381
  Important: you are not chatting with the user.
@@ -4355,9 +4389,11 @@ You must prefer factual evidence:
4355
4389
  - what concrete output / state / observation you actually got
4356
4390
  - whether that evidence satisfies the current milestone
4357
4391
  If you could not find evidence, say that explicitly in the JSON result, including what you checked.
4392
+ The JSON must include the exact current order and milestone identity.
4393
+ If you are not certain, fail closed instead of guessing.
4358
4394
 
4359
4395
  Format:
4360
- {"result":"factual deliverable with concrete evidence and observations only"}`;
4396
+ {"orderId":"${expectedOrderId}","milestoneIndex":${expectedIndex},"result":"factual deliverable with concrete evidence and observations only"}`;
4361
4397
  }
4362
4398
 
4363
4399
  return promptText;
@@ -4486,7 +4522,7 @@ Format:
4486
4522
  if (isKnownInvalidLocalAgentStdout(cleaned)) {
4487
4523
  return { ok: false, error: 'invalid_local_agent_stdout_known_failure' };
4488
4524
  }
4489
- return buildAgentCallbackAction(eventType, payload, { result: cleaned, summary: cleaned });
4525
+ return { ok: false, error: 'invalid_local_agent_stdout_requires_json' };
4490
4526
  }
4491
4527
  }
4492
4528
 
@@ -4609,7 +4645,7 @@ Format:
4609
4645
  const recoveryKey = options.recoveryKey || '';
4610
4646
  if (recoveryKey) {
4611
4647
  if (activeRecoveryKeys.has(recoveryKey)) {
4612
- log({ event: 'agent_hook_not_queued', eventType, dedupeKey, reason: 'recovery_key_active', recoveryKey });
4648
+ logRecoveryKeyActive(eventType, dedupeKey, recoveryKey);
4613
4649
  return false;
4614
4650
  }
4615
4651
  activeRecoveryKeys.add(recoveryKey);
@@ -5143,6 +5179,22 @@ Advance the current milestone strictly based on these approved results. Do not i
5143
5179
  let directExecutionSucceeded = false;
5144
5180
  const rejectLimitReached = event === 'milestone_rejected' && Number(payload?.submitCount || body?.submitCount || 0) >= 3;
5145
5181
  const directActions = rejectLimitReached ? [] : getDirectExecutableActions(event, recommendedActions, payload, currentPolicy);
5182
+ if (event === 'order_created' && directActions.length === 0 && !rejectLimitReached) {
5183
+ const skipReason = explainDirectExecutionSkip(event, recommendedActions, payload, currentPolicy);
5184
+ if (skipReason) {
5185
+ log({
5186
+ event: 'order_created_auto_accept_skipped',
5187
+ orderId: payload?.orderId || body?.orderId || '',
5188
+ amount: Number(payload?.priceAmount || 0),
5189
+ reason: skipReason,
5190
+ });
5191
+ pushTradeNotification('order_created_auto_accept_skipped', {
5192
+ ...payload,
5193
+ orderId: payload?.orderId || body?.orderId || '',
5194
+ reasonCode: skipReason,
5195
+ }, body).catch(e => log({ event: 'trade_notify_error', error: e.message }));
5196
+ }
5197
+ }
5146
5198
  if (rejectLimitReached) {
5147
5199
  log({ event: 'direct_action_skip_manual_arbitration', eventType: event, dedupeKey, orderId: payload?.orderId || body?.orderId || '', milestoneIndex: payload?.milestoneIndex ?? body?.milestoneIndex ?? null, reason: 'rejection_limit_reached' });
5148
5200
  }
@@ -5222,7 +5274,10 @@ Advance the current milestone strictly based on these approved results. Do not i
5222
5274
  }
5223
5275
  const { execFile } = await import('child_process');
5224
5276
  const finishHook = () => {
5225
- if (recoveryKey) activeRecoveryKeys.delete(recoveryKey);
5277
+ if (recoveryKey) {
5278
+ activeRecoveryKeys.delete(recoveryKey);
5279
+ recoveryKeyLogState.delete(recoveryKey);
5280
+ }
5226
5281
  activeHookWorkers = Math.max(0, activeHookWorkers - 1);
5227
5282
  processHookQueue();
5228
5283
  };
@@ -5237,15 +5292,15 @@ Advance the current milestone strictly based on these approved results. Do not i
5237
5292
  return;
5238
5293
  }
5239
5294
  log({ event: 'agent_session_spawn_error', eventType: hookEvent, dedupeKey: hookKey, error: gatewayResult.error, fallback: 'cli' });
5240
- spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg);
5295
+ spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg, hookPayload);
5241
5296
  } else if (spawnArgs.length > 0) {
5242
5297
  const promptArg = spawnArgs[spawnArgs.length - 1] || '';
5243
- spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg);
5298
+ spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg, hookPayload);
5244
5299
  }
5245
5300
 
5246
5301
  const MAX_ATTEMPTS = 5;
5247
5302
  const isMilestoneHook = ['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected', 'milestone_submitted'].includes(hookEvent);
5248
- const localHookTimeoutMs = isMilestoneHook ? 90000 : 600000;
5303
+ const localHookTimeoutMs = isMilestoneHook ? 240000 : 600000;
5249
5304
  const preparedInvocation = prepareHookInvocation(spawnCmd, spawnArgs, hookKey, Math.ceil(localHookTimeoutMs / 1000));
5250
5305
  const runHook = (attempt, invocation = preparedInvocation) => {
5251
5306
  const hookStartedAt = Date.now();
@@ -5543,7 +5598,7 @@ Advance the current milestone strictly based on these approved results. Do not i
5543
5598
 
5544
5599
  // ── Anchoring Warning ──
5545
5600
  if (!anchor) {
5546
- log({ event: 'anchor_missing', taskId, warning: 'Proof not anchored on-chain. Set ATEL_SOLANA_PRIVATE_KEY for verifiable trust.', timestamp: new Date().toISOString() });
5601
+ log({ event: 'anchor_missing', taskId, warning: 'Proof not anchored on-chain. Set a supported anchor key for verifiable trust.', timestamp: new Date().toISOString() });
5547
5602
  }
5548
5603
 
5549
5604
 
@@ -7129,22 +7184,32 @@ async function cmdVerifyProof(anchorTx, traceRoot) {
7129
7184
 
7130
7185
  console.log(JSON.stringify({ event: 'verifying_proof', anchor_tx: anchorTx, trace_root: traceRoot }));
7131
7186
 
7132
- const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
7133
- try {
7134
- const provider = new SolanaAnchorProvider({ rpcUrl });
7135
- const result = await provider.verify(traceRoot, anchorTx);
7136
- console.log(JSON.stringify({
7137
- verified: result.valid,
7138
- chain: 'solana',
7139
- anchor_tx: anchorTx,
7140
- trace_root: traceRoot,
7141
- detail: result.detail || (result.valid ? 'Memo matches trace_root' : 'Memo does not match'),
7142
- block: result.blockNumber,
7143
- timestamp: result.timestamp,
7144
- }, null, 2));
7145
- } catch (e) {
7146
- console.log(JSON.stringify({ verified: false, error: e.message }));
7187
+ const providers = [
7188
+ { chain: 'base', provider: new BaseAnchorProvider({ rpcUrl: process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org' }) },
7189
+ { chain: 'bsc', provider: new BSCAnchorProvider({ rpcUrl: process.env.ATEL_BSC_RPC_URL || 'https://bsc-dataseed.binance.org' }) },
7190
+ ];
7191
+ const attempts = [];
7192
+ for (const entry of providers) {
7193
+ try {
7194
+ const result = await entry.provider.verify(traceRoot, anchorTx);
7195
+ attempts.push({ chain: entry.chain, result });
7196
+ if (result.valid) {
7197
+ console.log(JSON.stringify({
7198
+ verified: true,
7199
+ chain: entry.chain,
7200
+ anchor_tx: anchorTx,
7201
+ trace_root: traceRoot,
7202
+ detail: result.detail || 'Anchor matches trace_root',
7203
+ block: result.blockNumber,
7204
+ timestamp: result.timestamp,
7205
+ }, null, 2));
7206
+ return;
7207
+ }
7208
+ } catch (e) {
7209
+ attempts.push({ chain: entry.chain, error: e.message });
7210
+ }
7147
7211
  }
7212
+ console.log(JSON.stringify({ verified: false, anchor_tx: anchorTx, trace_root: traceRoot, attempts }, null, 2));
7148
7213
  }
7149
7214
 
7150
7215
  async function cmdAudit(targetDidOrUrl, taskId) {
@@ -7256,13 +7321,16 @@ async function cmdRotate() {
7256
7321
 
7257
7322
  // Anchor rotation on-chain if possible
7258
7323
  let anchor = null;
7259
- const key = process.env.ATEL_SOLANA_PRIVATE_KEY;
7324
+ const chain = detectPreferredChain() || 'base';
7325
+ const key = getChainPrivateKey(chain);
7260
7326
  if (key) {
7261
7327
  try {
7262
- const s = new SolanaAnchorProvider({ rpcUrl: process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com', privateKey: key });
7328
+ const provider = chain === 'bsc'
7329
+ ? new BSCAnchorProvider({ rpcUrl: process.env.ATEL_BSC_RPC_URL || 'https://bsc-dataseed.binance.org', privateKey: key })
7330
+ : new BaseAnchorProvider({ rpcUrl: process.env.ATEL_BASE_RPC_URL || 'https://mainnet.base.org', privateKey: key, anchorRegistryAddress: process.env.ATEL_ANCHOR_REGISTRY_ADDRESS || undefined });
7263
7331
  const { createHash } = await import('node:crypto');
7264
7332
  const rotationHash = createHash('sha256').update(JSON.stringify(proof)).digest('hex');
7265
- anchor = await s.anchor(`rotation:${rotationHash}`, { oldDid, newDid: newIdentity.did, type: 'key_rotation' });
7333
+ anchor = await provider.anchor(`rotation:${rotationHash}`, { oldDid, newDid: newIdentity.did, type: 'key_rotation' });
7266
7334
  } catch (e) { console.log(JSON.stringify({ warning: 'On-chain anchor failed', error: e.message })); }
7267
7335
  }
7268
7336
 
@@ -7293,6 +7361,42 @@ async function cmdRotate() {
7293
7361
 
7294
7362
  const PLATFORM_URL = ATEL_PLATFORM;
7295
7363
 
7364
+ async function syncContactToPlatform(contactDid, options = {}) {
7365
+ const id = requireIdentity();
7366
+ const normalized = String(contactDid || '').trim();
7367
+ if (!normalized || normalized === id.did) return { ok: false, reason: 'invalid_contact' };
7368
+ try {
7369
+ await signedFetch('POST', '/contacts/v1/sync', {
7370
+ contactDid: normalized,
7371
+ alias: String(options.alias || '').trim(),
7372
+ notes: String(options.notes || '').trim(),
7373
+ });
7374
+ log({ event: 'contact_sync_ok', contactDid: normalized });
7375
+ return { ok: true };
7376
+ } catch (e) {
7377
+ log({ event: 'contact_sync_failed', contactDid: normalized, error: e.message || 'unknown_error' });
7378
+ return { ok: false, reason: e.message || 'sync_failed' };
7379
+ }
7380
+ }
7381
+
7382
+ async function syncAllFriendsToPlatform() {
7383
+ const friends = loadFriends();
7384
+ const accepted = Array.isArray(friends?.friends) ? friends.friends : [];
7385
+ if (accepted.length === 0) return { attempted: 0, synced: 0, failed: 0 };
7386
+ let synced = 0;
7387
+ let failed = 0;
7388
+ for (const friend of accepted) {
7389
+ const result = await syncContactToPlatform(friend.did, {
7390
+ alias: friend.alias || '',
7391
+ notes: friend.notes || ''
7392
+ });
7393
+ if (result.ok) synced += 1;
7394
+ else failed += 1;
7395
+ }
7396
+ log({ event: 'contacts_backfill_sync', attempted: accepted.length, synced, failed });
7397
+ return { attempted: accepted.length, synced, failed };
7398
+ }
7399
+
7296
7400
  async function signedFetch(method, path, payload = {}) {
7297
7401
  const id = requireIdentity();
7298
7402
  const { default: nacl } = await import('tweetnacl');
@@ -7424,6 +7528,10 @@ async function cmdWithdraw(amount, address, chain) {
7424
7528
  }
7425
7529
  // Smart wallet withdrawal (new flow)
7426
7530
  chain = chain || 'base';
7531
+ if (chain !== 'base' && chain !== 'bsc') {
7532
+ console.error('Unsupported withdrawal chain. Supported chains: base, bsc.');
7533
+ process.exit(1);
7534
+ }
7427
7535
 
7428
7536
  // Validate destination address
7429
7537
  const channel = 'crypto_' + chain;
@@ -7432,11 +7540,6 @@ async function cmdWithdraw(amount, address, chain) {
7432
7540
  console.error('Invalid EVM address. Must be 0x followed by 40 hex characters.');
7433
7541
  process.exit(1);
7434
7542
  }
7435
- } else if (channel === 'crypto_solana') {
7436
- if (!address.match(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/)) {
7437
- console.error('Invalid Solana address.');
7438
- process.exit(1);
7439
- }
7440
7543
  }
7441
7544
 
7442
7545
  try {
@@ -8132,9 +8235,9 @@ async function cmdComplete(orderId, taskId) {
8132
8235
  console.error('[complete] Anchoring proof on-chain...');
8133
8236
  anchor = await anchorOnChain(proof.trace_root, { proof_id: proof.proof_id, executorDid: id.did, requesterDid, taskId: effectiveTaskId, action: 'cli-complete' });
8134
8237
  if (anchor) {
8135
- console.error(`[complete] Anchored on Solana: ${anchor.txHash}`);
8238
+ console.error(`[complete] Anchored on-chain: ${anchor.txHash}`);
8136
8239
  } else {
8137
- console.error('[complete] WARNING: On-chain anchoring failed. Set ATEL_SOLANA_PRIVATE_KEY for verifiable trust.');
8240
+ console.error('[complete] WARNING: On-chain anchoring failed. Set a supported anchor key for verifiable trust.');
8138
8241
  }
8139
8242
  }
8140
8243
 
@@ -10558,7 +10661,7 @@ Auth Commands:
10558
10661
 
10559
10662
  Account Commands:
10560
10663
  balance Show platform account balance
10561
- deposit <amount> [channel] Deposit funds (channel: manual|crypto_solana|crypto_base|crypto_bsc|stripe|alipay)
10664
+ deposit <amount> [channel] Deposit funds (channel: manual|crypto_base|crypto_bsc|stripe|alipay)
10562
10665
  withdraw <amount> [channel] [address] Withdraw funds (address required for crypto)
10563
10666
  transactions List payment history
10564
10667
 
@@ -10690,8 +10793,6 @@ Environment:
10690
10793
  ATEL_REGISTRY Registry URL (default: https://api.atelai.org)
10691
10794
  ATEL_PLATFORM Platform URL (default: ATEL_REGISTRY value)
10692
10795
  ATEL_EXECUTOR_URL Local executor HTTP endpoint
10693
- ATEL_SOLANA_PRIVATE_KEY Solana key for on-chain anchoring
10694
- ATEL_SOLANA_RPC_URL Solana RPC (default: mainnet-beta)
10695
10796
  ATEL_BASE_PRIVATE_KEY Base chain key for on-chain anchoring
10696
10797
  ATEL_BSC_PRIVATE_KEY BSC chain key for on-chain anchoring
10697
10798