feat(eval): call-scoped host-tool policy for kernel-tool invoke - #1765
Merged
Merged
Conversation
Covers the policy seam (allow/deny/absent, deny-wins, exact name match, typed refusal payload), the live-worker kernel seam (denied nested host call never reaches the bridge, allowed one does, scope drops at settle, parent cell and queue unaffected) and the real-worker e2e where a host tool invokes the parent closures under scope.tools.deny. Refs #1731
kernelTools.invoke now takes an optional per-call execution scope
({ scope: { tools: { allow, deny } } }) that the worker applies to the
nested host calls the invoked closure makes. A call outside the scope is
refused inside the worker with kernel_tool_host_denied carrying
{ tool, call_id, reason }, so 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. A bare AbortSignal still
works and an unscoped call posts the message it always did; consumers
gate on kernelTools.capabilities.invokeScope.
Fixes #1731
This was referenced Sep 16, 2026
DescodeNet
pushed a commit
to DescodeNet/senpi
that referenced
this pull request
Sep 17, 2026
…ope surface PR code-yeongyu#1765 shipped kernelTools.invoke(request, { signal?, scope? }) and capabilities.invokeScope, but ExtensionContext.kernelTools still declared invoke(request, signal?: AbortSignal) with no capabilities. Alias ExtensionKernelTools to senpi-codemode KernelToolsCapability so the public typed surface cannot drift. Refs code-yeongyu#1731
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
kernelTools.invoke(request, options?)now accepts a per-call execution scope for the nested host calls the invoked closure makes:While that invocation is active, a
tool.<name>()outside the scope is refused inside the worker — before anything reaches the host bridge — with the typed errorkernel_tool_host_deniedcarrying{ tool, call_id, reason: "allow" | "deny" }on the invoking call's channel. The closure sees a rejected promise; the parent's own top-level cells and the parent run queue keep the parent's full tool surface.Semantics:
denywins overallow; anallowlist refuses every host tool it does not name (an emptyallowrefuses everything); a malformed list fails closed instead of widening the call.AbortSignal, and a call without a scope posts exactly the frame it always did.Consumers detect the capability with
kernelTools.capabilities.invokeScope === true(KERNEL_TOOLS_CAPABILITIES), so a host that still runs an older runtime can keep refusing narrowed grants instead of sending an option that would be silently ignored.Tests
Written failing-first (
test(eval): RED contract …commit), then made green:test/kernel-tools-scope.test.js— policy seam: allow/deny/absent, deny-wins, exact name match, malformed list fails closed, refusal payload.test/kernel-tools-invoke-scope.test.ts— live worker at the kernel seam: denied nested call never reaches the bridge, allowed one does, closure observes the rejection, unscoped invoke unchanged, scope dropped at settle, parent cell + queue + later cells unaffected.test/kernel-tools-invoke-scope-e2e.test.ts— real worker through the eval tool, in the style ofkernel-tools-host-dispatch.test.ts(fix(eval): enter the kernel-tool capability at each host tool dispatch #1755): a host tool resolvesctx.kernelTools, invokes the parent's closures withscope.tools.deny: ["write"]; the nestedtool.readsucceeds andtool.writefails closed withkernel_tool_host_denied; the host never sees awritecall.scripts/qa-kernel-tool-scope.ts— real-surface QA driver for the kernel/bridge change:Verified on a Darwin arm64 runner with node 24+/bun 1.4.2: full
packages/senpi-codemodesuite 132 files / 929 tests passed (1 file, 14 tests skipped), roottsc --noEmitclean,biome check --error-on-warnings,check:ts-importsandcheck:entry-graphsclean.Notes
packages/senpi-codemodechanged. A typed consumer readingExtensionContext.kernelTools(declared inpackages/coding-agent) still needs a cast to pass{ scope }or readcapabilities; widening that declaration is a separate, non-blocking follow-up.Fixes #1731
Summary by cubic
Adds call-scoped host-tool policy to
kernelTools.invoke, so a consumer granting a parent kernel tool to a child can narrow which host tools the invoked closure reaches instead of choosing between refusing the grant and letting it run with the parent's full permissions (#1731). A nestedtool.<name>()outside the scope is refused inside the worker withkernel_tool_host_deniedcarrying{ tool, call_id, reason }on the invoking call's channel — the closure sees a rejected promise, the refusal never reaches the host bridge, and the parent's own cells and queue keep the full tool surface. Nothing outsidepackages/senpi-codemodechanged; a typed consumer readingExtensionContext.kernelToolsstill needs a cast to pass{ scope }or readcapabilities.Semantics
denywins overallow; anallowlist refuses every host tool it does not name, and an empty or malformed list fails closed.kernel_tool_stale.AbortSignal, and a call without a scope posts exactly the frame it always did.kernelTools.capabilities.invokeScope === true, so a host on an older runtime can refuse narrowed grants instead of sending an option that would be silently ignored.Written for commit 3ea9e79. Summary will update on new commits.