@ckanner/dsh-plugin-flywheel 0.28.1 → 0.28.2

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 CHANGED
@@ -18,7 +18,7 @@ Go 在工作目录启动 `flywheel-dsh-worker`,写入**一条** UTF-8 JSON 行
18
18
  | `maxTokens` | 正整数,每次原生模型请求的输出上限,传入 SDK initialize |
19
19
  | `timeoutMs` | 1–86,400,000,默认 1,800,000;涵盖权限准备、初始化、运行和关闭 |
20
20
 
21
- `permissionMode` 仅允许 `read-only`、`workspace-write`。未知字段一律拒绝。v1 不接受 `sessionId` / resume、附件、任意额外 CLI 参数和额外写目录;不进行隐式冷启动替代恢复。
21
+ `permissionMode` 允许 `read-only`、`workspace-write` 和显式 `danger-full-access`。未知字段一律拒绝。v1 不接受 `sessionId` / resume、附件、任意额外 CLI 参数和额外写目录;不进行隐式冷启动替代恢复。
22
22
 
23
23
  ## 运行参数
24
24
 
@@ -46,12 +46,12 @@ worker 用 `dsh --profile <profile> --patch <临时覆盖文件>` 启动 DSH。
46
46
  发出 ready 前必须依次满足:
47
47
 
48
48
  1. 官方 SDK `initialize` 返回 `deepseek-harness-sdk-runtime`,adapter 接受指定 provider/model/effort。
49
- 2. worker 策略插件经真实 `ctx.shell` 执行本地无副作用的 `:` 探针;退出码为 0 且报告 `sandbox.enforcement:full`。不可用或部分实施的沙箱拒绝通过。
49
+ 2. worker 策略插件经真实 `ctx.shell` 执行本地无副作用的 `:` 探针;退出码为 0、模式匹配且未被拒绝。受限模式还须报告 `sandbox.enforcement:full`,不可用或部分实施的沙箱拒绝通过;明确完全权限时记录 `confinedShell:false` 并省略 enforcement,不伪造 OS 强制。
50
50
  3. `session/working-directory/get` 创建全新的 Session,返回实际 cwd。
51
51
  4. Session 的 `sandbox/mode`、`approval/policy` 被固定,`ctx.sandboxPolicy.resolve()` 与 `ctx.approval.effectivePolicy()` 的实际读值形成 `flywheel/policy` 事件。
52
- 5. worker 比对 policy 事件的 cwd、mode、`never` 和 full confinement。
52
+ 5. worker 比对 policy 事件的 cwd、mode、`never` 及对应权限证据:受限模式为完整强制,完全权限为明确无 OS 限制。
53
53
 
54
- 任一项缺失,不发送 `session/prompt`。`never` 在 rc.5 的含义是自动拒绝需要批准的提权操作;它不会授予无限权限。
54
+ 任一项缺失,不发送 `session/prompt`。`never` 控制交互审批,不单独扩大访问范围;必须同时显式选择 `danger-full-access` 才请求完全权限。既有静态拒绝或外部权限不足仍按真实失败处理。
55
55
 
56
56
  ### event:可审计的原始持久事件
57
57
 
@@ -99,7 +99,7 @@ Go 的 `DSHArgs.Doctor` 使用这个入口;为保持 v1 必填 `prompt` 的 ad
99
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
100
  ```
101
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`,不恢复旧线程。
102
+ 只接受这些字段;不接受 provider、resume、threadId、额外写目录、任意 CLI 参数或未知权限模式。requestId 非空且最多 256 字符;cwd 为绝对路径,Go 先规范化成物理目录。model/reasoningEffort/prompt 必须是非空文本,permissionMode 为 read-only/workspace-write/danger-full-access;最后一项在 Node adapter 边界映射为 machine API 的 `dangerously-bypass-approvals-and-sandbox`,Go 同时核对映射后的 requested 字段和 canonical 生效模式。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
103
 
104
104
  **这里与 DSH worker 的 EOF 语义不同:Go 写入一行后保持 stdin 开放。** stdin 是父进程的存活连接;未完成前 EOF、读取错误或第二条非空输入触发 AbortSignal,并等待插件有界清理。不能用 `echo … | worker` 的关闭 stdin 行为冒充正常调用。运行请求首行等待上限 10 秒;请求最多 1 MiB,machine 原始输出上限 1 MiB,序列化 progress 累计最多 1 MiB,完整终态帧最多 8 MiB。超限明确失败,不能裁掉回执后判成功。
105
105
 
@@ -119,7 +119,7 @@ Go 的 `DSHArgs.Doctor` 使用这个入口;为保持 v1 必填 `prompt` 的 ad
119
119
 
120
120
  启动前不能获得 machine receipt 时为 null。运行已开始则即使失败也保留完整 receipt;`error` 非 null、重复/丢失 result、尾部垃圾、请求身份或 adapter 版本不符都不能成功。本通道没有 DSH `ready` 帧,不能据此虚构运行前完整沙箱握手或实际 effort 回显。
121
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 配置已经允许的访问**。
122
+ Go 同时校验外层 worker 物理终态和插件 receipt:请求及 run/thread/turn 身份、模型、cwd、approval=never、实际沙箱模式、完整 sandboxPolicy、stopReason、cleanup 与执行事实。受限模式的 workspaceWrite 显式 writableRoots 必须处于工作区内,networkAccess 必须明确 false;显式完全权限要求 `sandbox:danger-full-access` 与精确的 `{type:"dangerFullAccess"}` policy,不要求不存在的受限模式字段;Codex 标准临时目录标记保留,不宣称只能写 cwd。插件 0.8.0 只在 turn 前检查模式等基本配置;完整策略是返回后的交付资格检查,**不能阻止自定义 Codex 配置已经允许的访问**。
123
123
 
