@wrongstack/tools 0.289.0 → 0.291.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/auto-proceed-loop-guard.d.ts +128 -0
- package/dist/auto-proceed-loop-guard.d.ts.map +1 -0
- package/dist/auto-proceed-loop-guard.js +46 -0
- package/dist/auto-proceed-loop-guard.js.map +7 -0
- package/dist/bash-kill-guard.d.ts +10 -1
- package/dist/bash-kill-guard.d.ts.map +1 -1
- package/dist/bash.js +140 -14
- package/dist/bash.js.map +2 -2
- package/dist/builtin.js +662 -203
- package/dist/builtin.js.map +4 -4
- package/dist/codebase-index/background-indexer.d.ts +1 -1
- package/dist/codebase-index/background-indexer.d.ts.map +1 -1
- package/dist/codebase-index/index.js +181 -104
- package/dist/codebase-index/index.js.map +3 -3
- package/dist/codebase-index/indexer.d.ts.map +1 -1
- package/dist/codebase-index/refs-extractor.d.ts +2 -17
- package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
- package/dist/codebase-index/ts-parser.d.ts.map +1 -1
- package/dist/codebase-index/worker.js +164 -96
- package/dist/codebase-index/worker.js.map +3 -3
- package/dist/codebase-index/writer.d.ts +33 -0
- package/dist/codebase-index/writer.d.ts.map +1 -1
- package/dist/exec-kill-guard.d.ts +29 -0
- package/dist/exec-kill-guard.d.ts.map +1 -0
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +670 -10
- package/dist/exec.js.map +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +765 -237
- package/dist/index.js.map +4 -4
- package/dist/kanban.d.ts +10 -0
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +34 -17
- package/dist/kanban.js.map +2 -2
- package/dist/pack.js +662 -203
- package/dist/pack.js.map +4 -4
- package/dist/plan.js.map +2 -2
- package/dist/read.d.ts.map +1 -1
- package/dist/read.js.map +2 -2
- package/dist/session-kanban.d.ts.map +1 -1
- package/dist/session-kanban.js +60 -3
- package/dist/session-kanban.js.map +2 -2
- package/dist/task.js.map +2 -2
- package/dist/todo.js.map +2 -2
- package/package.json +9 -5
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-proceed loop guard.
|
|
3
|
+
*
|
|
4
|
+
* ## Problem
|
|
5
|
+
*
|
|
6
|
+
* The REPL and TUI both implement an "auto" autonomy mode that, after every
|
|
7
|
+
* completed agent turn, feeds the next suggestion (or top auto="true" item)
|
|
8
|
+
* back as a fresh prompt. When the agent emits the same `<nextsteps>` block
|
|
9
|
+
* on every reply — which happens whenever the model's output is stable but
|
|
10
|
+
* autonomy treats the next step as "still actionable" — the loop self-feeds
|
|
11
|
+
* the same instruction 2–3 times in a row before the longer
|
|
12
|
+
* `autoProceedMaxIterations` cap (default 50) trips.
|
|
13
|
+
*
|
|
14
|
+
* The user reports the visible symptom: the same response, including the same
|
|
15
|
+
* `<nextsteps>` block, repeats. Manual input from the user is the only thing
|
|
16
|
+
* that breaks out. They suspect the next-steps mechanism is the source.
|
|
17
|
+
*
|
|
18
|
+
* The 50-iteration cap is intentionally loose — it is a runaway safety net,
|
|
19
|
+
* not a UX signal. By the time it trips, the user has already watched the
|
|
20
|
+
* same response 50 times.
|
|
21
|
+
*
|
|
22
|
+
* ## Approach
|
|
23
|
+
*
|
|
24
|
+
* A small, pure, browser-safe stateful helper that records the last few
|
|
25
|
+
* prompts fed through the auto-proceed / auto-submit path. It normalizes
|
|
26
|
+
* prompts (whitespace, casing) so trivial rewordings are not collapsed, but
|
|
27
|
+
* identical re-feeds are. When the same normalized prompt is fed
|
|
28
|
+
* `repeatThreshold` (default 2) times in a row, the guard returns
|
|
29
|
+
* `{ shouldHalt: true }`.
|
|
30
|
+
*
|
|
31
|
+
* Callers are expected to:
|
|
32
|
+
* 1. Call `record(prompt)` immediately before each auto-feed.
|
|
33
|
+
* 2. If `shouldHalt` is true, clear the suggestion + auto-suggestion store,
|
|
34
|
+
* cancel any pending countdown, and surface a "we detected a loop"
|
|
35
|
+
* message asking the user what's happening. Do NOT feed the prompt.
|
|
36
|
+
* 3. Call `reset()` whenever the user types anything manually (REPL manual
|
|
37
|
+
* input or any user-driven submit) so the next auto-feed starts clean.
|
|
38
|
+
*
|
|
39
|
+
* The default `repeatThreshold` is 2 — i.e. the second consecutive identical
|
|
40
|
+
* feed (the first re-feed of an already-seen prompt) halts the loop. That
|
|
41
|
+
* matches the user's report ("receiving the same prompt 2-3 times in a row
|
|
42
|
+
* causes the system to enter a loop"). Setting it to 2 catches the loop on
|
|
43
|
+
* the first identical re-feed rather than after the 50-iteration runaway cap trips.
|
|
44
|
+
*
|
|
45
|
+
* The window size is intentionally small (default 3). The loop we care about
|
|
46
|
+
* is the immediate repetition — a longer history would misfire on legitimate
|
|
47
|
+
* "do X, then do Y, then do X" sequences the model occasionally returns to.
|
|
48
|
+
*
|
|
49
|
+
* This module is BROWSER-SAFE — no Node-only imports — so it can be imported
|
|
50
|
+
* from Vite-bundled WebUI as well as Node-based CLI/TUI.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Normalize a prompt for repetition comparison. Whitespace is collapsed and
|
|
54
|
+
* trimmed; case is folded to lowercase. Nothing semantic is removed, so two
|
|
55
|
+
* prompts that differ in any meaningful way (extra word, different code,
|
|
56
|
+
* different file path) compare as distinct. Two prompts that differ only by
|
|
57
|
+
* leading/trailing whitespace and casing are treated as identical.
|
|
58
|
+
*/
|
|
59
|
+
export declare function normalizeForRepetition(prompt: string): string;
|
|
60
|
+
export interface LoopGuardOptions {
|
|
61
|
+
/**
|
|
62
|
+
* How many consecutive identical prompts (after normalization) trigger a
|
|
63
|
+
* halt. Default 2 — i.e. the second consecutive identical feed (the first
|
|
64
|
+
* re-feed of an already-seen prompt) halts the loop. Clamped to >= 2.
|
|
65
|
+
*/
|
|
66
|
+
repeatThreshold?: number;
|
|
67
|
+
/**
|
|
68
|
+
* How many of the most recent feeds to retain for comparison. The window
|
|
69
|
+
* is searched from newest to oldest; the guard cares about the run of
|
|
70
|
+
* identical entries ending at the most recent one, not about identical
|
|
71
|
+
* entries anywhere in history. Default 3.
|
|
72
|
+
*/
|
|
73
|
+
windowSize?: number;
|
|
74
|
+
}
|
|
75
|
+
export interface RepetitionSignal {
|
|
76
|
+
/** The normalized prompt that just got recorded. */
|
|
77
|
+
normalized: string;
|
|
78
|
+
/**
|
|
79
|
+
* Number of consecutive identical feeds ending at the most recent feed,
|
|
80
|
+
* including this one. Always >= 1.
|
|
81
|
+
*/
|
|
82
|
+
runLength: number;
|
|
83
|
+
/**
|
|
84
|
+
* True when the run length has crossed `repeatThreshold`. Callers must
|
|
85
|
+
* stop feeding and surface a user prompt instead.
|
|
86
|
+
*/
|
|
87
|
+
shouldHalt: boolean;
|
|
88
|
+
}
|
|
89
|
+
export interface AutoProceedLoopGuard {
|
|
90
|
+
/**
|
|
91
|
+
* Record a prompt that is about to be auto-fed. Returns the repetition
|
|
92
|
+
* signal for that prompt. When `shouldHalt` is true the caller MUST NOT
|
|
93
|
+
* feed the prompt — it has been recorded for post-mortem inspection, but
|
|
94
|
+
* the loop must be broken instead.
|
|
95
|
+
*/
|
|
96
|
+
record(prompt: string): RepetitionSignal;
|
|
97
|
+
/**
|
|
98
|
+
* Drop the history. Call this on any manual user input so a fresh run
|
|
99
|
+
* starts with no memory of the prior cycle.
|
|
100
|
+
*/
|
|
101
|
+
reset(): void;
|
|
102
|
+
/**
|
|
103
|
+
* Read-only view of the most recent normalized prompts (newest last).
|
|
104
|
+
* Useful for diagnostics and for the message shown to the user when the
|
|
105
|
+
* guard halts the loop.
|
|
106
|
+
*/
|
|
107
|
+
history(): readonly string[];
|
|
108
|
+
/**
|
|
109
|
+
* How many entries the guard is currently retaining.
|
|
110
|
+
*/
|
|
111
|
+
size(): number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build a fresh loop guard. Defaults: `repeatThreshold = 2`, `windowSize = 3`.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* const guard = createAutoProceedLoopGuard();
|
|
118
|
+
* for (const prompt of candidates) {
|
|
119
|
+
* const signal = guard.record(prompt);
|
|
120
|
+
* if (signal.shouldHalt) {
|
|
121
|
+
* haltAutoProceedAndAskUser(signal);
|
|
122
|
+
* break;
|
|
123
|
+
* }
|
|
124
|
+
* feedPrompt(prompt);
|
|
125
|
+
* }
|
|
126
|
+
*/
|
|
127
|
+
export declare function createAutoProceedLoopGuard(options?: LoopGuardOptions): AutoProceedLoopGuard;
|
|
128
|
+
//# sourceMappingURL=auto-proceed-loop-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-proceed-loop-guard.d.ts","sourceRoot":"","sources":["../src/auto-proceed-loop-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACzC;;;OAGG;IACH,KAAK,IAAI,IAAI,CAAC;IACd;;;;OAIG;IACH,OAAO,IAAI,SAAS,MAAM,EAAE,CAAC;IAC7B;;OAEG;IACH,IAAI,IAAI,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,gBAAqB,GAAG,oBAAoB,CAmD/F"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/auto-proceed-loop-guard.ts
|
|
2
|
+
function normalizeForRepetition(prompt) {
|
|
3
|
+
return prompt.replace(/\s+/g, " ").trim().toLowerCase();
|
|
4
|
+
}
|
|
5
|
+
function createAutoProceedLoopGuard(options = {}) {
|
|
6
|
+
const safeIntegerOption = (value, fallback) => {
|
|
7
|
+
if (value === void 0) return fallback;
|
|
8
|
+
if (!Number.isFinite(value)) return fallback;
|
|
9
|
+
return Math.floor(value);
|
|
10
|
+
};
|
|
11
|
+
const repeatThreshold = Math.max(2, safeIntegerOption(options.repeatThreshold, 2));
|
|
12
|
+
const windowSize = Math.max(repeatThreshold, safeIntegerOption(options.windowSize, 3));
|
|
13
|
+
let buffer = [];
|
|
14
|
+
function record(prompt) {
|
|
15
|
+
const normalized = normalizeForRepetition(prompt);
|
|
16
|
+
buffer.push(normalized);
|
|
17
|
+
if (buffer.length > windowSize) {
|
|
18
|
+
buffer = buffer.slice(buffer.length - windowSize);
|
|
19
|
+
}
|
|
20
|
+
let runLength = 0;
|
|
21
|
+
for (let i = buffer.length - 1; i >= 0; i--) {
|
|
22
|
+
if (buffer[i] === normalized) runLength++;
|
|
23
|
+
else break;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
normalized,
|
|
27
|
+
runLength,
|
|
28
|
+
shouldHalt: runLength >= repeatThreshold
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function reset() {
|
|
32
|
+
buffer = [];
|
|
33
|
+
}
|
|
34
|
+
function history() {
|
|
35
|
+
return buffer.slice();
|
|
36
|
+
}
|
|
37
|
+
function size() {
|
|
38
|
+
return buffer.length;
|
|
39
|
+
}
|
|
40
|
+
return { record, reset, history, size };
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
createAutoProceedLoopGuard,
|
|
44
|
+
normalizeForRepetition
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=auto-proceed-loop-guard.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/auto-proceed-loop-guard.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Auto-proceed loop guard.\n *\n * ## Problem\n *\n * The REPL and TUI both implement an \"auto\" autonomy mode that, after every\n * completed agent turn, feeds the next suggestion (or top auto=\"true\" item)\n * back as a fresh prompt. When the agent emits the same `<nextsteps>` block\n * on every reply \u2014 which happens whenever the model's output is stable but\n * autonomy treats the next step as \"still actionable\" \u2014 the loop self-feeds\n * the same instruction 2\u20133 times in a row before the longer\n * `autoProceedMaxIterations` cap (default 50) trips.\n *\n * The user reports the visible symptom: the same response, including the same\n * `<nextsteps>` block, repeats. Manual input from the user is the only thing\n * that breaks out. They suspect the next-steps mechanism is the source.\n *\n * The 50-iteration cap is intentionally loose \u2014 it is a runaway safety net,\n * not a UX signal. By the time it trips, the user has already watched the\n * same response 50 times.\n *\n * ## Approach\n *\n * A small, pure, browser-safe stateful helper that records the last few\n * prompts fed through the auto-proceed / auto-submit path. It normalizes\n * prompts (whitespace, casing) so trivial rewordings are not collapsed, but\n * identical re-feeds are. When the same normalized prompt is fed\n * `repeatThreshold` (default 2) times in a row, the guard returns\n * `{ shouldHalt: true }`.\n *\n * Callers are expected to:\n * 1. Call `record(prompt)` immediately before each auto-feed.\n * 2. If `shouldHalt` is true, clear the suggestion + auto-suggestion store,\n * cancel any pending countdown, and surface a \"we detected a loop\"\n * message asking the user what's happening. Do NOT feed the prompt.\n * 3. Call `reset()` whenever the user types anything manually (REPL manual\n * input or any user-driven submit) so the next auto-feed starts clean.\n *\n * The default `repeatThreshold` is 2 \u2014 i.e. the second consecutive identical\n * feed (the first re-feed of an already-seen prompt) halts the loop. That\n * matches the user's report (\"receiving the same prompt 2-3 times in a row\n * causes the system to enter a loop\"). Setting it to 2 catches the loop on\n * the first identical re-feed rather than after the 50-iteration runaway cap trips.\n *\n * The window size is intentionally small (default 3). The loop we care about\n * is the immediate repetition \u2014 a longer history would misfire on legitimate\n * \"do X, then do Y, then do X\" sequences the model occasionally returns to.\n *\n * This module is BROWSER-SAFE \u2014 no Node-only imports \u2014 so it can be imported\n * from Vite-bundled WebUI as well as Node-based CLI/TUI.\n */\n\n/**\n * Normalize a prompt for repetition comparison. Whitespace is collapsed and\n * trimmed; case is folded to lowercase. Nothing semantic is removed, so two\n * prompts that differ in any meaningful way (extra word, different code,\n * different file path) compare as distinct. Two prompts that differ only by\n * leading/trailing whitespace and casing are treated as identical.\n */\nexport function normalizeForRepetition(prompt: string): string {\n return prompt.replace(/\\s+/g, ' ').trim().toLowerCase();\n}\n\nexport interface LoopGuardOptions {\n /**\n * How many consecutive identical prompts (after normalization) trigger a\n * halt. Default 2 \u2014 i.e. the second consecutive identical feed (the first\n * re-feed of an already-seen prompt) halts the loop. Clamped to >= 2.\n */\n repeatThreshold?: number;\n /**\n * How many of the most recent feeds to retain for comparison. The window\n * is searched from newest to oldest; the guard cares about the run of\n * identical entries ending at the most recent one, not about identical\n * entries anywhere in history. Default 3.\n */\n windowSize?: number;\n}\n\nexport interface RepetitionSignal {\n /** The normalized prompt that just got recorded. */\n normalized: string;\n /**\n * Number of consecutive identical feeds ending at the most recent feed,\n * including this one. Always >= 1.\n */\n runLength: number;\n /**\n * True when the run length has crossed `repeatThreshold`. Callers must\n * stop feeding and surface a user prompt instead.\n */\n shouldHalt: boolean;\n}\n\nexport interface AutoProceedLoopGuard {\n /**\n * Record a prompt that is about to be auto-fed. Returns the repetition\n * signal for that prompt. When `shouldHalt` is true the caller MUST NOT\n * feed the prompt \u2014 it has been recorded for post-mortem inspection, but\n * the loop must be broken instead.\n */\n record(prompt: string): RepetitionSignal;\n /**\n * Drop the history. Call this on any manual user input so a fresh run\n * starts with no memory of the prior cycle.\n */\n reset(): void;\n /**\n * Read-only view of the most recent normalized prompts (newest last).\n * Useful for diagnostics and for the message shown to the user when the\n * guard halts the loop.\n */\n history(): readonly string[];\n /**\n * How many entries the guard is currently retaining.\n */\n size(): number;\n}\n\n/**\n * Build a fresh loop guard. Defaults: `repeatThreshold = 2`, `windowSize = 3`.\n *\n * @example\n * const guard = createAutoProceedLoopGuard();\n * for (const prompt of candidates) {\n * const signal = guard.record(prompt);\n * if (signal.shouldHalt) {\n * haltAutoProceedAndAskUser(signal);\n * break;\n * }\n * feedPrompt(prompt);\n * }\n */\nexport function createAutoProceedLoopGuard(options: LoopGuardOptions = {}): AutoProceedLoopGuard {\n // Validate options defensively. Non-finite values (NaN, Infinity) MUST\n // fall back to the documented default \u2014 a guard that silently disables\n // itself is worse than no guard at all. Specifically:\n // - `repeatThreshold: NaN` would cause `Math.floor(NaN) = NaN`,\n // `Math.max(2, NaN) = NaN`, and `runLength >= NaN` is always false,\n // making the guard permanent no-op.\n // - `windowSize: NaN` / `Infinity` would skip the trim branch entirely\n // (`buffer.length > NaN` and `buffer.length > Infinity` are both\n // false), so the buffer would grow without bound.\n const safeIntegerOption = (value: number | undefined, fallback: number): number => {\n if (value === undefined) return fallback;\n if (!Number.isFinite(value)) return fallback;\n return Math.floor(value);\n };\n const repeatThreshold = Math.max(2, safeIntegerOption(options.repeatThreshold, 2));\n const windowSize = Math.max(repeatThreshold, safeIntegerOption(options.windowSize, 3));\n let buffer: string[] = [];\n\n function record(prompt: string): RepetitionSignal {\n const normalized = normalizeForRepetition(prompt);\n buffer.push(normalized);\n if (buffer.length > windowSize) {\n buffer = buffer.slice(buffer.length - windowSize);\n }\n // Count the trailing run of identical normalized prompts.\n let runLength = 0;\n for (let i = buffer.length - 1; i >= 0; i--) {\n if (buffer[i] === normalized) runLength++;\n else break;\n }\n return {\n normalized,\n runLength,\n shouldHalt: runLength >= repeatThreshold,\n };\n }\n\n function reset(): void {\n buffer = [];\n }\n\n function history(): readonly string[] {\n return buffer.slice();\n }\n\n function size(): number {\n return buffer.length;\n }\n\n return { record, reset, history, size };\n}\n"],
|
|
5
|
+
"mappings": ";AA2DO,SAAS,uBAAuB,QAAwB;AAC7D,SAAO,OAAO,QAAQ,QAAQ,GAAG,EAAE,KAAK,EAAE,YAAY;AACxD;AAwEO,SAAS,2BAA2B,UAA4B,CAAC,GAAyB;AAU/F,QAAM,oBAAoB,CAAC,OAA2B,aAA6B;AACjF,QAAI,UAAU,OAAW,QAAO;AAChC,QAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,WAAO,KAAK,MAAM,KAAK;AAAA,EACzB;AACA,QAAM,kBAAkB,KAAK,IAAI,GAAG,kBAAkB,QAAQ,iBAAiB,CAAC,CAAC;AACjF,QAAM,aAAa,KAAK,IAAI,iBAAiB,kBAAkB,QAAQ,YAAY,CAAC,CAAC;AACrF,MAAI,SAAmB,CAAC;AAExB,WAAS,OAAO,QAAkC;AAChD,UAAM,aAAa,uBAAuB,MAAM;AAChD,WAAO,KAAK,UAAU;AACtB,QAAI,OAAO,SAAS,YAAY;AAC9B,eAAS,OAAO,MAAM,OAAO,SAAS,UAAU;AAAA,IAClD;AAEA,QAAI,YAAY;AAChB,aAAS,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;AAC3C,UAAI,OAAO,CAAC,MAAM,WAAY;AAAA,UACzB;AAAA,IACP;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,YAAY,aAAa;AAAA,IAC3B;AAAA,EACF;AAEA,WAAS,QAAc;AACrB,aAAS,CAAC;AAAA,EACZ;AAEA,WAAS,UAA6B;AACpC,WAAO,OAAO,MAAM;AAAA,EACtB;AAEA,WAAS,OAAe;AACtB,WAAO,OAAO;AAAA,EAChB;AAEA,SAAO,EAAE,QAAQ,OAAO,SAAS,KAAK;AACxC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -11,7 +11,14 @@
|
|
|
11
11
|
* - Shell -c wrapped: bash -c "kill -9 12345" (any shell path — see P2 #10)
|
|
12
12
|
* - Full path kills: /bin/kill -9 12345
|
|
13
13
|
* - Name-based kills: pkill, killall, pgrep
|
|
14
|
-
* - Windows equivalents: taskkill
|
|
14
|
+
* - Windows equivalents: taskkill, tskill
|
|
15
|
+
* - PowerShell Stop-Process / kill alias: Stop-Process -Name node, kill -Id 12345
|
|
16
|
+
* - WMIC process termination: wmic process where "name='node.exe'" delete
|
|
17
|
+
* - Script-based kill (script is named kill*.sh, kill*.ps1, kill*.bat)
|
|
18
|
+
*
|
|
19
|
+
* Security contract: every "Handles" bullet must map to both a detector
|
|
20
|
+
* AND a block path in isKillRelatedCommand + parseKillCommand + isKillProtected.
|
|
21
|
+
* Script-based kills are blocked conservatively (can't inspect script content).
|
|
15
22
|
*
|
|
16
23
|
* Known bypasses (NOT handled — this is a static regex parser, not a shell):
|
|
17
24
|
* Static analysis of shell strings is inherently defeatable by obfuscation.
|
|
@@ -25,6 +32,8 @@
|
|
|
25
32
|
* - String concatenation / quote-splitting: `ki''ll -9 12345`, `k"i"ll 12345`
|
|
26
33
|
* - Aliases and functions: `alias x=kill; x -9 12345`
|
|
27
34
|
* - eval / source: `eval "ki""ll -9 12345"`
|
|
35
|
+
* - node -e eval: `node -e "process.kill(12345)"` (handled by exec-kill-guard.ts)
|
|
36
|
+
* - Scripts not named kill/terminate/stop*: `runkill.sh`, `/tmp/cleanup.bat`
|
|
28
37
|
*
|
|
29
38
|
* Mitigation: rely on the permission policy (confirm/deny gate) and YOLO
|
|
30
39
|
* destructive detection as the primary controls; this guard is a best-effort
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bash-kill-guard.d.ts","sourceRoot":"","sources":["../src/bash-kill-guard.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"bash-kill-guard.d.ts","sourceRoot":"","sources":["../src/bash-kill-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAqBH,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA+ED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAkPpE;AAqBD;;GAEG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CA0CzE;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAqDxF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1E"}
|
package/dist/bash.js
CHANGED
|
@@ -1199,17 +1199,18 @@ function getPersistentProcessRegistry() {
|
|
|
1199
1199
|
|
|
1200
1200
|
// src/bash-kill-guard.ts
|
|
1201
1201
|
var isWin = os3.platform() === "win32";
|
|
1202
|
+
var SCRIPT_KILL_RE = /^(?:\.\\|\.\/)?(?:kill|terminate|stop)\S*\.(?:ps1|bat|cmd|sh)(?:\s|$)/i;
|
|
1203
|
+
var SCRIPT_KILL_RE_POSIX = /^(?:\.\/)?(?:kill|terminate|stop)\S*\.sh(?:\s|$)/i;
|
|
1204
|
+
var SCRIPT_KILL_FALLBACK_RE = /^\S*(?:kill|terminate|stop)\S*\.(?:ps1|bat|cmd|sh)\b/i;
|
|
1202
1205
|
function extractKillCommand(command) {
|
|
1203
1206
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1204
|
-
const shellCMatch = normalized.match(
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
if (shellCMatch?.[1]) {
|
|
1208
|
-
const inner = shellCMatch[1].trim();
|
|
1207
|
+
const shellCMatch = normalized.match(/^.+?\s+-c\s+(['"])([\s\S]+)\1$/);
|
|
1208
|
+
if (shellCMatch?.[2]) {
|
|
1209
|
+
const inner = shellCMatch[2].trim();
|
|
1209
1210
|
return isKillRelatedCommand(inner) ? inner : null;
|
|
1210
1211
|
}
|
|
1211
1212
|
const shellCUnquoted = normalized.match(
|
|
1212
|
-
|
|
1213
|
+
/^.+?\s+-c\s+(kill(?:\s+-s\s+[a-zA-Z0-9]+|\s+-[a-zA-Z0-9]+)?\s+\d+)$/
|
|
1213
1214
|
);
|
|
1214
1215
|
if (shellCUnquoted?.[1]) {
|
|
1215
1216
|
return shellCUnquoted[1];
|
|
@@ -1221,22 +1222,47 @@ function isKillRelatedCommand(cmd) {
|
|
|
1221
1222
|
if (isWin) {
|
|
1222
1223
|
if (/^taskkill\s/i.test(normalized)) return true;
|
|
1223
1224
|
if (/^tskill\s/i.test(normalized)) return true;
|
|
1225
|
+
if (/^(stop-process|kill|stop)\s/i.test(normalized)) return true;
|
|
1226
|
+
if (/^wmic\s+process\s/i.test(normalized) && /\bdelete\b/i.test(normalized)) return true;
|
|
1227
|
+
if (SCRIPT_KILL_RE.test(normalized)) return true;
|
|
1224
1228
|
return false;
|
|
1225
1229
|
}
|
|
1226
1230
|
if (/^kill(\s|$)/.test(normalized)) return true;
|
|
1227
1231
|
if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
|
|
1228
1232
|
if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
|
|
1233
|
+
if (SCRIPT_KILL_RE_POSIX.test(normalized)) return true;
|
|
1229
1234
|
return false;
|
|
1230
1235
|
}
|
|
1231
1236
|
function parseKillCommand(command) {
|
|
1232
1237
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1233
1238
|
if (isWin) {
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
|
|
1239
|
+
const hasTaskkillForce = /(?:^|\s)\/F(?=\s|$)/i.test(normalized);
|
|
1240
|
+
const isSimpleTaskkill = /^taskkill\s+/i.test(normalized) && !/[|&<>]/.test(normalized);
|
|
1241
|
+
const taskkillPidMatch = isSimpleTaskkill ? normalized.match(/(?:^|\s)\/PID\s+(\d+)(?=\s|$)/i) : null;
|
|
1242
|
+
if (taskkillPidMatch?.[1]) {
|
|
1243
|
+
return {
|
|
1244
|
+
pid: parseInt(taskkillPidMatch[1], 10),
|
|
1245
|
+
signal: hasTaskkillForce ? "FORCE" : "TERM",
|
|
1246
|
+
isGroupKill: false,
|
|
1247
|
+
isAllKill: false,
|
|
1248
|
+
originalCommand: command
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
const taskkillImMatch = isSimpleTaskkill ? normalized.match(/(?:^|\s)\/IM\s+([^\s/]+)(?=\s|$)/i) : null;
|
|
1252
|
+
if (taskkillImMatch?.[1]) {
|
|
1253
|
+
return {
|
|
1254
|
+
name: taskkillImMatch[1],
|
|
1255
|
+
signal: hasTaskkillForce ? "FORCE" : "TERM",
|
|
1256
|
+
isGroupKill: false,
|
|
1257
|
+
isAllKill: false,
|
|
1258
|
+
originalCommand: command
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const taskkillFiMatch = isSimpleTaskkill ? normalized.match(/(?:^|\s)\/FI\s+"IMAGENAME\s+eq\s+([^"]+)"(?=\s|$)/i) : null;
|
|
1262
|
+
if (taskkillFiMatch?.[1]) {
|
|
1237
1263
|
return {
|
|
1238
|
-
|
|
1239
|
-
signal:
|
|
1264
|
+
name: taskkillFiMatch[1],
|
|
1265
|
+
signal: hasTaskkillForce ? "FORCE" : "TERM",
|
|
1240
1266
|
isGroupKill: false,
|
|
1241
1267
|
isAllKill: false,
|
|
1242
1268
|
originalCommand: command
|
|
@@ -1244,18 +1270,109 @@ function parseKillCommand(command) {
|
|
|
1244
1270
|
}
|
|
1245
1271
|
const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
|
|
1246
1272
|
if (tskillMatch?.[1]) {
|
|
1247
|
-
const pidStr = tskillMatch[1];
|
|
1248
1273
|
return {
|
|
1249
|
-
pid: parseInt(
|
|
1274
|
+
pid: parseInt(tskillMatch[1], 10),
|
|
1250
1275
|
signal: "TERM",
|
|
1251
1276
|
isGroupKill: false,
|
|
1252
1277
|
isAllKill: false,
|
|
1253
1278
|
originalCommand: command
|
|
1254
1279
|
};
|
|
1255
1280
|
}
|
|
1281
|
+
const isStopProcIdCommand = /^(?:stop-process|kill)(?:\s+-(?:id|pid)\s+\d+|\s+-[a-zA-Z]+)+$/i.test(normalized);
|
|
1282
|
+
const stopProcIdMatch = normalized.match(/(?:^|\s)-(?:id|pid)\s+(\d+)(?=\s|$)/i);
|
|
1283
|
+
if (isStopProcIdCommand && stopProcIdMatch?.[1]) {
|
|
1284
|
+
return {
|
|
1285
|
+
pid: parseInt(stopProcIdMatch[1], 10),
|
|
1286
|
+
signal: "FORCE",
|
|
1287
|
+
isGroupKill: false,
|
|
1288
|
+
isAllKill: false,
|
|
1289
|
+
originalCommand: command
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
const killSignalOptionMatch = normalized.match(/^kill\s+-s\s+([a-zA-Z0-9]+)\s+(\d+)$/i);
|
|
1293
|
+
if (killSignalOptionMatch?.[1] && killSignalOptionMatch[2]) {
|
|
1294
|
+
return {
|
|
1295
|
+
pid: parseInt(killSignalOptionMatch[2], 10),
|
|
1296
|
+
signal: killSignalOptionMatch[1].toUpperCase(),
|
|
1297
|
+
isGroupKill: false,
|
|
1298
|
+
isAllKill: false,
|
|
1299
|
+
originalCommand: command
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
const killPosixMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z0-9]+)\s+)?(\d+)$/);
|
|
1303
|
+
if (killPosixMatch?.[2]) {
|
|
1304
|
+
const sig = killPosixMatch[1] ? killPosixMatch[1].slice(1).toUpperCase() : "TERM";
|
|
1305
|
+
return {
|
|
1306
|
+
pid: parseInt(killPosixMatch[2], 10),
|
|
1307
|
+
signal: sig,
|
|
1308
|
+
isGroupKill: false,
|
|
1309
|
+
isAllKill: false,
|
|
1310
|
+
originalCommand: command
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
const stopProcNameMatch = normalized.match(
|
|
1314
|
+
/^(?:stop-process|kill)\s+-(?:name|n)\s+(?:['"]([a-zA-Z0-9_.-]+)['"]|([a-zA-Z0-9_.-]+))(?:\s|$)/i
|
|
1315
|
+
);
|
|
1316
|
+
const stopProcName = stopProcNameMatch?.[1] ?? stopProcNameMatch?.[2];
|
|
1317
|
+
if (stopProcName) {
|
|
1318
|
+
return {
|
|
1319
|
+
name: stopProcName,
|
|
1320
|
+
signal: "FORCE",
|
|
1321
|
+
isGroupKill: false,
|
|
1322
|
+
isAllKill: false,
|
|
1323
|
+
originalCommand: command
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
const stopProcStandalone = normalized.match(
|
|
1327
|
+
/^(?:stop-process|kill)\s+['"]?([a-zA-Z][a-zA-Z0-9_.-]+)['"]?$/i
|
|
1328
|
+
);
|
|
1329
|
+
if (stopProcStandalone?.[1]) {
|
|
1330
|
+
return {
|
|
1331
|
+
name: stopProcStandalone[1],
|
|
1332
|
+
signal: "FORCE",
|
|
1333
|
+
isGroupKill: false,
|
|
1334
|
+
isAllKill: false,
|
|
1335
|
+
originalCommand: command
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
const wmicMatch = normalized.match(
|
|
1339
|
+
/^wmic\s+process\s+where\s+['"]?(?:name\s*=\s*['"]?)([a-zA-Z0-9_.-]+)/i
|
|
1340
|
+
);
|
|
1341
|
+
if (wmicMatch?.[1]) {
|
|
1342
|
+
return {
|
|
1343
|
+
name: wmicMatch[1],
|
|
1344
|
+
signal: "FORCE",
|
|
1345
|
+
isGroupKill: false,
|
|
1346
|
+
isAllKill: false,
|
|
1347
|
+
originalCommand: command
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
const killScriptMatch = normalized.match(SCRIPT_KILL_RE);
|
|
1351
|
+
if (killScriptMatch) {
|
|
1352
|
+
return {
|
|
1353
|
+
name: "kill-script",
|
|
1354
|
+
// sentinel — isKillProtected always blocks "kill-script"
|
|
1355
|
+
signal: "FORCE",
|
|
1356
|
+
isGroupKill: false,
|
|
1357
|
+
isAllKill: false,
|
|
1358
|
+
originalCommand: command
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1256
1361
|
return null;
|
|
1257
1362
|
}
|
|
1258
|
-
const
|
|
1363
|
+
const signalOptionMatch = normalized.match(/^kill\s+-s\s+([a-zA-Z0-9]+)\s+(\d+|-?\d+)$/i);
|
|
1364
|
+
if (signalOptionMatch?.[1] && signalOptionMatch[2]) {
|
|
1365
|
+
const pidOrGroup = signalOptionMatch[2];
|
|
1366
|
+
const isGroupKill = pidOrGroup.startsWith("-");
|
|
1367
|
+
return {
|
|
1368
|
+
pid: parseInt(isGroupKill ? pidOrGroup.slice(1) : pidOrGroup, 10),
|
|
1369
|
+
signal: signalOptionMatch[1].toUpperCase(),
|
|
1370
|
+
isGroupKill,
|
|
1371
|
+
isAllKill: false,
|
|
1372
|
+
originalCommand: command
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z0-9]+)\s+)?(\d+|-?\d+)$/);
|
|
1259
1376
|
if (simpleMatch) {
|
|
1260
1377
|
const signal = simpleMatch[1] ?? "-TERM";
|
|
1261
1378
|
const pidOrGroup = simpleMatch[2];
|
|
@@ -1315,6 +1432,9 @@ async function getProtectedEntries() {
|
|
|
1315
1432
|
}
|
|
1316
1433
|
async function isKillProtected(kill) {
|
|
1317
1434
|
const registry = getPersistentProcessRegistry();
|
|
1435
|
+
if (kill.name === "kill-script") {
|
|
1436
|
+
return true;
|
|
1437
|
+
}
|
|
1318
1438
|
if (kill.name) {
|
|
1319
1439
|
const entries = await getProtectedEntries();
|
|
1320
1440
|
const killNameLower = kill.name.toLowerCase();
|
|
@@ -1354,6 +1474,12 @@ async function checkAndBlockKillCommand(command) {
|
|
|
1354
1474
|
reason: `Blocked: complex kill pipeline detected \u2014 "${killCmd.slice(0, 50)}..."`
|
|
1355
1475
|
};
|
|
1356
1476
|
}
|
|
1477
|
+
if (SCRIPT_KILL_FALLBACK_RE.test(killCmd)) {
|
|
1478
|
+
return {
|
|
1479
|
+
blocked: true,
|
|
1480
|
+
reason: `Blocked: script-based kill detected \u2014 "${killCmd.slice(0, 80)}" may target protected WrongStack processes (cannot inspect script body).`
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1357
1483
|
return { blocked: false };
|
|
1358
1484
|
}
|
|
1359
1485
|
if (await isKillProtected(parsed)) {
|