--- summary: "Run OpenClaw embedded agent turns through the external GitHub Copilot SDK harness" title: "Copilot SDK harness" read_when: - You want to use the GitHub Copilot SDK harness for an agent - You need configuration examples for the `copilot` runtime - You are wiring an agent to subscription Copilot (github / openclaw / copilot) and want it to run through the Copilot CLI --- The external `@openclaw/copilot` plugin lets OpenClaw run embedded subscription Copilot agent turns through the GitHub Copilot CLI (`@github/copilot-sdk`) instead of the built-in PI harness. Use the Copilot SDK harness when you want the Copilot CLI session to own the low-level agent loop: native tool execution, native compaction (`infiniteSessions`), and CLI-managed thread state under `copilotHome`. OpenClaw still owns chat channels, session files, model selection, OpenClaw dynamic tools (bridged), approvals, media delivery, the visible transcript mirror, `/btw` side questions (handled by the in-tree PI fallback — see [Side questions (`/btw`)](#side-questions-btw)), and `openclaw doctor`. For the broader model/provider/runtime split, start with [Agent runtimes](/concepts/agent-runtimes). ## Requirements - OpenClaw with the `@openclaw/copilot` plugin installed. - If your config uses `plugins.allow`, include `copilot` (the manifest id declared by the plugin). A restrictive allowlist that uses the npm-style `@openclaw/copilot` package name will leave the plugin blocked and the runtime will not load even with `agentRuntime.id: "copilot"`. - A GitHub Copilot subscription that can drive the Copilot CLI (or a `gitHubToken` env / auth-profile entry for headless / cron runs). - A writable `copilotHome` directory. The harness defaults to `/copilot` when OpenClaw provides an agent directory, otherwise `~/.openclaw/agents//copilot` for full per-agent isolation. `openclaw doctor` runs the plugin [doctor contract](#doctor) for declarative session-state ownership and future compatibility migrations. It does not run Copilot CLI environment probes. ## Plugin install The Copilot runtime is an external plugin so the core `openclaw` package does not carry the `@github/copilot-sdk` dependency or its platform-specific `@github/copilot--` CLI binary. Together they add roughly 260 MB, so install them only for agents that opt into this runtime: ```bash openclaw plugins install @openclaw/copilot ``` The wizard installs the plugin the first time you select a `github-copilot/*` model **and** your config opts the model (or its provider) into the Copilot agent runtime via `agentRuntime: { id: "copilot" }` (see [Quickstart](#quickstart) below). Without the opt-in, openclaw uses its built-in GitHub Copilot provider and never installs the runtime plugin. The runtime resolves the SDK in this order: 1. `import("@github/copilot-sdk")` from the installed `@openclaw/copilot` package. 2. The well-known fallback dir `~/.openclaw/npm-runtime/copilot/` (the legacy on-demand install target). A missing SDK surfaces a single error with code `COPILOT_SDK_MISSING` and the plugin reinstall command above. ## Quickstart Pin one model (or one provider) to the harness: ```json5 { agents: { defaults: { model: "github-copilot/auto", models: { "github-copilot/auto": { agentRuntime: { id: "copilot" }, }, }, }, }, } ``` Both routes are equivalent. Use `agentRuntime.id` on a single model entry when only that model should be routed through the harness; set `agentRuntime.id` on a provider when every model under that provider should use it. `github-copilot/auto` is the portable starting point. Named Copilot models are account- and organization-policy-dependent, so only pin one after confirming that the authenticated Copilot CLI exposes it. ## Supported providers The harness advertises support for the canonical `github-copilot` provider (the same id owned by `extensions/github-copilot`): - `github-copilot` It also supports custom `models.providers` entries when the selected model has a non-empty `baseUrl` and one of these API shapes: - `openai-responses` - `openai-completions` - `ollama` (OpenAI-compatible completions) - `azure-openai-responses` - `anthropic-messages` Native provider ids such as `openai`, `anthropic`, `google`, and `ollama` remain owned by their native runtimes. Use a distinct custom provider id when routing an endpoint through Copilot BYOK. Copilot BYOK endpoints must be public-network HTTPS URLs. The harness gives the Copilot SDK a per-attempt loopback proxy URL, then forwards provider traffic through OpenClaw's guarded fetch path so DNS pinning and SSRF policy stay owned by OpenClaw. Use the native OpenClaw runtime for local Ollama, LM Studio, or LAN model servers. ## BYOK Copilot BYOK uses the SDK's session-level custom provider contract. OpenClaw passes the resolved model endpoint, API key, bearer-token mode, headers, model id, and context/output limits without moving provider transport logic into core. For example: ```json5 { agents: { defaults: { model: "custom-proxy/llama-3.1-8b", models: { "custom-proxy/llama-3.1-8b": { agentRuntime: { id: "copilot" }, }, }, }, }, models: { mode: "merge", providers: { "custom-proxy": { baseUrl: "https://api.example.com/v1", apiKey: "${CUSTOM_PROXY_API_KEY}", api: "openai-responses", authHeader: true, models: [{ id: "llama-3.1-8b", name: "Llama 3.1 8B" }], }, }, }, } ``` BYOK sessions are separately keyed from subscription sessions and from other endpoints or credential fingerprints. Rotating the key, headers, model, or endpoint creates a fresh Copilot SDK session instead of resuming incompatible state. ## Auth Per-agent precedence, applied during `runCopilotAttempt`: 1. **Explicit `useLoggedInUser: true`** on the attempt input. Uses the Copilot CLI's logged-in user resolved under the agent's `copilotHome`. 2. **Explicit `gitHubToken`** on the attempt input (with `profileId` + `profileVersion`). Useful for direct CLI invocations and tests where the caller wants to bypass auth-profile resolution. 3. **Contract-resolved `resolvedApiKey` + `authProfileId`** from the `EmbeddedRunAttemptParams` shape. This is the **production main path**: core resolves the agent's configured `github-copilot` auth profile (via `src/infra/provider-usage.auth.ts:resolveProviderAuths`) before invoking the harness, and the harness consumes both fields directly. This makes a `github-copilot:` auth profile work end-to-end for headless / cron / multi-profile setups without env vars. 4. **Env-var fallback** for direct CLI / dogfood runs where no auth profile is configured. The runtime checks the following vars in precedence order, mirroring the shipped `github-copilot` provider (`extensions/github-copilot/auth.ts`) and the documented Copilot SDK setup: 1. `OPENCLAW_GITHUB_TOKEN` -- harness-specific override; set this to pin a token for the OpenClaw harness without disturbing system-wide `gh` / Copilot CLI config. 2. `COPILOT_GITHUB_TOKEN` -- standard Copilot SDK / CLI env var. 3. `GH_TOKEN` -- standard `gh` CLI env var (matches the existing `github-copilot` provider precedence). 4. `GITHUB_TOKEN` -- generic GitHub token fallback. The first non-empty value wins; empty strings are treated as absent. The synthesised pool profile id is `env:` and the profileVersion is a non-reversible sha256 fingerprint of the token, so rotating the env value cleanly busts the client pool. 5. **Default `useLoggedInUser`** when no token signal is available. Each agent gets a dedicated `copilotHome` so Copilot CLI tokens, sessions, and config do not leak between agents on the same machine. The default is `/copilot` when the host hands the harness an agent directory (isolating SDK state from OpenClaw's `models.json` / `auth-profiles.json` in the same directory), or `~/.openclaw/agents//copilot` otherwise. Override with `copilotHome: ` on the attempt input when you need a custom location (for example, a shared mount for migration). Live harness tests use `OPENCLAW_COPILOT_AGENT_LIVE_TOKEN` when a direct token is needed. The shared live-test setup intentionally scrubs `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, and `GITHUB_TOKEN` after staging real auth profiles into the isolated test home, so passing a `gh auth token` value through the dedicated live-test variable avoids false skips without exposing the token to unrelated suites. ## Configuration surface The harness reads its config from per-attempt input (`runCopilotAttempt({...})`) plus a small set of env defaults inside `extensions/copilot/src/`: - `copilotHome` — per-agent CLI state directory (defaults documented above). - `model` — string or `{ provider, id, api?, baseUrl?, headers?, authHeader? }`. When omitted, OpenClaw uses the agent's normal model selection and the harness verifies the resolved provider is supported. - `reasoningEffort` — `"low" | "medium" | "high" | "xhigh"`. Maps from OpenClaw's `ThinkLevel` / `ReasoningLevel` resolution in `auto-reply/thinking.ts`. - `infiniteSessionConfig` — optional override for the SDK `infiniteSessions` block driven by `harness.compact`. Defaults are safe to leave as-is. - `hooksConfig` — optional native Copilot SDK `SessionHooks` compatibility config for tool/MCP, user-prompt, session, and error callbacks. It is separate from OpenClaw's portable lifecycle hooks. - `permissionPolicy` — optional override for the SDK's `onPermissionRequest` handler used for built-in SDK tool kinds (`shell`, `write`, `read`, `url`, `mcp`, `memory`, `hook`). Defaults to `rejectAllPolicy` as a safety net; in practice the SDK never invokes any of those kinds because every bridged OpenClaw tool is registered with `overridesBuiltInTool: true` and `skipPermission: true` so 100% of tool calls flow through OpenClaw's wrapped `execute()`. See [Permissions and ask_user](#permissions-and-ask_user). - `enableSessionTelemetry` — optional SDK session telemetry flag. OpenClaw plugin hooks do not need Copilot-specific attempt configuration. The harness runs `before_prompt_build` (and the legacy `before_agent_start` compatibility hook), `llm_input`, `llm_output`, and `agent_end` through the standard harness helpers. Successful SDK compactions also run `before_compaction` and `after_compaction`. Bridged OpenClaw tools continue to run `before_tool_call` and report `after_tool_call`; `hooksConfig` remains for native SDK-only callbacks that have no portable equivalent. Nothing in the rest of OpenClaw needs to know about these fields. Other plugins, channels, and core code only see the standard `AgentHarnessAttemptParams` / `AgentHarnessAttemptResult` shape. ## Compaction When `harness.compact` runs, the Copilot SDK harness: 1. Resumes the tracked SDK session without continuing pending work. 2. Calls the SDK's session-scoped history compaction RPC. 3. Returns the SDK compaction outcome without writing compatibility marker files under the workspace. The OpenClaw side transcript mirror (see below) continues to receive the post-compaction messages, so user-facing chat history stays consistent. ## Transcript mirroring `runCopilotAttempt` dual-writes each turn's mirrorable messages into the OpenClaw audit transcript via `extensions/copilot/src/dual-write-transcripts.ts`. The mirror is per-session scoped (`copilot:${sessionId}`) and uses a per-message identity (`${role}:${sha256_16(role,content)}`) so re-emits of prior-turn entries collide with existing on-disk keys and do not duplicate. The mirror is wrapped in two layers of failure containment so a transcript write failure cannot fail the attempt: an internal best-effort wrapper and a defense-in-depth `.catch(...)` at the attempt level. Failures are logged but not surfaced. ## Side questions (`/btw`) `/btw` is **not** native on this harness. `createCopilotAgentHarness()` deliberately leaves `harness.runSideQuestion` undefined, so OpenClaw's `/btw` dispatcher (`src/agents/btw.ts`) falls through to the same in-tree PI fallback path it uses for every non-Codex runtime: the configured model provider is called directly with a short side-question prompt and streamed back via `streamSimple` (no CLI session, no extra pool slot). This keeps Copilot CLI sessions reserved for the agent's main turn loop, and keeps `/btw` behavior identical to other PI-backed runtimes. The contract is asserted in [`extensions/copilot/harness.test.ts`](https://github.com/openclaw/openclaw/blob/main/extensions/copilot/harness.test.ts) under `describe("runSideQuestion")`. ## Doctor `extensions/copilot/doctor-contract-api.ts` is auto-loaded by `src/plugins/doctor-contract-registry.ts`. It contributes: - An empty `legacyConfigRules` (no retired fields at MVP). - A no-op `normalizeCompatibilityConfig` (kept so future field retirements have a stable in-tree home). - One `sessionRouteStateOwners` entry claiming provider `github-copilot`; runtime `copilot`; CLI session key `copilot`; auth profile prefix `github-copilot:`. ## Limitations - The harness claims `github-copilot` plus unowned custom BYOK provider ids. Manifest-owned native provider ids stay on their owning runtime even when `agentRuntime.id` is forced to `copilot`. - The harness does not deliver TUI; PI's TUI is unaffected and remains the fallback for whatever runtimes do not have a peer surface. - PI session state is not migrated when an agent switches to `copilot`. Selection is per attempt; existing PI sessions remain valid. - `ask_user` uses the same OpenClaw prompt-and-reply path as the Codex harness. When the Copilot SDK asks for user input, OpenClaw posts a blocking prompt to the active channel/TUI and the next queued user message resolves the SDK request. ## Permissions and ask_user Permission enforcement for bridged OpenClaw tools happens **inside the tool wrapper**, not via the SDK's `onPermissionRequest` callback. The same `wrapToolWithBeforeToolCallHook` that PI uses (`src/agents/pi-tools.before-tool-call.ts`) is applied by `createOpenClawCodingTools` to every coding tool: loop detection, trusted plugin policies, before-tool-call hooks, and two-phase plugin approvals via the gateway (`plugin.approval.request`) all run with the exact same code path as native PI attempts. To let that wrapper own the decision, the SDK Tool returned by `convertOpenClawToolToSdkTool` is marked with: - `overridesBuiltInTool: true` — replaces the Copilot CLI's built-in tool of the same name (edit, read, write, bash, …) so every tool invocation routes back to OpenClaw. - `skipPermission: true` — tells the SDK not to fire `onPermissionRequest({kind: "custom-tool"})` before invoking the tool. The wrapped `execute()` performs the richer OpenClaw policy check internally; an SDK-level prompt would either short-circuit OpenClaw's enforcement (if we allow-all) or block every tool call (if we reject-all) — neither matches PI parity. The in-tree codex harness uses the same split: bridged OpenClaw tools are wrapped (`extensions/codex/src/app-server/dynamic-tools.ts`) and the codex-app-server's _own_ native approval kinds (`item/commandExecution/requestApproval`, `item/fileChange/requestApproval`, `item/permissions/requestApproval`) are routed through `plugin.approval.request` (`extensions/codex/src/app-server/approval-bridge.ts`). The Copilot SDK equivalent — fail-closed `rejectAllPolicy` for any non-`custom-tool` kind that ever reaches `onPermissionRequest` — is the same safety net, and it does not fire in practice because `overridesBuiltInTool: true` displaces every built-in. For the wrapped-tool layer to make policy decisions equivalent to PI, the harness forwards the full PI attempt-tool context to `createOpenClawCodingTools` — identity (`senderIsOwner`, `memberRoleIds`, `ownerOnlyToolAllowlist`, …), channel/routing (`groupId`, `currentChannelId`, `replyToMode`, message-tool toggles), auth (`authProfileStore`), run identity (`sessionKey`/`runSessionKey` derived from `sandboxSessionKey`, `runId`), model context (`modelApi`, `modelContextWindowTokens`, `modelCompat`, `modelHasVision`), and run hooks (`onToolOutcome`, `onYield`). Without those fields, owner-only allowlists silently behave as deny-by-default, plugin-trust policies cannot resolve to the right scope, and `session_status: "current"` resolves to a stale sandbox key. The bridge builder is in `extensions/copilot/src/tool-bridge.ts` and mirrors the PI authoritative call at `src/agents/pi-embedded-runner/run/attempt.ts:1029-1117`. `runAttempt` already resolves sandbox context through the shared `resolveSandboxContext` seam, passes the SDK an effective working directory, and forwards `sandbox` plus the subagent-spawn workspace into the tool bridge. The bridge also forwards the bounded tool-construction controls it can enforce at the SDK boundary: `includeCoreTools`, the runtime tool allowlist, and `toolConstructionPlan`. The bridge also uses the shared harness tool-surface helper from `openclaw/plugin-sdk/agent-harness-tool-runtime` for PI parity. When tool-search is enabled, the SDK sees compact control tools plus a hidden catalog executor instead of every OpenClaw tool schema. When code mode is enabled, the helper builds the same code-mode control surface and catalog lifecycle used by other agent harnesses. Local-model lean defaults, runtime-compatible schema filtering, directory hydration, and catalog cleanup all stay in the shared helper so Copilot and Codex-adjacent harnesses do not drift. ### Session-level GitHub token The Copilot SDK contract distinguishes the **client-level** GitHub token (`CopilotClientOptions.gitHubToken`, used to authenticate the CLI process itself) from the **session-level** token (`SessionConfig.gitHubToken`, which determines content exclusion, model routing, and quota for that session and is honored on both `createSession` and `resumeSession`). The harness resolves auth once via `resolveCopilotAuth` and sets both fields when the auth mode is `gitHubToken` (an explicit `auth.gitHubToken` or a contract-resolved `resolvedApiKey` from a configured `github-copilot` auth profile). When the resolved mode is `useLoggedInUser`, the session-level field is omitted so the SDK keeps deriving identity from the logged-in identity. `ask_user` uses `SessionConfig.onUserInputRequest`. The bridge accepts choice indexes or labels for fixed-choice requests, accepts free-form answers when the SDK request allows them, and cancels a pending request when the OpenClaw attempt is aborted. ## Related - [Agent runtimes](/concepts/agent-runtimes) - [Codex harness](/plugins/codex-harness) - [Agent harness plugins (SDK reference)](/plugins/sdk-agent-harness)