@wrongstack/tools 0.274.0 → 0.275.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/audit.js +57 -19
- package/dist/audit.js.map +1 -1
- package/dist/bash.js +166 -83
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +503 -119
- package/dist/builtin.js.map +1 -1
- package/dist/circuit-breaker.js +1 -0
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/edit.js +1 -1
- package/dist/edit.js.map +1 -1
- package/dist/exec.js +136 -38
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js +9 -1
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +57 -19
- package/dist/format.js.map +1 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +514 -130
- package/dist/index.js.map +1 -1
- package/dist/install.js +65 -19
- package/dist/install.js.map +1 -1
- package/dist/lint.js +57 -19
- package/dist/lint.js.map +1 -1
- package/dist/pack.js +501 -118
- package/dist/pack.js.map +1 -1
- package/dist/process-registry.d.ts +30 -0
- package/dist/process-registry.js +57 -19
- package/dist/process-registry.js.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/test.js +57 -19
- package/dist/test.js.map +1 -1
- package/dist/typecheck.js +57 -19
- package/dist/typecheck.js.map +1 -1
- package/dist/write.js +2 -2
- package/dist/write.js.map +1 -1
- package/package.json +2 -2
package/dist/bash.js
CHANGED
|
@@ -338,6 +338,7 @@ var CircuitBreaker = class {
|
|
|
338
338
|
if (this.state === "open") return;
|
|
339
339
|
this.state = "open";
|
|
340
340
|
this.openedAt = Date.now();
|
|
341
|
+
this.window = [];
|
|
341
342
|
try {
|
|
342
343
|
this.onTrip?.();
|
|
343
344
|
} catch {
|
|
@@ -377,7 +378,7 @@ var SENSITIVE_FLAG_PATTERNS = [
|
|
|
377
378
|
/--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
|
|
378
379
|
// -f "value" style short flags
|
|
379
380
|
/(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
|
|
380
|
-
/(?<!\w)-
|
|
381
|
+
/(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
|
|
381
382
|
// env var–style secrets: TOKEN=x, API_KEY=y, etc.
|
|
382
383
|
/(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
|
|
383
384
|
// Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
|
|
@@ -438,12 +439,35 @@ var ProcessRegistryImpl = class {
|
|
|
438
439
|
register(info) {
|
|
439
440
|
this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
|
|
440
441
|
}
|
|
442
|
+
_isSafeSignalPid(pid) {
|
|
443
|
+
return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
|
|
444
|
+
}
|
|
445
|
+
_canSignalProcessGroup(p) {
|
|
446
|
+
return os2.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
|
|
447
|
+
}
|
|
448
|
+
_killChildDirect(p, signal) {
|
|
449
|
+
try {
|
|
450
|
+
p.child.kill(signal);
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
_killPosix(p, signal) {
|
|
455
|
+
if (this._canSignalProcessGroup(p)) {
|
|
456
|
+
try {
|
|
457
|
+
process.kill(-p.pid, signal);
|
|
458
|
+
return;
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
this._killChildDirect(p, signal);
|
|
463
|
+
}
|
|
441
464
|
/** Unregister a process by PID. Called on 'close' / 'exit' events. */
|
|
442
465
|
unregister(pid) {
|
|
443
466
|
this.processes.delete(pid);
|
|
444
467
|
}
|
|
445
468
|
/** Get a single process by PID. */
|
|
446
469
|
get(pid) {
|
|
470
|
+
this._pruneStale(pid);
|
|
447
471
|
return this.processes.get(pid);
|
|
448
472
|
}
|
|
449
473
|
/** Get all tracked processes. */
|
|
@@ -607,13 +631,14 @@ var ProcessRegistryImpl = class {
|
|
|
607
631
|
* Returns true if the process was found and kill was attempted.
|
|
608
632
|
*/
|
|
609
633
|
kill(pid, opts = {}) {
|
|
634
|
+
this._pruneStale(pid);
|
|
610
635
|
const p = this.processes.get(pid);
|
|
611
636
|
if (!p) return false;
|
|
612
637
|
if (p.killed) return true;
|
|
613
638
|
if (p.protected) return false;
|
|
614
639
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
615
|
-
const
|
|
616
|
-
if (
|
|
640
|
+
const isWin2 = os2.platform() === "win32";
|
|
641
|
+
if (isWin2) {
|
|
617
642
|
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
618
643
|
if (liveRealChild && killWin32Tree(pid)) {
|
|
619
644
|
const fallback = setTimeout(() => {
|
|
@@ -636,27 +661,12 @@ var ProcessRegistryImpl = class {
|
|
|
636
661
|
}
|
|
637
662
|
try {
|
|
638
663
|
if (force) {
|
|
639
|
-
|
|
640
|
-
process.kill(-pid, "SIGKILL");
|
|
641
|
-
} catch {
|
|
642
|
-
p.child.kill("SIGKILL");
|
|
643
|
-
}
|
|
664
|
+
this._killPosix(p, "SIGKILL");
|
|
644
665
|
} else {
|
|
645
|
-
|
|
646
|
-
process.kill(-pid, "SIGTERM");
|
|
647
|
-
} catch {
|
|
648
|
-
p.child.kill("SIGTERM");
|
|
649
|
-
}
|
|
666
|
+
this._killPosix(p, "SIGTERM");
|
|
650
667
|
const timer = setTimeout(() => {
|
|
651
668
|
if (this.processes.has(pid) && !p.child.killed) {
|
|
652
|
-
|
|
653
|
-
process.kill(-pid, "SIGKILL");
|
|
654
|
-
} catch {
|
|
655
|
-
try {
|
|
656
|
-
p.child.kill("SIGKILL");
|
|
657
|
-
} catch {
|
|
658
|
-
}
|
|
659
|
-
}
|
|
669
|
+
this._killPosix(p, "SIGKILL");
|
|
660
670
|
}
|
|
661
671
|
}, graceMs);
|
|
662
672
|
timer.unref?.();
|
|
@@ -691,6 +701,34 @@ var ProcessRegistryImpl = class {
|
|
|
691
701
|
}
|
|
692
702
|
return killed;
|
|
693
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Check whether a tracked process entry is stale — the child has exited
|
|
706
|
+
* (exitCode !== null) AND it's been in the registry long enough that the
|
|
707
|
+
* OS may have reused the PID for a new, unrelated process.
|
|
708
|
+
*
|
|
709
|
+
* P3 #24 (before-release.md): on POSIX, PIDs are reused after process
|
|
710
|
+
* exit. If a tracked process exits but its 'close' event hasn't fired yet
|
|
711
|
+
* (or was missed), the registry still holds the entry. A new process
|
|
712
|
+
* gets the same PID, and PID-based lookups (get, kill, shouldBlockKill)
|
|
713
|
+
* may incorrectly protect or target the wrong process.
|
|
714
|
+
*
|
|
715
|
+
* The 60s threshold is conservative — the OS typically waits much longer
|
|
716
|
+
* before reusing a PID, but we want to clean up before that becomes a risk.
|
|
717
|
+
*/
|
|
718
|
+
_isStaleEntry(entry) {
|
|
719
|
+
return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Remove a stale entry for a specific PID before any PID-based lookup.
|
|
723
|
+
* This prevents PID reuse from causing the registry to act on a dead
|
|
724
|
+
* process that has been replaced by a new one with the same PID.
|
|
725
|
+
*/
|
|
726
|
+
_pruneStale(pid) {
|
|
727
|
+
const entry = this.processes.get(pid);
|
|
728
|
+
if (entry && this._isStaleEntry(entry)) {
|
|
729
|
+
this.processes.delete(pid);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
694
732
|
};
|
|
695
733
|
var _registry;
|
|
696
734
|
function getProcessRegistry() {
|
|
@@ -1106,17 +1144,18 @@ function getPersistentProcessRegistry() {
|
|
|
1106
1144
|
}
|
|
1107
1145
|
|
|
1108
1146
|
// src/bash-kill-guard.ts
|
|
1147
|
+
var isWin = os2.platform() === "win32";
|
|
1109
1148
|
function extractKillCommand(command) {
|
|
1110
1149
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1111
1150
|
const shellCMatch = normalized.match(
|
|
1112
|
-
/^(
|
|
1151
|
+
/^(?:\S+(?:\s+\S+)?)?\s+-c\s+['"](.+?)['"]$/
|
|
1113
1152
|
);
|
|
1114
1153
|
if (shellCMatch?.[1]) {
|
|
1115
1154
|
const inner = shellCMatch[1].trim();
|
|
1116
1155
|
return isKillRelatedCommand(inner) ? inner : null;
|
|
1117
1156
|
}
|
|
1118
1157
|
const shellCUnquoted = normalized.match(
|
|
1119
|
-
/^(
|
|
1158
|
+
/^(?:\S+(?:\s+\S+)?)?\s+-c\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
|
|
1120
1159
|
);
|
|
1121
1160
|
if (shellCUnquoted?.[1]) {
|
|
1122
1161
|
return shellCUnquoted[1];
|
|
@@ -1125,15 +1164,43 @@ function extractKillCommand(command) {
|
|
|
1125
1164
|
}
|
|
1126
1165
|
function isKillRelatedCommand(cmd) {
|
|
1127
1166
|
const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1167
|
+
if (isWin) {
|
|
1168
|
+
if (/^taskkill\s/i.test(normalized)) return true;
|
|
1169
|
+
if (/^tskill\s/i.test(normalized)) return true;
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1128
1172
|
if (/^kill(\s|$)/.test(normalized)) return true;
|
|
1129
1173
|
if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
|
|
1130
|
-
if (/^taskkill\s/i.test(normalized)) return true;
|
|
1131
|
-
if (/^tskill\s/i.test(normalized)) return true;
|
|
1132
1174
|
if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
|
|
1133
1175
|
return false;
|
|
1134
1176
|
}
|
|
1135
1177
|
function parseKillCommand(command) {
|
|
1136
1178
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1179
|
+
if (isWin) {
|
|
1180
|
+
const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
|
|
1181
|
+
if (taskkillMatch?.[1]) {
|
|
1182
|
+
const pidStr = taskkillMatch[1];
|
|
1183
|
+
return {
|
|
1184
|
+
pid: parseInt(pidStr, 10),
|
|
1185
|
+
signal: normalized.includes("/F") ? "FORCE" : "TERM",
|
|
1186
|
+
isGroupKill: false,
|
|
1187
|
+
isAllKill: false,
|
|
1188
|
+
originalCommand: command
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
|
|
1192
|
+
if (tskillMatch?.[1]) {
|
|
1193
|
+
const pidStr = tskillMatch[1];
|
|
1194
|
+
return {
|
|
1195
|
+
pid: parseInt(pidStr, 10),
|
|
1196
|
+
signal: "TERM",
|
|
1197
|
+
isGroupKill: false,
|
|
1198
|
+
isAllKill: false,
|
|
1199
|
+
originalCommand: command
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1137
1204
|
const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
|
|
1138
1205
|
if (simpleMatch) {
|
|
1139
1206
|
const signal = simpleMatch[1] ?? "-TERM";
|
|
@@ -1177,28 +1244,6 @@ function parseKillCommand(command) {
|
|
|
1177
1244
|
if (pgrepMatch) {
|
|
1178
1245
|
return null;
|
|
1179
1246
|
}
|
|
1180
|
-
const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
|
|
1181
|
-
if (taskkillMatch?.[1]) {
|
|
1182
|
-
const pidStr = taskkillMatch[1];
|
|
1183
|
-
return {
|
|
1184
|
-
pid: parseInt(pidStr, 10),
|
|
1185
|
-
signal: normalized.includes("/F") ? "FORCE" : "TERM",
|
|
1186
|
-
isGroupKill: false,
|
|
1187
|
-
isAllKill: false,
|
|
1188
|
-
originalCommand: command
|
|
1189
|
-
};
|
|
1190
|
-
}
|
|
1191
|
-
const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
|
|
1192
|
-
if (tskillMatch?.[1]) {
|
|
1193
|
-
const pidStr = tskillMatch[1];
|
|
1194
|
-
return {
|
|
1195
|
-
pid: parseInt(pidStr, 10),
|
|
1196
|
-
signal: "TERM",
|
|
1197
|
-
isGroupKill: false,
|
|
1198
|
-
isAllKill: false,
|
|
1199
|
-
originalCommand: command
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
1247
|
return null;
|
|
1203
1248
|
}
|
|
1204
1249
|
async function getProtectedEntries() {
|
|
@@ -1275,7 +1320,7 @@ async function checkAndBlockKillCommand(command) {
|
|
|
1275
1320
|
}
|
|
1276
1321
|
return { blocked: false };
|
|
1277
1322
|
}
|
|
1278
|
-
function pickShell(
|
|
1323
|
+
function pickShell(platform4, command, env) {
|
|
1279
1324
|
const override = env.get("WRONGSTACK_SHELL")?.trim().toLowerCase();
|
|
1280
1325
|
if (override === "cmd" || override === "cmd.exe") return "cmd";
|
|
1281
1326
|
if (override === "powershell" || override === "powershell.exe") return "powershell";
|
|
@@ -1329,7 +1374,7 @@ function looksLikePowerShellExtended(command) {
|
|
|
1329
1374
|
}
|
|
1330
1375
|
function wrapPowerShellScript(command) {
|
|
1331
1376
|
const bootstrap = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$ConfirmPreference='None';$WhatIfPreference=$false";
|
|
1332
|
-
return
|
|
1377
|
+
return bootstrap + "\n$ErrorActionPreference='Stop'\n" + command + "\nif ($LASTEXITCODE -is [int]) { exit $LASTEXITCODE }";
|
|
1333
1378
|
}
|
|
1334
1379
|
var PS_VERB_RE = new RegExp(
|
|
1335
1380
|
// Boundaries: start-of-string, whitespace, `;`, `&`, `|`, `(`, `{`, `,`.
|
|
@@ -1509,10 +1554,10 @@ var bashTool = {
|
|
|
1509
1554
|
}));
|
|
1510
1555
|
}
|
|
1511
1556
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
|
|
1512
|
-
const
|
|
1557
|
+
const isWin2 = os2.platform() === "win32";
|
|
1513
1558
|
let plan;
|
|
1514
1559
|
let winShellKind;
|
|
1515
|
-
if (
|
|
1560
|
+
if (isWin2) {
|
|
1516
1561
|
const shell2 = pickShell("win32", input.command, {
|
|
1517
1562
|
get: (k) => process.env[k]
|
|
1518
1563
|
});
|
|
@@ -1541,7 +1586,7 @@ var bashTool = {
|
|
|
1541
1586
|
const shell = plan.bin;
|
|
1542
1587
|
const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
|
|
1543
1588
|
const env = buildChildEnv(ctx.session?.id);
|
|
1544
|
-
const detached = !
|
|
1589
|
+
const detached = !isWin2;
|
|
1545
1590
|
const startedAt = Date.now();
|
|
1546
1591
|
if (input.background) {
|
|
1547
1592
|
let buf2 = "";
|
|
@@ -1559,7 +1604,7 @@ var bashTool = {
|
|
|
1559
1604
|
// apply: the child gets a hidden console that grandchildren inherit.
|
|
1560
1605
|
// Windows children survive parent exit either way. POSIX keeps
|
|
1561
1606
|
// detached for the process-group kill semantics.
|
|
1562
|
-
detached: !
|
|
1607
|
+
detached: !isWin2,
|
|
1563
1608
|
windowsHide: true
|
|
1564
1609
|
});
|
|
1565
1610
|
if (plan.useStdin) {
|
|
@@ -1577,7 +1622,8 @@ var bashTool = {
|
|
|
1577
1622
|
command: redactCommand(input.command),
|
|
1578
1623
|
startedAt: Date.now(),
|
|
1579
1624
|
sessionId: ctx.session?.id,
|
|
1580
|
-
child: child2
|
|
1625
|
+
child: child2,
|
|
1626
|
+
processGroupLeader: detached && child2.pid === pid2
|
|
1581
1627
|
});
|
|
1582
1628
|
child2.on("close", () => registry.unregister(pid2));
|
|
1583
1629
|
}
|
|
@@ -1618,6 +1664,14 @@ var bashTool = {
|
|
|
1618
1664
|
pid: pid2
|
|
1619
1665
|
}
|
|
1620
1666
|
};
|
|
1667
|
+
ctx.recordSideEffect?.({
|
|
1668
|
+
toolUseId: `bash-bg-${Date.now()}`,
|
|
1669
|
+
toolName: "bash",
|
|
1670
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1671
|
+
input: { command: redactCommand(input.command), background: true },
|
|
1672
|
+
outcome: `launched (pid ${pid2 ?? "unknown"})`,
|
|
1673
|
+
risk: "shell"
|
|
1674
|
+
});
|
|
1621
1675
|
return;
|
|
1622
1676
|
}
|
|
1623
1677
|
const child = spawn(shell, args, {
|
|
@@ -1628,7 +1682,7 @@ var bashTool = {
|
|
|
1628
1682
|
stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
1629
1683
|
detached,
|
|
1630
1684
|
windowsHide: true,
|
|
1631
|
-
...
|
|
1685
|
+
...isWin2 ? {} : { signal: opts.signal }
|
|
1632
1686
|
});
|
|
1633
1687
|
if (plan.useStdin) {
|
|
1634
1688
|
try {
|
|
@@ -1645,7 +1699,8 @@ var bashTool = {
|
|
|
1645
1699
|
command: redactCommand(input.command),
|
|
1646
1700
|
startedAt: Date.now(),
|
|
1647
1701
|
sessionId: ctx.session?.id,
|
|
1648
|
-
child
|
|
1702
|
+
child,
|
|
1703
|
+
processGroupLeader: detached && child.pid === pid
|
|
1649
1704
|
});
|
|
1650
1705
|
}
|
|
1651
1706
|
let buf = "";
|
|
@@ -1654,7 +1709,7 @@ var bashTool = {
|
|
|
1654
1709
|
const timers = [];
|
|
1655
1710
|
const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
|
|
1656
1711
|
function killWithTimeout(child2, timeoutMs2) {
|
|
1657
|
-
if (
|
|
1712
|
+
if (isWin2) {
|
|
1658
1713
|
if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
|
|
1659
1714
|
const fallback = setTimeout(() => {
|
|
1660
1715
|
if (child2.exitCode === null) {
|
|
@@ -1674,34 +1729,22 @@ var bashTool = {
|
|
|
1674
1729
|
}
|
|
1675
1730
|
return;
|
|
1676
1731
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
} catch {
|
|
1682
|
-
child2.kill("SIGTERM");
|
|
1683
|
-
}
|
|
1684
|
-
} else {
|
|
1732
|
+
if (typeof child2.pid === "number") {
|
|
1733
|
+
registry.kill(child2.pid, { graceMs: timeoutMs2 });
|
|
1734
|
+
} else {
|
|
1735
|
+
try {
|
|
1685
1736
|
child2.kill("SIGTERM");
|
|
1737
|
+
} catch {
|
|
1686
1738
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
const killTimer = setTimeout(() => {
|
|
1690
|
-
try {
|
|
1691
|
-
if (typeof child2.pid === "number") {
|
|
1692
|
-
try {
|
|
1693
|
-
process.kill(-child2.pid, "SIGKILL");
|
|
1694
|
-
} catch {
|
|
1695
|
-
child2.kill("SIGKILL");
|
|
1696
|
-
}
|
|
1697
|
-
} else {
|
|
1739
|
+
const killTimer = setTimeout(() => {
|
|
1740
|
+
try {
|
|
1698
1741
|
child2.kill("SIGKILL");
|
|
1742
|
+
} catch {
|
|
1699
1743
|
}
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
killTimer.unref?.();
|
|
1744
|
+
}, timeoutMs2);
|
|
1745
|
+
timers.push(killTimer);
|
|
1746
|
+
killTimer.unref?.();
|
|
1747
|
+
}
|
|
1705
1748
|
}
|
|
1706
1749
|
const timer = setTimeout(() => {
|
|
1707
1750
|
timedOut = true;
|
|
@@ -1710,7 +1753,7 @@ var bashTool = {
|
|
|
1710
1753
|
timers.push(timer);
|
|
1711
1754
|
timer.unref?.();
|
|
1712
1755
|
const onAbort = () => killWithTimeout(child, 2e3);
|
|
1713
|
-
if (
|
|
1756
|
+
if (isWin2) {
|
|
1714
1757
|
if (opts.signal.aborted) onAbort();
|
|
1715
1758
|
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
1716
1759
|
}
|
|
@@ -1798,6 +1841,14 @@ ${hint}` : ""),
|
|
|
1798
1841
|
timed_out: timedOut
|
|
1799
1842
|
}
|
|
1800
1843
|
};
|
|
1844
|
+
ctx.recordSideEffect?.({
|
|
1845
|
+
toolUseId: `bash-${Date.now()}`,
|
|
1846
|
+
toolName: "bash",
|
|
1847
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1848
|
+
input: { command: redactCommand(input.command) },
|
|
1849
|
+
outcome: timedOut ? `timed out (exit ${c.code})` : `exit ${c.code}`,
|
|
1850
|
+
risk: "shell"
|
|
1851
|
+
});
|
|
1801
1852
|
return;
|
|
1802
1853
|
}
|
|
1803
1854
|
const now = Date.now();
|
|
@@ -1809,7 +1860,7 @@ ${hint}` : ""),
|
|
|
1809
1860
|
} finally {
|
|
1810
1861
|
for (const t of timers) clearTimeout(t);
|
|
1811
1862
|
spool.finalize();
|
|
1812
|
-
if (
|
|
1863
|
+
if (isWin2) opts.signal.removeEventListener("abort", onAbort);
|
|
1813
1864
|
child.stdout?.off("data", onData);
|
|
1814
1865
|
child.stderr?.off("data", onData);
|
|
1815
1866
|
child.stdout?.destroy();
|
|
@@ -1819,6 +1870,38 @@ ${hint}` : ""),
|
|
|
1819
1870
|
else killWithTimeout(child, 2e3);
|
|
1820
1871
|
}
|
|
1821
1872
|
}
|
|
1873
|
+
},
|
|
1874
|
+
/**
|
|
1875
|
+
* Tool-level teardown fired by `ToolExecutor.runToolCleanup()` when the
|
|
1876
|
+
* tool's run is aborted/timeout'd. The generator's `finally` block above
|
|
1877
|
+
* already force-kills the direct child, but that only runs if the
|
|
1878
|
+
* executor closes the async iterator (via `iter.return()`). When the
|
|
1879
|
+
* executor tears down without iterating — or a re-entrant abort races
|
|
1880
|
+
* with the generator — a bash-spawned process tree can survive in the
|
|
1881
|
+
* ProcessRegistry with `killed === false`, continuing to write files,
|
|
1882
|
+
* consume CPU, or hold inherited stdio pipes open for the rest of the
|
|
1883
|
+
* session.
|
|
1884
|
+
*
|
|
1885
|
+
* This is the defensive layer the executor calls via `tool.cleanup()`
|
|
1886
|
+
* (see `types/tool.ts`): kill every bash-owned process still tracked
|
|
1887
|
+
* for this session that hasn't exited yet. `registry.kill()` already
|
|
1888
|
+
* handles process-group / taskkill tree-kill and the SIGTERM→SIGKILL
|
|
1889
|
+
* grace window, so this just scopes the registry's existing kill path
|
|
1890
|
+
* to "this session's runaway bash children". Idempotent — a process
|
|
1891
|
+
* that already exited is skipped by `kill()` (it returns false), and a
|
|
1892
|
+
* `protected` infrastructure process (dev server the user intentionally
|
|
1893
|
+
* backgrounded) is left alone by design.
|
|
1894
|
+
*/
|
|
1895
|
+
async cleanup(_input, ctx) {
|
|
1896
|
+
const registry = getProcessRegistry();
|
|
1897
|
+
const sessionId = ctx.session?.id;
|
|
1898
|
+
if (!sessionId) return;
|
|
1899
|
+
for (const entry of registry.bySession(sessionId)) {
|
|
1900
|
+
if (entry.name !== "bash") continue;
|
|
1901
|
+
if (entry.child.exitCode !== null) continue;
|
|
1902
|
+
if (entry.protected) continue;
|
|
1903
|
+
registry.kill(entry.pid, { force: true });
|
|
1904
|
+
}
|
|
1822
1905
|
}
|
|
1823
1906
|
};
|
|
1824
1907
|
|