@lawreneliang/atel-sdk 0.4.1 → 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
@@ -4,88 +4,86 @@
4
4
  * ATEL CLI — Command-line interface for ATEL SDK
5
5
  *
6
6
  * Commands:
7
- * atel init [name] Create a new agent identity
8
- * atel info Show current agent identity
9
- * atel start [port] Start endpoint and listen for tasks
7
+ * atel init [name] Create agent identity + default policy
8
+ * atel info Show identity, capabilities, policy, network
9
+ * atel start [port] Start endpoint (auto network + auto register)
10
+ * atel setup [port] Network setup only (detect IP, UPnP, verify)
11
+ * atel verify Verify port reachability
10
12
  * atel inbox [count] Show received messages
11
13
  * atel register [name] [caps] [url] Register on public registry
12
14
  * atel search <capability> Search registry for agents
13
15
  * atel handshake <endpoint> [did] Handshake with a remote agent
14
16
  * atel task <endpoint> <json> Delegate a task to a remote agent
17
+ * atel result <taskId> <json> Submit execution result (from executor)
15
18
  */
16
19
 
17
20
  import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
18
21
  import { resolve } from 'node:path';
19
22
  import {
20
- AgentIdentity,
21
- AgentEndpoint,
22
- AgentClient,
23
- HandshakeManager,
24
- createMessage,
25
- RegistryClient,
26
- ExecutionTrace,
27
- ProofGenerator,
28
- SolanaAnchorProvider,
23
+ AgentIdentity, AgentEndpoint, AgentClient, HandshakeManager,
24
+ createMessage, RegistryClient, ExecutionTrace, ProofGenerator,
25
+ SolanaAnchorProvider, autoNetworkSetup, discoverPublicIP, verifyPortReachable,
29
26
  } from '@lawreneliang/atel-sdk';
30
27
 
31
28
  const ATEL_DIR = resolve(process.env.ATEL_DIR || '.atel');
32
29
  const IDENTITY_FILE = resolve(ATEL_DIR, 'identity.json');
33
30
  const REGISTRY_URL = process.env.ATEL_REGISTRY || 'http://47.251.8.19:8100';
34
- const WEBHOOK_URL = process.env.ATEL_WEBHOOK || '';
31
+ const EXECUTOR_URL = process.env.ATEL_EXECUTOR_URL || '';
35
32
  const INBOX_FILE = resolve(ATEL_DIR, 'inbox.jsonl');
33
+ const POLICY_FILE = resolve(ATEL_DIR, 'policy.json');
34
+ const TASKS_FILE = resolve(ATEL_DIR, 'tasks.json');
35
+ const NETWORK_FILE = resolve(ATEL_DIR, 'network.json');
36
+
37
+ const DEFAULT_POLICY = { rateLimit: 60, maxPayloadBytes: 1048576, maxConcurrent: 10, allowedDIDs: [], blockedDIDs: [] };
36
38
 
37
39
  // ─── Helpers ─────────────────────────────────────────────────────
38
40
 
