# `@zephyrcloudio/harness-plugin`

Authoring contracts for trusted Ze Harness plugins. This package has zero dependencies:
no `effect`, no Node builtins, no workspace packages. A plugin imports only this
contract plus its own code and any libraries its author chooses.

Because nothing here has shared identity, a plugin may hold a different copy of this
module than the host without breaking. Never use `instanceof` across the boundary;
match failures with the `is*` guards in this module instead.

```ts
import {
  asString,
  definePlugin,
  PluginError,
  type ToolDeclaration,
} from '@zephyrcloudio/harness-plugin';

interface ShoutInput {
  readonly text: string;
}

const shout: ToolDeclaration<ShoutInput> = {
  id: 'shout',
  description: 'Uppercase a short string.',
  inputSchema: {
    type: 'object',
    properties: { text: { type: 'string', minLength: 1 } },
    required: ['text'],
    additionalProperties: false,
  },
  permissions: async () => [{ action: 'shout', resources: ['*'], save: ['*'] }],
  execute: async (input) => {
    const text = asString(input.text);
    if (text === undefined) throw new PluginError({ message: 'Shout input is invalid.' });
    return text.toUpperCase();
  },
};

export default definePlugin({
  id: 'example.shout',
  setup: (context) => {
    context.tools.register(shout);
  },
});
```

`setup` is plain: it returns `void`, a cleanup function, or a `Promise` of either.
In a host revision, if `setup` throws a `PluginError` with a non-blank `publicMessage`
(trimmed to 512 characters), Core boots without the plugin and reports it as failed
with that message instead of refusing to start. A message-only `PluginError`, any other
setup throw, and any other malformed contribution still fail activation; only rejected
tool and journal-kind declarations degrade like a self-described setup failure. In a
Session revision, every one of these failures rejects the whole revision.
Everything else a plugin registers is plain as well: tool `execute` and `permissions`
functions, hook handlers, skill transforms, and event subscribers are all ordinary
`async` functions. Core validates the model input against `inputSchema` before calling
`execute`, and takes the returned JSON as-is. There is no output schema.

`inputSchema` (and a journal kind's `dataSchema`) is plain JSON Schema with an object root,
limited to the keywords `JsonSchema` lists. Common keywords outside that set, such as `pattern`,
`format`, `default`, and `examples`, reject the declaration at setup. Optional object members
must be omitted rather than set to `undefined`: Core rejects explicit `undefined` in projections,
activity updates, questions, and results.

A workspace read tool may set `discoversWorkspaceInstructions: true`. After a successful result with
`kind: "file"` or `kind: "directory"` (a plain string result counts as a file), Core uses the
authorized `read` projection's `metadata.canonicalPath` to discover nested `AGENTS.md` files and adds
any new files to the result as `loadedInstructions`. A result without a kind, or a call without that
`canonicalPath`, fails with `workspace_instruction_failed`. The standard Core `read` tool enables this
behavior.

A tool may set `directOnly: true` to keep it out of CodeMode scripts, so the model can only call it
directly. Core's `skill` and `task` tools and the standard `question` tool are direct-only.

A tool may set a synchronous `availableForModel(model)` predicate. Core calls it for each
model request with the `providerId`, catalog `modelId`, and provider-facing `upstreamId` of the
model that will receive the tools, after Agent and permission filtering, and omits the tool when
it returns `false`. When a plugin model (such as Auto) delegates a request, Core evaluates it
against the sibling that serves it, and only the tools that sibling was offered stay executable.
The predicate can only hide a tool; it never grants one. Tools from plugins loaded as Dynamic
Workers do not forward it and stay available to every model.

Throw `PluginError` for typed failures. From a tool, `message` stays private; only the
optional `publicMessage` reaches the model, so it must contain only safe client-facing text.
Any other throw fails the tool with a generic message, except a `QuestionDismissedError`,
which settles the call with code `question_dismissed`. A plugin model is different: a throw
from its event stream fails the model run with a message that includes the first 200
characters of `message`, which clients see, and `publicMessage` is ignored. Keep a model's
`message` safe to show. `WorkspaceFileError`, `WorkspaceShellError`, and `WebFetchError`
carry only safe failure codes; do not replace them with raw operating-system or network errors.

A `tool.execute.before` hook that answers `allow` resolves a permission decision of `ask`
without asking the user; it never overrides a configured `deny`. A hook that answers `deny`
for a direct tool call ends the Turn as failed with fault code `tool_denied`; inside a CodeMode
script, the same denial rejects that call in the script instead.

`PluginContext` exposes the activation identity, the plugin's options, the platform's
global `fetch` (no host policy or limits), `workspaceFiles` (resolve, read, write, remove a
file, and list a directory) / `workspaceShell` / `webFetch` host capabilities,
and registration for tools, agents, skills, skill sources and transforms, MCP
definitions, models, journal kinds, awaited hooks, and observational subscribers. File reads,
directory listings, shell output, and fetches are bounded by the limits this package exports;
writes are not. Only `workspaceFiles.resolvePath` uses the Session workdir: resolve a path first
and pass the returned `canonicalPath` to the other methods, which resolve relative paths against
the host root. Event subscribers are observational and lossy: each Turn buffers
`observerBufferSize` events (default 128) and drops the oldest when subscribers fall behind.

A model registration is a plain declaration plus an async iterable of model events.
Core validates every value. Sibling listing and delegation exclude plugin
models, so a router cannot call another plugin model, and Core runs a plugin model only
while at least one sibling model is available, even if it never delegates. Host and Session
scopes instantiate the same plugin and use the same registration validation. Plugins are
unsandboxed host-process code; configuration is an explicit trust grant.

A plugin reports bounded durable facts by registering journal kinds at setup
and calling `reportJournal` from a tool or model context. Core validates each
entry against its kind schema and caps, then stores it as soon as it is accepted,
so it remains even if the Turn later fails. Each kind's `maxEntriesPerTurn` bounds
that kind, and `MAX_JOURNAL_ENTRIES_PER_TURN` bounds every entry the Turn records
across all plugins and kinds, including entries already stored for it. Unregistered
kinds, schema violations, oversized payloads, and a report past either cap fail that
`reportJournal` call without writing; the Turn keeps running. Reports from a model
that Core calls outside a Turn, such as for Session titles or compaction, are
accepted and dropped.

Core owns the catalogs, policy, execution, and cleanup. MCP transports and credential
resolution remain host capabilities, not plugin declarations. Native LLM providers are
authored separately using `defineProvider` from `@zephyrcloudio/harness-llm`.

## Tool permissions

`ToolDeclaration.permissions` projects a validated, hook-transformed tool input into one
or more action/resource checks:

```ts
permissions: async (input) => [
  {
    action: 'deploy',
    resources: [input.environment],
    save: [input.environment],
  },
],
```

Core validates the projection before authorization and execution. `resources` are checked
now; `save` contains only the patterns that an **Allow always** reply may store for the
Session. The tool must derive both from its validated input. A tool without a projector
uses its tool ID as the action and `*` as both the resource and save pattern.

The exact validated projections are available as `context.permissions` during execution.
A tool may include safe JSON `metadata` in a projection when execution must use the same
canonical target that was authorized. The UI submits only `once`, `always`, or `reject`;
it never supplies saved patterns.

## Agent modes

`AgentDeclaration.mode` selects where the agent may run: `primary` agents take user turns,
`subagent` agents run only as spawned children through the `task` tool, and `all` (the
default when absent) runs in either seat. Sub-agent agents cannot be the default agent, and
primary-only agents are never spawnable.

Set `AgentDeclaration.fallbackDefault: true` to prefer a primary-capable agent when no explicit
default is selected. This does not compete with `context.agents.setDefault` or a configured default.
If several agents declare this preference, the first by lexical ID wins.

## Worker bundles

The Cloudflare Worker host loads plugins at runtime as Dynamic Workers, which cannot resolve
`node_modules`. A plugin that should run there is built into one self-contained ES module, with every
dependency (including this package) bundled in, and served over a credential-free `https://` URL:

```sh
esbuild src/index.ts --bundle --format=esm --platform=neutral --outfile=dist/worker.js
```

The module's default export is the plugin. A Session adds it as a `Bundle` source
(`ze-harness plugins add --session ID --bundle https://... --plugin-id <id>`); the host pins the
SHA-256 of the bytes it fetched as the artifact identity, so a changed file at the same URL never
replaces an activated plugin until it is refreshed. On the Worker:

- `tools`, `hooks`, `skills.register`, and `agents` (`register` and `setDefault`) are available.
  MCP servers, model providers, journal kinds, event subscribers, Skill sources, and Skill
  transforms fail `setup` explicitly.
- `setup` may run more than once for one activation, in different isolates, and no in-memory state
  survives between calls. Keep it deterministic for the same options: every run must register the
  same contributions, or calls fail closed. Keep state in workspace files.
- Host capabilities (`workspaceFiles`, `workspaceShell`, `webFetch`) work only while Core is calling
  the plugin (setup, a tool, or a hook), not from timers that outlive it.
- `ctx.fetch` and global `fetch` use the Worker's network access. Node-only APIs beyond the Workers
  `nodejs_compat` flag are unavailable.

## Examples

Each example is a file in the repository, shown unchanged. CI runs the first three against a live model; the Worker example is built by its command.

### A tool loaded from config

Registers a `double` tool. `ze-harness.config.json` loads the plugin by path and the CLI runs one prompt that uses it. Run it with `pnpm example:basic`.

`examples/basic/plugin.mjs`:

```js
import { definePlugin } from '@zephyrcloudio/harness-plugin';

export default definePlugin({
  id: 'example.basic',
  setup: (context) => {
    context.tools.register({
      id: 'double',
      description: 'Double a number',
      inputSchema: {
        type: 'object',
        properties: { value: { type: 'number' } },
        required: ['value'],
        additionalProperties: false,
      },
      execute: async (input) => ({ value: input['value'] * 2 }),
    });
  },
});
```

`examples/basic/ze-harness.config.json`:

```json
{
  "model": "openai/gpt-5-mini",
  "permissions": [{ "action": "*", "resource": "*", "effect": "allow" }],
  "plugins": ["./plugin.mjs"]
}
```

### A hook that approves tool calls

Adds a `tool.execute.before` hook that answers `{ approval: "allow" }` for every tool call. A hook allowance resolves a permission decision of `ask` without asking the user; it never overrides `deny`. Run it with `pnpm example:approval-hook`.

`examples/approval-hook/plugin.mjs`:

```js
import { definePlugin } from '@zephyrcloudio/harness-plugin';

export default definePlugin({
  id: 'example.approval-hook',
  setup: (context) => {
    context.tools.register({
      id: 'double',
      description: 'Double a number',
      inputSchema: {
        type: 'object',
        properties: { value: { type: 'number' } },
        required: ['value'],
        additionalProperties: false,
      },
      execute: async (input) => ({ value: input['value'] * 2 }),
    });
    context.hooks.register('tool.execute.before', async () => ({ approval: 'allow' }));
  },
});
```

`examples/approval-hook/ze-harness.config.json`:

```json
{
  "model": "openai/gpt-5-mini",
  "approval": { "timeoutMs": { "kind": "finite", "value": 30000 } },
  "plugins": ["./plugin.mjs"]
}
```

### An inline plugin in the embedded SDK

Passes the plugin object from the first example to `createEmbeddedHarness` through `inlinePlugins`, with no config file, and runs one Turn. Run it with `pnpm example:plugin-tool`.

`examples/plugin-tool/index.mjs`:

```js
import { textPrompt, TurnId } from '@zephyrcloudio/harness-client';
import { createEmbeddedHarness } from '@zephyrcloudio/harness-sdk';
// Live-model example: needs a real OPENAI_API_KEY. Without one in the session
// env the turn fails and this script throws. That is expected locally and okay
// to skip: CI runs every example with the key. See examples/README.md.
import { Effect } from 'effect';

import plugin from '../basic/plugin.mjs';

await Effect.runPromise(
  Effect.scoped(
    Effect.gen(function* () {
      const client = yield* createEmbeddedHarness({
        cwd: process.cwd(),
        databaseFilename: ':memory:',
        config: {
          model: 'openai/gpt-5-mini',
          permissions: [{ action: '*', resource: '*', effect: 'allow' }],
        },
        inlinePlugins: [plugin],
        eventPollIntervalMs: 1,
      });
      const session = yield* client.createSession();
      const turnId = TurnId.make(globalThis.crypto.randomUUID());
      yield* client.submitPrompt(session.id, {
        delivery: 'queue',
        turnId,
        prompt: textPrompt('double two'),
      });
      const turn = yield* client.watchTurn(session.id, turnId);
      if (turn.outcome?.kind !== 'completed') throw new Error('plugin tool example failed');
      process.stdout.write(`${turn.outcome.text}\n`);
    }),
  ),
);
```

### A bundle plugin for the Worker host

A notes plugin with a tool that projects its own edit permission, a hook that normalizes input, and a Skill. `build.mjs` bundles it, with this package inlined, into the single ES module the Worker host loads from a URL. Run it with `pnpm example:worker-plugin`.

`examples/worker-plugin/plugin.mjs`:

```js
import {
  asString,
  definePlugin,
  isWorkspaceFileError,
  PluginError,
} from '@zephyrcloudio/harness-plugin';

// A small notes plugin for the Worker host. It is bundled into one self-contained ES module
// (`pnpm example:worker-plugin`), hosted at an https URL, and added to a Session as a `Bundle`
// source. It shows the three contributions a bundle plugin can make: a tool with a permission
// projector, a hook, and a Skill. Setup may run more than once per activation, so it keeps no state.

const slug = (title) =>
  title
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '') || 'untitled';

export default definePlugin({
  id: 'example.notes',
  setup: (context) => {
    const folder = asString(context.options['folder']) ?? 'notes';

    context.tools.register({
      id: 'notes_save',
      description: `Save a Markdown note in the ${folder}/ folder, replacing a note with the same title.`,
      inputSchema: {
        type: 'object',
        properties: {
          title: { type: 'string', minLength: 1, maxLength: 120 },
          body: { type: 'string', description: 'Markdown body' },
        },
        required: ['title', 'body'],
        additionalProperties: false,
      },
      // Project the exact edit permission Core must authorize, carrying the canonical path so
      // execute touches only what was approved.
      permissions: async (input, tool) => {
        const target = await context.workspaceFiles.resolvePath({
          filePath: `${folder}/${slug(input.title)}.md`,
          cwd: tool.workdir,
        });
        return [
          {
            action: 'edit',
            resources: [target.resource],
            save: ['*'],
            metadata: { canonicalPath: target.canonicalPath },
          },
        ];
      },
      execute: async (input, tool) => {
        const filePath = asString(tool.permissions[0]?.metadata?.['canonicalPath']);
        if (filePath === undefined)
          throw new PluginError({ message: 'The note path was not authorized.' });
        try {
          const written = await context.workspaceFiles.writeText({
            filePath,
            content: `# ${input.title}\n\n${input.body.trim()}\n`,
            signal: tool.signal,
          });
          return { saved: filePath, bytes: written.byteLength };
        } catch (error) {
          if (isWorkspaceFileError(error)) {
            throw new PluginError({
              message: `Note write failed: ${error.code}`,
              publicMessage: 'The note could not be saved.',
            });
          }
          throw error;
        }
      },
    });

    // Normalize note titles before validation, permission projection, and execution.
    context.hooks.register('tool.execute.before', async (event) => {
      const title = asString(event.input?.['title']);
      if (event.toolId !== 'notes_save' || title === undefined) return undefined;
      return { input: { ...event.input, title: title.trim().replace(/\s+/g, ' ') } };
    });

    context.skills.register({
      id: 'note-style',
      displayName: 'Note style',
      description: 'House style for notes saved with notes_save.',
      version: '1.0.0',
      body: [
        '# Note style',
        '',
        '- Titles are short noun phrases in sentence case.',
        '- Start the body with a one-line summary, then bullet points.',
      ].join('\n'),
      modelAdvertisement: { advertised: true, userInvocable: true },
      publicMetadata: { tags: ['notes'] },
    });
  },
});
```

`examples/worker-plugin/build.mjs`:

```js
// Bundles the example plugin into the one self-contained ES module the Worker host loads from a
// `Bundle` URL. Every dependency, including @zephyrcloudio/harness-plugin, is inlined.
import { build } from 'esbuild';

