pi-permission-system 0.2.0 → 0.2.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 +15 -0
- package/README.md +39 -4
- package/config.json +4 -0
- package/package.json +60 -59
- package/src/extension-config.ts +106 -0
- package/src/index.ts +340 -16
- package/src/logging.ts +94 -0
- package/src/test.ts +58 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ 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
|
+
## [0.2.1] - 2026-03-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Extension configuration system (`config.json`) with `debugLog` and `permissionReviewLog` options
|
|
12
|
+
- JSONL debug logging to `logs/pi-permission-system-debug.jsonl` when `debugLog` is enabled
|
|
13
|
+
- JSONL permission review logging to `logs/pi-permission-system-permission-review.jsonl` for auditing
|
|
14
|
+
- Permission request event emission on `pi-permission-system:permission-request` channel for external consumers
|
|
15
|
+
- New `extension-config.ts` module for config file management and path resolution
|
|
16
|
+
- New `logging.ts` module with `createPermissionSystemLogger` for structured log output
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Replaced `console.warn`/`console.error` calls with structured logging to file
|
|
20
|
+
- Permission forwarding now logs request creation, response received, timeout, and user prompts
|
|
21
|
+
- Updated README documentation to cover extension config, logging, and event emission
|
|
22
|
+
|
|
8
23
|
## [0.2.0] - 2026-03-12
|
|
9
24
|
|
|
10
25
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🔐 pi-permission-system
|
|
2
2
|
|
|
3
|
-
[](package.json)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
6
|
Permission enforcement extension for the Pi coding agent that provides centralized, deterministic permission gates for tool, bash, MCP, skill, and special operations.
|
|
@@ -17,6 +17,8 @@ Permission enforcement extension for the Pi coding agent that provides centraliz
|
|
|
17
17
|
- **Skill Protection** — Controls which skills can be loaded or read from disk
|
|
18
18
|
- **Per-Agent Overrides** — Agent-specific permission policies via YAML frontmatter
|
|
19
19
|
- **Subagent Permission Forwarding** — Forwards `ask` confirmations from non-UI subagents back to the main interactive session
|
|
20
|
+
- **File-Based Review Logging** — Writes permission request/denial review entries to a file by default for later auditing
|
|
21
|
+
- **Optional Debug Logging** — Keeps verbose extension diagnostics in a separate file when enabled in `config.json`
|
|
20
22
|
- **JSON Schema Validation** — Full schema for editor autocomplete and config validation
|
|
21
23
|
|
|
22
24
|
## Installation
|
|
@@ -83,6 +85,26 @@ The extension integrates via Pi's lifecycle hooks:
|
|
|
83
85
|
|
|
84
86
|
## Configuration
|
|
85
87
|
|
|
88
|
+
### Extension Config File
|
|
89
|
+
|
|
90
|
+
**Location:** `~/.pi/agent/extensions/pi-permission-system/config.json`
|
|
91
|
+
|
|
92
|
+
The extension creates this file automatically when it is missing. It controls only extension-local logging behavior:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"debugLog": false,
|
|
97
|
+
"permissionReviewLog": true
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Key | Default | Description |
|
|
102
|
+
|-----|---------|-------------|
|
|
103
|
+
| `debugLog` | `false` | Enables verbose diagnostic logging to `logs/pi-permission-system-debug.jsonl` |
|
|
104
|
+
| `permissionReviewLog` | `true` | Enables the permission request/denial review log at `logs/pi-permission-system-permission-review.jsonl` |
|
|
105
|
+
|
|
106
|
+
Both logs write to files only under the extension directory. No debug output is printed to the terminal.
|
|
107
|
+
|
|
86
108
|
### Global Policy File
|
|
87
109
|
|
|
88
110
|
**Location:** `~/.pi/agent/pi-permissions.jsonc`
|
|
@@ -352,12 +374,25 @@ When a delegated or routed subagent runs without direct UI access, `ask` permiss
|
|
|
352
374
|
|
|
353
375
|
This keeps `ask` policies usable even when the original permission check happens inside a non-UI execution context.
|
|
354
376
|
|
|
377
|
+
### Logging
|
|
378
|
+
|
|
379
|
+
When the extension prompts, denies, or forwards permission requests, it can append structured JSONL entries under:
|
|
380
|
+
|
|
381
|
+
```text
|
|
382
|
+
~/.pi/agent/extensions/pi-permission-system/logs/
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
- `pi-permission-system-permission-review.jsonl` — enabled by default for permission review/audit history
|
|
386
|
+
- `pi-permission-system-debug.jsonl` — disabled by default and intended for troubleshooting
|
|
387
|
+
|
|
355
388
|
### Architecture
|
|
356
389
|
|
|
357
390
|
```
|
|
358
391
|
index.ts → Root Pi entrypoint shim
|
|
359
392
|
src/
|
|
360
|
-
├── index.ts → Extension bootstrap, permission checks, and subagent forwarding
|
|
393
|
+
├── index.ts → Extension bootstrap, permission checks, review logging, and subagent forwarding
|
|
394
|
+
├── extension-config.ts → Extension-local config loading and default creation
|
|
395
|
+
├── logging.ts → File-only debug/review logging helpers
|
|
361
396
|
├── permission-manager.ts → Policy loading, merging, and resolution with caching
|
|
362
397
|
├── bash-filter.ts → Bash command wildcard pattern matching
|
|
363
398
|
├── wildcard-matcher.ts → Shared wildcard pattern compilation and matching
|
|
@@ -366,9 +401,9 @@ src/
|
|
|
366
401
|
├── types.ts → TypeScript type definitions
|
|
367
402
|
└── test.ts → Test runner
|
|
368
403
|
schemas/
|
|
369
|
-
└── permissions.schema.json → JSON Schema for
|
|
404
|
+
└── permissions.schema.json → JSON Schema for policy validation
|
|
370
405
|
config/
|
|
371
|
-
└── config.example.json → Starter
|
|
406
|
+
└── config.example.json → Starter global policy template
|
|
372
407
|
```
|
|
373
408
|
|
|
374
409
|
#### Module Organization
|
package/config.json
ADDED
package/package.json
CHANGED
|
@@ -1,59 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-permission-system",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./index.ts",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./index.ts"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"index.ts",
|
|
12
|
-
"src",
|
|
13
|
-
"config
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"pi",
|
|
29
|
-
"pi
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"@
|
|
58
|
-
|
|
59
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-permission-system",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"src",
|
|
13
|
+
"config.json",
|
|
14
|
+
"config/config.example.json",
|
|
15
|
+
"schemas/permissions.schema.json",
|
|
16
|
+
"asset",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
|
|
23
|
+
"lint": "npm run build",
|
|
24
|
+
"test": "bun ./src/test.ts",
|
|
25
|
+
"check": "npm run lint && npm run test"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"pi-package",
|
|
29
|
+
"pi",
|
|
30
|
+
"pi-extension",
|
|
31
|
+
"permissions",
|
|
32
|
+
"policy",
|
|
33
|
+
"coding-agent"
|
|
34
|
+
],
|
|
35
|
+
"author": "MasuRii",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/MasuRii/pi-permission-system.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/MasuRii/pi-permission-system#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/MasuRii/pi-permission-system/issues"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"pi": {
|
|
52
|
+
"extensions": [
|
|
53
|
+
"./index.ts"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
58
|
+
"@sinclair/typebox": "*"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { toRecord } from "./common.js";
|
|
6
|
+
|
|
7
|
+
export const EXTENSION_ID = "pi-permission-system";
|
|
8
|
+
|
|
9
|
+
export interface PermissionSystemExtensionConfig {
|
|
10
|
+
debugLog: boolean;
|
|
11
|
+
permissionReviewLog: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PermissionSystemConfigLoadResult {
|
|
15
|
+
config: PermissionSystemExtensionConfig;
|
|
16
|
+
created: boolean;
|
|
17
|
+
warning?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_EXTENSION_CONFIG: PermissionSystemExtensionConfig = {
|
|
21
|
+
debugLog: false,
|
|
22
|
+
permissionReviewLog: true,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function resolveExtensionRoot(moduleUrl = import.meta.url): string {
|
|
26
|
+
return join(dirname(fileURLToPath(moduleUrl)), "..");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const EXTENSION_ROOT = resolveExtensionRoot();
|
|
30
|
+
export const CONFIG_PATH = join(EXTENSION_ROOT, "config.json");
|
|
31
|
+
export const LOGS_DIR = join(EXTENSION_ROOT, "logs");
|
|
32
|
+
export const DEBUG_LOG_PATH = join(LOGS_DIR, `${EXTENSION_ID}-debug.jsonl`);
|
|
33
|
+
export const PERMISSION_REVIEW_LOG_PATH = join(LOGS_DIR, `${EXTENSION_ID}-permission-review.jsonl`);
|
|
34
|
+
|
|
35
|
+
function cloneDefaultConfig(): PermissionSystemExtensionConfig {
|
|
36
|
+
return {
|
|
37
|
+
debugLog: DEFAULT_EXTENSION_CONFIG.debugLog,
|
|
38
|
+
permissionReviewLog: DEFAULT_EXTENSION_CONFIG.permissionReviewLog,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createDefaultConfigContent(): string {
|
|
43
|
+
return `${JSON.stringify(DEFAULT_EXTENSION_CONFIG, null, 2)}\n`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeConfig(raw: unknown): PermissionSystemExtensionConfig {
|
|
47
|
+
const record = toRecord(raw);
|
|
48
|
+
return {
|
|
49
|
+
debugLog: record.debugLog === true,
|
|
50
|
+
permissionReviewLog: record.permissionReviewLog !== false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ensureConfigDirectory(configPath: string): void {
|
|
55
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function ensurePermissionSystemConfig(configPath = CONFIG_PATH): { created: boolean; warning?: string } {
|
|
59
|
+
if (existsSync(configPath)) {
|
|
60
|
+
return { created: false };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
ensureConfigDirectory(configPath);
|
|
65
|
+
writeFileSync(configPath, createDefaultConfigContent(), "utf-8");
|
|
66
|
+
return { created: true };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
return {
|
|
70
|
+
created: false,
|
|
71
|
+
warning: `Failed to initialize permission-system config at '${configPath}': ${message}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function loadPermissionSystemConfig(configPath = CONFIG_PATH): PermissionSystemConfigLoadResult {
|
|
77
|
+
const ensureResult = ensurePermissionSystemConfig(configPath);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
81
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
82
|
+
const config = normalizeConfig(parsed);
|
|
83
|
+
return {
|
|
84
|
+
config,
|
|
85
|
+
created: ensureResult.created,
|
|
86
|
+
warning: ensureResult.warning,
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
return {
|
|
91
|
+
config: cloneDefaultConfig(),
|
|
92
|
+
created: ensureResult.created,
|
|
93
|
+
warning: ensureResult.warning ?? `Failed to read permission-system config at '${configPath}': ${message}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function ensurePermissionSystemLogsDirectory(logsDir = LOGS_DIR): string | undefined {
|
|
99
|
+
try {
|
|
100
|
+
mkdirSync(logsDir, { recursive: true });
|
|
101
|
+
return undefined;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
104
|
+
return `Failed to create permission-system log directory '${logsDir}': ${message}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
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";
|
|
@@ -67,6 +73,66 @@ type PermissionForwardingLocation = {
|
|
|
67
73
|
label: "primary" | "legacy";
|
|
68
74
|
};
|
|
69
75
|
|
|
76
|
+
type PermissionRequestSource = "tool_call" | "skill_input" | "skill_read";
|
|
77
|
+
type PermissionRequestState = "waiting" | "approved" | "denied";
|
|
78
|
+
|
|
79
|
+
type PermissionRequestEvent = {
|
|
80
|
+
requestId: string;
|
|
81
|
+
source: PermissionRequestSource;
|
|
82
|
+
state: PermissionRequestState;
|
|
83
|
+
message: string;
|
|
84
|
+
toolCallId?: string;
|
|
85
|
+
toolName?: string;
|
|
86
|
+
skillName?: string;
|
|
87
|
+
path?: string;
|
|
88
|
+
command?: string;
|
|
89
|
+
target?: string;
|
|
90
|
+
agentName?: string | null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const PERMISSION_REQUEST_EVENT_CHANNEL = "pi-permission-system:permission-request";
|
|
94
|
+
|
|
95
|
+
let extensionConfig: PermissionSystemExtensionConfig = { ...DEFAULT_EXTENSION_CONFIG };
|
|
96
|
+
const extensionLogger = createPermissionSystemLogger({
|
|
97
|
+
getConfig: () => extensionConfig,
|
|
98
|
+
});
|
|
99
|
+
const reportedLoggingWarnings = new Set<string>();
|
|
100
|
+
let loggingWarningReporter: ((message: string) => void) | null = null;
|
|
101
|
+
|
|
102
|
+
function setExtensionConfig(config: PermissionSystemExtensionConfig): void {
|
|
103
|
+
extensionConfig = {
|
|
104
|
+
debugLog: config.debugLog,
|
|
105
|
+
permissionReviewLog: config.permissionReviewLog,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function setLoggingWarningReporter(reporter: ((message: string) => void) | null): void {
|
|
110
|
+
loggingWarningReporter = reporter;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function reportLoggingWarning(message: string): void {
|
|
114
|
+
if (!loggingWarningReporter || reportedLoggingWarnings.has(message)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
reportedLoggingWarnings.add(message);
|
|
119
|
+
loggingWarningReporter(message);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function writeDebugLog(event: string, details: Record<string, unknown> = {}): void {
|
|
123
|
+
const warning = extensionLogger.debug(event, details);
|
|
124
|
+
if (warning) {
|
|
125
|
+
reportLoggingWarning(warning);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function writeReviewLog(event: string, details: Record<string, unknown> = {}): void {
|
|
130
|
+
const warning = extensionLogger.review(event, details);
|
|
131
|
+
if (warning) {
|
|
132
|
+
reportLoggingWarning(warning);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
70
136
|
function decodeXml(value: string): string {
|
|
71
137
|
return value
|
|
72
138
|
.replace(/</g, "<")
|
|
@@ -410,6 +476,13 @@ function formatSkillPathDenyReason(skill: SkillPromptEntry, readPath: string, ag
|
|
|
410
476
|
return `${subject} is not permitted to access skill '${skill.name}' via '${readPath}'.`;
|
|
411
477
|
}
|
|
412
478
|
|
|
479
|
+
function getPermissionLogContext(result: PermissionCheckResult): { command?: string; target?: string } {
|
|
480
|
+
return {
|
|
481
|
+
command: result.command,
|
|
482
|
+
target: result.target,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
413
486
|
function sleep(ms: number): Promise<void> {
|
|
414
487
|
return new Promise((resolve) => {
|
|
415
488
|
setTimeout(resolve, ms);
|
|
@@ -467,21 +540,21 @@ function isErrnoCode(error: unknown, code: string): boolean {
|
|
|
467
540
|
}
|
|
468
541
|
|
|
469
542
|
function logPermissionForwardingWarning(message: string, error?: unknown): void {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
543
|
+
const details = typeof error === "undefined"
|
|
544
|
+
? { message }
|
|
545
|
+
: { message, error: formatUnknownErrorMessage(error) };
|
|
474
546
|
|
|
475
|
-
|
|
547
|
+
writeReviewLog("permission_forwarding.warning", details);
|
|
548
|
+
writeDebugLog("permission_forwarding.warning", details);
|
|
476
549
|
}
|
|
477
550
|
|
|
478
551
|
function logPermissionForwardingError(message: string, error?: unknown): void {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
552
|
+
const details = typeof error === "undefined"
|
|
553
|
+
? { message }
|
|
554
|
+
: { message, error: formatUnknownErrorMessage(error) };
|
|
483
555
|
|
|
484
|
-
|
|
556
|
+
writeReviewLog("permission_forwarding.error", details);
|
|
557
|
+
writeDebugLog("permission_forwarding.error", details);
|
|
485
558
|
}
|
|
486
559
|
|
|
487
560
|
function ensureDirectoryExists(path: string, description: string): boolean {
|
|
@@ -685,6 +758,14 @@ async function waitForForwardedPermissionApproval(ctx: ExtensionContext, message
|
|
|
685
758
|
const requestPath = join(PERMISSION_FORWARDING_REQUESTS_DIR, `${requestId}.json`);
|
|
686
759
|
const responsePath = join(PERMISSION_FORWARDING_RESPONSES_DIR, `${requestId}.json`);
|
|
687
760
|
|
|
761
|
+
writeReviewLog("forwarded_permission.request_created", {
|
|
762
|
+
requestId,
|
|
763
|
+
requesterAgentName,
|
|
764
|
+
requesterSessionId: request.requesterSessionId,
|
|
765
|
+
requestPath,
|
|
766
|
+
responsePath,
|
|
767
|
+
});
|
|
768
|
+
|
|
688
769
|
try {
|
|
689
770
|
writeJsonFileAtomic(requestPath, request);
|
|
690
771
|
} catch (error) {
|
|
@@ -696,6 +777,12 @@ async function waitForForwardedPermissionApproval(ctx: ExtensionContext, message
|
|
|
696
777
|
while (Date.now() < deadline) {
|
|
697
778
|
if (existsSync(responsePath)) {
|
|
698
779
|
const response = readForwardedPermissionResponse(responsePath);
|
|
780
|
+
writeReviewLog("forwarded_permission.response_received", {
|
|
781
|
+
requestId,
|
|
782
|
+
approved: response?.approved ?? null,
|
|
783
|
+
responderSessionId: response?.responderSessionId ?? null,
|
|
784
|
+
responsePath,
|
|
785
|
+
});
|
|
699
786
|
safeDeleteFile(responsePath, "forwarded permission response");
|
|
700
787
|
safeDeleteFile(requestPath, "forwarded permission request");
|
|
701
788
|
return Boolean(response?.approved);
|
|
@@ -705,6 +792,11 @@ async function waitForForwardedPermissionApproval(ctx: ExtensionContext, message
|
|
|
705
792
|
}
|
|
706
793
|
|
|
707
794
|
logPermissionForwardingWarning(`Timed out waiting for forwarded permission response '${responsePath}'`);
|
|
795
|
+
writeReviewLog("forwarded_permission.response_timed_out", {
|
|
796
|
+
requestId,
|
|
797
|
+
requesterAgentName,
|
|
798
|
+
responsePath,
|
|
799
|
+
});
|
|
708
800
|
safeDeleteFile(requestPath, "forwarded permission request");
|
|
709
801
|
return false;
|
|
710
802
|
}
|
|
@@ -738,6 +830,14 @@ async function processForwardedPermissionRequests(ctx: ExtensionContext): Promis
|
|
|
738
830
|
continue;
|
|
739
831
|
}
|
|
740
832
|
|
|
833
|
+
writeReviewLog("forwarded_permission.prompted", {
|
|
834
|
+
requestId: request.id,
|
|
835
|
+
source: location.label,
|
|
836
|
+
requesterAgentName: request.requesterAgentName,
|
|
837
|
+
requesterSessionId: request.requesterSessionId,
|
|
838
|
+
requestPath,
|
|
839
|
+
});
|
|
840
|
+
|
|
741
841
|
let approved = false;
|
|
742
842
|
try {
|
|
743
843
|
approved = await ctx.ui.confirm("Permission Required (Subagent)", formatForwardedPermissionPrompt(request));
|
|
@@ -751,6 +851,13 @@ async function processForwardedPermissionRequests(ctx: ExtensionContext): Promis
|
|
|
751
851
|
}
|
|
752
852
|
|
|
753
853
|
const responsePath = join(location.responsesDir, `${request.id}.json`);
|
|
854
|
+
writeReviewLog(approved ? "forwarded_permission.approved" : "forwarded_permission.denied", {
|
|
855
|
+
requestId: request.id,
|
|
856
|
+
source: location.label,
|
|
857
|
+
requesterAgentName: request.requesterAgentName,
|
|
858
|
+
requesterSessionId: request.requesterSessionId,
|
|
859
|
+
responsePath,
|
|
860
|
+
});
|
|
754
861
|
try {
|
|
755
862
|
writeJsonFileAtomic(responsePath, {
|
|
756
863
|
approved,
|
|
@@ -788,6 +895,139 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
788
895
|
let permissionForwardingContext: ExtensionContext | null = null;
|
|
789
896
|
let permissionForwardingTimer: NodeJS.Timeout | null = null;
|
|
790
897
|
let isProcessingForwardedRequests = false;
|
|
898
|
+
let runtimeContext: ExtensionContext | null = null;
|
|
899
|
+
let lastConfigWarning: string | null = null;
|
|
900
|
+
|
|
901
|
+
const notifyWarning = (message: string): void => {
|
|
902
|
+
if (!runtimeContext?.hasUI) {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
runtimeContext.ui.notify(message, "warning");
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
const refreshExtensionConfig = (ctx?: ExtensionContext): void => {
|
|
910
|
+
if (ctx) {
|
|
911
|
+
runtimeContext = ctx;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const result = loadPermissionSystemConfig();
|
|
915
|
+
setExtensionConfig(result.config);
|
|
916
|
+
|
|
917
|
+
if (result.warning && result.warning !== lastConfigWarning) {
|
|
918
|
+
lastConfigWarning = result.warning;
|
|
919
|
+
notifyWarning(result.warning);
|
|
920
|
+
} else if (!result.warning) {
|
|
921
|
+
lastConfigWarning = null;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
writeDebugLog("config.loaded", {
|
|
925
|
+
created: result.created,
|
|
926
|
+
warning: result.warning ?? null,
|
|
927
|
+
debugLog: result.config.debugLog,
|
|
928
|
+
permissionReviewLog: result.config.permissionReviewLog,
|
|
929
|
+
});
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
setLoggingWarningReporter(notifyWarning);
|
|
933
|
+
refreshExtensionConfig();
|
|
934
|
+
|
|
935
|
+
const createPermissionRequestId = (prefix: string): string => {
|
|
936
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
const emitPermissionRequestEvent = (event: PermissionRequestEvent): void => {
|
|
940
|
+
try {
|
|
941
|
+
pi.events.emit(PERMISSION_REQUEST_EVENT_CHANNEL, event);
|
|
942
|
+
} catch (error) {
|
|
943
|
+
writeDebugLog("permission_request.event_emit_failed", {
|
|
944
|
+
requestId: event.requestId,
|
|
945
|
+
source: event.source,
|
|
946
|
+
state: event.state,
|
|
947
|
+
error: formatUnknownErrorMessage(error),
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
const reviewPermissionDecision = (
|
|
953
|
+
event: string,
|
|
954
|
+
details: {
|
|
955
|
+
requestId: string;
|
|
956
|
+
source: PermissionRequestSource;
|
|
957
|
+
agentName: string | null;
|
|
958
|
+
message: string;
|
|
959
|
+
toolCallId?: string;
|
|
960
|
+
toolName?: string;
|
|
961
|
+
skillName?: string;
|
|
962
|
+
path?: string;
|
|
963
|
+
command?: string;
|
|
964
|
+
target?: string;
|
|
965
|
+
resolution?: string;
|
|
966
|
+
},
|
|
967
|
+
): void => {
|
|
968
|
+
writeReviewLog(event, {
|
|
969
|
+
requestId: details.requestId,
|
|
970
|
+
source: details.source,
|
|
971
|
+
agentName: details.agentName,
|
|
972
|
+
message: details.message,
|
|
973
|
+
toolCallId: details.toolCallId ?? null,
|
|
974
|
+
toolName: details.toolName ?? null,
|
|
975
|
+
skillName: details.skillName ?? null,
|
|
976
|
+
path: details.path ?? null,
|
|
977
|
+
command: details.command ?? null,
|
|
978
|
+
target: details.target ?? null,
|
|
979
|
+
resolution: details.resolution ?? null,
|
|
980
|
+
});
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
const promptPermission = async (
|
|
984
|
+
ctx: ExtensionContext,
|
|
985
|
+
details: {
|
|
986
|
+
requestId: string;
|
|
987
|
+
source: PermissionRequestSource;
|
|
988
|
+
agentName: string | null;
|
|
989
|
+
message: string;
|
|
990
|
+
toolCallId?: string;
|
|
991
|
+
toolName?: string;
|
|
992
|
+
skillName?: string;
|
|
993
|
+
path?: string;
|
|
994
|
+
command?: string;
|
|
995
|
+
target?: string;
|
|
996
|
+
},
|
|
997
|
+
): Promise<boolean> => {
|
|
998
|
+
reviewPermissionDecision("permission_request.waiting", details);
|
|
999
|
+
emitPermissionRequestEvent({
|
|
1000
|
+
requestId: details.requestId,
|
|
1001
|
+
source: details.source,
|
|
1002
|
+
state: "waiting",
|
|
1003
|
+
message: details.message,
|
|
1004
|
+
toolCallId: details.toolCallId,
|
|
1005
|
+
toolName: details.toolName,
|
|
1006
|
+
skillName: details.skillName,
|
|
1007
|
+
path: details.path,
|
|
1008
|
+
command: details.command,
|
|
1009
|
+
target: details.target,
|
|
1010
|
+
agentName: details.agentName,
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
const approved = await confirmPermission(ctx, details.message);
|
|
1014
|
+
reviewPermissionDecision(approved ? "permission_request.approved" : "permission_request.denied", details);
|
|
1015
|
+
emitPermissionRequestEvent({
|
|
1016
|
+
requestId: details.requestId,
|
|
1017
|
+
source: details.source,
|
|
1018
|
+
state: approved ? "approved" : "denied",
|
|
1019
|
+
message: details.message,
|
|
1020
|
+
toolCallId: details.toolCallId,
|
|
1021
|
+
toolName: details.toolName,
|
|
1022
|
+
skillName: details.skillName,
|
|
1023
|
+
path: details.path,
|
|
1024
|
+
command: details.command,
|
|
1025
|
+
target: details.target,
|
|
1026
|
+
agentName: details.agentName,
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
return approved;
|
|
1030
|
+
};
|
|
791
1031
|
|
|
792
1032
|
const stopForwardedPermissionPolling = (): void => {
|
|
793
1033
|
if (permissionForwardingTimer) {
|
|
@@ -852,6 +1092,8 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
852
1092
|
};
|
|
853
1093
|
|
|
854
1094
|
pi.on("session_start", async (_event, ctx) => {
|
|
1095
|
+
runtimeContext = ctx;
|
|
1096
|
+
refreshExtensionConfig(ctx);
|
|
855
1097
|
permissionManager = new PermissionManager();
|
|
856
1098
|
activeSkillEntries = [];
|
|
857
1099
|
lastKnownActiveAgentName = getActiveAgentName(ctx);
|
|
@@ -859,16 +1101,21 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
859
1101
|
});
|
|
860
1102
|
|
|
861
1103
|
pi.on("session_switch", async (_event, ctx) => {
|
|
1104
|
+
runtimeContext = ctx;
|
|
1105
|
+
refreshExtensionConfig(ctx);
|
|
862
1106
|
activeSkillEntries = [];
|
|
863
1107
|
lastKnownActiveAgentName = getActiveAgentName(ctx);
|
|
864
1108
|
startForwardedPermissionPolling(ctx);
|
|
865
1109
|
});
|
|
866
1110
|
|
|
867
1111
|
pi.on("session_shutdown", async () => {
|
|
1112
|
+
runtimeContext = null;
|
|
868
1113
|
stopForwardedPermissionPolling();
|
|
869
1114
|
});
|
|
870
1115
|
|
|
871
1116
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
1117
|
+
runtimeContext = ctx;
|
|
1118
|
+
refreshExtensionConfig(ctx);
|
|
872
1119
|
startForwardedPermissionPolling(ctx);
|
|
873
1120
|
const agentName = resolveAgentName(ctx, event.systemPrompt);
|
|
874
1121
|
const allTools = pi.getAllTools();
|
|
@@ -899,6 +1146,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
899
1146
|
});
|
|
900
1147
|
|
|
901
1148
|
pi.on("input", async (event, ctx) => {
|
|
1149
|
+
runtimeContext = ctx;
|
|
902
1150
|
startForwardedPermissionPolling(ctx);
|
|
903
1151
|
const skillName = extractSkillNameFromInput(event.text);
|
|
904
1152
|
if (!skillName) {
|
|
@@ -911,6 +1159,12 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
911
1159
|
if (ctx.hasUI) {
|
|
912
1160
|
ctx.ui.notify(`Skill '${skillName}' is blocked because active agent context is unavailable.`, "warning");
|
|
913
1161
|
}
|
|
1162
|
+
writeReviewLog("permission_request.blocked", {
|
|
1163
|
+
source: "skill_input",
|
|
1164
|
+
skillName,
|
|
1165
|
+
agentName: null,
|
|
1166
|
+
resolution: "missing_agent_context",
|
|
1167
|
+
});
|
|
914
1168
|
return { action: "handled" };
|
|
915
1169
|
}
|
|
916
1170
|
|
|
@@ -921,15 +1175,35 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
921
1175
|
const resolvedAgent = agentName ?? "none";
|
|
922
1176
|
ctx.ui.notify(`Skill '${skillName}' is not permitted for agent '${resolvedAgent}'.`, "warning");
|
|
923
1177
|
}
|
|
1178
|
+
writeReviewLog("permission_request.blocked", {
|
|
1179
|
+
source: "skill_input",
|
|
1180
|
+
skillName,
|
|
1181
|
+
agentName,
|
|
1182
|
+
resolution: "policy_denied",
|
|
1183
|
+
});
|
|
924
1184
|
return { action: "handled" };
|
|
925
1185
|
}
|
|
926
1186
|
|
|
927
1187
|
if (check.state === "ask") {
|
|
1188
|
+
const message = formatSkillAskPrompt(skillName, agentName ?? undefined);
|
|
928
1189
|
if (!canRequestPermissionConfirmation(ctx)) {
|
|
1190
|
+
writeReviewLog("permission_request.blocked", {
|
|
1191
|
+
source: "skill_input",
|
|
1192
|
+
skillName,
|
|
1193
|
+
agentName,
|
|
1194
|
+
message,
|
|
1195
|
+
resolution: "confirmation_unavailable",
|
|
1196
|
+
});
|
|
929
1197
|
return { action: "handled" };
|
|
930
1198
|
}
|
|
931
1199
|
|
|
932
|
-
const approved = await
|
|
1200
|
+
const approved = await promptPermission(ctx, {
|
|
1201
|
+
requestId: createPermissionRequestId("skill-input"),
|
|
1202
|
+
source: "skill_input",
|
|
1203
|
+
agentName,
|
|
1204
|
+
message,
|
|
1205
|
+
skillName,
|
|
1206
|
+
});
|
|
933
1207
|
if (!approved) {
|
|
934
1208
|
return { action: "handled" };
|
|
935
1209
|
}
|
|
@@ -939,6 +1213,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
939
1213
|
});
|
|
940
1214
|
|
|
941
1215
|
pi.on("tool_call", async (event, ctx) => {
|
|
1216
|
+
runtimeContext = ctx;
|
|
942
1217
|
startForwardedPermissionPolling(ctx);
|
|
943
1218
|
const agentName = resolveAgentName(ctx);
|
|
944
1219
|
const toolName = getEventToolName(event);
|
|
@@ -969,6 +1244,13 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
969
1244
|
|
|
970
1245
|
if (matchedSkill) {
|
|
971
1246
|
if (matchedSkill.state === "deny") {
|
|
1247
|
+
writeReviewLog("permission_request.blocked", {
|
|
1248
|
+
source: "skill_read",
|
|
1249
|
+
skillName: matchedSkill.name,
|
|
1250
|
+
agentName,
|
|
1251
|
+
path: event.input.path,
|
|
1252
|
+
resolution: "policy_denied",
|
|
1253
|
+
});
|
|
972
1254
|
return {
|
|
973
1255
|
block: true,
|
|
974
1256
|
reason: formatSkillPathDenyReason(matchedSkill, event.input.path, agentName ?? undefined),
|
|
@@ -976,17 +1258,32 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
976
1258
|
}
|
|
977
1259
|
|
|
978
1260
|
if (matchedSkill.state === "ask") {
|
|
1261
|
+
const message = formatSkillPathAskPrompt(matchedSkill, event.input.path, agentName ?? undefined);
|
|
979
1262
|
if (!canRequestPermissionConfirmation(ctx)) {
|
|
1263
|
+
writeReviewLog("permission_request.blocked", {
|
|
1264
|
+
source: "skill_read",
|
|
1265
|
+
skillName: matchedSkill.name,
|
|
1266
|
+
agentName,
|
|
1267
|
+
path: event.input.path,
|
|
1268
|
+
message,
|
|
1269
|
+
resolution: "confirmation_unavailable",
|
|
1270
|
+
});
|
|
980
1271
|
return {
|
|
981
1272
|
block: true,
|
|
982
1273
|
reason: `Accessing skill '${matchedSkill.name}' requires approval, but no interactive UI is available.`,
|
|
983
1274
|
};
|
|
984
1275
|
}
|
|
985
1276
|
|
|
986
|
-
const approved = await
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1277
|
+
const approved = await promptPermission(ctx, {
|
|
1278
|
+
requestId: event.toolCallId,
|
|
1279
|
+
source: "skill_read",
|
|
1280
|
+
agentName,
|
|
1281
|
+
message,
|
|
1282
|
+
toolCallId: event.toolCallId,
|
|
1283
|
+
toolName: toolName,
|
|
1284
|
+
skillName: matchedSkill.name,
|
|
1285
|
+
path: event.input.path,
|
|
1286
|
+
});
|
|
990
1287
|
if (!approved) {
|
|
991
1288
|
return { block: true, reason: `User denied access to skill '${matchedSkill.name}'.` };
|
|
992
1289
|
}
|
|
@@ -996,8 +1293,17 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
996
1293
|
|
|
997
1294
|
const input = getEventInput(event);
|
|
998
1295
|
const check = permissionManager.checkPermission(toolName, input, agentName ?? undefined);
|
|
1296
|
+
const permissionLogContext = getPermissionLogContext(check);
|
|
999
1297
|
|
|
1000
1298
|
if (check.state === "deny") {
|
|
1299
|
+
writeReviewLog("permission_request.blocked", {
|
|
1300
|
+
source: "tool_call",
|
|
1301
|
+
toolCallId: event.toolCallId,
|
|
1302
|
+
toolName,
|
|
1303
|
+
agentName,
|
|
1304
|
+
...permissionLogContext,
|
|
1305
|
+
resolution: "policy_denied",
|
|
1306
|
+
});
|
|
1001
1307
|
return { block: true, reason: formatDenyReason(check, agentName ?? undefined) };
|
|
1002
1308
|
}
|
|
1003
1309
|
|
|
@@ -1008,14 +1314,32 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1008
1314
|
? "Using tool 'mcp' requires approval, but no interactive UI is available."
|
|
1009
1315
|
: `Using tool '${toolName}' requires approval, but no interactive UI is available.`;
|
|
1010
1316
|
|
|
1317
|
+
const message = formatAskPrompt(check, agentName ?? undefined);
|
|
1011
1318
|
if (!canRequestPermissionConfirmation(ctx)) {
|
|
1319
|
+
writeReviewLog("permission_request.blocked", {
|
|
1320
|
+
source: "tool_call",
|
|
1321
|
+
toolCallId: event.toolCallId,
|
|
1322
|
+
toolName,
|
|
1323
|
+
agentName,
|
|
1324
|
+
message,
|
|
1325
|
+
...permissionLogContext,
|
|
1326
|
+
resolution: "confirmation_unavailable",
|
|
1327
|
+
});
|
|
1012
1328
|
return {
|
|
1013
1329
|
block: true,
|
|
1014
1330
|
reason: unavailableReason,
|
|
1015
1331
|
};
|
|
1016
1332
|
}
|
|
1017
1333
|
|
|
1018
|
-
const approved = await
|
|
1334
|
+
const approved = await promptPermission(ctx, {
|
|
1335
|
+
requestId: event.toolCallId,
|
|
1336
|
+
source: "tool_call",
|
|
1337
|
+
agentName,
|
|
1338
|
+
message,
|
|
1339
|
+
toolCallId: event.toolCallId,
|
|
1340
|
+
toolName,
|
|
1341
|
+
...permissionLogContext,
|
|
1342
|
+
});
|
|
1019
1343
|
if (!approved) {
|
|
1020
1344
|
return { block: true, reason: formatUserDeniedReason(check) };
|
|
1021
1345
|
}
|
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
|
+
}
|
package/src/test.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
6
|
import { BashFilter } from "./bash-filter.js";
|
|
7
|
+
import { DEFAULT_EXTENSION_CONFIG, loadPermissionSystemConfig } from "./extension-config.js";
|
|
8
|
+
import { createPermissionSystemLogger } from "./logging.js";
|
|
7
9
|
import { PermissionManager } from "./permission-manager.js";
|
|
8
10
|
import { checkRequestedToolRegistration, getToolNameFromValue } from "./tool-registry.js";
|
|
9
11
|
import type { GlobalPermissionConfig } from "./types.js";
|
|
@@ -47,6 +49,61 @@ function runTest(name: string, testFn: () => void): void {
|
|
|
47
49
|
console.log(`[PASS] ${name}`);
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
runTest("Permission-system extension config defaults debug off and review log on", () => {
|
|
53
|
+
const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-config-"));
|
|
54
|
+
const configPath = join(baseDir, "config.json");
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = loadPermissionSystemConfig(configPath);
|
|
58
|
+
assert.equal(result.created, true);
|
|
59
|
+
assert.equal(result.warning, undefined);
|
|
60
|
+
assert.deepEqual(result.config, DEFAULT_EXTENSION_CONFIG);
|
|
61
|
+
assert.equal(existsSync(configPath), true);
|
|
62
|
+
|
|
63
|
+
const raw = JSON.parse(readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
64
|
+
assert.equal(raw.debugLog, false);
|
|
65
|
+
assert.equal(raw.permissionReviewLog, true);
|
|
66
|
+
} finally {
|
|
67
|
+
rmSync(baseDir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
runTest("Permission-system logger respects debug toggle and keeps review log enabled by default", () => {
|
|
72
|
+
const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-logs-"));
|
|
73
|
+
const logsDir = join(baseDir, "logs");
|
|
74
|
+
const debugLogPath = join(logsDir, "debug.jsonl");
|
|
75
|
+
const reviewLogPath = join(logsDir, "review.jsonl");
|
|
76
|
+
const config = { ...DEFAULT_EXTENSION_CONFIG };
|
|
77
|
+
const logger = createPermissionSystemLogger({
|
|
78
|
+
getConfig: () => config,
|
|
79
|
+
debugLogPath,
|
|
80
|
+
reviewLogPath,
|
|
81
|
+
ensureLogsDirectory: () => {
|
|
82
|
+
mkdirSync(logsDir, { recursive: true });
|
|
83
|
+
return undefined;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const initialDebugWarning = logger.debug("debug.disabled", { sample: true });
|
|
89
|
+
const reviewWarning = logger.review("permission_request.waiting", { toolName: "write" });
|
|
90
|
+
|
|
91
|
+
assert.equal(initialDebugWarning, undefined);
|
|
92
|
+
assert.equal(reviewWarning, undefined);
|
|
93
|
+
assert.equal(existsSync(debugLogPath), false);
|
|
94
|
+
assert.equal(existsSync(reviewLogPath), true);
|
|
95
|
+
assert.match(readFileSync(reviewLogPath, "utf8"), /permission_request\.waiting/);
|
|
96
|
+
|
|
97
|
+
config.debugLog = true;
|
|
98
|
+
const enabledDebugWarning = logger.debug("debug.enabled", { sample: true });
|
|
99
|
+
assert.equal(enabledDebugWarning, undefined);
|
|
100
|
+
assert.equal(existsSync(debugLogPath), true);
|
|
101
|
+
assert.match(readFileSync(debugLogPath, "utf8"), /debug\.enabled/);
|
|
102
|
+
} finally {
|
|
103
|
+
rmSync(baseDir, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
50
107
|
runTest("BashFilter uses opencode-style last-match hierarchy", () => {
|
|
51
108
|
const filter = new BashFilter(
|
|
52
109
|
{
|