dsh-use-codex 0.0.0-stage → 0.3.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-use-codex contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,390 @@
1
+ # DSH Use Codex
2
+
3
+ > Delegate a task from DeepSeek Harness to your local Codex CLI with one natural-language request, then bring the result back to the conversation.
4
+
5
+ English | [简体中文](README.md)
6
+
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+ ![Node.js](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)
9
+ ![DSH Plugin](https://img.shields.io/badge/DeepSeek%20Harness-plugin-4B6BFB)
10
+ ![Status](https://img.shields.io/badge/status-experimental-orange)
11
+
12
+ `dsh-use-codex` adds a small set of tools to [DeepSeek Harness (DSH)](https://github.com/deepseek-ai) for delegating one-shot tasks to the local [OpenAI Codex CLI](https://github.com/openai/codex). It is designed for code changes, debugging, repository analysis, and batch file processing while keeping execution inside explicitly authorized workspaces.
13
+
14
+ ## Contents
15
+
16
+ - [Features](#features)
17
+ - [Requirements](#requirements)
18
+ - [Installation](#installation)
19
+ - [Configuration](#configuration)
20
+ - [Usage](#usage)
21
+ - [Tool reference](#tool-reference)
22
+ - [How it works](#how-it-works)
23
+ - [Security model](#security-model)
24
+ - [Development and testing](#development-and-testing)
25
+ - [FAQ](#faq)
26
+ - [Project status](#project-status)
27
+ - [License](#license)
28
+
29
+ ## Features
30
+
31
+ - **Natural-language delegation**: ask DSH to "let Codex do..." or "hand this to Codex".
32
+ - **Three built-in tools**: run a task, inspect status, and toggle the plugin.
33
+ - **Workspace allowlist**: Codex can only run in explicitly authorized, real directories.
34
+ - **Symlink escape protection**: both the requested directory and configured roots are resolved to real paths.
35
+ - **Safe sandbox default**: `workspace-write` is used unless the caller requests otherwise.
36
+ - **Two-step dangerous-mode approval**: `danger-full-access` also requires explicit host configuration.
37
+ - **No shell interpolation**: the task is passed as a single argv value.
38
+ - **Process-group cleanup**: the complete Codex process group is terminated on timeout or cancellation.
39
+ - **Bounded output**: stdout and stderr are capped before being returned to the conversation.
40
+ - **Minimal state**: only the runtime toggle and update timestamp are persisted.
41
+ - **Zero runtime dependencies**: the plugin itself needs no additional npm packages.
42
+
43
+ ## Requirements
44
+
45
+ | Component | Requirement |
46
+ |---|---|
47
+ | Node.js | `>= 20` |
48
+ | DSH | A version supporting Cordis bundle patches and tool registration |
49
+ | Codex CLI | Supports `codex exec`, `-C`, `-s`, and `--ephemeral` |
50
+ | Operating system | macOS or Linux; Windows has not been validated |
51
+
52
+ Validated combination:
53
+
54
+ - DeepSeek Harness `0.2.1-rc.2`
55
+ - Node.js `22.x`
56
+ - Codex CLI `0.166.0`
57
+
58
+ Verify the Codex CLI first:
59
+
60
+ ```bash
61
+ codex --version
62
+ codex login status
63
+ ```
64
+
65
+ > When using a custom OpenAI-compatible provider, a logged-out result from `codex login status` may not indicate a problem. The authoritative check is whether `codex exec` can complete a task.
66
+
67
+ ## Installation
68
+
69
+ ### Install through the DSH plugin market index
70
+
71
+ In DSH, open **Discover → Plugin Market → Add index source** and add:
72
+
73
+ ```text
74
+ https://raw.githubusercontent.com/chaorenzeng/dsh-use-codex/main/market/index.json
75
+ ```
76
+
77
+ Search for **DSH Use Codex** and install it. The market index pins an immutable Git commit; the actual installation source remains this repository.
78
+
79
+ ### 1. Get the source
80
+
81
+ ```bash
82
+ git clone git@github.com:chaorenzeng/dsh-use-codex.git "$HOME/Projects/dsh-use-codex"
83
+ cd "$HOME/Projects/dsh-use-codex"
84
+ ```
85
+
86
+ ### 2. Register the dependency and bundle in a DSH profile
87
+
88
+ Edit the target DSH profile's `package.json`, for example `~/.dsh/profiles/web/package.json`:
89
+
90
+ ```json
91
+ {
92
+ "dependencies": {
93
+ "dsh-use-codex": "file:/absolute/path/to/dsh-use-codex"
94
+ },
95
+ "dsh": {
96
+ "profile": {
97
+ "bundles": [
98
+ "dsh-use-codex"
99
+ ]
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ Keep the existing dependencies and bundles in that file. Then install the profile dependencies:
106
+
107
+ ```bash
108
+ cd ~/.dsh/profiles/web
109
+ pnpm install
110
+ ```
111
+
112
+ If the profile uses another package manager, use the equivalent install command.
113
+
114
+ ### 3. Add local configuration
115
+
116
+ Override the plugin configuration in the target DSH profile's `cordis.patch.yml`:
117
+
118
+ ```yaml
119
+ - id: use-codex
120
+ name: dsh-use-codex
121
+ config:
122
+ enabled: true
123
+ bin: /absolute/path/to/codex
124
+ defaultCwd: /absolute/path/to/workspace
125
+ allowedRoots:
126
+ - /absolute/path/to/workspace
127
+ allowDangerFullAccess: false
128
+ timeoutSec: 900
129
+ maxOutputChars: 20000
130
+ ```
131
+
132
+ > Local paths, Codex credentials, and provider settings belong in machine-local configuration and must not be committed to a public repository.
133
+
134
+ ### 4. Restart DSH
135
+
136
+ Restart the DSH host so the profile loads the plugin for the first time. If it is managed by a macOS LaunchAgent:
137
+
138
+ ```bash
139
+ launchctl kickstart -k "gui/$(id -u)/<YOUR_DSH_LAUNCHD_LABEL>"
140
+ ```
141
+
142
+ After restarting, **create a new DSH conversation**. Existing conversations do not automatically receive newly registered tools.
143
+
144
+ ### 5. Verify the integration
145
+
146
+ Send this in a new conversation:
147
+
148
+ ```text
149
+ Let Codex do: reply only with USE_CODEX_OK. Do not read or modify any files.
150
+ ```
151
+
152
+ A `USE_CODEX_OK` response confirms that the plugin, Codex CLI, and tool-call path are working.
153
+
154
+ ## Configuration
155
+
156
+ ### Plugin options
157
+
158
+ | Option | Default | Description |
159
+ |---|---|---|
160
+ | `enabled` | `false` | Register tools when DSH starts; when disabled, no tools are registered |
161
+ | `bin` | `codex` | Codex CLI executable path; supports `~` expansion |
162
+ | `defaultCwd` | DSH process working directory | Used when `cwd` is omitted; must be an absolute directory inside the allowlist |
163
+ | `allowedRoots` | `[defaultCwd]` | Real directories in which Codex may run |
164
+ | `allowDangerFullAccess` | `false` | Allow an individual call to request `danger-full-access` |
165
+ | `timeoutSec` | `900` | Maximum run timeout, from `1` to `3600` seconds |
166
+ | `maxOutputChars` | `20000` | Maximum retained characters for stdout and stderr separately, from `1000` to `200000` |
167
+
168
+ The public [`cordis.patch.yml`](cordis.patch.yml) uses `/tmp` as a safe placeholder and sets `enabled: false`. Production settings should be kept in the machine-local profile patch.
169
+
170
+ ### Environment variables
171
+
172
+ | Variable | Description |
173
+ |---|---|
174
+ | `CODEX_BIN` | Codex CLI path used when `bin` is not configured |
175
+ | `USE_CODEX_STATE` | Overrides the runtime toggle state-file path |
176
+ | `DSH_HOME` | DSH data directory; state defaults to `use-codex.json` under this directory |
177
+
178
+ The runtime toggle state file defaults to:
179
+
180
+ ```text
181
+ ${DSH_HOME:-~/.dsh}/use-codex.json
182
+ ```
183
+
184
+ It is created with mode `0600` and contains only:
185
+
186
+ ```json
187
+ {
188
+ "enabled": true,
189
+ "updatedAt": "2026-01-01T00:00:00.000Z"
190
+ }
191
+ ```
192
+
193
+ ## Usage
194
+
195
+ ### Natural-language delegation
196
+
197
+ ```text
198
+ Let Codex do: inspect why the tests are failing, identify the root cause, and fix it. Do not commit.
199
+ ```
200
+
201
+ ```text
202
+ Hand this to Codex: analyze duplicated code under src and propose a plan before changing anything.
203
+ ```
204
+
205
+ DSH decides when to call `codex_run`. Give Codex a clear objective, scope, constraints, and acceptance criteria so it can complete the task independently.
206
+
207
+ ### Specify a workspace and sandbox
208
+
209
+ ```text
210
+ Ask Codex to summarize the architecture of /absolute/path/to/repo using the read-only sandbox.
211
+ ```
212
+
213
+ ### Check status
214
+
215
+ ```text
216
+ Check the status of Use Codex.
217
+ ```
218
+
219
+ ### Disable immediately
220
+
221
+ ```text
222
+ Disable Use Codex.
223
+ ```
224
+
225
+ New `codex_run` calls are rejected immediately. Re-enabling the plugin does not require a DSH restart.
226
+
227
+ ### Raw tool arguments
228
+
229
+ ```json
230
+ {
231
+ "task": "Run the relevant tests and fix the failures. Do not commit.",
232
+ "cwd": "/absolute/path/to/repository",
233
+ "sandbox": "workspace-write",
234
+ "timeoutSec": 600
235
+ }
236
+ ```
237
+
238
+ ## Tool reference
239
+
240
+ ### `codex_run`
241
+
242
+ Starts one non-interactive `codex exec` task in the requested workspace.
243
+
244
+ | Argument | Type | Required | Description |
245
+ |---|---|---|---|
246
+ | `task` | `string` | Yes | Complete task instructions; up to `100000` characters |
247
+ | `cwd` | `string` | No | Absolute workspace path that resolves inside `allowedRoots` |
248
+ | `sandbox` | `string` | No | `read-only`, `workspace-write`, or `danger-full-access` |
249
+ | `timeoutSec` | `number` | No | Per-call timeout, capped by the plugin's configured `timeoutSec` |
250
+
251
+ The default sandbox is `workspace-write`. `danger-full-access` is rejected unless the host sets `allowDangerFullAccess: true`.
252
+
253
+ ### `codex_status`
254
+
255
+ Reports:
256
+
257
+ - current runtime toggle state
258
+ - `codex --version`
259
+ - `codex login status`
260
+ - effective workspace allowlist
261
+ - whether dangerous mode is allowed
262
+ - timeout and output limits
263
+ - a summary of the most recent run in the current DSH process
264
+
265
+ ### `codex_switch`
266
+
267
+ | Argument | Type | Required | Description |
268
+ |---|---|---|---|
269
+ | `enabled` | `boolean` | Yes | `true` enables the plugin; `false` disables it |
270
+
271
+ The runtime toggle is persisted separately and does not affect other DSH plugins.
272
+
273
+ ## How it works
274
+
275
+ ```mermaid
276
+ flowchart LR
277
+ U[DSH user] --> A[DSH agent]
278
+ A -->|codex_run| B[dsh-use-codex]
279
+ B --> C{Validate workspace<br/>and security policy}
280
+ C -->|allowed| D[Codex CLI<br/>codex exec]
281
+ D --> W[Authorized workspace]
282
+ D --> B
283
+ B --> A
284
+ A --> U
285
+ C -->|rejected| A
286
+ ```
287
+
288
+ The effective command is equivalent to:
289
+
290
+ ```bash
291
+ codex exec \
292
+ -C /absolute/path/to/workspace \
293
+ -s workspace-write \
294
+ --skip-git-repo-check \
295
+ --ephemeral \
296
+ --color never \
297
+ -- "task text"
298
+ ```
299
+
300
+ `--ephemeral` disables Codex session reuse; it does not make the run read-only. With `workspace-write`, Codex may modify files inside the authorized workspace.
301
+
302
+ ## Security model
303
+
304
+ This plugin allows DSH to start the local Codex CLI. Treat any DSH conversation that can access the plugin as having controlled local execution capability.
305
+
306
+ ### Built-in protections
307
+
308
+ - The workspace must exist, be absolute, and resolve to a real filesystem path.
309
+ - The resolved workspace must remain inside `allowedRoots`.
310
+ - Symlinks cannot be used to escape an allowed root.
311
+ - The task is passed as one argv value without a shell.
312
+ - Timeout and cancellation terminate the Codex process group.
313
+ - stdout and stderr are bounded.
314
+ - Task text, working directories, and outputs are not written to plugin state.
315
+ - The public bundle disables dangerous mode by default.
316
+
317
+ ### Deployment guidance
318
+
319
+ - Install only in a trusted local DSH instance; never expose it to untrusted users.
320
+ - Keep `allowedRoots` as narrow as practical.
321
+ - Do not enable `allowDangerFullAccess` without additional host-level isolation.
322
+ - The Codex child inherits the DSH environment, which may contain credentials.
323
+ - `workspace-write` can still modify allowed files; do not allowlist sensitive directories.
324
+ - Although task text is not stored in plugin state, it may appear in DSH conversation history, host logs, or Codex logs.
325
+ - Keep DSH, Codex CLI, and dependencies up to date.
326
+
327
+ See [SECURITY.md](SECURITY.md) for the full security policy.
328
+
329
+ ## Development and testing
330
+
331
+ ```bash
332
+ git clone https://github.com/chaorenzeng/dsh-use-codex.git
333
+ cd dsh-use-codex
334
+ npm test
335
+ ```
336
+
337
+ Tests use temporary directories and a fake Codex executable. They do not call a network service, model, or private workspace. Coverage includes:
338
+
339
+ - plugin metadata and registration of all three tools
340
+ - empty tasks, missing paths, and paths outside the allowlist
341
+ - symlink escapes
342
+ - dangerous-mode rejection by default
343
+ - runtime toggling
344
+ - `workspace-write` and `--ephemeral` arguments
345
+ - process-group termination on timeout
346
+ - minimal state persistence
347
+
348
+ Inspect the actual npm package contents before publishing:
349
+
350
+ ```bash
351
+ npm pack --dry-run
352
+ ```
353
+
354
+ Issues and pull requests are welcome. Do not include credentials, private paths, full logs, or other sensitive information in public issues. Report security problems through the private process described in [SECURITY.md](SECURITY.md).
355
+
356
+ ## FAQ
357
+
358
+ ### Why are the tools missing in a new conversation?
359
+
360
+ Confirm that `dsh.profile.bundles` in `package.json` includes `dsh-use-codex`, `enabled` is `true` in the profile patch, and DSH was restarted. Then create a new conversation.
361
+
362
+ ### Why is the working directory rejected?
363
+
364
+ `cwd` must be absolute, exist, and resolve inside `allowedRoots`. Invalid allowlist entries are ignored during startup.
365
+
366
+ ### Why does a call fail when `cwd` is omitted?
367
+
368
+ `defaultCwd` must exist and be inside the allowlist. Set both `defaultCwd` and the corresponding `allowedRoots` entry explicitly.
369
+
370
+ ### Why does `codex_status` report a logged-out state?
371
+
372
+ With a custom OpenAI-compatible provider, `codex login status` may not reflect actual availability. Verify by running a real `codex exec` task instead.
373
+
374
+ ### Why does a task take so long?
375
+
376
+ One run may wait up to `900` seconds by default. Set `timeoutSec` within the configured maximum when a shorter or longer limit is required.
377
+
378
+ ### Is a restart required after disabling the plugin?
379
+
380
+ No. `codex_switch` takes effect immediately. A restart is only required when changing registration-time settings such as `enabled` in the profile configuration.
381
+
382
+ ## Project status
383
+
384
+ The project is experimental at the `0.x` stage. Tool arguments, configuration fields, and response formats may change as DSH and Codex CLI evolve. Pin validated DSH and Codex CLI versions and rerun the end-to-end check after upgrades.
385
+
386
+ This is an independent community plugin, not affiliated with or endorsed by DeepSeek or OpenAI. DeepSeek Harness, Codex, and related names and trademarks belong to their respective owners.
387
+
388
+ ## License
389
+
390
+ [MIT](LICENSE) © 2026 dsh-use-codex contributors
package/README.md CHANGED
@@ -1,3 +1,390 @@
1
- # Temporary Holding Version
1
+ # DSH Use Codex
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > 在 DeepSeek Harness 中用一句话把任务交给本机 Codex CLI,安全执行后把结果带回当前会话。
4
+
5
+ 简体中文 | [English](README.en.md)
6
+
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+ ![Node.js](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)
9
+ ![DSH Plugin](https://img.shields.io/badge/DeepSeek%20Harness-plugin-4B6BFB)
10
+ ![Status](https://img.shields.io/badge/status-experimental-orange)
11
+
12
+ `dsh-use-codex` 为 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai) 提供一组工具,把独立任务委派给本机 [OpenAI Codex CLI](https://github.com/openai/codex) 执行。它适合代码修改、故障排查、仓库分析和批量文件处理等一次性任务,并将执行结果返回 DSH 当前会话。
13
+
14
+ ## 目录
15
+
16
+ - [功能特性](#功能特性)
17
+ - [运行要求](#运行要求)
18
+ - [安装](#安装)
19
+ - [配置](#配置)
20
+ - [使用方式](#使用方式)
21
+ - [工具参考](#工具参考)
22
+ - [运行原理](#运行原理)
23
+ - [安全模型](#安全模型)
24
+ - [开发与测试](#开发与测试)
25
+ - [常见问题](#常见问题)
26
+ - [项目状态](#项目状态)
27
+ - [许可证](#许可证)
28
+
29
+ ## 功能特性
30
+
31
+ - **自然语言委派**:在 DSH 中说“让 Codex 做……”或“交给 Codex 执行……”。
32
+ - **三个内置工具**:任务执行、状态检查、即时开关。
33
+ - **工作目录白名单**:只允许 Codex 在显式授权的真实目录中运行。
34
+ - **软链逃逸防护**:同时解析工作目录与白名单的真实路径。
35
+ - **默认安全沙箱**:默认使用 Codex 的 `workspace-write` 模式。
36
+ - **危险模式双重授权**:`danger-full-access` 必须由宿主配置显式允许。
37
+ - **无 shell 拼接**:任务正文作为单个 argv 参数传递,避免 shell 注入。
38
+ - **进程组清理**:超时或取消时终止完整 Codex 进程组。
39
+ - **输出限长**:限制 stdout/stderr,避免单次结果淹没会话。
40
+ - **最小状态持久化**:状态文件只记录开关与更新时间,不保存任务、目录或输出。
41
+ - **零运行时依赖**:插件本身不需要额外 npm 依赖。
42
+
43
+ ## 运行要求
44
+
45
+ | 组件 | 要求 |
46
+ |---|---|
47
+ | Node.js | `>= 20` |
48
+ | DSH | 支持 Cordis bundle patch 与工具注册的版本 |
49
+ | Codex CLI | 支持 `codex exec`、`-C`、`-s`、`--ephemeral` |
50
+ | 操作系统 | macOS 或 Linux;Windows 尚未验证 |
51
+
52
+ 已验证组合:
53
+
54
+ - DeepSeek Harness `0.2.1-rc.2`
55
+ - Node.js `22.x`
56
+ - Codex CLI `0.166.0`
57
+
58
+ 先确认 Codex CLI 可用:
59
+
60
+ ```bash
61
+ codex --version
62
+ codex login status
63
+ ```
64
+
65
+ > 如果使用自定义 OpenAI-compatible provider,`codex login status` 显示未登录不一定是错误。请以 Codex CLI 能否实际执行 `codex exec` 为准。
66
+
67
+ ## 安装
68
+
69
+ ### 通过 DSH 插件市场索引安装
70
+
71
+ 在 DSH 的「发现 → 插件市场 → 添加索引源」中添加:
72
+
73
+ ```text
74
+ https://raw.githubusercontent.com/chaorenzeng/dsh-use-codex/main/market/index.json
75
+ ```
76
+
77
+ 然后搜索 **DSH Use Codex** 并安装。市场索引固定到不可变的 Git 提交,实际安装来源仍是本仓库。
78
+
79
+ ### 1. 获取源码
80
+
81
+ ```bash
82
+ git clone git@github.com:chaorenzeng/dsh-use-codex.git "$HOME/Projects/dsh-use-codex"
83
+ cd "$HOME/Projects/dsh-use-codex"
84
+ ```
85
+
86
+ ### 2. 在 DSH profile 中登记依赖与 bundle
87
+
88
+ 编辑目标 DSH profile 的 `package.json`,例如 `~/.dsh/profiles/web/package.json`:
89
+
90
+ ```json
91
+ {
92
+ "dependencies": {
93
+ "dsh-use-codex": "file:/absolute/path/to/dsh-use-codex"
94
+ },
95
+ "dsh": {
96
+ "profile": {
97
+ "bundles": [
98
+ "dsh-use-codex"
99
+ ]
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ 保留该文件中已有的依赖和 bundle,不要清空其他配置。然后同步 profile 依赖:
106
+
107
+ ```bash
108
+ cd ~/.dsh/profiles/web
109
+ pnpm install
110
+ ```
111
+
112
+ 如果 DSH profile 使用其他包管理器,请使用对应命令安装依赖。
113
+
114
+ ### 3. 添加本机配置
115
+
116
+ 在目标 DSH profile 的 `cordis.patch.yml` 中覆盖插件配置:
117
+
118
+ ```yaml
119
+ - id: use-codex
120
+ name: dsh-use-codex
121
+ config:
122
+ enabled: true
123
+ bin: /absolute/path/to/codex
124
+ defaultCwd: /absolute/path/to/workspace
125
+ allowedRoots:
126
+ - /absolute/path/to/workspace
127
+ allowDangerFullAccess: false
128
+ timeoutSec: 900
129
+ maxOutputChars: 20000
130
+ ```
131
+
132
+ > 真实路径、Codex 凭据和 provider 设置属于本机配置,不应提交到开源仓库。
133
+
134
+ ### 4. 重启 DSH
135
+
136
+ 重启 DSH 宿主,使 profile 首次加载插件和配置。若由 macOS LaunchAgent 管理,可使用:
137
+
138
+ ```bash
139
+ launchctl kickstart -k "gui/$(id -u)/<YOUR_DSH_LAUNCHD_LABEL>"
140
+ ```
141
+
142
+ 重启后必须**新建一个 DSH 会话**,旧会话不会自动获得新注册的工具。
143
+
144
+ ### 5. 验证
145
+
146
+ 在新建会话中发送:
147
+
148
+ ```text
149
+ 让 Codex 做:只回复 USE_CODEX_OK,不要读取或修改任何文件。
150
+ ```
151
+
152
+ 如果返回 `USE_CODEX_OK`,说明插件、Codex CLI 和工具调用链均正常。
153
+
154
+ ## 配置
155
+
156
+ ### 插件配置
157
+
158
+ | 配置项 | 默认值 | 说明 |
159
+ |---|---|---|
160
+ | `enabled` | `false` | 是否在 DSH 启动时注册工具;关闭时不注册任何工具 |
161
+ | `bin` | `codex` | Codex CLI 可执行文件路径;支持 `~` 展开 |
162
+ | `defaultCwd` | DSH 进程工作目录 | 未传 `cwd` 时使用,必须是白名单内的绝对目录 |
163
+ | `allowedRoots` | `[defaultCwd]` | 允许 Codex 工作的真实目录列表 |
164
+ | `allowDangerFullAccess` | `false` | 是否允许单次调用选择 `danger-full-access` |
165
+ | `timeoutSec` | `900` | 单次调用超时上限,范围 `1–3600` 秒 |
166
+ | `maxOutputChars` | `20000` | stdout 和 stderr 各自最多保留的字符数,范围 `1000–200000` |
167
+
168
+ 公开仓库中的 [`cordis.patch.yml`](cordis.patch.yml) 使用 `/tmp` 作为安全占位目录,并默认设置 `enabled: false`。部署时应只在个人 profile patch 中覆盖为真实配置。
169
+
170
+ ### 环境变量
171
+
172
+ | 环境变量 | 说明 |
173
+ |---|---|
174
+ | `CODEX_BIN` | 未配置 `bin` 时使用的 Codex CLI 路径 |
175
+ | `USE_CODEX_STATE` | 覆盖即时开关状态文件路径 |
176
+ | `DSH_HOME` | DSH 数据目录;状态默认写入其下的 `use-codex.json` |
177
+
178
+ 即时开关状态文件默认位于:
179
+
180
+ ```text
181
+ ${DSH_HOME:-~/.dsh}/use-codex.json
182
+ ```
183
+
184
+ 文件权限为 `0600`,内容仅包含:
185
+
186
+ ```json
187
+ {
188
+ "enabled": true,
189
+ "updatedAt": "2026-01-01T00:00:00.000Z"
190
+ }
191
+ ```
192
+
193
+ ## 使用方式
194
+
195
+ ### 自然语言调用
196
+
197
+ ```text
198
+ 让 Codex 做:检查当前仓库为什么测试失败,定位原因并修复;不要提交 commit。
199
+ ```
200
+
201
+ ```text
202
+ 交给 Codex 执行:分析 src 目录中的重复代码,先给出方案,不要直接修改。
203
+ ```
204
+
205
+ DSH 的模型会按需调用 `codex_run`。任务应写清目标、范围、约束和验收标准,以便 Codex 独立完成。
206
+
207
+ ### 指定工作目录和沙箱
208
+
209
+ ```text
210
+ 让 Codex 做:只读取这个仓库并总结架构。工作目录使用 /absolute/path/to/repo,使用 read-only 沙箱。
211
+ ```
212
+
213
+ ### 查看状态
214
+
215
+ ```text
216
+ 检查 Codex 委派的状态。
217
+ ```
218
+
219
+ ### 即时关闭
220
+
221
+ ```text
222
+ 关闭 Codex 委派。
223
+ ```
224
+
225
+ 关闭后 `codex_run` 会立即拒绝新任务;重新打开不需要重启 DSH。
226
+
227
+ ### 直接调用工具参数
228
+
229
+ ```json
230
+ {
231
+ "task": "运行相关测试并修复失败原因,不要提交 commit。",
232
+ "cwd": "/absolute/path/to/repository",
233
+ "sandbox": "workspace-write",
234
+ "timeoutSec": 600
235
+ }
236
+ ```
237
+
238
+ ## 工具参考
239
+
240
+ ### `codex_run`
241
+
242
+ 在指定工作目录中启动一次非交互式 `codex exec` 任务。
243
+
244
+ | 参数 | 类型 | 必填 | 说明 |
245
+ |---|---|---|---|
246
+ | `task` | `string` | 是 | 完整任务说明;最大 `100000` 字符 |
247
+ | `cwd` | `string` | 否 | 绝对工作目录,必须位于 `allowedRoots` 内 |
248
+ | `sandbox` | `string` | 否 | `read-only`、`workspace-write` 或 `danger-full-access` |
249
+ | `timeoutSec` | `number` | 否 | 本次超时,不能超过插件配置的 `timeoutSec` |
250
+
251
+ 默认沙箱为 `workspace-write`。`danger-full-access` 默认拒绝,只有在宿主配置中设置 `allowDangerFullAccess: true` 后才可选择。
252
+
253
+ ### `codex_status`
254
+
255
+ 只读检查以下信息:
256
+
257
+ - 即时开关状态
258
+ - `codex --version`
259
+ - `codex login status`
260
+ - 有效目录白名单
261
+ - 危险模式是否允许
262
+ - 超时与输出限制
263
+ - 当前 DSH 进程内最近一次调用的结果摘要
264
+
265
+ ### `codex_switch`
266
+
267
+ | 参数 | 类型 | 必填 | 说明 |
268
+ |---|---|---|---|
269
+ | `enabled` | `boolean` | 是 | `true` 打开,`false` 关闭 |
270
+
271
+ 即时开关持久化到状态文件,不影响 DSH 宿主中的其他插件。
272
+
273
+ ## 运行原理
274
+
275
+ ```mermaid
276
+ flowchart LR
277
+ U[DSH 用户] --> A[DSH Agent]
278
+ A -->|codex_run| B[dsh-use-codex]
279
+ B --> C{目录与安全校验}
280
+ C -->|通过| D[Codex CLI<br/>codex exec]
281
+ D --> W[授权工作目录]
282
+ D --> B
283
+ B --> A
284
+ A --> U
285
+ C -->|拒绝| A
286
+ ```
287
+
288
+ 插件最终执行的核心命令等价于:
289
+
290
+ ```bash
291
+ codex exec \
292
+ -C /absolute/path/to/workspace \
293
+ -s workspace-write \
294
+ --skip-git-repo-check \
295
+ --ephemeral \
296
+ --color never \
297
+ -- "任务正文"
298
+ ```
299
+
300
+ `--ephemeral` 表示不复用 Codex 会话,不表示只读。若 Codex 获得 `workspace-write`,它仍可能修改允许目录中的文件。
301
+
302
+ ## 安全模型
303
+
304
+ 本插件允许 DSH 启动本机 Codex,因此应把 DSH 会话视为具备受控本机执行能力。
305
+
306
+ ### 已有防护
307
+
308
+ - 工作目录必须存在、为绝对路径,并通过真实路径解析。
309
+ - 工作目录必须位于 `allowedRoots` 内。
310
+ - 解析软链后仍须位于白名单内,阻止软链逃逸。
311
+ - 任务作为单个 argv 参数传入,不经过 shell。
312
+ - 超时或取消时终止 Codex 进程组。
313
+ - stdout/stderr 有长度上限。
314
+ - 插件状态不保存任务正文、工作目录或执行输出。
315
+ - 公共 bundle 默认关闭危险模式。
316
+
317
+ ### 部署建议
318
+
319
+ - 只安装在可信的本机 DSH 中,不要把 DSH 暴露给不可信用户。
320
+ - 将 `allowedRoots` 收窄到实际需要的仓库或目录。
321
+ - 除非宿主机已做额外隔离,否则不要开启 `allowDangerFullAccess`。
322
+ - Codex 子进程会继承 DSH 环境变量,其中可能包含凭据;请检查 DSH 运行环境。
323
+ - `workspace-write` 仍允许修改工作区,不应把敏感目录加入白名单。
324
+ - 任务正文虽不写入插件状态,但仍可能出现在 DSH 对话记录、宿主日志或 Codex 自身日志中。
325
+ - 保持 DSH、Codex CLI 和依赖项更新。
326
+
327
+ 完整安全说明见 [SECURITY.md](SECURITY.md)。
328
+
329
+ ## 开发与测试
330
+
331
+ ```bash
332
+ git clone https://github.com/chaorenzeng/dsh-use-codex.git
333
+ cd dsh-use-codex
334
+ npm test
335
+ ```
336
+
337
+ 测试使用临时目录和假 Codex 可执行文件,不访问网络、真实模型或私有工作区。当前覆盖:
338
+
339
+ - 插件元数据与三个工具注册
340
+ - 空任务、无效目录和白名单外目录
341
+ - 软链逃逸
342
+ - 危险模式默认拒绝
343
+ - 即时开关
344
+ - `workspace-write` 与 `--ephemeral` 参数
345
+ - 超时进程组终止
346
+ - 状态文件最小化持久化
347
+
348
+ 提交前可检查 npm 实际打包内容:
349
+
350
+ ```bash
351
+ npm pack --dry-run
352
+ ```
353
+
354
+ 欢迎提交 Issue 和 Pull Request。请勿在公开 Issue 中提交凭据、私有路径、完整日志或其他敏感信息;安全问题请参考 [SECURITY.md](SECURITY.md) 的私密报告流程。
355
+
356
+ ## 常见问题
357
+
358
+ ### 为什么新建会话后看不到工具?
359
+
360
+ 先确认 `package.json` 的 `dsh.profile.bundles` 包含 `dsh-use-codex`,profile patch 中 `enabled` 为 `true`,然后重启 DSH 并新建会话。
361
+
362
+ ### 为什么提示工作目录不在白名单内?
363
+
364
+ `cwd` 必须是绝对路径、真实存在,并在解析软链后位于 `allowedRoots` 内。白名单中的无效目录也会被忽略。
365
+
366
+ ### 为什么不传 `cwd` 会失败?
367
+
368
+ `defaultCwd` 必须存在且位于白名单内。推荐在 profile 配置中同时显式设置 `defaultCwd` 和对应的 `allowedRoots`。
369
+
370
+ ### 为什么 `codex_status` 显示未登录?
371
+
372
+ 使用自定义 OpenAI-compatible provider 时,`codex login status` 可能不反映实际可用性。请直接验证 `codex exec` 是否能完成任务。
373
+
374
+ ### 为什么任务执行很久?
375
+
376
+ 单次执行默认最多等待 `900` 秒。可以在配置范围内通过 `timeoutSec` 调整,超出上限的值会被限制到插件配置的最大值。
377
+
378
+ ### 关闭插件后是否还需要重启?
379
+
380
+ 不需要。`codex_switch` 的关闭状态立即生效。若要在启动时不注册工具,则需在 profile 配置中设置 `enabled: false` 并重启 DSH。
381
+
382
+ ## 项目状态
383
+
384
+ 项目处于 `0.x` 实验阶段,工具参数、配置字段和返回格式可能随 DSH 与 Codex CLI 的演进调整。发布前建议固定已验证的 DSH 和 Codex CLI 版本,并在更新后进行端到端验证。
385
+
386
+ 本项目是独立的社区插件,与 DeepSeek 或 OpenAI 无隶属或官方背书关系。DeepSeek Harness、Codex 及相关名称和商标归各自权利人所有。
387
+
388
+ ## License
389
+
390
+ [MIT](LICENSE) © 2026 dsh-use-codex contributors
package/SECURITY.md ADDED
@@ -0,0 +1,30 @@
1
+ # Security Policy
2
+
3
+ ## Threat model
4
+
5
+ `dsh-use-codex` lets a DSH tool call start the local Codex CLI. The person or agent controlling DSH can therefore request local code execution within the configured boundary.
6
+
7
+ Only install this plugin in a trusted local DSH instance. Do not expose DSH or the plugin to untrusted users.
8
+
9
+ ## Defaults
10
+
11
+ - `enabled: false` in the public bundle patch
12
+ - `workspace-write` sandbox for normal runs
13
+ - `danger-full-access` disabled unless the host explicitly enables it
14
+ - Required working directory must resolve inside `allowedRoots`
15
+ - Symlink escapes are rejected
16
+ - The task is passed as an argv value without a shell
17
+ - Timeouts terminate the Codex process group
18
+ - Tasks, working directories, and outputs are not persisted by the plugin
19
+
20
+ ## Operator responsibilities
21
+
22
+ - Keep `allowedRoots` as narrow as possible
23
+ - Do not set `allowDangerFullAccess: true` unless the host is externally isolated
24
+ - Review Codex configuration and provider endpoints
25
+ - Treat inherited environment variables as available to the Codex child process
26
+ - Keep Codex CLI and DSH updated
27
+
28
+ ## Reporting
29
+
30
+ Please report vulnerabilities through the GitHub repository's private security advisory channel rather than a public issue. Include reproduction steps, affected versions, and the expected impact.
@@ -0,0 +1,19 @@
1
+ # Public-safe default bundle patch for dsh-use-codex.
2
+ # Keep machine-specific paths in your DSH profile patch, not in this repository.
3
+ #
4
+ # Security defaults:
5
+ # - enabled: false until the host explicitly opts in
6
+ # - workspace-write sandbox for codex_run
7
+ # - danger-full-access disabled
8
+ - insert:
9
+ - id: use-codex
10
+ name: dsh-use-codex
11
+ config:
12
+ enabled: false
13
+ bin: codex
14
+ defaultCwd: /tmp
15
+ allowedRoots:
16
+ - /tmp
17
+ allowDangerFullAccess: false
18
+ timeoutSec: 900
19
+ maxOutputChars: 20000
package/lib/index.js ADDED
@@ -0,0 +1,464 @@
1
+ /**
2
+ * dsh-use-codex — 让 DeepSeek Harness 通过工具调用本机 Codex CLI。
3
+ *
4
+ * 安全边界:
5
+ * 1. 工作目录必须是白名单内的真实绝对路径;软链逃逸会被拒绝。
6
+ * 2. 默认使用 workspace-write,危险模式需由宿主配置显式开启。
7
+ * 3. 不经过 shell,任务作为单个 argv 传给 Codex。
8
+ * 4. 超时或取消时终止 Codex 进程组,并限制输出大小。
9
+ * 5. 不持久化任务正文、工作目录或输出;状态文件只记录即时开关。
10
+ */
11
+ import { spawn } from 'node:child_process';
12
+ import {
13
+ existsSync,
14
+ mkdirSync,
15
+ readFileSync,
16
+ realpathSync,
17
+ renameSync,
18
+ statSync,
19
+ writeFileSync,
20
+ } from 'node:fs';
21
+ import { homedir, tmpdir } from 'node:os';
22
+ import { isAbsolute, join, resolve, sep } from 'node:path';
23
+
24
+ export const name = 'use-codex';
25
+ export const inject = ['tools'];
26
+
27
+ const DEFAULT_TIMEOUT_SEC = 900;
28
+ const DEFAULT_MAX_OUTPUT_CHARS = 20_000;
29
+ const MAX_TASK_CHARS = 100_000;
30
+ const STATE_FILE = process.env.USE_CODEX_STATE
31
+ || join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'use-codex.json');
32
+
33
+ function finiteNumber(value, fallback) {
34
+ const number = Number(value);
35
+ return Number.isFinite(number) ? number : fallback;
36
+ }
37
+
38
+ function clamp(value, min, max) {
39
+ return Math.min(Math.max(value, min), max);
40
+ }
41
+
42
+ function expandHome(input) {
43
+ const value = String(input ?? '').trim();
44
+ if (value === '~') return homedir();
45
+ if (value.startsWith(`~${sep}`)) return join(homedir(), value.slice(2));
46
+ return value;
47
+ }
48
+
49
+ function readState() {
50
+ try {
51
+ const parsed = JSON.parse(readFileSync(STATE_FILE, 'utf8'));
52
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
53
+ } catch {
54
+ return {};
55
+ }
56
+ }
57
+
58
+ function writeState(patch) {
59
+ const next = { ...readState(), ...patch, updatedAt: new Date().toISOString() };
60
+ const temp = `${STATE_FILE}.${process.pid}.tmp`;
61
+ try {
62
+ mkdirSync(join(STATE_FILE, '..'), { recursive: true, mode: 0o700 });
63
+ writeFileSync(temp, `${JSON.stringify(next, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
64
+ renameSync(temp, STATE_FILE);
65
+ return true;
66
+ } catch {
67
+ try { writeFileSync(temp, '', 'utf8'); } catch { /* ignore */ }
68
+ return false;
69
+ }
70
+ }
71
+
72
+ function resolveExistingDirectory(input) {
73
+ const expanded = expandHome(input);
74
+ if (!expanded) throw new Error('工作目录不能为空');
75
+ if (!isAbsolute(expanded)) throw new Error(`工作目录必须是绝对路径:${expanded}`);
76
+
77
+ const absolute = resolve(expanded);
78
+ if (!existsSync(absolute)) throw new Error(`工作目录不存在:${absolute}`);
79
+
80
+ let real;
81
+ try {
82
+ real = realpathSync.native(absolute);
83
+ } catch {
84
+ throw new Error(`无法解析工作目录:${absolute}`);
85
+ }
86
+
87
+ if (!statSync(real).isDirectory()) throw new Error(`工作目录不是文件夹:${absolute}`);
88
+ return real;
89
+ }
90
+
91
+ function isInside(target, root) {
92
+ return target === root || target.startsWith(`${root}${sep}`);
93
+ }
94
+
95
+ function createLimitedBuffer(limit) {
96
+ let text = '';
97
+ let totalChars = 0;
98
+
99
+ return {
100
+ append(value) {
101
+ const chunk = String(value ?? '');
102
+ totalChars += chunk.length;
103
+ if (text.length < limit) text += chunk.slice(0, limit - text.length);
104
+ },
105
+ finish() {
106
+ const trimmed = text.trim();
107
+ if (totalChars <= limit) return trimmed;
108
+ return `${trimmed}\n…(已截断:原始 ${totalChars} 字符,上限 ${limit} 字符)`;
109
+ },
110
+ };
111
+ }
112
+
113
+ function killProcessTree(child, signal) {
114
+ if (!child?.pid) return;
115
+ try {
116
+ process.kill(-child.pid, signal);
117
+ } catch {
118
+ try { child.kill(signal); } catch { /* 已退出 */ }
119
+ }
120
+ }
121
+
122
+ function getAbortSignal(context) {
123
+ if (context?.signal && typeof context.signal.addEventListener === 'function') return context.signal;
124
+ if (context?.abortSignal && typeof context.abortSignal.addEventListener === 'function') {
125
+ return context.abortSignal;
126
+ }
127
+ return null;
128
+ }
129
+
130
+ function runCodex(bin, argv, { cwd, timeoutMs, maxChars, signal }) {
131
+ return new Promise((resolvePromise) => {
132
+ const started = Date.now();
133
+ const stdout = createLimitedBuffer(maxChars);
134
+ const stderr = createLimitedBuffer(maxChars);
135
+ let child;
136
+ let timeoutTimer;
137
+ let hardKillTimer;
138
+ let finished = false;
139
+ let timedOut = false;
140
+ let aborted = false;
141
+
142
+ const cleanup = () => {
143
+ clearTimeout(timeoutTimer);
144
+ clearTimeout(hardKillTimer);
145
+ signal?.removeEventListener?.('abort', onAbort);
146
+ };
147
+
148
+ const finish = (code, spawnError = '') => {
149
+ if (finished) return;
150
+ finished = true;
151
+ cleanup();
152
+ const ok = code === 0 && !timedOut && !aborted && !spawnError;
153
+ resolvePromise({
154
+ ok,
155
+ code: timedOut ? 'timeout' : aborted ? 'aborted' : spawnError ? 'spawn-error' : code,
156
+ stdout: stdout.finish(),
157
+ stderr: stderr.finish(),
158
+ error: spawnError,
159
+ ms: Date.now() - started,
160
+ });
161
+ };
162
+
163
+ const onAbort = () => {
164
+ aborted = true;
165
+ killProcessTree(child, 'SIGTERM');
166
+ clearTimeout(hardKillTimer);
167
+ hardKillTimer = setTimeout(() => killProcessTree(child, 'SIGKILL'), 1_500);
168
+ };
169
+
170
+ if (signal?.aborted) {
171
+ aborted = true;
172
+ finish('aborted');
173
+ return;
174
+ }
175
+
176
+ try {
177
+ child = spawn(bin, argv, {
178
+ cwd,
179
+ detached: process.platform !== 'win32',
180
+ windowsHide: true,
181
+ env: { ...process.env, NO_COLOR: '1', CI: '1' },
182
+ stdio: ['ignore', 'pipe', 'pipe'],
183
+ });
184
+ } catch (error) {
185
+ finish('spawn-error', String(error?.message || error));
186
+ return;
187
+ }
188
+
189
+ child.stdout?.setEncoding('utf8');
190
+ child.stderr?.setEncoding('utf8');
191
+ child.stdout?.on('data', (chunk) => stdout.append(chunk));
192
+ child.stderr?.on('data', (chunk) => stderr.append(chunk));
193
+
194
+ child.on('error', (error) => finish('spawn-error', String(error?.message || error)));
195
+ child.on('close', (code, closeSignal) => {
196
+ finish(code ?? (closeSignal ? `signal:${closeSignal}` : 'unknown'));
197
+ });
198
+
199
+ timeoutTimer = setTimeout(() => {
200
+ timedOut = true;
201
+ killProcessTree(child, 'SIGTERM');
202
+ hardKillTimer = setTimeout(() => killProcessTree(child, 'SIGKILL'), 1_500);
203
+ }, timeoutMs);
204
+
205
+ signal?.addEventListener?.('abort', onAbort, { once: true });
206
+ });
207
+ }
208
+
209
+ function textTool({ name: toolName, description, parameters, timeoutMs, execute }) {
210
+ return {
211
+ name: toolName,
212
+ description,
213
+ parameters,
214
+ output: {
215
+ schema: { type: 'string' },
216
+ render: (_args, value) => [{ type: 'text', text: String(value ?? '') }],
217
+ },
218
+ ...(timeoutMs ? { timeoutMs } : {}),
219
+ execute,
220
+ };
221
+ }
222
+
223
+ export function apply(ctx, config = {}) {
224
+ if (config.enabled === false) {
225
+ ctx.logger?.info?.('[use-codex] disabled by configuration');
226
+ return;
227
+ }
228
+
229
+ const bin = expandHome(config.bin || process.env.CODEX_BIN || 'codex');
230
+ const defaultCwd = expandHome(config.defaultCwd || process.cwd());
231
+ const rawRoots = Array.isArray(config.allowedRoots) && config.allowedRoots.length
232
+ ? config.allowedRoots
233
+ : [defaultCwd];
234
+ const allowDanger = config.allowDangerFullAccess === true;
235
+ const maxTimeoutSec = clamp(
236
+ finiteNumber(config.timeoutSec, DEFAULT_TIMEOUT_SEC),
237
+ 1,
238
+ 3_600,
239
+ );
240
+ const maxOutputChars = Math.round(clamp(
241
+ finiteNumber(config.maxOutputChars, DEFAULT_MAX_OUTPUT_CHARS),
242
+ 1_000,
243
+ 200_000,
244
+ ));
245
+
246
+ const allowedRoots = [];
247
+ const rootWarnings = [];
248
+ for (const rawRoot of rawRoots) {
249
+ try {
250
+ const real = resolveExistingDirectory(rawRoot);
251
+ if (!allowedRoots.includes(real)) allowedRoots.push(real);
252
+ } catch (error) {
253
+ rootWarnings.push(String(error?.message || error));
254
+ }
255
+ }
256
+
257
+ const warnings = [...rootWarnings];
258
+ if (allowedRoots.length === 0) warnings.push('没有可用的白名单目录,codex_run 将拒绝执行');
259
+ try {
260
+ const realDefault = resolveExistingDirectory(defaultCwd);
261
+ if (!allowedRoots.some((root) => isInside(realDefault, root))) {
262
+ warnings.push('defaultCwd 不在白名单内,未显式提供 cwd 时 codex_run 将拒绝执行');
263
+ }
264
+ } catch (error) {
265
+ warnings.push(`defaultCwd 不可用:${String(error?.message || error)}`);
266
+ }
267
+
268
+ const enabledNow = () => readState().enabled !== false;
269
+ let lastRun = null;
270
+
271
+ const resolveRunCwd = (input) => {
272
+ const requested = String(input ?? '').trim() || defaultCwd;
273
+ const real = resolveExistingDirectory(requested);
274
+ if (!allowedRoots.some((root) => isInside(real, root))) {
275
+ throw new Error(`工作目录不在白名单内:${real}`);
276
+ }
277
+ return real;
278
+ };
279
+
280
+ const safeStatusCwd = () => {
281
+ try { return resolveRunCwd(defaultCwd); } catch { return tmpdir(); }
282
+ };
283
+
284
+ const tools = [
285
+ textTool({
286
+ name: 'codex_run',
287
+ description:
288
+ '让本机 OpenAI Codex CLI 执行一段独立任务并返回结果。'
289
+ + '当用户说“让 Codex 做……”“交给 Codex 执行……”,或希望把编码、排障、批量文件处理任务委派出去时调用。'
290
+ + '工作目录必须在 DSH 配置的白名单内;默认使用 workspace-write 沙箱;失败时如实返回失败原因。',
291
+ parameters: {
292
+ type: 'object',
293
+ additionalProperties: false,
294
+ properties: {
295
+ task: {
296
+ type: 'string',
297
+ minLength: 1,
298
+ maxLength: MAX_TASK_CHARS,
299
+ description: '完整、明确的任务说明,包含目标、约束和验收标准。',
300
+ },
301
+ cwd: {
302
+ type: 'string',
303
+ description: '可选的绝对工作目录,必须真实存在且位于白名单内;省略时使用 defaultCwd。',
304
+ },
305
+ sandbox: {
306
+ type: 'string',
307
+ enum: ['read-only', 'workspace-write', 'danger-full-access'],
308
+ description: 'Codex 沙箱模式,默认 workspace-write;danger-full-access 需宿主配置显式允许。',
309
+ },
310
+ timeoutSec: {
311
+ type: 'number',
312
+ minimum: 1,
313
+ description: '本次超时秒数,不超过插件配置的 timeoutSec。',
314
+ },
315
+ },
316
+ required: ['task'],
317
+ },
318
+ async execute(args, context) {
319
+ if (!enabledNow()) {
320
+ return '⛔ Codex 委派已关闭。请先用 codex_switch 打开。';
321
+ }
322
+
323
+ const task = String(args?.task ?? '').trim();
324
+ if (!task) return '❌ 任务不能为空。';
325
+ if (task.length > MAX_TASK_CHARS) return `❌ 任务过长,最多 ${MAX_TASK_CHARS} 字符。`;
326
+
327
+ const sandbox = String(args?.sandbox || 'workspace-write');
328
+ if (!['read-only', 'workspace-write', 'danger-full-access'].includes(sandbox)) {
329
+ return `❌ 不支持的沙箱模式:${sandbox}`;
330
+ }
331
+ if (sandbox === 'danger-full-access' && !allowDanger) {
332
+ return '❌ danger-full-access 未获宿主配置许可,拒绝执行。';
333
+ }
334
+
335
+ let cwd;
336
+ try {
337
+ cwd = resolveRunCwd(args?.cwd);
338
+ } catch (error) {
339
+ return `❌ ${String(error?.message || error)}`;
340
+ }
341
+
342
+ if (!existsSync(bin) && bin.includes(sep)) {
343
+ return `❌ 找不到 Codex 可执行文件:${bin}`;
344
+ }
345
+
346
+ const timeoutSec = clamp(
347
+ finiteNumber(args?.timeoutSec, maxTimeoutSec),
348
+ 1,
349
+ maxTimeoutSec,
350
+ );
351
+ const argv = [
352
+ 'exec',
353
+ '-C', cwd,
354
+ '-s', sandbox,
355
+ '--skip-git-repo-check',
356
+ '--ephemeral',
357
+ '--color', 'never',
358
+ '--',
359
+ task,
360
+ ];
361
+ const result = await runCodex(bin, argv, {
362
+ cwd,
363
+ timeoutMs: Math.round(timeoutSec * 1_000),
364
+ maxChars: maxOutputChars,
365
+ signal: getAbortSignal(context),
366
+ });
367
+
368
+ lastRun = {
369
+ at: new Date().toISOString(),
370
+ ok: result.ok,
371
+ code: result.code,
372
+ ms: result.ms,
373
+ sandbox,
374
+ };
375
+
376
+ const status = result.ok
377
+ ? `✅ Codex 执行完成(${(result.ms / 1_000).toFixed(1)}s,${sandbox})`
378
+ : `❌ Codex 执行失败(${result.code},${(result.ms / 1_000).toFixed(1)}s,${sandbox})`;
379
+ const sections = [status];
380
+ if (result.stdout) sections.push(`--- stdout ---\n${result.stdout}`);
381
+ if (result.stderr) sections.push(`--- stderr ---\n${result.stderr}`);
382
+ if (result.error) sections.push(`--- error ---\n${result.error}`);
383
+ if (sections.length === 1) sections.push('(无输出)');
384
+ return sections.join('\n\n');
385
+ },
386
+ }),
387
+
388
+ textTool({
389
+ name: 'codex_status',
390
+ description: '检查 Codex 委派插件的开关、Codex CLI 版本、认证状态、目录白名单和安全配置。',
391
+ parameters: {
392
+ type: 'object',
393
+ additionalProperties: false,
394
+ properties: {},
395
+ },
396
+ timeoutMs: 35_000,
397
+ async execute() {
398
+ const statusCwd = safeStatusCwd();
399
+ const version = await runCodex(bin, ['--version'], {
400
+ cwd: statusCwd,
401
+ timeoutMs: 15_000,
402
+ maxChars: 2_000,
403
+ });
404
+ const login = await runCodex(bin, ['login', 'status'], {
405
+ cwd: statusCwd,
406
+ timeoutMs: 15_000,
407
+ maxChars: 2_000,
408
+ });
409
+
410
+ const lines = [
411
+ `🔌 开关:${enabledNow() ? '打开' : '关闭'}`,
412
+ `📦 Codex:${version.ok ? version.stdout : `不可用(${version.error || version.stderr || version.code})`}`,
413
+ `🔑 认证:${login.stdout || login.stderr || (login.ok ? '未知' : login.code)}`,
414
+ `📁 白名单:${allowedRoots.length ? allowedRoots.join('、') : '(空)'}`,
415
+ `🛡 危险模式:${allowDanger ? '允许(危险)' : '禁止'}`,
416
+ `⏱ 超时上限:${maxTimeoutSec}s`,
417
+ `📏 输出上限:${maxOutputChars} 字符`,
418
+ ];
419
+ if (warnings.length) lines.push(`⚠️ 配置提示:${warnings.join(';')}`);
420
+ if (lastRun) {
421
+ lines.push(`🕘 本进程最近调用:${lastRun.at}|${lastRun.ok ? '成功' : '失败'}|${lastRun.ms}ms|${lastRun.sandbox}`);
422
+ }
423
+ return lines.join('\n');
424
+ },
425
+ }),
426
+
427
+ textTool({
428
+ name: 'codex_switch',
429
+ description: '即时打开或关闭 Codex 委派,不需要重启 DSH;关闭后 codex_run 会拒绝执行。',
430
+ parameters: {
431
+ type: 'object',
432
+ additionalProperties: false,
433
+ properties: {
434
+ enabled: {
435
+ type: 'boolean',
436
+ description: 'true 为打开,false 为关闭。',
437
+ },
438
+ },
439
+ required: ['enabled'],
440
+ },
441
+ timeoutMs: 10_000,
442
+ async execute(args) {
443
+ const enabled = args?.enabled === true;
444
+ if (!writeState({ enabled })) {
445
+ return '❌ 无法写入 Codex 委派状态,请检查 DSH 状态目录权限。';
446
+ }
447
+ return enabled
448
+ ? '🔓 Codex 委派已打开。'
449
+ : '🔒 Codex 委派已关闭;codex_run 从现在起拒绝执行。';
450
+ },
451
+ }),
452
+ ];
453
+
454
+ for (const tool of tools) {
455
+ const dispose = ctx.tools.register(tool);
456
+ if (typeof dispose === 'function') {
457
+ ctx.effect?.(() => dispose);
458
+ }
459
+ }
460
+
461
+ ctx.logger?.info?.(`[use-codex] registered ${tools.length} tools; allowed roots=${allowedRoots.length}; enabled=${enabledNow()}`);
462
+ }
463
+
464
+ export default { name, inject, apply };
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "dsh-use-codex",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "displayName": "DSH Use Codex",
4
+ "description": "通过 DeepSeek Harness 工具调用本机 Codex CLI 执行独立任务。",
5
+ "version": "0.3.0",
6
+ "type": "module",
7
+ "main": "lib/index.js",
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "license": "MIT",
13
+ "engines": {
14
+ "node": ">=20",
15
+ "dsh": ">=0.2.1-rc.1 <0.3.0"
16
+ },
17
+ "files": [
18
+ "lib/",
19
+ "README.md",
20
+ "README.en.md",
21
+ "SECURITY.md",
22
+ "LICENSE",
23
+ "cordis.patch.yml"
24
+ ],
25
+ "keywords": [
26
+ "dsh-plugin",
27
+ "deepseek-harness",
28
+ "codex",
29
+ "codex-cli",
30
+ "agent",
31
+ "delegation"
32
+ ],
33
+ "scripts": {
34
+ "test": "node selftest.mjs",
35
+ "prepack": "npm test"
36
+ },
37
+ "dsh": {
38
+ "bundle": {
39
+ "patch": "./cordis.patch.yml"
40
+ },
41
+ "install": {
42
+ "preferred": "tarball",
43
+ "tarball": "https://github.com/chaorenzeng/dsh-use-codex/archive/refs/heads/main.tar.gz",
44
+ "git": "https://github.com/chaorenzeng/dsh-use-codex.git"
45
+ }
46
+ },
47
+ "author": "chaorenzeng",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/chaorenzeng/dsh-use-codex.git"
51
+ },
52
+ "homepage": "https://github.com/chaorenzeng/dsh-use-codex#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/chaorenzeng/dsh-use-codex/issues"
55
+ }
56
+ }