Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/env-guard.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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");
}

Expand Down Expand Up @@ -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);
}
32 changes: 32 additions & 0 deletions src/unit-test/env-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading