@komarspn/pi-permission-system 16.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2234 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/config/config.example.json +39 -0
- package/package.json +82 -0
- package/schemas/permissions.schema.json +158 -0
- package/src/active-agent.ts +72 -0
- package/src/async-cache.ts +21 -0
- package/src/bash-arity.ts +210 -0
- package/src/builtin-tool-input-formatters.ts +82 -0
- package/src/canonicalize-path.ts +30 -0
- package/src/common.ts +121 -0
- package/src/config-loader.ts +432 -0
- package/src/config-modal.ts +259 -0
- package/src/config-paths.ts +47 -0
- package/src/config-reporter.ts +34 -0
- package/src/config-store.ts +222 -0
- package/src/decision-audit.ts +75 -0
- package/src/decision-reporter.ts +41 -0
- package/src/denial-messages.ts +232 -0
- package/src/expand-home.ts +28 -0
- package/src/extension-config.ts +79 -0
- package/src/extension-paths.ts +66 -0
- package/src/forwarded-permissions/io.ts +404 -0
- package/src/forwarded-permissions/permission-forwarder.ts +580 -0
- package/src/forwarding-manager.ts +74 -0
- package/src/gate-prompter.ts +12 -0
- package/src/handlers/before-agent-start.ts +94 -0
- package/src/handlers/gates/bash-command.ts +75 -0
- package/src/handlers/gates/bash-external-directory.ts +127 -0
- package/src/handlers/gates/bash-path-extractor.ts +15 -0
- package/src/handlers/gates/bash-path.ts +152 -0
- package/src/handlers/gates/bash-program.ts +1143 -0
- package/src/handlers/gates/bash-token-classification.ts +105 -0
- package/src/handlers/gates/candidate-check.ts +32 -0
- package/src/handlers/gates/descriptor.ts +81 -0
- package/src/handlers/gates/external-directory-messages.ts +20 -0
- package/src/handlers/gates/external-directory.ts +133 -0
- package/src/handlers/gates/helpers.ts +76 -0
- package/src/handlers/gates/path.ts +91 -0
- package/src/handlers/gates/runner.ts +186 -0
- package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
- package/src/handlers/gates/skill-input.ts +46 -0
- package/src/handlers/gates/skill-read.ts +87 -0
- package/src/handlers/gates/tool-call-gate-pipeline.ts +129 -0
- package/src/handlers/gates/tool.ts +102 -0
- package/src/handlers/gates/types.ts +13 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/lifecycle.ts +95 -0
- package/src/handlers/permission-gate-handler.ts +190 -0
- package/src/handlers/tool-call-boundary.ts +91 -0
- package/src/index.ts +225 -0
- package/src/input-normalizer.ts +157 -0
- package/src/logging.ts +113 -0
- package/src/mcp-targets.ts +170 -0
- package/src/node-modules-discovery.ts +76 -0
- package/src/normalize.ts +43 -0
- package/src/path-utils.ts +355 -0
- package/src/pattern-suggest.ts +132 -0
- package/src/permission-dialog.ts +138 -0
- package/src/permission-event-rpc.ts +223 -0
- package/src/permission-events.ts +266 -0
- package/src/permission-forwarding.ts +188 -0
- package/src/permission-gate.ts +94 -0
- package/src/permission-manager.ts +392 -0
- package/src/permission-merge.ts +32 -0
- package/src/permission-prompter.ts +142 -0
- package/src/permission-prompts.ts +93 -0
- package/src/permission-resolver.ts +109 -0
- package/src/permission-session.ts +189 -0
- package/src/permission-ui-prompt.ts +127 -0
- package/src/permissions-service.ts +63 -0
- package/src/persistent-approval-recorder.ts +139 -0
- package/src/policy-loader.ts +350 -0
- package/src/prompting-gateway.ts +104 -0
- package/src/rule.ts +188 -0
- package/src/scope-merge.ts +72 -0
- package/src/service-lifecycle.ts +49 -0
- package/src/service.ts +163 -0
- package/src/session-approval-recorder.ts +6 -0
- package/src/session-approval.ts +43 -0
- package/src/session-logger.ts +91 -0
- package/src/session-rules.ts +79 -0
- package/src/skill-prompt-sanitizer.ts +292 -0
- package/src/status.ts +35 -0
- package/src/subagent-context.ts +104 -0
- package/src/subagent-lifecycle-events.ts +72 -0
- package/src/subagent-registry.ts +105 -0
- package/src/synthesize.ts +92 -0
- package/src/system-prompt-sanitizer.ts +274 -0
- package/src/tool-access-extractor-registry.ts +68 -0
- package/src/tool-input-formatter-registry.ts +67 -0
- package/src/tool-input-preview.ts +34 -0
- package/src/tool-input-prompt-formatters.ts +63 -0
- package/src/tool-preview-formatter.ts +207 -0
- package/src/tool-registry.ts +148 -0
- package/src/types.ts +64 -0
- package/src/wildcard-matcher.ts +120 -0
- package/src/yolo-mode.ts +30 -0
- package/test/active-agent.test.ts +155 -0
- package/test/async-cache.test.ts +48 -0
- package/test/bash-arity.test.ts +144 -0
- package/test/bash-external-directory.test.ts +956 -0
- package/test/builtin-tool-input-formatters.test.ts +109 -0
- package/test/canonicalize-path.test.ts +93 -0
- package/test/common.test.ts +287 -0
- package/test/composition-root.test.ts +603 -0
- package/test/config-loader.test.ts +740 -0
- package/test/config-modal.test.ts +320 -0
- package/test/config-paths.test.ts +83 -0
- package/test/config-pipeline.test.ts +90 -0
- package/test/config-reporter.test.ts +147 -0
- package/test/config-store.test.ts +466 -0
- package/test/decision-audit.test.ts +72 -0
- package/test/decision-reporter.test.ts +112 -0
- package/test/denial-messages.test.ts +656 -0
- package/test/detect-permissive-bash-fallback.test.ts +56 -0
- package/test/expand-home.test.ts +93 -0
- package/test/extension-config.test.ts +129 -0
- package/test/extension-paths.test.ts +108 -0
- package/test/forwarded-permissions/io.test.ts +251 -0
- package/test/forwarding-manager.test.ts +194 -0
- package/test/handlers/before-agent-start.test.ts +317 -0
- package/test/handlers/external-directory-integration.test.ts +623 -0
- package/test/handlers/external-directory-session-dedup.test.ts +430 -0
- package/test/handlers/external-directory-symlink-acceptance.test.ts +149 -0
- package/test/handlers/gates/bash-command-metamorphic.test.ts +83 -0
- package/test/handlers/gates/bash-command.test.ts +191 -0
- package/test/handlers/gates/bash-external-directory.test.ts +269 -0
- package/test/handlers/gates/bash-path.test.ts +337 -0
- package/test/handlers/gates/bash-program.test.ts +410 -0
- package/test/handlers/gates/bash-token-classification.test.ts +241 -0
- package/test/handlers/gates/candidate-check.test.ts +52 -0
- package/test/handlers/gates/external-directory-messages.test.ts +61 -0
- package/test/handlers/gates/external-directory.test.ts +259 -0
- package/test/handlers/gates/helpers.test.ts +177 -0
- package/test/handlers/gates/path.test.ts +294 -0
- package/test/handlers/gates/runner.test.ts +447 -0
- package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
- package/test/handlers/gates/skill-input.test.ts +131 -0
- package/test/handlers/gates/skill-read.test.ts +158 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +252 -0
- package/test/handlers/gates/tool.test.ts +223 -0
- package/test/handlers/input-events.test.ts +168 -0
- package/test/handlers/input.test.ts +199 -0
- package/test/handlers/lifecycle.test.ts +221 -0
- package/test/handlers/tool-call-boundary.test.ts +145 -0
- package/test/handlers/tool-call-events.test.ts +277 -0
- package/test/handlers/tool-call.test.ts +395 -0
- package/test/handlers/validate-requested-tool.test.ts +92 -0
- package/test/helpers/gate-fixtures.ts +323 -0
- package/test/helpers/handler-fixtures.ts +335 -0
- package/test/helpers/make-fake-pi.ts +100 -0
- package/test/helpers/manager-harness.ts +112 -0
- package/test/helpers/session-fixtures.ts +204 -0
- package/test/input-normalizer.test.ts +367 -0
- package/test/logging.test.ts +51 -0
- package/test/mcp-targets.test.ts +233 -0
- package/test/node-modules-discovery.test.ts +97 -0
- package/test/normalize.test.ts +247 -0
- package/test/path-utils.test.ts +650 -0
- package/test/pattern-suggest.test.ts +248 -0
- package/test/permission-dialog.test.ts +241 -0
- package/test/permission-event-rpc.test.ts +541 -0
- package/test/permission-events.test.ts +402 -0
- package/test/permission-forwarder.test.ts +369 -0
- package/test/permission-forwarding.test.ts +315 -0
- package/test/permission-gate.test.ts +305 -0
- package/test/permission-manager-unified.test.ts +3368 -0
- package/test/permission-merge.test.ts +61 -0
- package/test/permission-prompter.test.ts +518 -0
- package/test/permission-prompts.test.ts +363 -0
- package/test/permission-resolver.test.ts +265 -0
- package/test/permission-session.test.ts +363 -0
- package/test/permission-ui-prompt.test.ts +146 -0
- package/test/permissions-service.test.ts +177 -0
- package/test/persistent-approval-recorder.test.ts +133 -0
- package/test/pi-infrastructure-read.test.ts +369 -0
- package/test/policy-loader.test.ts +561 -0
- package/test/prompting-gateway.test.ts +230 -0
- package/test/rule.test.ts +604 -0
- package/test/scope-merge.test.ts +116 -0
- package/test/service-lifecycle.test.ts +163 -0
- package/test/service.test.ts +308 -0
- package/test/session-approval.test.ts +75 -0
- package/test/session-logger.test.ts +200 -0
- package/test/session-rules.test.ts +304 -0
- package/test/session-start.test.ts +112 -0
- package/test/skill-prompt-sanitizer.test.ts +374 -0
- package/test/status.test.ts +10 -0
- package/test/subagent-context.test.ts +326 -0
- package/test/subagent-lifecycle-events.test.ts +132 -0
- package/test/subagent-registry.test.ts +145 -0
- package/test/synthesize.test.ts +300 -0
- package/test/system-prompt-sanitizer.test.ts +382 -0
- package/test/tool-access-extractor-registry.test.ts +77 -0
- package/test/tool-input-formatter-registry.test.ts +75 -0
- package/test/tool-input-preview.test.ts +129 -0
- package/test/tool-input-prompt-formatters.test.ts +115 -0
- package/test/tool-preview-formatter.test.ts +458 -0
- package/test/tool-registry.test.ts +197 -0
- package/test/wildcard-matcher.test.ts +424 -0
- package/test/yolo-mode.test.ts +188 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { getNonEmptyString, toRecord } from "./common";
|
|
2
|
+
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
3
|
+
import type { ToolInputFormatterLookup } from "./tool-input-formatter-registry";
|
|
4
|
+
import {
|
|
5
|
+
serializeToolInputPreview,
|
|
6
|
+
TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
|
|
7
|
+
TOOL_INPUT_PREVIEW_MAX_LENGTH,
|
|
8
|
+
TOOL_TEXT_SUMMARY_MAX_LENGTH,
|
|
9
|
+
truncateInlineText,
|
|
10
|
+
} from "./tool-input-preview";
|
|
11
|
+
import {
|
|
12
|
+
formatEditInputForPrompt,
|
|
13
|
+
formatReadInputForPrompt,
|
|
14
|
+
formatWriteInputForPrompt,
|
|
15
|
+
getPromptPath,
|
|
16
|
+
} from "./tool-input-prompt-formatters";
|
|
17
|
+
import type { PermissionCheckResult } from "./types";
|
|
18
|
+
|
|
19
|
+
export interface ToolPreviewFormatterOptions {
|
|
20
|
+
toolInputPreviewMaxLength: number;
|
|
21
|
+
toolTextSummaryMaxLength: number;
|
|
22
|
+
toolInputLogPreviewMaxLength: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ConfigurablePreviewLimits = Pick<
|
|
26
|
+
PermissionSystemExtensionConfig,
|
|
27
|
+
"toolInputPreviewMaxLength" | "toolTextSummaryMaxLength"
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve `ToolPreviewFormatterOptions` from a config object, falling back to
|
|
32
|
+
* the built-in defaults for any field that is absent.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveToolPreviewLimits(
|
|
35
|
+
config: ConfigurablePreviewLimits,
|
|
36
|
+
): ToolPreviewFormatterOptions {
|
|
37
|
+
return {
|
|
38
|
+
toolInputPreviewMaxLength:
|
|
39
|
+
config.toolInputPreviewMaxLength ?? TOOL_INPUT_PREVIEW_MAX_LENGTH,
|
|
40
|
+
toolTextSummaryMaxLength:
|
|
41
|
+
config.toolTextSummaryMaxLength ?? TOOL_TEXT_SUMMARY_MAX_LENGTH,
|
|
42
|
+
toolInputLogPreviewMaxLength: TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Formats tool inputs for permission prompts and review logs.
|
|
48
|
+
*
|
|
49
|
+
* Accepts configurable limits in its constructor — the single injection
|
|
50
|
+
* point for preview-length configuration (#266).
|
|
51
|
+
*/
|
|
52
|
+
export class ToolPreviewFormatter {
|
|
53
|
+
constructor(
|
|
54
|
+
private readonly options: ToolPreviewFormatterOptions,
|
|
55
|
+
private readonly customFormatters?: ToolInputFormatterLookup,
|
|
56
|
+
) {}
|
|
57
|
+
|
|
58
|
+
// ── Prompt formatting ───────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Collapse whitespace, trim, and truncate a string to fit inline.
|
|
62
|
+
* An explicit `maxLength` overrides the constructor default.
|
|
63
|
+
*/
|
|
64
|
+
sanitizeInlineText(value: string, maxLength?: number): string {
|
|
65
|
+
const limit = maxLength ?? this.options.toolTextSummaryMaxLength;
|
|
66
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
67
|
+
return normalized ? truncateInlineText(normalized, limit) : "empty text";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Serialize `input` to inline JSON and truncate at `toolInputPreviewMaxLength`. */
|
|
71
|
+
formatJsonInputForPrompt(input: unknown): string {
|
|
72
|
+
const inline = serializeToolInputPreview(input);
|
|
73
|
+
return inline
|
|
74
|
+
? `with input ${truncateInlineText(inline, this.options.toolInputPreviewMaxLength)}`
|
|
75
|
+
: "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Format search-tool (grep/find/ls) input for a permission prompt. */
|
|
79
|
+
formatSearchInputForPrompt(
|
|
80
|
+
toolName: string,
|
|
81
|
+
input: Record<string, unknown>,
|
|
82
|
+
): string {
|
|
83
|
+
const parts: string[] = [];
|
|
84
|
+
const path = getPromptPath(input);
|
|
85
|
+
const pattern = getNonEmptyString(input.pattern);
|
|
86
|
+
const glob = getNonEmptyString(input.glob);
|
|
87
|
+
|
|
88
|
+
if (pattern) {
|
|
89
|
+
parts.push(`pattern '${this.sanitizeInlineText(pattern)}'`);
|
|
90
|
+
}
|
|
91
|
+
if (glob) {
|
|
92
|
+
parts.push(`glob '${this.sanitizeInlineText(glob)}'`);
|
|
93
|
+
}
|
|
94
|
+
if (path) {
|
|
95
|
+
parts.push(`path '${path}'`);
|
|
96
|
+
} else if (
|
|
97
|
+
toolName === "find" ||
|
|
98
|
+
toolName === "grep" ||
|
|
99
|
+
toolName === "ls"
|
|
100
|
+
) {
|
|
101
|
+
parts.push("current working directory");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return parts.length > 0 ? `for ${parts.join(", ")}` : "";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Format any tool input for display in a permission ask-prompt.
|
|
109
|
+
*
|
|
110
|
+
* Dispatches to the appropriate pure formatter for known tools
|
|
111
|
+
* and falls back to inline JSON for everything else.
|
|
112
|
+
*/
|
|
113
|
+
formatToolInputForPrompt(toolName: string, input: unknown): string {
|
|
114
|
+
const inputRecord = toRecord(input);
|
|
115
|
+
|
|
116
|
+
const custom = this.customFormatters?.get(toolName);
|
|
117
|
+
if (custom) {
|
|
118
|
+
const rendered = custom(inputRecord);
|
|
119
|
+
if (rendered !== undefined) {
|
|
120
|
+
return rendered;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
switch (toolName) {
|
|
125
|
+
case "edit":
|
|
126
|
+
return formatEditInputForPrompt(inputRecord);
|
|
127
|
+
case "write":
|
|
128
|
+
return formatWriteInputForPrompt(inputRecord);
|
|
129
|
+
case "read":
|
|
130
|
+
return formatReadInputForPrompt(inputRecord);
|
|
131
|
+
case "find":
|
|
132
|
+
case "grep":
|
|
133
|
+
case "ls":
|
|
134
|
+
return this.formatSearchInputForPrompt(toolName, inputRecord);
|
|
135
|
+
case "mcp":
|
|
136
|
+
// The MCP target is already surfaced in formatAskPrompt's MCP branch.
|
|
137
|
+
// When no custom formatter is registered (or it declines), produce no
|
|
138
|
+
// additional preview rather than leaking the raw event JSON.
|
|
139
|
+
return "";
|
|
140
|
+
default:
|
|
141
|
+
return this.formatJsonInputForPrompt(input);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Log formatting ──────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/** Serialize `input` to inline JSON and truncate at `toolInputLogPreviewMaxLength`. */
|
|
148
|
+
formatGenericToolInputForLog(input: unknown): string | undefined {
|
|
149
|
+
const inline = serializeToolInputPreview(input);
|
|
150
|
+
return inline
|
|
151
|
+
? `input ${truncateInlineText(inline, this.options.toolInputLogPreviewMaxLength)}`
|
|
152
|
+
: undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Derive a loggable input preview string for the review log. */
|
|
156
|
+
getToolInputPreviewForLog(
|
|
157
|
+
result: PermissionCheckResult,
|
|
158
|
+
input: unknown,
|
|
159
|
+
pathBearingTools: ReadonlySet<string>,
|
|
160
|
+
): string | undefined {
|
|
161
|
+
if (
|
|
162
|
+
result.toolName === "bash" ||
|
|
163
|
+
result.toolName === "mcp" ||
|
|
164
|
+
result.source === "mcp"
|
|
165
|
+
) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (pathBearingTools.has(result.toolName)) {
|
|
170
|
+
const inputPreview = this.formatToolInputForPrompt(
|
|
171
|
+
result.toolName,
|
|
172
|
+
input,
|
|
173
|
+
);
|
|
174
|
+
return inputPreview
|
|
175
|
+
? truncateInlineText(
|
|
176
|
+
inputPreview,
|
|
177
|
+
this.options.toolInputLogPreviewMaxLength,
|
|
178
|
+
)
|
|
179
|
+
: undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return this.formatGenericToolInputForLog(input);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Build the structured log context object for a permission review log entry. */
|
|
186
|
+
getPermissionLogContext(
|
|
187
|
+
result: PermissionCheckResult,
|
|
188
|
+
input: unknown,
|
|
189
|
+
pathBearingTools: ReadonlySet<string>,
|
|
190
|
+
): {
|
|
191
|
+
command?: string;
|
|
192
|
+
target?: string;
|
|
193
|
+
toolInputPreview?: string;
|
|
194
|
+
origin?: string;
|
|
195
|
+
} {
|
|
196
|
+
return {
|
|
197
|
+
command: result.command,
|
|
198
|
+
target: result.target,
|
|
199
|
+
toolInputPreview: this.getToolInputPreviewForLog(
|
|
200
|
+
result,
|
|
201
|
+
input,
|
|
202
|
+
pathBearingTools,
|
|
203
|
+
),
|
|
204
|
+
origin: result.origin,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { getNonEmptyString, toRecord } from "./common";
|
|
2
|
+
|
|
3
|
+
/** Narrow interface for the Pi tool API subset used by handler classes. */
|
|
4
|
+
export interface ToolRegistry {
|
|
5
|
+
/** All registered tools (`pi.getAllTools()` — `ToolInfo[]`); kept defensively wide. */
|
|
6
|
+
getAll(): unknown[];
|
|
7
|
+
/** Currently active tool names (`pi.getActiveTools()`). */
|
|
8
|
+
getActive(): string[];
|
|
9
|
+
setActive(names: string[]): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ToolRegistrationCheckResult =
|
|
13
|
+
| {
|
|
14
|
+
status: "missing-tool-name";
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
status: "registered";
|
|
18
|
+
requestedToolName: string;
|
|
19
|
+
normalizedToolName: string;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
status: "unregistered";
|
|
23
|
+
requestedToolName: string;
|
|
24
|
+
normalizedToolName: string;
|
|
25
|
+
availableToolNames: string[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function normalizeToolName(
|
|
29
|
+
toolName: string,
|
|
30
|
+
aliases: Record<string, string>,
|
|
31
|
+
): string {
|
|
32
|
+
return aliases[toolName] || toolName;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildReverseAliases(
|
|
36
|
+
aliases: Record<string, string>,
|
|
37
|
+
): Map<string, string[]> {
|
|
38
|
+
const reverse = new Map<string, string[]>();
|
|
39
|
+
|
|
40
|
+
for (const [alias, canonical] of Object.entries(aliases)) {
|
|
41
|
+
const existing = reverse.get(canonical) ?? [];
|
|
42
|
+
if (!existing.includes(alias)) {
|
|
43
|
+
existing.push(alias);
|
|
44
|
+
}
|
|
45
|
+
reverse.set(canonical, existing);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return reverse;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function addToolNameVariants(
|
|
52
|
+
value: string,
|
|
53
|
+
names: Set<string>,
|
|
54
|
+
aliases: Record<string, string>,
|
|
55
|
+
reverseAliases: ReadonlyMap<string, readonly string[]>,
|
|
56
|
+
): void {
|
|
57
|
+
names.add(value);
|
|
58
|
+
|
|
59
|
+
const normalized = normalizeToolName(value, aliases);
|
|
60
|
+
names.add(normalized);
|
|
61
|
+
|
|
62
|
+
const canonicalFromAlias = aliases[value];
|
|
63
|
+
if (canonicalFromAlias) {
|
|
64
|
+
names.add(canonicalFromAlias);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const aliasValues = reverseAliases.get(value);
|
|
68
|
+
if (aliasValues) {
|
|
69
|
+
for (const alias of aliasValues) {
|
|
70
|
+
names.add(alias);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const aliasValuesForNormalized = reverseAliases.get(normalized);
|
|
75
|
+
if (aliasValuesForNormalized) {
|
|
76
|
+
for (const alias of aliasValuesForNormalized) {
|
|
77
|
+
names.add(alias);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getToolNameFromValue(value: unknown): string | null {
|
|
83
|
+
const direct = getNonEmptyString(value);
|
|
84
|
+
if (direct) {
|
|
85
|
+
return direct;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const record = toRecord(value);
|
|
89
|
+
const candidates = [record.toolName, record.name, record.tool];
|
|
90
|
+
|
|
91
|
+
for (const candidate of candidates) {
|
|
92
|
+
const stringValue = getNonEmptyString(candidate);
|
|
93
|
+
if (stringValue) {
|
|
94
|
+
return stringValue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function checkRequestedToolRegistration(
|
|
102
|
+
requestedToolName: string | null,
|
|
103
|
+
registeredTools: readonly unknown[],
|
|
104
|
+
aliases: Record<string, string> = {},
|
|
105
|
+
): ToolRegistrationCheckResult {
|
|
106
|
+
const requested = getNonEmptyString(requestedToolName);
|
|
107
|
+
if (!requested) {
|
|
108
|
+
return {
|
|
109
|
+
status: "missing-tool-name",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const normalizedToolName = normalizeToolName(requested, aliases);
|
|
114
|
+
const reverseAliases = buildReverseAliases(aliases);
|
|
115
|
+
|
|
116
|
+
const registeredLookup = new Set<string>();
|
|
117
|
+
const availableToolNames = new Set<string>();
|
|
118
|
+
|
|
119
|
+
for (const tool of registeredTools) {
|
|
120
|
+
const name = getToolNameFromValue(tool);
|
|
121
|
+
if (!name) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
availableToolNames.add(name);
|
|
126
|
+
addToolNameVariants(name, registeredLookup, aliases, reverseAliases);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const isRegistered =
|
|
130
|
+
registeredLookup.has(requested) || registeredLookup.has(normalizedToolName);
|
|
131
|
+
|
|
132
|
+
if (isRegistered) {
|
|
133
|
+
return {
|
|
134
|
+
status: "registered",
|
|
135
|
+
requestedToolName: requested,
|
|
136
|
+
normalizedToolName,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
status: "unregistered",
|
|
142
|
+
requestedToolName: requested,
|
|
143
|
+
normalizedToolName,
|
|
144
|
+
availableToolNames: [...availableToolNames].sort((a, b) =>
|
|
145
|
+
a.localeCompare(b),
|
|
146
|
+
),
|
|
147
|
+
};
|
|
148
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type PermissionState = "allow" | "deny" | "ask";
|
|
2
|
+
|
|
3
|
+
import type { RuleOrigin } from "./rule";
|
|
4
|
+
|
|
5
|
+
export type { RuleOrigin };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A deny action with an optional reason annotation, used when a pattern maps
|
|
9
|
+
* to an object instead of a plain PermissionState string.
|
|
10
|
+
*/
|
|
11
|
+
export interface DenyWithReason {
|
|
12
|
+
action: "deny";
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A pattern value: a PermissionState string OR a DenyWithReason object. */
|
|
17
|
+
export type PatternValue = PermissionState | DenyWithReason;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The on-disk permission shape inside the `"permission"` key.
|
|
21
|
+
* A surface value is a PermissionState string (shorthand for `{ "*": action }`)
|
|
22
|
+
* or a pattern→value map. Pattern values may be a PermissionState string or a
|
|
23
|
+
* DenyWithReason object. A top-level value is never a bare DenyWithReason.
|
|
24
|
+
*/
|
|
25
|
+
export type FlatPermissionConfig = Record<
|
|
26
|
+
string,
|
|
27
|
+
PermissionState | Record<string, PatternValue>
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Per-scope permission config shape after loading and validation.
|
|
32
|
+
* Holds only the flat permission map — all policy is expressed there.
|
|
33
|
+
*/
|
|
34
|
+
export interface ScopeConfig {
|
|
35
|
+
permission?: FlatPermissionConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Execution context of a bash command nested inside a substitution or subshell.
|
|
40
|
+
* Absent for current-shell (top-level) commands.
|
|
41
|
+
*/
|
|
42
|
+
export type BashCommandContext =
|
|
43
|
+
| "command_substitution"
|
|
44
|
+
| "process_substitution"
|
|
45
|
+
| "subshell";
|
|
46
|
+
|
|
47
|
+
export interface PermissionCheckResult {
|
|
48
|
+
toolName: string;
|
|
49
|
+
state: PermissionState;
|
|
50
|
+
/** Custom denial reason from a deny-with-reason pattern, when present. */
|
|
51
|
+
reason?: string;
|
|
52
|
+
matchedPattern?: string;
|
|
53
|
+
command?: string;
|
|
54
|
+
target?: string;
|
|
55
|
+
source: "tool" | "bash" | "mcp" | "skill" | "special" | "default" | "session";
|
|
56
|
+
/** Which source contributed the winning rule. */
|
|
57
|
+
origin: RuleOrigin;
|
|
58
|
+
/**
|
|
59
|
+
* Execution context of the offending nested command, when the winning bash
|
|
60
|
+
* unit came from a substitution or subshell. Absent for current-shell
|
|
61
|
+
* (top-level) commands.
|
|
62
|
+
*/
|
|
63
|
+
commandContext?: BashCommandContext;
|
|
64
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { expandHomePath } from "./expand-home";
|
|
2
|
+
|
|
3
|
+
export type CompiledWildcardPattern<TState> = {
|
|
4
|
+
pattern: string;
|
|
5
|
+
state: TState;
|
|
6
|
+
regex: RegExp;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type WildcardPatternMatch<TState> = {
|
|
10
|
+
state: TState;
|
|
11
|
+
matchedPattern: string;
|
|
12
|
+
matchedName: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Optional folding applied when matching path-surface patterns on Windows.
|
|
17
|
+
*
|
|
18
|
+
* - `caseInsensitive` compiles the pattern with the `i` flag so a mixed-case
|
|
19
|
+
* pattern matches a lowercased (canonicalized) path value.
|
|
20
|
+
* - `windowsSeparators` rewrites `/` to `\` in the expanded pattern so a
|
|
21
|
+
* forward-slash pattern matches a backslash-separated path value.
|
|
22
|
+
*/
|
|
23
|
+
export interface WildcardMatchOptions {
|
|
24
|
+
caseInsensitive?: boolean;
|
|
25
|
+
windowsSeparators?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function escapeRegExp(value: string): string {
|
|
29
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function compileWildcardPattern<TState>(
|
|
33
|
+
pattern: string,
|
|
34
|
+
state: TState,
|
|
35
|
+
options?: WildcardMatchOptions,
|
|
36
|
+
): CompiledWildcardPattern<TState> {
|
|
37
|
+
let expanded = expandHomePath(pattern);
|
|
38
|
+
if (options?.windowsSeparators) {
|
|
39
|
+
expanded = expanded.replaceAll("/", "\\");
|
|
40
|
+
}
|
|
41
|
+
let escaped = expanded
|
|
42
|
+
.split("*")
|
|
43
|
+
.map((part) => escapeRegExp(part).replaceAll("\\?", "."))
|
|
44
|
+
.join(".*");
|
|
45
|
+
|
|
46
|
+
// If the pattern ends with " *" (space + wildcard), make the trailing
|
|
47
|
+
// space-and-arguments portion optional so that e.g. "git *" matches both
|
|
48
|
+
// "git status" and bare "git". Mirrors OpenCode wildcard semantics.
|
|
49
|
+
if (escaped.endsWith(" .*")) {
|
|
50
|
+
escaped = `${escaped.slice(0, -3)}( .*)?`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
pattern,
|
|
55
|
+
state,
|
|
56
|
+
regex: new RegExp(`^${escaped}$`, options?.caseInsensitive ? "si" : "s"),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function compileWildcardPatternEntries<TState>(
|
|
61
|
+
entries: Iterable<readonly [string, TState]>,
|
|
62
|
+
): CompiledWildcardPattern<TState>[] {
|
|
63
|
+
return Array.from(entries, ([pattern, state]) =>
|
|
64
|
+
compileWildcardPattern(pattern, state),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _compileWildcardPatterns<TState>(
|
|
69
|
+
patterns: Record<string, TState>,
|
|
70
|
+
): CompiledWildcardPattern<TState>[] {
|
|
71
|
+
return compileWildcardPatternEntries(Object.entries(patterns));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function findCompiledWildcardMatch<TState>(
|
|
75
|
+
patterns: readonly CompiledWildcardPattern<TState>[],
|
|
76
|
+
name: string,
|
|
77
|
+
): WildcardPatternMatch<TState> | null {
|
|
78
|
+
const match = patterns.findLast((p) => p.regex.test(name));
|
|
79
|
+
if (match === undefined) return null;
|
|
80
|
+
return {
|
|
81
|
+
state: match.state,
|
|
82
|
+
matchedPattern: match.pattern,
|
|
83
|
+
matchedName: name,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Test whether `value` matches `pattern` using wildcard rules.
|
|
89
|
+
* `*` matches any sequence of characters (including empty).
|
|
90
|
+
* `?` matches exactly one character.
|
|
91
|
+
* Used by evaluate() for rule matching.
|
|
92
|
+
*/
|
|
93
|
+
export function wildcardMatch(
|
|
94
|
+
pattern: string,
|
|
95
|
+
value: string,
|
|
96
|
+
options?: WildcardMatchOptions,
|
|
97
|
+
): boolean {
|
|
98
|
+
return compileWildcardPattern(pattern, null, options).regex.test(value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function findCompiledWildcardMatchForNames<TState>(
|
|
102
|
+
patterns: readonly CompiledWildcardPattern<TState>[],
|
|
103
|
+
names: readonly string[],
|
|
104
|
+
): WildcardPatternMatch<TState> | null {
|
|
105
|
+
const normalizedNames = names
|
|
106
|
+
.map((value) => value.trim())
|
|
107
|
+
.filter((value) => value.length > 0);
|
|
108
|
+
if (normalizedNames.length === 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const name of normalizedNames) {
|
|
113
|
+
const match = findCompiledWildcardMatch(patterns, name);
|
|
114
|
+
if (match) {
|
|
115
|
+
return match;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null;
|
|
120
|
+
}
|
package/src/yolo-mode.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
2
|
+
import type { PermissionState } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface AskPermissionResolutionOptions {
|
|
5
|
+
config: PermissionSystemExtensionConfig;
|
|
6
|
+
hasUI: boolean;
|
|
7
|
+
isSubagent: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isYoloModeEnabled(
|
|
11
|
+
config: PermissionSystemExtensionConfig,
|
|
12
|
+
): boolean {
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion -- typed as boolean but may be undefined at runtime (untyped callers); Boolean() guards against that
|
|
14
|
+
return Boolean(config.yoloMode);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function shouldAutoApprovePermissionState(
|
|
18
|
+
state: PermissionState,
|
|
19
|
+
config: PermissionSystemExtensionConfig,
|
|
20
|
+
): boolean {
|
|
21
|
+
return state === "ask" && isYoloModeEnabled(config);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function canResolveAskPermissionRequest(
|
|
25
|
+
options: AskPermissionResolutionOptions,
|
|
26
|
+
): boolean {
|
|
27
|
+
return (
|
|
28
|
+
options.hasUI || options.isSubagent || isYoloModeEnabled(options.config)
|
|
29
|
+
);
|
|
30
|
+
}
|