@lawreneliang/atel-sdk 0.4.2 → 0.4.4

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
@@ -3,17 +3,12 @@
3
3
  /**
4
4
  * ATEL CLI — Command-line interface for ATEL SDK
5
5
  *
6
- * Async execution model:
7
- * 1. Receive task → security check → return accepted + taskId
8
- * 2. Forward to local executor (ATEL_EXECUTOR_URL)
9
- * 3. Executor completes → callback to atel
10
- * 4. Trace → Proof → on-chain anchor
11
- * 5. Push result back to sender (encrypted)
12
- *
13
6
  * Commands:
14
7
  * atel init [name] Create agent identity + default policy
15
- * atel info Show identity, capabilities, policy
16
- * atel start [port] Start endpoint (async execution)
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
17
12
  * atel inbox [count] Show received messages
18
13
  * atel register [name] [caps] [url] Register on public registry
19
14
  * atel search <capability> Search registry for agents
@@ -25,15 +20,10 @@
25
20
  import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
26
21
  import { resolve } from 'node:path';
27
22
  import {
28
- AgentIdentity,
29
- AgentEndpoint,
30
- AgentClient,
31
- HandshakeManager,
32
- createMessage,
33
- RegistryClient,
34
- ExecutionTrace,
35
- ProofGenerator,
36
- SolanaAnchorProvider,
23
+ AgentIdentity, AgentEndpoint, AgentClient, HandshakeManager,
24
+ createMessage, RegistryClient, ExecutionTrace, ProofGenerator,
25
+ SolanaAnchorProvider, autoNetworkSetup, collectCandidates, connectToAgent,
26
+ discoverPublicIP, checkReachable,
37
27
  } from '@lawreneliang/atel-sdk';
38
28
 
39
29
  const ATEL_DIR = resolve(process.env.ATEL_DIR || '.atel');
@@ -43,134 +33,43 @@ const EXECUTOR_URL = process.env.ATEL_EXECUTOR_URL || '';
43
33
  const INBOX_FILE = resolve(ATEL_DIR, 'inbox.jsonl');
44
34
  const POLICY_FILE = resolve(ATEL_DIR, 'policy.json');
45
35
  const TASKS_FILE = resolve(ATEL_DIR, 'tasks.json');
36
+ const NETWORK_FILE = resolve(ATEL_DIR, 'network.json');
46
37
 
47
- // ─── Default Policy ──────────────────────────────────────────────
48
-
49
- const DEFAULT_POLICY = {
50
- rateLimit: 60, // max tasks per minute
51
- maxPayloadBytes: 1048576, // 1MB
52
- maxConcurrent: 10,
53
- allowedDIDs: [], // empty = allow all
54
- blockedDIDs: [],
55
- };
38
+ const DEFAULT_POLICY = { rateLimit: 60, maxPayloadBytes: 1048576, maxConcurrent: 10, allowedDIDs: [], blockedDIDs: [] };
56
39
 
57
40
  // ─── Helpers ─────────────────────────────────────────────────────
58
41
 
59
- function ensureDir() {
60
- if (!existsSync(ATEL_DIR)) mkdirSync(ATEL_DIR, { recursive: true });
61
- }
42
+ function ensureDir() { if (!existsSync(ATEL_DIR)) mkdirSync(ATEL_DIR, { recursive: true }); }
43
+ function log(event) { ensureDir(); appendFileSync(INBOX_FILE, JSON.stringify(event) + '\n'); console.log(JSON.stringify(event)); }
62
44
 
63
- function log(event) {
64
- ensureDir();
65
- appendFileSync(INBOX_FILE, JSON.stringify(event) + '\n');
66
- console.log(JSON.stringify(event));
67
- }
45
+ 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)); }
46
+ 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')) }); }
47
+ function requireIdentity() { const id = loadIdentity(); if (!id) { console.error('No identity. Run: atel init'); process.exit(1); } return id; }
68
48
 
69
- function saveIdentity(identity) {
70
- ensureDir();
71
- writeFileSync(IDENTITY_FILE, JSON.stringify({
72
- agent_id: identity.agent_id,
73
- did: identity.did,
74
- publicKey: Buffer.from(identity.publicKey).toString('hex'),
75
- secretKey: Buffer.from(identity.secretKey).toString('hex'),
76
- }, null, 2));
77
- }
49
+ function loadCapabilities() { const f = resolve(ATEL_DIR, 'capabilities.json'); if (!existsSync(f)) return []; try { return JSON.parse(readFileSync(f, 'utf-8')); } catch { return []; } }
50
+ function saveCapabilities(c) { ensureDir(); writeFileSync(resolve(ATEL_DIR, 'capabilities.json'), JSON.stringify(c, null, 2)); }
51
+ function loadPolicy() { if (!existsSync(POLICY_FILE)) return { ...DEFAULT_POLICY }; try { return { ...DEFAULT_POLICY, ...JSON.parse(readFileSync(POLICY_FILE, 'utf-8')) }; } catch { return { ...DEFAULT_POLICY }; } }
52
+ function savePolicy(p) { ensureDir(); writeFileSync(POLICY_FILE, JSON.stringify(p, null, 2)); }
53
+ function loadTasks() { if (!existsSync(TASKS_FILE)) return {}; try { return JSON.parse(readFileSync(TASKS_FILE, 'utf-8')); } catch { return {}; } }
54
+ function saveTasks(t) { ensureDir(); writeFileSync(TASKS_FILE, JSON.stringify(t, null, 2)); }
55
+ function loadNetwork() { if (!existsSync(NETWORK_FILE)) return null; try { return JSON.parse(readFileSync(NETWORK_FILE, 'utf-8')); } catch { return null; } }
56
+ function saveNetwork(n) { ensureDir(); writeFileSync(NETWORK_FILE, JSON.stringify(n, null, 2)); }
78
57
 
79
- function loadIdentity() {
80
- if (!existsSync(IDENTITY_FILE)) return null;
81
- const data = JSON.parse(readFileSync(IDENTITY_FILE, 'utf-8'));
82
- return new AgentIdentity({
83
- agent_id: data.agent_id,
84
- publicKey: Uint8Array.from(Buffer.from(data.publicKey, 'hex')),
85
- secretKey: Uint8Array.from(Buffer.from(data.secretKey, 'hex')),
86
- });
87
- }
88
-
89
- function requireIdentity() {
90
- const id = loadIdentity();
91
- if (!id) { console.error('No identity found. Run: atel init'); process.exit(1); }
92
- return id;
93
- }
94
-
95
- function loadCapabilities() {
96
- const f = resolve(ATEL_DIR, 'capabilities.json');
97
- if (!existsSync(f)) return [];
98
- try { return JSON.parse(readFileSync(f, 'utf-8')); } catch { return []; }
99
- }
100
-
101
- function saveCapabilities(caps) {
102
- ensureDir();
103
- writeFileSync(resolve(ATEL_DIR, 'capabilities.json'), JSON.stringify(caps, null, 2));
104
- }
105
-
106
- function loadPolicy() {
107
- if (!existsSync(POLICY_FILE)) return { ...DEFAULT_POLICY };
108
- try { return { ...DEFAULT_POLICY, ...JSON.parse(readFileSync(POLICY_FILE, 'utf-8')) }; } catch { return { ...DEFAULT_POLICY }; }
109
- }
110
-
111
- function savePolicy(policy) {
112
- ensureDir();
113
- writeFileSync(POLICY_FILE, JSON.stringify(policy, null, 2));
114
- }
115
-
116
- function loadTasks() {
117
- if (!existsSync(TASKS_FILE)) return {};
118
- try { return JSON.parse(readFileSync(TASKS_FILE, 'utf-8')); } catch { return {}; }
119
- }
120
-
121
- function saveTasks(tasks) {
122
- ensureDir();
123
- writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2));
124
- }
125
-
126
- // ─── Policy Enforcement ──────────────────────────────────────────
58
+ // ─── Policy Enforcer ─────────────────────────────────────────────
127
59
 
128
60
  class PolicyEnforcer {
129
- constructor(policy) {
130
- this.policy = policy;
131
- this.requestLog = []; // timestamps of recent requests per DID
132
- this.activeTasks = 0;
133
- }
134
-
61
+ constructor(policy) { this.policy = policy; this.requestLog = []; this.activeTasks = 0; }
135
62
  check(message) {
136
- const p = this.policy;
137
- const fromDid = message.from;
138
- const payload = message.payload || {};
139
- const payloadSize = JSON.stringify(payload).length;
140
-
141
- // 1. Blocked DID
142
- if (p.blockedDIDs.length > 0 && p.blockedDIDs.includes(fromDid)) {
143
- return { allowed: false, reason: `DID ${fromDid} is blocked` };
144
- }
145
-
146
- // 2. Allowed DID whitelist (empty = allow all)
147
- if (p.allowedDIDs.length > 0 && !p.allowedDIDs.includes(fromDid)) {
148
- return { allowed: false, reason: `DID ${fromDid} is not in allowlist` };
149
- }
150
-
151
- // 3. Rate limit
152
- const now = Date.now();
153
- const windowMs = 60000;
154
- this.requestLog = this.requestLog.filter(t => now - t < windowMs);
155
- if (this.requestLog.length >= p.rateLimit) {
156
- return { allowed: false, reason: `Rate limit exceeded (${p.rateLimit}/min)` };
157
- }
158
-
159
- // 4. Payload size
160
- if (payloadSize > p.maxPayloadBytes) {
161
- return { allowed: false, reason: `Payload too large (${payloadSize} > ${p.maxPayloadBytes} bytes)` };
162
- }
163
-
164
- // 5. Concurrent tasks
165
- if (this.activeTasks >= p.maxConcurrent) {
166
- return { allowed: false, reason: `Max concurrent tasks reached (${p.maxConcurrent})` };
167
- }
168
-
169
- // Record this request
63
+ const p = this.policy, from = message.from, size = JSON.stringify(message.payload || {}).length, now = Date.now();
64
+ if (p.blockedDIDs.length > 0 && p.blockedDIDs.includes(from)) return { allowed: false, reason: `DID blocked` };
65
+ if (p.allowedDIDs.length > 0 && !p.allowedDIDs.includes(from)) return { allowed: false, reason: `DID not in allowlist` };
66
+ this.requestLog = this.requestLog.filter(t => now - t < 60000);
67
+ if (this.requestLog.length >= p.rateLimit) return { allowed: false, reason: `Rate limit (${p.rateLimit}/min)` };
68
+ if (size > p.maxPayloadBytes) return { allowed: false, reason: `Payload too large (${size} > ${p.maxPayloadBytes})` };
69
+ if (this.activeTasks >= p.maxConcurrent) return { allowed: false, reason: `Max concurrent (${p.maxConcurrent})` };
170
70
  this.requestLog.push(now);
171
71
  return { allowed: true };
172
72
  }
173
-
174
73
  taskStarted() { this.activeTasks++; }
175
74
  taskFinished() { this.activeTasks = Math.max(0, this.activeTasks - 1); }
176
75
  }
@@ -178,20 +77,14 @@ class PolicyEnforcer {
178
77
  // ─── On-chain Anchoring ──────────────────────────────────────────
179
78
 
180
79
  async function anchorOnChain(traceRoot, metadata) {
181
- const solanaKey = process.env.ATEL_SOLANA_PRIVATE_KEY;
182
- if (!solanaKey) return null;
80
+ const key = process.env.ATEL_SOLANA_PRIVATE_KEY;
81
+ if (!key) return null;
183
82
  try {
184
- const solana = new SolanaAnchorProvider({
185
- rpcUrl: process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com',
186
- privateKey: solanaKey,
187
- });
188
- const record = await solana.anchor(traceRoot, metadata);
189
- log({ event: 'proof_anchored', chain: 'solana', txHash: record.txHash, block: record.blockNumber, trace_root: traceRoot });
190
- return record;
191
- } catch (e) {
192
- log({ event: 'anchor_failed', chain: 'solana', error: e.message });
193
- return null;
194
- }
83
+ const s = new SolanaAnchorProvider({ rpcUrl: process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com', privateKey: key });
84
+ const r = await s.anchor(traceRoot, metadata);
85
+ log({ event: 'proof_anchored', chain: 'solana', txHash: r.txHash, block: r.blockNumber, trace_root: traceRoot });
86
+ return r;
87
+ } catch (e) { log({ event: 'anchor_failed', chain: 'solana', error: e.message }); return null; }
195
88
  }
196
89
 
197
90
  // ─── Commands ────────────────────────────────────────────────────
@@ -201,26 +94,38 @@ async function cmdInit(agentId) {
201
94
  const identity = new AgentIdentity({ agent_id: name });
202
95
  saveIdentity(identity);
203
96
  savePolicy(DEFAULT_POLICY);
204
- console.log(JSON.stringify({
205
- status: 'created',
206
- agent_id: identity.agent_id,
207
- did: identity.did,
208
- policy: POLICY_FILE,
209
- next: 'Run: atel register [name] [capabilities] [endpoint]',
210
- }, null, 2));
97
+ 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));
211
98
  }
212
99
 
213
100
  async function cmdInfo() {
214
101
  const id = requireIdentity();
215
- const caps = loadCapabilities();
216
- const policy = loadPolicy();
217
- console.log(JSON.stringify({
218
- agent_id: id.agent_id,
219
- did: id.did,
220
- capabilities: caps,
221
- policy,
222
- executor: EXECUTOR_URL || 'not configured (set ATEL_EXECUTOR_URL)',
223
- }, null, 2));
102
+ 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));
103
+ }
104
+
105
+ async function cmdSetup(port) {
106
+ const p = parseInt(port || '3100');
107
+ console.log(JSON.stringify({ event: 'network_setup', port: p }));
108
+ const net = await autoNetworkSetup(p);
109
+ for (const step of net.steps) console.log(JSON.stringify({ event: 'step', message: step }));
110
+ if (net.endpoint) {
111
+ saveNetwork({ publicIP: net.publicIP, port: p, endpoint: net.endpoint, upnp: net.upnpSuccess, reachable: net.reachable, configuredAt: new Date().toISOString() });
112
+ console.log(JSON.stringify({ status: 'ready', endpoint: net.endpoint }));
113
+ } else if (net.publicIP) {
114
+ const ep = `http://${net.publicIP}:${p}`;
115
+ saveNetwork({ publicIP: net.publicIP, port: p, endpoint: ep, upnp: false, reachable: false, needsManualPortForward: true, configuredAt: new Date().toISOString() });
116
+ 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` }));
117
+ } else {
118
+ console.log(JSON.stringify({ status: 'failed', error: 'Could not determine public IP' }));
119
+ }
120
+ }
121
+
122
+ async function cmdVerify() {
123
+ const net = loadNetwork();
124
+ if (!net) { console.error('No network config. Run: atel setup'); process.exit(1); }
125
+ console.log(JSON.stringify({ event: 'verifying', ip: net.publicIP, port: net.port }));
126
+ const result = await verifyPortReachable(net.publicIP, net.port);
127
+ console.log(JSON.stringify({ status: result.reachable ? 'reachable' : 'not_reachable', detail: result.detail }));
128
+ if (result.reachable) { net.reachable = true; net.needsManualPortForward = false; saveNetwork(net); }
224
129
  }
225
130
 
226
131
  async function cmdStart(port) {
@@ -231,193 +136,124 @@ async function cmdStart(port) {
231
136
  const policy = loadPolicy();
232
137
  const enforcer = new PolicyEnforcer(policy);
233
138
  const pendingTasks = loadTasks();
139
+
140
+ // ── Network: collect candidates ──
141
+ let networkConfig = loadNetwork();
142
+ if (!networkConfig) {
143
+ log({ event: 'network_setup', status: 'auto-detecting' });
144
+ networkConfig = await autoNetworkSetup(p);
145
+ for (const step of networkConfig.steps) log({ event: 'network_step', message: step });
146
+ delete networkConfig.steps;
147
+ saveNetwork(networkConfig);
148
+ } else {
149
+ log({ event: 'network_loaded', candidates: networkConfig.candidates.length });
150
+ }
151
+
152
+ // ── Start endpoint ──
234
153
  const endpoint = new AgentEndpoint(id, { port: p, host: '0.0.0.0' });
235
154
 
236
- // Result callback endpoint: POST /atel/v1/result
237
- // Executor calls this when done
155
+ // Result callback: POST /atel/v1/result (executor calls this when done)
238
156
  endpoint.app?.post?.('/atel/v1/result', async (req, res) => {
239
157
  const { taskId, result, success } = req.body || {};
240
- if (!taskId || !pendingTasks[taskId]) {
241
- res.status(404).json({ error: 'Unknown taskId' });
242
- return;
243
- }
244
-
158
+ if (!taskId || !pendingTasks[taskId]) { res.status(404).json({ error: 'Unknown taskId' }); return; }
245
159
  const task = pendingTasks[taskId];
246
160
  enforcer.taskFinished();
247
161
 
248
- // Complete the trace
249
162
  const trace = new ExecutionTrace(taskId, id);
250
- trace.append('TASK_ACCEPTED', { from: task.from, action: task.action, payload: task.payload });
163
+ trace.append('TASK_ACCEPTED', { from: task.from, action: task.action });
251
164
  trace.append('TASK_FORWARDED', { executor_url: EXECUTOR_URL });
252
165
  trace.append('TASK_RESULT', { success: success !== false, result });
166
+ if (success !== false) trace.finalize(typeof result === 'object' ? result : { result });
167
+ else trace.fail(new Error(result?.error || 'Execution failed'));
253
168
 
254
- if (success !== false) {
255
- trace.finalize(typeof result === 'object' ? result : { result });
256
- } else {
257
- trace.fail(new Error(result?.error || 'Execution failed'));
258
- }
259
-
260
- // Generate proof
261
169
  const proofGen = new ProofGenerator(trace, id);
262
- const proof = proofGen.generate(
263
- capTypes.join(',') || 'no-policy',
264
- `task-from-${task.from}`,
265
- JSON.stringify(result),
266
- );
267
-
268
- // Anchor on-chain
269
- const anchor = await anchorOnChain(proof.trace_root, {
270
- proof_id: proof.proof_id,
271
- task_from: task.from,
272
- action: task.action,
273
- taskId,
274
- });
275
-
276
- log({
277
- event: 'task_completed',
278
- taskId,
279
- from: task.from,
280
- action: task.action,
281
- success: success !== false,
282
- proof_id: proof.proof_id,
283
- trace_root: proof.trace_root,
284
- anchor_tx: anchor?.txHash || null,
285
- timestamp: new Date().toISOString(),
286
- });
287
-
288
- // Push result back to sender (encrypted, via their endpoint)
170
+ const proof = proofGen.generate(capTypes.join(',') || 'no-policy', `task-from-${task.from}`, JSON.stringify(result));
171
+ const anchor = await anchorOnChain(proof.trace_root, { proof_id: proof.proof_id, task_from: task.from, action: task.action, taskId });
172
+
173
+ log({ event: 'task_completed', taskId, from: task.from, action: task.action, success: success !== false, proof_id: proof.proof_id, anchor_tx: anchor?.txHash || null, timestamp: new Date().toISOString() });
174
+
175
+ // Push result back to sender
289
176
  if (task.senderEndpoint) {
290
177
  try {
291
178
  const client = new AgentClient(id);
292
179
  const hsManager = new HandshakeManager(id);
293
180
  await client.handshake(task.senderEndpoint, hsManager, task.from);
294
- const resultMsg = createMessage({
295
- type: 'task-result',
296
- from: id.did,
297
- to: task.from,
298
- payload: {
299
- taskId,
300
- status: success !== false ? 'completed' : 'failed',
301
- result,
302
- proof: { proof_id: proof.proof_id, trace_root: proof.trace_root },
303
- anchor: anchor ? { chain: 'solana', txHash: anchor.txHash } : null,
304
- },
305
- secretKey: id.secretKey,
306
- });
307
- await client.sendTask(task.senderEndpoint, resultMsg, hsManager);
308
- log({ event: 'result_pushed', taskId, to: task.from, endpoint: task.senderEndpoint });
309
- } catch (e) {
310
- log({ event: 'result_push_failed', taskId, to: task.from, error: e.message });
311
- }
181
+ const msg = createMessage({ type: 'task-result', from: id.did, to: task.from, payload: { taskId, status: success !== false ? 'completed' : 'failed', result, proof: { proof_id: proof.proof_id, trace_root: proof.trace_root }, anchor: anchor ? { chain: 'solana', txHash: anchor.txHash } : null }, secretKey: id.secretKey });
182
+ await client.sendTask(task.senderEndpoint, msg, hsManager);
183
+ log({ event: 'result_pushed', taskId, to: task.from });
184
+ } catch (e) { log({ event: 'result_push_failed', taskId, error: e.message }); }
312
185
  }
313
186
 
314
- // Cleanup
315
- delete pendingTasks[taskId];
316
- saveTasks(pendingTasks);
317
-
187
+ delete pendingTasks[taskId]; saveTasks(pendingTasks);
318
188
  res.json({ status: 'ok', proof_id: proof.proof_id, anchor_tx: anchor?.txHash || null });
319
189
  });
320
190
 
191
+ // Task handler
321
192
  endpoint.onTask(async (message, session) => {
322
193
  const payload = message.payload || {};
323
194
  const action = payload.action || payload.type || 'unknown';
324
195
 
325
- // ── Policy enforcement ──
326
- const policyCheck = enforcer.check(message);
327
- if (!policyCheck.allowed) {
328
- log({ event: 'task_rejected', from: message.from, action, reason: policyCheck.reason, timestamp: new Date().toISOString() });
329
- return { status: 'rejected', error: policyCheck.reason };
330
- }
196
+ // Policy check
197
+ const pc = enforcer.check(message);
198
+ if (!pc.allowed) { log({ event: 'task_rejected', from: message.from, action, reason: pc.reason, timestamp: new Date().toISOString() }); return { status: 'rejected', error: pc.reason }; }
331
199
 
332
- // ── Capability boundary check ──
200
+ // Capability check
333
201
  if (capTypes.length > 0 && !capTypes.includes(action) && !capTypes.includes('general')) {
334
- log({ event: 'task_rejected', from: message.from, action, reason: `Outside capability boundary: [${capTypes.join(', ')}]`, timestamp: new Date().toISOString() });
202
+ log({ event: 'task_rejected', from: message.from, action, reason: `Outside capability: [${capTypes.join(',')}]`, timestamp: new Date().toISOString() });
335
203
  return { status: 'rejected', error: `Action "${action}" outside capability boundary`, capabilities: capTypes };
336
204
  }
337
205
 
338
- // ── Accept task (async) ──
339
206
  const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
340
207
  enforcer.taskStarted();
341
208
 
342
- // Look up sender's endpoint from registry for result push-back
209
+ // Lookup sender endpoint for result push-back
343
210
  let senderEndpoint = null;
344
- try {
345
- const regClient = new RegistryClient({ registryUrl: REGISTRY_URL });
346
- const resp = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(message.from)}`);
347
- if (resp.ok) {
348
- const entry = await resp.json();
349
- senderEndpoint = entry.endpoint;
350
- }
351
- } catch { /* best effort */ }
352
-
353
- pendingTasks[taskId] = {
354
- from: message.from,
355
- action,
356
- payload,
357
- senderEndpoint,
358
- encrypted: !!session?.encrypted,
359
- acceptedAt: new Date().toISOString(),
360
- };
361
- saveTasks(pendingTasks);
211
+ try { const r = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(message.from)}`); if (r.ok) senderEndpoint = (await r.json()).endpoint; } catch {}
362
212
 
213
+ pendingTasks[taskId] = { from: message.from, action, payload, senderEndpoint, encrypted: !!session?.encrypted, acceptedAt: new Date().toISOString() };
214
+ saveTasks(pendingTasks);
363
215
  log({ event: 'task_accepted', taskId, from: message.from, action, encrypted: !!session?.encrypted, timestamp: new Date().toISOString() });
364
216
 
365
- // ── Forward to executor ──
217
+ // Forward to executor or echo
366
218
  if (EXECUTOR_URL) {
367
- try {
368
- const execPayload = { taskId, from: message.from, action, payload, encrypted: !!session?.encrypted };
369
- fetch(EXECUTOR_URL, {
370
- method: 'POST',
371
- headers: { 'Content-Type': 'application/json' },
372
- body: JSON.stringify(execPayload),
373
- }).catch(e => {
374
- log({ event: 'executor_forward_failed', taskId, error: e.message });
375
- });
376
- // Don't await — async execution
377
- } catch (e) {
378
- log({ event: 'executor_forward_failed', taskId, error: e.message });
379
- }
219
+ fetch(EXECUTOR_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId, from: message.from, action, payload, encrypted: !!session?.encrypted }) }).catch(e => log({ event: 'executor_forward_failed', taskId, error: e.message }));
220
+ return { status: 'accepted', taskId, message: 'Task accepted. Result will be pushed when ready.' };
380
221
  } else {
381
- // No executor configured — auto-complete with echo
222
+ // Echo mode
382
223
  enforcer.taskFinished();
383
224
  const trace = new ExecutionTrace(taskId, id);
384
225
  trace.append('TASK_ACCEPTED', { from: message.from, action, payload });
385
226
  const result = { status: 'no_executor', agent: id.agent_id, action, received_payload: payload };
386
- trace.append('TASK_ECHO', { result });
387
- trace.finalize(result);
227
+ trace.append('TASK_ECHO', { result }); trace.finalize(result);
388
228
  const proofGen = new ProofGenerator(trace, id);
389
229
  const proof = proofGen.generate(capTypes.join(',') || 'no-policy', `task-from-${message.from}`, JSON.stringify(result));
390
230
  const anchor = await anchorOnChain(proof.trace_root, { proof_id: proof.proof_id, task_from: message.from, action, taskId });
391
- delete pendingTasks[taskId];
392
- saveTasks(pendingTasks);
231
+ delete pendingTasks[taskId]; saveTasks(pendingTasks);
393
232
  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() });
394
233
  return { status: 'completed', taskId, result, proof, anchor };
395
234
  }
396
-
397
- // Return accepted immediately — result comes later via push-back
398
- return { status: 'accepted', taskId, message: 'Task accepted. Result will be pushed to your endpoint when ready.' };
399
- });
400
-
401
- endpoint.onProof(async (message, session) => {
402
- log({ event: 'proof_received', from: message.from, payload: message.payload, timestamp: new Date().toISOString() });
403
235
  });
404
236
 
405
- // Also handle task-result messages (results pushed back from other agents)
406
- endpoint.onMessage?.('task-result', async (message, session) => {
407
- log({ event: 'task_result_received', from: message.from, payload: message.payload, timestamp: new Date().toISOString() });
408
- });
237
+ endpoint.onProof(async (message) => { log({ event: 'proof_received', from: message.from, payload: message.payload, timestamp: new Date().toISOString() }); });
409
238
 
410
239
  await endpoint.start();
240
+
241
+ // Auto-register to Registry with candidates
242
+ if (capTypes.length > 0 && networkConfig.candidates.length > 0) {
243
+ try {
244
+ const regClient = new RegistryClient({ registryUrl: REGISTRY_URL });
245
+ // Pick best non-relay candidate as primary endpoint for backward compat
246
+ const bestDirect = networkConfig.candidates.find(c => c.type !== 'relay') || networkConfig.candidates[0];
247
+ await regClient.register({ name: id.agent_id, capabilities: caps, endpoint: bestDirect.url, candidates: networkConfig.candidates }, id);
248
+ log({ event: 'auto_registered', registry: REGISTRY_URL, candidates: networkConfig.candidates.length });
249
+ } catch (e) { log({ event: 'auto_register_failed', error: e.message }); }
250
+ }
251
+
411
252
  console.log(JSON.stringify({
412
- status: 'listening',
413
- agent_id: id.agent_id,
414
- did: id.did,
415
- endpoint: endpoint.getEndpointUrl(),
416
- port: p,
417
- capabilities: capTypes,
253
+ status: 'listening', agent_id: id.agent_id, did: id.did,
254
+ port: p, candidates: networkConfig.candidates, capabilities: capTypes,
418
255
  policy: { rateLimit: policy.rateLimit, maxPayloadBytes: policy.maxPayloadBytes, maxConcurrent: policy.maxConcurrent, allowedDIDs: policy.allowedDIDs.length, blockedDIDs: policy.blockedDIDs.length },
419
- executor: EXECUTOR_URL || 'not configured (echo mode)',
420
- inbox: INBOX_FILE,
256
+ executor: EXECUTOR_URL || 'echo mode', inbox: INBOX_FILE,
421
257
  }, null, 2));
422
258
 
423
259
  process.on('SIGINT', async () => { await endpoint.stop(); process.exit(0); });
@@ -426,10 +262,7 @@ async function cmdStart(port) {
426
262
 
427
263
  async function cmdInbox(count) {
428
264
  const n = parseInt(count || '20');
429
- if (!existsSync(INBOX_FILE)) {
430
- console.log(JSON.stringify({ messages: [], count: 0 }));
431
- return;
432
- }
265
+ if (!existsSync(INBOX_FILE)) { console.log(JSON.stringify({ messages: [], count: 0 })); return; }
433
266
  const lines = readFileSync(INBOX_FILE, 'utf-8').trim().split('\n').filter(Boolean);
434
267
  const messages = lines.slice(-n).map(l => JSON.parse(l));
435
268
  console.log(JSON.stringify({ messages, count: messages.length, total: lines.length }, null, 2));
@@ -437,15 +270,14 @@ async function cmdInbox(count) {
437
270
 
438
271
  async function cmdRegister(name, capabilities, endpointUrl) {
439
272
  const id = requireIdentity();
440
- const client = new RegistryClient({ registryUrl: REGISTRY_URL });
441
273
  const caps = (capabilities || 'general').split(',').map(c => ({ type: c.trim(), description: c.trim() }));
442
274
  saveCapabilities(caps);
443
- const entry = await client.register({
444
- name: name || id.agent_id,
445
- capabilities: caps,
446
- endpoint: endpointUrl || 'http://localhost:3100',
447
- }, id);
448
- console.log(JSON.stringify({ status: 'registered', did: entry.did, name: entry.name, capabilities: caps.map(c => c.type), registry: REGISTRY_URL }, null, 2));
275
+ // If no endpoint provided, use saved network config
276
+ let ep = endpointUrl;
277
+ if (!ep) { const net = loadNetwork(); ep = net?.endpoint || 'http://localhost:3100'; }
278
+ const client = new RegistryClient({ registryUrl: REGISTRY_URL });
279
+ const entry = await client.register({ name: name || id.agent_id, capabilities: caps, endpoint: ep }, id);
280
+ console.log(JSON.stringify({ status: 'registered', did: entry.did, name: entry.name, capabilities: caps.map(c => c.type), endpoint: ep, registry: REGISTRY_URL }, null, 2));
449
281
  }
450
282
 
451
283
  async function cmdSearch(capability) {
@@ -459,66 +291,87 @@ async function cmdHandshake(remoteEndpoint, remoteDid) {
459
291
  const client = new AgentClient(id);
460
292
  const hsManager = new HandshakeManager(id);
461
293
  let did = remoteDid;
462
- if (!did) {
463
- const health = await client.health(remoteEndpoint);
464
- did = health.did;
465
- }
294
+ if (!did) { const h = await client.health(remoteEndpoint); did = h.did; }
466
295
  const session = await client.handshake(remoteEndpoint, hsManager, did);
467
296
  console.log(JSON.stringify({ status: 'handshake_complete', sessionId: session.sessionId, remoteDid: did, encrypted: session.encrypted }, null, 2));
468
- const sessFile = resolve(ATEL_DIR, 'sessions.json');
469
- let sessions = {};
470
- if (existsSync(sessFile)) sessions = JSON.parse(readFileSync(sessFile, 'utf-8'));
297
+ const sf = resolve(ATEL_DIR, 'sessions.json');
298
+ let sessions = {}; if (existsSync(sf)) sessions = JSON.parse(readFileSync(sf, 'utf-8'));
471
299
  sessions[remoteEndpoint] = { did, sessionId: session.sessionId, encrypted: session.encrypted };
472
- writeFileSync(sessFile, JSON.stringify(sessions, null, 2));
300
+ writeFileSync(sf, JSON.stringify(sessions, null, 2));
473
301
  }
474
302
 
475
- async function cmdTask(remoteEndpoint, taskJson) {
303
+ async function cmdTask(target, taskJson) {
476
304
  const id = requireIdentity();
477
305
  const client = new AgentClient(id);
478
306
  const hsManager = new HandshakeManager(id);
479
- const sessFile = resolve(ATEL_DIR, 'sessions.json');
307
+
308
+ let remoteEndpoint = target;
480
309
  let remoteDid;
481
- if (existsSync(sessFile)) {
482
- const sessions = JSON.parse(readFileSync(sessFile, 'utf-8'));
483
- if (sessions[remoteEndpoint]) remoteDid = sessions[remoteEndpoint].did;
310
+
311
+ // If target looks like a DID or name, search Registry and try candidates
312
+ if (!target.startsWith('http')) {
313
+ const regClient = new RegistryClient({ registryUrl: REGISTRY_URL });
314
+ let entry;
315
+ // Try as DID first
316
+ try {
317
+ const resp = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(target)}`);
318
+ if (resp.ok) entry = await resp.json();
319
+ } catch {}
320
+ // Try as capability search
321
+ if (!entry) {
322
+ const results = await regClient.search({ type: target, limit: 5 });
323
+ if (results.length > 0) entry = results[0];
324
+ }
325
+ if (!entry) { console.error(`Agent not found: ${target}`); process.exit(1); }
326
+
327
+ remoteDid = entry.did;
328
+
329
+ // Try candidates if available
330
+ if (entry.candidates && entry.candidates.length > 0) {
331
+ console.log(JSON.stringify({ event: 'connecting', did: remoteDid, candidates: entry.candidates.length }));
332
+ const conn = await connectToAgent(entry.candidates);
333
+ if (conn) {
334
+ remoteEndpoint = conn.url;
335
+ console.log(JSON.stringify({ event: 'connected', type: conn.candidateType, url: conn.url, latencyMs: conn.latencyMs }));
336
+ } else {
337
+ console.error('All candidates unreachable'); process.exit(1);
338
+ }
339
+ } else {
340
+ remoteEndpoint = entry.endpoint;
341
+ }
484
342
  }
