@gotgenes/pi-permission-system 20.3.0 → 20.4.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/docs/configuration.md +3 -0
- package/docs/cross-extension-api.md +4 -0
- package/docs/subagent-integration.md +2 -2
- package/package.json +1 -1
- package/src/access-intent/access-path.ts +7 -5
- package/src/access-intent/bash/bash-path-resolver.ts +1 -2
- package/src/access-intent/bash/command-enumeration.ts +98 -24
- package/src/access-intent/bash/msys-bash-tokens.ts +2 -2
- package/src/access-intent/bash/parser.ts +39 -0
- package/src/access-intent/bash/program.ts +3 -3
- package/src/access-intent/bash/sync-commands.ts +31 -0
- package/src/access-intent/bash/token-classification.ts +18 -32
- package/src/access-intent/path-normalization.ts +24 -43
- package/src/access-intent/tool-kind.ts +57 -0
- package/src/authority/approval-escalator.ts +3 -3
- package/src/authority/authorizer-selection.ts +1 -1
- package/src/authority/authorizer.ts +2 -2
- package/src/authority/denying-authorizer.ts +1 -1
- package/src/authority/forwarded-request-server.ts +3 -3
- package/src/authority/forwarder-context.ts +1 -1
- package/src/authority/forwarding-io.ts +3 -3
- package/src/{forwarding-manager.ts → authority/forwarding-manager.ts} +2 -2
- package/src/authority/local-user-authorizer.ts +2 -2
- package/src/{permission-forwarding.ts → authority/permission-forwarding.ts} +1 -2
- package/src/authority/permission-prompter.ts +2 -2
- package/src/authority/subagent-context.ts +11 -14
- package/src/authority/subagent-detection.ts +5 -4
- package/src/bash-advisory-check.ts +38 -0
- package/src/denial-messages.ts +6 -9
- package/src/handlers/before-agent-start.ts +8 -0
- package/src/handlers/gates/bash-command.ts +22 -8
- package/src/handlers/gates/helpers.ts +12 -4
- package/src/handlers/gates/runner.ts +1 -1
- package/src/handlers/gates/tool-call-gate-pipeline.ts +3 -2
- package/src/handlers/gates/tool.ts +9 -4
- package/src/index.ts +23 -12
- package/src/input-normalizer.ts +53 -48
- package/src/{canonicalize-path.ts → path/canonicalize-path.ts} +4 -3
- package/src/path/path-containment.ts +24 -0
- package/src/path/path-flavor.ts +114 -0
- package/src/{pi-infrastructure-read.ts → path/pi-infrastructure-read.ts} +12 -17
- package/src/path-normalizer.ts +35 -59
- package/src/permission-gate.ts +1 -1
- package/src/permission-manager.ts +36 -56
- package/src/permission-prompts.ts +4 -3
- package/src/permission-session.ts +6 -5
- package/src/permissions-service.ts +8 -0
- package/src/rule.ts +19 -21
- package/src/session-approval.ts +1 -1
- package/src/tool-input-path.ts +17 -19
- package/src/tool-preview-formatter.ts +2 -5
- package/src/path-containment.ts +0 -56
- /package/src/{permission-dialog.ts → authority/permission-dialog.ts} +0 -0
- /package/src/{subagent-lifecycle-events.ts → authority/subagent-lifecycle-events.ts} +0 -0
- /package/src/{subagent-registry.ts → authority/subagent-registry.ts} +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { PATH_BEARING_TOOLS } from "#src/path-surfaces";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a tool invocation accesses — decided once from the tool name at the
|
|
5
|
+
* point an invocation enters the system.
|
|
6
|
+
*
|
|
7
|
+
* This is the single dispatch point that replaces the scattered
|
|
8
|
+
* `toolName === "bash"`/`"mcp"` re-derivation across the extraction consumers
|
|
9
|
+
* (`input-normalizer`, `tool-input-path`, the tool-call gate pipeline, and
|
|
10
|
+
* `permission-manager`'s source derivation) and the presentation consumers
|
|
11
|
+
* (`tool-preview-formatter`, `permission-prompts`, `denial-messages`, and
|
|
12
|
+
* `deriveDecisionValue`), which dispatch on {@link classifyToolKind} or
|
|
13
|
+
* {@link isMcpCheck}. Adding a tool kind means editing {@link classifyToolKind}
|
|
14
|
+
* plus the exhaustive switches the compiler flags — an OCP win over silent
|
|
15
|
+
* `===` comparisons a new variant sails past (#561).
|
|
16
|
+
*
|
|
17
|
+
* The value is plain data (a string union): `tool-kind.ts` imports no
|
|
18
|
+
* `AccessPath`, so `permission-manager.ts` may consume it without breaching the
|
|
19
|
+
* string boundary formalized in ADR-0002
|
|
20
|
+
* (`docs/decisions/0002-path-values-string-boundary.md`).
|
|
21
|
+
*
|
|
22
|
+
* - `bash` — its own token-based path gates; extraction product is the command.
|
|
23
|
+
* - `mcp` — extraction product is the qualified target.
|
|
24
|
+
* - `skill` — a distinct surface `normalizeInput`/`deriveSource` treat specially.
|
|
25
|
+
* - `path` — a path-bearing built-in (`read`/`write`/`edit`/`grep`/`find`/`ls`);
|
|
26
|
+
* extraction product is `input.path`.
|
|
27
|
+
* - `extension` — every other tool, plus the `external_directory`/`path` special
|
|
28
|
+
* surfaces that reach `deriveSource` as normalized names.
|
|
29
|
+
*/
|
|
30
|
+
export type ToolKind = "bash" | "mcp" | "skill" | "path" | "extension";
|
|
31
|
+
|
|
32
|
+
/** Classify a tool name into its {@link ToolKind}. */
|
|
33
|
+
export function classifyToolKind(toolName: string): ToolKind {
|
|
34
|
+
const name = toolName.trim();
|
|
35
|
+
if (name === "bash") return "bash";
|
|
36
|
+
if (name === "mcp") return "mcp";
|
|
37
|
+
if (name === "skill") return "skill";
|
|
38
|
+
if (PATH_BEARING_TOOLS.has(name)) return "path";
|
|
39
|
+
return "extension";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The resolved-check fields that decide MCP-ness. */
|
|
43
|
+
interface McpKindFields {
|
|
44
|
+
toolName: string;
|
|
45
|
+
source: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* True when a resolved check concerns an MCP call — either the invoked tool is
|
|
50
|
+
* `mcp`, or the winning rule matched on the `mcp` surface (`source`). The
|
|
51
|
+
* `source` disjunct is why this cannot reduce to `classifyToolKind(toolName)`:
|
|
52
|
+
* `deriveSource` can set `source` to `mcp` on a result whose `toolName` is a
|
|
53
|
+
* server-qualified string.
|
|
54
|
+
*/
|
|
55
|
+
export function isMcpCheck(check: McpKindFields): boolean {
|
|
56
|
+
return check.source === "mcp" || classifyToolKind(check.toolName) === "mcp";
|
|
57
|
+
}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
sleep,
|
|
19
19
|
writeJsonFileAtomic,
|
|
20
20
|
} from "#src/authority/forwarding-io";
|
|
21
|
-
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
21
|
+
import type { PermissionPromptDecision } from "#src/authority/permission-dialog";
|
|
22
22
|
import {
|
|
23
23
|
type ForwardedPermissionRequest,
|
|
24
24
|
type ForwardedPromptDisplay,
|
|
@@ -28,10 +28,10 @@ import {
|
|
|
28
28
|
type PermissionForwardingLocation,
|
|
29
29
|
resolvePermissionForwardingTargetSessionId,
|
|
30
30
|
SUBAGENT_PARENT_SESSION_ENV_CANDIDATES,
|
|
31
|
-
} from "#src/permission-forwarding";
|
|
31
|
+
} from "#src/authority/permission-forwarding";
|
|
32
|
+
import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
|
|
32
33
|
import { buildUiPrompt } from "#src/permission-ui-prompt";
|
|
33
34
|
import type { DebugReviewLogger } from "#src/session-logger";
|
|
34
|
-
import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
35
35
|
import { toRecord } from "#src/value-guards";
|
|
36
36
|
import type { Authorizer } from "./authorizer";
|
|
37
37
|
import type { PromptPermissionDetails } from "./permission-prompter";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
2
|
+
import type { PermissionPromptDecision } from "#src/authority/permission-dialog";
|
|
3
3
|
import {
|
|
4
4
|
type Authorizer,
|
|
5
5
|
type AuthorizerSelectionDeps,
|
|
@@ -2,10 +2,10 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type {
|
|
3
3
|
PermissionPromptDecision,
|
|
4
4
|
requestPermissionDecisionFromUi,
|
|
5
|
-
} from "#src/permission-dialog";
|
|
5
|
+
} from "#src/authority/permission-dialog";
|
|
6
|
+
import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
|
|
6
7
|
import type { PermissionEventBus } from "#src/permission-events";
|
|
7
8
|
import type { DebugReviewLogger } from "#src/session-logger";
|
|
8
|
-
import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
9
9
|
import { ParentAuthorizer } from "./approval-escalator";
|
|
10
10
|
import { DenyingAuthorizer } from "./denying-authorizer";
|
|
11
11
|
import { LocalUserAuthorizer } from "./local-user-authorizer";
|
|
@@ -3,17 +3,17 @@ import {
|
|
|
3
3
|
type ForwarderContext,
|
|
4
4
|
getSessionId,
|
|
5
5
|
} from "#src/authority/forwarder-context";
|
|
6
|
-
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
6
|
+
import type { PermissionPromptDecision } from "#src/authority/permission-dialog";
|
|
7
7
|
import {
|
|
8
8
|
type ForwardedPermissionRequest,
|
|
9
9
|
type ForwardedPermissionResponse,
|
|
10
10
|
isForwardedPermissionRequestForSession,
|
|
11
11
|
type PermissionForwardingLocation,
|
|
12
|
-
} from "#src/permission-forwarding";
|
|
12
|
+
} from "#src/authority/permission-forwarding";
|
|
13
|
+
import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
|
|
13
14
|
import { SessionApproval } from "#src/session-approval";
|
|
14
15
|
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
15
16
|
import type { DebugReviewLogger } from "#src/session-logger";
|
|
16
|
-
import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
17
17
|
import type { PermissionCheckResult } from "#src/types";
|
|
18
18
|
import type { AskEscalator } from "./authorizer-selection";
|
|
19
19
|
import {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SessionEntryView } from "#src/active-agent";
|
|
2
|
-
import type { PermissionDecisionUi } from "#src/permission-dialog";
|
|
2
|
+
import type { PermissionDecisionUi } from "#src/authority/permission-dialog";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Narrow context the forwarding subsystem reads: the UI gate (`hasUI`), the
|
|
@@ -9,15 +9,15 @@ import {
|
|
|
9
9
|
writeFileSync,
|
|
10
10
|
} from "node:fs";
|
|
11
11
|
|
|
12
|
-
import { isPermissionDecisionState } from "#src/permission-dialog";
|
|
13
|
-
import type { PermissionUiPromptSource } from "#src/permission-events";
|
|
12
|
+
import { isPermissionDecisionState } from "#src/authority/permission-dialog";
|
|
14
13
|
import {
|
|
15
14
|
createPermissionForwardingLocation,
|
|
16
15
|
type ForwardedPermissionRequest,
|
|
17
16
|
type ForwardedPermissionResponse,
|
|
18
17
|
type ForwardedSessionApproval,
|
|
19
18
|
type PermissionForwardingLocation,
|
|
20
|
-
} from "#src/permission-forwarding";
|
|
19
|
+
} from "#src/authority/permission-forwarding";
|
|
20
|
+
import type { PermissionUiPromptSource } from "#src/permission-events";
|
|
21
21
|
import type { DebugReviewLogger } from "#src/session-logger";
|
|
22
22
|
|
|
23
23
|
/** Valid `permissions:ui_prompt` source values, for tolerant request reads. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { InboxProcessor } from "./
|
|
3
|
-
import type { SubagentDetector } from "./authority/subagent-detection";
|
|
2
|
+
import type { InboxProcessor } from "./forwarded-request-server";
|
|
4
3
|
import { PERMISSION_FORWARDING_POLL_INTERVAL_MS } from "./permission-forwarding";
|
|
4
|
+
import type { SubagentDetector } from "./subagent-detection";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Narrow interface for the forwarding lifecycle used by `PermissionSession`.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { buildForwardedScopeLabels } from "#src/pattern-suggest";
|
|
2
1
|
import type {
|
|
3
2
|
PermissionDecisionUi,
|
|
4
3
|
PermissionPromptDecision,
|
|
5
4
|
RequestPermissionOptions,
|
|
6
5
|
requestPermissionDecisionFromUi,
|
|
7
|
-
} from "#src/permission-dialog";
|
|
6
|
+
} from "#src/authority/permission-dialog";
|
|
7
|
+
import { buildForwardedScopeLabels } from "#src/pattern-suggest";
|
|
8
8
|
import {
|
|
9
9
|
emitUiPromptEvent,
|
|
10
10
|
type PermissionEventBus,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
|
|
2
|
+
import type { PermissionUiPromptSource } from "#src/permission-events";
|
|
3
3
|
import type { PermissionDecisionState } from "./permission-dialog";
|
|
4
|
-
import type { PermissionUiPromptSource } from "./permission-events";
|
|
5
4
|
import type { SubagentSessionRegistry } from "./subagent-registry";
|
|
6
5
|
|
|
7
6
|
export const PERMISSION_FORWARDING_POLL_INTERVAL_MS = 250;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
2
|
-
import type { ForwardedSessionApproval } from "#src/permission-forwarding";
|
|
1
|
+
import type { PermissionPromptDecision } from "#src/authority/permission-dialog";
|
|
2
|
+
import type { ForwardedSessionApproval } from "#src/authority/permission-forwarding";
|
|
3
3
|
import type { ReviewLogger } from "#src/session-logger";
|
|
4
4
|
import type { Authorizer } from "./authorizer";
|
|
5
5
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
1
|
+
import { SUBAGENT_ENV_HINT_KEYS } from "#src/authority/permission-forwarding";
|
|
2
|
+
import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
|
|
3
|
+
import type { PathFlavor } from "#src/path/path-flavor";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Narrow context for subagent detection — the only session-manager readers
|
|
@@ -17,17 +16,15 @@ export interface SubagentDetectionContext {
|
|
|
17
16
|
|
|
18
17
|
export function normalizeFilesystemPath(
|
|
19
18
|
pathValue: string,
|
|
20
|
-
|
|
19
|
+
flavor: PathFlavor,
|
|
21
20
|
): string {
|
|
22
|
-
|
|
23
|
-
const normalizedPath = impl.normalize(pathValue);
|
|
24
|
-
return platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
|
|
21
|
+
return flavor.fold(flavor.impl.normalize(pathValue));
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
function isPathWithinDirectoryForSubagent(
|
|
28
25
|
pathValue: string,
|
|
29
26
|
directory: string,
|
|
30
|
-
|
|
27
|
+
flavor: PathFlavor,
|
|
31
28
|
): boolean {
|
|
32
29
|
if (!pathValue || !directory) {
|
|
33
30
|
return false;
|
|
@@ -37,7 +34,7 @@ function isPathWithinDirectoryForSubagent(
|
|
|
37
34
|
return true;
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
const sep =
|
|
37
|
+
const sep = flavor.impl.sep;
|
|
41
38
|
const prefix = directory.endsWith(sep) ? directory : `${directory}${sep}`;
|
|
42
39
|
return pathValue.startsWith(prefix);
|
|
43
40
|
}
|
|
@@ -71,7 +68,7 @@ export function isRegisteredSubagentChild(
|
|
|
71
68
|
export function isSubagentExecutionContext(
|
|
72
69
|
ctx: SubagentDetectionContext,
|
|
73
70
|
subagentSessionsDir: string,
|
|
74
|
-
|
|
71
|
+
flavor: PathFlavor,
|
|
75
72
|
registry?: SubagentSessionRegistry,
|
|
76
73
|
): boolean {
|
|
77
74
|
// 1. Explicit registry — in-process subagent extensions register by child
|
|
@@ -99,14 +96,14 @@ export function isSubagentExecutionContext(
|
|
|
99
96
|
return false;
|
|
100
97
|
}
|
|
101
98
|
|
|
102
|
-
const normalizedSessionDir = normalizeFilesystemPath(sessionDir,
|
|
99
|
+
const normalizedSessionDir = normalizeFilesystemPath(sessionDir, flavor);
|
|
103
100
|
const normalizedSubagentRoot = normalizeFilesystemPath(
|
|
104
101
|
subagentSessionsDir,
|
|
105
|
-
|
|
102
|
+
flavor,
|
|
106
103
|
);
|
|
107
104
|
return isPathWithinDirectoryForSubagent(
|
|
108
105
|
normalizedSessionDir,
|
|
109
106
|
normalizedSubagentRoot,
|
|
110
|
-
|
|
107
|
+
flavor,
|
|
111
108
|
);
|
|
112
109
|
}
|
|
@@ -3,7 +3,8 @@ import {
|
|
|
3
3
|
isSubagentExecutionContext,
|
|
4
4
|
type SubagentDetectionContext,
|
|
5
5
|
} from "#src/authority/subagent-context";
|
|
6
|
-
import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
6
|
+
import type { SubagentSessionRegistry } from "#src/authority/subagent-registry";
|
|
7
|
+
import type { PathFlavor } from "#src/path/path-flavor";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Narrow seam for the ask-path consumers: "is the current session a subagent?"
|
|
@@ -30,7 +31,7 @@ export interface RegisteredChildDetector {
|
|
|
30
31
|
/** Composition-root inputs for {@link SubagentDetection}. */
|
|
31
32
|
export interface SubagentDetectionDeps {
|
|
32
33
|
subagentSessionsDir: string;
|
|
33
|
-
|
|
34
|
+
flavor: PathFlavor;
|
|
34
35
|
registry?: SubagentSessionRegistry;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -38,7 +39,7 @@ export interface SubagentDetectionDeps {
|
|
|
38
39
|
* Single owner of subagent detection.
|
|
39
40
|
*
|
|
40
41
|
* Constructed once in the composition root with the detection inputs
|
|
41
|
-
* (`subagentSessionsDir`, `
|
|
42
|
+
* (`subagentSessionsDir`, `flavor`, `registry`) and shared across every
|
|
42
43
|
* consumer, replacing the dep triple those consumers previously threaded
|
|
43
44
|
* individually. Delegates to the pure detection functions in
|
|
44
45
|
* {@link ./subagent-context}, holding only the deps.
|
|
@@ -52,7 +53,7 @@ export class SubagentDetection
|
|
|
52
53
|
return isSubagentExecutionContext(
|
|
53
54
|
ctx,
|
|
54
55
|
this.deps.subagentSessionsDir,
|
|
55
|
-
this.deps.
|
|
56
|
+
this.deps.flavor,
|
|
56
57
|
this.deps.registry,
|
|
57
58
|
);
|
|
58
59
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parseBashCommandsSync } from "#src/access-intent/bash/sync-commands";
|
|
2
|
+
import { resolveBashCommandCheck } from "#src/handlers/gates/bash-command";
|
|
3
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve an advisory bash query at the gate's decomposed fidelity.
|
|
8
|
+
*
|
|
9
|
+
* When the tree-sitter parser is warm, the command is decomposed into its
|
|
10
|
+
* command-pattern units and routed through the same shared orchestrator the
|
|
11
|
+
* enforcement gate uses (`resolveBashCommandCheck`) — so a chained/nested
|
|
12
|
+
* command returns the most-restrictive decision (`deny > ask > allow`) and
|
|
13
|
+
* inherits the opaque-wrapper floor (#481) and the fail-closed
|
|
14
|
+
* `<unparseable-bash-command>` sentinel (#452), at parity with the gate.
|
|
15
|
+
*
|
|
16
|
+
* In the pre-warm window (`parseBashCommandsSync` returns `null`) it falls back
|
|
17
|
+
* to the pre-#309 whole-string match, so the advisory answer is never *weaker*
|
|
18
|
+
* than before — only strengthened once warm.
|
|
19
|
+
*
|
|
20
|
+
* Synchronous, preserving `PermissionsService.checkPermission`'s sync contract:
|
|
21
|
+
* the only async step (parser init) happens earlier, at `before_agent_start`.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveBashAdvisoryCheck(
|
|
24
|
+
command: string,
|
|
25
|
+
agentName: string | undefined,
|
|
26
|
+
resolver: ScopedPermissionResolver,
|
|
27
|
+
): PermissionCheckResult {
|
|
28
|
+
const commands = parseBashCommandsSync(command);
|
|
29
|
+
if (commands === null) {
|
|
30
|
+
return resolver.resolve({
|
|
31
|
+
kind: "tool",
|
|
32
|
+
surface: "bash",
|
|
33
|
+
input: { command },
|
|
34
|
+
agentName,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return resolveBashCommandCheck(command, commands, agentName, resolver);
|
|
38
|
+
}
|
package/src/denial-messages.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyToolKind, isMcpCheck } from "./access-intent/tool-kind";
|
|
1
2
|
import { EXTENSION_ID } from "./extension-config";
|
|
2
3
|
import type { BashCommandContext, PermissionCheckResult } from "./types";
|
|
3
4
|
|
|
@@ -127,7 +128,7 @@ function buildToolDenyBody(
|
|
|
127
128
|
parts.push(`Agent '${agentName}'`);
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
if (isMcpCheck(check)) {
|
|
131
|
+
if (isMcpCheck(check) && check.target) {
|
|
131
132
|
parts.push(`is not permitted to run MCP target '${check.target}'`);
|
|
132
133
|
} else {
|
|
133
134
|
parts.push(`is not permitted to run '${check.toolName}'`);
|
|
@@ -190,10 +191,10 @@ function buildUnavailableBody(ctx: DenialContext): string {
|
|
|
190
191
|
switch (ctx.kind) {
|
|
191
192
|
case "tool": {
|
|
192
193
|
const { check } = ctx;
|
|
193
|
-
if (check.toolName === "bash" && check.command) {
|
|
194
|
+
if (classifyToolKind(check.toolName) === "bash" && check.command) {
|
|
194
195
|
return `Running bash command '${check.command}' requires approval, but no interactive UI is available.`;
|
|
195
196
|
}
|
|
196
|
-
if (isMcpCheck(check)) {
|
|
197
|
+
if (isMcpCheck(check) && check.target) {
|
|
197
198
|
return "Using tool 'mcp' requires approval, but no interactive UI is available.";
|
|
198
199
|
}
|
|
199
200
|
return `Using tool '${check.toolName}' requires approval, but no interactive UI is available.`;
|
|
@@ -220,10 +221,10 @@ function buildUserDeniedBody(
|
|
|
220
221
|
switch (ctx.kind) {
|
|
221
222
|
case "tool": {
|
|
222
223
|
const { check } = ctx;
|
|
223
|
-
if (isMcpCheck(check)) {
|
|
224
|
+
if (isMcpCheck(check) && check.target) {
|
|
224
225
|
return `User denied MCP target '${check.target}'.${reasonSuffix(denialReason)}`;
|
|
225
226
|
}
|
|
226
|
-
if (check.toolName === "bash" && check.command) {
|
|
227
|
+
if (classifyToolKind(check.toolName) === "bash" && check.command) {
|
|
227
228
|
return `User denied bash command '${check.command}'.${reasonSuffix(denialReason)}`;
|
|
228
229
|
}
|
|
229
230
|
return `User denied tool '${check.toolName}'.${reasonSuffix(denialReason)}`;
|
|
@@ -243,10 +244,6 @@ function buildUserDeniedBody(
|
|
|
243
244
|
}
|
|
244
245
|
}
|
|
245
246
|
|
|
246
|
-
function isMcpCheck(check: PermissionCheckResult): boolean {
|
|
247
|
-
return (check.source === "mcp" || check.toolName === "mcp") && !!check.target;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
247
|
/** Render an external-path disclosure list for the bash deny body's path clause. */
|
|
251
248
|
function formatExternalPathList(paths: ExternalPathDisclosure[]): string {
|
|
252
249
|
return paths
|
|
@@ -40,12 +40,16 @@ export function shouldExposeTool(
|
|
|
40
40
|
* - `session` — encapsulates all mutable session state and lifecycle operations
|
|
41
41
|
* - `resolver` — owns permission-query surface: `getToolPermission`, skill check
|
|
42
42
|
* - `toolRegistry` — Pi tool API subset (getActive + setActive)
|
|
43
|
+
* - `warmParser` — warms the tree-sitter parser so the synchronous advisory
|
|
44
|
+
* bash path can decompose at gate parity; `before_agent_start` precedes any
|
|
45
|
+
* tool call, so triggering it here closes the pre-warm window (#309)
|
|
43
46
|
*/
|
|
44
47
|
export class AgentPrepHandler {
|
|
45
48
|
constructor(
|
|
46
49
|
private readonly session: PermissionSession,
|
|
47
50
|
private readonly resolver: PermissionResolver,
|
|
48
51
|
private readonly toolRegistry: ToolRegistry,
|
|
52
|
+
private readonly warmParser: () => void,
|
|
49
53
|
) {}
|
|
50
54
|
|
|
51
55
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
@@ -53,6 +57,10 @@ export class AgentPrepHandler {
|
|
|
53
57
|
event: BeforeAgentStartPayload,
|
|
54
58
|
ctx: ExtensionContext,
|
|
55
59
|
): Promise<BeforeAgentStartEventResult> {
|
|
60
|
+
// Fire-and-forget: warming is idempotent and best-effort, so it never
|
|
61
|
+
// delays agent start. A bash advisory query before it completes falls back
|
|
62
|
+
// to whole-string matching.
|
|
63
|
+
this.warmParser();
|
|
56
64
|
this.session.activate(ctx);
|
|
57
65
|
this.session.refreshConfig(ctx);
|
|
58
66
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BashCommand,
|
|
3
|
+
WrapperKind,
|
|
4
|
+
} from "#src/access-intent/bash/command-enumeration";
|
|
2
5
|
import { pickMostRestrictive } from "#src/handlers/gates/candidate-check";
|
|
3
6
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
4
7
|
import type { PermissionCheckResult } from "#src/types";
|
|
@@ -19,11 +22,13 @@ import type { PermissionCheckResult } from "#src/types";
|
|
|
19
22
|
* `commandContext` (set only for a nested command), so the prompt,
|
|
20
23
|
* session-approval suggestion, and decision event scope to that command.
|
|
21
24
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* A wrapper unit (flagged with a `wrapperKind` by the enumerator) hides or
|
|
26
|
+
* indirects the command that should be gated, so an `allow` is floored up to a
|
|
27
|
+
* synthetic `ask` — the `<opaque-bash-wrapper>` pattern for an inline-shell
|
|
28
|
+
* payload (`bash -c`/`eval`, #481) or `<indirection-bash-wrapper>` for a
|
|
29
|
+
* prefix/exec wrapper (`sudo`/`env`/`xargs`/`find -exec`/…, #490) — to keep it
|
|
30
|
+
* from riding a permissive rule; an explicit `deny`/`ask` on the wrapper is left
|
|
31
|
+
* untouched (`deny > ask > allow`).
|
|
27
32
|
*
|
|
28
33
|
* When `commands` is empty there are two cases. A trivially-empty command (an
|
|
29
34
|
* empty, whitespace-only, or comment-only line) has genuinely nothing to gate,
|
|
@@ -36,6 +41,15 @@ import type { PermissionCheckResult } from "#src/types";
|
|
|
36
41
|
* Pure and synchronous: the (async, tree-sitter) parse happens once in the
|
|
37
42
|
* handler, which passes the decomposed `commands` here.
|
|
38
43
|
*/
|
|
44
|
+
/**
|
|
45
|
+
* The synthetic `matchedPattern` recorded when a wrapper unit's `allow` is
|
|
46
|
+
* floored to `ask`, keyed by the wrapper kind that caused the floor.
|
|
47
|
+
*/
|
|
48
|
+
const WRAPPER_SENTINEL: Record<WrapperKind, string> = {
|
|
49
|
+
"opaque-payload": "<opaque-bash-wrapper>",
|
|
50
|
+
indirection: "<indirection-bash-wrapper>",
|
|
51
|
+
};
|
|
52
|
+
|
|
39
53
|
export function resolveBashCommandCheck(
|
|
40
54
|
command: string,
|
|
41
55
|
commands: BashCommand[],
|
|
@@ -69,11 +83,11 @@ export function resolveBashCommandCheck(
|
|
|
69
83
|
agentName,
|
|
70
84
|
});
|
|
71
85
|
const result =
|
|
72
|
-
cmd.
|
|
86
|
+
cmd.wrapperKind && base.state === "allow"
|
|
73
87
|
? {
|
|
74
88
|
...base,
|
|
75
89
|
state: "ask" as const,
|
|
76
|
-
matchedPattern:
|
|
90
|
+
matchedPattern: WRAPPER_SENTINEL[cmd.wrapperKind],
|
|
77
91
|
}
|
|
78
92
|
: base;
|
|
79
93
|
return cmd.context ? { ...result, commandContext: cmd.context } : result;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyToolKind } from "#src/access-intent/tool-kind";
|
|
1
2
|
import type {
|
|
2
3
|
PermissionDecisionEvent,
|
|
3
4
|
PermissionDecisionResolution,
|
|
@@ -14,10 +15,17 @@ export function deriveDecisionValue(
|
|
|
14
15
|
check: Pick<PermissionCheckResult, "command" | "target">,
|
|
15
16
|
path?: string,
|
|
16
17
|
): string {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
switch (classifyToolKind(toolName)) {
|
|
19
|
+
case "bash":
|
|
20
|
+
return check.command ?? toolName;
|
|
21
|
+
case "mcp":
|
|
22
|
+
return check.target ?? toolName;
|
|
23
|
+
case "path":
|
|
24
|
+
case "skill":
|
|
25
|
+
case "extension":
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || intentional: an empty path falls through to toolName (the original `if (path)` truthiness)
|
|
27
|
+
return path || toolName;
|
|
28
|
+
}
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
/**
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { AskEscalator } from "#src/authority/authorizer-selection";
|
|
2
|
+
import type { PermissionPromptDecision } from "#src/authority/permission-dialog";
|
|
2
3
|
import type { DecisionReporter } from "#src/decision-reporter";
|
|
3
4
|
import {
|
|
4
5
|
formatDenyReason,
|
|
5
6
|
formatUnavailableReason,
|
|
6
7
|
formatUserDeniedReason,
|
|
7
8
|
} from "#src/denial-messages";
|
|
8
|
-
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
9
9
|
import { applyPermissionGate } from "#src/permission-gate";
|
|
10
10
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
11
11
|
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AccessPath } from "#src/access-intent/access-path";
|
|
2
2
|
import { BashProgram } from "#src/access-intent/bash/program";
|
|
3
|
+
import { classifyToolKind } from "#src/access-intent/tool-kind";
|
|
3
4
|
import type { PathNormalizer } from "#src/path-normalizer";
|
|
4
5
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
5
6
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
@@ -77,7 +78,7 @@ export class ToolCallGatePipeline {
|
|
|
77
78
|
const command = getNonEmptyString(toRecord(tcc.input).command);
|
|
78
79
|
const normalizer = this.inputs.getPathNormalizer();
|
|
79
80
|
const bashProgram =
|
|
80
|
-
tcc.toolName === "bash" && command
|
|
81
|
+
classifyToolKind(tcc.toolName) === "bash" && command
|
|
81
82
|
? await BashProgram.parse(
|
|
82
83
|
command,
|
|
83
84
|
normalizer,
|
|
@@ -159,7 +160,7 @@ export class ToolCallGatePipeline {
|
|
|
159
160
|
command: string | null,
|
|
160
161
|
normalizer: PathNormalizer,
|
|
161
162
|
): { toolCheck: PermissionCheckResult; accessPath?: AccessPath } {
|
|
162
|
-
if (tcc.toolName === "bash" && bashProgram) {
|
|
163
|
+
if (classifyToolKind(tcc.toolName) === "bash" && bashProgram) {
|
|
163
164
|
return {
|
|
164
165
|
toolCheck: resolveBashCommandCheck(
|
|
165
166
|
command ?? "",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AccessPath } from "#src/access-intent/access-path";
|
|
2
|
+
import { classifyToolKind } from "#src/access-intent/tool-kind";
|
|
2
3
|
import { PATH_BEARING_TOOLS } from "#src/path-surfaces";
|
|
3
4
|
import { suggestSessionPattern } from "#src/pattern-suggest";
|
|
4
5
|
import { formatAskPrompt } from "#src/permission-prompts";
|
|
@@ -23,10 +24,14 @@ function deriveSuggestionValue(
|
|
|
23
24
|
check: PermissionCheckResult,
|
|
24
25
|
accessPath?: AccessPath,
|
|
25
26
|
): string {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
switch (classifyToolKind(tcc.toolName)) {
|
|
28
|
+
case "bash":
|
|
29
|
+
return check.command ?? "";
|
|
30
|
+
case "mcp":
|
|
31
|
+
return check.target ?? "mcp";
|
|
32
|
+
default:
|
|
33
|
+
return accessPath ? accessPath.value() : "*";
|
|
34
|
+
}
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/**
|