@code-yeongyu/senpi-codemode 2026.9.16-3 → 2026.9.17-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 CHANGED
@@ -12,6 +12,36 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.9.17-2] - 2026-09-17
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ ### Fixed
24
+
25
+ - JS eval kernel: a cell whose top-level declaration (`const`/`let`/`var`, plain or destructured) names an existing platform or prelude global (for example `const fetch = ...`) is now rejected before execution with an error naming the identifier and the rename remedy, instead of silently replacing that global for every later cell and wedging the session until a kernel reset. Cell-created globals stay re-declarable across cells, and explicit `globalThis.<name> = ...` assignments remain untouched as the deliberate escape hatch. (#1784)
26
+
27
+ ### Removed
28
+
29
+ ## [2026.9.17] - 2026-09-17
30
+
31
+ ### Breaking Changes
32
+
33
+ ### Added
34
+
35
+ - `kernelTools.invoke(request, options?)` accepts a per-call execution scope for the nested host calls the invoked closure makes: `{ scope: { tools: { allow?: string[], deny?: string[] } } }`. While that invocation is active, a `tool.<name>()` outside the scope is refused inside the worker with `kernel_tool_host_denied` carrying `{ tool, call_id, reason: "allow" | "deny" }`: the closure sees a rejected promise, the refusal never reaches the host bridge, and the parent's own cells and queue keep the parent's full tool surface. `deny` wins over `allow`, an `allow` list refuses every host tool it does not name, a malformed list fails closed, and the scope lives only for that call — it is dropped when the call settles (including interrupt and reset) and is never persisted. The second argument still accepts a bare `AbortSignal`, and a call without a scope posts exactly the message it always did. Consumers detect the feature through `kernelTools.capabilities.invokeScope === true`; `KERNEL_TOOLS_CAPABILITIES`, `KernelToolsCapabilities`, `KernelToolsInvokeOptions`, `KernelToolsInvokeScope`, `KernelToolsHostScope`, `KernelToolHostDenial` and `KernelToolHostDenialReason` are exported ([#1731](https://github.com/code-yeongyu/senpi/issues/1731)).
36
+
37
+ ### Changed
38
+
39
+ - Kernel-tools types bind to coding-agent's `ExtensionKernelTools` / `KernelToolInvokeOptions` / `KernelToolInvokeScope` (`KERNEL_TOOLS_CAPABILITIES satisfies ExtensionKernelTools["capabilities"]`) so the implementation cannot drift from the host declaration ([#1731](https://github.com/code-yeongyu/senpi/issues/1731)).
40
+
41
+ ### Fixed
42
+
43
+ ### Removed
44
+
15
45
  ## [2026.9.16-3] - 2026-09-16
16
46
 
17
47
  ### Breaking Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.9.16-3",
3
+ "version": "2026.9.17-2",
4
4
  "description": "Source-only senpi extension package for codemode evaluation tools",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -30,14 +30,14 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@babel/parser": "8.0.4",
33
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.16-3",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.17-2",
34
34
  "typebox": "1.3.27"
35
35
  },
36
36
  "peerDependencies": {
37
- "@code-yeongyu/senpi": "2026.9.16-3"
37
+ "@code-yeongyu/senpi": "2026.9.17-2"
38
38
  },
39
39
  "devDependencies": {
40
- "@code-yeongyu/senpi": "2026.9.16-3"
40
+ "@code-yeongyu/senpi": "2026.9.17-2"
41
41
  },
42
42
  "keywords": [
43
43
  "senpi",
@@ -1,10 +1,28 @@
1
1
  import { type Static, Type } from "typebox";
2
2
 
3
+ /** Payload of a `kernel_tool_host_denied` refusal: the host tool, the invoking call, the reason (#1731). */
4
+ const kernelToolHostDenialSchema = Type.Object({
5
+ tool: Type.String({ minLength: 1 }),
6
+ call_id: Type.String({ minLength: 1 }),
7
+ reason: Type.Union([Type.Literal("allow"), Type.Literal("deny")]),
8
+ });
9
+
3
10
  const kernelToolErrorSchema = Type.Object({
4
11
  message: Type.String(),
5
12
  name: Type.Optional(Type.String()),
6
13
  stack: Type.Optional(Type.String()),
7
14
  code: Type.Optional(Type.String()),
15
+ details: Type.Optional(kernelToolHostDenialSchema),
16
+ });
17
+
18
+ /** Per-call execution scope for the nested host calls the invoked closure makes (#1731). */
19
+ export const kernelToolInvokeScopeSchema = Type.Object({
20
+ tools: Type.Optional(
21
+ Type.Object({
22
+ allow: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
23
+ deny: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
24
+ }),
25
+ ),
8
26
  });
9
27
 
10
28
  export const kernelToolDescriptorSchema = Type.Object({
@@ -43,6 +61,7 @@ export const kernelToolHostToKernelSchemas = [
43
61
  definition_revision: Type.Integer({ minimum: 1 }),
44
62
  args: Type.Unknown(),
45
63
  call_id: Type.String({ minLength: 1 }),
64
+ scope: Type.Optional(kernelToolInvokeScopeSchema),
46
65
  }),
47
66
  Type.Object({
48
67
  type: Type.Literal("kernel-tool-cancel"),
package/src/index.ts CHANGED
@@ -263,10 +263,17 @@ function modelIdFrom(event: unknown): string | undefined {
263
263
  }
264
264
 
265
265
  export {
266
+ KERNEL_TOOLS_CAPABILITIES,
266
267
  KERNEL_TOOLS_UNSUPPORTED,
267
268
  type KernelToolDescriptor,
269
+ type KernelToolHostDenial,
270
+ type KernelToolHostDenialReason,
271
+ type KernelToolsCapabilities,
268
272
  type KernelToolsCapability,
269
273
  type KernelToolsDescribeResult,
274
+ type KernelToolsHostScope,
275
+ type KernelToolsInvokeOptions,
270
276
  type KernelToolsInvokeRequest,
277
+ type KernelToolsInvokeScope,
271
278
  } from "./kernels/js/kernel-tools-types.ts";
272
279
  export { enabledLanguagesFrom };
@@ -13,7 +13,11 @@ import {
13
13
  } from "./kernel-contract.ts";
14
14
  import { kernelToolError } from "./kernel-tools-errors.ts";
15
15
  import { KernelToolHostPump } from "./kernel-tools-host.ts";
16
- import type { KernelToolsDescribeResult, KernelToolsInvokeRequest } from "./kernel-tools-types.ts";
16
+ import type {
17
+ KernelToolsDescribeResult,
18
+ KernelToolsInvokeOptions,
19
+ KernelToolsInvokeRequest,
20
+ } from "./kernel-tools-types.ts";
17
21
  import { type JavaScriptKernelOptions, LocalModuleLoader } from "./local-module-loader.ts";
18
22
  import { terminateProcessTrees } from "./process-tree-host.ts";
19
23
  import { JavaScriptRunQueue, type PendingJavaScriptRun, stoppedResult } from "./run-queue.ts";
@@ -72,8 +76,11 @@ export class JavaScriptKernel {
72
76
  return this.#kernelTools.describe(names);
73
77
  }
74
78
 
75
- invokeKernelTool(request: KernelToolsInvokeRequest, signal?: AbortSignal): Promise<unknown> {
76
- return this.#kernelTools.invoke(request, signal);
79
+ invokeKernelTool(
80
+ request: KernelToolsInvokeRequest,
81
+ options?: AbortSignal | KernelToolsInvokeOptions,
82
+ ): Promise<unknown> {
83
+ return this.#kernelTools.invoke(request, options);
77
84
  }
78
85
 
79
86
  async run(input: JavaScriptRunInput): Promise<ResultMessage> {
@@ -1,3 +1,5 @@
1
+ import type { KernelToolsInvokeScope } from "./kernel-tools-types.ts";
2
+
1
3
  export const kernelToolCallContext: {
2
4
  run<T>(store: KernelToolCallStore, fn: () => T): T;
3
5
  getStore(): KernelToolCallStore | undefined;
@@ -8,6 +10,8 @@ export type KernelToolCallStore = {
8
10
  readonly callId: string;
9
11
  readonly generation: number;
10
12
  readonly signal: AbortSignal;
13
+ /** Host tools this call's nested bridge calls may reach; absent means the parent's full surface. */
14
+ readonly scope?: KernelToolsInvokeScope;
11
15
  };
12
16
 
13
17
  export function inKernelToolInvoke(): boolean;
@@ -7,16 +7,19 @@ export const KERNEL_TOOL_ERROR_CODES = Object.freeze([
7
7
  "kernel_tool_missing",
8
8
  "kernel_tool_failed",
9
9
  "kernel_tool_recursion",
10
+ "kernel_tool_host_denied",
10
11
  ]);
11
12
 
12
13
  export class KernelToolError extends Error {
13
- constructor(code, message) {
14
+ constructor(code, message, details) {
14
15
  super(message);
15
16
  this.name = "KernelToolError";
16
17
  this.code = code;
18
+ // Only `kernel_tool_host_denied` carries one today: { tool, call_id, reason }.
19
+ if (details !== undefined) this.details = details;
17
20
  }
18
21
  }
19
22
 
20
- export function kernelToolError(code, message) {
21
- return new KernelToolError(code, message);
23
+ export function kernelToolError(code, message, details) {
24
+ return new KernelToolError(code, message, details);
22
25
  }
@@ -7,20 +7,38 @@ export const KERNEL_TOOL_ERROR_CODES = [
7
7
  "kernel_tool_missing",
8
8
  "kernel_tool_failed",
9
9
  "kernel_tool_recursion",
10
+ "kernel_tool_host_denied",
10
11
  ] as const;
11
12
 
12
13
  export type KernelToolErrorCode = (typeof KERNEL_TOOL_ERROR_CODES)[number];
13
14
 
15
+ /** Why a nested host call was refused: it is outside the call's allow list, or named by its deny list. */
16
+ export type KernelToolHostDenialReason = "allow" | "deny";
17
+
18
+ /** Payload carried by `kernel_tool_host_denied`: the host tool, the invoking kernel-tool call, the reason. */
19
+ export type KernelToolHostDenial = {
20
+ readonly tool: string;
21
+ readonly call_id: string;
22
+ readonly reason: KernelToolHostDenialReason;
23
+ };
24
+
14
25
  export class KernelToolError extends Error {
15
26
  readonly name = "KernelToolError";
16
27
  readonly code: KernelToolErrorCode;
28
+ /** Structured payload for the codes that carry one; only `kernel_tool_host_denied` does today. */
29
+ readonly details?: KernelToolHostDenial;
17
30
 
18
- constructor(code: KernelToolErrorCode, message: string) {
31
+ constructor(code: KernelToolErrorCode, message: string, details?: KernelToolHostDenial) {
19
32
  super(message);
20
33
  this.code = code;
34
+ if (details !== undefined) this.details = details;
21
35
  }
22
36
  }
23
37
 
24
- export function kernelToolError(code: KernelToolErrorCode, message: string): KernelToolError {
25
- return new KernelToolError(code, message);
38
+ export function kernelToolError(
39
+ code: KernelToolErrorCode,
40
+ message: string,
41
+ details?: KernelToolHostDenial,
42
+ ): KernelToolError {
43
+ return new KernelToolError(code, message, details);
26
44
  }
@@ -2,7 +2,12 @@ import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/prot
2
2
  import { generateCorrelationId } from "../../bridge/protocol.ts";
3
3
  import { RESERVED_AGENT_TOOL } from "../../bridge/reserved.ts";
4
4
  import { kernelToolError } from "./kernel-tools-errors.ts";
5
- import type { KernelToolsDescribeResult, KernelToolsInvokeRequest } from "./kernel-tools-types.ts";
5
+ import type {
6
+ KernelToolsDescribeResult,
7
+ KernelToolsInvokeOptions,
8
+ KernelToolsInvokeRequest,
9
+ KernelToolsInvokeScope,
10
+ } from "./kernel-tools-types.ts";
6
11
 
7
12
  type KernelToolReply = Extract<
8
13
  KernelToHostMessage,
@@ -59,7 +64,13 @@ export class KernelToolHostPump {
59
64
  return { results: reply.results as KernelToolsDescribeResult["results"] };
60
65
  }
61
66
 
62
- async invoke(request: KernelToolsInvokeRequest, signal?: AbortSignal): Promise<unknown> {
67
+ /**
68
+ * `options` is the caller's abort signal, or `{ signal?, scope? }` where `scope` bounds the host
69
+ * tools the invoked closure may reach during this call only. A call without a scope posts exactly
70
+ * the message it always did (#1731).
71
+ */
72
+ async invoke(request: KernelToolsInvokeRequest, options?: AbortSignal | KernelToolsInvokeOptions): Promise<unknown> {
73
+ const { signal, scope } = normalizeInvokeOptions(options);
63
74
  this.events.dispatchEvent(new Event("nestedInvoke"));
64
75
  const reply = await this.#request(
65
76
  {
@@ -70,13 +81,14 @@ export class KernelToolHostPump {
70
81
  definition_revision: request.definition_revision,
71
82
  args: request.args,
72
83
  call_id: request.call_id,
84
+ ...wireScope(scope),
73
85
  },
74
86
  signal,
75
87
  );
76
88
  if (reply.type !== "kernel-tool-invoke-reply") {
77
89
  throw kernelToolError("kernel_tool_failed", "unexpected kernel-tool invoke reply");
78
90
  }
79
- if (!reply.ok) throw kernelToolError(codeOf(reply.error.code), reply.error.message);
91
+ if (!reply.ok) throw kernelToolError(codeOf(reply.error.code), reply.error.message, reply.error.details);
80
92
  return reply.value;
81
93
  }
82
94
 
@@ -112,6 +124,33 @@ export class KernelToolHostPump {
112
124
  }
113
125
  }
114
126
 
127
+ /**
128
+ * The scope as the protocol carries it: own copies of the caller's lists, and nothing at all when the
129
+ * caller named no host tools, so an unscoped call posts exactly the message it always did.
130
+ */
131
+ function wireScope(scope?: KernelToolsInvokeScope): { scope?: { tools: { allow?: string[]; deny?: string[] } } } {
132
+ const tools = scope?.tools;
133
+ if (tools === undefined) return {};
134
+ if (tools.allow === undefined && tools.deny === undefined) return {};
135
+ return {
136
+ scope: {
137
+ tools: {
138
+ ...(tools.allow === undefined ? {} : { allow: [...tools.allow] }),
139
+ ...(tools.deny === undefined ? {} : { deny: [...tools.deny] }),
140
+ },
141
+ },
142
+ };
143
+ }
144
+
145
+ function normalizeInvokeOptions(options?: AbortSignal | KernelToolsInvokeOptions): KernelToolsInvokeOptions {
146
+ if (options === undefined) return {};
147
+ return isAbortSignal(options) ? { signal: options } : options;
148
+ }
149
+
150
+ function isAbortSignal(options: AbortSignal | KernelToolsInvokeOptions): options is AbortSignal {
151
+ return options instanceof AbortSignal || "aborted" in options;
152
+ }
153
+
115
154
  function codeOf(
116
155
  code: string | undefined,
117
156
  ):
@@ -119,12 +158,14 @@ function codeOf(
119
158
  | "kernel_tool_stale"
120
159
  | "kernel_tool_missing"
121
160
  | "kernel_tool_recursion"
161
+ | "kernel_tool_host_denied"
122
162
  | "tools_unavailable"
123
163
  | "invalid_tool_definition" {
124
164
  if (
125
165
  code === "kernel_tool_stale" ||
126
166
  code === "kernel_tool_missing" ||
127
167
  code === "kernel_tool_recursion" ||
168
+ code === "kernel_tool_host_denied" ||
128
169
  code === "tools_unavailable" ||
129
170
  code === "invalid_tool_definition"
130
171
  ) {
@@ -21,7 +21,15 @@ export function createKernelToolPump({ getRuntime, emit, nestedInvokes }) {
21
21
  const runtime = getRuntime();
22
22
  if (!runtime?.kernelTools) throw kernelToolError("tools_unavailable", "JS runtime not initialized");
23
23
  const value = await kernelToolCallContext.run(
24
- { pendingTools, callId: message.call_id, generation: message.kernel_generation, signal: controller.signal },
24
+ // The scope lives only in this store: it is gone once the call settles, and it never
25
+ // reaches the top-level cell context or the next invocation (#1731).
26
+ {
27
+ pendingTools,
28
+ callId: message.call_id,
29
+ generation: message.kernel_generation,
30
+ signal: controller.signal,
31
+ scope: message.scope,
32
+ },
25
33
  () =>
26
34
  runtime.kernelTools.invoke(
27
35
  {
@@ -91,15 +99,21 @@ function pumpError(error) {
91
99
  message: error.message,
92
100
  stack: error.stack,
93
101
  ...(typeof error.code === "string" ? { code: error.code } : {}),
102
+ ...(isPlainRecord(error.details) ? { details: error.details } : {}),
94
103
  };
95
104
  }
96
105
  return { message: String(error) };
97
106
  }
98
107
 
108
+ function isPlainRecord(value) {
109
+ return value !== null && typeof value === "object" && !Array.isArray(value);
110
+ }
111
+
99
112
  function errorFrom(error) {
100
113
  const result = new Error(error.message);
101
114
  if (error.name) result.name = error.name;
102
115
  if (error.stack) result.stack = error.stack;
103
116
  if (typeof error.code === "string") result.code = error.code;
117
+ if (isPlainRecord(error.details)) result.details = error.details;
104
118
  return result;
105
119
  }
@@ -0,0 +1,9 @@
1
+ import type { KernelToolError, KernelToolHostDenialReason } from "./kernel-tools-errors.ts";
2
+ import type { KernelToolsInvokeScope } from "./kernel-tools-types.ts";
3
+
4
+ export function hostToolRefusal(
5
+ scope: KernelToolsInvokeScope | undefined,
6
+ toolName: string,
7
+ ): KernelToolHostDenialReason | null;
8
+
9
+ export function hostDeniedError(toolName: string, callId: string, reason: KernelToolHostDenialReason): KernelToolError;
@@ -0,0 +1,34 @@
1
+ import { kernelToolError } from "./kernel-tools-errors.js";
2
+
3
+ /**
4
+ * Call-scoped host-tool policy for one kernel-tool invocation (#1731). A call without a tool scope
5
+ * refuses nothing (today's behavior); `deny` wins over `allow`; an `allow` list refuses every host
6
+ * tool it does not name; a malformed list fails closed instead of widening the call's reach.
7
+ */
8
+ export function hostToolRefusal(scope, toolName) {
9
+ const tools = scope?.tools;
10
+ if (tools === null || typeof tools !== "object") return null;
11
+ if (tools.deny !== undefined) {
12
+ const deny = nameList(tools.deny);
13
+ if (deny === null || deny.includes(toolName)) return "deny";
14
+ }
15
+ if (tools.allow !== undefined) {
16
+ const allow = nameList(tools.allow);
17
+ if (allow === null || !allow.includes(toolName)) return "allow";
18
+ }
19
+ return null;
20
+ }
21
+
22
+ /** The typed refusal the closure sees and the invoking call reports. */
23
+ export function hostDeniedError(toolName, callId, reason) {
24
+ return kernelToolError(
25
+ "kernel_tool_host_denied",
26
+ `Host tool is outside this kernel tool call's scope: ${toolName} (${reason})`,
27
+ { tool: toolName, call_id: callId, reason },
28
+ );
29
+ }
30
+
31
+ function nameList(value) {
32
+ if (!Array.isArray(value)) return null;
33
+ return value.every((name) => typeof name === "string") ? value : null;
34
+ }
@@ -1,6 +1,7 @@
1
- import type { KernelToolErrorCode } from "./kernel-tools-errors.ts";
1
+ import type { ExtensionKernelTools, KernelToolInvokeOptions, KernelToolInvokeScope } from "@code-yeongyu/senpi";
2
+ import type { KernelToolErrorCode, KernelToolHostDenial, KernelToolHostDenialReason } from "./kernel-tools-errors.ts";
2
3
 
3
- export type { KernelToolErrorCode };
4
+ export type { KernelToolErrorCode, KernelToolHostDenial, KernelToolHostDenialReason };
4
5
 
5
6
  export type KernelToolDescriptor = {
6
7
  readonly name: string;
@@ -31,9 +32,22 @@ export type KernelToolsDescribeResult = {
31
32
  readonly results: readonly KernelToolsDescribeEntry[];
32
33
  };
33
34
 
35
+ /** Host declaration (#1731); aliases so this package cannot drift from `@code-yeongyu/senpi`. */
36
+ export type KernelToolsHostScope = NonNullable<KernelToolInvokeScope["tools"]>;
37
+ export type KernelToolsInvokeScope = KernelToolInvokeScope;
38
+ export type KernelToolsInvokeOptions = KernelToolInvokeOptions;
39
+
40
+ /** Stable capability markers a consumer gates on before sending an option this runtime may not know. */
41
+ export type KernelToolsCapabilities = ExtensionKernelTools["capabilities"];
42
+
43
+ export const KERNEL_TOOLS_CAPABILITIES = Object.freeze({
44
+ invokeScope: true as const,
45
+ }) satisfies ExtensionKernelTools["capabilities"];
46
+
34
47
  export type KernelToolsCapability = {
48
+ readonly capabilities: ExtensionKernelTools["capabilities"];
35
49
  describe(names: readonly string[]): Promise<KernelToolsDescribeResult>;
36
- invoke(request: KernelToolsInvokeRequest, signal?: AbortSignal): Promise<unknown>;
50
+ invoke: ExtensionKernelTools["invoke"];
37
51
  };
38
52
 
39
53
  export const KERNEL_TOOLS_UNSUPPORTED = {
@@ -1,6 +1,7 @@
1
1
  import { kernelToolCallContext } from "./kernel-tools-context.js";
2
2
  import { kernelToolError } from "./kernel-tools-errors.js";
3
3
  import { createKernelToolPump } from "./kernel-tools-pump.js";
4
+ import { hostDeniedError, hostToolRefusal } from "./kernel-tools-scope.js";
4
5
  import { JsWorkerRuntime } from "./worker-runtime.js";
5
6
 
6
7
  // Mirrors INTERRUPT_ACK_OP and CHILD_LIFECYCLE_OP in src/bridge/reserved.ts (this worker file cannot import TypeScript).
@@ -59,6 +60,12 @@ export function createWorkerCore(transport, options) {
59
60
  const nested = kernelToolCallContext.getStore();
60
61
  if (!nested && activeCell?.interruption) throw activeCell.interruption;
61
62
  if (nested?.signal.aborted) throw nested.signal.reason;
63
+ // A scoped kernel-tool call is refused here, before anything reaches the host bridge, so the
64
+ // closure sees a rejected promise and the parent's own cells keep their full tool surface (#1731).
65
+ if (nested) {
66
+ const refusal = hostToolRefusal(nested.scope, toolName);
67
+ if (refusal) throw hostDeniedError(toolName, nested.callId, refusal);
68
+ }
62
69
  const bag = nested?.pendingTools ?? pendingTools;
63
70
  const callId = `js-${crypto.randomUUID()}`;
64
71
  const promise = new Promise((resolve, reject) => bag.set(callId, { resolve, reject }));
@@ -1,3 +1,5 @@
1
+ import { findShadowedGlobalName } from "./worker-shadow-guard.js";
2
+
1
3
  export function indirectEval(source, filename) {
2
4
  const withPragma = filename ? `${source}\n//# sourceURL=${filename}` : source;
3
5
  const geval = globalThis.eval;
@@ -265,6 +267,12 @@ function rewriteDeclaration(code, declarationStart, start, end, keyword) {
265
267
  const bindings = [];
266
268
  collectPatternNames(pattern, bindings);
267
269
  if (bindings.length === 0) return undefined;
270
+ const shadowed = findShadowedGlobalName(bindings);
271
+ if (shadowed !== undefined) {
272
+ throw new Error(
273
+ `eval cell declares top-level \`${shadowed}\`, but a global with that name already exists in the JS kernel. Persisting it would replace that global for every later cell. Rename the binding (for example \`${shadowed}Local\`), or assign globalThis.${shadowed} explicitly if replacing it is truly intended.`,
274
+ );
275
+ }
268
276
  if (preserveDeclaration) {
269
277
  for (const name of bindings) assignments.push(`globalThis[${JSON.stringify(name)}] = ${name};`);
270
278
  continue;
@@ -0,0 +1,21 @@
1
+ // Snapshot is lazy on purpose: the first guard call is the first cell's
2
+ // transform, when prelude globals exist but no cell-created global does.
3
+ // A module-level snapshot would miss prelude globals; re-snapshotting later
4
+ // would wrongly protect cell-created names that must stay re-declarable.
5
+
6
+ let protectedGlobalNames;
7
+
8
+ function protectedGlobals() {
9
+ if (protectedGlobalNames === undefined) {
10
+ protectedGlobalNames = new Set(Object.getOwnPropertyNames(globalThis));
11
+ }
12
+ return protectedGlobalNames;
13
+ }
14
+
15
+ export function findShadowedGlobalName(bindings) {
16
+ const names = protectedGlobals();
17
+ for (const name of bindings) {
18
+ if (names.has(name)) return name;
19
+ }
20
+ return undefined;
21
+ }
@@ -1,4 +1,5 @@
1
1
  import type { KernelInterruptHandle } from "../../tool/types.ts";
2
+ import type { KernelToolsInvokeOptions } from "../js/kernel-tools-types.ts";
2
3
  import { rejectKernelToolsUnavailable } from "../kernel-tools-unavailable.ts";
3
4
  import type { PendingRun, PythonKernelRunOptions, PythonKernelStartOptions, ResultMessage } from "./kernel-contract.ts";
4
5
  import { failedPythonResult, PythonKernelTransport } from "./transport.ts";
@@ -40,7 +41,7 @@ export class PythonKernel {
40
41
  return rejectKernelToolsUnavailable();
41
42
  }
42
43
 
43
- invokeKernelTool(_request: unknown, _signal?: AbortSignal): Promise<never> {
44
+ invokeKernelTool(_request: unknown, _options?: AbortSignal | KernelToolsInvokeOptions): Promise<never> {
44
45
  return rejectKernelToolsUnavailable();
45
46
  }
46
47
 
@@ -1,6 +1,7 @@
1
1
  import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
2
2
  import { decodeBridgeFrame, encodeBridgeFrame, isKernelToHostMessage } from "../../bridge/protocol.ts";
3
3
  import type { KernelInterruptHandle } from "../../tool/types.ts";
4
+ import type { KernelToolsInvokeOptions } from "../js/kernel-tools-types.ts";
4
5
  import { rejectKernelToolsUnavailable } from "../kernel-tools-unavailable.ts";
5
6
  import { applySessionEnvironment } from "../session-env.ts";
6
7
  import type { KernelResult, KernelRunInput, SubprocessKernelOptions, ToolCallMessage } from "./subprocess-contract.ts";
@@ -78,7 +79,7 @@ export class SubprocessKernel {
78
79
  return rejectKernelToolsUnavailable();
79
80
  }
80
81
 
81
- invokeKernelTool(_request: unknown, _signal?: AbortSignal): Promise<never> {
82
+ invokeKernelTool(_request: unknown, _options?: AbortSignal | KernelToolsInvokeOptions): Promise<never> {
82
83
  return rejectKernelToolsUnavailable();
83
84
  }
84
85
 
@@ -1,8 +1,17 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { join } from "node:path";
3
- import { type AgentToolResult, type ExtensionContext, kernelToolsStorage } from "@code-yeongyu/senpi";
3
+ import {
4
+ type AgentToolResult,
5
+ type ExtensionContext,
6
+ type ExtensionKernelTools,
7
+ kernelToolsStorage,
8
+ } from "@code-yeongyu/senpi";
4
9
  import { DEFAULT_FOREGROUND_WINDOW_SECONDS, defaultCodemodeSettings } from "../config/settings.ts";
5
- import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
10
+ import {
11
+ KERNEL_TOOLS_CAPABILITIES,
12
+ type KernelToolsCapability,
13
+ type KernelToolsDescribeResult,
14
+ } from "../kernels/js/kernel-tools-types.ts";
6
15
  import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
7
16
  import { abortError, CellExecution, defaultTimeoutFactory } from "./cell-execution.ts";
8
17
  import { CellHandler, type CellState } from "./cell-handler.ts";
@@ -220,11 +229,12 @@ function jsKernelTools(kernel: EvalKernel, language: string): KernelToolsCapabil
220
229
  if (language !== "js") return undefined;
221
230
  if (!("describeKernelTools" in kernel) || typeof kernel.describeKernelTools !== "function") return undefined;
222
231
  const js = kernel as EvalKernel & {
223
- describeKernelTools: KernelToolsCapability["describe"];
224
- invokeKernelTool: KernelToolsCapability["invoke"];
232
+ describeKernelTools: (names: readonly string[]) => Promise<KernelToolsDescribeResult>;
233
+ invokeKernelTool: ExtensionKernelTools["invoke"];
225
234
  };
226
235
  return {
236
+ capabilities: KERNEL_TOOLS_CAPABILITIES,
227
237
  describe: (names) => js.describeKernelTools(names),
228
- invoke: (request, signal) => js.invokeKernelTool(request, signal),
229
- };
238
+ invoke: (request, options) => js.invokeKernelTool(request, options),
239
+ } satisfies ExtensionKernelTools;
230
240
  }