# `@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.<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_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 <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:

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

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

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