@code-yeongyu/senpi-codemode 2026.10.8 → 2026.10.10

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 (51) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +100 -4
  3. package/package.json +4 -4
  4. package/src/bridge/http-server.ts +9 -1
  5. package/src/bridge/protocol.ts +4 -0
  6. package/src/bridges/eval-virtual-schemas.ts +1 -0
  7. package/src/environments/js-environments.ts +208 -0
  8. package/src/environments/js-installer.ts +268 -0
  9. package/src/environments/js-revision-files.ts +91 -0
  10. package/src/environments/no-links.ts +132 -0
  11. package/src/environments/project-resolves.ts +54 -0
  12. package/src/environments/py-installer.ts +64 -4
  13. package/src/environments/python-environments.ts +85 -0
  14. package/src/environments/revision-store.ts +38 -7
  15. package/src/extension/bridge-tool-call.ts +2 -0
  16. package/src/extension/cell-kernel-tools.ts +28 -0
  17. package/src/extension/kernel-replacement.ts +20 -0
  18. package/src/extension/runtime-factory.ts +21 -0
  19. package/src/extension/session-manager-proxy.ts +6 -0
  20. package/src/extension/session-manager.ts +10 -2
  21. package/src/index.ts +2 -0
  22. package/src/kernels/jl/prelude.jl +6 -1
  23. package/src/kernels/js/cell-dispatch.ts +44 -0
  24. package/src/kernels/js/context-manager.ts +25 -15
  25. package/src/kernels/js/host-entries.ts +62 -0
  26. package/src/kernels/js/local-module-loader.ts +21 -5
  27. package/src/kernels/js/worker-core.js +2 -0
  28. package/src/kernels/js/worker-package-resolve.js +153 -0
  29. package/src/kernels/js/worker-runtime.js +3 -1
  30. package/src/kernels/js/workpool.js +5 -2
  31. package/src/kernels/py/host-cell.ts +8 -31
  32. package/src/kernels/py/kernel.ts +8 -2
  33. package/src/kernels/py/prelude.py +112 -9
  34. package/src/kernels/py/transport.ts +13 -1
  35. package/src/kernels/rb/workpool.rb +7 -1
  36. package/src/kernels/sandbox/sandbox-cell.ts +116 -0
  37. package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +1 -0
  38. package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +14 -1
  39. package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +4 -1
  40. package/src/kernels/sandbox/vendor/pi-codemode/types.ts +2 -0
  41. package/src/kernels/shared/cell-source-at-start.ts +18 -0
  42. package/src/kernels/shared/host-cell.ts +57 -0
  43. package/src/tool/cell-handler.ts +10 -12
  44. package/src/tool/eval-request.ts +23 -1
  45. package/src/tool/eval-tool-options.ts +6 -0
  46. package/src/tool/eval-tool.ts +16 -4
  47. package/src/tool/load-cell.ts +88 -0
  48. package/src/tool/magic-cell-host.ts +123 -0
  49. package/src/tool/magic-cells.ts +75 -0
  50. package/src/tool/run-eval-cell.ts +57 -0
  51. package/src/tool/types.ts +48 -1
@@ -7,12 +7,14 @@ import {
7
7
  kernelToolsStorage,
8
8
  } from "@code-yeongyu/senpi";
9
9
  import type { KernelToHostMessage } from "../bridge/protocol.ts";
10
+ import { resolveSandbox } from "../config/feature-settings.ts";
10
11
  import { DEFAULT_FOREGROUND_WINDOW_SECONDS, defaultCodemodeSettings } from "../config/settings.ts";
11
12
  import {
12
13
  KERNEL_TOOLS_CAPABILITIES,
13
14
  type KernelToolsCapability,
14
15
  type KernelToolsDescribeResult,
15
16
  } from "../kernels/js/kernel-tools-types.ts";
17
+ import { sandboxCellExecutor } from "../kernels/sandbox/sandbox-cell.ts";
16
18
  import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
17
19
  import { abortError, CellExecution, defaultTimeoutFactory } from "./cell-execution.ts";
18
20
  import { CellHandler, type CellState } from "./cell-handler.ts";
@@ -23,6 +25,8 @@ import { EvalKernelResetRefusedError } from "./eval-kernel-reset-refused-error.t
23
25
  import { evalTimeoutBehavior } from "./eval-request.ts";
24
26
  import type { CreateEvalToolOptions, EvalCellInvocation } from "./eval-tool-options.ts";
25
27
  import { describeTimeoutState } from "./interrupt-note.ts";
28
+ import { loadCell } from "./load-cell.ts";
29
+ import { planMagicCell } from "./magic-cell-host.ts";
26
30
  import type { EvalKernel, EvalToolDetails } from "./types.ts";
27
31
 
