diff --git a/src/env-guard.ts b/src/env-guard.ts index daa13a19..89a73e4b 100644 --- a/src/env-guard.ts +++ b/src/env-guard.ts @@ -1,4 +1,5 @@ import { derivePairId } from "./pair-registry"; +import { posix as posixPath, win32 as win32Path } from "node:path"; export type EnvGuardMode = "off" | "warn" | "fix" | "strict"; @@ -59,11 +60,11 @@ export function inspectAgentBridgeEnv(opts: { reasons.push(`AGENTBRIDGE_PAIR_ID=${actualPairId} does not match cwd-derived ${expectedPairId}`); } - if (actualPairId && stateDir && !stateDir.endsWith(`/pairs/${actualPairId}`)) { + if (actualPairId && stateDir && !stateDirMatchesPair(stateDir, actualPairId)) { reasons.push(`AGENTBRIDGE_STATE_DIR does not end with /pairs/${actualPairId}`); } - if (actualPairId && baseDir && stateDir && !stateDir.startsWith(`${baseDir}/`)) { + if (actualPairId && baseDir && stateDir && !isPathWithin(stateDir, baseDir)) { reasons.push("AGENTBRIDGE_BASE_DIR and AGENTBRIDGE_STATE_DIR disagree"); } @@ -115,3 +116,29 @@ export function guardAgentBridgeEnv(opts: { function nonEmpty(value: string | undefined): string | null { return value && value.length > 0 ? value : null; } + +/** Normalize paths independently of the host OS so Windows paths can be checked in tests on POSIX too. */ +function normalizePathForComparison(value: string): { path: string; windows: boolean } { + const windows = /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\"); + const normalized = windows + ? win32Path.normalize(value).replaceAll("\\", "/") + : posixPath.normalize(value); + return { path: windows ? normalized.toLowerCase() : normalized, windows }; +} + +function stateDirMatchesPair(stateDir: string, pairId: string): boolean { + const normalizedPath = normalizePathForComparison(stateDir); + const normalized = normalizedPath.path.replace(/\/+$/, ""); + const normalizedPairId = normalizedPath.windows ? pairId.toLowerCase() : pairId; + return normalized.endsWith(`/pairs/${normalizedPairId}`); +} + +function isPathWithin(candidate: string, parent: string): boolean { + const normalizedCandidate = normalizePathForComparison(candidate); + const normalizedParent = normalizePathForComparison(parent); + if (normalizedCandidate.windows !== normalizedParent.windows) return false; + + const parentPath = normalizedParent.path.replace(/\/+$/, ""); + const prefix = parentPath === "" ? "/" : `${parentPath}/`; + return normalizedCandidate.path.startsWith(prefix); +} diff --git a/src/unit-test/env-guard.test.ts b/src/unit-test/env-guard.test.ts index 00fd30ce..aa41fc25 100644 --- a/src/unit-test/env-guard.test.ts +++ b/src/unit-test/env-guard.test.ts @@ -41,6 +41,38 @@ describe("AgentBridge env guard", () => { expect(inspectAgentBridgeEnv({ cwd, env }).ok).toBe(true); }); + test("accepts native Windows state paths and preserves valid generated env in fix mode", () => { + const cwd = tempCwd("windows-path"); + const pairId = derivePairId(cwd, "main"); + const env = { + AGENTBRIDGE_PAIR_ID: pairId, + AGENTBRIDGE_PAIR_NAME: "main", + AGENTBRIDGE_BASE_DIR: "C:\\Users\\Trevor\\.agentbridge", + AGENTBRIDGE_STATE_DIR: `C:\\Users\\Trevor\\.agentbridge\\pairs\\${pairId}`, + AGENTBRIDGE_CONTROL_PORT: "4502", + } as NodeJS.ProcessEnv; + + const result = guardAgentBridgeEnv({ cwd, env, mode: "fix" }); + + expect(result.action).toBe("none"); + expect(result.ok).toBe(true); + expect(env.AGENTBRIDGE_PAIR_ID).toBe(pairId); + expect(env.AGENTBRIDGE_STATE_DIR).toBe(`C:\\Users\\Trevor\\.agentbridge\\pairs\\${pairId}`); + }); + + test("accepts mixed separators and case differences in Windows path comparisons", () => { + const cwd = tempCwd("windows-path-case"); + const pairId = derivePairId(cwd, "main"); + const env = { + AGENTBRIDGE_PAIR_ID: pairId, + AGENTBRIDGE_PAIR_NAME: "main", + AGENTBRIDGE_BASE_DIR: "c:/users/trevor/.AgentBridge/", + AGENTBRIDGE_STATE_DIR: `C:\\Users\\Trevor\\.agentbridge/pairs/${pairId.toUpperCase()}`, + } as NodeJS.ProcessEnv; + + expect(inspectAgentBridgeEnv({ cwd, env }).ok).toBe(true); + }); + test("fix mode clears stale generated env before pair resolution can enter manual mode", () => { const cwd = tempCwd("fix"); const oldCwd = tempCwd("old");