@wrongstack/core 0.306.4 → 0.307.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/chronicle/index.js +957 -953
- package/dist/chronicle/metrics-ingest.d.ts +55 -0
- package/dist/chronicle/metrics-schema.d.ts +86 -0
- package/dist/chronicle/metrics-store.d.ts +5 -120
- package/dist/chronicle/project-server.js +1083 -1084
- package/dist/chronicle/sqlite-journal-quota.d.ts +39 -0
- package/dist/chronicle/sqlite-journal-schema.d.ts +25 -0
- package/dist/chronicle/sqlite-journal.d.ts +12 -177
- package/dist/coordination/agents/index.js +1803 -1701
- package/dist/coordination/agents/project-agent-capture.d.ts +48 -0
- package/dist/coordination/agents/project-agent-identity.d.ts +3 -129
- package/dist/coordination/agents/project-agent-roster.d.ts +17 -0
- package/dist/coordination/agents/types.d.ts +6 -9
- package/dist/coordination/index.js +6051 -5880
- package/dist/coordination/mail-tools.d.ts +1 -1
- package/dist/coordination/mailbox-http-actor-query.d.ts +16 -0
- package/dist/coordination/mailbox-http-router.d.ts +0 -48
- package/dist/coordination/mailbox-http-sse.d.ts +12 -0
- package/dist/coordination/mailbox-message-types.d.ts +71 -0
- package/dist/coordination/mailbox-predicates.d.ts +13 -0
- package/dist/coordination/mailbox-project-server.js +1 -1
- package/dist/coordination/mailbox-session-sync.d.ts +8 -0
- package/dist/coordination/mailbox-types.d.ts +6 -793
- package/dist/core/agent-loop-context.d.ts +28 -0
- package/dist/core/agent-loop-detector.d.ts +25 -0
- package/dist/core/fallback-doctor.d.ts +72 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1859 -1335
- package/dist/defaults/index.js +3647 -3559
- package/dist/execution/autonomy-brain-llm.d.ts +55 -0
- package/dist/execution/autonomy-brain.d.ts +3 -156
- package/dist/execution/compaction-budget.d.ts +38 -0
- package/dist/execution/compaction-core.d.ts +3 -170
- package/dist/execution/compaction-elision.d.ts +58 -0
- package/dist/execution/council-orchestrator-helpers.d.ts +39 -0
- package/dist/execution/council-orchestrator.d.ts +3 -42
- package/dist/execution/council-response-parser.d.ts +56 -0
- package/dist/execution/index.js +2475 -2449
- package/dist/execution/prompt-enhancer.js +11 -3
- package/dist/execution/tool-executor-guard.d.ts +18 -0
- package/dist/execution/tool-executor-runner.d.ts +12 -0
- package/dist/execution/tool-executor.d.ts +0 -75
- package/dist/goal/index.js +19 -6
- package/dist/hq/index.js +8 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8021 -7405
- package/dist/infrastructure/index.js +3 -1
- package/dist/kernel/events/agent-events.d.ts +2 -0
- package/dist/kernel/events/provider-events.d.ts +13 -0
- package/dist/plugin/index.js +2590 -2561
- package/dist/plugins/auto-review-config.d.ts +53 -0
- package/dist/plugins/auto-review-git.d.ts +19 -0
- package/dist/plugins/auto-review-plugin.d.ts +2 -140
- package/dist/plugins/review-finding-verification.d.ts +7 -0
- package/dist/prompts/index.d.ts +1 -0
- package/dist/prompts/index.js +601 -0
- package/dist/prompts/prompt-journal.d.ts +103 -0
- package/dist/security/index.js +627 -652
- package/dist/security/permission-explain.d.ts +19 -0
- package/dist/security/permission-policy.d.ts +0 -101
- package/dist/security/yolo-risk.d.ts +5 -4
- package/dist/session-catalog/index.js +220 -212
- package/dist/session-catalog/project-server.js +227 -219
- package/dist/session-catalog/store-rebuild.d.ts +9 -0
- package/dist/session-catalog/store-schema.d.ts +48 -0
- package/dist/session-catalog/store.d.ts +0 -17
- package/dist/storage/director-state.d.ts +6 -0
- package/dist/storage/file-session-writer.d.ts +2 -58
- package/dist/storage/index.d.ts +2 -1
- package/dist/storage/index.js +2057 -1925
- package/dist/storage/orphan-lock-cleaner.d.ts +15 -0
- package/dist/storage/queue-store.d.ts +6 -0
- package/dist/storage/session-recovery.d.ts +9 -0
- package/dist/storage/session-store/rename-session.d.ts +16 -0
- package/dist/storage/session-store/resume-session.d.ts +26 -0
- package/dist/storage/session-store/session-store-clear.d.ts +11 -0
- package/dist/storage/session-store/session-store-index.d.ts +13 -0
- package/dist/storage/session-store.d.ts +1 -129
- package/dist/storage/session-summary-tracker.d.ts +39 -0
- package/dist/storage/session-write-buffer.d.ts +43 -0
- package/dist/storage/session-writer/session-writer-flush.d.ts +4 -0
- package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +28 -0
- package/dist/tools/fallback-agent-model-assign-tool.d.ts +18 -0
- package/dist/tools/fallback-leader-model-set-tool.d.ts +18 -0
- package/dist/tools/fallback-manage-tools.d.ts +8 -45
- package/dist/tools/fallback-profile-manage-tool.d.ts +16 -0
- package/dist/tools/fallback-provider-key-set-tool.d.ts +17 -0
- package/dist/tools/fallback-provider-manage-tool.d.ts +26 -0
- package/dist/tools/index.js +5124 -5071
- package/dist/types/config/root.d.ts +7 -2
- package/dist/types/default-config.d.ts +1 -1
- package/dist/types/index.d.ts +54 -55
- package/dist/types/index.js +953 -932
- package/dist/types/runtime-capability-manifest.d.ts +5 -5
- package/dist/types/session-markers.d.ts +12 -0
- package/dist/types/session-markers.js +19 -0
- package/dist/worktree/worktree-manager.d.ts +1 -1
- package/instructions/agents/architect.md +2 -0
- package/instructions/agents/code-reviewer.md +1 -0
- package/instructions/agents/debugger.md +2 -0
- package/instructions/agents/executor.md +2 -0
- package/instructions/agents/explore.md +3 -1
- package/instructions/agents/refactor.md +2 -0
- package/instructions/agents/reviewer.md +1 -0
- package/instructions/coordination/subagent-baseline.md +68 -7
- package/instructions/llm/prompt-enhancer.md +2 -1
- package/instructions/sections/tool/common-patterns.md +30 -0
- package/instructions/sections/tool/delegation-compact.md +3 -1
- package/instructions/sections/tool/delegation-full.md +5 -1
- package/instructions/system-lite.md +32 -4
- package/instructions/system-pro.md +63 -9
- package/instructions/system.md +56 -11
- package/package.json +9 -5
package/dist/security/index.js
CHANGED
|
@@ -1786,175 +1786,23 @@ function subjectForToolInput(toolName, input, subjectKey) {
|
|
|
1786
1786
|
return void 0;
|
|
1787
1787
|
}
|
|
1788
1788
|
|
|
1789
|
-
// src/security/permission-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
title: "WrongStack Trust Policy",
|
|
1801
|
-
type: "object",
|
|
1802
|
-
maxProperties: TRUST_POLICY_LIMITS.maxTools,
|
|
1803
|
-
propertyNames: {
|
|
1804
|
-
minLength: 1,
|
|
1805
|
-
maxLength: TRUST_POLICY_LIMITS.maxToolNameChars,
|
|
1806
|
-
not: { enum: ["__proto__", "prototype", "constructor"] }
|
|
1807
|
-
},
|
|
1808
|
-
additionalProperties: {
|
|
1809
|
-
type: "object",
|
|
1810
|
-
additionalProperties: false,
|
|
1811
|
-
properties: {
|
|
1812
|
-
allow: {
|
|
1813
|
-
type: "array",
|
|
1814
|
-
maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
|
|
1815
|
-
items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
|
|
1816
|
-
},
|
|
1817
|
-
deny: {
|
|
1818
|
-
type: "array",
|
|
1819
|
-
maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
|
|
1820
|
-
items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
|
|
1821
|
-
},
|
|
1822
|
-
auto: { type: "boolean" },
|
|
1823
|
-
trustWorkdir: { type: "boolean" },
|
|
1824
|
-
denyPrivate: { type: "boolean" }
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
});
|
|
1828
|
-
var RULE_FIELDS2 = /* @__PURE__ */ new Set(["allow", "deny", "auto", "trustWorkdir", "denyPrivate"]);
|
|
1829
|
-
var UNSAFE_PROPERTY_NAMES2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1830
|
-
function isRecord2(value) {
|
|
1831
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1832
|
-
}
|
|
1833
|
-
function error2(diagnostics, code, path9, message) {
|
|
1834
|
-
diagnostics.push({ severity: "error", code, path: path9, message });
|
|
1835
|
-
}
|
|
1836
|
-
function validatePatterns(value, path9, diagnostics) {
|
|
1837
|
-
if (!Array.isArray(value)) {
|
|
1838
|
-
error2(diagnostics, "invalid_pattern_list", path9, "must be an array of strings");
|
|
1839
|
-
return void 0;
|
|
1840
|
-
}
|
|
1841
|
-
if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
|
|
1842
|
-
error2(
|
|
1843
|
-
diagnostics,
|
|
1844
|
-
"too_many_patterns",
|
|
1845
|
-
path9,
|
|
1846
|
-
`must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
|
|
1847
|
-
);
|
|
1848
|
-
return void 0;
|
|
1849
|
-
}
|
|
1850
|
-
const patterns = [];
|
|
1851
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1852
|
-
for (const [index, pattern] of value.entries()) {
|
|
1853
|
-
const itemPath = `${path9}[${index}]`;
|
|
1854
|
-
if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
|
|
1855
|
-
error2(
|
|
1856
|
-
diagnostics,
|
|
1857
|
-
"invalid_pattern",
|
|
1858
|
-
itemPath,
|
|
1859
|
-
`must be a non-empty string up to ${TRUST_POLICY_LIMITS.maxPatternChars} characters`
|
|
1860
|
-
);
|
|
1861
|
-
continue;
|
|
1862
|
-
}
|
|
1863
|
-
if (seen.has(pattern)) {
|
|
1864
|
-
diagnostics.push({
|
|
1865
|
-
severity: "warning",
|
|
1866
|
-
code: "duplicate_pattern",
|
|
1867
|
-
path: itemPath,
|
|
1868
|
-
message: "duplicates an earlier pattern and was normalized away"
|
|
1869
|
-
});
|
|
1870
|
-
continue;
|
|
1871
|
-
}
|
|
1872
|
-
seen.add(pattern);
|
|
1873
|
-
patterns.push(pattern);
|
|
1874
|
-
}
|
|
1875
|
-
return patterns;
|
|
1876
|
-
}
|
|
1877
|
-
function validateTrustPolicy(value) {
|
|
1878
|
-
const diagnostics = [];
|
|
1879
|
-
if (!isRecord2(value)) {
|
|
1880
|
-
return {
|
|
1881
|
-
ok: false,
|
|
1882
|
-
diagnostics: [
|
|
1883
|
-
{
|
|
1884
|
-
severity: "error",
|
|
1885
|
-
code: "invalid_root",
|
|
1886
|
-
path: "$",
|
|
1887
|
-
message: "trust policy must be an object"
|
|
1888
|
-
}
|
|
1889
|
-
]
|
|
1890
|
-
};
|
|
1891
|
-
}
|
|
1892
|
-
const entries = Object.entries(value);
|
|
1893
|
-
if (entries.length > TRUST_POLICY_LIMITS.maxTools) {
|
|
1894
|
-
error2(
|
|
1895
|
-
diagnostics,
|
|
1896
|
-
"too_many_tools",
|
|
1897
|
-
"$",
|
|
1898
|
-
`must contain at most ${TRUST_POLICY_LIMITS.maxTools} tool rules`
|
|
1899
|
-
);
|
|
1900
|
-
}
|
|
1901
|
-
const policy = {};
|
|
1902
|
-
for (const [toolName, rawRule] of entries.slice(0, TRUST_POLICY_LIMITS.maxTools)) {
|
|
1903
|
-
const toolPath = `$[${JSON.stringify(toolName)}]`;
|
|
1904
|
-
if (toolName.length === 0 || toolName.length > TRUST_POLICY_LIMITS.maxToolNameChars) {
|
|
1905
|
-
error2(
|
|
1906
|
-
diagnostics,
|
|
1907
|
-
"invalid_tool_name",
|
|
1908
|
-
toolPath,
|
|
1909
|
-
`tool name must be 1-${TRUST_POLICY_LIMITS.maxToolNameChars} characters`
|
|
1910
|
-
);
|
|
1911
|
-
continue;
|
|
1912
|
-
}
|
|
1913
|
-
if (UNSAFE_PROPERTY_NAMES2.has(toolName)) {
|
|
1914
|
-
error2(
|
|
1915
|
-
diagnostics,
|
|
1916
|
-
"unsafe_tool_name",
|
|
1917
|
-
toolPath,
|
|
1918
|
-
"reserved object property names are not valid tool rules"
|
|
1919
|
-
);
|
|
1920
|
-
continue;
|
|
1921
|
-
}
|
|
1922
|
-
if (!isRecord2(rawRule)) {
|
|
1923
|
-
error2(diagnostics, "invalid_rule", toolPath, "tool rule must be an object");
|
|
1924
|
-
continue;
|
|
1925
|
-
}
|
|
1926
|
-
for (const field of Object.keys(rawRule)) {
|
|
1927
|
-
if (!RULE_FIELDS2.has(field)) {
|
|
1928
|
-
error2(
|
|
1929
|
-
diagnostics,
|
|
1930
|
-
"unknown_field",
|
|
1931
|
-
`${toolPath}.${field}`,
|
|
1932
|
-
`unknown policy field "${field}"`
|
|
1933
|
-
);
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
const rule = {};
|
|
1937
|
-
for (const field of ["allow", "deny"]) {
|
|
1938
|
-
if (rawRule[field] === void 0) continue;
|
|
1939
|
-
const patterns = validatePatterns(rawRule[field], `${toolPath}.${field}`, diagnostics);
|
|
1940
|
-
if (patterns) rule[field] = patterns;
|
|
1941
|
-
}
|
|
1942
|
-
for (const field of ["auto", "trustWorkdir", "denyPrivate"]) {
|
|
1943
|
-
const setting = rawRule[field];
|
|
1944
|
-
if (setting === void 0) continue;
|
|
1945
|
-
if (typeof setting !== "boolean") {
|
|
1946
|
-
error2(diagnostics, "invalid_boolean", `${toolPath}.${field}`, "must be a boolean");
|
|
1947
|
-
} else {
|
|
1948
|
-
rule[field] = setting;
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
policy[toolName] = rule;
|
|
1952
|
-
}
|
|
1953
|
-
return diagnostics.some((diagnostic) => diagnostic.severity === "error") ? { ok: false, diagnostics } : { ok: true, policy, diagnostics };
|
|
1789
|
+
// src/security/permission-helpers.ts
|
|
1790
|
+
import { realpathSync } from "node:fs";
|
|
1791
|
+
import * as path5 from "node:path";
|
|
1792
|
+
|
|
1793
|
+
// src/utils/wstack-paths.ts
|
|
1794
|
+
import * as os from "node:os";
|
|
1795
|
+
import * as path3 from "node:path";
|
|
1796
|
+
function wstackGlobalRoot() {
|
|
1797
|
+
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
1798
|
+
if (fromEnv && fromEnv.trim().length > 0) return path3.resolve(fromEnv);
|
|
1799
|
+
return path3.join(os.homedir(), ".wrongstack");
|
|
1954
1800
|
}
|
|
1955
1801
|
|
|
1956
1802
|
// src/security/yolo-risk.ts
|
|
1957
|
-
import * as
|
|
1803
|
+
import * as os2 from "node:os";
|
|
1804
|
+
import * as path4 from "node:path";
|
|
1805
|
+
var PROTECTED_STATE_BASENAMES = /^(?:config\.json|config\.local\.json|trust\.json|auth\.json|\.key)$/i;
|
|
1958
1806
|
var CATASTROPHIC_PATTERNS = [
|
|
1959
1807
|
/\b(?:mkfs(?:\.[a-z0-9]+)?|mke2fs|newfs)\b/i,
|
|
1960
1808
|
// make a filesystem — wipes a partition
|
|
@@ -2014,9 +1862,9 @@ function getInputString(input, key) {
|
|
|
2014
1862
|
function pathLooksInsideProject(rawPath, projectRoot) {
|
|
2015
1863
|
if (!projectRoot) return false;
|
|
2016
1864
|
if (rawPath === "~" || rawPath.startsWith("~/") || rawPath.startsWith("~\\")) return false;
|
|
2017
|
-
const resolved =
|
|
2018
|
-
const relative4 =
|
|
2019
|
-
return !!relative4 && !relative4.startsWith("..") && !
|
|
1865
|
+
const resolved = path4.resolve(projectRoot, rawPath);
|
|
1866
|
+
const relative4 = path4.relative(projectRoot, resolved);
|
|
1867
|
+
return !!relative4 && !relative4.startsWith("..") && !path4.isAbsolute(relative4);
|
|
2020
1868
|
}
|
|
2021
1869
|
function tokenizeShell(command) {
|
|
2022
1870
|
return command.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
@@ -2190,6 +2038,48 @@ function hasCatastrophicDelete(command) {
|
|
|
2190
2038
|
}
|
|
2191
2039
|
return false;
|
|
2192
2040
|
}
|
|
2041
|
+
function hasWriteToAgentStateRoot(command) {
|
|
2042
|
+
const tokens = tokenizeShell(command);
|
|
2043
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
2044
|
+
const t = tokens[i];
|
|
2045
|
+
if (t === ">" || t === ">>") {
|
|
2046
|
+
const target = tokens[i + 1];
|
|
2047
|
+
if (target && looksLikeAgentStateTarget(target)) return true;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2051
|
+
const t = tokens[i]?.toLowerCase();
|
|
2052
|
+
if (t === "tee") {
|
|
2053
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
2054
|
+
const arg = tokens[j];
|
|
2055
|
+
if (!arg || arg.startsWith("-")) continue;
|
|
2056
|
+
if (SHELL_OPERATORS.has(arg)) break;
|
|
2057
|
+
if (looksLikeAgentStateTarget(arg)) return true;
|
|
2058
|
+
break;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2063
|
+
const t = tokens[i]?.toLowerCase();
|
|
2064
|
+
if (t === "cp" || t === "copy" || t === "mv" || t === "move") {
|
|
2065
|
+
const args = commandSegment(tokens, i + 1);
|
|
2066
|
+
const dst = args.filter((a) => !a.startsWith("-") && !SHELL_OPERATORS.has(a)).pop();
|
|
2067
|
+
if (dst && looksLikeAgentStateTarget(dst)) return true;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
function looksLikeAgentStateTarget(rawPath) {
|
|
2073
|
+
const expanded = rawPath.replace(/^~([\\/])/, (_, sep2) => `${os2.homedir()}${sep2}`);
|
|
2074
|
+
const resolved = path4.resolve(expanded);
|
|
2075
|
+
const rootStr = wstackGlobalRoot();
|
|
2076
|
+
const resolvedNorm = resolved.replace(/\\/g, "/").toLowerCase();
|
|
2077
|
+
const rootNorm = path4.resolve(rootStr).replace(/\\/g, "/").toLowerCase();
|
|
2078
|
+
if (!resolvedNorm.startsWith(rootNorm) && !resolvedNorm.includes(".wrongstack")) {
|
|
2079
|
+
return false;
|
|
2080
|
+
}
|
|
2081
|
+
return PROTECTED_STATE_BASENAMES.test(path4.basename(resolved));
|
|
2082
|
+
}
|
|
2193
2083
|
function isClearlyDestructiveBashCommand(command, projectRoot) {
|
|
2194
2084
|
const trimmed = command.trim();
|
|
2195
2085
|
if (!trimmed) return false;
|
|
@@ -2198,85 +2088,12 @@ function isClearlyDestructiveBashCommand(command, projectRoot) {
|
|
|
2198
2088
|
if (hasExternalPublish(trimmed)) return true;
|
|
2199
2089
|
if (hasFindExec(trimmed)) return true;
|
|
2200
2090
|
if (hasCatastrophicDelete(trimmed)) return true;
|
|
2091
|
+
if (hasWriteToAgentStateRoot(trimmed)) return true;
|
|
2201
2092
|
if (CATASTROPHIC_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
|
|
2202
2093
|
if (HIGH_IMPACT_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
|
|
2203
2094
|
return false;
|
|
2204
2095
|
}
|
|
2205
2096
|
|
|
2206
|
-
// src/security/auto-approve-policy.ts
|
|
2207
|
-
var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
2208
|
-
allowedCapabilities;
|
|
2209
|
-
constructor(allowedCapabilities) {
|
|
2210
|
-
this.allowedCapabilities = allowedCapabilities ?? [
|
|
2211
|
-
ToolCapabilities.FS_READ,
|
|
2212
|
-
ToolCapabilities.NET_OUTBOUND
|
|
2213
|
-
];
|
|
2214
|
-
}
|
|
2215
|
-
static isMcpTool(name) {
|
|
2216
|
-
return name.startsWith("mcp__");
|
|
2217
|
-
}
|
|
2218
|
-
async evaluate(tool) {
|
|
2219
|
-
const caps = tool.capabilities ?? [];
|
|
2220
|
-
const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
|
|
2221
|
-
const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
|
|
2222
|
-
const mcpProxyAllowed = this.allowedCapabilities.includes(ToolCapabilities.MCP_PROXY);
|
|
2223
|
-
const dangerousNotAllowed = getDangerousCapabilities(tool).filter(
|
|
2224
|
-
(c) => !this.allowedCapabilities.includes(c)
|
|
2225
|
-
);
|
|
2226
|
-
const blocked = tool.permission === "deny" || isMcp && !mcpProxyAllowed || !hasAllowedCap || dangerousNotAllowed.length > 0;
|
|
2227
|
-
if (blocked) {
|
|
2228
|
-
const reason = isMcp && !mcpProxyAllowed ? `MCP tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow mcp.proxy explicitly` : tool.permission === "deny" ? "tool default deny" : dangerousNotAllowed.length > 0 ? `tool requires un-granted dangerous capability (needs: ${dangerousNotAllowed.join(", ")}, allowed: ${this.allowedCapabilities.join(", ")})` : `tool lacks allowed capability (has: ${caps.join(", ") || "none"}, allowed: ${this.allowedCapabilities.join(", ")})`;
|
|
2229
|
-
return {
|
|
2230
|
-
permission: "deny",
|
|
2231
|
-
source: "subagent_guard",
|
|
2232
|
-
reason
|
|
2233
|
-
};
|
|
2234
|
-
}
|
|
2235
|
-
return { permission: "auto", source: "yolo" };
|
|
2236
|
-
}
|
|
2237
|
-
async trust() {
|
|
2238
|
-
}
|
|
2239
|
-
async deny() {
|
|
2240
|
-
}
|
|
2241
|
-
denyOnce() {
|
|
2242
|
-
}
|
|
2243
|
-
allowOnce() {
|
|
2244
|
-
}
|
|
2245
|
-
async explain(tool) {
|
|
2246
|
-
const decision = await this.evaluate(tool);
|
|
2247
|
-
return {
|
|
2248
|
-
toolName: tool.name,
|
|
2249
|
-
subject: null,
|
|
2250
|
-
steps: [
|
|
2251
|
-
{
|
|
2252
|
-
rule: "subagent auto",
|
|
2253
|
-
matched: decision.permission === "auto",
|
|
2254
|
-
decision: decision.permission,
|
|
2255
|
-
source: decision.source,
|
|
2256
|
-
detail: decision.reason ?? `subagent policy: ${decision.permission}`
|
|
2257
|
-
}
|
|
2258
|
-
],
|
|
2259
|
-
winnerIndex: 0,
|
|
2260
|
-
decision
|
|
2261
|
-
};
|
|
2262
|
-
}
|
|
2263
|
-
async reload() {
|
|
2264
|
-
}
|
|
2265
|
-
};
|
|
2266
|
-
|
|
2267
|
-
// src/security/permission-helpers.ts
|
|
2268
|
-
import { realpathSync } from "node:fs";
|
|
2269
|
-
import * as path5 from "node:path";
|
|
2270
|
-
|
|
2271
|
-
// src/utils/wstack-paths.ts
|
|
2272
|
-
import * as os from "node:os";
|
|
2273
|
-
import * as path4 from "node:path";
|
|
2274
|
-
function wstackGlobalRoot() {
|
|
2275
|
-
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
2276
|
-
if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
|
|
2277
|
-
return path4.join(os.homedir(), ".wrongstack");
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
2097
|
// src/security/permission-helpers.ts
|
|
2281
2098
|
function matchesTrust(patterns, subject) {
|
|
2282
2099
|
return patterns.includes(subject) || matchAny(patterns, subject);
|
|
@@ -2412,8 +2229,558 @@ function shellCommandReadsSensitivePath(command) {
|
|
|
2412
2229
|
const rest = tokens.slice(i + 1);
|
|
2413
2230
|
if (rest.some((arg) => pathLooksSensitive(arg))) return true;
|
|
2414
2231
|
}
|
|
2415
|
-
return false;
|
|
2416
|
-
}
|
|
2232
|
+
return false;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// src/security/permission-explain.ts
|
|
2236
|
+
function explainPermissionTrace(state, tool, input, ctx) {
|
|
2237
|
+
const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
|
|
2238
|
+
const steps = [];
|
|
2239
|
+
let winnerIndex = -1;
|
|
2240
|
+
const add = (rule, matched, decision, source, detail) => {
|
|
2241
|
+
steps.push({ rule, matched, decision, source, detail });
|
|
2242
|
+
};
|
|
2243
|
+
if (state.policyInvalid) {
|
|
2244
|
+
add("policy invalid", true, "deny", "deny", "trust policy is invalid; all tools are denied");
|
|
2245
|
+
winnerIndex = 0;
|
|
2246
|
+
return {
|
|
2247
|
+
toolName: tool.name,
|
|
2248
|
+
subject,
|
|
2249
|
+
steps,
|
|
2250
|
+
winnerIndex,
|
|
2251
|
+
decision: { permission: "deny", source: "deny", reason: "trust policy is invalid" }
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
add("policy valid", false, "auto", "default", "trust policy loaded successfully");
|
|
2255
|
+
let namespaceEntry;
|
|
2256
|
+
for (const { pattern, value } of state.wildcardEntries) {
|
|
2257
|
+
if (matchGlob(pattern, tool.name)) {
|
|
2258
|
+
namespaceEntry = value;
|
|
2259
|
+
break;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
const entry = state.policy[tool.name] ?? namespaceEntry;
|
|
2263
|
+
const namespaceSource = namespaceEntry ? `wildcard entry matched (${Object.keys(state.policy).find((k) => k.includes("*") && matchGlob(k, tool.name))})` : "no namespace match";
|
|
2264
|
+
const cacheKey = `${tool.name}::${subject ?? tool.name}`;
|
|
2265
|
+
if (state.sessionDenied.has(cacheKey)) {
|
|
2266
|
+
add(
|
|
2267
|
+
"session soft deny",
|
|
2268
|
+
true,
|
|
2269
|
+
"deny",
|
|
2270
|
+
"deny",
|
|
2271
|
+
'user pressed "no" earlier in this session \u2014 blocked until reload'
|
|
2272
|
+
);
|
|
2273
|
+
winnerIndex = steps.length - 1;
|
|
2274
|
+
return {
|
|
2275
|
+
toolName: tool.name,
|
|
2276
|
+
subject,
|
|
2277
|
+
steps,
|
|
2278
|
+
winnerIndex,
|
|
2279
|
+
decision: {
|
|
2280
|
+
permission: "deny",
|
|
2281
|
+
source: "deny",
|
|
2282
|
+
reason: "session soft deny (user pressed no)"
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
add(
|
|
2287
|
+
"session soft deny",
|
|
2288
|
+
false,
|
|
2289
|
+
"deny",
|
|
2290
|
+
"deny",
|
|
2291
|
+
"no session-level soft deny for this tool+subject"
|
|
2292
|
+
);
|
|
2293
|
+
if (state.sessionAllowed.has(cacheKey)) {
|
|
2294
|
+
add(
|
|
2295
|
+
"session soft allow",
|
|
2296
|
+
true,
|
|
2297
|
+
"auto",
|
|
2298
|
+
"trust",
|
|
2299
|
+
'user pressed "yes" in current session \u2014 one-shot auto-approve (consumed)'
|
|
2300
|
+
);
|
|
2301
|
+
winnerIndex = steps.length - 1;
|
|
2302
|
+
return {
|
|
2303
|
+
toolName: tool.name,
|
|
2304
|
+
subject,
|
|
2305
|
+
steps,
|
|
2306
|
+
winnerIndex,
|
|
2307
|
+
decision: {
|
|
2308
|
+
permission: "auto",
|
|
2309
|
+
source: "trust",
|
|
2310
|
+
reason: "session one-shot allow (user pressed yes)"
|
|
2311
|
+
}
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
add(
|
|
2315
|
+
"session soft allow",
|
|
2316
|
+
false,
|
|
2317
|
+
"auto",
|
|
2318
|
+
"trust",
|
|
2319
|
+
"no session-level one-shot allow for this tool+subject"
|
|
2320
|
+
);
|
|
2321
|
+
if (entry?.deny && subject && matchesTrust(entry.deny, subject)) {
|
|
2322
|
+
add(
|
|
2323
|
+
"trust deny",
|
|
2324
|
+
true,
|
|
2325
|
+
"deny",
|
|
2326
|
+
"deny",
|
|
2327
|
+
`subject "${subject}" matched a deny pattern in trust file`
|
|
2328
|
+
);
|
|
2329
|
+
winnerIndex = steps.length - 1;
|
|
2330
|
+
return {
|
|
2331
|
+
toolName: tool.name,
|
|
2332
|
+
subject,
|
|
2333
|
+
steps,
|
|
2334
|
+
winnerIndex,
|
|
2335
|
+
decision: { permission: "deny", source: "deny", reason: "matched deny pattern" }
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
add(
|
|
2339
|
+
"trust deny",
|
|
2340
|
+
false,
|
|
2341
|
+
"deny",
|
|
2342
|
+
"deny",
|
|
2343
|
+
`no deny pattern matched (namespace: ${namespaceSource})`
|
|
2344
|
+
);
|
|
2345
|
+
if (tool.permission === "deny") {
|
|
2346
|
+
add(
|
|
2347
|
+
"tool default deny",
|
|
2348
|
+
true,
|
|
2349
|
+
"deny",
|
|
2350
|
+
"default",
|
|
2351
|
+
`tool "${tool.name}" has permission: deny by default`
|
|
2352
|
+
);
|
|
2353
|
+
winnerIndex = steps.length - 1;
|
|
2354
|
+
return {
|
|
2355
|
+
toolName: tool.name,
|
|
2356
|
+
subject,
|
|
2357
|
+
steps,
|
|
2358
|
+
winnerIndex,
|
|
2359
|
+
decision: { permission: "deny", source: "default", reason: "tool default deny" }
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
add(
|
|
2363
|
+
"tool default deny",
|
|
2364
|
+
false,
|
|
2365
|
+
"deny",
|
|
2366
|
+
"default",
|
|
2367
|
+
`tool "${tool.name}" has permission: "${tool.permission}"`
|
|
2368
|
+
);
|
|
2369
|
+
if (entry?.allow && subject && matchesTrust(entry.allow, subject)) {
|
|
2370
|
+
add(
|
|
2371
|
+
"trust allow",
|
|
2372
|
+
true,
|
|
2373
|
+
"auto",
|
|
2374
|
+
"trust",
|
|
2375
|
+
`subject "${subject}" matched an allow pattern in trust file`
|
|
2376
|
+
);
|
|
2377
|
+
winnerIndex = steps.length - 1;
|
|
2378
|
+
return {
|
|
2379
|
+
toolName: tool.name,
|
|
2380
|
+
subject,
|
|
2381
|
+
steps,
|
|
2382
|
+
winnerIndex,
|
|
2383
|
+
decision: { permission: "auto", source: "trust", reason: "matched allow pattern" }
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
add(
|
|
2387
|
+
"trust allow",
|
|
2388
|
+
false,
|
|
2389
|
+
"auto",
|
|
2390
|
+
"trust",
|
|
2391
|
+
`no allow pattern matched (namespace: ${namespaceSource})`
|
|
2392
|
+
);
|
|
2393
|
+
if (entry?.auto) {
|
|
2394
|
+
add("trust auto", true, "auto", "trust", `trust file has auto: true for "${tool.name}"`);
|
|
2395
|
+
winnerIndex = steps.length - 1;
|
|
2396
|
+
return {
|
|
2397
|
+
toolName: tool.name,
|
|
2398
|
+
subject,
|
|
2399
|
+
steps,
|
|
2400
|
+
winnerIndex,
|
|
2401
|
+
decision: { permission: "auto", source: "trust", reason: "trust auto" }
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
add("trust auto", false, "auto", "trust", `no auto flag for "${tool.name}" in trust file`);
|
|
2405
|
+
if (!state.yolo && state.isSensitiveReadCall(tool, input)) {
|
|
2406
|
+
const hasDelegate2 = state.promptDelegatePresent;
|
|
2407
|
+
add(
|
|
2408
|
+
"sensitive read",
|
|
2409
|
+
true,
|
|
2410
|
+
"confirm",
|
|
2411
|
+
"default",
|
|
2412
|
+
hasDelegate2 ? "sensitive file read detected \u2014 would prompt user for approval" : "sensitive file read detected \u2014 returns confirm (no prompt delegate)"
|
|
2413
|
+
);
|
|
2414
|
+
winnerIndex = steps.length - 1;
|
|
2415
|
+
return {
|
|
2416
|
+
toolName: tool.name,
|
|
2417
|
+
subject,
|
|
2418
|
+
steps,
|
|
2419
|
+
winnerIndex,
|
|
2420
|
+
decision: {
|
|
2421
|
+
permission: "confirm",
|
|
2422
|
+
source: "default",
|
|
2423
|
+
riskTier: "standard",
|
|
2424
|
+
reason: "sensitive file read needs explicit approval"
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
add(
|
|
2429
|
+
"sensitive read",
|
|
2430
|
+
false,
|
|
2431
|
+
"confirm",
|
|
2432
|
+
"default",
|
|
2433
|
+
"not a sensitive read call (or YOLO bypasses this check)"
|
|
2434
|
+
);
|
|
2435
|
+
if (state.yolo) {
|
|
2436
|
+
if (state.yoloBlockedAsDestructive(tool, input, ctx)) {
|
|
2437
|
+
add(
|
|
2438
|
+
"yolo destructive gate",
|
|
2439
|
+
true,
|
|
2440
|
+
"confirm",
|
|
2441
|
+
"yolo_destructive",
|
|
2442
|
+
"YOLO is active but this is a clearly destructive command \u2014 still asking"
|
|
2443
|
+
);
|
|
2444
|
+
winnerIndex = steps.length - 1;
|
|
2445
|
+
return {
|
|
2446
|
+
toolName: tool.name,
|
|
2447
|
+
subject,
|
|
2448
|
+
steps,
|
|
2449
|
+
winnerIndex,
|
|
2450
|
+
decision: {
|
|
2451
|
+
permission: "confirm",
|
|
2452
|
+
source: "yolo_destructive",
|
|
2453
|
+
riskTier: "destructive",
|
|
2454
|
+
reason: "destructive command needs explicit approval even in YOLO mode"
|
|
2455
|
+
}
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
add(
|
|
2459
|
+
"yolo",
|
|
2460
|
+
true,
|
|
2461
|
+
"auto",
|
|
2462
|
+
"yolo",
|
|
2463
|
+
"YOLO mode is active \u2014 auto-approving every non-denied call"
|
|
2464
|
+
);
|
|
2465
|
+
winnerIndex = steps.length - 1;
|
|
2466
|
+
return {
|
|
2467
|
+
toolName: tool.name,
|
|
2468
|
+
subject,
|
|
2469
|
+
steps,
|
|
2470
|
+
winnerIndex,
|
|
2471
|
+
decision: { permission: "auto", source: "yolo" }
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
add("yolo", false, "auto", "yolo", "YOLO mode is not active");
|
|
2475
|
+
if (tool.name === "write" && subject) {
|
|
2476
|
+
const isAgentState = isInsideAgentStateRoot(subject);
|
|
2477
|
+
const hasRead = ctx.hasRead(subject) && !isAgentState;
|
|
2478
|
+
add(
|
|
2479
|
+
"write smart bypass",
|
|
2480
|
+
hasRead,
|
|
2481
|
+
"auto",
|
|
2482
|
+
"context",
|
|
2483
|
+
isAgentState ? `file "${subject}" is WrongStack's own state \u2014 bypass never applies, write always confirms` : hasRead ? `file "${subject}" was already read in this session \u2014 auto-approving write` : `file "${subject}" was not read in this session \u2014 bypass does not apply`
|
|
2484
|
+
);
|
|
2485
|
+
if (hasRead) {
|
|
2486
|
+
winnerIndex = steps.length - 1;
|
|
2487
|
+
return {
|
|
2488
|
+
toolName: tool.name,
|
|
2489
|
+
subject,
|
|
2490
|
+
steps,
|
|
2491
|
+
winnerIndex,
|
|
2492
|
+
decision: {
|
|
2493
|
+
permission: "auto",
|
|
2494
|
+
source: "context",
|
|
2495
|
+
reason: "file already read in this session"
|
|
2496
|
+
}
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
} else {
|
|
2500
|
+
add(
|
|
2501
|
+
"write smart bypass",
|
|
2502
|
+
false,
|
|
2503
|
+
"auto",
|
|
2504
|
+
"context",
|
|
2505
|
+
'tool is not "write" or has no subject \u2014 bypass does not apply'
|
|
2506
|
+
);
|
|
2507
|
+
}
|
|
2508
|
+
const hasWriteCap = hasCapability(tool, ToolCapabilities.FS_WRITE);
|
|
2509
|
+
const hasShellCap = hasCapability(tool, [
|
|
2510
|
+
ToolCapabilities.SHELL_ARBITRARY,
|
|
2511
|
+
ToolCapabilities.SHELL_RESTRICTED,
|
|
2512
|
+
ToolCapabilities.SHELL_EXEC
|
|
2513
|
+
]);
|
|
2514
|
+
const hasInstallCap = hasCapability(tool, ToolCapabilities.PACKAGE_INSTALL);
|
|
2515
|
+
const hasConfigCap = hasCapability(tool, ToolCapabilities.CONFIG_MUTATE);
|
|
2516
|
+
const hasSubagentCap = hasCapability(tool, ToolCapabilities.SUBAGENT_SPAWN);
|
|
2517
|
+
const isMutating = tool.mutating || hasWriteCap || hasShellCap || hasInstallCap || hasConfigCap || hasSubagentCap;
|
|
2518
|
+
if (tool.permission === "auto" && !isMutating) {
|
|
2519
|
+
add(
|
|
2520
|
+
"safe default auto",
|
|
2521
|
+
true,
|
|
2522
|
+
"auto",
|
|
2523
|
+
"default",
|
|
2524
|
+
`tool "${tool.name}" has auto permission and is not mutating \u2014 safe to auto-approve`
|
|
2525
|
+
);
|
|
2526
|
+
winnerIndex = steps.length - 1;
|
|
2527
|
+
return {
|
|
2528
|
+
toolName: tool.name,
|
|
2529
|
+
subject,
|
|
2530
|
+
steps,
|
|
2531
|
+
winnerIndex,
|
|
2532
|
+
decision: { permission: "auto", source: "default" }
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
add(
|
|
2536
|
+
"mutating default confirm",
|
|
2537
|
+
true,
|
|
2538
|
+
"confirm",
|
|
2539
|
+
"default",
|
|
2540
|
+
isMutating ? `tool "${tool.name}" is mutating (default auto not enough) \u2014 needs confirmation` : `tool "${tool.name}" has "${tool.permission}" permission \u2014 needs confirmation`
|
|
2541
|
+
);
|
|
2542
|
+
winnerIndex = steps.length - 1;
|
|
2543
|
+
const hasDelegate = state.promptDelegatePresent;
|
|
2544
|
+
return {
|
|
2545
|
+
toolName: tool.name,
|
|
2546
|
+
subject,
|
|
2547
|
+
steps,
|
|
2548
|
+
winnerIndex,
|
|
2549
|
+
decision: {
|
|
2550
|
+
permission: "confirm",
|
|
2551
|
+
source: "default",
|
|
2552
|
+
...hasDelegate ? { reason: "would prompt user via delegate" } : {}
|
|
2553
|
+
}
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// src/security/permission-policy-schema.ts
|
|
2558
|
+
var TRUST_POLICY_SCHEMA_VERSION = 1;
|
|
2559
|
+
var TRUST_POLICY_LIMITS = Object.freeze({
|
|
2560
|
+
maxTools: 1e3,
|
|
2561
|
+
maxPatternsPerRule: 1e3,
|
|
2562
|
+
maxToolNameChars: 256,
|
|
2563
|
+
maxPatternChars: 4096
|
|
2564
|
+
});
|
|
2565
|
+
var TRUST_POLICY_JSON_SCHEMA = Object.freeze({
|
|
2566
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
2567
|
+
$id: "https://wrongstack.dev/schemas/trust-policy-v1.json",
|
|
2568
|
+
title: "WrongStack Trust Policy",
|
|
2569
|
+
type: "object",
|
|
2570
|
+
maxProperties: TRUST_POLICY_LIMITS.maxTools,
|
|
2571
|
+
propertyNames: {
|
|
2572
|
+
minLength: 1,
|
|
2573
|
+
maxLength: TRUST_POLICY_LIMITS.maxToolNameChars,
|
|
2574
|
+
not: { enum: ["__proto__", "prototype", "constructor"] }
|
|
2575
|
+
},
|
|
2576
|
+
additionalProperties: {
|
|
2577
|
+
type: "object",
|
|
2578
|
+
additionalProperties: false,
|
|
2579
|
+
properties: {
|
|
2580
|
+
allow: {
|
|
2581
|
+
type: "array",
|
|
2582
|
+
maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
|
|
2583
|
+
items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
|
|
2584
|
+
},
|
|
2585
|
+
deny: {
|
|
2586
|
+
type: "array",
|
|
2587
|
+
maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
|
|
2588
|
+
items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
|
|
2589
|
+
},
|
|
2590
|
+
auto: { type: "boolean" },
|
|
2591
|
+
trustWorkdir: { type: "boolean" },
|
|
2592
|
+
denyPrivate: { type: "boolean" }
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
});
|
|
2596
|
+
var RULE_FIELDS2 = /* @__PURE__ */ new Set(["allow", "deny", "auto", "trustWorkdir", "denyPrivate"]);
|
|
2597
|
+
var UNSAFE_PROPERTY_NAMES2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2598
|
+
function isRecord2(value) {
|
|
2599
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2600
|
+
}
|
|
2601
|
+
function error2(diagnostics, code, path9, message) {
|
|
2602
|
+
diagnostics.push({ severity: "error", code, path: path9, message });
|
|
2603
|
+
}
|
|
2604
|
+
function validatePatterns(value, path9, diagnostics) {
|
|
2605
|
+
if (!Array.isArray(value)) {
|
|
2606
|
+
error2(diagnostics, "invalid_pattern_list", path9, "must be an array of strings");
|
|
2607
|
+
return void 0;
|
|
2608
|
+
}
|
|
2609
|
+
if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
|
|
2610
|
+
error2(
|
|
2611
|
+
diagnostics,
|
|
2612
|
+
"too_many_patterns",
|
|
2613
|
+
path9,
|
|
2614
|
+
`must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
|
|
2615
|
+
);
|
|
2616
|
+
return void 0;
|
|
2617
|
+
}
|
|
2618
|
+
const patterns = [];
|
|
2619
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2620
|
+
for (const [index, pattern] of value.entries()) {
|
|
2621
|
+
const itemPath = `${path9}[${index}]`;
|
|
2622
|
+
if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
|
|
2623
|
+
error2(
|
|
2624
|
+
diagnostics,
|
|
2625
|
+
"invalid_pattern",
|
|
2626
|
+
itemPath,
|
|
2627
|
+
`must be a non-empty string up to ${TRUST_POLICY_LIMITS.maxPatternChars} characters`
|
|
2628
|
+
);
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2631
|
+
if (seen.has(pattern)) {
|
|
2632
|
+
diagnostics.push({
|
|
2633
|
+
severity: "warning",
|
|
2634
|
+
code: "duplicate_pattern",
|
|
2635
|
+
path: itemPath,
|
|
2636
|
+
message: "duplicates an earlier pattern and was normalized away"
|
|
2637
|
+
});
|
|
2638
|
+
continue;
|
|
2639
|
+
}
|
|
2640
|
+
seen.add(pattern);
|
|
2641
|
+
patterns.push(pattern);
|
|
2642
|
+
}
|
|
2643
|
+
return patterns;
|
|
2644
|
+
}
|
|
2645
|
+
function validateTrustPolicy(value) {
|
|
2646
|
+
const diagnostics = [];
|
|
2647
|
+
if (!isRecord2(value)) {
|
|
2648
|
+
return {
|
|
2649
|
+
ok: false,
|
|
2650
|
+
diagnostics: [
|
|
2651
|
+
{
|
|
2652
|
+
severity: "error",
|
|
2653
|
+
code: "invalid_root",
|
|
2654
|
+
path: "$",
|
|
2655
|
+
message: "trust policy must be an object"
|
|
2656
|
+
}
|
|
2657
|
+
]
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
const entries = Object.entries(value);
|
|
2661
|
+
if (entries.length > TRUST_POLICY_LIMITS.maxTools) {
|
|
2662
|
+
error2(
|
|
2663
|
+
diagnostics,
|
|
2664
|
+
"too_many_tools",
|
|
2665
|
+
"$",
|
|
2666
|
+
`must contain at most ${TRUST_POLICY_LIMITS.maxTools} tool rules`
|
|
2667
|
+
);
|
|
2668
|
+
}
|
|
2669
|
+
const policy = {};
|
|
2670
|
+
for (const [toolName, rawRule] of entries.slice(0, TRUST_POLICY_LIMITS.maxTools)) {
|
|
2671
|
+
const toolPath = `$[${JSON.stringify(toolName)}]`;
|
|
2672
|
+
if (toolName.length === 0 || toolName.length > TRUST_POLICY_LIMITS.maxToolNameChars) {
|
|
2673
|
+
error2(
|
|
2674
|
+
diagnostics,
|
|
2675
|
+
"invalid_tool_name",
|
|
2676
|
+
toolPath,
|
|
2677
|
+
`tool name must be 1-${TRUST_POLICY_LIMITS.maxToolNameChars} characters`
|
|
2678
|
+
);
|
|
2679
|
+
continue;
|
|
2680
|
+
}
|
|
2681
|
+
if (UNSAFE_PROPERTY_NAMES2.has(toolName)) {
|
|
2682
|
+
error2(
|
|
2683
|
+
diagnostics,
|
|
2684
|
+
"unsafe_tool_name",
|
|
2685
|
+
toolPath,
|
|
2686
|
+
"reserved object property names are not valid tool rules"
|
|
2687
|
+
);
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
if (!isRecord2(rawRule)) {
|
|
2691
|
+
error2(diagnostics, "invalid_rule", toolPath, "tool rule must be an object");
|
|
2692
|
+
continue;
|
|
2693
|
+
}
|
|
2694
|
+
for (const field of Object.keys(rawRule)) {
|
|
2695
|
+
if (!RULE_FIELDS2.has(field)) {
|
|
2696
|
+
error2(
|
|
2697
|
+
diagnostics,
|
|
2698
|
+
"unknown_field",
|
|
2699
|
+
`${toolPath}.${field}`,
|
|
2700
|
+
`unknown policy field "${field}"`
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
const rule = {};
|
|
2705
|
+
for (const field of ["allow", "deny"]) {
|
|
2706
|
+
if (rawRule[field] === void 0) continue;
|
|
2707
|
+
const patterns = validatePatterns(rawRule[field], `${toolPath}.${field}`, diagnostics);
|
|
2708
|
+
if (patterns) rule[field] = patterns;
|
|
2709
|
+
}
|
|
2710
|
+
for (const field of ["auto", "trustWorkdir", "denyPrivate"]) {
|
|
2711
|
+
const setting = rawRule[field];
|
|
2712
|
+
if (setting === void 0) continue;
|
|
2713
|
+
if (typeof setting !== "boolean") {
|
|
2714
|
+
error2(diagnostics, "invalid_boolean", `${toolPath}.${field}`, "must be a boolean");
|
|
2715
|
+
} else {
|
|
2716
|
+
rule[field] = setting;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
policy[toolName] = rule;
|
|
2720
|
+
}
|
|
2721
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error") ? { ok: false, diagnostics } : { ok: true, policy, diagnostics };
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
// src/security/auto-approve-policy.ts
|
|
2725
|
+
var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
2726
|
+
allowedCapabilities;
|
|
2727
|
+
constructor(allowedCapabilities) {
|
|
2728
|
+
this.allowedCapabilities = allowedCapabilities ?? [
|
|
2729
|
+
ToolCapabilities.FS_READ,
|
|
2730
|
+
ToolCapabilities.NET_OUTBOUND
|
|
2731
|
+
];
|
|
2732
|
+
}
|
|
2733
|
+
static isMcpTool(name) {
|
|
2734
|
+
return name.startsWith("mcp__");
|
|
2735
|
+
}
|
|
2736
|
+
async evaluate(tool) {
|
|
2737
|
+
const caps = tool.capabilities ?? [];
|
|
2738
|
+
const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
|
|
2739
|
+
const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
|
|
2740
|
+
const mcpProxyAllowed = this.allowedCapabilities.includes(ToolCapabilities.MCP_PROXY);
|
|
2741
|
+
const dangerousNotAllowed = getDangerousCapabilities(tool).filter(
|
|
2742
|
+
(c) => !this.allowedCapabilities.includes(c)
|
|
2743
|
+
);
|
|
2744
|
+
const blocked = tool.permission === "deny" || isMcp && !mcpProxyAllowed || !hasAllowedCap || dangerousNotAllowed.length > 0;
|
|
2745
|
+
if (blocked) {
|
|
2746
|
+
const reason = isMcp && !mcpProxyAllowed ? `MCP tool ${tool.name} is not auto-approved for subagents \u2014 ask the leader to allow mcp.proxy explicitly` : tool.permission === "deny" ? "tool default deny" : dangerousNotAllowed.length > 0 ? `tool requires un-granted dangerous capability (needs: ${dangerousNotAllowed.join(", ")}, allowed: ${this.allowedCapabilities.join(", ")})` : `tool lacks allowed capability (has: ${caps.join(", ") || "none"}, allowed: ${this.allowedCapabilities.join(", ")})`;
|
|
2747
|
+
return {
|
|
2748
|
+
permission: "deny",
|
|
2749
|
+
source: "subagent_guard",
|
|
2750
|
+
reason
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
return { permission: "auto", source: "yolo" };
|
|
2754
|
+
}
|
|
2755
|
+
async trust() {
|
|
2756
|
+
}
|
|
2757
|
+
async deny() {
|
|
2758
|
+
}
|
|
2759
|
+
denyOnce() {
|
|
2760
|
+
}
|
|
2761
|
+
allowOnce() {
|
|
2762
|
+
}
|
|
2763
|
+
async explain(tool) {
|
|
2764
|
+
const decision = await this.evaluate(tool);
|
|
2765
|
+
return {
|
|
2766
|
+
toolName: tool.name,
|
|
2767
|
+
subject: null,
|
|
2768
|
+
steps: [
|
|
2769
|
+
{
|
|
2770
|
+
rule: "subagent auto",
|
|
2771
|
+
matched: decision.permission === "auto",
|
|
2772
|
+
decision: decision.permission,
|
|
2773
|
+
source: decision.source,
|
|
2774
|
+
detail: decision.reason ?? `subagent policy: ${decision.permission}`
|
|
2775
|
+
}
|
|
2776
|
+
],
|
|
2777
|
+
winnerIndex: 0,
|
|
2778
|
+
decision
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
async reload() {
|
|
2782
|
+
}
|
|
2783
|
+
};
|
|
2417
2784
|
|
|
2418
2785
|
// src/security/permission-policy.ts
|
|
2419
2786
|
function fsWriteTargetPaths(input) {
|
|
@@ -2449,70 +2816,13 @@ var DefaultPermissionPolicy = class {
|
|
|
2449
2816
|
loaded = false;
|
|
2450
2817
|
trustFile;
|
|
2451
2818
|
yolo;
|
|
2452
|
-
/**
|
|
2453
|
-
* Session-scoped "soft deny" map. When the user presses 'n' (block once),
|
|
2454
|
-
* the tool+pattern is added here. If the LLM retries in the same session,
|
|
2455
|
-
* we return deny directly without asking again.
|
|
2456
|
-
*
|
|
2457
|
-
* Cleared on reload() since reload = fresh trust file snapshot.
|
|
2458
|
-
*/
|
|
2459
2819
|
sessionDenied = /* @__PURE__ */ new Map();
|
|
2460
|
-
/**
|
|
2461
|
-
* Session-scoped one-shot "soft trust" map. When the user presses 'y', the
|
|
2462
|
-
* tool+pattern is added here so the immediate confirm re-run can proceed.
|
|
2463
|
-
* The entry is consumed on first use; future calls must ask again unless the
|
|
2464
|
-
* user chose persistent trust.
|
|
2465
|
-
*
|
|
2466
|
-
* Cleared on reload().
|
|
2467
|
-
*/
|
|
2468
2820
|
sessionAllowed = /* @__PURE__ */ new Map();
|
|
2469
|
-
/**
|
|
2470
|
-
* Interactive prompt delegate. When set, `evaluate()` calls it to get a
|
|
2471
|
-
* user decision synchronously (CLI REPL path). When cleared (TUI / WebUI),
|
|
2472
|
-
* `evaluate()` returns `confirm` so the caller can emit
|
|
2473
|
-
* `tool.confirm_needed` for the UI layer to handle.
|
|
2474
|
-
*
|
|
2475
|
-
* Mutable so the host can switch from CLI-prompt to event-driven
|
|
2476
|
-
* confirmation at runtime (e.g. when `--goal` forces TUI mode after
|
|
2477
|
-
* the agent was already constructed).
|
|
2478
|
-
*/
|
|
2479
2821
|
promptDelegate;
|
|
2480
|
-
/** Pre-compiled wildcard patterns — rebuilt on reload for O(1) lookup. */
|
|
2481
2822
|
wildcardEntries = [];
|
|
2482
|
-
/**
|
|
2483
|
-
* Evaluate-result cache. Keyed by `tool.name::subject::permissionFingerprint`
|
|
2484
|
-
* so repeated calls with the same tool+input skip namespace matching,
|
|
2485
|
-
* subject computation, and pattern matching (matchAny). The fingerprint
|
|
2486
|
-
* suffix covers the `Tool` fields `evaluate()` reads (`permission`,
|
|
2487
|
-
* `riskTier`, `mutating`, `capabilities`), so a tool redefined via
|
|
2488
|
-
* `ToolRegistry.wrap()` misses the cache instead of inheriting a stale
|
|
2489
|
-
* verdict.
|
|
2490
|
-
*
|
|
2491
|
-
* Cleared on any state change (reload, trust, deny, yolo toggle) because
|
|
2492
|
-
* the result depends on the full policy state. The write-tool smart-bypass
|
|
2493
|
-
* (step 7 in `evaluate()`) is not cached since `ctx.hasRead()` changes
|
|
2494
|
-
* dynamically within a session.
|
|
2495
|
-
*
|
|
2496
|
-
* P3 #17 (before-release.md): capped at 500 entries via LRU eviction. An
|
|
2497
|
-
* iteration that evaluates thousands of unique tool+subject combinations
|
|
2498
|
-
* (e.g. repeated `tool-help` or `bash` with different commands) can grow
|
|
2499
|
-
* the cache without bound. 500 is far more than any single iteration needs;
|
|
2500
|
-
* the LRU evicts the least-recently-used entries first.
|
|
2501
|
-
*/
|
|
2502
2823
|
_evalCache = new LruCache(500);
|
|
2503
2824
|
policyDiagnostics = [];
|
|
2504
2825
|
policyInvalid = false;
|
|
2505
|
-
/**
|
|
2506
|
-
* When true, YOLO also auto-approves clearly destructive shell commands.
|
|
2507
|
-
*
|
|
2508
|
-
* WS-008: `isClearlyDestructiveBashCommand` was exported and heavily tested
|
|
2509
|
-
* but called from zero production modules, so YOLO returned `auto` for
|
|
2510
|
-
* everything — including `rm -rf`, git history rewrites, and external
|
|
2511
|
-
* publishes. The `'yolo_destructive'` decision source, the getter/setter on
|
|
2512
|
-
* PermissionPolicy, and the TUI branch that reads it all existed; only the
|
|
2513
|
-
* gate itself was missing. Defaults to false: destructive commands still ask,
|
|
2514
|
-
* even in YOLO.
|
|
2515
|
-
*/
|
|
2516
2826
|
yoloDestructive;
|
|
2517
2827
|
constructor(opts) {
|
|
2518
2828
|
this.trustFile = opts.trustFile;
|
|
@@ -2520,30 +2830,13 @@ var DefaultPermissionPolicy = class {
|
|
|
2520
2830
|
this.yoloDestructive = opts.yoloDestructive ?? false;
|
|
2521
2831
|
this.promptDelegate = opts.promptDelegate;
|
|
2522
2832
|
}
|
|
2523
|
-
/** Allow YOLO to cover destructive shell commands too (off by default). */
|
|
2524
2833
|
setYoloDestructive(enabled) {
|
|
2525
2834
|
if (this.yoloDestructive !== enabled) this._evalCache.clear();
|
|
2526
2835
|
this.yoloDestructive = enabled;
|
|
2527
2836
|
}
|
|
2528
|
-
/** Whether YOLO currently extends to destructive shell commands. */
|
|
2529
2837
|
getYoloDestructive() {
|
|
2530
2838
|
return this.yoloDestructive;
|
|
2531
2839
|
}
|
|
2532
|
-
/**
|
|
2533
|
-
* True when this call is an FS_WRITE whose target lands inside WrongStack's
|
|
2534
|
-
* own state root.
|
|
2535
|
-
*
|
|
2536
|
-
* FS_WRITE tools disagree on where the target path lives: write/edit use
|
|
2537
|
-
* `path`, replace/format/git `files` (string OR array), patch `directory`,
|
|
2538
|
-
* design `out`, scaffold writes under `cwd`/`template`. Checking only
|
|
2539
|
-
* `path`/`file_path` let a state-root write merely switch tools, so every
|
|
2540
|
-
* path-bearing key is inspected.
|
|
2541
|
-
*
|
|
2542
|
-
* Shared by the YOLO carve-out and the eval-cache guard so the two cannot
|
|
2543
|
-
* drift: the carve-out scans all path keys while the cache key fingerprints
|
|
2544
|
-
* only the subject key, so the cache must consult the same predicate or a
|
|
2545
|
-
* secondary state-root key can be replayed a cached `auto`.
|
|
2546
|
-
*/
|
|
2547
2840
|
hasAgentStateWriteTarget(tool, input, ctx) {
|
|
2548
2841
|
if (!hasCapability(tool, ToolCapabilities.FS_WRITE)) return false;
|
|
2549
2842
|
for (const targetPath of fsWriteTargetPaths(input)) {
|
|
@@ -2553,11 +2846,6 @@ var DefaultPermissionPolicy = class {
|
|
|
2553
2846
|
}
|
|
2554
2847
|
return false;
|
|
2555
2848
|
}
|
|
2556
|
-
/**
|
|
2557
|
-
* True when YOLO is on but this specific call is a destructive shell command
|
|
2558
|
-
* the user has not opted to auto-approve. Shared by `evaluate()` and the
|
|
2559
|
-
* explain/trace path so the two cannot drift.
|
|
2560
|
-
*/
|
|
2561
2849
|
yoloBlockedAsDestructive(tool, input, ctx) {
|
|
2562
2850
|
if (!this.yolo || this.yoloDestructive) return false;
|
|
2563
2851
|
if (this.hasAgentStateWriteTarget(tool, input, ctx)) return true;
|
|
@@ -2567,25 +2855,16 @@ var DefaultPermissionPolicy = class {
|
|
|
2567
2855
|
if (!command) return false;
|
|
2568
2856
|
return isClearlyDestructiveBashCommand(command, ctx.projectRoot);
|
|
2569
2857
|
}
|
|
2570
|
-
/**
|
|
2571
|
-
* Replace (or clear) the interactive prompt delegate at runtime.
|
|
2572
|
-
* Used by the CLI to switch from inline prompts (REPL) to event-driven
|
|
2573
|
-
* confirmation (TUI) when the run mode is determined after the policy
|
|
2574
|
-
* was constructed (e.g. `--goal` auto-flipping to TUI).
|
|
2575
|
-
*/
|
|
2576
2858
|
setPromptDelegate(delegate) {
|
|
2577
2859
|
this.promptDelegate = delegate;
|
|
2578
2860
|
}
|
|
2579
|
-
/** Toggle YOLO (auto-approve) mode at runtime. */
|
|
2580
2861
|
setYolo(enabled) {
|
|
2581
2862
|
if (this.yolo !== enabled) this._evalCache.clear();
|
|
2582
2863
|
this.yolo = enabled;
|
|
2583
2864
|
}
|
|
2584
|
-
/** Check whether YOLO mode is currently active. */
|
|
2585
2865
|
getYolo() {
|
|
2586
2866
|
return this.yolo;
|
|
2587
2867
|
}
|
|
2588
|
-
/** Read-only diagnostics for policy inspector/editor surfaces. */
|
|
2589
2868
|
getPolicyDiagnostics() {
|
|
2590
2869
|
return this.policyDiagnostics.map((diagnostic) => ({ ...diagnostic }));
|
|
2591
2870
|
}
|
|
@@ -2854,7 +3133,6 @@ var DefaultPermissionPolicy = class {
|
|
|
2854
3133
|
throw err;
|
|
2855
3134
|
}
|
|
2856
3135
|
}
|
|
2857
|
-
/** Persist a deny rule — this tool+pattern pair is permanently blocked. */
|
|
2858
3136
|
async deny(rule) {
|
|
2859
3137
|
if (!this.loaded) await this.reload();
|
|
2860
3138
|
if (this.policyInvalid) {
|
|
@@ -2875,335 +3153,32 @@ var DefaultPermissionPolicy = class {
|
|
|
2875
3153
|
throw err;
|
|
2876
3154
|
}
|
|
2877
3155
|
}
|
|
2878
|
-
/** Block this tool+pattern for the rest of this session (no trust file). */
|
|
2879
3156
|
denyOnce(rule) {
|
|
2880
3157
|
this.sessionDenied.set(`${rule.tool}::${rule.pattern}`, true);
|
|
2881
3158
|
this._evalCache.clear();
|
|
2882
3159
|
}
|
|
2883
|
-
/** Auto-approve this tool+pattern once (no trust file). */
|
|
2884
3160
|
allowOnce(rule) {
|
|
2885
3161
|
this.sessionAllowed.set(`${rule.tool}::${rule.pattern}`, true);
|
|
2886
3162
|
this._evalCache.clear();
|
|
2887
3163
|
}
|
|
2888
|
-
/**
|
|
2889
|
-
* Side-effect-free permission evaluation trace. Mirrors `evaluate()`
|
|
2890
|
-
* step-by-step without prompting the user, writing to trust files, or
|
|
2891
|
-
* mutating session state. Returns the full ordered trace and winner.
|
|
2892
|
-
*/
|
|
2893
3164
|
async explain(tool, input, ctx) {
|
|
2894
3165
|
if (!this.loaded) await this.reload();
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
};
|
|
2911
|
-
}
|
|
2912
|
-
add("policy valid", false, "auto", "default", "trust policy loaded successfully");
|
|
2913
|
-
const namespaceEntry = this.findNamespaceEntry(tool.name);
|
|
2914
|
-
const entry = this.policy[tool.name] ?? namespaceEntry;
|
|
2915
|
-
const namespaceSource = namespaceEntry ? `wildcard entry matched (${Object.keys(this.policy).find((k) => k.includes("*") && matchGlob(k, tool.name))})` : "no namespace match";
|
|
2916
|
-
const cacheKey = `${tool.name}::${subject ?? tool.name}`;
|
|
2917
|
-
if (this.sessionDenied.has(cacheKey)) {
|
|
2918
|
-
add(
|
|
2919
|
-
"session soft deny",
|
|
2920
|
-
true,
|
|
2921
|
-
"deny",
|
|
2922
|
-
"deny",
|
|
2923
|
-
'user pressed "no" earlier in this session \u2014 blocked until reload'
|
|
2924
|
-
);
|
|
2925
|
-
winnerIndex = steps.length - 1;
|
|
2926
|
-
return {
|
|
2927
|
-
toolName: tool.name,
|
|
2928
|
-
subject,
|
|
2929
|
-
steps,
|
|
2930
|
-
winnerIndex,
|
|
2931
|
-
decision: {
|
|
2932
|
-
permission: "deny",
|
|
2933
|
-
source: "deny",
|
|
2934
|
-
reason: "session soft deny (user pressed no)"
|
|
2935
|
-
}
|
|
2936
|
-
};
|
|
2937
|
-
}
|
|
2938
|
-
add(
|
|
2939
|
-
"session soft deny",
|
|
2940
|
-
false,
|
|
2941
|
-
"deny",
|
|
2942
|
-
"deny",
|
|
2943
|
-
"no session-level soft deny for this tool+subject"
|
|
2944
|
-
);
|
|
2945
|
-
if (this.sessionAllowed.has(cacheKey)) {
|
|
2946
|
-
add(
|
|
2947
|
-
"session soft allow",
|
|
2948
|
-
true,
|
|
2949
|
-
"auto",
|
|
2950
|
-
"trust",
|
|
2951
|
-
'user pressed "yes" in current session \u2014 one-shot auto-approve (consumed)'
|
|
2952
|
-
);
|
|
2953
|
-
winnerIndex = steps.length - 1;
|
|
2954
|
-
return {
|
|
2955
|
-
toolName: tool.name,
|
|
2956
|
-
subject,
|
|
2957
|
-
steps,
|
|
2958
|
-
winnerIndex,
|
|
2959
|
-
decision: {
|
|
2960
|
-
permission: "auto",
|
|
2961
|
-
source: "trust",
|
|
2962
|
-
reason: "session one-shot allow (user pressed yes)"
|
|
2963
|
-
}
|
|
2964
|
-
};
|
|
2965
|
-
}
|
|
2966
|
-
add(
|
|
2967
|
-
"session soft allow",
|
|
2968
|
-
false,
|
|
2969
|
-
"auto",
|
|
2970
|
-
"trust",
|
|
2971
|
-
"no session-level one-shot allow for this tool+subject"
|
|
2972
|
-
);
|
|
2973
|
-
if (entry?.deny && subject && matchesTrust(entry.deny, subject)) {
|
|
2974
|
-
add(
|
|
2975
|
-
"trust deny",
|
|
2976
|
-
true,
|
|
2977
|
-
"deny",
|
|
2978
|
-
"deny",
|
|
2979
|
-
`subject "${subject}" matched a deny pattern in trust file`
|
|
2980
|
-
);
|
|
2981
|
-
winnerIndex = steps.length - 1;
|
|
2982
|
-
return {
|
|
2983
|
-
toolName: tool.name,
|
|
2984
|
-
subject,
|
|
2985
|
-
steps,
|
|
2986
|
-
winnerIndex,
|
|
2987
|
-
decision: { permission: "deny", source: "deny", reason: "matched deny pattern" }
|
|
2988
|
-
};
|
|
2989
|
-
}
|
|
2990
|
-
add(
|
|
2991
|
-
"trust deny",
|
|
2992
|
-
false,
|
|
2993
|
-
"deny",
|
|
2994
|
-
"deny",
|
|
2995
|
-
`no deny pattern matched (namespace: ${namespaceSource})`
|
|
2996
|
-
);
|
|
2997
|
-
if (tool.permission === "deny") {
|
|
2998
|
-
add(
|
|
2999
|
-
"tool default deny",
|
|
3000
|
-
true,
|
|
3001
|
-
"deny",
|
|
3002
|
-
"default",
|
|
3003
|
-
`tool "${tool.name}" has permission: deny by default`
|
|
3004
|
-
);
|
|
3005
|
-
winnerIndex = steps.length - 1;
|
|
3006
|
-
return {
|
|
3007
|
-
toolName: tool.name,
|
|
3008
|
-
subject,
|
|
3009
|
-
steps,
|
|
3010
|
-
winnerIndex,
|
|
3011
|
-
decision: { permission: "deny", source: "default", reason: "tool default deny" }
|
|
3012
|
-
};
|
|
3013
|
-
}
|
|
3014
|
-
add(
|
|
3015
|
-
"tool default deny",
|
|
3016
|
-
false,
|
|
3017
|
-
"deny",
|
|
3018
|
-
"default",
|
|
3019
|
-
`tool "${tool.name}" has permission: "${tool.permission}"`
|
|
3020
|
-
);
|
|
3021
|
-
if (entry?.allow && subject && matchesTrust(entry.allow, subject)) {
|
|
3022
|
-
add(
|
|
3023
|
-
"trust allow",
|
|
3024
|
-
true,
|
|
3025
|
-
"auto",
|
|
3026
|
-
"trust",
|
|
3027
|
-
`subject "${subject}" matched an allow pattern in trust file`
|
|
3028
|
-
);
|
|
3029
|
-
winnerIndex = steps.length - 1;
|
|
3030
|
-
return {
|
|
3031
|
-
toolName: tool.name,
|
|
3032
|
-
subject,
|
|
3033
|
-
steps,
|
|
3034
|
-
winnerIndex,
|
|
3035
|
-
decision: { permission: "auto", source: "trust", reason: "matched allow pattern" }
|
|
3036
|
-
};
|
|
3037
|
-
}
|
|
3038
|
-
add(
|
|
3039
|
-
"trust allow",
|
|
3040
|
-
false,
|
|
3041
|
-
"auto",
|
|
3042
|
-
"trust",
|
|
3043
|
-
`no allow pattern matched (namespace: ${namespaceSource})`
|
|
3044
|
-
);
|
|
3045
|
-
if (entry?.auto) {
|
|
3046
|
-
add("trust auto", true, "auto", "trust", `trust file has auto: true for "${tool.name}"`);
|
|
3047
|
-
winnerIndex = steps.length - 1;
|
|
3048
|
-
return {
|
|
3049
|
-
toolName: tool.name,
|
|
3050
|
-
subject,
|
|
3051
|
-
steps,
|
|
3052
|
-
winnerIndex,
|
|
3053
|
-
decision: { permission: "auto", source: "trust", reason: "trust auto" }
|
|
3054
|
-
};
|
|
3055
|
-
}
|
|
3056
|
-
add("trust auto", false, "auto", "trust", `no auto flag for "${tool.name}" in trust file`);
|
|
3057
|
-
if (!this.yolo && this.isSensitiveReadCall(tool, input)) {
|
|
3058
|
-
const hasDelegate2 = this.promptDelegate !== void 0;
|
|
3059
|
-
add(
|
|
3060
|
-
"sensitive read",
|
|
3061
|
-
true,
|
|
3062
|
-
hasDelegate2 ? "confirm" : "confirm",
|
|
3063
|
-
"default",
|
|
3064
|
-
hasDelegate2 ? "sensitive file read detected \u2014 would prompt user for approval" : "sensitive file read detected \u2014 returns confirm (no prompt delegate)"
|
|
3065
|
-
);
|
|
3066
|
-
winnerIndex = steps.length - 1;
|
|
3067
|
-
return {
|
|
3068
|
-
toolName: tool.name,
|
|
3069
|
-
subject,
|
|
3070
|
-
steps,
|
|
3071
|
-
winnerIndex,
|
|
3072
|
-
decision: {
|
|
3073
|
-
permission: "confirm",
|
|
3074
|
-
source: "default",
|
|
3075
|
-
riskTier: "standard",
|
|
3076
|
-
reason: "sensitive file read needs explicit approval"
|
|
3077
|
-
}
|
|
3078
|
-
};
|
|
3079
|
-
}
|
|
3080
|
-
add(
|
|
3081
|
-
"sensitive read",
|
|
3082
|
-
false,
|
|
3083
|
-
"confirm",
|
|
3084
|
-
"default",
|
|
3085
|
-
"not a sensitive read call (or YOLO bypasses this check)"
|
|
3086
|
-
);
|
|
3087
|
-
if (this.yolo) {
|
|
3088
|
-
if (this.yoloBlockedAsDestructive(tool, input, ctx)) {
|
|
3089
|
-
add(
|
|
3090
|
-
"yolo destructive gate",
|
|
3091
|
-
true,
|
|
3092
|
-
"confirm",
|
|
3093
|
-
"yolo_destructive",
|
|
3094
|
-
"YOLO is active but this is a clearly destructive command \u2014 still asking"
|
|
3095
|
-
);
|
|
3096
|
-
winnerIndex = steps.length - 1;
|
|
3097
|
-
return {
|
|
3098
|
-
toolName: tool.name,
|
|
3099
|
-
subject,
|
|
3100
|
-
steps,
|
|
3101
|
-
winnerIndex,
|
|
3102
|
-
decision: {
|
|
3103
|
-
permission: "confirm",
|
|
3104
|
-
source: "yolo_destructive",
|
|
3105
|
-
riskTier: "destructive",
|
|
3106
|
-
reason: "destructive command needs explicit approval even in YOLO mode"
|
|
3107
|
-
}
|
|
3108
|
-
};
|
|
3109
|
-
}
|
|
3110
|
-
add(
|
|
3111
|
-
"yolo",
|
|
3112
|
-
true,
|
|
3113
|
-
"auto",
|
|
3114
|
-
"yolo",
|
|
3115
|
-
"YOLO mode is active \u2014 auto-approving every non-denied call"
|
|
3116
|
-
);
|
|
3117
|
-
winnerIndex = steps.length - 1;
|
|
3118
|
-
return {
|
|
3119
|
-
toolName: tool.name,
|
|
3120
|
-
subject,
|
|
3121
|
-
steps,
|
|
3122
|
-
winnerIndex,
|
|
3123
|
-
decision: { permission: "auto", source: "yolo" }
|
|
3124
|
-
};
|
|
3125
|
-
}
|
|
3126
|
-
add("yolo", false, "auto", "yolo", "YOLO mode is not active");
|
|
3127
|
-
if (tool.name === "write" && subject) {
|
|
3128
|
-
const isAgentState = isInsideAgentStateRoot(subject);
|
|
3129
|
-
const hasRead = ctx.hasRead(subject) && !isAgentState;
|
|
3130
|
-
add(
|
|
3131
|
-
"write smart bypass",
|
|
3132
|
-
hasRead,
|
|
3133
|
-
"auto",
|
|
3134
|
-
"context",
|
|
3135
|
-
isAgentState ? `file "${subject}" is WrongStack's own state \u2014 bypass never applies, write always confirms` : hasRead ? `file "${subject}" was already read in this session \u2014 auto-approving write` : `file "${subject}" was not read in this session \u2014 bypass does not apply`
|
|
3136
|
-
);
|
|
3137
|
-
if (hasRead) {
|
|
3138
|
-
winnerIndex = steps.length - 1;
|
|
3139
|
-
return {
|
|
3140
|
-
toolName: tool.name,
|
|
3141
|
-
subject,
|
|
3142
|
-
steps,
|
|
3143
|
-
winnerIndex,
|
|
3144
|
-
decision: {
|
|
3145
|
-
permission: "auto",
|
|
3146
|
-
source: "context",
|
|
3147
|
-
reason: "file already read in this session"
|
|
3148
|
-
}
|
|
3149
|
-
};
|
|
3150
|
-
}
|
|
3151
|
-
} else {
|
|
3152
|
-
add(
|
|
3153
|
-
"write smart bypass",
|
|
3154
|
-
false,
|
|
3155
|
-
"auto",
|
|
3156
|
-
"context",
|
|
3157
|
-
'tool is not "write" or has no subject \u2014 bypass does not apply'
|
|
3158
|
-
);
|
|
3159
|
-
}
|
|
3160
|
-
const hasWriteCap = hasCapability(tool, ToolCapabilities.FS_WRITE);
|
|
3161
|
-
const hasShellCap = hasCapability(tool, [
|
|
3162
|
-
ToolCapabilities.SHELL_ARBITRARY,
|
|
3163
|
-
ToolCapabilities.SHELL_RESTRICTED,
|
|
3164
|
-
ToolCapabilities.SHELL_EXEC
|
|
3165
|
-
]);
|
|
3166
|
-
const hasInstallCap = hasCapability(tool, ToolCapabilities.PACKAGE_INSTALL);
|
|
3167
|
-
const hasConfigCap = hasCapability(tool, ToolCapabilities.CONFIG_MUTATE);
|
|
3168
|
-
const hasSubagentCap = hasCapability(tool, ToolCapabilities.SUBAGENT_SPAWN);
|
|
3169
|
-
const isMutating = tool.mutating || hasWriteCap || hasShellCap || hasInstallCap || hasConfigCap || hasSubagentCap;
|
|
3170
|
-
if (tool.permission === "auto" && !isMutating) {
|
|
3171
|
-
add(
|
|
3172
|
-
"safe default auto",
|
|
3173
|
-
true,
|
|
3174
|
-
"auto",
|
|
3175
|
-
"default",
|
|
3176
|
-
`tool "${tool.name}" has auto permission and is not mutating \u2014 safe to auto-approve`
|
|
3177
|
-
);
|
|
3178
|
-
winnerIndex = steps.length - 1;
|
|
3179
|
-
return {
|
|
3180
|
-
toolName: tool.name,
|
|
3181
|
-
subject,
|
|
3182
|
-
steps,
|
|
3183
|
-
winnerIndex,
|
|
3184
|
-
decision: { permission: "auto", source: "default" }
|
|
3185
|
-
};
|
|
3186
|
-
}
|
|
3187
|
-
add(
|
|
3188
|
-
"mutating default confirm",
|
|
3189
|
-
true,
|
|
3190
|
-
"confirm",
|
|
3191
|
-
"default",
|
|
3192
|
-
isMutating ? `tool "${tool.name}" is mutating (default auto not enough) \u2014 needs confirmation` : `tool "${tool.name}" has "${tool.permission}" permission \u2014 needs confirmation`
|
|
3166
|
+
return explainPermissionTrace(
|
|
3167
|
+
{
|
|
3168
|
+
policy: this.policy,
|
|
3169
|
+
policyInvalid: this.policyInvalid,
|
|
3170
|
+
wildcardEntries: this.wildcardEntries,
|
|
3171
|
+
sessionDenied: this.sessionDenied,
|
|
3172
|
+
sessionAllowed: this.sessionAllowed,
|
|
3173
|
+
yolo: this.yolo,
|
|
3174
|
+
promptDelegatePresent: this.promptDelegate !== void 0,
|
|
3175
|
+
isSensitiveReadCall: (t, inp) => this.isSensitiveReadCall(t, inp),
|
|
3176
|
+
yoloBlockedAsDestructive: (t, inp, c) => this.yoloBlockedAsDestructive(t, inp, c)
|
|
3177
|
+
},
|
|
3178
|
+
tool,
|
|
3179
|
+
input,
|
|
3180
|
+
ctx
|
|
3193
3181
|
);
|
|
3194
|
-
winnerIndex = steps.length - 1;
|
|
3195
|
-
const hasDelegate = this.promptDelegate !== void 0;
|
|
3196
|
-
return {
|
|
3197
|
-
toolName: tool.name,
|
|
3198
|
-
subject,
|
|
3199
|
-
steps,
|
|
3200
|
-
winnerIndex,
|
|
3201
|
-
decision: {
|
|
3202
|
-
permission: "confirm",
|
|
3203
|
-
source: "default",
|
|
3204
|
-
...hasDelegate ? { reason: "would prompt user via delegate" } : {}
|
|
3205
|
-
}
|
|
3206
|
-
};
|
|
3207
3182
|
}
|
|
3208
3183
|
findNamespaceEntry(toolName) {
|
|
3209
3184
|
for (const { pattern, value } of this.wildcardEntries) {
|