28
32
  export async function runEvalCell(
@@ -205,11 +209,21 @@ async function executeCell(
205
209
  const pending = handler.handle(message);
206
210
  void pending.catch((error: unknown) => execution.cancel(error));
207
211
  };
212
+ let releaseCellKernelTools: (() => void) | undefined;
208
213
  try {
209
214
  const kernel = await execution.wait(options.kernelManager.getKernel(invocation.input.language, onMessage));
210
215
  // Computed before the handler so its construction-time snapshot captures this cell's capability.
211
216
  // Worker messages later restore that snapshot before calling host tools (#1754, #2512).
212
217
  const kernelTools = kernelToolsFor(kernel);
218
+ // Subprocess kernels call the host over the bridge, outside this async context. Their calls carry a fresh secret
219
+ // minted for this run and sent only to this cell's kernel, never the model-visible cell id, so code in another
220
+ // kernel cannot name this cell into its tools. JS calls already run inside this cell's scope (#1754).
221
+ const bridgeCellToken =
222
+ kernelTools === undefined || invocation.input.language === "js" ? undefined : randomUUID();
223
+ releaseCellKernelTools =
224
+ kernelTools === undefined || bridgeCellToken === undefined
225
+ ? undefined
226
+ : options.kernelManager.bindCellKernelTools?.(bridgeCellToken, kernelTools);
213
227
  const runBound = async (): Promise<AgentToolResult<EvalToolDetails>> => {
214
228
  const queue = kernel.queueSnapshot();
215
229
  state.queuedBehind = [...(queue.activeCellId === null ? [] : [queue.activeCellId]), ...queue.queuedCellIds];
@@ -249,10 +263,51 @@ async function executeCell(
249
263
  }
250
264
  if (invocation.input.reset) await execution.wait(kernel.reset());
251
265
  execution.setKernel(kernel);
266
+ const magic = planMagicCell(
267
+ invocation.input.language,
268
+ invocation.input.code,
269
+ options.pythonEnvironments,
270
+ options.jsEnvironments,
271
+ );
272
+ // Resolved when the cell's turn comes in the kernel's queue: a %load reads the file the cells ahead of it
273
+ // wrote, and a refusal settles in queue order like any cell.
274
+ const loadOptions = { cwd: invocation.ctx.cwd, artifactsDir: options.artifactsDir };
275
+ // An isolated cell never runs a file outside its sandbox: %load there is refused.
276
+ const isolatedLoad = invocation.input.isolate === true && magic.kind === "load";
277
+ const resolveAtStart = isolatedLoad
278
+ ? () => ({
279
+ ok: false as const,
280
+ message:
281
+ "isolate: true cannot run a %load cell: an isolated cell sees no host files, and loading one would break that isolation. Run %load in a normal cell, or paste the code into the isolated cell.",
282
+ })
283
+ : magic.kind === "load"
284
+ ? () => loadCell(magic.target, loadOptions)
285
+ : magic.kind === "refused"
286
+ ? () => ({ ok: false as const, message: magic.message })
287
+ : undefined;
288
+ const isolated =
289
+ invocation.input.isolate === true && resolveAtStart === undefined
290
+ ? sandboxCellExecutor(invocation.input.code, {
291
+ sandbox: resolveSandbox(options.settings ?? {}),
292
+ executeTool: options.executeTool,
293
+ toolNames: () => (options.listTools?.() ?? []).map((tool) => tool.name),
294
+ describeTool: (name) => options.listTools?.().find((tool) => tool.name === name)?.description,
295
+ })
296
+ : undefined;
297
+ const environments = invocation.input.language === "py" ? options.pythonEnvironments : undefined;
298
+ const envRoot = environments === undefined ? undefined : () => environments.activeRoot ?? "";
299
+ const jsEnvironments = invocation.input.language === "js" ? options.jsEnvironments : undefined;
300
+ const packageRoot = jsEnvironments === undefined ? undefined : () => jsEnvironments.packageRoot;
252
301
  const result = await execution.wait(
253
302
  kernel.run({
254
303
  cellId: invocation.cellId,
255
304
  code: invocation.input.code,
305
+ ...(resolveAtStart === undefined ? {} : { resolveAtStart }),
306
+ ...(magic.kind === "host" ? { host: magic.executor } : {}),
307
+ ...(isolated === undefined ? {} : { host: isolated }),
308
+ ...(envRoot === undefined ? {} : { envRoot }),
309
+ ...(packageRoot === undefined ? {} : { packageRoot }),
310
+ ...(bridgeCellToken === undefined ? {} : { bridgeCellToken }),
256
311
  kernelPreludes: options.kernelPreludes?.(),
257
312
  onMessage,
258
313
  onStarted: () => {
@@ -285,6 +340,8 @@ async function executeCell(
285
340
  if (error instanceof Error && error.name === "TimeoutError") throw await describeTimeoutState(error, execution);
286
341
  throw error;
287
342
  } finally {
343
+ // First, so nothing below can skip it: from here on a host call carrying this run's secret gets no kernel tools.
344
+ releaseCellKernelTools?.();
288
345
  state.active = false;
289
346
  bridgeAbortController.abort();
290
347
  execution.finish();
package/src/tool/types.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  DEFAULT_RUN_BUDGET_SECONDS,
9
9
  defaultCodemodeSettings,
10
10
  } from "../config/settings.ts";
11
+ import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
11
12
  import type { TruncationMeta } from "../output/output-meta.ts";
12
13
 
13
14
  export const evalLanguageOrder = ["js", "py", "rb", "jl"] as const;
@@ -51,6 +52,8 @@ export interface EvalToolInput {
51
52
  readonly timeout?: number;
52
53
  readonly on_timeout?: "detach" | "error";
53
54
  readonly reset?: boolean;
55
+ /** Run in a fresh sandbox VM instead of the persistent kernel; accepted only while `sandbox.enabled` is on. */
56
+ readonly isolate?: boolean;
54
57
  }
55
58
 
56
59
  export interface EvalListInput {
@@ -116,14 +119,26 @@ const fullEvalInputSchema = Type.Object(
116
119
  /** Runtime accepts a discriminated run/control union. */
117
120
  export type EvalInputSchema = TUnsafe<EvalToolRequest> & Pick<typeof fullEvalInputSchema, "properties">;
118
121
 
122
+ const ISOLATE_FIELD_DESCRIPTION =
123
+ "js only. Fresh sandbox per call: no persistence, no fs/net/process/timers; only tool.* plus print/display.";
124
+
119
125
  export function createEvalInputSchema(
120
126
  enabled: EnabledEvalLanguages,
121
127
  deadlines: EvalDeadlineSeconds = defaultEvalDeadlineSeconds,
128
+ options: { readonly sandbox?: boolean } = {},
122
129
  ): EvalInputSchema {
123
130
  const languages = enabledLanguageList(enabled);
124
131
  if (languages.length === 0) throw new Error("eval requires at least one enabled language");
125
132
  const languageSchema = evalLanguageUnion(languages);
126
- const properties = evalInputProperties(languageSchema, deadlines);
133
+ // The sandbox field exists in the schema only while sandbox cells are turned on, so the default schema
134
+ // (and the prompt built from it) stays exactly what it was.
135
+ const properties =
136
+ options.sandbox === true && languages.includes("js")
137
+ ? {
138
+ ...evalInputProperties(languageSchema, deadlines),
139
+ isolate: Type.Optional(Type.Boolean({ description: ISOLATE_FIELD_DESCRIPTION })),
140
+ }
141
+ : evalInputProperties(languageSchema, deadlines);
127
142
  return Type.Unsafe<EvalToolRequest>(
128
143
  Type.Object(properties, {
129
144
  // Keep branches self-contained for Mistral-hosted GLM (#2240).
@@ -167,8 +182,35 @@ export interface EvalKernelRunInput {
167
182
  readonly onMessage?: (message: KernelToHostMessage) => void;
168
183
  /** Globals of the tools active when the cell was submitted; kernels without preludes ignore them. */
169
184
  readonly kernelPreludes?: readonly KernelPreludeContribution[];
185
+ /**
186
+ * Python only: read when the cell starts running (not when it was queued), so a cell queued behind `%pip`
187
+ * sees the revision it published. `""` means no environment: the previous revision leaves the import path.
188
+ */
189
+ readonly envRoot?: () => string;
190
+ /** The file a `%load` cell runs: tracebacks name it and its relative imports resolve from its directory. */
191
+ readonly sourceFile?: string;
192
+ /**
193
+ * JavaScript only: the session's managed package revision; bare imports that do not resolve from cwd fall back to
194
+ * it. Read when the cell starts running (not when it was queued), so a cell queued behind `%bun add` resolves the
195
+ * revision that install published.
196
+ */
197
+ readonly packageRoot?: () => string | undefined;
198
+ /**
199
+ * Called when the cell's turn comes in the kernel's queue, never earlier: what to run instead of `code` (a `%load`
200
+ * cell's file), or a refusal that settles the cell as failed in queue order.
201
+ */
202
+ readonly resolveAtStart?: () => CellSourceAtStart;
203
+ /**
204
+ * Subprocess kernels attach this to the cell's host calls, and the host gives those calls the cell's kernel tools.
205
+ * A fresh secret per run, sent only to the kernel that runs the cell; never a model-visible id.
206
+ */
207
+ readonly bridgeCellToken?: string;
170
208
  }
171
209
 
210
+ export type CellSourceAtStart =
211
+ | { readonly ok: true; readonly code: string; readonly sourceFile?: string }
212
+ | { readonly ok: false; readonly message: string };
213
+
172
214
  export interface KernelInterruptHandle {
173
215
  /** Resolves once the kernel knows whether user state survived the interrupt. */
174
216
  readonly stateRetained: Promise<boolean>;
@@ -205,6 +247,11 @@ export interface EvalKernelManager {
205
247
  * Identity-checked, so releasing a superseded listener never unbinds a newer cell's listener.
206
248
  */
207
249
  releaseKernelListener?(language: EvalLanguage, onMessage: (message: KernelToHostMessage) => void): void;
250
+ /**
251
+ * Binds a running cell's kernel-tools capability to the host calls that carry `token` (subprocess kernels reach
252
+ * the host over the bridge, outside the cell's async context). The returned release runs when the cell settles.
253
+ */
254
+ bindCellKernelTools?(token: string, capability: KernelToolsCapability): () => void;
208
255
  }
209
256
 
210
257
  export type ExecuteTool = (