@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.
Files changed (59) hide show
  1. package/docs/ARCHITECTURE.md +101 -17
  2. package/docs/SECURITY_REVIEW.md +13 -1
  3. package/docs/THIRD_PARTY_NOTICES.md +43 -10
  4. package/package.json +10 -4
  5. package/src/agentLoop.js +25 -3
  6. package/src/agentProfiles.js +135 -0
  7. package/src/agentProfiles.ts +143 -0
  8. package/src/approvalGrants.js +17 -35
  9. package/src/approvalGrants.ts +54 -0
  10. package/src/auditLog.d.ts +3 -0
  11. package/src/browser/camofoxRuntime.js +93 -0
  12. package/src/browser/camofoxRuntime.ts +112 -0
  13. package/src/browser/causyneBrowser.js +495 -0
  14. package/src/browser/causyneBrowser.ts +594 -0
  15. package/src/browser/searchProviders.js +156 -0
  16. package/src/browser/searchProviders.ts +156 -0
  17. package/src/browser/snapshotWindow.js +28 -0
  18. package/src/browser/snapshotWindow.ts +40 -0
  19. package/src/browser/urlPolicy.js +89 -0
  20. package/src/browser/urlPolicy.ts +89 -0
  21. package/src/checkpoints.js +19 -6
  22. package/src/commands.js +37 -8
  23. package/src/config.d.ts +2 -0
  24. package/src/hooks.js +121 -107
  25. package/src/hooks.ts +193 -0
  26. package/src/mcp/mcpClient.js +520 -132
  27. package/src/mcp/mcpClient.ts +584 -0
  28. package/src/mcp/mcpRegistry.js +399 -185
  29. package/src/mcp/mcpRegistry.ts +451 -0
  30. package/src/planCache.js +473 -450
  31. package/src/planCache.ts +840 -0
  32. package/src/plugins/pluginManager.js +40 -5
  33. package/src/plugins/pluginSkills.js +84 -0
  34. package/src/plugins/pluginSkills.ts +106 -0
  35. package/src/provisioning/capabilityProvisioner.d.ts +10 -0
  36. package/src/provisioning/capabilityProvisioner.js +5 -0
  37. package/src/provisioning/capabilityTools.js +2 -1
  38. package/src/sandbox.js +32 -2
  39. package/src/sessionJournal.js +471 -0
  40. package/src/sessionJournal.ts +496 -0
  41. package/src/sessionStore.js +3 -279
  42. package/src/skillsManager.js +18 -0
  43. package/src/subagentJournal.js +215 -0
  44. package/src/subagentJournal.ts +254 -0
  45. package/src/taskShape.js +179 -198
  46. package/src/taskShape.ts +259 -0
  47. package/src/tools/browserAutomator.js +45 -241
  48. package/src/tools/browserAutomator.ts +57 -0
  49. package/src/tools/manageLocalAiDependencies.js +1 -1
  50. package/src/tools/subagentOrchestrator.js +73 -12
  51. package/src/tools/webFetch.js +183 -68
  52. package/src/tools/webFetch.ts +185 -0
  53. package/src/tools/webSearch.js +50 -75
  54. package/src/tools/webSearch.ts +56 -0
  55. package/src/types/contracts.js +8 -0
  56. package/src/types/contracts.ts +263 -0
  57. package/src/utils/paths.d.ts +3 -0
  58. package/src/version.d.ts +1 -0
  59. package/src/mcp/jsonRpc.js +0 -165
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // THE SCOPE IS THE PROCESS, AND THAT IS THE WHOLE SAFETY MODEL.
10
10
  // This is a module-level Set. It is never written to config.json, never
11
-
11
+ // serialized, and never restored. When the process ends, every standing
12
12
  // approval is gone — the same contract opencode states to the user in its own
13
13
  // words: "This will allow … until OpenCode is restarted."
14
14
  //
