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
|
@@ -73,12 +73,19 @@ function startServer(options = {}) {
|
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// Task stop endpoint —
|
|
76
|
+
// Task stop endpoint — task execution may live in a runtime worker, so ask
|
|
77
|
+
// every worker and keep the old in-process path as a recovery fallback.
|
|
77
78
|
const taskStopMatch = url.pathname.match(/^\/api\/wall-e\/tasks\/([^/]+)\/stop$/);
|
|
78
79
|
if (taskStopMatch && req.method === 'POST') {
|
|
79
80
|
try {
|
|
81
|
+
let stopped = false;
|
|
82
|
+
try {
|
|
83
|
+
const { broadcastRuntimeWorker } = require('./lib/runtime-worker-pool');
|
|
84
|
+
const results = await broadcastRuntimeWorker('tasks.stop', { taskId: taskStopMatch[1] }, { timeoutMs: 1500 });
|
|
85
|
+
stopped = results.some((entry) => entry.ok && entry.result?.stopped);
|
|
86
|
+
} catch {}
|
|
80
87
|
const { stopTask } = require('./loops/tasks');
|
|
81
|
-
|
|
88
|
+
stopped = stopTask(taskStopMatch[1]) || stopped;
|
|
82
89
|
const body = JSON.stringify({ data: { stopped } });
|
|
83
90
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
|
|
84
91
|
res.end(body);
|
|
@@ -94,9 +101,20 @@ function startServer(options = {}) {
|
|
|
94
101
|
const taskRunNowMatch = url.pathname.match(/^\/api\/wall-e\/tasks\/([^/]+)\/run-now$/);
|
|
95
102
|
if (taskRunNowMatch && req.method === 'POST') {
|
|
96
103
|
try {
|
|
97
|
-
const
|
|
98
|
-
// Fire and forget — respond immediately, task runs in
|
|
99
|
-
|
|
104
|
+
const taskId = taskRunNowMatch[1];
|
|
105
|
+
// Fire and forget — respond immediately, task runs in the runtime
|
|
106
|
+
// worker when available, with local execution as the opt-out/failure
|
|
107
|
+
// fallback.
|
|
108
|
+
(async () => {
|
|
109
|
+
try {
|
|
110
|
+
const { requestRuntimeWorker } = require('./lib/runtime-worker-pool');
|
|
111
|
+
await requestRuntimeWorker('tasks.runById', { taskId }, { heavy: true, timeoutMs: 0 });
|
|
112
|
+
} catch (workerErr) {
|
|
113
|
+
console.warn('[wall-e] run-now worker unavailable; falling back locally:', workerErr.message);
|
|
114
|
+
const { runTaskById } = require('./loops/tasks');
|
|
115
|
+
await runTaskById(taskId);
|
|
116
|
+
}
|
|
117
|
+
})().catch(e => {
|
|
100
118
|
console.error('[wall-e] run-now background error:', e.message);
|
|
101
119
|
});
|
|
102
120
|
const body = JSON.stringify({ data: { started: true } });
|
|
@@ -110,12 +128,19 @@ function startServer(options = {}) {
|
|
|
110
128
|
return;
|
|
111
129
|
}
|
|
112
130
|
|
|
113
|
-
// Task logs endpoint —
|
|
131
|
+
// Task logs endpoint — task execution may live in a runtime worker.
|
|
114
132
|
const taskLogsMatch = url.pathname.match(/^\/api\/wall-e\/tasks\/([^/]+)\/logs$/);
|
|
115
133
|
if (taskLogsMatch && req.method === 'GET') {
|
|
116
134
|
try {
|
|
135
|
+
let logs = null;
|
|
136
|
+
try {
|
|
137
|
+
const { broadcastRuntimeWorker } = require('./lib/runtime-worker-pool');
|
|
138
|
+
const results = await broadcastRuntimeWorker('tasks.getLogs', { taskId: taskLogsMatch[1] }, { timeoutMs: 1500 });
|
|
139
|
+
const hit = results.find((entry) => entry.ok && entry.result);
|
|
140
|
+
if (hit) logs = hit.result;
|
|
141
|
+
} catch {}
|
|
117
142
|
const { getTaskLogs } = require('./loops/tasks');
|
|
118
|
-
|
|
143
|
+
logs = logs || getTaskLogs(taskLogsMatch[1]);
|
|
119
144
|
const body = JSON.stringify({ data: logs || { lines: [], status: 'unknown' } });
|
|
120
145
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
|
|
121
146
|
res.end(body);
|
|
@@ -60,7 +60,14 @@ function runScriptSkill(skill, task = {}, opts = {}) {
|
|
|
60
60
|
for (const line of chunk.split('\n').filter(Boolean)) {
|
|
61
61
|
log(line);
|
|
62
62
|
if (line.startsWith('CHECKPOINT:') && typeof opts.onCheckpoint === 'function') {
|
|
63
|
-
|
|
63
|
+
try {
|
|
64
|
+
const maybePromise = opts.onCheckpoint(line.slice(11).trim());
|
|
65
|
+
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
66
|
+
maybePromise.catch(err => log(`[checkpoint skipped] ${err?.message || err}`));
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
log(`[checkpoint skipped] ${err?.message || err}`);
|
|
70
|
+
}
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
});
|
|
@@ -16,6 +16,7 @@ try { telemetry = require('../telemetry'); } catch { telemetry = { trackError()
|
|
|
16
16
|
|
|
17
17
|
const DECISION_TELEMETRY_MIN_INTERVAL_MS = 60 * 60 * 1000;
|
|
18
18
|
const _lastDecisionTelemetry = new Map();
|
|
19
|
+
const _lastBackoffRecoveryLog = new Map();
|
|
19
20
|
let _slackAlertMigrationDone = false;
|
|
20
21
|
|
|
21
22
|
function isBrainReady() {
|
|
@@ -54,6 +55,25 @@ function shouldTrackDecisionTelemetry(skillName, decision) {
|
|
|
54
55
|
return true;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
function shouldLogBackoffRecovery(skillName, decision) {
|
|
59
|
+
const details = decision?.details || {};
|
|
60
|
+
const signature = [
|
|
61
|
+
decision?.reason || '',
|
|
62
|
+
details.consecutive_failures || 0,
|
|
63
|
+
details.next_allowed_at || '',
|
|
64
|
+
details.backoff_ms || 0,
|
|
65
|
+
details.schedule_source || '',
|
|
66
|
+
].join('|');
|
|
67
|
+
const key = String(skillName || '');
|
|
68
|
+
if (_lastBackoffRecoveryLog.get(key) === signature) return false;
|
|
69
|
+
_lastBackoffRecoveryLog.set(key, signature);
|
|
70
|
+
if (_lastBackoffRecoveryLog.size > 500) {
|
|
71
|
+
const first = _lastBackoffRecoveryLog.keys().next().value;
|
|
72
|
+
if (first) _lastBackoffRecoveryLog.delete(first);
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
// ── Service alerts ─────────────────────────────────────────────────────────
|
|
58
78
|
// Stored in kv_store as JSON array. Surfaces auth failures, version updates, etc.
|
|
59
79
|
|
|
@@ -132,6 +152,48 @@ function clearServiceAlerts(service) {
|
|
|
132
152
|
brain.setKv('service_alerts', JSON.stringify(alerts));
|
|
133
153
|
}
|
|
134
154
|
|
|
155
|
+
function _providerAlertMatches(alert, provider, model) {
|
|
156
|
+
const providerNeedle = String(provider || '').trim().toLowerCase();
|
|
157
|
+
const modelNeedle = String(model || '').trim().toLowerCase();
|
|
158
|
+
const providerError = alert?.provider_error || alert?.providerError || {};
|
|
159
|
+
const alertProvider = String(providerError.provider || alert?.provider || '').trim().toLowerCase();
|
|
160
|
+
const alertModel = String(providerError.model || alert?.model || '').trim().toLowerCase();
|
|
161
|
+
if (providerNeedle && alertProvider && alertProvider !== providerNeedle) return false;
|
|
162
|
+
if (modelNeedle && alertModel && alertModel !== modelNeedle) return false;
|
|
163
|
+
if (providerNeedle && !alertProvider && !String(alert?.type || '').toLowerCase().includes(providerNeedle)) return false;
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function _isTransientProviderAlert(alert) {
|
|
168
|
+
const providerError = alert?.provider_error || alert?.providerError || {};
|
|
169
|
+
const text = [
|
|
170
|
+
alert?.service,
|
|
171
|
+
alert?.type,
|
|
172
|
+
alert?.title,
|
|
173
|
+
alert?.message,
|
|
174
|
+
providerError.type,
|
|
175
|
+
providerError.title,
|
|
176
|
+
providerError.message,
|
|
177
|
+
providerError.userMessage,
|
|
178
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
179
|
+
if (!text.includes('ai_provider') && String(alert?.service || '') !== 'ai_provider') return false;
|
|
180
|
+
if (/auth|quota|billing|credit|insufficient|invalid|forbidden|unauthorized|permission/.test(text)) return false;
|
|
181
|
+
return /timeout|timed out|abort|aborted|network|unavailable|overloaded|rate_limit|connection|reset|econn/.test(text);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function clearRecoveredProviderAlerts({ provider, model } = {}) {
|
|
185
|
+
const before = getServiceAlerts();
|
|
186
|
+
const after = before.filter((alert) => {
|
|
187
|
+
if (!_isTransientProviderAlert(alert)) return true;
|
|
188
|
+
if (!_providerAlertMatches(alert, provider, model)) return true;
|
|
189
|
+
return false;
|
|
190
|
+
});
|
|
191
|
+
if (after.length !== before.length) {
|
|
192
|
+
brain.setKv('service_alerts', JSON.stringify(after));
|
|
193
|
+
}
|
|
194
|
+
return { cleared: before.length - after.length };
|
|
195
|
+
}
|
|
196
|
+
|
|
135
197
|
function clearResolvedSlackHealthAlerts({ authenticated = false, ownerConfigured = false } = {}) {
|
|
136
198
|
if (!authenticated) return { cleared: 0 };
|
|
137
199
|
const before = getServiceAlerts();
|
|
@@ -631,7 +693,7 @@ async function runDueSkills(opts = {}) {
|
|
|
631
693
|
}
|
|
632
694
|
// Recovery logger: long-form structured trace for post-incident
|
|
633
695
|
// debugging (Items E + A of error-recovery deep-dive).
|
|
634
|
-
if (decision.reason === 'backed_off') {
|
|
696
|
+
if (decision.reason === 'backed_off' && shouldLogBackoffRecovery(skill.name, decision)) {
|
|
635
697
|
try {
|
|
636
698
|
require('../lib/recovery-logger').warn('skill-dispatch', 'backoff_applied', {
|
|
637
699
|
skill: skill.name,
|
|
@@ -909,6 +971,7 @@ module.exports = {
|
|
|
909
971
|
addServiceAlert,
|
|
910
972
|
dismissServiceAlert,
|
|
911
973
|
clearServiceAlerts,
|
|
974
|
+
clearRecoveredProviderAlerts,
|
|
912
975
|
clearResolvedSlackHealthAlerts,
|
|
913
976
|
clearResolvedGoogleWorkspaceHealthAlerts,
|
|
914
977
|
_test: {
|
|
@@ -15,6 +15,69 @@ function providerId(provider) {
|
|
|
15
15
|
return provider.type || provider.provider || provider.id || '';
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const OLLAMA_GEMMA_CORE_TOOL_NAMES = new Set([
|
|
19
|
+
'read_file',
|
|
20
|
+
'write_file',
|
|
21
|
+
'run_shell',
|
|
22
|
+
'bg_output',
|
|
23
|
+
'bg_kill',
|
|
24
|
+
'glob',
|
|
25
|
+
'grep_files',
|
|
26
|
+
'list_directory',
|
|
27
|
+
'browser_screenshot',
|
|
28
|
+
'browser_smoke_test',
|
|
29
|
+
'check_url',
|
|
30
|
+
'web_search',
|
|
31
|
+
'web_fetch',
|
|
32
|
+
'start_static_server',
|
|
33
|
+
'stop_static_server',
|
|
34
|
+
'pdf_info',
|
|
35
|
+
'pdf_render_pages',
|
|
36
|
+
'pdf_read_pages',
|
|
37
|
+
'make_pdf',
|
|
38
|
+
'edit_file',
|
|
39
|
+
'apply_patch',
|
|
40
|
+
'multi_edit',
|
|
41
|
+
'update_todos',
|
|
42
|
+
'lsp_symbols',
|
|
43
|
+
'lsp_definition',
|
|
44
|
+
'lsp_references',
|
|
45
|
+
'lsp_diagnostics',
|
|
46
|
+
'lsp_hover',
|
|
47
|
+
'lsp_implementation',
|
|
48
|
+
'ask_user',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
function isGemma4Ollama(provider, model) {
|
|
52
|
+
return providerId(provider) === 'ollama' && /^gemma4:/i.test(String(model || ''));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function compactToolsForGemma4Ollama(ctx = {}, tools = []) {
|
|
56
|
+
if (process.env.WALLE_OLLAMA_FULL_TOOL_CATALOG === '1' || process.env.WALLE_OLLAMA_COMPACT_TOOLS === '0') {
|
|
57
|
+
return tools;
|
|
58
|
+
}
|
|
59
|
+
const provider = ctx.provider || '';
|
|
60
|
+
const model = String(ctx.model || '');
|
|
61
|
+
if (!isGemma4Ollama(provider, model)) return tools;
|
|
62
|
+
const core = tools.filter((tool) => OLLAMA_GEMMA_CORE_TOOL_NAMES.has(tool?.name));
|
|
63
|
+
if (core.length === 0) return tools;
|
|
64
|
+
|
|
65
|
+
const promptCapabilities = ctx.promptCapabilities || {};
|
|
66
|
+
const explicitSkills = Array.isArray(promptCapabilities.skills) && promptCapabilities.skills.length > 0;
|
|
67
|
+
const explicitMcp = Boolean(
|
|
68
|
+
promptCapabilities.mcp
|
|
69
|
+
&& (
|
|
70
|
+
(Array.isArray(promptCapabilities.mcp.servers) && promptCapabilities.mcp.servers.length > 0)
|
|
71
|
+
|| (Array.isArray(promptCapabilities.mcp.tools) && promptCapabilities.mcp.tools.length > 0)
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
const extras = tools.filter((tool) => (
|
|
75
|
+
(explicitSkills && (tool?.name === 'load_skill' || tool?.name === 'skill'))
|
|
76
|
+
|| (explicitMcp && tool?.name === 'mcp_call')
|
|
77
|
+
));
|
|
78
|
+
return [...core, ...extras];
|
|
79
|
+
}
|
|
80
|
+
|
|
18
81
|
function isOpenWeightReasoningModel(provider, model) {
|
|
19
82
|
if (providerId(provider) !== 'ollama') return false;
|
|
20
83
|
return isReasoningCapableOllamaModel(model);
|
|
@@ -178,12 +241,14 @@ Do not make any edits. Review the diff and assess quality.`);
|
|
|
178
241
|
// -- tool.definitions: adapt per model --
|
|
179
242
|
mw.use('tool.definitions', async (ctx, tools) => {
|
|
180
243
|
const provider = ctx.provider || opts.provider;
|
|
181
|
-
|
|
244
|
+
const model = String(ctx.model || opts.model || '');
|
|
245
|
+
if (providerId(provider) === 'ollama') {
|
|
182
246
|
// Brief descriptions for small models
|
|
183
|
-
|
|
247
|
+
const shortened = tools.map(t => ({
|
|
184
248
|
...t,
|
|
185
249
|
description: t.description.split('.')[0] + '.',
|
|
186
250
|
}));
|
|
251
|
+
return compactToolsForGemma4Ollama({ ...ctx, provider, model }, shortened);
|
|
187
252
|
}
|
|
188
253
|
return tools;
|
|
189
254
|
});
|
|
@@ -10,8 +10,29 @@ const https = require('https');
|
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
11
|
const execAsync = promisify(exec);
|
|
12
12
|
const { SHELL_ALLOWLIST, SHELL_DENYLIST } = require('./shell-policy');
|
|
13
|
+
const shellSandbox = require('../coding/shell-sandbox');
|
|
13
14
|
|
|
14
15
|
const HOME = process.env.HOME || '/tmp';
|
|
16
|
+
|
|
17
|
+
// When WALLE_SHELL_SANDBOX is on and an OS sandbox backend exists, return the
|
|
18
|
+
// sandbox-wrapped argv for a run_shell command (Seatbelt/bubblewrap; writes contained to
|
|
19
|
+
// cwd+tmp, network allowed). Returns null to run un-sandboxed (flag off, no backend, or a
|
|
20
|
+
// hard-denylisted head — those keep going through the normal allowlist gate and are denied
|
|
21
|
+
// there). Under the sandbox the OS is the safety boundary, so the allowlist requirement is
|
|
22
|
+
// skipped for the command; the destructive denylist is still honored here as a floor.
|
|
23
|
+
function maybeSandboxShellArgv(commandStr, cwd) {
|
|
24
|
+
if (!shellSandbox.shellSandboxEnabled() || !shellSandbox.sandboxAvailable()) return null;
|
|
25
|
+
// Defense-in-depth floor: never sandbox a command whose ANY clause head is on the hard
|
|
26
|
+
// denylist (sudo/dd/mkfs/…). Split on shell operators so `echo x && sudo y` is caught too.
|
|
27
|
+
// Returning null routes it through the normal allowlist gate, which denies it. The OS
|
|
28
|
+
// sandbox is still the real boundary for everything that does run.
|
|
29
|
+
const clauses = String(commandStr || '').split(/&&|\|\||[|;\n]/);
|
|
30
|
+
for (const clause of clauses) {
|
|
31
|
+
const head = path.basename(clause.trim().split(/\s+/)[0] || '');
|
|
32
|
+
if (head && SHELL_DENYLIST.has(head)) return null;
|
|
33
|
+
}
|
|
34
|
+
return shellSandbox.wrapShellArgv({ command: commandStr, cwd });
|
|
35
|
+
}
|
|
15
36
|
const MAIL_AUTOMATION_LOCK_PATH = process.env.WALLE_MAIL_LOCK_PATH ||
|
|
16
37
|
path.join(os.tmpdir(), 'wall-e-mail-appleevent.lock');
|
|
17
38
|
const MAIL_AUTOMATION_COOLDOWN_PATH = process.env.WALLE_MAIL_COOLDOWN_PATH ||
|
|
@@ -1527,9 +1548,14 @@ async function runShell(commandOrOpts, legacyArgs, legacyOpts) {
|
|
|
1527
1548
|
let background = false;
|
|
1528
1549
|
let sessionId = '';
|
|
1529
1550
|
let persist = false;
|
|
1551
|
+
// signal: aborts the child when the turn is cancelled (user Stop). onProgress:
|
|
1552
|
+
// a heartbeat callback so a long-running command shows live motion instead of a
|
|
1553
|
+
// frozen "Running run_shell" card. Both are best-effort and optional.
|
|
1554
|
+
let signal = null;
|
|
1555
|
+
let onProgress = null;
|
|
1530
1556
|
if (typeof commandOrOpts === 'object' && commandOrOpts !== null) {
|
|
1531
|
-
// New format: runShell({ command, args, timeout_ms, cwd, background })
|
|
1532
|
-
({ command, args, timeout_ms = 30000, cwd, background = false, sessionId = '', persist = false } = commandOrOpts);
|
|
1557
|
+
// New format: runShell({ command, args, timeout_ms, cwd, background, signal, onProgress })
|
|
1558
|
+
({ command, args, timeout_ms = 30000, cwd, background = false, sessionId = '', persist = false, signal = null, onProgress = null } = commandOrOpts);
|
|
1533
1559
|
} else {
|
|
1534
1560
|
// Legacy format: runShell(command, args, { timeout_ms })
|
|
1535
1561
|
command = commandOrOpts;
|
|
@@ -1562,28 +1588,84 @@ async function runShell(commandOrOpts, legacyArgs, legacyOpts) {
|
|
|
1562
1588
|
return runShellBackground({ command: commandStr, cwd, sessionId, persist });
|
|
1563
1589
|
}
|
|
1564
1590
|
|
|
1565
|
-
//
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1591
|
+
// Run contained in the OS sandbox when enabled; otherwise gate on the static
|
|
1592
|
+
// allowlist via tree-sitter analysis.
|
|
1593
|
+
const sandboxArgv = maybeSandboxShellArgv(commandStr, cwd);
|
|
1594
|
+
if (!sandboxArgv) {
|
|
1595
|
+
const analysis = await analyzeShellCommand(commandStr, cwd || process.cwd());
|
|
1596
|
+
if (!analysis.allowed) {
|
|
1597
|
+
throw new Error(analysis.reason);
|
|
1598
|
+
}
|
|
1569
1599
|
}
|
|
1570
1600
|
|
|
1601
|
+
// Per-command timeout: honor the caller's value (the controller defaults it from
|
|
1602
|
+
// WALLE_SHELL_TIMEOUT_MS) but clamp to a hard ceiling so a single command can
|
|
1603
|
+
// never wedge a turn indefinitely.
|
|
1604
|
+
const SHELL_HARD_TIMEOUT_CAP_MS = 600000; // 10 min
|
|
1605
|
+
const SHELL_PROGRESS_AFTER_MS = Number(process.env.WALLE_SHELL_PROGRESS_AFTER_MS) || 8000;
|
|
1606
|
+
let effectiveTimeout = Number(timeout_ms) > 0 ? Number(timeout_ms) : (Number(process.env.WALLE_SHELL_TIMEOUT_MS) || 30000);
|
|
1607
|
+
effectiveTimeout = Math.min(effectiveTimeout, SHELL_HARD_TIMEOUT_CAP_MS);
|
|
1608
|
+
|
|
1609
|
+
// Heartbeat: surface a "still running (Ns)" progress beat for long commands so a
|
|
1610
|
+
// multi-second shell step doesn't read as a frozen card. Gated by WALLE_SHELL_PROGRESS.
|
|
1611
|
+
const startedAt = Date.now();
|
|
1612
|
+
const progressEnabled = typeof onProgress === 'function' && process.env.WALLE_SHELL_PROGRESS !== '0';
|
|
1613
|
+
let hbTimer = null;
|
|
1614
|
+
if (progressEnabled) {
|
|
1615
|
+
hbTimer = setInterval(() => {
|
|
1616
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1617
|
+
try {
|
|
1618
|
+
onProgress({
|
|
1619
|
+
type: 'shell_progress',
|
|
1620
|
+
message: `run_shell still running (${Math.round(elapsedMs / 1000)}s): ${commandStr.slice(0, 80)}`,
|
|
1621
|
+
elapsedMs,
|
|
1622
|
+
command: commandStr.slice(0, 200),
|
|
1623
|
+
});
|
|
1624
|
+
} catch { /* progress is best-effort */ }
|
|
1625
|
+
}, SHELL_PROGRESS_AFTER_MS);
|
|
1626
|
+
if (hbTimer.unref) hbTimer.unref();
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// Serial loops (for…do…done / while…do…done) of separate sub-commands are the
|
|
1630
|
+
// classic budget sink — nudge the model toward one batched command or background.
|
|
1631
|
+
const looksLikeSerialLoop = /\bfor\b[\s\S]*\bdo\b[\s\S]*\bdone\b/.test(commandStr) || /\bwhile\b[\s\S]*\bdo\b[\s\S]*\bdone\b/.test(commandStr);
|
|
1632
|
+
|
|
1571
1633
|
try {
|
|
1572
|
-
const { stdout, stderr } =
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1634
|
+
const { stdout, stderr } = sandboxArgv
|
|
1635
|
+
? await execFileAsync(sandboxArgv[0], sandboxArgv.slice(1), {
|
|
1636
|
+
timeout: effectiveTimeout,
|
|
1637
|
+
maxBuffer: 1024 * 1024, // 1MB
|
|
1638
|
+
cwd: cwd || undefined,
|
|
1639
|
+
env: { ...process.env, HOME },
|
|
1640
|
+
signal: signal || undefined,
|
|
1641
|
+
})
|
|
1642
|
+
: await execAsync(commandStr, {
|
|
1643
|
+
timeout: effectiveTimeout,
|
|
1644
|
+
maxBuffer: 1024 * 1024, // 1MB
|
|
1645
|
+
cwd: cwd || undefined,
|
|
1646
|
+
env: { ...process.env, HOME },
|
|
1647
|
+
shell: '/bin/bash',
|
|
1648
|
+
signal: signal || undefined,
|
|
1649
|
+
});
|
|
1650
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1651
|
+
const result = {
|
|
1580
1652
|
stdout: stdout.slice(0, 50000),
|
|
1581
1653
|
stderr: stderr.slice(0, 5000),
|
|
1582
1654
|
};
|
|
1655
|
+
if (looksLikeSerialLoop && elapsedMs >= SHELL_PROGRESS_AFTER_MS) {
|
|
1656
|
+
result.hint = `This serial loop took ${Math.round(elapsedMs / 1000)}s. For many similar operations, prefer ONE batched command (most CLIs accept multiple paths) or run it with background:true and poll bg_output, instead of a loop of separate run_shell calls.`;
|
|
1657
|
+
}
|
|
1658
|
+
return result;
|
|
1583
1659
|
} catch (err) {
|
|
1584
|
-
//
|
|
1660
|
+
// User Stop / turn abort: the child was killed via the AbortSignal. Return a
|
|
1661
|
+
// clean cancelled result rather than a confusing "Command failed: SIGTERM".
|
|
1662
|
+
if (signal?.aborted || err.code === 'ABORT_ERR' || err.name === 'AbortError') {
|
|
1663
|
+
return { stdout: (err.stdout || '').slice(0, 50000), stderr: (err.stderr || '').slice(0, 5000), cancelled: true, error: 'Command cancelled' };
|
|
1664
|
+
}
|
|
1665
|
+
// Timeout / killed: re-throw so callers see the failure, with a route forward.
|
|
1585
1666
|
if (err.killed || err.signal) {
|
|
1586
|
-
|
|
1667
|
+
const why = err.signal === 'SIGTERM' ? `timed out after ${effectiveTimeout}ms` : (err.signal || 'killed');
|
|
1668
|
+
throw new Error(`Command failed: ${why}. For long-running work, pass background:true and poll bg_output, or raise timeout_ms (cap ${SHELL_HARD_TIMEOUT_CAP_MS}ms).`);
|
|
1587
1669
|
}
|
|
1588
1670
|
// Non-zero exit code: return output as result, not as error
|
|
1589
1671
|
// This is important for test runners that exit 1 on test failure
|
|
@@ -1595,6 +1677,8 @@ async function runShell(commandOrOpts, legacyArgs, legacyOpts) {
|
|
|
1595
1677
|
};
|
|
1596
1678
|
}
|
|
1597
1679
|
throw err; // Re-throw actual errors (spawn failure, etc.)
|
|
1680
|
+
} finally {
|
|
1681
|
+
if (hbTimer) clearInterval(hbTimer);
|
|
1598
1682
|
}
|
|
1599
1683
|
}
|
|
1600
1684
|
|
|
@@ -3075,19 +3159,25 @@ async function runShellBackground({ command, cwd, sessionId = '', persist = fals
|
|
|
3075
3159
|
if (!command || typeof command !== 'string') return { ok: false, error: 'command is required' };
|
|
3076
3160
|
const { analyzeShellCommand, initParser } = require('./shell-analyzer');
|
|
3077
3161
|
await initParser();
|
|
3078
|
-
const
|
|
3079
|
-
if (!
|
|
3162
|
+
const sandboxArgv = maybeSandboxShellArgv(command, cwd);
|
|
3163
|
+
if (!sandboxArgv) {
|
|
3164
|
+
const analysis = await analyzeShellCommand(command, cwd || process.cwd());
|
|
3165
|
+
if (!analysis.allowed) return { ok: false, error: analysis.reason };
|
|
3166
|
+
}
|
|
3080
3167
|
|
|
3081
3168
|
const logFile = path.join(os.tmpdir(), `walle-bg-${Date.now()}-${Math.random().toString(36).slice(2)}.log`);
|
|
3082
3169
|
const logFd = fs.openSync(logFile, 'a');
|
|
3083
3170
|
let child;
|
|
3084
3171
|
try {
|
|
3085
|
-
child = require('child_process').spawn(
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3172
|
+
child = require('child_process').spawn(
|
|
3173
|
+
sandboxArgv ? sandboxArgv[0] : '/bin/bash',
|
|
3174
|
+
sandboxArgv ? sandboxArgv.slice(1) : ['-c', command],
|
|
3175
|
+
{
|
|
3176
|
+
cwd: cwd || undefined,
|
|
3177
|
+
env: { ...process.env, HOME },
|
|
3178
|
+
stdio: ['ignore', logFd, logFd],
|
|
3179
|
+
detached: true, // own process group so bg_kill can stop child trees
|
|
3180
|
+
});
|
|
3091
3181
|
} catch (err) {
|
|
3092
3182
|
fs.closeSync(logFd);
|
|
3093
3183
|
return { ok: false, error: err.message };
|
|
@@ -4801,14 +4891,14 @@ function getLspManager() { return _lspManager; }
|
|
|
4801
4891
|
|
|
4802
4892
|
// ── Tool executor dispatch ──
|
|
4803
4893
|
|
|
4804
|
-
async function executeLocalTool(name, input) {
|
|
4894
|
+
async function executeLocalTool(name, input, ctx = {}) {
|
|
4805
4895
|
// LSP tools (SP5) — language-aware code intelligence
|
|
4806
4896
|
if (name.startsWith('lsp_')) {
|
|
4807
4897
|
return _executeLspTool(name, input);
|
|
4808
4898
|
}
|
|
4809
4899
|
|
|
4810
4900
|
switch (name) {
|
|
4811
|
-
case 'run_shell': return runShell({ command: input.command, args: input.args, timeout_ms: input.timeout_ms, cwd: input.cwd, background: input.background, sessionId: input.sessionId || input.session_id, persist: input.persist });
|
|
4901
|
+
case 'run_shell': return runShell({ command: input.command, args: input.args, timeout_ms: input.timeout_ms, cwd: input.cwd, background: input.background, sessionId: input.sessionId || input.session_id, persist: input.persist, signal: ctx?.signal || undefined, onProgress: ctx?.onProgress || undefined });
|
|
4812
4902
|
case 'bg_output': return bgOutput(input);
|
|
4813
4903
|
case 'bg_kill': return bgKill(input);
|
|
4814
4904
|
case 'read_file': return readFile(input.file_path, { max_bytes: input.max_bytes, offset: input.offset, limit: input.limit, sessionId: input.sessionId, projectRoot: input.projectRoot });
|
|
@@ -69,6 +69,15 @@ let _ctmBaseUrl = null;
|
|
|
69
69
|
let _ctmToken = null;
|
|
70
70
|
let _userRulesCache = null;
|
|
71
71
|
|
|
72
|
+
// How long to wait for CTM's auto-approver (/api/permissions/walle-check) before
|
|
73
|
+
// giving up and falling through to the local static rules. Must comfortably cover
|
|
74
|
+
// the goal-aligned LLM verifier, whose own budget is BUILTIN_TIMEOUT_MS = 20s in
|
|
75
|
+
// claude-task-manager/lib/auto-approval-verifier.js. The previous 2s cut the
|
|
76
|
+
// verifier off for EVERY medium+ command (anything with a loop, an unknown binary,
|
|
77
|
+
// etc.) → the bridge aborted → the agent parked on a static "ask" for routine local
|
|
78
|
+
// commands. 25s = 20s verifier cap + network/parse overhead. Env-overridable.
|
|
79
|
+
const CTM_APPROVAL_TIMEOUT_MS = Number(process.env.WALLE_CTM_APPROVAL_TIMEOUT_MS) || 25000;
|
|
80
|
+
|
|
72
81
|
function configure({ ctmUrl, ctmToken } = {}) {
|
|
73
82
|
_ctmBaseUrl = ctmUrl || null;
|
|
74
83
|
_ctmToken = ctmToken || null;
|
|
@@ -154,7 +163,7 @@ function listTrustedDirectories() {
|
|
|
154
163
|
*
|
|
155
164
|
* Returns: { decision: 'allow'|'deny'|'ask', source, reason, ruleId? }
|
|
156
165
|
*/
|
|
157
|
-
async function checkPermission({ tool, command, commandTokens, commandClauses, args, projectPath, sessionId, mode }) {
|
|
166
|
+
async function checkPermission({ tool, command, commandTokens, commandClauses, args, projectPath, sessionId, mode, goal }) {
|
|
158
167
|
// Layer 1: Hardcoded denylist (safety floor — never overridable)
|
|
159
168
|
if (tool === 'run_shell') {
|
|
160
169
|
// Extract first command token for denylist check
|
|
@@ -180,13 +189,16 @@ async function checkPermission({ tool, command, commandTokens, commandClauses, a
|
|
|
180
189
|
// so coding never hard-breaks offline.
|
|
181
190
|
if (_ctmBaseUrl && tool === 'run_shell') {
|
|
182
191
|
try {
|
|
183
|
-
|
|
192
|
+
// `goal` (the coding task) lets CTM's goal-aligned verifier judge by
|
|
193
|
+
// goal-alignment instead of its weaker no-goal path; best-effort, a missing
|
|
194
|
+
// goal degrades to the prior behaviour.
|
|
195
|
+
const body = { tool, command: command || '', args: args || [], project_path: projectPath || '', session_id: sessionId || '', goal: goal || '', agent: 'walle' };
|
|
184
196
|
const url = `${_ctmBaseUrl}/api/permissions/walle-check${_ctmToken ? '?token=' + encodeURIComponent(_ctmToken) : ''}`;
|
|
185
197
|
const resp = await fetch(url, {
|
|
186
198
|
method: 'POST',
|
|
187
199
|
headers: { 'Content-Type': 'application/json' },
|
|
188
200
|
body: JSON.stringify(body),
|
|
189
|
-
signal: AbortSignal.timeout(
|
|
201
|
+
signal: AbortSignal.timeout(CTM_APPROVAL_TIMEOUT_MS),
|
|
190
202
|
});
|
|
191
203
|
if (resp.ok) {
|
|
192
204
|
const result = await resp.json();
|
|
@@ -269,6 +281,42 @@ async function checkPermission({ tool, command, commandTokens, commandClauses, a
|
|
|
269
281
|
return { decision: 'allow', source: 'default', reason: 'No restrictions' };
|
|
270
282
|
}
|
|
271
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Register a parked approval with CTM so it surfaces in the shared "Pending" tab
|
|
286
|
+
* and survives a page reload. Best-effort: when CTM is unreachable the local ask
|
|
287
|
+
* still stands on its own (durable transcript card + WORK bar), so a failure here
|
|
288
|
+
* must never block the turn. Returns the CTM row id, or null.
|
|
289
|
+
*/
|
|
290
|
+
async function registerEscalation({ requestId, tool, command, pattern, projectPath, sessionId, reason, riskLevel }) {
|
|
291
|
+
if (!_ctmBaseUrl || !requestId) return null;
|
|
292
|
+
try {
|
|
293
|
+
const url = `${_ctmBaseUrl}/api/permissions/walle-register${_ctmToken ? '?token=' + encodeURIComponent(_ctmToken) : ''}`;
|
|
294
|
+
const resp = await fetch(url, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
body: JSON.stringify({
|
|
298
|
+
request_id: requestId,
|
|
299
|
+
tool: tool || '',
|
|
300
|
+
command: command || '',
|
|
301
|
+
pattern: pattern || '',
|
|
302
|
+
project_path: projectPath || '',
|
|
303
|
+
session_id: sessionId || '',
|
|
304
|
+
reason: reason || '',
|
|
305
|
+
risk_level: riskLevel || 'medium',
|
|
306
|
+
agent: 'walle',
|
|
307
|
+
}),
|
|
308
|
+
signal: AbortSignal.timeout(4000),
|
|
309
|
+
});
|
|
310
|
+
if (resp.ok) {
|
|
311
|
+
const result = await resp.json();
|
|
312
|
+
return result && result.rowId != null ? result.rowId : null;
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
// CTM unavailable — the local ask still surfaces; nothing to do.
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
272
320
|
/**
|
|
273
321
|
* Record a permission decision (for learning)
|
|
274
322
|
*/
|
|
@@ -286,7 +334,7 @@ async function recordDecision({ tool, command, args, decision, sessionId, projec
|
|
|
286
334
|
}
|
|
287
335
|
|
|
288
336
|
module.exports = {
|
|
289
|
-
checkPermission, recordDecision, configure,
|
|
337
|
+
checkPermission, recordDecision, registerEscalation, configure,
|
|
290
338
|
evaluateBashClauses,
|
|
291
339
|
DENYLIST, ALWAYS_ASK_TOOLS, NAVIGATION_BUILTINS,
|
|
292
340
|
isDirectoryTrusted, trustDirectory, untrustDirectory, listTrustedDirectories,
|
|
@@ -145,6 +145,42 @@ const DEFAULT_RULES = [
|
|
|
145
145
|
{ permission: 'bash', pattern: 'sed *', action: 'allow' },
|
|
146
146
|
{ permission: 'bash', pattern: 'jq *', action: 'allow' },
|
|
147
147
|
{ permission: 'bash', pattern: 'diff *', action: 'allow' },
|
|
148
|
+
// More read-only inspection commands: allow. These only READ files/metadata or
|
|
149
|
+
// print derived info — they never write, delete, or execute another binary — so
|
|
150
|
+
// they should never park a coding turn awaiting approval. (`du` was the trailing
|
|
151
|
+
// clause that parked the live `cd … && find … | du -sh` turn for ~24 min: the
|
|
152
|
+
// per-clause most-restrictive evaluator returned `ask` because `du` had no rule.)
|
|
153
|
+
{ permission: 'bash', pattern: 'du *', action: 'allow' },
|
|
154
|
+
{ permission: 'bash', pattern: 'df *', action: 'allow' },
|
|
155
|
+
{ permission: 'bash', pattern: 'stat *', action: 'allow' },
|
|
156
|
+
{ permission: 'bash', pattern: 'file *', action: 'allow' },
|
|
157
|
+
{ permission: 'bash', pattern: 'tree *', action: 'allow' },
|
|
158
|
+
{ permission: 'bash', pattern: 'realpath *', action: 'allow' },
|
|
159
|
+
{ permission: 'bash', pattern: 'readlink *', action: 'allow' },
|
|
160
|
+
{ permission: 'bash', pattern: 'basename *', action: 'allow' },
|
|
161
|
+
{ permission: 'bash', pattern: 'dirname *', action: 'allow' },
|
|
162
|
+
{ permission: 'bash', pattern: 'which *', action: 'allow' },
|
|
163
|
+
{ permission: 'bash', pattern: 'type *', action: 'allow' },
|
|
164
|
+
{ permission: 'bash', pattern: 'whoami *', action: 'allow' },
|
|
165
|
+
{ permission: 'bash', pattern: 'id *', action: 'allow' },
|
|
166
|
+
{ permission: 'bash', pattern: 'hostname *', action: 'allow' },
|
|
167
|
+
{ permission: 'bash', pattern: 'uname *', action: 'allow' },
|
|
168
|
+
{ permission: 'bash', pattern: 'printf *', action: 'allow' },
|
|
169
|
+
{ permission: 'bash', pattern: 'seq *', action: 'allow' },
|
|
170
|
+
{ permission: 'bash', pattern: 'nl *', action: 'allow' },
|
|
171
|
+
{ permission: 'bash', pattern: 'column *', action: 'allow' },
|
|
172
|
+
{ permission: 'bash', pattern: 'tac *', action: 'allow' },
|
|
173
|
+
{ permission: 'bash', pattern: 'rev *', action: 'allow' },
|
|
174
|
+
{ permission: 'bash', pattern: 'fold *', action: 'allow' },
|
|
175
|
+
{ permission: 'bash', pattern: 'comm *', action: 'allow' },
|
|
176
|
+
{ permission: 'bash', pattern: 'cksum *', action: 'allow' },
|
|
177
|
+
{ permission: 'bash', pattern: 'shasum *', action: 'allow' },
|
|
178
|
+
{ permission: 'bash', pattern: 'md5sum *', action: 'allow' },
|
|
179
|
+
{ permission: 'bash', pattern: 'sha256sum *', action: 'allow' },
|
|
180
|
+
{ permission: 'bash', pattern: 'xxd *', action: 'allow' },
|
|
181
|
+
{ permission: 'bash', pattern: 'od *', action: 'allow' },
|
|
182
|
+
{ permission: 'bash', pattern: 'hexdump *', action: 'allow' },
|
|
183
|
+
{ permission: 'bash', pattern: 'strings *', action: 'allow' },
|
|
148
184
|
{ permission: 'bash', pattern: 'git status *', action: 'allow' },
|
|
149
185
|
{ permission: 'bash', pattern: 'git log *', action: 'allow' },
|
|
150
186
|
{ permission: 'bash', pattern: 'git diff *', action: 'allow' },
|