@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
@@ -0,0 +1,388 @@
1
+ import { MessageChannel, type MessagePort, type Transferable, Worker, type WorkerOptions } from "node:worker_threads";
2
+ import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
3
+ import {
4
+ invocationSession,
5
+ type InvocationSession,
6
+ registerInvocationSession,
7
+ } from "../shared/invocation-authority.ts";
8
+ import type { WorkerRetirement } from "./interrupt-bounds.ts";
9
+ import type { CellAuthorityToken, JavaScriptKernelOptions } from "./kernel-contract.ts";
10
+ import type { KernelToolsInvokeOptions, KernelToolsInvokeRequest } from "./kernel-tools-types.ts";
11
+ import { type PermissionSession, registerPermissionKernel, unregisterPermissionKernel } from "./permission-sessions.ts";
12
+ import { forwardWorkerMessage, workerErrorFrame } from "./permission-worker-frames.ts";
13
+ import {
14
+ createPermissionSlots,
15
+ interruptPermissionSlots,
16
+ type PermissionRequest,
17
+ servePermissionSlot,
18
+ } from "./permission-slots.mjs";
19
+
20
+ export { revokeSessionKernels } from "./permission-sessions.ts";
21
+
22
+ type WorkerCommand =
23
+ | { type: "check"; slot: number; generation: number }
24
+ | { type: "retire" }
25
+ | {
26
+ type: "spawn";
27
+ script: string;
28
+ token?: string;
29
+ port: MessagePort;
30
+ options: WorkerOptions & { preload?: string | string[] };
31
+ };
32
+
33
+ export class KernelPermissionHost {
34
+ readonly kernelId = crypto.randomUUID();
35
+ readonly #options: JavaScriptKernelOptions;
36
+ readonly #session: PermissionSession;
37
+ readonly #retire: () => Promise<WorkerRetirement>;
38
+ readonly #tokens = new Map<string, CellAuthorityToken>();
39
+ readonly #runs = new Map<string, CellAuthorityToken>();
40
+ readonly #invocations = new Map<string, CellAuthorityToken>();
41
+ readonly #invokerBindings = new Map<string, number>();
42
+ readonly #retained = new Map<string, Set<CellAuthorityToken>>();
43
+ readonly #pools = new Set<SharedArrayBuffer>();
44
+ readonly #ports = new Set<MessagePort>();
45
+ readonly #children = new Map<Worker, Promise<void>>();
46
+ readonly #releaseSession: () => void;
47
+ #kernel: CellAuthorityToken;
48
+ #revoked = false;
49
+ #retiring?: Promise<void>;
50
+
51
+ constructor(options: JavaScriptKernelOptions, retire: () => Promise<WorkerRetirement>) {
52
+ this.#options = options;
53
+ this.#retire = retire;
54
+ this.#kernel = this.#mint();
55
+ this.#session = registerPermissionKernel(options.sessionId, this);
56
+ this.#releaseSession = registerInvocationSession(options.sessionId, () => this.#snapshot());
57
+ }
58
+
59
+ get blocked(): boolean {
60
+ return this.#revoked || this.#session.dead || this.#session.pending !== undefined || this.#retiring !== undefined;
61
+ }
62
+
63
+ async admit(): Promise<void> {
64
+ await this.#session.pending;
65
+ await this.#retiring;
66
+ this.assertSessionLive();
67
+ }
68
+
69
+ assertSessionLive(): void {
70
+ if (this.#session.dead) throw new Error("Permission authority unavailable; restart the session");
71
+ }
72
+
73
+ #snapshot(): InvocationSession {
74
+ return {
75
+ cwd: this.#options.cwd,
76
+ state: this.#options.permissions?.state() ?? { epoch: 0, active: false, rules: [] },
77
+ };
78
+ }
79
+
80
+ #mint(
81
+ signal?: AbortSignal,
82
+ session = { sessionId: this.#options.sessionId, ...this.#snapshot() },
83
+ ): CellAuthorityToken {
84
+ const { state } = session;
85
+ if (!Number.isInteger(state.epoch) || state.epoch < 0 || state.epoch >= 2 ** 29)
86
+ throw new Error("Invalid permission epoch");
87
+ const authority = new SharedArrayBuffer(4);
88
+ Atomics.store(new Int32Array(authority), 0, (state.epoch << 2) | 2 | Number(state.active));
89
+ return {
90
+ id: crypto.randomUUID(),
91
+ sessionId: session.sessionId,
92
+ cwd: session.cwd,
93
+ authority,
94
+ rules: state.rules,
95
+ signal,
96
+ };
97
+ }
98
+
99
+ /** Resolve host-only caller identity before admitting any invocation frame to the worker. */
100
+ resolveInvocationToken(
101
+ _request: KernelToolsInvokeRequest,
102
+ options?: AbortSignal | KernelToolsInvokeOptions,
103
+ ): CellAuthorityToken {
104
+ const signal = options && ("aborted" in options ? options : options.signal);
105
+ const session = invocationSession(
106
+ this.#options.sessionId,
107
+ this.#snapshot(),
108
+ options,
109
+ this.#options.permissions?.resolveSession,
110
+ );
111
+ const token = this.#mint(signal, session);
112
+ if (session.sessionId !== this.#options.sessionId) {
113
+ const binding = registerPermissionKernel(session.sessionId, this);
114
+ if (binding.dead || binding.pending) {
115
+ if (!this.#invokerBindings.has(session.sessionId)) unregisterPermissionKernel(session.sessionId, this);
116
+ throw new Error("Permission authority unavailable");
117
+ }
118
+ this.#invokerBindings.set(session.sessionId, (this.#invokerBindings.get(session.sessionId) ?? 0) + 1);
119
+ }
120
+ this.#tokens.set(token.id, token);
121
+ return token;
122
+ }
123
+
124
+ invocation(request: KernelToolsInvokeRequest, options?: AbortSignal | KernelToolsInvokeOptions): () => void {
125
+ const token = this.resolveInvocationToken(request, options);
126
+ this.#invocations.set(request.call_id, token);
127
+ const cancel = () => this.#kill(token);
128
+ token.signal?.addEventListener("abort", cancel, { once: true });
129
+ if (token.signal?.aborted) cancel();
130
+ return () => {
131
+ token.signal?.removeEventListener("abort", cancel);
132
+ this.#invocations.delete(request.call_id);
133
+ this.#release(token);
134
+ };
135
+ }
136
+
137
+ frame(
138
+ message: HostToKernelMessage,
139
+ ): HostToKernelMessage & { authority?: ReturnType<KernelPermissionHost["wireToken"]> } {
140
+ let token: CellAuthorityToken | undefined;
141
+ if (message.type === "run") {
142
+ token = this.#mint();
143
+ this.#tokens.set(token.id, token);
144
+ this.#runs.set(message.cellId, token);
145
+ } else if (message.type === "kernel-tool-invoke") {
146
+ token = this.#invocations.get(message.call_id);
147
+ } else if (message.type === "interrupt" || message.type === "close") {
148
+ // An interrupt (including a timeout) ends work like settlement: inactive continuations survive.
149
+ for (const token of this.#tokens.values()) {
150
+ if (message.type === "interrupt") this.#release(token);
151
+ else this.#kill(token);
152
+ }
153
+ if (message.type === "close") this.#endRetained();
154
+ for (const pool of this.#pools) interruptPermissionSlots(pool);
155
+ }
156
+ return token ? { ...message, authority: this.wireToken(token) } : message;
157
+ }
158
+
159
+ settle(message: KernelToHostMessage): void {
160
+ if (message.type !== "result") return;
161
+ const token = this.#runs.get(message.cellId);
162
+ this.#runs.delete(message.cellId);
163
+ if (token) this.#release(token);
164
+ }
165
+
166
+ #release(token: CellAuthorityToken): void {
167
+ if (!this.#tokens.delete(token.id)) return;
168
+ // Settled no-deny work keeps its async resources; the kernel word still revokes them. A live
169
+ // inactive token is retained rather than forgotten, and keeps its invoker binding for as long as it
170
+ // is alive, so that session's revocation still reaches the continuation it left behind. Nothing
171
+ // reports when that continuation actually finishes, so the binding ends with the token, which is
172
+ // what closing the kernel does; it does not end earlier.
173
+ const word = Atomics.load(new Int32Array(token.authority), 0);
174
+ if (word & 2 && !(word & 1)) {
175
+ if (this.#invokerBindings.has(token.sessionId)) {
176
+ const held = this.#retained.get(token.sessionId) ?? new Set<CellAuthorityToken>();
177
+ held.add(token);
178
+ this.#retained.set(token.sessionId, held);
179
+ }
180
+ this.#sweepRetained(token.sessionId);
181
+ return;
182
+ }
183
+ this.#kill(token);
184
+ this.#unbind(token.sessionId);
185
+ }
186
+
187
+ /** A retained token holds its own binding and gives it back once that token is dead, not before. */
188
+ #sweepRetained(sessionId: string): void {
189
+ const held = this.#retained.get(sessionId);
190
+ if (!held) return;
191
+ for (const token of held) {
192
+ if (Atomics.load(new Int32Array(token.authority), 0) & 2) continue;
193
+ held.delete(token);
194
+ this.#unbind(sessionId);
195
+ }
196
+ if (held.size === 0) this.#retained.delete(sessionId);
197
+ }
198
+
199
+ /** Closing the kernel ends every continuation it was still holding open for an invoker. */
200
+ #endRetained(): void {
201
+ for (const [sessionId, held] of this.#retained) {
202
+ for (const token of held) this.#kill(token);
203
+ this.#sweepRetained(sessionId);
204
+ }
205
+ }
206
+
207
+ #unbind(sessionId: string): void {
208
+ const count = this.#invokerBindings.get(sessionId);
209
+ if (count === undefined) return;
210
+ if (count > 1) this.#invokerBindings.set(sessionId, count - 1);
211
+ else {
212
+ this.#invokerBindings.delete(sessionId);
213
+ unregisterPermissionKernel(sessionId, this);
214
+ }
215
+ }
216
+
217
+ /** Worker retirement ends every continuation, so no invoker revocation needs to reach this kernel. */
218
+ #unbindInvokers(): void {
219
+ for (const sessionId of this.#invokerBindings.keys()) unregisterPermissionKernel(sessionId, this);
220
+ this.#invokerBindings.clear();
221
+ this.#retained.clear();
222
+ }
223
+
224
+ #kill(token: CellAuthorityToken): void {
225
+ Atomics.and(new Int32Array(token.authority), 0, ~2);
226
+ }
227
+
228
+ wireToken(token: CellAuthorityToken) {
229
+ return { id: token.id, authority: token.authority, rules: token.rules };
230
+ }
231
+
232
+ bootstrap() {
233
+ if (this.#revoked) {
234
+ this.#kernel = this.#mint();
235
+ this.#revoked = false;
236
+ }
237
+ return this.#channel();
238
+ }
239
+
240
+ #channel(token?: CellAuthorityToken) {
241
+ const slots = createPermissionSlots();
242
+ const { port1, port2 } = new MessageChannel();
243
+ this.#pools.add(slots);
244
+ this.#ports.add(port1);
245
+ port1.on("message", (message: WorkerCommand) => {
246
+ if (message.type === "check") {
247
+ void servePermissionSlot(slots, message.slot, message.generation, (request) => this.#decide(request));
248
+ } else if (message.type === "retire") {
249
+ this.revoke();
250
+ void this.retire();
251
+ } else if (message.type === "spawn") {
252
+ this.#spawnChild(message);
253
+ }
254
+ });
255
+ port1.on("close", () => {
256
+ this.#pools.delete(slots);
257
+ this.#ports.delete(port1);
258
+ });
259
+ return {
260
+ port: port2,
261
+ slots,
262
+ kernel: this.wireToken(this.#kernel),
263
+ deadlineMs: this.#options.permissions?.checkDeadlineMs,
264
+ token: token && this.wireToken(token),
265
+ cwd: this.#options.cwd,
266
+ };
267
+ }
268
+
269
+ async #decide(request: PermissionRequest) {
270
+ const token = this.#tokens.get(request.token);
271
+ if (!token || this.#revoked || this.#session.dead || !(Atomics.load(new Int32Array(token.authority), 0) & 2)) {
272
+ throw new Error("Permission authority unavailable");
273
+ }
274
+ const decide = this.#options.permissions?.decide;
275
+ if (!decide) throw new Error("Cell decider unavailable");
276
+ const answer = await decide(token, request.call);
277
+ if (!(Atomics.load(new Int32Array(token.authority), 0) & 2)) throw new Error("Permission authority revoked");
278
+ return answer;
279
+ }
280
+
281
+ #spawnChild(message: Extract<WorkerCommand, { type: "spawn" }>): void {
282
+ const token = message.token ? this.#tokens.get(message.token) : undefined;
283
+ if (this.blocked || (message.token && !token)) {
284
+ message.port.close();
285
+ return;
286
+ }
287
+ const permission = this.#channel(token);
288
+ const messageKey = crypto.randomUUID();
289
+ const { preload, ...workerOptions } = message.options;
290
+ let worker: Worker;
291
+ try {
292
+ worker = new Worker(new URL("./permission-worker-entry.js", import.meta.url), {
293
+ ...workerOptions,
294
+ workerData: {
295
+ permission: { ...permission, messageKey },
296
+ enforced: token !== undefined,
297
+ script: message.script,
298
+ preload,
299
+ userWorkerData: workerOptions.workerData,
300
+ },
301
+ transferList: [permission.port],
302
+ });
303
+ } catch (error) {
304
+ permission.port.close();
305
+ message.port.postMessage(workerErrorFrame(error));
306
+ message.port.postMessage({ type: "exit" });
307
+ message.port.close();
308
+ return;
309
+ }
310
+ const exited = new Promise<void>((resolve) =>
311
+ worker.once("exit", () => {
312
+ message.port.postMessage({ type: "exit" });
313
+ message.port.close();
314
+ this.#children.delete(worker);
315
+ resolve();
316
+ }),
317
+ );
318
+ this.#children.set(worker, exited);
319
+ worker.on("error", (error: Error) => message.port.postMessage(workerErrorFrame(error)));
320
+ worker.on("online", () => message.port.postMessage({ type: "open" }));
321
+ worker.on("message", (frame: unknown) => {
322
+ if (
323
+ frame !== null &&
324
+ typeof frame === "object" &&
325
+ "key" in frame &&
326
+ frame.key === messageKey &&
327
+ "value" in frame &&
328
+ "transfer" in frame &&
329
+ Array.isArray(frame.transfer)
330
+ ) {
331
+ forwardWorkerMessage(message.port, frame.value, frame.transfer);
332
+ } else forwardWorkerMessage(message.port, frame, []);
333
+ });
334
+ message.port.on("message", (frame: { type: string; value?: unknown; transfer?: Transferable[] }) => {
335
+ if (frame.type === "terminate") void worker.terminate();
336
+ else if (frame.type === "message") worker.postMessage(frame.value, frame.transfer);
337
+ });
338
+ }
339
+
340
+ revoke(): void {
341
+ this.#revoked = true;
342
+ this.#kill(this.#kernel);
343
+ for (const token of this.#tokens.values()) this.#kill(token);
344
+ for (const pool of this.#pools) interruptPermissionSlots(pool);
345
+ }
346
+
347
+ retire(): Promise<void> {
348
+ if (this.#retiring) return this.#retiring;
349
+ const retirement = (async () => {
350
+ let outcome: WorkerRetirement;
351
+ try {
352
+ outcome = await this.#retire();
353
+ } catch {
354
+ outcome = "abandoned";
355
+ }
356
+ if (outcome === "abandoned") this.#failed();
357
+ })();
358
+ this.#retiring = retirement;
359
+ void retirement
360
+ .finally(() => {
361
+ if (this.#retiring === retirement) this.#retiring = undefined;
362
+ })
363
+ .catch(() => {});
364
+ return retirement;
365
+ }
366
+
367
+ #failed(): void {
368
+ this.#session.dead = true;
369
+ this.#options.onRevocationFailed?.({ sessionId: this.#options.sessionId, kernelId: this.kernelId });
370
+ }
371
+
372
+ async terminateChildren(): Promise<void> {
373
+ const children = [...this.#children];
374
+ for (const [worker] of children) void worker.terminate();
375
+ await Promise.all(children.map(([, exited]) => exited));
376
+ for (const port of this.#ports) port.close();
377
+ for (const token of this.#tokens.values()) this.#release(token);
378
+ this.#unbindInvokers();
379
+ this.#runs.clear();
380
+ }
381
+
382
+ dispose(): void {
383
+ this.revoke();
384
+ this.#releaseSession();
385
+ this.#unbindInvokers();
386
+ unregisterPermissionKernel(this.#options.sessionId, this);
387
+ }
388
+ }
@@ -0,0 +1,205 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { pathToFileURL } from "node:url";
3
+ import { MessageChannel, parentPort } from "node:worker_threads";
4
+ import { authorityDecision } from "./permission-authority.mjs";
5
+ import { createPermissionRequester } from "./permission-slots.mjs";
6
+ import { createCodePermissionGuard } from "./interpose-code.js";
7
+
8
+ let initialized = false;
9
+
10
+ export async function initializePermissionRuntime(install, workerData, engine) {
11
+ if (initialized) throw new Error("Permission runtime already initialized");
12
+ initialized = true;
13
+ const context = new AsyncLocalStorage();
14
+ const bound = new Map();
15
+ const data = workerData.permission;
16
+ // The bootstrap transport is host authority, not user-visible worker data.
17
+ delete workerData.permission;
18
+ const inactiveWord = new SharedArrayBuffer(4);
19
+ Atomics.store(new Int32Array(inactiveWord), 0, 2);
20
+ const kernel = data?.kernel ?? { id: "", authority: inactiveWord, rules: [] };
21
+ const requester = data && createPermissionRequester(data.slots, {
22
+ wake: (slot, generation) => data.port.postMessage({ type: "check", slot, generation }),
23
+ retire: () => data.port.postMessage({ type: "retire" }),
24
+ deadlineMs: data.deadlineMs,
25
+ });
26
+ const refusal = (reason, rule) => {
27
+ const error = new Error(`${reason}${rule ? ` (${rule.permission}: ${rule.pattern}, ${rule.layer})` : ""}`);
28
+ error.name = "CellPermissionDeniedError";
29
+ error.code = "cell_permission_denied";
30
+ error.rule = rule;
31
+ return error;
32
+ };
33
+ // Resolution hooks and module top-level bodies run outside the importing cell's async context. A
34
+ // token that is importing opens a LOADING WINDOW; an out-of-context operation is attributed to that
35
+ // token only while it is the sole loader, bound, active and alive, and no other bound token is active
36
+ // (design 6.4 / N4). Any other shape keeps today's refusal: never a pass that was a refusal before.
37
+ const loading = new Map();
38
+ const activeBit = (entry) => Atomics.load(new Int32Array(entry.authority), 0) & 1;
39
+ const aliveBit = (entry) => Atomics.load(new Int32Array(entry.authority), 0) & 2;
40
+ const load = async (thunk) => {
41
+ const token = context.getStore();
42
+ if (!token) return thunk();
43
+ const entry = loading.get(token.id);
44
+ if (entry) entry.depth += 1;
45
+ else loading.set(token.id, { token, depth: 1 });
46
+ try {
47
+ return await thunk();
48
+ } finally {
49
+ const current = loading.get(token.id);
50
+ if (current && (current.depth -= 1) === 0) loading.delete(token.id);
51
+ }
52
+ };
53
+ Object.defineProperty(load, "pending", { value: () => loading.size });
54
+ Object.defineProperty(globalThis, Symbol.for("senpi.kernel.permissionLoad"), { value: load });
55
+ const soleLoader = () => {
56
+ if (loading.size !== 1) return undefined;
57
+ const [{ token }] = loading.values();
58
+ if (!bound.has(token.id) || !activeBit(token) || !aliveBit(token)) return undefined;
59
+ for (const other of bound.values()) if (other.id !== token.id && activeBit(other)) return undefined;
60
+ return token;
61
+ };
62
+ const authorize = (operation) => {
63
+ const token = context.getStore() ?? soleLoader();
64
+ const disposition = authorityDecision(token, token ? [] : [...bound.values()], kernel);
65
+ if (disposition === "pass") return;
66
+ const rule = token?.rules[0] ?? [...bound.values()].find((entry) => entry.rules.length)?.rules[0] ?? kernel.rules[0];
67
+ if (disposition === "refuse" || !requester || !token) throw refusal("Permission authority unavailable", rule);
68
+ try {
69
+ const { call, unsupported, granted } = operation();
70
+ if (unsupported) throw refusal("Operation cannot be checked", rule);
71
+ if (granted) return;
72
+ const result = requester.check({ token: token.id, call });
73
+ // Revocation may have raced an already-published answer.
74
+ if (!(Atomics.load(new Int32Array(token.authority), 0) & 2)) throw new Error("Permission authority revoked");
75
+ if (result.action === "deny") throw refusal("Operation denied", result.rule);
76
+ } catch (error) {
77
+ if (error?.code === "cell_permission_denied") throw error;
78
+ throw refusal(error instanceof Error ? error.message : "Permission check failed", rule);
79
+ }
80
+ };
81
+ install(authorize);
82
+ const codeGuard = createCodePermissionGuard(authorize);
83
+ const runAuthority = async (token, run, source) => {
84
+ if (!token && authorityDecision(undefined, [...bound.values()], kernel) !== "pass") {
85
+ throw refusal("Permission authority unavailable", kernel.rules[0]);
86
+ }
87
+ if (token) bound.set(token.id, token);
88
+ try { return await context.run(token, () => { codeGuard.scan(source); return run(); }); }
89
+ finally { if (token) bound.delete(token.id); }
90
+ };
91
+
92
+ // Only a configured kernel replaces the constructor; the entry already put a forwarder on the module
93
+ // before its ESM namespace existed, and this class is what it constructs.
94
+ if (data && globalThis.Worker) {
95
+ engine.installGuardedWorker(class Worker extends EventTarget {
96
+ #port;
97
+ onmessage = null;
98
+ onerror = null;
99
+ onopen = null;
100
+ onclose = null;
101
+ onmessageerror = null;
102
+ constructor(script, options = {}) {
103
+ super();
104
+ const token = context.getStore();
105
+ if (authorityDecision(token, [...bound.values()], kernel) === "refuse") throw refusal("Permission authority unavailable", token?.rules[0] ?? kernel.rules[0]);
106
+ const { port1, port2 } = new MessageChannel();
107
+ this.#port = port1;
108
+ port1.on("message", (frame) => {
109
+ if (frame.type === "message") {
110
+ const event = new MessageEvent("message", { data: frame.value });
111
+ if (frame.value === undefined) Object.defineProperty(event, "data", { value: undefined });
112
+ this.onmessage?.(event);
113
+ this.dispatchEvent(event);
114
+ } else if (frame.type === "error") {
115
+ const event = new ErrorEvent("error", {
116
+ message: frame.message, error: frame.error,
117
+ filename: frame.filename, lineno: frame.lineno, colno: frame.colno,
118
+ });
119
+ this.onerror?.(event);
120
+ this.dispatchEvent(event);
121
+ } else if (frame.type === "messageerror") {
122
+ const event = new MessageEvent("messageerror", { data: frame.error });
123
+ this.onmessageerror?.(event);
124
+ this.dispatchEvent(event);
125
+ } else if (frame.type === "open") {
126
+ const event = new Event("open");
127
+ this.onopen?.(event);
128
+ this.dispatchEvent(event);
129
+ } else if (frame.type === "exit") {
130
+ port1.close();
131
+ const event = new Event("close");
132
+ this.onclose?.(event);
133
+ this.dispatchEvent(event);
134
+ }
135
+ });
136
+ port1.on("messageerror", (error) => {
137
+ const event = new MessageEvent("messageerror", { data: error });
138
+ this.onmessageerror?.(event);
139
+ this.dispatchEvent(event);
140
+ });
141
+ const href = new URL(String(script), pathToFileURL(`${process.cwd()}/`)).href;
142
+ // Inactive resources belong to the kernel lifetime, not the completed run's registry entry.
143
+ const activeToken = token && (Atomics.load(new Int32Array(token.authority), 0) & 1) ? token.id : undefined;
144
+ data.port.postMessage({ type: "spawn", script: href, token: activeToken, port: port2, options }, [port2]);
145
+ }
146
+ postMessage(value, options = []) {
147
+ const transfer = Array.isArray(options) ? options : options.transfer ?? [];
148
+ this.#port.postMessage({ type: "message", value, transfer }, transfer);
149
+ }
150
+ terminate() { this.#port.postMessage({ type: "terminate" }); }
151
+ ref() { this.#port.ref(); return this; }
152
+ unref() { this.#port.unref(); return this; }
153
+ });
154
+ }
155
+
156
+ if (workerData.script) {
157
+ const { installSessionCwd } = await import("./worker-cwd.js");
158
+ installSessionCwd(data.cwd, { permissionInterposed: true });
159
+ globalThis.postMessage = (value, options = []) => {
160
+ const transfer = Array.isArray(options) ? options : options.transfer ?? [];
161
+ parentPort.postMessage({ key: data.messageKey, value, transfer }, transfer);
162
+ };
163
+ globalThis.close = () => parentPort.close();
164
+ const events = new EventTarget();
165
+ globalThis.addEventListener = events.addEventListener.bind(events);
166
+ globalThis.removeEventListener = events.removeEventListener.bind(events);
167
+ parentPort.on("message", (value) => context.run(data.token, () => {
168
+ const event = new MessageEvent("message", { data: value });
169
+ if (value === undefined) Object.defineProperty(event, "data", { value: undefined });
170
+ globalThis.onmessage?.(event);
171
+ events.dispatchEvent(event);
172
+ }));
173
+ engine.activate();
174
+ await runAuthority(data.token, async () => {
175
+ // User preloads still precede the script, but never precede interposition.
176
+ const preloads = typeof workerData.preload === "string" ? [workerData.preload] : workerData.preload ?? [];
177
+ const require = process.getBuiltinModule("node:module").createRequire(pathToFileURL(`${data.cwd}/__preload__.js`));
178
+ const { isAbsolute } = process.getBuiltinModule("node:path");
179
+ for (const preload of preloads) {
180
+ const resolved = /^[a-z][a-z\d+.-]*:/iu.test(preload) && !isAbsolute(preload) ? preload : require.resolve(preload);
181
+ await load(() => import(isAbsolute(resolved) ? pathToFileURL(resolved).href : resolved));
182
+ }
183
+ await load(() => import(workerData.script));
184
+ });
185
+ return;
186
+ }
187
+ const { createWorkerCore } = await import("./worker-core.js");
188
+ const { initializeResolverIO } = await import("./worker-package-resolve.js");
189
+ initializeResolverIO(engine.resolverIO);
190
+ const { initializeProcessCleanup } = await import("./process-tree.js");
191
+ const { initializeWindowsCleanup } = await import("./windows-tree-kill.js");
192
+ initializeProcessCleanup(engine.cleanupExecFile);
193
+ initializeWindowsCleanup(engine.cleanupExecFile);
194
+ createWorkerCore({
195
+ send: (message) => parentPort.postMessage(message),
196
+ onMessage(handler) { parentPort.on("message", handler); return () => parentPort.off("message", handler); },
197
+ close() { parentPort.close(); setTimeout(() => process.exit(0), 0); },
198
+ }, {
199
+ cwd: workerData.cwd,
200
+ parallelPoolWidth: workerData.parallelPoolWidth,
201
+ cwdInstallOptions: { permissionInterposed: true },
202
+ runAuthority,
203
+ permissionIO: engine.permissionIO,
204
+ });
205
+ }
@@ -0,0 +1,46 @@
1
+ interface SessionKernel {
2
+ revoke(): void;
3
+ retire(): Promise<void>;
4
+ }
5
+
6
+ export interface PermissionSession {
7
+ dead: boolean;
8
+ pending?: Promise<void>;
9
+ readonly kernels: Set<SessionKernel>;
10
+ }
11
+
12
+ const sessions = new Map<string, PermissionSession>();
13
+
14
+ export function registerPermissionKernel(sessionId: string, kernel: SessionKernel): PermissionSession {
15
+ let session = sessions.get(sessionId);
16
+ if (!session) {
17
+ session = { dead: false, kernels: new Set() };
18
+ sessions.set(sessionId, session);
19
+ }
20
+ session.kernels.add(kernel);
21
+ return session;
22
+ }
23
+
24
+ export function unregisterPermissionKernel(sessionId: string, kernel: SessionKernel): void {
25
+ const session = sessions.get(sessionId);
26
+ if (!session) return;
27
+ session.kernels.delete(kernel);
28
+ if (!session.dead && !session.pending && session.kernels.size === 0) sessions.delete(sessionId);
29
+ }
30
+
31
+ /** All words go DEAD synchronously, before retirement or another admission can run. */
32
+ export function revokeSessionKernels(sessionId: string): Promise<void> {
33
+ const session = sessions.get(sessionId);
34
+ if (!session) return Promise.resolve();
35
+ if (session.pending) return session.pending;
36
+ const kernels = [...session.kernels];
37
+ for (const kernel of kernels) kernel.revoke();
38
+ const pending = Promise.all(kernels.map((kernel) => kernel.retire())).then(() => {});
39
+ session.pending = pending;
40
+ void pending
41
+ .finally(() => {
42
+ if (session.pending === pending) session.pending = undefined;
43
+ })
44
+ .catch(() => {});
45
+ return pending;
46
+ }
@@ -0,0 +1,51 @@
1
+ export interface CellRule {
2
+ readonly permission: string;
3
+ readonly pattern: string;
4
+ readonly layer: string;
5
+ }
6
+ export interface CellCanonicalPath {
7
+ /** PATH_IDENTITY_VERSION of the kernel that computed it; the host ignores any other version. */
8
+ readonly v: number;
9
+ readonly path: string;
10
+ readonly foldCase: boolean;
11
+ }
12
+ export type CellOperation = {
13
+ readonly tool: "bash" | "read" | "write" | "ls";
14
+ readonly input: {
15
+ readonly command?: string;
16
+ /** The path as written by the cell, byte-for-byte. */
17
+ readonly path?: string;
18
+ /** The absolute string the operation hands to the runtime. */
19
+ readonly executedPath?: string;
20
+ readonly canonical?: CellCanonicalPath;
21
+ /** Error code when the kernel could not canonicalize the executed path. */
22
+ readonly unresolvable?: string;
23
+ };
24
+ readonly cwd?: string;
25
+ };
26
+ export type CellDecision = { readonly action: "pass" } | { readonly action: "deny"; readonly rule: CellRule };
27
+ export interface PermissionRequest {
28
+ readonly token: string;
29
+ readonly call: CellOperation;
30
+ }
31
+ export function createPermissionSlots(): SharedArrayBuffer;
32
+ export function permissionControl(buffer: SharedArrayBuffer, index: number): number;
33
+ export function interruptPermissionSlot(buffer: SharedArrayBuffer, index: number, observed: number): boolean;
34
+ export function interruptPermissionSlots(buffer: SharedArrayBuffer): void;
35
+ export function createPermissionRequester(
36
+ buffer: SharedArrayBuffer,
37
+ hooks: {
38
+ wake(index: number, generation: number): void;
39
+ wait?: typeof Atomics.wait;
40
+ generationLimit?: number;
41
+ deadlineMs?: number;
42
+ retire?(): void;
43
+ },
44
+ ): { check(request: PermissionRequest): CellDecision };
45
+ export function servePermissionSlot(
46
+ buffer: SharedArrayBuffer,
47
+ index: number,
48
+ generation: number,
49
+ decide: (request: PermissionRequest) => CellDecision | Promise<CellDecision>,
50
+ hooks?: { claimed?(): void | Promise<void> },
51
+ ): Promise<void>;