fix: renew content approval for verified OpenRig Codex plugin upgrades

This commit is contained in:
dev-driver committed 2026-10-24 22:47:44 +00:00
1 parent 32633a3d8f
commit ed8ca39666
4 files changed
+282 -30

No files matched your search

+6 -4
View File
@@ -37,10 +37,12 @@ launched before this change, a manual run) the hooks do what they always did. Th
the seat's own `settings.local.json` copy only when that entry is exactly OpenRig's and the script it names is the same
script; an entry from an older install doesn't match, so both report until the seat's next launch refreshes it.
OpenRig automatically installs its Codex plugin for seats that select it. On its first installation, the six exact shipped
hook definitions are trusted before launch, after checking the installed hook files against this daemon's shipped copy.
Existing plugin disables and hook choices are retained. An existing untrusted installation stays under native review:
Codex's "continue without trusting" leaves no record, so OpenRig does not assume an absent trust record is permission.
The file check happens only when trust is granted: a later edit to the cached hook scripts keeps that trust, as with any hook trusted in Codex.
hook definitions and bundle content are trusted before launch, after checking the entire installed plugin against this daemon's shipped copy.
On upgrade, OpenRig renews content approval only for a previously approved OpenRig-owned bundle with unchanged approved hook definitions.
It keeps explicit disables, refusals, unknown config layers and other plugins' state. An installation without a proven prior approval stays under native review.
Codex's "continue without trusting" leaves no record: a skip of an otherwise eligible OpenRig-owned update may therefore be superseded on retry.
OpenRig records its approved bundle in `CODEX_HOME/.openrig-core-hook-trust.json` and leaves Codex's immutable `.hook-trust` baseline untouched.
Codex 0.164 checks bundle contents when hooks run; a later edit requires native approval again. Older Codex versions enforce their own trust rules.
The Codex
column above then applies only to a Codex seat whose plugin selection includes openrig-core: every Codex hook command
exits 0 before running anything unless OpenRig gave that seat `OPENRIG_SEAT_PROJECT_DIR` and an explicit
@@ -66,6 +66,7 @@ const codexHookTrust = createRequire(import.meta.url)("../../assets/plugins/open
import { listNativeProcesses, observeCodexPaneProcess, observeCodexForkProcess, verifyPaneIdleShell, type NativeProcessRow } from "../domain/native-process-lineage.js";
import { recordSeatSkillWrite, seatPluginSkillsLossNote, seatProjectionComplete, seatSkillWriteRefusal } from "../domain/seat-skill-storage.js";
import { codexPluginContentHash, planOwnedCodexContentApproval, recordOwnedCodexApproval } from "../domain/codex-owned-plugin-trust.js";
// Shared by all probes of ONE launch, never reset by a delayed screen or an
// ambiguous transport result. A separately requested launch gets a new attempt.
@@ -311,9 +312,9 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
/**
* Install the vendored openrig-core plugin into Codex through Codex's own commands, so a seat that selected it gets its
* refocus and skills, trusting only exact shipped hooks on its first install. It runs only when the person's config
* refocus and skills, trusting exact shipped hooks on first install and renewing proven owned content approvals. It runs only when the person's config
* doesn't disable the plugin (codexCorePluginEnablement), and only when the installed version differs from the vendored
* one. Codex owns its plugin cache; OpenRig writes the marketplace manifest and first-install exact-hook trust.
* one. Codex owns its plugin cache; its immutable legacy trust baseline is never rewritten.
*/
async ensureCodexCorePlugin(): Promise<{ outcome: "installed" | "current" | "skipped"; reason: string }> {
const report = (outcome: "installed" | "current" | "skipped", reason: string) => {
@@ -328,28 +329,19 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
if (typeof version !== "string" || !/^[0-9A-Za-z._-]+$/.test(version)) return report("skipped", "the vendored plugin's Codex manifest has no usable version");
const shipped = nodePath.resolve(import.meta.dirname, "../../assets/plugins/openrig-core");
const ownFiles: string[] = [];
const collect = (dir: string, relative = "hooks") => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const child = nodePath.join(relative, entry.name);
if (entry.isDirectory()) collect(nodePath.join(dir, entry.name), child);
else ownFiles.push(child);
}
};
collect(nodePath.join(shipped, "hooks"));
ownFiles.push(".codex-plugin/plugin.json");
const matchesShipped = (dir: string) => ownFiles.every(relative => {
try { return fs.readFileSync(nodePath.join(dir, relative)).equals(fs.readFileSync(nodePath.join(shipped, relative))); }
const matchesShipped = (dir: string) => {
try { return codexPluginContentHash(dir, true) === codexPluginContentHash(shipped, true); }
catch { return false; }
});
if (!matchesShipped(pluginDir)) return report("skipped", "vendored hook files differ from this daemon's shipped plugin; existing choices are left alone");
};
if (!matchesShipped(pluginDir)) return report("skipped", "vendored files differ from this daemon's shipped plugin; existing choices are left alone");
const codexHome = nodePath.dirname(this.resolveCodexConfigPath());
const layers: CodexConfigLayerRead[] = codexReadableConfigLayers(codexHome, this.codexSystemConfigDir).map((layer) => {
const readLayers = (): CodexConfigLayerRead[] => codexReadableConfigLayers(codexHome, this.codexSystemConfigDir).map((layer) => {
if (!this.fs.exists(layer.path)) return { ...layer, state: "absent" as const };
try { return { ...layer, state: "read" as const, content: this.fs.readFile(layer.path) }; }
catch (error) { return { ...layer, state: "unreadable" as const, reason: (error as Error).message }; }
});
const layers = readLayers();
const enablement = codexCorePluginEnablement(layers);
if (enablement.decision !== "allow") return report("skipped", enablement.reason);
@@ -370,11 +362,27 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
const cachedDir = nodePath.join(cacheRoot, version);
const cached = this.fs.exists(cachedDir);
// Codex's "continue without trusting" choice writes no state. An existing install/configuration may therefore be a
// decline even with no hooks.state entry. Only the very first installation earns automatic trust; reinstalls and
// upgrades retain whatever native choices already exist, including an unrecorded decline.
// decline even with no hooks.state entry. Absent grants still stay under native review. A proven owned grant may
// renew its content approval; an unrecorded skip of that exact owned upgrade cannot be distinguished from pending approval.
const firstInstall = !this.fs.exists(cacheRoot) && !layers.some(layer => layer.state === "read"
&& Object.hasOwn((parseToml(layer.content).plugins ?? {}) as object, CODEX_CORE_PLUGIN_KEY));
const renewContentApproval = (freshGrant = false) => {
const configPath = this.resolveCodexConfigPath();
const currentLayers = readLayers();
if (codexCorePluginEnablement(currentLayers).decision !== "allow") return;
const before = this.fs.readFile(configPath);
const currentSource = recordedMarketplaceSource(before);
if (currentSource == null || canonical(currentSource) !== canonical(pluginsDir) || !userLayerEnablesCorePlugin(before)) return;
let content = before;
const planned = planOwnedCodexContentApproval({ configPath, content, layers: currentLayers, shipped, installed: cachedDir, freshGrant });
if (!planned) return;
for (const [key, hash] of Object.entries(planned.hashes)) content = upsertCodexHookTrust(content, key, hash, "trusted_content_hash");
if (this.fs.readFile(configPath) !== before) return; // A concurrent native choice wins.
if (content !== before) this.fs.writeFile(configPath, content);
recordOwnedCodexApproval(configPath, planned.approval);
};
if (cached && recorded !== undefined && userLayerEnablesCorePlugin(userConfig)) {
renewContentApproval();
return report("current", `${CODEX_CORE_PLUGIN_KEY} ${version} is installed and registered`);
}
const manifestPath = nodePath.join(pluginsDir, CODEX_MARKETPLACE_MANIFEST_PATH);
@@ -390,9 +398,11 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
}
const installed = await this.runCodexCli(["plugin", "add", CODEX_CORE_PLUGIN_KEY], env);
if (installed.status !== 0) return report("skipped", `codex plugin add failed: ${(installed.stderr || installed.stdout).trim() || `exit ${installed.status}`}`);
// Trust is over normalized definitions, not script contents. Byte equality is an additional pre-grant check;
// Codex remains responsible for later native review if definitions change. No cache files are rewritten here.
if (firstInstall && matchesShipped(cachedDir)) {
// First install grants declarations; Codex0.164 also requires full-bundle content approval. Existing declaration
// choices are never rewritten. No cache files or native baseline files are rewritten here.
let freshGrants = 0;
const grantLayers = readLayers();
if (firstInstall && codexCorePluginEnablement(grantLayers).decision === "allow" && matchesShipped(cachedDir)) {
let content = this.fs.readFile(this.resolveCodexConfigPath());
const hookManifest = JSON.parse(fs.readFileSync(nodePath.join(shipped, "hooks/codex.json"), "utf8"));
for (const [event, groups] of Object.entries(hookManifest.hooks) as Array<[string, Array<{ matcher?: string; hooks: Array<{ command: string; timeout: number }> }>]> ) {
@@ -407,13 +417,15 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
const value = doc as { hooks?: { state?: Record<string, unknown> } };
return Object.hasOwn(value.hooks?.state ?? {}, key) || Object.values(doc).some(hasChoice);
};
if (!hasChoice(parseToml(content)) && !layers.some(layer => layer.state === "read" && hasChoice(parseToml(layer.content)))) {
if (!hasChoice(parseToml(content)) && !grantLayers.some(layer => layer.state === "read" && hasChoice(parseToml(layer.content)))) {
content = upsertCodexHookTrust(content, key, hash);
freshGrants++;
}
}));
}
this.fs.writeFile(this.resolveCodexConfigPath(), content);
}
renewContentApproval(freshGrants === 6);
return report("installed", `${CODEX_CORE_PLUGIN_KEY} ${version} through Codex's own install (${enablement.reason})`);
}
@@ -1743,9 +1755,9 @@ export function computeCodexHookTrust(
* `[hooks.state]` / `[projects]` entry and the managed hook block byte-identical. Same key+hash
* ⇒ no-op. Only called for our authored inline or exact first-install plugin keys (never blanket trust).
*/
export function upsertCodexHookTrust(content: string, key: string, hash: string): string {
export function upsertCodexHookTrust(content: string, key: string, hash: string, field: "trusted_hash" | "trusted_content_hash" = "trusted_hash"): string {
const header = `[hooks.state.${JSON.stringify(key)}]`;
const trustLine = `trusted_hash = ${JSON.stringify(hash)}`;
const trustLine = `${field} = ${JSON.stringify(hash)}`;
const lines = content.length > 0 ? content.split("\n") : [];
const headerIndex = lines.findIndex((line) => line.trim() === header);
@@ -1764,7 +1776,7 @@ export function upsertCodexHookTrust(content: string, key: string, hash: string)
}
const hashIndex = lines.findIndex(
(line, index) => index > headerIndex && index < nextSectionIndex && line.trim().startsWith("trusted_hash"),
(line, index) => index > headerIndex && index < nextSectionIndex && line.trim().startsWith(field),
);
if (hashIndex >= 0) {
lines[hashIndex] = trustLine;
@@ -0,0 +1,141 @@
import fs from "node:fs";
import path from "node:path";
import { createHash, randomUUID } from "node:crypto";
import { createRequire } from "node:module";
import { parse } from "smol-toml";
import { CODEX_CORE_PLUGIN_KEY, type CodexConfigLayerRead } from "./codex-core-plugin-install.js";
const { computeCodexHookTrust } = createRequire(import.meta.url)("../../assets/plugins/openrig-core/hooks/scripts/codex-hook-trust.cjs");
// Published openrig-core0.1.17: Codex0.164's native baseline and the complete shipped tree agree.
const LEGACY_CONTENT = process.platform === "win32" ? "sha256:f94b92a51872b7a5c0ba57d762a8cd68208a10f4e4ffed701220feafab626889" : "sha256:3317076e0df0d10402c17f26a17f26a21264980a6f0b551a1e21a3c0f785efca";
const LEGACY_DECLARATIONS: Record<string, string> = {
session_start: "a746b5abb568d50725b08bf39660bbf08d15fa3d3a5961208862e7beaadba43d",
user_prompt_submit_0: "f56dfb2a70982d6a7ae4a6c4ace58b943e16dcb08fe3bc87d3c8803477f4a5c1",
user_prompt_submit_1: "c648714b3a02675ba4ea029b8e62dbcd77569afc317ad4f1bc3e8cbfbe7b34ee",
stop: "bb444b151db38aff570d0ceadc2cc84b21afb021f00b15b326aef55a27d87d40",
post_compact: "f4dd49518599c67ac23c80daef3537bb03b02e23a1e8c1da1210534766afefae",
permission_request: "75c66b08f505a22f84def73ecf6d61e98dd1b24ed8ecdcc05306eba3013e64d4",
};
const legacyDeclarations = Object.fromEntries(Object.entries(LEGACY_DECLARATIONS).map(([event, hash]) => {
const match = /^(user_prompt_submit)_([01])$/.exec(event);
return [`${CODEX_CORE_PLUGIN_KEY}:hooks/codex.json:${match?.[1] ?? event}:${match?.[2] ?? 0}:0`, `sha256:${hash}`];
}));
const readFlags = fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0);
/** Codex rust-v0.164.0 plugin_fingerprint.rs. Strict comparison also includes entries Codex excludes as generated files. */
export function codexPluginContentHash(root: string, strict = false): string {
if (!fs.lstatSync(root).isDirectory()) throw new Error("plugin root is not a directory");
const hash = createHash("sha256").update("codex-plugin-bundle-v1");
const length = (value: number) => { const bytes = Buffer.alloc(8); bytes.writeBigUInt64LE(BigInt(value)); hash.update(bytes); };
const visit = (file: string) => {
const name = path.basename(file);
if (file !== root && !strict && (name === "__pycache__" || name === ".venv" || name.endsWith(".pyc") || name.endsWith(".egg-info"))) return;
const relative = Buffer.from(path.relative(root, file)); length(relative.length); hash.update(relative);
const before = fs.lstatSync(file);
if (before.isDirectory()) {
hash.update("directory");
for (const child of fs.readdirSync(file).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)))) visit(path.join(file, child));
} else if (before.isFile()) {
hash.update("file"); length(before.size);
const fd = fs.openSync(file, readFlags);
try {
const opened = fs.fstatSync(fd);
if (!opened.isFile() || opened.ino !== before.ino || opened.dev !== before.dev) throw new Error("plugin file changed while fingerprinting");
const buffer = Buffer.alloc(65536); let total = 0;
while (total <= before.size) {
const count = fs.readSync(fd, buffer, 0, Math.min(buffer.length, before.size + 1 - total), null);
if (!count) break;
total += count; hash.update(buffer.subarray(0, count));
}
if (total !== before.size) throw new Error("plugin file changed while fingerprinting");
} finally { fs.closeSync(fd); }
} else throw new Error("unsupported plugin entry type");
};
visit(root);
return `sha256:${hash.digest("hex")}`;
}
function readRecord(file: string): string | undefined {
let fd: number;
try { fd = fs.openSync(file, readFlags); }
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; throw error; }
try {
const info = fs.fstatSync(fd);
if (!info.isFile() || info.size > 65536 || (process.platform !== "win32" && (info.mode & 0o022) !== 0)
|| (process.getuid && info.uid !== process.getuid())) throw new Error("unreadable hook approval record");
return fs.readFileSync(fd, "utf8");
} finally { fs.closeSync(fd); }
}
export function codexLegacyPluginTrustPath(configPath: string): string {
const hash = createHash("sha256");
for (const part of [configPath, CODEX_CORE_PLUGIN_KEY]) {
const bytes = Buffer.from(part), size = Buffer.alloc(8); size.writeBigUInt64LE(BigInt(bytes.length)); hash.update(size); hash.update(bytes);
}
return path.join(path.dirname(configPath), ".hook-trust", hash.digest("hex"));
}
interface OwnedApproval { schema: 1; plugin: string; contentHash: string; declarations: Record<string, string> }
export const ownedCodexApprovalPath = (configPath: string) => path.join(path.dirname(configPath), ".openrig-core-hook-trust.json");
/** No native baseline is changed. Existing declarations must already be approved; only content approval may advance. */
export function planOwnedCodexContentApproval(input: {
configPath: string; content: string; layers: CodexConfigLayerRead[]; shipped: string; installed: string; freshGrant?: boolean;
}): { hashes: Record<string, string>; approval: OwnedApproval } | undefined {
try {
if (codexPluginContentHash(input.shipped, true) !== codexPluginContentHash(input.installed, true)) return;
const contentHash = codexPluginContentHash(input.installed);
const declarations: Record<string, string> = {};
const manifest = JSON.parse(fs.readFileSync(path.join(input.shipped, "hooks/codex.json"), "utf8"));
for (const [event, groups] of Object.entries(manifest.hooks) as Array<[string, Array<{ matcher?: string; hooks: Array<{ command: string; timeout: number }> }>]> ) {
groups.forEach((group, groupIndex) => group.hooks.forEach((handler, handlerIndex) => {
const { key, hash } = computeCodexHookTrust(event, { ...handler, matcher: group.matcher, timeoutSec: handler.timeout,
keySource: `${CODEX_CORE_PLUGIN_KEY}:hooks/codex.json`, groupIndex, handlerIndex });
declarations[key] = hash;
}));
}
if (Object.keys(declarations).length !== 6) return;
const docs = input.layers.map(layer => {
if (layer.state === "unreadable") throw new Error("unknown config layer");
return { ...layer, doc: layer.state === "read" ? parse(layer.content) : {} };
});
const doc = parse(input.content) as any;
const otherChoice = (value: any, key: string, checkRoot = true): boolean => value !== null && typeof value === "object"
&& ((checkRoot && Object.hasOwn(value.hooks?.state ?? {}, key)) || Object.values(value).some(child => otherChoice(child, key)));
const nestedPluginChoice = (value: any, nested = false): boolean => value !== null && typeof value === "object"
&& ((nested && (Object.hasOwn(value.plugins ?? {}, CODEX_CORE_PLUGIN_KEY) || Object.hasOwn(value.plugins ?? {}, "_default")))
|| Object.values(value).some(child => nestedPluginChoice(child, true)));
if (docs.some(layer => nestedPluginChoice(layer.doc))) return;
const states = doc.hooks?.state ?? {};
for (const [key, hash] of Object.entries(declarations)) {
const state = states[key];
if (!state || (state.enabled !== undefined && state.enabled !== true) || state.trusted_hash !== hash
|| Object.keys(state).some(field => !["enabled", "trusted_hash", "trusted_content_hash"].includes(field))
|| !input.content.split("\n").some(line => line.trim() === `[hooks.state.${JSON.stringify(key)}]`)
|| docs.some(layer => otherChoice(layer.doc, key, layer.path !== input.configPath))) return;
}
const approval: OwnedApproval = { schema: 1, plugin: CODEX_CORE_PLUGIN_KEY, contentHash, declarations };
const hashes = Object.fromEntries(Object.keys(declarations).map(key => [key, contentHash]));
if (input.freshGrant) return { hashes, approval };
if (Object.keys(declarations).every(key => states[key].trusted_content_hash === contentHash)) return { hashes: {}, approval };
const baseline = readRecord(codexLegacyPluginTrustPath(input.configPath));
if (Object.keys(declarations).every(key => (states[key].trusted_content_hash ?? baseline) === contentHash)) return { hashes, approval };
const saved = readRecord(ownedCodexApprovalPath(input.configPath));
const previous: OwnedApproval = saved === undefined ? { schema: 1, plugin: CODEX_CORE_PLUGIN_KEY,
contentHash: LEGACY_CONTENT, declarations: legacyDeclarations } : JSON.parse(saved);
if (previous.schema !== 1 || previous.plugin !== CODEX_CORE_PLUGIN_KEY || !/^sha256:[a-f0-9]{64}$/.test(previous.contentHash)
|| Object.keys(previous.declarations).length !== 6) return;
for (const [key, hash] of Object.entries(declarations)) {
if (previous.declarations[key] !== hash || (states[key].trusted_content_hash ?? baseline) !== previous.contentHash) return;
}
return { hashes, approval };
} catch { return undefined; } // Unknown ownership or config leaves native review in place.
}
export function recordOwnedCodexApproval(configPath: string, approval: OwnedApproval): void {
const file = ownedCodexApprovalPath(configPath), content = `${JSON.stringify(approval, null, 2)}\n`;
if (readRecord(file) === content) return;
const temporary = `${file}.${randomUUID()}.tmp`;
try { fs.writeFileSync(temporary, content, { flag: "wx", mode: 0o600 }); fs.renameSync(temporary, file); }
finally { try { fs.unlinkSync(temporary); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } }
}
@@ -4,9 +4,11 @@ import os from "node:os";
import path from "node:path";
import http from "node:http";
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { parse } from "smol-toml";
import { CodexRuntimeAdapter } from "../src/adapters/codex-runtime-adapter.js";
import type { TmuxAdapter } from "../src/adapters/tmux.js";
import { codexPluginContentHash, codexLegacyPluginTrustPath, planOwnedCodexContentApproval } from "../src/domain/codex-owned-plugin-trust.js";
const shipped = path.resolve(import.meta.dirname, "../assets/plugins/openrig-core");
const roots: string[] = [];
@@ -50,6 +52,7 @@ it("a fresh install has all six exact plugin hooks trusted before the first seat
"openrig-core@openrig:hooks/codex.json:permission_request:0:0": "sha256:75c66b08f505a22f84def73ecf6d61e98dd1b24ed8ecdcc05306eba3013e64d4"
};
expect(Object.fromEntries(Object.entries(states).map(([key,value]:[string,any])=>[key,value.trusted_hash]))).toEqual(expected);
expect(Object.values(states).every((state: any) => state.trusted_content_hash === codexPluginContentHash(shipped))).toBe(true);
expect(f.calls.map(a=>a.slice(0,2))).toEqual([["plugin","marketplace"],["plugin","add"]]);
});
it.each(['[plugins."openrig-core@openrig"]\nenabled = false\n','[plugins._default]\nenabled = false\n'])("a disabled plugin stays disabled on restart and upgrade: %s", async config => {
@@ -112,3 +115,97 @@ it("a same-named marketplace owned by someone else is not installed or trusted",
const f=fixture(config);await f.adapter().ensureCodexCorePlugin();
expect(fs.readFileSync(f.file,"utf8")).toBe(config);expect(f.calls).toEqual([]);
});
// The interrupted-upgrade state: the new bundle is already installed, but Codex0.164
// retains the declaration-only grants and immutable content baseline from published0.7.0.
async function legacyUpgrade() {
const f = fixture();
await f.adapter().ensureCodexCorePlugin();
fs.writeFileSync(f.file, fs.readFileSync(f.file, "utf8").replace(/^trusted_content_hash = .*\n/gm, ""));
fs.rmSync(path.join(f.home, ".openrig-core-hook-trust.json"), { force: true });
const identity = createHash("sha256");
for (const part of [f.file, "openrig-core@openrig"]) {
const bytes = Buffer.from(part), length = Buffer.alloc(8); length.writeBigUInt64LE(BigInt(bytes.length));
identity.update(length); identity.update(bytes);
}
const baseline = path.join(f.home, ".hook-trust", identity.digest("hex"));
fs.mkdirSync(path.dirname(baseline), { recursive: true });
fs.writeFileSync(baseline, process.platform === "win32" ? "sha256:f94b92a51872b7a5c0ba57d762a8cd68208a10f4e4ffed701220feafab626889" : "sha256:3317076e0df0d10402c17f26a17f26a21264980a6f0b551a1e21a3c0f785efca");
return { ...f, baseline };
}
it("an already-installed owned upgrade renews all six content approvals without changing other choices or the native baseline", async () => {
const f = await legacyUpgrade();
fs.appendFileSync(f.file, '\n[hooks.state."outside:stop:0:0"]\nenabled = false\ntrusted_hash = "outside-choice"\n');
const before = fs.readFileSync(f.file, "utf8"), baseline = fs.readFileSync(f.baseline, "utf8"), calls = f.calls.length;
const expectedContent = codexPluginContentHash(shipped);
await f.adapter().ensureCodexCorePlugin();
const states = Object.entries(f.read().hooks.state).filter(([key]) => key.startsWith("openrig-core@openrig:"));
expect(states).toHaveLength(6);
for (const [, state] of states as Array<[string, any]>) {
expect(state.trusted_content_hash).toBe(expectedContent);
}
expect(fs.readFileSync(f.file, "utf8").replace(/^trusted_content_hash = .*\n/gm, "")).toBe(before);
expect(fs.readFileSync(f.baseline, "utf8")).toBe(baseline);
expect(f.calls).toHaveLength(calls);
const renewed = fs.readFileSync(f.file, "utf8");
await f.adapter().ensureCodexCorePlugin();
expect(fs.readFileSync(f.file, "utf8")).toBe(renewed);
expect(JSON.parse(fs.readFileSync(path.join(f.home, ".openrig-core-hook-trust.json"), "utf8"))).toMatchObject({
contentHash: expectedContent,
});
});
it.each(["disabled", "refused", "unknown baseline", "changed script", "changed skill", "extra file", ...(process.platform === "win32" ? [] : ["symlink"]), "unknown layer", "profile refusal", "profile plugin disable", "invalid provenance"])(
"an owned upgrade preserves native review for %s", async condition => {
const f = await legacyUpgrade();
const header = '[hooks.state."openrig-core@openrig:hooks/codex.json:stop:0:0"]';
if (condition === "disabled") fs.writeFileSync(f.file, fs.readFileSync(f.file, "utf8").replace(header, `${header}\nenabled = false`));
if (condition === "refused") fs.writeFileSync(f.file, fs.readFileSync(f.file, "utf8").replace(/trusted_hash = "sha256:bb444[^"]+"/, 'trusted_hash = "not-the-owned-declaration"'));
if (condition === "unknown baseline") fs.writeFileSync(f.baseline, "unknown");
if (condition === "changed script") fs.appendFileSync(path.join(f.cache, "hooks/scripts/refocus.cjs"), "\nmodified");
if (condition === "changed skill") fs.appendFileSync(path.join(f.cache, "skills/openrig-user/SKILL.md"), "\nmodified");
if (condition === "extra file") fs.writeFileSync(path.join(f.cache, "unexpected"), "extra");
if (condition === "symlink") fs.symlinkSync(f.plugin, path.join(f.cache, "unexpected-link"));
if (condition === "unknown layer") { fs.mkdirSync(path.join(f.home, "../etc")); fs.writeFileSync(path.join(f.home, "../etc/config.toml"), "not valid toml"); }
if (condition === "profile refusal") fs.appendFileSync(f.file, '\n[profiles.special.hooks.state."openrig-core@openrig:hooks/codex.json:stop:0:0"]\nenabled = false\n');
if (condition === "profile plugin disable") fs.appendFileSync(f.file, '\n[profiles.special.plugins."openrig-core@openrig"]\nenabled = false\n');
if (condition === "invalid provenance") fs.writeFileSync(path.join(f.home, ".openrig-core-hook-trust.json"), "broken");
const before = fs.readFileSync(f.file, "utf8");
await f.adapter().ensureCodexCorePlugin();
expect(fs.readFileSync(f.file, "utf8")).toBe(before);
if (condition !== "invalid provenance") expect(fs.existsSync(path.join(f.home, ".openrig-core-hook-trust.json"))).toBe(false);
});
it("the owned receipt carries approval into a later bundle while preserving the previous declarations", async () => {
const f = fixture(); await f.adapter().ensureCodexCorePlugin();
const next = path.join(f.home, "next-shipped"); fs.cpSync(shipped, next, { recursive: true });
fs.appendFileSync(path.join(next, "skills/openrig-user/SKILL.md"), "\nnext shipped revision");
fs.cpSync(next, f.cache, { recursive: true });
const plan = planOwnedCodexContentApproval({ configPath: f.file, content: fs.readFileSync(f.file, "utf8"),
layers: [{ name: "user config", path: f.file, state: "read", content: fs.readFileSync(f.file, "utf8") }], shipped: next, installed: f.cache });
expect(plan).toBeDefined();
expect(Object.values(plan!.hashes)).toEqual(Array(6).fill(codexPluginContentHash(next)));
expect(f.read().hooks.state["openrig-core@openrig:hooks/codex.json:stop:0:0"].trusted_hash).toBe(plan!.approval.declarations["openrig-core@openrig:hooks/codex.json:stop:0:0"]);
});
it("records an already-approved current legacy bundle for subsequent upgrades", async () => {
const f = await legacyUpgrade(), current = codexPluginContentHash(shipped);
fs.writeFileSync(f.baseline, current);
await f.adapter().ensureCodexCorePlugin();
expect(Object.values(f.read().hooks.state).every((state: any) => state.trusted_content_hash === current)).toBe(true);
expect(JSON.parse(fs.readFileSync(path.join(f.home, ".openrig-core-hook-trust.json"), "utf8"))).toMatchObject({ contentHash: current });
expect(fs.readFileSync(f.baseline, "utf8")).toBe(current);
});
it("uses the native bundle fingerprint and legacy identity recipes", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-fingerprint-")); roots.push(root);
fs.mkdirSync(path.join(root, "nested")); fs.writeFileSync(path.join(root, "hello.txt"), "hello\n");
fs.writeFileSync(path.join(root, "nested/other.txt"), "world\n");
// Independent Python implementation of the rust-v0.164.0 byte stream, not the grant planner.
expect(codexPluginContentHash(root)).toBe(process.platform === "win32" ? "sha256:e7690d54fade38332715024d03cf7c7e5cb26f77dbba1c60e589e2be9ef42d5e" : "sha256:bc4d2c619163c3c9e4fd0a0740b7de1e5b39b3eb2cef3d52b8a8f582a1f13ce5");
fs.mkdirSync(path.join(root, ".venv")); fs.writeFileSync(path.join(root, ".venv/ignored"), "generated");
expect(codexPluginContentHash(root)).toBe(process.platform === "win32" ? "sha256:e7690d54fade38332715024d03cf7c7e5cb26f77dbba1c60e589e2be9ef42d5e" : "sha256:bc4d2c619163c3c9e4fd0a0740b7de1e5b39b3eb2cef3d52b8a8f582a1f13ce5");
expect(codexPluginContentHash(root, true)).not.toBe(codexPluginContentHash(root));
expect(path.basename(codexLegacyPluginTrustPath("/home/operator/.codex/config.toml"))).toBe("5107db44b2124b5303069076c8cbcc824811a62710c30a9188b2f8465e5e7cb4");
});