Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions changes/unreleased/4390.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!-- changes-log: packages/coding-agent/src/core/extensions/changes.md -->
## 2026-10-26 - Host-bound kernel-tool invoker identity (senpi#4390, part of #4309)

### What changed

- `packages/coding-agent/src/core/extensions/kernel-tools-context.ts`: add host-only invocation identity.
- `packages/coding-agent/src/core/extensions/runner.ts`: stamp the invoking SessionManager's identity after every caller-supplied option.
- `packages/coding-agent/src/core/extensions/wrapper.ts`: bind dispatcher-free tool calls to their host context.
- `packages/coding-agent/src/core/extensions/loader.ts`: bind registered event and hook handlers to their emitting host context without replacing their registered-handler metadata. A stale context (session replacement reports `session_extensions_removed` on the invalidated runner) binds no invoker, so its handler still runs without borrowing the emitting scope's identity.

### Why

- `packages/coding-agent/src/core/extensions/kernel-tools-context.ts`: cross-session kernel permissions need a host-authenticated caller.
- `packages/coding-agent/src/core/extensions/runner.ts`: cell arguments and inherited capabilities must not select the permission session.
- `packages/coding-agent/src/core/extensions/wrapper.ts`: a capability captured in a parent still runs with the child's host identity.
- `packages/coding-agent/src/core/extensions/loader.ts`: a child handler must not inherit a parent's task-tool invoker and bypass cross-session admission.

### Why an extension could not handle it

- `packages/coding-agent/src/core/extensions/kernel-tools-context.ts`: this contract crosses the dispatcher and kernel host.
- `packages/coding-agent/src/core/extensions/runner.ts`: only the dispatcher owns the calling session's SessionManager.
- `packages/coding-agent/src/core/extensions/wrapper.ts`: the host wrapper owns the fallback execution boundary.
- `packages/coding-agent/src/core/extensions/loader.ts`: handler registration is the common host boundary for every event and hook dispatch path.

### Expected merge conflict zones

- `packages/coding-agent/src/core/extensions/kernel-tools-context.ts`: invocation options.
- `packages/coding-agent/src/core/extensions/runner.ts`: the lazy kernelTools context getter.
- `packages/coding-agent/src/core/extensions/wrapper.ts`: registered tool execution.
- `packages/coding-agent/src/core/extensions/loader.ts`: the owned registered-handler wrapper.

<!-- changes-log: packages/coding-agent/src/core/changes.md -->
## 2026-10-26 - Bind kernel invoker authority at tool dispatch (senpi#4390, part of #4309)

### What changed

- `packages/coding-agent/src/core/tool-dispatch/tool-dispatcher.ts`: bind the calling session's identity in host async context around server tool execution; context-free wrappers and child/observer contexts without a session manager retain the owner path without inheriting an ambient invoker.

### Why

- `packages/coding-agent/src/core/tool-dispatch/tool-dispatcher.ts`: inherited parent capabilities must use the child's dispatch identity, not the session that created the capability.

### Why an extension could not handle it

- `packages/coding-agent/src/core/tool-dispatch/tool-dispatcher.ts`: the dispatcher has the actual calling SessionManager; tool arguments are untrusted.

### Expected merge conflict zones

- `packages/coding-agent/src/core/tool-dispatch/tool-dispatcher.ts`: BaseToolDispatcher.runOnServer.

<!-- changes-log: packages/senpi-codemode/changes.md -->
## 2026-10-26 - Cross-session kernel-tool permission authority (senpi#4390, part of #4309)

### What changed

