@wrongstack/tools 0.291.0 → 0.291.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bash-kill-guard.d.ts +10 -1
- package/dist/bash-kill-guard.d.ts.map +1 -1
- package/dist/bash.js +140 -14
- package/dist/bash.js.map +2 -2
- package/dist/builtin.js +464 -90
- package/dist/builtin.js.map +4 -4
- package/dist/exec-kill-guard.d.ts +29 -0
- package/dist/exec-kill-guard.d.ts.map +1 -0
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +670 -10
- package/dist/exec.js.map +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +490 -113
- package/dist/index.js.map +4 -4
- package/dist/pack.js +464 -90
- package/dist/pack.js.map +4 -4
- package/package.json +3 -3
package/dist/exec.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
emitProcessOutput,
|
|
6
6
|
emitProcessStarted
|
|
7
7
|
} from "@wrongstack/core";
|
|
8
|
-
import { toErrorMessage } from "@wrongstack/core/utils/error";
|
|
8
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils/error";
|
|
9
9
|
|
|
10
10
|
// src/_env.ts
|
|
11
11
|
import { buildChildEnv } from "@wrongstack/core";
|
|
@@ -734,8 +734,8 @@ var ProcessRegistryImpl = class {
|
|
|
734
734
|
if (p.killed) return true;
|
|
735
735
|
if (p.protected) return false;
|
|
736
736
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
737
|
-
const
|
|
738
|
-
if (
|
|
737
|
+
const isWin3 = os.platform() === "win32";
|
|
738
|
+
if (isWin3) {
|
|
739
739
|
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
740
740
|
const directFallback = () => {
|
|
741
741
|
if (p.child.exitCode === null) {
|
|
@@ -1164,8 +1164,655 @@ function levelRank(level) {
|
|
|
1164
1164
|
}
|
|
1165
1165
|
}
|
|
1166
1166
|
|
|
1167
|
+
// src/exec-kill-guard.ts
|
|
1168
|
+
import * as os3 from "node:os";
|
|
1169
|
+
import * as path5 from "node:path";
|
|
1170
|
+
|
|
1171
|
+
// src/process-registry-persistent.ts
|
|
1172
|
+
import * as fs2 from "node:fs/promises";
|
|
1173
|
+
import * as os2 from "node:os";
|
|
1174
|
+
import * as path4 from "node:path";
|
|
1175
|
+
var REGISTRY_FILE = ".wrongstack/process-registry.json";
|
|
1176
|
+
function toErrorMessage(err) {
|
|
1177
|
+
return err instanceof Error ? err.message : String(err);
|
|
1178
|
+
}
|
|
1179
|
+
function emitStructuredLog(level, event, message, error) {
|
|
1180
|
+
const payload = {
|
|
1181
|
+
level,
|
|
1182
|
+
event,
|
|
1183
|
+
message,
|
|
1184
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1185
|
+
};
|
|
1186
|
+
if (error !== void 0) {
|
|
1187
|
+
payload.error = toErrorMessage(error);
|
|
1188
|
+
}
|
|
1189
|
+
console.log(JSON.stringify(payload));
|
|
1190
|
+
}
|
|
1191
|
+
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
1192
|
+
var STALE_THRESHOLD_MS = 3e4;
|
|
1193
|
+
var LOCKFILE = ".wrongstack/.process-registry.lock";
|
|
1194
|
+
function generateInstanceId() {
|
|
1195
|
+
const hostname2 = os2.hostname();
|
|
1196
|
+
const pid = process.pid;
|
|
1197
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
1198
|
+
return `${hostname2}:${pid}:${random}`;
|
|
1199
|
+
}
|
|
1200
|
+
function isNodeError(err) {
|
|
1201
|
+
return typeof err === "object" && err !== null && "code" in err;
|
|
1202
|
+
}
|
|
1203
|
+
async function acquireLock(lockfilePath, timeoutMs = 5e3) {
|
|
1204
|
+
const start = Date.now();
|
|
1205
|
+
const pidStr = String(process.pid);
|
|
1206
|
+
const hostStr = os2.hostname();
|
|
1207
|
+
while (Date.now() - start < timeoutMs) {
|
|
1208
|
+
try {
|
|
1209
|
+
await fs2.writeFile(lockfilePath, `${pidStr}:${hostStr}:${Date.now()}`, { flag: "wx" });
|
|
1210
|
+
return async () => {
|
|
1211
|
+
try {
|
|
1212
|
+
await fs2.unlink(lockfilePath);
|
|
1213
|
+
} catch {
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
if (isNodeError(err) && err.code === "EEXIST") {
|
|
1218
|
+
try {
|
|
1219
|
+
const content = await fs2.readFile(lockfilePath, "utf-8");
|
|
1220
|
+
const parts = content.split(":");
|
|
1221
|
+
const lockPidStr = parts[0] ?? "0";
|
|
1222
|
+
const lockPid = parseInt(lockPidStr, 10);
|
|
1223
|
+
if (process.platform !== "win32") {
|
|
1224
|
+
try {
|
|
1225
|
+
process.kill(lockPid, 0);
|
|
1226
|
+
} catch {
|
|
1227
|
+
await fs2.unlink(lockfilePath);
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
} catch {
|
|
1232
|
+
try {
|
|
1233
|
+
await fs2.unlink(lockfilePath);
|
|
1234
|
+
} catch {
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
throw err;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
throw new Error(`Failed to acquire lock after ${timeoutMs}ms`);
|
|
1244
|
+
}
|
|
1245
|
+
async function readRegistryFile(filePath) {
|
|
1246
|
+
try {
|
|
1247
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
1248
|
+
const parsed = JSON.parse(content);
|
|
1249
|
+
if (parsed.instances && Array.isArray(parsed.instances)) {
|
|
1250
|
+
parsed.instances = new Map(parsed.instances);
|
|
1251
|
+
}
|
|
1252
|
+
return parsed;
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
1255
|
+
return {
|
|
1256
|
+
version: 1,
|
|
1257
|
+
instances: /* @__PURE__ */ new Map(),
|
|
1258
|
+
protectedPatterns: ["wrongstack", "node"],
|
|
1259
|
+
lastCleanup: Date.now()
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
throw err;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
async function writeRegistryFile(filePath, data) {
|
|
1266
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1267
|
+
const content = JSON.stringify(data, (_k, v) => {
|
|
1268
|
+
if (v instanceof Map) {
|
|
1269
|
+
return Array.from(v.entries());
|
|
1270
|
+
}
|
|
1271
|
+
return v;
|
|
1272
|
+
}, 2);
|
|
1273
|
+
await fs2.writeFile(tmpPath, content, "utf-8");
|
|
1274
|
+
await fs2.rename(tmpPath, filePath);
|
|
1275
|
+
}
|
|
1276
|
+
var PersistentProcessRegistry = class {
|
|
1277
|
+
instanceId;
|
|
1278
|
+
registryPath;
|
|
1279
|
+
lockPath;
|
|
1280
|
+
baseRegistry;
|
|
1281
|
+
heartbeatInterval = null;
|
|
1282
|
+
isShuttingDown = false;
|
|
1283
|
+
constructor(baseRegistry) {
|
|
1284
|
+
this.instanceId = generateInstanceId();
|
|
1285
|
+
const homeDir = os2.homedir();
|
|
1286
|
+
this.registryPath = path4.join(homeDir, REGISTRY_FILE);
|
|
1287
|
+
this.lockPath = path4.join(homeDir, LOCKFILE);
|
|
1288
|
+
this.baseRegistry = baseRegistry ?? getProcessRegistry();
|
|
1289
|
+
this.ensureDirectory().catch((err) => {
|
|
1290
|
+
emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
async ensureDirectory() {
|
|
1294
|
+
const dir = path4.dirname(this.registryPath);
|
|
1295
|
+
try {
|
|
1296
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
if (!isNodeError(err) || err.code !== "EEXIST") throw err;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Start the heartbeat and periodic cleanup tasks.
|
|
1303
|
+
*/
|
|
1304
|
+
start() {
|
|
1305
|
+
if (this.heartbeatInterval) return;
|
|
1306
|
+
this.syncToPersistent();
|
|
1307
|
+
this.heartbeatInterval = setInterval(() => {
|
|
1308
|
+
this.heartbeat();
|
|
1309
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
1310
|
+
this.heartbeatInterval.unref?.();
|
|
1311
|
+
setInterval(() => {
|
|
1312
|
+
this.cleanupStaleEntries();
|
|
1313
|
+
}, STALE_THRESHOLD_MS).unref?.();
|
|
1314
|
+
this.registerMainProcess();
|
|
1315
|
+
process.on("exit", () => this.syncToPersistent());
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Stop the heartbeat and clean up.
|
|
1319
|
+
*/
|
|
1320
|
+
stop() {
|
|
1321
|
+
this.isShuttingDown = true;
|
|
1322
|
+
if (this.heartbeatInterval) {
|
|
1323
|
+
clearInterval(this.heartbeatInterval);
|
|
1324
|
+
this.heartbeatInterval = null;
|
|
1325
|
+
}
|
|
1326
|
+
this.syncToPersistent();
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Register the main WrongStack process as protected.
|
|
1330
|
+
*/
|
|
1331
|
+
registerMainProcess() {
|
|
1332
|
+
const mainPid = process.pid;
|
|
1333
|
+
this.updatePersistentEntry({
|
|
1334
|
+
pid: mainPid,
|
|
1335
|
+
name: "wrongstack-main",
|
|
1336
|
+
command: process.argv.slice(0, 3).join(" "),
|
|
1337
|
+
startedAt: Date.now(),
|
|
1338
|
+
lastHeartbeat: Date.now(),
|
|
1339
|
+
instanceId: this.instanceId,
|
|
1340
|
+
hostname: os2.hostname(),
|
|
1341
|
+
protected: true,
|
|
1342
|
+
spawnMode: "main",
|
|
1343
|
+
parentPid: process.ppid,
|
|
1344
|
+
platform: process.platform
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Register a spawned child process with the persistent registry.
|
|
1349
|
+
*/
|
|
1350
|
+
registerChildProcess(pid, name, command, sessionId, spawnMode = "spawn") {
|
|
1351
|
+
const entry = {
|
|
1352
|
+
pid,
|
|
1353
|
+
name,
|
|
1354
|
+
command,
|
|
1355
|
+
startedAt: Date.now(),
|
|
1356
|
+
lastHeartbeat: Date.now(),
|
|
1357
|
+
instanceId: this.instanceId,
|
|
1358
|
+
hostname: os2.hostname(),
|
|
1359
|
+
protected: true,
|
|
1360
|
+
// All WrongStack child processes are protected by default
|
|
1361
|
+
spawnMode,
|
|
1362
|
+
parentPid: process.pid,
|
|
1363
|
+
platform: process.platform
|
|
1364
|
+
};
|
|
1365
|
+
if (sessionId) {
|
|
1366
|
+
entry.sessionId = sessionId;
|
|
1367
|
+
}
|
|
1368
|
+
this.updatePersistentEntry(entry);
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Update or add an entry in the persistent registry.
|
|
1372
|
+
*/
|
|
1373
|
+
async updatePersistentEntry(entry) {
|
|
1374
|
+
const release = await acquireLock(this.lockPath);
|
|
1375
|
+
try {
|
|
1376
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1377
|
+
data.instances.set(String(entry.pid), entry);
|
|
1378
|
+
const child = null;
|
|
1379
|
+
this.baseRegistry.register({
|
|
1380
|
+
pid: entry.pid,
|
|
1381
|
+
name: entry.name,
|
|
1382
|
+
command: entry.command,
|
|
1383
|
+
startedAt: entry.startedAt,
|
|
1384
|
+
sessionId: entry.sessionId,
|
|
1385
|
+
protected: entry.protected,
|
|
1386
|
+
child
|
|
1387
|
+
});
|
|
1388
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1389
|
+
} finally {
|
|
1390
|
+
await release();
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Unregister a process from the persistent registry.
|
|
1395
|
+
*/
|
|
1396
|
+
async unregister(pid) {
|
|
1397
|
+
const release = await acquireLock(this.lockPath);
|
|
1398
|
+
try {
|
|
1399
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1400
|
+
data.instances.delete(String(pid));
|
|
1401
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1402
|
+
} finally {
|
|
1403
|
+
await release();
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Send heartbeat to mark all this instance's processes as alive.
|
|
1408
|
+
*/
|
|
1409
|
+
heartbeat() {
|
|
1410
|
+
if (this.isShuttingDown) return;
|
|
1411
|
+
this.syncToPersistent();
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Sync this instance's processes to the persistent registry.
|
|
1415
|
+
*/
|
|
1416
|
+
async syncToPersistent() {
|
|
1417
|
+
const release = await acquireLock(this.lockPath);
|
|
1418
|
+
try {
|
|
1419
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1420
|
+
const now = Date.now();
|
|
1421
|
+
const updatedInstances = /* @__PURE__ */ new Map();
|
|
1422
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
1423
|
+
if (entry.instanceId === this.instanceId) {
|
|
1424
|
+
entry.lastHeartbeat = now;
|
|
1425
|
+
}
|
|
1426
|
+
if (entry.instanceId === this.instanceId || now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
|
|
1427
|
+
updatedInstances.set(_pidStr, entry);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
data.instances = updatedInstances;
|
|
1431
|
+
data.lastCleanup = now;
|
|
1432
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
emitStructuredLog("warn", "process_registry.sync_failed", "PersistentProcessRegistry: sync failed", err);
|
|
1435
|
+
} finally {
|
|
1436
|
+
await release();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Remove entries for processes that are no longer running.
|
|
1441
|
+
*/
|
|
1442
|
+
async cleanupStaleEntries() {
|
|
1443
|
+
const release = await acquireLock(this.lockPath);
|
|
1444
|
+
try {
|
|
1445
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1446
|
+
const now = Date.now();
|
|
1447
|
+
const stalePids = [];
|
|
1448
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
1449
|
+
const age = now - entry.lastHeartbeat;
|
|
1450
|
+
if (age > STALE_THRESHOLD_MS) {
|
|
1451
|
+
try {
|
|
1452
|
+
if (process.platform !== "win32") {
|
|
1453
|
+
process.kill(entry.pid, 0);
|
|
1454
|
+
} else {
|
|
1455
|
+
emitStructuredLog(
|
|
1456
|
+
"debug",
|
|
1457
|
+
"process_registry.stale_pid_check",
|
|
1458
|
+
`PersistentProcessRegistry: checking stale pid ${entry.pid} (${age}ms old)`
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
} catch {
|
|
1462
|
+
stalePids.push(_pidStr);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if (stalePids.length > 0) {
|
|
1467
|
+
for (const pidStr of stalePids) {
|
|
1468
|
+
data.instances.delete(pidStr);
|
|
1469
|
+
}
|
|
1470
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1471
|
+
}
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
emitStructuredLog("warn", "process_registry.cleanup_failed", "PersistentProcessRegistry: cleanup failed", err);
|
|
1474
|
+
} finally {
|
|
1475
|
+
await release();
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Check if a PID belongs to a WrongStack process and should be protected.
|
|
1480
|
+
*/
|
|
1481
|
+
async isProtectedPid(pid) {
|
|
1482
|
+
const release = await acquireLock(this.lockPath);
|
|
1483
|
+
try {
|
|
1484
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1485
|
+
const entry = data.instances.get(String(pid));
|
|
1486
|
+
if (!entry) return false;
|
|
1487
|
+
if (Date.now() - entry.lastHeartbeat > STALE_THRESHOLD_MS) {
|
|
1488
|
+
return false;
|
|
1489
|
+
}
|
|
1490
|
+
return entry.protected;
|
|
1491
|
+
} finally {
|
|
1492
|
+
await release();
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Get all protected PIDs from all WrongStack instances.
|
|
1497
|
+
*/
|
|
1498
|
+
async getAllProtectedPids() {
|
|
1499
|
+
const release = await acquireLock(this.lockPath);
|
|
1500
|
+
try {
|
|
1501
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1502
|
+
const now = Date.now();
|
|
1503
|
+
const protectedPids = [];
|
|
1504
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
1505
|
+
if (entry.protected && now - entry.lastHeartbeat < STALE_THRESHOLD_MS) {
|
|
1506
|
+
protectedPids.push(entry.pid);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return protectedPids;
|
|
1510
|
+
} finally {
|
|
1511
|
+
await release();
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Get complete status of all tracked processes across all instances.
|
|
1516
|
+
*/
|
|
1517
|
+
async getGlobalStatus() {
|
|
1518
|
+
const release = await acquireLock(this.lockPath);
|
|
1519
|
+
try {
|
|
1520
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1521
|
+
const now = Date.now();
|
|
1522
|
+
const instances = /* @__PURE__ */ new Map();
|
|
1523
|
+
let protectedCount = 0;
|
|
1524
|
+
let staleCount = 0;
|
|
1525
|
+
for (const [_pidStr, entry] of data.instances) {
|
|
1526
|
+
const instanceEntries = instances.get(entry.instanceId) ?? [];
|
|
1527
|
+
instanceEntries.push(entry);
|
|
1528
|
+
instances.set(entry.instanceId, instanceEntries);
|
|
1529
|
+
if (entry.protected) protectedCount++;
|
|
1530
|
+
if (now - entry.lastHeartbeat > STALE_THRESHOLD_MS) staleCount++;
|
|
1531
|
+
}
|
|
1532
|
+
return {
|
|
1533
|
+
instances,
|
|
1534
|
+
totalProcesses: data.instances.size,
|
|
1535
|
+
protectedCount,
|
|
1536
|
+
staleCount
|
|
1537
|
+
};
|
|
1538
|
+
} finally {
|
|
1539
|
+
await release();
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Get the instance ID for this process.
|
|
1544
|
+
*/
|
|
1545
|
+
getInstanceId() {
|
|
1546
|
+
return this.instanceId;
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Check if a kill command should be blocked.
|
|
1550
|
+
* Returns true if the kill should be blocked (target is a WrongStack process).
|
|
1551
|
+
*/
|
|
1552
|
+
async shouldBlockKill(pid) {
|
|
1553
|
+
const protectedPids = await this.getAllProtectedPids();
|
|
1554
|
+
return protectedPids.includes(pid);
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Add a pattern-based protection rule.
|
|
1558
|
+
* Processes whose command matches any protected pattern are protected.
|
|
1559
|
+
*/
|
|
1560
|
+
async addProtectedPattern(pattern) {
|
|
1561
|
+
const release = await acquireLock(this.lockPath);
|
|
1562
|
+
try {
|
|
1563
|
+
const data = await readRegistryFile(this.registryPath);
|
|
1564
|
+
if (!data.protectedPatterns.includes(pattern)) {
|
|
1565
|
+
data.protectedPatterns.push(pattern);
|
|
1566
|
+
await writeRegistryFile(this.registryPath, data);
|
|
1567
|
+
}
|
|
1568
|
+
} finally {
|
|
1569
|
+
await release();
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
var _persistentRegistry;
|
|
1574
|
+
function getPersistentProcessRegistry() {
|
|
1575
|
+
if (!_persistentRegistry) {
|
|
1576
|
+
_persistentRegistry = new PersistentProcessRegistry();
|
|
1577
|
+
}
|
|
1578
|
+
return _persistentRegistry;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// src/exec-kill-guard.ts
|
|
1582
|
+
var isWin = os3.platform() === "win32";
|
|
1583
|
+
async function checkExecKillCommand(cmd, args) {
|
|
1584
|
+
if (!cmd) return { blocked: false };
|
|
1585
|
+
const cmdLower = cmd.toLowerCase().trim();
|
|
1586
|
+
const fullCommand = [cmdLower, ...args].join(" ").replace(/\s+/g, " ").trim();
|
|
1587
|
+
if (isWin) {
|
|
1588
|
+
if (cmdLower === "taskkill" || cmdLower === "taskkill.exe") {
|
|
1589
|
+
const hasForce = args.some((a) => a.toUpperCase() === "/F" || a.toUpperCase() === "-F");
|
|
1590
|
+
const signal = hasForce ? "FORCE" : "TERM";
|
|
1591
|
+
for (let i = 0; i < args.length; i++) {
|
|
1592
|
+
const a = args[i];
|
|
1593
|
+
if (a.toUpperCase() === "/IM" || a.toUpperCase() === "-IM") {
|
|
1594
|
+
const nameArg = args[i + 1];
|
|
1595
|
+
if (nameArg) {
|
|
1596
|
+
const result = await checkKillTarget({ name: nameArg, signal, cmd: fullCommand });
|
|
1597
|
+
if (result.blocked) return result;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
for (let i = 0; i < args.length; i++) {
|
|
1602
|
+
const a = args[i];
|
|
1603
|
+
if (a.toUpperCase() === "/PID" || a.toUpperCase() === "-PID") {
|
|
1604
|
+
const pidArg = args[i + 1];
|
|
1605
|
+
if (pidArg && /^\d+$/.test(pidArg)) {
|
|
1606
|
+
const result = await checkKillTarget({
|
|
1607
|
+
pid: parseInt(pidArg, 10),
|
|
1608
|
+
signal,
|
|
1609
|
+
cmd: fullCommand
|
|
1610
|
+
});
|
|
1611
|
+
if (result.blocked) return result;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
for (let i = 0; i < args.length; i++) {
|
|
1616
|
+
const a = args[i];
|
|
1617
|
+
if (a.toUpperCase() === "/FI" || a.toUpperCase() === "-FI") {
|
|
1618
|
+
const filterArg = args[i + 1];
|
|
1619
|
+
if (filterArg) {
|
|
1620
|
+
const nameMatch = filterArg.match(/IMAGENAME\s+eq\s+"?([^"\s]+)/i);
|
|
1621
|
+
if (nameMatch?.[1]) {
|
|
1622
|
+
const result = await checkKillTarget({ name: nameMatch[1], signal, cmd: fullCommand });
|
|
1623
|
+
if (result.blocked) return result;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return { blocked: false };
|
|
1629
|
+
}
|
|
1630
|
+
if (cmdLower === "powershell" || cmdLower === "powershell.exe" || cmdLower === "pwsh" || cmdLower === "pwsh.exe" || cmdLower === "cmd" || cmdLower === "cmd.exe") {
|
|
1631
|
+
const shellFlagIndex = args.findIndex((arg) => {
|
|
1632
|
+
const lower = arg.toLowerCase();
|
|
1633
|
+
return lower === "-c" || lower === "-command" || lower === "/c";
|
|
1634
|
+
});
|
|
1635
|
+
if (shellFlagIndex >= 0) {
|
|
1636
|
+
const innerTokens = tokenizeShellCommand(args.slice(shellFlagIndex + 1).join(" "));
|
|
1637
|
+
const innerCommand = innerTokens[0];
|
|
1638
|
+
if (innerCommand) {
|
|
1639
|
+
const result = await checkExecKillCommand(innerCommand, innerTokens.slice(1));
|
|
1640
|
+
if (result.blocked) return result;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
if (cmdLower === "stop-process" || cmdLower === "kill") {
|
|
1645
|
+
for (let i = 0; i < args.length; i++) {
|
|
1646
|
+
const a = args[i];
|
|
1647
|
+
if (a === "-Name" || a === "-n") {
|
|
1648
|
+
const nameArg = args[i + 1]?.replace(/^['"]|['"]$/g, "");
|
|
1649
|
+
if (nameArg) {
|
|
1650
|
+
const result = await checkKillTarget({
|
|
1651
|
+
name: nameArg,
|
|
1652
|
+
signal: "FORCE",
|
|
1653
|
+
cmd: fullCommand
|
|
1654
|
+
});
|
|
1655
|
+
if (result.blocked) return result;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
if (a === "-Id" || a === "-PID" || a === "-pid") {
|
|
1659
|
+
const pidArg = args[i + 1];
|
|
1660
|
+
if (pidArg && /^\d+$/.test(pidArg)) {
|
|
1661
|
+
const result = await checkKillTarget({
|
|
1662
|
+
pid: parseInt(pidArg, 10),
|
|
1663
|
+
signal: "FORCE",
|
|
1664
|
+
cmd: fullCommand
|
|
1665
|
+
});
|
|
1666
|
+
if (result.blocked) return result;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
const firstNonFlag = args.find((a) => !a.startsWith("-"));
|
|
1671
|
+
if (firstNonFlag) {
|
|
1672
|
+
const name = firstNonFlag.replace(/^['"]|['"]$/g, "");
|
|
1673
|
+
const result = await checkKillTarget({ name, signal: "TERM", cmd: fullCommand });
|
|
1674
|
+
if (result.blocked) return result;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
if (cmdLower === "wmic" || cmdLower === "wmic.exe") {
|
|
1678
|
+
const joined = args.join(" ").toLowerCase();
|
|
1679
|
+
if (/\bprocess\b/.test(joined) && /\bdelete\b/.test(joined)) {
|
|
1680
|
+
const nameMatch = joined.match(/name\s*=\s*['"]?([^'"]+)/);
|
|
1681
|
+
if (nameMatch?.[1]) {
|
|
1682
|
+
const result = await checkKillTarget({
|
|
1683
|
+
name: nameMatch[1].trim(),
|
|
1684
|
+
signal: "FORCE",
|
|
1685
|
+
cmd: fullCommand
|
|
1686
|
+
});
|
|
1687
|
+
if (result.blocked) return result;
|
|
1688
|
+
}
|
|
1689
|
+
return {
|
|
1690
|
+
blocked: true,
|
|
1691
|
+
reason: "Blocked: wmic process delete targets all matched processes \u2014 would include protected WrongStack processes."
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
if (cmdLower === "node" || cmdLower === "node.exe") {
|
|
1696
|
+
if (args.includes("-e") || args.includes("--eval")) {
|
|
1697
|
+
const evalIdx = args.indexOf("-e") !== -1 ? args.indexOf("-e") : args.indexOf("--eval");
|
|
1698
|
+
const evalCode = args[evalIdx + 1] ?? "";
|
|
1699
|
+
if (/\bprocess\.kill\s*\(/.test(evalCode)) {
|
|
1700
|
+
const pidMatch = evalCode.match(/process\.kill\s*\(\s*(\d+)/);
|
|
1701
|
+
if (pidMatch?.[1]) {
|
|
1702
|
+
const pid = parseInt(pidMatch[1], 10);
|
|
1703
|
+
const result = await checkKillTarget({ pid, signal: "SIGTERM", cmd: fullCommand });
|
|
1704
|
+
if (result.blocked) return result;
|
|
1705
|
+
}
|
|
1706
|
+
return {
|
|
1707
|
+
blocked: true,
|
|
1708
|
+
reason: "Blocked: node -e with process.kill() \u2014 would target protected WrongStack process(es)."
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
} else {
|
|
1714
|
+
if (cmdLower === "kill") {
|
|
1715
|
+
for (const a of args) {
|
|
1716
|
+
const num = a.replace(/^-/, "");
|
|
1717
|
+
if (/^\d+$/.test(num)) {
|
|
1718
|
+
const pid = parseInt(num, 10);
|
|
1719
|
+
const result = await checkKillTarget({ pid, signal: "SIGTERM", cmd: fullCommand });
|
|
1720
|
+
if (result.blocked) return result;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
if (cmdLower === "pkill" || cmdLower === "killall") {
|
|
1725
|
+
const firstNonFlag = args.find((a) => !a.startsWith("-"));
|
|
1726
|
+
if (firstNonFlag) {
|
|
1727
|
+
const result = await checkKillTarget({
|
|
1728
|
+
name: firstNonFlag,
|
|
1729
|
+
signal: "SIGTERM",
|
|
1730
|
+
cmd: fullCommand
|
|
1731
|
+
});
|
|
1732
|
+
if (result.blocked) return result;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return { blocked: false };
|
|
1737
|
+
}
|
|
1738
|
+
function tokenizeShellCommand(command) {
|
|
1739
|
+
const tokens = [];
|
|
1740
|
+
let current = "";
|
|
1741
|
+
let quote = null;
|
|
1742
|
+
for (const char of command.trim()) {
|
|
1743
|
+
if (quote) {
|
|
1744
|
+
if (char === quote) quote = null;
|
|
1745
|
+
else current += char;
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
if (char === '"' || char === "'") {
|
|
1749
|
+
quote = char;
|
|
1750
|
+
} else if (/\s/.test(char)) {
|
|
1751
|
+
if (current) {
|
|
1752
|
+
tokens.push(current);
|
|
1753
|
+
current = "";
|
|
1754
|
+
}
|
|
1755
|
+
} else {
|
|
1756
|
+
current += char;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
if (current) tokens.push(current);
|
|
1760
|
+
return tokens;
|
|
1761
|
+
}
|
|
1762
|
+
async function checkKillTarget(target) {
|
|
1763
|
+
const registry = getPersistentProcessRegistry();
|
|
1764
|
+
if (target.pid !== void 0) {
|
|
1765
|
+
const blocked = await registry.shouldBlockKill(target.pid);
|
|
1766
|
+
if (blocked) {
|
|
1767
|
+
return {
|
|
1768
|
+
blocked: true,
|
|
1769
|
+
reason: `Blocked: kill ${target.signal} PID ${target.pid} targets a protected WrongStack process (${target.cmd.slice(0, 80)}).`
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
if (target.pid === process.pid) {
|
|
1773
|
+
return {
|
|
1774
|
+
blocked: true,
|
|
1775
|
+
reason: "Blocked: cannot kill the current WrongStack process."
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
if (target.pid === process.ppid) {
|
|
1779
|
+
return {
|
|
1780
|
+
blocked: true,
|
|
1781
|
+
reason: "Blocked: cannot kill the parent terminal hosting WrongStack."
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
return { blocked: false };
|
|
1785
|
+
}
|
|
1786
|
+
if (target.name) {
|
|
1787
|
+
const nameLower = target.name.toLowerCase().replace(/\.exe$/, "");
|
|
1788
|
+
if (nameLower.includes("wrongstack")) {
|
|
1789
|
+
return {
|
|
1790
|
+
blocked: true,
|
|
1791
|
+
reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
const currentImage = path5.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
|
|
1795
|
+
const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
|
|
1796
|
+
if (targetsNodeRuntime && currentImage === "node") {
|
|
1797
|
+
return {
|
|
1798
|
+
blocked: true,
|
|
1799
|
+
reason: `Blocked: kill ${target.signal} '${target.name}' would kill the active WrongStack node.exe runtime.`
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
const protectedPids = await registry.getAllProtectedPids();
|
|
1803
|
+
if (protectedPids.length > 0 && targetsNodeRuntime) {
|
|
1804
|
+
return {
|
|
1805
|
+
blocked: true,
|
|
1806
|
+
reason: `Blocked: kill ${target.signal} '${target.name}' would kill all node.exe processes including active WrongStack instance(s).`
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
return { blocked: false };
|
|
1810
|
+
}
|
|
1811
|
+
return { blocked: false };
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1167
1814
|
// src/exec.ts
|
|
1168
|
-
var
|
|
1815
|
+
var isWin2 = process.platform === "win32";
|
|
1169
1816
|
var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
1170
1817
|
// JS / TS toolchain
|
|
1171
1818
|
"node",
|
|
@@ -1970,6 +2617,19 @@ var execTool = {
|
|
|
1970
2617
|
const args = (input.args ?? []).slice(0, MAX_ARGS);
|
|
1971
2618
|
const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS));
|
|
1972
2619
|
const danger = detectDanger(cmd, args, dangerBypass);
|
|
2620
|
+
const killCheck = await checkExecKillCommand(cmd, args);
|
|
2621
|
+
if (killCheck.blocked) {
|
|
2622
|
+
return {
|
|
2623
|
+
command: cmd,
|
|
2624
|
+
args,
|
|
2625
|
+
stdout: "",
|
|
2626
|
+
stderr: killCheck.reason ?? "Kill command blocked: targets a protected WrongStack process.",
|
|
2627
|
+
exitCode: 1,
|
|
2628
|
+
truncated: false,
|
|
2629
|
+
allowed: false,
|
|
2630
|
+
danger
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
1973
2633
|
const argError = validateArgs(cmd, args);
|
|
1974
2634
|
if (argError) {
|
|
1975
2635
|
return {
|
|
@@ -2020,7 +2680,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
2020
2680
|
let timedOut = false;
|
|
2021
2681
|
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT });
|
|
2022
2682
|
const resolved = resolveWin32Command(cmd);
|
|
2023
|
-
const needsShell =
|
|
2683
|
+
const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
2024
2684
|
const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
|
|
2025
2685
|
const spawnCmd = shim?.command ?? resolved;
|
|
2026
2686
|
const spawnArgs = shim?.args ?? args;
|
|
@@ -2045,7 +2705,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
2045
2705
|
env: buildChildEnv(sessionId),
|
|
2046
2706
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2047
2707
|
windowsHide: true,
|
|
2048
|
-
...
|
|
2708
|
+
...isWin2 ? {} : { signal },
|
|
2049
2709
|
...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
|
|
2050
2710
|
});
|
|
2051
2711
|
} catch (err) {
|
|
@@ -2063,7 +2723,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
2063
2723
|
command: cmd,
|
|
2064
2724
|
args,
|
|
2065
2725
|
stdout: "",
|
|
2066
|
-
stderr: `spawn failed: ${
|
|
2726
|
+
stderr: `spawn failed: ${toErrorMessage2(err)}`,
|
|
2067
2727
|
exitCode: 1,
|
|
2068
2728
|
truncated: false,
|
|
2069
2729
|
allowed: true,
|
|
@@ -2084,7 +2744,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
2084
2744
|
const isAbort = err && err.code === "ABORT_ERR";
|
|
2085
2745
|
const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
|
|
2086
2746
|
clearTimeout(timer);
|
|
2087
|
-
if (
|
|
2747
|
+
if (isWin2) signal.removeEventListener("abort", onAbort);
|
|
2088
2748
|
if (typeof pid === "number") registry.unregister(pid);
|
|
2089
2749
|
registry.afterCall(Date.now() - startedAt, true);
|
|
2090
2750
|
spool.finalize();
|
|
@@ -2117,7 +2777,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
2117
2777
|
if (typeof pid === "number") registry.kill(pid, { force: true });
|
|
2118
2778
|
else child.kill("SIGTERM");
|
|
2119
2779
|
};
|
|
2120
|
-
if (
|
|
2780
|
+
if (isWin2) {
|
|
2121
2781
|
if (signal.aborted) onAbort();
|
|
2122
2782
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2123
2783
|
}
|
|
@@ -2137,7 +2797,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
2137
2797
|
});
|
|
2138
2798
|
child.on("close", (code) => {
|
|
2139
2799
|
clearTimeout(timer);
|
|
2140
|
-
if (
|
|
2800
|
+
if (isWin2) signal.removeEventListener("abort", onAbort);
|
|
2141
2801
|
if (typeof pid === "number") registry.unregister(pid);
|
|
2142
2802
|
const durationMs = Date.now() - startedAt;
|
|
2143
2803
|
const exitCode = killed ? 124 : code ?? 1;
|