dsh-creator-mode-plus 0.3.8 → 0.3.11
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/AGENTS.md +15 -3
- package/CHANGELOG.md +35 -1
- package/README.en.md +29 -5
- package/README.md +29 -5
- package/cordis.patch.yml +3 -0
- package/docs/bridge-contract.md +25 -38
- package/docs/development-compilation.md +100 -0
- package/docs/development-integration-plan.md +199 -0
- package/docs/development-pre-wiring-baseline.md +26 -0
- package/docs/dshx-v0.7-alignment.md +24 -9
- package/docs/executor-authorization-design.md +416 -0
- package/dshx.yml +7 -0
- package/package.json +17 -3
- package/preset/skills/creator-mode-plus/SKILL.md +11 -0
- package/preset/skills/creator-mode-plus/maintenance.md +5 -11
- package/scripts/install.mjs +225 -7
- package/scripts/verify-dshx.mjs +1 -1
- package/scripts/verify-harness-install.mjs +10 -8
- package/src/client.js +1 -1
- package/src/compatibility.js +19 -7
- package/src/core-boundary.js +151 -0
- package/src/delivery.js +1 -1
- package/src/desktop-profile.js +95 -0
- package/src/development-execution.js +237 -0
- package/src/development-invocation.js +267 -0
- package/src/development-policy.js +216 -0
- package/src/development-target.js +569 -0
- package/src/development-tasks.js +342 -0
- package/src/index.js +102 -35
- package/src/runner.js +43 -25
- package/src/safety.js +50 -4
- package/src/takeover.js +3 -3
package/scripts/install.mjs
CHANGED
|
@@ -27,7 +27,8 @@ const LEGACY_ROWS = [
|
|
|
27
27
|
]
|
|
28
28
|
const LEGACY_SIX_TOOL_PERSONA = 'Create file-backed DeepSeek Harness plugins through the six-tool DSHX v0.7 fixed bridge. Treat the official browser WebUI and public Cordis/client extension points as the compatibility target. App-shell APIs and wrapper-specific behavior are outside the supported surface. Harness update planning is read-only inside this session; prepare, verify, apply, rollback, and process control belong to the external DSHX supervisor.'
|
|
29
29
|
const SEVEN_TOOL_PERSONA = 'Create and safely remove file-backed DeepSeek Harness plugins through the seven-tool DSHX v0.7 fixed bridge. Treat the official browser WebUI and public Cordis/client extension points as the compatibility target. App-shell APIs and wrapper-specific behavior are outside the supported surface. Whole-plugin teardown must use dshx_remove_plugin so the live Host deactivates first and source is preserved. Harness update planning is read-only inside this session; prepare, verify, apply, rollback, and process control belong to the external DSHX supervisor.'
|
|
30
|
-
const
|
|
30
|
+
const LEGACY_CURRENT_PERSONA = `${SEVEN_TOOL_PERSONA} DSH.app, direct dsh web, and dshx are launchers for one long-lived Web Host per DSH_HOME; never start a second same-Home Host or keep an isolated verifier alive.`
|
|
31
|
+
const CURRENT_PERSONA = 'Create and safely remove external DeepSeek Harness plugins through the fixed DSHX v0.7 bridge and public Cordis/client extension points. Official Harness source, shipped presets, installed official packages and their generated artifacts are read-only, including copies and worktrees. Never add a Host API, patch official files or rebuild official subprojects for a plugin. Report a missing public API or use a plugin-only alternative. User takeover, automatic approval and --force cannot override CORE_SOURCE_IMMUTABLE. Whole-plugin teardown must use dshx_remove_plugin to deactivate the live Host first and preserve source. Harness version inventory is read-only; source-changing update stages are disabled even for external supervisors. DSH.app, direct dsh web, and dshx are launchers for one long-lived Web Host per DSH_HOME; never start a second same-Home Host or keep an isolated verifier alive.'
|
|
31
32
|
const LEGACY_SIX_TOOL_COMMENT = '# Bridge v2: six fixed dshx tools plus external Guardian lifecycle hooks; no shell, arbitrary argv, or model process control.'
|
|
32
33
|
const SEVEN_TOOL_COMMENT = '# Bridge v2: seven fixed dshx tools plus external Guardian lifecycle hooks; no arbitrary argv, raw plugin teardown, or model process control.'
|
|
33
34
|
|
|
@@ -35,6 +36,16 @@ const STANDARD_PRESET_PATHS = [
|
|
|
35
36
|
'packages/preset/agent-presets/presets/standard',
|
|
36
37
|
'apps/cli/config/agent-presets/standard',
|
|
37
38
|
]
|
|
39
|
+
const STANDARD_PATCH_PATH = 'packages/bundle/web-app/presets/standard.patch.yml'
|
|
40
|
+
const WEB_PROFILE = 'web'
|
|
41
|
+
const PROFILE_INCLUDE_PATH = 'creator-mode-plus/agent.cordis.yml'
|
|
42
|
+
const PROFILE_INCLUDE = `# Creator Mode+ preset include. The installer owns this block.
|
|
43
|
+
- insert:
|
|
44
|
+
- id: creator-mode-plus-preset
|
|
45
|
+
name: cordis:include
|
|
46
|
+
config:
|
|
47
|
+
path: ${PROFILE_INCLUDE_PATH}
|
|
48
|
+
`
|
|
38
49
|
|
|
39
50
|
export function standardPresetAt(root) {
|
|
40
51
|
for (const relative of STANDARD_PRESET_PATHS) {
|
|
@@ -44,6 +55,139 @@ export function standardPresetAt(root) {
|
|
|
44
55
|
throw new Error(`Creator Mode+ installer cannot find the shipped Standard preset under ${root}`)
|
|
45
56
|
}
|
|
46
57
|
|
|
58
|
+
/** Shipped Standard composition. 0.1.7 stores it as a bundle patch; older lines use a preset directory. */
|
|
59
|
+
export function standardSourceAt(root) {
|
|
60
|
+
const patch = join(root, STANDARD_PATCH_PATH)
|
|
61
|
+
if (existsSync(patch)) return Object.freeze({ kind: 'patch', path: patch })
|
|
62
|
+
return Object.freeze({ kind: 'directory', path: standardPresetAt(root) })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function escapeRegExp(value) {
|
|
66
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function countManagedRow(text, row) {
|
|
70
|
+
const exact = exactRowCount(text, row)
|
|
71
|
+
if (exact > 0) return exact
|
|
72
|
+
const [idLine, nameLine] = row.split('\n')
|
|
73
|
+
const pattern = new RegExp(
|
|
74
|
+
`^[ \\t]*${escapeRegExp(idLine)}\\n[ \\t]*${escapeRegExp(nameLine.trim())}(?=\\n|$)`,
|
|
75
|
+
'gm',
|
|
76
|
+
)
|
|
77
|
+
return [...text.matchAll(pattern)].length
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function reindentPlugins(plugins) {
|
|
81
|
+
const body = plugins.split('\n').map(line => (line.startsWith(' ') ? line.slice(4) : line)).join('\n')
|
|
82
|
+
return body.endsWith('\n') ? body : `${body}\n`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rewritePatchPersona(plugins) {
|
|
86
|
+
const match = /^([ \t]*)prefix: You are a coding agent powered by the \{\{model\}\} model\.$/m.exec(plugins)
|
|
87
|
+
if (!match) {
|
|
88
|
+
throw new Error('Creator Mode+ installer expected the shipped Standard persona prefix')
|
|
89
|
+
}
|
|
90
|
+
const indent = match[1]
|
|
91
|
+
const lines = [
|
|
92
|
+
'You are Creator Mode+, a coding agent powered by the {{model}} model.',
|
|
93
|
+
'',
|
|
94
|
+
CURRENT_PERSONA,
|
|
95
|
+
'',
|
|
96
|
+
'Load the `creator-mode-plus` skill before creating, activating, removing, hot-reloading, updating Harness, or validating a DSH plugin. Keep Harness core and shipped presets unchanged.',
|
|
97
|
+
]
|
|
98
|
+
const body = lines.map(line => (line.length === 0 ? '' : `${indent} ${line}`)).join('\n')
|
|
99
|
+
const replacement = `${indent}prefix: |-\n${body}`
|
|
100
|
+
return plugins.slice(0, match.index) + replacement + plugins.slice(match.index + match[0].length)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function deriveCreatorDeclaration(standardPatch) {
|
|
104
|
+
const marker = '\n plugins:\n'
|
|
105
|
+
const index = standardPatch.indexOf(marker)
|
|
106
|
+
if (index < 0 || standardPatch.indexOf(marker, index + marker.length) >= 0) {
|
|
107
|
+
throw new Error('Creator Mode+ installer expected exactly one Standard preset plugin list')
|
|
108
|
+
}
|
|
109
|
+
let plugins = rewritePatchPersona(reindentPlugins(standardPatch.slice(index + marker.length)))
|
|
110
|
+
plugins = replaceOnce(
|
|
111
|
+
plugins,
|
|
112
|
+
` - id: skill-filesystem\n name: '@deepseek-ai/dsh-skill-filesystem'`,
|
|
113
|
+
` - id: skill-filesystem\n name: '@deepseek-ai/dsh-skill-filesystem'\n config:\n customSkillDirs:\n - !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('skills/', baseUrl))"`,
|
|
114
|
+
'skill filesystem',
|
|
115
|
+
)
|
|
116
|
+
plugins = replaceOnce(
|
|
117
|
+
plugins,
|
|
118
|
+
` - id: tool-skill\n name: '@deepseek-ai/dsh-tool-skill'`,
|
|
119
|
+
` - id: tool-skill\n name: '@deepseek-ai/dsh-tool-skill'\n\n # Bridge v2: seven fixed dshx tools plus external Guardian lifecycle hooks; no arbitrary argv, raw plugin teardown, or model process control.\n - id: dsh-creator-mode-plus\n name: dsh-creator-mode-plus`,
|
|
120
|
+
'tool skill',
|
|
121
|
+
)
|
|
122
|
+
const description = '在 DSH 对话中创建、修改并加载插件,继续验证实际功能;支持保留当前会话的热更新和故障恢复。'
|
|
123
|
+
return `# Creator Mode+ starts from the shipped Standard preset and adds the fixed dshx bridge.
|
|
124
|
+
- id: preset-creator-mode-plus
|
|
125
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
126
|
+
config:
|
|
127
|
+
id: creator-mode-plus
|
|
128
|
+
name: Creator Mode+
|
|
129
|
+
description: ${JSON.stringify(description)}
|
|
130
|
+
order: 20
|
|
131
|
+
plugins:
|
|
132
|
+
${plugins}`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function profilePresetDir(dshHome) {
|
|
136
|
+
return join(dshHome, 'profiles', WEB_PROFILE, 'creator-mode-plus')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function withoutPatchComments(text) {
|
|
140
|
+
return text.replace(/^[ \t]*#.*$/gm, '').trim()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ensureProfileInclude(profileDir) {
|
|
144
|
+
mkdirSync(profileDir, { recursive: true })
|
|
145
|
+
const patchPath = join(profileDir, 'cordis.patch.yml')
|
|
146
|
+
const existing = existsSync(patchPath) ? readFileSync(patchPath, 'utf8') : ''
|
|
147
|
+
const body = withoutPatchComments(existing)
|
|
148
|
+
// Official profile init writes a comment header plus an empty array. A later
|
|
149
|
+
// sequence item after `[]` is not a patch entry, so replace that array.
|
|
150
|
+
if (body === '[]' || body.startsWith('[]\n')) {
|
|
151
|
+
const repaired = existing.replace(/^[ \t]*\[\][ \t]*\r?\n/m, '')
|
|
152
|
+
const next = repaired.includes(`path: ${PROFILE_INCLUDE_PATH}`)
|
|
153
|
+
? repaired
|
|
154
|
+
: `${repaired.endsWith('\n') || repaired.length === 0 ? repaired : `${repaired}\n`}${PROFILE_INCLUDE}`
|
|
155
|
+
if (next !== existing) writeFileSync(patchPath, next)
|
|
156
|
+
return patchPath
|
|
157
|
+
}
|
|
158
|
+
if (existing.includes(`path: ${PROFILE_INCLUDE_PATH}`)) return patchPath
|
|
159
|
+
const prefix = existing.length === 0 || existing.endsWith('\n') ? existing : `${existing}\n`
|
|
160
|
+
writeFileSync(patchPath, `${prefix}${PROFILE_INCLUDE}`)
|
|
161
|
+
return patchPath
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function writeProfilePreset(target, composition) {
|
|
165
|
+
const root = dirname(target)
|
|
166
|
+
mkdirSync(root, { recursive: true })
|
|
167
|
+
const temporaryRoot = mkdtempSync(join(root, '.dsh-creator-mode-plus-install-'))
|
|
168
|
+
const staging = join(temporaryRoot, 'creator-mode-plus')
|
|
169
|
+
try {
|
|
170
|
+
mkdirSync(staging, { recursive: true })
|
|
171
|
+
writeFileSync(join(staging, 'agent.cordis.yml'), composition)
|
|
172
|
+
cpSync(join(packageRoot, 'preset/preset.yml'), join(staging, 'preset.yml'))
|
|
173
|
+
cpSync(join(packageRoot, 'preset/skills'), join(staging, 'skills'), { recursive: true })
|
|
174
|
+
tightenTree(staging)
|
|
175
|
+
renameSync(staging, target)
|
|
176
|
+
} finally {
|
|
177
|
+
rmSync(temporaryRoot, { recursive: true, force: true })
|
|
178
|
+
}
|
|
179
|
+
ensureProfileInclude(root)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function recognizedLegacyRow(composition) {
|
|
183
|
+
const matches = LEGACY_ROWS.filter(row => countManagedRow(composition, row) === 1)
|
|
184
|
+
const legacyCount = LEGACY_ROWS.reduce((count, row) => count + countManagedRow(composition, row), 0)
|
|
185
|
+
if (legacyCount !== 1 || matches.length !== 1) {
|
|
186
|
+
throw new Error('Creator Mode+ preset does not contain exactly one recognized managed plugin row; refusing an unsafe update')
|
|
187
|
+
}
|
|
188
|
+
return matches[0]
|
|
189
|
+
}
|
|
190
|
+
|
|
47
191
|
function replaceOnce(text, search, replacement, label) {
|
|
48
192
|
const first = text.indexOf(search)
|
|
49
193
|
if (first < 0 || text.indexOf(search, first + search.length) >= 0) {
|
|
@@ -116,11 +260,11 @@ function creatorComposition(standard) {
|
|
|
116
260
|
function migrateManagedSafetyCopy(text) {
|
|
117
261
|
let next = migratePersona015(text)
|
|
118
262
|
for (const [before, after] of [
|
|
119
|
-
[
|
|
263
|
+
[LEGACY_CURRENT_PERSONA, CURRENT_PERSONA],
|
|
264
|
+
[LEGACY_SIX_TOOL_PERSONA, CURRENT_PERSONA],
|
|
120
265
|
[SEVEN_TOOL_PERSONA, CURRENT_PERSONA],
|
|
121
266
|
[LEGACY_SIX_TOOL_COMMENT, SEVEN_TOOL_COMMENT],
|
|
122
267
|
]) {
|
|
123
|
-
if (before === SEVEN_TOOL_PERSONA && next.includes(CURRENT_PERSONA)) continue
|
|
124
268
|
const first = next.indexOf(before)
|
|
125
269
|
if (first < 0) continue
|
|
126
270
|
if (next.indexOf(before, first + before.length) >= 0) {
|
|
@@ -135,9 +279,9 @@ function refreshManagedAssets(target, root, migrateLegacy) {
|
|
|
135
279
|
const compositionPath = join(target, 'agent.cordis.yml')
|
|
136
280
|
const originalComposition = existsSync(compositionPath) ? readFileSync(compositionPath, 'utf8') : ''
|
|
137
281
|
let composition = originalComposition
|
|
138
|
-
const currentCount =
|
|
139
|
-
const matchingLegacyRows = LEGACY_ROWS.filter(row =>
|
|
140
|
-
const legacyCount = LEGACY_ROWS.reduce((count, row) => count +
|
|
282
|
+
const currentCount = countManagedRow(composition, CURRENT_ROW)
|
|
283
|
+
const matchingLegacyRows = LEGACY_ROWS.filter(row => countManagedRow(composition, row) === 1)
|
|
284
|
+
const legacyCount = LEGACY_ROWS.reduce((count, row) => count + countManagedRow(composition, row), 0)
|
|
141
285
|
|
|
142
286
|
if (currentCount === 1 && legacyCount === 0) {
|
|
143
287
|
// Current composition remains user-owned; only managed assets are refreshed.
|
|
@@ -185,6 +329,70 @@ function refreshManagedAssets(target, root, migrateLegacy) {
|
|
|
185
329
|
}
|
|
186
330
|
}
|
|
187
331
|
|
|
332
|
+
function installFromStandardPatch({ sourcePath, dshHome, compatibility, upgrade, migrateLegacy }) {
|
|
333
|
+
const target = profilePresetDir(dshHome)
|
|
334
|
+
const legacyDirs = [
|
|
335
|
+
join(dshHome, '.agent-presets', CURRENT_PRESET_ID),
|
|
336
|
+
join(dshHome, '.agent-presets', LEGACY_PRESET_ID),
|
|
337
|
+
].filter(path => existsSync(path))
|
|
338
|
+
const currentExists = existsSync(target)
|
|
339
|
+
const result = (action) => ({
|
|
340
|
+
target,
|
|
341
|
+
action,
|
|
342
|
+
dshxVersion: compatibility.dshxVersion,
|
|
343
|
+
creatorBridgeVersion: compatibility.creatorBridgeVersion,
|
|
344
|
+
dshxContract: compatibility.contractId,
|
|
345
|
+
layout: 'profile-include',
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
if (currentExists && legacyDirs.length > 0) {
|
|
349
|
+
throw new Error(`both ${target} and a legacy .agent-presets Creator Mode+ exist; refusing to choose or overwrite either preset`)
|
|
350
|
+
}
|
|
351
|
+
if (currentExists) {
|
|
352
|
+
if (!upgrade && !migrateLegacy) {
|
|
353
|
+
throw new Error(`Creator Mode+ already exists at ${target}; pass --upgrade to refresh only managed assets`)
|
|
354
|
+
}
|
|
355
|
+
refreshManagedAssets(target, dirname(target), false)
|
|
356
|
+
ensureProfileInclude(dirname(target))
|
|
357
|
+
return result('updated')
|
|
358
|
+
}
|
|
359
|
+
if (legacyDirs.length > 0) {
|
|
360
|
+
if (!migrateLegacy) {
|
|
361
|
+
throw new Error(`legacy Creator Mode+ exists at ${legacyDirs[0]}; pass --migrate-legacy after adding the standalone package`)
|
|
362
|
+
}
|
|
363
|
+
if (legacyDirs.length > 1) {
|
|
364
|
+
throw new Error(`both ${legacyDirs[0]} and ${legacyDirs[1]} exist; refusing to choose or overwrite either preset`)
|
|
365
|
+
}
|
|
366
|
+
const compositionPath = join(legacyDirs[0], 'agent.cordis.yml')
|
|
367
|
+
let composition = readFileSync(compositionPath, 'utf8')
|
|
368
|
+
if (countManagedRow(composition, CURRENT_ROW) !== 1) {
|
|
369
|
+
composition = composition.replace(recognizedLegacyRow(composition), CURRENT_ROW)
|
|
370
|
+
}
|
|
371
|
+
composition = migrateManagedSafetyCopy(composition)
|
|
372
|
+
const indented = composition.replace(/[ \t]+$/gm, '').replace(/\n$/, '').split('\n')
|
|
373
|
+
.map(line => (line.length === 0 ? '' : ` ${line}`))
|
|
374
|
+
.join('\n')
|
|
375
|
+
const description = '在 DSH 对话中创建、修改并加载插件,继续验证实际功能;支持保留当前会话的热更新和故障恢复。'
|
|
376
|
+
writeProfilePreset(target, `# Creator Mode+ preset migrated from .agent-presets. The legacy directory is preserved.
|
|
377
|
+
- id: preset-creator-mode-plus
|
|
378
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
379
|
+
config:
|
|
380
|
+
id: creator-mode-plus
|
|
381
|
+
name: Creator Mode+
|
|
382
|
+
description: ${JSON.stringify(description)}
|
|
383
|
+
order: 20
|
|
384
|
+
plugins:
|
|
385
|
+
${indented}
|
|
386
|
+
`)
|
|
387
|
+
return result('migrated')
|
|
388
|
+
}
|
|
389
|
+
if (upgrade || migrateLegacy) {
|
|
390
|
+
throw new Error('no existing Creator Mode+ preset found to update or migrate')
|
|
391
|
+
}
|
|
392
|
+
writeProfilePreset(target, deriveCreatorDeclaration(readFileSync(sourcePath, 'utf8')))
|
|
393
|
+
return result('installed')
|
|
394
|
+
}
|
|
395
|
+
|
|
188
396
|
/** Install or safely refresh the user-owned Creator Mode+ preset. */
|
|
189
397
|
export function installCreatorModePlus(options = {}) {
|
|
190
398
|
const harnessRoot = resolveHarnessRoot({
|
|
@@ -195,8 +403,18 @@ export function installCreatorModePlus(options = {}) {
|
|
|
195
403
|
moduleDir: options.moduleDir,
|
|
196
404
|
})
|
|
197
405
|
const compatibility = inspectDshxCompatibility(harnessRoot)
|
|
198
|
-
const
|
|
406
|
+
const sourceInfo = standardSourceAt(harnessRoot)
|
|
199
407
|
const dshHome = resolve(options.dshHome || process.env.DSH_HOME || join(homedir(), '.dsh'))
|
|
408
|
+
if (sourceInfo.kind === 'patch') {
|
|
409
|
+
return installFromStandardPatch({
|
|
410
|
+
sourcePath: sourceInfo.path,
|
|
411
|
+
dshHome,
|
|
412
|
+
compatibility,
|
|
413
|
+
upgrade: options.upgrade,
|
|
414
|
+
migrateLegacy: options.migrateLegacy,
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
const source = sourceInfo.path
|
|
200
418
|
const root = join(dshHome, '.agent-presets')
|
|
201
419
|
const currentTarget = join(root, CURRENT_PRESET_ID)
|
|
202
420
|
const legacyTarget = join(root, LEGACY_PRESET_ID)
|
package/scripts/verify-dshx.mjs
CHANGED
|
@@ -67,7 +67,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1]
|
|
|
67
67
|
process.stdout.write('Usage: node scripts/verify-dshx.mjs [--harness /absolute/path/to/deepseek-harness]\n')
|
|
68
68
|
} else {
|
|
69
69
|
const result = verifyDshxAlignment(options)
|
|
70
|
-
process.stdout.write('
|
|
70
|
+
process.stdout.write('CREATOR_MODE_PLUS_DSHX_V091_COMPATIBILITY_PASS\n')
|
|
71
71
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
72
72
|
}
|
|
73
73
|
} catch (error) {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { tmpdir } from 'node:os'
|
|
10
10
|
import { join, resolve } from 'node:path'
|
|
11
11
|
import { pathToFileURL } from 'node:url'
|
|
12
|
-
import { installCreatorModePlus,
|
|
12
|
+
import { installCreatorModePlus, standardSourceAt } from './install.mjs'
|
|
13
13
|
|
|
14
14
|
function digest(path) {
|
|
15
15
|
return createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
@@ -36,12 +36,11 @@ function parseArguments(argv) {
|
|
|
36
36
|
|
|
37
37
|
/** Exercise the real shipped Standard preset in an isolated disposable DSH_HOME. */
|
|
38
38
|
export function verifyHarnessInstall(harnessRoot) {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const sourcePreset = join(standardPreset, 'preset.yml')
|
|
39
|
+
const standard = standardSourceAt(harnessRoot)
|
|
40
|
+
const sourcePreset = standard.kind === 'directory' ? join(standard.path, 'preset.yml') : undefined
|
|
42
41
|
const before = {
|
|
43
|
-
composition: digest(
|
|
44
|
-
preset: digest(sourcePreset),
|
|
42
|
+
composition: digest(standard.path),
|
|
43
|
+
...(sourcePreset ? { preset: digest(sourcePreset) } : {}),
|
|
45
44
|
}
|
|
46
45
|
const dshHome = mkdtempSync(join(tmpdir(), 'creator-mode-plus-rc2-install-'))
|
|
47
46
|
try {
|
|
@@ -54,7 +53,7 @@ export function verifyHarnessInstall(harnessRoot) {
|
|
|
54
53
|
if (!composition.includes('name: dsh-creator-mode-plus')) throw new Error('isolated preset is missing the standalone bridge row')
|
|
55
54
|
if (!skill.includes('dshx_status') || !skill.includes('dshx_hot_reload')) throw new Error('isolated preset is missing the Creator delivery workflow')
|
|
56
55
|
const maintenance = readFileSync(join(installed.target, 'skills/creator-mode-plus/maintenance.md'), 'utf8')
|
|
57
|
-
if (!skill.includes('(maintenance.md#harness-and-launcher-maintenance)') || !maintenance.includes('update plan
|
|
56
|
+
if (!skill.includes('(maintenance.md#harness-and-launcher-maintenance)') || !maintenance.includes('Only `update plan` remains available')) throw new Error('isolated preset is missing the referenced Harness maintenance branch')
|
|
58
57
|
if (!existsSync(join(installed.target, 'skills/creator-mode-plus/verification-recovery.md'))) throw new Error('isolated preset is missing the referenced verification branch')
|
|
59
58
|
|
|
60
59
|
const firstStamp = statSync(compositionPath)
|
|
@@ -64,7 +63,10 @@ export function verifyHarnessInstall(harnessRoot) {
|
|
|
64
63
|
throw new Error('managed asset refresh changed the unchanged agent.cordis.yml stamp')
|
|
65
64
|
}
|
|
66
65
|
if (!existsSync(skillPath)) throw new Error('managed upgrade removed the Creator Mode+ skill')
|
|
67
|
-
if (digest(
|
|
66
|
+
if (digest(standard.path) !== before.composition) {
|
|
67
|
+
throw new Error('isolated install changed a shipped Harness preset')
|
|
68
|
+
}
|
|
69
|
+
if (sourcePreset && digest(sourcePreset) !== before.preset) {
|
|
68
70
|
throw new Error('isolated install changed a shipped Harness preset')
|
|
69
71
|
}
|
|
70
72
|
|
package/src/client.js
CHANGED
|
@@ -49,7 +49,7 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
try {
|
|
50
50
|
const response = await fetch(endpoint, {
|
|
51
51
|
method: 'POST',
|
|
52
|
-
headers: { 'content-type': 'application/json' },
|
|
52
|
+
headers: { 'content-type': 'application/json', 'x-dsh-creator-client': '1' },
|
|
53
53
|
body: JSON.stringify({ failedIds, message: String(message || '').slice(0, 8000) }),
|
|
54
54
|
})
|
|
55
55
|
const result = await response.json()
|
package/src/compatibility.js
CHANGED
|
@@ -5,11 +5,12 @@ export const CREATOR_BRIDGE_VERSION = 2
|
|
|
5
5
|
|
|
6
6
|
export const DSHX_CONTRACT = Object.freeze({
|
|
7
7
|
id: 'dshx-v0.7/creator-bridge-v2',
|
|
8
|
-
release: 'v0.
|
|
9
|
-
minimum: '0.
|
|
10
|
-
maximumExclusive: '0.
|
|
8
|
+
release: 'v0.9.1',
|
|
9
|
+
minimum: '0.9.1',
|
|
10
|
+
maximumExclusive: '0.10.0',
|
|
11
11
|
capabilities: Object.freeze([
|
|
12
12
|
'creator-session-claims',
|
|
13
|
+
'plugin-only-core-immutability',
|
|
13
14
|
'human-confirmed-creator-takeover',
|
|
14
15
|
'workspace-scaffold',
|
|
15
16
|
'bounded-new-client-activation',
|
|
@@ -24,7 +25,7 @@ export const DSHX_CONTRACT = Object.freeze({
|
|
|
24
25
|
'bounded-same-pid-server-hot-reload',
|
|
25
26
|
'single-home-web-host',
|
|
26
27
|
'isolated-verify-home',
|
|
27
|
-
'
|
|
28
|
+
'read-only-harness-update-plan',
|
|
28
29
|
]),
|
|
29
30
|
})
|
|
30
31
|
|
|
@@ -47,6 +48,7 @@ export const CREATOR_MODEL_TOOLS = Object.freeze([
|
|
|
47
48
|
])
|
|
48
49
|
|
|
49
50
|
export const DSHX_SURFACE_MARKERS = Object.freeze({
|
|
51
|
+
'src/core-boundary.js': Object.freeze(['CORE_SOURCE_IMMUTABLE', 'assertPluginSource', 'creatorCoreMutationReason']),
|
|
50
52
|
'src/internal/creator-claims.mjs': Object.freeze(['beginTakeover', 'commitTakeover', 'CREATOR_TAKEOVER_GRANT_REQUIRED', 'revocations']),
|
|
51
53
|
'src/creator-plus/takeover.js': Object.freeze(['userQuestions', 'ensureTakeoverFence', 'CREATOR_OWNER_NOT_QUIESCENT']),
|
|
52
54
|
'src/internal/browser-access.ts': Object.freeze(['bindBrowserAccess', 'assertSameBrowserHost', 'BROWSER_ADAPTER_REQUIRED', 'configureSessionBrowserAdapter', 'sessionBrowserAdapter']),
|
|
@@ -87,9 +89,19 @@ export const DSHX_SURFACE_MARKERS = Object.freeze({
|
|
|
87
89
|
'PROFILE_DEPENDENCY_REMOVED',
|
|
88
90
|
]),
|
|
89
91
|
'src/commands/update.ts': Object.freeze([
|
|
90
|
-
'dshx update plan
|
|
91
|
-
"action
|
|
92
|
+
'dshx update plan',
|
|
93
|
+
"action !== 'plan'",
|
|
94
|
+
'CORE_SOURCE_IMMUTABLE',
|
|
92
95
|
]),
|
|
96
|
+
'src/internal/types.ts': Object.freeze([
|
|
97
|
+
'DESK_HARNESS_TAG',
|
|
98
|
+
'dsh-v0.2.0-rc.2',
|
|
99
|
+
'DESK_HARNESS_SHA',
|
|
100
|
+
'639ed015397290b3745d163aafe02ffee4aa3f84',
|
|
101
|
+
'DSH_PEER_RANGE',
|
|
102
|
+
'>=0.2.0-rc.1 <0.2.1',
|
|
103
|
+
]),
|
|
104
|
+
'src/creator-plus/runner.js': Object.freeze(["ctx.on('agent/created'"]),
|
|
93
105
|
'src/internal/creator.ts': Object.freeze(['bridgeVersion !== 2', 'workspaceRoot']),
|
|
94
106
|
'src/internal/guardian.ts': Object.freeze([
|
|
95
107
|
'export function armGuardian',
|
|
@@ -174,7 +186,7 @@ export const DSHX_SURFACE_MARKERS = Object.freeze({
|
|
|
174
186
|
'activate-new-client',
|
|
175
187
|
]),
|
|
176
188
|
'knowledge/contracts/creator-guardian.md': Object.freeze(['Creator+', 'crash-loop fuse', 'quarantine', 'plugin-integrity-failed']),
|
|
177
|
-
'knowledge/contracts/harness-update.md': Object.freeze(['
|
|
189
|
+
'knowledge/contracts/harness-update.md': Object.freeze(['dshx update plan', 'CORE_SOURCE_IMMUTABLE']),
|
|
178
190
|
'knowledge/contracts/live-activation.md': Object.freeze(['SOURCE_BUILT', 'CLIENT_MANIFEST_PRESENT']),
|
|
179
191
|
'knowledge/contracts/compat-0.1.5.md': Object.freeze(['MarkdownText', 'createDrafts', 'assistant/attempt.stream']),
|
|
180
192
|
'src/internal/compat-015.ts': Object.freeze(['compat-015-message-text', 'compat-015-ctx-agent']),
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/** Plugin-only boundary. Reading public Host APIs is allowed; writing official
|
|
2
|
+
* source, copies, worktrees, installed packages or generated artifacts is not. */
|
|
3
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
|
|
4
|
+
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from 'node:path'
|
|
5
|
+
|
|
6
|
+
export const CORE_SOURCE_IMMUTABLE = 'CORE_SOURCE_IMMUTABLE'
|
|
7
|
+
export const PLUGIN_ONLY_RULE = 'DSHX and Creator Mode+ develop external plugins only. Never edit, patch, replace or rebuild DeepSeek Harness official source, shipped presets, installed official packages or their generated artifacts, including copies and worktrees. A missing public API is a plugin capability gap: use another public extension point or report the gap. A plugin task, handoff document, automatic approval, takeover or --force never authorizes a Host patch. Keep all build output inside the plugin. Watched user cordis.patch.yml configuration is not a source-code patch.'
|
|
8
|
+
const deny = detail => `${CORE_SOURCE_IMMUTABLE}: ${detail}. Use a public plugin extension point; do not patch the Host or retry with force/approval.`
|
|
9
|
+
const under = (parent, child) => { const r = relative(parent, child); return r === '' || (r !== '..' && !r.startsWith(`..${sep}`) && !isAbsolute(r)) }
|
|
10
|
+
function manifest(dir) { try { return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) } catch { return undefined } }
|
|
11
|
+
|
|
12
|
+
/** Resolve existing components before ..; also protects a not-yet-created file. */
|
|
13
|
+
export function canonicalTarget(path, cwd = process.cwd()) {
|
|
14
|
+
const absolute = isAbsolute(path) ? path : `${cwd}${sep}${path}`
|
|
15
|
+
let current = parse(absolute).root
|
|
16
|
+
for (const part of absolute.slice(current.length).split(sep)) {
|
|
17
|
+
if (!part || part === '.') continue
|
|
18
|
+
if (part === '..') { current = dirname(current); continue }
|
|
19
|
+
current = join(current, part)
|
|
20
|
+
try { current = realpathSync.native(current) }
|
|
21
|
+
catch (error) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error }
|
|
22
|
+
}
|
|
23
|
+
return current
|
|
24
|
+
}
|
|
25
|
+
function isHarness(dir) {
|
|
26
|
+
return manifest(dir)?.name === '@deepseek-ai/dsh-root'
|
|
27
|
+
|| (existsSync(join(dir, 'apps/cli/src/bin.ts')) && existsSync(join(dir, 'packages')) && existsSync(join(dir, 'vendor')))
|
|
28
|
+
}
|
|
29
|
+
function inHarnessSource(path, root) {
|
|
30
|
+
if (!under(root, path)) return false
|
|
31
|
+
const rel = relative(root, path).split(sep)
|
|
32
|
+
// Official tree surfaces are protected; sibling, independent external
|
|
33
|
+
// plugin directories remain supported. Real targets are resolved first.
|
|
34
|
+
if (rel.length === 1 && (rel[0] === '' || !existsSync(path) || !lstatSync(path).isDirectory())) return true
|
|
35
|
+
return ['apps', 'packages', 'vendor', 'scripts', 'config', 'configs', 'src',
|
|
36
|
+
'docs', 'assets', 'patches', 'benchmarks', 'native', 'python', 'snapshots', 'website', '.git', '.agents', '.github'].includes(rel[0])
|
|
37
|
+
}
|
|
38
|
+
export function coreWriteReason(path, harnessRoot, cwd) {
|
|
39
|
+
const target = canonicalTarget(path, cwd)
|
|
40
|
+
if (harnessRoot && inHarnessSource(target, canonicalTarget(harnessRoot))) return deny(`protected Harness path ${target}`)
|
|
41
|
+
let cursor = target
|
|
42
|
+
while (true) {
|
|
43
|
+
const pkg = manifest(cursor)
|
|
44
|
+
if (typeof pkg?.name === 'string' && pkg.name.startsWith('@deepseek-ai/') && pkg.name !== '@deepseek-ai/dsh-root') return deny(`official package ${pkg.name}: ${target}`)
|
|
45
|
+
if (isHarness(cursor) && inHarnessSource(target, cursor)) return deny(`official checkout or worktree ${cursor}: ${target}`)
|
|
46
|
+
const parent = dirname(cursor)
|
|
47
|
+
if (parent === cursor) return undefined
|
|
48
|
+
cursor = parent
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function assertPluginSource(harnessRoot, dir, entry) {
|
|
52
|
+
const reason = coreWriteReason(dir, harnessRoot)
|
|
53
|
+
if (reason) throw new Error(reason)
|
|
54
|
+
if (entry) {
|
|
55
|
+
const target = canonicalTarget(entry)
|
|
56
|
+
const denied = coreWriteReason(target, harnessRoot)
|
|
57
|
+
if (denied) throw new Error(denied)
|
|
58
|
+
if (!under(canonicalTarget(dir), target)) throw new Error(deny(`plugin entry escapes its source directory: ${entry}`))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const PATCH_TARGET = /^(?:\+\+\+|---)\s+(?:[ab]\/)?(?:packages|apps|vendor|scripts|config)\/|^\*\*\* (?:Update|Add|Delete) File:\s*(?:[ab]\/)?(?:packages|apps|vendor|scripts|config)\//m
|
|
62
|
+
const SKIP = new Set(['node_modules', 'lib', 'dist', 'out', 'coverage', 'tests', 'test', 'fixtures'])
|
|
63
|
+
/** Release gate for concrete Host patches and compiler writes; not a proof that
|
|
64
|
+
* arbitrary third-party programs are safe. Never executes plugin scripts. */
|
|
65
|
+
export function auditPluginSource(harnessRoot, dir) {
|
|
66
|
+
const findings = []
|
|
67
|
+
assertPluginSource(harnessRoot, dir)
|
|
68
|
+
let count = 0
|
|
69
|
+
function walk(folder) {
|
|
70
|
+
for (const entry of readdirSync(folder, { withFileTypes: true })) {
|
|
71
|
+
if (entry.name.startsWith('.') || SKIP.has(entry.name)) continue
|
|
72
|
+
if (++count > 10000) throw new Error(deny('plugin boundary inventory exceeds 10000 files'))
|
|
73
|
+
const path = join(folder, entry.name)
|
|
74
|
+
if (entry.isSymbolicLink()) {
|
|
75
|
+
const reason = coreWriteReason(path, harnessRoot)
|
|
76
|
+
if (reason) findings.push({ path, message: reason })
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
if (entry.isDirectory()) { walk(path); continue }
|
|
80
|
+
if (!entry.isFile() || lstatSync(path).size > 1024 * 1024) continue
|
|
81
|
+
if (/\.(patch|diff)$/.test(entry.name) && PATCH_TARGET.test(readFileSync(path, 'utf8'))) {
|
|
82
|
+
findings.push({ path, message: deny('plugin contains a patch targeting official Host files') })
|
|
83
|
+
}
|
|
84
|
+
if (/^tsconfig.*\.json$/.test(entry.name)) {
|
|
85
|
+
// Conservative JSONC extraction: relevant path strings only, no code.
|
|
86
|
+
const source = readFileSync(path, 'utf8')
|
|
87
|
+
for (const match of source.matchAll(/"(?:outDir|outFile|declarationDir|tsBuildInfoFile)"\s*:\s*"([^"\n]+)"/g)) {
|
|
88
|
+
const output = canonicalTarget(match[1], folder)
|
|
89
|
+
if (!under(canonicalTarget(dir), output)) findings.push({ path, message: deny(`TypeScript output escapes the plugin: ${match[1]}`) })
|
|
90
|
+
}
|
|
91
|
+
const refs = source.match(/"references"\s*:\s*\[([\s\S]*?)\]/)?.[1] ?? ''
|
|
92
|
+
for (const match of refs.matchAll(/"path"\s*:\s*"([^"\n]+)"/g)) {
|
|
93
|
+
const reason = coreWriteReason(match[1], harnessRoot, folder)
|
|
94
|
+
if (reason) findings.push({ path, message: deny(`TypeScript project reference can rebuild official source: ${match[1]}`) })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
walk(dir)
|
|
100
|
+
return findings
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readOnlyShell(command) {
|
|
104
|
+
if (/[;&|`$><\n]/.test(command)) return false
|
|
105
|
+
if (/^\s*(?:cat|head|tail|ls|pwd|stat|realpath|readlink|rg)\b/.test(command)) return !/--pre\b/.test(command)
|
|
106
|
+
return /^\s*git\s+(?:(?:-C\s+(?:"[^"]+"|'[^']+'|\S+)\s+))?(?:status|diff|show|log|ls-files|rev-parse)\b/.test(command)
|
|
107
|
+
&& !/--(?:ext-diff|textconv|output|exec-path)\b/.test(command)
|
|
108
|
+
}
|
|
109
|
+
export function creatorCoreMutationReason(exec, harnessRoot, policy) {
|
|
110
|
+
const args = exec?.arguments ?? {}, cwd = exec?.agent?.session?.header?.cwd ?? process.cwd()
|
|
111
|
+
if (['write', 'edit', 'write_file', 'edit_file', 'delete_file', 'move_file', 'copy_file'].includes(exec?.name)) {
|
|
112
|
+
for (const key of ['file_path', 'path', 'destination', 'source', 'target']) {
|
|
113
|
+
if (typeof args[key] === 'string') { const reason = coreWriteReason(args[key], harnessRoot, cwd); if (reason) return reason }
|
|
114
|
+
}
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
if (exec?.name === 'apply_patch') {
|
|
118
|
+
const patch = args.patch ?? args.input ?? ''
|
|
119
|
+
for (const match of patch.matchAll(/^\*\*\* (?:(?:Update|Add|Delete) File|Move to):\s*(.+)$/gm)) {
|
|
120
|
+
const reason = coreWriteReason(match[1], harnessRoot, cwd); if (reason) return reason
|
|
121
|
+
}
|
|
122
|
+
return undefined
|
|
123
|
+
}
|
|
124
|
+
if (!['bash', 'terminal_open', 'terminal_send'].includes(exec?.name)) return undefined
|
|
125
|
+
const command = args.command ?? args.text ?? ''
|
|
126
|
+
if (readOnlyShell(command)) return undefined
|
|
127
|
+
if (args.sandbox_permissions === 'danger-full-access') return deny('unconfined shell execution cannot protect official source')
|
|
128
|
+
if (!policy || !['read-only', 'workspace-write', 'danger-full-access'].includes(policy.mode)) {
|
|
129
|
+
return `CREATOR_SANDBOX_UNAVAILABLE: cannot verify a confined shell policy${policy?.unavailableReason ? ` (${policy.unavailableReason})` : ''}. The command was not executed. Repair Creator Mode+ sandbox service wiring; this is not evidence of an official-source write.`
|
|
130
|
+
}
|
|
131
|
+
if (exec.name === 'bash' && policy.shellConfined === false) return deny('the active shell executor does not enforce the sandbox policy')
|
|
132
|
+
if (policy?.mode === 'danger-full-access') return deny('switch Creator shell to workspace-write so the Host sandbox confines build scripts')
|
|
133
|
+
if (policy?.mode === 'workspace-write' && harnessRoot && under(canonicalTarget(policy.workspaceRoot), canonicalTarget(harnessRoot))) return deny('the shell workspace contains the official Harness; use a plugin-only workspace')
|
|
134
|
+
const workdir = canonicalTarget(args.workdir ?? args.cwd ?? cwd, cwd)
|
|
135
|
+
const atCore = coreWriteReason(workdir, harnessRoot)
|
|
136
|
+
if (atCore) return atCore
|
|
137
|
+
if (/\b(?:install-host-seam|patch-host|host-patches)(?:[/.\s]|$)/.test(command)) return deny('Host patch installation is outside plugin development')
|
|
138
|
+
for (const match of command.matchAll(/\/(?:[^\s"'`()[\]{};,<>|]+)/g)) {
|
|
139
|
+
const reason = coreWriteReason(match[0], harnessRoot, workdir)
|
|
140
|
+
if (reason) return reason
|
|
141
|
+
}
|
|
142
|
+
// This catches direct shell/REPL targets and git -C. The Host sandbox remains
|
|
143
|
+
// responsible for code that assembles a path dynamically.
|
|
144
|
+
for (const match of command.matchAll(/"([^"\n]+)"|'([^'\n]+)'|([^\s;|&()<>]+)/g)) {
|
|
145
|
+
const token = match[1] ?? match[2] ?? match[3]
|
|
146
|
+
if (!token.includes('/') || token.includes('://')) continue
|
|
147
|
+
const reason = coreWriteReason(token.replace(/^[\w]+=/, ''), harnessRoot, workdir)
|
|
148
|
+
if (reason) return reason
|
|
149
|
+
}
|
|
150
|
+
return undefined
|
|
151
|
+
}
|
package/src/delivery.js
CHANGED
|
@@ -90,7 +90,7 @@ function validatedHotReload(result, pluginId, expectedPid, expectedPort) {
|
|
|
90
90
|
}
|
|
91
91
|
if (report?.command !== 'hot-reload' || report?.ok !== true
|
|
92
92
|
|| !evidence.includes('HOST_MODULE_RELOADED') || data?.hostRestart !== false || data?.behaviorVerified !== false
|
|
93
|
-
|| value?.pluginId !== pluginId || value?.profile
|
|
93
|
+
|| value?.pluginId !== pluginId || !['web', 'desktop'].includes(value?.profile)
|
|
94
94
|
|| value?.hostPid !== expectedPid || value?.hostPort !== expectedPort || value?.hostRestart !== false
|
|
95
95
|
|| typeof value?.transactionId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value.transactionId)
|
|
96
96
|
|| typeof value?.hmrEntryId !== 'string' || !/^dshx-hot-reload-hmr-[a-z0-9-]+$/.test(value.hmrEntryId)
|