- `packages/senpi-codemode/src/kernels/js/permission-host.ts`, `kernel-contract.ts`, and `context-manager.ts`: mint invoker tokens and reject cross-session invocations before dispatch when either session enforces. A settled inactive invocation keeps its invoker binding until the worker retires, so that session's revocation still reaches the continuations it left behind.
- `packages/senpi-codemode/src/kernels/js/permission-worker-frames.ts`: discover transferred MessagePorts without spreading collections, so a frame with an array or Set past the engine's argument limit is still delivered (#4387 review follow-up).
- `packages/senpi-codemode/src/kernels/shared/invocation-authority.ts` and `kernel-tools-pump.ts`: resolve host-only session snapshots, forward invocation options into the host hook, and release authority on settlement. Cross-session refusals identify the authority barrier and enforcing session, not an operation matched by the first deny rule.
- `packages/senpi-codemode/src/kernels/py/kernel.ts` and `kernel-contract.ts`: apply the same refusal before Python tool bodies execute.
- `packages/senpi-codemode/src/kernels/js/permission-core.md`: document the integration resolver for invoking sessions without kernels.
- `packages/senpi-codemode/test/gate/allowlist.json` and `baseline.json`: approve only this PR's two invocation-authority import memberships and invocation contracts, preserving every inherited baseline cell and approval.

### Why

- Kernel tools retained the owning session's authority even when another session invoked them. Either session's deny rules must prevent sharing retained kernel resources.

### Why an extension could not handle it

- Caller identity and invocation admission belong to the dispatcher, shared pump, and kernel host; cell code cannot authenticate its own caller.

### Expected merge conflict zones

- Kernel-tool invocation, permission token minting, Python startup/close, and the shared host pump.
- The real permission-system registry is connected by the integration PR through the typed session resolver; this PR remains stacked on the JS permission core.
- Known limitation: reads performed by the runtime's native module resolver (bare packages, tsconfig aliases, package.json #imports, symlinked packages) are not covered by this work; tracked in #4380.

<!-- changes-log: packages/coding-agent/CHANGELOG.md -->
### Added
- Bind kernel-tool invocation identity to the dispatcher's own session rather than tool arguments ([#4390](https://github.com/code-yeongyu/senpi/pull/4390), part of #4309).

