@24klynx/permissions 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,244 @@
1
+ import { CommandSafety, ToolDescriptor, ToolInvocation } from "@lynx/tools";
2
+
3
+ //#region src/modes.d.ts
4
+ /**
5
+ * Permission modes — the 6 operating modes that control how
6
+ * tool execution permissions are resolved.
7
+ *
8
+ * default — ask user for every RequiresApproval tool
9
+ * auto — LLM YOLO classifier decides, user can override
10
+ * yolo — auto‑approve everything, max throughput
11
+ * dontAsk — deny all RequiresApproval tools silently
12
+ * headless — no TUI, auto‑deny interactive prompts
13
+ * plan — read‑only mode, all mutating tools denied
14
+ */
15
+ type PermissionMode = "default" | "auto" | "yolo" | "dontAsk" | "headless" | "plan";
16
+ interface ModeBehavior {
17
+ /** Auto‑approve Safe tools without asking. */
18
+ autoApproveSafe: boolean;
19
+ /** Auto‑approve WorkspaceSafe tools. */
20
+ autoApproveWorkspaceSafe: boolean;
21
+ /** Auto‑deny Dangerous tools. */
22
+ autoDenyDangerous: boolean;
23
+ /** Use LLM classifier for RequiresApproval tools. */
24
+ useClassifier: boolean;
25
+ /** Show interactive prompts to the user. */
26
+ showPrompts: boolean;
27
+ /** Allow any file‑modifying tools. */
28
+ allowWrites: boolean;
29
+ }
30
+ /** Get the behavior profile for a given mode. */
31
+ declare function getModeBehavior(mode: PermissionMode): ModeBehavior;
32
+ /** List all valid mode names. */
33
+ declare function listModes(): PermissionMode[];
34
+ /** Validate that a string is a recognized mode. */
35
+ declare function isValidMode(value: string): value is PermissionMode;
36
+ //#endregion
37
+ //#region src/rules.d.ts
38
+ interface PermissionRule {
39
+ /** Glob pattern matching tool names (e.g. "bash", "read_*", "*"). */
40
+ glob: string;
41
+ /** Optional argument pattern for tool‑specific rules (e.g. "git push *"). */
42
+ args?: string;
43
+ /** Override the safety classification for matched tools. */
44
+ safety?: CommandSafety;
45
+ /** Explicitly allow matched tools. */
46
+ allow?: boolean;
47
+ /** Explicitly deny matched tools. */
48
+ deny?: boolean;
49
+ /** Human‑readable reason shown to the user. */
50
+ reason?: string;
51
+ }
52
+ interface RuleMatch {
53
+ rule: PermissionRule;
54
+ source: RuleSource;
55
+ }
56
+ type RuleSource = "blocklist" | "builtin" | "plugin" | "config" | "workspace" | "project" | "user" | "session";
57
+ interface RuleEngine {
58
+ /** Register a rule from a specific source. */
59
+ add(source: RuleSource, rule: PermissionRule): void;
60
+ /** Remove all rules from a source. */
61
+ clearSource(source: RuleSource): void;
62
+ /** Find all rules that match a tool name, sorted by priority (highest first). */
63
+ match(toolName: string): RuleMatch[];
64
+ /** Check if any matching rule explicitly denies the tool. */
65
+ isDenied(toolName: string): boolean;
66
+ /** Check if any matching rule explicitly allows the tool. */
67
+ isAllowed(toolName: string): boolean;
68
+ }
69
+ /**
70
+ * Create a rule matching engine.
71
+ *
72
+ * Rules are evaluated lazily — each match() call re‑scans
73
+ * the rule set for the given tool name.
74
+ */
75
+ declare function createRuleEngine(): RuleEngine;
76
+ //#endregion
77
+ //#region src/denial.d.ts
78
+ /**
79
+ * Denial tracker with circuit breaker.
80
+ *
81
+ * When a user denies 3 consecutive tool requests within the same
82
+ * turn, the circuit breaker trips and auto‑denies all subsequent
83
+ * requests for the rest of that turn.
84
+ *
85
+ * The breaker resets at the start of each new turn.
86
+ */
87
+ interface DenialTracker {
88
+ /** Record a denial from the user. */
89
+ recordDenial(): void;
90
+ /** Record an approval (resets the consecutive count). */
91
+ recordApproval(): void;
92
+ /** Check whether the circuit breaker has tripped. */
93
+ isTripped(): boolean;
94
+ /** Reset the tracker for a new turn. */
95
+ resetTurn(): void;
96
+ /** Current consecutive denial count. */
97
+ readonly consecutiveDenials: number;
98
+ }
99
+ /**
100
+ * Create a denial tracker.
101
+ *
102
+ * One instance per session — it tracks user behavior across
103
+ * multiple tool invocations within a single turn.
104
+ */
105
+ declare function createDenialTracker(): DenialTracker;
106
+ //#endregion
107
+ //#region src/pipeline.d.ts
108
+ interface PipelineContext {
109
+ mode: PermissionMode;
110
+ behavior: ModeBehavior;
111
+ ruleEngine: RuleEngine;
112
+ denialTracker: DenialTracker;
113
+ /** User callback — returns true to allow, false to deny. */
114
+ askUser?: (invocation: ToolInvocation) => Promise<boolean>;
115
+ /** LLM classifier callback — returns true to allow. */
116
+ classify?: (invocation: ToolInvocation) => Promise<boolean>;
117
+ }
118
+ interface PipelineResult {
119
+ allowed: boolean;
120
+ reason: string;
121
+ step: number;
122
+ /** If true, the result was determined without user interaction. */
123
+ automatic: boolean;
124
+ }
125
+ /**
126
+ * Run a tool invocation through the 14‑step permission pipeline.
127
+ *
128
+ * Each step is an independent function. The pipeline short‑circuits
129
+ * on the first non‑null result.
130
+ */
131
+ declare function runPipeline(tool: ToolDescriptor, invocation: ToolInvocation, ctx: PipelineContext): Promise<PipelineResult>;
132
+ //#endregion
133
+ //#region src/classifier.d.ts
134
+ interface YoloClassifier {
135
+ /**
136
+ * Ask the classifier whether a tool invocation should be allowed.
137
+ *
138
+ * Returns true if the invocation looks safe, false otherwise.
139
+ * Never throws — on error, falls back to a conservative heuristic.
140
+ */
141
+ classify(tool: ToolDescriptor, invocation: ToolInvocation): Promise<boolean>;
142
+ }
143
+ /** LLM completion function signature — injected via DI. */
144
+ type ClassifyLlmCall = (prompt: string) => Promise<string>;
145
+ /** Options for creating the classifier. */
146
+ interface CreateYoloClassifierOptions {
147
+ /**
148
+ * Optional LLM completion function.
149
+ *
150
+ * When provided, the classifier sends a ~200‑token prompt to a
151
+ * fast model (e.g. Haiku / Flash) and parses "allow" / "deny".
152
+ * When omitted, falls back to the heuristic classifier only.
153
+ */
154
+ llmCall?: ClassifyLlmCall;
155
+ /** Timeout in milliseconds for the LLM call. Default 500. */
156
+ timeoutMs?: number;
157
+ }
158
+ /**
159
+ * Create a YOLO classifier.
160
+ *
161
+ * When `llmCall` is provided, prompts a fast LLM to decide each
162
+ * classification. A cache avoids redundant calls for identical
163
+ * (tool, safety, args) combinations within the same session.
164
+ *
165
+ * Falls back to a heuristic classifier on timeout or error.
166
+ */
167
+ declare function createYoloClassifier(opts?: CreateYoloClassifierOptions): YoloClassifier;
168
+ //#endregion
169
+ //#region src/sources.d.ts
170
+ interface RulesLoader {
171
+ /** Load all rules from a source into the engine. */
172
+ load(source: RuleSource, rules: PermissionRule[]): void;
173
+ /** Reload rules from a source (clears previous entries). */
174
+ reload(source: RuleSource, rules: PermissionRule[]): void;
175
+ }
176
+ /**
177
+ * Create a rules loader that populates a RuleEngine from
178
+ * configuration sources.
179
+ *
180
+ * Call load() for each source at startup, then reload() if
181
+ * the user edits their settings mid‑session.
182
+ */
183
+ declare function createRulesLoader(engine: RuleEngine): RulesLoader;
184
+ /**
185
+ * Load permission rules from the user's config directory.
186
+ *
187
+ * Reads two files:
188
+ * - ~/.lynx/settings.json → `permissions.rules` array
189
+ * - <workspace>/.lynx/permissions.json → top‑level rules array
190
+ *
191
+ * Parsing errors are silently ignored (don't block startup).
192
+ *
193
+ * @returns Number of rules loaded, or 0 if no files found.
194
+ */
195
+ declare function loadFromDisk(loader: RulesLoader, workspace?: string): number;
196
+ //#endregion
197
+ //#region src/mcp-channel.d.ts
198
+ /**
199
+ * MCP 频道权限 — 按频道限制可用的 MCP 服务器和工具。
200
+ *
201
+ * 每个频道可配置允许的服务器列表和工具列表,
202
+ * 以及是否需要用户批准。未匹配频道时回退到默认策略。
203
+ */
204
+ /** MCP 频道权限策略 — 每条规则绑定一个频道。 */
205
+ interface McpChannelPolicy {
206
+ /** 频道名称("wechat"、"feishu"、"web" 等)。 */
207
+ channelName: string;
208
+ /** 允许的 MCP 服务器名称列表。 */
209
+ allowedServers: string[];
210
+ /** 允许的工具名列表(原始名称,非命名空间格式)。 */
211
+ allowedTools: string[];
212
+ /** 是否需要用户批准。 */
213
+ requireApproval: boolean;
214
+ }
215
+ /** 权限检查结果。 */
216
+ interface McpCheckResult {
217
+ allowed: boolean;
218
+ reason: string;
219
+ }
220
+ /**
221
+ * 默认策略 — 无限制(所有频道可访问所有 MCP 工具)。
222
+ *
223
+ * 适用于内部工具和未明确配置频道的场景。
224
+ */
225
+ declare const DEFAULT_MCP_CHANNEL_POLICY: McpChannelPolicy[];
226
+ /**
227
+ * 创建 MCP 频道权限检查器。
228
+ *
229
+ * 返回一个函数:给定频道名和工具名,返回是否允许及原因。
230
+ * 检查逻辑:
231
+ * 1. 如果 channel 为 undefined,使用默认策略(全部允许)
232
+ * 2. 查找匹配 channelName 的策略
233
+ * 3. 未找到匹配策略 → 回退到第一个匹配的频道或拒绝
234
+ * 4. 找到匹配策略 → 检查 server 是否在 allowedServers 中
235
+ * 5. 检查 toolName 是否在 allowedTools 中(或 allowedTools 包含 "*")
236
+ * 6. 全部通过 → 返回 allowed: true,附带是否需要批准的信息
237
+ *
238
+ * @param policies - 频道权限策略列表
239
+ * @returns 权限检查函数
240
+ */
241
+ declare function createMcpChannelChecker(policies: McpChannelPolicy[]): (channel: string | undefined, toolName: string, serverName?: string) => McpCheckResult;
242
+ //#endregion
243
+ export { DEFAULT_MCP_CHANNEL_POLICY, type DenialTracker, type McpChannelPolicy, type McpCheckResult, type ModeBehavior, type PermissionMode, type PermissionRule, type PipelineContext, type PipelineResult, type RuleEngine, type RuleMatch, type RuleSource, type RulesLoader, type YoloClassifier, createDenialTracker, createMcpChannelChecker, createRuleEngine, createRulesLoader, createYoloClassifier, getModeBehavior, isValidMode, listModes, loadFromDisk, runPipeline };
244
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/modes.ts","../src/rules.ts","../src/denial.ts","../src/pipeline.ts","../src/classifier.ts","../src/sources.ts","../src/mcp-channel.ts"],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,666 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ //#region src/modes.ts
5
+ const MODE_TABLE = {
6
+ default: {
7
+ autoApproveSafe: true,
8
+ autoApproveWorkspaceSafe: false,
9
+ autoDenyDangerous: true,
10
+ useClassifier: false,
11
+ showPrompts: true,
12
+ allowWrites: true
13
+ },
14
+ auto: {
15
+ autoApproveSafe: true,
16
+ autoApproveWorkspaceSafe: true,
17
+ autoDenyDangerous: true,
18
+ useClassifier: true,
19
+ showPrompts: false,
20
+ allowWrites: true
21
+ },
22
+ yolo: {
23
+ autoApproveSafe: true,
24
+ autoApproveWorkspaceSafe: true,
25
+ autoDenyDangerous: false,
26
+ useClassifier: false,
27
+ showPrompts: false,
28
+ allowWrites: true
29
+ },
30
+ dontAsk: {
31
+ autoApproveSafe: true,
32
+ autoApproveWorkspaceSafe: false,
33
+ autoDenyDangerous: true,
34
+ useClassifier: false,
35
+ showPrompts: false,
36
+ allowWrites: false
37
+ },
38
+ headless: {
39
+ autoApproveSafe: true,
40
+ autoApproveWorkspaceSafe: false,
41
+ autoDenyDangerous: true,
42
+ useClassifier: false,
43
+ showPrompts: false,
44
+ allowWrites: true
45
+ },
46
+ plan: {
47
+ autoApproveSafe: true,
48
+ autoApproveWorkspaceSafe: false,
49
+ autoDenyDangerous: true,
50
+ useClassifier: false,
51
+ showPrompts: false,
52
+ allowWrites: false
53
+ }
54
+ };
55
+ /** Get the behavior profile for a given mode. */
56
+ function getModeBehavior(mode) {
57
+ return MODE_TABLE[mode];
58
+ }
59
+ /** List all valid mode names. */
60
+ function listModes() {
61
+ return Object.keys(MODE_TABLE);
62
+ }
63
+ /** Validate that a string is a recognized mode. */
64
+ function isValidMode(value) {
65
+ return value in MODE_TABLE;
66
+ }
67
+ //#endregion
68
+ //#region src/rules.ts
69
+ /** Priority weight — higher number = higher priority. */
70
+ const SOURCE_PRIORITY = {
71
+ blocklist: 0,
72
+ builtin: 1,
73
+ plugin: 2,
74
+ config: 3,
75
+ workspace: 4,
76
+ project: 5,
77
+ user: 6,
78
+ session: 7
79
+ };
80
+ function globMatch(pattern, value) {
81
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
82
+ return new RegExp(`^${regex}$`).test(value);
83
+ }
84
+ /**
85
+ * Create a rule matching engine.
86
+ *
87
+ * Rules are evaluated lazily — each match() call re‑scans
88
+ * the rule set for the given tool name.
89
+ */
90
+ function createRuleEngine() {
91
+ const rulesBySource = /* @__PURE__ */ new Map();
92
+ for (const rule of [
93
+ {
94
+ glob: "sudo",
95
+ deny: true,
96
+ reason: "Privilege escalation is forbidden"
97
+ },
98
+ {
99
+ glob: "su",
100
+ deny: true,
101
+ reason: "Privilege escalation is forbidden"
102
+ },
103
+ {
104
+ glob: "doas",
105
+ deny: true,
106
+ reason: "Privilege escalation is forbidden"
107
+ }
108
+ ]) {
109
+ const existing = rulesBySource.get("blocklist") ?? [];
110
+ existing.push(rule);
111
+ rulesBySource.set("blocklist", existing);
112
+ }
113
+ return {
114
+ add(source, rule) {
115
+ const existing = rulesBySource.get(source) ?? [];
116
+ existing.push(rule);
117
+ rulesBySource.set(source, existing);
118
+ },
119
+ clearSource(source) {
120
+ rulesBySource.delete(source);
121
+ },
122
+ match(toolName) {
123
+ const matches = [];
124
+ for (const [source, rules] of rulesBySource) for (const rule of rules) if (globMatch(rule.glob, toolName)) matches.push({
125
+ rule,
126
+ source
127
+ });
128
+ matches.sort((a, b) => SOURCE_PRIORITY[b.source] - SOURCE_PRIORITY[a.source]);
129
+ return matches;
130
+ },
131
+ isDenied(toolName) {
132
+ const matches = this.match(toolName);
133
+ if (matches.length === 0) return false;
134
+ return matches[0].rule.deny === true;
135
+ },
136
+ isAllowed(toolName) {
137
+ const matches = this.match(toolName);
138
+ if (matches.length === 0) return false;
139
+ return matches[0].rule.allow === true;
140
+ }
141
+ };
142
+ }
143
+ //#endregion
144
+ //#region src/pipeline.ts
145
+ function step1_unknownTool(tool) {
146
+ if (!tool) return {
147
+ allowed: false,
148
+ reason: "未知工具",
149
+ step: 1,
150
+ automatic: true
151
+ };
152
+ return null;
153
+ }
154
+ function step2_safeTool(tool) {
155
+ if (tool.safety === "Safe") return {
156
+ allowed: true,
157
+ reason: "安全工具 — 始终允许",
158
+ step: 2,
159
+ automatic: true
160
+ };
161
+ return null;
162
+ }
163
+ function step3_dangerousTool(tool, _inv, ctx) {
164
+ if (tool.safety === "Dangerous" && ctx.behavior.autoDenyDangerous) return {
165
+ allowed: false,
166
+ reason: "危险工具 — 已自动拒绝",
167
+ step: 3,
168
+ automatic: true
169
+ };
170
+ return null;
171
+ }
172
+ function step4_ruleDeny(_tool, invocation, ctx) {
173
+ if (ctx.ruleEngine.isDenied(invocation.toolName)) return {
174
+ allowed: false,
175
+ reason: "被权限规则拒绝",
176
+ step: 4,
177
+ automatic: true
178
+ };
179
+ return null;
180
+ }
181
+ function step5_ruleAllow(_tool, invocation, ctx) {
182
+ if (ctx.ruleEngine.isAllowed(invocation.toolName)) return {
183
+ allowed: true,
184
+ reason: "被权限规则允许",
185
+ step: 5,
186
+ automatic: true
187
+ };
188
+ return null;
189
+ }
190
+ function step6_planModeDeniesWrites(tool, _inv, ctx) {
191
+ if (ctx.mode === "plan" && tool.kind !== "ReadOnly") return {
192
+ allowed: false,
193
+ reason: `写入工具 "${tool.name}" 在计划模式下被拒绝`,
194
+ step: 6,
195
+ automatic: true
196
+ };
197
+ return null;
198
+ }
199
+ function step7_circuitBreaker(_tool, _inv, ctx) {
200
+ if (ctx.denialTracker.isTripped()) return {
201
+ allowed: false,
202
+ reason: "熔断器已触发 — 自动拒绝",
203
+ step: 7,
204
+ automatic: true
205
+ };
206
+ return null;
207
+ }
208
+ function step8_yolo(_tool, _inv, ctx) {
209
+ if (ctx.mode === "yolo") return {
210
+ allowed: true,
211
+ reason: "YOLO 模式 — 已自动批准",
212
+ step: 8,
213
+ automatic: true
214
+ };
215
+ return null;
216
+ }
217
+ function step9_dontAsk(_tool, _inv, ctx) {
218
+ if (ctx.mode === "dontAsk") return {
219
+ allowed: false,
220
+ reason: "免问模式 — 已自动拒绝",
221
+ step: 9,
222
+ automatic: true
223
+ };
224
+ return null;
225
+ }
226
+ function step10_headless(_tool, _inv, ctx) {
227
+ if (ctx.mode === "headless") return {
228
+ allowed: false,
229
+ reason: "无头模式 — 无交互式提示",
230
+ step: 10,
231
+ automatic: true
232
+ };
233
+ return null;
234
+ }
235
+ async function step11_autoClassify(_tool, invocation, ctx) {
236
+ if (ctx.mode !== "auto" || !ctx.classify) return null;
237
+ try {
238
+ const allowed = await ctx.classify(invocation);
239
+ return {
240
+ allowed,
241
+ reason: allowed ? "自动分类为安全" : "自动分类为不安全",
242
+ step: 11,
243
+ automatic: true
244
+ };
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+ async function step12_defaultAskUser(_tool, invocation, ctx) {
250
+ if (ctx.mode !== "default") return null;
251
+ if (!ctx.askUser) return {
252
+ allowed: false,
253
+ reason: "默认模式 — 无用户提示可用",
254
+ step: 12,
255
+ automatic: true
256
+ };
257
+ try {
258
+ const allowed = await ctx.askUser(invocation);
259
+ return {
260
+ allowed,
261
+ reason: allowed ? "用户已批准" : "用户已拒绝",
262
+ step: 13,
263
+ automatic: false
264
+ };
265
+ } catch {
266
+ return {
267
+ allowed: false,
268
+ reason: "用户提示失败",
269
+ step: 13,
270
+ automatic: false
271
+ };
272
+ }
273
+ }
274
+ /** 兜底步骤 — 始终返回结果。 */
275
+ function step14_defaultDeny() {
276
+ return {
277
+ allowed: false,
278
+ reason: "默认拒绝 — 无规则匹配",
279
+ step: 14,
280
+ automatic: true
281
+ };
282
+ }
283
+ const PIPELINE = [
284
+ step1_unknownTool,
285
+ step2_safeTool,
286
+ step3_dangerousTool,
287
+ step4_ruleDeny,
288
+ step5_ruleAllow,
289
+ step6_planModeDeniesWrites,
290
+ step7_circuitBreaker,
291
+ step8_yolo,
292
+ step9_dontAsk,
293
+ step10_headless,
294
+ step11_autoClassify,
295
+ step12_defaultAskUser,
296
+ step14_defaultDeny
297
+ ];
298
+ /**
299
+ * Run a tool invocation through the 14‑step permission pipeline.
300
+ *
301
+ * Each step is an independent function. The pipeline short‑circuits
302
+ * on the first non‑null result.
303
+ */
304
+ async function runPipeline(tool, invocation, ctx) {
305
+ for (const step of PIPELINE) {
306
+ const result = await step(tool, invocation, ctx);
307
+ if (result) return result;
308
+ }
309
+ return {
310
+ allowed: false,
311
+ reason: "默认拒绝 — 无规则匹配",
312
+ step: 14,
313
+ automatic: true
314
+ };
315
+ }
316
+ //#endregion
317
+ //#region src/classifier.ts
318
+ const DEFAULT_TIMEOUT_MS = 500;
319
+ /** Compact prompt template (~200 tokens). */
320
+ const CLASSIFIER_PROMPT = `You are a security classifier. Given a tool invocation, respond with ONLY "allow" or "deny".
321
+
322
+ Rules:
323
+ - Read-only operations (read_file, ls, grep, search) → allow
324
+ - File writes within the workspace → allow
325
+ - Network requests → allow
326
+ - Shell commands that modify system state → deny
327
+ - Commands that delete files outside the workspace → deny
328
+ - Commands that install packages → deny
329
+ - Escalation commands (sudo, su) → deny
330
+
331
+ Tool: {toolName}
332
+ Description: {toolDescription}
333
+ Input: {toolInput}
334
+
335
+ Response:`;
336
+ /** Patterns that always result in deny. */
337
+ const DENY_PATTERNS = [
338
+ "sudo",
339
+ "rm -rf /",
340
+ "chmod 777",
341
+ "DROP TABLE",
342
+ "DELETE FROM",
343
+ "shutdown",
344
+ "reboot",
345
+ "format",
346
+ "mkfs",
347
+ "curl | bash",
348
+ "wget -O - |",
349
+ "> /dev/sda",
350
+ "dd if="
351
+ ];
352
+ /** Patterns that indicate safe dev workflows. */
353
+ const ALLOW_PATTERNS = [
354
+ "npm test",
355
+ "npm run",
356
+ "pnpm test",
357
+ "pnpm build",
358
+ "git ",
359
+ "node ",
360
+ "tsc",
361
+ "vitest",
362
+ "eslint",
363
+ "prettier"
364
+ ];
365
+ function heuristicClassify(tool, invocation) {
366
+ if (tool.kind === "ReadOnly") return true;
367
+ const payload = JSON.stringify(invocation.payload).toLowerCase();
368
+ for (const pattern of DENY_PATTERNS) if (payload.includes(pattern.toLowerCase())) return false;
369
+ for (const pattern of ALLOW_PATTERNS) if (payload.includes(pattern.toLowerCase())) return true;
370
+ return tool.safety === "Safe";
371
+ }
372
+ /** Cache key derived from tool identity + input hash. */
373
+ function cacheKey(tool, invocation) {
374
+ const inputHash = JSON.stringify(invocation.payload);
375
+ return `${tool.name}:${tool.safety}:${inputHash}`;
376
+ }
377
+ /**
378
+ * Create a YOLO classifier.
379
+ *
380
+ * When `llmCall` is provided, prompts a fast LLM to decide each
381
+ * classification. A cache avoids redundant calls for identical
382
+ * (tool, safety, args) combinations within the same session.
383
+ *
384
+ * Falls back to a heuristic classifier on timeout or error.
385
+ */
386
+ function createYoloClassifier(opts = {}) {
387
+ const { llmCall, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
388
+ const cache = /* @__PURE__ */ new Map();
389
+ return { async classify(tool, invocation) {
390
+ const key = cacheKey(tool, invocation);
391
+ const cached = cache.get(key);
392
+ if (cached !== void 0) return cached;
393
+ if (llmCall) try {
394
+ const prompt = CLASSIFIER_PROMPT.replace("{toolName}", tool.name).replace("{toolDescription}", tool.description).replace("{toolInput}", JSON.stringify(invocation.payload));
395
+ const decision = (await Promise.race([llmCall(prompt), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("Classifier timeout")), timeoutMs))])).trim().toLowerCase().startsWith("allow");
396
+ cache.set(key, decision);
397
+ return decision;
398
+ } catch {}
399
+ const decision = heuristicClassify(tool, invocation);
400
+ cache.set(key, decision);
401
+ return decision;
402
+ } };
403
+ }
404
+ //#endregion
405
+ //#region src/denial.ts
406
+ /** Number of consecutive denials before the breaker trips. */
407
+ const DENIAL_THRESHOLD = 3;
408
+ /**
409
+ * Create a denial tracker.
410
+ *
411
+ * One instance per session — it tracks user behavior across
412
+ * multiple tool invocations within a single turn.
413
+ */
414
+ function createDenialTracker() {
415
+ let consecutive = 0;
416
+ return {
417
+ get consecutiveDenials() {
418
+ return consecutive;
419
+ },
420
+ recordDenial() {
421
+ consecutive++;
422
+ },
423
+ recordApproval() {
424
+ consecutive = 0;
425
+ },
426
+ isTripped() {
427
+ return consecutive >= DENIAL_THRESHOLD;
428
+ },
429
+ resetTurn() {
430
+ consecutive = 0;
431
+ }
432
+ };
433
+ }
434
+ //#endregion
435
+ //#region src/sources.ts
436
+ /**
437
+ * Permission rule source loading — reads rules from each of the
438
+ * 7 priority layers and feeds them into the RuleEngine.
439
+ *
440
+ * Sources (lowest to highest priority):
441
+ * 0. blocklist — hardcoded (loaded by RuleEngine at construction)
442
+ * 1. builtin — default safety rules
443
+ * 2. plugin — plugin‑declared rules
444
+ * 3. config — ~/.lynx/config.json permissions.allow/deny
445
+ * 4. workspace — .lynx/permissions.json in the session workspace
446
+ * 5. project — directive files (CLAUDE.md)
447
+ * 6. user — ~/.lynx/settings.json
448
+ * 7. session — runtime overrides (set via /permissions command)
449
+ */
450
+ const BUILTIN_RULES = [
451
+ {
452
+ glob: "read_file",
453
+ allow: true,
454
+ reason: "Read‑only file access"
455
+ },
456
+ {
457
+ glob: "write_file",
458
+ allow: true,
459
+ reason: "Workspace‑scoped file write"
460
+ },
461
+ {
462
+ glob: "glob",
463
+ allow: true,
464
+ reason: "File pattern matching"
465
+ },
466
+ {
467
+ glob: "grep",
468
+ allow: true,
469
+ reason: "Content search"
470
+ },
471
+ {
472
+ glob: "ls",
473
+ allow: true,
474
+ reason: "Directory listing"
475
+ },
476
+ {
477
+ glob: "bash",
478
+ allow: true,
479
+ reason: "Shell access (permission‑gated)"
480
+ },
481
+ {
482
+ glob: "rm",
483
+ deny: true,
484
+ reason: "File deletion requires confirmation"
485
+ }
486
+ ];
487
+ /** 常见工具名映射(config.json 友好名 → 实际工具名)。 */
488
+ const TOOL_NAME_MAP = {
489
+ Bash: "bash",
490
+ Read: "read_file",
491
+ Write: "write_file",
492
+ Edit: "edit",
493
+ Glob: "glob",
494
+ Grep: "grep",
495
+ Web: "web_fetch",
496
+ WebSearch: "web_search",
497
+ Task: "task",
498
+ Ask: "ask_user_question"
499
+ };
500
+ /**
501
+ * 解析 config.json 权限条目。
502
+ *
503
+ * 格式:
504
+ * "ToolName" → { glob: "tool_name", allow: true/false }
505
+ * "ToolName(args)" → { glob: "tool_name", args: "args", allow: true/false }
506
+ *
507
+ * 友好名(如 Bash、Read)会自动映射为实际工具名。
508
+ */
509
+ function parsePermissionEntry(raw, allow) {
510
+ const match = raw.match(/^(\w+)(?:\((.+)\))?$/);
511
+ if (!match) return null;
512
+ const friendlyName = match[1];
513
+ const toolName = TOOL_NAME_MAP[friendlyName] ?? friendlyName.toLowerCase();
514
+ const args = match[2]?.trim();
515
+ const rule = {
516
+ glob: toolName,
517
+ allow
518
+ };
519
+ if (args) rule.args = args;
520
+ return rule;
521
+ }
522
+ /**
523
+ * 从 config.json 的 permissions.allow/deny 解析权限规则。
524
+ * 返回解析出的规则数组;解析失败静默跳过。
525
+ */
526
+ function parseConfigPermissions(raw) {
527
+ if (!raw || typeof raw !== "object") return [];
528
+ const perms = raw;
529
+ const allowEntries = Array.isArray(perms.allow) ? perms.allow : [];
530
+ const denyEntries = Array.isArray(perms.deny) ? perms.deny : [];
531
+ const rules = [];
532
+ for (const entry of allowEntries) {
533
+ const rule = parsePermissionEntry(entry, true);
534
+ if (rule) rules.push(rule);
535
+ }
536
+ for (const entry of denyEntries) {
537
+ const rule = parsePermissionEntry(entry, false);
538
+ if (rule) rules.push(rule);
539
+ }
540
+ return rules;
541
+ }
542
+ /**
543
+ * Create a rules loader that populates a RuleEngine from
544
+ * configuration sources.
545
+ *
546
+ * Call load() for each source at startup, then reload() if
547
+ * the user edits their settings mid‑session.
548
+ */
549
+ function createRulesLoader(engine) {
550
+ const loader = {
551
+ load(source, rules) {
552
+ for (const rule of rules) engine.add(source, rule);
553
+ },
554
+ reload(source, rules) {
555
+ engine.clearSource(source);
556
+ loader.load(source, rules);
557
+ }
558
+ };
559
+ engine.clearSource("builtin");
560
+ for (const rule of BUILTIN_RULES) engine.add("builtin", rule);
561
+ return loader;
562
+ }
563
+ /**
564
+ * Load permission rules from the user's config directory.
565
+ *
566
+ * Reads two files:
567
+ * - ~/.lynx/settings.json → `permissions.rules` array
568
+ * - <workspace>/.lynx/permissions.json → top‑level rules array
569
+ *
570
+ * Parsing errors are silently ignored (don't block startup).
571
+ *
572
+ * @returns Number of rules loaded, or 0 if no files found.
573
+ */
574
+ function loadFromDisk(loader, workspace) {
575
+ let loaded = 0;
576
+ const userSettingsPath = join(homedir(), ".lynx", "settings.json");
577
+ try {
578
+ if (existsSync(userSettingsPath)) {
579
+ const raw = readFileSync(userSettingsPath, "utf-8");
580
+ const rules = JSON.parse(raw)?.permissions?.rules;
581
+ if (Array.isArray(rules) && rules.length > 0) {
582
+ loader.reload("user", rules);
583
+ loaded += rules.length;
584
+ }
585
+ }
586
+ } catch {}
587
+ const configPath = join(homedir(), ".lynx", "config.json");
588
+ try {
589
+ if (existsSync(configPath)) {
590
+ const raw = readFileSync(configPath, "utf-8");
591
+ const configRules = parseConfigPermissions(JSON.parse(raw).permissions ?? {});
592
+ if (configRules.length > 0) {
593
+ loader.reload("config", configRules);
594
+ loaded += configRules.length;
595
+ }
596
+ }
597
+ } catch {}
598
+ if (workspace) {
599
+ const projectSettingsPath = join(workspace, ".lynx", "permissions.json");
600
+ try {
601
+ if (existsSync(projectSettingsPath)) {
602
+ const raw = readFileSync(projectSettingsPath, "utf-8");
603
+ const rules = JSON.parse(raw)?.rules;
604
+ if (Array.isArray(rules) && rules.length > 0) {
605
+ loader.reload("workspace", rules);
606
+ loaded += rules.length;
607
+ }
608
+ }
609
+ } catch {}
610
+ }
611
+ return loaded;
612
+ }
613
+ //#endregion
614
+ //#region src/mcp-channel.ts
615
+ /**
616
+ * 默认策略 — 无限制(所有频道可访问所有 MCP 工具)。
617
+ *
618
+ * 适用于内部工具和未明确配置频道的场景。
619
+ */
620
+ const DEFAULT_MCP_CHANNEL_POLICY = [];
621
+ /**
622
+ * 创建 MCP 频道权限检查器。
623
+ *
624
+ * 返回一个函数:给定频道名和工具名,返回是否允许及原因。
625
+ * 检查逻辑:
626
+ * 1. 如果 channel 为 undefined,使用默认策略(全部允许)
627
+ * 2. 查找匹配 channelName 的策略
628
+ * 3. 未找到匹配策略 → 回退到第一个匹配的频道或拒绝
629
+ * 4. 找到匹配策略 → 检查 server 是否在 allowedServers 中
630
+ * 5. 检查 toolName 是否在 allowedTools 中(或 allowedTools 包含 "*")
631
+ * 6. 全部通过 → 返回 allowed: true,附带是否需要批准的信息
632
+ *
633
+ * @param policies - 频道权限策略列表
634
+ * @returns 权限检查函数
635
+ */
636
+ function createMcpChannelChecker(policies) {
637
+ return function checkChannel(channel, toolName, serverName) {
638
+ if (!channel) return {
639
+ allowed: true,
640
+ reason: "无频道限制(channel 未指定)"
641
+ };
642
+ const policy = policies.find((p) => p.channelName === channel);
643
+ if (!policy) return {
644
+ allowed: false,
645
+ reason: `频道 "${channel}" 未配置 MCP 权限策略`
646
+ };
647
+ if (serverName && policy.allowedServers.length > 0) {
648
+ if (!policy.allowedServers.some((s) => s === serverName || s === "*")) return {
649
+ allowed: false,
650
+ reason: `MCP 服务器 "${serverName}" 不在频道 "${channel}" 的允许列表中`
651
+ };
652
+ }
653
+ if (!policy.allowedTools.some((t) => t === toolName || t === "*")) return {
654
+ allowed: false,
655
+ reason: `工具 "${toolName}" 不在频道 "${channel}" 的允许列表中`
656
+ };
657
+ return {
658
+ allowed: true,
659
+ reason: `频道 "${channel}" 允许使用 "${toolName}"${policy.requireApproval ? "(需要用户批准)" : ""}`
660
+ };
661
+ };
662
+ }
663
+ //#endregion
664
+ export { DEFAULT_MCP_CHANNEL_POLICY, createDenialTracker, createMcpChannelChecker, createRuleEngine, createRulesLoader, createYoloClassifier, getModeBehavior, isValidMode, listModes, loadFromDisk, runPipeline };
665
+
666
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/modes.ts","../src/rules.ts","../src/pipeline.ts","../src/classifier.ts","../src/denial.ts","../src/sources.ts","../src/mcp-channel.ts"],"sourcesContent":["/**\n * Permission modes — the 6 operating modes that control how\n * tool execution permissions are resolved.\n *\n * default — ask user for every RequiresApproval tool\n * auto — LLM YOLO classifier decides, user can override\n * yolo — auto‑approve everything, max throughput\n * dontAsk — deny all RequiresApproval tools silently\n * headless — no TUI, auto‑deny interactive prompts\n * plan — read‑only mode, all mutating tools denied\n */\n\n// ── Types ────────────────────────────────────────────\n\nexport type PermissionMode = \"default\" | \"auto\" | \"yolo\" | \"dontAsk\" | \"headless\" | \"plan\";\n\nexport interface ModeBehavior {\n /** Auto‑approve Safe tools without asking. */\n autoApproveSafe: boolean;\n /** Auto‑approve WorkspaceSafe tools. */\n autoApproveWorkspaceSafe: boolean;\n /** Auto‑deny Dangerous tools. */\n autoDenyDangerous: boolean;\n /** Use LLM classifier for RequiresApproval tools. */\n useClassifier: boolean;\n /** Show interactive prompts to the user. */\n showPrompts: boolean;\n /** Allow any file‑modifying tools. */\n allowWrites: boolean;\n}\n\n// ── Mode table ───────────────────────────────────────\n\nconst MODE_TABLE: Record<PermissionMode, ModeBehavior> = {\n default: {\n autoApproveSafe: true,\n autoApproveWorkspaceSafe: false,\n autoDenyDangerous: true,\n useClassifier: false,\n showPrompts: true,\n allowWrites: true,\n },\n auto: {\n autoApproveSafe: true,\n autoApproveWorkspaceSafe: true,\n autoDenyDangerous: true,\n useClassifier: true,\n showPrompts: false,\n allowWrites: true,\n },\n yolo: {\n autoApproveSafe: true,\n autoApproveWorkspaceSafe: true,\n autoDenyDangerous: false,\n useClassifier: false,\n showPrompts: false,\n allowWrites: true,\n },\n dontAsk: {\n autoApproveSafe: true,\n autoApproveWorkspaceSafe: false,\n autoDenyDangerous: true,\n useClassifier: false,\n showPrompts: false,\n allowWrites: false,\n },\n headless: {\n autoApproveSafe: true,\n autoApproveWorkspaceSafe: false,\n autoDenyDangerous: true,\n useClassifier: false,\n showPrompts: false,\n allowWrites: true,\n },\n plan: {\n autoApproveSafe: true,\n autoApproveWorkspaceSafe: false,\n autoDenyDangerous: true,\n useClassifier: false,\n showPrompts: false,\n allowWrites: false,\n },\n};\n\n// ── Public API ───────────────────────────────────────\n\n/** Get the behavior profile for a given mode. */\nexport function getModeBehavior(mode: PermissionMode): ModeBehavior {\n return MODE_TABLE[mode];\n}\n\n/** List all valid mode names. */\nexport function listModes(): PermissionMode[] {\n return Object.keys(MODE_TABLE) as PermissionMode[];\n}\n\n/** Validate that a string is a recognized mode. */\nexport function isValidMode(value: string): value is PermissionMode {\n return value in MODE_TABLE;\n}\n","/**\n * Permission rule matching engine.\n *\n * Rules are evaluated in priority order from multiple sources:\n * 1. Session overrides (highest)\n * 2. User settings\n * 3. Project settings\n * 4. Workspace settings\n * 5. Plugin defaults\n * 6. Built‑in defaults\n * 7. Hardcoded blocklist (lowest — always evaluated first)\n *\n * Each rule has a glob pattern that matches tool names.\n * When multiple rules match, higher‑priority sources win.\n */\n\nimport type { CommandSafety } from \"@lynx/tools\";\n\n// ── Types ────────────────────────────────────────────\n\nexport interface PermissionRule {\n /** Glob pattern matching tool names (e.g. \"bash\", \"read_*\", \"*\"). */\n glob: string;\n /** Optional argument pattern for tool‑specific rules (e.g. \"git push *\"). */\n args?: string;\n /** Override the safety classification for matched tools. */\n safety?: CommandSafety;\n /** Explicitly allow matched tools. */\n allow?: boolean;\n /** Explicitly deny matched tools. */\n deny?: boolean;\n /** Human‑readable reason shown to the user. */\n reason?: string;\n}\n\nexport interface RuleMatch {\n rule: PermissionRule;\n source: RuleSource;\n}\n\nexport type RuleSource =\n | \"blocklist\" // 0 — hardcoded, always deny\n | \"builtin\" // 1 — built‑in defaults\n | \"plugin\" // 2 — plugin‑provided rules\n | \"config\" // 3 — ~/.lynx/config.json permissions.allow/deny\n | \"workspace\" // 4 — .lynx/permissions.json in the workspace\n | \"project\" // 5 — CLAUDE.md project settings\n | \"user\" // 6 — ~/.lynx/settings.json\n | \"session\"; // 7 — runtime session override\n\n/** Priority weight — higher number = higher priority. */\nconst SOURCE_PRIORITY: Record<RuleSource, number> = {\n blocklist: 0,\n builtin: 1,\n plugin: 2,\n config: 3,\n workspace: 4,\n project: 5,\n user: 6,\n session: 7,\n};\n\n// ── Glob matching ────────────────────────────────────\n\nfunction globMatch(pattern: string, value: string): boolean {\n // Simple wildcard matching: * matches any sequence, ? matches one char\n const regex = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\");\n return new RegExp(`^${regex}$`).test(value);\n}\n\n// ── Public API ───────────────────────────────────────\n\nexport interface RuleEngine {\n /** Register a rule from a specific source. */\n add(source: RuleSource, rule: PermissionRule): void;\n\n /** Remove all rules from a source. */\n clearSource(source: RuleSource): void;\n\n /** Find all rules that match a tool name, sorted by priority (highest first). */\n match(toolName: string): RuleMatch[];\n\n /** Check if any matching rule explicitly denies the tool. */\n isDenied(toolName: string): boolean;\n\n /** Check if any matching rule explicitly allows the tool. */\n isAllowed(toolName: string): boolean;\n}\n\n/**\n * Create a rule matching engine.\n *\n * Rules are evaluated lazily — each match() call re‑scans\n * the rule set for the given tool name.\n */\nexport function createRuleEngine(): RuleEngine {\n const rulesBySource = new Map<RuleSource, PermissionRule[]>();\n\n // Hardcoded blocklist — always denied\n const BLOCKLIST: PermissionRule[] = [\n { glob: \"sudo\", deny: true, reason: \"Privilege escalation is forbidden\" },\n { glob: \"su\", deny: true, reason: \"Privilege escalation is forbidden\" },\n { glob: \"doas\", deny: true, reason: \"Privilege escalation is forbidden\" },\n ];\n\n // Register the blocklist immediately\n for (const rule of BLOCKLIST) {\n const existing = rulesBySource.get(\"blocklist\") ?? [];\n existing.push(rule);\n rulesBySource.set(\"blocklist\", existing);\n }\n\n const engine: RuleEngine = {\n add(source: RuleSource, rule: PermissionRule): void {\n const existing = rulesBySource.get(source) ?? [];\n existing.push(rule);\n rulesBySource.set(source, existing);\n },\n\n clearSource(source: RuleSource): void {\n rulesBySource.delete(source);\n },\n\n match(toolName: string): RuleMatch[] {\n const matches: RuleMatch[] = [];\n\n for (const [source, rules] of rulesBySource) {\n for (const rule of rules) {\n if (globMatch(rule.glob, toolName)) {\n matches.push({ rule, source });\n }\n }\n }\n\n // Sort by source priority (highest first)\n matches.sort((a, b) => SOURCE_PRIORITY[b.source] - SOURCE_PRIORITY[a.source]);\n\n return matches;\n },\n\n isDenied(toolName: string): boolean {\n const matches = this.match(toolName);\n // Higher‑priority source wins\n if (matches.length === 0) return false;\n const top = matches[0];\n return top.rule.deny === true;\n },\n\n isAllowed(toolName: string): boolean {\n const matches = this.match(toolName);\n if (matches.length === 0) return false;\n const top = matches[0];\n return top.rule.allow === true;\n },\n };\n\n return engine;\n}\n","/**\n * 14‑step permission pipeline — evaluates every tool invocation\n * through a sequence of checks, short‑circuiting on the first\n * definitive answer.\n *\n * Steps:\n * 1. Tool not found in registry → deny\n * 2. Tool is Safe → auto‑approve\n * 3. Tool is Dangerous → auto‑deny (unless yolo mode)\n * 4. Rule engine explicit deny → deny\n * 5. Rule engine explicit allow → allow\n * 6. Mode behavior check (plan mode denies writes, etc.)\n * 7. Denial tracker circuit breaker → deny\n * 8. Mode: yolo → allow\n * 9. Mode: dontAsk → deny\n * 10. Mode: headless → deny (no interactive TUI)\n * 11. Mode: auto → LLM classifier\n * 12. Mode: default → ask user\n * 13. User response → allow/deny\n * 14. Default: deny\n */\n\nimport type { ToolDescriptor, ToolInvocation } from \"@lynx/tools\";\nimport type { PermissionMode, ModeBehavior } from \"./modes.js\";\nimport type { RuleEngine } from \"./rules.js\";\nimport type { DenialTracker } from \"./denial.js\";\n\n// ── Types ────────────────────────────────────────────\n\nexport interface PipelineContext {\n mode: PermissionMode;\n behavior: ModeBehavior;\n ruleEngine: RuleEngine;\n denialTracker: DenialTracker;\n /** User callback — returns true to allow, false to deny. */\n askUser?: (invocation: ToolInvocation) => Promise<boolean>;\n /** LLM classifier callback — returns true to allow. */\n classify?: (invocation: ToolInvocation) => Promise<boolean>;\n}\n\nexport interface PipelineResult {\n allowed: boolean;\n reason: string;\n step: number;\n /** If true, the result was determined without user interaction. */\n automatic: boolean;\n}\n\n// ── Step function type ────────────────────────────────\n\n/** A single pipeline step. Returns a result to short‑circuit, or null to continue. */\ntype StepFn = (\n tool: ToolDescriptor,\n invocation: ToolInvocation,\n ctx: PipelineContext,\n) => Promise<PipelineResult | null> | PipelineResult | null;\n\n// ── Step implementations ──────────────────────────────\n\nfunction step1_unknownTool(tool: ToolDescriptor): PipelineResult | null {\n if (!tool) {\n return { allowed: false, reason: \"未知工具\", step: 1, automatic: true };\n }\n return null;\n}\n\nfunction step2_safeTool(tool: ToolDescriptor): PipelineResult | null {\n if (tool.safety === \"Safe\") {\n return { allowed: true, reason: \"安全工具 — 始终允许\", step: 2, automatic: true };\n }\n return null;\n}\n\nfunction step3_dangerousTool(\n tool: ToolDescriptor,\n _inv: ToolInvocation,\n ctx: PipelineContext,\n): PipelineResult | null {\n if (tool.safety === \"Dangerous\" && ctx.behavior.autoDenyDangerous) {\n return { allowed: false, reason: \"危险工具 — 已自动拒绝\", step: 3, automatic: true };\n }\n return null;\n}\n\nfunction step4_ruleDeny(\n _tool: ToolDescriptor,\n invocation: ToolInvocation,\n ctx: PipelineContext,\n): PipelineResult | null {\n if (ctx.ruleEngine.isDenied(invocation.toolName)) {\n return { allowed: false, reason: \"被权限规则拒绝\", step: 4, automatic: true };\n }\n return null;\n}\n\nfunction step5_ruleAllow(\n _tool: ToolDescriptor,\n invocation: ToolInvocation,\n ctx: PipelineContext,\n): PipelineResult | null {\n if (ctx.ruleEngine.isAllowed(invocation.toolName)) {\n return { allowed: true, reason: \"被权限规则允许\", step: 5, automatic: true };\n }\n return null;\n}\n\nfunction step6_planModeDeniesWrites(\n tool: ToolDescriptor,\n _inv: ToolInvocation,\n ctx: PipelineContext,\n): PipelineResult | null {\n if (ctx.mode === \"plan\" && tool.kind !== \"ReadOnly\") {\n return {\n allowed: false,\n reason: `写入工具 \"${tool.name}\" 在计划模式下被拒绝`,\n step: 6,\n automatic: true,\n };\n }\n return null;\n}\n\nfunction step7_circuitBreaker(\n _tool: ToolDescriptor,\n _inv: ToolInvocation,\n ctx: PipelineContext,\n): PipelineResult | null {\n if (ctx.denialTracker.isTripped()) {\n return {\n allowed: false,\n reason: \"熔断器已触发 — 自动拒绝\",\n step: 7,\n automatic: true,\n };\n }\n return null;\n}\n\nfunction step8_yolo(\n _tool: ToolDescriptor,\n _inv: ToolInvocation,\n ctx: PipelineContext,\n): PipelineResult | null {\n if (ctx.mode === \"yolo\") {\n return { allowed: true, reason: \"YOLO 模式 — 已自动批准\", step: 8, automatic: true };\n }\n return null;\n}\n\nfunction step9_dontAsk(\n _tool: ToolDescriptor,\n _inv: ToolInvocation,\n ctx: PipelineContext,\n): PipelineResult | null {\n if (ctx.mode === \"dontAsk\") {\n return { allowed: false, reason: \"免问模式 — 已自动拒绝\", step: 9, automatic: true };\n }\n return null;\n}\n\nfunction step10_headless(\n _tool: ToolDescriptor,\n _inv: ToolInvocation,\n ctx: PipelineContext,\n): PipelineResult | null {\n if (ctx.mode === \"headless\") {\n return {\n allowed: false,\n reason: \"无头模式 — 无交互式提示\",\n step: 10,\n automatic: true,\n };\n }\n return null;\n}\n\nasync function step11_autoClassify(\n _tool: ToolDescriptor,\n invocation: ToolInvocation,\n ctx: PipelineContext,\n): Promise<PipelineResult | null> {\n if (ctx.mode !== \"auto\" || !ctx.classify) return null;\n try {\n const allowed = await ctx.classify(invocation);\n return {\n allowed,\n reason: allowed ? \"自动分类为安全\" : \"自动分类为不安全\",\n step: 11,\n automatic: true,\n };\n } catch {\n return null; // Classifier failed — fall through\n }\n}\n\nasync function step12_defaultAskUser(\n _tool: ToolDescriptor,\n invocation: ToolInvocation,\n ctx: PipelineContext,\n): Promise<PipelineResult | null> {\n if (ctx.mode !== \"default\") return null;\n if (!ctx.askUser) {\n return {\n allowed: false,\n reason: \"默认模式 — 无用户提示可用\",\n step: 12,\n automatic: true,\n };\n }\n try {\n const allowed = await ctx.askUser(invocation);\n return {\n allowed,\n reason: allowed ? \"用户已批准\" : \"用户已拒绝\",\n step: 13,\n automatic: false,\n };\n } catch {\n return { allowed: false, reason: \"用户提示失败\", step: 13, automatic: false };\n }\n}\n\n/** 兜底步骤 — 始终返回结果。 */\nfunction step14_defaultDeny(): PipelineResult {\n return { allowed: false, reason: \"默认拒绝 — 无规则匹配\", step: 14, automatic: true };\n}\n\n// ── Ordered pipeline ──────────────────────────────────\n\nconst PIPELINE: StepFn[] = [\n step1_unknownTool,\n step2_safeTool,\n step3_dangerousTool,\n step4_ruleDeny,\n step5_ruleAllow,\n step6_planModeDeniesWrites,\n step7_circuitBreaker,\n step8_yolo,\n step9_dontAsk,\n step10_headless,\n step11_autoClassify,\n step12_defaultAskUser,\n step14_defaultDeny,\n];\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * Run a tool invocation through the 14‑step permission pipeline.\n *\n * Each step is an independent function. The pipeline short‑circuits\n * on the first non‑null result.\n */\nexport async function runPipeline(\n tool: ToolDescriptor,\n invocation: ToolInvocation,\n ctx: PipelineContext,\n): Promise<PipelineResult> {\n for (const step of PIPELINE) {\n const result = await step(tool, invocation, ctx);\n if (result) return result;\n }\n // 不可达 — step14 始终返回结果\n return { allowed: false, reason: \"默认拒绝 — 无规则匹配\", step: 14, automatic: true };\n}\n","/**\n * YOLO classifier — a lightweight LLM‑based permission decider\n * for \"auto\" mode. Uses a ~200‑token prompt and expects a\n * yes/no response within 500ms.\n *\n * Pattern (inspired by FengMing's `classifyPermissionTool` +\n * Claude Code's auto‑mode classifier):\n * 1. Check cache → return immediately if hit\n * 2. Call fast LLM with compact prompt → allow/deny\n * 3. On timeout or error → fall back to heuristic\n *\n * The classifier is stateless — each call is independent.\n * It should NOT be used for Dangerous tools (those are\n * auto‑denied regardless of classifier output).\n */\n\nimport type { ToolInvocation, ToolDescriptor } from \"@lynx/tools\";\n\n// ── Types ────────────────────────────────────────────\n\nexport interface YoloClassifier {\n /**\n * Ask the classifier whether a tool invocation should be allowed.\n *\n * Returns true if the invocation looks safe, false otherwise.\n * Never throws — on error, falls back to a conservative heuristic.\n */\n classify(tool: ToolDescriptor, invocation: ToolInvocation): Promise<boolean>;\n}\n\n/** LLM completion function signature — injected via DI. */\nexport type ClassifyLlmCall = (prompt: string) => Promise<string>;\n\n/** Options for creating the classifier. */\nexport interface CreateYoloClassifierOptions {\n /**\n * Optional LLM completion function.\n *\n * When provided, the classifier sends a ~200‑token prompt to a\n * fast model (e.g. Haiku / Flash) and parses \"allow\" / \"deny\".\n * When omitted, falls back to the heuristic classifier only.\n */\n llmCall?: ClassifyLlmCall;\n\n /** Timeout in milliseconds for the LLM call. Default 500. */\n timeoutMs?: number;\n}\n\n// ── Constants ────────────────────────────────────────\n\nconst DEFAULT_TIMEOUT_MS = 500;\n\n/** Compact prompt template (~200 tokens). */\nconst CLASSIFIER_PROMPT = `You are a security classifier. Given a tool invocation, respond with ONLY \"allow\" or \"deny\".\n\nRules:\n- Read-only operations (read_file, ls, grep, search) → allow\n- File writes within the workspace → allow\n- Network requests → allow\n- Shell commands that modify system state → deny\n- Commands that delete files outside the workspace → deny\n- Commands that install packages → deny\n- Escalation commands (sudo, su) → deny\n\nTool: {toolName}\nDescription: {toolDescription}\nInput: {toolInput}\n\nResponse:`;\n\n// ── Heuristic fallback ───────────────────────────────\n\n/** Patterns that always result in deny. */\nconst DENY_PATTERNS = [\n \"sudo\",\n \"rm -rf /\",\n \"chmod 777\",\n \"DROP TABLE\",\n \"DELETE FROM\",\n \"shutdown\",\n \"reboot\",\n \"format\",\n \"mkfs\",\n \"curl | bash\",\n \"wget -O - |\",\n \"> /dev/sda\",\n \"dd if=\",\n] as const;\n\n/** Patterns that indicate safe dev workflows. */\nconst ALLOW_PATTERNS = [\n \"npm test\",\n \"npm run\",\n \"pnpm test\",\n \"pnpm build\",\n \"git \",\n \"node \",\n \"tsc\",\n \"vitest\",\n \"eslint\",\n \"prettier\",\n] as const;\n\nfunction heuristicClassify(tool: ToolDescriptor, invocation: ToolInvocation): boolean {\n // ReadOnly tools are always safe\n if (tool.kind === \"ReadOnly\") return true;\n\n // Check for dangerous patterns in the payload\n const payload = JSON.stringify(invocation.payload).toLowerCase();\n\n for (const pattern of DENY_PATTERNS) {\n if (payload.includes(pattern.toLowerCase())) {\n return false;\n }\n }\n\n for (const pattern of ALLOW_PATTERNS) {\n if (payload.includes(pattern.toLowerCase())) {\n return true;\n }\n }\n\n // Conservative default\n return tool.safety === \"Safe\";\n}\n\n// ── Cache ────────────────────────────────────────────\n\n/** Cache key derived from tool identity + input hash. */\nfunction cacheKey(tool: ToolDescriptor, invocation: ToolInvocation): string {\n const inputHash = JSON.stringify(invocation.payload);\n return `${tool.name}:${tool.safety}:${inputHash}`;\n}\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * Create a YOLO classifier.\n *\n * When `llmCall` is provided, prompts a fast LLM to decide each\n * classification. A cache avoids redundant calls for identical\n * (tool, safety, args) combinations within the same session.\n *\n * Falls back to a heuristic classifier on timeout or error.\n */\nexport function createYoloClassifier(opts: CreateYoloClassifierOptions = {}): YoloClassifier {\n const { llmCall, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;\n\n // Session‑scoped cache\n const cache = new Map<string, boolean>();\n\n const classifier: YoloClassifier = {\n async classify(tool: ToolDescriptor, invocation: ToolInvocation): Promise<boolean> {\n // 1. Check cache\n const key = cacheKey(tool, invocation);\n const cached = cache.get(key);\n if (cached !== undefined) return cached;\n\n // 2. Try LLM classifier\n if (llmCall) {\n try {\n const prompt = CLASSIFIER_PROMPT.replace(\"{toolName}\", tool.name)\n .replace(\"{toolDescription}\", tool.description)\n .replace(\"{toolInput}\", JSON.stringify(invocation.payload));\n\n const raw = await Promise.race([\n llmCall(prompt),\n new Promise<string>((_, reject) =>\n setTimeout(() => reject(new Error(\"Classifier timeout\")), timeoutMs),\n ),\n ]);\n\n const decision = raw.trim().toLowerCase().startsWith(\"allow\");\n cache.set(key, decision);\n return decision;\n } catch {\n // Timeout or LLM error → fall through to heuristic\n }\n }\n\n // 3. Heuristic fallback\n const decision = heuristicClassify(tool, invocation);\n cache.set(key, decision);\n return decision;\n },\n };\n\n return classifier;\n}\n","/**\n * Denial tracker with circuit breaker.\n *\n * When a user denies 3 consecutive tool requests within the same\n * turn, the circuit breaker trips and auto‑denies all subsequent\n * requests for the rest of that turn.\n *\n * The breaker resets at the start of each new turn.\n */\n\n// ── Types ────────────────────────────────────────────\n\nexport interface DenialTracker {\n /** Record a denial from the user. */\n recordDenial(): void;\n /** Record an approval (resets the consecutive count). */\n recordApproval(): void;\n /** Check whether the circuit breaker has tripped. */\n isTripped(): boolean;\n /** Reset the tracker for a new turn. */\n resetTurn(): void;\n /** Current consecutive denial count. */\n readonly consecutiveDenials: number;\n}\n\n// ── Constants ────────────────────────────────────────\n\n/** Number of consecutive denials before the breaker trips. */\nconst DENIAL_THRESHOLD = 3;\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * Create a denial tracker.\n *\n * One instance per session — it tracks user behavior across\n * multiple tool invocations within a single turn.\n */\nexport function createDenialTracker(): DenialTracker {\n let consecutive = 0;\n\n const tracker: DenialTracker = {\n get consecutiveDenials() {\n return consecutive;\n },\n\n recordDenial(): void {\n consecutive++;\n },\n\n recordApproval(): void {\n consecutive = 0;\n },\n\n isTripped(): boolean {\n return consecutive >= DENIAL_THRESHOLD;\n },\n\n resetTurn(): void {\n consecutive = 0;\n },\n };\n\n return tracker;\n}\n","/**\n * Permission rule source loading — reads rules from each of the\n * 7 priority layers and feeds them into the RuleEngine.\n *\n * Sources (lowest to highest priority):\n * 0. blocklist — hardcoded (loaded by RuleEngine at construction)\n * 1. builtin — default safety rules\n * 2. plugin — plugin‑declared rules\n * 3. config — ~/.lynx/config.json permissions.allow/deny\n * 4. workspace — .lynx/permissions.json in the session workspace\n * 5. project — directive files (CLAUDE.md)\n * 6. user — ~/.lynx/settings.json\n * 7. session — runtime overrides (set via /permissions command)\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { RuleEngine, PermissionRule, RuleSource } from \"./rules.js\";\n\n// ── Types ────────────────────────────────────────────\n\nexport interface RulesLoader {\n /** Load all rules from a source into the engine. */\n load(source: RuleSource, rules: PermissionRule[]): void;\n /** Reload rules from a source (clears previous entries). */\n reload(source: RuleSource, rules: PermissionRule[]): void;\n}\n\n// ── Built‑in defaults ────────────────────────────────\n\nconst BUILTIN_RULES: PermissionRule[] = [\n { glob: \"read_file\", allow: true, reason: \"Read‑only file access\" },\n { glob: \"write_file\", allow: true, reason: \"Workspace‑scoped file write\" },\n { glob: \"glob\", allow: true, reason: \"File pattern matching\" },\n { glob: \"grep\", allow: true, reason: \"Content search\" },\n { glob: \"ls\", allow: true, reason: \"Directory listing\" },\n { glob: \"bash\", allow: true, reason: \"Shell access (permission‑gated)\" },\n { glob: \"rm\", deny: true, reason: \"File deletion requires confirmation\" },\n];\n\n// ── Permission string parser ────────────────────────\n\n/** 常见工具名映射(config.json 友好名 → 实际工具名)。 */\nconst TOOL_NAME_MAP: Record<string, string> = {\n Bash: \"bash\",\n Read: \"read_file\",\n Write: \"write_file\",\n Edit: \"edit\",\n Glob: \"glob\",\n Grep: \"grep\",\n Web: \"web_fetch\",\n WebSearch: \"web_search\",\n Task: \"task\",\n Ask: \"ask_user_question\",\n};\n\n/**\n * 解析 config.json 权限条目。\n *\n * 格式:\n * \"ToolName\" → { glob: \"tool_name\", allow: true/false }\n * \"ToolName(args)\" → { glob: \"tool_name\", args: \"args\", allow: true/false }\n *\n * 友好名(如 Bash、Read)会自动映射为实际工具名。\n */\nfunction parsePermissionEntry(raw: string, allow: boolean): PermissionRule | null {\n const match = raw.match(/^(\\w+)(?:\\((.+)\\))?$/);\n if (!match) return null;\n\n const friendlyName = match[1]!;\n const toolName = TOOL_NAME_MAP[friendlyName] ?? friendlyName.toLowerCase();\n const args = match[2]?.trim();\n\n const rule: PermissionRule = { glob: toolName, allow };\n if (args) rule.args = args;\n return rule;\n}\n\n/**\n * 从 config.json 的 permissions.allow/deny 解析权限规则。\n * 返回解析出的规则数组;解析失败静默跳过。\n */\nfunction parseConfigPermissions(raw: unknown): PermissionRule[] {\n if (!raw || typeof raw !== \"object\") return [];\n\n const perms = raw as Record<string, unknown>;\n const allowEntries = Array.isArray(perms.allow) ? (perms.allow as string[]) : [];\n const denyEntries = Array.isArray(perms.deny) ? (perms.deny as string[]) : [];\n\n const rules: PermissionRule[] = [];\n for (const entry of allowEntries) {\n const rule = parsePermissionEntry(entry, true);\n if (rule) rules.push(rule);\n }\n for (const entry of denyEntries) {\n const rule = parsePermissionEntry(entry, false);\n if (rule) rules.push(rule);\n }\n return rules;\n}\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * Create a rules loader that populates a RuleEngine from\n * configuration sources.\n *\n * Call load() for each source at startup, then reload() if\n * the user edits their settings mid‑session.\n */\nexport function createRulesLoader(engine: RuleEngine): RulesLoader {\n const loader: RulesLoader = {\n load(source: RuleSource, rules: PermissionRule[]): void {\n for (const rule of rules) {\n engine.add(source, rule);\n }\n },\n\n reload(source: RuleSource, rules: PermissionRule[]): void {\n engine.clearSource(source);\n loader.load(source, rules);\n },\n };\n\n // Load built‑in defaults immediately\n engine.clearSource(\"builtin\");\n for (const rule of BUILTIN_RULES) {\n engine.add(\"builtin\", rule);\n }\n\n return loader;\n}\n\n/**\n * Load permission rules from the user's config directory.\n *\n * Reads two files:\n * - ~/.lynx/settings.json → `permissions.rules` array\n * - <workspace>/.lynx/permissions.json → top‑level rules array\n *\n * Parsing errors are silently ignored (don't block startup).\n *\n * @returns Number of rules loaded, or 0 if no files found.\n */\nexport function loadFromDisk(loader: RulesLoader, workspace?: string): number {\n let loaded = 0;\n\n // 1. User‑level settings (~/.lynx/settings.json)\n const userSettingsPath = join(homedir(), \".lynx\", \"settings.json\");\n try {\n if (existsSync(userSettingsPath)) {\n const raw = readFileSync(userSettingsPath, \"utf-8\");\n const settings = JSON.parse(raw) as {\n permissions?: { rules?: PermissionRule[] };\n };\n const rules = settings?.permissions?.rules;\n if (Array.isArray(rules) && rules.length > 0) {\n loader.reload(\"user\", rules);\n loaded += rules.length;\n }\n }\n } catch {\n // Corrupt file or permission denied — don't block startup\n }\n\n // 2. Config‑level permissions (~/.lynx/config.json → permissions.allow/deny)\n const configPath = join(homedir(), \".lynx\", \"config.json\");\n try {\n if (existsSync(configPath)) {\n const raw = readFileSync(configPath, \"utf-8\");\n const cfg = JSON.parse(raw) as { permissions?: { allow?: string[]; deny?: string[] } };\n const configRules = parseConfigPermissions(cfg.permissions ?? {});\n if (configRules.length > 0) {\n loader.reload(\"config\", configRules);\n loaded += configRules.length;\n }\n }\n } catch {\n // Corrupt config — don't block startup\n }\n\n // 3. Project‑level settings (<workspace>/.lynx/permissions.json)\n if (workspace) {\n const projectSettingsPath = join(workspace, \".lynx\", \"permissions.json\");\n try {\n if (existsSync(projectSettingsPath)) {\n const raw = readFileSync(projectSettingsPath, \"utf-8\");\n const settings = JSON.parse(raw) as { rules?: PermissionRule[] };\n const rules = settings?.rules;\n if (Array.isArray(rules) && rules.length > 0) {\n loader.reload(\"workspace\", rules);\n loaded += rules.length;\n }\n }\n } catch {\n // Corrupt file or permission denied — don't block startup\n }\n }\n\n return loaded;\n}\n","/**\n * MCP 频道权限 — 按频道限制可用的 MCP 服务器和工具。\n *\n * 每个频道可配置允许的服务器列表和工具列表,\n * 以及是否需要用户批准。未匹配频道时回退到默认策略。\n */\n\n// ── Types ────────────────────────────────────────────\n\n/** MCP 频道权限策略 — 每条规则绑定一个频道。 */\nexport interface McpChannelPolicy {\n /** 频道名称(\"wechat\"、\"feishu\"、\"web\" 等)。 */\n channelName: string;\n /** 允许的 MCP 服务器名称列表。 */\n allowedServers: string[];\n /** 允许的工具名列表(原始名称,非命名空间格式)。 */\n allowedTools: string[];\n /** 是否需要用户批准。 */\n requireApproval: boolean;\n}\n\n/** 权限检查结果。 */\nexport interface McpCheckResult {\n allowed: boolean;\n reason: string;\n}\n\n// ── Constants ──────────────────────────────────────────\n\n/**\n * 默认策略 — 无限制(所有频道可访问所有 MCP 工具)。\n *\n * 适用于内部工具和未明确配置频道的场景。\n */\nexport const DEFAULT_MCP_CHANNEL_POLICY: McpChannelPolicy[] = [];\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * 创建 MCP 频道权限检查器。\n *\n * 返回一个函数:给定频道名和工具名,返回是否允许及原因。\n * 检查逻辑:\n * 1. 如果 channel 为 undefined,使用默认策略(全部允许)\n * 2. 查找匹配 channelName 的策略\n * 3. 未找到匹配策略 → 回退到第一个匹配的频道或拒绝\n * 4. 找到匹配策略 → 检查 server 是否在 allowedServers 中\n * 5. 检查 toolName 是否在 allowedTools 中(或 allowedTools 包含 \"*\")\n * 6. 全部通过 → 返回 allowed: true,附带是否需要批准的信息\n *\n * @param policies - 频道权限策略列表\n * @returns 权限检查函数\n */\nexport function createMcpChannelChecker(\n policies: McpChannelPolicy[],\n): (channel: string | undefined, toolName: string, serverName?: string) => McpCheckResult {\n return function checkChannel(\n channel: string | undefined,\n toolName: string,\n serverName?: string,\n ): McpCheckResult {\n // 未指定频道 → 默认允许\n if (!channel) {\n return { allowed: true, reason: \"无频道限制(channel 未指定)\" };\n }\n\n // 查找匹配策略\n const policy = policies.find((p) => p.channelName === channel);\n\n if (!policy) {\n // 无匹配策略 — 默认拒绝(安全优先)\n return {\n allowed: false,\n reason: `频道 \"${channel}\" 未配置 MCP 权限策略`,\n };\n }\n\n // 检查服务器\n if (serverName && policy.allowedServers.length > 0) {\n const serverAllowed = policy.allowedServers.some((s) => s === serverName || s === \"*\");\n if (!serverAllowed) {\n return {\n allowed: false,\n reason: `MCP 服务器 \"${serverName}\" 不在频道 \"${channel}\" 的允许列表中`,\n };\n }\n }\n\n // 检查工具\n const toolAllowed = policy.allowedTools.some((t) => t === toolName || t === \"*\");\n if (!toolAllowed) {\n return {\n allowed: false,\n reason: `工具 \"${toolName}\" 不在频道 \"${channel}\" 的允许列表中`,\n };\n }\n\n const approvalNote = policy.requireApproval ? \"(需要用户批准)\" : \"\";\n return {\n allowed: true,\n reason: `频道 \"${channel}\" 允许使用 \"${toolName}\"${approvalNote}`,\n };\n };\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@24klynx/permissions",
3
+ "version": "0.1.0",
4
+ "description": "Permission system — 14-step pipeline, rule engine, YOLO classifier, denial tracker",
5
+ "type": "module",
6
+ "main": "./dist/index.mjs",
7
+ "types": "./dist/index.d.mts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "types": "./dist/index.d.mts"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@24klynx/core": "0.1.0",
16
+ "@24klynx/tools": "0.1.0"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "build": "tsdown --config-loader tsx",
26
+ "test": "vitest run --passWithNoTests",
27
+ "typecheck": "tsgo --noEmit"
28
+ }
29
+ }