/** * `ExtensionHost` — orchestrates the loaded extensions: hook chaining in load * order, non-blocking event fanout, tool proxies, and the ext→host delegate seam. * The TS sibling of the Rust host's `extension/host.rs`. * * The security-critical part is {@link foldHookChain}: how per-extension hook * outcomes combine, and what happens on timeout/crash. It is a pure function so it * can be tested exhaustively against adversarial inputs without spawning anything. */ import type { Tool } from '../agent.js'; import { type DiscoveredExtension } from './manifest.js'; import { type CommandExecuteResult, type CommandRegistration, type Completion, type Context, type HookOutcome, type HostInfo, PROTOCOL_VERSION, type ShortcutRegistration, type Tier, type WorkspaceInfo } from './protocol.js'; export { PROTOCOL_VERSION }; /** Classifies a hook by its failure policy and default timeout. */ export type HookType = 'tool_call' | 'user_bash' | 'tool_result' | 'input' | 'before_agent_start' | 'context' | 'before_provider_request' | 'message_end' | 'session_before_compact' | 'session_before_tree'; export declare function hookTypeFromName(name: string): HookType | undefined; /** * Fail-closed hooks (`tool_call`, `user_bash`) block the operation when an * extension times out or crashes. Everything else fails open (proceeds). */ export declare function hookFailClosed(hook: HookType): boolean; /** * Default hook timeout (ms): 60s for fail-closed (they gate execution), 5s for * fail-open. Manifest `hook_timeout_ms` overrides this. */ export declare function hookDefaultTimeoutMs(hook: HookType): number; /** One extension's reply within a hook chain, as seen by the fold. */ export type HookStep = { kind: 'replied'; outcome: HookOutcome; } | { kind: 'failed'; }; /** The folded result of a whole hook chain. */ export type FoldedHook = { kind: 'proceed'; value: unknown; } | { kind: 'blocked'; reason: string; }; /** * Fold a hook chain over `input`, in load order. `steps` are the per-extension * results in that order. This is the security-critical policy: * * - `continue` → value unchanged, next extension sees it. * - `modify` → value replaced by the patch, next extension sees the patch. * - `block` → short-circuit; the operation is vetoed (honored for every hook). * - failed → for a fail-closed hook, block; for a fail-open hook, proceed unchanged. */ export declare function foldHookChain(hook: HookType, input: unknown, steps: readonly HookStep[]): FoldedHook; /** * Does `ext` own `tool`? Extension tools are namespaced `.` (the MCP * convention). A native tool (`bash`, `file-write`) has no `.` prefix, so no * extension owns it. */ export declare function toolOwnedBy(ext: string, tool: string): boolean; /** * Security guard for a `tool_call` Modify. The `tool_call` hook fires over EVERY * pending call the model made — native tools (`bash`, `file-write`) included — * and a `modify` is otherwise applied verbatim as a full `{tool, arguments}` * replacement. Without this guard, enabling ANY extension lets its `tool_call` * hook silently rewrite a native call's arguments, or redirect it to a different * tool, with zero oversight. * * A `modify` is honored only when both hold; otherwise it is downgraded to * `continue` (the ORIGINAL call is preserved) and logged: * * 1. It does not change which tool runs — the `tool` field is immutable across a * hook. Redirecting call A to a different tool is never legitimate. * 2. The acting extension OWNS the tool being called (`.`). A modify * targeting a native tool or another extension's tool is rejected. * * `continue`/`block` are returned untouched — an extension blocking any call is * always safe and useful; only MUTATION is scoped. */ export declare function guardToolCallModify(actingExt: string, callTool: string, outcome: HookOutcome): HookOutcome; /** * Effective event subscriptions: what the extension asked for at handshake, * clamped to what its manifest `[capabilities] events` declared. An empty declared * list means "no declared filter" → trust the handshake as-is; a non-empty list is * the outer bound the extension can never widen past. */ export declare function effectiveSubscriptions(declared: readonly string[], requested: readonly string[]): Set; /** * The two-tier deadlock guard: a session-mutating ext→host action is valid only * when it presents a COMMAND-tier context whose epoch is still current. An * event-tier context, or a stale token minted before a reload bumped the epoch, is * rejected with `-32003 ContextViolation`. Security-critical; a pure function so it * can be tested exhaustively. */ export declare function validateCommandContext(params: unknown, currentEpoch: number): void; /** * The host's side of ext→host requests. The engine ships headless defaults * ({@link DefaultHostDelegate}); frontends (smooth-code, the daemon, the servers) * subclass it and override. */ export interface HostDelegate { /** Answer a `ui/request`. Headless default: no UI available. */ uiRequest(ext: string, params: unknown): Promise; /** `kv/get`. */ kvGet(ext: string, key: string): Promise; /** `kv/set`. */ kvSet(ext: string, key: string, value: unknown): Promise; /** `exec/run`. Headless default: deny. */ execRun(ext: string, params: unknown): Promise; /** `session/send_message`. Context already validated. Default: unavailable. */ sessionSendMessage(ext: string, params: unknown): Promise; /** `session/send_user_message`. Context already validated. Default: unavailable. */ sessionSendUserMessage(ext: string, params: unknown): Promise; /** `session/append_entry`. Context already validated. Default: unavailable. */ sessionAppendEntry(ext: string, params: unknown): Promise; /** A `tool/update` progress notification during an in-flight `tool/execute`. Fire-and-forget. */ toolUpdate(ext: string, params: unknown): void; } /** The engine's headless delegate: NoUI, JSON-file kv, exec denied, session disabled. */ export declare class DefaultHostDelegate implements HostDelegate { uiRequest(_ext: string, _params: unknown): Promise; kvGet(ext: string, key: string): Promise; kvSet(ext: string, key: string, value: unknown): Promise; execRun(_ext: string, _params: unknown): Promise; sessionSendMessage(_ext: string, _params: unknown): Promise; sessionSendUserMessage(_ext: string, _params: unknown): Promise; sessionAppendEntry(_ext: string, _params: unknown): Promise; toolUpdate(_ext: string, _params: unknown): void; } /** A `(name, error message)` pair for an extension that failed to load. */ export type LoadFailure = [name: string, error: string]; /** Orchestrates the set of loaded extensions in load order. */ export declare class ExtensionHost { private readonly host; private readonly workspace; private readonly mode; private readonly uiCapabilities; private extensions; private epoch; private constructor(); /** An empty host: no extensions, every hook a passthrough. The zero-cost default. */ static empty(): ExtensionHost; /** * Load and initialize each discovered extension. Per-extension failures (spawn, * handshake) are tolerated and returned alongside the host. In an untrusted * workspace, project-scoped extensions are skipped. */ static load(discovered: DiscoveredExtension[], host: HostInfo, workspace: WorkspaceInfo, mode: string, uiCapabilities: string[], delegate: HostDelegate): Promise<{ host: ExtensionHost; failures: LoadFailure[]; }>; private loadOne; /** Send `initialize` and parse the registrations. Shared by load and reload. */ private initialize; /** Number of successfully loaded extensions. */ get length(): number; isEmpty(): boolean; /** Names of loaded extensions, in load order. */ names(): string[]; /** * A fresh dispatch context. Session-mutating actions need `command` tier. The * token embeds the current epoch so it is invalidated across reloads. */ context(tier: Tier): Context; /** Bump the epoch, invalidating every previously minted context token. */ bumpEpoch(): void; /** True if any loaded extension subscribed to `event`. */ hasSubscriber(event: string): boolean; /** * Fire-and-forget event fanout to every subscribed extension. Non-blocking: a * slow or dead extension never stalls the caller (bounded, lossy observe lane). */ dispatchEvent(event: string, payload: unknown): void; /** * Run a hook across every extension in load order, folding the chain. Each * extension sees the prior extension's patch. Fail-open/closed per hook type. */ runHook(hook: HookType, input: unknown): Promise; /** Convenience: run the `tool_call` hook (fail-closed) on a pending call. */ runToolCallHook(tool: string, args: unknown): Promise; /** * Run the `before_agent_start` hook on a system prompt, returning the * possibly-rewritten prompt. Fail-open: a blocked/failed hook leaves it unchanged. */ beforeAgentStart(systemPrompt: string): Promise; /** * Tool proxies for every eager tool every extension registered. Names are dotted * `.`. Deferred tools are returned by {@link deferredTools}. */ tools(): Tool[]; /** Deferred tool proxies. */ deferredTools(): Tool[]; private collectTools; /** * Eager tool proxies for a single extension, minted at the CURRENT epoch. The * frontend calls this after a {@link reload} to re-register the reloaded * extension's tools (its old proxies carry a stale context). */ toolsFor(extName: string): Tool[]; /** Every registered slash-command across all extensions, paired with the owning extension name. */ commands(): Array<[string, CommandRegistration]>; /** Every keyboard shortcut across all extensions, paired with the owning extension name. */ shortcuts(): Array<[string, ShortcutRegistration]>; private commandOwner; /** * Dispatch a registered slash-command to its owning extension with a COMMAND-tier * context. Pass `extName` to disambiguate a command registered by more than one * extension; `undefined` picks the first match in load order. */ runCommand(extName: string | undefined, command: string, args: unknown): Promise; /** * Ask the extension that owns `command` for argument completions given the * `partial` text typed so far. Returns an empty list on error (best-effort — * never fail the caller's keystroke). */ completeCommand(extName: string | undefined, command: string, partial: string): Promise; /** * Hot-reload a single extension by name: notify it (`session_shutdown` reason * `reload`), bump the epoch so every context token it still holds is invalidated, * respawn its subprocess (the generation guard discards any late reply), re-run * `initialize`, then notify it (`session_start` reason `reload`). The caller * re-registers the extension's tools via {@link toolsFor}. */ reload(name: string): Promise; /** Gracefully shut down every extension (5s grace each, then SIGKILL). */ shutdownAll(): Promise; } //# sourceMappingURL=host.d.ts.map