@code-yeongyu/senpi-codemode 2026.10.27-1 → 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.
Files changed (65) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/package.json +4 -4
  3. package/src/bridge/http-server.ts +18 -5
  4. package/src/bridge/permission-check.ts +76 -0
  5. package/src/bridge/protocol.ts +7 -0
  6. package/src/kernels/jl/kernel.ts +3 -0
  7. package/src/kernels/js/context-manager.ts +26 -8
  8. package/src/kernels/js/inline-worker-entry.js +1 -23
  9. package/src/kernels/js/inline-worker.ts +7 -2
  10. package/src/kernels/js/interpose-bun.js +220 -0
  11. package/src/kernels/js/interpose-capabilities.js +29 -0
  12. package/src/kernels/js/interpose-code.js +8 -0
  13. package/src/kernels/js/interpose-fs.js +124 -0
  14. package/src/kernels/js/interpose-modules.js +107 -0
  15. package/src/kernels/js/interpose-paths.js +80 -0
  16. package/src/kernels/js/interpose-process.js +97 -0
  17. package/src/kernels/js/interpose-resources.js +59 -0
  18. package/src/kernels/js/interpose-sqlite.js +104 -0
  19. package/src/kernels/js/interpose.js +86 -0
  20. package/src/kernels/js/kernel-contract.ts +31 -0
  21. package/src/kernels/js/kernel-tools-pump.js +4 -2
  22. package/src/kernels/js/local-module-loader.ts +9 -4
  23. package/src/kernels/js/permission-authority.d.mts +10 -0
  24. package/src/kernels/js/permission-authority.mjs +11 -0
  25. package/src/kernels/js/permission-core.md +114 -0
  26. package/src/kernels/js/permission-host.ts +388 -0
  27. package/src/kernels/js/permission-runtime.js +205 -0
  28. package/src/kernels/js/permission-sessions.ts +46 -0
  29. package/src/kernels/js/permission-slots.d.mts +51 -0
  30. package/src/kernels/js/permission-slots.mjs +133 -0
  31. package/src/kernels/js/permission-worker-entry.js +1 -0
  32. package/src/kernels/js/permission-worker-frames.ts +55 -0
  33. package/src/kernels/js/process-tree.js +9 -1
  34. package/src/kernels/js/rewrite-imports.ts +44 -0
  35. package/src/kernels/js/windows-tree-kill.js +10 -2
  36. package/src/kernels/js/worker-core.d.ts +2 -0
  37. package/src/kernels/js/worker-core.js +5 -2
  38. package/src/kernels/js/worker-cwd.js +14 -8
  39. package/src/kernels/js/worker-entry.js +1 -23
  40. package/src/kernels/js/worker-file-helpers.js +58 -0
  41. package/src/kernels/js/worker-host.ts +24 -6
  42. package/src/kernels/js/worker-package-resolve.js +10 -2
  43. package/src/kernels/js/worker-runtime.js +5 -65
  44. package/src/kernels/js/worker-slot.ts +5 -1
  45. package/src/kernels/js/worker-startup.ts +7 -5
  46. package/src/kernels/js/worker-webview.js +17 -16
  47. package/src/kernels/py/interpose.py +765 -0
  48. package/src/kernels/py/kernel-contract.ts +4 -0
  49. package/src/kernels/py/kernel-tools-host.ts +5 -1
  50. package/src/kernels/py/kernel.ts +45 -5
  51. package/src/kernels/py/path_identity.py +162 -0
  52. package/src/kernels/py/path_identity_posix.py +74 -0
  53. package/src/kernels/py/path_identity_windows.py +93 -0
  54. package/src/kernels/py/permission-core.md +103 -0
  55. package/src/kernels/py/permission-host.ts +198 -0
  56. package/src/kernels/py/permission_bridge.py +145 -0
  57. package/src/kernels/py/prelude.py +40 -5
  58. package/src/kernels/py/transport.ts +15 -8
  59. package/src/kernels/rb/kernel.ts +3 -0
  60. package/src/kernels/shared/invocation-authority.ts +59 -0
  61. package/src/kernels/shared/kernel-tools-pump.ts +22 -1
  62. package/src/kernels/shared/path-identity.d.mts +42 -0
  63. package/src/kernels/shared/path-identity.mjs +149 -0
  64. package/src/kernels/shared/subprocess-contract.ts +2 -0
  65. package/src/kernels/shared/subprocess-kernel.ts +28 -0