343
+
344
+ // Handshake
345
+ const sf = resolve(ATEL_DIR, 'sessions.json');
485
346
  if (!remoteDid) {
486
- const health = await client.health(remoteEndpoint);
487
- remoteDid = health.did;
488
- const session = await client.handshake(remoteEndpoint, hsManager, remoteDid);
489
- let sessions = {};
490
- if (existsSync(sessFile)) sessions = JSON.parse(readFileSync(sessFile, 'utf-8'));
491
- sessions[remoteEndpoint] = { did: remoteDid, sessionId: session.sessionId, encrypted: session.encrypted };
492
- writeFileSync(sessFile, JSON.stringify(sessions, null, 2));
493
- } else {
494
- await client.handshake(remoteEndpoint, hsManager, remoteDid);
347
+ const h = await client.health(remoteEndpoint); remoteDid = h.did;
495
348
  }
349
+ await client.handshake(remoteEndpoint, hsManager, remoteDid);
350
+ let sessions = {}; if (existsSync(sf)) sessions = JSON.parse(readFileSync(sf, 'utf-8'));
351
+ sessions[remoteEndpoint] = { did: remoteDid };
352
+ writeFileSync(sf, JSON.stringify(sessions, null, 2));
353
+
354
+ // Send task
496
355
  const payload = typeof taskJson === 'string' ? JSON.parse(taskJson) : taskJson;
