@code-yeongyu/senpi-codemode 2026.10.27-2 → 2026.10.30-2

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 (66) hide show
  1. package/CHANGELOG.md +49 -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/group-signal-notice.js +277 -1
  9. package/src/kernels/js/inline-worker-entry.js +1 -23
  10. package/src/kernels/js/inline-worker.ts +7 -2
  11. package/src/kernels/js/interpose-bun.js +220 -0
  12. package/src/kernels/js/interpose-capabilities.js +29 -0
  13. package/src/kernels/js/interpose-code.js +8 -0
  14. package/src/kernels/js/interpose-fs.js +124 -0
  15. package/src/kernels/js/interpose-modules.js +107 -0
  16. package/src/kernels/js/interpose-paths.js +80 -0
  17. package/src/kernels/js/interpose-process.js +97 -0
  18. package/src/kernels/js/interpose-resources.js +59 -0
  19. package/src/kernels/js/interpose-sqlite.js +104 -0
  20. package/src/kernels/js/interpose.js +86 -0
  21. package/src/kernels/js/kernel-contract.ts +31 -0
  22. package/src/kernels/js/kernel-tools-pump.js +4 -2
  23. package/src/kernels/js/local-module-loader.ts +9 -4
  24. package/src/kernels/js/permission-authority.d.mts +10 -0
  25. package/src/kernels/js/permission-authority.mjs +11 -0
  26. package/src/kernels/js/permission-core.md +114 -0
  27. package/src/kernels/js/permission-host.ts +388 -0
  28. package/src/kernels/js/permission-runtime.js +205 -0
  29. package/src/kernels/js/permission-sessions.ts +46 -0
  30. package/src/kernels/js/permission-slots.d.mts +51 -0
  31. package/src/kernels/js/permission-slots.mjs +133 -0
  32. package/src/kernels/js/permission-worker-entry.js +1 -0
  33. package/src/kernels/js/permission-worker-frames.ts +55 -0
  34. package/src/kernels/js/process-tree.js +9 -1
  35. package/src/kernels/js/rewrite-imports.ts +44 -0
  36. package/src/kernels/js/windows-tree-kill.js +10 -2
  37. package/src/kernels/js/worker-core.d.ts +2 -0
  38. package/src/kernels/js/worker-core.js +5 -2
  39. package/src/kernels/js/worker-cwd.js +14 -8
  40. package/src/kernels/js/worker-entry.js +1 -23
  41. package/src/kernels/js/worker-file-helpers.js +58 -0
  42. package/src/kernels/js/worker-host.ts +24 -6
  43. package/src/kernels/js/worker-package-resolve.js +10 -2
  44. package/src/kernels/js/worker-runtime.js +5 -65
  45. package/src/kernels/js/worker-slot.ts +5 -1
  46. package/src/kernels/js/worker-startup.ts +7 -5
  47. package/src/kernels/js/worker-webview.js +17 -16
  48. package/src/kernels/py/interpose.py +765 -0
  49. package/src/kernels/py/kernel-contract.ts +4 -0
  50. package/src/kernels/py/kernel-tools-host.ts +5 -1
  51. package/src/kernels/py/kernel.ts +45 -5
  52. package/src/kernels/py/path_identity.py +162 -0
  53. package/src/kernels/py/path_identity_posix.py +74 -0
  54. package/src/kernels/py/path_identity_windows.py +93 -0
  55. package/src/kernels/py/permission-core.md +103 -0
  56. package/src/kernels/py/permission-host.ts +198 -0
  57. package/src/kernels/py/permission_bridge.py +145 -0
  58. package/src/kernels/py/prelude.py +40 -5
  59. package/src/kernels/py/transport.ts +15 -8
  60. package/src/kernels/rb/kernel.ts +3 -0
  61. package/src/kernels/shared/invocation-authority.ts +59 -0
  62. package/src/kernels/shared/kernel-tools-pump.ts +22 -1
  63. package/src/kernels/shared/path-identity.d.mts +42 -0
  64. package/src/kernels/shared/path-identity.mjs +149 -0
  65. package/src/kernels/shared/subprocess-contract.ts +2 -0
  66. package/src/kernels/shared/subprocess-kernel.ts +28 -0
@@ -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