@@ -2,6 +2,7 @@ import { Buffer } from "node:buffer";
2
2
  import { syncBuiltinESMExports } from "node:module";
3
3
  import { isAbsolute, resolve, sep } from "node:path";
4
4
  import { isMainThread } from "node:worker_threads";
5
+ import { FS_PERMISSION_PATHS } from "./interpose-fs.js";
5
6
 
6
7
  // A worker thread cannot chdir (process.chdir throws ERR_WORKER_UNSUPPORTED_OPERATION under Node
7
8
  // and Bun), so every relative path a cell used resolved against the host process directory: in a
@@ -73,19 +74,22 @@ export function installSessionCwd(cwd, options) {
73
74
  const path = process.getBuiltinModule("node:path");
74
75
  const resolveFromProcess = path.resolve;
75
76
  path.resolve = (...segments) => resolveFromProcess(root, ...segments);
76
- patchFs(process.getBuiltinModule("node:fs"), process.getBuiltinModule("node:fs/promises"), at, root);
77
+ patchFs(process.getBuiltinModule("node:fs"), process.getBuiltinModule("node:fs/promises"), at, root, options?.permissionInterposed);
77
78
  patchChildProcess(process.getBuiltinModule("node:child_process"), at, root);
78
- patchBun(globalThis.Bun, at, root);
79
+ patchBun(globalThis.Bun, at, root, options?.permissionInterposed);
79
80
  syncBuiltinESMExports();
80
81
  }
81
82
 
