Copilot SDK Harness Plugin for OpenClaw

Learn how the @openclaw/copilot plugin runs agent turns through GitHub Copilot CLI, with setup requirements and ownership boundaries. Ideal for developers integrating Copilot into OpenClaw.

Read this 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 executes embedded subscription Copilot agent turns through the GitHub Copilot CLI (@github/copilot-sdk) rather than OpenClaw's internal harness. The low-level agent loop is owned by the Copilot CLI session: native tool execution, native compaction (infiniteSessions), and CLI-managed thread state under copilotHome. OpenClaw retains ownership of chat channels, session files, model selection, dynamic tools (bridged), approvals, media delivery, the visible transcript mirror, /btw side questions (see Side questions (/btw)), and openclaw doctor.

For the broader model/provider/runtime split, begin with Agent runtimes.

Requirements

  • OpenClaw with the @openclaw/copilot plugin installed.
  • If your config uses plugins.allow, include copilot (the manifest id the plugin declares). An allowlist entry for the npm package name @openclaw/copilot will not match and leaves the plugin blocked, even with agentRuntime.id: "copilot" set.
  • A GitHub Copilot subscription that can drive the Copilot CLI, or a gitHubToken env var / auth-profile entry for headless or cron runs.
  • A writable copilotHome directory. Defaults to <agentDir>/copilot when OpenClaw provides an agent directory, otherwise ~/.openclaw/agents/<agentId>/copilot.

openclaw doctor runs the plugin's doctor contract for session-state ownership and future config migrations. It does not probe the Copilot CLI environment.

Install

The Copilot runtime ships as an external plugin so the core openclaw package does not carry @github/copilot-sdk or its platform-specific @github/copilot-<platform>-<arch> CLI binary (roughly 260 MB together). Install it only for agents that opt into this runtime:

openclaw plugins install @openclaw/copilot

