fix(windows): compare reviewed plugin paths canonical-to-canonical under LPAC

Hosted Windows (LPAC) refuses lstat on the drive root, so Node's JS
realpathSync — which walks every ancestor — failed the reviewed-closure
checks with EPERM lstat 'C:\' (13 failures on #6815, mostly
extension_host::native_mcp and raw_agent_presets). The earlier attempt
(4e1905b66, reverted) used realpathSync.native but compared its spelling
against raw input.

New src/dsh/canonical-path.ts is the one comparison rule: canonicalize
with realpathSync.native on win32 (GetFinalPathNameByHandle, no ancestor
walk) and realpathSync elsewhere, strip \\?\ and \\?\UNC\, compare
case-insensitively on win32, and express containment as
relative(canonical root, canonical target). A canonical path is never
compared to a raw one. resolve-hooks keys closures by canonical root and
remembers the raw root; the Node load hook now also refuses symlinked
closure modules (under --preserve-symlinks an in-closure symlink pointing
outside kept its in-closure URL and loaded — reproduced on HEAD).

Evidence (macOS): npm --prefix crates/tui/extension-host test: 406 tests,
399 pass, 0 fail, 7 skipped (new canonical-path test passes);
cargo test -p codewhale-tui --lib -- extension_host dsh: 264-265 pass,
2-3 plugins::install::dsh failures that differ per run and pass in
isolation (parallel-load flake under investigation). Windows LPAC itself
cannot run here; hosted Windows CI is the proof.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 authored and Hunter Bown committed 2026-10-05 07:56:55 -07:00
1 parent b059d4f0ef
commit 6958ca6dd7
8 files changed
+470 -102

No files matched your search

