@gotgenes/pi-permission-system 10.3.0 → 10.3.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 +7 -0
- package/package.json +1 -1
- package/src/config-store.ts +7 -23
- package/src/index.ts +65 -30
- package/src/permission-session.ts +4 -5
- package/src/permissions-service.ts +3 -5
- package/src/session-logger.ts +46 -9
- package/test/composition-root.test.ts +85 -1
- package/test/config-store.test.ts +16 -40
- package/test/permission-session.test.ts +14 -1
- package/test/session-logger.test.ts +151 -64
- package/src/runtime.ts +0 -147
- package/test/runtime.test.ts +0 -303
|
@@ -1,113 +1,200 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { existsSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { DEBUG_LOG_FILENAME, REVIEW_LOG_FILENAME } from "#src/config-paths";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_EXTENSION_CONFIG,
|
|
8
|
+
type PermissionSystemExtensionConfig,
|
|
9
|
+
} from "#src/extension-config";
|
|
10
|
+
import type { SessionLoggerDeps } from "#src/session-logger";
|
|
3
11
|
import { createSessionLogger } from "#src/session-logger";
|
|
4
12
|
|
|
5
13
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
)
|
|
15
|
+
let tempDir: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tempDir = mkdtempSync(join(tmpdir(), "ps-session-logger-"));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function makeDeps(
|
|
22
|
+
overrides: {
|
|
23
|
+
globalLogsDir?: string;
|
|
24
|
+
getConfig?: () => PermissionSystemExtensionConfig;
|
|
25
|
+
} = {},
|
|
26
|
+
) {
|
|
10
27
|
return {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
globalLogsDir: overrides.globalLogsDir ?? tempDir,
|
|
29
|
+
getConfig:
|
|
30
|
+
overrides.getConfig ??
|
|
31
|
+
((): PermissionSystemExtensionConfig => ({
|
|
32
|
+
...DEFAULT_EXTENSION_CONFIG,
|
|
33
|
+
})),
|
|
34
|
+
notify: vi.fn<(message: string) => void>(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A `globalLogsDir` that cannot be created: a file at the parent path blocks it. */
|
|
39
|
+
function makeBlockedLogsDir(): string {
|
|
40
|
+
const barrier = join(tempDir, "barrier");
|
|
41
|
+
writeFileSync(barrier, "");
|
|
42
|
+
return join(barrier, "logs");
|
|
16
43
|
}
|
|
17
44
|
|
|
18
45
|
// ── createSessionLogger ────────────────────────────────────────────────────
|
|
19
46
|
|
|
20
47
|
describe("createSessionLogger", () => {
|
|
48
|
+
// ── debug ────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
21
50
|
describe("debug", () => {
|
|
22
|
-
it("
|
|
23
|
-
const
|
|
24
|
-
|
|
51
|
+
it("writes a JSONL line to the debug log file when debugLog is true", () => {
|
|
52
|
+
const deps = makeDeps({
|
|
53
|
+
getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, debugLog: true }),
|
|
54
|
+
});
|
|
55
|
+
const logger = createSessionLogger(deps);
|
|
25
56
|
|
|
26
57
|
logger.debug("test.event", { key: "value" });
|
|
27
58
|
|
|
28
|
-
expect(
|
|
29
|
-
|
|
30
|
-
});
|
|
59
|
+
expect(existsSync(join(tempDir, DEBUG_LOG_FILENAME))).toBe(true);
|
|
60
|
+
expect(deps.notify).not.toHaveBeenCalled();
|
|
31
61
|
});
|
|
32
62
|
|
|
33
|
-
it("
|
|
34
|
-
|
|
35
|
-
const
|
|
63
|
+
it("does not write to the debug log when debugLog is false", () => {
|
|
64
|
+
// DEFAULT_EXTENSION_CONFIG.debugLog === false
|
|
65
|
+
const deps = makeDeps();
|
|
66
|
+
const logger = createSessionLogger(deps);
|
|
36
67
|
|
|
37
68
|
logger.debug("test.event");
|
|
38
69
|
|
|
39
|
-
expect(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
70
|
+
expect(existsSync(join(tempDir, DEBUG_LOG_FILENAME))).toBe(false);
|
|
71
|
+
expect(deps.notify).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("reads getConfig at write time — a mid-session toggle change takes effect", () => {
|
|
75
|
+
let debugLog = true;
|
|
76
|
+
const deps = makeDeps({
|
|
77
|
+
getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, debugLog }),
|
|
78
|
+
});
|
|
79
|
+
const logger = createSessionLogger(deps);
|
|
80
|
+
debugLog = false;
|
|
81
|
+
|
|
82
|
+
logger.debug("test.event");
|
|
83
|
+
|
|
84
|
+
expect(existsSync(join(tempDir, DEBUG_LOG_FILENAME))).toBe(false);
|
|
43
85
|
});
|
|
44
86
|
});
|
|
45
87
|
|
|
88
|
+
// ── review ───────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
46
90
|
describe("review", () => {
|
|
47
|
-
it("
|
|
48
|
-
|
|
49
|
-
const
|
|
91
|
+
it("writes a JSONL line to the review log file when permissionReviewLog is true", () => {
|
|
92
|
+
// DEFAULT_EXTENSION_CONFIG.permissionReviewLog === true
|
|
93
|
+
const deps = makeDeps();
|
|
94
|
+
const logger = createSessionLogger(deps);
|
|
50
95
|
|
|
51
96
|
logger.review("permission.granted", { agentName: "coder" });
|
|
52
97
|
|
|
53
|
-
expect(
|
|
54
|
-
|
|
55
|
-
{ agentName: "coder" },
|
|
56
|
-
);
|
|
98
|
+
expect(existsSync(join(tempDir, REVIEW_LOG_FILENAME))).toBe(true);
|
|
99
|
+
expect(deps.notify).not.toHaveBeenCalled();
|
|
57
100
|
});
|
|
58
101
|
|
|
59
|
-
it("
|
|
60
|
-
const
|
|
61
|
-
|
|
102
|
+
it("does not write to the review log when permissionReviewLog is false", () => {
|
|
103
|
+
const deps = makeDeps({
|
|
104
|
+
getConfig: () => ({
|
|
105
|
+
...DEFAULT_EXTENSION_CONFIG,
|
|
106
|
+
permissionReviewLog: false,
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
const logger = createSessionLogger(deps);
|
|
62
110
|
|
|
63
111
|
logger.review("permission.granted");
|
|
64
112
|
|
|
65
|
-
expect(
|
|
66
|
-
|
|
67
|
-
undefined,
|
|
68
|
-
);
|
|
113
|
+
expect(existsSync(join(tempDir, REVIEW_LOG_FILENAME))).toBe(false);
|
|
114
|
+
expect(deps.notify).not.toHaveBeenCalled();
|
|
69
115
|
});
|
|
70
116
|
});
|
|
71
117
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
118
|
+
// ── IO-failure warnings ───────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
describe("IO-failure warnings", () => {
|
|
121
|
+
it("calls notify with the error message when the logs directory cannot be created", () => {
|
|
122
|
+
const deps = makeDeps({
|
|
123
|
+
globalLogsDir: makeBlockedLogsDir(),
|
|
124
|
+
getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, debugLog: true }),
|
|
79
125
|
});
|
|
80
|
-
const logger = createSessionLogger(
|
|
126
|
+
const logger = createSessionLogger(deps);
|
|
81
127
|
|
|
82
|
-
logger.
|
|
128
|
+
logger.debug("test.event");
|
|
129
|
+
|
|
130
|
+
expect(deps.notify).toHaveBeenCalledOnce();
|
|
131
|
+
expect(deps.notify).toHaveBeenCalledWith(
|
|
132
|
+
expect.stringContaining("Failed to"),
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("deduplicates the same IO-failure warning across multiple writes", () => {
|
|
137
|
+
const deps = makeDeps({
|
|
138
|
+
globalLogsDir: makeBlockedLogsDir(),
|
|
139
|
+
getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG, debugLog: true }),
|
|
140
|
+
});
|
|
141
|
+
const logger = createSessionLogger(deps);
|
|
142
|
+
|
|
143
|
+
logger.debug("event.one");
|
|
144
|
+
logger.debug("event.two");
|
|
145
|
+
|
|
146
|
+
expect(deps.notify).toHaveBeenCalledOnce();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("shares the dedup set across debug and review — same message notified only once", () => {
|
|
150
|
+
const deps = makeDeps({
|
|
151
|
+
globalLogsDir: makeBlockedLogsDir(),
|
|
152
|
+
getConfig: () => ({
|
|
153
|
+
...DEFAULT_EXTENSION_CONFIG,
|
|
154
|
+
debugLog: true,
|
|
155
|
+
permissionReviewLog: true,
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
const logger = createSessionLogger(deps);
|
|
83
159
|
|
|
84
|
-
|
|
160
|
+
logger.debug("event.one"); // emits warning
|
|
161
|
+
logger.review("event.two"); // same error message → suppressed
|
|
162
|
+
|
|
163
|
+
expect(deps.notify).toHaveBeenCalledOnce();
|
|
85
164
|
});
|
|
165
|
+
});
|
|
86
166
|
|
|
87
|
-
|
|
88
|
-
const runtime = makeRuntime({ runtimeContext: null });
|
|
89
|
-
const logger = createSessionLogger(runtime);
|
|
167
|
+
// ── warn ──────────────────────────────────────────────────────────────────
|
|
90
168
|
|
|
91
|
-
|
|
169
|
+
describe("warn", () => {
|
|
170
|
+
it("calls notify with the message directly", () => {
|
|
171
|
+
const deps = makeDeps();
|
|
172
|
+
const logger = createSessionLogger(deps);
|
|
173
|
+
|
|
174
|
+
logger.warn("Something went wrong");
|
|
175
|
+
|
|
176
|
+
expect(deps.notify).toHaveBeenCalledWith("Something went wrong");
|
|
92
177
|
});
|
|
93
178
|
|
|
94
|
-
it("
|
|
95
|
-
const
|
|
96
|
-
const logger = createSessionLogger(
|
|
179
|
+
it("calls notify for every warn — not deduplicated", () => {
|
|
180
|
+
const deps = makeDeps();
|
|
181
|
+
const logger = createSessionLogger(deps);
|
|
97
182
|
|
|
98
|
-
|
|
99
|
-
logger.warn("
|
|
183
|
+
logger.warn("same message");
|
|
184
|
+
logger.warn("same message");
|
|
100
185
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
runtime.runtimeContext = {
|
|
104
|
-
ui: { notify, setStatus: vi.fn(), select: vi.fn(), input: vi.fn() },
|
|
105
|
-
} as unknown as ExtensionRuntime["runtimeContext"];
|
|
186
|
+
expect(deps.notify).toHaveBeenCalledTimes(2);
|
|
187
|
+
});
|
|
106
188
|
|
|
107
|
-
|
|
189
|
+
it("does not throw when notify is a no-op", () => {
|
|
190
|
+
const deps: SessionLoggerDeps = {
|
|
191
|
+
globalLogsDir: tempDir,
|
|
192
|
+
getConfig: () => ({ ...DEFAULT_EXTENSION_CONFIG }),
|
|
193
|
+
notify: () => {},
|
|
194
|
+
};
|
|
195
|
+
const logger = createSessionLogger(deps);
|
|
108
196
|
|
|
109
|
-
expect(
|
|
110
|
-
expect(notify).toHaveBeenCalledWith("late warning", "warning");
|
|
197
|
+
expect(() => logger.warn("test")).not.toThrow();
|
|
111
198
|
});
|
|
112
199
|
});
|
|
113
200
|
});
|
package/src/runtime.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
type ExtensionContext,
|
|
4
|
-
getAgentDir,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
|
|
7
|
-
import { DEBUG_LOG_FILENAME, REVIEW_LOG_FILENAME } from "./config-paths";
|
|
8
|
-
import { ConfigStore, type RuntimeContextRef } from "./config-store";
|
|
9
|
-
import { ensurePermissionSystemLogsDirectory } from "./extension-config";
|
|
10
|
-
import { computeExtensionPaths, type ExtensionPaths } from "./extension-paths";
|
|
11
|
-
|
|
12
|
-
export type { ExtensionPaths } from "./extension-paths";
|
|
13
|
-
|
|
14
|
-
import { createPermissionSystemLogger } from "./logging";
|
|
15
|
-
import { PermissionManager } from "./permission-manager";
|
|
16
|
-
import { SessionRules } from "./session-rules";
|
|
17
|
-
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Mutable session state — the subset of ExtensionRuntime that holds
|
|
21
|
-
* per-session fields. `PermissionSession` now owns these for handler
|
|
22
|
-
* use; this interface remains so `ExtensionRuntime` can still serve
|
|
23
|
-
* as the internal composition root (config-modal, RPC handlers).
|
|
24
|
-
*/
|
|
25
|
-
interface SessionState {
|
|
26
|
-
runtimeContext: ExtensionContext | null;
|
|
27
|
-
permissionManager: PermissionManager;
|
|
28
|
-
readonly sessionRules: SessionRules;
|
|
29
|
-
activeSkillEntries: SkillPromptEntry[];
|
|
30
|
-
lastKnownActiveAgentName: string | null;
|
|
31
|
-
lastActiveToolsCacheKey: string | null;
|
|
32
|
-
lastPromptStateCacheKey: string | null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Runtime context object created once inside `piPermissionSystemExtension()`.
|
|
37
|
-
*
|
|
38
|
-
* Holds all path constants (derived from `getAgentDir()` at construction time),
|
|
39
|
-
* mutable extension state, and the log-writing methods — eliminating the
|
|
40
|
-
* module-scope cached constants and setter-injection pattern that previously
|
|
41
|
-
* lived in `src/index.ts`.
|
|
42
|
-
*
|
|
43
|
-
* Tests construct this via `createExtensionRuntime({ agentDir: tmpDir })`
|
|
44
|
-
* without timing issues around `PI_CODING_AGENT_DIR`.
|
|
45
|
-
*/
|
|
46
|
-
export interface ExtensionRuntime extends ExtensionPaths, SessionState {
|
|
47
|
-
/** The store that owns extension config. */
|
|
48
|
-
configStore: ConfigStore;
|
|
49
|
-
|
|
50
|
-
// ── Logging (backed by logger created at construction) ─────────────────
|
|
51
|
-
writeDebugLog(event: string, details?: Record<string, unknown>): void;
|
|
52
|
-
writeReviewLog(event: string, details?: Record<string, unknown>): void;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ── Factory ────────────────────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Create a fully-initialized `ExtensionRuntime`.
|
|
59
|
-
*
|
|
60
|
-
* Calls `getAgentDir()` at invocation time (never at module scope), so tests
|
|
61
|
-
* may set `PI_CODING_AGENT_DIR` before calling the factory.
|
|
62
|
-
*/
|
|
63
|
-
export function createExtensionRuntime(options?: {
|
|
64
|
-
agentDir?: string;
|
|
65
|
-
}): ExtensionRuntime {
|
|
66
|
-
const agentDir = options?.agentDir ?? getAgentDir();
|
|
67
|
-
const paths = computeExtensionPaths(agentDir);
|
|
68
|
-
|
|
69
|
-
const permissionManager = new PermissionManager({ agentDir });
|
|
70
|
-
|
|
71
|
-
const runtime: ExtensionRuntime = {
|
|
72
|
-
...paths,
|
|
73
|
-
runtimeContext: null,
|
|
74
|
-
configStore: null as unknown as ConfigStore,
|
|
75
|
-
permissionManager,
|
|
76
|
-
activeSkillEntries: [],
|
|
77
|
-
lastKnownActiveAgentName: null,
|
|
78
|
-
lastActiveToolsCacheKey: null,
|
|
79
|
-
lastPromptStateCacheKey: null,
|
|
80
|
-
sessionRules: new SessionRules(),
|
|
81
|
-
// Logging methods are replaced below after the logger is constructed.
|
|
82
|
-
writeDebugLog: () => {},
|
|
83
|
-
writeReviewLog: () => {},
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// Transitional RuntimeContextRef: reads/writes the still-runtime-owned
|
|
87
|
-
// `runtimeContext` field until Step 4 (#337) unifies context onto
|
|
88
|
-
// PermissionSession.
|
|
89
|
-
const contextRef: RuntimeContextRef = {
|
|
90
|
-
get: () => runtime.runtimeContext,
|
|
91
|
-
set: (ctx) => {
|
|
92
|
-
runtime.runtimeContext = ctx;
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const configStore = new ConfigStore({
|
|
97
|
-
agentDir,
|
|
98
|
-
context: contextRef,
|
|
99
|
-
policyPaths: permissionManager,
|
|
100
|
-
logger: {
|
|
101
|
-
// Deferred-binding: `runtime.writeDebugLog` is replaced below after
|
|
102
|
-
// the logger is constructed — same deferred pattern as before Step 2.
|
|
103
|
-
writeDebugLog: (e, d) => runtime.writeDebugLog(e, d),
|
|
104
|
-
writeReviewLog: (e, d) => runtime.writeReviewLog(e, d),
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
runtime.configStore = configStore;
|
|
108
|
-
|
|
109
|
-
const reportedLoggingWarnings = new Set<string>();
|
|
110
|
-
const logger = createPermissionSystemLogger({
|
|
111
|
-
getConfig: () => configStore.current(),
|
|
112
|
-
debugLogPath: join(paths.globalLogsDir, DEBUG_LOG_FILENAME),
|
|
113
|
-
reviewLogPath: join(paths.globalLogsDir, REVIEW_LOG_FILENAME),
|
|
114
|
-
ensureLogsDirectory: () =>
|
|
115
|
-
ensurePermissionSystemLogsDirectory(paths.globalLogsDir),
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const reportLoggingWarning = (message: string): void => {
|
|
119
|
-
if (reportedLoggingWarnings.has(message)) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
reportedLoggingWarnings.add(message);
|
|
123
|
-
runtime.runtimeContext?.ui.notify(message, "warning");
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
runtime.writeDebugLog = (
|
|
127
|
-
event: string,
|
|
128
|
-
details: Record<string, unknown> = {},
|
|
129
|
-
): void => {
|
|
130
|
-
const warning = logger.debug(event, details);
|
|
131
|
-
if (warning) {
|
|
132
|
-
reportLoggingWarning(warning);
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
runtime.writeReviewLog = (
|
|
137
|
-
event: string,
|
|
138
|
-
details: Record<string, unknown> = {},
|
|
139
|
-
): void => {
|
|
140
|
-
const warning = logger.review(event, details);
|
|
141
|
-
if (warning) {
|
|
142
|
-
reportLoggingWarning(warning);
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
return runtime;
|
|
147
|
-
}
|