@bubblebrain-ai/bubble 0.0.58 → 0.0.60
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/README.md +56 -6
- package/dist/agent/child-runner.js +5 -1
- package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
- package/dist/agent/internal-reminder-sanitizer.js +7 -1
- package/dist/agent/profiles.js +4 -4
- package/dist/agent/routing-catalog.d.ts +19 -1
- package/dist/agent/routing-catalog.js +32 -13
- package/dist/agent/subagent/runtime.d.ts +28 -0
- package/dist/agent/subagent/runtime.js +34 -30
- package/dist/agent/subagent-control.d.ts +7 -0
- package/dist/agent/subagent-control.js +2 -0
- package/dist/agent/subagent-lifecycle-reminder.js +2 -1
- package/dist/agent/subagent-wait-guidance.d.ts +2 -0
- package/dist/agent/subagent-wait-guidance.js +8 -0
- package/dist/agent/subtask-policy.js +4 -4
- package/dist/agent.d.ts +20 -11
- package/dist/agent.js +200 -117
- package/dist/anthropic-thinking.d.ts +4 -0
- package/dist/anthropic-thinking.js +11 -0
- package/dist/approval/controller.d.ts +20 -6
- package/dist/approval/controller.js +67 -8
- package/dist/approval/protected-paths.d.ts +1 -0
- package/dist/approval/protected-paths.js +35 -0
- package/dist/approval/session-cache.d.ts +12 -26
- package/dist/approval/session-cache.js +20 -50
- package/dist/approval/types.d.ts +27 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +25 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/context/checkpoint.d.ts +18 -0
- package/dist/context/checkpoint.js +117 -0
- package/dist/context/compact-llm.js +7 -0
- package/dist/context/compact.d.ts +8 -0
- package/dist/context/compact.js +40 -7
- package/dist/context/llm-compactor.d.ts +36 -2
- package/dist/context/llm-compactor.js +227 -44
- package/dist/context/session-write-lock.d.ts +20 -0
- package/dist/context/session-write-lock.js +162 -0
- package/dist/context/tool-images.d.ts +3 -0
- package/dist/context/tool-images.js +35 -0
- package/dist/context/usage.js +22 -5
- package/dist/feishu/agent-host/run-driver.js +56 -9
- package/dist/lsp/service.d.ts +13 -1
- package/dist/lsp/service.js +163 -43
- package/dist/main.js +82 -16
- package/dist/mcp/config.d.ts +4 -0
- package/dist/mcp/config.js +17 -0
- package/dist/mcp/manager.d.ts +13 -1
- package/dist/mcp/manager.js +32 -1
- package/dist/mcp/transports.js +11 -0
- package/dist/memory/db.d.ts +1 -0
- package/dist/memory/db.js +20 -4
- package/dist/memory/phase1.js +122 -16
- package/dist/memory/prompts.js +3 -0
- package/dist/model-catalog.js +19 -3
- package/dist/model-config.d.ts +1 -0
- package/dist/model-config.js +5 -5
- package/dist/model-pricing.js +26 -9
- package/dist/network/provider-transport-log.d.ts +12 -0
- package/dist/network/provider-transport-log.js +32 -0
- package/dist/network/provider-transport.js +6 -1
- package/dist/network/retry.d.ts +1 -1
- package/dist/network/retry.js +1 -1
- package/dist/oauth/grok.d.ts +1 -0
- package/dist/oauth/grok.js +20 -16
- package/dist/oauth/openai-codex.d.ts +1 -0
- package/dist/oauth/openai-codex.js +29 -25
- package/dist/oauth/refresh-control.d.ts +12 -0
- package/dist/oauth/refresh-control.js +42 -0
- package/dist/oauth/storage.d.ts +55 -1
- package/dist/oauth/storage.js +281 -16
- package/dist/permissions/rule.d.ts +6 -3
- package/dist/permissions/rule.js +71 -12
- package/dist/permissions/settings.d.ts +28 -1
- package/dist/permissions/settings.js +93 -3
- package/dist/permissions/shell-command.d.ts +36 -0
- package/dist/permissions/shell-command.js +226 -0
- package/dist/permissions/trust-prompt.d.ts +10 -0
- package/dist/permissions/trust-prompt.js +34 -0
- package/dist/permissions/trust.d.ts +26 -0
- package/dist/permissions/trust.js +113 -0
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +24 -7
- package/dist/prompt/environment.js +2 -2
- package/dist/prompt/reminders.js +1 -1
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-anthropic.d.ts +6 -2
- package/dist/provider-anthropic.js +98 -18
- package/dist/provider-error-record.d.ts +9 -0
- package/dist/provider-error-record.js +42 -14
- package/dist/provider-grok.js +5 -1
- package/dist/provider-openai-codex.d.ts +8 -0
- package/dist/provider-openai-codex.js +107 -24
- package/dist/provider-registry.d.ts +45 -2
- package/dist/provider-registry.js +300 -88
- package/dist/provider-transform.js +8 -0
- package/dist/provider-zen-errors.d.ts +9 -0
- package/dist/provider-zen-errors.js +36 -0
- package/dist/provider.js +36 -5
- package/dist/sdk/index.d.ts +69 -8
- package/dist/sdk/index.js +252 -39
- package/dist/sdk/replay-event-log.d.ts +7 -0
- package/dist/sdk/replay-event-log.js +66 -16
- package/dist/session-context-fence.d.ts +16 -0
- package/dist/session-context-fence.js +26 -0
- package/dist/session-log.d.ts +11 -2
- package/dist/session-log.js +187 -39
- package/dist/session-types.d.ts +5 -1
- package/dist/session.d.ts +39 -7
- package/dist/session.js +341 -93
- package/dist/skills/format.d.ts +31 -1
- package/dist/skills/format.js +45 -24
- package/dist/slash-commands/commands.d.ts +3 -0
- package/dist/slash-commands/commands.js +29 -14
- package/dist/system-prompt.d.ts +3 -1
- package/dist/tasks/manager.d.ts +7 -0
- package/dist/tasks/manager.js +97 -18
- package/dist/tools/agent-lifecycle.js +11 -7
- package/dist/tools/child-tools.d.ts +1 -1
- package/dist/tools/child-tools.js +4 -4
- package/dist/tools/edit-apply.d.ts +0 -3
- package/dist/tools/edit-apply.js +45 -249
- package/dist/tools/edit.js +3 -3
- package/dist/tools/exit-plan-mode.d.ts +5 -0
- package/dist/tools/exit-plan-mode.js +3 -2
- package/dist/tools/file-state.d.ts +4 -0
- package/dist/tools/file-state.js +12 -3
- package/dist/tools/grep.js +20 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/ls.d.ts +3 -0
- package/dist/tools/ls.js +85 -0
- package/dist/tools/path-utils.d.ts +6 -0
- package/dist/tools/path-utils.js +38 -1
- package/dist/tools/sensitive-paths.d.ts +8 -0
- package/dist/tools/sensitive-paths.js +85 -3
- package/dist/tools/skill.d.ts +2 -0
- package/dist/tools/skill.js +34 -2
- package/dist/tools/task-tools.js +11 -7
- package/dist/tui/app.js +22 -12
- package/dist/tui/components/approval-dialog.d.ts +5 -5
- package/dist/tui/components/approval-dialog.js +16 -51
- package/dist/tui/components/skills-panel.d.ts +4 -0
- package/dist/tui/components/skills-panel.js +15 -1
- package/dist/tui/components/streaming-message.js +7 -2
- package/dist/tui/components/tasks-pane.d.ts +38 -0
- package/dist/tui/components/tasks-pane.js +182 -32
- package/dist/tui/components/transcript.d.ts +11 -4
- package/dist/tui/components/transcript.js +158 -71
- package/dist/tui/composer-autocomplete.d.ts +17 -2
- package/dist/tui/composer-autocomplete.js +79 -2
- package/dist/tui/controller/controller.d.ts +9 -4
- package/dist/tui/controller/controller.js +87 -95
- package/dist/tui/controller/goal-runtime-controller.js +1 -1
- package/dist/tui/controller/session-transition.js +1 -1
- package/dist/tui/model/subagent-view.d.ts +11 -19
- package/dist/tui/model/subagent-view.js +56 -85
- package/dist/tui/model/task-lifecycle.d.ts +57 -0
- package/dist/tui/model/task-lifecycle.js +124 -0
- package/dist/tui/model/trace-groups.js +10 -9
- package/dist/tui/trace-groups.js +10 -9
- package/dist/types.d.ts +20 -0
- package/dist/variant/variant-resolver.js +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
- package/package.json +11 -3
- package/dist/prompt/skills.d.ts +0 -2
- package/dist/prompt/skills.js +0 -4
- package/dist/tools/glob.d.ts +0 -5
- package/dist/tools/glob.js +0 -199
package/dist/permissions/rule.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { isAbsolute, resolve } from "node:path";
|
|
3
3
|
import picomatch from "picomatch";
|
|
4
|
+
import { analyzeShellCommand, effectiveCommandTokens, shellTokens } from "./shell-command.js";
|
|
4
5
|
const KNOWN_TOOLS = new Set([
|
|
5
6
|
"Bash",
|
|
6
7
|
"Read",
|
|
@@ -80,11 +81,14 @@ export function parseRules(inputs) {
|
|
|
80
81
|
/**
|
|
81
82
|
* Does `rule` match `query`?
|
|
82
83
|
*
|
|
83
|
-
* Tool name must match (or rule.tool is "*"
|
|
84
|
-
*
|
|
84
|
+
* Tool name must match (or rule.tool is "*"; `mcp__server` covers every
|
|
85
|
+
* `mcp__server__tool`). If the rule has no pattern, it matches any use of that
|
|
86
|
+
* tool. Otherwise the tool-specific matcher decides. `list` matters for Bash:
|
|
87
|
+
* an allow rule must cover every command a compound command runs, while a
|
|
88
|
+
* deny rule fires if it covers any of them.
|
|
85
89
|
*/
|
|
86
|
-
export function matchRule(rule, query) {
|
|
87
|
-
if (rule.tool !== "*" && rule.tool !== query.tool) {
|
|
90
|
+
export function matchRule(rule, query, list = "allow") {
|
|
91
|
+
if (rule.tool !== "*" && rule.tool !== query.tool && !isMcpServerRule(rule, query)) {
|
|
88
92
|
return false;
|
|
89
93
|
}
|
|
90
94
|
if (rule.pattern === undefined) {
|
|
@@ -95,7 +99,9 @@ export function matchRule(rule, query) {
|
|
|
95
99
|
case "Bash":
|
|
96
100
|
if (!("command" in query) || typeof query.command !== "string")
|
|
97
101
|
return false;
|
|
98
|
-
return
|
|
102
|
+
return list === "deny"
|
|
103
|
+
? bashDenyMatches(rule.pattern, query.command)
|
|
104
|
+
: bashAllowMatches(rule.pattern, query.command);
|
|
99
105
|
case "Read":
|
|
100
106
|
case "Write":
|
|
101
107
|
case "Edit":
|
|
@@ -121,30 +127,83 @@ export function matchRule(rule, query) {
|
|
|
121
127
|
*/
|
|
122
128
|
export function checkPermission(rules, query) {
|
|
123
129
|
for (const rule of rules.deny) {
|
|
124
|
-
if (matchRule(rule, query)) {
|
|
130
|
+
if (matchRule(rule, query, "deny")) {
|
|
125
131
|
return { decision: "deny", rule };
|
|
126
132
|
}
|
|
127
133
|
}
|
|
134
|
+
if (query.tool === "Bash" && "command" in query && typeof query.command === "string") {
|
|
135
|
+
return checkBashAllow(rules.allow, query.command);
|
|
136
|
+
}
|
|
128
137
|
for (const rule of rules.allow) {
|
|
129
|
-
if (matchRule(rule, query)) {
|
|
138
|
+
if (matchRule(rule, query, "allow")) {
|
|
130
139
|
return { decision: "allow", rule };
|
|
131
140
|
}
|
|
132
141
|
}
|
|
133
142
|
return { decision: "ask" };
|
|
134
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* A compound command is allowed when every simple command in it is covered
|
|
146
|
+
* by some allow rule (different rules may cover different parts), and it has
|
|
147
|
+
* nothing opaque — unless a rule grants any command (`Bash`, `*`, `Bash(:*)`).
|
|
148
|
+
*/
|
|
149
|
+
function checkBashAllow(allowRules, command) {
|
|
150
|
+
const bashRules = allowRules.filter((rule) => rule.tool === "Bash" || rule.tool === "*");
|
|
151
|
+
const anyCommand = bashRules.find((rule) => rule.pattern === undefined || tokenize(rule.pattern).join(" ") === ":*");
|
|
152
|
+
if (anyCommand)
|
|
153
|
+
return { decision: "allow", rule: anyCommand };
|
|
154
|
+
const analysis = analyzeShellCommand(command);
|
|
155
|
+
if (analysis.opaque || analysis.segments.length === 0)
|
|
156
|
+
return { decision: "ask" };
|
|
157
|
+
let first;
|
|
158
|
+
for (const segment of analysis.segments) {
|
|
159
|
+
const tokens = shellTokens(segment);
|
|
160
|
+
const rule = bashRules.find((candidate) => matchBash(candidate.pattern, tokens));
|
|
161
|
+
if (!rule)
|
|
162
|
+
return { decision: "ask" };
|
|
163
|
+
first ??= rule;
|
|
164
|
+
}
|
|
165
|
+
return first ? { decision: "allow", rule: first } : { decision: "ask" };
|
|
166
|
+
}
|
|
135
167
|
// --- per-tool matchers ---------------------------------------------------
|
|
168
|
+
/** `mcp__github` (no pattern) covers every tool of that MCP server. */
|
|
169
|
+
function isMcpServerRule(rule, query) {
|
|
170
|
+
return rule.pattern === undefined
|
|
171
|
+
&& rule.tool.startsWith("mcp__")
|
|
172
|
+
&& query.tool.startsWith(`${rule.tool}__`);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Allow: every simple command the line runs must match, and nothing opaque
|
|
176
|
+
* (substitution, file redirection, heredoc) may ride along.
|
|
177
|
+
*/
|
|
178
|
+
function bashAllowMatches(pattern, command) {
|
|
179
|
+
// `Bash(:*)` is an explicit "any command" grant.
|
|
180
|
+
if (tokenize(pattern).join(" ") === ":*")
|
|
181
|
+
return true;
|
|
182
|
+
const analysis = analyzeShellCommand(command);
|
|
183
|
+
if (analysis.opaque || analysis.segments.length === 0)
|
|
184
|
+
return false;
|
|
185
|
+
return analysis.segments.every((segment) => matchBash(pattern, shellTokens(segment)));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Deny: any simple command matching — as written, or with leading
|
|
189
|
+
* assignments/wrappers (`sudo`, `env`, ...) stripped — blocks the line.
|
|
190
|
+
*/
|
|
191
|
+
function bashDenyMatches(pattern, command) {
|
|
192
|
+
const analysis = analyzeShellCommand(command);
|
|
193
|
+
if (analysis.segments.length === 0)
|
|
194
|
+
return matchBash(pattern, shellTokens(command));
|
|
195
|
+
return analysis.segments.some((segment) => matchBash(pattern, shellTokens(segment)) || matchBash(pattern, effectiveCommandTokens(segment)));
|
|
196
|
+
}
|
|
136
197
|
/**
|
|
137
198
|
* Bash pattern matching.
|
|
138
199
|
*
|
|
139
200
|
* - `git status` → command tokens equal ["git","status"] exactly
|
|
140
201
|
* - `git status:*` → command tokens start with ["git","status"]
|
|
141
202
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* literally. v1 does not try to parse shell grammar.
|
|
203
|
+
* Matches one simple command's tokens; bashAllowMatches / bashDenyMatches
|
|
204
|
+
* split compound commands first (see shell-command.ts).
|
|
145
205
|
*/
|
|
146
|
-
function matchBash(pattern,
|
|
147
|
-
const cmdTokens = tokenize(command);
|
|
206
|
+
function matchBash(pattern, cmdTokens) {
|
|
148
207
|
let ruleTokens = tokenize(pattern);
|
|
149
208
|
let prefixMatch = false;
|
|
150
209
|
if (ruleTokens.length > 0) {
|
|
@@ -11,12 +11,19 @@
|
|
|
11
11
|
* `allow` / `deny` arrays are concatenated across scopes (with the rule text
|
|
12
12
|
* itself carrying provenance via `PermissionRule.source`).
|
|
13
13
|
*
|
|
14
|
+
* Allow rules and LSP server definitions from the project and local scopes
|
|
15
|
+
* live inside the repository, so they only take effect once the user trusts
|
|
16
|
+
* this folder (asked at startup, see trust.ts); until then they are reported in
|
|
17
|
+
* `untrusted` (MCP servers from those files are gated the same way by
|
|
18
|
+
* loadMcpConfig).
|
|
19
|
+
*
|
|
14
20
|
* Parse errors do not fail the load; they collect into `diagnostics` so callers
|
|
15
21
|
* can surface them in /permissions or on startup without taking the agent down.
|
|
16
22
|
*/
|
|
17
23
|
import type { PermissionMode } from "../types.js";
|
|
18
24
|
import { type LspConfig } from "../lsp/config.js";
|
|
19
|
-
import type
|
|
25
|
+
import { type RepoCapabilities } from "./trust.js";
|
|
26
|
+
import type { PermissionRule, PermissionRuleSet } from "./types.js";
|
|
20
27
|
export type SettingsScope = "user" | "project" | "local";
|
|
21
28
|
export type RuleList = "allow" | "deny";
|
|
22
29
|
export interface RawSettings {
|
|
@@ -36,6 +43,10 @@ export interface MergedSettings {
|
|
|
36
43
|
defaultMode?: PermissionMode;
|
|
37
44
|
lsp?: LspConfig;
|
|
38
45
|
ruleSet: PermissionRuleSet;
|
|
46
|
+
/** Project/local allow rules ignored until the user trusts them. */
|
|
47
|
+
untrustedAllow: PermissionRule[];
|
|
48
|
+
/** Everything in the repository settings that waits for trust (allow rules, MCP servers, LSP servers). */
|
|
49
|
+
untrusted: RepoCapabilities;
|
|
39
50
|
diagnostics: SettingsDiagnostic[];
|
|
40
51
|
}
|
|
41
52
|
export interface SettingsManagerOptions {
|
|
@@ -46,9 +57,25 @@ export declare class SettingsManager {
|
|
|
46
57
|
private readonly paths;
|
|
47
58
|
private raw;
|
|
48
59
|
private fileDiagnostics;
|
|
60
|
+
private readonly cwd;
|
|
61
|
+
private readonly bubbleHome?;
|
|
62
|
+
/** Cached per load: whether the repository's capability settings are trusted. */
|
|
63
|
+
private repoTrusted;
|
|
49
64
|
constructor(cwd: string, options?: SettingsManagerOptions);
|
|
50
65
|
/** Re-read all three files from disk. */
|
|
51
66
|
reload(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Trust the repository's current capability settings (allow rules, MCP
|
|
69
|
+
* servers, LSP servers from the project and local files).
|
|
70
|
+
*/
|
|
71
|
+
trustRepoConfig(): void;
|
|
72
|
+
private repoRaw;
|
|
73
|
+
/**
|
|
74
|
+
* Bubble's own /permissions edits keep an existing trust: rules the user
|
|
75
|
+
* adds through Bubble are their decision. An untrusted rule set stays
|
|
76
|
+
* untrusted — editing it must not launder rules the user never reviewed.
|
|
77
|
+
*/
|
|
78
|
+
private keepRepoTrustAfterWrite;
|
|
52
79
|
getPath(scope: SettingsScope): string;
|
|
53
80
|
/** Merged view for runtime consumption. Does not hit disk — call `reload()` first if stale. */
|
|
54
81
|
getMerged(): MergedSettings;
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* `allow` / `deny` arrays are concatenated across scopes (with the rule text
|
|
12
12
|
* itself carrying provenance via `PermissionRule.source`).
|
|
13
13
|
*
|
|
14
|
+
* Allow rules and LSP server definitions from the project and local scopes
|
|
15
|
+
* live inside the repository, so they only take effect once the user trusts
|
|
16
|
+
* this folder (asked at startup, see trust.ts); until then they are reported in
|
|
17
|
+
* `untrusted` (MCP servers from those files are gated the same way by
|
|
18
|
+
* loadMcpConfig).
|
|
19
|
+
*
|
|
14
20
|
* Parse errors do not fail the load; they collect into `diagnostics` so callers
|
|
15
21
|
* can surface them in /permissions or on startup without taking the agent down.
|
|
16
22
|
*/
|
|
@@ -19,6 +25,7 @@ import { dirname, join } from "node:path";
|
|
|
19
25
|
import { getBubbleHome } from "../bubble-home.js";
|
|
20
26
|
import { normalizeLspConfig } from "../lsp/config.js";
|
|
21
27
|
import { parseRules } from "./rule.js";
|
|
28
|
+
import { isRepoConfigTrusted, repoCapabilities, trustRepoConfig } from "./trust.js";
|
|
22
29
|
const KNOWN_MODES = new Set([
|
|
23
30
|
"default",
|
|
24
31
|
"plan",
|
|
@@ -32,8 +39,14 @@ export class SettingsManager {
|
|
|
32
39
|
local: null,
|
|
33
40
|
};
|
|
34
41
|
fileDiagnostics = [];
|
|
42
|
+
cwd;
|
|
43
|
+
bubbleHome;
|
|
44
|
+
/** Cached per load: whether the repository's capability settings are trusted. */
|
|
45
|
+
repoTrusted = false;
|
|
35
46
|
constructor(cwd, options = {}) {
|
|
36
47
|
const bubbleHome = options.bubbleHome ?? getBubbleHome();
|
|
48
|
+
this.cwd = cwd;
|
|
49
|
+
this.bubbleHome = bubbleHome;
|
|
37
50
|
this.paths = {
|
|
38
51
|
user: join(bubbleHome, "settings.json"),
|
|
39
52
|
project: join(cwd, ".bubble", "settings.json"),
|
|
@@ -47,6 +60,31 @@ export class SettingsManager {
|
|
|
47
60
|
for (const scope of ["user", "project", "local"]) {
|
|
48
61
|
this.raw[scope] = this.readFile(scope);
|
|
49
62
|
}
|
|
63
|
+
this.repoTrusted = isRepoConfigTrusted(this.cwd, this.repoRaw(), { bubbleHome: this.bubbleHome });
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Trust the repository's current capability settings (allow rules, MCP
|
|
67
|
+
* servers, LSP servers from the project and local files).
|
|
68
|
+
*/
|
|
69
|
+
trustRepoConfig() {
|
|
70
|
+
trustRepoConfig(this.cwd, this.repoRaw(), { bubbleHome: this.bubbleHome });
|
|
71
|
+
this.repoTrusted = true;
|
|
72
|
+
}
|
|
73
|
+
repoRaw() {
|
|
74
|
+
return { project: this.raw.project, local: this.raw.local };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Bubble's own /permissions edits keep an existing trust: rules the user
|
|
78
|
+
* adds through Bubble are their decision. An untrusted rule set stays
|
|
79
|
+
* untrusted — editing it must not launder rules the user never reviewed.
|
|
80
|
+
*/
|
|
81
|
+
keepRepoTrustAfterWrite(scope, wasTrusted) {
|
|
82
|
+
if (scope === "user")
|
|
83
|
+
return;
|
|
84
|
+
if (wasTrusted)
|
|
85
|
+
this.trustRepoConfig();
|
|
86
|
+
else
|
|
87
|
+
this.repoTrusted = isRepoConfigTrusted(this.cwd, this.repoRaw(), { bubbleHome: this.bubbleHome });
|
|
50
88
|
}
|
|
51
89
|
getPath(scope) {
|
|
52
90
|
return this.paths[scope];
|
|
@@ -58,12 +96,21 @@ export class SettingsManager {
|
|
|
58
96
|
let lsp;
|
|
59
97
|
const allow = [];
|
|
60
98
|
const deny = [];
|
|
99
|
+
const untrustedAllow = [];
|
|
61
100
|
for (const scope of ["user", "project", "local"]) {
|
|
62
101
|
const data = this.raw[scope];
|
|
63
102
|
if (!data)
|
|
64
103
|
continue;
|
|
65
104
|
if ("lsp" in data) {
|
|
66
|
-
const
|
|
105
|
+
const trustedLsp = scope === "user" || this.repoTrusted;
|
|
106
|
+
const parsed = trustedLsp ? normalizeLspConfig(data.lsp) : untrustedLspSwitches(data.lsp);
|
|
107
|
+
if (!trustedLsp && repoCapabilities(data).lspServers.length > 0) {
|
|
108
|
+
diagnostics.push({
|
|
109
|
+
scope,
|
|
110
|
+
path: this.paths[scope],
|
|
111
|
+
message: `Ignored LSP server definitions from this repository (${repoCapabilities(data).lspServers.join(", ")}): the folder is not trusted (Bubble asks at startup).`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
67
114
|
if (parsed === undefined) {
|
|
68
115
|
diagnostics.push({
|
|
69
116
|
scope,
|
|
@@ -71,7 +118,7 @@ export class SettingsManager {
|
|
|
71
118
|
message: "Ignored lsp setting — expected boolean or object.",
|
|
72
119
|
});
|
|
73
120
|
}
|
|
74
|
-
else {
|
|
121
|
+
else if (parsed !== null) {
|
|
75
122
|
lsp = parsed;
|
|
76
123
|
}
|
|
77
124
|
}
|
|
@@ -93,7 +140,19 @@ export class SettingsManager {
|
|
|
93
140
|
}
|
|
94
141
|
if (Array.isArray(perms.allow)) {
|
|
95
142
|
const parsed = parseRules(perms.allow);
|
|
96
|
-
|
|
143
|
+
if (scope === "user" || this.repoTrusted) {
|
|
144
|
+
allow.push(...parsed.rules);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
untrustedAllow.push(...parsed.rules);
|
|
148
|
+
if (parsed.rules.length > 0) {
|
|
149
|
+
diagnostics.push({
|
|
150
|
+
scope,
|
|
151
|
+
path: this.paths[scope],
|
|
152
|
+
message: `Ignored ${parsed.rules.length} allow rule${parsed.rules.length === 1 ? "" : "s"} from this repository: the folder is not trusted (Bubble asks at startup).`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
97
156
|
for (const err of parsed.errors) {
|
|
98
157
|
diagnostics.push({
|
|
99
158
|
scope,
|
|
@@ -118,6 +177,10 @@ export class SettingsManager {
|
|
|
118
177
|
defaultMode,
|
|
119
178
|
lsp,
|
|
120
179
|
ruleSet: { allow, deny },
|
|
180
|
+
untrustedAllow,
|
|
181
|
+
untrusted: this.repoTrusted
|
|
182
|
+
? { allow: [], mcpServers: [], lspServers: [] }
|
|
183
|
+
: mergeCapabilities(repoCapabilities(this.raw.project), repoCapabilities(this.raw.local)),
|
|
121
184
|
diagnostics,
|
|
122
185
|
};
|
|
123
186
|
}
|
|
@@ -137,9 +200,11 @@ export class SettingsManager {
|
|
|
137
200
|
}
|
|
138
201
|
current.push(rule);
|
|
139
202
|
permissions[list] = current;
|
|
203
|
+
const wasTrusted = this.repoTrusted;
|
|
140
204
|
const next = { ...raw, permissions };
|
|
141
205
|
this.writeFile(scope, next);
|
|
142
206
|
this.raw[scope] = next;
|
|
207
|
+
this.keepRepoTrustAfterWrite(scope, wasTrusted);
|
|
143
208
|
return true;
|
|
144
209
|
}
|
|
145
210
|
/**
|
|
@@ -159,9 +224,11 @@ export class SettingsManager {
|
|
|
159
224
|
// Drop the key if empty, keep file readable
|
|
160
225
|
if (current.length === 0)
|
|
161
226
|
delete nextPermissions[list];
|
|
227
|
+
const wasTrusted = this.repoTrusted;
|
|
162
228
|
const next = { ...raw, permissions: nextPermissions };
|
|
163
229
|
this.writeFile(scope, next);
|
|
164
230
|
this.raw[scope] = next;
|
|
231
|
+
this.keepRepoTrustAfterWrite(scope, wasTrusted);
|
|
165
232
|
return true;
|
|
166
233
|
}
|
|
167
234
|
// -- internal --------------------------------------------------------
|
|
@@ -197,3 +264,26 @@ export class SettingsManager {
|
|
|
197
264
|
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
198
265
|
}
|
|
199
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* What an untrusted repository may still say about LSP: turning it off, or
|
|
269
|
+
* turning individual servers off. Commands, env and options need trust.
|
|
270
|
+
* Returns null when nothing usable is left (the scope then does not override).
|
|
271
|
+
*/
|
|
272
|
+
function untrustedLspSwitches(value) {
|
|
273
|
+
if (typeof value === "boolean")
|
|
274
|
+
return value === false ? false : null;
|
|
275
|
+
const parsed = normalizeLspConfig(value);
|
|
276
|
+
if (parsed === undefined || typeof parsed === "boolean")
|
|
277
|
+
return parsed;
|
|
278
|
+
const switches = Object.fromEntries(Object.entries(parsed)
|
|
279
|
+
.filter(([, server]) => server.disabled === true)
|
|
280
|
+
.map(([id]) => [id, { disabled: true }]));
|
|
281
|
+
return Object.keys(switches).length > 0 ? switches : null;
|
|
282
|
+
}
|
|
283
|
+
function mergeCapabilities(a, b) {
|
|
284
|
+
return {
|
|
285
|
+
allow: [...a.allow, ...b.allow],
|
|
286
|
+
mcpServers: [...new Set([...a.mcpServers, ...b.mcpServers])],
|
|
287
|
+
lspServers: [...new Set([...a.lspServers, ...b.lspServers])],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Just enough shell grammar for permission matching.
|
|
3
|
+
*
|
|
4
|
+
* Prefix rules ("git status", `Bash(npm run:*)`) used to compare against the
|
|
5
|
+
* raw command string, so an approved `git status` also approved
|
|
6
|
+
* `git status && rm -rf ~`, and a `Bash(rm:*)` deny rule missed
|
|
7
|
+
* `echo ok && rm -rf x`. This splits a command into the simple commands it
|
|
8
|
+
* runs and flags constructs whose effect cannot be judged from the visible
|
|
9
|
+
* command words. It is deliberately not a full parser: anything it does not
|
|
10
|
+
* understand is marked opaque, and opaque commands are never auto-allowed.
|
|
11
|
+
*/
|
|
12
|
+
export interface ShellCommandAnalysis {
|
|
13
|
+
/**
|
|
14
|
+
* Simple commands in source order, including ones nested inside command
|
|
15
|
+
* substitution, backticks and process substitution. Leading control
|
|
16
|
+
* keywords (`then`, `do`, `!`, `{`, ...) are stripped.
|
|
17
|
+
*/
|
|
18
|
+
segments: string[];
|
|
19
|
+
/**
|
|
20
|
+
* The command contains something an allow rule cannot vouch for: command
|
|
21
|
+
* or process substitution, backticks, a redirection to anything but
|
|
22
|
+
* /dev/null or another fd, a heredoc, a compound statement header
|
|
23
|
+
* (for/case/function), or unterminated quoting.
|
|
24
|
+
*/
|
|
25
|
+
opaque: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function analyzeShellCommand(command: string): ShellCommandAnalysis;
|
|
28
|
+
export declare function shellTokens(segment: string): string[];
|
|
29
|
+
/**
|
|
30
|
+
* The words of a segment with leading `VAR=value` assignments and command
|
|
31
|
+
* wrappers (`sudo -u x`, `env`, `nohup`, ...) removed. Deny rules match
|
|
32
|
+
* against this too, so `sudo rm -rf x` and `FOO=1 rm -rf x` still hit a
|
|
33
|
+
* `Bash(rm:*)` deny. Best effort — deny rules on shell commands can always be
|
|
34
|
+
* evaded by a determined command (e.g. `bash -c`), and are documented as such.
|
|
35
|
+
*/
|
|
36
|
+
export declare function effectiveCommandTokens(segment: string): string[];
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Just enough shell grammar for permission matching.
|
|
3
|
+
*
|
|
4
|
+
* Prefix rules ("git status", `Bash(npm run:*)`) used to compare against the
|
|
5
|
+
* raw command string, so an approved `git status` also approved
|
|
6
|
+
* `git status && rm -rf ~`, and a `Bash(rm:*)` deny rule missed
|
|
7
|
+
* `echo ok && rm -rf x`. This splits a command into the simple commands it
|
|
8
|
+
* runs and flags constructs whose effect cannot be judged from the visible
|
|
9
|
+
* command words. It is deliberately not a full parser: anything it does not
|
|
10
|
+
* understand is marked opaque, and opaque commands are never auto-allowed.
|
|
11
|
+
*/
|
|
12
|
+
/** Reserved words that may precede a command without changing what it runs. */
|
|
13
|
+
const LEADING_KEYWORDS = new Set([
|
|
14
|
+
"if", "then", "else", "elif", "fi", "do", "done", "while", "until", "!", "{", "}", "time",
|
|
15
|
+
]);
|
|
16
|
+
/** Statement headers whose words are not a command at all. */
|
|
17
|
+
const OPAQUE_HEADERS = new Set(["for", "case", "select", "function", "esac"]);
|
|
18
|
+
const WORD_BREAK = /[\s;&|()<>]/;
|
|
19
|
+
export function analyzeShellCommand(command) {
|
|
20
|
+
const segments = [];
|
|
21
|
+
let opaque = false;
|
|
22
|
+
let current = "";
|
|
23
|
+
// Open contexts: a double quote, a `$(`/`<(`/`>(`/subshell closed by `)`,
|
|
24
|
+
// or a backtick substitution.
|
|
25
|
+
const stack = [];
|
|
26
|
+
const flush = () => {
|
|
27
|
+
const tokens = current.trim().split(/\s+/).filter(Boolean);
|
|
28
|
+
current = "";
|
|
29
|
+
while (tokens.length > 0 && LEADING_KEYWORDS.has(tokens[0]))
|
|
30
|
+
tokens.shift();
|
|
31
|
+
if (tokens.length === 0)
|
|
32
|
+
return;
|
|
33
|
+
if (OPAQUE_HEADERS.has(tokens[0]))
|
|
34
|
+
opaque = true;
|
|
35
|
+
segments.push(tokens.join(" "));
|
|
36
|
+
};
|
|
37
|
+
const n = command.length;
|
|
38
|
+
for (let i = 0; i < n; i++) {
|
|
39
|
+
const c = command[i];
|
|
40
|
+
const next = command[i + 1];
|
|
41
|
+
const top = stack[stack.length - 1];
|
|
42
|
+
if (top === '"') {
|
|
43
|
+
if (c === "\\") {
|
|
44
|
+
current += c + (next ?? "");
|
|
45
|
+
i++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (c === '"') {
|
|
49
|
+
stack.pop();
|
|
50
|
+
current += c;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (c === "$" && next === "(") {
|
|
54
|
+
opaque = true;
|
|
55
|
+
flush();
|
|
56
|
+
stack.push(")");
|
|
57
|
+
i++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (c === "`") {
|
|
61
|
+
opaque = true;
|
|
62
|
+
flush();
|
|
63
|
+
stack.push("`");
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
current += c;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === "'") {
|
|
70
|
+
const end = command.indexOf("'", i + 1);
|
|
71
|
+
if (end < 0) {
|
|
72
|
+
opaque = true;
|
|
73
|
+
current += command.slice(i);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
current += command.slice(i, end + 1);
|
|
77
|
+
i = end;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (c === "\\") {
|
|
81
|
+
current += c + (next ?? "");
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (c === '"') {
|
|
86
|
+
stack.push('"');
|
|
87
|
+
current += c;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (c === "`") {
|
|
91
|
+
opaque = true;
|
|
92
|
+
flush();
|
|
93
|
+
if (top === "`")
|
|
94
|
+
stack.pop();
|
|
95
|
+
else
|
|
96
|
+
stack.push("`");
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (c === "$" && next === "(") {
|
|
100
|
+
opaque = true;
|
|
101
|
+
flush();
|
|
102
|
+
stack.push(")");
|
|
103
|
+
i++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if ((c === "<" || c === ">") && next === "(") {
|
|
107
|
+
opaque = true;
|
|
108
|
+
flush();
|
|
109
|
+
stack.push(")");
|
|
110
|
+
i++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (c === "<" || c === ">" || (c === "&" && next === ">")) {
|
|
114
|
+
// A redirection. Drop an fd number glued to the operator ("2>").
|
|
115
|
+
current = current.replace(/(^|\s)\d+$/, "$1");
|
|
116
|
+
let j = i + 1;
|
|
117
|
+
let operator = c;
|
|
118
|
+
while (j < n && /[<>&|]/.test(command[j])) {
|
|
119
|
+
operator += command[j];
|
|
120
|
+
j++;
|
|
121
|
+
}
|
|
122
|
+
if (operator.startsWith("<<"))
|
|
123
|
+
opaque = true; // heredoc / herestring
|
|
124
|
+
while (j < n && (command[j] === " " || command[j] === "\t"))
|
|
125
|
+
j++;
|
|
126
|
+
let k = j;
|
|
127
|
+
while (k < n && !WORD_BREAK.test(command[k]))
|
|
128
|
+
k++;
|
|
129
|
+
const target = command.slice(j, k);
|
|
130
|
+
const harmless = target === "/dev/null" || (operator.endsWith("&") && /^(\d+|-)$/.test(target));
|
|
131
|
+
if (!harmless)
|
|
132
|
+
opaque = true;
|
|
133
|
+
i = k - 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (c === ")") {
|
|
137
|
+
flush();
|
|
138
|
+
if (top === ")")
|
|
139
|
+
stack.pop();
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (c === "(") {
|
|
143
|
+
flush();
|
|
144
|
+
stack.push(")");
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (c === ";" || c === "\n" || c === "&" || c === "|") {
|
|
148
|
+
flush();
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
current += c;
|
|
152
|
+
}
|
|
153
|
+
flush();
|
|
154
|
+
if (stack.length > 0)
|
|
155
|
+
opaque = true;
|
|
156
|
+
return { segments, opaque };
|
|
157
|
+
}
|
|
158
|
+
export function shellTokens(segment) {
|
|
159
|
+
return segment.trim().split(/\s+/).filter(Boolean);
|
|
160
|
+
}
|
|
161
|
+
/** Wrappers that run their argument as the real command. */
|
|
162
|
+
const COMMAND_WRAPPERS = new Set(["command", "exec", "nohup", "builtin", "env", "sudo", "doas", "nice", "xargs"]);
|
|
163
|
+
/**
|
|
164
|
+
* Wrapper options that take a separate argument (`sudo -u root`), so that
|
|
165
|
+
* argument is not mistaken for the command. `env -S` is deliberately absent:
|
|
166
|
+
* its argument is the command line itself.
|
|
167
|
+
*/
|
|
168
|
+
const WRAPPER_OPTION_ARGS = {
|
|
169
|
+
sudo: { short: "CDghpRrTtUu", long: ["chdir", "chroot", "close-from", "command-timeout", "group", "host", "other-user", "prompt", "role", "type", "user"] },
|
|
170
|
+
doas: { short: "Cu", long: [] },
|
|
171
|
+
env: { short: "aCPu", long: ["argv0", "chdir", "unset"] },
|
|
172
|
+
nice: { short: "n", long: ["adjustment"] },
|
|
173
|
+
// GNU --eof/--replace/--max-lines take a value only as `--opt=value`.
|
|
174
|
+
xargs: { short: "adEIJLnPRSs", long: ["arg-file", "delimiter", "max-args", "max-chars", "max-procs", "process-slot-var"] },
|
|
175
|
+
exec: { short: "a", long: [] },
|
|
176
|
+
};
|
|
177
|
+
/** getopt rules: in `-Eu root` the last letter takes the next word; in `-uroot` it is attached. */
|
|
178
|
+
function optionTakesNextWord(wrapper, option) {
|
|
179
|
+
const spec = WRAPPER_OPTION_ARGS[wrapper];
|
|
180
|
+
if (!spec)
|
|
181
|
+
return false;
|
|
182
|
+
if (option.startsWith("--"))
|
|
183
|
+
return !option.includes("=") && spec.long.includes(option.slice(2));
|
|
184
|
+
for (let index = 1; index < option.length; index++) {
|
|
185
|
+
if (spec.short.includes(option[index]))
|
|
186
|
+
return index === option.length - 1;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The words of a segment with leading `VAR=value` assignments and command
|
|
192
|
+
* wrappers (`sudo -u x`, `env`, `nohup`, ...) removed. Deny rules match
|
|
193
|
+
* against this too, so `sudo rm -rf x` and `FOO=1 rm -rf x` still hit a
|
|
194
|
+
* `Bash(rm:*)` deny. Best effort — deny rules on shell commands can always be
|
|
195
|
+
* evaded by a determined command (e.g. `bash -c`), and are documented as such.
|
|
196
|
+
*/
|
|
197
|
+
export function effectiveCommandTokens(segment) {
|
|
198
|
+
const tokens = shellTokens(segment).map(unquoteWord);
|
|
199
|
+
let changed = true;
|
|
200
|
+
while (changed && tokens.length > 0) {
|
|
201
|
+
changed = false;
|
|
202
|
+
while (tokens.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0])) {
|
|
203
|
+
tokens.shift();
|
|
204
|
+
changed = true;
|
|
205
|
+
}
|
|
206
|
+
if (tokens.length > 0 && COMMAND_WRAPPERS.has(tokens[0])) {
|
|
207
|
+
const wrapper = tokens.shift();
|
|
208
|
+
while (tokens.length > 0 && tokens[0].startsWith("-")) {
|
|
209
|
+
const option = tokens.shift();
|
|
210
|
+
if (option === "--")
|
|
211
|
+
break;
|
|
212
|
+
if (optionTakesNextWord(wrapper, option))
|
|
213
|
+
tokens.shift();
|
|
214
|
+
}
|
|
215
|
+
changed = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// `/bin/rm` and `./rm` run the same program a `Bash(rm:*)` deny names.
|
|
219
|
+
if (tokens.length > 0 && tokens[0].includes("/"))
|
|
220
|
+
tokens[0] = tokens[0].slice(tokens[0].lastIndexOf("/") + 1);
|
|
221
|
+
return tokens;
|
|
222
|
+
}
|
|
223
|
+
/** `'rm'`, `"rm"` and `r\m` are all the word rm to the shell. */
|
|
224
|
+
function unquoteWord(word) {
|
|
225
|
+
return word.replace(/\\(.)/g, "$1").replace(/['"]/g, "");
|
|
226
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folder trust, the Kimi Code way: when the repository's `.bubble` settings
|
|
3
|
+
* would grant something (allow rules, MCP servers, LSP servers), ask once at
|
|
4
|
+
* startup before any of it loads. Trusting records the exact settings; any
|
|
5
|
+
* later change asks again. Declining keeps them off for this run only.
|
|
6
|
+
*/
|
|
7
|
+
export declare function promptRepoTrust(cwd: string, io?: {
|
|
8
|
+
input: NodeJS.ReadableStream;
|
|
9
|
+
output: NodeJS.WritableStream;
|
|
10
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { describeRepoCapabilities, isRepoConfigTrusted, mergedRepoCapabilities, readRepoSettings, trustRepoConfig, } from "./trust.js";
|
|
4
|
+
/**
|
|
5
|
+
* Folder trust, the Kimi Code way: when the repository's `.bubble` settings
|
|
6
|
+
* would grant something (allow rules, MCP servers, LSP servers), ask once at
|
|
7
|
+
* startup before any of it loads. Trusting records the exact settings; any
|
|
8
|
+
* later change asks again. Declining keeps them off for this run only.
|
|
9
|
+
*/
|
|
10
|
+
export async function promptRepoTrust(cwd, io = { input: process.stdin, output: process.stdout }) {
|
|
11
|
+
// Read once: what gets trusted is exactly what was shown.
|
|
12
|
+
const raw = readRepoSettings(cwd);
|
|
13
|
+
if (isRepoConfigTrusted(cwd, raw))
|
|
14
|
+
return true;
|
|
15
|
+
const say = (line) => io.output.write(`${line}\n`);
|
|
16
|
+
say(chalk.bold(`\nThis folder's .bubble settings would enable:`));
|
|
17
|
+
for (const line of describeRepoCapabilities(mergedRepoCapabilities(raw)))
|
|
18
|
+
say(` ${line}`);
|
|
19
|
+
say(chalk.dim("Only trust folders whose settings you have reviewed; MCP servers run commands on your machine."));
|
|
20
|
+
const rl = createInterface({ input: io.input, output: io.output });
|
|
21
|
+
try {
|
|
22
|
+
const answer = (await rl.question("Trust this folder? [Y/n] ")).trim().toLowerCase();
|
|
23
|
+
const trusted = answer === "" || answer === "y" || answer === "yes";
|
|
24
|
+
if (trusted)
|
|
25
|
+
trustRepoConfig(cwd, raw);
|
|
26
|
+
say(chalk.dim(trusted
|
|
27
|
+
? "Trusted. Any change to these settings will ask again.\n"
|
|
28
|
+
: "Not trusted: these settings stay off this session.\n"));
|
|
29
|
+
return trusted;
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
rl.close();
|
|
33
|
+
}
|
|
34
|
+
}
|