@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,94 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BeforeAgentStartEventResult,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { PermissionResolver } from "#src/permission-resolver";
|
|
6
|
+
import type { PermissionSession } from "#src/permission-session";
|
|
7
|
+
import { resolveSkillPromptEntries } from "#src/skill-prompt-sanitizer";
|
|
8
|
+
import { sanitizeAvailableToolsSection } from "#src/system-prompt-sanitizer";
|
|
9
|
+
import { getToolNameFromValue, type ToolRegistry } from "#src/tool-registry";
|
|
10
|
+
import type { PermissionState } from "#src/types";
|
|
11
|
+
|
|
12
|
+
/** Minimal subset of BeforeAgentStartEvent used by this handler. */
|
|
13
|
+
interface BeforeAgentStartPayload {
|
|
14
|
+
systemPrompt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pure helper: returns true when the tool should be exposed to the agent.
|
|
19
|
+
* Checks the tool-level permission (not command-level) so that a blanket
|
|
20
|
+
* `bash: deny` hides the tool entirely before any invocation is attempted.
|
|
21
|
+
*/
|
|
22
|
+
export function shouldExposeTool(
|
|
23
|
+
toolName: string,
|
|
24
|
+
agentName: string | null,
|
|
25
|
+
getToolPermission: (toolName: string, agentName?: string) => PermissionState,
|
|
26
|
+
): boolean {
|
|
27
|
+
const toolPermission = getToolPermission(toolName, agentName ?? undefined);
|
|
28
|
+
return toolPermission !== "deny";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Handles the `before_agent_start` event: tool filtering + prompt sanitization.
|
|
33
|
+
*
|
|
34
|
+
* Recomputes the active tool set and the returned system-prompt override on
|
|
35
|
+
* every fire (no memoization): the override must be returned each turn so that
|
|
36
|
+
* skill filtering is reapplied and the wire prompt stays byte-stable, rather
|
|
37
|
+
* than letting Pi reset to its skill-unfiltered base prompt on a cache hit.
|
|
38
|
+
*
|
|
39
|
+
* Constructor deps:
|
|
40
|
+
* - `session` — encapsulates all mutable session state and lifecycle operations
|
|
41
|
+
* - `resolver` — owns permission-query surface: `getToolPermission`, skill check
|
|
42
|
+
* - `toolRegistry` — Pi tool API subset (getActive + setActive)
|
|
43
|
+
*/
|
|
44
|
+
export class AgentPrepHandler {
|
|
45
|
+
constructor(
|
|
46
|
+
private readonly session: PermissionSession,
|
|
47
|
+
private readonly resolver: PermissionResolver,
|
|
48
|
+
private readonly toolRegistry: ToolRegistry,
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
52
|
+
async handle(
|
|
53
|
+
event: BeforeAgentStartPayload,
|
|
54
|
+
ctx: ExtensionContext,
|
|
55
|
+
): Promise<BeforeAgentStartEventResult> {
|
|
56
|
+
this.session.activate(ctx);
|
|
57
|
+
this.session.refreshConfig(ctx);
|
|
58
|
+
|
|
59
|
+
const agentName = this.session.resolveAgentName(ctx, event.systemPrompt);
|
|
60
|
+
const activeTools = this.toolRegistry.getActive();
|
|
61
|
+
const allowedTools: string[] = [];
|
|
62
|
+
|
|
63
|
+
for (const tool of activeTools) {
|
|
64
|
+
const toolName = getToolNameFromValue(tool);
|
|
65
|
+
if (!toolName) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (
|
|
69
|
+
shouldExposeTool(toolName, agentName, (t, a) =>
|
|
70
|
+
this.resolver.getToolPermission(t, a),
|
|
71
|
+
)
|
|
72
|
+
) {
|
|
73
|
+
allowedTools.push(toolName);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.toolRegistry.setActive(allowedTools);
|
|
78
|
+
|
|
79
|
+
const toolPromptResult = sanitizeAvailableToolsSection(
|
|
80
|
+
event.systemPrompt,
|
|
81
|
+
allowedTools,
|
|
82
|
+
);
|
|
83
|
+
const skillPromptResult = resolveSkillPromptEntries(
|
|
84
|
+
toolPromptResult.prompt,
|
|
85
|
+
this.resolver,
|
|
86
|
+
agentName,
|
|
87
|
+
ctx.cwd,
|
|
88
|
+
);
|
|
89
|
+
this.session.setActiveSkillEntries(skillPromptResult.entries);
|
|
90
|
+
return skillPromptResult.prompt !== event.systemPrompt
|
|
91
|
+
? { systemPrompt: skillPromptResult.prompt }
|
|
92
|
+
: {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { BashCommand } from "#src/handlers/gates/bash-program";
|
|
2
|
+
import { pickMostRestrictive } from "#src/handlers/gates/candidate-check";
|
|
3
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the bash command-pattern decision for a (possibly chained) command.
|
|
8
|
+
*
|
|
9
|
+
* A bash invocation may be a shell program with several commands joined by
|
|
10
|
+
* `&&`, `||`, `;`, `|`, `&`, or newlines. Matching the whole string against the
|
|
11
|
+
* bash patterns lets a denied command ride through on an allowed leading one
|
|
12
|
+
* (issue #301). Instead, the caller supplies the program's command units (from
|
|
13
|
+
* the shared `BashProgram.commands()` parse) — including those nested inside
|
|
14
|
+
* substitutions and subshells (#306); each is evaluated on the `bash` surface
|
|
15
|
+
* and the most restrictive result wins (`deny > ask > allow`).
|
|
16
|
+
*
|
|
17
|
+
* The selected result carries the offending sub-command in `command`, its rule
|
|
18
|
+
* in `matchedPattern`, and the offending command's execution context in
|
|
19
|
+
* `commandContext` (set only for a nested command), so the prompt,
|
|
20
|
+
* session-approval suggestion, and decision event scope to that command.
|
|
21
|
+
*
|
|
22
|
+
* When `commands` is empty there are two cases. A trivially-empty command (an
|
|
23
|
+
* empty, whitespace-only, or comment-only line) has genuinely nothing to gate,
|
|
24
|
+
* so the whole `command` is resolved as before. A non-empty command that parsed
|
|
25
|
+
* to zero command units (a parse anomaly or an opaque program) fails closed to
|
|
26
|
+
* a synthetic `ask` so a permissive top-level `*` cannot silently allow an
|
|
27
|
+
* unparseable command (e.g. `cd /repo && git push` riding a top-level allow on
|
|
28
|
+
* the empty-parse path) — #452.
|
|
29
|
+
*
|
|
30
|
+
* Pure and synchronous: the (async, tree-sitter) parse happens once in the
|
|
31
|
+
* handler, which passes the decomposed `commands` here.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveBashCommandCheck(
|
|
34
|
+
command: string,
|
|
35
|
+
commands: BashCommand[],
|
|
36
|
+
agentName: string | undefined,
|
|
37
|
+
resolver: ScopedPermissionResolver,
|
|
38
|
+
): PermissionCheckResult {
|
|
39
|
+
if (commands.length === 0) {
|
|
40
|
+
if (isTriviallyEmptyCommand(command)) {
|
|
41
|
+
return resolver.resolve("bash", { command }, agentName);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
state: "ask",
|
|
45
|
+
toolName: "bash",
|
|
46
|
+
source: "bash",
|
|
47
|
+
origin: "builtin",
|
|
48
|
+
command,
|
|
49
|
+
matchedPattern: "<unparseable-bash-command>",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const results = commands.map((cmd) => {
|
|
54
|
+
const result = resolver.resolve("bash", { command: cmd.text }, agentName);
|
|
55
|
+
return cmd.context ? { ...result, commandContext: cmd.context } : result;
|
|
56
|
+
});
|
|
57
|
+
return (
|
|
58
|
+
pickMostRestrictive(results) ??
|
|
59
|
+
resolver.resolve("bash", { command }, agentName)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* True when a command has genuinely nothing to gate: it is empty,
|
|
65
|
+
* whitespace-only, or contains only comment lines (every non-blank line starts
|
|
66
|
+
* with `#`). Such a command yields zero command units legitimately, so the
|
|
67
|
+
* whole-string resolve is safe rather than a parse anomaly.
|
|
68
|
+
*/
|
|
69
|
+
function isTriviallyEmptyCommand(command: string): boolean {
|
|
70
|
+
const lines = command
|
|
71
|
+
.split("\n")
|
|
72
|
+
.map((line) => line.trim())
|
|
73
|
+
.filter((line) => line.length > 0);
|
|
74
|
+
return lines.every((line) => line.startsWith("#"));
|
|
75
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { getNonEmptyString, toRecord } from "#src/common";
|
|
2
|
+
import { getExternalDirectoryPolicyValues } from "#src/path-utils";
|
|
3
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
|
+
import { SessionApproval } from "#src/session-approval";
|
|
5
|
+
import { deriveApprovalPattern } from "#src/session-rules";
|
|
6
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
7
|
+
import type { BashProgram } from "./bash-program";
|
|
8
|
+
import { pickMostRestrictive } from "./candidate-check";
|
|
9
|
+
import type { GateResult } from "./descriptor";
|
|
10
|
+
import { formatBashExternalDirectoryAskPrompt } from "./external-directory-messages";
|
|
11
|
+
import type { ToolCallContext } from "./types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a pure descriptor for the bash external-directory permission gate.
|
|
15
|
+
*
|
|
16
|
+
* Reads the external paths from the injected `BashProgram` and checks whether
|
|
17
|
+
* any reference directories outside the working directory. Returns `null` when the gate
|
|
18
|
+
* does not apply (tool is not bash, no CWD, or no external paths found).
|
|
19
|
+
* Returns a `GateBypass` when all paths are allowed (by config or session rule).
|
|
20
|
+
* Returns a `GateDescriptor` with multi-pattern sessionApproval for uncovered paths.
|
|
21
|
+
*/
|
|
22
|
+
export function describeBashExternalDirectoryGate(
|
|
23
|
+
tcc: ToolCallContext,
|
|
24
|
+
bashProgram: BashProgram | null,
|
|
25
|
+
resolver: ScopedPermissionResolver,
|
|
26
|
+
): GateResult {
|
|
27
|
+
if (tcc.toolName !== "bash" || !tcc.cwd) return null;
|
|
28
|
+
|
|
29
|
+
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
30
|
+
if (!command) return null;
|
|
31
|
+
|
|
32
|
+
if (!bashProgram) return null;
|
|
33
|
+
|
|
34
|
+
const externalPaths = bashProgram.externalPaths(tcc.cwd);
|
|
35
|
+
if (externalPaths.length === 0) return null;
|
|
36
|
+
|
|
37
|
+
// Collect paths whose resolved state is not already "allow".
|
|
38
|
+
// Checking state (not source) ensures config-level allow rules (source: "special")
|
|
39
|
+
// suppress the prompt just as session-level allow rules (source: "session") do.
|
|
40
|
+
const uncoveredEntries: Array<{
|
|
41
|
+
path: string;
|
|
42
|
+
check: PermissionCheckResult;
|
|
43
|
+
}> = [];
|
|
44
|
+
for (const p of externalPaths) {
|
|
45
|
+
// Match each path against both its typed and symlink-resolved aliases on
|
|
46
|
+
// the external_directory surface, so a config pattern on either form
|
|
47
|
+
// applies (#418).
|
|
48
|
+
const check = resolver.resolvePathPolicy(
|
|
49
|
+
getExternalDirectoryPolicyValues(p, tcc.cwd),
|
|
50
|
+
tcc.agentName ?? undefined,
|
|
51
|
+
"external_directory",
|
|
52
|
+
);
|
|
53
|
+
if (check.state !== "allow") {
|
|
54
|
+
uncoveredEntries.push({ path: p, check });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const uncoveredPaths = uncoveredEntries.map(({ path }) => path);
|
|
58
|
+
|
|
59
|
+
if (uncoveredPaths.length === 0) {
|
|
60
|
+
return {
|
|
61
|
+
action: "allow",
|
|
62
|
+
log: {
|
|
63
|
+
event: "permission_request.session_approved",
|
|
64
|
+
details: {
|
|
65
|
+
source: "tool_call",
|
|
66
|
+
toolCallId: tcc.toolCallId,
|
|
67
|
+
toolName: tcc.toolName,
|
|
68
|
+
agentName: tcc.agentName,
|
|
69
|
+
command,
|
|
70
|
+
externalPaths,
|
|
71
|
+
resolution: "session_approved",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Use the most restrictive check among uncovered paths as the pre-check result.
|
|
78
|
+
// This ensures a config-level "deny" rule is not downgraded to "ask" by the
|
|
79
|
+
// generic "*" catch-all that the old path-less checkPermission call returned.
|
|
80
|
+
const worstCheck =
|
|
81
|
+
pickMostRestrictive(uncoveredEntries.map(({ check }) => check)) ??
|
|
82
|
+
uncoveredEntries[0].check;
|
|
83
|
+
|
|
84
|
+
const bashExtMessage = formatBashExternalDirectoryAskPrompt(
|
|
85
|
+
command,
|
|
86
|
+
uncoveredPaths,
|
|
87
|
+
tcc.cwd,
|
|
88
|
+
tcc.agentName ?? undefined,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const patterns = uncoveredPaths.map((p) => deriveApprovalPattern(p));
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
surface: "external_directory",
|
|
95
|
+
input: {},
|
|
96
|
+
denialContext: {
|
|
97
|
+
kind: "bash_external_directory",
|
|
98
|
+
command,
|
|
99
|
+
externalPaths: uncoveredPaths,
|
|
100
|
+
cwd: tcc.cwd,
|
|
101
|
+
agentName: tcc.agentName ?? undefined,
|
|
102
|
+
},
|
|
103
|
+
sessionApproval: SessionApproval.multiple("external_directory", patterns),
|
|
104
|
+
promptDetails: {
|
|
105
|
+
source: "tool_call",
|
|
106
|
+
agentName: tcc.agentName,
|
|
107
|
+
message: bashExtMessage,
|
|
108
|
+
toolCallId: tcc.toolCallId,
|
|
109
|
+
toolName: tcc.toolName,
|
|
110
|
+
command,
|
|
111
|
+
},
|
|
112
|
+
logContext: {
|
|
113
|
+
source: "tool_call",
|
|
114
|
+
toolCallId: tcc.toolCallId,
|
|
115
|
+
toolName: tcc.toolName,
|
|
116
|
+
agentName: tcc.agentName,
|
|
117
|
+
command,
|
|
118
|
+
externalPaths: uncoveredPaths,
|
|
119
|
+
message: bashExtMessage,
|
|
120
|
+
},
|
|
121
|
+
decision: {
|
|
122
|
+
surface: "external_directory",
|
|
123
|
+
value: command,
|
|
124
|
+
},
|
|
125
|
+
preCheck: worstCheck,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BashProgram } from "./bash-program";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract paths from a bash command string that resolve outside CWD.
|
|
5
|
+
*
|
|
6
|
+
* Thin facade over {@link BashProgram.externalPaths}; parses the command and
|
|
7
|
+
* returns the cd-aware external paths. See `BashProgram` for the parsing and
|
|
8
|
+
* resolution semantics.
|
|
9
|
+
*/
|
|
10
|
+
export async function extractExternalPathsFromBashCommand(
|
|
11
|
+
command: string,
|
|
12
|
+
cwd: string,
|
|
13
|
+
): Promise<string[]> {
|
|
14
|
+
return (await BashProgram.parse(command)).externalPaths(cwd);
|
|
15
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { getNonEmptyString, toRecord } from "#src/common";
|
|
2
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
3
|
+
import { SessionApproval } from "#src/session-approval";
|
|
4
|
+
import { deriveApprovalPattern } from "#src/session-rules";
|
|
5
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
6
|
+
import type { BashProgram } from "./bash-program";
|
|
7
|
+
import { pickMostRestrictive } from "./candidate-check";
|
|
8
|
+
import type { GateResult } from "./descriptor";
|
|
9
|
+
import { formatPathAskPrompt } from "./path";
|
|
10
|
+
import type { ToolCallContext } from "./types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build a pure descriptor for the cross-cutting path permission gate (bash).
|
|
14
|
+
*
|
|
15
|
+
* Reads path-rule candidates from the injected `BashProgram` (the broader
|
|
16
|
+
* `path`-rule filter, accepting dot-files and relative paths). Each candidate
|
|
17
|
+
* pairs the raw token with cd-aware policy values; the gate evaluates those
|
|
18
|
+
* values against the `path` permission surface and returns the most
|
|
19
|
+
* restrictive result, while prompts, logs, and session approvals use the raw
|
|
20
|
+
* token.
|
|
21
|
+
*
|
|
22
|
+
* Returns `null` when the gate does not apply (tool is not bash, no command,
|
|
23
|
+
* no tokens extracted, or all tokens evaluate to `allow`).
|
|
24
|
+
* Returns a `GateBypass` when all tokens are session-covered.
|
|
25
|
+
* Returns a `GateDescriptor` for the most restrictive token needing a check.
|
|
26
|
+
*/
|
|
27
|
+
export function describeBashPathGate(
|
|
28
|
+
tcc: ToolCallContext,
|
|
29
|
+
bashProgram: BashProgram | null,
|
|
30
|
+
resolver: ScopedPermissionResolver,
|
|
31
|
+
): GateResult {
|
|
32
|
+
if (tcc.toolName !== "bash") return null;
|
|
33
|
+
|
|
34
|
+
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
35
|
+
if (!command) return null;
|
|
36
|
+
|
|
37
|
+
if (!bashProgram) return null;
|
|
38
|
+
|
|
39
|
+
const candidates = bashProgram.pathRuleCandidates(tcc.cwd);
|
|
40
|
+
if (candidates.length === 0) return null;
|
|
41
|
+
const tokens = candidates.map(({ token }) => token);
|
|
42
|
+
|
|
43
|
+
// Tokens whose resolved state needs a check (deny/ask), paired with the raw
|
|
44
|
+
// token (prompt/decision display) and its policy values (the first of which
|
|
45
|
+
// is the canonical absolute path the approval pattern is derived from).
|
|
46
|
+
const uncovered: Array<{
|
|
47
|
+
token: string;
|
|
48
|
+
policyValues: readonly string[];
|
|
49
|
+
check: PermissionCheckResult;
|
|
50
|
+
}> = [];
|
|
51
|
+
let allSessionCovered = true;
|
|
52
|
+
|
|
53
|
+
for (const { token, policyValues } of candidates) {
|
|
54
|
+
const check = resolver.resolvePathPolicy(
|
|
55
|
+
policyValues,
|
|
56
|
+
tcc.agentName ?? undefined,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// No explicit path rule matched — only the universal default fired.
|
|
60
|
+
// Treat this token as unrestricted to preserve backward compatibility
|
|
61
|
+
// for configs without a "path" key (#58).
|
|
62
|
+
if (check.matchedPattern === undefined && check.source !== "session") {
|
|
63
|
+
allSessionCovered = false;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (check.source !== "session") {
|
|
68
|
+
allSessionCovered = false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (check.state === "deny") {
|
|
72
|
+
uncovered.push({ token, policyValues, check });
|
|
73
|
+
break; // Short-circuit on deny.
|
|
74
|
+
}
|
|
75
|
+
if (check.state === "ask") {
|
|
76
|
+
uncovered.push({ token, policyValues, check });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// All tokens are session-covered — bypass.
|
|
81
|
+
if (allSessionCovered) {
|
|
82
|
+
return {
|
|
83
|
+
action: "allow",
|
|
84
|
+
log: {
|
|
85
|
+
event: "permission_request.session_approved",
|
|
86
|
+
details: {
|
|
87
|
+
source: "tool_call",
|
|
88
|
+
toolCallId: tcc.toolCallId,
|
|
89
|
+
toolName: tcc.toolName,
|
|
90
|
+
agentName: tcc.agentName,
|
|
91
|
+
command,
|
|
92
|
+
tokens,
|
|
93
|
+
resolution: "session_approved",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Pick the most restrictive (deny > ask > allow, first-wins) uncovered token.
|
|
100
|
+
const worstCheck = pickMostRestrictive(uncovered.map(({ check }) => check));
|
|
101
|
+
const worstEntry = worstCheck
|
|
102
|
+
? uncovered.find(({ check }) => check === worstCheck)
|
|
103
|
+
: undefined;
|
|
104
|
+
const worstToken = worstEntry?.token ?? null;
|
|
105
|
+
|
|
106
|
+
// All tokens evaluate to allow — no restriction.
|
|
107
|
+
if (!worstCheck || !worstToken || !worstEntry) return null;
|
|
108
|
+
|
|
109
|
+
// Derive the pattern from the canonical absolute policy value (the cd-aware
|
|
110
|
+
// resolved path), so it matches the values a later call produces. Falls back
|
|
111
|
+
// to the raw token only when no base was resolvable (no cwd / unknown cd).
|
|
112
|
+
const approvalBase = worstEntry.policyValues[0] ?? worstToken;
|
|
113
|
+
const pattern = deriveApprovalPattern(approvalBase);
|
|
114
|
+
const askMessage = formatPathAskPrompt(
|
|
115
|
+
tcc.toolName,
|
|
116
|
+
worstToken,
|
|
117
|
+
tcc.agentName ?? undefined,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
surface: "path",
|
|
122
|
+
input: { path: worstToken },
|
|
123
|
+
denialContext: {
|
|
124
|
+
kind: "bash_path",
|
|
125
|
+
command,
|
|
126
|
+
pathValue: worstToken,
|
|
127
|
+
agentName: tcc.agentName ?? undefined,
|
|
128
|
+
},
|
|
129
|
+
sessionApproval: SessionApproval.single("path", pattern),
|
|
130
|
+
promptDetails: {
|
|
131
|
+
source: "tool_call",
|
|
132
|
+
agentName: tcc.agentName,
|
|
133
|
+
message: askMessage,
|
|
134
|
+
toolCallId: tcc.toolCallId,
|
|
135
|
+
toolName: tcc.toolName,
|
|
136
|
+
command,
|
|
137
|
+
},
|
|
138
|
+
logContext: {
|
|
139
|
+
source: "tool_call",
|
|
140
|
+
toolCallId: tcc.toolCallId,
|
|
141
|
+
toolName: tcc.toolName,
|
|
142
|
+
agentName: tcc.agentName,
|
|
143
|
+
command,
|
|
144
|
+
path: worstToken,
|
|
145
|
+
},
|
|
146
|
+
decision: {
|
|
147
|
+
surface: "path",
|
|
148
|
+
value: worstToken,
|
|
149
|
+
},
|
|
150
|
+
preCheck: worstCheck,
|
|
151
|
+
};
|
|
152
|
+
}
|