fix(windows): resolve reviewed plugin paths without walking up to C:\

Hosted Windows on 54f2b1a69 still failed 13 tests (down from 92). Six
native MCP / preset activations died with `EPERM: operation not
permitted, lstat 'C:\'`: the reviewed-closure checks in the extension host
(admitReviewedClosure, importReviewedModule, the Bun onLoad symlink check
and the shell-hook config check) call JS realpathSync, which lstats every
ancestor from the drive root, and a Windows LPAC host cannot read C:\.
On Windows only, use realpathSync.native, which asks the OS for the opened
file's final path and needs access to that file alone. macOS/Linux keep the
JS implementation unchanged. The symlink/closure checks are unchanged.

Rebuilt with a clean npm ci + node build.mjs: exactly the four bundles that
embed these modules changed; the builtin harness/mcp bundles and
builtin-modules.json rebuilt byte-identically.

Validation (macOS): extension-host tsc clean (outside vendored upstream);
npm --prefix crates/tui/extension-host test: 405 tests, 398 passed, 0
failed, 7 skipped; cargo test -p codewhale-tui --lib --
plugins::install::dsh::tests: 26 passed, 0 failed (twice). Windows runtime
proof is hosted Windows CI. Not addressed here: Bun's own entry-module
resolution under the LPAC (3 tests).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-04 22:43:32 -07:00
1 parent 8ff176e3ea
commit 4e1905b667
6 files changed
+36 -21

No files matched your search