39
- async function notify(event) {
40
- appendFileSync(INBOX_FILE, JSON.stringify(event) + '\n');
41
- if (WEBHOOK_URL) {
42
- try {
43
- await fetch(WEBHOOK_URL, {
44
- method: 'POST',
45
- headers: { 'Content-Type': 'application/json' },
46
- body: JSON.stringify(event),
47
- });
48
- } catch (e) {
49
- console.error(JSON.stringify({ event: 'webhook_failed', error: e.message }));
50
- }
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)); }
43
+
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; }
47
+
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)); }
56
+
57
+ // ─── Policy Enforcer ─────────────────────────────────────────────
58
+
59
+ class PolicyEnforcer {
60
+ constructor(policy) { this.policy = policy; this.requestLog = []; this.activeTasks = 0; }
61
+ check(message) {
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})` };
69
+ this.requestLog.push(now);
70
+ return { allowed: true };
51
71
  }
72
+ taskStarted() { this.activeTasks++; }
73
+ taskFinished() { this.activeTasks = Math.max(0, this.activeTasks - 1); }
52
74
  }
53
75
 
54
- function saveIdentity(identity) {
55
- if (!existsSync(ATEL_DIR)) mkdirSync(ATEL_DIR, { recursive: true });
56
- writeFileSync(IDENTITY_FILE, JSON.stringify({
57
- agent_id: identity.agent_id,
58
- did: identity.did,
59
- publicKey: Buffer.from(identity.publicKey).toString('hex'),
60
- secretKey: Buffer.from(identity.secretKey).toString('hex'),
61
- }, null, 2));
62
- }
63
-
64
- function loadIdentity() {
65
- if (!existsSync(IDENTITY_FILE)) return null;
66
- const data = JSON.parse(readFileSync(IDENTITY_FILE, 'utf-8'));
67
- return new AgentIdentity({
68
- agent_id: data.agent_id,
69
- publicKey: Uint8Array.from(Buffer.from(data.publicKey, 'hex')),
70
- secretKey: Uint8Array.from(Buffer.from(data.secretKey, 'hex')),
71
- });
72
- }
73
-
74
- function requireIdentity() {
75
- const id = loadIdentity();
76
- if (!id) { console.error('No identity found. Run: atel init'); process.exit(1); }
77
- return id;
78
- }
79
-
80
- // Load registered capabilities from .atel/capabilities.json
81
- function loadCapabilities() {
82
- const capFile = resolve(ATEL_DIR, 'capabilities.json');
83
- if (!existsSync(capFile)) return [];
84
- try { return JSON.parse(readFileSync(capFile, 'utf-8')); } catch { return []; }
85
- }
86
-
87
- function saveCapabilities(caps) {
88
- writeFileSync(resolve(ATEL_DIR, 'capabilities.json'), JSON.stringify(caps, null, 2));
76
+ // ─── On-chain Anchoring ──────────────────────────────────────────
77
+
78
+ async function anchorOnChain(traceRoot, metadata) {
79
+ const key = process.env.ATEL_SOLANA_PRIVATE_KEY;
80
+ if (!key) return null;
81
+ try {
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; }
89
87
  }
90
88
 
91
89
  // ─── Commands ────────────────────────────────────────────────────
@@ -94,22 +92,39 @@ async function cmdInit(agentId) {
94
92
  const name = agentId || `agent-${Date.now()}`;
95
93
  const identity = new AgentIdentity({ agent_id: name });
96
94
  saveIdentity(identity);
97
- console.log(JSON.stringify({
98
- status: 'created',
99
- agent_id: identity.agent_id,
100
- did: identity.did,
101
- next: 'Run: atel register [name] [capabilities] [endpoint]',
102
- }, null, 2));
95
+ savePolicy(DEFAULT_POLICY);
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));
103
97
  }
104
98
 
105
99
  async function cmdInfo() {
106
100
  const id = requireIdentity();
107
- const caps = loadCapabilities();
108
- console.log(JSON.stringify({
109
- agent_id: id.agent_id,
110
- did: id.did,
111
- capabilities: caps,
112
- }, 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); }
113
128
  }
114
129
 
115
130
  async function cmdStart(port) {
@@ -117,138 +132,130 @@ async function cmdStart(port) {
117
132
  const p = parseInt(port || '3100');
118
133
  const caps = loadCapabilities();
119
134
  const capTypes = caps.map(c => c.type || c);
135
+ const policy = loadPolicy();
136
+ const enforcer = new PolicyEnforcer(policy);
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 ──
120
157
  const endpoint = new AgentEndpoint(id, { port: p, host: '0.0.0.0' });
121
158
 
122
- endpoint.onTask(async (message, session) => {
123
- const payload = message.payload || {};
124
- const action = payload.action || payload.type || 'unknown';
159
+ // Result callback: POST /atel/v1/result (executor calls this when done)
160
+ endpoint.app?.post?.('/atel/v1/result', async (req, res) => {
161
+ const { taskId, result, success } = req.body || {};
162
+ if (!taskId || !pendingTasks[taskId]) { res.status(404).json({ error: 'Unknown taskId' }); return; }
163
+ const task = pendingTasks[taskId];
164
+ enforcer.taskFinished();
125
165
 
126
- const taskEvent = {
127
- event: 'task_received',
128
- from: message.from,
129
- action,
130
- payload,
131
- encrypted: !!session?.encrypted,
132
- timestamp: new Date().toISOString(),
133
- };
134
- console.log(JSON.stringify(taskEvent));
135
- await notify(taskEvent);
136
-
137
- // ── Capability boundary check ──
138
- if (capTypes.length > 0 && !capTypes.includes(action) && !capTypes.includes('general')) {
139
- const rejectEvent = {
140
- event: 'task_rejected',
141
- from: message.from,
142
- action,
143
- reason: `Action "${action}" is outside my capability boundary. My capabilities: [${capTypes.join(', ')}]`,
144
- timestamp: new Date().toISOString(),
145
- };
146
- console.log(JSON.stringify(rejectEvent));
147
- await notify(rejectEvent);
148
- return {
149
- status: 'rejected',
150
- error: rejectEvent.reason,
151
- capabilities: capTypes,
152
- };
153
- }
166
+ const trace = new ExecutionTrace(taskId, id);
167
+ trace.append('TASK_ACCEPTED', { from: task.from, action: task.action });
168
+ trace.append('TASK_FORWARDED', { executor_url: EXECUTOR_URL });
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'));
154
172
 
155
- // ── Full ATEL execution flow: Trace → Execute → Proof ──
156
- const trace = new ExecutionTrace(`task-${Date.now()}`, id);
157
- trace.append('TASK_ACCEPTED', { from: message.from, action, payload, encrypted: !!session?.encrypted });
173
+ const proofGen = new ProofGenerator(trace, id);
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 });
158
176
 
159
- let result;
160
- let success = true;
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() });
161
178
 
162
- try {
163
- // Default execution: acknowledge and return payload info
164
- result = {
165
- status: 'executed',
166
- agent: id.agent_id,
167
- action,
168
- received_payload: payload,
169
- capabilities: capTypes,
170
- };
171
- trace.append('TASK_EXECUTED', { action, result });
172
- } catch (err) {
173
- success = false;
174
- result = { status: 'error', error: err.message };
175
- trace.fail(err);
179
+ // Push result back to sender
180
+ if (task.senderEndpoint) {
181
+ try {
182
+ const client = new AgentClient(id);
183
+ const hsManager = new HandshakeManager(id);
184
+ await client.handshake(task.senderEndpoint, hsManager, task.from);
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 }); }
176
189
  }
177
190
 
178
- if (success && !trace.isFinalized() && !trace.isFailed()) {
179
- trace.finalize(typeof result === 'object' ? result : { result });
180
- }
191
+ delete pendingTasks[taskId]; saveTasks(pendingTasks);
192
+ res.json({ status: 'ok', proof_id: proof.proof_id, anchor_tx: anchor?.txHash || null });
193
+ });
181
194
 
182
- // Generate cryptographic proof
183
- const proofGen = new ProofGenerator(trace, id);
184
- const proof = proofGen.generate(
185
- capTypes.join(',') || 'no-policy',
186
- `task-from-${message.from}`,
187
- JSON.stringify(result),
188
- );
189
-
190
- // Anchor proof on-chain (Solana) if configured
191
- let anchor = null;
192
- const solanaKey = process.env.ATEL_SOLANA_PRIVATE_KEY;
193
- if (solanaKey) {
194
- try {
195
- const solana = new SolanaAnchorProvider({
196
- rpcUrl: process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com',
197
- privateKey: solanaKey,
198
- });
199
- anchor = await solana.anchor(proof.trace_root, {
200
- proof_id: proof.proof_id,
201
- task_from: message.from,
202
- action,
203
- });
204
- console.log(JSON.stringify({
205
- event: 'proof_anchored',
206
- chain: 'solana',
207
- txHash: anchor.txHash,
208
- block: anchor.blockNumber,
209
- trace_root: proof.trace_root,
210
- }));
211
- } catch (e) {
212
- console.error(JSON.stringify({ event: 'anchor_failed', chain: 'solana', error: e.message }));
213
- }
195
+ // Task handler
196
+ endpoint.onTask(async (message, session) => {
197
+ const payload = message.payload || {};
198
+ const action = payload.action || payload.type || 'unknown';
199
+
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 }; }
203
+
204
+ // Capability check
205
+ if (capTypes.length > 0 && !capTypes.includes(action) && !capTypes.includes('general')) {
206
+ log({ event: 'task_rejected', from: message.from, action, reason: `Outside capability: [${capTypes.join(',')}]`, timestamp: new Date().toISOString() });
207
+ return { status: 'rejected', error: `Action "${action}" outside capability boundary`, capabilities: capTypes };
214
208
  }
215
209
 
216
- const completionEvent = {
217
- event: 'task_completed',
218
- from: message.from,
219
- action,
220
- success,
221
- proof_id: proof.proof_id,
222
- trace_root: proof.trace_root,
223
- timestamp: new Date().toISOString(),
224
- };
225
- console.log(JSON.stringify(completionEvent));
226
- await notify(completionEvent);
227
-
228
- return { status: success ? 'completed' : 'failed', result, proof, anchor };
210
+ const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
211
+ enforcer.taskStarted();
212
+
213
+ // Lookup sender endpoint for result push-back
214
+ let senderEndpoint = null;
215
+ try { const r = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(message.from)}`); if (r.ok) senderEndpoint = (await r.json()).endpoint; } catch {}
216
+
217
+ pendingTasks[taskId] = { from: message.from, action, payload, senderEndpoint, encrypted: !!session?.encrypted, acceptedAt: new Date().toISOString() };
218
+ saveTasks(pendingTasks);
219
+ log({ event: 'task_accepted', taskId, from: message.from, action, encrypted: !!session?.encrypted, timestamp: new Date().toISOString() });
220
+
221
+ // Forward to executor or echo
222
+ if (EXECUTOR_URL) {
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.' };
225
+ } else {
226
+ // Echo mode
227
+ enforcer.taskFinished();
228
+ const trace = new ExecutionTrace(taskId, id);
229
+ trace.append('TASK_ACCEPTED', { from: message.from, action, payload });
230
+ const result = { status: 'no_executor', agent: id.agent_id, action, received_payload: payload };
231
+ trace.append('TASK_ECHO', { result }); trace.finalize(result);
232
+ const proofGen = new ProofGenerator(trace, id);
233
+ const proof = proofGen.generate(capTypes.join(',') || 'no-policy', `task-from-${message.from}`, JSON.stringify(result));
234
+ const anchor = await anchorOnChain(proof.trace_root, { proof_id: proof.proof_id, task_from: message.from, action, taskId });
235
+ delete pendingTasks[taskId]; saveTasks(pendingTasks);
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() });
237
+ return { status: 'completed', taskId, result, proof, anchor };
238
+ }
229
239
  });
