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 +8 -7
- package/README.en.md +30 -9
- package/README.md +33 -60
- package/cordis.patch.yml +1 -1
- package/lib/agy/keepalive.js +21 -0
- package/lib/doctor.js +22 -5
- package/lib/index.d.ts +11 -0
- package/lib/index.js +12 -1
- package/lib/provider/config.d.ts +1 -1
- package/lib/provider/config.js +3 -3
- package/lib/web/router.js +91 -3
- package/package.json +6 -3
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
|
-
|
|
26
|
-
|
|
27
|
-
This project
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
[](https://www.npmjs.com/package/dsh-agy-cli)
|
|
4
4
|
[](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/
|
|
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
|
-
|
|
8
|
+
**What this plugin does, in three points:**
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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.
|
|
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
|
[](https://www.npmjs.com/package/dsh-agy-cli)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
|
-
把本机已登录的 **Google Antigravity CLI(`agy`)** 接入 [DeepSeek Harness (DSH)](https://github.com/
|
|
6
|
+
把本机已登录的 **Google Antigravity CLI(`agy`)** 接入 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 作为一级模型提供方。
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
>
|
|
10
|
-
> **血统与出处**:源码主体来自 `dsh-agy-provider`(原作者 Darkings),设置页 UI 部分源自 [`dsh-agy-link`](https://github.com/amlyczz/dsh-agy-link) 的面板代码精简而来;接续期间新增的能力各自标注了借鉴出处(见下文「功能」节)。
|
|
8
|
+
**这个插件的三件事:**
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
## 凭据与网络行为(如实写清)
|
|
54
19
|
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- **会话管理 +
|
|
72
|
-
- **图片输入**(experimental):附件经图片桥落盘,由最小权限的 view_file Agent
|
|
73
|
-
- **配额面板与凭据保活**:loopback-only
|
|
74
|
-
-
|
|
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
|
-
|
|
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 全量 →
|
|
63
|
+
npm test # tsc 全量 → 320 例 node --test
|
|
91
64
|
```
|
|
92
65
|
|
|
93
66
|
## 许可
|
|
94
67
|
|
|
95
|
-
MIT
|
|
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.
|
|
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
|
package/lib/agy/keepalive.js
CHANGED
|
@@ -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
|
-
&&
|
|
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
|
-
|
|
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 };
|
package/lib/provider/config.d.ts
CHANGED
|
@@ -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-
|
|
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;
|
package/lib/provider/config.js
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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/
|
|
114
|
+
"url": "https://github.com/Gaudi-XIX/dsh-agy-cli/issues"
|
|
112
115
|
},
|
|
113
|
-
"homepage": "https://github.com/
|
|
116
|
+
"homepage": "https://github.com/Gaudi-XIX/dsh-agy-cli#readme"
|
|
114
117
|
}
|