codiedev 0.9.39 → 0.9.41
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/codie.js +18 -1
- package/dist/codieLauncher.d.ts +5 -0
- package/dist/codieLauncher.js +22 -1
- 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 +42 -12
- 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/codie.js
CHANGED
|
@@ -232,9 +232,26 @@ async function main() {
|
|
|
232
232
|
OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
|
|
233
233
|
};
|
|
234
234
|
const runtimeArgs = (0, codieLauncher_1.withCodieWorkingDirectory)(parsed.args, process.cwd());
|
|
235
|
-
const
|
|
235
|
+
const nonInteractiveRun = runtimeArgs[0] === "run";
|
|
236
|
+
if (nonInteractiveRun)
|
|
237
|
+
env.CODIEDEV_SECURITY_BLOCK_OUTPUT = "marker";
|
|
238
|
+
const result = (0, child_process_1.spawnSync)(runtime, runtimeArgs, {
|
|
239
|
+
stdio: nonInteractiveRun ? ["inherit", "inherit", "pipe"] : "inherit",
|
|
240
|
+
encoding: nonInteractiveRun ? "utf8" : undefined,
|
|
241
|
+
env,
|
|
242
|
+
});
|
|
236
243
|
if (result.error)
|
|
237
244
|
throw result.error;
|
|
245
|
+
if (nonInteractiveRun) {
|
|
246
|
+
const stderr = typeof result.stderr === "string" ? result.stderr : result.stderr?.toString() ?? "";
|
|
247
|
+
const blockReason = (0, codieLauncher_1.extractCodieSecurityBlock)(stderr);
|
|
248
|
+
if (blockReason) {
|
|
249
|
+
console.error(`Codie Security blocked this action:\n${blockReason}`);
|
|
250
|
+
}
|
|
251
|
+
else if (stderr) {
|
|
252
|
+
process.stderr.write(stderr);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
238
255
|
process.exitCode = result.status === null ? 1 : result.status;
|
|
239
256
|
}
|
|
240
257
|
main().catch((error) => {
|
package/dist/codieLauncher.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ export interface CatalogModel {
|
|
|
2
2
|
id: string;
|
|
3
3
|
name?: string;
|
|
4
4
|
}
|
|
5
|
+
export declare const CODIE_SECURITY_BLOCK_MARKER = "__CODIEDEV_SECURITY_BLOCK__";
|
|
6
|
+
/** Extract the private adapter marker used to carry a local block reason
|
|
7
|
+
* across OpenCode's generic 500 boundary. The marker never contains prompt or
|
|
8
|
+
* tool input — only the deterministic policy reason. */
|
|
9
|
+
export declare function extractCodieSecurityBlock(stderr: string): string | undefined;
|
|
5
10
|
/**
|
|
6
11
|
* Pin non-interactive runs to the directory where `codie` was launched.
|
|
7
12
|
* OpenCode can reuse a previously-known project instance; without an explicit
|
package/dist/codieLauncher.js
CHANGED
|
@@ -42,7 +42,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
42
42
|
};
|
|
43
43
|
})();
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.HIDDEN_WORDMARK = exports.CODIEDEV_WORDMARK = exports.LEGACY_CODIEDEV_WORDMARK = exports.STOCK_WORDMARK = exports.BRAND_PLUGIN_SPEC = void 0;
|
|
45
|
+
exports.HIDDEN_WORDMARK = exports.CODIEDEV_WORDMARK = exports.LEGACY_CODIEDEV_WORDMARK = exports.STOCK_WORDMARK = exports.BRAND_PLUGIN_SPEC = exports.CODIE_SECURITY_BLOCK_MARKER = void 0;
|
|
46
|
+
exports.extractCodieSecurityBlock = extractCodieSecurityBlock;
|
|
46
47
|
exports.withCodieWorkingDirectory = withCodieWorkingDirectory;
|
|
47
48
|
exports.bareModel = bareModel;
|
|
48
49
|
exports.buildCodieConfig = buildCodieConfig;
|
|
@@ -62,6 +63,26 @@ const DEFAULT_NAMES = {
|
|
|
62
63
|
"z-ai/glm-5.2": "GLM 5.2 · Fireworks ZDR",
|
|
63
64
|
"moonshotai/kimi-k2.7-code": "Kimi K2.7 Code · Fireworks ZDR",
|
|
64
65
|
};
|
|
66
|
+
exports.CODIE_SECURITY_BLOCK_MARKER = "__CODIEDEV_SECURITY_BLOCK__";
|
|
67
|
+
/** Extract the private adapter marker used to carry a local block reason
|
|
68
|
+
* across OpenCode's generic 500 boundary. The marker never contains prompt or
|
|
69
|
+
* tool input — only the deterministic policy reason. */
|
|
70
|
+
function extractCodieSecurityBlock(stderr) {
|
|
71
|
+
for (const line of stderr.split(/\r?\n/)) {
|
|
72
|
+
const markerIndex = line.indexOf(exports.CODIE_SECURITY_BLOCK_MARKER);
|
|
73
|
+
if (markerIndex < 0)
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(line.slice(markerIndex + exports.CODIE_SECURITY_BLOCK_MARKER.length));
|
|
77
|
+
if (typeof parsed?.reason === "string" && parsed.reason.trim())
|
|
78
|
+
return parsed.reason.trim();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Ignore malformed or foreign stderr and preserve the original runtime error.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
65
86
|
/**
|
|
66
87
|
* Pin non-interactive runs to the directory where `codie` was launched.
|
|
67
88
|
* OpenCode can reuse a previously-known project instance; without an explicit
|
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` +
|
|
@@ -1091,20 +1093,44 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
|
|
|
1091
1093
|
`}\n\n` +
|
|
1092
1094
|
`function enforceSync(invocation, payload) {\n` +
|
|
1093
1095
|
` const result = spawnSync(invocation.command, invocation.args, { input: JSON.stringify(payload), encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 1500 });\n` +
|
|
1094
|
-
` if (result.error) return;\n` +
|
|
1096
|
+
` if (result.error) return null;\n` +
|
|
1095
1097
|
` try {\n` +
|
|
1096
1098
|
` const decision = JSON.parse(result.stdout || "{}");\n` +
|
|
1097
1099
|
` const denied = decision?.decision === "block" || decision?.hookSpecificOutput?.permissionDecision === "deny";\n` +
|
|
1098
|
-
`
|
|
1100
|
+
` return denied ? (decision?.reason || decision?.hookSpecificOutput?.permissionDecisionReason || "Codie security policy blocked this action") : null;\n` +
|
|
1099
1101
|
` } catch (error) {\n` +
|
|
1100
|
-
` if (error instanceof SyntaxError) return;\n` +
|
|
1101
|
-
`
|
|
1102
|
+
` if (error instanceof SyntaxError) return null;\n` +
|
|
1103
|
+
` return null;\n` +
|
|
1102
1104
|
` }\n` +
|
|
1103
1105
|
`}\n\n` +
|
|
1104
1106
|
`export const CodiedevCapture = async ({ client, directory, worktree }) => {\n` +
|
|
1107
|
+
` const enforce = async (invocation, payload) => {\n` +
|
|
1108
|
+
` const reason = enforceSync(invocation, payload);\n` +
|
|
1109
|
+
` if (!reason) return;\n` +
|
|
1110
|
+
` try { await client.tui.showToast({ title: "Codie Security", message: reason, variant: "error", duration: 10000 }); } catch {}\n` +
|
|
1111
|
+
` if (process.env.CODIEDEV_SECURITY_BLOCK_OUTPUT === "marker") {\n` +
|
|
1112
|
+
` process.stderr.write("__CODIEDEV_SECURITY_BLOCK__" + JSON.stringify({ reason }) + "\\n");\n` +
|
|
1113
|
+
` } else {\n` +
|
|
1114
|
+
` process.stderr.write("\\nCodie Security blocked this action:\\n" + reason + "\\n");\n` +
|
|
1115
|
+
` }\n` +
|
|
1116
|
+
` throw new Error(reason);\n` +
|
|
1117
|
+
` };\n` +
|
|
1118
|
+
` const resolveCwd = async (id, hint) => {\n` +
|
|
1119
|
+
` if (typeof hint === "string" && hint && hint !== "/") { sessionDirectories.set(id, hint); return hint; }\n` +
|
|
1120
|
+
` const remembered = sessionDirectories.get(id);\n` +
|
|
1121
|
+
` if (remembered) return remembered;\n` +
|
|
1122
|
+
` try {\n` +
|
|
1123
|
+
` const response = await client.session.get({ path: { id } });\n` +
|
|
1124
|
+
` const session = response?.data ?? response;\n` +
|
|
1125
|
+
` if (typeof session?.directory === "string" && session.directory) { sessionDirectories.set(id, session.directory); return session.directory; }\n` +
|
|
1126
|
+
` } catch {}\n` +
|
|
1127
|
+
` const fallback = worktree || directory || process.cwd();\n` +
|
|
1128
|
+
` sessionDirectories.set(id, fallback);\n` +
|
|
1129
|
+
` return fallback;\n` +
|
|
1130
|
+
` };\n` +
|
|
1105
1131
|
` const finalizeAll = () => {\n` +
|
|
1106
1132
|
` for (const [id, transcriptPath] of transcriptPaths) {\n` +
|
|
1107
|
-
` const cwd = worktree || directory || process.cwd();\n` +
|
|
1133
|
+
` const cwd = sessionDirectories.get(id) || worktree || directory || process.cwd();\n` +
|
|
1108
1134
|
` dispatchSync(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
|
|
1109
1135
|
` }\n` +
|
|
1110
1136
|
` };\n` +
|
|
@@ -1112,25 +1138,27 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
|
|
|
1112
1138
|
` return ({\n` +
|
|
1113
1139
|
` "chat.message": async (input, output) => {\n` +
|
|
1114
1140
|
` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
|
|
1115
|
-
` const cwd =
|
|
1141
|
+
` const cwd = await resolveCwd(id);\n` +
|
|
1116
1142
|
` const prompt = Array.isArray(output?.parts) ? output.parts.filter((part) => part?.type === "text").map((part) => part.text || "").join("\\n") : "";\n` +
|
|
1117
|
-
`
|
|
1143
|
+
` await enforce(PROMPT, { sessionId: id, cwd, prompt });\n` +
|
|
1118
1144
|
` },\n` +
|
|
1119
1145
|
` "tool.execute.before": async (input, output) => {\n` +
|
|
1120
1146
|
` if (!AGENT_SECURITY_ENABLED) return;\n` +
|
|
1121
|
-
` const
|
|
1122
|
-
`
|
|
1147
|
+
` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
|
|
1148
|
+
` const cwd = await resolveCwd(id);\n` +
|
|
1149
|
+
` await enforce(SECURITY_PRE, { sessionId: id, cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: output?.args ?? output });\n` +
|
|
1123
1150
|
` },\n` +
|
|
1124
1151
|
` "tool.execute.after": async (input, output) => {\n` +
|
|
1125
1152
|
` if (!AGENT_SECURITY_ENABLED) return;\n` +
|
|
1126
|
-
` const
|
|
1127
|
-
` const
|
|
1153
|
+
` const id = input?.sessionID ?? input?.sessionId ?? "unknown";\n` +
|
|
1154
|
+
` const cwd = await resolveCwd(id);\n` +
|
|
1155
|
+
` const payload = { sessionId: id, cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: input?.args, toolResponse: output };\n` +
|
|
1128
1156
|
` dispatch(output?.error ? SECURITY_FAILURE : SECURITY_POST, payload);\n` +
|
|
1129
1157
|
` },\n` +
|
|
1130
1158
|
` event: async ({ event }) => {\n` +
|
|
1131
1159
|
` const id = sessionId(event);\n` +
|
|
1132
1160
|
` if (!id) return;\n` +
|
|
1133
|
-
` const cwd =
|
|
1161
|
+
` const cwd = await resolveCwd(id, event?.properties?.info?.directory);\n` +
|
|
1134
1162
|
` if (event.type === "session.created") {\n` +
|
|
1135
1163
|
` dispatch(START, { session_id: id, cwd, workspace_roots: [cwd] });\n` +
|
|
1136
1164
|
` return;\n` +
|
|
@@ -1139,6 +1167,7 @@ function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
|
|
|
1139
1167
|
` const transcriptPath = transcriptPaths.get(id);\n` +
|
|
1140
1168
|
` if (transcriptPath) dispatch(FINISH, { session_id: id, transcript_path: transcriptPath, cwd, workspace_roots: [cwd] });\n` +
|
|
1141
1169
|
` transcriptPaths.delete(id);\n` +
|
|
1170
|
+
` sessionDirectories.delete(id);\n` +
|
|
1142
1171
|
` fingerprints.delete(id);\n` +
|
|
1143
1172
|
` return;\n` +
|
|
1144
1173
|
` }\n` +
|
|
@@ -1908,6 +1937,7 @@ function installCodexHook(agentSecurityEnabled = true) {
|
|
|
1908
1937
|
fs.mkdirSync(CODEX_DIR, { recursive: true });
|
|
1909
1938
|
}
|
|
1910
1939
|
fs.writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksFile, null, 2), "utf8");
|
|
1940
|
+
(0, codexHookTrust_1.writeCodexHookTrust)(CODEX_CONFIG_PATH, CODEX_HOOKS_PATH, hooksFile);
|
|
1911
1941
|
}
|
|
1912
1942
|
// Cursor's hooks.json uses { version: 1, hooks: { sessionEnd: [{ command, type, timeout }] } }.
|
|
1913
1943
|
// `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.41",
|
|
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",
|