@lawreneliang/atel-sdk 0.4.2 → 0.4.3

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