mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 17:10:50 -10:00
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:
1 parent
8ff176e3ea
commit
4e1905b667
6 files changed
+36
-21
No files matched your search
+7
-5
@@ -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 = {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user