Skip to content

Commit 641cd40

Browse files
committed
fix(codex): keep doctor sidecar scan inside stateDir
1 parent 3e93458 commit 641cd40

3 files changed

Lines changed: 41 additions & 1 deletion

File tree

extensions/codex/doctor-contract-api.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,37 @@ describe("codex doctor contract", () => {
226226
await fs.rm(stateDir, { recursive: true, force: true });
227227
});
228228

229+
it("does not scan above stateDir when a session store sits at its parent", async () => {
230+
const outerDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-doctor-outer-"));
231+
const stateDir = path.join(outerDir, "state");
232+
await fs.mkdir(stateDir, { recursive: true });
233+
const strayDir = path.join(outerDir, "unrelated");
234+
await fs.mkdir(strayDir, { recursive: true });
235+
await fs.writeFile(
236+
path.join(strayDir, "foreign.jsonl.codex-app-server.json"),
237+
JSON.stringify({ schemaVersion: 2, threadId: "thread-foreign" }),
238+
"utf8",
239+
);
240+
const env = { ...process.env, OPENCLAW_STATE_DIR: stateDir };
241+
const params = {
242+
// The store dir is exactly the parent of stateDir; doctor must treat it
243+
// as an external store (indexed reads only), not a scannable state root.
244+
config: { session: { store: path.join(outerDir, "sessions.json") } },
245+
env,
246+
stateDir,
247+
oauthDir: path.join(stateDir, "oauth"),
248+
context: createDoctorContext(env),
249+
};
250+
const migration = stateMigrations[0];
251+
if (!migration) {
252+
throw new Error("missing Codex binding migration");
253+
}
254+
255+
await expect(migration.detectLegacyState(params)).resolves.toBeNull();
256+
257+
await fs.rm(outerDir, { recursive: true, force: true });
258+
});
259+
229260
it("renames old approval-routed destructive plugin policy values", () => {
230261
const original = {
231262
plugins: {

extensions/codex/src/app-server/session-binding.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,10 @@ export function createCodexAppServerBindingStore(
585585
},
586586
};
587587
},
588+
// Plain clears may expire immediately: a stale generation that re-sets
589+
// the key afterwards is fenced by ownsStoredSessionGeneration on read
590+
// and displaced via reclaim-generation; durable stable-key fences come
591+
// from retireSessionGeneration, not runtime clears.
588592
mutation.kind === "clear" && !retainLegacyClear && !leaseContext.getStore()?.has(key)
589593
? 1
590594
: undefined,

extensions/codex/src/migration/session-binding-sidecars.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,12 @@ async function isRegularFile(filePath: string): Promise<boolean> {
492492

493493
function isPathWithin(root: string, candidate: string): boolean {
494494
const relative = path.relative(root, candidate);
495-
return relative === "" || (!relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
495+
// Bare ".." (candidate is root's parent) must stay outside; treating it as
496+
// inside would let doctor recursively scan the whole tree above stateDir.
497+
return (
498+
relative === "" ||
499+
(relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
500+
);
496501
}
497502

498503
async function canonicalizePath(filePath: string): Promise<string> {

0 commit comments

Comments
 (0)