@lawrenceliang-btc/atel-sdk 0.9.23 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/atel.mjs CHANGED
@@ -56,7 +56,7 @@ import { resolve, join } from 'node:path';
56
56
  import crypto from 'node:crypto';
57
57
  import {
58
58
  AgentIdentity, AgentEndpoint, AgentClient, HandshakeManager,
59
- createMessage, RegistryClient, ExecutionTrace, ProofGenerator,
59
+ createMessage, verifyMessage, parseDID, RegistryClient, ExecutionTrace, ProofGenerator,
60
60
  SolanaAnchorProvider, BaseAnchorProvider, BSCAnchorProvider,
61
61
  autoNetworkSetup, collectCandidates, connectToAgent,
62
62
  discoverPublicIP, checkReachable, ContentAuditor, TrustScoreClient,
@@ -64,7 +64,9 @@ import {
64
64
  TrustGraph, calculateTaskWeight,
65
65
  } from '@lawrenceliang-btc/atel-sdk';
66
66
  import { TunnelManager, HeartbeatManager } from './tunnel-manager.mjs';
67
- import { initializeOllama, getOllamaStatus } from './ollama-manager.mjs';
67
+ // ollama-manager removed SDK does not run local models
68
+ const initializeOllama = async () => {};
69
+ const getOllamaStatus = async () => ({ running: false, models: [] });
68
70
  import { parseAttachmentFlags, processAttachments } from './atel-attachment-helpers.mjs';
69
71
 
70
72
  const ATEL_DIR = resolve(process.env.ATEL_DIR || '.atel');
@@ -258,6 +260,23 @@ Friend Management Commands:
258
260
  Examples:
259
261
  atel friend status
260
262
 
263
+ ── How Friend System Affects Communication ──────────────────────────
264
+
265
+ By default, agents operate in "friends_only" mode:
266
+ • P2P messages (atel send) and tasks will be REJECTED if you are not a friend
267
+ • You must send a friend request and have it accepted first
268
+
269
+ Typical flow to communicate with a new agent:
270
+ 1. atel friend request <their-did> --message "Hi, lets connect!"
271
+ 2. They run: atel friend pending (to see your request)
272
+ 3. They run: atel friend accept <req-id> (to accept)
273
+ 4. Now you can: atel send <their-did> "Hello!"
274
+
275
+ To allow communication WITHOUT friend relationship (open mode):
276
+ Edit .atel/policy.json and set:
277
+ { "relationshipPolicy": { "defaultMode": "open" } }
278
+ Restart your agent. Both sides can configure this independently.
279
+
261
280
  For more information, visit: https://docs.atel.io/friend-system
262
281
  `);
263
282
  }
@@ -1927,25 +1946,13 @@ async function cmdStart(port) {
1927
1946
  const toolGatewayServer = await startToolGatewayProxy(toolProxyPort, id, policy);
1928
1947
  log({ event: 'tool_gateway_started', port: toolProxyPort });
1929
1948
 
1930
- // ── Built-in Executor (auto-start if no external ATEL_EXECUTOR_URL) ──
1931
- let builtinExecutor = null;
1932
- if (!EXECUTOR_URL) {
1933
- const executorPort = p + 2;
1934
- try {
1935
- const { BuiltinExecutor } = await import('../dist/executor/index.js');
1936
- builtinExecutor = new BuiltinExecutor({
1937
- port: executorPort,
1938
- callbackUrl: `http://127.0.0.1:${p}/atel/v1/result`,
1939
- gatewayUrl: getGatewayUrl(),
1940
- contextPath: join(ATEL_DIR, 'agent-context.md'),
1941
- log,
1942
- });
1943
- await builtinExecutor.start();
1944
- EXECUTOR_URL = `http://127.0.0.1:${executorPort}`;
1945
- log({ event: 'builtin_executor_started', port: executorPort, url: EXECUTOR_URL });
1946
- } catch (e) {
1947
- log({ event: 'builtin_executor_failed', error: e.message, note: 'Falling back to echo mode. Set ATEL_EXECUTOR_URL for external executor.' });
1948
- }
1949
+ // ── Executor: Agent brings their own AI. SDK does not run a built-in executor. ──
1950
+ // If ATEL_EXECUTOR_URL is set, P2P tasks are forwarded there.
1951
+ // Milestone work is done by the Agent itself (not the SDK).
1952
+ if (EXECUTOR_URL) {
1953
+ log({ event: 'external_executor_configured', url: EXECUTOR_URL });
1954
+ } else {
1955
+ log({ event: 'no_executor', note: 'No ATEL_EXECUTOR_URL set. Agent handles tasks via notifications + CLI.' });
1949
1956
  }
1950
1957
 
1951
1958
  // ── Trust Score Client (persistent) ──
@@ -2067,17 +2074,74 @@ async function cmdStart(port) {
2067
2074
  }
2068
2075
  });
2069
2076
 
