codiedev 0.9.68 → 0.9.69

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.
@@ -17,7 +17,13 @@ export interface CodexHookTrustEntry {
17
17
  eventName: string;
18
18
  command: string;
19
19
  currentHash: string;
20
+ /** Plugin hooks only: Codex also pins approval to the bundle's contents. */
21
+ contentHash?: string;
20
22
  }
23
+ /** Codex's fingerprint of an installed plugin bundle, byte-for-byte the
24
+ * algorithm in codex-rs hooks/src/engine/plugin_fingerprint.rs. Codex skips a
25
+ * plugin hook unless `trusted_content_hash` equals this (Codex 0.163+). */
26
+ export declare function codexPluginContentHash(root: string): string;
21
27
  /** Codex hashes a canonical, normalized hook identity before it will execute a
22
28
  * user hook. Keep this compatible with codex_config::version_for_toml. */
23
29
  export declare function codexCommandHookHash(eventName: string, group: MatcherGroup, handler: CommandHandler): string;
@@ -32,7 +38,10 @@ export declare function inspectCodexHookTrust(configPath: string, hooksPath: str
32
38
  trusted: number;
33
39
  };
34
40
  /** Trust entries for every command handler in a pack's hooks.json. */
35
- export declare function packCodexHookTrustEntries(hooksPath: string, hooksFile: unknown): CodexHookTrustEntry[];
41
+ export declare function packCodexHookTrustEntries(hooksPath: string, hooksFile: unknown, plugin?: {
42
+ id: string;
43
+ root: string;
44
+ }): CodexHookTrustEntry[];
36
45
  /** Regenerate the pack trust block from scratch with exactly `entries`.
37
46
  * Entries for packs that were removed or whose hooks changed disappear; the
38
47
  * `codiedev-hook-trust` block and all other config are left untouched. */
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.codexPluginContentHash = codexPluginContentHash;
36
37
  exports.codexCommandHookHash = codexCommandHookHash;
37
38
  exports.codiedevCodexHookTrustEntries = codiedevCodexHookTrustEntries;
38
39
  exports.writeCodexHookTrust = writeCodexHookTrust;
@@ -69,6 +70,46 @@ const MATCHER_EVENTS = new Set([
69
70
  "SubagentStart",
70
71
  "SubagentStop",
71
72
  ]);
73
+ /** Codex's fingerprint of an installed plugin bundle, byte-for-byte the
74
+ * algorithm in codex-rs hooks/src/engine/plugin_fingerprint.rs. Codex skips a
75
+ * plugin hook unless `trusted_content_hash` equals this (Codex 0.163+). */
76
+ function codexPluginContentHash(root) {
77
+ const hash = crypto.createHash("sha256");
78
+ hash.update("codex-plugin-bundle-v1");
79
+ const u64 = (n) => {
80
+ const b = Buffer.alloc(8);
81
+ b.writeBigUInt64LE(BigInt(n));
82
+ return b;
83
+ };
84
+ const pending = [root];
85
+ while (pending.length) {
86
+ const current = pending.pop();
87
+ const relative = path.relative(root, current).split(path.sep).join("/");
88
+ const name = path.posix.basename(relative);
89
+ if (name === "__pycache__" || name === ".venv" || name.endsWith(".pyc") || name.endsWith(".egg-info"))
90
+ continue;
91
+ const nameBytes = Buffer.from(relative, "utf8");
92
+ hash.update(u64(nameBytes.length));
93
+ hash.update(nameBytes);
94
+ const stat = fs.lstatSync(current);
95
+ if (stat.isDirectory()) {
96
+ hash.update("directory");
97
+ // Rust sorts by OsString, i.e. bytewise; push reversed so the smallest pops first.
98
+ const entries = fs.readdirSync(current).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
99
+ for (const entry of entries.reverse())
100
+ pending.push(path.join(current, entry));
101
+ }
102
+ else if (stat.isFile()) {
103
+ hash.update("file");
104
+ hash.update(u64(stat.size));
105
+ hash.update(fs.readFileSync(current));
106
+ }
107
+ else {
108
+ throw new Error(`unsupported plugin entry type: ${current}`);
109
+ }
110
+ }
111
+ return `sha256:${hash.digest("hex")}`;
112
+ }
72
113
  function canonicalize(value) {
73
114
  if (Array.isArray(value))
74
115
  return value.map(canonicalize);
@@ -157,6 +198,32 @@ function codiedevCodexHookTrustEntries(hooksPath, hooksFile) {
157
198
  }
158
199
  return entries;
159
200
  }
