@wrongstack/tools 0.5.6 → 0.6.0
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/bash.d.ts +2 -1
- package/dist/bash.js +361 -3
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +408 -12
- package/dist/builtin.js.map +1 -1
- package/dist/circuit-breaker.d.ts +111 -0
- package/dist/circuit-breaker.js +150 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/exec.js +355 -4
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js.map +1 -1
- package/dist/grep.js +9 -4
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +412 -13
- package/dist/index.js.map +1 -1
- package/dist/logs.js +9 -4
- package/dist/logs.js.map +1 -1
- package/dist/pack.js +408 -12
- package/dist/pack.js.map +1 -1
- package/dist/process-registry.d.ts +112 -0
- package/dist/process-registry.js +327 -0
- package/dist/process-registry.js.map +1 -0
- package/dist/replace.js +9 -4
- package/dist/replace.js.map +1 -1
- package/dist/scaffold.js +2 -1
- package/dist/scaffold.js.map +1 -1
- package/package.json +10 -2
package/dist/index.js
CHANGED
|
@@ -281,12 +281,17 @@ function findSimilarity(haystack, needle) {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
// src/_regex.ts
|
|
284
|
-
var MAX_PATTERN_LEN =
|
|
284
|
+
var MAX_PATTERN_LEN = 256;
|
|
285
285
|
var DANGEROUS_PATTERNS = [
|
|
286
|
-
/(\([^)]*[+*][^)]*\))[+*]/,
|
|
287
286
|
// (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
|
|
288
|
-
/(\(
|
|
289
|
-
|
|
287
|
+
/(\([^)]*[+*][^)]*\))[+*]/,
|
|
288
|
+
/(\(\?:[^)]*[+*][^)]*\))[+*]/,
|
|
289
|
+
// Adjacent quantifiers: a++ a*+
|
|
290
|
+
/[+*]{2,}/,
|
|
291
|
+
// Quantifier on alternation with length 2+
|
|
292
|
+
/\([^|)]+\|[^)]+\)[+*][+*]/,
|
|
293
|
+
// Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
|
|
294
|
+
/[\(\[][^)\]]*[+*][^)\]]*[\)\]][^)]*\?\??/
|
|
290
295
|
];
|
|
291
296
|
function compileUserRegex(pattern, flags) {
|
|
292
297
|
if (typeof pattern !== "string") {
|
|
@@ -854,6 +859,326 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
854
859
|
};
|
|
855
860
|
}
|
|
856
861
|
|
|
862
|
+
// src/circuit-breaker.ts
|
|
863
|
+
var DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;
|
|
864
|
+
var DEFAULT_SLOW_CALL_THRESHOLD_MS = 6e4;
|
|
865
|
+
var DEFAULT_MAX_SLOW_CALLS = 3;
|
|
866
|
+
var DEFAULT_WINDOW_MS = 6e4;
|
|
867
|
+
var DEFAULT_MAX_CALLS_PER_WINDOW = 30;
|
|
868
|
+
var DEFAULT_COOLDOWN_MS = 3e4;
|
|
869
|
+
var CircuitBreaker = class {
|
|
870
|
+
maxConsecutiveFailures;
|
|
871
|
+
slowCallThresholdMs;
|
|
872
|
+
maxSlowCalls;
|
|
873
|
+
windowMs;
|
|
874
|
+
maxCallsPerWindow;
|
|
875
|
+
cooldownMs;
|
|
876
|
+
state = "closed";
|
|
877
|
+
consecutiveFailures = 0;
|
|
878
|
+
window = [];
|
|
879
|
+
lastFailureAt = null;
|
|
880
|
+
lastSlowAt = null;
|
|
881
|
+
/** Timestamp when the breaker was opened (for cooldown calculation). */
|
|
882
|
+
openedAt = null;
|
|
883
|
+
/** Timestamp when the last call ran (for half-open gate). */
|
|
884
|
+
lastCallAt = null;
|
|
885
|
+
constructor(config = {}) {
|
|
886
|
+
this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
|
|
887
|
+
this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
|
|
888
|
+
this.maxSlowCalls = config.maxSlowCalls ?? DEFAULT_MAX_SLOW_CALLS;
|
|
889
|
+
this.windowMs = config.windowMs ?? DEFAULT_WINDOW_MS;
|
|
890
|
+
this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
|
|
891
|
+
this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Returns true if the circuit allows a new call to proceed.
|
|
895
|
+
* When false, callers should abort the tool call and return a
|
|
896
|
+
* circuit-breaker error instead of spawning a process.
|
|
897
|
+
*/
|
|
898
|
+
get canProceed() {
|
|
899
|
+
this._checkStateTransition();
|
|
900
|
+
return this.state !== "open";
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Snapshot of the current breaker state for observability (`/kill`).
|
|
904
|
+
*/
|
|
905
|
+
snapshot() {
|
|
906
|
+
this._checkStateTransition();
|
|
907
|
+
const now = Date.now();
|
|
908
|
+
let cooldownRemaining = null;
|
|
909
|
+
if (this.openedAt !== null && this.state === "open") {
|
|
910
|
+
const elapsed = now - this.openedAt;
|
|
911
|
+
cooldownRemaining = Math.max(0, this.cooldownMs - elapsed);
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
state: this.state,
|
|
915
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
916
|
+
slowCallsInWindow: this.window.filter((c) => c.slow).length,
|
|
917
|
+
callsInWindow: this.window.length,
|
|
918
|
+
windowMs: this.windowMs,
|
|
919
|
+
cooldownRemainingMs: cooldownRemaining,
|
|
920
|
+
lastFailureAt: this.lastFailureAt,
|
|
921
|
+
lastSlowAt: this.lastSlowAt
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Call this BEFORE spawning a bash/exec process.
|
|
926
|
+
* Returns true if the call is allowed; false if the breaker is open.
|
|
927
|
+
* When false, callers MUST NOT spawn a process.
|
|
928
|
+
*/
|
|
929
|
+
beforeCall() {
|
|
930
|
+
this._checkStateTransition();
|
|
931
|
+
if (this.state === "open") return false;
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Call this AFTER a bash/exec process finishes (success or failure).
|
|
936
|
+
* `durationMs` is the wall-clock time the process ran.
|
|
937
|
+
* `failed` is true when the process returned a non-zero exit code or
|
|
938
|
+
* threw an exception before spawning.
|
|
939
|
+
*/
|
|
940
|
+
afterCall(durationMs, failed) {
|
|
941
|
+
const now = Date.now();
|
|
942
|
+
this.lastCallAt = now;
|
|
943
|
+
if (this.state === "half-open") {
|
|
944
|
+
if (failed) {
|
|
945
|
+
this._trip();
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
this._reset();
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
this._pruneWindow(now);
|
|
952
|
+
const slow = durationMs >= this.slowCallThresholdMs;
|
|
953
|
+
this.window.push({ at: now, failed, slow });
|
|
954
|
+
if (failed) {
|
|
955
|
+
this.consecutiveFailures++;
|
|
956
|
+
this.lastFailureAt = now;
|
|
957
|
+
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
|
|
958
|
+
this._trip();
|
|
959
|
+
}
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
this.consecutiveFailures = 0;
|
|
963
|
+
if (slow) {
|
|
964
|
+
this.lastSlowAt = now;
|
|
965
|
+
const slowCount = this.window.filter((c) => c.slow).length;
|
|
966
|
+
if (slowCount >= this.maxSlowCalls) {
|
|
967
|
+
this._trip();
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const callCount = this.window.length;
|
|
971
|
+
if (callCount >= this.maxCallsPerWindow) {
|
|
972
|
+
this._trip();
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/** Force the breaker open. Used by /kill force and Ctrl+C. */
|
|
976
|
+
forceOpen() {
|
|
977
|
+
this._trip();
|
|
978
|
+
}
|
|
979
|
+
/** Force a reset to closed. Used by tests and /kill reset. */
|
|
980
|
+
forceReset() {
|
|
981
|
+
this._reset();
|
|
982
|
+
}
|
|
983
|
+
_trip() {
|
|
984
|
+
if (this.state === "open") return;
|
|
985
|
+
this.state = "open";
|
|
986
|
+
this.openedAt = Date.now();
|
|
987
|
+
}
|
|
988
|
+
_reset() {
|
|
989
|
+
this.state = "closed";
|
|
990
|
+
this.consecutiveFailures = 0;
|
|
991
|
+
this.window = [];
|
|
992
|
+
this.openedAt = null;
|
|
993
|
+
}
|
|
994
|
+
/** Transition from open → half-open when cooldown elapses. */
|
|
995
|
+
_checkStateTransition() {
|
|
996
|
+
if (this.state !== "open" || this.openedAt === null) return;
|
|
997
|
+
const elapsed = Date.now() - this.openedAt;
|
|
998
|
+
if (elapsed >= this.cooldownMs) {
|
|
999
|
+
this.state = "half-open";
|
|
1000
|
+
this.openedAt = null;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
_pruneWindow(now) {
|
|
1004
|
+
const cutoff = now - this.windowMs;
|
|
1005
|
+
this.window = this.window.filter((c) => c.at >= cutoff);
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
// src/process-registry.ts
|
|
1010
|
+
var DEFAULT_GRACE_MS = 2e3;
|
|
1011
|
+
var ProcessRegistryImpl = class {
|
|
1012
|
+
processes = /* @__PURE__ */ new Map();
|
|
1013
|
+
breaker;
|
|
1014
|
+
constructor(breakerConfig) {
|
|
1015
|
+
this.breaker = new CircuitBreaker(breakerConfig);
|
|
1016
|
+
}
|
|
1017
|
+
register(info) {
|
|
1018
|
+
this.processes.set(info.pid, { ...info, killed: false });
|
|
1019
|
+
}
|
|
1020
|
+
/** Unregister a process by PID. Called on 'close' / 'exit' events. */
|
|
1021
|
+
unregister(pid) {
|
|
1022
|
+
this.processes.delete(pid);
|
|
1023
|
+
}
|
|
1024
|
+
/** Get a single process by PID. */
|
|
1025
|
+
get(pid) {
|
|
1026
|
+
return this.processes.get(pid);
|
|
1027
|
+
}
|
|
1028
|
+
/** Get all tracked processes. */
|
|
1029
|
+
list() {
|
|
1030
|
+
return Array.from(this.processes.values());
|
|
1031
|
+
}
|
|
1032
|
+
/** Get processes filtered by name (e.g. 'bash', 'exec'). */
|
|
1033
|
+
byName(name) {
|
|
1034
|
+
return this.list().filter((p) => p.name === name);
|
|
1035
|
+
}
|
|
1036
|
+
/** Get processes filtered by session. */
|
|
1037
|
+
bySession(sessionId) {
|
|
1038
|
+
return this.list().filter((p) => p.sessionId === sessionId);
|
|
1039
|
+
}
|
|
1040
|
+
/** Count of active (non-killed) processes. */
|
|
1041
|
+
get activeCount() {
|
|
1042
|
+
let n = 0;
|
|
1043
|
+
for (const p of this.processes.values()) {
|
|
1044
|
+
if (!p.killed) n++;
|
|
1045
|
+
}
|
|
1046
|
+
return n;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Combined stats for observability — used by /ps and the TUI status bar.
|
|
1050
|
+
*/
|
|
1051
|
+
stats() {
|
|
1052
|
+
return {
|
|
1053
|
+
activeCount: this.activeCount,
|
|
1054
|
+
totalCount: this.processes.size,
|
|
1055
|
+
breaker: this.breaker.snapshot()
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Returns true if the circuit allows a new bash/exec call to proceed.
|
|
1060
|
+
* When false, callers MUST NOT spawn a process.
|
|
1061
|
+
*/
|
|
1062
|
+
get canProceed() {
|
|
1063
|
+
return this.breaker.canProceed;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Called before spawning a process. Returns true if allowed; false if
|
|
1067
|
+
* the circuit breaker is open.
|
|
1068
|
+
*/
|
|
1069
|
+
beforeCall() {
|
|
1070
|
+
return this.breaker.beforeCall();
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Called after a process finishes. `durationMs` is wall-clock time;
|
|
1074
|
+
* `failed` is true for non-zero exit codes.
|
|
1075
|
+
*/
|
|
1076
|
+
afterCall(durationMs, failed) {
|
|
1077
|
+
this.breaker.afterCall(durationMs, failed);
|
|
1078
|
+
}
|
|
1079
|
+
/** Force-open the circuit breaker (Ctrl+C, /kill force). */
|
|
1080
|
+
forceBreakerOpen() {
|
|
1081
|
+
this.breaker.forceOpen();
|
|
1082
|
+
}
|
|
1083
|
+
/** Force-reset the circuit breaker to closed (/kill reset). */
|
|
1084
|
+
forceBreakerReset() {
|
|
1085
|
+
this.breaker.forceReset();
|
|
1086
|
+
}
|
|
1087
|
+
/** Kill a single process by PID.
|
|
1088
|
+
*
|
|
1089
|
+
* On POSIX: sends SIGTERM to the *process group* (-pid) so that
|
|
1090
|
+
* runaway grandchild processes (`sleep 9999 & disown`) are also killed.
|
|
1091
|
+
* After `graceMs` a SIGKILL is sent if the process hasn't exited.
|
|
1092
|
+
*
|
|
1093
|
+
* On Windows: `child.kill()` maps to TerminateProcess — process groups
|
|
1094
|
+
* are not meaningfully supported. A second `force=true` call sends
|
|
1095
|
+
* SIGKILL (which maps to TerminateProcess again — the distinction is
|
|
1096
|
+
* in the exit code, not the signal).
|
|
1097
|
+
*
|
|
1098
|
+
* Returns true if the process was found and kill was attempted.
|
|
1099
|
+
*/
|
|
1100
|
+
kill(pid, opts = {}) {
|
|
1101
|
+
const p = this.processes.get(pid);
|
|
1102
|
+
if (!p) return false;
|
|
1103
|
+
if (p.killed) return true;
|
|
1104
|
+
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
1105
|
+
const isWin = os.platform() === "win32";
|
|
1106
|
+
if (isWin) {
|
|
1107
|
+
try {
|
|
1108
|
+
p.child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
p.killed = true;
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
if (force) {
|
|
1116
|
+
try {
|
|
1117
|
+
process.kill(-pid, "SIGKILL");
|
|
1118
|
+
} catch {
|
|
1119
|
+
p.child.kill("SIGKILL");
|
|
1120
|
+
}
|
|
1121
|
+
} else {
|
|
1122
|
+
try {
|
|
1123
|
+
process.kill(-pid, "SIGTERM");
|
|
1124
|
+
} catch {
|
|
1125
|
+
p.child.kill("SIGTERM");
|
|
1126
|
+
}
|
|
1127
|
+
const timer = setTimeout(() => {
|
|
1128
|
+
if (this.processes.has(pid) && !p.child.killed) {
|
|
1129
|
+
try {
|
|
1130
|
+
process.kill(-pid, "SIGKILL");
|
|
1131
|
+
} catch {
|
|
1132
|
+
try {
|
|
1133
|
+
p.child.kill("SIGKILL");
|
|
1134
|
+
} catch {
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}, graceMs);
|
|
1139
|
+
timer.unref?.();
|
|
1140
|
+
}
|
|
1141
|
+
} catch {
|
|
1142
|
+
}
|
|
1143
|
+
p.killed = true;
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Kill all tracked processes.
|
|
1148
|
+
* Returns the PIDs that were kill()ed.
|
|
1149
|
+
*/
|
|
1150
|
+
killAll(opts = {}) {
|
|
1151
|
+
const pids = Array.from(this.processes.keys());
|
|
1152
|
+
const killed = [];
|
|
1153
|
+
for (const pid of pids) {
|
|
1154
|
+
if (this.kill(pid, opts)) killed.push(pid);
|
|
1155
|
+
}
|
|
1156
|
+
return killed;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Kill all processes for a specific session.
|
|
1160
|
+
* Returns the PIDs that were kill()ed.
|
|
1161
|
+
*/
|
|
1162
|
+
killSession(sessionId, opts = {}) {
|
|
1163
|
+
const pids = this.bySession(sessionId).map((p) => p.pid);
|
|
1164
|
+
const killed = [];
|
|
1165
|
+
for (const pid of pids) {
|
|
1166
|
+
if (this.kill(pid, opts)) killed.push(pid);
|
|
1167
|
+
}
|
|
1168
|
+
return killed;
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
var _registry;
|
|
1172
|
+
function getProcessRegistry() {
|
|
1173
|
+
if (!_registry) {
|
|
1174
|
+
_registry = new ProcessRegistryImpl();
|
|
1175
|
+
}
|
|
1176
|
+
return _registry;
|
|
1177
|
+
}
|
|
1178
|
+
function _resetProcessRegistry() {
|
|
1179
|
+
_registry = void 0;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
857
1182
|
// src/bash.ts
|
|
858
1183
|
var MAX_OUTPUT = 32768;
|
|
859
1184
|
var DEFAULT_TIMEOUT = 3e4;
|
|
@@ -892,12 +1217,27 @@ var bashTool = {
|
|
|
892
1217
|
},
|
|
893
1218
|
async *executeStream(input, ctx, opts) {
|
|
894
1219
|
if (!input?.command) throw new Error("bash: command is required");
|
|
1220
|
+
const registry = getProcessRegistry();
|
|
1221
|
+
if (!registry.beforeCall()) {
|
|
1222
|
+
yield {
|
|
1223
|
+
type: "final",
|
|
1224
|
+
output: {
|
|
1225
|
+
output: "",
|
|
1226
|
+
exit_code: 1,
|
|
1227
|
+
timed_out: false,
|
|
1228
|
+
pid: null,
|
|
1229
|
+
error: "bash: circuit breaker open \u2014 too many consecutive failures or slow calls. Use /kill to inspect or /kill reset to recover."
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
895
1234
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT, 6e5));
|
|
896
1235
|
const isWin = os.platform() === "win32";
|
|
897
1236
|
const shell = isWin ? process.env["COMSPEC"] ?? "cmd.exe" : process.env["SHELL"] ?? "/bin/bash";
|
|
898
1237
|
const args = isWin ? ["/c", input.command] : ["-c", input.command];
|
|
899
1238
|
const env = buildChildEnv(ctx.session?.id);
|
|
900
1239
|
const detached = isWin ? !!input.background : true;
|
|
1240
|
+
const startedAt = Date.now();
|
|
901
1241
|
if (input.background) {
|
|
902
1242
|
let buf2 = "";
|
|
903
1243
|
let truncated = false;
|
|
@@ -908,7 +1248,18 @@ var bashTool = {
|
|
|
908
1248
|
detached: true,
|
|
909
1249
|
signal: opts.signal
|
|
910
1250
|
});
|
|
911
|
-
const
|
|
1251
|
+
const pid2 = child2.pid;
|
|
1252
|
+
if (typeof pid2 === "number") {
|
|
1253
|
+
registry.register({
|
|
1254
|
+
pid: pid2,
|
|
1255
|
+
name: "bash",
|
|
1256
|
+
command: input.command,
|
|
1257
|
+
startedAt: Date.now(),
|
|
1258
|
+
sessionId: ctx.session?.id,
|
|
1259
|
+
child: child2
|
|
1260
|
+
});
|
|
1261
|
+
child2.on("close", () => registry.unregister(pid2));
|
|
1262
|
+
}
|
|
912
1263
|
child2.stdout?.on("data", (chunk) => {
|
|
913
1264
|
if (!truncated) {
|
|
914
1265
|
const remain = MAX_OUTPUT - buf2.length;
|
|
@@ -928,15 +1279,16 @@ var bashTool = {
|
|
|
928
1279
|
}
|
|
929
1280
|
});
|
|
930
1281
|
child2.on("close", () => {
|
|
1282
|
+
registry.afterCall(Date.now() - startedAt, false);
|
|
931
1283
|
});
|
|
932
|
-
if (typeof
|
|
1284
|
+
if (typeof pid2 === "number") child2.unref();
|
|
933
1285
|
yield {
|
|
934
1286
|
type: "final",
|
|
935
1287
|
output: {
|
|
936
1288
|
output: truncated ? buf2.slice(0, MAX_OUTPUT) + "\u2026[truncated]" : buf2,
|
|
937
1289
|
exit_code: null,
|
|
938
1290
|
timed_out: false,
|
|
939
|
-
pid
|
|
1291
|
+
pid: pid2
|
|
940
1292
|
}
|
|
941
1293
|
};
|
|
942
1294
|
return;
|
|
@@ -948,6 +1300,17 @@ var bashTool = {
|
|
|
948
1300
|
detached,
|
|
949
1301
|
signal: opts.signal
|
|
950
1302
|
});
|
|
1303
|
+
const pid = child.pid;
|
|
1304
|
+
if (typeof pid === "number") {
|
|
1305
|
+
registry.register({
|
|
1306
|
+
pid,
|
|
1307
|
+
name: "bash",
|
|
1308
|
+
command: input.command,
|
|
1309
|
+
startedAt: Date.now(),
|
|
1310
|
+
sessionId: ctx.session?.id,
|
|
1311
|
+
child
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
951
1314
|
let buf = "";
|
|
952
1315
|
let pending = "";
|
|
953
1316
|
let timedOut = false;
|
|
@@ -1030,10 +1393,13 @@ var bashTool = {
|
|
|
1030
1393
|
});
|
|
1031
1394
|
child.on("error", (err) => {
|
|
1032
1395
|
for (const t of timers) clearTimeout(t);
|
|
1396
|
+
registry.afterCall(Date.now() - startedAt, true);
|
|
1033
1397
|
push({ kind: "error", err });
|
|
1034
1398
|
});
|
|
1035
1399
|
child.on("close", (code) => {
|
|
1036
1400
|
for (const t of timers) clearTimeout(t);
|
|
1401
|
+
if (typeof pid === "number") registry.unregister(pid);
|
|
1402
|
+
registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
|
|
1037
1403
|
push({ kind: "end", code });
|
|
1038
1404
|
});
|
|
1039
1405
|
try {
|
|
@@ -1124,14 +1490,21 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
1124
1490
|
// go run could execute arbitrary .go files; -ldflags could inject build-time code
|
|
1125
1491
|
go: [/^-ldflags$/],
|
|
1126
1492
|
// bun --preload is similar to node --require
|
|
1127
|
-
bun: [/^--preload$/],
|
|
1493
|
+
bun: [/^--preload$/, /^run$/, /^bunx$/, /^create$/, /^init$/],
|
|
1128
1494
|
// docker build/run can create containers with host access;
|
|
1129
1495
|
// only allow read-only commands (ps, images, version)
|
|
1130
1496
|
docker: [/^build$/, /^run$/, /^exec$/, /^push$/, /^pull$/],
|
|
1131
1497
|
// find -exec/-ok/-execdir execute arbitrary commands
|
|
1132
1498
|
find: [/^-exec$/, /^-exec;$/, /^-ok$/, /^-ok;$/, /^-execdir$/, /^-execdir;$/, /^-exec=/, /^-ok=/, /^-execdir=/],
|
|
1133
1499
|
// rm -rf / is catastrophic — block root and home targets
|
|
1134
|
-
rm: [/^\/$/, /^\/\*$/, /^~$/]
|
|
1500
|
+
rm: [/^\/$/, /^\/\*$/, /^~$/],
|
|
1501
|
+
// npm run/exec/create/pack/publish can execute arbitrary scripts or publish malware
|
|
1502
|
+
npm: [/^run$/, /^exec$/, /^create$/, /^init$/, /^pack$/, /^publish$/, /^deploy$/],
|
|
1503
|
+
// pnpm run/dlx/exec/create can execute arbitrary scripts
|
|
1504
|
+
pnpm: [/^run$/, /^dlx$/, /^exec$/, /^create$/, /^init$/, /^pack$/, /^publish$/, /^deploy$/],
|
|
1505
|
+
// npx should only be used for --version; any package name is a vector for
|
|
1506
|
+
// malicious package execution (typosquatting, dependency confusion)
|
|
1507
|
+
npx: [/^[^\s]+$/]
|
|
1135
1508
|
};
|
|
1136
1509
|
function validateArgs(cmd, args) {
|
|
1137
1510
|
const blocked = BLOCKED_ARG_PATTERNS[cmd];
|
|
@@ -1164,6 +1537,18 @@ var execTool = {
|
|
|
1164
1537
|
required: ["command"]
|
|
1165
1538
|
},
|
|
1166
1539
|
async execute(input, ctx, opts) {
|
|
1540
|
+
const registry = getProcessRegistry();
|
|
1541
|
+
if (!registry.canProceed) {
|
|
1542
|
+
return {
|
|
1543
|
+
command: input.command,
|
|
1544
|
+
args: input.args ?? [],
|
|
1545
|
+
stdout: "",
|
|
1546
|
+
stderr: "Circuit breaker is open \u2014 too many consecutive failures. Use /kill reset to recover.",
|
|
1547
|
+
exitCode: 1,
|
|
1548
|
+
truncated: false,
|
|
1549
|
+
allowed: false
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1167
1552
|
const cmd = input.command.trim();
|
|
1168
1553
|
if (!cmd)
|
|
1169
1554
|
return {
|
|
@@ -1223,15 +1608,23 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1223
1608
|
let stdout = "";
|
|
1224
1609
|
let stderr = "";
|
|
1225
1610
|
let killed = false;
|
|
1611
|
+
const startedAt = Date.now();
|
|
1226
1612
|
const child = spawn(cmd, args, {
|
|
1227
1613
|
cwd,
|
|
1228
1614
|
signal,
|
|
1229
1615
|
env: buildChildEnv(sessionId),
|
|
1230
1616
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1231
1617
|
});
|
|
1618
|
+
const registry = getProcessRegistry();
|
|
1619
|
+
const pid = child.pid;
|
|
1620
|
+
if (typeof pid === "number") {
|
|
1621
|
+
const fullCommand = `${cmd} ${args.join(" ")}`;
|
|
1622
|
+
registry.register({ pid, name: "exec", command: fullCommand, startedAt: Date.now(), sessionId, child });
|
|
1623
|
+
}
|
|
1232
1624
|
const timer = setTimeout(() => {
|
|
1233
1625
|
killed = true;
|
|
1234
|
-
|
|
1626
|
+
if (typeof pid === "number") registry.kill(pid);
|
|
1627
|
+
else child.kill("SIGTERM");
|
|
1235
1628
|
}, timeout);
|
|
1236
1629
|
child.stdout?.on("data", (chunk) => {
|
|
1237
1630
|
if (stdout.length < MAX_OUTPUT2) stdout += chunk.toString();
|
|
@@ -1241,18 +1634,24 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1241
1634
|
});
|
|
1242
1635
|
child.on("close", (code) => {
|
|
1243
1636
|
clearTimeout(timer);
|
|
1637
|
+
if (typeof pid === "number") registry.unregister(pid);
|
|
1638
|
+
const durationMs = Date.now() - startedAt;
|
|
1639
|
+
const exitCode = killed ? 124 : code ?? 1;
|
|
1640
|
+
registry.afterCall(durationMs, exitCode !== 0);
|
|
1244
1641
|
resolve5({
|
|
1245
1642
|
command: cmd,
|
|
1246
1643
|
args,
|
|
1247
1644
|
stdout: stdout.slice(0, MAX_OUTPUT2),
|
|
1248
1645
|
stderr: stderr.slice(0, MAX_OUTPUT2),
|
|
1249
|
-
exitCode
|
|
1646
|
+
exitCode,
|
|
1250
1647
|
truncated: stdout.length >= MAX_OUTPUT2 || stderr.length >= MAX_OUTPUT2,
|
|
1251
1648
|
allowed: true
|
|
1252
1649
|
});
|
|
1253
1650
|
});
|
|
1254
1651
|
child.on("error", (err) => {
|
|
1255
1652
|
clearTimeout(timer);
|
|
1653
|
+
if (typeof pid === "number") registry.unregister(pid);
|
|
1654
|
+
registry.afterCall(Date.now() - startedAt, true);
|
|
1256
1655
|
resolve5({
|
|
1257
1656
|
command: cmd,
|
|
1258
1657
|
args,
|
|
@@ -3958,7 +4357,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
3958
4357
|
const fullPath = target;
|
|
3959
4358
|
if (!dryRun) {
|
|
3960
4359
|
await fs4.mkdir(path.dirname(fullPath), { recursive: true });
|
|
3961
|
-
await
|
|
4360
|
+
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
3962
4361
|
}
|
|
3963
4362
|
files.push(resolvedPath);
|
|
3964
4363
|
filesCreated++;
|
|
@@ -4525,6 +4924,6 @@ var builtinToolsPack = {
|
|
|
4525
4924
|
tools: builtinTools
|
|
4526
4925
|
};
|
|
4527
4926
|
|
|
4528
|
-
export { auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, createModeTool, diffTool, documentTool, editTool, execTool, fetchTool, forgetTool, formatTool, gitTool, globTool, grepTool, installTool, jsonTool, lintTool, logsTool, outdatedTool, patchTool, planTool, readTool, rememberTool, replaceTool, scaffoldTool, searchTool, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
4927
|
+
export { CircuitBreaker, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, createModeTool, diffTool, documentTool, editTool, execTool, fetchTool, forgetTool, formatTool, getProcessRegistry, gitTool, globTool, grepTool, installTool, jsonTool, lintTool, logsTool, outdatedTool, patchTool, planTool, readTool, rememberTool, replaceTool, scaffoldTool, searchTool, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
4529
4928
|
//# sourceMappingURL=index.js.map
|
|
4530
4929
|
//# sourceMappingURL=index.js.map
|