@gotgenes/pi-permission-system 20.2.0 → 20.3.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 +13 -0
- package/docs/subagent-integration.md +2 -0
- package/package.json +1 -1
- package/src/authority/approval-escalator.ts +15 -5
- package/src/authority/forwarded-request-server.ts +49 -1
- package/src/authority/forwarding-io.ts +27 -0
- package/src/authority/local-user-authorizer.ts +26 -1
- package/src/authority/permission-prompter.ts +8 -0
- package/src/handlers/gates/runner.ts +3 -0
- package/src/index.ts +4 -0
- package/src/pattern-suggest.ts +29 -0
- package/src/permission-dialog.ts +26 -0
- package/src/permission-forwarding.ts +21 -0
- package/src/session-approval.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [20.3.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.2.0...pi-permission-system-v20.3.0) (2026-07-09)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** forward the child's session-approval suggestion ([fa04d8c](https://github.com/gotgenes/pi-packages/commit/fa04d8ca3485ca3f6e29bbe8d53146f5eeb6672c))
|
|
14
|
+
* **pi-permission-system:** offer whole-session scope on forwarded approvals ([bd2be07](https://github.com/gotgenes/pi-packages/commit/bd2be0799923ed809cd0939047486fdc1b2a477f))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
* **pi-permission-system:** record forwarded grant-scope selection (Phase 9 Step 4) ([a3d1fca](https://github.com/gotgenes/pi-packages/commit/a3d1fca183dc5e4d6fb15ec6ec78199f5d092bc3))
|
|
20
|
+
|
|
8
21
|
## [20.2.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v20.1.0...pi-permission-system-v20.2.0) (2026-07-09)
|
|
9
22
|
|
|
10
23
|
|
|
@@ -17,6 +17,7 @@ The integration enables:
|
|
|
17
17
|
2. **Per-agent policy enforcement** - the permission system's `before_agent_start` handler resolves the agent name from the `<active_agent>` system-prompt tag and applies per-agent `permission:` frontmatter overrides.
|
|
18
18
|
3. **`ask`-state forwarding** - when a child triggers an `ask` permission, the request forwards to the parent session's UI through the existing polling mechanism.
|
|
19
19
|
The parent approves or denies, and the child resumes.
|
|
20
|
+
When the parent approves "for this session," it chooses a scope: **this subagent only** (the least-privilege default) records the grant on the requesting child, while **the whole session** records it on the serving parent so the parent and all its subagents resolve it without re-prompting.
|
|
20
21
|
|
|
21
22
|
No configuration is required - the integration is automatic when both extensions are installed.
|
|
22
23
|
When `@gotgenes/pi-permission-system` is not installed, `@gotgenes/pi-subagents` emits its lifecycle events with no subscriber - a harmless no-op.
|
|
@@ -25,6 +26,7 @@ When `@gotgenes/pi-permission-system` is not installed, `@gotgenes/pi-subagents`
|
|
|
25
26
|
|
|
26
27
|
When a delegated or routed subagent runs without direct UI access, `ask` permissions can still be enforced by forwarding the confirmation request through Pi session directories.
|
|
27
28
|
The main interactive session polls for forwarded requests, shows the confirmation prompt, writes the response, and the subagent resumes once that decision is available.
|
|
29
|
+
A parent `allow`/`deny` rule governs a child's escalation directly (the serving node resolves it as recorded authority before prompting), and a "whole session" grant recorded on the parent auto-approves later forwards of the same pattern.
|
|
28
30
|
|
|
29
31
|
This keeps `ask` policies usable even when the original permission check happens inside a non-UI execution context.
|
|
30
32
|
|
package/package.json
CHANGED
|
@@ -22,6 +22,7 @@ import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
|
22
22
|
import {
|
|
23
23
|
type ForwardedPermissionRequest,
|
|
24
24
|
type ForwardedPromptDisplay,
|
|
25
|
+
type ForwardedSessionApproval,
|
|
25
26
|
PERMISSION_FORWARDING_POLL_INTERVAL_MS,
|
|
26
27
|
PERMISSION_FORWARDING_TIMEOUT_MS,
|
|
27
28
|
type PermissionForwardingLocation,
|
|
@@ -98,11 +99,16 @@ export class ParentAuthorizer implements Authorizer {
|
|
|
98
99
|
details: PromptPermissionDetails,
|
|
99
100
|
): Promise<PermissionPromptDecision> {
|
|
100
101
|
const uiPrompt = buildUiPrompt(details);
|
|
101
|
-
return this.waitForForwardedApproval(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
return this.waitForForwardedApproval(
|
|
103
|
+
this.ctx,
|
|
104
|
+
details.message,
|
|
105
|
+
{
|
|
106
|
+
source: uiPrompt.source,
|
|
107
|
+
surface: uiPrompt.surface,
|
|
108
|
+
value: uiPrompt.value,
|
|
109
|
+
},
|
|
110
|
+
details.sessionApproval,
|
|
111
|
+
);
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
// ── Private methods ────────────────────────────────────────────────────
|
|
@@ -111,6 +117,7 @@ export class ParentAuthorizer implements Authorizer {
|
|
|
111
117
|
ctx: ForwarderContext,
|
|
112
118
|
message: string,
|
|
113
119
|
forwarded?: ForwardedPromptDisplay,
|
|
120
|
+
sessionApproval?: ForwardedSessionApproval,
|
|
114
121
|
): Promise<PermissionPromptDecision> {
|
|
115
122
|
const requesterSessionId = getSessionId(ctx);
|
|
116
123
|
const targetSessionId = resolvePermissionForwardingTargetSessionId({
|
|
@@ -156,6 +163,7 @@ export class ParentAuthorizer implements Authorizer {
|
|
|
156
163
|
requesterSessionId,
|
|
157
164
|
targetSessionId,
|
|
158
165
|
forwarded,
|
|
166
|
+
sessionApproval,
|
|
159
167
|
);
|
|
160
168
|
const requestPath = join(location.requestsDir, `${request.id}.json`);
|
|
161
169
|
const responsePath = join(location.responsesDir, `${request.id}.json`);
|
|
@@ -194,6 +202,7 @@ export class ParentAuthorizer implements Authorizer {
|
|
|
194
202
|
requesterSessionId: string,
|
|
195
203
|
targetSessionId: string,
|
|
196
204
|
forwarded?: ForwardedPromptDisplay,
|
|
205
|
+
sessionApproval?: ForwardedSessionApproval,
|
|
197
206
|
): ForwardedPermissionRequest {
|
|
198
207
|
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
|
|
199
208
|
const requesterAgentName =
|
|
@@ -214,6 +223,7 @@ export class ParentAuthorizer implements Authorizer {
|
|
|
214
223
|
value: forwarded.value,
|
|
215
224
|
}
|
|
216
225
|
: {}),
|
|
226
|
+
...(sessionApproval ? { sessionApproval } : {}),
|
|
217
227
|
};
|
|
218
228
|
}
|
|
219
229
|
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
isForwardedPermissionRequestForSession,
|
|
11
11
|
type PermissionForwardingLocation,
|
|
12
12
|
} from "#src/permission-forwarding";
|
|
13
|
+
import { SessionApproval } from "#src/session-approval";
|
|
14
|
+
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
13
15
|
import type { DebugReviewLogger } from "#src/session-logger";
|
|
14
16
|
import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
15
17
|
import type { PermissionCheckResult } from "#src/types";
|
|
@@ -61,6 +63,11 @@ export interface ForwardedRequestServerDeps {
|
|
|
61
63
|
policy: ServingPolicy;
|
|
62
64
|
/** Escalation seam to the serving session's selected `Authorizer` on `ask`. */
|
|
63
65
|
escalator: AskEscalator;
|
|
66
|
+
/**
|
|
67
|
+
* The serving session's `SessionRules`. Records a whole-session grant when a
|
|
68
|
+
* human approves a forwarded request for the entire serving session.
|
|
69
|
+
*/
|
|
70
|
+
recorder: SessionApprovalRecorder;
|
|
64
71
|
/** In-process subagent registry, read only by the one-hop canary. */
|
|
65
72
|
registry?: SubagentSessionRegistry;
|
|
66
73
|
}
|
|
@@ -115,6 +122,11 @@ function buildForwardedAskDetails(
|
|
|
115
122
|
requesterAgentName: request.requesterAgentName || null,
|
|
116
123
|
requesterSessionId: request.requesterSessionId || null,
|
|
117
124
|
},
|
|
125
|
+
// Carries the child's suggestion so LocalUserAuthorizer can offer the
|
|
126
|
+
// whole-session grant scope; absent for a legacy/version-skew request.
|
|
127
|
+
...(request.sessionApproval
|
|
128
|
+
? { sessionApproval: request.sessionApproval }
|
|
129
|
+
: {}),
|
|
118
130
|
};
|
|
119
131
|
}
|
|
120
132
|
|
|
@@ -132,6 +144,7 @@ export class ForwardedRequestServer implements InboxProcessor {
|
|
|
132
144
|
private readonly logger: DebugReviewLogger;
|
|
133
145
|
private readonly policy: ServingPolicy;
|
|
134
146
|
private readonly escalator: AskEscalator;
|
|
147
|
+
private readonly recorder: SessionApprovalRecorder;
|
|
135
148
|
private readonly registry: SubagentSessionRegistry | undefined;
|
|
136
149
|
|
|
137
150
|
constructor(deps: ForwardedRequestServerDeps) {
|
|
@@ -139,6 +152,7 @@ export class ForwardedRequestServer implements InboxProcessor {
|
|
|
139
152
|
this.logger = deps.logger;
|
|
140
153
|
this.policy = deps.policy;
|
|
141
154
|
this.escalator = deps.escalator;
|
|
155
|
+
this.recorder = deps.recorder;
|
|
142
156
|
this.registry = deps.registry;
|
|
143
157
|
}
|
|
144
158
|
|
|
@@ -237,10 +251,44 @@ export class ForwardedRequestServer implements InboxProcessor {
|
|
|
237
251
|
location,
|
|
238
252
|
requestPath,
|
|
239
253
|
currentSessionId,
|
|
240
|
-
decision,
|
|
254
|
+
this.applyGrantScope(request, decision, forwardedPermissionLogDetails),
|
|
241
255
|
);
|
|
242
256
|
}
|
|
243
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Apply the human's grant-scope choice on a forwarded approval.
|
|
260
|
+
*
|
|
261
|
+
* A whole-session grant (`approved_for_serving_session`) records the child's
|
|
262
|
+
* suggested pattern into this serving node's `SessionRules` — the single
|
|
263
|
+
* source of truth for the scope — and is then translated to a plain
|
|
264
|
+
* `approved` so the child records nothing (its next identical action
|
|
265
|
+
* re-forwards and resolves as recorded authority). Every other decision
|
|
266
|
+
* passes through unchanged (`approved_for_session` → the child records).
|
|
267
|
+
*/
|
|
268
|
+
private applyGrantScope(
|
|
269
|
+
request: ForwardedPermissionRequest,
|
|
270
|
+
decision: PermissionPromptDecision,
|
|
271
|
+
logDetails: Record<string, unknown>,
|
|
272
|
+
): PermissionPromptDecision {
|
|
273
|
+
if (decision.state !== "approved_for_serving_session") {
|
|
274
|
+
return decision;
|
|
275
|
+
}
|
|
276
|
+
if (request.sessionApproval) {
|
|
277
|
+
this.recorder.recordSessionApproval(
|
|
278
|
+
SessionApproval.multiple(
|
|
279
|
+
request.sessionApproval.surface,
|
|
280
|
+
request.sessionApproval.patterns,
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
this.logger.review("forwarded_permission.session_recorded", {
|
|
284
|
+
...logDetails,
|
|
285
|
+
surface: request.sessionApproval.surface,
|
|
286
|
+
patterns: request.sessionApproval.patterns,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return { approved: true, state: "approved" };
|
|
290
|
+
}
|
|
291
|
+
|
|
244
292
|
/**
|
|
245
293
|
* Persist the served decision: write the response file the child polls for,
|
|
246
294
|
* log the outcome, and delete the drained request. The symmetric "respond"
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
createPermissionForwardingLocation,
|
|
16
16
|
type ForwardedPermissionRequest,
|
|
17
17
|
type ForwardedPermissionResponse,
|
|
18
|
+
type ForwardedSessionApproval,
|
|
18
19
|
type PermissionForwardingLocation,
|
|
19
20
|
} from "#src/permission-forwarding";
|
|
20
21
|
import type { DebugReviewLogger } from "#src/session-logger";
|
|
@@ -41,6 +42,31 @@ function asNullableDisplayString(value: unknown): string | null | undefined {
|
|
|
41
42
|
return undefined;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Narrow an unknown value to a `ForwardedSessionApproval`, or `undefined`.
|
|
47
|
+
*
|
|
48
|
+
* Tolerant read: the child's session-approval suggestion is optional (absent
|
|
49
|
+
* on an older child) and only accepted when well-formed — a non-empty surface
|
|
50
|
+
* and an all-string patterns array.
|
|
51
|
+
*/
|
|
52
|
+
function asForwardedSessionApproval(
|
|
53
|
+
value: unknown,
|
|
54
|
+
): ForwardedSessionApproval | undefined {
|
|
55
|
+
if (typeof value !== "object" || value === null) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const candidate = value as Partial<ForwardedSessionApproval>;
|
|
59
|
+
if (
|
|
60
|
+
typeof candidate.surface !== "string" ||
|
|
61
|
+
candidate.surface.length === 0 ||
|
|
62
|
+
!Array.isArray(candidate.patterns) ||
|
|
63
|
+
!candidate.patterns.every((pattern) => typeof pattern === "string")
|
|
64
|
+
) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return { surface: candidate.surface, patterns: [...candidate.patterns] };
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
export function formatUnknownErrorMessage(error: unknown): string {
|
|
45
71
|
if (error instanceof Error && error.message) {
|
|
46
72
|
return error.message;
|
|
@@ -323,6 +349,7 @@ export function readForwardedPermissionRequest(
|
|
|
323
349
|
source: asUiPromptSource(parsed.source),
|
|
324
350
|
surface: asNullableDisplayString(parsed.surface),
|
|
325
351
|
value: asNullableDisplayString(parsed.value),
|
|
352
|
+
sessionApproval: asForwardedSessionApproval(parsed.sessionApproval),
|
|
326
353
|
};
|
|
327
354
|
} catch (error) {
|
|
328
355
|
logPermissionForwardingWarning(
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { buildForwardedScopeLabels } from "#src/pattern-suggest";
|
|
1
2
|
import type {
|
|
2
3
|
PermissionDecisionUi,
|
|
3
4
|
PermissionPromptDecision,
|
|
5
|
+
RequestPermissionOptions,
|
|
4
6
|
requestPermissionDecisionFromUi,
|
|
5
7
|
} from "#src/permission-dialog";
|
|
6
8
|
import {
|
|
@@ -45,7 +47,30 @@ export class LocalUserAuthorizer implements Authorizer {
|
|
|
45
47
|
? "Permission Required (Subagent)"
|
|
46
48
|
: "Permission Required",
|
|
47
49
|
details.message,
|
|
48
|
-
details
|
|
50
|
+
buildRequestOptions(details),
|
|
49
51
|
);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A forwarded ask carrying a session-approval suggestion offers the scope
|
|
57
|
+
* choice (subagent vs whole session); any other ask keeps its single
|
|
58
|
+
* "for this session" option (custom label when the gate supplied one).
|
|
59
|
+
*/
|
|
60
|
+
function buildRequestOptions(
|
|
61
|
+
details: PromptPermissionDetails,
|
|
62
|
+
): RequestPermissionOptions | undefined {
|
|
63
|
+
const pattern = details.sessionApproval?.patterns[0];
|
|
64
|
+
if (details.forwarding && details.sessionApproval && pattern) {
|
|
65
|
+
return {
|
|
66
|
+
sessionScope: buildForwardedScopeLabels(
|
|
67
|
+
details.forwarding.requesterAgentName,
|
|
68
|
+
details.sessionApproval.surface,
|
|
69
|
+
pattern,
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return details.sessionLabel
|
|
74
|
+
? { sessionLabel: details.sessionLabel }
|
|
75
|
+
: undefined;
|
|
76
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PermissionPromptDecision } from "#src/permission-dialog";
|
|
2
|
+
import type { ForwardedSessionApproval } from "#src/permission-forwarding";
|
|
2
3
|
import type { ReviewLogger } from "#src/session-logger";
|
|
3
4
|
import type { Authorizer } from "./authorizer";
|
|
4
5
|
|
|
@@ -38,6 +39,13 @@ export interface PromptPermissionDetails {
|
|
|
38
39
|
value?: string | null;
|
|
39
40
|
/** Present iff this ask was forwarded from a subagent; drives the non-degraded broadcast + "(Subagent)" title. */
|
|
40
41
|
forwarding?: ForwardedAskProvenance;
|
|
42
|
+
/**
|
|
43
|
+
* The session-approval suggestion for this ask. On the child's escalation it
|
|
44
|
+
* rides into the forwarded request; on the serving node it lets the dialog
|
|
45
|
+
* offer a whole-session grant scope. Absent when the gate computed no
|
|
46
|
+
* suggestion.
|
|
47
|
+
*/
|
|
48
|
+
sessionApproval?: ForwardedSessionApproval;
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
/**
|
|
@@ -147,6 +147,9 @@ export class GateRunner {
|
|
|
147
147
|
const decision = await this.prompter.escalate({
|
|
148
148
|
requestId: toolCallId,
|
|
149
149
|
...descriptor.promptDetails,
|
|
150
|
+
...(descriptor.sessionApproval
|
|
151
|
+
? { sessionApproval: descriptor.sessionApproval.toForwardedData() }
|
|
152
|
+
: {}),
|
|
150
153
|
});
|
|
151
154
|
autoApproved = decision.autoApproved === true;
|
|
152
155
|
confirmationUnavailable = decision.confirmationUnavailable === true;
|
package/src/index.ts
CHANGED
|
@@ -132,6 +132,10 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
132
132
|
logger,
|
|
133
133
|
policy: servingPolicy,
|
|
134
134
|
escalator: authorizerSelection,
|
|
135
|
+
// Records a whole-session grant into the same SessionRules the resolver and
|
|
136
|
+
// gate runner read, so a serving-scope grant governs the parent and future
|
|
137
|
+
// forwarded resolutions.
|
|
138
|
+
recorder: sessionRules,
|
|
135
139
|
registry: subagentRegistry,
|
|
136
140
|
});
|
|
137
141
|
|
package/src/pattern-suggest.ts
CHANGED
|
@@ -62,6 +62,35 @@ export function suggestMcpPattern(target: string): string {
|
|
|
62
62
|
return "*";
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** Scope labels for the forwarded-approval two-step scope select. */
|
|
66
|
+
export interface ForwardedScopeLabels {
|
|
67
|
+
/** Least-privilege default: record on the requesting subagent only. */
|
|
68
|
+
subagentLabel: string;
|
|
69
|
+
/** Record on the serving node — covers the parent and all subagents. */
|
|
70
|
+
servingSessionLabel: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build the two scope labels shown when a human grants a forwarded request
|
|
75
|
+
* "for this session."
|
|
76
|
+
*
|
|
77
|
+
* The subagent option names the requester (least privilege); the whole-session
|
|
78
|
+
* option restates the surface + pattern being granted session-wide.
|
|
79
|
+
*/
|
|
80
|
+
export function buildForwardedScopeLabels(
|
|
81
|
+
agentName: string | null,
|
|
82
|
+
surface: string,
|
|
83
|
+
pattern: string,
|
|
84
|
+
): ForwardedScopeLabels {
|
|
85
|
+
const subagentLabel = agentName
|
|
86
|
+
? `This subagent ('${agentName}') only`
|
|
87
|
+
: "This subagent only";
|
|
88
|
+
return {
|
|
89
|
+
subagentLabel,
|
|
90
|
+
servingSessionLabel: `The whole session — allow ${surface} "${pattern}" for parent and all subagents`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
65
94
|
/** Surface-aware human-readable labels for the session-approval option. */
|
|
66
95
|
function buildLabel(pattern: string, surface: string): string {
|
|
67
96
|
switch (surface) {
|
package/src/permission-dialog.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type PermissionDecisionState =
|
|
2
2
|
| "approved"
|
|
3
3
|
| "approved_for_session"
|
|
4
|
+
| "approved_for_serving_session"
|
|
4
5
|
| "denied"
|
|
5
6
|
| "denied_with_reason";
|
|
6
7
|
|
|
@@ -67,6 +68,7 @@ export function isPermissionDecisionState(
|
|
|
67
68
|
return (
|
|
68
69
|
value === "approved" ||
|
|
69
70
|
value === "approved_for_session" ||
|
|
71
|
+
value === "approved_for_serving_session" ||
|
|
70
72
|
value === "denied" ||
|
|
71
73
|
value === "denied_with_reason"
|
|
72
74
|
);
|
|
@@ -75,6 +77,15 @@ export function isPermissionDecisionState(
|
|
|
75
77
|
export interface RequestPermissionOptions {
|
|
76
78
|
/** Override the "for this session" option label (e.g. to show the suggested pattern). */
|
|
77
79
|
sessionLabel?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Forwarded asks only: when set, choosing the "for this session" option opens
|
|
82
|
+
* a second select asking whether the grant applies to the requesting subagent
|
|
83
|
+
* only (the least-privilege default) or the whole serving session.
|
|
84
|
+
*/
|
|
85
|
+
sessionScope?: {
|
|
86
|
+
subagentLabel: string;
|
|
87
|
+
servingSessionLabel: string;
|
|
88
|
+
};
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
export async function requestPermissionDecisionFromUi(
|
|
@@ -103,6 +114,21 @@ export async function requestPermissionDecisionFromUi(
|
|
|
103
114
|
}
|
|
104
115
|
|
|
105
116
|
if (selected === sessionOption) {
|
|
117
|
+
if (options?.sessionScope) {
|
|
118
|
+
const scope = await ui.select(`${title}\nApply this session grant to:`, [
|
|
119
|
+
options.sessionScope.subagentLabel,
|
|
120
|
+
options.sessionScope.servingSessionLabel,
|
|
121
|
+
]);
|
|
122
|
+
return {
|
|
123
|
+
approved: true,
|
|
124
|
+
// A cancelled scope select (undefined) falls back to the
|
|
125
|
+
// least-privilege subagent scope.
|
|
126
|
+
state:
|
|
127
|
+
scope === options.sessionScope.servingSessionLabel
|
|
128
|
+
? "approved_for_serving_session"
|
|
129
|
+
: "approved_for_session",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
106
132
|
return {
|
|
107
133
|
approved: true,
|
|
108
134
|
state: "approved_for_session",
|
|
@@ -53,6 +53,20 @@ export interface ForwardedPromptDisplay {
|
|
|
53
53
|
value: string | null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The child's session-approval suggestion, relayed to the serving node so a
|
|
58
|
+
* human who grants "the whole session" records the same pattern the child
|
|
59
|
+
* would have recorded locally.
|
|
60
|
+
*
|
|
61
|
+
* A plain data shape (not the `SessionApproval` value object) so it serializes
|
|
62
|
+
* onto the forwarded request; the serving node rebuilds a `SessionApproval`
|
|
63
|
+
* from it via `SessionApproval.multiple`.
|
|
64
|
+
*/
|
|
65
|
+
export interface ForwardedSessionApproval {
|
|
66
|
+
surface: string;
|
|
67
|
+
patterns: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
export type ForwardedPermissionRequest = {
|
|
57
71
|
id: string;
|
|
58
72
|
createdAt: number;
|
|
@@ -69,6 +83,13 @@ export type ForwardedPermissionRequest = {
|
|
|
69
83
|
source?: PermissionUiPromptSource;
|
|
70
84
|
surface?: string | null;
|
|
71
85
|
value?: string | null;
|
|
86
|
+
/**
|
|
87
|
+
* The child's session-approval suggestion. Present when the child computed a
|
|
88
|
+
* "for this session" pattern for the ask; lets the serving node record a
|
|
89
|
+
* whole-session grant. Optional for version-skew tolerance (an older child
|
|
90
|
+
* omits it, and the serving dialog then offers no scope choice).
|
|
91
|
+
*/
|
|
92
|
+
sessionApproval?: ForwardedSessionApproval;
|
|
72
93
|
};
|
|
73
94
|
|
|
74
95
|
export type ForwardedPermissionResponse = {
|
package/src/session-approval.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ForwardedSessionApproval } from "./permission-forwarding";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Value object for a session-scoped approval: one surface, one-or-more patterns.
|
|
3
5
|
*
|
|
@@ -40,4 +42,13 @@ export class SessionApproval {
|
|
|
40
42
|
if (pattern === undefined) return undefined;
|
|
41
43
|
return { surface: this.surface, pattern };
|
|
42
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Plain data shape for relaying this approval on a forwarded request, so the
|
|
48
|
+
* serving node can record the same pattern(s) as a whole-session grant.
|
|
49
|
+
* Returns a defensive copy of the patterns.
|
|
50
|
+
*/
|
|
51
|
+
toForwardedData(): ForwardedSessionApproval {
|
|
52
|
+
return { surface: this.surface, patterns: [...this.patterns] };
|
|
53
|
+
}
|
|
43
54
|
}
|