# Ze Harness > Ze is an agent harness: one Harness core runs Sessions, the model and tool loop, plugins, and a Linux workspace, from the `ze-harness` CLI, an embedded SDK, a Node.js server, or a Cloudflare Worker. # `@zephyrcloudio/harness-cli` The `ze-harness` command-line client and its embedded-or-remote composition edge. Requires Node.js 26.7 or later. Command help and shell completion scripts come from Effect 4's CLI command tree. Run `ze-harness --help` or `ze-harness session --help` to inspect commands and flags. Run `ze-harness completions --help` for installation examples. Generate a script without starting the daemon (redirect the script to a file): ```sh ze-harness completions bash > ~/.local/share/bash-completion/completions/ze-harness ze-harness completions zsh > ~/.zsh/completions/_ze-harness ze-harness completions fish > ~/.config/fish/completions/ze-harness.fish ``` For Zsh, add `~/.zsh/completions` to `fpath` before running `compinit`. Bash needs `bash-completion` installed; Fish loads its completion directory automatically. ```sh ze-harness ze-harness tui ze-harness --config ze-harness.config.json run "hello" ze-harness --remote http://127.0.0.1:4317 --server my-server chat ``` ## Interactive TUI Run `ze-harness` with no arguments in a terminal to open the interactive TUI. It launches with `--experimental-ffi`; the CLI re-executes itself automatically when the flag is missing. ```sh ze-harness ze-harness tui ze-harness tui ze-harness -s ``` Use `-s ` (or `--resume `) to resume a Session. The `--session` flag on `session fork` assigns the new fork's ID; it does not resume a Session. The opening screen has a single input. Type a prompt and press Enter to start a Session, or press Ctrl+X O to open an existing Session by ID. From a source checkout, `pnpm clean:host` stops the local daemon and moves Ze Harness's XDG config, data (including Sessions and saved provider logins), cache, and `~/.ze-harness` into a recoverable backup under `~/.Trash` on macOS (or `~/.local/share/ze-harness-backups` elsewhere). It does not change project config files or shell-provided API keys. If a provider key is exported in your shell, it can still make models available after the reset. On Windows, this source-checkout helper still uses the Unix-style default data path. After migration, set `XDG_DATA_HOME` to the absolute native data parent (normally `$env:LOCALAPPDATA`) before running it so it backs up the data rather than the legacy junction. On first launch with no usable model, the TUI opens Connect provider automatically. When a provider is already connected (for example through a shell-exported API key), it skips the prompt. It also opens when no model provider is available. Run `ze-harness tui --onboarding` to open provider setup again on demand (`ze-harness --onboarding` also works). Press Ctrl+X P (or Ctrl+Alt+P) to reopen it later without leaving the TUI. Choose OpenAI's Codex login to open the displayed URL and enter its device code, or choose an API key connection. For API keys, the TUI briefly suspends while its terminal prompts securely for the key. After connection, it refreshes models. Ctrl+Alt+O hides optional first-run tips on future launches, independently of the one-time provider prompt; the connection action remains visible until a provider is available. A missing provider still needs to be connected before sending a prompt. The preference is stored alongside model favorites under `model-preferences.json` in the [durable state directory](#durable-state-paths). Local skill discovery includes `~/.codex/skills`, `~/.claude/skills`, `~/.agents/skills`, `$XDG_CONFIG_HOME/opencode/{skill,skills}`, and `~/.ze-harness/skills`, plus matching project-directory sources. Additional directories can be set in the host's `skills` configuration. Keyboard controls. **Ctrl+X** is the leader key: press it, then the second key. - **Enter** — send a prompt, follow up on the running Turn, or accept the highlighted slash command - **Ctrl+V** (also **Ctrl+X V**) — paste an image or text from the local clipboard - **Ctrl+X R** — remove the last attached image; click an attachment to remove it - **Shift+Enter** (also **Ctrl+J** or **Alt+Enter**) — newline in composer - **Tab** — complete the selected slash command when command suggestions are open, otherwise cycle the primary agent (**Shift+Tab** cycles back) - **↑/↓** (also **Ctrl+P / Ctrl+N**) — move in the open suggestion menu; **Esc** hides it - **Esc** — close overlay, cancel a pending paste, hide the suggestion menu, cancel the active Turn, or return to the parent Session from a sub-agent, in that order - **Ctrl+X M / A / K** — open the model, agent, or skill picker - **Ctrl+X E** — cycle the selected model's reasoning effort - **Ctrl+X P** (or **Ctrl+Alt+P**) — connect a provider - **Ctrl+X S** — list Sessions of this project, newest activity first - **Ctrl+X N** — start a new Session - **Ctrl+X C** — compact the Session context - **Ctrl+X F** — open the fork picker - **Ctrl+X G** — list sub-agents; **Ctrl+X ↓** opens the first one directly - **Ctrl+X T** — toggle details for the latest tool call - **Ctrl+X O** — open a Session by ID (**Ctrl+O** still works on the opening screen) - **Ctrl+C** — quit; the Session keeps running in the background - **Ctrl+X Q** or **Ctrl+D** — quit as well; `/exit` (also `/quit`, `/q`) does the same from the composer - **↑/↓ · Enter · Tab · Ctrl+R · Esc** — move, select, favorite, refresh, and close in the model picker - **↑/↓ · Enter · Ctrl+R · Esc** — move, select, refresh, and close in the agent picker - **↑/↓ · Tab · Space/Enter · Ctrl+R · Esc** — move, details, toggle, refresh, and close in the skill picker - **↑/↓ · Enter · Esc** — move, select, and close in the fork and sub-agent pickers - **↑/↓ · Enter · Ctrl+R · Esc** — move, select, refresh, and close in the session picker - **←/→/Tab · ↑/↓ · Enter · Esc** — choose, scroll, select, and reject (or go back) in permission requests - **Ctrl+B** — detach foreground sub-agents to the background (otherwise moves left) - **Alt+↑** — return to the parent Session from a sub-agent - **Alt+← / Alt+→** — move between sub-agent siblings - **Ctrl+Alt+K** — pin or unpin the shortcut panel; it also appears while a leader sequence is pending Paste screenshots with **Ctrl+V** on the opening screen or in a conversation. You can also paste or drop a local PNG, JPEG, GIF, or WebP file path (one file per paste). Images appear above the input; press **Enter** to send them with your text, or on their own. Images stay attached if submission fails and clear when you switch Sessions. Press **Esc** to cancel a pending paste and continue editing. Existing `/image`, `/base64`, and `/send` draft commands also work with pasted attachments. Clipboard reading uses macOS system tools, PowerShell on Windows/WSL, or `wl-paste` (from `wl-clipboard`) / `xclip` on Linux. It runs on the machine running the TUI, including when connected to a remote Ze server. If your terminal intercepts Ctrl+V, use **Ctrl+X V** for image clipboard reads. Ordinary terminal paste still handles text and file paths. In an SSH terminal without access to a graphical clipboard, paste a path available to the TUI. Selecting text with the mouse or clicking the short Session ID copies to the local clipboard through the same system tools (`pbcopy`, PowerShell, `wl-copy`/`xclip`/`xsel`), plus an OSC 52 escape for terminals that honor it, including over SSH. The TUI shows `you` for prompts carrying this machine's stable author identity and a short author ID for other writers. Entries written before authorship fall back to the connection match: `you` for the current client, a short Client ID otherwise. Permission requests appear inline. Reply with **Allow once**, **Allow always**, or **Reject**; **Esc** rejects the open request. The first durable reply wins. **Allow always** confirms the exact Session patterns proposed by the Tool. The status row shows when approval or a response is required. Brief stream disconnects resume from the last update without a notice. A longer outage shows `Reconnecting` within about two seconds and keeps its error above the composer until the stream catches up; meanwhile active Turns refresh once a second so pending requests still appear, and the working spinner pauses. A stream that cannot resume is restarted with backoff and reloads Session history. Running tool activity appears as one collapsed line. Click a line to expand its latest bounded preview and full-output workspace artifact path when one exists. Completed tool output appears under its tool call. Output longer than three lines is collapsed with an inline expansion control. Execute groups show a summary of the tools the agent ran. Click the group to switch between that summary and its code and output; click **N more** to reveal hidden calls. Read previews show file contents with line numbers, shell previews show command output, and edits show diffs. Paths are relative to the Session's working directory, including when connected to a remote server. For a single call, **details** opens its result without repeating the summary header. **Ctrl+X T** switches Execute source/output. **Ctrl+X J** selects the next child (**Ctrl+X Shift+J** the previous), **Ctrl+X D** toggles that child's details, **Ctrl+X L** reveals ten more calls, and **Ctrl+X Shift+L** folds the selected revealed range, or the latest revealed range. Trace shortcuts close source/output first. Clipped shell commands and full failure diagnostics remain inspectable; native tools never show raw argument blocks. Pause stays a non-expandable duration/status row with its full failure diagnostic. Rejecting an approval inside Execute skips that operation; the script may continue. Cancel the Turn to stop the whole execution. Reasoning stays in an expandable block. Its header shows the observed thinking duration once the block finishes alongside a first-line preview. Completed Turns show the model and elapsed execution time below the response, excluding queue wait. New TUI Sessions receive a small built-in assistant system prompt; the opening screen does not ask users to configure it. They also start in the directory the client runs from: the CLI sends its working directory and the server stores it (relative inside the server root, absolute otherwise). Pass `--workdir PATH` for an explicit directory, or `--no-workdir` to use the server root. When the client directory cannot be used at all, the Session uses the server root and the TUI says so. Type `/` on the opening screen or in the conversation composer to load available commands. The opening screen lists only the commands that work before the first prompt (`/new`, `/queue`, `/sessions`, `/models`, `/effort`, `/skills`, `/exit`); full command text for anything else still reports why it needs a Session. `/new [TEXT]` starts a new Session and switches to it, leaving the previous Session running on the server; with `TEXT` that prompt is submitted as the first Turn. **Ctrl+X N** does the same without a prompt. `/exit` (aliases `/quit`, `/q`) quits the application from either screen. `/sessions [QUERY]` lists this project's Sessions newest-first and switches on select; the previous Session keeps running on the server. `/queue TEXT` always creates a later Turn. `/steer TEXT` always follows up on the running Turn and reports an error rather than silently queueing when that Turn is no longer steerable. `/models [QUERY]` opens the model picker; picking a model that advertises reasoning effort levels then prompts for the level. `/effort [LEVEL|default]` sets the effort for the next Turn directly, bare `/effort` opens the effort picker, and **Ctrl+X E** cycles through the advertised levels. The header shows the model the next Turn will use, including the selected effort, plus live context-window use like `87.9K (8%)` once a Turn reports model usage. `/agents [QUERY]` opens the agent picker with the primary agents; the header shows the agent the next Turn will use. `/skills [QUERY]` opens a multi-select picker. Selected Skills load into the next queued Turn and then clear. `/fork [SEQUENCE|TURN-ID]` forks the Session before a Turn and opens the fork; bare `/fork` opens the fork picker. Favorites, recent models, and per-model effort levels persist across runs in `model-preferences.json` under the [durable state directory](#durable-state-paths). Task tool rows show a live one-line summary of their sub-agent: the latest tool while it runs, tool-call count and duration once it settles, and a `(background)` marker when the chain detached. Click a task row, type `/subagents`, or press **Ctrl+X G** to open a sub-agent and watch its Turns, thinking, and tool calls live; the composer then prompts that sub-agent, which keeps its pinned agent. `/back`, **Esc**, or **Alt+↑** returns to the parent, and **Alt+← / Alt+→** moves between siblings. Only direct sub-agents open; **Ctrl+B** detaches blocking foreground chains to the background. ## Durable state paths The database, provider `auth.json`, model preferences, author identity, and daemon state share one directory. An absolute `XDG_DATA_HOME` selects `$XDG_DATA_HOME/ze-harness` on every platform; relative values are ignored. The default is `~/.local/share/ze-harness` on Unix and `%LOCALAPPDATA%\ze-harness` on Windows, with `%USERPROFILE%\AppData\Local\ze-harness` as the fallback when `LOCALAPPDATA` is missing or relative. `XDG_CONFIG_HOME` and `XDG_CACHE_HOME` are unchanged. Existing Windows `~/.local/share/ze-harness` data stays in use while its registered daemon is live. Stop the daemon, then invoke the CLI again to migrate to the native location. Migration renames the directory and leaves a junction at its old location so existing launchers and absolute paths still work. Locked or cross-volume directories remain on the legacy root with a warning; independent directories at both locations produce an error. See [migration guidance](/docs/server-node.md) before resolving a conflict or moving data manually. Absolute `--database` and `ZE_HARNESS_DB` overrides still select their own database; relative `ZE_HARNESS_DB` values resolve under the shared state directory. ## Local and remote commands Local commands connect to one shared daemon on this machine: the first command starts it in the background and later commands reuse it, so Sessions and running Turns survive closing the client. `service status`, `service start`, `service stop`, and `service restart` manage the daemon explicitly; restarting picks up configuration, plugin, MCP server, and Skill changes. First start wins: a `--database` or `--config` flag that differs from what the daemon started with is refused, so restart the daemon or pass `--embedded` for that command. Stopping or restarting ends running Turns: the CLI names them first so nothing is lost silently. `serve` starts a foreground server instead. With `--embedded`, commands run in-process without the daemon (scripts and tests). With `--remote`, commands use only the network Client against the named `--server` server. The default local database is one shared `ze-harness.db` file under the [durable state directory](#durable-state-paths); the daemon and `--embedded` commands may open it at once (SQLite serializes writers, and a busy database surfaces a retryable error rather than corrupting state). Provider API keys can be managed through the same local or remote Client path: ```sh ze-harness provider list ze-harness provider connect openai --label personal ze-harness provider login openai ze-harness provider disconnect openai ``` `provider connect` requires an interactive terminal and reads the key without echoing it, so the secret does not appear in shell history or process arguments. The local command stores the key once in the global `auth.json` in the [durable state directory](#durable-state-paths), shared by every working directory and database; `--label` is accepted but labels persist on remote servers only. With `--remote URL --server NAME`, the remote server stores it instead. Provider list and command results are JSON and never include credential values. Keys connected before the global `auth.json` stay in their per-project databases; reconnect once with `provider connect`. `provider login PROVIDER` signs in to a provider that supports login; `provider list` shows each provider's `logins`. Open the printed URL, enter the displayed code, and approve the login. The host runs the exchange, so the command works over SSH and waits until you approve, the provider rejects the request, the code expires, or you press Ctrl-C (which cancels the login). The URL and code appear on stderr; stdout contains only the safe connection result. Use `--method ID` when a provider offers several login methods. - `openai` signs in with a ChatGPT account (enable device code authorization in your ChatGPT security settings if OpenAI rejects it). Requests from that login go to the ChatGPT Codex Responses deployment; configured base URLs and routes apply only to API-key connections. Choose a model your ChatGPT plan supports with `run --model openai/MODEL`. - `tap` signs in to TAP with your TAP account; a first sign-in creates it. Before the code, login shows the notice the TAP app shows: by continuing you acknowledge Zephyr Cloud's Terms of Service and Privacy Policy, and agree to receive occasional product emails and notifications. `tap/*` models are TAP credits and free models; your own provider keys stay under that provider's id, such as `opencode-go/*`. To use a key you stored in TAP (or the TAP Gateway dashboard) instead of a local one, set the provider's `via` to `tap`, for example `"providers": { "tap": true, "openrouter": { "via": "tap" } }`. `openrouter/*` requests then go through the TAP Gateway (at the origin of the `tap` base URL) with your TAP sign-in, and `provider list` shows `"source": "via", "via": "tap"`, available once you are signed in to TAP. Errors name TAP, for example "No openrouter key is stored in TAP". - Providers configured with `login.device.discoveryURL` use the standard OAuth 2.0 device flow. An optional `login.device.notice` (`text` and `links`, each link a `label` and `url`) is shown before the code. A login replaces the provider's stored connection. The Node host saves access and refresh tokens in the global `auth.json`, refreshes them as requests need them (including during a long Turn), and retries once with a fresh token if the provider rejects an expired one. If the provider rejects the refresh token, run `provider login` again. An explicitly configured `providers..options.apiKey` takes precedence over a stored login. Use `provider disconnect` to remove a login, or `provider connect` to switch to an API key. Local daemon, embedded, and remote Node hosts support login; Cloudflare Worker hosts report login as unsupported. `run` accepts `--model PROVIDER/MODEL`, `--system-prompt TEXT`, and `--json`. Images attach with `--image URL`, `--image-base64 MIME:BASE64`, or `--image-file PATH` (a local JPEG, PNG, GIF, or WebP file read from the machine running the CLI, also in `--remote` mode); the server may enforce a configured per-image size cap. The prompt text precedes images in the submitted content; images of the same option type retain their order. `session create` accepts `--model PROVIDER/MODEL`, `--system-prompt TEXT`, and `--workdir PATH` (a server-rooted working directory; the server root is the default). `session list` prints the current project's Sessions oldest-first as JSON, and `server delete` cancels and deletes them all (workspace files stay); both accept `--all` for every project. Projects resolve from git remote hashes like opencode, with directories outside any repository sharing the `global` project. `session fork ` copies settled history into a new Session; `--turn ID|SEQUENCE` forks before one Turn and `--session ID` selects the new identity. Forks inherit the parent workdir, model, system prompt, and permission rules, and record lineage on the new Session. `session children ` prints one Session's spawned children oldest-first as JSON, and `session approvals ` prints the pending permission requests owned by its descendant sub-agent Sessions oldest-first as JSON. `session background ` detaches its blocking foreground sub-agent chains (their results arrive as queued turns). Task tool calls render with their agent, state, and summary. Append external context for the next admitted work without creating a Turn or calling a model: ```sh ze-harness session conversation append "Remember this for the next request." cat context.txt | ze-harness session conversation append - ``` The command prints the durable receipt as JSON. Use the same `--database` and working directory as the local Session, or add `--remote URL --server NAME` for a network Server. Work already admitted before the append does not see the entry. Session system prompts are durable: they are stored with the Session, returned to Session readers, and sent with every model request in that Session. They must not contain credentials. Remote permission and Session policy commands are: ```sh ze-harness --remote URL --server NAME permission reply [--message TEXT] ze-harness --remote URL --server NAME permission saved list ze-harness --remote URL --server NAME permission saved remove ze-harness --remote URL --server NAME session config show ze-harness --remote URL --server NAME session config replace ``` `--message` attaches feedback to a `reject` reply and is stored with the denial. A `reject` also denies the Session's other pending requests. A reply addressed to a parent Session for a descendant sub-agent request is forwarded to the owning Session, and `run`, `chat`, and the TUI surface those descendant approvals inline so sub-agents never wait unseen. They learn about sub-agent approvals and questions from the server activity stream (`watchServer`): each subscribes live, then bootstraps once from `session children` and the pending lists when the stream announces its cursor; nothing polls while idle. The TUI and CLI choose only the reply. They cannot choose or widen the patterns saved by an `always` reply. In `chat`, enter `/steer TEXT` to redirect the currently running Turn. Any other nonempty line queues a new Turn. See the [CLI quickstarts](https://github.com/ZephyrCloudIO/ze-harness/blob/main/README.md#local-run-and-chat) and [database rules](https://github.com/ZephyrCloudIO/ze-harness/blob/main/README.md#database). ## Install Install the native executable without Node.js or GitHub authentication. `stable` tracks the latest completed production release; `dev` tracks completed builds from `main`. Public archives, signed metadata and installers are served from `downloads.ze.dev`. ```sh curl -fsSL https://downloads.ze.dev/install.sh | sh curl -fsSL https://downloads.ze.dev/install.sh | sh -s -- --channel dev curl -fsSL https://downloads.ze.dev/install.sh | sh -s -- --version 0.3.0 ``` On Windows PowerShell: ```powershell irm https://downloads.ze.dev/install.ps1 | iex & ([scriptblock]::Create((irm https://downloads.ze.dev/install.ps1))) -Channel dev ``` Homebrew installs the stable SEA binary from the [ZephyrCloudIO tap](https://github.com/ZephyrCloudIO/homebrew-ze) on supported macOS and glibc Linux machines: ```sh brew install zephyrcloudio/ze/ze-harness brew upgrade ze-harness ``` After [the WinGet community submission](https://github.com/microsoft/winget-pkgs) is accepted and indexed, Windows x64 users can install and upgrade the portable package with: ```powershell winget install --id ZephyrCloudIO.ZeHarness -e ``` Before upgrading a WinGet installation, stop the local daemon and let active Turns finish: ```powershell ze-harness service stop winget upgrade --id ZephyrCloudIO.ZeHarness -e ``` Stopping the daemon ends any Turns still running. Windows locks executables that are in use, and the local daemon runs from the installed executable, so WinGet cannot replace it while it runs. Homebrew and WinGet installs are managed by those package managers, including future upgrades. They do not use the native installer's launcher or its versioned install directory. The native installer verifies signed release metadata and the archive checksum; Homebrew and WinGet verify the archive SHA-256 in their package definitions. Uninstall with `brew uninstall ze-harness` or `winget uninstall --id ZephyrCloudIO.ZeHarness -e`, respectively. These uninstall commands do not remove CLI configuration or Session data. Supported builds: macOS 13.5+ on Apple Silicon; Linux x64/ARM64 with kernel 4.18+, glibc 2.28+, and `libatomic`; and Windows 10+/Server 2016+ on x64. Native installs require internet access. The Unix installer needs a POSIX shell, `curl`, `openssl`, `mktemp`, and `tar` (Linux) or `unzip` (macOS); Windows needs PowerShell 5.1+. Windows ARM64, Intel macOS, and Alpine/musl are not currently release targets. A version pin selects an immutable archive; rerun the installer to change versions. In PowerShell use `-Version`, `-Dir`, and `-NoModifyPath`; on Unix use `--version`, `--dir`, and `--no-modify-path`. The launcher is installed in `~/.ze-harness/bin` (or a custom `--dir`/`-Dir`). Executables and the install marker are stored under `${XDG_DATA_HOME:-~/.local/share}/ze-harness` on Unix; Windows uses `$env:XDG_DATA_HOME\ze-harness` when `XDG_DATA_HOME` is an absolute path, otherwise `$HOME\.local\share\ze-harness`. On invocation, the CLI attempts the Windows [state migration](#durable-state-paths), preserving the installer's path with a junction; an in-use executable can defer the rename. The installer does not alter the CLI's existing `XDG_CONFIG_HOME` configuration or `XDG_CACHE_HOME` cache. Unix installs add the launcher directory to the shell profile unless disabled; Windows installs add it to the user PATH. Each installed version keeps `LICENSE` and `THIRD_PARTY_LICENSES.txt` beside the executable. Open a new terminal after installing. If an old unmanaged executable occupies the launcher path, move it aside before installing. New installs record the install channel or an explicit version pin alongside the launcher path. `ze-harness update --check` checks the signed channel pointer. `ze-harness update` verifies the signed manifest and archive checksum before installing. With a registered local daemon it stages the release and, when no Turns are running, restarts the daemon onto it; with running Turns it leaves the daemon unchanged and says so. Once active Turns finish, `ze-harness service restart` activates a prepared update and starts the daemon from the new executable. That explicit restart can interrupt active Turns, so wait for them to finish before using it. A staged release that the installed CLI already matches or passes (for example after reinstalling with the installer) is discarded instead of blocking a restart. ### Upgrading the local daemon A daemon keeps running the executable that started it, so after an upgrade it must restart onto the new version. That handoff happens without a separate command: - The installers finish by running `ze-harness service restart --if-idle`. Pass `--no-restart-daemon` (`-NoRestartDaemon` on Windows) to skip it. - `ze-harness update` hands off after staging, as above. - When the installed native CLI is newer than the running daemon, the first local command hands off and prints `Local daemon updated from to .` Every handoff restarts only an idle daemon: running Turns, Sessions that cannot be inspected, or a daemon whose protocol differs leave it running and unchanged. A handoff never moves the daemon to an older release. `ZE_HARNESS_DISABLE_AUTOUPDATE=1` turns off the automatic handoff on ordinary commands. If the new version cannot start, the CLI restarts the previous executable so work can continue, reports why, and does not retry the automatic handoff for that version until an explicit `service restart` succeeds. While a daemon starts, the CLI shows its progress: a bar on a terminal, or one line per backup or migration step otherwise. Before migrating a database that already holds data, the daemon copies it to `ze-harness.db.pre-.bak`; the bar follows that copy byte by byte, then each migration. A large database can take a while, and the CLI waits for as long as the daemon is alive and reporting. A failed migration rolls back completely, and the CLI shows the daemon's own error. Legacy native installs lack reliable pinning metadata and must explicitly opt in with `ze-harness update --auto on` or reinstall. Channel-following native installations check for a new signed release on local daemon startup and every six hours while it runs. A newer version is verified and staged in a separate process, without interrupting the daemon or activating an incompatible old TUI. Run `ze-harness update` when current work finishes to switch both the CLI launcher and the daemon to a staged release. Use `ze-harness update --auto off` to disable background checks, or `--auto on` to enable them. Existing native installs do not get background updates unless explicitly enabled with `--auto on`; explicit version pins stay pinned. Homebrew and WinGet updates use their own package managers. The CLI observes `Ze-Harness-Version: /` on normal daemon and remote-server responses, so connecting does not add a handshake request. A running TUI shows a notice when the version differs or changes, including on a stream reconnection. Protocol differences warn that operations may fail; response observation does not cancel current work or retry prompts. Non-TUI commands send these notices to stderr, preserving stdout. When a TUI sees a newer server release and native auto-update is enabled, it independently checks its configured **signed release channel** once for that CLI session. The server header never selects a download or changes the channel/pin. A separate CLI process verifies and stages an available channel update; only successful staging produces the downloaded/restart notice. It never restarts a remote server or silently switches this CLI. When local work finishes, exit the TUI and run `update` to activate the release and restart the daemon onto it. Repeated update commands reuse a verified staged copy of the same channel release. Native version slots are retained until uninstall. Automatic pruning is deferred until the host can track all live CLI/TUI processes using older executables, not only the daemon. `ze-harness uninstall` removes the native launcher and versioned executables while leaving Sessions and credentials intact. Stop the local daemon first. Use `ze-harness uninstall --remove-user-data --yes` to also remove the local XDG data, config, and cache directories and user-authored global skills in `~/.ze-harness/skills`. Symlinked application data directories are refused rather than deleting their targets. This does not delete custom databases outside those directories or project files. On Windows cleanup runs after the CLI exits. Remove any PATH entry added by the installer from your shell profile or Windows user PATH separately. For Homebrew and WinGet installations, the CLI provides the package-manager uninstall command instead of modifying managed files. ## Binary releases SEA builds generate `THIRD_PARTY_LICENSES.txt` with `generate-license-file` and append the Node.js notice fetched from the build runtime's exact release tag. Building the native archives requires network access to `raw.githubusercontent.com` for that notice. Each release is stored at `https://downloads.ze.dev/releases/v/` with `manifest.json`, `manifest.json.sig`, `checksums.txt`, and the platform archives. The channels at `/channels/stable.json` and `/channels/dev.json` each contain a signed pointer bound to its channel. Installers contain the release certificate, verify the pointer and detached manifest signatures with RSA/SHA-256, then verify the archive SHA-256 from the signed manifest **before** activating it. The public certificate for independent verification is at `/keys/.crt` (DER). Release assets are also retained on their GitHub Releases for future public visibility. Neither this release signature nor an archive checksum replaces OS-native code signing. For release maintainers: set GitHub Actions variable `ZE_RELEASE_CERT_BASE64` to the base64 DER X.509 RSA certificate, secret `ZE_RELEASE_PRIVATE_KEY` to its matching PEM private key, variables `ZE_R2_ACCOUNT_ID` and `ZE_R2_BUCKET`, and secrets `ZE_R2_ACCESS_KEY_ID` and `ZE_R2_SECRET_ACCESS_KEY` to write-scoped R2 credentials. For an initial keypair, generate it **outside the repository**: ```sh openssl req -x509 -newkey rsa:3072 -nodes -days 3650 \ -subj '/CN=Ze Harness Release' -keyout release-key.pem -out release.crt.pem openssl x509 -in release.crt.pem -outform DER -out release.crt openssl base64 -A -in release.crt # ZE_RELEASE_CERT_BASE64 openssl x509 -in release.crt.pem -noout -fingerprint -sha256 ``` Store the entire `release-key.pem` contents as the private-key secret. Only the certificate is public. The release workflow checks that the certificate matches the signing key before publication. Keep the signing key outside the repository, record and publish its SHA-256 certificate fingerprint through an independently controlled channel. Public certificates have immutable fingerprinted paths; changing the release signing key requires a separate installer migration supporting both keys before switching the signed release channel. R2 must expose the bucket publicly as `downloads.ze.dev`; release jobs verify reads through that domain before advancing channel pointers. Configure signing and R2 settings before merging: the dev release job on every `main` push intentionally fails closed without them. Versioned R2 objects, including dev versions, are retained so pinned installs remain available even after old GitHub dev prereleases are pruned; plan R2 storage accordingly. The publisher uploads up to three immutable objects concurrently and verifies each through the public domain, retrying transient read errors. Each public read defaults to a 120-second timeout; set `ZE_R2_READBACK_TIMEOUT_MS` on the release job if the network needs longer. If publication stops partway, use **Re-run failed jobs** in GitHub Actions to reuse the same built archives. **Re-run all jobs** rebuilds archives with new timestamps and may produce different hashes, which the immutable-object preflight rejects. In that case, publish a new version rather than overwrite already published versioned objects. Stable release publication also runs `.github/workflows/package-managers.yml` after R2 is verified. It reads the immutable signed manifest, verifies each archive's SHA-256 and legal notices, tests the generated Homebrew formula on macOS, and uploads the formula and WinGet manifests as a workflow artifact. The Homebrew job commits the formula directly to [`homebrew-ze`](https://github.com/ZephyrCloudIO/homebrew-ze); independently, the WinGet job commits one version into [`ZephyrCloudIO/winget-pkgs`](https://github.com/ZephyrCloudIO/winget-pkgs) and opens a PR against `microsoft/winget-pkgs:master`. A package-manager failure does not undo the SEA release; rerun failed jobs, or dispatch **Package managers** with a published stable version for a backfill. Install the existing `zephyr-workflows-automation` GitHub App on both public repositories with Contents write permission. The jobs mint short-lived, repository-scoped App tokens using `ZE_WORKFLOW_AUTOMATION_CLIENT_ID` and `ZE_WORKFLOW_AUTOMATION_PRIVATE_KEY`, already used by the release-please workflow. To open the upstream PR, set the separate `ZE_WINGET_SUBMIT_TOKEN` Actions secret to a classic personal access token with only the `public_repo` scope and an expiration. GitHub does not yet support fine-grained personal access tokens for contributing to public repositories outside your organizations; this classic scope is limited to public repositories but cannot target only Microsoft's repository. The user token is used only in the WinGet PR job, never for commits or downloads. The WinGet PR discloses that the source repository is currently private and leaves Windows-local validation unchecked; Microsoft runs its own checks, and acceptance and indexing may lag the stable release. Verify on Windows with `winget validate --manifest ` and `winget install --manifest ` using the workflow artifact. The binary behaves like the Node CLI with two packaging limits: MCP stdio servers run without the process-tree supervisor (only the direct child is reaped on shutdown), and the TUI extracts its native assets to a versioned cache folder on first launch. macOS may require the user to approve an unnotarized executable in Privacy & Security. ## Injected-host mode `serve --injected-host` is the official injected-host entrypoint for embedding supervisors; the supervisor launches this binary instead of building its own. It serves the same harness HTTP/SSE protocol on loopback with `credentialMode: 'injected-only'`: stored records, configured keys, and environment reads fail closed instead of resolving ambient authority. Provider, attachment, and tool frames arrive through the injected ports (embedder-implemented providers, content resolution after visibility filtering, host custody around final dispatch), never from the environment. The ready line prints ` mode=injected-host` for the supervisor to parse. ## Agent Client Protocol `ze-harness acp` serves the [Agent Client Protocol](https://agentclientprotocol.com) over standard input and output, so ACP clients such as Zed or T3 Code can drive Ze Harness as their agent. Point the client at the `ze-harness` executable with the single argument `acp`. The command runs an embedded host and needs no daemon; stdout carries only protocol frames and every diagnostic goes to stderr. Each ACP session is one Ze Session in the client's working directory, with symlinks resolved. Prompts queue Turns that stream text, reasoning, tool calls, and usage back as session updates. Tool approvals arrive as ACP permission requests with **Allow once**, **Always allow in this session** (when the Tool proposes save patterns), and **Reject**. Questions become form elicitations when the client supports them and are dismissed otherwise. Primary agents such as `build` and `plan` are offered as session modes; the model and reasoning effort are session config options. Sessions can be listed, loaded with their history replayed, resumed, forked, and closed. ACP has no steering: a prompt sent while a Turn runs is rejected, and clients cancel and resend instead. MCP servers the client sends are attached to that Session only. Stdio servers run as local subprocesses of the `acp` process, with its user's authority. Their header and environment values stay in the `acp` process memory and are never written to the database, config, or events; clients resend them whenever they load or resume a Session. Ze sends MCP credential headers only over HTTPS, so an `http://` server with headers is reported on stderr and does not connect. SSE MCP servers are not supported. ACP Sessions use their own database, `acp.db` in the shared state directory, so clients that list the daemon's Sessions do not see editor work. Pass `--database` or set `ZE_HARNESS_DB` to share a database instead. Providers, configuration, and plugins load as they do for local commands, so connect providers once with `ze-harness provider connect`. ## Public TypeScript exports - `CliComposition` - `CliInputError` - `CliIO` - `defaultDatabasePath` - `detectTuiRequest` - `GlobalOptions` - `runCli` - `runClientCommand` - `TUI_FFI_FLAG` - `TUI_REEXEC_ENV` - `TuiCommands` - `TuiLaunch` - `TuiRunner` - `VERSION` ## Command reference The output of `ze-harness --help`. ```text DESCRIPTION Run and manage AI coding Sessions with Ze Harness USAGE ze-harness [flags] FLAGS --remote string Connect to a remote Harness instead of the local daemon --embedded Run in-process instead of using the local daemon --resume, -s string Resume an existing Session in the TUI --server string Select a server; requires --remote --token string Authenticate to a remote Harness with a bearer token --config string Use a local configuration file; unavailable with --remote --database string Use a local SQLite database path or ':memory:' --mcp-callback-port string Listen locally for MCP OAuth callbacks GLOBAL FLAGS --help, -h Show help information --version, -v Show version information --wizard Start wizard mode for a command --completions Print shell completion script (choices: bash, zsh, fish, sh) --log-level Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none) SUBCOMMANDS tui Open the interactive terminal interface run Run one prompt and print the result chat Start a line-oriented interactive chat completions Print a shell completion script; see the installation examples below update Check for and install the latest version from the installation channel uninstall Remove this native installation while retaining user data session Create and manage Sessions; some commands require a remote server provider Manage model provider connections mcp Use MCP servers, resources, prompts, and OAuth permission Reply to and manage tool permissions on a remote server question Answer user questions asked by tools on a remote server compaction Inspect and cancel context compactions turn Manage Turns on a remote server plugins Inspect and manage plugins serve Start a local Harness HTTP server acp Serve the Agent Client Protocol over standard input and output service Manage the local Harness daemon server Manage a remote Harness server runtime Manage the remote Harness runtime EXAMPLES # Open the interactive TUI in a terminal ze-harness # Resume a Session in the TUI ze-harness -s 550e8400-e29b-41d4-a716-446655440000 # Open the provider setup dialog ze-harness tui --onboarding # Run one prompt locally ze-harness run "Explain this repository" # List Sessions on a remote server ze-harness --remote http://127.0.0.1:4317 --server main session list # Show help for a command ze-harness session --help # Install shell completions ze-harness completions --help ``` --- Generated from [`packages/cli/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/cli/README.md) and [`packages/cli/src/`](https://github.com/ZephyrCloudIO/ze-harness/tree/main/packages/cli/src). --- # `@zephyrcloudio/harness-client` Typed Effect Client for the Ze Harness v1 protocol. Use network Fetch with `baseUrl`, or inject Fetch for an in-process handler: ```ts import { createClient } from '@zephyrcloudio/harness-client'; const client = createClient({ baseUrl: 'http://127.0.0.1:4317' }); ``` Every session call addresses one harness server. Local hosts serve only the default server, so the option can be omitted there; remote callers pass the target server explicitly: ```ts import { ServerName, createClient } from '@zephyrcloudio/harness-client'; const remote = createClient({ baseUrl: 'https://harness.example', server: ServerName.make('my-server'), }); ``` Pass `onServerVersion: ({ protocolVersion, harnessVersion }) => { ... }` to observe the `Ze-Harness-Version` response header, such as `1/0.6.0`. The callback runs for the first valid value and each subsequent change, on ordinary responses and newly opened SSE streams. No handshake request is added and no response body is consumed to obtain the version. Missing or malformed headers are ignored. Callback exceptions and protocol/version differences do not reject, retry, or cancel API calls; the observer is for application notices, not enforcement. An existing SSE response keeps its original headers until the next connection. Client methods return Effects. `watchTurn` and `watchSession` validate SSE progress, reconnect where safe, apply text deltas and tool-activity replacements, and reconcile gaps from authoritative snapshots. Tool activity checkpoints must advance monotonically and match the requested tool part. The Client does not import Core or Server. `watchSession` resumes transport failures from its last safe activity cursor. Before each retry it emits `session.reconnecting` with the error and delay, and it emits `session.resumed` once the resumed stream catches up. Servers repeat the cursor while idle, so the watch also treats three missed heartbeats (`SESSION_EVENT_HEARTBEAT_MILLIS` × 3) without any event as a lost connection. When a reconciliation read overtakes queued stream snapshots or deltas, the Client keeps the newer state and consumes the covered revisions without moving the Turn backwards. `watchTurn` reconnects after transport and server (5xx) failures and resumes after its last known revision. While a Turn is quiet, servers send `turn.heartbeat` and never leave a gap longer than `TURN_EVENT_HEARTBEAT_MILLIS`, so the watch treats three such intervals (`TURN_EVENT_HEARTBEAT_MILLIS` × 3) without any event as a lost connection. Heartbeats are not forwarded to `onEvent`. On both watches, a response body that fails mid-stream, such as a closed or timed-out connection, is a transport failure and resumes. Malformed events are still protocol failures. `submitPrompt` requires either queue delivery with a `turnId`, or steer delivery with a `steerId` and `expectedTurnId`. `client.watchServer({ afterServerCursor?, onEvent? })` watches every Session on the server from one stream. It pages no history. Without `afterServerCursor` it starts live: subscribe first, read the current cursor from `server.ready`, then bootstrap with `listSessions()` and, per Session, `listDescendantPendingApprovals` and `listDescendantPendingQuestions`; events that arrive during the bootstrap are upserts over it. Pass `afterServerCursor` instead to replay every stamped row after a cursor observed earlier (zero replays everything). It emits `server.ready` on the first event, carrying the cursor the server announced, `session.created` (a Session snapshot, repeatable after a resume, so upsert by `session.id`), `session.title`, `session.deleted`, `session.turn.updated` (the authoritative Turn, reconciled through `getTurn` when a durable row names state the watch does not hold, carrying the `serverCursor` of the event that produced it; a 404 or 409 on that read means the Session or Turn is gone or closing for deletion, so the Turn is dropped and the stream continues until its `session.deleted` arrives), `server.activity` when the cursor advances past rows that produced no event, and the same `server.reconnecting` and `server.resumed` lifecycle as `watchSession`, with the same idle timeout (`SERVER_EVENT_HEARTBEAT_MILLIS` × 3) and backoff. Live text deltas are not on this stream; watch a Session for those. Use `appendConversation(sessionId, { content })` to add durable user context without creating a Turn or calling a model. The Client generates an entry ID unless `entryId` is supplied. Work already admitted before the append does not see the entry. Pass `authorId` to attribute the entry to a stable human author; receipts and reads echo it back. Use `getSessionConfiguration` and revision-checked `replaceSessionConfiguration` for ordered Session rules. Use `replyPermission` with `once`, `always`, or `reject`. `listSavedPermissions` and `removeSavedPermission` manage the durable allow grants created by `always`. Use `forkSession(sourceId, { id?, turnId? })` to copy settled history before an optional Turn into a new Session; an absent `turnId` forks the whole history. Pass `through` instead to copy through a named message inclusively — a Turn message selects its whole Turn, a conversation entry selects itself — and `visibility` to restrict the copy to what the forking actor may observe. The fork inherits the parent workdir, model, system prompt, and permission rules, and records `forkedFrom` lineage on the new Session. Use `listSessions()` for the server's Sessions oldest-first, and `deleteServer()` to cancel and delete them all (workspace files stay). Pass `workdir` to `createSession` for a per-Session working directory; the server root is the default. Use `listSessionChildren(sessionId)` for a Session's spawned children, `promoteSessionBackground(sessionId)` to detach its blocking foreground sub-agent chains, and `capabilities()` to report host feature flags. ## Model discovery and provider management The client provides typed discovery and connection management: - `client.getRuntimeModels()` and `client.watchRuntimeModels()`: fetch the runtime model snapshot, or take a single-shot current-revision check. - `client.getSessionModels(sessionId)` and `client.watchSessionModels(sessionId)`: fetch the snapshot a Session adopted, or take a single-shot current-revision check. - `client.listProviders()` and `client.getProvider(providerId)`: list and inspect safe provider summaries and connection status. - `client.connectProviderKey(providerId, { apiKey, label? })`: connect a provider API key atomically. - `client.updateProviderConnection(providerId, connectionId, { label })`: update a connection's label. - `client.deleteProviderConnection(providerId, connectionId)`: delete a stored connection. The `watch*Models` streams are single-shot, not long-lived subscriptions: the server emits the current revision once and closes. Re-call to re-check, and use the `get*` snapshot calls for fresh catalog data. ## Public TypeScript exports - `AddSessionPluginRequest` - `AgentId` - `AppendConversationInput` - `AuthorId` - `CatalogSnapshot` - `ClientError` - `ClientId` - `ClientWorkdir` - `ClientWorkdirValue` - `CompleteMcpCredentialCallbackInput` - `ContextCompactionId` - `ContextCompactionOutcome` - `ContextCompactionReceipt` - `ContextCompactionSnapshot` - `ConversationContent` - `ConversationEntryId` - `ConversationEntryReceipt` - `createClient` - `CreateClientOptions` - `CreateSessionInput` - `ForkSessionInput` - `HARNESS_PROTOCOL_VERSION` - `HarnessClient` - `HarnessFetch` - `imageBase64Prompt` - `ImageMimeType` - `ImageReference` - `imageUrlPrompt` - `ListPluginJournalOptions` - `ListSessionCompactionsOptions` - `ListSessionTurnsOptions` - `MCP_CREDENTIAL_AUTHORIZATION_REQUIRED_REASONS` - `MCP_CREDENTIAL_CALLBACK_DENIED_REASONS` - `MCP_CREDENTIAL_REVOCATION_STATUSES` - `MCP_PROTOCOL_VERSION` - `ModelRef` - `PendingDescendantApproval` - `PendingDescendantQuestion` - `PermissionReply` - `PermissionRequest` - `PermissionRequestId` - `PluginJournalCursorInput` - `ProjectId` - `PromptContent` - `PromptContentPart` - `QuestionAnswer` - `QuestionInfo` - `QuestionOption` - `QuestionRequest` - `QuestionRequestId` - `ReasoningEffort` - `SavedPermission` - `SavedPermissionId` - `SERVER_VERSION_HEADER` - `ServerActivityCursor` - `ServerName` - `ServerVersion` - `ServerWatchEvent` - `Session` - `SessionActivityCursor` - `SessionAgentEntryValue` - `SessionAgentsResponseValue` - `SessionCompactionHistoryCursor` - `SessionCompactionState` - `SessionConfiguration` - `SessionConfigurationValue` - `SessionHistoryCursor` - `SessionId` - `SessionWatchEvent` - `SessionWorkdir` - `SkillId` - `SkillListEntryValue` - `SkillSelection` - `sniffImageMime` - `StartMcpCredentialInput` - `SteerId` - `SteerReceipt` - `SubmitContextCompactionInput` - `SubmitMcpPromptInput` - `SubmitPromptReceipt` - `SubmitPromptRequest` - `textPrompt` - `ToolPermissionRequest` - `TurnId` - `TurnSnapshot` - `TurnWatchEvent` - `WatchServerOptions` - `WatchSessionOptions` - `WatchTurnOptions` --- Generated from [`packages/client/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/client/README.md). --- # @zephyrcloudio/harness-codemode Run JavaScript that combines tools and returns selected results. For using CodeMode in the terminal, see the [CLI guide](/docs/cli.md). ## Usage ```ts import { CodeMode, Tool, renderResult } from '@zephyrcloudio/harness-codemode'; import { Effect, Schema } from 'effect'; const runtime = CodeMode.make({ tools: { echo: Tool.make({ description: 'Return the supplied JSON value.', input: Schema.Json, output: Schema.Json, execute: (value) => Effect.succeed(value), }), }, }); const result = await Effect.runPromise(runtime.execute('return await tools.echo("hello");')); console.log(renderResult(result)); // hello ``` Use `await` for dependent calls and `Promise.all` or `Promise.allSettled` for independent work. Await every call you need: returning interrupts unfinished work. Use `return` for the result; console output, including `console.table`, is also included when execution finishes. Scripts can access only the supplied tools, with no imports or direct filesystem/network access. Use synchronous `search({ query })` inside a script to find an unfamiliar tool's path and signature. Effect schemas validate tool arguments and results; plain JSON Schemas describe them but do not validate them. Tool authorization remains the host's responsibility. ## Display formatting ```ts const { formatCodeMode } = await import('@zephyrcloudio/harness-codemode/format'); const displaySource = await formatCodeMode(source); ``` Formatting accepts JavaScript and TypeScript and leaves invalid source unchanged. Execution still requires JavaScript; use formatted text for display only. For repeated renders, create a reusable formatter with `makeCodeModeFormatter()` from the same import path. ## Limits The interpreter currently has these fixed limits: - String length: 16,777,216 code units. - Array length: 10,000,000 elements. - Pending promises: 10,000. - Plain-data value depth: 32. - Function call depth: 10,000; resets when an await resumes. - Console formatting depth: 32. - Signature schema rendering depth: 8. Execution limits produce an error; console and signature depth limits abbreviate displayed values. These limits are not configurable. There is no default execution timeout or total tool-call limit. ## Public TypeScript exports - `CodeMode` - `Tool` --- Generated from [`packages/codemode/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/codemode/README.md). --- # Configuration file The Node host (the `ze-harness` CLI, the SDK, and the Node.js server) reads a global `config.json` and the nearest project `ze-harness.config.json`. Every file uses this format. Later sources override earlier ones; the server-node package README describes how agents, providers, and permissions merge. The full JSON Schema is at [/ze-harness.config.schema.json](https://github.com/ze-harness.config.schema.json). ## `model` The default model as `provider/model`, such as `openai/gpt-5-mini`. ```ts string ``` ## `agents` Agents by ID. An entry defines a new agent or overrides fields of a built-in or plugin agent with the same ID. ```ts Record ``` ## `defaultAgent` The agent new Turns use when they name none. It must support primary use. ```ts string ``` ## `mcp` MCP servers the host connects to. IDs and names must be unique. ```ts Array< | { kind: "local"; id: string; // Stable server identity, unique across MCP servers. (max length 128) // Server name. The model sees each of the server's Tools as `_`. (max length 128) name: string; enabled?: boolean; // `false` keeps the server listed but never connects it. Defaults to `true`. order?: number; // Sort key for the order of servers' Tools and instructions. Defaults to 0. // Also gives the model a `_read-resource` Tool for reading the server's resources. // Defaults to `false`. resourceReadTool?: boolean; // MCP protocol versions to accept; the server must answer with one. Defaults to `2025-06-18`, // the only version Core supports. (at least 1) protocolVersions?: string[]; clientCapabilities?: { roots: false; sampling: false; elicitation: false }; // Time limits in milliseconds. Reaching one fails that operation. Defaults to unlimited. limits?: { // One connection attempt: starting the transport, initializing, and reading the catalog. startupMs: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; // One catalog read. catalogMs: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; // One Tool call, resource read, or prompt resolution. executionMs: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; // `manual` never retries; `retry` retries a retryable connection failure every `delayMs`, up to // `maxAttempts` counting the first attempt. Defaults to retrying every 1,000 ms without limit. reconnect?: | { kind: "manual" } | { kind: "retry"; delayMs: number; // min 1 maxAttempts: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; // A server the host starts as a child process and talks to over stdio. transport: { kind: "stdio"; command: string; // The executable the host starts. arguments?: string[]; // Command-line arguments. // Environment variables for the server process. The Node host passes only these, plus `PATH` // and temporary-directory variables. environment?: Record; }; } | { kind: "remote"; id: string; // Stable server identity, unique across MCP servers. (max length 128) // Server name. The model sees each of the server's Tools as `_`. (max length 128) name: string; enabled?: boolean; // `false` keeps the server listed but never connects it. Defaults to `true`. order?: number; // Sort key for the order of servers' Tools and instructions. Defaults to 0. // Also gives the model a `_read-resource` Tool for reading the server's resources. // Defaults to `false`. resourceReadTool?: boolean; // MCP protocol versions to accept; the server must answer with one. Defaults to `2025-06-18`, // the only version Core supports. (at least 1) protocolVersions?: string[]; clientCapabilities?: { roots: false; sampling: false; elicitation: false }; // Time limits in milliseconds. Reaching one fails that operation. Defaults to unlimited. limits?: { // One connection attempt: starting the transport, initializing, and reading the catalog. startupMs: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; // One catalog read. catalogMs: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; // One Tool call, resource read, or prompt resolution. executionMs: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; // `manual` never retries; `retry` retries a retryable connection failure every `delayMs`, up to // `maxAttempts` counting the first attempt. Defaults to retrying every 1,000 ms without limit. reconnect?: | { kind: "manual" } | { kind: "retry"; delayMs: number; // min 1 maxAttempts: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; // A server the host reaches over MCP streamable HTTP. transport: { kind: "streamable-http"; endpoint: string; // An `http://` or `https://` URL with no credentials in it. (uri) // Request headers, resolved from credential references. headers?: Record; // An OAuth credential that authorizes requests. The host then owns `Authorization`. oauth?: { kind: "oauth"; reference: string /* max length 256 */ }; }; } > ``` ## `plugins` Trusted plugins to load: a source string, or an object with options. Plugins run unsandboxed with full process authority. ```ts Array< | string | { // An exact npm package name, or a local path starting with `./`, `../`, or `/`. Local paths // resolve from the config file that declares them. source: string; // JSON passed to the plugin as `context.options` in `setup`. Defaults to `{}`. options?: Record; // `false` removes the same source declared by an earlier config layer. Defaults to `true`. enabled?: boolean; } > ``` ## `skills` Extra local directories to search for `SKILL.md` files, besides the standard locations. Relative paths resolve from the working directory. ```ts string[] ``` ## `providers` Model providers by ID. `true` enables a catalog provider with defaults. Without this map, OpenAI, OpenCode Go, and OpenRouter are enabled; with it, only listed providers are. ```ts Record; enabled?: boolean; // `false` disables a provider. Defaults to `true`. // A built-in provider whose transport and model list this entry inherits, so one provider can // have several entries with separate credentials. It cannot name this entry or another entry // with a `base`. (max length 128) base?: string; // A provider that holds this provider's key, such as `tap`. Requests then authenticate with // that provider's credential instead of a local key. (max length 128) via?: string; // Provider definition fields: routes, authentication, headers, credential environment names, // and models. A provider the models.dev catalog does not know needs a full definition. definition?: unknown; } > ``` ## `observerBufferSize` Capacity of each Turn's in-memory event queue for in-process observers. When it is full the oldest events are dropped; durable Turn state is unaffected. Defaults to 128. ```ts number ``` ## `contentFlushMillis` Longest time, in milliseconds, streamed model content waits before it is committed and published. Block boundaries and the end of a response commit immediately; 0 commits every provider chunk. Defaults to 100. ```ts number ``` ## `codemode` CodeMode, which lets the model combine tool calls in one script. ```ts { enabled?: boolean /* `false` turns CodeMode off. Defaults to `true`. */ } ``` ## `steering` ```ts { // How many batches of steering input one Turn may take in. Defaults to unlimited. maxBatches?: { kind: "unlimited" } | { kind: "finite"; value: number }; } ``` ## `subagents` ```ts { // How many levels of sub-agents may nest. The default, 1, lets a Turn start sub-agents that // cannot start their own. maxDepth?: { kind: "unlimited" } | { kind: "finite"; value: number }; } ``` ## `permissions` Ordered permission rules, appended after Ze's defaults and earlier config layers. The last matching rule wins. The defaults allow everything inside the Session workdir and ask for paths outside it and for `.env` reads. ```ts { action: string; // The tool action to match, such as `read`, `edit`, or `shell`. `*` matches any. // A pattern over the resource, such as a path or shell command. `*` matches any characters and // `?` one. resource: string; // `allow` runs the operation, `ask` waits for the user, and `deny` refuses it. effect: "allow" | "ask" | "deny"; }[] ``` ## `permissionMatching` ```ts { // Whether rule patterns match case-sensitively. Defaults to `true`, or `false` on Windows. caseSensitive?: boolean; } ``` ## `approval` ```ts { // How long a permission request may wait for an answer. Defaults to unlimited. timeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; } ``` ## `timeouts` ```ts { // The longest a Turn may run. Defaults to unlimited. turnMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; } ``` ## `provider` Model request retries. Core retries only attempts the provider rejected before emitting output. ```ts { // Attempts per model request, including retries. Defaults to unlimited. maxAttempts?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; // The longest one model request attempt may run. Defaults to unlimited. attemptTimeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; // The longest one model request may take across all its attempts. Defaults to unlimited. elapsedMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; // The delay between retries. Defaults to none. pacing?: | { kind: "none" } | { kind: "fixed"; delayMs: number } | { kind: "exponential"; initialDelayMs: number; // min 1 maximumDelayMs: number; // min 1 jitter: boolean; }; } ``` ## `context` ```ts { // Context units each model request reserves beyond the model's output limit. Defaults to 1024. safetyReserveUnits?: number; } ``` ## `images` ```ts { // The largest base64 payload, in bytes, of one attached image. URL images are not measured. // Defaults to unlimited. maxBase64Bytes?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; } ``` ## `JsonValue` ```ts string | number | boolean | null | JsonValue[] | Record ``` --- Generated from [`packages/server-node/src/bootstrap/config/schema.ts`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/server-node/src/bootstrap/config/schema.ts). --- # `@zephyrcloudio/harness` The platform-neutral Ze Harness Core. It owns durable Session coordination, Turn execution, plugin generations, policy, cancellation, terminal outcomes, and the Storage service contract. Advanced hosts compose `createCoreRuntime`, `makeSessionServices`, and a `StorageService` from the package root. Most applications should use `@zephyrcloudio/harness-sdk` instead. ```ts import { createCoreRuntime, makeSessionServices, Storage } from '@zephyrcloudio/harness'; ``` Core has no Node, HTTP, config-discovery, plugin-loading, CLI, SDK, or SQLite imports. Required state changes are awaited; events are observational only. Core owns ordered action/resource permission evaluation, Tool projection validation, durable pending requests, Session grants, and terminal settlement. Configured rules use full-string `*` and `?` matching; the last matching rule wins and no match asks. The effective order is Ze defaults, Server, Session, then Agent. Ze defaults allow every Tool inside the session workdir and ask only for `external_directory` paths and `.env` reads (`.env.example` stays allowed). Saved grants and hook allowances never override a configured deny. ## Hybrid CodeMode Agents can use `execute({ code })` to combine tool calls in JavaScript. Direct tools remain available, and each operation inside a script still requires its usual permissions. `execute` itself never asks for approval. Rejecting one operation can be handled by the script; cancelling the Turn stops the whole execution. Interrupted scripts are not automatically replayed after a restart. CodeMode is on by default. Each host has one switch to turn it off: | Host | Setting | | ------------------------------------------ | ----------------------------------------------------------------------------------------- | | Node server, CLI, and SDK | `"codemode": { "enabled": false }` in `ze-harness.config.json` or the SDK `config` option | | `WorkerHarnessHost` embedders, such as TAP | `codemode: { enabled: false }` passed to the constructor | | The Worker deployment in `packages/worker` | `HARNESS_CODEMODE_ENABLED=false`, which that Worker passes as the `codemode` option | A configured `execute` deny rule also hides it. See the [CLI guide](/docs/cli.md) for inspecting executions and the [CodeMode package](/docs/codemode.md) for scripting usage and limits. ## Activity streams `SessionServices.observation` exposes two cursor streams over durable state. `watchSessionActivity` pages one Session's changed Turns (and its title) after a per-Session `SessionActivityCursor`. `watchServerActivity` pages every stamped Session, Turn, and Session deletion in the server's database after a `ServerActivityCursor`: one monotonic counter per database that is stamped in the same transaction as the change and never reused after a delete, so one reader observes spawned children, their approvals and questions, and deletions without polling each Session. Omit `afterServerCursor` to watch live from the current cursor (`Storage.readServerActivityCursor`); pass one to replay from it. Both streams wake on the in-process activity hub, re-read when idle if `idleRefresh` is set, and advance to the page's observed cursor once caught up. ## Conversation entries The composed `SessionServices` expose a `conversation` slice that appends outside conversation to a Session without admitting work: ```ts import { ConversationEntryId } from '@zephyrcloudio/harness-schema'; const receipt = yield * services.conversation.appendConversation({ sessionId, request: { entryId: ConversationEntryId.make('00000000-0000-4000-8000-000000000002'), content: [{ type: 'text', text: 'Alice: the deploy finished.' }], }, attribution: { clientId }, }); ``` Each entry commits to Session history under the next history revision as a plain user message. It never wakes the runner, never creates a Turn, and never calls a model. Every Turn and manual compaction pins its model-visible history to its own admission revision, so work admitted before an append never sees it; the next admitted work sees the full conversation. Replaying the same `entryId` with identical content returns the recorded receipt, and reusing it with different content fails with `ConversationEntryConflict`. ## Provider authority and catalog composition Core owns `ProviderAuthority`, which: - Loads models and pricing from a host-supplied models.dev snapshot or the bundled offline snapshot (see [Model catalog](#model-catalog)). - Compiles one declarative provider plan for Turn registration and discovery. The plan maps supported models.dev package and shape metadata to Ze's native `chat`, `responses`, and `messages` adapters, while unknown explicit packages and shapes fail closed. - Resolves credentials from valid stored connections, a configured `providers..options.apiKey`, or each definition's environment variables. Discovery checks stored state directly; Turn authentication checks the configured key first. - The discovery default is the configured default when it names an available composed model, otherwise no default (discovery stays up so credentials can be connected later). With no configured default, the first available model in lexical ref order. Turn setup instead fails loudly on a configured-but-unavailable ref — never a silent substitution to another provider. - Emits atomic catalog revisions upon credential change and manages `reconnect_required` status safely. Provider discovery adds model IDs to the snapshot list; explicit `definition.models` entries take precedence. Expected discovery failures log a warning and retain snapshot and configured models. Unknown discovered or configured models default to text input/output with tools enabled. Explicit capabilities, including `tools: false`, take precedence. Unknown limits remain zero until supplied by discovery or config; no context or output window is guessed. ## Model catalog One pipeline keeps two catalogs in step: - The **bundled snapshot** (`src/model/bundled-models.json`) ships in every build, so a harness works with no download. - The **published catalog** is a models.dev `api.json` document that Node hosts refresh from `ZE_HARNESS_MODELS_URL` (see the [Server Node README](/docs/server-node.md)). It uses the models.dev format so every released binary reads it with the normalizer it already applies to models.dev; there is no second format. `pnpm update:models-dev` (from the repository root) builds Core, fetches models.dev, and applies `scripts/model-catalog-corrections.json`. It keeps only providers the harness can use: those whose declared package maps to a native protocol, plus the declarative presets. It then normalizes that document with the runtime normalizer and rewrites the bundled snapshot only when provider content changed. Pass `--published ` to also write the published document; the bundled snapshot is that document's normalized form, so shipped and published cannot drift. The build refuses to write anything when `anthropic`, `openai`, `opencode-go`, or `openrouter` has no usable models. Set `MODELS_DEV_URL` to override the `https://models.dev/api.json` source. Commit the updated bundle and rebuild before distributing it. The normalizer maps each model's wire protocol from models.dev metadata: a model's `provider.shape` (`completions` is `chat`, `responses` is `responses`) wins over its `provider.npm` (`@ai-sdk/openai-compatible` is `chat`, `@ai-sdk/openai` is `responses`, `@ai-sdk/anthropic` is `messages`), which wins over the provider's package. Unknown explicit packages, shapes, statuses, and invalid catalog IDs are dropped rather than silently routed. ### Corrections `scripts/model-catalog-corrections.json` fixes upstream data before it is published or bundled: ```json { "models": [ { "provider": "openai", "model": "gpt-x", "reason": "Retired upstream", "hidden": true }, { "provider": "openrouter", "model": "a/b", "reason": "Typo", "displayName": "A B" }, { "provider": "opencode-go", "model": "c", "reason": "Served on /messages", "protocol": "messages" } ] } ``` Each entry names one model and a `reason`, and either hides it or sets `displayName` and/or `protocol` (`chat`, `responses`, or `messages`; written as the matching models.dev package). Unknown fields, duplicate entries, entries that change nothing, and entries for models upstream no longer lists fail the build, so a stale correction is removed instead of silently ignored. ### TAP credits With `TAP_GATEWAY_URL` set (for example `https://gateway.theaiplatform.app`), the build adds a `tap` provider (TAP credits) from the gateway's public `GET /v1/credits/models` listing: chat protocol, base URL `/v1/credits`, gateway prices, and limits and capabilities from the matching models.dev OpenRouter model. Listed models without a models.dev OpenRouter entry are skipped because the listing has no output limit. Without `TAP_GATEWAY_URL` the section is omitted. The `tap` preset owns routing and keeps discovering models from `GET /v1/credits/models` whether or not the section is present. The preset base URL takes precedence over the section's URL, and the section supplies limits and prices for the models it lists. ### Publishing `.github/workflows/publish-models.yml` runs daily and on demand. It builds the published document, validates it with the harness normalizer (`scripts/publish-model-catalog.mjs`), and, on `main` once the bucket is configured, uploads it to R2 as `api.json` with `Cache-Control: public, max-age=300`. R2 derives the `ETag` from the bytes, so an unchanged catalog answers a harness's `If-None-Match` with `304`. Pull requests that change the pipeline build and validate without uploading. One-time setup before Node hosts default to the published catalog: 1. Create an R2 bucket in the Cloudflare account that owns `theaiplatform.app`. 2. Attach the custom domain `models.theaiplatform.app` to the bucket. 3. Create an R2 API token with Object Read & Write on that bucket only. 4. Add repository variables `ZE_MODELS_R2_ACCOUNT_ID` and `ZE_MODELS_R2_BUCKET`, and secrets `ZE_MODELS_R2_ACCESS_KEY_ID` and `ZE_MODELS_R2_SECRET_ACCESS_KEY` from the token. Optionally add variable `ZE_MODELS_TAP_GATEWAY_URL` (the gateway origin) to publish the `tap` section. 5. Run the workflow, then check that `https://models.theaiplatform.app/api.json` returns an `ETag` and answers that `ETag` in `If-None-Match` with `304`. 6. Change the `ZE_HARNESS_MODELS_URL` default in `packages/server-node/src/bootstrap/models-dev-cache.ts` and the Server Node README to `https://models.theaiplatform.app/api.json`. ## Named provider entries Each `providers.` entry is its own provider with its own credential. A named entry can inherit the transport and model list of a built-in with `base`, then override the display name, credential source, and any definition fields: ```jsonc { "providers": { "openai-work": { "base": "openai", "definition": { "displayName": "Work GPT", "credentials": { "environment": ["OPENAI_WORK_API_KEY"] }, }, }, "openai-personal": { "base": "openai", "definition": { "displayName": "Personal GPT", "credentials": { "environment": ["OPENAI_PERSONAL_API_KEY"] }, }, }, }, } ``` Models are addressed per entry (`openai-work/gpt-5-mini`), and each entry keeps one stored connection, one live-port key, and its own environment list. The `base` must name a known preset or snapshot provider, cannot point at the entry itself, and cannot chain through another `base` entry. Unknown, self, and chained bases fail closed at setup. ## MCP screenshots MCP tool results containing images preserve the ordered text and image blocks plus any `structuredContent` as a multipart canonical tool result. Other MCP blocks remain JSON parts, and private `_meta` values are excluded. Audio and blob resources beyond 10 MB decoded are replaced with an explicit omission note instead of model text. Supported images use canonical base64 with PNG, JPEG, GIF, or WebP MIME types; unsupported image content fails explicitly. Results survive tool settlement, checkpoints, history replay, and compaction input. The selected model must accept images, and the existing `images.maxBase64Bytes` setting also applies to tool images. New oversized results settle as failed tools with a request for smaller output, allowing the model to continue; oversized images already in history still fail request validation. URL images are not measured by this base64-only limit. Failed MCP calls retain their image content and failure status. Exact JSON text duplicates of `structuredContent` are omitted; accompanying prose remains. Text and JSON tools retain their existing result formats. This adds image handling to MCP tools; ordinary plugin tool return values continue to be interpreted as JSON. The default request-size estimator counts serialized bytes, including base64 images. Hosts that need provider-specific image token accounting can supply `requestSizeEstimator` to `createCoreRuntime`. ## Public TypeScript exports - `AdmittedPrompt` - `AdmittedSteer` - `AppendConversationCommand` - `AppendConversationEntryCommand` - `CancelSessionMcpCredentialCommand` - `ClaimedContextCompaction` - `ClaimedSessionWork` - `ClaimedTurn` - `CompleteSessionMcpCredentialCallbackCommand` - `ConfiguredAgent` - `ContextCompactionConflict` - `ContextCompactionNotFound` - `ConversationEntryConflict` - `CoreMcpCandidate` - `CoreRuntime` - `CoreRuntimeCandidate` - `CoreRuntimeConfig` - `createCoreRuntime` - `CreateCoreRuntimeOptions` - `defaultProviderIds` - `DeleteSessionCommand` - `DurableContentResolutionError` - `DurableContentResolver` - `DurableContextSelection` - `DurableHistoryMessage` - `ForkSessionCommand` - `ForkSessionInput` - `GenerationCompanion` - `HistoryVisibilityConflict` - `InvalidServerCursor` - `InvalidSessionCursor` - `InvalidTurnTransition` - `ListPluginJournalInput` - `ListServerActivityInput` - `ListSessionActivityInput` - `ListSessionCompactionsInput` - `ListSessionConversationInput` - `ListSessionTurnsInput` - `ListTurnEventsInput` - `loadBundledModelsDevSnapshot` - `makeLateBoundMcpCredentialConnections` - `makeReconnectingMcpCredentialLifecycle` - `makeSessionMcpCredentialRuntime` - `makeSessionMcpCredentialServerResolver` - `makeSessionMutationGate` - `makeSessionServices` - `MCP_CREDENTIAL_AUTHORIZATION_REQUIRED_REASONS` - `MCP_CREDENTIAL_CALLBACK_DENIED_REASONS` - `MCP_CREDENTIAL_REVOCATION_STATUSES` - `McpCredentialHostConnections` - `McpCredentialIdentity` - `McpPromptReservation` - `McpPromptReservationInput` - `NormalizedModelRecord` - `NormalizedModelsDevSnapshot` - `NormalizedProviderRecord` - `PermissionConflict` - `PermissionReplyReceipt` - `PermissionReplyResolution` - `PermissionRequestNotFound` - `PermissionResolution` - `PluginJournalPage` - `PreparedTurnInput` - `PrivateHistoryMessageContinuation` - `PrivateModelMessageContinuation` - `PrivateSessionPluginIntegration` - `PrivateTurnMessageContinuation` - `ProgrammaticPluginCandidate` - `PromotedSteer` - `PromptAdmissionContext` - `PromptAdmissionStale` - `PromptConflict` - `ProviderConfigEntry` - `ProviderConfigMap` - `ProviderDefinitionMap` - `PublicMcpCatalogPrompt` - `PublicMcpCatalogResource` - `PublicMcpCatalogResourceTemplate` - `PublicMcpCatalogServer` - `PublicMcpCatalogSnapshot` - `PublicMcpCatalogTool` - `PublicMcpResourceAnnotations` - `PublicMcpResourceContents` - `PublicMcpResourceReadResult` - `PublicMcpServerCatalog` - `publicSessionMcpCredentialForgetResult` - `publicSessionMcpCredentialStatus` - `QuestionConflict` - `QuestionReplyReceipt` - `QuestionRequestNotFound` - `QuestionResolution` - `QuestionResolutionInput` - `ReadMcpResourceCommand` - `RecoveryResult` - `RejectQuestionInput` - `ReloadResult` - `ReplaceSessionConfigurationInput` - `ReplyPermissionInput` - `ReplyQuestionInput` - `RequestSizeEstimationError` - `RequestSizeEstimator` - `ResolvedModel` - `ResolvedPrompt` - `RunInput` - `RuntimeCandidateError` - `RuntimeStartupError` - `SavedPermissionNotFound` - `ServerActivityEntry` - `ServerActivityPage` - `ServerDeletionReceipt` - `ServerName` - `SessionActivityPage` - `SessionCompactionHistoryBoundary` - `SessionCompactionPosition` - `SessionCompactionsApi` - `SessionCompactionsPage` - `SessionCompactionsService` - `SessionCompactionState` - `SessionConfigurationConflict` - `SessionConflict` - `SessionConversationApi` - `SessionConversationPage` - `SessionConversationService` - `SessionDeletionApi` - `SessionDeletionReceipt` - `SessionDeletionService` - `SessionExecutionApi` - `SessionExecutionService` - `SessionHistoryBoundary` - `SessionLifecycleApi` - `SessionLifecycleService` - `SessionMcpCredentialAuthorization` - `SessionMcpCredentialAuthorizationRequiredReason` - `SessionMcpCredentialCallbackDeniedReason` - `SessionMcpCredentialError` - `sessionMcpCredentialFailure` - `SessionMcpCredentialForgetResult` - `SessionMcpCredentialRevocationStatus` - `SessionMcpCredentialRuntime` - `SessionMcpCredentialRuntimeInput` - `SessionMcpCredentialServerBinding` - `SessionMcpCredentialStatus` - `SessionMcpError` - `SessionMcpRuntime` - `SessionMcpSurfaceApi` - `SessionMcpSurfaceService` - `SessionMutationGate` - `SessionMutationLease` - `SessionNotFound` - `SessionObservationApi` - `SessionObservationService` - `SessionOverlayRevision` - `SessionPromptsApi` - `SessionPromptsService` - `SessionRuntime` - `SessionRuntimeError` - `SessionServiceClosed` - `SessionServices` - `SessionServicesInput` - `SessionTurnRef` - `SessionTurnsPage` - `SessionTurnState` - `StartSessionMcpCredentialCommand` - `SteerConflict` - `SteeringModelBoundaryInput` - `SteeringModelBoundaryResult` - `SteeringTerminalBoundaryResult` - `SteerRejected` - `Storage` - `StorageError` - `StorageFailure` - `StorageService` - `SubmitContextCompactionCommand` - `SubmitMcpPromptCommand` - `SubmitPromptCommand` - `TerminalCommit` - `TerminalProjection` - `TerminalToolOutputArtifact` - `ToolCallCompletion` - `ToolCallNotFound` - `ToolCallRequest` - `ToolCallSettlement` - `ToolDispatchError` - `ToolDispatchLease` - `ToolDispatchLifecycle` - `ToolDispatchOutcome` - `ToolPermissionCallbackResolution` - `Turn` - `TurnEventsPage` - `TurnNotFound` - `VERSION` - `WatchServerActivityInput` - `WatchSessionActivityInput` - `WebFetch` - `WebFetchError` - `WebFetchFormat` - `withSessionMutation` - `withSessionMutationExclusive` - `WorkspaceDirectoryEntry` - `WorkspaceFileError` - `WorkspaceFiles` - `WorkspaceFileTarget` - `WorkspaceShell` - `WorkspaceShellError` --- Generated from [`packages/harness/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/harness/README.md). --- # Ze Harness API Typed HTTP API for durable agent Sessions and Turns. 66 operations. The full OpenAPI 3.1.0 document is at [/openapi.json](https://github.com/openapi.json). Authentication: operations marked with security use `bearer` (http Bearer). ## System ### `GET /v1/health` **Health check.** Reports whether the HTTP service is available. Operation ID: `health` Response `200` (`application/json`): ```ts { status: "ok" } ``` Errors: `400` ValidationErrorEncoded, `403` ForbiddenErrorEncoded, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/capabilities` **Host capabilities.** Reports the feature flags this host supports. Operation ID: `capabilities` Response `200` (`application/json`): ```ts { backgroundSubagents: boolean; protocolVersion?: number; harnessVersion?: string; } ``` Errors: `400` ValidationErrorEncoded, `403` ForbiddenErrorEncoded, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/logs` **Read daemon logs.** Reads a bounded, newest-first page of structured daemon log entries. Hosts without a log store report unavailable. Operation ID: `logs` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `limit` | query | `string` | no | | `level` | query | `"debug" \| "info" \| "warn" \| "error"` | no | Response `200` (`application/json`): ```ts { entries: { timestamp: string; level: "debug" | "info" | "warn" | "error"; run: string; service: string; event: string; fields: Record; }[]; truncated: boolean; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ## Sessions Durable Session and Turn operations. ### `GET /v1/servers/{server}/sessions/{sessionId}` **Get a Session.** Returns current Session metadata. Operation ID: `getSession` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { session: { id: string; // uuid defaultModel?: string; systemPrompt?: string; title?: string; // max length 100 workdir: string; // max length 1024 projectId: string; // max length 128 forkedFrom?: { parentSessionId: string; // uuid turnId?: string; // uuid through?: | { kind: "turnMessage"; turnId: string /* uuid */; messageIndex: number } | { kind: "conversationEntry"; entryId: string /* uuid */ }; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; spawnedFrom?: { parentSessionId: string /* uuid */ }; agent?: string; // max length 128 configuration: { revision: number; // min 1 permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; approval: { timeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; }; createdAt: number; updatedAt: number; nextSequence: number; // min 1 contextUse?: { model: string; usedUnits: number; contextUnits: number }; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `PUT /v1/servers/{server}/sessions/{sessionId}` **Create a Session.** Creates a Session with caller-selected identity. Operation ID: `createSession` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts { defaultModel?: string; systemPrompt?: string; workdir?: string; // max length 1024 clientWorkdir?: string; // max length 1024 permissions?: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; approval?: { timeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; } ``` Response `200` (`application/json`): ```ts { session: { id: string; // uuid defaultModel?: string; systemPrompt?: string; title?: string; // max length 100 workdir: string; // max length 1024 projectId: string; // max length 128 forkedFrom?: { parentSessionId: string; // uuid turnId?: string; // uuid through?: | { kind: "turnMessage"; turnId: string /* uuid */; messageIndex: number } | { kind: "conversationEntry"; entryId: string /* uuid */ }; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; spawnedFrom?: { parentSessionId: string /* uuid */ }; agent?: string; // max length 128 configuration: { revision: number; // min 1 permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; approval: { timeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; }; createdAt: number; updatedAt: number; nextSequence: number; // min 1 contextUse?: { model: string; usedUnits: number; contextUnits: number }; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `DELETE /v1/servers/{server}/sessions/{sessionId}` **Delete a Session.** Cancels the Session work, finalizes its private state, and deletes its public history. The exact replay is idempotent and the deleted identity stays retired. Operation ID: `deleteSession` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts null | { reason?: string } ``` Response `200` (`application/json`): ```ts { deleted: { sessionId: string /* uuid */ } } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `PUT /v1/servers/{server}/sessions/{sessionId}/title` **Set a Session title.** Sets the Session display title. The same title is a no-op. Operation ID: `setSessionTitle` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts { title: string /* max length 100 */ } ``` Response `200` (`application/json`): ```ts { session: { id: string; // uuid defaultModel?: string; systemPrompt?: string; title?: string; // max length 100 workdir: string; // max length 1024 projectId: string; // max length 128 forkedFrom?: { parentSessionId: string; // uuid turnId?: string; // uuid through?: | { kind: "turnMessage"; turnId: string /* uuid */; messageIndex: number } | { kind: "conversationEntry"; entryId: string /* uuid */ }; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; spawnedFrom?: { parentSessionId: string /* uuid */ }; agent?: string; // max length 128 configuration: { revision: number; // min 1 permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; approval: { timeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; }; createdAt: number; updatedAt: number; nextSequence: number; // min 1 contextUse?: { model: string; usedUnits: number; contextUnits: number }; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `PUT /v1/servers/{server}/sessions/{sessionId}/default-model` **Set a Session default model.** Sets the Session default model for later Turns. The same model is a no-op. Operation ID: `setSessionDefaultModel` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts { defaultModel: string } ``` Response `200` (`application/json`): ```ts { session: { id: string; // uuid defaultModel?: string; systemPrompt?: string; title?: string; // max length 100 workdir: string; // max length 1024 projectId: string; // max length 128 forkedFrom?: { parentSessionId: string; // uuid turnId?: string; // uuid through?: | { kind: "turnMessage"; turnId: string /* uuid */; messageIndex: number } | { kind: "conversationEntry"; entryId: string /* uuid */ }; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; spawnedFrom?: { parentSessionId: string /* uuid */ }; agent?: string; // max length 128 configuration: { revision: number; // min 1 permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; approval: { timeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; }; createdAt: number; updatedAt: number; nextSequence: number; // min 1 contextUse?: { model: string; usedUnits: number; contextUnits: number }; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions` **List Sessions.** Returns one server’s Sessions, oldest first, optionally filtered by project. Operation ID: `listSessions` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `projectId` | query | `string` | no | Response `200` (`application/json`): ```ts { sessions: { id: string; // uuid defaultModel?: string; systemPrompt?: string; title?: string; // max length 100 workdir: string; // max length 1024 projectId: string; // max length 128 forkedFrom?: { parentSessionId: string; // uuid turnId?: string; // uuid through?: | { kind: "turnMessage"; turnId: string /* uuid */; messageIndex: number } | { kind: "conversationEntry"; entryId: string /* uuid */ }; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; spawnedFrom?: { parentSessionId: string /* uuid */ }; agent?: string; // max length 128 configuration: { revision: number; // min 1 permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; approval: { timeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; }; createdAt: number; updatedAt: number; nextSequence: number; // min 1 contextUse?: { model: string; usedUnits: number; contextUnits: number }; }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/events` **Watch a server.** Streams every Session creation, title, deletion, and Turn update on one server, without the live text deltas. Without afterServerCursor the stream starts live from the current cursor and announces it with its first server.cursor; with one it replays every stamped row after that cursor. A caught-up stream repeats its current server.cursor at least every 15 seconds, so a longer silence means the connection is gone. Operation ID: `watchServer` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `afterServerCursor` | query | `string` | no | Response `200`: no body. Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `DELETE /v1/servers/{server}` **Delete a server.** Cancels and deletes Sessions on one server, optionally filtered by project. Workspace files stay. Operation ID: `deleteServer` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `projectId` | query | `string` | no | Response `200` (`application/json`): ```ts { deleted: { server: string /* max length 128 */; sessionIds: string[] } } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/children` **List Session children.** Returns the Sessions spawned directly from one parent, oldest first. Operation ID: `listSessionChildren` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { sessions: { id: string; // uuid defaultModel?: string; systemPrompt?: string; title?: string; // max length 100 workdir: string; // max length 1024 projectId: string; // max length 128 forkedFrom?: { parentSessionId: string; // uuid turnId?: string; // uuid through?: | { kind: "turnMessage"; turnId: string /* uuid */; messageIndex: number } | { kind: "conversationEntry"; entryId: string /* uuid */ }; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; spawnedFrom?: { parentSessionId: string /* uuid */ }; agent?: string; // max length 128 configuration: { revision: number; // min 1 permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; approval: { timeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; }; createdAt: number; updatedAt: number; nextSequence: number; // min 1 contextUse?: { model: string; usedUnits: number; contextUnits: number }; }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/background` **Detach Session sub-agents.** Detaches running foreground sub-agent chains to the background. Their results arrive as queued turns. Operation ID: `promoteSessionBackground` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { promoted: boolean } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/configuration` **Get Session configuration.** Returns the versioned Session permission and approval configuration. Operation ID: `getSessionConfiguration` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { configuration: { revision: number; // min 1 permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; approval: { timeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `PUT /v1/servers/{server}/sessions/{sessionId}/configuration` **Replace Session configuration.** Replaces the ordered Session rules when the expected revision is current. Operation ID: `replaceSessionConfiguration` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts { expectedRevision: number; // min 1 permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; approval: { timeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; } ``` Response `200` (`application/json`): ```ts { configuration: { revision: number; // min 1 permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; approval: { timeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/permissions/saved` **List saved permissions.** Returns durable Session-scoped allow grants. Operation ID: `listSavedPermissions` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { permissions: { id: string; // uuid sessionId: string; // uuid action: string; resource: string; createdAt: number; sourcePermissionRequestId: string; // uuid }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `DELETE /v1/servers/{server}/sessions/{sessionId}/permissions/saved/{savedPermissionId}` **Remove saved permission.** Removes one durable Session-scoped allow grant. Operation ID: `removeSavedPermission` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `savedPermissionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { permission: { id: string; // uuid sessionId: string; // uuid action: string; resource: string; createdAt: number; sourcePermissionRequestId: string; // uuid }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/forks/{forkedSessionId}` **Fork a Session.** Copies settled history before an optional Turn into a new Session with caller-selected identity. Operation ID: `forkSession` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `forkedSessionId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts | null | { turnId?: string; // uuid through?: | { kind: "turnMessage"; turnId: string /* uuid */; messageIndex: number } | { kind: "conversationEntry"; entryId: string /* uuid */ }; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; } ``` Response `200` (`application/json`): ```ts { session: { id: string; // uuid defaultModel?: string; systemPrompt?: string; title?: string; // max length 100 workdir: string; // max length 1024 projectId: string; // max length 128 forkedFrom?: { parentSessionId: string; // uuid turnId?: string; // uuid through?: | { kind: "turnMessage"; turnId: string /* uuid */; messageIndex: number } | { kind: "conversationEntry"; entryId: string /* uuid */ }; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; spawnedFrom?: { parentSessionId: string /* uuid */ }; agent?: string; // max length 128 configuration: { revision: number; // min 1 permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; approval: { timeoutMs?: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; }; createdAt: number; updatedAt: number; nextSequence: number; // min 1 contextUse?: { model: string; usedUnits: number; contextUnits: number }; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/skills` **List Session Skills.** Returns safe metadata for the effective host and Session Skill catalog. Operation ID: `sessionSkills` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { sessionId: string; // uuid skills: { id: string; // max length 128 displayName: string; description: string; advertised: boolean; userInvocable: boolean; origin: "host" | "session"; }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/turns` **List Session Turns.** Reads a bounded, stable Session history page. Operation ID: `listSessionTurns` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `limit` | query | `string` | no | | `afterSequence` | query | `string` | no | | `throughSequence` | query | `string` | no | | `snapshotActivityCursor` | query | `string` | no | Response `200` (`application/json`): ```ts { sessionId: string; // uuid boundary: { throughSequence: number; activityCursor: number }; turns: { activityCursor: number; workSequence: number; // min 1 turn: { id: string; // uuid sessionId: string; // uuid sequence: number; // min 1 request: { sessionId: string; // uuid turnId: string; // uuid // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; conversationEntry?: { entryId: string /* uuid */; historyRevision: number /* min 1 */ }; model?: string; agent?: string; // max length 128 skills?: string[]; // at least 1 reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; attribution?: { clientId: string; // max length 256 authorId?: string; // max length 256 }; model: string; modelLeaseId: string; // max length 68 requestOptionsId: string; // uuid policy: { version: 4; permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; permissionMatching: { caseSensitive: boolean }; approval: { timeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; timeouts: { turnMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; steering: { maxBatches: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; subagents: { maxDepth: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; provider: { maxAttempts: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; attemptTimeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; elapsedMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; pacing: | { kind: "none" } | { kind: "fixed"; delayMs: number } | { kind: "exponential"; initialDelayMs: number; // min 1 maximumDelayMs: number; // min 1 jitter: boolean; }; }; context: { safetyReserve: { units: number; origin: "configured" | "platform" } }; images: { maxBase64Bytes: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; }; steering: { gate: "open" | "closed"; promotedBatchCount: number; steers: { sessionId: string; // uuid turnId: string; // uuid steerId: string; // uuid sequence: number; // min 1 // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; acceptedAt: number; state: | { state: "accepted" } | { state: "promoted"; batch: number /* min 1 */; promotedAt: number } | { state: "undelivered"; reason: "process_lost" | "turn_cancelled" | "turn_failed"; undeliveredAt: number; }; }[]; }; phase: "pending" | "running" | "completed" | "cancelled" | "failed"; revision: number; parts: Array< | { type: "text"; id: string; // uuid assistantMessageIndex: number; text: string; } | { type: "refusal"; id: string; // uuid assistantMessageIndex: number; text: string; } | { type: "reasoning"; id: string; // uuid assistantMessageIndex: number; text: string; startedAt?: number; completedAt?: number; } | { type: "tool"; id: string; // uuid toolCallId: string; toolId: string; origin?: | { kind: "model" } | { kind: "codemode"; parentToolCallId: string; ordinal: number; // min 1 ownerPluginId?: string; binding?: { snapshotId: string; bindingId: string; modelMessageId: string }; }; execution?: { result: unknown; logs: string[] }; instructions?: Array< | { kind: "instruction"; id: string; // max length 512 revision: string; // max length 512 order: number; role: "system" | "developer" | "user"; content: string; afterToolCallId?: string; } | { kind: "skill"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 content: string; } | { kind: "plugin"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 } >; assistantMessageIndex: number; modelInput?: unknown; input: unknown; state: "requested" | "completed" | "failed" | "denied"; output?: unknown; fault?: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; canonicalResult?: | { format: "content"; // at least 1 value: Array< | { type: "text"; text: string } | { type: "json"; value: unknown } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } >; } | { format: "text"; value: string } | { format: "json"; value: unknown }; approval: | { state: "none" } | { state: "pending"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; } | { state: "resolved"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; resolvedAt: number; decision: "allow" | "deny"; source: "config" | "saved" | "hook" | "callback" | "remote" | "default" | "timeout" | "terminal"; reply?: "once" | "always" | "reject"; replyMessage?: string; // max length 4096 }; question?: | { state: "pending"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; } | { state: "answered"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; resolvedAt: number; answers: string[][]; // at least 1 } | { state: "dismissed"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; resolvedAt: number; }; requestedAt: number; completedAt?: number; activity?: { checkpoint: number; // min 1 updatedAt: number; message?: string; // max length 1024 preview?: string; observedBytes: number; truncated: boolean; artifact?: { id: string; // uuid path: string; observedBytes: number; state: "writing" | "complete" | "incomplete"; }; childSessionId?: string; // uuid }; artifact?: { id: string; // uuid path: string; observedBytes: number; state: "writing" | "complete" | "incomplete"; }; } | { type: "steering"; id: string; // uuid batch: number; // min 1 promotedAt: number; steers: { steerId: string; // uuid sequence: number; // min 1 // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; acceptedAt: number; }[]; } >; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; contextUse?: { model: string; usedUnits: number; contextUnits: number }; cancellation?: { reason?: string; requestedAt: number }; outcome?: | { kind: "completed"; turnId: string; text: string } | { kind: "cancelled"; turnId: string; reason?: string } | { kind: "failed"; turnId: string; fault: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; } | { kind: "limit_reached"; turnId: string; limit: | { name: "turn_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "approval_wait"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_attempt_timeout"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "steering_batches"; origin: "configured" | "provider" | "platform"; value: number; unit: "batches"; } | { name: "provider_attempts"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "attempts"; } | { name: "model_context"; origin: "configured" | "provider" | "platform"; value: number; unit: "model_units"; estimatedInputUnits: number; outputReserveUnits: number; safetyReserveUnits: number; availableInputUnits: number; estimator: string; // max length 128 }; diagnostic: { code: string /* max length 128 */; message: string /* max length 4096 */ }; }; acceptedAt: number; startedAt?: number; completedAt?: number; }; }[]; nextAfterSequence?: number; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/journal` **List Session plugin journal entries.** Reads durable plugin-reported facts in record order, optionally for one Turn. Operation ID: `listPluginJournal` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `limit` | query | `string` | no | | `turnId` | query | `string (uuid)` | no | | `afterTurnSequence` | query | `string` | no | | `afterSequence` | query | `string` | no | Response `200` (`application/json`): ```ts { sessionId: string; // uuid entries: { sessionId: string; // uuid turnId: string; // uuid sequence: number; // min 1 pluginId: string; kind: string; summary: string; data: unknown; logicalCallOrdinal?: number; // min 1 toolCallId?: string; recordedAt: number; }[]; next?: { turnSequence: number; sequence: number }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/events` **Watch a Session.** Streams observational Session updates after a durable cursor. A caught-up stream repeats its current session.cursor at least every 15 seconds, so a longer silence means the connection is gone. Operation ID: `watchSession` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `afterActivityCursor` | query | `string` | yes | Response `200`: no body. Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/prompts` **Submit a prompt.** Durably admits queued work or steering input. Operation ID: `submitPrompt` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `x-ze-harness-client-id` | header | `string` | yes | Request body (`application/json`): ```ts | { delivery: "queue"; turnId: string; // uuid // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; conversationEntry?: { entryId: string /* uuid */; historyRevision: number /* min 1 */ }; model?: string; agent?: string; // max length 128 skills?: string[]; // at least 1 reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; authorId?: string; // max length 256 providerOptions?: unknown; } | { delivery: "steer"; steerId: string; // uuid expectedTurnId: string; // uuid // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; authorId?: string; // max length 256 } ``` Response `202` (`application/json`): ```ts { receipt: | { delivery: "queue"; sessionId: string; // uuid turnId: string; // uuid modelLeaseId: string; // max length 68 requestOptionsId: string; // uuid sequence: number; // min 1 acceptedAt: number; attribution?: { clientId: string; // max length 256 authorId?: string; // max length 256 }; } | { delivery: "steer"; sessionId: string; // uuid turnId: string; // uuid steerId: string; // uuid sequence: number; // min 1 acceptedAt: number; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/conversation` **Append a conversation entry.** Appends a durable user message for later work without admitting a Turn or calling a model. Operation ID: `appendConversation` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `x-ze-harness-client-id` | header | `string` | yes | Request body (`application/json`): ```ts { entryId: string; // uuid // at least 1 content: Array< | { type: "text"; text: string } | { type: "content-reference"; reference: string /* max length 2048 */ } >; authorId?: string; // max length 256 } ``` Response `200` (`application/json`): ```ts { receipt: { sessionId: string; // uuid entryId: string; // uuid historyRevision: number; // min 1 acceptedAt: number; attribution?: { clientId: string; // max length 256 authorId?: string; // max length 256 }; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/mcp` **Get the MCP catalog.** Returns the effective host and Session MCP catalog without private metadata. Operation ID: `getMcpCatalog` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { catalog: { revision: number; servers: { origin: "host" | "session"; serverId: string; name: string; // max length 128 order: number; protocolVersion: string; capabilities: { tools: { supported: boolean; listChanged: boolean }; resources: { supported: boolean; listChanged: boolean; subscribe: boolean }; prompts: { supported: boolean; listChanged: boolean }; logging: boolean; completions: boolean; }; instructions?: string; catalog: { revision: number; tools: { name: string; title?: string; description?: string; inputSchema: unknown; outputSchema?: unknown; annotations?: { title?: string; readOnlyHint?: boolean; destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean; }; }[]; resources: { uri: string; name: string; title?: string; description?: string; mimeType?: string; annotations?: { audience?: ("user" | "assistant")[]; priority?: number; // max 1 lastModified?: string; }; size?: number; }[]; resourceTemplates: { uriTemplate: string; name: string; title?: string; description?: string; mimeType?: string; annotations?: { audience?: ("user" | "assistant")[]; priority?: number; // max 1 lastModified?: string; }; }[]; prompts: { name: string; title?: string; description?: string; arguments?: { name: string; title?: string; description?: string; required?: boolean }[]; }[]; }; }[]; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/mcp/{serverName}/prompts/{promptName}` **Submit an MCP prompt.** Resolves an MCP prompt and durably admits it through the ordinary prompt path. Operation ID: `submitMcpPrompt` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `serverName` | path | `string` | yes | | `promptName` | path | `string` | yes | | `x-ze-harness-client-id` | header | `string` | yes | Request body (`application/json`): ```ts { turnId: string; // uuid arguments?: Record; model?: string; providerOptions?: unknown; } ``` Response `202` (`application/json`): ```ts { receipt: | { delivery: "queue"; sessionId: string; // uuid turnId: string; // uuid modelLeaseId: string; // max length 68 requestOptionsId: string; // uuid sequence: number; // min 1 acceptedAt: number; attribution?: { clientId: string; // max length 256 authorId?: string; // max length 256 }; } | { delivery: "steer"; sessionId: string; // uuid turnId: string; // uuid steerId: string; // uuid sequence: number; // min 1 acceptedAt: number; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/mcp/{serverName}/resources/read` **Read an MCP resource.** Reads one resource advertised by the effective leased MCP catalog. Operation ID: `readMcpResource` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `serverName` | path | `string` | yes | Request body (`application/json`): ```ts { uri: string } ``` Response `200` (`application/json`): ```ts { resource: { contents: Array< | { uri: string; mimeType?: string; text: string } | { uri: string; mimeType?: string; blob: string } >; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/compactions` **List context compactions.** Reads a bounded, stable page of durable Session compactions. Operation ID: `listSessionCompactions` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `limit` | query | `string` | no | | `afterSequence` | query | `string` | no | | `afterOrdinal` | query | `string` | no | | `throughSequence` | query | `string` | no | | `throughOrdinal` | query | `string` | no | | `snapshotActivityCursor` | query | `string` | no | Response `200` (`application/json`): ```ts { sessionId: string; // uuid boundary: { throughSequence: number; throughOrdinal: number; activityCursor: number }; compactions: { sequence: number; // min 1 ordinal: number; // min 1 compaction: { id: string; // uuid sessionId: string; // uuid sequence: number; // min 1 request: { sessionId: string; // uuid compactionId: string; // uuid model?: string; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; attribution?: { clientId: string; // max length 256 authorId?: string; // max length 256 }; trigger: | { kind: "manual" } | { kind: "preflight"; turnId: string /* uuid */ } | { kind: "overflow"; turnId: string /* uuid */ }; model: string; modelLeaseId: string; // max length 68 requestOptionsId: string; // uuid policy: { version: 4; permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; permissionMatching: { caseSensitive: boolean }; approval: { timeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; timeouts: { turnMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; steering: { maxBatches: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; subagents: { maxDepth: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; provider: { maxAttempts: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; attemptTimeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; elapsedMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; pacing: | { kind: "none" } | { kind: "fixed"; delayMs: number } | { kind: "exponential"; initialDelayMs: number; // min 1 maximumDelayMs: number; // min 1 jitter: boolean; }; }; context: { safetyReserve: { units: number; origin: "configured" | "platform" } }; images: { maxBase64Bytes: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; }; phase: "pending" | "running" | "completed" | "cancelled" | "failed"; revision: number; cancellation?: { reason?: string; requestedAt: number }; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; outcome?: | { kind: "completed"; checkpoint: { id: string; // uuid compactionId: string; // uuid sessionId: string; // uuid throughHistoryRevision: number; // min 1 throughChronologyRevision: number; sourceMessageCount: number; // min 1 summary: { version: 1; overview: string; // max length 262144 keyFacts: string[]; decisions: string[]; openTasks: string[]; }; chronology: { revision: number; // min 1 historyRevision: number; boundaryOrder: number; change: "activated" | "updated" | "removed"; source: | { kind: "instruction"; id: string; // max length 512 revision: string; // max length 512 order: number; role: "system" | "developer" | "user"; content: string; afterToolCallId?: string; } | { kind: "skill"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 content: string; } | { kind: "plugin"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 }; }[]; estimate: { estimator: string; // max length 128 inputUnits: number; contextUnits: number; outputUnits: number; safetyReserveUnits: number; availableInputUnits: number; fits: boolean; }; createdAt: number; }; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; } | { kind: "cancelled"; reason?: string; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; } | { kind: "failed"; fault: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; } | { kind: "no_compressible_history"; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; } | { kind: "limit_reached"; limit: | { name: "turn_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "approval_wait"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_attempt_timeout"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "steering_batches"; origin: "configured" | "provider" | "platform"; value: number; unit: "batches"; } | { name: "provider_attempts"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "attempts"; } | { name: "model_context"; origin: "configured" | "provider" | "platform"; value: number; unit: "model_units"; estimatedInputUnits: number; outputReserveUnits: number; safetyReserveUnits: number; availableInputUnits: number; estimator: string; // max length 128 }; diagnostic: { code: string /* max length 128 */; message: string /* max length 4096 */ }; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; }; acceptedAt: number; startedAt?: number; completedAt?: number; }; }[]; next?: { sequence: number /* min 1 */; ordinal: number /* min 1 */ }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/compactions` **Compact Session context.** Durably admits an ordered manual context compaction. Operation ID: `submitContextCompaction` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `x-ze-harness-client-id` | header | `string` | yes | Request body (`application/json`): ```ts { compactionId: string; // uuid model?: string; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; authorId?: string; // max length 256 providerOptions?: unknown; } ``` Response `202` (`application/json`): ```ts { receipt: { delivery: "compaction"; sessionId: string; // uuid compactionId: string; // uuid modelLeaseId: string; // max length 68 requestOptionsId: string; // uuid sequence: number; // min 1 acceptedAt: number; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/compactions/{compactionId}` **Get context compaction.** Returns authoritative context compaction state. Operation ID: `getContextCompaction` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `compactionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { compaction: { id: string; // uuid sessionId: string; // uuid sequence: number; // min 1 request: { sessionId: string; // uuid compactionId: string; // uuid model?: string; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; attribution?: { clientId: string; // max length 256 authorId?: string; // max length 256 }; trigger: | { kind: "manual" } | { kind: "preflight"; turnId: string /* uuid */ } | { kind: "overflow"; turnId: string /* uuid */ }; model: string; modelLeaseId: string; // max length 68 requestOptionsId: string; // uuid policy: { version: 4; permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; permissionMatching: { caseSensitive: boolean }; approval: { timeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; timeouts: { turnMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; steering: { maxBatches: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; subagents: { maxDepth: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; provider: { maxAttempts: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; attemptTimeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; elapsedMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; pacing: | { kind: "none" } | { kind: "fixed"; delayMs: number } | { kind: "exponential"; initialDelayMs: number; // min 1 maximumDelayMs: number; // min 1 jitter: boolean; }; }; context: { safetyReserve: { units: number; origin: "configured" | "platform" } }; images: { maxBase64Bytes: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; }; phase: "pending" | "running" | "completed" | "cancelled" | "failed"; revision: number; cancellation?: { reason?: string; requestedAt: number }; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; outcome?: | { kind: "completed"; checkpoint: { id: string; // uuid compactionId: string; // uuid sessionId: string; // uuid throughHistoryRevision: number; // min 1 throughChronologyRevision: number; sourceMessageCount: number; // min 1 summary: { version: 1; overview: string; // max length 262144 keyFacts: string[]; decisions: string[]; openTasks: string[]; }; chronology: { revision: number; // min 1 historyRevision: number; boundaryOrder: number; change: "activated" | "updated" | "removed"; source: | { kind: "instruction"; id: string; // max length 512 revision: string; // max length 512 order: number; role: "system" | "developer" | "user"; content: string; afterToolCallId?: string; } | { kind: "skill"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 content: string; } | { kind: "plugin"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 }; }[]; estimate: { estimator: string; // max length 128 inputUnits: number; contextUnits: number; outputUnits: number; safetyReserveUnits: number; availableInputUnits: number; fits: boolean; }; createdAt: number; }; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; } | { kind: "cancelled"; reason?: string; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; } | { kind: "failed"; fault: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; } | { kind: "no_compressible_history"; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; } | { kind: "limit_reached"; limit: | { name: "turn_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "approval_wait"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_attempt_timeout"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "steering_batches"; origin: "configured" | "provider" | "platform"; value: number; unit: "batches"; } | { name: "provider_attempts"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "attempts"; } | { name: "model_context"; origin: "configured" | "provider" | "platform"; value: number; unit: "model_units"; estimatedInputUnits: number; outputReserveUnits: number; safetyReserveUnits: number; availableInputUnits: number; estimator: string; // max length 128 }; diagnostic: { code: string /* max length 128 */; message: string /* max length 4096 */ }; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; }; acceptedAt: number; startedAt?: number; completedAt?: number; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/compactions/{compactionId}/cancel` **Cancel context compaction.** Requests cancellation of pending or running context compaction. Operation ID: `cancelContextCompaction` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `compactionId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts { reason?: string } ``` Response `200` (`application/json`): ```ts { compaction: { id: string; // uuid sessionId: string; // uuid sequence: number; // min 1 request: { sessionId: string; // uuid compactionId: string; // uuid model?: string; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; attribution?: { clientId: string; // max length 256 authorId?: string; // max length 256 }; trigger: | { kind: "manual" } | { kind: "preflight"; turnId: string /* uuid */ } | { kind: "overflow"; turnId: string /* uuid */ }; model: string; modelLeaseId: string; // max length 68 requestOptionsId: string; // uuid policy: { version: 4; permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; permissionMatching: { caseSensitive: boolean }; approval: { timeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; timeouts: { turnMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; steering: { maxBatches: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; subagents: { maxDepth: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; provider: { maxAttempts: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; attemptTimeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; elapsedMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; pacing: | { kind: "none" } | { kind: "fixed"; delayMs: number } | { kind: "exponential"; initialDelayMs: number; // min 1 maximumDelayMs: number; // min 1 jitter: boolean; }; }; context: { safetyReserve: { units: number; origin: "configured" | "platform" } }; images: { maxBase64Bytes: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; }; phase: "pending" | "running" | "completed" | "cancelled" | "failed"; revision: number; cancellation?: { reason?: string; requestedAt: number }; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; outcome?: | { kind: "completed"; checkpoint: { id: string; // uuid compactionId: string; // uuid sessionId: string; // uuid throughHistoryRevision: number; // min 1 throughChronologyRevision: number; sourceMessageCount: number; // min 1 summary: { version: 1; overview: string; // max length 262144 keyFacts: string[]; decisions: string[]; openTasks: string[]; }; chronology: { revision: number; // min 1 historyRevision: number; boundaryOrder: number; change: "activated" | "updated" | "removed"; source: | { kind: "instruction"; id: string; // max length 512 revision: string; // max length 512 order: number; role: "system" | "developer" | "user"; content: string; afterToolCallId?: string; } | { kind: "skill"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 content: string; } | { kind: "plugin"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 }; }[]; estimate: { estimator: string; // max length 128 inputUnits: number; contextUnits: number; outputUnits: number; safetyReserveUnits: number; availableInputUnits: number; fits: boolean; }; createdAt: number; }; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; } | { kind: "cancelled"; reason?: string; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; } | { kind: "failed"; fault: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; } | { kind: "no_compressible_history"; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; } | { kind: "limit_reached"; limit: | { name: "turn_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "approval_wait"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_attempt_timeout"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "steering_batches"; origin: "configured" | "provider" | "platform"; value: number; unit: "batches"; } | { name: "provider_attempts"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "attempts"; } | { name: "model_context"; origin: "configured" | "provider" | "platform"; value: number; unit: "model_units"; estimatedInputUnits: number; outputReserveUnits: number; safetyReserveUnits: number; availableInputUnits: number; estimator: string; // max length 128 }; diagnostic: { code: string /* max length 128 */; message: string /* max length 4096 */ }; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; }; acceptedAt: number; startedAt?: number; completedAt?: number; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/turns/{turnId}` **Get a Turn.** Returns authoritative public Turn state. Operation ID: `getTurn` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `turnId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { turn: { id: string; // uuid sessionId: string; // uuid sequence: number; // min 1 request: { sessionId: string; // uuid turnId: string; // uuid // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; conversationEntry?: { entryId: string /* uuid */; historyRevision: number /* min 1 */ }; model?: string; agent?: string; // max length 128 skills?: string[]; // at least 1 reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; attribution?: { clientId: string; // max length 256 authorId?: string; // max length 256 }; model: string; modelLeaseId: string; // max length 68 requestOptionsId: string; // uuid policy: { version: 4; permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; permissionMatching: { caseSensitive: boolean }; approval: { timeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; timeouts: { turnMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; steering: { maxBatches: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; subagents: { maxDepth: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; provider: { maxAttempts: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; attemptTimeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; elapsedMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; pacing: | { kind: "none" } | { kind: "fixed"; delayMs: number } | { kind: "exponential"; initialDelayMs: number; // min 1 maximumDelayMs: number; // min 1 jitter: boolean; }; }; context: { safetyReserve: { units: number; origin: "configured" | "platform" } }; images: { maxBase64Bytes: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; }; steering: { gate: "open" | "closed"; promotedBatchCount: number; steers: { sessionId: string; // uuid turnId: string; // uuid steerId: string; // uuid sequence: number; // min 1 // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; acceptedAt: number; state: | { state: "accepted" } | { state: "promoted"; batch: number /* min 1 */; promotedAt: number } | { state: "undelivered"; reason: "process_lost" | "turn_cancelled" | "turn_failed"; undeliveredAt: number; }; }[]; }; phase: "pending" | "running" | "completed" | "cancelled" | "failed"; revision: number; parts: Array< | { type: "text"; id: string; // uuid assistantMessageIndex: number; text: string; } | { type: "refusal"; id: string; // uuid assistantMessageIndex: number; text: string; } | { type: "reasoning"; id: string; // uuid assistantMessageIndex: number; text: string; startedAt?: number; completedAt?: number; } | { type: "tool"; id: string; // uuid toolCallId: string; toolId: string; origin?: | { kind: "model" } | { kind: "codemode"; parentToolCallId: string; ordinal: number; // min 1 ownerPluginId?: string; binding?: { snapshotId: string; bindingId: string; modelMessageId: string }; }; execution?: { result: unknown; logs: string[] }; instructions?: Array< | { kind: "instruction"; id: string; // max length 512 revision: string; // max length 512 order: number; role: "system" | "developer" | "user"; content: string; afterToolCallId?: string; } | { kind: "skill"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 content: string; } | { kind: "plugin"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 } >; assistantMessageIndex: number; modelInput?: unknown; input: unknown; state: "requested" | "completed" | "failed" | "denied"; output?: unknown; fault?: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; canonicalResult?: | { format: "content"; // at least 1 value: Array< | { type: "text"; text: string } | { type: "json"; value: unknown } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } >; } | { format: "text"; value: string } | { format: "json"; value: unknown }; approval: | { state: "none" } | { state: "pending"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; } | { state: "resolved"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; resolvedAt: number; decision: "allow" | "deny"; source: "config" | "saved" | "hook" | "callback" | "remote" | "default" | "timeout" | "terminal"; reply?: "once" | "always" | "reject"; replyMessage?: string; // max length 4096 }; question?: | { state: "pending"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; } | { state: "answered"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; resolvedAt: number; answers: string[][]; // at least 1 } | { state: "dismissed"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; resolvedAt: number; }; requestedAt: number; completedAt?: number; activity?: { checkpoint: number; // min 1 updatedAt: number; message?: string; // max length 1024 preview?: string; observedBytes: number; truncated: boolean; artifact?: { id: string; // uuid path: string; observedBytes: number; state: "writing" | "complete" | "incomplete"; }; childSessionId?: string; // uuid }; artifact?: { id: string; // uuid path: string; observedBytes: number; state: "writing" | "complete" | "incomplete"; }; } | { type: "steering"; id: string; // uuid batch: number; // min 1 promotedAt: number; steers: { steerId: string; // uuid sequence: number; // min 1 // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; acceptedAt: number; }[]; } >; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; contextUse?: { model: string; usedUnits: number; contextUnits: number }; cancellation?: { reason?: string; requestedAt: number }; outcome?: | { kind: "completed"; turnId: string; text: string } | { kind: "cancelled"; turnId: string; reason?: string } | { kind: "failed"; turnId: string; fault: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; } | { kind: "limit_reached"; turnId: string; limit: | { name: "turn_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "approval_wait"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_attempt_timeout"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "steering_batches"; origin: "configured" | "provider" | "platform"; value: number; unit: "batches"; } | { name: "provider_attempts"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "attempts"; } | { name: "model_context"; origin: "configured" | "provider" | "platform"; value: number; unit: "model_units"; estimatedInputUnits: number; outputReserveUnits: number; safetyReserveUnits: number; availableInputUnits: number; estimator: string; // max length 128 }; diagnostic: { code: string /* max length 128 */; message: string /* max length 4096 */ }; }; acceptedAt: number; startedAt?: number; completedAt?: number; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/turns/{turnId}/events` **Watch a Turn.** Streams observational Turn snapshots, content deltas, and durable state events after a revision cursor. The stream starts with the current snapshot, then replays the durable events recorded after afterRevision so observers see missed state transitions, then continues with live Turn revisions. While the Turn is quiet, the stream sends turn.heartbeat at least every 15 seconds, so a longer silence means the connection is gone. Operation ID: `watchTurn` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `turnId` | path | `string (uuid)` | yes | | `afterRevision` | query | `string` | no | Response `200`: no body. Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/turns/{turnId}/cancel` **Cancel a Turn.** Requests cancellation of active Turn work. Operation ID: `cancelTurn` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `turnId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts { reason?: string } ``` Response `200` (`application/json`): ```ts { turn: { id: string; // uuid sessionId: string; // uuid sequence: number; // min 1 request: { sessionId: string; // uuid turnId: string; // uuid // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; conversationEntry?: { entryId: string /* uuid */; historyRevision: number /* min 1 */ }; model?: string; agent?: string; // max length 128 skills?: string[]; // at least 1 reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; attribution?: { clientId: string; // max length 256 authorId?: string; // max length 256 }; model: string; modelLeaseId: string; // max length 68 requestOptionsId: string; // uuid policy: { version: 4; permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; permissionMatching: { caseSensitive: boolean }; approval: { timeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; timeouts: { turnMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; steering: { maxBatches: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; subagents: { maxDepth: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; provider: { maxAttempts: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; attemptTimeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; elapsedMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; pacing: | { kind: "none" } | { kind: "fixed"; delayMs: number } | { kind: "exponential"; initialDelayMs: number; // min 1 maximumDelayMs: number; // min 1 jitter: boolean; }; }; context: { safetyReserve: { units: number; origin: "configured" | "platform" } }; images: { maxBase64Bytes: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; }; steering: { gate: "open" | "closed"; promotedBatchCount: number; steers: { sessionId: string; // uuid turnId: string; // uuid steerId: string; // uuid sequence: number; // min 1 // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; acceptedAt: number; state: | { state: "accepted" } | { state: "promoted"; batch: number /* min 1 */; promotedAt: number } | { state: "undelivered"; reason: "process_lost" | "turn_cancelled" | "turn_failed"; undeliveredAt: number; }; }[]; }; phase: "pending" | "running" | "completed" | "cancelled" | "failed"; revision: number; parts: Array< | { type: "text"; id: string; // uuid assistantMessageIndex: number; text: string; } | { type: "refusal"; id: string; // uuid assistantMessageIndex: number; text: string; } | { type: "reasoning"; id: string; // uuid assistantMessageIndex: number; text: string; startedAt?: number; completedAt?: number; } | { type: "tool"; id: string; // uuid toolCallId: string; toolId: string; origin?: | { kind: "model" } | { kind: "codemode"; parentToolCallId: string; ordinal: number; // min 1 ownerPluginId?: string; binding?: { snapshotId: string; bindingId: string; modelMessageId: string }; }; execution?: { result: unknown; logs: string[] }; instructions?: Array< | { kind: "instruction"; id: string; // max length 512 revision: string; // max length 512 order: number; role: "system" | "developer" | "user"; content: string; afterToolCallId?: string; } | { kind: "skill"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 content: string; } | { kind: "plugin"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 } >; assistantMessageIndex: number; modelInput?: unknown; input: unknown; state: "requested" | "completed" | "failed" | "denied"; output?: unknown; fault?: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; canonicalResult?: | { format: "content"; // at least 1 value: Array< | { type: "text"; text: string } | { type: "json"; value: unknown } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } >; } | { format: "text"; value: string } | { format: "json"; value: unknown }; approval: | { state: "none" } | { state: "pending"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; } | { state: "resolved"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; resolvedAt: number; decision: "allow" | "deny"; source: "config" | "saved" | "hook" | "callback" | "remote" | "default" | "timeout" | "terminal"; reply?: "once" | "always" | "reject"; replyMessage?: string; // max length 4096 }; question?: | { state: "pending"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; } | { state: "answered"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; resolvedAt: number; answers: string[][]; // at least 1 } | { state: "dismissed"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; resolvedAt: number; }; requestedAt: number; completedAt?: number; activity?: { checkpoint: number; // min 1 updatedAt: number; message?: string; // max length 1024 preview?: string; observedBytes: number; truncated: boolean; artifact?: { id: string; // uuid path: string; observedBytes: number; state: "writing" | "complete" | "incomplete"; }; childSessionId?: string; // uuid }; artifact?: { id: string; // uuid path: string; observedBytes: number; state: "writing" | "complete" | "incomplete"; }; } | { type: "steering"; id: string; // uuid batch: number; // min 1 promotedAt: number; steers: { steerId: string; // uuid sequence: number; // min 1 // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; acceptedAt: number; }[]; } >; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; contextUse?: { model: string; usedUnits: number; contextUnits: number }; cancellation?: { reason?: string; requestedAt: number }; outcome?: | { kind: "completed"; turnId: string; text: string } | { kind: "cancelled"; turnId: string; reason?: string } | { kind: "failed"; turnId: string; fault: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; } | { kind: "limit_reached"; turnId: string; limit: | { name: "turn_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "approval_wait"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_attempt_timeout"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "steering_batches"; origin: "configured" | "provider" | "platform"; value: number; unit: "batches"; } | { name: "provider_attempts"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "attempts"; } | { name: "model_context"; origin: "configured" | "provider" | "platform"; value: number; unit: "model_units"; estimatedInputUnits: number; outputReserveUnits: number; safetyReserveUnits: number; availableInputUnits: number; estimator: string; // max length 128 }; diagnostic: { code: string /* max length 128 */; message: string /* max length 4096 */ }; }; acceptedAt: number; startedAt?: number; completedAt?: number; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/permissions/{permissionRequestId}/reply` **Reply to permission request.** Records once, always, or reject for a durable pending permission request. A reply addressed to an ancestor Session is forwarded to the descendant sub-agent Session that owns the request, and the returned Turn is the owning Turn. Operation ID: `replyPermission` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `permissionRequestId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts { reply: "once" | "always" | "reject"; message?: string /* max length 4096 */ } ``` Response `200` (`application/json`): ```ts { requestId: string; // uuid turn: { id: string; // uuid sessionId: string; // uuid sequence: number; // min 1 request: { sessionId: string; // uuid turnId: string; // uuid // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; conversationEntry?: { entryId: string /* uuid */; historyRevision: number /* min 1 */ }; model?: string; agent?: string; // max length 128 skills?: string[]; // at least 1 reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; attribution?: { clientId: string; // max length 256 authorId?: string; // max length 256 }; model: string; modelLeaseId: string; // max length 68 requestOptionsId: string; // uuid policy: { version: 4; permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; permissionMatching: { caseSensitive: boolean }; approval: { timeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; timeouts: { turnMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; steering: { maxBatches: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; subagents: { maxDepth: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; provider: { maxAttempts: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; attemptTimeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; elapsedMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; pacing: | { kind: "none" } | { kind: "fixed"; delayMs: number } | { kind: "exponential"; initialDelayMs: number; // min 1 maximumDelayMs: number; // min 1 jitter: boolean; }; }; context: { safetyReserve: { units: number; origin: "configured" | "platform" } }; images: { maxBase64Bytes: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; }; steering: { gate: "open" | "closed"; promotedBatchCount: number; steers: { sessionId: string; // uuid turnId: string; // uuid steerId: string; // uuid sequence: number; // min 1 // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; acceptedAt: number; state: | { state: "accepted" } | { state: "promoted"; batch: number /* min 1 */; promotedAt: number } | { state: "undelivered"; reason: "process_lost" | "turn_cancelled" | "turn_failed"; undeliveredAt: number; }; }[]; }; phase: "pending" | "running" | "completed" | "cancelled" | "failed"; revision: number; parts: Array< | { type: "text"; id: string; // uuid assistantMessageIndex: number; text: string; } | { type: "refusal"; id: string; // uuid assistantMessageIndex: number; text: string; } | { type: "reasoning"; id: string; // uuid assistantMessageIndex: number; text: string; startedAt?: number; completedAt?: number; } | { type: "tool"; id: string; // uuid toolCallId: string; toolId: string; origin?: | { kind: "model" } | { kind: "codemode"; parentToolCallId: string; ordinal: number; // min 1 ownerPluginId?: string; binding?: { snapshotId: string; bindingId: string; modelMessageId: string }; }; execution?: { result: unknown; logs: string[] }; instructions?: Array< | { kind: "instruction"; id: string; // max length 512 revision: string; // max length 512 order: number; role: "system" | "developer" | "user"; content: string; afterToolCallId?: string; } | { kind: "skill"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 content: string; } | { kind: "plugin"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 } >; assistantMessageIndex: number; modelInput?: unknown; input: unknown; state: "requested" | "completed" | "failed" | "denied"; output?: unknown; fault?: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; canonicalResult?: | { format: "content"; // at least 1 value: Array< | { type: "text"; text: string } | { type: "json"; value: unknown } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } >; } | { format: "text"; value: string } | { format: "json"; value: unknown }; approval: | { state: "none" } | { state: "pending"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; } | { state: "resolved"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; resolvedAt: number; decision: "allow" | "deny"; source: "config" | "saved" | "hook" | "callback" | "remote" | "default" | "timeout" | "terminal"; reply?: "once" | "always" | "reject"; replyMessage?: string; // max length 4096 }; question?: | { state: "pending"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; } | { state: "answered"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; resolvedAt: number; answers: string[][]; // at least 1 } | { state: "dismissed"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; resolvedAt: number; }; requestedAt: number; completedAt?: number; activity?: { checkpoint: number; // min 1 updatedAt: number; message?: string; // max length 1024 preview?: string; observedBytes: number; truncated: boolean; artifact?: { id: string; // uuid path: string; observedBytes: number; state: "writing" | "complete" | "incomplete"; }; childSessionId?: string; // uuid }; artifact?: { id: string; // uuid path: string; observedBytes: number; state: "writing" | "complete" | "incomplete"; }; } | { type: "steering"; id: string; // uuid batch: number; // min 1 promotedAt: number; steers: { steerId: string; // uuid sequence: number; // min 1 // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; acceptedAt: number; }[]; } >; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; contextUse?: { model: string; usedUnits: number; contextUnits: number }; cancellation?: { reason?: string; requestedAt: number }; outcome?: | { kind: "completed"; turnId: string; text: string } | { kind: "cancelled"; turnId: string; reason?: string } | { kind: "failed"; turnId: string; fault: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; } | { kind: "limit_reached"; turnId: string; limit: | { name: "turn_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "approval_wait"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_attempt_timeout"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "steering_batches"; origin: "configured" | "provider" | "platform"; value: number; unit: "batches"; } | { name: "provider_attempts"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "attempts"; } | { name: "model_context"; origin: "configured" | "provider" | "platform"; value: number; unit: "model_units"; estimatedInputUnits: number; outputReserveUnits: number; safetyReserveUnits: number; availableInputUnits: number; estimator: string; // max length 128 }; diagnostic: { code: string /* max length 128 */; message: string /* max length 4096 */ }; }; acceptedAt: number; startedAt?: number; completedAt?: number; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/permissions/descendants/pending` **List descendant approvals.** Returns the pending permission requests owned by descendant sub-agent Sessions, oldest first. Operation ID: `listDescendantPendingApprovals` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { approvals: { request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; agent?: string; // max length 128 }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/questions/{questionRequestId}/reply` **Reply to question request.** Records answers for a durable pending user question. A reply addressed to an ancestor Session is forwarded to the descendant sub-agent Session that owns the request, and the returned Turn is the owning Turn. Operation ID: `replyQuestion` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `questionRequestId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts { answers: string[][] /* at least 1 */ } ``` Response `200` (`application/json`): ```ts { requestId: string; // uuid turn: { id: string; // uuid sessionId: string; // uuid sequence: number; // min 1 request: { sessionId: string; // uuid turnId: string; // uuid // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; conversationEntry?: { entryId: string /* uuid */; historyRevision: number /* min 1 */ }; model?: string; agent?: string; // max length 128 skills?: string[]; // at least 1 reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; attribution?: { clientId: string; // max length 256 authorId?: string; // max length 256 }; model: string; modelLeaseId: string; // max length 68 requestOptionsId: string; // uuid policy: { version: 4; permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; permissionMatching: { caseSensitive: boolean }; approval: { timeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; timeouts: { turnMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; steering: { maxBatches: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; subagents: { maxDepth: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; provider: { maxAttempts: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; attemptTimeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; elapsedMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; pacing: | { kind: "none" } | { kind: "fixed"; delayMs: number } | { kind: "exponential"; initialDelayMs: number; // min 1 maximumDelayMs: number; // min 1 jitter: boolean; }; }; context: { safetyReserve: { units: number; origin: "configured" | "platform" } }; images: { maxBase64Bytes: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; }; steering: { gate: "open" | "closed"; promotedBatchCount: number; steers: { sessionId: string; // uuid turnId: string; // uuid steerId: string; // uuid sequence: number; // min 1 // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; acceptedAt: number; state: | { state: "accepted" } | { state: "promoted"; batch: number /* min 1 */; promotedAt: number } | { state: "undelivered"; reason: "process_lost" | "turn_cancelled" | "turn_failed"; undeliveredAt: number; }; }[]; }; phase: "pending" | "running" | "completed" | "cancelled" | "failed"; revision: number; parts: Array< | { type: "text"; id: string; // uuid assistantMessageIndex: number; text: string; } | { type: "refusal"; id: string; // uuid assistantMessageIndex: number; text: string; } | { type: "reasoning"; id: string; // uuid assistantMessageIndex: number; text: string; startedAt?: number; completedAt?: number; } | { type: "tool"; id: string; // uuid toolCallId: string; toolId: string; origin?: | { kind: "model" } | { kind: "codemode"; parentToolCallId: string; ordinal: number; // min 1 ownerPluginId?: string; binding?: { snapshotId: string; bindingId: string; modelMessageId: string }; }; execution?: { result: unknown; logs: string[] }; instructions?: Array< | { kind: "instruction"; id: string; // max length 512 revision: string; // max length 512 order: number; role: "system" | "developer" | "user"; content: string; afterToolCallId?: string; } | { kind: "skill"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 content: string; } | { kind: "plugin"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 } >; assistantMessageIndex: number; modelInput?: unknown; input: unknown; state: "requested" | "completed" | "failed" | "denied"; output?: unknown; fault?: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; canonicalResult?: | { format: "content"; // at least 1 value: Array< | { type: "text"; text: string } | { type: "json"; value: unknown } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } >; } | { format: "text"; value: string } | { format: "json"; value: unknown }; approval: | { state: "none" } | { state: "pending"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; } | { state: "resolved"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; resolvedAt: number; decision: "allow" | "deny"; source: "config" | "saved" | "hook" | "callback" | "remote" | "default" | "timeout" | "terminal"; reply?: "once" | "always" | "reject"; replyMessage?: string; // max length 4096 }; question?: | { state: "pending"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; } | { state: "answered"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; resolvedAt: number; answers: string[][]; // at least 1 } | { state: "dismissed"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; resolvedAt: number; }; requestedAt: number; completedAt?: number; activity?: { checkpoint: number; // min 1 updatedAt: number; message?: string; // max length 1024 preview?: string; observedBytes: number; truncated: boolean; artifact?: { id: string; // uuid path: string; observedBytes: number; state: "writing" | "complete" | "incomplete"; }; childSessionId?: string; // uuid }; artifact?: { id: string; // uuid path: string; observedBytes: number; state: "writing" | "complete" | "incomplete"; }; } | { type: "steering"; id: string; // uuid batch: number; // min 1 promotedAt: number; steers: { steerId: string; // uuid sequence: number; // min 1 // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; acceptedAt: number; }[]; } >; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; contextUse?: { model: string; usedUnits: number; contextUnits: number }; cancellation?: { reason?: string; requestedAt: number }; outcome?: | { kind: "completed"; turnId: string; text: string } | { kind: "cancelled"; turnId: string; reason?: string } | { kind: "failed"; turnId: string; fault: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; } | { kind: "limit_reached"; turnId: string; limit: | { name: "turn_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "approval_wait"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_attempt_timeout"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "steering_batches"; origin: "configured" | "provider" | "platform"; value: number; unit: "batches"; } | { name: "provider_attempts"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "attempts"; } | { name: "model_context"; origin: "configured" | "provider" | "platform"; value: number; unit: "model_units"; estimatedInputUnits: number; outputReserveUnits: number; safetyReserveUnits: number; availableInputUnits: number; estimator: string; // max length 128 }; diagnostic: { code: string /* max length 128 */; message: string /* max length 4096 */ }; }; acceptedAt: number; startedAt?: number; completedAt?: number; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/questions/{questionRequestId}/reject` **Reject question request.** Dismisses a durable pending user question; the waiting tool fails with question_dismissed. Forwarding matches the question reply. Operation ID: `rejectQuestion` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `questionRequestId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { requestId: string; // uuid turn: { id: string; // uuid sessionId: string; // uuid sequence: number; // min 1 request: { sessionId: string; // uuid turnId: string; // uuid // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; conversationEntry?: { entryId: string /* uuid */; historyRevision: number /* min 1 */ }; model?: string; agent?: string; // max length 128 skills?: string[]; // at least 1 reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; visibility?: { floorHistoryRevision?: number; excludedTurnIds?: string[]; excludedEntryIds?: string[]; }; }; attribution?: { clientId: string; // max length 256 authorId?: string; // max length 256 }; model: string; modelLeaseId: string; // max length 68 requestOptionsId: string; // uuid policy: { version: 4; permissions: { action: string; resource: string; effect: "allow" | "ask" | "deny" }[]; permissionMatching: { caseSensitive: boolean }; approval: { timeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; timeouts: { turnMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; steering: { maxBatches: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; subagents: { maxDepth: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; origin: "configured" | "provider" | "platform"; }; }; provider: { maxAttempts: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; attemptTimeoutMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; elapsedMs: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; pacing: | { kind: "none" } | { kind: "fixed"; delayMs: number } | { kind: "exponential"; initialDelayMs: number; // min 1 maximumDelayMs: number; // min 1 jitter: boolean; }; }; context: { safetyReserve: { units: number; origin: "configured" | "platform" } }; images: { maxBase64Bytes: | { kind: "unlimited"; origin: "absent" | "configured" | "provider" | "platform" } | { kind: "finite"; value: number; // min 1 origin: "configured" | "provider" | "platform"; }; }; }; steering: { gate: "open" | "closed"; promotedBatchCount: number; steers: { sessionId: string; // uuid turnId: string; // uuid steerId: string; // uuid sequence: number; // min 1 // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; acceptedAt: number; state: | { state: "accepted" } | { state: "promoted"; batch: number /* min 1 */; promotedAt: number } | { state: "undelivered"; reason: "process_lost" | "turn_cancelled" | "turn_failed"; undeliveredAt: number; }; }[]; }; phase: "pending" | "running" | "completed" | "cancelled" | "failed"; revision: number; parts: Array< | { type: "text"; id: string; // uuid assistantMessageIndex: number; text: string; } | { type: "refusal"; id: string; // uuid assistantMessageIndex: number; text: string; } | { type: "reasoning"; id: string; // uuid assistantMessageIndex: number; text: string; startedAt?: number; completedAt?: number; } | { type: "tool"; id: string; // uuid toolCallId: string; toolId: string; origin?: | { kind: "model" } | { kind: "codemode"; parentToolCallId: string; ordinal: number; // min 1 ownerPluginId?: string; binding?: { snapshotId: string; bindingId: string; modelMessageId: string }; }; execution?: { result: unknown; logs: string[] }; instructions?: Array< | { kind: "instruction"; id: string; // max length 512 revision: string; // max length 512 order: number; role: "system" | "developer" | "user"; content: string; afterToolCallId?: string; } | { kind: "skill"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 content: string; } | { kind: "plugin"; id: string; // max length 512 revision: string; // max length 512 order: number; reference: string; // max length 2048 } >; assistantMessageIndex: number; modelInput?: unknown; input: unknown; state: "requested" | "completed" | "failed" | "denied"; output?: unknown; fault?: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; canonicalResult?: | { format: "content"; // at least 1 value: Array< | { type: "text"; text: string } | { type: "json"; value: unknown } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } >; } | { format: "text"; value: string } | { format: "json"; value: unknown }; approval: | { state: "none" } | { state: "pending"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; } | { state: "resolved"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; action: string; resources: string[]; // at least 1 save?: string[]; metadata?: unknown; }; requestedAt: number; resolvedAt: number; decision: "allow" | "deny"; source: "config" | "saved" | "hook" | "callback" | "remote" | "default" | "timeout" | "terminal"; reply?: "once" | "always" | "reject"; replyMessage?: string; // max length 4096 }; question?: | { state: "pending"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; } | { state: "answered"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; resolvedAt: number; answers: string[][]; // at least 1 } | { state: "dismissed"; request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; resolvedAt: number; }; requestedAt: number; completedAt?: number; activity?: { checkpoint: number; // min 1 updatedAt: number; message?: string; // max length 1024 preview?: string; observedBytes: number; truncated: boolean; artifact?: { id: string; // uuid path: string; observedBytes: number; state: "writing" | "complete" | "incomplete"; }; childSessionId?: string; // uuid }; artifact?: { id: string; // uuid path: string; observedBytes: number; state: "writing" | "complete" | "incomplete"; }; } | { type: "steering"; id: string; // uuid batch: number; // min 1 promotedAt: number; steers: { steerId: string; // uuid sequence: number; // min 1 // at least 1 prompt: Array< | { type: "text"; text: string } | { type: "image"; source: | { type: "url"; url: string; mimeType?: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; } | { type: "base64"; data: string; mimeType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; }; } | { type: "content-reference"; reference: string /* max length 2048 */ } >; attribution: { clientId: string; // max length 256 authorId?: string; // max length 256 }; acceptedAt: number; }[]; } >; usage?: { promptTokens: number | "Infinity" | "-Infinity" | "NaN"; completionTokens: number | "Infinity" | "-Infinity" | "NaN"; totalTokens: number | "Infinity" | "-Infinity" | "NaN"; cachedPromptTokens?: number | "Infinity" | "-Infinity" | "NaN"; reasoningCompletionTokens?: number | "Infinity" | "-Infinity" | "NaN"; }; contextUse?: { model: string; usedUnits: number; contextUnits: number }; cancellation?: { reason?: string; requestedAt: number }; outcome?: | { kind: "completed"; turnId: string; text: string } | { kind: "cancelled"; turnId: string; reason?: string } | { kind: "failed"; turnId: string; fault: { code: string; // max length 128 message: string; // max length 4096 providerCode?: string; // max length 128 }; } | { kind: "limit_reached"; turnId: string; limit: | { name: "turn_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "approval_wait"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_elapsed"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "provider_attempt_timeout"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "milliseconds"; } | { name: "steering_batches"; origin: "configured" | "provider" | "platform"; value: number; unit: "batches"; } | { name: "provider_attempts"; origin: "configured" | "provider" | "platform"; value: number; // min 1 unit: "attempts"; } | { name: "model_context"; origin: "configured" | "provider" | "platform"; value: number; unit: "model_units"; estimatedInputUnits: number; outputReserveUnits: number; safetyReserveUnits: number; availableInputUnits: number; estimator: string; // max length 128 }; diagnostic: { code: string /* max length 128 */; message: string /* max length 4096 */ }; }; acceptedAt: number; startedAt?: number; completedAt?: number; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/questions/descendants/pending` **List descendant questions.** Returns the pending user questions owned by descendant sub-agent Sessions, oldest first. Operation ID: `listDescendantPendingQuestions` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { questions: { request: { id: string; // uuid sessionId: string; // uuid turnId: string; // uuid toolId: string; toolCallId: string; // at least 1 questions: { question: string; // max length 4096 header: string; // max length 30 // at least 1 options: { label: string; // max length 256 description: string; // max length 4096 }[]; multiple?: boolean; custom?: boolean; }[]; }; requestedAt: number; agent?: string; // max length 128 }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/plugins` **Get Session plugin status.** Returns the redacted desired and active Session plugin revisions, activation state, and the effective runtime plugin inventory. Operation ID: `sessionPlugins` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { status: { sessionId: string; // uuid desired?: { id: string; sequence: number; source: | { _tag: "Package"; name: string; requestedVersion?: string } | { _tag: "File"; uri: string } | { _tag: "Git"; url: string; ref?: string } | { _tag: "Bundle"; url: string } | { _tag: "Static"; id: string }; resolved: { pluginId: string; version: string; integrity?: string }; mcpServers: { serverId: string; name: string; kind: "local" | "remote"; resourceReadTool: boolean; }[]; skills: Array< | { kind: "declaration"; id: string; version: string } | { kind: "source"; id: string; sourceKind: "workspace" | "http" } >; createdAt: number; }; desiredActivation?: { revisionId: string; state: "pending" | "started" | "active" | "failed"; failure?: string; }; active?: { id: string; sequence: number; source: | { _tag: "Package"; name: string; requestedVersion?: string } | { _tag: "File"; uri: string } | { _tag: "Git"; url: string; ref?: string } | { _tag: "Bundle"; url: string } | { _tag: "Static"; id: string }; resolved: { pluginId: string; version: string; integrity?: string }; mcpServers: { serverId: string; name: string; kind: "local" | "remote"; resourceReadTool: boolean; }[]; skills: Array< | { kind: "declaration"; id: string; version: string } | { kind: "source"; id: string; sourceKind: "workspace" | "http" } >; createdAt: number; }; plugins: { source: string; generationId: string; pluginId?: string; packageVersion?: string; status: "active" | "failed"; registrations: string[]; error?: string; }[]; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `PUT /v1/servers/{server}/sessions/{sessionId}/plugins` **Add a Session plugin.** Records an idempotent desired Session plugin revision that replaces the previous one and requests its activation. Operation ID: `addSessionPlugin` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts { id: string; source: | { _tag: "Package"; name: string; requestedVersion?: string } | { _tag: "File"; uri: string } | { _tag: "Git"; url: string; ref?: string } | { _tag: "Bundle"; url: string } | { _tag: "Static"; id: string }; resolved: { pluginId: string; version?: string; integrity?: string }; options: unknown; mcpServers: Array< | { kind: "local"; id: string; // max length 128 name: string; // max length 128 enabled?: boolean; order?: number; resourceReadTool?: boolean; protocolVersions: string[]; // at least 1 clientCapabilities?: { roots: false; sampling: false; elicitation: false }; limits?: { startupMs: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; catalogMs: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; executionMs: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; reconnect?: | { kind: "manual" } | { kind: "retry"; delayMs: number; // min 1 maxAttempts: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; transport: { kind: "stdio"; command: string; arguments: string[]; environment: Record; }; } | { kind: "remote"; id: string; // max length 128 name: string; // max length 128 enabled?: boolean; order?: number; resourceReadTool?: boolean; protocolVersions: string[]; // at least 1 clientCapabilities?: { roots: false; sampling: false; elicitation: false }; limits?: { startupMs: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; catalogMs: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; executionMs: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; reconnect?: | { kind: "manual" } | { kind: "retry"; delayMs: number; // min 1 maxAttempts: { kind: "unlimited" } | { kind: "finite"; value: number /* min 1 */ }; }; transport: { kind: "streamable-http"; endpoint: string; // max length 2048 headers: Record; oauth?: { kind: "oauth"; reference: string /* max length 256 */ }; }; } >; skills: Array< | { kind: "declaration"; declaration: { id: string; // max length 128 displayName: string; description: string; body: string; version: string; modelAdvertisement: { advertised: boolean; userInvocable: boolean }; publicMetadata: { author?: string; license?: string; homepage?: string; tags: string[] }; files?: string[]; }; } | { kind: "source"; source: | { kind: "workspace"; id: string /* max length 128 */; directory: string } | { kind: "http"; id: string; // max length 128 catalogUrl: string; // max length 2048 headers: { name: string /* max length 128 */; value: string /* max length 4096 */ }[]; }; } >; } ``` Response `200` (`application/json`): ```ts { status: { sessionId: string; // uuid desired?: { id: string; sequence: number; source: | { _tag: "Package"; name: string; requestedVersion?: string } | { _tag: "File"; uri: string } | { _tag: "Git"; url: string; ref?: string } | { _tag: "Bundle"; url: string } | { _tag: "Static"; id: string }; resolved: { pluginId: string; version: string; integrity?: string }; mcpServers: { serverId: string; name: string; kind: "local" | "remote"; resourceReadTool: boolean; }[]; skills: Array< | { kind: "declaration"; id: string; version: string } | { kind: "source"; id: string; sourceKind: "workspace" | "http" } >; createdAt: number; }; desiredActivation?: { revisionId: string; state: "pending" | "started" | "active" | "failed"; failure?: string; }; active?: { id: string; sequence: number; source: | { _tag: "Package"; name: string; requestedVersion?: string } | { _tag: "File"; uri: string } | { _tag: "Git"; url: string; ref?: string } | { _tag: "Bundle"; url: string } | { _tag: "Static"; id: string }; resolved: { pluginId: string; version: string; integrity?: string }; mcpServers: { serverId: string; name: string; kind: "local" | "remote"; resourceReadTool: boolean; }[]; skills: Array< | { kind: "declaration"; id: string; version: string } | { kind: "source"; id: string; sourceKind: "workspace" | "http" } >; createdAt: number; }; plugins: { source: string; generationId: string; pluginId?: string; packageVersion?: string; status: "active" | "failed"; registrations: string[]; error?: string; }[]; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `DELETE /v1/servers/{server}/sessions/{sessionId}/plugins` **Remove the Session plugin.** Clears the Session overlay so later Turns use the host baseline. Operation ID: `removeSessionPlugin` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts unknown ``` Response `200` (`application/json`): ```ts { status: { sessionId: string; // uuid desired?: { id: string; sequence: number; source: | { _tag: "Package"; name: string; requestedVersion?: string } | { _tag: "File"; uri: string } | { _tag: "Git"; url: string; ref?: string } | { _tag: "Bundle"; url: string } | { _tag: "Static"; id: string }; resolved: { pluginId: string; version: string; integrity?: string }; mcpServers: { serverId: string; name: string; kind: "local" | "remote"; resourceReadTool: boolean; }[]; skills: Array< | { kind: "declaration"; id: string; version: string } | { kind: "source"; id: string; sourceKind: "workspace" | "http" } >; createdAt: number; }; desiredActivation?: { revisionId: string; state: "pending" | "started" | "active" | "failed"; failure?: string; }; active?: { id: string; sequence: number; source: | { _tag: "Package"; name: string; requestedVersion?: string } | { _tag: "File"; uri: string } | { _tag: "Git"; url: string; ref?: string } | { _tag: "Bundle"; url: string } | { _tag: "Static"; id: string }; resolved: { pluginId: string; version: string; integrity?: string }; mcpServers: { serverId: string; name: string; kind: "local" | "remote"; resourceReadTool: boolean; }[]; skills: Array< | { kind: "declaration"; id: string; version: string } | { kind: "source"; id: string; sourceKind: "workspace" | "http" } >; createdAt: number; }; plugins: { source: string; generationId: string; pluginId?: string; packageVersion?: string; status: "active" | "failed"; registrations: string[]; error?: string; }[]; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/plugins/reload` **Reload the Session plugin.** Requests lazy reconstruction and activation of the desired overlay revision. Operation ID: `reloadSessionPlugin` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts unknown ``` Response `200` (`application/json`): ```ts { status: { sessionId: string; // uuid desired?: { id: string; sequence: number; source: | { _tag: "Package"; name: string; requestedVersion?: string } | { _tag: "File"; uri: string } | { _tag: "Git"; url: string; ref?: string } | { _tag: "Bundle"; url: string } | { _tag: "Static"; id: string }; resolved: { pluginId: string; version: string; integrity?: string }; mcpServers: { serverId: string; name: string; kind: "local" | "remote"; resourceReadTool: boolean; }[]; skills: Array< | { kind: "declaration"; id: string; version: string } | { kind: "source"; id: string; sourceKind: "workspace" | "http" } >; createdAt: number; }; desiredActivation?: { revisionId: string; state: "pending" | "started" | "active" | "failed"; failure?: string; }; active?: { id: string; sequence: number; source: | { _tag: "Package"; name: string; requestedVersion?: string } | { _tag: "File"; uri: string } | { _tag: "Git"; url: string; ref?: string } | { _tag: "Bundle"; url: string } | { _tag: "Static"; id: string }; resolved: { pluginId: string; version: string; integrity?: string }; mcpServers: { serverId: string; name: string; kind: "local" | "remote"; resourceReadTool: boolean; }[]; skills: Array< | { kind: "declaration"; id: string; version: string } | { kind: "source"; id: string; sourceKind: "workspace" | "http" } >; createdAt: number; }; plugins: { source: string; generationId: string; pluginId?: string; packageVersion?: string; status: "active" | "failed"; registrations: string[]; error?: string; }[]; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/plugins/refresh` **Refresh the Session plugin.** Re-resolves the active overlay source; only this mutation may adopt a newer unpinned artifact. Operation ID: `refreshSessionPlugin` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Request body (`application/json`): ```ts { id: string } ``` Response `200` (`application/json`): ```ts { status: { sessionId: string; // uuid desired?: { id: string; sequence: number; source: | { _tag: "Package"; name: string; requestedVersion?: string } | { _tag: "File"; uri: string } | { _tag: "Git"; url: string; ref?: string } | { _tag: "Bundle"; url: string } | { _tag: "Static"; id: string }; resolved: { pluginId: string; version: string; integrity?: string }; mcpServers: { serverId: string; name: string; kind: "local" | "remote"; resourceReadTool: boolean; }[]; skills: Array< | { kind: "declaration"; id: string; version: string } | { kind: "source"; id: string; sourceKind: "workspace" | "http" } >; createdAt: number; }; desiredActivation?: { revisionId: string; state: "pending" | "started" | "active" | "failed"; failure?: string; }; active?: { id: string; sequence: number; source: | { _tag: "Package"; name: string; requestedVersion?: string } | { _tag: "File"; uri: string } | { _tag: "Git"; url: string; ref?: string } | { _tag: "Bundle"; url: string } | { _tag: "Static"; id: string }; resolved: { pluginId: string; version: string; integrity?: string }; mcpServers: { serverId: string; name: string; kind: "local" | "remote"; resourceReadTool: boolean; }[]; skills: Array< | { kind: "declaration"; id: string; version: string } | { kind: "source"; id: string; sourceKind: "workspace" | "http" } >; createdAt: number; }; plugins: { source: string; generationId: string; pluginId?: string; packageVersion?: string; status: "active" | "failed"; registrations: string[]; error?: string; }[]; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/mcp-credentials/{serverName}/authorize` **Start MCP OAuth authorization.** Starts or idempotently resumes user authorization for one remote MCP server and returns only the pending status and the authorization URL. Operation ID: `startMcpCredential` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `serverName` | path | `string` | yes | Request body (`application/json`): ```ts { resource?: string; challenge?: { scheme: "Bearer"; resourceMetadata: string; error?: "invalid_token" | "insufficient_scope"; scope?: string; }; authorizationServer?: string; client?: { clientId: string; tokenEndpointAuthMethod: "none" }; } ``` Response `202` (`application/json`): ```ts { status: { kind: "pending"; generation: number /* min 1 */ }; authorizationUrl: string; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/mcp-credentials/{serverName}` **Get MCP OAuth credential status.** Returns the secret-free status of one Session MCP server credential. Operation ID: `getMcpCredential` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `serverName` | path | `string` | yes | Response `200` (`application/json`): ```ts { status: | { kind: "absent"; generation: number } | { kind: "pending"; generation: number /* min 1 */ } | { kind: "authorized"; generation: number /* min 1 */ } | { kind: "expired"; generation: number /* min 1 */; expiredAtEpochMs: number } | { kind: "authorization-required"; generation: number; // min 1 reason: "client-registration-uncertain" | "authorization-code-uncertain" | "refresh-uncertain"; } | { kind: "forgotten"; generation: number; // min 1 revocation: "completed" | "unsupported" | "not-applicable" | "not-attempted" | "failed" | "uncertain"; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/mcp-credentials/{serverName}/callback` **Complete MCP OAuth callback.** Completes one external OAuth callback through the private lifecycle; the exchanged token never appears in the response. Operation ID: `completeMcpCredentialCallback` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `serverName` | path | `string` | yes | Request body (`application/json`): ```ts { callback: | { kind: "success"; state: string; code: string; issuer?: string } | { kind: "denied"; state: string; error: "access_denied" | "consent_required" | "interaction_required" | "invalid_request" | "invalid_scope" | "login_required" | "server_error" | "temporarily_unavailable" | "unauthorized_client" | "unsupported_response_type"; issuer?: string; }; } ``` Response `200` (`application/json`): ```ts { status: | { kind: "absent"; generation: number } | { kind: "pending"; generation: number /* min 1 */ } | { kind: "authorized"; generation: number /* min 1 */ } | { kind: "expired"; generation: number /* min 1 */; expiredAtEpochMs: number } | { kind: "authorization-required"; generation: number; // min 1 reason: "client-registration-uncertain" | "authorization-code-uncertain" | "refresh-uncertain"; } | { kind: "forgotten"; generation: number; // min 1 revocation: "completed" | "unsupported" | "not-applicable" | "not-attempted" | "failed" | "uncertain"; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/mcp-credentials/{serverName}/cancel` **Cancel MCP OAuth authorization.** Cancels a pending MCP OAuth authorization and returns the resulting status. Operation ID: `cancelMcpCredential` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `serverName` | path | `string` | yes | Request body (`application/json`): ```ts { generation?: number /* min 1 */ } ``` Response `200` (`application/json`): ```ts { status: | { kind: "absent"; generation: number } | { kind: "pending"; generation: number /* min 1 */ } | { kind: "authorized"; generation: number /* min 1 */ } | { kind: "expired"; generation: number /* min 1 */; expiredAtEpochMs: number } | { kind: "authorization-required"; generation: number; // min 1 reason: "client-registration-uncertain" | "authorization-code-uncertain" | "refresh-uncertain"; } | { kind: "forgotten"; generation: number; // min 1 revocation: "completed" | "unsupported" | "not-applicable" | "not-attempted" | "failed" | "uncertain"; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/servers/{server}/sessions/{sessionId}/mcp-credentials/{serverName}/forget` **Forget MCP OAuth credential.** Revokes and forgets one Session MCP server credential and retires its live connections. Operation ID: `forgetMcpCredential` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | | `serverName` | path | `string` | yes | Request body (`application/json`): ```ts unknown ``` Response `200` (`application/json`): ```ts { generation: number; // min 1 revocation: "completed" | "unsupported" | "not-applicable" | "not-attempted" | "failed" | "uncertain"; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/models` **Get Session models.** Returns the adopted host catalog snapshot for the Session. Operation ID: `sessionModels` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { hostRevision: string; // max length 128 defaultModel?: string; // max length 641 providers: { id: string; // max length 128 displayName: string; // max length 256 availability: | { status: "available" } | { status: "unavailable"; reason: "missing_credentials" | "reconnect_required" | "disabled" | "unsupported" | "temporarily_unavailable"; }; }[]; models: { providerId: string; // max length 128 ref: string; // max length 641 id: string; // max length 512 upstreamId: string; // max length 512 displayName: string; // max length 256 capabilities: { input: ("text" | "image")[]; // at least 1 output: "text"[]; // at least 1 tools: boolean; image?: { referenceTypes: ("url" | "base64")[]; // at least 1 mimeTypes: ("image/jpeg" | "image/png" | "image/gif" | "image/webp")[]; // at least 1 }; // at least 1 reasoningEfforts?: ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max")[]; }; limits: { contextUnits: number; outputUnits: number }; pricing: | { version: "v1"; currency: string; // max length 3 unit: "million_model_units"; rates: { input: string; // max length 128 output: string; // max length 128 cacheReadInput?: string; // max length 128 cacheWriteInput?: string; // max length 128 }; } | { version: "unknown" }; defaults: { temperature?: number; topP?: number; // max 1 maxOutputUnits?: number; // min 1 stopSequences?: string[]; // at least 1 seed?: number; reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; }; availability: | { status: "available" } | { status: "unavailable"; reason: "missing_credentials" | "reconnect_required" | "disabled" | "unsupported" | "temporarily_unavailable"; }; }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/models/events` **Stream Session model revision events.** Streams revision-only SSE events whenever the Session adopts a new host catalog revision. Operation ID: `sessionModelEvents` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200`: no body. Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/servers/{server}/sessions/{sessionId}/agents` **List Session agents.** Returns the effective host and Session agents with their display metadata and modes. Operation ID: `sessionAgents` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `server` | path | `string` | yes | | `sessionId` | path | `string (uuid)` | yes | Response `200` (`application/json`): ```ts { defaultAgent?: string; // max length 128 agents: { id: string; // max length 128 displayName: string; description: string; model?: string; // max length 641 mode: "primary" | "subagent" | "all"; origin: "host" | "session"; }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ## Runtime ### `GET /v1/runtime/plugins` **List runtime plugins.** Returns safe plugin registration metadata. Operation ID: `plugins` Response `200` (`application/json`): ```ts { plugins: { source: string; generationId: string; pluginId?: string; packageVersion?: string; status: "active" | "failed"; registrations: string[]; error?: string; }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/runtime/skills` **List runtime Skills.** Returns safe host Skill metadata. Operation ID: `skills` Response `200` (`application/json`): ```ts { skills: { id: string; // max length 128 displayName: string; description: string; advertised: boolean; userInvocable: boolean; origin: "host" | "session"; }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/runtime/reload` **Reload runtime.** Requests a trusted runtime plugin reload. Operation ID: `reload` Request body (`application/json`): ```ts unknown ``` Response `200` (`application/json`): ```ts { result: "reloaded" | "rejected"; plugins: { source: string; generationId: string; pluginId?: string; packageVersion?: string; status: "active" | "failed"; registrations: string[]; error?: string; }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/runtime/models` **List runtime models.** Returns the current host model catalog snapshot. Operation ID: `models` Response `200` (`application/json`): ```ts { hostRevision: string; // max length 128 defaultModel?: string; // max length 641 providers: { id: string; // max length 128 displayName: string; // max length 256 availability: | { status: "available" } | { status: "unavailable"; reason: "missing_credentials" | "reconnect_required" | "disabled" | "unsupported" | "temporarily_unavailable"; }; }[]; models: { providerId: string; // max length 128 ref: string; // max length 641 id: string; // max length 512 upstreamId: string; // max length 512 displayName: string; // max length 256 capabilities: { input: ("text" | "image")[]; // at least 1 output: "text"[]; // at least 1 tools: boolean; image?: { referenceTypes: ("url" | "base64")[]; // at least 1 mimeTypes: ("image/jpeg" | "image/png" | "image/gif" | "image/webp")[]; // at least 1 }; // at least 1 reasoningEfforts?: ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max")[]; }; limits: { contextUnits: number; outputUnits: number }; pricing: | { version: "v1"; currency: string; // max length 3 unit: "million_model_units"; rates: { input: string; // max length 128 output: string; // max length 128 cacheReadInput?: string; // max length 128 cacheWriteInput?: string; // max length 128 }; } | { version: "unknown" }; defaults: { temperature?: number; topP?: number; // max 1 maxOutputUnits?: number; // min 1 stopSequences?: string[]; // at least 1 seed?: number; reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; }; availability: | { status: "available" } | { status: "unavailable"; reason: "missing_credentials" | "reconnect_required" | "disabled" | "unsupported" | "temporarily_unavailable"; }; }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/runtime/models/refresh` **Refresh provider models.** Re-discovers models from the provider (one, or every discovery provider) and returns the resulting catalog. Connecting a provider, and a prompt naming a model the catalog lacks, refresh the same way. Operation ID: `refreshProviderModels` Request body (`application/json`): ```ts { providerId?: string /* max length 128 */ } ``` Response `200` (`application/json`): ```ts { providers: { providerId: string; // max length 128 status: "updated" | "unchanged" | "failed"; message?: string; }[]; snapshot: { hostRevision: string; // max length 128 defaultModel?: string; // max length 641 providers: { id: string; // max length 128 displayName: string; // max length 256 availability: | { status: "available" } | { status: "unavailable"; reason: "missing_credentials" | "reconnect_required" | "disabled" | "unsupported" | "temporarily_unavailable"; }; }[]; models: { providerId: string; // max length 128 ref: string; // max length 641 id: string; // max length 512 upstreamId: string; // max length 512 displayName: string; // max length 256 capabilities: { input: ("text" | "image")[]; // at least 1 output: "text"[]; // at least 1 tools: boolean; image?: { referenceTypes: ("url" | "base64")[]; // at least 1 mimeTypes: ("image/jpeg" | "image/png" | "image/gif" | "image/webp")[]; // at least 1 }; // at least 1 reasoningEfforts?: ("none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max")[]; }; limits: { contextUnits: number; outputUnits: number }; pricing: | { version: "v1"; currency: string; // max length 3 unit: "million_model_units"; rates: { input: string; // max length 128 output: string; // max length 128 cacheReadInput?: string; // max length 128 cacheWriteInput?: string; // max length 128 }; } | { version: "unknown" }; defaults: { temperature?: number; topP?: number; // max 1 maxOutputUnits?: number; // min 1 stopSequences?: string[]; // at least 1 seed?: number; reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; }; availability: | { status: "available" } | { status: "unavailable"; reason: "missing_credentials" | "reconnect_required" | "disabled" | "unsupported" | "temporarily_unavailable"; }; }[]; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/runtime/models/events` **Stream runtime model revision events.** Streams revision-only SSE events whenever the runtime model catalog revision changes. Operation ID: `modelEvents` Response `200`: no body. Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/runtime/providers` **List runtime providers.** Returns safe runtime provider summaries and connection status. Operation ID: `providers` Response `200` (`application/json`): ```ts { providers: { id: string; // max length 128 displayName: string; availability: | { status: "available" } | { status: "unavailable"; reason: "missing_credentials" | "reconnect_required" | "disabled" | "unsupported" | "temporarily_unavailable"; }; source: "stored" | "environment" | "none" | "via"; via?: string; // max length 128 connection?: { connectionId: string; // max length 128 providerId: string; // max length 128 label: string; // max length 256 method: "api_key" | "oauth"; status: "ready" | "reconnect_required"; createdAt: number; updatedAt: number; }; methods: ("api_key" | "oauth")[]; logins: { id: string; label: string; notice?: { text: string; links: { label: string; url: string }[] }; }[]; }[]; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/runtime/providers/{providerId}` **Get runtime provider.** Returns a safe runtime provider summary by ID. Operation ID: `getProvider` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `providerId` | path | `string` | yes | Response `200` (`application/json`): ```ts { provider: { id: string; // max length 128 displayName: string; availability: | { status: "available" } | { status: "unavailable"; reason: "missing_credentials" | "reconnect_required" | "disabled" | "unsupported" | "temporarily_unavailable"; }; source: "stored" | "environment" | "none" | "via"; via?: string; // max length 128 connection?: { connectionId: string; // max length 128 providerId: string; // max length 128 label: string; // max length 256 method: "api_key" | "oauth"; status: "ready" | "reconnect_required"; createdAt: number; updatedAt: number; }; methods: ("api_key" | "oauth")[]; logins: { id: string; label: string; notice?: { text: string; links: { label: string; url: string }[] }; }[]; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/runtime/providers/{providerId}/connect/key` **Connect provider API key.** Stores an API key for the provider, replacing any previous key atomically. Operation ID: `connectProviderKey` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `providerId` | path | `string` | yes | Request body (`application/json`): ```ts { apiKey: string; label?: string /* max length 256 */ } ``` Response `200` (`application/json`): ```ts { connection: { connectionId: string; // max length 128 providerId: string; // max length 128 label: string; // max length 256 method: "api_key" | "oauth"; status: "ready" | "reconnect_required"; createdAt: number; updatedAt: number; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `POST /v1/runtime/providers/{providerId}/logins` **Start provider login.** Starts a host-held login and returns the prompt to show the user. The host completes the provider exchange. Operation ID: `startProviderLogin` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `providerId` | path | `string` | yes | Request body (`application/json`): ```ts { methodId?: string } ``` Response `200` (`application/json`): ```ts | { status: "pending"; loginId: string; // max length 128 methodId: string; prompt: { kind: "device"; verificationUrl: string; userCode: string }; expiresAt: number | "Infinity" | "-Infinity" | "NaN"; } | { status: "connected"; loginId: string; // max length 128 connection: { connectionId: string; // max length 128 providerId: string; // max length 128 label: string; // max length 256 method: "api_key" | "oauth"; status: "ready" | "reconnect_required"; createdAt: number; updatedAt: number; }; } | { status: "failed"; loginId: string; // max length 128 reason: "denied" | "expired" | "cancelled" | "error"; message: string; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `GET /v1/runtime/providers/{providerId}/logins/{loginId}` **Get provider login.** Returns the login status, optionally waiting up to waitMs for it to change. Operation ID: `getProviderLogin` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `providerId` | path | `string` | yes | | `loginId` | path | `string` | yes | | `waitMs` | query | `string` | no | Response `200` (`application/json`): ```ts | { status: "pending"; loginId: string; // max length 128 methodId: string; prompt: { kind: "device"; verificationUrl: string; userCode: string }; expiresAt: number | "Infinity" | "-Infinity" | "NaN"; } | { status: "connected"; loginId: string; // max length 128 connection: { connectionId: string; // max length 128 providerId: string; // max length 128 label: string; // max length 256 method: "api_key" | "oauth"; status: "ready" | "reconnect_required"; createdAt: number; updatedAt: number; }; } | { status: "failed"; loginId: string; // max length 128 reason: "denied" | "expired" | "cancelled" | "error"; message: string; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `DELETE /v1/runtime/providers/{providerId}/logins/{loginId}` **Cancel provider login.** Cancels a pending login. Operation ID: `cancelProviderLogin` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `providerId` | path | `string` | yes | | `loginId` | path | `string` | yes | Response `200` (`application/json`): ```ts | { status: "pending"; loginId: string; // max length 128 methodId: string; prompt: { kind: "device"; verificationUrl: string; userCode: string }; expiresAt: number | "Infinity" | "-Infinity" | "NaN"; } | { status: "connected"; loginId: string; // max length 128 connection: { connectionId: string; // max length 128 providerId: string; // max length 128 label: string; // max length 256 method: "api_key" | "oauth"; status: "ready" | "reconnect_required"; createdAt: number; updatedAt: number; }; } | { status: "failed"; loginId: string; // max length 128 reason: "denied" | "expired" | "cancelled" | "error"; message: string; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `PATCH /v1/runtime/providers/{providerId}/connections/{connectionId}` **Update provider connection label.** Updates the metadata label for a provider connection. Operation ID: `updateProviderConnection` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `providerId` | path | `string` | yes | | `connectionId` | path | `string` | yes | Request body (`application/json`): ```ts { label: string /* max length 256 */ } ``` Response `200` (`application/json`): ```ts { connection: { connectionId: string; // max length 128 providerId: string; // max length 128 label: string; // max length 256 method: "api_key" | "oauth"; status: "ready" | "reconnect_required"; createdAt: number; updatedAt: number; }; } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ### `DELETE /v1/runtime/providers/{providerId}/connections/{connectionId}` **Delete provider connection.** Deletes the provider connection by matching provider ID and connection ID. Operation ID: `deleteProviderConnection` | Parameter | In | Type | Required | | --- | --- | --- | --- | | `providerId` | path | `string` | yes | | `connectionId` | path | `string` | yes | Response `200` (`application/json`): ```ts { success: boolean } ``` Errors: `400` ValidationErrorEncoded, `401` UnauthorizedErrorEncoded, `403` ForbiddenErrorEncoded, `404` NotFoundErrorEncoded, `409` ConflictErrors, `500` InternalServerErrorEncoded, `503` UnavailableErrorEncoded. ## Shared schemas ### `UnauthorizedErrorEncoded` ```ts { _tag: "UnauthorizedError"; message: string } ``` ### `UnavailableErrorEncoded` ```ts { _tag: "UnavailableError"; message: string; reason?: "model_unavailable" | "service_unavailable"; } ``` ### `InternalServerErrorEncoded` ```ts { _tag: "InternalServerError"; message: string } ``` ### `ConflictErrors` ```ts | ConflictErrorEncoded | SteerConflictErrorEncoded | TurnNotActiveErrorEncoded | TurnNotSteerableErrorEncoded ``` ### `TurnNotSteerableErrorEncoded` ```ts { _tag: "TurnNotSteerableError"; message: string } ``` ### `TurnNotActiveErrorEncoded` ```ts { _tag: "TurnNotActiveError"; message: string } ``` ### `SteerConflictErrorEncoded` ```ts { _tag: "SteerConflictError"; message: string } ``` ### `ConflictErrorEncoded` ```ts { _tag: "ConflictError"; message: string } ``` ### `NotFoundErrorEncoded` ```ts { _tag: "NotFoundError"; message: string; resource?: "session" | "turn" | "tool_call" | "mcp_server" | "mcp_prompt" | "mcp_resource" | "context_compaction" | "permission_request" | "question_request" | "saved_permission" | "endpoint" | "provider_login" | "provider" | "connection"; } ``` ### `ForbiddenErrorEncoded` ```ts { _tag: "ForbiddenError"; message: string } ``` ### `ValidationErrorEncoded` ```ts { _tag: "ValidationError"; message: string } ``` --- Generated from [`packages/protocol/src/`](https://github.com/ZephyrCloudIO/ze-harness/tree/main/packages/protocol/src). --- # `@zephyrcloudio/harness-llm` Portable Effect Schema and Stream contracts for one provider model attempt. The package owns canonical instructions and messages, model-visible tool descriptors, per-request generation and provider options, assistant-message-bound private continuation artifacts, exact attempt usage with optional provider billing provenance, ordered model events, typed failures, and the scoped `Model` service. Provider adapters lower these values to their own wire formats. It does not execute tools, retry requests, mutate history, persist data, run the model/tool continuation loop, or decide Turn outcomes. The scripted model is a deterministic test provider and uses only Effect synchronization primitives. Production Turns use this contract through the Core-owned continuation loop. Provider-specific wire values stay inside their native provider adapters. Stateful providers use `openModelRequestChain` to allocate one scoped chain per Turn. The chain is reused for retries and model/tool rounds, while request-chain state stays off the reusable service. `ModelService.stream` **MUST return its declared `Stream` synchronously**. It must not be `async` or return a `Promise`, thenable, or `Proxy` wrapper. Put asynchronous provider work inside the returned Stream. Malformed async returns are unsupported trusted-code behavior. Ordinary Promise rejection observation is best effort and is not a security boundary. Portable JavaScript cannot guarantee observation for Proxy-hidden Promises or Promises with hostile `Symbol.species` behavior whose `constructor` cannot be reversibly shadowed. A typed Turn outcome is not guaranteed. Under Node's strict unhandled-rejection behavior, the process may terminate and the raw rejection reason may appear in process diagnostics. ## Native provider authoring contract `defineProvider` defines a native LLM provider module. A native provider module exports its `defineProvider(...)` value as a named export (default exports are not used): - `definitionId`: stable lowercase branded string matching `/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/`. - `version`: author-owned version string. - `optionsSchema`: Effect Schema codec for provider options. - `credentials`: optional Effect Schema codec for private persisted credentials (e.g. API keys). - `requestOptionsSchema`: Effect Schema codec for per-request options. - `filterModel`: optional deterministic capability/protocol filter for catalog models. - `correctModel`: optional narrow corrections for catalog models (upstream ID, limits, etc.). - `acquireModel`: scoped synchronous Effect returning the `ModelService` for execution. All callbacks must be synchronous (no `async` / `Promise`): `acquireModel` returns its `Effect`, `filterModel` a `boolean`, `correctModel` the corrected metadata. Core decodes `options` once per setup with the definition's schema, so `acquireModel` receives already-validated options. `credentials` resolve live at each acquisition (stored/configured/env) and arrive as opaque `Redacted` values — validate emptiness defensively, as the compatible provider does. It constructs the `ModelService` directly instead of building a throwaway `Layer` per Turn. The acquisition input also carries the exact upstream model id, optional per-model API root, and selected wire `protocol` (`chat`, `responses`, or `messages`). Providers with a fixed protocol may ignore the protocol; a multi-protocol provider dispatches on it instead of hardcoding model lists. ## Declarative compatible providers `makeCompatibleProviderDefinition` builds one native provider definition from validated data. It uses Ze's Chat Completions, Responses, and Messages adapters directly; no AI SDK sits between Ze and the upstream API. The definition can configure routes, bearer/header/no authentication, protocol-specific authentication and headers, static headers, Session-derived headers, and typed failure classification. Core combines that transport with catalog-selected model protocols, endpoints, upstream IDs, and body defaults. Chat and Responses authenticate with bearer tokens by default. Messages uses `x-api-key` by default. An explicit authentication rule overrides these defaults. Unsupported protocols are rejected by the schema rather than routed to a fallback wire. ## Provider base URLs The protocol adapters share one base URL contract (`providers/base-url.ts`). A configured base URL must be an absolute `http(s)` URL without credentials, query, or fragment, including bare `?`/`#` delimiters. Query and fragment are rejected rather than stripped or preserved: silently rewriting the configured root would change proxy routing without telling the operator, and preserving them would forward ambient URL material into every provider request. Endpoint paths are joined from the validated parsed pathname, so trailing slashes never produce double slashes or a missing path segment. ## Provider request deadlines The protocol adapters share one deadline helper (`providers/timeout.ts`). `requestTimeoutMs` bounds the whole HTTP request as total elapsed time, not each idle interval. Omitted or null means unlimited. Progress never resets the deadline: request setup, error-body reads, and every response-body pull are bounded by the time remaining since the request started. Expiration fails the attempt with `ModelTimeoutFailure` (`timeoutKind: 'attempt-limit'`) after the adapter's normal body cleanup aborts the host request. ## Wire parser bounds The protocol adapters share one size bound (`providers/limits.ts`): a single SSE event and a single HTTP error body are each capped at 1 MiB. Overflow fails the attempt with a typed malformed failure, so one pathological payload cannot exhaust host memory before a typed failure is produced. ## Credential boundary threat model The provider already holds the API key by design (it authenticates every request). The boundary keeps the key out of the opaque values a provider hands back, which a proxy or gateway that reflects the `Authorization` header could echo: response IDs and their continuation reference, the provider finish reason, and failure evidence (provider codes, request IDs, header evidence). A value that contains the credential is dropped, not rewritten. Model output (text, reasoning, tool input, and continuation payloads) is not rewritten. As in OpenCode and Pi, keep secrets out of what the model can read instead: route calls through a gateway that holds the provider key, and carry credentials as `Redacted` values to the request header. Non-goals: the boundary is not a sandbox (adapters are trusted code), and it does not defend against a provider that misuses the key it already holds. ## Images in tool results `ToolResultValue` accepts `{ type: 'content', content: [...] }` alongside text and JSON. Each content part is text, JSON, or an image using the same URL/base64 references as user images. The durable equivalent is `CanonicalToolResult` with `format: 'content'` and the parts in `value`. Responses and the Codex Responses deployment send these parts inside `function_call_output.output`. Anthropic Messages nests them in `tool_result.content`. Chat Completions accepts only text in tool replies, so its adapter sends text and JSON in the tool reply and adds labeled images in a user message after the contiguous tool replies. Images retain their tool call IDs in those labels; canonical history keeps them attached to the original tool result. ## Public TypeScript exports - `AssistantContentPart` - `AssistantMessage` - `builtinCredentialTransports` - `builtinLoginModule` - `canonicalizeProviderFailure` - `CompatibleModelDiscovery` - `CompatibleProviderAuthentication` - `CompatibleProviderConfig` - `CompatibleProviderConfigValue` - `CompatibleProviderCredentials` - `CompatibleProviderCredentialsValue` - `CompatibleProviderFailureRules` - `CompatibleProviderOptions` - `CompatibleProviderOptionsValue` - `CompatibleProviderProtocolAuthentication` - `CompatibleProviderProtocolHeaders` - `compatibleProviderRequiresCredential` - `CompatibleProviderRoutes` - `CompletedContentPart` - `CredentialTransport` - `CredentialTransportBinding` - `credentialTransportRegistry` - `CredentialTransportRegistry` - `defineProvider` - `DeviceLoginDescriptor` - `discoverCompatibleModels` - `discoverDeviceLogin` - `DiscoveredRemoteModel` - `FinishReason` - `GenerationSettings` - `InvalidProviderDefinitionError` - `joinProviderEndpoint` - `LegacyProviderLogin` - `makeCodexLogin` - `makeCompatibleProviderDefinition` - `makeModelLayer` - `makeModelTurnContext` - `makeOAuth2DeviceLogin` - `makeScriptedModelLayer` - `MalformedProviderDataFailure` - `MalformedProviderResponseFailure` - `MalformedProviderStreamFailure` - `Model` - `ModelAttemptUsage` - `ModelAuthenticationFailure` - `ModelCallIdentity` - `ModelContextOverflowFailure` - `ModelEvent` - `ModelFailure` - `ModelImplementation` - `ModelIncompleteResponseFailure` - `ModelInstruction` - `ModelInstructions` - `ModelInvalidRequestFailure` - `ModelMessage` - `ModelMessages` - `ModelPermissionFailure` - `ModelProtocol` - `modelProviderId` - `ModelRateLimitFailure` - `ModelRequest` - `ModelRequestChain` - `ModelRequestChainImplementation` - `ModelRequestState` - `ModelRetryDirective` - `ModelService` - `ModelStreamTerminationFailure` - `ModelTimeoutFailure` - `ModelToolCallPart` - `ModelToolChoice` - `ModelToolDescriptor` - `ModelTransportFailure` - `ModelTurnContext` - `modelTurnContextCall` - `modelTurnContextSessionId` - `ModelUnavailableFailure` - `ModelUnsupportedCapabilityFailure` - `NoProviderOptions` - `normalizeProviderFailure` - `OAuth2RefreshFailure` - `openModelRequestChain` - `PrivateContinuationArtifact` - `PrivateContinuationArtifacts` - `PrivateMessageContinuation` - `PrivateMessageContinuations` - `ProviderBilledUnit` - `ProviderCost` - `ProviderCredentialSource` - `ProviderDefinition` - `ProviderFailureInput` - `ProviderLoginAttempt` - `ProviderLoginError` - `ProviderLoginMethodImplementation` - `ProviderLoginModule` - `ProviderModelAcquisitionInput` - `ProviderModelCorrection` - `ProviderModelDiscoveryError` - `ProviderModelFilter` - `ProviderRequestOptions` - `ProviderResponseId` - `ProviderUsageProvenance` - `ReasoningBoundaryPart` - `ReasoningPart` - `refreshOAuth2Credential` - `RefusalPart` - `RunnerCredential` - `ScriptedModel` - `ScriptedModelControl` - `ScriptedModelRequest` - `ScriptedModelScript` - `ScriptedModelStep` - `staticRunnerCredential` - `TextPart` - `ToolJsonSchema` - `ToolMessage` - `ToolResultPart` - `ToolResultValue` - `upgradeLegacyCodexCredential` - `UserContentPart` - `UserMessage` - `validateModelEventStream` --- Generated from [`packages/llm/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/llm/README.md). --- # `@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 = { 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 `); 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: `. */ 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: `. */ 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?: | { /** * `/*`, where `` 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; /** * 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/.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) | 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: `. */ 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 & { 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; /** * 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; /** * 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; }; /** * 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 { /** * 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) | 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; } ``` ### `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; /** 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; /** * 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; /** * 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; } /** * 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; } ``` ### `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; /** * 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; /** * 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; /** * 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; } /** The name of a hook. */ export type HookName = keyof HookHandlers; /** The handler signature for one hook name. */ export type HookHandler = 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 `/catalog.json`, a Harness catalog of the form * `{ "schemaVersion": 1, "version": "1.0.0", "skills": [{ "body": "/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:` 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; } ``` ### `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 `_`, 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 `_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>; }; }) /** 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>; /** * 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`: Core passes `execute` the * input after validating it against the declaration's `inputSchema`. */ readonly register: (declaration: ToolDeclaration) => 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: ( name: TName, handler: HookHandler, ) => 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; }; } /** * 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; /** * 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; } /** * 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). --- # `@zephyrcloudio/harness-protocol` The validated, versioned HTTP and SSE contract shared by the Server and Client. Import endpoint metadata, Effect schemas, limits, and transport error values from the package root: ```ts import { V1, TurnResponse, decodeProtocol } from '@zephyrcloudio/harness-protocol'; ``` The protocol does not perform network requests. Decode all external values, respect `MAX_WIRE_BYTES`, apply the shared byte-safe Session system-prompt schema, and treat SSE values as observations rather than commands. A caught-up Session event stream repeats its current `session.cursor` at least every `SESSION_EVENT_HEARTBEAT_MILLIS` (15 seconds), and a quiet Turn event stream sends `turn.heartbeat` at least every `TURN_EVENT_HEARTBEAT_MILLIS` (15 seconds), so clients can treat a longer silence as a lost connection. A `turn.heartbeat` carries no new state: every durable event through its `revision` has already been sent. The Fetch Server adds `Ze-Harness-Version: /` to responses, including SSE and API errors. For example, `1/0.6.0` advertises protocol 1 and release 0.6.0. The header is exposed through `Access-Control-Expose-Headers` without changing origin authorization. `SERVER_VERSION_HEADER`, `ServerVersion`, and `parseServerVersionHeader` define this advisory metadata. It is not an admission or compatibility gate: clients can warn without retrying an already accepted mutation. Old servers may omit it. The v1 contract includes revision-checked Session configuration replacement, durable permission replies (`once`, `always`, or `reject`), and listing or removing saved Session grants. Replies target a `PermissionRequestId`, not a Tool call ID. Sessions fork with `POST /v1/servers/:server/sessions/:sessionId/forks/:forkedSessionId`: settled history before an optional `turnId` is copied into the caller-selected identity, the targeted Turn stays behind for resending, and an absent body forks the whole settled history. A `through` boundary instead copies the named message inclusively (a Turn message selects its whole Turn; a conversation entry selects itself), and `visibility` restricts the copy to what the forking actor may observe. The fork inherits the parent workdir, default model, system prompt, and permission rules, and records `forkedFrom` lineage (parent Session plus the boundary) queryable on the Session; saved grants, overlay state, and unsettled work stay behind, and exact retries are idempotent. Sessions carry an immutable working directory (`workdir`, server root by default): relative file paths resolve inside it, shell commands run inside it, and file and shell permission resources are judged against it. Creation may send a `clientWorkdir` absolute path instead; the server resolves it against its root (an explicit `workdir` always wins) and rejects hints outside the root. List a server's Sessions with `GET /v1/servers/:server/sessions` (oldest first, deleted identities excluded) and delete a server — every Session plus shared credentials, with files kept — with `DELETE /v1/servers/:server`. Watch a whole server with `GET /v1/servers/:server/events?afterServerCursor=N`. Omit the cursor to start live from the current one, which the first `server.cursor` announces; pass one to replay every stamped row after it. The stream is the control-plane feed over the durable `ServerActivityCursor`: one monotonic counter per server database that stamps every Session creation and title change, every Turn change, and every Session deletion. Each `ServerStreamEvent` carries the `serverCursor` it was observed at and reuses the Session stream's Turn event shapes (`session.turn.snapshot`, `.tool.activity`, `.started`, `.usage`, `.tool.requested`, `.approval.requested`, `.approval.resolved`, `.tool.completed`, `.question.asked`, `.question.replied`, `.question.rejected`) with their per-Session envelope intact; the live text, refusal, and reasoning deltas stay on the per-Session stream. Session-scoped events are `session.created` (the Session snapshot, sent the first time a stream sees a Session and therefore repeatable after a resume), `session.title`, and `session.deleted` (`sessionId`, `deletedAt`). A Session's first event on a stream is always `session.created`: a Turn event for a Session the stream has not announced yet is preceded by one stamped one `serverCursor` before the Turn's, so a resume between the two replays the Turn and may repeat that `session.created`. A caught-up stream repeats `server.cursor` at least every `SERVER_EVENT_HEARTBEAT_MILLIS` (15 seconds), and a page whose rows produced no event still advances with `server.cursor`. Sub-agents spawn through the `task` tool, not the API: each call runs one prompt to completion on a child Session pinned to the named agent, or continues `task_id`, and settles to a result envelope. The child inherits the parent workdir and default model and records `spawnedFrom` lineage; deleting a parent deletes its spawned children while forks stay independent. Read children with `GET /v1/servers/:server/sessions/:sessionId/children`, report host support with `GET /v1/capabilities`, and detach blocking foreground chains with `POST /v1/servers/:server/sessions/:sessionId/background` (true when any chain detached; each result arrives as a queued parent turn). It also defines model discovery and provider connection endpoints: - Model discovery: `GET /v1/runtime/models`, `GET /v1/runtime/models/events`, `GET /v1/servers/:server/sessions/:sessionId/models`, and `GET /v1/servers/:server/sessions/:sessionId/models/events`. - Provider management: `GET /v1/runtime/providers`, `GET /v1/runtime/providers/:providerId`, `POST /v1/runtime/providers/:providerId/connect/key`, `PATCH /v1/runtime/providers/:providerId/connections/:connectionId`, and `DELETE /v1/runtime/providers/:providerId/connections/:connectionId`. - Revisions are single-shot current-revision checks over SSE: each `.../models/events` call emits the present revision once and closes. Clients re-fetch the corresponding `GET .../models` snapshot for fresh data. Secrets are never exposed over protocol routes. ## Public TypeScript exports - `AddSessionPluginRequest` - `AddSessionPluginRequestValue` - `AppendConversationReceiptResponse` - `AppendConversationRequest` - `AppendConversationRequestValue` - `Authorization` - `CancelContextCompactionRequest` - `CancelMcpCredentialRequest` - `CancelTurnRequest` - `CapabilitiesResponse` - `CapabilitiesResponseValue` - `CLIENT_ID_HEADER` - `ClientWorkdir` - `ClientWorkdirValue` - `CompleteMcpCredentialCallbackRequest` - `CompleteMcpCredentialCallbackRequestValue` - `ConflictError` - `ConflictErrors` - `ConnectProviderKeyRequest` - `ConnectProviderKeyResponse` - `ContextCompactionReceiptResponse` - `ContextCompactionResponse` - `CreateSessionRequest` - `CreateSessionRequestValue` - `DAEMON_LOG_PAGE_MAX` - `DaemonLogEntry` - `DaemonLogLevel` - `DaemonLogLevelValue` - `DaemonLogQuery` - `DaemonLogResponse` - `DaemonLogResponseValue` - `decodeProtocol` - `DeleteProviderConnectionResponse` - `DeleteSessionRequest` - `DescendantApprovalsResponse` - `DescendantQuestionsResponse` - `EndpointErrors` - `ForbiddenError` - `ForgetMcpCredentialRequest` - `ForkSessionRequest` - `HARNESS_PROTOCOL_VERSION` - `HarnessApi` - `HealthResponse` - `HealthResponseValue` - `InternalServerError` - `makeHarnessOpenApi` - `MAX_WIRE_BYTES` - `MCP_CREDENTIAL_AUTHORIZATION_REQUIRED_REASONS` - `MCP_CREDENTIAL_CALLBACK_DENIED_REASONS` - `MCP_CREDENTIAL_REVOCATION_STATUSES` - `McpCatalogPrompt` - `McpCatalogResource` - `McpCatalogResourceTemplate` - `McpCatalogResponse` - `McpCatalogServer` - `McpCatalogSnapshot` - `McpCatalogSnapshotValue` - `McpCatalogTool` - `McpCredentialChallenge` - `McpCredentialChallengeValue` - `McpCredentialForgetResponse` - `McpCredentialForgetResponseValue` - `McpCredentialStartResponse` - `McpCredentialStartResponseValue` - `McpCredentialStatus` - `McpCredentialStatusResponse` - `McpCredentialStatusValue` - `McpPromptArguments` - `McpPromptName` - `McpResourceContents` - `McpResourceReadResponse` - `McpResourceReadResult` - `McpResourceReadResultValue` - `McpServerCatalog` - `McpServerName` - `ModelRevisionEvent` - `NotFoundError` - `parseServerVersionHeader` - `PermissionReplyRequest` - `PermissionReplyResponse` - `PluginJournalCursor` - `PluginJournalQuery` - `PluginJournalResponse` - `PluginJournalResponseValue` - `ProviderModelRefreshOutcome` - `ProviderModelRefreshOutcomeValue` - `PublicPromptAdmission` - `QuestionReplyRequest` - `QuestionReplyResponse` - `ReadMcpResourceRequest` - `RefreshProviderModelsRequest` - `RefreshProviderModelsResponse` - `RefreshProviderModelsResponseValue` - `RefreshSessionPluginRequest` - `RefreshSessionPluginRequestValue` - `ReloadSessionPluginRequest` - `RemoveSessionPluginRequest` - `ReplaceSessionConfigurationRequest` - `REQUEST_ID_HEADER` - `RuntimeApi` - `RuntimePlugin` - `RuntimePluginsResponse` - `RuntimePluginValue` - `RuntimeProviderResponse` - `RuntimeProvidersResponse` - `RuntimeReloadRequest` - `RuntimeReloadResponse` - `RuntimeSkillsResponse` - `SafeFailure` - `SafeProviderSummary` - `SavedPermissionResponse` - `SavedPermissionsResponse` - `SchemaError` - `SERVER_EVENT_HEARTBEAT_MILLIS` - `SERVER_VERSION_HEADER` - `ServerCursorEvent` - `ServerDeletedResponse` - `ServerDeletedResponseValue` - `ServerSessionCreatedEvent` - `ServerSessionDeletedEvent` - `ServerSessionTitleEvent` - `ServerStreamEvent` - `ServerStreamEventValue` - `ServerTurnApprovalRequestedEvent` - `ServerTurnApprovalResolvedEvent` - `ServerTurnQuestionAskedEvent` - `ServerTurnQuestionRejectedEvent` - `ServerTurnQuestionRepliedEvent` - `ServerTurnSnapshotEvent` - `ServerTurnStartedEvent` - `ServerTurnToolActivityEvent` - `ServerTurnToolCompletedEvent` - `ServerTurnToolRequestedEvent` - `ServerTurnUsageEvent` - `ServerVersion` - `SESSION_COMPACTION_PAGE_MAX` - `SESSION_EVENT_HEARTBEAT_MILLIS` - `SESSION_JOURNAL_PAGE_MAX` - `SESSION_TURN_PAGE_MAX` - `SessionAgentEntry` - `SessionAgentEntryValue` - `SessionAgentsResponse` - `SessionAgentsResponseValue` - `SessionBackgroundResponse` - `SessionCompactionHistoryBoundary` - `SessionCompactionHistoryBoundaryValue` - `SessionCompactionPosition` - `SessionCompactionPositionValue` - `SessionCompactionsContinuationQuery` - `SessionCompactionsFirstQuery` - `SessionCompactionsQuery` - `SessionCompactionsResponse` - `SessionCompactionsResponseValue` - `SessionCompactionState` - `SessionConfigurationResponse` - `SessionCursorEvent` - `SessionDeletedResponse` - `SessionDeletedResponseValue` - `SessionHistoryBoundary` - `SessionHistoryBoundaryValue` - `SessionListQuery` - `SessionListResponse` - `SessionPluginActivation` - `SessionPluginActivationValue` - `SessionPluginRevision` - `SessionPluginRevisionValue` - `SessionPluginsResponse` - `SessionPluginsStatus` - `SessionPluginsStatusValue` - `SessionResponse` - `SessionsApi` - `SessionSkillsResponse` - `SessionStreamEvent` - `SessionStreamEventValue` - `SessionTurnApprovalRequestedEvent` - `SessionTurnApprovalResolvedEvent` - `SessionTurnQuestionAskedEvent` - `SessionTurnQuestionRejectedEvent` - `SessionTurnQuestionRepliedEvent` - `SessionTurnReasoningDeltaEvent` - `SessionTurnRefusalDeltaEvent` - `SessionTurnsContinuationQuery` - `SessionTurnsFirstQuery` - `SessionTurnSnapshotEvent` - `SessionTurnsQuery` - `SessionTurnsResponse` - `SessionTurnsResponseValue` - `SessionTurnStartedEvent` - `SessionTurnState` - `SessionTurnTextDeltaEvent` - `SessionTurnToolActivityEvent` - `SessionTurnToolCompletedEvent` - `SessionTurnToolRequestedEvent` - `SessionTurnUsageEvent` - `SkillListEntry` - `SkillListEntryValue` - `StartMcpCredentialRequest` - `StartMcpCredentialRequestValue` - `SteerConflictError` - `SubmitContextCompactionRequest` - `SubmitContextCompactionRequestValue` - `SubmitMcpPromptRequest` - `SubmitMcpPromptRequestValue` - `SubmitPromptReceiptResponse` - `SubmitPromptRequest` - `SubmitPromptRequestValue` - `SystemApi` - `toSafeProviderSummary` - `TURN_EVENT_HEARTBEAT_MILLIS` - `TurnApprovalRequestedEvent` - `TurnApprovalResolvedEvent` - `TurnHeartbeatEvent` - `TurnHeartbeatEventValue` - `TurnNotActiveError` - `TurnNotSteerableError` - `TurnQuestionAskedEvent` - `TurnQuestionRejectedEvent` - `TurnQuestionRepliedEvent` - `TurnReasoningDeltaEvent` - `TurnRefusalDeltaEvent` - `TurnResponse` - `TurnSnapshot` - `TurnSnapshotEvent` - `TurnSnapshotEventValue` - `TurnSnapshotValue` - `TurnStartedEvent` - `TurnStreamEvent` - `TurnStreamEventValue` - `TurnTerminalEvent` - `TurnTextDeltaEvent` - `TurnToolActivityEvent` - `TurnToolCompletedEvent` - `TurnToolRequestedEvent` - `TurnUsageEvent` - `UnauthorizedError` - `UnavailableError` - `UpdateProviderConnectionRequest` - `ValidationError` - `WatchServerQuery` - `WatchSessionQuery` - `WatchTurnQuery` --- Generated from [`packages/protocol/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/protocol/README.md). --- # `@zephyrcloudio/harness-schema` Shared Effect schemas and TypeScript types for Ze Harness domain, persistence, and wire values. Import branded IDs and validated values from the package root: ```ts import { ModelRef, PermissionRule, SessionId, TurnId } from '@zephyrcloudio/harness-schema'; ``` This is the lowest workspace boundary and imports no other workspace package. It contains values, not runtime services, config loading, or transport behavior. Permission values include ordered rules, Tool projections, durable requests and replies, saved Session grants, versioned Session configuration, and the exact effective policy stored with admitted work. Evaluation and persistence remain Core responsibilities. Catalog and connection schemas include `CatalogSnapshot`, `SafeProviderConnection`, `HostCatalogRevision`, and `ConnectionId`. They define the host-scoped model discovery and provider connection contracts used across all transports without exposing secrets. `TURN_FAULT_CODES` lists every `TurnFault.code` the harness itself mints, and `KnownTurnFaultCode` is its union. The harness types its own fault constructors against the list, so a new harness code cannot ship without appearing here. Consumers that render faults can map it exhaustively, for example `const labels = { ... } satisfies Record`, and fall back for anything else: `TurnFault.code` stays an open string because plugins, Tools, and providers mint their own codes, and the MCP runtime forwards transport reasons as `mcp_`. --- Generated from [`packages/schema/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/schema/README.md). --- # `@zephyrcloudio/harness-sdk` The embedded Node.js composition for Ze Harness. `createEmbeddedHarness` opens SQLite, uses the Node Server host to create the real Fetch handler, and returns a typed Client using injected Fetch. `listenEmbeddedHarness` can add a loopback listener to the same instance. ```ts import { createEmbeddedHarness, listenEmbeddedHarness } from '@zephyrcloudio/harness-sdk'; ``` Creation is scoped and requires `cwd` plus an explicit `databaseFilename`. Several processes may open the same database file; readers run concurrently and writers serialize. Provider keys live in the global `auth.json` (override with `authFilePath`), shared by every database. Plugins supplied by config or `inlinePlugins` are trusted and unsandboxed. The SDK re-exports `authFileDataRoot()` and `defaultAuthFilePath()` from Server Node. The shared state directory is `$XDG_DATA_HOME/ze-harness` when `XDG_DATA_HOME` is absolute; otherwise it is `~/.local/share/ze-harness` on Unix and `%LOCALAPPDATA%\ze-harness` on Windows (home `AppData\Local` fallback). Existing Windows legacy state is retained while its daemon is live, then migrated on a subsequent invocation with a compatibility junction. Conflicting roots are reported explicitly. See [Server Node's state-path guidance](/docs/server-node.md) for migration and locked-directory handling. `databaseFilename` remains caller-supplied. Use `sessionPluginArtifacts` to configure trusted Session package resolution: ```ts createEmbeddedHarness({ cwd: process.cwd(), databaseFilename: './harness.sqlite', config: { permissions: [{ action: '*', resource: '*', effect: 'ask' }], permissionMatching: { caseSensitive: true }, }, sessionPluginArtifacts: { cacheDirectory: '/path/to/cache', packageManager: { command: 'npm', registry: 'https://registry.npmjs.org' }, resolveTimeoutMs: 60_000, installTimeoutMs: 120_000, }, }); ``` The limits are optional and default to unlimited. See the Server Node README for source identity and trust details. The embedded SDK uses the same Node Server configuration path as `serve`, including ordered `permissions`, `permissionMatching`, and `approval.timeoutMs`. Native providers (OpenAI, OpenRouter) and the file-backed Provider Authority are composed automatically on the embedded instance. Session configuration and durable saved permissions are available through the returned Client. Pass `credentialMode: 'injected-only'` with `providerCredentials`, `mcpCredentialResolver`, `mcpOAuthTokenResolver`, `contentResolver`, `toolDispatch`, and `providerDefinitions` to run as an injected-port host with no ambient credential discovery. See the [embedded quickstart](https://github.com/ZephyrCloudIO/ze-harness/blob/main/README.md#embedded-sdk). ## Public TypeScript exports - `authFileDataRoot` - `ClientConnection` - `ClientDisconnection` - `createEmbeddedHarness` - `CreateEmbeddedHarnessOptions` - `Daemon` - `DAEMON_BASE_PORT` - `DAEMON_FILE_NAME` - `DAEMON_LOG_FILE_NAME` - `DAEMON_MAX_PORT` - `DAEMON_STARTUP_FILE_NAME` - `daemonDirectory` - `DaemonError` - `DaemonOptionsMismatch` - `DaemonStartObserver` - `DaemonStartupProgress` - `DaemonStartupReporter` - `DaemonStartupStep` - `DaemonStatusValue` - `DaemonTransportOptions` - `DaemonVersionMismatch` - `DECLARATIVE_SESSION_PLUGIN` - `defaultAuthFilePath` - `EmbeddedHarness` - `EmbeddedHarnessError` - `listenEmbeddedHarness` - `ListenOptions` - `makeDaemonLog` - `makeDaemonStartupReporter` - `makeMcpSecretCredentialResolver` - `NodeListener` - `resolveProjectIdentity` - `ServerListenError` - `startDaemonLogTrim` - `StorageMigrationProgress` --- Generated from [`packages/sdk/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/sdk/README.md). --- # `@zephyrcloudio/harness-server` Runtime-neutral Fetch HTTP handling for Ze Harness. `createServerHandler` exposes injected `SessionServices` through validated HTTP routes and SSE. ```ts import { createServerHandler } from '@zephyrcloudio/harness-server'; ``` The Server does not choose a storage implementation or import Node.js. Node config discovery, trusted plugin loading, and loopback hosting live in `@zephyrcloudio/harness-server-node`. Long-lived event streams stay observable while nothing changes. A caught-up Session stream repeats its `session.cursor`, a server stream repeats its `server.cursor`, and a Turn stream sends `turn.heartbeat` naming the revision it already sent, at least every 15 seconds. All come from an idle re-read of activity, which also recovers a missed wake. The SSE projections are exported for embedding hosts that serve them outside the Fetch handler: `turnEventStream`, `sessionEventStream`, and `serverEventStream`. The server stream wraps `SessionServices.observation.watchServerActivity` and projects Turn rows through the same functions as the Session stream, with one running-Turn memory per Session. A Session's first event on a stream is always `session.created`: a Session row is `session.created` the first time the stream sees that Session and `session.title` afterwards (storage stamps Session rows only on creation and title changes), and a Turn row for a Session the stream has not announced yet is preceded by a `session.created` read from the current Session, stamped one cursor before the Turn's so a resume between the two replays the Turn. A stream resumed after an existing Session was retitled therefore repeats `session.created`; consumers upsert by `session.id`. `HarnessServerOptions` accepts an optional `providerAuthority` service to serve model discovery and provider connection management routes over the versioned v1 protocol. The `/v1/runtime/models/events` and `/v1/servers/:server/sessions/:sessionId/models/events` routes are single-shot current-revision checks, not long-lived subscriptions: each emits one revision event and closes. Clients re-fetch the model snapshot endpoints for updates. The portable Streamable HTTP MCP factory in `./mcp-host` requires `McpHostBackgroundWork`. This host port keeps an abort-ignoring fetch and any late response-body cleanup owned after the transport scope closes. Node and Worker packages supply their platform-specific implementations. MCP catalog pages validate their standard fields and accept additional server metadata. A server may omit `resources/list`, `resources/templates/list`, or `prompts/list`: a method-not-found response to the first page of one of those optional lists is treated as an empty catalog for that list. Other request errors, `tools/list` failures, and malformed catalog fields still fail discovery. ## Dashboard `createServerHandler({ dashboard: { server } })` serves the dependency-free dashboard (`src/dashboard.html`, inlined at build time by `build/dashboard-asset.mjs`) at exactly `GET /dashboard`, before routing and without authentication. The Node daemon and `WorkerHarnessHost` both serve this one asset. The page is a single self-contained HTML document that does not assume its host: - **Server identity.** The handler replaces `%ZE_HARNESS_SERVER%` in `` with `dashboard.server`. A page served without a valid name shows an error instead of guessing. - **Relative requests.** Every call is document-relative (`v1/servers//…`, `v1/logs`), so the page works under any path prefix; at `/dashboard` they resolve to `/v1/…`. - **Login only when needed.** On load the page lists Sessions with the tab's stored bearer token, or none, and shows the token form only on `401`. A host that vouches for its caller never shows it, and the sign-out chip appears only while a token is in use. When storage is unavailable (an opaque-origin frame) the token is kept in memory only. - **Optional logs.** The log panel hides once `v1/logs` answers `404` or `503`. - **Headers.** `content-security-policy: default-src 'none'; style-src 'unsafe-inline'; script-src 'sha256-'; connect-src 'self'; base-uri 'none'; form-action 'none'`. The script hash is computed at build time, so an embedder that appends its own script hashes to `script-src` (CEF does for injected scripts) keeps the page's script running. There is no `frame-ancestors` or `X-Frame-Options`, so any parent may frame the page. The page's API calls must be same-origin with it (`connect-src 'self'`). An embedder serving it from a custom URL scheme (such as `tap-harness://localhost`) must register that scheme so its pages get a scheme/host/port origin; a page with an opaque origin has no `'self'` to match. ### Theme Every color, font, type size, weight, radius, shadow, and spacing value is a `--zh-*` custom property on `:root`; the defaults are the built-in dark theme. The page has one color scheme (dark) and takes no scheme parameter: an embedder restyles it, including a light palette and `--zh-color-scheme: light`, through tokens. An embedding parent window posts: ```ts iframe.contentWindow.postMessage( { type: 'ze-harness:theme', tokens: { '--zh-bg': '#ffffff', '--zh-text': '#1f2328' } }, '*', ); ``` The page applies only messages from its parent window, and only entries whose key is one of the tokens below and whose value is a string; everything else is ignored. The message carries theme, never data. Post it after the frame's `load` event; later messages override earlier ones. | Group | Tokens | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Scheme | `--zh-color-scheme` (`dark`) | | Colors | `--zh-bg`, `--zh-panel`, `--zh-inset`, `--zh-border`, `--zh-text`, `--zh-dim`, `--zh-faint`, `--zh-accent`, `--zh-ok`, `--zh-warn`, `--zh-bad`, `--zh-steer` | | Fonts | `--zh-font-sans`, `--zh-font-mono` | | Type size | `--zh-text-10`, `-10-5`, `-11`, `-11-5`, `-12`, `-12-5`, `-13`, `-14`, `-15`, `-16`, `-17` | | Type | `--zh-weight-normal`, `--zh-weight-medium`, `--zh-weight-semibold`, `--zh-leading`, `--zh-leading-prose`, `--zh-tracking-label` | | Radius | `--zh-radius-2`, `-3`, `-4`, `-5`, `-6`, `-8`, `--zh-radius-full` | | Shadow | `--zh-shadow-card`, `--zh-shadow-focus`, `--zh-shadow-focus-thin` | | Spacing | `--zh-space-1`, `-2`, `-3`, `-4`, `-5`, `-6`, `-7`, `-8`, `-9`, `-10`, `-12`, `-14`, `-16`, `-18`, `-20`, `-24`, `-28`, `-32`, `-44`, `-80` | Numbered type size, radius, and spacing tokens are named for their default pixel value (`--zh-text-10-5` is `10.5px`). Tinted surfaces (selection, pills, error banners, focus ring) mix the color tokens, so they follow a recolored palette. ## Public TypeScript exports - `AddSessionPluginAdminInput` - `bearerUnauthorizedResponse` - `ClientWorkdirResolver` - `createServerHandler` - `DaemonLogServiceApi` - `DashboardOptions` - `HarnessServer` - `HarnessServerOptions` - `isDashboardRequest` - `makeLazySessionPluginAdmin` - `makeSessionPluginAdmin` - `ProviderModelRefresher` - `publicSessionPluginInventory` - `RefreshSessionPluginAdminInput` - `RuntimeAdmin` - `RuntimeAdminUnavailable` - `serverEventStream` - `ServerEventStreamInput` - `SessionCatalogAdoptionStore` - `sessionEventStream` - `SessionEventStreamInput` - `SessionPluginActivationValue` - `SessionPluginAdmin` - `SessionPluginAdminConflict` - `SessionPluginAdminFailure` - `SessionPluginAdminInvalid` - `SessionPluginAdminStorageFailure` - `SessionPluginAdminUnavailable` - `SessionPluginRevisionValue` - `SessionPluginsStatusValue` - `SessionProjectResolver` - `turnEventStream` - `TurnEventStreamInput` - `verifyBearerRequest` --- Generated from [`packages/server/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/server/README.md). --- # `@zephyrcloudio/harness-server-node` Node.js hosting for Ze Harness, including filesystem config discovery and trusted plugin loading. `createServerHost` composes config, plugins, one Core runtime, and a caller-supplied `StorageService`. `listen` binds the runtime-neutral Fetch handler to loopback. Standard tools (`pause`, `read`, `write`, `edit`, `shell`, `webfetch`, `question`) and the `build`/`plan`/`general`/`explore` agents ship built in. No config entry is needed, and the built-in is always on: a legacy `"plugins": ["@zephyrcloudio/harness-plugin-core-tools"]` entry is ignored, a legacy entry with options fails closed, and a file-path copy of core tools fails closed on duplicate registration instead of replacing the built-in. Restrict individual tools with `deny` permission rules. ```ts import { createServerHost, listen } from '@zephyrcloudio/harness-server-node'; ``` The listener accepts only `127.0.0.1` or `::1`. Loaded plugins run unsandboxed with full process authority. `harnessConfigJsonSchema()` returns a JSON Schema (draft 2020-12) for `config.json` and `ze-harness.config.json`, generated from the schema that parses them. It describes the accepted shape; checks that run only in code, such as plugin source resolution, are not part of it. ze.dev publishes it at `/ze-harness.config.schema.json`. ## Durable state paths `authFileDataRoot()` owns the shared host data root; `defaultAuthFilePath()` and `daemonDirectory()` resolve files beneath its `ze-harness` directory. An absolute `XDG_DATA_HOME` overrides the root on every platform. Relative values are ignored. Without that override, Unix uses `~/.local/share/ze-harness`; Windows uses `%LOCALAPPDATA%\ze-harness`, falling back to `%USERPROFILE%\AppData\Local\ze-harness` when `LOCALAPPDATA` is missing or relative. Config and cache paths are unchanged. On Windows, an existing `~/.local/share/ze-harness` is renamed to the native location only when the target does not exist and no registered legacy daemon PID is live. A junction preserves the old location for launchers and stored absolute paths. While the legacy daemon is live, every default state path stays on the legacy root for that invocation; after stopping it, a subsequent invocation retries migration. An unreadable or invalid registration prevents migration with an explicit error. If a locked directory or a cross-volume rename prevents migration, a warning names the retained legacy root. Close processes using it and retry, or manually move it while all local servers and embedded database users are stopped and preserve the old path with a junction. Independent legacy and native directories cause an error rather than silently selecting one; reconcile them or explicitly select a root with an absolute `XDG_DATA_HOME`. Existing junctions to the native directory are accepted. Other legacy links, dangling junctions, and non-directory paths require explicit resolution. ## Local daemon `Daemon` manages one shared background server per machine: `transport()` (and `start()`) reuse the healthy registered server or spawn `serve --register` detached and wait for its health check. Registration lives in `server.json` (`{id, version, url, pid}`, mode `0600`) next to a `daemon-token` bearer secret and `daemon.log` under the [shared durable state directory](#durable-state-paths). Ports scan up from 4096. A racing spawner that loses the registration file terminates itself, and a server whose registration is replaced requests graceful host shutdown on a 10-second heartbeat. `register(address, options, onShutdown)` requires the host's shutdown callback; it must close the host scope rather than kill the process. The CLI supplies that callback. New daemons advertise shutdown support in their registration. `stop()` authenticates the daemon, sends `POST /_daemon/shutdown` with its bearer token, and waits for the PID to exit before permitting a restart. The endpoint is enabled only on the shared daemon's loopback listener and acknowledges with `202` before closing scopes. Self-termination uses the same host shutdown path on every platform, including Windows. Older daemons or failed shutdown requests fall back to `SIGTERM`; a still-running, matching registration can be force-killed after the existing 15-second grace period. Registration stays discoverable during cleanup and is reaped after process exit. Daemon servers set `authToken` on the host so loopback clients must present the shared secret; foreground `serve` without `--register` keeps serving unauthenticated. ## Daemon structured logging The daemon writes opencode-style logfmt lines to `daemon.log` (one stable file, mode `0600`): ```text timestamp=2026-09-29T07:00:00.000Z level=INFO run=a1b2c3d4 service=daemon event=client.connect connectionId=… remoteAddr=127.0.0.1 remotePort=51234 ``` Every line carries a fixed `timestamp level run service` prefix (`run` is a short boot id minted per `serve`) followed by `event` plus caller fields. Key events at `info`: `daemon.start` (after the listener binds, with its URL), `client.connect`, `client.disconnect` (with `durationMs`), and `turn.submitted` (server, session, turn, delivery, model when known, client id — never prompt text, tokens, or argv). Hosts pass the log to `createServerHost` as `daemonLog`; only `serve --register` does, so `turn.submitted` and `GET /v1/logs` exist only on the daemon, while foreground `serve` writes its connection lines to its own stderr. The daemon trims an overgrown log in place once per boot and hourly — a fixed cap (10 MiB) keeping the newest 5 MiB, line-aligned, with no env override by product decision — so the daemon's appending stderr keeps writing to the same file. The daemon child's stdout (its bound-URL line) shares the file, and the reader skips any line that is not a complete logfmt entry. `GET /v1/logs` (`limit` default/max 500, `level` floor) serves a bounded newest-first page with a `truncated` flag (older bytes exist beyond the read window); hosts without a daemon log (Worker, embedded, foreground serve) report `service_unavailable`. The dashboard renders the tail below the sessions grid with a level filter and pause toggle on the same 5-second poll. ## Dashboard The daemon passes `dashboard: true` to `createServerHost`, which serves the [`@zephyrcloudio/harness-server` dashboard](/docs/server.md) at `GET /dashboard` for the `local` server. The page is public and asks for the daemon token only when the API answers `401`; the loopback-only listener still applies its Host and Origin checks first. ## Workspace instructions Before admitting a prompt, Harness loads the non-empty `AGENTS.md` in the Session workdir. The file is applied as a developer instruction and re-read for every new prompt, so an edit or removal applies to the next admitted Turn. The admitted content is retained with that Turn for recovery and context compaction. The file must fit the existing 50 KiB workspace text-read limit; an unreadable, invalid, or oversized file fails prompt admission instead of being silently dropped. Successful standard file reads and directory listings below the Session workdir also discover any nested `AGENTS.md` files between the target and that workdir, nearest-first. Newly discovered files are attached to the read result. For direct reads, they are not attached again while that result remains in model-visible history. A later read can load them again after compaction or revert removes the result. Nested edits therefore take effect after the prior result leaves context, or in a new Session. Instructions discovered by script child reads are also retained in Session chronology, independently of the script's returned value. They appear as developer messages after the tool round. Known script-discovered files are re-read at new prompt and explicit compaction admission; edits and removals apply there while the currently admitted Turn retains its captured content. ## Configured agents File discovery belongs to the Node server host, including embedded use. Remote clients use the server's agent catalog, not configuration files on the client's machine. Worker deployments do not discover these local JSON or Markdown files. Define agents in the `agents` map in global `config.json`, project `ze-harness.config.json`, or programmatic configuration. `defaultAgent` selects the default primary-capable agent: ```json { "defaultAgent": "writer", "agents": { "writer": { "description": "Writes documentation", "mode": "primary", "system": "Write clear, concise documentation.", "model": "openai/gpt-5-mini" }, "build": { "description": "Our customized coding agent" } } } ``` Markdown agents live under `$XDG_DATA_HOME/ze-harness/agents` (or `~/.local/share/ze-harness/agents` when unset or non-absolute), using the existing data-directory system; there is no separate `ZE_DATA_DIR` environment variable. Each regular `.md` file uses YAML frontmatter for options and its non-empty trimmed body for the system prompt. Files without an opening frontmatter delimiter or a valid agent filename are ignored. Unreadable files, malformed options, and missing closing delimiters are silently skipped, matching OpenCode's best-effort loading: ```markdown --- description: Reviews changes for correctness mode: subagent --- Review the changes and report concrete defects with file references. ``` Symlinked files and directories are followed. Broken links and directory cycles are silently skipped. Relative paths determine agent IDs: `team/reviewer.md` defines `team/reviewer`, as in OpenCode. Discovery reads files sequentially in lexical order. YAML rejects duplicate keys and uses the parser's default alias-expansion limit. Both formats accept `displayName`, `description`, `system`, `model`, `mode`, ordered `permissions` rules, and `disabled`. A non-empty Markdown body replaces frontmatter `system`; an empty body leaves it unchanged. IDs use lowercase alphanumeric segments separated by `.`, `_`, or `-`; `/` separates nested directory namespaces. New custom agents require non-empty instructions and default to `primary`, their ID as display name, and a generated description. Patches to registered agents may omit instructions. Disabling an agent preserves its declaration for a later explicit `disabled: false` patch. Disabling an absent agent is a no-op. Model, instructions, and permissions feed the existing Turn and subagent runtime. Other OpenCode options, such as `hidden`, `color`, `steps`, and provider `request` overlays, are not supported and are rejected rather than ignored. Precedence is plugin/built-in declarations → global JSON → global Markdown (lexically sorted) → project JSON → programmatic config. Definitions merge by ID: specified fields replace earlier values, omitted fields remain, and permission rules append. `disabled: true` hides the agent; a later explicit `disabled: false` re-enables it. Duplicate declarations between plugins still fail. A configured default must exist and support primary use; otherwise activation fails. Without one, a plugin's default is preserved, then a plugin's `fallbackDefault` agent is preferred, then the first primary-capable agent. The built-in plugin marks `build` as its fallback default. Multiple fallback candidates are resolved by lexical ID order; an explicit default always takes precedence. Disabling an explicitly configured default requires selecting another default in the same or a later layer; otherwise activation fails rather than silently choosing a different agent. Runtime reload rescans the directory and creates a new immutable host baseline, including for Session overlays. Existing Turns retain their leased baseline. Malformed or unreadable Markdown definitions are silently skipped and do not reject the reload. JSON configuration failures remain explicit and leave the prior generation active. A Session pinned to a removed agent fails its next Turn explicitly rather than silently running without that agent. Nested agent IDs require Harness HTTP protocol version 2. Update remote clients alongside the server; older clients cannot decode IDs containing `/`. ## Local Skills Server Node discovers standard `SKILL.md` files from these locations: - `~/.ze-harness/skills` and project `.ze-harness/skills` - `$XDG_CONFIG_HOME/opencode/{skill,skills}` and project `.opencode/{skill,skills}` - global and project `.agents/skills` and `.claude/skills` Project directories are searched from the repository root to the active working directory. Add more local directories with `skills`; relative paths resolve from the active working directory: ```json { "skills": ["./team-skills", "~/shared/skills"] } ``` Each discovered file must be named `SKILL.md` and include YAML frontmatter with string `name` and `description` fields. Block descriptions and nested `metadata` are supported. Only safe metadata is advertised; the Markdown body and supporting-file manifest are returned when a Turn selects the Skill or the model calls `skill`. Runtime reload rescans every directory. Later sources replace duplicate discovered names and emit a warning. Malformed files are skipped without removing valid Skills. HTTP skill catalogs remain available only through the existing Harness plugin source contract; OpenCode `index.json` catalogs are not loaded. ## Provider authority and configuration Server Node compiles OpenAI-compatible providers from the `providers` map in `ze-harness.config.json`. A catalog provider such as `deepseek` needs no TypeScript: ```json { "model": "deepseek/deepseek-v4-flash", "providers": { "deepseek": {} } } ``` The models.dev catalog supplies known endpoints, models, metadata, and credential environment names. Node hosts load a validated cache at `$XDG_CACHE_HOME/ze-harness/models.json` (default `~/.cache/ze-harness/models.json`) when its data was published at or after the bundled snapshot was built, and use the bundle otherwise: the bundle is the floor, and when the cache was fetched does not count. The publish time is the response's `Last-Modified`. models.dev sends none, so for it a refresh that sees changed content records the previous check as a proven lower bound; a revalidation (`304` or unchanged content) never makes data newer, and a first fetch or a cache written by an older build proves nothing until a later refresh sees the content change. Missing or invalid cache data does not prevent startup. The shared local daemon refreshes `https://models.dev/api.json` in the background at startup and every hour, using conditional requests when available. Refresh failures preserve the previous cache and log a warning. Refreshed data takes effect on the next daemon start or `ze-harness runtime reload`; refresh never reloads plugins or changes an active Turn's catalog. Embedded Node hosts read the cache but do not start periodic refreshes. Worker hosts use the bundled snapshot unless the embedder supplies a fetched catalog, which follows the same precedence (see the [Worker host README](/docs/worker-host.md)). | Environment variable | Default | Purpose | | --------------------------------- | ----------------------------- | ---------------------------------------------------------------------- | | `ZE_HARNESS_DISABLE_MODELS_FETCH` | unset | Set to `1` to disable background requests; cached data remains usable. | | `ZE_HARNESS_MODELS_URL` | `https://models.dev/api.json` | Full URL of the models.dev-format catalog to refresh (see below). | `ZE_HARNESS_MODELS_URL` must serve a models.dev `api.json` document; the refresh normalizes it like models.dev. The published Ze catalog uses that format. Maintainers update the bundled JSON and build the published catalog from the repository root with `pnpm update:models-dev`. See the [Core README](/docs/harness.md) for the pipeline, corrections, and publishing. A custom provider without a `models` map discovers its inventory from its authenticated `GET /models` endpoint at startup and runtime reload. For example: ```json { "providers": { "local-proxy": { "definition": { "displayName": "Local proxy", "baseURL": "http://127.0.0.1:8317/v1", "protocol": "responses", "authentication": { "kind": "bearer" }, "credentials": { "environment": ["LOCAL_PROXY_API_KEY"] } } } } } ``` Stored provider keys work for discovery too. With no key, the provider remains visible for connection; reload the runtime after connecting to populate its models. Set `discovery: true` inside the definition to discover a known catalog provider or combine discovery with explicit model overrides. Discovery adds models to the catalog instead of replacing its list, and configured model overrides take precedence. Expected discovery failures log a warning and retain catalog and configured models. `discovery: false` uses only the catalog and configured models. The endpoint supplies model IDs. Ze enriches recognized IDs with models.dev capabilities and limits, preferring the configured catalog provider or the endpoint's `owned_by` provider. Qualified IDs and unambiguous bare IDs can also supply metadata. Ze never inherits another provider's endpoint or protocol. Unknown discovered or configured IDs support text input/output and tools by default; explicit model overrides can disable tools or supply image metadata. An unadvertised context or output limit is represented by zero, so Ze does not impose an invented limit. Context compaction requires known context and output limits. Discovery also reads `context_length` or `max_model_len` when the endpoint supplies it. Reload repeats discovery and creates a new runtime generation. Existing Turns retain their leased catalog; a failed reload keeps the previous generation. The model picker's refresh reads the current generation and does not itself perform a runtime reload. You can also declare the transport and models explicitly. For example, this runs an installed `gemma4:latest` model through Ollama's local OpenAI-compatible endpoint: ```json { "model": "ollama/gemma4", "providers": { "ollama": { "definition": { "displayName": "Local Ollama", "baseURL": "http://127.0.0.1:11434/v1", "protocol": "chat", "authentication": { "kind": "none" }, "models": { "gemma4": { "upstreamId": "gemma4:latest", "capabilities": { "input": ["text"], "output": ["text"], "tools": true }, "limits": { "contextUnits": 131072, "outputUnits": 8192 } } } } } } } ``` Replace the model name and metadata with values for your installed model. A keyed provider instead uses bearer or header authentication and lists its secret names under `credentials.environment`. - `chat`, `responses`, and `messages` use Ze's native wire adapters. Supported models.dev package and shape metadata selects the protocol per model; unknown explicit packages and shapes are rejected. - Definitions can set routes, bearer/header/no authentication, protocol-specific authentication and headers, static headers, Session-derived headers, credential environment names, model body defaults, and model metadata. `options` can override `baseURL`, headers, request timeout, and the configured `apiKey`. - Credentials resolve from valid stored connections, configured `options.apiKey`, and the definition's environment variables. Missing credentials remain a graceful unavailable state. On the embedded Node host, stored connections live in one global `auth.json` (opencode's exact shape, owner-only permissions) shared by every database; `ZE_HARNESS_AUTH_CONTENT` overrides it for scripted runs. An `auth.json` that is not a JSON object fails every provider connection operation and is never overwritten. The Worker keeps its singleton Durable Object authority. - Injected-only mode (`credentialMode: 'injected-only'` with injected `providerCredentials`, `mcpCredentialResolver`, and `mcpOAuthTokenResolver` ports) disables stored records, configured keys, env reads, the default env MCP resolver, and the stored MCP OAuth lifecycle: calls without port-supplied authority fail instead of resolving keys elsewhere. Embedding hosts additionally inject `contentResolver` (opaque attachment references, resolved only after visibility filtering), `toolDispatch` (host custody around final dispatch after Harness permission resolution), and `providerDefinitions` (embedder-implemented providers keyed by configured provider id, compiled into the plan; unknown ids fail startup). - Provider connection management routes are trusted on loopback like every other Node surface. - Without a `providers` map, OpenAI, OpenCode Go, and OpenRouter are enabled. With a map, only listed entries not marked `enabled: false` are enabled. An empty map fails startup instead of silently restoring defaults. ## Hybrid CodeMode Agents can combine tool calls using [CodeMode](/docs/harness.md). It is on by default. To turn it off, set `codemode.enabled` to `false` in `ze-harness.config.json`: ```json { "codemode": { "enabled": false } } ``` Existing tool permissions still apply. ## Permission configuration Strict JSON configuration accepts ordered `permissions` rules, optional `permissionMatching.caseSensitive`, and `approval.timeoutMs`. Global, project, and programmatic config layers append permission rules in that order, so the last matching rule wins. Ze defaults allow every Tool inside the session workdir and ask only for `external_directory` paths and `.env` reads; the resolved Server rules append after them, then Session and Agent rules. Omitted approval timeouts remain unlimited. Matching is case-sensitive by default, except on Windows where it defaults to case-insensitive. For `read`, `edit`, and `external_directory` rules, a leading `~`, `~/`, `$HOME`, or `$HOME/` in the resource is expanded to the server user's home directory when configuration loads. Shell resources are raw command text and are never expanded. Each Session's workdir is its permission boundary. File resources inside the workdir match workdir-relative, so a rule for one scoped Session reads the same in another; paths outside the workdir match absolute and also require `external_directory` for the parent directory. Sessions at the server root (`.`, the default) behave exactly as before. The `shell` tool additionally scans file-command arguments for outside-workdir paths and projects `external_directory` for them; that scan is a heuristic, not containment — the shell still runs with the server user's full authority. ```json { "permissions": [ { "action": "*", "resource": "*", "effect": "ask" }, { "action": "read", "resource": "*", "effect": "allow" } ], "permissionMatching": { "caseSensitive": true }, "approval": { "timeoutMs": { "kind": "unlimited" } } } ``` Server Node cannot attest its complete earliest-process loader state. Every Node generation therefore has unavailable cross-restart continuity, including empty, configured, inline, and reloaded generations. Fresh activation still works, but pending Turns are fenced before provider I/O after a restart. Static hosts such as the Worker can use deployment metadata as a verified generation attestation. ## Session plugin sources The Node host can load trusted Session plugins from npm packages, Git repositories, and local files. It resolves a package to an exact version and registry integrity value, a Git source to an exact commit, and a file to a SHA-256 snapshot before recording the Session revision. Retries and restarts load that recorded identity. Only an explicit Session plugin refresh may resolve a newer artifact. Package installation, module evaluation, setup, and cleanup run as trusted, unsandboxed host-process code. Package scripts are not suppressed. Credentials stay in the host package-manager and Git configuration; do not put credentials in source descriptors or plugin options. `NodePluginArtifactOptions` configures the cache directory, package-manager command, registry, leading command arguments, and optional resolution or installation time limits. Both limits are unlimited by default. The cache defaults to `ze-harness-plugin-artifacts` under the operating system temporary directory. ## Image attachment limits `images.maxBase64Bytes` caps the base64 payload of one attached image, in bytes. It accepts the usual execution-limit shape and is unlimited when omitted. URL images are never fetched to be measured, so the cap applies to base64 attachments only. Over-cap prompts fail explicitly before persistence or model IO. ```json { "images": { "maxBase64Bytes": { "kind": "finite", "value": 5242880 } } } ``` ## MCP stdio lifecycle Local MCP servers run as owned process trees. Closing a connection stops write admission, settles accepted writes, ends stdin, and waits for the writable stream to confirm closure. The default unlimited path actively destroys a blocked writer before process-tree termination. A blocked write cannot detach or prevent termination; it fails with the connection while shutdown remains owned. Cancelling a caller stops only that caller's wait: a write already accepted by the transport remains owned until the Node writer callback or an exact transport/process failure settles it. Shutdown actively closes the writer and reaps the tree, so a backpressured write cannot create a circular wait. Only after acknowledged stdin closure does `stdinCloseMs` measure how long the server may remain alive. With the default unlimited limits, an owned close still starts termination immediately and waits for the complete tree to be reaped. Unix sends `SIGTERM` to the process group after a configured `stdinCloseMs` and `SIGKILL` after `sigtermMs`. It does this through a retained supervisor that creates the process group before starting the target. The supervisor remains the group leader through the final signal and addresses its own current group. A watchdog in that group takes over the final signal if the supervisor fails. The host retires ownership only after the supervisor/watchdog settlement pipe closes and the supervisor exits. It does not poll or signal a reusable numeric process-group ID after those owners exit. The supervisor starts with only temporary-directory variables; target preloads, search paths, and credentials are passed only to the target after group ownership exists. Windows launches a long-lived supervisor in a Job Object. It joins the Job Object before it receives the target command or environment. The supervisor starts with only trusted Windows system and temporary-directory variables, so target preloads, search paths, and credentials cannot run in it. After a configured `stdinCloseMs`, it terminates a still-running direct child through the owned process handle. It waits the full remaining `sigtermMs` grace period for the owned tree, then terminates the Job Object. No PATH-resolved helper or reusable PID controls the target, and descendants remain owned after the direct child exits. On Unix, direct-child `exit` is observed separately from stdio `close`. Orphan cleanup therefore starts even when a descendant inherited stdout or stderr and keeps those pipes open. The target process does not inherit the host's full environment. The host copies only `PATH`, `TMPDIR`, `TEMP`, and `TMP`; Windows also receives `PATHEXT`, `SystemRoot`, and `WINDIR`. These values support executable lookup and ordinary platform runtime/temp discovery. Explicitly configured MCP environment references are resolved at the transport boundary and override the allowlist, including case-insensitive overrides on Windows. Windows rejects configured keys that differ only by case, such as `PATH` and `Path`, before resolving credentials or starting a process. Variables such as `HOME`, `NODE_OPTIONS`, `NODE_AUTH_TOKEN`, provider API keys, cloud credentials, and dynamic-loader settings are absent unless explicitly configured. This is credential isolation, not a security sandbox: the selected MCP command remains trusted, unsandboxed code with the authority of its operating-system process. `cleanupMs` bounds final tree reaping and stdout closure. `shutdownMs` bounds the complete sequence, including writer drain and acknowledged stdin closure. The first exact configured limit reached is preserved as the terminal `limit_reached` result. All lifecycle limits are unlimited unless the caller configures them. ## MCP Streamable HTTP ownership Each JSON POST response must be a JSON-RPC success or failure for that exact POST request ID. Other messages and malformed, retired, or mismatched IDs fail the connection instead of leaving a pending request. Request SSE streams apply the same exact ID and session-generation fence before publication, and standing GET streams reject responses rather than routing them to a concurrent POST. Fetch remains abortable, response-body reads remain interruptible, and every acquired body is owned before the fetch promise resumes the caller. A configured finite `cleanupMs` or `shutdownMs` also bounds an unresolved fetch that ignores abort; both limits remain unlimited by default. Redirects use canonical URL identity for cycle detection. Superseded bodies are released before the next hop, and credential and session headers are stripped when the resource changes. Redirect hops are unlimited by default. A caller may set `redirectHops` to a finite value; exhaustion returns a typed `limit_reached` result with that configured value. A first-time OAuth challenge carries no OAuth or configured credential headers. Those headers are attached only after authorization. ### Provider login Node hosts with provider authority storage run provider logins on the host. A provider supports login when its configuration (or built-in preset) declares `login`: either `{ "device": { "discoveryURL": "/auth/device" } }` for a provider that publishes an OAuth 2.0 device login descriptor (a path is resolved against the provider base URL; an optional `notice` with `text` and `links` is returned on the login method for clients to show before the code), or `{ "module": "" }` for a product-owned login such as the ChatGPT login used by `openai`. - `POST /v1/runtime/providers/:providerId/logins` starts a login and returns the prompt (verification URL and user code). Starting a new login cancels the provider's pending one. - `GET /v1/runtime/providers/:providerId/logins/:loginId?waitMs=N` returns the status, waiting up to `N` milliseconds (at most 30000) for it to change: `pending`, `connected` with the safe connection, or `failed` with `denied`, `expired`, `cancelled`, or `error` and a message. - `DELETE /v1/runtime/providers/:providerId/logins/:loginId` cancels a pending login. Polling, `slow_down` backoff, code expiry, token exchange, and credential writes happen on the host; device codes and tokens never leave it. Responses expose connection metadata only. Stored logins refresh on demand through the OAuth 2.0 `refresh_token` grant at the token endpoint recorded with the login. Model requests resolve the credential per request, refreshing when the token is within two minutes of its stored expiry or JWT `exp`; one refresh per provider runs at a time, storage is re-read first so a rotation made by another request or process is reused, and refresh plus persistence complete even if the requesting Turn is cancelled. A request rejected with HTTP 401 forces one refresh and retry. A rejected refresh token stops further refresh attempts until the provider is logged in again. Separate embedded hosts sharing an auth file still have last-writer-wins storage semantics; use the shared daemon for local commands. Injected-only hosts do not use stored logins or offer login. ### MCP OAuth credentials A remote MCP server configured with an OAuth credential reference resolves through the composed credential lifecycle, not a static value. When the host composes no lifecycle — no `mcpCredentials` option on the Node host, no composed runtime in the Worker Session object — the reference is unusable by construction and the transport fails with the typed `authorization_required` outcome. It is never treated as configured, working functionality, and configuration itself stores only the opaque reference; tokens, client secrets, and codes never appear in config, events, diagnostics, or logs. The loopback callback returns a self-contained success or error page. If an authorization server advertises RFC 9207 issuer support, its callback must include the exact `iss` value. Otherwise `iss` may be omitted; any supplied value is still checked exactly. Scope selection uses the MCP challenge first, then the protected resource's advertised scopes. Access tokens remain opaque and are accepted only when they are nonempty, visible ASCII values safe for one `Authorization` header. Held leases record their owner process id, so startup recovery resolves only abandoned leases: several hosts may share one database file without disturbing each other's live OAuth flows. Configuration stores an OAuth reference, never the credential itself: ```json { "kind": "remote", "id": "linear", "name": "linear", "transport": { "kind": "streamable-http", "endpoint": "https://mcp.linear.app/mcp", "headers": {}, "oauth": { "kind": "oauth", "reference": "linear-oauth" } } } ``` Unexpected stdout EOF is terminal even when the child process is still alive. It immediately fails pending routing and starts the same process-tree cleanup. The Windows supervisor requires Node 26.7 or newer with experimental FFI support. The host starts it with `--experimental-ffi`; startup returns a typed `unavailable` failure when that platform feature cannot be loaded. Stderr is never copied into diagnostics. A caller may create a diagnostic channel to receive only redacted identity and byte-count metadata. Publication is nonblocking and uses one sliding slot with cumulative byte and sequence counters, so a slow or absent subscriber cannot stall the MCP server. The channel remains usable across connection closure and ends only with the scope that created the channel. ## Public TypeScript exports - `authFileDataRoot` - `ClientConnection` - `ClientDisconnection` - `commitProjectIdentity` - `createServerHost` - `Daemon` - `DAEMON_BASE_PORT` - `DAEMON_FILE_NAME` - `DAEMON_LOG_FILE_NAME` - `DAEMON_MAX_PORT` - `DAEMON_STARTUP_FILE_NAME` - `DAEMON_TOKEN_FILE_NAME` - `DaemonDependencies` - `daemonDirectory` - `DaemonError` - `DaemonInterface` - `DaemonLog` - `DaemonLogFields` - `DaemonOptionsMismatch` - `DaemonScope` - `DaemonStartObserver` - `DaemonStartupProgress` - `DaemonStartupReporter` - `DaemonStartupStep` - `DaemonStatusValue` - `DaemonTransport` - `DaemonTransportOptions` - `DaemonVersionMismatch` - `DECLARATIVE_SESSION_PLUGIN` - `defaultAuthFilePath` - `FileProviderAuthorityStorageOptions` - `HarnessConfigError` - `harnessConfigJsonSchema` - `listen` - `ListenOptions` - `makeDaemon` - `makeDaemonLog` - `makeDaemonStartupReporter` - `makeFileProviderAuthorityStorage` - `makeMcpSecretCredentialResolver` - `makeNodePrivatePluginSourceLoader` - `MakeNodePrivatePluginSourceLoaderInput` - `makeSessionProjectResolver` - `NodeListener` - `NodePluginArtifactOptions` - `NodePrivatePluginSourceLoader` - `NonLoopbackHost` - `PluginCleanupError` - `PluginConfig` - `PluginLoadError` - `ResolveConfigInput` - `ResolvedConfig` - `ResolvedProject` - `resolveProjectIdentity` - `RuntimeConfig` - `ServerHost` - `ServerHostMcpCredentialCallbackOptions` - `ServerHostMcpCredentialOptions` - `ServerHostOptions` - `ServerListenError` - `SessionPluginComposition` - `startDaemonLogTrim` --- Generated from [`packages/server-node/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/server-node/README.md). --- # Durable Object storage Cloudflare Durable Object SQLite adapter for the Core-owned Harness `StorageService`. ```ts import { makeDurableObjectStorage } from '@zephyrcloudio/harness-storage-durable-object'; const storage = await Effect.runPromise( makeDurableObjectStorage({ storage: state.storage, server }), ); ``` The adapter uses structural Durable Object SQL types, so consumers do not need Cloudflare's worker types to import this package. One SQLite-backed Durable Object is permanently bound to one Harness `ServerName` and holds every Session of that server. ## Public TypeScript exports - `DurableObjectProviderAuthorityStorageOptions` - `DurableObjectSessionCatalogAdoptionStorageOptions` - `DurableObjectSqlBinding` - `DurableObjectSqlCursor` - `DurableObjectSqlRow` - `DurableObjectSqlStorage` - `DurableObjectSqlValue` - `DurableObjectStorageInput` - `DurableObjectStorageOptions` - `isStorageMigrationFailure` - `makeDurableObjectProviderAuthorityStorage` - `makeDurableObjectSessionCatalogAdoptionStorage` - `makeDurableObjectStorage` - `migrateDurableObjectStorage` - `PortableSessionCatalogAdoptionStorage` - `readDurableObjectServer` - `StorageMigrationDiverged` - `StorageMigrationFailed` - `StorageMigrationFailure` - `StorageNewerThanBinary` - `StorageResetRequired` - `StorageSchemaDrifted` --- Generated from [`packages/storage-durable-object/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/storage-durable-object/README.md). --- # `@zephyrcloudio/harness-storage-sqlite` The Node SQLite adapter for Core's `Storage` service. Use `makeSqliteStorage` in an Effect scope, or provide `sqliteStorageLayer`: ```ts import { makeSqliteStorage, sqliteStorageLayer } from '@zephyrcloudio/harness-storage-sqlite'; ``` The adapter owns schema migrations, transactions, SQL, and row decoding. It opens SQLite in WAL mode with a 5-second busy timeout, so several host processes may share one database file: readers run concurrently and writers serialize. Claims stamp the owner process id; recovery reaps only work whose owner is dead. Use `:memory:` only for ephemeral runs and tests. Two cursors order observation. `session.activity_cursor` is per Session and stamps changed `turn` rows for `listSessionActivity`. The one-row `server_activity` counter is per database and stamps `session`, `turn`, and `deleted_session` rows in the same transaction as the change for `listServerActivity`, and `readServerActivityCursor` reports its current value; it only grows, so a server cursor stays valid after Sessions are deleted or the deletion markers are purged. ## Schema migrations Every opener brings the database to this build's schema before it serves, on Node and in Durable Objects alike. Migrations are ordered TypeScript modules in `src/migration/`, listed in `src/migration/registry.ts` (`serverMigrations` for the server database, `providerAuthorityMigrations` for the Worker's provider authority object). The `harness_migration` table records which ones a database has applied, and `harness_schema_object` records the tables, indexes, and triggers those migrations left behind. On open the runner: 1. Reads the ledger and checks that every recorded schema object still exists with the same definition. With nothing pending it returns without writing. 2. On Node, copies a database that already holds data to `.pre-.bak` (owner-only, newest copy kept). If the copy cannot be written, nothing is migrated and the open fails with `StorageMigrationFailed`. In a Durable Object it logs the point-in-time recovery bookmark instead. 3. Turns foreign key enforcement off, takes the write lock (`BEGIN IMMEDIATE`, or `transactionSync`), re-reads the ledger, applies every pending migration, runs `PRAGMA foreign_key_check`, and commits them together. Any failure rolls the whole batch back. A database without a ledger that holds tables from a release before migrations existed fails with `StorageResetRequired`; move it aside (`pnpm clean:host` in a source checkout) and start again. A database migrated by a newer build opens only if this build knows its `compatible_floor` (the newest breaking migration); otherwise it fails with `StorageNewerThanBinary`. A ledger whose order differs from this build's list fails with `StorageMigrationDiverged`. A recorded table, index, or trigger that was dropped or redefined outside a migration, for example by hand or by a partial restore, fails with `StorageSchemaDrifted`. None of these write to the database. To change the schema: 1. Add `src/migration/-.ts` exporting a `Migration` whose `id` is `_`, and append it to the end of the registry list. 2. Set `breaking: true` when a build without the migration could no longer safely read or write the database, for example after a rename, a dropped column, or a changed JSON shape. Additive changes that older builds ignore stay `breaking: false`. 3. Write `up` against the synchronous `SqliteDatabase`. It may run SQL or read rows, decode and re-encode JSON columns with Effect Schema, and write them back. For changes `ALTER TABLE` cannot make, rebuild the table (create the new table, copy rows, drop the old one, rename); the runner has foreign keys off, so the drop never cascades. 4. Keep migrations short. A Durable Object migrates inside `blockConcurrencyWhile`, which has a 30-second limit, so large backfills belong in alarm-driven follow-up work. Never edit, remove, or reorder a migration once it has shipped. Runtime-neutral SQLite hosts can provide a synchronous structural bridge through the portable entry: ```ts import { makePortableSqliteStorage, type SqliteDatabase, } from '@zephyrcloudio/harness-storage-sqlite/portable'; ``` The bridge may provide `transaction` to use a host transaction API such as `DurableObjectStorage.transactionSync`. Otherwise, the portable core uses SQLite transaction SQL. The caller owns the database lifecycle. Bun and Deno are not supported package hosts. ## Public TypeScript exports - `applyMigrations` - `ApplyMigrationsOptions` - `isStorageMigrationFailure` - `makePortableProviderAuthorityStorage` - `makePortableSessionCatalogAdoptionStorage` - `makeSqliteHostStorage` - `makeSqliteStorage` - `Migration` - `MigrationRegistry` - `pendingMigrations` - `PortableProviderAuthorityStorageOptions` - `PortableSessionCatalogAdoptionStorage` - `PortableSessionCatalogAdoptionStorageOptions` - `providerAuthorityMigrations` - `ProviderAuthorityStorage` - `ProviderAuthorityStorageService` - `ProviderConnectionRecord` - `serverMigrations` - `SetProviderConnectionInput` - `SqliteHostStorage` - `sqliteStorageLayer` - `SqliteStorageOptions` - `StorageMigrationDiverged` - `StorageMigrationFailed` - `StorageMigrationFailure` - `StorageMigrationProgress` - `StorageNewerThanBinary` - `StorageResetRequired` - `StorageSchemaDrifted` --- Generated from [`packages/storage-sqlite/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/storage-sqlite/README.md). --- # Harness Worker Cloudflare Worker composition for Ze Harness. Each server Durable Object (`HarnessServerObject`, bound via `HARNESS_SERVERS`) hosts any number of Sessions sharing one SQLite database, one SQLite-backed Computer Workspace, one attached Container, and the alarm that coordinates Turn recovery with post-command filesystem synchronization. ## Servers and Sessions A Session is a context boundary: its turns, history, permission grants, and overlay state. A server is the files-and-state home: the SQLite database, the workspace files, the container, the host MCP definitions, and the shared host OAuth credentials. One named server holds N sessions; deleting a Session never disturbs its siblings. Every session route carries both identities: `/v1/servers/:server/sessions/:sessionId/...`. Server-level routes carry only the server: `GET /v1/servers/:server/sessions` lists the server's Sessions oldest-first, `GET /v1/servers/:server/events` streams the server's activity (below), and `DELETE /v1/servers/:server` deletes the server (below). The gateway forwards by server name and sets `x-zephyr-server` (plus `x-zephyr-session-id` on Session routes); the object rejects mismatched identities with 404. ### Server activity stream `GET /v1/servers/:server/events?afterServerCursor=` is the SSE feed the client's `watchServer` consumes: every Session creation, title change, and deletion plus every Turn change on any Session of the server, each stamped with the server-wide cursor, replayed from `afterServerCursor` and then live. The gateway streams the body through unchanged. The route is observational: it admits no work and arms no recovery, but the open response holds the Durable Object awake for as long as the client keeps it, with a heartbeat (`server.cursor`) every 15 seconds while nothing happens. A client that wants an idle server to hibernate closes the stream and resumes later from the last cursor it saw. Servers are created lazily: the first request addressed to a name boots that name's object, and creating the first Session there succeeds. There is no create-server call. ## Workspace instructions Before admitting a prompt, Harness loads the non-empty `AGENTS.md` in the Session workdir. Ancestors above the workdir are not loaded eagerly. The file is applied as a developer instruction and re-read for every new prompt, so an edit or removal applies to the next admitted Turn. The admitted content is retained with that Turn for recovery and context compaction. Each file must fit the existing 50 KiB workspace text-read limit; unreadable, invalid, or oversized files fail prompt admission instead of being silently dropped. Successful standard file reads and directory listings below the Session workdir also discover any nested `AGENTS.md` files between the target and that workdir, nearest-first. Newly discovered files are attached to the read result. For direct reads, they are not attached again while that result remains in model-visible history. A later read can load them again after compaction or revert removes the result. Nested edits therefore take effect after the prior result leaves context, or in a new Session. Instructions discovered by script child reads are also retained in Session chronology, independently of the script's returned value. They appear as developer messages after the tool round. Known script-discovered files are re-read at new prompt and explicit compaction admission; edits and removals apply there while the currently admitted Turn retains its captured content. ## Server lifecycle `DELETE /v1/servers/:server` cancels and deletes every Session through the ordinary Session deletion authority (running work is cancelled and settled, overlays are removed, Session credentials are forgotten with revocation), purges the retired Session identities so the emptied server starts with a fresh namespace, forgets the shared host credentials, and drops the adopted catalog pins. The receipt lists the deleted Session identities; repeating the delete is idempotent and reports an empty list. Kept deliberately: workspace files and provider configuration stay behind on every host. Server delete never removes user files (on Node they live on the caller's disk) and never touches provider connections (explicitly managed through the provider API). Full file teardown is a follow-up requiring explicit product sign-off, not part of this delete. Idle objects hibernate under Cloudflare's standard Durable Object lifecycle; the alarm self-disarms when nothing is due, so an emptied server costs nothing until its next request. SQLite pages freed by deletion are reused in place; there is no automatic vacuum or pruning in v1, so long-lived servers grow with retained history, credentials, and workspace files. ## Concurrency One Durable Object serves one request at a time through its input gate, but Turns are admitted concurrently across and within Sessions: two Turns on one server may execute model and tool work at the same time. Container and workspace writes are uncoordinated — Harness offers no file locking — so engines that run concurrent Turns must coordinate shared paths themselves. Sequenced multi-step work is fully supported; concurrent writes to one file are last-writer-wins at the container. Session admits, credential mutations, and overlay leases stay atomic through the per-Session mutation gate and the Durable Object's serialized execution. ## Hybrid CodeMode Agents can combine tool calls using [CodeMode](/docs/harness.md). It is on by default; set `HARNESS_CODEMODE_ENABLED` to `false` to turn it off. Existing tool permissions still apply. Interrupted scripts are not automatically replayed after a restart. ## Permission configuration The Worker applies these variables as the Server permission scope: | Worker variable | Value | | ----------------------------------- | ------------------------------------------------------------- | | `HARNESS_PERMISSIONS` | JSON array of ordered `{ action, resource, effect }` rules | | `HARNESS_PERMISSION_CASE_SENSITIVE` | `true` or `false`; defaults to `true` | | `HARNESS_APPROVAL_TIMEOUT_MS` | Positive milliseconds or `unlimited`; omitted means unlimited | The last matching rule wins, and no match asks. The effective order is Ze defaults (allow inside the session workdir, ask for `external_directory` paths and `.env` reads), then these Worker rules, then Session and Agent rules. Durable **Allow always** grants belong only to the owning Session and cannot override a configured deny. Operator rules for scoped Sessions match workdir-relative file resources. A relative `--workdir` given at Session creation resolves against the server's workspace root (`/workspace`), which remote clients cannot see; an absolute one is used as-is and must name a directory inside that root. Remote Sessions default to the whole workspace. ## Singleton provider authority Durable Object The deployment binds a singleton Durable Object, `HarnessProviderAuthorityObject`, via `HARNESS_PROVIDER_AUTHORITY`: - Statically bundles the supported models.dev catalog and Ze's native `chat`, `responses`, and `messages` wire adapters. OpenAI, OpenCode Go, and OpenRouter are enabled when `HARNESS_PROVIDERS` is absent. - Stores provider connections in Durable Object SQLite storage: a credential change persists the connection, then publishes a new catalog revision; concurrent connection writes are serialized. Label-only updates persist without publishing a new revision. - Backs the gateway's authenticated runtime model and provider management endpoints (`/v1/runtime/models`, `/v1/runtime/providers`) through typed Durable Object RPC methods; the gateway decodes each route once and maps the authority's typed failures to protocol errors. - Serves stored credentials to server Durable Objects through its `providerCredentials()` RPC. Each server runtime resolves its inference keys once at initialization — a stored key wins over the deployment environment key, and a stored-but-unreadable entry blocks the environment fallback so inference matches the catalog snapshot — so connection changes apply to servers initialized after the change, exactly like other deployment configuration. > Per-server provider configuration (stored on the server object instead of the singleton) > is planned and tracked separately; this deployment keeps the singleton authority above. ## Provider transport configuration `HARNESS_PROVIDERS` is an optional JSON object with the same provider map documented by [`@zephyrcloudio/harness-server-node`](/docs/server-node.md). For example, this Worker variable enables a known catalog provider: ```json { "HARNESS_PROVIDERS": "{\"deepseek\":{}}" } ``` The map is an allowlist. `"{}"` is invalid because it enables no provider. Unknown providers without a `models` map discover their inventory from the authenticated `GET /models` endpoint at startup and runtime reload (see [`@zephyrcloudio/harness-server-node`](/docs/server-node.md)). A `models` map may still be declared explicitly, with `discovery: true` combining discovery and explicit overrides. Keep API keys in stored provider connections or Worker secrets named by `definition.credentials.environment`; do not put secrets in the plain `HARNESS_PROVIDERS` variable. Definitions can select `chat`, `responses`, or `messages`, routes, authentication, headers, Session-derived headers, and per-model endpoints and body defaults. `options.requestTimeoutMs` is a positive integer in milliseconds or `null` for unlimited. The deadline covers the full HTTP request, not idle time between chunks, and expiration is a typed attempt-limit failure. Shared wire-parser bounds are documented in the [LLM package README](/docs/llm.md). ## MCP host boundary The server Durable Object connects remote MCP servers over Streamable HTTP. Local stdio is unavailable because Workers cannot spawn local processes; the Worker does not emulate stdio or silently fall back to HTTP. Cloudflare applies these platform limits: - Each isolate has 128 MB of memory, shared by its concurrent requests. - Workers Free allows 50 external subrequests and 1,000 subrequests to Cloudflare services per invocation. Workers Paid defaults to 10,000 subrequests per invocation; `limits.subrequests` can raise that configured limit up to 10 million. - Each request can have six outgoing connections waiting for response headers at once. An MCP SSE response stops using one of those six slots after its response headers arrive. - `DurableObjectState.waitUntil()` does not extend a Durable Object's lifetime. The Durable Object transport passes escaped cleanup to the actual state so it is not a floating promise while the current request, response stream, or runtime-recognized pending I/O keeps the object active. Plain fetch subrequests do not prevent eventual eviction, and Durable Objects provide no shutdown hook. - Scope and transport finalizers are best effort if Cloudflare abruptly terminates an isolate. These are platform ceilings, not hidden Harness defaults. Transport limits remain explicit and configurable, and reaching one is a typed terminal outcome. ## MCP OAuth credentials in the Worker Each server Durable Object composes one server-wide MCP OAuth credential lifecycle on its own SQLite database: one credential store, one lifecycle, startup recovery while the object blocks concurrency during initialization, and the composed `oauthTokenResolver` on the remote Streamable HTTP transport for first authorization, 401 challenge refresh, and deduplicated concurrent refresh. Host-configured servers share one host credential namespace across every Session on the server; overlay-declared servers keep per-Session credentials that leave with their Session. The Worker's `HARNESS_MCP_CALLBACK_BASE_URL` HTTPS origin is the public callback address; the gateway route `GET /oauth/callback/:server` forwards the external browser redirect to the owning server object, the pending OAuth state selects the completing Session, deployment host and origin policy still apply, and callback query values are never logged. An OAuth credential reference without a composed runtime is a typed `authorization_required` transport failure, never configured functionality. ## Rollout runbook The `HarnessServerObject` class on the `HARNESS_SERVERS` binding carries the `v3` SQLite migration tag, and `v4` formally retires the old `HarnessSessionObject` class (its orphaned objects are deleted on deploy); the `v1`/`v2` session-object history is inert. Because the pre-release data model changed incompatibly (session-owned to server-owned storage, fresh `v13` session schema), deploy into staging expecting wiped Durable Object state: 1. Deploy the Worker; the `v4` migration deletes the old session objects. 2. Verify addressing: `PUT /v1/servers//sessions/` lazily creates the server, `GET /v1/servers//sessions` lists it, and a second Session shares the object. 3. Verify lifecycle: `DELETE /v1/servers/` empties the server and a re-created Session reuses identities cleanly. 4. Verify OAuth: one browser flow authorizes a host server for every Session on it. 5. Promote the same build to production; no per-server migration step exists by design. Each server object migrates its own SQLite database the first time it wakes on new code (see the `harness-storage-sqlite` README), and idle objects catch up when next used. The release that introduced storage migrations requires wiping pre-migration Durable Object state once: an object created before it fails with `StorageResetRequired`. Provider authority objects adopt their existing `provider_connections` table in place. Before a `wrangler rollback`, check whether the newer build shipped a breaking migration; objects it migrated refuse to open under the older build with `StorageNewerThanBinary`. ## Embedding in a host Durable Object `@zephyrcloudio/harness-worker-host` exports the embeddable `WorkerHarnessHost`. One DO instance shares one host-provided `Workspace` (file home plus Computer backend) across N HarnessSessions on one SQLite database, with injected `providerDefinitions` (embedder-implemented providers, selected per turn), `contentResolver` (attachment references, resolved only after visibility filtering), and `toolDispatch` (host custody after Harness permission resolution). Embedding hosts pass `credentialMode: 'injected-only'` so credentials arrive only through injected ports; ambient discovery stays disabled. The host has no bearer check (in-DO calls are trusted) and no deployment-wide provider singleton. One multiplexed DO alarm serves Harness recovery, Computer sync, and host-owned participants (`registerAlarmParticipant` + `setParticipantDue`); direct SessionServices admissions call `notifyAdmission()` so the drain runs via `waitUntil`. The host's `model` (the standalone Worker's `HARNESS_MODEL`) is optional: without it, prompts that name no model and title generation use the catalog's first available model. The standalone `HarnessServerObject` is itself composed on this host with env-sourced config, proving the same path embedding hosts take. Pin the exact published version; all workspace packages share one version and `pack:smoke` enforces it. `host.watchTurn(sessionId, turnId, { afterRevision })` returns the same event stream the `/turns/:turnId/events` SSE route sends, as an Effect `Stream` with no HTTP or SSE parsing: durable rows after the cursor, synthesized deltas, heartbeats, and one terminal event after which the stream ends. Omit `afterRevision` to receive only events after the current revision; pass the last revision you saw to resume without a gap. ```ts import { Effect, Stream } from 'effect'; const events = await host.watchTurn(sessionId, turnId, { afterRevision }); await Effect.runPromise(Stream.runForEach(events, (event) => Effect.sync(() => render(event)))); ``` `host.watchSession(sessionId, { afterActivityCursor })` and `host.watchServer({ afterServerCursor })` are the in-process counterparts of the `/sessions/:sessionId/events` and `/events` SSE routes: the Session's or the whole server's activity after the cursor, with heartbeats, as a `Stream` that never ends on its own. Omitting the Session cursor replays that Session's stamped history; omitting the server cursor starts live, with a first `server.cursor` naming the current cursor to bootstrap from. Interrupt the stream to stop watching. Both are observational and arm no recovery. `host.fetch` also serves the server-level routes (the Session list, the activity stream, server deletion) for an embedder's in-process client. The host's `providers` map is an allowlist compiled against the host's catalog (the bundled snapshot, or a newer `modelCatalog`). By default (`unknownProviders: 'fail'`) an id with no catalog entry and no complete `definition` fails host construction for every session. Embedders that build the map from external input can pass `unknownProviders: 'report'`: the unknown ids are left out of the plan and the host constructs with the rest. The drop is never silent; `(await host.resources()).rejectedProviders` lists each `{ providerId, message }`, and `providerIds` lists what compiled. The diagnostic names only the id, never a credential. Pass `modelCatalog: { document, publishedAt }` to compile from a published models.dev-format catalog the embedder fetched instead of the bundled snapshot. It is used only when valid and published (`Last-Modified`) at or after the bundled snapshot's build time; otherwise the host uses the bundled snapshot and reports why on `(await host.resources()).modelCatalog`. See the [Worker host README](/docs/worker-host.md). ## Runtime plugins With a `worker_loaders` binding named `LOADER`, a server object loads Session plugins at runtime from prebuilt bundles with no redeploy. A Session adds one with a `Bundle` source, the credential-free `https://` URL of a self-contained ES module (see the [plugin README](/docs/plugin.md)): ```sh ze-harness --remote URL --server NAME plugins add --session SESSION_ID \ --bundle https://plugins.example.com/notes.js --plugin-id example.notes --options '{"folder":"notes"}' ``` - Resolution fetches the bundle and pins the SHA-256 of its bytes as the revision's `integrity` (`sha256:`) and, unless the client declared a version, its `version` (`bundle-sha256:`). A declared `integrity` that does not match the fetched bytes is a typed `declared_mismatch`; a changed file at the same URL never replaces an activated plugin until `plugins refresh`. - The bundle is cached in the server object's own SQLite by digest, so recovery after eviction reloads the exact activated bytes without the URL. A lost cache refetches and verifies the digest. - Each bundle runs in its own Dynamic Worker isolate, shared by every Session in the server object that activates the same bytes, and inherits the Worker's network access. Plugins are trusted code, as on Node. The isolate is stateless across calls; see the plugin README for what that means for `setup`. - `Package`, `File`, and `Git` sources are a typed unsupported capability on the Worker; `Bundle` sources are a typed unsupported capability on the Node host. - Without the binding the Worker stays static-only and `Bundle` sources fail as a typed unsupported capability. The `test` environment declares the binding, so `test/bundle-plugins.test.ts` runs a fixture bundle through miniflare's Worker Loader. Embedding hosts pass the binding as `dynamicPlugins: { loader: env.LOADER }` on `WorkerHarnessHostOptions`. Embedding hosts can also link Session plugins into their Worker at build time with `staticSessionPlugins` on `WorkerHarnessHostOptions`. Each entry names a `sourceId`, a `pluginId`, a `version`, and the plugin definition; a Session adds it with the `{ _tag: 'Static', id: sourceId }` source. Nothing is fetched or loaded at runtime, and static plugins are trusted host code. The deployed Worker links none; its tests link a Session overlay fixture through `test/support/worker.ts`. ### Server plugins (embedding hosts) An embedding host can also give its Durable Object a list of **server plugins**: bundles loaded into every Session of the object through the host generation, the embedder's counterpart of the Node config file's `plugins`. The embedder owns and persists the list; the harness resolves, loads, and reports it. ```ts const host = new WorkerHarnessHost({ ...options, dynamicPlugins: { loader: env.LOADER }, serverPlugins: [{ url: 'https://plugins.example.com/notes.js', pluginId: 'example.notes' }], }); await host.setServerPlugins([...entries]); // replaces the list and reloads the host generation await host.serverPlugins(); // each entry's url, pluginId, pinned integrity, and active/failed state ``` - Each entry names a bundle URL and the plugin id it must declare, with an optional `integrity: 'sha256:'` pin and `options`. An entry that cannot load is reported `failed` and left out of the generation; it never blocks the server. - A changed list is a distinct host generation (the revision id carries a digest of the loaded list). New Sessions get it immediately and existing Sessions on their next Turn; Session plugins follow the new host baseline. - `setServerPlugins` is refused with `WorkerServerPluginConflict` while any Session runs one of the plugin ids as its own Session plugin, so a room plugin never shadows a thread's copy. A new generation that does not activate is `WorkerServerPluginRejected`; the previous list stays active either way. - There are no HTTP routes or CLI commands for server plugins; the standalone Worker has none. Platform limits, all surfaced as explicit plugin failures rather than hangs: - Dynamic Workers require the Workers Paid plan and are billed per unique Dynamic Worker per day (each distinct bundle is one). - A Durable Object can have at most 10 distinct Dynamic Workers with requests in flight, across all of its Sessions. - Workers RPC messages are limited to 32 MiB, which bounds tool inputs, results, and hook payloads. ## Recovery backoff and quarantine A failing recovery drain records its outcome before surfacing it. The first failure re-arms the pending revision after 60s, then 2m, 4m, and so on up to a 30-minute cap. After 10 consecutive failures the revision moves to the `recovery_quarantine` diagnostic row (`revision`, `failures`, `error`, `quarantined_at`) and stops re-arming, so a deterministically failing drain — for example poisoned rows that fail validation on every read — can no longer burn alarm wakes at full rate. The quarantine `error` and the alarm log carry the error name only, never the message, so a validator that echoes a bad value cannot land in storage or logs. Both alarm paths (`WorkerHarnessHost.alarm()` and the standalone server object) run recovery only when that revision is due. An earlier Computer sync or participant wake does not run a backed-off drain. They catch the recorded failure, still run that other work, then succeed: the retry is already scheduled durably through backoff, so the platform never fast-retries a recorded recovery failure. Unexpected failures still propagate and fail the alarm. Recovery still runs first on every admission via `waitUntil`; the alarm is the backstop. A new admission after quarantine re-arms recovery with a fresh failure budget, and the quarantine row keeps the last stuck revision for operators. Read it through `HostAlarmCoordinator.quarantineState()` from inside the owning Durable Object. The alarm log names the error class; it does not repeat the message. ## Protocol references - [MCP 2025-06-18 authorization](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) - [MCP 2025-06-18 transports](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) - [MCP 2025-06-18 schema](https://modelcontextprotocol.io/specification/2025-06-18/schema) - [JSON-RPC 2.0](https://www.jsonrpc.org/specification) - [WHATWG server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html) - [RFC 9728: OAuth 2.0 Protected Resource Metadata](https://www.rfc-editor.org/rfc/rfc9728.html) - [Cloudflare Workers limits](https://developers.cloudflare.com/workers/platform/limits/) - [Cloudflare Worker context](https://developers.cloudflare.com/workers/runtime-apis/context/) - [Cloudflare Durable Object lifecycle](https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/) ## Public TypeScript exports - `HarnessProviderAuthorityObject` - `HarnessServerObject` - `WorkspaceProxy` --- Generated from [`packages/worker/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/worker/README.md). --- # `@zephyrcloudio/harness-worker-host` The embeddable Harness host for Durable Objects. This is the only Ze Harness package embedding hosts install: every other workspace package is bundled into `dist` at build time. ```ts import { WorkerHarnessHost } from '@zephyrcloudio/harness-worker-host'; ``` ## Embedding One Durable Object instance shares one host-provided `Workspace` (file home plus Computer backend) across N HarnessSessions on one SQLite database, with injected `providerDefinitions` (embedder-implemented providers, selected per turn), `contentResolver` (attachment references, resolved only after visibility filtering), and `toolDispatch` (host custody after Harness permission resolution). Credentials arrive only through injected ports; ambient discovery stays disabled. ```ts const host = new WorkerHarnessHost({ server: ServerName.make('embedded'), state: ctx, workspace, revisionId: 'host-revision-1', model: ModelRef.make('example/model'), providerDefinitions: { example: exampleProvider }, contentResolver: hostContentResolver, toolDispatch: hostToolDispatch, }); ``` `model` is the optional room default. Without it, prompts that name no model and title generation use the catalog's first available model; a per-turn `model` on the prompt request always wins, and the admitting turn's model also titles the session. One multiplexed DO alarm serves Harness recovery, Computer sync, and host-owned participants (`registerAlarmParticipant` + `setParticipantDue`). Direct `SessionServices` admissions call `notifyAdmission()` so the drain runs via `waitUntil`. Harness never closes the provided `Workspace`; the owning DO retains its lifetime. ## Hybrid CodeMode Agents can combine tool calls using [CodeMode](/docs/harness.md). It is on by default and needs no additional dependency. To turn it off, pass `codemode: { enabled: false }` to `WorkerHarnessHost`. Interrupted scripts are not automatically replayed after a restart. ## Watching a Turn `host.watchTurn(sessionId, turnId, { afterRevision })` returns the live events of one Turn as an Effect `Stream`, exactly as the `/turns/:turnId/events` SSE route streams them but with no HTTP request or SSE parsing: durable rows after the cursor, synthesized deltas, heartbeats, and one terminal event after which the stream ends. Omit `afterRevision` to receive only events after the current revision; pass the last revision you saw to resume without a gap. ```ts import { Effect, Stream } from 'effect'; const events = await host.watchTurn(sessionId, turnId, { afterRevision }); await Effect.runPromise(Stream.runForEach(events, (event) => Effect.sync(() => render(event)))); ``` ## Watching a Session or the server `host.watchSession(sessionId, { afterActivityCursor })` returns the Session's activity as a `Stream` and `host.watchServer({ afterServerCursor })` returns the whole server's activity (Session creations, titles, deletions, and every Turn change on any Session) as a `Stream`, exactly as the `/sessions/:sessionId/events` and `/events` SSE routes stream them. Omitting the Session cursor replays that Session's history. Omitting the server cursor starts live: the first event is a `server.cursor` naming the current cursor, so list Sessions and pending requests after it and nothing is missed; pass the last cursor you saw to replay every stamped row after it. Neither stream ends on its own and neither admits work or arms recovery: interrupt the stream to stop watching. A `session.created` event is an upsert by `session.id`, since a watch that starts after an existing Session was retitled sees it again. ```ts const activity = await host.watchServer(); // live; resume later with { afterServerCursor } await Effect.runPromise( Stream.runForEach(activity, (event) => Effect.sync(() => { if (event.type === 'session.created') upsertSession(event.session); }), ), ); ``` `host.fetch` also serves the server-level routes (the Session list, the activity stream, and server deletion), so an in-process client built on it reaches them without a gateway. ### Unknown provider ids `providers` is an allowlist compiled against the host's catalog (the bundled snapshot, or a newer `modelCatalog`, below). By default (`unknownProviders: 'fail'`) an id with no catalog entry and no complete `definition` fails host construction, and therefore every session on the object. Pass `unknownProviders: 'report'` to drop such ids instead: the host constructs with the remaining providers and reports what it dropped on `resources()`, so the embedder learns about every rejected id rather than finding out at the first turn. ```ts const host = new WorkerHarnessHost({ ...options, providers, unknownProviders: 'report' }); const { providerIds, rejectedProviders } = await host.resources(); for (const { providerId, message } of rejectedProviders) { console.warn(`provider ${providerId} dropped: ${message}`); } ``` `rejectedProviders` is empty under `'fail'`. Diagnostics name only the provider id, never a credential. ## Runtime provider credentials Injected-only hosts (`credentialMode: 'injected-only'`) write one provider's key at a time, at any point in the host's life. Turns read the live key at acquire time, so a write or rotation takes effect on the next turn with no reload; removals fail new acquires closed while in-flight attempts keep the key they acquired. ```ts await host.setProviderCredential('example', apiKey); const live = await host.hasProviderCredential('example'); await host.removeProviderCredential('example'); ``` Keys live in memory only: never persisted, never logged, cleared on restart. Presence is boolean-only; the key is never exposed. Unknown provider ids throw on write and remove (`has` answers `false`). These methods throw on non-injected hosts, where the live port mirrors durable storage instead. ### Per-turn credentials The slot holds one key per provider for the whole object. When several callers share one host and each should run on their own key, give the host a resolver instead of writing the slot per admission. The harness calls it on every model acquisition that serves session work, naming that work, and uses the answer for that acquisition only: ```ts const host = new WorkerHarnessHost({ // ... credentialMode: 'injected-only', resolveProviderCredential: async ({ providerId, scope }) => { // scope.kind is 'turn' | 'compaction' | 'detached'; every scope carries sessionId, // a turn carries turnId, a compaction carries compactionId, and a detached title // call carries the admitting turnId when one scheduled it. const owner = await lookupOwner(scope); if (owner === undefined) return undefined; // fall through to the slot const supplied = await custody.supply({ providerId, owner }); return supplied.ok ? { apiKey: supplied.apiKey } : { denied: supplied.message, code: supplied.code }; }, }); ``` Precedence at acquire time is fixed in Core (`packages/harness/src/plugin/setup.ts`): the frozen config key, the provider's OAuth connection (only without a config key), the live port, then the environment. `credentialMode: 'injected-only'` drops the config key and the environment, so on this host the live port is the only source, and the live port asks the resolver first and falls through to the slot when it returns `undefined` or a blank key. A Turn acquires its model once and reuses it for the tool loop, in-turn compaction, and steering continuations, so the resolver runs once per Turn. A Turn still pending across a restart asks again when it starts, so a revoked or rotated key takes effect at that point and nothing about the binding is persisted by the harness. `{ denied }` fails the acquire with `ModelAuthenticationFailure`; on a Turn that surfaces as `TurnFault.code === 'model_run_failed'` with the denial text as the message, so the text must name no secret. `code` becomes the fault's `providerCode`, for embedders that project fault codes and drop messages; it must match `[A-Za-z0-9][A-Za-z0-9._:/-]*` and be at most 128 characters, and a code outside that shape is dropped while the denial still surfaces. Plugin models that delegate to a sibling (the Auto lanes) pick their candidate siblings from the host-wide sources only, so which providers qualify is the same for every caller; the chosen sibling is then acquired through the resolver with the Turn's scope, like a native model. A resolver-only host that uses such plugins must still hold a slot or environment key for each provider they may delegate to, or no sibling qualifies and the Turn fails with "No configured sibling model is available". Scope-free reads (provider discovery, `hasProviderCredential`) and every write go to the slot and never call the resolver. Resolver exceptions fail the acquire the same way with a diagnostic that names only the provider. ## Provider catalog The bundle exports the harness's provider catalog so embedders can build their `providers` allowlist and model pickers from the same data the host compiles against instead of hardcoding ids. `loadBundledModelsDevSnapshot()` returns the models.dev snapshot copied into the harness at build time (`providers` is keyed by provider id; each record carries `displayName`, `api`, and `models`), and `defaultProviderIds` names the ids a host enables when `providers` is omitted. What this host actually compiled, after `providers`, `providerDefinitions`, and `enabled` filtering, is `(await host.resources()).providerIds`. ```ts import { loadBundledModelsDevSnapshot, defaultProviderIds, } from '@zephyrcloudio/harness-worker-host'; const { providers } = loadBundledModelsDevSnapshot(); const choices = Object.values(providers).map(({ id, displayName, models }) => ({ id, displayName, modelIds: Object.keys(models), })); const compiled = (await host.resources()).providerIds; // ids live on this host void defaultProviderIds; // ['openai', 'opencode-go', 'openrouter'] ``` The snapshot is a point-in-time copy of models.dev bundled into the package, not a live feed; a newer harness release carries a newer snapshot, and `modelCatalog` (below) catches a host up without one. Type-only exports `NormalizedModelsDevSnapshot`, `NormalizedProviderRecord`, `NormalizedModelRecord`, `ProviderConfigMap`, and `ProviderConfigEntry` describe the snapshot and the `providers` option. ### Published catalog Pass `modelCatalog: { document, publishedAt }` to compile from a catalog you fetched instead of the bundled snapshot. `document` is the parsed JSON of a models.dev-format catalog, the same document the Node daemon refreshes from `ZE_HARNESS_MODELS_URL` (default `https://models.dev/api.json`), passed unmodified: the host normalizes it exactly as the Node refresh does, keeping providers and per-model packages that map to a native protocol. `publishedAt` is when that document was published, in epoch milliseconds: the response's `Last-Modified` (R2 sets it at upload). Keep it with your cached copy; a `304` does not change it, and when you fetched the document does not count. The bundled snapshot is the floor, as on the Node host. The supplied catalog is used only when it is valid and `publishedAt` is at or after the bundled snapshot's build time, so a newer release is never dragged back by a stale published catalog; otherwise the host uses the bundled snapshot. An invalid or older catalog never fails host construction: the host logs a warning and reports which catalog it compiled on `resources().modelCatalog` (`source` is `'supplied'` or `'bundled'`, with `contentRevision`, `publishedAt` (the bundled snapshot's build time when it is the source), and a `diagnostic` when a supplied catalog was not used). The catalog is read once, when the host first starts; a newer catalog takes effect on the next host construction, so refresh it on your own schedule and pass the latest copy when the object starts. A response without `Last-Modified` proves no publish time; pass a `publishedAt` only when your source states one. Fetching and caching are the embedder's: use conditional requests (`If-None-Match`) and keep the last good document. `validateModelCatalog({ document, publishedAt })` applies the host's normalization without starting a host, so a degraded upstream response can be refused before it replaces your cached copy; it rejects with `ModelsDevSourceError` naming the problem. The models.dev document is several megabytes, more than one Durable Object storage value holds, so keep the cached copy somewhere sized for it (R2, Workers KV, or the Cache API). Normalizing it costs CPU on each host start, the same work the Node refresh does once per fetch. ```ts import { validateModelCatalog, WorkerHarnessHost } from '@zephyrcloudio/harness-worker-host'; const response = await fetch(catalogUrl, { headers: cached?.etag === undefined ? {} : { 'If-None-Match': cached.etag }, }); const lastModified = Date.parse(response.headers.get('last-modified') ?? ''); if (response.ok && Number.isFinite(lastModified)) { const fetched = { document: await response.json(), publishedAt: lastModified }; await validateModelCatalog(fetched); // rejects: keep the previous copy cached = { ...fetched, etag: response.headers.get('etag') ?? undefined }; } // a 304 keeps `cached` exactly as stored const host = new WorkerHarnessHost({ ...options, ...(cached === undefined ? undefined : { modelCatalog: { document: cached.document, publishedAt: cached.publishedAt } }), }); const { modelCatalog } = await host.resources(); if (modelCatalog.diagnostic !== undefined) console.warn(modelCatalog.diagnostic); ``` ## Dashboard Pass `dashboard: true` and `host.fetch` serves the Harness dashboard at `GET /dashboard`, filled in with the host's `server`. It is the same single HTML page the Node daemon serves. The page sends document-relative `v1/servers//…` requests, so mount it under any prefix that forwards GETs to `host.fetch` with the prefix stripped: ```ts const host = new WorkerHarnessHost({ server, state: ctx, workspace, revisionId, dashboard: true }); // In the embedder's proxy: /dashboard and /v1/... reach the host as /dashboard and /v1/... const response = await host.fetch(new Request(new URL(path, 'https://harness.local'), request)); ``` `host.fetch` trusts its caller, so the page never asks for a token, and it hides the log panel because this host has no `v1/logs`. The response allows framing (no `frame-ancestors`) and limits API calls to `connect-src 'self'`, so the page and its API share one origin; a custom-scheme page needs a scheme with a real (non-opaque) origin. An embedding parent can restyle the page with `frame.contentWindow.postMessage({ type: 'ze-harness:theme', tokens: { '--zh-bg': '#fff' } }, '*')`; the token names are listed in the [`harness-server` README](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/server/README.md#theme). ## Peers and versions The only runtime peers are `effect` and `@cloudflare/computer`, pinned exactly to this repo's catalog. They must be singletons shared with the host worker: duplicate `effect` breaks context tags and layers, and a duplicate Computer `Workspace` breaks file-home identity. The bundle has no top-level `cloudflare:workers` import, so it loads in plain Node (unit tests, tooling) without the `cloudflare:` scheme; platform APIs are only required when the host runs inside a Durable Object. Pin this package exactly; all Ze Harness packages share one version and `pack:smoke` enforces it. ## Public TypeScript exports - `AppendConversationCommand` - `AuthorId` - `ClientId` - `ConversationEntryConflict` - `ConversationEntryId` - `DurableContentResolutionError` - `DurableContentResolver` - `HistoryVisibility` - `InvalidServerCursor` - `KnownTurnFaultCode` - `ModelCallIdentity` - `ModelEvent` - `ModelFailure` - `ModelRef` - `ModelRequest` - `ModelsDevSourceError` - `ModelService` - `ModelTurnContext` - `ModelUsage` - `NormalizedModelRecord` - `NormalizedModelsDevSnapshot` - `NormalizedProviderRecord` - `PermissionRequest` - `PermissionRequestId` - `PermissionRequestNotFound` - `PromptAttribution` - `PromptConflict` - `PromptContent` - `ProviderConfigEntry` - `ProviderConfigMap` - `ProviderCredentialResolution` - `ProviderCredentialResolver` - `ProviderCredentialScope` - `ProviderDefinition` - `ProviderDefinitionMap` - `ProviderModelAcquisitionInput` - `PublicPromptAdmission` - `QuestionRequest` - `QuestionRequestId` - `QuestionRequestNotFound` - `ReplyPermissionInput` - `ReplyQuestionInput` - `ServerActivityCursor` - `ServerName` - `ServerStreamEvent` - `SessionActivityCursor` - `SessionActivityPage` - `SessionConflict` - `SessionId` - `SessionNotFound` - `SessionServices` - `SessionStreamEvent` - `SessionTitle` - `SessionTurnState` - `SteerId` - `SteerRejected` - `StorageError` - `StorageFailure` - `SubmitPromptCommand` - `ToolApprovalSnapshot` - `ToolDispatchError` - `ToolDispatchLease` - `ToolDispatchLifecycle` - `ToolDispatchOutcome` - `ToolQuestionSnapshot` - `TurnEvent` - `TurnEventsPage` - `TurnFault` - `TurnId` - `TurnNotFound` - `TurnOutcome` - `TurnPart` - `TurnPhase` - `TurnSnapshot` - `TurnStreamEvent` - `TurnTextPart` - `TurnToolPart` - `UnavailableError` - `WatchSessionActivityInput` - `WatchTurnEvent` - `WorkerHarnessHost` - `WorkerHarnessHostOptions` - `WorkerModelCatalog` - `WorkerModelCatalogStatus` - `WorkerServerPlugin` - `WorkerServerPluginConflict` - `WorkerServerPluginRejected` - `WorkerServerPluginStatus` - `WorkerStaticSessionPlugin` --- Generated from [`packages/worker-host/README.md`](https://github.com/ZephyrCloudIO/ze-harness/blob/main/packages/worker-host/README.md).