The setup wizard installs the plugin automatically the first time you select a github-copilot/* model and your config routes that model (or its provider) to the Copilot runtime via agentRuntime: { id: "copilot" }; see Quickstart. Without that opt-in, OpenClaw uses its built-in GitHub Copilot provider and never installs this plugin.

The runtime resolves the SDK in this order:

  1. import("@github/copilot-sdk") from the installed @openclaw/copilot package.
  2. The fallback dir ~/.openclaw/npm-runtime/copilot/ (legacy on-demand install target).

A missing SDK surfaces one error with code COPILOT_SDK_MISSING and the reinstall command above.

Quickstart

Pin one model (or one provider) to the harness:

{
  agents: {
    defaults: {
      model: "github-copilot/auto",
      models: {
        "github-copilot/auto": {
          agentRuntime: { id: "copilot" },
        },
      },
    },
  },
}

Set agentRuntime.id on a single model entry to route only that model through the harness, or on a provider to route every model under that provider.

github-copilot/auto is the portable starting point. Named Copilot models are account- and organization-policy-dependent; confirm your authenticated Copilot CLI actually exposes a model before pinning it.

Supported providers

The harness supports the canonical github-copilot provider (owned by extensions/github-copilot), plus custom models.providers entries when the model has a non-empty baseUrl and one of these api shapes:

  • anthropic-messages
  • azure-openai-responses
  • ollama (OpenAI-compatible completions)
  • openai-completions
  • openai-responses

Native provider ids (openai, anthropic, google, ollama) stay owned by their native runtimes. Use a distinct custom provider id to route an endpoint through Copilot BYOK instead.

Copilot BYOK endpoints must be public HTTPS URLs. The harness gives the Copilot SDK a per-attempt loopback proxy, 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; provider transport logic stays in the SDK, not core.

{
  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 keyed separately from subscription sessions and from other BYOK endpoints or credentials. Rotating the key, headers, model, or endpoint starts a fresh Copilot SDK session instead of resuming incompatible state.

Auth

Precedence, applied per agent during runCopilotAttempt:

  1. useLoggedInUser: true set explicitly on the attempt input, which draws on the Copilot CLI's authenticated user via the agent's copilotHome.

  2. gitHubToken supplied directly on the attempt input, needing both profileId and profileVersion. This suits direct CLI calls and tests that must skip auth-profile resolution.

  3. resolvedApiKey plus authProfileId resolved through the contract, which serves as the primary production route. Before the harness runs, Core determines the agent's configured github-copilot auth profile (src/infra/provider-usage.auth.ts:resolveProviderAuths). That way, a github-copilot:<profile> auth profile functions fully across headless, cron, or multi-profile scenarios without relying on environment variables.

  4. Fallback via environment variables, evaluated in this sequence (the first non-empty value takes precedence; empty strings are treated as missing; this mirrors the github-copilot provider priority shipped in extensions/github-copilot/auth.ts):

    1. OPENCLAW_GITHUB_TOKEN: a harness-specific override that pins a token for the OpenClaw harness without touching system-wide gh or Copilot CLI settings.
    2. COPILOT_GITHUB_TOKEN: the standard environment variable for the Copilot SDK and CLI.
    3. GH_TOKEN: the usual gh CLI environment variable.
    4. GITHUB_TOKEN: a general GitHub token fallback.

    The generated pool profile carries the id env:<NAME>, and its version is a one-way sha256 fingerprint of the token. Rotating the environment value therefore resets the client pool cleanly.

  5. useLoggedInUser used by default when no token signal exists.

A dedicated copilotHome is assigned to each agent, preventing Copilot CLI tokens, sessions, and configuration from crossing between agents on the same host. The default is <agentDir>/copilot, which keeps SDK state separate from OpenClaw's models.json and auth-profiles.json directories, or ~/.openclaw/agents/<agentId>/copilot when no agent directory is given. To specify a different location, such as a shared mount for migration, set copilotHome: <path> on the attempt input.

Live harness tests rely on OPENCLAW_COPILOT_AGENT_LIVE_TOKEN for a direct token. The shared live-test setup stages real auth profiles into the isolated test home and then removes COPILOT_GITHUB_TOKEN, GH_TOKEN, and GITHUB_TOKEN. Passing a gh auth token value through the dedicated variable prevents false skips without contaminating other test suites.

Configuration surface

The harness pulls configuration from per-attempt input (runCopilotAttempt({...})) along with a limited set of environment defaults found in extensions/copilot/src/:

FieldPurpose
copilotHomeThe per-agent CLI state directory, with defaults as described above.
modelA string or { provider, id, api?, baseUrl?, headers?, authHeader? }. Leave it out to use the agent's standard model selection; the harness confirms the resolved provider is supported.
reasoningEffort"low" | "medium" | "high" | "xhigh". This aligns with OpenClaw's ThinkLevel and ReasoningLevel resolution in auto-reply/thinking.ts.
infiniteSessionConfigAn optional override for the SDK infiniteSessions block controlled by harness.compact. It is safe to keep the default.
hooksConfigOptional native Copilot SDK SessionHooks configuration for tool/MCP, user-prompt, session, and error callbacks. This is separate from OpenClaw's portable lifecycle hooks.
permissionPolicyAn optional override for the SDK's onPermissionRequest handler covering built-in SDK tool kinds (shell, write, read, url, mcp, memory, hook). The fallback is rejectAllPolicy as a safeguard; see Permissions and ask_user for why it never actually triggers.
enableSessionTelemetryAn optional flag for SDK session telemetry.

OpenClaw plugin hooks don't require any Copilot-specific attempt configuration. The standard harness helpers execute before_prompt_build, llm_input, llm_output, and agent_end. When SDK compactions succeed, before_compaction and after_compaction also run. Bridged OpenClaw tools execute before_tool_call and report after_tool_call; hooksConfig is reserved for native SDK-only callbacks that have no portable counterpart.

These fields remain invisible to the rest of OpenClaw. Other plugins, channels, and core code only encounter the conventional AgentHarnessAttemptParams / AgentHarnessAttemptResult structure.

Compaction

During harness.compact execution, the Copilot SDK harness performs these steps:

  1. It resumes the tracked SDK session without restarting any pending work.
  2. It invokes the SDK's session-scoped history compaction RPC.
  3. It returns the compaction result without writing any compatibility marker files into the workspace.

The OpenClaw-side transcript mirror (detailed below) continues receiving post-compaction messages, so user-facing chat history remains consistent.

Transcript mirroring

runCopilotAttempt dual-writes each turn's mirrorable messages into the OpenClaw audit transcript through extensions/copilot/src/dual-write-transcripts.ts. The mirror is scoped per session (copilot:${sessionId}) and keyed per message (${role}:${sha256_16(role,content)}), so re-emitted prior-turn entries collide with existing on-disk keys instead of creating duplicates.

Two layers of failure containment protect the mirror so a transcript write failure never fails the attempt: an internal best-effort wrapper, plus a defense-in-depth .catch(...) at the attempt level. Failures are logged, not surfaced.

Side questions (/btw)

/btw is not native on this harness. createCopilotAgentHarness() intentionally leaves harness.runSideQuestion undefined (asserted in extensions/copilot/harness.test.ts, describe("runSideQuestion")), so OpenClaw's /btw dispatcher (src/agents/btw.ts) falls through to the same 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 non-Codex runtimes.

Doctor

The Copilot plugin contributes doctor repair metadata through its manifest and doctor contract:

  • An empty legacyConfigRules (no retired fields yet).
  • A no-op normalizeCompatibilityConfig (kept so future field retirements have a stable in-tree home).
  • Its manifest declares one sessionRouteStateOwners entry: 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.
  • No TUI surface; PI's TUI remains the fallback for runtimes without a peer surface.
  • PI session state does not migrate when an agent switches to copilot. Selection is per attempt; existing PI sessions remain valid.
  • ask_user uses the provider-neutral gateway question runtime. The Control UI shows the same question card as other OpenClaw questions, supported channels render choice buttons, and the next queued plain-text message resolves that gateway record before the SDK request returns.

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/agent-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 through the exact same code path as native PI attempts.

Each SDK tool returned by the Copilot tool bridge is marked with:

  • overridesBuiltInTool: true, replaces the Copilot CLI's built-in tool of the same name (edit, read, write, bash, ...) so every tool call routes back to OpenClaw.
  • skipPermission: true, tells the SDK not to fire onPermissionRequest({kind: "custom-tool"}) before invoking the tool. The wrapped execute() already performs the richer OpenClaw policy check; an SDK-level prompt would either short-circuit OpenClaw's enforcement (allow-all) or block every tool call (reject-all), neither matches PI parity.

The in-tree Codex harness follows the same separation: bridged OpenClaw tools get wrapped (extensions/codex/src/app-server/dynamic-tools.ts), while the codex-app-server's native approval kinds (item/commandExecution/requestApproval, item/fileChange/requestApproval, item/permissions/requestApproval) go through plugin.approval.request (extensions/codex/src/app-server/approval-bridge.ts). The Copilot SDK counterpart, which is fail-closed rejectAllPolicy for any non-custom-tool kind reaching onPermissionRequest, acts as the identical safeguard, though it never triggers in real usage because overridesBuiltInTool: true replaces all built-ins.

To let the wrapped-tool layer enforce policy decisions on par with PI, the harness passes the complete PI attempt-tool context into createOpenClawCodingTools: identity (senderIsOwner, memberRoleIds, ownerOnlyToolAllowlist, ...), channel and routing details (groupId, currentChannelId, replyToMode, message-tool toggles), auth (authProfileStore), run identity (sessionKey or runSessionKey derived from sandboxSessionKey, runId), model context (modelApi, modelContextWindowTokens, modelCompat, modelHasVision), and run hooks (onToolOutcome, onYield). Missing these fields causes owner-only allowlists to deny silently by default, plugin-trust policies fail to resolve to the correct scope, and session_status: "current" ends up with a stale sandbox key. The bridge builder is extensions/copilot/src/tool-bridge.ts, which mirrors the PI authoritative call at src/agents/embedded-agent-runner/run/attempt.ts:1262. runAttempt resolves sandbox context through the shared resolveSandboxContext seam, supplies the SDK with an effective working directory, and sends sandbox along with the subagent-spawn workspace into the tool bridge. Additionally, the bridge passes forward the bounded tool-construction controls it can enforce at the SDK boundary: includeCoreTools, the runtime tool allowlist, and toolConstructionPlan.

For PI parity, the bridge relies on the shared harness tool-surface helper from openclaw/plugin-sdk/agent-harness-tool-runtime. With tool-search enabled, the SDK sees compact control tools plus a hidden catalog executor, not every OpenClaw tool schema. In code mode, the helper constructs the same code-mode control surface and catalog lifecycle that other agent harnesses use. Local-model lean defaults, runtime-compatible schema filtering, directory hydration, and catalog cleanup remain within the shared helper, so Copilot and Codex-adjacent harnesses stay consistent.

Session-level GitHub token

The Copilot SDK contract separates the client-level GitHub token (CopilotClientOptions.gitHubToken, which authenticates the CLI process itself) from the session-level token (SessionConfig.gitHubToken, which governs content exclusion, model routing, and quota for that session; both createSession and resumeSession honor it). The harness performs auth resolution once via resolveCopilotAuth and populates both fields when the auth mode is gitHubToken (either an explicit auth.gitHubToken or a contract-resolved resolvedApiKey from a configured github-copilot auth profile). If the resolved mode is useLoggedInUser, the session-level field gets omitted, letting the SDK continue deriving identity from the logged-in identity.

ask_user makes use of SessionConfig.onUserInputRequest. The bridge registers SDK choices or option-less free-text prompts as gateway questions, accepts choice indexes or labels for fixed-choice requests, and takes free-form answers when the SDK request permits them. Aborting the OpenClaw attempt cancels the gateway record and yields an empty SDK answer.

2,223 words · updated Aug 14, 2026