@openclaw/copilot 插件通过 GitHub Copilot CLI(@github/copilot-sdk)运行嵌入式订阅版 Copilot
代理轮次,而不是使用 OpenClaw 内置的 harness。Copilot CLI 会负责底层的
代理循环:原生工具执行、原生压缩(infiniteSessions),以及由 CLI 在
copilotHome 下管理的线程状态。OpenClaw 仍然负责聊天
通道、会话文件、模型选择、动态工具(桥接)、审批、
媒体传递、可见的对话镜像、/btw 附加问题(参见
附加问题 (/btw)),以及 openclaw doctor。
关于更广泛的模型 / provider / runtime 拆分,请从
代理运行时 开始。
要求
- 安装了
@openclaw/copilot插件的 OpenClaw。 - 如果你的配置使用了
plugins.allow,请包含copilot(插件声明的 manifest id)。针对 npm 包名@openclaw/copilot的允许列表项不会匹配,即使设置了agentRuntime.id: "copilot"也会使插件被阻止。 - 一个可以驱动 Copilot CLI 的 GitHub Copilot 订阅,或者用于无头运行或定时任务运行的
gitHubToken环境变量 / auth-profile 条目。 - 一个可写的
copilotHome目录。当 OpenClaw 提供 agent 目录时,默认为<agentDir>/copilot,否则为~/.openclaw/agents/<agentId>/copilot。
openclaw doctor 会运行插件的 doctor contract,用于
session-state 所有权和未来的配置迁移。它不会探测
Copilot CLI 环境。
安装
Copilot 运行时作为外部插件随附,因此核心openclaw
包不包含 @github/copilot-sdk 或其平台特定的
@github/copilot-<platform>-<arch> CLI 二进制文件(总计大约 260 MB)。
仅为选择启用此运行时的 agent 安装它:
github-copilot/* 模型并且你的配置通过
agentRuntime: { id: "copilot" } 将该模型(或其提供方)路由到 Copilot 运行时时自动安装该插件;请参见
快速开始。如果没有启用该选项,OpenClaw 会使用其内置的
GitHub Copilot 提供方,并且不会安装此插件。
该运行时按以下顺序解析 SDK:
- 来自已安装的
@openclaw/copilot包的import("@github/copilot-sdk")。 - 回退目录
~/.openclaw/npm-runtime/copilot/(旧版按需 安装目标)。
COPILOT_SDK_MISSING,并附带
上面的重新安装命令。
快速开始
将一个模型(或一个 provider)固定到该 harness:agentRuntime.id,即可仅将该模型路由到
该 harness;或者在 provider 上设置,以路由该 provider 下的每个模型。
github-copilot/auto 是可移植的起点。命名的 Copilot 模型取决于
账号和组织策略;在固定之前,请确认你已通过身份验证的
Copilot CLI 实际上暴露了该模型。
支持的 provider
harness 支持规范的github-copilot provider(由
extensions/github-copilot 拥有),以及当
模型具有非空的 baseUrl 且具备以下任一 api 形状时的自定义 models.providers 条目:
anthropic-messagesazure-openai-responsesollama(OpenAI 兼容的 completions)openai-completionsopenai-responses
openai、anthropic、google、ollama)仍由
其原生运行时拥有。请改用不同的自定义 provider id,才能通过 Copilot BYOK
来路由端点。
Copilot BYOK 端点必须是公开的 HTTPS URL。harness 会为 Copilot SDK 提供
每次尝试对应的 loopback 代理,然后通过 OpenClaw 的受控 fetch 路径转发 provider 流量,
从而使 DNS pinning 和 SSRF 策略仍由 OpenClaw 管理。对于本地 Ollama、LM
Studio 或局域网模型服务器,请使用原生 OpenClaw 运行时。
BYOK
Copilot BYOK 使用 SDK 的会话级自定义提供商契约。OpenClaw 会传递解析后的模型端点、API 密钥、Bearer 令牌模式、请求头、模型 ID,以及上下文/输出限制;提供商传输逻辑保留在 SDK 中,而不是 核心中。认证
在runCopilotAttempt 中按每个 agent 应用的优先级:
-
在 attempt 输入中显式设置
useLoggedInUser: true—— 使用 该 agent 的copilotHome下 Copilot CLI 已登录的用户。 -
在 attempt 输入中显式设置
gitHubToken(需要profileId+profileVersion)。用于直接调用 CLI 和需要绕过 auth-profile 解析的测试。 -
合约解析得到的
resolvedApiKey+authProfileId—— 生产环境 主路径。Core 在调用 harness 之前会先解析 agent 配置的github-copilotauth profile(src/infra/provider-usage.auth.ts:resolveProviderAuths), 因此github-copilot:<profile>auth profile 可以在无头、cron 或多 profile 场景中端到端工作,而无需环境变量。 -
环境变量回退,按以下顺序检查(第一个非空值生效,空字符串视为缺失;与
已发布的
github-copilotprovider 优先级一致,见extensions/github-copilot/auth.ts):OPENCLAW_GITHUB_TOKEN—— harness 专用覆盖;可让你为 OpenClaw harness 固定一个 token,而不影响系统范围的gh/ Copilot CLI 配置。COPILOT_GITHUB_TOKEN—— 标准 Copilot SDK / CLI 环境变量。GH_TOKEN—— 标准ghCLI 环境变量。GITHUB_TOKEN—— 通用 GitHub token 回退。
env:<NAME>;profile version 是 token 的 不可逆 sha256 指纹,因此轮换环境变量值时会干净地使客户端池失效。 -
在没有 token 信号时的默认
useLoggedInUser。
copilotHome,因此 Copilot CLI 的 token、会话和
配置绝不会在同一台机器上的 agent 之间泄漏。默认值:
<agentDir>/copilot(使 SDK 状态与 OpenClaw 的 models.json /
auth-profiles.json 不放在同一目录下),或者在未提供 agent 目录时使用
~/.openclaw/agents/<agentId>/copilot。
可在 attempt 输入中通过 copilotHome: <path> 覆盖为自定义位置(例如用于迁移的
共享挂载)。
实时 harness 测试使用 OPENCLAW_COPILOT_AGENT_LIVE_TOKEN 作为直接 token。
共享的 live-test 设置会在将真实 auth profiles 暂存到隔离的测试 home 后清除
COPILOT_GITHUB_TOKEN、GH_TOKEN 和 GITHUB_TOKEN,因此通过专用变量传入的
gh auth token 值可以避免误判为跳过,同时不会泄漏到无关的测试套件中。
配置面
该 harness 从每次尝试的输入(runCopilotAttempt({...}))以及 extensions/copilot/src/ 中的一小组环境默认值读取配置:
OpenClaw 插件 hooks 无需任何 Copilot 专属的尝试配置。该
harness 通过标准 harness 辅助函数运行
before_prompt_build、llm_input、llm_output 和 agent_end。
成功的 SDK 压缩还会运行
before_compaction 和 after_compaction。桥接的 OpenClaw 工具会运行
before_tool_call 并报告 after_tool_call;对于没有可移植等价项的原生 SDK 专属回调,仍使用
hooksConfig。
OpenClaw 中没有其他内容需要了解这些字段。其他插件、通道和核心代码只会看到标准的 AgentHarnessAttemptParams /
AgentHarnessAttemptResult 形状。
压缩
当harness.compact 运行时,Copilot SDK harness 会:
- 在不继续未完成工作的情况下恢复已跟踪的 SDK 会话。
- 调用 SDK 的会话作用域历史压缩 RPC。
- 返回 SDK 的压缩结果,而不在工作区下写入兼容性标记 文件。
转录镜像
runCopilotAttempt 会通过 extensions/copilot/src/dual-write-transcripts.ts 将每一轮可镜像的消息双写到 OpenClaw 审计转录中。该镜像按会话(copilot:${sessionId})进行作用域划分,并按消息(${role}:${sha256_16(role,content)})进行键控,因此重新发出的先前轮次条目会与现有磁盘键发生冲突,而不会重复创建。
镜像外层包裹了两层故障隔离,以确保转录写入失败绝不会导致尝试失败:一层是内部尽力而为的包装器,另一层是尝试级别上的防御性 .catch(...)。失败只会被记录,不会向外抛出。
侧边问题(/btw)
/btw 在这个 harness 中不是原生支持的。createCopilotAgentHarness()
故意将 harness.runSideQuestion 留空未定义
(在 extensions/copilot/harness.test.ts 中的 describe("runSideQuestion") 里有断言),
因此 OpenClaw 的 /btw 分发器(src/agents/btw.ts)会回落到
它对所有非 Codex 运行时使用的相同路径:直接用一段简短的侧边问题提示调用
已配置的模型提供方,并通过 streamSimple 流式返回(不使用 CLI 会话,也不占用额外的池槽位)。
这使得 Copilot CLI 会话仅保留给代理的主轮次循环,
并让 /btw 的行为与其他非 Codex 运行时保持一致。
Doctor
Copilot 插件通过其清单和 doctor 契约提供修复元数据:- 空的
legacyConfigRules(目前尚无已弃用字段)。 - 无操作的
normalizeCompatibilityConfig(保留此项,以便未来弃用字段时 在代码库中拥有稳定的归属位置)。 - 其清单声明了一个
sessionRouteStateOwners条目:提供方为github-copilot,运行时为copilot,CLI 会话键为copilot,身份验证配置文件前缀为github-copilot:。
局限性
- 该 harness 声称支持
github-copilot以及未被拥有的自定义 BYOK provider id。 Manifest 所拥有的原生 provider id 仍由其所属的 runtime 处理,即使将agentRuntime.id强制设为copilot。 - 没有 TUI 界面;对于没有对等界面的 runtime,PI 的 TUI 仍是备用方案。
- 当 agent 切换到
copilot时,PI 会话状态不会迁移。 选择按每次尝试进行;现有的 PI 会话仍然有效。 ask_user使用与 provider 无关的网关提问 runtime。Control UI 显示与其他 OpenClaw 问题相同的问题卡片,受支持的渠道会渲染选项按钮,并且下一条排队中的纯文本消息会在 SDK 请求返回前解析该网关记录。
权限和 ask_user
桥接的 OpenClaw 工具的权限强制执行发生在工具包装器内部,而不是通过 SDK 的onPermissionRequest 回调。PI 使用的同一个
wrapToolWithBeforeToolCallHook(src/agents/agent-tools.before-tool-call.ts)也被
createOpenClawCodingTools 应用于每个编码工具:循环检测、受信任的插件策略、before-tool-call 钩子,以及通过网关(plugin.approval.request)进行的两阶段插件审批,全部都沿着与原生 PI 尝试完全相同的代码路径运行。
Copilot 工具桥接返回的每个 SDK 工具都标记为:
overridesBuiltInTool: true— 替换 Copilot CLI 中同名的内置工具(edit、read、write、bash、…),因此每次工具调用都会路由回 OpenClaw。skipPermission: true— 告诉 SDK 在调用工具之前不要触发onPermissionRequest({kind: "custom-tool"})。包装后的execute()已经执行了更丰富的 OpenClaw 策略检查;如果在 SDK 层再弹出提示,要么会短路 OpenClaw 的强制执行(全部允许),要么会阻止每一次工具调用(全部拒绝)——这两种情况都不符合 PI 的对等行为。
extensions/codex/src/app-server/dynamic-tools.ts),而 codex-app-server 自己原生的审批类型
(item/commandExecution/requestApproval、item/fileChange/requestApproval、item/permissions/requestApproval)则通过
plugin.approval.request 路由(extensions/codex/src/app-server/approval-bridge.ts)。Copilot SDK 的对应机制——对任何最终进入 onPermissionRequest 的非 custom-tool 类型使用 fail-closed 的 rejectAllPolicy——是同样的安全网,而实际上它从不会触发,因为 overridesBuiltInTool: true 会替换掉所有内置工具。
为了让被包装的工具层做出与 PI 等价的策略决定,harness 会将完整的 PI attempt-tool 上下文转发给
createOpenClawCodingTools:身份信息(senderIsOwner、memberRoleIds、ownerOnlyToolAllowlist、…)、频道/路由(groupId、
currentChannelId、replyToMode、消息工具开关)、认证(authProfileStore)、运行身份(由 sandboxSessionKey、runId 派生的
sessionKey / runSessionKey)、模型上下文(modelApi、
modelContextWindowTokens、modelCompat、modelHasVision)、以及运行钩子(onToolOutcome、onYield)。如果缺少这些字段,owner-only allowlist 会默认静默拒绝,插件信任策略无法解析到正确的作用域,而 session_status: "current" 会解析到过期的 sandbox key。桥接构建器是 extensions/copilot/src/tool-bridge.ts,它与 PI 的权威调用 src/agents/embedded-agent-runner/run/attempt.ts:1262 保持一致。runAttempt 通过共享的 resolveSandboxContext 接口解析 sandbox 上下文,向 SDK 传递有效的工作目录,并把 sandbox 以及子代理启动工作区转发到工具桥接中。该桥接还会转发它能在 SDK 边界强制执行的受限工具构建控制:includeCoreTools、运行时工具 allowlist,以及 toolConstructionPlan。
该桥接还使用来自
openclaw/plugin-sdk/agent-harness-tool-runtime 的共享 harness 工具面辅助实现 PI parity。启用
tool-search 时,SDK 会看到紧凑的控制工具加一个隐藏的
catalog 执行器,而不是每一个 OpenClaw 工具 schema。启用代码模式时,helper 会构建与其他 agent harness 使用的相同代码模式控制面和 catalog 生命周期。轻量本地模型默认值、
运行时兼容的 schema 过滤、目录 hydration,以及 catalog
清理都保留在共享 helper 中,这样 Copilot 和 Codex 相关
harness 就不会分叉。
会话级 GitHub token
Copilot SDK 合同区分客户端级 GitHub token (CopilotClientOptions.gitHubToken,用于认证 CLI 进程本身)
和会话级 token(SessionConfig.gitHubToken,决定该会话的内容排除、模型路由和配额;在 createSession 和 resumeSession 中都会生效)。harness 只通过一次 resolveCopilotAuth 解析认证,并在认证模式为 gitHubToken 时设置这两个字段(即显式的 auth.gitHubToken,或者从已配置的 github-copilot 认证配置中由契约解析出的 resolvedApiKey)。当解析出的模式是 useLoggedInUser 时,会省略会话级字段,这样 SDK 就会继续从已登录身份中推导身份信息。
ask_user 使用 SessionConfig.onUserInputRequest。桥接会将 SDK 的选择题或无选项的自由文本提示注册为网关问题;对于固定选项请求,会接受选项索引或标签;当 SDK 请求允许时,则接受自由格式的回答。中止 OpenClaw 尝试会取消网关记录,并返回一个空的 SDK 回答。