@ckanner/flywheel 0.33.0 → 0.34.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.
Files changed (151) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -0
  3. package/README.zh-CN.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +5 -1
  5. package/bin/flywheel-kimi-native-worker.js +5 -0
  6. package/bin/flywheel.js +8 -1
  7. package/bin/runtime.js +5 -1
  8. package/dist/bridge.mjs +89 -7
  9. package/dist/claude-code-pro-bridge.mjs +55 -6
  10. package/dist/client.js +107 -10
  11. package/dist/codex-pro-bridge.mjs +12 -5
  12. package/dist/control.mjs +1 -1
  13. package/dist/kimi-native-bridge.mjs +429 -0
  14. package/dist/kimi-native-runtime.mjs +233 -0
  15. package/dist/kimi-native-worker.mjs +77 -0
  16. package/dist/kimi-tower-candidate.mjs +227 -0
  17. package/dist/machine-worker.mjs +39 -5
  18. package/dist/managed-lcu.mjs +223 -0
  19. package/dist/native-doctor.mjs +36 -0
  20. package/dist/native-jobs.mjs +72 -0
  21. package/dist/native-policy.mjs +371 -0
  22. package/dist/native-resources.mjs +144 -0
  23. package/dist/native.mjs +251 -0
  24. package/dist/policy.mjs +13 -3
  25. package/dist/prewarm.mjs +4 -1
  26. package/dist/protocol.mjs +8 -1
  27. package/dist/worker.mjs +16 -9
  28. package/docs/README.md +9 -0
  29. package/docs/README.zh-CN.md +3 -0
  30. package/docs/design.md +34 -12
  31. package/docs/en/claude-code-pro.md +2 -2
  32. package/docs/en/configuration.md +17 -5
  33. package/docs/en/contributing.md +5 -3
  34. package/docs/en/flow-console.md +1 -1
  35. package/docs/en/getting-started.md +2 -2
  36. package/docs/en/model-policy.md +2 -2
  37. package/docs/en/native-capabilities.md +185 -0
  38. package/docs/en/releasing.md +2 -2
  39. package/docs/en/session-recovery.md +2 -2
  40. package/docs/plans/2026-11-13-native-harness-capabilities.en.md +258 -0
  41. package/docs/plans/2026-11-13-native-harness-capabilities.md +276 -0
  42. package/docs/plans/README.md +1 -0
  43. package/docs/validation/2026-11-14-desktop-native-configuration.md +128 -0
  44. package/docs/validation/2026-11-14-native-harness-implementation.md +215 -0
  45. package/docs/validation/2026-11-14-published-companions.md +15 -0
  46. package/docs/validation/2026-11-14-release-0.33.0.md +49 -0
  47. package/docs/validation/2026-11-14-release-0.33.1.md +78 -0
  48. package/docs/validation/README.md +4 -0
  49. package/docs/validation/evidence/2026-11-14-native-harness-candidate.json +255 -0
  50. package/docs/validation/evidence/2026-11-14-native-harness-sessions.json +6009 -0
  51. package/docs/validation/evidence/2026-11-14-native-harness-tower-candidate.json +192 -0
  52. package/docs/zh-CN/claude-code-pro.md +2 -2
  53. package/docs/zh-CN/configuration.md +21 -8
  54. package/docs/zh-CN/contributing.md +5 -3
  55. package/docs/zh-CN/flow-console.md +1 -1
  56. package/docs/zh-CN/getting-started.md +2 -2
  57. package/docs/zh-CN/model-policy.md +2 -2
  58. package/docs/zh-CN/native-capabilities.md +184 -0
  59. package/docs/zh-CN/releasing.md +2 -2
  60. package/docs/zh-CN/session-recovery.md +1 -1
  61. package/native/darwin-amd64/flywheel +0 -0
  62. package/native/darwin-arm64/flywheel +0 -0
  63. package/native/linux-amd64/flywheel +0 -0
  64. package/native/linux-arm64/flywheel +0 -0
  65. package/native/manifest.json +9 -9
  66. package/node_modules/dsh-preset-generator/CHANGELOG.md +77 -0
  67. package/node_modules/dsh-preset-generator/LICENSE +21 -0
  68. package/node_modules/dsh-preset-generator/README.md +149 -0
  69. package/node_modules/dsh-preset-generator/docs/README.zh.md +95 -0
  70. package/node_modules/dsh-preset-generator/package.json +43 -0
  71. package/node_modules/dsh-preset-generator/src/composition.mjs +150 -0
  72. package/node_modules/dsh-preset-generator/src/gen-presets.mjs +367 -0
  73. package/node_modules/dsh-preset-generator/src/policy.mjs +17 -0
  74. package/node_modules/dsh-subagent-codex-pro/CHANGELOG.md +16 -1
  75. package/node_modules/dsh-subagent-codex-pro/README.md +57 -4
  76. package/node_modules/dsh-subagent-codex-pro/assets/native-client.js +118 -0
  77. package/node_modules/dsh-subagent-codex-pro/cordis.patch.yml +6 -0
  78. package/node_modules/dsh-subagent-codex-pro/docs/README.zh.md +49 -2
  79. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle-validation.md +1 -1
  80. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.md +1 -1
  81. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.zh.md +1 -1
  82. package/node_modules/dsh-subagent-codex-pro/docs/machine.md +2 -2
  83. package/node_modules/dsh-subagent-codex-pro/docs/native-capabilities.md +254 -0
  84. package/node_modules/dsh-subagent-codex-pro/docs/native-capabilities.zh.md +180 -0
  85. package/node_modules/dsh-subagent-codex-pro/lib/config.js +56 -0
  86. package/node_modules/dsh-subagent-codex-pro/lib/config.js.map +1 -0
  87. package/node_modules/dsh-subagent-codex-pro/lib/desktop-native.js +109 -0
  88. package/node_modules/dsh-subagent-codex-pro/lib/desktop-native.js.map +1 -0
  89. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js +3 -4
  90. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js.map +1 -1
  91. package/node_modules/dsh-subagent-codex-pro/lib/index.js +64 -15
  92. package/node_modules/dsh-subagent-codex-pro/lib/index.js.map +1 -1
  93. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js +6 -2
  94. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js.map +1 -1
  95. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js +2 -2
  96. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js.map +1 -1
  97. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js +5 -2
  98. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js.map +1 -1
  99. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js +11 -0
  100. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js.map +1 -1
  101. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js +1 -0
  102. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js.map +1 -1
  103. package/node_modules/dsh-subagent-codex-pro/lib/machine.js +20 -3
  104. package/node_modules/dsh-subagent-codex-pro/lib/machine.js.map +1 -1
  105. package/node_modules/dsh-subagent-codex-pro/lib/native.js +297 -0
  106. package/node_modules/dsh-subagent-codex-pro/lib/native.js.map +1 -0
  107. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js +11 -1
  108. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js.map +1 -1
  109. package/node_modules/dsh-subagent-codex-pro/lib/run.js +19 -1
  110. package/node_modules/dsh-subagent-codex-pro/lib/run.js.map +1 -1
  111. package/node_modules/dsh-subagent-codex-pro/lib/tool.js +123 -57
  112. package/node_modules/dsh-subagent-codex-pro/lib/tool.js.map +1 -1
  113. package/node_modules/dsh-subagent-codex-pro/lib/types/config.d.ts +31 -0
  114. package/node_modules/dsh-subagent-codex-pro/lib/types/config.d.ts.map +1 -0
  115. package/node_modules/dsh-subagent-codex-pro/lib/types/desktop-native.d.ts +30 -0
  116. package/node_modules/dsh-subagent-codex-pro/lib/types/desktop-native.d.ts.map +1 -0
  117. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts +7 -5
  118. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts.map +1 -1
  119. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts +31 -0
  120. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts.map +1 -1
  121. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts +2 -0
  122. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts.map +1 -1
  123. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts.map +1 -1
  124. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts.map +1 -1
  125. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts +7 -0
  126. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts.map +1 -1
  127. package/node_modules/dsh-subagent-codex-pro/lib/types/native.d.ts +120 -0
  128. package/node_modules/dsh-subagent-codex-pro/lib/types/native.d.ts.map +1 -0
  129. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts +11 -0
  130. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts.map +1 -1
  131. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts +11 -0
  132. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts.map +1 -1
  133. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts +7 -1
  134. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts.map +1 -1
  135. package/node_modules/dsh-subagent-codex-pro/lib/wire.js +34 -6
  136. package/node_modules/dsh-subagent-codex-pro/lib/wire.js.map +1 -1
  137. package/node_modules/dsh-subagent-codex-pro/package.json +28 -4
  138. package/npm/install.js +2 -2
  139. package/package.json +25 -10
  140. package/plugins/dsh/KIMI_NATIVE_PROTOCOL.en.md +76 -0
  141. package/plugins/dsh/KIMI_NATIVE_PROTOCOL.md +76 -0
  142. package/plugins/dsh/PROTOCOL.md +151 -17
  143. package/plugins/dsh/README.md +15 -12
  144. package/plugins/dsh/README.zh-CN.md +8 -8
  145. package/plugins/dsh/THIRD_PARTY_NOTICES.md +5 -1
  146. package/plugins/dsh/vendor/README.md +40 -11
  147. package/plugins/dsh/vendor/chatgpt.json +21 -0
  148. package/plugins/dsh/vendor/claude-code-pro.json +21 -0
  149. package/plugins/dsh/vendor/codex-pro.json +13 -8
  150. package/plugins/dsh/vendor/lcu.json +21 -0
  151. package/plugins/dsh/vendor/preset-generator.json +16 -0
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gen-presets.mjs — materialize DSH agent presets from the installed base.
4
+ *
5
+ * ## Why this is not part of a plugin
6
+ *
7
+ * DSH agent presets have no inheritance: a preset's `plugins` list is its
8
+ * complete composition, and a patch replaces a whole entry rather than merging
9
+ * into it. So any custom preset has to restate the base preset, and the base
10
+ * ships inside the application and changes when DSH is upgraded.
11
+ *
12
+ * That job belongs to no single plugin. It started inside one, which meant a
13
+ * plugin's script was rewriting a profile region that also serves other plugins
14
+ * and the settings UI. Here it is a tool of its own: it reads the base, applies a
15
+ * policy, and writes the marked region of a profile patch.
16
+ *
17
+ * ## The marker is load-bearing
18
+ *
19
+ * `BEGIN` names this file's original path, and it has to keep naming it: a
20
+ * profile generated before this move already contains that exact string, and a
21
+ * different one would make this script append a second block instead of
22
+ * replacing the first.
23
+ *
24
+ * ## Usage
25
+ *
26
+ * node src/gen-presets.mjs # write into the desktop profile
27
+ * node src/gen-presets.mjs --dry-run # print, write nothing
28
+ * node src/gen-presets.mjs --out FILE # write to FILE instead
29
+ * node src/gen-presets.mjs --daily-only # refuse if the file already has heavy
30
+ * node src/gen-presets.mjs --asar PATH # override the app.asar path
31
+ * node src/gen-presets.mjs --profile DIR # override the profile directory
32
+ *
33
+ * @module dsh-preset-generator
34
+ */
35
+
36
+ import { closeSync, openSync, readFileSync, readSync, writeFileSync } from 'node:fs'
37
+ import { homedir } from 'node:os'
38
+ import { join } from 'node:path'
39
+ import { PRESETS } from './policy.mjs'
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Defaults
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const DEFAULT_ASAR =
46
+ '/Applications/DeepSeek Harness.app/Contents/Resources/app.asar'
47
+ const BASE_PRESET = 'dsh/node_modules/@deepseek-ai/dsh-web-app/presets/ptc.patch.yml'
48
+ const DEFAULT_PROFILE = join(homedir(), '.dsh', 'profiles', 'desktop')
49
+
50
+ const BEGIN = '# >>> BEGIN generated presets (scripts/gen-presets.mjs) >>>'
51
+ const END = '# <<< END generated presets <<<'
52
+
53
+ /** The two presets this script owns. Both are PTC: LCU now exposes its
54
+ * Computer/Browser runtime through the PTC `cua` bridge, not a native `js` tool.
55
+ * DSH 0.2.2 deprecates the presentation selector in favour of mounting
56
+ * `dsh-tool-executor-ptc` directly. */
57
+ // The pure SDK API consumes the same policies without running this CLI or
58
+ // writing the user's desktop profile. LCU attachment remains a separate grant.
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Minimal asar reader (Electron's asar is a simple header + flat blob)
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /**
65
+ * Read one file out of an asar archive without Electron.
66
+ * @param asarPath - absolute path to the `.asar` file
67
+ * @param innerPath - slash-separated path inside the archive
68
+ * @returns the file contents
69
+ */
70
+ function readAsarFile(asarPath, innerPath) {
71
+ const fd = openSync(asarPath, 'r')
72
+ try {
73
+ const head = Buffer.alloc(16)
74
+ readSync(fd, head, 0, 16, 0)
75
+ const baseOffset = 8 + head.readUInt32LE(4)
76
+ const jsonSize = head.readUInt32LE(12)
77
+ const json = Buffer.alloc(jsonSize)
78
+ readSync(fd, json, 0, jsonSize, 16)
79
+ let node = JSON.parse(json.toString('utf8'))
80
+ for (const segment of innerPath.split('/').filter(Boolean)) {
81
+ node = node.files?.[segment]
82
+ if (node === undefined) throw new Error(`not in archive: ${innerPath}`)
83
+ }
84
+ if (node.files !== undefined) throw new Error(`is a directory: ${innerPath}`)
85
+ const buf = Buffer.alloc(node.size)
86
+ readSync(fd, buf, 0, node.size, baseOffset + Number(node.offset))
87
+ return buf
88
+ } finally {
89
+ closeSync(fd)
90
+ }
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Line surgery
95
+ // ---------------------------------------------------------------------------
96
+
97
+ /** Indentation (0-based column of the first non-space char); -1 for blank lines. */
98
+ const indentOf = (line) => (line.trim() === '' ? -1 : line.search(/\S/))
99
+
100
+ /** Index of the child row `- id: <rowId>` inside a plugin list. */
101
+ function findRow(lines, rowId) {
102
+ const index = lines.findIndex((line) => line.trim().startsWith(`- id: ${rowId}`))
103
+ if (index < 0) throw new Error(`plugin row not found: ${rowId}`)
104
+ return index
105
+ }
106
+
107
+ /**
108
+ * Iterate the direct/indirect body lines of the row starting at `start`,
109
+ * stopping at the next row or any line dedented to the row's own level.
110
+ */
111
+ function* body(lines, start) {
112
+ const base = indentOf(lines[start])
113
+ for (let i = start + 1; i < lines.length; i += 1) {
114
+ const ind = indentOf(lines[i])
115
+ if (ind === -1) continue
116
+ if (ind <= base) return
117
+ yield i
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Remove `disabled: true` from one row, turning it on.
123
+ * @returns a new line array
124
+ */
125
+ function enableRow(lines, rowId) {
126
+ const start = findRow(lines, rowId)
127
+ for (const i of body(lines, start)) {
128
+ if (lines[i].trim().startsWith('disabled:')) {
129
+ return lines.filter((_, k) => k !== i)
130
+ }
131
+ }
132
+ return lines // already enabled — idempotent
133
+ }
134
+
135
+ /** Replace the base's tool-presentation selector with the concrete executor.
136
+ *
137
+ * DSH 0.2.2 carries `@deepseek-ai/dsh-tool-executor-ptc` in the base preset and
138
+ * deprecates `@deepseek-ai/dsh-agent-tool-presentation`. Older bases still have
139
+ * the selector with a `mode:` field. Both shapes are accepted here: the row is
140
+ * replaced in place, so the custom preset asks for the executor directly and
141
+ * never mounts the deprecated selector.
142
+ */
143
+ function setPresentation(lines, presentation) {
144
+ const rowId = `tool-executor-${presentation}`
145
+ const rowName = `@deepseek-ai/dsh-tool-executor-${presentation}`
146
+ const start = lines.findIndex((line) => {
147
+ const text = line.trim()
148
+ return /^- id: tool-executor-(native|ptc)$/.test(text) || text === '- id: tool-presentation'
149
+ })
150
+ if (start < 0) throw new Error('base preset has no tool-presentation or tool-executor row')
151
+
152
+ const base = indentOf(lines[start])
153
+ const indent = lines[start].slice(0, base)
154
+ let end = start + 1
155
+ while (end < lines.length) {
156
+ const ind = indentOf(lines[end])
157
+ if (ind !== -1 && ind <= base) break
158
+ end += 1
159
+ }
160
+ const trailing = []
161
+ while (end > start + 1 && lines[end - 1].trim() === '') {
162
+ trailing.unshift(lines[end - 1])
163
+ end -= 1
164
+ }
165
+ const replacement = [`${indent}- id: ${rowId}`, `${indent} name: '${rowName}'`]
166
+ return [...lines.slice(0, start), ...replacement, ...trailing, ...lines.slice(end)]
167
+ }
168
+
169
+ /** Extract the `plugins:` sub-tree (the row list) from a preset patch file. */
170
+ function extractPlugins(lines) {
171
+ const index = lines.findIndex((line) => /^\s*plugins:\s*$/.test(line))
172
+ if (index < 0) throw new Error('base preset has no `plugins:` key')
173
+ const base = indentOf(lines[index])
174
+ const out = []
175
+ for (let i = index + 1; i < lines.length; i += 1) {
176
+ const ind = indentOf(lines[i])
177
+ if (ind !== -1 && ind <= base) break
178
+ out.push(lines[i])
179
+ }
180
+ if (out.every((line) => line.trim() === '')) throw new Error('base `plugins:` is empty')
181
+ while (out.length > 0 && out[out.length - 1].trim() === '') out.pop()
182
+ return out
183
+ }
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Preset rendering
187
+ // ---------------------------------------------------------------------------
188
+
189
+ function renderPreset(spec, basePlugins) {
190
+ const plugins = spec.enable.reduce(enableRow, [...basePlugins])
191
+ const withMode = spec.presentation === null
192
+ ? plugins
193
+ : setPresentation(plugins, spec.presentation)
194
+ const out = [
195
+ ` - id: preset-${spec.id}`,
196
+ " name: '@deepseek-ai/dsh-agent-preset'",
197
+ ' config:',
198
+ ` id: ${spec.id}`,
199
+ ` name: '${spec.name}'`,
200
+ ` description: '${spec.description}'`,
201
+ ` order: ${spec.order}`,
202
+ ' plugins:',
203
+ ...withMode,
204
+ ...spec.append,
205
+ ]
206
+ return out
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Main
211
+ // ---------------------------------------------------------------------------
212
+
213
+ function parseArgs(argv) {
214
+ const opts = {
215
+ dryRun: false,
216
+ // Preserve both managed choices on routine upgrades. An existing heavy
217
+ // preset may be selected by the user or referenced by a plugin allowlist.
218
+ withHeavy: true,
219
+ out: undefined,
220
+ asar: DEFAULT_ASAR,
221
+ profile: DEFAULT_PROFILE,
222
+ }
223
+ for (let i = 0; i < argv.length; i += 1) {
224
+ const arg = argv[i]
225
+ if (arg === '--dry-run') opts.dryRun = true
226
+ else if (arg === '--with-heavy') opts.withHeavy = true
227
+ else if (arg === '--daily-only') opts.withHeavy = false
228
+ else if (arg === '--out') opts.out = argv[++i]
229
+ else if (arg === '--asar') opts.asar = argv[++i]
230
+ else if (arg === '--profile') opts.profile = argv[++i]
231
+ else throw new Error(`unknown argument: ${arg}`)
232
+ }
233
+ return opts
234
+ }
235
+
236
+ function buildBlock(baseLines, { withHeavy }) {
237
+ const basePlugins = extractPlugins(baseLines)
238
+ const specs = withHeavy ? [PRESETS.daily, PRESETS.heavy] : [PRESETS.daily]
239
+ const lines = ['- insert:']
240
+ for (const spec of specs) lines.push(...renderPreset(spec, basePlugins))
241
+ return { body: lines.join('\n'), basePlugins, specs }
242
+ }
243
+
244
+ /** Replace the marked block in place, or append one. */
245
+ /**
246
+ * Whether a top-level entry is the one this script wrote.
247
+ *
248
+ * The markers only say where the block lives, and the region is not exclusively
249
+ * ours: DSH's settings UI appends its own entries there. So the entry to replace
250
+ * is identified by what it contains — the `- insert:` shell carrying preset rows
251
+ * — rather than by being the only thing between the markers.
252
+ *
253
+ * @param entry - the lines of one top-level entry.
254
+ * @returns whether it is this script's.
255
+ */
256
+ function isOwnEntry(entry) {
257
+ return /^-\s+insert:\s*$/.test(entry[0]) && entry.some((line) => /^\s+- id: preset-/.test(line))
258
+ }
259
+
260
+ /**
261
+ * Replace this script's own entry inside the region, and nothing else.
262
+ *
263
+ * Everything else in the region is left exactly where it is. That is the whole
264
+ * point: entries the settings UI put there are neither moved nor re-emitted, so
265
+ * there is nothing to collect, nothing to de-duplicate, and no way for a
266
+ * repeated settings write to accumulate a copy per run.
267
+ *
268
+ * @param region - the text between the markers.
269
+ * @param body - the entry to write, without the markers.
270
+ * @returns the region with our entry replaced.
271
+ */
272
+ function replaceOwnEntry(region, body) {
273
+ const lines = region.split('\n')
274
+ const out = []
275
+ let replaced = false
276
+ let i = 0
277
+ while (i < lines.length) {
278
+ if (!/^-\s+\S/.test(lines[i])) {
279
+ out.push(lines[i])
280
+ i += 1
281
+ continue
282
+ }
283
+ const start = i
284
+ i += 1
285
+ while (i < lines.length && !/^-\s+\S/.test(lines[i])) i += 1
286
+ const entry = lines.slice(start, i)
287
+ if (!replaced && isOwnEntry(entry)) {
288
+ // The entry as consumed includes the blank lines that followed it, since
289
+ // they are not the start of another entry. They separate it from whatever
290
+ // comes next, so they are restored — otherwise the marker that follows ends
291
+ // up glued to the last line of the body.
292
+ const trailing = []
293
+ while (entry.length > 0 && entry[entry.length - 1].trim() === '') trailing.unshift(entry.pop())
294
+ out.push(...body.split('\n'), ...trailing)
295
+ replaced = true
296
+ continue
297
+ }
298
+ out.push(...entry)
299
+ }
300
+ // No entry of ours inside — a hand-edited file, or a region whose block was
301
+ // removed. Writing one is the same thing the first run does.
302
+ if (!replaced) out.unshift(body)
303
+ return out.join('\n')
304
+ }
305
+
306
+ function spliceBlock(text, body) {
307
+ const begin = text.indexOf(BEGIN)
308
+ const end = text.indexOf(END)
309
+ if (begin >= 0 && end > begin) {
310
+ // `region` runs from just after the BEGIN marker to just before END, so it
311
+ // already carries the newline that ends the marker line and the one before
312
+ // END. Adding either again would leave a blank line behind on every run.
313
+ const region = text.slice(begin + BEGIN.length, end)
314
+ return text.slice(0, begin)
315
+ + BEGIN + replaceOwnEntry(region, body) + END
316
+ + text.slice(end + END.length)
317
+ }
318
+ const separator = text === '' ? '' : text.endsWith('\n') ? '\n' : '\n\n'
319
+ return `${text}${separator}${BEGIN}\n${body}\n${END}\n`
320
+ }
321
+
322
+ function main() {
323
+ const opts = parseArgs(process.argv.slice(2))
324
+ const baseLines = readAsarFile(opts.asar, BASE_PRESET).toString('utf8').split('\n')
325
+ const { body, basePlugins, specs } = buildBlock(baseLines, opts)
326
+ const verb = opts.dryRun ? 'would write' : 'wrote'
327
+ const rowCount = basePlugins.filter((line) => /^\s*- id: /.test(line)).length
328
+
329
+ if (opts.dryRun) {
330
+ process.stdout.write(`${BEGIN}\n${body}\n${END}\n`)
331
+ } else {
332
+ const target = opts.out ?? join(opts.profile, 'cordis.patch.yml')
333
+ let before
334
+ let missing = false
335
+ try {
336
+ before = readFileSync(target, 'utf8')
337
+ } catch (error) {
338
+ // A new profile may not have a patch yet. Only absence is an empty
339
+ // document; an unreadable or invalid existing target must still fail.
340
+ if (error.code !== 'ENOENT') throw error
341
+ before = ''
342
+ missing = true
343
+ }
344
+ // Never delete a preset this file already had. A re-run is the documented
345
+ // response to a DSH upgrade, and it must not be able to take away the mode a
346
+ // user relies on as a side effect of an argument they did not pass.
347
+ for (const preset of ['daily', 'heavy']) {
348
+ const had = before.includes(`- id: preset-${preset}`)
349
+ const keeps = body.includes(`- id: preset-${preset}`)
350
+ if (had && !keeps) {
351
+ throw new Error(
352
+ `${target} already defines preset-${preset} and this run would remove it. `
353
+ + 'Re-run without --daily-only to keep every preset the file has.',
354
+ )
355
+ }
356
+ }
357
+ // Do not overwrite a patch another writer created after the missing read.
358
+ writeFileSync(target, spliceBlock(before, body), { mode: 0o600, flag: missing ? 'wx' : 'w' })
359
+ process.stderr.write(`${verb} ${target}\n`)
360
+ }
361
+ process.stderr.write(
362
+ `base: ${BASE_PRESET} (${rowCount} top-level rows)\n`
363
+ + `presets: ${specs.map((s) => s.id).join(', ')}\n`,
364
+ )
365
+ }
366
+
367
+ main()
@@ -0,0 +1,17 @@
1
+ /** Shared desktop and SDK intent. Names describe use, not separate GUI grants. */
2
+ export const PRESETS = Object.freeze({
3
+ daily: Object.freeze({
4
+ id: 'daily', name: '日常', description: '日常编码:PTC 程序化工具调用 + Codex 委派',
5
+ order: 5, presentation: 'ptc', enable: Object.freeze([]), append: Object.freeze([]),
6
+ }),
7
+ heavy: Object.freeze({
8
+ id: 'heavy', name: '重活', description: '重活:PTC Computer/Browser CUA + Codex 委派',
9
+ order: 6, presentation: 'ptc', enable: Object.freeze([]), append: Object.freeze([]),
10
+ }),
11
+ })
12
+
13
+ /** Return a detached policy; callers cannot alter future generated presets. */
14
+ export function presetPolicy(id) {
15
+ if (!Object.hasOwn(PRESETS, id)) throw new TypeError(`Unknown managed preset: ${String(id)}`)
16
+ return structuredClone(PRESETS[id])
17
+ }
@@ -1,6 +1,21 @@
1
1
  # Changelog
2
2
 
3
- ## 0.9.0 (source candidate, unreleased)
3
+ ## 0.10.0 (source candidate)
4
+
5
+ - Add a real DSH native settings page, a live Config schema, whole-object per-call overrides and explicit null/off semantics. Keep default inheritance distinct from off.
6
+ - Wire desktop/PTC delegation and the Host provider through native validation, app-server configuration, child lifecycle and receipts; invalidate implicit session reuse when the effective binding changes.
7
+ - Add a dedicated fresh, read-only subagent_codex_review tool. Review cannot become an ordinary delegation default or silently ignore prompt/schema/resume arguments.
8
+ - Preserve native receipts in direct tools and background results, report bounded Host evidence, reject unfinished children, and retain image attachment blocks in PTC rendering.
9
+ - Validate schemas with the actual DSH closed subset and PTC SDK generator, validate serialized settings before save, and exercise real React revisions/off/reset plus plugin-to-process cancellation and cleanup.
10
+
11
+ - Add optional native capability v1 controls, verified scoped Codex configuration, V2 backend selection and explicit off across both backends.
12
+ - Preserve full native agent tools/permissions; collect owned child states, model evidence and deduplicated baseline-aware usage without mixing observations into the model answer.
13
+ - Add independent fresh-thread native review, correct review model/effort configuration and explicit native combination constraints.
14
+ - Propagate child events and terminal native evidence through the existing guardian, machine supervisor and read-only recovery lifecycle.
15
+ - Keep the current child turn authoritative under replay and late completion, including failed terminal turns.
16
+ - Keep protocol 1, authentication and absent-native behavior compatible. See [native capabilities](docs/native-capabilities.md) for exact evidence and validation limits.
17
+
18
+ ## 0.9.0 (2026-11-12)
4
19
 
5
20
  - Add lifecycle v1 with awaited configuration-checked thread-ready, dispatch-intent,
6
21
  dispatch-possible and turn-accepted checkpoints. A failed ACK cannot silently
@@ -14,12 +14,65 @@ tools.subagent_codex({
14
14
  })
15
15
  ```
16
16
 
17
- ## 0.9.0 source candidate: durable lifecycle and recovery
17
+ ## Native capabilities in DSH and the machine API
18
+
19
+ The 0.10.0 source candidate exposes native agents in the **plugin itself**:
20
+ open the installed bundle in DSH's Plugins page and configure its **codex-pro**
21
+ row. The page offers inheritance, explicit off, concurrency, child model/effort,
22
+ and the V2 backend. Saving applies to the next invocation without remounting the
23
+ plugin; an active run keeps its original snapshot. A conflicting save retains
24
+ your draft until you explicitly reload.
25
+
26
+ `subagent_codex` accepts a per-call `native` value. DSH's existing PTC executor
27
+ automatically generates this tool's SDK; native Codex agents still use Codex's
28
+ own harness and tools. There is no separate PTC executor to install.
29
+
30
+ ```js
31
+ await tools.subagent_codex({
32
+ description: "Implement independent changes",
33
+ prompt: "Implement the task; delegate independent work when useful.",
34
+ native: { version: 1, agents: { enabled: true, maxConcurrentThreads: 3, backend: "v2" } },
35
+ })
36
+ await tools.subagent_codex_review({
37
+ description: "Review the branch",
38
+ target: { type: "baseBranch", branch: "main" },
39
+ })
40
+ ```
41
+
42
+ | Desktop native value | Meaning |
43
+ | --- | --- |
44
+ | Omitted | Read the current plugin default; if none is configured, leave the choice to the CLI. |
45
+ | `null` | Explicitly turn agents off across both backends. |
46
+ | A version-1 object | Replace the entire native default, without merging nested fields. |
47
+ | `{version:1, agents:{enabled:false}}` | Explicit off; active controls beside `enabled:false` are rejected. |
48
+
49
+ A present object without `agents` also turns agents off. An enabled override
50
+ under `modelSelection:false` must preserve exactly the configured child model
51
+ and effort, including whether they are omitted. Inherited settings are allowed;
52
+ changing or clearing those model choices requires editing the plugin settings.
53
+ Native controls never expand the configured permission policy.
54
+
55
+ Native review is a **separate, explicitly requested operation**:
56
+ `subagent_codex_review` always creates a fresh ephemeral read-only thread with
57
+ agents off. It accepts an explicit target and never accepts an ignored prompt,
58
+ resume id, image list or output schema. Normal delegation defaults cannot select
59
+ review.
60
+
61
+ Direct tool results keep full evidence in `native` alongside clean `content`.
62
+ Background jobs with native evidence return a JSON string envelope containing
63
+ `text`, `threadId` and `native`; jobs without native controls keep their
64
+ existing text result. The Host provider reads the same live defaults and reports
65
+ bounded native diagnostics through the official Host contract. The standalone
66
+ machine API retains its full receipt and recovery journal. See the
67
+ [complete contract](docs/native-capabilities.md) / [中文契约](docs/native-capabilities.zh.md)
68
+ for settings, per-call policy, provider boundaries, evidence and verification.
69
+
70
+ ## 0.9.0: durable lifecycle and recovery
18
71
 
19
72
  The public machine API now exposes awaitable typed checkpoints, a private
20
73
  per-invocation journal, and read-only recovery. An independent machine supervisor
21
- preserves the original terminal receipt after caller death. The source candidate
22
- is **not an npm release**. See the [lifecycle contract](docs/machine-lifecycle.md)
74
+ preserves the original terminal receipt after caller death. Version 0.10.0 remains
75
+ an unpublished source candidate. See the [lifecycle contract](docs/machine-lifecycle.md)
23
76
  and [中文说明](docs/machine-lifecycle.zh.md); existing DSH tool behavior is unchanged.
24
77
 
25
78
  ## 0.8.0 integration and migration
@@ -544,7 +597,7 @@ completed commands and file changes, plus Codex's `commentary` messages — so
544
597
  | continuable / thread reuse | ✅ `session` reuses one; `thread_id` continues any |
545
598
  | `maxDepth` / persona / `toolFilter` | ❌ not applicable — see below |
546
599
 
547
- The three refusals above are not gaps to be filled. A Codex delegation is a **leaf**: it has no DSH child
600
+ The three refusals above are not gaps to be filled. A Codex delegation is a **DSH leaf**: it has no DSH child
548
601
  agent, so a delegation-depth cap has nothing to cap. Its tools are Codex's own — the shell, the patch tool, its
549
602
  own MCP servers — not this harness's, so a **tool filter**, which names DSH tools, cannot describe them; restrict
550
603
  Codex through its own configuration instead. And a **persona** is a system instruction for a child agent, where
@@ -0,0 +1,118 @@
1
+ /* DSH browser closure format. React is supplied by the Host module table. */
2
+ window.__ModuleLoader__.load({
3
+ id: 'dsh-subagent-codex-pro',
4
+ factory(require) {
5
+ const React = require('react')
6
+ const h = React.createElement
7
+ const zh = typeof navigator !== 'undefined' && /^zh\b/i.test(navigator.language)
8
+ const copy = zh ? {
9
+ title: 'Codex 原生协作', mode: '默认能力', inherit: '继承配置', off: '全部关闭', agents: '启用原生 Agent',
10
+ limit: '最多并行子线程(留空使用 CLI 默认)', model: '子 Agent 默认模型(可选)', effort: '子 Agent 默认思考等级(可选)',
11
+ backend: '使用 v2 Agent 后端', save: '保存', reload: '重新载入', saving: '保存中…', saved: '已保存;下一次调用使用新设置。',
12
+ conflict: '未保存:配置可能已更新或不可写。草稿已保留;重新载入后再编辑。', unavailable: '当前配置不可用或不可写。',
13
+ invalidLimit: '并行数必须是大于零且小于 Number.MAX_SAFE_INTEGER 的整数。', invalidText: '子模型和思考等级不能包含 NUL,且 UTF-8 长度不能超过 65536 字节。', effective: '当前生效的 native 配置',
14
+ note: '每次调用可整体覆盖默认值。关闭或继承不会保存下面的 Agent 参数;独立 review 使用 subagent_codex_review 工具。模型选择关闭时,调用者不得改变配置中的子模型与思考等级。',
15
+ summary: '原生 Agent 默认配置与并行能力。',
16
+ } : {
17
+ title: 'Codex native collaboration', mode: 'Default capabilities', inherit: 'Inherit configuration', off: 'All off', agents: 'Enable native Agents',
18
+ limit: 'Maximum concurrent child threads (blank: CLI default)', model: 'Default child model (optional)', effort: 'Default child effort (optional)',
19
+ backend: 'Use the v2 Agent backend', save: 'Save', reload: 'Reload', saving: 'Saving…', saved: 'Saved. New settings apply to the next invocation.',
20
+ conflict: 'Not saved: configuration changed or is not writable. Your draft is retained; reload before editing again.', unavailable: 'Configuration is unavailable or read-only.',
21
+ invalidLimit: 'Concurrency must be a positive integer below Number.MAX_SAFE_INTEGER.', invalidText: 'Child model and effort cannot contain NUL or exceed 65536 UTF-8 bytes.', effective: 'Currently effective native configuration',
22
+ note: 'An invocation can replace these defaults as a whole. Off and inherit do not save Agent fields. Independent review uses subagent_codex_review. With model selection disabled, callers cannot change the configured child model or effort.',
23
+ summary: 'Native Agent defaults and concurrency.',
24
+ }
25
+ const own = (value, key) => value !== null && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, key)
26
+ const draftOf = state => {
27
+ const native = state?.value?.native
28
+ const agents = native && typeof native === 'object' ? native.agents : undefined
29
+ return {
30
+ mode: !own(state?.user, 'native') ? 'inherit' : native === null || agents?.enabled !== true ? 'off' : 'agents',
31
+ limit: agents?.maxConcurrentThreads === undefined ? '' : String(agents.maxConcurrentThreads),
32
+ model: agents?.defaultModel ?? '', effort: agents?.defaultEffort ?? '', backend: agents?.backend === 'v2',
33
+ }
34
+ }
35
+ const mutationOf = draft => {
36
+ if (draft.mode === 'inherit') return [{ op: 'unset', path: ['native'] }]
37
+ if (draft.mode === 'off') return [{ op: 'set', path: ['native'], value: null }]
38
+ const agents = { enabled: true }
39
+ if (draft.limit.trim() !== '') {
40
+ const n = Number(draft.limit)
41
+ if (!Number.isSafeInteger(n) || n < 1 || n >= Number.MAX_SAFE_INTEGER) throw new Error(copy.invalidLimit)
42
+ agents.maxConcurrentThreads = n
43
+ }
44
+ for (const [field, value] of [['defaultModel', draft.model.trim()], ['defaultEffort', draft.effort.trim()]]) {
45
+ if (value.includes('\0') || new TextEncoder().encode(value).byteLength > 65536) throw new Error(copy.invalidText)
46
+ if (value !== '') agents[field] = value
47
+ }
48
+ if (draft.backend) agents.backend = 'v2'
49
+ return [{ op: 'set', path: ['native'], value: { version: 1, agents } }]
50
+ }
51
+ function Editor({ form }) {
52
+ const state = form?.state
53
+ const [draft, setDraft] = React.useState(() => draftOf(state))
54
+ const [revision, setRevision] = React.useState(state?.revision)
55
+ const [dirty, setDirty] = React.useState(false)
56
+ const [busy, setBusy] = React.useState(false)
57
+ const [notice, setNotice] = React.useState('')
58
+ const mounted = React.useRef(true)
59
+ React.useEffect(() => { mounted.current = true; return () => { mounted.current = false } }, [])
60
+ React.useEffect(() => {
61
+ if (!dirty && !busy) { setDraft(draftOf(state)); setRevision(state?.revision) }
62
+ }, [state?.revision, state?.status, dirty, busy])
63
+ const ready = state?.status === 'ready' && state.writable && Number.isSafeInteger(state.revision)
64
+ const change = (field, value) => {
65
+ if (!ready || busy) return
66
+ if (!dirty) setRevision(state.revision)
67
+ setDraft(previous => ({ ...previous, [field]: value }))
68
+ setDirty(true); setNotice('')
69
+ }
70
+ const reload = () => {
71
+ setDraft(draftOf(state)); setRevision(state?.revision); setDirty(false); setNotice('')
72
+ }
73
+ const save = async event => {
74
+ event.preventDefault()
75
+ if (!ready || busy || !dirty) return
76
+ let ops
77
+ try { ops = mutationOf(draft) } catch (error) { setNotice(error.message); return }
78
+ setBusy(true); setNotice('')
79
+ try {
80
+ const accepted = await form.mutate(ops, revision)
81
+ if (!mounted.current) return
82
+ if (accepted) { setDirty(false); setNotice(copy.saved) }
83
+ else setNotice(copy.conflict)
84
+ } catch (error) {
85
+ if (mounted.current) setNotice(String(error?.message ?? error))
86
+ } finally { if (mounted.current) setBusy(false) }
87
+ }
88
+ const textField = (key, title, type = 'text') => h('label', { style: { display: 'grid', gap: 4 } }, title,
89
+ h('input', { type, value: draft[key], onChange: event => change(key, event.target.value), ...(type === 'number' ? { min: 1, step: 1 } : {}) }))
90
+ return h('form', { onSubmit: save, style: { display: 'grid', gap: 12 } },
91
+ h('h3', null, copy.title), h('p', null, copy.note),
92
+ h('fieldset', { disabled: !ready || busy, style: { display: 'grid', gap: 12, border: 0, padding: 0, margin: 0 } },
93
+ h('label', null, copy.mode, h('select', { value: draft.mode, onChange: event => change('mode', event.target.value) },
94
+ ...['inherit', 'off', 'agents'].map(value => h('option', { key: value, value }, copy[value])))),
95
+ draft.mode === 'agents' ? h('div', { style: { display: 'grid', gap: 10 } },
96
+ textField('limit', copy.limit, 'number'), textField('model', copy.model), textField('effort', copy.effort),
97
+ h('label', null, h('input', { type: 'checkbox', checked: draft.backend, onChange: event => change('backend', event.target.checked) }), ' ', copy.backend)) : null,
98
+ h('div', { style: { display: 'flex', gap: 8 } },
99
+ h('button', { type: 'submit', disabled: !dirty }, busy ? copy.saving : copy.save),
100
+ h('button', { type: 'button', onClick: reload, disabled: !dirty }, copy.reload))),
101
+ !ready ? h('p', { role: 'status' }, copy.unavailable) : null,
102
+ notice ? h('p', { role: 'status', 'aria-live': 'polite' }, notice) : null,
103
+ h('details', null, h('summary', null, copy.effective),
104
+ h('pre', { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, state?.value?.native === undefined ? '(CLI / inherited defaults)' : JSON.stringify(state.value.native, null, 2))))
105
+ }
106
+ function NativeSettings(props) {
107
+ return props.view === 'summary' ? h('span', null, copy.summary) : h(Editor, { form: props.form })
108
+ }
109
+ return {
110
+ inject: ['slots'],
111
+ apply(ctx) {
112
+ ctx.effect(() => ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
113
+ name: 'plugins.row.config', key: 'dsh-subagent-codex-pro#codex-pro',
114
+ }, NativeSettings)), 'codex-pro native settings')
115
+ },
116
+ }
117
+ },
118
+ })
@@ -28,3 +28,9 @@
28
28
  #
29
29
  # `modelSelection: false` hides `model` and `reasoning_effort` from the
30
30
  # tool schema entirely.
31
+
32
+ #
33
+ # Native defaults are intentionally unset. Configure the codex-pro row
34
+ # in the Plugins page or add version: 1 / agents here. native: null
35
+ # explicitly disables agents; omission inherits CLI defaults.
36
+ # Review is always an explicit subagent_codex_review operation.