@code-yeongyu/senpi-codemode 2026.9.16 → 2026.9.17-3
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 +68 -0
- package/package.json +4 -4
- package/src/bridge/kernel-tools-protocol.ts +19 -0
- package/src/index.ts +7 -0
- package/src/kernels/js/context-manager.ts +10 -3
- package/src/kernels/js/kernel-tools-context.d.ts +4 -0
- package/src/kernels/js/kernel-tools-errors.js +6 -3
- package/src/kernels/js/kernel-tools-errors.ts +21 -3
- package/src/kernels/js/kernel-tools-host.ts +44 -3
- package/src/kernels/js/kernel-tools-pump.js +15 -1
- package/src/kernels/js/kernel-tools-scope.d.ts +9 -0
- package/src/kernels/js/kernel-tools-scope.js +34 -0
- package/src/kernels/js/kernel-tools-types.ts +17 -3
- package/src/kernels/js/worker-core.js +7 -0
- package/src/kernels/js/worker-indirect-eval.js +8 -0
- package/src/kernels/js/worker-shadow-guard.js +21 -0
- package/src/kernels/py/kernel.ts +2 -1
- package/src/kernels/shared/subprocess-kernel.ts +2 -1
- package/src/tool/cell-handler.ts +23 -2
- package/src/tool/run-eval-cell.ts +20 -7
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,74 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.9.17-3] - 2026-09-17
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
## [2026.9.17-2] - 2026-09-17
|
|
28
|
+
|
|
29
|
+
### Breaking Changes
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- 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)
|
|
38
|
+
|
|
39
|
+
### Removed
|
|
40
|
+
|
|
41
|
+
## [2026.9.17] - 2026-09-17
|
|
42
|
+
|
|
43
|
+
### Breaking Changes
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- `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)).
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- 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)).
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
### Removed
|
|
56
|
+
|
|
57
|
+
## [2026.9.16-3] - 2026-09-16
|
|
58
|
+
|
|
59
|
+
### Breaking Changes
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
### Removed
|
|
68
|
+
|
|
69
|
+
## [2026.9.16-2] - 2026-09-16
|
|
70
|
+
|
|
71
|
+
### Breaking Changes
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- The JavaScript kernel-tool capability now reaches host tools called from inside a running eval cell. A cell that registers `tool(fn)` can hand those functions to its in-process children through `task`/`agent`/`workpool`, because `ExtensionContext.kernelTools` resolves for the duration of each host tool call the cell makes. It was undefined at the dispatch point — the worker's message loop ran outside the `kernelToolsStorage` scope that only covered the awaited run chain — so every grant was refused with `tools_unavailable` and the capability shipped in the previous release could not be used ([#1754](https://github.com/code-yeongyu/senpi/issues/1754)).
|
|
80
|
+
|
|
81
|
+
### Removed
|
|
82
|
+
|
|
15
83
|
## [2026.9.16] - 2026-09-16
|
|
16
84
|
|
|
17
85
|
### Breaking Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.17-3",
|
|
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.
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.17-3",
|
|
34
34
|
"typebox": "1.3.27"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
37
|
+
"@code-yeongyu/senpi": "2026.9.17-3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.9.17-3"
|
|
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 {
|
|
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(
|
|
76
|
-
|
|
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(
|
|
25
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
+
}
|
package/src/kernels/py/kernel.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
82
|
+
invokeKernelTool(_request: unknown, _options?: AbortSignal | KernelToolsInvokeOptions): Promise<never> {
|
|
82
83
|
return rejectKernelToolsUnavailable();
|
|
83
84
|
}
|
|
84
85
|
|
package/src/tool/cell-handler.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AgentToolResult,
|
|
3
|
+
type ExtensionContext,
|
|
4
|
+
kernelToolsStorage,
|
|
5
|
+
sanitizeTerminalLabel,
|
|
6
|
+
} from "@code-yeongyu/senpi";
|
|
2
7
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
8
|
import { RESERVED_SCHEMA_TOOL } from "../bridge/reserved.ts";
|
|
4
9
|
import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
|
|
@@ -8,6 +13,7 @@ import { appendSchemaHint } from "../bridges/schema-hint.ts";
|
|
|
8
13
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
9
14
|
import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
|
|
10
15
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
16
|
+
import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
|
|
11
17
|
import {
|
|
12
18
|
boundToolCallArgs,
|
|
13
19
|
capCodePoints,
|
|
@@ -39,6 +45,8 @@ export interface CellBridgeRuntime {
|
|
|
39
45
|
readonly ctx: ExtensionContext;
|
|
40
46
|
readonly artifactPath?: string;
|
|
41
47
|
readonly imageResizer?: EvalImageResizer;
|
|
48
|
+
/** This cell's live kernel-tool capability; only a JS kernel has one (#1754). */
|
|
49
|
+
readonly kernelTools?: KernelToolsCapability;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
export class CellHandler {
|
|
@@ -81,7 +89,7 @@ export class CellHandler {
|
|
|
81
89
|
this.#resultBuilder.display(message);
|
|
82
90
|
return;
|
|
83
91
|
case "tool-call": {
|
|
84
|
-
const pending = this.#
|
|
92
|
+
const pending = this.#dispatchToolCall(message);
|
|
85
93
|
this.#state.pendingBridgeCalls.push(pending);
|
|
86
94
|
await pending;
|
|
87
95
|
return;
|
|
@@ -114,6 +122,19 @@ export class CellHandler {
|
|
|
114
122
|
return this.#resultBuilder.liveResult();
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The worker's message loop fires outside the async context `run-eval-cell.ts` enters around the
|
|
127
|
+
* awaited run chain, so the capability has to be entered here — around the whole dispatch, including
|
|
128
|
+
* the reserved agent()/output() bridges where a host task tool resolves the parent's kernel tools —
|
|
129
|
+
* for exactly the duration of each host tool call this cell makes (#1754). Without a capability
|
|
130
|
+
* (py/rb/jl) the store stays empty and `ctx.kernelTools` remains undefined.
|
|
131
|
+
*/
|
|
132
|
+
async #dispatchToolCall(message: Extract<KernelToHostMessage, { type: "tool-call" }>): Promise<void> {
|
|
133
|
+
const kernelTools = this.#runtime.kernelTools;
|
|
134
|
+
if (kernelTools === undefined) return await this.#handleToolCall(message);
|
|
135
|
+
return await kernelToolsStorage.run(kernelTools, async () => await this.#handleToolCall(message));
|
|
136
|
+
}
|
|
137
|
+
|
|
117
138
|
async #handleToolCall(message: Extract<KernelToHostMessage, { type: "tool-call" }>): Promise<void> {
|
|
118
139
|
const startedAt = Date.now();
|
|
119
140
|
const metric = createToolCallMetric(message.toolName, startedAt);
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
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
|
|
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";
|
|
@@ -167,6 +176,9 @@ async function executeCell(
|
|
|
167
176
|
void pending.catch((error: unknown) => execution.cancel(error));
|
|
168
177
|
}),
|
|
169
178
|
);
|
|
179
|
+
// Computed before the handler so the cell's capability can be entered per host tool call from the
|
|
180
|
+
// worker's message loop, which runs outside the `kernelToolsStorage.run` context below (#1754).
|
|
181
|
+
const kernelTools = jsKernelTools(kernel, invocation.input.language);
|
|
170
182
|
const runBound = async (): Promise<AgentToolResult<EvalToolDetails>> => {
|
|
171
183
|
execution.setKernel(kernel);
|
|
172
184
|
const activeHandler = new CellHandler(kernel, state, {
|
|
@@ -179,6 +191,7 @@ async function executeCell(
|
|
|
179
191
|
? {}
|
|
180
192
|
: { artifactPath: join(options.artifactsDir, `eval-${randomUUID()}.log`) }),
|
|
181
193
|
...(options.imageResizer === undefined ? {} : { imageResizer: options.imageResizer }),
|
|
194
|
+
...(kernelTools === undefined ? {} : { kernelTools }),
|
|
182
195
|
});
|
|
183
196
|
handler = activeHandler;
|
|
184
197
|
cellManager.markRunning(
|
|
@@ -198,7 +211,6 @@ async function executeCell(
|
|
|
198
211
|
await execution.wait(Promise.all(state.pendingBridgeCalls));
|
|
199
212
|
return await handler.finalize(result);
|
|
200
213
|
};
|
|
201
|
-
const kernelTools = jsKernelTools(kernel, invocation.input.language);
|
|
202
214
|
return kernelTools ? await kernelToolsStorage.run(kernelTools, runBound) : await runBound();
|
|
203
215
|
} catch (error) {
|
|
204
216
|
if (handler && error instanceof Error && error.name === "CodemodeSessionDisposedError")
|
|
@@ -217,11 +229,12 @@ function jsKernelTools(kernel: EvalKernel, language: string): KernelToolsCapabil
|
|
|
217
229
|
if (language !== "js") return undefined;
|
|
218
230
|
if (!("describeKernelTools" in kernel) || typeof kernel.describeKernelTools !== "function") return undefined;
|
|
219
231
|
const js = kernel as EvalKernel & {
|
|
220
|
-
describeKernelTools:
|
|
221
|
-
invokeKernelTool:
|
|
232
|
+
describeKernelTools: (names: readonly string[]) => Promise<KernelToolsDescribeResult>;
|
|
233
|
+
invokeKernelTool: ExtensionKernelTools["invoke"];
|
|
222
234
|
};
|
|
223
235
|
return {
|
|
236
|
+
capabilities: KERNEL_TOOLS_CAPABILITIES,
|
|
224
237
|
describe: (names) => js.describeKernelTools(names),
|
|
225
|
-
invoke: (request,
|
|
226
|
-
};
|
|
238
|
+
invoke: (request, options) => js.invokeKernelTool(request, options),
|
|
239
|
+
} satisfies ExtensionKernelTools;
|
|
227
240
|
}
|