@wrongstack/tools 0.274.0 → 0.275.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/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/pack.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, execFileSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
3
|
+
import { buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import { statSync, mkdirSync, createWriteStream } from 'node:fs';
|
|
6
6
|
import * as fs2 from 'node:fs/promises';
|
|
@@ -280,6 +280,7 @@ var CircuitBreaker = class {
|
|
|
280
280
|
if (this.state === "open") return;
|
|
281
281
|
this.state = "open";
|
|
282
282
|
this.openedAt = Date.now();
|
|
283
|
+
this.window = [];
|
|
283
284
|
try {
|
|
284
285
|
this.onTrip?.();
|
|
285
286
|
} catch {
|
|
@@ -319,7 +320,7 @@ var SENSITIVE_FLAG_PATTERNS = [
|
|
|
319
320
|
/--(?: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,
|
|
320
321
|
// -f "value" style short flags
|
|
321
322
|
/(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
|
|
322
|
-
/(?<!\w)-
|
|
323
|
+
/(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
|
|
323
324
|
// env var–style secrets: TOKEN=x, API_KEY=y, etc.
|
|
324
325
|
/(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
|
|
325
326
|
// Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
|
|
@@ -380,12 +381,35 @@ var ProcessRegistryImpl = class {
|
|
|
380
381
|
register(info) {
|
|
381
382
|
this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
|
|
382
383
|
}
|
|
384
|
+
_isSafeSignalPid(pid) {
|
|
385
|
+
return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
|
|
386
|
+
}
|
|
387
|
+
_canSignalProcessGroup(p) {
|
|
388
|
+
return os2.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
|
|
389
|
+
}
|
|
390
|
+
_killChildDirect(p, signal) {
|
|
391
|
+
try {
|
|
392
|
+
p.child.kill(signal);
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
_killPosix(p, signal) {
|
|
397
|
+
if (this._canSignalProcessGroup(p)) {
|
|
398
|
+
try {
|
|
399
|
+
process.kill(-p.pid, signal);
|
|
400
|
+
return;
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
this._killChildDirect(p, signal);
|
|
405
|
+
}
|
|
383
406
|
/** Unregister a process by PID. Called on 'close' / 'exit' events. */
|
|
384
407
|
unregister(pid) {
|
|
385
408
|
this.processes.delete(pid);
|
|
386
409
|
}
|
|
387
410
|
/** Get a single process by PID. */
|
|
388
411
|
get(pid) {
|
|
412
|
+
this._pruneStale(pid);
|
|
389
413
|
return this.processes.get(pid);
|
|
390
414
|
}
|
|
391
415
|
/** Get all tracked processes. */
|
|
@@ -549,13 +573,14 @@ var ProcessRegistryImpl = class {
|
|
|
549
573
|
* Returns true if the process was found and kill was attempted.
|
|
550
574
|
*/
|
|
551
575
|
kill(pid, opts = {}) {
|
|
576
|
+
this._pruneStale(pid);
|
|
552
577
|
const p = this.processes.get(pid);
|
|
553
578
|
if (!p) return false;
|
|
554
579
|
if (p.killed) return true;
|
|
555
580
|
if (p.protected) return false;
|
|
556
581
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
557
|
-
const
|
|
558
|
-
if (
|
|
582
|
+
const isWin4 = os2.platform() === "win32";
|
|
583
|
+
if (isWin4) {
|
|
559
584
|
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
560
585
|
if (liveRealChild && killWin32Tree(pid)) {
|
|
561
586
|
const fallback = setTimeout(() => {
|
|
@@ -578,27 +603,12 @@ var ProcessRegistryImpl = class {
|
|
|
578
603
|
}
|
|
579
604
|
try {
|
|
580
605
|
if (force) {
|
|
581
|
-
|
|
582
|
-
process.kill(-pid, "SIGKILL");
|
|
583
|
-
} catch {
|
|
584
|
-
p.child.kill("SIGKILL");
|
|
585
|
-
}
|
|
606
|
+
this._killPosix(p, "SIGKILL");
|
|
586
607
|
} else {
|
|
587
|
-
|
|
588
|
-
process.kill(-pid, "SIGTERM");
|
|
589
|
-
} catch {
|
|
590
|
-
p.child.kill("SIGTERM");
|
|
591
|
-
}
|
|
608
|
+
this._killPosix(p, "SIGTERM");
|
|
592
609
|
const timer = setTimeout(() => {
|
|
593
610
|
if (this.processes.has(pid) && !p.child.killed) {
|
|
594
|
-
|
|
595
|
-
process.kill(-pid, "SIGKILL");
|
|
596
|
-
} catch {
|
|
597
|
-
try {
|
|
598
|
-
p.child.kill("SIGKILL");
|
|
599
|
-
} catch {
|
|
600
|
-
}
|
|
601
|
-
}
|
|
611
|
+
this._killPosix(p, "SIGKILL");
|
|
602
612
|
}
|
|
603
613
|
}, graceMs);
|
|
604
614
|
timer.unref?.();
|
|
@@ -633,6 +643,34 @@ var ProcessRegistryImpl = class {
|
|
|
633
643
|
}
|
|
634
644
|
return killed;
|
|
635
645
|
}
|
|
646
|
+
/**
|
|
647
|
+
* Check whether a tracked process entry is stale — the child has exited
|
|
648
|
+
* (exitCode !== null) AND it's been in the registry long enough that the
|
|
649
|
+
* OS may have reused the PID for a new, unrelated process.
|
|
650
|
+
*
|
|
651
|
+
* P3 #24 (before-release.md): on POSIX, PIDs are reused after process
|
|
652
|
+
* exit. If a tracked process exits but its 'close' event hasn't fired yet
|
|
653
|
+
* (or was missed), the registry still holds the entry. A new process
|
|
654
|
+
* gets the same PID, and PID-based lookups (get, kill, shouldBlockKill)
|
|
655
|
+
* may incorrectly protect or target the wrong process.
|
|
656
|
+
*
|
|
657
|
+
* The 60s threshold is conservative — the OS typically waits much longer
|
|
658
|
+
* before reusing a PID, but we want to clean up before that becomes a risk.
|
|
659
|
+
*/
|
|
660
|
+
_isStaleEntry(entry) {
|
|
661
|
+
return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Remove a stale entry for a specific PID before any PID-based lookup.
|
|
665
|
+
* This prevents PID reuse from causing the registry to act on a dead
|
|
666
|
+
* process that has been replaced by a new one with the same PID.
|
|
667
|
+
*/
|
|
668
|
+
_pruneStale(pid) {
|
|
669
|
+
const entry = this.processes.get(pid);
|
|
670
|
+
if (entry && this._isStaleEntry(entry)) {
|
|
671
|
+
this.processes.delete(pid);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
636
674
|
};
|
|
637
675
|
var _registry;
|
|
638
676
|
function getProcessRegistry() {
|
|
@@ -1506,17 +1544,18 @@ function getPersistentProcessRegistry() {
|
|
|
1506
1544
|
}
|
|
1507
1545
|
|
|
1508
1546
|
// src/bash-kill-guard.ts
|
|
1547
|
+
var isWin2 = os2.platform() === "win32";
|
|
1509
1548
|
function extractKillCommand(command) {
|
|
1510
1549
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1511
1550
|
const shellCMatch = normalized.match(
|
|
1512
|
-
/^(
|
|
1551
|
+
/^(?:\S+(?:\s+\S+)?)?\s+-c\s+['"](.+?)['"]$/
|
|
1513
1552
|
);
|
|
1514
1553
|
if (shellCMatch?.[1]) {
|
|
1515
1554
|
const inner = shellCMatch[1].trim();
|
|
1516
1555
|
return isKillRelatedCommand(inner) ? inner : null;
|
|
1517
1556
|
}
|
|
1518
1557
|
const shellCUnquoted = normalized.match(
|
|
1519
|
-
/^(
|
|
1558
|
+
/^(?:\S+(?:\s+\S+)?)?\s+-c\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
|
|
1520
1559
|
);
|
|
1521
1560
|
if (shellCUnquoted?.[1]) {
|
|
1522
1561
|
return shellCUnquoted[1];
|
|
@@ -1525,15 +1564,43 @@ function extractKillCommand(command) {
|
|
|
1525
1564
|
}
|
|
1526
1565
|
function isKillRelatedCommand(cmd) {
|
|
1527
1566
|
const normalized = cmd.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1567
|
+
if (isWin2) {
|
|
1568
|
+
if (/^taskkill\s/i.test(normalized)) return true;
|
|
1569
|
+
if (/^tskill\s/i.test(normalized)) return true;
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1528
1572
|
if (/^kill(\s|$)/.test(normalized)) return true;
|
|
1529
1573
|
if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
|
|
1530
|
-
if (/^taskkill\s/i.test(normalized)) return true;
|
|
1531
|
-
if (/^tskill\s/i.test(normalized)) return true;
|
|
1532
1574
|
if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
|
|
1533
1575
|
return false;
|
|
1534
1576
|
}
|
|
1535
1577
|
function parseKillCommand(command) {
|
|
1536
1578
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
1579
|
+
if (isWin2) {
|
|
1580
|
+
const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
|
|
1581
|
+
if (taskkillMatch?.[1]) {
|
|
1582
|
+
const pidStr = taskkillMatch[1];
|
|
1583
|
+
return {
|
|
1584
|
+
pid: parseInt(pidStr, 10),
|
|
1585
|
+
signal: normalized.includes("/F") ? "FORCE" : "TERM",
|
|
1586
|
+
isGroupKill: false,
|
|
1587
|
+
isAllKill: false,
|
|
1588
|
+
originalCommand: command
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
|
|
1592
|
+
if (tskillMatch?.[1]) {
|
|
1593
|
+
const pidStr = tskillMatch[1];
|
|
1594
|
+
return {
|
|
1595
|
+
pid: parseInt(pidStr, 10),
|
|
1596
|
+
signal: "TERM",
|
|
1597
|
+
isGroupKill: false,
|
|
1598
|
+
isAllKill: false,
|
|
1599
|
+
originalCommand: command
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
return null;
|
|
1603
|
+
}
|
|
1537
1604
|
const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
|
|
1538
1605
|
if (simpleMatch) {
|
|
1539
1606
|
const signal = simpleMatch[1] ?? "-TERM";
|
|
@@ -1577,28 +1644,6 @@ function parseKillCommand(command) {
|
|
|
1577
1644
|
if (pgrepMatch) {
|
|
1578
1645
|
return null;
|
|
1579
1646
|
}
|
|
1580
|
-
const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
|
|
1581
|
-
if (taskkillMatch?.[1]) {
|
|
1582
|
-
const pidStr = taskkillMatch[1];
|
|
1583
|
-
return {
|
|
1584
|
-
pid: parseInt(pidStr, 10),
|
|
1585
|
-
signal: normalized.includes("/F") ? "FORCE" : "TERM",
|
|
1586
|
-
isGroupKill: false,
|
|
1587
|
-
isAllKill: false,
|
|
1588
|
-
originalCommand: command
|
|
1589
|
-
};
|
|
1590
|
-
}
|
|
1591
|
-
const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
|
|
1592
|
-
if (tskillMatch?.[1]) {
|
|
1593
|
-
const pidStr = tskillMatch[1];
|
|
1594
|
-
return {
|
|
1595
|
-
pid: parseInt(pidStr, 10),
|
|
1596
|
-
signal: "TERM",
|
|
1597
|
-
isGroupKill: false,
|
|
1598
|
-
isAllKill: false,
|
|
1599
|
-
originalCommand: command
|
|
1600
|
-
};
|
|
1601
|
-
}
|
|
1602
1647
|
return null;
|
|
1603
1648
|
}
|
|
1604
1649
|
async function getProtectedEntries() {
|
|
@@ -1675,7 +1720,7 @@ async function checkAndBlockKillCommand(command) {
|
|
|
1675
1720
|
}
|
|
1676
1721
|
return { blocked: false };
|
|
1677
1722
|
}
|
|
1678
|
-
function pickShell(
|
|
1723
|
+
function pickShell(platform4, command, env) {
|
|
1679
1724
|
const override = env.get("WRONGSTACK_SHELL")?.trim().toLowerCase();
|
|
1680
1725
|
if (override === "cmd" || override === "cmd.exe") return "cmd";
|
|
1681
1726
|
if (override === "powershell" || override === "powershell.exe") return "powershell";
|
|
@@ -1729,7 +1774,7 @@ function looksLikePowerShellExtended(command) {
|
|
|
1729
1774
|
}
|
|
1730
1775
|
function wrapPowerShellScript(command) {
|
|
1731
1776
|
const bootstrap = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;$ConfirmPreference='None';$WhatIfPreference=$false";
|
|
1732
|
-
return
|
|
1777
|
+
return bootstrap + "\n$ErrorActionPreference='Stop'\n" + command + "\nif ($LASTEXITCODE -is [int]) { exit $LASTEXITCODE }";
|
|
1733
1778
|
}
|
|
1734
1779
|
var PS_VERB_RE = new RegExp(
|
|
1735
1780
|
// Boundaries: start-of-string, whitespace, `;`, `&`, `|`, `(`, `{`, `,`.
|
|
@@ -1874,10 +1919,10 @@ var bashTool = {
|
|
|
1874
1919
|
}));
|
|
1875
1920
|
}
|
|
1876
1921
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
|
|
1877
|
-
const
|
|
1922
|
+
const isWin4 = os2.platform() === "win32";
|
|
1878
1923
|
let plan;
|
|
1879
1924
|
let winShellKind;
|
|
1880
|
-
if (
|
|
1925
|
+
if (isWin4) {
|
|
1881
1926
|
const shell2 = pickShell("win32", input.command, {
|
|
1882
1927
|
get: (k) => process.env[k]
|
|
1883
1928
|
});
|
|
@@ -1906,7 +1951,7 @@ var bashTool = {
|
|
|
1906
1951
|
const shell = plan.bin;
|
|
1907
1952
|
const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
|
|
1908
1953
|
const env = buildChildEnv(ctx.session?.id);
|
|
1909
|
-
const detached = !
|
|
1954
|
+
const detached = !isWin4;
|
|
1910
1955
|
const startedAt = Date.now();
|
|
1911
1956
|
if (input.background) {
|
|
1912
1957
|
let buf2 = "";
|
|
@@ -1924,7 +1969,7 @@ var bashTool = {
|
|
|
1924
1969
|
// apply: the child gets a hidden console that grandchildren inherit.
|
|
1925
1970
|
// Windows children survive parent exit either way. POSIX keeps
|
|
1926
1971
|
// detached for the process-group kill semantics.
|
|
1927
|
-
detached: !
|
|
1972
|
+
detached: !isWin4,
|
|
1928
1973
|
windowsHide: true
|
|
1929
1974
|
});
|
|
1930
1975
|
if (plan.useStdin) {
|
|
@@ -1942,7 +1987,8 @@ var bashTool = {
|
|
|
1942
1987
|
command: redactCommand(input.command),
|
|
1943
1988
|
startedAt: Date.now(),
|
|
1944
1989
|
sessionId: ctx.session?.id,
|
|
1945
|
-
child: child2
|
|
1990
|
+
child: child2,
|
|
1991
|
+
processGroupLeader: detached && child2.pid === pid2
|
|
1946
1992
|
});
|
|
1947
1993
|
child2.on("close", () => registry.unregister(pid2));
|
|
1948
1994
|
}
|
|
@@ -1983,6 +2029,14 @@ var bashTool = {
|
|
|
1983
2029
|
pid: pid2
|
|
1984
2030
|
}
|
|
1985
2031
|
};
|
|
2032
|
+
ctx.recordSideEffect?.({
|
|
2033
|
+
toolUseId: `bash-bg-${Date.now()}`,
|
|
2034
|
+
toolName: "bash",
|
|
2035
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2036
|
+
input: { command: redactCommand(input.command), background: true },
|
|
2037
|
+
outcome: `launched (pid ${pid2 ?? "unknown"})`,
|
|
2038
|
+
risk: "shell"
|
|
2039
|
+
});
|
|
1986
2040
|
return;
|
|
1987
2041
|
}
|
|
1988
2042
|
const child = spawn(shell, args, {
|
|
@@ -1993,7 +2047,7 @@ var bashTool = {
|
|
|
1993
2047
|
stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
1994
2048
|
detached,
|
|
1995
2049
|
windowsHide: true,
|
|
1996
|
-
...
|
|
2050
|
+
...isWin4 ? {} : { signal: opts.signal }
|
|
1997
2051
|
});
|
|
1998
2052
|
if (plan.useStdin) {
|
|
1999
2053
|
try {
|
|
@@ -2010,7 +2064,8 @@ var bashTool = {
|
|
|
2010
2064
|
command: redactCommand(input.command),
|
|
2011
2065
|
startedAt: Date.now(),
|
|
2012
2066
|
sessionId: ctx.session?.id,
|
|
2013
|
-
child
|
|
2067
|
+
child,
|
|
2068
|
+
processGroupLeader: detached && child.pid === pid
|
|
2014
2069
|
});
|
|
2015
2070
|
}
|
|
2016
2071
|
let buf = "";
|
|
@@ -2019,7 +2074,7 @@ var bashTool = {
|
|
|
2019
2074
|
const timers = [];
|
|
2020
2075
|
const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
|
|
2021
2076
|
function killWithTimeout(child2, timeoutMs2) {
|
|
2022
|
-
if (
|
|
2077
|
+
if (isWin4) {
|
|
2023
2078
|
if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
|
|
2024
2079
|
const fallback = setTimeout(() => {
|
|
2025
2080
|
if (child2.exitCode === null) {
|
|
@@ -2039,34 +2094,22 @@ var bashTool = {
|
|
|
2039
2094
|
}
|
|
2040
2095
|
return;
|
|
2041
2096
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
} catch {
|
|
2047
|
-
child2.kill("SIGTERM");
|
|
2048
|
-
}
|
|
2049
|
-
} else {
|
|
2097
|
+
if (typeof child2.pid === "number") {
|
|
2098
|
+
registry.kill(child2.pid, { graceMs: timeoutMs2 });
|
|
2099
|
+
} else {
|
|
2100
|
+
try {
|
|
2050
2101
|
child2.kill("SIGTERM");
|
|
2102
|
+
} catch {
|
|
2051
2103
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
const killTimer = setTimeout(() => {
|
|
2055
|
-
try {
|
|
2056
|
-
if (typeof child2.pid === "number") {
|
|
2057
|
-
try {
|
|
2058
|
-
process.kill(-child2.pid, "SIGKILL");
|
|
2059
|
-
} catch {
|
|
2060
|
-
child2.kill("SIGKILL");
|
|
2061
|
-
}
|
|
2062
|
-
} else {
|
|
2104
|
+
const killTimer = setTimeout(() => {
|
|
2105
|
+
try {
|
|
2063
2106
|
child2.kill("SIGKILL");
|
|
2107
|
+
} catch {
|
|
2064
2108
|
}
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
killTimer.unref?.();
|
|
2109
|
+
}, timeoutMs2);
|
|
2110
|
+
timers.push(killTimer);
|
|
2111
|
+
killTimer.unref?.();
|
|
2112
|
+
}
|
|
2070
2113
|
}
|
|
2071
2114
|
const timer = setTimeout(() => {
|
|
2072
2115
|
timedOut = true;
|
|
@@ -2075,7 +2118,7 @@ var bashTool = {
|
|
|
2075
2118
|
timers.push(timer);
|
|
2076
2119
|
timer.unref?.();
|
|
2077
2120
|
const onAbort = () => killWithTimeout(child, 2e3);
|
|
2078
|
-
if (
|
|
2121
|
+
if (isWin4) {
|
|
2079
2122
|
if (opts.signal.aborted) onAbort();
|
|
2080
2123
|
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
2081
2124
|
}
|
|
@@ -2163,6 +2206,14 @@ ${hint}` : ""),
|
|
|
2163
2206
|
timed_out: timedOut
|
|
2164
2207
|
}
|
|
2165
2208
|
};
|
|
2209
|
+
ctx.recordSideEffect?.({
|
|
2210
|
+
toolUseId: `bash-${Date.now()}`,
|
|
2211
|
+
toolName: "bash",
|
|
2212
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2213
|
+
input: { command: redactCommand(input.command) },
|
|
2214
|
+
outcome: timedOut ? `timed out (exit ${c.code})` : `exit ${c.code}`,
|
|
2215
|
+
risk: "shell"
|
|
2216
|
+
});
|
|
2166
2217
|
return;
|
|
2167
2218
|
}
|
|
2168
2219
|
const now = Date.now();
|
|
@@ -2174,7 +2225,7 @@ ${hint}` : ""),
|
|
|
2174
2225
|
} finally {
|
|
2175
2226
|
for (const t of timers) clearTimeout(t);
|
|
2176
2227
|
spool.finalize();
|
|
2177
|
-
if (
|
|
2228
|
+
if (isWin4) opts.signal.removeEventListener("abort", onAbort);
|
|
2178
2229
|
child.stdout?.off("data", onData);
|
|
2179
2230
|
child.stderr?.off("data", onData);
|
|
2180
2231
|
child.stdout?.destroy();
|
|
@@ -2184,6 +2235,38 @@ ${hint}` : ""),
|
|
|
2184
2235
|
else killWithTimeout(child, 2e3);
|
|
2185
2236
|
}
|
|
2186
2237
|
}
|
|
2238
|
+
},
|
|
2239
|
+
/**
|
|
2240
|
+
* Tool-level teardown fired by `ToolExecutor.runToolCleanup()` when the
|
|
2241
|
+
* tool's run is aborted/timeout'd. The generator's `finally` block above
|
|
2242
|
+
* already force-kills the direct child, but that only runs if the
|
|
2243
|
+
* executor closes the async iterator (via `iter.return()`). When the
|
|
2244
|
+
* executor tears down without iterating — or a re-entrant abort races
|
|
2245
|
+
* with the generator — a bash-spawned process tree can survive in the
|
|
2246
|
+
* ProcessRegistry with `killed === false`, continuing to write files,
|
|
2247
|
+
* consume CPU, or hold inherited stdio pipes open for the rest of the
|
|
2248
|
+
* session.
|
|
2249
|
+
*
|
|
2250
|
+
* This is the defensive layer the executor calls via `tool.cleanup()`
|
|
2251
|
+
* (see `types/tool.ts`): kill every bash-owned process still tracked
|
|
2252
|
+
* for this session that hasn't exited yet. `registry.kill()` already
|
|
2253
|
+
* handles process-group / taskkill tree-kill and the SIGTERM→SIGKILL
|
|
2254
|
+
* grace window, so this just scopes the registry's existing kill path
|
|
2255
|
+
* to "this session's runaway bash children". Idempotent — a process
|
|
2256
|
+
* that already exited is skipped by `kill()` (it returns false), and a
|
|
2257
|
+
* `protected` infrastructure process (dev server the user intentionally
|
|
2258
|
+
* backgrounded) is left alone by design.
|
|
2259
|
+
*/
|
|
2260
|
+
async cleanup(_input, ctx) {
|
|
2261
|
+
const registry = getProcessRegistry();
|
|
2262
|
+
const sessionId = ctx.session?.id;
|
|
2263
|
+
if (!sessionId) return;
|
|
2264
|
+
for (const entry of registry.bySession(sessionId)) {
|
|
2265
|
+
if (entry.name !== "bash") continue;
|
|
2266
|
+
if (entry.child.exitCode !== null) continue;
|
|
2267
|
+
if (entry.protected) continue;
|
|
2268
|
+
registry.kill(entry.pid, { force: true });
|
|
2269
|
+
}
|
|
2187
2270
|
}
|
|
2188
2271
|
};
|
|
2189
2272
|
|
|
@@ -5134,6 +5217,229 @@ var codebaseStatsTool = {
|
|
|
5134
5217
|
};
|
|
5135
5218
|
}
|
|
5136
5219
|
};
|
|
5220
|
+
function normalizeOverrides(set) {
|
|
5221
|
+
const out = {};
|
|
5222
|
+
if (set && typeof set === "object") {
|
|
5223
|
+
for (const [k, v] of Object.entries(set)) {
|
|
5224
|
+
if (typeof v === "string") out[k] = v;
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
return out;
|
|
5228
|
+
}
|
|
5229
|
+
var designTool = {
|
|
5230
|
+
name: "design",
|
|
5231
|
+
category: "Design",
|
|
5232
|
+
description: 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: "list" (menu), "use" (load+pin a kit for a stack), "foundations" (baseline), "set" (override kit colors/tokens), "materialize" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), "verify" (scan UI files for off-palette colors).',
|
|
5233
|
+
usageHint: 'Flow: `design {action:"use", kit:"minimal-clarity", stack:"web"}` \u2192 optionally `design {action:"set", set:{primary:"oklch(62% 0.2 25)"}}` \u2192 `design {action:"materialize"}` to write tokens to disk \u2192 implement against them \u2192 `design {action:"verify"}`.',
|
|
5234
|
+
permission: "auto",
|
|
5235
|
+
mutating: false,
|
|
5236
|
+
capabilities: [],
|
|
5237
|
+
timeoutMs: 15e3,
|
|
5238
|
+
inputSchema: {
|
|
5239
|
+
type: "object",
|
|
5240
|
+
properties: {
|
|
5241
|
+
action: {
|
|
5242
|
+
type: "string",
|
|
5243
|
+
enum: ["list", "use", "foundations", "set", "materialize", "verify"],
|
|
5244
|
+
description: "list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; materialize = write tokens to a theme file; verify = scan UI for off-palette colors. Default: list."
|
|
5245
|
+
},
|
|
5246
|
+
kit: {
|
|
5247
|
+
type: "string",
|
|
5248
|
+
description: 'Kit id (required for "use"), e.g. "minimal-clarity", "neo-brutalist".'
|
|
5249
|
+
},
|
|
5250
|
+
stack: {
|
|
5251
|
+
type: "string",
|
|
5252
|
+
enum: ["web", "react-native", "flutter", "swiftui", "compose"],
|
|
5253
|
+
description: "Target stack \u2014 narrows guidance + materialize format. Default: web."
|
|
5254
|
+
},
|
|
5255
|
+
set: {
|
|
5256
|
+
type: "object",
|
|
5257
|
+
description: 'Token overrides for "set"/"use": { "primary": "oklch(\u2026)", "dark.bg": "#111" }. Bare key = both themes; "light."/"dark." prefix = that theme only. Empty value clears an override.',
|
|
5258
|
+
additionalProperties: { type: "string" }
|
|
5259
|
+
},
|
|
5260
|
+
out: {
|
|
5261
|
+
type: "string",
|
|
5262
|
+
description: "Materialize output path (project-relative). Defaults to a per-stack convention."
|
|
5263
|
+
},
|
|
5264
|
+
force: {
|
|
5265
|
+
type: "boolean",
|
|
5266
|
+
description: "Materialize: overwrite an existing file (default false \u2014 refuses to clobber)."
|
|
5267
|
+
},
|
|
5268
|
+
files: {
|
|
5269
|
+
type: "array",
|
|
5270
|
+
items: { type: "string" },
|
|
5271
|
+
description: "Verify: explicit project-relative files to scan. Default: a bounded UI-file walk."
|
|
5272
|
+
}
|
|
5273
|
+
},
|
|
5274
|
+
required: []
|
|
5275
|
+
},
|
|
5276
|
+
async execute(input, ctx) {
|
|
5277
|
+
const loader = getDesignKitLoader(ctx.projectRoot);
|
|
5278
|
+
const action = input.action ?? "list";
|
|
5279
|
+
const stack = input.stack && isDesignStack(input.stack) ? input.stack : void 0;
|
|
5280
|
+
if (action === "foundations") {
|
|
5281
|
+
const text = await loader.foundationsText(stack);
|
|
5282
|
+
return { action, stack, output: text || "No foundations document is installed." };
|
|
5283
|
+
}
|
|
5284
|
+
if (action === "use") {
|
|
5285
|
+
const kitId = input.kit?.trim();
|
|
5286
|
+
if (!kitId) {
|
|
5287
|
+
const menu2 = await loader.menuText();
|
|
5288
|
+
return { action, output: `No kit id provided.
|
|
5289
|
+
|
|
5290
|
+
${menu2}` };
|
|
5291
|
+
}
|
|
5292
|
+
const manifest = await loader.find(kitId);
|
|
5293
|
+
if (!manifest) {
|
|
5294
|
+
const menu2 = await loader.menuText();
|
|
5295
|
+
return { action, kit: kitId, output: `Kit "${kitId}" not found.
|
|
5296
|
+
|
|
5297
|
+
${menu2}` };
|
|
5298
|
+
}
|
|
5299
|
+
const resolvedStack = stack ?? manifest.stacks[0] ?? "web";
|
|
5300
|
+
const body = await loader.readBody(manifest.id, resolvedStack);
|
|
5301
|
+
const rawTokens = await loader.readTokens(manifest.id);
|
|
5302
|
+
const persisted = await loadActiveKit(ctx.projectRoot);
|
|
5303
|
+
const keepOverrides = persisted?.kit === manifest.id ? persisted.overrides ?? {} : {};
|
|
5304
|
+
const overrides = { ...keepOverrides, ...normalizeOverrides(input.set) };
|
|
5305
|
+
const tokens = rawTokens ? applyTokenOverrides(rawTokens, overrides) : rawTokens;
|
|
5306
|
+
setActiveKit(ctx, manifest.id, resolvedStack, overrides);
|
|
5307
|
+
await recordKitChoice(
|
|
5308
|
+
ctx.projectRoot,
|
|
5309
|
+
manifest.id,
|
|
5310
|
+
resolvedStack,
|
|
5311
|
+
"design-tool",
|
|
5312
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
5313
|
+
Object.keys(overrides).length ? overrides : void 0
|
|
5314
|
+
);
|
|
5315
|
+
const ovLine = Object.keys(overrides).length ? `
|
|
5316
|
+
Active color overrides: ${Object.entries(overrides).map(([k, v]) => `${k}=${v}`).join(", ")}
|
|
5317
|
+
` : "";
|
|
5318
|
+
const header = `# Active design kit: ${manifest.name} (${manifest.id}) \u2014 stack: ${resolvedStack}
|
|
5319
|
+
${manifest.aesthetic}
|
|
5320
|
+
${ovLine}
|
|
5321
|
+
Implement the UI faithfully to this spec. Keep light/dark, responsive, and WCAG AA.
|
|
5322
|
+
`;
|
|
5323
|
+
const tokenBlock = tokens ? `
|
|
5324
|
+
## Token snapshot (overrides applied)
|
|
5325
|
+
\`\`\`json
|
|
5326
|
+
${JSON.stringify(tokens, null, 2)}
|
|
5327
|
+
\`\`\`
|
|
5328
|
+
Tip: run \`design {action:"materialize"}\` to write these tokens to a real theme file.
|
|
5329
|
+
` : "";
|
|
5330
|
+
return {
|
|
5331
|
+
action,
|
|
5332
|
+
kit: manifest.id,
|
|
5333
|
+
stack: resolvedStack,
|
|
5334
|
+
output: `${header}${tokenBlock}
|
|
5335
|
+
${body}`
|
|
5336
|
+
};
|
|
5337
|
+
}
|
|
5338
|
+
if (action === "set") {
|
|
5339
|
+
const patch = normalizeOverrides(input.set);
|
|
5340
|
+
if (Object.keys(patch).length === 0) {
|
|
5341
|
+
return { action, output: 'No overrides given. Pass set:{ "primary": "oklch(\u2026)" }.' };
|
|
5342
|
+
}
|
|
5343
|
+
const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
|
|
5344
|
+
if (!merged) {
|
|
5345
|
+
return {
|
|
5346
|
+
action,
|
|
5347
|
+
output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
|
|
5348
|
+
};
|
|
5349
|
+
}
|
|
5350
|
+
setDesignOverrides(ctx, merged);
|
|
5351
|
+
return {
|
|
5352
|
+
action,
|
|
5353
|
+
output: `Overrides updated. Active overrides: ${Object.entries(merged).map(([k, v]) => `${k}=${v}`).join(", ")}
|
|
5354
|
+
These win over kit tokens. Run \`design {action:"materialize"}\` to write them to a theme file.`
|
|
5355
|
+
};
|
|
5356
|
+
}
|
|
5357
|
+
if (action === "materialize") {
|
|
5358
|
+
const active = await loadActiveKit(ctx.projectRoot);
|
|
5359
|
+
if (!active) {
|
|
5360
|
+
return {
|
|
5361
|
+
action,
|
|
5362
|
+
output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
|
|
5363
|
+
};
|
|
5364
|
+
}
|
|
5365
|
+
const resolvedStack = stack ?? (active.stack && isDesignStack(active.stack) ? active.stack : "web");
|
|
5366
|
+
const rawTokens = await loader.readTokens(active.kit);
|
|
5367
|
+
if (!rawTokens) {
|
|
5368
|
+
return { action, kit: active.kit, output: `Kit "${active.kit}" has no tokens.json.` };
|
|
5369
|
+
}
|
|
5370
|
+
const tokens = applyTokenOverrides(rawTokens, active.overrides);
|
|
5371
|
+
const result = materializeTokens({
|
|
5372
|
+
tokens,
|
|
5373
|
+
stack: resolvedStack,
|
|
5374
|
+
kitId: active.kit,
|
|
5375
|
+
outPath: input.out
|
|
5376
|
+
});
|
|
5377
|
+
const abs = path3.join(ctx.projectRoot, result.path);
|
|
5378
|
+
let exists = false;
|
|
5379
|
+
try {
|
|
5380
|
+
await fs2.access(abs);
|
|
5381
|
+
exists = true;
|
|
5382
|
+
} catch {
|
|
5383
|
+
}
|
|
5384
|
+
if (exists && !input.force) {
|
|
5385
|
+
return {
|
|
5386
|
+
action,
|
|
5387
|
+
kit: active.kit,
|
|
5388
|
+
stack: resolvedStack,
|
|
5389
|
+
path: result.path,
|
|
5390
|
+
output: `${result.path} already exists. Re-run with force:true to overwrite, or write this ${result.format} yourself:
|
|
5391
|
+
|
|
5392
|
+
\`\`\`
|
|
5393
|
+
${result.content}
|
|
5394
|
+
\`\`\``
|
|
5395
|
+
};
|
|
5396
|
+
}
|
|
5397
|
+
await fs2.mkdir(path3.dirname(abs), { recursive: true });
|
|
5398
|
+
await fs2.writeFile(abs, result.content);
|
|
5399
|
+
return {
|
|
5400
|
+
action,
|
|
5401
|
+
kit: active.kit,
|
|
5402
|
+
stack: resolvedStack,
|
|
5403
|
+
path: result.path,
|
|
5404
|
+
output: `Wrote ${result.format} to ${result.path}. Import these tokens in your UI so the kit palette is the source of truth. ${exists ? "(overwrote existing file)" : ""}`
|
|
5405
|
+
};
|
|
5406
|
+
}
|
|
5407
|
+
if (action === "verify") {
|
|
5408
|
+
const active = await loadActiveKit(ctx.projectRoot);
|
|
5409
|
+
if (!active) {
|
|
5410
|
+
return {
|
|
5411
|
+
action,
|
|
5412
|
+
output: 'No active kit to verify against. Pick one: `design {action:"use", kit:"<id>"}`.'
|
|
5413
|
+
};
|
|
5414
|
+
}
|
|
5415
|
+
const rawTokens = await loader.readTokens(active.kit);
|
|
5416
|
+
if (!rawTokens) {
|
|
5417
|
+
return { action, kit: active.kit, output: `Kit "${active.kit}" has no tokens.json.` };
|
|
5418
|
+
}
|
|
5419
|
+
const tokens = applyTokenOverrides(rawTokens, active.overrides);
|
|
5420
|
+
const report = await runDesignVerify(ctx.projectRoot, tokens, input.files);
|
|
5421
|
+
const pct = Math.round(report.score * 100);
|
|
5422
|
+
const top = report.violations.slice(0, 25).map((v) => ` ${v.file}:${v.line} \u2014 ${v.reason}: ${v.snippet}`).join("\n");
|
|
5423
|
+
const summary = `Adherence: ${pct}% on-palette across ${report.filesScanned} file(s). ${report.violations.length} violation(s).` + (report.violations.length ? `
|
|
5424
|
+
${top}${report.violations.length > 25 ? `
|
|
5425
|
+
\u2026and ${report.violations.length - 25} more` : ""}
|
|
5426
|
+
|
|
5427
|
+
Replace off-palette colors with kit tokens (or the materialized CSS vars / token utilities).` : "\nNo off-palette colors found \u2014 UI adheres to the kit palette.");
|
|
5428
|
+
return {
|
|
5429
|
+
action,
|
|
5430
|
+
kit: active.kit,
|
|
5431
|
+
output: summary,
|
|
5432
|
+
score: report.score,
|
|
5433
|
+
violations: report.violations.length
|
|
5434
|
+
};
|
|
5435
|
+
}
|
|
5436
|
+
const menu = await loader.menuText();
|
|
5437
|
+
return {
|
|
5438
|
+
action: "list",
|
|
5439
|
+
output: (menu || "No design kits are installed.") + '\n\nLoad one with `design {action:"use", kit:"<id>", stack:"<stack>"}`.'
|
|
5440
|
+
};
|
|
5441
|
+
}
|
|
5442
|
+
};
|
|
5137
5443
|
var diffTool = {
|
|
5138
5444
|
name: "diff",
|
|
5139
5445
|
category: "Filesystem",
|
|
@@ -5508,7 +5814,7 @@ var editTool = {
|
|
|
5508
5814
|
const newFile = toStyle(newFileLf, style);
|
|
5509
5815
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
5510
5816
|
const written = await fs2.stat(absPath);
|
|
5511
|
-
ctx.recordRead(absPath, written.mtimeMs);
|
|
5817
|
+
ctx.recordRead(absPath, written.mtimeMs, "write");
|
|
5512
5818
|
ctx.session.recordFileChange({
|
|
5513
5819
|
path: absPath,
|
|
5514
5820
|
action: "modified",
|
|
@@ -5551,7 +5857,7 @@ function findSimilarity(haystack, needle) {
|
|
|
5551
5857
|
}
|
|
5552
5858
|
return line;
|
|
5553
5859
|
}
|
|
5554
|
-
var
|
|
5860
|
+
var isWin3 = process.platform === "win32";
|
|
5555
5861
|
var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
5556
5862
|
// JS / TS toolchain
|
|
5557
5863
|
"node",
|
|
@@ -5561,12 +5867,25 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
5561
5867
|
"npx",
|
|
5562
5868
|
"bun",
|
|
5563
5869
|
"deno",
|
|
5870
|
+
"corepack",
|
|
5564
5871
|
"tsc",
|
|
5872
|
+
"tsx",
|
|
5873
|
+
"ts-node",
|
|
5874
|
+
"vite",
|
|
5565
5875
|
"vitest",
|
|
5566
5876
|
"jest",
|
|
5567
5877
|
"biome",
|
|
5568
5878
|
"eslint",
|
|
5569
5879
|
"prettier",
|
|
5880
|
+
"turbo",
|
|
5881
|
+
"nx",
|
|
5882
|
+
"webpack",
|
|
5883
|
+
"rollup",
|
|
5884
|
+
"parcel",
|
|
5885
|
+
"next",
|
|
5886
|
+
"astro",
|
|
5887
|
+
"playwright",
|
|
5888
|
+
"cypress",
|
|
5570
5889
|
// version control
|
|
5571
5890
|
"git",
|
|
5572
5891
|
// Rust
|
|
@@ -5579,10 +5898,22 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
5579
5898
|
"python3",
|
|
5580
5899
|
"pip",
|
|
5581
5900
|
"pip3",
|
|
5901
|
+
"pytest",
|
|
5902
|
+
"ruff",
|
|
5903
|
+
"mypy",
|
|
5904
|
+
"uv",
|
|
5905
|
+
"uvx",
|
|
5906
|
+
"poetry",
|
|
5907
|
+
"hatch",
|
|
5908
|
+
"tox",
|
|
5582
5909
|
// Ruby
|
|
5583
5910
|
"ruby",
|
|
5584
5911
|
"gem",
|
|
5585
5912
|
"bundle",
|
|
5913
|
+
// PHP
|
|
5914
|
+
"php",
|
|
5915
|
+
"composer",
|
|
5916
|
+
"phpunit",
|
|
5586
5917
|
// JVM
|
|
5587
5918
|
"java",
|
|
5588
5919
|
"javac",
|
|
@@ -5594,18 +5925,25 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
5594
5925
|
// C / C++ / native build
|
|
5595
5926
|
"make",
|
|
5596
5927
|
"cmake",
|
|
5597
|
-
// containers / orchestration
|
|
5928
|
+
// containers / orchestration
|
|
5598
5929
|
"docker",
|
|
5930
|
+
"podman",
|
|
5599
5931
|
"kubectl",
|
|
5600
5932
|
// common POSIX file/text utilities
|
|
5933
|
+
"pwd",
|
|
5601
5934
|
"ls",
|
|
5602
5935
|
"cat",
|
|
5603
5936
|
"head",
|
|
5604
5937
|
"tail",
|
|
5605
5938
|
"wc",
|
|
5606
5939
|
"grep",
|
|
5940
|
+
"rg",
|
|
5607
5941
|
"find",
|
|
5608
5942
|
"echo",
|
|
5943
|
+
"sort",
|
|
5944
|
+
"uniq",
|
|
5945
|
+
"sed",
|
|
5946
|
+
"awk",
|
|
5609
5947
|
"mkdir",
|
|
5610
5948
|
"cp",
|
|
5611
5949
|
"mv",
|
|
@@ -5621,8 +5959,7 @@ var MAX_ARGS = 20;
|
|
|
5621
5959
|
var MAX_OUTPUT2 = 2e5;
|
|
5622
5960
|
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
5623
5961
|
var BLOCKED_ARG_PATTERNS = {
|
|
5624
|
-
|
|
5625
|
-
python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
|
|
5962
|
+
python: [],
|
|
5626
5963
|
// git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
|
|
5627
5964
|
// -C <dir> changes working directory, bypassing cwd sandbox;
|
|
5628
5965
|
// -c/--config <k>=<v> injects config that runs commands
|
|
@@ -5638,15 +5975,10 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
5638
5975
|
/^--config=/,
|
|
5639
5976
|
/^--config-env=/
|
|
5640
5977
|
],
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
// bun --preload is similar to node --require
|
|
5646
|
-
bun: [/^--preload$/, /^run$/, /^bunx$/, /^create$/, /^init$/],
|
|
5647
|
-
// docker build/run can create containers with host access;
|
|
5648
|
-
// only allow read-only commands (ps, images, version)
|
|
5649
|
-
docker: [/^build$/, /^run$/, /^exec$/, /^push$/, /^pull$/],
|
|
5978
|
+
node: [],
|
|
5979
|
+
go: [],
|
|
5980
|
+
bun: [],
|
|
5981
|
+
docker: [],
|
|
5650
5982
|
// find -exec/-ok/-execdir execute arbitrary commands
|
|
5651
5983
|
find: [/^-exec$/, /^-exec;$/, /^-ok$/, /^-ok;$/, /^-execdir$/, /^-execdir;$/, /^-exec=/, /^-ok=/, /^-execdir=/],
|
|
5652
5984
|
// rm -rf / is catastrophic — block absolute paths, home, dot-dirs,
|
|
@@ -5654,15 +5986,49 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
5654
5986
|
// `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
|
|
5655
5987
|
// `rm -rf /*` targets the filesystem root. All are blocked.
|
|
5656
5988
|
rm: [/^\//, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
|
|
5657
|
-
// npm
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
pnpm: [
|
|
5661
|
-
|
|
5662
|
-
// malicious package execution (typosquatting, dependency confusion)
|
|
5663
|
-
npx: [/^[^\s]+$/]
|
|
5989
|
+
// npm/pnpm subcommands are checked separately below. Matching every arg here
|
|
5990
|
+
// over-blocked normal dev flows such as `pnpm vitest run ...`.
|
|
5991
|
+
npm: [],
|
|
5992
|
+
pnpm: [],
|
|
5993
|
+
npx: []
|
|
5664
5994
|
};
|
|
5995
|
+
var BLOCKED_SUBCOMMANDS = {
|
|
5996
|
+
docker: /* @__PURE__ */ new Set(["push"]),
|
|
5997
|
+
podman: /* @__PURE__ */ new Set(["push"]),
|
|
5998
|
+
npm: /* @__PURE__ */ new Set(["publish", "deploy"]),
|
|
5999
|
+
pnpm: /* @__PURE__ */ new Set(["publish", "deploy"]),
|
|
6000
|
+
yarn: /* @__PURE__ */ new Set(["publish"])
|
|
6001
|
+
};
|
|
6002
|
+
var BLOCKED_SUBCOMMAND_SEQUENCES = {
|
|
6003
|
+
yarn: [["npm", "publish"]]
|
|
6004
|
+
};
|
|
6005
|
+
function firstSubcommand(args) {
|
|
6006
|
+
for (const arg of args) {
|
|
6007
|
+
if (arg === "--") return null;
|
|
6008
|
+
if (!arg.startsWith("-")) return arg;
|
|
6009
|
+
}
|
|
6010
|
+
return null;
|
|
6011
|
+
}
|
|
6012
|
+
function subcommandArgs(args) {
|
|
6013
|
+
const out = [];
|
|
6014
|
+
for (const arg of args) {
|
|
6015
|
+
if (arg === "--") break;
|
|
6016
|
+
if (!arg.startsWith("-")) out.push(arg);
|
|
6017
|
+
}
|
|
6018
|
+
return out;
|
|
6019
|
+
}
|
|
5665
6020
|
function validateArgs(cmd, args) {
|
|
6021
|
+
const blockedSubcommands = BLOCKED_SUBCOMMANDS[cmd];
|
|
6022
|
+
const subcommand = firstSubcommand(args);
|
|
6023
|
+
if (blockedSubcommands && subcommand && blockedSubcommands.has(subcommand)) {
|
|
6024
|
+
return `Blocked subcommand "${subcommand}" for command "${cmd}"`;
|
|
6025
|
+
}
|
|
6026
|
+
const blockedSequences = BLOCKED_SUBCOMMAND_SEQUENCES[cmd];
|
|
6027
|
+
if (blockedSequences) {
|
|
6028
|
+
const actual = subcommandArgs(args);
|
|
6029
|
+
const blocked2 = blockedSequences.find((seq) => seq.every((part, idx) => actual[idx] === part));
|
|
6030
|
+
if (blocked2) return `Blocked subcommand "${blocked2.join(" ")}" for command "${cmd}"`;
|
|
6031
|
+
}
|
|
5666
6032
|
const blocked = BLOCKED_ARG_PATTERNS[cmd];
|
|
5667
6033
|
if (!blocked) return null;
|
|
5668
6034
|
for (const arg of args) {
|
|
@@ -5789,7 +6155,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
5789
6155
|
const startedAt = Date.now();
|
|
5790
6156
|
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
5791
6157
|
const resolved = resolveWin32Command(cmd);
|
|
5792
|
-
const needsShell =
|
|
6158
|
+
const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
5793
6159
|
const spawnCmd = needsShell ? cmd : resolved;
|
|
5794
6160
|
if (needsShell) assertSafeWin32ShellArgs(args);
|
|
5795
6161
|
let child;
|
|
@@ -5799,7 +6165,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
5799
6165
|
env: buildChildEnv(sessionId),
|
|
5800
6166
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5801
6167
|
windowsHide: true,
|
|
5802
|
-
...
|
|
6168
|
+
...isWin3 ? {} : { signal },
|
|
5803
6169
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
5804
6170
|
});
|
|
5805
6171
|
} catch (err) {
|
|
@@ -5819,7 +6185,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
5819
6185
|
const isAbort = err && err.code === "ABORT_ERR";
|
|
5820
6186
|
const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
|
|
5821
6187
|
clearTimeout(timer);
|
|
5822
|
-
if (
|
|
6188
|
+
if (isWin3) signal.removeEventListener("abort", onAbort);
|
|
5823
6189
|
if (typeof pid === "number") registry.unregister(pid);
|
|
5824
6190
|
registry.afterCall(Date.now() - startedAt, true);
|
|
5825
6191
|
spool.finalize();
|
|
@@ -5849,7 +6215,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
5849
6215
|
if (typeof pid === "number") registry.kill(pid, { force: true });
|
|
5850
6216
|
else child.kill("SIGTERM");
|
|
5851
6217
|
};
|
|
5852
|
-
if (
|
|
6218
|
+
if (isWin3) {
|
|
5853
6219
|
if (signal.aborted) onAbort();
|
|
5854
6220
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
5855
6221
|
}
|
|
@@ -5865,7 +6231,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
5865
6231
|
});
|
|
5866
6232
|
child.on("close", (code) => {
|
|
5867
6233
|
clearTimeout(timer);
|
|
5868
|
-
if (
|
|
6234
|
+
if (isWin3) signal.removeEventListener("abort", onAbort);
|
|
5869
6235
|
if (typeof pid === "number") registry.unregister(pid);
|
|
5870
6236
|
const durationMs = Date.now() - startedAt;
|
|
5871
6237
|
const exitCode = killed ? 124 : code ?? 1;
|
|
@@ -6029,7 +6395,7 @@ var fetchTool = {
|
|
|
6029
6395
|
if (!final) throw new Error("fetch: stream ended without final event");
|
|
6030
6396
|
return final;
|
|
6031
6397
|
},
|
|
6032
|
-
async *executeStream(input,
|
|
6398
|
+
async *executeStream(input, ctx, opts) {
|
|
6033
6399
|
if (!input?.url) throw new Error("fetch: url is required");
|
|
6034
6400
|
const u = new URL(input.url);
|
|
6035
6401
|
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
@@ -6101,6 +6467,14 @@ var fetchTool = {
|
|
|
6101
6467
|
url: res.url
|
|
6102
6468
|
}
|
|
6103
6469
|
};
|
|
6470
|
+
ctx.recordSideEffect?.({
|
|
6471
|
+
toolUseId: `fetch-${Date.now()}`,
|
|
6472
|
+
toolName: "fetch",
|
|
6473
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6474
|
+
input: { url: input.url, format: input.format },
|
|
6475
|
+
outcome: `HTTP ${res.status} (${ct})`,
|
|
6476
|
+
risk: "network"
|
|
6477
|
+
});
|
|
6104
6478
|
} finally {
|
|
6105
6479
|
clearTimeout(timer);
|
|
6106
6480
|
}
|
|
@@ -7193,6 +7567,14 @@ var installTool = {
|
|
|
7193
7567
|
}
|
|
7194
7568
|
}
|
|
7195
7569
|
}
|
|
7570
|
+
ctx.recordSideEffect?.({
|
|
7571
|
+
toolUseId: `install-${Date.now()}`,
|
|
7572
|
+
toolName: "install",
|
|
7573
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7574
|
+
input: { packages: pkgList, cwd, dry_run: Boolean(input.dry_run) },
|
|
7575
|
+
outcome: output.dry_run ? "dry run" : result.exitCode === 0 ? `installed ${pkgList.length || "all"} packages` : `failed (exit ${result.exitCode})`,
|
|
7576
|
+
risk: "package"
|
|
7577
|
+
});
|
|
7196
7578
|
yield { type: "final", output };
|
|
7197
7579
|
}
|
|
7198
7580
|
};
|
|
@@ -7282,8 +7664,8 @@ var jsonTool = {
|
|
|
7282
7664
|
};
|
|
7283
7665
|
}
|
|
7284
7666
|
};
|
|
7285
|
-
function query(data,
|
|
7286
|
-
const parts =
|
|
7667
|
+
function query(data, path22) {
|
|
7668
|
+
const parts = path22.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
7287
7669
|
let current = data;
|
|
7288
7670
|
for (const part of parts) {
|
|
7289
7671
|
if (current === null || current === void 0) return void 0;
|
|
@@ -7560,7 +7942,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
7560
7942
|
}
|
|
7561
7943
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
7562
7944
|
var MAX_TAIL_LINES = 1e5;
|
|
7563
|
-
async function fileLogs(
|
|
7945
|
+
async function fileLogs(path22, lines, filterRe, stream) {
|
|
7564
7946
|
const { createInterface } = await import('node:readline');
|
|
7565
7947
|
const { createReadStream } = await import('node:fs');
|
|
7566
7948
|
const entries = [];
|
|
@@ -7569,7 +7951,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
|
|
|
7569
7951
|
let writeIdx = 0;
|
|
7570
7952
|
let totalLines = 0;
|
|
7571
7953
|
const rl = createInterface({
|
|
7572
|
-
input: createReadStream(
|
|
7954
|
+
input: createReadStream(path22),
|
|
7573
7955
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
7574
7956
|
});
|
|
7575
7957
|
for await (const line of rl) {
|
|
@@ -7590,7 +7972,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
|
|
|
7590
7972
|
if (parsed) entries.push(parsed);
|
|
7591
7973
|
}
|
|
7592
7974
|
return {
|
|
7593
|
-
source:
|
|
7975
|
+
source: path22,
|
|
7594
7976
|
entries,
|
|
7595
7977
|
total: entries.length,
|
|
7596
7978
|
truncated: totalLines > effLines,
|
|
@@ -10155,7 +10537,7 @@ var writeTool = {
|
|
|
10155
10537
|
if (existed) {
|
|
10156
10538
|
if (!ctx.hasRead(absPath)) {
|
|
10157
10539
|
prev = await fs2.readFile(absPath, "utf8");
|
|
10158
|
-
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
10540
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "write");
|
|
10159
10541
|
} else {
|
|
10160
10542
|
prev = await fs2.readFile(absPath, "utf8");
|
|
10161
10543
|
}
|
|
@@ -10169,7 +10551,7 @@ var writeTool = {
|
|
|
10169
10551
|
const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
10170
10552
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
10171
10553
|
const stat11 = await fs2.stat(absPath);
|
|
10172
|
-
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
10554
|
+
ctx.recordRead(absPath, stat11.mtimeMs, "write");
|
|
10173
10555
|
ctx.session.recordFileChange({
|
|
10174
10556
|
path: absPath,
|
|
10175
10557
|
action: existed ? "modified" : "created",
|
|
@@ -10215,6 +10597,7 @@ var builtinTools = [
|
|
|
10215
10597
|
logsTool,
|
|
10216
10598
|
documentTool,
|
|
10217
10599
|
scaffoldTool,
|
|
10600
|
+
designTool,
|
|
10218
10601
|
toolSearchTool,
|
|
10219
10602
|
toolUseTool,
|
|
10220
10603
|
batchToolUseTool,
|