@xpr-agents/openclaw 0.8.0 → 0.8.2
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/README.md +25 -11
- package/dist/tools/a2a.d.ts.map +1 -1
- package/dist/tools/a2a.js +31 -0
- package/dist/tools/a2a.js.map +1 -1
- package/dist/util/ssrf.d.ts +4 -0
- package/dist/util/ssrf.d.ts.map +1 -0
- package/dist/util/ssrf.js +72 -0
- package/dist/util/ssrf.js.map +1 -0
- package/openclaw.plugin.json +94 -1
- package/package.json +3 -3
- package/skills/code-sandbox/dist/index.js +103 -102
- package/skills/code-sandbox/src/index.ts +104 -108
- package/skills/creative/dist/index.js +6 -2
- package/skills/creative/dist/ssrf.js +96 -0
- package/skills/creative/src/index.ts +7 -2
- package/skills/creative/src/ssrf.ts +91 -0
- package/skills/defi/dist/index.js +19 -0
- package/skills/defi/src/index.ts +22 -0
- package/skills/defi/test-read.mjs +1 -1
- package/skills/governance/test-read.mjs +1 -1
- package/skills/lending/test-read.mjs +1 -1
- package/skills/nft/dist/index.js +19 -0
- package/skills/nft/src/index.ts +18 -0
- package/skills/web-scraping/dist/index.js +4 -2
- package/skills/web-scraping/dist/ssrf.js +96 -0
- package/skills/web-scraping/src/index.ts +5 -2
- package/skills/web-scraping/src/ssrf.ts +91 -0
- package/skills/xmd/test-read.mjs +1 -1
|
@@ -15,60 +15,75 @@ const DEFAULT_TIMEOUT = 5000;
|
|
|
15
15
|
const MAX_TIMEOUT = 30000;
|
|
16
16
|
const MAX_OUTPUT_SIZE = 10 * 1024 * 1024; // 10MB
|
|
17
17
|
// ── Sandbox helpers ─────────────────────────────
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
18
|
+
//
|
|
19
|
+
// SECURITY: the context is given ONLY primitive strings — never a host function or
|
|
20
|
+
// object. That is the whole game with node:vm. `codeGeneration.strings:false` only
|
|
21
|
+
// disables eval/Function *for this context's own realm*; a host closure handed in
|
|
22
|
+
// (a console mock, atob/btoa via Buffer, anything) exposes `fn.constructor` — the
|
|
23
|
+
// HOST realm's Function, where code generation is still allowed — so
|
|
24
|
+
// `console.log.constructor("return process.env")()` would read the runner's secrets
|
|
25
|
+
// and `Object.getPrototypeOf(hostFn).constructor.prototype` would pollute the host.
|
|
26
|
+
// So: console, atob/btoa, INPUT parsing and the *result serialization* all run as
|
|
27
|
+
// sandbox-realm code (below). Serializing inside the sandbox also keeps it under the
|
|
28
|
+
// execution timeout — a malicious getter can no longer stall the host via a
|
|
29
|
+
// host-side JSON.stringify. The only value read back out is a JSON string (a
|
|
30
|
+
// primitive), which the host then parses safely.
|
|
31
|
+
/** Sandbox-realm preamble: pure-JS console/atob/btoa/INPUT, no host references. */
|
|
32
|
+
const SANDBOX_PREAMBLE = `
|
|
33
|
+
var __logs = [];
|
|
34
|
+
var console = {
|
|
35
|
+
log: function(){ __logs.push(Array.prototype.map.call(arguments, function(a){ return typeof a === 'object' ? JSON.stringify(a) : String(a); }).join(' ')); },
|
|
36
|
+
warn: function(){ __logs.push('[warn] ' + Array.prototype.map.call(arguments, function(a){ return typeof a === 'object' ? JSON.stringify(a) : String(a); }).join(' ')); },
|
|
37
|
+
error: function(){ __logs.push('[error] ' + Array.prototype.map.call(arguments, function(a){ return typeof a === 'object' ? JSON.stringify(a) : String(a); }).join(' ')); }
|
|
38
|
+
};
|
|
39
|
+
var __B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
40
|
+
function btoa(s){ s = String(s); var o = ''; for (var i = 0; i < s.length; ) {
|
|
41
|
+
var c1 = s.charCodeAt(i++), c2 = s.charCodeAt(i++), c3 = s.charCodeAt(i++);
|
|
42
|
+
var e1 = c1 >> 2, e2 = ((c1 & 3) << 4) | (c2 >> 4), e3 = ((c2 & 15) << 2) | (c3 >> 6), e4 = c3 & 63;
|
|
43
|
+
if (isNaN(c2)) { e3 = e4 = 64; } else if (isNaN(c3)) { e4 = 64; }
|
|
44
|
+
o += __B64.charAt(e1) + __B64.charAt(e2) + (e3 === 64 ? '=' : __B64.charAt(e3)) + (e4 === 64 ? '=' : __B64.charAt(e4));
|
|
45
|
+
} return o; }
|
|
46
|
+
function atob(s){ s = String(s).replace(/[^A-Za-z0-9+/=]/g, ''); var o = ''; for (var i = 0; i < s.length; ) {
|
|
47
|
+
var d1 = __B64.indexOf(s.charAt(i++)), d2 = __B64.indexOf(s.charAt(i++)), d3 = __B64.indexOf(s.charAt(i++)), d4 = __B64.indexOf(s.charAt(i++));
|
|
48
|
+
var c1 = (d1 << 2) | (d2 >> 4), c2 = ((d2 & 15) << 4) | (d3 >> 2), c3 = ((d3 & 3) << 6) | d4;
|
|
49
|
+
o += String.fromCharCode(c1); if (d3 !== 64 && d3 >= 0) o += String.fromCharCode(c2); if (d4 !== 64 && d4 >= 0) o += String.fromCharCode(c3);
|
|
50
|
+
} return o; }
|
|
51
|
+
`.trim();
|
|
52
|
+
/**
|
|
53
|
+
* Run a self-contained expression/body in a fresh vm realm and return the parsed
|
|
54
|
+
* outcome. `body` must be a statement list whose LAST expression is the user value
|
|
55
|
+
* to capture. Everything is serialized to a JSON string inside the sandbox.
|
|
56
|
+
*/
|
|
57
|
+
function runInSandbox(body, timeoutMs, inputJson) {
|
|
58
|
+
// Only a primitive string crosses into the realm — AND the context global is
|
|
59
|
+
// given a NULL prototype. If we hand vm.createContext an ordinary host object,
|
|
60
|
+
// the sandbox global inherits the HOST realm's Object.prototype, so
|
|
61
|
+
// `this.constructor.constructor("return process.env")()` walks to the host
|
|
62
|
+
// Function (where codeGeneration is allowed) and reads the runner's secrets —
|
|
63
|
+
// codeGeneration:false only covers THIS context's realm. A null-proto global
|
|
64
|
+
// makes `this.constructor` resolve to the sandbox realm's own Function, which
|
|
65
|
+
// the flag then blocks. (Confirmed escape via the global-object path, 2026-09-19.)
|
|
66
|
+
const sandboxGlobal = Object.create(null);
|
|
67
|
+
sandboxGlobal.__INPUT_JSON = inputJson;
|
|
68
|
+
const context = vm_1.default.createContext(sandboxGlobal, {
|
|
69
|
+
codeGeneration: { strings: false, wasm: false },
|
|
70
|
+
});
|
|
71
|
+
const wrapped = `${SANDBOX_PREAMBLE}
|
|
72
|
+
var INPUT = (typeof __INPUT_JSON === 'string') ? JSON.parse(__INPUT_JSON) : undefined;
|
|
73
|
+
var __result, __error = null;
|
|
74
|
+
try { __result = (function(){ ${body} \n})(); } catch (e) { __error = (e && e.message) ? String(e.message) : String(e); }
|
|
75
|
+
(function(){
|
|
76
|
+
try { return JSON.stringify({ ok: __error === null, result: __result === undefined ? null : __result, logs: __logs, error: __error }); }
|
|
77
|
+
catch (e) { return JSON.stringify({ ok: __error === null, result: String(__result), logs: __logs, error: __error }); }
|
|
78
|
+
})();`;
|
|
79
|
+
const script = new vm_1.default.Script(wrapped, { filename: 'sandbox.js' });
|
|
80
|
+
const out = script.runInContext(context, { timeout: timeoutMs });
|
|
81
|
+
if (typeof out !== 'string')
|
|
82
|
+
return { ok: false, logs: [], error: 'sandbox produced no serializable output' };
|
|
83
|
+
if (out.length > MAX_OUTPUT_SIZE)
|
|
84
|
+
return { ok: false, logs: [], error: 'Output exceeded 10MB limit', oversized: true };
|
|
85
|
+
const parsed = JSON.parse(out);
|
|
86
|
+
return parsed;
|
|
72
87
|
}
|
|
73
88
|
// ── Skill entry point ───────────────────────────
|
|
74
89
|
function codeSandboxSkill(api) {
|
|
@@ -97,48 +112,40 @@ function codeSandboxSkill(api) {
|
|
|
97
112
|
return { error: 'code parameter is required and must be a string' };
|
|
98
113
|
}
|
|
99
114
|
const timeoutMs = Math.min(Math.max(timeout || DEFAULT_TIMEOUT, 100), MAX_TIMEOUT);
|
|
100
|
-
const logs = [];
|
|
101
115
|
const startTime = Date.now();
|
|
116
|
+
let inputJson;
|
|
102
117
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
const result = script.runInContext(context, { timeout: timeoutMs });
|
|
118
|
+
inputJson = input === undefined ? undefined : JSON.stringify(input);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return { error: 'input could not be serialized to JSON' };
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const out = runInSandbox(code, timeoutMs, inputJson);
|
|
111
125
|
const durationMs = Date.now() - startTime;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
result: serialized.slice(0, 1000) + '... [truncated]',
|
|
116
|
-
logs,
|
|
117
|
-
duration_ms: durationMs,
|
|
118
|
-
warning: 'Output exceeded 10MB limit and was truncated',
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
// Parse back to preserve types (arrays, objects)
|
|
122
|
-
let parsed;
|
|
123
|
-
try {
|
|
124
|
-
parsed = JSON.parse(serialized);
|
|
126
|
+
if (out.oversized) {
|
|
127
|
+
return { error: 'Output exceeded 10MB limit', logs: out.logs, duration_ms: durationMs, warning: 'Output exceeded 10MB limit and was truncated' };
|
|
125
128
|
}
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
if (!out.ok) {
|
|
130
|
+
const message = out.error || 'unknown error';
|
|
131
|
+
if (message.includes('Code generation from strings disallowed')) {
|
|
132
|
+
return { error: 'eval() and Function() constructor are blocked in the sandbox. Use direct code instead.', logs: out.logs, duration_ms: durationMs };
|
|
133
|
+
}
|
|
134
|
+
return { error: message, logs: out.logs, duration_ms: durationMs };
|
|
128
135
|
}
|
|
129
|
-
return { result:
|
|
136
|
+
return { result: out.result, logs: out.logs, duration_ms: durationMs };
|
|
130
137
|
}
|
|
131
138
|
catch (err) {
|
|
132
139
|
const durationMs = Date.now() - startTime;
|
|
133
140
|
const message = err.message || String(err);
|
|
134
|
-
//
|
|
135
|
-
if (message.includes('Script execution timed out')) {
|
|
136
|
-
return { error: `Execution timed out after ${timeoutMs}ms. Keep code efficient or increase timeout (max ${MAX_TIMEOUT}ms).`, logs, duration_ms: durationMs };
|
|
141
|
+
// Timeout is a hard interrupt thrown to the host, so it lands here.
|
|
142
|
+
if (message.includes('Script execution timed out') || message.includes('timed out')) {
|
|
143
|
+
return { error: `Execution timed out after ${timeoutMs}ms. Keep code efficient or increase timeout (max ${MAX_TIMEOUT}ms).`, logs: [], duration_ms: durationMs };
|
|
137
144
|
}
|
|
138
145
|
if (message.includes('Code generation from strings disallowed')) {
|
|
139
|
-
return { error: 'eval() and Function() constructor are blocked in the sandbox. Use direct code instead.', logs, duration_ms: durationMs };
|
|
146
|
+
return { error: 'eval() and Function() constructor are blocked in the sandbox. Use direct code instead.', logs: [], duration_ms: durationMs };
|
|
140
147
|
}
|
|
141
|
-
return { error: message, logs, duration_ms: durationMs };
|
|
148
|
+
return { error: message, logs: [], duration_ms: durationMs };
|
|
142
149
|
}
|
|
143
150
|
},
|
|
144
151
|
});
|
|
@@ -162,26 +169,20 @@ function codeSandboxSkill(api) {
|
|
|
162
169
|
return { error: 'expression parameter is required and must be a string' };
|
|
163
170
|
}
|
|
164
171
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
serialized = JSON.parse(JSON.stringify(result));
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
serialized = String(result);
|
|
177
|
-
}
|
|
178
|
-
return {
|
|
179
|
-
result: serialized,
|
|
180
|
-
type: result === null ? 'null' : Array.isArray(result) ? 'array' : typeof result,
|
|
181
|
-
};
|
|
172
|
+
// Evaluate as the returned value of the sandbox body (same isolated realm,
|
|
173
|
+
// in-sandbox serialization). The value comes back as parsed JSON.
|
|
174
|
+
const out = runInSandbox(`return (${expression});`, DEFAULT_TIMEOUT, undefined);
|
|
175
|
+
if (!out.ok)
|
|
176
|
+
return { error: out.error || 'evaluation failed' };
|
|
177
|
+
const r = out.result;
|
|
178
|
+
const type = r === null ? 'null' : Array.isArray(r) ? 'array' : typeof r;
|
|
179
|
+
return { result: r, type };
|
|
182
180
|
}
|
|
183
181
|
catch (err) {
|
|
184
|
-
|
|
182
|
+
const message = err.message || String(err);
|
|
183
|
+
if (message.includes('timed out'))
|
|
184
|
+
return { error: `Execution timed out after ${DEFAULT_TIMEOUT}ms.` };
|
|
185
|
+
return { error: message };
|
|
185
186
|
}
|
|
186
187
|
},
|
|
187
188
|
});
|
|
@@ -25,61 +25,79 @@ const MAX_TIMEOUT = 30000;
|
|
|
25
25
|
const MAX_OUTPUT_SIZE = 10 * 1024 * 1024; // 10MB
|
|
26
26
|
|
|
27
27
|
// ── Sandbox helpers ─────────────────────────────
|
|
28
|
+
//
|
|
29
|
+
// SECURITY: the context is given ONLY primitive strings — never a host function or
|
|
30
|
+
// object. That is the whole game with node:vm. `codeGeneration.strings:false` only
|
|
31
|
+
// disables eval/Function *for this context's own realm*; a host closure handed in
|
|
32
|
+
// (a console mock, atob/btoa via Buffer, anything) exposes `fn.constructor` — the
|
|
33
|
+
// HOST realm's Function, where code generation is still allowed — so
|
|
34
|
+
// `console.log.constructor("return process.env")()` would read the runner's secrets
|
|
35
|
+
// and `Object.getPrototypeOf(hostFn).constructor.prototype` would pollute the host.
|
|
36
|
+
// So: console, atob/btoa, INPUT parsing and the *result serialization* all run as
|
|
37
|
+
// sandbox-realm code (below). Serializing inside the sandbox also keeps it under the
|
|
38
|
+
// execution timeout — a malicious getter can no longer stall the host via a
|
|
39
|
+
// host-side JSON.stringify. The only value read back out is a JSON string (a
|
|
40
|
+
// primitive), which the host then parses safely.
|
|
41
|
+
|
|
42
|
+
/** Sandbox-realm preamble: pure-JS console/atob/btoa/INPUT, no host references. */
|
|
43
|
+
const SANDBOX_PREAMBLE = `
|
|
44
|
+
var __logs = [];
|
|
45
|
+
var console = {
|
|
46
|
+
log: function(){ __logs.push(Array.prototype.map.call(arguments, function(a){ return typeof a === 'object' ? JSON.stringify(a) : String(a); }).join(' ')); },
|
|
47
|
+
warn: function(){ __logs.push('[warn] ' + Array.prototype.map.call(arguments, function(a){ return typeof a === 'object' ? JSON.stringify(a) : String(a); }).join(' ')); },
|
|
48
|
+
error: function(){ __logs.push('[error] ' + Array.prototype.map.call(arguments, function(a){ return typeof a === 'object' ? JSON.stringify(a) : String(a); }).join(' ')); }
|
|
49
|
+
};
|
|
50
|
+
var __B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
51
|
+
function btoa(s){ s = String(s); var o = ''; for (var i = 0; i < s.length; ) {
|
|
52
|
+
var c1 = s.charCodeAt(i++), c2 = s.charCodeAt(i++), c3 = s.charCodeAt(i++);
|
|
53
|
+
var e1 = c1 >> 2, e2 = ((c1 & 3) << 4) | (c2 >> 4), e3 = ((c2 & 15) << 2) | (c3 >> 6), e4 = c3 & 63;
|
|
54
|
+
if (isNaN(c2)) { e3 = e4 = 64; } else if (isNaN(c3)) { e4 = 64; }
|
|
55
|
+
o += __B64.charAt(e1) + __B64.charAt(e2) + (e3 === 64 ? '=' : __B64.charAt(e3)) + (e4 === 64 ? '=' : __B64.charAt(e4));
|
|
56
|
+
} return o; }
|
|
57
|
+
function atob(s){ s = String(s).replace(/[^A-Za-z0-9+/=]/g, ''); var o = ''; for (var i = 0; i < s.length; ) {
|
|
58
|
+
var d1 = __B64.indexOf(s.charAt(i++)), d2 = __B64.indexOf(s.charAt(i++)), d3 = __B64.indexOf(s.charAt(i++)), d4 = __B64.indexOf(s.charAt(i++));
|
|
59
|
+
var c1 = (d1 << 2) | (d2 >> 4), c2 = ((d2 & 15) << 4) | (d3 >> 2), c3 = ((d3 & 3) << 6) | d4;
|
|
60
|
+
o += String.fromCharCode(c1); if (d3 !== 64 && d3 >= 0) o += String.fromCharCode(c2); if (d4 !== 64 && d4 >= 0) o += String.fromCharCode(c3);
|
|
61
|
+
} return o; }
|
|
62
|
+
`.trim();
|
|
28
63
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
process: undefined,
|
|
67
|
-
globalThis: undefined,
|
|
68
|
-
global: undefined,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function serializeResult(value: unknown): string {
|
|
73
|
-
if (value === undefined) return 'undefined';
|
|
74
|
-
try {
|
|
75
|
-
const str = JSON.stringify(value, null, 2);
|
|
76
|
-
if (str.length > MAX_OUTPUT_SIZE) {
|
|
77
|
-
return str.slice(0, MAX_OUTPUT_SIZE) + '\n... [truncated at 10MB]';
|
|
78
|
-
}
|
|
79
|
-
return str;
|
|
80
|
-
} catch {
|
|
81
|
-
return String(value);
|
|
82
|
-
}
|
|
64
|
+
/**
|
|
65
|
+
* Run a self-contained expression/body in a fresh vm realm and return the parsed
|
|
66
|
+
* outcome. `body` must be a statement list whose LAST expression is the user value
|
|
67
|
+
* to capture. Everything is serialized to a JSON string inside the sandbox.
|
|
68
|
+
*/
|
|
69
|
+
function runInSandbox(
|
|
70
|
+
body: string,
|
|
71
|
+
timeoutMs: number,
|
|
72
|
+
inputJson: string | undefined,
|
|
73
|
+
): { ok: boolean; result?: unknown; logs: string[]; error?: string; oversized?: boolean } {
|
|
74
|
+
// Only a primitive string crosses into the realm — AND the context global is
|
|
75
|
+
// given a NULL prototype. If we hand vm.createContext an ordinary host object,
|
|
76
|
+
// the sandbox global inherits the HOST realm's Object.prototype, so
|
|
77
|
+
// `this.constructor.constructor("return process.env")()` walks to the host
|
|
78
|
+
// Function (where codeGeneration is allowed) and reads the runner's secrets —
|
|
79
|
+
// codeGeneration:false only covers THIS context's realm. A null-proto global
|
|
80
|
+
// makes `this.constructor` resolve to the sandbox realm's own Function, which
|
|
81
|
+
// the flag then blocks. (Confirmed escape via the global-object path, 2026-09-19.)
|
|
82
|
+
const sandboxGlobal: Record<string, unknown> = Object.create(null);
|
|
83
|
+
sandboxGlobal.__INPUT_JSON = inputJson;
|
|
84
|
+
const context = vm.createContext(sandboxGlobal, {
|
|
85
|
+
codeGeneration: { strings: false, wasm: false },
|
|
86
|
+
});
|
|
87
|
+
const wrapped = `${SANDBOX_PREAMBLE}
|
|
88
|
+
var INPUT = (typeof __INPUT_JSON === 'string') ? JSON.parse(__INPUT_JSON) : undefined;
|
|
89
|
+
var __result, __error = null;
|
|
90
|
+
try { __result = (function(){ ${body} \n})(); } catch (e) { __error = (e && e.message) ? String(e.message) : String(e); }
|
|
91
|
+
(function(){
|
|
92
|
+
try { return JSON.stringify({ ok: __error === null, result: __result === undefined ? null : __result, logs: __logs, error: __error }); }
|
|
93
|
+
catch (e) { return JSON.stringify({ ok: __error === null, result: String(__result), logs: __logs, error: __error }); }
|
|
94
|
+
})();`;
|
|
95
|
+
const script = new vm.Script(wrapped, { filename: 'sandbox.js' });
|
|
96
|
+
const out = script.runInContext(context, { timeout: timeoutMs }) as string;
|
|
97
|
+
if (typeof out !== 'string') return { ok: false, logs: [], error: 'sandbox produced no serializable output' };
|
|
98
|
+
if (out.length > MAX_OUTPUT_SIZE) return { ok: false, logs: [], error: 'Output exceeded 10MB limit', oversized: true };
|
|
99
|
+
const parsed = JSON.parse(out) as { ok: boolean; result?: unknown; logs: string[]; error?: string };
|
|
100
|
+
return parsed;
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
// ── Skill entry point ───────────────────────────
|
|
@@ -113,53 +131,41 @@ export default function codeSandboxSkill(api: SkillApi): void {
|
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
const timeoutMs = Math.min(Math.max(timeout || DEFAULT_TIMEOUT, 100), MAX_TIMEOUT);
|
|
116
|
-
const logs: string[] = [];
|
|
117
134
|
const startTime = Date.now();
|
|
118
135
|
|
|
136
|
+
let inputJson: string | undefined;
|
|
119
137
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Wrap code so the last expression is returned
|
|
126
|
-
const wrapped = `(function() {\n${code}\n})()`;
|
|
127
|
-
const script = new vm.Script(wrapped, { filename: 'sandbox.js' });
|
|
128
|
-
const result = script.runInContext(context, { timeout: timeoutMs });
|
|
129
|
-
const durationMs = Date.now() - startTime;
|
|
138
|
+
inputJson = input === undefined ? undefined : JSON.stringify(input);
|
|
139
|
+
} catch {
|
|
140
|
+
return { error: 'input could not be serialized to JSON' };
|
|
141
|
+
}
|
|
130
142
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
duration_ms: durationMs,
|
|
137
|
-
warning: 'Output exceeded 10MB limit and was truncated',
|
|
138
|
-
};
|
|
143
|
+
try {
|
|
144
|
+
const out = runInSandbox(code, timeoutMs, inputJson);
|
|
145
|
+
const durationMs = Date.now() - startTime;
|
|
146
|
+
if (out.oversized) {
|
|
147
|
+
return { error: 'Output exceeded 10MB limit', logs: out.logs, duration_ms: durationMs, warning: 'Output exceeded 10MB limit and was truncated' };
|
|
139
148
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
parsed = serialized === 'undefined' ? undefined : serialized;
|
|
149
|
+
if (!out.ok) {
|
|
150
|
+
const message = out.error || 'unknown error';
|
|
151
|
+
if (message.includes('Code generation from strings disallowed')) {
|
|
152
|
+
return { error: 'eval() and Function() constructor are blocked in the sandbox. Use direct code instead.', logs: out.logs, duration_ms: durationMs };
|
|
153
|
+
}
|
|
154
|
+
return { error: message, logs: out.logs, duration_ms: durationMs };
|
|
147
155
|
}
|
|
148
|
-
|
|
149
|
-
return { result: parsed, logs, duration_ms: durationMs };
|
|
156
|
+
return { result: out.result, logs: out.logs, duration_ms: durationMs };
|
|
150
157
|
} catch (err: any) {
|
|
151
158
|
const durationMs = Date.now() - startTime;
|
|
152
159
|
const message = err.message || String(err);
|
|
153
160
|
|
|
154
|
-
//
|
|
155
|
-
if (message.includes('Script execution timed out')) {
|
|
156
|
-
return { error: `Execution timed out after ${timeoutMs}ms. Keep code efficient or increase timeout (max ${MAX_TIMEOUT}ms).`, logs, duration_ms: durationMs };
|
|
161
|
+
// Timeout is a hard interrupt thrown to the host, so it lands here.
|
|
162
|
+
if (message.includes('Script execution timed out') || message.includes('timed out')) {
|
|
163
|
+
return { error: `Execution timed out after ${timeoutMs}ms. Keep code efficient or increase timeout (max ${MAX_TIMEOUT}ms).`, logs: [], duration_ms: durationMs };
|
|
157
164
|
}
|
|
158
165
|
if (message.includes('Code generation from strings disallowed')) {
|
|
159
|
-
return { error: 'eval() and Function() constructor are blocked in the sandbox. Use direct code instead.', logs, duration_ms: durationMs };
|
|
166
|
+
return { error: 'eval() and Function() constructor are blocked in the sandbox. Use direct code instead.', logs: [], duration_ms: durationMs };
|
|
160
167
|
}
|
|
161
|
-
|
|
162
|
-
return { error: message, logs, duration_ms: durationMs };
|
|
168
|
+
return { error: message, logs: [], duration_ms: durationMs };
|
|
163
169
|
}
|
|
164
170
|
},
|
|
165
171
|
});
|
|
@@ -185,27 +191,17 @@ export default function codeSandboxSkill(api: SkillApi): void {
|
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
let serialized: unknown;
|
|
197
|
-
try {
|
|
198
|
-
serialized = JSON.parse(JSON.stringify(result));
|
|
199
|
-
} catch {
|
|
200
|
-
serialized = String(result);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
result: serialized,
|
|
205
|
-
type: result === null ? 'null' : Array.isArray(result) ? 'array' : typeof result,
|
|
206
|
-
};
|
|
194
|
+
// Evaluate as the returned value of the sandbox body (same isolated realm,
|
|
195
|
+
// in-sandbox serialization). The value comes back as parsed JSON.
|
|
196
|
+
const out = runInSandbox(`return (${expression});`, DEFAULT_TIMEOUT, undefined);
|
|
197
|
+
if (!out.ok) return { error: out.error || 'evaluation failed' };
|
|
198
|
+
const r = out.result;
|
|
199
|
+
const type = r === null ? 'null' : Array.isArray(r) ? 'array' : typeof r;
|
|
200
|
+
return { result: r, type };
|
|
207
201
|
} catch (err: any) {
|
|
208
|
-
|
|
202
|
+
const message = err.message || String(err);
|
|
203
|
+
if (message.includes('timed out')) return { error: `Execution timed out after ${DEFAULT_TIMEOUT}ms.` };
|
|
204
|
+
return { error: message };
|
|
209
205
|
}
|
|
210
206
|
},
|
|
211
207
|
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.getDeliverable = getDeliverable;
|
|
10
10
|
exports.default = creativeSkill;
|
|
11
|
+
const ssrf_1 = require("./ssrf");
|
|
11
12
|
// ── Shared helpers ──────────────────────────────
|
|
12
13
|
const MAX_DELIVERABLES = 200;
|
|
13
14
|
const deliverables = new Map();
|
|
@@ -121,7 +122,9 @@ async function downloadFromUrl(url) {
|
|
|
121
122
|
if (!/^https?:\/\//.test(url))
|
|
122
123
|
return null;
|
|
123
124
|
try {
|
|
124
|
-
|
|
125
|
+
// SSRF: the URL is agent/job-controlled and this runs inside a private network.
|
|
126
|
+
// guardedFetch refuses private/internal targets and re-validates each redirect.
|
|
127
|
+
const resp = await (0, ssrf_1.guardedFetch)(url, { signal: AbortSignal.timeout(30000) });
|
|
125
128
|
if (!resp.ok)
|
|
126
129
|
return null;
|
|
127
130
|
const contentType = resp.headers.get('content-type') || 'application/octet-stream';
|
|
@@ -156,7 +159,8 @@ function extractImages(text) {
|
|
|
156
159
|
}
|
|
157
160
|
async function downloadImage(url) {
|
|
158
161
|
try {
|
|
159
|
-
|
|
162
|
+
// SSRF: image URLs come from agent-authored markdown — guard + re-validate redirects.
|
|
163
|
+
const resp = await (0, ssrf_1.guardedFetch)(url, { signal: AbortSignal.timeout(15000) });
|
|
160
164
|
if (!resp.ok)
|
|
161
165
|
return null;
|
|
162
166
|
const ct = (resp.headers.get('content-type') || '').split(';')[0].trim();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isPrivateAddress = isPrivateAddress;
|
|
4
|
+
exports.assertPublicUrl = assertPublicUrl;
|
|
5
|
+
exports.guardedFetch = guardedFetch;
|
|
6
|
+
/**
|
|
7
|
+
* SSRF guard for skill fetches.
|
|
8
|
+
*
|
|
9
|
+
* These fetches are steered by agent/job-controlled URLs, and the runner sits
|
|
10
|
+
* inside a private network with cloud metadata and internal services reachable.
|
|
11
|
+
* Resolve the host and refuse any loopback / private / link-local / ULA / CGNAT /
|
|
12
|
+
* metadata address, and follow redirects MANUALLY so a public host that
|
|
13
|
+
* 3xx-redirects to an internal one is re-validated at every hop.
|
|
14
|
+
*/
|
|
15
|
+
const promises_1 = require("node:dns/promises");
|
|
16
|
+
const node_net_1 = require("node:net");
|
|
17
|
+
/** True for loopback, private, link-local, ULA, CGNAT, multicast and unparseable addresses. */
|
|
18
|
+
function isPrivateAddress(ip) {
|
|
19
|
+
const v = (0, node_net_1.isIP)(ip);
|
|
20
|
+
if (v === 4) {
|
|
21
|
+
const p = ip.split('.').map(Number);
|
|
22
|
+
if (p.length !== 4 || p.some(n => Number.isNaN(n)))
|
|
23
|
+
return true;
|
|
24
|
+
const [a, b, c] = p;
|
|
25
|
+
return (a === 0 || a === 10 || a === 127 ||
|
|
26
|
+
(a === 169 && b === 254) || // link-local + cloud metadata
|
|
27
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
28
|
+
(a === 192 && b === 168) ||
|
|
29
|
+
(a === 192 && b === 0 && c === 0) || // 192.0.0.0/24 (IETF protocol assignments)
|
|
30
|
+
(a === 198 && (b === 18 || b === 19)) || // 198.18.0.0/15 (benchmarking)
|
|
31
|
+
(a === 100 && b >= 64 && b <= 127) || // CGNAT
|
|
32
|
+
a >= 224 // multicast / reserved
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (v === 6) {
|
|
36
|
+
const s = ip.toLowerCase();
|
|
37
|
+
return (s === '::1' || s === '::' ||
|
|
38
|
+
s.startsWith('::ffff:') || // IPv4-mapped
|
|
39
|
+
s.startsWith('64:ff9b') || // NAT64 well-known prefix
|
|
40
|
+
/^fe[89a-f]/.test(s) || // link-local fe80::/10 + site-local fec0::/10
|
|
41
|
+
s.startsWith('fc') || s.startsWith('fd') // unique-local
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
/** Resolve a URL's host and throw unless every address it maps to is public http(s). */
|
|
47
|
+
async function assertPublicUrl(urlStr) {
|
|
48
|
+
let host;
|
|
49
|
+
try {
|
|
50
|
+
const u = new URL(urlStr);
|
|
51
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
52
|
+
throw new Error(`Blocked non-http(s) URL: ${u.protocol}`);
|
|
53
|
+
}
|
|
54
|
+
host = u.hostname;
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
throw new Error(`Blocked invalid URL: ${e.message}`);
|
|
58
|
+
}
|
|
59
|
+
if ((0, node_net_1.isIP)(host)) {
|
|
60
|
+
if (isPrivateAddress(host))
|
|
61
|
+
throw new Error(`Blocked private address: ${host}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let addrs;
|
|
65
|
+
try {
|
|
66
|
+
addrs = await (0, promises_1.lookup)(host, { all: true });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Error(`Blocked host that does not resolve: ${host}`);
|
|
70
|
+
}
|
|
71
|
+
if (addrs.length === 0 || addrs.some(a => isPrivateAddress(a.address))) {
|
|
72
|
+
throw new Error(`Blocked host resolving to a private address: ${host}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* fetch() with an SSRF guard on every hop. Redirects are followed manually (up to
|
|
77
|
+
* maxRedirects) so each Location is re-validated — a public host cannot 302 to an
|
|
78
|
+
* internal one. Throws if a hop targets a private address or the redirect chain is
|
|
79
|
+
* too long.
|
|
80
|
+
*/
|
|
81
|
+
async function guardedFetch(url, init = {}, maxRedirects = 3) {
|
|
82
|
+
let current = url;
|
|
83
|
+
for (let i = 0; i <= maxRedirects; i++) {
|
|
84
|
+
await assertPublicUrl(current);
|
|
85
|
+
const resp = await fetch(current, { ...init, redirect: 'manual' });
|
|
86
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
87
|
+
const loc = resp.headers.get('location');
|
|
88
|
+
if (!loc)
|
|
89
|
+
return resp;
|
|
90
|
+
current = new URL(loc, current).toString();
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
return resp;
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`Too many redirects (>${maxRedirects})`);
|
|
96
|
+
}
|