mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
fix(router): accept Windows verbatim workspace paths (#1460)
This commit is contained in:
@@ -15,6 +15,7 @@ import { startHealthServer, type HealthSnapshot } from "./health.js";
|
||||
import { type InboundMessagePart, type MessageDeliveryResult, type OutboundMessagePart, normalizeOutboundParts, summarizeInboundPartsForPrompt, summarizeInboundPartsForReporter, textFromInboundParts } from "./media.js";
|
||||
import { MediaStore } from "./media-store.js";
|
||||
import { buildPermissionRules, createClient } from "./opencode.js";
|
||||
import { isWithinWorkspaceRootPath, normalizeScopedDirectoryPath } from "./path-scope.js";
|
||||
import { chunkText, formatInputSummary, truncateText } from "./text.js";
|
||||
import { createSlackAdapter } from "./slack.js";
|
||||
import { createTelegramAdapter, isTelegramPeerId } from "./telegram.js";
|
||||
@@ -442,25 +443,17 @@ export async function startBridge(config: Config, logger: Logger, reporter?: Bri
|
||||
|
||||
const formatPeer = (_channel: ChannelName, peerId: string) => peerId;
|
||||
|
||||
const normalizeDirectory = (input: string) => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return "";
|
||||
const unified = trimmed.replace(/\\/g, "/");
|
||||
const withoutTrailing = unified.replace(/\/+$/, "");
|
||||
const normalized = withoutTrailing || "/";
|
||||
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
||||
};
|
||||
const normalizeDirectory = (input: string) =>
|
||||
normalizeScopedDirectoryPath(input, process.platform);
|
||||
|
||||
const workspaceRootNormalized = normalizeDirectory(workspaceRoot);
|
||||
|
||||
const isWithinWorkspaceRoot = (candidate: string) => {
|
||||
const resolved = resolve(candidate || workspaceRoot);
|
||||
const relativePath = relative(workspaceRoot, resolved);
|
||||
if (!relativePath) return true;
|
||||
if (relativePath === ".") return true;
|
||||
if (relativePath.startsWith("..") || isAbsolute(relativePath)) return false;
|
||||
const boundary = workspaceRoot.endsWith(sep) ? workspaceRoot : `${workspaceRoot}${sep}`;
|
||||
return resolved === workspaceRoot || resolved.startsWith(boundary);
|
||||
return isWithinWorkspaceRootPath({
|
||||
workspaceRoot,
|
||||
candidate,
|
||||
platform: process.platform,
|
||||
});
|
||||
};
|
||||
|
||||
const resolveScopedDirectory = (input: string): { ok: true; directory: string } | { ok: false; error: string } => {
|
||||
|
||||
42
apps/opencode-router/src/path-scope.ts
Normal file
42
apps/opencode-router/src/path-scope.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { isAbsolute, relative, resolve } from "node:path";
|
||||
|
||||
export function normalizeScopedDirectoryPath(input: string, platform = process.platform) {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return "";
|
||||
const withoutVerbatim = /^\\\\\?\\UNC[\\/]/i.test(trimmed)
|
||||
? `\\${trimmed.slice(8)}`
|
||||
: /^\\\\\?\\[a-zA-Z]:[\\/]/.test(trimmed)
|
||||
? trimmed.slice(4)
|
||||
: trimmed;
|
||||
const unified = withoutVerbatim.replace(/\\/g, "/");
|
||||
const withoutTrailing = unified.replace(/\/+$/, "");
|
||||
const normalized = withoutTrailing || "/";
|
||||
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
||||
}
|
||||
|
||||
export function isWithinWorkspaceRootPath(input: {
|
||||
workspaceRoot: string;
|
||||
candidate: string;
|
||||
platform?: NodeJS.Platform;
|
||||
}) {
|
||||
const platform = input.platform ?? process.platform;
|
||||
const rootForComparison =
|
||||
platform === "win32"
|
||||
? normalizeScopedDirectoryPath(input.workspaceRoot, platform)
|
||||
: input.workspaceRoot;
|
||||
const resolved = resolve(input.candidate || input.workspaceRoot);
|
||||
const resolvedForComparison =
|
||||
platform === "win32"
|
||||
? normalizeScopedDirectoryPath(resolved, platform)
|
||||
: resolved;
|
||||
const relativePath = relative(rootForComparison, resolvedForComparison);
|
||||
if (!relativePath || relativePath === ".") return true;
|
||||
if (relativePath.startsWith("..") || isAbsolute(relativePath)) return false;
|
||||
const boundary = rootForComparison.endsWith("/")
|
||||
? rootForComparison
|
||||
: `${rootForComparison}/`;
|
||||
return (
|
||||
resolvedForComparison === rootForComparison ||
|
||||
resolvedForComparison.startsWith(boundary)
|
||||
);
|
||||
}
|
||||
49
apps/opencode-router/test/path-scope.test.js
Normal file
49
apps/opencode-router/test/path-scope.test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
isWithinWorkspaceRootPath,
|
||||
normalizeScopedDirectoryPath,
|
||||
} from "../dist/path-scope.js";
|
||||
|
||||
test("normalizeScopedDirectoryPath strips Windows verbatim prefixes", () => {
|
||||
const workspaceRoot = String.raw`G:\project\openwork_project`;
|
||||
const candidate = String.raw`\\?\G:\project\openwork_project`;
|
||||
|
||||
assert.equal(
|
||||
normalizeScopedDirectoryPath(workspaceRoot, "win32"),
|
||||
"g:/project/openwork_project",
|
||||
);
|
||||
assert.equal(
|
||||
normalizeScopedDirectoryPath(candidate, "win32"),
|
||||
"g:/project/openwork_project",
|
||||
);
|
||||
});
|
||||
|
||||
test("isWithinWorkspaceRootPath accepts Windows verbatim aliases for workspace root", () => {
|
||||
const workspaceRoot = String.raw`G:\project\openwork_project`;
|
||||
const candidate = String.raw`\\?\G:\project\openwork_project`;
|
||||
|
||||
assert.equal(
|
||||
isWithinWorkspaceRootPath({
|
||||
workspaceRoot,
|
||||
candidate,
|
||||
platform: "win32",
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("isWithinWorkspaceRootPath still rejects directories outside the workspace root", () => {
|
||||
const workspaceRoot = String.raw`G:\project\openwork_project`;
|
||||
const candidate = String.raw`\\?\G:\project\outside`;
|
||||
|
||||
assert.equal(
|
||||
isWithinWorkspaceRootPath({
|
||||
workspaceRoot,
|
||||
candidate,
|
||||
platform: "win32",
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user