@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.
- package/CHANGELOG.md +49 -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/group-signal-notice.js +277 -1
- 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
|
@@ -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 {
|
|
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
|
-
#
|
|
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.#
|
|
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
|
|
5
|
-
//
|
|
6
|
-
// `Bun`
|
|
7
|
-
//
|
|
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) =>
|
|
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
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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(
|
|
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
|
-
|
|
145
|
-
|
|
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
|
|