<!-- changes-log: packages/senpi-codemode/CHANGELOG.md -->
### Added
- Refuse cross-session JavaScript and Python kernel tools before their bodies run when either session enforces a deny rule, and mint invoker-scoped JS tokens when neither enforces ([#4390](https://github.com/code-yeongyu/senpi/pull/4390), part of #4309).
- Known limitation: reads performed by the runtime's native module resolver (bare packages, tsconfig aliases, package.json #imports, symlinked packages) are not covered by this work; tracked in #4380.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export type KernelToolInvokeScope = {
export type KernelToolInvokeOptions = {
signal?: AbortSignal;
scope?: KernelToolInvokeScope;
/** Host dispatcher identity, never read from a tool's arguments or cell data. */
invoker?: { readonly sessionId: string };
};

export type ExtensionKernelTools = {
Expand All @@ -31,3 +33,6 @@ export type ExtensionKernelTools = {
};

export const kernelToolsStorage = new AsyncLocalStorage<ExtensionKernelTools>();

/** The active host dispatcher, including when it executes a capability captured in another session. */
export const kernelToolInvokerStorage = new AsyncLocalStorage<{ readonly sessionId: string }>();
28 changes: 27 additions & 1 deletion packages/coding-agent/src/core/extensions/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
extensionModuleImporter,
rememberExtensionFactory,
} from "./extension-module-cache.ts";
import { kernelToolInvokerStorage } from "./kernel-tools-context.ts";
import { createLifecycleApi } from "./lifecycle-api.ts";
import { withProviderLoginStart } from "./provider-login-start.ts";
import { StaleExtensionContextError } from "./stale-context-error.ts";
Expand All @@ -65,6 +66,7 @@ import type {
EntryRendererOptions,
Extension,
ExtensionAPI,
ExtensionContext,
ExtensionFactory,
ExtensionRuntime,
ExtensionSessionProfile,
Expand All @@ -76,6 +78,7 @@ import type {
MemoryReporter,
MessageRenderer,
PendingProviderRegistration,
ProjectTrustContext,
ProviderConfig,
RegisteredChildModule,
RegisteredCommand,
Expand Down Expand Up @@ -396,6 +399,21 @@ export function drainPendingProviderRegistrations(runtime: ExtensionRuntime): Pe
* Registration methods write to the extension object.
* Action methods delegate to the shared runtime.
*/
/**
* The session a handler's host context belongs to. A stale context (session replacement still reports
* `session_extensions_removed` on the invalidated runner) has none: its handler runs with no ambient
* invoker, and its own guarded getters, kernelTools included, still refuse it.
*/
function handlerSessionId(context: ExtensionContext | ProjectTrustContext | undefined): string | undefined {
if (!context || !("sessionManager" in context)) return undefined;
try {
return context.sessionManager?.getSessionId();
} catch (error) {
if (error instanceof StaleExtensionContextError) return undefined;
throw error;
}
}

function createExtensionAPI(
extension: Extension,
runtime: ExtensionRuntime,
Expand Down Expand Up @@ -435,7 +453,15 @@ function createExtensionAPI(
options?: BeforeAgentStartHandlerOptions & ContextHandlerOptions,
): () => void {
assertActive();
const registeredHandler: HandlerFn = ownedBy(extension, (...args: unknown[]) => handler(...args));
const registeredHandler: HandlerFn = ownedBy(extension, (...args: unknown[]) => {
// A child event can run inside its parent's task-tool async scope.
// Bind the host context here, preserving registered-handler metadata.
const sessionId = handlerSessionId(args[1] as ExtensionContext | ProjectTrustContext | undefined);
const invoke = () => handler(...args);
return sessionId
? kernelToolInvokerStorage.run({ sessionId }, invoke)
: kernelToolInvokerStorage.exit(invoke);
});
const list = extension.handlers.get(event) ?? [];
list.push(registeredHandler);
extension.handlers.set(event, list);
Expand Down
18 changes: 16 additions & 2 deletions packages/coding-agent/src/core/extensions/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
runBudgetedHandler,
runBudgetedSequence,
} from "./handler-budget.ts";
import { kernelToolsStorage } from "./kernel-tools-context.ts";
import { type ExtensionKernelTools, kernelToolInvokerStorage, kernelToolsStorage } from "./kernel-tools-context.ts";
import { drainPendingProviderRegistrations } from "./loader.ts";
import { runSessionMoveHandlers, type SessionMoveEmission, type SessionMoveFailure } from "./session-move-emit.ts";
import { SessionStartTurnGate } from "./session-start-turn-gate.ts";
Expand Down Expand Up @@ -1579,7 +1579,21 @@ export class ExtensionRunner {
},
get kernelTools() {
runner.assertActive();
return kernelToolsStorage.getStore();
const tools = kernelToolsStorage.getStore();
if (!tools) return undefined;
return {
capabilities: tools.capabilities,
describe: (names) => tools.describe(names),
invoke: (request, options) => {
runner.assertActive();
return tools.invoke(request, {
...(options && ("aborted" in options ? { signal: options } : options)),
invoker: kernelToolInvokerStorage.getStore() ?? {
sessionId: runner.sessionManager.getSessionId(),
},
});
},
} satisfies ExtensionKernelTools;
},
get evalHandleHost() {
runner.assertActive();
Expand Down
5 changes: 4 additions & 1 deletion packages/coding-agent/src/core/extensions/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
import type { ToolDispatcher } from "../tool-dispatch/tool-dispatcher.ts";
import { wrapToolDefinition } from "../tools/tool-definition-wrapper.ts";
import { kernelToolInvokerStorage } from "./kernel-tools-context.ts";
import type { ExtensionRunner } from "./runner.ts";
import type { ExtensionContext, RegisteredTool } from "./types.ts";

Expand Down Expand Up @@ -45,7 +46,9 @@ export function wrapRegisteredTool(
ctx: context,
sourcePath: registeredTool.sourceInfo.path,
})
: await registeredTool.definition.execute(toolCallId, params, signal, onUpdate, context);
: await kernelToolInvokerStorage.run({ sessionId: context.sessionManager.getSessionId() }, () =>
registeredTool.definition.execute(toolCallId, params, signal, onUpdate, context),
);
} finally {
invocation?.dispose();
}
Expand Down
Loading
Loading