@lawrenceliang-btc/atel-sdk 0.8.7

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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +221 -0
  3. package/bin/atel.mjs +2692 -0
  4. package/bin/tunnel-manager.mjs +171 -0
  5. package/dist/anchor/base.d.ts +21 -0
  6. package/dist/anchor/base.js +26 -0
  7. package/dist/anchor/bsc.d.ts +20 -0
  8. package/dist/anchor/bsc.js +25 -0
  9. package/dist/anchor/evm.d.ts +99 -0
  10. package/dist/anchor/evm.js +262 -0
  11. package/dist/anchor/index.d.ts +173 -0
  12. package/dist/anchor/index.js +165 -0
  13. package/dist/anchor/mock.d.ts +43 -0
  14. package/dist/anchor/mock.js +100 -0
  15. package/dist/anchor/solana.d.ts +95 -0
  16. package/dist/anchor/solana.js +298 -0
  17. package/dist/auditor/index.d.ts +54 -0
  18. package/dist/auditor/index.js +141 -0
  19. package/dist/collaboration/index.d.ts +146 -0
  20. package/dist/collaboration/index.js +237 -0
  21. package/dist/crypto/index.d.ts +162 -0
  22. package/dist/crypto/index.js +231 -0
  23. package/dist/endpoint/index.d.ts +147 -0
  24. package/dist/endpoint/index.js +390 -0
  25. package/dist/envelope/index.d.ts +104 -0
  26. package/dist/envelope/index.js +156 -0
  27. package/dist/executor/index.d.ts +71 -0
  28. package/dist/executor/index.js +398 -0
  29. package/dist/gateway/index.d.ts +278 -0
  30. package/dist/gateway/index.js +520 -0
  31. package/dist/graph/index.d.ts +215 -0
  32. package/dist/graph/index.js +524 -0
  33. package/dist/handshake/index.d.ts +166 -0
  34. package/dist/handshake/index.js +287 -0
  35. package/dist/identity/index.d.ts +155 -0
  36. package/dist/identity/index.js +250 -0
  37. package/dist/index.d.ts +23 -0
  38. package/dist/index.js +28 -0
  39. package/dist/negotiation/index.d.ts +133 -0
  40. package/dist/negotiation/index.js +160 -0
  41. package/dist/network/index.d.ts +78 -0
  42. package/dist/network/index.js +207 -0
  43. package/dist/orchestrator/index.d.ts +190 -0
  44. package/dist/orchestrator/index.js +297 -0
  45. package/dist/policy/index.d.ts +100 -0
  46. package/dist/policy/index.js +206 -0
  47. package/dist/proof/index.d.ts +220 -0
  48. package/dist/proof/index.js +541 -0
  49. package/dist/registry/index.d.ts +98 -0
  50. package/dist/registry/index.js +129 -0
  51. package/dist/rollback/index.d.ts +76 -0
  52. package/dist/rollback/index.js +91 -0
  53. package/dist/schema/capability-schema.json +52 -0
  54. package/dist/schema/index.d.ts +128 -0
  55. package/dist/schema/index.js +163 -0
  56. package/dist/schema/task-schema.json +69 -0
  57. package/dist/score/index.d.ts +174 -0
  58. package/dist/score/index.js +275 -0
  59. package/dist/service/index.d.ts +34 -0
  60. package/dist/service/index.js +273 -0
  61. package/dist/service/server.d.ts +7 -0
  62. package/dist/service/server.js +22 -0
  63. package/dist/trace/index.d.ts +217 -0
  64. package/dist/trace/index.js +446 -0
  65. package/dist/trust/index.d.ts +84 -0
  66. package/dist/trust/index.js +107 -0
  67. package/dist/trust-sync/index.d.ts +30 -0
  68. package/dist/trust-sync/index.js +57 -0
  69. package/package.json +71 -0
  70. package/skill/SKILL.md +363 -0
  71. package/skill/references/commercial.md +184 -0
  72. package/skill/references/executor.md +356 -0
  73. package/skill/references/networking.md +64 -0
  74. package/skill/references/onchain.md +73 -0
  75. package/skill/references/security.md +96 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Module: Message Envelope
