@gotgenes/pi-permission-system 18.2.0 → 19.0.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 +25 -0
- package/README.md +1 -0
- package/config/config.example.json +1 -1
- package/docs/configuration.md +8 -2
- package/docs/migration/strict-config-validation.md +38 -0
- package/docs/opencode-compatibility.md +1 -1
- package/package.json +4 -2
- package/schemas/permissions.schema.json +57 -45
- package/src/{forwarded-permissions/permission-forwarder.ts → authority/approval-escalator.ts} +35 -301
- package/src/authority/forwarded-request-server.ts +285 -0
- package/src/authority/forwarder-context.ts +32 -0
- package/src/{subagent-context.ts → authority/subagent-context.ts} +2 -2
- package/src/authority/subagent-detection.ts +65 -0
- package/src/config-loader.ts +48 -79
- package/src/config-schema.ts +204 -0
- package/src/forwarding-manager.ts +4 -16
- package/src/index.ts +31 -20
- package/src/permission-prompter.ts +1 -1
- package/src/policy-loader.ts +8 -4
- package/src/prompting-gateway.ts +5 -19
- package/src/service-lifecycle.ts +3 -5
- package/src/session-logger.ts +1 -1
- package/src/types.ts +16 -26
- package/src/value-guards.ts +0 -17
- /package/src/{forwarded-permissions/io.ts → authority/forwarding-io.ts} +0 -0
package/src/{forwarded-permissions/permission-forwarder.ts → authority/approval-escalator.ts}
RENAMED
|
@@ -3,86 +3,53 @@ import { join } from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
getActiveAgentName,
|
|
5
5
|
getActiveAgentNameFromSystemPrompt,
|
|
6
|
-
type SessionEntryView,
|
|
7
6
|
} from "#src/active-agent";
|
|
8
|
-
import
|
|
9
|
-
|
|
7
|
+
import {
|
|
8
|
+
type ForwarderContext,
|
|
9
|
+
getSessionId,
|
|
10
|
+
} from "#src/authority/forwarder-context";
|
|
11
|
+
import {
|
|
12
|
+
cleanupPermissionForwardingLocationIfEmpty,
|
|
13
|
+
ensurePermissionForwardingLocation,
|
|
14
|
+
logPermissionForwardingError,
|
|
15
|
+
logPermissionForwardingWarning,
|
|
16
|
+
readForwardedPermissionResponse,
|
|
17
|
+
safeDeleteFile,
|
|
18
|
+
sleep,
|
|
19
|
+
writeJsonFileAtomic,
|
|
20
|
+
} from "#src/authority/forwarding-io";
|
|
21
|
+
import type { SubagentDetector } from "#src/authority/subagent-detection";
|
|
10
22
|
import type {
|
|
11
23
|
PermissionDecisionUi,
|
|
12
24
|
PermissionPromptDecision,
|
|
13
25
|
RequestPermissionOptions,
|
|
14
26
|
} from "#src/permission-dialog";
|
|
15
|
-
import {
|
|
16
|
-
emitUiPromptEvent,
|
|
17
|
-
type PermissionEventBus,
|
|
18
|
-
} from "#src/permission-events";
|
|
19
27
|
import {
|
|
20
28
|
type ForwardedPermissionRequest,
|
|
21
|
-
type ForwardedPermissionResponse,
|
|
22
29
|
type ForwardedPromptDisplay,
|
|
23
|
-
isForwardedPermissionRequestForSession,
|
|
24
30
|
PERMISSION_FORWARDING_POLL_INTERVAL_MS,
|
|
25
31
|
PERMISSION_FORWARDING_TIMEOUT_MS,
|
|
26
32
|
type PermissionForwardingLocation,
|
|
27
33
|
resolvePermissionForwardingTargetSessionId,
|
|
28
34
|
SUBAGENT_PARENT_SESSION_ENV_CANDIDATES,
|
|
29
35
|
} from "#src/permission-forwarding";
|
|
30
|
-
import { buildForwardedUiPrompt } from "#src/permission-ui-prompt";
|
|
31
36
|
import type { DebugReviewLogger } from "#src/session-logger";
|
|
32
|
-
import { isSubagentExecutionContext } from "#src/subagent-context";
|
|
33
37
|
import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
34
38
|
import { toRecord } from "#src/value-guards";
|
|
35
39
|
|
|
36
|
-
import {
|
|
37
|
-
cleanupPermissionForwardingLocationIfEmpty,
|
|
38
|
-
ensureDirectoryExists,
|
|
39
|
-
ensurePermissionForwardingLocation,
|
|
40
|
-
getExistingPermissionForwardingLocation,
|
|
41
|
-
listRequestFiles,
|
|
42
|
-
logPermissionForwardingError,
|
|
43
|
-
logPermissionForwardingWarning,
|
|
44
|
-
readForwardedPermissionRequest,
|
|
45
|
-
readForwardedPermissionResponse,
|
|
46
|
-
safeDeleteFile,
|
|
47
|
-
sleep,
|
|
48
|
-
writeJsonFileAtomic,
|
|
49
|
-
} from "./io";
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Narrow context the forwarder reads: the UI gate (`hasUI`), the dialog UI
|
|
53
|
-
* surface, and the three session-manager readers it uses directly or via
|
|
54
|
-
* {@link isSubagentExecutionContext} / {@link getActiveAgentName}.
|
|
55
|
-
*
|
|
56
|
-
* `getSystemPrompt` is read reflectively (see `getContextSystemPrompt`), so it
|
|
57
|
-
* is intentionally not a typed member. A full `ExtensionContext` satisfies this
|
|
58
|
-
* structurally, so production callers pass `ctx` unchanged.
|
|
59
|
-
*/
|
|
60
|
-
export interface ForwarderContext {
|
|
61
|
-
hasUI: boolean;
|
|
62
|
-
ui: PermissionDecisionUi;
|
|
63
|
-
sessionManager: {
|
|
64
|
-
getSessionId(): string;
|
|
65
|
-
getSessionDir(): string;
|
|
66
|
-
getEntries(): readonly SessionEntryView[];
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
40
|
/**
|
|
71
|
-
* Constructor config for `
|
|
41
|
+
* Constructor config for `ApprovalEscalator`.
|
|
72
42
|
*
|
|
73
43
|
* Replaces the `PermissionForwardingDeps` interface that was previously
|
|
74
|
-
* threaded into free functions in `polling.ts`. The
|
|
44
|
+
* threaded into free functions in `polling.ts`. The escalator consumes it
|
|
75
45
|
* once at construction and stores each member as a private readonly field.
|
|
76
46
|
*/
|
|
77
|
-
export interface
|
|
47
|
+
export interface ApprovalEscalatorDeps {
|
|
78
48
|
forwardingDir: string;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
/** In-process subagent session registry for detection and forwarding target resolution. */
|
|
49
|
+
/** Single owner of subagent detection; gates the forward-vs-deny decision. */
|
|
50
|
+
detection: SubagentDetector;
|
|
51
|
+
/** In-process subagent session registry for forwarding target resolution. */
|
|
83
52
|
registry?: SubagentSessionRegistry;
|
|
84
|
-
/** Event bus used for UI prompt broadcasts. */
|
|
85
|
-
events?: PermissionEventBus;
|
|
86
53
|
logger: DebugReviewLogger;
|
|
87
54
|
requestPermissionDecisionFromUi: (
|
|
88
55
|
ui: PermissionDecisionUi,
|
|
@@ -90,23 +57,10 @@ export interface PermissionForwarderDeps {
|
|
|
90
57
|
message: string,
|
|
91
58
|
options?: RequestPermissionOptions,
|
|
92
59
|
) => Promise<PermissionPromptDecision>;
|
|
93
|
-
/** Read current config for the retained forwarded-inbox yolo auto-approve check. */
|
|
94
|
-
config: ConfigReader;
|
|
95
60
|
}
|
|
96
61
|
|
|
97
62
|
// ── Module-private helpers ────────────────────────────────────────────────
|
|
98
63
|
|
|
99
|
-
function getSessionId(ctx: ForwarderContext): string {
|
|
100
|
-
try {
|
|
101
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
102
|
-
if (typeof sessionId === "string" && sessionId.trim()) {
|
|
103
|
-
return sessionId.trim();
|
|
104
|
-
}
|
|
105
|
-
} catch {}
|
|
106
|
-
|
|
107
|
-
return "unknown";
|
|
108
|
-
}
|
|
109
|
-
|
|
110
64
|
function getContextSystemPrompt(ctx: ForwarderContext): string | undefined {
|
|
111
65
|
const getSystemPrompt = toRecord(ctx).getSystemPrompt;
|
|
112
66
|
if (typeof getSystemPrompt !== "function") {
|
|
@@ -128,27 +82,14 @@ function getContextSystemPrompt(ctx: ForwarderContext): string | undefined {
|
|
|
128
82
|
}
|
|
129
83
|
}
|
|
130
84
|
|
|
131
|
-
function formatForwardedPermissionPrompt(
|
|
132
|
-
request: ForwardedPermissionRequest,
|
|
133
|
-
): string {
|
|
134
|
-
const agentName = request.requesterAgentName || "unknown";
|
|
135
|
-
const sessionId = request.requesterSessionId || "unknown";
|
|
136
|
-
return [
|
|
137
|
-
`Subagent '${agentName}' requested permission.`,
|
|
138
|
-
`Session ID: ${sessionId}`,
|
|
139
|
-
"",
|
|
140
|
-
request.message,
|
|
141
|
-
].join("\n");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
85
|
// ── Public seam interfaces ────────────────────────────────────────────────
|
|
145
86
|
|
|
146
87
|
/**
|
|
147
|
-
* Narrow seam describing what `PermissionPrompter` needs from the
|
|
88
|
+
* Narrow seam describing what `PermissionPrompter` needs from the escalator:
|
|
148
89
|
* a single method that resolves a permission decision for the current context
|
|
149
90
|
* (prompt directly when the session has UI, otherwise forward to the parent).
|
|
150
91
|
*
|
|
151
|
-
* Depending on the interface (not the concrete `
|
|
92
|
+
* Depending on the interface (not the concrete `ApprovalEscalator`) keeps
|
|
152
93
|
* the prompter's unit tests free of casts — they inject a plain
|
|
153
94
|
* `{ requestApproval: vi.fn() }` mock.
|
|
154
95
|
*/
|
|
@@ -161,35 +102,20 @@ export interface ApprovalRequester {
|
|
|
161
102
|
): Promise<PermissionPromptDecision>;
|
|
162
103
|
}
|
|
163
104
|
|
|
164
|
-
|
|
165
|
-
* Narrow seam describing what `ForwardingManager` needs from the forwarder:
|
|
166
|
-
* a single method that drains this session's forwarded-permission inbox.
|
|
167
|
-
*
|
|
168
|
-
* Depending on the interface (not the concrete `PermissionForwarder`) keeps
|
|
169
|
-
* the manager's unit tests free of casts — they inject a plain
|
|
170
|
-
* `{ processInbox: vi.fn() }` mock.
|
|
171
|
-
*/
|
|
172
|
-
export interface InboxProcessor {
|
|
173
|
-
processInbox(ctx: ForwarderContext): Promise<void>;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ── PermissionForwarder ───────────────────────────────────────────────────
|
|
105
|
+
// ── ApprovalEscalator ────────────────────────────────────────────────
|
|
177
106
|
|
|
178
107
|
/**
|
|
179
|
-
* Owner of the forwarded-permission behavior.
|
|
108
|
+
* Owner of the escalation-up role of the forwarded-permission behavior.
|
|
180
109
|
*
|
|
181
|
-
* Holds all forwarding state as private readonly fields and provides
|
|
182
|
-
* public
|
|
183
|
-
* the
|
|
184
|
-
*
|
|
185
|
-
* responses, and processing the parent-session inbox.
|
|
110
|
+
* Holds all forwarding state as private readonly fields and provides the
|
|
111
|
+
* public `requestApproval` method: deciding whether to prompt directly or
|
|
112
|
+
* forward to the parent, building and persisting request files, and polling
|
|
113
|
+
* for responses.
|
|
186
114
|
*/
|
|
187
|
-
export class
|
|
115
|
+
export class ApprovalEscalator implements ApprovalRequester {
|
|
188
116
|
private readonly forwardingDir: string;
|
|
189
|
-
private readonly
|
|
190
|
-
private readonly platform: NodeJS.Platform;
|
|
117
|
+
private readonly detection: SubagentDetector;
|
|
191
118
|
private readonly registry: SubagentSessionRegistry | undefined;
|
|
192
|
-
private readonly events: PermissionEventBus | undefined;
|
|
193
119
|
private readonly logger: DebugReviewLogger;
|
|
194
120
|
private readonly requestPermissionDecisionFromUi: (
|
|
195
121
|
ui: PermissionDecisionUi,
|
|
@@ -197,17 +123,13 @@ export class PermissionForwarder implements ApprovalRequester, InboxProcessor {
|
|
|
197
123
|
message: string,
|
|
198
124
|
options?: RequestPermissionOptions,
|
|
199
125
|
) => Promise<PermissionPromptDecision>;
|
|
200
|
-
private readonly config: ConfigReader;
|
|
201
126
|
|
|
202
|
-
constructor(deps:
|
|
127
|
+
constructor(deps: ApprovalEscalatorDeps) {
|
|
203
128
|
this.forwardingDir = deps.forwardingDir;
|
|
204
|
-
this.
|
|
205
|
-
this.platform = deps.platform;
|
|
129
|
+
this.detection = deps.detection;
|
|
206
130
|
this.registry = deps.registry;
|
|
207
|
-
this.events = deps.events;
|
|
208
131
|
this.logger = deps.logger;
|
|
209
132
|
this.requestPermissionDecisionFromUi = deps.requestPermissionDecisionFromUi;
|
|
210
|
-
this.config = deps.config;
|
|
211
133
|
}
|
|
212
134
|
|
|
213
135
|
// ── Public seam methods ────────────────────────────────────────────────
|
|
@@ -231,78 +153,13 @@ export class PermissionForwarder implements ApprovalRequester, InboxProcessor {
|
|
|
231
153
|
);
|
|
232
154
|
}
|
|
233
155
|
|
|
234
|
-
if (
|
|
235
|
-
!isSubagentExecutionContext(
|
|
236
|
-
ctx,
|
|
237
|
-
this.subagentSessionsDir,
|
|
238
|
-
this.platform,
|
|
239
|
-
this.registry,
|
|
240
|
-
)
|
|
241
|
-
) {
|
|
156
|
+
if (!this.detection.isSubagent(ctx)) {
|
|
242
157
|
return Promise.resolve({ approved: false, state: "denied" });
|
|
243
158
|
}
|
|
244
159
|
|
|
245
160
|
return this.waitForForwardedApproval(ctx, message, forwarded);
|
|
246
161
|
}
|
|
247
162
|
|
|
248
|
-
/** Drain and respond to this session's forwarded-permission inbox. */
|
|
249
|
-
async processInbox(ctx: ForwarderContext): Promise<void> {
|
|
250
|
-
if (!ctx.hasUI) {
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const currentSessionId = getSessionId(ctx);
|
|
255
|
-
const location = getExistingPermissionForwardingLocation(
|
|
256
|
-
this.forwardingDir,
|
|
257
|
-
currentSessionId,
|
|
258
|
-
);
|
|
259
|
-
if (!location) {
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const requestFiles = listRequestFiles(this.logger, location.requestsDir);
|
|
264
|
-
if (requestFiles.length === 0) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Defensively recreate responses/ before writing any response — a
|
|
269
|
-
// concurrent cleanup pass may have removed it between the requestsDir
|
|
270
|
-
// existence check above and the write inside processSingleForwardedRequest
|
|
271
|
-
// (the ENOENT write loop reported in issue #398).
|
|
272
|
-
if (
|
|
273
|
-
!ensureDirectoryExists(
|
|
274
|
-
this.logger,
|
|
275
|
-
location.responsesDir,
|
|
276
|
-
"permission forwarding responses",
|
|
277
|
-
)
|
|
278
|
-
) {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
for (const fileName of requestFiles) {
|
|
283
|
-
const requestPath = join(location.requestsDir, fileName);
|
|
284
|
-
const request = readForwardedPermissionRequest(this.logger, requestPath);
|
|
285
|
-
if (!request) {
|
|
286
|
-
safeDeleteFile(
|
|
287
|
-
this.logger,
|
|
288
|
-
requestPath,
|
|
289
|
-
`${location.label} forwarded permission request`,
|
|
290
|
-
);
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
await this.processSingleForwardedRequest(
|
|
295
|
-
ctx,
|
|
296
|
-
request,
|
|
297
|
-
location,
|
|
298
|
-
requestPath,
|
|
299
|
-
currentSessionId,
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
cleanupPermissionForwardingLocationIfEmpty(this.logger, location);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
163
|
// ── Private methods ────────────────────────────────────────────────────
|
|
307
164
|
|
|
308
165
|
private async waitForForwardedApproval(
|
|
@@ -313,12 +170,7 @@ export class PermissionForwarder implements ApprovalRequester, InboxProcessor {
|
|
|
313
170
|
const requesterSessionId = getSessionId(ctx);
|
|
314
171
|
const targetSessionId = resolvePermissionForwardingTargetSessionId({
|
|
315
172
|
hasUI: ctx.hasUI,
|
|
316
|
-
isSubagent:
|
|
317
|
-
ctx,
|
|
318
|
-
this.subagentSessionsDir,
|
|
319
|
-
this.platform,
|
|
320
|
-
this.registry,
|
|
321
|
-
),
|
|
173
|
+
isSubagent: this.detection.isSubagent(ctx),
|
|
322
174
|
currentSessionId: requesterSessionId,
|
|
323
175
|
env: process.env,
|
|
324
176
|
sessionId: requesterSessionId,
|
|
@@ -472,122 +324,4 @@ export class PermissionForwarder implements ApprovalRequester, InboxProcessor {
|
|
|
472
324
|
cleanupPermissionForwardingLocationIfEmpty(this.logger, location);
|
|
473
325
|
return { approved: false, state: "denied" };
|
|
474
326
|
}
|
|
475
|
-
|
|
476
|
-
private async processSingleForwardedRequest(
|
|
477
|
-
ctx: ForwarderContext,
|
|
478
|
-
request: ForwardedPermissionRequest,
|
|
479
|
-
location: PermissionForwardingLocation,
|
|
480
|
-
requestPath: string,
|
|
481
|
-
currentSessionId: string,
|
|
482
|
-
): Promise<void> {
|
|
483
|
-
if (!isForwardedPermissionRequestForSession(request, currentSessionId)) {
|
|
484
|
-
logPermissionForwardingWarning(
|
|
485
|
-
this.logger,
|
|
486
|
-
`Ignoring forwarded permission request '${request.id}' because it targets session '${request.targetSessionId}' instead of '${currentSessionId}'`,
|
|
487
|
-
);
|
|
488
|
-
safeDeleteFile(
|
|
489
|
-
this.logger,
|
|
490
|
-
requestPath,
|
|
491
|
-
`${location.label} forwarded permission request`,
|
|
492
|
-
);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const forwardedPermissionLogDetails = {
|
|
497
|
-
requestId: request.id,
|
|
498
|
-
source: location.label,
|
|
499
|
-
requesterAgentName: request.requesterAgentName,
|
|
500
|
-
requesterSessionId: request.requesterSessionId,
|
|
501
|
-
targetSessionId: request.targetSessionId,
|
|
502
|
-
requestPath,
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
let decision: PermissionPromptDecision = {
|
|
506
|
-
approved: false,
|
|
507
|
-
state: "denied",
|
|
508
|
-
};
|
|
509
|
-
// Last yolo check outside the composed ruleset: dissolves when
|
|
510
|
-
// processInbox is refactored onto evaluate() + Authorizer selection in
|
|
511
|
-
// the Phase 9 spine work.
|
|
512
|
-
if (isYoloModeEnabled(this.config.current())) {
|
|
513
|
-
this.logger.review(
|
|
514
|
-
"forwarded_permission.auto_approved",
|
|
515
|
-
forwardedPermissionLogDetails,
|
|
516
|
-
);
|
|
517
|
-
decision = { approved: true, state: "approved" };
|
|
518
|
-
} else {
|
|
519
|
-
this.logger.review(
|
|
520
|
-
"forwarded_permission.prompted",
|
|
521
|
-
forwardedPermissionLogDetails,
|
|
522
|
-
);
|
|
523
|
-
try {
|
|
524
|
-
const forwardedMessage = formatForwardedPermissionPrompt(request);
|
|
525
|
-
if (this.events) {
|
|
526
|
-
emitUiPromptEvent(
|
|
527
|
-
this.events,
|
|
528
|
-
buildForwardedUiPrompt({
|
|
529
|
-
requestId: request.id,
|
|
530
|
-
message: forwardedMessage,
|
|
531
|
-
requesterAgentName: request.requesterAgentName || null,
|
|
532
|
-
requesterSessionId: request.requesterSessionId || null,
|
|
533
|
-
source: request.source ?? null,
|
|
534
|
-
surface: request.surface ?? null,
|
|
535
|
-
value: request.value ?? null,
|
|
536
|
-
}),
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
decision = await this.requestPermissionDecisionFromUi(
|
|
540
|
-
ctx.ui,
|
|
541
|
-
"Permission Required (Subagent)",
|
|
542
|
-
forwardedMessage,
|
|
543
|
-
);
|
|
544
|
-
} catch (error) {
|
|
545
|
-
logPermissionForwardingError(
|
|
546
|
-
this.logger,
|
|
547
|
-
"Failed to show forwarded permission confirmation dialog",
|
|
548
|
-
error,
|
|
549
|
-
);
|
|
550
|
-
decision = { approved: false, state: "denied" };
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const responsePath = join(location.responsesDir, `${request.id}.json`);
|
|
555
|
-
this.logger.review(
|
|
556
|
-
decision.approved
|
|
557
|
-
? "forwarded_permission.approved"
|
|
558
|
-
: "forwarded_permission.denied",
|
|
559
|
-
{
|
|
560
|
-
requestId: request.id,
|
|
561
|
-
source: location.label,
|
|
562
|
-
requesterAgentName: request.requesterAgentName,
|
|
563
|
-
requesterSessionId: request.requesterSessionId,
|
|
564
|
-
targetSessionId: request.targetSessionId,
|
|
565
|
-
responsePath,
|
|
566
|
-
resolution: decision.state,
|
|
567
|
-
denialReason: decision.denialReason ?? null,
|
|
568
|
-
},
|
|
569
|
-
);
|
|
570
|
-
try {
|
|
571
|
-
writeJsonFileAtomic(this.logger, responsePath, {
|
|
572
|
-
approved: decision.approved,
|
|
573
|
-
state: decision.state,
|
|
574
|
-
denialReason: decision.denialReason,
|
|
575
|
-
responderSessionId: currentSessionId,
|
|
576
|
-
respondedAt: Date.now(),
|
|
577
|
-
} satisfies ForwardedPermissionResponse);
|
|
578
|
-
} catch (error) {
|
|
579
|
-
logPermissionForwardingError(
|
|
580
|
-
this.logger,
|
|
581
|
-
`Failed to write ${location.label} forwarded permission response '${responsePath}'`,
|
|
582
|
-
error,
|
|
583
|
-
);
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
safeDeleteFile(
|
|
588
|
-
this.logger,
|
|
589
|
-
requestPath,
|
|
590
|
-
`${location.label} forwarded permission request`,
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
327
|
}
|