From ed854640da2519daa01598464d3aa28baf3f54f2 Mon Sep 17 00:00:00 2001 From: Wilian Colombo Date: Wed, 2 Sep 2026 01:52:13 -0300 Subject: [PATCH] fix(resume): isolate resume keys by working directory hash to prevent cross-session collision (#102) --- src/backend/resume-store.ts | 38 ++++++++++++++++++++++++++++--------- test/resume-store.test.mts | 14 +++++++++++++- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/backend/resume-store.ts b/src/backend/resume-store.ts index aa3d478..46b8774 100644 --- a/src/backend/resume-store.ts +++ b/src/backend/resume-store.ts @@ -1,12 +1,14 @@ -// src/backend/resume-store.ts — file-backed sessionKey → backendSessionId, per backend (SPEC-3 §2.4, §4.3). +// src/backend/resume-store.ts — file-backed sessionKey -> backendSessionId, per backend (SPEC-3 §2.4, §4.3). +// Hardened against cross-session/cross-cwd result contamination (#102). import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; +import { createHash } from "node:crypto"; function rootDir(): string { return process.env.FLEET_RESUME_ROOT ?? join(process.env.HOME ?? "/tmp", ".pi", "agent", "cache", "fleet-resume"); } -/** Per-backend JSON map: { [sessionKey]: backendSessionId }. */ +/** Per-backend JSON map: { [scopedKey]: backendSessionId }. */ function fileFor(backendId: string): string { return join(rootDir(), `${backendId}.json`); } @@ -28,17 +30,35 @@ function writeMap(backendId: string, m: Record): void { } export class ResumeStore { - get(backendId: string, sessionKey: string): string | null { - return readMap(backendId)[sessionKey] ?? null; + private readonly defaultCwd?: string; + + constructor(defaultCwd?: string) { + this.defaultCwd = defaultCwd; + } + + private scopeKey(sessionKey: string, cwd?: string): string { + const activeCwd = cwd ?? this.defaultCwd; + if (!activeCwd) return sessionKey; + const cwdHash = createHash("sha256").update(activeCwd).digest("hex").slice(0, 12); + return `${cwdHash}:${sessionKey}`; + } + + get(backendId: string, sessionKey: string, cwd?: string): string | null { + const key = this.scopeKey(sessionKey, cwd); + return readMap(backendId)[key] ?? null; } - set(backendId: string, sessionKey: string, backendSessionId: string): void { + + set(backendId: string, sessionKey: string, backendSessionId: string, cwd?: string): void { + const key = this.scopeKey(sessionKey, cwd); const m = readMap(backendId); - m[sessionKey] = backendSessionId; + m[key] = backendSessionId; writeMap(backendId, m); } - clear(backendId: string, sessionKey: string): void { + + clear(backendId: string, sessionKey: string, cwd?: string): void { + const key = this.scopeKey(sessionKey, cwd); const m = readMap(backendId); - delete m[sessionKey]; + delete m[key]; writeMap(backendId, m); } -} \ No newline at end of file +} diff --git a/test/resume-store.test.mts b/test/resume-store.test.mts index ffc0e7b..f29a9e9 100644 --- a/test/resume-store.test.mts +++ b/test/resume-store.test.mts @@ -37,4 +37,16 @@ test("persists across instances (file-backed)", () => { s1.set("claude", "foo", "sess-1"); const s2 = new ResumeStore(); // re-reads the file strictEqual(s2.get("claude", "foo"), "sess-1"); -}); \ No newline at end of file +}); +test("isolates sessionKey resume when different cwds are provided (#102)", () => { + delete process.env.FLEET_RESUME_ROOT; + const s = new ResumeStore(); + const cwdA = "/Users/rector/local-dev/armory-fleet"; + const cwdB = "/Users/rector/local-dev/bug-bounty/hunts/layerzero"; + + s.set("claude", "general-purpose", "sess-alpha", cwdA); + s.set("claude", "general-purpose", "sess-beta", cwdB); + + strictEqual(s.get("claude", "general-purpose", cwdA), "sess-alpha"); + strictEqual(s.get("claude", "general-purpose", cwdB), "sess-beta"); +});