@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,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, synchronous token-classification helpers for bash path extraction.
|
|
3
|
+
*
|
|
4
|
+
* Exports two classifiers consumed by `bash-program.ts`:
|
|
5
|
+
* - `classifyTokenAsPathCandidate` — strict gate for the external-directory guard.
|
|
6
|
+
* - `classifyTokenAsRuleCandidate` — broader gate for cross-cutting `path` rules.
|
|
7
|
+
*
|
|
8
|
+
* Both classifiers share the private `rejectNonPathToken` predicate that captures
|
|
9
|
+
* the seven rejection cases common to both (the production clone this module was
|
|
10
|
+
* extracted to eliminate).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ── Public classifiers ─────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Strict path-candidate classifier for the external-directory guard.
|
|
17
|
+
*
|
|
18
|
+
* Accepts tokens that unambiguously look like filesystem paths:
|
|
19
|
+
* - Absolute paths (starting with `/`)
|
|
20
|
+
* - Home-relative paths (starting with `~/`)
|
|
21
|
+
* - Parent-traversal paths (containing `..`)
|
|
22
|
+
*
|
|
23
|
+
* Returns the raw token string if it qualifies, or `null` to skip.
|
|
24
|
+
*/
|
|
25
|
+
export function classifyTokenAsPathCandidate(token: string): string | null {
|
|
26
|
+
if (rejectNonPathToken(token)) return null;
|
|
27
|
+
|
|
28
|
+
if (token.startsWith("/")) return token;
|
|
29
|
+
if (token.startsWith("~/")) return token;
|
|
30
|
+
if (token.includes("..")) return token;
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Broader token classifier for cross-cutting `path` permission rules.
|
|
37
|
+
*
|
|
38
|
+
* Accepts the same shapes as `classifyTokenAsPathCandidate`, plus:
|
|
39
|
+
* - Dot-files and `./`-relative paths (starting with `.`)
|
|
40
|
+
* - Any relative path containing `/` (e.g. `src/foo.ts`)
|
|
41
|
+
*
|
|
42
|
+
* The `~/foo` case is covered by `includes("/")` — no separate `~/` branch needed.
|
|
43
|
+
*
|
|
44
|
+
* Does NOT require the strict "must start with `/` or `~/` or contain `..`"
|
|
45
|
+
* gate that the external-directory classifier uses.
|
|
46
|
+
*
|
|
47
|
+
* Returns the raw token string if it qualifies, or `null` to skip.
|
|
48
|
+
*/
|
|
49
|
+
export function classifyTokenAsRuleCandidate(token: string): string | null {
|
|
50
|
+
if (rejectNonPathToken(token)) return null;
|
|
51
|
+
|
|
52
|
+
if (token.startsWith(".")) return token;
|
|
53
|
+
if (token.includes("/")) return token; // covers ~/ paths and all relative paths with /
|
|
54
|
+
if (token.includes("..")) return token; // bare ".." (no slash)
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Private rejection predicate ────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* URL pattern to skip tokens that look like URLs rather than paths.
|
|
63
|
+
*/
|
|
64
|
+
const URL_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Regex metacharacter sequences that are never found in real filesystem paths.
|
|
68
|
+
* If a token contains any of these, it is almost certainly a regex pattern
|
|
69
|
+
* (e.g. a grep argument) rather than a path.
|
|
70
|
+
*/
|
|
71
|
+
const REGEX_METACHAR_PATTERN = /\.\*|\.\+|\\\||\\\(|\\\)|\[.*?\]|\^\//;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Shared rejection prelude: returns `true` when a token can never be a
|
|
75
|
+
* filesystem path, regardless of which classifier is asking.
|
|
76
|
+
*
|
|
77
|
+
* Rejects: empty tokens, flags (leading `-`), env assignments (`FOO=/bar`),
|
|
78
|
+
* URLs, `@scope/package` patterns, bare-slash tokens, and regex metacharacter
|
|
79
|
+
* sequences.
|
|
80
|
+
*/
|
|
81
|
+
function rejectNonPathToken(token: string): boolean {
|
|
82
|
+
if (!token) return true;
|
|
83
|
+
if (token.startsWith("-")) return true;
|
|
84
|
+
|
|
85
|
+
// Env assignment: = appears before any / (FOO=/bar is an assignment,
|
|
86
|
+
// /foo=bar is not because the slash comes first).
|
|
87
|
+
const eqIndex = token.indexOf("=");
|
|
88
|
+
const slashIndex = token.indexOf("/");
|
|
89
|
+
if (eqIndex !== -1 && (slashIndex === -1 || eqIndex < slashIndex))
|
|
90
|
+
return true;
|
|
91
|
+
|
|
92
|
+
if (URL_PATTERN.test(token)) return true;
|
|
93
|
+
|
|
94
|
+
// @scope/package patterns (npm scoped packages) — but @/ is allowed through
|
|
95
|
+
// since it looks like an absolute-rooted path, not an npm scope.
|
|
96
|
+
if (token.startsWith("@") && !token.startsWith("@/")) return true;
|
|
97
|
+
|
|
98
|
+
// Bare-slash tokens (/, //, ///) resolve to filesystem root and are never
|
|
99
|
+
// meaningful path arguments in practice.
|
|
100
|
+
if (/^\/+$/.test(token)) return true;
|
|
101
|
+
|
|
102
|
+
if (REGEX_METACHAR_PATTERN.test(token)) return true;
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
2
|
+
|
|
3
|
+
/** Restrictiveness ordering: deny is the most restrictive, allow the least. */
|
|
4
|
+
const RESTRICTIVENESS: Record<PermissionState, number> = {
|
|
5
|
+
allow: 0,
|
|
6
|
+
ask: 1,
|
|
7
|
+
deny: 2,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Select the most restrictive permission result from a list (deny > ask > allow).
|
|
12
|
+
*
|
|
13
|
+
* The first occurrence wins on ties, so a caller passing results in candidate
|
|
14
|
+
* order receives the earliest worst case. Returns `undefined` for an empty list.
|
|
15
|
+
*
|
|
16
|
+
* Shared by the bash gates (path, external-directory) to combine the per-candidate
|
|
17
|
+
* `checkPermission` results their tree-sitter token extraction produces.
|
|
18
|
+
*/
|
|
19
|
+
export function pickMostRestrictive(
|
|
20
|
+
results: readonly PermissionCheckResult[],
|
|
21
|
+
): PermissionCheckResult | undefined {
|
|
22
|
+
let worst: PermissionCheckResult | undefined;
|
|
23
|
+
for (const result of results) {
|
|
24
|
+
if (
|
|
25
|
+
worst === undefined ||
|
|
26
|
+
RESTRICTIVENESS[result.state] > RESTRICTIVENESS[worst.state]
|
|
27
|
+
) {
|
|
28
|
+
worst = result;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return worst;
|
|
32
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { DenialContext } from "#src/denial-messages";
|
|
2
|
+
import type { PermissionDecisionEvent } from "#src/permission-events";
|
|
3
|
+
import type { PromptPermissionDetails } from "#src/permission-prompter";
|
|
4
|
+
import type { SessionApproval } from "#src/session-approval";
|
|
5
|
+
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
6
|
+
|
|
7
|
+
// ── Descriptor types ───────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pure output of a gate function — describes what to check and how to present it.
|
|
11
|
+
*
|
|
12
|
+
* The gate runner (`runGateCheck`) uses this descriptor to execute the
|
|
13
|
+
* mechanical check→log→emit→approve cycle without the gate needing to know
|
|
14
|
+
* about logging, event emission, or session-rule recording.
|
|
15
|
+
*/
|
|
16
|
+
export interface GateDescriptor {
|
|
17
|
+
/** Permission surface to check (e.g. "bash", "external_directory", "skill"). */
|
|
18
|
+
surface: string;
|
|
19
|
+
/** Input passed to checkPermission. */
|
|
20
|
+
input: unknown;
|
|
21
|
+
/** Structured denial context — the runner formats messages from this. */
|
|
22
|
+
denialContext: DenialContext;
|
|
23
|
+
/**
|
|
24
|
+
* Session-approval suggestion for the "for this session" option.
|
|
25
|
+
* Wraps either a single pattern or multiple patterns behind a unified
|
|
26
|
+
* interface — the runner never needs to know which case applies.
|
|
27
|
+
*/
|
|
28
|
+
sessionApproval?: SessionApproval;
|
|
29
|
+
/** Details passed to the interactive permission prompt (requestId is added by the runner). */
|
|
30
|
+
promptDetails: Omit<PromptPermissionDetails, "requestId">;
|
|
31
|
+
/** Extra context fields written to the review log alongside gate outcomes. */
|
|
32
|
+
logContext: Record<string, unknown>;
|
|
33
|
+
/** Surface and value for the decision event (may differ from the check surface). */
|
|
34
|
+
decision: {
|
|
35
|
+
surface: string;
|
|
36
|
+
value: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* When set, the gate has already resolved the permission state
|
|
40
|
+
* (e.g. from a skill entry match). The runner uses this directly
|
|
41
|
+
* instead of calling checkPermission.
|
|
42
|
+
*/
|
|
43
|
+
preResolved?: {
|
|
44
|
+
state: PermissionState;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* When set, the runner uses this pre-computed check result directly
|
|
48
|
+
* instead of calling checkPermission. Used when the orchestrator has
|
|
49
|
+
* already performed the check (e.g. to build messages from the result).
|
|
50
|
+
*/
|
|
51
|
+
preCheck?: PermissionCheckResult;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Early allow result — gate has determined the action without needing the runner.
|
|
56
|
+
*
|
|
57
|
+
* Used for cases like Pi infrastructure read bypass where the gate short-circuits
|
|
58
|
+
* with a deterministic allow before reaching the permission check.
|
|
59
|
+
*/
|
|
60
|
+
export interface GateBypass {
|
|
61
|
+
action: "allow";
|
|
62
|
+
/** Optional review log entry to emit. */
|
|
63
|
+
log?: { event: string; details: Record<string, unknown> };
|
|
64
|
+
/** Optional decision event to emit. */
|
|
65
|
+
decision?: PermissionDecisionEvent;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Union of possible gate function return values. */
|
|
69
|
+
export type GateResult = GateDescriptor | GateBypass | null;
|
|
70
|
+
|
|
71
|
+
// ── Type guard helpers ─────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/** Check whether a GateResult is a GateBypass (early allow). */
|
|
74
|
+
export function isGateBypass(result: GateResult): result is GateBypass {
|
|
75
|
+
return result !== null && "action" in result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Check whether a GateResult is a GateDescriptor (needs runner). */
|
|
79
|
+
export function isGateDescriptor(result: GateResult): result is GateDescriptor {
|
|
80
|
+
return result !== null && !("action" in result);
|
|
81
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function formatExternalDirectoryAskPrompt(
|
|
2
|
+
toolName: string,
|
|
3
|
+
pathValue: string,
|
|
4
|
+
cwd: string,
|
|
5
|
+
agentName?: string,
|
|
6
|
+
): string {
|
|
7
|
+
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
8
|
+
return `${subject} requested tool '${toolName}' for path '${pathValue}' outside working directory '${cwd}'. Allow this external directory access?`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatBashExternalDirectoryAskPrompt(
|
|
12
|
+
command: string,
|
|
13
|
+
externalPaths: string[],
|
|
14
|
+
cwd: string,
|
|
15
|
+
agentName?: string,
|
|
16
|
+
): string {
|
|
17
|
+
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
18
|
+
const pathList = externalPaths.join(", ");
|
|
19
|
+
return `${subject} requested bash command '${command}' which references path(s) outside working directory '${cwd}': ${pathList}. Allow this external directory access?`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalNormalizePathForComparison,
|
|
3
|
+
getExternalDirectoryPolicyValues,
|
|
4
|
+
getToolInputPath,
|
|
5
|
+
isPathOutsideWorkingDirectory,
|
|
6
|
+
isPiInfrastructureRead,
|
|
7
|
+
normalizePathForComparison,
|
|
8
|
+
} from "#src/path-utils";
|
|
9
|
+
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
10
|
+
import { SessionApproval } from "#src/session-approval";
|
|
11
|
+
import { deriveApprovalPattern } from "#src/session-rules";
|
|
12
|
+
import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
|
|
13
|
+
import type { GateResult } from "./descriptor";
|
|
14
|
+
import { formatExternalDirectoryAskPrompt } from "./external-directory-messages";
|
|
15
|
+
import type { ToolCallContext } from "./types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build a pure descriptor for the external-directory permission gate.
|
|
19
|
+
*
|
|
20
|
+
* Returns `null` when the gate does not apply (no CWD, tool is not
|
|
21
|
+
* path-bearing, or path is inside the working directory).
|
|
22
|
+
* Returns a `GateBypass` for Pi infrastructure reads.
|
|
23
|
+
* Returns a `GateDescriptor` for external paths needing a permission check.
|
|
24
|
+
*/
|
|
25
|
+
export function describeExternalDirectoryGate(
|
|
26
|
+
tcc: ToolCallContext,
|
|
27
|
+
infraDirs: string[],
|
|
28
|
+
resolver: ScopedPermissionResolver,
|
|
29
|
+
extractors?: ToolAccessExtractorLookup,
|
|
30
|
+
): GateResult {
|
|
31
|
+
if (!tcc.cwd) return null;
|
|
32
|
+
|
|
33
|
+
const externalDirectoryPath = getToolInputPath(
|
|
34
|
+
tcc.toolName,
|
|
35
|
+
tcc.input,
|
|
36
|
+
extractors,
|
|
37
|
+
);
|
|
38
|
+
if (!externalDirectoryPath) return null;
|
|
39
|
+
|
|
40
|
+
if (!isPathOutsideWorkingDirectory(externalDirectoryPath, tcc.cwd)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The boundary decision (above) and the infrastructure-read containment
|
|
45
|
+
// check (below) use the canonical, symlink-resolved path; pattern matching
|
|
46
|
+
// uses the typed and resolved aliases (#418).
|
|
47
|
+
const canonicalExtPath = canonicalNormalizePathForComparison(
|
|
48
|
+
externalDirectoryPath,
|
|
49
|
+
tcc.cwd,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// ── Pi infrastructure read bypass ──────────────────────────────────────
|
|
53
|
+
if (
|
|
54
|
+
isPiInfrastructureRead(tcc.toolName, canonicalExtPath, infraDirs, tcc.cwd)
|
|
55
|
+
) {
|
|
56
|
+
return {
|
|
57
|
+
action: "allow",
|
|
58
|
+
log: {
|
|
59
|
+
event: "permission_request.infrastructure_auto_allowed",
|
|
60
|
+
details: {
|
|
61
|
+
source: "tool_call",
|
|
62
|
+
toolCallId: tcc.toolCallId,
|
|
63
|
+
toolName: tcc.toolName,
|
|
64
|
+
agentName: tcc.agentName,
|
|
65
|
+
path: externalDirectoryPath,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
decision: {
|
|
69
|
+
surface: tcc.toolName,
|
|
70
|
+
value: externalDirectoryPath,
|
|
71
|
+
result: "allow",
|
|
72
|
+
resolution: "infrastructure_auto_allowed",
|
|
73
|
+
origin: null,
|
|
74
|
+
agentName: tcc.agentName ?? null,
|
|
75
|
+
matchedPattern: null,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Build descriptor for permission check ───────────────────────────────
|
|
81
|
+
const extDirMessage = formatExternalDirectoryAskPrompt(
|
|
82
|
+
tcc.toolName,
|
|
83
|
+
externalDirectoryPath,
|
|
84
|
+
tcc.cwd,
|
|
85
|
+
tcc.agentName ?? undefined,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Match against both the typed and symlink-resolved aliases on the
|
|
89
|
+
// external_directory surface, so a config pattern on either form applies
|
|
90
|
+
// (#418). The runner consumes this preCheck and skips its own resolve.
|
|
91
|
+
const preCheck = resolver.resolvePathPolicy(
|
|
92
|
+
getExternalDirectoryPolicyValues(externalDirectoryPath, tcc.cwd),
|
|
93
|
+
tcc.agentName ?? undefined,
|
|
94
|
+
"external_directory",
|
|
95
|
+
);
|
|
96
|
+
const pattern = deriveApprovalPattern(
|
|
97
|
+
normalizePathForComparison(externalDirectoryPath, tcc.cwd),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
surface: "external_directory",
|
|
102
|
+
input: {},
|
|
103
|
+
preCheck,
|
|
104
|
+
denialContext: {
|
|
105
|
+
kind: "external_directory",
|
|
106
|
+
toolName: tcc.toolName,
|
|
107
|
+
pathValue: externalDirectoryPath,
|
|
108
|
+
cwd: tcc.cwd,
|
|
109
|
+
agentName: tcc.agentName ?? undefined,
|
|
110
|
+
},
|
|
111
|
+
sessionApproval: SessionApproval.single("external_directory", pattern),
|
|
112
|
+
promptDetails: {
|
|
113
|
+
source: "tool_call",
|
|
114
|
+
agentName: tcc.agentName,
|
|
115
|
+
message: extDirMessage,
|
|
116
|
+
toolCallId: tcc.toolCallId,
|
|
117
|
+
toolName: tcc.toolName,
|
|
118
|
+
path: externalDirectoryPath,
|
|
119
|
+
},
|
|
120
|
+
logContext: {
|
|
121
|
+
source: "tool_call",
|
|
122
|
+
toolCallId: tcc.toolCallId,
|
|
123
|
+
toolName: tcc.toolName,
|
|
124
|
+
agentName: tcc.agentName,
|
|
125
|
+
path: externalDirectoryPath,
|
|
126
|
+
message: extDirMessage,
|
|
127
|
+
},
|
|
128
|
+
decision: {
|
|
129
|
+
surface: "external_directory",
|
|
130
|
+
value: externalDirectoryPath,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PermissionDecisionEvent,
|
|
3
|
+
PermissionDecisionResolution,
|
|
4
|
+
} from "#src/permission-events";
|
|
5
|
+
import type { PermissionCheckResult } from "#src/types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Derive the human-readable value for a decision event from a check result.
|
|
9
|
+
* Bash → extracted command; MCP → qualified target;
|
|
10
|
+
* path-bearing tools → file path; others → tool name.
|
|
11
|
+
*/
|
|
12
|
+
export function deriveDecisionValue(
|
|
13
|
+
toolName: string,
|
|
14
|
+
check: Pick<PermissionCheckResult, "command" | "target">,
|
|
15
|
+
path?: string,
|
|
16
|
+
): string {
|
|
17
|
+
if (toolName === "bash") return check.command ?? toolName;
|
|
18
|
+
if (toolName === "mcp") return check.target ?? toolName;
|
|
19
|
+
if (path) return path;
|
|
20
|
+
return toolName;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build a `PermissionDecisionEvent` from the gate's inputs.
|
|
25
|
+
*
|
|
26
|
+
* Centralises the `origin / agentName / matchedPattern ?? null` normalization
|
|
27
|
+
* that is otherwise duplicated across the session-hit path and the gate-result
|
|
28
|
+
* path in `runGateCheck`.
|
|
29
|
+
*/
|
|
30
|
+
export function buildDecisionEvent(
|
|
31
|
+
decision: { surface: string; value: string },
|
|
32
|
+
check: Pick<PermissionCheckResult, "origin" | "matchedPattern">,
|
|
33
|
+
agentName: string | null,
|
|
34
|
+
result: "allow" | "deny",
|
|
35
|
+
resolution: PermissionDecisionResolution,
|
|
36
|
+
): PermissionDecisionEvent {
|
|
37
|
+
return {
|
|
38
|
+
surface: decision.surface,
|
|
39
|
+
value: decision.value,
|
|
40
|
+
result,
|
|
41
|
+
resolution,
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- ?? null normalises undefined to null for the log record
|
|
43
|
+
origin: check.origin ?? null,
|
|
44
|
+
agentName: agentName ?? null,
|
|
45
|
+
matchedPattern: check.matchedPattern ?? null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Map the gate outcome back to a PermissionDecisionResolution.
|
|
51
|
+
*
|
|
52
|
+
* @param state - The permission state passed to the gate.
|
|
53
|
+
* @param action - The gate's resulting action ("allow" | "block").
|
|
54
|
+
* @param hasSession - True when the gate result carries a sessionApproval
|
|
55
|
+
* (indicates the user chose "for this session").
|
|
56
|
+
* @param canConfirm - Whether an interactive prompt was available.
|
|
57
|
+
*/
|
|
58
|
+
export function deriveResolution(
|
|
59
|
+
state: "allow" | "deny" | "ask",
|
|
60
|
+
action: "allow" | "block",
|
|
61
|
+
hasSession: boolean,
|
|
62
|
+
canConfirm: boolean,
|
|
63
|
+
autoApproved = false,
|
|
64
|
+
persistentApprovalScope?: "project" | "global",
|
|
65
|
+
): PermissionDecisionResolution {
|
|
66
|
+
if (state === "allow") return "policy_allow";
|
|
67
|
+
if (state === "deny") return "policy_deny";
|
|
68
|
+
// state === "ask"
|
|
69
|
+
if (action === "allow") {
|
|
70
|
+
if (autoApproved) return "auto_approved";
|
|
71
|
+
if (persistentApprovalScope === "project") return "user_approved_for_project";
|
|
72
|
+
if (persistentApprovalScope === "global") return "user_approved_globally";
|
|
73
|
+
return hasSession ? "user_approved_for_session" : "user_approved";
|
|
74
|
+
}
|
|
75
|
+
return canConfirm ? "user_denied" : "confirmation_unavailable";
|
|
76
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getToolInputPath, normalizePathForComparison } from "#src/path-utils";
|
|
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 { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
|
|
6
|
+
import type { GateDescriptor, GateResult } from "./descriptor";
|
|
7
|
+
import type { ToolCallContext } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a pure descriptor for the cross-cutting path permission gate (tools).
|
|
11
|
+
*
|
|
12
|
+
* Returns `null` when the gate does not apply (tool is not path-bearing,
|
|
13
|
+
* no extractable path, the `path` surface evaluates to `allow`, or no
|
|
14
|
+
* explicit `path` rule matched — i.e. only the universal default fired).
|
|
15
|
+
* Returns a `GateDescriptor` when the path matches a `deny` or `ask` rule.
|
|
16
|
+
*/
|
|
17
|
+
export function describePathGate(
|
|
18
|
+
tcc: ToolCallContext,
|
|
19
|
+
resolver: ScopedPermissionResolver,
|
|
20
|
+
extractors?: ToolAccessExtractorLookup,
|
|
21
|
+
): GateResult {
|
|
22
|
+
const filePath = getToolInputPath(tcc.toolName, tcc.input, extractors);
|
|
23
|
+
if (!filePath) return null;
|
|
24
|
+
|
|
25
|
+
const check = resolver.resolve(
|
|
26
|
+
"path",
|
|
27
|
+
{ path: filePath },
|
|
28
|
+
tcc.agentName ?? undefined,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (check.state === "allow") return null;
|
|
32
|
+
|
|
33
|
+
// No explicit path rule matched — only the universal default fired.
|
|
34
|
+
// Skip the gate to preserve backward compatibility: configs without a
|
|
35
|
+
// "path" key should not trigger path-level prompts (#58).
|
|
36
|
+
if (check.matchedPattern === undefined) return null;
|
|
37
|
+
|
|
38
|
+
// Resolve to the canonical (cwd-anchored, absolute) path so the approval
|
|
39
|
+
// pattern matches the policy values a later call produces.
|
|
40
|
+
const approvalPath = tcc.cwd
|
|
41
|
+
? normalizePathForComparison(filePath, tcc.cwd)
|
|
42
|
+
: filePath;
|
|
43
|
+
const pattern = deriveApprovalPattern(approvalPath);
|
|
44
|
+
|
|
45
|
+
const descriptor: GateDescriptor = {
|
|
46
|
+
surface: "path",
|
|
47
|
+
input: { path: filePath },
|
|
48
|
+
denialContext: {
|
|
49
|
+
kind: "path",
|
|
50
|
+
toolName: tcc.toolName,
|
|
51
|
+
pathValue: filePath,
|
|
52
|
+
agentName: tcc.agentName ?? undefined,
|
|
53
|
+
},
|
|
54
|
+
sessionApproval: SessionApproval.single("path", pattern),
|
|
55
|
+
promptDetails: {
|
|
56
|
+
source: "tool_call",
|
|
57
|
+
agentName: tcc.agentName,
|
|
58
|
+
message: formatPathAskPrompt(
|
|
59
|
+
tcc.toolName,
|
|
60
|
+
filePath,
|
|
61
|
+
tcc.agentName ?? undefined,
|
|
62
|
+
),
|
|
63
|
+
toolCallId: tcc.toolCallId,
|
|
64
|
+
toolName: tcc.toolName,
|
|
65
|
+
path: filePath,
|
|
66
|
+
},
|
|
67
|
+
logContext: {
|
|
68
|
+
source: "tool_call",
|
|
69
|
+
toolCallId: tcc.toolCallId,
|
|
70
|
+
toolName: tcc.toolName,
|
|
71
|
+
agentName: tcc.agentName,
|
|
72
|
+
path: filePath,
|
|
73
|
+
},
|
|
74
|
+
decision: {
|
|
75
|
+
surface: "path",
|
|
76
|
+
value: filePath,
|
|
77
|
+
},
|
|
78
|
+
preCheck: check,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return descriptor;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function formatPathAskPrompt(
|
|
85
|
+
toolName: string,
|
|
86
|
+
pathValue: string,
|
|
87
|
+
agentName?: string,
|
|
88
|
+
): string {
|
|
89
|
+
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
90
|
+
return `${subject} requested tool '${toolName}' for path '${pathValue}'. Allow this path access?`;
|
|
91
|
+
}
|