124
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
125
 
package/README.en.md CHANGED
@@ -7,7 +7,7 @@ provides its DSH SDK and Codex Pro workers. The engine owns task state, budgets,
7
7
  verification, Git delivery, and process lifecycles. The desktop is a control and
8
8
  observation interface; it does not need a parent model session to keep a task alive.
9
9
 
10
- This guide describes the **Flywheel 0.28.1** interfaces. Source availability does
10
+ This guide describes the **Flywheel 0.28.2** interfaces. Source availability does
11
11
  not mean that npm `latest` already contains them. Check the registry version and
12
12
  the [release guide](https://github.com/ckanner/flywheel/blob/main/docs/en/releasing.md) before installing. The desktop
13
13
  compatibility baseline is **DSH Mac 0.2.1-rc.5**, not a claim that every DSH build
@@ -17,6 +17,8 @@ or provider adapter is compatible.
17
17
  uses the composer popup. Upgrade the CLI and desktop plugin to the same new
18
18
  version; merging source does not update an installed plugin.
19
19
 
20
+ Subscription preset migration and `permissions.mode` are available from **0.28.2**. Upgrades preserve existing configuration; only an explicit migration backs it up and updates model bindings and permissions.
21
+
20
22
  ## Components
21
23
 
22
24
  | Component | Responsibility |
@@ -162,10 +164,14 @@ actual inference result. Keep `warn`, `unknown`, and `skipped` distinct from
162
164
 
163
165
  Open a DSH session and choose **Flywheel** in the native view bar alongside
164
166
  Chat and Trajectory. If the entry is not directly visible, check that bar's
165
- More menu. Select the absolute task worktree path as the repository directory
166
- and the task directory inside that same worktree. A blank repository field
167
- uses the current session's starting directory. Prepare and review the contracts
168
- first; the view does not turn an empty directory into a complete task plan.
167
+ More menu. Ask the chat assistant to prepare and review the task using
168
+ `flywheel-new`; it reports the actual paths. **Task files directory** contains
169
+ the task description and acceptance files, for example
170
+ `/Users/me/project/docs/fix-login`. **Project directory (optional)** can stay
171
+ blank to use the current DSH session directory; specify the matching project
172
+ worktree when the task lives elsewhere. Configuration and diagnostic actions
173
+ can also leave the task directory blank. These are project paths, not plugin
174
+ installation paths.
169
175
 
170
176
  The panel, `/flywheel`, and `flywheel_control` share these Go operations:
171
177
 
@@ -188,6 +194,13 @@ entry point, select its task directory and refresh. Effective configuration come
188
194
  from the engine's selected file; installing the plugin or changing DSH's chat
189
195
  model does not migrate that configuration.
190
196
 
197
+ **Check installation and configuration** performs local checks without starting
198
+ the SDK. **Check DSH connection** additionally starts a temporary SDK process,
199
+ checks its configuration, directory and permissions, and closes it. Use these
200
+ when installing, upgrading or troubleshooting; they are not required before
201
+ every task. Neither sends an inference prompt or proves account access, quota
202
+ or actual model response capability.
203
+
191
204
  ### View, request, and task lifetimes
192
205
 
193
206
  Task and repository drafts and fetched snapshots are retained separately for
@@ -271,12 +284,15 @@ Conversely, desktop installation and sign-in do not prove that the SDK profile
271
284
  loads and authorizes the same capabilities. Verify each selected harness through
272
285
  its own configuration and supported authorization flow.
273
286
 
274
- The DSH worker requests `read-only` or `workspace-write`, fixes approvals to
275
- `never`, and verifies the effective SDK policy before sending the task prompt.
276
- In the checked Mac source, the DSH sandbox uses its Seatbelt runner and filesystem
277
- fence. Workspace writes include DSH's permitted temporary area; this is not a
278
- network-isolation guarantee. Missing or incomplete enforcement is a failure,
279
- not a reason to switch to unrestricted access.
287
+ The DSH worker supports `read-only`, `workspace-write`, and explicit
288
+ `danger-full-access`, with approvals fixed to `never`. New subscription presets
289
+ select `permissions.mode:full-access` for every stage; Kimi print retains native
290
+ auto behavior. Existing files without this setting retain role-based access
291
+ until explicitly migrated. The actual SDK policy is checked before prompting.
292
+ Restricted modes use the checked Mac Seatbelt runner and filesystem fence;
293
+ workspace writes include DSH's permitted temporary area. Explicit full access
294
+ is recorded without claiming OS confinement. Host static denials, account
295
+ restrictions, and operating-system permissions remain in effect.
280
296
 
281
297
  The SDK overlay disables worker-owned delegation, workflow/goal drivers, model
282
298
  titles, and automatic model compaction so the Go owner can bound work and
@@ -286,14 +302,15 @@ not a claim that every DSH web operation requires a model: the official
286
302
  [web tool](https://github.com/deepseek-ai/deepseek-harness/blob/fcc0ad462172522aaf5c93a7b2e01f6b9f0b0159/packages/web/tool-web/README.md)
287
303
  configures search and fetch independently, while the
288
304
  [DeepSeek search backend](https://github.com/deepseek-ai/deepseek-harness/blob/fcc0ad462172522aaf5c93a7b2e01f6b9f0b0159/packages/web/web-search-deepseek/README.md)
289
- does use an auxiliary model request. This UI fix keeps worker policy unchanged.
305
+ does use an auxiliary model request. Full access changes the host access policy;
306
+ these tool and auxiliary-model restrictions still apply.
290
307
  Restoring retrieval or [automatic compaction](https://github.com/deepseek-ai/deepseek-harness/blob/fcc0ad462172522aaf5c93a7b2e01f6b9f0b0159/packages/compaction/compaction-basic/README.md)
291
308
  requires a separate check of accounting, budgets, cancellation, and directory
292
309
  ownership; the view's location does not establish those guarantees.
293
310
 
294
311
  Codex Pro uses fresh ephemeral calls and checks the model, directory, approval
295
312
  policy, and sandbox mode before execution. In version 0.8.0, the full
296
- `sandboxPolicy` reaches Flywheel only in the returned receipt. Flywheel rejects
313
+ `sandboxPolicy` reaches Flywheel only in the returned receipt. In restricted modes, Flywheel rejects
297
314
  delivery when that policy permits unexpected writable roots or network access,
298
315
  but **this check happens after execution and cannot undo access already
299
316
  permitted by custom Codex configuration**. It is not a complete pre-execution
package/README.md CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  **Flywheel 0.28.1 引入原生会话视图**;0.28.0 仍使用输入区底部弹层。需要升级 CLI 与桌面插件到同一新版本,源码合并不会自动更新已安装的插件。
10
10
 
11
+ 订阅预设迁移命令和 `permissions.mode` 从 **0.28.2** 开始提供。升级保留已有配置;只有用户明确应用迁移时才备份并更新模型与权限。
12
+
11
13
  ## 提供什么
12
14
 
13
15
  | 部分 | 行为 |
@@ -84,7 +86,7 @@ reasoningEffort = off | low | high | max
84
86
 
85
87
  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。
86
88
 
87
- 新调用固定使用 ephemeral 会话和 `read-only`/`workspace-write`,approval 为 never。插件运行前核对模型、cwd 和权限模式;完整 sandboxPolicy 由 Flywheel 在返回后审查,允许 Codex 标准临时目录、拒绝工作区外额外 roots 与网络启用。0.8.0 尚无公开的运行前完整策略 guard,事后拒绝交付不能阻止先前的访问;可信 Codex 配置仍是本机运行前提。
89
+ 新调用使用 ephemeral 会话,权限由 `permissions.mode` 决定,approval 为 never。role-based 选择 `read-only`/`workspace-write`;full-access 请求 `danger-full-access`。插件运行前核对模型、cwd 和权限模式,完整 sandboxPolicy 由 Flywheel 在返回后审查。受限模式允许 Codex 标准临时目录、拒绝工作区外额外 roots 与网络启用;完全权限则核对 `dangerFullAccess` 策略,不声称 OS 隔离。0.8.0 尚无公开的运行前完整策略 guard,事后拒绝交付不能阻止先前的访问;可信 Codex 配置仍是本机运行前提。
88
90
 
89
91
  完整 machine receipt 写入指标,保留 accepted effort、未知 effectiveEffort、usage 完整性与进程终态。插件正常收尾可能主动 TERM/KILL app-server,其物理 exit code 可以为空;Flywheel 只有在完整终态证明任务完成、清理已确认且外层 worker 干净退出时才能继续。取消、超时、截断、启动或清理失败均阻止成功。Go 为此 worker 预留至少 10 秒终止宽限,避免截断插件内部清理。
90
92
 
@@ -121,7 +123,7 @@ node --input-type=module -e 'console.log(JSON.stringify({type:"start",protocol:1
121
123
 
122
124
  ## 桌面使用
123
125
 
124
- 打开一个 DSH 会话,在顶部“对话/轨迹”所在的视图栏选择 **Flywheel**。入口由 DSH 的原生视图栏管理;若未直接显示,检查该栏的更多视图入口。填写已准备并审阅契约的任务目录,例如 `/repo/docs/my-task`。仓库目录默认取当前 DSH 会话的起始目录,也可填明确的绝对路径;生产任务应显式选择任务集成 worktree 及其内部的任务目录。按钮只执行以下 Go JSON 控制接口:
126
+ 打开一个 DSH 会话,在顶部“对话/轨迹”所在的视图栏选择 **Flywheel**。入口由 DSH 的原生视图栏管理;若未直接显示,检查该栏的更多视图入口。先在“对话”中让助手按 `flywheel-new` 准备任务说明与验收清单,审阅后由助手给出实际路径。“任务资料目录”就是这些文件所在的目录,例如 `/Users/me/project/docs/fix-login`,不是插件安装目录。“项目目录(可选)”默认取当前 DSH 会话目录,可留空;任务在另一份 worktree 时填对应的项目路径,例如 `/Users/me/project`。只看配置或做检查时,任务资料目录也可留空。按钮只执行以下 Go JSON 控制接口:
125
127
 
126
128
  ```sh
127
129
  flywheel control status --task-dir /repo/docs/my-task --cwd /repo
@@ -139,7 +141,7 @@ flywheel control doctor --task-dir /repo/docs/my-task --cwd /repo --probe
139
141
 
140
142
  模型工具 `flywheel_control` 接受 `action`、`taskDir`、`cwd`,以及仅用于 doctor 的可选 `probe` 布尔值。config/doctor 可省 taskDir,cwd 可沿用会话目录;start/status/stop 必须指定任务。输入统一校验,使用 argv 调用,不拼接 shell 命令。打开视图不会启动任务或探测;视图只在点击时读取状态,不自动轮询,也不自动发现其他入口启动的任务。查看已有任务时,选择同一任务目录再刷新。
141
143
 
142
- “查看模型配置”展示配置来源、展开后的阶段绑定及 provider/channel;“检查环境”不启动 SDK,“SDK 无推理检查”显式启用 probe。doctor 的合法退出码 1 会显示逐项诊断,超时、坏协议或不匹配的任务身份仍视为控制错误。`ok:true` 不代表账号、额度和真实推理通过。
144
+ “查看模型配置”展示实际配置来源、展开后的阶段绑定及 provider/channel/权限。“检查安装与配置”仅做本地预检,不启动 SDK;“检查 DSH 接入”额外临时启动 SDK、核对配置/目录/权限后退出。首次安装、升级或排障时按需使用,平时不必每次点击;两项都不向模型发送消息,也不验证账号、额度和实际回答能力。doctor 的合法退出码 1 会显示逐项诊断,超时、坏协议或不匹配的任务身份仍视为控制错误。`ok:true` 不代表账号、额度和真实推理通过。
143
145
 
144
146
  状态区区分未初始化、运行、已完成、暂停、停止、升级和预算停止;全部 packet 可见,不会只展示前几个。未知 usage/cost 明确保留,原始 JSON 默认折叠。配置显示引擎实际选择的文件及阶段绑定;已有用户配置不会因安装或切换 DSH 对话模型而自动迁移。
145
147
 
@@ -165,9 +167,9 @@ flywheel control doctor --task-dir /repo/docs/my-task --cwd /repo --probe
165
167
 
166
168
  每个调用使用新的 SDK Session;provider/model/effort 在该进程中固定。v1 不支持 resume、图像/附件输入或 SDK 原生 output schema。文本与报告文件由 Go 的冻结合同和 JSON Schema 验收,不能把 SDK idle 当成验收完成。
167
169
 
168
- worker 只支持 `read-only` 与 `workspace-write`。Mac 上由 DSH 的 Seatbelt bash runner 和 native fs fence 执行权限。workspace-write 的写入范围是**实际工作目录加 DSH 规定的系统临时区**,并非额外目录列表,也不是网络隔离。策略插件将 approval 固定为 `never`,即拒绝提权。
170
+ worker 支持 `read-only`、`workspace-write` 和显式 `danger-full-access`。新订阅预设通过 `permissions.mode:full-access` 为全部阶段选择完全权限,approval 固定为 `never`;Kimi print 使用原生 auto,不逐步请求用户点击允许。已有文件缺省仍保留 role-based,需通过[配置迁移](../../docs/configuration.md#将旧配置切换到已有的三种资源)显式采用新方案。受限模式由 Mac 上 DSH 的 Seatbelt bash runner 和 native fs fence 执行,workspace-write 范围是**实际工作目录加 DSH 规定的系统临时区**。full-access 如实记录无 OS 沙箱限制;宿主静态拒绝、账号与操作系统权限不被绕过。
169
171
 
170
- 执行报告先写到 worktree 内的任务报告路径,由 Go 校验后归档到主任务目录。不得为报告写入而把整个任务目录或用户目录设成 full access。
172
+ 执行报告仍先写到 worktree 内的任务报告路径,由 Go 校验后归档到主任务目录。权限模式来自明确配置,不能因为报告写入失败而暗中扩大它;任务边界、冻结验收和候选身份检查保持有效。
171
173
 
172
174
  默认覆盖会关闭 DSH 自主委派、workflow、goal 驱动、模型生成标题及自动模型压缩,避免在一个 Go 工作单元之外再启动辅助模型调用。需要压缩/继续执行时由 Go 显式决定下一次任务;超出上下文或输出 token 限制不会伪装成成功。SDK profile 应只装所需的可信 provider /工具,第三方 Host 插件本身是进程内代码,不能由本包替它提供任意代码隔离。
173
175
 
package/dist/client.js CHANGED
@@ -96,26 +96,28 @@ function createViewStore() {
96
96
 
97
97
  const zh = {
98
98
  view: 'Flywheel',
99
- title: '任务控制',
100
- description: '查看 Flywheel 任务的进度与验收结果,在需要时启动、恢复或停止任务',
99
+ title: 'Flywheel 任务',
100
+ description: '准备任务后,在这里查看进度、启动、继续或停止',
101
101
  lifecycle: '任务在后台独立运行。切换视图或会话不会停止任务;需要停止时请使用这里的“停止”',
102
- selection: '选择任务',
103
- taskDir: '任务目录',
104
- taskHint: '填写已准备好契约的绝对路径;只查看配置或检查环境时可留空',
105
- cwd: '仓库目录',
106
- cwdHint: '留空使用当前 DSH 会话目录,也可指定任务 worktree 的绝对路径',
107
- pathInvalid: '请输入以 / 开头的绝对路径',
102
+ selection: '打开已有任务',
103
+ taskDir: '任务资料目录',
104
+ taskHint: '填写助手准备任务时给出的完整路径,里面保存任务说明和验收清单;只查看配置或做检查时可留空',
105
+ cwd: '项目目录(可选)',
106
+ cwdHint: '默认使用当前 DSH 会话的项目目录,可留空。任务在其他项目或工作区时,填写对应的完整路径',
107
+ pathInvalid: '请填写以 / 开头的完整路径',
108
108
  pathControl: '路径不能包含换行或空字符',
109
- taskRequired: '填写任务目录后可刷新、启动或停止任务',
109
+ taskRequired: '填写任务资料目录后,可刷新状态、启动或停止任务',
110
110
  status: '刷新状态',
111
111
  start: '启动',
112
112
  stop: '停止',
113
113
  config: '查看模型配置',
114
- doctor: '检查环境',
115
- probe: 'SDK 无推理检查',
114
+ doctor: '检查安装与配置',
115
+ probe: '检查 DSH 接入',
116
+ doctorHelp: '检查安装与配置:核对本机工具、模型配置和所选任务文件,不会启动任务',
117
+ probeHelp: '检查 DSH 接入:临时启动独立的 DSH 运行环境,检查配置、项目目录和权限后退出;可能联网读取模型目录',
116
118
  busy: '正在等待“{action}”确认…',
117
119
  busyOther: '正在等待上一个目录的“{action}”结束;其结果不会覆盖当前选择',
118
- mutationLock: '启动或停止确认前,暂时不能切换任务目录',
120
+ mutationLock: '启动或停止确认前,暂时不能切换任务资料目录或项目目录',
119
121
  unknownOutcome: '“{action}”结果未确认',
120
122
  unknownHelp: '请求可能已经送达引擎。先刷新这个任务的状态,再决定下一步操作',
121
123
  reconcile: '刷新待确认任务',
@@ -127,7 +129,7 @@ const zh = {
127
129
  feedback: '操作反馈',
128
130
  statusHeading: '任务状态',
129
131
  emptyHeading: '还没有任务状态',
130
- emptyBody: '在“对话”中准备并审阅任务契约,然后填写任务目录并刷新状态。已有的后台任务也可在这里继续查看',
132
+ emptyBody: '先在“对话”里让助手按 flywheel-new 准备任务,审阅任务说明和验收标准,再填写助手给出的任务资料目录。已有任务可直接使用原目录',
131
133
  emptySelected: '点击“刷新状态”查看这个任务;打开本页不会自动启动任务',
132
134
  lastChecked: '上次确认:{time}',
133
135
  stale: '本次状态未确认,下方保留上次结果',
@@ -171,12 +173,13 @@ const zh = {
171
173
  tokensMissing: ';{count} 次调用用量未知,合计不完整',
172
174
  tokenLimit: '每日 Token 限额:{limit} · 预算策略:{policy}',
173
175
  budgetWarning: '预算提示:{message}',
174
- advanced: '模型配置与环境检查',
175
- advancedHelp: '查看当前任务实际使用的绑定,或在启动前检查本地环境',
176
+ advanced: '配置与排查(按需)',
177
+ advancedHelp: '首次安装、升级或运行失败时可用,平时不必每次点击;两项检查都不向模型发送消息,也不能验证账号、额度或实际回答能力',
176
178
  configuration: '实际模型配置',
179
+ configurationHelp: '这里显示 Flywheel 从下方来源读取的实际生效配置;DSH 对话输入框当前选择的模型不会替代它',
177
180
  source: '配置来源:{source}',
178
181
  embedded: '内嵌模板',
179
- scope: '检查范围:{scope}',
182
+ scope: '配置层级:{scope}',
180
183
  outOfScope: '未用于本次检查',
181
184
  model: '模型',
182
185
  profile: 'Profile',
@@ -185,6 +188,9 @@ const zh = {
185
188
  channel: '渠道',
186
189
  thinking: '推理',
187
190
  permission: '阶段权限要求',
191
+ permissionFull: '完全权限(无需逐步确认)',
192
+ permissionWorkspace: '项目内写入',
193
+ permissionReadOnly: '只读',
188
194
  session: '会话恢复',
189
195
  enabled: '启用',
190
196
  disabled: '关闭',
@@ -202,9 +208,9 @@ const zh = {
202
208
  diagnostic: '环境检查结果',
203
209
  doctorPassed: '检查结束:未发现阻断项',
204
210
  doctorFailed: '检查结束:存在阻断项',
205
- localOnly: '本次只检查本地环境,未启动 SDK 探测',
206
- probeRequested: '已请求 SDK 无推理检查',
207
- inferenceBoundary: '未发送推理请求;账号授权、额度和真实模型能力未通过本次检查验证',
211
+ localOnly: '本次检查本机工具、配置和所选任务文件,未启动 DSH 接入检查',
212
+ probeRequested: '已请求 DSH 接入检查(不调用模型)',
213
+ inferenceBoundary: '没有向模型发送消息;账号授权、可用额度和实际回答能力仍未验证',
208
214
  pass: '通过',
209
215
  fail: '失败',
210
216
  warn: '注意',
@@ -216,26 +222,28 @@ const zh = {
216
222
  };
217
223
  const en = {
218
224
  view: 'Flywheel',
219
- title: 'Task controls',
220
- description: 'Inspect Flywheel progress and verification results, then start, resume or stop a task when needed',
225
+ title: 'Flywheel tasks',
226
+ description: 'After preparing a task, inspect its progress, start, resume or stop it here',
221
227
  lifecycle: 'Tasks run independently in the background. Switching views or sessions does not stop a task; use Stop here when needed.',
222
- selection: 'Select a task',
223
- taskDir: 'Task directory',
224
- taskHint: 'Absolute path to a prepared task contract; optional for configuration and environment checks',
225
- cwd: 'Repository directory',
226
- cwdHint: 'Leave blank to use this DSH session directory, or enter the absolute task worktree path',
227
- pathInvalid: 'Enter an absolute path beginning with /',
228
+ selection: 'Open an existing task',
229
+ taskDir: 'Task files directory',
230
+ taskHint: 'Use the full path supplied by the assistant when it prepared the task instructions and acceptance checks; leave blank to inspect configuration or run diagnostics',
231
+ cwd: 'Project directory (optional)',
232
+ cwdHint: 'Leave blank to use the project directory of this DSH session. If the task belongs to another project or worktree, enter that full path.',
233
+ pathInvalid: 'Enter a full path beginning with /',
228
234
  pathControl: 'Paths cannot contain line breaks or null characters',
229
- taskRequired: 'Enter a task directory to refresh, start or stop a task',
235
+ taskRequired: 'Enter the task files directory to refresh status, start or stop the task',
230
236
  status: 'Refresh status',
231
237
  start: 'Start',
232
238
  stop: 'Stop',
233
239
  config: 'Inspect model configuration',
234
- doctor: 'Check environment',
235
- probe: 'SDK check without inference',
240
+ doctor: 'Check installation and configuration',
241
+ probe: 'Check DSH integration',
242
+ doctorHelp: 'Installation and configuration: check local tools, model settings and selected task files without starting the task.',
243
+ probeHelp: 'DSH integration: briefly start a separate DSH runtime, verify its configuration, project directory and permissions, then close it. It may fetch a model catalog online.',
236
244
  busy: 'Waiting for “{action}” confirmation…',
237
245
  busyOther: 'Waiting for “{action}” for the previous directory; its result will not replace the current selection',
238
- mutationLock: 'Task directories are locked until start or stop is confirmed',
246
+ mutationLock: 'Task and project directories are locked until start or stop is confirmed',
239
247
  unknownOutcome: '“{action}” outcome is unconfirmed',
240
248
  unknownHelp: 'The request may have reached the engine. Refresh this task before choosing the next action.',
241
249
  reconcile: 'Refresh unconfirmed task',
@@ -247,7 +255,7 @@ const en = {
247
255
  feedback: 'Operation feedback',
248
256
  statusHeading: 'Task status',
249
257
  emptyHeading: 'No task status yet',
250
- emptyBody: 'Prepare and review a task contract in Chat, then enter its directory and refresh. Existing background tasks can also be inspected here.',
258
+ emptyBody: 'In Chat, ask the assistant to prepare a task with flywheel-new. Review its instructions and acceptance checks, then enter the task files directory it provides. For an existing task, use its original directory.',
251
259
  emptySelected: 'Refresh status to inspect this task. Opening this view does not start it.',
252
260
  lastChecked: 'Last confirmed: {time}',
253
261
  stale: 'Current status is unconfirmed; the previous result is retained below',
@@ -291,12 +299,13 @@ const en = {
291
299
  tokensMissing: '; usage is unknown for {count} calls, so this total is incomplete',
292
300
  tokenLimit: 'Daily token limit: {limit} · Budget policy: {policy}',
293
301
  budgetWarning: 'Budget notice: {message}',
294
- advanced: 'Model configuration and environment',
295
- advancedHelp: 'Inspect the bindings used by this task or check the local environment before starting',
302
+ advanced: 'Configuration and troubleshooting (optional)',
303
+ advancedHelp: 'Useful after installation, upgrades or a failed run; neither check is required before every task. Neither sends messages to a model or verifies account access, quota or real model responses.',
296
304
  configuration: 'Effective model configuration',
305
+ configurationHelp: 'These are the effective Flywheel settings loaded from the source below. The model selected in the DSH chat composer does not replace them.',
297
306
  source: 'Configuration source: {source}',
298
307
  embedded: 'Embedded template',
299
- scope: 'Check scope: {scope}',
308
+ scope: 'Configuration level: {scope}',
300
309
  outOfScope: 'Not used for this check',
301
310
  model: 'Model',
302
311
  profile: 'Profile',
@@ -305,6 +314,9 @@ const en = {
305
314
  channel: 'Channel',
306
315
  thinking: 'Reasoning',
307
316
  permission: 'Required permission',
317
+ permissionFull: 'Full access (no step-by-step approval)',
318
+ permissionWorkspace: 'Write within the project',
319
+ permissionReadOnly: 'Read only',
308
320
  session: 'Session resume',
309
321
  enabled: 'Enabled',
310
322
  disabled: 'Disabled',
@@ -322,9 +334,9 @@ const en = {
322
334
  diagnostic: 'Environment check results',
323
335
  doctorPassed: 'Check complete: no blockers found',
324
336
  doctorFailed: 'Check complete: blockers found',
325
- localOnly: 'Only the local environment was checked; no SDK probe ran',
326
- probeRequested: 'An SDK check without inference was requested',
327
- inferenceBoundary: 'No inference request was sent. Account authorization, quota and actual model capability remain unverified.',
337
+ localOnly: 'Local tools, configuration and selected task files were checked; no DSH integration check ran.',
338
+ probeRequested: 'A DSH integration check was requested (no model call)',
339
+ inferenceBoundary: 'No message was sent to a model. Account access, available quota and real model responses remain unverified.',
328
340
  pass: 'Passed',
329
341
  fail: 'Failed',
330
342
  warn: 'Notice',
@@ -348,6 +360,11 @@ const packetLabels = {
348
360
  const roleLabels = { plan: 'planRole', execute: 'executeRole', review: 'reviewRole', judge: 'judgeRole',
349
361
  intake: 'intakeRole', garden: 'gardenRole', 'judge-panel': 'panelRole' };
350
362
  const checkLabels = { pass: 'pass', fail: 'fail', warn: 'warn', unknown: 'unverified', skipped: 'skipped' };
363
+ const permissionLabels = {
364
+ 'danger-full-access': 'permissionFull',
365
+ 'workspace-write': 'permissionWorkspace',
366
+ 'read-only': 'permissionReadOnly',
367
+ };
351
368
  const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
352
369
  const textValue = (value, fallback = '—') => typeof value === 'string' || typeof value === 'number' ? String(value) : fallback;
353
370
  const selectionOf = value => ({ taskDir: value.taskDir.trim(), cwd: value.cwd.trim() });
@@ -506,6 +523,7 @@ function Configuration({ value, t }) {
506
523
  h('dt', null, t(name)), h('dd', null, code ? h('code', null, value) : value));
507
524
  return h('section', { className: css.section, 'aria-label': t('configuration') },
508
525
  h('h3', { className: css.heading }, t('configuration')),
526
+ h('p', { className: css.meta }, t('configurationHelp')),
509
527
  h('p', { className: css.meta }, t('source', { source: textValue(value.source, '') || t('embedded') })),
510
528
  h('p', { className: css.meta }, t('scope', { scope: textValue(value.scope) })),
511
529
  h('div', { className: css.bindings }, ...(value.bindings ?? []).map((binding, index) =>
@@ -521,7 +539,8 @@ function Configuration({ value, t }) {
521
539
  field('provider', textValue(binding?.provider, '') || (binding?.harness === 'dsh' ? t('notConfigured') : t('nativeProvider'))),
522
540
  field('channel', textValue(binding?.channel, '') || t('notConfigured')),
523
541
  field('thinking', textValue(binding?.thinking, '') || t('default')),
524
- field('permission', textValue(binding?.permissionMode, '') || t('undeclared')),
542
+ field('permission', Object.hasOwn(permissionLabels, binding?.permissionMode) ?
543
+ t(permissionLabels[binding.permissionMode]) : textValue(binding?.permissionMode, '') || t('undeclared')),
525
544
  field('session', t(binding?.residentSession ? 'enabled' : 'disabled')))))));
526
545
  }
527
546
 
@@ -639,6 +658,7 @@ function TaskView({ useStore, actions, useControl, changeSelection, run, reconci
639
658
  const invalidCwd = pathProblem(selection.cwd);
640
659
  const button = (action, labelKey, needsTask, probe = false, variant = 'outline') => h(Button, {
641
660
  key: labelKey, variant, size: 'sm',
661
+ 'aria-describedby': ['doctor', 'probe'].includes(labelKey) ? formId + '-' + labelKey + '-help' : undefined,
642
662
  disabled: busy || !!invalidTask || !!invalidCwd || (needsTask && !selection.taskDir.trim()) ||
643
663
  (mutates(action) && control.uncertainty !== null),
644
664
  onClick: () => run(action, selection, probe),
@@ -650,7 +670,7 @@ function TaskView({ useStore, actions, useControl, changeSelection, run, reconci
650
670
  'aria-invalid': !!problem, 'aria-describedby': formId + '-' + name + '-help',
651
671
  onChange: event => changeSelection({ taskDir: selection.taskDir, cwd: selection.cwd, [name]: event.target.value }) }),
652
672
  h('p', { id: formId + '-' + name + '-help', className: problem ? css.errorText : css.meta }, t(problem ?? hint)));
653
- return h('section', { className: css.page, 'aria-label': 'Flywheel ' + t('title') },
673
+ return h('section', { className: css.page, 'aria-label': t('title') },
654
674
  h('style', { 'data-plugin': '@ckanner/dsh-plugin-flywheel',
655
675
  'data-plugin-css': '@ckanner/dsh-plugin-flywheel/client.module.css',
656
676
  }, stylesheet),
@@ -659,8 +679,8 @@ function TaskView({ useStore, actions, useControl, changeSelection, run, reconci
659
679
  h('p', { className: css.meta }, t('description'))),
660
680
  h('section', { className: css.selection, 'aria-label': t('selection') },
661
681
  h('div', { className: css.fields },
662
- field('taskDir', 'taskHint', '/path/to/repo/docs/task', invalidTask),
663
- field('cwd', 'cwdHint', '/path/to/repo', invalidCwd)),
682
+ field('taskDir', 'taskHint', '/Users/me/project/docs/fix-login', invalidTask),
683
+ field('cwd', 'cwdHint', '/Users/me/project', invalidCwd)),
664
684
  h('div', { className: css.actions },
665
685
  button('status', 'status', true, false, 'primary'),
666
686
  button('start', 'start', true), button('stop', 'stop', true))),
@@ -674,6 +694,8 @@ function TaskView({ useStore, actions, useControl, changeSelection, run, reconci
674
694
  h('div', { className: css.actions },
675
695
  button('config', 'config', false), button('doctor', 'doctor', false),
676
696
  button('doctor', 'probe', false, true)),
697
+ h('p', { className: css.meta, id: formId + '-doctor-help' }, t('doctorHelp')),
698
+ h('p', { className: css.meta, id: formId + '-probe-help' }, t('probeHelp')),
677
699
  h(Diagnostic, { value: control.diagnostic, t }),
678
700
  h(Configuration, { value: control.configuration, t }))),
679
701
  control.raw && h('details', { className: css.details },
@@ -38,7 +38,7 @@ export function machineRequest(value, api, env) {
38
38
  const keys = new Set(['type', 'protocol', 'requestId', 'cwd', 'model', 'reasoningEffort', 'permissionMode', 'prompt', 'timeoutMs']);
39
39
  if (value === null || typeof value !== 'object' || Array.isArray(value) || value.type !== 'start' || value.protocol !== 1 ||
40
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)) {
41
+ !['read-only', 'workspace-write', 'danger-full-access'].includes(value.permissionMode)) {
42
42
  throw new WorkerError('INVALID_REQUEST', 'Expected one protocol=1 start request with an explicit supported permission mode');
43
43
  }
44
44
  if (value.timeoutMs !== undefined && (!Number.isSafeInteger(value.timeoutMs) || value.timeoutMs < 1 || value.timeoutMs > 2147483647)) {
@@ -46,7 +46,9 @@ export function machineRequest(value, api, env) {
46
46
  }
47
47
  try {
48
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,
49
+ effort: value.reasoningEffort,
50
+ permissionMode: value.permissionMode === 'danger-full-access' ? 'dangerously-bypass-approvals-and-sandbox' : value.permissionMode,
51
+ prompt: value.prompt,
50
52
  ...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }),
51
53
  command: env.FLYWHEEL_CODEX_BIN || 'codex', threadMode: 'ephemeral', maxOutputBytes: LIMITS.machineOutputBytes });
52
54
  } catch (error) {
package/dist/policy.js CHANGED
@@ -4,20 +4,22 @@ export const inject = ['sandboxPolicy', 'approval', 'shell'];
4
4
 
5
5
  export async function apply(ctx, config) {
6
6
  const { permissionMode, cwd } = config;
7
- if (!['read-only', 'workspace-write'].includes(permissionMode) || typeof cwd !== 'string' ||
7
+ const fullAccess = permissionMode === 'danger-full-access';
8
+ if (!['read-only', 'workspace-write', 'danger-full-access'].includes(permissionMode) || typeof cwd !== 'string' ||
8
9
  ctx.shell.sandboxMode === undefined) {
9
- throw new Error('Flywheel requires a confined DSH shell and explicit workspace policy');
10
+ throw new Error('Flywheel requires a sandbox-capable DSH shell and explicit permission policy');
10
11
  }
11
- // This local no-op proves that the actual runner starts and reports full
12
- // enforcement. Merely selecting a policy value is not readiness evidence.
12
+ // Probe the official runner. Restricted modes must prove full enforcement;
13
+ // full access must truthfully report that no confinement was applied.
13
14
  const probe = await ctx.shell.execute(ctx.shell.resolve({
14
15
  command: ':', workdir: cwd, timeoutMs: 5000,
15
16
  sandboxPolicy: { mode: permissionMode, workspaceRoot: cwd },
16
17
  }));
17
18
  const result = await probe.result();
18
19
  if (result.exitCode !== 0 || result.timedOut || result.aborted ||
19
- result.sandbox?.mode !== permissionMode || result.sandbox?.enforcement !== 'full') {
20
- throw new Error('Flywheel sandbox readiness probe did not prove full enforcement');
20
+ result.sandbox?.mode !== permissionMode ||
21
+ (fullAccess ? result.sandbox.denied !== false || result.sandbox.enforcement !== undefined : result.sandbox.enforcement !== 'full')) {
22
+ throw new Error(fullAccess ? 'Flywheel readiness probe did not confirm explicit full access' : 'Flywheel sandbox readiness probe did not prove full enforcement');
21
23
  }
22
24
  ctx.on('session/created', session => {
23
25
  // A fresh SDK session has no inherited permission facts. Pin both canonical
@@ -30,8 +32,8 @@ export async function apply(ctx, config) {
30
32
  cwd: policy.workspaceRoot,
31
33
  permissionMode: policy.mode,
32
34
  approvalPolicy: ctx.approval.effectivePolicy(session),
33
- confinedShell: ctx.shell.sandboxMode !== undefined,
34
- enforcement: result.sandbox.enforcement,
35
+ confinedShell: !fullAccess,
36
+ ...(fullAccess ? {} : { enforcement: result.sandbox.enforcement }),
35
37
  });
36
38
  });
37
39
  }
package/dist/protocol.js CHANGED
@@ -49,8 +49,8 @@ export async function validateStart(value) {
49
49
  const provider = nonempty(value.provider, 'provider');
50
50
  const model = nonempty(value.model, 'model');
51
51
  const prompt = nonempty(value.prompt, 'prompt', MAX_FRAME_BYTES);
52
- if (!['read-only', 'workspace-write'].includes(value.permissionMode)) {
53
- throw new BridgeError('INVALID_REQUEST', 'permissionMode must be read-only or workspace-write');
52
+ if (!['read-only', 'workspace-write', 'danger-full-access'].includes(value.permissionMode)) {
53
+ throw new BridgeError('INVALID_REQUEST', 'permissionMode must be read-only, workspace-write or danger-full-access');
54
54
  }
55
55
  if (value.reasoningEffort !== undefined) nonempty(value.reasoningEffort, 'reasoningEffort', 64);
56
56
  if (value.maxTokens !== undefined && (!Number.isSafeInteger(value.maxTokens) || value.maxTokens < 1)) {
@@ -151,10 +151,12 @@ export class SessionFold {
151
151
 
152
152
  verifiedPolicy(request) {
153
153
  const policy = this.policy;
154
+ const fullAccess = request.permissionMode === 'danger-full-access';
154
155
  if (!policy || policy.protocol !== PROTOCOL || policy.permissionMode !== request.permissionMode ||
155
156
  policy.cwd !== request.cwd || policy.approvalPolicy !== 'never' ||
156
- policy.confinedShell !== true || policy.enforcement !== 'full') {
157
- throw new BridgeError('INVALID_REQUEST', 'DSH did not attest the requested cwd and confined permission policy; prompt withheld');
157
+ policy.confinedShell !== !fullAccess ||
158
+ (fullAccess ? policy.enforcement !== undefined : policy.enforcement !== 'full')) {
159
+ throw new BridgeError('INVALID_REQUEST', 'DSH did not attest the requested cwd and permission policy; prompt withheld');
158
160
  }
159
161
  }
160
162
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckanner/dsh-plugin-flywheel",
3
- "version": "0.28.1",
3
+ "version": "0.28.2",
4
4
  "description": "Flywheel engine controls with owned DSH SDK and Codex Pro machine workers",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",