@gotgenes/pi-permission-system 20.0.0 → 20.2.0
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 +25 -0
- package/docs/configuration.md +3 -0
- package/docs/cross-extension-api.md +2 -1
- package/package.json +1 -1
- package/src/access-intent/bash/bash-path-resolver.ts +6 -1
- package/src/access-intent/bash/token-classification.ts +30 -1
- package/src/authority/approval-escalator.ts +37 -89
- package/src/authority/authorizer-selection.ts +86 -0
- package/src/authority/authorizer.ts +72 -0
- package/src/authority/denying-authorizer.ts +20 -0
- package/src/authority/forwarded-request-server.ts +163 -89
- package/src/authority/local-user-authorizer.ts +51 -0
- package/src/{permission-prompter.ts → authority/permission-prompter.ts} +45 -49
- package/src/authority/subagent-detection.ts +1 -1
- package/src/config-loader.ts +1 -1
- package/src/handlers/gates/descriptor.ts +1 -1
- package/src/handlers/gates/helpers.ts +4 -3
- package/src/handlers/gates/runner.ts +8 -7
- package/src/index.ts +46 -34
- package/src/normalize.ts +1 -1
- package/src/path-normalizer.ts +12 -0
- package/src/permission-dialog.ts +8 -0
- package/src/permission-forwarding.ts +2 -1
- package/src/permission-gate.ts +15 -21
- package/src/permission-manager.ts +1 -1
- package/src/permission-session.ts +6 -7
- package/src/permission-ui-prompt.ts +39 -54
- package/src/session-logger.ts +1 -1
- package/src/types.ts +20 -0
- package/src/value-guards.ts +0 -22
- package/src/gate-prompter.ts +0 -12
- package/src/prompting-gateway.ts +0 -89
package/src/index.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { getAgentDir, getPackageDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import {
|
|
4
|
-
ApprovalEscalator,
|
|
5
|
-
type ApprovalEscalatorDeps,
|
|
6
|
-
} from "./authority/approval-escalator";
|
|
3
|
+
import { AuthorizerSelection } from "./authority/authorizer-selection";
|
|
7
4
|
import {
|
|
8
5
|
ForwardedRequestServer,
|
|
9
|
-
type
|
|
6
|
+
type ServingPolicy,
|
|
10
7
|
} from "./authority/forwarded-request-server";
|
|
8
|
+
import { PermissionPrompter } from "./authority/permission-prompter";
|
|
11
9
|
import { SubagentDetection } from "./authority/subagent-detection";
|
|
12
10
|
import { registerBuiltinToolInputFormatters } from "./builtin-tool-input-formatters";
|
|
13
11
|
import { registerPermissionSystemCommand } from "./config-modal";
|
|
@@ -27,13 +25,12 @@ import { GateRunner } from "./handlers/gates/runner";
|
|
|
27
25
|
import { SkillInputGatePipeline } from "./handlers/gates/skill-input-gate-pipeline";
|
|
28
26
|
import { ToolCallGatePipeline } from "./handlers/gates/tool-call-gate-pipeline";
|
|
29
27
|
import { createFailClosedToolCall } from "./handlers/tool-call-boundary";
|
|
28
|
+
import { buildAccessIntentForSurface } from "./input-normalizer";
|
|
30
29
|
import { requestPermissionDecisionFromUi } from "./permission-dialog";
|
|
31
30
|
import { PermissionManager } from "./permission-manager";
|
|
32
|
-
import { PermissionPrompter } from "./permission-prompter";
|
|
33
31
|
import { PermissionResolver } from "./permission-resolver";
|
|
34
32
|
import { PermissionSession } from "./permission-session";
|
|
35
33
|
import { LocalPermissionsService } from "./permissions-service";
|
|
36
|
-
import { PromptingGateway } from "./prompting-gateway";
|
|
37
34
|
import { PermissionServiceLifecycle } from "./service-lifecycle";
|
|
38
35
|
import { PermissionSessionLogger } from "./session-logger";
|
|
39
36
|
import { SessionRules } from "./session-rules";
|
|
@@ -94,33 +91,48 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
94
91
|
logger,
|
|
95
92
|
});
|
|
96
93
|
|
|
97
|
-
const
|
|
98
|
-
|
|
94
|
+
const prompter = new PermissionPrompter({ logger });
|
|
95
|
+
|
|
96
|
+
const authorizerSelection = new AuthorizerSelection({
|
|
99
97
|
detection: subagentDetection,
|
|
100
|
-
|
|
101
|
-
logger,
|
|
98
|
+
events: pi.events,
|
|
102
99
|
requestPermissionDecisionFromUi,
|
|
103
|
-
};
|
|
104
|
-
const escalator = new ApprovalEscalator(escalatorDeps);
|
|
105
|
-
|
|
106
|
-
const requestServerDeps: ForwardedRequestServerDeps = {
|
|
107
100
|
forwardingDir: paths.forwardingDir,
|
|
101
|
+
registry: subagentRegistry,
|
|
108
102
|
logger,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
prompter,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Resolver composes the manager + session ruleset and owns the
|
|
107
|
+
// access-path → path-values unwrap. Constructed here (before `session`) so
|
|
108
|
+
// the forwarded-request server's ServingPolicy can resolve against it; the
|
|
109
|
+
// service and gates below share this one instance.
|
|
110
|
+
const resolver = new PermissionResolver(permissionManager, sessionRules);
|
|
111
|
+
|
|
112
|
+
// Serving a forwarded request is resolution: evaluate (surface, value)
|
|
113
|
+
// against the serving node's composed base ruleset (agentName undefined —
|
|
114
|
+
// the child already applied its own per-agent overrides before forwarding).
|
|
115
|
+
// The session.getPathNormalizer() read is deferred behind the closure: inbox
|
|
116
|
+
// polling starts at session_start, after `session` is assigned — the same
|
|
117
|
+
// deferred-binding precedent as the logger notify sink below.
|
|
118
|
+
const servingPolicy: ServingPolicy = {
|
|
119
|
+
check: (surface, value) =>
|
|
120
|
+
resolver.resolve(
|
|
121
|
+
buildAccessIntentForSurface(
|
|
122
|
+
surface,
|
|
123
|
+
value ?? undefined,
|
|
124
|
+
session.getPathNormalizer(),
|
|
125
|
+
undefined,
|
|
126
|
+
),
|
|
127
|
+
),
|
|
112
128
|
};
|
|
113
|
-
const requestServer = new ForwardedRequestServer(requestServerDeps);
|
|
114
129
|
|
|
115
|
-
const
|
|
130
|
+
const requestServer = new ForwardedRequestServer({
|
|
131
|
+
forwardingDir: paths.forwardingDir,
|
|
116
132
|
logger,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const gateway = new PromptingGateway({
|
|
122
|
-
detection: subagentDetection,
|
|
123
|
-
prompter,
|
|
133
|
+
policy: servingPolicy,
|
|
134
|
+
escalator: authorizerSelection,
|
|
135
|
+
registry: subagentRegistry,
|
|
124
136
|
});
|
|
125
137
|
|
|
126
138
|
session = new PermissionSession(
|
|
@@ -129,7 +141,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
129
141
|
permissionManager,
|
|
130
142
|
sessionRules,
|
|
131
143
|
configStore,
|
|
132
|
-
|
|
144
|
+
authorizerSelection,
|
|
133
145
|
hostPlatform,
|
|
134
146
|
);
|
|
135
147
|
|
|
@@ -148,11 +160,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
148
160
|
),
|
|
149
161
|
});
|
|
150
162
|
|
|
151
|
-
// Resolver composes the manager + session ruleset and owns the
|
|
152
|
-
// access-path → path-values unwrap; the service routes its policy
|
|
153
|
-
// queries through it, so it is constructed before it.
|
|
154
|
-
const resolver = new PermissionResolver(permissionManager, sessionRules);
|
|
155
|
-
|
|
156
163
|
const permissionsService = new LocalPermissionsService(
|
|
157
164
|
resolver,
|
|
158
165
|
session,
|
|
@@ -196,7 +203,12 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
196
203
|
const agentPrep = new AgentPrepHandler(session, resolver, toolRegistry);
|
|
197
204
|
|
|
198
205
|
const reporter = new GateDecisionReporter(logger, pi.events);
|
|
199
|
-
const gateRunner = new GateRunner(
|
|
206
|
+
const gateRunner = new GateRunner(
|
|
207
|
+
resolver,
|
|
208
|
+
sessionRules,
|
|
209
|
+
authorizerSelection,
|
|
210
|
+
reporter,
|
|
211
|
+
);
|
|
200
212
|
const toolCallGatePipeline = new ToolCallGatePipeline(
|
|
201
213
|
resolver,
|
|
202
214
|
session,
|
package/src/normalize.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Rule, Ruleset } from "./rule";
|
|
2
2
|
import type { FlatPermissionConfig } from "./types";
|
|
3
|
-
import { isDenyWithReason, isPermissionState } from "./
|
|
3
|
+
import { isDenyWithReason, isPermissionState } from "./types";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Convert a flat permission config into a Ruleset.
|
package/src/path-normalizer.ts
CHANGED
|
@@ -112,6 +112,18 @@ export class PathNormalizer {
|
|
|
112
112
|
return this.impl.isAbsolute(pathValue);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* True when the host platform treats a backslash as a path separator (win32).
|
|
117
|
+
*
|
|
118
|
+
* The bash rule-candidate classifier reads this to decide whether a
|
|
119
|
+
* backslash-relative token (`dir\file`) is path-shaped: on win32 `\` is a
|
|
120
|
+
* separator, but on POSIX it is a legal filename character (#520). Keeping the
|
|
121
|
+
* decision here means no bash-layer module re-reads `process.platform`.
|
|
122
|
+
*/
|
|
123
|
+
usesWindowsSeparators(): boolean {
|
|
124
|
+
return this.platform === "win32";
|
|
125
|
+
}
|
|
126
|
+
|
|
115
127
|
/**
|
|
116
128
|
* Interpret a literal `cd` target's effect on the effective base.
|
|
117
129
|
*
|
package/src/permission-dialog.ts
CHANGED
|
@@ -14,6 +14,14 @@ export type PermissionPromptDecision = {
|
|
|
14
14
|
* rather than "user_approved" in the permissions:decision broadcast.
|
|
15
15
|
*/
|
|
16
16
|
autoApproved?: true;
|
|
17
|
+
/**
|
|
18
|
+
* True when no live authority was reachable and the DenyingAuthorizer denied
|
|
19
|
+
* this ask (a no-UI, non-subagent session). Consumed by deriveResolution (the
|
|
20
|
+
* decision-event resolution), the gate (block reason), and PermissionPrompter
|
|
21
|
+
* (review-entry resolution) to emit "confirmation_unavailable" rather than a
|
|
22
|
+
* plain user denial.
|
|
23
|
+
*/
|
|
24
|
+
confirmationUnavailable?: true;
|
|
17
25
|
};
|
|
18
26
|
|
|
19
27
|
export interface PermissionDecisionUi {
|
|
@@ -44,7 +44,8 @@ const SESSION_FORWARDING_RESPONSES_DIRECTORY_NAME = "responses";
|
|
|
44
44
|
* can emit a non-degraded `permissions:ui_prompt` event.
|
|
45
45
|
*
|
|
46
46
|
* Carried separately from the prompt message because the parent reconstructs
|
|
47
|
-
* the original event
|
|
47
|
+
* the original event from the escalated ask's details (`buildUiPrompt`), not
|
|
48
|
+
* from the message text.
|
|
48
49
|
*/
|
|
49
50
|
export interface ForwardedPromptDisplay {
|
|
50
51
|
source: PermissionUiPromptSource;
|
package/src/permission-gate.ts
CHANGED
|
@@ -10,10 +10,11 @@ export interface PermissionGateParams {
|
|
|
10
10
|
/** The resolved permission state from checkPermission(). */
|
|
11
11
|
state: "allow" | "deny" | "ask";
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Escalate the ask to the session's Authorizer for a decision. Called for
|
|
15
|
+
* every `ask`; the DenyingAuthorizer answers by denying with the
|
|
16
|
+
* `confirmationUnavailable` marker when no live authority is reachable.
|
|
17
|
+
*/
|
|
17
18
|
promptForApproval: () => Promise<PermissionPromptDecision>;
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -45,14 +46,7 @@ export interface PermissionGateParams {
|
|
|
45
46
|
export async function applyPermissionGate(
|
|
46
47
|
params: PermissionGateParams,
|
|
47
48
|
): Promise<PermissionGateResult> {
|
|
48
|
-
const {
|
|
49
|
-
state,
|
|
50
|
-
canConfirm,
|
|
51
|
-
promptForApproval,
|
|
52
|
-
writeLog,
|
|
53
|
-
logContext,
|
|
54
|
-
messages,
|
|
55
|
-
} = params;
|
|
49
|
+
const { state, promptForApproval, writeLog, logContext, messages } = params;
|
|
56
50
|
|
|
57
51
|
if (state === "deny") {
|
|
58
52
|
writeLog("permission_request.blocked", {
|
|
@@ -63,17 +57,17 @@ export async function applyPermissionGate(
|
|
|
63
57
|
}
|
|
64
58
|
|
|
65
59
|
if (state === "ask") {
|
|
66
|
-
if (!canConfirm) {
|
|
67
|
-
writeLog("permission_request.blocked", {
|
|
68
|
-
...logContext,
|
|
69
|
-
resolution: "confirmation_unavailable",
|
|
70
|
-
});
|
|
71
|
-
return { action: "block", reason: messages.unavailableReason };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
60
|
const decision = await promptForApproval();
|
|
75
61
|
if (!decision.approved) {
|
|
76
|
-
|
|
62
|
+
// The gate writes no review entry for an ask denial — the prompter
|
|
63
|
+
// brackets it (waiting/denied). The block reason distinguishes an
|
|
64
|
+
// absent-authority denial (confirmationUnavailable) from a user denial.
|
|
65
|
+
return {
|
|
66
|
+
action: "block",
|
|
67
|
+
reason: decision.confirmationUnavailable
|
|
68
|
+
? messages.unavailableReason
|
|
69
|
+
: messages.userDeniedReason(decision),
|
|
70
|
+
};
|
|
77
71
|
}
|
|
78
72
|
if (decision.state === "approved_for_session" && params.sessionApproval) {
|
|
79
73
|
return { action: "allow", sessionApproval: params.sessionApproval };
|
|
@@ -34,7 +34,7 @@ import type {
|
|
|
34
34
|
PermissionCheckResult,
|
|
35
35
|
PermissionState,
|
|
36
36
|
} from "./types";
|
|
37
|
-
import { isPermissionState } from "./
|
|
37
|
+
import { isPermissionState } from "./types";
|
|
38
38
|
import { wildcardMatch } from "./wildcard-matcher";
|
|
39
39
|
|
|
40
40
|
const BUILT_IN_TOOL_PERMISSION_NAMES = new Set([
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
getActiveAgentName,
|
|
4
4
|
getActiveAgentNameFromSystemPrompt,
|
|
5
5
|
} from "./active-agent";
|
|
6
|
-
|
|
6
|
+
import type { AuthorizerSelectionLifecycle } from "./authority/authorizer-selection";
|
|
7
7
|
import type { SessionConfigStore } from "./config-store";
|
|
8
8
|
import type { PermissionSystemExtensionConfig } from "./extension-config";
|
|
9
9
|
import type { ExtensionPaths } from "./extension-paths";
|
|
@@ -11,7 +11,6 @@ import type { ForwardingController } from "./forwarding-manager";
|
|
|
11
11
|
import type { ToolCallGateInputs } from "./handlers/gates/tool-call-gate-pipeline";
|
|
12
12
|
import { PathNormalizer } from "./path-normalizer";
|
|
13
13
|
import type { ScopedPermissionManager } from "./permission-manager";
|
|
14
|
-
import type { PromptingGatewayLifecycle } from "./prompting-gateway";
|
|
15
14
|
|
|
16
15
|
import type { SessionRules } from "./session-rules";
|
|
17
16
|
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
@@ -33,7 +32,7 @@ import type { PathRuleTokenMatcher } from "./types";
|
|
|
33
32
|
* - `ExtensionPaths` — immutable path constants
|
|
34
33
|
* - `ForwardingController` — polling lifecycle
|
|
35
34
|
* - `SessionConfigStore` — owns extension config; provides refresh, log, read
|
|
36
|
-
* - `
|
|
35
|
+
* - `AuthorizerSelectionLifecycle` — authorizer-selection lifecycle forwarded via activate/deactivate
|
|
37
36
|
*/
|
|
38
37
|
export class PermissionSession implements ToolCallGateInputs {
|
|
39
38
|
private context: ExtensionContext | null = null;
|
|
@@ -47,7 +46,7 @@ export class PermissionSession implements ToolCallGateInputs {
|
|
|
47
46
|
private readonly permissionManager: ScopedPermissionManager,
|
|
48
47
|
private readonly sessionRules: SessionRules,
|
|
49
48
|
private readonly configStore: SessionConfigStore,
|
|
50
|
-
private readonly
|
|
49
|
+
private readonly authorizerSelection: AuthorizerSelectionLifecycle,
|
|
51
50
|
private readonly platform: NodeJS.Platform,
|
|
52
51
|
) {
|
|
53
52
|
// Placeholder until the first activate(ctx) binds the real cwd; every gate
|
|
@@ -71,14 +70,14 @@ export class PermissionSession implements ToolCallGateInputs {
|
|
|
71
70
|
this.context = ctx;
|
|
72
71
|
this.pathNormalizer = new PathNormalizer(this.platform, ctx.cwd);
|
|
73
72
|
this.forwarding.start(ctx);
|
|
74
|
-
this.
|
|
73
|
+
this.authorizerSelection.activate(ctx);
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
/** Clear the context, stop forwarding, and deactivate the
|
|
76
|
+
/** Clear the context, stop forwarding, and deactivate the authorizer selection. */
|
|
78
77
|
deactivate(): void {
|
|
79
78
|
this.context = null;
|
|
80
79
|
this.forwarding.stop();
|
|
81
|
-
this.
|
|
80
|
+
this.authorizerSelection.deactivate();
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
/** Return the current runtime context, or null if not activated. */
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized construction for `permissions:ui_prompt` payloads.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* public contract's shape — including the normalized `surface`/`value`
|
|
6
|
-
* projection — lives in exactly one place and
|
|
4
|
+
* The single builder `buildUiPrompt` handles both direct and forwarded asks, so
|
|
5
|
+
* the public contract's shape — including the normalized `surface`/`value`
|
|
6
|
+
* projection and the `forwarding` context — lives in exactly one place and
|
|
7
|
+
* cannot drift by source.
|
|
7
8
|
*
|
|
8
9
|
* This module is a leaf: it owns narrow input types that each call site's
|
|
9
10
|
* domain object satisfies structurally, so it imports nothing from the
|
|
@@ -11,8 +12,8 @@
|
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import type {
|
|
15
|
+
ForwardedPromptContext,
|
|
14
16
|
PermissionUiPromptEvent,
|
|
15
|
-
PermissionUiPromptSource,
|
|
16
17
|
} from "./permission-events";
|
|
17
18
|
|
|
18
19
|
/** Input for a direct (non-forwarded) tool or skill prompt. */
|
|
@@ -28,18 +29,42 @@ export interface DirectPromptInput {
|
|
|
28
29
|
target?: string;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Input for any UI prompt — direct or forwarded.
|
|
34
|
+
*
|
|
35
|
+
* A direct prompt supplies only the `DirectPromptInput` fields and lets
|
|
36
|
+
* `surface`/`value` derive and `forwarding` default to `null`; a forwarded ask
|
|
37
|
+
* supplies the child's original `surface`/`value` projection explicitly and a
|
|
38
|
+
* populated `forwarding` context, so the parent's broadcast stays non-degraded
|
|
39
|
+
* (the #292 contract hardening).
|
|
40
|
+
*/
|
|
41
|
+
export interface UiPromptInput extends DirectPromptInput {
|
|
42
|
+
/** Explicit display surface; falls back to the derived projection when omitted. */
|
|
40
43
|
surface?: string | null;
|
|
41
|
-
/**
|
|
44
|
+
/** Explicit display value; falls back to the derived projection when omitted. */
|
|
42
45
|
value?: string | null;
|
|
46
|
+
/** Forwarding context for a forwarded subagent ask; `null`/omitted for a direct prompt. */
|
|
47
|
+
forwarding?: ForwardedPromptContext | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a `permissions:ui_prompt` event from either a direct or a forwarded ask.
|
|
52
|
+
*
|
|
53
|
+
* `surface`/`value` use the explicit override when the caller sets them (an
|
|
54
|
+
* explicit `null` is honored, not treated as "derive"); otherwise they fall
|
|
55
|
+
* back to the direct-prompt projection. `forwarding` passes through, defaulting
|
|
56
|
+
* to `null`.
|
|
57
|
+
*/
|
|
58
|
+
export function buildUiPrompt(input: UiPromptInput): PermissionUiPromptEvent {
|
|
59
|
+
return {
|
|
60
|
+
requestId: input.requestId,
|
|
61
|
+
source: input.source,
|
|
62
|
+
surface: input.surface !== undefined ? input.surface : directSurface(input),
|
|
63
|
+
value: input.value !== undefined ? input.value : directValue(input),
|
|
64
|
+
agentName: input.agentName,
|
|
65
|
+
message: input.message,
|
|
66
|
+
forwarding: input.forwarding ?? null,
|
|
67
|
+
};
|
|
43
68
|
}
|
|
44
69
|
|
|
45
70
|
/** Normalized display surface for a direct prompt. */
|
|
@@ -61,43 +86,3 @@ function directValue(input: DirectPromptInput): string | null {
|
|
|
61
86
|
null
|
|
62
87
|
);
|
|
63
88
|
}
|
|
64
|
-
|
|
65
|
-
/** Build the UI prompt event for a direct tool/skill prompt. */
|
|
66
|
-
export function buildDirectUiPrompt(
|
|
67
|
-
input: DirectPromptInput,
|
|
68
|
-
): PermissionUiPromptEvent {
|
|
69
|
-
return {
|
|
70
|
-
requestId: input.requestId,
|
|
71
|
-
source: input.source,
|
|
72
|
-
surface: directSurface(input),
|
|
73
|
-
value: directValue(input),
|
|
74
|
-
agentName: input.agentName,
|
|
75
|
-
message: input.message,
|
|
76
|
-
forwarding: null,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Build the UI prompt event for a file-forwarded subagent prompt.
|
|
82
|
-
*
|
|
83
|
-
* `source` defaults to `"tool_call"` (the dominant forwarded origin) when the
|
|
84
|
-
* persisted request predates carrying it — a parent on a newer version may read
|
|
85
|
-
* a request written by an older child during an upgrade. The consumer still
|
|
86
|
-
* receives the notify-now signal, message, and forwarding context.
|
|
87
|
-
*/
|
|
88
|
-
export function buildForwardedUiPrompt(
|
|
89
|
-
input: ForwardedPromptInput,
|
|
90
|
-
): PermissionUiPromptEvent {
|
|
91
|
-
return {
|
|
92
|
-
requestId: input.requestId,
|
|
93
|
-
source: input.source ?? "tool_call",
|
|
94
|
-
surface: input.surface ?? null,
|
|
95
|
-
value: input.value ?? null,
|
|
96
|
-
agentName: input.requesterAgentName,
|
|
97
|
-
message: input.message,
|
|
98
|
-
forwarding: {
|
|
99
|
-
requesterAgentName: input.requesterAgentName,
|
|
100
|
-
requesterSessionId: input.requesterSessionId,
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
}
|
package/src/session-logger.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface ReviewLogger {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Logging seam for consumers that write both debug and review entries.
|
|
22
|
-
* Injected into `ConfigStore`, `
|
|
22
|
+
* Injected into `ConfigStore`, `ParentAuthorizer`, and `ForwardedRequestServer`.
|
|
23
23
|
*/
|
|
24
24
|
export interface DebugReviewLogger extends ReviewLogger {
|
|
25
25
|
debug(event: string, details?: Record<string, unknown>): void;
|
package/src/types.ts
CHANGED
|
@@ -63,3 +63,23 @@ export interface PermissionCheckResult {
|
|
|
63
63
|
*/
|
|
64
64
|
commandContext?: BashCommandContext;
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
export function isPermissionState(value: unknown): value is PermissionState {
|
|
68
|
+
return value === "allow" || value === "deny" || value === "ask";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Narrow type guard: a raw value representing a DenyWithReason object.
|
|
73
|
+
* Accepts `{ action: "deny" }` and `{ action: "deny", reason: "…" }`.
|
|
74
|
+
* Rejects a non-string `reason` to keep malformed config out of the rule set.
|
|
75
|
+
*/
|
|
76
|
+
export function isDenyWithReason(value: unknown): value is DenyWithReason {
|
|
77
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const record = value as Record<string, unknown>;
|
|
81
|
+
return (
|
|
82
|
+
record.action === "deny" &&
|
|
83
|
+
(record.reason === undefined || typeof record.reason === "string")
|
|
84
|
+
);
|
|
85
|
+
}
|
package/src/value-guards.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { DenyWithReason, PermissionState } from "./types";
|
|
2
|
-
|
|
3
1
|
export function toRecord(value: unknown): Record<string, unknown> {
|
|
4
2
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5
3
|
return {};
|
|
@@ -16,23 +14,3 @@ export function getNonEmptyString(value: unknown): string | null {
|
|
|
16
14
|
const trimmed = value.trim();
|
|
17
15
|
return trimmed.length > 0 ? trimmed : null;
|
|
18
16
|
}
|
|
19
|
-
|
|
20
|
-
export function isPermissionState(value: unknown): value is PermissionState {
|
|
21
|
-
return value === "allow" || value === "deny" || value === "ask";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Narrow type guard: a raw value representing a DenyWithReason object.
|
|
26
|
-
* Accepts `{ action: "deny" }` and `{ action: "deny", reason: "…" }`.
|
|
27
|
-
* Rejects a non-string `reason` to keep malformed config out of the rule set.
|
|
28
|
-
*/
|
|
29
|
-
export function isDenyWithReason(value: unknown): value is DenyWithReason {
|
|
30
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
const record = value as Record<string, unknown>;
|
|
34
|
-
return (
|
|
35
|
-
record.action === "deny" &&
|
|
36
|
-
(record.reason === undefined || typeof record.reason === "string")
|
|
37
|
-
);
|
|
38
|
-
}
|
package/src/gate-prompter.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { PermissionPromptDecision } from "./permission-dialog";
|
|
2
|
-
import type { PromptPermissionDetails } from "./permission-prompter";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The prompting role the gate runner needs: a yes/no on whether an
|
|
6
|
-
* interactive confirmation is possible, and the prompt itself. The context
|
|
7
|
-
* is bound by the implementor, not threaded per call.
|
|
8
|
-
*/
|
|
9
|
-
export interface GatePrompter {
|
|
10
|
-
canConfirm(): boolean;
|
|
11
|
-
prompt(details: PromptPermissionDetails): Promise<PermissionPromptDecision>;
|
|
12
|
-
}
|
package/src/prompting-gateway.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { SubagentDetector } from "./authority/subagent-detection";
|
|
3
|
-
import type { GatePrompter } from "./gate-prompter";
|
|
4
|
-
import type { PermissionPromptDecision } from "./permission-dialog";
|
|
5
|
-
import type {
|
|
6
|
-
PermissionPrompterApi,
|
|
7
|
-
PromptPermissionDetails,
|
|
8
|
-
} from "./permission-prompter";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Dependencies required by PromptingGateway.
|
|
12
|
-
*
|
|
13
|
-
* All fields are actively consumed:
|
|
14
|
-
* - `detection` drives `canConfirm()`.
|
|
15
|
-
* - `prompter` is called by `prompt()`.
|
|
16
|
-
*/
|
|
17
|
-
export interface PromptingGatewayDeps {
|
|
18
|
-
/** Single owner of subagent detection; drives `canConfirm()`. */
|
|
19
|
-
detection: SubagentDetector;
|
|
20
|
-
/** Resolves the permission decision: direct UI dialog or forwarded to parent. */
|
|
21
|
-
prompter: PermissionPrompterApi;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The lifecycle slice of the gateway that PermissionSession drives.
|
|
26
|
-
*
|
|
27
|
-
* PermissionSession calls activate/deactivate to keep the gateway's stored
|
|
28
|
-
* context in sync with its own — the same pattern used for ForwardingController.
|
|
29
|
-
*/
|
|
30
|
-
export interface PromptingGatewayLifecycle {
|
|
31
|
-
activate(ctx: ExtensionContext): void;
|
|
32
|
-
deactivate(): void;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Context-owning implementation of the GatePrompter role.
|
|
37
|
-
*
|
|
38
|
-
* Owns the stored ExtensionContext and the "can we prompt?" policy
|
|
39
|
-
* (UI / subagent), replacing the four twin methods that previously
|
|
40
|
-
* lived on PermissionSession.
|
|
41
|
-
*
|
|
42
|
-
* Lifecycle: PermissionSession drives activate/deactivate so the stored
|
|
43
|
-
* context mirrors the session context without independent call-site changes.
|
|
44
|
-
*/
|
|
45
|
-
export class PromptingGateway
|
|
46
|
-
implements GatePrompter, PromptingGatewayLifecycle
|
|
47
|
-
{
|
|
48
|
-
private context: ExtensionContext | null = null;
|
|
49
|
-
|
|
50
|
-
constructor(private readonly deps: PromptingGatewayDeps) {}
|
|
51
|
-
|
|
52
|
-
/** Store the current extension context. */
|
|
53
|
-
activate(ctx: ExtensionContext): void {
|
|
54
|
-
this.context = ctx;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Clear the stored context. */
|
|
58
|
-
deactivate(): void {
|
|
59
|
-
this.context = null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Whether an interactive permission prompt can be shown.
|
|
64
|
-
*
|
|
65
|
-
* Returns false when no context is active. Otherwise true when the
|
|
66
|
-
* context has UI or is a forwarding subagent — the two Authorizer-
|
|
67
|
-
* selection predicates the Phase 9 spine will consume. Yolo-mode never
|
|
68
|
-
* reaches this policy: it is resolved upstream at the composition stage.
|
|
69
|
-
*/
|
|
70
|
-
canConfirm(): boolean {
|
|
71
|
-
if (this.context === null) return false;
|
|
72
|
-
return this.context.hasUI || this.deps.detection.isSubagent(this.context);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Prompt the user for a permission decision using the stored context.
|
|
77
|
-
*
|
|
78
|
-
* Rejects if no context is active — canConfirm() guards this in normal use.
|
|
79
|
-
* Implements {@link GatePrompter}.
|
|
80
|
-
*/
|
|
81
|
-
prompt(details: PromptPermissionDetails): Promise<PermissionPromptDecision> {
|
|
82
|
-
if (this.context === null) {
|
|
83
|
-
return Promise.reject(
|
|
84
|
-
new Error("prompt called before the session was activated"),
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
return this.deps.prompter.prompt(this.context, details);
|
|
88
|
-
}
|
|
89
|
-
}
|