# dsh-oauth-copilot

[![npm](https://img.shields.io/npm/v/dsh-oauth-copilot)](https://www.npmjs.com/package/dsh-oauth-copilot) · [GitHub](https://github.com/FMDD61/dsh-oauth-copilot)

English | **[中文文档](README_zh.md)**

面向 [DeepSeek Harness](https://www.npmjs.com/package/@deepseek-ai/dsh) 的 GitHub Copilot 登录与模型路由插件。

通过设备码流程登录 GitHub，把 Copilot 授权存入 harness 凭据记录，并注册基于 pi-ai Copilot 模型目录的 **github-copilot** LLM 路由——全部走官方 seam（`ctx.credentials` / `ctx.authorization` / `ctx.llm`），不依赖第三方 OAuth 基座。

> **状态：0.3.2** —— 已针对 dsh **0.2.1-rc.2** / pi-ai **1.3.0** 构建并验证（typecheck/build/tests 全绿，含一条复现 `modelErrors` 契约崩溃的组合级目录解析回归测试）。peer 范围 `>=0.1.5-rc.1 <0.3.0-0 || >=0.2.1-0 <0.3.0-0` 覆盖 0.1.x 与 0.2.x 两条宿主线 —— **含 `0.2.1-rc.1` / `0.2.1-rc.2`** —— 并拒绝整个 `0.3.0` 预发布线。`npm run verify:gate` 可复算该门禁，并额外断言 `0.3.0` 那条线始终被拒。

## 为什么不基于社区 dsh-oauth

当前 dsh-oauth（0.2.0）构建于 0.1.0-rc.6/rc.8 包线，其 peer 与出货版 dsh 0.1.x 线在 semver 上互斥（npm 拒绝同 profile 安装）。因此本插件直接对接官方 seam（凭据记录、授权 flow、LLM 适配器），dsh-llm-pi-ai 已注册的 Copilot flow 可直接复用。

## 安装

发布形态（无需 clone 与构建）：

```sh
dsh plugin --profile web add dsh-oauth-copilot
```

重启 `dsh web` 生效。插件的 `cordis.patch.yml`（`llm-github-copilot`）经 `dsh.bundle.patch` 自动加载；profile patch 中的 `authorization` 服务条目激活登录机制（见兼容性注记）。

开发形态（仓库检出）：

```sh
npm install && npm run build
dsh plugin --profile web add /path/to/dsh-oauth-copilot
```

## 使用 —— 人工 CLI 为主路径

认证是**纯人工终端操作**，全程不经过 LLM——非官方模型即使被提示注入，也无法发起或撤销授权。

```sh
# CLI 随 npm 包提供。可全局安装……
npm install -g dsh-oauth-copilot
dsh-copilot-auth login          # 打印设备码 URL 与代码；浏览器完成授权
# ……或直接用 npx 免安装运行：
npx -y dsh-oauth-copilot login

dsh-copilot-auth status         # grant 过期时间与可用模型
dsh-copilot-auth refresh        # 重新拉取账号可用模型清单并写入 grant
dsh-copilot-auth logout         # 移除本地 grant
```

仅支持 github.com（GitHub Enterprise 不支持，见"已知限制"）。命令绝不回显密钥；provider 错误先脱敏；凭据文件以 0600 原子写落盘。

## 模型选择器说明

选择器列出 pi-ai 的 advisory 目录（pi-ai 0.87.1 下 32 个 Copilot 模型），**并非每个模型在账号下都可用**：选择不可用模型会得到 HTTP 400 "model not supported"。`dsh-copilot-auth refresh` 会把账号可用清单（`policy=enabled`）写入 grant，此后选择器只显示可用项；刷新失败/为空时仍是全量 advisory 列表——重试 `refresh`，或按 `status` 输出里的模型选择。

## 可选：模型端工具（默认关闭）

模型可执行的工具（`github_copilot_login` / `github_copilot_status` / `github_copilot_logout`）**默认不注册**，仅在 `authorization` 服务已挂载 **且** 配置 `enableModelTools: true` 时出现。工具错误在进入模型上下文前一律脱敏。

```yaml
- id: llm-github-copilot
  config:
    enableModelTools: true       # 显式开启；默认 false
    loginWindowMs: 180000        # 登录工具等待授权的最长时间（毫秒）
    streamIdleTimeoutMs: 300000
```

## 已知限制

- **GitHub Enterprise 不支持。** 凭据白名单仅接受官方端点（`proxy.individual.githubcopilot.com`），模型登录工具与 CLI 均显式拒绝企业域。这是安全决策：伪造的 `enterpriseUrl` 会把 pi-ai 的自动 token 刷新指向攻击者服务器。
- **peer 范围横跨 0.1.x 与 0.2.x 两条宿主线。** 6 个 `@deepseek-ai/dsh-*` peer 为 `>=0.1.5-rc.1 <0.3.0-0 || >=0.2.1-0 <0.3.0-0`，pi-ai 依赖为 `>=0.85.1 <2.0.0`（pi-ai 在 dsh 0.2.1 里从 0.x 升到了 1.x）。宿主兼容门禁（`semver.satisfies(runtimeVersion, range, { includePrerelease: true })`）对**整包**判定：任一 `@deepseek-ai/dsh-*` peer 不在运行版本范围内，该 bundle 会被整体跳过，模型选择器里的 github-copilot 入口随之消失。`npm run verify:gate` 按已知宿主版本复算该门禁。`modelErrors` 成员是附加字段，不读取它的宿主会直接忽略。
- CLI 输出为中文（欢迎双语 PR）。
- 请勿在另一进程正在写凭据文件时执行 `dsh-copilot-auth login/logout`：CLI 使用 0600 原子写，但未持有 dsh 的跨进程写锁。

## FAQ

- **`dsh-copilot-auth: command not found`** —— npm 全局安装包（`npm install -g dsh-oauth-copilot`），或直接用 `npx -y dsh-oauth-copilot login`。
- **模型请求返回 HTTP 400 "model not supported"** —— 先运行 `dsh-copilot-auth refresh`，再按 `status` 输出选模型。
- **如何彻底撤销授权？** `dsh-copilot-auth logout` 移除本地 grant；GitHub → Settings → Applications 撤销 Copilot 授权。
- **Web UI 里登录按钮在哪？** 没有（dsh 的 Models 页不暴露 OAuth 入口）；CLI 即登录路径。
- **如何更新插件？** `npm install -g dsh-oauth-copilot@latest && dsh plugin --profile web add dsh-oauth-copilot`（或更新 profile package.json 版本），然后重启 dsh web。

## 安全姿态

已对照社区实现（dsh-oauth / dsh-oauth-openai）并完成多轮对抗性评审；既定决策：

- **凭据校验**（`src/credential-store.ts` 与 CLI）：存储载荷做 schema 校验；`proxy-ep` 端点**仅**接受官方主机，且任何非空 `enterpriseUrl` 都会拒绝记录——同时封堵流量重定向与 refresh token 外泄两条路径。
- **设备码流程**固定在 github.com：无 redirect_uri、无本地回调端口、scope 固定最小 `read:user`、每 key 单一授权尝试。
- **错误脱敏**：provider 错误先做 JWT/token 正则脱敏再截断展示。
- **模型端权限**：登录/注销工具默认关闭；CLI 路径完全不需要模型参与。
- 凭据 0600 存储，不进入 settings/describe/环境变量，CLI 绝不回显。

## 工作原理

| 部件 | 文件 | 职责 |
|---|---|---|
| 模型路由 | `src/adapter.ts` | 以 `PiAiAdapter` 注册 `github-copilot`；请求级 apiKey 留空，交由 pi-ai 原生 OAuth 认证 |
| 凭据桥 | `src/credential-store.ts` | 严格校验的 harness 记录 `llm-pi-ai/github-copilot`（kind `grant`）↔ pi-ai `CredentialStore` + 环境 `AuthContext` |
| 人工 CLI | `scripts/dsh-copilot-auth.mjs` | 纯人工设备码登录/状态/刷新/注销，写入同一凭据记录 |
| 登录工具 | `src/login-tool.ts` | 可选模型工具，驱动 `ctx.authorization` flow（仅显式开启时注册） |

## 开发

```sh
npm install
npm run typecheck
npm test          # 单元 + 组合 + 凭据校验
npm run build
npm run verify:gate   # 按已知 dsh 宿主版本复算插件兼容门禁
# scripts/ 只随仓库发布，不含在 npm 包内：请在仓库检出后运行
node scripts/smoke.mjs           # 在临时 DSH_HOME 中把构建产物挂到真实 seam 上冒烟
node scripts/smoke-noauth.mjs    # 验证无 authorization 服务时插件树照常启动
node scripts/verify-live.mjs     # 真实 grant 端到端探测（使用真实 grant，发送一个最小请求）
node scripts/verify-vision.mjs   # opencode-go 视觉路由探测（REASONING_EFFORT=low|high|max|off）
```

## 兼容性注记

**peer 范围与宿主门禁。** 只要有任何一个名为 `@deepseek-ai/dsh` 或以 `@deepseek-ai/dsh-` 开头的 peer 落在范围之外，dsh 就会**整包跳过**；判定用的是 `semver.satisfies(host, range, { includePrerelease: true })`。那个 flag 对**上界**尤其要紧：`0.3.0-rc.1` **小于** `0.3.0`，所以写成 `<0.3.0` 的范围会痛快地放行整条 `0.3.0` 预发布线 —— 一个从未测过的 dsh。因此这里的范围以 **`<0.3.0-0`** 结尾，只有它能真正排除那条线；`npm run verify:gate` 会双向断言（支持的两条线通过、`0.3.0` 那条被拒）。

**范围的下界有一个镜像陷阱。** `includePrerelease: true` 会放行落在范围内的任何预发布宿主；但**不带该 flag** 的普通 `semver.satisfies`（npm 与 pnpm 判定 peer 时用的正是它）只在该版本与某个比较符的 `major.minor.patch` 元组相同时才放行预发布版。而 dsh 至今发布的**每一个版本都是预发布版**，于是在 npm 眼里 `>=0.1.5-rc.1 <0.3.0-0` 等于「与任何版本都不兼容」，而 dsh 门禁却读成「0.1.5-rc.1 及以上全都兼容」。因此 6 个 peer 都追加了 **`|| >=0.2.1-0 <0.3.0-0`**，让两种判定在 0.2.1 这条线上一致。

模型路由始终激活；登录工具需要 `authorization` 服务挂载 + 显式开启。出货版 dsh-base profile 默认不挂该服务——profile patch 已补充挂载。服务缺失绝不能拖垮整个插件树。

## License

MIT