await build({
  entryPoints: [new URL('./plugin.mjs', import.meta.url).pathname],
  outfile: new URL('./dist/worker.js', import.meta.url).pathname,
  bundle: true,
  format: 'esm',
  platform: 'neutral',
  target: 'es2022',
  logLevel: 'info',
});
```

## API reference

Every declaration the package exports, with its documentation, grouped by module. [/harness-plugin.d.ts](https://github.com/harness-plugin.d.ts) has all of them in one file.

### `json`

Exports: `JsonObject`, `JsonValue`, `JsonSchema`.

```ts
/**
 * A plain JSON object. Members may be typed `undefined` so optional fields type-check, but a
 * value sent to Core must omit such members: Core rejects a member whose value is `undefined`.
 */
export interface JsonObject {
  readonly [key: string]: JsonValue | undefined;
}

/**
 * Any JSON value. Core rejects values that are not plain JSON, such as `undefined`, `NaN`,
 * `Infinity`, functions, and class instances like `Date`.
 */
export type JsonValue =
  string | number | boolean | null | JsonObject | readonly JsonValue[];

/**
 * A JSON Schema document written as plain data; no schema builder is needed or provided.
 *
 * Where a plugin declares one (a tool's `inputSchema` or a journal kind's `dataSchema`), the root
 * must be `type: 'object'` and every schema in it may use only these keywords: `$comment`,
 * `$defs`, `$ref` (local `#/...` pointers only), `allOf`, `anyOf`, `oneOf`, `not`, `if`, `then`,
 * `else`, `type`, `const`, `enum`, `title`, `description`, `properties`, `required`,
 * `additionalProperties`, `propertyNames`, `dependentRequired`, `dependentSchemas`,
 * `minProperties`, `maxProperties`, `prefixItems`, `items`, `contains`, `minContains`,
 * `maxContains`, `minItems`, `maxItems`, `uniqueItems`, `minimum`, `maximum`,
 * `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, `minLength`, and `maxLength`. The root
 * may also carry `$schema`, which Core ignores. Any other keyword, such as `pattern`, `format`,
 * `default`, or `examples`, rejects the declaration at setup.
 */
export type JsonSchema = JsonObject;
```

### `error`

Exports: `asString`, `PluginFailure`, `PluginError`, `isPluginError`.

```ts
/**
 * Returns `cause` when it is a primitive string and `undefined` for everything else,
 * including `String` wrapper objects. Never throws and never runs code the value defines:
 * objects (including proxies and prototype-less objects) are rejected by identity before any
 * conversion, so `toString`, `valueOf` and `Symbol.toPrimitive` are never invoked. Only
 * primitives reach `String`, which converts them without user code, and string identity
 * holds exactly for primitive strings.
 *
 * @param cause - Any value.
 */
export declare const asString: (cause: unknown) => string | undefined;

/**
 * Structural shape of a {@link PluginError}, which is what {@link isPluginError} narrows to and
 * what Core reads from any thrown value tagged `PluginError`.
 */
export interface PluginFailure {
  /** Structural tag that identifies the failure across module copies. */
  readonly _tag: "PluginError";
  /**
   * Diagnostic for the plugin author. Tool, permission, hook, and setup failures never expose it.
   * A failure thrown from a plugin model's event stream is the exception: its first 200
   * characters appear in the Turn's fault message, which clients see.
   */
  readonly message: string;
  /**
   * Safe text that may reach the model and clients. Core trims it and keeps at most 4,096
   * characters (512 for setup failures); an empty or blank value counts as absent.
   */
  readonly publicMessage?: string | undefined;
}

/**
 * Typed failure for plugin code to throw from setup, tools, hooks, and other plugin callbacks.
 *
 * When a tool's `execute` or `permissions` throws one, the call fails and the model sees `publicMessage`, or `Tool execution failed.`
 * without one; the Turn continues. When a hook throws one, the Turn fails with `publicMessage` as
 * its fault message. When a host-scope plugin's `setup` throws one with `publicMessage`, Core
 * starts without that plugin and reports it as failed with that message. Other thrown values
 * expose no message: Core substitutes a generic one.
 */
export declare class PluginError extends Error implements PluginFailure {
  /** Structural tag that identifies the failure across module copies. */
  readonly _tag: "PluginError";
  /** Safe text that may reach the model and clients; see {@link PluginFailure.publicMessage}. */
  readonly publicMessage?: string | undefined;
  /**
   * @param input.message - Diagnostic for the plugin author, kept as the error's `message`.
   * @param input.publicMessage - Safe client-facing text. Leave it out when nothing is safe to
   * share.
   */
  constructor(input: {
    readonly message: string;
    readonly publicMessage?: string | undefined;
  });
}

/**
 * Matches a `PluginError` from any copy of this package by its own `_tag` data property.
 * Never throws and never invokes accessors or conversion methods on `cause`; only a Proxy's
 * own traps can run, and anything they throw reports no match.
 *
 * @param cause - Any caught value.
 */
export declare const isPluginError: (cause: unknown) => cause is PluginFailure;
```

### `limits`

Exports: `MAX_FILE_READ_BYTES`, `MAX_SHELL_OUTPUT_BYTES`, `MAX_SHELL_TIMEOUT_MS`, `MAX_DIRECTORY_ENTRIES`, `DEFAULT_WEB_FETCH_TIMEOUT_MS`, `MAX_WEB_FETCH_RESPONSE_BYTES`, `MAX_WEB_FETCH_TIMEOUT_MS`, `MAX_JOURNAL_KIND_LENGTH`, `MAX_JOURNAL_SUMMARY_LENGTH`, `MAX_JOURNAL_DATA_BYTES`, `MAX_JOURNAL_ENTRIES_PER_TURN`.

```ts
/**
 * Largest file `workspaceFiles.readText` returns: 51,200 bytes (50 KiB). It is both the default
 * and the ceiling for `maxBytes`; a larger file fails with `too_large`.
 */
export declare const MAX_FILE_READ_BYTES = 51200;

/**
 * Shell output kept in memory: 51,200 bytes (50 KiB). `workspaceShell.execute` returns the last
 * 50 KiB of combined output and, when more was produced, writes the full output to an artifact
 * file.
 */
export declare const MAX_SHELL_OUTPUT_BYTES = 51200;

/**
 * Largest accepted shell `timeoutMs`: 2,147,483,647 ms (2^31 - 1, about 24.8 days), the longest
 * delay a JavaScript timer supports. The Worker host also uses it when no timeout is given.
 */
export declare const MAX_SHELL_TIMEOUT_MS = 2147483647;

/** Largest page `workspaceFiles.readDirectory` returns: 2,000 entries. Also the default `limit`. */
export declare const MAX_DIRECTORY_ENTRIES = 2000;

/** `webFetch.fetchText` timeout when `timeoutMs` is absent: 30,000 ms (30 seconds). */
export declare const DEFAULT_WEB_FETCH_TIMEOUT_MS = 30000;

/**
 * Largest response body `webFetch.fetchText` reads: 5,242,880 bytes (5 MiB). A larger declared
 * or actual body fails with `too_large`.
 */
export declare const MAX_WEB_FETCH_RESPONSE_BYTES = 5242880;

/** Largest accepted `webFetch.fetchText` `timeoutMs`: 120,000 ms (2 minutes). */
export declare const MAX_WEB_FETCH_TIMEOUT_MS = 120000;

/**
 * Longest journal kind name: 64 characters. A kind is also a lowercase identifier: a letter, then
 * lowercase letters and digits, optionally in segments joined by `.`, `_`, or `-`.
 */
export declare const MAX_JOURNAL_KIND_LENGTH = 64;

/**
 * Longest stored journal entry summary: 256 UTF-16 code units. Core truncates a longer summary
 * instead of rejecting the entry.
 */
export declare const MAX_JOURNAL_SUMMARY_LENGTH = 256;

/**
 * Largest journal entry `data`: 4,096 bytes (4 KiB), measured as the UTF-8 length of its JSON
 * serialization. A journal kind's `maxDataBytes` may lower it, never raise it. A larger entry is
 * rejected.
 */
export declare const MAX_JOURNAL_DATA_BYTES = 4096;

/**
 * Most journal entries one Turn may record across every plugin and kind: 50. It is also the
 * default and upper bound of a kind's `maxEntriesPerTurn`. Core rejects a report that would
 * exceed either cap without writing it; the Turn continues.
 */
export declare const MAX_JOURNAL_ENTRIES_PER_TURN = 50;
```

### `workspace`

Exports: `WorkspaceFileErrorCode`, `WorkspaceFileFailure`, `WorkspaceFileError`, `isWorkspaceFileError`, `WorkspaceShellErrorCode`, `WorkspaceShellFailure`, `WorkspaceShellError`, `isWorkspaceShellError`, `SessionWorkdir`, `WorkspaceDirectoryEntry`, `WorkspaceFileTarget`, `WorkspaceFiles`, `WorkspaceShell`.

```ts
/**
 * Safe failure code carried by a {@link WorkspaceFileError}. Both built-in hosts use the same
 * codes for the same conditions.
 */
export type WorkspaceFileErrorCode =
  /**
   * The path is empty or contains a NUL character, the working directory escapes the host root,
   * or symlink resolution fails (for example, after more than 40 links).
   */
  | "invalid_path"
  /** Nothing exists at the path. */
  | "not_found"
  /**
   * The target has the wrong kind: `readText` reached something other than a regular file,
   * `writeText` or `removeFile` reached a directory, `readDirectory` reached something other
   * than a directory, or a parent component of the path is not a directory.
   */
  | "not_file"
  /** The file is larger than `MAX_FILE_READ_BYTES` and no smaller `maxBytes` was passed. */
  | "too_large"
  /**
   * The file is larger than a caller `maxBytes` at or below `MAX_FILE_READ_BYTES`, or
   * `maxBytes` is not a positive safe integer.
   */
  | "max_bytes_exceeded"
  /** The file content is not valid UTF-8. */
  | "invalid_utf8"
  /** Any other filesystem failure. The underlying operating-system error is not exposed. */
  | "io";

/** Structural shape of a workspace file failure, as narrowed by {@link isWorkspaceFileError}. */
export interface WorkspaceFileFailure {
  /** Always `'WorkspaceFileError'`. */
  readonly _tag: "WorkspaceFileError";
  /** What went wrong. Safe to show to the model or user. */
  readonly code: WorkspaceFileErrorCode;
}

/**
 * Rejection of a {@link WorkspaceFiles} method. It carries only a safe code, never a raw path or
 * operating-system error. A tool that rethrows it fails with a generic message; to tell the model
 * why, map the code to a `PluginError` `publicMessage`. Match it with
 * {@link isWorkspaceFileError}, not `instanceof`.
 */
export declare class WorkspaceFileError
  extends Error
  implements WorkspaceFileFailure
{
  /** Always `'WorkspaceFileError'`. */
  readonly _tag: "WorkspaceFileError";
  /** What went wrong. */
  readonly code: WorkspaceFileErrorCode;
  /**
   * @param input - The failure code. The message becomes `Workspace file failed: <code>`.
   */
  constructor(input: { readonly code: WorkspaceFileErrorCode });
}

/**
 * Matches a `WorkspaceFileError` from any copy of this package, including failures rebuilt for plugins
 * loaded as Worker bundles, by its own `_tag` data property. Never throws and never invokes
 * accessors or conversion methods on `cause`; only a Proxy's own traps can run, and anything they
 * throw reports no match.
 *
 * @param cause - Any caught value.
 */
export declare const isWorkspaceFileError: (
  cause: unknown,
) => cause is WorkspaceFileFailure;

/** Safe failure code carried by a {@link WorkspaceShellError}. */
export type WorkspaceShellErrorCode =
  /**
   * The command is empty or contains a NUL character, or `cwd` resolves outside the host root.
   */
  | "invalid_command"
  /** `timeoutMs` is not a finite number greater than 0 and at most `MAX_SHELL_TIMEOUT_MS`. */
  | "invalid_timeout"
  /**
   * The command could not start or its run failed: the process or container execution errored,
   * the output artifact could not be written, or `onOutput` threw.
   */
  | "spawn_failed";

/** Structural shape of a workspace shell failure, as narrowed by {@link isWorkspaceShellError}. */
export interface WorkspaceShellFailure {
  /** Always `'WorkspaceShellError'`. */
  readonly _tag: "WorkspaceShellError";
  /** What went wrong. Safe to show to the model or user. */
  readonly code: WorkspaceShellErrorCode;
}

/**
 * Rejection of {@link WorkspaceShell.execute}. A command that runs and exits non-zero or times
 * out is not an error; it resolves with its exit code or `timedOut`. Match it with
 * {@link isWorkspaceShellError}, not `instanceof`.
 */
export declare class WorkspaceShellError
  extends Error
  implements WorkspaceShellFailure
{
  /** Always `'WorkspaceShellError'`. */
  readonly _tag: "WorkspaceShellError";
  /** What went wrong. */
  readonly code: WorkspaceShellErrorCode;
  /**
   * @param input - The failure code. The message becomes `Workspace shell failed: <code>`.
   */
  constructor(input: { readonly code: WorkspaceShellErrorCode });
}

/**
 * Matches a `WorkspaceShellError` from any copy of this package, including failures rebuilt for plugins
 * loaded as Worker bundles, by its own `_tag` data property. Never throws and never invokes
 * accessors or conversion methods on `cause`; only a Proxy's own traps can run, and anything they
 * throw reports no match.
 *
 * @param cause - Any caught value.
 */
export declare const isWorkspaceShellError: (
  cause: unknown,
) => cause is WorkspaceShellFailure;

/**
 * A Session working directory, as received in a tool context's `workdir`. Core validates it when
 * the Session is created: non-empty, at most 1024 characters, forward slashes only, and no `..`
 * segments. A Session created without one uses `.`, the host root.
 *
 * On the Node host a relative value resolves inside the server root and must stay there, and an
 * absolute value names a directory on the same machine. On the Worker host the workdir resolves
 * inside `/workspace` and must stay there.
 */
export type SessionWorkdir = string;

/** One entry of a {@link WorkspaceFiles.readDirectory} listing. */
export interface WorkspaceDirectoryEntry {
  /** Entry name within the directory, not a path. */
  readonly name: string;
  /**
   * `file` for a regular file, `directory` for a directory, and `other` for anything else. A
   * symlink reports its target's kind, or `other` when the target cannot be read.
   */
  readonly kind: "file" | "directory" | "other";
}

/**
 * A path resolved against a Session working directory for authorization. Return `resource` from
 * a tool's `permissions` projection, keep `canonicalPath` in the projection's `metadata`, and
 * pass that same `canonicalPath` to the file operation in `execute`, so the operation acts on
 * exactly the path that was authorized.
 */
export interface WorkspaceFileTarget {
  /**
   * Absolute path with every existing symlink resolved (a container path on the Worker host).
   * Pass it to the file operation after authorization.
   */
  readonly canonicalPath: string;
  /**
   * Permission resource, using forward slashes. Inside the working directory it is relative to it
   * (`.` for the directory itself); outside it is the absolute canonical path.
   */
  readonly resource: string;
  /**
   * Present only when the target lies outside the working directory. Project it as an additional
   * `external_directory` permission, which the default permission policy asks the user about.
   */
  readonly externalDirectory?:
    | {
        /**
         * `<dir>/*`, where `<dir>` is the target itself when it is an existing directory and its
         * parent directory otherwise.
         */
        readonly resource: string;
        /** Pattern an **Allow always** reply may save. Both hosts set it equal to `resource`. */
        readonly save: string;
      }
    | undefined;
}

/**
 * Host filesystem access, available as `context.workspaceFiles`. The Node host reads the server
 * machine's filesystem with the server user's authority; the Worker host reads the Session
 * server's shared Computer workspace, rooted at `/workspace` in its container.
 *
 * Only {@link WorkspaceFiles.resolvePath} applies a working directory. The other methods resolve a
 * relative path against the host root (the Node server root or `/workspace`) and accept any
 * absolute path, so pass them the `canonicalPath` you authorized. The host checks no permissions
 * itself; authorization comes from the tool's permission projection.
 *
 * Methods reject with a {@link WorkspaceFileError}. Aborting `signal` rejects the pending call
 * promptly with an error that is not a `WorkspaceFileError`. For plugins loaded as Worker bundles,
 * the passed `signal` is ignored and the host uses the signal of the call it is serving instead.
 * When the host provides no filesystem, every method throws a `PluginError`.
 */
