@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
package/src/taskShape.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TaskPackScore,
|
|
3
|
+
TaskPhase,
|
|
4
|
+
TaskShape,
|
|
5
|
+
WorkspaceContext
|
|
6
|
+
} from './types/contracts.js';
|
|
7
|
+
|
|
8
|
+
interface SignalSet {
|
|
9
|
+
strong: string[];
|
|
10
|
+
weak: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ArchetypeDefinition {
|
|
14
|
+
label: string;
|
|
15
|
+
match: SignalSet;
|
|
16
|
+
phases: TaskPhase[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PACK_SIGNALS: Record<string, SignalSet> = {
|
|
20
|
+
game: {
|
|
21
|
+
strong: ['game', 'gamedev', 'permainan', 'godot', 'unity', 'unreal', 'rpg', 'platformer', 'roguelike', 'arcade', 'voxel', 'minecraft', 'tilemap', 'spritesheet', 'boss fight', 'level design'],
|
|
22
|
+
weak: ['sprite', 'player', 'enemy', 'quest', 'npc', 'gameplay', 'multiplayer', 'leaderboard']
|
|
23
|
+
},
|
|
24
|
+
web: {
|
|
25
|
+
strong: ['web app', 'webapp', 'website', 'situs', 'landing page', 'homepage', 'frontend', 'front-end', 'dashboard', 'portfolio', 'react', 'vue', 'svelte', 'angular', 'nextjs', 'next.js', 'nuxt', 'astro', 'tailwind', 'html', 'css'],
|
|
26
|
+
weak: ['web', 'ui', 'ux', 'page', 'halaman', 'responsive', 'browser', 'dom', 'component', 'layout', 'design', 'desain', 'tampilan']
|
|
27
|
+
},
|
|
28
|
+
media: {
|
|
29
|
+
strong: ['generate image', 'generate video', 'generate music', 'text to speech', 'voiceover', 'blender', 'davinci', 'resolve', 'render', '3d model', 'animation', 'animasi'],
|
|
30
|
+
weak: ['image', 'gambar', 'photo', 'foto', 'video', 'audio', 'sound', 'suara', 'music', 'musik', 'lagu', 'thumbnail', 'poster', 'logo', 'illustration']
|
|
31
|
+
},
|
|
32
|
+
system: {
|
|
33
|
+
strong: ['kill process', 'port', 'zip', 'unzip', 'tar', 'archive', 'clipboard', 'printer', 'print', 'cetak'],
|
|
34
|
+
weak: ['process', 'proses', 'compress', 'extract', 'ekstrak', 'open app', 'launch']
|
|
35
|
+
},
|
|
36
|
+
advanced: {
|
|
37
|
+
strong: ['pdf', 'forge a tool', 'custom tool', 'plugin', 'plugins', 'custom plugin', 'subagent', 'sub-agent', 'parallel agent'],
|
|
38
|
+
weak: ['symbols', 'ast', 'refactor', 'orchestrate', 'swarm']
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const STRONG_WEIGHT = 3;
|
|
43
|
+
const WEAK_WEIGHT = 1;
|
|
44
|
+
const PACK_THRESHOLD = 3;
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Task archetypes. Each one carries the phase plan the agent should follow and
|
|
48
|
+
// the tools that matter in each phase.
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
const ARCHETYPES: Record<string, ArchetypeDefinition> = {
|
|
52
|
+
integrate: {
|
|
53
|
+
label: 'Build an application that integrates live external data',
|
|
54
|
+
match: {
|
|
55
|
+
strong: ['realtime', 'real-time', 'real time', 'api', 'rest', 'webhook', 'integration', 'integrasi', 'sync', 'sinkron', 'automation', 'automasi', 'otomasi', 'automate', 'otomatis', 'booking', 'tiket', 'ticket', 'calendar', 'kalender', 'jadwal', 'schedule', 'payment', 'pembayaran'],
|
|
56
|
+
weak: ['live', 'stream', 'fetch', 'data', 'third party', 'oauth', 'token']
|
|
57
|
+
},
|
|
58
|
+
phases: [
|
|
59
|
+
{ name: 'Research the data sources FIRST', tools: ['web_search', 'web_fetch'], note: 'Identify the exact official REST APIs, their auth model, rate limits and response shape before writing any code. Prefer documented JSON APIs over browser scraping — reading JSON always beats reading DOM.' },
|
|
60
|
+
{ name: 'Verify access and versions', tools: ['sysRun', 'web_fetch'], note: 'Probe one real endpoint and check current package versions with `npm view <pkg> version` before pinning anything. Never assume a version from memory.' },
|
|
61
|
+
{ name: 'Scaffold', tools: ['fs_write', 'sysRun'], note: 'Create the project structure and dependency manifest.' },
|
|
62
|
+
{ name: 'Implement', tools: ['fs_read', 'fs_edit', 'MultiEdit'], note: 'Build the integration layer first, the UI second. Handle auth failures and rate limits explicitly.' },
|
|
63
|
+
{ name: 'Verify', tools: ['code_verify_syntax', 'sysRun'], note: 'Run the build, lint and type-check. Exercise the live data path end to end, not just the happy path.' }
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
build_app: {
|
|
67
|
+
label: 'Build a new application from scratch',
|
|
68
|
+
match: {
|
|
69
|
+
strong: ['build me', 'buatkan', 'buat aku', 'create a', 'make me', 'bikin', 'develop a', 'new app', 'from scratch'],
|
|
70
|
+
weak: ['app', 'aplikasi', 'project', 'proyek', 'tool', 'system', 'sistem']
|
|
71
|
+
},
|
|
72
|
+
phases: [
|
|
73
|
+
{ name: 'Plan', tools: ['TodoWrite'], note: 'Break the build into atomic, verifiable steps before touching the filesystem.' },
|
|
74
|
+
{ name: 'Scaffold', tools: ['fs_write', 'sysRun'], note: 'Create structure and install dependencies at verified current versions.' },
|
|
75
|
+
{ name: 'Implement', tools: ['fs_write', 'fs_edit', 'MultiEdit'], note: 'Write runnable code with every import and dependency present.' },
|
|
76
|
+
{ name: 'Verify', tools: ['code_verify_syntax', 'sysRun'], note: 'Build, lint and run it. Never assume code works.' }
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
debug: {
|
|
80
|
+
label: 'Diagnose and fix a defect in existing code',
|
|
81
|
+
match: {
|
|
82
|
+
strong: ['bug', 'error', 'crash', 'fix', 'perbaiki', 'benerin', 'broken', 'rusak', 'not working', 'gagal', 'failing', 'exception', 'stack trace', 'regression'],
|
|
83
|
+
weak: ['issue', 'masalah', 'problem', 'wrong', 'salah', 'debug']
|
|
84
|
+
},
|
|
85
|
+
phases: [
|
|
86
|
+
{ name: 'Reproduce', tools: ['sysRun', 'fs_read'], note: 'Reproduce the failure and capture the exact error before changing anything.' },
|
|
87
|
+
{ name: 'Locate', tools: ['code_search_ripgrep', 'codebase_search', 'fs_read'], note: 'Search several phrasings in parallel. Read the file before patching it.' },
|
|
88
|
+
{ name: 'Fix', tools: ['fs_edit', 'MultiEdit'], note: 'Make the smallest correct change. Do not touch unrelated code.' },
|
|
89
|
+
{ name: 'Verify', tools: ['sysRun', 'code_verify_syntax'], note: 'Confirm the original reproduction now passes.' }
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
research: {
|
|
93
|
+
label: 'Investigate and report, without changing the system',
|
|
94
|
+
match: {
|
|
95
|
+
strong: ['explain', 'jelaskan', 'analyse', 'analyze', 'analisa', 'research', 'riset', 'compare', 'bandingkan', 'what is', 'apa itu', 'how does', 'bagaimana', 'review', 'audit'],
|
|
96
|
+
weak: ['understand', 'pahami', 'find out', 'cari tahu', 'summarize', 'ringkas']
|
|
97
|
+
},
|
|
98
|
+
phases: [
|
|
99
|
+
{ name: 'Gather', tools: ['code_search_ripgrep', 'fs_read', 'web_search', 'web_fetch'], note: 'Search broadly with several phrasings in parallel before concluding.' },
|
|
100
|
+
{ name: 'Report', tools: [], note: 'Answer concisely and cite exact file paths or URLs. Do not modify anything unless asked.' }
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
maintain: {
|
|
104
|
+
label: 'Operate on files, processes or the machine itself',
|
|
105
|
+
match: {
|
|
106
|
+
strong: ['organize', 'rapikan', 'clean up', 'bersihkan', 'rename', 'ganti nama', 'move', 'pindahkan', 'backup', 'cadangkan', 'delete', 'hapus', 'install', 'deploy', 'commit', 'merge'],
|
|
107
|
+
weak: ['folder', 'directory', 'file', 'berkas', 'desktop', 'downloads']
|
|
108
|
+
},
|
|
109
|
+
phases: [
|
|
110
|
+
{ name: 'Survey', tools: ['fs_list', 'fs_find'], note: 'List and count what you are about to touch, and report it, before acting.' },
|
|
111
|
+
{ name: 'Act', tools: ['fs_move', 'fs_write', 'sysRun'], note: 'Prefer reversible operations. Flag anything destructive for approval.' },
|
|
112
|
+
{ name: 'Confirm', tools: ['fs_list'], note: 'Verify the end state matches what the user asked for.' }
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const ARCHETYPE_NAMES = Object.keys(ARCHETYPES);
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Scoring
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
function scoreSignals(text: string, signals: SignalSet): { score: number; hits: string[] } {
|
|
124
|
+
let score = 0;
|
|
125
|
+
const hits: string[] = [];
|
|
126
|
+
|
|
127
|
+
for (const term of signals.strong || []) {
|
|
128
|
+
if (text.includes(term)) {
|
|
129
|
+
score += STRONG_WEIGHT;
|
|
130
|
+
hits.push(term);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const term of signals.weak || []) {
|
|
134
|
+
// Word-boundary check for short weak terms so 'ui' cannot match 'build'
|
|
135
|
+
// and 'race' cannot match 'trace' — the exact class of false positive that
|
|
136
|
+
// used to drag ordinary tasks into game and web enforcement.
|
|
137
|
+
const pattern = new RegExp(`(^|[^a-z0-9])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}($|[^a-z0-9])`, 'i');
|
|
138
|
+
if (pattern.test(text)) {
|
|
139
|
+
score += WEAK_WEIGHT;
|
|
140
|
+
hits.push(term);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { score, hits };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Analyse a request into the shape of the work it implies.
|
|
149
|
+
*
|
|
150
|
+
*/
|
|
151
|
+
export function analyzeTask(prompt: string, workspaceContext: WorkspaceContext | null = null): TaskShape {
|
|
152
|
+
const text = String(prompt || '').toLowerCase();
|
|
153
|
+
|
|
154
|
+
// Which capability packs are worth their token cost for this request.
|
|
155
|
+
const packScores: TaskPackScore[] = [];
|
|
156
|
+
for (const packName of Object.keys(PACK_SIGNALS)) {
|
|
157
|
+
const signals = PACK_SIGNALS[packName];
|
|
158
|
+
if (!signals) continue;
|
|
159
|
+
const { score } = scoreSignals(text, signals);
|
|
160
|
+
if (score >= PACK_THRESHOLD) {
|
|
161
|
+
packScores.push({ name: packName, score });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Workspace evidence outranks keywords: a Godot project on disk is far more
|
|
166
|
+
// reliable than any phrasing in the prompt.
|
|
167
|
+
if (workspaceContext) {
|
|
168
|
+
const signals = [
|
|
169
|
+
workspaceContext.framework,
|
|
170
|
+
workspaceContext.projectType,
|
|
171
|
+
workspaceContext.designSystem,
|
|
172
|
+
Array.isArray(workspaceContext.keyFiles) ? workspaceContext.keyFiles.join(' ') : ''
|
|
173
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
174
|
+
|
|
175
|
+
const bump = (name: string, amount: number): void => {
|
|
176
|
+
const existing = packScores.find(p => p.name === name);
|
|
177
|
+
if (existing) existing.score += amount;
|
|
178
|
+
else packScores.push({ name, score: amount });
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (/godot|project\.godot|unity|\.tscn/.test(signals)) bump('game', 5);
|
|
182
|
+
if (/react|vue|svelte|next|nuxt|astro|angular|tailwind|vite|index\.html/.test(signals)) bump('web', 4);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
packScores.sort((a, b) => b.score - a.score);
|
|
186
|
+
|
|
187
|
+
// Which archetype the work follows.
|
|
188
|
+
let bestArchetype: string | null = null;
|
|
189
|
+
let bestScore = 0;
|
|
190
|
+
for (const name of ARCHETYPE_NAMES) {
|
|
191
|
+
const definition = ARCHETYPES[name];
|
|
192
|
+
if (!definition) continue;
|
|
193
|
+
const { score } = scoreSignals(text, definition.match);
|
|
194
|
+
if (score > bestScore) {
|
|
195
|
+
bestScore = score;
|
|
196
|
+
bestArchetype = name;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// A build request that also talks about live external data is an integration
|
|
201
|
+
// job first and a build job second — that ordering is what stops the agent
|
|
202
|
+
// scaffolding a UI before it knows whether the API even exists.
|
|
203
|
+
const integrate = ARCHETYPES.integrate;
|
|
204
|
+
if (!integrate) throw new Error('integrate task archetype is unavailable');
|
|
205
|
+
const integrationScore = scoreSignals(text, integrate.match).score;
|
|
206
|
+
if (integrationScore >= STRONG_WEIGHT && bestArchetype === 'build_app') {
|
|
207
|
+
bestArchetype = 'integrate';
|
|
208
|
+
bestScore = integrationScore;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const archetype = bestArchetype || 'build_app';
|
|
212
|
+
const confidence = bestScore >= 6 ? 'high' : (bestScore >= 3 ? 'medium' : 'low');
|
|
213
|
+
|
|
214
|
+
const definition = ARCHETYPES[archetype];
|
|
215
|
+
if (!definition) throw new Error(`task archetype ${archetype} is unavailable`);
|
|
216
|
+
return {
|
|
217
|
+
archetype,
|
|
218
|
+
archetypeLabel: definition.label,
|
|
219
|
+
packs: packScores,
|
|
220
|
+
phases: definition.phases,
|
|
221
|
+
confidence
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Render the analysis as a compact brief for the system prompt.
|
|
227
|
+
*
|
|
228
|
+
* This is the piece that turns "here are 28 tools" into "here is the order to
|
|
229
|
+
* use them in". It costs roughly 150-200 tokens and routinely saves whole
|
|
230
|
+
* exploratory steps, each of which costs thousands.
|
|
231
|
+
*/
|
|
232
|
+
export function buildTaskBrief(
|
|
233
|
+
shape: TaskShape | null | undefined,
|
|
234
|
+
availableToolNames: readonly string[] | null | undefined
|
|
235
|
+
): string {
|
|
236
|
+
if (!shape) return '';
|
|
237
|
+
|
|
238
|
+
const available = new Set(availableToolNames || []);
|
|
239
|
+
const lines: string[] = [];
|
|
240
|
+
|
|
241
|
+
lines.push('# TASK SHAPE (analysed from your request)');
|
|
242
|
+
lines.push(`Work type: ${shape.archetypeLabel}.`);
|
|
243
|
+
lines.push('Follow this sequence. Do not skip ahead — each phase de-risks the next.');
|
|
244
|
+
|
|
245
|
+
shape.phases.forEach((phase, index) => {
|
|
246
|
+
const tools = (phase.tools || []).filter(name => available.has(name));
|
|
247
|
+
const toolList = tools.length > 0 ? ` [${tools.join(', ')}]` : '';
|
|
248
|
+
lines.push(`${index + 1}. ${phase.name}${toolList} — ${phase.note}`);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
lines.push('Tools outside this list remain fully available; search for them with tool_search when a phase genuinely needs one.');
|
|
252
|
+
|
|
253
|
+
return lines.join('\n');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Pack names above threshold, highest score first. */
|
|
257
|
+
export function packsFromShape(shape: TaskShape | null | undefined): string[] {
|
|
258
|
+
return shape && Array.isArray(shape.packs) ? shape.packs.map(p => p.name) : [];
|
|
259
|
+
}
|
|
@@ -1,252 +1,56 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { ensureCapability, loadCapabilityPackage } from '../provisioning/capabilityProvisioner.js';
|
|
5
|
-
import { causyneHome } from '../utils/paths.js';
|
|
6
|
-
|
|
7
|
-
function getChromeExecutablePath() {
|
|
8
|
-
const platform = os.platform();
|
|
9
|
-
if (platform === 'win32') {
|
|
10
|
-
return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
|
|
11
|
-
} else if (platform === 'darwin') {
|
|
12
|
-
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
13
|
-
} else {
|
|
14
|
-
return '/usr/bin/google-chrome';
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getChromeUserDataDir() {
|
|
19
|
-
const platform = os.platform();
|
|
20
|
-
const home = os.homedir();
|
|
21
|
-
if (platform === 'win32') {
|
|
22
|
-
return `${home}\\AppData\\Local\\Google\\Chrome\\User Data`;
|
|
23
|
-
} else if (platform === 'darwin') {
|
|
24
|
-
return `${home}/Library/Application Support/Google/Chrome`;
|
|
25
|
-
} else {
|
|
26
|
-
return `${home}/.config/google-chrome`;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let activeBrowser = null;
|
|
31
|
-
let activePage = null;
|
|
32
|
-
|
|
1
|
+
import { BrowserCapabilityError, causyneBrowser } from '../browser/causyneBrowser.js';
|
|
33
2
|
export const browserAct = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
type: 'object',
|
|
39
|
-
properties: {
|
|
40
|
-
usePersonalProfile: {
|
|
41
|
-
type: 'boolean',
|
|
42
|
-
description: 'Set to true ONLY if you explicitly want to use your personal Google Chrome profile. Defaults to false (uses safe dedicated profile ~/.causyne/chrome_profile).'
|
|
43
|
-
},
|
|
44
|
-
profileDirectory: {
|
|
45
|
-
type: 'string',
|
|
46
|
-
description: 'Specific profile sub-folder when usePersonalProfile is true (e.g. "Profile 1", "Default", "Profile 2").'
|
|
47
|
-
},
|
|
48
|
-
actions: {
|
|
49
|
-
type: 'array',
|
|
50
|
-
description: 'List of actions to perform in order',
|
|
51
|
-
items: {
|
|
3
|
+
schema: {
|
|
4
|
+
name: 'browser.act',
|
|
5
|
+
description: 'Use the dedicated Causyne Camoufox browser for resilient search, navigation, accessible snapshots, interaction, extraction, and workspace-scoped downloads. Take a snapshot before interacting by ref.',
|
|
6
|
+
parameters: {
|
|
52
7
|
type: 'object',
|
|
53
8
|
properties: {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
9
|
+
actions: {
|
|
10
|
+
type: 'array',
|
|
11
|
+
minItems: 1,
|
|
12
|
+
maxItems: 40,
|
|
13
|
+
items: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
type: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
enum: ['newTab', 'switchTab', 'listTabs', 'closeTab', 'search', 'goto', 'snapshot', 'scrape', 'click', 'clickIndex', 'type', 'typeIndex', 'selectOption', 'keyPress', 'scroll', 'back', 'forward', 'reload', 'extract', 'download', 'screenshot']
|
|
19
|
+
},
|
|
20
|
+
tabId: { type: 'string', description: 'Tab id returned by newTab or listTabs.' },
|
|
21
|
+
url: { type: 'string', description: 'Public http(s) URL for goto or newTab.' },
|
|
22
|
+
query: { type: 'string', description: 'Search query.' },
|
|
23
|
+
ref: { type: 'string', description: 'Current accessibility ref, for example e12.' },
|
|
24
|
+
index: { type: 'number', description: 'Legacy numeric form of an accessibility ref.' },
|
|
25
|
+
selector: { type: 'string', description: 'CSS selector when a current accessibility ref is unavailable.' },
|
|
26
|
+
text: { type: 'string' },
|
|
27
|
+
value: { type: 'string' },
|
|
28
|
+
key: { type: 'string' },
|
|
29
|
+
direction: { type: 'string', enum: ['up', 'down', 'left', 'right'] },
|
|
30
|
+
amount: { type: 'number' },
|
|
31
|
+
offset: { type: 'number', description: 'Character offset for a paged accessibility snapshot.' },
|
|
32
|
+
outputFile: { type: 'string', description: 'Download or screenshot destination inside the selected workspace.' },
|
|
33
|
+
fullPage: { type: 'boolean', description: 'Capture the complete page for a screenshot instead of the visible viewport.' }
|
|
34
|
+
},
|
|
35
|
+
required: ['type'],
|
|
36
|
+
additionalProperties: false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
63
39
|
},
|
|
64
|
-
required: ['
|
|
65
|
-
|
|
40
|
+
required: ['actions'],
|
|
41
|
+
additionalProperties: false
|
|
66
42
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
async execute(args) {
|
|
72
|
-
const vision = await ensureCapability('vision');
|
|
73
|
-
if (!vision.ok) return { success: false, error: vision.reason, requiresConsent: vision.requiresConsent };
|
|
74
|
-
const puppeteer = await loadCapabilityPackage('puppeteer-core');
|
|
75
|
-
|
|
76
|
-
let results = [];
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
if (!activeBrowser || !activeBrowser.isConnected()) {
|
|
80
|
-
const executablePath = getChromeExecutablePath();
|
|
81
|
-
const primaryUserDataDir = getChromeUserDataDir();
|
|
82
|
-
let connectedOrLaunched = false;
|
|
83
|
-
|
|
84
|
-
// Layer 1: Attempt connection to active Chrome Remote Debugging port (9222) for zero-interruption live session reuse
|
|
43
|
+
},
|
|
44
|
+
async execute(args) {
|
|
85
45
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const resp = await fetch('http://127.0.0.1:9222/json/version', { signal: controller.signal });
|
|
89
|
-
clearTimeout(timeoutId);
|
|
90
|
-
if (resp.ok) {
|
|
91
|
-
const data = await resp.json();
|
|
92
|
-
if (data.webSocketDebuggerUrl) {
|
|
93
|
-
activeBrowser = await puppeteer.connect({
|
|
94
|
-
browserWSEndpoint: data.webSocketDebuggerUrl,
|
|
95
|
-
defaultViewport: null
|
|
96
|
-
});
|
|
97
|
-
connectedOrLaunched = true;
|
|
98
|
-
results.push('[Sovereign Browser] Attached directly to active Chrome session via port 9222 (Layer 1).');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
} catch (debugErr) {
|
|
102
|
-
// Port 9222 not active, proceed to Layer 2 / Layer 3 decision
|
|
46
|
+
const results = await causyneBrowser().run(args.actions ?? []);
|
|
47
|
+
return { success: true, output: JSON.stringify(results, null, 2), data: results };
|
|
103
48
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const fallbackDir = path.join(causyneHome(), 'chrome_profile');
|
|
108
|
-
if (!fs.existsSync(fallbackDir)) fs.mkdirSync(fallbackDir, { recursive: true });
|
|
109
|
-
|
|
110
|
-
if (!args.usePersonalProfile) {
|
|
111
|
-
// Layer 2: Sovereign Dedicated Agent Profile (100% Safe - Never risks logging out personal Chrome accounts)
|
|
112
|
-
activeBrowser = await puppeteer.launch({
|
|
113
|
-
executablePath,
|
|
114
|
-
userDataDir: fallbackDir,
|
|
115
|
-
headless: false,
|
|
116
|
-
defaultViewport: null,
|
|
117
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
118
|
-
});
|
|
119
|
-
connectedOrLaunched = true;
|
|
120
|
-
results.push('[Sovereign Browser] Launched Chrome using Sovereign Dedicated Profile (~/.causyne/chrome_profile). Your personal Google Chrome accounts remain 100% untouched and protected.');
|
|
121
|
-
} else {
|
|
122
|
-
// Layer 3: Explicit Personal Profile Guard (Strictly when usePersonalProfile: true is requested)
|
|
123
|
-
try {
|
|
124
|
-
const profileArgs = ['--no-sandbox', '--disable-setuid-sandbox'];
|
|
125
|
-
if (args.profileDirectory) {
|
|
126
|
-
profileArgs.push(`--profile-directory=${args.profileDirectory}`);
|
|
127
|
-
}
|
|
128
|
-
activeBrowser = await puppeteer.launch({
|
|
129
|
-
executablePath,
|
|
130
|
-
userDataDir: primaryUserDataDir,
|
|
131
|
-
headless: false,
|
|
132
|
-
defaultViewport: null,
|
|
133
|
-
args: profileArgs
|
|
134
|
-
});
|
|
135
|
-
connectedOrLaunched = true;
|
|
136
|
-
results.push(`[Sovereign Browser] Launched primary profile (${args.profileDirectory || 'Default'}) explicitly requested via usePersonalProfile.`);
|
|
137
|
-
} catch (launchErr) {
|
|
138
|
-
const isProfileLock = launchErr.message.includes('already running') ||
|
|
139
|
-
launchErr.message.includes('SingletonLock') ||
|
|
140
|
-
launchErr.message.includes('EBUSY') ||
|
|
141
|
-
launchErr.message.includes('lock') ||
|
|
142
|
-
launchErr.message.includes('user data directory is already in use');
|
|
143
|
-
if (isProfileLock) {
|
|
144
|
-
// Safe fallback to dedicated profile so task never fails and never corrupts personal cookies
|
|
145
|
-
results.push(`[Sovereign Profile Guard] Your personal Chrome profile (${args.profileDirectory || 'Default'}) is currently running and locked without remote debugging port 9222. Automatically switched to Sovereign Dedicated Profile (~/.causyne/chrome_profile) to protect your personal account session from logout.`);
|
|
146
|
-
activeBrowser = await puppeteer.launch({
|
|
147
|
-
executablePath,
|
|
148
|
-
userDataDir: fallbackDir,
|
|
149
|
-
headless: false,
|
|
150
|
-
defaultViewport: null,
|
|
151
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
152
|
-
});
|
|
153
|
-
connectedOrLaunched = true;
|
|
154
|
-
} else {
|
|
155
|
-
throw launchErr;
|
|
156
|
-
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error instanceof BrowserCapabilityError) {
|
|
51
|
+
return { success: false, error: error.message, requiresConsent: error.requiresConsent };
|
|
157
52
|
}
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const pages = await activeBrowser.pages();
|
|
162
|
-
activePage = pages[0] || (await activeBrowser.newPage());
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
for (const action of args.actions) {
|
|
166
|
-
if (action.type === 'goto') {
|
|
167
|
-
await activePage.goto(action.url, { waitUntil: 'networkidle2' });
|
|
168
|
-
results.push(`Navigated to ${action.url}`);
|
|
169
|
-
}
|
|
170
|
-
else if (action.type === 'click') {
|
|
171
|
-
await activePage.waitForSelector(action.selector, { timeout: action.timeoutMs || 5000 });
|
|
172
|
-
await activePage.click(action.selector);
|
|
173
|
-
results.push(`Clicked ${action.selector}`);
|
|
174
|
-
}
|
|
175
|
-
else if (action.type === 'type') {
|
|
176
|
-
await activePage.waitForSelector(action.selector, { timeout: action.timeoutMs || 5000 });
|
|
177
|
-
await activePage.type(action.selector, action.text, { delay: action.humanDelayMs || 45 });
|
|
178
|
-
results.push(`Typed text into ${action.selector} with human keystroke delay`);
|
|
179
|
-
}
|
|
180
|
-
// Phase 1 AGI Upgrade: DOM-to-Markdown Interaction
|
|
181
|
-
else if (action.type === 'clickIndex') {
|
|
182
|
-
const selector = `[data-causyne-id="${action.index}"]`;
|
|
183
|
-
await activePage.waitForSelector(selector, { timeout: action.timeoutMs || 5000 });
|
|
184
|
-
await activePage.click(selector);
|
|
185
|
-
results.push(`Clicked element mapped to index [${action.index}]`);
|
|
186
|
-
}
|
|
187
|
-
else if (action.type === 'typeIndex') {
|
|
188
|
-
const selector = `[data-causyne-id="${action.index}"]`;
|
|
189
|
-
await activePage.waitForSelector(selector, { timeout: action.timeoutMs || 5000 });
|
|
190
|
-
await activePage.type(selector, action.text, { delay: action.humanDelayMs || 45 });
|
|
191
|
-
results.push(`Typed text into element mapped to index [${action.index}] with human keystroke delay`);
|
|
192
|
-
}
|
|
193
|
-
else if (action.type === 'selectOption') {
|
|
194
|
-
if (action.index !== undefined) {
|
|
195
|
-
const selector = `[data-causyne-id="${action.index}"]`;
|
|
196
|
-
await activePage.waitForSelector(selector, { timeout: action.timeoutMs || 5000 });
|
|
197
|
-
await activePage.select(selector, action.value || action.text);
|
|
198
|
-
} else if (action.selector) {
|
|
199
|
-
await activePage.waitForSelector(action.selector, { timeout: action.timeoutMs || 5000 });
|
|
200
|
-
await activePage.select(action.selector, action.value || action.text);
|
|
201
|
-
}
|
|
202
|
-
results.push(`Selected option [${action.value || action.text}] from dropdown`);
|
|
203
|
-
}
|
|
204
|
-
else if (action.type === 'keyPress') {
|
|
205
|
-
await activePage.keyboard.press(action.key || 'Enter');
|
|
206
|
-
results.push(`Pressed keyboard key [${action.key || 'Enter'}]`);
|
|
207
|
-
}
|
|
208
|
-
else if (action.type === 'scroll') {
|
|
209
|
-
await activePage.evaluate(() => {
|
|
210
|
-
window.scrollBy({ top: window.innerHeight * 0.75, behavior: 'smooth' });
|
|
211
|
-
});
|
|
212
|
-
results.push(`Scrolled page smoothly`);
|
|
213
|
-
}
|
|
214
|
-
else if (action.type === 'waitFor') {
|
|
215
|
-
await activePage.waitForSelector(action.selector, { timeout: action.timeoutMs || 10000 });
|
|
216
|
-
results.push(`Element ${action.selector} appeared`);
|
|
217
|
-
}
|
|
218
|
-
else if (action.type === 'scrape') {
|
|
219
|
-
// Phase 1 AGI Upgrade: AXTree Mapper injection
|
|
220
|
-
const mappedMarkdown = await activePage.evaluate(() => {
|
|
221
|
-
let idx = 1;
|
|
222
|
-
let lines = [];
|
|
223
|
-
const elements = document.querySelectorAll('a, button, input, textarea, [role="button"], select');
|
|
224
|
-
|
|
225
|
-
elements.forEach(el => {
|
|
226
|
-
// Ignore hidden elements
|
|
227
|
-
const style = window.getComputedStyle(el);
|
|
228
|
-
if (style.display === 'none' || style.visibility === 'hidden' || el.offsetWidth === 0) return;
|
|
229
|
-
|
|
230
|
-
let label = el.innerText || el.value || el.placeholder || el.getAttribute('aria-label') || el.name || 'unlabeled';
|
|
231
|
-
label = label.trim().replace(/\n/g, ' ');
|
|
232
|
-
if (!label) return; // Skip empty elements
|
|
233
|
-
|
|
234
|
-
const tagName = el.tagName.toLowerCase();
|
|
235
|
-
lines.push(`[${idx}] ${tagName}: ${label}`);
|
|
236
|
-
|
|
237
|
-
el.setAttribute('data-causyne-id', idx.toString());
|
|
238
|
-
idx++;
|
|
239
|
-
});
|
|
240
|
-
return lines.join('\n');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
results.push(`Scraped DOM Map:\n${mappedMarkdown}`);
|
|
53
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
244
54
|
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return { success: true, log: results };
|
|
248
|
-
} catch (e) {
|
|
249
|
-
return { success: false, error: e.message };
|
|
250
55
|
}
|
|
251
|
-
}
|
|
252
56
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { BrowserCapabilityError, causyneBrowser, type BrowserAction } from '../browser/causyneBrowser.js';
|
|
2
|
+
|
|
3
|
+
export const browserAct = {
|
|
4
|
+
schema: {
|
|
5
|
+
name: 'browser.act',
|
|
6
|
+
description: 'Use the dedicated Causyne Camoufox browser for resilient search, navigation, accessible snapshots, interaction, extraction, and workspace-scoped downloads. Take a snapshot before interacting by ref.',
|
|
7
|
+
parameters: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
actions: {
|
|
11
|
+
type: 'array',
|
|
12
|
+
minItems: 1,
|
|
13
|
+
maxItems: 40,
|
|
14
|
+
items: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
type: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
enum: ['newTab', 'switchTab', 'listTabs', 'closeTab', 'search', 'goto', 'snapshot', 'scrape', 'click', 'clickIndex', 'type', 'typeIndex', 'selectOption', 'keyPress', 'scroll', 'back', 'forward', 'reload', 'extract', 'download', 'screenshot']
|
|
20
|
+
},
|
|
21
|
+
tabId: { type: 'string', description: 'Tab id returned by newTab or listTabs.' },
|
|
22
|
+
url: { type: 'string', description: 'Public http(s) URL for goto or newTab.' },
|
|
23
|
+
query: { type: 'string', description: 'Search query.' },
|
|
24
|
+
ref: { type: 'string', description: 'Current accessibility ref, for example e12.' },
|
|
25
|
+
index: { type: 'number', description: 'Legacy numeric form of an accessibility ref.' },
|
|
26
|
+
selector: { type: 'string', description: 'CSS selector when a current accessibility ref is unavailable.' },
|
|
27
|
+
text: { type: 'string' },
|
|
28
|
+
value: { type: 'string' },
|
|
29
|
+
key: { type: 'string' },
|
|
30
|
+
direction: { type: 'string', enum: ['up', 'down', 'left', 'right'] },
|
|
31
|
+
amount: { type: 'number' },
|
|
32
|
+
offset: { type: 'number', description: 'Character offset for a paged accessibility snapshot.' },
|
|
33
|
+
outputFile: { type: 'string', description: 'Download or screenshot destination inside the selected workspace.' },
|
|
34
|
+
fullPage: { type: 'boolean', description: 'Capture the complete page for a screenshot instead of the visible viewport.' }
|
|
35
|
+
},
|
|
36
|
+
required: ['type'],
|
|
37
|
+
additionalProperties: false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
required: ['actions'],
|
|
42
|
+
additionalProperties: false
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async execute(args: { actions?: BrowserAction[] }) {
|
|
47
|
+
try {
|
|
48
|
+
const results = await causyneBrowser().run(args.actions ?? []);
|
|
49
|
+
return { success: true, output: JSON.stringify(results, null, 2), data: results };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof BrowserCapabilityError) {
|
|
52
|
+
return { success: false, error: error.message, requiresConsent: error.requiresConsent };
|
|
53
|
+
}
|
|
54
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -55,7 +55,7 @@ export const manageLocalDependencies = {
|
|
|
55
55
|
type: 'object',
|
|
56
56
|
properties: {
|
|
57
57
|
action: { type: 'string', enum: ['setup_venv', 'pipInst', 'check_and_setupEng', 'grant_capability', 'list_capabilities', 'grant_mcp_server', 'list_mcp_servers'] },
|
|
58
|
-
group: { type: 'string', description: 'Capability group for grant_capability:
|
|
58
|
+
group: { type: 'string', description: 'Capability group for grant_capability: browser (dedicated Camoufox web runtime), vision (screen capture), ocr (read text from images), system (process and load inspection), desktop (clipboard, open, recycle bin), archive (zip and unzip).' },
|
|
59
59
|
engine: { type: 'string', enum: ['godot', 'blender'], description: 'Engine to locate when action is check_and_setupEng' },
|
|
60
60
|
simulateMissing: { type: 'boolean', description: 'Simulate missing binary to verify sovereign fallback cascade' },
|
|
61
61
|
packages: { type: 'array', items: { type: 'string' }, description: 'Exact PyPI pins only, for example pillow==11.3.0. URLs, paths, VCS references and pip options are rejected.' }
|