@wrongstack/core 0.309.0 → 0.309.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/coordination/director.d.ts +7 -0
- package/dist/coordination/explore-companion.d.ts +9 -6
- package/dist/coordination/index.js +331 -59
- package/dist/coordination/mutation-engine.d.ts +5 -3
- package/dist/core/index.js +39 -9
- package/dist/defaults/index.js +479 -101
- package/dist/execution/index.js +27 -9
- package/dist/hq/index.js +45 -5
- package/dist/index.js +640 -131
- package/dist/infrastructure/index.js +22 -3
- package/dist/observability/index.js +1 -1
- package/dist/plugin/index.js +113 -12
- package/dist/prompts/index.js +360 -3
- package/dist/security/auto-approve-policy.d.ts +2 -2
- package/dist/security/index.js +177 -50
- package/dist/security/permission-helpers.d.ts +11 -0
- package/dist/security/permission-policy.d.ts +10 -1
- package/dist/security/yolo-risk.d.ts +17 -0
- package/dist/session-catalog/index.js +32 -2
- package/dist/session-catalog/project-server.js +32 -2
- package/dist/skills/index.js +39 -6
- package/dist/storage/index.js +44 -3
- package/dist/types/tool.d.ts +15 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +54 -4
- package/dist/utils/terminal-sanitize.d.ts +41 -0
- package/dist/utils/tool-subject.d.ts +1 -1
- package/instructions/agents/chaos-monkey.md +5 -1
- package/package.json +4 -4
package/dist/defaults/index.js
CHANGED
|
@@ -5575,11 +5575,13 @@ var CHAOS_MONKEY_AGENT = {
|
|
|
5575
5575
|
tools: [...TOOLS.build],
|
|
5576
5576
|
skillNames: ["testing", "typescript-strict"],
|
|
5577
5577
|
spawnBudgetExempt: true,
|
|
5578
|
-
//
|
|
5579
|
-
//
|
|
5580
|
-
//
|
|
5581
|
-
//
|
|
5582
|
-
|
|
5578
|
+
// Run in the live checkout: mutation targets are usually freshly
|
|
5579
|
+
// written and uncommitted — a worktree spawned from HEAD would not
|
|
5580
|
+
// contain them and every mutant would drift. The mutation_test tool
|
|
5581
|
+
// honors this value as its default; callers can still override per
|
|
5582
|
+
// call via its `chaosWorktree` input when targets are committed and
|
|
5583
|
+
// isolation is wanted.
|
|
5584
|
+
worktree: "off",
|
|
5583
5585
|
// Report travels via submit_result + final text, not the leader's stream.
|
|
5584
5586
|
textStream: "silent",
|
|
5585
5587
|
toolStream: "silent"
|
|
@@ -9640,24 +9642,29 @@ var TOKEN_PATTERNS = [
|
|
|
9640
9642
|
kind: "return-null",
|
|
9641
9643
|
// `return <expr>;` where expr is not already null/undefined/void.
|
|
9642
9644
|
regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
|
|
9643
|
-
replace: () => "return null;"
|
|
9645
|
+
replace: () => "return null;",
|
|
9646
|
+
endpointsInCode: true
|
|
9644
9647
|
}
|
|
9645
9648
|
];
|
|
9646
9649
|
function planMutations(file, source, opts = {}) {
|
|
9647
9650
|
const maxPerFile = opts.maxPerFile ?? 25;
|
|
9648
9651
|
const out = [];
|
|
9649
9652
|
const lines = source.split("\n");
|
|
9653
|
+
const masks = computeLineMasks(source);
|
|
9650
9654
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
9651
9655
|
const line = lines[lineIdx];
|
|
9652
9656
|
const t = line.trim();
|
|
9653
|
-
if (t.startsWith("//")
|
|
9657
|
+
if (t.startsWith("//")) continue;
|
|
9658
|
+
const codeRanges = masks[lineIdx];
|
|
9659
|
+
const inCode = (start) => codeRanges.some(([s, e]) => start >= s && start < e);
|
|
9654
9660
|
for (const pattern of TOKEN_PATTERNS) {
|
|
9655
9661
|
pattern.regex.lastIndex = 0;
|
|
9656
9662
|
let m;
|
|
9657
9663
|
while ((m = pattern.regex.exec(line)) !== null) {
|
|
9658
9664
|
const token = m.groups?.["op"] ?? m[0];
|
|
9659
9665
|
const tokenStart = m.index + m[0].indexOf(token);
|
|
9660
|
-
if (
|
|
9666
|
+
if (!inCode(tokenStart)) continue;
|
|
9667
|
+
if (pattern.endpointsInCode && !inCode(tokenStart + token.length - 1)) continue;
|
|
9661
9668
|
const original = line.slice(tokenStart, tokenStart + token.length);
|
|
9662
9669
|
const replacement = pattern.replace(token);
|
|
9663
9670
|
if (replacement === original) continue;
|
|
@@ -9676,19 +9683,179 @@ function planMutations(file, source, opts = {}) {
|
|
|
9676
9683
|
}
|
|
9677
9684
|
return out.slice(0, maxPerFile);
|
|
9678
9685
|
}
|
|
9679
|
-
function
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9686
|
+
function computeLineMasks(source) {
|
|
9687
|
+
const lines = source.split("\n");
|
|
9688
|
+
const masks = lines.map(() => []);
|
|
9689
|
+
const stack = [{ kind: "code", depth: 0, parens: [] }];
|
|
9690
|
+
let inBlockComment = false;
|
|
9691
|
+
let lastToken = null;
|
|
9692
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
9693
|
+
const line = lines[lineIdx];
|
|
9694
|
+
const ranges = masks[lineIdx];
|
|
9695
|
+
let runStart = null;
|
|
9696
|
+
const closeRun = (end) => {
|
|
9697
|
+
if (runStart !== null && end > runStart) ranges.push([runStart, end]);
|
|
9698
|
+
runStart = null;
|
|
9699
|
+
};
|
|
9700
|
+
let i = 0;
|
|
9701
|
+
if (inBlockComment) {
|
|
9702
|
+
const close = line.indexOf("*/");
|
|
9703
|
+
if (close === -1) continue;
|
|
9704
|
+
inBlockComment = false;
|
|
9705
|
+
i = close + 2;
|
|
9706
|
+
}
|
|
9707
|
+
while (i < line.length) {
|
|
9708
|
+
const top = stack[stack.length - 1];
|
|
9709
|
+
const c = line[i];
|
|
9710
|
+
if (top.kind === "template") {
|
|
9711
|
+
if (c === "\\") {
|
|
9712
|
+
i += 2;
|
|
9713
|
+
continue;
|
|
9714
|
+
}
|
|
9715
|
+
if (c === "`") {
|
|
9716
|
+
stack.pop();
|
|
9717
|
+
lastToken = "`";
|
|
9718
|
+
i++;
|
|
9719
|
+
continue;
|
|
9720
|
+
}
|
|
9721
|
+
if (c === "$" && line[i + 1] === "{") {
|
|
9722
|
+
stack.push({ kind: "code", depth: 0, parens: [] });
|
|
9723
|
+
lastToken = "${";
|
|
9724
|
+
i += 2;
|
|
9725
|
+
continue;
|
|
9726
|
+
}
|
|
9727
|
+
i++;
|
|
9728
|
+
continue;
|
|
9729
|
+
}
|
|
9730
|
+
if (/[\w$]/.test(c)) {
|
|
9731
|
+
let j = i + 1;
|
|
9732
|
+
while (j < line.length && /[\w$]/.test(line[j])) j++;
|
|
9733
|
+
lastToken = line.slice(i, j);
|
|
9734
|
+
if (runStart === null) runStart = i;
|
|
9735
|
+
i = j;
|
|
9736
|
+
continue;
|
|
9737
|
+
}
|
|
9738
|
+
if (c === "'" || c === '"') {
|
|
9739
|
+
closeRun(i);
|
|
9740
|
+
i++;
|
|
9741
|
+
while (i < line.length && line[i] !== c) {
|
|
9742
|
+
if (line[i] === "\\") i++;
|
|
9743
|
+
i++;
|
|
9744
|
+
}
|
|
9745
|
+
i++;
|
|
9746
|
+
lastToken = c;
|
|
9747
|
+
continue;
|
|
9748
|
+
}
|
|
9749
|
+
if (c === "`") {
|
|
9750
|
+
closeRun(i);
|
|
9751
|
+
stack.push({ kind: "template", depth: 0, parens: [] });
|
|
9752
|
+
i++;
|
|
9753
|
+
continue;
|
|
9754
|
+
}
|
|
9755
|
+
if (c === "/" && line[i + 1] === "/") {
|
|
9756
|
+
closeRun(i);
|
|
9757
|
+
break;
|
|
9758
|
+
}
|
|
9759
|
+
if (c === "/" && line[i + 1] === "*") {
|
|
9760
|
+
closeRun(i);
|
|
9761
|
+
const close = line.indexOf("*/", i + 2);
|
|
9762
|
+
if (close === -1) {
|
|
9763
|
+
inBlockComment = true;
|
|
9764
|
+
break;
|
|
9765
|
+
}
|
|
9766
|
+
i = close + 2;
|
|
9767
|
+
continue;
|
|
9768
|
+
}
|
|
9769
|
+
if (c === "/") {
|
|
9770
|
+
if (!tokenCanEndOperand(lastToken)) {
|
|
9771
|
+
closeRun(i);
|
|
9772
|
+
const next = skipRegexLiteral(line, i);
|
|
9773
|
+
lastToken = next > i + 1 ? "regex" : "/";
|
|
9774
|
+
i = next;
|
|
9775
|
+
continue;
|
|
9776
|
+
}
|
|
9777
|
+
}
|
|
9778
|
+
if (c === "(") {
|
|
9779
|
+
top.parens.push(CONTROL_KEYWORDS.has(lastToken ?? "") ? "control" : "expr");
|
|
9780
|
+
lastToken = c;
|
|
9781
|
+
} else if (c === ")") {
|
|
9782
|
+
const kind = top.parens.pop() ?? "expr";
|
|
9783
|
+
lastToken = kind === "control" ? "control-paren-close" : ")";
|
|
9784
|
+
} else if (c === "{") {
|
|
9785
|
+
top.depth++;
|
|
9786
|
+
lastToken = c;
|
|
9787
|
+
} else if (c === "}") {
|
|
9788
|
+
if (top.depth > 0) {
|
|
9789
|
+
top.depth--;
|
|
9790
|
+
lastToken = c;
|
|
9791
|
+
} else if (stack.length > 1) {
|
|
9792
|
+
closeRun(i);
|
|
9793
|
+
stack.pop();
|
|
9794
|
+
i++;
|
|
9795
|
+
continue;
|
|
9796
|
+
} else {
|
|
9797
|
+
lastToken = c;
|
|
9798
|
+
}
|
|
9799
|
+
} else if (c !== " " && c !== " " && c !== "\r") {
|
|
9800
|
+
lastToken = c;
|
|
9801
|
+
}
|
|
9802
|
+
if (runStart === null) runStart = i;
|
|
9803
|
+
i++;
|
|
9804
|
+
}
|
|
9805
|
+
closeRun(line.length);
|
|
9806
|
+
}
|
|
9807
|
+
return masks;
|
|
9808
|
+
}
|
|
9809
|
+
var KEYWORDS_BEFORE_REGEX = /* @__PURE__ */ new Set([
|
|
9810
|
+
"return",
|
|
9811
|
+
"typeof",
|
|
9812
|
+
"instanceof",
|
|
9813
|
+
"in",
|
|
9814
|
+
"of",
|
|
9815
|
+
"new",
|
|
9816
|
+
"delete",
|
|
9817
|
+
"void",
|
|
9818
|
+
"throw",
|
|
9819
|
+
"case",
|
|
9820
|
+
"do",
|
|
9821
|
+
"else",
|
|
9822
|
+
"yield",
|
|
9823
|
+
"await"
|
|
9824
|
+
]);
|
|
9825
|
+
var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
|
|
9826
|
+
function tokenCanEndOperand(token) {
|
|
9827
|
+
if (token === null) return false;
|
|
9828
|
+
if (/^[\w$]+$/.test(token)) return !KEYWORDS_BEFORE_REGEX.has(token);
|
|
9829
|
+
return token === ")" || token === "]" || token === "." || token === '"' || token === "'" || token === "`";
|
|
9830
|
+
}
|
|
9831
|
+
function skipRegexLiteral(line, start) {
|
|
9832
|
+
let i = start + 1;
|
|
9833
|
+
let inClass = false;
|
|
9834
|
+
while (i < line.length) {
|
|
9835
|
+
const ch = line[i];
|
|
9836
|
+
if (ch === "\\") {
|
|
9837
|
+
i += 2;
|
|
9838
|
+
continue;
|
|
9839
|
+
}
|
|
9840
|
+
if (inClass) {
|
|
9841
|
+
if (ch === "]") inClass = false;
|
|
9842
|
+
i++;
|
|
9843
|
+
continue;
|
|
9844
|
+
}
|
|
9845
|
+
if (ch === "[") {
|
|
9846
|
+
inClass = true;
|
|
9847
|
+
i++;
|
|
9848
|
+
continue;
|
|
9849
|
+
}
|
|
9850
|
+
if (ch === "/") {
|
|
9851
|
+
i++;
|
|
9852
|
+
break;
|
|
9853
|
+
}
|
|
9854
|
+
if (ch === "\n" || ch === "\r") return line.length;
|
|
9855
|
+
i++;
|
|
9688
9856
|
}
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
return /['"]/.test(window);
|
|
9857
|
+
while (i < line.length && /[a-z]/.test(line[i])) i++;
|
|
9858
|
+
return i;
|
|
9692
9859
|
}
|
|
9693
9860
|
function parseMutationReport(text) {
|
|
9694
9861
|
const candidates = [];
|
|
@@ -9739,7 +9906,7 @@ function normalizeMutantEntry(value) {
|
|
|
9739
9906
|
const rec = value;
|
|
9740
9907
|
const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
|
|
9741
9908
|
const status = rec["status"];
|
|
9742
|
-
if (!id || status !== "killed" && status !== "survived" && status !== "skipped") {
|
|
9909
|
+
if (!id || status !== "killed" && status !== "survived" && status !== "skipped" && status !== "killed-by-hang") {
|
|
9743
9910
|
return void 0;
|
|
9744
9911
|
}
|
|
9745
9912
|
return {
|
|
@@ -9795,7 +9962,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
|
|
|
9795
9962
|
},
|
|
9796
9963
|
chaosWorktree: {
|
|
9797
9964
|
anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
|
|
9798
|
-
description: "Worktree override for the chaos agent.
|
|
9965
|
+
description: "Worktree override for the chaos agent. Defaults to the roster policy for chaos-monkey ('off'), because mutation targets are usually freshly written and uncommitted \u2014 a worktree from HEAD would not contain them and every mutant would drift to skipped. Only pass 'auto' or 'required' when the targets are committed."
|
|
9799
9966
|
},
|
|
9800
9967
|
timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
|
|
9801
9968
|
reportOnly: {
|
|
@@ -9817,8 +9984,16 @@ function makeMutationTestTool(director, roster, opts = {}) {
|
|
|
9817
9984
|
error: "No mutable sites found in the given targets (after comment/string filtering)."
|
|
9818
9985
|
};
|
|
9819
9986
|
}
|
|
9987
|
+
const chaosBase = roster?.[CHAOS_ROLE];
|
|
9988
|
+
if (!chaosBase) {
|
|
9989
|
+
return {
|
|
9990
|
+
verdict: "inconclusive",
|
|
9991
|
+
passed: false,
|
|
9992
|
+
error: "chaos-monkey role missing from the roster \u2014 refusing to spawn a saboteur without its prompt/tools contract. Build the toolset with a roster that includes 'chaos-monkey' (FLEET_ROSTER does)."
|
|
9993
|
+
};
|
|
9994
|
+
}
|
|
9820
9995
|
const chaosSubagentId = await director.spawn(
|
|
9821
|
-
makeChaosConfig(
|
|
9996
|
+
makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
|
|
9822
9997
|
);
|
|
9823
9998
|
const chaosTaskId = await director.assign({
|
|
9824
9999
|
id: randomUUID8(),
|
|
@@ -9836,6 +10011,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
|
|
|
9836
10011
|
);
|
|
9837
10012
|
const attempts = [];
|
|
9838
10013
|
let current = survivors;
|
|
10014
|
+
let rerunUnknowns = [];
|
|
9839
10015
|
while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
|
|
9840
10016
|
const attemptNo = attempts.length + 1;
|
|
9841
10017
|
const strengthenTaskId = await director.assign({
|
|
@@ -9857,7 +10033,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
|
|
|
9857
10033
|
}
|
|
9858
10034
|
const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
|
|
9859
10035
|
const rerunSubagentId = await director.spawn(
|
|
9860
|
-
makeChaosConfig(
|
|
10036
|
+
makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
|
|
9861
10037
|
);
|
|
9862
10038
|
const rerunTaskId = await director.assign({
|
|
9863
10039
|
id: randomUUID8(),
|
|
@@ -9867,29 +10043,36 @@ function makeMutationTestTool(director, roster, opts = {}) {
|
|
|
9867
10043
|
});
|
|
9868
10044
|
const [rerunResult] = await director.awaitTasks([rerunTaskId]);
|
|
9869
10045
|
const passN = collectOutcomes(rerunResult, survivorPlan);
|
|
9870
|
-
const stillSurviving = passN.filter((m) => m.status
|
|
10046
|
+
const stillSurviving = passN.filter((m) => !isKill(m.status));
|
|
10047
|
+
rerunUnknowns = passN.filter((m) => m.status === "skipped");
|
|
9871
10048
|
attempts.push({
|
|
9872
10049
|
attempt: attemptNo,
|
|
9873
10050
|
survivorsBefore: current,
|
|
9874
10051
|
strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
|
|
9875
10052
|
rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
|
|
9876
10053
|
survivorsAfter: stillSurviving,
|
|
9877
|
-
suspectedEquivalent: stillSurviving.filter((m) => current.some((c) => c.id === m.id)).map((m) => m.id)
|
|
10054
|
+
suspectedEquivalent: stillSurviving.filter((m) => m.status === "survived" && current.some((c) => c.id === m.id)).map((m) => m.id)
|
|
9878
10055
|
});
|
|
9879
|
-
current = stillSurviving
|
|
9880
|
-
if (passN.every((m) => m.status === "skipped")) break;
|
|
10056
|
+
current = stillSurviving;
|
|
9881
10057
|
}
|
|
9882
|
-
const finalSurvivors = current;
|
|
10058
|
+
const finalSurvivors = current.filter((m) => m.status === "survived");
|
|
9883
10059
|
const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
|
|
9884
10060
|
const skippedCount = pass1.filter((m) => m.status === "skipped").length;
|
|
9885
|
-
const
|
|
9886
|
-
const
|
|
10061
|
+
const rerunUnknownCount = rerunUnknowns.length;
|
|
10062
|
+
const score = plan.length === 0 ? 0 : pass1.filter((m) => isKill(m.status)).length / plan.length;
|
|
10063
|
+
const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 || rerunUnknownCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
|
|
9887
10064
|
return {
|
|
9888
10065
|
verdict,
|
|
9889
10066
|
passed: verdict === "pass",
|
|
9890
10067
|
mutationScore: Number.parseFloat(score.toFixed(3)),
|
|
9891
10068
|
planned: plan.length,
|
|
9892
|
-
killed: pass1.filter((m) => m.status
|
|
10069
|
+
killed: pass1.filter((m) => isKill(m.status)).length,
|
|
10070
|
+
// Breakout of `killed`: how many kills were detected by the test
|
|
10071
|
+
// command hanging rather than by a failing assertion. A subset of
|
|
10072
|
+
// `killed`, surfaced so a director can distinguish a hang-heavy
|
|
10073
|
+
// suite (mutants breaking termination, not assertions) from an
|
|
10074
|
+
// assertion-strong one. hangHeavy = killedByHang === killed.
|
|
10075
|
+
killedByHang: pass1.filter((m) => m.status === "killed-by-hang").length,
|
|
9893
10076
|
survived: pass1.filter((m) => m.status === "survived").length,
|
|
9894
10077
|
skipped: pass1.filter((m) => m.status === "skipped").length,
|
|
9895
10078
|
finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
|
|
@@ -9897,7 +10080,11 @@ function makeMutationTestTool(director, roster, opts = {}) {
|
|
|
9897
10080
|
strengthenAttempts: attempts.length,
|
|
9898
10081
|
attempts,
|
|
9899
10082
|
chaosTaskId,
|
|
9900
|
-
|
|
10083
|
+
// Unverified leftovers from the strengthen loop: surfaced so the
|
|
10084
|
+
// caller can see WHICH mutants lack kill evidence, and counted by
|
|
10085
|
+
// the verdict gate above.
|
|
10086
|
+
unverifiedFromRerun: rerunUnknowns.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
|
|
10087
|
+
nextAction: finalSurvivors.length === 0 && rerunUnknownCount === 0 && skippedCount === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
|
|
9901
10088
|
};
|
|
9902
10089
|
}
|
|
9903
10090
|
};
|
|
@@ -9913,7 +10100,7 @@ function normalizeMutationTestInput(input) {
|
|
|
9913
10100
|
maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
|
|
9914
10101
|
maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
|
|
9915
10102
|
repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
|
|
9916
|
-
chaosWorktree: raw["chaosWorktree"]
|
|
10103
|
+
chaosWorktree: normalizeWorktreeOverride(raw["chaosWorktree"]),
|
|
9917
10104
|
timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
|
|
9918
10105
|
reportOnly: raw["reportOnly"] === true
|
|
9919
10106
|
};
|
|
@@ -9935,8 +10122,7 @@ function buildPlan(i, projectRoot) {
|
|
|
9935
10122
|
}
|
|
9936
10123
|
return plan;
|
|
9937
10124
|
}
|
|
9938
|
-
function makeChaosConfig(
|
|
9939
|
-
const base = roster?.[CHAOS_ROLE] ?? getAgentDefinition(CHAOS_ROLE)?.config ?? { name: "Chaos Monkey", role: CHAOS_ROLE };
|
|
10125
|
+
function makeChaosConfig(base, worktree) {
|
|
9940
10126
|
return { ...instantiateRosterConfig2(CHAOS_ROLE, base), worktree };
|
|
9941
10127
|
}
|
|
9942
10128
|
function buildChaosTask(plan, i, pass, priorSurvivors) {
|
|
@@ -9952,7 +10138,7 @@ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join(
|
|
|
9952
10138
|
"For each mutant, in order:",
|
|
9953
10139
|
"1. Apply ONLY that mutation at its exact (file, line, column).",
|
|
9954
10140
|
`2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
|
|
9955
|
-
"3. Record killed (tests failed \u2014 quote first failing assertion)
|
|
10141
|
+
"3. Record killed (tests failed \u2014 quote first failing assertion), survived (suite green), or killed-by-hang (the test command timed out or was aborted \u2014 the mutation broke the suite by non-termination; record the timeout as evidence, do NOT report it as survived).",
|
|
9956
10142
|
"4. Restore the file byte-for-byte before the next mutant.",
|
|
9957
10143
|
"",
|
|
9958
10144
|
"Mutants:",
|
|
@@ -9964,23 +10150,49 @@ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join(
|
|
|
9964
10150
|
].join("\n");
|
|
9965
10151
|
}
|
|
9966
10152
|
function buildStrengthenTask(survivors, i, attempt) {
|
|
10153
|
+
const confirmed = survivors.filter((s) => s.status === "survived");
|
|
10154
|
+
const unverified = survivors.filter((s) => s.status === "skipped");
|
|
10155
|
+
const row = (s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`;
|
|
9967
10156
|
return [
|
|
9968
|
-
`Strengthen the tests so
|
|
9969
|
-
"",
|
|
9970
|
-
"Each survivor below was a deliberate sabotage of production code that the current suite did NOT catch:",
|
|
9971
|
-
...survivors.map((s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`),
|
|
10157
|
+
`Strengthen the tests so the mutants below die (attempt ${attempt}).`,
|
|
9972
10158
|
"",
|
|
9973
|
-
|
|
10159
|
+
...confirmed.length > 0 ? [
|
|
10160
|
+
"CONFIRMED SURVIVORS \u2014 each was a deliberate sabotage of production code that the current suite did NOT catch:",
|
|
10161
|
+
...confirmed.map(row),
|
|
10162
|
+
""
|
|
10163
|
+
] : [],
|
|
10164
|
+
...unverified.length > 0 ? [
|
|
10165
|
+
"UNVERIFIED \u2014 these mutations were never actually re-tested (the re-verify pass skipped or did not report them). Do NOT assume the suite misses them: first apply each mutation, run the tests, and confirm it really survives; if the tests already fail, report that instead of writing new assertions.",
|
|
10166
|
+
...unverified.map(row),
|
|
10167
|
+
""
|
|
10168
|
+
] : [],
|
|
10169
|
+
`Test command that must fail under each CONFIRMED mutant: ${i.testCommand}`,
|
|
9974
10170
|
"",
|
|
9975
|
-
"For each survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
|
|
10171
|
+
"For each CONFIRMED survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
|
|
9976
10172
|
].join("\n");
|
|
9977
10173
|
}
|
|
9978
10174
|
function collectOutcomes(result, plan) {
|
|
9979
10175
|
const fromText = parseTextOutcomes(result);
|
|
9980
10176
|
if (fromText.length > 0) {
|
|
9981
|
-
const
|
|
9982
|
-
const matched =
|
|
9983
|
-
|
|
10177
|
+
const remaining = [...plan];
|
|
10178
|
+
const matched = [];
|
|
10179
|
+
for (const m of fromText) {
|
|
10180
|
+
const idx = remaining.findIndex((p) => p.id === m.id);
|
|
10181
|
+
if (idx === -1) continue;
|
|
10182
|
+
remaining.splice(idx, 1);
|
|
10183
|
+
matched.push(m);
|
|
10184
|
+
}
|
|
10185
|
+
if (matched.length > 0) {
|
|
10186
|
+
const missing = remaining.map((p) => ({
|
|
10187
|
+
id: p.id,
|
|
10188
|
+
file: p.file,
|
|
10189
|
+
line: p.line,
|
|
10190
|
+
kind: p.kind,
|
|
10191
|
+
status: "skipped",
|
|
10192
|
+
evidence: "not reported by chaos task"
|
|
10193
|
+
}));
|
|
10194
|
+
return [...matched, ...missing];
|
|
10195
|
+
}
|
|
9984
10196
|
}
|
|
9985
10197
|
return plan.map((p) => ({
|
|
9986
10198
|
id: p.id,
|
|
@@ -9991,6 +10203,9 @@ function collectOutcomes(result, plan) {
|
|
|
9991
10203
|
evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
|
|
9992
10204
|
}));
|
|
9993
10205
|
}
|
|
10206
|
+
function isKill(status) {
|
|
10207
|
+
return status === "killed" || status === "killed-by-hang";
|
|
10208
|
+
}
|
|
9994
10209
|
function parseTextOutcomes(result) {
|
|
9995
10210
|
const text = typeof result?.result === "string" ? result.result : void 0;
|
|
9996
10211
|
if (!text) return [];
|
|
@@ -10693,6 +10908,14 @@ var PATTERNS = [
|
|
|
10693
10908
|
anchor: "sk-ant-"
|
|
10694
10909
|
},
|
|
10695
10910
|
{ type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
|
|
10911
|
+
{
|
|
10912
|
+
// `xai` is a first-class provider in this codebase, but its key shape was
|
|
10913
|
+
// absent here — so the one credential format WrongStack itself hands users
|
|
10914
|
+
// was the one the scrubber could not recognize (audit 2026-08-20).
|
|
10915
|
+
type: "xai_key",
|
|
10916
|
+
regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
|
|
10917
|
+
anchor: "xai-"
|
|
10918
|
+
},
|
|
10696
10919
|
{ type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
|
|
10697
10920
|
{ type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
|
|
10698
10921
|
{ type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
|
|
@@ -10783,8 +11006,8 @@ var PATTERNS = [
|
|
|
10783
11006
|
// replacement so the separator between adjacent secrets is preserved
|
|
10784
11007
|
// rather than collapsed. Capture groups are therefore: 1=leading
|
|
10785
11008
|
// delimiter, 2=key name, 3=value.
|
|
10786
|
-
regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
|
|
10787
|
-
anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
|
|
11009
|
+
regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
|
|
11010
|
+
anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
|
|
10788
11011
|
},
|
|
10789
11012
|
{
|
|
10790
11013
|
type: "json_credential_key",
|
|
@@ -10901,6 +11124,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
|
|
|
10901
11124
|
var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
|
|
10902
11125
|
var SCRUB_CHUNK_BYTES = 64 * 1024;
|
|
10903
11126
|
var SCRUB_OVERLAP_BYTES = 1024;
|
|
11127
|
+
var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
|
|
11128
|
+
var PEM_END_MARKER = "-----END";
|
|
11129
|
+
var MAX_PEM_BLOCK_BYTES = 64 * 1024;
|
|
11130
|
+
var PEM_END_LINE_TOLERANCE = 64;
|
|
11131
|
+
function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
|
|
11132
|
+
const head = text.slice(chunkStart, proposedEnd);
|
|
11133
|
+
const lastBegin = head.lastIndexOf("-----BEGIN ");
|
|
11134
|
+
if (lastBegin === -1) return proposedEnd;
|
|
11135
|
+
const fromBegin = text.slice(chunkStart + lastBegin);
|
|
11136
|
+
const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
|
|
11137
|
+
if (!marker || marker.index !== 0) return proposedEnd;
|
|
11138
|
+
const bodyStart = marker[0].length;
|
|
11139
|
+
const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
|
|
11140
|
+
const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
|
|
11141
|
+
if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
|
|
11142
|
+
return proposedEnd;
|
|
11143
|
+
}
|
|
11144
|
+
const lineEnd = fromBegin.indexOf("\n", closeIdx);
|
|
11145
|
+
const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
|
|
11146
|
+
return Math.max(proposedEnd, end);
|
|
11147
|
+
}
|
|
10904
11148
|
var PATTERN_ANCHORS = [
|
|
10905
11149
|
...new Set(
|
|
10906
11150
|
PATTERNS.flatMap(
|
|
@@ -10937,6 +11181,7 @@ var DefaultSecretScrubber = class {
|
|
|
10937
11181
|
}
|
|
10938
11182
|
}
|
|
10939
11183
|
end = safe === -1 ? end : safe + 1;
|
|
11184
|
+
end = extendChunkBoundaryPastPem(text, i, end);
|
|
10940
11185
|
}
|
|
10941
11186
|
out.push(this.scrubOne(text.slice(i, end)));
|
|
10942
11187
|
i = end;
|
|
@@ -14768,9 +15013,21 @@ function renderCommandLine(command, args) {
|
|
|
14768
15013
|
});
|
|
14769
15014
|
return [command, ...rendered].join(" ");
|
|
14770
15015
|
}
|
|
14771
|
-
function
|
|
15016
|
+
function renderSubjectFields(obj, fields) {
|
|
15017
|
+
const parts = [];
|
|
15018
|
+
for (const field of fields) {
|
|
15019
|
+
const value = obj[field];
|
|
15020
|
+
if (value === void 0 || value === null || value === "" || value === false) continue;
|
|
15021
|
+
const str = String(value);
|
|
15022
|
+
parts.push(`${field}=${/\s/.test(str) ? `"${str.replace(/"/g, '\\"')}"` : str}`);
|
|
15023
|
+
}
|
|
15024
|
+
return parts.join(" ");
|
|
15025
|
+
}
|
|
15026
|
+
function subjectForToolInput(toolName, input, subjectKey, subjectFields) {
|
|
14772
15027
|
if (!input || typeof input !== "object") return void 0;
|
|
14773
15028
|
const obj = input;
|
|
15029
|
+
const extra = subjectFields && subjectFields.length > 0 ? renderSubjectFields(obj, subjectFields) : "";
|
|
15030
|
+
const withExtra = (base) => extra ? `${base} ${extra}` : base;
|
|
14774
15031
|
if (subjectKey) {
|
|
14775
15032
|
const value = obj[subjectKey];
|
|
14776
15033
|
if (Array.isArray(value)) {
|
|
@@ -14786,9 +15043,9 @@ function subjectForToolInput(toolName, input, subjectKey) {
|
|
|
14786
15043
|
if (subjectKey === "command") {
|
|
14787
15044
|
const rendered = renderCommandLine(value, obj["args"]);
|
|
14788
15045
|
if (value === "commit" && obj["dry_run"] === true) {
|
|
14789
|
-
return `${escapeGlobSubject(rendered)}:dry-run`;
|
|
15046
|
+
return `${escapeGlobSubject(withExtra(rendered))}:dry-run`;
|
|
14790
15047
|
}
|
|
14791
|
-
return escapeGlobSubject(rendered);
|
|
15048
|
+
return escapeGlobSubject(withExtra(rendered));
|
|
14792
15049
|
}
|
|
14793
15050
|
if (subjectKey === "directory" && obj["dry_run"] === true) {
|
|
14794
15051
|
return `${escapeGlobSubject(value)}:dry-run`;
|
|
@@ -20778,6 +21035,7 @@ function worktreeOwnerLabel(task, config) {
|
|
|
20778
21035
|
}
|
|
20779
21036
|
|
|
20780
21037
|
// src/coordination/director.ts
|
|
21038
|
+
var BUSY_REARM_FLOOR_MS = 1e3;
|
|
20781
21039
|
var Director = class _Director {
|
|
20782
21040
|
/* eslint-disable-next-line @typescript-eslint/no-unused-vars — just a cast helper */
|
|
20783
21041
|
static _asManifestEntry(v) {
|
|
@@ -20843,6 +21101,13 @@ var Director = class _Director {
|
|
|
20843
21101
|
subagentIdleTimeoutMs;
|
|
20844
21102
|
retireSubagentOnTaskComplete;
|
|
20845
21103
|
subagentIdleTimers = /* @__PURE__ */ new Map();
|
|
21104
|
+
/**
|
|
21105
|
+
* Effective idle window per subagent (spawn-time `idleTimeoutMs` override
|
|
21106
|
+
* or the Director-wide default; undefined = no window). Internal-task
|
|
21107
|
+
* completion re-arms with THIS value, not the Director-wide default, so
|
|
21108
|
+
* a subagent-configured window survives its first internal probe.
|
|
21109
|
+
*/
|
|
21110
|
+
subagentIdleDelayMs = /* @__PURE__ */ new Map();
|
|
20846
21111
|
sharedScratchpadPath;
|
|
20847
21112
|
maxSpawns;
|
|
20848
21113
|
maxSpawnDepth;
|
|
@@ -20999,7 +21264,13 @@ var Director = class _Director {
|
|
|
20999
21264
|
handleTaskCompleted(payload) {
|
|
21000
21265
|
const r = payload.result;
|
|
21001
21266
|
const settled = this.tasks.settle(r);
|
|
21002
|
-
if (settled.internal)
|
|
21267
|
+
if (settled.internal) {
|
|
21268
|
+
this.armSubagentIdleRetirement(
|
|
21269
|
+
r.subagentId,
|
|
21270
|
+
this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
|
|
21271
|
+
);
|
|
21272
|
+
return;
|
|
21273
|
+
}
|
|
21003
21274
|
const title = this.tasks.descriptionFor(r.taskId, payload.task.description ?? r.taskId);
|
|
21004
21275
|
if (!settled.consumedInBand && this.taskResultNotifier) {
|
|
21005
21276
|
const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
|
|
@@ -21064,7 +21335,7 @@ var Director = class _Director {
|
|
|
21064
21335
|
}
|
|
21065
21336
|
this.armSubagentIdleRetirement(
|
|
21066
21337
|
r.subagentId,
|
|
21067
|
-
this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
|
|
21338
|
+
this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
|
|
21068
21339
|
);
|
|
21069
21340
|
}
|
|
21070
21341
|
extensionsFor(subagentId) {
|
|
@@ -21150,6 +21421,7 @@ var Director = class _Director {
|
|
|
21150
21421
|
this.resolveSpawnModel(config);
|
|
21151
21422
|
const subagentId = await spawn3(this, config, priceLookup);
|
|
21152
21423
|
const perSubagentIdleMs = typeof config.idleTimeoutMs === "number" && Number.isFinite(config.idleTimeoutMs) && config.idleTimeoutMs >= 0 ? config.idleTimeoutMs : this.subagentIdleTimeoutMs;
|
|
21424
|
+
this.subagentIdleDelayMs.set(subagentId, perSubagentIdleMs);
|
|
21153
21425
|
this.armSubagentIdleRetirement(subagentId, perSubagentIdleMs);
|
|
21154
21426
|
return subagentId;
|
|
21155
21427
|
}
|
|
@@ -21203,6 +21475,7 @@ var Director = class _Director {
|
|
|
21203
21475
|
this.budgetPolicy.dispose();
|
|
21204
21476
|
for (const timer of this.subagentIdleTimers.values()) clearTimeout(timer);
|
|
21205
21477
|
this.subagentIdleTimers.clear();
|
|
21478
|
+
this.subagentIdleDelayMs.clear();
|
|
21206
21479
|
await this.coordinator.stopAll();
|
|
21207
21480
|
this.tasks.resolveWaitersOnShutdown();
|
|
21208
21481
|
for (const b of this.subagentBridges.values()) {
|
|
@@ -21261,6 +21534,7 @@ var Director = class _Director {
|
|
|
21261
21534
|
}
|
|
21262
21535
|
async remove(subagentId) {
|
|
21263
21536
|
this.clearSubagentIdleRetirement(subagentId);
|
|
21537
|
+
this.subagentIdleDelayMs.delete(subagentId);
|
|
21264
21538
|
void this.appendSessionEvent({
|
|
21265
21539
|
type: "agent_stopped",
|
|
21266
21540
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -21313,9 +21587,13 @@ var Director = class _Director {
|
|
|
21313
21587
|
const timer = setTimeout(() => {
|
|
21314
21588
|
this.subagentIdleTimers.delete(subagentId);
|
|
21315
21589
|
const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
|
|
21316
|
-
if (entry
|
|
21590
|
+
if (entry === void 0) return;
|
|
21591
|
+
if (entry.status !== "idle") {
|
|
21592
|
+
this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
|
|
21593
|
+
return;
|
|
21594
|
+
}
|
|
21317
21595
|
if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
|
|
21318
|
-
this.armSubagentIdleRetirement(subagentId,
|
|
21596
|
+
this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
|
|
21319
21597
|
return;
|
|
21320
21598
|
}
|
|
21321
21599
|
void this.remove(subagentId).catch(
|
|
@@ -28587,6 +28865,10 @@ var DefaultSkillLoader = class {
|
|
|
28587
28865
|
);
|
|
28588
28866
|
for (const e of entries) {
|
|
28589
28867
|
if (!await entryIsDirectory(dir, e)) continue;
|
|
28868
|
+
if (!isValidSkillNameFormat(e.name)) {
|
|
28869
|
+
this.skipped.push({ dir, entry: e.name, reason: "invalid-name-format" });
|
|
28870
|
+
continue;
|
|
28871
|
+
}
|
|
28590
28872
|
const skillFile = path30.join(dir, e.name, "SKILL.md");
|
|
28591
28873
|
let raw;
|
|
28592
28874
|
try {
|
|
@@ -30260,7 +30542,7 @@ var ToolExecutor = class _ToolExecutor {
|
|
|
30260
30542
|
return { result, tool, durationMs: Date.now() - start };
|
|
30261
30543
|
}
|
|
30262
30544
|
if (effectivePermission === "confirm") {
|
|
30263
|
-
const suggestedPattern = boundary.decision === "confirm" ? `kanban-boundary:${boundary.path ?? tool.name}` : subjectForToolInput(tool.name, use.input, tool.subjectKey) ?? tool.name;
|
|
30545
|
+
const suggestedPattern = boundary.decision === "confirm" ? `kanban-boundary:${boundary.path ?? tool.name}` : subjectForToolInput(tool.name, use.input, tool.subjectKey, tool.subjectFields) ?? tool.name;
|
|
30264
30546
|
if (this.opts.confirmAwaiter) {
|
|
30265
30547
|
const awaiter = this.opts.confirmAwaiter;
|
|
30266
30548
|
const choice = await new Promise(
|
|
@@ -32790,7 +33072,9 @@ function hasRecursiveForceDelete(command, projectRoot) {
|
|
|
32790
33072
|
if (token === "rd" || token === "rmdir") {
|
|
32791
33073
|
const args = commandSegment(tokens, i + 1).map((arg) => arg.toLowerCase());
|
|
32792
33074
|
if (args.includes("/s")) {
|
|
32793
|
-
const targets = args.filter(
|
|
33075
|
+
const targets = args.filter(
|
|
33076
|
+
(arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
|
|
33077
|
+
);
|
|
32794
33078
|
if (targets.length === 0) return true;
|
|
32795
33079
|
if (targets.some(isCatastrophicDeleteTarget)) return true;
|
|
32796
33080
|
if (targets.some((target) => !pathLooksInsideProject(target, projectRoot))) return true;
|
|
@@ -32859,7 +33143,8 @@ function hasFindExec(command) {
|
|
|
32859
33143
|
function isCatastrophicDeleteTarget(rawTarget) {
|
|
32860
33144
|
const t = rawTarget.replace(/^['"]|['"]$/g, "").trim();
|
|
32861
33145
|
if (!t) return false;
|
|
32862
|
-
if (t === "*" || t === "." || t === "./" || t === ".\\" || t === "./*" || t === ".\\*")
|
|
33146
|
+
if (t === "*" || t === "." || t === "./" || t === ".\\" || t === "./*" || t === ".\\*")
|
|
33147
|
+
return true;
|
|
32863
33148
|
const s = t.replace(/[\\/]\*+$/, "").replace(/[\\/]+$/, "");
|
|
32864
33149
|
if (s === "") return true;
|
|
32865
33150
|
if (s === "~" || /^\$HOME$/i.test(s) || /^%USERPROFILE%$/i.test(s)) return true;
|
|
@@ -32899,12 +33184,16 @@ function hasCatastrophicDelete(command) {
|
|
|
32899
33184
|
const args = tokens.slice(i + 1);
|
|
32900
33185
|
const recursive = args.some((arg) => arg.toLowerCase() === "/s");
|
|
32901
33186
|
if (!recursive) continue;
|
|
32902
|
-
const targets = args.filter(
|
|
33187
|
+
const targets = args.filter(
|
|
33188
|
+
(arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
|
|
33189
|
+
);
|
|
32903
33190
|
if (targets.some(isCatastrophicDeleteTarget)) return true;
|
|
32904
33191
|
}
|
|
32905
33192
|
if (token === "del" || token === "erase") {
|
|
32906
33193
|
const args = tokens.slice(i + 1);
|
|
32907
|
-
const targets = args.filter(
|
|
33194
|
+
const targets = args.filter(
|
|
33195
|
+
(arg) => !arg.startsWith("-") && !arg.startsWith("/") && !SHELL_OPERATORS.has(arg)
|
|
33196
|
+
);
|
|
32908
33197
|
if (targets.some(isCatastrophicDeleteTarget)) return true;
|
|
32909
33198
|
}
|
|
32910
33199
|
}
|
|
@@ -32965,6 +33254,47 @@ function isClearlyDestructiveBashCommand(command, projectRoot) {
|
|
|
32965
33254
|
if (HIGH_IMPACT_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
|
|
32966
33255
|
return false;
|
|
32967
33256
|
}
|
|
33257
|
+
var WELL_KNOWN_CREDENTIAL_ENV_VARS = /* @__PURE__ */ new Set([
|
|
33258
|
+
"ANTHROPIC_API_KEY",
|
|
33259
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
33260
|
+
"OPENAI_API_KEY",
|
|
33261
|
+
"AZURE_OPENAI_API_KEY",
|
|
33262
|
+
"GEMINI_API_KEY",
|
|
33263
|
+
"GOOGLE_API_KEY",
|
|
33264
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
33265
|
+
"GOOGLE_GENERATIVE_AI_API_KEY",
|
|
33266
|
+
"GROQ_API_KEY",
|
|
33267
|
+
"MISTRAL_API_KEY",
|
|
33268
|
+
"COHERE_API_KEY",
|
|
33269
|
+
"DEEPSEEK_API_KEY",
|
|
33270
|
+
"XAI_API_KEY",
|
|
33271
|
+
"OPENROUTER_API_KEY",
|
|
33272
|
+
"PERPLEXITY_API_KEY",
|
|
33273
|
+
"TOGETHER_API_KEY",
|
|
33274
|
+
"FIREWORKS_API_KEY",
|
|
33275
|
+
"HUGGINGFACE_API_KEY",
|
|
33276
|
+
"HF_TOKEN",
|
|
33277
|
+
"GITHUB_TOKEN",
|
|
33278
|
+
"GH_TOKEN",
|
|
33279
|
+
"NPM_TOKEN",
|
|
33280
|
+
"AWS_ACCESS_KEY_ID",
|
|
33281
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
33282
|
+
"AWS_SESSION_TOKEN",
|
|
33283
|
+
"AZURE_CLIENT_SECRET",
|
|
33284
|
+
"GITLAB_TOKEN",
|
|
33285
|
+
"SLACK_TOKEN",
|
|
33286
|
+
"STRIPE_SECRET_KEY",
|
|
33287
|
+
"TELEGRAM_BOT_TOKEN",
|
|
33288
|
+
"WRONGSTACK_VAULT_PASSPHRASE"
|
|
33289
|
+
]);
|
|
33290
|
+
function attachesWellKnownCredential(input) {
|
|
33291
|
+
if (!input || typeof input !== "object") return false;
|
|
33292
|
+
const envVars = input["envVars"];
|
|
33293
|
+
if (!Array.isArray(envVars)) return false;
|
|
33294
|
+
return envVars.some(
|
|
33295
|
+
(name) => typeof name === "string" && WELL_KNOWN_CREDENTIAL_ENV_VARS.has(name.toUpperCase())
|
|
33296
|
+
);
|
|
33297
|
+
}
|
|
32968
33298
|
|
|
32969
33299
|
// src/security/permission-helpers.ts
|
|
32970
33300
|
function matchesTrust(patterns, subject) {
|
|
@@ -32981,7 +33311,7 @@ function hasShellSubject(tool) {
|
|
|
32981
33311
|
]);
|
|
32982
33312
|
}
|
|
32983
33313
|
function alwaysAllowUnavailableReason(tool, input) {
|
|
32984
|
-
const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
|
|
33314
|
+
const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
|
|
32985
33315
|
if (subject !== void 0) return void 0;
|
|
32986
33316
|
return `"always allow" needs a subject to remember, and ${tool.name} calls do not carry one (no subjectKey, and no path/url/name input). Recording it would store a rule that can never match. Approve this call, or set a trust rule for ${tool.name} explicitly.`;
|
|
32987
33317
|
}
|
|
@@ -33041,8 +33371,18 @@ var AGENT_STATE_SENSITIVE_BASENAMES = /^(?:config\.json|config\.local\.json|trus
|
|
|
33041
33371
|
function unescapeGlobSubject(value) {
|
|
33042
33372
|
return value.replace(/\\([*?[\]])/g, "$1");
|
|
33043
33373
|
}
|
|
33374
|
+
function stripAdsSuffix(forwardSlashPath) {
|
|
33375
|
+
const cut = forwardSlashPath.lastIndexOf("/");
|
|
33376
|
+
const dir = cut === -1 ? "" : forwardSlashPath.slice(0, cut + 1);
|
|
33377
|
+
const base = cut === -1 ? forwardSlashPath : forwardSlashPath.slice(cut + 1);
|
|
33378
|
+
const colon = base.indexOf(":");
|
|
33379
|
+
if (colon === -1 || cut === -1 && colon === 1 && base.length <= 2) return forwardSlashPath;
|
|
33380
|
+
return dir + base.slice(0, colon);
|
|
33381
|
+
}
|
|
33044
33382
|
function normalizeForCompare(value) {
|
|
33045
|
-
const forward =
|
|
33383
|
+
const forward = stripAdsSuffix(
|
|
33384
|
+
unescapeGlobSubject(value).replace(/\\/g, "/").replace(/\/+$/, "")
|
|
33385
|
+
);
|
|
33046
33386
|
return process.platform === "win32" ? forward.toLowerCase() : forward;
|
|
33047
33387
|
}
|
|
33048
33388
|
function realpathOfNearestExisting(p) {
|
|
@@ -33079,7 +33419,7 @@ function isProtectedAgentStatePath(absPath) {
|
|
|
33079
33419
|
return AGENT_STATE_SENSITIVE_BASENAMES.test(path40.basename(normalizeForCompare(absPath)));
|
|
33080
33420
|
}
|
|
33081
33421
|
function pathLooksSensitive(rawPath) {
|
|
33082
|
-
const normalized = stripShellQuotes(rawPath).replace(/\\/g, "/");
|
|
33422
|
+
const normalized = stripAdsSuffix(stripShellQuotes(rawPath).replace(/\\/g, "/"));
|
|
33083
33423
|
if (SENSITIVE_READ_PATHS.some((pattern) => pattern.test(normalized))) return true;
|
|
33084
33424
|
return isProtectedAgentStatePath(normalized);
|
|
33085
33425
|
}
|
|
@@ -33103,10 +33443,24 @@ function shellCommandReadsSensitivePath(command) {
|
|
|
33103
33443
|
}
|
|
33104
33444
|
return false;
|
|
33105
33445
|
}
|
|
33446
|
+
function isSensitiveReadCall(tool, input) {
|
|
33447
|
+
const isReadTool = hasCapability(tool, ToolCapabilities.FS_READ) || tool.name === "read" || tool.name === "grep" || tool.name === "glob" || tool.name === "tree";
|
|
33448
|
+
if (isReadTool && inputPathLooksSensitive(input)) return true;
|
|
33449
|
+
const hasShellCap = hasCapability(tool, [
|
|
33450
|
+
ToolCapabilities.SHELL_ARBITRARY,
|
|
33451
|
+
ToolCapabilities.SHELL_RESTRICTED,
|
|
33452
|
+
ToolCapabilities.SHELL_EXEC
|
|
33453
|
+
]);
|
|
33454
|
+
if (!hasShellCap && tool.name !== "bash" && tool.name !== "shell" && tool.name !== "exec") {
|
|
33455
|
+
return false;
|
|
33456
|
+
}
|
|
33457
|
+
const command = shellCommandLineFromInput(input);
|
|
33458
|
+
return command ? shellCommandReadsSensitivePath(command) : false;
|
|
33459
|
+
}
|
|
33106
33460
|
|
|
33107
33461
|
// src/security/permission-explain.ts
|
|
33108
33462
|
function explainPermissionTrace(state, tool, input, ctx) {
|
|
33109
|
-
const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
|
|
33463
|
+
const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
|
|
33110
33464
|
const steps = [];
|
|
33111
33465
|
let winnerIndex = -1;
|
|
33112
33466
|
const add = (rule, matched, decision, source, detail) => {
|
|
@@ -33327,13 +33681,7 @@ function explainPermissionTrace(state, tool, input, ctx) {
|
|
|
33327
33681
|
}
|
|
33328
33682
|
};
|
|
33329
33683
|
}
|
|
33330
|
-
add(
|
|
33331
|
-
"yolo",
|
|
33332
|
-
true,
|
|
33333
|
-
"auto",
|
|
33334
|
-
"yolo",
|
|
33335
|
-
"YOLO mode is active \u2014 auto-approving every non-denied call"
|
|
33336
|
-
);
|
|
33684
|
+
add("yolo", true, "auto", "yolo", "YOLO mode is active \u2014 auto-approving every non-denied call");
|
|
33337
33685
|
winnerIndex = steps.length - 1;
|
|
33338
33686
|
return {
|
|
33339
33687
|
toolName: tool.name,
|
|
@@ -33604,7 +33952,14 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
|
33604
33952
|
static isMcpTool(name) {
|
|
33605
33953
|
return name.startsWith("mcp__");
|
|
33606
33954
|
}
|
|
33607
|
-
async evaluate(tool) {
|
|
33955
|
+
async evaluate(tool, input) {
|
|
33956
|
+
if (input !== void 0 && isSensitiveReadCall(tool, input)) {
|
|
33957
|
+
return {
|
|
33958
|
+
permission: "deny",
|
|
33959
|
+
source: "subagent_guard",
|
|
33960
|
+
reason: "subagents may not read credential-bearing paths \u2014 the leader must perform this read so the user can approve it"
|
|
33961
|
+
};
|
|
33962
|
+
}
|
|
33608
33963
|
const caps = tool.capabilities ?? [];
|
|
33609
33964
|
const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
|
|
33610
33965
|
const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
|
|
@@ -33631,8 +33986,8 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
|
33631
33986
|
}
|
|
33632
33987
|
allowOnce() {
|
|
33633
33988
|
}
|
|
33634
|
-
async explain(tool) {
|
|
33635
|
-
const decision = await this.evaluate(tool);
|
|
33989
|
+
async explain(tool, input) {
|
|
33990
|
+
const decision = await this.evaluate(tool, input);
|
|
33636
33991
|
return {
|
|
33637
33992
|
toolName: tool.name,
|
|
33638
33993
|
subject: null,
|
|
@@ -33682,6 +34037,17 @@ function fsWriteTargetPaths(input) {
|
|
|
33682
34037
|
}
|
|
33683
34038
|
return out;
|
|
33684
34039
|
}
|
|
34040
|
+
function mergeTrustEntries(exact, wildcard) {
|
|
34041
|
+
if (!exact) return wildcard;
|
|
34042
|
+
if (!wildcard) return exact;
|
|
34043
|
+
const deny = [...wildcard.deny ?? [], ...exact.deny ?? []];
|
|
34044
|
+
const merged = {
|
|
34045
|
+
...wildcard,
|
|
34046
|
+
...exact
|
|
34047
|
+
};
|
|
34048
|
+
if (deny.length > 0) merged.deny = [...new Set(deny)];
|
|
34049
|
+
return merged;
|
|
34050
|
+
}
|
|
33685
34051
|
var DefaultPermissionPolicy = class {
|
|
33686
34052
|
policy = {};
|
|
33687
34053
|
loaded = false;
|
|
@@ -33720,6 +34086,7 @@ var DefaultPermissionPolicy = class {
|
|
|
33720
34086
|
yoloBlockedAsDestructive(tool, input, ctx) {
|
|
33721
34087
|
if (!this.yolo || this.yoloDestructive) return false;
|
|
33722
34088
|
if (this.hasAgentStateWriteTarget(tool, input, ctx)) return true;
|
|
34089
|
+
if (attachesWellKnownCredential(input)) return true;
|
|
33723
34090
|
const isShellSurface = tool.name === "bash" || tool.name === "exec" || (tool.capabilities ?? []).includes("shell.arbitrary");
|
|
33724
34091
|
if (!isShellSurface) return false;
|
|
33725
34092
|
const command = getInputString(input, "command") ?? shellCommandLineFromInput(input);
|
|
@@ -33813,8 +34180,8 @@ var DefaultPermissionPolicy = class {
|
|
|
33813
34180
|
};
|
|
33814
34181
|
}
|
|
33815
34182
|
const namespaceEntry = this.findNamespaceEntry(tool.name);
|
|
33816
|
-
const entry = this.policy[tool.name]
|
|
33817
|
-
const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
|
|
34183
|
+
const entry = mergeTrustEntries(this.policy[tool.name], namespaceEntry);
|
|
34184
|
+
const subject = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields);
|
|
33818
34185
|
const cacheKey = `${tool.name}::${subject ?? tool.name}`;
|
|
33819
34186
|
const evalKey = `${cacheKey}::${permissionFingerprint(tool)}`;
|
|
33820
34187
|
if (tool.name !== "write" && !this.hasAgentStateWriteTarget(tool, input, ctx)) {
|
|
@@ -33831,15 +34198,6 @@ var DefaultPermissionPolicy = class {
|
|
|
33831
34198
|
this._evalCache.set(evalKey, decision);
|
|
33832
34199
|
return decision;
|
|
33833
34200
|
}
|
|
33834
|
-
if (this.sessionAllowed.has(cacheKey)) {
|
|
33835
|
-
this.sessionAllowed.delete(cacheKey);
|
|
33836
|
-
const decision = {
|
|
33837
|
-
permission: "auto",
|
|
33838
|
-
source: "trust",
|
|
33839
|
-
reason: "session one-shot allow (user pressed yes)"
|
|
33840
|
-
};
|
|
33841
|
-
return decision;
|
|
33842
|
-
}
|
|
33843
34201
|
if (entry?.deny && subject && matchesTrust(entry.deny, subject)) {
|
|
33844
34202
|
this._logDeny(tool.name, subject, "matched deny pattern");
|
|
33845
34203
|
const decision = {
|
|
@@ -33850,6 +34208,15 @@ var DefaultPermissionPolicy = class {
|
|
|
33850
34208
|
this._evalCache.set(evalKey, decision);
|
|
33851
34209
|
return decision;
|
|
33852
34210
|
}
|
|
34211
|
+
if (this.sessionAllowed.has(cacheKey)) {
|
|
34212
|
+
this.sessionAllowed.delete(cacheKey);
|
|
34213
|
+
const decision = {
|
|
34214
|
+
permission: "auto",
|
|
34215
|
+
source: "trust",
|
|
34216
|
+
reason: "session one-shot allow (user pressed yes)"
|
|
34217
|
+
};
|
|
34218
|
+
return decision;
|
|
34219
|
+
}
|
|
33853
34220
|
if (tool.permission === "deny") {
|
|
33854
34221
|
this._logDeny(tool.name, subject, "tool default deny");
|
|
33855
34222
|
const decision = {
|
|
@@ -33860,6 +34227,7 @@ var DefaultPermissionPolicy = class {
|
|
|
33860
34227
|
this._evalCache.set(evalKey, decision);
|
|
33861
34228
|
return decision;
|
|
33862
34229
|
}
|
|
34230
|
+
const denyUnevaluated = Boolean(entry?.deny?.length) && subject === void 0;
|
|
33863
34231
|
const allowMatches = hasShellSubject(tool) ? matchesCommandTrust : matchesTrust;
|
|
33864
34232
|
if (entry?.allow && subject && allowMatches(entry.allow, subject)) {
|
|
33865
34233
|
const decision = {
|
|
@@ -33870,7 +34238,7 @@ var DefaultPermissionPolicy = class {
|
|
|
33870
34238
|
this._evalCache.set(evalKey, decision);
|
|
33871
34239
|
return decision;
|
|
33872
34240
|
}
|
|
33873
|
-
if (entry?.auto) {
|
|
34241
|
+
if (entry?.auto && !denyUnevaluated) {
|
|
33874
34242
|
const decision = { permission: "auto", source: "trust" };
|
|
33875
34243
|
this._evalCache.set(evalKey, decision);
|
|
33876
34244
|
return decision;
|
|
@@ -33970,19 +34338,10 @@ var DefaultPermissionPolicy = class {
|
|
|
33970
34338
|
}
|
|
33971
34339
|
return { permission: "confirm", source: "default" };
|
|
33972
34340
|
}
|
|
34341
|
+
// Delegates to the shared helper so the subagent policy applies the exact
|
|
34342
|
+
// same rule — see `isSensitiveReadCall` in ./permission-helpers.ts.
|
|
33973
34343
|
isSensitiveReadCall(tool, input) {
|
|
33974
|
-
|
|
33975
|
-
if (isReadTool && inputPathLooksSensitive(input)) return true;
|
|
33976
|
-
const hasShellCap = hasCapability(tool, [
|
|
33977
|
-
ToolCapabilities.SHELL_ARBITRARY,
|
|
33978
|
-
ToolCapabilities.SHELL_RESTRICTED,
|
|
33979
|
-
ToolCapabilities.SHELL_EXEC
|
|
33980
|
-
]);
|
|
33981
|
-
if (!hasShellCap && tool.name !== "bash" && tool.name !== "shell" && tool.name !== "exec") {
|
|
33982
|
-
return false;
|
|
33983
|
-
}
|
|
33984
|
-
const command = shellCommandLineFromInput(input);
|
|
33985
|
-
return command ? shellCommandReadsSensitivePath(command) : false;
|
|
34344
|
+
return isSensitiveReadCall(tool, input);
|
|
33986
34345
|
}
|
|
33987
34346
|
async trust(rule) {
|
|
33988
34347
|
if (!this.loaded) await this.reload();
|
|
@@ -34106,7 +34465,7 @@ function walk2(node, vault, transform) {
|
|
|
34106
34465
|
}
|
|
34107
34466
|
return out;
|
|
34108
34467
|
}
|
|
34109
|
-
var SECRET_KEY_PATTERN = /(?:
|
|
34468
|
+
var SECRET_KEY_PATTERN = /(?:api[-_]?key|auth[-_]?token|authorization|proxy-authorization|cookie|bearer|secret|password|passwd|pwd|refresh[-_]?token|session[-_]?key|access[_-]?token|private[_-]?key|token\b)/i;
|
|
34110
34469
|
var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
|
|
34111
34470
|
function isSecretField(name) {
|
|
34112
34471
|
const lc = name.toLowerCase();
|
|
@@ -34215,6 +34574,14 @@ function keyFileNeedsHardening(keyFile, opts) {
|
|
|
34215
34574
|
}
|
|
34216
34575
|
return false;
|
|
34217
34576
|
}
|
|
34577
|
+
function mkdirSecretDirSync(dir) {
|
|
34578
|
+
fs14.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
34579
|
+
if (process.platform === "win32") return;
|
|
34580
|
+
try {
|
|
34581
|
+
fs14.chmodSync(dir, 448);
|
|
34582
|
+
} catch {
|
|
34583
|
+
}
|
|
34584
|
+
}
|
|
34218
34585
|
function writeKeyFileAtomicSync(keyFile, content) {
|
|
34219
34586
|
const tmp = `${keyFile}.${randomBytes3(4).toString("hex")}.tmp`;
|
|
34220
34587
|
const fd = fs14.openSync(tmp, "w", 384);
|
|
@@ -34362,7 +34729,7 @@ var DefaultSecretVault = class {
|
|
|
34362
34729
|
const oldVersion = this._keyVersion;
|
|
34363
34730
|
const newKey = randomBytes3(KEY_BYTES);
|
|
34364
34731
|
const newVersion = oldVersion + 1;
|
|
34365
|
-
|
|
34732
|
+
mkdirSecretDirSync(path42.dirname(this.keyFile));
|
|
34366
34733
|
const passphrase = getVaultPassphrase();
|
|
34367
34734
|
if (passphrase) {
|
|
34368
34735
|
writeKeyFileAtomicSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase));
|
|
@@ -34446,7 +34813,7 @@ var DefaultSecretVault = class {
|
|
|
34446
34813
|
} catch (err) {
|
|
34447
34814
|
if (err.code !== "ENOENT") throw err;
|
|
34448
34815
|
}
|
|
34449
|
-
|
|
34816
|
+
mkdirSecretDirSync(path42.dirname(this.keyFile));
|
|
34450
34817
|
const key = randomBytes3(KEY_BYTES);
|
|
34451
34818
|
const passphrase = getVaultPassphrase();
|
|
34452
34819
|
const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
|
|
@@ -35148,6 +35515,17 @@ var IN_PROJECT_DENIED_PATHS = [
|
|
|
35148
35515
|
// See discover-mailbox-bridge.ts:findWorkspaceCliEntry.
|
|
35149
35516
|
path: "features.mailboxBridge",
|
|
35150
35517
|
reason: "Enables the mailbox bridge, whose CLI-entry resolution walks up from the project root \u2014 a repo-supplied packages/cli/dist/index.js would be spawned on WebUI boot."
|
|
35518
|
+
},
|
|
35519
|
+
{
|
|
35520
|
+
// `plugins` is already denied above, so a repo cannot ADD a plugin. This
|
|
35521
|
+
// closes the other half: a repo could previously ship
|
|
35522
|
+
// `{"features":{"pluginsTrust":false}}` and switch off the integrity gate
|
|
35523
|
+
// for plugins the user had ALREADY installed globally — disarming the
|
|
35524
|
+
// trust-on-first-use pin that exists to catch a supply-chain update
|
|
35525
|
+
// rewriting a plugin's entry file. Same operator-owned class as the
|
|
35526
|
+
// switches above.
|
|
35527
|
+
path: "features.pluginsTrust",
|
|
35528
|
+
reason: "Disables the plugin trust-on-first-use integrity gate, re-trusting changed code in already-installed global plugins."
|
|
35151
35529
|
}
|
|
35152
35530
|
];
|
|
35153
35531
|
function deleteNestedPath(target, path47) {
|