@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/skillsand project.ze-harness/skills$XDG_CONFIG_HOME/opencode/{skill,skills}and project.opencode/{skill,skills}- global and project
.agents/skillsand.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, andmessagesuse 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.
optionscan overridebaseURL, headers, request timeout, and the configuredapiKey. - 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 globalauth.json(opencode's exact shape, owner-only permissions) shared by every database;ZE_HARNESS_AUTH_CONTENToverrides it for scripted runs. Anauth.jsonthat 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 injectedproviderCredentials,mcpCredentialResolver, andmcpOAuthTokenResolverports) 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 injectcontentResolver(opaque attachment references, resolved only after visibility filtering),toolDispatch(host custody around final dispatch after Harness permission resolution), andproviderDefinitions(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
providersmap, OpenAI, OpenCode Go, and OpenRouter are enabled. With a map, only listed entries not markedenabled: falseare 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/loginsstarts 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=Nreturns the status, waiting up toNmilliseconds (at most 30000) for it to change:pending,connectedwith the safe connection, orfailedwithdenied,expired,cancelled, orerrorand a message.DELETE /v1/runtime/providers/:providerId/logins/:loginIdcancels 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
authFileDataRootClientConnectionClientDisconnectioncommitProjectIdentitycreateServerHostDaemonDAEMON_BASE_PORTDAEMON_FILE_NAMEDAEMON_LOG_FILE_NAMEDAEMON_MAX_PORTDAEMON_STARTUP_FILE_NAMEDAEMON_TOKEN_FILE_NAMEDaemonDependenciesdaemonDirectoryDaemonErrorDaemonInterfaceDaemonLogDaemonLogFieldsDaemonOptionsMismatchDaemonScopeDaemonStartObserverDaemonStartupProgressDaemonStartupReporterDaemonStartupStepDaemonStatusValueDaemonTransportDaemonTransportOptionsDaemonVersionMismatchDECLARATIVE_SESSION_PLUGINdefaultAuthFilePathFileProviderAuthorityStorageOptionsHarnessConfigErrorharnessConfigJsonSchemalistenListenOptionsmakeDaemonmakeDaemonLogmakeDaemonStartupReportermakeFileProviderAuthorityStoragemakeMcpSecretCredentialResolvermakeNodePrivatePluginSourceLoaderMakeNodePrivatePluginSourceLoaderInputmakeSessionProjectResolverNodeListenerNodePluginArtifactOptionsNodePrivatePluginSourceLoaderNonLoopbackHostPluginCleanupErrorPluginConfigPluginLoadErrorResolveConfigInputResolvedConfigResolvedProjectresolveProjectIdentityRuntimeConfigServerHostServerHostMcpCredentialCallbackOptionsServerHostMcpCredentialOptionsServerHostOptionsServerListenErrorSessionPluginCompositionstartDaemonLogTrim