export interface WorkspaceFiles {
  /**
   * Resolves a path against a Session working directory. The target need not exist yet, so it
   * also works for files about to be created; symlinks in the existing part of the path are
   * resolved. Use it in a tool's `permissions` projection.
   *
   * @returns The canonical target and its permission resources.
   * @throws {@link WorkspaceFileError} `invalid_path` when the path or working directory is
   *   invalid. Other resolution failures report `invalid_path` on the Node host and `not_found`
   *   or `io` on the Worker host.
   */
  readonly resolvePath: (input: {
    /** Relative to `cwd`, or absolute. Must be non-empty and contain no NUL character. */
    readonly filePath: string;
    /**
     * Session working directory, normally the tool context's `workdir`. Absent resolves at the
     * host root. It is the permission boundary: targets inside it get a workdir-relative
     * `resource`; targets outside it get an absolute `resource` and an `externalDirectory`.
     */
    readonly cwd?: SessionWorkdir | undefined;
  }) => Promise<WorkspaceFileTarget>;
  /**
   * Reads a whole file as UTF-8 text. The host checks the file size before reading and fails
   * rather than returning partial content.
   *
   * @returns `filePath` echoed unchanged, the decoded `content`, and its `byteLength` in bytes.
   * @throws {@link WorkspaceFileError} `not_found`, `not_file`, `too_large`,
   *   `max_bytes_exceeded`, `invalid_utf8`, `invalid_path`, or `io`.
   */
  readonly readText: (input: {
    /** File to read, normally an authorized `canonicalPath`. */
    readonly filePath: string;
    /**
     * Optional per-call byte allowance, a positive safe integer. The effective limit is the
     * smaller of this and `MAX_FILE_READ_BYTES`. Absent means `MAX_FILE_READ_BYTES`.
     */
    readonly maxBytes?: number | undefined;
    /** Cancels the read. */
    readonly signal: AbortSignal;
  }) => Promise<{
    readonly filePath: string;
    readonly content: string;
    readonly byteLength: number;
  }>;
  /**
   * Creates or replaces a file with UTF-8 text, creating missing parent directories. The host
   * applies no size limit.
   *
   * @returns `filePath` echoed unchanged and the written `byteLength` in UTF-8 bytes.
   * @throws {@link WorkspaceFileError} `not_file`, `invalid_path`, or `io`.
   */
  readonly writeText: (input: {
    /** File to write, normally an authorized `canonicalPath`. */
    readonly filePath: string;
    /** Full new content. */
    readonly content: string;
    /** Cancels the write. A write already in progress may still complete. */
    readonly signal: AbortSignal;
  }) => Promise<{
    readonly filePath: string;
    readonly byteLength: number;
  }>;
  /**
   * Removes one file. A symlink is removed as a link; its target is untouched.
   *
   * @returns `filePath` echoed unchanged.
   * @throws {@link WorkspaceFileError} `not_found` when nothing exists at the path, `not_file`
   *   for a directory, or `invalid_path` / `io`.
   */
  readonly removeFile: (input: {
    /** File to remove, normally an authorized `canonicalPath`. */
    readonly filePath: string;
    /** Cancels the removal. */
    readonly signal: AbortSignal;
  }) => Promise<{
    readonly filePath: string;
  }>;
  /**
   * Lists one page of a directory's entries, sorted by name.
   *
   * @returns `dirPath` echoed unchanged, the page of `entries`, the `offset` used,
   *   `totalEntries` in the whole directory, and `truncated: true` when entries remain after
   *   this page.
   * @throws {@link WorkspaceFileError} `not_found`, `not_file` when the path is not a directory,
   *   `invalid_path` when `offset` or `limit` is not an integer of at least 1 (checked before the
   *   directory is read), or `io`.
   */
  readonly readDirectory: (input: {
    /** Directory to list, normally an authorized `canonicalPath`. */
    readonly dirPath: string;
    /** 1-based index of the first entry to return: an integer of at least 1; defaults to 1. */
    readonly offset?: number | undefined;
    /**
     * Maximum entries to return: an integer of at least 1. Defaults to, and is capped at,
     * `MAX_DIRECTORY_ENTRIES`.
     */
    readonly limit?: number | undefined;
    /** Cancels the listing. */
    readonly signal: AbortSignal;
  }) => Promise<{
    readonly dirPath: string;
    readonly entries: readonly WorkspaceDirectoryEntry[];
    readonly offset: number;
    readonly totalEntries: number;
    readonly truncated: boolean;
  }>;
}

/**
 * Host command execution, available as `context.workspaceShell`. On the Node host the command runs
 * through the system shell as the server user, with only a fixed allowlist of environment
 * variables (such as `PATH`, `HOME`, `LANG`, `TERM`, and `TMPDIR`). On the Worker host it runs in
 * the Session server's Computer container. The host checks no permissions itself; authorization
 * comes from the tool's permission projection.
 *
 * For plugins loaded as Worker bundles, the passed `signal` is ignored and the host uses the
 * signal of the call it is serving instead. When the host provides no shell, `execute` throws a
 * `PluginError`.
 */
export interface WorkspaceShell {
  /**
   * Runs one shell command to completion and returns its combined output.
   *
   * A non-zero exit or a timeout still resolves. Aborting `signal` stops the command (on Node,
   * with its child processes) and rejects with an error that is not a
   * {@link WorkspaceShellError}.
   *
   * @returns
   * - `output`: stdout and stderr interleaved in arrival order, keeping only the last
   *   `MAX_SHELL_OUTPUT_BYTES`.
   * - `exitCode`: the process exit code; `null` after a timeout, or on Node when a signal ended
   *   the process.
   * - `signal`: on Node, the signal that ended the process, such as `SIGKILL`; always `null` on the
   *   Worker host.
   * - `timedOut`: `true` when `timeoutMs` elapsed and the host stopped the command.
   * - `truncated`: `true` when earlier output was dropped from `output`.
   * - `artifact`: present when total output exceeded `MAX_SHELL_OUTPUT_BYTES`. The host then
   *   writes the full output to `.ze-harness/tool-output/<id>.log`, relative to the command's
   *   working directory on Node and to `/workspace` on the Worker host, and `artifact.path` holds
   *   that relative path. See {@link TerminalToolOutputArtifact}.
   * @throws {@link WorkspaceShellError} `invalid_command`, `invalid_timeout`, or `spawn_failed`.
   */
  readonly execute: (input: {
    /** Shell command line. Must be non-empty and contain no NUL character. */
    readonly command: string;
    /**
     * Session working directory to run in, normally the tool context's `workdir`. Absent runs at
     * the host root.
     */
    readonly cwd?: SessionWorkdir | undefined;
    /**
     * Optional time limit in milliseconds, greater than 0 and at most
     * `MAX_SHELL_TIMEOUT_MS`. When absent the Node host applies no timeout and the Worker
     * host uses `MAX_SHELL_TIMEOUT_MS`.
     */
    readonly timeoutMs?: number | undefined;
    /** Cancels the command. */
    readonly signal: AbortSignal;
    /**
     * Receives throttled progress while output arrives, plus a final update when the command
     * ends. `preview` holds the latest output tail, `observedBytes` counts all output so far, and
     * `artifact` appears once output spills to a file. The host awaits each call, so forward it
     * to the tool context's `reportActivity` to stream output to the user. A throw fails the call
     * with `spawn_failed`.
     */
    readonly onOutput?:
      ((activity: ToolActivityUpdate) => void | Promise<void>) | undefined;
  }) => Promise<{
    readonly output: string;
    readonly exitCode: number | null;
    readonly signal: string | null;
    readonly timedOut: boolean;
    readonly truncated: boolean;
    readonly artifact?: TerminalToolOutputArtifact | undefined;
  }>;
}
```

### `web-fetch`

Exports: `WebFetchErrorCode`, `WebFetchFailure`, `WebFetchError`, `isWebFetchError`, `WebFetchFormat`, `WebFetch`.

```ts
/** Safe failure code carried by a {@link WebFetchError}. */
export type WebFetchErrorCode =
  /** The URL does not parse, is not `http:` or `https:`, has no host, or embeds credentials. */
  | "invalid_url"
  /**
   * `timeoutMs` is not a positive safe integer, or exceeds `MAX_WEB_FETCH_TIMEOUT_MS`.
   */
  | "invalid_timeout"
  /** The request failed for any other reason, such as a DNS, connection, or TLS error. */
  | "request_failed"
  /** The timeout elapsed before the response body was fully read. */
  | "timed_out"
  /** The final response, after redirects, has a non-2xx status. */
  | "http_status"
  /**
   * The declared `Content-Length` or the bytes actually read exceed
   * `MAX_WEB_FETCH_RESPONSE_BYTES`.
   */
  | "too_large"
  /**
   * The response `Content-Type` is not textual. Accepted types are `text/*`, `application/json`,
   * `*+json`, `application/xml`, `*+xml`, `application/javascript`, and
   * `application/x-javascript`; a missing `Content-Type` is also accepted.
   */
  | "unsupported_content_type"
  /** The response body is not valid UTF-8. */
  | "invalid_utf8";

/** Structural shape of a web fetch failure, as narrowed by {@link isWebFetchError}. */
export interface WebFetchFailure {
  /** Always `'WebFetchError'`. */
  readonly _tag: "WebFetchError";
  /** What went wrong. Safe to show to the model or user. */
  readonly code: WebFetchErrorCode;
}

/**
 * Rejection of {@link WebFetch.fetchText}. It carries only a safe code, never the raw network
 * error or response body. Match it with {@link isWebFetchError}, not `instanceof`.
 */
export declare class WebFetchError extends Error implements WebFetchFailure {
  /** Always `'WebFetchError'`. */
  readonly _tag: "WebFetchError";
  /** What went wrong. */
  readonly code: WebFetchErrorCode;
  /**
   * @param input - The failure code. The message becomes `Web fetch failed: <code>`.
   */
  constructor(input: { readonly code: WebFetchErrorCode });
}

/**
 * Matches a `WebFetchError` from any copy of this package, including failures rebuilt for plugins
 * loaded as Worker bundles, by its own `_tag` data property. Never throws and never invokes
 * accessors or conversion methods on `cause`; only a Proxy's own traps can run, and anything they
 * throw reports no match.
 *
 * @param cause - Any caught value.
 */
export declare const isWebFetchError: (
  cause: unknown,
) => cause is WebFetchFailure;

/**
 * Preferred response format. It only sets the request's `Accept` header preference; the host
 * returns the body as received and does not convert between formats.
 */
export type WebFetchFormat =
  /** Prefers `text/markdown`, then plain text, then HTML. */
  | "markdown"
  /** Prefers `text/plain`, then Markdown, then HTML. */
  | "text"
  /** Prefers `text/html` or `application/xhtml+xml`, then plain text, then Markdown. */
  | "html";

/**
 * Bounded HTTP text fetching through the host's network access, available as `context.webFetch`.
 * The Node host uses the server process's network and the Worker host uses the Worker's; both
 * behave the same. Unlike the plain `context.fetch`, it validates the URL, enforces a timeout and
 * a response size cap, accepts only UTF-8 text, and fails with safe codes.
 *
 * The host checks only the URL's shape. Authorize the target through the tool's permission
 * projection. When the host provides no web fetch, `fetchText` throws a `PluginError`.
 */
export interface WebFetch {
  /**
   * Sends one `GET` request, follows redirects, and reads the whole body as UTF-8 text. The
   * request carries a browser-like `User-Agent`; when the response is a Cloudflare challenge
   * (`403` with `cf-mitigated: challenge`), the host retries once with a plain `User-Agent`.
   *
   * Aborting `signal` rejects with an error that is not a {@link WebFetchError}. For plugins loaded
   * as Worker bundles, the passed `signal` is ignored and the host uses the signal of the call it
   * is serving instead.
   *
   * @returns `content` as the decoded body, `contentType` as the raw `Content-Type` header (empty
   *   when absent), and `byteLength` as the body size in bytes.
   * @throws {@link WebFetchError} with any {@link WebFetchErrorCode}.
   */
  readonly fetchText: (input: {
    /** Absolute `http:` or `https:` URL without embedded credentials. */
    readonly url: string;
    /** Preferred response format, sent as the `Accept` header. */
    readonly format: WebFetchFormat;
    /**
     * Time limit in milliseconds for the whole request, including reading the body. A positive
     * integer up to `MAX_WEB_FETCH_TIMEOUT_MS`; defaults to `DEFAULT_WEB_FETCH_TIMEOUT_MS`.
     */
    readonly timeoutMs?: number | undefined;
    /** Cancels the request. */
    readonly signal: AbortSignal;
  }) => Promise<{
    readonly content: string;
    readonly contentType: string;
    readonly byteLength: number;
  }>;
}
```

### `permission`

Exports: `PermissionEffect`, `PermissionRule`, `PermissionRuleset`, `PermissionProjection`, `ToolApprovalDecision`.

```ts
/**
 * Outcome of a permission rule: run without asking, ask the user, or refuse. When no rule matches
 * an action and resource, the effect is `ask`.
 */
export type PermissionEffect = "allow" | "ask" | "deny";

/**
 * One permission rule. `action` and `resource` are patterns: `*` matches any run of characters,
 * `?` matches one character, and backslashes are treated as `/` on both sides. A pattern ending
 * in ` *` also matches without that suffix, so `git status *` matches `git status`. Matching is
 * case-sensitive unless the host sets `permissionMatching.caseSensitive` to false.
 */
export interface PermissionRule {
  /** Pattern matched against a projection's `action`. */
  readonly action: string;
  /** Pattern matched against each of a projection's `resources`. */
  readonly resource: string;
  /** What happens when this rule is the last one matching. */
  readonly effect: PermissionEffect;
}

/**
 * Ordered permission rules where the last matching rule wins. Core appends host rules, then
 * Session rules, then the active Agent's rules, so later layers override earlier ones.
 */
export type PermissionRuleset = readonly PermissionRule[];

/**
 * One action and resource check that a tool asks Core to authorize before `execute` runs. A
 * tool's `permissions` function returns one or more, derived from its validated input.
 *
 * Core checks each projection in order. Each resource is evaluated against the rules; the
 * projection is denied if any resource is denied, asks if any asks, and is allowed otherwise. A
 * configured deny or a hook `deny` refuses it. Otherwise a configured allow, a matching saved
 * permission, or a hook `allow` approves it, and anything left asks the user through a
 * permission request that carries `action`, `resources`, `save`, and `metadata`. If no client can
 * answer, the request is denied. A denied projection fails the Turn with `tool_denied` and the
 * tool never runs. When a configured approval wait limit expires, the Turn fails with
 * `approval_wait_limit_reached`.
 *
 * Omit `save` and `metadata` instead of setting them to `undefined`; Core rejects explicit
 * `undefined`, which fails the Turn.
 */
export interface PermissionProjection {
  /** Action name matched against rule `action` patterns, such as `read` or `deploy`. */
  readonly action: string;
  /** Concrete resources checked now, such as paths or environments. Must be non-empty. */
  readonly resources: readonly [string, ...string[]];
  /**
   * Patterns an **Allow always** reply stores for the rest of the Session, each as an allow rule
   * for this `action`. Core then also resolves other pending requests in the Session that the new
   * rules allow. Saved rules never override a configured deny. Absent or empty means **Allow
   * always** saves nothing and acts like **Allow once**.
   */
  readonly save?: readonly string[] | undefined;
  /**
   * JSON that rules never match against. It travels with the permission request that clients see
   * and comes back in `context.permissions` during `execute`, so a tool can act on the exact
   * target that was approved. Keep it free of secrets.
   */
  readonly metadata?: JsonObject | undefined;
}

/**
 * A resolved approval, as returned by hooks and reported in `approval.resolved` events. A
 * `tool.execute.before` hook returning `deny` refuses every projection of
 * the call. A hook returning `allow` approves projections that would otherwise ask, but never
 * overrides a configured deny. When several hooks answer, `deny` wins.
 */
export type ToolApprovalDecision = "allow" | "deny";
```

### `question`

Exports: `QuestionOption`, `QuestionPrompt`, `QuestionAnswer`, `QuestionRequest`, `QuestionDismissedFailure`, `QuestionDismissedError`, `isQuestionDismissedError`.

```ts
/** One selectable choice in a {@link QuestionPrompt}. */
export interface QuestionOption {
  /** Short display text, 1 to 256 characters. Selecting the option answers with this label. */
  readonly label: string;
  /** Explains the choice, up to 4,096 characters; may be empty. */
  readonly description: string;
}

/**
 * One question for the user, passed to `askQuestion`. Omit optional fields instead of setting them
 * to `undefined`; Core rejects explicit `undefined`.
 */