201
+ /** Remove our marker block, keeping any table that isn't a hook-trust entry.
202
+ * Codex appends new tables (e.g. `[marketplaces.codiedev-packs]`,
203
+ * `[plugins."<id>"]`) at the end of config.toml, which lands them before our
204
+ * closing marker when our block is last. Dropping them with the block silently
205
+ * unregistered the pack from Codex on every reinstall. */
206
+ function stripBlockKeepingForeign(text, begin, end) {
207
+ const foreign = [];
208
+ const stripped = text.replace(new RegExp(`\\n?${begin}\\n([\\s\\S]*?)${end}\\n?`, "g"), (_match, body) => {
209
+ let table = null;
210
+ const flush = () => {
211
+ if (table && table.join("").trim())
212
+ foreign.push(table.join("\n").trimEnd());
213
+ };
214
+ for (const line of body.split("\n")) {
215
+ if (/^\s*\[/.test(line)) {
216
+ flush();
217
+ table = /^\s*\[hooks\.state\./.test(line) ? null : [];
218
+ }
219
+ if (table)
220
+ table.push(line);
221
+ }
222
+ flush();
223
+ return "\n";
224
+ });
225
+ return foreign.length ? `${stripped.trimEnd()}\n\n${foreign.join("\n\n")}\n` : stripped;
226
+ }
160
227
  function tableHeader(key) {
161
228
  return `[hooks.state.${JSON.stringify(key)}]`;
162
229
  }
@@ -181,7 +248,7 @@ function removeTable(lines, header) {
181
248
  function writeCodexHookTrust(configPath, hooksPath, hooksFile) {
182
249
  const entries = codiedevCodexHookTrustEntries(hooksPath, hooksFile);
183
250
  let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
184
- existing = existing.replace(new RegExp(`\\n?${TRUST_BEGIN}\\n[\\s\\S]*?${TRUST_END}\\n?`, "g"), "\n");
251
+ existing = stripBlockKeepingForeign(existing, TRUST_BEGIN, TRUST_END);
185
252
  let lines = existing.split("\n");
186
253
  for (const entry of entries)
187
254
  lines = removeTable(lines, tableHeader(entry.key));
@@ -237,11 +304,16 @@ function inspectCodexHookTrust(configPath, hooksPath, hooksFile, requiredSubcomm
237
304
  const PACK_TRUST_BEGIN = "# codiedev-pack-trust:begin";
238
305
  const PACK_TRUST_END = "# codiedev-pack-trust:end";
239
306
  /** Trust entries for every command handler in a pack's hooks.json. */
240
- function packCodexHookTrustEntries(hooksPath, hooksFile) {
307
+ function packCodexHookTrustEntries(hooksPath, hooksFile, plugin) {
241
308
  const events = hooksFile?.hooks;
242
309
  if (!events || typeof events !== "object")
243
310
  return [];
244
- const source = normalizedHooksPath(hooksPath);
311
+ // Codex keys a plugin's hooks by "<plugin id>:<path inside the plugin>",
312
+ // not by the file's absolute path (codex-rs declarations::plugin_hook_key_source).
313
+ const source = plugin
314
+ ? `${plugin.id}:${path.relative(plugin.root, hooksPath).split(path.sep).join("/")}`
315
+ : normalizedHooksPath(hooksPath);
316
+ const contentHash = plugin ? codexPluginContentHash(plugin.root) : undefined;
245
317
  const entries = [];
246
318
  for (const [eventName, groupsValue] of Object.entries(events)) {
247
319
  const eventLabel = EVENT_LABELS[eventName];
@@ -260,6 +332,7 @@ function packCodexHookTrustEntries(hooksPath, hooksFile) {
260
332
  eventName,
261
333
  command: handler.command,
262
334
  currentHash: codexCommandHookHash(eventName, group, handler),
335
+ ...(contentHash ? { contentHash } : {}),
263
336
  });
264
337
  });
265
338
  });
@@ -271,7 +344,7 @@ function packCodexHookTrustEntries(hooksPath, hooksFile) {
271
344
  * `codiedev-hook-trust` block and all other config are left untouched. */
272
345
  function writeCodexPackHookTrust(configPath, entries) {
273
346
  let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
274
- existing = existing.replace(new RegExp(`\\n?${PACK_TRUST_BEGIN}\\n[\\s\\S]*?${PACK_TRUST_END}\\n?`, "g"), "\n");
347
+ existing = stripBlockKeepingForeign(existing, PACK_TRUST_BEGIN, PACK_TRUST_END);
275
348
  let lines = existing.split("\n");
276
349
  // A stray table for the same key outside our block would make the TOML
277
350
  // invalid (duplicate table) — drop it so ours is authoritative.
@@ -291,6 +364,7 @@ function writeCodexPackHookTrust(configPath, entries) {
291
364
  tableHeader(entry.key),
292
365
  "enabled = true",
293
366
  `trusted_hash = ${JSON.stringify(entry.currentHash)}`,
367
+ ...(entry.contentHash ? [`trusted_content_hash = ${JSON.stringify(entry.contentHash)}`] : []),
294
368
  "",
295
369
  ]).join("\n");
296
370
  const next = `${base}${base ? "\n\n" : ""}${PACK_TRUST_BEGIN}\n${trustBlock}${PACK_TRUST_END}\n`;
@@ -94,7 +94,9 @@ function codexPackTrustEntries(registry, codexDir) {
94
94
  if (!hooksPath)
95
95
  continue;
96
96
  try {
97
- bySlug.set(slug, (0, codexHookTrust_1.packCodexHookTrustEntries)(hooksPath, JSON.parse(fs.readFileSync(hooksPath, "utf8"))));
97
+ // hooks/hooks.json sits at the root of the installed plugin bundle.
98
+ const plugin = { id: `${slug}@${paths_1.MARKETPLACE_NAME}`, root: path.dirname(path.dirname(hooksPath)) };
99
+ bySlug.set(slug, (0, codexHookTrust_1.packCodexHookTrustEntries)(hooksPath, JSON.parse(fs.readFileSync(hooksPath, "utf8")), plugin));
98
100
  }
99
101
  catch {
100
102
  // unreadable hooks → not trusted
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.68",
3
+ "version": "0.9.69",
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",