@wrongstack/tools 0.270.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/audit.js +10 -5
- package/dist/audit.js.map +1 -1
- package/dist/{background-indexer-CJ5JiV5i.d.ts → background-indexer-BoTUw0EM.d.ts} +1 -1
- package/dist/bash.js +580 -9
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +846 -172
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.d.ts +13 -3
- package/dist/codebase-index/index.js +129 -74
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +126 -71
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/exec.js +5 -2
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js +24 -1
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +10 -5
- package/dist/format.js.map +1 -1
- package/dist/grep.js +55 -33
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +317 -3
- package/dist/index.js +1405 -200
- package/dist/index.js.map +1 -1
- package/dist/install.js +10 -5
- package/dist/install.js.map +1 -1
- package/dist/lint.js +10 -5
- package/dist/lint.js.map +1 -1
- package/dist/pack.js +846 -172
- package/dist/pack.js.map +1 -1
- package/dist/process-registry.js +5 -2
- package/dist/process-registry.js.map +1 -1
- package/dist/read.js +2 -2
- package/dist/read.js.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/test.js +10 -5
- package/dist/test.js.map +1 -1
- package/dist/typecheck.js +10 -5
- package/dist/typecheck.js.map +1 -1
- package/package.json +2 -2
package/dist/bash.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import * as
|
|
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
|
|
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
|
|
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
|
|
27
|
-
if (now - st.mtimeMs > SPOOL_RETENTION_MS) await
|
|
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
|
-
})
|
|
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 =
|
|
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 =
|
|
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;
|