@wrongstack/tools 0.297.0 → 0.298.1
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/dist/_spawn-stream.d.ts +2 -0
- package/dist/_util.d.ts +29 -1
- package/dist/audit.js +113 -65
- package/dist/bash.js +5 -4
- package/dist/batch-tool-use.js +70 -7
- package/dist/browser/index.js +31 -1
- package/dist/builtin.js +908 -390
- package/dist/codebase-index/index.js +68 -16
- package/dist/codebase-index/project-server-protocol.d.ts +26 -0
- package/dist/codebase-index/project-server.js +60 -16
- package/dist/codebase-index/worker.js +36 -13
- package/dist/codebase-index/writer-search-helpers.d.ts +16 -0
- package/dist/codebase-index/writer.d.ts +13 -1
- package/dist/design.d.ts +1 -1
- package/dist/design.js +4 -3
- package/dist/diff.js +11 -1
- package/dist/e2e.js +9 -6
- package/dist/edit.js +9 -6
- package/dist/exec-allowlist.d.ts +15 -0
- package/dist/exec.js +627 -568
- package/dist/format.js +116 -65
- package/dist/git.js +22 -2
- package/dist/glob.js +11 -5
- package/dist/grep.js +160 -104
- package/dist/index.js +859 -329
- package/dist/install.js +120 -68
- package/dist/json.js +64 -18
- package/dist/kanban-board-inputs.d.ts +21 -4
- package/dist/kanban.js +20 -5
- package/dist/languages/index.js +128 -71
- package/dist/lint.js +113 -65
- package/dist/outdated.js +86 -26
- package/dist/pack.js +908 -390
- package/dist/patch.js +11 -1
- package/dist/plan.js +32 -10
- package/dist/read.js +77 -23
- package/dist/replace.js +11 -3
- package/dist/scaffold.js +9 -6
- package/dist/session-kanban.js +21 -5
- package/dist/task.js +3 -1
- package/dist/test.js +113 -65
- package/dist/todo.js +3 -1
- package/dist/tool-tier.js +908 -390
- package/dist/tool-use.d.ts +1 -1
- package/dist/tool-use.js +28 -4
- package/dist/tree.d.ts +1 -0
- package/dist/tree.js +44 -8
- package/dist/typecheck.js +113 -65
- package/dist/write.js +9 -6
- package/package.json +7 -6
- package/dist/_concurrency.d.ts.map +0 -1
- package/dist/_danger-detect.d.ts.map +0 -1
- package/dist/_edit-match.d.ts.map +0 -1
- package/dist/_env.d.ts.map +0 -1
- package/dist/_fetch-guard.d.ts.map +0 -1
- package/dist/_output-spool.d.ts.map +0 -1
- package/dist/_redact-command.d.ts.map +0 -1
- package/dist/_regex.d.ts.map +0 -1
- package/dist/_session-shell.d.ts.map +0 -1
- package/dist/_shell-pick.d.ts.map +0 -1
- package/dist/_spawn-stream.d.ts.map +0 -1
- package/dist/_syntax-check.d.ts.map +0 -1
- package/dist/_util.d.ts.map +0 -1
- package/dist/_win32-resolve.d.ts.map +0 -1
- package/dist/audit.d.ts.map +0 -1
- package/dist/audit.js.map +0 -7
- package/dist/auto-proceed-loop-guard.d.ts.map +0 -1
- package/dist/auto-proceed-loop-guard.js.map +0 -7
- package/dist/bash-kill-guard.d.ts.map +0 -1
- package/dist/bash.d.ts.map +0 -1
- package/dist/bash.js.map +0 -7
- package/dist/batch-tool-use.d.ts.map +0 -1
- package/dist/batch-tool-use.js.map +0 -7
- package/dist/browser/artifacts.d.ts.map +0 -1
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/index.js.map +0 -7
- package/dist/browser/manager.d.ts.map +0 -1
- package/dist/browser/network-guard-proxy.d.ts.map +0 -1
- package/dist/browser/security.d.ts.map +0 -1
- package/dist/browser/tools.d.ts.map +0 -1
- package/dist/browser/types.d.ts.map +0 -1
- package/dist/builtin.d.ts.map +0 -1
- package/dist/builtin.js.map +0 -7
- package/dist/circuit-breaker.d.ts.map +0 -1
- package/dist/circuit-breaker.js.map +0 -7
- package/dist/codebase-index/background-indexer.d.ts.map +0 -1
- package/dist/codebase-index/bm25.d.ts.map +0 -1
- package/dist/codebase-index/circuit-breaker.d.ts.map +0 -1
- package/dist/codebase-index/codebase-index-tool.d.ts.map +0 -1
- package/dist/codebase-index/codebase-search-tool.d.ts.map +0 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts.map +0 -1
- package/dist/codebase-index/dead-code-scan.d.ts.map +0 -1
- package/dist/codebase-index/generic-parser.d.ts.map +0 -1
- package/dist/codebase-index/gitignore.d.ts.map +0 -1
- package/dist/codebase-index/go-parser.d.ts.map +0 -1
- package/dist/codebase-index/index-service.d.ts.map +0 -1
- package/dist/codebase-index/index.d.ts.map +0 -1
- package/dist/codebase-index/index.js.map +0 -7
- package/dist/codebase-index/indexer.d.ts.map +0 -1
- package/dist/codebase-index/json-parser.d.ts.map +0 -1
- package/dist/codebase-index/languages.d.ts.map +0 -1
- package/dist/codebase-index/lsp-kind.d.ts.map +0 -1
- package/dist/codebase-index/parser-dispatch.d.ts.map +0 -1
- package/dist/codebase-index/project-server-cache.d.ts.map +0 -1
- package/dist/codebase-index/project-server-client.d.ts.map +0 -1
- package/dist/codebase-index/project-server-endpoint.d.ts.map +0 -1
- package/dist/codebase-index/project-server-protocol.d.ts.map +0 -1
- package/dist/codebase-index/project-server.d.ts.map +0 -1
- package/dist/codebase-index/project-server.js.map +0 -7
- package/dist/codebase-index/py-parser.d.ts.map +0 -1
- package/dist/codebase-index/refs-extractor.d.ts.map +0 -1
- package/dist/codebase-index/rs-parser.d.ts.map +0 -1
- package/dist/codebase-index/schema.d.ts.map +0 -1
- package/dist/codebase-index/spawn-gate.d.ts.map +0 -1
- package/dist/codebase-index/sqlite-runtime.d.ts.map +0 -1
- package/dist/codebase-index/ts-parser.d.ts.map +0 -1
- package/dist/codebase-index/worker-protocol.d.ts.map +0 -1
- package/dist/codebase-index/worker.d.ts.map +0 -1
- package/dist/codebase-index/worker.js.map +0 -7
- package/dist/codebase-index/writer-admin.d.ts.map +0 -1
- package/dist/codebase-index/writer-bulk-insert.d.ts.map +0 -1
- package/dist/codebase-index/writer-graph-helpers.d.ts.map +0 -1
- package/dist/codebase-index/writer-graph-reader.d.ts.map +0 -1
- package/dist/codebase-index/writer-helpers.d.ts.map +0 -1
- package/dist/codebase-index/writer-pragmas.d.ts.map +0 -1
- package/dist/codebase-index/writer-ref-mapper.d.ts.map +0 -1
- package/dist/codebase-index/writer-schema.d.ts.map +0 -1
- package/dist/codebase-index/writer-search-helpers.d.ts.map +0 -1
- package/dist/codebase-index/writer-store-pool.d.ts.map +0 -1
- package/dist/codebase-index/writer.d.ts.map +0 -1
- package/dist/codebase-index/yaml-parser.d.ts.map +0 -1
- package/dist/design.d.ts.map +0 -1
- package/dist/design.js.map +0 -7
- package/dist/diff.d.ts.map +0 -1
- package/dist/diff.js.map +0 -7
- package/dist/document.d.ts.map +0 -1
- package/dist/document.js.map +0 -7
- package/dist/e2e.d.ts.map +0 -1
- package/dist/e2e.js.map +0 -7
- package/dist/edit.d.ts.map +0 -1
- package/dist/edit.js.map +0 -7
- package/dist/exec-kill-guard.d.ts.map +0 -1
- package/dist/exec.d.ts.map +0 -1
- package/dist/exec.js.map +0 -7
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js.map +0 -7
- package/dist/format.d.ts.map +0 -1
- package/dist/format.js.map +0 -7
- package/dist/git.d.ts.map +0 -1
- package/dist/git.js.map +0 -7
- package/dist/glob.d.ts.map +0 -1
- package/dist/glob.js.map +0 -7
- package/dist/grep.d.ts.map +0 -1
- package/dist/grep.js.map +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -7
- package/dist/install.d.ts.map +0 -1
- package/dist/install.js.map +0 -7
- package/dist/json.d.ts.map +0 -1
- package/dist/json.js.map +0 -7
- package/dist/kanban-board-inputs.d.ts.map +0 -1
- package/dist/kanban-decomposition-actions.d.ts.map +0 -1
- package/dist/kanban-detail-actions.d.ts.map +0 -1
- package/dist/kanban-evidence-bridge.d.ts.map +0 -1
- package/dist/kanban-presence.d.ts.map +0 -1
- package/dist/kanban-split-task-handler.d.ts.map +0 -1
- package/dist/kanban-task-inputs.d.ts.map +0 -1
- package/dist/kanban-tool-results.d.ts.map +0 -1
- package/dist/kanban-tool-schema.d.ts.map +0 -1
- package/dist/kanban-tool-types.d.ts.map +0 -1
- package/dist/kanban.d.ts.map +0 -1
- package/dist/kanban.js.map +0 -7
- package/dist/languages/detect.d.ts.map +0 -1
- package/dist/languages/diagnostics.d.ts.map +0 -1
- package/dist/languages/execute-tool.d.ts.map +0 -1
- package/dist/languages/execute.d.ts.map +0 -1
- package/dist/languages/index.d.ts.map +0 -1
- package/dist/languages/index.js.map +0 -7
- package/dist/languages/legacy-bridge.d.ts.map +0 -1
- package/dist/languages/package-tool.d.ts.map +0 -1
- package/dist/languages/plan.d.ts.map +0 -1
- package/dist/languages/profile-helpers.d.ts.map +0 -1
- package/dist/languages/profiles/additional.d.ts.map +0 -1
- package/dist/languages/profiles/primary.d.ts.map +0 -1
- package/dist/languages/registry.d.ts.map +0 -1
- package/dist/languages/tool.d.ts.map +0 -1
- package/dist/languages/types.d.ts.map +0 -1
- package/dist/lint.d.ts.map +0 -1
- package/dist/lint.js.map +0 -7
- package/dist/logs.d.ts.map +0 -1
- package/dist/logs.js.map +0 -7
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js.map +0 -7
- package/dist/mode.d.ts.map +0 -1
- package/dist/mode.js.map +0 -7
- package/dist/next-steps.d.ts.map +0 -1
- package/dist/next-steps.js.map +0 -7
- package/dist/outdated.d.ts.map +0 -1
- package/dist/outdated.js.map +0 -7
- package/dist/pack.d.ts.map +0 -1
- package/dist/pack.js.map +0 -7
- package/dist/patch.d.ts.map +0 -1
- package/dist/patch.js.map +0 -7
- package/dist/plan.d.ts.map +0 -1
- package/dist/plan.js.map +0 -7
- package/dist/process-guardian.d.ts.map +0 -1
- package/dist/process-registry-persistent.d.ts.map +0 -1
- package/dist/process-registry.d.ts.map +0 -1
- package/dist/process-registry.js.map +0 -7
- package/dist/ps-slash.d.ts.map +0 -1
- package/dist/ps-slash.js.map +0 -7
- package/dist/read.d.ts.map +0 -1
- package/dist/read.js.map +0 -7
- package/dist/replace.d.ts.map +0 -1
- package/dist/replace.js.map +0 -7
- package/dist/scaffold.d.ts.map +0 -1
- package/dist/scaffold.js.map +0 -7
- package/dist/search.d.ts.map +0 -1
- package/dist/search.js.map +0 -7
- package/dist/session-kanban.d.ts.map +0 -1
- package/dist/session-kanban.js.map +0 -7
- package/dist/set-working-dir.d.ts.map +0 -1
- package/dist/skill.d.ts.map +0 -1
- package/dist/skill.js.map +0 -7
- package/dist/spawn-background.d.ts.map +0 -1
- package/dist/task.d.ts.map +0 -1
- package/dist/task.js.map +0 -7
- package/dist/test.d.ts.map +0 -1
- package/dist/test.js.map +0 -7
- package/dist/todo.d.ts.map +0 -1
- package/dist/todo.js.map +0 -7
- package/dist/tool-diff.d.ts.map +0 -1
- package/dist/tool-diff.js.map +0 -7
- package/dist/tool-help.d.ts.map +0 -1
- package/dist/tool-help.js.map +0 -7
- package/dist/tool-icon-map.d.ts.map +0 -1
- package/dist/tool-icons.d.ts.map +0 -1
- package/dist/tool-icons.js.map +0 -7
- package/dist/tool-search.d.ts.map +0 -1
- package/dist/tool-search.js.map +0 -7
- package/dist/tool-summary.d.ts.map +0 -1
- package/dist/tool-summary.js.map +0 -7
- package/dist/tool-tier.d.ts.map +0 -1
- package/dist/tool-tier.js.map +0 -7
- package/dist/tool-use.d.ts.map +0 -1
- package/dist/tool-use.js.map +0 -7
- package/dist/tree.d.ts.map +0 -1
- package/dist/tree.js.map +0 -7
- package/dist/typecheck.d.ts.map +0 -1
- package/dist/typecheck.js.map +0 -7
- package/dist/win32.d.ts.map +0 -1
- package/dist/win32.js.map +0 -7
- package/dist/write.d.ts.map +0 -1
- package/dist/write.js.map +0 -7
package/dist/tool-tier.js
CHANGED
|
@@ -81,11 +81,12 @@ function createOutputSpool(opts) {
|
|
|
81
81
|
return {
|
|
82
82
|
write(text) {
|
|
83
83
|
if (finalized || !text) return;
|
|
84
|
-
|
|
84
|
+
const textBytes = Buffer.byteLength(text, "utf8");
|
|
85
|
+
totalBytes += textBytes;
|
|
85
86
|
if (!stream && !failed) {
|
|
86
|
-
if (headBytes +
|
|
87
|
+
if (headBytes + textBytes <= threshold) {
|
|
87
88
|
head += text;
|
|
88
|
-
headBytes +=
|
|
89
|
+
headBytes += textBytes;
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
92
|
head += text;
|
|
@@ -95,7 +96,7 @@ function createOutputSpool(opts) {
|
|
|
95
96
|
}
|
|
96
97
|
if (stream) {
|
|
97
98
|
if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
|
|
98
|
-
droppedBytes +=
|
|
99
|
+
droppedBytes += textBytes;
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
102
|
stream.write(text);
|
|
@@ -126,6 +127,73 @@ var init_output_spool = __esm({
|
|
|
126
127
|
}
|
|
127
128
|
});
|
|
128
129
|
|
|
130
|
+
// src/_win32-resolve.ts
|
|
131
|
+
import * as fs from "node:fs";
|
|
132
|
+
import * as path2 from "node:path";
|
|
133
|
+
function resolveWin32Command(cmd) {
|
|
134
|
+
if (process.platform !== "win32") return cmd;
|
|
135
|
+
if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
|
|
136
|
+
return cmd;
|
|
137
|
+
}
|
|
138
|
+
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
139
|
+
const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
|
|
140
|
+
for (const dir of pathDirs) {
|
|
141
|
+
const base = path2.join(dir, cmd);
|
|
142
|
+
for (const ext of pathext) {
|
|
143
|
+
const full = `${base}${ext}`;
|
|
144
|
+
try {
|
|
145
|
+
fs.accessSync(full, fs.constants.X_OK);
|
|
146
|
+
return full;
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return cmd;
|
|
152
|
+
}
|
|
153
|
+
function resolvePowerShell(cmd) {
|
|
154
|
+
if (process.platform !== "win32") return cmd;
|
|
155
|
+
const lower = cmd.toLowerCase();
|
|
156
|
+
if (lower !== "pwsh" && lower !== "powershell" && lower !== "pwsh.exe" && lower !== "powershell.exe") {
|
|
157
|
+
return resolveWin32Command(cmd);
|
|
158
|
+
}
|
|
159
|
+
const primary = lower.startsWith("pwsh") ? "pwsh.exe" : "powershell.exe";
|
|
160
|
+
const fallback = lower.startsWith("pwsh") ? "powershell.exe" : "pwsh.exe";
|
|
161
|
+
const resolved = resolveWin32Command(primary);
|
|
162
|
+
if (resolved !== primary) {
|
|
163
|
+
const fb = resolveWin32Command(fallback);
|
|
164
|
+
return fb === fallback ? cmd : fb;
|
|
165
|
+
}
|
|
166
|
+
return resolved;
|
|
167
|
+
}
|
|
168
|
+
function assertSafeWin32ShellArgs(args) {
|
|
169
|
+
for (const arg of args) {
|
|
170
|
+
if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function buildWin32CmdShimInvocation(command, args = []) {
|
|
178
|
+
assertSafeWin32ShellArgs([command, ...args]);
|
|
179
|
+
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
180
|
+
return {
|
|
181
|
+
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
182
|
+
args: ["/d", "/c", line],
|
|
183
|
+
windowsVerbatimArguments: true
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function quoteWin32CmdArg(arg) {
|
|
187
|
+
return `"${arg}"`;
|
|
188
|
+
}
|
|
189
|
+
var WIN32_SHELL_META;
|
|
190
|
+
var init_win32_resolve = __esm({
|
|
191
|
+
"src/_win32-resolve.ts"() {
|
|
192
|
+
"use strict";
|
|
193
|
+
WIN32_SHELL_META = /[&|<>"\r\n\0]/;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
129
197
|
// src/circuit-breaker.ts
|
|
130
198
|
var DEFAULT_MAX_CONSECUTIVE_FAILURES, DEFAULT_SLOW_CALL_THRESHOLD_MS, DEFAULT_MAX_SLOW_CALLS, DEFAULT_WINDOW_MS, DEFAULT_MAX_CALLS_PER_WINDOW, DEFAULT_COOLDOWN_MS, CircuitBreaker;
|
|
131
199
|
var init_circuit_breaker = __esm({
|
|
@@ -767,73 +835,6 @@ var init_process_registry = __esm({
|
|
|
767
835
|
}
|
|
768
836
|
});
|
|
769
837
|
|
|
770
|
-
// src/_win32-resolve.ts
|
|
771
|
-
import * as fs from "node:fs";
|
|
772
|
-
import * as path2 from "node:path";
|
|
773
|
-
function resolveWin32Command(cmd) {
|
|
774
|
-
if (process.platform !== "win32") return cmd;
|
|
775
|
-
if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
|
|
776
|
-
return cmd;
|
|
777
|
-
}
|
|
778
|
-
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
779
|
-
const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
|
|
780
|
-
for (const dir of pathDirs) {
|
|
781
|
-
const base = path2.join(dir, cmd);
|
|
782
|
-
for (const ext of pathext) {
|
|
783
|
-
const full = `${base}${ext}`;
|
|
784
|
-
try {
|
|
785
|
-
fs.accessSync(full, fs.constants.X_OK);
|
|
786
|
-
return full;
|
|
787
|
-
} catch {
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
return cmd;
|
|
792
|
-
}
|
|
793
|
-
function resolvePowerShell(cmd) {
|
|
794
|
-
if (process.platform !== "win32") return cmd;
|
|
795
|
-
const lower = cmd.toLowerCase();
|
|
796
|
-
if (lower !== "pwsh" && lower !== "powershell" && lower !== "pwsh.exe" && lower !== "powershell.exe") {
|
|
797
|
-
return resolveWin32Command(cmd);
|
|
798
|
-
}
|
|
799
|
-
const primary = lower.startsWith("pwsh") ? "pwsh.exe" : "powershell.exe";
|
|
800
|
-
const fallback = lower.startsWith("pwsh") ? "powershell.exe" : "pwsh.exe";
|
|
801
|
-
const resolved = resolveWin32Command(primary);
|
|
802
|
-
if (resolved !== primary) {
|
|
803
|
-
const fb = resolveWin32Command(fallback);
|
|
804
|
-
return fb === fallback ? cmd : fb;
|
|
805
|
-
}
|
|
806
|
-
return resolved;
|
|
807
|
-
}
|
|
808
|
-
function assertSafeWin32ShellArgs(args) {
|
|
809
|
-
for (const arg of args) {
|
|
810
|
-
if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
|
|
811
|
-
throw new Error(
|
|
812
|
-
'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
function buildWin32CmdShimInvocation(command, args = []) {
|
|
818
|
-
assertSafeWin32ShellArgs([command, ...args]);
|
|
819
|
-
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
820
|
-
return {
|
|
821
|
-
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
822
|
-
args: ["/d", "/c", line],
|
|
823
|
-
windowsVerbatimArguments: true
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
function quoteWin32CmdArg(arg) {
|
|
827
|
-
return `"${arg}"`;
|
|
828
|
-
}
|
|
829
|
-
var WIN32_SHELL_META;
|
|
830
|
-
var init_win32_resolve = __esm({
|
|
831
|
-
"src/_win32-resolve.ts"() {
|
|
832
|
-
"use strict";
|
|
833
|
-
WIN32_SHELL_META = /[&|<>"\r\n\0]/;
|
|
834
|
-
}
|
|
835
|
-
});
|
|
836
|
-
|
|
837
838
|
// src/_spawn-stream.ts
|
|
838
839
|
import { spawn as spawn2 } from "node:child_process";
|
|
839
840
|
import {
|
|
@@ -846,8 +847,11 @@ async function* spawnStream(opts) {
|
|
|
846
847
|
const max = opts.maxBytes ?? 2e5;
|
|
847
848
|
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
848
849
|
const maxQueue = opts.maxQueueSize ?? 500;
|
|
850
|
+
const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
|
|
849
851
|
let stdout = "";
|
|
850
852
|
let stderr = "";
|
|
853
|
+
let stdoutRetainedBytes = 0;
|
|
854
|
+
let stderrRetainedBytes = 0;
|
|
851
855
|
let pending2 = "";
|
|
852
856
|
let error;
|
|
853
857
|
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
@@ -889,6 +893,7 @@ async function* spawnStream(opts) {
|
|
|
889
893
|
});
|
|
890
894
|
}
|
|
891
895
|
const queue = [];
|
|
896
|
+
let queuedBytes = 0;
|
|
892
897
|
let waiter;
|
|
893
898
|
let paused = false;
|
|
894
899
|
const wake = () => {
|
|
@@ -899,7 +904,7 @@ async function* spawnStream(opts) {
|
|
|
899
904
|
}
|
|
900
905
|
};
|
|
901
906
|
const resume = () => {
|
|
902
|
-
if (paused && queue.length
|
|
907
|
+
if (paused && queue.length <= Math.floor(maxQueue / 2) && queuedBytes <= Math.floor(maxQueueBytes / 2)) {
|
|
903
908
|
paused = false;
|
|
904
909
|
child.stdout?.resume();
|
|
905
910
|
child.stderr?.resume();
|
|
@@ -909,11 +914,16 @@ async function* spawnStream(opts) {
|
|
|
909
914
|
const s = c.toString();
|
|
910
915
|
stdoutBytes += c.byteLength;
|
|
911
916
|
emitProcessOutput({ pid, stream: "stdout", chunk: c });
|
|
912
|
-
if (
|
|
917
|
+
if (stdoutRetainedBytes < max) {
|
|
918
|
+
const retained = utf8Prefix(s, max - stdoutRetainedBytes);
|
|
919
|
+
stdout += retained;
|
|
920
|
+
stdoutRetainedBytes += Buffer.byteLength(retained, "utf8");
|
|
921
|
+
}
|
|
913
922
|
spool.write(s);
|
|
914
|
-
queue.push({ kind: "out", data: s });
|
|
923
|
+
queue.push({ kind: "out", data: s, bytes: c.byteLength });
|
|
924
|
+
queuedBytes += c.byteLength;
|
|
915
925
|
wake();
|
|
916
|
-
if (!paused && queue.length >= maxQueue) {
|
|
926
|
+
if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
|
|
917
927
|
paused = true;
|
|
918
928
|
child.stdout?.pause();
|
|
919
929
|
child.stderr?.pause();
|
|
@@ -923,11 +933,16 @@ async function* spawnStream(opts) {
|
|
|
923
933
|
const s = c.toString();
|
|
924
934
|
stderrBytes += c.byteLength;
|
|
925
935
|
emitProcessOutput({ pid, stream: "stderr", chunk: c });
|
|
926
|
-
if (
|
|
936
|
+
if (stderrRetainedBytes < max) {
|
|
937
|
+
const retained = utf8Prefix(s, max - stderrRetainedBytes);
|
|
938
|
+
stderr += retained;
|
|
939
|
+
stderrRetainedBytes += Buffer.byteLength(retained, "utf8");
|
|
940
|
+
}
|
|
927
941
|
spool.write(s);
|
|
928
|
-
queue.push({ kind: "err", data: s });
|
|
942
|
+
queue.push({ kind: "err", data: s, bytes: c.byteLength });
|
|
943
|
+
queuedBytes += c.byteLength;
|
|
929
944
|
wake();
|
|
930
|
-
if (!paused && queue.length >= maxQueue) {
|
|
945
|
+
if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
|
|
931
946
|
paused = true;
|
|
932
947
|
child.stdout?.pause();
|
|
933
948
|
child.stderr?.pause();
|
|
@@ -937,7 +952,9 @@ async function* spawnStream(opts) {
|
|
|
937
952
|
child.stderr?.on("data", onErr);
|
|
938
953
|
child.on("error", (e) => {
|
|
939
954
|
error = e.message;
|
|
940
|
-
|
|
955
|
+
const bytes = Buffer.byteLength(e.message, "utf8");
|
|
956
|
+
queue.push({ kind: "error", data: e.message, bytes });
|
|
957
|
+
queuedBytes += bytes;
|
|
941
958
|
wake();
|
|
942
959
|
});
|
|
943
960
|
const completeTelemetry = (code, signal, timedOut = false) => {
|
|
@@ -958,7 +975,13 @@ async function* spawnStream(opts) {
|
|
|
958
975
|
if (typeof pid === "number") registry.unregister(pid);
|
|
959
976
|
const exitCode2 = code ?? (signal ? 1 : 0);
|
|
960
977
|
completeTelemetry(exitCode2, signal ?? void 0);
|
|
961
|
-
queue.push({
|
|
978
|
+
queue.push({
|
|
979
|
+
kind: "close",
|
|
980
|
+
data: "",
|
|
981
|
+
bytes: 0,
|
|
982
|
+
code: exitCode2,
|
|
983
|
+
...signal ? { signal } : {}
|
|
984
|
+
});
|
|
962
985
|
wake();
|
|
963
986
|
});
|
|
964
987
|
const onAbort = () => {
|
|
@@ -970,7 +993,7 @@ async function* spawnStream(opts) {
|
|
|
970
993
|
} catch {
|
|
971
994
|
}
|
|
972
995
|
}
|
|
973
|
-
queue.push({ kind: "close", data: "", code: 124 });
|
|
996
|
+
queue.push({ kind: "close", data: "", bytes: 0, code: 124 });
|
|
974
997
|
completeTelemetry(124, "SIGKILL", true);
|
|
975
998
|
wake();
|
|
976
999
|
};
|
|
@@ -988,6 +1011,7 @@ async function* spawnStream(opts) {
|
|
|
988
1011
|
});
|
|
989
1012
|
}
|
|
990
1013
|
const chunk = queue.shift();
|
|
1014
|
+
queuedBytes -= chunk.bytes;
|
|
991
1015
|
resume();
|
|
992
1016
|
if (chunk.kind === "close") {
|
|
993
1017
|
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
@@ -1014,7 +1038,7 @@ async function* spawnStream(opts) {
|
|
|
1014
1038
|
stdout: spooled ? stdout + spoolNote(spooled) : stdout,
|
|
1015
1039
|
stderr,
|
|
1016
1040
|
exitCode,
|
|
1017
|
-
truncated:
|
|
1041
|
+
truncated: stdoutBytes > stdoutRetainedBytes || stderrBytes > stderrRetainedBytes,
|
|
1018
1042
|
error,
|
|
1019
1043
|
spoolPath: spooled?.path,
|
|
1020
1044
|
spoolBytes: spooled?.bytes
|
|
@@ -1038,13 +1062,27 @@ async function* spawnStream(opts) {
|
|
|
1038
1062
|
}
|
|
1039
1063
|
}
|
|
1040
1064
|
}
|
|
1065
|
+
function utf8Prefix(text, maxBytes) {
|
|
1066
|
+
if (maxBytes <= 0) return "";
|
|
1067
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
1068
|
+
let low = 0;
|
|
1069
|
+
let high = text.length;
|
|
1070
|
+
while (low < high) {
|
|
1071
|
+
const mid = Math.ceil((low + high) / 2);
|
|
1072
|
+
if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) low = mid;
|
|
1073
|
+
else high = mid - 1;
|
|
1074
|
+
}
|
|
1075
|
+
let end = low;
|
|
1076
|
+
if (end > 0 && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
|
|
1077
|
+
return text.slice(0, end);
|
|
1078
|
+
}
|
|
1041
1079
|
var isWin;
|
|
1042
1080
|
var init_spawn_stream = __esm({
|
|
1043
1081
|
"src/_spawn-stream.ts"() {
|
|
1044
1082
|
"use strict";
|
|
1045
1083
|
init_output_spool();
|
|
1046
|
-
init_process_registry();
|
|
1047
1084
|
init_win32_resolve();
|
|
1085
|
+
init_process_registry();
|
|
1048
1086
|
isWin = process.platform === "win32";
|
|
1049
1087
|
}
|
|
1050
1088
|
});
|
|
@@ -1058,14 +1096,14 @@ function sha256hex(content) {
|
|
|
1058
1096
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1059
1097
|
}
|
|
1060
1098
|
async function detectPackageManager(cwd) {
|
|
1061
|
-
const { stat:
|
|
1099
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
1062
1100
|
try {
|
|
1063
|
-
await
|
|
1101
|
+
await stat18(`${cwd}/pnpm-lock.yaml`);
|
|
1064
1102
|
return "pnpm";
|
|
1065
1103
|
} catch {
|
|
1066
1104
|
}
|
|
1067
1105
|
try {
|
|
1068
|
-
await
|
|
1106
|
+
await stat18(`${cwd}/yarn.lock`);
|
|
1069
1107
|
return "yarn";
|
|
1070
1108
|
} catch {
|
|
1071
1109
|
}
|
|
@@ -1093,11 +1131,15 @@ function safeResolve(input, ctx) {
|
|
|
1093
1131
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
1094
1132
|
}
|
|
1095
1133
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
1096
|
-
|
|
1134
|
+
await resolveRealInsideRoot(absPath, ctx);
|
|
1135
|
+
}
|
|
1136
|
+
async function resolveRealInsideRoot(absPath, ctx) {
|
|
1137
|
+
if (ctx.allowOutsideProjectRoot) return absPath;
|
|
1097
1138
|
const realRoots = await Promise.all(
|
|
1098
1139
|
allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
|
|
1099
1140
|
);
|
|
1100
1141
|
let probe2 = absPath;
|
|
1142
|
+
const pendingTail = [];
|
|
1101
1143
|
for (; ; ) {
|
|
1102
1144
|
let real;
|
|
1103
1145
|
try {
|
|
@@ -1105,13 +1147,16 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
1105
1147
|
} catch (err) {
|
|
1106
1148
|
if (err.code === "ENOENT") {
|
|
1107
1149
|
const parent = path3.dirname(probe2);
|
|
1108
|
-
if (parent === probe2) return;
|
|
1150
|
+
if (parent === probe2) return absPath;
|
|
1151
|
+
pendingTail.unshift(path3.basename(probe2));
|
|
1109
1152
|
probe2 = parent;
|
|
1110
1153
|
continue;
|
|
1111
1154
|
}
|
|
1112
1155
|
throw err;
|
|
1113
1156
|
}
|
|
1114
|
-
if (isInsideAny(real, realRoots))
|
|
1157
|
+
if (isInsideAny(real, realRoots)) {
|
|
1158
|
+
return pendingTail.length > 0 ? path3.join(real, ...pendingTail) : real;
|
|
1159
|
+
}
|
|
1115
1160
|
throw new Error(
|
|
1116
1161
|
`Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
|
|
1117
1162
|
);
|
|
@@ -1119,8 +1164,7 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
1119
1164
|
}
|
|
1120
1165
|
async function safeResolveReal(input, ctx) {
|
|
1121
1166
|
const abs = safeResolve(input, ctx);
|
|
1122
|
-
await
|
|
1123
|
-
return abs;
|
|
1167
|
+
return await resolveRealInsideRoot(abs, ctx);
|
|
1124
1168
|
}
|
|
1125
1169
|
function truncateMiddle(s, max) {
|
|
1126
1170
|
if (Buffer.byteLength(s, "utf8") <= max) return s;
|
|
@@ -2684,8 +2728,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
|
|
|
2684
2728
|
collectFileEvidence(directory, fullPath, entry.name, profiles, state);
|
|
2685
2729
|
}
|
|
2686
2730
|
}
|
|
2687
|
-
function collectFileEvidence(directory, fullPath,
|
|
2688
|
-
const lower =
|
|
2731
|
+
function collectFileEvidence(directory, fullPath, basename13, profiles, state) {
|
|
2732
|
+
const lower = basename13.toLowerCase();
|
|
2689
2733
|
const extension = path4.extname(lower);
|
|
2690
2734
|
for (const profile of profiles) {
|
|
2691
2735
|
const detector = profile.detectors.find(
|
|
@@ -2696,7 +2740,7 @@ function collectFileEvidence(directory, fullPath, basename12, profiles, state) {
|
|
|
2696
2740
|
candidate.evidence.push({
|
|
2697
2741
|
kind: detector.kind,
|
|
2698
2742
|
path: fullPath,
|
|
2699
|
-
value:
|
|
2743
|
+
value: basename13,
|
|
2700
2744
|
weight: detector.weight
|
|
2701
2745
|
});
|
|
2702
2746
|
if (detector.kind === "manifest" || detector.kind === "config") {
|
|
@@ -2824,8 +2868,8 @@ function normalizeLimits(input) {
|
|
|
2824
2868
|
async function canonicalDirectory(input) {
|
|
2825
2869
|
const resolved = path4.resolve(input);
|
|
2826
2870
|
const real = await fs2.realpath(resolved);
|
|
2827
|
-
const
|
|
2828
|
-
if (!
|
|
2871
|
+
const stat18 = await fs2.stat(real);
|
|
2872
|
+
if (!stat18.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
|
|
2829
2873
|
return real;
|
|
2830
2874
|
}
|
|
2831
2875
|
async function canonicalInside(input, root, label) {
|
|
@@ -3381,6 +3425,7 @@ import * as fs3 from "node:fs/promises";
|
|
|
3381
3425
|
import * as path6 from "node:path";
|
|
3382
3426
|
async function planLanguageOperation(options) {
|
|
3383
3427
|
validateOperationOptions(options.operation, options.operationOptions?.packages);
|
|
3428
|
+
validateFilterOption(options.operationOptions?.filter);
|
|
3384
3429
|
const detection = await detectLanguageWorkspaces(options);
|
|
3385
3430
|
const candidates = detection.workspaces.filter(
|
|
3386
3431
|
(workspace) => workspace.capabilities.includes(options.operation)
|
|
@@ -3553,6 +3598,15 @@ function workspaceDepth(workspaceRoot, projectRoot) {
|
|
|
3553
3598
|
const relative12 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
|
|
3554
3599
|
return relative12 === "" ? 0 : relative12.split(path6.sep).length;
|
|
3555
3600
|
}
|
|
3601
|
+
function validateFilterOption(filter) {
|
|
3602
|
+
if (filter === void 0) return;
|
|
3603
|
+
if (filter.startsWith("-") || filter.includes(" --")) {
|
|
3604
|
+
throw new Error(`Invalid test filter (parsed as a runner option): "${filter}"`);
|
|
3605
|
+
}
|
|
3606
|
+
if (filter.length > MAX_ARGUMENT_LENGTH || /[\r\n\0]/.test(filter)) {
|
|
3607
|
+
throw new Error("Invalid test filter (oversized or contains a control character)");
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3556
3610
|
function validateOperationOptions(operation, packages) {
|
|
3557
3611
|
if (!operation.startsWith("package-") || !packages) return;
|
|
3558
3612
|
for (const value of packages) {
|
|
@@ -3594,7 +3648,7 @@ var init_plan = __esm({
|
|
|
3594
3648
|
MAX_ARGUMENT_LENGTH = 4096;
|
|
3595
3649
|
PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
|
|
3596
3650
|
COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
|
|
3597
|
-
GO_MODULE_RE = /^(?:[a-z0-9
|
|
3651
|
+
GO_MODULE_RE = /^(?:[a-z0-9-]+\.)+[a-z0-9-]+\/[a-z0-9._~+/@-]+$/i;
|
|
3598
3652
|
PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
|
|
3599
3653
|
}
|
|
3600
3654
|
});
|
|
@@ -3714,8 +3768,8 @@ async function executeInternal(options, startedAt) {
|
|
|
3714
3768
|
const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
|
|
3715
3769
|
if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
|
|
3716
3770
|
const safeTarget = await assertContainedFile(target, options.projectRoot);
|
|
3717
|
-
const
|
|
3718
|
-
if (
|
|
3771
|
+
const stat18 = await fs4.stat(safeTarget);
|
|
3772
|
+
if (stat18.size > MAX_INTERNAL_SOURCE_BYTES) {
|
|
3719
3773
|
return unavailableResult(
|
|
3720
3774
|
options,
|
|
3721
3775
|
`Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
|
|
@@ -4002,8 +4056,8 @@ async function snapshotPaths(paths) {
|
|
|
4002
4056
|
const existing = [];
|
|
4003
4057
|
for (const candidate of paths) {
|
|
4004
4058
|
try {
|
|
4005
|
-
const
|
|
4006
|
-
if (!
|
|
4059
|
+
const stat18 = await fs4.stat(candidate);
|
|
4060
|
+
if (!stat18.isFile()) continue;
|
|
4007
4061
|
existing.push(candidate);
|
|
4008
4062
|
} catch {
|
|
4009
4063
|
}
|
|
@@ -4031,8 +4085,8 @@ async function snapshotSizes(paths) {
|
|
|
4031
4085
|
const sizes = /* @__PURE__ */ new Map();
|
|
4032
4086
|
for (const candidate of paths) {
|
|
4033
4087
|
try {
|
|
4034
|
-
const
|
|
4035
|
-
if (
|
|
4088
|
+
const stat18 = await fs4.stat(candidate);
|
|
4089
|
+
if (stat18.isFile()) sizes.set(candidate, stat18.size);
|
|
4036
4090
|
} catch {
|
|
4037
4091
|
}
|
|
4038
4092
|
}
|
|
@@ -4192,6 +4246,9 @@ var init_execute_tool = __esm({
|
|
|
4192
4246
|
useInstead: ["language_info", "install", "audit", "outdated"]
|
|
4193
4247
|
},
|
|
4194
4248
|
permission: "confirm",
|
|
4249
|
+
// WS-046: gives permission decisions something to key on.
|
|
4250
|
+
// The action performed (build/test/lint); the cwd is where, not what.
|
|
4251
|
+
subjectKey: "action",
|
|
4195
4252
|
mutating: true,
|
|
4196
4253
|
riskTier: "standard",
|
|
4197
4254
|
capabilities: ["shell.restricted", "fs.write"],
|
|
@@ -4424,6 +4481,9 @@ var init_package_tool = __esm({
|
|
|
4424
4481
|
useInstead: ["language_info", "language", "install", "audit", "outdated"]
|
|
4425
4482
|
},
|
|
4426
4483
|
permission: "confirm",
|
|
4484
|
+
// WS-046: gives permission decisions something to key on.
|
|
4485
|
+
// The package operation performed — install/remove are not interchangeable.
|
|
4486
|
+
subjectKey: "operation",
|
|
4427
4487
|
mutating: true,
|
|
4428
4488
|
riskTier: "destructive",
|
|
4429
4489
|
capabilities: ["shell.restricted", "fs.write", "net.outbound", "package.install"],
|
|
@@ -6475,9 +6535,9 @@ function parseSymbols6(opts) {
|
|
|
6475
6535
|
function regexParse2(opts) {
|
|
6476
6536
|
const { file, content, lang } = opts;
|
|
6477
6537
|
const symbols = [];
|
|
6478
|
-
const
|
|
6479
|
-
const isPackageJson =
|
|
6480
|
-
const isTsconfig =
|
|
6538
|
+
const basename13 = path22.basename(file).toLowerCase();
|
|
6539
|
+
const isPackageJson = basename13 === "package.json";
|
|
6540
|
+
const isTsconfig = basename13 === "tsconfig.json" || basename13 === "tsconfig.build.json";
|
|
6481
6541
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
6482
6542
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
6483
6543
|
const lines = content.split("\n");
|
|
@@ -8373,30 +8433,59 @@ ${hint}` : ""),
|
|
|
8373
8433
|
import {
|
|
8374
8434
|
GOVERNED_TOOL_EXECUTOR_META_KEY
|
|
8375
8435
|
} from "@wrongstack/core/types";
|
|
8436
|
+
var BATCHABLE_TOOL_NAMES = [
|
|
8437
|
+
"read",
|
|
8438
|
+
"glob",
|
|
8439
|
+
"grep",
|
|
8440
|
+
"tree",
|
|
8441
|
+
"codebase-stats",
|
|
8442
|
+
"codebase-search",
|
|
8443
|
+
"language_info",
|
|
8444
|
+
"tool_search",
|
|
8445
|
+
"memory_for_file",
|
|
8446
|
+
"memory_for_path",
|
|
8447
|
+
"memory_search"
|
|
8448
|
+
];
|
|
8449
|
+
var BATCHABLE_TOOLS = new Set(BATCHABLE_TOOL_NAMES);
|
|
8450
|
+
var MAX_BATCH_CALLS = 8;
|
|
8451
|
+
var MAX_PARALLEL_BATCH_CALLS = 3;
|
|
8376
8452
|
var batchToolUseTool = {
|
|
8377
8453
|
name: "batch_tool_use",
|
|
8378
8454
|
category: "Meta",
|
|
8379
|
-
description: "Execute a batch of
|
|
8380
|
-
usageHint:
|
|
8455
|
+
description: "Execute a small batch of independent, read-only discovery calls. Process, mutation, browser-action, MCP, delegation, and nested meta tools are not eligible.",
|
|
8456
|
+
usageHint: `BOUNDED READ-ONLY BATCHING:
|
|
8457
|
+
|
|
8458
|
+
- Use only for a small set of independent reads or searches that are all needed now.
|
|
8459
|
+
- Do not wrap normal sequential reasoning or every tool call in a batch.
|
|
8460
|
+
- \`parallel: true\` (default) runs at most three eligible calls concurrently.
|
|
8461
|
+
- \`stop_on_error: true\` makes it fail fast on the first error.
|
|
8462
|
+
- Eligible tools: ${BATCHABLE_TOOL_NAMES.join(", ")}.`,
|
|
8381
8463
|
permission: "confirm",
|
|
8464
|
+
// Scheduling barrier: keep the outer meta-call out of ToolExecutor's
|
|
8465
|
+
// non-mutating fan-out even though every eligible inner tool is read-only.
|
|
8382
8466
|
mutating: true,
|
|
8383
8467
|
timeoutMs: 12e4,
|
|
8384
|
-
capabilities: ["tool.
|
|
8468
|
+
capabilities: ["tool.meta"],
|
|
8385
8469
|
icon: "meta",
|
|
8386
8470
|
inputSchema: {
|
|
8387
8471
|
type: "object",
|
|
8388
8472
|
properties: {
|
|
8389
8473
|
calls: {
|
|
8390
8474
|
type: "array",
|
|
8475
|
+
maxItems: MAX_BATCH_CALLS,
|
|
8391
8476
|
items: {
|
|
8392
8477
|
type: "object",
|
|
8393
8478
|
properties: {
|
|
8394
|
-
tool: {
|
|
8479
|
+
tool: {
|
|
8480
|
+
type: "string",
|
|
8481
|
+
enum: [...BATCHABLE_TOOL_NAMES],
|
|
8482
|
+
description: "An explicitly batch-safe, read-only discovery tool."
|
|
8483
|
+
},
|
|
8395
8484
|
input: { type: "object" }
|
|
8396
8485
|
},
|
|
8397
8486
|
required: ["tool"]
|
|
8398
8487
|
},
|
|
8399
|
-
description:
|
|
8488
|
+
description: `Up to ${MAX_BATCH_CALLS} independent, batch-safe read calls.`
|
|
8400
8489
|
},
|
|
8401
8490
|
stop_on_error: {
|
|
8402
8491
|
type: "boolean",
|
|
@@ -8419,6 +8508,9 @@ var batchToolUseTool = {
|
|
|
8419
8508
|
stop_on_error: false
|
|
8420
8509
|
};
|
|
8421
8510
|
}
|
|
8511
|
+
if (input.calls.length > MAX_BATCH_CALLS) {
|
|
8512
|
+
throw new Error(`batch_tool_use: calls must contain at most ${MAX_BATCH_CALLS} items`);
|
|
8513
|
+
}
|
|
8422
8514
|
const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
|
|
8423
8515
|
if (typeof governedExecute !== "function") {
|
|
8424
8516
|
return {
|
|
@@ -8438,8 +8530,11 @@ var batchToolUseTool = {
|
|
|
8438
8530
|
let succeeded = 0;
|
|
8439
8531
|
let failed = 0;
|
|
8440
8532
|
if (input.parallel !== false) {
|
|
8441
|
-
const
|
|
8442
|
-
|
|
8533
|
+
const allResults = await mapWithConcurrency(
|
|
8534
|
+
input.calls,
|
|
8535
|
+
MAX_PARALLEL_BATCH_CALLS,
|
|
8536
|
+
(call) => executeSingle(call, ctx, governedExecute)
|
|
8537
|
+
);
|
|
8443
8538
|
results.push(...allResults);
|
|
8444
8539
|
succeeded = allResults.filter((r) => r.success).length;
|
|
8445
8540
|
failed = allResults.filter((r) => !r.success).length;
|
|
@@ -8464,8 +8559,28 @@ var batchToolUseTool = {
|
|
|
8464
8559
|
};
|
|
8465
8560
|
}
|
|
8466
8561
|
};
|
|
8562
|
+
async function mapWithConcurrency(items, limit, run) {
|
|
8563
|
+
const results = new Array(items.length);
|
|
8564
|
+
let nextIndex = 0;
|
|
8565
|
+
const worker2 = async () => {
|
|
8566
|
+
while (nextIndex < items.length) {
|
|
8567
|
+
const index = nextIndex++;
|
|
8568
|
+
results[index] = await run(items[index]);
|
|
8569
|
+
}
|
|
8570
|
+
};
|
|
8571
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker2()));
|
|
8572
|
+
return results;
|
|
8573
|
+
}
|
|
8467
8574
|
async function executeSingle(call, ctx, governedExecute) {
|
|
8468
8575
|
const start = Date.now();
|
|
8576
|
+
if (call.tool === batchToolUseTool.name) {
|
|
8577
|
+
return {
|
|
8578
|
+
tool: call.tool,
|
|
8579
|
+
success: false,
|
|
8580
|
+
error: "recursive batch_tool_use execution is not allowed",
|
|
8581
|
+
executionMs: Date.now() - start
|
|
8582
|
+
};
|
|
8583
|
+
}
|
|
8469
8584
|
const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
|
|
8470
8585
|
if (!tool) {
|
|
8471
8586
|
return {
|
|
@@ -8475,6 +8590,14 @@ async function executeSingle(call, ctx, governedExecute) {
|
|
|
8475
8590
|
executionMs: Date.now() - start
|
|
8476
8591
|
};
|
|
8477
8592
|
}
|
|
8593
|
+
if (!BATCHABLE_TOOLS.has(tool.name) || tool.mutating) {
|
|
8594
|
+
return {
|
|
8595
|
+
tool: call.tool,
|
|
8596
|
+
success: false,
|
|
8597
|
+
error: `tool "${call.tool}" is not eligible for batch execution; call it directly. Allowed: ${BATCHABLE_TOOL_NAMES.join(", ")}`,
|
|
8598
|
+
executionMs: Date.now() - start
|
|
8599
|
+
};
|
|
8600
|
+
}
|
|
8478
8601
|
try {
|
|
8479
8602
|
const result = await governedExecute(call.tool, call.input);
|
|
8480
8603
|
return {
|
|
@@ -8511,8 +8634,14 @@ import * as path10 from "node:path";
|
|
|
8511
8634
|
import { atomicWrite, ulid } from "@wrongstack/core/utils";
|
|
8512
8635
|
var ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8513
8636
|
var sweptRoots = /* @__PURE__ */ new Set();
|
|
8637
|
+
var MAX_SWEPT_ROOTS = 128;
|
|
8514
8638
|
function sweepOldArtifacts(root) {
|
|
8515
8639
|
if (sweptRoots.has(root)) return;
|
|
8640
|
+
while (sweptRoots.size >= MAX_SWEPT_ROOTS) {
|
|
8641
|
+
const oldest = sweptRoots.values().next().value;
|
|
8642
|
+
if (oldest === void 0) break;
|
|
8643
|
+
sweptRoots.delete(oldest);
|
|
8644
|
+
}
|
|
8516
8645
|
sweptRoots.add(root);
|
|
8517
8646
|
void (async () => {
|
|
8518
8647
|
const cutoff = Date.now() - ARTIFACT_RETENTION_MS;
|
|
@@ -8535,8 +8664,8 @@ function sweepOldArtifacts(root) {
|
|
|
8535
8664
|
for (const name of names) {
|
|
8536
8665
|
const target = path10.join(dir, name);
|
|
8537
8666
|
try {
|
|
8538
|
-
const
|
|
8539
|
-
if (
|
|
8667
|
+
const stat18 = await fs7.stat(target);
|
|
8668
|
+
if (stat18.isFile() && stat18.mtimeMs < cutoff) {
|
|
8540
8669
|
await fs7.rm(target, { force: true });
|
|
8541
8670
|
removed++;
|
|
8542
8671
|
}
|
|
@@ -8564,14 +8693,14 @@ var BrowserArtifactStore = class {
|
|
|
8564
8693
|
}
|
|
8565
8694
|
async record(id, sessionId, kind, target, mimeType) {
|
|
8566
8695
|
await fs7.chmod(target, 384).catch(() => void 0);
|
|
8567
|
-
const [
|
|
8696
|
+
const [stat18, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
|
|
8568
8697
|
const artifact = {
|
|
8569
8698
|
id,
|
|
8570
8699
|
kind,
|
|
8571
8700
|
sensitivity: "sensitive",
|
|
8572
8701
|
path: target,
|
|
8573
8702
|
mimeType,
|
|
8574
|
-
sizeBytes:
|
|
8703
|
+
sizeBytes: stat18.size,
|
|
8575
8704
|
sha256,
|
|
8576
8705
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8577
8706
|
};
|
|
@@ -8941,7 +9070,7 @@ var BrowserSessionManager = class {
|
|
|
8941
9070
|
constructor(options, launcher = defaultLauncher) {
|
|
8942
9071
|
this.launcher = launcher;
|
|
8943
9072
|
this.artifacts = new BrowserArtifactStore(options.artifactRoot);
|
|
8944
|
-
this.allowPrivateHosts = options.allowPrivateHosts ??
|
|
9073
|
+
this.allowPrivateHosts = options.allowPrivateHosts ?? false;
|
|
8945
9074
|
this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
|
|
8946
9075
|
this.networkProxy = new BrowserNetworkGuardProxy({
|
|
8947
9076
|
allowPrivateHosts: this.allowPrivateHosts,
|
|
@@ -9164,8 +9293,8 @@ var BrowserSessionManager = class {
|
|
|
9164
9293
|
if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
|
|
9165
9294
|
throw new Error("browser: upload files must not escape the project root through a symlink");
|
|
9166
9295
|
}
|
|
9167
|
-
const
|
|
9168
|
-
if (!
|
|
9296
|
+
const stat18 = await fs8.stat(realFile);
|
|
9297
|
+
if (!stat18.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
|
|
9169
9298
|
resolved.push(realFile);
|
|
9170
9299
|
}
|
|
9171
9300
|
await this.runPageOperation(
|
|
@@ -9514,6 +9643,9 @@ var browserScreenshotTool = {
|
|
|
9514
9643
|
description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
|
|
9515
9644
|
usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
|
|
9516
9645
|
permission: "confirm",
|
|
9646
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9647
|
+
// rule must distinguish clicking one control from another.
|
|
9648
|
+
subjectKey: "selector",
|
|
9517
9649
|
mutating: true,
|
|
9518
9650
|
riskTier: "safe",
|
|
9519
9651
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9543,6 +9675,9 @@ function selectorTool(name, description, run) {
|
|
|
9543
9675
|
description,
|
|
9544
9676
|
usageHint: `${name}({ sessionId, selector })`,
|
|
9545
9677
|
permission: "confirm",
|
|
9678
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9679
|
+
// rule must distinguish clicking one control from another.
|
|
9680
|
+
subjectKey: "selector",
|
|
9546
9681
|
mutating: true,
|
|
9547
9682
|
riskTier: "standard",
|
|
9548
9683
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9572,6 +9707,9 @@ var browserTypeTool = {
|
|
|
9572
9707
|
description: "Fill an input in an owned browser session. Use secretEnv instead of text for credentials so values stay out of tool arguments and session audit.",
|
|
9573
9708
|
usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
|
|
9574
9709
|
permission: "confirm",
|
|
9710
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9711
|
+
// rule must distinguish clicking one control from another.
|
|
9712
|
+
subjectKey: "selector",
|
|
9575
9713
|
mutating: true,
|
|
9576
9714
|
riskTier: "standard",
|
|
9577
9715
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9606,6 +9744,9 @@ var browserSelectTool = {
|
|
|
9606
9744
|
description: "Select an option in an owned browser session.",
|
|
9607
9745
|
usageHint: "browser_select({ sessionId, selector, value })",
|
|
9608
9746
|
permission: "confirm",
|
|
9747
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9748
|
+
// rule must distinguish clicking one control from another.
|
|
9749
|
+
subjectKey: "selector",
|
|
9609
9750
|
mutating: true,
|
|
9610
9751
|
riskTier: "standard",
|
|
9611
9752
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9634,6 +9775,9 @@ var browserPressTool = {
|
|
|
9634
9775
|
description: "Press a keyboard key in an owned browser session.",
|
|
9635
9776
|
usageHint: "browser_press({ sessionId, key })",
|
|
9636
9777
|
permission: "confirm",
|
|
9778
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9779
|
+
// rule must distinguish clicking one control from another.
|
|
9780
|
+
subjectKey: "key",
|
|
9637
9781
|
mutating: true,
|
|
9638
9782
|
riskTier: "standard",
|
|
9639
9783
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9652,6 +9796,9 @@ var browserDragTool = {
|
|
|
9652
9796
|
description: "Drag one element to another in an owned browser session.",
|
|
9653
9797
|
usageHint: "browser_drag({ sessionId, from, to })",
|
|
9654
9798
|
permission: "confirm",
|
|
9799
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9800
|
+
// rule must distinguish clicking one control from another.
|
|
9801
|
+
subjectKey: "from",
|
|
9655
9802
|
mutating: true,
|
|
9656
9803
|
riskTier: "standard",
|
|
9657
9804
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9701,6 +9848,9 @@ var browserEvaluateTool = {
|
|
|
9701
9848
|
description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
|
|
9702
9849
|
usageHint: "browser_evaluate({ sessionId, expression })",
|
|
9703
9850
|
permission: "confirm",
|
|
9851
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9852
|
+
// rule must distinguish clicking one control from another.
|
|
9853
|
+
subjectKey: "expression",
|
|
9704
9854
|
mutating: true,
|
|
9705
9855
|
riskTier: "destructive",
|
|
9706
9856
|
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
|
|
@@ -9723,6 +9873,9 @@ var browserUploadTool = {
|
|
|
9723
9873
|
description: "Upload project-local files through a file input in an owned browser session.",
|
|
9724
9874
|
usageHint: "browser_upload({ sessionId, selector, files })",
|
|
9725
9875
|
permission: "confirm",
|
|
9876
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9877
|
+
// rule must distinguish clicking one control from another.
|
|
9878
|
+
subjectKey: "selector",
|
|
9726
9879
|
mutating: true,
|
|
9727
9880
|
riskTier: "destructive",
|
|
9728
9881
|
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
|
|
@@ -10658,6 +10811,7 @@ var REFS_INDEX_SQL = [
|
|
|
10658
10811
|
var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
|
|
10659
10812
|
|
|
10660
10813
|
// src/codebase-index/writer-search-helpers.ts
|
|
10814
|
+
var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
|
|
10661
10815
|
function normalizeSearchLimit(limit) {
|
|
10662
10816
|
return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
|
|
10663
10817
|
}
|
|
@@ -10687,8 +10841,8 @@ function buildWriterSearchWhere(query, filter) {
|
|
|
10687
10841
|
}
|
|
10688
10842
|
if (query.trim()) {
|
|
10689
10843
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
10690
|
-
conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
|
|
10691
|
-
for (const token of tokens) values.push(`%${token}%`);
|
|
10844
|
+
conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
|
|
10845
|
+
for (const token of tokens) values.push(`%${escapeLike(token)}%`);
|
|
10692
10846
|
}
|
|
10693
10847
|
return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
|
|
10694
10848
|
}
|
|
@@ -10797,6 +10951,7 @@ var StorePool = class {
|
|
|
10797
10951
|
|
|
10798
10952
|
// src/codebase-index/writer.ts
|
|
10799
10953
|
var DB_FILE2 = "index.db";
|
|
10954
|
+
var MAX_STATEMENT_CACHE = 128;
|
|
10800
10955
|
var IndexStore = class _IndexStore {
|
|
10801
10956
|
db;
|
|
10802
10957
|
/** Absolute path to this project's index directory. */
|
|
@@ -10834,12 +10989,31 @@ var IndexStore = class _IndexStore {
|
|
|
10834
10989
|
// Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
|
|
10835
10990
|
// an empty or pre-existing corpus makes a stale IDF table meaningless.
|
|
10836
10991
|
bm25Dirty = true;
|
|
10837
|
-
/**
|
|
10992
|
+
/**
|
|
10993
|
+
* Prepare-once helper: compile `sql` on first use, reuse thereafter.
|
|
10994
|
+
*
|
|
10995
|
+
* Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
|
|
10996
|
+
* the fallback search builder emits one `text LIKE ?` clause per query token
|
|
10997
|
+
* — so the SQL varies with the token count and a stream of differently-sized
|
|
10998
|
+
* queries grew the cache without limit. Sage's store already bounds its
|
|
10999
|
+
* equivalent at 128 (WS-096).
|
|
11000
|
+
*
|
|
11001
|
+
* Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
|
|
11002
|
+
* insertRefs, …) at the young end, so a burst of one-off search SQL evicts
|
|
11003
|
+
* itself rather than the reindex hot path.
|
|
11004
|
+
*/
|
|
10838
11005
|
stmt(sql) {
|
|
10839
|
-
|
|
10840
|
-
if (
|
|
10841
|
-
|
|
10842
|
-
this.stmtCache.set(sql,
|
|
11006
|
+
const cached = this.stmtCache.get(sql);
|
|
11007
|
+
if (cached !== void 0) {
|
|
11008
|
+
this.stmtCache.delete(sql);
|
|
11009
|
+
this.stmtCache.set(sql, cached);
|
|
11010
|
+
return cached;
|
|
11011
|
+
}
|
|
11012
|
+
const s = this.db.prepare(sql);
|
|
11013
|
+
this.stmtCache.set(sql, s);
|
|
11014
|
+
if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
|
|
11015
|
+
const oldest = this.stmtCache.keys().next();
|
|
11016
|
+
if (!oldest.done) this.stmtCache.delete(oldest.value);
|
|
10843
11017
|
}
|
|
10844
11018
|
return s;
|
|
10845
11019
|
}
|
|
@@ -11112,7 +11286,7 @@ var IndexStore = class _IndexStore {
|
|
|
11112
11286
|
const { where, values } = built;
|
|
11113
11287
|
const limit = normalizeSearchLimit(opts?.limit);
|
|
11114
11288
|
const limitSql = limit !== void 0 ? " LIMIT ?" : "";
|
|
11115
|
-
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment
|
|
11289
|
+
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
|
|
11116
11290
|
const binds = limit !== void 0 ? [...values, limit] : values;
|
|
11117
11291
|
const rows = this.stmt(sql).all(
|
|
11118
11292
|
...binds
|
|
@@ -11241,11 +11415,13 @@ var IndexStore = class _IndexStore {
|
|
|
11241
11415
|
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
11242
11416
|
searchRankedFallback(query, filter, limit) {
|
|
11243
11417
|
if (!query.trim()) {
|
|
11244
|
-
const
|
|
11245
|
-
if (
|
|
11246
|
-
return { results: this.search(query, filter, { limit }), total };
|
|
11418
|
+
const total2 = this.countSearch(query, filter);
|
|
11419
|
+
if (total2 === 0) return { results: [], total: 0 };
|
|
11420
|
+
return { results: this.search(query, filter, { limit }), total: total2 };
|
|
11247
11421
|
}
|
|
11248
|
-
const
|
|
11422
|
+
const total = this.countSearch(query, filter);
|
|
11423
|
+
if (total === 0) return { results: [], total: 0 };
|
|
11424
|
+
const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
|
|
11249
11425
|
if (candidates.length === 0) return { results: [], total: 0 };
|
|
11250
11426
|
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
11251
11427
|
const bm25 = this.getOrBuildBm25();
|
|
@@ -11271,7 +11447,7 @@ var IndexStore = class _IndexStore {
|
|
|
11271
11447
|
const c = expectDefined2(candidateById.get(id));
|
|
11272
11448
|
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
11273
11449
|
});
|
|
11274
|
-
return { results, total
|
|
11450
|
+
return { results, total };
|
|
11275
11451
|
}
|
|
11276
11452
|
getAllIndexable() {
|
|
11277
11453
|
return getAllIndexableWithStatement((sql) => this.stmt(sql));
|
|
@@ -11690,12 +11866,12 @@ var buildIdCache;
|
|
|
11690
11866
|
function projectIndexServerBuildId(entrypoint) {
|
|
11691
11867
|
const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path16.resolve(entrypoint);
|
|
11692
11868
|
try {
|
|
11693
|
-
const
|
|
11694
|
-
if (buildIdCache?.file === file && buildIdCache.mtimeMs ===
|
|
11869
|
+
const stat18 = fs11.statSync(file);
|
|
11870
|
+
if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat18.mtimeMs && buildIdCache.size === stat18.size) {
|
|
11695
11871
|
return buildIdCache.buildId;
|
|
11696
11872
|
}
|
|
11697
11873
|
const buildId = createHash4("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
|
|
11698
|
-
buildIdCache = { file, mtimeMs:
|
|
11874
|
+
buildIdCache = { file, mtimeMs: stat18.mtimeMs, size: stat18.size, buildId };
|
|
11699
11875
|
return buildId;
|
|
11700
11876
|
} catch {
|
|
11701
11877
|
return `unreadable:${path16.basename(file)}`;
|
|
@@ -11865,6 +12041,8 @@ var ProjectServerConnection = class {
|
|
|
11865
12041
|
connectResolve = null;
|
|
11866
12042
|
connectReject = null;
|
|
11867
12043
|
nextId = 1;
|
|
12044
|
+
/** WS-027: read from the owner-only metadata file — see currentAuthToken. */
|
|
12045
|
+
authToken;
|
|
11868
12046
|
pending = /* @__PURE__ */ new Map();
|
|
11869
12047
|
transition(status, options = {}) {
|
|
11870
12048
|
const previous = connectionStates.get(this.endpoint);
|
|
@@ -12001,6 +12179,31 @@ var ProjectServerConnection = class {
|
|
|
12001
12179
|
this.transition("offline");
|
|
12002
12180
|
maybeStopHeartbeatLoop();
|
|
12003
12181
|
}
|
|
12182
|
+
/**
|
|
12183
|
+
* WS-027: the per-process token, read from the daemon's owner-only metadata
|
|
12184
|
+
* file — never from the `hello` frame, which the daemon sends to every
|
|
12185
|
+
* socket that connects (the mistake WS-028 found in the SAGE daemon).
|
|
12186
|
+
*
|
|
12187
|
+
* Read lazily and re-read while unknown: the daemon starts listening before
|
|
12188
|
+
* it writes metadata (endpoint ownership has to be won first), and a
|
|
12189
|
+
* respawned daemon mints a new token, so a cached wrong one would be sticky.
|
|
12190
|
+
*/
|
|
12191
|
+
currentAuthToken() {
|
|
12192
|
+
if (this.authToken === void 0) {
|
|
12193
|
+
try {
|
|
12194
|
+
const raw = fs12.readFileSync(
|
|
12195
|
+
projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
|
|
12196
|
+
"utf8"
|
|
12197
|
+
);
|
|
12198
|
+
const parsed = JSON.parse(raw);
|
|
12199
|
+
if (typeof parsed.authToken === "string" && parsed.authToken.length > 0) {
|
|
12200
|
+
this.authToken = parsed.authToken;
|
|
12201
|
+
}
|
|
12202
|
+
} catch {
|
|
12203
|
+
}
|
|
12204
|
+
}
|
|
12205
|
+
return this.authToken;
|
|
12206
|
+
}
|
|
12004
12207
|
request(message, options) {
|
|
12005
12208
|
const socket = this.socket;
|
|
12006
12209
|
if (!socket || socket.destroyed) {
|
|
@@ -12044,7 +12247,7 @@ var ProjectServerConnection = class {
|
|
|
12044
12247
|
return;
|
|
12045
12248
|
}
|
|
12046
12249
|
}
|
|
12047
|
-
this.write({ ...message, id });
|
|
12250
|
+
this.write({ ...message, id, authToken: this.currentAuthToken() });
|
|
12048
12251
|
});
|
|
12049
12252
|
}
|
|
12050
12253
|
async ensureConnected(spawnIfMissing) {
|
|
@@ -12091,6 +12294,7 @@ var ProjectServerConnection = class {
|
|
|
12091
12294
|
connectOnce() {
|
|
12092
12295
|
this.socket?.destroy();
|
|
12093
12296
|
this.socket = null;
|
|
12297
|
+
this.authToken = void 0;
|
|
12094
12298
|
this.info = null;
|
|
12095
12299
|
this.activity = null;
|
|
12096
12300
|
this.health = null;
|
|
@@ -12700,9 +12904,9 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12700
12904
|
const statReadParse = await Promise.allSettled(
|
|
12701
12905
|
batchFiles.map(
|
|
12702
12906
|
async (file) => {
|
|
12703
|
-
let
|
|
12907
|
+
let stat18;
|
|
12704
12908
|
try {
|
|
12705
|
-
|
|
12909
|
+
stat18 = await fs17.stat(file, statOpts);
|
|
12706
12910
|
} catch (e) {
|
|
12707
12911
|
if (isAbortError(e)) throw e;
|
|
12708
12912
|
return {
|
|
@@ -12714,21 +12918,21 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12714
12918
|
missing: isMissingPathError(e)
|
|
12715
12919
|
};
|
|
12716
12920
|
}
|
|
12717
|
-
if (!
|
|
12921
|
+
if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
|
|
12718
12922
|
const lang = detectLang(file);
|
|
12719
|
-
if (!lang) return { file, stat:
|
|
12720
|
-
if (
|
|
12923
|
+
if (!lang) return { file, stat: stat18, lang: "", parsed: null };
|
|
12924
|
+
if (stat18.size > MAX_INDEX_FILE_BYTES) {
|
|
12721
12925
|
return {
|
|
12722
12926
|
file,
|
|
12723
|
-
stat:
|
|
12927
|
+
stat: stat18,
|
|
12724
12928
|
lang,
|
|
12725
12929
|
parsed: null,
|
|
12726
|
-
error: `file too large (${
|
|
12930
|
+
error: `file too large (${stat18.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
12727
12931
|
};
|
|
12728
12932
|
}
|
|
12729
12933
|
const meta = existingMeta.get(file);
|
|
12730
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
12731
|
-
return { file, stat:
|
|
12934
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
|
|
12935
|
+
return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
|
|
12732
12936
|
}
|
|
12733
12937
|
let content;
|
|
12734
12938
|
try {
|
|
@@ -12737,7 +12941,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12737
12941
|
if (isAbortError(e)) throw e;
|
|
12738
12942
|
return {
|
|
12739
12943
|
file,
|
|
12740
|
-
stat:
|
|
12944
|
+
stat: stat18,
|
|
12741
12945
|
lang,
|
|
12742
12946
|
parsed: null,
|
|
12743
12947
|
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
@@ -12749,13 +12953,13 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12749
12953
|
} catch (e) {
|
|
12750
12954
|
return {
|
|
12751
12955
|
file,
|
|
12752
|
-
stat:
|
|
12956
|
+
stat: stat18,
|
|
12753
12957
|
lang,
|
|
12754
12958
|
parsed: null,
|
|
12755
12959
|
error: `parse error: ${e instanceof Error ? e.message : String(e)}`
|
|
12756
12960
|
};
|
|
12757
12961
|
}
|
|
12758
|
-
return { file, stat:
|
|
12962
|
+
return { file, stat: stat18, lang, parsed, content };
|
|
12759
12963
|
}
|
|
12760
12964
|
)
|
|
12761
12965
|
);
|
|
@@ -12776,7 +12980,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12776
12980
|
errors.push(`${file}: ${result.error}`);
|
|
12777
12981
|
continue;
|
|
12778
12982
|
}
|
|
12779
|
-
const { stat:
|
|
12983
|
+
const { stat: stat18, lang, parsed } = result;
|
|
12780
12984
|
if (result.skippedMeta) {
|
|
12781
12985
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
12782
12986
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -12788,7 +12992,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12788
12992
|
store.upsertFile({
|
|
12789
12993
|
file,
|
|
12790
12994
|
lang,
|
|
12791
|
-
mtimeMs: Math.floor(
|
|
12995
|
+
mtimeMs: Math.floor(stat18.mtimeMs),
|
|
12792
12996
|
symbolCount: 0,
|
|
12793
12997
|
lastIndexed: Date.now()
|
|
12794
12998
|
});
|
|
@@ -12800,7 +13004,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12800
13004
|
store.replaceEmptyFile({
|
|
12801
13005
|
file,
|
|
12802
13006
|
lang,
|
|
12803
|
-
mtimeMs: Math.floor(
|
|
13007
|
+
mtimeMs: Math.floor(stat18.mtimeMs),
|
|
12804
13008
|
symbolCount: 0,
|
|
12805
13009
|
lastIndexed: Date.now()
|
|
12806
13010
|
});
|
|
@@ -12812,7 +13016,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12812
13016
|
lang,
|
|
12813
13017
|
symbols: parsed.symbols,
|
|
12814
13018
|
refs: parsed.refs ?? [],
|
|
12815
|
-
mtimeMs: Math.floor(
|
|
13019
|
+
mtimeMs: Math.floor(stat18.mtimeMs),
|
|
12816
13020
|
symbolCount: parsed.symbols.length
|
|
12817
13021
|
});
|
|
12818
13022
|
deleteForFiles.push(file);
|
|
@@ -13865,8 +14069,9 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
13865
14069
|
const content = fs19.readFileSync(epFile, "utf8");
|
|
13866
14070
|
const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
|
|
13867
14071
|
const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
|
|
13868
|
-
|
|
13869
|
-
|
|
14072
|
+
for (; ; ) {
|
|
14073
|
+
const match = reExportRe.exec(strippedContent);
|
|
14074
|
+
if (match === null) break;
|
|
13870
14075
|
const moduleSpec = match[1];
|
|
13871
14076
|
const resolvedFiles = resolveModulePath(epFile, moduleSpec, indexedFiles);
|
|
13872
14077
|
for (const rf of resolvedFiles) {
|
|
@@ -14072,6 +14277,9 @@ var designTool = {
|
|
|
14072
14277
|
description: 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: "list" (menu), "use" (load+pin a kit for a stack), "foundations" (baseline), "set" (override kit colors/tokens), "materialize" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), "verify" (scan UI files for off-palette colors).',
|
|
14073
14278
|
usageHint: 'Flow: `design {action:"use", kit:"minimal-clarity", stack:"web"}` \u2192 optionally `design {action:"set", set:{primary:"oklch(62% 0.2 25)"}}` \u2192 `design {action:"materialize"}` to write tokens to disk \u2192 implement against them \u2192 `design {action:"verify"}`.',
|
|
14074
14279
|
permission: "confirm",
|
|
14280
|
+
// WS-046: gives permission decisions something to key on.
|
|
14281
|
+
// The action performed; `out` is optional so it cannot be the subject.
|
|
14282
|
+
subjectKey: "action",
|
|
14075
14283
|
mutating: true,
|
|
14076
14284
|
capabilities: ["fs.write"],
|
|
14077
14285
|
timeoutMs: 15e3,
|
|
@@ -14252,9 +14460,7 @@ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme fi
|
|
|
14252
14460
|
const abs = path25.join(absParent, path25.basename(absResolved));
|
|
14253
14461
|
const rel = path25.relative(root, abs);
|
|
14254
14462
|
if (rel.startsWith("..") || path25.isAbsolute(rel)) {
|
|
14255
|
-
throw new Error(
|
|
14256
|
-
`design: materialize path "${result.path}" would escape the project root`
|
|
14257
|
-
);
|
|
14463
|
+
throw new Error(`design: materialize path "${result.path}" would escape the project root`);
|
|
14258
14464
|
}
|
|
14259
14465
|
let exists = false;
|
|
14260
14466
|
try {
|
|
@@ -14329,6 +14535,7 @@ import { statSync as statSync3 } from "node:fs";
|
|
|
14329
14535
|
import * as fs21 from "node:fs/promises";
|
|
14330
14536
|
import * as path26 from "node:path";
|
|
14331
14537
|
import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
|
|
14538
|
+
var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
|
|
14332
14539
|
var diffTool = {
|
|
14333
14540
|
name: "diff",
|
|
14334
14541
|
category: "Filesystem",
|
|
@@ -14336,6 +14543,7 @@ var diffTool = {
|
|
|
14336
14543
|
usageHint: 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\n\n- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes).\n- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\n- `staged: true` \u2192 only show staged changes.\n- `mode` can be "unified", "stat", or "side-by-side" (only affects the git-diff path).\n\nNOTE: For a true file-vs-file unified diff, supply `a` and `b` so the tool delegates to `git diff`. The `files`-only path is a line-numbered dump, not a diff.\n\nThis tool has important safety guards against flag injection (see previous security findings).',
|
|
14337
14544
|
permission: "auto",
|
|
14338
14545
|
mutating: false,
|
|
14546
|
+
maxOutputBytes: 262144,
|
|
14339
14547
|
capabilities: ["fs.read"],
|
|
14340
14548
|
icon: "diff",
|
|
14341
14549
|
timeoutMs: 1e4,
|
|
@@ -14411,8 +14619,8 @@ function findGitDir(cwd) {
|
|
|
14411
14619
|
let dir = cwd;
|
|
14412
14620
|
for (let i = 0; i < 20; i++) {
|
|
14413
14621
|
try {
|
|
14414
|
-
const
|
|
14415
|
-
if (
|
|
14622
|
+
const stat18 = statSync3(path26.join(dir, ".git"));
|
|
14623
|
+
if (stat18.isDirectory()) return dir;
|
|
14416
14624
|
} catch {
|
|
14417
14625
|
}
|
|
14418
14626
|
const parent = path26.dirname(dir);
|
|
@@ -14454,10 +14662,18 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
14454
14662
|
};
|
|
14455
14663
|
}
|
|
14456
14664
|
const results = [];
|
|
14665
|
+
let truncated = false;
|
|
14457
14666
|
for (const file of files) {
|
|
14458
14667
|
const absPath = safeResolve(file, ctx);
|
|
14459
|
-
const
|
|
14460
|
-
if (!
|
|
14668
|
+
const stat18 = await fs21.stat(absPath).catch(() => null);
|
|
14669
|
+
if (!stat18?.isFile()) continue;
|
|
14670
|
+
if (stat18.size > MAX_FILE_DUMP_BYTES) {
|
|
14671
|
+
truncated = true;
|
|
14672
|
+
results.push(
|
|
14673
|
+
`--- ${file} (skipped: ${stat18.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
|
|
14674
|
+
);
|
|
14675
|
+
continue;
|
|
14676
|
+
}
|
|
14461
14677
|
const content = await fs21.readFile(absPath, "utf8");
|
|
14462
14678
|
const lines = content.split(/\r?\n/);
|
|
14463
14679
|
results.push(formatWithLineNumbers(file, lines));
|
|
@@ -14465,7 +14681,7 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
14465
14681
|
return {
|
|
14466
14682
|
diff: results.join("\n\n"),
|
|
14467
14683
|
files,
|
|
14468
|
-
truncated
|
|
14684
|
+
truncated,
|
|
14469
14685
|
mode: input.mode ?? "unified"
|
|
14470
14686
|
};
|
|
14471
14687
|
}
|
|
@@ -14558,8 +14774,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
14558
14774
|
for (const f of files) {
|
|
14559
14775
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
14560
14776
|
try {
|
|
14561
|
-
const
|
|
14562
|
-
if (
|
|
14777
|
+
const stat18 = await fs22.stat(absPath);
|
|
14778
|
+
if (stat18.isFile()) resolved.push(absPath);
|
|
14563
14779
|
} catch {
|
|
14564
14780
|
}
|
|
14565
14781
|
}
|
|
@@ -15412,7 +15628,7 @@ var editTool = {
|
|
|
15412
15628
|
});
|
|
15413
15629
|
}
|
|
15414
15630
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
15415
|
-
const
|
|
15631
|
+
const stat18 = await fs23.stat(absPath).catch((err) => {
|
|
15416
15632
|
if (err.code === "ENOENT") {
|
|
15417
15633
|
throw new ToolValidationError({
|
|
15418
15634
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
@@ -15422,7 +15638,7 @@ var editTool = {
|
|
|
15422
15638
|
}
|
|
15423
15639
|
throw err;
|
|
15424
15640
|
});
|
|
15425
|
-
if (!
|
|
15641
|
+
if (!stat18.isFile()) {
|
|
15426
15642
|
throw new ToolValidationError({
|
|
15427
15643
|
message: `edit: "${input.path}" is not a regular file`,
|
|
15428
15644
|
field: "path"
|
|
@@ -15452,7 +15668,7 @@ var editTool = {
|
|
|
15452
15668
|
});
|
|
15453
15669
|
}
|
|
15454
15670
|
}
|
|
15455
|
-
if (autoRead && updated.mtimeMs >
|
|
15671
|
+
if (autoRead && updated.mtimeMs > stat18.mtimeMs + mtimeTolerance) {
|
|
15456
15672
|
throw new ToolValidationError({
|
|
15457
15673
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
15458
15674
|
field: "path",
|
|
@@ -15575,10 +15791,6 @@ import {
|
|
|
15575
15791
|
emitProcessStarted as emitProcessStarted3
|
|
15576
15792
|
} from "@wrongstack/core/observability";
|
|
15577
15793
|
import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
|
|
15578
|
-
init_output_spool();
|
|
15579
|
-
init_util();
|
|
15580
|
-
init_process_registry();
|
|
15581
|
-
init_win32_resolve();
|
|
15582
15794
|
|
|
15583
15795
|
// src/_danger-detect.ts
|
|
15584
15796
|
var argHas = (args, value) => args.includes(value);
|
|
@@ -15830,6 +16042,48 @@ var RULES = [
|
|
|
15830
16042
|
return args.some((a) => /^[0-7]{3,4}$/.test(a) && /7/.test(a));
|
|
15831
16043
|
},
|
|
15832
16044
|
reason: "chmod with world-writable octal mode (e.g. 777)"
|
|
16045
|
+
},
|
|
16046
|
+
// ----- network recon / offensive scanners (caution) -----
|
|
16047
|
+
// These tools are purpose-built for network reconnaissance and offensive
|
|
16048
|
+
// security. They have legitimate uses in dev (security testing, infra
|
|
16049
|
+
// debugging) but their presence in an agent's command stream is worth a
|
|
16050
|
+
// caution banner so the user can notice an unexpected scan.
|
|
16051
|
+
{
|
|
16052
|
+
id: "network-scanner",
|
|
16053
|
+
level: "caution",
|
|
16054
|
+
test: (cmd) => cmd === "nmap" || cmd === "masscan" || cmd === "zmap" || cmd === "nuclei" || cmd === "hping3" || cmd === "naabu" || cmd === "katana" || cmd === "amass" || cmd === "subfinder" || cmd === "httpx" || cmd === "rustscan" || cmd === "zgrab",
|
|
16055
|
+
reason: "network scanner / offensive reconnaissance tool"
|
|
16056
|
+
},
|
|
16057
|
+
// ----- process termination (caution) -----
|
|
16058
|
+
// kill/killall/pkill terminate processes. The exec kill guard
|
|
16059
|
+
// (exec-kill-guard.ts) hard-blocks attempts on protected WrongStack
|
|
16060
|
+
// processes; this rule adds a caution banner for generic process
|
|
16061
|
+
// termination so the user sees a warning even for non-protected targets.
|
|
16062
|
+
{
|
|
16063
|
+
id: "process-kill",
|
|
16064
|
+
level: "caution",
|
|
16065
|
+
test: (cmd) => cmd === "kill" || cmd === "killall" || cmd === "pkill",
|
|
16066
|
+
reason: "process termination command"
|
|
16067
|
+
},
|
|
16068
|
+
// ----- shell/interpreter launchers (caution) -----
|
|
16069
|
+
// `env bash -c '…'`, `timeout sh`, `nohup perl -e '…'` etc. wrap a child
|
|
16070
|
+
// binary that bypasses the allowlist name-gate entirely (the wrapper
|
|
16071
|
+
// resolves the child from PATH, not from the allowlist). We flag caution
|
|
16072
|
+
// when a known launcher has a shell/interpreter as its target program.
|
|
16073
|
+
{
|
|
16074
|
+
id: "shell-launcher",
|
|
16075
|
+
level: "caution",
|
|
16076
|
+
test: (cmd, args) => {
|
|
16077
|
+
const launchers = ["env", "timeout", "nohup", "nice", "script", "expect", "tmux", "screen", "byobu", "dtach"];
|
|
16078
|
+
if (!launchers.includes(cmd)) return false;
|
|
16079
|
+
const shells = ["sh", "bash", "zsh", "fish", "python", "python3", "perl", "ruby", "node", "pwsh", "powershell", "cmd"];
|
|
16080
|
+
if (cmd === "env") {
|
|
16081
|
+
const prog = args.find((a) => !a.includes("=") && !a.startsWith("-"));
|
|
16082
|
+
return prog !== void 0 && shells.includes(prog);
|
|
16083
|
+
}
|
|
16084
|
+
return args.some((a) => shells.includes(a));
|
|
16085
|
+
},
|
|
16086
|
+
reason: "launcher wrapping a shell/interpreter (bypasses allowlist name-gate)"
|
|
15833
16087
|
}
|
|
15834
16088
|
];
|
|
15835
16089
|
function detectDanger(cmd, args, bypass) {
|
|
@@ -15861,6 +16115,11 @@ function levelRank(level) {
|
|
|
15861
16115
|
}
|
|
15862
16116
|
}
|
|
15863
16117
|
|
|
16118
|
+
// src/exec.ts
|
|
16119
|
+
init_output_spool();
|
|
16120
|
+
init_util();
|
|
16121
|
+
init_win32_resolve();
|
|
16122
|
+
|
|
15864
16123
|
// src/exec-kill-guard.ts
|
|
15865
16124
|
import * as os8 from "node:os";
|
|
15866
16125
|
import * as path29 from "node:path";
|
|
@@ -16096,8 +16355,7 @@ async function checkKillTarget(target) {
|
|
|
16096
16355
|
return { blocked: false };
|
|
16097
16356
|
}
|
|
16098
16357
|
|
|
16099
|
-
// src/exec.ts
|
|
16100
|
-
var isWin4 = process.platform === "win32";
|
|
16358
|
+
// src/exec-allowlist.ts
|
|
16101
16359
|
var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
16102
16360
|
// JS / TS toolchain
|
|
16103
16361
|
"node",
|
|
@@ -16176,7 +16434,11 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16176
16434
|
"docker",
|
|
16177
16435
|
"podman",
|
|
16178
16436
|
"kubectl",
|
|
16179
|
-
// network
|
|
16437
|
+
// network fetch tools. Per-arg safety is NOT enforced by BLOCKED_ARG_PATTERNS
|
|
16438
|
+
// (no curl/wget entries) — the real boundary is the per-call `confirm`
|
|
16439
|
+
// permission gate (exec.ts permission:'confirm', subjectKey:'command'),
|
|
16440
|
+
// the `pipe-to-shell` danger rule, and the kill guard. The allowlist is a
|
|
16441
|
+
// guidance layer, not a security boundary on its own.
|
|
16180
16442
|
"curl",
|
|
16181
16443
|
"wget",
|
|
16182
16444
|
// common POSIX file/text utilities
|
|
@@ -16229,9 +16491,15 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16229
16491
|
"powershell.exe",
|
|
16230
16492
|
"pwsh",
|
|
16231
16493
|
"pwsh.exe",
|
|
16232
|
-
// [core] Extended default allowlist (added 4b3d18d1 + this commit).
|
|
16233
|
-
//
|
|
16234
|
-
//
|
|
16494
|
+
// [core] Extended default allowlist (added 4b3d18d1 + this commit). Broadly
|
|
16495
|
+
// used dev binaries — NOT all non-destructive: this section contains
|
|
16496
|
+
// process/system tools (kill, chown, mount, userdel), network scanners
|
|
16497
|
+
// (nmap, masscan, zmap, nuclei), and launchers (env, perl, timeout, tmux).
|
|
16498
|
+
// The real safety boundary for these is the per-call `confirm` permission
|
|
16499
|
+
// gate (exec.ts permission:'confirm', subjectKey:'command' renders the full
|
|
16500
|
+
// invocation) plus the danger rules in _danger-detect.ts. Per-arg hard-deny
|
|
16501
|
+
// is still enforced by BLOCKED_ARG_PATTERNS + bash-kill-guard.ts, but only
|
|
16502
|
+
// for the commands that have entries there (git, find, rm).
|
|
16235
16503
|
// --- Archives & compression ---
|
|
16236
16504
|
"7z",
|
|
16237
16505
|
"7za",
|
|
@@ -16403,10 +16671,7 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16403
16671
|
"git-lfs",
|
|
16404
16672
|
"tig",
|
|
16405
16673
|
"lazygit",
|
|
16406
|
-
// --- POSIX text utilities (extended
|
|
16407
|
-
// grep/find/echo/awk/mkdir/cp/mv/rm/touch from the base list above are
|
|
16408
|
-
// omitted — the Set is de-duplicated at runtime but a clean literal is
|
|
16409
|
-
// easier to maintain) ---
|
|
16674
|
+
// --- POSIX text utilities (extended) ---
|
|
16410
16675
|
"gawk",
|
|
16411
16676
|
"tr",
|
|
16412
16677
|
"cut",
|
|
@@ -16439,39 +16704,16 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16439
16704
|
"locate",
|
|
16440
16705
|
"which",
|
|
16441
16706
|
"whereis",
|
|
16442
|
-
|
|
16443
|
-
|
|
16444
|
-
|
|
16445
|
-
|
|
16446
|
-
|
|
16447
|
-
|
|
16448
|
-
"fc",
|
|
16449
|
-
"jobs",
|
|
16450
|
-
"bg",
|
|
16451
|
-
"fg",
|
|
16452
|
-
"wait",
|
|
16453
|
-
"ulimit",
|
|
16454
|
-
"umask",
|
|
16707
|
+
// NOTE: shell builtins with no standalone binary (type, hash, pushd,
|
|
16708
|
+
// popd, dirs, history, fc, jobs, bg, fg, wait, ulimit, umask, trap,
|
|
16709
|
+
// exit, return, source, ., alias, unalias, set, unset, export, readonly,
|
|
16710
|
+
// typeset, declare, local, eval, exec) are intentionally excluded —
|
|
16711
|
+
// spawn() would fail with ENOENT on every platform because they only
|
|
16712
|
+
// exist inside a shell process. Use the `bash` tool for builtins.
|
|
16455
16713
|
"nice",
|
|
16456
16714
|
"nohup",
|
|
16457
16715
|
"timeout",
|
|
16458
16716
|
"time",
|
|
16459
|
-
"trap",
|
|
16460
|
-
"exit",
|
|
16461
|
-
"return",
|
|
16462
|
-
"source",
|
|
16463
|
-
".",
|
|
16464
|
-
"alias",
|
|
16465
|
-
"unalias",
|
|
16466
|
-
"set",
|
|
16467
|
-
"unset",
|
|
16468
|
-
"export",
|
|
16469
|
-
"readonly",
|
|
16470
|
-
"typeset",
|
|
16471
|
-
"declare",
|
|
16472
|
-
"local",
|
|
16473
|
-
"eval",
|
|
16474
|
-
"exec",
|
|
16475
16717
|
// --- Process / system inspection ---
|
|
16476
16718
|
"htop",
|
|
16477
16719
|
"top",
|
|
@@ -16702,6 +16944,10 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16702
16944
|
"sublert",
|
|
16703
16945
|
"chaos"
|
|
16704
16946
|
]);
|
|
16947
|
+
|
|
16948
|
+
// src/exec.ts
|
|
16949
|
+
init_process_registry();
|
|
16950
|
+
var isWin4 = process.platform === "win32";
|
|
16705
16951
|
var allowedCommands = new Set(DEFAULT_ALLOWED_COMMANDS);
|
|
16706
16952
|
var normalizeCmd = (c) => c.trim();
|
|
16707
16953
|
var dangerBypass = /* @__PURE__ */ new Set();
|
|
@@ -16733,7 +16979,17 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
16733
16979
|
bun: [],
|
|
16734
16980
|
docker: [],
|
|
16735
16981
|
// find -exec/-ok/-execdir execute arbitrary commands
|
|
16736
|
-
find: [
|
|
16982
|
+
find: [
|
|
16983
|
+
/^-exec$/,
|
|
16984
|
+
/^-exec;$/,
|
|
16985
|
+
/^-ok$/,
|
|
16986
|
+
/^-ok;$/,
|
|
16987
|
+
/^-execdir$/,
|
|
16988
|
+
/^-execdir;$/,
|
|
16989
|
+
/^-exec=/,
|
|
16990
|
+
/^-ok=/,
|
|
16991
|
+
/^-execdir=/
|
|
16992
|
+
],
|
|
16737
16993
|
// rm -rf / is catastrophic — block absolute paths, home, dot-dirs,
|
|
16738
16994
|
// and glob patterns that could expand to dangerous targets.
|
|
16739
16995
|
// `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
|
|
@@ -16804,6 +17060,14 @@ var execTool = {
|
|
|
16804
17060
|
useInstead: ["bash"]
|
|
16805
17061
|
},
|
|
16806
17062
|
permission: "confirm",
|
|
17063
|
+
// WS-046: without this, every exec call collapsed onto the bare tool name.
|
|
17064
|
+
// Three consequences followed: "always allow" stored a rule that could never
|
|
17065
|
+
// match, pressing "no" once blocked ALL exec for the session, and the
|
|
17066
|
+
// permission cache keyed one decision for every command. `subjectKey:
|
|
17067
|
+
// 'command'` renders the FULL invocation (`command` + `args`) — using the
|
|
17068
|
+
// program alone would make `exec git status` and `exec git push --force` the
|
|
17069
|
+
// same subject, so trusting one would silently authorize the other.
|
|
17070
|
+
subjectKey: "command",
|
|
16807
17071
|
mutating: true,
|
|
16808
17072
|
riskTier: "standard",
|
|
16809
17073
|
timeoutMs: DEFAULT_TIMEOUT_MS3,
|
|
@@ -17020,7 +17284,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
17020
17284
|
const pid = child.pid;
|
|
17021
17285
|
if (typeof pid === "number") {
|
|
17022
17286
|
const fullCommand = `${cmd} ${args.join(" ")}`;
|
|
17023
|
-
registry.register({
|
|
17287
|
+
registry.register({
|
|
17288
|
+
pid,
|
|
17289
|
+
name: "exec",
|
|
17290
|
+
command: redactCommand(fullCommand),
|
|
17291
|
+
startedAt: Date.now(),
|
|
17292
|
+
sessionId,
|
|
17293
|
+
child
|
|
17294
|
+
});
|
|
17024
17295
|
}
|
|
17025
17296
|
const timer = setTimeout(() => {
|
|
17026
17297
|
killed = true;
|
|
@@ -17460,6 +17731,9 @@ var formatTool = {
|
|
|
17460
17731
|
description: "Format source files according to project style (Biome). Can also run in check-only mode.",
|
|
17461
17732
|
usageHint: "RUN REGULARLY:\n\n- Use on changed files before committing.\n- `check: true` verifies formatting without making changes (useful in CI-like flows).\nThis project has very consistent formatting expectations. Always ensure your changes are formatted.",
|
|
17462
17733
|
permission: "confirm",
|
|
17734
|
+
// WS-046: gives permission decisions something to key on.
|
|
17735
|
+
// The files being rewritten are the subject; the fixer is how, not what.
|
|
17736
|
+
subjectKey: "files",
|
|
17463
17737
|
mutating: true,
|
|
17464
17738
|
capabilities: ["fs.write", "shell.restricted"],
|
|
17465
17739
|
icon: "code",
|
|
@@ -17564,13 +17838,13 @@ var formatTool = {
|
|
|
17564
17838
|
}
|
|
17565
17839
|
};
|
|
17566
17840
|
async function detectFixer(cwd) {
|
|
17567
|
-
const { stat:
|
|
17841
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
17568
17842
|
try {
|
|
17569
|
-
await
|
|
17843
|
+
await stat18(`${cwd}/biome.json`);
|
|
17570
17844
|
return "biome";
|
|
17571
17845
|
} catch {
|
|
17572
17846
|
try {
|
|
17573
|
-
await
|
|
17847
|
+
await stat18(`${cwd}/.prettierrc`);
|
|
17574
17848
|
return "prettier";
|
|
17575
17849
|
} catch {
|
|
17576
17850
|
return "biome";
|
|
@@ -17597,6 +17871,10 @@ var gitTool = {
|
|
|
17597
17871
|
// Conservative: any of these may mutate. The non-mutating commands
|
|
17598
17872
|
// (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
|
|
17599
17873
|
// and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
|
|
17874
|
+
// WS-046: gives permission decisions something to key on.
|
|
17875
|
+
// The git subcommand (status/commit/push) is what a trust rule needs to
|
|
17876
|
+
// distinguish; `git status` and `git push --force` must not share a subject.
|
|
17877
|
+
subjectKey: "command",
|
|
17600
17878
|
mutating: true,
|
|
17601
17879
|
capabilities: ["fs.write", "shell.restricted"],
|
|
17602
17880
|
timeoutMs: TIMEOUT_MS2,
|
|
@@ -17665,6 +17943,8 @@ var gitTool = {
|
|
|
17665
17943
|
truncated: false
|
|
17666
17944
|
};
|
|
17667
17945
|
}
|
|
17946
|
+
const branchGuard = validateBranchInput(input);
|
|
17947
|
+
if (branchGuard) return branchGuard;
|
|
17668
17948
|
if (input.command === "worktree") {
|
|
17669
17949
|
const guard = validateWorktreeInput(input, ctx.projectRoot);
|
|
17670
17950
|
if (guard) return guard;
|
|
@@ -17713,6 +17993,18 @@ var gitTool = {
|
|
|
17713
17993
|
return result;
|
|
17714
17994
|
}
|
|
17715
17995
|
};
|
|
17996
|
+
function validateBranchInput(input) {
|
|
17997
|
+
const branch = input.branch;
|
|
17998
|
+
if (branch === void 0) return null;
|
|
17999
|
+
if (!branch.startsWith("-") && !branch.includes(" --")) return null;
|
|
18000
|
+
return {
|
|
18001
|
+
command: input.command,
|
|
18002
|
+
stdout: "",
|
|
18003
|
+
stderr: `unsafe branch name (parsed as a git option): ${branch}`,
|
|
18004
|
+
exitCode: 1,
|
|
18005
|
+
truncated: false
|
|
18006
|
+
};
|
|
18007
|
+
}
|
|
17716
18008
|
function validateWorktreeInput(input, projectRoot) {
|
|
17717
18009
|
const reject = (stderr) => ({
|
|
17718
18010
|
command: "worktree",
|
|
@@ -17721,7 +18013,6 @@ function validateWorktreeInput(input, projectRoot) {
|
|
|
17721
18013
|
exitCode: 1,
|
|
17722
18014
|
truncated: false
|
|
17723
18015
|
});
|
|
17724
|
-
if (input.branch?.startsWith("-")) return reject(`unsafe branch name: ${input.branch}`);
|
|
17725
18016
|
if (input.worktreePath?.startsWith("-")) {
|
|
17726
18017
|
return reject(`unsafe worktree path: ${input.worktreePath}`);
|
|
17727
18018
|
}
|
|
@@ -17739,8 +18030,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
17739
18030
|
let dir = cwd;
|
|
17740
18031
|
for (let i = 0; i < 20; i++) {
|
|
17741
18032
|
try {
|
|
17742
|
-
const
|
|
17743
|
-
if (
|
|
18033
|
+
const stat18 = statSync4(`${dir}/.git`);
|
|
18034
|
+
if (stat18.isDirectory() || stat18.isFile()) return dir;
|
|
17744
18035
|
} catch {
|
|
17745
18036
|
}
|
|
17746
18037
|
if (dir === root) break;
|
|
@@ -17775,7 +18066,10 @@ function buildArgs(input) {
|
|
|
17775
18066
|
...files.length ? ["--", ...files] : []
|
|
17776
18067
|
];
|
|
17777
18068
|
case "branch":
|
|
17778
|
-
return input.branch ? [
|
|
18069
|
+
return input.branch ? [
|
|
18070
|
+
"branch",
|
|
18071
|
+
...input.branch.startsWith("-") || input.branch.includes(" --") ? [] : [input.branch]
|
|
18072
|
+
] : ["branch"];
|
|
17779
18073
|
case "checkout":
|
|
17780
18074
|
return [
|
|
17781
18075
|
"checkout",
|
|
@@ -17868,7 +18162,7 @@ import * as path30 from "node:path";
|
|
|
17868
18162
|
import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
|
|
17869
18163
|
|
|
17870
18164
|
// src/_concurrency.ts
|
|
17871
|
-
async function
|
|
18165
|
+
async function mapWithConcurrency2(items, limit, fn) {
|
|
17872
18166
|
if (items.length === 0) return [];
|
|
17873
18167
|
const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));
|
|
17874
18168
|
const results = new Array(items.length);
|
|
@@ -18003,10 +18297,10 @@ var globTool = {
|
|
|
18003
18297
|
}
|
|
18004
18298
|
if (truncated) return;
|
|
18005
18299
|
}
|
|
18006
|
-
await
|
|
18300
|
+
await mapWithConcurrency2(matchedFiles, WALK_CONCURRENCY, pushResult);
|
|
18007
18301
|
if (truncated) return;
|
|
18008
18302
|
const remainingSubdirs = truncated ? [] : subdirs;
|
|
18009
|
-
await
|
|
18303
|
+
await mapWithConcurrency2(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk(full, rel));
|
|
18010
18304
|
};
|
|
18011
18305
|
await walk(base, "");
|
|
18012
18306
|
results.sort((a, b) => b.mtime - a.mtime);
|
|
@@ -18015,12 +18309,16 @@ var globTool = {
|
|
|
18015
18309
|
};
|
|
18016
18310
|
|
|
18017
18311
|
// src/grep.ts
|
|
18018
|
-
import { expectDefined as expectDefined7 } from "@wrongstack/core/utils";
|
|
18019
18312
|
import { spawn as spawn11 } from "node:child_process";
|
|
18020
18313
|
import * as fs25 from "node:fs/promises";
|
|
18021
18314
|
import * as path31 from "node:path";
|
|
18022
18315
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
18023
|
-
import {
|
|
18316
|
+
import {
|
|
18317
|
+
buildChildEnv as buildChildEnv5,
|
|
18318
|
+
compileGlob as compileGlob3,
|
|
18319
|
+
DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS3,
|
|
18320
|
+
expectDefined as expectDefined7
|
|
18321
|
+
} from "@wrongstack/core/utils";
|
|
18024
18322
|
|
|
18025
18323
|
// src/_regex.ts
|
|
18026
18324
|
var MAX_PATTERN_LEN = 256;
|
|
@@ -18073,6 +18371,8 @@ var DEFAULT_IGNORE3 = DEFAULT_WALK_IGNORE_DIRS3;
|
|
|
18073
18371
|
var NATIVE_SCAN_CONCURRENCY = 32;
|
|
18074
18372
|
var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
|
|
18075
18373
|
var NATIVE_MAX_FILE_BYTES = 1e6;
|
|
18374
|
+
var RG_MAX_QUEUE_CHUNKS = 128;
|
|
18375
|
+
var RG_MAX_QUEUE_CHARS = 8 * 1024 * 1024;
|
|
18076
18376
|
var grepTool = {
|
|
18077
18377
|
name: "grep",
|
|
18078
18378
|
category: "Search",
|
|
@@ -18200,9 +18500,19 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
18200
18500
|
const FLUSH_AT = 16;
|
|
18201
18501
|
const MAX_BUF_BYTES = 1e6;
|
|
18202
18502
|
let bufOverflow = false;
|
|
18203
|
-
const child = spawn11("rg", args, {
|
|
18503
|
+
const child = spawn11("rg", args, {
|
|
18504
|
+
signal,
|
|
18505
|
+
env: buildChildEnv5(),
|
|
18506
|
+
// rg diagnostics are not part of the tool result. Ignoring stderr avoids
|
|
18507
|
+
// an unread pipe filling up and pinning the child when rg reports many
|
|
18508
|
+
// filesystem errors.
|
|
18509
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
18510
|
+
windowsHide: true
|
|
18511
|
+
});
|
|
18204
18512
|
const queue = [];
|
|
18513
|
+
let queuedChars = 0;
|
|
18205
18514
|
let waiter;
|
|
18515
|
+
let paused = false;
|
|
18206
18516
|
const wake = () => {
|
|
18207
18517
|
if (waiter) {
|
|
18208
18518
|
const w = waiter;
|
|
@@ -18210,93 +18520,133 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
18210
18520
|
w();
|
|
18211
18521
|
}
|
|
18212
18522
|
};
|
|
18213
|
-
|
|
18214
|
-
queue.
|
|
18523
|
+
const pauseIfFlooded = () => {
|
|
18524
|
+
if (paused || queue.length < RG_MAX_QUEUE_CHUNKS && queuedChars < RG_MAX_QUEUE_CHARS)
|
|
18525
|
+
return;
|
|
18526
|
+
paused = true;
|
|
18527
|
+
child.stdout?.pause();
|
|
18528
|
+
};
|
|
18529
|
+
const resumeIfDrained = () => {
|
|
18530
|
+
if (!paused || queue.length >= RG_MAX_QUEUE_CHUNKS || queuedChars >= RG_MAX_QUEUE_CHARS)
|
|
18531
|
+
return;
|
|
18532
|
+
paused = false;
|
|
18533
|
+
child.stdout?.resume();
|
|
18534
|
+
};
|
|
18535
|
+
const onStdoutData = (c) => {
|
|
18536
|
+
const data = c.toString();
|
|
18537
|
+
queue.push({ kind: "out", data });
|
|
18538
|
+
queuedChars += data.length;
|
|
18215
18539
|
wake();
|
|
18216
|
-
|
|
18217
|
-
|
|
18540
|
+
pauseIfFlooded();
|
|
18541
|
+
};
|
|
18542
|
+
const onError = (e) => {
|
|
18218
18543
|
queue.push({ kind: "error", data: e.message });
|
|
18219
18544
|
wake();
|
|
18220
|
-
}
|
|
18221
|
-
|
|
18545
|
+
};
|
|
18546
|
+
const onClose = () => {
|
|
18222
18547
|
queue.push({ kind: "close", data: "" });
|
|
18223
18548
|
wake();
|
|
18224
|
-
}
|
|
18549
|
+
};
|
|
18550
|
+
child.stdout?.on("data", onStdoutData);
|
|
18551
|
+
child.on("error", onError);
|
|
18552
|
+
child.on("close", onClose);
|
|
18225
18553
|
let pendingBatch = [];
|
|
18226
18554
|
let errored = false;
|
|
18227
|
-
|
|
18228
|
-
|
|
18229
|
-
|
|
18230
|
-
|
|
18231
|
-
|
|
18232
|
-
|
|
18233
|
-
|
|
18234
|
-
if (c.kind === "error") {
|
|
18235
|
-
errored = true;
|
|
18236
|
-
continue;
|
|
18237
|
-
}
|
|
18238
|
-
if (c.kind === "close") break;
|
|
18239
|
-
buf += c.data;
|
|
18240
|
-
if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
|
|
18241
|
-
bufOverflow = true;
|
|
18242
|
-
buf = buf.slice(-MAX_BUF_BYTES);
|
|
18243
|
-
try {
|
|
18244
|
-
child.kill("SIGTERM");
|
|
18245
|
-
} catch {
|
|
18555
|
+
let closed = false;
|
|
18556
|
+
try {
|
|
18557
|
+
for (; ; ) {
|
|
18558
|
+
while (queue.length === 0) {
|
|
18559
|
+
await new Promise((r) => {
|
|
18560
|
+
waiter = r;
|
|
18561
|
+
});
|
|
18246
18562
|
}
|
|
18247
|
-
|
|
18248
|
-
|
|
18249
|
-
|
|
18250
|
-
|
|
18251
|
-
|
|
18252
|
-
|
|
18253
|
-
|
|
18254
|
-
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
|
|
18258
|
-
|
|
18259
|
-
|
|
18563
|
+
const c = expectDefined7(queue.shift());
|
|
18564
|
+
queuedChars = Math.max(0, queuedChars - c.data.length);
|
|
18565
|
+
resumeIfDrained();
|
|
18566
|
+
if (c.kind === "error") {
|
|
18567
|
+
errored = true;
|
|
18568
|
+
continue;
|
|
18569
|
+
}
|
|
18570
|
+
if (c.kind === "close") {
|
|
18571
|
+
closed = true;
|
|
18572
|
+
break;
|
|
18573
|
+
}
|
|
18574
|
+
buf += c.data;
|
|
18575
|
+
if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
|
|
18576
|
+
bufOverflow = true;
|
|
18577
|
+
buf = buf.slice(-MAX_BUF_BYTES);
|
|
18578
|
+
try {
|
|
18579
|
+
child.kill("SIGTERM");
|
|
18580
|
+
} catch {
|
|
18581
|
+
}
|
|
18582
|
+
}
|
|
18583
|
+
const idx = buf.lastIndexOf("\n");
|
|
18584
|
+
if (idx === -1) continue;
|
|
18585
|
+
const ready = buf.slice(0, idx);
|
|
18586
|
+
buf = buf.slice(idx + 1);
|
|
18587
|
+
for (const line of ready.split("\n")) {
|
|
18588
|
+
if (!line) continue;
|
|
18589
|
+
totalLines++;
|
|
18590
|
+
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18591
|
+
if (matches.length < limit) {
|
|
18592
|
+
matches.push(line);
|
|
18593
|
+
pendingBatch.push(line);
|
|
18594
|
+
batchSinceFlush++;
|
|
18595
|
+
}
|
|
18596
|
+
}
|
|
18597
|
+
if (batchSinceFlush >= FLUSH_AT) {
|
|
18598
|
+
yield {
|
|
18599
|
+
type: "partial_output",
|
|
18600
|
+
text: pendingBatch.join("\n"),
|
|
18601
|
+
data: { matches_so_far: matches.length }
|
|
18602
|
+
};
|
|
18603
|
+
pendingBatch = [];
|
|
18604
|
+
batchSinceFlush = 0;
|
|
18605
|
+
}
|
|
18606
|
+
}
|
|
18607
|
+
if (buf.trim()) {
|
|
18608
|
+
for (const line of buf.split("\n")) {
|
|
18609
|
+
if (!line) continue;
|
|
18610
|
+
totalLines++;
|
|
18611
|
+
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18612
|
+
if (matches.length < limit) {
|
|
18613
|
+
matches.push(line);
|
|
18614
|
+
pendingBatch.push(line);
|
|
18615
|
+
}
|
|
18260
18616
|
}
|
|
18261
18617
|
}
|
|
18262
|
-
if (
|
|
18618
|
+
if (pendingBatch.length > 0) {
|
|
18263
18619
|
yield {
|
|
18264
18620
|
type: "partial_output",
|
|
18265
18621
|
text: pendingBatch.join("\n"),
|
|
18266
18622
|
data: { matches_so_far: matches.length }
|
|
18267
18623
|
};
|
|
18268
|
-
pendingBatch = [];
|
|
18269
|
-
batchSinceFlush = 0;
|
|
18270
|
-
}
|
|
18271
|
-
}
|
|
18272
|
-
if (buf.trim()) {
|
|
18273
|
-
for (const line of buf.split("\n")) {
|
|
18274
|
-
if (!line) continue;
|
|
18275
|
-
totalLines++;
|
|
18276
|
-
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18277
|
-
if (matches.length < limit) {
|
|
18278
|
-
matches.push(line);
|
|
18279
|
-
pendingBatch.push(line);
|
|
18280
|
-
}
|
|
18281
18624
|
}
|
|
18282
|
-
|
|
18283
|
-
if (pendingBatch.length > 0) {
|
|
18625
|
+
if (errored) throw new Error("rg: spawn error");
|
|
18284
18626
|
yield {
|
|
18285
|
-
type: "
|
|
18286
|
-
|
|
18287
|
-
|
|
18627
|
+
type: "final",
|
|
18628
|
+
output: {
|
|
18629
|
+
matches,
|
|
18630
|
+
count: mode === "count" ? totalCount : totalLines,
|
|
18631
|
+
truncated: totalLines > limit || bufOverflow,
|
|
18632
|
+
used: "rg"
|
|
18633
|
+
}
|
|
18288
18634
|
};
|
|
18289
|
-
}
|
|
18290
|
-
|
|
18291
|
-
|
|
18292
|
-
|
|
18293
|
-
|
|
18294
|
-
|
|
18295
|
-
|
|
18296
|
-
|
|
18297
|
-
|
|
18635
|
+
} finally {
|
|
18636
|
+
child.stdout?.off("data", onStdoutData);
|
|
18637
|
+
child.off("error", onError);
|
|
18638
|
+
child.off("close", onClose);
|
|
18639
|
+
child.stdout?.destroy();
|
|
18640
|
+
queue.length = 0;
|
|
18641
|
+
queuedChars = 0;
|
|
18642
|
+
waiter = void 0;
|
|
18643
|
+
if (!closed && child.exitCode === null) {
|
|
18644
|
+
try {
|
|
18645
|
+
child.kill("SIGTERM");
|
|
18646
|
+
} catch {
|
|
18647
|
+
}
|
|
18298
18648
|
}
|
|
18299
|
-
}
|
|
18649
|
+
}
|
|
18300
18650
|
}
|
|
18301
18651
|
function parseRgCountLine(line) {
|
|
18302
18652
|
const idx = line.lastIndexOf(":");
|
|
@@ -18326,8 +18676,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18326
18676
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
18327
18677
|
if (globRe) globRe.lastIndex = 0;
|
|
18328
18678
|
try {
|
|
18329
|
-
const
|
|
18330
|
-
if (!
|
|
18679
|
+
const stat18 = await fs25.stat(full);
|
|
18680
|
+
if (!stat18.isFile() || stat18.size > maxBytes || stopped || signal.aborted) return;
|
|
18331
18681
|
const file = await fs25.open(full, "r");
|
|
18332
18682
|
try {
|
|
18333
18683
|
let bytesReadTotal = 0;
|
|
@@ -18427,8 +18777,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18427
18777
|
files.push({ full, name: e.name });
|
|
18428
18778
|
}
|
|
18429
18779
|
}
|
|
18430
|
-
await
|
|
18431
|
-
await
|
|
18780
|
+
await mapWithConcurrency2(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
|
|
18781
|
+
await mapWithConcurrency2(
|
|
18432
18782
|
subdirs,
|
|
18433
18783
|
Math.min(16, NATIVE_SCAN_CONCURRENCY),
|
|
18434
18784
|
({ full, rel }) => walk(full, rel)
|
|
@@ -18447,7 +18797,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18447
18797
|
init_spawn_stream();
|
|
18448
18798
|
init_util();
|
|
18449
18799
|
init_legacy_bridge();
|
|
18450
|
-
import { join as
|
|
18800
|
+
import { join as join25 } from "node:path";
|
|
18451
18801
|
import {
|
|
18452
18802
|
detectEcosystem as detectPackageEcosystem,
|
|
18453
18803
|
recordPackageAction
|
|
@@ -18458,6 +18808,10 @@ var installTool = {
|
|
|
18458
18808
|
description: "Install, update or manage packages using the detected package manager (pnpm/npm/yarn). Strongly preferred over raw shell commands for dependency management because it is structured and safer.",
|
|
18459
18809
|
usageHint: "ALWAYS USE THIS INSTEAD OF BASH FOR PACKAGE WORK:\n\n- Empty `packages` \u2192 normal `install` (respects lockfile).\n- Provide names \u2192 adds/updates specific packages.\n- `dry_run: true` for safe preview.\n- Set `save` appropriately.\nThis tool has proper capability declaration and is heavily recommended in the security posture of the project.",
|
|
18460
18810
|
permission: "confirm",
|
|
18811
|
+
// WS-046: gives permission decisions something to key on.
|
|
18812
|
+
// What gets installed is the subject — installing a package runs its
|
|
18813
|
+
// lifecycle scripts, so `express` and `evil-pkg` must not be interchangeable.
|
|
18814
|
+
subjectKey: "packages",
|
|
18461
18815
|
mutating: true,
|
|
18462
18816
|
riskTier: "standard",
|
|
18463
18817
|
icon: "package",
|
|
@@ -18627,10 +18981,10 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
18627
18981
|
case "pnpm":
|
|
18628
18982
|
case "yarn":
|
|
18629
18983
|
case "npm":
|
|
18630
|
-
return
|
|
18984
|
+
return join25(cwd, "package.json");
|
|
18631
18985
|
/* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
|
|
18632
18986
|
default:
|
|
18633
|
-
return
|
|
18987
|
+
return join25(cwd, "package.json");
|
|
18634
18988
|
}
|
|
18635
18989
|
}
|
|
18636
18990
|
|
|
@@ -18638,6 +18992,24 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
18638
18992
|
init_util();
|
|
18639
18993
|
import * as fs26 from "node:fs/promises";
|
|
18640
18994
|
import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
|
|
18995
|
+
var MAX_JSON_FILE_BYTES = 16 * 1024 * 1024;
|
|
18996
|
+
var MAX_JSON_FILE_BYTES_HUMAN = "16 MiB";
|
|
18997
|
+
var JsonFileTooLargeError = class extends Error {
|
|
18998
|
+
constructor(filePath, size) {
|
|
18999
|
+
super(
|
|
19000
|
+
`json: "${filePath}" is ${size} bytes \u2014 exceeds the ${MAX_JSON_FILE_BYTES_HUMAN} file-size limit. Extract the relevant portion into a smaller file first, or use a shell tool to query it directly.`
|
|
19001
|
+
);
|
|
19002
|
+
this.name = "JsonFileTooLargeError";
|
|
19003
|
+
}
|
|
19004
|
+
};
|
|
19005
|
+
async function readJsonFileBounded(filePath, ctx) {
|
|
19006
|
+
const resolved = await safeResolveReal(filePath, ctx);
|
|
19007
|
+
const stat18 = await fs26.stat(resolved);
|
|
19008
|
+
if (stat18.size > MAX_JSON_FILE_BYTES) {
|
|
19009
|
+
throw new JsonFileTooLargeError(filePath, stat18.size);
|
|
19010
|
+
}
|
|
19011
|
+
return fs26.readFile(resolved, "utf8");
|
|
19012
|
+
}
|
|
18641
19013
|
var jsonTool = {
|
|
18642
19014
|
name: "json",
|
|
18643
19015
|
category: "Data",
|
|
@@ -18645,6 +19017,7 @@ var jsonTool = {
|
|
|
18645
19017
|
usageHint: 'VERY USEFUL FOR DATA INSPECTION:\n\n- `action: "parse"` (default): read/pretty-print/convert JSON, JSON5, or YAML from `file` or `data`.\n- `action: "query"`: JMESPath-like query (`a.b[0].c`, `items[*].name`, filters, functions).\n- `action: "validate"`: validate data against a JSON Schema (`schema` param).\n- `action: "transform"`: chain multiple JMESPath transforms (`transforms` param).\n- `action: "merge"`: deep merge `base` and `patch` objects (`conflictResolution` param).\nPrefer this over raw `read` + manual parsing when dealing with configuration or data files.',
|
|
18646
19018
|
permission: "auto",
|
|
18647
19019
|
mutating: false,
|
|
19020
|
+
maxOutputBytes: 262144,
|
|
18648
19021
|
timeoutMs: 5e3,
|
|
18649
19022
|
capabilities: ["fs.read"],
|
|
18650
19023
|
icon: "json",
|
|
@@ -18711,9 +19084,15 @@ async function executeParse(input, ctx) {
|
|
|
18711
19084
|
let raw;
|
|
18712
19085
|
if (input.file) {
|
|
18713
19086
|
try {
|
|
18714
|
-
raw = await
|
|
18715
|
-
} catch {
|
|
18716
|
-
return {
|
|
19087
|
+
raw = await readJsonFileBounded(input.file, ctx);
|
|
19088
|
+
} catch (error) {
|
|
19089
|
+
return {
|
|
19090
|
+
data: null,
|
|
19091
|
+
formatted: "",
|
|
19092
|
+
type: "unknown",
|
|
19093
|
+
action: "parse",
|
|
19094
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read file"
|
|
19095
|
+
};
|
|
18717
19096
|
}
|
|
18718
19097
|
} else if (input.data) {
|
|
18719
19098
|
raw = input.data;
|
|
@@ -18769,10 +19148,16 @@ async function executeQuery(input, ctx) {
|
|
|
18769
19148
|
let parsed;
|
|
18770
19149
|
if (input.file) {
|
|
18771
19150
|
try {
|
|
18772
|
-
const raw = await
|
|
19151
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
18773
19152
|
parsed = JSON.parse(raw);
|
|
18774
|
-
} catch {
|
|
18775
|
-
return {
|
|
19153
|
+
} catch (error) {
|
|
19154
|
+
return {
|
|
19155
|
+
data: null,
|
|
19156
|
+
formatted: "",
|
|
19157
|
+
type: "unknown",
|
|
19158
|
+
action: "query",
|
|
19159
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
|
|
19160
|
+
};
|
|
18776
19161
|
}
|
|
18777
19162
|
} else if (input.data) {
|
|
18778
19163
|
try {
|
|
@@ -18811,10 +19196,16 @@ async function executeValidate(input, ctx) {
|
|
|
18811
19196
|
let parsed;
|
|
18812
19197
|
if (input.file) {
|
|
18813
19198
|
try {
|
|
18814
|
-
const raw = await
|
|
19199
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
18815
19200
|
parsed = JSON.parse(raw);
|
|
18816
|
-
} catch {
|
|
18817
|
-
return {
|
|
19201
|
+
} catch (error) {
|
|
19202
|
+
return {
|
|
19203
|
+
data: null,
|
|
19204
|
+
formatted: "",
|
|
19205
|
+
type: "unknown",
|
|
19206
|
+
action: "validate",
|
|
19207
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
|
|
19208
|
+
};
|
|
18818
19209
|
}
|
|
18819
19210
|
} else if (input.data) {
|
|
18820
19211
|
try {
|
|
@@ -18853,10 +19244,16 @@ async function executeTransform(input, ctx) {
|
|
|
18853
19244
|
let parsed;
|
|
18854
19245
|
if (input.file) {
|
|
18855
19246
|
try {
|
|
18856
|
-
const raw = await
|
|
19247
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
18857
19248
|
parsed = JSON.parse(raw);
|
|
18858
|
-
} catch {
|
|
18859
|
-
return {
|
|
19249
|
+
} catch (error) {
|
|
19250
|
+
return {
|
|
19251
|
+
data: null,
|
|
19252
|
+
formatted: "",
|
|
19253
|
+
type: "unknown",
|
|
19254
|
+
action: "transform",
|
|
19255
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
|
|
19256
|
+
};
|
|
18860
19257
|
}
|
|
18861
19258
|
} else if (input.data) {
|
|
18862
19259
|
try {
|
|
@@ -19460,6 +19857,10 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
19460
19857
|
}
|
|
19461
19858
|
|
|
19462
19859
|
// src/kanban-board-inputs.ts
|
|
19860
|
+
function agentSettableGate(enforcement) {
|
|
19861
|
+
if (enforcement === void 0 || enforcement === "off") return {};
|
|
19862
|
+
return { completionGate: { enforcement } };
|
|
19863
|
+
}
|
|
19463
19864
|
function boardCreateInput(input, title) {
|
|
19464
19865
|
return {
|
|
19465
19866
|
title,
|
|
@@ -19472,7 +19873,7 @@ function boardCreateInput(input, title) {
|
|
|
19472
19873
|
decomposition: input.atomicityDecomposition ?? "propose"
|
|
19473
19874
|
}
|
|
19474
19875
|
} : {},
|
|
19475
|
-
...input.gateEnforcement
|
|
19876
|
+
...agentSettableGate(input.gateEnforcement)
|
|
19476
19877
|
};
|
|
19477
19878
|
}
|
|
19478
19879
|
function boardUpdatePatch(input) {
|
|
@@ -19486,7 +19887,7 @@ function boardUpdatePatch(input) {
|
|
|
19486
19887
|
decomposition: input.atomicityDecomposition ?? "propose"
|
|
19487
19888
|
}
|
|
19488
19889
|
} : {},
|
|
19489
|
-
...input.gateEnforcement
|
|
19890
|
+
...agentSettableGate(input.gateEnforcement)
|
|
19490
19891
|
};
|
|
19491
19892
|
}
|
|
19492
19893
|
function duplicateBoardOptions(input) {
|
|
@@ -19501,6 +19902,7 @@ function duplicateBoardOptions(input) {
|
|
|
19501
19902
|
|
|
19502
19903
|
// src/kanban-task-inputs.ts
|
|
19503
19904
|
import { randomUUID } from "node:crypto";
|
|
19905
|
+
import { clampSubagentCapabilities } from "@wrongstack/core/security";
|
|
19504
19906
|
function taskInput(input) {
|
|
19505
19907
|
const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
|
|
19506
19908
|
return {
|
|
@@ -19598,6 +20000,10 @@ function taskPatch(input) {
|
|
|
19598
20000
|
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
19599
20001
|
};
|
|
19600
20002
|
}
|
|
20003
|
+
function clampRequestedCapabilities(requested) {
|
|
20004
|
+
if (requested === void 0) return void 0;
|
|
20005
|
+
return clampSubagentCapabilities(requested).granted;
|
|
20006
|
+
}
|
|
19601
20007
|
function assignmentInput(input) {
|
|
19602
20008
|
return {
|
|
19603
20009
|
agentId: input.agentId,
|
|
@@ -19608,7 +20014,7 @@ function assignmentInput(input) {
|
|
|
19608
20014
|
fallbackProfile: input.fallbackProfile,
|
|
19609
20015
|
fallbackModels: input.fallbackModels,
|
|
19610
20016
|
tools: input.tools,
|
|
19611
|
-
allowedCapabilities: input.allowedCapabilities,
|
|
20017
|
+
allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities),
|
|
19612
20018
|
assignee: input.assignee,
|
|
19613
20019
|
leaseId: input.leaseId,
|
|
19614
20020
|
claimedAt: input.claimedAt,
|
|
@@ -19635,7 +20041,7 @@ function assignmentForTaskCreate(input) {
|
|
|
19635
20041
|
...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
|
|
19636
20042
|
...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
|
|
19637
20043
|
...input.tools !== void 0 ? { tools: input.tools } : {},
|
|
19638
|
-
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: input.allowedCapabilities } : {},
|
|
20044
|
+
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
|
|
19639
20045
|
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
19640
20046
|
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
19641
20047
|
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
@@ -19846,7 +20252,13 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
19846
20252
|
moveTasksToColumnId: { type: "string" },
|
|
19847
20253
|
atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
|
|
19848
20254
|
atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
|
|
19849
|
-
gateEnforcement: {
|
|
20255
|
+
gateEnforcement: {
|
|
20256
|
+
type: "string",
|
|
20257
|
+
// WS-023: `'off'` is deliberately absent. The agent whose work this gate
|
|
20258
|
+
// checks must not be able to switch it off; it may only tighten. Turning
|
|
20259
|
+
// a gate off stays a human decision, made through board config.
|
|
20260
|
+
enum: ["strict", "soft"]
|
|
20261
|
+
},
|
|
19850
20262
|
subtasks: {
|
|
19851
20263
|
type: "array",
|
|
19852
20264
|
minItems: 2,
|
|
@@ -19953,7 +20365,9 @@ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
|
|
|
19953
20365
|
description: "Live session work: todos, tasks, and plan items.",
|
|
19954
20366
|
tags: sessionBoardTags(sessionId),
|
|
19955
20367
|
columns: SESSION_KANBAN_COLUMNS,
|
|
19956
|
-
generatedBy: `session-kanban:${sessionId}
|
|
20368
|
+
generatedBy: `session-kanban:${sessionId}`,
|
|
20369
|
+
kind: "session_mirror",
|
|
20370
|
+
retention: { mode: "archive_after_ttl", ttlMs: 7 * 24 * 60 * 60 * 1e3 }
|
|
19957
20371
|
});
|
|
19958
20372
|
}
|
|
19959
20373
|
if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
|
|
@@ -20987,11 +21401,11 @@ var lintTool = {
|
|
|
20987
21401
|
}
|
|
20988
21402
|
};
|
|
20989
21403
|
async function detectLinter(cwd) {
|
|
20990
|
-
const { stat:
|
|
21404
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
20991
21405
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
20992
21406
|
for (const f of checks) {
|
|
20993
21407
|
try {
|
|
20994
|
-
await
|
|
21408
|
+
await stat18(`${cwd}/${f}`);
|
|
20995
21409
|
if (f.includes("biome")) return "biome";
|
|
20996
21410
|
if (f.includes("eslint")) return "eslint";
|
|
20997
21411
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -21235,6 +21649,9 @@ var outdatedTool = {
|
|
|
21235
21649
|
// tool.confirm_needed flow on every invocation. M-1 originally
|
|
21236
21650
|
// fixed four sibling tools (mcp_control, shellcheck, shellcheck (scan mode),
|
|
21237
21651
|
// search) but missed this one; applying the same contract here.
|
|
21652
|
+
// WS-046: gives permission decisions something to key on.
|
|
21653
|
+
// The working directory scanned.
|
|
21654
|
+
subjectKey: "cwd",
|
|
21238
21655
|
mutating: true,
|
|
21239
21656
|
// Capability is outbound network — the tool only hits the package
|
|
21240
21657
|
// registry over HTTP, never touches the filesystem or runs shell.
|
|
@@ -21416,6 +21833,10 @@ var patchTool = {
|
|
|
21416
21833
|
useInstead: ["edit"]
|
|
21417
21834
|
},
|
|
21418
21835
|
permission: "confirm",
|
|
21836
|
+
// WS-046: gives permission decisions something to key on.
|
|
21837
|
+
// The target directory. The patch body itself is unstable (every diff
|
|
21838
|
+
// differs), so it could never re-match its own stored rule.
|
|
21839
|
+
subjectKey: "directory",
|
|
21419
21840
|
mutating: true,
|
|
21420
21841
|
capabilities: ["fs.write"],
|
|
21421
21842
|
icon: "edit",
|
|
@@ -21482,8 +21903,8 @@ var patchTool = {
|
|
|
21482
21903
|
const before = beforeContents.get(target) ?? null;
|
|
21483
21904
|
const after = await readTextForTracking(target);
|
|
21484
21905
|
if (after === null || after === before) continue;
|
|
21485
|
-
const
|
|
21486
|
-
if (
|
|
21906
|
+
const stat18 = await fs27.stat(target).catch(() => null);
|
|
21907
|
+
if (stat18) ctx.recordRead?.(target, stat18.mtimeMs, "write", sha256hex(after));
|
|
21487
21908
|
ctx.session?.recordFileChange?.({
|
|
21488
21909
|
path: target,
|
|
21489
21910
|
action: before === null ? "created" : "modified",
|
|
@@ -21508,8 +21929,8 @@ var patchTool = {
|
|
|
21508
21929
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
21509
21930
|
async function readTextForTracking(absPath) {
|
|
21510
21931
|
try {
|
|
21511
|
-
const
|
|
21512
|
-
if (!
|
|
21932
|
+
const stat18 = await fs27.stat(absPath);
|
|
21933
|
+
if (!stat18.isFile() || stat18.size > MAX_TRACKING_BYTES) return null;
|
|
21513
21934
|
const buf = await fs27.readFile(absPath);
|
|
21514
21935
|
if (buf.includes(0)) return null;
|
|
21515
21936
|
return buf.toString("utf8");
|
|
@@ -21539,7 +21960,13 @@ function runPatch(args, cwd, signal) {
|
|
|
21539
21960
|
let stdout = "";
|
|
21540
21961
|
let stderr = "";
|
|
21541
21962
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
21542
|
-
const child = spawn14("patch", args, {
|
|
21963
|
+
const child = spawn14("patch", args, {
|
|
21964
|
+
cwd,
|
|
21965
|
+
signal,
|
|
21966
|
+
env,
|
|
21967
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
21968
|
+
windowsHide: true
|
|
21969
|
+
});
|
|
21543
21970
|
child.stdout?.on("data", (c) => {
|
|
21544
21971
|
stdout += c.toString();
|
|
21545
21972
|
});
|
|
@@ -21560,6 +21987,7 @@ function extractPatchedFiles(output) {
|
|
|
21560
21987
|
}
|
|
21561
21988
|
|
|
21562
21989
|
// src/plan.ts
|
|
21990
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
21563
21991
|
import {
|
|
21564
21992
|
addPlanItem,
|
|
21565
21993
|
clearPlan,
|
|
@@ -21567,20 +21995,20 @@ import {
|
|
|
21567
21995
|
formatPlan,
|
|
21568
21996
|
getPlanTemplate,
|
|
21569
21997
|
mutatePlan as mutatePlan2,
|
|
21998
|
+
mutateTasks as mutateTasks2,
|
|
21570
21999
|
removePlanItem,
|
|
21571
22000
|
setPlanItemStatus
|
|
21572
22001
|
} from "@wrongstack/core/storage";
|
|
21573
|
-
import {
|
|
21574
|
-
mutateTasks as mutateTasks2
|
|
21575
|
-
} from "@wrongstack/core/storage";
|
|
21576
22002
|
import { formatTaskList } from "@wrongstack/core/utils";
|
|
21577
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
21578
22003
|
var planTool = {
|
|
21579
22004
|
name: "plan",
|
|
21580
22005
|
category: "Session",
|
|
21581
22006
|
description: 'Manage a session-persistent strategic plan. The plan is written to disk and survives conversation resumptions within the same session, but is isolated to this session \u2014 other sessions have their own separate plans. Unlike todos (which are per-turn and lost on restart), a plan tracks high-level progress across multiple turns. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute. By default plans are isolated to this session; use `scope: "project"` to store the plan in a shared project-level file visible to all sessions.',
|
|
21582
22007
|
usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns within a session. Plans survive resume but are not shared across separate sessions.\nUse `scope: "project"` to use a shared project-level plan file.',
|
|
21583
22008
|
permission: "confirm",
|
|
22009
|
+
// WS-046: gives permission decisions something to key on.
|
|
22010
|
+
// The action performed; plan has no single file or path subject.
|
|
22011
|
+
subjectKey: "action",
|
|
21584
22012
|
mutating: true,
|
|
21585
22013
|
capabilities: ["fs.write"],
|
|
21586
22014
|
icon: "plan",
|
|
@@ -21637,7 +22065,10 @@ var planTool = {
|
|
|
21637
22065
|
let planPath;
|
|
21638
22066
|
if (input.scope === "project") {
|
|
21639
22067
|
if (typeof sessionPlanPath === "string") {
|
|
21640
|
-
const lastSep = Math.max(
|
|
22068
|
+
const lastSep = Math.max(
|
|
22069
|
+
sessionPlanPath.lastIndexOf("/"),
|
|
22070
|
+
sessionPlanPath.lastIndexOf("\\")
|
|
22071
|
+
);
|
|
21641
22072
|
planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
|
|
21642
22073
|
}
|
|
21643
22074
|
} else {
|
|
@@ -21674,7 +22105,11 @@ var planTool = {
|
|
|
21674
22105
|
case "start":
|
|
21675
22106
|
case "done": {
|
|
21676
22107
|
if (!input.target) {
|
|
21677
|
-
early = mkResult(
|
|
22108
|
+
early = mkResult(
|
|
22109
|
+
p,
|
|
22110
|
+
false,
|
|
22111
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
22112
|
+
);
|
|
21678
22113
|
return p;
|
|
21679
22114
|
}
|
|
21680
22115
|
const next = setPlanItemStatus(
|
|
@@ -21702,7 +22137,11 @@ var planTool = {
|
|
|
21702
22137
|
}
|
|
21703
22138
|
case "promote": {
|
|
21704
22139
|
if (!input.target) {
|
|
21705
|
-
early = mkResult(
|
|
22140
|
+
early = mkResult(
|
|
22141
|
+
p,
|
|
22142
|
+
false,
|
|
22143
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
22144
|
+
);
|
|
21706
22145
|
return p;
|
|
21707
22146
|
}
|
|
21708
22147
|
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
@@ -21745,7 +22184,11 @@ var planTool = {
|
|
|
21745
22184
|
return clearPlan(p);
|
|
21746
22185
|
case "taskify": {
|
|
21747
22186
|
if (!input.target) {
|
|
21748
|
-
early = mkResult(
|
|
22187
|
+
early = mkResult(
|
|
22188
|
+
p,
|
|
22189
|
+
false,
|
|
22190
|
+
"taskify requires `target` (plan item id|index|substring)."
|
|
22191
|
+
);
|
|
21749
22192
|
return p;
|
|
21750
22193
|
}
|
|
21751
22194
|
let itemIdx = -1;
|
|
@@ -21818,7 +22261,11 @@ var planTool = {
|
|
|
21818
22261
|
${formatTaskList(taskFile.tasks)}`
|
|
21819
22262
|
);
|
|
21820
22263
|
} catch (err) {
|
|
21821
|
-
return mkResult(
|
|
22264
|
+
return mkResult(
|
|
22265
|
+
plan,
|
|
22266
|
+
false,
|
|
22267
|
+
`taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
22268
|
+
);
|
|
21822
22269
|
}
|
|
21823
22270
|
}
|
|
21824
22271
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
@@ -21895,9 +22342,9 @@ var readTool = {
|
|
|
21895
22342
|
}
|
|
21896
22343
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
21897
22344
|
const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
|
|
21898
|
-
let
|
|
22345
|
+
let stat18;
|
|
21899
22346
|
try {
|
|
21900
|
-
|
|
22347
|
+
stat18 = await fs28.stat(absPath);
|
|
21901
22348
|
} catch (err) {
|
|
21902
22349
|
const code = err.code;
|
|
21903
22350
|
if (code === "ENOENT") {
|
|
@@ -21916,7 +22363,7 @@ var readTool = {
|
|
|
21916
22363
|
cause: err
|
|
21917
22364
|
});
|
|
21918
22365
|
}
|
|
21919
|
-
if (!
|
|
22366
|
+
if (!stat18.isFile()) {
|
|
21920
22367
|
throw new FsError({
|
|
21921
22368
|
message: `read: "${input.path}" is not a regular file`,
|
|
21922
22369
|
code: "FS_READ_FAILED",
|
|
@@ -21924,23 +22371,23 @@ var readTool = {
|
|
|
21924
22371
|
context: { reason: "not-a-regular-file" }
|
|
21925
22372
|
});
|
|
21926
22373
|
}
|
|
21927
|
-
if (
|
|
22374
|
+
if (stat18.size > MAX_BYTES2) {
|
|
21928
22375
|
throw new FsError({
|
|
21929
|
-
message: `read: file too large (${
|
|
22376
|
+
message: `read: file too large (${stat18.size} bytes, limit ${MAX_BYTES2})`,
|
|
21930
22377
|
code: "FS_READ_FAILED",
|
|
21931
22378
|
path: absPath,
|
|
21932
|
-
context: { size:
|
|
22379
|
+
context: { size: stat18.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
21933
22380
|
});
|
|
21934
22381
|
}
|
|
21935
22382
|
const offset = Math.max(1, input.offset ?? 1);
|
|
21936
22383
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
21937
22384
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
21938
22385
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
21939
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
21940
|
-
ctx.recordRead(absPath,
|
|
22386
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat18.mtimeMs, offset, requestedEnd)) {
|
|
22387
|
+
ctx.recordRead(absPath, stat18.mtimeMs);
|
|
21941
22388
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
21942
22389
|
return {
|
|
21943
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
22390
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat18.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
21944
22391
|
total_lines: prior.totalLines,
|
|
21945
22392
|
encoding: "utf8",
|
|
21946
22393
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -21958,11 +22405,11 @@ var readTool = {
|
|
|
21958
22405
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
21959
22406
|
const total = allLines.length;
|
|
21960
22407
|
if (input.mode === "summary") {
|
|
21961
|
-
ctx.recordRead(absPath,
|
|
21962
|
-
rememberReadRange(ctx, absPath,
|
|
22408
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
|
|
22409
|
+
rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, Math.min(total, 200));
|
|
21963
22410
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
21964
22411
|
return {
|
|
21965
|
-
text: summarizeFile(input.path,
|
|
22412
|
+
text: summarizeFile(input.path, stat18.size, allLines),
|
|
21966
22413
|
total_lines: total,
|
|
21967
22414
|
encoding: "utf8",
|
|
21968
22415
|
truncated: total > 200,
|
|
@@ -21974,8 +22421,8 @@ var readTool = {
|
|
|
21974
22421
|
};
|
|
21975
22422
|
}
|
|
21976
22423
|
if (limit === 0) {
|
|
21977
|
-
ctx.recordRead(absPath,
|
|
21978
|
-
rememberReadRange(ctx, absPath,
|
|
22424
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
|
|
22425
|
+
rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, 0);
|
|
21979
22426
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
21980
22427
|
return {
|
|
21981
22428
|
text: "",
|
|
@@ -21987,8 +22434,8 @@ var readTool = {
|
|
|
21987
22434
|
};
|
|
21988
22435
|
}
|
|
21989
22436
|
if (offset > total) {
|
|
21990
|
-
ctx.recordRead(absPath,
|
|
21991
|
-
rememberReadRange(ctx, absPath,
|
|
22437
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
|
|
22438
|
+
rememberReadRange(ctx, absPath, stat18.mtimeMs, total, total + 1, total + 1);
|
|
21992
22439
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
21993
22440
|
return {
|
|
21994
22441
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
@@ -22003,8 +22450,8 @@ var readTool = {
|
|
|
22003
22450
|
const truncated = offset - 1 + slice.length < total;
|
|
22004
22451
|
const width = String(offset + slice.length - 1).length;
|
|
22005
22452
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
22006
|
-
ctx.recordRead(absPath,
|
|
22007
|
-
rememberReadRange(ctx, absPath,
|
|
22453
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
|
|
22454
|
+
rememberReadRange(ctx, absPath, stat18.mtimeMs, total, offset, offset + slice.length - 1);
|
|
22008
22455
|
const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22009
22456
|
return {
|
|
22010
22457
|
text: numbered,
|
|
@@ -22110,20 +22557,20 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
22110
22557
|
}
|
|
22111
22558
|
|
|
22112
22559
|
// src/replace.ts
|
|
22113
|
-
import { expectDefined as expectDefined8 } from "@wrongstack/core/utils";
|
|
22114
22560
|
import { spawn as spawn15 } from "node:child_process";
|
|
22115
22561
|
import * as fs29 from "node:fs/promises";
|
|
22116
22562
|
import * as path33 from "node:path";
|
|
22563
|
+
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
22117
22564
|
import {
|
|
22118
22565
|
atomicWrite as atomicWrite3,
|
|
22119
22566
|
buildChildEnv as buildChildEnv9,
|
|
22120
22567
|
compileGlob as compileGlob4,
|
|
22121
22568
|
detectNewlineStyle as detectNewlineStyle2,
|
|
22569
|
+
expectDefined as expectDefined8,
|
|
22122
22570
|
normalizeToLf as normalizeToLf2,
|
|
22123
22571
|
toStyle as toStyle2,
|
|
22124
22572
|
unifiedDiff as unifiedDiff2
|
|
22125
22573
|
} from "@wrongstack/core/utils";
|
|
22126
|
-
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
22127
22574
|
init_util();
|
|
22128
22575
|
var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
22129
22576
|
var replaceTool = {
|
|
@@ -22132,6 +22579,10 @@ var replaceTool = {
|
|
|
22132
22579
|
description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool. Dry-run is ON by default \u2014 set `dry_run: false` to apply changes.",
|
|
22133
22580
|
usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
|
|
22134
22581
|
permission: "confirm",
|
|
22582
|
+
// WS-046: gives permission decisions something to key on.
|
|
22583
|
+
// The file scope being rewritten, not the pattern: a trust rule should say
|
|
22584
|
+
// where a bulk replace may run.
|
|
22585
|
+
subjectKey: "files",
|
|
22135
22586
|
mutating: true,
|
|
22136
22587
|
capabilities: ["fs.write"],
|
|
22137
22588
|
icon: "edit",
|
|
@@ -22204,8 +22655,8 @@ var replaceTool = {
|
|
|
22204
22655
|
}
|
|
22205
22656
|
const rel = path33.relative(realRoot, realPath);
|
|
22206
22657
|
if (rel.startsWith("..") || path33.isAbsolute(rel)) continue;
|
|
22207
|
-
const
|
|
22208
|
-
if (!
|
|
22658
|
+
const stat18 = await fs29.stat(realPath).catch(() => null);
|
|
22659
|
+
if (!stat18?.isFile()) continue;
|
|
22209
22660
|
let content;
|
|
22210
22661
|
try {
|
|
22211
22662
|
const buf = await fs29.readFile(realPath);
|
|
@@ -22230,7 +22681,7 @@ var replaceTool = {
|
|
|
22230
22681
|
totalReplacements += count;
|
|
22231
22682
|
if (!dryRun) {
|
|
22232
22683
|
const newContent = toStyle2(newContentLf, style);
|
|
22233
|
-
await atomicWrite3(realPath, newContent, { mode:
|
|
22684
|
+
await atomicWrite3(realPath, newContent, { mode: stat18.mode & 511 });
|
|
22234
22685
|
const written = await fs29.stat(realPath).catch(() => null);
|
|
22235
22686
|
if (written) {
|
|
22236
22687
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
@@ -22270,8 +22721,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
22270
22721
|
const resolved = [];
|
|
22271
22722
|
for (const p of parts) {
|
|
22272
22723
|
const absPath = safeResolve(p, ctx);
|
|
22273
|
-
const
|
|
22274
|
-
if (
|
|
22724
|
+
const stat18 = await fs29.stat(absPath).catch(() => null);
|
|
22725
|
+
if (stat18?.isFile()) {
|
|
22275
22726
|
resolved.push(absPath);
|
|
22276
22727
|
}
|
|
22277
22728
|
}
|
|
@@ -22291,7 +22742,11 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
22291
22742
|
function checkRg() {
|
|
22292
22743
|
return new Promise((resolve16) => {
|
|
22293
22744
|
try {
|
|
22294
|
-
const p = spawn15("rg", ["--version"], {
|
|
22745
|
+
const p = spawn15("rg", ["--version"], {
|
|
22746
|
+
env: buildChildEnv9(),
|
|
22747
|
+
stdio: "ignore",
|
|
22748
|
+
windowsHide: true
|
|
22749
|
+
});
|
|
22295
22750
|
p.on("error", () => resolve16(false));
|
|
22296
22751
|
p.on("close", (code) => resolve16(code === 0));
|
|
22297
22752
|
} catch {
|
|
@@ -22342,8 +22797,8 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
22342
22797
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
22343
22798
|
const full = path33.join(dir, e.name);
|
|
22344
22799
|
try {
|
|
22345
|
-
const
|
|
22346
|
-
if (
|
|
22800
|
+
const stat18 = await fs29.lstat(full);
|
|
22801
|
+
if (stat18.isSymbolicLink()) continue;
|
|
22347
22802
|
} catch {
|
|
22348
22803
|
continue;
|
|
22349
22804
|
}
|
|
@@ -22467,6 +22922,7 @@ var scaffoldTool = {
|
|
|
22467
22922
|
usageHint: "PREFERRED FOR SCAFFOLDING:\n\n- Use built-in templates when they match your needs (e.g. react-component, npm-package).\n- Supports `dry_run` so you can preview exactly what will be created.\n- Has the powerful `fs.write.outside-project` capability \u2014 review paths carefully.\nMuch cleaner and safer than manually writing multiple files.",
|
|
22468
22923
|
permission: "confirm",
|
|
22469
22924
|
mutating: true,
|
|
22925
|
+
subjectKey: "name",
|
|
22470
22926
|
capabilities: ["fs.write.outside-project", "fs.write"],
|
|
22471
22927
|
icon: "scaffold",
|
|
22472
22928
|
timeoutMs: 3e4,
|
|
@@ -22541,15 +22997,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
22541
22997
|
output: dryRun ? `Would create ${filesCreated} files: ${files.join(", ")}` : `Created ${filesCreated} files: ${files.join(", ")}`
|
|
22542
22998
|
};
|
|
22543
22999
|
}
|
|
23000
|
+
function escapeRegexLiteral(value) {
|
|
23001
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23002
|
+
}
|
|
22544
23003
|
function substituteVars(content, name, vars) {
|
|
23004
|
+
const kebab = name.toLowerCase().replace(/\s+/g, "-");
|
|
23005
|
+
const pascal = name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase());
|
|
22545
23006
|
let result = content;
|
|
22546
|
-
result = result.replace(/\{\{name\}\}/g,
|
|
22547
|
-
result = result.replace(
|
|
22548
|
-
/\{\{Name\}\}/g,
|
|
22549
|
-
name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase())
|
|
22550
|
-
);
|
|
23007
|
+
result = result.replace(/\{\{name\}\}/g, () => kebab);
|
|
23008
|
+
result = result.replace(/\{\{Name\}\}/g, () => pascal);
|
|
22551
23009
|
for (const [k, v] of Object.entries(vars)) {
|
|
22552
|
-
result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
|
|
23010
|
+
result = result.replace(new RegExp(`\\{\\{${escapeRegexLiteral(k)}\\}\\}`, "g"), () => v);
|
|
22553
23011
|
}
|
|
22554
23012
|
return result;
|
|
22555
23013
|
}
|
|
@@ -23485,11 +23943,11 @@ var testTool = {
|
|
|
23485
23943
|
}
|
|
23486
23944
|
};
|
|
23487
23945
|
async function detectRunner(cwd) {
|
|
23488
|
-
const { stat:
|
|
23946
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
23489
23947
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
23490
23948
|
for (const f of candidates) {
|
|
23491
23949
|
try {
|
|
23492
|
-
await
|
|
23950
|
+
await stat18(path35.join(cwd, f));
|
|
23493
23951
|
if (f.includes("vitest")) return "vitest";
|
|
23494
23952
|
if (f.includes("jest")) return "jest";
|
|
23495
23953
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -23886,12 +24344,19 @@ var toolSearchTool = {
|
|
|
23886
24344
|
};
|
|
23887
24345
|
|
|
23888
24346
|
// src/tool-use.ts
|
|
24347
|
+
import {
|
|
24348
|
+
GOVERNED_TOOL_EXECUTOR_META_KEY as GOVERNED_TOOL_EXECUTOR_META_KEY2
|
|
24349
|
+
} from "@wrongstack/core/types";
|
|
23889
24350
|
var toolUseTool = {
|
|
23890
24351
|
name: "tool_use",
|
|
23891
24352
|
category: "Meta",
|
|
23892
24353
|
description: "Directly execute any registered tool by its exact name, bypassing normal discovery. This is a powerful meta-tool intended for cases where the agent has a clear plan and knows precisely which tool to invoke.",
|
|
23893
24354
|
usageHint: "ADVANCED META TOOL \u2014 USE WITH CARE:\n\n- Only use when you are certain of the exact tool name and its expected input shape.\n- Prefer using the normal tool calling mechanism when possible.\n- Very useful in batch-tool-use or when orchestrating complex workflows programmatically.\n- The call still goes through full permission checks and capability validation.",
|
|
23894
24355
|
permission: "confirm",
|
|
24356
|
+
// WS-046: gives permission decisions something to key on.
|
|
24357
|
+
// The tool being invoked through this indirection — the whole point of a
|
|
24358
|
+
// permission decision here is WHICH tool is being reached.
|
|
24359
|
+
subjectKey: "tool",
|
|
23895
24360
|
mutating: true,
|
|
23896
24361
|
timeoutMs: 6e4,
|
|
23897
24362
|
capabilities: ["tool.mutate.any"],
|
|
@@ -23910,7 +24375,7 @@ var toolUseTool = {
|
|
|
23910
24375
|
},
|
|
23911
24376
|
required: ["tool"]
|
|
23912
24377
|
},
|
|
23913
|
-
async execute(input, ctx
|
|
24378
|
+
async execute(input, ctx) {
|
|
23914
24379
|
const start = Date.now();
|
|
23915
24380
|
if (!input?.tool) {
|
|
23916
24381
|
return {
|
|
@@ -23929,6 +24394,14 @@ var toolUseTool = {
|
|
|
23929
24394
|
executionMs: Date.now() - start
|
|
23930
24395
|
};
|
|
23931
24396
|
}
|
|
24397
|
+
if (tool.name === toolUseTool.name) {
|
|
24398
|
+
return {
|
|
24399
|
+
tool: input.tool,
|
|
24400
|
+
success: false,
|
|
24401
|
+
error: "tool_use: recursive meta-tool execution is not allowed",
|
|
24402
|
+
executionMs: Date.now() - start
|
|
24403
|
+
};
|
|
24404
|
+
}
|
|
23932
24405
|
if (tool.permission === "deny") {
|
|
23933
24406
|
return {
|
|
23934
24407
|
tool: input.tool,
|
|
@@ -23937,12 +24410,21 @@ var toolUseTool = {
|
|
|
23937
24410
|
executionMs: Date.now() - start
|
|
23938
24411
|
};
|
|
23939
24412
|
}
|
|
24413
|
+
const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY2];
|
|
24414
|
+
if (typeof governedExecute !== "function") {
|
|
24415
|
+
return {
|
|
24416
|
+
tool: input.tool,
|
|
24417
|
+
success: false,
|
|
24418
|
+
error: "tool_use: governed nested execution is unavailable; call the tool directly",
|
|
24419
|
+
executionMs: Date.now() - start
|
|
24420
|
+
};
|
|
24421
|
+
}
|
|
23940
24422
|
try {
|
|
23941
|
-
const result = await
|
|
24423
|
+
const result = await governedExecute(input.tool, input.input ?? {});
|
|
23942
24424
|
return {
|
|
23943
24425
|
tool: input.tool,
|
|
23944
|
-
success:
|
|
23945
|
-
result,
|
|
24426
|
+
success: result.success,
|
|
24427
|
+
...result.success ? { result: result.result } : { error: result.error ?? "nested tool failed" },
|
|
23946
24428
|
executionMs: Date.now() - start
|
|
23947
24429
|
};
|
|
23948
24430
|
} catch (e) {
|
|
@@ -23958,10 +24440,12 @@ var toolUseTool = {
|
|
|
23958
24440
|
|
|
23959
24441
|
// src/tree.ts
|
|
23960
24442
|
init_util();
|
|
23961
|
-
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
23962
24443
|
import * as fs32 from "node:fs/promises";
|
|
23963
24444
|
import * as path36 from "node:path";
|
|
24445
|
+
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
23964
24446
|
var DEFAULT_IGNORE5 = [...DEFAULT_WALK_IGNORE_DIRS4, ".wrongstack", ".ssh", ".gnupg", ".aws"];
|
|
24447
|
+
var DEFAULT_MAX_ENTRIES2 = 5e3;
|
|
24448
|
+
var MAX_TREE_OUTPUT_BYTES = 256 * 1024;
|
|
23965
24449
|
var treeTool = {
|
|
23966
24450
|
name: "tree",
|
|
23967
24451
|
category: "Filesystem",
|
|
@@ -24005,6 +24489,12 @@ var treeTool = {
|
|
|
24005
24489
|
show_hidden: {
|
|
24006
24490
|
type: "boolean",
|
|
24007
24491
|
description: "Show hidden files starting with . (default: false)"
|
|
24492
|
+
},
|
|
24493
|
+
max_entries: {
|
|
24494
|
+
type: "integer",
|
|
24495
|
+
description: `Maximum entries retained in the rendered tree (default ${DEFAULT_MAX_ENTRIES2}).`,
|
|
24496
|
+
minimum: 1,
|
|
24497
|
+
maximum: 5e4
|
|
24008
24498
|
}
|
|
24009
24499
|
}
|
|
24010
24500
|
},
|
|
@@ -24018,7 +24508,7 @@ var treeTool = {
|
|
|
24018
24508
|
if (!final) throw new Error("tree: stream ended without final event");
|
|
24019
24509
|
return final;
|
|
24020
24510
|
},
|
|
24021
|
-
async *executeStream(input, ctx) {
|
|
24511
|
+
async *executeStream(input, ctx, opts) {
|
|
24022
24512
|
const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
|
|
24023
24513
|
const maxDepth = input.depth ?? 3;
|
|
24024
24514
|
const showFiles = input.show_files ?? true;
|
|
@@ -24026,8 +24516,17 @@ var treeTool = {
|
|
|
24026
24516
|
const showHidden = input.show_hidden ?? false;
|
|
24027
24517
|
const exclude = /* @__PURE__ */ new Set([...DEFAULT_IGNORE5, ...input.exclude ?? []]);
|
|
24028
24518
|
const filterGlob = input.glob;
|
|
24519
|
+
const maxEntries = input.max_entries ?? DEFAULT_MAX_ENTRIES2;
|
|
24520
|
+
const signal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
|
|
24029
24521
|
const lines = [basePath];
|
|
24030
24522
|
const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };
|
|
24523
|
+
const retention = {
|
|
24524
|
+
entries: 0,
|
|
24525
|
+
outputBytes: Buffer.byteLength(basePath, "utf8"),
|
|
24526
|
+
maxEntries,
|
|
24527
|
+
maxOutputBytes: MAX_TREE_OUTPUT_BYTES,
|
|
24528
|
+
truncated: false
|
|
24529
|
+
};
|
|
24031
24530
|
const queue = [];
|
|
24032
24531
|
const FLUSH_EVERY = 200;
|
|
24033
24532
|
let lastEmittedTotal = 0;
|
|
@@ -24054,12 +24553,19 @@ var treeTool = {
|
|
|
24054
24553
|
isLast: true,
|
|
24055
24554
|
totalFiles: totals.totalFiles,
|
|
24056
24555
|
totalDirs: totals.totalDirs,
|
|
24057
|
-
onProgress: tickProgress
|
|
24556
|
+
onProgress: tickProgress,
|
|
24557
|
+
retention,
|
|
24558
|
+
signal
|
|
24058
24559
|
});
|
|
24059
24560
|
let walkDone = false;
|
|
24060
|
-
walkPromise.
|
|
24061
|
-
|
|
24062
|
-
|
|
24561
|
+
void walkPromise.then(
|
|
24562
|
+
() => {
|
|
24563
|
+
walkDone = true;
|
|
24564
|
+
},
|
|
24565
|
+
() => {
|
|
24566
|
+
walkDone = true;
|
|
24567
|
+
}
|
|
24568
|
+
);
|
|
24063
24569
|
while (!walkDone || queue.length > 0) {
|
|
24064
24570
|
if (queue.length > 0) {
|
|
24065
24571
|
yield expectDefined10(queue.shift());
|
|
@@ -24082,13 +24588,15 @@ var treeTool = {
|
|
|
24082
24588
|
tree: lines.join("\n"),
|
|
24083
24589
|
total_files: totals.totalFiles.value,
|
|
24084
24590
|
total_dirs: totals.totalDirs.value,
|
|
24085
|
-
truncated:
|
|
24591
|
+
truncated: retention.truncated,
|
|
24086
24592
|
path: basePath
|
|
24087
24593
|
}
|
|
24088
24594
|
};
|
|
24089
24595
|
}
|
|
24090
24596
|
};
|
|
24091
24597
|
async function walkDir(dir, depth, opts) {
|
|
24598
|
+
opts.signal.throwIfAborted();
|
|
24599
|
+
if (opts.retention.truncated) return;
|
|
24092
24600
|
const entries = await fs32.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
24093
24601
|
const filtered = entries.filter((e) => {
|
|
24094
24602
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
@@ -24108,6 +24616,8 @@ async function walkDir(dir, depth, opts) {
|
|
|
24108
24616
|
return a.name.localeCompare(b.name);
|
|
24109
24617
|
});
|
|
24110
24618
|
for (let i = 0; i < items.length; i++) {
|
|
24619
|
+
opts.signal.throwIfAborted();
|
|
24620
|
+
if (opts.retention.truncated) return;
|
|
24111
24621
|
const entry = items[i];
|
|
24112
24622
|
if (!entry) continue;
|
|
24113
24623
|
const isLast = i === items.length - 1;
|
|
@@ -24116,7 +24626,15 @@ async function walkDir(dir, depth, opts) {
|
|
|
24116
24626
|
const displayName = entry.name + (entry.isDirectory() ? "/" : "");
|
|
24117
24627
|
if (!opts.showDirs && entry.isDirectory()) continue;
|
|
24118
24628
|
if (!opts.showFiles && entry.isFile()) continue;
|
|
24119
|
-
opts.
|
|
24629
|
+
const line = opts.prefix + branch + displayName;
|
|
24630
|
+
const lineBytes = Buffer.byteLength(line, "utf8") + 1;
|
|
24631
|
+
if (opts.retention.entries >= opts.retention.maxEntries || opts.retention.outputBytes + lineBytes > opts.retention.maxOutputBytes) {
|
|
24632
|
+
opts.retention.truncated = true;
|
|
24633
|
+
return;
|
|
24634
|
+
}
|
|
24635
|
+
opts.lines.push(line);
|
|
24636
|
+
opts.retention.entries++;
|
|
24637
|
+
opts.retention.outputBytes += lineBytes;
|
|
24120
24638
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
24121
24639
|
const childPrefix = opts.prefix + connector;
|
|
24122
24640
|
await walkDir(path36.join(dir, entry.name), depth + 1, {
|
|
@@ -24231,11 +24749,11 @@ var typecheckTool = {
|
|
|
24231
24749
|
}
|
|
24232
24750
|
};
|
|
24233
24751
|
async function findTsConfig(cwd) {
|
|
24234
|
-
const { stat:
|
|
24752
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
24235
24753
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
24236
24754
|
for (const f of candidates) {
|
|
24237
24755
|
try {
|
|
24238
|
-
const s = await
|
|
24756
|
+
const s = await stat18(path37.join(cwd, f));
|
|
24239
24757
|
if (s.isFile()) return path37.join(cwd, f);
|
|
24240
24758
|
} catch {
|
|
24241
24759
|
}
|
|
@@ -24310,12 +24828,12 @@ async function prepareWrite(input, ctx) {
|
|
|
24310
24828
|
let existed = false;
|
|
24311
24829
|
let prev = "";
|
|
24312
24830
|
try {
|
|
24313
|
-
const
|
|
24314
|
-
existed =
|
|
24831
|
+
const stat18 = await fs33.stat(absPath);
|
|
24832
|
+
existed = stat18.isFile();
|
|
24315
24833
|
if (existed) {
|
|
24316
24834
|
if (!ctx.hasRead(absPath)) {
|
|
24317
24835
|
prev = await fs33.readFile(absPath, "utf8");
|
|
24318
|
-
ctx.recordRead(absPath,
|
|
24836
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "write", sha256hex(prev));
|
|
24319
24837
|
} else {
|
|
24320
24838
|
prev = await fs33.readFile(absPath, "utf8");
|
|
24321
24839
|
}
|
|
@@ -24332,8 +24850,8 @@ async function finishWrite(input, ctx, prepared, signal) {
|
|
|
24332
24850
|
await atomicWrite5(prepared.absPath, input.content);
|
|
24333
24851
|
const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
24334
24852
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
24335
|
-
const
|
|
24336
|
-
ctx.recordRead(prepared.absPath,
|
|
24853
|
+
const stat18 = await fs33.stat(prepared.absPath);
|
|
24854
|
+
ctx.recordRead(prepared.absPath, stat18.mtimeMs, "write", sha256hex(input.content));
|
|
24337
24855
|
ctx.session.recordFileChange({
|
|
24338
24856
|
path: prepared.absPath,
|
|
24339
24857
|
action: prepared.existed ? "modified" : "created",
|