@erica-s/ai-agent-notify 2.1.5
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/.published +1 -0
- package/README.md +100 -0
- package/assets/icons/info.png +0 -0
- package/assets/icons/permission.png +0 -0
- package/assets/icons/stop.png +0 -0
- package/bin/cli.js +3104 -0
- package/lib/codex-sidecar-state.js +314 -0
- package/lib/notification-sources.js +411 -0
- package/package.json +41 -0
- package/postinstall.js +54 -0
- package/scripts/activate-window.ps1 +26 -0
- package/scripts/activate-window.vbs +13 -0
- package/scripts/codex-notify-wrapper.vbs +29 -0
- package/scripts/find-hwnd.ps1 +144 -0
- package/scripts/get-shell-pid.ps1 +69 -0
- package/scripts/notify.ps1 +193 -0
- package/scripts/register-protocol.ps1 +18 -0
- package/scripts/start-hidden.vbs +24 -0
- package/scripts/start-tab-color-watcher.ps1 +41 -0
- package/scripts/tab-color-watcher.ps1 +391 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,3104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn, spawnSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const readline = require("readline");
|
|
8
|
+
const { StringDecoder } = require("string_decoder");
|
|
9
|
+
const {
|
|
10
|
+
findSidecarTerminalContextForProjectDir,
|
|
11
|
+
findSidecarTerminalContextForSession,
|
|
12
|
+
writeSidecarRecord,
|
|
13
|
+
} = require("../lib/codex-sidecar-state");
|
|
14
|
+
const {
|
|
15
|
+
createNotificationSpec,
|
|
16
|
+
normalizeIncomingNotification,
|
|
17
|
+
} = require("../lib/notification-sources");
|
|
18
|
+
|
|
19
|
+
const PACKAGE_VERSION = readPackageVersion();
|
|
20
|
+
const LOG_DIR = path.join(os.tmpdir(), "ai-agent-notify");
|
|
21
|
+
const IS_DEV = !fs.existsSync(path.join(__dirname, "..", ".published"));
|
|
22
|
+
const SIDECAR_SESSION_RESOLUTION_POLL_MS = 1000;
|
|
23
|
+
const SIDECAR_SESSION_RESOLUTION_TIMEOUT_MS = 90 * 1000;
|
|
24
|
+
const SIDECAR_SESSION_RESOLUTION_MAX_PAST_MS = 30 * 1000;
|
|
25
|
+
const SIDECAR_SESSION_RESOLUTION_MAX_FUTURE_MS = 10 * 60 * 1000;
|
|
26
|
+
const CODEX_APPROVAL_NOTIFY_GRACE_MS = 1000;
|
|
27
|
+
const CODEX_READ_ONLY_APPROVAL_NOTIFY_GRACE_MS = 5 * 1000;
|
|
28
|
+
const CODEX_APPROVAL_BATCH_WINDOW_MS = 500;
|
|
29
|
+
const RECENT_REQUIRE_ESCALATED_TTL_MS = 30 * 60 * 1000;
|
|
30
|
+
const SESSION_APPROVAL_CONFIRM_LOOKBACK_MS = 5 * 60 * 1000;
|
|
31
|
+
const SESSION_APPROVAL_GRANT_TTL_MS = 30 * 60 * 1000;
|
|
32
|
+
const MAX_RECENT_REQUIRE_ESCALATED_EVENTS_PER_SESSION = 64;
|
|
33
|
+
const MAX_SESSION_APPROVAL_GRANTS_PER_SESSION = 128;
|
|
34
|
+
const COMMAND_APPROVAL_ROOT_MAX_DEPTH = 8;
|
|
35
|
+
const COMMAND_APPROVAL_ROOT_MARKERS = [
|
|
36
|
+
"package.json",
|
|
37
|
+
"package-lock.json",
|
|
38
|
+
"pnpm-lock.yaml",
|
|
39
|
+
"yarn.lock",
|
|
40
|
+
"pyproject.toml",
|
|
41
|
+
"Cargo.toml",
|
|
42
|
+
"go.mod",
|
|
43
|
+
"pom.xml",
|
|
44
|
+
"Gemfile",
|
|
45
|
+
"composer.json",
|
|
46
|
+
".git",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
if (require.main === module) {
|
|
50
|
+
runCli();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runCli() {
|
|
54
|
+
try {
|
|
55
|
+
ensureWindows();
|
|
56
|
+
await main();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(error && error.message ? error.message : String(error));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ensureWindows() {
|
|
64
|
+
if (process.platform !== "win32") {
|
|
65
|
+
throw new Error("ai-agent-notify currently only supports Windows.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function main() {
|
|
70
|
+
const argv = process.argv.slice(2);
|
|
71
|
+
|
|
72
|
+
if (argv[0] === "codex-session-watch" || argv[0] === "--codex-session-watch") {
|
|
73
|
+
await runCodexSessionWatchMode(
|
|
74
|
+
argv[0] === "--codex-session-watch" ? argv.slice(1) : argv.slice(1)
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (argv[0] === "codex-mcp-sidecar" || argv[0] === "mcp-sidecar") {
|
|
80
|
+
await runCodexMcpSidecarMode(argv.slice(1));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (argv[0] === "--help" || argv[0] === "help") {
|
|
85
|
+
printHelp();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await runDefaultNotifyMode(argv);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function printHelp() {
|
|
93
|
+
process.stdout.write(
|
|
94
|
+
[
|
|
95
|
+
"Usage:",
|
|
96
|
+
" ai-agent-notify",
|
|
97
|
+
" ai-agent-notify codex-session-watch [--sessions-dir <path>] [--tui-log <path>] [--poll-ms <ms>]",
|
|
98
|
+
" ai-agent-notify codex-mcp-sidecar",
|
|
99
|
+
"",
|
|
100
|
+
"Modes:",
|
|
101
|
+
" default Read notification JSON from stdin or argv and show a notification",
|
|
102
|
+
" codex-session-watch Watch local Codex rollout files and TUI logs for approval events",
|
|
103
|
+
" codex-mcp-sidecar Run a minimal MCP sidecar that records Codex terminal/session hints and ensures codex-session-watch is running",
|
|
104
|
+
"",
|
|
105
|
+
"Flags:",
|
|
106
|
+
" --shell-pid <pid> Override the detected shell pid",
|
|
107
|
+
" --sessions-dir <path> Override the Codex sessions directory (default: %USERPROFILE%\\.codex\\sessions)",
|
|
108
|
+
" --tui-log <path> Override the Codex TUI log path (default: %USERPROFILE%\\.codex\\log\\codex-tui.log)",
|
|
109
|
+
" --poll-ms <ms> Poll interval for session file scanning (default: 1000)",
|
|
110
|
+
"",
|
|
111
|
+
].join(os.EOL)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function runDefaultNotifyMode(argv) {
|
|
116
|
+
const stdinData = readStdin();
|
|
117
|
+
const notification = normalizeIncomingNotification({
|
|
118
|
+
argv,
|
|
119
|
+
stdinData,
|
|
120
|
+
env: process.env,
|
|
121
|
+
});
|
|
122
|
+
const sessionId = notification.sessionId || "unknown";
|
|
123
|
+
const runtime = createRuntime(sessionId);
|
|
124
|
+
const terminal = detectTerminalContext(argv, runtime.log);
|
|
125
|
+
|
|
126
|
+
runtime.log(
|
|
127
|
+
`started mode=notify source=${notification.sourceId} transport=${notification.transport || "none"} session=${sessionId}`
|
|
128
|
+
);
|
|
129
|
+
runtime.log(notification.debugSummary);
|
|
130
|
+
|
|
131
|
+
const child = emitNotification({
|
|
132
|
+
source: notification.source,
|
|
133
|
+
eventName: notification.eventName,
|
|
134
|
+
title: notification.title,
|
|
135
|
+
message: notification.message,
|
|
136
|
+
rawEventType: notification.rawEventType,
|
|
137
|
+
runtime,
|
|
138
|
+
terminal,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
child.on("close", (code) => {
|
|
142
|
+
runtime.log(`notify.ps1 exited code=${code}`);
|
|
143
|
+
process.exit(code || 0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
child.on("error", (error) => {
|
|
147
|
+
runtime.log(`spawn failed: ${error.message}`);
|
|
148
|
+
process.exit(0);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function runCodexSessionWatchMode(argv) {
|
|
153
|
+
if (argv[0] === "--help" || argv[0] === "help") {
|
|
154
|
+
printHelp();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const sessionsDir =
|
|
159
|
+
getArgValue(argv, "--sessions-dir") ||
|
|
160
|
+
getEnvFirst(["TOAST_NOTIFY_CODEX_SESSIONS_DIR"]) ||
|
|
161
|
+
path.join(getCodexHomeDir(), "sessions");
|
|
162
|
+
const tuiLogPath =
|
|
163
|
+
getArgValue(argv, "--tui-log") ||
|
|
164
|
+
getEnvFirst(["TOAST_NOTIFY_CODEX_TUI_LOG"]) ||
|
|
165
|
+
path.join(getCodexHomeDir(), "log", "codex-tui.log");
|
|
166
|
+
const pollMs = parsePositiveInteger(getArgValue(argv, "--poll-ms"), 1000);
|
|
167
|
+
|
|
168
|
+
const runtime = createRuntime(`codex-session-watch-${Date.now()}`);
|
|
169
|
+
const instanceLock = acquireSingleInstanceLock("codex-session-watch", runtime.log);
|
|
170
|
+
if (!instanceLock.acquired) {
|
|
171
|
+
runtime.log(
|
|
172
|
+
`another codex-session-watch is already running pid=${instanceLock.existingPid || "unknown"}`
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const terminal = createNeutralTerminalContext();
|
|
178
|
+
const fileStates = new Map();
|
|
179
|
+
const sessionProjectDirs = new Map();
|
|
180
|
+
const sessionApprovalContexts = new Map();
|
|
181
|
+
const sessionApprovalGrants = new Map();
|
|
182
|
+
const recentRequireEscalatedEvents = new Map();
|
|
183
|
+
const emittedEventKeys = new Map();
|
|
184
|
+
const pendingApprovalNotifications = new Map();
|
|
185
|
+
const pendingApprovalCallIds = new Map();
|
|
186
|
+
const approvedCommandRuleCache = createApprovedCommandRuleCache(
|
|
187
|
+
path.join(getCodexHomeDir(), "rules", "default.rules")
|
|
188
|
+
);
|
|
189
|
+
let tuiLogState = null;
|
|
190
|
+
let initialScan = true;
|
|
191
|
+
let scanInProgress = false;
|
|
192
|
+
let shuttingDown = false;
|
|
193
|
+
|
|
194
|
+
runtime.log(
|
|
195
|
+
`started mode=codex-session-watch sessionsDir=${sessionsDir} tuiLogPath=${tuiLogPath} pollMs=${pollMs}`
|
|
196
|
+
);
|
|
197
|
+
runtime.log(`acquired single-instance lock file=${instanceLock.lockPath}`);
|
|
198
|
+
|
|
199
|
+
if (!fileExistsCaseInsensitive(sessionsDir)) {
|
|
200
|
+
runtime.log(`sessions dir not found yet: ${sessionsDir}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!fileExistsCaseInsensitive(tuiLogPath)) {
|
|
204
|
+
runtime.log(`tui log not found yet: ${tuiLogPath}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const interval = setInterval(scanOnce, pollMs);
|
|
208
|
+
|
|
209
|
+
process.on("exit", () => releaseSingleInstanceLock(instanceLock, runtime.log));
|
|
210
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
211
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
212
|
+
|
|
213
|
+
scanOnce();
|
|
214
|
+
initialScan = false;
|
|
215
|
+
|
|
216
|
+
function shutdown(signal) {
|
|
217
|
+
if (shuttingDown) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
shuttingDown = true;
|
|
221
|
+
clearInterval(interval);
|
|
222
|
+
runtime.log(`stopped mode=codex-session-watch signal=${signal}`);
|
|
223
|
+
releaseSingleInstanceLock(instanceLock, runtime.log);
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function scanOnce() {
|
|
228
|
+
if (scanInProgress) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
scanInProgress = true;
|
|
233
|
+
try {
|
|
234
|
+
const files = listRolloutFiles(sessionsDir, runtime.log);
|
|
235
|
+
const existing = new Set(files);
|
|
236
|
+
|
|
237
|
+
files.forEach((filePath) => {
|
|
238
|
+
let stat;
|
|
239
|
+
try {
|
|
240
|
+
stat = fs.statSync(filePath);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
runtime.log(`stat failed file=${filePath} error=${error.message}`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let state = fileStates.get(filePath);
|
|
247
|
+
if (!state) {
|
|
248
|
+
state = createSessionFileState(filePath);
|
|
249
|
+
fileStates.set(filePath, state);
|
|
250
|
+
|
|
251
|
+
if (initialScan) {
|
|
252
|
+
bootstrapExistingSessionFileState(state, stat, runtime.log);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
runtime.log(
|
|
256
|
+
`tracking session file=${filePath} position=${state.position} sessionId=${state.sessionId || "unknown"} cwd=${state.cwd || ""}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
consumeSessionFileUpdates(state, stat, {
|
|
261
|
+
runtime,
|
|
262
|
+
terminal,
|
|
263
|
+
emittedEventKeys,
|
|
264
|
+
pendingApprovalNotifications,
|
|
265
|
+
pendingApprovalCallIds,
|
|
266
|
+
recentRequireEscalatedEvents,
|
|
267
|
+
sessionApprovalGrants,
|
|
268
|
+
approvedCommandRuleCache,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (state.sessionId && state.cwd) {
|
|
272
|
+
sessionProjectDirs.set(state.sessionId, state.cwd);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (state.sessionId && (state.approvalPolicy || state.sandboxPolicy)) {
|
|
276
|
+
sessionApprovalContexts.set(state.sessionId, {
|
|
277
|
+
approvalPolicy: state.approvalPolicy || "",
|
|
278
|
+
sandboxPolicy: state.sandboxPolicy || null,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
Array.from(fileStates.keys()).forEach((filePath) => {
|
|
284
|
+
if (!existing.has(filePath)) {
|
|
285
|
+
fileStates.delete(filePath);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
tuiLogState = syncCodexTuiLogState(tuiLogState, tuiLogPath, {
|
|
290
|
+
initialScan,
|
|
291
|
+
runtime,
|
|
292
|
+
terminal,
|
|
293
|
+
emittedEventKeys,
|
|
294
|
+
sessionProjectDirs,
|
|
295
|
+
sessionApprovalContexts,
|
|
296
|
+
pendingApprovalNotifications,
|
|
297
|
+
pendingApprovalCallIds,
|
|
298
|
+
recentRequireEscalatedEvents,
|
|
299
|
+
sessionApprovalGrants,
|
|
300
|
+
approvedCommandRuleCache,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
flushPendingApprovalNotifications({
|
|
304
|
+
runtime,
|
|
305
|
+
terminal,
|
|
306
|
+
emittedEventKeys,
|
|
307
|
+
pendingApprovalNotifications,
|
|
308
|
+
pendingApprovalCallIds,
|
|
309
|
+
});
|
|
310
|
+
pruneEmittedEventKeys(emittedEventKeys, 4096);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
runtime.log(`session scan failed: ${error.message}`);
|
|
313
|
+
} finally {
|
|
314
|
+
scanInProgress = false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function runCodexMcpSidecarMode(argv) {
|
|
320
|
+
if (argv[0] === "--help" || argv[0] === "help") {
|
|
321
|
+
printHelp();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const runtime = createRuntime(`codex-mcp-sidecar-${Date.now()}`);
|
|
326
|
+
ensureCodexSessionWatchRunning(runtime.log);
|
|
327
|
+
const parentInfo = findParentInfo(runtime.log);
|
|
328
|
+
const sessionsDir = path.join(getCodexHomeDir(), "sessions");
|
|
329
|
+
const recordId = `codex-mcp-sidecar-${process.pid}-${Date.now()}`;
|
|
330
|
+
let sidecarRecord = writeSidecarRecord({
|
|
331
|
+
recordId,
|
|
332
|
+
pid: process.pid,
|
|
333
|
+
parentPid: process.ppid,
|
|
334
|
+
cwd: process.cwd(),
|
|
335
|
+
sessionId: "",
|
|
336
|
+
startedAt: new Date().toISOString(),
|
|
337
|
+
resolvedAt: "",
|
|
338
|
+
hwnd: parentInfo.hwnd,
|
|
339
|
+
shellPid: parentInfo.shellPid,
|
|
340
|
+
isWindowsTerminal: parentInfo.isWindowsTerminal,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
runtime.log(
|
|
344
|
+
`started mode=codex-mcp-sidecar cwd=${sidecarRecord.cwd} shellPid=${sidecarRecord.shellPid || ""} hwnd=${sidecarRecord.hwnd || ""} sessionsDir=${sessionsDir}`
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const resolver = startSidecarSessionResolver({
|
|
348
|
+
getCurrentRecord: () => sidecarRecord,
|
|
349
|
+
updateRecord(nextRecord) {
|
|
350
|
+
sidecarRecord = writeSidecarRecord(nextRecord);
|
|
351
|
+
return sidecarRecord;
|
|
352
|
+
},
|
|
353
|
+
sessionsDir,
|
|
354
|
+
log: runtime.log,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
let cleanedUp = false;
|
|
358
|
+
const cleanup = () => {
|
|
359
|
+
if (cleanedUp) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
cleanedUp = true;
|
|
363
|
+
resolver.stop();
|
|
364
|
+
runtime.log(
|
|
365
|
+
`stopped mode=codex-mcp-sidecar recordId=${recordId} sessionId=${sidecarRecord.sessionId || ""} retained=1`
|
|
366
|
+
);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
process.on("exit", cleanup);
|
|
370
|
+
process.on("SIGINT", () => {
|
|
371
|
+
cleanup();
|
|
372
|
+
process.exit(0);
|
|
373
|
+
});
|
|
374
|
+
process.on("SIGTERM", () => {
|
|
375
|
+
cleanup();
|
|
376
|
+
process.exit(0);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
await Promise.all([serveMinimalMcpServer({ runtime }), resolver.done]);
|
|
381
|
+
} finally {
|
|
382
|
+
cleanup();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function startSidecarSessionResolver({ getCurrentRecord, updateRecord, sessionsDir, log }) {
|
|
387
|
+
let attempts = 0;
|
|
388
|
+
let interval = null;
|
|
389
|
+
let stopped = false;
|
|
390
|
+
let resolveDone = () => {};
|
|
391
|
+
const done = new Promise((resolve) => {
|
|
392
|
+
resolveDone = resolve;
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const tick = () => {
|
|
396
|
+
if (stopped) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const currentRecord = getCurrentRecord();
|
|
401
|
+
if (!currentRecord || currentRecord.sessionId) {
|
|
402
|
+
stop();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
attempts += 1;
|
|
407
|
+
const candidate = resolveSidecarSessionCandidate({
|
|
408
|
+
cwd: currentRecord.cwd,
|
|
409
|
+
sessionsDir,
|
|
410
|
+
startedAtMs: Date.parse(currentRecord.startedAt),
|
|
411
|
+
log,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (candidate) {
|
|
415
|
+
const resolvedAt = new Date().toISOString();
|
|
416
|
+
updateRecord({
|
|
417
|
+
...currentRecord,
|
|
418
|
+
sessionId: candidate.sessionId,
|
|
419
|
+
resolvedAt,
|
|
420
|
+
});
|
|
421
|
+
log(
|
|
422
|
+
`resolved mcp sidecar sessionId=${candidate.sessionId} file=${candidate.filePath} scoreMs=${candidate.score} reference=${candidate.referenceKind}`
|
|
423
|
+
);
|
|
424
|
+
stop();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (attempts * SIDECAR_SESSION_RESOLUTION_POLL_MS >= SIDECAR_SESSION_RESOLUTION_TIMEOUT_MS) {
|
|
429
|
+
log(
|
|
430
|
+
`mcp sidecar session resolution timed out cwd=${currentRecord.cwd} timeoutMs=${SIDECAR_SESSION_RESOLUTION_TIMEOUT_MS}`
|
|
431
|
+
);
|
|
432
|
+
stop();
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
interval = setInterval(tick, SIDECAR_SESSION_RESOLUTION_POLL_MS);
|
|
437
|
+
tick();
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
done,
|
|
441
|
+
stop,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
function stop() {
|
|
445
|
+
if (stopped) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
stopped = true;
|
|
449
|
+
if (interval) {
|
|
450
|
+
clearInterval(interval);
|
|
451
|
+
interval = null;
|
|
452
|
+
}
|
|
453
|
+
resolveDone();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function resolveSidecarSessionCandidate({ cwd, sessionsDir, startedAtMs, log }) {
|
|
458
|
+
if (!cwd || !sessionsDir || !fileExistsCaseInsensitive(sessionsDir)) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const candidates = [];
|
|
463
|
+
|
|
464
|
+
listRolloutFiles(sessionsDir, log).forEach((filePath) => {
|
|
465
|
+
const rolloutStartedAtMs = parseRolloutTimestampFromPath(filePath);
|
|
466
|
+
let stat = null;
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
stat = fs.statSync(filePath);
|
|
470
|
+
} catch {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const metadata = readRolloutMetadata(filePath, log);
|
|
475
|
+
if (!metadata.sessionId || !isSameWindowsPath(metadata.cwd, cwd)) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const reference = pickBestSidecarCandidateReference(
|
|
480
|
+
[
|
|
481
|
+
{
|
|
482
|
+
kind: "latest_event",
|
|
483
|
+
timestampMs: metadata.latestEventAtMs,
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
kind: "mtime",
|
|
487
|
+
timestampMs: stat.mtimeMs,
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
kind: "rollout_start",
|
|
491
|
+
timestampMs: rolloutStartedAtMs,
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
startedAtMs
|
|
495
|
+
);
|
|
496
|
+
if (!reference) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
candidates.push({
|
|
501
|
+
filePath,
|
|
502
|
+
sessionId: metadata.sessionId,
|
|
503
|
+
score: reference.score,
|
|
504
|
+
isFutureMatch: reference.signedDistanceMs >= 0,
|
|
505
|
+
referenceStartedAtMs: reference.timestampMs,
|
|
506
|
+
referenceKind: reference.kind,
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
return pickSidecarSessionCandidate(candidates);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function pickBestSidecarCandidateReference(references, startedAtMs) {
|
|
514
|
+
if (!Array.isArray(references) || !startedAtMs) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const priority = {
|
|
519
|
+
latest_event: 3,
|
|
520
|
+
mtime: 2,
|
|
521
|
+
rollout_start: 1,
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const candidates = references
|
|
525
|
+
.filter(
|
|
526
|
+
(reference) =>
|
|
527
|
+
reference &&
|
|
528
|
+
reference.timestampMs &&
|
|
529
|
+
isSidecarResolutionTimeMatch({
|
|
530
|
+
candidateStartedAtMs: reference.timestampMs,
|
|
531
|
+
sidecarStartedAtMs: startedAtMs,
|
|
532
|
+
})
|
|
533
|
+
)
|
|
534
|
+
.map((reference) => ({
|
|
535
|
+
kind: reference.kind,
|
|
536
|
+
timestampMs: reference.timestampMs,
|
|
537
|
+
signedDistanceMs: reference.timestampMs - startedAtMs,
|
|
538
|
+
score: Math.abs(reference.timestampMs - startedAtMs),
|
|
539
|
+
priority: priority[reference.kind] || 0,
|
|
540
|
+
}))
|
|
541
|
+
.sort(
|
|
542
|
+
(left, right) =>
|
|
543
|
+
left.score - right.score ||
|
|
544
|
+
right.priority - left.priority ||
|
|
545
|
+
Number(right.signedDistanceMs >= 0) - Number(left.signedDistanceMs >= 0)
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
return candidates[0] || null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function pickSidecarSessionCandidate(candidates) {
|
|
552
|
+
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const sorted = candidates
|
|
557
|
+
.slice()
|
|
558
|
+
.sort(
|
|
559
|
+
(left, right) =>
|
|
560
|
+
left.score - right.score ||
|
|
561
|
+
Number(right.isFutureMatch === true) - Number(left.isFutureMatch === true) ||
|
|
562
|
+
right.referenceStartedAtMs - left.referenceStartedAtMs ||
|
|
563
|
+
left.sessionId.localeCompare(right.sessionId)
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
const best = sorted[0];
|
|
567
|
+
const second = sorted[1];
|
|
568
|
+
if (!best || best.score > 2 * 60 * 1000) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (
|
|
573
|
+
second &&
|
|
574
|
+
Math.abs(best.score - second.score) < 3000 &&
|
|
575
|
+
(best.isFutureMatch === second.isFutureMatch || best.isFutureMatch !== true)
|
|
576
|
+
) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return best;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function isSidecarResolutionTimeMatch({ candidateStartedAtMs, sidecarStartedAtMs }) {
|
|
584
|
+
if (!candidateStartedAtMs || !sidecarStartedAtMs) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const signedDistanceMs = candidateStartedAtMs - sidecarStartedAtMs;
|
|
589
|
+
return (
|
|
590
|
+
signedDistanceMs >= -SIDECAR_SESSION_RESOLUTION_MAX_PAST_MS &&
|
|
591
|
+
signedDistanceMs <= SIDECAR_SESSION_RESOLUTION_MAX_FUTURE_MS
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function parseRolloutTimestampFromPath(filePath) {
|
|
596
|
+
const match = path
|
|
597
|
+
.basename(filePath)
|
|
598
|
+
.match(/^rollout-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-.+\.jsonl$/i);
|
|
599
|
+
|
|
600
|
+
if (!match) {
|
|
601
|
+
return 0;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const [, datePart, hourPart, minutePart, secondPart] = match;
|
|
605
|
+
const parsed = new Date(
|
|
606
|
+
`${datePart}T${hourPart}:${minutePart}:${secondPart}`
|
|
607
|
+
).getTime();
|
|
608
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function isSameWindowsPath(left, right) {
|
|
612
|
+
if (!left || !right) {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return normalizeWindowsPath(left) === normalizeWindowsPath(right);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function normalizeWindowsPath(value) {
|
|
620
|
+
try {
|
|
621
|
+
return path.resolve(value).replace(/\//g, "\\").toLowerCase();
|
|
622
|
+
} catch {
|
|
623
|
+
return String(value || "")
|
|
624
|
+
.replace(/\//g, "\\")
|
|
625
|
+
.toLowerCase();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function serveMinimalMcpServer({ runtime }) {
|
|
630
|
+
const reader = readline.createInterface({
|
|
631
|
+
input: process.stdin,
|
|
632
|
+
crlfDelay: Infinity,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
return new Promise((resolve) => {
|
|
636
|
+
reader.on("line", (line) => {
|
|
637
|
+
if (!line.trim()) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
let message;
|
|
642
|
+
try {
|
|
643
|
+
message = JSON.parse(stripUtf8Bom(line));
|
|
644
|
+
} catch (error) {
|
|
645
|
+
runtime.log(`mcp parse failed error=${error.message}`);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
handleMcpServerMessage(message, runtime.log);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
reader.on("close", resolve);
|
|
653
|
+
process.stdin.on("end", resolve);
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function handleMcpServerMessage(message, log) {
|
|
658
|
+
if (!message || typeof message.method !== "string") {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const hasId = Object.prototype.hasOwnProperty.call(message, "id");
|
|
663
|
+
if (typeof log === "function") {
|
|
664
|
+
log(`mcp method received method=${message.method} hasId=${hasId ? "1" : "0"}`);
|
|
665
|
+
}
|
|
666
|
+
if (!hasId) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
switch (message.method) {
|
|
671
|
+
case "initialize":
|
|
672
|
+
writeMcpResult(message.id, {
|
|
673
|
+
protocolVersion:
|
|
674
|
+
message &&
|
|
675
|
+
message.params &&
|
|
676
|
+
typeof message.params.protocolVersion === "string" &&
|
|
677
|
+
message.params.protocolVersion
|
|
678
|
+
? message.params.protocolVersion
|
|
679
|
+
: "2025-03-26",
|
|
680
|
+
capabilities: {
|
|
681
|
+
prompts: {},
|
|
682
|
+
resources: {},
|
|
683
|
+
tools: {},
|
|
684
|
+
},
|
|
685
|
+
serverInfo: {
|
|
686
|
+
name: "ai-agent-notify",
|
|
687
|
+
version: PACKAGE_VERSION,
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
return;
|
|
691
|
+
case "ping":
|
|
692
|
+
writeMcpResult(message.id, {});
|
|
693
|
+
return;
|
|
694
|
+
case "tools/list":
|
|
695
|
+
writeMcpResult(message.id, { tools: [] });
|
|
696
|
+
return;
|
|
697
|
+
case "resources/list":
|
|
698
|
+
writeMcpResult(message.id, { resources: [] });
|
|
699
|
+
return;
|
|
700
|
+
case "resources/templates/list":
|
|
701
|
+
writeMcpResult(message.id, { resourceTemplates: [] });
|
|
702
|
+
return;
|
|
703
|
+
case "prompts/list":
|
|
704
|
+
writeMcpResult(message.id, { prompts: [] });
|
|
705
|
+
return;
|
|
706
|
+
default:
|
|
707
|
+
log(`mcp unsupported method=${message.method}`);
|
|
708
|
+
writeMcpError(message.id, -32601, `Method not found: ${message.method}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function writeMcpResult(id, result) {
|
|
713
|
+
process.stdout.write(
|
|
714
|
+
`${JSON.stringify({
|
|
715
|
+
jsonrpc: "2.0",
|
|
716
|
+
id,
|
|
717
|
+
result,
|
|
718
|
+
})}\n`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function writeMcpError(id, code, message) {
|
|
723
|
+
process.stdout.write(
|
|
724
|
+
`${JSON.stringify({
|
|
725
|
+
jsonrpc: "2.0",
|
|
726
|
+
id,
|
|
727
|
+
error: {
|
|
728
|
+
code,
|
|
729
|
+
message,
|
|
730
|
+
},
|
|
731
|
+
})}\n`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function createRuntime(logId) {
|
|
736
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
737
|
+
const logFile = path.join(LOG_DIR, `${logId}.log`);
|
|
738
|
+
|
|
739
|
+
function log(message) {
|
|
740
|
+
const line = `[${new Date().toISOString()}] [node pid=${process.pid}] ${message}\n`;
|
|
741
|
+
process.stderr.write(line);
|
|
742
|
+
try {
|
|
743
|
+
fs.appendFileSync(logFile, line);
|
|
744
|
+
} catch {}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return {
|
|
748
|
+
isDev: IS_DEV,
|
|
749
|
+
logFile,
|
|
750
|
+
log,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function readPackageVersion() {
|
|
755
|
+
try {
|
|
756
|
+
const packageJson = JSON.parse(
|
|
757
|
+
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
|
|
758
|
+
);
|
|
759
|
+
return packageJson.version || "0.0.0";
|
|
760
|
+
} catch {
|
|
761
|
+
return "0.0.0";
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function readStdin() {
|
|
766
|
+
if (process.stdin.isTTY) {
|
|
767
|
+
return "";
|
|
768
|
+
}
|
|
769
|
+
return fs.readFileSync(0, { encoding: "utf8" });
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function getArgValue(argv, name) {
|
|
773
|
+
const index = argv.indexOf(name);
|
|
774
|
+
return index >= 0 && index + 1 < argv.length ? argv[index + 1] : "";
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function getEnvFirst(names) {
|
|
778
|
+
for (const name of names) {
|
|
779
|
+
const value = process.env[name];
|
|
780
|
+
if (typeof value === "string" && value.trim()) {
|
|
781
|
+
return value.trim();
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return "";
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function parsePositiveInteger(rawValue, fallbackValue) {
|
|
789
|
+
const parsed = parseInt(rawValue, 10);
|
|
790
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallbackValue;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function ensureCodexSessionWatchRunning(log) {
|
|
794
|
+
const state = querySingleInstanceLock("codex-session-watch");
|
|
795
|
+
if (state.running) {
|
|
796
|
+
if (typeof log === "function") {
|
|
797
|
+
log(`codex-session-watch already running pid=${state.pid} lock=${state.lockPath}`);
|
|
798
|
+
}
|
|
799
|
+
return { launched: false, pid: state.pid, lockPath: state.lockPath };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (state.pid && typeof log === "function") {
|
|
803
|
+
log(`codex-session-watch lock is stale pid=${state.pid} lock=${state.lockPath}`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const child = launchCodexSessionWatchHidden([], log);
|
|
807
|
+
return {
|
|
808
|
+
launched: true,
|
|
809
|
+
pid: child && child.pid ? child.pid : null,
|
|
810
|
+
lockPath: state.lockPath,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function launchCodexSessionWatchHidden(watcherArgs, log) {
|
|
815
|
+
const launchArgs = buildCodexSessionWatchLaunchArgs(watcherArgs);
|
|
816
|
+
const wscriptPath = getWindowsScriptHostPath();
|
|
817
|
+
const launcherScript = getHiddenLauncherScriptPath();
|
|
818
|
+
|
|
819
|
+
if (fileExistsCaseInsensitive(wscriptPath) && fileExistsCaseInsensitive(launcherScript)) {
|
|
820
|
+
const child = spawnDetachedHiddenProcess(
|
|
821
|
+
wscriptPath,
|
|
822
|
+
[launcherScript, ...launchArgs],
|
|
823
|
+
"codex-session-watch launcher",
|
|
824
|
+
log
|
|
825
|
+
);
|
|
826
|
+
if (typeof log === "function") {
|
|
827
|
+
log(`spawned codex-session-watch launcher pid=${child.pid || ""}`);
|
|
828
|
+
}
|
|
829
|
+
return child;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const child = spawnDetachedHiddenProcess(
|
|
833
|
+
process.execPath,
|
|
834
|
+
[path.resolve(__filename), "codex-session-watch", ...watcherArgs],
|
|
835
|
+
"codex-session-watch direct",
|
|
836
|
+
log
|
|
837
|
+
);
|
|
838
|
+
if (typeof log === "function") {
|
|
839
|
+
log(`spawned codex-session-watch directly pid=${child.pid || ""}`);
|
|
840
|
+
}
|
|
841
|
+
return child;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function spawnDetachedHiddenProcess(command, args, label, log) {
|
|
845
|
+
const child = spawn(command, args, {
|
|
846
|
+
detached: true,
|
|
847
|
+
stdio: "ignore",
|
|
848
|
+
windowsHide: true,
|
|
849
|
+
});
|
|
850
|
+
child.on("error", (error) => {
|
|
851
|
+
if (typeof log === "function") {
|
|
852
|
+
log(`${label} spawn failed: ${error.message}`);
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
child.unref();
|
|
856
|
+
return child;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function buildCodexSessionWatchLaunchArgs(watcherArgs) {
|
|
860
|
+
const extraArgs = Array.isArray(watcherArgs) ? watcherArgs : [];
|
|
861
|
+
return [process.execPath, path.resolve(__filename), "codex-session-watch", ...extraArgs];
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function getWindowsScriptHostPath() {
|
|
865
|
+
return path.join(process.env.SystemRoot || "C:\\Windows", "System32", "wscript.exe");
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function getHiddenLauncherScriptPath() {
|
|
869
|
+
return path.join(__dirname, "..", "scripts", "start-hidden.vbs");
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function acquireSingleInstanceLock(lockName, log) {
|
|
873
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
874
|
+
const lockPath = getSingleInstanceLockPath(lockName);
|
|
875
|
+
|
|
876
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
877
|
+
try {
|
|
878
|
+
const handle = fs.openSync(lockPath, "wx");
|
|
879
|
+
fs.writeFileSync(
|
|
880
|
+
handle,
|
|
881
|
+
JSON.stringify({
|
|
882
|
+
pid: process.pid,
|
|
883
|
+
startedAt: new Date().toISOString(),
|
|
884
|
+
}),
|
|
885
|
+
"utf8"
|
|
886
|
+
);
|
|
887
|
+
fs.closeSync(handle);
|
|
888
|
+
return { acquired: true, lockPath };
|
|
889
|
+
} catch (error) {
|
|
890
|
+
if (!error || error.code !== "EEXIST") {
|
|
891
|
+
throw error;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const existingPid = readLockPid(lockPath);
|
|
895
|
+
if (isProcessRunning(existingPid)) {
|
|
896
|
+
return { acquired: false, lockPath, existingPid };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
try {
|
|
900
|
+
fs.unlinkSync(lockPath);
|
|
901
|
+
if (typeof log === "function") {
|
|
902
|
+
log(`removed stale lock file=${lockPath} pid=${existingPid || "unknown"}`);
|
|
903
|
+
}
|
|
904
|
+
} catch {
|
|
905
|
+
return { acquired: false, lockPath, existingPid };
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return { acquired: false, lockPath };
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function querySingleInstanceLock(lockName) {
|
|
914
|
+
const lockPath = getSingleInstanceLockPath(lockName);
|
|
915
|
+
const pid = readLockPid(lockPath);
|
|
916
|
+
|
|
917
|
+
return {
|
|
918
|
+
lockPath,
|
|
919
|
+
pid,
|
|
920
|
+
running: isProcessRunning(pid),
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function getSingleInstanceLockPath(lockName) {
|
|
925
|
+
return path.join(LOG_DIR, `${lockName}.lock`);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function releaseSingleInstanceLock(lockInfo, log) {
|
|
929
|
+
if (!lockInfo || !lockInfo.lockPath || lockInfo.released) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const ownerPid = readLockPid(lockInfo.lockPath);
|
|
934
|
+
if (ownerPid && ownerPid !== process.pid) {
|
|
935
|
+
lockInfo.released = true;
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
try {
|
|
940
|
+
if (fs.existsSync(lockInfo.lockPath)) {
|
|
941
|
+
fs.unlinkSync(lockInfo.lockPath);
|
|
942
|
+
if (typeof log === "function") {
|
|
943
|
+
log(`released single-instance lock file=${lockInfo.lockPath}`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
} catch {}
|
|
947
|
+
|
|
948
|
+
lockInfo.released = true;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function readLockPid(lockPath) {
|
|
952
|
+
try {
|
|
953
|
+
const payload = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
954
|
+
const pid = parseInt(payload && payload.pid, 10);
|
|
955
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
956
|
+
} catch {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function isProcessRunning(pid) {
|
|
962
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
try {
|
|
967
|
+
process.kill(pid, 0);
|
|
968
|
+
return true;
|
|
969
|
+
} catch {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function getCodexHomeDir() {
|
|
975
|
+
return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function createNeutralTerminalContext() {
|
|
979
|
+
return {
|
|
980
|
+
hwnd: null,
|
|
981
|
+
shellPid: null,
|
|
982
|
+
isWindowsTerminal: false,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function stripUtf8Bom(value) {
|
|
987
|
+
return typeof value === "string" ? value.replace(/^\uFEFF/, "") : value;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function getExplicitShellPid(argv) {
|
|
991
|
+
const raw =
|
|
992
|
+
getArgValue(argv, "--shell-pid") ||
|
|
993
|
+
getEnvFirst(["TOAST_NOTIFY_SHELL_PID"]) ||
|
|
994
|
+
"";
|
|
995
|
+
const pid = parseInt(raw, 10);
|
|
996
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function fileExistsCaseInsensitive(targetPath) {
|
|
1000
|
+
try {
|
|
1001
|
+
return fs.existsSync(targetPath);
|
|
1002
|
+
} catch {
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function listRolloutFiles(rootDir, log) {
|
|
1008
|
+
if (!rootDir || !fileExistsCaseInsensitive(rootDir)) {
|
|
1009
|
+
return [];
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const files = [];
|
|
1013
|
+
const pendingDirs = [rootDir];
|
|
1014
|
+
|
|
1015
|
+
while (pendingDirs.length > 0) {
|
|
1016
|
+
const currentDir = pendingDirs.pop();
|
|
1017
|
+
let entries = [];
|
|
1018
|
+
|
|
1019
|
+
try {
|
|
1020
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
log(`readdir failed dir=${currentDir} error=${error.message}`);
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
entries.forEach((entry) => {
|
|
1027
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
1028
|
+
|
|
1029
|
+
if (entry.isDirectory()) {
|
|
1030
|
+
pendingDirs.push(fullPath);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
if (entry.isFile() && /^rollout-.*\.jsonl$/i.test(entry.name)) {
|
|
1035
|
+
files.push(fullPath);
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function createSessionFileState(filePath) {
|
|
1044
|
+
return {
|
|
1045
|
+
filePath,
|
|
1046
|
+
position: 0,
|
|
1047
|
+
partial: "",
|
|
1048
|
+
decoder: new StringDecoder("utf8"),
|
|
1049
|
+
sessionId: parseSessionIdFromRolloutPath(filePath),
|
|
1050
|
+
cwd: "",
|
|
1051
|
+
turnId: "",
|
|
1052
|
+
approvalPolicy: "",
|
|
1053
|
+
sandboxPolicy: null,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function createTailFileState(filePath) {
|
|
1058
|
+
return {
|
|
1059
|
+
filePath,
|
|
1060
|
+
position: 0,
|
|
1061
|
+
partial: "",
|
|
1062
|
+
decoder: new StringDecoder("utf8"),
|
|
1063
|
+
applyPatchCapture: null,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function bootstrapExistingSessionFileState(state, stat, log) {
|
|
1068
|
+
const metadata = readRolloutMetadata(state.filePath, log);
|
|
1069
|
+
|
|
1070
|
+
if (metadata.sessionId) {
|
|
1071
|
+
state.sessionId = metadata.sessionId;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (metadata.cwd) {
|
|
1075
|
+
state.cwd = metadata.cwd;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (metadata.approvalPolicy) {
|
|
1079
|
+
state.approvalPolicy = metadata.approvalPolicy;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (metadata.sandboxPolicy) {
|
|
1083
|
+
state.sandboxPolicy = metadata.sandboxPolicy;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
state.position = stat.size;
|
|
1087
|
+
state.partial = "";
|
|
1088
|
+
state.decoder = new StringDecoder("utf8");
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function bootstrapTailFileState(state, stat) {
|
|
1092
|
+
state.position = stat.size;
|
|
1093
|
+
state.partial = "";
|
|
1094
|
+
state.decoder = new StringDecoder("utf8");
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function parseSessionIdFromRolloutPath(filePath) {
|
|
1098
|
+
const match = path
|
|
1099
|
+
.basename(filePath)
|
|
1100
|
+
.match(/^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-(.+)\.jsonl$/i);
|
|
1101
|
+
return match ? match[1] : "";
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function readRolloutMetadata(filePath, log) {
|
|
1105
|
+
const result = {
|
|
1106
|
+
sessionId: parseSessionIdFromRolloutPath(filePath),
|
|
1107
|
+
cwd: "",
|
|
1108
|
+
approvalPolicy: "",
|
|
1109
|
+
sandboxPolicy: null,
|
|
1110
|
+
latestEventAtMs: 0,
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
try {
|
|
1114
|
+
const stat = fs.statSync(filePath);
|
|
1115
|
+
if (!stat.size) {
|
|
1116
|
+
return result;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const headBytesToRead = Math.min(stat.size, 65536);
|
|
1120
|
+
const headBuffer = readFileRange(filePath, 0, headBytesToRead);
|
|
1121
|
+
consumeRolloutMetadataChunk(result, headBuffer, false);
|
|
1122
|
+
|
|
1123
|
+
if (stat.size > headBytesToRead) {
|
|
1124
|
+
const tailBytesToRead = Math.min(stat.size, 262144);
|
|
1125
|
+
const tailBuffer = readFileRange(filePath, stat.size - tailBytesToRead, tailBytesToRead);
|
|
1126
|
+
consumeRolloutMetadataChunk(result, tailBuffer, true);
|
|
1127
|
+
}
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
log(`metadata read failed file=${filePath} error=${error.message}`);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return result;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function consumeRolloutMetadataChunk(result, buffer, preferLatestTurnContext) {
|
|
1136
|
+
const lines = buffer.toString("utf8").split(/\r?\n/);
|
|
1137
|
+
|
|
1138
|
+
for (const line of lines) {
|
|
1139
|
+
if (!line.trim()) {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
let record;
|
|
1144
|
+
try {
|
|
1145
|
+
record = JSON.parse(stripUtf8Bom(line));
|
|
1146
|
+
} catch {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const recordTimestampMs = Date.parse(record.timestamp || "");
|
|
1151
|
+
if (Number.isFinite(recordTimestampMs) && recordTimestampMs > result.latestEventAtMs) {
|
|
1152
|
+
result.latestEventAtMs = recordTimestampMs;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (record.type === "session_meta" && record.payload) {
|
|
1156
|
+
if (record.payload.id) {
|
|
1157
|
+
result.sessionId = record.payload.id;
|
|
1158
|
+
}
|
|
1159
|
+
if (!result.cwd && record.payload.cwd) {
|
|
1160
|
+
result.cwd = record.payload.cwd;
|
|
1161
|
+
}
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (record.type !== "turn_context" || !record.payload) {
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (record.payload.cwd && (preferLatestTurnContext || !result.cwd)) {
|
|
1170
|
+
result.cwd = record.payload.cwd;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (record.payload.approval_policy && (preferLatestTurnContext || !result.approvalPolicy)) {
|
|
1174
|
+
result.approvalPolicy = record.payload.approval_policy;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (record.payload.sandbox_policy && (preferLatestTurnContext || !result.sandboxPolicy)) {
|
|
1178
|
+
result.sandboxPolicy = record.payload.sandbox_policy;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function readFileRange(filePath, start, length) {
|
|
1184
|
+
const buffer = Buffer.alloc(length);
|
|
1185
|
+
const fd = fs.openSync(filePath, "r");
|
|
1186
|
+
|
|
1187
|
+
try {
|
|
1188
|
+
fs.readSync(fd, buffer, 0, length, start);
|
|
1189
|
+
} finally {
|
|
1190
|
+
fs.closeSync(fd);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
return buffer;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function consumeSessionFileUpdates(
|
|
1197
|
+
state,
|
|
1198
|
+
stat,
|
|
1199
|
+
{
|
|
1200
|
+
runtime,
|
|
1201
|
+
terminal,
|
|
1202
|
+
emittedEventKeys,
|
|
1203
|
+
pendingApprovalNotifications,
|
|
1204
|
+
pendingApprovalCallIds,
|
|
1205
|
+
recentRequireEscalatedEvents,
|
|
1206
|
+
sessionApprovalGrants,
|
|
1207
|
+
approvedCommandRuleCache,
|
|
1208
|
+
}
|
|
1209
|
+
) {
|
|
1210
|
+
if (stat.size < state.position) {
|
|
1211
|
+
runtime.log(`session file truncated file=${state.filePath}`);
|
|
1212
|
+
state.position = 0;
|
|
1213
|
+
state.partial = "";
|
|
1214
|
+
state.decoder = new StringDecoder("utf8");
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (stat.size === state.position) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const chunk = readFileRange(state.filePath, state.position, stat.size - state.position);
|
|
1222
|
+
state.position = stat.size;
|
|
1223
|
+
|
|
1224
|
+
const text = state.partial + state.decoder.write(chunk);
|
|
1225
|
+
const lines = text.split(/\r?\n/);
|
|
1226
|
+
state.partial = lines.pop() || "";
|
|
1227
|
+
|
|
1228
|
+
lines.forEach((line) => {
|
|
1229
|
+
if (!line.trim()) {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
handleSessionRecord(state, line, {
|
|
1234
|
+
runtime,
|
|
1235
|
+
terminal,
|
|
1236
|
+
emittedEventKeys,
|
|
1237
|
+
pendingApprovalNotifications,
|
|
1238
|
+
pendingApprovalCallIds,
|
|
1239
|
+
recentRequireEscalatedEvents,
|
|
1240
|
+
sessionApprovalGrants,
|
|
1241
|
+
approvedCommandRuleCache,
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function syncCodexTuiLogState(state, tuiLogPath, context) {
|
|
1247
|
+
if (!tuiLogPath || !fileExistsCaseInsensitive(tuiLogPath)) {
|
|
1248
|
+
return state;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
let stat;
|
|
1252
|
+
try {
|
|
1253
|
+
stat = fs.statSync(tuiLogPath);
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
context.runtime.log(`tui log stat failed file=${tuiLogPath} error=${error.message}`);
|
|
1256
|
+
return state;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
let nextState = state;
|
|
1260
|
+
if (!nextState || nextState.filePath !== tuiLogPath) {
|
|
1261
|
+
nextState = createTailFileState(tuiLogPath);
|
|
1262
|
+
if (context.initialScan) {
|
|
1263
|
+
bootstrapTailFileState(nextState, stat);
|
|
1264
|
+
}
|
|
1265
|
+
context.runtime.log(`tracking tui log file=${tuiLogPath} position=${nextState.position}`);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
consumeCodexTuiLogUpdates(nextState, stat, context);
|
|
1269
|
+
return nextState;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function consumeCodexTuiLogUpdates(
|
|
1273
|
+
state,
|
|
1274
|
+
stat,
|
|
1275
|
+
{
|
|
1276
|
+
runtime,
|
|
1277
|
+
terminal,
|
|
1278
|
+
emittedEventKeys,
|
|
1279
|
+
sessionProjectDirs,
|
|
1280
|
+
sessionApprovalContexts,
|
|
1281
|
+
pendingApprovalNotifications,
|
|
1282
|
+
pendingApprovalCallIds,
|
|
1283
|
+
recentRequireEscalatedEvents,
|
|
1284
|
+
sessionApprovalGrants,
|
|
1285
|
+
approvedCommandRuleCache,
|
|
1286
|
+
}
|
|
1287
|
+
) {
|
|
1288
|
+
if (stat.size < state.position) {
|
|
1289
|
+
runtime.log(`tui log truncated file=${state.filePath}`);
|
|
1290
|
+
state.position = 0;
|
|
1291
|
+
state.partial = "";
|
|
1292
|
+
state.decoder = new StringDecoder("utf8");
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (stat.size === state.position) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const chunk = readFileRange(state.filePath, state.position, stat.size - state.position);
|
|
1300
|
+
state.position = stat.size;
|
|
1301
|
+
|
|
1302
|
+
const text = state.partial + state.decoder.write(chunk);
|
|
1303
|
+
const lines = text.split(/\r?\n/);
|
|
1304
|
+
state.partial = lines.pop() || "";
|
|
1305
|
+
|
|
1306
|
+
lines.forEach((line) => {
|
|
1307
|
+
handleCodexTuiLogLine(state, line, {
|
|
1308
|
+
runtime,
|
|
1309
|
+
terminal,
|
|
1310
|
+
emittedEventKeys,
|
|
1311
|
+
sessionProjectDirs,
|
|
1312
|
+
sessionApprovalContexts,
|
|
1313
|
+
pendingApprovalNotifications,
|
|
1314
|
+
pendingApprovalCallIds,
|
|
1315
|
+
recentRequireEscalatedEvents,
|
|
1316
|
+
sessionApprovalGrants,
|
|
1317
|
+
approvedCommandRuleCache,
|
|
1318
|
+
});
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function parseJsonObjectMaybe(value) {
|
|
1323
|
+
if (!value) {
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
if (typeof value === "string") {
|
|
1328
|
+
try {
|
|
1329
|
+
const parsed = JSON.parse(value);
|
|
1330
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
1331
|
+
} catch {
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function getCodexExecApprovalDescriptor(toolName, args) {
|
|
1340
|
+
const command = typeof args.command === "string" ? args.command.trim() : "";
|
|
1341
|
+
if (command) {
|
|
1342
|
+
return `${toolName || "tool"}:${command}`;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
return toolName || "tool";
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function buildApprovalDedupeKey({
|
|
1349
|
+
sessionId,
|
|
1350
|
+
turnId,
|
|
1351
|
+
callId,
|
|
1352
|
+
approvalId,
|
|
1353
|
+
fallbackId,
|
|
1354
|
+
approvalKind,
|
|
1355
|
+
descriptor,
|
|
1356
|
+
}) {
|
|
1357
|
+
return [
|
|
1358
|
+
sessionId || "unknown",
|
|
1359
|
+
approvalKind || "permission",
|
|
1360
|
+
turnId || approvalId || callId || fallbackId || "unknown",
|
|
1361
|
+
descriptor || "",
|
|
1362
|
+
].join("|");
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function emitCodexApprovalNotification({ event, runtime, terminal, emittedEventKeys, origin }) {
|
|
1366
|
+
if (!shouldEmitEventKey(emittedEventKeys, event.dedupeKey)) {
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
runtime.log(
|
|
1371
|
+
`${origin} event matched type=${event.eventType} sessionId=${event.sessionId || "unknown"} turnId=${event.turnId || ""} cwd=${event.projectDir || ""}`
|
|
1372
|
+
);
|
|
1373
|
+
|
|
1374
|
+
const notificationTerminal = resolveApprovalTerminalContext({
|
|
1375
|
+
sessionId: event.sessionId,
|
|
1376
|
+
projectDir: event.projectDir,
|
|
1377
|
+
fallbackTerminal: terminal,
|
|
1378
|
+
log: runtime.log,
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
const child = emitNotification({
|
|
1382
|
+
source: event.source,
|
|
1383
|
+
eventName: event.eventName,
|
|
1384
|
+
title: event.title,
|
|
1385
|
+
message: event.message,
|
|
1386
|
+
rawEventType: event.eventType,
|
|
1387
|
+
runtime,
|
|
1388
|
+
terminal: notificationTerminal,
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
child.on("close", (code) => {
|
|
1392
|
+
runtime.log(
|
|
1393
|
+
`notify.ps1 exited code=${code} sessionId=${event.sessionId || "unknown"} eventType=${event.eventType}`
|
|
1394
|
+
);
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
child.on("error", (error) => {
|
|
1398
|
+
runtime.log(
|
|
1399
|
+
`notify.ps1 spawn failed sessionId=${event.sessionId || "unknown"} eventType=${event.eventType} error=${error.message}`
|
|
1400
|
+
);
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function queuePendingApprovalNotification({
|
|
1407
|
+
runtime,
|
|
1408
|
+
pendingApprovalNotifications,
|
|
1409
|
+
pendingApprovalCallIds,
|
|
1410
|
+
emittedEventKeys,
|
|
1411
|
+
event,
|
|
1412
|
+
}) {
|
|
1413
|
+
const key = event.dedupeKey || `${event.sessionId || "unknown"}|${event.turnId || "unknown"}`;
|
|
1414
|
+
if (key && emittedEventKeys && emittedEventKeys.has(key)) {
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const existing = pendingApprovalNotifications.get(key);
|
|
1418
|
+
|
|
1419
|
+
if (existing) {
|
|
1420
|
+
if (!existing.callId && event.callId) {
|
|
1421
|
+
existing.callId = event.callId;
|
|
1422
|
+
pendingApprovalCallIds.set(event.callId, key);
|
|
1423
|
+
}
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const graceMs = getCodexApprovalNotifyGraceMs(event);
|
|
1428
|
+
const pending = {
|
|
1429
|
+
...event,
|
|
1430
|
+
pendingSinceMs: Date.now(),
|
|
1431
|
+
deadlineMs: Date.now() + graceMs,
|
|
1432
|
+
graceMs,
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
pendingApprovalNotifications.set(key, pending);
|
|
1436
|
+
if (pending.callId) {
|
|
1437
|
+
pendingApprovalCallIds.set(pending.callId, key);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
runtime.log(
|
|
1441
|
+
`queued approval pending sessionId=${pending.sessionId || "unknown"} turnId=${pending.turnId || ""} callId=${pending.callId || ""} graceMs=${graceMs} deadlineMs=${pending.deadlineMs}`
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function cancelPendingApprovalNotification({
|
|
1446
|
+
runtime,
|
|
1447
|
+
pendingApprovalNotifications,
|
|
1448
|
+
pendingApprovalCallIds,
|
|
1449
|
+
callId,
|
|
1450
|
+
reason,
|
|
1451
|
+
}) {
|
|
1452
|
+
if (!callId) {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
const key = pendingApprovalCallIds.get(callId);
|
|
1457
|
+
if (!key) {
|
|
1458
|
+
return false;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
return cancelPendingApprovalNotificationByKey({
|
|
1462
|
+
runtime,
|
|
1463
|
+
pendingApprovalNotifications,
|
|
1464
|
+
pendingApprovalCallIds,
|
|
1465
|
+
key,
|
|
1466
|
+
reason,
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function cancelPendingApprovalNotificationByKey({
|
|
1471
|
+
runtime,
|
|
1472
|
+
pendingApprovalNotifications,
|
|
1473
|
+
pendingApprovalCallIds,
|
|
1474
|
+
key,
|
|
1475
|
+
reason,
|
|
1476
|
+
}) {
|
|
1477
|
+
if (!key) {
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const pending = pendingApprovalNotifications.get(key);
|
|
1482
|
+
if (!pending) {
|
|
1483
|
+
pendingApprovalCallIds.forEach((mappedKey, mappedCallId) => {
|
|
1484
|
+
if (mappedKey === key) {
|
|
1485
|
+
pendingApprovalCallIds.delete(mappedCallId);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
return false;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
pendingApprovalNotifications.delete(key);
|
|
1492
|
+
if (pending.callId) {
|
|
1493
|
+
pendingApprovalCallIds.delete(pending.callId);
|
|
1494
|
+
}
|
|
1495
|
+
runtime.log(
|
|
1496
|
+
`cancelled approval pending sessionId=${pending.sessionId || "unknown"} turnId=${pending.turnId || ""} callId=${pending.callId || ""} reason=${reason || "unknown"}`
|
|
1497
|
+
);
|
|
1498
|
+
return true;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function cancelPendingApprovalNotificationsBySuppression({
|
|
1502
|
+
runtime,
|
|
1503
|
+
pendingApprovalNotifications,
|
|
1504
|
+
pendingApprovalCallIds,
|
|
1505
|
+
sessionId,
|
|
1506
|
+
turnId = "",
|
|
1507
|
+
approvalPolicy = "",
|
|
1508
|
+
sandboxPolicy = null,
|
|
1509
|
+
approvedCommandRules = [],
|
|
1510
|
+
sessionApprovalGrants,
|
|
1511
|
+
nowMs = Date.now(),
|
|
1512
|
+
}) {
|
|
1513
|
+
if (!runtime || !pendingApprovalNotifications || !sessionId) {
|
|
1514
|
+
return 0;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
let cancelled = 0;
|
|
1518
|
+
Array.from(pendingApprovalNotifications.entries()).forEach(([key, pending]) => {
|
|
1519
|
+
if (!pending || pending.sessionId !== sessionId) {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
if (turnId && pending.turnId && pending.turnId !== turnId) {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const suppressionReason =
|
|
1527
|
+
getCodexRequireEscalatedSuppressionReason({
|
|
1528
|
+
event: pending,
|
|
1529
|
+
approvalPolicy,
|
|
1530
|
+
sandboxPolicy,
|
|
1531
|
+
approvedCommandRules,
|
|
1532
|
+
}) ||
|
|
1533
|
+
getSessionRequireEscalatedSuppressionReason({
|
|
1534
|
+
event: pending,
|
|
1535
|
+
nowMs,
|
|
1536
|
+
sessionApprovalGrants,
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
if (!suppressionReason) {
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
if (
|
|
1544
|
+
cancelPendingApprovalNotificationByKey({
|
|
1545
|
+
runtime,
|
|
1546
|
+
pendingApprovalNotifications,
|
|
1547
|
+
pendingApprovalCallIds,
|
|
1548
|
+
key,
|
|
1549
|
+
reason: suppressionReason,
|
|
1550
|
+
})
|
|
1551
|
+
) {
|
|
1552
|
+
cancelled += 1;
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
return cancelled;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function buildPendingApprovalBatchKey(event) {
|
|
1560
|
+
if (!event) {
|
|
1561
|
+
return "";
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (event.eventType === "require_escalated_tool_call") {
|
|
1565
|
+
return [event.sessionId || "unknown", event.turnId || "unknown", event.eventType].join("|");
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
return event.dedupeKey || [event.sessionId || "unknown", event.turnId || "unknown", event.eventType || ""].join("|");
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function shouldBatchPendingApproval(representative, pending) {
|
|
1572
|
+
if (!representative || !pending) {
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
if (buildPendingApprovalBatchKey(representative) !== buildPendingApprovalBatchKey(pending)) {
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
if (representative.eventType !== "require_escalated_tool_call") {
|
|
1581
|
+
return representative.dedupeKey === pending.dedupeKey;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
const representativePendingSince = Number.isFinite(representative.pendingSinceMs)
|
|
1585
|
+
? representative.pendingSinceMs
|
|
1586
|
+
: 0;
|
|
1587
|
+
const pendingSince = Number.isFinite(pending.pendingSinceMs)
|
|
1588
|
+
? pending.pendingSinceMs
|
|
1589
|
+
: representativePendingSince;
|
|
1590
|
+
|
|
1591
|
+
return Math.abs(pendingSince - representativePendingSince) <= CODEX_APPROVAL_BATCH_WINDOW_MS;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function drainPendingApprovalBatch({
|
|
1595
|
+
pendingApprovalNotifications,
|
|
1596
|
+
pendingApprovalCallIds,
|
|
1597
|
+
representativeKey,
|
|
1598
|
+
}) {
|
|
1599
|
+
if (!pendingApprovalNotifications || !representativeKey) {
|
|
1600
|
+
return { batchKey: "", count: 0, representative: null };
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const representative = pendingApprovalNotifications.get(representativeKey);
|
|
1604
|
+
if (!representative) {
|
|
1605
|
+
return { batchKey: "", count: 0, representative: null };
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const batchKey = buildPendingApprovalBatchKey(representative);
|
|
1609
|
+
const removed = [];
|
|
1610
|
+
|
|
1611
|
+
Array.from(pendingApprovalNotifications.entries()).forEach(([key, pending]) => {
|
|
1612
|
+
if (!shouldBatchPendingApproval(representative, pending)) {
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
pendingApprovalNotifications.delete(key);
|
|
1617
|
+
if (pending.callId) {
|
|
1618
|
+
pendingApprovalCallIds.delete(pending.callId);
|
|
1619
|
+
}
|
|
1620
|
+
removed.push({ key, pending });
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
return {
|
|
1624
|
+
batchKey,
|
|
1625
|
+
count: removed.length,
|
|
1626
|
+
representative,
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function flushPendingApprovalNotifications({
|
|
1631
|
+
runtime,
|
|
1632
|
+
terminal,
|
|
1633
|
+
emittedEventKeys,
|
|
1634
|
+
pendingApprovalNotifications,
|
|
1635
|
+
pendingApprovalCallIds,
|
|
1636
|
+
}) {
|
|
1637
|
+
const now = Date.now();
|
|
1638
|
+
Array.from(pendingApprovalNotifications.entries()).forEach(([key, pending]) => {
|
|
1639
|
+
if (!pendingApprovalNotifications.has(key)) {
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
if (pending.deadlineMs > now) {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
const batch = drainPendingApprovalBatch({
|
|
1647
|
+
pendingApprovalNotifications,
|
|
1648
|
+
pendingApprovalCallIds,
|
|
1649
|
+
representativeKey: key,
|
|
1650
|
+
});
|
|
1651
|
+
if (!batch.representative) {
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (batch.count > 1) {
|
|
1656
|
+
runtime.log(
|
|
1657
|
+
`grouped approval batch sessionId=${batch.representative.sessionId || "unknown"} turnId=${batch.representative.turnId || ""} batchSize=${batch.count}`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
emitCodexApprovalNotification({
|
|
1662
|
+
event: batch.representative,
|
|
1663
|
+
runtime,
|
|
1664
|
+
terminal,
|
|
1665
|
+
emittedEventKeys,
|
|
1666
|
+
origin: "pending",
|
|
1667
|
+
});
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function createApprovedCommandRuleCache(filePath) {
|
|
1672
|
+
return {
|
|
1673
|
+
filePath,
|
|
1674
|
+
mtimeMs: -1,
|
|
1675
|
+
size: -1,
|
|
1676
|
+
rules: [],
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function getApprovedCommandRules(cache, log) {
|
|
1681
|
+
if (!cache || !cache.filePath || !fileExistsCaseInsensitive(cache.filePath)) {
|
|
1682
|
+
return [];
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
let stat;
|
|
1686
|
+
try {
|
|
1687
|
+
stat = fs.statSync(cache.filePath);
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
log(`approved rules stat failed file=${cache.filePath} error=${error.message}`);
|
|
1690
|
+
return cache.rules || [];
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
if (cache.mtimeMs === stat.mtimeMs && cache.size === stat.size && Array.isArray(cache.rules)) {
|
|
1694
|
+
return cache.rules;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
try {
|
|
1698
|
+
const content = fs.readFileSync(cache.filePath, "utf8");
|
|
1699
|
+
cache.rules = parseApprovedCommandRules(content);
|
|
1700
|
+
cache.mtimeMs = stat.mtimeMs;
|
|
1701
|
+
cache.size = stat.size;
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
log(`approved rules read failed file=${cache.filePath} error=${error.message}`);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
return cache.rules || [];
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function parseApprovedCommandRules(content) {
|
|
1710
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
1711
|
+
const rules = [];
|
|
1712
|
+
|
|
1713
|
+
lines.forEach((line) => {
|
|
1714
|
+
if (!line.includes('decision="allow"') || !line.includes("prefix_rule(")) {
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
const match = line.match(/prefix_rule\(pattern=(\[[\s\S]*\]), decision="allow"\)\s*$/);
|
|
1719
|
+
if (!match) {
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
let pattern;
|
|
1724
|
+
try {
|
|
1725
|
+
pattern = JSON.parse(match[1]);
|
|
1726
|
+
} catch {
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
if (!Array.isArray(pattern) || !pattern.every((value) => typeof value === "string")) {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const shellCommand = extractApprovedRuleShellCommand(pattern);
|
|
1735
|
+
rules.push({
|
|
1736
|
+
pattern,
|
|
1737
|
+
shellCommand,
|
|
1738
|
+
shellCommandTokens: shellCommand ? extractLeadingCommandTokens(shellCommand) : [],
|
|
1739
|
+
});
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
return rules;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function extractApprovedRuleShellCommand(pattern) {
|
|
1746
|
+
if (!Array.isArray(pattern) || pattern.length < 3) {
|
|
1747
|
+
return "";
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
const exeName = path.basename(pattern[0] || "").toLowerCase();
|
|
1751
|
+
const arg1 = String(pattern[1] || "").toLowerCase();
|
|
1752
|
+
if ((exeName === "powershell.exe" || exeName === "powershell" || exeName === "pwsh.exe" || exeName === "pwsh") && arg1 === "-command") {
|
|
1753
|
+
return String(pattern[2] || "").trim();
|
|
1754
|
+
}
|
|
1755
|
+
if ((exeName === "cmd.exe" || exeName === "cmd") && arg1 === "/c") {
|
|
1756
|
+
return String(pattern[2] || "").trim();
|
|
1757
|
+
}
|
|
1758
|
+
return "";
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function getCodexApprovalNotifyGraceMs(event) {
|
|
1762
|
+
if (
|
|
1763
|
+
event &&
|
|
1764
|
+
event.eventType === "require_escalated_tool_call" &&
|
|
1765
|
+
isLikelyReadOnlyShellCommand(event.toolArgs)
|
|
1766
|
+
) {
|
|
1767
|
+
return CODEX_READ_ONLY_APPROVAL_NOTIFY_GRACE_MS;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
return CODEX_APPROVAL_NOTIFY_GRACE_MS;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function getCodexRequireEscalatedSuppressionReason({
|
|
1774
|
+
event,
|
|
1775
|
+
approvalPolicy,
|
|
1776
|
+
sandboxPolicy,
|
|
1777
|
+
approvedCommandRules,
|
|
1778
|
+
}) {
|
|
1779
|
+
if (!event || event.eventType !== "require_escalated_tool_call" || !event.toolArgs) {
|
|
1780
|
+
return "";
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (approvalPolicy === "never") {
|
|
1784
|
+
return "approval_policy_never";
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (sandboxPolicy && sandboxPolicy.type === "danger-full-access") {
|
|
1788
|
+
return "danger_full_access";
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
if (
|
|
1792
|
+
isLikelyReadOnlyShellCommand(event.toolArgs) &&
|
|
1793
|
+
matchesApprovedCommandRule(event.toolArgs, approvedCommandRules)
|
|
1794
|
+
) {
|
|
1795
|
+
return "approved_rule";
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
return "";
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function pruneRecentRequireEscalatedEvents(recentRequireEscalatedEvents, sessionId, nowMs = Date.now()) {
|
|
1802
|
+
if (!recentRequireEscalatedEvents || !sessionId) {
|
|
1803
|
+
return [];
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const recent = recentRequireEscalatedEvents.get(sessionId);
|
|
1807
|
+
if (!Array.isArray(recent) || !recent.length) {
|
|
1808
|
+
recentRequireEscalatedEvents.delete(sessionId);
|
|
1809
|
+
return [];
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
const next = recent.filter(
|
|
1813
|
+
(item) => item && typeof item.seenAtMs === "number" && item.seenAtMs + RECENT_REQUIRE_ESCALATED_TTL_MS >= nowMs
|
|
1814
|
+
);
|
|
1815
|
+
if (next.length) {
|
|
1816
|
+
recentRequireEscalatedEvents.set(sessionId, next);
|
|
1817
|
+
} else {
|
|
1818
|
+
recentRequireEscalatedEvents.delete(sessionId);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
return next;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function rememberRecentRequireEscalatedEvent(recentRequireEscalatedEvents, event, nowMs = Date.now()) {
|
|
1825
|
+
if (
|
|
1826
|
+
!recentRequireEscalatedEvents ||
|
|
1827
|
+
!event ||
|
|
1828
|
+
event.eventType !== "require_escalated_tool_call" ||
|
|
1829
|
+
!event.sessionId ||
|
|
1830
|
+
!event.toolArgs
|
|
1831
|
+
) {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const sessionId = event.sessionId;
|
|
1836
|
+
const recent = pruneRecentRequireEscalatedEvents(recentRequireEscalatedEvents, sessionId, nowMs).filter(
|
|
1837
|
+
(item) => item.dedupeKey !== event.dedupeKey
|
|
1838
|
+
);
|
|
1839
|
+
|
|
1840
|
+
recent.push({
|
|
1841
|
+
dedupeKey: event.dedupeKey || "",
|
|
1842
|
+
projectDir: event.projectDir || "",
|
|
1843
|
+
sessionId,
|
|
1844
|
+
seenAtMs: nowMs,
|
|
1845
|
+
toolArgs: event.toolArgs,
|
|
1846
|
+
turnId: event.turnId || "",
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
while (recent.length > MAX_RECENT_REQUIRE_ESCALATED_EVENTS_PER_SESSION) {
|
|
1850
|
+
recent.shift();
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
recentRequireEscalatedEvents.set(sessionId, recent);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
function pruneSessionApprovalGrants(sessionApprovalGrants, sessionId, nowMs = Date.now()) {
|
|
1857
|
+
if (!sessionApprovalGrants || !sessionId) {
|
|
1858
|
+
return [];
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
const grants = sessionApprovalGrants.get(sessionId);
|
|
1862
|
+
if (!Array.isArray(grants) || !grants.length) {
|
|
1863
|
+
sessionApprovalGrants.delete(sessionId);
|
|
1864
|
+
return [];
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
const next = grants.filter(
|
|
1868
|
+
(item) => item && typeof item.confirmedAtMs === "number" && item.confirmedAtMs + SESSION_APPROVAL_GRANT_TTL_MS >= nowMs
|
|
1869
|
+
);
|
|
1870
|
+
if (next.length) {
|
|
1871
|
+
sessionApprovalGrants.set(sessionId, next);
|
|
1872
|
+
} else {
|
|
1873
|
+
sessionApprovalGrants.delete(sessionId);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
return next;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function rememberSessionApprovalRoots(
|
|
1880
|
+
sessionApprovalGrants,
|
|
1881
|
+
sessionId,
|
|
1882
|
+
roots,
|
|
1883
|
+
{ confirmedAtMs = Date.now(), source = "", turnId = "" } = {}
|
|
1884
|
+
) {
|
|
1885
|
+
if (!sessionApprovalGrants || !sessionId || !Array.isArray(roots) || !roots.length) {
|
|
1886
|
+
return 0;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
const grants = pruneSessionApprovalGrants(sessionApprovalGrants, sessionId, confirmedAtMs);
|
|
1890
|
+
let added = 0;
|
|
1891
|
+
|
|
1892
|
+
roots.forEach((root) => {
|
|
1893
|
+
const normalizedRoot = normalizeShellCommandPath(root);
|
|
1894
|
+
if (!normalizedRoot) {
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
const existing = grants.find((item) => item.root === normalizedRoot);
|
|
1899
|
+
if (existing) {
|
|
1900
|
+
existing.confirmedAtMs = confirmedAtMs;
|
|
1901
|
+
existing.source = source || existing.source || "";
|
|
1902
|
+
existing.turnId = turnId || existing.turnId || "";
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
grants.push({
|
|
1907
|
+
confirmedAtMs,
|
|
1908
|
+
root: normalizedRoot,
|
|
1909
|
+
source,
|
|
1910
|
+
turnId,
|
|
1911
|
+
});
|
|
1912
|
+
added += 1;
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
while (grants.length > MAX_SESSION_APPROVAL_GRANTS_PER_SESSION) {
|
|
1916
|
+
grants.shift();
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
if (grants.length) {
|
|
1920
|
+
sessionApprovalGrants.set(sessionId, grants);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
return added;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function confirmSessionApprovalForRecentEvents({
|
|
1927
|
+
recentRequireEscalatedEvents,
|
|
1928
|
+
runtime,
|
|
1929
|
+
sessionApprovalGrants,
|
|
1930
|
+
sessionId,
|
|
1931
|
+
source,
|
|
1932
|
+
turnId,
|
|
1933
|
+
nowMs = Date.now(),
|
|
1934
|
+
}) {
|
|
1935
|
+
if (!sessionId || !recentRequireEscalatedEvents || !sessionApprovalGrants) {
|
|
1936
|
+
return 0;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
const recent = pruneRecentRequireEscalatedEvents(recentRequireEscalatedEvents, sessionId, nowMs);
|
|
1940
|
+
if (!recent.length) {
|
|
1941
|
+
return 0;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
const roots = Array.from(
|
|
1945
|
+
new Set(
|
|
1946
|
+
recent
|
|
1947
|
+
.filter(
|
|
1948
|
+
(item) =>
|
|
1949
|
+
item &&
|
|
1950
|
+
item.seenAtMs + SESSION_APPROVAL_CONFIRM_LOOKBACK_MS >= nowMs &&
|
|
1951
|
+
(!turnId || !item.turnId || item.turnId === turnId)
|
|
1952
|
+
)
|
|
1953
|
+
.flatMap((item) => extractCommandApprovalRoots(item.toolArgs))
|
|
1954
|
+
)
|
|
1955
|
+
);
|
|
1956
|
+
|
|
1957
|
+
const added = rememberSessionApprovalRoots(sessionApprovalGrants, sessionId, roots, {
|
|
1958
|
+
confirmedAtMs: nowMs,
|
|
1959
|
+
source,
|
|
1960
|
+
turnId,
|
|
1961
|
+
});
|
|
1962
|
+
|
|
1963
|
+
if (added > 0 && runtime && typeof runtime.log === "function") {
|
|
1964
|
+
runtime.log(
|
|
1965
|
+
`confirmed session approval sessionId=${sessionId} turnId=${turnId || ""} source=${source || ""} roots=${roots.join(";")}`
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
return added;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
function getSessionRequireEscalatedSuppressionReason({
|
|
1973
|
+
event,
|
|
1974
|
+
nowMs = Date.now(),
|
|
1975
|
+
sessionApprovalGrants,
|
|
1976
|
+
}) {
|
|
1977
|
+
if (
|
|
1978
|
+
!event ||
|
|
1979
|
+
event.eventType !== "require_escalated_tool_call" ||
|
|
1980
|
+
!event.sessionId ||
|
|
1981
|
+
!event.toolArgs ||
|
|
1982
|
+
!isLikelyReadOnlyShellCommand(event.toolArgs)
|
|
1983
|
+
) {
|
|
1984
|
+
return "";
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
const grants = pruneSessionApprovalGrants(sessionApprovalGrants, event.sessionId, nowMs);
|
|
1988
|
+
if (!grants.length) {
|
|
1989
|
+
return "";
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
const roots = extractCommandApprovalRoots(event.toolArgs);
|
|
1993
|
+
if (!roots.length) {
|
|
1994
|
+
return "";
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
const matched = roots.some((root) => grants.some((grant) => isPathWithinRoot(root, grant.root)));
|
|
1998
|
+
return matched ? "session_recent_read_grant" : "";
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
function matchesApprovedCommandRule(args, approvedCommandRules) {
|
|
2002
|
+
if (!args || !Array.isArray(approvedCommandRules) || !approvedCommandRules.length) {
|
|
2003
|
+
return false;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
const normalizedCommand = normalizeShellCommandForMatch(args.command);
|
|
2007
|
+
const normalizedPrefixRule = normalizePrefixRule(args.prefix_rule);
|
|
2008
|
+
|
|
2009
|
+
return approvedCommandRules.some((rule) => {
|
|
2010
|
+
if (!rule || !rule.shellCommand) {
|
|
2011
|
+
return false;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
const normalizedRuleCommand = normalizeShellCommandForMatch(rule.shellCommand);
|
|
2015
|
+
if (normalizedCommand && normalizedRuleCommand && normalizedCommand === normalizedRuleCommand) {
|
|
2016
|
+
return true;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
if (normalizedPrefixRule.length && arrayStartsWith(rule.shellCommandTokens || [], normalizedPrefixRule)) {
|
|
2020
|
+
return true;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
return false;
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
function normalizeShellCommandForMatch(command) {
|
|
2028
|
+
return String(command || "").trim().replace(/\s+/g, " ").toLowerCase();
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function normalizePrefixRule(prefixRule) {
|
|
2032
|
+
if (!Array.isArray(prefixRule)) {
|
|
2033
|
+
return [];
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
return prefixRule
|
|
2037
|
+
.filter((value) => typeof value === "string")
|
|
2038
|
+
.map((value) => stripMatchingQuotes(String(value).trim()).toLowerCase())
|
|
2039
|
+
.filter(Boolean);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function isLikelyReadOnlyShellCommand(args) {
|
|
2043
|
+
if (!args || typeof args.command !== "string") {
|
|
2044
|
+
return false;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
const tokens = extractLeadingCommandTokens(args.command);
|
|
2048
|
+
if (!tokens.length) {
|
|
2049
|
+
return false;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
const command = tokens[0];
|
|
2053
|
+
if (
|
|
2054
|
+
new Set([
|
|
2055
|
+
"cat",
|
|
2056
|
+
"dir",
|
|
2057
|
+
"findstr",
|
|
2058
|
+
"get-childitem",
|
|
2059
|
+
"get-content",
|
|
2060
|
+
"ls",
|
|
2061
|
+
"rg",
|
|
2062
|
+
"select-string",
|
|
2063
|
+
"type",
|
|
2064
|
+
]).has(command)
|
|
2065
|
+
) {
|
|
2066
|
+
return true;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
if (command === "git") {
|
|
2070
|
+
return new Set(["branch", "diff", "log", "remote", "rev-parse", "show", "status"]).has(tokens[1] || "");
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
if (command === "node") {
|
|
2074
|
+
return tokens[1] === "-c";
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
return false;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
function extractCommandApprovalRoots(args) {
|
|
2081
|
+
if (!args || typeof args.command !== "string") {
|
|
2082
|
+
return [];
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
const workdir = normalizeShellCommandPath(args.workdir);
|
|
2086
|
+
const roots = new Set();
|
|
2087
|
+
const absolutePathPattern = /[A-Za-z]:[\\/][^"'`\r\n|;]+/g;
|
|
2088
|
+
|
|
2089
|
+
const pushRoot = (value) => {
|
|
2090
|
+
const normalized = normalizeShellCommandPath(value);
|
|
2091
|
+
if (!normalized) {
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
let root = normalized;
|
|
2096
|
+
const fsPath = normalized.replace(/\//g, path.sep);
|
|
2097
|
+
if (path.extname(fsPath)) {
|
|
2098
|
+
root = normalizeShellCommandPath(findCommandApprovalRootPath(path.dirname(fsPath)));
|
|
2099
|
+
} else {
|
|
2100
|
+
root = normalizeShellCommandPath(findCommandApprovalRootPath(fsPath));
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
if (root) {
|
|
2104
|
+
roots.add(root);
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
|
|
2108
|
+
let match;
|
|
2109
|
+
while ((match = absolutePathPattern.exec(args.command)) !== null) {
|
|
2110
|
+
pushRoot(match[0]);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
tokenizeShellCommand(args.command).forEach((token) => {
|
|
2114
|
+
const candidate = normalizePathCandidate(token);
|
|
2115
|
+
if (!candidate) {
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
if (isWindowsAbsolutePath(candidate)) {
|
|
2120
|
+
pushRoot(candidate);
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
if (!workdir || !looksLikeRelativePathCandidate(candidate)) {
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
pushRoot(path.resolve(workdir.replace(/\//g, path.sep), candidate));
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
return Array.from(roots);
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
function extractLeadingCommandTokens(command) {
|
|
2135
|
+
const tokens = tokenizeShellCommand(command);
|
|
2136
|
+
const operators = new Set(["|", ";", "&&", "||"]);
|
|
2137
|
+
const result = [];
|
|
2138
|
+
let seenCommand = false;
|
|
2139
|
+
|
|
2140
|
+
for (const token of tokens) {
|
|
2141
|
+
if (!token) {
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
if (!seenCommand) {
|
|
2146
|
+
if (operators.has(token)) {
|
|
2147
|
+
continue;
|
|
2148
|
+
}
|
|
2149
|
+
if (looksLikePowerShellAssignment(token)) {
|
|
2150
|
+
continue;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
seenCommand = true;
|
|
2154
|
+
result.push(normalizeShellToken(token));
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
if (operators.has(token)) {
|
|
2159
|
+
break;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
result.push(normalizeShellToken(token));
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
return result;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
function tokenizeShellCommand(command) {
|
|
2169
|
+
const text = String(command || "");
|
|
2170
|
+
const tokens = [];
|
|
2171
|
+
let current = "";
|
|
2172
|
+
let quote = "";
|
|
2173
|
+
|
|
2174
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
2175
|
+
const char = text[index];
|
|
2176
|
+
const next = text[index + 1] || "";
|
|
2177
|
+
|
|
2178
|
+
if (quote) {
|
|
2179
|
+
if (char === quote) {
|
|
2180
|
+
quote = "";
|
|
2181
|
+
} else {
|
|
2182
|
+
current += char;
|
|
2183
|
+
}
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
if (char === '"' || char === "'") {
|
|
2188
|
+
quote = char;
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
if ((char === "&" && next === "&") || (char === "|" && next === "|")) {
|
|
2193
|
+
if (current.trim()) {
|
|
2194
|
+
tokens.push(current.trim());
|
|
2195
|
+
}
|
|
2196
|
+
tokens.push(char + next);
|
|
2197
|
+
current = "";
|
|
2198
|
+
index += 1;
|
|
2199
|
+
continue;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
if (char === "|" || char === ";") {
|
|
2203
|
+
if (current.trim()) {
|
|
2204
|
+
tokens.push(current.trim());
|
|
2205
|
+
}
|
|
2206
|
+
tokens.push(char);
|
|
2207
|
+
current = "";
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
if (/\s/.test(char)) {
|
|
2212
|
+
if (current.trim()) {
|
|
2213
|
+
tokens.push(current.trim());
|
|
2214
|
+
current = "";
|
|
2215
|
+
}
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
current += char;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
if (current.trim()) {
|
|
2223
|
+
tokens.push(current.trim());
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
return tokens;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
function looksLikePowerShellAssignment(token) {
|
|
2230
|
+
return /^\$[A-Za-z_][A-Za-z0-9_:.]*=/.test(String(token || ""));
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
function normalizePathCandidate(value) {
|
|
2234
|
+
return stripMatchingQuotes(String(value || "").trim())
|
|
2235
|
+
.replace(/^[([{]+/, "")
|
|
2236
|
+
.replace(/[)\],;]+$/, "");
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
function isWindowsAbsolutePath(value) {
|
|
2240
|
+
return /^[A-Za-z]:[\\/]/.test(String(value || ""));
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
function looksLikeRelativePathCandidate(value) {
|
|
2244
|
+
const text = String(value || "");
|
|
2245
|
+
if (!text || /^[A-Za-z]:[\\/]/.test(text) || /^[A-Za-z]+:\/\//.test(text) || text.startsWith("$")) {
|
|
2246
|
+
return false;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
return text.startsWith(".") || /[\\/]/.test(text);
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
function normalizeShellCommandPath(value) {
|
|
2253
|
+
const candidate = normalizePathCandidate(value);
|
|
2254
|
+
if (!isWindowsAbsolutePath(candidate)) {
|
|
2255
|
+
return "";
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
let normalized = candidate.replace(/\\/g, "/");
|
|
2259
|
+
if (normalized.length > 3) {
|
|
2260
|
+
normalized = normalized.replace(/\/+$/, "");
|
|
2261
|
+
}
|
|
2262
|
+
return normalized.toLowerCase();
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
function isPathWithinRoot(candidatePath, rootPath) {
|
|
2266
|
+
const candidate = normalizeShellCommandPath(candidatePath);
|
|
2267
|
+
const root = normalizeShellCommandPath(rootPath);
|
|
2268
|
+
if (!candidate || !root) {
|
|
2269
|
+
return false;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
return candidate === root || candidate.startsWith(`${root}/`);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
function findCommandApprovalRootPath(value) {
|
|
2276
|
+
let currentPath = "";
|
|
2277
|
+
try {
|
|
2278
|
+
currentPath = path.resolve(String(value || ""));
|
|
2279
|
+
} catch {
|
|
2280
|
+
return String(value || "");
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
let bestGitRoot = "";
|
|
2284
|
+
let currentDir = currentPath;
|
|
2285
|
+
|
|
2286
|
+
for (let depth = 0; depth <= COMMAND_APPROVAL_ROOT_MAX_DEPTH; depth += 1) {
|
|
2287
|
+
const marker = findCommandApprovalRootMarker(currentDir);
|
|
2288
|
+
if (marker && marker !== ".git") {
|
|
2289
|
+
return currentDir;
|
|
2290
|
+
}
|
|
2291
|
+
if (marker === ".git" && !bestGitRoot) {
|
|
2292
|
+
bestGitRoot = currentDir;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
const parentDir = path.dirname(currentDir);
|
|
2296
|
+
if (!parentDir || parentDir === currentDir) {
|
|
2297
|
+
break;
|
|
2298
|
+
}
|
|
2299
|
+
currentDir = parentDir;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
return bestGitRoot || currentPath;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
function findCommandApprovalRootMarker(dirPath) {
|
|
2306
|
+
if (!dirPath) {
|
|
2307
|
+
return "";
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
return COMMAND_APPROVAL_ROOT_MARKERS.find((marker) =>
|
|
2311
|
+
fs.existsSync(path.join(dirPath, marker))
|
|
2312
|
+
) || "";
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
function normalizeShellToken(token) {
|
|
2316
|
+
return stripMatchingQuotes(String(token || "").trim()).toLowerCase();
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
function stripMatchingQuotes(value) {
|
|
2320
|
+
if (
|
|
2321
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
2322
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
2323
|
+
) {
|
|
2324
|
+
return value.slice(1, -1);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
return value;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
function arrayStartsWith(values, prefix) {
|
|
2331
|
+
if (!Array.isArray(values) || !Array.isArray(prefix) || prefix.length === 0) {
|
|
2332
|
+
return false;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
if (values.length < prefix.length) {
|
|
2336
|
+
return false;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
for (let index = 0; index < prefix.length; index += 1) {
|
|
2340
|
+
if (values[index] !== prefix[index]) {
|
|
2341
|
+
return false;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
return true;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
|
|
2349
|
+
function handleSessionRecord(
|
|
2350
|
+
state,
|
|
2351
|
+
line,
|
|
2352
|
+
{
|
|
2353
|
+
runtime,
|
|
2354
|
+
terminal,
|
|
2355
|
+
emittedEventKeys,
|
|
2356
|
+
pendingApprovalNotifications,
|
|
2357
|
+
pendingApprovalCallIds,
|
|
2358
|
+
recentRequireEscalatedEvents,
|
|
2359
|
+
sessionApprovalGrants,
|
|
2360
|
+
approvedCommandRuleCache,
|
|
2361
|
+
}
|
|
2362
|
+
) {
|
|
2363
|
+
let record;
|
|
2364
|
+
try {
|
|
2365
|
+
record = JSON.parse(stripUtf8Bom(line));
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
runtime.log(`failed to parse session line file=${state.filePath} error=${error.message}`);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
if (record.type === "session_meta" && record.payload) {
|
|
2372
|
+
if (record.payload.id) {
|
|
2373
|
+
state.sessionId = record.payload.id;
|
|
2374
|
+
}
|
|
2375
|
+
if (record.payload.cwd) {
|
|
2376
|
+
state.cwd = record.payload.cwd;
|
|
2377
|
+
}
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
if (record.type === "turn_context" && record.payload) {
|
|
2382
|
+
if (record.payload.cwd) {
|
|
2383
|
+
state.cwd = record.payload.cwd;
|
|
2384
|
+
}
|
|
2385
|
+
if (record.payload.turn_id) {
|
|
2386
|
+
state.turnId = record.payload.turn_id;
|
|
2387
|
+
}
|
|
2388
|
+
if (record.payload.approval_policy) {
|
|
2389
|
+
state.approvalPolicy = record.payload.approval_policy;
|
|
2390
|
+
}
|
|
2391
|
+
if (record.payload.sandbox_policy) {
|
|
2392
|
+
state.sandboxPolicy = record.payload.sandbox_policy;
|
|
2393
|
+
}
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
if (
|
|
2398
|
+
record.type === "response_item" &&
|
|
2399
|
+
record.payload &&
|
|
2400
|
+
record.payload.type === "function_call_output" &&
|
|
2401
|
+
record.payload.call_id
|
|
2402
|
+
) {
|
|
2403
|
+
cancelPendingApprovalNotification({
|
|
2404
|
+
runtime,
|
|
2405
|
+
pendingApprovalNotifications,
|
|
2406
|
+
pendingApprovalCallIds,
|
|
2407
|
+
callId: record.payload.call_id,
|
|
2408
|
+
reason: "function_call_output",
|
|
2409
|
+
});
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
if (isApprovedCommandRuleSavedRecord(record)) {
|
|
2414
|
+
confirmSessionApprovalForRecentEvents({
|
|
2415
|
+
recentRequireEscalatedEvents,
|
|
2416
|
+
runtime,
|
|
2417
|
+
sessionApprovalGrants,
|
|
2418
|
+
sessionId: state.sessionId || parseSessionIdFromRolloutPath(state.filePath) || "",
|
|
2419
|
+
source: "approved_rule_saved",
|
|
2420
|
+
turnId: state.turnId || "",
|
|
2421
|
+
});
|
|
2422
|
+
cancelPendingApprovalNotificationsBySuppression({
|
|
2423
|
+
runtime,
|
|
2424
|
+
pendingApprovalNotifications,
|
|
2425
|
+
pendingApprovalCallIds,
|
|
2426
|
+
sessionId: state.sessionId || parseSessionIdFromRolloutPath(state.filePath) || "",
|
|
2427
|
+
turnId: state.turnId || "",
|
|
2428
|
+
approvalPolicy: state.approvalPolicy || "",
|
|
2429
|
+
sandboxPolicy: state.sandboxPolicy || null,
|
|
2430
|
+
approvedCommandRules: getApprovedCommandRules(approvedCommandRuleCache, runtime.log),
|
|
2431
|
+
sessionApprovalGrants,
|
|
2432
|
+
});
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
if (
|
|
2437
|
+
(record.type !== "event_msg" && record.type !== "response_item") ||
|
|
2438
|
+
!record.payload ||
|
|
2439
|
+
typeof record.payload.type !== "string"
|
|
2440
|
+
) {
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
const event = buildCodexSessionEvent(state, record);
|
|
2445
|
+
if (!event) {
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
if (event.eventType === "require_escalated_tool_call") {
|
|
2450
|
+
const suppressionReason = getCodexRequireEscalatedSuppressionReason({
|
|
2451
|
+
event,
|
|
2452
|
+
approvalPolicy: state.approvalPolicy,
|
|
2453
|
+
sandboxPolicy: state.sandboxPolicy,
|
|
2454
|
+
approvedCommandRules: getApprovedCommandRules(approvedCommandRuleCache, runtime.log),
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
if (suppressionReason) {
|
|
2458
|
+
runtime.log(
|
|
2459
|
+
`suppressed session require_escalated sessionId=${event.sessionId || "unknown"} turnId=${event.turnId || ""} reason=${suppressionReason}`
|
|
2460
|
+
);
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
const sessionSuppressionReason = getSessionRequireEscalatedSuppressionReason({
|
|
2465
|
+
event,
|
|
2466
|
+
sessionApprovalGrants,
|
|
2467
|
+
});
|
|
2468
|
+
if (sessionSuppressionReason) {
|
|
2469
|
+
runtime.log(
|
|
2470
|
+
`suppressed session require_escalated sessionId=${event.sessionId || "unknown"} turnId=${event.turnId || ""} reason=${sessionSuppressionReason}`
|
|
2471
|
+
);
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
rememberRecentRequireEscalatedEvent(recentRequireEscalatedEvents, event);
|
|
2476
|
+
|
|
2477
|
+
if (event.approvalDispatch === "immediate") {
|
|
2478
|
+
emitCodexApprovalNotification({
|
|
2479
|
+
event,
|
|
2480
|
+
runtime,
|
|
2481
|
+
terminal,
|
|
2482
|
+
emittedEventKeys,
|
|
2483
|
+
origin: "session",
|
|
2484
|
+
});
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
queuePendingApprovalNotification({
|
|
2489
|
+
runtime,
|
|
2490
|
+
pendingApprovalNotifications,
|
|
2491
|
+
pendingApprovalCallIds,
|
|
2492
|
+
emittedEventKeys,
|
|
2493
|
+
event,
|
|
2494
|
+
});
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
emitCodexApprovalNotification({
|
|
2499
|
+
event,
|
|
2500
|
+
runtime,
|
|
2501
|
+
terminal,
|
|
2502
|
+
emittedEventKeys,
|
|
2503
|
+
origin: "session",
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
function handleCodexTuiLogLine(
|
|
2508
|
+
tuiState,
|
|
2509
|
+
line,
|
|
2510
|
+
{
|
|
2511
|
+
runtime,
|
|
2512
|
+
terminal,
|
|
2513
|
+
emittedEventKeys,
|
|
2514
|
+
sessionProjectDirs,
|
|
2515
|
+
sessionApprovalContexts,
|
|
2516
|
+
pendingApprovalNotifications,
|
|
2517
|
+
pendingApprovalCallIds,
|
|
2518
|
+
recentRequireEscalatedEvents,
|
|
2519
|
+
sessionApprovalGrants,
|
|
2520
|
+
approvedCommandRuleCache,
|
|
2521
|
+
}
|
|
2522
|
+
) {
|
|
2523
|
+
if (!line || !line.trim()) {
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
const confirmation = parseCodexTuiApprovalConfirmation(line);
|
|
2528
|
+
if (confirmation) {
|
|
2529
|
+
const approvalContext = sessionApprovalContexts.get(confirmation.sessionId || "");
|
|
2530
|
+
confirmSessionApprovalForRecentEvents({
|
|
2531
|
+
recentRequireEscalatedEvents,
|
|
2532
|
+
runtime,
|
|
2533
|
+
sessionApprovalGrants,
|
|
2534
|
+
sessionId: confirmation.sessionId,
|
|
2535
|
+
source: confirmation.source,
|
|
2536
|
+
});
|
|
2537
|
+
cancelPendingApprovalNotificationsBySuppression({
|
|
2538
|
+
runtime,
|
|
2539
|
+
pendingApprovalNotifications,
|
|
2540
|
+
pendingApprovalCallIds,
|
|
2541
|
+
sessionId: confirmation.sessionId,
|
|
2542
|
+
approvalPolicy: (approvalContext && approvalContext.approvalPolicy) || "",
|
|
2543
|
+
sandboxPolicy: (approvalContext && approvalContext.sandboxPolicy) || null,
|
|
2544
|
+
sessionApprovalGrants,
|
|
2545
|
+
});
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
const event = buildCodexTuiApprovalEvent(tuiState, line, {
|
|
2550
|
+
sessionProjectDirs,
|
|
2551
|
+
sessionApprovalContexts,
|
|
2552
|
+
});
|
|
2553
|
+
if (!event) {
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
const approvalContext = sessionApprovalContexts.get(event.sessionId || "");
|
|
2558
|
+
const suppressionReason = getCodexRequireEscalatedSuppressionReason({
|
|
2559
|
+
event,
|
|
2560
|
+
approvalPolicy: approvalContext && approvalContext.approvalPolicy,
|
|
2561
|
+
sandboxPolicy: approvalContext && approvalContext.sandboxPolicy,
|
|
2562
|
+
approvedCommandRules: getApprovedCommandRules(approvedCommandRuleCache, runtime.log),
|
|
2563
|
+
});
|
|
2564
|
+
|
|
2565
|
+
if (suppressionReason) {
|
|
2566
|
+
runtime.log(
|
|
2567
|
+
`suppressed tui require_escalated sessionId=${event.sessionId || "unknown"} turnId=${event.turnId || ""} reason=${suppressionReason}`
|
|
2568
|
+
);
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
const sessionSuppressionReason = getSessionRequireEscalatedSuppressionReason({
|
|
2573
|
+
event,
|
|
2574
|
+
sessionApprovalGrants,
|
|
2575
|
+
});
|
|
2576
|
+
if (sessionSuppressionReason) {
|
|
2577
|
+
runtime.log(
|
|
2578
|
+
`suppressed tui require_escalated sessionId=${event.sessionId || "unknown"} turnId=${event.turnId || ""} reason=${sessionSuppressionReason}`
|
|
2579
|
+
);
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
rememberRecentRequireEscalatedEvent(recentRequireEscalatedEvents, event);
|
|
2584
|
+
|
|
2585
|
+
queuePendingApprovalNotification({
|
|
2586
|
+
runtime,
|
|
2587
|
+
pendingApprovalNotifications,
|
|
2588
|
+
pendingApprovalCallIds,
|
|
2589
|
+
emittedEventKeys,
|
|
2590
|
+
event,
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
function buildCodexSessionEvent(state, record) {
|
|
2595
|
+
const payload = record && record.payload;
|
|
2596
|
+
if (!payload || typeof payload.type !== "string") {
|
|
2597
|
+
return null;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
const sessionId = state.sessionId || parseSessionIdFromRolloutPath(state.filePath) || "unknown";
|
|
2601
|
+
const projectDir = payload.cwd || state.cwd || "";
|
|
2602
|
+
const turnId = payload.turn_id || state.turnId || "";
|
|
2603
|
+
const callId = payload.call_id || "";
|
|
2604
|
+
const approvalId = payload.approval_id || "";
|
|
2605
|
+
|
|
2606
|
+
if (record.type === "response_item" && payload.type === "function_call") {
|
|
2607
|
+
const args = parseJsonObjectMaybe(payload.arguments);
|
|
2608
|
+
if (!args || args.sandbox_permissions !== "require_escalated") {
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
const descriptor = getCodexExecApprovalDescriptor(payload.name, args);
|
|
2613
|
+
const approvalProjectDir = args.workdir || projectDir;
|
|
2614
|
+
return {
|
|
2615
|
+
...createNotificationSpec({
|
|
2616
|
+
sourceId: "codex-session-watch",
|
|
2617
|
+
sessionId,
|
|
2618
|
+
turnId,
|
|
2619
|
+
eventName: "PermissionRequest",
|
|
2620
|
+
projectDir: approvalProjectDir,
|
|
2621
|
+
rawEventType: "require_escalated_tool_call",
|
|
2622
|
+
}),
|
|
2623
|
+
eventType: "require_escalated_tool_call",
|
|
2624
|
+
approvalDispatch: "pending",
|
|
2625
|
+
callId,
|
|
2626
|
+
toolArgs: args,
|
|
2627
|
+
dedupeKey: buildApprovalDedupeKey({
|
|
2628
|
+
sessionId,
|
|
2629
|
+
turnId,
|
|
2630
|
+
callId,
|
|
2631
|
+
approvalKind: "exec",
|
|
2632
|
+
descriptor,
|
|
2633
|
+
}),
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
if (record.type !== "event_msg") {
|
|
2638
|
+
return null;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
switch (payload.type) {
|
|
2642
|
+
case "exec_approval_request":
|
|
2643
|
+
case "request_permissions":
|
|
2644
|
+
return {
|
|
2645
|
+
...createNotificationSpec({
|
|
2646
|
+
sourceId: "codex-session-watch",
|
|
2647
|
+
sessionId,
|
|
2648
|
+
turnId,
|
|
2649
|
+
eventName: "PermissionRequest",
|
|
2650
|
+
projectDir,
|
|
2651
|
+
rawEventType: payload.type,
|
|
2652
|
+
}),
|
|
2653
|
+
eventType: payload.type,
|
|
2654
|
+
dedupeKey: buildApprovalDedupeKey({
|
|
2655
|
+
sessionId,
|
|
2656
|
+
turnId,
|
|
2657
|
+
callId,
|
|
2658
|
+
approvalId,
|
|
2659
|
+
approvalKind: "exec",
|
|
2660
|
+
}),
|
|
2661
|
+
};
|
|
2662
|
+
case "apply_patch_approval_request":
|
|
2663
|
+
return {
|
|
2664
|
+
...createNotificationSpec({
|
|
2665
|
+
sourceId: "codex-session-watch",
|
|
2666
|
+
sessionId,
|
|
2667
|
+
turnId,
|
|
2668
|
+
eventName: "PermissionRequest",
|
|
2669
|
+
projectDir,
|
|
2670
|
+
rawEventType: payload.type,
|
|
2671
|
+
}),
|
|
2672
|
+
eventType: payload.type,
|
|
2673
|
+
dedupeKey: buildApprovalDedupeKey({
|
|
2674
|
+
sessionId,
|
|
2675
|
+
turnId,
|
|
2676
|
+
callId,
|
|
2677
|
+
approvalId,
|
|
2678
|
+
approvalKind: "patch",
|
|
2679
|
+
}),
|
|
2680
|
+
};
|
|
2681
|
+
default:
|
|
2682
|
+
return null;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
function buildCodexTuiApprovalEvent(tuiState, line, { sessionProjectDirs, sessionApprovalContexts }) {
|
|
2687
|
+
if (!line.includes("ToolCall: shell_command ") || !line.includes('"sandbox_permissions":"require_escalated"')) {
|
|
2688
|
+
return null;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
const match = line.match(
|
|
2692
|
+
/thread_id=([^}:]+).*?submission\.id="([^"]+)".*?(?:turn\.id=([^ ]+).*?)?ToolCall: shell_command (\{.*\}) thread_id=/
|
|
2693
|
+
);
|
|
2694
|
+
|
|
2695
|
+
if (!match) {
|
|
2696
|
+
return null;
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
const [, sessionId, submissionId, turnIdFromLog, rawArgs] = match;
|
|
2700
|
+
const args = parseJsonObjectMaybe(rawArgs);
|
|
2701
|
+
if (!args || args.sandbox_permissions !== "require_escalated") {
|
|
2702
|
+
return null;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
const turnId = turnIdFromLog || submissionId;
|
|
2706
|
+
const projectDir = args.workdir || sessionProjectDirs.get(sessionId) || "";
|
|
2707
|
+
const descriptor = getCodexExecApprovalDescriptor("shell_command", args);
|
|
2708
|
+
|
|
2709
|
+
return {
|
|
2710
|
+
...createNotificationSpec({
|
|
2711
|
+
sourceId: "codex-session-watch",
|
|
2712
|
+
sessionId,
|
|
2713
|
+
turnId,
|
|
2714
|
+
eventName: "PermissionRequest",
|
|
2715
|
+
projectDir,
|
|
2716
|
+
rawEventType: "require_escalated_tool_call",
|
|
2717
|
+
}),
|
|
2718
|
+
eventType: "require_escalated_tool_call",
|
|
2719
|
+
approvalDispatch: "pending",
|
|
2720
|
+
approvalPolicy:
|
|
2721
|
+
sessionApprovalContexts && sessionApprovalContexts.get(sessionId)
|
|
2722
|
+
? sessionApprovalContexts.get(sessionId).approvalPolicy || ""
|
|
2723
|
+
: "",
|
|
2724
|
+
callId: "",
|
|
2725
|
+
toolArgs: args,
|
|
2726
|
+
dedupeKey: buildApprovalDedupeKey({
|
|
2727
|
+
sessionId,
|
|
2728
|
+
turnId,
|
|
2729
|
+
fallbackId: submissionId,
|
|
2730
|
+
approvalKind: "exec",
|
|
2731
|
+
descriptor,
|
|
2732
|
+
}),
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
function parseCodexTuiApprovalConfirmation(line) {
|
|
2737
|
+
if (!line || !line.includes("thread_id=")) {
|
|
2738
|
+
return null;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
let source = "";
|
|
2742
|
+
if (line.includes('otel.name="op.dispatch.exec_approval"')) {
|
|
2743
|
+
source = "tui_exec_approval";
|
|
2744
|
+
} else if (line.includes('otel.name="op.dispatch.patch_approval"')) {
|
|
2745
|
+
source = "tui_patch_approval";
|
|
2746
|
+
} else {
|
|
2747
|
+
return null;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
const match = line.match(/thread_id=([^}:]+)/);
|
|
2751
|
+
if (!match) {
|
|
2752
|
+
return null;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
return {
|
|
2756
|
+
sessionId: match[1],
|
|
2757
|
+
source,
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
function isApprovedCommandRuleSavedRecord(record) {
|
|
2762
|
+
if (
|
|
2763
|
+
!record ||
|
|
2764
|
+
record.type !== "response_item" ||
|
|
2765
|
+
!record.payload ||
|
|
2766
|
+
record.payload.type !== "message" ||
|
|
2767
|
+
record.payload.role !== "developer" ||
|
|
2768
|
+
!Array.isArray(record.payload.content)
|
|
2769
|
+
) {
|
|
2770
|
+
return false;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
return record.payload.content.some(
|
|
2774
|
+
(item) =>
|
|
2775
|
+
item &&
|
|
2776
|
+
item.type === "input_text" &&
|
|
2777
|
+
typeof item.text === "string" &&
|
|
2778
|
+
item.text.startsWith("Approved command prefix saved:")
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
function shouldEmitEventKey(emittedEventKeys, eventKey) {
|
|
2783
|
+
if (!eventKey) {
|
|
2784
|
+
return true;
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
if (emittedEventKeys.has(eventKey)) {
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
emittedEventKeys.set(eventKey, Date.now());
|
|
2792
|
+
return true;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
function resolveApprovalTerminalContext({ sessionId, projectDir, fallbackTerminal, log }) {
|
|
2796
|
+
const terminal = findSidecarTerminalContextForSession(sessionId, log);
|
|
2797
|
+
if (!terminal || (!terminal.hwnd && !terminal.shellPid)) {
|
|
2798
|
+
const projectFallback = findSidecarTerminalContextForProjectDir(projectDir, log);
|
|
2799
|
+
if (!projectFallback || !projectFallback.hwnd) {
|
|
2800
|
+
if (typeof log === "function") {
|
|
2801
|
+
log(
|
|
2802
|
+
`approval terminal fallback used sessionId=${sessionId || "unknown"} projectDir=${projectDir || ""} reason=no_sidecar_match`
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
return fallbackTerminal;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
if (typeof log === "function") {
|
|
2809
|
+
log(
|
|
2810
|
+
`approval terminal project fallback used sessionId=${sessionId || "unknown"} projectDir=${projectDir || ""} hwnd=${projectFallback.hwnd || ""}`
|
|
2811
|
+
);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
return {
|
|
2815
|
+
hwnd: projectFallback.hwnd,
|
|
2816
|
+
shellPid: null,
|
|
2817
|
+
isWindowsTerminal: false,
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
if (typeof log === "function") {
|
|
2822
|
+
log(
|
|
2823
|
+
`sidecar terminal matched sessionId=${sessionId} shellPid=${terminal.shellPid || ""} hwnd=${terminal.hwnd || ""}`
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
return {
|
|
2828
|
+
hwnd: terminal.hwnd,
|
|
2829
|
+
shellPid: terminal.shellPid,
|
|
2830
|
+
isWindowsTerminal: terminal.isWindowsTerminal,
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
function pruneEmittedEventKeys(emittedEventKeys, maxSize) {
|
|
2835
|
+
while (emittedEventKeys.size > maxSize) {
|
|
2836
|
+
const firstKey = emittedEventKeys.keys().next();
|
|
2837
|
+
if (firstKey.done) {
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
emittedEventKeys.delete(firstKey.value);
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
function detectShellPid(log) {
|
|
2845
|
+
const detectScript = path.join(__dirname, "..", "scripts", "get-shell-pid.ps1");
|
|
2846
|
+
const result = spawnSync(
|
|
2847
|
+
"powershell",
|
|
2848
|
+
[
|
|
2849
|
+
"-NoProfile",
|
|
2850
|
+
"-ExecutionPolicy",
|
|
2851
|
+
"Bypass",
|
|
2852
|
+
"-File",
|
|
2853
|
+
detectScript,
|
|
2854
|
+
"-StartPid",
|
|
2855
|
+
String(process.pid),
|
|
2856
|
+
],
|
|
2857
|
+
{ encoding: "utf8" }
|
|
2858
|
+
);
|
|
2859
|
+
|
|
2860
|
+
writeChildStderr(result, log);
|
|
2861
|
+
|
|
2862
|
+
const pid = parseInt((result.stdout || "").trim(), 10);
|
|
2863
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
function findParentInfo(log) {
|
|
2867
|
+
const findScript = path.join(__dirname, "..", "scripts", "find-hwnd.ps1");
|
|
2868
|
+
const result = spawnSync(
|
|
2869
|
+
"powershell",
|
|
2870
|
+
[
|
|
2871
|
+
"-NoProfile",
|
|
2872
|
+
"-ExecutionPolicy",
|
|
2873
|
+
"Bypass",
|
|
2874
|
+
"-File",
|
|
2875
|
+
findScript,
|
|
2876
|
+
"-StartPid",
|
|
2877
|
+
String(process.pid),
|
|
2878
|
+
"-IncludeShellPid",
|
|
2879
|
+
],
|
|
2880
|
+
{ encoding: "utf8" }
|
|
2881
|
+
);
|
|
2882
|
+
|
|
2883
|
+
writeChildStderr(result, log);
|
|
2884
|
+
|
|
2885
|
+
const parts = (result.stdout || "").trim().split("|");
|
|
2886
|
+
const hwnd = parseInt(parts[0], 10);
|
|
2887
|
+
const shellPid = parseInt(parts[1], 10);
|
|
2888
|
+
const isWindowsTerminal = parts[2] === "1";
|
|
2889
|
+
|
|
2890
|
+
return {
|
|
2891
|
+
hwnd: hwnd > 0 ? hwnd : null,
|
|
2892
|
+
shellPid: shellPid > 0 ? shellPid : null,
|
|
2893
|
+
isWindowsTerminal,
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
function detectTerminalContext(argv, log) {
|
|
2898
|
+
const parentInfo = findParentInfo(log);
|
|
2899
|
+
const shellPid = getExplicitShellPid(argv) || detectShellPid(log) || parentInfo.shellPid;
|
|
2900
|
+
|
|
2901
|
+
if (!shellPid) {
|
|
2902
|
+
log("no shell pid detected; tab color watcher disabled");
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
return {
|
|
2906
|
+
hwnd: parentInfo.hwnd,
|
|
2907
|
+
shellPid,
|
|
2908
|
+
isWindowsTerminal: parentInfo.isWindowsTerminal,
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
function emitNotification({ source, eventName, title, message, rawEventType, runtime, terminal }) {
|
|
2913
|
+
const envVars = {
|
|
2914
|
+
PATH: process.env.PATH || "",
|
|
2915
|
+
PATHEXT: process.env.PATHEXT || "",
|
|
2916
|
+
TOAST_NOTIFY_EVENT: eventName,
|
|
2917
|
+
TOAST_NOTIFY_IS_DEV: runtime.isDev ? "1" : "0",
|
|
2918
|
+
TOAST_NOTIFY_LOG_FILE: runtime.logFile,
|
|
2919
|
+
};
|
|
2920
|
+
|
|
2921
|
+
if (source) {
|
|
2922
|
+
envVars.TOAST_NOTIFY_SOURCE = source;
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
if (title) {
|
|
2926
|
+
envVars.TOAST_NOTIFY_TITLE = title;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
if (message) {
|
|
2930
|
+
envVars.TOAST_NOTIFY_MESSAGE = message;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
if (rawEventType) {
|
|
2934
|
+
envVars.TOAST_NOTIFY_RAW_EVENT = rawEventType;
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
if (terminal.hwnd) {
|
|
2938
|
+
envVars.TOAST_NOTIFY_HWND = String(terminal.hwnd);
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
writeWindowsTerminalColor(eventName, terminal, runtime.log);
|
|
2942
|
+
startTabColorWatcher({
|
|
2943
|
+
eventName,
|
|
2944
|
+
runtime,
|
|
2945
|
+
terminal,
|
|
2946
|
+
});
|
|
2947
|
+
|
|
2948
|
+
const scriptPath = path.join(__dirname, "..", "scripts", "notify.ps1");
|
|
2949
|
+
return spawn(
|
|
2950
|
+
"powershell",
|
|
2951
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath],
|
|
2952
|
+
{ stdio: ["ignore", "inherit", "inherit"], env: envVars }
|
|
2953
|
+
);
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
function writeWindowsTerminalColor(eventName, terminal, log) {
|
|
2957
|
+
if (!terminal || !terminal.isWindowsTerminal) {
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
const colorMap = {
|
|
2962
|
+
Stop: "rgb:33/cc/33",
|
|
2963
|
+
PermissionRequest: "rgb:ff/99/00",
|
|
2964
|
+
};
|
|
2965
|
+
const tabColor = colorMap[eventName];
|
|
2966
|
+
|
|
2967
|
+
if (!tabColor) {
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
const oscSet = `\x1b]4;264;${tabColor}\x1b\\`;
|
|
2972
|
+
|
|
2973
|
+
try {
|
|
2974
|
+
if (process.stdout && !process.stdout.destroyed) {
|
|
2975
|
+
process.stdout.write(oscSet);
|
|
2976
|
+
}
|
|
2977
|
+
if (process.stderr && !process.stderr.destroyed) {
|
|
2978
|
+
process.stderr.write(oscSet);
|
|
2979
|
+
}
|
|
2980
|
+
} catch (error) {
|
|
2981
|
+
log(`initial tab color write failed: ${error.message}`);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
function startTabColorWatcher({ eventName, runtime, terminal }) {
|
|
2986
|
+
if (!terminal.isWindowsTerminal) {
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
if (!terminal.shellPid) {
|
|
2991
|
+
runtime.log("tab color watcher not started because no shell pid was detected");
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
try {
|
|
2996
|
+
const launcherScript = path.join(
|
|
2997
|
+
__dirname,
|
|
2998
|
+
"..",
|
|
2999
|
+
"scripts",
|
|
3000
|
+
"start-tab-color-watcher.ps1"
|
|
3001
|
+
);
|
|
3002
|
+
const watcherPidFile = path.join(
|
|
3003
|
+
LOG_DIR,
|
|
3004
|
+
`watcher-${process.pid}-${Date.now()}.pid`
|
|
3005
|
+
);
|
|
3006
|
+
const result = spawnSync(
|
|
3007
|
+
"powershell",
|
|
3008
|
+
[
|
|
3009
|
+
"-NoProfile",
|
|
3010
|
+
"-ExecutionPolicy",
|
|
3011
|
+
"Bypass",
|
|
3012
|
+
"-File",
|
|
3013
|
+
launcherScript,
|
|
3014
|
+
"-TargetPid",
|
|
3015
|
+
String(terminal.shellPid),
|
|
3016
|
+
"-HookEvent",
|
|
3017
|
+
eventName,
|
|
3018
|
+
...(terminal.hwnd ? ["-TerminalHwnd", String(terminal.hwnd)] : []),
|
|
3019
|
+
"-WatcherPidFile",
|
|
3020
|
+
watcherPidFile,
|
|
3021
|
+
],
|
|
3022
|
+
{
|
|
3023
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
3024
|
+
env: {
|
|
3025
|
+
...process.env,
|
|
3026
|
+
TOAST_NOTIFY_LOG_FILE: runtime.logFile,
|
|
3027
|
+
},
|
|
3028
|
+
}
|
|
3029
|
+
);
|
|
3030
|
+
|
|
3031
|
+
if (result.error) {
|
|
3032
|
+
throw result.error;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
const watcherPidRaw = fs.existsSync(watcherPidFile)
|
|
3036
|
+
? fs.readFileSync(watcherPidFile, "utf8").trim()
|
|
3037
|
+
: "";
|
|
3038
|
+
|
|
3039
|
+
try {
|
|
3040
|
+
if (fs.existsSync(watcherPidFile)) {
|
|
3041
|
+
fs.unlinkSync(watcherPidFile);
|
|
3042
|
+
}
|
|
3043
|
+
} catch {}
|
|
3044
|
+
|
|
3045
|
+
const watcherPid = parseInt(watcherPidRaw, 10);
|
|
3046
|
+
if (result.status === 0 && Number.isInteger(watcherPid) && watcherPid > 0) {
|
|
3047
|
+
runtime.log(
|
|
3048
|
+
`tab-color-watcher spawned pid=${watcherPid} shellPid=${terminal.shellPid}`
|
|
3049
|
+
);
|
|
3050
|
+
} else {
|
|
3051
|
+
runtime.log(
|
|
3052
|
+
`tab-color-watcher launcher exited status=${result.status} without child pid shellPid=${terminal.shellPid}`
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3055
|
+
} catch (error) {
|
|
3056
|
+
runtime.log(`tab-color-watcher spawn failed: ${error.message}`);
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
function writeChildStderr(result, log) {
|
|
3061
|
+
if (!result || !result.stderr) {
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
result.stderr
|
|
3066
|
+
.trim()
|
|
3067
|
+
.split(/\r?\n/)
|
|
3068
|
+
.filter(Boolean)
|
|
3069
|
+
.forEach((line) => log(line));
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
function safeStringify(value) {
|
|
3073
|
+
try {
|
|
3074
|
+
return JSON.stringify(value);
|
|
3075
|
+
} catch {
|
|
3076
|
+
return String(value);
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
module.exports = {
|
|
3081
|
+
buildCodexSessionEvent,
|
|
3082
|
+
buildCodexTuiApprovalEvent,
|
|
3083
|
+
buildApprovalDedupeKey,
|
|
3084
|
+
buildPendingApprovalBatchKey,
|
|
3085
|
+
cancelPendingApprovalNotificationsBySuppression,
|
|
3086
|
+
confirmSessionApprovalForRecentEvents,
|
|
3087
|
+
drainPendingApprovalBatch,
|
|
3088
|
+
extractCommandApprovalRoots,
|
|
3089
|
+
getCodexApprovalNotifyGraceMs,
|
|
3090
|
+
getCodexRequireEscalatedSuppressionReason,
|
|
3091
|
+
getSessionRequireEscalatedSuppressionReason,
|
|
3092
|
+
handleMcpServerMessage,
|
|
3093
|
+
isLikelyReadOnlyShellCommand,
|
|
3094
|
+
matchesApprovedCommandRule,
|
|
3095
|
+
rememberRecentRequireEscalatedEvent,
|
|
3096
|
+
getCodexExecApprovalDescriptor,
|
|
3097
|
+
parseApprovedCommandRules,
|
|
3098
|
+
parseJsonObjectMaybe,
|
|
3099
|
+
parseRolloutTimestampFromPath,
|
|
3100
|
+
pickSidecarSessionCandidate,
|
|
3101
|
+
resolveSidecarSessionCandidate,
|
|
3102
|
+
resolveApprovalTerminalContext,
|
|
3103
|
+
shouldBatchPendingApproval,
|
|
3104
|
+
};
|