export interface QuestionPrompt {
  /** The full question, 1 to 4,096 characters. */
  readonly question: string;
  /** Short title clients show for the question, 1 to 30 characters. */
  readonly header: string;
  /** The choices offered. At least one is required. */
  readonly options: readonly [QuestionOption, ...QuestionOption[]];
  /** Lets the user select more than one option. Clients treat absent as false. */
  readonly multiple?: boolean | undefined;
  /** Allow a custom typed answer. Clients treat absent as true. */
  readonly custom?: boolean | undefined;
}

/**
 * The user's answer to one question: the selected option labels, or typed text when `custom`
 * allows it. Each answer has at least one non-empty string of at most 4,096 characters. `multiple`
 * and `custom` guide clients; Core does not check answers against the options.
 */
export type QuestionAnswer = readonly string[];

/**
 * A question batch as Core recorded it, carried by the `question.asked` event that observers and
 * clients receive.
 */
export interface QuestionRequest {
  /** Core-assigned request id; a client's reply names it. */
  readonly id: string;
  /** The Session the asking call runs in. */
  readonly sessionId: string;
  /** The Turn the asking call belongs to. */
  readonly turnId: string;
  /** The tool that asked. */
  readonly toolId: string;
  /** The tool call that asked. */
  readonly toolCallId: string;
  /** The prompts, in the order answers are returned. */
  readonly questions: readonly [QuestionPrompt, ...QuestionPrompt[]];
}

/**
 * Structural shape of {@link QuestionDismissedError}, which is what
 * {@link isQuestionDismissedError} narrows to.
 */
export interface QuestionDismissedFailure {
  /** Structural tag that identifies the failure across module copies. */
  readonly _tag: "QuestionDismissedError";
  /** Fault code the call settles with when the failure escapes `execute`. */
  readonly code: "question_dismissed";
}

/**
 * Rejection from `askQuestion` when the user dismisses the questions, the host has no way to ask,
 * or the reply is malformed. A tool may catch it and continue. If it escapes `execute`, the call
 * fails with code `question_dismissed` and the model sees `The user dismissed this question.`
 * Match it with {@link isQuestionDismissedError}, not `instanceof`.
 */
export declare class QuestionDismissedError
  extends Error
  implements QuestionDismissedFailure
{
  /** Structural tag that identifies the failure across module copies. */
  readonly _tag: "QuestionDismissedError";
  /** Fault code the call settles with when the failure escapes `execute`. */
  readonly code: "question_dismissed";
  /** Creates the error with the fixed message `The user dismissed this question.` */
  constructor();
}

/**
 * Matches a `QuestionDismissedError` from any copy of this package by its own `_tag` data
 * property. Never throws and never invokes accessors or conversion methods on `cause`; only a
 * Proxy's own traps can run, and anything they throw reports no match.
 *
 * @param cause - Any caught value.
 */
export declare const isQuestionDismissedError: (
  cause: unknown,
) => cause is QuestionDismissedFailure;
```

### `tool`

Exports: `ToolOutputArtifactState`, `ToolOutputArtifact`, `TerminalToolOutputArtifact`, `ToolActivityUpdate`, `ToolExecutionContext`, `ToolPermissionContext`, `ToolModelContext`, `ToolDeclaration`.

```ts
/**
 * Lifecycle of a {@link ToolOutputArtifact}: `writing` while output is still being appended,
 * `complete` once the file holds the full output, and `incomplete` when writing stopped before
 * the output ended.
 */
export type ToolOutputArtifactState = "writing" | "complete" | "incomplete";

/**
 * A workspace file that holds tool output too large to keep inline. The host shell writes one
 * when command output exceeds `MAX_SHELL_OUTPUT_BYTES`; a tool forwards it to clients through
 * {@link ToolActivityUpdate.artifact}.
 */
export interface ToolOutputArtifact {
  /** Artifact identity. Core rejects an activity update whose artifact id is not a UUID. */
  readonly id: string;
  /**
   * Normalized relative workspace path of the file: no leading `/`, no `\`, and no empty, `.`, or
   * `..` segments.
   */
  readonly path: string;
  /** Bytes of output observed so far: a non-negative safe integer. */
  readonly observedBytes: number;
  /** Whether the file is still being written, complete, or cut short. */
  readonly state: ToolOutputArtifactState;
}

/** A {@link ToolOutputArtifact} after its producer finished, so it is no longer `writing`. */
export type TerminalToolOutputArtifact = Omit<ToolOutputArtifact, "state"> & {
  readonly state: "complete" | "incomplete";
};

/**
 * One progress report for a running tool call, sent through
 * {@link ToolExecutionContext.reportActivity}. Each report replaces the call's stored activity
 * snapshot, which clients can read. Observers and clients also receive a `tool.progress` event
 * with a checkpoint counter, `observedBytes`, `truncated`, and the artifact state, but not
 * `message` or `preview`. The model never sees activity.
 *
 * Omit optional fields instead of setting them to `undefined`; Core rejects explicit `undefined`.
 */
export interface ToolActivityUpdate {
  /** Short status line for clients, at most 1,024 characters. */
  readonly message?: string | undefined;
  /** Latest output excerpt for clients, at most 50 KiB of UTF-8. */
  readonly preview?: string | undefined;
  /** Total output bytes observed so far: a non-negative safe integer. */
  readonly observedBytes: number;
  /** Whether `preview` omits part of the observed output. */
  readonly truncated: boolean;
  /** Workspace file holding the full output, when the output spilled to one. */
  readonly artifact?: ToolOutputArtifact | undefined;
}

/** The second argument to {@link ToolDeclaration.execute}. */
export type ToolExecutionContext = {
  /** The Session running the call. */
  readonly sessionId: string;
  /** The Turn that requested the call. */
  readonly turnId: string;
  /** The Session's working directory. */
  readonly workdir: SessionWorkdir;
  /**
   * Identifies this call. Permission requests, questions, journal entries, and tool events for the
   * call carry the same value.
   */
  readonly toolCallId: string;
  /**
   * Aborts when the Turn is cancelled or ends, and, for a call made from a CodeMode script, when
   * that script call is interrupted. If it has aborted by the time `execute` settles, Core treats
   * the call as interrupted and discards the result, so stop work promptly.
   */
  readonly signal: AbortSignal;
  /**
   * The projections that authorized this call, after Core validated them and removed exact
   * duplicates. Read `metadata` from here to act on the same canonical target that was approved.
   */
  readonly permissions: readonly PermissionProjection[];
  /**
   * Reports progress for clients. Core applies one call's reports one at a time. Rejects with a
   * `PluginError` when the update is malformed or exceeds its limits.
   */
  readonly reportActivity: (activity: ToolActivityUpdate) => Promise<void>;
  /**
   * Asks the user one or more questions and waits for the reply with no time limit. Core persists
   * the request and publishes a `question.asked` event so a client can answer it. Resolves with one
   * answer per question, in order. Rejects with `QuestionDismissedError` when the user
   * dismisses the questions, when the host has no way to ask, or when the reply is malformed, and
   * with a `PluginError` when the prompts are invalid. A dismissal that `execute` does not
   * catch fails the call with code `question_dismissed`.
   */
  readonly askQuestion: (
    questions: readonly [QuestionPrompt, ...QuestionPrompt[]],
  ) => Promise<readonly [QuestionAnswer, ...QuestionAnswer[]]>;
  /**
   * Records one durable journal fact for this Turn, attributed to this plugin and call. The kind
   * must be registered by this plugin at setup. Resolves after the entry is stored. Rejects with a
   * `PluginError` without writing when the kind is unregistered, the data fails the kind's
   * schema or byte cap, or the report would pass the kind's per-Turn cap or the Turn-wide
   * `MAX_JOURNAL_ENTRIES_PER_TURN`; the Turn continues. The rejection carries no
   * `publicMessage`, so if `execute` lets it escape, the model sees only a generic failure.
   */
  readonly reportJournal: (entry: JournalEntry) => Promise<void>;
};

/**
 * The second argument to {@link ToolDeclaration.permissions}: the
 * {@link ToolExecutionContext} identity fields and signal, without its callbacks or permissions.
 */
export type ToolPermissionContext = Pick<
  ToolExecutionContext,
  "sessionId" | "turnId" | "workdir" | "toolCallId" | "signal"
>;

/** Identity of the model that will receive a request's tool descriptors. */
export interface ToolModelContext {
  /** Provider of the model. */
  readonly providerId: string;
  /** Catalog model id inside the provider; may be a configured alias. */
  readonly modelId: string;
  /** Model id sent to the provider. Prefer it for model-family decisions. */
  readonly upstreamId: string;
}

/**
 * A tool the model can call, registered with `context.tools.register` during setup.
 *
 * For each call the model makes, Core validates the input against `inputSchema`, runs
 * `tool.execute.before` hooks (revalidating any input they replace), calls `permissions` to get
 * the projections to authorize, and resolves approval. When one model response requests several
 * calls, Core prepares and authorizes them one at a time in the model's order, then runs every
 * approved `execute` concurrently. Invalid input fails only that call, and the model sees
 * `Tool input validation failed.`
 *
 * @typeParam TInput - Shape of the validated input. Core validates against `inputSchema` only;
 * keeping this type in line with the schema is the author's job.
 */
export interface ToolDeclaration<TInput extends object = JsonObject> {
  /**
   * Name the model calls the tool by. It must be non-empty and unique among the tools loaded
   * together. `skill` and `task` are reserved by Core, and a Session-scoped plugin cannot reuse a
   * host tool's id. It is also the default permission action, and Core hides the tool from the
   * model when the last permission rule whose action matches this id denies resource `*`.
   */
  readonly id: string;
  /** Non-empty description sent to the model with the tool. */
  readonly description: string;
  /**
   * Plain JSON Schema for the input object, sent to the model as the tool's parameters. Its root
   * must be `type: 'object'` and it must use only the keywords {@link JsonSchema} lists, or Core
   * rejects the declaration at setup. Core validates every call's input against it before
   * `permissions` and `execute` run.
   */
  readonly inputSchema: JsonSchema;
  /**
   * Pure, synchronous model filter. Core evaluates it per request for the model that receives the
   * tools, after Agent and permission filtering; returning false hides the tool from that request.
   * It can only remove the tool, never grant it. Absent means available to every model. A throw or
   * a non-boolean result is a plugin failure, not `false`. Plugins loaded as Worker bundles cannot
   * forward it, so their tools stay available to every model.
   */
  readonly availableForModel?:
    ((model: ToolModelContext) => boolean) | undefined;
  /**
   * Marks a workspace read whose successful result should discover nested `AGENTS.md` files. A
   * string result counts as a file read; a JSON result must carry `kind: 'file'` or
   * `kind: 'directory'`. Core finds the target in the `metadata.canonicalPath` of the call's `read`
   * projection, appends newly found instructions to the result, and fails the call when the result
   * kind or target is missing.
   */
  readonly discoversWorkspaceInstructions?: true | undefined;
  /**
   * Keeps the tool out of CodeMode scripts, so the model can only call it directly. Use it for
   * tools that interact with the user or otherwise need their own model turn.
   */
  readonly directOnly?: true | undefined;
  /**
   * Projects the validated, hook-transformed input into the action and resource checks Core
   * authorizes before `execute` runs. Core calls it once per call and validates the result.
   * Absent means one projection `{ action: id, resources: ['*'], save: ['*'] }`.
   *
   * A throw or rejection fails only this call: the model sees the `PluginError`
   * `publicMessage`, or `Tool execution failed.` A result that is not a non-empty array of valid
   * projections fails the Turn.
   */
  readonly permissions?:
    | ((
        input: TInput,
        context: ToolPermissionContext,
      ) => Promise<readonly PermissionProjection[]>)
    | undefined;
  /**
   * Runs one approved call and returns plain JSON; there is no output schema. Core sends a string
   * to the model as a text result and any other JSON value as a JSON result, and stores it with
   * the call. A value that is not JSON, including an object member set to `undefined`, fails the
   * call with code `invalid_output`.
   *
   * To fail the call, throw a `PluginError`: the model sees only its `publicMessage` (or
   * `Tool execution failed.` without one) and the Turn continues. Any other throw fails the call
   * with the generic message.
   */
  readonly execute: (
    input: TInput,
    context: ToolExecutionContext,
  ) => Promise<JsonValue>;
}
```

### `journal`

Exports: `JournalKindDeclaration`, `JournalEntry`.

```ts
/**
 * One durable fact kind this plugin may report, registered with `context.journal.registerKind`
 * during setup. Kinds are scoped to the registering plugin. An invalid or duplicate declaration
 * rejects the plugin's setup; a host-scope plugin is then skipped and reported as failed.
 */
export interface JournalKindDeclaration {
  /**
   * Kind name, unique within this plugin: at most `MAX_JOURNAL_KIND_LENGTH` (64) characters of
   * lowercase letters and digits, starting with a letter, with single `.`, `_`, or `-`
   * separators.
   */
  readonly kind: string;
  /**
   * Plain JSON Schema with a root `type: 'object'`, using only the keywords {@link JsonSchema}
   * lists. Core validates every entry's data against it.
   */
  readonly dataSchema: JsonSchema;
  /**
   * Maximum UTF-8 bytes of an entry's serialized data: an integer from 1 to
   * `MAX_JOURNAL_DATA_BYTES` (4,096). Absent means the global cap.
   */
  readonly maxDataBytes?: number | undefined;
  /**
   * Maximum entries of this kind per Turn: an integer from 1 to `MAX_JOURNAL_ENTRIES_PER_TURN`
   * (50). Absent means the global cap. Entries of every plugin and kind also share the Turn-wide
   * `MAX_JOURNAL_ENTRIES_PER_TURN`, so a report can be rejected below its own kind's cap.
   */
  readonly maxEntriesPerTurn?: number | undefined;
}

/**
 * One bounded durable fact reported with `reportJournal` from a tool or model context. Core
 * validates it against the kind's declaration and stores it with the Turn, attributed to the
 * plugin and the reporting call. Stored entries stay even if the Turn later fails. Clients read
 * them through the Session's plugin journal; the model does not see them.
 */
export interface JournalEntry {
  /** A kind this plugin registered at setup. */
  readonly kind: string;
  /**
   * One-line description for readers. Core truncates it to `MAX_JOURNAL_SUMMARY_LENGTH` (256)
   * characters instead of rejecting it, but rejects unpaired UTF-16 surrogates.
   */
  readonly summary: string;
  /**
   * The fact itself. It must match the kind's `dataSchema` and serialize within its byte cap, and
   * it must omit members rather than set them to `undefined`.
   */
  readonly data: JsonObject;
}
```

### `message`

Exports: `ImageMimeType`, `ImageReference`, `PromptTextPart`, `PromptImagePart`, `PromptContentPart`, `PromptContent`, `ReasoningPart`, `ReasoningBoundaryPart`, `RefusalPart`, `ModelToolCallPart`, `ToolResultContentPart`, `ToolResultValue`, `ToolResultPart`, `AssistantContentPart`, `ModelMessage`, `ModelInstruction`, `ModelInstructions`, `ModelToolChoice`, `ModelToolDescriptor`, `GenerationSettings`, `ImageReferenceKind`.

```ts
/** Image formats Core accepts. */
export type ImageMimeType =
  "image/jpeg" | "image/png" | "image/gif" | "image/webp";

/** Where an image's bytes come from. */
export type ImageReference =
  /**
   * An absolute `http` or `https` URL without embedded credentials. `mimeType` is optional and,
   * when present, must be one the model accepts.
   */
  | {
      readonly type: "url";
      readonly url: string;
      readonly mimeType?: ImageMimeType | undefined;
    }
  /** Inline canonical base64 data without a `data:` URI prefix, with its required MIME type. */
  | {
      readonly type: "base64";
      readonly data: string;
      readonly mimeType: ImageMimeType;
    };

/** A run of text. */
export interface PromptTextPart {
  readonly type: "text";
  /** The text. Core requires it to be non-empty. */
  readonly text: string;
}

/** An image in a prompt, a user message, or a tool result. */
export interface PromptImagePart {
  readonly type: "image";
  /** The image's source. */
  readonly source: ImageReference;
}

/** One part of user-authored content. */
export type PromptContentPart = PromptTextPart | PromptImagePart;

/** Non-empty user-authored content, such as a Turn's prompt. */
export type PromptContent = readonly [
  PromptContentPart,
  ...PromptContentPart[],
];

/** Reasoning the model exposes as text. */
export interface ReasoningPart {
  readonly type: "reasoning";
  /** The reasoning text. Core requires it to be non-empty. */
  readonly text: string;
}

