codiedev 0.9.39 → 0.9.40
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/dist/codexHookTrust.d.ts +34 -0
- package/dist/codexHookTrust.js +228 -0
- package/dist/connect.js +1 -0
- package/dist/hook.js +25 -14
- package/dist/securityInstall.d.ts +1 -0
- package/dist/securityInstall.js +16 -6
- package/dist/utils.js +26 -7
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
interface CommandHandler {
|
|
2
|
+
type?: unknown;
|
|
3
|
+
command?: unknown;
|
|
4
|
+
commandWindows?: unknown;
|
|
5
|
+
command_windows?: unknown;
|
|
6
|
+
timeout?: unknown;
|
|
7
|
+
async?: unknown;
|
|
8
|
+
statusMessage?: unknown;
|
|
9
|
+
additionalContextLimit?: unknown;
|
|
10
|
+
}
|
|
11
|
+
interface MatcherGroup {
|
|
12
|
+
matcher?: unknown;
|
|
13
|
+
hooks?: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface CodexHookTrustEntry {
|
|
16
|
+
key: string;
|
|
17
|
+
eventName: string;
|
|
18
|
+
command: string;
|
|
19
|
+
currentHash: string;
|
|
20
|
+
}
|
|
21
|
+
/** Codex hashes a canonical, normalized hook identity before it will execute a
|
|
22
|
+
* user hook. Keep this compatible with codex_config::version_for_toml. */
|
|
23
|
+
export declare function codexCommandHookHash(eventName: string, group: MatcherGroup, handler: CommandHandler): string;
|
|
24
|
+
export declare function codiedevCodexHookTrustEntries(hooksPath: string, hooksFile: unknown): CodexHookTrustEntry[];
|
|
25
|
+
/** `codiedev connect` is an explicit install action, so trust only the exact
|
|
26
|
+
* Codiedev commands it just wrote. Any later command change produces a new
|
|
27
|
+
* hash and Codex disables that hook until connect is run again. */
|
|
28
|
+
export declare function writeCodexHookTrust(configPath: string, hooksPath: string, hooksFile: unknown): CodexHookTrustEntry[];
|
|
29
|
+
export declare function inspectCodexHookTrust(configPath: string, hooksPath: string, hooksFile: unknown, requiredSubcommands?: string[]): {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
total: number;
|
|
32
|
+
trusted: number;
|
|
33
|
+
};
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.codexCommandHookHash = codexCommandHookHash;
|
|
37
|
+
exports.codiedevCodexHookTrustEntries = codiedevCodexHookTrustEntries;
|
|
38
|
+
exports.writeCodexHookTrust = writeCodexHookTrust;
|
|
39
|
+
exports.inspectCodexHookTrust = inspectCodexHookTrust;
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const detection_1 = require("./detection");
|
|
44
|
+
const TRUST_BEGIN = "# codiedev-hook-trust:begin";
|
|
45
|
+
const TRUST_END = "# codiedev-hook-trust:end";
|
|
46
|
+
const EVENT_LABELS = {
|
|
47
|
+
PreToolUse: "pre_tool_use",
|
|
48
|
+
PermissionRequest: "permission_request",
|
|
49
|
+
PostToolUse: "post_tool_use",
|
|
50
|
+
PreCompact: "pre_compact",
|
|
51
|
+
PostCompact: "post_compact",
|
|
52
|
+
SessionStart: "session_start",
|
|
53
|
+
SessionEnd: "session_end",
|
|
54
|
+
UserPromptSubmit: "user_prompt_submit",
|
|
55
|
+
SubagentStart: "subagent_start",
|
|
56
|
+
SubagentStop: "subagent_stop",
|
|
57
|
+
Stop: "stop",
|
|
58
|
+
};
|
|
59
|
+
const MATCHER_EVENTS = new Set([
|
|
60
|
+
"PreToolUse",
|
|
61
|
+
"PermissionRequest",
|
|
62
|
+
"PostToolUse",
|
|
63
|
+
"PreCompact",
|
|
64
|
+
"PostCompact",
|
|
65
|
+
"SessionStart",
|
|
66
|
+
"SessionEnd",
|
|
67
|
+
"SubagentStart",
|
|
68
|
+
"SubagentStop",
|
|
69
|
+
]);
|
|
70
|
+
function canonicalize(value) {
|
|
71
|
+
if (Array.isArray(value))
|
|
72
|
+
return value.map(canonicalize);
|
|
73
|
+
if (value && typeof value === "object") {
|
|
74
|
+
return Object.fromEntries(Object.entries(value)
|
|
75
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
76
|
+
.map(([key, child]) => [key, canonicalize(child)]));
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function normalizedTimeout(eventName, value) {
|
|
81
|
+
const configured = typeof value === "number" && Number.isFinite(value)
|
|
82
|
+
? Math.floor(value)
|
|
83
|
+
: undefined;
|
|
84
|
+
if (eventName === "SessionEnd")
|
|
85
|
+
return Math.min(3, Math.max(1, configured ?? 1));
|
|
86
|
+
return Math.max(1, configured ?? 600);
|
|
87
|
+
}
|
|
88
|
+
/** Codex hashes a canonical, normalized hook identity before it will execute a
|
|
89
|
+
* user hook. Keep this compatible with codex_config::version_for_toml. */
|
|
90
|
+
function codexCommandHookHash(eventName, group, handler) {
|
|
91
|
+
const normalizedHandler = {
|
|
92
|
+
type: "command",
|
|
93
|
+
command: String(handler.command ?? ""),
|
|
94
|
+
timeout: normalizedTimeout(eventName, handler.timeout),
|
|
95
|
+
async: handler.async === true,
|
|
96
|
+
};
|
|
97
|
+
const commandWindows = typeof handler.commandWindows === "string"
|
|
98
|
+
? handler.commandWindows
|
|
99
|
+
: typeof handler.command_windows === "string"
|
|
100
|
+
? handler.command_windows
|
|
101
|
+
: undefined;
|
|
102
|
+
if (commandWindows !== undefined)
|
|
103
|
+
normalizedHandler.commandWindows = commandWindows;
|
|
104
|
+
if (typeof handler.statusMessage === "string")
|
|
105
|
+
normalizedHandler.statusMessage = handler.statusMessage;
|
|
106
|
+
if (typeof handler.additionalContextLimit === "number") {
|
|
107
|
+
normalizedHandler.additionalContextLimit = handler.additionalContextLimit;
|
|
108
|
+
}
|
|
109
|
+
const identity = {
|
|
110
|
+
event_name: EVENT_LABELS[eventName],
|
|
111
|
+
hooks: [normalizedHandler],
|
|
112
|
+
};
|
|
113
|
+
if (MATCHER_EVENTS.has(eventName) && typeof group.matcher === "string") {
|
|
114
|
+
identity.matcher = group.matcher;
|
|
115
|
+
}
|
|
116
|
+
const serialized = JSON.stringify(canonicalize(identity));
|
|
117
|
+
return `sha256:${crypto.createHash("sha256").update(serialized).digest("hex")}`;
|
|
118
|
+
}
|
|
119
|
+
function normalizedHooksPath(hooksPath) {
|
|
120
|
+
try {
|
|
121
|
+
return fs.realpathSync.native(hooksPath);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return path.resolve(hooksPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function codiedevCodexHookTrustEntries(hooksPath, hooksFile) {
|
|
128
|
+
const events = hooksFile?.hooks;
|
|
129
|
+
if (!events || typeof events !== "object")
|
|
130
|
+
return [];
|
|
131
|
+
const source = normalizedHooksPath(hooksPath);
|
|
132
|
+
const entries = [];
|
|
133
|
+
for (const [eventName, groupsValue] of Object.entries(events)) {
|
|
134
|
+
const eventLabel = EVENT_LABELS[eventName];
|
|
135
|
+
if (!eventLabel || !Array.isArray(groupsValue))
|
|
136
|
+
continue;
|
|
137
|
+
groupsValue.forEach((groupValue, groupIndex) => {
|
|
138
|
+
const group = groupValue;
|
|
139
|
+
if (!Array.isArray(group.hooks))
|
|
140
|
+
return;
|
|
141
|
+
group.hooks.forEach((handlerValue, handlerIndex) => {
|
|
142
|
+
const handler = handlerValue;
|
|
143
|
+
if (handler.type !== "command" || typeof handler.command !== "string")
|
|
144
|
+
return;
|
|
145
|
+
if (!(0, detection_1.isCodiedevHookCommand)(handler.command))
|
|
146
|
+
return;
|
|
147
|
+
entries.push({
|
|
148
|
+
key: `${source}:${eventLabel}:${groupIndex}:${handlerIndex}`,
|
|
149
|
+
eventName,
|
|
150
|
+
command: handler.command,
|
|
151
|
+
currentHash: codexCommandHookHash(eventName, group, handler),
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return entries;
|
|
157
|
+
}
|
|
158
|
+
function tableHeader(key) {
|
|
159
|
+
return `[hooks.state.${JSON.stringify(key)}]`;
|
|
160
|
+
}
|
|
161
|
+
function removeTable(lines, header) {
|
|
162
|
+
const output = [];
|
|
163
|
+
let skipping = false;
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
if (line.trim() === header) {
|
|
166
|
+
skipping = true;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (skipping && /^\s*\[/.test(line))
|
|
170
|
+
skipping = false;
|
|
171
|
+
if (!skipping)
|
|
172
|
+
output.push(line);
|
|
173
|
+
}
|
|
174
|
+
return output;
|
|
175
|
+
}
|
|
176
|
+
/** `codiedev connect` is an explicit install action, so trust only the exact
|
|
177
|
+
* Codiedev commands it just wrote. Any later command change produces a new
|
|
178
|
+
* hash and Codex disables that hook until connect is run again. */
|
|
179
|
+
function writeCodexHookTrust(configPath, hooksPath, hooksFile) {
|
|
180
|
+
const entries = codiedevCodexHookTrustEntries(hooksPath, hooksFile);
|
|
181
|
+
let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
182
|
+
existing = existing.replace(new RegExp(`\\n?${TRUST_BEGIN}\\n[\\s\\S]*?${TRUST_END}\\n?`, "g"), "\n");
|
|
183
|
+
let lines = existing.split("\n");
|
|
184
|
+
for (const entry of entries)
|
|
185
|
+
lines = removeTable(lines, tableHeader(entry.key));
|
|
186
|
+
const base = lines.join("\n").trimEnd();
|
|
187
|
+
const trustBlock = entries.flatMap((entry) => [
|
|
188
|
+
tableHeader(entry.key),
|
|
189
|
+
"enabled = true",
|
|
190
|
+
`trusted_hash = ${JSON.stringify(entry.currentHash)}`,
|
|
191
|
+
"",
|
|
192
|
+
]).join("\n");
|
|
193
|
+
const next = `${base}${base ? "\n\n" : ""}${TRUST_BEGIN}\n${trustBlock}${TRUST_END}\n`;
|
|
194
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
195
|
+
fs.writeFileSync(configPath, next, { encoding: "utf8", mode: 0o600 });
|
|
196
|
+
fs.chmodSync(configPath, 0o600);
|
|
197
|
+
return entries;
|
|
198
|
+
}
|
|
199
|
+
function configuredTrust(configText, key) {
|
|
200
|
+
const lines = configText.split("\n");
|
|
201
|
+
const header = tableHeader(key);
|
|
202
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
203
|
+
if (start < 0)
|
|
204
|
+
return { enabled: true };
|
|
205
|
+
let enabled = true;
|
|
206
|
+
let hash;
|
|
207
|
+
for (let index = start + 1; index < lines.length && !/^\s*\[/.test(lines[index]); index += 1) {
|
|
208
|
+
const enabledMatch = lines[index].match(/^\s*enabled\s*=\s*(true|false)\s*$/);
|
|
209
|
+
if (enabledMatch)
|
|
210
|
+
enabled = enabledMatch[1] === "true";
|
|
211
|
+
const hashMatch = lines[index].match(/^\s*trusted_hash\s*=\s*"([^"]+)"\s*$/);
|
|
212
|
+
if (hashMatch)
|
|
213
|
+
hash = hashMatch[1];
|
|
214
|
+
}
|
|
215
|
+
return { enabled, hash };
|
|
216
|
+
}
|
|
217
|
+
function inspectCodexHookTrust(configPath, hooksPath, hooksFile, requiredSubcommands = []) {
|
|
218
|
+
const configText = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
219
|
+
const allEntries = codiedevCodexHookTrustEntries(hooksPath, hooksFile);
|
|
220
|
+
const entries = requiredSubcommands.length === 0
|
|
221
|
+
? allEntries
|
|
222
|
+
: allEntries.filter((entry) => requiredSubcommands.some((subcommand) => entry.command.trim().endsWith(subcommand)));
|
|
223
|
+
const trusted = entries.filter((entry) => {
|
|
224
|
+
const state = configuredTrust(configText, entry.key);
|
|
225
|
+
return state.enabled && state.hash === entry.currentHash;
|
|
226
|
+
}).length;
|
|
227
|
+
return { ok: entries.length > 0 && trusted === entries.length, total: entries.length, trusted };
|
|
228
|
+
}
|
package/dist/connect.js
CHANGED
|
@@ -292,6 +292,7 @@ async function runConnect(args = []) {
|
|
|
292
292
|
path.join(home, ".claude", "settings.json"),
|
|
293
293
|
path.join(home, ".codex", "hooks.json"),
|
|
294
294
|
path.join(home, ".config", "opencode", "plugins", "codiedev.js"),
|
|
295
|
+
path.join(home, ".codex", "config.toml"),
|
|
295
296
|
];
|
|
296
297
|
// Snapshot before any hook update. Rollback is intentionally not automatic:
|
|
297
298
|
// an administrator can inspect these 0600 copies before restoring them.
|
package/dist/hook.js
CHANGED
|
@@ -269,7 +269,7 @@ function postJsonForResponse(url, body, bearerToken, timeoutMs) {
|
|
|
269
269
|
// Local policy runs synchronously before a tool executes. Workflow telemetry
|
|
270
270
|
// is dispatched separately and fail-open, so a detector outage or timeout can
|
|
271
271
|
// never delay or override the deterministic endpoint decision.
|
|
272
|
-
async function runSecurityHook(hookInput, phase, source) {
|
|
272
|
+
async function runSecurityHook(hookInput, phase, source, uploadTelemetry = true) {
|
|
273
273
|
const config = (0, utils_1.readConfig)();
|
|
274
274
|
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
275
275
|
return;
|
|
@@ -292,7 +292,8 @@ async function runSecurityHook(hookInput, phase, source) {
|
|
|
292
292
|
outcome: "ok",
|
|
293
293
|
detail: `phase=${phase} localDecision=block category=${localDecision.category}`,
|
|
294
294
|
});
|
|
295
|
-
|
|
295
|
+
if (uploadTelemetry)
|
|
296
|
+
dispatchSecurityEventUpload(event);
|
|
296
297
|
process.stdout.write(JSON.stringify({
|
|
297
298
|
hookSpecificOutput: {
|
|
298
299
|
hookEventName: "PreToolUse",
|
|
@@ -304,16 +305,25 @@ async function runSecurityHook(hookInput, phase, source) {
|
|
|
304
305
|
}
|
|
305
306
|
if (!event)
|
|
306
307
|
return;
|
|
307
|
-
|
|
308
|
+
if (uploadTelemetry)
|
|
309
|
+
dispatchSecurityEventUpload(event);
|
|
308
310
|
}
|
|
309
|
-
function runPromptSecurityHook(hookInput, source) {
|
|
311
|
+
function runPromptSecurityHook(hookInput, source, uploadTelemetry = true) {
|
|
310
312
|
const config = (0, utils_1.readConfig)();
|
|
311
313
|
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
312
314
|
return false;
|
|
313
315
|
const decision = (0, security_1.evaluatePromptSecurity)(hookInput.prompt, (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId), source);
|
|
314
316
|
if (decision.action !== "block")
|
|
315
317
|
return false;
|
|
316
|
-
|
|
318
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
319
|
+
stage: "security",
|
|
320
|
+
sessionId: hookInput.session_id ?? hookInput.sessionId ?? "unknown",
|
|
321
|
+
outcome: "ok",
|
|
322
|
+
detail: `phase=prompt localDecision=block category=${decision.category} telemetry=${uploadTelemetry ? "enabled" : "suppressed"} cwd=${hookInput.cwd ?? "unknown"}`,
|
|
323
|
+
});
|
|
324
|
+
if (uploadTelemetry) {
|
|
325
|
+
dispatchSecurityEventUpload((0, security_1.buildBlockedSecurityPromptEvent)(hookInput, source, decision.category ?? "secret_in_prompt"));
|
|
326
|
+
}
|
|
317
327
|
process.stdout.write(JSON.stringify({
|
|
318
328
|
decision: "block",
|
|
319
329
|
reason: decision.reason,
|
|
@@ -566,13 +576,10 @@ async function main() {
|
|
|
566
576
|
catch {
|
|
567
577
|
process.exit(0);
|
|
568
578
|
}
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
if ((0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput)) {
|
|
574
|
-
process.exit(0);
|
|
575
|
-
}
|
|
579
|
+
// Privacy exclusions suppress every Codiedev upload, but they must never
|
|
580
|
+
// disable local security enforcement. Security hooks run first with telemetry
|
|
581
|
+
// disabled; all other hook modes still exit before making a network request.
|
|
582
|
+
const captureIgnored = (0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput);
|
|
576
583
|
const securityCommand = process.argv[2] ?? "";
|
|
577
584
|
const securityMatch = securityCommand.match(/^security-(pre|post|failure)(?:-(codex|codie))?$/);
|
|
578
585
|
if (securityMatch) {
|
|
@@ -581,7 +588,7 @@ async function main() {
|
|
|
581
588
|
: securityMatch[2] === "codie"
|
|
582
589
|
? "codie-harness"
|
|
583
590
|
: "claude-code";
|
|
584
|
-
await runSecurityHook(hookInput, securityMatch[1], source);
|
|
591
|
+
await runSecurityHook(hookInput, securityMatch[1], source, !captureIgnored);
|
|
585
592
|
process.exit(0);
|
|
586
593
|
}
|
|
587
594
|
// UserPromptSubmit discovery path — separate from session capture.
|
|
@@ -596,12 +603,16 @@ async function main() {
|
|
|
596
603
|
: "claude-code";
|
|
597
604
|
// Security runs first and locally. A blocked prompt is erased before it
|
|
598
605
|
// reaches the model, so discovery must not send it to the backend.
|
|
599
|
-
if (runPromptSecurityHook(hookInput, source))
|
|
606
|
+
if (runPromptSecurityHook(hookInput, source, !captureIgnored))
|
|
607
|
+
return;
|
|
608
|
+
if (captureIgnored)
|
|
600
609
|
return;
|
|
601
610
|
dispatchAllowedPromptSecurityEvent(hookInput, source);
|
|
602
611
|
await runDiscover(hookInput.prompt);
|
|
603
612
|
return;
|
|
604
613
|
}
|
|
614
|
+
if (captureIgnored)
|
|
615
|
+
process.exit(0);
|
|
605
616
|
// StopFailure (matcher rate_limit): post the rate-limit event.
|
|
606
617
|
if (process.argv[2] === "limit") {
|
|
607
618
|
await runStopFailure(hookInput);
|
|
@@ -57,5 +57,6 @@ export declare function restoreSecurityInstallSnapshot(manifest: SecurityInstall
|
|
|
57
57
|
export declare function inspectSecurityInstallation(paths?: {
|
|
58
58
|
claudeSettingsPath?: string;
|
|
59
59
|
codexHooksPath?: string;
|
|
60
|
+
codexConfigPath?: string;
|
|
60
61
|
codiePluginPath?: string;
|
|
61
62
|
}): SecurityInstallInspection[];
|
package/dist/securityInstall.js
CHANGED
|
@@ -45,6 +45,7 @@ const os = __importStar(require("os"));
|
|
|
45
45
|
const path = __importStar(require("path"));
|
|
46
46
|
const configDir_1 = require("./configDir");
|
|
47
47
|
const detection_1 = require("./detection");
|
|
48
|
+
const codexHookTrust_1 = require("./codexHookTrust");
|
|
48
49
|
const version_1 = require("./version");
|
|
49
50
|
const MANIFEST_FILE = "security-install.json";
|
|
50
51
|
function sha256File(filePath) {
|
|
@@ -169,7 +170,7 @@ function hookCommands(parsed, eventName) {
|
|
|
169
170
|
}
|
|
170
171
|
return commands;
|
|
171
172
|
}
|
|
172
|
-
function inspectJsonSurface(surface, filePath) {
|
|
173
|
+
function inspectJsonSurface(surface, filePath, codexConfigPath) {
|
|
173
174
|
if (!fs.existsSync(filePath)) {
|
|
174
175
|
return { surface, status: "not-installed", detail: `${filePath} not found` };
|
|
175
176
|
}
|
|
@@ -180,14 +181,23 @@ function inspectJsonSurface(surface, filePath) {
|
|
|
180
181
|
["UserPromptSubmit", `discover${suffix}`],
|
|
181
182
|
["PreToolUse", `security-pre${suffix}`],
|
|
182
183
|
["PostToolUse", `security-post${suffix}`],
|
|
183
|
-
|
|
184
|
+
...(surface === "claude-code"
|
|
185
|
+
? [["PostToolUseFailure", `security-failure${suffix}`]]
|
|
186
|
+
: []),
|
|
184
187
|
];
|
|
185
188
|
const missing = expected
|
|
186
189
|
.filter(([eventName, subcommand]) => !hookCommands(parsed, eventName).some((command) => command.trim().endsWith(subcommand)))
|
|
187
190
|
.map(([eventName]) => eventName);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
if (missing.length > 0) {
|
|
192
|
+
return { surface, status: "fail", detail: `missing security hooks: ${missing.join(", ")}` };
|
|
193
|
+
}
|
|
194
|
+
if (surface === "codex") {
|
|
195
|
+
const trust = (0, codexHookTrust_1.inspectCodexHookTrust)(codexConfigPath ?? path.join(path.dirname(filePath), "config.toml"), filePath, parsed, ["discover-codex", "security-pre-codex", "security-post-codex"]);
|
|
196
|
+
if (!trust.ok) {
|
|
197
|
+
return { surface, status: "fail", detail: `Codex hook trust missing or stale (${trust.trusted}/${trust.total} trusted)` };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { surface, status: "pass", detail: "prompt guard and tool telemetry hooks installed and active" };
|
|
191
201
|
}
|
|
192
202
|
catch (error) {
|
|
193
203
|
return { surface, status: "fail", detail: `invalid hook configuration: ${error.message}` };
|
|
@@ -200,7 +210,7 @@ function inspectSecurityInstallation(paths = {}) {
|
|
|
200
210
|
const codie = paths.codiePluginPath ?? path.join(home, ".config", "opencode", "plugins", "codiedev.js");
|
|
201
211
|
const results = [
|
|
202
212
|
inspectJsonSurface("claude-code", claude),
|
|
203
|
-
inspectJsonSurface("codex", codex),
|
|
213
|
+
inspectJsonSurface("codex", codex, paths.codexConfigPath),
|
|
204
214
|
];
|
|
205
215
|
if (!fs.existsSync(codie)) {
|
|
206
216
|
results.push({ surface: "codie-harness", status: "not-installed", detail: `${codie} not found` });
|
package/dist/utils.js
CHANGED
|
@@ -117,6 +117,7 @@ const detection_1 = require("./detection");
|
|
|
117
117
|
const captureLog_1 = require("./captureLog");
|
|
118
118
|
const configDir_1 = require("./configDir");
|
|
119
119
|
Object.defineProperty(exports, "codiedevStateDir", { enumerable: true, get: function () { return configDir_1.codiedevStateDir; } });
|
|
120
|
+
const codexHookTrust_1 = require("./codexHookTrust");
|
|
120
121
|
const CONFIG_DIR = (0, configDir_1.codiedevStateDir)();
|
|
121
122
|
exports.CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
122
123
|
function readConfig() {
|
|
@@ -1076,6 +1077,7 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
|
|
|
1076
1077
|
`const AGENT_SECURITY_ENABLED = ${JSON.stringify(agentSecurityEnabled)};\n` +
|
|
1077
1078
|
`const fingerprints = new Map();\n\n` +
|
|
1078
1079
|
`const transcriptPaths = new Map();\n\n` +
|
|
1080
|
+
`const sessionDirectories = new Map();\n\n` +
|
|
1079
1081
|
`function sessionId(event) {\n` +
|
|
1080
1082
|
` return event?.properties?.sessionID ?? event?.properties?.sessionId ?? event?.properties?.info?.id ?? null;\n` +
|
|
1081
1083
|
`}\n\n` +
|
|
@@ -1102,9 +1104,22 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
|
|
|
1102
1104
|
` }\n` +
|
|
1103
1105
|
`}\n\n` +
|
|
1104
1106
|
`export const CodiedevCapture = async ({ client, directory, worktree }) => {\n` +
|
|
1107
|
+
` const resolveCwd = async (id, hint) => {\n` +
|
|
1108
|
+
` if (typeof hint === "string" && hint && hint !== "/") { sessionDirectories.set(id, hint); return hint; }\n` +
|
|
1109
|
+
` const remembered = sessionDirectories.get(id);\n` +
|
|
1110
|
+
` if (remembered) return remembered;\n` +
|
|
1111
|
+
` try {\n` +
|
|
1112
|
+
` const response = await client.session.get({ path: { id } });\n` +
|
|
1113
|
+
` const session = response?.data ?? response;\n` +
|
|
1114
|
+
` if (typeof session?.directory === "string" && session.directory) { sessionDirectories.set(id, session.directory); return session.directory; }\n` +
|
|
1115
|
+
` } catch {}\n` +
|
|
1116
|
+
` const fallback = worktree || directory || process.cwd();\n` +
|
|
1117
|
+
` sessionDirectories.set(id, fallback);\n` +
|
|
1118
|
+
` return fallback;\n` +
|
|
1119
|
+
` };\n` +
|
|
1105
1120
|
` const finalizeAll = () => {\n` +
|
|
1106
1121
|
` for (const [id, transcriptPath] of transcriptPaths) {\n` +
|
|
1107
|
-
` const cwd = worktree || directory || process.cwd();\n` +
|
|
1122
|
+
` const cwd = sessionDirectories.get(id) || worktree || directory || process.cwd();\n` +
|
|
1108
1123
|
` dispatchSync(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
|
|
1109
1124
|
` }\n` +
|
|
1110
1125
|
` };\n` +
|
|
@@ -1112,25 +1127,27 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
|
|
|
1112
1127
|
` return ({\n` +
|
|
1113
1128
|
` "chat.message": async (input, output) => {\n` +
|
|
1114
1129
|
` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
|
|
1115
|
-
` const cwd =
|
|
1130
|
+
` const cwd = await resolveCwd(id);\n` +
|
|
1116
1131
|
` const prompt = Array.isArray(output?.parts) ? output.parts.filter((part) => part?.type === "text").map((part) => part.text || "").join("\\n") : "";\n` +
|
|
1117
1132
|
` enforceSync(PROMPT, { sessionId: id, cwd, prompt });\n` +
|
|
1118
1133
|
` },\n` +
|
|
1119
1134
|
` "tool.execute.before": async (input, output) => {\n` +
|
|
1120
1135
|
` if (!AGENT_SECURITY_ENABLED) return;\n` +
|
|
1121
|
-
` const
|
|
1122
|
-
`
|
|
1136
|
+
` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
|
|
1137
|
+
` const cwd = await resolveCwd(id);\n` +
|
|
1138
|
+
` enforceSync(SECURITY_PRE, { sessionId: id, cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: output?.args ?? output });\n` +
|
|
1123
1139
|
` },\n` +
|
|
1124
1140
|
` "tool.execute.after": async (input, output) => {\n` +
|
|
1125
1141
|
` if (!AGENT_SECURITY_ENABLED) return;\n` +
|
|
1126
|
-
` const
|
|
1127
|
-
` const
|
|
1142
|
+
` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
|
|
1143
|
+
` const cwd = await resolveCwd(id);\n` +
|
|
1144
|
+
` const payload = { sessionId: id, cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: input?.args, toolResponse: output };\n` +
|
|
1128
1145
|
` dispatch(output?.error ? SECURITY_FAILURE : SECURITY_POST, payload);\n` +
|
|
1129
1146
|
` },\n` +
|
|
1130
1147
|
` event: async ({ event }) => {\n` +
|
|
1131
1148
|
` const id = sessionId(event);\n` +
|
|
1132
1149
|
` if (!id) return;\n` +
|
|
1133
|
-
` const cwd =
|
|
1150
|
+
` const cwd = await resolveCwd(id, event?.properties?.info?.directory);\n` +
|
|
1134
1151
|
` if (event.type === "session.created") {\n` +
|
|
1135
1152
|
` dispatch(START, { session_id: id, cwd, workspace_roots: [cwd] });\n` +
|
|
1136
1153
|
` return;\n` +
|
|
@@ -1139,6 +1156,7 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
|
|
|
1139
1156
|
` const transcriptPath = transcriptPaths.get(id);\n` +
|
|
1140
1157
|
` if (transcriptPath) dispatch(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
|
|
1141
1158
|
` transcriptPaths.delete(id);\n` +
|
|
1159
|
+
` sessionDirectories.delete(id);\n` +
|
|
1142
1160
|
` fingerprints.delete(id);\n` +
|
|
1143
1161
|
` return;\n` +
|
|
1144
1162
|
` }\n` +
|
|
@@ -1908,6 +1926,7 @@ function installCodexHook(agentSecurityEnabled = true) {
|
|
|
1908
1926
|
fs.mkdirSync(CODEX_DIR, { recursive: true });
|
|
1909
1927
|
}
|
|
1910
1928
|
fs.writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
|
|
1929
|
+
(0, codexHookTrust_1.writeCodexHookTrust)(CODEX_CONFIG_PATH, CODEX_HOOKS_PATH, hooksFile);
|
|
1911
1930
|
}
|
|
1912
1931
|
// Cursor's hooks.json uses { version: 1, hooks: { sessionEnd: [{ command, type, timeout }] } }.
|
|
1913
1932
|
// `sessionEnd` fires when a conversation closes (completed, aborted, error,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.40",
|
|
4
4
|
"description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codiedev": "dist/cli.js",
|