@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.

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. 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):

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 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:

{
  "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:

---
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:

{
  "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:

{
  "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).

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 for the pipeline, corrections, and publishing.

A custom provider without a models map discovers its inventory from its authenticated GET <baseURL>/models endpoint at startup and runtime reload. For example:

{
  "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:

{
  "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. It is on by default. To turn it off, set codemode.enabled to false in ze-harness.config.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.

{
  "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.

{
  "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": "<id>" } 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:

{
  "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