230
240
 
231
- endpoint.onProof(async (message, session) => {
232
- const proofEvent = {
233
- event: 'proof_received',
234
- from: message.from,
235
- payload: message.payload,
236
- timestamp: new Date().toISOString(),
237
- };
238
- console.log(JSON.stringify(proofEvent));
239
- await notify(proofEvent);
240
- });
241
+ endpoint.onProof(async (message) => { log({ event: 'proof_received', from: message.from, payload: message.payload, timestamp: new Date().toISOString() }); });
241
242
 
242
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
+
243
254
  console.log(JSON.stringify({
244
- status: 'listening',
245
- agent_id: id.agent_id,
246
- did: id.did,
247
- endpoint: endpoint.getEndpointUrl(),
248
- port: p,
249
- capabilities: capTypes,
250
- inbox: INBOX_FILE,
251
- webhook: WEBHOOK_URL || 'not configured (set ATEL_WEBHOOK)',
255
+ status: 'listening', agent_id: id.agent_id, did: id.did,
256
+ endpoint: publicEndpoint || `http://0.0.0.0:${p}`, port: p, capabilities: capTypes,
257
+ policy: { rateLimit: policy.rateLimit, maxPayloadBytes: policy.maxPayloadBytes, maxConcurrent: policy.maxConcurrent, allowedDIDs: policy.allowedDIDs.length, blockedDIDs: policy.blockedDIDs.length },
258
+ executor: EXECUTOR_URL || 'echo mode', inbox: INBOX_FILE,
252
259
  }, null, 2));
