pi-permission-system 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +66 -30
- package/config.json +4 -0
- package/package.json +60 -59
- package/schemas/permissions.schema.json +2 -0
- package/src/extension-config.ts +106 -0
- package/src/index.ts +341 -36
- package/src/logging.ts +94 -0
- package/src/permission-manager.ts +13 -34
- package/src/test.ts +173 -23
package/src/index.ts
CHANGED
|
@@ -4,6 +4,12 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { dirname, join, normalize, resolve, sep } from "node:path";
|
|
5
5
|
|
|
6
6
|
import { toRecord } from "./common.js";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_EXTENSION_CONFIG,
|
|
9
|
+
loadPermissionSystemConfig,
|
|
10
|
+
type PermissionSystemExtensionConfig,
|
|
11
|
+
} from "./extension-config.js";
|
|
12
|
+
import { createPermissionSystemLogger } from "./logging.js";
|
|
7
13
|
import { PermissionManager } from "./permission-manager.js";
|
|
8
14
|
import { sanitizeAvailableToolsSection } from "./system-prompt-sanitizer.js";
|
|
9
15
|
import { checkRequestedToolRegistration, getToolNameFromValue } from "./tool-registry.js";
|
|
@@ -21,8 +27,6 @@ const LEGACY_PERMISSION_FORWARDING_RESPONSES_DIR = join(LEGACY_PERMISSION_FORWAR
|
|
|
21
27
|
const PERMISSION_FORWARDING_POLL_INTERVAL_MS = 250;
|
|
22
28
|
const PERMISSION_FORWARDING_TIMEOUT_MS = 10 * 60 * 1000;
|
|
23
29
|
const SUBAGENT_ENV_HINT_KEYS = ["PI_IS_SUBAGENT", "PI_SUBAGENT_SESSION_ID", "PI_AGENT_ROUTER_SUBAGENT"] as const;
|
|
24
|
-
const ORCHESTRATOR_AGENT_NAME = "orchestrator";
|
|
25
|
-
const DELEGATION_TOOL_NAME = "task";
|
|
26
30
|
|
|
27
31
|
const AVAILABLE_SKILLS_OPEN_TAG = "<available_skills>";
|
|
28
32
|
const AVAILABLE_SKILLS_CLOSE_TAG = "</available_skills>";
|
|
@@ -67,6 +71,66 @@ type PermissionForwardingLocation = {
|
|
|
67
71
|
label: "primary" | "legacy";
|
|
68
72
|
};
|
|
69
73
|
|
|
74
|
+
type PermissionRequestSource = "tool_call" | "skill_input" | "skill_read";
|
|
75
|
+
type PermissionRequestState = "waiting" | "approved" | "denied";
|
|
76
|
+
|
|
77
|
+
type PermissionRequestEvent = {
|
|
78
|
+
requestId: string;
|
|
79
|
+
source: PermissionRequestSource;
|
|
80
|
+
state: PermissionRequestState;
|
|
81
|
+
message: string;
|
|
82
|
+
toolCallId?: string;
|
|
83
|
+
toolName?: string;
|
|
84
|
+
skillName?: string;
|
|
85
|
+
path?: string;
|
|
86
|
+
command?: string;
|
|
87
|
+
target?: string;
|
|
88
|
+
agentName?: string | null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const PERMISSION_REQUEST_EVENT_CHANNEL = "pi-permission-system:permission-request";
|
|
92
|
+
|
|
93
|
+
let extensionConfig: PermissionSystemExtensionConfig = { ...DEFAULT_EXTENSION_CONFIG };
|
|
94
|
+
const extensionLogger = createPermissionSystemLogger({
|
|
95
|
+
getConfig: () => extensionConfig,
|
|
96
|
+
});
|
|
97
|
+
const reportedLoggingWarnings = new Set<string>();
|
|
98
|
+
let loggingWarningReporter: ((message: string) => void) | null = null;
|
|
99
|
+
|
|
100
|
+
function setExtensionConfig(config: PermissionSystemExtensionConfig): void {
|
|
101
|
+
extensionConfig = {
|
|
102
|
+
debugLog: config.debugLog,
|
|
103
|
+
permissionReviewLog: config.permissionReviewLog,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function setLoggingWarningReporter(reporter: ((message: string) => void) | null): void {
|
|
108
|
+
loggingWarningReporter = reporter;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function reportLoggingWarning(message: string): void {
|
|
112
|
+
if (!loggingWarningReporter || reportedLoggingWarnings.has(message)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
reportedLoggingWarnings.add(message);
|
|
117
|
+
loggingWarningReporter(message);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function writeDebugLog(event: string, details: Record<string, unknown> = {}): void {
|
|
121
|
+
const warning = extensionLogger.debug(event, details);
|
|
122
|
+
if (warning) {
|
|
123
|
+
reportLoggingWarning(warning);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function writeReviewLog(event: string, details: Record<string, unknown> = {}): void {
|
|
128
|
+
const warning = extensionLogger.review(event, details);
|
|
129
|
+
if (warning) {
|
|
130
|
+
reportLoggingWarning(warning);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
70
134
|
function decodeXml(value: string): string {
|
|
71
135
|
return value
|
|
72
136
|
.replace(/</g, "<")
|
|
@@ -311,15 +375,6 @@ function getActiveAgentNameFromSystemPrompt(systemPrompt: string | undefined): s
|
|
|
311
375
|
return normalizeAgentName(match[1]);
|
|
312
376
|
}
|
|
313
377
|
|
|
314
|
-
function isDelegationAllowedAgent(agentName: string | null): boolean {
|
|
315
|
-
return Boolean(agentName && agentName.toLowerCase() === ORCHESTRATOR_AGENT_NAME);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function getDelegationBlockReason(agentName: string | null): string {
|
|
319
|
-
const resolvedAgent = agentName ?? "none";
|
|
320
|
-
return `Tool '${DELEGATION_TOOL_NAME}' is restricted to '${ORCHESTRATOR_AGENT_NAME}'. Active agent '${resolvedAgent}' cannot delegate.`;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
378
|
function formatMissingToolNameReason(): string {
|
|
324
379
|
return "Tool call was blocked because no tool name was provided. Use a registered tool name from pi.getAllTools().";
|
|
325
380
|
}
|
|
@@ -331,7 +386,7 @@ function formatUnknownToolReason(toolName: string, availableToolNames: readonly
|
|
|
331
386
|
|
|
332
387
|
const mcpHint = toolName === "mcp"
|
|
333
388
|
? ""
|
|
334
|
-
: " If this was intended as an MCP server tool, call the
|
|
389
|
+
: " If this was intended as an MCP server tool, call the registered 'mcp' tool when available (for example: {\"tool\":\"server:tool\"}).";
|
|
335
390
|
|
|
336
391
|
return `Tool '${toolName}' is not registered in this runtime and was blocked before permission checks.${mcpHint} Registered tools: ${availableList}.`;
|
|
337
392
|
}
|
|
@@ -410,6 +465,13 @@ function formatSkillPathDenyReason(skill: SkillPromptEntry, readPath: string, ag
|
|
|
410
465
|
return `${subject} is not permitted to access skill '${skill.name}' via '${readPath}'.`;
|
|
411
466
|
}
|
|
412
467
|
|
|
468
|
+
function getPermissionLogContext(result: PermissionCheckResult): { command?: string; target?: string } {
|
|
469
|
+
return {
|
|
470
|
+
command: result.command,
|
|
471
|
+
target: result.target,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
413
475
|
function sleep(ms: number): Promise<void> {
|
|
414
476
|
return new Promise((resolve) => {
|
|
415
477
|
setTimeout(resolve, ms);
|
|
@@ -467,21 +529,21 @@ function isErrnoCode(error: unknown, code: string): boolean {
|
|
|
467
529
|
}
|
|
468
530
|
|
|
469
531
|
function logPermissionForwardingWarning(message: string, error?: unknown): void {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
532
|
+
const details = typeof error === "undefined"
|
|
533
|
+
? { message }
|
|
534
|
+
: { message, error: formatUnknownErrorMessage(error) };
|
|
474
535
|
|
|
475
|
-
|
|
536
|
+
writeReviewLog("permission_forwarding.warning", details);
|
|
537
|
+
writeDebugLog("permission_forwarding.warning", details);
|
|
476
538
|
}
|
|
477
539
|
|
|
478
540
|
function logPermissionForwardingError(message: string, error?: unknown): void {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
541
|
+
const details = typeof error === "undefined"
|
|
542
|
+
? { message }
|
|
543
|
+
: { message, error: formatUnknownErrorMessage(error) };
|
|
483
544
|
|
|
484
|
-
|
|
545
|
+
writeReviewLog("permission_forwarding.error", details);
|
|
546
|
+
writeDebugLog("permission_forwarding.error", details);
|
|
485
547
|
}
|
|
486
548
|
|
|
487
549
|
function ensureDirectoryExists(path: string, description: string): boolean {
|
|
@@ -685,6 +747,14 @@ async function waitForForwardedPermissionApproval(ctx: ExtensionContext, message
|
|
|
685
747
|
const requestPath = join(PERMISSION_FORWARDING_REQUESTS_DIR, `${requestId}.json`);
|
|
686
748
|
const responsePath = join(PERMISSION_FORWARDING_RESPONSES_DIR, `${requestId}.json`);
|
|
687
749
|
|
|
750
|
+
writeReviewLog("forwarded_permission.request_created", {
|
|
751
|
+
requestId,
|
|
752
|
+
requesterAgentName,
|
|
753
|
+
requesterSessionId: request.requesterSessionId,
|
|
754
|
+
requestPath,
|
|
755
|
+
responsePath,
|
|
756
|
+
});
|
|
757
|
+
|
|
688
758
|
try {
|
|
689
759
|
writeJsonFileAtomic(requestPath, request);
|
|
690
760
|
} catch (error) {
|
|
@@ -696,6 +766,12 @@ async function waitForForwardedPermissionApproval(ctx: ExtensionContext, message
|
|
|
696
766
|
while (Date.now() < deadline) {
|
|
697
767
|
if (existsSync(responsePath)) {
|
|
698
768
|
const response = readForwardedPermissionResponse(responsePath);
|
|
769
|
+
writeReviewLog("forwarded_permission.response_received", {
|
|
770
|
+
requestId,
|
|
771
|
+
approved: response?.approved ?? null,
|
|
772
|
+
responderSessionId: response?.responderSessionId ?? null,
|
|
773
|
+
responsePath,
|
|
774
|
+
});
|
|
699
775
|
safeDeleteFile(responsePath, "forwarded permission response");
|
|
700
776
|
safeDeleteFile(requestPath, "forwarded permission request");
|
|
701
777
|
return Boolean(response?.approved);
|
|
@@ -705,6 +781,11 @@ async function waitForForwardedPermissionApproval(ctx: ExtensionContext, message
|
|
|
705
781
|
}
|
|
706
782
|
|
|
707
783
|
logPermissionForwardingWarning(`Timed out waiting for forwarded permission response '${responsePath}'`);
|
|
784
|
+
writeReviewLog("forwarded_permission.response_timed_out", {
|
|
785
|
+
requestId,
|
|
786
|
+
requesterAgentName,
|
|
787
|
+
responsePath,
|
|
788
|
+
});
|
|
708
789
|
safeDeleteFile(requestPath, "forwarded permission request");
|
|
709
790
|
return false;
|
|
710
791
|
}
|
|
@@ -738,6 +819,14 @@ async function processForwardedPermissionRequests(ctx: ExtensionContext): Promis
|
|
|
738
819
|
continue;
|
|
739
820
|
}
|
|
740
821
|
|
|
822
|
+
writeReviewLog("forwarded_permission.prompted", {
|
|
823
|
+
requestId: request.id,
|
|
824
|
+
source: location.label,
|
|
825
|
+
requesterAgentName: request.requesterAgentName,
|
|
826
|
+
requesterSessionId: request.requesterSessionId,
|
|
827
|
+
requestPath,
|
|
828
|
+
});
|
|
829
|
+
|
|
741
830
|
let approved = false;
|
|
742
831
|
try {
|
|
743
832
|
approved = await ctx.ui.confirm("Permission Required (Subagent)", formatForwardedPermissionPrompt(request));
|
|
@@ -751,6 +840,13 @@ async function processForwardedPermissionRequests(ctx: ExtensionContext): Promis
|
|
|
751
840
|
}
|
|
752
841
|
|
|
753
842
|
const responsePath = join(location.responsesDir, `${request.id}.json`);
|
|
843
|
+
writeReviewLog(approved ? "forwarded_permission.approved" : "forwarded_permission.denied", {
|
|
844
|
+
requestId: request.id,
|
|
845
|
+
source: location.label,
|
|
846
|
+
requesterAgentName: request.requesterAgentName,
|
|
847
|
+
requesterSessionId: request.requesterSessionId,
|
|
848
|
+
responsePath,
|
|
849
|
+
});
|
|
754
850
|
try {
|
|
755
851
|
writeJsonFileAtomic(responsePath, {
|
|
756
852
|
approved,
|
|
@@ -788,6 +884,139 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
788
884
|
let permissionForwardingContext: ExtensionContext | null = null;
|
|
789
885
|
let permissionForwardingTimer: NodeJS.Timeout | null = null;
|
|
790
886
|
let isProcessingForwardedRequests = false;
|
|
887
|
+
let runtimeContext: ExtensionContext | null = null;
|
|
888
|
+
let lastConfigWarning: string | null = null;
|
|
889
|
+
|
|
890
|
+
const notifyWarning = (message: string): void => {
|
|
891
|
+
if (!runtimeContext?.hasUI) {
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
runtimeContext.ui.notify(message, "warning");
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
const refreshExtensionConfig = (ctx?: ExtensionContext): void => {
|
|
899
|
+
if (ctx) {
|
|
900
|
+
runtimeContext = ctx;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const result = loadPermissionSystemConfig();
|
|
904
|
+
setExtensionConfig(result.config);
|
|
905
|
+
|
|
906
|
+
if (result.warning && result.warning !== lastConfigWarning) {
|
|
907
|
+
lastConfigWarning = result.warning;
|
|
908
|
+
notifyWarning(result.warning);
|
|
909
|
+
} else if (!result.warning) {
|
|
910
|
+
lastConfigWarning = null;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
writeDebugLog("config.loaded", {
|
|
914
|
+
created: result.created,
|
|
915
|
+
warning: result.warning ?? null,
|
|
916
|
+
debugLog: result.config.debugLog,
|
|
917
|
+
permissionReviewLog: result.config.permissionReviewLog,
|
|
918
|
+
});
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
setLoggingWarningReporter(notifyWarning);
|
|
922
|
+
refreshExtensionConfig();
|
|
923
|
+
|
|
924
|
+
const createPermissionRequestId = (prefix: string): string => {
|
|
925
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
const emitPermissionRequestEvent = (event: PermissionRequestEvent): void => {
|
|
929
|
+
try {
|
|
930
|
+
pi.events.emit(PERMISSION_REQUEST_EVENT_CHANNEL, event);
|
|
931
|
+
} catch (error) {
|
|
932
|
+
writeDebugLog("permission_request.event_emit_failed", {
|
|
933
|
+
requestId: event.requestId,
|
|
934
|
+
source: event.source,
|
|
935
|
+
state: event.state,
|
|
936
|
+
error: formatUnknownErrorMessage(error),
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
const reviewPermissionDecision = (
|
|
942
|
+
event: string,
|
|
943
|
+
details: {
|
|
944
|
+
requestId: string;
|
|
945
|
+
source: PermissionRequestSource;
|
|
946
|
+
agentName: string | null;
|
|
947
|
+
message: string;
|
|
948
|
+
toolCallId?: string;
|
|
949
|
+
toolName?: string;
|
|
950
|
+
skillName?: string;
|
|
951
|
+
path?: string;
|
|
952
|
+
command?: string;
|
|
953
|
+
target?: string;
|
|
954
|
+
resolution?: string;
|
|
955
|
+
},
|
|
956
|
+
): void => {
|
|
957
|
+
writeReviewLog(event, {
|
|
958
|
+
requestId: details.requestId,
|
|
959
|
+
source: details.source,
|
|
960
|
+
agentName: details.agentName,
|
|
961
|
+
message: details.message,
|
|
962
|
+
toolCallId: details.toolCallId ?? null,
|
|
963
|
+
toolName: details.toolName ?? null,
|
|
964
|
+
skillName: details.skillName ?? null,
|
|
965
|
+
path: details.path ?? null,
|
|
966
|
+
command: details.command ?? null,
|
|
967
|
+
target: details.target ?? null,
|
|
968
|
+
resolution: details.resolution ?? null,
|
|
969
|
+
});
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
const promptPermission = async (
|
|
973
|
+
ctx: ExtensionContext,
|
|
974
|
+
details: {
|
|
975
|
+
requestId: string;
|
|
976
|
+
source: PermissionRequestSource;
|
|
977
|
+
agentName: string | null;
|
|
978
|
+
message: string;
|
|
979
|
+
toolCallId?: string;
|
|
980
|
+
toolName?: string;
|
|
981
|
+
skillName?: string;
|
|
982
|
+
path?: string;
|
|
983
|
+
command?: string;
|
|
984
|
+
target?: string;
|
|
985
|
+
},
|
|
986
|
+
): Promise<boolean> => {
|
|
987
|
+
reviewPermissionDecision("permission_request.waiting", details);
|
|
988
|
+
emitPermissionRequestEvent({
|
|
989
|
+
requestId: details.requestId,
|
|
990
|
+
source: details.source,
|
|
991
|
+
state: "waiting",
|
|
992
|
+
message: details.message,
|
|
993
|
+
toolCallId: details.toolCallId,
|
|
994
|
+
toolName: details.toolName,
|
|
995
|
+
skillName: details.skillName,
|
|
996
|
+
path: details.path,
|
|
997
|
+
command: details.command,
|
|
998
|
+
target: details.target,
|
|
999
|
+
agentName: details.agentName,
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
const approved = await confirmPermission(ctx, details.message);
|
|
1003
|
+
reviewPermissionDecision(approved ? "permission_request.approved" : "permission_request.denied", details);
|
|
1004
|
+
emitPermissionRequestEvent({
|
|
1005
|
+
requestId: details.requestId,
|
|
1006
|
+
source: details.source,
|
|
1007
|
+
state: approved ? "approved" : "denied",
|
|
1008
|
+
message: details.message,
|
|
1009
|
+
toolCallId: details.toolCallId,
|
|
1010
|
+
toolName: details.toolName,
|
|
1011
|
+
skillName: details.skillName,
|
|
1012
|
+
path: details.path,
|
|
1013
|
+
command: details.command,
|
|
1014
|
+
target: details.target,
|
|
1015
|
+
agentName: details.agentName,
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
return approved;
|
|
1019
|
+
};
|
|
791
1020
|
|
|
792
1021
|
const stopForwardedPermissionPolling = (): void => {
|
|
793
1022
|
if (permissionForwardingTimer) {
|
|
@@ -840,10 +1069,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
840
1069
|
};
|
|
841
1070
|
|
|
842
1071
|
const shouldExposeTool = (toolName: string, agentName: string | null): boolean => {
|
|
843
|
-
if (toolName === DELEGATION_TOOL_NAME && !isDelegationAllowedAgent(agentName)) {
|
|
844
|
-
return false;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
1072
|
// Use tool-level permission check for tool injection decisions
|
|
848
1073
|
// This ensures that agent-specific tool deny rules (e.g., bash: deny) are respected
|
|
849
1074
|
// before any command-level permissions are considered
|
|
@@ -852,6 +1077,8 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
852
1077
|
};
|
|
853
1078
|
|
|
854
1079
|
pi.on("session_start", async (_event, ctx) => {
|
|
1080
|
+
runtimeContext = ctx;
|
|
1081
|
+
refreshExtensionConfig(ctx);
|
|
855
1082
|
permissionManager = new PermissionManager();
|
|
856
1083
|
activeSkillEntries = [];
|
|
857
1084
|
lastKnownActiveAgentName = getActiveAgentName(ctx);
|
|
@@ -859,16 +1086,21 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
859
1086
|
});
|
|
860
1087
|
|
|
861
1088
|
pi.on("session_switch", async (_event, ctx) => {
|
|
1089
|
+
runtimeContext = ctx;
|
|
1090
|
+
refreshExtensionConfig(ctx);
|
|
862
1091
|
activeSkillEntries = [];
|
|
863
1092
|
lastKnownActiveAgentName = getActiveAgentName(ctx);
|
|
864
1093
|
startForwardedPermissionPolling(ctx);
|
|
865
1094
|
});
|
|
866
1095
|
|
|
867
1096
|
pi.on("session_shutdown", async () => {
|
|
1097
|
+
runtimeContext = null;
|
|
868
1098
|
stopForwardedPermissionPolling();
|
|
869
1099
|
});
|
|
870
1100
|
|
|
871
1101
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
1102
|
+
runtimeContext = ctx;
|
|
1103
|
+
refreshExtensionConfig(ctx);
|
|
872
1104
|
startForwardedPermissionPolling(ctx);
|
|
873
1105
|
const agentName = resolveAgentName(ctx, event.systemPrompt);
|
|
874
1106
|
const allTools = pi.getAllTools();
|
|
@@ -899,6 +1131,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
899
1131
|
});
|
|
900
1132
|
|
|
901
1133
|
pi.on("input", async (event, ctx) => {
|
|
1134
|
+
runtimeContext = ctx;
|
|
902
1135
|
startForwardedPermissionPolling(ctx);
|
|
903
1136
|
const skillName = extractSkillNameFromInput(event.text);
|
|
904
1137
|
if (!skillName) {
|
|
@@ -911,6 +1144,12 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
911
1144
|
if (ctx.hasUI) {
|
|
912
1145
|
ctx.ui.notify(`Skill '${skillName}' is blocked because active agent context is unavailable.`, "warning");
|
|
913
1146
|
}
|
|
1147
|
+
writeReviewLog("permission_request.blocked", {
|
|
1148
|
+
source: "skill_input",
|
|
1149
|
+
skillName,
|
|
1150
|
+
agentName: null,
|
|
1151
|
+
resolution: "missing_agent_context",
|
|
1152
|
+
});
|
|
914
1153
|
return { action: "handled" };
|
|
915
1154
|
}
|
|
916
1155
|
|
|
@@ -921,15 +1160,35 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
921
1160
|
const resolvedAgent = agentName ?? "none";
|
|
922
1161
|
ctx.ui.notify(`Skill '${skillName}' is not permitted for agent '${resolvedAgent}'.`, "warning");
|
|
923
1162
|
}
|
|
1163
|
+
writeReviewLog("permission_request.blocked", {
|
|
1164
|
+
source: "skill_input",
|
|
1165
|
+
skillName,
|
|
1166
|
+
agentName,
|
|
1167
|
+
resolution: "policy_denied",
|
|
1168
|
+
});
|
|
924
1169
|
return { action: "handled" };
|
|
925
1170
|
}
|
|
926
1171
|
|
|
927
1172
|
if (check.state === "ask") {
|
|
1173
|
+
const message = formatSkillAskPrompt(skillName, agentName ?? undefined);
|
|
928
1174
|
if (!canRequestPermissionConfirmation(ctx)) {
|
|
1175
|
+
writeReviewLog("permission_request.blocked", {
|
|
1176
|
+
source: "skill_input",
|
|
1177
|
+
skillName,
|
|
1178
|
+
agentName,
|
|
1179
|
+
message,
|
|
1180
|
+
resolution: "confirmation_unavailable",
|
|
1181
|
+
});
|
|
929
1182
|
return { action: "handled" };
|
|
930
1183
|
}
|
|
931
1184
|
|
|
932
|
-
const approved = await
|
|
1185
|
+
const approved = await promptPermission(ctx, {
|
|
1186
|
+
requestId: createPermissionRequestId("skill-input"),
|
|
1187
|
+
source: "skill_input",
|
|
1188
|
+
agentName,
|
|
1189
|
+
message,
|
|
1190
|
+
skillName,
|
|
1191
|
+
});
|
|
933
1192
|
if (!approved) {
|
|
934
1193
|
return { action: "handled" };
|
|
935
1194
|
}
|
|
@@ -939,6 +1198,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
939
1198
|
});
|
|
940
1199
|
|
|
941
1200
|
pi.on("tool_call", async (event, ctx) => {
|
|
1201
|
+
runtimeContext = ctx;
|
|
942
1202
|
startForwardedPermissionPolling(ctx);
|
|
943
1203
|
const agentName = resolveAgentName(ctx);
|
|
944
1204
|
const toolName = getEventToolName(event);
|
|
@@ -959,16 +1219,19 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
959
1219
|
};
|
|
960
1220
|
}
|
|
961
1221
|
|
|
962
|
-
if (toolName === DELEGATION_TOOL_NAME && !isDelegationAllowedAgent(agentName)) {
|
|
963
|
-
return { block: true, reason: getDelegationBlockReason(agentName) };
|
|
964
|
-
}
|
|
965
|
-
|
|
966
1222
|
if (isToolCallEventType("read", event) && activeSkillEntries.length > 0) {
|
|
967
1223
|
const normalizedReadPath = normalizePathForComparison(event.input.path, ctx.cwd);
|
|
968
1224
|
const matchedSkill = findSkillPathMatch(normalizedReadPath, activeSkillEntries);
|
|
969
1225
|
|
|
970
1226
|
if (matchedSkill) {
|
|
971
1227
|
if (matchedSkill.state === "deny") {
|
|
1228
|
+
writeReviewLog("permission_request.blocked", {
|
|
1229
|
+
source: "skill_read",
|
|
1230
|
+
skillName: matchedSkill.name,
|
|
1231
|
+
agentName,
|
|
1232
|
+
path: event.input.path,
|
|
1233
|
+
resolution: "policy_denied",
|
|
1234
|
+
});
|
|
972
1235
|
return {
|
|
973
1236
|
block: true,
|
|
974
1237
|
reason: formatSkillPathDenyReason(matchedSkill, event.input.path, agentName ?? undefined),
|
|
@@ -976,17 +1239,32 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
976
1239
|
}
|
|
977
1240
|
|
|
978
1241
|
if (matchedSkill.state === "ask") {
|
|
1242
|
+
const message = formatSkillPathAskPrompt(matchedSkill, event.input.path, agentName ?? undefined);
|
|
979
1243
|
if (!canRequestPermissionConfirmation(ctx)) {
|
|
1244
|
+
writeReviewLog("permission_request.blocked", {
|
|
1245
|
+
source: "skill_read",
|
|
1246
|
+
skillName: matchedSkill.name,
|
|
1247
|
+
agentName,
|
|
1248
|
+
path: event.input.path,
|
|
1249
|
+
message,
|
|
1250
|
+
resolution: "confirmation_unavailable",
|
|
1251
|
+
});
|
|
980
1252
|
return {
|
|
981
1253
|
block: true,
|
|
982
1254
|
reason: `Accessing skill '${matchedSkill.name}' requires approval, but no interactive UI is available.`,
|
|
983
1255
|
};
|
|
984
1256
|
}
|
|
985
1257
|
|
|
986
|
-
const approved = await
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1258
|
+
const approved = await promptPermission(ctx, {
|
|
1259
|
+
requestId: event.toolCallId,
|
|
1260
|
+
source: "skill_read",
|
|
1261
|
+
agentName,
|
|
1262
|
+
message,
|
|
1263
|
+
toolCallId: event.toolCallId,
|
|
1264
|
+
toolName: toolName,
|
|
1265
|
+
skillName: matchedSkill.name,
|
|
1266
|
+
path: event.input.path,
|
|
1267
|
+
});
|
|
990
1268
|
if (!approved) {
|
|
991
1269
|
return { block: true, reason: `User denied access to skill '${matchedSkill.name}'.` };
|
|
992
1270
|
}
|
|
@@ -996,8 +1274,17 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
996
1274
|
|
|
997
1275
|
const input = getEventInput(event);
|
|
998
1276
|
const check = permissionManager.checkPermission(toolName, input, agentName ?? undefined);
|
|
1277
|
+
const permissionLogContext = getPermissionLogContext(check);
|
|
999
1278
|
|
|
1000
1279
|
if (check.state === "deny") {
|
|
1280
|
+
writeReviewLog("permission_request.blocked", {
|
|
1281
|
+
source: "tool_call",
|
|
1282
|
+
toolCallId: event.toolCallId,
|
|
1283
|
+
toolName,
|
|
1284
|
+
agentName,
|
|
1285
|
+
...permissionLogContext,
|
|
1286
|
+
resolution: "policy_denied",
|
|
1287
|
+
});
|
|
1001
1288
|
return { block: true, reason: formatDenyReason(check, agentName ?? undefined) };
|
|
1002
1289
|
}
|
|
1003
1290
|
|
|
@@ -1008,14 +1295,32 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1008
1295
|
? "Using tool 'mcp' requires approval, but no interactive UI is available."
|
|
1009
1296
|
: `Using tool '${toolName}' requires approval, but no interactive UI is available.`;
|
|
1010
1297
|
|
|
1298
|
+
const message = formatAskPrompt(check, agentName ?? undefined);
|
|
1011
1299
|
if (!canRequestPermissionConfirmation(ctx)) {
|
|
1300
|
+
writeReviewLog("permission_request.blocked", {
|
|
1301
|
+
source: "tool_call",
|
|
1302
|
+
toolCallId: event.toolCallId,
|
|
1303
|
+
toolName,
|
|
1304
|
+
agentName,
|
|
1305
|
+
message,
|
|
1306
|
+
...permissionLogContext,
|
|
1307
|
+
resolution: "confirmation_unavailable",
|
|
1308
|
+
});
|
|
1012
1309
|
return {
|
|
1013
1310
|
block: true,
|
|
1014
1311
|
reason: unavailableReason,
|
|
1015
1312
|
};
|
|
1016
1313
|
}
|
|
1017
1314
|
|
|
1018
|
-
const approved = await
|
|
1315
|
+
const approved = await promptPermission(ctx, {
|
|
1316
|
+
requestId: event.toolCallId,
|
|
1317
|
+
source: "tool_call",
|
|
1318
|
+
agentName,
|
|
1319
|
+
message,
|
|
1320
|
+
toolCallId: event.toolCallId,
|
|
1321
|
+
toolName,
|
|
1322
|
+
...permissionLogContext,
|
|
1323
|
+
});
|
|
1019
1324
|
if (!approved) {
|
|
1020
1325
|
return { block: true, reason: formatUserDeniedReason(check) };
|
|
1021
1326
|
}
|
package/src/logging.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEBUG_LOG_PATH,
|
|
5
|
+
EXTENSION_ID,
|
|
6
|
+
LOGS_DIR,
|
|
7
|
+
PERMISSION_REVIEW_LOG_PATH,
|
|
8
|
+
ensurePermissionSystemLogsDirectory,
|
|
9
|
+
type PermissionSystemExtensionConfig,
|
|
10
|
+
} from "./extension-config.js";
|
|
11
|
+
|
|
12
|
+
function safeJsonStringify(value: unknown): string {
|
|
13
|
+
const seen = new WeakSet<object>();
|
|
14
|
+
return JSON.stringify(value, (_key, currentValue) => {
|
|
15
|
+
if (currentValue instanceof Error) {
|
|
16
|
+
return {
|
|
17
|
+
name: currentValue.name,
|
|
18
|
+
message: currentValue.message,
|
|
19
|
+
stack: currentValue.stack,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof currentValue === "bigint") {
|
|
24
|
+
return currentValue.toString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof currentValue === "object" && currentValue !== null) {
|
|
28
|
+
if (seen.has(currentValue)) {
|
|
29
|
+
return "[Circular]";
|
|
30
|
+
}
|
|
31
|
+
seen.add(currentValue);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return currentValue;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PermissionSystemLogger {
|
|
39
|
+
debug: (event: string, details?: Record<string, unknown>) => string | undefined;
|
|
40
|
+
review: (event: string, details?: Record<string, unknown>) => string | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface PermissionSystemLoggerOptions {
|
|
44
|
+
getConfig: () => PermissionSystemExtensionConfig;
|
|
45
|
+
debugLogPath?: string;
|
|
46
|
+
reviewLogPath?: string;
|
|
47
|
+
ensureLogsDirectory?: () => string | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createPermissionSystemLogger(options: PermissionSystemLoggerOptions): PermissionSystemLogger {
|
|
51
|
+
const debugLogPath = options.debugLogPath ?? DEBUG_LOG_PATH;
|
|
52
|
+
const reviewLogPath = options.reviewLogPath ?? PERMISSION_REVIEW_LOG_PATH;
|
|
53
|
+
const ensureLogsDirectory = options.ensureLogsDirectory ?? (() => ensurePermissionSystemLogsDirectory(LOGS_DIR));
|
|
54
|
+
|
|
55
|
+
const writeLine = (stream: "debug" | "review", path: string, event: string, details: Record<string, unknown>): string | undefined => {
|
|
56
|
+
const directoryError = ensureLogsDirectory();
|
|
57
|
+
if (directoryError) {
|
|
58
|
+
return directoryError;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const line = safeJsonStringify({
|
|
63
|
+
timestamp: new Date().toISOString(),
|
|
64
|
+
extension: EXTENSION_ID,
|
|
65
|
+
stream,
|
|
66
|
+
event,
|
|
67
|
+
...details,
|
|
68
|
+
});
|
|
69
|
+
appendFileSync(path, `${line}\n`, "utf-8");
|
|
70
|
+
return undefined;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
return `Failed to write permission-system ${stream} log '${path}': ${message}`;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const debug = (event: string, details: Record<string, unknown> = {}): string | undefined => {
|
|
78
|
+
if (!options.getConfig().debugLog) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return writeLine("debug", debugLogPath, event, details);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const review = (event: string, details: Record<string, unknown> = {}): string | undefined => {
|
|
86
|
+
if (!options.getConfig().permissionReviewLog) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return writeLine("review", reviewLogPath, event, details);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return { debug, review };
|
|
94
|
+
}
|