@causyne/agent 1.10.17 → 1.10.18
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 +25 -8
- package/docs/SECURITY_REVIEW.md +4 -4
- package/docs/THIRD_PARTY_NOTICES.md +3 -0
- package/package.json +1 -1
- package/src/agentLoop.js +99 -19
- package/src/browser/{camofoxRuntime.js → browserRuntime.js} +10 -15
- package/src/browser/{camofoxRuntime.ts → browserRuntime.ts} +11 -16
- package/src/browser/causyneBrowser.js +6 -6
- package/src/browser/causyneBrowser.ts +9 -9
- package/src/cliMode.js +13 -1
- package/src/decisionPolicy.js +68 -0
- package/src/decisionPolicy.ts +106 -0
- package/src/implementationPlan.js +50 -0
- package/src/implementationPlan.ts +59 -0
- package/src/provisioning/capabilityProvisioner.js +1 -1
- package/src/taskShape.js +1 -1
- package/src/taskShape.ts +1 -1
- package/src/toolPacks.js +7 -5
- package/src/tools/browserAutomator.js +1 -1
- package/src/tools/browserAutomator.ts +1 -1
- package/src/tools/manageLocalAiDependencies.js +1 -1
- package/src/tools/registry.js +4 -3
- package/src/tools/requestDecision.js +87 -0
- package/src/tools/requestDecision.ts +107 -0
- package/src/tools/updatePlan.js +37 -84
- package/src/tools/updatePlan.ts +48 -0
- package/src/tools/webFetch.js +1 -1
- package/src/tools/webFetch.ts +1 -1
- package/src/tools/webSearch.js +1 -1
- package/src/tools/webSearch.ts +1 -1
- package/src/tui/ask.js +41 -1
- package/src/tui/chatRenderer.js +28 -0
- package/src/tui/dialogs.js +91 -0
- package/src/tui/prompt.js +32 -3
- package/src/tui/toolDisplay.js +0 -1
- package/src/tools/todoWrite.js +0 -75
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -31,7 +31,7 @@ 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`, `agentProfiles.ts`, `subagentJournal.ts`, `modelRouter.js`, `tokenTracker.js` |
|
|
34
|
+
| Orchestration and budgets | `agentLoop.js`, `decisionPolicy.ts`, `implementationPlan.ts`, `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/` |
|
|
@@ -49,6 +49,20 @@ Runtime validation is still required at model, filesystem, subprocess and
|
|
|
49
49
|
extension boundaries. Rust is reserved for a measured, narrow native worker;
|
|
50
50
|
no Rust rewrite is underway.
|
|
51
51
|
|
|
52
|
+
## Planning and durable decisions
|
|
53
|
+
|
|
54
|
+
Complex implementation uses `update_plan` to maintain a validated checklist in
|
|
55
|
+
session state. The TUI keeps the current steps visible beside the input surface;
|
|
56
|
+
plans are not written into the user's project. A mutating tool call on a complex
|
|
57
|
+
task is held until the model has created this plan.
|
|
58
|
+
|
|
59
|
+
`request_decision` handles choices with durable product, architecture, data,
|
|
60
|
+
security, or workflow consequences. It presents two to four concrete options
|
|
61
|
+
and a custom-answer path. This checkpoint is distinct from a tool permission:
|
|
62
|
+
selecting a design never grants filesystem or command access. The checkpoint
|
|
63
|
+
runs in the provider-independent agent loop, so Mode C can remove routine
|
|
64
|
+
approval friction without silently choosing an unresolved architecture.
|
|
65
|
+
|
|
52
66
|
## Tool and extension boundaries
|
|
53
67
|
|
|
54
68
|
Tool schemas are assembled once from native tools, explicitly enabled plugins,
|
|
@@ -92,16 +106,19 @@ browser OAuth is not implemented yet; an HTTP 401 is surfaced as
|
|
|
92
106
|
|
|
93
107
|
Causyne's web foundation has two layers. Lightweight TypeScript search and
|
|
94
108
|
fetch clients work without a browser runtime. Rendered navigation uses a
|
|
95
|
-
dedicated in-process
|
|
96
|
-
|
|
109
|
+
dedicated in-process Causyne Browser adapter built from a locally reviewed
|
|
110
|
+
upstream browser engine. Required upstream names and licenses remain in the
|
|
111
|
+
third-party notices. The adapter is deliberately smaller than the reference
|
|
112
|
+
service:
|
|
97
113
|
it does not import an HTTP server, inbound port, cloud deployment layer, VNC,
|
|
98
114
|
telemetry, remote WebSocket control, or OpenClaw integration.
|
|
99
115
|
|
|
100
|
-
|
|
101
|
-
into the user's private Causyne capability directory only after an
|
|
102
|
-
browser capability grant. Causyne does not download a browser binary. Advanced
|
|
103
|
-
rendered browsing requires a separately installed
|
|
104
|
-
`
|
|
116
|
+
The browser engine and Playwright are exact-pinned optional capability packages
|
|
117
|
+
installed into the user's private Causyne capability directory only after an
|
|
118
|
+
explicit browser capability grant. Causyne does not download a browser binary. Advanced
|
|
119
|
+
rendered browsing requires a separately installed supported browser runtime
|
|
120
|
+
selected by `CAUSYNE_BROWSER_EXECUTABLE` or discovered from a standard system
|
|
121
|
+
location.
|
|
105
122
|
The main npm package therefore stays small and web search remains useful when
|
|
106
123
|
the optional rendered browser is absent.
|
|
107
124
|
|
package/docs/SECURITY_REVIEW.md
CHANGED
|
@@ -66,12 +66,12 @@ The active source takes a different architecture and distribution path:
|
|
|
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
68
|
- Web search uses bounded HTTP providers without requiring a browser binary.
|
|
69
|
-
Interactive rendered browsing uses an outbound, in-process
|
|
70
|
-
Causyne does not run
|
|
69
|
+
Interactive rendered browsing uses an outbound, in-process Causyne Browser adapter.
|
|
70
|
+
Causyne does not run an inbound browser-control server, inbound port, remote control,
|
|
71
71
|
VNC, telemetry, cloud deployment, or OpenClaw integration. Optional launcher
|
|
72
72
|
packages require an explicit capability grant. Causyne never downloads the
|
|
73
|
-
|
|
74
|
-
through `
|
|
73
|
+
browser runtime; an existing external installation must be selected
|
|
74
|
+
through `CAUSYNE_BROWSER_EXECUTABLE` or a standard system location.
|
|
75
75
|
- Browser navigation and subresources accept only public HTTP(S) destinations.
|
|
76
76
|
Hostnames are resolved and checked against private, loopback, link-local,
|
|
77
77
|
multicast, reserved, local, internal, and metadata ranges. Non-browser
|
|
@@ -27,6 +27,9 @@ No dependency in the reviewed main lockfile was marked with an install script.
|
|
|
27
27
|
Optional capability dependencies are installed only after approval into a
|
|
28
28
|
separate local environment and are not covered by the main lockfile review.
|
|
29
29
|
|
|
30
|
+
The product-facing browser capability is **Causyne Browser**. Upstream names
|
|
31
|
+
below are used only for required attribution and exact dependency identifiers.
|
|
32
|
+
|
|
30
33
|
The optional browser capability installs `camoufox-js` 0.11.5 under MPL-2.0
|
|
31
34
|
and `playwright-core` 1.58.0 under Apache-2.0. They remain separate packages in
|
|
32
35
|
the user's local capability directory and are not copied into Causyne Agent's
|
package/package.json
CHANGED
package/src/agentLoop.js
CHANGED
|
@@ -20,6 +20,7 @@ import { analyzeTask, buildTaskBrief, packsFromShape } from './taskShape.js';
|
|
|
20
20
|
import { lookupPlan, recordPlan, recordMiss, renderPlanHint, toStep } from './planCache.js';
|
|
21
21
|
import { routedConfig } from './modelRouter.js';
|
|
22
22
|
import { assessStep, isThrashing } from './uncertainty.js';
|
|
23
|
+
import { assessDecisionRequirement, isMutatingToolCall, requiresImplementationPlan } from './decisionPolicy.js';
|
|
23
24
|
import { VFETracker } from './vfeTracker.js';
|
|
24
25
|
import {
|
|
25
26
|
routePacks,
|
|
@@ -75,11 +76,15 @@ Specifically, you excel at:
|
|
|
75
76
|
- **No Chitchat**: Tools are for actions, text output is *only* for communication. Do not add explanatory comments within tool calls or code blocks.
|
|
76
77
|
- **File Paths**: ALWAYS output the absolute path of any file you create, modify, or generate so the user knows exactly where it is.
|
|
77
78
|
|
|
78
|
-
# Task Management & Planning
|
|
79
|
-
-
|
|
79
|
+
# Task Management & Planning
|
|
80
|
+
- Use update_plan to create a visible implementation checklist before complex implementation. Create atomic, verb-led, verifiable steps and keep exactly one step in progress.
|
|
80
81
|
- Check off completed TODOs immediately before reporting progress. Do not batch up multiple task completions.
|
|
81
82
|
- Reconcile and close the todo list when all goal tasks are done. If the user asks you to implement, do not output a separate text-based plan. Just build and display the todo list.
|
|
82
83
|
|
|
84
|
+
# Durable Decisions
|
|
85
|
+
- Use request_decision when multiple materially different architecture, product, data, security, or workflow directions are valid. Give 2-4 concrete options with consequences; the UI adds a custom-answer choice.
|
|
86
|
+
- A decision is separate from permission. Do not ask for routine implementation details that can be resolved from the repository. Even in Mode C, pause for an unresolved durable architecture decision before changing the system.
|
|
87
|
+
|
|
83
88
|
# Maximize Parallel Tool Calls (Speed is Key)
|
|
84
89
|
- CRITICAL INSTRUCTION: For maximum efficiency, whenever you perform multiple operations, invoke all relevant tools concurrently (in parallel) rather than sequentially.
|
|
85
90
|
- Searching for different patterns (imports, usage, definitions) MUST happen in parallel. Reading multiple files MUST happen in parallel.
|
|
@@ -344,6 +349,12 @@ export class AgentLoop {
|
|
|
344
349
|
this.circuitBreakerUnlocked = false;
|
|
345
350
|
this.stopRequested = false;
|
|
346
351
|
this.pendingApprovals = new Map(); // Maps toolCallId -> resolve function
|
|
352
|
+
this.waitForDecision = async () => null;
|
|
353
|
+
this._decisionAnswers = new Map();
|
|
354
|
+
this._decisionRequirement = { required: false, kind: 'architecture', reason: '' };
|
|
355
|
+
this._decisionResolved = true;
|
|
356
|
+
this._implementationPlan = [];
|
|
357
|
+
this._implementationPlanRequired = false;
|
|
347
358
|
// A delegated subagent can tighten these two ceilings. They are applied
|
|
348
359
|
// after every config refresh so run() cannot accidentally replace a
|
|
349
360
|
// worker's declared bounds with the parent tier defaults.
|
|
@@ -700,6 +711,58 @@ export class AgentLoop {
|
|
|
700
711
|
}
|
|
701
712
|
}
|
|
702
713
|
|
|
714
|
+
updateImplementationPlan(update) {
|
|
715
|
+
this._implementationPlan = update.plan.map(item => ({ ...item }));
|
|
716
|
+
this.emitEvent({
|
|
717
|
+
type: 'agent_plan',
|
|
718
|
+
explanation: update.explanation || '',
|
|
719
|
+
plan: this._implementationPlan.map(item => ({ status: item.status, text: item.step, id: item.id }))
|
|
720
|
+
});
|
|
721
|
+
auditLog.log({
|
|
722
|
+
event: 'implementation_plan_updated', taskId: this.taskId,
|
|
723
|
+
steps: this._implementationPlan.length,
|
|
724
|
+
completed: this._implementationPlan.filter(item => item.status === 'completed').length
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async requestDecision(request) {
|
|
729
|
+
const fingerprint = createHash('sha256')
|
|
730
|
+
.update(JSON.stringify({ kind: request.kind, question: request.question, options: request.options.map(option => option.id) }))
|
|
731
|
+
.digest('hex');
|
|
732
|
+
const remembered = this._decisionAnswers.get(fingerprint);
|
|
733
|
+
if (remembered) return remembered;
|
|
734
|
+
|
|
735
|
+
this.emitEvent({ type: 'decision_request', request });
|
|
736
|
+
auditLog.log({ event: 'decision_requested', taskId: this.taskId, kind: request.kind, optionCount: request.options.length });
|
|
737
|
+
const answer = await this.waitForDecision(request);
|
|
738
|
+
if (!answer || typeof answer.label !== 'string' || !answer.label.trim()) {
|
|
739
|
+
this.emitEvent({ type: 'decision_result', request, answer: null });
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
const normalized = {
|
|
743
|
+
requestId: request.id,
|
|
744
|
+
optionId: typeof answer.optionId === 'string' ? answer.optionId : null,
|
|
745
|
+
label: answer.label.trim(),
|
|
746
|
+
custom: answer.custom === true
|
|
747
|
+
};
|
|
748
|
+
this._decisionAnswers.set(fingerprint, normalized);
|
|
749
|
+
if (request.kind === 'architecture') this._decisionResolved = true;
|
|
750
|
+
this.emitEvent({ type: 'decision_result', request, answer: normalized });
|
|
751
|
+
auditLog.log({ event: 'decision_answered', taskId: this.taskId, kind: request.kind, custom: normalized.custom });
|
|
752
|
+
return normalized;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
mutationCheckpoint(functionName, args = {}) {
|
|
756
|
+
if (!isMutatingToolCall(functionName, args)) return null;
|
|
757
|
+
if (this._decisionRequirement.required && !this._decisionResolved) {
|
|
758
|
+
return 'ARCHITECTURE_DECISION_REQUIRED: Before changing the system, call request_decision with kind "architecture", 2-4 concrete options, their consequences, and a clear reason. Do not ask about routine details; this checkpoint exists because the current request crosses a durable architecture boundary.';
|
|
759
|
+
}
|
|
760
|
+
if (this._implementationPlanRequired && this._implementationPlan.length === 0) {
|
|
761
|
+
return 'IMPLEMENTATION_PLAN_REQUIRED: Call update_plan with atomic, verifiable steps and exactly one in_progress step before mutating this complex task.';
|
|
762
|
+
}
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
|
|
703
766
|
updateTierLimits() {
|
|
704
767
|
this.config = loadConfig();
|
|
705
768
|
if (typeof this.initializeProvider === 'function') {
|
|
@@ -816,10 +879,26 @@ export class AgentLoop {
|
|
|
816
879
|
this._lessonSimilarity = 0;
|
|
817
880
|
this._lastToolResult = null;
|
|
818
881
|
this._paradigmShiftPending = false;
|
|
882
|
+
this._implementationPlan = [];
|
|
883
|
+
this._decisionAnswers = new Map();
|
|
884
|
+
this._decisionRequirement = assessDecisionRequirement(this.userPrompt || '');
|
|
885
|
+
this._decisionResolved = !this._decisionRequirement.required;
|
|
886
|
+
this._implementationPlanRequired = false;
|
|
819
887
|
if (this.vfeTracker) this.vfeTracker.reset();
|
|
820
888
|
|
|
821
889
|
if (this.userPrompt) {
|
|
822
890
|
this.taskShape = analyzeTask(this.userPrompt, this.workspaceContext);
|
|
891
|
+
this._implementationPlanRequired = requiresImplementationPlan(
|
|
892
|
+
this.userPrompt,
|
|
893
|
+
this.taskShape?.archetype || '',
|
|
894
|
+
this.taskShape?.confidence || ''
|
|
895
|
+
);
|
|
896
|
+
// request_decision is comparatively large and most tasks never need it.
|
|
897
|
+
// On providers without server-side schema deferral, expose it from the
|
|
898
|
+
// first model call only when the deterministic task policy found an
|
|
899
|
+
// unresolved durable choice. This keeps the resident prompt within its
|
|
900
|
+
// measured budget without making the checkpoint undiscoverable.
|
|
901
|
+
if (this._decisionRequirement.required) this.unlockedTools.add('request_decision');
|
|
823
902
|
|
|
824
903
|
// Scored routing, with the regex router as a safety net so a pack can
|
|
825
904
|
// never be missed just because it scored below threshold.
|
|
@@ -1077,9 +1156,8 @@ export class AgentLoop {
|
|
|
1077
1156
|
// `tool_search_tool_regex`) keep the whole core in context for free —
|
|
1078
1157
|
// the API holds the schemas server-side and expands them on demand,
|
|
1079
1158
|
// with no discovery round trip. Every OTHER provider was serialising
|
|
1080
|
-
// all 24 core schemas into every request
|
|
1081
|
-
//
|
|
1082
|
-
// tokens) and let `tool_search` — the same mechanism that already
|
|
1159
|
+
// all 24 core schemas into every request. There, ship the small resident
|
|
1160
|
+
// set and let `tool_search` — the same mechanism that already
|
|
1083
1161
|
// unlocks packs — fetch the rest.
|
|
1084
1162
|
const residentOnly = this.llm.supportsToolSearch?.() !== true;
|
|
1085
1163
|
let dynamicTools = await getDynamicTools(this.config.autonomyMode, this.llm.supportsVision, {
|
|
@@ -1408,18 +1486,6 @@ export class AgentLoop {
|
|
|
1408
1486
|
this.messages.push(assistantMsg);
|
|
1409
1487
|
|
|
1410
1488
|
if (response.content) {
|
|
1411
|
-
// V5: Parse TodoWrite checklist for visual plan display
|
|
1412
|
-
const planRegex = /-\s*\[([ xX\/])\]\s*(.*)/g;
|
|
1413
|
-
const planMatch = [...response.content.matchAll(planRegex)];
|
|
1414
|
-
if (planMatch.length > 0) {
|
|
1415
|
-
const planTasks = planMatch.map(m => {
|
|
1416
|
-
const marker = m[1].toLowerCase();
|
|
1417
|
-
const status = marker === 'x' ? 'completed' : (marker === '/' ? 'in_progress' : 'pending');
|
|
1418
|
-
return { status, text: m[2].trim() };
|
|
1419
|
-
});
|
|
1420
|
-
this.emitEvent({ type: 'agent_plan', plan: planTasks });
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
1489
|
this.emitEvent({ type: 'agent_message', content: response.content });
|
|
1424
1490
|
}
|
|
1425
1491
|
|
|
@@ -1549,7 +1615,7 @@ export class AgentLoop {
|
|
|
1549
1615
|
let webGuardMsg = '';
|
|
1550
1616
|
|
|
1551
1617
|
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
|
|
1618
|
+
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 Browser runtime. If the user declines, read the final compiled HTML/CSS instead. This check fires once.]`;
|
|
1553
1619
|
} else if (hasVision && !visionReady && !visionDeclined) {
|
|
1554
1620
|
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.]`;
|
|
1555
1621
|
} else if (hasVision && visionReady && !this.webVerificationState.screenshotTaken) {
|
|
@@ -1803,6 +1869,20 @@ export class AgentLoop {
|
|
|
1803
1869
|
continue;
|
|
1804
1870
|
}
|
|
1805
1871
|
|
|
1872
|
+
const checkpointError = this.mutationCheckpoint(functionName, args);
|
|
1873
|
+
if (checkpointError) {
|
|
1874
|
+
this.messages.push({
|
|
1875
|
+
role: 'tool',
|
|
1876
|
+
tool_call_id: toolCall.id,
|
|
1877
|
+
name: functionName,
|
|
1878
|
+
content: JSON.stringify({
|
|
1879
|
+
success: false,
|
|
1880
|
+
error: checkpointError
|
|
1881
|
+
})
|
|
1882
|
+
});
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1806
1886
|
// A policy hook runs after the built-in sandbox but before any user
|
|
1807
1887
|
// approval prompt, so a deterministic denial never asks the user to
|
|
1808
1888
|
// approve work that will be rejected anyway.
|
|
@@ -2266,7 +2346,7 @@ export class AgentLoop {
|
|
|
2266
2346
|
//
|
|
2267
2347
|
// Only payloads worth the trade are folded. A small `{"success":true}`
|
|
2268
2348
|
// ack costs less than the pointer that would replace it, and every
|
|
2269
|
-
//
|
|
2349
|
+
// Plan updates would otherwise collide with unrelated plan updates.
|
|
2270
2350
|
const OBSERVATION_FOLD_FLOOR = 600;
|
|
2271
2351
|
if (resultString.length >= OBSERVATION_FOLD_FLOOR) {
|
|
2272
2352
|
const digest = createHash('sha256').update(resultString).digest('hex');
|
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
function assertExecutable(candidate) {
|
|
5
5
|
const stat = statSync(candidate);
|
|
6
6
|
if (!stat.isFile() && !stat.isSymbolicLink()) {
|
|
7
|
-
throw new Error(`
|
|
7
|
+
throw new Error(`Causyne Browser executable is not a file: ${candidate}`);
|
|
8
8
|
}
|
|
9
9
|
if (process.platform !== 'win32')
|
|
10
10
|
accessSync(candidate, constants.X_OK);
|
|
@@ -61,33 +61,28 @@ function resourceDirectoryFor(executablePath) {
|
|
|
61
61
|
}
|
|
62
62
|
return path.dirname(executablePath);
|
|
63
63
|
}
|
|
64
|
-
export function
|
|
65
|
-
const configured = process.env.
|
|
66
|
-
const
|
|
67
|
-
const candidates = configured
|
|
68
|
-
? [path.resolve(configured)]
|
|
69
|
-
: configuredHome
|
|
70
|
-
? [expectedExecutable(path.resolve(configuredHome))]
|
|
71
|
-
: candidateExecutables();
|
|
64
|
+
export function resolveBrowserRuntime() {
|
|
65
|
+
const configured = process.env.CAUSYNE_BROWSER_EXECUTABLE?.trim();
|
|
66
|
+
const candidates = configured ? [path.resolve(configured)] : candidateExecutables();
|
|
72
67
|
const executablePath = candidates.find(existsSync);
|
|
73
68
|
if (!executablePath) {
|
|
74
|
-
throw new Error('
|
|
75
|
-
'Install a
|
|
69
|
+
throw new Error('Causyne Browser runtime is not installed. Causyne does not download browser binaries automatically. ' +
|
|
70
|
+
'Install a supported browser runtime separately, then set CAUSYNE_BROWSER_EXECUTABLE to its executable path.');
|
|
76
71
|
}
|
|
77
72
|
assertExecutable(executablePath);
|
|
78
73
|
const resolvedExecutable = realpathSync(executablePath);
|
|
79
74
|
const installDirectory = findInstallDirectory(resolvedExecutable);
|
|
80
75
|
if (!installDirectory) {
|
|
81
|
-
throw new Error(`
|
|
82
|
-
'Keep
|
|
76
|
+
throw new Error(`Causyne Browser runtime at ${resolvedExecutable} is not a complete distribution. ` +
|
|
77
|
+
'Keep version.json beside its platform bundle and point CAUSYNE_BROWSER_EXECUTABLE at that bundle.');
|
|
83
78
|
}
|
|
84
79
|
const resourceDirectory = resourceDirectoryFor(resolvedExecutable);
|
|
85
80
|
if (!existsSync(path.join(resourceDirectory, 'properties.json'))) {
|
|
86
|
-
throw new Error(`
|
|
81
|
+
throw new Error(`Causyne Browser runtime metadata was not found in ${resourceDirectory}.`);
|
|
87
82
|
}
|
|
88
83
|
return {
|
|
89
84
|
installDirectory,
|
|
90
85
|
resourceDirectory,
|
|
91
|
-
source: configured
|
|
86
|
+
source: configured ? 'environment' : 'system'
|
|
92
87
|
};
|
|
93
88
|
}
|
|
@@ -6,7 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
// discovery at commit 2c0036a8b18bf9cd983376751ff4d52dfd9add0f. Causyne
|
|
7
7
|
// keeps this integration layer in TypeScript and never downloads a browser.
|
|
8
8
|
|
|
9
|
-
export interface
|
|
9
|
+
export interface PreparedBrowserRuntime {
|
|
10
10
|
installDirectory: string;
|
|
11
11
|
resourceDirectory: string;
|
|
12
12
|
source: 'environment' | 'system';
|
|
@@ -15,7 +15,7 @@ export interface PreparedCamoufoxRuntime {
|
|
|
15
15
|
function assertExecutable(candidate: string): void {
|
|
16
16
|
const stat = statSync(candidate);
|
|
17
17
|
if (!stat.isFile() && !stat.isSymbolicLink()) {
|
|
18
|
-
throw new Error(`
|
|
18
|
+
throw new Error(`Causyne Browser executable is not a file: ${candidate}`);
|
|
19
19
|
}
|
|
20
20
|
if (process.platform !== 'win32') accessSync(candidate, constants.X_OK);
|
|
21
21
|
}
|
|
@@ -74,19 +74,14 @@ function resourceDirectoryFor(executablePath: string): string {
|
|
|
74
74
|
return path.dirname(executablePath);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export function
|
|
78
|
-
const configured = process.env.
|
|
79
|
-
const
|
|
80
|
-
const candidates = configured
|
|
81
|
-
? [path.resolve(configured)]
|
|
82
|
-
: configuredHome
|
|
83
|
-
? [expectedExecutable(path.resolve(configuredHome))]
|
|
84
|
-
: candidateExecutables();
|
|
77
|
+
export function resolveBrowserRuntime(): PreparedBrowserRuntime {
|
|
78
|
+
const configured = process.env.CAUSYNE_BROWSER_EXECUTABLE?.trim();
|
|
79
|
+
const candidates = configured ? [path.resolve(configured)] : candidateExecutables();
|
|
85
80
|
const executablePath = candidates.find(existsSync);
|
|
86
81
|
if (!executablePath) {
|
|
87
82
|
throw new Error(
|
|
88
|
-
'
|
|
89
|
-
'Install a
|
|
83
|
+
'Causyne Browser runtime is not installed. Causyne does not download browser binaries automatically. ' +
|
|
84
|
+
'Install a supported browser runtime separately, then set CAUSYNE_BROWSER_EXECUTABLE to its executable path.'
|
|
90
85
|
);
|
|
91
86
|
}
|
|
92
87
|
|
|
@@ -95,18 +90,18 @@ export function resolveCamoufoxRuntime(): PreparedCamoufoxRuntime {
|
|
|
95
90
|
const installDirectory = findInstallDirectory(resolvedExecutable);
|
|
96
91
|
if (!installDirectory) {
|
|
97
92
|
throw new Error(
|
|
98
|
-
`
|
|
99
|
-
'Keep
|
|
93
|
+
`Causyne Browser runtime at ${resolvedExecutable} is not a complete distribution. ` +
|
|
94
|
+
'Keep version.json beside its platform bundle and point CAUSYNE_BROWSER_EXECUTABLE at that bundle.'
|
|
100
95
|
);
|
|
101
96
|
}
|
|
102
97
|
const resourceDirectory = resourceDirectoryFor(resolvedExecutable);
|
|
103
98
|
if (!existsSync(path.join(resourceDirectory, 'properties.json'))) {
|
|
104
|
-
throw new Error(`
|
|
99
|
+
throw new Error(`Causyne Browser runtime metadata was not found in ${resourceDirectory}.`);
|
|
105
100
|
}
|
|
106
101
|
|
|
107
102
|
return {
|
|
108
103
|
installDirectory,
|
|
109
104
|
resourceDirectory,
|
|
110
|
-
source: configured
|
|
105
|
+
source: configured ? 'environment' : 'system'
|
|
111
106
|
};
|
|
112
107
|
}
|
|
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { capabilityDirectory, ensureCapability, loadCapabilityPackage } from '../provisioning/capabilityProvisioner.js';
|
|
5
|
-
import {
|
|
5
|
+
import { resolveBrowserRuntime } from './browserRuntime.js';
|
|
6
6
|
import { assertPublicWebUrl } from './urlPolicy.js';
|
|
7
7
|
import { windowSnapshot } from './snapshotWindow.js';
|
|
8
8
|
const NAVIGATION_TIMEOUT_MS = 30_000;
|
|
@@ -113,7 +113,7 @@ const SEARCH_EXTRACT_SCRIPT = `(() => {
|
|
|
113
113
|
export class CausyneBrowser {
|
|
114
114
|
workspace;
|
|
115
115
|
ensureBrowserCapability;
|
|
116
|
-
|
|
116
|
+
loadBrowserEngine;
|
|
117
117
|
resolveRuntime;
|
|
118
118
|
profileDirectory;
|
|
119
119
|
visible;
|
|
@@ -129,8 +129,8 @@ export class CausyneBrowser {
|
|
|
129
129
|
constructor(options = {}) {
|
|
130
130
|
this.workspace = path.resolve(options.workspace ?? process.cwd());
|
|
131
131
|
this.ensureBrowserCapability = options.ensureBrowserCapability ?? (() => ensureCapability('browser'));
|
|
132
|
-
this.
|
|
133
|
-
this.resolveRuntime = options.resolveRuntime ??
|
|
132
|
+
this.loadBrowserEngine = options.loadBrowserEngine ?? (async () => await loadCapabilityPackage('camoufox-js'));
|
|
133
|
+
this.resolveRuntime = options.resolveRuntime ?? resolveBrowserRuntime;
|
|
134
134
|
this.profileDirectory = path.resolve(options.profileDirectory ?? path.join(capabilityDirectory(), 'browser-profile'));
|
|
135
135
|
this.visible = options.visible ?? process.env.CAUSYNE_BROWSER_VISIBLE === '1';
|
|
136
136
|
this.validateUrl = options.validateUrl ?? assertPublicWebUrl;
|
|
@@ -215,9 +215,9 @@ export class CausyneBrowser {
|
|
|
215
215
|
const runtime = this.resolveRuntime();
|
|
216
216
|
process.env.CAMOUFOX_INSTALL_DIR = runtime.installDirectory;
|
|
217
217
|
await fs.mkdir(this.profileDirectory, { recursive: true, mode: 0o700 });
|
|
218
|
-
const module = await this.
|
|
218
|
+
const module = await this.loadBrowserEngine();
|
|
219
219
|
if (!module || typeof module.Camoufox !== 'function')
|
|
220
|
-
throw new Error('
|
|
220
|
+
throw new Error('Causyne Browser has no usable launcher');
|
|
221
221
|
this.session = await module.Camoufox({
|
|
222
222
|
headless: !this.visible,
|
|
223
223
|
user_data_dir: this.profileDirectory,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
ensureCapability,
|
|
8
8
|
loadCapabilityPackage
|
|
9
9
|
} from '../provisioning/capabilityProvisioner.js';
|
|
10
|
-
import {
|
|
10
|
+
import { resolveBrowserRuntime, type PreparedBrowserRuntime } from './browserRuntime.js';
|
|
11
11
|
import { assertPublicWebUrl } from './urlPolicy.js';
|
|
12
12
|
import { windowSnapshot, type SnapshotWindow } from './snapshotWindow.js';
|
|
13
13
|
|
|
@@ -76,7 +76,7 @@ interface BrowserSession {
|
|
|
76
76
|
on?(event: 'page', handler: (page: BrowserPage) => void): void;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
interface
|
|
79
|
+
interface BrowserEngineModule {
|
|
80
80
|
Camoufox(options: Record<string, unknown>): Promise<BrowserSession>;
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -132,8 +132,8 @@ export interface BrowserActionResult {
|
|
|
132
132
|
interface RuntimeOptions {
|
|
133
133
|
workspace?: string;
|
|
134
134
|
ensureBrowserCapability?: () => Promise<{ ok: boolean; reason?: string; requiresConsent?: boolean }>;
|
|
135
|
-
|
|
136
|
-
resolveRuntime?: () =>
|
|
135
|
+
loadBrowserEngine?: () => Promise<BrowserEngineModule>;
|
|
136
|
+
resolveRuntime?: () => PreparedBrowserRuntime;
|
|
137
137
|
profileDirectory?: string;
|
|
138
138
|
visible?: boolean;
|
|
139
139
|
validateUrl?: (value: string) => Promise<URL>;
|
|
@@ -240,7 +240,7 @@ const SEARCH_EXTRACT_SCRIPT = `(() => {
|
|
|
240
240
|
export class CausyneBrowser {
|
|
241
241
|
private readonly workspace: string;
|
|
242
242
|
private readonly ensureBrowserCapability: NonNullable<RuntimeOptions['ensureBrowserCapability']>;
|
|
243
|
-
private readonly
|
|
243
|
+
private readonly loadBrowserEngine: NonNullable<RuntimeOptions['loadBrowserEngine']>;
|
|
244
244
|
private readonly resolveRuntime: NonNullable<RuntimeOptions['resolveRuntime']>;
|
|
245
245
|
private readonly profileDirectory: string;
|
|
246
246
|
private readonly visible: boolean;
|
|
@@ -257,8 +257,8 @@ export class CausyneBrowser {
|
|
|
257
257
|
constructor(options: RuntimeOptions = {}) {
|
|
258
258
|
this.workspace = path.resolve(options.workspace ?? process.cwd());
|
|
259
259
|
this.ensureBrowserCapability = options.ensureBrowserCapability ?? (() => ensureCapability('browser'));
|
|
260
|
-
this.
|
|
261
|
-
this.resolveRuntime = options.resolveRuntime ??
|
|
260
|
+
this.loadBrowserEngine = options.loadBrowserEngine ?? (async () => await loadCapabilityPackage('camoufox-js') as BrowserEngineModule);
|
|
261
|
+
this.resolveRuntime = options.resolveRuntime ?? resolveBrowserRuntime;
|
|
262
262
|
this.profileDirectory = path.resolve(options.profileDirectory ?? path.join(capabilityDirectory(), 'browser-profile'));
|
|
263
263
|
this.visible = options.visible ?? process.env.CAUSYNE_BROWSER_VISIBLE === '1';
|
|
264
264
|
this.validateUrl = options.validateUrl ?? assertPublicWebUrl;
|
|
@@ -340,8 +340,8 @@ export class CausyneBrowser {
|
|
|
340
340
|
const runtime = this.resolveRuntime();
|
|
341
341
|
process.env.CAMOUFOX_INSTALL_DIR = runtime.installDirectory;
|
|
342
342
|
await fs.mkdir(this.profileDirectory, { recursive: true, mode: 0o700 });
|
|
343
|
-
const module = await this.
|
|
344
|
-
if (!module || typeof module.Camoufox !== 'function') throw new Error('
|
|
343
|
+
const module = await this.loadBrowserEngine();
|
|
344
|
+
if (!module || typeof module.Camoufox !== 'function') throw new Error('Causyne Browser has no usable launcher');
|
|
345
345
|
this.session = await module.Camoufox({
|
|
346
346
|
headless: !this.visible,
|
|
347
347
|
user_data_dir: this.profileDirectory,
|
package/src/cliMode.js
CHANGED
|
@@ -12,7 +12,7 @@ import { getPackageVersion } from './version.js';
|
|
|
12
12
|
import { resolveCharPool } from './contextManager.js';
|
|
13
13
|
import { PRIMARY_ACTIONS, homeStatusLine, homeHint, moreLayer, configureInPrompt, mcpPicker, PROVIDERS } from './tui/home.js';
|
|
14
14
|
import { SelectLayer } from './tui/dialogs.js';
|
|
15
|
-
import { askSelect, askText, askConfirm } from './tui/ask.js';
|
|
15
|
+
import { askSelect, askText, askConfirm, askDecision } from './tui/ask.js';
|
|
16
16
|
import { renderReport } from './reporting.js';
|
|
17
17
|
import { grantAlways } from './approvalGrants.js';
|
|
18
18
|
import { auditLog } from './auditLog.js';
|
|
@@ -151,6 +151,15 @@ function makeApprovalHandler(conn, dialogs = null) {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/** Keep implementation choices distinct from tool permission decisions. */
|
|
155
|
+
function makeDecisionHandler(conn, dialogs = null) {
|
|
156
|
+
return async (request) => {
|
|
157
|
+
conn.stopSpinner();
|
|
158
|
+
if (dialogs && typeof dialogs.decide === 'function') return await dialogs.decide(request);
|
|
159
|
+
return await askDecision(request);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
154
163
|
/** Muted one-liner for menu feedback that is not success and not an error. */
|
|
155
164
|
function dimNotice(text) { return paint('muted', text); }
|
|
156
165
|
|
|
@@ -263,6 +272,7 @@ export async function runTask(task) {
|
|
|
263
272
|
|
|
264
273
|
// Local human-in-the-loop, showing WHAT is being approved (see L6).
|
|
265
274
|
loop.waitForApproval = makeApprovalHandler(conn);
|
|
275
|
+
loop.waitForDecision = makeDecisionHandler(conn);
|
|
266
276
|
|
|
267
277
|
await loop.run();
|
|
268
278
|
console.log('\n\x1b[32m[DONE]\x1b[0m Task completed.\n');
|
|
@@ -322,6 +332,7 @@ export async function chatMode({ home = false } = {}) {
|
|
|
322
332
|
});
|
|
323
333
|
|
|
324
334
|
loop.waitForApproval = makeApprovalHandler(conn);
|
|
335
|
+
loop.waitForDecision = makeDecisionHandler(conn);
|
|
325
336
|
|
|
326
337
|
// The prompt owns the keyboard so the chat can offer history, completion and
|
|
327
338
|
// typing while a turn runs. It is skipped when stdin is not a terminal (piped
|
|
@@ -378,6 +389,7 @@ export async function chatMode({ home = false } = {}) {
|
|
|
378
389
|
// prompt. The panel shows the risk, the arguments and a scrollable diff in
|
|
379
390
|
// one frame, and dismissing it denies.
|
|
380
391
|
loop.waitForApproval = makeApprovalHandler(conn, prompt);
|
|
392
|
+
loop.waitForDecision = makeDecisionHandler(conn, prompt);
|
|
381
393
|
// From here the modal asks the question, so the renderer must stop
|
|
382
394
|
// printing the full request into the transcript as well — see
|
|
383
395
|
// ChatRenderer.onApprovalRequest().
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const ARCHITECTURE_TERMS = [
|
|
2
|
+
'architecture', 'arsitektur',
|
|
3
|
+
'framework', 'runtime', 'database', 'storage', 'authentication', 'authorization',
|
|
4
|
+
'deployment', 'hosting', 'protocol', 'plugin system', 'sandbox', 'license',
|
|
5
|
+
'typescript', 'javascript', 'python', 'rust', 'kotlin', 'swift'
|
|
6
|
+
];
|
|
7
|
+
const CHANGE_TERMS = [
|
|
8
|
+
'change', 'replace', 'switch', 'choose', 'decide', 'redesign', 'restructure',
|
|
9
|
+
'rewrite', 'rebuild', 'migration', 'migrate',
|
|
10
|
+
'ubah', 'ganti', 'pilih', 'tentukan', 'rombak', 'beralih', 'migrasi'
|
|
11
|
+
];
|
|
12
|
+
const EXPLICIT_CHOICE_PATTERNS = [
|
|
13
|
+
/\b(?:i|we)\s+(?:choose|chose|selected?|decided? (?:on|to use))\s+[a-z0-9@._+-]+/i,
|
|
14
|
+
/\b(?:must use|will use|switch to|rewrite (?:to|in)|migrate to)\s+[a-z0-9@._+-]+/i,
|
|
15
|
+
/\b(?:saya pilih|aku pilih|kita pilih|harus memakai|beralih ke|ubah ke|ganti ke)\s+[a-z0-9@._+-]+/i,
|
|
16
|
+
/^(?:please\s+)?(?:use|choose|gunakan|pilih)\s+[a-z0-9@._+-]+/i,
|
|
17
|
+
/\b(?:do not|don't|jangan)\s+(?:use|gunakan|pakai)\s+[a-z0-9@._+-]+/i
|
|
18
|
+
];
|
|
19
|
+
const MUTATING_TOOLS = new Set([
|
|
20
|
+
'fs_write', 'fs_edit', 'MultiEdit', 'fs_delete', 'fs_move', 'sysRun', 'sysRunBg',
|
|
21
|
+
'gitOp', 'manage_local_dependencies', 'plugin_manage', 'tool_forge', 'create_skill'
|
|
22
|
+
]);
|
|
23
|
+
function containsAny(text, terms) {
|
|
24
|
+
return terms.some(term => text.includes(term));
|
|
25
|
+
}
|
|
26
|
+
export function assessDecisionRequirement(prompt) {
|
|
27
|
+
const raw = String(prompt ?? '').trim();
|
|
28
|
+
const text = raw.toLowerCase();
|
|
29
|
+
const architectural = containsAny(text, ARCHITECTURE_TERMS);
|
|
30
|
+
const change = containsAny(text, CHANGE_TERMS);
|
|
31
|
+
const alternatives = /\b(?:or|versus|vs\.?|atau|dibanding(?:kan)? dengan)\b/i.test(raw);
|
|
32
|
+
const asksForChoice = /\b(?:which|what should|should we|mana|apakah sebaiknya|bagaimana sebaiknya)\b/i.test(raw);
|
|
33
|
+
const delegatesBroadRedesign = /\b(?:redesign|restructure|rombak)\s+(?:the\s+)?(?:architecture|system|runtime|database|arsitektur|sistem)\b/i.test(raw);
|
|
34
|
+
const explicit = EXPLICIT_CHOICE_PATTERNS.some(pattern => pattern.test(raw));
|
|
35
|
+
if (!architectural || explicit || (!alternatives && !asksForChoice && !(change && delegatesBroadRedesign))) {
|
|
36
|
+
return { required: false, kind: 'architecture', reason: '' };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
required: true,
|
|
40
|
+
kind: 'architecture',
|
|
41
|
+
reason: 'The request reaches a durable architecture boundary with more than one plausible implementation.'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function requiresImplementationPlan(prompt, archetype = '', confidence = '') {
|
|
45
|
+
const text = String(prompt ?? '').toLowerCase();
|
|
46
|
+
if (archetype === 'build_app' || archetype === 'integrate')
|
|
47
|
+
return true;
|
|
48
|
+
if (confidence === 'high' && /\b(?:implement|build|create|migrate|rewrite|refactor|integrate|buat|bangun|ubah)\b/i.test(text)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return containsAny(text, ARCHITECTURE_TERMS) && containsAny(text, CHANGE_TERMS);
|
|
52
|
+
}
|
|
53
|
+
export function isMutatingToolCall(toolName, args = {}) {
|
|
54
|
+
if (MUTATING_TOOLS.has(toolName)) {
|
|
55
|
+
if (toolName === 'sysRun' || toolName === 'sysRunBg') {
|
|
56
|
+
const command = String(args.command ?? '');
|
|
57
|
+
return !/^\s*(?:pwd|ls|find|rg|grep|cat|sed|head|tail|git\s+(?:status|diff|log|show)|npm\s+(?:view|audit|test)|node\s+--check)\b/i.test(command);
|
|
58
|
+
}
|
|
59
|
+
if (toolName === 'gitOp') {
|
|
60
|
+
const action = String(args.action ?? args.command ?? args.operation ?? '');
|
|
61
|
+
return !/^(?:status|diff|log|show|branch|remote)$/i.test(action.trim());
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (toolName.startsWith('plugin__') || toolName.startsWith('mcp__'))
|
|
66
|
+
return false;
|
|
67
|
+
return false;
|
|
68
|
+
}
|