@@ -17,50 +17,32 @@
17
17
  // a session the user has forgotten they configured. If that is ever wanted it
18
18
  // should be an explicit, listed, revocable config entry — not a quiet
19
19
  // side-effect of a keypress in a modal.
20
-
21
20
  /** Tool names approved for the rest of this process. Deliberately in memory. */
22
21
  const standingApprovals = new Set();
23
-
24
- /**
25
- * Record a standing approval for one tool.
26
- *
27
- * @param {string} toolName Canonical tool name, e.g. 'sysRun'.
28
- */
22
+ function normalizeToolName(toolName) {
23
+ return String(toolName || '').trim();
24
+ }
25
+ /** Record a standing approval for one canonical tool name. */
29
26
  export function grantAlways(toolName) {
30
- const name = String(toolName || '').trim();
31
- if (name) standingApprovals.add(name);
27
+ const name = normalizeToolName(toolName);
28
+ if (name)
29
+ standingApprovals.add(name);
32
30
  }
33
-
34
- /**
35
- * True when this tool may run without asking again.
36
- *
37
- * @param {string} toolName
38
- * @returns {boolean}
39
- */
31
+ /** True when this tool may run without asking again. */
40
32
  export function isAlwaysAllowed(toolName) {
41
- return standingApprovals.has(String(toolName || '').trim());
33
+ return standingApprovals.has(normalizeToolName(toolName));
42
34
  }
43
-
44
35
  /** Every standing approval, for `/approvals` and for the audit trail. */
45
36
  export function listAlwaysAllowed() {
46
- return [...standingApprovals].sort();
37
+ return [...standingApprovals].sort();
47
38
  }
48
-
49
- /**
50
- * Take them all back.
51
- *
52
- * A grant with no way out is a trap, so this is reachable from `/approvals
53
- * clear` as well as from a restart.
54
- *
55
- * @returns {number} how many were revoked
56
- */
39
+ /** Take every standing approval back and return how many were revoked. */
57
40
  export function revokeAllAlways() {
58
- const count = standingApprovals.size;
59
- standingApprovals.clear();
60
- return count;
41
+ const count = standingApprovals.size;
42
+ standingApprovals.clear();
43
+ return count;
61
44
  }
62
-
63
- /** Revoke one. @returns {boolean} true when there was something to revoke. */
45
+ /** Revoke one standing approval. */
64
46
  export function revokeAlways(toolName) {
65
- return standingApprovals.delete(String(toolName || '').trim());
47
+ return standingApprovals.delete(normalizeToolName(toolName));
66
48
  }
