@wrongstack/tools 0.269.0 → 0.272.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.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
- import * as os from 'node:os';
2
+ import * as os2 from 'node:os';
3
3
  import * as Core from '@wrongstack/core';
4
4
  import { buildChildEnv, expectDefined, wstackGlobalRoot } from '@wrongstack/core';
5
5
  import { mkdirSync, createWriteStream } from 'node:fs';
6
- import * as fsp from 'node:fs/promises';
6
+ import * as fs from 'node:fs/promises';
7
7
  import * as path from 'node:path';
8
8
 
9
9
  // src/bash.ts
@@ -19,12 +19,12 @@ function sweepOldSpoolFiles(dir) {
19
19
  void (async () => {
20
20
  try {
21
21
  const now = Date.now();
22
- for (const name of await fsp.readdir(dir)) {
22
+ for (const name of await fs.readdir(dir)) {
23
23
  if (!name.endsWith(".log")) continue;
24
24
  const p = path.join(dir, name);
25
25
  try {
26
- const st = await fsp.stat(p);
27
- if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fsp.unlink(p);
26
+ const st = await fs.stat(p);
27
+ if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fs.unlink(p);
28
28
  } catch {
29
29
  }
30
30
  }
@@ -403,10 +403,13 @@ function redactCommand(cmd) {
403
403
  var DEFAULT_GRACE_MS = 2e3;
404
404
  function killWin32Tree(pid) {
405
405
  try {
406
- spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
406
+ const child = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
407
407
  stdio: "ignore",
408
408
  windowsHide: true
409
- }).unref();
409
+ });
410
+ child.on("error", () => {
411
+ });
412
+ child.unref();
410
413
  return true;
411
414
  } catch {
412
415
  return false;
@@ -608,7 +611,7 @@ var ProcessRegistryImpl = class {
608
611
  if (p.killed) return true;
609
612
  if (p.protected) return false;
610
613
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
611
- const isWin = os.platform() === "win32";
614
+ const isWin = os2.platform() === "win32";
612
615
  if (isWin) {
613
616
  const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
614
617
  if (liveRealChild && killWin32Tree(pid)) {
@@ -695,6 +698,560 @@ function getProcessRegistry() {
695
698
  }
696
699
  return _registry;
697
700
  }
701
+ var REGISTRY_FILE = ".wrongstack/process-registry.json";
702
+ var HEARTBEAT_INTERVAL_MS = 5e3;
703
+ var STALE_THRESHOLD_MS = 3e4;
704
+ var LOCKFILE = ".wrongstack/.process-registry.lock";
705
+ function generateInstanceId() {
706
+ const hostname2 = os2.hostname();
707
+ const pid = process.pid;
708
+ const random = Math.random().toString(36).slice(2, 8);
709
+ return `${hostname2}:${pid}:${random}`;
710
+ }
711
+ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
712
+ const start = Date.now();
713
+ const pidStr = String(process.pid);
714
+ const hostStr = os2.hostname();
715
+ while (Date.now() - start < timeoutMs) {
716
+ try {
717
+ await fs.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });
718
+ return async () => {
719
+ try {
720
+ await fs.unlink(lockfilePath);
721
+ } catch {
722
+ }
723
+ };
724
+ } catch (err) {
725
+ if (err.code === "EEXIST") {
726
+ try {
727
+ const content = await fs.readFile(lockfilePath, "utf-8");
728
+ const parts = content.split(":");
729
+ const lockPidStr = parts[0] ?? "0";
730
+ const lockPid = parseInt(lockPidStr, 10);
731
+ if (process.platform !== "win32") {
732
+ try {
733
+ process.kill(lockPid, 0);
734
+ } catch {
735
+ await fs.unlink(lockfilePath);
736
+ continue;
737
+ }
738
+ }
739
+ } catch {
740
+ try {
741
+ await fs.unlink(lockfilePath);
742
+ } catch {
743
+ }
744
+ }
745
+ await new Promise((r) => setTimeout(r, 100));
746
+ continue;
747
+ }
748
+ throw err;
749
+ }
750
+ }
751
+ throw new Error(`Failed to acquire lock after ${timeoutMs}ms`);
752
+ }
753
+ async function readRegistryFile(filePath) {
754
+ try {
755
+ const content = await fs.readFile(filePath, "utf-8");
756
+ const parsed = JSON.parse(content);
757
+ if (parsed.instances && Array.isArray(parsed.instances)) {
758
+ parsed.instances = new Map(parsed.instances);
759
+ }
760
+ return parsed;
761
+ } catch (err) {
762
+ if (err.code === "ENOENT") {
763
+ return {
764
+ version: 1,
765
+ instances: /* @__PURE__ */ new Map(),
766
+ protectedPatterns: ["wrongstack", "node"],
767
+ lastCleanup: Date.now()
768
+ };
769
+ }
770
+ throw err;
771
+ }
772
+ }
773
+ async function writeRegistryFile(filePath, data) {
774
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
775
+ const content = JSON.stringify(data, (_k, v) => {
776
+ if (v instanceof Map) {
777
+ return Array.from(v.entries());
778
+ }
779
+ return v;
780
+ }, 2);
781
+ await fs.writeFile(tmpPath, content, "utf-8");
782
+ await fs.rename(tmpPath, filePath);
783
+ }
784
+ var PersistentProcessRegistry = class {
785
+ instanceId;
786
+ registryPath;
787
+ lockPath;
788
+ baseRegistry;
789
+ heartbeatInterval = null;
790
+ isShuttingDown = false;
791
+ constructor(baseRegistry) {
792
+ this.instanceId = generateInstanceId();
793
+ const homeDir = os2.homedir();
794
+ this.registryPath = path.join(homeDir, REGISTRY_FILE);
795
+ this.lockPath = path.join(homeDir, LOCKFILE);
796
+ this.baseRegistry = baseRegistry ?? getProcessRegistry();
797
+ this.ensureDirectory().catch((err) => {
798
+ console.error("PersistentProcessRegistry: failed to create .wrongstack dir", err);
799
+ });
800
+ }
801
+ async ensureDirectory() {
802
+ const dir = path.dirname(this.registryPath);
803
+ try {
804
+ await fs.mkdir(dir, { recursive: true });
805
+ } catch (err) {
806
+ if (err.code !== "EEXIST") throw err;
807
+ }
808
+ }
809
+ /**
810
+ * Start the heartbeat and periodic cleanup tasks.
811
+ */
812
+ start() {
813
+ if (this.heartbeatInterval) return;
814
+ this.syncToPersistent();
815
+ this.heartbeatInterval = setInterval(() => {
816
+ this.heartbeat();
817
+ }, HEARTBEAT_INTERVAL_MS);
818
+ this.heartbeatInterval.unref?.();
819
+ setInterval(() => {
820
+ this.cleanupStaleEntries();
821
+ }, STALE_THRESHOLD_MS).unref?.();
822
+ this.registerMainProcess();
823
+ process.on("exit", () => this.syncToPersistent());
824
+ }
825
+ /**
826
+ * Stop the heartbeat and clean up.
827
+ */
828
+ stop() {
829
+ this.isShuttingDown = true;
830
+ if (this.heartbeatInterval) {
831
+ clearInterval(this.heartbeatInterval);
832
+ this.heartbeatInterval = null;
833
+ }
834
+ this.syncToPersistent();
835
+ }
836
+ /**
837
+ * Register the main WrongStack process as protected.
838
+ */
839
+ registerMainProcess() {
840
+ const mainPid = process.pid;
841
+ this.updatePersistentEntry({
842
+ pid: mainPid,
843
+ name: "wrongstack-main",
844
+ command: process.argv.slice(0, 3).join(" "),
845
+ startedAt: Date.now(),
846
+ lastHeartbeat: Date.now(),
847
+ instanceId: this.instanceId,
848
+ hostname: os2.hostname(),
849
+ protected: true,
850
+ spawnMode: "main",
851
+ parentPid: process.ppid,
852
+ platform: process.platform
853
+ });
854
+ }
855
+ /**
856
+ * Register a spawned child process with the persistent registry.
857
+ */
858
+ registerChildProcess(pid, name, command, sessionId, spawnMode = "spawn") {
859
+ const entry = {
860
+ pid,
861
+ name,
862
+ command,
863
+ startedAt: Date.now(),
864
+ lastHeartbeat: Date.now(),
865
+ instanceId: this.instanceId,
866
+ hostname: os2.hostname(),
867
+ protected: true,
868
+ // All WrongStack child processes are protected by default
869
+ spawnMode,
870
+ parentPid: process.pid,
871
+ platform: process.platform
872
+ };
873
+ if (sessionId) {
874
+ entry.sessionId = sessionId;
875
+ }
876
+ this.updatePersistentEntry(entry);
877
+ }
878
+ /**
879
+ * Update or add an entry in the persistent registry.
880
+ */
881
+ async updatePersistentEntry(entry) {
882
+ const release = await acquireLock(this.lockPath);
883
+ try {
884
+ const data = await readRegistryFile(this.registryPath);
885
+ data.instances.set(String(entry.pid), entry);
886
+ this.baseRegistry.register({
887
+ pid: entry.pid,
888
+ name: entry.name,
889
+ command: entry.command,
890
+ startedAt: entry.startedAt,
891
+ sessionId: entry.sessionId,
892
+ protected: entry.protected,
893
+ child: null
894
+ // Main process has no child handle
895
+ });
896
+ await writeRegistryFile(this.registryPath, data);
897
+ } finally {
898
+ await release();
899
+ }
900
+ }
901
+ /**
902
+ * Unregister a process from the persistent registry.
903
+ */
904
+ async unregister(pid) {
905
+ const release = await acquireLock(this.lockPath);
906
+ try {
907
+ const data = await readRegistryFile(this.registryPath);
908
+ data.instances.delete(String(pid));
909
+ await writeRegistryFile(this.registryPath, data);
910
+ } finally {
911
+ await release();
912
+ }
913
+ }
914
+ /**
915
+ * Send heartbeat to mark all this instance's processes as alive.
916
+ */
917
+ heartbeat() {
918
+ if (this.isShuttingDown) return;
919
+ this.syncToPersistent();
920
+ }
921
+ /**
922
+ * Sync this instance's processes to the persistent registry.
923
+ */
924
+ async syncToPersistent() {
925
+ const release = await acquireLock(this.lockPath);
926
+ try {
927
+ const data = await readRegistryFile(this.registryPath);
928
+ const now = Date.now();
929
+ const updatedInstances = /* @__PURE__ */ new Map();
930
+ for (const [_pidStr, entry] of data.instances) {
931
+ if (entry.instanceId === this.instanceId) {
932
+ entry.lastHeartbeat = now;
933
+ }
934
+ if (entry.instanceId === this.instanceId || now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
935
+ updatedInstances.set(_pidStr, entry);
936
+ }
937
+ }
938
+ data.instances = updatedInstances;
939
+ data.lastCleanup = now;
940
+ await writeRegistryFile(this.registryPath, data);
941
+ } catch (err) {
942
+ console.error("PersistentProcessRegistry: sync failed", err);
943
+ } finally {
944
+ await release();
945
+ }
946
+ }
947
+ /**
948
+ * Remove entries for processes that are no longer running.
949
+ */
950
+ async cleanupStaleEntries() {
951
+ const release = await acquireLock(this.lockPath);
952
+ try {
953
+ const data = await readRegistryFile(this.registryPath);
954
+ const now = Date.now();
955
+ const stalePids = [];
956
+ for (const [_pidStr, entry] of data.instances) {
957
+ const age = now - entry.lastHeartbeat;
958
+ if (age > STALE_THRESHOLD_MS) {
959
+ try {
960
+ if (process.platform !== "win32") {
961
+ process.kill(entry.pid, 0);
962
+ } else {
963
+ console.log(`PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`);
964
+ }
965
+ } catch {
966
+ stalePids.push(_pidStr);
967
+ }
968
+ }
969
+ }
970
+ if (stalePids.length > 0) {
971
+ for (const pidStr of stalePids) {
972
+ data.instances.delete(pidStr);
973
+ }
974
+ await writeRegistryFile(this.registryPath, data);
975
+ }
976
+ } catch (err) {
977
+ console.error("PersistentProcessRegistry: cleanup failed", err);
978
+ } finally {
979
+ await release();
980
+ }
981
+ }
982
+ /**
983
+ * Check if a PID belongs to a WrongStack process and should be protected.
984
+ */
985
+ async isProtectedPid(pid) {
986
+ const release = await acquireLock(this.lockPath);
987
+ try {
988
+ const data = await readRegistryFile(this.registryPath);
989
+ const entry = data.instances.get(String(pid));
990
+ if (!entry) return false;
991
+ if (Date.now() - entry.lastHeartbeat > STALE_THRESHOLD_MS) {
992
+ return false;
993
+ }
994
+ return entry.protected;
995
+ } finally {
996
+ await release();
997
+ }
998
+ }
999
+ /**
1000
+ * Get all protected PIDs from all WrongStack instances.
1001
+ */
1002
+ async getAllProtectedPids() {
1003
+ const release = await acquireLock(this.lockPath);
1004
+ try {
1005
+ const data = await readRegistryFile(this.registryPath);
1006
+ const now = Date.now();
1007
+ const protectedPids = [];
1008
+ for (const [_pidStr, entry] of data.instances) {
1009
+ if (entry.protected && now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
1010
+ protectedPids.push(entry.pid);
1011
+ }
1012
+ }
1013
+ return protectedPids;
1014
+ } finally {
1015
+ await release();
1016
+ }
1017
+ }
1018
+ /**
1019
+ * Get complete status of all tracked processes across all instances.
1020
+ */
1021
+ async getGlobalStatus() {
1022
+ const release = await acquireLock(this.lockPath);
1023
+ try {
1024
+ const data = await readRegistryFile(this.registryPath);
1025
+ const now = Date.now();
1026
+ const instances = /* @__PURE__ */ new Map();
1027
+ let protectedCount = 0;
1028
+ let staleCount = 0;
1029
+ for (const [_pidStr, entry] of data.instances) {
1030
+ const instanceEntries = instances.get(entry.instanceId) ?? [];
1031
+ instanceEntries.push(entry);
1032
+ instances.set(entry.instanceId, instanceEntries);
1033
+ if (entry.protected) protectedCount++;
1034
+ if (now - entry.lastHeartbeat > STALE_THRESHOLD_MS) staleCount++;
1035
+ }
1036
+ return {
1037
+ instances,
1038
+ totalProcesses: data.instances.size,
1039
+ protectedCount,
1040
+ staleCount
1041
+ };
1042
+ } finally {
1043
+ await release();
1044
+ }
1045
+ }
1046
+ /**
1047
+ * Get the instance ID for this process.
1048
+ */
1049
+ getInstanceId() {
1050
+ return this.instanceId;
1051
+ }
1052
+ /**
1053
+ * Check if a kill command should be blocked.
1054
+ * Returns true if the kill should be blocked (target is a WrongStack process).
1055
+ */
1056
+ async shouldBlockKill(pid) {
1057
+ const protectedPids = await this.getAllProtectedPids();
1058
+ return protectedPids.includes(pid);
1059
+ }
1060
+ /**
1061
+ * Add a pattern-based protection rule.
1062
+ * Processes whose command matches any protected pattern are protected.
1063
+ */
1064
+ async addProtectedPattern(pattern) {
1065
+ const release = await acquireLock(this.lockPath);
1066
+ try {
1067
+ const data = await readRegistryFile(this.registryPath);
1068
+ if (!data.protectedPatterns.includes(pattern)) {
1069
+ data.protectedPatterns.push(pattern);
1070
+ await writeRegistryFile(this.registryPath, data);
1071
+ }
1072
+ } finally {
1073
+ await release();
1074
+ }
1075
+ }
1076
+ };
1077
+ var _persistentRegistry;
1078
+ function getPersistentProcessRegistry() {
1079
+ if (!_persistentRegistry) {
1080
+ _persistentRegistry = new PersistentProcessRegistry();
1081
+ }
1082
+ return _persistentRegistry;
1083
+ }
1084
+
1085
+ // src/bash-kill-guard.ts
1086
+ function extractKillCommand(command) {
1087
+ const normalized = command.replace(/\s+/g, " ").trim();
1088
+ const shellCMatch = normalized.match(
1089
+ /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+['"](.+?)['"]$/
1090
+ );
1091
+ if (shellCMatch?.[1]) {
1092
+ const inner = shellCMatch[1].trim();
1093
+ return isKillRelatedCommand(inner) ? inner : null;
1094
+ }
1095
+ const shellCUnquoted = normalized.match(
1096
+ /^(?:\/\w+)?\/?(?:bin|usr)\/(?:ba)?sh\s+-[c]\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
1097
+ );
1098
+ if (shellCUnquoted?.[1]) {
1099
+ return shellCUnquoted[1];
1100
+ }
1101
+ return null;
1102
+ }
1103
+ function isKillRelatedCommand(cmd) {
1104
+ const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
1105
+ if (/^kill(\s|$)/.test(normalized)) return true;
1106
+ if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
1107
+ if (/^taskkill\s/i.test(normalized)) return true;
1108
+ if (/^tskill\s/i.test(normalized)) return true;
1109
+ if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
1110
+ return false;
1111
+ }
1112
+ function parseKillCommand(command) {
1113
+ const normalized = command.replace(/\s+/g, " ").trim();
1114
+ const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
1115
+ if (simpleMatch) {
1116
+ const signal = simpleMatch[1] ?? "-TERM";
1117
+ const pidOrGroup = simpleMatch[2];
1118
+ if (!pidOrGroup) return null;
1119
+ const isGroupKill = pidOrGroup.startsWith("-");
1120
+ const pid = isGroupKill ? parseInt(pidOrGroup.slice(1), 10) : parseInt(pidOrGroup, 10);
1121
+ return {
1122
+ pid,
1123
+ signal: signal.slice(1),
1124
+ isGroupKill,
1125
+ isAllKill: false,
1126
+ originalCommand: command
1127
+ };
1128
+ }
1129
+ const pkillMatch = normalized.match(/^pkill\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
1130
+ if (pkillMatch?.[2]) {
1131
+ const name = pkillMatch[2];
1132
+ const signalMatch = pkillMatch[1];
1133
+ return {
1134
+ name,
1135
+ signal: signalMatch ? signalMatch.slice(1) : "TERM",
1136
+ isGroupKill: false,
1137
+ isAllKill: false,
1138
+ originalCommand: command
1139
+ };
1140
+ }
1141
+ const killallMatch = normalized.match(/^killall\s+(?:(-[a-zA-Z]+)\s+)?(.+)$/);
1142
+ if (killallMatch?.[2]) {
1143
+ const name = killallMatch[2];
1144
+ const signalMatch = killallMatch[1];
1145
+ return {
1146
+ name,
1147
+ signal: signalMatch ? signalMatch.slice(1) : "TERM",
1148
+ isGroupKill: false,
1149
+ isAllKill: false,
1150
+ originalCommand: command
1151
+ };
1152
+ }
1153
+ const pgrepMatch = normalized.match(/^pgrep\s+(.+)$/);
1154
+ if (pgrepMatch) {
1155
+ return null;
1156
+ }
1157
+ const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
1158
+ if (taskkillMatch?.[1]) {
1159
+ const pidStr = taskkillMatch[1];
1160
+ return {
1161
+ pid: parseInt(pidStr, 10),
1162
+ signal: normalized.includes("/F") ? "FORCE" : "TERM",
1163
+ isGroupKill: false,
1164
+ isAllKill: false,
1165
+ originalCommand: command
1166
+ };
1167
+ }
1168
+ const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
1169
+ if (tskillMatch?.[1]) {
1170
+ const pidStr = tskillMatch[1];
1171
+ return {
1172
+ pid: parseInt(pidStr, 10),
1173
+ signal: "TERM",
1174
+ isGroupKill: false,
1175
+ isAllKill: false,
1176
+ originalCommand: command
1177
+ };
1178
+ }
1179
+ return null;
1180
+ }
1181
+ async function getProtectedEntries() {
1182
+ const registry = getPersistentProcessRegistry();
1183
+ const status = await registry.getGlobalStatus();
1184
+ const entries = [];
1185
+ for (const instanceEntries of status.instances.values()) {
1186
+ for (const entry of instanceEntries) {
1187
+ if (entry.protected && Date.now() - entry.lastHeartbeat < 3e4) {
1188
+ entries.push(entry);
1189
+ }
1190
+ }
1191
+ }
1192
+ return entries;
1193
+ }
1194
+ async function isKillProtected(kill) {
1195
+ const registry = getPersistentProcessRegistry();
1196
+ if (kill.name) {
1197
+ const entries = await getProtectedEntries();
1198
+ const killNameLower = kill.name.toLowerCase();
1199
+ for (const entry of entries) {
1200
+ if (entry.name && entry.name.toLowerCase().includes(killNameLower)) {
1201
+ return true;
1202
+ }
1203
+ }
1204
+ if (killNameLower.includes("wrongstack")) {
1205
+ return true;
1206
+ }
1207
+ if (killNameLower.includes("node") && entries.length > 0) {
1208
+ return true;
1209
+ }
1210
+ return false;
1211
+ }
1212
+ if (kill.isGroupKill) {
1213
+ const protectedPids = await registry.getAllProtectedPids();
1214
+ return protectedPids.length > 0;
1215
+ }
1216
+ if (kill.pid !== void 0) {
1217
+ return registry.shouldBlockKill(kill.pid);
1218
+ }
1219
+ return false;
1220
+ }
1221
+ async function checkAndBlockKillCommand(command) {
1222
+ const normalized = command.replace(/\s+/g, " ").trim();
1223
+ const killCmd = extractKillCommand(normalized) || (isKillRelatedCommand(normalized) ? normalized : null);
1224
+ if (!killCmd) {
1225
+ return { blocked: false };
1226
+ }
1227
+ const parsed = parseKillCommand(killCmd);
1228
+ if (!parsed) {
1229
+ if (killCmd.includes("kill") && /kill\s+.*\|/.test(killCmd)) {
1230
+ return {
1231
+ blocked: true,
1232
+ reason: `Blocked: complex kill pipeline detected \u2014 "${killCmd.slice(0, 50)}..."`
1233
+ };
1234
+ }
1235
+ return { blocked: false };
1236
+ }
1237
+ if (await isKillProtected(parsed)) {
1238
+ let target;
1239
+ if (parsed.name) {
1240
+ target = `process name "${parsed.name}"`;
1241
+ } else if (parsed.pid !== void 0) {
1242
+ target = `PID ${parsed.pid}`;
1243
+ } else {
1244
+ target = "(unknown target)";
1245
+ }
1246
+ const signal = parsed.signal ? ` (${parsed.signal})` : "";
1247
+ const groupNote = parsed.isGroupKill ? " (process group)" : "";
1248
+ return {
1249
+ blocked: true,
1250
+ reason: `Blocked: kill${signal} ${target}${groupNote} targets a protected WrongStack process.`
1251
+ };
1252
+ }
1253
+ return { blocked: false };
1254
+ }
698
1255
 
699
1256
  // src/bash.ts
700
1257
  var MAX_OUTPUT = 32768;
@@ -764,6 +1321,20 @@ var bashTool = {
764
1321
  };
765
1322
  return;
766
1323
  }
1324
+ const killCheck = await checkAndBlockKillCommand(input.command);
1325
+ if (killCheck.blocked) {
1326
+ yield {
1327
+ type: "final",
1328
+ output: {
1329
+ output: "",
1330
+ exit_code: 1,
1331
+ timed_out: false,
1332
+ pid: null,
1333
+ error: killCheck.reason || "Kill command blocked: targets a protected WrongStack process."
1334
+ }
1335
+ };
1336
+ return;
1337
+ }
767
1338
  const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
768
1339
  if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
769
1340
  console.warn(JSON.stringify({
@@ -776,7 +1347,7 @@ var bashTool = {
776
1347
  }));
777
1348
  }
778
1349
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
779
- const isWin = os.platform() === "win32";
1350
+ const isWin = os2.platform() === "win32";
780
1351
  const shell = (() => {
781
1352
  const explicit = process.env[isWin ? "WRONGSTACK_COMSPEC" : "WRONGSTACK_SHELL"];
782
1353
  if (explicit) return explicit;
@@ -807,8 +1378,7 @@ var bashTool = {
807
1378
  // Windows children survive parent exit either way. POSIX keeps
808
1379
  // detached for the process-group kill semantics.
809
1380
  detached: !isWin,
810
- windowsHide: true,
811
- signal: opts.signal
1381
+ windowsHide: true
812
1382
  });
813
1383
  const pid2 = child2.pid;
814
1384
  if (typeof pid2 === "number") {
@@ -836,7 +1406,17 @@ var bashTool = {
836
1406
  };
837
1407
  child2.stdout?.on("data", onBgData);
838
1408
  child2.stderr?.on("data", onBgData);
1409
+ const cleanupBackground = () => {
1410
+ child2.stdout?.off("data", onBgData);
1411
+ child2.stderr?.off("data", onBgData);
1412
+ };
1413
+ child2.on("error", () => {
1414
+ cleanupBackground();
1415
+ if (typeof pid2 === "number") registry.unregister(pid2);
1416
+ registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
1417
+ });
839
1418
  child2.on("close", () => {
1419
+ cleanupBackground();
840
1420
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
841
1421
  });
842
1422
  if (typeof pid2 === "number") child2.unref();