+85 -26
View File
@@ -6102,6 +6102,52 @@ var init_skill_filesystem = __esm({
}
});
// src/dsh/canonical-path.ts
import { realpathSync } from "node:fs";
import { posix, win32 } from "node:path";
function canonicalPath(path, platform = process.platform) {
return stripVerbatim(platform === "win32" ? realpathSync.native(path) : realpathSync(path), platform);
}
function stripVerbatim(path, platform = process.platform) {
if (platform !== "win32") return path;
if (/^[\\/]{2}\?[\\/]UNC[\\/]/i.test(path)) return `\\\\${path.slice(8)}`;
if (/^[\\/]{2}\?[\\/]/.test(path)) return path.slice(4);
return path;
}
function pathKey(path, platform = process.platform) {
if (platform !== "win32") return posix.normalize(path);
return win32.normalize(stripVerbatim(path, platform)).toLowerCase();
}
function samePath(a, b, platform = process.platform) {
return pathKey(a, platform) === pathKey(b, platform);
}
function insideKey(root, target, platform = process.platform) {
const path = platform === "win32" ? win32 : posix;
const inside = path.relative(stripVerbatim(root, platform), stripVerbatim(target, platform));
if (inside === ".." || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside)) return void 0;
return platform === "win32" ? inside.split(win32.sep).join("/") : inside;
}
function isUnlinkedInside(root, key, target, platform = process.platform) {
let canonicalRoot, canonicalTarget;
try {
canonicalRoot = canonicalPath(root, platform);
canonicalTarget = canonicalPath(target, platform);
} catch {
return false;
}
return unlinkedKeyMatches(canonicalRoot, key, canonicalTarget, platform);
}
function unlinkedKeyMatches(canonicalRoot, key, canonicalTarget, platform = process.platform) {
if (!key || key.split("/").some((part) => !part || part === "." || part === "..")) return false;
const path = platform === "win32" ? win32 : posix;
return samePath(path.join(stripVerbatim(canonicalRoot, platform), ...key.split("/")), canonicalTarget, platform);
}
var init_canonical_path = __esm({
"src/dsh/canonical-path.ts"() {
"use strict";
}
});
// src/dsh/upstream/hooks/hook-protocol/src/matcher.ts
function isMatchAll(matcher) {
return matcher === void 0 || matcher === "" || matcher === "*";
@@ -6250,14 +6296,14 @@ var init_config2 = __esm({
// src/dsh/shell-hooks.ts
import { createHash } from "node:crypto";
import { readFileSync, realpathSync, lstatSync } from "node:fs";
import { readFileSync, lstatSync } from "node:fs";
import { resolve, relative, isAbsolute, sep } from "node:path";
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] || !isUnlinkedInside(root, inside, 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");
@@ -6282,6 +6328,8 @@ var EVENTS;
var init_shell_hooks = __esm({
"src/dsh/shell-hooks.ts"() {
"use strict";
init_canonical_path();
init_canonical_path();
init_config();
init_config2();
EVENTS = { SessionStart: "session_start", UserPromptSubmit: "message_submit", PreToolUse: "tool_call_before", PostToolUse: "tool_call_after", Stop: "turn_end", SubagentStart: "subagent_spawn", SubagentStop: "subagent_complete" };
@@ -12005,10 +12053,19 @@ var init_bun_closure = __esm({
// src/dsh/resolve-hooks.ts
import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "node:url";
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";
import { resolve as resolve2, isAbsolute as isAbsolute2, dirname } from "node:path";
function keyIn(closure, path) {
const canonical = insideKey(closure.canonicalRoot, path);
if (canonical !== void 0) return canonical;
for (const root of closure.rawRoots) {
const raw = insideKey(root, path);
if (raw !== void 0) return raw;
}
}
function admitReviewedClosure(baseUrl, files) {
const root = realpathSync2(resolve2(fileURLToPath2(baseUrl)));
const rawRoot = resolve2(fileURLToPath2(baseUrl));
const canonicalRoot = canonicalPath(rawRoot);
const id = pathKey(canonicalRoot);
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");
@@ -12016,41 +12073,43 @@ function admitReviewedClosure(baseUrl, files) {
if (!key || key.split("/").some((part) => !part || part === "." || part === "..") || key.includes("\\") || key.includes(":") || isAbsolute2(key) || !/^[a-f0-9]{64}$/.test(files[key])) throw new Error("invalid reviewed composition closure file");
accepted[key] = files[key];
}
const existing = reviewedClosures.get(root);
const existing = reviewedClosures.get(id);
if (existing) {
if (Object.keys(existing.files).length !== keys.length || keys.some((key) => existing.files[key] !== accepted[key])) throw new Error("the same composition root carries different file receipts");
existing.refs++;
} else reviewedClosures.set(root, { files: Object.freeze(accepted), refs: 1 });
existing.rawRoots.add(rawRoot);
} else reviewedClosures.set(id, { files: Object.freeze(accepted), refs: 1, canonicalRoot, rawRoots: /* @__PURE__ */ new Set([rawRoot]) });
let disposed = false;
return () => {
if (disposed) return;
disposed = true;
const current2 = reviewedClosures.get(root);
if (current2 && --current2.refs === 0) reviewedClosures.delete(root);
const current2 = reviewedClosures.get(id);
if (current2 && --current2.refs === 0) reviewedClosures.delete(id);
};
}
async function importReviewedModule(baseUrl, path) {
const root = realpathSync2(resolve2(fileURLToPath2(baseUrl)));
const receipt = reviewedClosures.get(root);
const rawRoot = resolve2(fileURLToPath2(baseUrl));
const id = pathKey(canonicalPath(rawRoot));
const receipt = reviewedClosures.get(id);
if (!receipt) throw new Error("composition module closure is no longer admitted");
const entry = resolve2(root, path);
const entry = resolve2(rawRoot, path);
const admitted = closureAt(pathToFileURL2(entry).href);
if (!admitted || admitted.root !== root || !(admitted.path in receipt.files)) throw new Error("composition module is absent from reviewed closure");
if (!admitted || admitted.root !== id || !(admitted.path in receipt.files)) throw new Error("composition module is absent from reviewed closure");
return import(pathToFileURL2(entry).href);
}
function closureAt(url) {
if (!url?.startsWith("file:")) return;
const path = resolve2(fileURLToPath2(url));
for (const [root, receipt] of reviewedClosures) {
const inside = relative2(root, path);
if (inside === ".." || inside.startsWith(`..${sep2}`) || isAbsolute2(inside)) continue;
return { root, receipt, path: inside.split(sep2).join("/") };
const inside = keyIn(receipt, path);
if (inside !== void 0) return { root, receipt, path: inside };
}
}
var reviewedClosures;
var init_resolve_hooks = __esm({
"src/dsh/resolve-hooks.ts"() {
"use strict";
init_canonical_path();
init_runtime();
init_bun_closure();
reviewedClosures = /* @__PURE__ */ new Map();
@@ -12071,7 +12130,7 @@ __export(composition_exports, {
});
import { createHash as createHash2 } from "node:crypto";
import { readFile as readFile2 } from "node:fs/promises";
import { isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3, sep as sep3 } from "node:path";
import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
import { fileURLToPath as fileURLToPath3 } from "node:url";
function digest(bytes) {
return createHash2("sha256").update(bytes).digest("hex");
@@ -12346,8 +12405,8 @@ var init_composition = __esm({
async importReviewed(module) {
const root = fileURLToPath3(this.base);
const path = resolve3(root, module.path);
const inside = relative3(root, path);
if (inside.startsWith(`..${sep3}`) || inside === ".." || isAbsolute3(inside)) throw new Error("module escapes reviewed bundle");
const inside = relative2(root, path);
if (inside.startsWith(`..${sep2}`) || inside === ".." || isAbsolute3(inside)) throw new Error("module escapes reviewed bundle");
if (this.spec.files[module.path] !== module.sha256 || digest(await readFile2(path)) !== module.sha256) throw new Error("module changed after review");
return importReviewedModule(this.base, module.path);
}
@@ -12827,7 +12886,7 @@ var init_composition_inventory = __esm({
// src/dsh/agent-presets.ts
import { createHash as createHash3 } from "node:crypto";
import { resolve as resolve5, relative as relative4, dirname as dirname3, sep as sep4 } from "node:path";
import { resolve as resolve5, relative as relative3, dirname as dirname3, sep as sep3 } from "node:path";
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL5 } from "node:url";
function plain(path) {
return typeof path === "string" && path.length > 0 && path.length <= 4096 && !/[\\:\u0000-\u001f\u007f]/u.test(path) && !path.startsWith("/") && path.split("/").every((p) => p !== "" && p !== "." && p !== "..");
@@ -12879,9 +12938,9 @@ async function reviewAgentPresets(input) {
const files = composition.files;
if (!files || Object.keys(files).length > 4096 || !Array.isArray(input.directories) || input.directories.length > 4096) fail("invalid source inventory");
const base = resolve5("codewhale-reviewed-agent-presets");
const baseUrl = pathToFileURL5(`${base}${sep4}`).href;
const baseUrl = pathToFileURL5(`${base}${sep3}`).href;
function key(path) {
const inside = relative4(base, path).split(sep4).join("/");
const inside = relative3(base, path).split(sep3).join("/");
if (!plain(inside)) fail("path escapes the admitted source root");
return inside;
}
@@ -12900,9 +12959,9 @@ async function reviewAgentPresets(input) {
const parsed = /* @__PURE__ */ new Map();
const io = {
async readFile(path) {
const relative5 = key(path);
if (invalid.has(relative5)) throw new Error("composition exceeds its data/shape bound");
const text2 = documents.get(relative5);
const relative4 = key(path);
if (invalid.has(relative4)) throw new Error("composition exceeds its data/shape bound");
const text2 = documents.get(relative4);
if (text2 === void 0) throw new Error("document not admitted");
return text2;
},
@@ -12953,7 +13012,7 @@ async function reviewAgentPresets(input) {
}
});
if (candidates.length !== 1) fail("shipped root requires exactly one admitted agent-presets package");
const folder = dirname3(candidates[0][0]).split(sep4).join("/");
const folder = dirname3(candidates[0][0]).split(sep3).join("/");
const root = folder === "." ? "presets" : `${folder}/presets`;
if (!directories.has(root)) fail("shipped preset root was not admitted");
roots.push({ path: resolve5(base, root), trust: "system" });
+96 -25
View File
@@ -6478,6 +6478,53 @@ var init_skill_filesystem = __esm({
}
});
// src/dsh/canonical-path.ts
import { realpathSync } from "node:fs";
import { posix, win32 } from "node:path";
function canonicalPath(path, platform = process.platform) {
return stripVerbatim(platform === "win32" ? realpathSync.native(path) : realpathSync(path), platform);
}
function stripVerbatim(path, platform = process.platform) {
if (platform !== "win32") return path;
if (/^[\\/]{2}\?[\\/]UNC[\\/]/i.test(path)) return `\\\\${path.slice(8)}`;
if (/^[\\/]{2}\?[\\/]/.test(path)) return path.slice(4);
return path;
}
function pathKey(path, platform = process.platform) {
if (platform !== "win32") return posix.normalize(path);
return win32.normalize(stripVerbatim(path, platform)).toLowerCase();
}
function samePath(a, b, platform = process.platform) {
return pathKey(a, platform) === pathKey(b, platform);
}
function insideKey(root, target, platform = process.platform) {
const path = platform === "win32" ? win32 : posix;
const inside = path.relative(stripVerbatim(root, platform), stripVerbatim(target, platform));
if (inside === ".." || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside)) return void 0;
return platform === "win32" ? inside.split(win32.sep).join("/") : inside;
}
function isUnlinkedInside(root, key, target, platform = process.platform) {
let canonicalRoot, canonicalTarget;
try {
canonicalRoot = canonicalPath(root, platform);
canonicalTarget = canonicalPath(target, platform);
} catch {
return false;
}
return unlinkedKeyMatches(canonicalRoot, key, canonicalTarget, platform);
}
function unlinkedKeyMatches(canonicalRoot, key, canonicalTarget, platform = process.platform) {
if (!key || key.split("/").some((part) => !part || part === "." || part === "..")) return false;
const path = platform === "win32" ? win32 : posix;
return samePath(path.join(stripVerbatim(canonicalRoot, platform), ...key.split("/")), canonicalTarget, platform);
}
var init_canonical_path = __esm({
"src/dsh/canonical-path.ts"() {
"use strict";
init_define_BUILTIN_MODULE_DIGESTS();
}
});
// src/dsh/upstream/hooks/hook-protocol/src/matcher.ts
function isMatchAll(matcher) {
return matcher === void 0 || matcher === "" || matcher === "*";
@@ -6629,14 +6676,14 @@ var init_config2 = __esm({
// src/dsh/shell-hooks.ts
import { createHash } from "node:crypto";
import { readFileSync, realpathSync, lstatSync } from "node:fs";
import { readFileSync, lstatSync } from "node:fs";
import { resolve as resolve2, relative, isAbsolute, sep } from "node:path";
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 = 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] || !isUnlinkedInside(root, inside, 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");
@@ -6662,6 +6709,8 @@ var init_shell_hooks = __esm({
"src/dsh/shell-hooks.ts"() {
"use strict";
init_define_BUILTIN_MODULE_DIGESTS();
init_canonical_path();
init_canonical_path();
init_config();
init_config2();
EVENTS = { SessionStart: "session_start", UserPromptSubmit: "message_submit", PreToolUse: "tool_call_before", PostToolUse: "tool_call_after", Stop: "turn_end", SubagentStart: "subagent_spawn", SubagentStop: "subagent_complete" };
@@ -12631,8 +12680,8 @@ var init_bun_closure = __esm({
import * as nodeModule2 from "node:module";
import { createHash as createHash2 } from "node:crypto";
import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL3 } from "node:url";
import { relative as relative2, resolve as resolve3, sep as sep2, isAbsolute as isAbsolute2, dirname } from "node:path";
import { readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
import { resolve as resolve3, isAbsolute as isAbsolute2, dirname } from "node:path";
import { readFileSync as readFileSync2 } from "node:fs";
function packageName(specifier) {
const parts = specifier.split("/");
return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
@@ -12677,7 +12726,7 @@ function installBunResolver(modules) {
const name = String(specifier);
const closure = closureAt(caller);
if (!closure) throw new Error("composition module closure is no longer admitted");
const target = checkedBunSpecifier(name, closure.root, closure.receipt.files, caller, false);
const target = checkedBunSpecifier(name, closure.receipt, caller, false);
const singleton = classifySpecifier(target);
if (singleton !== null) return modules[singleton];
return import(target, options);
@@ -12703,13 +12752,13 @@ 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) || !unlinkedInside(closure.receipt, closure.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" };
let source;
try {
source = prepareBunSource(bytes.toString("utf8"), args.path, (specifier, require3) => checkedBunSpecifier(specifier, closure.root, closure.receipt.files, pathToFileURL3(args.path).href, require3));
source = prepareBunSource(bytes.toString("utf8"), args.path, (specifier, require3) => checkedBunSpecifier(specifier, closure.receipt, pathToFileURL3(args.path).href, require3));
} catch (error) {
if (error instanceof SyntaxError) throw new Error("reviewed composition module has unsupported JavaScript syntax");
throw error;
@@ -12719,7 +12768,7 @@ function installBunResolver(modules) {
}
});
}
function checkedBunSpecifier(specifier, root, files, caller, require3) {
function checkedBunSpecifier(specifier, closure, caller, require3) {
const key = classifySpecifier(specifier);
if (key !== null) {
if (!(key in globalThis[REGISTRY_KEY])) throw new UnsupportedPeerError(specifier);
@@ -12730,12 +12779,31 @@ function checkedBunSpecifier(specifier, root, files, caller, require3) {
const path = file ? require3 && !specifier.startsWith("file:") ? resolve3(dirname(fileURLToPath2(caller)), specifier) : resolve3(fileURLToPath2(file)) : void 0;
if (!path) throw new Error("bare dependency is absent from this reviewed composition; package its reviewed relative source");
if (file?.search || file?.hash) throw new Error('Bun does not preserve reviewed module query or fragment identity; select [extension_host] runtime = "node" for this composition');
const inside = relative2(root, path).split(sep2).join("/");
if (inside === ".." || inside.startsWith("../") || isAbsolute2(inside) || !(inside in files)) throw new Error("composition import escapes the reviewed file closure");
const inside = keyIn(closure, path);
if (inside === void 0 || !(inside in closure.files)) throw new Error("composition import escapes the reviewed file closure");
return require3 ? path : file.href;
}
function keyIn(closure, path) {
const canonical = insideKey(closure.canonicalRoot, path);
if (canonical !== void 0) return canonical;
for (const root of closure.rawRoots) {
const raw = insideKey(root, path);
if (raw !== void 0) return raw;
}
}
function unlinkedInside(closure, key, path) {
let canonical;
try {
canonical = canonicalPath(path);
} catch {
return false;
}
return unlinkedKeyMatches(closure.canonicalRoot, key, canonical);
}
function admitReviewedClosure(baseUrl, files) {
const root = realpathSync2(resolve3(fileURLToPath2(baseUrl)));
const rawRoot = resolve3(fileURLToPath2(baseUrl));
const canonicalRoot = canonicalPath(rawRoot);
const id = pathKey(canonicalRoot);
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");
@@ -12743,35 +12811,36 @@ function admitReviewedClosure(baseUrl, files) {
if (!key || key.split("/").some((part) => !part || part === "." || part === "..") || key.includes("\\") || key.includes(":") || isAbsolute2(key) || !/^[a-f0-9]{64}$/.test(files[key])) throw new Error("invalid reviewed composition closure file");
accepted[key] = files[key];
}
const existing = reviewedClosures.get(root);
const existing = reviewedClosures.get(id);
if (existing) {
if (Object.keys(existing.files).length !== keys.length || keys.some((key) => existing.files[key] !== accepted[key])) throw new Error("the same composition root carries different file receipts");
existing.refs++;
} else reviewedClosures.set(root, { files: Object.freeze(accepted), refs: 1 });
existing.rawRoots.add(rawRoot);
} else reviewedClosures.set(id, { files: Object.freeze(accepted), refs: 1, canonicalRoot, rawRoots: /* @__PURE__ */ new Set([rawRoot]) });
let disposed = false;
return () => {
if (disposed) return;
disposed = true;
const current2 = reviewedClosures.get(root);
if (current2 && --current2.refs === 0) reviewedClosures.delete(root);
const current2 = reviewedClosures.get(id);
if (current2 && --current2.refs === 0) reviewedClosures.delete(id);
};
}
async function importReviewedModule(baseUrl, path) {
const root = realpathSync2(resolve3(fileURLToPath2(baseUrl)));
const receipt = reviewedClosures.get(root);
const rawRoot = resolve3(fileURLToPath2(baseUrl));
const id = pathKey(canonicalPath(rawRoot));
const receipt = reviewedClosures.get(id);
if (!receipt) throw new Error("composition module closure is no longer admitted");
const entry = resolve3(root, path);
const entry = resolve3(rawRoot, path);
const admitted = closureAt(pathToFileURL3(entry).href);
if (!admitted || admitted.root !== root || !(admitted.path in receipt.files)) throw new Error("composition module is absent from reviewed closure");
if (!admitted || admitted.root !== id || !(admitted.path in receipt.files)) throw new Error("composition module is absent from reviewed closure");
return import(pathToFileURL3(entry).href);
}
function closureAt(url) {
if (!url?.startsWith("file:")) return;
const path = resolve3(fileURLToPath2(url));
for (const [root, receipt] of reviewedClosures) {
const inside = relative2(root, path);
if (inside === ".." || inside.startsWith(`..${sep2}`) || isAbsolute2(inside)) continue;
return { root, receipt, path: inside.split(sep2).join("/") };
const inside = keyIn(receipt, path);
if (inside !== void 0) return { root, receipt, path: inside };
}
}
function installResolveHooks(modules) {
@@ -12807,6 +12876,7 @@ function installResolveHooks(modules) {
const closure = closureAt(url);
if (closure) {
if (!(closure.path in closure.receipt.files) || result.source === void 0 || result.source === null) throw new Error("module was absent from reviewed composition closure");
if (!unlinkedInside(closure.receipt, closure.path, fileURLToPath2(url))) throw new Error("module was absent from reviewed composition closure or contains a symbolic link");
const source = typeof result.source === "string" ? Buffer.from(result.source) : Buffer.from(result.source);
if (source.length > 64 * 1024 * 1024 || createHash2("sha256").update(source).digest("hex") !== closure.receipt.files[closure.path]) throw new Error("composition module bytes changed after review");
}
@@ -12819,6 +12889,7 @@ var init_resolve_hooks = __esm({
"src/dsh/resolve-hooks.ts"() {
"use strict";
init_define_BUILTIN_MODULE_DIGESTS();
init_canonical_path();
init_runtime();
init_bun_closure();
SCHEME = "codewhale-host:";
@@ -14789,7 +14860,7 @@ __export(composition_exports, {
});
import { createHash as createHash5 } from "node:crypto";
import { readFile as readFile3 } from "node:fs/promises";
import { isAbsolute as isAbsolute4, relative as relative3, resolve as resolve4, sep as sep3 } from "node:path";
import { isAbsolute as isAbsolute4, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
import { fileURLToPath as fileURLToPath5 } from "node:url";
function digest(bytes) {
return createHash5("sha256").update(bytes).digest("hex");
@@ -15065,8 +15136,8 @@ var init_composition = __esm({
async importReviewed(module) {
const root = fileURLToPath5(this.base);
const path = resolve4(root, module.path);
const inside = relative3(root, path);
if (inside.startsWith(`..${sep3}`) || inside === ".." || isAbsolute4(inside)) throw new Error("module escapes reviewed bundle");
const inside = relative2(root, path);
if (inside.startsWith(`..${sep2}`) || inside === ".." || isAbsolute4(inside)) throw new Error("module escapes reviewed bundle");
if (this.spec.files[module.path] !== module.sha256 || digest(await readFile3(path)) !== module.sha256) throw new Error("module changed after review");
return importReviewedModule(this.base, module.path);
}
+85 -26
View File
@@ -6053,6 +6053,52 @@ var init_skill_filesystem = __esm({
}
});
// src/dsh/canonical-path.ts
import { realpathSync } from "node:fs";
import { posix, win32 } from "node:path";
function canonicalPath(path, platform = process.platform) {
return stripVerbatim(platform === "win32" ? realpathSync.native(path) : realpathSync(path), platform);
}
function stripVerbatim(path, platform = process.platform) {
if (platform !== "win32") return path;
if (/^[\\/]{2}\?[\\/]UNC[\\/]/i.test(path)) return `\\\\${path.slice(8)}`;
if (/^[\\/]{2}\?[\\/]/.test(path)) return path.slice(4);
return path;
}
function pathKey(path, platform = process.platform) {
if (platform !== "win32") return posix.normalize(path);
return win32.normalize(stripVerbatim(path, platform)).toLowerCase();
}
function samePath(a, b, platform = process.platform) {
return pathKey(a, platform) === pathKey(b, platform);
}
function insideKey(root, target, platform = process.platform) {
const path = platform === "win32" ? win32 : posix;
const inside = path.relative(stripVerbatim(root, platform), stripVerbatim(target, platform));
if (inside === ".." || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside)) return void 0;
return platform === "win32" ? inside.split(win32.sep).join("/") : inside;
}
function isUnlinkedInside(root, key, target, platform = process.platform) {
let canonicalRoot, canonicalTarget;
try {
canonicalRoot = canonicalPath(root, platform);
canonicalTarget = canonicalPath(target, platform);
} catch {
return false;
}
return unlinkedKeyMatches(canonicalRoot, key, canonicalTarget, platform);
}
function unlinkedKeyMatches(canonicalRoot, key, canonicalTarget, platform = process.platform) {
if (!key || key.split("/").some((part) => !part || part === "." || part === "..")) return false;
const path = platform === "win32" ? win32 : posix;
return samePath(path.join(stripVerbatim(canonicalRoot, platform), ...key.split("/")), canonicalTarget, platform);
}
var init_canonical_path = __esm({
"src/dsh/canonical-path.ts"() {
"use strict";
}
});
// src/dsh/upstream/hooks/hook-protocol/src/matcher.ts
function isMatchAll(matcher) {
return matcher === void 0 || matcher === "" || matcher === "*";
@@ -6201,14 +6247,14 @@ var init_config2 = __esm({
// src/dsh/shell-hooks.ts
import { createHash } from "node:crypto";
import { readFileSync, realpathSync, lstatSync } from "node:fs";
import { readFileSync, lstatSync } from "node:fs";
import { resolve, relative, isAbsolute, sep } from "node:path";
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] || !isUnlinkedInside(root, inside, 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");
@@ -6233,6 +6279,8 @@ var EVENTS;
var init_shell_hooks = __esm({
"src/dsh/shell-hooks.ts"() {
"use strict";
init_canonical_path();
init_canonical_path();
init_config();
init_config2();
EVENTS = { SessionStart: "session_start", UserPromptSubmit: "message_submit", PreToolUse: "tool_call_before", PostToolUse: "tool_call_after", Stop: "turn_end", SubagentStart: "subagent_spawn", SubagentStop: "subagent_complete" };
@@ -11956,10 +12004,19 @@ var init_bun_closure = __esm({
// src/dsh/resolve-hooks.ts
import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "node:url";
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";
import { resolve as resolve2, isAbsolute as isAbsolute2, dirname } from "node:path";
function keyIn(closure, path) {
const canonical = insideKey(closure.canonicalRoot, path);
if (canonical !== void 0) return canonical;
for (const root of closure.rawRoots) {
const raw = insideKey(root, path);
if (raw !== void 0) return raw;
}
}
function admitReviewedClosure(baseUrl, files) {
const root = realpathSync2(resolve2(fileURLToPath2(baseUrl)));
const rawRoot = resolve2(fileURLToPath2(baseUrl));
const canonicalRoot = canonicalPath(rawRoot);
const id = pathKey(canonicalRoot);
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");
@@ -11967,41 +12024,43 @@ function admitReviewedClosure(baseUrl, files) {
if (!key || key.split("/").some((part) => !part || part === "." || part === "..") || key.includes("\\") || key.includes(":") || isAbsolute2(key) || !/^[a-f0-9]{64}$/.test(files[key])) throw new Error("invalid reviewed composition closure file");
accepted[key] = files[key];
}
const existing = reviewedClosures.get(root);
const existing = reviewedClosures.get(id);
if (existing) {
if (Object.keys(existing.files).length !== keys.length || keys.some((key) => existing.files[key] !== accepted[key])) throw new Error("the same composition root carries different file receipts");
existing.refs++;
} else reviewedClosures.set(root, { files: Object.freeze(accepted), refs: 1 });
existing.rawRoots.add(rawRoot);
} else reviewedClosures.set(id, { files: Object.freeze(accepted), refs: 1, canonicalRoot, rawRoots: /* @__PURE__ */ new Set([rawRoot]) });
let disposed = false;
return () => {
if (disposed) return;
disposed = true;
const current2 = reviewedClosures.get(root);
if (current2 && --current2.refs === 0) reviewedClosures.delete(root);
const current2 = reviewedClosures.get(id);
if (current2 && --current2.refs === 0) reviewedClosures.delete(id);
};
}
async function importReviewedModule(baseUrl, path) {
const root = realpathSync2(resolve2(fileURLToPath2(baseUrl)));
const receipt = reviewedClosures.get(root);
const rawRoot = resolve2(fileURLToPath2(baseUrl));
const id = pathKey(canonicalPath(rawRoot));
const receipt = reviewedClosures.get(id);
if (!receipt) throw new Error("composition module closure is no longer admitted");
const entry = resolve2(root, path);
const entry = resolve2(rawRoot, path);
const admitted = closureAt(pathToFileURL2(entry).href);
if (!admitted || admitted.root !== root || !(admitted.path in receipt.files)) throw new Error("composition module is absent from reviewed closure");
if (!admitted || admitted.root !== id || !(admitted.path in receipt.files)) throw new Error("composition module is absent from reviewed closure");
return import(pathToFileURL2(entry).href);
}
function closureAt(url) {
if (!url?.startsWith("file:")) return;
const path = resolve2(fileURLToPath2(url));
for (const [root, receipt] of reviewedClosures) {
const inside = relative2(root, path);
if (inside === ".." || inside.startsWith(`..${sep2}`) || isAbsolute2(inside)) continue;
return { root, receipt, path: inside.split(sep2).join("/") };
const inside = keyIn(receipt, path);
if (inside !== void 0) return { root, receipt, path: inside };
}
}
var reviewedClosures;
var init_resolve_hooks = __esm({
"src/dsh/resolve-hooks.ts"() {
"use strict";
init_canonical_path();
init_runtime();
init_bun_closure();
reviewedClosures = /* @__PURE__ */ new Map();
@@ -12477,7 +12536,7 @@ var init_composition_inventory = __esm({
// src/dsh/agent-presets.ts
import { createHash as createHash2 } from "node:crypto";
import { resolve as resolve4, relative as relative3, dirname as dirname3, sep as sep3 } from "node:path";
import { resolve as resolve4, relative as relative2, dirname as dirname3, sep as sep2 } from "node:path";
import { fileURLToPath as fileURLToPath4, pathToFileURL as pathToFileURL5 } from "node:url";
function plain(path) {
return typeof path === "string" && path.length > 0 && path.length <= 4096 && !/[\\:\u0000-\u001f\u007f]/u.test(path) && !path.startsWith("/") && path.split("/").every((p) => p !== "" && p !== "." && p !== "..");
@@ -12529,9 +12588,9 @@ async function reviewAgentPresets(input) {
const files = composition.files;
if (!files || Object.keys(files).length > 4096 || !Array.isArray(input.directories) || input.directories.length > 4096) fail("invalid source inventory");
const base = resolve4("codewhale-reviewed-agent-presets");
const baseUrl = pathToFileURL5(`${base}${sep3}`).href;
const baseUrl = pathToFileURL5(`${base}${sep2}`).href;
function key(path) {
const inside = relative3(base, path).split(sep3).join("/");
const inside = relative2(base, path).split(sep2).join("/");
if (!plain(inside)) fail("path escapes the admitted source root");
return inside;
}
@@ -12550,9 +12609,9 @@ async function reviewAgentPresets(input) {
const parsed = /* @__PURE__ */ new Map();
const io = {
async readFile(path) {
const relative5 = key(path);
if (invalid.has(relative5)) throw new Error("composition exceeds its data/shape bound");
const text2 = documents.get(relative5);
const relative4 = key(path);
if (invalid.has(relative4)) throw new Error("composition exceeds its data/shape bound");
const text2 = documents.get(relative4);
if (text2 === void 0) throw new Error("document not admitted");
return text2;
},
@@ -12603,7 +12662,7 @@ async function reviewAgentPresets(input) {
}
});
if (candidates.length !== 1) fail("shipped root requires exactly one admitted agent-presets package");
const folder = dirname3(candidates[0][0]).split(sep3).join("/");
const folder = dirname3(candidates[0][0]).split(sep2).join("/");
const root = folder === "." ? "presets" : `${folder}/presets`;
if (!directories.has(root)) fail("shipped preset root was not admitted");
roots.push({ path: resolve4(base, root), trust: "system" });
@@ -12786,7 +12845,7 @@ __export(composition_exports, {
});
import { createHash as createHash3 } from "node:crypto";
import { readFile as readFile2 } from "node:fs/promises";
import { isAbsolute as isAbsolute4, relative as relative4, resolve as resolve5, sep as sep4 } from "node:path";
import { isAbsolute as isAbsolute4, relative as relative3, resolve as resolve5, sep as sep3 } from "node:path";
import { fileURLToPath as fileURLToPath5 } from "node:url";
function digest(bytes) {
return createHash3("sha256").update(bytes).digest("hex");
@@ -13061,8 +13120,8 @@ var init_composition = __esm({
async importReviewed(module) {
const root = fileURLToPath5(this.base);
const path = resolve5(root, module.path);
const inside = relative4(root, path);
if (inside.startsWith(`..${sep4}`) || inside === ".." || isAbsolute4(inside)) throw new Error("module escapes reviewed bundle");
const inside = relative3(root, path);
if (inside.startsWith(`..${sep3}`) || inside === ".." || isAbsolute4(inside)) throw new Error("module escapes reviewed bundle");
if (this.spec.files[module.path] !== module.sha256 || digest(await readFile2(path)) !== module.sha256) throw new Error("module changed after review");
return importReviewedModule(this.base, module.path);
}
+49 -3
View File
@@ -1,8 +1,49 @@
// src/dsh/shell-hooks.ts
import { createHash } from "node:crypto";
import { readFileSync, realpathSync, lstatSync } from "node:fs";
import { readFileSync, lstatSync } from "node:fs";
import { resolve, relative, isAbsolute, sep } from "node:path";
// src/dsh/canonical-path.ts
import { realpathSync } from "node:fs";
import { posix, win32 } from "node:path";
function canonicalPath(path, platform = process.platform) {
return stripVerbatim(platform === "win32" ? realpathSync.native(path) : realpathSync(path), platform);
}
function stripVerbatim(path, platform = process.platform) {
if (platform !== "win32") return path;
if (/^[\\/]{2}\?[\\/]UNC[\\/]/i.test(path)) return `\\\\${path.slice(8)}`;
if (/^[\\/]{2}\?[\\/]/.test(path)) return path.slice(4);
return path;
}
function pathKey(path, platform = process.platform) {
if (platform !== "win32") return posix.normalize(path);
return win32.normalize(stripVerbatim(path, platform)).toLowerCase();
}
function samePath(a, b, platform = process.platform) {
return pathKey(a, platform) === pathKey(b, platform);
}
function insideKey(root, target, platform = process.platform) {
const path = platform === "win32" ? win32 : posix;
const inside = path.relative(stripVerbatim(root, platform), stripVerbatim(target, platform));
if (inside === ".." || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside)) return void 0;
return platform === "win32" ? inside.split(win32.sep).join("/") : inside;
}
function isUnlinkedInside(root, key, target, platform = process.platform) {
let canonicalRoot, canonicalTarget;
try {
canonicalRoot = canonicalPath(root, platform);
canonicalTarget = canonicalPath(target, platform);
} catch {
return false;
}
return unlinkedKeyMatches(canonicalRoot, key, canonicalTarget, platform);
}
function unlinkedKeyMatches(canonicalRoot, key, canonicalTarget, platform = process.platform) {
if (!key || key.split("/").some((part) => !part || part === "." || part === "..")) return false;
const path = platform === "win32" ? win32 : posix;
return samePath(path.join(stripVerbatim(canonicalRoot, platform), ...key.split("/")), canonicalTarget, platform);
}
// src/dsh/upstream/hooks/hook-protocol/src/matcher.ts
function isMatchAll(matcher) {
return matcher === void 0 || matcher === "" || matcher === "*";
@@ -136,7 +177,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] || !isUnlinkedInside(root, inside, 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");
@@ -158,5 +199,10 @@ function reviewedHookModule(dialect, root, files) {
} };
}
export {
reviewedHookModule
insideKey,
pathKey,
reviewedHookModule,
samePath,
stripVerbatim,
unlinkedKeyMatches
};
@@ -0,0 +1,77 @@
/**
* One canonical-path rule for the reviewed-closure checks.
*
* Windows: JS `realpathSync` lstats every ancestor starting at the drive root,
* and a Windows LPAC (AppContainer) host cannot read `C:\` (EPERM).
* `realpathSync.native` asks the OS for the opened file's final path
* (GetFinalPathNameByHandle) and needs access to that file alone. Its spelling
* differs from the caller's: a `\\?\` prefix, long names in place of 8.3 short
* names (`RUNNER~1`), drive-letter and on-disk case. So a canonical path is
* never compared with a raw one. Both sides go through `canonicalPath`, then
* `pathKey`, and containment is `relative(canonical root, canonical target)`.
*
* Elsewhere `realpathSync` is unchanged.
*/
import { realpathSync } from 'node:fs'
import { posix, win32 } from 'node:path'
type Platform = NodeJS.Platform
export function canonicalPath(path: string, platform: Platform = process.platform): string {
return stripVerbatim(platform === 'win32' ? realpathSync.native(path) : realpathSync(path), platform)
}
/** Drop the Win32 verbatim prefix: `\\?\C:\x` → `C:\x`, `\\?\UNC\h\s` → `\\h\s`. Case is kept. */
export function stripVerbatim(path: string, platform: Platform = process.platform): string {
if (platform !== 'win32') return path
if (/^[\\/]{2}\?[\\/]UNC[\\/]/i.test(path)) return `\\\\${path.slice(8)}`
if (/^[\\/]{2}\?[\\/]/.test(path)) return path.slice(4)
return path
}
/** The identity used for equality: normalized and, on Windows, case-folded. Never joined or displayed. */
export function pathKey(path: string, platform: Platform = process.platform): string {
if (platform !== 'win32') return posix.normalize(path)
return win32.normalize(stripVerbatim(path, platform)).toLowerCase()
}
export function samePath(a: string, b: string, platform: Platform = process.platform): boolean {
return pathKey(a, platform) === pathKey(b, platform)
}
/**
* The `/`-separated path of `target` inside `root`, or `undefined` when it is
* outside. Both arguments must be spelled the same way (both raw, or both
* from `canonicalPath`). `''` is the root itself, which is never a file key.
* Win32 `relative` compares case-insensitively and keeps `target`'s case.
*/
export function insideKey(root: string, target: string, platform: Platform = process.platform): string | undefined {
const path = platform === 'win32' ? win32 : posix
const inside = path.relative(stripVerbatim(root, platform), stripVerbatim(target, platform))
if (inside === '..' || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside)) return undefined
return platform === 'win32' ? inside.split(win32.sep).join('/') : inside
}
/**
* True when `target` names `root/key` with no symbolic link (or junction)
* inside the root: its canonical path is the canonical root joined with the
* reviewed key. Links above the root are the root's own location and are
* absorbed by canonicalizing it. A missing or unreadable target is false.
*/
export function isUnlinkedInside(root: string, key: string, target: string, platform: Platform = process.platform): boolean {
let canonicalRoot: string, canonicalTarget: string
try {
canonicalRoot = canonicalPath(root, platform)
canonicalTarget = canonicalPath(target, platform)
} catch {
return false
}
return unlinkedKeyMatches(canonicalRoot, key, canonicalTarget, platform)
}
/** Pure half of `isUnlinkedInside`, over already-canonical paths. */
export function unlinkedKeyMatches(canonicalRoot: string, key: string, canonicalTarget: string, platform: Platform = process.platform): boolean {
if (!key || key.split('/').some((part) => !part || part === '.' || part === '..')) return false
const path = platform === 'win32' ? win32 : posix
return samePath(path.join(stripVerbatim(canonicalRoot, platform), ...key.split('/')), canonicalTarget, platform)
}
@@ -19,8 +19,9 @@
import * as nodeModule from 'node:module'
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'
import {resolve,isAbsolute,dirname} from 'node:path'
import {readFileSync} from 'node:fs'
import { canonicalPath, insideKey, pathKey, unlinkedKeyMatches } from './canonical-path.ts'
import { RUNTIME } from '../runtime.ts'
import { prepareBunSource, REVIEWED_IMPORT } from './bun-closure.ts'
@@ -124,7 +125,7 @@ function installBunResolver(modules: Record<string, Record<string, unknown>>) {
const name=String(specifier)
const closure=closureAt(caller)
if(!closure)throw new Error('composition module closure is no longer admitted')
const target=checkedBunSpecifier(name,closure.root,closure.receipt.files,caller,false)
const target=checkedBunSpecifier(name,closure.receipt,caller,false)
const singleton=classifySpecifier(target)
if(singleton!==null)return modules[singleton]
return import(target,options as any)
@@ -152,14 +153,14 @@ 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) || !unlinkedInside(closure.receipt,closure.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
// JSON semantics (including __proto__ data keys) via a JS data module.
if(closure.path.endsWith('.json'))return {contents:`export default JSON.parse(${JSON.stringify(bytes.toString('utf8'))});`,loader:'js'}
let source:string
try {source=prepareBunSource(bytes.toString('utf8'),args.path,(specifier,require)=>checkedBunSpecifier(specifier,closure.root,closure.receipt.files,pathToFileURL(args.path).href,require))}
try {source=prepareBunSource(bytes.toString('utf8'),args.path,(specifier,require)=>checkedBunSpecifier(specifier,closure.receipt,pathToFileURL(args.path).href,require))}
catch(error){if(error instanceof SyntaxError)throw new Error('reviewed composition module has unsupported JavaScript syntax');throw error}
return {contents:source,loader:'js'}
})
@@ -167,7 +168,7 @@ function installBunResolver(modules: Record<string, Record<string, unknown>>) {
})
}
function checkedBunSpecifier(specifier:string,root:string,files:Readonly<Record<string,string>>,caller:string,require:boolean):string {
function checkedBunSpecifier(specifier:string,closure:ReviewedClosure,caller:string,require:boolean):string {
const key=classifySpecifier(specifier)
if(key!==null) {
if(!(key in (globalThis as any)[REGISTRY_KEY]))throw new UnsupportedPeerError(specifier)
@@ -179,17 +180,39 @@ function checkedBunSpecifier(specifier:string,root:string,files:Readonly<Record<
const path=file?(require && !specifier.startsWith('file:')?resolve(dirname(fileURLToPath(caller)),specifier):resolve(fileURLToPath(file))):undefined
if(!path)throw new Error('bare dependency is absent from this reviewed composition; package its reviewed relative source')
if(file?.search || file?.hash)throw new Error('Bun does not preserve reviewed module query or fragment identity; select [extension_host] runtime = \"node\" for this composition')
const inside=relative(root,path).split(sep).join('/')
if(inside==='..' || inside.startsWith('../') || isAbsolute(inside) || !(inside in files))throw new Error('composition import escapes the reviewed file closure')
const inside=keyIn(closure,path)
if(inside===undefined || !(inside in closure.files))throw new Error('composition import escapes the reviewed file closure')
return require?path:file!.href
}
// Module graph admission for an already-reviewed composition. This is an
// ephemeral resolver index over the existing tree's file receipt, not an owner,
// session or plugin state store. Native JS remains arbitrary co-resident code.
const reviewedClosures=new Map<string,{files:Readonly<Record<string,string>>,refs:number}>()
//
// Keyed by the canonical root's `pathKey`. A module path is matched lexically
// against the canonical root or a raw root it was admitted under — never a raw
// spelling against a canonical one (see canonical-path.ts): on Windows the two
// differ in prefix, 8.3 names and case for the very same directory.
interface ReviewedClosure {files:Readonly<Record<string,string>>,refs:number,canonicalRoot:string,rawRoots:Set<string>}
const reviewedClosures=new Map<string,ReviewedClosure>()
function keyIn(closure:ReviewedClosure,path:string):string|undefined {
const canonical=insideKey(closure.canonicalRoot,path)
if(canonical!==undefined)return canonical
for(const root of closure.rawRoots) {
const raw=insideKey(root,path)
if(raw!==undefined)return raw
}
}
/** `path` is `root/key` with no link inside the root (both sides canonicalized). */
function unlinkedInside(closure:ReviewedClosure,key:string,path:string):boolean {
let canonical:string
try {canonical=canonicalPath(path)} catch {return false}
return unlinkedKeyMatches(closure.canonicalRoot,key,canonical)
}
export function admitReviewedClosure(baseUrl:string,files:Readonly<Record<string,string>>):()=>void {
const root=realpathSync(resolve(fileURLToPath(baseUrl)))
const rawRoot=resolve(fileURLToPath(baseUrl))
const canonicalRoot=canonicalPath(rawRoot)
const id=pathKey(canonicalRoot)
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')
@@ -197,31 +220,34 @@ export function admitReviewedClosure(baseUrl:string,files:Readonly<Record<string
if (!key || key.split('/').some(part=>!part || part==='.' || part==='..') || key.includes('\\') || key.includes(':') || isAbsolute(key) || !/^[a-f0-9]{64}$/.test(files[key])) throw new Error('invalid reviewed composition closure file')
accepted[key]=files[key]
}
const existing=reviewedClosures.get(root)
const existing=reviewedClosures.get(id)
if(existing) {
if(Object.keys(existing.files).length!==keys.length || keys.some(key=>existing.files[key]!==accepted[key])) throw new Error('the same composition root carries different file receipts')
existing.refs++
} else reviewedClosures.set(root,{files:Object.freeze(accepted),refs:1})
existing.rawRoots.add(rawRoot)
} else reviewedClosures.set(id,{files:Object.freeze(accepted),refs:1,canonicalRoot,rawRoots:new Set([rawRoot])})
let disposed=false
return ()=>{if(disposed)return;disposed=true;const current=reviewedClosures.get(root);if(current && --current.refs===0)reviewedClosures.delete(root)}
return ()=>{if(disposed)return;disposed=true;const current=reviewedClosures.get(id);if(current && --current.refs===0)reviewedClosures.delete(id)}
}
/** 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 receipt=reviewedClosures.get(root)
const rawRoot=resolve(fileURLToPath(baseUrl))
const id=pathKey(canonicalPath(rawRoot))
const receipt=reviewedClosures.get(id)
if(!receipt)throw new Error('composition module closure is no longer admitted')
const entry=resolve(root,path)
// The entry keeps the caller's spelling of the root: the one the composition
// already read and hashed the module through.
const entry=resolve(rawRoot,path)
const admitted=closureAt(pathToFileURL(entry).href)
if(!admitted || admitted.root!==root || !(admitted.path in receipt.files))throw new Error('composition module is absent from reviewed closure')
if(!admitted || admitted.root!==id || !(admitted.path in receipt.files))throw new Error('composition module is absent from reviewed closure')
return import(pathToFileURL(entry).href)
}
function closureAt(url:string|undefined) {
if(!url?.startsWith('file:'))return
const path=resolve(fileURLToPath(url))
for(const [root,receipt] of reviewedClosures) {
const inside=relative(root,path)
if(inside==='..' || inside.startsWith(`..${sep}`) || isAbsolute(inside))continue
return {root,receipt,path:inside.split(sep).join('/')}
const inside=keyIn(receipt,path)
if(inside!==undefined)return {root,receipt,path:inside}
}
}
@@ -264,6 +290,9 @@ export function installResolveHooks(modules: Record<string, Record<string, unkno
const closure=closureAt(url)
if(closure) {
if(!(closure.path in closure.receipt.files) || result.source===undefined || result.source===null) throw new Error('module was absent from reviewed composition closure')
// Windows runs Node with --preserve-symlinks, so a linked module keeps
// its in-closure URL; refuse it here as the Bun loader does.
if(!unlinkedInside(closure.receipt,closure.path,fileURLToPath(url))) throw new Error('module was absent from reviewed composition closure or contains a symbolic link')
const source=typeof result.source==='string'?Buffer.from(result.source):Buffer.from(result.source as Uint8Array)
if(source.length>64*1024*1024 || createHash('sha256').update(source).digest('hex')!==closure.receipt.files[closure.path]) throw new Error('composition module bytes changed after review')
}
@@ -1,7 +1,10 @@
/** 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'
import { readFileSync,lstatSync } from 'node:fs'
import { resolve,relative,isAbsolute,sep } from 'node:path'
import { isUnlinkedInside } from './canonical-path.ts'
// Re-exported for the pure path-normalization unit test (test/canonical-path.test.mjs).
export { insideKey, pathKey, samePath, stripVerbatim, unlinkedKeyMatches } from './canonical-path.ts'
import { parseClaudeCodeConfig } from './upstream/hooks/hooks-claude-code/src/config.ts'
import { parseCodexConfig } from './upstream/hooks/hooks-codex/src/config.ts'
const EVENTS:Record<string,string>={SessionStart:'session_start',UserPromptSubmit:'message_submit',PreToolUse:'tool_call_before',PostToolUse:'tool_call_after',Stop:'turn_end',SubagentStart:'subagent_spawn',SubagentStop:'subagent_complete'}
@@ -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] || !isUnlinkedInside(root,inside,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.
@@ -0,0 +1,24 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {insideKey,pathKey,samePath,stripVerbatim,unlinkedKeyMatches} from '../dist/shell-hooks.mjs'
// Pure string normalization: runs on every OS by naming the platform.
test('win32 canonical compare ignores the verbatim prefix and case, never the path itself',()=>{
assert.equal(stripVerbatim('\\\\?\\C:\\Users\\runneradmin\\x','win32'),'C:\\Users\\runneradmin\\x')
assert.equal(stripVerbatim('\\\\?\\UNC\\host\\share\\x','win32'),'\\\\host\\share\\x')
assert.ok(samePath('\\\\?\\C:\\Users\\RunnerAdmin\\Bundle\\hooks.json','c:\\users\\runneradmin\\bundle\\hooks.json','win32'))
assert.equal(pathKey('\\\\?\\C:\\A\\B','win32'),pathKey('c:\\a\\b','win32'))
assert.ok(!samePath('C:\\a\\b','C:\\a\\c','win32'))
// Case folds only on win32.
assert.ok(!samePath('/a/B','/a/b','linux'))
// Containment against a differently spelled canonical root keeps the target's case.
assert.equal(insideKey('\\\\?\\C:\\Users\\RUNNERADMIN\\bundle','c:\\users\\runneradmin\\bundle\\Mod\\a.js','win32'),'Mod/a.js')
assert.equal(insideKey('C:\\bundle','C:\\bundle-evil\\a.js','win32'),undefined)
assert.equal(insideKey('C:\\bundle','D:\\bundle\\a.js','win32'),undefined)
assert.equal(insideKey('/r','/r/../x','linux'),undefined)
// A file is unlinked when its canonical path is the canonical root joined with its reviewed key.
assert.ok(unlinkedKeyMatches('\\\\?\\C:\\Users\\runneradmin\\bundle','hooks.json','C:\\USERS\\RunnerAdmin\\Bundle\\HOOKS.JSON','win32'))
assert.ok(!unlinkedKeyMatches('C:\\bundle','link.json','C:\\bundle\\hooks.json','win32'))
assert.ok(!unlinkedKeyMatches('C:\\bundle','../x.json','C:\\x.json','win32'))
assert.ok(!unlinkedKeyMatches('/r','link.json','/r/hooks.json','linux'))
})