fix: fail closed on invalid codex resume bindings

This commit is contained in:
kris
2026-04-04 13:08:08 +08:00
parent 40b78c5cae
commit da78e82a90
2 changed files with 45 additions and 18 deletions

View File

@@ -1,3 +1,5 @@
import os from "node:os";
import { readFileSync } from "node:fs";
import { constants } from "node:fs";
import { access, stat } from "node:fs/promises";
import { DatabaseSync } from "node:sqlite";
@@ -21,6 +23,20 @@ function resolveResumeTarget(config, task) {
};
}
function defaultCodexPath(relativePath) {
return resolve(os.homedir(), ".codex", relativePath);
}
function loadThreadWorkspaceHints(globalStatePath) {
try {
const raw = readFileSync(resolve(globalStatePath), "utf8");
const parsed = JSON.parse(raw);
return new Map(Object.entries(parsed["thread-workspace-root-hints"] ?? {}));
} catch {
return new Map();
}
}
function shouldPreflightResumeTask(task) {
const taskType = String(task?.taskType || "").trim();
if (taskType === "dispatch_execution") {
@@ -47,7 +63,7 @@ function buildStructuredTaskBindingError(code, message, details) {
}
function inspectCodexThreadBinding(config, targetThreadRef, targetFolderRef) {
const stateDbPath = trimToDefined(config?.codexStateDbPath);
const stateDbPath = trimToDefined(config?.codexStateDbPath || defaultCodexPath("state_5.sqlite"));
if (!stateDbPath) {
return {
status: "skipped",
@@ -66,7 +82,10 @@ function inspectCodexThreadBinding(config, targetThreadRef, targetFolderRef) {
};
}
const threadCwd = trimToDefined(row.cwd) || "";
const workspaceHints = loadThreadWorkspaceHints(
trimToDefined(config?.codexGlobalStatePath || defaultCodexPath(".codex-global-state.json")),
);
const threadCwd = trimToDefined(workspaceHints.get(targetThreadRef)) || trimToDefined(row.cwd) || "";
if (threadCwd && resolve(threadCwd) !== resolve(targetFolderRef)) {
return {
status: "mismatch",
@@ -129,22 +148,6 @@ export async function prepareCodexTaskExecution(config, task, outputFile) {
};
}
if (bindingInspection.status === "mismatch") {
return {
ok: false,
error: buildStructuredTaskBindingError(
"LOCAL_AGENT_CODEX_THREAD_BINDING_MISMATCH",
`LOCAL_AGENT_CODEX_THREAD_BINDING_MISMATCH: 目标线程绑定的 cwd 与当前目录不一致,已拒绝 codex exec resume。cwd=${resumeTarget.cwd} liveCwd=${bindingInspection.threadCwd}`,
{
cwd: resumeTarget.cwd,
liveCwd: bindingInspection.threadCwd,
targetThreadRef,
targetCodexFolderRef: resumeTarget.targetFolderRef,
},
),
};
}
try {
const folderStat = await stat(resumeTarget.cwd);
if (!folderStat.isDirectory()) {
@@ -166,6 +169,22 @@ export async function prepareCodexTaskExecution(config, task, outputFile) {
};
}
if (bindingInspection.status === "mismatch") {
return {
ok: false,
error: buildStructuredTaskBindingError(
"LOCAL_AGENT_CODEX_THREAD_BINDING_MISMATCH",
`LOCAL_AGENT_CODEX_THREAD_BINDING_MISMATCH: 目标线程绑定的 cwd 与当前目录不一致,已拒绝 codex exec resume。cwd=${resumeTarget.cwd} liveCwd=${bindingInspection.threadCwd}`,
{
cwd: resumeTarget.cwd,
liveCwd: bindingInspection.threadCwd,
targetThreadRef,
targetCodexFolderRef: resumeTarget.targetFolderRef,
},
),
};
}
return {
ok: true,
execution: buildCodexTaskExecution(config, task, outputFile),

View File

@@ -170,10 +170,18 @@ test("master agent reply without target thread stays on ephemeral exec", () => {
test("conversation reply preflight fails closed when target cwd is missing", async () => {
const missingFolder = "/tmp/boss-local-agent-missing-workdir";
const stateDbPath = await createCodexStateDb([
{
id: "019d-thread-real",
cwd: "/Users/kris/code/boss",
title: "Real thread",
},
]);
const result = await prepareCodexTaskExecution(
{
masterAgentWorkdir: "/Users/kris/code/boss",
masterAgentSandbox: "workspace-write",
codexStateDbPath: stateDbPath,
},
{
taskType: "conversation_reply",