@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.
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/bin/atel.mjs +2692 -0
- package/bin/tunnel-manager.mjs +171 -0
- package/dist/anchor/base.d.ts +21 -0
- package/dist/anchor/base.js +26 -0
- package/dist/anchor/bsc.d.ts +20 -0
- package/dist/anchor/bsc.js +25 -0
- package/dist/anchor/evm.d.ts +99 -0
- package/dist/anchor/evm.js +262 -0
- package/dist/anchor/index.d.ts +173 -0
- package/dist/anchor/index.js +165 -0
- package/dist/anchor/mock.d.ts +43 -0
- package/dist/anchor/mock.js +100 -0
- package/dist/anchor/solana.d.ts +95 -0
- package/dist/anchor/solana.js +298 -0
- package/dist/auditor/index.d.ts +54 -0
- package/dist/auditor/index.js +141 -0
- package/dist/collaboration/index.d.ts +146 -0
- package/dist/collaboration/index.js +237 -0
- package/dist/crypto/index.d.ts +162 -0
- package/dist/crypto/index.js +231 -0
- package/dist/endpoint/index.d.ts +147 -0
- package/dist/endpoint/index.js +390 -0
- package/dist/envelope/index.d.ts +104 -0
- package/dist/envelope/index.js +156 -0
- package/dist/executor/index.d.ts +71 -0
- package/dist/executor/index.js +398 -0
- package/dist/gateway/index.d.ts +278 -0
- package/dist/gateway/index.js +520 -0
- package/dist/graph/index.d.ts +215 -0
- package/dist/graph/index.js +524 -0
- package/dist/handshake/index.d.ts +166 -0
- package/dist/handshake/index.js +287 -0
- package/dist/identity/index.d.ts +155 -0
- package/dist/identity/index.js +250 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +28 -0
- package/dist/negotiation/index.d.ts +133 -0
- package/dist/negotiation/index.js +160 -0
- package/dist/network/index.d.ts +78 -0
- package/dist/network/index.js +207 -0
- package/dist/orchestrator/index.d.ts +190 -0
- package/dist/orchestrator/index.js +297 -0
- package/dist/policy/index.d.ts +100 -0
- package/dist/policy/index.js +206 -0
- package/dist/proof/index.d.ts +220 -0
- package/dist/proof/index.js +541 -0
- package/dist/registry/index.d.ts +98 -0
- package/dist/registry/index.js +129 -0
- package/dist/rollback/index.d.ts +76 -0
- package/dist/rollback/index.js +91 -0
- package/dist/schema/capability-schema.json +52 -0
- package/dist/schema/index.d.ts +128 -0
- package/dist/schema/index.js +163 -0
- package/dist/schema/task-schema.json +69 -0
- package/dist/score/index.d.ts +174 -0
- package/dist/score/index.js +275 -0
- package/dist/service/index.d.ts +34 -0
- package/dist/service/index.js +273 -0
- package/dist/service/server.d.ts +7 -0
- package/dist/service/server.js +22 -0
- package/dist/trace/index.d.ts +217 -0
- package/dist/trace/index.js +446 -0
- package/dist/trust/index.d.ts +84 -0
- package/dist/trust/index.js +107 -0
- package/dist/trust-sync/index.d.ts +30 -0
- package/dist/trust-sync/index.js +57 -0
- package/package.json +71 -0
- package/skill/SKILL.md +363 -0
- package/skill/references/commercial.md +184 -0
- package/skill/references/executor.md +356 -0
- package/skill/references/networking.md +64 -0
- package/skill/references/onchain.md +73 -0
- 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
|
+
}
|