/**
 * Marks the position of a reasoning item that has no visible text, so provider-private reasoning
 * state can stay attached to it. A model can only emit it whole, never as deltas.
 */
export interface ReasoningBoundaryPart {
  readonly type: "reasoning-boundary";
}

/** The model's refusal to answer. */
export interface RefusalPart {
  readonly type: "refusal";
  /** The refusal text. Core requires it to be non-empty. */
  readonly text: string;
}

/** One tool call made by the model. */
export interface ModelToolCallPart {
  readonly type: "tool-call";
  /** Non-empty call id, unique within its response. The matching result carries the same id. */
  readonly id: string;
  /** Non-empty name of the called tool. */
  readonly name: string;
  /**
   * The call's input. Core validates it against the tool's `inputSchema` before running the
   * tool; invalid input becomes an error result instead.
   */
  readonly input: JsonObject;
}

/** One part of a {@link ToolResultValue} of type `content`. */
export type ToolResultContentPart =
  /** Text; may be empty. */
  | {
      readonly type: "text";
      readonly text: string;
    }
  /** Any JSON value. */
  | {
      readonly type: "json";
      readonly value: JsonValue;
    }
  /** An image. */
  | PromptImagePart;

/**
 * Model-visible output of one tool call. A plugin tool's string result becomes `text` and any
 * other JSON result becomes `json`. Only Core-owned adapters, such as MCP tools, produce
 * `content`, which may include images. A failed call usually carries `json` with an `error`
 * message.
 */
export type ToolResultValue =
  /** Ordered parts, which may mix text, JSON, and images. */
  | {
      readonly type: "content";
      readonly content: readonly [
        ToolResultContentPart,
        ...ToolResultContentPart[],
      ];
    }
  /** A string result. */
  | {
      readonly type: "text";
      readonly text: string;
    }
  /** A JSON result. */
  | {
      readonly type: "json";
      readonly value: JsonValue;
    };

/** The result of one tool call, sent back to the model in a `tool` message. */
export interface ToolResultPart {
  readonly type: "tool-result";
  /** The `id` of the {@link ModelToolCallPart} this answers. */
  readonly toolCallId: string;
  /** What the model sees. */
  readonly output: ToolResultValue;
  /** True when the call failed; `output` then describes the failure. */
  readonly isError: boolean;
}

/** One part of an assistant message, kept in the order the model produced its outputs. */
export type AssistantContentPart =
  | PromptTextPart
  | ReasoningPart
  | ReasoningBoundaryPart
  | RefusalPart
  | ModelToolCallPart;

/** One message in the conversation sent to a model. Each message has non-empty `content`. */
export type ModelMessage =
  /**
   * Core-authored context placed at a point in the conversation, such as workspace instructions
   * discovered by a tool call.
   */
  | {
      readonly role: "developer";
      readonly content: readonly {
        readonly type: "text";
        readonly text: string;
      }[];
    }
  /** User input, such as the Turn's prompt or a follow-up message. */
  | {
      readonly role: "user";
      readonly content: readonly PromptContentPart[];
    }
  /** One earlier model response: its outputs in order, including any tool calls. */
  | {
      readonly role: "assistant";
      readonly content: readonly AssistantContentPart[];
    }
  /**
   * A result answering a call in the preceding assistant message. Core sends one result per
   * message, in call order.
   */
  | {
      readonly role: "tool";
      readonly content: readonly ToolResultPart[];
    };

/** One instruction that precedes the conversation. */
export interface ModelInstruction {
  /**
   * Instruction role. `system` carries the Session system prompt and Core's system-level notes;
   * `developer` carries supplementary guidance such as workspace instructions.
   */
  readonly role: "system" | "developer";
  /** The instruction. Core requires it to be non-empty. */
  readonly text: string;
}

/** Ordered instructions, sent before the messages of a model request. */
export type ModelInstructions = readonly ModelInstruction[];

/** How the model must use the request's tools. */
export type ModelToolChoice =
  /** The model decides whether to call tools. */
  | {
      readonly mode: "auto";
    }
  /** The model must not call tools. */
  | {
      readonly mode: "none";
    }
  /** The model must call at least one tool. The request must offer at least one. */
  | {
      readonly mode: "required";
    }
  /** The model must call the named tool. The request must offer it. */
  | {
      readonly mode: "tool";
      readonly name: string;
    };

/** One tool as the model sees it. */
export interface ModelToolDescriptor {
  /** Non-empty name the model calls the tool by, unique within a request. */
  readonly name: string;
  /** Non-empty description telling the model what the tool does. */
  readonly description: string;
  /** JSON Schema for the tool's input object. */
  readonly inputSchema: JsonSchema;
}

/** Generation settings for a model request. Every field is optional; absent means unset. */
export interface GenerationSettings {
  /** Sampling temperature; a finite number of at least 0. */
  readonly temperature?: number | undefined;
  /** Nucleus sampling probability, from 0 to 1. */
  readonly topP?: number | undefined;
  /** Maximum output units for one response; a positive integer within the model's output limit. */
  readonly maxOutputUnits?: number | undefined;
  /** Non-empty strings that end generation when produced. */
  readonly stopSequences?: readonly [string, ...string[]] | undefined;
  /** Sampling seed; a safe integer. */
  readonly seed?: number | undefined;
  /** How much reasoning to spend, for models that support it. */
  readonly reasoningEffort?:
    | "none"
    | "minimal"
    | "low"
    | "medium"
    | "high"
    | "xhigh"
    | "max"
    | undefined;
}

/** Kind of image reference a model accepts: a URL or inline base64 data. */
export type ImageReferenceKind = "url" | "base64";
```

### `model`

Exports: `PluginModelMetadata`, `PluginModelCandidate`, `PluginModelRequest`, `PluginModelEventUsage`, `PluginModelEvent`, `PluginModelStreamContext`, `ModelDeclaration`.

```ts
/**
 * Catalog metadata for one plugin-contributed model. Core validates every field when the plugin
 * activates and publishes the result in the model catalog; invalid metadata fails activation.
 */
export interface PluginModelMetadata {
  /** Name shown in the catalog. Non-empty, at most 256 characters, no surrounding whitespace. */
  readonly displayName: string;
  /** What the model accepts and produces. Core checks prompts and requests against it. */
  readonly capabilities: {
    /**
     * Accepted input modalities, without duplicates. Include `'image'` exactly when `image` is
     * set. Core rejects prompts and history with content the model does not accept.
     */
    readonly input: readonly ["text" | "image", ...("text" | "image")[]];
    /** Produced output modalities. Only `'text'` is supported. */
    readonly output: readonly ["text", ..."text"[]];
    /**
     * Whether the model takes tools. When false, requests carry no tool descriptors and a tool
     * call in the response fails the Turn.
     */
    readonly tools: boolean;
    /**
     * Accepted image reference kinds and MIME types, each without duplicates. Required when
     * `input` includes `'image'` and forbidden otherwise. An image whose kind or declared MIME
     * type is not listed is rejected before the model is called.
     */
    readonly image?:
      | {
          readonly referenceTypes: readonly [
            ImageReferenceKind,
            ...ImageReferenceKind[],
          ];
          readonly mimeTypes: readonly [ImageMimeType, ...ImageMimeType[]];
        }
      | undefined;
  };
  /**
   * Model limits as non-negative integers, where zero means not advertised. When `contextUnits`
   * is non-zero, `outputUnits` must not exceed it. Core sizes requests against `contextUnits`,
   * compacting history when a request would not fit, and rejects a `maxOutputUnits` setting
   * above a non-zero `outputUnits`.
   */
  readonly limits: {
    readonly contextUnits: number;
    readonly outputUnits: number;
  };
  /** Catalog price. Absent means `{ version: 'unknown' }`, which is not a zero price. */
  readonly pricing?:
    | {
        /** Known pricing. */
        readonly version: "v1";
        /** Three-letter uppercase currency code, such as `USD`. */
        readonly currency: string;
        /** Rates are per million model units. */
        readonly unit: "million_model_units";
        /** Prices as plain decimal strings without sign or exponent, such as `'0.15'`. */
        readonly rates: {
          /** Price of uncached input units. */
          readonly input: string;
          /** Price of output units, including reasoning. */
          readonly output: string;
          /** Price of input units read from a provider cache. */
          readonly cacheReadInput?: string | undefined;
          /** Price of input units written to a provider cache. */
          readonly cacheWriteInput?: string | undefined;
        };
      }
    | {
        /** The price is not known. */
        readonly version: "unknown";
      }
    | undefined;
  /**
   * Default generation settings for this model. Agent settings and per-request overrides take
   * precedence. `maxOutputUnits` must fit a non-zero `limits.outputUnits`.
   */
  readonly defaults?: GenerationSettings | undefined;
}

/**
 * One sibling model a plugin model may delegate to, as returned by
 * {@link PluginModelStreamContext.listCandidates}. Siblings are composed, non-plugin models that
 * are available and whose provider has a credential or needs none. Read-only data.
 */
export interface PluginModelCandidate {
  /** Catalog reference `providerId/modelId`. Pass it to {@link PluginModelStreamContext.delegate}. */
  readonly ref: string;
  /** Provider that serves the sibling. */
  readonly providerId: string;
  /** Catalog model id inside the provider. */
  readonly modelId: string;
  /** Name shown in the catalog. */
  readonly displayName: string;
  /** The sibling's input, output, tool, and image capabilities. */
  readonly capabilities: PluginModelMetadata["capabilities"];
  /** The sibling's limits; zero means not advertised. */
  readonly limits: PluginModelMetadata["limits"];
  /** The sibling's catalog price; `{ version: 'unknown' }` when not known. */
  readonly pricing: NonNullable<PluginModelMetadata["pricing"]>;
  /** The sibling's catalog generation defaults, or `undefined` when it has none. */
  readonly defaults: GenerationSettings | undefined;
}

/**
 * One provider-neutral model request. Core passes it to {@link ModelDeclaration.stream}, and a
 * plugin model passes one to {@link PluginModelStreamContext.delegate}.
 */
export interface PluginModelRequest {
  /**
   * Reference of the model being called, `providerId/modelId`. `delegate` ignores this field and
   * uses its `ref` argument instead.
   */
  readonly model: string;
  /**
   * Ordered instructions that precede the conversation, such as the Session system prompt and
   * workspace instructions.
   */
  readonly instructions: ModelInstructions;
  /**
   * The conversation so far, oldest first; never empty. After a tool round, the assistant message
   * that called the tools is followed by one `tool` message per result.
   */
  readonly messages: readonly ModelMessage[];
  /**
   * Tools the model may call this request, with unique names. Empty when the model does not take
   * tools. Core does not apply each Tool's `availableForModel` filter for a plugin model; it
   * applies it for the sibling when the request is delegated.
   */
  readonly tools: readonly ModelToolDescriptor[];
  /** How the model must use `tools`. `required` needs at least one tool; `tool` must name one. */
  readonly toolChoice: ModelToolChoice;
  /**
   * Effective generation settings. In a Turn these are the model defaults, then Agent settings,
   * then request overrides.
   */
  readonly settings: GenerationSettings;
  /**
   * Provider-specific options for this request only. Plugin models accept none, so Core passes an
   * empty object or `undefined`. On `delegate`, a value is forwarded as the sibling's options.
   */
  readonly providerOptions: JsonObject | undefined;
}

/**
 * Token-like usage for one model response, as non-negative integers. The three input buckets are
 * exclusive; the other fields are totals that must agree with them.
 */
export interface PluginModelEventUsage {
  /** Input units neither read from nor written to a provider cache. */
  readonly freshInputUnits: number;
  /** Input units read from a provider cache. */
  readonly cacheReadInputUnits: number;
  /** Input units written to a provider cache. */
  readonly cacheWriteInputUnits: number;
  /** All input units: the sum of the three input buckets. */
  readonly inputUnits: number;
  /** All output units, including `reasoningUnits`. */
  readonly outputUnits: number;
  /** Output units spent on reasoning; at most `outputUnits`. */
  readonly reasoningUnits: number;
  /** `inputUnits + outputUnits`, without counting reasoning twice. */
  readonly totalUnits: number;
}

/**
 * One event in a plugin model's response stream. Core validates each value and the sequence:
 *
 * - `ResponseStarted` comes first, exactly once.
 * - Each new output, content or tool call, takes the next `index`, starting at 0. Outputs may
 *   interleave, but every output must complete before `ResponseFinished`.
 * - `Usage` is optional, at most once, after every output has completed; no output events follow.
 * - `ResponseFinished` comes last. Ending the stream without it, or yielding anything after it,
 *   fails the request.
 *
 * Core assembles the completed outputs, in `index` order, into the assistant message that
 * history and the next request carry. Any violation fails the model request as malformed.
 */
export type PluginModelEvent =
  /** The response has begun. A failure after this event is not retried. */
  | {
      readonly _tag: "ResponseStarted";
    }
  /**
   * A fragment of text, reasoning, or refusal output. The first delta for an `index` opens the
   * output; later deltas for it must repeat its `contentId` and `contentType`.
   */
  | {
      readonly _tag: "ContentDelta";
      /** Position of this output in the response. */
      readonly index: number;
      /** Non-empty id of this output, unique among the response's outputs. */
      readonly contentId: string;
      /** Kind of output. A reasoning boundary has no deltas. */
      readonly contentType: "text" | "reasoning" | "refusal";
      /** Non-empty text fragment. A surrogate pair may span two deltas. */
      readonly delta: string;
    }
  /**
   * Completes one content output. After deltas, `content` must match their `contentType`, and its
   * text must equal their concatenation. Without prior deltas, it adds a complete output at the
   * next index; a `reasoning-boundary` part can only be added this way.
   */
  | {
      readonly _tag: "ContentCompleted";
      /** Position of this output in the response. */
      readonly index: number;
      /** Non-empty output id; the same as its deltas used, if any. */
      readonly contentId: string;
      /** The complete output. Text, reasoning, and refusal text must be non-empty. */
      readonly content:
        PromptTextPart | ReasoningPart | ReasoningBoundaryPart | RefusalPart;
    }
  /**
   * A fragment of a tool call's JSON input. The first delta for an `index` opens the call; later
   * deltas for it must repeat its `toolCallId` and `toolName`.
   */
  | {
      readonly _tag: "ToolCallInputDelta";
      /** Position of this output in the response. */
      readonly index: number;
      /** Non-empty call id, unique among the response's outputs. */
      readonly toolCallId: string;
      /** Non-empty name of the called tool. */
      readonly toolName: string;
      /** Non-empty fragment of the input's JSON text. */
      readonly delta: string;
    }
  /**
   * Completes one tool call. After input deltas, its id and name must match them, and their
   * concatenation must parse to a JSON object equal to `toolCall.input`. Without prior deltas, it
   * adds a complete call at the next index.
   */
  | {
      readonly _tag: "ToolCallCompleted";
      /** Position of this output in the response. */
      readonly index: number;
      /**
       * The complete call. Its `name` must be a tool from the request; a call to any other name
       * fails the Turn.
       */
      readonly toolCall: ModelToolCallPart;
    }
  /**
   * Usage for this response. Core adds it to the Turn's usage totals, reports context-window use
   * from `totalUnits` against this model's `contextUnits`, and publishes it to observers.
   */
  | {
      readonly _tag: "Usage";
      /** Units consumed by this response. */
      readonly usage: PluginModelEventUsage;
    }
  /** Ends the response. Nothing may follow it. */
  | {
      readonly _tag: "ResponseFinished";
      /**
       * Why the response ended:
       *
       * - `stop`: the model finished naturally.
       * - `tool-calls`: the model called tools. Required exactly when the response completed at
       *   least one tool call. Core then runs the calls and calls the model again with their
       *   results.
       * - `length`: the output limit was reached.
       * - `content-filter`: output was withheld by a content filter.
       * - `other`: any other reason.
       *
       * With any other reason Core runs no tools, and the Turn settles unless queued user input
       * continues it. Context summaries require `stop`.
       */
      readonly reason:
        "stop" | "tool-calls" | "length" | "content-filter" | "other";
    };

