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.
@@ -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 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.`
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
- [LEGACY_SIX_TOOL_PERSONA, SEVEN_TOOL_PERSONA],
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 = exactRowCount(composition, CURRENT_ROW)
139
- const matchingLegacyRows = LEGACY_ROWS.filter(row => exactRowCount(composition, row) === 1)
140
- const legacyCount = LEGACY_ROWS.reduce((count, row) => count + exactRowCount(composition, row), 0)
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 source = standardPresetAt(harnessRoot)
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)
@@ -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('CREATOR_MODE_PLUS_DSHX_V078_COMPATIBILITY_PASS\n')
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, standardPresetAt } from './install.mjs'
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 standardPreset = standardPresetAt(harnessRoot)
40
- const sourceComposition = join(standardPreset, 'agent.cordis.yml')
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(sourceComposition),
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 → prepare → verify → apply')) throw new Error('isolated preset is missing the referenced Harness maintenance branch')
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(sourceComposition) !== before.composition || digest(sourcePreset) !== before.preset) {
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()
@@ -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.7.8',
9
- minimum: '0.7.8',
10
- maximumExclusive: '0.8.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
- 'transactional-harness-update-assistant',
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|prepare|verify|apply|rollback',
91
- "action === 'apply' || action === 'rollback'",
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(['plan → prepare → verify → apply', 'rollback']),
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 !== 'web'
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)