3
+ *
4
+ * Standardized message format for ATEL inter-agent communication.
5
+ * Every message is signed by the sender and verified by the receiver.
6
+ * Includes nonce + timestamp for replay protection.
7
+ */
8
+ import { v4 as uuidv4 } from 'uuid';
9
+ import { sign, verify, serializePayload } from '../identity/index.js';
10
+ // ─── Custom Errors ───────────────────────────────────────────────
11
+ export class EnvelopeError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = 'EnvelopeError';
15
+ }
16
+ }
17
+ // ─── Constants ───────────────────────────────────────────────────
18
+ /** Maximum age of a message before it's considered expired (5 minutes) */
19
+ const MAX_MESSAGE_AGE_MS = 5 * 60 * 1000;
20
+ // ─── Core Functions ──────────────────────────────────────────────
21
+ /**
22
+ * Create and sign an ATEL message.
23
+ *
24
+ * @param options - Message creation options.
25
+ * @returns A signed ATELMessage.
26
+ */
27
+ export function createMessage(options) {
28
+ const { type, from, to, payload, secretKey } = options;
29
+ const unsigned = {
30
+ envelope: 'atel.msg.v1',
31
+ type,
32
+ from,
33
+ to,
34
+ timestamp: new Date().toISOString(),
35
+ nonce: uuidv4(),
36
+ payload,
37
+ };
38
+ const signable = serializePayload(unsigned);
39
+ const signature = sign(signable, secretKey);
40
+ return { ...unsigned, signature };
41
+ }
42
+ /**
43
+ * Verify an ATEL message's signature and freshness.
44
+ *
45
+ * Checks:
46
+ * 1. Envelope version is supported
47
+ * 2. Required fields are present
48
+ * 3. Timestamp is not too old (replay protection)
49
+ * 4. Signature is valid against sender's public key
50
+ *
51
+ * @param message - The message to verify.
52
+ * @param senderPublicKey - The sender's 32-byte Ed25519 public key.
53
+ * @param options - Optional verification settings.
54
+ * @returns Verification result.
55
+ */
56
+ export function verifyMessage(message, senderPublicKey, options) {
57
+ // Check envelope version
58
+ if (message.envelope !== 'atel.msg.v1') {
59
+ return { valid: false, error: `Unsupported envelope version: ${message.envelope}` };
60
+ }
61
+ // Check required fields
62
+ if (!message.from || !message.to || !message.type || !message.nonce) {
63
+ return { valid: false, error: 'Missing required fields' };
64
+ }
65
+ // Check timestamp freshness (replay protection)
66
+ if (!options?.skipTimestampCheck) {
67
+ const maxAge = options?.maxAgeMs ?? MAX_MESSAGE_AGE_MS;
68
+ const messageTime = new Date(message.timestamp).getTime();
69
+ const now = Date.now();
70
+ if (isNaN(messageTime)) {
71
+ return { valid: false, error: 'Invalid timestamp format' };
72
+ }
73
+ if (now - messageTime > maxAge) {
74
+ return { valid: false, error: 'Message expired (timestamp too old)' };
75
+ }
76
+ if (messageTime > now + 30_000) {
77
+ return { valid: false, error: 'Message timestamp is in the future' };
78
+ }
79
+ }
80
+ // Verify signature
81
+ const { signature, ...unsigned } = message;
82
+ const signable = serializePayload(unsigned);
83
+ if (!verify(signable, signature, senderPublicKey)) {
84
+ return { valid: false, error: 'Invalid signature' };
85
+ }
86
+ return { valid: true };
87
+ }
88
+ /**
89
+ * Serialize an ATEL message to JSON string for transmission.
90
+ *
91
+ * @param message - The message to serialize.
92
+ * @returns JSON string.
93
+ */
94
+ export function serializeMessage(message) {
95
+ return JSON.stringify(message);
96
+ }
97
+ /**
98
+ * Deserialize a JSON string into an ATEL message.
99
+ *
100
+ * @param json - The JSON string to parse.
101
+ * @returns The parsed ATELMessage.
102
+ * @throws EnvelopeError if parsing fails.
103
+ */
104
+ export function deserializeMessage(json) {
105
+ try {
106
+ const parsed = JSON.parse(json);
107
+ if (!parsed.envelope || !parsed.type || !parsed.from) {
108
+ throw new EnvelopeError('Invalid ATEL message structure');
109
+ }
110
+ return parsed;
111
+ }
112
+ catch (e) {
113
+ if (e instanceof EnvelopeError)
114
+ throw e;
115
+ throw new EnvelopeError(`Failed to parse ATEL message: ${e.message}`);
116
+ }
117
+ }
118
+ // ─── Nonce Tracker (Replay Protection) ───────────────────────────
119
+ /**
120
+ * Tracks seen nonces to prevent replay attacks.
121
+ * Automatically evicts expired entries.
122
+ */
123
+ export class NonceTracker {
124
+ seen = new Map();
125
+ maxAgeMs;
126
+ constructor(maxAgeMs = MAX_MESSAGE_AGE_MS * 2) {
127
+ this.maxAgeMs = maxAgeMs;
128
+ }
129
+ /**
130
+ * Check if a nonce has been seen before. If not, record it.
131
+ *
132
+ * @param nonce - The nonce to check.
133
+ * @returns True if the nonce is new (not a replay).
134
+ */
135
+ check(nonce) {
136
+ this.evict();
137
+ if (this.seen.has(nonce)) {
138
+ return false; // Replay detected
139
+ }
140
+ this.seen.set(nonce, Date.now());
141
+ return true;
142
+ }
143
+ /** Remove expired nonces. */
144
+ evict() {
145
+ const cutoff = Date.now() - this.maxAgeMs;
146
+ for (const [nonce, ts] of this.seen) {
147
+ if (ts < cutoff) {
148
+ this.seen.delete(nonce);
149
+ }
150
+ }
151
+ }
152
+ /** Get the number of tracked nonces. */
153
+ get size() {
154
+ return this.seen.size;
155
+ }
156
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Module: Built-in Executor
3
+ *
4
+ * Default executor that bridges ATEL tasks to OpenClaw agent sessions.
5
+ * Automatically started by `atel start` when no external ATEL_EXECUTOR_URL is set.
6
+ *
7
+ * Flow:
8
+ * 1. Receives task from ATEL endpoint
9
+ * 2. Reads agent-context.md for shared context (if exists)
10
+ * 3. Calls OpenClaw Gateway → sessions_spawn
11
+ * 4. Polls for result via sessions_history
12
+ * 5. Callbacks result to ATEL endpoint
13
+ *
14
+ * Security:
15
+ * - Business-layer payload audit (capability mismatch, fs ops, network, code exec)
16
+ * - Configurable via policy.json
17
+ * - Agent context file provides identity without exposing private data
18
+ */
19
+ export interface BuiltinExecutorConfig {
20
+ /** Port to listen on */
21
+ port: number;
22
+ /** ATEL agent callback URL (for result delivery) */
23
+ callbackUrl: string;
24
+ /** OpenClaw Gateway URL */
25
+ gatewayUrl?: string;
26
+ /** OpenClaw Gateway auth token */
27
+ gatewayToken?: string;
28
+ /** Path to agent-context.md (optional shared context) */
29
+ contextPath?: string;
30
+ /** ToolGateway proxy URL (optional) */
31
+ toolProxyUrl?: string;
32
+ /** Logger function */
33
+ log?: (obj: Record<string, unknown>) => void;
34
+ }
35
+ export interface ExecutorAuditResult {
36
+ safe: boolean;
37
+ reason?: string;
38
+ severity?: 'low' | 'medium' | 'high' | 'critical';
39
+ }
40
+ export interface TaskRequest {
41
+ taskId: string;
42
+ from: string;
43
+ action: string;
44
+ payload: Record<string, unknown>;
45
+ toolProxy?: string;
46
+ }
47
+ export declare class BuiltinExecutor {
48
+ private app;
49
+ private server;
50
+ private config;
51
+ private agentContext;
52
+ private taskHistoryPath;
53
+ private log;
54
+ constructor(config: BuiltinExecutorConfig);
55
+ private loadContext;
56
+ private loadTaskHistory;
57
+ private saveTaskHistory;
58
+ private setupRoutes;
59
+ private processTask;
60
+ private buildPrompt;
61
+ private executeDirect;
62
+ private pollResultFile;
63
+ private executeViaTool;
64
+ private finalizeTool;
65
+ private callback;
66
+ /** Business-layer security audit */
67
+ auditPayload(action: string, payload: Record<string, unknown>): ExecutorAuditResult;
68
+ start(): Promise<void>;
69
+ stop(): Promise<void>;
70
+ get url(): string;
71
+ }
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Module: Built-in Executor
3
+ *
4
+ * Default executor that bridges ATEL tasks to OpenClaw agent sessions.
5
+ * Automatically started by `atel start` when no external ATEL_EXECUTOR_URL is set.
6
+ *
7
+ * Flow:
8
+ * 1. Receives task from ATEL endpoint
9
+ * 2. Reads agent-context.md for shared context (if exists)
10
+ * 3. Calls OpenClaw Gateway → sessions_spawn
11
+ * 4. Polls for result via sessions_history
12
+ * 5. Callbacks result to ATEL endpoint
13
+ *
14
+ * Security:
15
+ * - Business-layer payload audit (capability mismatch, fs ops, network, code exec)
16
+ * - Configurable via policy.json
17
+ * - Agent context file provides identity without exposing private data
18
+ */
19
+ import express from 'express';
20
+ import { readFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
21
+ import { join } from 'node:path';
22
+ // ─── Built-in Executor ──────────────────────────────────────────
23
+ export class BuiltinExecutor {
24
+ app;
25
+ server = null;
26
+ config;
27
+ agentContext = '';
28
+ taskHistoryPath = '';
29
+ log;
30
+ constructor(config) {
31
+ this.config = {
32
+ gatewayUrl: 'http://127.0.0.1:18789',
33
+ gatewayToken: '',
34
+ contextPath: '',
35
+ toolProxyUrl: '',
36
+ ...config,
37
+ };
38
+ this.log = config.log || ((obj) => console.log(`[Executor] ${JSON.stringify(obj)}`));
39
+ // Auto-detect gateway token
40
+ if (!this.config.gatewayToken) {
41
+ try {
42
+ const home = process.env.HOME || '';
43
+ const c = JSON.parse(readFileSync(join(home, '.openclaw/openclaw.json'), 'utf-8'));
44
+ this.config.gatewayToken = c.gateway?.auth?.token || '';
45
+ }
46
+ catch { /* ignore */ }
47
+ }
48
+ // Load agent context
49
+ this.loadContext();
50
+ // Task history path
51
+ this.taskHistoryPath = join(process.cwd(), '.atel', 'task-history.md');
52
+ // Setup express
53
+ this.app = express();
54
+ this.app.use(express.json({ limit: '1mb' }));
55
+ this.setupRoutes();
56
+ }
57
+ loadContext() {
58
+ // Try multiple paths for agent context
59
+ const paths = [
60
+ this.config.contextPath,
61
+ join(process.cwd(), '.atel', 'agent-context.md'),
62
+ join(process.cwd(), 'agent-context.md'),
63
+ ].filter(Boolean);
64
+ for (const p of paths) {
65
+ if (existsSync(p)) {
66
+ this.agentContext = readFileSync(p, 'utf-8');
67
+ this.log({ event: 'context_loaded', path: p, size: this.agentContext.length });
68
+ return;
69
+ }
70
+ }
71
+ this.log({ event: 'context_not_found', note: 'No agent-context.md found, tasks will run without shared context' });
72
+ }
73
+ loadTaskHistory(limit = 10) {
74
+ try {
75
+ if (!existsSync(this.taskHistoryPath))
76
+ return '';
77
+ const content = readFileSync(this.taskHistoryPath, 'utf-8');
78
+ // Get last N entries (each entry starts with "### ")
79
+ const entries = content.split(/(?=^### )/m).filter(e => e.trim());
80
+ const recent = entries.slice(-limit).join('\n');
81
+ if (recent) {
82
+ this.log({ event: 'history_loaded', entries: entries.length, using: Math.min(entries.length, limit) });
83
+ }
84
+ return recent;
85
+ }
86
+ catch {
87
+ return '';
88
+ }
89
+ }
90
+ saveTaskHistory(taskId, from, action, payload, result, success) {
91
+ try {
92
+ const timestamp = new Date().toISOString();
93
+ const text = (payload.text || payload.message || payload.task || payload.query || '');
94
+ const resultText = typeof result === 'object' && result !== null
95
+ ? (result.response || JSON.stringify(result)).toString().slice(0, 200)
96
+ : String(result).slice(0, 200);
97
+ const entry = `### ${timestamp} | ${action} | from: ${from.slice(-8)}
98
+ - Task: ${text.slice(0, 150)}
99
+ - Result: ${success ? resultText : 'FAILED'}
100
+ - Status: ${success ? 'success' : 'failed'}
101
+
102
+ `;
103
+ appendFileSync(this.taskHistoryPath, entry);
104
+ this.log({ event: 'history_saved', taskId });
105
+ }
106
+ catch (e) {
107
+ this.log({ event: 'history_save_failed', taskId, error: e.message });
108
+ }
109
+ }
110
+ setupRoutes() {
111
+ this.app.get('/health', (_req, res) => {
112
+ res.json({ status: 'ok', type: 'builtin-executor', gateway: this.config.gatewayUrl, hasContext: !!this.agentContext });
113
+ });
114
+ // Internal endpoint for ToolGateway to call back into executor
115
+ this.app.post('/internal/openclaw_agent', async (req, res) => {
116
+ const { tool, input } = req.body;
117
+ try {
118
+ const prompt = input?.prompt || input?.text || JSON.stringify(input);
119
+ const taskId = input?.taskId || `internal-${Date.now()}`;
120
+ const result = await this.executeDirect(prompt, taskId);
121
+ res.json(result);
122
+ }
123
+ catch (e) {
124
+ const msg = e instanceof Error ? e.message : String(e);
125
+ res.status(500).json({ error: msg });
126
+ }
127
+ });
128
+ this.app.post('/', async (req, res) => {
129
+ const { taskId, from, action, payload, toolProxy } = req.body;
130
+ this.log({ event: 'task_received', taskId, from, action, toolProxy: toolProxy || 'none' });
131
+ // Respond immediately
132
+ res.json({ status: 'accepted', taskId });
133
+ // Process async
134
+ this.processTask({ taskId, from, action, payload, toolProxy: toolProxy || this.config.toolProxyUrl || '' });
135
+ });
136
+ }
137
+ async processTask(task) {
138
+ const { taskId, from, action, payload, toolProxy } = task;
139
+ try {
140
+ // ── ToolGateway init ──
141
+ if (toolProxy) {
142
+ this.log({ event: 'toolgateway_init', taskId });
143
+ await fetch(`${toolProxy}/init`, {
144
+ method: 'POST',
145
+ headers: { 'Content-Type': 'application/json' },
146
+ body: JSON.stringify({ taskId }),
147
+ });
148
+ // Register openclaw_agent tool so ToolGateway can record trace
149
+ await fetch(`${toolProxy}/register`, {
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/json' },
152
+ body: JSON.stringify({
153
+ taskId,
154
+ tool: 'openclaw_agent',
155
+ endpoint: `http://127.0.0.1:${this.config.port}/internal/openclaw_agent`,
156
+ }),
157
+ });
158
+ this.log({ event: 'toolgateway_registered', taskId, tool: 'openclaw_agent' });
159
+ }
160
+ // ── Security audit ──
161
+ const audit = this.auditPayload(action, payload);
162
+ if (!audit.safe) {
163
+ this.log({ event: 'task_rejected', taskId, reason: audit.reason });
164
+ if (toolProxy) {
165
+ await this.finalizeTool(toolProxy, taskId, false, { error: `Security: ${audit.reason}` });
166
+ }
167
+ await this.callback(taskId, { error: `Security: ${audit.reason}`, severity: audit.severity }, false);
168
+ return;
169
+ }
170
+ // ── Build prompt with context ──
171
+ const prompt = this.buildPrompt(from, action, payload);
172
+ this.log({ event: 'executing', taskId, promptLength: prompt.length });
173
+ // ── Execute via OpenClaw Gateway ──
174
+ let result;
175
+ if (toolProxy) {
176
+ result = await this.executeViaTool(toolProxy, taskId, prompt, action, from);
177
+ }
178
+ else {
179
+ result = await this.executeDirect(prompt, taskId);
180
+ }
181
+ this.log({ event: 'task_completed', taskId });
182
+ // ── Save task history ──
183
+ this.saveTaskHistory(taskId, from, action, payload, result, true);
184
+ // ── Finalize trace ──
185
+ let trace = null;
186
+ if (toolProxy) {
187
+ trace = await this.finalizeTool(toolProxy, taskId, true, result);
188
+ }
189
+ // ── Callback ──
190
+ await this.callback(taskId, result, true, trace);
191
+ }
192
+ catch (e) {
193
+ const msg = e instanceof Error ? e.message : String(e);
194
+ this.log({ event: 'task_failed', taskId, error: msg });
195
+ // Save failed task to history
196
+ this.saveTaskHistory(taskId, from, action, payload, { error: msg }, false);
197
+ if (toolProxy) {
198
+ await this.finalizeTool(toolProxy, taskId, false, { error: msg }).catch(() => { });
199
+ }
200
+ await this.callback(taskId, { error: msg }, false).catch(() => { });
201
+ }
202
+ }
203
+ buildPrompt(from, action, payload) {
204
+ const text = (payload.text || payload.message || payload.task || payload.query || JSON.stringify(payload));
205
+ const guides = {
206
+ translation: `Translate the following text to ${payload.target_lang || 'the target language'}. Return only the translation.`,
207
+ coding: 'Help with the following coding task. Provide working code with brief explanation.',
208
+ research: 'Research the following topic and provide useful, accurate information.',
209
+ general: 'Complete the following task.',
210
+ };
211
+ const guide = guides[action] || guides.general;
212
+ let prompt = '';
213
+ if (this.agentContext) {
214
+ prompt += `## Agent Context\n${this.agentContext}\n\n`;
215
+ }
216
+ const history = this.loadTaskHistory();
217
+ if (history) {
218
+ prompt += `## Recent Task History\n${history}\n\n`;
219
+ }
220
+ prompt += `## Task\n${guide}\n\n${text}`;
221
+ return prompt;
222
+ }
223
+ async executeDirect(prompt, taskId) {
224
+ const { gatewayUrl, gatewayToken } = this.config;
225
+ // Result file: sub-session writes result here, we poll for it
226
+ const resultDir = join(process.cwd(), '.atel', 'results');
227
+ try {
228
+ mkdirSync(resultDir, { recursive: true });
229
+ }
230
+ catch { /* exists */ }
231
+ const resultFile = join(resultDir, `${taskId}.json`);
232
+ // Wrap prompt: instruct sub-agent to write result to file
233
+ const wrappedPrompt = `${prompt}
234
+
235
+ IMPORTANT: After completing the task, you MUST write your final answer to this file using the write tool:
236
+ File: ${resultFile}
237
+ Write ONLY a JSON object: {"done":true,"response":"<your answer here>"}
238
+ Do not include any other text in the file. This is required for the result to be delivered.`;
239
+ // Spawn sub-session via Gateway HTTP API
240
+ const spawnResp = await fetch(`${gatewayUrl}/tools/invoke`, {
241
+ method: 'POST',
242
+ headers: {
243
+ 'Content-Type': 'application/json',
244
+ 'Authorization': `Bearer ${gatewayToken}`,
245
+ },
246
+ body: JSON.stringify({
247
+ tool: 'sessions_spawn',
248
+ args: { task: wrappedPrompt, runTimeoutSeconds: 120 },
249
+ }),
250
+ signal: AbortSignal.timeout(10000),
251
+ });
252
+ if (!spawnResp.ok) {
253
+ const errText = await spawnResp.text();
254
+ if (errText.includes('not_found') || errText.includes('not available')) {
255
+ throw new Error('sessions_spawn not available via Gateway HTTP API. Add "sessions_spawn" to gateway.tools.allow in openclaw.json and restart gateway.');
256
+ }
257
+ throw new Error(`Gateway spawn failed: ${spawnResp.status} ${errText}`);
258
+ }
259
+ const spawnData = await spawnResp.json();
260
+ const childKey = spawnData.result?.details?.childSessionKey || spawnData.result?.childSessionKey;
261
+ this.log({ event: 'session_spawned', taskId, childKey, resultFile });
262
+ // Poll for result file
263
+ return this.pollResultFile(resultFile, taskId, 120000);
264
+ }
265
+ async pollResultFile(resultFile, taskId, timeoutMs) {
266
+ const deadline = Date.now() + timeoutMs;
267
+ const interval = 2000;
268
+ while (Date.now() < deadline) {
269
+ await new Promise(r => setTimeout(r, interval));
270
+ try {
271
+ if (existsSync(resultFile)) {
272
+ const content = readFileSync(resultFile, 'utf-8').trim();
273
+ try {
274
+ const parsed = JSON.parse(content);
275
+ if (parsed.done) {
276
+ this.log({ event: 'result_received', taskId, method: 'file' });
277
+ // Cleanup
278
+ try {
279
+ require('node:fs').unlinkSync(resultFile);
280
+ }
281
+ catch { /* ignore */ }
282
+ return { response: parsed.response, agent: 'builtin-executor', action: taskId.split('-')[0] || 'general' };
283
+ }
284
+ }
285
+ catch {
286
+ // File exists but not valid JSON yet, keep polling
287
+ }
288
+ }
289
+ }
290
+ catch { /* retry */ }
291
+ }
292
+ throw new Error(`Task ${taskId} timed out after ${timeoutMs}ms`);
293
+ }
294
+ async executeViaTool(toolProxy, taskId, prompt, action, from) {
295
+ const resp = await fetch(`${toolProxy}/call`, {
296
+ method: 'POST',
297
+ headers: { 'Content-Type': 'application/json' },
298
+ body: JSON.stringify({
299
+ taskId,
300
+ tool: 'openclaw_agent',
301
+ input: { prompt, action, from },
302
+ }),
303
+ });
304
+ if (!resp.ok) {
305
+ throw new Error(`ToolGateway call failed: ${resp.status} ${await resp.text()}`);
306
+ }
307
+ const result = await resp.json();
308
+ return result.output;
309
+ }
310
+ async finalizeTool(toolProxy, taskId, success, result) {
311
+ try {
312
+ const resp = await fetch(`${toolProxy}/finalize`, {
313
+ method: 'POST',
314
+ headers: { 'Content-Type': 'application/json' },
315
+ body: JSON.stringify({ taskId, success, result }),
316
+ });
317
+ if (resp.ok) {
318
+ const data = await resp.json();
319
+ this.log({ event: 'trace_finalized', taskId, events: data.trace?.events?.length || 0 });
320
+ return data.trace;
321
+ }
322
+ }
323
+ catch { /* ignore */ }
324
+ return null;
325
+ }
326
+ async callback(taskId, result, success, trace) {
327
+ const body = { taskId, result, success };
328
+ if (trace)
329
+ body.trace = trace;
330
+ await fetch(this.config.callbackUrl, {
331
+ method: 'POST',
332
+ headers: { 'Content-Type': 'application/json' },
333
+ body: JSON.stringify(body),
334
+ });
335
+ this.log({ event: 'callback_sent', taskId, success });
336
+ }
337
+ /** Business-layer security audit */
338
+ auditPayload(action, payload) {
339
+ const text = JSON.stringify(payload).toLowerCase();
340
+ // Cross-capability mismatch
341
+ const mismatch = {
342
+ translation: [/send.*email/i, /database/i, /api.*call/i, /execute.*code/i],
343
+ coding: [/send.*email/i, /database.*query/i, /translate/i],
344
+ research: [/execute/i, /run.*code/i, /send.*email/i],
345
+ };
346
+ if (mismatch[action]) {
347
+ for (const p of mismatch[action]) {
348
+ if (p.test(text))
349
+ return { safe: false, reason: `Action "${action}" mismatch: ${p.source}`, severity: 'medium' };
350
+ }
351
+ }
352
+ // File system ops
353
+ for (const p of [/read.*file/i, /write.*file/i, /create.*file/i, /delete.*file/i, /save.*to.*disk/i]) {
354
+ if (p.test(text))
355
+ return { safe: false, reason: 'File system operations require consent', severity: 'high' };
356
+ }
357
+ // External network
358
+ for (const p of [/fetch.*http/i, /api.*request/i, /call.*external/i, /webhook/i]) {
359
+ if (p.test(text))
360
+ return { safe: false, reason: 'External network requests require consent', severity: 'high' };
361
+ }
362
+ // Code execution
363
+ for (const p of [/execute.*code/i, /run.*script/i, /eval\(/i, /exec\(/i]) {
364
+ if (p.test(text))
365
+ return { safe: false, reason: 'Code execution not allowed', severity: 'critical' };
366
+ }
367
+ return { safe: true };
368
+ }
369
+ // ─── Lifecycle ──────────────────────────────────────────────────
370
+ async start() {
371
+ return new Promise((resolve) => {
372
+ this.server = this.app.listen(this.config.port, '127.0.0.1', () => {
373
+ this.log({
374
+ event: 'started',
375
+ port: this.config.port,
376
+ gateway: this.config.gatewayUrl,
377
+ callback: this.config.callbackUrl,
378
+ hasToken: !!this.config.gatewayToken,
379
+ hasContext: !!this.agentContext,
380
+ });
381
+ resolve();
382
+ });
383
+ });
384
+ }
385
+ async stop() {
386
+ if (this.server) {
387
+ return new Promise((resolve) => {
388
+ this.server.close(() => {
389
+ this.log({ event: 'stopped' });
390
+ resolve();
391
+ });
392
+ });
393
+ }
394
+ }
395
+ get url() {
396
+ return `http://127.0.0.1:${this.config.port}`;
397
+ }
398
+ }