497
356
  const msg = createMessage({ type: 'task', from: id.did, to: remoteDid, payload, secretKey: id.secretKey });
498
357
  const result = await client.sendTask(remoteEndpoint, msg, hsManager);
499
- console.log(JSON.stringify({ status: 'task_sent', remoteDid, result }, null, 2));
358
+ console.log(JSON.stringify({ status: 'task_sent', remoteDid, via: remoteEndpoint, result }, null, 2));
500
359
  }
501
360
 
502
361
  async function cmdResult(taskId, resultJson) {
503
- // Submit execution result back to local atel endpoint
504
362
  const result = typeof resultJson === 'string' ? JSON.parse(resultJson) : resultJson;
505
- const localUrl = `http://localhost:${process.env.ATEL_PORT || '3100'}/atel/v1/result`;
506
- const resp = await fetch(localUrl, {
507
- method: 'POST',
508
- headers: { 'Content-Type': 'application/json' },
509
- body: JSON.stringify({ taskId, result, success: true }),
510
- });
511
- const data = await resp.json();
512
- console.log(JSON.stringify(data, null, 2));
363
+ 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 }) });
364
+ console.log(JSON.stringify(await resp.json(), null, 2));
513
365
  }
514
366
 
515
367
  // ─── Main ────────────────────────────────────────────────────────
