From d695ec2e2cbac754be77e6b3033f26ba6f2c28a5 Mon Sep 17 00:00:00 2001 From: Eduardo Piva Date: Mon, 6 Jul 2026 21:22:43 -0700 Subject: [PATCH 1/4] docs: draft remote harness event protocol RFC --- ...agent-harness-bridge-and-event-protocol.md | 1084 +++++++++++++++++ 1 file changed, 1084 insertions(+) create mode 100644 rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md diff --git a/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md b/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md new file mode 100644 index 00000000..f4b252b7 --- /dev/null +++ b/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md @@ -0,0 +1,1084 @@ +--- +title: Remote Agent Harness Bridge and Event Protocol +authors: + - Omar Shahine + - Eduardo Piva +created: 2026-07-07 +last_updated: 2026-07-07 +status: draft +issue: +rfc_pr: +--- + +# Proposal: Remote Agent Harness Bridge and Event Protocol + +Assistance note: this RFC updates the remote AgentHarness bridge draft after +reviewing OpenClaw's current AgentHarness callbacks, Codex app-server event +projection, Copilot SDK event bridge, Codex upstream protocol events, and the +portable remote-harness implementation currently carried in Lobster. + +## Summary + +Add a first-class remote AgentHarness bridge to OpenClaw and standardize the +streaming event protocol that lets a selected native harness run outside the +Gateway process while preserving OpenClaw's local lifecycle, progress, +approval, tool, plan, compaction, transcript, and user-visible delivery events. + +## Motivation + +OpenClaw can already route selected provider/model turns through native +AgentHarness plugins such as Codex and Copilot. Those harnesses do more than +return final assistant text. They stream assistant deltas, reasoning snapshots, +plans, tool lifecycle, command output, patch summaries, compaction state, +approvals, permission requests, user-input prompts, usage snapshots, lifecycle +state, and harness-specific diagnostic events into OpenClaw surfaces. + +A remote harness that only transports `RunStart`, assistant text, a few tool +notifications, and a terminal result is not equivalent to a local Codex, Pi, or +Copilot harness. It would lose the local OpenClaw events that the Gateway uses +to update WebChat, CLI/TUI status, Control UI, transcripts, task mirrors, +replay-safety decisions, approval resolution, and lifecycle persistence. + +Without an upstream event contract, every host or container runtime has to +invent its own partial mapping. That makes the boundary unclear and creates +compatibility risks: + +- assistant text may render but plans, reasoning, command output, and patch + summaries may disappear; +- approval or permission prompts may bypass OpenClaw policy or become + unreviewable; +- tool execution progress may be visible in one harness but not another; +- compaction and replay-safety state may be wrong after a remote native turn; +- container hosts may accidentally trust request headers for session, run, or + workspace identity; +- future open-source harness or container-runtime contributions cannot prove + conformance against one shared contract. + +This RFC standardizes the remote bridge as an AgentHarness plugin plus an event +protocol. The protocol is transport-neutral, fail-closed, and designed to carry +both harness-originated events and OpenClaw-local event callbacks that must cross +an out-of-process boundary. + +## Goals + +- Define a platform-neutral remote AgentHarness bridge selected through existing + provider/model `agentRuntime.id` configuration. +- Standardize the v1 frame protocol for run control, terminal outcomes, + approval decisions, tool callbacks, and typed event streaming. +- Make OpenClaw's local AgentHarness callback/event surface explicit so Codex, + Pi, Copilot, and future harnesses can preserve equivalent user-visible + behavior when remote. +- Preserve OpenClaw event streams for `lifecycle`, `assistant`, `thinking`, + `tool`, `item`, `plan`, `approval`, `command_output`, `patch`, `compaction`, + `error`, and namespaced harness-extension streams. +- Preserve fail-closed runtime selection: an explicit remote-harness selection + must run remotely or fail; it must not silently fall back to local OpenClaw. +- Keep identity authority host-owned. `RunStart` context, not request headers, + is authoritative for run, session, workspace, and user context. +- Avoid provider/model credentials in the remote agent process unless a future + accepted RFC defines a credential or model-proxy contract. +- Provide a conformance target for future open-source remote-harness and + container-runtime contributions. + +## Non-Goals + +- This RFC does not make remote harness the default OpenClaw runtime. +- This RFC does not define a specific cloud worker pool, deployment system, + Azure/Plex/EV2 integration, or rollout audience mechanism. +- This RFC does not distribute provider, model, Graph, M365, GitHub, or other + caller credentials to remote agent containers. +- This RFC does not define a general-purpose remote filesystem or shell API + outside the harness/tool event contract. +- This RFC does not change OpenClaw's approval, tool-policy, or memory-policy + decisions; it transports those decisions across the remote boundary. +- This RFC does not require every harness to emit every event. It requires every + v1 bridge implementation to preserve and forward the event classes it observes + or produces, and to fail closed for unsupported request/decision frames. +- This RFC does not standardize host-specific container allocation internals in + v1. It defines the bridge contract that container runtimes must satisfy. + +## Proposal + +Introduce a bundled `remote-harness` AgentHarness plugin and a public remote +harness event protocol package. The plugin has two sides: + +1. **Gateway-side harness.** Runs inside the OpenClaw Gateway process, registers + an AgentHarness, resolves explicit runtime selection, opens the configured + transport, sends `RunStart`, maps remote frames into OpenClaw callbacks, + forwards OpenClaw decisions back to the host, and returns the terminal + `AgentHarnessAttemptResult`. +2. **Host-side runtime.** Runs outside the Gateway process or in a separate + container, validates `RunStart`, selects an allowlisted inner harness, runs + the native harness, and sends remote frames for all assistant, reasoning, + plan, tool, approval, command, patch, compaction, lifecycle, and diagnostic + events it observes. + +The bridge is a protocol and lifecycle contract, not a deployment contract. A +host can connect the sides with stdio, WebSocket, TCP, Unix socket, or a +host-owned adapter transport that is explicitly wired by the embedding runtime. + +### Plugin registration and selection + +The current OpenClaw registration contract remains the source of truth: + +```ts +import type { AgentHarness } from "openclaw/plugin-sdk/agent-harness"; +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; + +const remoteHarnessAgentHarness: AgentHarness = { + id: "remote-harness", + label: "Remote Harness", + supports(ctx) { + return ctx.requestedRuntime === "remote-harness" + ? { supported: true, priority: 100 } + : { supported: false, reason: "remote-harness requires explicit runtime id" }; + }, + async runAttempt(params) { + return await runRemoteHarnessAttempt(params); + }, +}; + +export default definePluginEntry({ + id: "remote-harness", + name: "Remote Harness", + register(api) { + api.registerAgentHarness(remoteHarnessAgentHarness); + }, +}); +``` + +Manifest metadata is planner/catalog data, not registration by itself: + +```json +{ + "id": "remote-harness", + "name": "Remote Harness", + "activation": { + "onStartup": false, + "onAgentHarnesses": ["remote-harness"] + }, + "agentHarnesses": [ + { "id": "remote-harness", "displayName": "Remote Harness" } + ] +} +``` + +`activation.onAgentHarnesses` tells plugin loading that a runtime id can require +this plugin. The runtime behavior is registered by `api.registerAgentHarness`. +If OpenClaw does not yet accept top-level `agentHarnesses` manifest metadata, +that field is treated as proposed catalog metadata for this RFC, not as a v1 +runtime dependency. + +Runtime selection uses existing provider/model policy: + +```json +{ + "models": { + "providers": { + "openai": { + "agentRuntime": { "id": "remote-harness" } + } + } + } +} +``` + +Model-scoped policy wins, provider-scoped policy follows, and explicit plugin +runtime selections fail closed when the harness is missing, unsupported, or +unavailable. Whole-agent/session runtime pins remain ignored by selection. + +### Configuration + +The Gateway-side plugin requires explicit fail-closed configuration: + +```ts +type RemoteHarnessConfig = { + schemaVersion: 1; + failClosed: true; + allowFallback: false; + allowedInnerHarnessIds: string[]; + defaultInnerHarnessId: string; + transport: RemoteTransportConfig; + egress: RemoteHarnessEgressConfig; + eventStreams?: RemoteHarnessEventStreamPolicy; +}; + +type RemoteTransportConfig = + | { kind: "stdio"; command?: string; args?: string[] } + | { kind: "websocket"; url: string; headers?: Record; connect?: boolean } + | { kind: "unix"; path: string; connect?: boolean } + | { kind: "tcp"; host: string; port: number; connect?: boolean } + | { kind: "hostUri"; uriTemplate: string }; + +type RemoteHarnessEgressConfig = + | { mode: "disabled"; requireProxy: false } + | { mode: "proxy"; requireProxy: true; proxyUrl: string }; + +type RemoteHarnessEventStreamPolicy = { + /** Unknown namespaced streams are forwarded only when this is true. */ + allowUnknownNamespacedStreams?: boolean; + /** Optional allowlist for extension streams such as codex_app_server.item. */ + extensionStreamAllowlist?: string[]; +}; +``` + +Invariants: + +- `failClosed` is always `true`. +- `allowFallback` is always `false`. +- `allowedInnerHarnessIds` must contain at least one id. +- `defaultInnerHarnessId` must be present in `allowedInnerHarnessIds`. +- Unknown top-level config fields are rejected. +- Transport configuration must not contain bearer tokens, session tokens, or + broad credentials. +- `hostUri` is a routing adapter transport. It is not identity authority and is + valid only when the embedding host installs a validator that gates the URI on + trusted runtime state. +- `connector` is reserved for a future connector SDK and is not an accepted v1 + transport kind. A v1 configuration that names `connector` must be rejected + fail-closed instead of silently mapping it to another transport. +- Container or host-runtime connectivity is outside the OpenClaw-visible + transport contract. A host-side runtime may call hostnames, service DNS names, + sidecars, reverse proxies, or service-mesh endpoints that its container network + exposes. That layer owns auth, certificates, routing policy, retries, and + connection management; the remote-harness spec only sees the resulting + stdio/WebSocket/TCP/Unix/host adapter endpoint. +- If egress proxy enforcement is configured, startup fails unless enforcement is + actually wired by the host. + +### Frame envelope + +Every protocol frame is JSON-serializable and carries a stable stream context: + +```ts +type RemoteHarnessStreamContext = { + protocolVersion: 1; + streamId: string; + runId: string; + sessionKey: string; + workspaceId: string; + seq: number; +}; + +type RemoteHarnessFrame = RemoteHarnessStreamContext & { + kind: string; +}; +``` + +Frame invariants: + +- `RunStart` is the first accepted Gateway-to-host frame for a stream. +- `RunAccepted` must precede non-terminal host-to-Gateway event frames. +- `protocolVersion`, `streamId`, `runId`, `sessionKey`, and `workspaceId` are + immutable for the stream. +- Sequence numbers are strictly increasing per direction. +- Unknown non-extension frame kinds fail closed. +- Unknown extension streams are rejected unless allowed by `eventStreams` policy. +- Malformed frames close or reject the stream without crashing host loops. +- `RunFinal`, `RunFailed`, or `RunCancelled` ends the stream. +- Post-terminal duplicate/stale frames can be ignored only after validating the + immutable stream context. + +### Gateway-to-host frames + +Gateway-to-host frames carry run control and OpenClaw decisions: + +```ts +type BrainFrame = + | RunStartFrame + | CancelRunFrame + | HeartbeatFrame + | ApprovalDecisionFrame + | ToolAuthorizationDecisionFrame + | MemoryWriteDecisionFrame + | PermissionDecisionFrame + | UserInputResponseFrame + | DynamicToolResponseFrame + | ModelProxyChunkFrame + | ModelProxyFinalFrame + | ModelProxyErrorFrame + | CompactSessionFrame + | ResetSessionFrame; +``` + +#### `RunStart` + +`RunStart` carries host-authoritative context. The remote agent must not replace +these fields with request headers, URL paths, hostnames, or arbitrary transport +metadata. + +```ts +type RunStartFrame = RemoteHarnessStreamContext & { + kind: "RunStart"; + innerHarnessId: string; + input: JsonValue; + context: { + schemaVersion: 1; + agentId?: string; + sessionId: string; + sessionFile?: string; + sandboxSessionKey?: string; + workspaceDir?: string; + cwd?: string; + provider: string; + modelId: string; + requestedModelId?: string | null; + runtimePlan?: JsonValue; + trigger?: "cron" | "heartbeat" | "manual" | "memory" | "overflow" | "user"; + message?: { + channel?: string; + provider?: string; + to?: string; + threadId?: string | number; + currentMessageId?: string | number; + senderId?: string | null; + senderName?: string | null; + senderUsername?: string | null; + senderIsOwner?: boolean; + groupId?: string | null; + groupChannel?: string | null; + groupSpace?: string | null; + memberRoleIds?: string[]; + }; + toolPolicy?: { + toolsAllow?: string[]; + disableTools?: boolean; + requireExplicitMessageTarget?: boolean; + }; + streaming?: { + blockReplyBreak?: "text_end" | "message_end"; + suppressLiveStreamOutput?: boolean; + silentExpected?: boolean; + }; + limits?: { + timeoutMs: number; + runTimeoutOverrideMs?: number; + contextTokenBudget?: number; + }; + }; +}; +``` + +`RunStart.context` intentionally contains runtime facts and routing metadata, not +provider credentials. Sensitive auth remains Gateway-owned or harness-owned. + +#### Decisions and callbacks + +```ts +type ApprovalDecisionFrame = RemoteHarnessStreamContext & { + kind: "ApprovalDecision"; + requestId: string; + approved: boolean; + reason?: string; + scope?: "turn" | "session"; +}; + +type ToolAuthorizationDecisionFrame = RemoteHarnessStreamContext & { + kind: "ToolAuthorizationDecision"; + requestId: string; + approved: boolean; + reason?: string; +}; + +type MemoryWriteDecisionFrame = RemoteHarnessStreamContext & { + kind: "MemoryWriteDecision"; + requestId: string; + approved: boolean; + reason?: string; +}; + +type PermissionDecisionFrame = RemoteHarnessStreamContext & { + kind: "PermissionDecision"; + requestId: string; + permissions: JsonValue; + scope: "turn" | "session"; + strictAutoReview?: boolean; +}; + +type UserInputResponseFrame = RemoteHarnessStreamContext & { + kind: "UserInputResponse"; + requestId: string; + answers: Record; +}; + +type DynamicToolResponseFrame = RemoteHarnessStreamContext & { + kind: "DynamicToolResponse"; + requestId: string; + contentItems: JsonValue[]; + success: boolean; +}; +``` + +`PermissionDecision`, `UserInputResponse`, and `DynamicToolResponse` are required +because Codex exposes request-permissions, request-user-input, and dynamic tool +calls as first-class protocol events, while OpenClaw owns the user-visible +review and tool execution surfaces. + +#### Model proxy frames + +Model proxy frames are reserved for a future host-defined egress path. A v1 +implementation may reject `ModelProxyRequest` from the host and never emit these +responses. If implemented, the Gateway owns provider credentials and streams +model output back as: + +```ts +type ModelProxyChunkFrame = RemoteHarnessStreamContext & { + kind: "ModelProxyChunk"; + requestId: string; + delta: string; +}; + +type ModelProxyFinalFrame = RemoteHarnessStreamContext & { + kind: "ModelProxyFinal"; + requestId: string; + output: JsonValue; + usage?: JsonValue; +}; + +type ModelProxyErrorFrame = RemoteHarnessStreamContext & { + kind: "ModelProxyError"; + requestId: string; + error: { message: string; code?: string }; +}; +``` + +### Host-to-Gateway frames + +Host-to-Gateway frames carry lifecycle, event streams, requests, and terminal +outcomes: + +```ts +type AgentFrame = + | RunAcceptedFrame + | AssistantMessageStartFrame + | AssistantDeltaFrame + | VisibleReplyFrame + | ReasoningDeltaFrame + | ReasoningEndFrame + | ToolStartedFrame + | ToolProgressFrame + | ToolFinishedFrame + | ToolStreamBoundaryFrame + | AgentEventFrame + | AgentToolResultFrame + | ExecutionStartedFrame + | ExecutionPhaseFrame + | RunProgressFrame + | LaneWaitFrame + | SessionIdChangedFrame + | UsageUpdateFrame + | ApprovalRequestedFrame + | ToolAuthorizationRequestedFrame + | MemoryWriteRequestedFrame + | PermissionRequestedFrame + | UserInputRequestedFrame + | DynamicToolCallRequestedFrame + | ModelProxyRequestFrame + | CheckpointFrame + | ArtifactProducedFrame + | RunFinalFrame + | RunFailedFrame + | RunCancelledFrame + | HeartbeatFrame; +``` + +Common host-to-Gateway frame shapes: + +```ts +type RunAcceptedFrame = RemoteHarnessStreamContext & { + kind: "RunAccepted"; + selectedInnerHarnessId: string; + selectedInnerHarnessVersion: string; +}; + +type AssistantMessageStartFrame = RemoteHarnessStreamContext & { + kind: "AssistantMessageStart"; + itemId?: string; +}; + +type ExecutionStartedFrame = RemoteHarnessStreamContext & { + kind: "ExecutionStarted"; + lifecycleGeneration?: string; +}; + +type ExecutionPhaseFrame = RemoteHarnessStreamContext & { + kind: "ExecutionPhase"; + phase: + | "runner_entered" + | "workspace" + | "runtime_plugins" + | "before_agent_reply" + | "model_resolution" + | "auth" + | "context_engine" + | "attempt_dispatch" + | "context_assembled" + | "turn_accepted" + | "process_spawned" + | "tool_execution_started" + | "assistant_output_started" + | "model_call_started"; + provider?: string; + model?: string; + backend?: string; + source?: string; + tool?: string; + toolCallId?: string; + itemId?: string; + firstModelCallStarted?: boolean; +}; + +type RunProgressFrame = RemoteHarnessStreamContext & { + kind: "RunProgress"; + reason: string; + provider?: string; + model?: string; + backend?: string; +}; + +type LaneWaitFrame = RemoteHarnessStreamContext & { + kind: "LaneWait"; + waitMs: number; + queuedAhead: number; + waiting?: boolean; +}; + +type SessionIdChangedFrame = RemoteHarnessStreamContext & { + kind: "SessionIdChanged"; + sessionId: string; +}; + +type UsageUpdateFrame = RemoteHarnessStreamContext & { + kind: "UsageUpdate"; + usage: JsonValue; + source?: "assistant" | "model" | "harness"; +}; + +type AgentToolResultFrame = RemoteHarnessStreamContext & { + kind: "AgentToolResult"; + toolName: string; + result: JsonValue; + isError: boolean; +}; + +type ToolStreamBoundaryFrame = RemoteHarnessStreamContext & { + kind: "ToolStreamBoundary"; + toolCallId?: string; +}; + +type CheckpointFrame = RemoteHarnessStreamContext & { + kind: "Checkpoint"; + checkpointId: string; + state: JsonValue; +}; + +type ArtifactProducedFrame = RemoteHarnessStreamContext & { + kind: "ArtifactProduced"; + artifactId: string; + uri: string; + metadata?: JsonValue; +}; + +type ModelProxyRequestFrame = RemoteHarnessStreamContext & { + kind: "ModelProxyRequest"; + requestId: string; + model: string; + payload: JsonValue; +}; +``` + +Common control frames shared by both directions: + +```ts +type HeartbeatFrame = RemoteHarnessStreamContext & { + kind: "Heartbeat"; + sentAt?: string; +}; + +type CancelRunFrame = RemoteHarnessStreamContext & { + kind: "CancelRun"; + reason?: string; +}; + +type CompactSessionFrame = RemoteHarnessStreamContext & { + kind: "CompactSession"; + reason?: "manual" | "threshold" | "overflow"; +}; + +type ResetSessionFrame = RemoteHarnessStreamContext & { + kind: "ResetSession"; + reason?: "new" | "reset" | "idle" | "daily" | "compaction" | "deleted" | "unknown"; +}; +``` + +### Required OpenClaw event mapping + +The following mapping is the contract that remote harnesses must preserve when +they observe or produce the corresponding behavior. Implementations may add +harness-specific extension events, but they must not drop the canonical event +when one is available. + +| OpenClaw local surface | Remote frame or stream | Required behavior | +| --- | --- | --- | +| `onExecutionStarted({ lifecycleGeneration })` | `ExecutionStarted` | Signals that the selected harness has entered execution and gives the Gateway the owning lifecycle generation when known. | +| `onExecutionPhase(...)` | `ExecutionPhase` and `AgentEvent(stream="item")` when user-visible | Carries milestones such as `runner_entered`, `workspace`, `runtime_plugins`, `before_agent_reply`, `model_resolution`, `auth`, `context_engine`, `attempt_dispatch`, `context_assembled`, `turn_accepted`, `process_spawned`, `tool_execution_started`, `assistant_output_started`, and `model_call_started`. | +| `onLaneWait(...)` | `LaneWait` | Preserves queued/waiting status before execution. | +| `onRunProgress(...)` | `RunProgress` | Preserves provider/model/backend progress messages. | +| `onSessionIdChanged(sessionId)` | `SessionIdChanged` | Lets the Gateway update active session identity after native resume/start decisions. | +| `onAssistantMessageStart()` | `AssistantMessageStart` | Marks the first assistant item before deltas. | +| `onPartialReply(payload)` | `AssistantDelta` | Streams assistant text with `delta`, cumulative `text`, optional `replace`, and optional usage. | +| `onBlockReply(payload)` / `onBlockReplyFlush()` | `VisibleReply` | Carries text/media/source-reply payloads that should be delivered as block replies rather than only append deltas. | +| `onReasoningStream(payload)` | `ReasoningDelta` | Carries reasoning text snapshots or deltas, media URLs, and `isReasoningSnapshot`. | +| `onReasoningEnd()` | `ReasoningEnd` | Closes a reasoning block so channels can flush thinking UI. | +| `onToolResult(payload)` | `ToolProgress` or `VisibleReply` | Carries user-visible tool result text/media/status that channels should render. | +| `onAgentToolResult({ toolName, result, isError })` | `AgentToolResult` | Carries sanitized private per-tool result for replay-safety and diagnostics. | +| `onToolStreamBoundary()` | `ToolStreamBoundary` | Preserves spacing/order after a tool finishes streaming. | +| `onAgentEvent({ stream, data, sessionKey })` | `AgentEvent` | Generic sequenced OpenClaw event bus frame for canonical and extension streams. | +| `onUserMessagePersisted(message)` | `AgentEvent(stream="lifecycle", data.phase="user_message_persisted")` | Lets remote/native harnesses report the point at which the user message is durably in transcript. | +| `onAssistantErrorMessagePersisted(message)` | `AgentEvent(stream="lifecycle", data.phase="assistant_error_persisted")` | Lets the Gateway distinguish persisted error text from transient stream failures. | +| `onAttemptTimeoutArmed()` | `AgentEvent(stream="lifecycle", data.phase="timeout_armed")` | Preserves watchdog observability. | +| `onAttemptTimeout(error)` | `AgentEvent(stream="lifecycle", data.phase="timeout")` and terminal metadata | Preserves timeout phase and liveness data. | +| `onAttemptAbort()` | `RunCancelled` or `AgentEvent(stream="lifecycle", data.phase="aborted")` | Preserves explicit native abort acknowledgement. | + +### Canonical `AgentEvent` streams + +`AgentEvent` is the escape hatch that keeps the remote protocol aligned with +OpenClaw's local event bus without adding a new top-level frame for every UI +or diagnostic stream: + +```ts +type AgentEventFrame = RemoteHarnessStreamContext & { + kind: "AgentEvent"; + stream: + | "lifecycle" + | "tool" + | "assistant" + | "error" + | "item" + | "plan" + | "approval" + | "command_output" + | "patch" + | "compaction" + | "thinking" + | `${string}.${string}`; + data: Record; + sessionKey?: string; + agentId?: string; +}; +``` + +Canonical stream requirements: + +- `lifecycle`: emits `phase: "start"`, `"finishing"`, `"end"`, `"error"`, + timeout, abort, and persistence-related phases. Terminal lifecycle events carry + `stopReason`, `yielded`, `timeoutPhase`, `providerStarted`, `aborted`, + `livenessState`, and `replayInvalid` when known. +- `assistant`: emits assistant snapshots/deltas, including replacement snapshots + for provisional/native items that append-only partial-reply consumers cannot + safely concatenate. +- `thinking`: emits reasoning snapshots/deltas for live thinking UI. +- `tool`: emits tool `start`, `update`, and `result` with sanitized args/results, + tool name, tool call id, error state, and metadata. +- `item`: emits normalized activity-feed items with `itemId`, `phase`, `kind`, + `title`, `status`, timestamps, progress text, approval ids, and summaries. +- `plan`: emits plan updates, exit-plan-mode requests/completions, explanation, + steps, actions, selected action, and feedback. +- `approval`: emits approval `requested` and `resolved` events for command, + plugin, permission, and unknown approval families. +- `command_output`: emits command stdout/stderr deltas and terminal command + output with status, exit code, duration, and cwd when known. +- `patch`: emits patch summary with added, modified, deleted files and summary + text. +- `compaction`: emits compaction `start` and `end`, completed/aborted state, + retry state, backend, item/thread/turn ids when known. +- `error`: emits non-terminal stream/provider/harness errors that should be + visible to diagnostics but do not replace terminal `RunFailed`. +- Namespaced extension streams such as `codex_app_server.item`, + `codex_app_server.guardian`, `codex_app_server.hook`, and + `codex_app_server.lifecycle` are allowed only when the Gateway configuration + admits the namespace or the stream is known to the plugin. + +### Typed event frames + +Typed frames exist for high-value event classes that every host should implement +without inspecting generic stream payloads: + +```ts +type AssistantDeltaFrame = RemoteHarnessStreamContext & { + kind: "AssistantDelta"; + delta: string; + text?: string; + replace?: boolean; + replaceable?: boolean; + usage?: JsonValue; +}; + +type VisibleReplyFrame = RemoteHarnessStreamContext & { + kind: "VisibleReply"; + text?: string; + mediaUrls?: string[]; + audioAsVoice?: boolean; + trustedLocalMedia?: boolean; + sourceReply?: JsonValue; + final?: boolean; +}; + +type ReasoningDeltaFrame = RemoteHarnessStreamContext & { + kind: "ReasoningDelta"; + text?: string; + delta?: string; + mediaUrls?: string[]; + isReasoningSnapshot?: boolean; +}; + +type ReasoningEndFrame = RemoteHarnessStreamContext & { + kind: "ReasoningEnd"; +}; + +type ToolStartedFrame = RemoteHarnessStreamContext & { + kind: "ToolStarted"; + toolCallId: string; + name: string; + args?: JsonValue; + meta?: string; + startedAt?: number; +}; + +type ToolProgressFrame = RemoteHarnessStreamContext & { + kind: "ToolProgress"; + toolCallId: string; + name?: string; + message?: string; + partialResult?: JsonValue; + progressText?: string; +}; + +type ToolFinishedFrame = RemoteHarnessStreamContext & { + kind: "ToolFinished"; + toolCallId: string; + name: string; + result: JsonValue; + isError: boolean; + meta?: string; + endedAt?: number; +}; +``` + +Typed frames and `AgentEvent` are complementary. A host that emits +`ToolStarted` should also emit the matching canonical `AgentEvent(stream="tool")` +and `AgentEvent(stream="item")` when it has enough data to preserve OpenClaw's +activity feed. The Gateway deduplicates by `toolCallId`/`itemId`. + +### Requests initiated by the remote host + +Remote native harnesses can need OpenClaw-owned decisions or actions. These are +host-to-Gateway request frames: + +```ts +type ApprovalRequestedFrame = RemoteHarnessStreamContext & { + kind: "ApprovalRequested"; + requestId: string; + prompt: string; + details?: JsonValue; +}; + +type ToolAuthorizationRequestedFrame = RemoteHarnessStreamContext & { + kind: "ToolAuthorizationRequested"; + requestId: string; + toolName: string; + details?: JsonValue; +}; + +type MemoryWriteRequestedFrame = RemoteHarnessStreamContext & { + kind: "MemoryWriteRequested"; + requestId: string; + proposedMemory: JsonValue; +}; + +type PermissionRequestedFrame = RemoteHarnessStreamContext & { + kind: "PermissionRequested"; + requestId: string; + reason?: string; + permissions: JsonValue; + cwd?: string; + environmentId?: string; +}; + +type UserInputRequestedFrame = RemoteHarnessStreamContext & { + kind: "UserInputRequested"; + requestId: string; + questions: Array<{ + id: string; + header: string; + question: string; + isOther?: boolean; + isSecret?: boolean; + options?: Array<{ label: string; description: string }>; + }>; + autoResolutionMs?: number; +}; + +type DynamicToolCallRequestedFrame = RemoteHarnessStreamContext & { + kind: "DynamicToolCallRequested"; + requestId: string; + namespace?: string; + tool: string; + arguments: JsonValue; +}; +``` + +The Gateway must apply the same approval, user-input, dynamic-tool, memory, and +permission policies it would apply for an in-process harness. If a policy or UI +route is unavailable, the Gateway returns a denial/unavailable decision rather +than letting the remote host proceed implicitly. + +### Terminal frames and result mapping + +```ts +type RunFinalFrame = RemoteHarnessStreamContext & { + kind: "RunFinal"; + result: JsonValue; + assistantTexts?: string[]; + usage?: JsonValue; + replayMetadata?: JsonValue; + itemLifecycle?: { + startedCount: number; + completedCount: number; + activeCount: number; + }; +}; + +type RunFailedFrame = RemoteHarnessStreamContext & { + kind: "RunFailed"; + error: { message: string; code?: string; data?: JsonValue }; + replaySafe?: boolean; +}; + +type RunCancelledFrame = RemoteHarnessStreamContext & { + kind: "RunCancelled"; + reason?: string; + timedOut?: boolean; + timeoutPhase?: string; +}; +``` + +The Gateway maps terminal frames into `AgentHarnessAttemptResult`. A remote +harness must send the final assistant text, usage, replay-safety, and item +lifecycle metadata it knows so the outer OpenClaw attempt can make the same +fallback, retry, transcript, and lifecycle decisions as a local native harness. + +### Existing event-source mapping + +The v1 bridge must be able to represent these current sources: + +| Source | Events observed today | Remote mapping | +| --- | --- | --- | +| OpenClaw embedded runner | execution started/phase, lane wait, run progress, session id change, assistant partials, block replies, reasoning, tool results, private tool results, generic agent events, tool stream boundaries, user/error persistence, timeout/abort hooks | typed frames plus `AgentEvent` canonical streams | +| OpenClaw agent event bus | `lifecycle`, `tool`, `assistant`, `error`, `item`, `plan`, `approval`, `command_output`, `patch`, `compaction`, `thinking`, extension streams | `AgentEvent` | +| Codex app-server plugin | assistant deltas with replaceable snapshots, reasoning snapshots, plan updates, item started/completed, tool progress, compaction start/end, approval events, permission requests, user-input requests, dynamic tool calls, MCP/tool events, guardian/hook diagnostics, native lifecycle streams | typed frames for common events, `PermissionRequested`, `UserInputRequested`, `DynamicToolCallRequested`, and namespaced `AgentEvent` for Codex-specific diagnostics | +| Upstream Codex protocol | `TurnStarted`, `TurnComplete`, `AgentMessageContentDelta`, `PlanDelta`, `ReasoningContentDelta`, `ItemStarted`, `ItemCompleted`, `ExecCommandBegin`, `ExecCommandOutputDelta`, `ExecCommandEnd`, `PatchApplyBegin`, `PatchApplyUpdated`, `PatchApplyEnd`, `RequestPermissions`, `RequestUserInput`, `DynamicToolCallRequest`, `McpToolCallBegin`, `McpToolCallEnd`, `SubAgentActivity`, hook events | typed frames and canonical/namespaced `AgentEvent` streams | +| Copilot SDK bridge | assistant message deltas, assistant usage, tool start/complete counts, plan changed, exit-plan-mode request/completion, subagent started/completed/failed, compaction start/complete, session idle/error/abort | typed assistant/usage frames, `AgentEvent(stream="plan")`, `AgentEvent(stream="item")`, `AgentEvent(stream="compaction")`, terminal failure/cancel frames | +| Portable Lobster remote-harness plugin | `AssistantDelta`, `ReasoningDelta`, tool started/progress/finished, checkpoint, artifact, approval/tool-auth/memory requests, terminal frames, heartbeat | retained as the minimal subset, extended by `AgentEvent`, visible reply, plan, command output, patch, compaction, user-input, permission, dynamic tool, usage, and lifecycle frames | + +### Host-side inner harness API + +The host-side API remains small but must expose event callbacks instead of only +assistant/tool callbacks: + +```ts +type InnerRunRequest = { + runId: string; + sessionKey: string; + workspaceId: string; + streamId: string; + input: JsonValue; + context: RunStartFrame["context"]; +}; + +type InnerHarnessCallbacks = { + assistantDelta(frame: Omit): Promise; + visibleReply(frame: Omit): Promise; + reasoningDelta(frame: Omit): Promise; + reasoningEnd(): Promise; + toolStarted(frame: Omit): Promise; + toolProgress(frame: Omit): Promise; + toolFinished(frame: Omit): Promise; + agentEvent(frame: Omit): Promise; + requestApproval(frame: Omit): Promise; + requestPermission(frame: Omit): Promise; + requestUserInput(frame: Omit): Promise; + callDynamicTool(frame: Omit): Promise; +}; + +type InnerHarness = { + id: string; + version: string; + run( + request: InnerRunRequest, + callbacks: InnerHarnessCallbacks, + signal: AbortSignal, + ): Promise; +}; +``` + +A generic command inner harness can exist for local and product integration +experiments. Command execution must use an allowlist-style environment: + +- inherit only ambient process basics such as `PATH`, `HOME`, `LANG`, `TZ`, and + trusted CA bundle variables; +- pass explicit host-provided environment entries only after filtering sensitive + credential-shaped names; +- abort the child process on cancellation; +- stream stdout as assistant text only after the process succeeds unless the + command explicitly speaks the remote-harness event protocol. + +`deterministic-poc` may exist for repeatable tests, but it is test-only and must +not be a production fallback. + +### Transport authority and trusted identity + +The transport is a routing mechanism, not authority. + +The upstream plugin must not require bearer tokens in OpenClaw-visible +configuration. If a host needs authenticated routing, it should put authority +outside the plugin config, such as: + +- process-local sockets; +- reverse-proxy admission; +- container-provided DNS names, sidecars, service-mesh routes, or hostnames that + are reachable only from the host-side runtime container; +- host-managed connector objects, if a deployment provides them outside this + spec; +- short-lived substream metadata produced by the host runtime; +- container-runtime capability state that the transport adapter consults before + accepting a bridge connection. + +The remote-harness protocol does not standardize connector auth, connection +pooling, service discovery, retries, certificate handling, or credential +projection. Deployments such as a containerized host runtime can solve those +concerns in the container or host platform and expose only a reachable endpoint +to the bridge. + +The remote agent must not treat hostnames, URL paths, headers, or arbitrary +request metadata as proof of user, session, workspace, or run identity. +`RunStart.context` is the authoritative stream identity. + +### Container-runtime contribution boundary + +This RFC deliberately separates upstreamable bridge contracts from host-specific +container orchestration: + +| Surface | Upstream contract | Host-specific implementation | +| --- | --- | --- | +| Remote AgentHarness plugin | yes | no | +| Protocol frame types and validators | yes | no | +| Host runtime and inner-harness registry | yes | no | +| Reference command harness/container | yes | optional | +| Event conformance suite | yes | no | +| Container allocator interface | yes, as a future RFC or implementation issue | provider-specific allocator | +| Worker pool, deployment, EV2, Azure, Plex, or ECS wiring | no | yes | +| Container DNS, hostnames, sidecars, service mesh, certificates, and connection management | no | yes | +| Rollout audience authoring | no | yes | +| Runtime admission invariants | yes | host chooses storage/evaluation mechanism | + +Any container runtime that claims remote-harness support must prove: + +- explicit remote-harness requests fail closed when disabled, disallowed, + unavailable, or unready; +- headerless rollout misses can remain on existing local Gateway behavior; +- request headers cannot set session, run, workspace, or user identity; +- the adapter accepts remote bridge traffic only while trusted runtime state says + the current Gateway is in remote-harness mode; +- raw provider/model/caller credentials are not projected into the Agent + container by default; +- all required event streams from this RFC are forwarded or intentionally denied + with a visible conformance failure. + +### Compatibility and migration + +This is additive: + +- existing OpenClaw runtimes continue to work unchanged; +- existing plugins are not required to implement remote harness; +- `remote-harness` is selected only by explicit provider/model runtime + configuration; +- unsupported transports or unavailable remote hosts fail the selected attempt; +- v1 frame validation rejects unknown core frame kinds; +- `AgentEvent` provides a controlled extension path for existing harness-specific + streams without requiring a protocol revision for every diagnostic event. + +The initial implementation may include a compatibility shim for current +AgentHarness SDK types. That shim should stay in one package boundary and be +removed once the public SDK exposes native remote-harness frame and event types. + +### Transcripts and context-engine runtime settings + +Remote harness runs participate in transcript and lifecycle persistence through +the same AgentHarness result, assistant delivery callbacks, user/error message +persistence callbacks, and `AgentEvent` streams that local harnesses use. v1 does +not add a remote-only transcript side channel. + +Remote harness runs also participate in context-engine behavior through the +normal OpenClaw attempt lifecycle. The bridge preserves context-engine execution +phase events and carries selected runtime facts in `RunStart.context`, but v1 +does not define a new remote-only context-engine runtime-settings payload or let +the remote host mutate OpenClaw context-engine selection, routing, fallback, or +auth policy. + +## Rationale + +The selected design uses the AgentHarness plugin seam instead of adding a new +special-purpose execution path to OpenClaw core. That keeps the OpenClaw model +simple: provider/model runtime selection chooses a harness, and the harness owns +execution details. A remote harness is operationally different, but it still +produces the same attempt events and terminal result as a local native harness. + +A typed frame protocol is preferred over forwarding opaque process stdio because +OpenClaw relies on structured events. Tool progress, activity-feed items, +command output, patch summaries, approvals, plans, compaction, reasoning, +artifacts, lifecycle, cancellation, and final results should not be parsed out +of free-form text. + +A generic `AgentEvent` frame is included because OpenClaw already has an event +bus with stable stream names and harness-specific extension streams. Defining a +new top-level frame for every Codex or Copilot diagnostic would freeze too much +provider detail into the core remote-harness protocol. Conversely, using only a +generic event frame would make common assistant/tool/approval behavior harder to +implement correctly. The mixed design gives strict typed frames for common +contract surfaces and a namespaced extension stream for harness-specific detail. + +Fail-closed selection is deliberate. If a user or host explicitly selects remote +execution, silently falling back to a local model runtime can violate deployment +policy, egress policy, reproducibility expectations, canary controls, or +container-isolation guarantees. `auto` and headerless default behavior can remain +conservative, but explicit remote selection must either run remotely or fail. + +The proposal avoids putting tokens in plugin config because plugin config is +often visible to host tooling, logs, diagnostics, or user-editable config files. +Host integrations can still enforce authenticated routing, but that authority +belongs outside the OpenClaw-visible transport object. + +## Unresolved questions + +- Should `hostUri` be accepted as a public transport name, or should upstream use + a different name such as `hostAdapter` for validated reverse-proxy paths? +- Should model proxy frames be removed from v1 until a separate model-egress RFC + is accepted, or kept as reserved fail-closed frame names? +- Which package should own the public TypeScript frame types: + `openclaw/plugin-sdk/agent-harness`, a new `openclaw/remote-harness-protocol`, + or both with one re-exporting the other? +- Should `RunStart.context.workspaceDir` be exposed to remote hosts as a path, + or should v1 carry only an opaque `workspaceId` plus host-owned workspace + mounting metadata? +- Should `AgentEvent` extension streams be allowlisted by namespace, by plugin + id, or by the selected inner harness id? +- What is the minimum conformance suite for third-party remote harness hosts: + static schema validation only, golden frame transcripts, or an executable + fake host that drives OpenClaw callbacks end-to-end? +- Should OpenClaw require every remote harness to emit both typed frames and the + matching canonical `AgentEvent`, or should the Gateway synthesize canonical + agent events from typed frames when possible? +- Should native subagent/task mirroring be standardized in this RFC or in a + follow-up RFC that covers multi-agent task runtimes across Codex, Copilot, Pi, + and OpenClaw-native subagents? From f877ec6a6c2806ee18097c68788fdbb15b7b07c5 Mon Sep 17 00:00:00 2001 From: Eduardo Piva Date: Mon, 6 Jul 2026 21:34:50 -0700 Subject: [PATCH 2/4] docs: refactor remote harness RFC for protocol review --- ...agent-harness-bridge-and-event-protocol.md | 1529 ++++++----------- rfcs/0010/protocol-v1-reference.md | 206 +++ 2 files changed, 736 insertions(+), 999 deletions(-) create mode 100644 rfcs/0010/protocol-v1-reference.md diff --git a/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md b/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md index f4b252b7..af8a1d64 100644 --- a/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md +++ b/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md @@ -3,8 +3,8 @@ title: Remote Agent Harness Bridge and Event Protocol authors: - Omar Shahine - Eduardo Piva -created: 2026-07-07 -last_updated: 2026-07-07 +created: 2026-07-06 +last_updated: 2026-07-06 status: draft issue: rfc_pr: @@ -12,937 +12,444 @@ rfc_pr: # Proposal: Remote Agent Harness Bridge and Event Protocol -Assistance note: this RFC updates the remote AgentHarness bridge draft after -reviewing OpenClaw's current AgentHarness callbacks, Codex app-server event -projection, Copilot SDK event bridge, Codex upstream protocol events, and the -portable remote-harness implementation currently carried in Lobster. - ## Summary -Add a first-class remote AgentHarness bridge to OpenClaw and standardize the -streaming event protocol that lets a selected native harness run outside the -Gateway process while preserving OpenClaw's local lifecycle, progress, -approval, tool, plan, compaction, transcript, and user-visible delivery events. +Define a transport-neutral protocol and lifecycle contract for running selected +AgentHarness attempts outside the OpenClaw Gateway process while preserving the +observable behavior of a local AgentHarness: lifecycle, assistant output, +reasoning, plans, tools, approvals, command output, patch summaries, compaction, +transcripts, cancellation, and terminal results. ## Motivation -OpenClaw can already route selected provider/model turns through native -AgentHarness plugins such as Codex and Copilot. Those harnesses do more than -return final assistant text. They stream assistant deltas, reasoning snapshots, -plans, tool lifecycle, command output, patch summaries, compaction state, -approvals, permission requests, user-input prompts, usage snapshots, lifecycle -state, and harness-specific diagnostic events into OpenClaw surfaces. - -A remote harness that only transports `RunStart`, assistant text, a few tool -notifications, and a terminal result is not equivalent to a local Codex, Pi, or -Copilot harness. It would lose the local OpenClaw events that the Gateway uses -to update WebChat, CLI/TUI status, Control UI, transcripts, task mirrors, -replay-safety decisions, approval resolution, and lifecycle persistence. - -Without an upstream event contract, every host or container runtime has to -invent its own partial mapping. That makes the boundary unclear and creates -compatibility risks: - -- assistant text may render but plans, reasoning, command output, and patch - summaries may disappear; -- approval or permission prompts may bypass OpenClaw policy or become - unreviewable; -- tool execution progress may be visible in one harness but not another; -- compaction and replay-safety state may be wrong after a remote native turn; -- container hosts may accidentally trust request headers for session, run, or - workspace identity; -- future open-source harness or container-runtime contributions cannot prove - conformance against one shared contract. - -This RFC standardizes the remote bridge as an AgentHarness plugin plus an event -protocol. The protocol is transport-neutral, fail-closed, and designed to carry -both harness-originated events and OpenClaw-local event callbacks that must cross -an out-of-process boundary. +OpenClaw can route provider/model turns through native AgentHarness plugins such +as Codex and Copilot. Those harnesses do more than return final assistant text. +They stream assistant deltas, reasoning snapshots, plans, tool lifecycle, +command output, patch summaries, compaction state, approvals, permission +requests, user-input prompts, usage snapshots, lifecycle state, and +harness-specific diagnostics into OpenClaw surfaces. + +A remote harness that transports only a prompt, assistant text, and terminal +result is not equivalent to a local harness. It can render text while silently +losing the events that the Gateway uses for WebChat, CLI/TUI status, Control UI, +transcripts, task mirrors, replay-safety decisions, approval resolution, and +lifecycle persistence. + +The missing upstream contract also blocks portable contributions. Hosts that run +agents in containers, warm pools, separate processes, or provider-specific +sandboxes must currently invent partial mappings. That creates compatibility and +security risks: + +- plans, reasoning, tool progress, command output, patch summaries, and + compaction events may disappear; +- approval, permission, and user-input requests may bypass OpenClaw policy or + become unreviewable; +- local and remote runs may disagree about transcript, replay-safety, or + terminal lifecycle state; +- container hosts may accidentally treat request headers or route metadata as + identity authority; +- third-party remote harness hosts have no shared conformance target. + +This RFC standardizes the remote bridge as an AgentHarness protocol boundary. It +keeps OpenClaw responsible for session orchestration, user-visible event +semantics, approval decisions, and terminal persistence while allowing the +selected native harness to execute in a separate host-managed process or +container. ## Goals -- Define a platform-neutral remote AgentHarness bridge selected through existing - provider/model `agentRuntime.id` configuration. -- Standardize the v1 frame protocol for run control, terminal outcomes, - approval decisions, tool callbacks, and typed event streaming. -- Make OpenClaw's local AgentHarness callback/event surface explicit so Codex, - Pi, Copilot, and future harnesses can preserve equivalent user-visible - behavior when remote. -- Preserve OpenClaw event streams for `lifecycle`, `assistant`, `thinking`, - `tool`, `item`, `plan`, `approval`, `command_output`, `patch`, `compaction`, - `error`, and namespaced harness-extension streams. -- Preserve fail-closed runtime selection: an explicit remote-harness selection - must run remotely or fail; it must not silently fall back to local OpenClaw. -- Keep identity authority host-owned. `RunStart` context, not request headers, - is authoritative for run, session, workspace, and user context. -- Avoid provider/model credentials in the remote agent process unless a future - accepted RFC defines a credential or model-proxy contract. -- Provide a conformance target for future open-source remote-harness and +- Define the actors, workflow, and lifecycle for a remote AgentHarness bridge. +- Preserve local-vs-remote behavioral equivalence for OpenClaw's observable + AgentHarness event surface. +- Keep runtime selection explicit and fail-closed: when a model/provider selects + the remote harness, the attempt either runs remotely or fails visibly. +- Keep the protocol transport-neutral: stdio, WebSocket, TCP, Unix socket, and + host-adapter paths are routing choices, not semantic differences. +- Keep identity authority host-owned and explicit. `RunStart` stream context, + not headers, hostnames, URL paths, or arbitrary transport metadata, is + authoritative for run/session/workspace identity. +- Keep OpenClaw-owned decisions in OpenClaw: approvals, permissions, dynamic + tools, memory-write decisions, and user-input prompts are requested by the + remote host and resolved by the Gateway through normal policy/UI routes. +- Avoid provider/model/caller credentials in OpenClaw-visible remote-harness + configuration or default remote-agent container projection. +- Define a conformance target for future upstream remote-harness and container-runtime contributions. ## Non-Goals - This RFC does not make remote harness the default OpenClaw runtime. -- This RFC does not define a specific cloud worker pool, deployment system, - Azure/Plex/EV2 integration, or rollout audience mechanism. +- This RFC does not define a cloud worker pool, deployment system, Azure/Plex/EV2 + integration, ECS/rollout policy, or production audience gate. +- This RFC does not standardize connector auth, service discovery, certificates, + retries, or connection pooling; those can be owned by the host-side runtime + container or platform. - This RFC does not distribute provider, model, Graph, M365, GitHub, or other caller credentials to remote agent containers. -- This RFC does not define a general-purpose remote filesystem or shell API - outside the harness/tool event contract. -- This RFC does not change OpenClaw's approval, tool-policy, or memory-policy - decisions; it transports those decisions across the remote boundary. -- This RFC does not require every harness to emit every event. It requires every - v1 bridge implementation to preserve and forward the event classes it observes - or produces, and to fail closed for unsupported request/decision frames. -- This RFC does not standardize host-specific container allocation internals in - v1. It defines the bridge contract that container runtimes must satisfy. +- This RFC does not define a general-purpose remote filesystem, shell, or tool + execution API outside the AgentHarness event/request contract. +- This RFC does not change OpenClaw approval, permission, tool, memory, or + context-engine policy. It transports requests and decisions across the remote + boundary. +- This RFC does not require every harness to emit every event class. It requires + a bridge to preserve the event classes that the selected native harness + observes or produces and to fail closed for unsupported core frames. ## Proposal -Introduce a bundled `remote-harness` AgentHarness plugin and a public remote -harness event protocol package. The plugin has two sides: - -1. **Gateway-side harness.** Runs inside the OpenClaw Gateway process, registers - an AgentHarness, resolves explicit runtime selection, opens the configured - transport, sends `RunStart`, maps remote frames into OpenClaw callbacks, - forwards OpenClaw decisions back to the host, and returns the terminal - `AgentHarnessAttemptResult`. -2. **Host-side runtime.** Runs outside the Gateway process or in a separate - container, validates `RunStart`, selects an allowlisted inner harness, runs - the native harness, and sends remote frames for all assistant, reasoning, - plan, tool, approval, command, patch, compaction, lifecycle, and diagnostic - events it observes. - -The bridge is a protocol and lifecycle contract, not a deployment contract. A -host can connect the sides with stdio, WebSocket, TCP, Unix socket, or a -host-owned adapter transport that is explicitly wired by the embedding runtime. - -### Plugin registration and selection - -The current OpenClaw registration contract remains the source of truth: - -```ts -import type { AgentHarness } from "openclaw/plugin-sdk/agent-harness"; -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; - -const remoteHarnessAgentHarness: AgentHarness = { - id: "remote-harness", - label: "Remote Harness", - supports(ctx) { - return ctx.requestedRuntime === "remote-harness" - ? { supported: true, priority: 100 } - : { supported: false, reason: "remote-harness requires explicit runtime id" }; - }, - async runAttempt(params) { - return await runRemoteHarnessAttempt(params); - }, -}; - -export default definePluginEntry({ - id: "remote-harness", - name: "Remote Harness", - register(api) { - api.registerAgentHarness(remoteHarnessAgentHarness); - }, -}); -``` - -Manifest metadata is planner/catalog data, not registration by itself: - -```json -{ - "id": "remote-harness", - "name": "Remote Harness", - "activation": { - "onStartup": false, - "onAgentHarnesses": ["remote-harness"] - }, - "agentHarnesses": [ - { "id": "remote-harness", "displayName": "Remote Harness" } - ] -} -``` - -`activation.onAgentHarnesses` tells plugin loading that a runtime id can require -this plugin. The runtime behavior is registered by `api.registerAgentHarness`. -If OpenClaw does not yet accept top-level `agentHarnesses` manifest metadata, -that field is treated as proposed catalog metadata for this RFC, not as a v1 -runtime dependency. - -Runtime selection uses existing provider/model policy: - -```json -{ - "models": { - "providers": { - "openai": { - "agentRuntime": { "id": "remote-harness" } - } - } - } -} -``` - -Model-scoped policy wins, provider-scoped policy follows, and explicit plugin -runtime selections fail closed when the harness is missing, unsupported, or -unavailable. Whole-agent/session runtime pins remain ignored by selection. - -### Configuration - -The Gateway-side plugin requires explicit fail-closed configuration: - -```ts -type RemoteHarnessConfig = { - schemaVersion: 1; - failClosed: true; - allowFallback: false; - allowedInnerHarnessIds: string[]; - defaultInnerHarnessId: string; - transport: RemoteTransportConfig; - egress: RemoteHarnessEgressConfig; - eventStreams?: RemoteHarnessEventStreamPolicy; -}; - -type RemoteTransportConfig = - | { kind: "stdio"; command?: string; args?: string[] } - | { kind: "websocket"; url: string; headers?: Record; connect?: boolean } - | { kind: "unix"; path: string; connect?: boolean } - | { kind: "tcp"; host: string; port: number; connect?: boolean } - | { kind: "hostUri"; uriTemplate: string }; - -type RemoteHarnessEgressConfig = - | { mode: "disabled"; requireProxy: false } - | { mode: "proxy"; requireProxy: true; proxyUrl: string }; - -type RemoteHarnessEventStreamPolicy = { - /** Unknown namespaced streams are forwarded only when this is true. */ - allowUnknownNamespacedStreams?: boolean; - /** Optional allowlist for extension streams such as codex_app_server.item. */ - extensionStreamAllowlist?: string[]; -}; -``` - -Invariants: - -- `failClosed` is always `true`. -- `allowFallback` is always `false`. -- `allowedInnerHarnessIds` must contain at least one id. -- `defaultInnerHarnessId` must be present in `allowedInnerHarnessIds`. -- Unknown top-level config fields are rejected. -- Transport configuration must not contain bearer tokens, session tokens, or - broad credentials. -- `hostUri` is a routing adapter transport. It is not identity authority and is - valid only when the embedding host installs a validator that gates the URI on - trusted runtime state. -- `connector` is reserved for a future connector SDK and is not an accepted v1 - transport kind. A v1 configuration that names `connector` must be rejected - fail-closed instead of silently mapping it to another transport. -- Container or host-runtime connectivity is outside the OpenClaw-visible - transport contract. A host-side runtime may call hostnames, service DNS names, - sidecars, reverse proxies, or service-mesh endpoints that its container network - exposes. That layer owns auth, certificates, routing policy, retries, and - connection management; the remote-harness spec only sees the resulting - stdio/WebSocket/TCP/Unix/host adapter endpoint. -- If egress proxy enforcement is configured, startup fails unless enforcement is - actually wired by the host. - -### Frame envelope - -Every protocol frame is JSON-serializable and carries a stable stream context: - -```ts -type RemoteHarnessStreamContext = { - protocolVersion: 1; - streamId: string; - runId: string; - sessionKey: string; - workspaceId: string; - seq: number; -}; - -type RemoteHarnessFrame = RemoteHarnessStreamContext & { - kind: string; -}; -``` - -Frame invariants: - -- `RunStart` is the first accepted Gateway-to-host frame for a stream. -- `RunAccepted` must precede non-terminal host-to-Gateway event frames. -- `protocolVersion`, `streamId`, `runId`, `sessionKey`, and `workspaceId` are - immutable for the stream. +Add a bundled `remote-harness` AgentHarness plugin and a public remote harness +protocol reference. The plugin is selected through existing provider/model +runtime policy and connects the Gateway to a host-side runtime that executes an +allowlisted inner harness. + +The RFC body defines the interoperability contract and the normative protocol +schema tables. Supporting validation matrices and golden transcript sketches live +in the sidecar protocol reference: +[`0010/protocol-v1-reference.md`](0010/protocol-v1-reference.md). + +### Actors and responsibility boundary + +| Actor | Responsibility | +| --- | --- | +| OpenClaw Gateway | Selects the AgentHarness, owns session orchestration, applies policy decisions, persists lifecycle/transcript state, and emits user-visible events. | +| Gateway-side remote harness | Implements the local AgentHarness interface, opens the configured transport, validates protocol ordering, maps remote frames to OpenClaw callbacks/events, and maps terminal frames to `AgentHarnessAttemptResult`. | +| Transport | Carries ordered JSON-serializable frames. It is a route, not identity authority. | +| Host-side runtime | Validates `RunStart`, selects an allowlisted inner harness, runs it, streams observed events back to the Gateway, and asks the Gateway for OpenClaw-owned decisions. | +| Inner harness | Codex, Copilot, Pi, command-style reference harness, or another native harness running in the host runtime. | +| Container/platform layer | Optional deployment boundary that owns hostnames, DNS, sidecars, service mesh, certs, connector objects, routing policy, and connection management. | + +The upstream protocol standardizes the Gateway-to-host contract. It does not +standardize how a host deploys, scales, warms, authenticates, or connects its +runtime containers. + +### End-to-end remote harness workflow + +A conforming remote run follows this workflow: + +1. **Runtime selection.** Existing model/provider runtime policy selects + `remote-harness`. Selection is explicit; `auto` may keep existing local + behavior. +2. **Fail-closed readiness check.** The Gateway verifies the remote-harness + plugin, configuration, transport, and allowlisted inner harness are available. + If any required piece is missing, the attempt fails instead of falling back to + local model execution. +3. **Transport open.** The Gateway-side harness opens stdio, WebSocket, TCP, + Unix socket, or a host-adapter endpoint. Transport auth and connection + management remain outside the OpenClaw-visible config. +4. **Run start.** The Gateway sends `RunStart` as the first accepted frame. It + carries the stream identity, selected inner harness id, model/provider facts, + session/workspace context, tool policy hints, streaming mode, and limits. It + does not carry broad provider or caller credentials. +5. **Run accepted.** The host validates immutable identity, selects the inner + harness, and sends `RunAccepted` before non-terminal event frames. +6. **Event streaming.** The host streams assistant, reasoning, plan, tool, + command, patch, approval, item, compaction, usage, lifecycle, and diagnostic + events as they occur. The Gateway maps them to normal OpenClaw callbacks and + event streams. +7. **Gateway-owned decisions.** When the host needs approval, permission, + user-input, dynamic-tool, or memory-write decisions, it sends a request frame. + The Gateway applies normal OpenClaw policy/UI routing and returns an explicit + decision. If no safe route exists, the decision is denied or unavailable; the + host must not proceed implicitly. +8. **Cancellation and compaction.** Gateway cancellation, heartbeat, reset, or + compaction-control frames are correlated to the same immutable stream + identity. Post-terminal duplicates may be ignored only after identity + validation. +9. **Terminal result.** The host sends exactly one terminal frame: final, failed, + or cancelled. The Gateway records terminal lifecycle state and returns the + corresponding AgentHarness attempt result. +10. **Persistence and cleanup.** The Gateway persists transcripts/lifecycle + through the same local AgentHarness mechanisms. The host/container runtime + releases remote resources according to host policy. + +### Runtime selection and configuration requirements + +The bridge is registered as an AgentHarness and selected through existing +provider/model runtime configuration. The runtime id is `remote-harness`. + +Normative selection requirements: + +- model-scoped runtime policy wins over provider-scoped runtime policy; +- whole-session or whole-agent runtime pins remain out of scope; +- explicit `remote-harness` selection fails closed when the plugin is missing, + unsupported, disabled, unconfigured, or unable to reach a required host; +- local fallback is allowed only through existing `auto` selection behavior, not + after an explicit remote-harness selection. + +Configuration requirements: + +| Area | Requirement | +| --- | --- | +| fallback | Remote selection is fail-closed and must not silently use a local runtime. | +| inner harnesses | At least one inner harness id is allowlisted; the default inner harness must be allowlisted. | +| transports | v1 allows stdio, WebSocket, TCP, Unix socket, and host-adapter routing paths. | +| connector | `connector` is not a v1 OpenClaw transport kind. Container/platform networking can provide connector-like reachability outside the spec. | +| credentials | OpenClaw-visible config must not contain broad bearer, provider, model, caller, Graph/M365, GitHub, or session credentials. | +| egress | If a host configures egress proxy enforcement, startup fails unless that enforcement is actually wired by the host/platform. | +| unknown fields | Unknown top-level config fields are rejected to avoid silent policy drift. | + +### Protocol lifecycle and invariants + +Every frame belongs to one stream and carries immutable stream identity. The v1 +envelope schema is: + +| Field | Required | Type | Semantics | +| --- | --- | --- | --- | +| `protocolVersion` | yes | `1` | Protocol version for this frame. | +| `streamId` | yes | string | Opaque id for this remote stream. | +| `runId` | yes | string | OpenClaw run id for the attempt. | +| `sessionKey` | yes | string | OpenClaw session key for the attempt. | +| `workspaceId` | yes | string | Opaque workspace identity for this stream. | +| `seq` | yes | positive integer | Monotonic sequence number within one direction. | +| `kind` | yes | string | Exact frame kind token from the schema tables below, or an admitted extension frame kind. | + +A core frame is encoded as one JSON object: the envelope fields and that frame's +payload fields are siblings at the top level. `AgentEvent` is the exception for +stream-specific payloads: it carries the canonical or namespaced stream name in +`stream` and nests stream-specific fields under `data`. + +The RFC-level invariants are: + +- `RunStart` is the first accepted Gateway-to-host frame. +- `RunAccepted` precedes all non-terminal host-to-Gateway event frames. +- Protocol version, stream id, run id, session key, and workspace id are + immutable for a stream. - Sequence numbers are strictly increasing per direction. -- Unknown non-extension frame kinds fail closed. -- Unknown extension streams are rejected unless allowed by `eventStreams` policy. +- Request and response frames correlate by `requestId`; they do not correlate by + `seq`, because each direction has its own sequence counter. +- Unknown core frame kinds fail closed in v1. +- Unknown extension event streams are rejected unless allowlisted by policy. - Malformed frames close or reject the stream without crashing host loops. -- `RunFinal`, `RunFailed`, or `RunCancelled` ends the stream. -- Post-terminal duplicate/stale frames can be ignored only after validating the - immutable stream context. - -### Gateway-to-host frames - -Gateway-to-host frames carry run control and OpenClaw decisions: - -```ts -type BrainFrame = - | RunStartFrame - | CancelRunFrame - | HeartbeatFrame - | ApprovalDecisionFrame - | ToolAuthorizationDecisionFrame - | MemoryWriteDecisionFrame - | PermissionDecisionFrame - | UserInputResponseFrame - | DynamicToolResponseFrame - | ModelProxyChunkFrame - | ModelProxyFinalFrame - | ModelProxyErrorFrame - | CompactSessionFrame - | ResetSessionFrame; -``` - -#### `RunStart` - -`RunStart` carries host-authoritative context. The remote agent must not replace -these fields with request headers, URL paths, hostnames, or arbitrary transport -metadata. - -```ts -type RunStartFrame = RemoteHarnessStreamContext & { - kind: "RunStart"; - innerHarnessId: string; - input: JsonValue; - context: { - schemaVersion: 1; - agentId?: string; - sessionId: string; - sessionFile?: string; - sandboxSessionKey?: string; - workspaceDir?: string; - cwd?: string; - provider: string; - modelId: string; - requestedModelId?: string | null; - runtimePlan?: JsonValue; - trigger?: "cron" | "heartbeat" | "manual" | "memory" | "overflow" | "user"; - message?: { - channel?: string; - provider?: string; - to?: string; - threadId?: string | number; - currentMessageId?: string | number; - senderId?: string | null; - senderName?: string | null; - senderUsername?: string | null; - senderIsOwner?: boolean; - groupId?: string | null; - groupChannel?: string | null; - groupSpace?: string | null; - memberRoleIds?: string[]; - }; - toolPolicy?: { - toolsAllow?: string[]; - disableTools?: boolean; - requireExplicitMessageTarget?: boolean; - }; - streaming?: { - blockReplyBreak?: "text_end" | "message_end"; - suppressLiveStreamOutput?: boolean; - silentExpected?: boolean; - }; - limits?: { - timeoutMs: number; - runTimeoutOverrideMs?: number; - contextTokenBudget?: number; - }; - }; -}; -``` - -`RunStart.context` intentionally contains runtime facts and routing metadata, not -provider credentials. Sensitive auth remains Gateway-owned or harness-owned. - -#### Decisions and callbacks - -```ts -type ApprovalDecisionFrame = RemoteHarnessStreamContext & { - kind: "ApprovalDecision"; - requestId: string; - approved: boolean; - reason?: string; - scope?: "turn" | "session"; -}; - -type ToolAuthorizationDecisionFrame = RemoteHarnessStreamContext & { - kind: "ToolAuthorizationDecision"; - requestId: string; - approved: boolean; - reason?: string; -}; - -type MemoryWriteDecisionFrame = RemoteHarnessStreamContext & { - kind: "MemoryWriteDecision"; - requestId: string; - approved: boolean; - reason?: string; -}; - -type PermissionDecisionFrame = RemoteHarnessStreamContext & { - kind: "PermissionDecision"; - requestId: string; - permissions: JsonValue; - scope: "turn" | "session"; - strictAutoReview?: boolean; -}; - -type UserInputResponseFrame = RemoteHarnessStreamContext & { - kind: "UserInputResponse"; - requestId: string; - answers: Record; -}; - -type DynamicToolResponseFrame = RemoteHarnessStreamContext & { - kind: "DynamicToolResponse"; - requestId: string; - contentItems: JsonValue[]; - success: boolean; -}; -``` - -`PermissionDecision`, `UserInputResponse`, and `DynamicToolResponse` are required -because Codex exposes request-permissions, request-user-input, and dynamic tool -calls as first-class protocol events, while OpenClaw owns the user-visible -review and tool execution surfaces. - -#### Model proxy frames - -Model proxy frames are reserved for a future host-defined egress path. A v1 -implementation may reject `ModelProxyRequest` from the host and never emit these -responses. If implemented, the Gateway owns provider credentials and streams -model output back as: - -```ts -type ModelProxyChunkFrame = RemoteHarnessStreamContext & { - kind: "ModelProxyChunk"; - requestId: string; - delta: string; -}; - -type ModelProxyFinalFrame = RemoteHarnessStreamContext & { - kind: "ModelProxyFinal"; - requestId: string; - output: JsonValue; - usage?: JsonValue; -}; - -type ModelProxyErrorFrame = RemoteHarnessStreamContext & { - kind: "ModelProxyError"; - requestId: string; - error: { message: string; code?: string }; -}; -``` - -### Host-to-Gateway frames - -Host-to-Gateway frames carry lifecycle, event streams, requests, and terminal -outcomes: - -```ts -type AgentFrame = - | RunAcceptedFrame - | AssistantMessageStartFrame - | AssistantDeltaFrame - | VisibleReplyFrame - | ReasoningDeltaFrame - | ReasoningEndFrame - | ToolStartedFrame - | ToolProgressFrame - | ToolFinishedFrame - | ToolStreamBoundaryFrame - | AgentEventFrame - | AgentToolResultFrame - | ExecutionStartedFrame - | ExecutionPhaseFrame - | RunProgressFrame - | LaneWaitFrame - | SessionIdChangedFrame - | UsageUpdateFrame - | ApprovalRequestedFrame - | ToolAuthorizationRequestedFrame - | MemoryWriteRequestedFrame - | PermissionRequestedFrame - | UserInputRequestedFrame - | DynamicToolCallRequestedFrame - | ModelProxyRequestFrame - | CheckpointFrame - | ArtifactProducedFrame - | RunFinalFrame - | RunFailedFrame - | RunCancelledFrame - | HeartbeatFrame; -``` - -Common host-to-Gateway frame shapes: - -```ts -type RunAcceptedFrame = RemoteHarnessStreamContext & { - kind: "RunAccepted"; - selectedInnerHarnessId: string; - selectedInnerHarnessVersion: string; -}; - -type AssistantMessageStartFrame = RemoteHarnessStreamContext & { - kind: "AssistantMessageStart"; - itemId?: string; -}; - -type ExecutionStartedFrame = RemoteHarnessStreamContext & { - kind: "ExecutionStarted"; - lifecycleGeneration?: string; -}; - -type ExecutionPhaseFrame = RemoteHarnessStreamContext & { - kind: "ExecutionPhase"; - phase: - | "runner_entered" - | "workspace" - | "runtime_plugins" - | "before_agent_reply" - | "model_resolution" - | "auth" - | "context_engine" - | "attempt_dispatch" - | "context_assembled" - | "turn_accepted" - | "process_spawned" - | "tool_execution_started" - | "assistant_output_started" - | "model_call_started"; - provider?: string; - model?: string; - backend?: string; - source?: string; - tool?: string; - toolCallId?: string; - itemId?: string; - firstModelCallStarted?: boolean; -}; - -type RunProgressFrame = RemoteHarnessStreamContext & { - kind: "RunProgress"; - reason: string; - provider?: string; - model?: string; - backend?: string; -}; - -type LaneWaitFrame = RemoteHarnessStreamContext & { - kind: "LaneWait"; - waitMs: number; - queuedAhead: number; - waiting?: boolean; -}; - -type SessionIdChangedFrame = RemoteHarnessStreamContext & { - kind: "SessionIdChanged"; - sessionId: string; -}; - -type UsageUpdateFrame = RemoteHarnessStreamContext & { - kind: "UsageUpdate"; - usage: JsonValue; - source?: "assistant" | "model" | "harness"; -}; - -type AgentToolResultFrame = RemoteHarnessStreamContext & { - kind: "AgentToolResult"; - toolName: string; - result: JsonValue; - isError: boolean; -}; - -type ToolStreamBoundaryFrame = RemoteHarnessStreamContext & { - kind: "ToolStreamBoundary"; - toolCallId?: string; -}; - -type CheckpointFrame = RemoteHarnessStreamContext & { - kind: "Checkpoint"; - checkpointId: string; - state: JsonValue; -}; - -type ArtifactProducedFrame = RemoteHarnessStreamContext & { - kind: "ArtifactProduced"; - artifactId: string; - uri: string; - metadata?: JsonValue; -}; - -type ModelProxyRequestFrame = RemoteHarnessStreamContext & { - kind: "ModelProxyRequest"; - requestId: string; - model: string; - payload: JsonValue; -}; -``` - -Common control frames shared by both directions: - -```ts -type HeartbeatFrame = RemoteHarnessStreamContext & { - kind: "Heartbeat"; - sentAt?: string; -}; - -type CancelRunFrame = RemoteHarnessStreamContext & { - kind: "CancelRun"; - reason?: string; -}; - -type CompactSessionFrame = RemoteHarnessStreamContext & { - kind: "CompactSession"; - reason?: "manual" | "threshold" | "overflow"; -}; - -type ResetSessionFrame = RemoteHarnessStreamContext & { - kind: "ResetSession"; - reason?: "new" | "reset" | "idle" | "daily" | "compaction" | "deleted" | "unknown"; -}; -``` - -### Required OpenClaw event mapping - -The following mapping is the contract that remote harnesses must preserve when -they observe or produce the corresponding behavior. Implementations may add -harness-specific extension events, but they must not drop the canonical event -when one is available. - -| OpenClaw local surface | Remote frame or stream | Required behavior | +- Exactly one terminal frame ends the stream. +- Post-terminal stale or duplicate frames can be ignored only after immutable + identity is validated. + +### Frame families and required semantics + +The protocol has a small set of frame families. The schema tables in this RFC +are normative. The sidecar reference provides validation examples and golden +transcripts. + +| Direction | Frame family | Purpose | Required handling | +| --- | --- | --- | --- | +| Gateway → Host | Run control | start, cancel, heartbeat, reset, compaction-control | Host validates stream identity and applies only to the current run. | +| Gateway → Host | Decisions | approval, permission, user input, dynamic tool, memory write | Host correlates by request id and must fail closed on unavailable or denied decisions. | +| Gateway → Host | Optional model proxy response | model chunk/final/error, if a separate host-egress design enables it | v1 implementations may reject model-proxy requests unless explicitly supported. | +| Host → Gateway | Run acknowledgement | selected inner harness and version | Must occur before non-terminal events. | +| Host → Gateway | Observable events | assistant, reasoning, lifecycle, item, plan, tool, command output, patch, compaction, approval, usage, diagnostic streams | Gateway maps them to normal OpenClaw callbacks and event streams. | +| Host → Gateway | Decision requests | approval, permission, user input, dynamic tool, memory write | Gateway owns policy and returns explicit decision frames. | +| Host → Gateway | Artifacts and checkpoints | host-produced artifacts and recoverable state | Gateway may persist or surface according to existing policy. | +| Host → Gateway | Terminal outcome | final, failed, cancelled | Gateway records lifecycle, transcript, replay-safety, and attempt result. | + +### Event preservation contract + +Remote/local equivalence means that a user-visible or lifecycle-relevant event +produced by the selected inner harness must reach the same OpenClaw surface that +a local harness would have reached, unless the bridge explicitly rejects the +unsupported event before execution. + +The bridge must preserve these canonical event classes: + +| Event class | Required semantics | +| --- | --- | +| lifecycle | start, finishing, end, error, timeout, abort, user-message persistence, assistant-error persistence, provider-started, liveness, replay-invalid, and terminal metadata when known. | +| assistant | incremental assistant text, cumulative text snapshots, replacement snapshots for provisional/native items, final visible text, and usage when known. | +| thinking/reasoning | reasoning deltas or snapshots, section boundaries when known, media references when supported, and explicit reasoning-end flushes. | +| plan | plan updates, plan-mode request/completion, explanation, steps, actions, selected action, feedback, and approval state when known. | +| tool | tool start/update/result, sanitized args/results, tool call id, tool name, error state, metadata, and private tool-result diagnostics needed for replay safety. | +| item/activity | normalized activity-feed entries with id, phase, kind, title, status, progress, timestamps, summaries, and approval ids when available. | +| approval | command/plugin/permission/unknown approval requested/resolved state, pending/unavailable/approved/denied/failed state, and user-visible message. | +| command output | stdout/stderr deltas, terminal output, status, exit code, duration, and cwd when known. | +| patch | patch begin/update/end or summary, including added/modified/deleted files and summary text. | +| compaction | compaction start/end, completed/aborted state, retry state, backend/item/thread/turn ids when known. | +| diagnostics | non-terminal stream/provider/harness errors that should be visible to diagnostics without replacing terminal failure semantics. | +| extension streams | namespaced harness-specific streams, such as Codex app-server diagnostics, only when the namespace or stream is allowed by Gateway policy. | + +`AgentEvent` uses this canonical stream vocabulary: + +| Stream identifier | Event class | Closed phase/status vocabulary when present | | --- | --- | --- | -| `onExecutionStarted({ lifecycleGeneration })` | `ExecutionStarted` | Signals that the selected harness has entered execution and gives the Gateway the owning lifecycle generation when known. | -| `onExecutionPhase(...)` | `ExecutionPhase` and `AgentEvent(stream="item")` when user-visible | Carries milestones such as `runner_entered`, `workspace`, `runtime_plugins`, `before_agent_reply`, `model_resolution`, `auth`, `context_engine`, `attempt_dispatch`, `context_assembled`, `turn_accepted`, `process_spawned`, `tool_execution_started`, `assistant_output_started`, and `model_call_started`. | -| `onLaneWait(...)` | `LaneWait` | Preserves queued/waiting status before execution. | -| `onRunProgress(...)` | `RunProgress` | Preserves provider/model/backend progress messages. | -| `onSessionIdChanged(sessionId)` | `SessionIdChanged` | Lets the Gateway update active session identity after native resume/start decisions. | -| `onAssistantMessageStart()` | `AssistantMessageStart` | Marks the first assistant item before deltas. | -| `onPartialReply(payload)` | `AssistantDelta` | Streams assistant text with `delta`, cumulative `text`, optional `replace`, and optional usage. | -| `onBlockReply(payload)` / `onBlockReplyFlush()` | `VisibleReply` | Carries text/media/source-reply payloads that should be delivered as block replies rather than only append deltas. | -| `onReasoningStream(payload)` | `ReasoningDelta` | Carries reasoning text snapshots or deltas, media URLs, and `isReasoningSnapshot`. | -| `onReasoningEnd()` | `ReasoningEnd` | Closes a reasoning block so channels can flush thinking UI. | -| `onToolResult(payload)` | `ToolProgress` or `VisibleReply` | Carries user-visible tool result text/media/status that channels should render. | -| `onAgentToolResult({ toolName, result, isError })` | `AgentToolResult` | Carries sanitized private per-tool result for replay-safety and diagnostics. | -| `onToolStreamBoundary()` | `ToolStreamBoundary` | Preserves spacing/order after a tool finishes streaming. | -| `onAgentEvent({ stream, data, sessionKey })` | `AgentEvent` | Generic sequenced OpenClaw event bus frame for canonical and extension streams. | -| `onUserMessagePersisted(message)` | `AgentEvent(stream="lifecycle", data.phase="user_message_persisted")` | Lets remote/native harnesses report the point at which the user message is durably in transcript. | -| `onAssistantErrorMessagePersisted(message)` | `AgentEvent(stream="lifecycle", data.phase="assistant_error_persisted")` | Lets the Gateway distinguish persisted error text from transient stream failures. | -| `onAttemptTimeoutArmed()` | `AgentEvent(stream="lifecycle", data.phase="timeout_armed")` | Preserves watchdog observability. | -| `onAttemptTimeout(error)` | `AgentEvent(stream="lifecycle", data.phase="timeout")` and terminal metadata | Preserves timeout phase and liveness data. | -| `onAttemptAbort()` | `RunCancelled` or `AgentEvent(stream="lifecycle", data.phase="aborted")` | Preserves explicit native abort acknowledgement. | - -### Canonical `AgentEvent` streams - -`AgentEvent` is the escape hatch that keeps the remote protocol aligned with -OpenClaw's local event bus without adding a new top-level frame for every UI -or diagnostic stream: - -```ts -type AgentEventFrame = RemoteHarnessStreamContext & { - kind: "AgentEvent"; - stream: - | "lifecycle" - | "tool" - | "assistant" - | "error" - | "item" - | "plan" - | "approval" - | "command_output" - | "patch" - | "compaction" - | "thinking" - | `${string}.${string}`; - data: Record; - sessionKey?: string; - agentId?: string; -}; -``` - -Canonical stream requirements: - -- `lifecycle`: emits `phase: "start"`, `"finishing"`, `"end"`, `"error"`, - timeout, abort, and persistence-related phases. Terminal lifecycle events carry - `stopReason`, `yielded`, `timeoutPhase`, `providerStarted`, `aborted`, - `livenessState`, and `replayInvalid` when known. -- `assistant`: emits assistant snapshots/deltas, including replacement snapshots - for provisional/native items that append-only partial-reply consumers cannot - safely concatenate. -- `thinking`: emits reasoning snapshots/deltas for live thinking UI. -- `tool`: emits tool `start`, `update`, and `result` with sanitized args/results, - tool name, tool call id, error state, and metadata. -- `item`: emits normalized activity-feed items with `itemId`, `phase`, `kind`, - `title`, `status`, timestamps, progress text, approval ids, and summaries. -- `plan`: emits plan updates, exit-plan-mode requests/completions, explanation, - steps, actions, selected action, and feedback. -- `approval`: emits approval `requested` and `resolved` events for command, - plugin, permission, and unknown approval families. -- `command_output`: emits command stdout/stderr deltas and terminal command - output with status, exit code, duration, and cwd when known. -- `patch`: emits patch summary with added, modified, deleted files and summary - text. -- `compaction`: emits compaction `start` and `end`, completed/aborted state, - retry state, backend, item/thread/turn ids when known. -- `error`: emits non-terminal stream/provider/harness errors that should be - visible to diagnostics but do not replace terminal `RunFailed`. -- Namespaced extension streams such as `codex_app_server.item`, - `codex_app_server.guardian`, `codex_app_server.hook`, and - `codex_app_server.lifecycle` are allowed only when the Gateway configuration - admits the namespace or the stream is known to the plugin. - -### Typed event frames - -Typed frames exist for high-value event classes that every host should implement -without inspecting generic stream payloads: - -```ts -type AssistantDeltaFrame = RemoteHarnessStreamContext & { - kind: "AssistantDelta"; - delta: string; - text?: string; - replace?: boolean; - replaceable?: boolean; - usage?: JsonValue; -}; - -type VisibleReplyFrame = RemoteHarnessStreamContext & { - kind: "VisibleReply"; - text?: string; - mediaUrls?: string[]; - audioAsVoice?: boolean; - trustedLocalMedia?: boolean; - sourceReply?: JsonValue; - final?: boolean; -}; - -type ReasoningDeltaFrame = RemoteHarnessStreamContext & { - kind: "ReasoningDelta"; - text?: string; - delta?: string; - mediaUrls?: string[]; - isReasoningSnapshot?: boolean; -}; - -type ReasoningEndFrame = RemoteHarnessStreamContext & { - kind: "ReasoningEnd"; -}; - -type ToolStartedFrame = RemoteHarnessStreamContext & { - kind: "ToolStarted"; - toolCallId: string; - name: string; - args?: JsonValue; - meta?: string; - startedAt?: number; -}; - -type ToolProgressFrame = RemoteHarnessStreamContext & { - kind: "ToolProgress"; - toolCallId: string; - name?: string; - message?: string; - partialResult?: JsonValue; - progressText?: string; -}; - -type ToolFinishedFrame = RemoteHarnessStreamContext & { - kind: "ToolFinished"; - toolCallId: string; - name: string; - result: JsonValue; - isError: boolean; - meta?: string; - endedAt?: number; -}; -``` - -Typed frames and `AgentEvent` are complementary. A host that emits -`ToolStarted` should also emit the matching canonical `AgentEvent(stream="tool")` -and `AgentEvent(stream="item")` when it has enough data to preserve OpenClaw's -activity feed. The Gateway deduplicates by `toolCallId`/`itemId`. - -### Requests initiated by the remote host - -Remote native harnesses can need OpenClaw-owned decisions or actions. These are -host-to-Gateway request frames: - -```ts -type ApprovalRequestedFrame = RemoteHarnessStreamContext & { - kind: "ApprovalRequested"; - requestId: string; - prompt: string; - details?: JsonValue; -}; - -type ToolAuthorizationRequestedFrame = RemoteHarnessStreamContext & { - kind: "ToolAuthorizationRequested"; - requestId: string; - toolName: string; - details?: JsonValue; -}; - -type MemoryWriteRequestedFrame = RemoteHarnessStreamContext & { - kind: "MemoryWriteRequested"; - requestId: string; - proposedMemory: JsonValue; -}; - -type PermissionRequestedFrame = RemoteHarnessStreamContext & { - kind: "PermissionRequested"; - requestId: string; - reason?: string; - permissions: JsonValue; - cwd?: string; - environmentId?: string; -}; - -type UserInputRequestedFrame = RemoteHarnessStreamContext & { - kind: "UserInputRequested"; - requestId: string; - questions: Array<{ - id: string; - header: string; - question: string; - isOther?: boolean; - isSecret?: boolean; - options?: Array<{ label: string; description: string }>; - }>; - autoResolutionMs?: number; -}; - -type DynamicToolCallRequestedFrame = RemoteHarnessStreamContext & { - kind: "DynamicToolCallRequested"; - requestId: string; - namespace?: string; - tool: string; - arguments: JsonValue; -}; -``` - -The Gateway must apply the same approval, user-input, dynamic-tool, memory, and -permission policies it would apply for an in-process harness. If a policy or UI -route is unavailable, the Gateway returns a denial/unavailable decision rather -than letting the remote host proceed implicitly. - -### Terminal frames and result mapping - -```ts -type RunFinalFrame = RemoteHarnessStreamContext & { - kind: "RunFinal"; - result: JsonValue; - assistantTexts?: string[]; - usage?: JsonValue; - replayMetadata?: JsonValue; - itemLifecycle?: { - startedCount: number; - completedCount: number; - activeCount: number; - }; -}; - -type RunFailedFrame = RemoteHarnessStreamContext & { - kind: "RunFailed"; - error: { message: string; code?: string; data?: JsonValue }; - replaySafe?: boolean; -}; - -type RunCancelledFrame = RemoteHarnessStreamContext & { - kind: "RunCancelled"; - reason?: string; - timedOut?: boolean; - timeoutPhase?: string; -}; -``` - -The Gateway maps terminal frames into `AgentHarnessAttemptResult`. A remote -harness must send the final assistant text, usage, replay-safety, and item -lifecycle metadata it knows so the outer OpenClaw attempt can make the same -fallback, retry, transcript, and lifecycle decisions as a local native harness. - -### Existing event-source mapping - -The v1 bridge must be able to represent these current sources: - -| Source | Events observed today | Remote mapping | +| `lifecycle` | lifecycle | `phase`: `start`, `finishing`, `end`, `error`, `timeout_armed`, `timeout`, `aborted`, `user_message_persisted`, `assistant_error_persisted` | +| `assistant` | assistant | no required phase; payload uses text/delta/replacement fields when present | +| `thinking` | thinking/reasoning | no required phase; payload uses text/delta/snapshot fields when present | +| `tool` | tool | `phase`: `start`, `update`, `result` | +| `item` | item/activity | `phase`: `start`, `update`, `end`; `status`: `running`, `completed`, `failed`, `blocked` | +| `plan` | plan | `phase`: `update` | +| `approval` | approval | `phase`: `requested`, `resolved`; `status`: `pending`, `unavailable`, `approved`, `denied`, `failed`; `kind`: `exec`, `plugin`, `permission`, `unknown` | +| `command_output` | command output | `phase`: `delta`, `end` | +| `patch` | patch | `phase`: `end` | +| `compaction` | compaction | `phase`: `start`, `end`; optional `completed` and `willRetry` booleans | +| `error` | diagnostics | no required phase; payload includes sanitized error data | +| namespaced streams | extension streams | governed by the admitted namespace or stream contract | + +A bridge may use both typed common frames and a generic event frame. Typed frames +make common behavior easy to implement correctly. The generic event frame +preserves existing OpenClaw streams and harness-specific extension streams. + +### Protocol schema summary + +All frames include the envelope fields above. The tables below list each core +frame's payload schema. Receivers reject missing required fields and reject +unknown core frame kinds in v1. + +#### Gateway-to-host frame schemas + +| Frame kind | Required payload fields | Optional payload fields | Semantics | +| --- | --- | --- | --- | +| `RunStart` | `innerHarnessId`, `input`, `context` | none | Starts the stream and supplies selected harness, prompt/input, and host-authoritative run context. | +| `CancelRun` | none | `reason` | Requests cancellation for the current stream. | +| `Heartbeat` | none | `sentAt` | Liveness signal. | +| `ResetSession` | none | `reason` | Requests host-side session reset for the current stream. | +| `CompactSession` | none | `reason` | Requests host-side compaction for the current stream. | +| `ApprovalDecision` | `requestId`, `approved` | `reason`, `scope` | Resolves a host approval request. | +| `ToolAuthorizationDecision` | `requestId`, `approved` | `reason` | Resolves a host tool-authorization request. | +| `MemoryWriteDecision` | `requestId`, `approved` | `reason` | Resolves a host memory-write request. | +| `PermissionDecision` | `requestId`, `permissions`, `scope` | `strictAutoReview` | Resolves a host permission request. | +| `UserInputResponse` | `requestId`, `answers` | none | Resolves a host user-input request. | +| `DynamicToolResponse` | `requestId`, `contentItems`, `success` | none | Returns a Gateway-executed dynamic-tool result. | +| `ModelProxyChunk` | `requestId`, `delta` | none | Reserved model-proxy response chunk when model proxying is enabled. | +| `ModelProxyFinal` | `requestId`, `output` | `usage` | Reserved model-proxy final response when model proxying is enabled. | +| `ModelProxyError` | `requestId`, `error` | none | Reserved model-proxy error response when model proxying is enabled. | + +`RunStart.context` is a structured object with these v1 fields: + +| Field | Required | Type | Semantics | +| --- | --- | --- | --- | +| `schemaVersion` | yes | `1` | Context schema version. | +| `sessionId` | yes | string | OpenClaw runtime session id. | +| `provider` | yes | string | Resolved provider id. | +| `modelId` | yes | string | Resolved model id. | +| `agentId` | no | string | Agent id when known. | +| `sessionFile` | no | string | Legacy/session artifact hint when still available. | +| `sandboxSessionKey` | no | string | Sandbox-specific session key when different from the visible session key. | +| `workspaceDir` | no | string | Host-visible workspace path when the deployment exposes one. | +| `cwd` | no | string | Working directory for command-style harnesses. | +| `requestedModelId` | no | string or null | Model originally requested before resolution. | +| `runtimePlan` | no | JSON value | Host-curated runtime plan/debug metadata. | +| `trigger` | no | enum | Run trigger such as user, cron, heartbeat, memory, manual, overflow. | +| `message` | no | object | Messaging/channel metadata such as channel, provider, recipient, thread, sender, group, and role ids. | +| `toolPolicy` | no | object | Tool allow/disable/targeting hints. | +| `streaming` | no | object | Block-reply and live-streaming hints. | +| `limits` | no | object | Timeout and context-token-budget hints. | + +#### Host-to-Gateway frame schemas + +| Frame kind | Required payload fields | Optional payload fields | Semantics | +| --- | --- | --- | --- | +| `RunAccepted` | `selectedInnerHarnessId`, `selectedInnerHarnessVersion` | none | Acknowledges selected inner harness before non-terminal events. | +| `AssistantMessageStart` | none | `itemId` | Marks the beginning of an assistant item. | +| `AssistantDelta` | `delta` | `text`, `replace`, `replaceable`, `usage` | Streams assistant text or replacement snapshots. | +| `VisibleReply` | none | `text`, `mediaUrls`, `audioAsVoice`, `trustedLocalMedia`, `sourceReply`, `final` | Delivers block-reply payloads. | +| `ReasoningDelta` | none | `text`, `delta`, `mediaUrls`, `isReasoningSnapshot` | Streams reasoning or thinking snapshots/deltas. | +| `ReasoningEnd` | none | none | Closes a reasoning block. | +| `ToolStarted` | `toolCallId`, `name` | `args`, `meta`, `startedAt` | Starts a tool item. | +| `ToolProgress` | `toolCallId` | `name`, `message`, `partialResult`, `progressText` | Updates tool progress. | +| `ToolFinished` | `toolCallId`, `name`, `result`, `isError` | `meta`, `endedAt` | Completes a tool item. | +| `ToolStreamBoundary` | none | `toolCallId` | Marks a stream boundary after tool output. | +| `AgentToolResult` | `toolName`, `result`, `isError` | none | Reports private/sanitized tool result diagnostics. | +| `AgentEvent` | `stream`, `data` | `sessionKey`, `agentId` | Carries canonical or allowlisted namespaced OpenClaw event streams. | +| `ExecutionStarted` | none | `lifecycleGeneration` | Reports native execution start. | +| `ExecutionPhase` | `phase` | `provider`, `model`, `backend`, `source`, `tool`, `toolCallId`, `itemId`, `firstModelCallStarted` | Reports execution milestones. | +| `RunProgress` | `reason` | `provider`, `model`, `backend` | Reports provider/model/backend progress. | +| `LaneWait` | `waitMs`, `queuedAhead` | `waiting` | Reports queue wait state. | +| `SessionIdChanged` | `sessionId` | none | Reports native session id changes. | +| `UsageUpdate` | `usage` | `source` | Reports provider/model/harness usage. | +| `ApprovalRequested` | `requestId`, `prompt` | `details` | Requests Gateway approval. | +| `ToolAuthorizationRequested` | `requestId`, `toolName` | `details` | Requests Gateway tool authorization. | +| `MemoryWriteRequested` | `requestId`, `proposedMemory` | none | Requests Gateway memory-write decision. | +| `PermissionRequested` | `requestId`, `permissions` | `reason`, `cwd`, `environmentId` | Requests Gateway permission decision. | +| `UserInputRequested` | `requestId`, `questions` | `autoResolutionMs` | Requests Gateway-mediated user input. | +| `DynamicToolCallRequested` | `requestId`, `tool`, `arguments` | `namespace` | Requests Gateway dynamic-tool execution. | +| `ModelProxyRequest` | `requestId`, `model`, `payload` | none | Reserved model-proxy request when model proxying is enabled. | +| `Checkpoint` | `checkpointId`, `state` | none | Reports recoverable host state. | +| `ArtifactProduced` | `artifactId`, `uri` | `metadata` | Reports host-produced artifact. | +| `RunFinal` | `result` | `assistantTexts`, `usage`, `replayMetadata`, `itemLifecycle` | Completes the run successfully. | +| `RunFailed` | `error` | `replaySafe` | Completes the run with failure. | +| `RunCancelled` | none | `reason`, `timedOut`, `timeoutPhase` | Completes the run as cancelled or timed out. | + +#### Shared payload object schemas + +| Object | Shape and rules | +| --- | --- | +| `requestId` | Non-empty string generated by the requester and unique among in-flight requests within the stream. Decision/response frames must echo it exactly. | +| `scope` | Closed enum: `turn` or `session`. | +| `input` | JSON-serializable value supplied by the Gateway to the selected inner harness. It is opaque to the transport protocol and is not identity authority. Implementations must not put broad provider/caller credentials in it. | +| `usage` | JSON object with optional non-negative integer fields `inputTokens`, `outputTokens`, `totalTokens`, `reasoningTokens`, `cacheReadTokens`, and `cacheWriteTokens`, plus optional `provider` and `model` strings. Unknown provider-specific usage fields may appear only under a namespaced object key. | +| `error` | JSON object with required `code` and `message`; optional `details` is a JSON object. `message` is sanitized user/diagnostic text and must not contain raw credentials, token prefixes, stack traces, or unbounded provider payloads. | +| `result` | JSON object. Interoperable terminal data belongs in sibling fields such as `assistantTexts`, `usage`, `replayMetadata`, and `itemLifecycle`; `result` may carry inner-harness-specific output or `{ "status": "completed" }` when no extra output is needed. | +| `questions` | Array of user-input questions. Each question has required `id`, `header`, and `question`, plus optional `isOther`, `isSecret`, and option labels/descriptions. | +| `permissions` | JSON object interpreted by Gateway permission policy. Empty object means no additional permission is granted. | + +`error.code` uses this v1 closed set unless a namespaced extension code is +explicitly admitted by policy: + +| Code | Meaning | +| --- | --- | +| `protocol_violation` | The remote protocol was malformed or ordered incorrectly. | +| `remote_unavailable` | The host runtime or transport was unavailable. | +| `inner_harness_unavailable` | The selected inner harness was unavailable or not allowlisted. | +| `approval_denied` | Gateway approval policy denied or could not route the approval request. | +| `permission_denied` | Gateway permission policy denied or could not route the permission request. | +| `user_input_unavailable` | Required user input could not be collected safely. | +| `dynamic_tool_failed` | Gateway dynamic-tool execution failed or was denied. | +| `cancelled` | The run was cancelled. | +| `timeout` | The run timed out. | +| `provider_error` | The selected inner harness reported a sanitized provider/runtime failure. | +| `unknown` | A sanitized failure that does not fit another v1 code. | + +`AgentEvent.stream` admits the canonical streams listed in the event preservation +contract plus allowlisted namespaced extension streams. `AgentEvent.data` is a +JSON object whose fields are interpreted according to the named stream. When a +host emits both a typed frame and a matching `AgentEvent`, the Gateway correlates +by stable ids such as `toolCallId`, `itemId`, `requestId`, and the frame +envelope. + +### Host-initiated requests and Gateway-owned decisions + +Remote native harnesses sometimes need decisions or actions that OpenClaw owns. +They must request them from the Gateway instead of acting independently. + +| Host request | Gateway-owned decision | Required fail-closed behavior | | --- | --- | --- | -| OpenClaw embedded runner | execution started/phase, lane wait, run progress, session id change, assistant partials, block replies, reasoning, tool results, private tool results, generic agent events, tool stream boundaries, user/error persistence, timeout/abort hooks | typed frames plus `AgentEvent` canonical streams | -| OpenClaw agent event bus | `lifecycle`, `tool`, `assistant`, `error`, `item`, `plan`, `approval`, `command_output`, `patch`, `compaction`, `thinking`, extension streams | `AgentEvent` | -| Codex app-server plugin | assistant deltas with replaceable snapshots, reasoning snapshots, plan updates, item started/completed, tool progress, compaction start/end, approval events, permission requests, user-input requests, dynamic tool calls, MCP/tool events, guardian/hook diagnostics, native lifecycle streams | typed frames for common events, `PermissionRequested`, `UserInputRequested`, `DynamicToolCallRequested`, and namespaced `AgentEvent` for Codex-specific diagnostics | -| Upstream Codex protocol | `TurnStarted`, `TurnComplete`, `AgentMessageContentDelta`, `PlanDelta`, `ReasoningContentDelta`, `ItemStarted`, `ItemCompleted`, `ExecCommandBegin`, `ExecCommandOutputDelta`, `ExecCommandEnd`, `PatchApplyBegin`, `PatchApplyUpdated`, `PatchApplyEnd`, `RequestPermissions`, `RequestUserInput`, `DynamicToolCallRequest`, `McpToolCallBegin`, `McpToolCallEnd`, `SubAgentActivity`, hook events | typed frames and canonical/namespaced `AgentEvent` streams | -| Copilot SDK bridge | assistant message deltas, assistant usage, tool start/complete counts, plan changed, exit-plan-mode request/completion, subagent started/completed/failed, compaction start/complete, session idle/error/abort | typed assistant/usage frames, `AgentEvent(stream="plan")`, `AgentEvent(stream="item")`, `AgentEvent(stream="compaction")`, terminal failure/cancel frames | -| Portable Lobster remote-harness plugin | `AssistantDelta`, `ReasoningDelta`, tool started/progress/finished, checkpoint, artifact, approval/tool-auth/memory requests, terminal frames, heartbeat | retained as the minimal subset, extended by `AgentEvent`, visible reply, plan, command output, patch, compaction, user-input, permission, dynamic tool, usage, and lifecycle frames | - -### Host-side inner harness API - -The host-side API remains small but must expose event callbacks instead of only -assistant/tool callbacks: - -```ts -type InnerRunRequest = { - runId: string; - sessionKey: string; - workspaceId: string; - streamId: string; - input: JsonValue; - context: RunStartFrame["context"]; -}; - -type InnerHarnessCallbacks = { - assistantDelta(frame: Omit): Promise; - visibleReply(frame: Omit): Promise; - reasoningDelta(frame: Omit): Promise; - reasoningEnd(): Promise; - toolStarted(frame: Omit): Promise; - toolProgress(frame: Omit): Promise; - toolFinished(frame: Omit): Promise; - agentEvent(frame: Omit): Promise; - requestApproval(frame: Omit): Promise; - requestPermission(frame: Omit): Promise; - requestUserInput(frame: Omit): Promise; - callDynamicTool(frame: Omit): Promise; -}; - -type InnerHarness = { - id: string; - version: string; - run( - request: InnerRunRequest, - callbacks: InnerHarnessCallbacks, - signal: AbortSignal, - ): Promise; -}; -``` - -A generic command inner harness can exist for local and product integration -experiments. Command execution must use an allowlist-style environment: - -- inherit only ambient process basics such as `PATH`, `HOME`, `LANG`, `TZ`, and - trusted CA bundle variables; -- pass explicit host-provided environment entries only after filtering sensitive - credential-shaped names; -- abort the child process on cancellation; -- stream stdout as assistant text only after the process succeeds unless the - command explicitly speaks the remote-harness event protocol. - -`deterministic-poc` may exist for repeatable tests, but it is test-only and must -not be a production fallback. - -### Transport authority and trusted identity +| approval | command/plugin approval policy, UI routing, reviewer state | If no approval route exists, return denied/unavailable. | +| permission | filesystem/network/environment permission policy and grant scope | If the policy cannot be evaluated, deny or return unavailable. | +| user input | prompt presentation, secret handling, auto-resolution, response persistence | If the user route is unavailable, return explicit empty/denied/unavailable result according to policy. | +| dynamic tool | tool lookup, argument validation, execution, result redaction | Unknown or denied tools fail closed; host must not synthesize success. | +| memory write | memory policy, storage target, review state | Denied/unavailable means no memory write. | +| optional model proxy | provider/model credential use and response streaming | Out of core v1 unless a host enables a separate model-egress contract. | + +This preserves Codex-style request-permissions, request-user-input, and dynamic +tool calls without putting OpenClaw policy into the remote container. + +### Terminal result, replay safety, and persistence + +The terminal frame maps to the local `AgentHarnessAttemptResult` and lifecycle +persistence. A conforming host must provide the terminal information it knows: + +- final assistant text or structured result; +- provider/model usage snapshots; +- replay-safety metadata and whether side effects may have occurred; +- item lifecycle counts or active-item state when available; +- stop reason, timeout phase, cancellation reason, yielded state, and provider + started state when known; +- sanitized failure code/message for failed runs. + +The Gateway, not the host, owns final persistence semantics. It uses the normal +AgentHarness result, assistant callbacks, user/error message persistence +callbacks, and `AgentEvent` streams to update transcripts and lifecycle state. +v1 does not add a remote-only transcript side channel. + +### Transport authority, identity, and credentials The transport is a routing mechanism, not authority. -The upstream plugin must not require bearer tokens in OpenClaw-visible -configuration. If a host needs authenticated routing, it should put authority -outside the plugin config, such as: +The remote agent must not treat hostnames, URL paths, headers, or arbitrary +request metadata as proof of user, session, workspace, or run identity. +`RunStart` stream context is authoritative. + +The upstream plugin must not require bearer tokens or broad credentials in +OpenClaw-visible configuration. If a host needs authenticated routing, it can +provide authority outside this spec through: - process-local sockets; - reverse-proxy admission; @@ -951,134 +458,158 @@ outside the plugin config, such as: - host-managed connector objects, if a deployment provides them outside this spec; - short-lived substream metadata produced by the host runtime; -- container-runtime capability state that the transport adapter consults before - accepting a bridge connection. +- container-runtime capability state consulted by a host adapter. The remote-harness protocol does not standardize connector auth, connection pooling, service discovery, retries, certificate handling, or credential -projection. Deployments such as a containerized host runtime can solve those -concerns in the container or host platform and expose only a reachable endpoint -to the bridge. - -The remote agent must not treat hostnames, URL paths, headers, or arbitrary -request metadata as proof of user, session, workspace, or run identity. -`RunStart.context` is the authoritative stream identity. +projection. Deployments such as Lobster can solve those concerns in the +container or host platform and expose only a reachable endpoint to the bridge. ### Container-runtime contribution boundary This RFC deliberately separates upstreamable bridge contracts from host-specific container orchestration: -| Surface | Upstream contract | Host-specific implementation | +| Surface | Upstream contract | Host/container responsibility | | --- | --- | --- | -| Remote AgentHarness plugin | yes | no | -| Protocol frame types and validators | yes | no | -| Host runtime and inner-harness registry | yes | no | -| Reference command harness/container | yes | optional | -| Event conformance suite | yes | no | -| Container allocator interface | yes, as a future RFC or implementation issue | provider-specific allocator | -| Worker pool, deployment, EV2, Azure, Plex, or ECS wiring | no | yes | +| Remote AgentHarness plugin and lifecycle | yes | no | +| v1 protocol frame families, invariants, and validation | yes | no | +| Host runtime and inner-harness registry semantics | yes | host chooses implementation | +| Reference command harness/container | optional upstream reference | host may replace | +| Event conformance suite | yes | host must pass | +| Container allocator interface | future RFC or implementation issue | provider-specific allocator | +| Worker pool, deployment, EV2/Azure/Plex/ECS wiring | no | yes | | Container DNS, hostnames, sidecars, service mesh, certificates, and connection management | no | yes | | Rollout audience authoring | no | yes | -| Runtime admission invariants | yes | host chooses storage/evaluation mechanism | +| Runtime admission storage/evaluation | invariant is upstream | mechanism is host-specific | Any container runtime that claims remote-harness support must prove: - explicit remote-harness requests fail closed when disabled, disallowed, unavailable, or unready; -- headerless rollout misses can remain on existing local Gateway behavior; - request headers cannot set session, run, workspace, or user identity; - the adapter accepts remote bridge traffic only while trusted runtime state says the current Gateway is in remote-harness mode; -- raw provider/model/caller credentials are not projected into the Agent +- raw provider/model/caller credentials are not projected into the remote agent container by default; -- all required event streams from this RFC are forwarded or intentionally denied - with a visible conformance failure. +- required event classes are forwarded or the attempt fails with a visible + conformance error before user-visible work is lost. + +### Context-engine runtime settings + +Remote harness runs participate in context-engine behavior through the normal +OpenClaw attempt lifecycle. The bridge preserves context-engine execution phase +events and carries selected runtime facts in `RunStart`, but v1 does not define +a new remote-only context-engine runtime-settings payload or let the remote host +mutate context-engine selection, routing, fallback, or auth policy. -### Compatibility and migration +### Compatibility, versioning, and migration -This is additive: +This proposal is additive: -- existing OpenClaw runtimes continue to work unchanged; +- existing local AgentHarness runtimes continue to work unchanged; - existing plugins are not required to implement remote harness; -- `remote-harness` is selected only by explicit provider/model runtime - configuration; +- `remote-harness` is selected only by explicit provider/model runtime policy; - unsupported transports or unavailable remote hosts fail the selected attempt; -- v1 frame validation rejects unknown core frame kinds; -- `AgentEvent` provides a controlled extension path for existing harness-specific - streams without requiring a protocol revision for every diagnostic event. +- v1 rejects unknown core frame kinds; +- v1 allows controlled extension streams through explicit namespace or stream + allowlisting; +- incompatible frame changes require a protocol-version bump; +- additive fields are allowed only when old receivers can ignore them without + changing policy or user-visible semantics. + +The initial implementation may include a compatibility shim for current SDK +surfaces. That shim should stay at one package boundary and should not become a +second protocol. + +### Conformance requirements + +A third-party host claiming v1 support should pass a conformance suite that +covers at least: + +1. **Schema validation.** Required envelope fields, immutable identity, + monotonic sequences, terminal uniqueness, and fail-closed unknown core frames. +2. **Golden lifecycle transcripts.** Successful run, tool run, approval denial, + permission request, user-input request, dynamic-tool call, compaction, patch, + command output, cancellation, timeout, malformed frame, and unsupported event. +3. **Event preservation.** The Gateway receives the canonical event classes + listed in this RFC for the corresponding host-side behavior. +4. **Security/authority.** Request headers and route metadata cannot override + `RunStart` identity; broad credentials are not present in OpenClaw-visible + config or default remote-agent container environment. +5. **Executable fake host.** A minimal fake host drives the Gateway-side harness + end-to-end through the above transcripts. Schema-only validation is not + sufficient for an accepted implementation. -The initial implementation may include a compatibility shim for current -AgentHarness SDK types. That shim should stay in one package boundary and be -removed once the public SDK exposes native remote-harness frame and event types. +## Rationale -### Transcripts and context-engine runtime settings +### Why use the AgentHarness seam -Remote harness runs participate in transcript and lifecycle persistence through -the same AgentHarness result, assistant delivery callbacks, user/error message -persistence callbacks, and `AgentEvent` streams that local harnesses use. v1 does -not add a remote-only transcript side channel. +The selected design keeps OpenClaw's runtime model simple: provider/model policy +selects an AgentHarness, and the harness owns execution details. A remote +harness is operationally different, but it still produces the same attempt +events and terminal result as a local native harness. Adding a special core +execution path would duplicate selection, lifecycle, approval, transcript, and +retry semantics that already exist around AgentHarness attempts. -Remote harness runs also participate in context-engine behavior through the -normal OpenClaw attempt lifecycle. The bridge preserves context-engine execution -phase events and carries selected runtime facts in `RunStart.context`, but v1 -does not define a new remote-only context-engine runtime-settings payload or let -the remote host mutate OpenClaw context-engine selection, routing, fallback, or -auth policy. +### Why typed common frames plus generic event streams -## Rationale +Common behavior such as assistant deltas, tool lifecycle, decisions, and +terminal results needs strict semantics so independent hosts interoperate. At the +same time, OpenClaw already has stable event stream names and harness-specific +extension streams. A generic event frame preserves those surfaces without +freezing every Codex, Copilot, or future harness diagnostic into the core +protocol. + +The protocol therefore uses typed frame families for common contract surfaces and +a controlled generic event stream for canonical and namespaced extension events. +The sidecar reference carries exact field shapes; the RFC body carries the +reviewable interoperability decision. + +### Why fail closed + +If a user or host explicitly selects remote execution, silent local fallback can +violate deployment policy, egress policy, reproducibility expectations, canary +controls, or container-isolation guarantees. `auto` selection may keep existing +conservative behavior, but explicit remote selection must either run remotely or +fail visibly. + +### Why keep connector behavior outside v1 + +Some deployments can already solve connector-like routing inside a container or +host platform by giving the host-side runtime DNS names, hostnames, sidecars, +reverse proxies, service mesh routes, or host-managed connector objects. The +OpenClaw bridge does not need to standardize auth or connection management to be +useful. Keeping connector out of v1 prevents the RFC from depending on a +connector SDK that does not exist while still allowing containerized hosts to +connect to reachable services. + +### Why avoid credentials in OpenClaw-visible config -The selected design uses the AgentHarness plugin seam instead of adding a new -special-purpose execution path to OpenClaw core. That keeps the OpenClaw model -simple: provider/model runtime selection chooses a harness, and the harness owns -execution details. A remote harness is operationally different, but it still -produces the same attempt events and terminal result as a local native harness. - -A typed frame protocol is preferred over forwarding opaque process stdio because -OpenClaw relies on structured events. Tool progress, activity-feed items, -command output, patch summaries, approvals, plans, compaction, reasoning, -artifacts, lifecycle, cancellation, and final results should not be parsed out -of free-form text. - -A generic `AgentEvent` frame is included because OpenClaw already has an event -bus with stable stream names and harness-specific extension streams. Defining a -new top-level frame for every Codex or Copilot diagnostic would freeze too much -provider detail into the core remote-harness protocol. Conversely, using only a -generic event frame would make common assistant/tool/approval behavior harder to -implement correctly. The mixed design gives strict typed frames for common -contract surfaces and a namespaced extension stream for harness-specific detail. - -Fail-closed selection is deliberate. If a user or host explicitly selects remote -execution, silently falling back to a local model runtime can violate deployment -policy, egress policy, reproducibility expectations, canary controls, or -container-isolation guarantees. `auto` and headerless default behavior can remain -conservative, but explicit remote selection must either run remotely or fail. - -The proposal avoids putting tokens in plugin config because plugin config is -often visible to host tooling, logs, diagnostics, or user-editable config files. -Host integrations can still enforce authenticated routing, but that authority -belongs outside the OpenClaw-visible transport object. +Plugin configuration is often visible to host tooling, logs, diagnostics, or +user-editable files. Transport credentials, provider/model credentials, and +caller credentials belong in host-managed runtime state, not in a portable RFC +schema. The bridge carries only the endpoint and protocol frames needed to +operate the selected run. ## Unresolved questions -- Should `hostUri` be accepted as a public transport name, or should upstream use - a different name such as `hostAdapter` for validated reverse-proxy paths? -- Should model proxy frames be removed from v1 until a separate model-egress RFC - is accepted, or kept as reserved fail-closed frame names? -- Which package should own the public TypeScript frame types: +- Should the host-adapter transport be named `hostUri`, `hostAdapter`, or + something else that better describes validated reverse-proxy/container paths? +- Should model-proxy request/response frames be entirely deferred to a separate + model-egress RFC, or kept as reserved fail-closed v1 frame families in the + protocol reference? +- Which package should own the public frame types: `openclaw/plugin-sdk/agent-harness`, a new `openclaw/remote-harness-protocol`, or both with one re-exporting the other? -- Should `RunStart.context.workspaceDir` be exposed to remote hosts as a path, - or should v1 carry only an opaque `workspaceId` plus host-owned workspace - mounting metadata? -- Should `AgentEvent` extension streams be allowlisted by namespace, by plugin - id, or by the selected inner harness id? -- What is the minimum conformance suite for third-party remote harness hosts: - static schema validation only, golden frame transcripts, or an executable - fake host that drives OpenClaw callbacks end-to-end? -- Should OpenClaw require every remote harness to emit both typed frames and the - matching canonical `AgentEvent`, or should the Gateway synthesize canonical - agent events from typed frames when possible? +- Should v1 carry only an opaque `workspaceId`, or should OpenClaw expose + workspace path/mount metadata as a first-class AgentHarness attempt parameter + before this RFC is accepted? +- Should `AgentEvent` extension streams be allowlisted by namespace, plugin id, + or selected inner harness id? +- Should the Gateway require hosts to emit both typed frames and canonical event + frames, or should the Gateway synthesize canonical events from typed frames + whenever possible? - Should native subagent/task mirroring be standardized in this RFC or in a follow-up RFC that covers multi-agent task runtimes across Codex, Copilot, Pi, and OpenClaw-native subagents? diff --git a/rfcs/0010/protocol-v1-reference.md b/rfcs/0010/protocol-v1-reference.md new file mode 100644 index 00000000..7c7c4bab --- /dev/null +++ b/rfcs/0010/protocol-v1-reference.md @@ -0,0 +1,206 @@ +# Remote Harness Protocol v1 Supporting Reference + +This sidecar supports RFC 0010. The RFC body owns the normative proposal, +protocol schema tables, and vocabulary. This document keeps review aids that +would make the RFC body harder to read inline: validation matrices, golden +transcript sketches, and conformance scenarios. + +## Validation matrix + +| Area | Valid case | Invalid case | Required behavior | +| --- | --- | --- | --- | +| Envelope version | `protocolVersion` is `1` | absent, null, or not `1` | Reject frame and fail closed for core frames. | +| Stream identity | `streamId`, `runId`, `sessionKey`, and `workspaceId` match the stream | any immutable identity field changes mid-stream | Reject stream as protocol violation. | +| Sequencing | `seq` increases by direction | duplicate, zero, negative, or lower/equal sequence | Reject or close stream without applying payload. | +| First frame | Gateway sends `RunStart` first | any other Gateway-to-host frame starts stream | Host rejects stream. | +| Acceptance ordering | Host sends `RunAccepted` before non-terminal events | host sends assistant/tool/event frames before `RunAccepted` | Gateway rejects stream. | +| Terminal uniqueness | exactly one `RunFinal`, `RunFailed`, or `RunCancelled` | more than one terminal frame | Gateway ignores stale duplicate only after identity validation and records protocol error. | +| Unknown core kind | kind is listed in RFC schema tables | unknown non-extension kind | Reject fail-closed in v1. | +| Extension stream | namespaced stream is allowlisted | namespaced stream not allowlisted | Reject event or fail attempt according to policy. | +| Gateway decisions | host correlates explicit decision by `requestId` | host proceeds without decision or with mismatched `requestId` | Treat as protocol violation. | +| Transport authority | identity comes from `RunStart` | host header/path/hostname attempts to set identity | Ignore transport metadata and reject if it conflicts with stream context. | +| Credentials | config contains only endpoint/routing data | OpenClaw-visible config contains broad bearer/provider/caller tokens | Reject configuration. | + +## Golden transcript sketches + +These sketches show event order and required semantic preservation. They are not +wire dumps; the RFC body defines the schema fields. + +### Successful assistant-only run + +```text +Gateway -> Host: RunStart(seq=1) +Host -> Gateway: RunAccepted(seq=1) +Host -> Gateway: AgentEvent(stream=lifecycle, phase=start, seq=2) +Host -> Gateway: AssistantMessageStart(seq=3) +Host -> Gateway: AssistantDelta(delta="hello", text="hello", seq=4) +Host -> Gateway: UsageUpdate(seq=5) +Host -> Gateway: AgentEvent(stream=lifecycle, phase=end, seq=6) +Host -> Gateway: RunFinal(seq=7) +``` + +Expected Gateway behavior: + +- local lifecycle start/end events are emitted; +- assistant text streams to live surfaces; +- usage is attached when supported; +- terminal result maps to a successful AgentHarness attempt. + +### Approval denial + +```text +Gateway -> Host: RunStart(seq=1) +Host -> Gateway: RunAccepted(seq=1) +Host -> Gateway: ApprovalRequested(requestId=approval-1, seq=2) +Gateway -> Host: ApprovalDecision(requestId=approval-1, approved=false, seq=2) +Host -> Gateway: AgentEvent(stream=approval, status=denied, seq=3) +Host -> Gateway: RunFailed(error.code=approval_denied, seq=4) +``` + +Expected Gateway behavior: + +- approval is presented through normal OpenClaw policy/UI routes; +- denial is visible in approval event state; +- host does not run the denied action; +- terminal failure is replay-safe when no side effect occurred. + +### Permission denial + +```text +Gateway -> Host: RunStart(seq=1) +Host -> Gateway: RunAccepted(seq=1) +Host -> Gateway: PermissionRequested(requestId=perm-1, seq=2) +Gateway -> Host: PermissionDecision(requestId=perm-1, permissions={}, scope=turn, seq=2) +Host -> Gateway: AgentEvent(stream=approval, status=denied/unavailable, seq=3) +Host -> Gateway: RunFailed(error.code=permission_denied, seq=4) +``` + +Expected Gateway behavior: + +- OpenClaw owns permission policy and grant scope; +- missing UI route results in denied or unavailable, not implicit approval; +- terminal lifecycle records the failure. + +### Dynamic tool call + +```text +Gateway -> Host: RunStart(seq=1) +Host -> Gateway: RunAccepted(seq=1) +Host -> Gateway: DynamicToolCallRequested(requestId=tool-1, tool=web_search, seq=2) +Gateway -> Host: DynamicToolResponse(requestId=tool-1, success=true, seq=2) +Host -> Gateway: ToolStarted(toolCallId=tool-1, seq=3) +Host -> Gateway: ToolFinished(toolCallId=tool-1, isError=false, seq=4) +Host -> Gateway: RunFinal(seq=5) +``` + +Expected Gateway behavior: + +- Gateway validates and executes the dynamic tool; +- result redaction follows normal tool policy; +- tool lifecycle is visible in the activity stream. + +### Command output and patch summary + +```text +Gateway -> Host: RunStart(seq=1) +Host -> Gateway: RunAccepted(seq=1) +Host -> Gateway: ToolStarted(toolCallId=exec-1, name=bash, seq=2) +Host -> Gateway: AgentEvent(stream=item, kind=command, phase=start, seq=3) +Host -> Gateway: AgentEvent(stream=command_output, phase=delta, seq=4) +Host -> Gateway: ToolFinished(toolCallId=exec-1, isError=false, seq=5) +Host -> Gateway: AgentEvent(stream=command_output, phase=end, seq=6) +Host -> Gateway: AgentEvent(stream=patch, phase=end, seq=7) +Host -> Gateway: RunFinal(seq=8) +``` + +Expected Gateway behavior: + +- command output streams as command output, not only generic text; +- patch summary reaches the patch stream; +- activity-feed item state transitions from running to completed. + +### Compaction followed by retry + +```text +Gateway -> Host: RunStart(seq=1) +Host -> Gateway: RunAccepted(seq=1) +Host -> Gateway: AgentEvent(stream=compaction, phase=start, seq=2) +Host -> Gateway: AgentEvent(stream=compaction, phase=end, completed=true, willRetry=true, seq=3) +Host -> Gateway: AssistantMessageStart(seq=4) +Host -> Gateway: AssistantDelta(seq=5) +Host -> Gateway: RunFinal(seq=6) +``` + +Expected Gateway behavior: + +- before/after compaction hooks or equivalent lifecycle are observable; +- retry state is visible; +- stale usage before the latest compaction is not treated as the final turn's + usage. + +### Cancellation + +```text +Gateway -> Host: RunStart(seq=1) +Host -> Gateway: RunAccepted(seq=1) +Gateway -> Host: CancelRun(reason=user, seq=2) +Host -> Gateway: RunCancelled(reason=user, seq=2) +``` + +Expected Gateway behavior: + +- cancellation is correlated to the same stream identity; +- terminal lifecycle records cancellation; +- no later non-terminal frames are applied. + +### Malformed unknown core frame + +```text +Gateway -> Host: RunStart(seq=1) +Host -> Gateway: RunAccepted(seq=1) +Host -> Gateway: UnknownCoreFrame(seq=2) +Gateway: reject stream and surface remote protocol failure +``` + +Expected Gateway behavior: + +- unknown core frame does not get ignored silently; +- user-visible terminal failure or diagnostic is emitted; +- the host cannot smuggle unsupported behavior through an extension path. + +## Conformance checklist + +A v1 implementation passes conformance when it proves: + +- frame schemas validate for all core frame families listed in the RFC; +- `RunStart` ordering, `RunAccepted` ordering, immutable identity, sequencing, + and terminal uniqueness are enforced; +- unknown core frame kinds fail closed; +- unknown extension streams require allowlist admission; +- Gateway-owned decision requests never proceed without an explicit decision; +- assistant, reasoning, plan, tool, item, approval, command output, patch, + compaction, lifecycle, diagnostic, usage, and terminal events map to expected + OpenClaw surfaces; +- cancellation and timeout produce terminal lifecycle metadata; +- request headers, route metadata, and hostnames cannot override stream identity; +- OpenClaw-visible config and default remote-agent container environment do not + contain broad provider/model/caller credentials; +- golden transcript tests and an executable fake host cover success, approval + denial, permission denial, user-input, dynamic-tool, compaction, patch, + command output, cancellation, timeout, malformed frame, and unsupported event + cases. + +## Suggested test fixture inventory + +| Fixture | Purpose | +| --- | --- | +| `success-assistant-only.jsonl` | Minimal accepted run with assistant text and final result. | +| `approval-denied.jsonl` | Approval request/decision correlation and fail-closed denial. | +| `permission-denied.jsonl` | Permission policy remains Gateway-owned. | +| `dynamic-tool-success.jsonl` | Gateway executes and returns a dynamic-tool result. | +| `command-output-patch.jsonl` | Command output and patch event preservation. | +| `compaction-retry.jsonl` | Compaction lifecycle and retry state. | +| `cancelled.jsonl` | Cancellation ordering and terminal state. | +| `malformed-identity-change.jsonl` | Immutable identity enforcement. | +| `unknown-core-frame.jsonl` | v1 unknown core frame fail-closed behavior. | +| `unknown-extension-stream.jsonl` | Extension stream allowlist behavior. | From c73996a68737065215adcf247d3c4ff0168a9ea3 Mon Sep 17 00:00:00 2001 From: Eduardo Piva Date: Mon, 6 Jul 2026 21:43:16 -0700 Subject: [PATCH 3/4] docs: link remote harness RFC PR --- rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md b/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md index af8a1d64..938af08b 100644 --- a/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md +++ b/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md @@ -7,7 +7,7 @@ created: 2026-07-06 last_updated: 2026-07-06 status: draft issue: -rfc_pr: +rfc_pr: https://github.com/openclaw/rfcs/pull/31 --- # Proposal: Remote Agent Harness Bridge and Event Protocol From b337b35023421ecc87f58fe46a8389d632f119ce Mon Sep 17 00:00:00 2001 From: Eduardo Piva Date: Tue, 7 Jul 2026 09:57:03 -0700 Subject: [PATCH 4/4] docs: clarify remote harness upstream contract --- ...agent-harness-bridge-and-event-protocol.md | 87 +++++++++++++------ 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md b/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md index 938af08b..74490a98 100644 --- a/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md +++ b/rfcs/0010-remote-agent-harness-bridge-and-event-protocol.md @@ -64,7 +64,7 @@ container. - Keep runtime selection explicit and fail-closed: when a model/provider selects the remote harness, the attempt either runs remotely or fails visibly. - Keep the protocol transport-neutral: stdio, WebSocket, TCP, Unix socket, and - host-adapter paths are routing choices, not semantic differences. + `hostAdapter` paths are routing choices, not semantic differences. - Keep identity authority host-owned and explicit. `RunStart` stream context, not headers, hostnames, URL paths, or arbitrary transport metadata, is authoritative for run/session/workspace identity. @@ -79,8 +79,9 @@ container. ## Non-Goals - This RFC does not make remote harness the default OpenClaw runtime. -- This RFC does not define a cloud worker pool, deployment system, Azure/Plex/EV2 - integration, ECS/rollout policy, or production audience gate. +- This RFC does not define a cloud worker pool, deployment system, + host-specific rollout policy, deployment health gate, or production audience + gate. - This RFC does not standardize connector auth, service discovery, certificates, retries, or connection pooling; those can be owned by the host-side runtime container or platform. @@ -122,6 +123,36 @@ The upstream protocol standardizes the Gateway-to-host contract. It does not standardize how a host deploys, scales, warms, authenticates, or connects its runtime containers. +### Upstream contract and host adapter boundary + +This RFC defines the public OpenClaw contract for portable remote-harness +implementations. Implementations should treat the names and semantics in this +RFC as the upstream source of truth instead of exposing product- or +platform-specific dialects as protocol surface. + +The upstream contract owns: + +- the portable `remote-harness` AgentHarness shape; +- the protocol envelope, frame families, frame names, and wire-field casing; +- event preservation semantics and canonical `AgentEvent` stream names; +- fail-closed runtime selection behavior; +- conformance fixtures and validation expectations; +- the eventual SDK/package ownership for public frame types. + +The host or container adapter owns: + +- rollout targeting and deployment policy; +- container or worker allocation; +- reverse-proxy, sidecar, service-mesh, or service-DNS admission; +- certificates, connection pooling, retries, and route authentication; +- deployment health gates; +- platform-specific identity and routing integration below the `RunStart` + identity boundary. + +If a host uses local aliases, URLs, hostnames, or adapter-specific routing terms, +those names are translated at the host adapter boundary. They are not additional +v1 protocol names unless this RFC lists them. + ### End-to-end remote harness workflow A conforming remote run follows this workflow: @@ -134,7 +165,7 @@ A conforming remote run follows this workflow: If any required piece is missing, the attempt fails instead of falling back to local model execution. 3. **Transport open.** The Gateway-side harness opens stdio, WebSocket, TCP, - Unix socket, or a host-adapter endpoint. Transport auth and connection + Unix socket, or a `hostAdapter` endpoint. Transport auth and connection management remain outside the OpenClaw-visible config. 4. **Run start.** The Gateway sends `RunStart` as the first accepted frame. It carries the stream identity, selected inner harness id, model/provider facts, @@ -182,8 +213,8 @@ Configuration requirements: | --- | --- | | fallback | Remote selection is fail-closed and must not silently use a local runtime. | | inner harnesses | At least one inner harness id is allowlisted; the default inner harness must be allowlisted. | -| transports | v1 allows stdio, WebSocket, TCP, Unix socket, and host-adapter routing paths. | -| connector | `connector` is not a v1 OpenClaw transport kind. Container/platform networking can provide connector-like reachability outside the spec. | +| transports | v1 allows `stdio`, `websocket`, `tcp`, `unix`, and `hostAdapter` routing paths. | +| connector | `connector` and `grpc` are not v1 OpenClaw transport kinds. Container/platform networking can provide connector-like reachability outside the spec. | | credentials | OpenClaw-visible config must not contain broad bearer, provider, model, caller, Graph/M365, GitHub, or session credentials. | | egress | If a host configures egress proxy enforcement, startup fails unless that enforcement is actually wired by the host/platform. | | unknown fields | Unknown top-level config fields are rejected to avoid silent policy drift. | @@ -234,7 +265,7 @@ transcripts. | --- | --- | --- | --- | | Gateway → Host | Run control | start, cancel, heartbeat, reset, compaction-control | Host validates stream identity and applies only to the current run. | | Gateway → Host | Decisions | approval, permission, user input, dynamic tool, memory write | Host correlates by request id and must fail closed on unavailable or denied decisions. | -| Gateway → Host | Optional model proxy response | model chunk/final/error, if a separate host-egress design enables it | v1 implementations may reject model-proxy requests unless explicitly supported. | +| Gateway → Host | Optional model-egress response | model chunk/final/error, if a separate host-egress design enables it | v1 implementations may reject `ModelRequest` unless explicitly supported. | | Host → Gateway | Run acknowledgement | selected inner harness and version | Must occur before non-terminal events. | | Host → Gateway | Observable events | assistant, reasoning, lifecycle, item, plan, tool, command output, patch, compaction, approval, usage, diagnostic streams | Gateway maps them to normal OpenClaw callbacks and event streams. | | Host → Gateway | Decision requests | approval, permission, user input, dynamic tool, memory write | Gateway owns policy and returns explicit decision frames. | @@ -246,7 +277,10 @@ transcripts. Remote/local equivalence means that a user-visible or lifecycle-relevant event produced by the selected inner harness must reach the same OpenClaw surface that a local harness would have reached, unless the bridge explicitly rejects the -unsupported event before execution. +unsupported event before execution. A v1 bridge preserves the event classes the +selected inner harness observes or produces. It is not required to synthesize +event classes unsupported by that inner harness, but it must not collapse +supported structured events into plain assistant text or terminal-only output. The bridge must preserve these canonical event classes: @@ -289,8 +323,10 @@ preserves existing OpenClaw streams and harness-specific extension streams. ### Protocol schema summary All frames include the envelope fields above. The tables below list each core -frame's payload schema. Receivers reject missing required fields and reject -unknown core frame kinds in v1. +frame's payload schema. v1 wire fields use the casing listed in these schema +tables. Host adapters may translate local aliases at their boundary, but casing +aliases are not part of v1 conformance unless this RFC lists them. Receivers +reject missing required fields and reject unknown core frame kinds in v1. #### Gateway-to-host frame schemas @@ -307,9 +343,9 @@ unknown core frame kinds in v1. | `PermissionDecision` | `requestId`, `permissions`, `scope` | `strictAutoReview` | Resolves a host permission request. | | `UserInputResponse` | `requestId`, `answers` | none | Resolves a host user-input request. | | `DynamicToolResponse` | `requestId`, `contentItems`, `success` | none | Returns a Gateway-executed dynamic-tool result. | -| `ModelProxyChunk` | `requestId`, `delta` | none | Reserved model-proxy response chunk when model proxying is enabled. | -| `ModelProxyFinal` | `requestId`, `output` | `usage` | Reserved model-proxy final response when model proxying is enabled. | -| `ModelProxyError` | `requestId`, `error` | none | Reserved model-proxy error response when model proxying is enabled. | +| `ModelChunk` | `requestId`, `delta` | none | Reserved model-egress response chunk when Gateway-owned model egress is enabled. | +| `ModelFinal` | `requestId`, `output` | `usage` | Reserved model-egress final response when Gateway-owned model egress is enabled. | +| `ModelError` | `requestId`, `error` | none | Reserved model-egress error response when Gateway-owned model egress is enabled. | `RunStart.context` is a structured object with these v1 fields: @@ -360,7 +396,7 @@ unknown core frame kinds in v1. | `PermissionRequested` | `requestId`, `permissions` | `reason`, `cwd`, `environmentId` | Requests Gateway permission decision. | | `UserInputRequested` | `requestId`, `questions` | `autoResolutionMs` | Requests Gateway-mediated user input. | | `DynamicToolCallRequested` | `requestId`, `tool`, `arguments` | `namespace` | Requests Gateway dynamic-tool execution. | -| `ModelProxyRequest` | `requestId`, `model`, `payload` | none | Reserved model-proxy request when model proxying is enabled. | +| `ModelRequest` | `requestId`, `model`, `payload` | none | Reserved host request for Gateway-owned model egress when enabled. | | `Checkpoint` | `checkpointId`, `state` | none | Reports recoverable host state. | | `ArtifactProduced` | `artifactId`, `uri` | `metadata` | Reports host-produced artifact. | | `RunFinal` | `result` | `assistantTexts`, `usage`, `replayMetadata`, `itemLifecycle` | Completes the run successfully. | @@ -416,7 +452,7 @@ They must request them from the Gateway instead of acting independently. | user input | prompt presentation, secret handling, auto-resolution, response persistence | If the user route is unavailable, return explicit empty/denied/unavailable result according to policy. | | dynamic tool | tool lookup, argument validation, execution, result redaction | Unknown or denied tools fail closed; host must not synthesize success. | | memory write | memory policy, storage target, review state | Denied/unavailable means no memory write. | -| optional model proxy | provider/model credential use and response streaming | Out of core v1 unless a host enables a separate model-egress contract. | +| optional model egress | provider/model credential use and response streaming | Reserved in v1; unavailable unless the Gateway enables an explicit model-egress path. | This preserves Codex-style request-permissions, request-user-input, and dynamic tool calls without putting OpenClaw policy into the remote container. @@ -462,8 +498,8 @@ provide authority outside this spec through: The remote-harness protocol does not standardize connector auth, connection pooling, service discovery, retries, certificate handling, or credential -projection. Deployments such as Lobster can solve those concerns in the -container or host platform and expose only a reachable endpoint to the bridge. +projection. A host can solve those concerns in the container or platform layer +and expose only a reachable endpoint to the bridge. ### Container-runtime contribution boundary @@ -478,7 +514,7 @@ container orchestration: | Reference command harness/container | optional upstream reference | host may replace | | Event conformance suite | yes | host must pass | | Container allocator interface | future RFC or implementation issue | provider-specific allocator | -| Worker pool, deployment, EV2/Azure/Plex/ECS wiring | no | yes | +| Worker pool, deployment, rollout, and deployment-health wiring | no | yes | | Container DNS, hostnames, sidecars, service mesh, certificates, and connection management | no | yes | | Rollout audience authoring | no | yes | | Runtime admission storage/evaluation | invariant is upstream | mechanism is host-specific | @@ -524,8 +560,11 @@ second protocol. ### Conformance requirements -A third-party host claiming v1 support should pass a conformance suite that -covers at least: +This RFC defines the conformance scenarios. The sidecar contains reviewable +golden transcript sketches. Implementation PRs that claim v1 support should add +executable fixtures or a fake-host test harness for these scenarios. A +third-party host claiming v1 support should pass a conformance suite that covers +at least: 1. **Schema validation.** Required envelope fields, immutable identity, monotonic sequences, terminal uniqueness, and fail-closed unknown core frames. @@ -594,11 +633,9 @@ operate the selected run. ## Unresolved questions -- Should the host-adapter transport be named `hostUri`, `hostAdapter`, or - something else that better describes validated reverse-proxy/container paths? -- Should model-proxy request/response frames be entirely deferred to a separate - model-egress RFC, or kept as reserved fail-closed v1 frame families in the - protocol reference? +- Should reserved `ModelRequest` / `ModelChunk` / `ModelFinal` / `ModelError` + frames remain in v1 as fail-closed model-egress frame families, or should + model egress move entirely to a follow-up RFC? - Which package should own the public frame types: `openclaw/plugin-sdk/agent-harness`, a new `openclaw/remote-harness-protocol`, or both with one re-exporting the other?