@ckanner/dsh-plugin-flywheel 0.27.3 → 0.28.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/PROTOCOL.md +57 -4
- package/README.en.md +245 -0
- package/README.md +28 -7
- package/dist/codex-pro-bridge.js +60 -0
- package/dist/codex-pro-worker.js +117 -0
- package/package.json +10 -4
package/PROTOCOL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Go ↔ DSH worker 协议 v1
|
|
1
|
+
# Go ↔ DSH / Codex Pro worker 协议 v1
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
此文件记录两种独立 worker 协议。前半部分对应 `internal/drivers/dsh.go` 与官方 DSH SDK worker;[Codex Pro 部分](#codex-pro-worker协议)对应 `internal/drivers/codex_pro.go` 和插件 machine API worker。Go 负责状态机、验收、费用上限及最终进程判断;worker 仅执行一次有边界的调用。两者之间只有本地 stdio,不开放 HTTP 端口。
|
|
4
4
|
|
|
5
5
|
## 启动与输入
|
|
6
6
|
|
|
@@ -89,6 +89,58 @@ v1 每次调用使用全新 session。Go 重试时重新提交冻结任务输入
|
|
|
89
89
|
|
|
90
90
|
Go 的 `DSHArgs.Doctor` 使用这个入口;为保持 v1 必填 `prompt` 的 admission schema,它会用固定的探针说明替代调用者文本,worker 不将其交给 `session/prompt`。doctor 仍必须具备匹配的 ready、可靠 result、正常 SDK shutdown 与物理退出,不能因为没有推理而放松清理或权限检查。
|
|
91
91
|
|
|
92
|
+
## Codex Pro worker协议
|
|
93
|
+
|
|
94
|
+
公开入口为 `@ckanner/dsh-plugin-flywheel/codex-pro-worker`,可执行入口为 `flywheel-codex-pro-worker`。`FLYWHEEL_CODEX_PRO_WORKER` 允许显式覆盖;npm 启动器解析同版本依赖,不依靠碰巧可用的全局旧 worker。worker 实际导入已发布的 `dsh-subagent-codex-pro@0.8.0` 的 `./machine` API,严格检查包名、版本和机器协议 1。Node 最低 22.18.0,仅支持 macOS/Linux。
|
|
95
|
+
|
|
96
|
+
### 请求与父进程连接
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{"type":"start","protocol":1,"requestId":"call-123","cwd":"/absolute/worktree","model":"gpt-6.1-sol","reasoningEffort":"high","permissionMode":"workspace-write","prompt":"执行冻结工作单元","timeoutMs":1800000}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
只接受这些字段;不接受 provider、resume、threadId、额外写目录、任意 CLI 参数或危险权限模式。requestId 非空且最多 256 字符;cwd 为绝对路径,Go 先规范化成物理目录。model/reasoningEffort/prompt 必须是非空文本,permissionMode 仅 read-only/workspace-write。timeoutMs 显式值必须为 1–2,147,483,647 的安全整数,省略使用上游 600,000 毫秒;worker 不接受通过 0 关闭期限。Go 传递有效 wall deadline 和 high 默认 thinking;worker 将 reasoningEffort 映射到 machine 的 effort,固定 threadMode=ephemeral、command=`FLYWHEEL_CODEX_BIN` 或 `codex`,不恢复旧线程。
|
|
103
|
+
|
|
104
|
+
**这里与 DSH worker 的 EOF 语义不同:Go 写入一行后保持 stdin 开放。** stdin 是父进程的存活连接;未完成前 EOF、读取错误或第二条非空输入触发 AbortSignal,并等待插件有界清理。不能用 `echo … | worker` 的关闭 stdin 行为冒充正常调用。运行请求首行等待上限 10 秒;请求最多 1 MiB,machine 原始输出上限 1 MiB,序列化 progress 累计最多 1 MiB,完整终态帧最多 8 MiB。超限明确失败,不能裁掉回执后判成功。
|
|
105
|
+
|
|
106
|
+
### 进度与完整结果
|
|
107
|
+
|
|
108
|
+
零或多个进度帧:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{"protocol":1,"type":"progress","requestId":"call-123","text":"插件报告的活动"}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
最后恰好一个 result,字段结构如下,`receipt` 为插件实际完整对象:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{"protocol":1,"type":"result","requestId":"call-123","adapter":{"package":"dsh-subagent-codex-pro","version":"0.8.0","protocol":1},"receipt":null,"error":{"code":"INVALID_REQUEST","message":"启动请求未通过校验"}}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
启动前不能获得 machine receipt 时为 null。运行已开始则即使失败也保留完整 receipt;`error` 非 null、重复/丢失 result、尾部垃圾、请求身份或 adapter 版本不符都不能成功。本通道没有 DSH `ready` 帧,不能据此虚构运行前完整沙箱握手或实际 effort 回显。
|
|
121
|
+
|
|
122
|
+
Go 同时校验外层 worker 物理终态和插件 receipt:请求及 run/thread/turn 身份、模型、cwd、approval=never、实际沙箱模式、完整 sandboxPolicy、stopReason、cleanup 与执行事实。workspaceWrite 的显式 writableRoots 必须处于工作区内,networkAccess 必须明确 false;Codex 标准临时目录标记保留,不宣称只能写 cwd。插件 0.8.0 只在 turn 前检查模式等基本配置;完整策略是返回后的交付资格检查,**不能阻止自定义 Codex 配置已经允许的访问**。
|
|
123
|
+
|
|
124
|
+
effort 的证据只可为 turn/start 请求接受,保留 `effectiveEffort:null`。usage 使用 `run` 增量,不能把 threadTotal 当成本次消耗;只有 complete 才记入可知预算,partial/unknown 原样存入 receipt 并增加未知调用计数。cached input/reasoning output 是总 input/output 的子项,不重复累加。订阅 costUSD 保持 null、costStatus 保持 unknown。receipt 没有实际 provider 时保持未知,不根据 channel 发明 provider。
|
|
125
|
+
|
|
126
|
+
### 取消与进程终态
|
|
127
|
+
|
|
128
|
+
worker 捕获 SIGINT/SIGTERM/SIGHUP,先取消并等待插件返回清理结果,再分别退出 130/143/129;父连接断开为失败。Go 为 Codex Pro worker 保留至少 10 秒终止宽限,全局 kill switch 不提前缩短该子进程的宽限。插件 guardian 拥有 app-server 进程组;worker 意外死亡由插件自己的连接与 guardian 负责终止其拥有的进程。Go 未收到完整回执时不能把内部清理记为已确认,主动脱离拥有进程组的外部程序不属于此保证。
|
|
129
|
+
|
|
130
|
+
已启动 worker 后缺少可信 `cleanup.confirmed` 会记录 `CleanupError`,即使外层进程已退出。引擎和建议模块保存诊断后停止本次运行,不执行传输重试、渠道降级、Plan 冷启动或下一任务。完整回执中确认清理的普通服务错误仍使用原重试策略。恢复前先确认该次调用遗留的进程已停止;等待固定秒数不构成清理确认。
|
|
131
|
+
|
|
132
|
+
正常完成后 app-server 仍可能需要 TERM/KILL 收尾。只有 completed、cleanup.confirmed、无取消/超时/截断/失败事实且外层 worker 正常退出时,才允许 receipt 中正常 terminationRequested 导致的信号或未知 Codex exit code。不能将 supervisor exit code 冒充模型进程的物理退出码,也不能把文本答案当终态证明。
|
|
133
|
+
|
|
134
|
+
### 无推理诊断
|
|
135
|
+
|
|
136
|
+
`flywheel-codex-pro-worker --doctor` 不读取 stdin,不调用 runMachine,不启动 app-server,仅导入并核验公开接口。成功返回单个 JSON:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{"protocol":1,"type":"doctor","package":"dsh-subagent-codex-pro","version":"0.8.0","machineProtocol":1,"available":true,"inferenceTested":false}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Go doctor 同时检查 Node 与 Codex 命令。包导入成功不证明账号、模型、完整策略或真实推理能力,`--probe` 也不会对 Codex Pro 发模型任务。
|
|
143
|
+
|
|
92
144
|
## 桌面控制响应(独立于 worker JSONL)
|
|
93
145
|
|
|
94
146
|
桌面控制层调用 Go 的 `control` 命令,只接收一个 JSON 文档,不把它与上述 worker 流混用。原 `start/status/stop` 字段不变,新增两种 action:
|
|
@@ -124,7 +176,8 @@ config/doctor 的 Go CLI 将 SIGTERM/SIGINT 转为 context 取消,等待 watch
|
|
|
124
176
|
| `steps` | 顺序记录 `global-cli`、`skills`、`desktop-launcher`、`dsh-plugin`、`preflight`;失败时只含已开始的步骤 |
|
|
125
177
|
| `ready` | 本地 doctor 是否无阻断项;诊断未完成时为 `false` |
|
|
126
178
|
| `authentication` | 固定 `unknown`,没有自动登录或真实模型请求 |
|
|
127
|
-
| `prefix`、`paths` | 得到后才记录;paths 包含 CLI、原生二进制、worker 与生成的 launcher 的绝对路径 |
|
|
179
|
+
| `prefix`、`paths` | 得到后才记录;paths 包含 CLI、原生二进制、DSH worker、codexProWorker 与生成的 launcher 的绝对路径 |
|
|
180
|
+
| `adapter` | 实际导入通过后记录 Codex Pro 包名、精确 0.8.0、protocol 1 与 inferenceTested=false;不代表账号已就绪 |
|
|
128
181
|
| `skills` | 安装成功时的数量及准确目录;每项结果保存在 skills 步骤的 `summary` |
|
|
129
182
|
| `diagnostic` | 合法的完整本地 doctor 报告,包括失败检查项;不执行 `--probe` |
|
|
130
183
|
| `error`、`cancelledBy` | 失败原因、可选取消信号;不是成功结果的一部分 |
|
|
@@ -139,7 +192,7 @@ config/doctor 的 Go CLI 将 SIGTERM/SIGINT 转为 context 取消,等待 watch
|
|
|
139
192
|
|
|
140
193
|
### GUI 安装记录
|
|
141
194
|
|
|
142
|
-
安装器生成 `~/.config/flywheel/installation.json`(0600)与 `npm-launcher`(0755)。记录包含 `protocol:1`、`owner:"@ckanner/flywheel"`、`version`、`prefix`、`cli`、`worker`、`node` 和 `launcher:{path,sha256}
|
|
195
|
+
安装器生成 `~/.config/flywheel/installation.json`(0600)与 `npm-launcher`(0755)。记录包含 `protocol:1`、`owner:"@ckanner/flywheel"`、`version`、`prefix`、`cli`、`worker`、`codexProWorker`、`adapter`、`node` 和 `launcher:{path,sha256}`。它只描述软件入口,不保存账号凭证,不构成第二份任务状态。旧记录仍按原字段读取,升级 setup 生成新字段;用户修改的 launcher 继续受保护。
|
|
143
196
|
|
|
144
197
|
launcher 使用绝对 Node 与 CLI 路径,设置安装时可见的绝对工具 PATH 目录,并在用户没有显式设置 `FLYWHEEL_DSH_BIN` 时使用已核对的桌面命令。npm CLI 启动器解析版本一致的原生包和 worker,缺失或版本不匹配时拒绝继续,不隐式下载另一份二进制。临时 npx 缓存与 `node_modules/.bin` 不被记录成持久工具 PATH。
|
|
145
198
|
|
package/README.en.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Flywheel for DSH
|
|
2
|
+
|
|
3
|
+
[简体中文](README.md) · [Project overview](https://github.com/ckanner/flywheel/blob/main/README.en.md) · [Getting started](https://github.com/ckanner/flywheel/blob/main/docs/en/getting-started.md)
|
|
4
|
+
|
|
5
|
+
This package connects the DSH desktop to Flywheel's deterministic Go engine and
|
|
6
|
+
provides its DSH SDK and Codex Pro workers. The engine owns task state, budgets,
|
|
7
|
+
verification, Git delivery, and process lifecycles. The desktop is a control and
|
|
8
|
+
observation interface; it does not need a parent model session to keep a task alive.
|
|
9
|
+
|
|
10
|
+
This guide describes the **Flywheel 0.28.0** interfaces. Source availability does
|
|
11
|
+
not mean that npm `latest` already contains them. Check the registry version and
|
|
12
|
+
the [release guide](https://github.com/ckanner/flywheel/blob/main/docs/en/releasing.md) before installing. The desktop
|
|
13
|
+
compatibility baseline is **DSH Mac 0.2.1-rc.5**, not a claim that every DSH build
|
|
14
|
+
or provider adapter is compatible.
|
|
15
|
+
|
|
16
|
+
## Components
|
|
17
|
+
|
|
18
|
+
| Component | Responsibility |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Flywheel panel | Inspect configuration, run local checks, and start, observe, or stop a task |
|
|
21
|
+
| `/flywheel` command | Invoke the Go control interface from an explicit desktop command |
|
|
22
|
+
| `flywheel_control` tool | Expose the same control operations to the host model |
|
|
23
|
+
| `flywheel-dsh-worker` | Run one native model session through an independent DSH SDK profile |
|
|
24
|
+
| `flywheel-codex-pro-worker` | Call the public machine API of the pinned `dsh-subagent-codex-pro@0.8.0` package |
|
|
25
|
+
|
|
26
|
+
The plugin has no separate task database and does not write `state.json`.
|
|
27
|
+
Kimi CLI, Codex Pro, and the explicit legacy Codex CLI path have separate Go
|
|
28
|
+
drivers. A DSH LLM provider and an external agent tool are different interfaces.
|
|
29
|
+
|
|
30
|
+
## Install the complete application
|
|
31
|
+
|
|
32
|
+
You need **Node.js 22.18.0 or later**, npm, and the existing DSH Mac desktop with
|
|
33
|
+
its desktop-installed `dsh` command. Verify the desktop version and the command
|
|
34
|
+
you are actually running. Do not guess an executable path inside an application
|
|
35
|
+
bundle.
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm view @ckanner/flywheel version
|
|
39
|
+
npx -y @ckanner/flywheel@latest install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The explicit installer installs the resolved Flywheel version, the matching Go
|
|
43
|
+
platform binary and workers, and ten embedded skills. It creates a desktop
|
|
44
|
+
launcher, registers the same plugin version with the desktop profile, and checks
|
|
45
|
+
the installed versions. It uses the desktop's plugin management interface:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
dsh plugin --profile desktop add --save-exact --ignore-scripts @ckanner/dsh-plugin-flywheel@<resolved-version>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The standalone npm DSH CLI cannot manage the reserved `desktop` profile. If the
|
|
52
|
+
desktop command is not on PATH, pass `--dsh-bin /absolute/path/to/dsh` to the
|
|
53
|
+
Flywheel installer. Reload or restart as instructed by the host.
|
|
54
|
+
|
|
55
|
+
Use `--prefix "$HOME/.local"` if you need a writable installation prefix, and add
|
|
56
|
+
its `bin` directory to your shell PATH. `--no-dsh` explicitly skips desktop
|
|
57
|
+
registration. Go CLI packages are provided for macOS and Linux, on arm64 and
|
|
58
|
+
x64; Linux package availability does not establish support for the Mac desktop
|
|
59
|
+
or its sandbox. See the [installation guide](https://github.com/ckanner/flywheel/blob/main/docs/en/getting-started.md)
|
|
60
|
+
for the full options and supported workflow.
|
|
61
|
+
|
|
62
|
+
Software installation and model readiness are separate. With `--json`,
|
|
63
|
+
`ok: true` means installation succeeded; `ready: false` can still report missing
|
|
64
|
+
Kimi or Codex executables or other local prerequisites. Authentication and actual
|
|
65
|
+
inference remain unverified. Flywheel does not install or update DSH, sign in to
|
|
66
|
+
model services, copy credentials, or provide subscription access.
|
|
67
|
+
|
|
68
|
+
Ordinary `npm install --global @ckanner/flywheel` only installs package files.
|
|
69
|
+
It has no postinstall hook that changes your profile or skills. To finish setup
|
|
70
|
+
for an existing persistent installation, run:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
flywheel setup --no-global
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Installing `@ckanner/dsh-plugin-flywheel` alone gives you this plugin and its
|
|
77
|
+
workers, not the complete Flywheel application.
|
|
78
|
+
|
|
79
|
+
## Prepare the SDK profile and selected models
|
|
80
|
+
|
|
81
|
+
The native DSH worker uses `sdk` by default. A provider installed or authorized
|
|
82
|
+
in the desktop profile is not automatically present in the SDK profile.
|
|
83
|
+
`FLYWHEEL_DSH_PROFILE` can select another SDK profile, but cannot select
|
|
84
|
+
`desktop`. Keep `DSH_HOME` consistent with the configuration you intend to use.
|
|
85
|
+
|
|
86
|
+
The native DeepSeek provider is `deepseek-official`, with `deepseek-flash` in
|
|
87
|
+
the default configuration. At the checked rc.5 baseline its effort values are
|
|
88
|
+
`off`, `low`, `high`, and `max`; there is no `medium`. Other providers have their
|
|
89
|
+
own accepted values. For native GPT routes, install and authorize the selected
|
|
90
|
+
ChatGPT adapter in the SDK profile. One checked compatibility example is:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
dsh plugin --profile sdk add dsh-plugin-chatgpt@0.1.17
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This example does not migrate desktop credentials or prove that your account
|
|
97
|
+
can use every model in Flywheel's configuration. Confirm the exact model IDs
|
|
98
|
+
and effort values against the selected provider. The
|
|
99
|
+
[configuration guide](https://github.com/ckanner/flywheel/blob/main/docs/en/configuration.md) explains profiles,
|
|
100
|
+
provider IDs, account channels, and existing-configuration migration.
|
|
101
|
+
|
|
102
|
+
The default `hard` route uses `harness: "codex-pro"`. Its worker directly imports
|
|
103
|
+
`dsh-subagent-codex-pro/machine`; the package runs Codex app-server using your
|
|
104
|
+
existing Codex installation and login. It does not ask a DSH parent model to
|
|
105
|
+
delegate again. `subagent_codex_pro` is not an LLM provider ID, and this route does
|
|
106
|
+
not silently fall back to `codex exec`.
|
|
107
|
+
|
|
108
|
+
| Setting | Meaning |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `FLYWHEEL_DSH_BIN` | Explicit DSH executable; otherwise use the configured launcher environment or PATH |
|
|
111
|
+
| `FLYWHEEL_DSH_PROFILE` | Independent SDK profile, default `sdk` |
|
|
112
|
+
| `DSH_HOME` | Optional DSH configuration root |
|
|
113
|
+
| `FLYWHEEL_DSH_WORKER` | Explicit DSH worker override; the npm launcher normally resolves the matching dependency |
|
|
114
|
+
| `FLYWHEEL_CODEX_PRO_WORKER` | Explicit Codex Pro worker override; normally resolved by the npm launcher |
|
|
115
|
+
| `FLYWHEEL_CODEX_BIN` | Codex executable used by the plugin, default `codex` |
|
|
116
|
+
| `CODEX_HOME` | Optional existing Codex configuration and login directory |
|
|
117
|
+
| `FLYWHEEL_KIMI_BIN` | Kimi executable for routes that use its CLI, default `kimi` |
|
|
118
|
+
|
|
119
|
+
The selected executables and configuration must be visible to the process that
|
|
120
|
+
starts the engine. A shell export does not update an already running desktop.
|
|
121
|
+
|
|
122
|
+
## Check the installation without inference
|
|
123
|
+
|
|
124
|
+
Run these from the task's integration worktree, after preparing and reviewing
|
|
125
|
+
its contracts. Replace the task name with your own:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
flywheel config inspect --cwd "$PWD" --task-dir "$PWD/docs/my-task" --json
|
|
129
|
+
flywheel doctor --cwd "$PWD" --task-dir "$PWD/docs/my-task" --json
|
|
130
|
+
flywheel doctor --cwd "$PWD" --task-dir "$PWD/docs/my-task" --probe --json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Omit `--task-dir` to inspect the general configuration. Ordinary doctor checks
|
|
134
|
+
local configuration and commands. `--probe` additionally initializes the DSH
|
|
135
|
+
SDK, checks its working directory and permission enforcement, and closes the
|
|
136
|
+
session. Provider initialization may query a model catalog or create SDK
|
|
137
|
+
session/cache files, but it does not send an inference prompt or run task
|
|
138
|
+
acceptance commands. It does not write Flywheel state or create worktrees.
|
|
139
|
+
|
|
140
|
+
For Codex Pro, doctor imports the pinned machine API and checks local prerequisites;
|
|
141
|
+
it does not start an inference task. The worker's narrower package check is:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
flywheel-codex-pro-worker --doctor
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Its successful response reports `available: true` and `inferenceTested: false`.
|
|
148
|
+
Neither check proves account authorization, quota, model availability, or an
|
|
149
|
+
actual inference result. Keep `warn`, `unknown`, and `skipped` distinct from
|
|
150
|
+
`pass`. A blocking diagnostic returns exit code 1 with the report intact.
|
|
151
|
+
|
|
152
|
+
## Use the desktop controls
|
|
153
|
+
|
|
154
|
+
Open the **Flywheel** panel below the DSH session input. Select the absolute
|
|
155
|
+
task worktree path as the repository directory and the task directory inside
|
|
156
|
+
that same worktree. Prepare and review the contracts first; the panel does not
|
|
157
|
+
turn an empty directory into a complete task plan.
|
|
158
|
+
|
|
159
|
+
The panel, `/flywheel`, and `flywheel_control` share these Go operations:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
flywheel control start --cwd "$PWD" --task-dir "$PWD/docs/my-task"
|
|
163
|
+
flywheel control status --cwd "$PWD" --task-dir "$PWD/docs/my-task"
|
|
164
|
+
flywheel control stop --cwd "$PWD" --task-dir "$PWD/docs/my-task"
|
|
165
|
+
flywheel control config --cwd "$PWD"
|
|
166
|
+
flywheel control doctor --cwd "$PWD" --task-dir "$PWD/docs/my-task" --probe
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Starting a real task calls the selected models. `start` launches a background
|
|
170
|
+
engine; closing the panel or desktop is not a stop request. Refresh status to
|
|
171
|
+
observe it, and use `stop` to request shutdown. `ok: true` describes a successful
|
|
172
|
+
control operation; task completion is recorded in `state.status`.
|
|
173
|
+
|
|
174
|
+
Use one task integration worktree for preparation, planning, milestone
|
|
175
|
+
verification, and the eventual PR. The engine manages packet worktrees for
|
|
176
|
+
execution, L1, and review. A successful packet is merged into the branch of the
|
|
177
|
+
checkout from which the engine was started. The engine does not push, open
|
|
178
|
+
GitHub PRs, merge remote PRs, or publish packages. See the
|
|
179
|
+
[worktree and PR workflow](https://github.com/ckanner/flywheel/blob/main/docs/en/workflow.md).
|
|
180
|
+
|
|
181
|
+
## Permissions and lifecycle boundaries
|
|
182
|
+
|
|
183
|
+
The DSH worker requests `read-only` or `workspace-write`, fixes approvals to
|
|
184
|
+
`never`, and verifies the effective SDK policy before sending the task prompt.
|
|
185
|
+
On the checked Mac host, the DSH sandbox uses its Seatbelt runner and filesystem
|
|
186
|
+
fence. Workspace writes include DSH's permitted temporary area; this is not a
|
|
187
|
+
network-isolation guarantee. Missing or incomplete enforcement is a failure,
|
|
188
|
+
not a reason to switch to unrestricted access.
|
|
189
|
+
|
|
190
|
+
Codex Pro uses fresh ephemeral calls and checks the model, directory, approval
|
|
191
|
+
policy, and sandbox mode before execution. In version 0.8.0, the full
|
|
192
|
+
`sandboxPolicy` reaches Flywheel only in the returned receipt. Flywheel rejects
|
|
193
|
+
delivery when that policy permits unexpected writable roots or network access,
|
|
194
|
+
but **this check happens after execution and cannot undo access already
|
|
195
|
+
permitted by custom Codex configuration**. It is not a complete pre-execution
|
|
196
|
+
policy guard. Kimi CLI print mode likewise does not provide an OS sandbox that
|
|
197
|
+
this adapter can promise.
|
|
198
|
+
|
|
199
|
+
A worktree separates Git working files and index state; it is not a security
|
|
200
|
+
sandbox. The current engine can fall back to `in-place` if packet worktree
|
|
201
|
+
creation fails. If the task requires strict workspace separation, stop and
|
|
202
|
+
resolve that condition before continuing. Restarting alone does not convert
|
|
203
|
+
saved `in-place` state into a new isolated worktree.
|
|
204
|
+
|
|
205
|
+
Go owns worker shutdown; Codex Pro also owns a separate guardian/app-server
|
|
206
|
+
lifecycle. Text output or the outer worker exiting does not prove that every
|
|
207
|
+
owned process has stopped. When cleanup cannot be confirmed, Flywheel preserves
|
|
208
|
+
the workspace and audit evidence and stops automatic retries, fallback, cold
|
|
209
|
+
restarts, and later work in that run. Confirm the owned processes have ended
|
|
210
|
+
before recovery. A successful stop operation is not proof that an arbitrary
|
|
211
|
+
escaped process has been cleaned up.
|
|
212
|
+
|
|
213
|
+
Complete receipts preserve configuration, usage, and termination evidence.
|
|
214
|
+
Accepted effort is not proof of the server's effective reasoning effort.
|
|
215
|
+
Unreported usage and dollar cost remain unknown; subscriptions are not recorded
|
|
216
|
+
as free calls. The current DSH bridge does not support session resume or image
|
|
217
|
+
and attachment inputs. Its protocol and implementation details are documented
|
|
218
|
+
in [PROTOCOL.md](PROTOCOL.md) (Chinese).
|
|
219
|
+
|
|
220
|
+
## Upgrade and maintain the desktop entry
|
|
221
|
+
|
|
222
|
+
The installer records an absolute Node/CLI launcher under
|
|
223
|
+
`~/.config/flywheel/`, including its content hash and the checked tool paths.
|
|
224
|
+
An explicit plugin `flywheelBinary` setting takes precedence. Without an
|
|
225
|
+
explicit setting, the plugin uses a valid installation record; only a missing
|
|
226
|
+
record permits legacy PATH lookup. A damaged record or changed launcher fails
|
|
227
|
+
visibly instead of selecting a different engine.
|
|
228
|
+
|
|
229
|
+
After upgrading Flywheel, moving the npm prefix, changing Node, or adding a
|
|
230
|
+
required CLI, run `flywheel setup --no-global` from a terminal where those tools
|
|
231
|
+
are available. Retain your chosen prefix and DSH executable options. Reload the
|
|
232
|
+
desktop as needed and run its checks again. Custom configuration and modified
|
|
233
|
+
skills are preserved and reported as conflicts; save your changes and resolve
|
|
234
|
+
the conflict rather than deleting their ownership records.
|
|
235
|
+
|
|
236
|
+
Existing Flywheel configuration is not automatically migrated. To adopt the new
|
|
237
|
+
plugin path from 0.27.x, change the effective hard-route harness from `codex` to
|
|
238
|
+
`codex-pro`, including any referenced profile, and inspect the expanded result.
|
|
239
|
+
The full upgrade and removal procedure is in
|
|
240
|
+
[Getting started](https://github.com/ckanner/flywheel/blob/main/docs/en/getting-started.md).
|
|
241
|
+
|
|
242
|
+
For source changes and offline verification, read the
|
|
243
|
+
[contribution guide](https://github.com/ckanner/flywheel/blob/main/docs/contributing.md). The repository currently uses
|
|
244
|
+
`UNLICENSED`; an npm release or a future public repository does not by itself
|
|
245
|
+
establish an open-source license.
|
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Flywheel 的 DSH 桌面入口与原生 worker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.en.md) · 简体中文
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
目标是 **DSH Mac 桌面 0.2.1-rc.5** 的已存在接口。Go 继续管理状态机、任务、worktree、验收和成本上限;本包提供桌面操作入口、原生 SDK 执行通道和 Codex Pro machine API worker。无需 alpha、OAR、Forge 或一直活着的父智能体。
|
|
6
|
+
|
|
7
|
+
完整的 npm 安装、SDK 配置到首个任务流程见 [Mac 入门教程](../../docs/getting-started.md),适用于 Flywheel **0.28.0 及以后的完整 npm 发行版**。安装前用 `npm view @ckanner/flywheel version` 确认 registry 的 latest 已更新到所需版本;源码合并或 GitHub Release 不代表 npm 已发行。旧 0.22.1 没有本教程使用的安装与 DSH 接口,当前发行状态见[发行记录](../../docs/releasing.md#3-失败恢复)。
|
|
6
8
|
|
|
7
9
|
## 提供什么
|
|
8
10
|
|
|
@@ -12,13 +14,14 @@
|
|
|
12
14
|
| `/flywheel` | 人工 slash command,通过已有 DSH commands Remote 调用 Go CLI;不会将控制指令交给模型执行 |
|
|
13
15
|
| `flywheel_control` | 模型工具,复用同一个校验与 CLI 控制路径;后台 start 很快返回,stop 等待引擎确认 |
|
|
14
16
|
| `flywheel-dsh-worker` | 独立 Node 进程,通过官方 `dsh --profile sdk` stdio JSON-RPC 执行一个原生模型会话 |
|
|
15
|
-
| worker policy | 发请求前验证 cwd、实际权限和完整沙箱,自动拒绝需要批准的扩权 |
|
|
17
|
+
| DSH worker policy | 发请求前验证 cwd、实际权限和完整沙箱,自动拒绝需要批准的扩权 |
|
|
18
|
+
| `flywheel-codex-pro-worker` | 直接导入 `dsh-subagent-codex-pro@0.8.0` 的公开 `./machine` 入口,传递任务与取消并返回完整插件回执 |
|
|
16
19
|
|
|
17
|
-
插件没有自己的任务数据库,不直接读写 `state.json`。桌面退出不会让后台 Go 任务失去父智能体;Go 拥有 worker 生命周期。原生 SDK 入口只接收已注册的 LLM adapter provider;`subagent_codex_pro`、Kimi ACP、Teams 都不是此接口的 provider
|
|
20
|
+
插件没有自己的任务数据库,不直接读写 `state.json`。桌面退出不会让后台 Go 任务失去父智能体;Go 拥有 worker 生命周期。原生 SDK 入口只接收已注册的 LLM adapter provider;`subagent_codex_pro`、Kimi ACP、Teams 都不是此接口的 provider。Kimi CLI、Codex Pro machine API 和旧显式 Codex CLI 使用各自 Go driver;Codex Pro 复用同一插件 runner,不经过 DSH 父模型或工具名转发。
|
|
18
21
|
|
|
19
22
|
## 一条命令安装到自己的 Mac
|
|
20
23
|
|
|
21
|
-
已有 Node.js 22 或更高、npm 和 DSH Mac 桌面后,先确认 About、桌面安装的 `dsh --version` 与实际运行入口,再运行:
|
|
24
|
+
已有 Node.js 22.18.0 或更高、npm 和 DSH Mac 桌面后,先确认 About、桌面安装的 `dsh --version` 与实际运行入口,再运行:
|
|
22
25
|
|
|
23
26
|
```sh
|
|
24
27
|
npx -y @ckanner/flywheel@latest install
|
|
@@ -71,6 +74,24 @@ reasoningEffort = off | low | high | max
|
|
|
71
74
|
|
|
72
75
|
`high` 是默认平衡档,**没有 `medium`**。其他 provider 使用自身的合法 effort。SDK README 的历史示例可能仍写 `deepseek-v4-flash`;以所用发行包与 adapter 的实际 catalog 为准。本桥接不替换模型 slug、不跨 provider 降级。
|
|
73
76
|
|
|
77
|
+
## Codex Pro 插件执行通道
|
|
78
|
+
|
|
79
|
+
默认 `hard` 路由使用 `harness: "codex-pro"`。本包精确依赖 `dsh-subagent-codex-pro@0.8.0`,独立 worker 直接导入公开的 `dsh-subagent-codex-pro/machine`,将模型、effort、工作目录和权限传给插件。DSH 控制面、Go 状态机、插件 runner 各自保持原职责;不需要桌面父模型二次委派,也不会自动回退旧 `codex exec`。
|
|
80
|
+
|
|
81
|
+
npm 启动器自动解析并注入 `FLYWHEEL_CODEX_PRO_WORKER`。独立 Go 二进制默认从 PATH 查找 `flywheel-codex-pro-worker`,源码开发可显式设为构建后的 `dist/codex-pro-worker.js`。`FLYWHEEL_CODEX_BIN` 可指定底层 Codex 可执行程序,默认 `codex`;`CODEX_HOME` 沿用用户选择的配置/登录目录。Go 会将这些运行必需项交给插件,不把凭证复制到配置、契约或 Git。
|
|
82
|
+
|
|
83
|
+
新调用固定使用 ephemeral 会话和 `read-only`/`workspace-write`,approval 为 never。插件运行前核对模型、cwd 和权限模式;完整 sandboxPolicy 由 Flywheel 在返回后审查,允许 Codex 标准临时目录、拒绝工作区外额外 roots 与网络启用。0.8.0 尚无公开的运行前完整策略 guard,事后拒绝交付不能阻止先前的访问;可信 Codex 配置仍是本机运行前提。
|
|
84
|
+
|
|
85
|
+
完整 machine receipt 写入指标,保留 accepted effort、未知 effectiveEffort、usage 完整性与进程终态。插件正常收尾可能主动 TERM/KILL app-server,其物理 exit code 可以为空;Flywheel 只有在完整终态证明任务完成、清理已确认且外层 worker 干净退出时才能继续。取消、超时、截断、启动或清理失败均阻止成功。Go 为此 worker 预留至少 10 秒终止宽限,避免截断插件内部清理。
|
|
86
|
+
|
|
87
|
+
仅检查包入口、不调用模型可运行:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
flywheel-codex-pro-worker --doctor
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
成功输出单行协议 1 JSON,精确版本 0.8.0,`available:true`、`inferenceTested:false`。它不启动 Codex app-server,不读取登录凭证,不证明账号或实际沙箱可用。日常仍应使用下面的 Go doctor,以核对完整配置来源与所需阶段。
|
|
94
|
+
|
|
74
95
|
## 无模型请求的 doctor
|
|
75
96
|
|
|
76
97
|
日常使用 Go 命令或面板;Go 会解析实际配置,避免手工输入与任务绑定不一致:
|
|
@@ -144,7 +165,7 @@ worker 只支持 `read-only` 与 `workspace-write`。Mac 上由 DSH 的 Seatbelt
|
|
|
144
165
|
|
|
145
166
|
卸载先停止活动任务,在 DSH 的所用 profile 移除该插件并重新载入,再执行 `npm uninstall --global @ckanner/flywheel`,自定义 prefix 时加同一参数。npm 卸载不自动删除用户配置、skills、任务文件或安装记录,也不删除模型工具及凭证。
|
|
146
167
|
|
|
147
|
-
开发构建需要 Node.js 22 或更高;Go 引擎还需要 Go 1.27
|
|
168
|
+
开发构建需要 Node.js 22.18.0 或更高;Go 引擎还需要 Go 1.27 或更高。生产插件精确依赖 `dsh-subagent-codex-pro@0.8.0`;不打包 DSH、React 或 Codex 可执行程序;可选 peer 元数据约束桌面接口版本。React 在浏览器端从 DSH 的 ModuleLoader 获取。`npm ci` 安装锁定的 React 18、ReactDOM 与 jsdom **开发依赖**,用于真实 DOM 测试,它们不进入发布包。在仓库根运行:
|
|
148
169
|
|
|
149
170
|
```sh
|
|
150
171
|
npm --prefix plugins/dsh ci --ignore-scripts
|
|
@@ -155,7 +176,7 @@ npm run build:npm
|
|
|
155
176
|
FLYWHEEL_NPM_PACKAGES_DIR=dist-npm npm run test:install
|
|
156
177
|
```
|
|
157
178
|
|
|
158
|
-
`build:npm` 在隔离源码副本中构建 worker 和四个平台 Go 二进制,输出六个同版本 npm 包与候选校验记录,包含 `ckanner-dsh-plugin-flywheel-0.
|
|
179
|
+
`build:npm` 在隔离源码副本中构建 worker 和四个平台 Go 二进制,输出六个同版本 npm 包与候选校验记录,包含 `ckanner-dsh-plugin-flywheel-0.28.0.tgz`。所有包的许可沿用仓库 `UNLICENSED`。tarball/Actions artifact 用于开发或备用验证;正式发布必须使用已经过门禁与真实 npm 安装测试的同一候选,不能在测试后重新编译再发布。上述测试不调用真实模型,也不使用用户账号或桌面 profile。
|
|
159
180
|
|
|
160
181
|
## 实现依据
|
|
161
182
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Public machine API consumer. Go remains the owner of scheduling and acceptance.
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
export const ADAPTER = Object.freeze({ package: 'dsh-subagent-codex-pro', version: '0.8.0', protocol: 1 });
|
|
5
|
+
export const LIMITS = Object.freeze({ requestBytes: 1024 * 1024, machineOutputBytes: 1024 * 1024,
|
|
6
|
+
progressBytes: 1024 * 1024, receiptFrameBytes: 8 * 1024 * 1024, admissionMs: 10000 });
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
export class WorkerError extends Error {
|
|
10
|
+
constructor(code, message) { super(message); this.name = 'WorkerError'; this.code = code; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The doctor executes this same import, but never calls runMachine. */
|
|
14
|
+
export async function loadAdapter() {
|
|
15
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
16
|
+
if (major < 22 || (major === 22 && minor < 18)) throw new WorkerError('UNSUPPORTED_RUNTIME', 'Codex Pro requires Node.js 22.18.0 or newer');
|
|
17
|
+
if (!['darwin', 'linux'].includes(process.platform)) throw new WorkerError('UNSUPPORTED_RUNTIME', 'Codex Pro requires a supported POSIX platform');
|
|
18
|
+
let manifest, api;
|
|
19
|
+
try {
|
|
20
|
+
manifest = require('dsh-subagent-codex-pro/package.json');
|
|
21
|
+
} catch {
|
|
22
|
+
throw new WorkerError('ADAPTER_UNAVAILABLE', 'Install the exact dsh-subagent-codex-pro@0.8.0 dependency with Flywheel');
|
|
23
|
+
}
|
|
24
|
+
if (manifest.name !== ADAPTER.package || manifest.version !== ADAPTER.version) {
|
|
25
|
+
throw new WorkerError('ADAPTER_MISMATCH', 'Codex Pro package identity or version does not match the pinned contract');
|
|
26
|
+
}
|
|
27
|
+
try { api = await import('dsh-subagent-codex-pro/machine'); }
|
|
28
|
+
catch { throw new WorkerError('ADAPTER_UNAVAILABLE', 'The pinned Codex Pro public machine API could not be imported'); }
|
|
29
|
+
if (api.MACHINE_PROTOCOL_VERSION !== ADAPTER.protocol ||
|
|
30
|
+
typeof api.parseMachineRequest !== 'function' || typeof api.runMachine !== 'function' || typeof api.MachineInputError !== 'function') {
|
|
31
|
+
throw new WorkerError('ADAPTER_MISMATCH', 'Codex Pro package identity, version or public machine API does not match the pinned contract');
|
|
32
|
+
}
|
|
33
|
+
return api;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Reject unsupported fields and permissions before any process or prompt starts. */
|
|
37
|
+
export function machineRequest(value, api, env) {
|
|
38
|
+
const keys = new Set(['type', 'protocol', 'requestId', 'cwd', 'model', 'reasoningEffort', 'permissionMode', 'prompt', 'timeoutMs']);
|
|
39
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || value.type !== 'start' || value.protocol !== 1 ||
|
|
40
|
+
Object.keys(value).some(key => !keys.has(key)) || typeof value.requestId !== 'string' || value.requestId.length > 256 ||
|
|
41
|
+
!['read-only', 'workspace-write'].includes(value.permissionMode)) {
|
|
42
|
+
throw new WorkerError('INVALID_REQUEST', 'Expected one protocol=1 start request with an explicit supported permission mode');
|
|
43
|
+
}
|
|
44
|
+
if (value.timeoutMs !== undefined && (!Number.isSafeInteger(value.timeoutMs) || value.timeoutMs < 1 || value.timeoutMs > 2147483647)) {
|
|
45
|
+
throw new WorkerError('INVALID_REQUEST', 'timeoutMs must be a positive safe integer no greater than 2147483647');
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return api.parseMachineRequest({ protocol: 1, requestId: value.requestId, cwd: value.cwd, model: value.model,
|
|
49
|
+
effort: value.reasoningEffort, permissionMode: value.permissionMode, prompt: value.prompt,
|
|
50
|
+
...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }),
|
|
51
|
+
command: env.FLYWHEEL_CODEX_BIN || 'codex', threadMode: 'ephemeral', maxOutputBytes: LIMITS.machineOutputBytes });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new WorkerError('INVALID_REQUEST', error instanceof api.MachineInputError ? error.message : 'Invalid Codex Pro machine request');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function doctorResult() {
|
|
58
|
+
return { protocol: 1, type: 'doctor', package: ADAPTER.package, version: ADAPTER.version,
|
|
59
|
+
machineProtocol: ADAPTER.protocol, available: true, inferenceTested: false };
|
|
60
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One owned invocation. stdin remains a lifeline to the Go parent after admission.
|
|
3
|
+
import { ADAPTER, LIMITS, WorkerError, loadAdapter, machineRequest, doctorResult } from './codex-pro-bridge.js';
|
|
4
|
+
|
|
5
|
+
const controller = new AbortController();
|
|
6
|
+
let cancelledBy, inputFailure, outputFailure, requestId = 'invalid-request', finished = false;
|
|
7
|
+
const cancel = signal => { cancelledBy ??= signal; controller.abort(); };
|
|
8
|
+
const handlers = new Map(['SIGTERM', 'SIGINT', 'SIGHUP'].map(signal => [signal, () => cancel(signal)]));
|
|
9
|
+
for (const [signal, handler] of handlers) process.on(signal, handler);
|
|
10
|
+
process.stdout.on('error', () => {
|
|
11
|
+
outputFailure ??= new WorkerError('OUTPUT_ERROR', 'The result consumer closed or failed');
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
controller.abort();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function write(frame) {
|
|
17
|
+
if (outputFailure) return;
|
|
18
|
+
const line = JSON.stringify(frame) + '\n';
|
|
19
|
+
if (Buffer.byteLength(line) > LIMITS.receiptFrameBytes) throw new WorkerError('OUTPUT_LIMIT', 'Codex Pro result exceeded the complete receipt frame limit');
|
|
20
|
+
process.stdout.write(line);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Do not close stdin after the first line: parent death must cancel the run. */
|
|
24
|
+
function input() {
|
|
25
|
+
let bytes = 0, buffer = Buffer.alloc(0), admitted = false, resolveInput, rejectInput;
|
|
26
|
+
const promise = new Promise((resolve, reject) => { resolveInput = resolve; rejectInput = reject; });
|
|
27
|
+
const fail = (code, message) => {
|
|
28
|
+
if (finished) return;
|
|
29
|
+
inputFailure ??= new WorkerError(code, message);
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
rejectInput(inputFailure);
|
|
32
|
+
controller.abort();
|
|
33
|
+
};
|
|
34
|
+
const timer = setTimeout(() => fail('INPUT_TIMEOUT', 'No complete start request arrived within 10 seconds'), LIMITS.admissionMs);
|
|
35
|
+
const onData = chunk => {
|
|
36
|
+
bytes += chunk.length;
|
|
37
|
+
if (bytes > LIMITS.requestBytes) { fail('INPUT_LIMIT', 'The request exceeded the 1 MiB input limit'); return; }
|
|
38
|
+
if (admitted) {
|
|
39
|
+
if (chunk.toString('utf8').trim() !== '') fail('INVALID_REQUEST', 'Only one start request is allowed');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
43
|
+
const newline = buffer.indexOf(10);
|
|
44
|
+
if (newline < 0) return;
|
|
45
|
+
admitted = true;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
if (buffer.subarray(newline + 1).toString('utf8').trim() !== '') { fail('INVALID_REQUEST', 'Only one start request is allowed'); return; }
|
|
48
|
+
try { resolveInput(JSON.parse(buffer.subarray(0, newline).toString('utf8'))); }
|
|
49
|
+
catch { fail('INVALID_REQUEST', 'The first line must contain one JSON request'); }
|
|
50
|
+
buffer = Buffer.alloc(0);
|
|
51
|
+
};
|
|
52
|
+
const onEnd = () => fail('PARENT_DISCONNECTED', 'The parent stdin lifeline closed before completion');
|
|
53
|
+
const onError = () => fail('INPUT_ERROR', 'The parent stdin lifeline failed');
|
|
54
|
+
const onAbort = () => { clearTimeout(timer); rejectInput(new WorkerError('CANCELLED', 'Codex Pro worker was cancelled before admission')); };
|
|
55
|
+
process.stdin.on('data', onData);
|
|
56
|
+
process.stdin.once('end', onEnd);
|
|
57
|
+
process.stdin.once('error', onError);
|
|
58
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
59
|
+
if (controller.signal.aborted) onAbort();
|
|
60
|
+
return { promise, stop() {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
process.stdin.off('data', onData); process.stdin.off('end', onEnd); process.stdin.off('error', onError);
|
|
63
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
64
|
+
process.stdin.pause();
|
|
65
|
+
process.stdin.destroy();
|
|
66
|
+
} };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let lifeline;
|
|
70
|
+
try {
|
|
71
|
+
const args = process.argv.slice(2);
|
|
72
|
+
if (args.length > 1 || (args.length === 1 && args[0] !== '--doctor')) throw new WorkerError('INVALID_REQUEST', 'Use flywheel-codex-pro-worker [--doctor]');
|
|
73
|
+
if (args[0] === '--doctor') {
|
|
74
|
+
await loadAdapter();
|
|
75
|
+
write(doctorResult());
|
|
76
|
+
process.exitCode = 0;
|
|
77
|
+
} else {
|
|
78
|
+
lifeline = input();
|
|
79
|
+
const value = await lifeline.promise;
|
|
80
|
+
if (typeof value?.requestId === 'string' && value.requestId.trim() !== '' && value.requestId.length <= 256) requestId = value.requestId;
|
|
81
|
+
const api = await loadAdapter();
|
|
82
|
+
if (controller.signal.aborted) throw inputFailure || new WorkerError('CANCELLED', 'Codex Pro worker was cancelled before the run');
|
|
83
|
+
const request = machineRequest(value, api, process.env);
|
|
84
|
+
requestId = request.requestId;
|
|
85
|
+
let progressBytes = 0;
|
|
86
|
+
const receipt = await api.runMachine(request, { signal: controller.signal, env: process.env, onProgress(text) {
|
|
87
|
+
if (controller.signal.aborted) return;
|
|
88
|
+
const frame = { protocol: 1, type: 'progress', requestId, text };
|
|
89
|
+
progressBytes += Buffer.byteLength(JSON.stringify(frame) + '\n');
|
|
90
|
+
if (progressBytes > LIMITS.progressBytes) {
|
|
91
|
+
outputFailure ??= new WorkerError('OUTPUT_LIMIT', 'Codex Pro progress exceeded the 1 MiB stream limit');
|
|
92
|
+
controller.abort();
|
|
93
|
+
} else write(frame);
|
|
94
|
+
} });
|
|
95
|
+
finished = true;
|
|
96
|
+
lifeline.stop();
|
|
97
|
+
const error = inputFailure || outputFailure;
|
|
98
|
+
// An output-limit error still allows a bounded terminal receipt. A broken
|
|
99
|
+
// stdout cannot, but the run has already settled and cleanup was awaited.
|
|
100
|
+
if (outputFailure?.code === 'OUTPUT_LIMIT') outputFailure = undefined;
|
|
101
|
+
write({ protocol: 1, type: 'result', requestId, adapter: ADAPTER, receipt,
|
|
102
|
+
error: error ? { code: error.code, message: error.message } : null });
|
|
103
|
+
process.exitCode = receipt.ok && !error && !controller.signal.aborted ? 0 : 1;
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
finished = true;
|
|
107
|
+
lifeline?.stop();
|
|
108
|
+
const known = error instanceof WorkerError ? error : new WorkerError('WORKER_ERROR', 'Codex Pro worker failed before a complete receipt was available');
|
|
109
|
+
write({ protocol: 1, type: 'result', requestId, adapter: ADAPTER, receipt: null,
|
|
110
|
+
error: { code: known.code, message: known.message } });
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
} finally {
|
|
113
|
+
finished = true;
|
|
114
|
+
lifeline?.stop();
|
|
115
|
+
if (cancelledBy) process.exitCode = { SIGINT: 130, SIGTERM: 143, SIGHUP: 129 }[cancelledBy];
|
|
116
|
+
for (const [signal, handler] of handlers) process.off(signal, handler);
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ckanner/dsh-plugin-flywheel",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Flywheel engine controls
|
|
3
|
+
"version": "0.28.0",
|
|
4
|
+
"description": "Flywheel engine controls with owned DSH SDK and Codex Pro machine workers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=22.
|
|
8
|
+
"node": ">=22.18.0",
|
|
9
9
|
"dsh": "0.2.1-rc.5"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
@@ -13,15 +13,18 @@
|
|
|
13
13
|
"./client": "./dist/client.js",
|
|
14
14
|
"./worker-policy": "./dist/policy.js",
|
|
15
15
|
"./worker": "./dist/worker.js",
|
|
16
|
+
"./codex-pro-worker": "./dist/codex-pro-worker.js",
|
|
16
17
|
"./package.json": "./package.json"
|
|
17
18
|
},
|
|
18
19
|
"bin": {
|
|
19
|
-
"flywheel-dsh-worker": "./dist/worker.js"
|
|
20
|
+
"flywheel-dsh-worker": "./dist/worker.js",
|
|
21
|
+
"flywheel-codex-pro-worker": "./dist/codex-pro-worker.js"
|
|
20
22
|
},
|
|
21
23
|
"files": [
|
|
22
24
|
"dist",
|
|
23
25
|
"cordis.patch.yml",
|
|
24
26
|
"README.md",
|
|
27
|
+
"README.en.md",
|
|
25
28
|
"PROTOCOL.md"
|
|
26
29
|
],
|
|
27
30
|
"dsh": {
|
|
@@ -59,6 +62,9 @@
|
|
|
59
62
|
"check": "npm test",
|
|
60
63
|
"prepack": "npm run build"
|
|
61
64
|
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"dsh-subagent-codex-pro": "0.8.0"
|
|
67
|
+
},
|
|
62
68
|
"devDependencies": {
|
|
63
69
|
"jsdom": "26.1.0",
|
|
64
70
|
"react": "18.3.1",
|