@causyne/agent 1.10.13 → 1.10.17
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/docs/ARCHITECTURE.md +101 -17
- package/docs/SECURITY_REVIEW.md +13 -1
- package/docs/THIRD_PARTY_NOTICES.md +43 -10
- package/package.json +10 -4
- package/src/agentLoop.js +25 -3
- package/src/agentProfiles.js +135 -0
- package/src/agentProfiles.ts +143 -0
- package/src/approvalGrants.js +17 -35
- package/src/approvalGrants.ts +54 -0
- package/src/auditLog.d.ts +3 -0
- package/src/browser/camofoxRuntime.js +93 -0
- package/src/browser/camofoxRuntime.ts +112 -0
- package/src/browser/causyneBrowser.js +495 -0
- package/src/browser/causyneBrowser.ts +594 -0
- package/src/browser/searchProviders.js +156 -0
- package/src/browser/searchProviders.ts +156 -0
- package/src/browser/snapshotWindow.js +28 -0
- package/src/browser/snapshotWindow.ts +40 -0
- package/src/browser/urlPolicy.js +89 -0
- package/src/browser/urlPolicy.ts +89 -0
- package/src/checkpoints.js +19 -6
- package/src/commands.js +37 -8
- package/src/config.d.ts +2 -0
- package/src/hooks.js +121 -107
- package/src/hooks.ts +193 -0
- package/src/mcp/mcpClient.js +520 -132
- package/src/mcp/mcpClient.ts +584 -0
- package/src/mcp/mcpRegistry.js +399 -185
- package/src/mcp/mcpRegistry.ts +451 -0
- package/src/planCache.js +473 -450
- package/src/planCache.ts +840 -0
- package/src/plugins/pluginManager.js +40 -5
- package/src/plugins/pluginSkills.js +84 -0
- package/src/plugins/pluginSkills.ts +106 -0
- package/src/provisioning/capabilityProvisioner.d.ts +10 -0
- package/src/provisioning/capabilityProvisioner.js +5 -0
- package/src/provisioning/capabilityTools.js +2 -1
- package/src/sandbox.js +32 -2
- package/src/sessionJournal.js +471 -0
- package/src/sessionJournal.ts +496 -0
- package/src/sessionStore.js +3 -279
- package/src/skillsManager.js +18 -0
- package/src/subagentJournal.js +215 -0
- package/src/subagentJournal.ts +254 -0
- package/src/taskShape.js +179 -198
- package/src/taskShape.ts +259 -0
- package/src/tools/browserAutomator.js +45 -241
- package/src/tools/browserAutomator.ts +57 -0
- package/src/tools/manageLocalAiDependencies.js +1 -1
- package/src/tools/subagentOrchestrator.js +73 -12
- package/src/tools/webFetch.js +183 -68
- package/src/tools/webFetch.ts +185 -0
- package/src/tools/webSearch.js +50 -75
- package/src/tools/webSearch.ts +56 -0
- package/src/types/contracts.js +8 -0
- package/src/types/contracts.ts +263 -0
- package/src/utils/paths.d.ts +3 -0
- package/src/version.d.ts +1 -0
- package/src/mcp/jsonRpc.js +0 -165
|
@@ -4,6 +4,7 @@ import crypto from 'node:crypto';
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { causyneHome } from '../utils/paths.js';
|
|
7
|
+
import { parsePluginSkills } from './pluginSkills.js';
|
|
7
8
|
|
|
8
9
|
const PLUGIN_ROOT = path.join(causyneHome(), 'plugins');
|
|
9
10
|
const STATE_FILE = path.join(PLUGIN_ROOT, 'state.json');
|
|
@@ -131,14 +132,15 @@ function readPlugin(directory) {
|
|
|
131
132
|
if (typeof manifest.description !== 'string' || !manifest.description.trim()) {
|
|
132
133
|
throw new Error('Plugin description is required');
|
|
133
134
|
}
|
|
134
|
-
|
|
135
|
-
if (
|
|
135
|
+
const manifestTools = manifest.tools ?? [];
|
|
136
|
+
if (!Array.isArray(manifestTools)) throw new Error('Plugin tools must be an array');
|
|
137
|
+
if (manifestTools.length > MAX_TOOLS_PER_PLUGIN) {
|
|
136
138
|
throw new Error(`Plugin exceeds ${MAX_TOOLS_PER_PLUGIN} tools`);
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
const pluginCapabilities = validateCapabilities(manifest.capabilities, manifest.name);
|
|
140
142
|
const names = new Set();
|
|
141
|
-
const tools =
|
|
143
|
+
const tools = manifestTools.map((tool) => {
|
|
142
144
|
if (!TOOL_NAME.test(tool?.name || '')) throw new Error(`Invalid tool name ${JSON.stringify(tool?.name)}`);
|
|
143
145
|
if (names.has(tool.name)) throw new Error(`Duplicate tool name ${tool.name}`);
|
|
144
146
|
names.add(tool.name);
|
|
@@ -164,6 +166,10 @@ function readPlugin(directory) {
|
|
|
164
166
|
: DEFAULT_TIMEOUT_MS
|
|
165
167
|
};
|
|
166
168
|
});
|
|
169
|
+
const skills = parsePluginSkills(directory, manifest.name, manifest.skills);
|
|
170
|
+
if (tools.length === 0 && skills.length === 0) {
|
|
171
|
+
throw new Error('Plugin must contribute at least one tool or skill');
|
|
172
|
+
}
|
|
167
173
|
|
|
168
174
|
return {
|
|
169
175
|
directory,
|
|
@@ -173,7 +179,8 @@ function readPlugin(directory) {
|
|
|
173
179
|
// changing plugin.json and would inherit stale trust.
|
|
174
180
|
digest: digestPluginDirectory(directory),
|
|
175
181
|
manifest: { ...manifest, capabilities: pluginCapabilities },
|
|
176
|
-
tools
|
|
182
|
+
tools,
|
|
183
|
+
skills
|
|
177
184
|
};
|
|
178
185
|
}
|
|
179
186
|
|
|
@@ -202,6 +209,7 @@ export function listPlugins() {
|
|
|
202
209
|
digest: plugin.digest,
|
|
203
210
|
capabilities: plugin.manifest.capabilities,
|
|
204
211
|
tools: plugin.tools.map((tool) => tool.name),
|
|
212
|
+
skills: plugin.skills.map((skill) => `${skill.domain}/${skill.id}`),
|
|
205
213
|
path: plugin.directory
|
|
206
214
|
});
|
|
207
215
|
} catch (error) {
|
|
@@ -218,7 +226,14 @@ export function enablePlugin(name) {
|
|
|
218
226
|
const state = readState();
|
|
219
227
|
state.plugins[name] = { enabled: true, digest: plugin.digest, enabledAt: Date.now() };
|
|
220
228
|
writeState(state);
|
|
221
|
-
return {
|
|
229
|
+
return {
|
|
230
|
+
ok: true,
|
|
231
|
+
name,
|
|
232
|
+
digest: plugin.digest,
|
|
233
|
+
capabilities: plugin.manifest.capabilities,
|
|
234
|
+
tools: plugin.tools.map((tool) => tool.name),
|
|
235
|
+
skills: plugin.skills.map((skill) => `${skill.domain}/${skill.id}`)
|
|
236
|
+
};
|
|
222
237
|
} catch (error) {
|
|
223
238
|
return { ok: false, error: error.message };
|
|
224
239
|
}
|
|
@@ -314,6 +329,26 @@ export function getPluginToolDefinitions() {
|
|
|
314
329
|
}));
|
|
315
330
|
}
|
|
316
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Reviewed, enabled plugins may contribute prompt-only skills. Discovery has
|
|
334
|
+
* already parsed these as inert UTF-8 data; no plugin module is imported.
|
|
335
|
+
*/
|
|
336
|
+
export function getPluginSkills() {
|
|
337
|
+
const state = readState();
|
|
338
|
+
const skills = [];
|
|
339
|
+
for (const directory of pluginDirectories()) {
|
|
340
|
+
try {
|
|
341
|
+
const plugin = readPlugin(directory);
|
|
342
|
+
const trust = safeObject(state.plugins[plugin.manifest.name]);
|
|
343
|
+
if (!trust.enabled || trust.digest !== plugin.digest) continue;
|
|
344
|
+
skills.push(...plugin.skills);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
// Invalid or modified plugins contribute nothing to the prompt.
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return skills;
|
|
350
|
+
}
|
|
351
|
+
|
|
317
352
|
function permissionArgs(tool, workspaceRoot) {
|
|
318
353
|
// Node resolves `.js` module type by walking to package.json. Grant that one
|
|
319
354
|
// metadata file explicitly; granting the repository root would let an
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const MAX_SKILLS_PER_PLUGIN = 32;
|
|
4
|
+
const MAX_SKILL_BYTES = 32 * 1024;
|
|
5
|
+
const SKILL_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
6
|
+
function resolveInside(root, relativePath) {
|
|
7
|
+
const resolvedRoot = path.resolve(root);
|
|
8
|
+
const resolved = path.resolve(root, relativePath);
|
|
9
|
+
if (resolved === resolvedRoot || !resolved.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
10
|
+
throw new Error(`Skill path escapes plugin root: ${relativePath}`);
|
|
11
|
+
}
|
|
12
|
+
return resolved;
|
|
13
|
+
}
|
|
14
|
+
function cleanLine(value, maximum) {
|
|
15
|
+
return String(value ?? '')
|
|
16
|
+
// eslint-disable-next-line no-control-regex
|
|
17
|
+
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
|
18
|
+
.replace(/\s+/g, ' ')
|
|
19
|
+
.trim()
|
|
20
|
+
.slice(0, maximum);
|
|
21
|
+
}
|
|
22
|
+
function readSkillFile(file) {
|
|
23
|
+
const stat = fs.lstatSync(file);
|
|
24
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
25
|
+
throw new Error(`${file} must be a regular file`);
|
|
26
|
+
if (stat.size > MAX_SKILL_BYTES)
|
|
27
|
+
throw new Error(`${file} exceeds ${MAX_SKILL_BYTES / 1024} KB`);
|
|
28
|
+
return fs.readFileSync(file, 'utf8')
|
|
29
|
+
// Keep newlines and tabs, but remove control bytes that can corrupt a prompt.
|
|
30
|
+
// eslint-disable-next-line no-control-regex
|
|
31
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate plugin-contributed skills without importing or evaluating plugin
|
|
35
|
+
* code. A skill becomes visible only after the caller verifies the plugin's
|
|
36
|
+
* whole-directory digest against the explicit enablement record.
|
|
37
|
+
*/
|
|
38
|
+
export function parsePluginSkills(directory, pluginName, value) {
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
return [];
|
|
41
|
+
if (!Array.isArray(value))
|
|
42
|
+
throw new Error('Plugin skills must be an array');
|
|
43
|
+
if (value.length > MAX_SKILLS_PER_PLUGIN) {
|
|
44
|
+
throw new Error(`Plugin exceeds ${MAX_SKILLS_PER_PLUGIN} skills`);
|
|
45
|
+
}
|
|
46
|
+
const domain = `plugin-${pluginName}`;
|
|
47
|
+
const ids = new Set();
|
|
48
|
+
return value.map((raw, index) => {
|
|
49
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
50
|
+
throw new Error(`Skill ${index + 1} must be an object`);
|
|
51
|
+
}
|
|
52
|
+
const input = raw;
|
|
53
|
+
const id = String(input.id ?? '');
|
|
54
|
+
if (!SKILL_ID.test(id))
|
|
55
|
+
throw new Error(`Invalid plugin skill id ${JSON.stringify(id)}`);
|
|
56
|
+
if (ids.has(id))
|
|
57
|
+
throw new Error(`Duplicate plugin skill id ${id}`);
|
|
58
|
+
ids.add(id);
|
|
59
|
+
const name = cleanLine(input.name ?? id, 120);
|
|
60
|
+
const description = cleanLine(input.description, 400);
|
|
61
|
+
if (!name)
|
|
62
|
+
throw new Error(`Plugin skill ${id} needs a name`);
|
|
63
|
+
if (!description)
|
|
64
|
+
throw new Error(`Plugin skill ${id} needs a description`);
|
|
65
|
+
if (typeof input.file !== 'string' || !input.file) {
|
|
66
|
+
throw new Error(`Plugin skill ${id} needs a file`);
|
|
67
|
+
}
|
|
68
|
+
const file = resolveInside(directory, input.file);
|
|
69
|
+
const content = readSkillFile(file);
|
|
70
|
+
if (!content.trim())
|
|
71
|
+
throw new Error(`Plugin skill ${id} is empty`);
|
|
72
|
+
return {
|
|
73
|
+
domain,
|
|
74
|
+
id,
|
|
75
|
+
name,
|
|
76
|
+
description,
|
|
77
|
+
content,
|
|
78
|
+
source: 'plugin',
|
|
79
|
+
plugin: pluginName,
|
|
80
|
+
path: file
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export { MAX_SKILLS_PER_PLUGIN, MAX_SKILL_BYTES };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const MAX_SKILLS_PER_PLUGIN = 32;
|
|
5
|
+
const MAX_SKILL_BYTES = 32 * 1024;
|
|
6
|
+
const SKILL_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
7
|
+
|
|
8
|
+
export interface PluginSkillManifestInput {
|
|
9
|
+
id?: unknown;
|
|
10
|
+
name?: unknown;
|
|
11
|
+
description?: unknown;
|
|
12
|
+
file?: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PluginSkill {
|
|
16
|
+
domain: string;
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
content: string;
|
|
21
|
+
source: 'plugin';
|
|
22
|
+
plugin: string;
|
|
23
|
+
path: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveInside(root: string, relativePath: string): string {
|
|
27
|
+
const resolvedRoot = path.resolve(root);
|
|
28
|
+
const resolved = path.resolve(root, relativePath);
|
|
29
|
+
if (resolved === resolvedRoot || !resolved.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
30
|
+
throw new Error(`Skill path escapes plugin root: ${relativePath}`);
|
|
31
|
+
}
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cleanLine(value: unknown, maximum: number): string {
|
|
36
|
+
return String(value ?? '')
|
|
37
|
+
// eslint-disable-next-line no-control-regex
|
|
38
|
+
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
|
39
|
+
.replace(/\s+/g, ' ')
|
|
40
|
+
.trim()
|
|
41
|
+
.slice(0, maximum);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readSkillFile(file: string): string {
|
|
45
|
+
const stat = fs.lstatSync(file);
|
|
46
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${file} must be a regular file`);
|
|
47
|
+
if (stat.size > MAX_SKILL_BYTES) throw new Error(`${file} exceeds ${MAX_SKILL_BYTES / 1024} KB`);
|
|
48
|
+
return fs.readFileSync(file, 'utf8')
|
|
49
|
+
// Keep newlines and tabs, but remove control bytes that can corrupt a prompt.
|
|
50
|
+
// eslint-disable-next-line no-control-regex
|
|
51
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate plugin-contributed skills without importing or evaluating plugin
|
|
56
|
+
* code. A skill becomes visible only after the caller verifies the plugin's
|
|
57
|
+
* whole-directory digest against the explicit enablement record.
|
|
58
|
+
*/
|
|
59
|
+
export function parsePluginSkills(
|
|
60
|
+
directory: string,
|
|
61
|
+
pluginName: string,
|
|
62
|
+
value: unknown
|
|
63
|
+
): PluginSkill[] {
|
|
64
|
+
if (value === undefined) return [];
|
|
65
|
+
if (!Array.isArray(value)) throw new Error('Plugin skills must be an array');
|
|
66
|
+
if (value.length > MAX_SKILLS_PER_PLUGIN) {
|
|
67
|
+
throw new Error(`Plugin exceeds ${MAX_SKILLS_PER_PLUGIN} skills`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const domain = `plugin-${pluginName}`;
|
|
71
|
+
const ids = new Set<string>();
|
|
72
|
+
return value.map((raw, index) => {
|
|
73
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
74
|
+
throw new Error(`Skill ${index + 1} must be an object`);
|
|
75
|
+
}
|
|
76
|
+
const input = raw as PluginSkillManifestInput;
|
|
77
|
+
const id = String(input.id ?? '');
|
|
78
|
+
if (!SKILL_ID.test(id)) throw new Error(`Invalid plugin skill id ${JSON.stringify(id)}`);
|
|
79
|
+
if (ids.has(id)) throw new Error(`Duplicate plugin skill id ${id}`);
|
|
80
|
+
ids.add(id);
|
|
81
|
+
|
|
82
|
+
const name = cleanLine(input.name ?? id, 120);
|
|
83
|
+
const description = cleanLine(input.description, 400);
|
|
84
|
+
if (!name) throw new Error(`Plugin skill ${id} needs a name`);
|
|
85
|
+
if (!description) throw new Error(`Plugin skill ${id} needs a description`);
|
|
86
|
+
if (typeof input.file !== 'string' || !input.file) {
|
|
87
|
+
throw new Error(`Plugin skill ${id} needs a file`);
|
|
88
|
+
}
|
|
89
|
+
const file = resolveInside(directory, input.file);
|
|
90
|
+
const content = readSkillFile(file);
|
|
91
|
+
if (!content.trim()) throw new Error(`Plugin skill ${id} is empty`);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
domain,
|
|
95
|
+
id,
|
|
96
|
+
name,
|
|
97
|
+
description,
|
|
98
|
+
content,
|
|
99
|
+
source: 'plugin' as const,
|
|
100
|
+
plugin: pluginName,
|
|
101
|
+
path: file
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { MAX_SKILLS_PER_PLUGIN, MAX_SKILL_BYTES };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CapabilityVerdict {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
group: string;
|
|
4
|
+
reason?: string;
|
|
5
|
+
requiresConsent?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function ensureCapability(group: string): Promise<CapabilityVerdict>;
|
|
9
|
+
export function loadCapabilityPackage(packageName: string): Promise<unknown>;
|
|
10
|
+
export function capabilityDirectory(): string;
|
|
@@ -15,6 +15,10 @@ const CAPABILITY_DIR = path.join(causyneHome(), 'capabilities');
|
|
|
15
15
|
// ("let it see the screen"), not once per npm package. Names stay in value
|
|
16
16
|
// position — see the D-5 note above.
|
|
17
17
|
const GROUP_PACKAGES = {
|
|
18
|
+
browser: [
|
|
19
|
+
['camoufox-js', '0.11.5'],
|
|
20
|
+
['playwright-core', '1.58.0']
|
|
21
|
+
],
|
|
18
22
|
vision: [
|
|
19
23
|
['puppeteer-core', '25.8.0'],
|
|
20
24
|
['screenshot-desktop', '1.15.6']
|
|
@@ -40,6 +44,7 @@ const GROUP_PACKAGES = {
|
|
|
40
44
|
|
|
41
45
|
// than listing package names.
|
|
42
46
|
export const GROUP_DESCRIPTIONS = {
|
|
47
|
+
browser: 'use an existing Camoufox installation for rendered navigation, extraction, and workspace-scoped downloads; Causyne never downloads the browser binary automatically',
|
|
43
48
|
vision: 'drive a browser and capture the screen, so the agent can look at what it built',
|
|
44
49
|
ocr: 'read text out of images and screenshots',
|
|
45
50
|
system: 'list running processes and read system load',
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
import { isGroupDeclined } from './capabilityConsent.js';
|
|
12
12
|
|
|
13
13
|
export const TOOLS_BY_GROUP = {
|
|
14
|
-
|
|
14
|
+
browser: ['browser_act'],
|
|
15
|
+
vision: ['screenshot', 'vision_inspect_ui', 'game_canvas_audit', 'game_web_runtime_inspector'],
|
|
15
16
|
ocr: [],
|
|
16
17
|
system: ['process_list'],
|
|
17
18
|
desktop: ['clipboard', 'desktop_open'],
|
package/src/sandbox.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import os from 'os';
|
|
3
|
+
import { isIP } from 'node:net';
|
|
3
4
|
import { loadConfig } from './config.js';
|
|
4
5
|
import { isDevEmulator } from './devMode.js';
|
|
5
6
|
|
|
@@ -276,6 +277,27 @@ export class Sandbox {
|
|
|
276
277
|
}
|
|
277
278
|
}
|
|
278
279
|
|
|
280
|
+
if (toolName === 'browser_act') {
|
|
281
|
+
const actions = Array.isArray(args.actions) ? args.actions : [];
|
|
282
|
+
let hasDownload = false;
|
|
283
|
+
for (const action of actions) {
|
|
284
|
+
if (action && action.url && this.isInternalUrl(action.url)) {
|
|
285
|
+
return { allowed: false, reason: 'Cannot navigate the agent browser to an internal or invalid URL', risk: 'forbidden' };
|
|
286
|
+
}
|
|
287
|
+
if (action && ['download', 'screenshot'].includes(action.type)) {
|
|
288
|
+
if (!action.outputFile || !this.isPathAllowed(path.resolve(this.workspacePaths[0], action.outputFile))) {
|
|
289
|
+
return { allowed: false, reason: 'Browser downloads must stay inside the selected workspace', risk: 'forbidden' };
|
|
290
|
+
}
|
|
291
|
+
hasDownload = true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (hasDownload) return { allowed: true, risk: 'extreme', reason: 'Writes browser content to the selected workspace' };
|
|
295
|
+
if (actions.some(action => ['click', 'clickIndex', 'type', 'typeIndex', 'selectOption', 'keyPress'].includes(action?.type))) {
|
|
296
|
+
return { allowed: true, risk: 'dangerous', reason: 'Interacts with a website through the dedicated agent browser' };
|
|
297
|
+
}
|
|
298
|
+
return { allowed: true, risk: 'safe' };
|
|
299
|
+
}
|
|
300
|
+
|
|
279
301
|
if (toolName === 'game_asset_fetch' && args.action === 'download_asset') {
|
|
280
302
|
if (!args.url || this.isInternalUrl(args.url)) {
|
|
281
303
|
return { allowed: false, reason: 'Cannot download from an internal or invalid URL', risk: 'forbidden' };
|
|
@@ -390,11 +412,19 @@ export class Sandbox {
|
|
|
390
412
|
isInternalUrl(urlString) {
|
|
391
413
|
try {
|
|
392
414
|
const url = new URL(urlString);
|
|
393
|
-
|
|
415
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return true;
|
|
416
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '');
|
|
417
|
+
if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local') || host.endsWith('.internal')) return true;
|
|
394
418
|
|
|
395
419
|
// Block localhost, private IPs, cloud metadata
|
|
396
|
-
const blockedRegex = /^(localhost|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|
|
|
420
|
+
const blockedRegex = /^(localhost|0\.\d+\.\d+\.\d+|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|169\.254\.\d+\.\d+|192\.168\.\d+\.\d+|192\.0\.(0|2)\.\d+|192\.88\.99\.\d+|198\.(18|19)\.\d+\.\d+|198\.51\.100\.\d+|203\.0\.113\.\d+|(22[4-9]|23\d|24\d|25[0-5])\.\d+\.\d+\.\d+)$/;
|
|
397
421
|
if (blockedRegex.test(host)) return true;
|
|
422
|
+
if (host === 'metadata.google.internal') return true;
|
|
423
|
+
if (isIP(host) === 6) {
|
|
424
|
+
const normalized = host.split('%')[0];
|
|
425
|
+
if (normalized === '::' || normalized === '::1' || /^(fc|fd|fe8|fe9|fea|feb|fec|fed|fee|fef|ff|2001:db8:|64:ff9b:)/.test(normalized)) return true;
|
|
426
|
+
if (normalized.startsWith('::ffff:')) return true;
|
|
427
|
+
}
|
|
398
428
|
|
|
399
429
|
return false;
|
|
400
430
|
} catch(e) {
|