@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.
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 for inspecting executions and the CodeMode package 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:
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).
- 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, andmessagesadapters, while unknown explicit packages and shapes fail closed. - Resolves credentials from valid stored connections, a configured
providers.<id>.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_requiredstatus 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.jsondocument that Node hosts refresh fromZE_HARNESS_MODELS_URL(see the Server Node README). 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 <file> 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:
{
"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 <gateway>/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:
- Create an R2 bucket in the Cloudflare account that owns
theaiplatform.app. - Attach the custom domain
models.theaiplatform.appto the bucket. - Create an R2 API token with Object Read & Write on that bucket only.
- Add repository variables
ZE_MODELS_R2_ACCOUNT_IDandZE_MODELS_R2_BUCKET, and secretsZE_MODELS_R2_ACCESS_KEY_IDandZE_MODELS_R2_SECRET_ACCESS_KEYfrom the token. Optionally add variableZE_MODELS_TAP_GATEWAY_URL(the gateway origin) to publish thetapsection. - Run the workflow, then check that
https://models.theaiplatform.app/api.jsonreturns anETagand answers thatETaginIf-None-Matchwith304. - Change the
ZE_HARNESS_MODELS_URLdefault inpackages/server-node/src/bootstrap/models-dev-cache.tsand the Server Node README tohttps://models.theaiplatform.app/api.json.
Named provider entries
Each providers.<id> 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:
{
"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
AdmittedPromptAdmittedSteerAppendConversationCommandAppendConversationEntryCommandCancelSessionMcpCredentialCommandClaimedContextCompactionClaimedSessionWorkClaimedTurnCompleteSessionMcpCredentialCallbackCommandConfiguredAgentContextCompactionConflictContextCompactionNotFoundConversationEntryConflictCoreMcpCandidateCoreRuntimeCoreRuntimeCandidateCoreRuntimeConfigcreateCoreRuntimeCreateCoreRuntimeOptionsdefaultProviderIdsDeleteSessionCommandDurableContentResolutionErrorDurableContentResolverDurableContextSelectionDurableHistoryMessageForkSessionCommandForkSessionInputGenerationCompanionHistoryVisibilityConflictInvalidServerCursorInvalidSessionCursorInvalidTurnTransitionListPluginJournalInputListServerActivityInputListSessionActivityInputListSessionCompactionsInputListSessionConversationInputListSessionTurnsInputListTurnEventsInputloadBundledModelsDevSnapshotmakeLateBoundMcpCredentialConnectionsmakeReconnectingMcpCredentialLifecyclemakeSessionMcpCredentialRuntimemakeSessionMcpCredentialServerResolvermakeSessionMutationGatemakeSessionServicesMCP_CREDENTIAL_AUTHORIZATION_REQUIRED_REASONSMCP_CREDENTIAL_CALLBACK_DENIED_REASONSMCP_CREDENTIAL_REVOCATION_STATUSESMcpCredentialHostConnectionsMcpCredentialIdentityMcpPromptReservationMcpPromptReservationInputNormalizedModelRecordNormalizedModelsDevSnapshotNormalizedProviderRecordPermissionConflictPermissionReplyReceiptPermissionReplyResolutionPermissionRequestNotFoundPermissionResolutionPluginJournalPagePreparedTurnInputPrivateHistoryMessageContinuationPrivateModelMessageContinuationPrivateSessionPluginIntegrationPrivateTurnMessageContinuationProgrammaticPluginCandidatePromotedSteerPromptAdmissionContextPromptAdmissionStalePromptConflictProviderConfigEntryProviderConfigMapProviderDefinitionMapPublicMcpCatalogPromptPublicMcpCatalogResourcePublicMcpCatalogResourceTemplatePublicMcpCatalogServerPublicMcpCatalogSnapshotPublicMcpCatalogToolPublicMcpResourceAnnotationsPublicMcpResourceContentsPublicMcpResourceReadResultPublicMcpServerCatalogpublicSessionMcpCredentialForgetResultpublicSessionMcpCredentialStatusQuestionConflictQuestionReplyReceiptQuestionRequestNotFoundQuestionResolutionQuestionResolutionInputReadMcpResourceCommandRecoveryResultRejectQuestionInputReloadResultReplaceSessionConfigurationInputReplyPermissionInputReplyQuestionInputRequestSizeEstimationErrorRequestSizeEstimatorResolvedModelResolvedPromptRunInputRuntimeCandidateErrorRuntimeStartupErrorSavedPermissionNotFoundServerActivityEntryServerActivityPageServerDeletionReceiptServerNameSessionActivityPageSessionCompactionHistoryBoundarySessionCompactionPositionSessionCompactionsApiSessionCompactionsPageSessionCompactionsServiceSessionCompactionStateSessionConfigurationConflictSessionConflictSessionConversationApiSessionConversationPageSessionConversationServiceSessionDeletionApiSessionDeletionReceiptSessionDeletionServiceSessionExecutionApiSessionExecutionServiceSessionHistoryBoundarySessionLifecycleApiSessionLifecycleServiceSessionMcpCredentialAuthorizationSessionMcpCredentialAuthorizationRequiredReasonSessionMcpCredentialCallbackDeniedReasonSessionMcpCredentialErrorsessionMcpCredentialFailureSessionMcpCredentialForgetResultSessionMcpCredentialRevocationStatusSessionMcpCredentialRuntimeSessionMcpCredentialRuntimeInputSessionMcpCredentialServerBindingSessionMcpCredentialStatusSessionMcpErrorSessionMcpRuntimeSessionMcpSurfaceApiSessionMcpSurfaceServiceSessionMutationGateSessionMutationLeaseSessionNotFoundSessionObservationApiSessionObservationServiceSessionOverlayRevisionSessionPromptsApiSessionPromptsServiceSessionRuntimeSessionRuntimeErrorSessionServiceClosedSessionServicesSessionServicesInputSessionTurnRefSessionTurnsPageSessionTurnStateStartSessionMcpCredentialCommandSteerConflictSteeringModelBoundaryInputSteeringModelBoundaryResultSteeringTerminalBoundaryResultSteerRejectedStorageStorageErrorStorageFailureStorageServiceSubmitContextCompactionCommandSubmitMcpPromptCommandSubmitPromptCommandTerminalCommitTerminalProjectionTerminalToolOutputArtifactToolCallCompletionToolCallNotFoundToolCallRequestToolCallSettlementToolDispatchErrorToolDispatchLeaseToolDispatchLifecycleToolDispatchOutcomeToolPermissionCallbackResolutionTurnTurnEventsPageTurnNotFoundVERSIONWatchServerActivityInputWatchSessionActivityInputWebFetchWebFetchErrorWebFetchFormatwithSessionMutationwithSessionMutationExclusiveWorkspaceDirectoryEntryWorkspaceFileErrorWorkspaceFilesWorkspaceFileTargetWorkspaceShellWorkspaceShellError