create-walle 0.9.25 → 0.9.26
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/README.md +8 -0
- package/bin/create-walle.js +815 -45
- package/package.json +2 -2
- package/template/bin/ctm-dev-cleanup.js +90 -4
- package/template/bin/ctm-launch.sh +49 -1
- package/template/bin/dev.sh +45 -1
- package/template/bin/ensure-stable-node.js +132 -0
- package/template/bin/install-service.sh +9 -0
- package/template/claude-task-manager/api-prompts.js +899 -119
- package/template/claude-task-manager/approval-agent.js +360 -40
- package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
- package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
- package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
- package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
- package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
- package/template/claude-task-manager/db.js +399 -48
- package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
- package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
- package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
- package/template/claude-task-manager/lib/approval-hook.js +200 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
- package/template/claude-task-manager/lib/auth-rules.js +11 -0
- package/template/claude-task-manager/lib/background-llm.js +32 -4
- package/template/claude-task-manager/lib/codesign-identity.js +140 -0
- package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
- package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
- package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
- package/template/claude-task-manager/lib/codex-paths.js +73 -0
- package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
- package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
- package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
- package/template/claude-task-manager/lib/command-targets.js +163 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
- package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
- package/template/claude-task-manager/lib/escalation-review.js +80 -3
- package/template/claude-task-manager/lib/flow-control.js +52 -0
- package/template/claude-task-manager/lib/fs-watcher.js +24 -15
- package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
- package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
- package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
- package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
- package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
- package/template/claude-task-manager/lib/perf-tracker.js +29 -2
- package/template/claude-task-manager/lib/permission-match.js +146 -16
- package/template/claude-task-manager/lib/project-slug.js +33 -0
- package/template/claude-task-manager/lib/prompt-intent.js +51 -4
- package/template/claude-task-manager/lib/read-pool-client.js +48 -3
- package/template/claude-task-manager/lib/real-node.js +73 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
- package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
- package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
- package/template/claude-task-manager/lib/session-history.js +5 -7
- package/template/claude-task-manager/lib/session-host-manager.js +19 -0
- package/template/claude-task-manager/lib/session-jobs.js +6 -0
- package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
- package/template/claude-task-manager/lib/session-messages-page.js +211 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
- package/template/claude-task-manager/lib/session-standup.js +8 -0
- package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
- package/template/claude-task-manager/lib/session-token-usage.js +30 -8
- package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
- package/template/claude-task-manager/lib/storage-migration.js +2 -1
- package/template/claude-task-manager/lib/transcript-store.js +179 -12
- package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
- package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
- package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
- package/template/claude-task-manager/package.json +5 -2
- package/template/claude-task-manager/prompt-harvest.js +31 -11
- package/template/claude-task-manager/providers/claude-code.js +29 -1
- package/template/claude-task-manager/providers/codex.js +13 -1
- package/template/claude-task-manager/public/css/setup.css +11 -0
- package/template/claude-task-manager/public/css/walle-session.css +132 -4
- package/template/claude-task-manager/public/css/walle.css +89 -0
- package/template/claude-task-manager/public/icon-16.png +0 -0
- package/template/claude-task-manager/public/icon-32.png +0 -0
- package/template/claude-task-manager/public/icon-512.png +0 -0
- package/template/claude-task-manager/public/index.html +2483 -165
- package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
- package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
- package/template/claude-task-manager/public/js/message-renderer.js +60 -1
- package/template/claude-task-manager/public/js/prompts.js +13 -1
- package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
- package/template/claude-task-manager/public/js/setup.js +54 -10
- package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
- package/template/claude-task-manager/public/js/stream-view.js +78 -0
- package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
- package/template/claude-task-manager/public/js/tool-state.js +155 -0
- package/template/claude-task-manager/public/js/walle-session.js +887 -326
- package/template/claude-task-manager/public/js/walle.js +306 -195
- package/template/claude-task-manager/public/m/app.css +1 -0
- package/template/claude-task-manager/public/m/app.js +33 -3
- package/template/claude-task-manager/queue-engine.js +45 -1
- package/template/claude-task-manager/server.js +3367 -540
- package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
- package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
- package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
- package/template/claude-task-manager/workers/session-host-process.js +10 -0
- package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
- package/template/package.json +2 -3
- package/template/shared/icons/AppIcon-ctm.icns +0 -0
- package/template/shared/icons/AppIcon-walle.icns +0 -0
- package/template/wall-e/agent.js +139 -18
- package/template/wall-e/api-walle.js +201 -22
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
- package/template/wall-e/brain.js +1049 -39
- package/template/wall-e/chat.js +427 -86
- package/template/wall-e/coding/acceptance-contract.js +26 -1
- package/template/wall-e/coding/action-memory-policy.js +353 -0
- package/template/wall-e/coding/action-memory-store.js +814 -0
- package/template/wall-e/coding/initial-messages.js +197 -0
- package/template/wall-e/coding/no-progress-guard.js +327 -0
- package/template/wall-e/coding/permission-service.js +88 -22
- package/template/wall-e/coding/session-workspaces.js +81 -0
- package/template/wall-e/coding/shell-sandbox.js +124 -0
- package/template/wall-e/coding/stream-processor.js +63 -2
- package/template/wall-e/coding/tool-execution-controller.js +14 -1
- package/template/wall-e/coding/tool-registry.js +1 -1
- package/template/wall-e/coding/transcript-writer.js +3 -0
- package/template/wall-e/coding-orchestrator.js +636 -35
- package/template/wall-e/coding-prompts.js +51 -2
- package/template/wall-e/docs/model-routing-policy.md +59 -0
- package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
- package/template/wall-e/extraction/knowledge-extractor.js +76 -23
- package/template/wall-e/http/chat-api.js +30 -12
- package/template/wall-e/http/model-admin.js +93 -1
- package/template/wall-e/lib/background-lanes.js +133 -0
- package/template/wall-e/lib/boot-profile.js +11 -0
- package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
- package/template/wall-e/lib/brain-read-pool-client.js +311 -0
- package/template/wall-e/lib/diagnostics-flags.js +87 -0
- package/template/wall-e/lib/event-loop-monitor.js +74 -3
- package/template/wall-e/lib/mcp-integration.js +7 -1
- package/template/wall-e/lib/real-node.js +98 -0
- package/template/wall-e/lib/runtime-health.js +206 -0
- package/template/wall-e/lib/runtime-worker-pool.js +101 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
- package/template/wall-e/lib/scheduler.js +446 -17
- package/template/wall-e/lib/service-health.js +61 -2
- package/template/wall-e/lib/service-readiness.js +258 -0
- package/template/wall-e/lib/usage.js +152 -0
- package/template/wall-e/lib/worker-thread-pool.js +389 -0
- package/template/wall-e/llm/client.js +81 -4
- package/template/wall-e/llm/default-fallback.js +54 -8
- package/template/wall-e/llm/mlx.js +536 -73
- package/template/wall-e/llm/mlx.plugin.json +1 -1
- package/template/wall-e/llm/ollama.js +342 -43
- package/template/wall-e/llm/provider-error.js +18 -1
- package/template/wall-e/llm/provider-health-state.js +176 -0
- package/template/wall-e/llm/routing-policy.js +796 -0
- package/template/wall-e/llm/supported-models.js +5 -0
- package/template/wall-e/loops/tasks.js +60 -14
- package/template/wall-e/loops/think.js +89 -24
- package/template/wall-e/mcp-server.js +192 -28
- package/template/wall-e/server.js +32 -7
- package/template/wall-e/skills/script-skill-runner.js +8 -1
- package/template/wall-e/skills/skill-planner.js +64 -1
- package/template/wall-e/tools/builtin-middleware.js +67 -2
- package/template/wall-e/tools/local-tools.js +116 -26
- package/template/wall-e/tools/permission-checker.js +52 -4
- package/template/wall-e/tools/permission-rules.js +36 -0
- package/template/wall-e/tools/shell-analyzer.js +46 -1
- package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
- package/template/wall-e/training/real-trajectory-miner.js +2617 -0
- package/template/wall-e/training/replay-eval-analysis.js +151 -0
- package/template/wall-e/training/run-shell-command-selector.js +277 -0
- package/template/wall-e/training/tool-sft-dataset.js +312 -0
- package/template/wall-e/training/tool-sft-renderers.js +144 -0
- package/template/wall-e/training/tool-trace-harvester.js +1440 -0
- package/template/wall-e/training/trajectory-action-selector.js +364 -0
- package/template/wall-e/weather-runtime.js +232 -0
- package/template/wall-e/workers/brain-owner-worker.js +162 -0
- package/template/wall-e/workers/brain-read-worker.js +148 -0
- package/template/wall-e/workers/runtime-worker.js +145 -0
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
-
const { checkPermission } = require('../tools/permission-checker');
|
|
5
|
+
const { checkPermission, registerEscalation } = require('../tools/permission-checker');
|
|
6
6
|
const { analyzeShellCommand, initParser: initShellParser, bashArityPrefix } = require('../tools/shell-analyzer');
|
|
7
|
+
const shellSandbox = require('./shell-sandbox');
|
|
7
8
|
|
|
8
9
|
// Shell builtins that only navigate; skipped when choosing the approval card's
|
|
9
10
|
// display pattern so a leading `cd` doesn't label the request as "cd *".
|
|
@@ -52,13 +53,14 @@ class PermissionService {
|
|
|
52
53
|
this.pending = _sharedPending;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
async authorize({ sessionId = '', tool, input = {}, cwd = '', projectRoot = '', mode = '', metadata = {}, timeoutMs, headless = false } = {}) {
|
|
56
|
+
async authorize({ sessionId = '', tool, input = {}, cwd = '', projectRoot = '', mode = '', goal = '', metadata = {}, timeoutMs, headless = false } = {}) {
|
|
56
57
|
const permissionContextId = metadata.permissionContextId || metadata.permission_context_id || createPermissionContextId();
|
|
57
58
|
const requests = await buildToolPermissionRequests(tool, input, {
|
|
58
59
|
cwd,
|
|
59
60
|
projectRoot: projectRoot || cwd,
|
|
60
61
|
sessionId,
|
|
61
62
|
mode,
|
|
63
|
+
goal,
|
|
62
64
|
metadata: { ...metadata, permissionContextId },
|
|
63
65
|
});
|
|
64
66
|
if (requests.length === 0) return { decision: 'allow', source: 'default', reason: 'No permission required', permissionContextId };
|
|
@@ -82,10 +84,19 @@ class PermissionService {
|
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
if (headless || (timeoutMs ?? this.timeoutMs) === 0) {
|
|
87
|
+
// With the OS shell sandbox on, an unlisted shell command is not a dead-end:
|
|
88
|
+
// let it through to the runner, which executes it CONTAINED (writes bounded to
|
|
89
|
+
// the workspace). The hard-denylist already returned 'deny' above, so this only
|
|
90
|
+
// reaches non-destructive commands.
|
|
91
|
+
const sandboxableShell = (request.permission === 'bash' || request.tool === 'run_shell')
|
|
92
|
+
&& shellSandbox.shellSandboxEnabled()
|
|
93
|
+
&& shellSandbox.sandboxAvailable();
|
|
85
94
|
return {
|
|
86
|
-
decision: this.headlessPolicy === 'allow' ? 'allow' : 'deny',
|
|
87
|
-
source: 'headless',
|
|
88
|
-
reason:
|
|
95
|
+
decision: (this.headlessPolicy === 'allow' || sandboxableShell) ? 'allow' : 'deny',
|
|
96
|
+
source: sandboxableShell ? 'sandbox' : 'headless',
|
|
97
|
+
reason: sandboxableShell
|
|
98
|
+
? `Sandboxed run_shell for ${request.permission}/${request.pattern}`
|
|
99
|
+
: `Permission required for ${request.permission}/${request.pattern}`,
|
|
89
100
|
request,
|
|
90
101
|
permissionContextId,
|
|
91
102
|
};
|
|
@@ -93,6 +104,7 @@ class PermissionService {
|
|
|
93
104
|
|
|
94
105
|
const reply = await this.ask({
|
|
95
106
|
...request,
|
|
107
|
+
check: request.check,
|
|
96
108
|
sessionId,
|
|
97
109
|
metadata: { ...metadata, ...request.metadata, evaluated },
|
|
98
110
|
permissionContextId,
|
|
@@ -104,7 +116,7 @@ class PermissionService {
|
|
|
104
116
|
return { decision: 'allow', source: 'permissions', reason: 'Permission checks passed', permissionContextId };
|
|
105
117
|
}
|
|
106
118
|
|
|
107
|
-
ask({ sessionId = '', permission, pattern = '*', patterns = null, tool = '', fingerprint = '', metadata = {}, permissionContextId = '', timeoutMs } = {}) {
|
|
119
|
+
ask({ sessionId = '', permission, pattern = '*', patterns = null, tool = '', fingerprint = '', metadata = {}, permissionContextId = '', timeoutMs, check = null } = {}) {
|
|
108
120
|
if (!permission) throw new Error('permission is required');
|
|
109
121
|
const id = `perm_${crypto.randomUUID().slice(0, 8)}`;
|
|
110
122
|
const contextId = permissionContextId || metadata.permissionContextId || metadata.permission_context_id || createPermissionContextId();
|
|
@@ -124,12 +136,31 @@ class PermissionService {
|
|
|
124
136
|
fingerprint: fingerprint || metadata.fingerprint || '',
|
|
125
137
|
permissionContextId: contextId,
|
|
126
138
|
metadata: { ...metadata, permissionContextId: contextId },
|
|
139
|
+
// The original check payload, kept so a later rule change (an "always"
|
|
140
|
+
// rule added here OR an allow rule added in CTM's Permission Manager) can
|
|
141
|
+
// RE-EVALUATE this parked request and auto-resolve it — instead of leaving
|
|
142
|
+
// it hung until timeout. See _reEvaluatePending().
|
|
143
|
+
check: check || null,
|
|
127
144
|
createdAt: Date.now(),
|
|
128
145
|
timer,
|
|
129
146
|
resolve,
|
|
130
147
|
};
|
|
131
148
|
this.pending.set(id, entry);
|
|
132
149
|
this._emit('permission.asked', publicEntry(entry));
|
|
150
|
+
// Register the park with CTM (best-effort, non-blocking) so it ALSO appears
|
|
151
|
+
// in the shared "Pending" tab and survives reload. Resolving it there calls
|
|
152
|
+
// back this request id to unpark; a failure is harmless (the local ask + the
|
|
153
|
+
// durable transcript card already surface it).
|
|
154
|
+
registerEscalation({
|
|
155
|
+
requestId: id,
|
|
156
|
+
tool,
|
|
157
|
+
command: (metadata && metadata.command) || '',
|
|
158
|
+
pattern,
|
|
159
|
+
projectPath: (metadata && metadata.projectRoot) || '',
|
|
160
|
+
sessionId,
|
|
161
|
+
reason: (metadata && metadata.evaluated && metadata.evaluated.reason) || '',
|
|
162
|
+
riskLevel: (metadata && metadata.evaluated && metadata.evaluated.riskLevel) || 'medium',
|
|
163
|
+
}).catch(() => {});
|
|
133
164
|
});
|
|
134
165
|
}
|
|
135
166
|
|
|
@@ -139,7 +170,7 @@ class PermissionService {
|
|
|
139
170
|
const decision = reply === 'once' || reply === 'always' ? 'allow' : 'deny';
|
|
140
171
|
this._resolveEntry(entry, { decision, reply, message, source: 'user' });
|
|
141
172
|
if (reply === 'always') {
|
|
142
|
-
|
|
173
|
+
this.rulesStore.addAlways({
|
|
143
174
|
projectRoot: entry.metadata.projectRoot || '',
|
|
144
175
|
permission: entry.permission,
|
|
145
176
|
pattern: entry.pattern,
|
|
@@ -147,11 +178,58 @@ class PermissionService {
|
|
|
147
178
|
fingerprint: entry.fingerprint || entry.metadata.fingerprint || '',
|
|
148
179
|
metadata: { ...entry.metadata, message },
|
|
149
180
|
});
|
|
150
|
-
|
|
181
|
+
// Cascade: the new rule may also cover OTHER parked requests in this
|
|
182
|
+
// session (e.g. a prefix rule `du *` covers every pending `du …`). Re-run
|
|
183
|
+
// the full decision against each one and auto-resolve the now-allowed ones,
|
|
184
|
+
// instead of only the exact fingerprint match the old path handled.
|
|
185
|
+
void this._reEvaluatePending({ sessionId: entry.sessionId });
|
|
151
186
|
}
|
|
152
187
|
return true;
|
|
153
188
|
}
|
|
154
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Re-evaluate every still-parked request (optionally scoped to one session)
|
|
192
|
+
* and resolve any that a freshly-added rule now allows. This is the self-heal
|
|
193
|
+
* for the "parked forever even though the approver would allow it" class:
|
|
194
|
+
* called after a local "always" reply, and exported as reEvaluatePending() so
|
|
195
|
+
* CTM can call it the instant a matching Permission Manager rule is added.
|
|
196
|
+
* Each pending entry carries its original `check`, so the decision is the same
|
|
197
|
+
* one authorize() makes — no duplicated policy logic.
|
|
198
|
+
*/
|
|
199
|
+
async _reEvaluatePending({ sessionId = '' } = {}) {
|
|
200
|
+
for (const entry of [...this.pending.values()]) {
|
|
201
|
+
if (sessionId && entry.sessionId !== sessionId) continue;
|
|
202
|
+
let allow = false;
|
|
203
|
+
// (1) An "always" rule now covering this request (prefix-matched, no
|
|
204
|
+
// fingerprint filter so it cascades beyond the exact command).
|
|
205
|
+
try {
|
|
206
|
+
const rule = this.rulesStore.findAlways({
|
|
207
|
+
projectRoot: entry.metadata?.projectRoot || '',
|
|
208
|
+
permission: entry.permission,
|
|
209
|
+
pattern: entry.pattern,
|
|
210
|
+
tool: entry.tool,
|
|
211
|
+
});
|
|
212
|
+
if (rule) allow = true;
|
|
213
|
+
} catch { /* store unavailable — fall through to a fresh evaluation */ }
|
|
214
|
+
// (2) A static/CTM rule now allowing it (e.g. a new Permission Manager
|
|
215
|
+
// allow rule, or the read-only allowlist after a config reload).
|
|
216
|
+
if (!allow && entry.check) {
|
|
217
|
+
try {
|
|
218
|
+
const evaluated = await this.evaluatePermission(entry.check);
|
|
219
|
+
if (evaluated && evaluated.decision === 'allow') allow = true;
|
|
220
|
+
} catch { /* evaluation failed — leave parked, never auto-deny here */ }
|
|
221
|
+
}
|
|
222
|
+
if (allow && this.pending.has(entry.id)) {
|
|
223
|
+
this._resolveEntry(entry, { decision: 'allow', reply: 'always', source: 'cascade' });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Public trigger for the CTM bridge: a rule was added in CTM; re-check parks. */
|
|
229
|
+
reEvaluatePending(opts = {}) {
|
|
230
|
+
return this._reEvaluatePending(opts);
|
|
231
|
+
}
|
|
232
|
+
|
|
155
233
|
list({ sessionId = '' } = {}) {
|
|
156
234
|
const out = [];
|
|
157
235
|
for (const entry of this.pending.values()) {
|
|
@@ -168,18 +246,6 @@ class PermissionService {
|
|
|
168
246
|
}
|
|
169
247
|
}
|
|
170
248
|
|
|
171
|
-
_resolveCompatibleAlways(sourceEntry, rule, message) {
|
|
172
|
-
for (const entry of [...this.pending.values()]) {
|
|
173
|
-
if (entry.id === sourceEntry.id) continue;
|
|
174
|
-
if (entry.permission !== sourceEntry.permission) continue;
|
|
175
|
-
if (entry.tool !== sourceEntry.tool) continue;
|
|
176
|
-
if ((entry.metadata.projectRoot || '') !== (sourceEntry.metadata.projectRoot || '')) continue;
|
|
177
|
-
if (entry.pattern !== sourceEntry.pattern) continue;
|
|
178
|
-
if ((entry.fingerprint || '') !== (sourceEntry.fingerprint || '')) continue;
|
|
179
|
-
this._resolveEntry(entry, { decision: 'allow', reply: 'always', message, source: 'always', rule });
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
249
|
_resolveEntry(entry, result) {
|
|
184
250
|
if (entry.timer) clearTimeout(entry.timer);
|
|
185
251
|
this.pending.delete(entry.id);
|
|
@@ -204,7 +270,7 @@ class PermissionService {
|
|
|
204
270
|
}
|
|
205
271
|
}
|
|
206
272
|
|
|
207
|
-
async function buildToolPermissionRequests(tool, input = {}, { cwd = '', projectRoot = '', sessionId = '', mode = '', metadata = {} } = {}) {
|
|
273
|
+
async function buildToolPermissionRequests(tool, input = {}, { cwd = '', projectRoot = '', sessionId = '', mode = '', goal = '', metadata = {} } = {}) {
|
|
208
274
|
const root = path.resolve(projectRoot || cwd || process.cwd());
|
|
209
275
|
const base = { sessionId, tool, projectRoot: root, metadata: { ...metadata, projectRoot: root } };
|
|
210
276
|
if (!tool) return [];
|
|
@@ -225,7 +291,7 @@ async function buildToolPermissionRequests(tool, input = {}, { cwd = '', project
|
|
|
225
291
|
permission: 'bash',
|
|
226
292
|
pattern,
|
|
227
293
|
metadata: { ...base.metadata, command, commandTokens },
|
|
228
|
-
check: { tool: 'run_shell', command, commandTokens, commandClauses, projectPath: root, sessionId, mode },
|
|
294
|
+
check: { tool: 'run_shell', command, commandTokens, commandClauses, projectPath: root, sessionId, mode, goal },
|
|
229
295
|
}], input);
|
|
230
296
|
}
|
|
231
297
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Track the set of working directories a single coding session has touched.
|
|
4
|
+
//
|
|
5
|
+
// Wall-E resolves a coding turn's cwd per-request (coding-run-controller), so one
|
|
6
|
+
// long-lived session reused across two unrelated repos silently follows whichever
|
|
7
|
+
// directory it was last pointed at. Asked about a file built earlier in a DIFFERENT
|
|
8
|
+
// directory, the agent globbed its current cwd and missed it (session c3f3af97:
|
|
9
|
+
// built a site in one repo, was later reused for a different app repo, then "show
|
|
10
|
+
// me the web page file path" returned the current repo's files). Mirrors how
|
|
11
|
+
// OpenCode/Claude Code keep a session grounded: surface every root the session has
|
|
12
|
+
// worked in, every turn, instead of relying on a cwd-scoped search.
|
|
13
|
+
//
|
|
14
|
+
// Stored in brain KV (synchronous, survives restarts) keyed by the CTM chat
|
|
15
|
+
// session id — the stable key that groups sequential runTurn calls into one
|
|
16
|
+
// logical coding session (NOT the per-run transcript id).
|
|
17
|
+
|
|
18
|
+
const KEY_PREFIX = 'coding_session_workspaces:';
|
|
19
|
+
const ANCHOR_KEY_PREFIX = 'coding_session_anchor:';
|
|
20
|
+
const MAX_WORKSPACES = 12;
|
|
21
|
+
|
|
22
|
+
function readWorkspaces(brain, sessionKey) {
|
|
23
|
+
if (!brain || !sessionKey || typeof brain.getKv !== 'function') return [];
|
|
24
|
+
try {
|
|
25
|
+
const raw = brain.getKv(KEY_PREFIX + sessionKey);
|
|
26
|
+
if (!raw) return [];
|
|
27
|
+
const arr = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
28
|
+
return Array.isArray(arr) ? arr.filter((v) => typeof v === 'string' && v) : [];
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Append cwd to the session's recorded set (dedup, most-recent-wins cap) and
|
|
35
|
+
// persist. Returns the updated set. Never throws — a brain hiccup must not break a
|
|
36
|
+
// coding turn; the worst case is the prompt missing the extra-directory hint.
|
|
37
|
+
function recordSessionWorkspace(brain, sessionKey, cwd) {
|
|
38
|
+
const existing = readWorkspaces(brain, sessionKey);
|
|
39
|
+
if (!cwd) return existing;
|
|
40
|
+
if (existing.includes(cwd)) return existing;
|
|
41
|
+
const updated = [...existing, cwd].slice(-MAX_WORKSPACES);
|
|
42
|
+
if (brain && sessionKey && typeof brain.setKv === 'function') {
|
|
43
|
+
try { brain.setKv(KEY_PREFIX + sessionKey, JSON.stringify(updated)); } catch { /* best-effort */ }
|
|
44
|
+
}
|
|
45
|
+
return updated;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readSessionAnchor(brain, sessionKey) {
|
|
49
|
+
if (!brain || !sessionKey || typeof brain.getKv !== 'function') return '';
|
|
50
|
+
try {
|
|
51
|
+
const raw = brain.getKv(ANCHOR_KEY_PREFIX + sessionKey);
|
|
52
|
+
return typeof raw === 'string' ? raw : '';
|
|
53
|
+
} catch {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The session's anchor is the FIRST workspace root it was grounded in — set ONCE and
|
|
59
|
+
// never changed, so a coding session has a stable "home" project (the OpenCode/Claude
|
|
60
|
+
// one-session-per-project model) even when a later turn is pointed at a different
|
|
61
|
+
// directory. Returns the anchor (existing or newly set), or '' when none is available.
|
|
62
|
+
function recordSessionAnchor(brain, sessionKey, root) {
|
|
63
|
+
const existing = readSessionAnchor(brain, sessionKey);
|
|
64
|
+
if (existing) return existing;
|
|
65
|
+
if (!root) return '';
|
|
66
|
+
const anchor = String(root);
|
|
67
|
+
if (brain && sessionKey && typeof brain.setKv === 'function') {
|
|
68
|
+
try { brain.setKv(ANCHOR_KEY_PREFIX + sessionKey, anchor); } catch { /* best-effort */ }
|
|
69
|
+
}
|
|
70
|
+
return anchor;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
recordSessionWorkspace,
|
|
75
|
+
readWorkspaces,
|
|
76
|
+
recordSessionAnchor,
|
|
77
|
+
readSessionAnchor,
|
|
78
|
+
KEY_PREFIX,
|
|
79
|
+
ANCHOR_KEY_PREFIX,
|
|
80
|
+
MAX_WORKSPACES,
|
|
81
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// OS sandbox for the coding agent's run_shell. Wall-E runs shell commands itself
|
|
4
|
+
// (execAsync/spawn '/bin/bash' in tools/local-tools.js), so to run a command CONTAINED
|
|
5
|
+
// — instead of dead-ending an unlisted command at the headless-deny gate — we wrap the
|
|
6
|
+
// spawn with the host's OS sandbox: macOS Seatbelt (`sandbox-exec`) or Linux bubblewrap
|
|
7
|
+
// (`bwrap`). The containment is "auto-allow contained" (Claude Code model): reads,
|
|
8
|
+
// process-exec, and NETWORK are allowed; filesystem WRITES are bounded to the workspace
|
|
9
|
+
// roots. The truly destructive denylist (sudo/dd/mkfs/…) is still hard-denied upstream —
|
|
10
|
+
// a filesystem sandbox can't neutralize privilege/device/system ops.
|
|
11
|
+
//
|
|
12
|
+
// Pure + dependency-free (system binaries only); detection accepts injected platform/probe
|
|
13
|
+
// so it is unit-testable. Returns null when no sandbox backend exists → callers fall back
|
|
14
|
+
// to today's behavior (no false safety).
|
|
15
|
+
|
|
16
|
+
const os = require('node:os');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const { execFileSync } = require('node:child_process');
|
|
20
|
+
|
|
21
|
+
function _hasBin(bin) {
|
|
22
|
+
if (bin === 'sandbox-exec' && fs.existsSync('/usr/bin/sandbox-exec')) return true;
|
|
23
|
+
for (const which of ['/usr/bin/which', 'which']) {
|
|
24
|
+
try {
|
|
25
|
+
execFileSync(which, [bin], { stdio: 'ignore' });
|
|
26
|
+
return true;
|
|
27
|
+
} catch { /* try next */ }
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function detectBackend({ platform = process.platform, hasBin = _hasBin } = {}) {
|
|
33
|
+
if (platform === 'darwin') return hasBin('sandbox-exec') ? 'seatbelt' : null;
|
|
34
|
+
if (platform === 'linux') return hasBin('bwrap') ? 'bubblewrap' : null;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sandboxAvailable(opts = {}) {
|
|
39
|
+
return detectBackend(opts) !== null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Opt-in kill switch (default OFF). When on AND a backend is available, run_shell
|
|
43
|
+
// commands run contained instead of dead-ending at the headless-deny gate.
|
|
44
|
+
function shellSandboxEnabled(env = process.env) {
|
|
45
|
+
const v = String((env && env.WALLE_SHELL_SANDBOX) || '').toLowerCase();
|
|
46
|
+
return v === '1' || v === 'true' || v === 'on' || v === 'yes';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Seatbelt string literals are double-quoted; escape backslash and quote so a path can
|
|
50
|
+
// never break out of the profile.
|
|
51
|
+
function _seatbeltQuote(p) {
|
|
52
|
+
return String(p).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildSeatbeltProfile({ writableRoots = [] } = {}) {
|
|
56
|
+
const roots = (Array.isArray(writableRoots) ? writableRoots : [])
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.map((r) => path.resolve(String(r)));
|
|
59
|
+
const writeAllows = roots.map((r) => `(allow file-write* (subpath "${_seatbeltQuote(r)}"))`);
|
|
60
|
+
return [
|
|
61
|
+
'(version 1)',
|
|
62
|
+
'(allow default)', // reads, process-exec, network all allowed
|
|
63
|
+
'(deny file-write*)', // ...writes denied by default
|
|
64
|
+
...writeAllows, // ...except under the workspace roots
|
|
65
|
+
'(allow file-write* (regex #"^/dev/"))', // tty / null / stdout / stderr
|
|
66
|
+
].join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function defaultWritableRoots(cwd) {
|
|
70
|
+
const roots = [];
|
|
71
|
+
if (cwd) roots.push(path.resolve(String(cwd)));
|
|
72
|
+
roots.push(os.tmpdir());
|
|
73
|
+
return roots;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Add the canonical (realpath) form of each root. macOS symlinks /var -> /private/var
|
|
77
|
+
// and /tmp -> /private/tmp; Seatbelt matches the REAL path, so a subpath of /var/folders
|
|
78
|
+
// would deny a write whose canonical path is /private/var/folders. Include both.
|
|
79
|
+
function _expandRootsWithRealpath(roots) {
|
|
80
|
+
const out = [];
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
for (const r of roots) {
|
|
83
|
+
const abs = path.resolve(String(r));
|
|
84
|
+
if (!seen.has(abs)) { seen.add(abs); out.push(abs); }
|
|
85
|
+
try {
|
|
86
|
+
const real = fs.realpathSync(abs);
|
|
87
|
+
if (real && real !== abs && !seen.has(real)) { seen.add(real); out.push(real); }
|
|
88
|
+
} catch { /* root may not exist yet — keep the resolved form */ }
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Build the argv that runs `command` (a /bin/bash -c string) inside the sandbox. Returns
|
|
94
|
+
// null when no backend is available so the caller can fall back to an unsandboxed spawn.
|
|
95
|
+
function wrapShellArgv({ command, cwd, writableRoots, platform = process.platform, hasBin = _hasBin } = {}) {
|
|
96
|
+
const backend = detectBackend({ platform, hasBin });
|
|
97
|
+
if (!backend) return null;
|
|
98
|
+
const requested = Array.isArray(writableRoots) && writableRoots.length
|
|
99
|
+
? writableRoots.filter(Boolean)
|
|
100
|
+
: defaultWritableRoots(cwd);
|
|
101
|
+
const roots = _expandRootsWithRealpath(requested);
|
|
102
|
+
const cmd = String(command || '');
|
|
103
|
+
|
|
104
|
+
if (backend === 'seatbelt') {
|
|
105
|
+
const profile = buildSeatbeltProfile({ writableRoots: roots });
|
|
106
|
+
return ['sandbox-exec', '-p', profile, '/bin/bash', '-c', cmd];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// bubblewrap: read-only root, the workspace roots bound read-write, /dev + /proc, and
|
|
110
|
+
// a shared network namespace (writes contained, network allowed).
|
|
111
|
+
const argv = ['bwrap', '--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc'];
|
|
112
|
+
for (const r of roots) argv.push('--bind', r, r);
|
|
113
|
+
argv.push('--share-net', '--die-with-parent', '/bin/bash', '-c', cmd);
|
|
114
|
+
return argv;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
detectBackend,
|
|
119
|
+
sandboxAvailable,
|
|
120
|
+
shellSandboxEnabled,
|
|
121
|
+
buildSeatbeltProfile,
|
|
122
|
+
defaultWritableRoots,
|
|
123
|
+
wrapShellArgv,
|
|
124
|
+
};
|
|
@@ -93,15 +93,19 @@ async function* streamFromChat(provider, request) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
class StreamProcessor extends EventEmitter {
|
|
96
|
-
constructor({ provider, model, transcript, toolExecutor, snapshotService, permissionService, artifactStore, now, emitLaneEvents = true, headless = false } = {}) {
|
|
96
|
+
constructor({ provider, model, transcript, toolExecutor, prepareToolCall, snapshotService, permissionService, artifactStore, now, emitLaneEvents = true, headless = false, goal = '' } = {}) {
|
|
97
97
|
super();
|
|
98
98
|
if (!provider) throw new Error('provider is required');
|
|
99
99
|
this.provider = provider;
|
|
100
100
|
this.model = model;
|
|
101
101
|
this.transcript = transcript || null;
|
|
102
102
|
this.toolExecutor = toolExecutor || null;
|
|
103
|
+
this.prepareToolCall = typeof prepareToolCall === 'function' ? prepareToolCall : null;
|
|
103
104
|
this.snapshotService = snapshotService || null;
|
|
104
105
|
this.permissionService = permissionService || null;
|
|
106
|
+
// Coding task forwarded to the permission layer for CTM's goal-aligned
|
|
107
|
+
// auto-approver (best-effort context, never a gate on its own).
|
|
108
|
+
this.goal = goal || '';
|
|
105
109
|
this.now = now || (() => new Date().toISOString());
|
|
106
110
|
this.artifactStore = artifactStore === false ? null : (artifactStore || new ArtifactStore({ now: this.now }));
|
|
107
111
|
this.emitLaneEvents = emitLaneEvents !== false;
|
|
@@ -149,6 +153,9 @@ class StreamProcessor extends EventEmitter {
|
|
|
149
153
|
hadEdit: false,
|
|
150
154
|
verified: false,
|
|
151
155
|
events: [],
|
|
156
|
+
// Turn abort signal — forwarded to tool execution so user Stop kills an
|
|
157
|
+
// in-flight run_shell child instead of just abandoning the promise.
|
|
158
|
+
signal,
|
|
152
159
|
};
|
|
153
160
|
|
|
154
161
|
await this._runtimeThread(sessionId, cwd, {
|
|
@@ -415,6 +422,7 @@ class StreamProcessor extends EventEmitter {
|
|
|
415
422
|
async _runTool(call, state, { sessionId, cwd }) {
|
|
416
423
|
const toolStartedAt = Date.now();
|
|
417
424
|
try {
|
|
425
|
+
call = await this._prepareToolCall(call, state, { sessionId, cwd });
|
|
418
426
|
await this._runtimeItem(sessionId, cwd, {
|
|
419
427
|
threadId: state.threadId,
|
|
420
428
|
turnId: state.turnId,
|
|
@@ -533,6 +541,10 @@ class StreamProcessor extends EventEmitter {
|
|
|
533
541
|
type: 'permission_request',
|
|
534
542
|
sessionId,
|
|
535
543
|
permId: entry.id,
|
|
544
|
+
// toolCallId correlates this approval to the exact tool task in the
|
|
545
|
+
// CTM WORK-bar registry, so it flips THAT tool to awaiting_permission
|
|
546
|
+
// (not a racy "most-recent pending" guess).
|
|
547
|
+
toolCallId: call.id,
|
|
536
548
|
tool: call.name,
|
|
537
549
|
command: (call.input && call.input.command) || '',
|
|
538
550
|
args: Array.isArray(call.input && call.input.args) ? call.input.args : [],
|
|
@@ -549,6 +561,7 @@ class StreamProcessor extends EventEmitter {
|
|
|
549
561
|
input: call.input,
|
|
550
562
|
cwd,
|
|
551
563
|
projectRoot: cwd,
|
|
564
|
+
goal: this.goal,
|
|
552
565
|
headless: this.headless,
|
|
553
566
|
metadata: { toolCallId: call.id },
|
|
554
567
|
});
|
|
@@ -578,6 +591,7 @@ class StreamProcessor extends EventEmitter {
|
|
|
578
591
|
type: 'permission_resolved',
|
|
579
592
|
sessionId,
|
|
580
593
|
permId: askedPermId,
|
|
594
|
+
toolCallId: call.id,
|
|
581
595
|
tool: call.name,
|
|
582
596
|
decision: permission.decision,
|
|
583
597
|
source: permission.source || '',
|
|
@@ -623,7 +637,7 @@ class StreamProcessor extends EventEmitter {
|
|
|
623
637
|
confidence: Confidence.HIGH,
|
|
624
638
|
data: { toolCallId: call.id, name: call.name },
|
|
625
639
|
});
|
|
626
|
-
let result = await this.toolExecutor(call, { sessionId, cwd, model: state.model, provider: state.provider });
|
|
640
|
+
let result = await this.toolExecutor(call, { sessionId, cwd, model: state.model, provider: state.provider, signal: state.signal });
|
|
627
641
|
await this._recordDiagnostics(sessionId, cwd, call, result);
|
|
628
642
|
const typedArtifacts = this._storeTypedArtifacts(result, { sessionId, cwd, call });
|
|
629
643
|
const routed = this._routeToolOutput(result, { sessionId, cwd, call });
|
|
@@ -742,6 +756,53 @@ class StreamProcessor extends EventEmitter {
|
|
|
742
756
|
}
|
|
743
757
|
}
|
|
744
758
|
|
|
759
|
+
async _prepareToolCall(call, state, { sessionId, cwd }) {
|
|
760
|
+
if (!this.prepareToolCall) return call;
|
|
761
|
+
try {
|
|
762
|
+
const prepared = await this.prepareToolCall(call, { sessionId, cwd, model: state.model, provider: state.provider });
|
|
763
|
+
const nextCall = normalizeToolCall(prepared?.call || call);
|
|
764
|
+
if (!nextCall.id) nextCall.id = call.id;
|
|
765
|
+
const index = state.toolCalls.findIndex((item) => String(item.id) === String(call.id));
|
|
766
|
+
if (index >= 0) state.toolCalls[index] = nextCall;
|
|
767
|
+
if (prepared?.decision && prepared.decision.action && prepared.decision.action !== 'skip') {
|
|
768
|
+
await this._record(sessionId, cwd, 'tool', {
|
|
769
|
+
state: 'prepared',
|
|
770
|
+
toolCallId: nextCall.id,
|
|
771
|
+
name: nextCall.name,
|
|
772
|
+
input: nextCall.input,
|
|
773
|
+
actionMemory: {
|
|
774
|
+
decision: prepared.decision,
|
|
775
|
+
candidateCount: prepared.candidateCount || 0,
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
this._emit({
|
|
779
|
+
type: 'action_memory',
|
|
780
|
+
sessionId,
|
|
781
|
+
toolCallId: nextCall.id,
|
|
782
|
+
tool: nextCall.name,
|
|
783
|
+
decision: prepared.decision,
|
|
784
|
+
candidateCount: prepared.candidateCount || 0,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
return nextCall;
|
|
788
|
+
} catch (err) {
|
|
789
|
+
await this._record(sessionId, cwd, 'tool', {
|
|
790
|
+
state: 'prepare_failed',
|
|
791
|
+
toolCallId: call.id,
|
|
792
|
+
name: call.name,
|
|
793
|
+
error: err.message,
|
|
794
|
+
});
|
|
795
|
+
this._emit({
|
|
796
|
+
type: 'action_memory',
|
|
797
|
+
sessionId,
|
|
798
|
+
toolCallId: call.id,
|
|
799
|
+
tool: call.name,
|
|
800
|
+
error: err.message,
|
|
801
|
+
});
|
|
802
|
+
return call;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
745
806
|
_routeToolOutput(result, { sessionId, call }) {
|
|
746
807
|
if (!this.artifactStore?.routeToolOutput) return { result, artifact: null };
|
|
747
808
|
return this.artifactStore.routeToolOutput(result, {
|
|
@@ -23,6 +23,7 @@ class ToolExecutionController {
|
|
|
23
23
|
model = '',
|
|
24
24
|
mode = '',
|
|
25
25
|
runtimeMode = '',
|
|
26
|
+
goal = '',
|
|
26
27
|
headless = false,
|
|
27
28
|
benchmark = false,
|
|
28
29
|
handlePermissions = true,
|
|
@@ -43,6 +44,9 @@ class ToolExecutionController {
|
|
|
43
44
|
this.model = model;
|
|
44
45
|
this.mode = mode;
|
|
45
46
|
this.runtimeMode = runtimeMode;
|
|
47
|
+
// The coding task / user request — forwarded to the permission layer so CTM's
|
|
48
|
+
// goal-aligned auto-approver can judge by goal-alignment. Best-effort context.
|
|
49
|
+
this.goal = goal || '';
|
|
46
50
|
this.headless = Boolean(headless);
|
|
47
51
|
this.benchmark = Boolean(benchmark);
|
|
48
52
|
this.handlePermissions = handlePermissions !== false;
|
|
@@ -85,6 +89,7 @@ class ToolExecutionController {
|
|
|
85
89
|
cwd: input.cwd || base.cwd,
|
|
86
90
|
projectRoot: base.projectRoot,
|
|
87
91
|
mode: base.mode,
|
|
92
|
+
goal: base.goal || this.goal,
|
|
88
93
|
headless: this.headless || this.benchmark,
|
|
89
94
|
metadata: { toolCallId },
|
|
90
95
|
});
|
|
@@ -167,6 +172,7 @@ class ToolExecutionController {
|
|
|
167
172
|
model: ctx.model || this.model,
|
|
168
173
|
mode: ctx.mode || this.mode,
|
|
169
174
|
runtimeMode: ctx.runtimeMode || this.runtimeMode,
|
|
175
|
+
goal: ctx.goal || this.goal,
|
|
170
176
|
llmCtx: ctx.llmCtx || {
|
|
171
177
|
sessionId: ctx.sessionId || this.sessionId,
|
|
172
178
|
cwd: ctx.cwd || this.cwd,
|
|
@@ -177,6 +183,11 @@ class ToolExecutionController {
|
|
|
177
183
|
},
|
|
178
184
|
interactive: ctx.interactive,
|
|
179
185
|
onTodos: ctx.onTodos,
|
|
186
|
+
// Abort signal (user Stop / turn cancel) + progress heartbeat, forwarded to
|
|
187
|
+
// long-running tools (run_shell) so Stop kills the child and a slow command
|
|
188
|
+
// shows live motion. Best-effort; both may be undefined.
|
|
189
|
+
signal: ctx.signal,
|
|
190
|
+
onProgress: ctx.onProgress,
|
|
180
191
|
};
|
|
181
192
|
}
|
|
182
193
|
|
|
@@ -192,7 +203,7 @@ class ToolExecutionController {
|
|
|
192
203
|
if (name === 'glob' && !input.directory) input.directory = base.cwd;
|
|
193
204
|
if (name === 'grep_files' && !input.directory) input.directory = base.cwd;
|
|
194
205
|
if (name === 'run_shell') {
|
|
195
|
-
input.timeout_ms = input.timeout_ms || 30000;
|
|
206
|
+
input.timeout_ms = input.timeout_ms || Number(process.env.WALLE_SHELL_TIMEOUT_MS) || 30000;
|
|
196
207
|
input.cwd = input.cwd || base.cwd;
|
|
197
208
|
}
|
|
198
209
|
return input;
|
|
@@ -244,6 +255,8 @@ class ToolExecutionController {
|
|
|
244
255
|
provider: base.provider,
|
|
245
256
|
llmCtx: base.llmCtx,
|
|
246
257
|
lifecycleHooks: this.lifecycleHooks,
|
|
258
|
+
signal: base.signal,
|
|
259
|
+
onProgress: base.onProgress,
|
|
247
260
|
});
|
|
248
261
|
}
|
|
249
262
|
|
|
@@ -163,7 +163,7 @@ class ToolRegistry {
|
|
|
163
163
|
|
|
164
164
|
const local = this.localTools || safeLocalTools();
|
|
165
165
|
if (local?.executeLocalTool) {
|
|
166
|
-
const result = await local.executeLocalTool(name, args);
|
|
166
|
+
const result = await local.executeLocalTool(name, args, ctx);
|
|
167
167
|
if (result !== null && result !== undefined) return result;
|
|
168
168
|
}
|
|
169
169
|
return invalidTool(name, `Unknown tool: ${name}`);
|
|
@@ -100,6 +100,9 @@ class CodingTranscript {
|
|
|
100
100
|
record.modelId = opts.model || opts.modelId || this.defaults.modelId || '';
|
|
101
101
|
record.modelProvider = opts.provider || opts.modelProvider || this.defaults.modelProvider || '';
|
|
102
102
|
record.tokens = opts.tokens || {};
|
|
103
|
+
// Claude-Code-parity per-message usage breakdown (input/output/cache_* tokens),
|
|
104
|
+
// mirroring its JSONL `message.usage`. `tokens` stays for backward compat.
|
|
105
|
+
record.usage = opts.usage || null;
|
|
103
106
|
record.cost = Number.isFinite(opts.cost) ? opts.cost : null;
|
|
104
107
|
return this.append(record);
|
|
105
108
|
}
|