/** Per-call context Core passes to {@link ModelDeclaration.stream}. */
export interface PluginModelStreamContext {
  /** Session this call belongs to, or empty when Core calls the model without one. */
  readonly sessionId: string;
  /**
   * Turn this call belongs to. Empty outside a Turn, such as when Core generates a Session title
   * or summarizes context between Turns.
   */
  readonly turnId: string;
  /**
   * Aborted when the Turn is cancelled or ends, or when a call outside a Turn ends. Pass it to
   * any I/O. Once it is aborted, Core treats a throw from the stream as cancellation.
   */
  readonly signal: AbortSignal;
  /**
   * Lists the sibling models this call may delegate to: available, composed models whose
   * provider has a credential or needs none. Plugin-contributed models are never included. Core
   * takes the snapshot when it acquires the model, once per Turn, so the list does not change
   * mid-Turn. It is never empty: when no sibling is available, the model call fails as
   * unavailable before `stream` runs.
   *
   * @returns The sibling candidates.
   */
  readonly listCandidates: () => Promise<readonly PluginModelCandidate[]>;
  /**
   * Sends one request to a sibling model and yields its validated events as they arrive. Yield
   * them unchanged (for example with `yield*`) to answer with the sibling's response; they
   * already form one complete, valid sequence. Ending iteration early releases the sibling.
   *
   * In a Turn, Core removes the tools the sibling cannot use before sending, and only tools
   * offered to the sibling that served the response can then be executed.
   *
   * @param ref - A candidate `ref` from `listCandidates`.
   * @param request - The request to send. Its `model` field is ignored.
   * @returns The sibling's events.
   * @throws `PluginError` during iteration when `ref` is not a sibling candidate, when the
   *   request is invalid (including a `toolChoice` that names a tool the sibling cannot use), or
   *   when the sibling fails. Rethrow that same error unchanged so Core reports the sibling's
   *   original failure and retries it when retryable; any other error becomes a generic plugin
   *   model failure.
   */
  readonly delegate: (
    ref: string,
    request: PluginModelRequest,
  ) => AsyncIterable<PluginModelEvent>;
  /**
   * Reports one bounded durable fact about this model call. The kind must be registered at
   * setup; Core validates the entry and stores it as soon as it is accepted. Outside a Turn,
   * Core accepts the entry and discards it.
   *
   * @param entry - The fact to record.
   * @throws `PluginError` when Core rejects the entry, such as for an unregistered kind, invalid
   *   data, or a report past its kind's `maxEntriesPerTurn` or `MAX_JOURNAL_ENTRIES_PER_TURN`.
   *   Nothing is written and the Turn continues.
   */
  readonly reportJournal: (entry: JournalEntry) => Promise<void>;
}

/**
 * A model a plugin contributes to the catalog, registered with `context.models.register` during
 * setup. Core lists it in the catalog as `providerId/modelId`, where it can be selected like any
 * other model, and calls `stream` for each model request. A router delegates each request to a
 * sibling; a synthetic model yields its own events.
 */
export interface ModelDeclaration {
  /**
   * Provider id, at most 128 characters: lowercase letters and digits in segments joined by `.`,
   * `_`, or `-`, starting with a letter. It must not name a composed native provider. Several
   * declarations may share one provider id.
   */
  readonly providerId: string;
  /**
   * Provider name shown in the catalog, with the same rules as a model display name. Declarations
   * that share a provider id use the first registered name.
   */
  readonly providerDisplayName: string;
  /**
   * Model id within the provider, at most 512 characters: letters, digits, `.`, `_`, `:`, and `-`,
   * in `/`-separated segments that each start with a letter or digit. `providerId/modelId` must be
   * unique across the catalog.
   */
  readonly modelId: string;
  /** Catalog metadata. Core validates it when the plugin activates. */
  readonly metadata: PluginModelMetadata;
  /**
   * Produces the response to one model request as a stream of events. Core calls it for every
   * model request to this model: each step of a Turn's model and tool loop, each retry, and
   * calls outside a Turn such as Session titles and context summaries. A response that finishes
   * with `tool-calls` leads Core to run the tools and call `stream` again with their results.
   *
   * Core pulls the iterable one event at a time and validates each event and the sequence. Core
   * retries a request, calling `stream` again, only for a retryable sibling failure rethrown from
   * `delegate` before this stream yields `ResponseStarted`.
   *
   * @param request - The request to answer.
   * @param context - Session and Turn identity, the abort signal, and sibling access.
   * @returns An async iterable of events, such as an async generator.
   * @throws Any error to fail the model request. A `delegate` error rethrown unchanged keeps the
   *   sibling's failure. Any other error fails without retry; the diagnostic includes the start of
   *   a thrown `PluginError`'s `message` (its `publicMessage` is not used), or is generic for other
   *   errors.
   */
  readonly stream: (
    request: PluginModelRequest,
    context: PluginModelStreamContext,
  ) => AsyncIterable<PluginModelEvent>;
}
```

### `hook`

Exports: `TurnBeforeInput`, `ModelRequestBeforeInput`, `ToolExecuteBeforeInput`, `ToolExecuteBeforeResult`, `ToolExecuteAfterInput`, `HookHandlers`, `HookName`, `HookHandler`, `HookResult`.

```ts
/** Input to `turn.before`. Read-only; the handler cannot change the prompt or model. */
export type TurnBeforeInput = {
  /** The Turn that is starting. */
  readonly turnId: string;
  /** The Turn's user prompt, with stored content references resolved. */
  readonly prompt: PromptContent;
  /** The provider ID and catalog model ID that serve the Turn. */
  readonly model: {
    readonly providerId: string;
    readonly modelId: string;
  };
};

/** Input to `model.request.before`. Read-only; the handler cannot change the request. */
export type ModelRequestBeforeInput = {
  /** The Turn making the request. */
  readonly turnId: string;
  /** Provider ID of the Turn's model. For a plugin model, this is the plugin model's provider. */
  readonly providerId: string;
  /** Catalog model ID of the Turn's model. */
  readonly modelId: string;
  /**
   * Zero-based index of this request within the Turn. It advances each time the hooks run, so it
   * counts requests that were prepared but not sent.
   */
  readonly iteration: number;
  /**
   * The Session's system prompt, when one is set. Other instructions Core adds to the request are
   * not included.
   */
  readonly systemPrompt?: string | undefined;
  /** The conversation messages Core will send, after fitting the request to the context window. */
  readonly messages: readonly ModelMessage[];
};

/** Input to `tool.execute.before`. */
export type ToolExecuteBeforeInput = {
  /** The Turn making the call. */
  readonly turnId: string;
  /** ID of the Tool being called. */
  readonly toolId: string;
  /** ID of this call, matching `toolCallId` on the call's Turn events. */
  readonly toolCallId: string;
  /**
   * The call's input as a JSON object, already validated against the Tool's `inputSchema`. When an
   * earlier hook replaced it, this is the replacement.
   */
  readonly input: unknown;
};

/** What a `tool.execute.before` handler may return. Return nothing to leave the call unchanged. */
export type ToolExecuteBeforeResult = {
  /**
   * Replacement input for the call. Core uses it whenever the property is present, even when its
   * value is `undefined`, and validates it against the Tool's `inputSchema` again. Invalid
   * replacement input fails the call with an input validation error the model sees; the Tool does
   * not run and later hooks are skipped. Valid input is what later hooks, the Tool's `permissions`
   * projector, and `execute` receive.
   */
  readonly input?: unknown;
  /**
   * Decides the call's permission checks. When any hook returns `deny`, every check for the call is
   * denied, even ones configuration allows. A direct call then fails the Turn with fault code
   * `tool_denied`; a call from a CodeMode script is rejected inside the script instead. `allow`
   * from any hook, with no `deny`, allows each check that neither configuration nor the Session's
   * saved permissions already decide, so the user is not asked. Configured denials still apply.
   * Either decision appears as `source: 'hook'` on `approval.resolved` events. Omit it to leave
   * permission resolution unchanged.
   */
  readonly approval?: ToolApprovalDecision | undefined;
};

/** Input to `tool.execute.after`. */
export type ToolExecuteAfterInput = ToolExecuteBeforeInput & {
  /** Whether the call succeeded. */
  readonly ok: boolean;
  /**
   * The result the model sees: a string for text results, a JSON value for JSON results, or an
   * array of content parts. A failed call usually has `{ error: string }` with the safe failure
   * message.
   */
  readonly result: unknown;
};

/**
 * Hook names and their handler signatures. Register with `context.hooks.register(name, handler)`.
 *
 * Core awaits every handler, one at a time: host hooks first, then Session hooks, each in plugin
 * order and then registration order. A throw or rejection fails the Turn with fault code
 * `runtime_failed`. The fault message is the `publicMessage` of a thrown `PluginError`, trimmed and
 * cut to 4,096 characters, or a generic message when there is none.
 */
export interface HookHandlers {
  /**
   * Runs once per Turn, after the `turn.started` event and before the first model request.
   * Core ignores the returned value.
   */
  readonly "turn.before": (input: TurnBeforeInput) => Promise<void>;
  /**
   * Runs before each model request in the Turn. Core can still skip sending the request after the
   * hooks run, for example when a steering message arrives or the Turn is cancelled. Core ignores
   * the returned value.
   */
  readonly "model.request.before": (
    input: ModelRequestBeforeInput,
  ) => Promise<void>;
  /**
   * Runs before each Tool call whose model input passed validation, including calls from CodeMode
   * scripts, and before Core projects and resolves the call's permissions. Each handler sees the
   * input as replaced by earlier handlers. Returning a value that is not an object, or an
   * `approval` other than `allow` or `deny`, fails the Turn.
   */
  readonly "tool.execute.before": (
    input: ToolExecuteBeforeInput,
  ) => Promise<void | ToolExecuteBeforeResult>;
  /**
   * Runs after a Tool call that Core executed has settled, been recorded, and published its
   * `tool.completed` event. It does not run for calls that never executed, such as denied calls
   * or calls with invalid input. Core ignores the returned value.
   */
  readonly "tool.execute.after": (
    input: ToolExecuteAfterInput,
  ) => Promise<void>;
}

/** The name of a hook. */
export type HookName = keyof HookHandlers;

/** The handler signature for one hook name. */
export type HookHandler<TName extends HookName> = HookHandlers[TName];

/** The settled result of any plain hook handler. */
export type HookResult = void | ToolExecuteBeforeResult;
```

### `agent`

Exports: `AgentToolSelection`, `AgentMode`, `AgentDeclaration`.

```ts
/**
 * Which Tools an Agent offers the model. Names are effective Tool IDs, including Core Tools such as
 * `skill` and `task` and projected MCP Tool names. Agent selection can only narrow the catalog;
 * permission rules and per-model filters still apply afterwards.
 */
export type AgentToolSelection =
  /** Every Tool in the effective catalog. This is the default. */
  | {
      readonly mode: "all";
    }
  /** No Tools. The CodeMode executor is also withheld. */
  | {
      readonly mode: "none";
    }
  /**
   * Only the named Tools. Omitting `skill` hides Skills from the model and rejects user-selected
   * Skills; omitting `task` prevents spawning sub-agents.
   */
  | {
      readonly mode: "include";
      /**
       * Required Tool names: unique, lowercase identifiers. A Turn fails when one is not in the
       * effective catalog.
       */
      readonly names: readonly [string, ...string[]];
      /**
       * Tool names included only when the effective catalog provides them. They must be unique and
       * must not repeat a required name.
       */
      readonly optionalNames?: readonly string[] | undefined;
    };

/**
 * Where an Agent may run: `primary` Agents take user Turns, `subagent` Agents run only as children
 * spawned through the `task` Tool, and `all` runs in either seat.
 */
export type AgentMode = "primary" | "subagent" | "all";

/**
 * An Agent profile registered with `context.agents.register`. Core validates it when the plugin
 * activates; an invalid declaration fails activation.
 *
 * Agent IDs share one namespace per scope: two host plugins cannot declare the same ID, and a
 * Session plugin cannot reuse a host Agent ID. On the host, a configured Agent entry with the same
 * ID overrides the declared display name, description, instructions, model, and mode, appends its
 * permission rules after the declared ones, and can disable the Agent. Configuration does not apply
 * to Session-scope Agents.
 */
export interface AgentDeclaration {
  /**
   * Stable Agent ID of at most 128 characters: one or more `/`-separated segments, each starting
   * with a lowercase letter and made of lowercase letters and digits joined by single `.`, `_`, or
   * `-` characters (`review`, `team/review-docs`). Clients and the `task` Tool select the Agent by
   * this ID.
   */
  readonly id: string;
  /** Name shown to clients. Single-line, trimmed, NFC-normalized text of at most 256 characters. */
  readonly displayName: string;
  /**
   * What the Agent is for, shown to clients. For an Agent that can run as a sub-agent, the model
   * also sees it in the `task` Tool's list of available agent types. Single-line, trimmed,
   * NFC-normalized text of at most 1,024 characters.
   */
  readonly description: string;
  /**
   * System and developer instructions for every model request this Agent makes. They come first,
   * before Core's other instructions such as the system prompt, MCP server instructions, and the
   * Skill list. Defaults to none.
   */
  readonly instructions?: ModelInstructions | undefined;
  /**
   * Catalog reference in `provider/model` form. A Turn uses it when neither the prompt nor the
   * Session selects a model; without it, the host's default model applies. A spawned sub-agent
   * uses it, falling back to its parent Turn's model. Core resolves it when a Turn runs, and the
   * Turn fails if no available model matches.
   */
  readonly model?: string | undefined;
  /** Tools offered to the model. Defaults to `{ mode: 'all' }`. */
  readonly tools?: AgentToolSelection | undefined;
  /**
   * How the model must use Tools. Defaults to `auto`, which Core sends as `none` when the request
   * has no Tools. A Turn fails when the mode is `required` and no Tools remain, or when it names a
   * Tool missing from the effective catalog.
   */
  readonly toolChoice?: ModelToolChoice | undefined;
  /**
   * Generation settings layered over the model's catalog defaults; a prompt's reasoning effort
   * overrides this value. A Turn fails when `maxOutputUnits` exceeds the model's output limit, or
   * when the model advertises its reasoning efforts and this one is not among them.
   */
  readonly settings?: GenerationSettings | undefined;
  /**
   * Permission rules appended after the host's and Session's rules. The last matching rule wins, so
   * these take precedence. A spawned sub-agent also inherits its parent Session's deny rules, and
   * cannot use `task` unless one of these rules matches the `task` action.
   */
  readonly permissions?: PermissionRuleset | undefined;
  /** Where this Agent may run. Defaults to `all`. A `subagent` Agent cannot be the default. */
  readonly mode?: AgentMode | undefined;
  /**
   * Prefers this primary-capable Agent as the default when neither `context.agents.setDefault` nor
   * configuration selects one. When several Agents set it, the first by lexical ID wins. Without
   * any preference, Core falls back to the first primary-capable Agent by lexical ID.
   */
  readonly fallbackDefault?: boolean | undefined;
}
```

### `skill`

Exports: `SkillDeclaration`, `SkillHttpHeader`, `WorkspaceSkillSource`, `HttpSkillSource`, `SkillSource`, `Skill`, `SkillTransformPatch`, `SkillTransform`.

```ts
/**
 * A Skill registered directly with `context.skills.register`. Core validates it when the plugin
 * activates; an invalid declaration fails activation.
 *
 * The model loads a Skill by calling Core's `skill` Tool with its exact ID and receives its base
 * location, body, and supporting-file manifest as the Tool result. That Tool is direct-only, and
 * loading a Skill is authorized as the `skill` action on the Skill ID. Skill IDs share one
 * namespace per scope across direct registrations and sources: a duplicate fails activation
 * (except that a Skill discovered by a `directory` source replaces an earlier discovered one), and
 * a Session Skill cannot reuse a host Skill ID.
 */
