@code-yeongyu/senpi-codemode 2026.10.27-2 → 2026.10.30-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/package.json +4 -4
- package/src/bridge/http-server.ts +18 -5
- package/src/bridge/permission-check.ts +76 -0
- package/src/bridge/protocol.ts +7 -0
- package/src/kernels/jl/kernel.ts +3 -0
- package/src/kernels/js/context-manager.ts +26 -8
- package/src/kernels/js/inline-worker-entry.js +1 -23
- package/src/kernels/js/inline-worker.ts +7 -2
- package/src/kernels/js/interpose-bun.js +220 -0
- package/src/kernels/js/interpose-capabilities.js +29 -0
- package/src/kernels/js/interpose-code.js +8 -0
- package/src/kernels/js/interpose-fs.js +124 -0
- package/src/kernels/js/interpose-modules.js +107 -0
- package/src/kernels/js/interpose-paths.js +80 -0
- package/src/kernels/js/interpose-process.js +97 -0
- package/src/kernels/js/interpose-resources.js +59 -0
- package/src/kernels/js/interpose-sqlite.js +104 -0
- package/src/kernels/js/interpose.js +86 -0
- package/src/kernels/js/kernel-contract.ts +31 -0
- package/src/kernels/js/kernel-tools-pump.js +4 -2
- package/src/kernels/js/local-module-loader.ts +9 -4
- package/src/kernels/js/permission-authority.d.mts +10 -0
- package/src/kernels/js/permission-authority.mjs +11 -0
- package/src/kernels/js/permission-core.md +114 -0
- package/src/kernels/js/permission-host.ts +388 -0
- package/src/kernels/js/permission-runtime.js +205 -0
- package/src/kernels/js/permission-sessions.ts +46 -0
- package/src/kernels/js/permission-slots.d.mts +51 -0
- package/src/kernels/js/permission-slots.mjs +133 -0
- package/src/kernels/js/permission-worker-entry.js +1 -0
- package/src/kernels/js/permission-worker-frames.ts +55 -0
- package/src/kernels/js/process-tree.js +9 -1
- package/src/kernels/js/rewrite-imports.ts +44 -0
- package/src/kernels/js/windows-tree-kill.js +10 -2
- package/src/kernels/js/worker-core.d.ts +2 -0
- package/src/kernels/js/worker-core.js +5 -2
- package/src/kernels/js/worker-cwd.js +14 -8
- package/src/kernels/js/worker-entry.js +1 -23
- package/src/kernels/js/worker-file-helpers.js +58 -0
- package/src/kernels/js/worker-host.ts +24 -6
- package/src/kernels/js/worker-package-resolve.js +10 -2
- package/src/kernels/js/worker-runtime.js +5 -65
- package/src/kernels/js/worker-slot.ts +5 -1
- package/src/kernels/js/worker-startup.ts +7 -5
- package/src/kernels/js/worker-webview.js +17 -16
- package/src/kernels/py/interpose.py +765 -0
- package/src/kernels/py/kernel-contract.ts +4 -0
- package/src/kernels/py/kernel-tools-host.ts +5 -1
- package/src/kernels/py/kernel.ts +45 -5
- package/src/kernels/py/path_identity.py +162 -0
- package/src/kernels/py/path_identity_posix.py +74 -0
- package/src/kernels/py/path_identity_windows.py +93 -0
- package/src/kernels/py/permission-core.md +103 -0
- package/src/kernels/py/permission-host.ts +198 -0
- package/src/kernels/py/permission_bridge.py +145 -0
- package/src/kernels/py/prelude.py +40 -5
- package/src/kernels/py/transport.ts +15 -8
- package/src/kernels/rb/kernel.ts +3 -0
- package/src/kernels/shared/invocation-authority.ts +59 -0
- package/src/kernels/shared/kernel-tools-pump.ts +22 -1
- package/src/kernels/shared/path-identity.d.mts +42 -0
- package/src/kernels/shared/path-identity.mjs +149 -0
- package/src/kernels/shared/subprocess-contract.ts +2 -0
- package/src/kernels/shared/subprocess-kernel.ts +28 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { checkPath, executionPath, replaceMethod } from "./interpose-paths.js";
|
|
2
|
+
import { createResourceTable } from "./interpose-resources.js";
|
|
3
|
+
|
|
4
|
+
export const FS_PERMISSION_PATHS = new Set([
|
|
5
|
+
"writeFile", "appendFile", "truncate", "mkdir", "mkdtemp", "rm", "rmdir", "unlink",
|
|
6
|
+
"symlink", "chmod", "chown", "lchmod", "lchown", "utimes", "lutimes", "rename", "link", "copyFile", "cp",
|
|
7
|
+
"readFile", "readlink", "realpath", "readdir", "opendir", "open", "openAsBlob",
|
|
8
|
+
"createReadStream", "createWriteStream",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export function installFilesystemPermissions(check) {
|
|
12
|
+
const fs = process.getBuiltinModule("node:fs");
|
|
13
|
+
const resources = createResourceTable(check);
|
|
14
|
+
const promises = [...new Set([fs.promises, process.getBuiltinModule("node:fs/promises")])];
|
|
15
|
+
const decisions = {
|
|
16
|
+
writeFile: [[0, "write"]], appendFile: [[0, "write"]], truncate: [[0, "write"]],
|
|
17
|
+
mkdir: [[0, "write"]], mkdtemp: [[0, "write"]], rm: [[0, "write"]], rmdir: [[0, "write"]],
|
|
18
|
+
unlink: [[0, "write"]], symlink: [[1, "write"]], chmod: [[0, "write"]], chown: [[0, "write"]],
|
|
19
|
+
lchmod: [[0, "write"]], lchown: [[0, "write"]],
|
|
20
|
+
utimes: [[0, "write"]], lutimes: [[0, "write"]], rename: [[0, "write"], [1, "write"]],
|
|
21
|
+
link: [[0, "write"], [1, "write"]], copyFile: [[0, "read"], [1, "write"]], cp: [[0, "read"], [1, "write"]],
|
|
22
|
+
readFile: [[0, "read"]], readlink: [[0, "read"]], realpath: [[0, "read"]],
|
|
23
|
+
readdir: [[0, "ls"]], opendir: [[0, "ls"]], openAsBlob: [[0, "read"]],
|
|
24
|
+
};
|
|
25
|
+
const guard = (original, entries) => function (...args) {
|
|
26
|
+
const actual = [...args];
|
|
27
|
+
for (const [index, tool] of entries) {
|
|
28
|
+
const value = args[index];
|
|
29
|
+
if (typeof value === "number" || (value !== null && typeof value === "object" && typeof value.fd === "number")) {
|
|
30
|
+
resources.use(value, tool === "write" ? "write" : "read");
|
|
31
|
+
} else {
|
|
32
|
+
checkPath(check, tool, value);
|
|
33
|
+
actual[index] = executionPath(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return original.apply(this, actual);
|
|
37
|
+
};
|
|
38
|
+
for (const [name, entries] of Object.entries(decisions)) {
|
|
39
|
+
replaceMethod(fs, name, (original) => guard(original, entries));
|
|
40
|
+
replaceMethod(fs, `${name}Sync`, (original) => guard(original, entries));
|
|
41
|
+
for (const owner of promises) replaceMethod(owner, name, (original) => guard(original, entries));
|
|
42
|
+
}
|
|
43
|
+
replaceMethod(fs.realpath, "native", (original) => guard(original, [[0, "read"]]));
|
|
44
|
+
replaceMethod(fs.realpathSync, "native", (original) => guard(original, [[0, "read"]]));
|
|
45
|
+
for (const owner of [fs, ...promises]) {
|
|
46
|
+
for (const name of ["glob", "globSync"]) replaceMethod(owner, name, (original) => function (pattern, ...args) {
|
|
47
|
+
const options = args[0];
|
|
48
|
+
const cwd = options && typeof options === "object" ? options.cwd ?? process.cwd() : process.cwd();
|
|
49
|
+
checkPath(check, "ls", cwd);
|
|
50
|
+
const effective = options && typeof options === "object" ? { ...options, cwd: executionPath(cwd) } : { cwd: process.cwd() };
|
|
51
|
+
return typeof options === "function"
|
|
52
|
+
? original.call(this, pattern, effective, ...args)
|
|
53
|
+
: original.call(this, pattern, effective, ...args.slice(1));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
replaceMethod(fs, "openSync", (original) => function (file, flags, ...args) {
|
|
57
|
+
const modes = resources.open(file, flags);
|
|
58
|
+
return resources.record(original.call(this, executionPath(file), flags, ...args), file, modes);
|
|
59
|
+
});
|
|
60
|
+
replaceMethod(fs, "open", (original) => function (file, flags, ...args) {
|
|
61
|
+
if (typeof flags === "function") { args = [flags]; flags = undefined; }
|
|
62
|
+
const modes = resources.open(file, flags);
|
|
63
|
+
const callback = args.at(-1);
|
|
64
|
+
if (typeof callback !== "function") return original.call(this, executionPath(file), flags, ...args);
|
|
65
|
+
return original.call(this, executionPath(file), flags, ...args.slice(0, -1), (error, fd) => {
|
|
66
|
+
if (!error) resources.record(fd, file, modes);
|
|
67
|
+
callback(error, fd);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
for (const owner of promises) replaceMethod(owner, "open", (original) => function (file, flags, ...args) {
|
|
71
|
+
const modes = resources.open(file, flags);
|
|
72
|
+
return original.call(this, executionPath(file), flags, ...args).then((handle) => resources.handle(handle, file, modes));
|
|
73
|
+
});
|
|
74
|
+
for (const [mode, names] of [
|
|
75
|
+
["read", ["read", "readv"]],
|
|
76
|
+
["write", ["write", "writev", "ftruncate", "fchmod", "fchown", "futimes", "fsync", "fdatasync"]],
|
|
77
|
+
]) {
|
|
78
|
+
for (const name of names) {
|
|
79
|
+
for (const key of [name, `${name}Sync`]) replaceMethod(fs, key, (original) => function (fd, ...args) {
|
|
80
|
+
resources.use(fd, mode);
|
|
81
|
+
return original.call(this, fd, ...args);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
replaceMethod(fs, "closeSync", (original) => function (fd, ...args) {
|
|
86
|
+
resources.use(fd);
|
|
87
|
+
const result = original.call(this, fd, ...args);
|
|
88
|
+
resources.forget(fd);
|
|
89
|
+
return result;
|
|
90
|
+
});
|
|
91
|
+
replaceMethod(fs, "close", (original) => function (fd, callback) {
|
|
92
|
+
resources.use(fd);
|
|
93
|
+
return original.call(this, fd, (error) => { if (!error) resources.forget(fd); callback(error); });
|
|
94
|
+
});
|
|
95
|
+
for (const [name, mode] of [["createReadStream", "read"], ["createWriteStream", "write"]]) {
|
|
96
|
+
replaceMethod(fs, name, (original) => function (file, options) {
|
|
97
|
+
const fd = options?.fd;
|
|
98
|
+
if (fd != null) resources.use(fd, mode);
|
|
99
|
+
else checkPath(check, mode, file);
|
|
100
|
+
const stream = original.call(this, executionPath(file), options);
|
|
101
|
+
resources.record(stream, file, [mode]);
|
|
102
|
+
stream.once("open", (descriptor) => resources.record(descriptor, file, [mode]));
|
|
103
|
+
const methods = mode === "write" ? ["write", "end"] : ["read"];
|
|
104
|
+
for (const method of methods) replaceMethod(stream, method, (fn) => function (...args) {
|
|
105
|
+
resources.use(stream, mode);
|
|
106
|
+
return fn.apply(this, args);
|
|
107
|
+
});
|
|
108
|
+
return stream;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const protectBlob = (blob, file) => {
|
|
112
|
+
resources.record(blob, file, ["read"]);
|
|
113
|
+
for (const name of ["text", "bytes", "arrayBuffer", "stream", "slice"]) replaceMethod(blob, name, (fn) => function (...params) {
|
|
114
|
+
resources.use(this, "read");
|
|
115
|
+
const value = fn.apply(this, params);
|
|
116
|
+
return name === "slice" ? protectBlob(value, file) : value;
|
|
117
|
+
});
|
|
118
|
+
return blob;
|
|
119
|
+
};
|
|
120
|
+
replaceMethod(fs, "openAsBlob", (original) => function (file, ...args) {
|
|
121
|
+
return original.call(this, file, ...args).then((blob) => protectBlob(blob, file));
|
|
122
|
+
});
|
|
123
|
+
return resources;
|
|
124
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { checkPath, requestedPath } from "./interpose-paths.js";
|
|
2
|
+
|
|
3
|
+
const { dirname, isAbsolute, resolve } = process.getBuiltinModule("node:path");
|
|
4
|
+
const { fileURLToPath } = process.getBuiltinModule("node:url");
|
|
5
|
+
|
|
6
|
+
export function installModulePermissions(check, userWorker = false) {
|
|
7
|
+
const module = process.getBuiltinModule("node:module");
|
|
8
|
+
const nativeRequire = module.createRequire(import.meta.url);
|
|
9
|
+
const resolveSync = globalThis.Bun?.resolveSync;
|
|
10
|
+
let sealed = false;
|
|
11
|
+
// Resolving a specifier re-enters this hook, because the engine resolver is itself observed. That
|
|
12
|
+
// re-entry is one import still being decided, not another import, so it must not ask again.
|
|
13
|
+
let resolving = false;
|
|
14
|
+
// A user worker's own script reaches its own workerData and parentPort through this module, which
|
|
15
|
+
// #4387 keeps transparent, so only the cell context treats the module itself as a capability.
|
|
16
|
+
const forbidden = userWorker
|
|
17
|
+
? /^(?:bun:ffi|(?:node:)?vm)$/u
|
|
18
|
+
: /^(?:bun:ffi|(?:node:)?(?:vm|worker_threads))$/u;
|
|
19
|
+
const workerThreads = /^(?:node:)?worker_threads$/u;
|
|
20
|
+
// A cell already owns these as globals, so handing them back over require grants nothing, while
|
|
21
|
+
// Worker, parentPort and workerData stay out of reach: the raw Worker would spawn an uninterposed
|
|
22
|
+
// one. Only require can substitute a namespace, so import and getBuiltinModule stay refused.
|
|
23
|
+
const portPrimitives = () =>
|
|
24
|
+
Object.freeze(
|
|
25
|
+
Object.fromEntries(
|
|
26
|
+
["MessageChannel", "MessagePort", "BroadcastChannel"]
|
|
27
|
+
.filter((name) => name in globalThis)
|
|
28
|
+
.map((name) => [name, globalThis[name]]),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
const pathLike = (name) => name.startsWith(".") || isAbsolute(name) || name.startsWith("file:");
|
|
32
|
+
const guard = (source, importer) => {
|
|
33
|
+
const specifier = String(source);
|
|
34
|
+
if (!sealed || resolving) return;
|
|
35
|
+
if (forbidden.test(specifier) || specifier.endsWith(".node")) check(() => ({ unsupported: true }));
|
|
36
|
+
if (!pathLike(specifier)) return;
|
|
37
|
+
const parent = importer?.startsWith("file:") ? fileURLToPath(importer) : importer;
|
|
38
|
+
const base = parent ? dirname(parent) : process.cwd();
|
|
39
|
+
const candidate = specifier.startsWith("file:") ? fileURLToPath(specifier) : specifier;
|
|
40
|
+
let target;
|
|
41
|
+
resolving = true;
|
|
42
|
+
try {
|
|
43
|
+
target = resolveSync ? resolveSync(candidate, base) : nativeRequire.resolve(resolve(base, candidate));
|
|
44
|
+
} catch {
|
|
45
|
+
target = isAbsolute(candidate) ? candidate : resolve(base, candidate);
|
|
46
|
+
} finally {
|
|
47
|
+
resolving = false;
|
|
48
|
+
}
|
|
49
|
+
checkPath(check, "read", target);
|
|
50
|
+
};
|
|
51
|
+
// Only resolution is observed: Bun owns every loader and import attribute.
|
|
52
|
+
if (globalThis.Bun?.plugin) {
|
|
53
|
+
Bun.plugin({
|
|
54
|
+
name: "senpi-cell-permissions",
|
|
55
|
+
setup(build) {
|
|
56
|
+
build.onResolve({ filter: /.*/u }, (args) => { guard(args.path, args.importer); });
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// The decision comes first. A cell nothing is enforced against sees the engine's own module, so
|
|
61
|
+
// no-rules behaviour is unchanged and matches what import() returns; only a refusal is answered
|
|
62
|
+
// with the port primitives the cell already owns as globals.
|
|
63
|
+
const requireGuard = (specifier, importer) => {
|
|
64
|
+
if (!sealed || !workerThreads.test(String(specifier))) {
|
|
65
|
+
guard(specifier, importer);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
guard(specifier, importer);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error?.code !== "cell_permission_denied") throw error;
|
|
72
|
+
return portPrimitives();
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
};
|
|
76
|
+
const originalRequire = module.prototype.require;
|
|
77
|
+
module.prototype.require = function (specifier) {
|
|
78
|
+
const substitute = requireGuard(specifier, this.filename);
|
|
79
|
+
if (substitute) return substitute;
|
|
80
|
+
if (specifier === "bun" && globalThis.Bun) return globalThis[Symbol.for("senpi.kernel.bun")] ?? globalThis.Bun;
|
|
81
|
+
return originalRequire.call(this, specifier);
|
|
82
|
+
};
|
|
83
|
+
const originalCreateRequire = module.createRequire;
|
|
84
|
+
module.createRequire = function (...args) {
|
|
85
|
+
const require = originalCreateRequire.apply(this, args);
|
|
86
|
+
const guarded = function (specifier) {
|
|
87
|
+
const substitute = requireGuard(specifier, requestedPath(args[0]) ?? String(args[0]));
|
|
88
|
+
if (substitute) return substitute;
|
|
89
|
+
if (specifier === "bun" && globalThis.Bun) return globalThis[Symbol.for("senpi.kernel.bun")] ?? globalThis.Bun;
|
|
90
|
+
return require(specifier);
|
|
91
|
+
};
|
|
92
|
+
Object.assign(guarded, require);
|
|
93
|
+
guarded.cache = new Proxy(require.cache, {
|
|
94
|
+
set(target, name, value) { check(() => ({ unsupported: true })); return Reflect.set(target, name, value); },
|
|
95
|
+
deleteProperty(target, name) { check(() => ({ unsupported: true })); return Reflect.deleteProperty(target, name); },
|
|
96
|
+
defineProperty(target, name, descriptor) { check(() => ({ unsupported: true })); return Reflect.defineProperty(target, name, descriptor); },
|
|
97
|
+
});
|
|
98
|
+
return guarded;
|
|
99
|
+
};
|
|
100
|
+
const getBuiltin = process.getBuiltinModule;
|
|
101
|
+
process.getBuiltinModule = function (name) {
|
|
102
|
+
if (sealed && forbidden.test(name)) check(() => ({ unsupported: true }));
|
|
103
|
+
return getBuiltin.call(this, name);
|
|
104
|
+
};
|
|
105
|
+
Object.defineProperty(globalThis, Symbol.for("senpi.kernel.permissionImport"), { value: guard });
|
|
106
|
+
return () => { sealed = true; };
|
|
107
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createPathIdentity, PATH_IDENTITY_VERSION } from "../shared/path-identity.mjs";
|
|
2
|
+
|
|
3
|
+
const path = process.getBuiltinModule("node:path");
|
|
4
|
+
const fs = process.getBuiltinModule("node:fs");
|
|
5
|
+
const { fileURLToPath } = process.getBuiltinModule("node:url");
|
|
6
|
+
|
|
7
|
+
// Captured at module evaluation, before any interposition or cwd patch replaces them: the identity
|
|
8
|
+
// must resolve with the engine's own native realpath and must never reach the live `path.resolve`
|
|
9
|
+
// that worker-cwd.js rebinds to the session root (design N11).
|
|
10
|
+
const identity = createPathIdentity({
|
|
11
|
+
realpathNative: fs.realpathSync.native,
|
|
12
|
+
lstat: fs.lstatSync,
|
|
13
|
+
readlink: fs.readlinkSync,
|
|
14
|
+
pathApi: {
|
|
15
|
+
join: path.join,
|
|
16
|
+
dirname: path.dirname,
|
|
17
|
+
basename: path.basename,
|
|
18
|
+
isAbsolute: path.isAbsolute,
|
|
19
|
+
sep: path.sep,
|
|
20
|
+
win32Resolve: path.win32.resolve,
|
|
21
|
+
},
|
|
22
|
+
platform: process.platform,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export function requestedPath(value) {
|
|
26
|
+
if (value instanceof URL) return fileURLToPath(value);
|
|
27
|
+
if (value instanceof Uint8Array) {
|
|
28
|
+
const text = Buffer.from(value).toString();
|
|
29
|
+
return Buffer.from(text).equals(value) ? text : undefined;
|
|
30
|
+
}
|
|
31
|
+
return typeof value === "string" ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function executionPath(value) {
|
|
35
|
+
if (typeof value === "string") return path.resolve(process.cwd(), value);
|
|
36
|
+
if (value instanceof Uint8Array && !path.isAbsolute(Buffer.from(value).toString("latin1"))) {
|
|
37
|
+
return Buffer.concat([Buffer.from(process.cwd() + path.sep), value]);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The written spelling, the executed spelling, and the kernel-computed canonical form (or why there is none). */
|
|
43
|
+
function pathIdentityInput(value, executed = requestedPath(executionPath(value))) {
|
|
44
|
+
const requested = requestedPath(value);
|
|
45
|
+
if (requested === undefined || executed === undefined) return { input: { path: requested }, unsupported: true };
|
|
46
|
+
const input = { path: requested, executedPath: executed };
|
|
47
|
+
try {
|
|
48
|
+
const { path: canonical, foldCase } = identity.canonical(executed);
|
|
49
|
+
input.canonical = { v: PATH_IDENTITY_VERSION, path: canonical, foldCase };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
input.unresolvable = typeof error?.code === "string" ? error.code : "EUNKNOWN";
|
|
52
|
+
}
|
|
53
|
+
return { input, unsupported: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function checkPath(check, tool, value, options = {}) {
|
|
57
|
+
try {
|
|
58
|
+
check(() => {
|
|
59
|
+
const { input, unsupported } = pathIdentityInput(value, options.executed);
|
|
60
|
+
return { call: { tool, input, cwd: options.cwd ?? process.cwd() }, unsupported };
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error?.code === "cell_permission_denied" && typeof value === "string") error.message += `: ${value}`;
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function replaceMethod(owner, name, wrap) {
|
|
69
|
+
const original = owner?.[name];
|
|
70
|
+
if (typeof original !== "function") return;
|
|
71
|
+
const wrapped = wrap(original);
|
|
72
|
+
for (const key of Reflect.ownKeys(original)) {
|
|
73
|
+
if (key === "length" || key === "name" || key === "prototype" || Object.hasOwn(wrapped, key)) continue;
|
|
74
|
+
Object.defineProperty(wrapped, key, Object.getOwnPropertyDescriptor(original, key));
|
|
75
|
+
}
|
|
76
|
+
Object.defineProperty(owner, name, {
|
|
77
|
+
...(Object.getOwnPropertyDescriptor(owner, name) ?? { configurable: true, writable: true, enumerable: false }),
|
|
78
|
+
value: wrapped,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { replaceMethod, requestedPath } from "./interpose-paths.js";
|
|
2
|
+
|
|
3
|
+
const { basename } = process.getBuiltinModule("node:path");
|
|
4
|
+
// Bun gives an explicit stdio[i] priority over the matching named option, so both are decided rather
|
|
5
|
+
// than one standing in for the other. The first three slots carry one direction each; a slot at
|
|
6
|
+
// index 3 or above has no named option and is handed to the child as a descriptor it can both read
|
|
7
|
+
// and write.
|
|
8
|
+
const SLOT_MODES = [["read"], ["write"], ["write"]];
|
|
9
|
+
const EXTRA_SLOT_MODES = ["read", "write"];
|
|
10
|
+
const quote = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
11
|
+
const cwdOf = (value) => requestedPath(value) ?? process.cwd();
|
|
12
|
+
|
|
13
|
+
export function installProcessPermissions(check, files) {
|
|
14
|
+
const cp = process.getBuiltinModule("node:child_process");
|
|
15
|
+
for (const name of ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync", "fork"]) {
|
|
16
|
+
replaceMethod(cp, name, (original) => function (...parameters) {
|
|
17
|
+
check(() => {
|
|
18
|
+
const [file, args, options] = parameters;
|
|
19
|
+
const shellText = name === "exec" || name === "execSync";
|
|
20
|
+
const argv = Array.isArray(args) ? args : [];
|
|
21
|
+
const opts = shellText ? args : Array.isArray(args) || args == null ? options : args;
|
|
22
|
+
const shell = opts?.shell;
|
|
23
|
+
const command = name === "fork"
|
|
24
|
+
? [opts?.execPath ?? process.execPath, ...(opts?.execArgv ?? process.execArgv), file, ...argv].map(quote).join(" ")
|
|
25
|
+
: shellText ? file : shell ? [file, ...argv].join(" ") : [file, ...argv].map(quote).join(" ");
|
|
26
|
+
return {
|
|
27
|
+
call: { tool: "bash", input: { command }, cwd: cwdOf(opts?.cwd) },
|
|
28
|
+
unsupported: typeof shell === "string" && !/^(?:sh|bash|zsh|dash)$/u.test(basename(shell)),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
return original.apply(this, parameters);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const bun = globalThis.Bun;
|
|
35
|
+
if (!bun) return;
|
|
36
|
+
for (const name of ["spawn", "spawnSync"]) replaceMethod(bun, name, (original) => function (...parameters) {
|
|
37
|
+
const [first, second] = parameters;
|
|
38
|
+
const options = Array.isArray(first) ? second : first;
|
|
39
|
+
check(() => {
|
|
40
|
+
const argv = Array.isArray(first) ? first : first?.cmd;
|
|
41
|
+
return {
|
|
42
|
+
call: { tool: "bash", input: { command: Array.isArray(argv) ? argv.map(quote).join(" ") : "" }, cwd: cwdOf(options?.cwd) },
|
|
43
|
+
unsupported: !Array.isArray(argv),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
const slots = Array.isArray(options?.stdio) ? options.stdio : [];
|
|
47
|
+
files.source(options?.stdin); // sink:spawn-stdin
|
|
48
|
+
files.handle(options?.stdout, "write"); // sink:spawn-stdout
|
|
49
|
+
files.handle(options?.stderr, "write"); // sink:spawn-stderr
|
|
50
|
+
for (const [index, slot] of slots.entries()) {
|
|
51
|
+
for (const mode of SLOT_MODES[index] ?? EXTRA_SLOT_MODES) {
|
|
52
|
+
if (mode === "read") files.source(slot); // sink:spawn-stdio-read
|
|
53
|
+
if (mode === "write") files.handle(slot, "write"); // sink:spawn-stdio-write
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return original.apply(this, parameters);
|
|
57
|
+
});
|
|
58
|
+
const shell = bun.$;
|
|
59
|
+
if (typeof shell !== "function") return;
|
|
60
|
+
let shellCwd;
|
|
61
|
+
const render = (value) => {
|
|
62
|
+
if (Array.isArray(value)) return value.map(render).join(" ");
|
|
63
|
+
if (value && typeof value === "object") {
|
|
64
|
+
if (typeof value.raw === "string") return value.raw;
|
|
65
|
+
check(() => ({ unsupported: true }));
|
|
66
|
+
}
|
|
67
|
+
return shell.escape(String(value));
|
|
68
|
+
};
|
|
69
|
+
const guarded = function (strings, ...values) {
|
|
70
|
+
check(() => ({
|
|
71
|
+
call: {
|
|
72
|
+
tool: "bash",
|
|
73
|
+
input: { command: strings.map((part, index) => part + (index < values.length ? render(values[index]) : "")).join("") },
|
|
74
|
+
cwd: cwdOf(shellCwd),
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
return shell(strings, ...values);
|
|
78
|
+
};
|
|
79
|
+
for (const key of Reflect.ownKeys(shell)) {
|
|
80
|
+
if (key !== "Shell" && !Object.hasOwn(guarded, key)) Object.defineProperty(guarded, key, Object.getOwnPropertyDescriptor(shell, key));
|
|
81
|
+
}
|
|
82
|
+
for (const name of ["cwd", "env", "nothrow", "throws"]) {
|
|
83
|
+
if (typeof shell[name] !== "function") continue;
|
|
84
|
+
guarded[name] = (...args) => {
|
|
85
|
+
if (name === "cwd") shellCwd = args[0];
|
|
86
|
+
shell[name](...args);
|
|
87
|
+
return guarded;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (shell.Shell) guarded.Shell = new Proxy(shell.Shell, {
|
|
91
|
+
construct(target, args, newTarget) {
|
|
92
|
+
check(() => ({ unsupported: true }));
|
|
93
|
+
return Reflect.construct(target, args, newTarget);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
bun.$ = guarded;
|
|
97
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { checkPath, replaceMethod } from "./interpose-paths.js";
|
|
2
|
+
|
|
3
|
+
const { constants } = process.getBuiltinModule("node:fs");
|
|
4
|
+
|
|
5
|
+
function openModes(flags = "r") {
|
|
6
|
+
flags ??= "r";
|
|
7
|
+
if (typeof flags === "number") {
|
|
8
|
+
const access = flags & (constants.O_RDONLY | constants.O_WRONLY | constants.O_RDWR);
|
|
9
|
+
if (access === constants.O_WRONLY) return ["write"];
|
|
10
|
+
return access === constants.O_RDWR || (flags & (constants.O_CREAT | constants.O_TRUNC)) !== 0 ? ["read", "write"] : ["read"];
|
|
11
|
+
}
|
|
12
|
+
return flags.includes("+") ? ["read", "write"] : /^[wax]/u.test(flags) ? ["write"] : ["read"];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createResourceTable(check) {
|
|
16
|
+
const descriptors = new Map();
|
|
17
|
+
const objects = new WeakMap();
|
|
18
|
+
const table = (resource) => typeof resource === "number" ? descriptors : objects;
|
|
19
|
+
const record = (resource, path, modes) => {
|
|
20
|
+
if (typeof resource === "number" || (resource !== null && typeof resource === "object")) {
|
|
21
|
+
table(resource).set(resource, { path, modes });
|
|
22
|
+
}
|
|
23
|
+
return resource;
|
|
24
|
+
};
|
|
25
|
+
const forget = (resource) => table(resource).delete(resource);
|
|
26
|
+
const use = (resource, mode) => {
|
|
27
|
+
check(() => {
|
|
28
|
+
const grant = table(resource).get(resource);
|
|
29
|
+
return { granted: true, unsupported: !grant || (mode !== undefined && !grant.modes.includes(mode)) };
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const path = (resource) => table(resource).get(resource)?.path;
|
|
33
|
+
const open = (file, flags) => {
|
|
34
|
+
const modes = openModes(flags);
|
|
35
|
+
for (const mode of modes) checkPath(check, mode === "read" ? "read" : "write", file);
|
|
36
|
+
return modes;
|
|
37
|
+
};
|
|
38
|
+
const handle = (value, file, modes) => {
|
|
39
|
+
record(value, file, modes);
|
|
40
|
+
record(value.fd, file, modes);
|
|
41
|
+
for (const [mode, names] of [
|
|
42
|
+
["read", ["read", "readv", "readFile", "readLines", "createReadStream", "readableWebStream"]],
|
|
43
|
+
["write", ["write", "writev", "writeFile", "appendFile", "truncate", "chmod", "chown", "utimes", "sync", "datasync", "createWriteStream"]],
|
|
44
|
+
]) {
|
|
45
|
+
for (const name of names) replaceMethod(value, name, (original) => function (...args) {
|
|
46
|
+
use(this, mode);
|
|
47
|
+
const result = original.apply(this, args);
|
|
48
|
+
if (/Stream$/u.test(name)) record(result, file, [mode]);
|
|
49
|
+
return result;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
replaceMethod(value, "close", (original) => function (...args) {
|
|
53
|
+
const fd = this.fd;
|
|
54
|
+
return original.apply(this, args).then((result) => { forget(fd); forget(this); return result; });
|
|
55
|
+
});
|
|
56
|
+
return value;
|
|
57
|
+
};
|
|
58
|
+
return { record, forget, use, path, open, handle };
|
|
59
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { checkPath, executionPath, replaceMethod } from "./interpose-paths.js";
|
|
2
|
+
|
|
3
|
+
// Skip quoted data/identifiers and comments, retaining semicolons to examine every statement.
|
|
4
|
+
function sqlWords(sql) {
|
|
5
|
+
return String(sql).replace(/--[^\n]*|\/\*[\s\S]*?\*\/|'(?:''|[^'])*'|"(?:""|[^"])*"|`(?:``|[^`])*`|\[[^\]]*\]/gu, " ")
|
|
6
|
+
.toUpperCase().match(/[A-Z_]+|;/gu) ?? [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function reachesOtherFile(sql) {
|
|
10
|
+
let first;
|
|
11
|
+
for (const word of sqlWords(sql)) {
|
|
12
|
+
if (word === ";") { first = undefined; continue; }
|
|
13
|
+
first ??= word;
|
|
14
|
+
if (first === "ATTACH" || (first === "VACUUM" && word === "INTO")) return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function installSqlitePermissions(check) {
|
|
20
|
+
const paths = new WeakMap();
|
|
21
|
+
const memory = (path) => path === ":memory:" || path === "";
|
|
22
|
+
const file = (path, mode) => { if (!memory(path)) checkPath(check, mode, path); };
|
|
23
|
+
const refuse = () => check(() => ({ unsupported: true }));
|
|
24
|
+
const guardSql = (sql) => check(() => ({ granted: true, unsupported: reachesOtherFile(sql) }));
|
|
25
|
+
const install = (sqlite, name, node) => {
|
|
26
|
+
const Original = sqlite[name];
|
|
27
|
+
if (!Original) return;
|
|
28
|
+
const readonly = (options) => node ? options?.readOnly === true
|
|
29
|
+
: typeof options === "number" ? (options & sqlite.constants.SQLITE_OPEN_READONLY) !== 0 : options?.readonly === true;
|
|
30
|
+
const opening = (path, options) => {
|
|
31
|
+
if (node && options?.allowExtension) refuse();
|
|
32
|
+
file(path, "read");
|
|
33
|
+
if (!readonly(options)) file(path, "write");
|
|
34
|
+
};
|
|
35
|
+
const proto = Original.prototype;
|
|
36
|
+
const statement = (node ? sqlite.StatementSync : sqlite.Statement)?.prototype;
|
|
37
|
+
for (const method of ["run", "get", "all", "iterate", "values"]) replaceMethod(statement, method, (original) => function (...args) {
|
|
38
|
+
check(() => ({ granted: true }));
|
|
39
|
+
const result = original.apply(this, args);
|
|
40
|
+
if (method === "iterate") replaceMethod(result, "next", (next) => function (...parameters) {
|
|
41
|
+
check(() => ({ granted: true }));
|
|
42
|
+
return next.apply(this, parameters);
|
|
43
|
+
});
|
|
44
|
+
return result;
|
|
45
|
+
});
|
|
46
|
+
if (node) {
|
|
47
|
+
const setAuthorizer = proto.setAuthorizer;
|
|
48
|
+
replaceMethod(proto, "setAuthorizer", () => function (callback) {
|
|
49
|
+
return setAuthorizer.call(this, (operation, ...args) => {
|
|
50
|
+
if (operation === sqlite.constants.SQLITE_ATTACH) refuse();
|
|
51
|
+
return callback?.(operation, ...args) ?? sqlite.constants.SQLITE_OK;
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
for (const method of ["exec", "run", "prepare", "query"]) replaceMethod(proto, method, (original) => function (sql, ...args) {
|
|
56
|
+
// Also guards runtimes which translate SQLite authorizer throws to SQLITE_AUTH.
|
|
57
|
+
guardSql(sql);
|
|
58
|
+
return original.call(this, sql, ...args);
|
|
59
|
+
});
|
|
60
|
+
replaceMethod(proto, "enableLoadExtension", (original) => function (enabled) {
|
|
61
|
+
if (enabled) refuse();
|
|
62
|
+
return original.call(this, enabled);
|
|
63
|
+
});
|
|
64
|
+
replaceMethod(proto, "loadExtension", (original) => function (...args) {
|
|
65
|
+
refuse();
|
|
66
|
+
return original.apply(this, args);
|
|
67
|
+
});
|
|
68
|
+
sqlite[name] = new Proxy(Original, {
|
|
69
|
+
construct(target, args, newTarget) {
|
|
70
|
+
const [path = ":memory:", options] = args;
|
|
71
|
+
opening(path, options);
|
|
72
|
+
const actual = memory(path) ? path : executionPath(path);
|
|
73
|
+
const db = Reflect.construct(target, [actual, ...args.slice(1)], newTarget);
|
|
74
|
+
paths.set(db, path);
|
|
75
|
+
if (node && typeof db.setAuthorizer === "function") db.setAuthorizer(null);
|
|
76
|
+
return db;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (sqlite.default === Original) sqlite.default = sqlite[name];
|
|
80
|
+
if (!node) {
|
|
81
|
+
replaceMethod(Original, "open", (original) => function (path, options) {
|
|
82
|
+
opening(path, options);
|
|
83
|
+
const db = original.call(this, memory(path) ? path : executionPath(path), options);
|
|
84
|
+
paths.set(db, path);
|
|
85
|
+
return db;
|
|
86
|
+
});
|
|
87
|
+
replaceMethod(Original, "setCustomSQLite", (original) => function (...args) {
|
|
88
|
+
refuse();
|
|
89
|
+
return original.apply(this, args);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (node) replaceMethod(sqlite, "backup", (original) => function (db, dest, ...args) {
|
|
93
|
+
file(paths.get(db), "read");
|
|
94
|
+
file(dest, "write");
|
|
95
|
+
return original.call(this, db, executionPath(dest), ...args);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const nodeSqlite = process.getBuiltinModule("node:sqlite");
|
|
99
|
+
install(nodeSqlite, "DatabaseSync", true);
|
|
100
|
+
if (globalThis.Bun) {
|
|
101
|
+
const require = process.getBuiltinModule("node:module").createRequire(import.meta.url);
|
|
102
|
+
install(require("bun:sqlite"), "Database", false);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// This module must be the first import in every worker. No builtin ESM namespace exists yet.
|
|
2
|
+
import { installFilesystemPermissions } from "./interpose-fs.js";
|
|
3
|
+
import { installProcessPermissions } from "./interpose-process.js";
|
|
4
|
+
import { installBunPermissions } from "./interpose-bun.js";
|
|
5
|
+
import { installSqlitePermissions } from "./interpose-sqlite.js";
|
|
6
|
+
import { installModulePermissions } from "./interpose-modules.js";
|
|
7
|
+
import { installCapabilityPermissions } from "./interpose-capabilities.js";
|
|
8
|
+
import { checkPath } from "./interpose-paths.js";
|
|
9
|
+
|
|
10
|
+
// Bootstrap is engine-owned; there is no user statement until this one-time handoff finishes.
|
|
11
|
+
let check = () => {};
|
|
12
|
+
let engineDepth = 0;
|
|
13
|
+
const authorize = (operation) => { if (engineDepth === 0) return check(operation); };
|
|
14
|
+
const fs = process.getBuiltinModule("node:fs");
|
|
15
|
+
const workerThreads = process.getBuiltinModule("node:worker_threads");
|
|
16
|
+
const bootstrap = workerThreads.workerData;
|
|
17
|
+
// Restore the public value before any builtin namespace or user preload observes it.
|
|
18
|
+
if (bootstrap.script) workerThreads.workerData = bootstrap.userWorkerData;
|
|
19
|
+
// Bun gives each worker one ESM namespace per builtin, copied from the module object at its first
|
|
20
|
+
// import and never refreshed, so the guarded constructor must be in place before that import. Only a
|
|
21
|
+
// user worker started under an active token is enforced against for its whole life, so only it gets
|
|
22
|
+
// the forwarder; every other context keeps the engine's own Worker on the module.
|
|
23
|
+
let guardedWorker;
|
|
24
|
+
let forwarder;
|
|
25
|
+
if (bootstrap.script && bootstrap.enforced) {
|
|
26
|
+
// A function, not a class: it hands back the instance the guarded class builds for new.target.
|
|
27
|
+
forwarder = function Worker(...args) {
|
|
28
|
+
if (!new.target) throw new TypeError("Class constructor Worker cannot be invoked without 'new'");
|
|
29
|
+
if (!guardedWorker) throw new Error("Worker is unavailable until the permission runtime starts");
|
|
30
|
+
return Reflect.construct(guardedWorker, args, new.target);
|
|
31
|
+
};
|
|
32
|
+
Object.setPrototypeOf(forwarder.prototype, EventTarget.prototype);
|
|
33
|
+
Object.defineProperty(workerThreads, "Worker", { value: forwarder, writable: false, configurable: false, enumerable: true });
|
|
34
|
+
}
|
|
35
|
+
const installGuardedWorker = (Guarded) => {
|
|
36
|
+
if (!forwarder) {
|
|
37
|
+
globalThis.Worker = Guarded;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
guardedWorker = Guarded;
|
|
41
|
+
Object.setPrototypeOf(forwarder.prototype, Guarded.prototype);
|
|
42
|
+
globalThis.Worker = forwarder;
|
|
43
|
+
};
|
|
44
|
+
const originals = { ...fs.promises, readFileSync: fs.readFileSync, realpathSync: fs.realpathSync };
|
|
45
|
+
const execFile = process.getBuiltinModule("node:child_process").execFile;
|
|
46
|
+
const cleanupExecFile = (...args) => {
|
|
47
|
+
engineDepth++;
|
|
48
|
+
try { return execFile(...args); }
|
|
49
|
+
finally { engineDepth--; }
|
|
50
|
+
};
|
|
51
|
+
const { dirname } = process.getBuiltinModule("node:path");
|
|
52
|
+
const permissionIO = {
|
|
53
|
+
read(raw, actual) { checkPath(authorize, "read", raw, { executed: actual }); return originals.readFile(actual, "utf8"); },
|
|
54
|
+
async write(raw, actual, data) {
|
|
55
|
+
checkPath(authorize, "write", raw, { executed: actual });
|
|
56
|
+
await originals.mkdir(dirname(actual), { recursive: true });
|
|
57
|
+
await originals.writeFile(actual, data);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const resources = installFilesystemPermissions(authorize);
|
|
61
|
+
installProcessPermissions(authorize, installBunPermissions(authorize, resources));
|
|
62
|
+
installSqlitePermissions(authorize);
|
|
63
|
+
const sealModules = installModulePermissions(authorize, Boolean(bootstrap.script));
|
|
64
|
+
|
|
65
|
+
// The sole bootstrap handoff is consumed before user code. Neither module exports the originals.
|
|
66
|
+
const { initializePermissionRuntime } = await import("./permission-runtime.js");
|
|
67
|
+
let runtimeAuthorize;
|
|
68
|
+
let activated = false;
|
|
69
|
+
const activate = () => {
|
|
70
|
+
if (activated) return;
|
|
71
|
+
activated = true;
|
|
72
|
+
check = runtimeAuthorize;
|
|
73
|
+
sealModules();
|
|
74
|
+
installCapabilityPermissions(authorize);
|
|
75
|
+
};
|
|
76
|
+
// The worker bootstrap and the engine hooks are separate inputs: the bootstrap carries the
|
|
77
|
+
// permission transport this worker must consume and delete, the engine carries the authorized IO
|
|
78
|
+
// the runtime hands to the cell prelude. Collapsing them into one argument drops one of the two.
|
|
79
|
+
await initializePermissionRuntime((authorize) => { runtimeAuthorize = authorize; }, bootstrap, {
|
|
80
|
+
permissionIO,
|
|
81
|
+
resolverIO: originals,
|
|
82
|
+
cleanupExecFile,
|
|
83
|
+
activate,
|
|
84
|
+
installGuardedWorker,
|
|
85
|
+
});
|
|
86
|
+
activate();
|