@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/docs/ARCHITECTURE.md
CHANGED
|
@@ -31,18 +31,23 @@ are application policies and require adversarial review.
|
|
|
31
31
|
| Concern | Source |
|
|
32
32
|
| --- | --- |
|
|
33
33
|
| Invocation and local session | `entrypoint.js`, `cliMode.js`, `tui/` |
|
|
34
|
-
| Orchestration and budgets | `agentLoop.js`, `modelRouter.js`, `tokenTracker.js` |
|
|
34
|
+
| Orchestration and budgets | `agentLoop.js`, `agentProfiles.ts`, `subagentJournal.ts`, `modelRouter.js`, `tokenTracker.js` |
|
|
35
35
|
| Host execution | `tools/registry.js`, `sandbox.js`, `execSafe.js` |
|
|
36
36
|
| Models | `providers/`, `modelCatalogue.js` |
|
|
37
37
|
| Context and memory | `memory.js`, `memoryManager.js`, `contextManager.js`, `cortex/` |
|
|
38
|
-
| Recovery | `checkpoints.js`, `gitSnapshot.js`, `sessionStore.js` |
|
|
39
|
-
| Extensions | `mcp/`, `plugins/`, `hooks.
|
|
38
|
+
| Recovery | `checkpoints.js`, `gitSnapshot.js`, `sessionJournal.ts`, `sessionStore.js` |
|
|
39
|
+
| Extensions | `mcp/`, `plugins/`, `hooks.ts`, `skillsManager.js`, `toolPacks.js` |
|
|
40
40
|
| Readable interpreter source | `src/tool-scripts/` |
|
|
41
41
|
|
|
42
|
-
The
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
The shipping runtime is ESM on Node.js 24+. Incremental strict TypeScript
|
|
43
|
+
migration is active: `tsconfig.runtime.json` emits readable JavaScript beside
|
|
44
|
+
strict TypeScript runtime modules, shared contracts live under `src/types/`,
|
|
45
|
+
and retained JavaScript has a separately named migration ratchet. Session,
|
|
46
|
+
plugin-skill, task-shape, delegated-agent, hook, approval-grant, plan-cache and
|
|
47
|
+
MCP boundaries now compile from strict TypeScript.
|
|
48
|
+
Runtime validation is still required at model, filesystem, subprocess and
|
|
49
|
+
extension boundaries. Rust is reserved for a measured, narrow native worker;
|
|
50
|
+
no Rust rewrite is underway.
|
|
46
51
|
|
|
47
52
|
## Tool and extension boundaries
|
|
48
53
|
|
|
@@ -54,11 +59,79 @@ the task shape or `tool_search` makes them relevant.
|
|
|
54
59
|
Local plugins use a static `plugin.json` manifest. Creating a plugin leaves it
|
|
55
60
|
disabled. Enabling it pins a digest of every file the plugin process may read;
|
|
56
61
|
any later manifest, source, helper or asset change revokes trust. Discovery
|
|
57
|
-
validates static manifests and hashes bytes without importing JavaScript, so
|
|
58
|
-
during pre-trust initialization.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
validates static manifests and hashes bytes without importing JavaScript, so
|
|
63
|
+
code cannot run during pre-trust initialization. Reviewed plugins may contribute
|
|
64
|
+
namespaced, progressively disclosed Markdown skills; these remain inert data
|
|
65
|
+
and disappear from the catalogue if the digest changes. A plugin tool runs in a
|
|
66
|
+
separate Node process with the permission model enabled and only its declared
|
|
67
|
+
workspace, network, or subprocess capabilities. Plugin calls still pass through
|
|
68
|
+
Causyne's sandbox and approval gates.
|
|
69
|
+
|
|
70
|
+
MCP uses the official Model Context Protocol client packages rather than a
|
|
71
|
+
project-specific JSON-RPC implementation. It supports local stdio servers and
|
|
72
|
+
remote Streamable HTTP servers, with legacy SSE fallback when configured for
|
|
73
|
+
automatic negotiation. The SDK owns initialization, capability negotiation,
|
|
74
|
+
protocol framing, request cancellation, and transport shutdown. Causyne adds
|
|
75
|
+
connection timeouts, bounded output, current-workspace roots, concurrent
|
|
76
|
+
discovery, `tools/list_changed` refresh, structured content handling, and
|
|
77
|
+
namespaced access to server resources and prompts.
|
|
78
|
+
|
|
79
|
+
Each MCP grant is bound to a SHA-256 digest of the complete server definition.
|
|
80
|
+
Changing a command, URL, environment, header, working directory, timeout,
|
|
81
|
+
protocol mode, or tool filter invalidates the old grant. Disconnect and revoke
|
|
82
|
+
also invalidate an in-flight handshake so a stale connection cannot become
|
|
83
|
+
active afterward. Per-server include and exclude patterns limit which
|
|
84
|
+
capabilities enter the model's catalogue. Connection state distinguishes
|
|
85
|
+
disabled, connecting, connected, failed, and authentication-required servers;
|
|
86
|
+
one failing server does not prevent native tools or other MCP servers from
|
|
87
|
+
loading. Static bearer or custom headers may be configured locally. Interactive
|
|
88
|
+
browser OAuth is not implemented yet; an HTTP 401 is surfaced as
|
|
89
|
+
`needs_auth` instead of being reported as a generic transport failure.
|
|
90
|
+
|
|
91
|
+
## Agent browser boundary
|
|
92
|
+
|
|
93
|
+
Causyne's web foundation has two layers. Lightweight TypeScript search and
|
|
94
|
+
fetch clients work without a browser runtime. Rendered navigation uses a
|
|
95
|
+
dedicated in-process Camoufox adapter derived from the locally reviewed Camofox
|
|
96
|
+
Browser source. The adapter is deliberately smaller than the reference service:
|
|
97
|
+
it does not import an HTTP server, inbound port, cloud deployment layer, VNC,
|
|
98
|
+
telemetry, remote WebSocket control, or OpenClaw integration.
|
|
99
|
+
|
|
100
|
+
Camoufox and Playwright are exact-pinned optional capability packages installed
|
|
101
|
+
into the user's private Causyne capability directory only after an explicit
|
|
102
|
+
browser capability grant. Causyne does not download a browser binary. Advanced
|
|
103
|
+
rendered browsing requires a separately installed Camoufox bundle selected by
|
|
104
|
+
`CAUSYNE_CAMOFOX_EXECUTABLE` or discovered from a standard system location.
|
|
105
|
+
The main npm package therefore stays small and web search remains useful when
|
|
106
|
+
the optional rendered browser is absent.
|
|
107
|
+
|
|
108
|
+
The browser runs headless by default with WebRTC blocked and a Causyne-owned
|
|
109
|
+
profile. It provides tabs, navigation, accessibility snapshots with bounded
|
|
110
|
+
pagination, stable element references, form interaction, bounded extraction,
|
|
111
|
+
and atomic downloads restricted to the selected workspace. Search first uses
|
|
112
|
+
bounded DuckDuckGo HTML and Bing RSS providers, then uses rendered provider
|
|
113
|
+
pages when the optional browser is available. Failures remain visible after
|
|
114
|
+
every provider is exhausted instead of being replaced with fabricated results.
|
|
115
|
+
|
|
116
|
+
Every top-level navigation and browser subresource passes through the public
|
|
117
|
+
URL policy. The policy accepts only HTTP(S), rejects embedded credentials,
|
|
118
|
+
resolves hostnames before access, and blocks loopback, private, link-local,
|
|
119
|
+
multicast, reserved, local, internal, and metadata addresses. Redirects in the
|
|
120
|
+
non-browser fetch path are revalidated one at a time. Responses and extracted
|
|
121
|
+
text are bounded, and network downloads require the existing approval gate.
|
|
122
|
+
These controls reduce SSRF and unbounded-output risk while preserving normal
|
|
123
|
+
public-web use from the user's selected workspace.
|
|
124
|
+
|
|
125
|
+
User-defined subagent profiles are static JSON manifests under the local Causyne
|
|
126
|
+
state directory. A profile supplies bounded instructions, an allowlist of tools,
|
|
127
|
+
and hard step/token ceilings. It cannot widen its own scope, cannot recursively
|
|
128
|
+
delegate, and does not load executable project code. Calls may narrow a
|
|
129
|
+
profile's tool scope or budgets, but cannot exceed them.
|
|
130
|
+
|
|
131
|
+
Completed delegated runs write a separate append-only JSONL sidechain under the
|
|
132
|
+
local Causyne state directory. The parent receives only the worker's final
|
|
133
|
+
result, while the bounded sidechain retains its messages and outcome for local
|
|
134
|
+
inspection. A torn final append preserves earlier complete events.
|
|
62
135
|
|
|
63
136
|
Hooks are user-owned argv arrays stored in the local Causyne configuration.
|
|
64
137
|
They never pass through a shell and project repositories are not searched for
|
|
@@ -66,10 +139,19 @@ startup hooks. `PreToolUse` runs after the built-in sandbox and before an
|
|
|
66
139
|
approval prompt; exit code 2 denies, and any other failure fails closed.
|
|
67
140
|
`PostToolUse` can annotate a result or reject it without rewriting history.
|
|
68
141
|
|
|
69
|
-
Host execution remains the
|
|
70
|
-
the
|
|
71
|
-
|
|
72
|
-
|
|
142
|
+
Host execution remains the current architecture because Causyne must operate in
|
|
143
|
+
the directory and desktop environment the user selected. Container work is
|
|
144
|
+
deferred and is not part of the present implementation plan.
|
|
145
|
+
|
|
146
|
+
## Durable sessions
|
|
147
|
+
|
|
148
|
+
Each workspace-scoped conversation is stored as an append-only JSONL event
|
|
149
|
+
journal. Reads project `session.created`, `messages.appended`,
|
|
150
|
+
`messages.windowed`, and explicit replacement events into the current session.
|
|
151
|
+
A torn final append preserves prior complete events, while complete corruption
|
|
152
|
+
fails safely. Legacy per-session JSON and global history files migrate on read.
|
|
153
|
+
The compatibility `sessionStore.js` entry keeps existing callers stable while
|
|
154
|
+
the strict TypeScript journal emits the JavaScript used at runtime.
|
|
73
155
|
|
|
74
156
|
## Plan cache and context pressure
|
|
75
157
|
|
|
@@ -77,7 +159,9 @@ Completed multi-step runs teach argument-free plan templates. Lookup tries an
|
|
|
77
159
|
indexed exact signature first, then bounded lexical similarity only for a task
|
|
78
160
|
shape with enough confidence. Latency, hit type and recent hit rate are recorded;
|
|
79
161
|
lookup temporarily cools down when a populated cache repeatedly misses while
|
|
80
|
-
new successful plans may continue warming it.
|
|
162
|
+
new successful plans may continue warming it. Persisted plans and metrics are
|
|
163
|
+
validated before entering the typed runtime; malformed entries are ignored.
|
|
164
|
+
`/plans import` accepts sanitized
|
|
81
165
|
offline samples for known workloads, and `/plans` exposes the measurements.
|
|
82
166
|
`npm run benchmark:plan-cache` measures exact lookup, fuzzy lookup, misses and
|
|
83
167
|
prewarming at bounded cache sizes in an isolated temporary state directory. It
|
package/docs/SECURITY_REVIEW.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Security review and migration status
|
|
2
2
|
|
|
3
|
-
Review date: 2026-10-
|
|
3
|
+
Review date: 2026-10-21. This is a scoped source and package review, not a
|
|
4
4
|
claim that Causyne Agent is free from vulnerabilities.
|
|
5
5
|
|
|
6
6
|
## OpenSSF records
|
|
@@ -65,6 +65,18 @@ The active source takes a different architecture and distribution path:
|
|
|
65
65
|
- `web_fetch` downloads and curated game-asset downloads require the highest
|
|
66
66
|
approval class. Curated assets are size-limited, constrained to catalog URLs,
|
|
67
67
|
protected from output path traversal, and are not automatically extracted.
|
|
68
|
+
- Web search uses bounded HTTP providers without requiring a browser binary.
|
|
69
|
+
Interactive rendered browsing uses an outbound, in-process Camoufox adapter.
|
|
70
|
+
Causyne does not run Camofox Browser's server, inbound port, remote control,
|
|
71
|
+
VNC, telemetry, cloud deployment, or OpenClaw integration. Optional launcher
|
|
72
|
+
packages require an explicit capability grant. Causyne never downloads the
|
|
73
|
+
Camoufox browser binary; an existing external installation must be selected
|
|
74
|
+
through `CAUSYNE_CAMOFOX_EXECUTABLE` or a standard system location.
|
|
75
|
+
- Browser navigation and subresources accept only public HTTP(S) destinations.
|
|
76
|
+
Hostnames are resolved and checked against private, loopback, link-local,
|
|
77
|
+
multicast, reserved, local, internal, and metadata ranges. Non-browser
|
|
78
|
+
redirects are checked at every hop. Downloads are bounded, atomic, restricted
|
|
79
|
+
to the selected workspace, and remain in the highest approval class.
|
|
68
80
|
- The package allowlist excludes the legacy archive, Android source, private
|
|
69
81
|
operating documents, credentials, installers, bytecode, and generated
|
|
70
82
|
tarballs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Third-party software review
|
|
2
2
|
|
|
3
|
-
Review date: 2026-10-
|
|
3
|
+
Review date: 2026-10-21.
|
|
4
4
|
|
|
5
5
|
Causyne Agent's npm package declares third-party packages as dependencies; it
|
|
6
6
|
does not bundle their source into the project tarball. Each dependency remains
|
|
@@ -9,22 +9,55 @@ license identifiers:
|
|
|
9
9
|
|
|
10
10
|
| License expression | Lockfile package entries |
|
|
11
11
|
| --- | ---: |
|
|
12
|
-
| Apache-2.0 |
|
|
13
|
-
| MIT |
|
|
12
|
+
| Apache-2.0 | 25 |
|
|
13
|
+
| MIT | 20 |
|
|
14
|
+
| ISC | 2 |
|
|
14
15
|
|
|
15
|
-
The lockfile contained
|
|
16
|
-
|
|
17
|
-
rest. npm's audit metadata reported 12 installed packages.
|
|
16
|
+
The lockfile contained 47 dependency entries in total. Development and
|
|
17
|
+
platform-specific optional entries account for most of the graph.
|
|
18
18
|
|
|
19
|
-
The direct runtime dependencies are
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`
|
|
19
|
+
The direct runtime dependencies are `@modelcontextprotocol/client`,
|
|
20
|
+
`acorn-walk`, and `commander`. Direct development dependencies are
|
|
21
|
+
`@babel/parser`, `@modelcontextprotocol/node`, `@modelcontextprotocol/server`,
|
|
22
|
+
`@types/node`, `acorn`, `typescript`, and `zod`. The three direct MCP packages
|
|
23
|
+
are licensed under Apache-2.0. Exact versions, transitive dependencies, license
|
|
24
|
+
identifiers, and integrity hashes are recorded in `package-lock.json`.
|
|
23
25
|
|
|
24
26
|
No dependency in the reviewed main lockfile was marked with an install script.
|
|
25
27
|
Optional capability dependencies are installed only after approval into a
|
|
26
28
|
separate local environment and are not covered by the main lockfile review.
|
|
27
29
|
|
|
30
|
+
The optional browser capability installs `camoufox-js` 0.11.5 under MPL-2.0
|
|
31
|
+
and `playwright-core` 1.58.0 under Apache-2.0. They remain separate packages in
|
|
32
|
+
the user's local capability directory and are not copied into Causyne Agent's
|
|
33
|
+
npm tarball. Causyne does not download or bundle the Camoufox browser runtime.
|
|
34
|
+
|
|
35
|
+
The snapshot windowing and external executable bridge in `src/browser/` are
|
|
36
|
+
adapted from the MIT-licensed Camofox Browser project at source commit
|
|
37
|
+
`2c0036a8b18bf9cd983376751ff4d52dfd9add0f`. Its required notice is:
|
|
38
|
+
|
|
39
|
+
> MIT License
|
|
40
|
+
>
|
|
41
|
+
> Copyright (c) 2025 Jo, Inc
|
|
42
|
+
>
|
|
43
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
44
|
+
> of this software and associated documentation files (the "Software"), to deal
|
|
45
|
+
> in the Software without restriction, including without limitation the rights
|
|
46
|
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
47
|
+
> copies of the Software, and to permit persons to whom the Software is
|
|
48
|
+
> furnished to do so, subject to the following conditions:
|
|
49
|
+
>
|
|
50
|
+
> The above copyright notice and this permission notice shall be included in all
|
|
51
|
+
> copies or substantial portions of the Software.
|
|
52
|
+
>
|
|
53
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
54
|
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
55
|
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
56
|
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
57
|
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
58
|
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
59
|
+
> SOFTWARE.
|
|
60
|
+
|
|
28
61
|
Repository scanning found no third-party copyright header or license marker in
|
|
29
62
|
the project-authored source and skill files. That negative scan alone cannot
|
|
30
63
|
establish authorship or permission. On 2026-10-19, the licensor confirmed that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@causyne/agent",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.17",
|
|
4
4
|
"description": "Agentic AI for software, tools, and computer workflows",
|
|
5
5
|
"main": "bin/causyne.js",
|
|
6
6
|
"type": "module",
|
|
@@ -23,13 +23,15 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"start": "node ./bin/causyne.js",
|
|
25
25
|
"test": "node --test --import ./tests/helpers/isolateState.js \"tests/**/*.test.js\"",
|
|
26
|
-
"build": "node scripts/compile_skills.js",
|
|
26
|
+
"build": "node scripts/compile_skills.js && tsc -p tsconfig.runtime.json",
|
|
27
27
|
"benchmark:plan-cache": "node src/benchmarks/planCache.js",
|
|
28
28
|
"check:release": "node scripts/check_release.js",
|
|
29
29
|
"check:public": "npm run check:release && node scripts/check_public_release.js",
|
|
30
30
|
"prepublishOnly": "npm run check:public && npm test && npm run typecheck:strict",
|
|
31
31
|
"typecheck": "tsc -p tsconfig.json",
|
|
32
|
-
"typecheck:
|
|
32
|
+
"typecheck:ts": "tsc -p tsconfig.strict.json",
|
|
33
|
+
"typecheck:js-ratchet": "tsc -p tsconfig.js-ratchet.json",
|
|
34
|
+
"typecheck:strict": "npm run typecheck:ts && npm run typecheck:js-ratchet"
|
|
33
35
|
},
|
|
34
36
|
"engines": {
|
|
35
37
|
"node": ">=24.0.0"
|
|
@@ -51,14 +53,18 @@
|
|
|
51
53
|
"url": "https://github.com/ikbal-causyne/causyne-agent/issues"
|
|
52
54
|
},
|
|
53
55
|
"dependencies": {
|
|
56
|
+
"@modelcontextprotocol/client": "2.3.1",
|
|
54
57
|
"acorn-walk": "^8.3.5",
|
|
55
58
|
"commander": "^11.1.0"
|
|
56
59
|
},
|
|
57
60
|
"devDependencies": {
|
|
58
61
|
"@babel/parser": "^8.0.7",
|
|
62
|
+
"@modelcontextprotocol/node": "2.1.1",
|
|
63
|
+
"@modelcontextprotocol/server": "2.3.1",
|
|
59
64
|
"@types/node": "^22.20.2",
|
|
60
65
|
"acorn": "^8.18.0",
|
|
61
|
-
"typescript": "^7.0.2"
|
|
66
|
+
"typescript": "^7.0.2",
|
|
67
|
+
"zod": "4.6.7"
|
|
62
68
|
},
|
|
63
69
|
"private": false
|
|
64
70
|
}
|
package/src/agentLoop.js
CHANGED
|
@@ -344,6 +344,11 @@ export class AgentLoop {
|
|
|
344
344
|
this.circuitBreakerUnlocked = false;
|
|
345
345
|
this.stopRequested = false;
|
|
346
346
|
this.pendingApprovals = new Map(); // Maps toolCallId -> resolve function
|
|
347
|
+
// A delegated subagent can tighten these two ceilings. They are applied
|
|
348
|
+
// after every config refresh so run() cannot accidentally replace a
|
|
349
|
+
// worker's declared bounds with the parent tier defaults.
|
|
350
|
+
this.maxStepsCeiling = null;
|
|
351
|
+
this.taskTokenLimit = null;
|
|
347
352
|
|
|
348
353
|
// Limits based on tier/budget & emulator fast bypass
|
|
349
354
|
this.updateTierLimits();
|
|
@@ -719,6 +724,17 @@ export class AgentLoop {
|
|
|
719
724
|
} else {
|
|
720
725
|
this.maxSteps = 300;
|
|
721
726
|
}
|
|
727
|
+
if (Number.isInteger(this.maxStepsCeiling) && this.maxStepsCeiling > 0) {
|
|
728
|
+
this.maxSteps = Math.min(this.maxSteps, this.maxStepsCeiling);
|
|
729
|
+
}
|
|
730
|
+
if (Number.isInteger(this.taskTokenLimit) && this.taskTokenLimit > 0 && this.tokenTracker) {
|
|
731
|
+
this.tokenTracker.budget = {
|
|
732
|
+
...this.tokenTracker.budget,
|
|
733
|
+
enabled: true,
|
|
734
|
+
hardStop: true,
|
|
735
|
+
maxTokensPerTask: this.taskTokenLimit
|
|
736
|
+
};
|
|
737
|
+
}
|
|
722
738
|
this.maxCircuitBreaker = this.maxSteps;
|
|
723
739
|
}
|
|
724
740
|
|
|
@@ -1521,18 +1537,24 @@ export class AgentLoop {
|
|
|
1521
1537
|
// if it was declined, fall through to the path that needs nothing.
|
|
1522
1538
|
let visionReady = false;
|
|
1523
1539
|
let visionDeclined = false;
|
|
1540
|
+
let browserReady = false;
|
|
1541
|
+
let browserDeclined = false;
|
|
1524
1542
|
try {
|
|
1525
1543
|
const { groupStatus, isGroupDeclined } = await import('./provisioning/capabilityProvisioner.js');
|
|
1526
1544
|
visionReady = groupStatus('vision').state === 'ready';
|
|
1527
1545
|
visionDeclined = isGroupDeclined('vision');
|
|
1546
|
+
browserReady = groupStatus('browser').state === 'ready';
|
|
1547
|
+
browserDeclined = isGroupDeclined('browser');
|
|
1528
1548
|
} catch (e) { /* treat as not ready */ }
|
|
1529
1549
|
let webGuardMsg = '';
|
|
1530
1550
|
|
|
1531
|
-
if (
|
|
1532
|
-
webGuardMsg = `[ANTI-SLOP CHECK: You are building a Web/UI project and must
|
|
1551
|
+
if (!browserReady && !browserDeclined) {
|
|
1552
|
+
webGuardMsg = `[ANTI-SLOP CHECK: You are building a Web/UI project and must inspect it before finishing, but the dedicated browser capability is not enabled yet. Call \`manage_local_dependencies\` with action "grant_capability" and group "browser" to request the Causyne Camoufox runtime. If the user declines, read the final compiled HTML/CSS instead. This check fires once.]`;
|
|
1553
|
+
} else if (hasVision && !visionReady && !visionDeclined) {
|
|
1554
|
+
webGuardMsg = `[ANTI-SLOP CHECK: The browser is available, but desktop screen capture is not enabled. Call \`manage_local_dependencies\` with action "grant_capability" and group "vision" to request it, or use \`browser_act\` snapshots and its workspace-scoped screenshot action. This check fires once.]`;
|
|
1533
1555
|
} else if (hasVision && visionReady && !this.webVerificationState.screenshotTaken) {
|
|
1534
1556
|
webGuardMsg = `[ANTI-SLOP CHECK: You are building a Web/UI project but attempted to finish without VISUALLY verifying your work. Use \`screenshot\` or \`vision_inspect_ui\` to look at the UI in a browser. A human looks at their work to avoid slop. This check fires once — if there is genuinely nothing visual to inspect, say so plainly and finish.]`;
|
|
1535
|
-
} else if ((!hasVision || visionDeclined) && !this.webVerificationState.browserActUsed && !this.webVerificationState.screenshotTaken) {
|
|
1557
|
+
} else if ((!hasVision || visionDeclined) && browserReady && !this.webVerificationState.browserActUsed && !this.webVerificationState.screenshotTaken) {
|
|
1536
1558
|
webGuardMsg = `[TRANSCENDENCE CHECK: You are building a Web/UI project. As a vision-disabled model you cannot screenshot, but you MUST use \`browser_act\` to inspect the DOM, or read the final compiled HTML/CSS, to verify semantic correctness before finishing. This check fires once.]`;
|
|
1537
1559
|
}
|
|
1538
1560
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { causyneHome } from './utils/paths.js';
|
|
4
|
+
const PROFILE_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
5
|
+
const TOOL_NAME = /^[a-zA-Z][a-zA-Z0-9_.-]{0,127}$/;
|
|
6
|
+
const MAX_PROFILES = 64;
|
|
7
|
+
const MAX_PROFILE_BYTES = 24 * 1024;
|
|
8
|
+
const MAX_INSTRUCTIONS = 16 * 1024;
|
|
9
|
+
const MAX_TOOLS = 64;
|
|
10
|
+
export function agentProfilesRoot() {
|
|
11
|
+
return path.join(causyneHome(), 'agents');
|
|
12
|
+
}
|
|
13
|
+
function cleanLine(value, maximum) {
|
|
14
|
+
return String(value ?? '')
|
|
15
|
+
// eslint-disable-next-line no-control-regex
|
|
16
|
+
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
|
17
|
+
.replace(/\s+/g, ' ')
|
|
18
|
+
.trim()
|
|
19
|
+
.slice(0, maximum);
|
|
20
|
+
}
|
|
21
|
+
function cleanInstructions(value) {
|
|
22
|
+
return String(value ?? '')
|
|
23
|
+
// Preserve newlines and tabs while excluding prompt-control bytes.
|
|
24
|
+
// eslint-disable-next-line no-control-regex
|
|
25
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
function boundedInteger(value, fallback, minimum, maximum) {
|
|
29
|
+
if (value === undefined)
|
|
30
|
+
return fallback;
|
|
31
|
+
if (!Number.isInteger(value))
|
|
32
|
+
return null;
|
|
33
|
+
return Math.max(minimum, Math.min(Number(value), maximum));
|
|
34
|
+
}
|
|
35
|
+
function parseProfile(file, expectedName) {
|
|
36
|
+
try {
|
|
37
|
+
const stat = fs.lstatSync(file);
|
|
38
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
39
|
+
return { ok: false, error: 'profile must be a regular file' };
|
|
40
|
+
if (stat.size > MAX_PROFILE_BYTES)
|
|
41
|
+
return { ok: false, error: `profile exceeds ${MAX_PROFILE_BYTES / 1024} KB` };
|
|
42
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
43
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
44
|
+
return { ok: false, error: 'profile must be a JSON object' };
|
|
45
|
+
}
|
|
46
|
+
const value = raw;
|
|
47
|
+
if (value.schemaVersion !== 1)
|
|
48
|
+
return { ok: false, error: 'schemaVersion must be 1' };
|
|
49
|
+
if (value.name !== expectedName || !PROFILE_ID.test(expectedName)) {
|
|
50
|
+
return { ok: false, error: 'profile name must match its lowercase file name' };
|
|
51
|
+
}
|
|
52
|
+
const description = cleanLine(value.description, 400);
|
|
53
|
+
if (!description)
|
|
54
|
+
return { ok: false, error: 'description is required' };
|
|
55
|
+
const instructions = cleanInstructions(value.instructions);
|
|
56
|
+
if (!instructions)
|
|
57
|
+
return { ok: false, error: 'instructions are required' };
|
|
58
|
+
if (Buffer.byteLength(instructions, 'utf8') > MAX_INSTRUCTIONS) {
|
|
59
|
+
return { ok: false, error: `instructions exceed ${MAX_INSTRUCTIONS / 1024} KB` };
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(value.toolScope) || value.toolScope.length === 0 || value.toolScope.length > MAX_TOOLS) {
|
|
62
|
+
return { ok: false, error: `toolScope must contain 1-${MAX_TOOLS} tool names` };
|
|
63
|
+
}
|
|
64
|
+
const toolScope = [];
|
|
65
|
+
for (const rawTool of value.toolScope) {
|
|
66
|
+
if (typeof rawTool !== 'string' || !TOOL_NAME.test(rawTool)) {
|
|
67
|
+
return { ok: false, error: `invalid tool name ${JSON.stringify(rawTool)}` };
|
|
68
|
+
}
|
|
69
|
+
const normalized = rawTool.replace(/\./g, '_');
|
|
70
|
+
if (!toolScope.includes(normalized))
|
|
71
|
+
toolScope.push(normalized);
|
|
72
|
+
}
|
|
73
|
+
const maxSteps = boundedInteger(value.maxSteps, 25, 1, 100);
|
|
74
|
+
if (maxSteps === null)
|
|
75
|
+
return { ok: false, error: 'maxSteps must be an integer' };
|
|
76
|
+
const maxTokens = boundedInteger(value.maxTokens, 50_000, 1_000, 200_000);
|
|
77
|
+
if (maxTokens === null)
|
|
78
|
+
return { ok: false, error: 'maxTokens must be an integer' };
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
profile: {
|
|
82
|
+
schemaVersion: 1,
|
|
83
|
+
name: expectedName,
|
|
84
|
+
description,
|
|
85
|
+
instructions,
|
|
86
|
+
toolScope,
|
|
87
|
+
maxSteps,
|
|
88
|
+
maxTokens,
|
|
89
|
+
path: file
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
return { ok: false, error: message };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function loadAgentProfile(name) {
|
|
99
|
+
const id = String(name ?? '');
|
|
100
|
+
if (!PROFILE_ID.test(id))
|
|
101
|
+
return { ok: false, error: 'invalid profile name' };
|
|
102
|
+
return parseProfile(path.join(agentProfilesRoot(), `${id}.json`), id);
|
|
103
|
+
}
|
|
104
|
+
export function listAgentProfiles() {
|
|
105
|
+
const profiles = [];
|
|
106
|
+
const problems = [];
|
|
107
|
+
const root = agentProfilesRoot();
|
|
108
|
+
try {
|
|
109
|
+
if (!fs.existsSync(root))
|
|
110
|
+
return { profiles, problems };
|
|
111
|
+
const entries = fs.readdirSync(root, { withFileTypes: true })
|
|
112
|
+
.filter((entry) => entry.name.endsWith('.json') && !entry.name.startsWith('.'))
|
|
113
|
+
.slice(0, MAX_PROFILES + 1);
|
|
114
|
+
if (entries.length > MAX_PROFILES)
|
|
115
|
+
problems.push(`only the first ${MAX_PROFILES} profiles were inspected`);
|
|
116
|
+
for (const entry of entries.slice(0, MAX_PROFILES)) {
|
|
117
|
+
const name = entry.name.slice(0, -5);
|
|
118
|
+
if (!entry.isFile() || !PROFILE_ID.test(name)) {
|
|
119
|
+
problems.push(`${entry.name}: invalid profile file`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const loaded = parseProfile(path.join(root, entry.name), name);
|
|
123
|
+
if (loaded.ok)
|
|
124
|
+
profiles.push(loaded.profile);
|
|
125
|
+
else
|
|
126
|
+
problems.push(`${entry.name}: ${loaded.error}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
problems.push(`could not read profiles: ${message}`);
|
|
132
|
+
}
|
|
133
|
+
return { profiles, problems };
|
|
134
|
+
}
|
|
135
|
+
export { MAX_PROFILES, MAX_PROFILE_BYTES, MAX_INSTRUCTIONS, MAX_TOOLS };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { causyneHome } from './utils/paths.js';
|
|
4
|
+
|
|
5
|
+
const PROFILE_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
6
|
+
const TOOL_NAME = /^[a-zA-Z][a-zA-Z0-9_.-]{0,127}$/;
|
|
7
|
+
const MAX_PROFILES = 64;
|
|
8
|
+
const MAX_PROFILE_BYTES = 24 * 1024;
|
|
9
|
+
const MAX_INSTRUCTIONS = 16 * 1024;
|
|
10
|
+
const MAX_TOOLS = 64;
|
|
11
|
+
|
|
12
|
+
export interface AgentProfile {
|
|
13
|
+
schemaVersion: 1;
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
instructions: string;
|
|
17
|
+
toolScope: string[];
|
|
18
|
+
maxSteps: number;
|
|
19
|
+
maxTokens: number;
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type AgentProfileResult =
|
|
24
|
+
| { ok: true; profile: AgentProfile }
|
|
25
|
+
| { ok: false; error: string };
|
|
26
|
+
|
|
27
|
+
export function agentProfilesRoot(): string {
|
|
28
|
+
return path.join(causyneHome(), 'agents');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cleanLine(value: unknown, maximum: number): string {
|
|
32
|
+
return String(value ?? '')
|
|
33
|
+
// eslint-disable-next-line no-control-regex
|
|
34
|
+
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
|
35
|
+
.replace(/\s+/g, ' ')
|
|
36
|
+
.trim()
|
|
37
|
+
.slice(0, maximum);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function cleanInstructions(value: unknown): string {
|
|
41
|
+
return String(value ?? '')
|
|
42
|
+
// Preserve newlines and tabs while excluding prompt-control bytes.
|
|
43
|
+
// eslint-disable-next-line no-control-regex
|
|
44
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
45
|
+
.trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function boundedInteger(value: unknown, fallback: number, minimum: number, maximum: number): number | null {
|
|
49
|
+
if (value === undefined) return fallback;
|
|
50
|
+
if (!Number.isInteger(value)) return null;
|
|
51
|
+
return Math.max(minimum, Math.min(Number(value), maximum));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseProfile(file: string, expectedName: string): AgentProfileResult {
|
|
55
|
+
try {
|
|
56
|
+
const stat = fs.lstatSync(file);
|
|
57
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return { ok: false, error: 'profile must be a regular file' };
|
|
58
|
+
if (stat.size > MAX_PROFILE_BYTES) return { ok: false, error: `profile exceeds ${MAX_PROFILE_BYTES / 1024} KB` };
|
|
59
|
+
const raw: unknown = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
60
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
61
|
+
return { ok: false, error: 'profile must be a JSON object' };
|
|
62
|
+
}
|
|
63
|
+
const value = raw as Record<string, unknown>;
|
|
64
|
+
if (value.schemaVersion !== 1) return { ok: false, error: 'schemaVersion must be 1' };
|
|
65
|
+
if (value.name !== expectedName || !PROFILE_ID.test(expectedName)) {
|
|
66
|
+
return { ok: false, error: 'profile name must match its lowercase file name' };
|
|
67
|
+
}
|
|
68
|
+
const description = cleanLine(value.description, 400);
|
|
69
|
+
if (!description) return { ok: false, error: 'description is required' };
|
|
70
|
+
const instructions = cleanInstructions(value.instructions);
|
|
71
|
+
if (!instructions) return { ok: false, error: 'instructions are required' };
|
|
72
|
+
if (Buffer.byteLength(instructions, 'utf8') > MAX_INSTRUCTIONS) {
|
|
73
|
+
return { ok: false, error: `instructions exceed ${MAX_INSTRUCTIONS / 1024} KB` };
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(value.toolScope) || value.toolScope.length === 0 || value.toolScope.length > MAX_TOOLS) {
|
|
76
|
+
return { ok: false, error: `toolScope must contain 1-${MAX_TOOLS} tool names` };
|
|
77
|
+
}
|
|
78
|
+
const toolScope: string[] = [];
|
|
79
|
+
for (const rawTool of value.toolScope) {
|
|
80
|
+
if (typeof rawTool !== 'string' || !TOOL_NAME.test(rawTool)) {
|
|
81
|
+
return { ok: false, error: `invalid tool name ${JSON.stringify(rawTool)}` };
|
|
82
|
+
}
|
|
83
|
+
const normalized = rawTool.replace(/\./g, '_');
|
|
84
|
+
if (!toolScope.includes(normalized)) toolScope.push(normalized);
|
|
85
|
+
}
|
|
86
|
+
const maxSteps = boundedInteger(value.maxSteps, 25, 1, 100);
|
|
87
|
+
if (maxSteps === null) return { ok: false, error: 'maxSteps must be an integer' };
|
|
88
|
+
const maxTokens = boundedInteger(value.maxTokens, 50_000, 1_000, 200_000);
|
|
89
|
+
if (maxTokens === null) return { ok: false, error: 'maxTokens must be an integer' };
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
profile: {
|
|
94
|
+
schemaVersion: 1,
|
|
95
|
+
name: expectedName,
|
|
96
|
+
description,
|
|
97
|
+
instructions,
|
|
98
|
+
toolScope,
|
|
99
|
+
maxSteps,
|
|
100
|
+
maxTokens,
|
|
101
|
+
path: file
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
+
return { ok: false, error: message };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function loadAgentProfile(name: unknown): AgentProfileResult {
|
|
111
|
+
const id = String(name ?? '');
|
|
112
|
+
if (!PROFILE_ID.test(id)) return { ok: false, error: 'invalid profile name' };
|
|
113
|
+
return parseProfile(path.join(agentProfilesRoot(), `${id}.json`), id);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function listAgentProfiles(): { profiles: AgentProfile[]; problems: string[] } {
|
|
117
|
+
const profiles: AgentProfile[] = [];
|
|
118
|
+
const problems: string[] = [];
|
|
119
|
+
const root = agentProfilesRoot();
|
|
120
|
+
try {
|
|
121
|
+
if (!fs.existsSync(root)) return { profiles, problems };
|
|
122
|
+
const entries = fs.readdirSync(root, { withFileTypes: true })
|
|
123
|
+
.filter((entry) => entry.name.endsWith('.json') && !entry.name.startsWith('.'))
|
|
124
|
+
.slice(0, MAX_PROFILES + 1);
|
|
125
|
+
if (entries.length > MAX_PROFILES) problems.push(`only the first ${MAX_PROFILES} profiles were inspected`);
|
|
126
|
+
for (const entry of entries.slice(0, MAX_PROFILES)) {
|
|
127
|
+
const name = entry.name.slice(0, -5);
|
|
128
|
+
if (!entry.isFile() || !PROFILE_ID.test(name)) {
|
|
129
|
+
problems.push(`${entry.name}: invalid profile file`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const loaded = parseProfile(path.join(root, entry.name), name);
|
|
133
|
+
if (loaded.ok) profiles.push(loaded.profile);
|
|
134
|
+
else problems.push(`${entry.name}: ${loaded.error}`);
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
138
|
+
problems.push(`could not read profiles: ${message}`);
|
|
139
|
+
}
|
|
140
|
+
return { profiles, problems };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { MAX_PROFILES, MAX_PROFILE_BYTES, MAX_INSTRUCTIONS, MAX_TOOLS };
|