export interface SkillDeclaration {
  /**
   * Stable Skill ID and the value passed to the `skill` Tool. At most 128 characters: a lowercase
   * letter followed by lowercase letters and digits joined by single `.`, `_`, or `-` characters.
   */
  readonly id: string;
  /**
   * Name shown to clients and in the model's Skill list. Non-empty, trimmed, NFC-normalized text
   * without control characters.
   */
  readonly displayName: string;
  /**
   * When to use the Skill, shown to clients and in the model's Skill list. Non-empty, single-line,
   * trimmed, NFC-normalized text without control characters.
   */
  readonly description: string;
  /**
   * Markdown instructions returned when the Skill loads. Must contain non-whitespace text, be
   * NFC-normalized, and contain no carriage returns or control characters other than tab and
   * newline.
   */
  readonly body: string;
  /**
   * Skill content version, such as `1.0.0`. It starts with a letter or digit and uses only
   * letters, digits, and `._+:-`. It is not sent to the model.
   */
  readonly version: string;
  /** How the Skill is offered to the model and the user. */
  readonly modelAdvertisement: {
    /**
     * Lists the Skill (ID, name, description, and `userInvocable`) in a developer instruction on
     * each model request whose Tools include `skill` and whose permissions do not deny loading it.
     * An unadvertised Skill can still be loaded by exact ID.
     */
    readonly advertised: boolean;
    /**
     * Lets a user select the Skill in a prompt. Core loads selected Skills through the `skill`
     * Tool before the first model call. A Turn fails when its prompt selects a Skill without this
     * flag, or when the Agent's Tools do not include `skill`.
     */
    readonly userInvocable: boolean;
  };
  /**
   * Descriptive metadata kept in the Skill catalog and passed to Skill transforms. It is not sent
   * to the model or included in client Skill listings.
   */
  readonly publicMetadata: {
    /** Author name: trimmed, NFC-normalized text without control characters. */
    readonly author?: string | undefined;
    /** License name, such as `MIT` or `Apache-2.0`: letters, digits, spaces, and `.()+-`. */
    readonly license?: string | undefined;
    /** Canonical `https://` URL with no credentials, query, or fragment. */
    readonly homepage?: string | undefined;
    /** Unique lowercase identifier tags, such as `computer-use`. May be empty. */
    readonly tags: readonly string[];
  };
  /**
   * Supporting-file paths relative to the Skill: unique, lowercase relative paths of letters,
   * digits, `.`, `_`, `-`, and `/`, with no `.` or `..` segments. Defaults to none. A
   * plugin-registered Skill has no location an ordinary Tool can read, so the loaded Skill reports
   * only how many supporting files it omits.
   */
  readonly files?: readonly string[] | undefined;
}

/** One private HTTP header sent with every request an HTTP Skill source makes. */
export interface SkillHttpHeader {
  /** HTTP header name. Names must be unique, ignoring case. */
  readonly name: string;
  /** Header value. It must be a valid HTTP field value. */
  readonly value: string;
}

/**
 * Loads Skills from the workspace through the host's workspace file access. Core reads the source
 * when the plugin's scope activates, not on each Turn. Supporting files of these Skills are listed
 * for the model to read with the ordinary `read` Tool.
 */
export interface WorkspaceSkillSource {
  readonly kind: "workspace";
  /**
   * Source ID, unique in its scope and distinct from the IDs of plugins that register Skills
   * directly. Same character rules as {@link SkillDeclaration.id}.
   */
  readonly id: string;
  /**
   * Directory to load. For the `catalog` format it is a portable lowercase relative path; for the
   * `directory` format it is any path the host's workspace file access can read.
   */
  readonly directory: string;
  /**
   * `catalog` (the default) reads `<directory>/catalog.json`, a Harness catalog of the form
   * `{ "schemaVersion": 1, "version": "1.0.0", "skills": [{ "body": "<id>/skill.md" }] }`, where
   * each entry may also list `files` under its Skill directory.
   * Each `skill.md` starts with `---`-delimited `key: value` lines for `id`, `name`, `description`,
   * `version`, `advertised`, and `user-invocable`, plus optional `author`, `license`, `homepage`,
   * and a JSON `tags` array; `id` must match the Skill's directory. Any invalid entry fails
   * activation.
   *
   * `directory` scans the tree for standard `SKILL.md` files with YAML frontmatter. `name` and
   * `description` are required, `name` becomes the Skill ID and display name, `version` defaults
   * to `1`, and Skills are advertised and user-invocable unless their metadata says otherwise.
   * Unreadable or malformed files are skipped with a warning, and when two files declare the same
   * name, the later path in lexical order wins.
   */
  readonly format?: "catalog" | "directory" | undefined;
}

/**
 * Loads a Harness Skill catalog over HTTP when the plugin's scope activates. Core follows
 * redirects only within the catalog's origin, requires status 200, and accepts only JSON catalogs
 * and Markdown or plain-text Skill bodies located under the catalog's directory. Any failure fails
 * activation.
 */
export interface HttpSkillSource {
  readonly kind: "http";
  /**
   * Source ID, unique in its scope and distinct from the IDs of plugins that register Skills
   * directly. Same character rules as {@link SkillDeclaration.id}.
   */
  readonly id: string;
  /**
   * Absolute, canonical `http://` or `https://` URL whose path ends in `/catalog.json`, with no
   * credentials, query, fragment, or percent-encoding. It must use `https://` when `headers` is not
   * empty.
   */
  readonly catalogUrl: string;
  /**
   * Private headers, such as an authorization token, sent with every catalog and body request.
   * When any are set, the source's supporting files are not listed for the model, because no
   * ordinary Tool can send these headers.
   */
  readonly headers: readonly SkillHttpHeader[];
}

/**
 * A Skill source registered with `context.skills.registerSource`. Not available to plugins loaded
 * as Worker bundles.
 */
export type SkillSource = WorkspaceSkillSource | HttpSkillSource;

/** A validated Skill as it appears in the catalog. Skill transforms receive this value. */
export interface Skill {
  /** Skill ID, the value passed to the `skill` Tool. */
  readonly id: string;
  /** Name shown to clients and in the model's Skill list. */
  readonly displayName: string;
  /** Single-line summary shown to clients and in the model's Skill list. */
  readonly description: string;
  /** Markdown instructions returned when the Skill loads. */
  readonly body: string;
  /**
   * Where supporting files resolve: `ze-plugin:<pluginId>` for a plugin-registered Skill, the
   * Skill's directory for a workspace source, or its package URL for an HTTP source. The loaded
   * Skill shows it to the model.
   */
  readonly baseLocation: string;
  /** The source that provided the Skill. A plugin-registered Skill's source ID is its plugin ID. */
  readonly source: {
    readonly id: string;
    /** `plugin` for a Skill from `context.skills.register`; otherwise the source kind. */
    readonly kind: "plugin" | "workspace" | "http";
  };
  readonly version: {
    /** The Skill's own `version`. */
    readonly skillVersion: string;
    /**
     * The source's version: the catalog `version`, a digest of a scanned directory's contents, or,
     * for a plugin-registered Skill, the ID of the host generation or Session plugin revision that
     * registered it.
     */
    readonly sourceVersion: string;
  };
  /** See {@link SkillDeclaration.modelAdvertisement}. */
  readonly modelAdvertisement: SkillDeclaration["modelAdvertisement"];
  /** See {@link SkillDeclaration.publicMetadata}. */
  readonly publicMetadata: SkillDeclaration["publicMetadata"];
  /** Supporting-file paths relative to `baseLocation`, in lexical order. */
  readonly files: readonly string[];
}

/**
 * Fields a Skill transform replaces. Each present field replaces the whole current value, including
 * the nested `modelAdvertisement` and `publicMetadata` objects; absent fields stay unchanged. The
 * result must satisfy the same rules as a {@link SkillDeclaration}. ID, base location, source, and
 * files cannot change.
 */
export interface SkillTransformPatch {
  readonly displayName?: string | undefined;
  readonly description?: string | undefined;
  readonly body?: string | undefined;
  /** Replaces `version.skillVersion`. */
  readonly version?: string | undefined;
  readonly modelAdvertisement?:
    SkillDeclaration["modelAdvertisement"] | undefined;
  readonly publicMetadata?: SkillDeclaration["publicMetadata"] | undefined;
}

/**
 * Rewrites Skills, registered with `context.skills.registerTransform`. After every Skill and
 * source in a scope has loaded, Core runs that scope's transforms in registration order over every
 * Skill in the scope, including Skills from other plugins; each transform sees the previous one's
 * output. Not available to plugins loaded as Worker bundles.
 */
export interface SkillTransform {
  /** Transform ID, unique in its scope. Same character rules as {@link SkillDeclaration.id}. */
  readonly id: string;
  /**
   * Returns the fields to replace, or `undefined` to keep the Skill unchanged. A rejection, a
   * throw, or an invalid patch fails activation.
   */
  readonly transform: (
    skill: Skill,
  ) => Promise<SkillTransformPatch | undefined>;
}
```

### `mcp`

Exports: `ExecutionLimit`, `McpCredentialReference`, `McpLimits`, `McpReconnectPolicy`, `McpClientCapabilities`, `McpServerDefinition`.

```ts
/** A bound that is either `unlimited` or `finite` with a positive safe-integer `value`. */
export type ExecutionLimit =
  | {
      readonly kind: "unlimited";
    }
  | {
      readonly kind: "finite";
      readonly value: number;
    };

/**
 * Names a credential the host resolves when it opens the connection, so secret values never
 * appear in the definition. `reference` is 1 to 256 characters: a letter followed by letters,
 * digits, and `_.:/-`.
 *
 * - `environment` names a variable in the host's environment. The Node host resolves it from its
 *   own process environment unless the embedding application supplies its own resolver.
 * - `secret` names a secret in a host-provided credential store; the Node host resolves it only
 *   through an injected resolver.
 * - `oauth` names an OAuth credential; use it as a remote transport's `oauth` field.
 *
 * An unresolved reference fails the connection attempt.
 */
export type McpCredentialReference = {
  readonly kind: "environment" | "secret" | "oauth";
  readonly reference: string;
};

/** Time limits in milliseconds. Reaching one fails that operation. */
export interface McpLimits {
  /** One connection attempt: starting the transport, initializing, and reading the catalog. */
  readonly startupMs: ExecutionLimit;
  /** One catalog read, including the read during connection and rereads after a change. */
  readonly catalogMs: ExecutionLimit;
  /** One Tool call, resource read, or prompt resolution. */
  readonly executionMs: ExecutionLimit;
}

/**
 * How Core reconnects after a retryable connection failure, including the first connection
 * attempt. Non-retryable failures are never retried automatically.
 */
export type McpReconnectPolicy =
  /** No automatic retries. The server stays unavailable until the host requests a reconnect. */
  | {
      readonly kind: "manual";
    }
  /**
   * Retries every `delayMs` milliseconds (a positive integer). `maxAttempts` bounds the attempts,
   * counting the first; once it is reached the server stays unavailable until the host requests a
   * reconnect.
   */
  | {
      readonly kind: "retry";
      readonly delayMs: number;
      readonly maxAttempts: ExecutionLimit;
    };

/**
 * Client capabilities Core announces to the server. Core supports none of roots, sampling, or
 * elicitation, so every field must be `false`.
 */
export interface McpClientCapabilities {
  readonly roots: false;
  readonly sampling: false;
  readonly elicitation: false;
}

/** Fields shared by local and remote MCP server definitions. */
interface McpCommonServerDefinition {
  /**
   * Stable server identity, used for credential ownership and status. At most 128 characters: a
   * lowercase letter followed by lowercase letters and digits joined by single `.`, `_`, or `-`
   * characters.
   */
  readonly id: string;
  /**
   * Server name, with the same character rules as `id`. Core exposes each server Tool to the model
   * as `<name>_<tool>`, replacing characters outside `A-Z`, `a-z`, `0-9`, `_`, and `-` with `_`,
   * and clients address the server's resources and prompts by this name.
   */
  readonly name: string;
  /**
   * Defaults to `true`. A disabled server stays registered and listed but never connects and
   * contributes no Tools.
   */
  readonly enabled?: boolean | undefined;
  /**
   * Sort key, a non-negative integer that defaults to `0`. Servers are ordered by `order`, then
   * host servers before Session servers, then by `name`; the order sets the sequence of their Tools
   * and instructions in model requests.
   */
  readonly order?: number | undefined;
  /**
   * Also exposes a `<name>_read-resource` Tool that lets the model read the server's resources by
   * URI. Defaults to `false`, which leaves resource reads to clients.
   */
  readonly resourceReadTool?: boolean | undefined;
  /**
   * MCP protocol versions to accept, without duplicates. Every entry must be a version Core
   * supports, currently only `2025-06-18`, and the server must answer with one of them.
   */
  readonly protocolVersions: readonly [string, ...string[]];
  /** Defaults to every capability `false`. */
  readonly clientCapabilities?: McpClientCapabilities | undefined;
  /** Defaults to every limit `unlimited`. */
  readonly limits?: McpLimits | undefined;
  /** Defaults to retrying every 1,000 ms with unlimited attempts. */
  readonly reconnect?: McpReconnectPolicy | undefined;
}

/**
 * An MCP server registered with `context.mcp.register`. Core validates it when the plugin
 * activates; an invalid definition fails activation, as does an `id` or `name` already used in
 * the same scope. The host scope also includes servers from host configuration, and a Session
 * server cannot reuse a host server's `id` or `name`.
 *
 * Enabled servers start connecting in the background when their scope activates. Each model
 * request uses the Tools of the servers that are connected at that moment, so an unavailable
 * server contributes nothing rather than failing the Turn. A projected Tool name that duplicates
 * another Tool fails the Turn. Server instructions from the MCP handshake are added to model
 * requests as developer instructions, and Core re-reads a server's catalog when the server reports
 * that it changed. Not available to plugins loaded as Worker bundles.
 */
export type McpServerDefinition =
  /** A server the host starts as a child process and talks to over stdio. */
  | (McpCommonServerDefinition & {
      readonly kind: "local";
      readonly transport: {
        readonly kind: "stdio";
        /** Executable to run. The Node host starts it in its configured working directory. */
        readonly command: string;
        /** Command-line arguments, passed as-is. */
        readonly arguments: readonly string[];
        /**
         * Child environment variables, keyed by variable name (`[A-Za-z_][A-Za-z0-9_]*`, at most
         * 128 characters) and resolved from credential references. The Node host passes the child
         * only these plus `PATH` and the temporary-directory variables (and the Windows system
         * variables on Windows), keeping its other environment variables out.
         */
        readonly environment: Readonly<Record<string, McpCredentialReference>>;
      };
    })
  /** A server the host reaches over MCP streamable HTTP. */
  | (McpCommonServerDefinition & {
      readonly kind: "remote";
      readonly transport: {
        readonly kind: "streamable-http";
        /**
         * `http://` or `https://` URL of at most 2,048 characters, with no credentials, fragment,
         * or credential-like query parameters such as `token` or `api_key`.
         */
        readonly endpoint: string;
        /**
         * Request headers resolved from credential references. Names must be unique ignoring case
         * and cannot be headers the transport owns, such as `accept`, `content-type`, `host`,
         * `mcp-protocol-version`, or `mcp-session-id`.
         */
        readonly headers: Readonly<Record<string, McpCredentialReference>>;
        /**
         * OAuth credential that authorizes requests; its `kind` must be `oauth`. When set, the host
         * owns the `Authorization` header, and a static `authorization` entry in `headers` is
         * rejected.
         */
        readonly oauth?: McpCredentialReference | undefined;
      };
    });
```

### `session`

Exports: `ModelUsage`, `PermissionRequest`, `PermissionReply`, `ToolApprovalSource`, `TurnFault`, `ExecutionLimitOrigin`, `ExecutionLimitEvidence`, `TurnOutcome`, `TurnEvent`.

```ts
/**
 * Token usage for one model request attempt, as the provider reported it. Values are not
 * cumulative across the Turn.
 */
export interface ModelUsage {
  /** Input tokens, including cached and cache-written input. */
  readonly promptTokens: number;
  /** Output tokens, including reasoning tokens. */
  readonly completionTokens: number;
  /** `promptTokens` plus `completionTokens`. */
  readonly totalTokens: number;
  /** Input tokens read from the provider's prompt cache. Absent when zero. */
  readonly cachedPromptTokens?: number | undefined;
  /** Output tokens spent on reasoning. Absent when zero. */
  readonly reasoningCompletionTokens?: number | undefined;
}

/**
 * One permission check Core resolves before a Tool call runs. Core creates one request for each
 * distinct projection returned by the Tool's `permissions` projector.
 */
export interface PermissionRequest {
  /** Unique request ID. Replies and `approval.resolved` events reference it. */
  readonly id: string;
  /** The Session the call belongs to. */
  readonly sessionId: string;
  /** The Turn the call belongs to. */
  readonly turnId: string;
  /** ID of the Tool being called. */
  readonly toolId: string;
  /** ID of the Tool call. */
  readonly toolCallId: string;
  /** The projected action. A Tool without a `permissions` projector uses its Tool ID. */
  readonly action: string;
  /** The projected resources this check covers. Never empty. */
  readonly resources: readonly [string, ...string[]];
  /** The patterns an `always` reply saves for the Session. Absent when the projection has none. */
  readonly save?: readonly string[] | undefined;
  /** Safe JSON metadata from the projection. */
  readonly metadata?: JsonObject | undefined;
}

/**
 * A user's answer to a permission request: `once` allows this request, `always` also saves the
 * request's `save` patterns as allowed for the Session, and `reject` denies it.
 */
export type PermissionReply = "once" | "always" | "reject";

