pi-permission-system 0.2.2 → 0.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 +19 -0
- package/README.md +1 -1
- package/config.json +2 -1
- package/package.json +5 -4
- package/src/config-modal-test.ts +217 -0
- package/src/config-modal.ts +231 -0
- package/src/extension-config.ts +45 -3
- package/src/index.ts +183 -135
- package/src/permission-dialog.ts +83 -0
- package/src/permission-forwarding.ts +102 -0
- package/src/test.ts +210 -2
- package/src/types-shims.d.ts +166 -0
- package/src/yolo-mode.ts +23 -0
- package/src/zellij-modal.ts +1001 -0
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isToolCallEventType, type ExtensionAPI, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
1
|
+
import { isToolCallEventType, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, normalize, resolve, sep } from "node:path";
|
|
@@ -6,27 +6,35 @@ import { dirname, join, normalize, resolve, sep } from "node:path";
|
|
|
6
6
|
import { toRecord } from "./common.js";
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_EXTENSION_CONFIG,
|
|
9
|
+
getPermissionSystemConfigPath,
|
|
9
10
|
loadPermissionSystemConfig,
|
|
11
|
+
normalizePermissionSystemConfig,
|
|
12
|
+
savePermissionSystemConfig,
|
|
10
13
|
type PermissionSystemExtensionConfig,
|
|
11
14
|
} from "./extension-config.js";
|
|
12
15
|
import { createPermissionSystemLogger } from "./logging.js";
|
|
16
|
+
import { registerPermissionSystemCommand } from "./config-modal.js";
|
|
17
|
+
import {
|
|
18
|
+
createPermissionForwardingLocation,
|
|
19
|
+
isForwardedPermissionRequestForSession,
|
|
20
|
+
PERMISSION_FORWARDING_POLL_INTERVAL_MS,
|
|
21
|
+
PERMISSION_FORWARDING_TIMEOUT_MS,
|
|
22
|
+
resolvePermissionForwardingTargetSessionId,
|
|
23
|
+
SUBAGENT_ENV_HINT_KEYS,
|
|
24
|
+
type ForwardedPermissionRequest,
|
|
25
|
+
type ForwardedPermissionResponse,
|
|
26
|
+
type PermissionForwardingLocation,
|
|
27
|
+
} from "./permission-forwarding.js";
|
|
13
28
|
import { PermissionManager } from "./permission-manager.js";
|
|
14
29
|
import { sanitizeAvailableToolsSection } from "./system-prompt-sanitizer.js";
|
|
15
30
|
import { checkRequestedToolRegistration, getToolNameFromValue } from "./tool-registry.js";
|
|
16
31
|
import type { PermissionCheckResult, PermissionState } from "./types.js";
|
|
32
|
+
import { canResolveAskPermissionRequest, shouldAutoApprovePermissionState } from "./yolo-mode.js";
|
|
17
33
|
|
|
18
34
|
const PI_AGENT_DIR = join(homedir(), ".pi", "agent");
|
|
19
35
|
const SESSIONS_DIR = join(PI_AGENT_DIR, "sessions");
|
|
20
36
|
const SUBAGENT_SESSIONS_DIR = join(PI_AGENT_DIR, "subagent-sessions");
|
|
21
37
|
const PERMISSION_FORWARDING_DIR = join(SESSIONS_DIR, "permission-forwarding");
|
|
22
|
-
const PERMISSION_FORWARDING_REQUESTS_DIR = join(PERMISSION_FORWARDING_DIR, "requests");
|
|
23
|
-
const PERMISSION_FORWARDING_RESPONSES_DIR = join(PERMISSION_FORWARDING_DIR, "responses");
|
|
24
|
-
const LEGACY_PERMISSION_FORWARDING_DIR = join(PI_AGENT_DIR, "permission-forwarding");
|
|
25
|
-
const LEGACY_PERMISSION_FORWARDING_REQUESTS_DIR = join(LEGACY_PERMISSION_FORWARDING_DIR, "requests");
|
|
26
|
-
const LEGACY_PERMISSION_FORWARDING_RESPONSES_DIR = join(LEGACY_PERMISSION_FORWARDING_DIR, "responses");
|
|
27
|
-
const PERMISSION_FORWARDING_POLL_INTERVAL_MS = 250;
|
|
28
|
-
const PERMISSION_FORWARDING_TIMEOUT_MS = 10 * 60 * 1000;
|
|
29
|
-
const SUBAGENT_ENV_HINT_KEYS = ["PI_IS_SUBAGENT", "PI_SUBAGENT_SESSION_ID", "PI_AGENT_ROUTER_SUBAGENT"] as const;
|
|
30
38
|
|
|
31
39
|
const AVAILABLE_SKILLS_OPEN_TAG = "<available_skills>";
|
|
32
40
|
const AVAILABLE_SKILLS_CLOSE_TAG = "</available_skills>";
|
|
@@ -51,26 +59,6 @@ type SkillPromptSection = {
|
|
|
51
59
|
entries: Array<{ name: string; description: string; location: string }>;
|
|
52
60
|
};
|
|
53
61
|
|
|
54
|
-
type ForwardedPermissionRequest = {
|
|
55
|
-
id: string;
|
|
56
|
-
createdAt: number;
|
|
57
|
-
requesterSessionId: string;
|
|
58
|
-
requesterAgentName: string;
|
|
59
|
-
message: string;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
type ForwardedPermissionResponse = {
|
|
63
|
-
approved: boolean;
|
|
64
|
-
responderSessionId: string;
|
|
65
|
-
respondedAt: number;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
type PermissionForwardingLocation = {
|
|
69
|
-
requestsDir: string;
|
|
70
|
-
responsesDir: string;
|
|
71
|
-
label: "primary" | "legacy";
|
|
72
|
-
};
|
|
73
|
-
|
|
74
62
|
type PermissionRequestSource = "tool_call" | "skill_input" | "skill_read";
|
|
75
63
|
type PermissionRequestState = "waiting" | "approved" | "denied";
|
|
76
64
|
|
|
@@ -98,10 +86,7 @@ const reportedLoggingWarnings = new Set<string>();
|
|
|
98
86
|
let loggingWarningReporter: ((message: string) => void) | null = null;
|
|
99
87
|
|
|
100
88
|
function setExtensionConfig(config: PermissionSystemExtensionConfig): void {
|
|
101
|
-
extensionConfig =
|
|
102
|
-
debugLog: config.debugLog,
|
|
103
|
-
permissionReviewLog: config.permissionReviewLog,
|
|
104
|
-
};
|
|
89
|
+
extensionConfig = normalizePermissionSystemConfig(config);
|
|
105
90
|
}
|
|
106
91
|
|
|
107
92
|
function setLoggingWarningReporter(reporter: ((message: string) => void) | null): void {
|
|
@@ -514,7 +499,11 @@ function isSubagentExecutionContext(ctx: ExtensionContext): boolean {
|
|
|
514
499
|
}
|
|
515
500
|
|
|
516
501
|
function canRequestPermissionConfirmation(ctx: ExtensionContext): boolean {
|
|
517
|
-
return
|
|
502
|
+
return canResolveAskPermissionRequest({
|
|
503
|
+
config: extensionConfig,
|
|
504
|
+
hasUI: ctx.hasUI,
|
|
505
|
+
isSubagent: isSubagentExecutionContext(ctx),
|
|
506
|
+
});
|
|
518
507
|
}
|
|
519
508
|
|
|
520
509
|
function formatUnknownErrorMessage(error: unknown): string {
|
|
@@ -556,54 +545,35 @@ function ensureDirectoryExists(path: string, description: string): boolean {
|
|
|
556
545
|
}
|
|
557
546
|
}
|
|
558
547
|
|
|
559
|
-
function
|
|
560
|
-
|
|
561
|
-
const responsesReady = ensureDirectoryExists(PERMISSION_FORWARDING_RESPONSES_DIR, "permission forwarding responses");
|
|
562
|
-
return requestsReady && responsesReady;
|
|
548
|
+
function getPermissionForwardingLocationForSession(sessionId: string): PermissionForwardingLocation {
|
|
549
|
+
return createPermissionForwardingLocation(PERMISSION_FORWARDING_DIR, sessionId);
|
|
563
550
|
}
|
|
564
551
|
|
|
565
|
-
function
|
|
566
|
-
|
|
567
|
-
return true;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (!existsSync(LEGACY_PERMISSION_FORWARDING_DIR)) {
|
|
571
|
-
logPermissionForwardingWarning(`Legacy permission-forwarding root '${LEGACY_PERMISSION_FORWARDING_DIR}' does not exist`);
|
|
572
|
-
return false;
|
|
573
|
-
}
|
|
574
|
-
|
|
552
|
+
function ensurePermissionForwardingLocation(sessionId: string): PermissionForwardingLocation | null {
|
|
553
|
+
let location: PermissionForwardingLocation;
|
|
575
554
|
try {
|
|
576
|
-
|
|
577
|
-
return true;
|
|
555
|
+
location = getPermissionForwardingLocationForSession(sessionId);
|
|
578
556
|
} catch (error) {
|
|
579
|
-
logPermissionForwardingError(
|
|
580
|
-
|
|
581
|
-
error,
|
|
582
|
-
);
|
|
583
|
-
return false;
|
|
557
|
+
logPermissionForwardingError("Failed to resolve permission forwarding location", error);
|
|
558
|
+
return null;
|
|
584
559
|
}
|
|
585
|
-
}
|
|
586
560
|
|
|
587
|
-
|
|
588
|
-
const
|
|
561
|
+
const sessionRootReady = ensureDirectoryExists(location.sessionRootDir, "permission forwarding session root");
|
|
562
|
+
const requestsReady = ensureDirectoryExists(location.requestsDir, "permission forwarding requests");
|
|
563
|
+
const responsesReady = ensureDirectoryExists(location.responsesDir, "permission forwarding responses");
|
|
589
564
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
requestsDir: PERMISSION_FORWARDING_REQUESTS_DIR,
|
|
593
|
-
responsesDir: PERMISSION_FORWARDING_RESPONSES_DIR,
|
|
594
|
-
label: "primary",
|
|
595
|
-
});
|
|
596
|
-
}
|
|
565
|
+
return sessionRootReady && requestsReady && responsesReady ? location : null;
|
|
566
|
+
}
|
|
597
567
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
568
|
+
function getExistingPermissionForwardingLocation(sessionId: string): PermissionForwardingLocation | null {
|
|
569
|
+
let location: PermissionForwardingLocation;
|
|
570
|
+
try {
|
|
571
|
+
location = getPermissionForwardingLocationForSession(sessionId);
|
|
572
|
+
} catch {
|
|
573
|
+
return null;
|
|
604
574
|
}
|
|
605
575
|
|
|
606
|
-
return
|
|
576
|
+
return existsSync(location.requestsDir) ? location : null;
|
|
607
577
|
}
|
|
608
578
|
|
|
609
579
|
function tryRemoveDirectoryIfEmpty(path: string, description: string): void {
|
|
@@ -634,14 +604,10 @@ function tryRemoveDirectoryIfEmpty(path: string, description: string): void {
|
|
|
634
604
|
}
|
|
635
605
|
}
|
|
636
606
|
|
|
637
|
-
function
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
tryRemoveDirectoryIfEmpty(LEGACY_PERMISSION_FORWARDING_REQUESTS_DIR, "legacy permission forwarding requests");
|
|
643
|
-
tryRemoveDirectoryIfEmpty(LEGACY_PERMISSION_FORWARDING_RESPONSES_DIR, "legacy permission forwarding responses");
|
|
644
|
-
tryRemoveDirectoryIfEmpty(LEGACY_PERMISSION_FORWARDING_DIR, "legacy permission forwarding root");
|
|
607
|
+
function cleanupPermissionForwardingLocationIfEmpty(location: PermissionForwardingLocation): void {
|
|
608
|
+
tryRemoveDirectoryIfEmpty(location.requestsDir, `${location.label} permission forwarding requests`);
|
|
609
|
+
tryRemoveDirectoryIfEmpty(location.responsesDir, `${location.label} permission forwarding responses`);
|
|
610
|
+
tryRemoveDirectoryIfEmpty(location.sessionRootDir, `${location.label} permission forwarding session root`);
|
|
645
611
|
}
|
|
646
612
|
|
|
647
613
|
function safeDeleteFile(filePath: string, description: string): void {
|
|
@@ -677,6 +643,7 @@ function readForwardedPermissionRequest(filePath: string): ForwardedPermissionRe
|
|
|
677
643
|
|| typeof parsed.id !== "string"
|
|
678
644
|
|| typeof parsed.createdAt !== "number"
|
|
679
645
|
|| typeof parsed.requesterSessionId !== "string"
|
|
646
|
+
|| typeof parsed.targetSessionId !== "string"
|
|
680
647
|
|| typeof parsed.requesterAgentName !== "string"
|
|
681
648
|
|| typeof parsed.message !== "string"
|
|
682
649
|
) {
|
|
@@ -688,6 +655,7 @@ function readForwardedPermissionRequest(filePath: string): ForwardedPermissionRe
|
|
|
688
655
|
id: parsed.id,
|
|
689
656
|
createdAt: parsed.createdAt,
|
|
690
657
|
requesterSessionId: parsed.requesterSessionId,
|
|
658
|
+
targetSessionId: parsed.targetSessionId,
|
|
691
659
|
requesterAgentName: parsed.requesterAgentName,
|
|
692
660
|
message: parsed.message,
|
|
693
661
|
};
|
|
@@ -729,8 +697,26 @@ function formatForwardedPermissionPrompt(request: ForwardedPermissionRequest): s
|
|
|
729
697
|
}
|
|
730
698
|
|
|
731
699
|
async function waitForForwardedPermissionApproval(ctx: ExtensionContext, message: string): Promise<boolean> {
|
|
732
|
-
|
|
733
|
-
|
|
700
|
+
const requesterSessionId = getSessionId(ctx);
|
|
701
|
+
const targetSessionId = resolvePermissionForwardingTargetSessionId({
|
|
702
|
+
hasUI: ctx.hasUI,
|
|
703
|
+
isSubagent: isSubagentExecutionContext(ctx),
|
|
704
|
+
currentSessionId: requesterSessionId,
|
|
705
|
+
env: process.env,
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
if (!targetSessionId) {
|
|
709
|
+
logPermissionForwardingError(
|
|
710
|
+
"Permission forwarding target session could not be resolved from subagent runtime metadata (expected PI_AGENT_ROUTER_PARENT_SESSION_ID)",
|
|
711
|
+
);
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const location = ensurePermissionForwardingLocation(targetSessionId);
|
|
716
|
+
if (!location) {
|
|
717
|
+
logPermissionForwardingError(
|
|
718
|
+
`Permission forwarding is unavailable because session-scoped directories could not be prepared for '${targetSessionId}'`,
|
|
719
|
+
);
|
|
734
720
|
return false;
|
|
735
721
|
}
|
|
736
722
|
|
|
@@ -739,18 +725,20 @@ async function waitForForwardedPermissionApproval(ctx: ExtensionContext, message
|
|
|
739
725
|
const request: ForwardedPermissionRequest = {
|
|
740
726
|
id: requestId,
|
|
741
727
|
createdAt: Date.now(),
|
|
742
|
-
requesterSessionId
|
|
728
|
+
requesterSessionId,
|
|
729
|
+
targetSessionId,
|
|
743
730
|
requesterAgentName,
|
|
744
731
|
message,
|
|
745
732
|
};
|
|
746
733
|
|
|
747
|
-
const requestPath = join(
|
|
748
|
-
const responsePath = join(
|
|
734
|
+
const requestPath = join(location.requestsDir, `${requestId}.json`);
|
|
735
|
+
const responsePath = join(location.responsesDir, `${requestId}.json`);
|
|
749
736
|
|
|
750
737
|
writeReviewLog("forwarded_permission.request_created", {
|
|
751
738
|
requestId,
|
|
752
739
|
requesterAgentName,
|
|
753
740
|
requesterSessionId: request.requesterSessionId,
|
|
741
|
+
targetSessionId,
|
|
754
742
|
requestPath,
|
|
755
743
|
responsePath,
|
|
756
744
|
});
|
|
@@ -770,10 +758,12 @@ async function waitForForwardedPermissionApproval(ctx: ExtensionContext, message
|
|
|
770
758
|
requestId,
|
|
771
759
|
approved: response?.approved ?? null,
|
|
772
760
|
responderSessionId: response?.responderSessionId ?? null,
|
|
761
|
+
targetSessionId,
|
|
773
762
|
responsePath,
|
|
774
763
|
});
|
|
775
764
|
safeDeleteFile(responsePath, "forwarded permission response");
|
|
776
765
|
safeDeleteFile(requestPath, "forwarded permission request");
|
|
766
|
+
cleanupPermissionForwardingLocationIfEmpty(location);
|
|
777
767
|
return Boolean(response?.approved);
|
|
778
768
|
}
|
|
779
769
|
|
|
@@ -784,9 +774,11 @@ async function waitForForwardedPermissionApproval(ctx: ExtensionContext, message
|
|
|
784
774
|
writeReviewLog("forwarded_permission.response_timed_out", {
|
|
785
775
|
requestId,
|
|
786
776
|
requesterAgentName,
|
|
777
|
+
targetSessionId,
|
|
787
778
|
responsePath,
|
|
788
779
|
});
|
|
789
780
|
safeDeleteFile(requestPath, "forwarded permission request");
|
|
781
|
+
cleanupPermissionForwardingLocationIfEmpty(location);
|
|
790
782
|
return false;
|
|
791
783
|
}
|
|
792
784
|
|
|
@@ -795,74 +787,85 @@ async function processForwardedPermissionRequests(ctx: ExtensionContext): Promis
|
|
|
795
787
|
return;
|
|
796
788
|
}
|
|
797
789
|
|
|
798
|
-
const
|
|
799
|
-
|
|
790
|
+
const currentSessionId = getSessionId(ctx);
|
|
791
|
+
const location = getExistingPermissionForwardingLocation(currentSessionId);
|
|
792
|
+
if (!location) {
|
|
800
793
|
return;
|
|
801
794
|
}
|
|
802
795
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
}
|
|
810
|
-
|
|
796
|
+
let requestFiles: string[] = [];
|
|
797
|
+
try {
|
|
798
|
+
requestFiles = readdirSync(location.requestsDir)
|
|
799
|
+
.filter((name) => name.endsWith(".json"))
|
|
800
|
+
.sort();
|
|
801
|
+
} catch (error) {
|
|
802
|
+
logPermissionForwardingWarning(`Failed to read ${location.label} permission forwarding requests from '${location.requestsDir}'`, error);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
for (const fileName of requestFiles) {
|
|
807
|
+
const requestPath = join(location.requestsDir, fileName);
|
|
808
|
+
const request = readForwardedPermissionRequest(requestPath);
|
|
809
|
+
if (!request) {
|
|
810
|
+
safeDeleteFile(requestPath, `${location.label} forwarded permission request`);
|
|
811
811
|
continue;
|
|
812
812
|
}
|
|
813
813
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
814
|
+
if (!isForwardedPermissionRequestForSession(request, currentSessionId)) {
|
|
815
|
+
logPermissionForwardingWarning(
|
|
816
|
+
`Ignoring forwarded permission request '${request.id}' because it targets session '${request.targetSessionId}' instead of '${currentSessionId}'`,
|
|
817
|
+
);
|
|
818
|
+
safeDeleteFile(requestPath, `${location.label} forwarded permission request`);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
821
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
822
|
+
const forwardedPermissionLogDetails = {
|
|
823
|
+
requestId: request.id,
|
|
824
|
+
source: location.label,
|
|
825
|
+
requesterAgentName: request.requesterAgentName,
|
|
826
|
+
requesterSessionId: request.requesterSessionId,
|
|
827
|
+
targetSessionId: request.targetSessionId,
|
|
828
|
+
requestPath,
|
|
829
|
+
};
|
|
829
830
|
|
|
830
|
-
|
|
831
|
+
let approved = false;
|
|
832
|
+
if (shouldAutoApprovePermissionState("ask", extensionConfig)) {
|
|
833
|
+
writeReviewLog("forwarded_permission.auto_approved", forwardedPermissionLogDetails);
|
|
834
|
+
approved = true;
|
|
835
|
+
} else {
|
|
836
|
+
writeReviewLog("forwarded_permission.prompted", forwardedPermissionLogDetails);
|
|
831
837
|
try {
|
|
832
838
|
approved = await ctx.ui.confirm("Permission Required (Subagent)", formatForwardedPermissionPrompt(request));
|
|
833
839
|
} catch (error) {
|
|
834
840
|
logPermissionForwardingError("Failed to show forwarded permission confirmation dialog", error);
|
|
835
841
|
approved = false;
|
|
836
842
|
}
|
|
843
|
+
}
|
|
837
844
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
} catch (error) {
|
|
857
|
-
logPermissionForwardingError(`Failed to write ${location.label} forwarded permission response '${responsePath}'`, error);
|
|
858
|
-
continue;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
safeDeleteFile(requestPath, `${location.label} forwarded permission request`);
|
|
845
|
+
const responsePath = join(location.responsesDir, `${request.id}.json`);
|
|
846
|
+
writeReviewLog(approved ? "forwarded_permission.approved" : "forwarded_permission.denied", {
|
|
847
|
+
requestId: request.id,
|
|
848
|
+
source: location.label,
|
|
849
|
+
requesterAgentName: request.requesterAgentName,
|
|
850
|
+
requesterSessionId: request.requesterSessionId,
|
|
851
|
+
targetSessionId: request.targetSessionId,
|
|
852
|
+
responsePath,
|
|
853
|
+
});
|
|
854
|
+
try {
|
|
855
|
+
writeJsonFileAtomic(responsePath, {
|
|
856
|
+
approved,
|
|
857
|
+
responderSessionId: currentSessionId,
|
|
858
|
+
respondedAt: Date.now(),
|
|
859
|
+
} satisfies ForwardedPermissionResponse);
|
|
860
|
+
} catch (error) {
|
|
861
|
+
logPermissionForwardingError(`Failed to write ${location.label} forwarded permission response '${responsePath}'`, error);
|
|
862
|
+
continue;
|
|
862
863
|
}
|
|
864
|
+
|
|
865
|
+
safeDeleteFile(requestPath, `${location.label} forwarded permission request`);
|
|
863
866
|
}
|
|
864
867
|
|
|
865
|
-
|
|
868
|
+
cleanupPermissionForwardingLocationIfEmpty(location);
|
|
866
869
|
}
|
|
867
870
|
|
|
868
871
|
async function confirmPermission(ctx: ExtensionContext, message: string): Promise<boolean> {
|
|
@@ -915,12 +918,39 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
915
918
|
warning: result.warning ?? null,
|
|
916
919
|
debugLog: result.config.debugLog,
|
|
917
920
|
permissionReviewLog: result.config.permissionReviewLog,
|
|
921
|
+
yoloMode: result.config.yoloMode,
|
|
922
|
+
});
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
const saveExtensionConfig = (next: PermissionSystemExtensionConfig, ctx: ExtensionCommandContext): void => {
|
|
926
|
+
const normalized = normalizePermissionSystemConfig(next);
|
|
927
|
+
const saved = savePermissionSystemConfig(normalized);
|
|
928
|
+
if (!saved.success) {
|
|
929
|
+
if (saved.error) {
|
|
930
|
+
ctx.ui.notify(saved.error, "error");
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
setExtensionConfig(normalized);
|
|
936
|
+
lastConfigWarning = null;
|
|
937
|
+
|
|
938
|
+
writeDebugLog("config.saved", {
|
|
939
|
+
debugLog: normalized.debugLog,
|
|
940
|
+
permissionReviewLog: normalized.permissionReviewLog,
|
|
941
|
+
yoloMode: normalized.yoloMode,
|
|
918
942
|
});
|
|
919
943
|
};
|
|
920
944
|
|
|
921
945
|
setLoggingWarningReporter(notifyWarning);
|
|
922
946
|
refreshExtensionConfig();
|
|
923
947
|
|
|
948
|
+
registerPermissionSystemCommand(pi, {
|
|
949
|
+
getConfig: () => extensionConfig,
|
|
950
|
+
setConfig: saveExtensionConfig,
|
|
951
|
+
getConfigPath: getPermissionSystemConfigPath,
|
|
952
|
+
});
|
|
953
|
+
|
|
924
954
|
const createPermissionRequestId = (prefix: string): string => {
|
|
925
955
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
|
|
926
956
|
};
|
|
@@ -984,6 +1014,24 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
984
1014
|
target?: string;
|
|
985
1015
|
},
|
|
986
1016
|
): Promise<boolean> => {
|
|
1017
|
+
if (shouldAutoApprovePermissionState("ask", extensionConfig)) {
|
|
1018
|
+
reviewPermissionDecision("permission_request.auto_approved", details);
|
|
1019
|
+
emitPermissionRequestEvent({
|
|
1020
|
+
requestId: details.requestId,
|
|
1021
|
+
source: details.source,
|
|
1022
|
+
state: "approved",
|
|
1023
|
+
message: details.message,
|
|
1024
|
+
toolCallId: details.toolCallId,
|
|
1025
|
+
toolName: details.toolName,
|
|
1026
|
+
skillName: details.skillName,
|
|
1027
|
+
path: details.path,
|
|
1028
|
+
command: details.command,
|
|
1029
|
+
target: details.target,
|
|
1030
|
+
agentName: details.agentName,
|
|
1031
|
+
});
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
987
1035
|
reviewPermissionDecision("permission_request.waiting", details);
|
|
988
1036
|
emitPermissionRequestEvent({
|
|
989
1037
|
requestId: details.requestId,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type PermissionDecisionState = "approved" | "denied" | "denied_with_reason";
|
|
2
|
+
|
|
3
|
+
export type PermissionPromptDecision = {
|
|
4
|
+
approved: boolean;
|
|
5
|
+
state: PermissionDecisionState;
|
|
6
|
+
denialReason?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export interface PermissionDecisionUi {
|
|
10
|
+
select(title: string, options: string[]): Promise<string | undefined>;
|
|
11
|
+
input(title: string, placeholder?: string): Promise<string | undefined>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const APPROVE_OPTION = "Yes";
|
|
15
|
+
const DENY_OPTION = "No";
|
|
16
|
+
const DENY_WITH_REASON_OPTION = "No, provide reason";
|
|
17
|
+
const PERMISSION_DECISION_OPTIONS = [
|
|
18
|
+
APPROVE_OPTION,
|
|
19
|
+
DENY_OPTION,
|
|
20
|
+
DENY_WITH_REASON_OPTION,
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export function normalizePermissionDenialReason(value: unknown): string | undefined {
|
|
24
|
+
if (typeof value !== "string") {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createDeniedPermissionDecision(
|
|
33
|
+
denialReason?: string,
|
|
34
|
+
): PermissionPromptDecision {
|
|
35
|
+
const normalizedReason = normalizePermissionDenialReason(denialReason);
|
|
36
|
+
return normalizedReason
|
|
37
|
+
? {
|
|
38
|
+
approved: false,
|
|
39
|
+
state: "denied_with_reason",
|
|
40
|
+
denialReason: normalizedReason,
|
|
41
|
+
}
|
|
42
|
+
: {
|
|
43
|
+
approved: false,
|
|
44
|
+
state: "denied",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isPermissionDecisionState(
|
|
49
|
+
value: unknown,
|
|
50
|
+
): value is PermissionDecisionState {
|
|
51
|
+
return value === "approved" || value === "denied" || value === "denied_with_reason";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function requestPermissionDecisionFromUi(
|
|
55
|
+
ui: PermissionDecisionUi,
|
|
56
|
+
title: string,
|
|
57
|
+
message: string,
|
|
58
|
+
): Promise<PermissionPromptDecision> {
|
|
59
|
+
const selected = await ui.select(
|
|
60
|
+
`${title}\n${message}`,
|
|
61
|
+
[...PERMISSION_DECISION_OPTIONS],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (selected === APPROVE_OPTION) {
|
|
65
|
+
return {
|
|
66
|
+
approved: true,
|
|
67
|
+
state: "approved",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (selected === DENY_WITH_REASON_OPTION) {
|
|
72
|
+
const denialReason = normalizePermissionDenialReason(
|
|
73
|
+
await ui.input(
|
|
74
|
+
`${title}\nShare why this request was denied (optional).`,
|
|
75
|
+
"Reason shown back to the agent",
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return createDeniedPermissionDecision(denialReason);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return createDeniedPermissionDecision();
|
|
83
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const PERMISSION_FORWARDING_POLL_INTERVAL_MS = 250;
|
|
4
|
+
export const PERMISSION_FORWARDING_TIMEOUT_MS = 10 * 60 * 1000;
|
|
5
|
+
export const SUBAGENT_ENV_HINT_KEYS = ["PI_IS_SUBAGENT", "PI_SUBAGENT_SESSION_ID", "PI_AGENT_ROUTER_SUBAGENT"] as const;
|
|
6
|
+
export const SUBAGENT_PARENT_SESSION_ENV_KEY = "PI_AGENT_ROUTER_PARENT_SESSION_ID";
|
|
7
|
+
|
|
8
|
+
const SESSION_FORWARDING_ROOT_DIRECTORY_NAME = "sessions";
|
|
9
|
+
const SESSION_FORWARDING_REQUESTS_DIRECTORY_NAME = "requests";
|
|
10
|
+
const SESSION_FORWARDING_RESPONSES_DIRECTORY_NAME = "responses";
|
|
11
|
+
|
|
12
|
+
export type ForwardedPermissionRequest = {
|
|
13
|
+
id: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
requesterSessionId: string;
|
|
16
|
+
targetSessionId: string;
|
|
17
|
+
requesterAgentName: string;
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ForwardedPermissionResponse = {
|
|
22
|
+
approved: boolean;
|
|
23
|
+
responderSessionId: string;
|
|
24
|
+
respondedAt: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type PermissionForwardingLocation = {
|
|
28
|
+
sessionId: string;
|
|
29
|
+
sessionRootDir: string;
|
|
30
|
+
requestsDir: string;
|
|
31
|
+
responsesDir: string;
|
|
32
|
+
label: "primary";
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function normalizePermissionForwardingSessionId(value: unknown): string | null {
|
|
36
|
+
if (typeof value !== "string") {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const trimmed = value.trim();
|
|
41
|
+
if (!trimmed || trimmed.toLowerCase() === "unknown") {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return trimmed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function encodeSessionIdForPath(sessionId: string): string {
|
|
49
|
+
return encodeURIComponent(sessionId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createPermissionForwardingLocation(
|
|
53
|
+
forwardingRootDir: string,
|
|
54
|
+
sessionId: string,
|
|
55
|
+
): PermissionForwardingLocation {
|
|
56
|
+
const normalizedSessionId = normalizePermissionForwardingSessionId(sessionId);
|
|
57
|
+
if (!normalizedSessionId) {
|
|
58
|
+
throw new Error("Permission forwarding session id must be a non-empty string.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sessionRootDir = join(
|
|
62
|
+
forwardingRootDir,
|
|
63
|
+
SESSION_FORWARDING_ROOT_DIRECTORY_NAME,
|
|
64
|
+
encodeSessionIdForPath(normalizedSessionId),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
sessionId: normalizedSessionId,
|
|
69
|
+
sessionRootDir,
|
|
70
|
+
requestsDir: join(sessionRootDir, SESSION_FORWARDING_REQUESTS_DIRECTORY_NAME),
|
|
71
|
+
responsesDir: join(sessionRootDir, SESSION_FORWARDING_RESPONSES_DIRECTORY_NAME),
|
|
72
|
+
label: "primary",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolvePermissionForwardingTargetSessionId(options: {
|
|
77
|
+
hasUI: boolean;
|
|
78
|
+
isSubagent: boolean;
|
|
79
|
+
currentSessionId?: string | null;
|
|
80
|
+
env?: NodeJS.ProcessEnv;
|
|
81
|
+
}): string | null {
|
|
82
|
+
if (options.hasUI) {
|
|
83
|
+
return normalizePermissionForwardingSessionId(options.currentSessionId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!options.isSubagent) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return normalizePermissionForwardingSessionId(
|
|
91
|
+
options.env?.[SUBAGENT_PARENT_SESSION_ENV_KEY],
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isForwardedPermissionRequestForSession(
|
|
96
|
+
request: Pick<ForwardedPermissionRequest, "targetSessionId">,
|
|
97
|
+
sessionId: string | null | undefined,
|
|
98
|
+
): boolean {
|
|
99
|
+
const normalizedRequestSessionId = normalizePermissionForwardingSessionId(request.targetSessionId);
|
|
100
|
+
const normalizedSessionId = normalizePermissionForwardingSessionId(sessionId);
|
|
101
|
+
return normalizedRequestSessionId !== null && normalizedRequestSessionId === normalizedSessionId;
|
|
102
|
+
}
|