+7 -5
View File
@@ -6257,7 +6257,7 @@ function reviewedHookModule(dialect, root, files) {
if (!config || typeof config.configPath !== "string") throw new Error("hook bridge needs its reviewed configPath");
const path = resolve(root, config.configPath);
const inside = relative(root, path).split(sep).join("/");
if (!inside || inside.startsWith("../") || isAbsolute(inside) || !files[inside] || realpathSync(path) !== path || !lstatSync(path).isFile()) throw new Error("hook config is absent from the reviewed regular-file closure");
if (!inside || inside.startsWith("../") || isAbsolute(inside) || !files[inside] || canonicalPath(path) !== path || !lstatSync(path).isFile()) throw new Error("hook config is absent from the reviewed regular-file closure");
const bytes = readFileSync(path);
if (bytes.length > 1024 * 1024 || createHash("sha256").update(bytes).digest("hex") !== files[inside]) throw new Error("hook config changed after review or exceeds 1 MiB");
if (config.projectDir !== void 0) throw new Error("explicit projectDir is unsupported; each process uses its current core workspace");
@@ -6278,12 +6278,13 @@ function reviewedHookModule(dialect, root, files) {
if (count === 0) throw new Error("reviewed hook config has no supported command hooks");
} };
}
var EVENTS;
var canonicalPath, EVENTS;
var init_shell_hooks = __esm({
"src/dsh/shell-hooks.ts"() {
"use strict";
init_config();
init_config2();
canonicalPath = (path) => process.platform === "win32" ? realpathSync.native(path) : realpathSync(path);
EVENTS = { SessionStart: "session_start", UserPromptSubmit: "message_submit", PreToolUse: "tool_call_before", PostToolUse: "tool_call_after", Stop: "turn_end", SubagentStart: "subagent_spawn", SubagentStop: "subagent_complete" };
}
});
@@ -12008,7 +12009,7 @@ import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from
import { relative as relative2, resolve as resolve2, sep as sep2, isAbsolute as isAbsolute2, dirname } from "node:path";
import { readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
function admitReviewedClosure(baseUrl, files) {
const root = realpathSync2(resolve2(fileURLToPath2(baseUrl)));
const root = canonicalPath2(resolve2(fileURLToPath2(baseUrl)));
const accepted = /* @__PURE__ */ Object.create(null);
const keys = Object.keys(files);
if (keys.length > 4096) throw new Error("reviewed composition closure exceeds its file limit");
@@ -12030,7 +12031,7 @@ function admitReviewedClosure(baseUrl, files) {
};
}
async function importReviewedModule(baseUrl, path) {
const root = realpathSync2(resolve2(fileURLToPath2(baseUrl)));
const root = canonicalPath2(resolve2(fileURLToPath2(baseUrl)));
const receipt = reviewedClosures.get(root);
if (!receipt) throw new Error("composition module closure is no longer admitted");
const entry = resolve2(root, path);
@@ -12047,12 +12048,13 @@ function closureAt(url) {
return { root, receipt, path: inside.split(sep2).join("/") };
}
}
var reviewedClosures;
var canonicalPath2, reviewedClosures;
var init_resolve_hooks = __esm({
"src/dsh/resolve-hooks.ts"() {
"use strict";
init_runtime();
init_bun_closure();
canonicalPath2 = process.platform === "win32" ? (path) => realpathSync2.native(path) : (path) => realpathSync2(path);
reviewedClosures = /* @__PURE__ */ new Map();
}
});
@@ -6636,7 +6636,7 @@ function reviewedHookModule(dialect, root, files) {
if (!config || typeof config.configPath !== "string") throw new Error("hook bridge needs its reviewed configPath");
const path = resolve2(root, config.configPath);
const inside = relative(root, path).split(sep).join("/");
if (!inside || inside.startsWith("../") || isAbsolute(inside) || !files[inside] || realpathSync(path) !== path || !lstatSync(path).isFile()) throw new Error("hook config is absent from the reviewed regular-file closure");
if (!inside || inside.startsWith("../") || isAbsolute(inside) || !files[inside] || canonicalPath(path) !== path || !lstatSync(path).isFile()) throw new Error("hook config is absent from the reviewed regular-file closure");
const bytes = readFileSync(path);
if (bytes.length > 1024 * 1024 || createHash("sha256").update(bytes).digest("hex") !== files[inside]) throw new Error("hook config changed after review or exceeds 1 MiB");
if (config.projectDir !== void 0) throw new Error("explicit projectDir is unsupported; each process uses its current core workspace");
@@ -6657,13 +6657,14 @@ function reviewedHookModule(dialect, root, files) {
if (count === 0) throw new Error("reviewed hook config has no supported command hooks");
} };
}
var EVENTS;
var canonicalPath, EVENTS;
var init_shell_hooks = __esm({
"src/dsh/shell-hooks.ts"() {
"use strict";
init_define_BUILTIN_MODULE_DIGESTS();
init_config();
init_config2();
canonicalPath = (path) => process.platform === "win32" ? realpathSync.native(path) : realpathSync(path);
EVENTS = { SessionStart: "session_start", UserPromptSubmit: "message_submit", PreToolUse: "tool_call_before", PostToolUse: "tool_call_after", Stop: "turn_end", SubagentStart: "subagent_spawn", SubagentStop: "subagent_complete" };
}
});
@@ -12703,7 +12704,7 @@ function installBunResolver(modules) {
const loader2 = extension === "tsx" ? "tsx" : extension === "ts" || extension === "mts" || extension === "cts" ? "ts" : extension === "jsx" || extension === "js" ? "jsx" : "js";
return { contents: readFileSync2(args.path, "utf8"), loader: loader2 };
}
if (!(closure.path in closure.receipt.files) || realpathSync2(args.path) !== args.path) throw new Error("module was absent from reviewed composition closure or contains a symbolic link");
if (!(closure.path in closure.receipt.files) || canonicalPath2(args.path) !== args.path) throw new Error("module was absent from reviewed composition closure or contains a symbolic link");
const bytes = readFileSync2(args.path);
if (bytes.length > 64 * 1024 * 1024 || createHash2("sha256").update(bytes).digest("hex") !== closure.receipt.files[closure.path]) throw new Error("composition module bytes changed after review");
if (closure.path.endsWith(".json")) return { contents: `export default JSON.parse(${JSON.stringify(bytes.toString("utf8"))});`, loader: "js" };
@@ -12735,7 +12736,7 @@ function checkedBunSpecifier(specifier, root, files, caller, require3) {
return require3 ? path : file.href;
}
function admitReviewedClosure(baseUrl, files) {
const root = realpathSync2(resolve3(fileURLToPath2(baseUrl)));
const root = canonicalPath2(resolve3(fileURLToPath2(baseUrl)));
const accepted = /* @__PURE__ */ Object.create(null);
const keys = Object.keys(files);
if (keys.length > 4096) throw new Error("reviewed composition closure exceeds its file limit");
@@ -12757,7 +12758,7 @@ function admitReviewedClosure(baseUrl, files) {
};
}
async function importReviewedModule(baseUrl, path) {
const root = realpathSync2(resolve3(fileURLToPath2(baseUrl)));
const root = canonicalPath2(resolve3(fileURLToPath2(baseUrl)));
const receipt = reviewedClosures.get(root);
if (!receipt) throw new Error("composition module closure is no longer admitted");
const entry = resolve3(root, path);
@@ -12814,13 +12815,14 @@ function installResolveHooks(modules) {
}
});
}
var SCHEME, REGISTRY_KEY, SINGLETONS, SUBPATH_SINGLETONS, UnsupportedPeerError, PEER_PATH, reviewedClosures, installed;
var canonicalPath2, SCHEME, REGISTRY_KEY, SINGLETONS, SUBPATH_SINGLETONS, UnsupportedPeerError, PEER_PATH, reviewedClosures, installed;
var init_resolve_hooks = __esm({
"src/dsh/resolve-hooks.ts"() {
"use strict";
init_define_BUILTIN_MODULE_DIGESTS();
init_runtime();
init_bun_closure();
canonicalPath2 = process.platform === "win32" ? (path) => realpathSync2.native(path) : (path) => realpathSync2(path);
SCHEME = "codewhale-host:";
REGISTRY_KEY = /* @__PURE__ */ Symbol.for("codewhale.extension-host.modules");
SINGLETONS = {
+7 -5
View File
@@ -6208,7 +6208,7 @@ function reviewedHookModule(dialect, root, files) {
if (!config || typeof config.configPath !== "string") throw new Error("hook bridge needs its reviewed configPath");
const path = resolve(root, config.configPath);
const inside = relative(root, path).split(sep).join("/");
if (!inside || inside.startsWith("../") || isAbsolute(inside) || !files[inside] || realpathSync(path) !== path || !lstatSync(path).isFile()) throw new Error("hook config is absent from the reviewed regular-file closure");
if (!inside || inside.startsWith("../") || isAbsolute(inside) || !files[inside] || canonicalPath(path) !== path || !lstatSync(path).isFile()) throw new Error("hook config is absent from the reviewed regular-file closure");
const bytes = readFileSync(path);
if (bytes.length > 1024 * 1024 || createHash("sha256").update(bytes).digest("hex") !== files[inside]) throw new Error("hook config changed after review or exceeds 1 MiB");
if (config.projectDir !== void 0) throw new Error("explicit projectDir is unsupported; each process uses its current core workspace");
@@ -6229,12 +6229,13 @@ function reviewedHookModule(dialect, root, files) {
if (count === 0) throw new Error("reviewed hook config has no supported command hooks");
} };
}
var EVENTS;
var canonicalPath, EVENTS;
var init_shell_hooks = __esm({
"src/dsh/shell-hooks.ts"() {
"use strict";
init_config();
init_config2();
canonicalPath = (path) => process.platform === "win32" ? realpathSync.native(path) : realpathSync(path);
EVENTS = { SessionStart: "session_start", UserPromptSubmit: "message_submit", PreToolUse: "tool_call_before", PostToolUse: "tool_call_after", Stop: "turn_end", SubagentStart: "subagent_spawn", SubagentStop: "subagent_complete" };
}
});
@@ -11959,7 +11960,7 @@ import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from
import { relative as relative2, resolve as resolve2, sep as sep2, isAbsolute as isAbsolute2, dirname } from "node:path";
import { readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
function admitReviewedClosure(baseUrl, files) {
const root = realpathSync2(resolve2(fileURLToPath2(baseUrl)));
const root = canonicalPath2(resolve2(fileURLToPath2(baseUrl)));
const accepted = /* @__PURE__ */ Object.create(null);
const keys = Object.keys(files);
if (keys.length > 4096) throw new Error("reviewed composition closure exceeds its file limit");
@@ -11981,7 +11982,7 @@ function admitReviewedClosure(baseUrl, files) {
};
}
async function importReviewedModule(baseUrl, path) {
const root = realpathSync2(resolve2(fileURLToPath2(baseUrl)));
const root = canonicalPath2(resolve2(fileURLToPath2(baseUrl)));
const receipt = reviewedClosures.get(root);
if (!receipt) throw new Error("composition module closure is no longer admitted");
const entry = resolve2(root, path);
@@ -11998,12 +11999,13 @@ function closureAt(url) {
return { root, receipt, path: inside.split(sep2).join("/") };
}
}
var reviewedClosures;
var canonicalPath2, reviewedClosures;
var init_resolve_hooks = __esm({
"src/dsh/resolve-hooks.ts"() {
"use strict";
init_runtime();
init_bun_closure();
canonicalPath2 = process.platform === "win32" ? (path) => realpathSync2.native(path) : (path) => realpathSync2(path);
reviewedClosures = /* @__PURE__ */ new Map();
}
});
+2 -1
View File
@@ -130,13 +130,14 @@ function parseCodexConfig(raw) {
}
// src/dsh/shell-hooks.ts
var canonicalPath = (path) => process.platform === "win32" ? realpathSync.native(path) : realpathSync(path);
var EVENTS = { SessionStart: "session_start", UserPromptSubmit: "message_submit", PreToolUse: "tool_call_before", PostToolUse: "tool_call_after", Stop: "turn_end", SubagentStart: "subagent_spawn", SubagentStop: "subagent_complete" };
function reviewedHookModule(dialect, root, files) {
return { name: `hooks-${dialect}`, inject: ["shellHooks"], apply(ctx, config) {
if (!config || typeof config.configPath !== "string") throw new Error("hook bridge needs its reviewed configPath");
const path = resolve(root, config.configPath);
const inside = relative(root, path).split(sep).join("/");
if (!inside || inside.startsWith("../") || isAbsolute(inside) || !files[inside] || realpathSync(path) !== path || !lstatSync(path).isFile()) throw new Error("hook config is absent from the reviewed regular-file closure");
if (!inside || inside.startsWith("../") || isAbsolute(inside) || !files[inside] || canonicalPath(path) !== path || !lstatSync(path).isFile()) throw new Error("hook config is absent from the reviewed regular-file closure");
const bytes = readFileSync(path);
if (bytes.length > 1024 * 1024 || createHash("sha256").update(bytes).digest("hex") !== files[inside]) throw new Error("hook config changed after review or exceeds 1 MiB");
if (config.projectDir !== void 0) throw new Error("explicit projectDir is unsupported; each process uses its current core workspace");
@@ -21,6 +21,11 @@ import {createHash} from 'node:crypto'
import {fileURLToPath,pathToFileURL} from 'node:url'
import {relative,resolve,sep,isAbsolute,dirname} from 'node:path'
import {readFileSync,realpathSync} from 'node:fs'
// JS realpath lstats every ancestor from the drive root, and a Windows LPAC
// host cannot read `C:\` (EPERM). The native call asks Windows for the opened
// file's final path, which needs access to that file only. Other platforms
// keep the JS implementation unchanged.
const canonicalPath:(path:string)=>string=process.platform==='win32'?path=>realpathSync.native(path):path=>realpathSync(path)
import { RUNTIME } from '../runtime.ts'
import { prepareBunSource, REVIEWED_IMPORT } from './bun-closure.ts'
@@ -152,7 +157,7 @@ function installBunResolver(modules: Record<string, Record<string, unknown>>) {
:extension==='jsx' || extension==='js'?'jsx':'js'
return {contents:readFileSync(args.path,'utf8'),loader}
}
if(!(closure.path in closure.receipt.files) || realpathSync(args.path)!==args.path)throw new Error('module was absent from reviewed composition closure or contains a symbolic link')
if(!(closure.path in closure.receipt.files) || canonicalPath(args.path)!==args.path)throw new Error('module was absent from reviewed composition closure or contains a symbolic link')
const bytes=readFileSync(args.path)
if(bytes.length>64*1024*1024 || createHash('sha256').update(bytes).digest('hex')!==closure.receipt.files[closure.path])throw new Error('composition module bytes changed after review')
// Bun 1.4 runtime onLoad treats its JSON loader as JS. Preserve exact
@@ -189,7 +194,7 @@ function checkedBunSpecifier(specifier:string,root:string,files:Readonly<Record<
// session or plugin state store. Native JS remains arbitrary co-resident code.
const reviewedClosures=new Map<string,{files:Readonly<Record<string,string>>,refs:number}>()
export function admitReviewedClosure(baseUrl:string,files:Readonly<Record<string,string>>):()=>void {
const root=realpathSync(resolve(fileURLToPath(baseUrl)))
const root=canonicalPath(resolve(fileURLToPath(baseUrl)))
const accepted:Record<string,string>=Object.create(null)
const keys=Object.keys(files)
if (keys.length>4096) throw new Error('reviewed composition closure exceeds its file limit')
@@ -207,7 +212,7 @@ export function admitReviewedClosure(baseUrl:string,files:Readonly<Record<string
}
/** Both runtimes consume the same exact admitted source-file receipt. */
export async function importReviewedModule(baseUrl:string,path:string):Promise<unknown> {
const root=realpathSync(resolve(fileURLToPath(baseUrl)))
const root=canonicalPath(resolve(fileURLToPath(baseUrl)))
const receipt=reviewedClosures.get(root)
if(!receipt)throw new Error('composition module closure is no longer admitted')
const entry=resolve(root,path)
@@ -1,6 +1,9 @@
/** Pinned configuration/matcher semantics adapted onto the existing core hook catalog. No agent/session runtime. */
import { createHash } from 'node:crypto'
import { readFileSync,realpathSync,lstatSync } from 'node:fs'
// Same rule as resolve-hooks: a Windows LPAC cannot lstat the drive root that
// JS realpath walks, so ask Windows for the opened file's final path there.
const canonicalPath=(path:string):string=>process.platform==='win32'?realpathSync.native(path):realpathSync(path)
import { resolve,relative,isAbsolute,sep } from 'node:path'
import { parseClaudeCodeConfig } from './upstream/hooks/hooks-claude-code/src/config.ts'
import { parseCodexConfig } from './upstream/hooks/hooks-codex/src/config.ts'
@@ -9,7 +12,7 @@ export function reviewedHookModule(dialect:'claude-code'|'codex',root:string,fil
return {name:`hooks-${dialect}`,inject:['shellHooks'],apply(ctx:any,config:any) {
if(!config || typeof config.configPath!=='string')throw new Error('hook bridge needs its reviewed configPath')
const path=resolve(root,config.configPath);const inside=relative(root,path).split(sep).join('/')
if(!inside || inside.startsWith('../') || isAbsolute(inside) || !files[inside] || realpathSync(path)!==path || !lstatSync(path).isFile())throw new Error('hook config is absent from the reviewed regular-file closure')
if(!inside || inside.startsWith('../') || isAbsolute(inside) || !files[inside] || canonicalPath(path)!==path || !lstatSync(path).isFile())throw new Error('hook config is absent from the reviewed regular-file closure')
const bytes=readFileSync(path)
if(bytes.length>1024*1024 || createHash('sha256').update(bytes).digest('hex')!==files[inside])throw new Error('hook config changed after review or exceeds 1 MiB')
// The only substitutions are the sealed bundle and current per-call core workspace.