dsh-codex-connect 0.1.0-alpha.4.10
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/INSTALL.md +89 -0
- package/LICENSE +202 -0
- package/MIGRATION.md +12 -0
- package/NOTICE +15 -0
- package/README.i18n.yaml +6 -0
- package/README.md +193 -0
- package/compatibility.json +28 -0
- package/cordis.patch.yml +8 -0
- package/docs/README.zh.md +193 -0
- package/docs/assets/en/hero.jpg +0 -0
- package/docs/assets/en/model-selector.jpg +0 -0
- package/docs/assets/en/oauth-status.jpg +0 -0
- package/docs/assets/en/plugin-configuration.jpg +0 -0
- package/docs/assets/en/plugin-entry.jpg +0 -0
- package/docs/assets/hero.jpg +0 -0
- package/docs/assets/model-selector.jpg +0 -0
- package/docs/assets/oauth-status.jpg +0 -0
- package/docs/assets/plugin-configuration.jpg +0 -0
- package/docs/assets/zh/hero.jpg +0 -0
- package/docs/assets/zh/model-selector.jpg +0 -0
- package/docs/assets/zh/oauth-status.jpg +0 -0
- package/docs/assets/zh/plugin-configuration.jpg +0 -0
- package/docs/assets/zh/plugin-entry.jpg +0 -0
- package/docs/design.i18n.yaml +4 -0
- package/docs/design.md +29 -0
- package/docs/design.zh.md +25 -0
- package/lib/bin.d.ts +6 -0
- package/lib/bin.js +230 -0
- package/lib/client.js +1642 -0
- package/lib/index.d.ts +443 -0
- package/lib/index.js +2 -0
- package/lib/invariant.d.ts +14 -0
- package/lib/invariant.js +15 -0
- package/lib/src-DLX8Rs0k.js +2080 -0
- package/package.json +155 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Codex Connect
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-codex-connect)
|
|
4
|
+
|
|
5
|
+
[English](../README.md) | 中文
|
|
6
|
+
|
|
7
|
+
通过 OAuth 将你的 ChatGPT 订阅连接到 DeepSeek Harness,同时保留用户自主默认项、Harness 原生审批、非敏感诊断和可靠的会话恢复。
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="https://raw.githubusercontent.com/franksong2702/dsh-codex-connect/main/docs/assets/zh/hero.jpg" alt="Codex Connect — 通过 ChatGPT OAuth 连接 DeepSeek Harness" width="100%">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
`dsh-codex-connect` 提供 `openai-codex` 模型目录和独立的 ChatGPT OAuth 登录。模型仍走 Harness 标准 LLM 服务,因此流式输出、工具调用、reasoning replay、压缩、文件系统控制、权限门禁和审批提示仍由 Harness 负责。ChatGPT 订阅不会因此变成 OpenAI Platform API 凭据。
|
|
14
|
+
|
|
15
|
+
安装是增量的:bundle 不会替换当前主模型或搜索路由;独立搜索提供方和 `view_image` 工具也默认关闭,必须显式开启。
|
|
16
|
+
|
|
17
|
+
本中文指南中的 UI 截图均来自中文本地化的 Harness 界面;[English 版](../README.md) 使用同一状态的英文截图。模型与提供方标识保留其规范拼写,不随界面语言翻译。
|
|
18
|
+
|
|
19
|
+
## 五分钟快速开始
|
|
20
|
+
|
|
21
|
+
本指南使用 `web` profile。请把 `web` 替换成你已经在用的 Harness profile 名称。你需要先有可用的 `dsh` 安装;如果在 DeepSeek Harness 源码 checkout 中运行,请在命令前加 `pnpm`。
|
|
22
|
+
|
|
23
|
+
### 1. 将插件装入一个 profile
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
dsh plugin --profile web add dsh-codex-connect@alpha
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
预期结果:包被加入该 profile。这个动作不会更改 profile 的默认模型或全局搜索路由。
|
|
30
|
+
|
|
31
|
+
如需精确复现这个版本,使用 `dsh plugin --profile web add dsh-codex-connect@0.1.0-alpha.4.10`。对应 GitHub prerelease 已创建但 npm 不可用时,可使用 `dsh plugin --profile web add 'github:franksong2702/dsh-codex-connect#v0.1.0-alpha.4.10'`。本地 checkout 可安装为 `link:/absolute/path/to/dsh-codex-connect`。
|
|
32
|
+
|
|
33
|
+
### 2. 启动 Harness
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
dsh web
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
预期结果:所选 profile 的 Harness Web UI 打开。
|
|
40
|
+
|
|
41
|
+
### 3. 找到 Codex Connect 卡片
|
|
42
|
+
|
|
43
|
+
打开 **设置 → 插件 → 插件配置 → Codex Connect**。
|
|
44
|
+
|
|
45
|
+
预期结果:新安装时账户区显示 **尚未登录**,并出现 **使用 ChatGPT 登录** 按钮。之后管理可选能力也在同一张卡片中完成。
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<img src="https://raw.githubusercontent.com/franksong2702/dsh-codex-connect/main/docs/assets/zh/plugin-entry.jpg" alt="Harness 插件配置中的中文 Codex Connect 折叠入口" width="720">
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
### 4. 使用 ChatGPT 登录
|
|
52
|
+
|
|
53
|
+
点击 **使用 ChatGPT 登录**,并自行完成浏览器审批。不要把授权 URL、授权码、token 或账户标识复制到 Issue、日志或配置文件中。
|
|
54
|
+
|
|
55
|
+
预期结果:账户区变为 **已登录**。下图展示的是完成本步骤后的成功状态,不是开始登录前的页面。
|
|
56
|
+
|
|
57
|
+
<p align="center">
|
|
58
|
+
<img src="https://raw.githubusercontent.com/franksong2702/dsh-codex-connect/main/docs/assets/zh/oauth-status.jpg" alt="Harness 插件配置中的中文 Codex Connect 已登录状态" width="720">
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
### 5. 选择模型并做一次安全检查
|
|
62
|
+
|
|
63
|
+
打开 Harness 原生模型选择器,为当前正在使用的 agent 或会话选择一个 `openai-codex` 模型。这个选择与写入 profile 的默认模型或全局搜索路由是两件事。
|
|
64
|
+
|
|
65
|
+
选择器会把可用项归在 **OpenAI Codex** 下。`GPT-5.6 Luna` 一类模型标识是规范名称,因此会保留原样,不翻译。
|
|
66
|
+
|
|
67
|
+
<p align="center">
|
|
68
|
+
<img src="https://raw.githubusercontent.com/franksong2702/dsh-codex-connect/main/docs/assets/zh/model-selector.jpg" alt="中文 DeepSeek Harness 模型选择器中的 OpenAI Codex 模型分组" width="360">
|
|
69
|
+
</p>
|
|
70
|
+
|
|
71
|
+
如需在本机确认插件配置行,运行:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
dsh --profile web --dump-config
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
预期结果:配置中恰好有一条 `llm-openai-codex`。请只在本机查看这份配置输出,它可能包含无关的 profile 设置。
|
|
78
|
+
|
|
79
|
+
如需不启动 OAuth 的非敏感状态和诊断输出,运行:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
dsh plugin --profile web exec dsh-codex-connect status --json
|
|
83
|
+
dsh plugin --profile web exec dsh-codex-connect doctor --json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
预期结果:`status --json` 报告 `signed-in` 并以 `0` 退出,`doctor --json` 只输出一条非敏感 JSON。尚未登录时 `status --json` 会以 `1` 退出;回到第 4 步登录即可,不要把它当作插件故障。
|
|
87
|
+
|
|
88
|
+
## 可选能力(默认关闭)
|
|
89
|
+
|
|
90
|
+
安装后的 bundle 只注册模型提供方,默认不额外启用任何能力:
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
- id: llm-openai-codex
|
|
94
|
+
config:
|
|
95
|
+
enableSearch: false
|
|
96
|
+
enableImageTool: false
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
打开 **设置 → 插件 → 插件配置 → Codex Connect**,即可在同一张卡片中管理账户和这些选项。**保存更改**只影响本插件的能力配置并即时生效,绝不会选择默认模型或全局搜索路由。
|
|
100
|
+
|
|
101
|
+
### 只开启你准备使用的能力
|
|
102
|
+
|
|
103
|
+
- `enableSearch: true` 会把 Codex 注册为可选择的搜索提供方,不会把它选为 profile 的全局搜索路由。
|
|
104
|
+
- `enableImageTool: true` 会为具备视觉能力的模型启用 `view_image`,用于审批后的本地读取和公网图片获取。
|
|
105
|
+
|
|
106
|
+
下图是有人显式开启能力之后的配置示例,不是新安装的默认状态。本中文指南使用中文本地化截图;English 版展示同一状态的英文截图。
|
|
107
|
+
|
|
108
|
+
<p align="center">
|
|
109
|
+
<img src="https://raw.githubusercontent.com/franksong2702/dsh-codex-connect/main/docs/assets/zh/plugin-configuration.jpg" alt="中文 Codex Connect 显式开启可选能力后的配置示例" width="720">
|
|
110
|
+
</p>
|
|
111
|
+
|
|
112
|
+
### 单独更改默认模型或全局搜索路由
|
|
113
|
+
|
|
114
|
+
如需把 Codex 模型设为新 agent 的默认模型,需要自行添加或修改独立的 Harness 配置项:
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
- id: agent-default-model
|
|
118
|
+
config:
|
|
119
|
+
provider: openai-codex
|
|
120
|
+
model: gpt-5.6-sol
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
如需把 Codex 选为 profile 的全局搜索路由,还需要另做一次显式配置:
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
- id: llm-openai-codex
|
|
127
|
+
config:
|
|
128
|
+
enableSearch: true
|
|
129
|
+
searchMode: live
|
|
130
|
+
searchContextSize: medium
|
|
131
|
+
|
|
132
|
+
- id: web
|
|
133
|
+
config:
|
|
134
|
+
searchProvider: openai-codex
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| 字段 | 默认值 | 可选值 |
|
|
138
|
+
|---|---:|---|
|
|
139
|
+
| `enableSearch` | `false` | boolean |
|
|
140
|
+
| `enableImageTool` | `false` | boolean |
|
|
141
|
+
| `searchModel` | `gpt-5.6-sol` | Codex model id |
|
|
142
|
+
| `searchMode` | `cached` | `cached`、`indexed`、`live` |
|
|
143
|
+
| `searchContextSize` | `medium` | `low`、`medium`、`high` |
|
|
144
|
+
| `searchMaxOutputTokens` | `10000` | 正整数 |
|
|
145
|
+
|
|
146
|
+
## 重新登录、诊断与冲突
|
|
147
|
+
|
|
148
|
+
- 卡片显示 **重新登录**,或服务端要求重新认证时,点击该操作并完成同一套安全的浏览器流程。它会保留本插件的能力配置,不会偷偷改动默认模型或全局搜索路由。不要为了刷新会话而运行 `logout`。
|
|
149
|
+
- `doctor` 只读取进程与文件系统元数据。`doctor --json` 只输出一条可解析的非敏感 JSON,包含 schema version 1、包/版本/Node 信息、认证文件状态与安全 mode、能力、冲突状态和提示;它省略认证文件绝对路径以及 OAuth、账户和过期时间信息。
|
|
150
|
+
- `status --json` 只输出 signed-in 或 signed-out 状态及包元数据。它只为判断登录态读取认证文件,但不会输出认证文件内容或启动 OAuth。
|
|
151
|
+
- OAuth 单独存储于 `$DSH_HOME/.openai-codex-auth.json`(默认 `~/.dsh`)。`~/.codex/auth.json` 不会被复制或修改。支持的平台上,父目录与文件使用仅所有者可访问权限;写入采用原子替换,刷新写入使用跨进程文件锁。
|
|
152
|
+
- 默认情况下,OAuth 路由只接受 loopback 浏览器请求。当 DSH 在一台设备运行,而你从可信网络中的另一台设备打开 DSH 时,请在运行 DSH 的设备上显式批准浏览器地址栏中的 origin:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
dsh plugin --profile web exec dsh-codex-connect trust-origin http://192.168.1.20:3080
|
|
156
|
+
dsh plugin --profile web exec dsh-codex-connect trusted-origins
|
|
157
|
+
dsh plugin --profile web exec dsh-codex-connect untrust-origin http://192.168.1.20:3080
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
将示例替换为浏览器地址栏中的精确 origin,包括协议和端口;不要填写当前访问设备的 IP、裸主机、路径、query 或 fragment。只在你信任的网络中使用,不要把该路由暴露到公网;不适合显式信任网络时,请使用 SSH tunnel。浏览器页面只会显示并复制这条命令,不会自行修改授权列表。
|
|
161
|
+
- 启动报告 `openai-codex` 冲突时,旧 `dsh-codex` bundle 或手动 provider 配置可能已占用该 adapter。先检查有效配置,只移除已确认的冲突所有者。不要删除认证文件或无关 provider。
|
|
162
|
+
- 移除包不会删除 OAuth 状态;只有确实需要删除凭据时才运行 `logout`。
|
|
163
|
+
|
|
164
|
+
## 兼容性与安全边界
|
|
165
|
+
|
|
166
|
+
- 当前唯一已验证的兼容组合是 DSH 插件 API packages `0.1.0-rc.7`、`@earendil-works/pi-ai` `0.82.1` 和 Node.js `^22.19.0 || >=24.0.0`;详见 [compatibility.json](../compatibility.json)。Alpha 4.10 使用 rc.7 的 keyed 插件配置 slot;DSH rc.6 用户应继续使用 Alpha 4.9。
|
|
167
|
+
- 升级时请将 DSH 插件 API packages 与 `@earendil-works/pi-ai` 作为一组升级,再运行 `dsh-codex-connect doctor --json` 和兼容性检查。本契约不对未来版本作判断。
|
|
168
|
+
- ChatGPT 套餐资格、模型权限、额度和后端行为由 OpenAI 控制,可能变化。
|
|
169
|
+
- Codex 端点不会强制普通 Responses 的 `max_output_tokens` 字段。Harness 压缩仍可工作,但这个摘要上限不能由服务端在该路由上强制。
|
|
170
|
+
- shell、文件系统、skills、MCP、subagents、审批、权限、附件、会话持久化、压缩与恢复继续由当前 Harness profile 提供。
|
|
171
|
+
- 远程 `view_image` 只允许公共 HTTP(S) 目标;每一次 DNS 结果与重定向都会重新检查,并将连接固定到已验证地址,从而阻止 localhost、私网、link-local 服务和云元数据地址。
|
|
172
|
+
- 安装、构建、测试、doctor 和包内容验证均不需要真实 OAuth。
|
|
173
|
+
|
|
174
|
+
详见 [安装运行手册](../INSTALL.md)、[Alpha 发布清单](../RELEASING.md)、[MIGRATION.md](../MIGRATION.md) 与 [架构说明](design.zh.md)。
|
|
175
|
+
|
|
176
|
+
## 开发
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
pnpm install --frozen-lockfile
|
|
180
|
+
pnpm run check
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## 发布
|
|
184
|
+
|
|
185
|
+
维护者通过[手动 OIDC 发布 workflow](../.github/workflows/release.yml)发布 Alpha;`latest` 的独立短期提升步骤见 [Alpha 发布清单](../RELEASING.md)。
|
|
186
|
+
|
|
187
|
+
## 法律与致谢
|
|
188
|
+
|
|
189
|
+
Codex Connect 的修改与新增工作 Copyright 2026 Frank Song。本项目包含派生自 [Yan-Zero/dsh-codex](https://github.com/Yan-Zero/dsh-codex) 的软件;上游内容继续保留 Copyright 2026 Yan-Zero。两部分均按 Apache-2.0 发布,详情见 [NOTICE](../NOTICE)。本项目与 OpenAI、ChatGPT、Codex、DeepSeek 或 DeepSeek Harness 不存在隶属关系,也未获得其背书。
|
|
190
|
+
|
|
191
|
+
## 许可证
|
|
192
|
+
|
|
193
|
+
Apache-2.0
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/design.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Codex Connect: Alpha design
|
|
2
|
+
|
|
3
|
+
## Ownership and composition
|
|
4
|
+
|
|
5
|
+
The package registers `openai-codex` through Harness's public `LlmRuntime` and `PiAiAdapter` surfaces. The main model path is not a one-shot subagent: it remains the normal Harness agent loop, preserving native tool approvals, permission policy, streaming, attachment resolution, reasoning replay, session persistence, compaction, and recovery.
|
|
6
|
+
|
|
7
|
+
The bundle patch inserts only `llm-openai-codex`. It never writes `agent-default-model` or `web.searchProvider`. `enableSearch` and `enableImageTool` are capability gates defaulting to `false`; the optional service injections are not registered while disabled.
|
|
8
|
+
|
|
9
|
+
The Host registers `llm-openai-codex` as the plugin-owned settings namespace and declares `OpenAI Codex` in the LLM configurable-provider directory. The browser binds that namespace through Harness's settings-scope transport and renders account, quota, Save/Discard capability controls in the existing Plugin configuration card. Revision-fenced field writes preserve unrelated settings. Committed changes reconcile search and image registrations live; the default-model and global-search namespaces are never written.
|
|
10
|
+
|
|
11
|
+
## OAuth persistence
|
|
12
|
+
|
|
13
|
+
The plugin uses `$DSH_HOME/.openai-codex-auth.json`, separate from Codex CLI/Desktop state. The file format is strict and versioned. POSIX reads reject group/world-accessible files. Parent directories and files are created with owner-only modes, writes are atomic, and refresh mutations use the Harness cross-process file lock. Callers receive cloned credentials. Browser-origin trust is a separate `$DSH_HOME/.openai-codex-trusted-origins.json` sidecar with `version: 1`, `mode: "allowlist"`, and exact normalized HTTP(S) origins; it never contains OAuth material and is changed only by the standalone CLI.
|
|
14
|
+
|
|
15
|
+
The settings routes and CLI reuse the existing OAuth path and route names for migration compatibility. Only an explicit login operation emits an authorization URL or code. Browser requests are accepted by default only from loopback; a remote request must use an exact effective HTTP(S) origin in the current sidecar, must not carry cross-site Fetch Metadata, and must match any supplied Origin exactly. The sidecar is re-read for every request, and unknown fields or modes fail closed. A login challenge accepts only credential-free HTTPS URLs and fails closed after 30 seconds or when the provider finishes without a URL; logout and disposal cancel pending waiters. Status responses are redacted. Doctor uses `lstat` metadata and never opens the document.
|
|
16
|
+
|
|
17
|
+
## Search and images
|
|
18
|
+
|
|
19
|
+
When `enableSearch: true`, the plugin registers its standalone search provider and secret-free request event. Harness still requires explicit `web.searchProvider: openai-codex` when multiple providers exist. Search responses are mapped to Harness text and citation records.
|
|
20
|
+
|
|
21
|
+
When `enableImageTool: true`, `view_image` is registered only after tools, filesystem, and attachment services are available. Local files remain bounded by the Harness filesystem surface. Remote images allow only credential-free public HTTP(S): all DNS answers must be public unicast, each redirect is revalidated, and each socket is pinned to the validated address to close DNS-rebinding gaps. The tool also checks bounded bytes, accepted media signatures, and current-model image support before saving a Harness attachment.
|
|
22
|
+
|
|
23
|
+
## Conflicts and diagnostics
|
|
24
|
+
|
|
25
|
+
Before registration the plugin checks current provider ids. An existing `openai-codex` adapter produces a focused message naming the likely legacy-bundle or manual-provider cause. The boot-free CLI `doctor` reports package/runtime version, OAuth path metadata, capability defaults, and safe conflict guidance without returning auth content.
|
|
26
|
+
|
|
27
|
+
## Compatibility boundary
|
|
28
|
+
|
|
29
|
+
The Alpha pins Harness `0.1.0-rc.7` development dependencies and uses its keyed `settings.plugin.item` contract; supported Node.js is `^22.19.0 || >=24.0.0`. It pins `@earendil-works/pi-ai` `0.82.1`. Backend eligibility, quotas, models, and protocol details remain controlled upstream. Tests use temporary OAuth documents and mocked network responses; CI does not perform real authentication.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Codex Connect:Alpha 设计
|
|
2
|
+
|
|
3
|
+
## 所有权与组合
|
|
4
|
+
|
|
5
|
+
本包通过 Harness 公共 `LlmRuntime` 与 `PiAiAdapter` 注册 `openai-codex`。主模型路径不是一次性 subagent,而是标准 Harness agent loop,因此原生工具审批、权限策略、流式输出、附件解析、reasoning replay、会话持久化、压缩与恢复均保持有效。
|
|
6
|
+
|
|
7
|
+
bundle patch 只插入 `llm-openai-codex`,不会写入 `agent-default-model` 或 `web.searchProvider`。`enableSearch` 与 `enableImageTool` 默认均为 `false`;关闭时不会注册对应可选服务。
|
|
8
|
+
|
|
9
|
+
Host 将 `llm-openai-codex` 注册为插件自有 settings namespace,并在 LLM 可配置 provider 目录中声明显示名为 `OpenAI Codex`。浏览器通过 Harness settings-scope transport 绑定该 namespace,把账户、额度以及带保存/放弃的能力配置放在现有“插件配置”卡片中。带 revision 防护的逐字段写入不会覆盖无关设置;提交后会即时协调搜索与图片能力的注册状态,且绝不写入默认模型或全局搜索 namespace。
|
|
10
|
+
|
|
11
|
+
## OAuth 持久化
|
|
12
|
+
|
|
13
|
+
插件使用 `$DSH_HOME/.openai-codex-auth.json`,与 Codex CLI/Desktop 状态分离。文件格式严格且有版本号;POSIX 上会拒绝组/其他用户可读文件。父目录和文件按仅所有者权限创建,写入采用原子替换,刷新修改使用 Harness 跨进程文件锁,返回给调用方的是凭据副本。浏览器 origin 授权单独存放于 `$DSH_HOME/.openai-codex-trusted-origins.json`,格式为 `version: 1`、`mode: "allowlist"` 和规范化的精确 HTTP(S) origin;其中不含 OAuth 内容,且只能通过独立 CLI 修改。
|
|
14
|
+
|
|
15
|
+
为兼容迁移,设置页路由、OAuth 路径和 provider id 不改名。浏览器请求默认只允许 loopback;远程请求必须使用当前 sidecar 中的精确有效 HTTP(S) origin,不能带 cross-site Fetch Metadata,若带 Origin 还必须精确匹配。每次请求都会重新读取 sidecar;未知字段或错误 mode 会快速失败。登录挑战只接受不含凭据的 HTTPS 地址;30 秒内未得到地址、provider 已结束但没有地址、退出登录或插件卸载时,所有 waiter 都会被清理。只有显式登录会输出授权 URL 或代码;状态输出会脱敏。doctor 只用 `lstat` 检查元数据,不打开文件。
|
|
16
|
+
|
|
17
|
+
## 搜索与图片
|
|
18
|
+
|
|
19
|
+
仅当 `enableSearch: true` 时注册 Codex 独立搜索提供方和不含凭据的请求事件。多 provider 环境仍需显式设置 `web.searchProvider: openai-codex`。仅当 `enableImageTool: true` 且 tools、filesystem、attachments 服务存在时注册 `view_image`。本地文件继续受 Harness 文件系统边界与大小限制;远程图片只允许不含凭据的公共 HTTP(S),所有 DNS 结果必须是公共单播地址,每次重定向都会重新验证,并把实际连接固定到已验证地址以关闭 DNS rebinding 缺口。
|
|
20
|
+
|
|
21
|
+
## 冲突、诊断与兼容边界
|
|
22
|
+
|
|
23
|
+
注册前检查现有 provider id;发现 `openai-codex` 已被占用时,给出旧 bundle 或手动 provider 配置的定向迁移提示。boot-free CLI doctor 只报告包/运行时版本、OAuth 路径元数据、能力默认值和安全提示。
|
|
24
|
+
|
|
25
|
+
Alpha 固定使用 Harness `0.1.0-rc.7` 开发依赖,并采用其 keyed `settings.plugin.item` 契约;Node.js 支持 `^22.19.0 || >=24.0.0`。`@earendil-works/pi-ai` 固定为 `0.82.1`。资格、额度、模型和后端协议仍由上游控制。测试仅使用临时 OAuth 文档和模拟网络响应,CI 不执行真实认证。
|
package/lib/bin.d.ts
ADDED
package/lib/bin.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { E as CODEX_CONNECT_VERSION, J as loginOpenAICodex, U as OpenAICodexTrustedOriginsStore, W as normalizeTrustedOrigin, X as openAICodexAuthStatus, Y as logoutOpenAICodex, et as openAICodexAuthPath, w as diagnoseOpenAICodex } from "./src-DLX8Rs0k.js";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { realpathSync } from "node:fs";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
//#region src/bin.ts
|
|
8
|
+
/** Standalone credential CLI for the optional OpenAI Codex bundle. */
|
|
9
|
+
const JSON_SCHEMA_VERSION = 1;
|
|
10
|
+
/** Open one trusted HTTPS URL with the platform browser, best effort. */
|
|
11
|
+
function openBrowser(rawUrl) {
|
|
12
|
+
const url = new URL(rawUrl);
|
|
13
|
+
if (url.protocol !== "https:") throw new Error(`refusing to open non-HTTPS authorization URL from ${url.host}`);
|
|
14
|
+
const command = process.platform === "win32" ? {
|
|
15
|
+
file: "rundll32.exe",
|
|
16
|
+
args: ["url.dll,FileProtocolHandler", url.href]
|
|
17
|
+
} : process.platform === "darwin" ? {
|
|
18
|
+
file: "open",
|
|
19
|
+
args: [url.href]
|
|
20
|
+
} : {
|
|
21
|
+
file: "xdg-open",
|
|
22
|
+
args: [url.href]
|
|
23
|
+
};
|
|
24
|
+
try {
|
|
25
|
+
const child = spawn(command.file, command.args, {
|
|
26
|
+
detached: true,
|
|
27
|
+
stdio: "ignore",
|
|
28
|
+
windowsHide: true
|
|
29
|
+
});
|
|
30
|
+
child.on("error", () => {});
|
|
31
|
+
child.unref();
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
/** Remove token-like strings from an external OAuth diagnostic. */
|
|
35
|
+
function safeMessage(error) {
|
|
36
|
+
return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]");
|
|
37
|
+
}
|
|
38
|
+
/** Render one provider event without exposing stored credentials. */
|
|
39
|
+
function notify(event, useBrowser) {
|
|
40
|
+
switch (event.type) {
|
|
41
|
+
case "auth_url":
|
|
42
|
+
process.stdout.write(`Open this URL to sign in:\n${event.url}\n`);
|
|
43
|
+
if (event.instructions !== void 0) process.stdout.write(`${event.instructions}\n`);
|
|
44
|
+
if (useBrowser) openBrowser(event.url);
|
|
45
|
+
break;
|
|
46
|
+
case "device_code":
|
|
47
|
+
process.stdout.write(`Open this URL to sign in:\n${event.verificationUri}\nEnter code: ${event.userCode}\n`);
|
|
48
|
+
if (useBrowser) openBrowser(event.verificationUri);
|
|
49
|
+
break;
|
|
50
|
+
case "info":
|
|
51
|
+
case "progress": process.stdout.write(`${event.message}\n`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Answer a provider auth prompt through the terminal. */
|
|
55
|
+
async function answerPrompt(prompt, deviceCode, question) {
|
|
56
|
+
if (prompt.type === "select") {
|
|
57
|
+
const wanted = deviceCode ? "device_code" : "browser";
|
|
58
|
+
if (!prompt.options.some((option) => option.id === wanted)) throw new Error(`OpenAI Codex login did not offer the requested ${wanted} method`);
|
|
59
|
+
return wanted;
|
|
60
|
+
}
|
|
61
|
+
const suffix = prompt.placeholder === void 0 ? "" : ` (${prompt.placeholder})`;
|
|
62
|
+
return question(`${prompt.message}${suffix}: `, { ...prompt.signal === void 0 ? {} : { signal: prompt.signal } });
|
|
63
|
+
}
|
|
64
|
+
/** Print the standalone command help. */
|
|
65
|
+
function printHelp() {
|
|
66
|
+
process.stdout.write([
|
|
67
|
+
"Usage: dsh-codex-connect <doctor|login|logout|status> [--device-code|--json]",
|
|
68
|
+
" dsh-codex-connect trust-origin <origin>",
|
|
69
|
+
" dsh-codex-connect trusted-origins [--json]",
|
|
70
|
+
" dsh-codex-connect untrust-origin <origin>",
|
|
71
|
+
"",
|
|
72
|
+
" doctor inspect secret-free runtime and OAuth file metadata",
|
|
73
|
+
" login sign in with a separate ChatGPT OAuth session",
|
|
74
|
+
" logout remove the dsh credential without changing ~/.codex",
|
|
75
|
+
" status report non-secret dsh credential state",
|
|
76
|
+
" trust-origin allow one exact browser origin to reach Web OAuth routes",
|
|
77
|
+
" trusted-origins list the currently allowed browser origins",
|
|
78
|
+
" untrust-origin remove one exact browser origin from the allowlist",
|
|
79
|
+
" --device-code use headless device-code login (login only)",
|
|
80
|
+
" --json emit one secret-free JSON document (doctor/status/trusted-origins only)",
|
|
81
|
+
""
|
|
82
|
+
].join("\n"));
|
|
83
|
+
}
|
|
84
|
+
function doctorExitCode(report) {
|
|
85
|
+
const credentialFailure = report.credentialFile.state === "permissions-too-broad" || report.credentialFile.state === "not-a-regular-file" || report.credentialFile.state === "unreadable-metadata";
|
|
86
|
+
const compatibilityFailure = report.compatibility !== void 0 && report.compatibility.status !== "compatible";
|
|
87
|
+
return credentialFailure || compatibilityFailure ? 1 : 0;
|
|
88
|
+
}
|
|
89
|
+
/** Project the diagnostic report without its absolute credential pathname. */
|
|
90
|
+
function doctorJson(report) {
|
|
91
|
+
const result = {
|
|
92
|
+
schemaVersion: JSON_SCHEMA_VERSION,
|
|
93
|
+
package: report.package,
|
|
94
|
+
version: report.version,
|
|
95
|
+
node: report.node,
|
|
96
|
+
credentialFile: {
|
|
97
|
+
state: report.credentialFile.state,
|
|
98
|
+
...report.credentialFile.mode === void 0 ? {} : { mode: report.credentialFile.mode }
|
|
99
|
+
},
|
|
100
|
+
capabilities: report.capabilities,
|
|
101
|
+
providerConflict: report.providerConflict,
|
|
102
|
+
hints: report.hints
|
|
103
|
+
};
|
|
104
|
+
if (report.compatibility !== void 0) result.compatibility = report.compatibility;
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
function printJson(value) {
|
|
108
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
109
|
+
}
|
|
110
|
+
/** Execute one boot-free credential command. */
|
|
111
|
+
async function run(argv) {
|
|
112
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
113
|
+
printHelp();
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
const [rawAction, ...flags] = argv;
|
|
117
|
+
if (![
|
|
118
|
+
"doctor",
|
|
119
|
+
"login",
|
|
120
|
+
"logout",
|
|
121
|
+
"status",
|
|
122
|
+
"trust-origin",
|
|
123
|
+
"trusted-origins",
|
|
124
|
+
"untrust-origin"
|
|
125
|
+
].includes(rawAction)) {
|
|
126
|
+
process.stderr.write(`dsh-codex-connect: expected doctor, login, logout, status, trust-origin, trusted-origins, or untrust-origin; got ${JSON.stringify(rawAction)}\n`);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
const action = rawAction;
|
|
130
|
+
const originArgument = action === "trust-origin" || action === "untrust-origin" ? flags[0] : void 0;
|
|
131
|
+
const optionFlags = action === "trust-origin" || action === "untrust-origin" ? flags.slice(1) : flags;
|
|
132
|
+
const deviceCode = optionFlags.includes("--device-code");
|
|
133
|
+
const jsonOutput = optionFlags.includes("--json");
|
|
134
|
+
if (optionFlags.filter((flag) => flag !== "--device-code" && flag !== "--json").length > 0 || deviceCode && action !== "login" || jsonOutput && (action === "login" || action === "logout" || deviceCode) || (action === "trust-origin" || action === "untrust-origin") && (originArgument === void 0 || optionFlags.length !== 0)) {
|
|
135
|
+
process.stderr.write(`dsh-codex-connect: invalid options for ${action}: ${flags.join(" ")}\n`);
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
switch (action) {
|
|
140
|
+
case "doctor": {
|
|
141
|
+
const report = await diagnoseOpenAICodex();
|
|
142
|
+
if (jsonOutput) {
|
|
143
|
+
printJson(doctorJson(report));
|
|
144
|
+
return doctorExitCode(report);
|
|
145
|
+
}
|
|
146
|
+
process.stdout.write([
|
|
147
|
+
`Codex Connect ${report.version} on ${report.node}`,
|
|
148
|
+
`OAuth file metadata: ${report.credentialFile.state} (${report.credentialFile.path})`,
|
|
149
|
+
...report.compatibility === void 0 ? [] : [`Compatibility: ${report.compatibility.status} (Node ${report.compatibility.node.installed ?? "unknown"}; DSH API ${report.compatibility.packages["@deepseek-ai/dsh-llm"].installed ?? "unknown"}; pi-ai ${report.compatibility.packages["@earendil-works/pi-ai"].installed ?? "unknown"})`],
|
|
150
|
+
`Optional capability defaults: search=${report.capabilities.search ? "enabled" : "disabled"}, imageTool=${report.capabilities.imageTool ? "enabled" : "disabled"}`,
|
|
151
|
+
"Harness defaults: unchanged by this plugin",
|
|
152
|
+
...report.hints.map((hint) => `Hint: ${hint}`),
|
|
153
|
+
""
|
|
154
|
+
].join("\n"));
|
|
155
|
+
return doctorExitCode(report);
|
|
156
|
+
}
|
|
157
|
+
case "status": {
|
|
158
|
+
const status = await openAICodexAuthStatus();
|
|
159
|
+
if (jsonOutput) {
|
|
160
|
+
printJson({
|
|
161
|
+
schemaVersion: JSON_SCHEMA_VERSION,
|
|
162
|
+
package: "dsh-codex-connect",
|
|
163
|
+
version: CODEX_CONNECT_VERSION,
|
|
164
|
+
status: status.authenticated ? "signed-in" : "signed-out"
|
|
165
|
+
});
|
|
166
|
+
return status.authenticated ? 0 : 1;
|
|
167
|
+
}
|
|
168
|
+
if (!status.authenticated) {
|
|
169
|
+
process.stdout.write("Codex Connect: signed out\n");
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
const expires = status.expiresAt;
|
|
173
|
+
const suffix = expires === void 0 || Number.isNaN(expires.valueOf()) ? "" : `; access token expires ${expires.toISOString()} (refresh is automatic)`;
|
|
174
|
+
process.stdout.write(`Codex Connect: signed in${suffix}\n`);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
case "trusted-origins": {
|
|
178
|
+
const origins = await new OpenAICodexTrustedOriginsStore().list();
|
|
179
|
+
if (jsonOutput) printJson({
|
|
180
|
+
schemaVersion: JSON_SCHEMA_VERSION,
|
|
181
|
+
origins
|
|
182
|
+
});
|
|
183
|
+
else for (const origin of origins) process.stdout.write(`${origin}\n`);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
case "trust-origin": {
|
|
187
|
+
if (originArgument === void 0) return 1;
|
|
188
|
+
const normalized = normalizeTrustedOrigin(originArgument);
|
|
189
|
+
const origins = await new OpenAICodexTrustedOriginsStore().trust(originArgument);
|
|
190
|
+
process.stdout.write(`Trusted browser origin: ${normalized}\n`);
|
|
191
|
+
process.stdout.write(`Trusted origins: ${origins.join(", ") || "(none)"}\n`);
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
case "untrust-origin": {
|
|
195
|
+
if (originArgument === void 0) return 1;
|
|
196
|
+
const normalized = normalizeTrustedOrigin(originArgument);
|
|
197
|
+
const origins = await new OpenAICodexTrustedOriginsStore().untrust(originArgument);
|
|
198
|
+
process.stdout.write(`Untrusted browser origin: ${normalized}\n`);
|
|
199
|
+
process.stdout.write(`Trusted origins: ${origins.join(", ") || "(none)"}\n`);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
case "logout":
|
|
203
|
+
await logoutOpenAICodex();
|
|
204
|
+
process.stdout.write(`Codex Connect: signed out; removed ${openAICodexAuthPath()}\n`);
|
|
205
|
+
return 0;
|
|
206
|
+
case "login": {
|
|
207
|
+
const readline = createInterface({
|
|
208
|
+
input: process.stdin,
|
|
209
|
+
output: process.stdout
|
|
210
|
+
});
|
|
211
|
+
try {
|
|
212
|
+
await loginOpenAICodex({
|
|
213
|
+
prompt: (prompt) => answerPrompt(prompt, deviceCode, (text, options) => readline.question(text, options)),
|
|
214
|
+
notify: (event) => notify(event, true)
|
|
215
|
+
});
|
|
216
|
+
} finally {
|
|
217
|
+
readline.close();
|
|
218
|
+
}
|
|
219
|
+
process.stdout.write(`Codex Connect: signed in; credentials saved to ${openAICodexAuthPath()}\n`);
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
process.stderr.write(`dsh-codex-connect: ${action} failed: ${safeMessage(error)}\n`);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) process.exitCode = await run(process.argv.slice(2));
|
|
229
|
+
//#endregion
|
|
230
|
+
export { run };
|