516
368
 
517
369
  const [,, cmd, ...args] = process.argv;
518
-
519
370
  const commands = {
520
371
  init: () => cmdInit(args[0]),
521
372
  info: () => cmdInfo(),
373
+ setup: () => cmdSetup(args[0]),
374
+ verify: () => cmdVerify(),
522
375
  start: () => cmdStart(args[0]),
523
376
  inbox: () => cmdInbox(args[0]),
524
377
  register: () => cmdRegister(args[0], args[1], args[2]),
@@ -534,9 +387,11 @@ if (!cmd || !commands[cmd]) {
534
387
  Usage: atel <command> [args]
535
388
 
536
389
  Commands:
537
- init [name] Create agent identity + default policy
538
- info Show identity, capabilities, policy
539
- start [port] Start endpoint (default: 3100)
390
+ init [name] Create agent identity + security policy
391
+ info Show identity, capabilities, network, policy
392
+ setup [port] Configure network (detect IP, UPnP, verify)
393
+ verify Verify port reachability
394
+ start [port] Start endpoint (auto network + auto register)
540
395
  inbox [count] Show received messages (default: 20)
541
396
  register [name] [caps] [endpoint] Register on public registry
542
397
  search <capability> Search registry for agents
@@ -545,22 +400,16 @@ Commands:
545
400
  result <taskId> <json> Submit execution result (from executor)
546
401
 
547
402
  Environment:
548
- ATEL_DIR Identity directory (default: .atel)
549
- ATEL_REGISTRY Registry URL (default: http://47.251.8.19:8100)
550
- ATEL_EXECUTOR_URL Local executor HTTP endpoint (for async task forwarding)
551
- ATEL_SOLANA_PRIVATE_KEY Solana private key for on-chain anchoring
552
- ATEL_SOLANA_RPC_URL Solana RPC URL (default: mainnet-beta)
553
-
554
- Security Policy (.atel/policy.json):
555
- rateLimit Max tasks per minute (default: 60)
556
- maxPayloadBytes Max payload size (default: 1MB)
557
- maxConcurrent Max concurrent tasks (default: 10)
558
- allowedDIDs DID whitelist (empty = allow all)
559
- blockedDIDs DID blacklist`);
403
+ ATEL_DIR Identity directory (default: .atel)
404
+ ATEL_REGISTRY Registry URL (default: http://47.251.8.19:8100)
405
+ ATEL_EXECUTOR_URL Local executor HTTP endpoint
406
+ ATEL_SOLANA_PRIVATE_KEY Solana key for on-chain anchoring
407
+ ATEL_SOLANA_RPC_URL Solana RPC (default: mainnet-beta)
408
+
409
+ Network: atel start auto-detects public IP, attempts UPnP port mapping,
410
+ and registers to the Registry. If UPnP fails, configure port forwarding
411
+ on your router and run: atel verify`);
560
412
  process.exit(cmd ? 1 : 0);
561
413
  }
562
414
 
563
- commands[cmd]().catch(err => {
564
- console.error(JSON.stringify({ error: err.message }));
565
- process.exit(1);
566
- });
415
+ commands[cmd]().catch(err => { console.error(JSON.stringify({ error: err.message })); process.exit(1); });