@wrongstack/core 0.306.3 → 0.307.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +1738 -1690
- 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/index.js +6103 -5890
- 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/conversation-state.d.ts +1 -2
- package/dist/core/fallback-doctor.d.ts +72 -0
- package/dist/core/fallback-model.d.ts +19 -1
- package/dist/core/fallback-profile-manager.d.ts +21 -3
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +1143 -766
- package/dist/defaults/index.js +3833 -3595
- package/dist/execution/auto-compaction-middleware.d.ts +64 -0
- 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 -166
- 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.d.ts +1 -1
- package/dist/execution/index.js +10016 -9903
- package/dist/execution/prompt-enhancer.js +11 -3
- package/dist/execution/retry-policy.d.ts +27 -0
- 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 +58 -17
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8731 -7756
- package/dist/infrastructure/index.js +24 -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 +2123 -1918
- 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/prompt-journal.d.ts +94 -0
- package/dist/security/index.js +677 -667
- 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 +270 -227
- package/dist/session-catalog/project-server.js +277 -234
- 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/cloud-config-sync/sanitize.d.ts +18 -0
- package/dist/storage/cloud-config-sync.d.ts +1 -1
- 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 +2121 -1736
- package/dist/storage/orphan-lock-cleaner.d.ts +15 -0
- package/dist/storage/provider-config-watcher.d.ts +9 -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/strict-empty-check.d.ts +7 -0
- package/dist/storage/session-store.d.ts +2 -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.d.ts +1 -1
- package/dist/tools/index.js +5150 -5090
- package/dist/types/config/root.d.ts +21 -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/types/session.d.ts +7 -0
- package/dist/worktree/worktree-manager.d.ts +1 -1
- package/instructions/coordination/subagent-baseline.md +2 -2
- package/instructions/llm/prompt-enhancer.md +2 -1
- package/instructions/sections/tool/common-patterns.md +21 -0
- package/instructions/sections/tool/delegation-compact.md +3 -1
- package/instructions/sections/tool/delegation-full.md +5 -1
- package/instructions/system-lite.md +28 -3
- package/instructions/system-pro.md +37 -8
- package/instructions/system.md +4 -2
- package/package.json +5 -5
package/dist/security/index.js
CHANGED
|
@@ -894,6 +894,24 @@ function validateDirectoryPolicy(value) {
|
|
|
894
894
|
import { homedir } from "node:os";
|
|
895
895
|
|
|
896
896
|
// src/security/secret-scrubber.ts
|
|
897
|
+
var JSON_CREDENTIAL_KEY_ANCHORS = [
|
|
898
|
+
'Key"',
|
|
899
|
+
'key"',
|
|
900
|
+
'KEY"',
|
|
901
|
+
'token"',
|
|
902
|
+
'Token"',
|
|
903
|
+
'TOKEN"',
|
|
904
|
+
'secret"',
|
|
905
|
+
'Secret"',
|
|
906
|
+
'SECRET"',
|
|
907
|
+
'password"',
|
|
908
|
+
'Password"',
|
|
909
|
+
'PASSWORD"',
|
|
910
|
+
'authorization"',
|
|
911
|
+
'Authorization"',
|
|
912
|
+
'bearer"',
|
|
913
|
+
'Bearer"'
|
|
914
|
+
];
|
|
897
915
|
var PATTERNS = [
|
|
898
916
|
// Anchored at the start where possible so partial matches inside larger
|
|
899
917
|
// strings don't trigger false positives.
|
|
@@ -996,6 +1014,30 @@ var PATTERNS = [
|
|
|
996
1014
|
regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
|
|
997
1015
|
anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
|
|
998
1016
|
},
|
|
1017
|
+
{
|
|
1018
|
+
type: "json_credential_key",
|
|
1019
|
+
// The JSON counterpart to `high_entropy_env`, and the pattern that the
|
|
1020
|
+
// now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
|
|
1021
|
+
// anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
|
|
1022
|
+
// "this text may hold a secret", every pattern then declined to match, and
|
|
1023
|
+
// the value went out verbatim. `high_entropy_env` cannot cover these: it
|
|
1024
|
+
// requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
|
|
1025
|
+
// never matched.
|
|
1026
|
+
//
|
|
1027
|
+
// Tool results are routinely serialised as JSON, and a credential with no
|
|
1028
|
+
// recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
|
|
1029
|
+
// has no other pattern that can catch it — this is the only thing standing
|
|
1030
|
+
// between such a value and the session JSONL, chronicle, HQ broadcast and
|
|
1031
|
+
// the model's own context.
|
|
1032
|
+
//
|
|
1033
|
+
// The key may carry a prefix (`"anthropicApiKey"`), but the credential word
|
|
1034
|
+
// must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
|
|
1035
|
+
// the closing quote has to follow the word immediately.
|
|
1036
|
+
// Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
|
|
1037
|
+
// out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
|
|
1038
|
+
regex: /("[A-Za-z0-9_]*(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s*:\s*")([^"\\]{8,512})(")/gi,
|
|
1039
|
+
anchor: JSON_CREDENTIAL_KEY_ANCHORS
|
|
1040
|
+
},
|
|
999
1041
|
// ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
|
|
1000
1042
|
// The plugin runtime carried 37 patterns while this scrubber — the one that
|
|
1001
1043
|
// guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
|
|
@@ -1078,9 +1120,12 @@ var PATTERNS = [
|
|
|
1078
1120
|
anchor: "GOCSPX-"
|
|
1079
1121
|
}
|
|
1080
1122
|
];
|
|
1081
|
-
var SIMPLE_PATTERNS = PATTERNS.filter(
|
|
1123
|
+
var SIMPLE_PATTERNS = PATTERNS.filter(
|
|
1124
|
+
(p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
|
|
1125
|
+
);
|
|
1082
1126
|
var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
|
|
1083
1127
|
var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
|
|
1128
|
+
var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
|
|
1084
1129
|
var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
|
|
1085
1130
|
var SCRUB_CHUNK_BYTES = 64 * 1024;
|
|
1086
1131
|
var SCRUB_OVERLAP_BYTES = 1024;
|
|
@@ -1091,20 +1136,7 @@ var PATTERN_ANCHORS = [
|
|
|
1091
1136
|
)
|
|
1092
1137
|
)
|
|
1093
1138
|
];
|
|
1094
|
-
var
|
|
1095
|
-
'"apiKey"',
|
|
1096
|
-
'"api_key"',
|
|
1097
|
-
'"token"',
|
|
1098
|
-
'"secret"',
|
|
1099
|
-
'"password"',
|
|
1100
|
-
'"authorization"',
|
|
1101
|
-
'"bearer"',
|
|
1102
|
-
'"private_key"',
|
|
1103
|
-
'"access_token"',
|
|
1104
|
-
'"refresh_token"',
|
|
1105
|
-
'"client_secret"'
|
|
1106
|
-
];
|
|
1107
|
-
var ALL_ANCHORS = [...PATTERN_ANCHORS, ...JSON_KEY_ANCHORS];
|
|
1139
|
+
var ALL_ANCHORS = PATTERN_ANCHORS;
|
|
1108
1140
|
function hasCredentialAnchors(text) {
|
|
1109
1141
|
for (const anchor of ALL_ANCHORS) {
|
|
1110
1142
|
if (text.includes(anchor)) return true;
|
|
@@ -1153,6 +1185,9 @@ var DefaultSecretScrubber = class {
|
|
|
1153
1185
|
out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
|
|
1154
1186
|
return `${lead}${key}=[REDACTED:high_entropy_env]`;
|
|
1155
1187
|
});
|
|
1188
|
+
out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
|
|
1189
|
+
return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
|
|
1190
|
+
});
|
|
1156
1191
|
return out;
|
|
1157
1192
|
}
|
|
1158
1193
|
/**
|
|
@@ -1751,175 +1786,23 @@ function subjectForToolInput(toolName, input, subjectKey) {
|
|
|
1751
1786
|
return void 0;
|
|
1752
1787
|
}
|
|
1753
1788
|
|
|
1754
|
-
// src/security/permission-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
title: "WrongStack Trust Policy",
|
|
1766
|
-
type: "object",
|
|
1767
|
-
maxProperties: TRUST_POLICY_LIMITS.maxTools,
|
|
1768
|
-
propertyNames: {
|
|
1769
|
-
minLength: 1,
|
|
1770
|
-
maxLength: TRUST_POLICY_LIMITS.maxToolNameChars,
|
|
1771
|
-
not: { enum: ["__proto__", "prototype", "constructor"] }
|
|
1772
|
-
},
|
|
1773
|
-
additionalProperties: {
|
|
1774
|
-
type: "object",
|
|
1775
|
-
additionalProperties: false,
|
|
1776
|
-
properties: {
|
|
1777
|
-
allow: {
|
|
1778
|
-
type: "array",
|
|
1779
|
-
maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
|
|
1780
|
-
items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
|
|
1781
|
-
},
|
|
1782
|
-
deny: {
|
|
1783
|
-
type: "array",
|
|
1784
|
-
maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
|
|
1785
|
-
items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
|
|
1786
|
-
},
|
|
1787
|
-
auto: { type: "boolean" },
|
|
1788
|
-
trustWorkdir: { type: "boolean" },
|
|
1789
|
-
denyPrivate: { type: "boolean" }
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
});
|
|
1793
|
-
var RULE_FIELDS2 = /* @__PURE__ */ new Set(["allow", "deny", "auto", "trustWorkdir", "denyPrivate"]);
|
|
1794
|
-
var UNSAFE_PROPERTY_NAMES2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1795
|
-
function isRecord2(value) {
|
|
1796
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1797
|
-
}
|
|
1798
|
-
function error2(diagnostics, code, path9, message) {
|
|
1799
|
-
diagnostics.push({ severity: "error", code, path: path9, message });
|
|
1800
|
-
}
|
|
1801
|
-
function validatePatterns(value, path9, diagnostics) {
|
|
1802
|
-
if (!Array.isArray(value)) {
|
|
1803
|
-
error2(diagnostics, "invalid_pattern_list", path9, "must be an array of strings");
|
|
1804
|
-
return void 0;
|
|
1805
|
-
}
|
|
1806
|
-
if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
|
|
1807
|
-
error2(
|
|
1808
|
-
diagnostics,
|
|
1809
|
-
"too_many_patterns",
|
|
1810
|
-
path9,
|
|
1811
|
-
`must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
|
|
1812
|
-
);
|
|
1813
|
-
return void 0;
|
|
1814
|
-
}
|
|
1815
|
-
const patterns = [];
|
|
1816
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1817
|
-
for (const [index, pattern] of value.entries()) {
|
|
1818
|
-
const itemPath = `${path9}[${index}]`;
|
|
1819
|
-
if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
|
|
1820
|
-
error2(
|
|
1821
|
-
diagnostics,
|
|
1822
|
-
"invalid_pattern",
|
|
1823
|
-
itemPath,
|
|
1824
|
-
`must be a non-empty string up to ${TRUST_POLICY_LIMITS.maxPatternChars} characters`
|
|
1825
|
-
);
|
|
1826
|
-
continue;
|
|
1827
|
-
}
|
|
1828
|
-
if (seen.has(pattern)) {
|
|
1829
|
-
diagnostics.push({
|
|
1830
|
-
severity: "warning",
|
|
1831
|
-
code: "duplicate_pattern",
|
|
1832
|
-
path: itemPath,
|
|
1833
|
-
message: "duplicates an earlier pattern and was normalized away"
|
|
1834
|
-
});
|
|
1835
|
-
continue;
|
|
1836
|
-
}
|
|
1837
|
-
seen.add(pattern);
|
|
1838
|
-
patterns.push(pattern);
|
|
1839
|
-
}
|
|
1840
|
-
return patterns;
|
|
1841
|
-
}
|
|
1842
|
-
function validateTrustPolicy(value) {
|
|
1843
|
-
const diagnostics = [];
|
|
1844
|
-
if (!isRecord2(value)) {
|
|
1845
|
-
return {
|
|
1846
|
-
ok: false,
|
|
1847
|
-
diagnostics: [
|
|
1848
|
-
{
|
|
1849
|
-
severity: "error",
|
|
1850
|
-
code: "invalid_root",
|
|
1851
|
-
path: "$",
|
|
1852
|
-
message: "trust policy must be an object"
|
|
1853
|
-
}
|
|
1854
|
-
]
|
|
1855
|
-
};
|
|
1856
|
-
}
|
|
1857
|
-
const entries = Object.entries(value);
|
|
1858
|
-
if (entries.length > TRUST_POLICY_LIMITS.maxTools) {
|
|
1859
|
-
error2(
|
|
1860
|
-
diagnostics,
|
|
1861
|
-
"too_many_tools",
|
|
1862
|
-
"$",
|
|
1863
|
-
`must contain at most ${TRUST_POLICY_LIMITS.maxTools} tool rules`
|
|
1864
|
-
);
|
|
1865
|
-
}
|
|
1866
|
-
const policy = {};
|
|
1867
|
-
for (const [toolName, rawRule] of entries.slice(0, TRUST_POLICY_LIMITS.maxTools)) {
|
|
1868
|
-
const toolPath = `$[${JSON.stringify(toolName)}]`;
|
|
1869
|
-
if (toolName.length === 0 || toolName.length > TRUST_POLICY_LIMITS.maxToolNameChars) {
|
|
1870
|
-
error2(
|
|
1871
|
-
diagnostics,
|
|
1872
|
-
"invalid_tool_name",
|
|
1873
|
-
toolPath,
|
|
1874
|
-
`tool name must be 1-${TRUST_POLICY_LIMITS.maxToolNameChars} characters`
|
|
1875
|
-
);
|
|
1876
|
-
continue;
|
|
1877
|
-
}
|
|
1878
|
-
if (UNSAFE_PROPERTY_NAMES2.has(toolName)) {
|
|
1879
|
-
error2(
|
|
1880
|
-
diagnostics,
|
|
1881
|
-
"unsafe_tool_name",
|
|
1882
|
-
toolPath,
|
|
1883
|
-
"reserved object property names are not valid tool rules"
|
|
1884
|
-
);
|
|
1885
|
-
continue;
|
|
1886
|
-
}
|
|
1887
|
-
if (!isRecord2(rawRule)) {
|
|
1888
|
-
error2(diagnostics, "invalid_rule", toolPath, "tool rule must be an object");
|
|
1889
|
-
continue;
|
|
1890
|
-
}
|
|
1891
|
-
for (const field of Object.keys(rawRule)) {
|
|
1892
|
-
if (!RULE_FIELDS2.has(field)) {
|
|
1893
|
-
error2(
|
|
1894
|
-
diagnostics,
|
|
1895
|
-
"unknown_field",
|
|
1896
|
-
`${toolPath}.${field}`,
|
|
1897
|
-
`unknown policy field "${field}"`
|
|
1898
|
-
);
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
const rule = {};
|
|
1902
|
-
for (const field of ["allow", "deny"]) {
|
|
1903
|
-
if (rawRule[field] === void 0) continue;
|
|
1904
|
-
const patterns = validatePatterns(rawRule[field], `${toolPath}.${field}`, diagnostics);
|
|
1905
|
-
if (patterns) rule[field] = patterns;
|
|
1906
|
-
}
|
|
1907
|
-
for (const field of ["auto", "trustWorkdir", "denyPrivate"]) {
|
|
1908
|
-
const setting = rawRule[field];
|
|
1909
|
-
if (setting === void 0) continue;
|
|
1910
|
-
if (typeof setting !== "boolean") {
|
|
1911
|
-
error2(diagnostics, "invalid_boolean", `${toolPath}.${field}`, "must be a boolean");
|
|
1912
|
-
} else {
|
|
1913
|
-
rule[field] = setting;
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
policy[toolName] = rule;
|
|
1917
|
-
}
|
|
1918
|
-
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");
|
|
1919
1800
|
}
|
|
1920
1801
|
|
|
1921
1802
|
// src/security/yolo-risk.ts
|
|
1922
|
-
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;
|
|
1923
1806
|
var CATASTROPHIC_PATTERNS = [
|
|
1924
1807
|
/\b(?:mkfs(?:\.[a-z0-9]+)?|mke2fs|newfs)\b/i,
|
|
1925
1808
|
// make a filesystem — wipes a partition
|
|
@@ -1979,9 +1862,9 @@ function getInputString(input, key) {
|
|
|
1979
1862
|
function pathLooksInsideProject(rawPath, projectRoot) {
|
|
1980
1863
|
if (!projectRoot) return false;
|
|
1981
1864
|
if (rawPath === "~" || rawPath.startsWith("~/") || rawPath.startsWith("~\\")) return false;
|
|
1982
|
-
const resolved =
|
|
1983
|
-
const relative4 =
|
|
1984
|
-
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);
|
|
1985
1868
|
}
|
|
1986
1869
|
function tokenizeShell(command) {
|
|
1987
1870
|
return command.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
@@ -2155,6 +2038,48 @@ function hasCatastrophicDelete(command) {
|
|
|
2155
2038
|
}
|
|
2156
2039
|
return false;
|
|
2157
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
|
+
}
|
|
2158
2083
|
function isClearlyDestructiveBashCommand(command, projectRoot) {
|
|
2159
2084
|
const trimmed = command.trim();
|
|
2160
2085
|
if (!trimmed) return false;
|
|
@@ -2163,85 +2088,12 @@ function isClearlyDestructiveBashCommand(command, projectRoot) {
|
|
|
2163
2088
|
if (hasExternalPublish(trimmed)) return true;
|
|
2164
2089
|
if (hasFindExec(trimmed)) return true;
|
|
2165
2090
|
if (hasCatastrophicDelete(trimmed)) return true;
|
|
2091
|
+
if (hasWriteToAgentStateRoot(trimmed)) return true;
|
|
2166
2092
|
if (CATASTROPHIC_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
|
|
2167
2093
|
if (HIGH_IMPACT_PATTERNS.some((pattern) => pattern.test(trimmed))) return true;
|
|
2168
2094
|
return false;
|
|
2169
2095
|
}
|
|
2170
2096
|
|
|
2171
|
-
// src/security/auto-approve-policy.ts
|
|
2172
|
-
var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
2173
|
-
allowedCapabilities;
|
|
2174
|
-
constructor(allowedCapabilities) {
|
|
2175
|
-
this.allowedCapabilities = allowedCapabilities ?? [
|
|
2176
|
-
ToolCapabilities.FS_READ,
|
|
2177
|
-
ToolCapabilities.NET_OUTBOUND
|
|
2178
|
-
];
|
|
2179
|
-
}
|
|
2180
|
-
static isMcpTool(name) {
|
|
2181
|
-
return name.startsWith("mcp__");
|
|
2182
|
-
}
|
|
2183
|
-
async evaluate(tool) {
|
|
2184
|
-
const caps = tool.capabilities ?? [];
|
|
2185
|
-
const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
|
|
2186
|
-
const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
|
|
2187
|
-
const mcpProxyAllowed = this.allowedCapabilities.includes(ToolCapabilities.MCP_PROXY);
|
|
2188
|
-
const dangerousNotAllowed = getDangerousCapabilities(tool).filter(
|
|
2189
|
-
(c) => !this.allowedCapabilities.includes(c)
|
|
2190
|
-
);
|
|
2191
|
-
const blocked = tool.permission === "deny" || isMcp && !mcpProxyAllowed || !hasAllowedCap || dangerousNotAllowed.length > 0;
|
|
2192
|
-
if (blocked) {
|
|
2193
|
-
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(", ")})`;
|
|
2194
|
-
return {
|
|
2195
|
-
permission: "deny",
|
|
2196
|
-
source: "subagent_guard",
|
|
2197
|
-
reason
|
|
2198
|
-
};
|
|
2199
|
-
}
|
|
2200
|
-
return { permission: "auto", source: "yolo" };
|
|
2201
|
-
}
|
|
2202
|
-
async trust() {
|
|
2203
|
-
}
|
|
2204
|
-
async deny() {
|
|
2205
|
-
}
|
|
2206
|
-
denyOnce() {
|
|
2207
|
-
}
|
|
2208
|
-
allowOnce() {
|
|
2209
|
-
}
|
|
2210
|
-
async explain(tool) {
|
|
2211
|
-
const decision = await this.evaluate(tool);
|
|
2212
|
-
return {
|
|
2213
|
-
toolName: tool.name,
|
|
2214
|
-
subject: null,
|
|
2215
|
-
steps: [
|
|
2216
|
-
{
|
|
2217
|
-
rule: "subagent auto",
|
|
2218
|
-
matched: decision.permission === "auto",
|
|
2219
|
-
decision: decision.permission,
|
|
2220
|
-
source: decision.source,
|
|
2221
|
-
detail: decision.reason ?? `subagent policy: ${decision.permission}`
|
|
2222
|
-
}
|
|
2223
|
-
],
|
|
2224
|
-
winnerIndex: 0,
|
|
2225
|
-
decision
|
|
2226
|
-
};
|
|
2227
|
-
}
|
|
2228
|
-
async reload() {
|
|
2229
|
-
}
|
|
2230
|
-
};
|
|
2231
|
-
|
|
2232
|
-
// src/security/permission-helpers.ts
|
|
2233
|
-
import { realpathSync } from "node:fs";
|
|
2234
|
-
import * as path5 from "node:path";
|
|
2235
|
-
|
|
2236
|
-
// src/utils/wstack-paths.ts
|
|
2237
|
-
import * as os from "node:os";
|
|
2238
|
-
import * as path4 from "node:path";
|
|
2239
|
-
function wstackGlobalRoot() {
|
|
2240
|
-
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
2241
|
-
if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
|
|
2242
|
-
return path4.join(os.homedir(), ".wrongstack");
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
2097
|
// src/security/permission-helpers.ts
|
|
2246
2098
|
function matchesTrust(patterns, subject) {
|
|
2247
2099
|
return patterns.includes(subject) || matchAny(patterns, subject);
|
|
@@ -2377,8 +2229,558 @@ function shellCommandReadsSensitivePath(command) {
|
|
|
2377
2229
|
const rest = tokens.slice(i + 1);
|
|
2378
2230
|
if (rest.some((arg) => pathLooksSensitive(arg))) return true;
|
|
2379
2231
|
}
|
|
2380
|
-
return false;
|
|
2381
|
-
}
|
|
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
|
+
};
|
|
2382
2784
|
|
|
2383
2785
|
// src/security/permission-policy.ts
|
|
2384
2786
|
function fsWriteTargetPaths(input) {
|
|
@@ -2414,70 +2816,13 @@ var DefaultPermissionPolicy = class {
|
|
|
2414
2816
|
loaded = false;
|
|
2415
2817
|
trustFile;
|
|
2416
2818
|
yolo;
|
|
2417
|
-
/**
|
|
2418
|
-
* Session-scoped "soft deny" map. When the user presses 'n' (block once),
|
|
2419
|
-
* the tool+pattern is added here. If the LLM retries in the same session,
|
|
2420
|
-
* we return deny directly without asking again.
|
|
2421
|
-
*
|
|
2422
|
-
* Cleared on reload() since reload = fresh trust file snapshot.
|
|
2423
|
-
*/
|
|
2424
2819
|
sessionDenied = /* @__PURE__ */ new Map();
|
|
2425
|
-
/**
|
|
2426
|
-
* Session-scoped one-shot "soft trust" map. When the user presses 'y', the
|
|
2427
|
-
* tool+pattern is added here so the immediate confirm re-run can proceed.
|
|
2428
|
-
* The entry is consumed on first use; future calls must ask again unless the
|
|
2429
|
-
* user chose persistent trust.
|
|
2430
|
-
*
|
|
2431
|
-
* Cleared on reload().
|
|
2432
|
-
*/
|
|
2433
2820
|
sessionAllowed = /* @__PURE__ */ new Map();
|
|
2434
|
-
/**
|
|
2435
|
-
* Interactive prompt delegate. When set, `evaluate()` calls it to get a
|
|
2436
|
-
* user decision synchronously (CLI REPL path). When cleared (TUI / WebUI),
|
|
2437
|
-
* `evaluate()` returns `confirm` so the caller can emit
|
|
2438
|
-
* `tool.confirm_needed` for the UI layer to handle.
|
|
2439
|
-
*
|
|
2440
|
-
* Mutable so the host can switch from CLI-prompt to event-driven
|
|
2441
|
-
* confirmation at runtime (e.g. when `--goal` forces TUI mode after
|
|
2442
|
-
* the agent was already constructed).
|
|
2443
|
-
*/
|
|
2444
2821
|
promptDelegate;
|
|
2445
|
-
/** Pre-compiled wildcard patterns — rebuilt on reload for O(1) lookup. */
|
|
2446
2822
|
wildcardEntries = [];
|
|
2447
|
-
/**
|
|
2448
|
-
* Evaluate-result cache. Keyed by `tool.name::subject::permissionFingerprint`
|
|
2449
|
-
* so repeated calls with the same tool+input skip namespace matching,
|
|
2450
|
-
* subject computation, and pattern matching (matchAny). The fingerprint
|
|
2451
|
-
* suffix covers the `Tool` fields `evaluate()` reads (`permission`,
|
|
2452
|
-
* `riskTier`, `mutating`, `capabilities`), so a tool redefined via
|
|
2453
|
-
* `ToolRegistry.wrap()` misses the cache instead of inheriting a stale
|
|
2454
|
-
* verdict.
|
|
2455
|
-
*
|
|
2456
|
-
* Cleared on any state change (reload, trust, deny, yolo toggle) because
|
|
2457
|
-
* the result depends on the full policy state. The write-tool smart-bypass
|
|
2458
|
-
* (step 7 in `evaluate()`) is not cached since `ctx.hasRead()` changes
|
|
2459
|
-
* dynamically within a session.
|
|
2460
|
-
*
|
|
2461
|
-
* P3 #17 (before-release.md): capped at 500 entries via LRU eviction. An
|
|
2462
|
-
* iteration that evaluates thousands of unique tool+subject combinations
|
|
2463
|
-
* (e.g. repeated `tool-help` or `bash` with different commands) can grow
|
|
2464
|
-
* the cache without bound. 500 is far more than any single iteration needs;
|
|
2465
|
-
* the LRU evicts the least-recently-used entries first.
|
|
2466
|
-
*/
|
|
2467
2823
|
_evalCache = new LruCache(500);
|
|
2468
2824
|
policyDiagnostics = [];
|
|
2469
2825
|
policyInvalid = false;
|
|
2470
|
-
/**
|
|
2471
|
-
* When true, YOLO also auto-approves clearly destructive shell commands.
|
|
2472
|
-
*
|
|
2473
|
-
* WS-008: `isClearlyDestructiveBashCommand` was exported and heavily tested
|
|
2474
|
-
* but called from zero production modules, so YOLO returned `auto` for
|
|
2475
|
-
* everything — including `rm -rf`, git history rewrites, and external
|
|
2476
|
-
* publishes. The `'yolo_destructive'` decision source, the getter/setter on
|
|
2477
|
-
* PermissionPolicy, and the TUI branch that reads it all existed; only the
|
|
2478
|
-
* gate itself was missing. Defaults to false: destructive commands still ask,
|
|
2479
|
-
* even in YOLO.
|
|
2480
|
-
*/
|
|
2481
2826
|
yoloDestructive;
|
|
2482
2827
|
constructor(opts) {
|
|
2483
2828
|
this.trustFile = opts.trustFile;
|
|
@@ -2485,30 +2830,13 @@ var DefaultPermissionPolicy = class {
|
|
|
2485
2830
|
this.yoloDestructive = opts.yoloDestructive ?? false;
|
|
2486
2831
|
this.promptDelegate = opts.promptDelegate;
|
|
2487
2832
|
}
|
|
2488
|
-
/** Allow YOLO to cover destructive shell commands too (off by default). */
|
|
2489
2833
|
setYoloDestructive(enabled) {
|
|
2490
2834
|
if (this.yoloDestructive !== enabled) this._evalCache.clear();
|
|
2491
2835
|
this.yoloDestructive = enabled;
|
|
2492
2836
|
}
|
|
2493
|
-
/** Whether YOLO currently extends to destructive shell commands. */
|
|
2494
2837
|
getYoloDestructive() {
|
|
2495
2838
|
return this.yoloDestructive;
|
|
2496
2839
|
}
|
|
2497
|
-
/**
|
|
2498
|
-
* True when this call is an FS_WRITE whose target lands inside WrongStack's
|
|
2499
|
-
* own state root.
|
|
2500
|
-
*
|
|
2501
|
-
* FS_WRITE tools disagree on where the target path lives: write/edit use
|
|
2502
|
-
* `path`, replace/format/git `files` (string OR array), patch `directory`,
|
|
2503
|
-
* design `out`, scaffold writes under `cwd`/`template`. Checking only
|
|
2504
|
-
* `path`/`file_path` let a state-root write merely switch tools, so every
|
|
2505
|
-
* path-bearing key is inspected.
|
|
2506
|
-
*
|
|
2507
|
-
* Shared by the YOLO carve-out and the eval-cache guard so the two cannot
|
|
2508
|
-
* drift: the carve-out scans all path keys while the cache key fingerprints
|
|
2509
|
-
* only the subject key, so the cache must consult the same predicate or a
|
|
2510
|
-
* secondary state-root key can be replayed a cached `auto`.
|
|
2511
|
-
*/
|
|
2512
2840
|
hasAgentStateWriteTarget(tool, input, ctx) {
|
|
2513
2841
|
if (!hasCapability(tool, ToolCapabilities.FS_WRITE)) return false;
|
|
2514
2842
|
for (const targetPath of fsWriteTargetPaths(input)) {
|
|
@@ -2518,11 +2846,6 @@ var DefaultPermissionPolicy = class {
|
|
|
2518
2846
|
}
|
|
2519
2847
|
return false;
|
|
2520
2848
|
}
|
|
2521
|
-
/**
|
|
2522
|
-
* True when YOLO is on but this specific call is a destructive shell command
|
|
2523
|
-
* the user has not opted to auto-approve. Shared by `evaluate()` and the
|
|
2524
|
-
* explain/trace path so the two cannot drift.
|
|
2525
|
-
*/
|
|
2526
2849
|
yoloBlockedAsDestructive(tool, input, ctx) {
|
|
2527
2850
|
if (!this.yolo || this.yoloDestructive) return false;
|
|
2528
2851
|
if (this.hasAgentStateWriteTarget(tool, input, ctx)) return true;
|
|
@@ -2532,25 +2855,16 @@ var DefaultPermissionPolicy = class {
|
|
|
2532
2855
|
if (!command) return false;
|
|
2533
2856
|
return isClearlyDestructiveBashCommand(command, ctx.projectRoot);
|
|
2534
2857
|
}
|
|
2535
|
-
/**
|
|
2536
|
-
* Replace (or clear) the interactive prompt delegate at runtime.
|
|
2537
|
-
* Used by the CLI to switch from inline prompts (REPL) to event-driven
|
|
2538
|
-
* confirmation (TUI) when the run mode is determined after the policy
|
|
2539
|
-
* was constructed (e.g. `--goal` auto-flipping to TUI).
|
|
2540
|
-
*/
|
|
2541
2858
|
setPromptDelegate(delegate) {
|
|
2542
2859
|
this.promptDelegate = delegate;
|
|
2543
2860
|
}
|
|
2544
|
-
/** Toggle YOLO (auto-approve) mode at runtime. */
|
|
2545
2861
|
setYolo(enabled) {
|
|
2546
2862
|
if (this.yolo !== enabled) this._evalCache.clear();
|
|
2547
2863
|
this.yolo = enabled;
|
|
2548
2864
|
}
|
|
2549
|
-
/** Check whether YOLO mode is currently active. */
|
|
2550
2865
|
getYolo() {
|
|
2551
2866
|
return this.yolo;
|
|
2552
2867
|
}
|
|
2553
|
-
/** Read-only diagnostics for policy inspector/editor surfaces. */
|
|
2554
2868
|
getPolicyDiagnostics() {
|
|
2555
2869
|
return this.policyDiagnostics.map((diagnostic) => ({ ...diagnostic }));
|
|
2556
2870
|
}
|
|
@@ -2819,7 +3133,6 @@ var DefaultPermissionPolicy = class {
|
|
|
2819
3133
|
throw err;
|
|
2820
3134
|
}
|
|
2821
3135
|
}
|
|
2822
|
-
/** Persist a deny rule — this tool+pattern pair is permanently blocked. */
|
|
2823
3136
|
async deny(rule) {
|
|
2824
3137
|
if (!this.loaded) await this.reload();
|
|
2825
3138
|
if (this.policyInvalid) {
|
|
@@ -2840,335 +3153,32 @@ var DefaultPermissionPolicy = class {
|
|
|
2840
3153
|
throw err;
|
|
2841
3154
|
}
|
|
2842
3155
|
}
|
|
2843
|
-
/** Block this tool+pattern for the rest of this session (no trust file). */
|
|
2844
3156
|
denyOnce(rule) {
|
|
2845
3157
|
this.sessionDenied.set(`${rule.tool}::${rule.pattern}`, true);
|
|
2846
3158
|
this._evalCache.clear();
|
|
2847
3159
|
}
|
|
2848
|
-
/** Auto-approve this tool+pattern once (no trust file). */
|
|
2849
3160
|
allowOnce(rule) {
|
|
2850
3161
|
this.sessionAllowed.set(`${rule.tool}::${rule.pattern}`, true);
|
|
2851
3162
|
this._evalCache.clear();
|
|
2852
3163
|
}
|
|
2853
|
-
/**
|
|
2854
|
-
* Side-effect-free permission evaluation trace. Mirrors `evaluate()`
|
|
2855
|
-
* step-by-step without prompting the user, writing to trust files, or
|
|
2856
|
-
* mutating session state. Returns the full ordered trace and winner.
|
|
2857
|
-
*/
|
|
2858
3164
|
async explain(tool, input, ctx) {
|
|
2859
3165
|
if (!this.loaded) await this.reload();
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
};
|
|
2876
|
-
}
|
|
2877
|
-
add("policy valid", false, "auto", "default", "trust policy loaded successfully");
|
|
2878
|
-
const namespaceEntry = this.findNamespaceEntry(tool.name);
|
|
2879
|
-
const entry = this.policy[tool.name] ?? namespaceEntry;
|
|
2880
|
-
const namespaceSource = namespaceEntry ? `wildcard entry matched (${Object.keys(this.policy).find((k) => k.includes("*") && matchGlob(k, tool.name))})` : "no namespace match";
|
|
2881
|
-
const cacheKey = `${tool.name}::${subject ?? tool.name}`;
|
|
2882
|
-
if (this.sessionDenied.has(cacheKey)) {
|
|
2883
|
-
add(
|
|
2884
|
-
"session soft deny",
|
|
2885
|
-
true,
|
|
2886
|
-
"deny",
|
|
2887
|
-
"deny",
|
|
2888
|
-
'user pressed "no" earlier in this session \u2014 blocked until reload'
|
|
2889
|
-
);
|
|
2890
|
-
winnerIndex = steps.length - 1;
|
|
2891
|
-
return {
|
|
2892
|
-
toolName: tool.name,
|
|
2893
|
-
subject,
|
|
2894
|
-
steps,
|
|
2895
|
-
winnerIndex,
|
|
2896
|
-
decision: {
|
|
2897
|
-
permission: "deny",
|
|
2898
|
-
source: "deny",
|
|
2899
|
-
reason: "session soft deny (user pressed no)"
|
|
2900
|
-
}
|
|
2901
|
-
};
|
|
2902
|
-
}
|
|
2903
|
-
add(
|
|
2904
|
-
"session soft deny",
|
|
2905
|
-
false,
|
|
2906
|
-
"deny",
|
|
2907
|
-
"deny",
|
|
2908
|
-
"no session-level soft deny for this tool+subject"
|
|
2909
|
-
);
|
|
2910
|
-
if (this.sessionAllowed.has(cacheKey)) {
|
|
2911
|
-
add(
|
|
2912
|
-
"session soft allow",
|
|
2913
|
-
true,
|
|
2914
|
-
"auto",
|
|
2915
|
-
"trust",
|
|
2916
|
-
'user pressed "yes" in current session \u2014 one-shot auto-approve (consumed)'
|
|
2917
|
-
);
|
|
2918
|
-
winnerIndex = steps.length - 1;
|
|
2919
|
-
return {
|
|
2920
|
-
toolName: tool.name,
|
|
2921
|
-
subject,
|
|
2922
|
-
steps,
|
|
2923
|
-
winnerIndex,
|
|
2924
|
-
decision: {
|
|
2925
|
-
permission: "auto",
|
|
2926
|
-
source: "trust",
|
|
2927
|
-
reason: "session one-shot allow (user pressed yes)"
|
|
2928
|
-
}
|
|
2929
|
-
};
|
|
2930
|
-
}
|
|
2931
|
-
add(
|
|
2932
|
-
"session soft allow",
|
|
2933
|
-
false,
|
|
2934
|
-
"auto",
|
|
2935
|
-
"trust",
|
|
2936
|
-
"no session-level one-shot allow for this tool+subject"
|
|
2937
|
-
);
|
|
2938
|
-
if (entry?.deny && subject && matchesTrust(entry.deny, subject)) {
|
|
2939
|
-
add(
|
|
2940
|
-
"trust deny",
|
|
2941
|
-
true,
|
|
2942
|
-
"deny",
|
|
2943
|
-
"deny",
|
|
2944
|
-
`subject "${subject}" matched a deny pattern in trust file`
|
|
2945
|
-
);
|
|
2946
|
-
winnerIndex = steps.length - 1;
|
|
2947
|
-
return {
|
|
2948
|
-
toolName: tool.name,
|
|
2949
|
-
subject,
|
|
2950
|
-
steps,
|
|
2951
|
-
winnerIndex,
|
|
2952
|
-
decision: { permission: "deny", source: "deny", reason: "matched deny pattern" }
|
|
2953
|
-
};
|
|
2954
|
-
}
|
|
2955
|
-
add(
|
|
2956
|
-
"trust deny",
|
|
2957
|
-
false,
|
|
2958
|
-
"deny",
|
|
2959
|
-
"deny",
|
|
2960
|
-
`no deny pattern matched (namespace: ${namespaceSource})`
|
|
2961
|
-
);
|
|
2962
|
-
if (tool.permission === "deny") {
|
|
2963
|
-
add(
|
|
2964
|
-
"tool default deny",
|
|
2965
|
-
true,
|
|
2966
|
-
"deny",
|
|
2967
|
-
"default",
|
|
2968
|
-
`tool "${tool.name}" has permission: deny by default`
|
|
2969
|
-
);
|
|
2970
|
-
winnerIndex = steps.length - 1;
|
|
2971
|
-
return {
|
|
2972
|
-
toolName: tool.name,
|
|
2973
|
-
subject,
|
|
2974
|
-
steps,
|
|
2975
|
-
winnerIndex,
|
|
2976
|
-
decision: { permission: "deny", source: "default", reason: "tool default deny" }
|
|
2977
|
-
};
|
|
2978
|
-
}
|
|
2979
|
-
add(
|
|
2980
|
-
"tool default deny",
|
|
2981
|
-
false,
|
|
2982
|
-
"deny",
|
|
2983
|
-
"default",
|
|
2984
|
-
`tool "${tool.name}" has permission: "${tool.permission}"`
|
|
2985
|
-
);
|
|
2986
|
-
if (entry?.allow && subject && matchesTrust(entry.allow, subject)) {
|
|
2987
|
-
add(
|
|
2988
|
-
"trust allow",
|
|
2989
|
-
true,
|
|
2990
|
-
"auto",
|
|
2991
|
-
"trust",
|
|
2992
|
-
`subject "${subject}" matched an allow pattern in trust file`
|
|
2993
|
-
);
|
|
2994
|
-
winnerIndex = steps.length - 1;
|
|
2995
|
-
return {
|
|
2996
|
-
toolName: tool.name,
|
|
2997
|
-
subject,
|
|
2998
|
-
steps,
|
|
2999
|
-
winnerIndex,
|
|
3000
|
-
decision: { permission: "auto", source: "trust", reason: "matched allow pattern" }
|
|
3001
|
-
};
|
|
3002
|
-
}
|
|
3003
|
-
add(
|
|
3004
|
-
"trust allow",
|
|
3005
|
-
false,
|
|
3006
|
-
"auto",
|
|
3007
|
-
"trust",
|
|
3008
|
-
`no allow pattern matched (namespace: ${namespaceSource})`
|
|
3009
|
-
);
|
|
3010
|
-
if (entry?.auto) {
|
|
3011
|
-
add("trust auto", true, "auto", "trust", `trust file has auto: true for "${tool.name}"`);
|
|
3012
|
-
winnerIndex = steps.length - 1;
|
|
3013
|
-
return {
|
|
3014
|
-
toolName: tool.name,
|
|
3015
|
-
subject,
|
|
3016
|
-
steps,
|
|
3017
|
-
winnerIndex,
|
|
3018
|
-
decision: { permission: "auto", source: "trust", reason: "trust auto" }
|
|
3019
|
-
};
|
|
3020
|
-
}
|
|
3021
|
-
add("trust auto", false, "auto", "trust", `no auto flag for "${tool.name}" in trust file`);
|
|
3022
|
-
if (!this.yolo && this.isSensitiveReadCall(tool, input)) {
|
|
3023
|
-
const hasDelegate2 = this.promptDelegate !== void 0;
|
|
3024
|
-
add(
|
|
3025
|
-
"sensitive read",
|
|
3026
|
-
true,
|
|
3027
|
-
hasDelegate2 ? "confirm" : "confirm",
|
|
3028
|
-
"default",
|
|
3029
|
-
hasDelegate2 ? "sensitive file read detected \u2014 would prompt user for approval" : "sensitive file read detected \u2014 returns confirm (no prompt delegate)"
|
|
3030
|
-
);
|
|
3031
|
-
winnerIndex = steps.length - 1;
|
|
3032
|
-
return {
|
|
3033
|
-
toolName: tool.name,
|
|
3034
|
-
subject,
|
|
3035
|
-
steps,
|
|
3036
|
-
winnerIndex,
|
|
3037
|
-
decision: {
|
|
3038
|
-
permission: "confirm",
|
|
3039
|
-
source: "default",
|
|
3040
|
-
riskTier: "standard",
|
|
3041
|
-
reason: "sensitive file read needs explicit approval"
|
|
3042
|
-
}
|
|
3043
|
-
};
|
|
3044
|
-
}
|
|
3045
|
-
add(
|
|
3046
|
-
"sensitive read",
|
|
3047
|
-
false,
|
|
3048
|
-
"confirm",
|
|
3049
|
-
"default",
|
|
3050
|
-
"not a sensitive read call (or YOLO bypasses this check)"
|
|
3051
|
-
);
|
|
3052
|
-
if (this.yolo) {
|
|
3053
|
-
if (this.yoloBlockedAsDestructive(tool, input, ctx)) {
|
|
3054
|
-
add(
|
|
3055
|
-
"yolo destructive gate",
|
|
3056
|
-
true,
|
|
3057
|
-
"confirm",
|
|
3058
|
-
"yolo_destructive",
|
|
3059
|
-
"YOLO is active but this is a clearly destructive command \u2014 still asking"
|
|
3060
|
-
);
|
|
3061
|
-
winnerIndex = steps.length - 1;
|
|
3062
|
-
return {
|
|
3063
|
-
toolName: tool.name,
|
|
3064
|
-
subject,
|
|
3065
|
-
steps,
|
|
3066
|
-
winnerIndex,
|
|
3067
|
-
decision: {
|
|
3068
|
-
permission: "confirm",
|
|
3069
|
-
source: "yolo_destructive",
|
|
3070
|
-
riskTier: "destructive",
|
|
3071
|
-
reason: "destructive command needs explicit approval even in YOLO mode"
|
|
3072
|
-
}
|
|
3073
|
-
};
|
|
3074
|
-
}
|
|
3075
|
-
add(
|
|
3076
|
-
"yolo",
|
|
3077
|
-
true,
|
|
3078
|
-
"auto",
|
|
3079
|
-
"yolo",
|
|
3080
|
-
"YOLO mode is active \u2014 auto-approving every non-denied call"
|
|
3081
|
-
);
|
|
3082
|
-
winnerIndex = steps.length - 1;
|
|
3083
|
-
return {
|
|
3084
|
-
toolName: tool.name,
|
|
3085
|
-
subject,
|
|
3086
|
-
steps,
|
|
3087
|
-
winnerIndex,
|
|
3088
|
-
decision: { permission: "auto", source: "yolo" }
|
|
3089
|
-
};
|
|
3090
|
-
}
|
|
3091
|
-
add("yolo", false, "auto", "yolo", "YOLO mode is not active");
|
|
3092
|
-
if (tool.name === "write" && subject) {
|
|
3093
|
-
const isAgentState = isInsideAgentStateRoot(subject);
|
|
3094
|
-
const hasRead = ctx.hasRead(subject) && !isAgentState;
|
|
3095
|
-
add(
|
|
3096
|
-
"write smart bypass",
|
|
3097
|
-
hasRead,
|
|
3098
|
-
"auto",
|
|
3099
|
-
"context",
|
|
3100
|
-
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`
|
|
3101
|
-
);
|
|
3102
|
-
if (hasRead) {
|
|
3103
|
-
winnerIndex = steps.length - 1;
|
|
3104
|
-
return {
|
|
3105
|
-
toolName: tool.name,
|
|
3106
|
-
subject,
|
|
3107
|
-
steps,
|
|
3108
|
-
winnerIndex,
|
|
3109
|
-
decision: {
|
|
3110
|
-
permission: "auto",
|
|
3111
|
-
source: "context",
|
|
3112
|
-
reason: "file already read in this session"
|
|
3113
|
-
}
|
|
3114
|
-
};
|
|
3115
|
-
}
|
|
3116
|
-
} else {
|
|
3117
|
-
add(
|
|
3118
|
-
"write smart bypass",
|
|
3119
|
-
false,
|
|
3120
|
-
"auto",
|
|
3121
|
-
"context",
|
|
3122
|
-
'tool is not "write" or has no subject \u2014 bypass does not apply'
|
|
3123
|
-
);
|
|
3124
|
-
}
|
|
3125
|
-
const hasWriteCap = hasCapability(tool, ToolCapabilities.FS_WRITE);
|
|
3126
|
-
const hasShellCap = hasCapability(tool, [
|
|
3127
|
-
ToolCapabilities.SHELL_ARBITRARY,
|
|
3128
|
-
ToolCapabilities.SHELL_RESTRICTED,
|
|
3129
|
-
ToolCapabilities.SHELL_EXEC
|
|
3130
|
-
]);
|
|
3131
|
-
const hasInstallCap = hasCapability(tool, ToolCapabilities.PACKAGE_INSTALL);
|
|
3132
|
-
const hasConfigCap = hasCapability(tool, ToolCapabilities.CONFIG_MUTATE);
|
|
3133
|
-
const hasSubagentCap = hasCapability(tool, ToolCapabilities.SUBAGENT_SPAWN);
|
|
3134
|
-
const isMutating = tool.mutating || hasWriteCap || hasShellCap || hasInstallCap || hasConfigCap || hasSubagentCap;
|
|
3135
|
-
if (tool.permission === "auto" && !isMutating) {
|
|
3136
|
-
add(
|
|
3137
|
-
"safe default auto",
|
|
3138
|
-
true,
|
|
3139
|
-
"auto",
|
|
3140
|
-
"default",
|
|
3141
|
-
`tool "${tool.name}" has auto permission and is not mutating \u2014 safe to auto-approve`
|
|
3142
|
-
);
|
|
3143
|
-
winnerIndex = steps.length - 1;
|
|
3144
|
-
return {
|
|
3145
|
-
toolName: tool.name,
|
|
3146
|
-
subject,
|
|
3147
|
-
steps,
|
|
3148
|
-
winnerIndex,
|
|
3149
|
-
decision: { permission: "auto", source: "default" }
|
|
3150
|
-
};
|
|
3151
|
-
}
|
|
3152
|
-
add(
|
|
3153
|
-
"mutating default confirm",
|
|
3154
|
-
true,
|
|
3155
|
-
"confirm",
|
|
3156
|
-
"default",
|
|
3157
|
-
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
|
|
3158
3181
|
);
|
|
3159
|
-
winnerIndex = steps.length - 1;
|
|
3160
|
-
const hasDelegate = this.promptDelegate !== void 0;
|
|
3161
|
-
return {
|
|
3162
|
-
toolName: tool.name,
|
|
3163
|
-
subject,
|
|
3164
|
-
steps,
|
|
3165
|
-
winnerIndex,
|
|
3166
|
-
decision: {
|
|
3167
|
-
permission: "confirm",
|
|
3168
|
-
source: "default",
|
|
3169
|
-
...hasDelegate ? { reason: "would prompt user via delegate" } : {}
|
|
3170
|
-
}
|
|
3171
|
-
};
|
|
3172
3182
|
}
|
|
3173
3183
|
findNamespaceEntry(toolName) {
|
|
3174
3184
|
for (const { pattern, value } of this.wildcardEntries) {
|