← Files Codex Process JobsARCHIVED FILE
scripts/preferences.mjs
4.86 KB · Sep 30, 2026 · 23:13 UTC
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { ensureStateDirs, resolveStateRoot } from "./state.mjs";
export const PREFERENCES_SCHEMA_VERSION = 1;
export const COMPLETION_MODES = new Set(["auto", "report", "inspect"]);
const MAX_PREFERENCES_BYTES = 16 * 1024;
const PREFERENCE_KEYS = new Set([
"schemaVersion",
"completionMode",
"notifyUser",
"cliLiveInjection",
]);
export function resolvePreferencesFile(env = process.env) {
return path.join(resolveStateRoot(env), "config.json");
}
function defaultPreferences() {
return {
schemaVersion: PREFERENCES_SCHEMA_VERSION,
completionMode: "auto",
notifyUser: null,
cliLiveInjection: false,
};
}
function validatePreferences(value, file) {
if (value == null || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`Process-jobs preferences must be a JSON object: ${file}`);
}
for (const key of Object.keys(value)) {
if (!PREFERENCE_KEYS.has(key)) {
throw new Error(`Unknown process-jobs preference key ${key} in ${file}.`);
}
}
if (value.schemaVersion !== PREFERENCES_SCHEMA_VERSION) {
throw new Error(`Unsupported process-jobs preferences schema in ${file}.`);
}
if (!COMPLETION_MODES.has(value.completionMode)) {
throw new Error(`Invalid completionMode in ${file}.`);
}
if (value.notifyUser != null && typeof value.notifyUser !== "boolean") {
throw new Error(`Invalid notifyUser in ${file}.`);
}
if (value.cliLiveInjection != null && typeof value.cliLiveInjection !== "boolean") {
throw new Error(`Invalid cliLiveInjection in ${file}.`);
}
return {
schemaVersion: PREFERENCES_SCHEMA_VERSION,
completionMode: value.completionMode,
// null means "no durable preference": launch resolution may then apply a
// surface default instead of treating the absence as an explicit opt-out.
notifyUser: value.notifyUser ?? null,
// Experimental CLI injection remains fail-closed and opt-in. Missing this
// field in an older schema-v1 file therefore preserves the safe default.
cliLiveInjection: value.cliLiveInjection ?? false,
};
}
function validatePrivateFile(file) {
const stat = fs.lstatSync(file);
if (!stat.isFile() || stat.isSymbolicLink()) {
throw new Error(`Process-jobs preferences path is not a regular file: ${file}`);
}
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
throw new Error(`Process-jobs preferences are not owned by the current user: ${file}`);
}
if ((stat.mode & 0o077) !== 0) {
throw new Error(`Process-jobs preferences are accessible by group or other users: ${file}`);
}
if (stat.size > MAX_PREFERENCES_BYTES) {
throw new Error(`Process-jobs preferences exceed ${MAX_PREFERENCES_BYTES} bytes: ${file}`);
}
}
export function readPreferences(env = process.env) {
const file = resolvePreferencesFile(env);
try {
validatePrivateFile(file);
} catch (error) {
if (error?.code === "ENOENT") return defaultPreferences();
throw error;
}
let parsed;
try {
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
} catch (error) {
throw new Error(`Unable to parse process-jobs preferences at ${file}: ${error.message}`);
}
return validatePreferences(parsed, file);
}
export function writePreferences(
{ completionMode = null, notifyUser = null, cliLiveInjection = null },
env = process.env,
) {
ensureStateDirs(env);
const file = resolvePreferencesFile(env);
const current = fs.existsSync(file) ? readPreferences(env) : defaultPreferences();
const nextCompletionMode = completionMode ?? current.completionMode;
// "default" clears the stored preference back to null so surface defaults
// apply again; null/undefined preserves whatever is currently stored.
const nextNotifyUser = notifyUser === "default" ? null : notifyUser ?? current.notifyUser;
const nextCliLiveInjection = cliLiveInjection ?? current.cliLiveInjection;
if (!COMPLETION_MODES.has(nextCompletionMode)) {
throw new Error(`completion mode must be one of: ${[...COMPLETION_MODES].join(", ")}`);
}
if (nextNotifyUser != null && typeof nextNotifyUser !== "boolean") {
throw new Error("notifyUser must be a boolean.");
}
if (typeof nextCliLiveInjection !== "boolean") {
throw new Error("cliLiveInjection must be a boolean.");
}
const preferences = {
schemaVersion: PREFERENCES_SCHEMA_VERSION,
completionMode: nextCompletionMode,
notifyUser: nextNotifyUser,
cliLiveInjection: nextCliLiveInjection,
};
const temporary = `${file}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
try {
fs.writeFileSync(temporary, `${JSON.stringify(preferences, null, 2)}\n`, {
mode: 0o600,
flag: "wx",
});
fs.renameSync(temporary, file);
fs.chmodSync(file, 0o600);
} finally {
try {
fs.rmSync(temporary, { force: true });
} catch {}
}
return preferences;
}
SHA-256: a4fc21996537a4ef3cd444dd23c00b9f54825fb4056127249f227d314f98f6df