2070
- // Webhook notification: POST /atel/v1/notify (platform calls this for order events)
2077
+ // ── Event dedup (processed eventIds) ──
2078
+ const processedEvents = new Set();
2079
+
2080
+ // Webhook notification: POST /atel/v1/notify
2081
+ // SDK only: logs, writes inbox, prints prompt. Does NOT execute any actions.
2082
+ // Agent reads inbox or webhook to decide what to do.
2071
2083
  endpoint.app?.post?.('/atel/v1/notify', async (req, res) => {
2072
- const { event, payload } = req.body || {};
2073
- if (!event || !payload) {
2074
- res.status(400).json({ error: 'event and payload required' });
2084
+ const body = req.body || {};
2085
+ // Support both old format {event, payload} and new format {eventId, eventType, payload}
2086
+ const event = body.eventType || body.event;
2087
+ const payload = body.payload || {};
2088
+ const eventId = body.eventId;
2089
+ const prompt = body.prompt || payload.prompt;
2090
+ const recommendedActions = body.recommendedActions || payload.recommendedActions;
2091
+
2092
+ if (!event) {
2093
+ res.status(400).json({ error: 'event/eventType required' });
2075
2094
  return;
2076
2095
  }
2077
2096
 
2078
- log({ event: 'webhook_received', type: event, payload });
2097
+ // Dedup: skip already processed events
2098
+ if (eventId && processedEvents.has(eventId)) {
2099
+ log({ event: 'event_dedup_skip', eventId, eventType: event });
2100
+ res.json({ status: 'already_processed', eventId });
2101
+ return;
2102
+ }
2103
+ if (eventId) {
2104
+ processedEvents.add(eventId);
2105
+ // Keep set bounded
2106
+ if (processedEvents.size > 1000) {
2107
+ const first = processedEvents.values().next().value;
2108
+ processedEvents.delete(first);
2109
+ }
2110
+ }
2111
+
2112
+ // Log the full event
2113
+ log({ event: 'notification_received', eventId, eventType: event, orderId: body.orderId || payload.orderId, prompt: prompt ? prompt.substring(0, 100) : undefined });
2114
+
2115
+ // ── Universal notification handler: print prompt + log + respond ──
2116
+ // SDK does NOT execute any actions. Agent reads inbox/webhook and decides.
2117
+ if (prompt) {
2118
+ console.log(`\n📨 [${event}] ${prompt.split('\n')[0]}`);
2119
+ }
2120
+ if (recommendedActions && Array.isArray(recommendedActions)) {
2121
+ for (const action of recommendedActions) {
2122
+ if (action.command) console.log(` → ${action.command.join(' ')}`);
2123
+ }
2124
+ }
2125
+ console.log('');
2126
+
2127
+ // Write full event to inbox for Agent program to read
2128
+ log({
2129
+ event: 'notification',
2130
+ eventId,
2131
+ eventType: event,
2132
+ orderId: body.orderId || payload.orderId,
2133
+ payload,
2134
+ prompt,
2135
+ recommendedActions,
2136
+ sequenceNo: body.sequenceNo,
2137
+ dedupeKey: body.dedupeKey,
2138
+ });
2139
+
2140
+ res.json({ status: 'received', eventId, eventType: event });
2141
+ return;
2079
2142
 
2080
- if (event === 'order_created') {
2143
+ // ── Legacy event handlers below (kept but unreachable for reference) ──
2144
+ if (event === 'order_created_legacy') {
2081
2145
  // New order notification - decide whether to accept
2082
2146
  const { orderId, requesterDid, capabilityType, priceAmount, description } = payload;
2083
2147
 
@@ -2178,35 +2242,55 @@ async function cmdStart(port) {
2178
2242
  return;
2179
2243
  }
2180
2244
 
2181
- // Auto-accept (default)
2245
+ // Auto-accept with retry (default)
2182
2246
  log({ event: 'order_auto_accept', orderId, requesterDid, capabilityType });
2183
-
2184
- // Call platform API to accept
2185
- try {
2186
- const timestamp = new Date().toISOString(); // RFC3339 format
2187
- const payload = {}; // Empty payload for accept
2188
- const signPayload = { did: id.did, timestamp, payload };
2189
- const signature = sign(signPayload, id.secretKey);
2190
-
2191
- const signedRequest = {
2192
- did: id.did,
2193
- timestamp,
2194
- signature,
2195
- payload
2196
- };
2197
-
2198
- log({ event: 'order_accept_calling_api', orderId, platform: ATEL_PLATFORM });
2199
-
2200
- const acceptResp = await fetch(`${ATEL_PLATFORM}/trade/v1/order/${orderId}/accept`, {
2201
- method: 'POST',
2202
- headers: { 'Content-Type': 'application/json' },
2203
- body: JSON.stringify(signedRequest),
2204
- signal: AbortSignal.timeout(10000), // 10秒超时
2205
- });
2206
-
2207
- log({ event: 'order_accept_response', orderId, status: acceptResp.status, ok: acceptResp.ok });
2208
-
2209
- if (acceptResp.ok) {
2247
+
2248
+ // Call platform API to accept (retry up to 3 times for transient RPC failures)
2249
+ let acceptOk = false;
2250
+ let lastError = '';
2251
+ for (let attempt = 0; attempt < 3; attempt++) {
2252
+ try {
2253
+ const timestamp = new Date().toISOString();
2254
+ const payload = {};
2255
+ const signPayload = { did: id.did, timestamp, payload };
2256
+ const signature = sign(signPayload, id.secretKey);
2257
+ const signedRequest = { did: id.did, timestamp, signature, payload };
2258
+
2259
+ log({ event: 'order_accept_calling_api', orderId, platform: ATEL_PLATFORM, attempt: attempt + 1 });
2260
+
2261
+ const acceptResp = await fetch(`${ATEL_PLATFORM}/trade/v1/order/${orderId}/accept`, {
2262
+ method: 'POST',
2263
+ headers: { 'Content-Type': 'application/json' },
2264
+ body: JSON.stringify(signedRequest),
2265
+ signal: AbortSignal.timeout(30000), // 30s timeout (escrow tx can take 10-15s)
2266
+ });
2267
+
2268
+ log({ event: 'order_accept_response', orderId, status: acceptResp.status, ok: acceptResp.ok, attempt: attempt + 1 });
2269
+
2270
+ if (acceptResp.ok) {
2271
+ acceptOk = true;
2272
+ break;
2273
+ }
2274
+
2275
+ const errBody = await acceptResp.text();
2276
+ lastError = errBody;
2277
+ log({ event: 'order_accept_failed', orderId, error: errBody, status: acceptResp.status, attempt: attempt + 1 });
2278
+
2279
+ // Retry if server hints it's a transient failure
2280
+ const shouldRetry = errBody.includes('retry') || errBody.includes('RPC') || errBody.includes('balance check failed');
2281
+ if (!shouldRetry || attempt >= 2) break;
2282
+ log({ event: 'order_accept_retrying', orderId, waitMs: 5000 });
2283
+ await new Promise(r => setTimeout(r, 5000));
2284
+ } catch (err) {
2285
+ lastError = err.message;
2286
+ log({ event: 'order_accept_error', orderId, error: err.message, attempt: attempt + 1 });
2287
+ if (attempt >= 2) break;
2288
+ await new Promise(r => setTimeout(r, 5000));
2289
+ }
2290
+ }
2291
+
2292
+ if (acceptOk) {
2293
+ try {
2210
2294
  log({ event: 'order_accepted', orderId });
2211
2295
  res.json({ status: 'accepted', orderId });
2212
2296
 
@@ -2243,15 +2327,12 @@ async function cmdStart(port) {
2243
2327
  log({ event: 'milestone_auto_approve_error', orderId, error: e.message });
2244
2328
  }
2245
2329
  }, 2000);
2246
- } else {
2247
- const error = await acceptResp.text();
2248
- log({ event: 'order_accept_failed', orderId, error, status: acceptResp.status });
2249
- res.status(500).json({ error: 'accept failed: ' + error });
2330
+ } catch (e) {
2331
+ log({ event: 'milestone_auto_approve_outer_error', orderId, error: e.message });
2250
2332
  }
2251
- } catch (err) {
2252
- log({ event: 'order_accept_error', orderId, error: err.message, stack: err.stack });
2253
- console.error('[ERROR] Order accept failed:', err);
2254
- res.status(500).json({ error: err.message });
2333
+ } else {
2334
+ log({ event: 'order_accept_gave_up', orderId, lastError });
2335
+ res.status(500).json({ error: 'accept failed after retries: ' + lastError });
2255
2336
  }
2256
2337
  return;
2257
2338
  }
@@ -2345,25 +2426,31 @@ async function cmdStart(port) {
2345
2426
  return;
2346
2427
  }
2347
2428
 
2348
- // Milestone events
2429
+ // Milestone events (helpers defined above, shared with /atel/v1/result handler)
2349
2430
  if (event === 'milestone_submitted') {
2350
2431
  const { orderId, milestoneIndex, resultSummary, submitCount } = payload;
2351
- log({
2352
- event: 'milestone_submitted_notification',
2353
- orderId,
2354
- milestoneIndex,
2355
- resultSummary: resultSummary || '(no summary)',
2356
- submitCount,
2357
- message: `M${milestoneIndex} submitted. Review the content and verify:`,
2358
- action: `atel milestone-verify ${orderId} ${milestoneIndex} --pass (or --reject "reason")`,
2359
- });
2360
- // Print prominently so the agent/user actually sees the content
2361
- console.log(`\n📋 [Milestone M${milestoneIndex} submitted for ${orderId}]`);
2362
- console.log(` Content: ${resultSummary || '(no summary provided)'}`);
2363
- console.log(` Attempt: ${submitCount || 1}/3`);
2364
- console.log(` Action: atel milestone-verify ${orderId} ${milestoneIndex} --pass`);
2365
- console.log(` atel milestone-verify ${orderId} ${milestoneIndex} --reject "reason"\n`);
2432
+ log({ event: 'milestone_submitted_notification', orderId, milestoneIndex, resultSummary: resultSummary || '(no summary)', submitCount });
2433
+ console.log(`\n📋 M${milestoneIndex} submitted for ${orderId}. Agent reviewing...`);
2366
2434
  res.json({ status: 'received', event });
2435
+ // Requester Agent: auto-review using agent's own brain (async callback)
2436
+ setTimeout(async () => {
2437
+ try {
2438
+ const ms = await getMilestoneInfo(orderId);
2439
+ if (!ms || !ms.milestones) return;
2440
+ const m = ms.milestones[milestoneIndex];
2441
+ if (!m) return;
2442
+ const orderDesc = ms.description || orderId;
2443
+ const submittedContent = m.result_summary || resultSummary || '(no content)';
2444
+ const prompt = `You are an AI Agent reviewing a milestone submission.\n\nOrder: ${orderDesc}\nMilestone M${milestoneIndex}: ${m.description || m.title}\nSubmitted content: ${submittedContent}\n\nEvaluate: Does the submission adequately fulfill the milestone goal?\nReply with EXACTLY one of:\n- PASS\n- REJECT: <specific reason>`;
2445
+ console.log(`\n🔍 Reviewing M${milestoneIndex} for ${orderId}...`);
2446
+ await agentExecute(prompt, `review-${orderId}-${milestoneIndex}`, { orderId, milestoneIndex, type: 'verify' });
2447
+ } catch (e) {
2448
+ // Fallback: auto-pass if agent brain unavailable
2449
+ log({ event: 'auto_review_fallback', orderId, milestone: milestoneIndex, error: e.message });
2450
+ console.log(`\n✅ Auto-pass M${milestoneIndex} (review fallback)`);
2451
+ await autoMilestoneVerify(orderId, milestoneIndex, true);
2452
+ }
2453
+ }, 3000);
2367
2454
  return;
2368
2455
  }
2369
2456
 
@@ -2371,8 +2458,23 @@ async function cmdStart(port) {
2371
2458
  const { orderId, milestoneIndex, currentMilestone, totalMilestones, allComplete } = payload;
2372
2459
  if (allComplete) {
2373
2460
  log({ event: 'all_milestones_complete', orderId, message: 'Settlement in progress' });
2461
+ console.log(`\n🎉 All milestones complete for ${orderId}. Settlement in progress.`);
2374
2462
  } else {
2375
- log({ event: 'milestone_verified_notification', orderId, milestoneIndex, next: currentMilestone, message: `M${milestoneIndex} verified. Ready to submit M${currentMilestone}` });
2463
+ log({ event: 'milestone_verified_notification', orderId, milestoneIndex, next: currentMilestone });
2464
+ console.log(`\n✅ M${milestoneIndex} verified for ${orderId}. Working on M${currentMilestone}...`);
2465
+ // Auto-submit next milestone using agent's own brain (async callback)
2466
+ setTimeout(async () => {
2467
+ try {
2468
+ const ms = await getMilestoneInfo(orderId);
2469
+ if (!ms || !ms.milestones) return;
2470
+ const nextM = ms.milestones[currentMilestone];
2471
+ if (!nextM) return;
2472
+ const orderDesc = ms.description || orderId;
2473
+ const prompt = `You are an AI Agent executing a paid task.\n\nOrder: ${orderDesc}\nMilestone M${currentMilestone}: ${nextM.description || nextM.title}\n\nPrevious milestones have been completed and approved. Now complete this milestone. Provide a thorough, detailed deliverable. Output ONLY the deliverable content.`;
2474
+ console.log(`\n🤖 Working on M${currentMilestone} for ${orderId}...`);
2475
+ await agentExecute(prompt, `milestone-${orderId}-${currentMilestone}`, { orderId, milestoneIndex: currentMilestone, type: 'submit' });
2476
+ } catch (e) { log({ event: 'auto_milestone_next_error', orderId, error: e.message }); }
2477
+ }, 3000);
2376
2478
  }
2377
2479
  res.json({ status: 'received', event });
2378
2480
  return;
@@ -2380,8 +2482,22 @@ async function cmdStart(port) {
2380
2482
 
2381
2483
  if (event === 'milestone_rejected') {
2382
2484
  const { orderId, milestoneIndex, rejectReason } = payload;
2383
- log({ event: 'milestone_rejected_notification', orderId, milestoneIndex, rejectReason, message: 'Resubmit with improvements' });
2485
+ log({ event: 'milestone_rejected_notification', orderId, milestoneIndex, rejectReason });
2486
+ console.log(`\n❌ M${milestoneIndex} rejected for ${orderId}: ${rejectReason}. Improving...`);
2384
2487
  res.json({ status: 'received', event });
2488
+ // Auto-resubmit with improvements using agent's own brain (async callback)
2489
+ setTimeout(async () => {
2490
+ try {
2491
+ const ms = await getMilestoneInfo(orderId);
2492
+ if (!ms || !ms.milestones) return;
2493
+ const m = ms.milestones[milestoneIndex];
2494
+ if (!m) return;
2495
+ const orderDesc = ms.description || orderId;
2496
+ const prompt = `You are an AI Agent. Your previous submission was REJECTED.\n\nOrder: ${orderDesc}\nMilestone M${milestoneIndex}: ${m.description || m.title}\nRejection reason: ${rejectReason}\n\nImprove your work based on the feedback. Output ONLY the improved deliverable.`;
2497
+ console.log(`\n🔄 Improving M${milestoneIndex} for ${orderId}...`);
2498
+ await agentExecute(prompt, `milestone-${orderId}-${milestoneIndex}-retry`, { orderId, milestoneIndex, type: 'submit' });
2499
+ } catch (e) { log({ event: 'auto_milestone_resubmit_error', orderId, error: e.message }); }
2500
+ }, 3000);
2385
2501
  return;
2386
2502
  }
2387
2503
 
@@ -2392,6 +2508,68 @@ async function cmdStart(port) {
2392
2508
  return;
2393
2509
  }
2394
2510
 
2511
+ // Requester: order was accepted by executor (USDC locked) → auto-approve plan
2512
+ if (event === 'order_accepted' && payload.executorDid) {
2513
+ const { orderId, executorDid, escrowTx } = payload;
2514
+ log({ event: 'order_accepted_by_executor', orderId, executorDid, escrowTx });
2515
+ console.log(`\n📋 Order ${orderId} accepted! Auto-approving milestone plan...`);
2516
+ res.json({ status: 'received', event });
2517
+ // Auto-approve milestone plan (non-blocking)
2518
+ setTimeout(async () => {
2519
+ try {
2520
+ for (let wait = 0; wait < 10; wait++) {
2521
+ await new Promise(r => setTimeout(r, 3000));
2522
+ const ms = await getMilestoneInfo(orderId);
2523
+ if (ms && ms.totalMilestones > 0) {
2524
+ const ts = new Date().toISOString();
2525
+ const pl = { approved: true };
2526
+ const sig = sign({ did: id.did, timestamp: ts, payload: pl }, id.secretKey);
2527
+ const fbResp = await fetch(`${PLATFORM_URL}/trade/v1/order/${orderId}/milestones/feedback`, {
2528
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
2529
+ body: JSON.stringify({ did: id.did, timestamp: ts, signature: sig, payload: pl }),
2530
+ signal: AbortSignal.timeout(10000),
2531
+ });
2532
+ log({ event: 'requester_auto_approved_plan', orderId, ok: fbResp.ok });
2533
+ break;
2534
+ }
2535
+ }
2536
+ } catch (e) { log({ event: 'requester_auto_approve_error', orderId, error: e.message }); }
2537
+ }, 2000);
2538
+ return;
2539
+ }
2540
+
2541
+ // Both parties: milestone plan confirmed, execution started
2542
+ if (event === 'milestone_plan_confirmed') {
2543
+ const { orderId, message } = payload;
2544
+ log({ event: 'milestone_plan_confirmed', orderId, message });
2545
+ console.log(`\n✅ Milestone plan confirmed for ${orderId}. Execution started.`);
2546
+ res.json({ status: 'received', event });
2547
+ // Executor: auto-submit M0 using agent's own brain (non-blocking, async callback)
2548
+ if (message && message.includes('Submit milestone')) {
2549
+ setTimeout(async () => {
2550
+ try {
2551
+ const ms = await getMilestoneInfo(orderId);
2552
+ if (!ms || !ms.milestones || ms.milestones.length === 0) return;
2553
+ const m0 = ms.milestones[0];
2554
+ const orderDesc = ms.description || orderId;
2555
+ const prompt = `You are an AI Agent executing a paid task.\n\nOrder: ${orderDesc}\nMilestone M0: ${m0.description || m0.title}\n\nComplete this milestone. Provide a thorough, detailed deliverable. Output ONLY the deliverable content.`;
2556
+ console.log(`\n🤖 Working on M0 for ${orderId}...`);
2557
+ await agentExecute(prompt, `milestone-${orderId}-0`, { orderId, milestoneIndex: 0, type: 'submit' });
2558
+ } catch (e) { log({ event: 'auto_milestone_0_error', orderId, error: e.message }); }
2559
+ }, 3000);
2560
+ }
2561
+ return;
2562
+ }
2563
+
2564
+ // Both parties: order settled, payment released
2565
+ if (event === 'order_settled') {
2566
+ const { orderId, message } = payload;
2567
+ log({ event: 'order_settled_notification', orderId, message });
2568
+ console.log(`\n💰 Order ${orderId} settled! ${message || 'Check: atel balance'}`);
2569
+ res.json({ status: 'received', event });
2570
+ return;
2571
+ }
2572
+
2395
2573
  // Unknown event type
2396
2574
  res.json({ status: 'ignored', event });
2397
2575
  });
@@ -2400,6 +2578,8 @@ async function cmdStart(port) {
2400
2578
  endpoint.app?.post?.('/atel/v1/result', async (req, res) => {
2401
2579
  const { taskId, result, success, trace: executorTrace } = req.body || {};
2402
2580
  if (!taskId || !pendingTasks[taskId]) { res.status(404).json({ error: 'Unknown taskId' }); return; }
2581
+
2582
+ // Milestone auto-execution removed. Agent handles results via its own AI.
2403
2583
  const task = pendingTasks[taskId];
2404
2584
  const startTime = new Date(task.acceptedAt).getTime();
2405
2585
  const durationMs = Date.now() - startTime;
@@ -3276,7 +3456,14 @@ async function cmdStart(port) {
3276
3456
  body: JSON.stringify({ did: id.did }), signal: AbortSignal.timeout(5000),
3277
3457
  });
3278
3458
  if (!resp.ok) return;
3279
- const { requests } = await resp.json();
3459
+ const data = await resp.json();
3460
+ // Relay returns {messages: [{id, sender, message, createdAt}]}
3461
+ // Each message.message contains {method, path, body}
3462
+ const rawMessages = data.messages || data.requests || [];
3463
+ const requests = rawMessages.map(m => {
3464
+ const inner = m.message || m;
3465
+ return typeof inner === 'string' ? JSON.parse(inner) : inner;
3466
+ });
3280
3467
  for (const req of requests) {
3281
3468
  // Forward to local endpoint
3282
3469
  try {
@@ -3342,14 +3529,14 @@ async function cmdStart(port) {
3342
3529
  process.on('SIGINT', async () => {
3343
3530
  heartbeat.stop();
3344
3531
  if (tunnelManager) await tunnelManager.stop();
3345
- if (builtinExecutor) await builtinExecutor.stop();
3532
+
3346
3533
  await endpoint.stop();
3347
3534
  process.exit(0);
3348
3535
  });
3349
3536
  process.on('SIGTERM', async () => {
3350
3537
  heartbeat.stop();
3351
3538
  if (tunnelManager) await tunnelManager.stop();
3352
- if (builtinExecutor) await builtinExecutor.stop();
3539
+
3353
3540
  await endpoint.stop();
3354
3541
  process.exit(0);
3355
3542
  });
@@ -5441,14 +5628,32 @@ async function cmdFriendRequest(args) {
5441
5628
  };
5442
5629
 
5443
5630
  // Sign the request
5444
- const signedRequest = createMessage(request, id.secretKey);
5631
+ const signedRequest = createMessage({ ...request, secretKey: id.secretKey });
5445
5632
 
5446
- // Try to send via P2P
5633
+ // Try to send via P2P — look up registry for endpoint
5447
5634
  let sent = false;
5448
5635
  let error = null;
5449
5636
 
5450
5637
  try {
5451
- const result = await sendP2PMessage(targetDid, signedRequest);
5638
+ // Resolve endpoint from registry
5639
+ let endpoint = null;
5640
+ try {
5641
+ const resp = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(targetDid)}`, { signal: AbortSignal.timeout(5000) });
5642
+ if (resp.ok) {
5643
+ const entry = await resp.json();
5644
+ const candidates = entry.endpoint_candidates || [];
5645
+ const direct = candidates.find(c => c.type === 'direct');
5646
+ const relay = candidates.find(c => c.type === 'relay');
5647
+ endpoint = direct?.url || relay?.url || entry.endpoint;
5648
+ }
5649
+ } catch (e) {
5650
+ throw new Error(`Cannot find agent in registry: ${e.message}`);
5651
+ }
5652
+ if (!endpoint) throw new Error('Agent has no reachable endpoint');
5653
+
5654
+ const hsManager = new HandshakeManager(id);
5655
+ const client = new AgentClient(id);
5656
+ await client.sendTask(endpoint, signedRequest, hsManager);
5452
5657
  sent = true;
5453
5658
  } catch (err) {
5454
5659
  error = err.message;
@@ -5538,7 +5743,7 @@ async function cmdFriendAccept(args) {
5538
5743
  }
5539
5744
  };
5540
5745
 
5541
- const signedMsg = createMessage(acceptMsg, id.secretKey);
5746
+ const signedMsg = createMessage({ ...acceptMsg, secretKey: id.secretKey });
5542
5747
 
5543
5748
  try {
5544
5749
  await sendP2PMessage(request.from, signedMsg);
@@ -5604,7 +5809,7 @@ async function cmdFriendReject(args) {
5604
5809
  }
5605
5810
  };
5606
5811
 
5607
- const signedMsg = createMessage(rejectMsg, id.secretKey);
5812
+ const signedMsg = createMessage({ ...rejectMsg, secretKey: id.secretKey });
5608
5813
 
5609
5814
  try {
5610
5815
  await sendP2PMessage(request.from, signedMsg);
@@ -5742,6 +5947,214 @@ async function cmdFriendStatus(args) {
5742
5947
  console.log('');
5743
5948
  }
5744
5949
 
5950
+ // ─── P2P Send Helper ────────────────────────────────────────────
5951
+ // Resolves DID → endpoint via registry, then sends a signed message.
5952
+ // Replaces the previously undefined sendP2PMessage.
5953
+ async function sendP2PMessage(targetDid, signedMsg) {
5954
+ const id = loadIdentity();
5955
+ if (!id) throw new Error('No identity found');
5956
+
5957
+ let endpoint = null;
5958
+ try {
5959
+ const resp = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(targetDid)}`, { signal: AbortSignal.timeout(5000) });
5960
+ if (resp.ok) {
5961
+ const entry = await resp.json();
5962
+ const candidates = entry.endpoint_candidates || [];
5963
+ const direct = candidates.find(c => c.type === 'direct');
5964
+ const relay = candidates.find(c => c.type === 'relay');
5965
+ endpoint = direct?.url || relay?.url || entry.endpoint;
5966
+ }
5967
+ } catch (e) {
5968
+ throw new Error(`Registry lookup failed for ${targetDid}: ${e.message}`);
5969
+ }
5970
+ if (!endpoint) throw new Error(`No reachable endpoint for ${targetDid}`);
5971
+
5972
+ const hsManager = new HandshakeManager(id);
5973
+ const client = new AgentClient(id);
5974
+ return await client.sendTask(endpoint, signedMsg, hsManager);
5975
+ }
5976
+
5977
+ // ─── Send Command (Rich Media P2P Messaging) ─────────────────────
5978
+
5979
+ function showSendHelp() {
5980
+ console.log(`
5981
+ atal send — Send a message (with optional rich media) to another agent
5982
+
5983
+ Usage:
5984
+ atel send <did|alias|endpoint> "message text"
5985
+ atel send <did> "look at this" --image ./photo.jpg
5986
+ atel send <did> "here is the file" --file ./report.pdf
5987
+ atel send <did> "listen" --audio ./voice.mp3
5988
+ atel send <did> "watch this" --video ./clip.mp4
5989
+
5990
+ Flags:
5991
+ --image <path> Attach image (jpg/png/gif/webp, max 10MB, up to 9)
5992
+ --file <path> Attach file (pdf/zip/txt/json, max 100MB, up to 5)
5993
+ --audio <path> Attach audio (mp3/wav/ogg/m4a, max 50MB)
5994
+ --video <path> Attach video (mp4/webm/mov, max 500MB)
5995
+ --json Output result as JSON
5996
+
5997
+ Notes:
5998
+ By default, agents require a mutual friend relationship before accepting
5999
+ P2P messages. If you get a NOT_FRIEND error, run:
6000
+ atel friend request <did>
6001
+ and ask the other side to accept with:
6002
+ atel friend accept <request-id>
6003
+
6004
+ To disable the friend requirement on your own agent, set in policy.json:
6005
+ { "relationshipPolicy": { "defaultMode": "open" } }
6006
+
6007
+ Examples:
6008
+ atel send did:atel:ed25519:ABC "Hello!"
6009
+ atel send @alice "Check this" --image ./screenshot.png
6010
+ atel send did:atel:ed25519:ABC "Report" --file ./q1.pdf
6011
+ `);
6012
+ }
6013
+
6014
+ async function cmdSend(args) {
6015
+ const target = args._[0];
6016
+ const text = args._[1] || '';
6017
+ const isJson = rawArgs.includes('--json');
6018
+
6019
+ if (!target) { showSendHelp(); process.exit(1); }
6020
+
6021
+ const id = loadIdentity();
6022
+ if (!id) { console.error('No identity found. Run: atel init'); process.exit(1); }
6023
+
6024
+ // Parse attachment flags
6025
+ const attachmentFlags = parseAttachmentFlags(rawArgs);
6026
+ const hasAttachments = attachmentFlags.images.length > 0 ||
6027
+ attachmentFlags.files.length > 0 ||
6028
+ attachmentFlags.audios.length > 0 ||
6029
+ attachmentFlags.videos.length > 0;
6030
+
6031
+ if (!text && !hasAttachments) {
6032
+ console.error('Error: message text or at least one attachment is required');
6033
+ showSendHelp(); process.exit(1);
6034
+ }
6035
+
6036
+ // Build payload
6037
+ const msgId = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
6038
+ const payload = {
6039
+ action: 'general', // broad compatibility with all SDK versions
6040
+ msgType: 'message', // identifies this as a p2p chat message
6041
+ msgId,
6042
+ text,
6043
+ timestamp: new Date().toISOString(),
6044
+ };
6045
+
6046
+ // Upload attachments
6047
+ if (hasAttachments) {
6048
+ try {
6049
+ if (!isJson) process.stderr.write('Uploading attachments...\n');
6050
+ const processed = await processAttachments(attachmentFlags, id.did, msgId);
6051
+ if (processed.images.length > 0) payload.images = processed.images;
6052
+ if (processed.attachments.length > 0) payload.attachments = processed.attachments;
6053
+ if (!isJson) {
6054
+ const total = (processed.images.length + processed.attachments.length);
6055
+ process.stderr.write(` ✓ ${total} attachment(s) ready\n`);
6056
+ }
6057
+ } catch (err) {
6058
+ console.error(`Attachment upload failed: ${err.message}`);
6059
+ process.exit(1);
6060
+ }
6061
+ }
6062
+
6063
+ // Resolve target → endpoint + remoteDid
6064
+ let remoteEndpoint = target;
6065
+ let remoteDid;
6066
+
6067
+ // Resolve @alias
6068
+ try { const r = resolveDID(target); if (r !== target) remoteEndpoint = r; } catch (_) {}
6069
+
6070
+ if (!remoteEndpoint.startsWith('http')) {
6071
+ // Registry lookup
6072
+ let entry;
6073
+ try {
6074
+ const resp = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(remoteEndpoint)}`, { signal: AbortSignal.timeout(5000) });
6075
+ if (resp.ok) entry = await resp.json();
6076
+ } catch (_) {}
6077
+ if (!entry) {
6078
+ const regClient = new RegistryClient({ registryUrl: REGISTRY_URL });
6079
+ const results = await regClient.search({ type: remoteEndpoint, limit: 1 });
6080
+ if (results.length > 0) entry = results[0];
6081
+ }
6082
+ if (!entry) { console.error(`Agent not found: ${target}`); process.exit(1); }
6083
+ remoteDid = entry.did;
6084
+ const candidates = entry.endpoint_candidates || [];
6085
+ const direct = candidates.find(c => c.type === 'direct');
6086
+ const relay = candidates.find(c => c.type === 'relay');
6087
+ remoteEndpoint = direct?.url || relay?.url || entry.endpoint;
6088
+ } else {
6089
+ remoteDid = target.startsWith('did:') ? target : undefined;
6090
+ // Try to get remoteDid from handshake cache for direct endpoints
6091
+ if (!remoteDid) {
6092
+ try {
6093
+ const tmpHs = new HandshakeManager(id);
6094
+ const session = await tmpHs.getOrCreate(remoteEndpoint);
6095
+ if (session?.remoteDid) remoteDid = session.remoteDid;
6096
+ } catch (_) {}
6097
+ }
6098
+ }
6099
+
6100
+ if (!remoteEndpoint) {
6101
+ console.error(`No reachable endpoint for: ${target}`);
6102
+ process.exit(1);
6103
+ }
6104
+
6105
+ // Send
6106
+ try {
6107
+ const hsManager = new HandshakeManager(id);
6108
+ const client = new AgentClient(id);
6109
+ const msg = createMessage({ type: 'task', from: id.did, to: remoteDid || remoteEndpoint, payload, secretKey: id.secretKey });
6110
+ const result = await client.sendTask(remoteEndpoint, msg, hsManager);
6111
+
6112
+ // Surface friendly errors
6113
+ const inner = result?.result;
6114
+ if (inner?.code === 'NOT_FRIEND' || inner?.error?.includes('NOT_FRIEND') || inner?.error?.includes('not a friend')) {
6115
+ if (isJson) {
6116
+ console.log(JSON.stringify({ status: 'error', code: 'NOT_FRIEND', message: inner.error, hint: `Run: atel friend request ${remoteDid || target}` }));
6117
+ } else {
6118
+ console.error(`✗ Message blocked: the recipient requires a friend relationship.`);
6119
+ console.error(` Run: atel friend request ${remoteDid || target}`);
6120
+ console.error(` Then ask them to: atel friend accept <request-id>`);
6121
+ }
6122
+ process.exit(1);
6123
+ }
6124
+ if (inner?.status === 'rejected') {
6125
+ if (isJson) {
6126
+ console.log(JSON.stringify({ status: 'error', message: inner.error || 'Message rejected', result }));
6127
+ } else {
6128
+ console.error(`✗ Message rejected: ${inner.error || 'unknown reason'}`);
6129
+ }
6130
+ process.exit(1);
6131
+ }
6132
+
6133
+ if (isJson) {
6134
+ console.log(JSON.stringify({ status: 'sent', msgId, to: remoteDid || remoteEndpoint, attachments: (payload.images?.length || 0) + (payload.attachments?.length || 0), result }, null, 2));
6135
+ } else {
6136
+ console.log(`✓ Message sent to ${remoteDid || remoteEndpoint}`);
6137
+ if (payload.images?.length) console.log(` Images: ${payload.images.length}`);
6138
+ if (payload.attachments?.length) console.log(` Attachments: ${payload.attachments.length}`);
6139
+ }
6140
+ } catch (err) {
6141
+ if (isJson) {
6142
+ console.log(JSON.stringify({ status: 'error', message: err.message }));
6143
+ } else {
6144
+ // Surface specific known errors in human-readable form
6145
+ if (err.message?.includes('fetch failed') || err.message?.includes('ECONNREFUSED')) {
6146
+ console.error(`✗ Cannot reach agent at ${remoteEndpoint}`);
6147
+ console.error(` The agent may be offline or behind a firewall.`);
6148
+ } else if (err.message?.includes('401') || err.message?.includes('Unauthorized')) {
6149
+ console.error(`✗ Authentication failed — try: atel handshake ${remoteEndpoint}`);
6150
+ } else {
6151
+ console.error(`✗ Send failed: ${err.message}`);
6152
+ }
6153
+ }
6154
+ process.exit(1);
6155
+ }
6156
+ }
6157
+
5745
6158
  // ─── Temporary Session Commands ──────────────────────────────────
5746
6159
 
5747
6160
  async function cmdTempAllow(args) {
@@ -6212,6 +6625,14 @@ const commands = {
6212
6625
  console.error(' status [--json]');
6213
6626
  process.exit(1);
6214
6627
  },
6628
+ // Send (Rich Media P2P Message)
6629
+ send: () => {
6630
+ if (rawArgs.includes('--help') || rawArgs.includes('-h') || args.length === 0) {
6631
+ showSendHelp();
6632
+ process.exit(0);
6633
+ }
6634
+ return cmdSend({ _: args, json: rawArgs.includes('--json') });
6635
+ },
6215
6636
  // Alias System
6216
6637
  alias: () => {
6217
6638
  const subCmd = args[0];
@@ -6248,6 +6669,7 @@ Protocol Commands:
6248
6669
  register [name] [caps] [endpoint] Register on public registry (caps: "type1:price1,type2:price2" or "type1,type2" for free)
6249
6670
  search <capability> Search registry for agents (shows pricing info)
6250
6671
  handshake <endpoint> [did] Handshake with remote agent
6672
+ send <target> "text" [--image/--file/--audio/--video <path>] Send P2P message with optional rich media
6251
6673
  task <target> <json> Delegate task (auto trust check)
6252
6674
  result <taskId> <json> Submit execution result (from executor)
6253
6675
  check <did> [risk] Check agent trust (risk: low|medium|high|critical)