@@ -0,0 +1,54 @@
1
+ // "Allow always" — standing approvals, for the length of this process only.
2
+ //
3
+ // WHY THIS EXISTS. Being asked to approve the same tool for the twentieth time
4
+ // in one task does not make anyone safer; it makes them stop reading and start
5
+ // hammering the approve key, which is the exact failure the gate exists to
6
+ // prevent. A gate people are numb to is not a gate. So the honest answer is to
7
+ // let someone who has decided a tool is fine say so once, deliberately.
8
+ //
9
+ // THE SCOPE IS THE PROCESS, AND THAT IS THE WHOLE SAFETY MODEL.
10
+ // This is a module-level Set. It is never written to config.json, never
11
+ // serialized, and never restored. When the process ends, every standing
12
+ // approval is gone — the same contract opencode states to the user in its own
13
+ // words: "This will allow … until OpenCode is restarted."
14
+ //
15
+ // Persisting these to disk would be a different feature with a different risk
16
+ // profile: a permission granted in a hurry six weeks ago, silently applying to
17
+ // a session the user has forgotten they configured. If that is ever wanted it
18
+ // should be an explicit, listed, revocable config entry — not a quiet
19
+ // side-effect of a keypress in a modal.
20
+
21
+ /** Tool names approved for the rest of this process. Deliberately in memory. */
22
+ const standingApprovals = new Set<string>();
23
+
24
+ function normalizeToolName(toolName: unknown): string {
25
+ return String(toolName || '').trim();
26
+ }
27
+
28
+ /** Record a standing approval for one canonical tool name. */
29
+ export function grantAlways(toolName: unknown): void {
30
+ const name = normalizeToolName(toolName);
31
+ if (name) standingApprovals.add(name);
32
+ }
33
+
34
+ /** True when this tool may run without asking again. */
35
+ export function isAlwaysAllowed(toolName: unknown): boolean {
36
+ return standingApprovals.has(normalizeToolName(toolName));
37
+ }
38
+
39
+ /** Every standing approval, for `/approvals` and for the audit trail. */
40
+ export function listAlwaysAllowed(): string[] {
41
+ return [...standingApprovals].sort();
42
+ }
43
+
44
+ /** Take every standing approval back and return how many were revoked. */
45
+ export function revokeAllAlways(): number {
46
+ const count = standingApprovals.size;
47
+ standingApprovals.clear();
48
+ return count;
49
+ }
50
+
51
+ /** Revoke one standing approval. */
52
+ export function revokeAlways(toolName: unknown): boolean {
53
+ return standingApprovals.delete(normalizeToolName(toolName));
54
+ }
@@ -0,0 +1,3 @@
1
+ export const auditLog: {
2
+ log(event: Record<string, unknown>): void;
3
+ };
@@ -0,0 +1,93 @@
1
+ import { accessSync, constants, existsSync, realpathSync, statSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ function assertExecutable(candidate) {
5
+ const stat = statSync(candidate);
6
+ if (!stat.isFile() && !stat.isSymbolicLink()) {
7
+ throw new Error(`Camoufox executable is not a file: ${candidate}`);
8
+ }
9
+ if (process.platform !== 'win32')
10
+ accessSync(candidate, constants.X_OK);
11
+ }
12
+ function candidateExecutables() {
13
+ const home = os.homedir();
14
+ if (process.platform === 'darwin') {
15
+ return [
16
+ path.join(home, 'Library', 'Caches', 'camoufox', 'Camoufox.app', 'Contents', 'MacOS', 'camoufox'),
17
+ '/Applications/Camoufox.app/Contents/MacOS/camoufox',
18
+ path.join(home, 'Applications', 'Camoufox.app', 'Contents', 'MacOS', 'camoufox')
19
+ ];
20
+ }
21
+ if (process.platform === 'win32') {
22
+ const localAppData = process.env.LOCALAPPDATA;
23
+ const programFiles = process.env.ProgramFiles;
24
+ return [
25
+ ...(localAppData ? [path.join(localAppData, 'camoufox', 'camoufox.exe')] : []),
26
+ ...(localAppData ? [path.join(localAppData, 'Camoufox', 'camoufox.exe')] : []),
27
+ ...(programFiles ? [path.join(programFiles, 'Camoufox', 'camoufox.exe')] : [])
28
+ ];
29
+ }
30
+ return [
31
+ path.join(home, '.cache', 'camoufox', 'camoufox-bin'),
32
+ '/opt/camoufox/camoufox-bin',
33
+ path.join(home, '.local', 'share', 'camoufox', 'camoufox-bin'),
34
+ '/usr/local/lib/camoufox/camoufox-bin'
35
+ ];
36
+ }
37
+ function expectedExecutable(installDirectory) {
38
+ if (process.platform === 'darwin') {
39
+ return path.join(installDirectory, 'Camoufox.app', 'Contents', 'MacOS', 'camoufox');
40
+ }
41
+ if (process.platform === 'win32')
42
+ return path.join(installDirectory, 'camoufox.exe');
43
+ return path.join(installDirectory, 'camoufox-bin');
44
+ }
45
+ function findInstallDirectory(executablePath) {
46
+ let current = path.dirname(executablePath);
47
+ for (let depth = 0; depth < 7; depth += 1) {
48
+ if (existsSync(path.join(current, 'version.json')) && existsSync(expectedExecutable(current))) {
49
+ return current;
50
+ }
51
+ const parent = path.dirname(current);
52
+ if (parent === current)
53
+ break;
54
+ current = parent;
55
+ }
56
+ return null;
57
+ }
58
+ function resourceDirectoryFor(executablePath) {
59
+ if (process.platform === 'darwin') {
60
+ return path.resolve(path.dirname(executablePath), '..', 'Resources');
61
+ }
62
+ return path.dirname(executablePath);
63
+ }
64
+ export function resolveCamoufoxRuntime() {
65
+ const configured = process.env.CAUSYNE_CAMOFOX_EXECUTABLE?.trim();
66
+ const configuredHome = process.env.CAMOUFOX_INSTALL_DIR?.trim();
67
+ const candidates = configured
68
+ ? [path.resolve(configured)]
69
+ : configuredHome
70
+ ? [expectedExecutable(path.resolve(configuredHome))]
71
+ : candidateExecutables();
72
+ const executablePath = candidates.find(existsSync);
73
+ if (!executablePath) {
74
+ throw new Error('Camoufox is not installed. Causyne does not download browser binaries automatically. ' +
75
+ 'Install a trusted Camoufox build separately, then set CAUSYNE_CAMOFOX_EXECUTABLE to its executable path.');
76
+ }
77
+ assertExecutable(executablePath);
78
+ const resolvedExecutable = realpathSync(executablePath);
79
+ const installDirectory = findInstallDirectory(resolvedExecutable);
80
+ if (!installDirectory) {
81
+ throw new Error(`Camoufox at ${resolvedExecutable} is not inside a complete distribution. ` +
82
+ 'Keep the distribution version.json beside its platform bundle and point CAUSYNE_CAMOFOX_EXECUTABLE at that bundle.');
83
+ }
84
+ const resourceDirectory = resourceDirectoryFor(resolvedExecutable);
85
+ if (!existsSync(path.join(resourceDirectory, 'properties.json'))) {
86
+ throw new Error(`Camoufox properties.json was not found in ${resourceDirectory}.`);
87
+ }
88
+ return {
89
+ installDirectory,
90
+ resourceDirectory,
91
+ source: configured || configuredHome ? 'environment' : 'system'
92
+ };
93
+ }
@@ -0,0 +1,112 @@
1
+ import { accessSync, constants, existsSync, realpathSync, statSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ // Adapted from Jo Inc's MIT-licensed Camofox Browser external executable
6
+ // discovery at commit 2c0036a8b18bf9cd983376751ff4d52dfd9add0f. Causyne
7
+ // keeps this integration layer in TypeScript and never downloads a browser.
8
+
9
+ export interface PreparedCamoufoxRuntime {
10
+ installDirectory: string;
11
+ resourceDirectory: string;
12
+ source: 'environment' | 'system';
13
+ }
14
+
15
+ function assertExecutable(candidate: string): void {
16
+ const stat = statSync(candidate);
17
+ if (!stat.isFile() && !stat.isSymbolicLink()) {
18
+ throw new Error(`Camoufox executable is not a file: ${candidate}`);
19
+ }
20
+ if (process.platform !== 'win32') accessSync(candidate, constants.X_OK);
21
+ }
22
+
23
+ function candidateExecutables(): string[] {
24
+ const home = os.homedir();
25
+ if (process.platform === 'darwin') {
26
+ return [
27
+ path.join(home, 'Library', 'Caches', 'camoufox', 'Camoufox.app', 'Contents', 'MacOS', 'camoufox'),
28
+ '/Applications/Camoufox.app/Contents/MacOS/camoufox',
29
+ path.join(home, 'Applications', 'Camoufox.app', 'Contents', 'MacOS', 'camoufox')
30
+ ];
31
+ }
32
+ if (process.platform === 'win32') {
33
+ const localAppData = process.env.LOCALAPPDATA;
34
+ const programFiles = process.env.ProgramFiles;
35
+ return [
36
+ ...(localAppData ? [path.join(localAppData, 'camoufox', 'camoufox.exe')] : []),
37
+ ...(localAppData ? [path.join(localAppData, 'Camoufox', 'camoufox.exe')] : []),
38
+ ...(programFiles ? [path.join(programFiles, 'Camoufox', 'camoufox.exe')] : [])
39
+ ];
40
+ }
41
+ return [
42
+ path.join(home, '.cache', 'camoufox', 'camoufox-bin'),
43
+ '/opt/camoufox/camoufox-bin',
44
+ path.join(home, '.local', 'share', 'camoufox', 'camoufox-bin'),
45
+ '/usr/local/lib/camoufox/camoufox-bin'
46
+ ];
47
+ }
48
+
49
+ function expectedExecutable(installDirectory: string): string {
50
+ if (process.platform === 'darwin') {
51
+ return path.join(installDirectory, 'Camoufox.app', 'Contents', 'MacOS', 'camoufox');
52
+ }
53
+ if (process.platform === 'win32') return path.join(installDirectory, 'camoufox.exe');
54
+ return path.join(installDirectory, 'camoufox-bin');
55
+ }
56
+
57
+ function findInstallDirectory(executablePath: string): string | null {
58
+ let current = path.dirname(executablePath);
59
+ for (let depth = 0; depth < 7; depth += 1) {
60
+ if (existsSync(path.join(current, 'version.json')) && existsSync(expectedExecutable(current))) {
61
+ return current;
62
+ }
63
+ const parent = path.dirname(current);
64
+ if (parent === current) break;
65
+ current = parent;
66
+ }
67
+ return null;
68
+ }
69
+
70
+ function resourceDirectoryFor(executablePath: string): string {
71
+ if (process.platform === 'darwin') {
72
+ return path.resolve(path.dirname(executablePath), '..', 'Resources');
73
+ }
74
+ return path.dirname(executablePath);
75
+ }
76
+
77
+ export function resolveCamoufoxRuntime(): PreparedCamoufoxRuntime {
78
+ const configured = process.env.CAUSYNE_CAMOFOX_EXECUTABLE?.trim();
79
+ const configuredHome = process.env.CAMOUFOX_INSTALL_DIR?.trim();
80
+ const candidates = configured
81
+ ? [path.resolve(configured)]
82
+ : configuredHome
83
+ ? [expectedExecutable(path.resolve(configuredHome))]
84
+ : candidateExecutables();
85
+ const executablePath = candidates.find(existsSync);
86
+ if (!executablePath) {
87
+ throw new Error(
88
+ 'Camoufox is not installed. Causyne does not download browser binaries automatically. ' +
89
+ 'Install a trusted Camoufox build separately, then set CAUSYNE_CAMOFOX_EXECUTABLE to its executable path.'
90
+ );
91
+ }
92
+
93
+ assertExecutable(executablePath);
94
+ const resolvedExecutable = realpathSync(executablePath);
95
+ const installDirectory = findInstallDirectory(resolvedExecutable);
96
+ if (!installDirectory) {
97
+ throw new Error(
98
+ `Camoufox at ${resolvedExecutable} is not inside a complete distribution. ` +
99
+ 'Keep the distribution version.json beside its platform bundle and point CAUSYNE_CAMOFOX_EXECUTABLE at that bundle.'
100
+ );
101
+ }
102
+ const resourceDirectory = resourceDirectoryFor(resolvedExecutable);
103
+ if (!existsSync(path.join(resourceDirectory, 'properties.json'))) {
104
+ throw new Error(`Camoufox properties.json was not found in ${resourceDirectory}.`);
105
+ }
106
+
107
+ return {
108
+ installDirectory,
109
+ resourceDirectory,
110
+ source: configured || configuredHome ? 'environment' : 'system'
111
+ };
112
+ }