@gotgenes/pi-permission-system 16.1.0 → 16.2.1
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 +28 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/access-intent/access-intent.ts +57 -0
- package/src/access-intent/bash/command-enumeration.ts +79 -2
- package/src/access-intent/bash/parser.ts +2 -0
- package/src/access-intent/bash/program.ts +3 -0
- package/src/builtin-tool-input-formatters.ts +1 -1
- package/src/config-loader.ts +7 -7
- package/src/forwarded-permissions/permission-forwarder.ts +1 -1
- package/src/handlers/gates/bash-command.ts +32 -3
- package/src/handlers/gates/bash-external-directory.ts +1 -1
- package/src/handlers/gates/bash-path.ts +7 -5
- package/src/handlers/gates/external-directory-policy.ts +9 -8
- package/src/handlers/gates/path.ts +6 -5
- package/src/handlers/gates/runner.ts +6 -5
- package/src/handlers/gates/skill-read.ts +1 -1
- package/src/handlers/gates/tool-call-gate-pipeline.ts +7 -6
- package/src/handlers/permission-gate-handler.ts +1 -2
- package/src/handlers/tool-call-boundary.ts +1 -2
- package/src/input-normalizer.ts +1 -1
- package/src/mcp-targets.ts +1 -1
- package/src/normalize.ts +1 -1
- package/src/path-utils.ts +1 -1
- package/src/permission-event-rpc.ts +4 -6
- package/src/permission-manager.ts +40 -56
- package/src/permission-prompts.ts +1 -1
- package/src/permission-resolver.ts +45 -57
- package/src/permissions-service.ts +2 -4
- package/src/policy-loader.ts +2 -2
- package/src/skill-prompt-sanitizer.ts +2 -2
- package/src/tool-input-prompt-formatters.ts +1 -1
- package/src/tool-preview-formatter.ts +1 -1
- package/src/tool-registry.ts +1 -1
- package/src/{common.ts → value-guards.ts} +0 -66
- package/src/yaml-frontmatter.ts +65 -0
- package/test/access-intent/bash/node-text.test.ts +1 -0
- package/test/access-intent/bash/program.test.ts +67 -0
- package/test/handlers/before-agent-start.test.ts +6 -9
- package/test/handlers/external-directory-integration.test.ts +40 -153
- package/test/handlers/external-directory-session-dedup.test.ts +38 -286
- package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -3
- package/test/handlers/gates/bash-command.test.ts +89 -23
- package/test/handlers/gates/bash-external-directory.test.ts +37 -33
- package/test/handlers/gates/bash-path.test.ts +13 -9
- package/test/handlers/gates/external-directory-policy.test.ts +34 -22
- package/test/handlers/gates/external-directory.test.ts +11 -5
- package/test/handlers/gates/path.test.ts +30 -25
- package/test/handlers/gates/runner.test.ts +6 -1
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +2 -2
- package/test/handlers/input.test.ts +1 -1
- package/test/helpers/external-directory-fixtures.ts +269 -0
- package/test/helpers/gate-fixtures.ts +12 -24
- package/test/helpers/handler-fixtures.ts +21 -15
- package/test/helpers/session-fixtures.ts +3 -18
- package/test/permission-event-rpc.test.ts +24 -20
- package/test/permission-manager-unified.test.ts +445 -209
- package/test/permission-resolver.test.ts +112 -89
- package/test/permissions-service.test.ts +10 -7
- package/test/skill-prompt-sanitizer.test.ts +30 -7
- package/test/{common.test.ts → value-guards.test.ts} +2 -96
- package/test/yaml-frontmatter.test.ts +91 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [16.2.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v16.2.0...pi-permission-system-v16.2.1) (2026-06-27)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** floor opaque bash -c/eval wrappers to ask ([#481](https://github.com/gotgenes/pi-packages/issues/481)) ([e69493c](https://github.com/gotgenes/pi-packages/commit/e69493c00a04fdcdc1a972ef04ffee5839c93d2a))
|
|
14
|
+
* **pi-permission-system:** strip env-var assignment prefix from bash command units ([#481](https://github.com/gotgenes/pi-packages/issues/481)) ([1c99fb3](https://github.com/gotgenes/pi-packages/commit/1c99fb3835442dce3cb01b89f26db0a39084b7af))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
* **pi-permission-system:** document env-prefix stripping and opaque bash-wrapper floor ([#481](https://github.com/gotgenes/pi-packages/issues/481)) ([40c0012](https://github.com/gotgenes/pi-packages/commit/40c001270249ace6a9d471c92063f9f09aa67238))
|
|
20
|
+
* **pi-permission-system:** note opaque-wrapper sentinel in README and skill ([#481](https://github.com/gotgenes/pi-packages/issues/481)) ([e226fe7](https://github.com/gotgenes/pi-packages/commit/e226fe7a49218fac73f2dcb8c84cb13608d48fa0))
|
|
21
|
+
* **pi-permission-system:** note prefix strip and opaque flag in commands() JSDoc ([#481](https://github.com/gotgenes/pi-packages/issues/481)) ([1892098](https://github.com/gotgenes/pi-packages/commit/18920980e0b17b6dd5e51a75fd103663f9313b32))
|
|
22
|
+
|
|
23
|
+
## [16.2.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v16.1.0...pi-permission-system-v16.2.0) (2026-06-26)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
* **pi-permission-system:** add ScopedPermissionManager.check(intent) ([#478](https://github.com/gotgenes/pi-packages/issues/478)) ([7cb600a](https://github.com/gotgenes/pi-packages/commit/7cb600a11efd4bc60bd31c70d85550c7e1b28058))
|
|
29
|
+
* **pi-permission-system:** narrow ScopedPermissionResolver to resolve(intent) ([#478](https://github.com/gotgenes/pi-packages/issues/478)) ([908176f](https://github.com/gotgenes/pi-packages/commit/908176f8341e05d9181d5cf22ede10a6d5a470d0))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Documentation
|
|
33
|
+
|
|
34
|
+
* **pi-permission-system:** record resolve(intent) narrowing ([#478](https://github.com/gotgenes/pi-packages/issues/478)) ([fb8f986](https://github.com/gotgenes/pi-packages/commit/fb8f986e15e5db20d84030d0f77b2aa4aad39aab))
|
|
35
|
+
|
|
8
36
|
## [16.1.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v16.0.2...pi-permission-system-v16.1.0) (2026-06-26)
|
|
9
37
|
|
|
10
38
|
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Permission enforcement extension for the [Pi](https://pi.mariozechner.at/) codin
|
|
|
19
19
|
- **Gates MCP and skill access** at server, tool, and skill-name granularity
|
|
20
20
|
- **Protects sensitive file patterns** — cross-cutting `path` rules deny `.env`, `~/.ssh/*`, etc. across all tools and bash at once
|
|
21
21
|
- **Guards external paths** — prompts before file tools or bash commands reach outside `cwd`
|
|
22
|
-
- **Fails closed** — an internal gate error blocks the tool (with a `gate_error` review-log entry), and an unparseable bash command prompts (`ask`) rather than passing silently
|
|
22
|
+
- **Fails closed** — an internal gate error blocks the tool (with a `gate_error` review-log entry), and an unparseable bash command — or an opaque `bash -c`/`eval` wrapper — prompts (`ask`) rather than passing silently
|
|
23
23
|
- **Forwards prompts from subagents** — `ask` policies work even in non-UI execution contexts
|
|
24
24
|
- **Broadcasts UI prompt events** — `permissions:ui_prompt` fires only when the permission system is about to invoke the active user-facing permission UI
|
|
25
25
|
- **Native [`@gotgenes/pi-subagents`](https://github.com/gotgenes/pi-subagents) integration** — in-process child sessions register with the permission system automatically, enabling per-agent policy enforcement and `ask`-state forwarding to the parent UI without configuration
|
package/package.json
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AccessPath } from "#src/access-intent/access-path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Raw tool input the manager must normalize (path / bash / MCP / extension tools).
|
|
5
|
+
*
|
|
6
|
+
* The `surface` is the tool name fed to `normalizeInput` (e.g. `"read"`, `"bash"`,
|
|
7
|
+
* an MCP server name).
|
|
8
|
+
*/
|
|
9
|
+
export interface ToolAccessIntent {
|
|
10
|
+
kind: "tool";
|
|
11
|
+
/** Tool name fed to input normalization. */
|
|
12
|
+
surface: string;
|
|
13
|
+
input: unknown;
|
|
14
|
+
agentName?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Precomputed equivalent policy values for a path-shaped surface.
|
|
19
|
+
*
|
|
20
|
+
* Used by the bash-path gate (`path` surface), whose cd-resolved lexical
|
|
21
|
+
* `string[]` carry no canonical-boundary notion.
|
|
22
|
+
*/
|
|
23
|
+
export interface PathValuesAccessIntent {
|
|
24
|
+
kind: "path-values";
|
|
25
|
+
/** `"path"` or `"external_directory"`. */
|
|
26
|
+
surface: string;
|
|
27
|
+
values: readonly string[];
|
|
28
|
+
agentName?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An `AccessPath` value object for a path-shaped surface.
|
|
33
|
+
*
|
|
34
|
+
* Used by the external-directory gates; lets `AccessPath` flow into the
|
|
35
|
+
* resolver as a first-class variant so the resolver — not the gate — asks it
|
|
36
|
+
* for `matchValues()` (Tell-Don't-Ask).
|
|
37
|
+
*/
|
|
38
|
+
export interface AccessPathAccessIntent {
|
|
39
|
+
kind: "access-path";
|
|
40
|
+
surface: string;
|
|
41
|
+
path: AccessPath;
|
|
42
|
+
agentName?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What a gate emits — the full three-variant union. */
|
|
46
|
+
export type AccessIntent =
|
|
47
|
+
| ToolAccessIntent
|
|
48
|
+
| PathValuesAccessIntent
|
|
49
|
+
| AccessPathAccessIntent;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What the manager consumes — the `access-path` variant has already been
|
|
53
|
+
* unwrapped to `path-values` by the resolver via `path.matchValues()`.
|
|
54
|
+
*
|
|
55
|
+
* The manager stays string-based and never imports `AccessPath`.
|
|
56
|
+
*/
|
|
57
|
+
export type ResolvedAccessIntent = ToolAccessIntent | PathValuesAccessIntent;
|
|
@@ -18,6 +18,12 @@ export interface BashCommand {
|
|
|
18
18
|
* for a current-shell (top-level) command.
|
|
19
19
|
*/
|
|
20
20
|
readonly context?: BashCommandContext;
|
|
21
|
+
/**
|
|
22
|
+
* True when this is an opaque-payload wrapper (`bash -c`/`eval`) whose inner
|
|
23
|
+
* program is not re-parsed; its decision is floored to at least `ask` so it
|
|
24
|
+
* cannot ride a permissive `allow`.
|
|
25
|
+
*/
|
|
26
|
+
readonly opaque?: boolean;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
// ── Command enumeration ──────────────────────────────────────────────────────
|
|
@@ -75,6 +81,9 @@ const NESTED_EXECUTION_CONTEXTS = new Map<string, BashCommandContext>([
|
|
|
75
81
|
*
|
|
76
82
|
* The enclosing command/subshell is always still emitted whole, so adding the
|
|
77
83
|
* nested units can only ever produce a more-restrictive decision, never weaker.
|
|
84
|
+
*
|
|
85
|
+
* Each emitted command unit has any leading `variable_assignment` prefix
|
|
86
|
+
* stripped (so an env-var prefix cannot defeat a command-pattern rule).
|
|
78
87
|
*/
|
|
79
88
|
export function collectCommands(node: TSNode): BashCommand[] {
|
|
80
89
|
const out: BashCommand[] = [];
|
|
@@ -93,7 +102,9 @@ function collectCommandsInto(
|
|
|
93
102
|
if (COMMAND_ENUM_SKIP.has(node.type)) return;
|
|
94
103
|
|
|
95
104
|
if (node.type === "command") {
|
|
96
|
-
out.push(
|
|
105
|
+
out.push(
|
|
106
|
+
makeUnit(commandUnitText(node), context, isOpaqueWrapperCommand(node)),
|
|
107
|
+
);
|
|
97
108
|
// A command's text already contains any substitution; descend its subtree
|
|
98
109
|
// to ALSO emit the inner commands of command/process substitutions.
|
|
99
110
|
collectSubstitutionCommands(node, out);
|
|
@@ -119,8 +130,74 @@ function collectCommandsInto(
|
|
|
119
130
|
function makeUnit(
|
|
120
131
|
text: string,
|
|
121
132
|
context: BashCommandContext | undefined,
|
|
133
|
+
opaque = false,
|
|
122
134
|
): BashCommand {
|
|
123
|
-
|
|
135
|
+
const unit: BashCommand = context ? { text, context } : { text };
|
|
136
|
+
return opaque ? { ...unit, opaque } : unit;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Shell command names whose `-c` flag introduces an opaque inline program.
|
|
141
|
+
*/
|
|
142
|
+
const SHELL_WRAPPER_NAMES = new Set(["bash", "sh", "dash", "zsh", "ksh"]);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* True when a `command` node is an opaque-payload wrapper: a shell
|
|
146
|
+
* (`bash`/`sh`/`dash`/`zsh`/`ksh`) invoked with a `-c` flag, or `eval`. Its
|
|
147
|
+
* inner program is a quoted argument the enumerator does not re-parse, so the
|
|
148
|
+
* wrapper's decision is later floored to at least `ask`.
|
|
149
|
+
*
|
|
150
|
+
* The command name is matched on its basename (so `/bin/bash -c …` counts), and
|
|
151
|
+
* a `-c` flag is recognized within a short-flag cluster (`-c`, `-ec`, `-xc`).
|
|
152
|
+
* A leading `variable_assignment` prefix is skipped, matching `commandUnitText`.
|
|
153
|
+
*/
|
|
154
|
+
function isOpaqueWrapperCommand(node: TSNode): boolean {
|
|
155
|
+
let commandName: string | undefined;
|
|
156
|
+
let sawShortFlagC = false;
|
|
157
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
158
|
+
const child = node.child(i);
|
|
159
|
+
if (!child?.isNamed) continue;
|
|
160
|
+
if (child.type === "variable_assignment") continue;
|
|
161
|
+
if (commandName === undefined) {
|
|
162
|
+
commandName = basename(child.text);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const text = child.text;
|
|
166
|
+
if (text === "--") break;
|
|
167
|
+
if (text.startsWith("-") && !text.startsWith("--") && text.includes("c")) {
|
|
168
|
+
sawShortFlagC = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (commandName === undefined) return false;
|
|
172
|
+
if (commandName === "eval") return true;
|
|
173
|
+
return SHELL_WRAPPER_NAMES.has(commandName) && sawShortFlagC;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The final path segment of a command name (`/bin/bash` → `bash`). */
|
|
177
|
+
function basename(name: string): string {
|
|
178
|
+
const slash = name.lastIndexOf("/");
|
|
179
|
+
return slash === -1 ? name : name.slice(slash + 1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The command-pattern text of a `command` node, with any leading
|
|
184
|
+
* `variable_assignment` prefix stripped.
|
|
185
|
+
*
|
|
186
|
+
* An env-var prefix (`AWS_PROFILE=prod aws …`, `PGPASSWORD=…`) is part of the
|
|
187
|
+
* `command` node's text but must not defeat a rule that gates the underlying
|
|
188
|
+
* command, so matching targets the text from the first non-assignment child
|
|
189
|
+
* (the `command_name`) onward, sliced verbatim to preserve spacing. A pure
|
|
190
|
+
* assignment (`FOO=bar`, no `command_name`) runs no command and is returned
|
|
191
|
+
* unchanged.
|
|
192
|
+
*/
|
|
193
|
+
function commandUnitText(node: TSNode): string {
|
|
194
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
195
|
+
const child = node.child(i);
|
|
196
|
+
if (child?.isNamed && child.type !== "variable_assignment") {
|
|
197
|
+
return node.text.slice(child.startIndex - node.startIndex);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return node.text;
|
|
124
201
|
}
|
|
125
202
|
|
|
126
203
|
function descendCommandChildren(
|
|
@@ -8,6 +8,8 @@ import { memoizeAsyncWithRetry } from "#src/async-cache";
|
|
|
8
8
|
export interface TSNode {
|
|
9
9
|
readonly type: string;
|
|
10
10
|
readonly text: string;
|
|
11
|
+
/** Absolute byte offset of this node's start in the parsed source. */
|
|
12
|
+
readonly startIndex: number;
|
|
11
13
|
readonly childCount: number;
|
|
12
14
|
/** False for anonymous tokens (operators, delimiters); true for named nodes. */
|
|
13
15
|
readonly isNamed: boolean;
|
|
@@ -61,6 +61,9 @@ export class BashProgram {
|
|
|
61
61
|
* Splits on the shell chain operators (`&&`, `||`, `;`, `|`, `&`, newlines);
|
|
62
62
|
* quotes, command substitution, and subshells are respected by the parser and
|
|
63
63
|
* are NOT split — a subshell or other compound statement is emitted whole.
|
|
64
|
+
* Each unit has any leading `variable_assignment` prefix stripped, and an
|
|
65
|
+
* opaque-payload wrapper (`bash -c`/`eval`) is flagged `opaque` so its decision
|
|
66
|
+
* is floored to `ask`.
|
|
64
67
|
* May be empty (e.g. an empty command or a comment-only line); callers fall
|
|
65
68
|
* back to the whole command so the surface is never evaluated weaker than
|
|
66
69
|
* before.
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* through exactly the same path a third-party extension would use.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { toRecord } from "./common";
|
|
9
8
|
import type {
|
|
10
9
|
ToolInputFormatter,
|
|
11
10
|
ToolInputFormatterRegistry,
|
|
12
11
|
} from "./tool-input-formatter-registry";
|
|
13
12
|
import { truncateInlineText } from "./tool-input-preview";
|
|
13
|
+
import { toRecord } from "./value-guards";
|
|
14
14
|
|
|
15
15
|
/** Maximum total length of the generated argument summary (before "with " prefix). */
|
|
16
16
|
const MCP_ARGS_SUMMARY_MAX_LENGTH = 160;
|
package/src/config-loader.ts
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { normalize } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
isDenyWithReason,
|
|
5
|
-
isPermissionState,
|
|
6
|
-
normalizeOptionalPositiveInt,
|
|
7
|
-
normalizeOptionalStringArray,
|
|
8
|
-
toRecord,
|
|
9
|
-
} from "./common";
|
|
10
3
|
import {
|
|
11
4
|
getGlobalConfigPath,
|
|
12
5
|
getLegacyExtensionConfigPath,
|
|
@@ -16,6 +9,13 @@ import {
|
|
|
16
9
|
} from "./config-paths";
|
|
17
10
|
import { mergeFlatPermissions } from "./permission-merge";
|
|
18
11
|
import type { FlatPermissionConfig, PatternValue } from "./types";
|
|
12
|
+
import {
|
|
13
|
+
isDenyWithReason,
|
|
14
|
+
isPermissionState,
|
|
15
|
+
normalizeOptionalPositiveInt,
|
|
16
|
+
normalizeOptionalStringArray,
|
|
17
|
+
toRecord,
|
|
18
|
+
} from "./value-guards";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Unified config shape combining runtime knobs and flat permission policy.
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
getActiveAgentNameFromSystemPrompt,
|
|
6
6
|
type SessionEntryView,
|
|
7
7
|
} from "#src/active-agent";
|
|
8
|
-
import { toRecord } from "#src/common";
|
|
9
8
|
import type { ConfigReader } from "#src/config-store";
|
|
10
9
|
import type {
|
|
11
10
|
PermissionDecisionUi,
|
|
@@ -31,6 +30,7 @@ import { buildForwardedUiPrompt } from "#src/permission-ui-prompt";
|
|
|
31
30
|
import type { DebugReviewLogger } from "#src/session-logger";
|
|
32
31
|
import { isSubagentExecutionContext } from "#src/subagent-context";
|
|
33
32
|
import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
33
|
+
import { toRecord } from "#src/value-guards";
|
|
34
34
|
import { shouldAutoApprovePermissionState } from "#src/yolo-mode";
|
|
35
35
|
|
|
36
36
|
import {
|
|
@@ -19,6 +19,12 @@ import type { PermissionCheckResult } from "#src/types";
|
|
|
19
19
|
* `commandContext` (set only for a nested command), so the prompt,
|
|
20
20
|
* session-approval suggestion, and decision event scope to that command.
|
|
21
21
|
*
|
|
22
|
+
* An opaque-payload wrapper unit (`bash -c`/`eval`, flagged `opaque` by the
|
|
23
|
+
* enumerator) has its inner program hidden behind a quoted argument, so an
|
|
24
|
+
* `allow` is floored up to a synthetic `ask` (the `<opaque-bash-wrapper>`
|
|
25
|
+
* pattern) to keep it from riding a permissive rule; an explicit `deny`/`ask`
|
|
26
|
+
* on the wrapper is left untouched (`deny > ask > allow`).
|
|
27
|
+
*
|
|
22
28
|
* When `commands` is empty there are two cases. A trivially-empty command (an
|
|
23
29
|
* empty, whitespace-only, or comment-only line) has genuinely nothing to gate,
|
|
24
30
|
* so the whole `command` is resolved as before. A non-empty command that parsed
|
|
@@ -38,7 +44,12 @@ export function resolveBashCommandCheck(
|
|
|
38
44
|
): PermissionCheckResult {
|
|
39
45
|
if (commands.length === 0) {
|
|
40
46
|
if (isTriviallyEmptyCommand(command)) {
|
|
41
|
-
return resolver.resolve(
|
|
47
|
+
return resolver.resolve({
|
|
48
|
+
kind: "tool",
|
|
49
|
+
surface: "bash",
|
|
50
|
+
input: { command },
|
|
51
|
+
agentName,
|
|
52
|
+
});
|
|
42
53
|
}
|
|
43
54
|
return {
|
|
44
55
|
state: "ask",
|
|
@@ -51,12 +62,30 @@ export function resolveBashCommandCheck(
|
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
const results = commands.map((cmd) => {
|
|
54
|
-
const
|
|
65
|
+
const base = resolver.resolve({
|
|
66
|
+
kind: "tool",
|
|
67
|
+
surface: "bash",
|
|
68
|
+
input: { command: cmd.text },
|
|
69
|
+
agentName,
|
|
70
|
+
});
|
|
71
|
+
const result =
|
|
72
|
+
cmd.opaque && base.state === "allow"
|
|
73
|
+
? {
|
|
74
|
+
...base,
|
|
75
|
+
state: "ask" as const,
|
|
76
|
+
matchedPattern: "<opaque-bash-wrapper>",
|
|
77
|
+
}
|
|
78
|
+
: base;
|
|
55
79
|
return cmd.context ? { ...result, commandContext: cmd.context } : result;
|
|
56
80
|
});
|
|
57
81
|
return (
|
|
58
82
|
pickMostRestrictive(results) ??
|
|
59
|
-
resolver.resolve(
|
|
83
|
+
resolver.resolve({
|
|
84
|
+
kind: "tool",
|
|
85
|
+
surface: "bash",
|
|
86
|
+
input: { command },
|
|
87
|
+
agentName,
|
|
88
|
+
})
|
|
60
89
|
);
|
|
61
90
|
}
|
|
62
91
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { BashProgram } from "#src/access-intent/bash/program";
|
|
2
|
-
import { getNonEmptyString, toRecord } from "#src/common";
|
|
3
2
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
3
|
import { SessionApproval } from "#src/session-approval";
|
|
5
4
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
5
|
+
import { getNonEmptyString, toRecord } from "#src/value-guards";
|
|
6
6
|
import type { GateResult } from "./descriptor";
|
|
7
7
|
import { formatBashExternalDirectoryAskPrompt } from "./external-directory-messages";
|
|
8
8
|
import { selectUncoveredExternalPaths } from "./external-directory-policy";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { BashProgram } from "#src/access-intent/bash/program";
|
|
2
|
-
import { getNonEmptyString, toRecord } from "#src/common";
|
|
3
2
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
3
|
import { SessionApproval } from "#src/session-approval";
|
|
5
4
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
6
5
|
import type { PermissionCheckResult } from "#src/types";
|
|
6
|
+
import { getNonEmptyString, toRecord } from "#src/value-guards";
|
|
7
7
|
import { pickMostRestrictive } from "./candidate-check";
|
|
8
8
|
import type { GateResult } from "./descriptor";
|
|
9
9
|
import { formatPathAskPrompt } from "./path";
|
|
@@ -51,10 +51,12 @@ export function describeBashPathGate(
|
|
|
51
51
|
let allSessionCovered = true;
|
|
52
52
|
|
|
53
53
|
for (const { token, policyValues } of candidates) {
|
|
54
|
-
const check = resolver.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
const check = resolver.resolve({
|
|
55
|
+
kind: "path-values",
|
|
56
|
+
surface: "path",
|
|
57
|
+
values: policyValues,
|
|
58
|
+
agentName: tcc.agentName ?? undefined,
|
|
59
|
+
});
|
|
58
60
|
|
|
59
61
|
// No explicit path rule matched — only the universal default fired.
|
|
60
62
|
// Treat this token as unrestricted to preserve backward compatibility
|
|
@@ -19,10 +19,10 @@ export interface UncoveredExternalPaths {
|
|
|
19
19
|
/**
|
|
20
20
|
* Resolve one external path's policy on the `external_directory` surface.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* (#418). This is the single source for the
|
|
25
|
-
*
|
|
22
|
+
* Emits an `access-path` {@link AccessIntent}; the resolver unwraps it via
|
|
23
|
+
* {@link AccessPath.matchValues} so a config pattern on either the typed or
|
|
24
|
+
* symlink-resolved alias applies (#418). This is the single source for the
|
|
25
|
+
* external-directory resolve that the two external-directory gates previously
|
|
26
26
|
* duplicated.
|
|
27
27
|
*/
|
|
28
28
|
export function resolveExternalDirectoryPolicy(
|
|
@@ -30,11 +30,12 @@ export function resolveExternalDirectoryPolicy(
|
|
|
30
30
|
resolver: ScopedPermissionResolver,
|
|
31
31
|
agentName: string | undefined,
|
|
32
32
|
): PermissionCheckResult {
|
|
33
|
-
return resolver.
|
|
34
|
-
path
|
|
33
|
+
return resolver.resolve({
|
|
34
|
+
kind: "access-path",
|
|
35
|
+
surface: "external_directory",
|
|
36
|
+
path,
|
|
35
37
|
agentName,
|
|
36
|
-
|
|
37
|
-
);
|
|
38
|
+
});
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -22,11 +22,12 @@ export function describePathGate(
|
|
|
22
22
|
const filePath = getToolInputPath(tcc.toolName, tcc.input, extractors);
|
|
23
23
|
if (!filePath) return null;
|
|
24
24
|
|
|
25
|
-
const check = resolver.resolve(
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const check = resolver.resolve({
|
|
26
|
+
kind: "tool",
|
|
27
|
+
surface: "path",
|
|
28
|
+
input: { path: filePath },
|
|
29
|
+
agentName: tcc.agentName ?? undefined,
|
|
30
|
+
});
|
|
30
31
|
|
|
31
32
|
if (check.state === "allow") return null;
|
|
32
33
|
|
|
@@ -77,11 +77,12 @@ export class GateRunner {
|
|
|
77
77
|
origin: "builtin",
|
|
78
78
|
};
|
|
79
79
|
} else {
|
|
80
|
-
check = this.resolver.resolve(
|
|
81
|
-
|
|
82
|
-
descriptor.
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
check = this.resolver.resolve({
|
|
81
|
+
kind: "tool",
|
|
82
|
+
surface: descriptor.surface,
|
|
83
|
+
input: descriptor.input,
|
|
84
|
+
agentName: agentName ?? undefined,
|
|
85
|
+
});
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
// 2. Session-hit fast path
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { toRecord } from "#src/common";
|
|
2
1
|
import { normalizePathForComparison } from "#src/path-utils";
|
|
3
2
|
import { formatSkillPathAskPrompt } from "#src/permission-prompts";
|
|
4
3
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
5
4
|
import { findSkillPathMatch } from "#src/skill-prompt-sanitizer";
|
|
5
|
+
import { toRecord } from "#src/value-guards";
|
|
6
6
|
import type { GateDescriptor } from "./descriptor";
|
|
7
7
|
import type { ToolCallContext } from "./types";
|
|
8
8
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BashProgram } from "#src/access-intent/bash/program";
|
|
2
|
-
import { getNonEmptyString, toRecord } from "#src/common";
|
|
3
2
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
3
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
5
4
|
import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
ToolPreviewFormatter,
|
|
9
8
|
type ToolPreviewFormatterOptions,
|
|
10
9
|
} from "#src/tool-preview-formatter";
|
|
10
|
+
import { getNonEmptyString, toRecord } from "#src/value-guards";
|
|
11
11
|
import { resolveBashCommandCheck } from "./bash-command";
|
|
12
12
|
import { describeBashExternalDirectoryGate } from "./bash-external-directory";
|
|
13
13
|
import { describeBashPathGate } from "./bash-path";
|
|
@@ -102,11 +102,12 @@ export class ToolCallGatePipeline {
|
|
|
102
102
|
tcc.agentName ?? undefined,
|
|
103
103
|
this.resolver,
|
|
104
104
|
)
|
|
105
|
-
: this.resolver.resolve(
|
|
106
|
-
|
|
107
|
-
tcc.
|
|
108
|
-
tcc.
|
|
109
|
-
|
|
105
|
+
: this.resolver.resolve({
|
|
106
|
+
kind: "tool",
|
|
107
|
+
surface: tcc.toolName,
|
|
108
|
+
input: tcc.input,
|
|
109
|
+
agentName: tcc.agentName ?? undefined,
|
|
110
|
+
});
|
|
110
111
|
const toolDescriptor = describeToolGate(tcc, toolCheck, formatter);
|
|
111
112
|
toolDescriptor.preCheck = toolCheck;
|
|
112
113
|
return toolDescriptor;
|
|
@@ -2,8 +2,6 @@ import type {
|
|
|
2
2
|
ExtensionContext,
|
|
3
3
|
InputEventResult,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
|
|
6
|
-
import { toRecord } from "#src/common";
|
|
7
5
|
import {
|
|
8
6
|
formatMissingToolNameReason,
|
|
9
7
|
formatUnknownToolReason,
|
|
@@ -14,6 +12,7 @@ import {
|
|
|
14
12
|
getToolNameFromValue,
|
|
15
13
|
type ToolRegistry,
|
|
16
14
|
} from "#src/tool-registry";
|
|
15
|
+
import { toRecord } from "#src/value-guards";
|
|
17
16
|
import type { GateRunner } from "./gates/runner";
|
|
18
17
|
import type {
|
|
19
18
|
GateNotifier,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
import { toRecord } from "#src/common";
|
|
4
2
|
import type { DecisionRecorder } from "#src/decision-audit";
|
|
5
3
|
import type { DecisionReporter } from "#src/decision-reporter";
|
|
4
|
+
import { toRecord } from "#src/value-guards";
|
|
6
5
|
import type { GateOutcome } from "./gates/types";
|
|
7
6
|
|
|
8
7
|
/** The SDK-facing result shape for a `tool_call` handler. */
|
package/src/input-normalizer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stripBashCommentLines } from "./bash-arity";
|
|
2
|
-
import { getNonEmptyString, toRecord } from "./common";
|
|
3
2
|
import { createMcpPermissionTargets } from "./mcp-targets";
|
|
4
3
|
import { getPathPolicyValues, PATH_BEARING_TOOLS } from "./path-utils";
|
|
4
|
+
import { getNonEmptyString, toRecord } from "./value-guards";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Construct a surface-appropriate input object from a raw value string.
|
package/src/mcp-targets.ts
CHANGED
package/src/normalize.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { isDenyWithReason, isPermissionState } from "./common";
|
|
2
1
|
import type { Rule, Ruleset } from "./rule";
|
|
3
2
|
import type { FlatPermissionConfig } from "./types";
|
|
3
|
+
import { isDenyWithReason, isPermissionState } from "./value-guards";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Convert a flat permission config into a Ruleset.
|
package/src/path-utils.ts
CHANGED
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
} from "node:path";
|
|
9
9
|
|
|
10
10
|
import { canonicalizePath } from "./canonicalize-path";
|
|
11
|
-
import { getNonEmptyString, toRecord } from "./common";
|
|
12
11
|
import { expandHomePath } from "./expand-home";
|
|
13
12
|
import type { ToolAccessExtractorLookup } from "./tool-access-extractor-registry";
|
|
13
|
+
import { getNonEmptyString, toRecord } from "./value-guards";
|
|
14
14
|
import { wildcardMatch } from "./wildcard-matcher";
|
|
15
15
|
|
|
16
16
|
export function normalizePathForComparison(
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
PERMISSIONS_RPC_CHECK_CHANNEL,
|
|
27
27
|
PERMISSIONS_RPC_PROMPT_CHANNEL,
|
|
28
28
|
} from "./permission-events";
|
|
29
|
-
import type {
|
|
29
|
+
import type { ScopedPermissionManager } from "./permission-manager";
|
|
30
30
|
import { buildRpcUiPrompt } from "./permission-ui-prompt";
|
|
31
31
|
import type { ReviewLogger } from "./session-logger";
|
|
32
32
|
import type { SessionRules } from "./session-rules";
|
|
@@ -34,7 +34,7 @@ import type { SessionRules } from "./session-rules";
|
|
|
34
34
|
/** Dependencies injected into the RPC handler registry. */
|
|
35
35
|
export interface PermissionRpcDeps {
|
|
36
36
|
/** The shared PermissionManager instance. */
|
|
37
|
-
permissionManager: Pick<
|
|
37
|
+
permissionManager: Pick<ScopedPermissionManager, "check">;
|
|
38
38
|
/** The shared SessionRules instance. */
|
|
39
39
|
sessionRules: Pick<SessionRules, "getRuleset">;
|
|
40
40
|
/**
|
|
@@ -109,10 +109,8 @@ function handleCheckRpc(
|
|
|
109
109
|
|
|
110
110
|
const input = buildInputForSurface(surface, value);
|
|
111
111
|
const sessionRules = deps.sessionRules.getRuleset();
|
|
112
|
-
const result = deps.permissionManager.
|
|
113
|
-
surface,
|
|
114
|
-
input,
|
|
115
|
-
agentName ?? undefined,
|
|
112
|
+
const result = deps.permissionManager.check(
|
|
113
|
+
{ kind: "tool", surface, input, agentName: agentName ?? undefined },
|
|
116
114
|
sessionRules,
|
|
117
115
|
);
|
|
118
116
|
|