253
260
 
254
261
  process.on('SIGINT', async () => { await endpoint.stop(); process.exit(0); });
@@ -257,10 +264,7 @@ async function cmdStart(port) {
257
264
 
258
265
  async function cmdInbox(count) {
259
266
  const n = parseInt(count || '20');
260
- if (!existsSync(INBOX_FILE)) {
261
- console.log(JSON.stringify({ messages: [], count: 0 }));
262
- return;
263
- }
267
+ if (!existsSync(INBOX_FILE)) { console.log(JSON.stringify({ messages: [], count: 0 })); return; }
264
268
  const lines = readFileSync(INBOX_FILE, 'utf-8').trim().split('\n').filter(Boolean);
265
269
  const messages = lines.slice(-n).map(l => JSON.parse(l));
266
270
  console.log(JSON.stringify({ messages, count: messages.length, total: lines.length }, null, 2));
@@ -268,24 +272,14 @@ async function cmdInbox(count) {
268
272
 
269
273
  async function cmdRegister(name, capabilities, endpointUrl) {
270
274
  const id = requireIdentity();
271
- const client = new RegistryClient({ registryUrl: REGISTRY_URL });
272
- const caps = (capabilities || 'general').split(',').map(c => ({
273
- type: c.trim(), description: c.trim(),
274
- }));
275
- // Save capabilities locally for boundary checking
275
+ const caps = (capabilities || 'general').split(',').map(c => ({ type: c.trim(), description: c.trim() }));
276
276
  saveCapabilities(caps);
277
- const entry = await client.register({
278
- name: name || id.agent_id,
279
- capabilities: caps,
280
- endpoint: endpointUrl || `http://localhost:3100`,
281
- }, id);
282
- console.log(JSON.stringify({
283
- status: 'registered',
284
- did: entry.did,
285
- name: entry.name,
286
- capabilities: caps.map(c => c.type),
287
- registry: REGISTRY_URL,
288
- }, 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));
289
283
  }
290
284
 
291
285
  async function cmdSearch(capability) {
@@ -299,64 +293,56 @@ async function cmdHandshake(remoteEndpoint, remoteDid) {
299
293
  const client = new AgentClient(id);
300
294
  const hsManager = new HandshakeManager(id);
301
295
  let did = remoteDid;
302
- if (!did) {
303
- const health = await client.health(remoteEndpoint);
304
- did = health.did;
305
- }
296
+ if (!did) { const h = await client.health(remoteEndpoint); did = h.did; }
306
297
  const session = await client.handshake(remoteEndpoint, hsManager, did);
307
- console.log(JSON.stringify({
308
- status: 'handshake_complete',
309
- sessionId: session.sessionId,
310
- remoteDid: did,
311
- encrypted: session.encrypted,
312
- }, null, 2));
313
- const sessFile = resolve(ATEL_DIR, 'sessions.json');
314
- let sessions = {};
315
- if (existsSync(sessFile)) sessions = JSON.parse(readFileSync(sessFile, 'utf-8'));
298
+ console.log(JSON.stringify({ status: 'handshake_complete', sessionId: session.sessionId, remoteDid: did, encrypted: session.encrypted }, null, 2));
299
+ const sf = resolve(ATEL_DIR, 'sessions.json');
300
+ let sessions = {}; if (existsSync(sf)) sessions = JSON.parse(readFileSync(sf, 'utf-8'));
316
301
  sessions[remoteEndpoint] = { did, sessionId: session.sessionId, encrypted: session.encrypted };
317
- writeFileSync(sessFile, JSON.stringify(sessions, null, 2));
302
+ writeFileSync(sf, JSON.stringify(sessions, null, 2));
318
303
  }
319
304
 
320
305
  async function cmdTask(remoteEndpoint, taskJson) {
321
306
  const id = requireIdentity();
322
307
  const client = new AgentClient(id);
323
308
  const hsManager = new HandshakeManager(id);
324
- const sessFile = resolve(ATEL_DIR, 'sessions.json');
309
+ const sf = resolve(ATEL_DIR, 'sessions.json');
325
310
  let remoteDid;
326
- if (existsSync(sessFile)) {
327
- const sessions = JSON.parse(readFileSync(sessFile, 'utf-8'));
328
- if (sessions[remoteEndpoint]) remoteDid = sessions[remoteEndpoint].did;
329
- }
311
+ if (existsSync(sf)) { const s = JSON.parse(readFileSync(sf, 'utf-8')); if (s[remoteEndpoint]) remoteDid = s[remoteEndpoint].did; }
330
312
  if (!remoteDid) {
331
- const health = await client.health(remoteEndpoint);
332
- remoteDid = health.did;
313
+ const h = await client.health(remoteEndpoint); remoteDid = h.did;
333
314
  const session = await client.handshake(remoteEndpoint, hsManager, remoteDid);
334
- let sessions = {};
335
- if (existsSync(sessFile)) sessions = JSON.parse(readFileSync(sessFile, 'utf-8'));
315
+ let sessions = {}; if (existsSync(sf)) sessions = JSON.parse(readFileSync(sf, 'utf-8'));
336
316
  sessions[remoteEndpoint] = { did: remoteDid, sessionId: session.sessionId, encrypted: session.encrypted };
337
- writeFileSync(sessFile, JSON.stringify(sessions, null, 2));
338
- } else {
339
- await client.handshake(remoteEndpoint, hsManager, remoteDid);
340
- }
317
+ writeFileSync(sf, JSON.stringify(sessions, null, 2));
318
+ } else { await client.handshake(remoteEndpoint, hsManager, remoteDid); }
341
319
  const payload = typeof taskJson === 'string' ? JSON.parse(taskJson) : taskJson;
342
320
  const msg = createMessage({ type: 'task', from: id.did, to: remoteDid, payload, secretKey: id.secretKey });
343
321
  const result = await client.sendTask(remoteEndpoint, msg, hsManager);
344
322
  console.log(JSON.stringify({ status: 'task_sent', remoteDid, result }, null, 2));
345
323
  }
346
324
 
325
+ async function cmdResult(taskId, resultJson) {
326
+ const result = typeof resultJson === 'string' ? JSON.parse(resultJson) : resultJson;
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));
329
+ }
330
+
347
331
  // ─── Main ────────────────────────────────────────────────────────
348
332
 
349
333
  const [,, cmd, ...args] = process.argv;
350
-
351
334
  const commands = {
352
335
  init: () => cmdInit(args[0]),
353
336
  info: () => cmdInfo(),
337
+ setup: () => cmdSetup(args[0]),
338
+ verify: () => cmdVerify(),
354
339
  start: () => cmdStart(args[0]),
355
340
  inbox: () => cmdInbox(args[0]),
356
341
  register: () => cmdRegister(args[0], args[1], args[2]),
357
342
  search: () => cmdSearch(args[0]),
358
343
  handshake: () => cmdHandshake(args[0], args[1]),
359
344
  task: () => cmdTask(args[0], args[1]),
345
+ result: () => cmdResult(args[0], args[1]),
360
346
  };
361
347
 
362
348
  if (!cmd || !commands[cmd]) {
@@ -365,23 +351,29 @@ if (!cmd || !commands[cmd]) {
365
351
  Usage: atel <command> [args]
366
352
 
367
353
  Commands:
368
- init [name] Create agent identity
369
- info Show agent identity + capabilities
370
- 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)
371
359
  inbox [count] Show received messages (default: 20)
372
360
  register [name] [caps] [endpoint] Register on public registry
373
361
  search <capability> Search registry for agents
374
362
  handshake <endpoint> [did] Handshake with remote agent
375
363
  task <endpoint> <json> Delegate task to remote agent
364
+ result <taskId> <json> Submit execution result (from executor)
376
365
 
377
366
  Environment:
378
- ATEL_DIR Identity directory (default: .atel)
379
- ATEL_REGISTRY Registry URL (default: http://47.251.8.19:8100)
380
- ATEL_WEBHOOK Webhook URL for task notifications (optional)`);
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`);
381
376
  process.exit(cmd ? 1 : 0);
382
377
  }
383
378
 
384
- commands[cmd]().catch(err => {
385
- console.error(JSON.stringify({ error: err.message }));
386
- process.exit(1);
387
- });
379
+ commands[cmd]().catch(err => { console.error(JSON.stringify({ error: err.message })); process.exit(1); });
@@ -1 +1 @@
1
- var _0x476e83=_0x2a9b;function _0x2a9b(_0x5f34b6,_0x42923b){_0x5f34b6=_0x5f34b6-0xec;var _0x22d026=_0x22d0();var _0x2a9baa=_0x22d026[_0x5f34b6];if(_0x2a9b['YequIh']===undefined){var _0x5e127e=function(_0xab93f4){var _0x396e24='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x2eac82='',_0x36aea9='';for(var _0x4ec691=0x0,_0x1312d8,_0x2333ba,_0xe90437=0x0;_0x2333ba=_0xab93f4['charAt'](_0xe90437++);~_0x2333ba&&(_0x1312d8=_0x4ec691%0x4?_0x1312d8*0x40+_0x2333ba:_0x2333ba,_0x4ec691++%0x4)?_0x2eac82+=String['fromCharCode'](0xff&_0x1312d8>>(-0x2*_0x4ec691&0x6)):0x0){_0x2333ba=_0x396e24['indexOf'](_0x2333ba);}for(var _0x5b87a3=0x0,_0xbc37f6=_0x2eac82['length'];_0x5b87a3<_0xbc37f6;_0x5b87a3++){_0x36aea9+='%'+('00'+_0x2eac82['charCodeAt'](_0x5b87a3)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x36aea9);};_0x2a9b['gWshJl']=_0x5e127e,_0x2a9b['WOkJhG']={},_0x2a9b['YequIh']=!![];}var _0xdc556b=_0x22d026[0x0],_0x131490=_0x5f34b6+_0xdc556b,_0x1a0c07=_0x2a9b['WOkJhG'][_0x131490];return!_0x1a0c07?(_0x2a9baa=_0x2a9b['gWshJl'](_0x2a9baa),_0x2a9b['WOkJhG'][_0x131490]=_0x2a9baa):_0x2a9baa=_0x1a0c07,_0x2a9baa;}(function(_0x2d7fdf,_0x1f2b81){var _0x5863e2=_0x2a9b,_0x487383=_0x2d7fdf();while(!![]){try{var _0x345dc7=parseInt(_0x5863e2(0xf8))/0x1+parseInt(_0x5863e2(0xec))/0x2*(parseInt(_0x5863e2(0xf3))/0x3)+-parseInt(_0x5863e2(0xf9))/0x4*(parseInt(_0x5863e2(0xef))/0x5)+-parseInt(_0x5863e2(0xf0))/0x6*(parseInt(_0x5863e2(0xf2))/0x7)+parseInt(_0x5863e2(0xfb))/0x8*(-parseInt(_0x5863e2(0xf7))/0x9)+parseInt(_0x5863e2(0xfa))/0xa+parseInt(_0x5863e2(0xf5))/0xb;if(_0x345dc7===_0x1f2b81)break;else _0x487383['push'](_0x487383['shift']());}catch(_0x3cee47){_0x487383['push'](_0x487383['shift']());}}}(_0x22d0,0x1b992));import{EvmAnchorProvider}from'./evm.js';export class BaseAnchorProvider extends EvmAnchorProvider{static [_0x476e83(0xf1)]=_0x476e83(0xed);constructor(_0xe8ec07){var _0x294047=_0x476e83,_0xe504a7={};_0xe504a7['RVhYK']='base';var _0x17fdb3=_0xe504a7,_0x179ad6={};_0x179ad6['rpcUrl']=_0xe8ec07?.[_0x294047(0xee)]??BaseAnchorProvider[_0x294047(0xf1)],_0x179ad6[_0x294047(0xf4)]=_0xe8ec07?.['privateKey'],super('Base',_0x17fdb3[_0x294047(0xf6)],_0x179ad6);}}function _0x22d0(){var _0x4f9a42=['owjNAu5cAW','mtyZmZu0wwzhz0vw','odiWAujoCxP6','nJq3mJyWDK1wt1rM','nte5mJi0AxHLCwjb','nhn6EKX2uq','Ahr0Chm6lY9TywLUBMv0lMjHC2uUB3jN','CNbJvxjS','ntmZnxLnzuHvwa','ntrgzNfwu3q','revgqvvmvf9suenFvvjm','mti5otLWwxzeEKC','nZC1mdHNthDQA04','ChjPDMf0zuTLEq','mtq3mda1mwj0q2TOEG','uLzOwuS'];_0x22d0=function(){return _0x4f9a42;};return _0x22d0();}
1
+ var _0x483110=_0x4cb5;(function(_0x1fd45e,_0x567a4e){var _0x19d003=_0x4cb5,_0x108dcc=_0x1fd45e();while(!![]){try{var _0x1929b7=parseInt(_0x19d003(0x18b))/0x1+-parseInt(_0x19d003(0x18c))/0x2*(parseInt(_0x19d003(0x197))/0x3)+-parseInt(_0x19d003(0x18f))/0x4+parseInt(_0x19d003(0x194))/0x5*(-parseInt(_0x19d003(0x198))/0x6)+parseInt(_0x19d003(0x196))/0x7*(-parseInt(_0x19d003(0x18d))/0x8)+-parseInt(_0x19d003(0x191))/0x9+parseInt(_0x19d003(0x193))/0xa;if(_0x1929b7===_0x567a4e)break;else _0x108dcc['push'](_0x108dcc['shift']());}catch(_0xd6d414){_0x108dcc['push'](_0x108dcc['shift']());}}}(_0x4f55,0xba4a9));import{EvmAnchorProvider}from'./evm.js';function _0x4cb5(_0x533730,_0x3072ed){_0x533730=_0x533730-0x18b;var _0x4f5540=_0x4f55();var _0x4cb5c6=_0x4f5540[_0x533730];if(_0x4cb5['DnpPFI']===undefined){var _0x709a45=function(_0x1c066a){var _0x33535='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x3fa596='',_0xc3f8db='';for(var _0x165f94=0x0,_0x2b7516,_0x26e591,_0x21cdcd=0x0;_0x26e591=_0x1c066a['charAt'](_0x21cdcd++);~_0x26e591&&(_0x2b7516=_0x165f94%0x4?_0x2b7516*0x40+_0x26e591:_0x26e591,_0x165f94++%0x4)?_0x3fa596+=String['fromCharCode'](0xff&_0x2b7516>>(-0x2*_0x165f94&0x6)):0x0){_0x26e591=_0x33535['indexOf'](_0x26e591);}for(var _0x251e1b=0x0,_0x38bdda=_0x3fa596['length'];_0x251e1b<_0x38bdda;_0x251e1b++){_0xc3f8db+='%'+('00'+_0x3fa596['charCodeAt'](_0x251e1b)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0xc3f8db);};_0x4cb5['hrDzih']=_0x709a45,_0x4cb5['kntpEe']={},_0x4cb5['DnpPFI']=!![];}var _0x16c097=_0x4f5540[0x0],_0x2f96b0=_0x533730+_0x16c097,_0x3db64a=_0x4cb5['kntpEe'][_0x2f96b0];return!_0x3db64a?(_0x4cb5c6=_0x4cb5['hrDzih'](_0x4cb5c6),_0x4cb5['kntpEe'][_0x2f96b0]=_0x4cb5c6):_0x4cb5c6=_0x3db64a,_0x4cb5c6;}function _0x4f55(){var _0x2285b9=['nJe5nZeXDKfZrwTT','mtb1uKfQvvO','oeHxqxLzqW','yMfZzq','ndeYntKYogHcCuPtvW','revgqvvmvf9suenFvvjm','mZq5otC4nwnwvwnyBW','qMfZzq','ndq0odiWmZb1EMPlCei','mte4nde1r2r5s3LM','zenuqKW','otG0ode3ne5VCgvjra','nZG3mteZz3H0tKj4','ndjpBvbhzuO'];_0x4f55=function(){return _0x2285b9;};return _0x4f55();}export class BaseAnchorProvider extends EvmAnchorProvider{static [_0x483110(0x190)]='https://mainnet.base.org';constructor(_0x5c6a7f){var _0x4ae5bb=_0x483110,_0x11bae9={};_0x11bae9[_0x4ae5bb(0x195)]=_0x4ae5bb(0x192);var _0x2bc7fe=_0x11bae9,_0x566c4f={};_0x566c4f['rpcUrl']=_0x5c6a7f?.['rpcUrl']??BaseAnchorProvider[_0x4ae5bb(0x190)],_0x566c4f['privateKey']=_0x5c6a7f?.['privateKey'],super(_0x2bc7fe['dCTBL'],_0x4ae5bb(0x18e),_0x566c4f);}}