dsh-agy-cli 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 dsh-agy-provider contributors
3
+ Copyright (c) 2026 Darkings and dsh-agy-provider contributors; dsh-agy-cli additions copyright (c) 2026 Gaudi-XIX
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -22,9 +22,10 @@ SOFTWARE.
22
22
 
23
23
  ---
24
24
 
25
- Continuation notice
26
- -------------------
27
- This project continues the dsh-agy-provider plugin originally created by Darkings
28
- (https://github.com/darkings/dsh-agy-provider, commits up to 2026-08-28) and
29
- maintained as a continuation by Gaudi-XIX since 2026-10. All original commit
30
- attribution is preserved in the git history under the same MIT license.
25
+ Upstream attribution
26
+ --------------------
27
+ This project contains code derived from the MIT-licensed plugin
28
+ https://github.com/darkings/dsh-agy-provider (commits up to 2026-08-28).
29
+ Copyright (c) 2026 Darkings and the dsh-agy-provider contributors, under the
30
+ same MIT terms reproduced above. The full commit history is preserved in this
31
+ repository's git log.
package/README.en.md CHANGED
@@ -3,16 +3,26 @@
3
3
  [![npm](https://img.shields.io/npm/v/dsh-agy-cli)](https://www.npmjs.com/package/dsh-agy-cli)
4
4
  [![license](https://img.shields.io/github/license/Gaudi-XIX/dsh-agy-cli)](LICENSE)
5
5
 
6
- Runs the locally authenticated **Google Antigravity CLI (`agy`)** as a first-class model provider for [DeepSeek Harness (DSH)](https://github.com/darkings/dsh). **All upstream traffic is signed by the official binary itself; tool execution, approvals and workspace sandbox stay 100% inside DSH.**
6
+ Runs the locally authenticated **Google Antigravity CLI (`agy`)** as a first-class model provider for [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness).
7
7
 
8
- > **Continuation notice**: this project continues [`darkings/dsh-agy-provider`](https://github.com/darkings/dsh-agy-provider) — the original author has been inactive since 2026-08-28. Maintained here since 2026-10-12 (DSH 0.2.0-rc.2 host adaptation, MCP tool bridge, a full-source security audit). The git history preserves every original attribution; MIT license unchanged. The model route id stays `agy`.
8
+ **What this plugin does, in three points:**
9
9
 
10
- For the full market-position survey (25 AGY-family plugins across 6 technical dimensions, the three provider routes, and honest comparisons against `dsh-agy-safe` and `dsh-antigravity-acp`), see [README.md](README.md) (Chinese). The short version:
10
+ 1. **Generation is signed by the official binary.** Every model request goes out through the official `agy.exe`; the plugin does not sign it, proxy it, or replace its generation path, so the model-side risk surface matches the official client.
11
+ 2. **Spends your Antigravity quota.** Requests draw on your own subscription under the same pool and rules as the official client. No API key.
12
+ 3. **AGY is only the model; DSH owns the harness.** Tool execution, approvals, sandbox and context budgeting stay 100% inside DSH; AGY's own agent loop is stripped.
11
13
 
12
- - **Dimension**: model-provider integration — the largest of the 6 dimensions (12 of 25 plugins).
13
- - **Our cell**: *official channel + DSH-owned harness*. The only cell of the three provider routes that is immune to how strong or weak the upstream agent's own harness is. Its CLI front now has exactly one maintained plugin — this one.
14
- - **Versus the OAuth-extraction majority (8 plugins)**: they gain freedom (account pools, image generation) at the cost of forged-client traffic and ban risk; we keep zero forged traffic.
15
- - **Versus `dsh-antigravity-acp`**: same harness ownership, different protocol — it speaks the official ACP standard through a separately downloaded server process (~52 s cold start), we speak the CLI's private stream-json through the light binary you already have (seconds to start). Our edge is lightness and the widest-compatible channel; theirs is a standards-grade contract and deeper feature polish. Both are honest choices of the same first principles: no token extraction, official-signed traffic, tools owned by DSH.
14
+ > **Platform: Windows only.** The quota panel and credential keep-alive depend on Windows Credential Manager. The other features have no cross-platform implementation and are untested on macOS/Linux.
15
+
16
+ ---
17
+
18
+ ## Credentials and network behaviour (stated plainly)
19
+
20
+ - **Reads**: the Google OAuth blob that AGY stores in Windows Credential Manager (target `gemini:antigravity`), via `CredRead` in `src/agy/keepalive.ts`. Used for two things: deciding whether the token is close to expiry and should trigger one official-CLI renewal, and reading `access_token` for the quota queries below. The token stays in process memory; it is never logged or placed in an error message.
21
+ - **Quota queries — the only outbound request the plugin makes itself**: the token above is sent as `Authorization: Bearer` to the read-only quota RPCs of `https://daily-cloudcode-pa.googleapis.com` and `https://cloudcode-pa.googleapis.com`, together with AGY client identity headers (`User-Agent` / `X-Goog-Api-Client`) — the server returns different quota groups per client, and omitting them drops the Gemini group. **Those requests go to those two Google endpoints only; nothing is sent to third parties.**
22
+ - **Not done**: no interception, replay or forgery of generation requests; no token handed to third parties; no credentials read beyond the target above.
23
+ - **Renewal**: token rotation is performed entirely by the official `agy.exe`; the plugin only launches it once before expiry.
24
+
25
+ Evidence for the endpoint semantics and identity-header behaviour: `docs/prod-generation-feasibility-20261020.md`.
16
26
 
17
27
  ## Install
18
28
 
@@ -20,8 +30,19 @@ For the full market-position survey (25 AGY-family plugins across 6 technical di
20
30
  dsh plugin --profile <your profile> add dsh-agy-cli
21
31
  ```
22
32
 
23
- Requires the official Antigravity CLI installed and signed in locally; Node ≥ 22. Migrating from `dsh-agy-provider` or `dsh-agy-safe`: the route id is unchanged (`agy`); the runtime data directory moved from `~/.dsh/agy-link/` to `~/.dsh/agy-cli/`.
33
+ Requires Windows, the official Antigravity CLI installed and signed in locally, and Node ≥ 22.
34
+
35
+ **One more step, or AGY will report an unknown agent** (the bundled agent presets are not copied into AGY's agent directory by the install alone):
36
+
37
+ ```console
38
+ npx dsh-agy-cli agents install read-only --apply
39
+ npx dsh-agy-cli agents install workspace-write --apply
40
+ ```
41
+
42
+ `--apply` writes into `~/.gemini/antigravity-cli/gemini-root-config/agents`; add `--backup` first if you want a copy, and `agents list` shows the current state. After a host restart the plugin's card appears under Settings → Models (model visibility, reasoning effort, quota panel).
43
+
44
+ **Diagnostics**: `npx dsh-agy-cli doctor --profile <profile>` — install, bundle declaration, agent presets, config parsing and credential state, without spending quota.
24
45
 
25
46
  ## License
26
47
 
27
- MIT. Original work copyright Darkings (darkings/dsh-agy-provider); this continuation preserves the full attribution history under the same license.
48
+ MIT. Source derived from Darkings' `dsh-agy-provider` (MIT); the original copyright and full commit history are preserved in `LICENSE` and in this repository's git history.
package/README.md CHANGED
@@ -3,76 +3,38 @@
3
3
  [![npm](https://img.shields.io/npm/v/dsh-agy-cli)](https://www.npmjs.com/package/dsh-agy-cli)
4
4
  [![license](https://img.shields.io/github/license/Gaudi-XIX/dsh-agy-cli)](LICENSE)
5
5
 
6
- 把本机已登录的 **Google Antigravity CLI(`agy`)** 接入 [DeepSeek Harness (DSH)](https://github.com/darkings/dsh) 作为一级模型提供方。**全部上游流量由官方二进制自己签名发起;工具执行、审批策略、工作区沙箱 100% 留在 DSH。**
6
+ 把本机已登录的 **Google Antigravity CLI(`agy`)** 接入 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 作为一级模型提供方。
7
7
 
8
- > **接续声明**:本项目是 [`darkings/dsh-agy-provider`](https://github.com/darkings/dsh-agy-provider) 的接续维护版——原作者自 2026-08-28 起停更,本仓库自 2026-10-12 起接续(适配 DSH 0.2.0-rc.2 宿主、MCP 工具桥、全源码安全审计),git 历史完整保留原作者全部署名,MIT 协议不变。模型路由 id 保持 `agy`,从原版迁移无需改配置。
9
- >
10
- > **血统与出处**:源码主体来自 `dsh-agy-provider`(原作者 Darkings),设置页 UI 部分源自 [`dsh-agy-link`](https://github.com/amlyczz/dsh-agy-link) 的面板代码精简而来;接续期间新增的能力各自标注了借鉴出处(见下文「功能」节)。
8
+ **这个插件的三件事:**
11
9
 
12
- ---
13
-
14
- ## 生态位置:一张 25 家插件的地图(调研结论)
15
-
16
- 写这个 README 之前,我们把这个市场(awesome-dsh-plugin,6,896 条)里全部 25 个 Antigravity 相关插件逐个查了一遍。先给你地形,再说我们在哪。
17
-
18
- ### 25 家分属 6 个技术维度
19
-
20
- | 维度 | 家数 | 干什么 |
21
- |---|---|---|
22
- | **模型 provider 接入** | **12** | 把 Antigravity 额度变成 DSH 里可选的模型路由 ← **我们在这里** |
23
- | 视觉/图像工具 | 4 | 借额度看图、画图 |
24
- | 子代理委派 | 2 | agy 当一次性编码子代理(工具面,不是模型路由) |
25
- | 会话数据 | 2 | 读 Antigravity 本地会话历史 |
26
- | 工作流外围 | 2 | 外部 agent 控制面等 |
27
- | 配额监控 | 1 | 多平台额度仪表盘 |
28
-
29
- ### 我们所在的维度里,12 家分三条路线
30
-
31
- 分界线只有两条本质问题:**凭据怎么碰、harness(工具执行/审批/上下文管理)归谁**。
32
-
33
- | 路线 | 家数 | 代表 | 代价 |
34
- |---|---|---|---|
35
- | **OAuth 凭据提取直连** | 8 | dsh-antigravity-oauth、dsh-subscriptions、dsh-antigravity-auth | 自由度最高(多账号池、图像、搜索),但伪造客户端直连私有 RPC——`dsh-antigravity-oauth` 的描述自己写着 "carries account-ban risk" |
36
- | **官方 CLI 委托(harness 归 CLI)** | 1 | dsh-agy-link(活跃) | 合规,但工具在 CLI 内部黑盒里跑,DSH 的审批/沙箱够不着 |
37
- | **官方通道 + DSH 主导 harness** | 3 | **本插件**、dsh-agy-safe(停更)、dsh-antigravity-acp(活跃,走 ACP 协议) | 合规 + DSH 治理兼得,是 12 家里唯一对「harness 强弱」免疫的子格 |
38
-
39
- ### 这条线现在的真实格局
40
-
41
- 同格三家,截至 2026-10-26:**dsh-agy-safe 停更于 09-08**(npm 0.1.8 后无活动),**上游原版停更于 08-28**;`dsh-antigravity-acp` 活跃但已岔去 **ACP server 进程线**(见下)。也就是说——**「官方 CLI 委托 + DSH 主导」这条 CLI 战线,目前只剩本仓库一家在维护**。原版与 agy-safe 的存量用户(两者合计月下载约千级)面对的是停更插件与新宿主(0.2.0-rc.2)不兼容的现实。
10
+ 1. **生成流量由官方二进制自己签名** —— 每一轮模型请求都通过官方 `agy.exe` 发出,插件不代它签名、不接管它的生成链路,模型侧的风控面与官方客户端一致。
11
+ 2. **吃到 Antigravity 账号的官方额度** —— 消耗打在你自己的套餐上,与官方客户端同池同规则,不需要 API Key。
12
+ 3. **AGY 只当模型,DSH 接管 harness** —— 工具执行、审批、沙箱、上下文预算 100% 在 DSH;AGY 自带的 agent 循环被剥离。
42
13
 
43
- ### 三条第一性原理,本插件逐条这样满足
14
+ > **平台限制**:**仅 Windows**。配额面板与凭据保活依赖 Windows Credential Manager;其余功能无跨平台实现,未在 macOS/Linux 验证。
44
15
 
45
- 1. **完全安全合规**:不抽取任何 token、不伪造任何网络请求——所有上游流量由官方 `agy.exe` 二进制自己签名。凭据只在本机内存中流经(保活/配额面板读凭据管理器 blob 仅用于判过期与身份),不落日志、不进错误消息。
46
- 2. **原样享受官方账户额度**:消耗打在你自己的 Antigravity 套餐上,与官方客户端同池同规则。是否有客户端差异化的计费政策,无读数不宣称。
47
- 3. **DSH 的 harness 替代 AGY 的 harness**:AGY CLI 被剥离工具执行权,降维为纯模型推理端——工具调用意图在管道层被拦截,执行权 100% 收归 DSH 的 ToolRuntime,服从 DSH 的沙箱三档(read-only / workspace-write / danger-full-access)与审批规则。
48
-
49
- ## 与两个邻居的诚实对比
50
-
51
- **vs `dsh-agy-safe`(同路线前辈,停更)**:同为 CLI 无头桥接 + 工具契约回传。它有一条我们敬佩的特色——历史指纹链(会话哈希检测上下文分叉后重放完整历史);我们的 resume 走 conversation 绑定 + mismatch 自动重放,思路不同目标相同。它停更后无法跟进 DSH 0.2.0-rc.2;本仓库接续了这个生态位并完成两轮全源码安全审计(原型污染防线、MCP 桥面闸门、面板加固——详见 CHANGELOG 0.11.0/0.12.0)。
16
+ ---
52
17
 
53
- **vs `dsh-antigravity-acp`(活跃,最强同格竞品)**:架构同构(工具执行都归 DSH),**协议分岔**是本质差异——
18
+ ## 凭据与网络行为(如实写清)
54
19
 
55
- | | 本插件(stream-json CLI 线) | dsh-antigravity-acp(ACP server 线) |
56
- |---|---|---|
57
- | 连接的进程 | 官方 `agy` CLI 本体(秒级冷启动) | 官方独立发布的 ACP server 进程(PyInstaller,冷启动实测约 52 秒,需预热设计) |
58
- | 协议地位 | 闭源二进制的私有行协议,靠实测固化,官方改一行就要跟 | 公开标准协议(Agent Client Protocol),有规范有版本 |
59
- | 工具边界的保证方式 | 协议不管工具,靠工程纪律事后收权(fail-closed 准入 + 桥面闸门 + 审计) | 协议天生剥夺——agent 只有「请求工具」的发言权,执行权写在规范里归 client |
60
- | 依赖 | 只需本机 CLI 装好登录 | Node ≥ 22.19 + 下载维护 ACP server 进程 |
20
+ 这一点容易被含糊带过,所以直接列出来:
61
21
 
62
- 诚实说:它的工程完成度目前是标杆(设置页、额度药丸、连接预热、图片输入)。我们的差异化立足点是**轻**(秒级起轮、零额外下载、只依赖 CLI 登录态)与 CLI print 模式这条兼容面更宽的通道。两条线是协议分岔不是替代关系;若官方未来把 CLI 模式冻结,迁移到 ACP 线只需换传输层,harness 设计不变。
22
+ - **读取**:Windows Credential Manager 里 AGY 写入的 Google OAuth blob(target `gemini:antigravity`),由 `src/agy/keepalive.ts` 的 `CredRead` 完成。用途有两个:判断令牌是否临近过期并触发一次官方 CLI 续期;读取 `access_token` 用于下面的配额查询。读到的令牌只在进程内使用,不落日志、不进错误消息。
23
+ - **配额查询(唯一的插件自带出站请求)**:把上面读到的 `access_token` 作为 `Authorization: Bearer` 发往 `https://daily-cloudcode-pa.googleapis.com` 与 `https://cloudcode-pa.googleapis.com` 的只读配额 RPC(`retrieveUserQuotaSummary` / `fetchAvailableModels`),并附带 AGY 客户端的身份头(`User-Agent` / `X-Goog-Api-Client`)——服务端对不同客户端返回的配额分组不同,不带身份头会漏掉 Gemini 组。**请求只发往 Google 这两个端点,不外传第三方。**
24
+ - **不做的**:不拦截、不重放、不伪造生成请求;不把令牌交给任何第三方;不读取除上述 target 以外的凭据。
25
+ - **续期**:令牌轮换完全由官方 `agy.exe` 自己完成,插件只负责在到期前拉起它一次。
63
26
 
64
- **差距清单(我们自己的短板,不藏着)**:对比 acp,我们没有对话旁的额度药丸、没有连接预热(worker 复用已有,冷启动预热未做)、图片走落盘桥而非真实像素传入;私有行协议存在上游漂移风险;工程面 320 例测试对它的 600+,数量不在一个量级(但我们的门禁把测试数锁进脚本,删一个用例会红)。**本轮已补**:历史指纹链(agy-safe 的特色,已按我们的结构实现并测试)。
27
+ 实测依据见 `docs/prod-generation-feasibility-20261020.md`(两个端点的配额语义、身份头对返回值的影响)。
65
28
 
66
- ## 功能(接续版增量)
29
+ ## 功能
67
30
 
68
31
  - **双工具路线**:MCP 桥把 DSH 工具交给 AGY(真工具事件、过程流可视化,推荐),失败自动降级为 prompt 契约
69
32
  - **DSH 主导 harness**:工作区、审批、沙箱、上下文预算全在 DSH;AGY 只出模型
70
33
  - **流式 + 状态机**:`stdinMode: hold` 防回滚、思考链折叠、尾流交付、断连保文本
71
- - **会话管理 + 历史指纹链**(指纹设计借自 dsh-agy-safe):resume / full 双模式,每轮成功后记录 DSH 历史指纹;resume 前本地比对前缀,检出历史被编辑/压缩/截断即废弃旧会话并从完整历史重建,delta prompt 不会落在过期上下文上;死亡会话自动重放,会话映射落盘可迁移
72
- - **图片输入**(experimental):附件经图片桥落盘,由最小权限的 view_file Agent 查看——机制与 acp 的「真实像素传入」不同,但通道已存在
73
- - **配额面板与凭据保活**:loopback-only;保活读凭据仅判过期,刷新由官方二进制自己完成
74
- - **安全加固(0.11.0/0.12.0)**:原型污染防线、MCP 桥面闸门、面板 loopback 门与原子写、凭据脱敏补漏——全部有测试背书
75
- - **路线图**(对比 acp 尚缺,按需补):对话旁的额度药丸(需宿主 UI slot,当前配额在设置面板)、连接预热(`transport: persistent` 的 worker 复用已覆盖进程重用,冷启动预热未做)
34
+ - **会话管理 + 历史指纹链**:resume / full 双模式,每轮成功后记录 DSH 历史指纹;resume 前本地比对前缀,检出历史被编辑/压缩/截断即废弃旧会话并从完整历史重建,delta prompt 不会落在过期上下文上;死亡会话自动重放,会话映射落盘可迁移
35
+ - **图片输入**(experimental):附件经图片桥落盘,由最小权限的 view_file Agent 查看
36
+ - **配额面板与凭据保活**:loopback-only;面板显示真实双窗口配额,令牌临近过期时自动拉起官方 CLI 续期
37
+ - **安全加固**:原型污染防线、MCP 桥面闸门、面板 loopback 门与原子写、凭据脱敏——全部有测试背书
76
38
 
77
39
  ## 安装
78
40
 
@@ -80,16 +42,27 @@
80
42
  dsh plugin --profile <你的 profile> add dsh-agy-cli
81
43
  ```
82
44
 
83
- 前置要求:本机已安装并登录官方 Antigravity CLI(`agy`);Node ≥ 22。迁移自 `dsh-agy-provider` 或 `dsh-agy-safe`:模型路由 id 保持 `agy`,无需改模型配置;运行数据目录由 `~/.dsh/agy-link/` 变更为 `~/.dsh/agy-cli/`(旧的 runtime-overrides.json 可手动搬移)。
45
+ 前置要求:Windows;本机已安装并登录官方 Antigravity CLI(`agy`);Node ≥ 22。
46
+
47
+ **装完必须再跑一步**(否则 AGY 会报 unknown agent——插件自带的 Agent 预设没有装进 AGY 的 agent 目录):
48
+
49
+ ```console
50
+ npx dsh-agy-cli agents install read-only --apply
51
+ npx dsh-agy-cli agents install workspace-write --apply
52
+ ```
53
+
54
+ `--apply` 会写入 `~/.gemini/antigravity-cli/gemini-root-config/agents`;`--backup` 可先备份,`agents list` 可查看当前状态。宿主重启后,设置页「模型」里会出现本插件的配置卡(模型可见性、推理强度、配额面板)。
55
+
56
+ **诊断**:`npx dsh-agy-cli doctor --profile <profile>` —— 覆盖安装、bundle 声明、Agent 预设、配置解析、凭据状态,全程不消耗额度。
84
57
 
85
58
  ## 开发与门禁
86
59
 
87
60
  ```bash
88
61
  npm ci --ignore-scripts
89
62
  npm run typecheck
90
- npm test # tsc 全量 → 317 例 node --test
63
+ npm test # tsc 全量 → 320 例 node --test
91
64
  ```
92
65
 
93
66
  ## 许可
94
67
 
95
- MIT。原作品版权归 Darkings(darkings/dsh-agy-provider)所有,本接续版在同等许可证下延续并保留完整署名历史。
68
+ MIT。源码衍生自 Darkings 的 `dsh-agy-provider`(MIT);原作者版权与完整提交历史保留于 `LICENSE` 与本仓 git 历史。
package/cordis.patch.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # Bundle entry is enabled by default when installed through dsh plugin add.
2
2
  # Install this package into a DSH profile with:
3
- # npx @deepseek-ai/dsh plugin --profile web add dsh-agy-cli@0.10.0
3
+ # npx @deepseek-ai/dsh plugin --profile web add dsh-agy-cli@0.12.1
4
4
  - insert:
5
5
  - id: dsh-agy-cli
6
6
  name: dsh-agy-cli
@@ -178,6 +178,27 @@ function blobIsFresh(now) {
178
178
  return cachedBlob !== undefined && now - cachedBlobAt < KEYRING_FRESH_MS;
179
179
  }
180
180
  async function readKeyringBlobUncached() {
181
+ // Credential seam, checked before the platform gate. Why: there is no Windows
182
+ // Credential Manager on Linux/macOS (and none on a CI runner), so every chain
183
+ // behind this read — the quota panel, its 4 daily-host tests — could only ever
184
+ // be green on the one dev machine that happened to hold a real login. That is
185
+ // a fake green, not a passing suite. `DSH_AGY_KEYRING_BLOB` feeds the exact
186
+ // base64 OAuth blob agy.exe stores through the identical decode path, so a
187
+ // test (or an operator without a keyring) can stand in for the credential.
188
+ // Unset => behavior unchanged. Set => the blob is trusted as if read from the
189
+ // store, on every platform: whoever can set this env can already read that
190
+ // store as the same user, so it grants nothing new.
191
+ const injected = process.env.DSH_AGY_KEYRING_BLOB?.trim();
192
+ if (injected !== undefined && injected.length > 0) {
193
+ try {
194
+ return decodeKeyringBlob(injected);
195
+ }
196
+ catch {
197
+ // A malformed blob is "no credential", never a crash and never a partial
198
+ // identity: fail closed to the same shape the unreadable keyring gives.
199
+ return { accessToken: null };
200
+ }
201
+ }
181
202
  if (process.platform !== 'win32')
182
203
  return { accessToken: null };
183
204
  return await new Promise((resolve) => {
package/lib/doctor.js CHANGED
@@ -6,7 +6,6 @@ import { createRequire } from 'node:module';
6
6
  import { redactText } from './agy/redact.js';
7
7
  import { getAgentPreset, listAgentPresets, readAgentPresetTemplate, } from './agent-presets.js';
8
8
  import { diagnoseProvider, } from './diagnostics.js';
9
- import { Config } from './provider/config.js';
10
9
  const require = createRequire(import.meta.url);
11
10
  function loadPackageMetadata() {
12
11
  try {
@@ -396,12 +395,13 @@ function effectiveFromDump(parsed, dumpStatus) {
396
395
  function parseAgentFrontmatter(source) {
397
396
  const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(source);
398
397
  if (match === null || match[1] === undefined) {
399
- return { name: null, tools: null, commandExecutionPolicy: null, mcpServersEmpty: null, subagent: null };
398
+ return { name: null, tools: null, commandExecutionPolicy: null, mcpServersEmpty: null, inheritMcp: null, subagent: null };
400
399
  }
401
400
  let name = null;
402
401
  let tools = null;
403
402
  let commandExecutionPolicy = null;
404
403
  let mcpServersEmpty = null;
404
+ let inheritMcp = null;
405
405
  let subagent = null;
406
406
  let collectingTools = false;
407
407
  for (const line of match[1].split(/\r?\n/)) {
@@ -424,10 +424,12 @@ function parseAgentFrontmatter(source) {
424
424
  commandExecutionPolicy = rawValue || null;
425
425
  if (field[1] === 'mcpServers')
426
426
  mcpServersEmpty = rawValue === '[]';
427
+ if (field[1] === 'inheritMcp')
428
+ inheritMcp = booleanValue(rawValue);
427
429
  if (field[1] === 'subagent')
428
430
  subagent = booleanValue(rawValue);
429
431
  }
430
- return { name, tools, commandExecutionPolicy, mcpServersEmpty, subagent };
432
+ return { name, tools, commandExecutionPolicy, mcpServersEmpty, inheritMcp, subagent };
431
433
  }
432
434
  function presetForAgent(agent, agentPreset) {
433
435
  return getAgentPreset(agentPreset) ?? listAgentPresets().find(preset => preset.agentName === agent);
@@ -456,10 +458,19 @@ async function inspectAgent(agent, agentPreset) {
456
458
  const actualTools = frontmatter.tools ?? [];
457
459
  const toolsMatch = actualTools.length === preset.tools.length
458
460
  && actualTools.every((tool, index) => tool === preset.tools[index]);
461
+ // The MCP half of the contract differs per preset: the four closed
462
+ // presets declare `mcpServers: []`, while the DSH bridge preset
463
+ // (`mcp-executor`) must inherit the host's `dsh` MCP server
464
+ // (`inheritMcp: true`, since 2b7988a) and carries no explicit list.
465
+ // Requiring `mcpServers: []` there made doctor flag its own default
466
+ // agent as invalid on every clean install.
467
+ const mcpContractSatisfied = preset.id === 'mcp-executor'
468
+ ? frontmatter.inheritMcp === true
469
+ : frontmatter.mcpServersEmpty === true;
459
470
  const valid = frontmatter.name === preset.agentName
460
471
  && toolsMatch
461
472
  && frontmatter.commandExecutionPolicy === 'off'
462
- && frontmatter.mcpServersEmpty === true
473
+ && mcpContractSatisfied
463
474
  && frontmatter.subagent === false;
464
475
  const capability = {
465
476
  knownPreset: true,
@@ -890,11 +901,17 @@ export async function runDoctorCli(argv, stdout = text => process.stdout.write(t
890
901
  dshBin = args[dshBinIdx + 1];
891
902
  }
892
903
  try {
904
+ // Do NOT pre-resolve the config here. `enabled` is a `.volatile()` field,
905
+ // so one pass through the schema replaces it with a host-style reference
906
+ // object (`{ get() }`); feeding that back through the same schema (which
907
+ // diagnoseProvider does for its own defaults) throws
908
+ // `$.enabled expected boolean but got [object Object]`, and the whole
909
+ // doctor command dies as DOCTOR_EXECUTION_FAILED. diagnoseProvider already
910
+ // parses `options.config ?? {}` exactly once on its own.
893
911
  const result = await runDoctor({
894
912
  ...(profile === undefined ? {} : { profile }),
895
913
  ...(dshHome === undefined ? {} : { dshHome }),
896
914
  ...(dshBin === undefined ? {} : { dshBin }),
897
- config: Config({}),
898
915
  });
899
916
  if (jsonOutput) {
900
917
  stdout(`${JSON.stringify(result, null, 2)}\n`);
package/lib/index.d.ts CHANGED
@@ -11,6 +11,17 @@ import { BundleConfig, Config as ConfigSchema, createConfigSchema, type Config a
11
11
  import type { ModelConfig, ToolPolicy } from './provider/config.js';
12
12
  import { MockAdapter } from './provider/mock.js';
13
13
  export declare const name = "dsh-agy-cli";
14
+ /**
15
+ * Hard-injected harness services. `workspaceRegistry` is deliberately NOT here:
16
+ * it is provided by the web/desktop layer only (@deepseek-ai/dsh-api-workspace-
17
+ * controller is mounted by dsh-web-app), so declaring it as a requirement makes
18
+ * this plugin stay `pending (waiting for service: workspaceRegistry)` on the
19
+ * headless profile - apply() would never run, no adapter would register, and
20
+ * every headless request would die with NO_ADAPTER. The runtime reads the
21
+ * registry through service(), which falls back to the root context, and fails
22
+ * closed with DSH_WORKSPACE_UNAVAILABLE when a tool request genuinely needs it
23
+ * and the host does not provide one.
24
+ */
14
25
  export declare const inject: string[];
15
26
  /** Programmatic library default: enabled=false, toolPolicy=reject. */
16
27
  export declare const Config: import("@deepseek-ai/schemastery").default<ConfigSchema>;
package/lib/index.js CHANGED
@@ -8,7 +8,18 @@ import { configuredModels, readConfigFlag } from './provider/config.js';
8
8
  import { SessionRegistry } from './session/store.js';
9
9
  import { FileSessionStore, resolveSessionStorePath } from './session/file-store.js';
10
10
  export const name = 'dsh-agy-cli';
11
- export const inject = ['llm', 'sessions', 'workspaceRegistry', 'sandboxPolicy', 'permissionPresets', 'approval'];
11
+ /**
12
+ * Hard-injected harness services. `workspaceRegistry` is deliberately NOT here:
13
+ * it is provided by the web/desktop layer only (@deepseek-ai/dsh-api-workspace-
14
+ * controller is mounted by dsh-web-app), so declaring it as a requirement makes
15
+ * this plugin stay `pending (waiting for service: workspaceRegistry)` on the
16
+ * headless profile - apply() would never run, no adapter would register, and
17
+ * every headless request would die with NO_ADAPTER. The runtime reads the
18
+ * registry through service(), which falls back to the root context, and fails
19
+ * closed with DSH_WORKSPACE_UNAVAILABLE when a tool request genuinely needs it
20
+ * and the host does not provide one.
21
+ */
22
+ export const inject = ['llm', 'sessions', 'sandboxPolicy', 'permissionPresets', 'approval'];
12
23
  /** Programmatic library default: enabled=false, toolPolicy=reject. */
13
24
  export const Config = ConfigSchema;
14
25
  export { AgyAdapter, MockAdapter };
@@ -68,7 +68,7 @@ export interface Config {
68
68
  /**
69
69
  * Where the durable `DSH sessionId -> AGY conversationId` map lives, so
70
70
  * `sessionMode: resume` survives a host restart. Empty uses
71
- * `<DSH_HOME>/agy-provider/sessions.json`. A value ending in `.json` is a
71
+ * `<DSH_HOME>/agy-cli/sessions.json`. A value ending in `.json` is a
72
72
  * file; anything else is a directory holding `sessions.json`.
73
73
  */
74
74
  sessionStorePath?: string;
@@ -88,7 +88,7 @@ export function createConfigSchema(defaults = {}) {
88
88
  idleTimeoutMs: z.number().min(1).max(3_600_000).default(300_000).description('Inactivity budget per request ms; re-armed on every output line. Must stay below timeoutMs').hidden(),
89
89
  stdinMode: z.union(['hold', 'close']).default('hold').description('Keep the request pipe open until AGY closes the turn (hold) or end it as soon as the frame is written (close)').hidden(),
90
90
  sessionMode: z.union(['resume', 'full']).default('full').description('Session mode'),
91
- sessionStorePath: z.string().default('').description('Durable sessionId to conversationId map path; empty uses DSH_HOME/agy-provider/sessions.json'),
91
+ sessionStorePath: z.string().default('').description('Durable sessionId to conversationId map path; empty uses DSH_HOME/agy-cli/sessions.json'),
92
92
  minimumAgyVersion: z.string().pattern(/^\d+\.\d+\.\d+$/).default('1.1.15').description('Minimum AGY version').hidden(),
93
93
  maxConcurrent: z.natural().min(1).max(64).default(4).description('Max concurrent AGY processes').hidden(),
94
94
  maxQueue: z.natural().max(256).default(32).description('Max queue length').hidden(),
@@ -148,7 +148,7 @@ export function createConfigSchema(defaults = {}) {
148
148
  idleTimeoutMs: { $description: '单次请求的无输出静默预算(毫秒);每有一行输出即重新计时' },
149
149
  stdinMode: { $description: '请求管道收尾方式:hold=等 AGY 自己结束本轮再关(默认,避免中途无输出被判死), close=写完即关' },
150
150
  sessionMode: { $description: '会话模式:full=每轮完整历史, resume=复用 AGY 会话' },
151
- sessionStorePath: { $description: '会话映射(DSH 会话→AGY 对话)的落盘路径;留空用 DSH_HOME/agy-provider/sessions.json,让 resume 模式在宿主重启后仍能续话' },
151
+ sessionStorePath: { $description: '会话映射(DSH 会话→AGY 对话)的落盘路径;留空用 DSH_HOME/agy-cli/sessions.json,让 resume 模式在宿主重启后仍能续话' },
152
152
  minimumAgyVersion: { $description: '最低允许的 AGY 版本' },
153
153
  maxConcurrent: { $description: '最大并发 AGY 进程数' },
154
154
  maxQueue: { $description: '最大排队长度' },
@@ -187,7 +187,7 @@ export function createConfigSchema(defaults = {}) {
187
187
  idleTimeoutMs: { $description: 'Per-request inactivity budget (ms); re-armed on every output line' },
188
188
  stdinMode: { $description: 'How the request pipe is retired: hold=wait for AGY to finish the turn (default), close=end right after the frame is written' },
189
189
  sessionMode: { $description: 'Session mode: full or resume' },
190
- sessionStorePath: { $description: 'Path of the durable DSH session to AGY conversation map; empty uses DSH_HOME/agy-provider/sessions.json so resume keeps working after a host restart' },
190
+ sessionStorePath: { $description: 'Path of the durable DSH session to AGY conversation map; empty uses DSH_HOME/agy-cli/sessions.json so resume keeps working after a host restart' },
191
191
  minimumAgyVersion: { $description: 'Minimum AGY version' },
192
192
  maxConcurrent: { $description: 'Max concurrent AGY processes' },
193
193
  maxQueue: { $description: 'Max queue length' },
package/lib/web/router.js CHANGED
@@ -9,6 +9,7 @@ import { configuredModels, normalizeModelId, readConfigFlag } from '../provider/
9
9
  import { isLoopback } from '../mcp/endpoint.js';
10
10
  import { readAgyRuntimeOverrides, saveAgyRuntimeOverrides } from '../agy/overrides.js';
11
11
  import { lastKeepaliveOutcome, readKeyringBlob, refreshAgyTokenIfDue } from '../agy/keepalive.js';
12
+ import { resolveAgyExecutable } from '../agy/process.js';
12
13
  const CORDIS_PATCH_PATH = join(homedir(), '.dsh', 'profiles', 'desktop', 'cordis.patch.yml');
13
14
  /**
14
15
  * Endpoint hosts for the Cloud Code Assist API. The split is not two accounts
@@ -139,11 +140,94 @@ async function readIdentityFromKeyring(options = {}) {
139
140
  async function readAccessTokenFromKeyring(options = {}) {
140
141
  return (await readIdentityFromKeyring(options)).accessToken;
141
142
  }
142
- function fetchHttpsThroughHttpProxy(targetUrlStr, accessToken, proxyUrl = 'http://127.0.0.1:12450') {
143
+ // Proxy resolution for the quota RPCs. Was: a hardcoded default of
144
+ // `http://127.0.0.1:12450` — the dev machine's tunnel. On any other host (CI,
145
+ // users without that exact proxy) the CONNECT failed silently behind a
146
+ // `.catch(() => null)`, so the quota panel showed nothing and the quota tests
147
+ // only passed where the proxy happened to be listening.
148
+ // Now: use the standard proxy environment (`HTTPS_PROXY`, checked case-first,
149
+ // then lowercase — Node's own env handling accepts both) when it is set, and a
150
+ // plain direct HTTPS request when it is not. Same behavior in production and
151
+ // in tests: no ambient-network dependency left in the fetch path.
152
+ // The panel shows which AGY binary it is talking to. `resolveAgyExecutable`
153
+ // walks PATH, so running it on every `/status` poll (the panel polls) would do
154
+ // a filesystem scan per request. The answer is stable for the life of the host
155
+ // process, so memoize the auto-discovered one; an explicit `agyPath` is read
156
+ // straight from config and stays reactive to settings changes.
157
+ let memoAgyBin;
158
+ function reportAgyBin(agyPath) {
159
+ const configured = agyPath?.trim();
160
+ if (configured)
161
+ return configured;
162
+ if (memoAgyBin === undefined)
163
+ memoAgyBin = resolveAgyExecutable();
164
+ return memoAgyBin;
165
+ }
166
+ function resolveQuotaProxyUrl() {
167
+ const raw = process.env.HTTPS_PROXY ?? process.env.https_proxy;
168
+ const trimmed = raw?.trim();
169
+ if (!trimmed)
170
+ return undefined;
171
+ try {
172
+ const url = new URL(trimmed);
173
+ // A CONNECT tunnel only makes sense for an http(s) proxy endpoint; socks://
174
+ // and friends need an agent we deliberately do not depend on.
175
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
176
+ return undefined;
177
+ return url.toString();
178
+ }
179
+ catch {
180
+ return undefined;
181
+ }
182
+ }
183
+ function fetchHttpsDirect(targetUrlStr, accessToken) {
184
+ return new Promise((resolve, reject) => {
185
+ try {
186
+ const targetUrl = new URL(targetUrlStr);
187
+ const body = JSON.stringify({});
188
+ const httpsReq = https.request({
189
+ host: targetUrl.hostname,
190
+ path: targetUrl.pathname + targetUrl.search,
191
+ method: 'POST',
192
+ headers: {
193
+ Authorization: `Bearer ${accessToken}`,
194
+ 'Content-Type': 'application/json',
195
+ ...AGY_IDENTITY_HEADERS,
196
+ 'Content-Length': Buffer.byteLength(body),
197
+ },
198
+ timeout: 10000,
199
+ }, (httpsRes) => {
200
+ const chunks = [];
201
+ httpsRes.on('data', (d) => chunks.push(d));
202
+ httpsRes.on('end', () => {
203
+ try {
204
+ resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
205
+ }
206
+ catch (e) {
207
+ reject(e);
208
+ }
209
+ });
210
+ });
211
+ httpsReq.on('timeout', () => {
212
+ httpsReq.destroy(new Error('HTTPS pa.googleapis.com timed out after 10000ms'));
213
+ });
214
+ httpsReq.on('error', (err) => reject(err));
215
+ httpsReq.write(body);
216
+ httpsReq.end();
217
+ }
218
+ catch (e) {
219
+ reject(e);
220
+ }
221
+ });
222
+ }
223
+ function fetchHttpsThroughHttpProxy(targetUrlStr, accessToken, proxyUrl) {
224
+ const effectiveProxy = proxyUrl ?? resolveQuotaProxyUrl();
225
+ if (!effectiveProxy)
226
+ return fetchHttpsDirect(targetUrlStr, accessToken);
143
227
  return new Promise((resolve, reject) => {
144
228
  try {
145
229
  const targetUrl = new URL(targetUrlStr);
146
- const proxy = new URL(proxyUrl);
230
+ const proxy = new URL(effectiveProxy);
147
231
  const connectReq = http.request({
148
232
  host: proxy.hostname,
149
233
  port: proxy.port,
@@ -745,7 +829,11 @@ export function setupWebRoutes(ctx, activeConfig, modelDiscovery) {
745
829
  : '本机主账号 (gemini:antigravity)';
746
830
  sendJson(res, 200, {
747
831
  plugin: 'dsh-agy-cli',
748
- bin: activeConfig.agyPath || 'F:\\AgentData\\antigravity\\cli\\agy.exe',
832
+ // Never fall back to a developer's absolute path: this value ships to
833
+ // every panel and used to leak `F:\AgentData\antigravity\cli\agy.exe`
834
+ // into every user's settings page. Empty agyPath means "auto-discover",
835
+ // so report what the spawn path actually resolves to.
836
+ bin: reportAgyBin(activeConfig.agyPath),
749
837
  version: CLOUD_CODE_API_VERSION,
750
838
  enabled: readConfigFlag(activeConfig.enabled),
751
839
  permissionMode: 'dsh-owned',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agy-cli",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -73,6 +73,9 @@
73
73
  "engines": {
74
74
  "node": ">=20"
75
75
  },
76
+ "dependencies": {
77
+ "@deepseek-ai/schemastery": "^3.18.4"
78
+ },
76
79
  "devDependencies": {
77
80
  "@deepseek-ai/cordis": "^4.0.4",
78
81
  "@deepseek-ai/dsh-attachment": "^0.2.0-rc.2",
@@ -108,7 +111,7 @@
108
111
  ],
109
112
  "author": "",
110
113
  "bugs": {
111
- "url": "https://github.com/darkings/dsh-agy-cli/issues"
114
+ "url": "https://github.com/Gaudi-XIX/dsh-agy-cli/issues"
112
115
  },
113
- "homepage": "https://github.com/darkings/dsh-agy-cli#readme"
116
+ "homepage": "https://github.com/Gaudi-XIX/dsh-agy-cli#readme"
114
117
  }