82
- function patchFs(fs, promises, at, root) {
83
+ function patchFs(fs, promises, at, root, permissionInterposed) {
83
84
  const promiseModules = [...new Set([fs.promises, promises])];
84
85
  for (const [name, indexes] of Object.entries(FS_PATH_ARGS)) {
86
+ if (permissionInterposed && FS_PERMISSION_PATHS.has(name)) continue;
85
87
  replace(fs, name, (fn) => resolvingArgs(fn, indexes, at));
86
- replace(fs, `${name}Sync`, (fn) => resolvingArgs(fn, indexes, at));
88
+ // The early wrapper must see the requested spelling before cwd resolution (#4309).
89
+ if (!(permissionInterposed && name === "writeFile")) replace(fs, `${name}Sync`, (fn) => resolvingArgs(fn, indexes, at));
87
90
  for (const target of promiseModules) replace(target, name, (fn) => resolvingArgs(fn, indexes, at));
88
91
  }
92
+ if (permissionInterposed) return;
89
93
  replace(fs.realpath, "native", (fn) => resolvingArgs(fn, [0], at));
90
94
  replace(fs.realpathSync, "native", (fn) => resolvingArgs(fn, [0], at));
91
95
  for (const target of [fs, ...promiseModules]) {
@@ -105,15 +109,17 @@ function patchChildProcess(childProcess, at, root) {
105
109
  for (const name of CHILD_PROCESS_COMMAND_ARGS) replace(childProcess, name, (fn) => withOptionsCwd(fn, () => 1, root));
106
110
  }
107
111
 
108
- function patchBun(bun, at, root) {
112
+ function patchBun(bun, at, root, permissionInterposed) {
109
113
  if (bun === null || typeof bun !== "object") return;
110
- replace(bun, "file", (fn) => resolvingArgs(fn, [0], at));
111
- replace(bun, "write", (fn) => resolvingArgs(fn, [0], at));
114
+ if (!permissionInterposed) {
115
+ replace(bun, "file", (fn) => resolvingArgs(fn, [0], at));
116
+ replace(bun, "write", (fn) => resolvingArgs(fn, [0], at));
117
+ }
112
118
  replace(bun, "spawn", (fn) => withBunSpawnCwd(fn, root));
113
119
  replace(bun, "spawnSync", (fn) => withBunSpawnCwd(fn, root, false));
114
120
  if (typeof bun.$?.cwd === "function") bun.$.cwd(root);
115
121
  const glob = bun.Glob?.prototype;
116
- if (glob) {
122
+ if (glob && !permissionInterposed) {
117
123
  for (const name of ["scan", "scanSync"]) {
118
124
  replace(glob, name, (fn) =>
119
125
  function scan(options) {
@@ -1,23 +1 @@
1
- import { parentPort, workerData } from "node:worker_threads";
2
- import { createWorkerCore } from "./worker-core.js";
3
-
4
- if (!parentPort) throw new Error("JS kernel worker missing parentPort");
5
-
6
- const transport = {
7
- send(message) {
8
- parentPort.postMessage(message);
9
- },
10
- onMessage(handler) {
11
- parentPort.on("message", handler);
12
- return () => parentPort.off("message", handler);
13
- },
14
- close() {
15
- parentPort.close();
16
- setTimeout(() => process.exit(0), 0);
17
- },
18
- };
19
-
20
- createWorkerCore(transport, {
21
- cwd: workerData.cwd,
22
- parallelPoolWidth: workerData.parallelPoolWidth,
23
- });
1
+ import "./interpose.js";
@@ -0,0 +1,58 @@
1
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, normalize, resolve, sep } from "node:path";
3
+
4
+ export function createFileHelpers(options, emit) {
5
+ const roots = { ...options.localRoots };
6
+ if (options.artifactsDir && !roots.local) roots.local = join(options.artifactsDir, "local");
7
+ const resolvePath = (raw, operation) => {
8
+ const match = /^([a-z][a-z0-9+.-]*):\/\/(.*)$/iu.exec(raw);
9
+ if (!match) return isAbsolute(raw) ? normalize(raw) : resolve(options.cwd, raw);
10
+ const scheme = match[1].toLowerCase();
11
+ const root = roots[scheme];
12
+ if (!root) throw new Error(`Protocol paths are not supported by ${operation}(): ${raw}`);
13
+ let relative;
14
+ try { relative = decodeURIComponent(match[2].replaceAll("\\", "/")); }
15
+ catch (error) {
16
+ if (error instanceof URIError) throw new Error(`Invalid URL encoding in ${scheme}:// path: ${raw}`);
17
+ throw error;
18
+ }
19
+ if (isAbsolute(relative) || relative.split("/").includes("..")) throw new Error(`Path traversal is not allowed in ${scheme}:// URLs: ${raw}`);
20
+ const rootPath = resolve(root);
21
+ const path = resolve(rootPath, relative);
22
+ if (path !== rootPath && !path.startsWith(`${rootPath}${sep}`)) throw new Error(`${scheme}:// path escapes its root`);
23
+ return path;
24
+ };
25
+ return {
26
+ async read(rawPath, settings) {
27
+ const path = resolvePath(String(rawPath), "read");
28
+ const info = await stat(path);
29
+ if (info.isDirectory()) throw new Error(`Directory paths are not supported by read(): ${path}`);
30
+ let text = options.permissionIO
31
+ ? await options.permissionIO.read(String(rawPath), path) : await readFile(path, "utf8");
32
+ const offset = typeof settings.offset === "number" ? settings.offset : 1;
33
+ const limit = typeof settings.limit === "number" ? settings.limit : undefined;
34
+ if (offset > 1 || limit !== undefined) {
35
+ const start = Math.max(0, offset - 1);
36
+ text = text.split(/\r?\n/u).slice(start, limit === undefined ? undefined : start + limit).join("\n");
37
+ }
38
+ emit({ op: "read", path, bytes: info.size, chars: text.length });
39
+ return text;
40
+ },
41
+ async write(rawPath, content) {
42
+ const path = resolvePath(String(rawPath), "write");
43
+ const data = await writeData(content);
44
+ if (options.permissionIO) await options.permissionIO.write(String(rawPath), path, data);
45
+ else { await mkdir(dirname(path), { recursive: true }); await writeFile(path, data); }
46
+ emit({ op: "write", path, bytes: typeof data === "string" ? Buffer.byteLength(data) : data.byteLength });
47
+ return path;
48
+ },
49
+ };
50
+ }
51
+
52
+ async function writeData(value) {
53
+ if (typeof value === "string" || value instanceof Uint8Array) return value;
54
+ if (value instanceof Blob) return new Uint8Array(await value.arrayBuffer());
55
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
56
+ if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
57
+ throw new TypeError("write() expects string, Blob, ArrayBuffer, or TypedArray data");
58
+ }
@@ -4,6 +4,7 @@ import type { KernelToHostMessage } from "../../bridge/protocol.ts";
4
4
  import { describeExit } from "../shared/kernel-death.ts";
5
5
  import type { WorkerLike } from "./inline-worker.ts";
6
6
  import type { JavaScriptKernelMode } from "./kernel-contract.ts";
7
+ import type { KernelPermissionHost } from "./permission-host.ts";
7
8
 
8
9
  declare global {
9
10
  // Inert outside the gate. Bun does not refresh named builtin constructor exports.
@@ -41,10 +42,16 @@ export function spawnNodeWorker(
41
42
  cwd: string,
42
43
  parallelPoolWidth: number,
43
44
  mode: JavaScriptKernelMode = "worker",
45
+ permissions?: KernelPermissionHost,
44
46
  ): WorkerLike {
45
- const worker = new Worker(url, { workerData: { cwd, parallelPoolWidth } });
47
+ const permission = permissions?.bootstrap();
48
+ const worker = new Worker(url, {
49
+ workerData: { cwd, parallelPoolWidth, permission },
50
+ transferList: permission ? [permission.port] : [],
51
+ });
52
+ const exited = new Promise<void>((resolve) => worker.once("exit", () => resolve()));
46
53
  globalThis.__senpiCodemodeGateObserveResource?.("workers", worker, "exit");
47
- return wrapNodeWorker(worker, mode);
54
+ return wrapNodeWorker(worker, mode, exited, permissions);
48
55
  }
49
56
 
50
57
  export function waitForReady(worker: WorkerLike, signal: AbortSignal): Promise<void> {
@@ -106,13 +113,21 @@ export function crashedResult(
106
113
  return { type: "result", cellId, ok: false, error: bridgeError(error), durationMs };
107
114
  }
108
115
 
109
- function wrapNodeWorker(worker: Worker, mode: JavaScriptKernelMode): WorkerLike {
116
+ function wrapNodeWorker(
117
+ worker: Worker,
118
+ mode: JavaScriptKernelMode,
119
+ exited: Promise<void>,
120
+ permissions?: KernelPermissionHost,
121
+ ): WorkerLike {
110
122
  return {
111
123
  mode,
112
124
  postMessage: (message, transfer) =>
113
- worker.postMessage(message, transfer === undefined ? undefined : [...transfer]),
125
+ worker.postMessage(permissions?.frame(message) ?? message, transfer === undefined ? undefined : [...transfer]),
114
126
  onMessage(handler) {
115
- const listener = (message: KernelToHostMessage): void => handler(message);
127
+ const listener = (message: KernelToHostMessage): void => {
128
+ permissions?.settle(message);
129
+ handler(message);
130
+ };
116
131
  worker.on("message", listener);
117
132
  return () => worker.off("message", listener);
118
133
  },
@@ -133,7 +148,10 @@ function wrapNodeWorker(worker: Worker, mode: JavaScriptKernelMode): WorkerLike
133
148
  };
134
149
  },
135
150
  async terminate() {
136
- await worker.terminate();
151
+ permissions?.revoke();
152
+ const children = permissions?.terminateChildren();
153
+ void worker.terminate();
154
+ await Promise.all([exited, children]);
137
155
  },
138
156
  };
139
157
  }
@@ -9,6 +9,14 @@ import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  // conditions), so ESM-only packages load and the first root that has the package wins.
10
10
  const RESOLVE_PACKAGE = Symbol.for("senpi.kernel.resolvePackage");
11
11
  const CONDITIONS = typeof globalThis.Bun === "object" ? ["bun", "node", "import", "default"] : ["node", "import", "default"];
12
+ let resolverIO = { readFileSync, realpathSync };
13
+ let initializedIO = false;
14
+
15
+ export function initializeResolverIO(io) {
16
+ if (initializedIO) throw new Error("Resolver IO already initialized");
17
+ initializedIO = true;
18
+ resolverIO = { readFileSync: io.readFileSync, realpathSync: io.realpathSync };
19
+ }
12
20
 
13
21
  function isFile(path) {
14
22
  try {
@@ -93,7 +101,7 @@ function legacyFile(path) {
93
101
  function packageEntry(packageDir, subpath, specifier) {
94
102
  let manifest;
95
103
  try {
96
- manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
104
+ manifest = JSON.parse(resolverIO.readFileSync(join(packageDir, "package.json"), "utf8"));
97
105
  } catch {
98
106
  return legacyFile(join(packageDir, subpath === "." ? "index.js" : subpath));
99
107
  }
@@ -139,7 +147,7 @@ export function resolvePackage(specifier, rootUrls) {
139
147
  if (entry === undefined) continue;
140
148
  let real = entry;
141
149
  try {
142
- real = realpathSync(entry);
150
+ real = resolverIO.realpathSync(entry);
143
151
  } catch {
144
152
  // A missing target fails in the import itself, with the runtime's own error.
145
153
  }
@@ -1,8 +1,7 @@
1
1
  // allow: SIZE_OK — private runtime state and installed globals must stay in one worker module.
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { createCellRequire, createRequire } from "./worker-require.js";
4
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
5
- import { dirname, isAbsolute, join, normalize, resolve, sep } from "node:path";
4
+ import { createFileHelpers } from "./worker-file-helpers.js";
6
5
  import { inspect } from "node:util";
7
6
  import { encodeDisplayImage, resolveDisplayOps } from "./display-image.js";
8
7
  import { terminateProcessGroups, terminateProcessTrees } from "./process-tree.js";
@@ -24,12 +23,10 @@ const PRELUDE_SOURCE_URL = "senpi:kernel-prelude";
24
23
  // How long a child gets to honour SIGTERM before SIGKILL. Short, because the
25
24
  // cell has already produced its value and the caller is waiting on settle.
26
25
  const CHILD_TERMINATION_GRACE_MS = 1_000;
27
- const INTERNAL_URL = /^([a-z][a-z0-9+.-]*):\/\/(.*)$/iu;
28
26
 
29
27
  export class JsWorkerRuntime {
30
- #cwd;
31
28
  #parallelPoolWidth;
32
- #localRoots;
29
+ #files;
33
30
  #env = new Map();
34
31
  #hooks = null;
35
32
  #pendingDisplays = [];
@@ -44,12 +41,10 @@ export class JsWorkerRuntime {
44
41
  #tools;
45
42
 
46
43
  constructor(options) {
47
- this.#cwd = options.cwd;
48
44
  this.#parallelPoolWidth = options.parallelPoolWidth;
49
45
  this.#onChildEvent = typeof options.onChildEvent === "function" ? options.onChildEvent : null;
50
46
  this.#onShellWaitChange = options.onShellWaitChange;
51
- this.#localRoots = { ...(options.localRoots ?? {}) };
52
- if (options.artifactsDir && !this.#localRoots.local) this.#localRoots.local = join(options.artifactsDir, "local");
47
+ this.#files = createFileHelpers(options, (event) => this.#emitStatus(event));
53
48
  this.#tools = createKernelToolRegistry({
54
49
  generation: options.kernelGeneration ?? 1,
55
50
  hostToolNames: options.hostToolNames ?? [],
@@ -166,8 +161,8 @@ export class JsWorkerRuntime {
166
161
  globalThis.log = message => this.#hooks?.emit({ type: "log", message: String(message) });
167
162
  globalThis.phase = title => this.#hooks?.emit({ type: "phase", title: String(title) });
168
163
  globalThis.env = (key, value) => this.#envHelper(key, value);
169
- globalThis.read = async (path, options, ...rest) => await this.#read(path, helperOptions("read", options, rest));
170
- globalThis.write = async (path, content) => await this.#write(path, content);
164
+ globalThis.read = async (path, options, ...rest) => await this.#files.read(path, helperOptions("read", options, rest));
165
+ globalThis.write = async (path, content) => await this.#files.write(path, content);
171
166
  globalThis.output = async (...args) => await this.#output(args);
172
167
  globalThis.tool_schema = async name => await this.#toolSchema(name);
173
168
  globalThis.agent = async (prompt, options, ...rest) => await this.#agent(prompt, options, rest);
@@ -297,53 +292,6 @@ export class JsWorkerRuntime {
297
292
  return result;
298
293
  }
299
294
 
300
- async #read(rawPath, options) {
301
- const path = this.#resolvePath(String(rawPath), "read");
302
- const info = await stat(path);
303
- if (info.isDirectory()) throw new Error(`Directory paths are not supported by read(): ${path}`);
304
- let text = await readFile(path, "utf8");
305
- const offset = typeof options.offset === "number" ? options.offset : 1;
306
- const limit = typeof options.limit === "number" ? options.limit : undefined;
307
- if (offset > 1 || limit !== undefined) {
308
- const lines = text.split(/\r?\n/u);
309
- const start = Math.max(0, offset - 1);
310
- text = lines.slice(start, limit === undefined ? undefined : start + limit).join("\n");
311
- }
312
- this.#emitStatus({ op: "read", path, bytes: info.size, chars: text.length });
313
- return text;
314
- }
315
-
316
- async #write(rawPath, content) {
317
- const path = this.#resolvePath(String(rawPath), "write");
318
- const data = await writeData(content);
319
- await mkdir(dirname(path), { recursive: true });
320
- await writeFile(path, data);
321
- this.#emitStatus({ op: "write", path, bytes: typeof data === "string" ? Buffer.byteLength(data) : data.byteLength });
322
- return path;
323
- }
324
-
325
- #resolvePath(rawPath, operation) {
326
- const match = INTERNAL_URL.exec(rawPath);
327
- if (!match) return isAbsolute(rawPath) ? normalize(rawPath) : resolve(this.#cwd, rawPath);
328
- const scheme = match[1].toLowerCase();
329
- const root = this.#localRoots[scheme];
330
- if (!root) throw new Error(`Protocol paths are not supported by ${operation}(): ${rawPath}`);
331
- let relative;
332
- try {
333
- relative = decodeURIComponent(match[2].replaceAll("\\", "/"));
334
- } catch (error) {
335
- if (error instanceof URIError) throw new Error(`Invalid URL encoding in ${scheme}:// path: ${rawPath}`);
336
- throw error;
337
- }
338
- if (isAbsolute(relative) || relative.split("/").includes("..")) {
339
- throw new Error(`Path traversal is not allowed in ${scheme}:// URLs: ${rawPath}`);
340
- }
341
- const rootPath = resolve(root);
342
- const path = resolve(rootPath, relative);
343
- if (path !== rootPath && !path.startsWith(`${rootPath}${sep}`)) throw new Error(`${scheme}:// path escapes its root`);
344
- return path;
345
- }
346
-
347
295
  async #output(args) {
348
296
  let ids = args;
349
297
  let options = {};
@@ -502,14 +450,6 @@ function reservedTool(globalName, helperName) {
502
450
  return value;
503
451
  }
504
452
 
505
- async function writeData(value) {
506
- if (typeof value === "string" || value instanceof Uint8Array) return value;
507
- if (value instanceof Blob) return new Uint8Array(await value.arrayBuffer());
508
- if (value instanceof ArrayBuffer) return new Uint8Array(value);
509
- if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
510
- throw new TypeError("write() expects string, Blob, ArrayBuffer, or TypedArray data");
511
- }
512
-
513
453
  function chunkToString(chunk, encoding) {
514
454
  if (typeof chunk === "string") return chunk;
515
455
  if (chunk instanceof Uint8Array) return Buffer.from(chunk).toString(encoding ?? "utf8");
@@ -3,6 +3,7 @@ import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/prot
3
3
  import type { WorkerLike } from "./inline-worker.ts";
4
4
  import { retireWorker, type WorkerRetirement } from "./interrupt-bounds.ts";
5
5
  import type { JavaScriptKernelMode } from "./kernel-contract.ts";
6
+ import type { KernelPermissionHost } from "./permission-host.ts";
6
7
  import type { JavaScriptKernelOptions } from "./local-module-loader.ts";
7
8
  import { KernelWebViewClients } from "./webview-host.ts";
8
9
  import { WorkerStartupCancelledError } from "./worker-host.ts";
@@ -18,6 +19,7 @@ export interface WorkerSlotListeners {
18
19
  export class WorkerSlot {
19
20
  readonly #options: JavaScriptKernelOptions;
20
21
  readonly #listeners: WorkerSlotListeners;
22
+ readonly #permissions?: KernelPermissionHost;
21
23
  #worker: WorkerLike | null = null;
22
24
  #lastProcessPid: number | undefined;
23
25
  #webViews: KernelWebViewClients | null = null;
@@ -26,9 +28,10 @@ export class WorkerSlot {
26
28
  #ready: Promise<void> | null = null;
27
29
  #startupAbort: AbortController | null = null;
28
30
 
29
- constructor(options: JavaScriptKernelOptions, listeners: WorkerSlotListeners) {
31
+ constructor(options: JavaScriptKernelOptions, listeners: WorkerSlotListeners, permissions?: KernelPermissionHost) {
30
32
  this.#options = options;
31
33
  this.#listeners = listeners;
34
+ this.#permissions = permissions;
32
35
  }
33
36
 
34
37
  get mode(): JavaScriptKernelMode {
@@ -65,6 +68,7 @@ export class WorkerSlot {
65
68
  const ready = startWorkerWithInlineFallback(
66
69
  {
67
70
  options: this.#options,
71
+ permissions: this.#permissions,
68
72
  kernelGeneration: generation,
69
73
  publish: (worker) => this.#publish(worker, generation),
70
74
  isCurrent: (worker) => this.#isCurrent(worker, generation),
@@ -4,6 +4,7 @@ import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } fro
4
4
  import { createInlineWorker, type WorkerLike } from "./inline-worker.ts";
5
5
  import { retireWorker } from "./interrupt-bounds.ts";
6
6
  import { resolveKernelToolNameSource } from "./kernel-contract.ts";
7
+ import type { KernelPermissionHost } from "./permission-host.ts";
7
8
  import { type JavaScriptKernelOptions, localBridgeConnection } from "./local-module-loader.ts";
8
9
  import { resolveJsProcessEntryUrl, spawnProcessWorker } from "./process-worker.ts";
9
10
  import { spawnNodeWorker, WorkerStartupCancelledError, waitForReady } from "./worker-host.ts";
@@ -35,6 +36,7 @@ export class ProcessKernelStartupTimeoutError extends Error {
35
36
  export interface WorkerStartupHooks {
36
37
  readonly options: JavaScriptKernelOptions;
37
38
  readonly kernelGeneration: number;
39
+ readonly permissions?: KernelPermissionHost;
38
40
  /** Wires the worker into the kernel; throws `WorkerStartupCancelledError` once the generation is stale. */
39
41
  publish(worker: WorkerLike): void;
40
42
  isCurrent(worker: WorkerLike): boolean;
@@ -43,7 +45,7 @@ export interface WorkerStartupHooks {
43
45
  }
44
46
 
45
47
  export async function startWorkerWithInlineFallback(hooks: WorkerStartupHooks, signal: AbortSignal): Promise<void> {
46
- let worker = spawnWorker(hooks.options);
48
+ let worker = spawnWorker(hooks.options, hooks.permissions);
47
49
  hooks.publish(worker);
48
50
  try {
49
51
  await initializeWorker(worker, hooks, signal);
@@ -65,12 +67,12 @@ export async function startWorkerWithInlineFallback(hooks: WorkerStartupHooks, s
65
67
  await worker.terminate();
66
68
  }
67
69
  if (!hooks.canFallBackInline()) throw new WorkerStartupCancelledError();
68
- worker = createInlineWorker(hooks.options.cwd, hooks.options.parallelPoolWidth);
70
+ worker = createInlineWorker(hooks.options.cwd, hooks.options.parallelPoolWidth, hooks.permissions);
69
71
  hooks.publish(worker);
70
72
  await initializeWorker(worker, hooks, signal);
71
73
  }
72
74
 
73
- function spawnWorker(options: JavaScriptKernelOptions): WorkerLike {
75
+ function spawnWorker(options: JavaScriptKernelOptions, permissions?: KernelPermissionHost): WorkerLike {
74
76
  if (options.isolation === "process") {
75
77
  const url = resolveJsProcessEntryUrl();
76
78
  return spawnProcessWorker(url, {
@@ -82,10 +84,10 @@ function spawnWorker(options: JavaScriptKernelOptions): WorkerLike {
82
84
  }
83
85
  try {
84
86
  const url = options.workerEntryUrl ?? resolveJsWorkerEntryUrl();
85
- return spawnNodeWorker(url, options.cwd, options.parallelPoolWidth);
87
+ return spawnNodeWorker(url, options.cwd, options.parallelPoolWidth, "worker", permissions);
86
88
  } catch (error) {
87
89
  if (!(error instanceof Error)) throw error;
88
- return createInlineWorker(options.cwd, options.parallelPoolWidth);
90
+ return createInlineWorker(options.cwd, options.parallelPoolWidth, permissions);
89
91
  }
90
92
  }
91
93
 
@@ -1,10 +1,11 @@
1
1
  import { onCellRelease } from "./cell-run-context.js";
2
2
  import { DELIVER_EVENT, WebViewPortClient, webViewError } from "./worker-webview-client.js";
3
3
 
4
- // Bun constructs "chrome"-backend WebViews only on the process main thread, and this kernel runs in
5
- // a worker where `globalThis.Bun` and `Bun.WebView` are non-writable. Cells therefore run with a
6
- // `Bun` parameter bound to a stand-in whose `WebView` forwards Chrome-backed views to the
7
- // main-thread service over a private port; every other `Bun.*` member is the real one.
4
+ // Bun's WebViews are only safe on the process main thread (Chrome is constructed nowhere else, and
5
+ // the WebKit host client is bound to one thread), and this kernel runs in a worker where
6
+ // `globalThis.Bun` and `Bun.WebView` are non-writable. Cells therefore run with a `Bun` parameter
7
+ // bound to a stand-in whose `WebView` forwards every view to the main-thread service over a private
8
+ // port; every other `Bun.*` member is the real one.
8
9
  const KERNEL_BUN = Symbol.for("senpi.kernel.bun");
9
10
  const KERNEL_PROCESS_MODE = Symbol.for("senpi.kernel.processMode");
10
11
 
@@ -27,7 +28,8 @@ function shadowBun(real, WebView) {
27
28
  return new Proxy(
28
29
  {},
29
30
  {
30
- get: (_target, key) => (key === "WebView" ? WebView : Reflect.get(real, key)),
31
+ get: (_target, key) => key === "WebView" ? WebView
32
+ : key === "plugin" ? globalThis[Symbol.for("senpi.kernel.permissionBunPlugin")] ?? real.plugin : Reflect.get(real, key),
31
33
  set: (_target, key, value) => Reflect.set(real, key, value),
32
34
  has: (_target, key) => key === "WebView" || Reflect.has(real, key),
33
35
  ownKeys: () => Reflect.ownKeys(real),
@@ -40,14 +42,13 @@ function shadowBun(real, WebView) {
40
42
  );
41
43
  }
42
44
 
43
- // The macOS default (WebKit) stays a native worker view: its host process serves any thread.
44
- // A process-mode kernel already runs its cells on the child's main thread, so the native
45
- // WebView constructs there directly and no MessagePort proxy is ever needed.
46
- function needsMainThread(options) {
47
- if (globalThis[KERNEL_PROCESS_MODE]) return false;
48
- const backend = options?.backend;
49
- if (backend === undefined) return process.platform !== "darwin";
50
- return backend === "chrome" || (typeof backend === "object" && backend !== null && backend.type === "chrome");
45
+ // Every backend goes to the main thread, WebKit included (senpi#4794): Bun's WebKit host client is
46
+ // one per process, bound to the thread that first spawned it, and it settles every view's replies
47
+ // in that thread's heap. A second kernel worker's views then corrupt the process heap, and once the
48
+ // binding worker exits no later view can spawn the host. A process-mode kernel already runs its
49
+ // cells on the child's main thread, so the native WebView constructs there directly.
50
+ function needsMainThread() {
51
+ return !globalThis[KERNEL_PROCESS_MODE];
51
52
  }
52
53
 
53
54
  function closedError(method) {
@@ -96,7 +97,7 @@ function createKernelWebView(NativeWebView, requestPort) {
96
97
  onNavigationFailed = null;
97
98
 
98
99
  constructor(options = {}) {
99
- if (!needsMainThread(options)) {
100
+ if (!needsMainThread()) {
100
101
  const view = new NativeWebView(options);
101
102
  const ref = new WeakRef(view);
102
103
  natives.add(ref);
@@ -141,8 +142,8 @@ function createKernelWebView(NativeWebView, requestPort) {
141
142
  scroll(dx, dy) { return this.#call("scroll", [dx, dy]); }
142
143
  scrollTo(...args) { return this.#call("scrollTo", args); }
143
144
  resize(width, height) { return this.#call("resize", [width, height]); }
144
- back() { return this.#call("back", []); }
145
- forward() { return this.#call("forward", []); }
145
+ goBack() { return this.#call("goBack", []); }
146
+ goForward() { return this.#call("goForward", []); }
146
147
  reload() { return this.#call("reload", []); }
147
148
  cdp(...args) { return this.#call("cdp", args); }
148
149