/** What resolved a permission request. */
export type ToolApprovalSource =
  /** A configured permission rule. */
  | "config"
  /** A permission the Session saved from an earlier `always` reply. */
  | "saved"
  /** A `tool.execute.before` hook's `approval`. */
  | "hook"
  /** The embedding host's permission callback. */
  | "callback"
  /** A reply submitted to the Session, for example from a client. */
  | "remote"
  /**
   * Core's built-in decision: a denial when no one can be asked, or the allowance Core gives the
   * CodeMode executor's own call.
   */
  | "default"
  /** The approval wait limit expired. */
  | "timeout"
  /** The CodeMode script that made the call ended while the request was pending. */
  | "terminal";

/** Why a Turn failed. Safe to show to users. */
export interface TurnFault {
  /**
   * Stable failure code of at most 128 characters, such as `runtime_failed`, `tool_denied`, or a
   * limit code like `turn_elapsed_limit_reached`. Plugins, Tools, and providers can add their own
   * codes, so treat unknown values as generic failures.
   */
  readonly code: string;
  /** Safe failure text of 1 to 4,096 characters. */
  readonly message: string;
  /** The provider's own error code, when the failure came from a model provider. */
  readonly providerCode?: string | undefined;
}

/**
 * Where a limit's value came from: `configured` in harness configuration, `provider` from the
 * model or provider, or `platform` as a built-in Core or host default.
 */
export type ExecutionLimitOrigin = "configured" | "provider" | "platform";

/** The limit a Turn reached, with the value that applied. */
export type ExecutionLimitEvidence =
  /**
   * A time limit: `turn_elapsed` for the whole Turn, `approval_wait` for one permission request,
   * `provider_elapsed` for one model request including retries, and `provider_attempt_timeout` for
   * one provider attempt.
   */
  | {
      readonly name:
        | "turn_elapsed"
        | "approval_wait"
        | "provider_elapsed"
        | "provider_attempt_timeout";
      /** Where the limit came from. */
      readonly origin: ExecutionLimitOrigin;
      /** The limit in milliseconds. */
      readonly value: number;
      readonly unit: "milliseconds";
    }
  /** The number of steering message batches one Turn accepts. */
  | {
      readonly name: "steering_batches";
      /** Where the limit came from. */
      readonly origin: ExecutionLimitOrigin;
      /** The maximum number of batches. */
      readonly value: number;
      readonly unit: "batches";
    }
  /** The number of provider attempts for one model request. */
  | {
      readonly name: "provider_attempts";
      /** Where the limit came from. */
      readonly origin: ExecutionLimitOrigin;
      /** The maximum number of attempts. */
      readonly value: number;
      readonly unit: "attempts";
    }
  /** The request did not fit the model's context window. */
  | {
      readonly name: "model_context";
      /** Where the limit came from. */
      readonly origin: ExecutionLimitOrigin;
      /** The model's context window size. */
      readonly value: number;
      readonly unit: "model_units";
      /** Estimated size of the request input. */
      readonly estimatedInputUnits: number;
      /** Units reserved for the model's output. */
      readonly outputReserveUnits: number;
      /** Additional units kept free as a safety margin. */
      readonly safetyReserveUnits: number;
      /** `value` minus both reserves, never below zero: the input budget the estimate exceeded. */
      readonly availableInputUnits: number;
      /** Name of the estimator that produced `estimatedInputUnits`. */
      readonly estimator: string;
    };

/** How a Turn ended. */
export type TurnOutcome =
  /**
   * The Turn finished. `text` is the model's visible text across the Turn's model requests: text
   * and refusal output, without reasoning.
   */
  | {
      readonly kind: "completed";
      readonly turnId: string;
      readonly text: string;
    }
  /** The Turn was cancelled. `reason` is the cancellation reason, when one was given. */
  | {
      readonly kind: "cancelled";
      readonly turnId: string;
      readonly reason?: string | undefined;
    }
  /** The Turn failed. */
  | {
      readonly kind: "failed";
      readonly turnId: string;
      readonly fault: TurnFault;
    }
  /** The Turn stopped at a limit. `diagnostic` names the limit code and describes it. */
  | {
      readonly kind: "limit_reached";
      readonly turnId: string;
      readonly limit: ExecutionLimitEvidence;
      readonly diagnostic: TurnFault;
    };

/**
 * An observational Turn event, delivered to `context.events.subscribe` handlers. Every event
 * carries the `turnId` of the Turn that emitted it. Tool events also carry the Tool's `toolId` and
 * the call's `toolCallId`.
 */
export type TurnEvent =
  /** The Turn started. Published once, before the `turn.before` hooks. */
  | {
      readonly type: "turn.started";
      readonly turnId: string;
      /** The provider ID and catalog model ID that serve the Turn. */
      readonly model: {
        readonly providerId: string;
        readonly modelId: string;
      };
    }
  /** Streamed model output: visible text, a refusal, or reasoning. */
  | {
      readonly type:
        "model.text.delta" | "model.refusal.delta" | "model.reasoning.delta";
      readonly turnId: string;
      /** The output part this text belongs to. */
      readonly partId: string;
      /** Position of `delta` in the part's full text, in UTF-16 code units. */
      readonly offset: number;
      /** The new text. Never empty. */
      readonly delta: string;
    }
  /** Usage for one model request attempt, including context compaction requests. */
  | {
      readonly type: "model.usage";
      readonly turnId: string;
      readonly usage: ModelUsage;
    }
  /** Core recorded a Tool call, before resolving its permissions. */
  | {
      readonly type: "tool.requested";
      readonly turnId: string;
      readonly toolId: string;
      readonly toolCallId: string;
    }
  /** A permission check needs an answer, and the Turn waits for it. */
  | {
      readonly type: "approval.requested";
      readonly turnId: string;
      readonly toolId: string;
      readonly toolCallId: string;
      /** The request awaiting an answer. */
      readonly request: PermissionRequest;
    }
  /** A permission check was decided, whether automatically or by a reply. */
  | {
      readonly type: "approval.resolved";
      readonly turnId: string;
      readonly toolId: string;
      readonly toolCallId: string;
      /** The `id` of the resolved {@link PermissionRequest}. */
      readonly requestId: string;
      /** Whether the check allowed the call. */
      readonly decision: ToolApprovalDecision;
      /** What decided it. */
      readonly source: ToolApprovalSource;
      /** The user's reply, when one decided it. */
      readonly reply?: PermissionReply | undefined;
      /** The user's message with a `reject` reply, when given. */
      readonly replyMessage?: string | undefined;
    }
  /** A Tool call settled, successfully or not. */
  | {
      readonly type: "tool.completed";
      readonly turnId: string;
      readonly toolId: string;
      readonly toolCallId: string;
      /** Whether the call succeeded. */
      readonly ok: boolean;
    }
  /** A running Tool reported activity through `reportActivity`. */
  | {
      readonly type: "tool.progress";
      readonly turnId: string;
      readonly toolId: string;
      readonly toolCallId: string;
      /** Report sequence number for this call, starting at 1. */
      readonly checkpoint: number;
      /** Output bytes the Tool has observed so far, as it reported. */
      readonly observedBytes: number;
      /** Whether the Tool reported its output as truncated. */
      readonly truncated: boolean;
      /** State of the output artifact, when the Tool reported one. */
      readonly artifactState?: ToolOutputArtifactState | undefined;
    }
  /** A running Tool asked the user questions and waits for answers. */
  | {
      readonly type: "question.asked";
      readonly turnId: string;
      readonly toolId: string;
      readonly toolCallId: string;
      /** The pending questions. */
      readonly request: QuestionRequest;
    }
  /** The user answered a Tool's questions. */
  | {
      readonly type: "question.replied";
      readonly turnId: string;
      readonly toolId: string;
      readonly toolCallId: string;
      /** The `id` of the answered {@link QuestionRequest}. */
      readonly requestId: string;
      /** One answer per question, in question order. */
      readonly answers: readonly [QuestionAnswer, ...QuestionAnswer[]];
    }
  /** A Tool's questions were dismissed, or no one could answer them. */
  | {
      readonly type: "question.rejected";
      readonly turnId: string;
      readonly toolId: string;
      readonly toolCallId: string;
      /** The `id` of the dismissed {@link QuestionRequest}. */
      readonly requestId: string;
    }
  /** The Turn settled. This is the Turn's last event. */
  | {
      readonly type: "turn.terminal";
      readonly turnId: string;
      readonly outcome: TurnOutcome;
    };
```

### `plugin`

Exports: `PluginActivation`, `PluginContext`, `PluginCleanup`, `Plugin`, `definePlugin`.

```ts
/**
 * Where and for which plugin revision `setup` is running. Each activation runs `setup` again with
 * a fresh {@link PluginContext}, so a plugin can be active in several activations at once.
 */
export type PluginActivation =
  /**
   * The host's shared baseline. Its contributions apply to every Session the host serves.
   */
  | {
      readonly scope: "host";
      /** Identity of the host plugin revision. `version` is a non-negative integer. */
      readonly revision: {
        readonly id: string;
        readonly version: number;
      };
    }
  /**
   * One Session's plugin overlay. Its contributions apply only to that Session's Turns and add to
   * the host's.
   */
  | {
      readonly scope: "session";
      /** The Session this overlay belongs to. */
      readonly sessionId: string;
      /** Identity of the Session's plugin revision. `version` is a non-negative integer. */
      readonly revision: {
        readonly id: string;
        readonly version: number;
      };
    };

/**
 * The only argument to {@link Plugin.setup}. Register contributions while `setup` runs; Core
 * applies them once `setup` settles.
 *
 * Each `register` call checks the declaration's basic shape and throws a {@link PluginError} at
 * the call site when it is wrong. Core then validates every contribution in full. A rejected
 * contribution or an uncaught throw fails the activation; see {@link Plugin.setup} for the cases
 * where a host activation continues without the plugin instead.
 *
 * On the Cloudflare Worker host, `skills.registerSource`, `skills.registerTransform`,
 * `mcp.register`, `models.register`, `journal.registerKind`, and `events.subscribe` throw a
 * `PluginError` because those contributions are unsupported there.
 */
export interface PluginContext {
  /** Core-owned identity of this activation, never supplied through plugin options. */
  readonly activation: PluginActivation;
  /** This plugin's configured options as an owned JSON copy, or `{}` when none are set. */
  readonly options: JsonObject;
  /**
   * The platform's global `fetch`. Core applies no limits, policy, or redaction to it; use
   * `webFetch` for bounded page fetches.
   */
  readonly fetch: typeof fetch;
  /**
   * Bounded file access inside the Session workspace. Every method throws a `PluginError` when the
   * host provides no workspace files capability.
   */
  readonly workspaceFiles: WorkspaceFiles;
  /**
   * Runs shell commands in the Session workspace. `execute` throws a `PluginError` when the host
   * provides no shell capability.
   */
  readonly workspaceShell: WorkspaceShell;
  /**
   * Fetches a web page as bounded text. `fetchText` throws a `PluginError` when the host provides
   * no web fetch capability.
   */
  readonly webFetch: WebFetch;
  /** Tool registration. */
  readonly tools: {
    /**
     * Adds a Tool. Tool IDs must be unique across the Session's effective catalog, and `skill`
     * and `task` are reserved for Core.
     *
     * The `never` input type accepts any `ToolDeclaration<TInput>`: Core passes `execute` the
     * input after validating it against the declaration's `inputSchema`.
     */
    readonly register: (declaration: ToolDeclaration<never>) => void;
  };
  /** Agent registration. */
  readonly agents: {
    /** Adds an Agent profile. See {@link AgentDeclaration}. */
    readonly register: (declaration: AgentDeclaration) => void;
    /**
     * Names the Agent that Turns use when neither the request nor the Session selects one. A
     * revision accepts one default across all its plugins; a second call fails activation. The
     * Agent must exist in the effective catalog and must not be `subagent`-only. A Session
     * revision's default replaces the host's, and a default set in host configuration replaces a
     * host plugin's.
     */
    readonly setDefault: (agentId: string) => void;
  };
  /** Skill registration. */
  readonly skills: {
    /** Adds one Skill. See {@link SkillDeclaration}. */
    readonly register: (declaration: SkillDeclaration) => void;
    /** Adds a workspace directory or HTTP catalog that Core loads Skills from. */
    readonly registerSource: (source: SkillSource) => void;
    /** Adds a transform that Core applies to loaded Skills. See {@link SkillTransform}. */
    readonly registerTransform: (transform: SkillTransform) => void;
  };
  /** MCP server registration. */
  readonly mcp: {
    /** Adds an MCP server definition. Core owns the connection, transport, and credentials. */
    readonly register: (definition: McpServerDefinition) => void;
  };
  /** Model registration. */
  readonly models: {
    /** Adds a plugin model under its own provider ID. See {@link ModelDeclaration}. */
    readonly register: (declaration: ModelDeclaration) => void;
  };
  /** Journal registration. */
  readonly journal: {
    /**
     * Declares a journal kind that this plugin's tools and models may report with
     * `reportJournal`. Kinds are scoped to the plugin; registering the same kind twice fails
     * activation.
     */
    readonly registerKind: (declaration: JournalKindDeclaration) => void;
  };
  /** Hook registration. */
  readonly hooks: {
    /**
     * Adds an awaited handler for one hook. Core runs host hooks before Session hooks, and within a
     * scope in plugin order, then registration order. An unknown hook name fails activation after
     * `setup` returns. See `HookHandlers`.
     */
    readonly register: <TName extends HookName>(
      name: TName,
      handler: HookHandler<TName>,
    ) => void;
  };
  /** Observation. */
  readonly events: {
    /**
     * Adds an observer for Turn events. A host activation's subscribers receive the events of every
     * Turn the host runs, each as an owned copy.
     *
     * Delivery runs off the Turn: Core queues each event and does not wait for subscribers, so they
     * cannot affect control flow. For each event, Core calls subscribers one at a time, in plugin
     * order and then registration order, and awaits each. A throw or rejection is ignored. The host
     * queue holds `observerBufferSize` pending events (128 by default); when subscribers fall
     * further behind, the oldest pending events are dropped.
     */
    readonly subscribe: (
      handler: (event: TurnEvent) => void | Promise<void>,
    ) => void;
  };
}

/**
 * A cleanup returned from `setup`. Core calls it once when the activation's scope closes, such as
 * when its revision is retired or the host shuts down. Core awaits it; a throw or rejection is
 * logged and shutdown continues.
 */
export type PluginCleanup = () => void | Promise<void>;

/**
 * A trusted plugin. Node and Worker hosts load it from the module's default export. Core reads
 * only `id` and `setup`.
 */
export interface Plugin {
  /**
   * Stable plugin ID, also the key for this plugin's options in configuration. Core requires at
   * most 128 characters that start with a lowercase letter and use lowercase letters and digits
   * joined by single `.`, `_`, or `-` characters (`example.shout`). IDs must be unique within a
   * revision.
   */
  readonly id: string;
  /**
   * Registers this plugin's contributions. Core calls it once per activation, before any Turn uses
   * that activation, and awaits it. It returns nothing or a {@link PluginCleanup}; any other value
   * fails activation.
   *
   * A throw or rejection fails the activation. On the host, a {@link PluginError} with a
   * non-blank `publicMessage` instead skips the plugin: Core rolls back everything it registered,
   * runs any cleanup it returned, reports it as failed with the trimmed `publicMessage` (at most
   * 512 characters), and continues with the other plugins. A host plugin whose Tool or journal kind
   * Core rejects is skipped the same way. Session activations never skip a plugin; the whole
   * revision is rejected.
   */
  readonly setup: (
    context: PluginContext,
  ) => void | PluginCleanup | Promise<void | PluginCleanup>;
}

/**
 * Checks and freezes a plugin definition. Core accepts any object with the same shape, so this
 * helper only moves the basic checks to module load and drops every other property.
 *
 * @param plugin - The plugin's `id` and `setup`.
 * @returns A frozen {@link Plugin} with only `id` and `setup`.
 * @throws {PluginError} When `id` is not a non-empty primitive string or `setup` is not a
 *   function. Core checks the full ID format later, at activation.
 */
export declare const definePlugin: (plugin: {
  readonly id: string;
  readonly setup: Plugin["setup"];
}) => Plugin;
```

---

Generated from [`packages/plugin/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/plugin/README.md), [`packages/plugin/src/`](https://github.com/ZephyrCloudIO/ze-harness/tree/main/packages/plugin/src), and [`examples/`](https://github.com/ZephyrCloudIO/ze-harness/tree/main/examples).
