dsh-creator-mode-plus 0.3.8 → 0.3.12

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/src/runner.js CHANGED
@@ -63,7 +63,7 @@ function isAllowedArgs(args) {
63
63
  }
64
64
  if (args.length === 6 && args[0] === 'activate-new-client' && PLUGIN_ID.test(args[1])) {
65
65
  return args[2] === '--profile'
66
- && args[3] === 'web'
66
+ && ['web', 'desktop'].includes(args[3])
67
67
  && args[4] === '--port'
68
68
  && /^\d{1,5}$/.test(args[5])
69
69
  && Number(args[5]) >= 1
@@ -71,7 +71,7 @@ function isAllowedArgs(args) {
71
71
  }
72
72
  if (args.length === 7 && args[0] === 'hot-reload' && PLUGIN_ID.test(args[1])) {
73
73
  return args[2] === '--profile'
74
- && args[3] === 'web'
74
+ && ['web', 'desktop'].includes(args[3])
75
75
  && args[4] === '--port'
76
76
  && /^\d{1,5}$/.test(args[5])
77
77
  && Number(args[5]) >= 1
@@ -97,7 +97,7 @@ export function currentWebPort(argv = process.argv) {
97
97
  return value
98
98
  }
99
99
 
100
- function contextFromExecution(exec, hostPort) {
100
+ function contextFromExecution(exec, hostPort, options = {}) {
101
101
  const sessionId = exec?.agent?.id
102
102
  if (typeof sessionId !== 'string' || sessionId.length === 0) {
103
103
  throw new Error('dsh-creator-mode-plus: bridge v2 requires the calling DSH session identity')
@@ -120,6 +120,7 @@ function contextFromExecution(exec, hostPort) {
120
120
  hostPid: process.pid,
121
121
  hostParentPid: process.ppid,
122
122
  hostPort,
123
+ ...(options.hostProfile === 'desktop' ? { hostProfile: 'desktop', hostRoot: options.hostRoot, hostHome: options.hostHome } : {}),
123
124
  bridgeVersion: CREATOR_BRIDGE_VERSION,
124
125
  ...workspaceRoot === undefined ? {} : { workspaceRoot: resolve(workspaceRoot) },
125
126
  }
@@ -232,14 +233,32 @@ function executeDshx(args, exec, options = {}) {
232
233
  throw new Error('dsh-creator-mode-plus: refusing an operation outside bridge v2')
233
234
  }
234
235
  const runtime = resolveDshxRuntime(options)
236
+ if (options.hostProfile === 'desktop') args = args.map((value, index) => index > 0 && args[index - 1] === '--profile' ? 'desktop' : value)
237
+ const signal = exec?.signal
238
+ const result = (code, stdout = '', stderr = '', startup = '') => ({
239
+ command: `dshx ${args.join(' ')}`,
240
+ exitCode: code ?? 1,
241
+ stdout: redactStartupOutput(stdout.trim(), startup),
242
+ stderr: redactStartupOutput(stderr.trim(), startup),
243
+ dshxVersion: runtime.dshxVersion,
244
+ creatorBridgeVersion: runtime.bridgeVersion,
245
+ dshxContract: runtime.contractId,
246
+ dshxCapabilities: runtime.capabilities,
247
+ ...((args[0] === 'activate-new-client' || args[0] === 'hot-reload')
248
+ ? { hostPid: process.pid, hostPort: Number(args[5]) }
249
+ : {}),
250
+ })
251
+ const aborted = () => result(1, '', 'dshx operation aborted before spawn')
252
+ // An already-cancelled invocation must never start a child, even briefly.
253
+ if (signal?.aborted) return Promise.resolve(aborted())
235
254
  const argv = ['--import', runtime.loader, runtime.cli, ...args, ...args[0] === 'activation-plan' ? ['--json'] : [], '--harness', runtime.root]
236
255
  const spawnProcess = options.spawnProcess ?? spawn
237
256
  const hostPort = options.hostPort ?? currentWebPort()
238
- const creatorContext = contextFromExecution(exec, hostPort)
257
+ const creatorContext = contextFromExecution(exec, hostPort, options)
239
258
  const startup = privateStartupUrl(options.getWebStartupUrl, hostPort)
240
- const signal = exec?.signal
259
+ const profileAccess = options.createProfileAccess?.(args, exec, runtime.root)
241
260
  return new Promise((resolveResult, reject) => {
242
- const child = spawnProcess(process.execPath, argv, {
261
+ const spawnOptions = {
243
262
  cwd: runtime.root,
244
263
  env: {
245
264
  ...process.env,
@@ -247,9 +266,15 @@ function executeDshx(args, exec, options = {}) {
247
266
  DSHX_CREATOR_CONTEXT: JSON.stringify(creatorContext),
248
267
  DSHX_CREATOR_TAKEOVER_GRANT: args[0] === 'creator' && args[1] === 'takeover' && options.takeoverGrant ? JSON.stringify(options.takeoverGrant) : '',
249
268
  DSHX_WEB_STARTUP_URL: startup,
269
+ ...profileAccess?.env,
270
+ DSHX_DESKTOP_PLUGIN_ID: args[0] === 'creator' ? args[2] ?? '' : args[1] ?? '',
250
271
  },
251
272
  stdio: ['ignore', 'pipe', 'pipe'],
252
- })
273
+ }
274
+ // Trusted option callbacks above can synchronously cancel the invocation.
275
+ if (signal?.aborted) { profileAccess?.dispose(); resolveResult(aborted()); return }
276
+ let child
277
+ try { child = spawnProcess(process.execPath, argv, spawnOptions) } catch (error) { profileAccess?.dispose(); reject(error); return }
253
278
  let stdout = ''
254
279
  let stderr = ''
255
280
  child.stdout.on('data', chunk => { stdout = appendBounded(stdout, chunk) })
@@ -258,22 +283,11 @@ function executeDshx(args, exec, options = {}) {
258
283
  const abort = () => { child.kill('SIGTERM') }
259
284
  if (signal?.aborted) abort()
260
285
  else signal?.addEventListener('abort', abort, { once: true })
261
- child.once('error', reject)
286
+ child.once('error', error => { profileAccess?.dispose(); reject(error) })
262
287
  child.once('close', (code) => {
288
+ profileAccess?.dispose();
263
289
  signal?.removeEventListener('abort', abort)
264
- resolveResult({
265
- command: `dshx ${args.join(' ')}`,
266
- exitCode: code ?? 1,
267
- stdout: redactStartupOutput(stdout.trim(), startup),
268
- stderr: redactStartupOutput(stderr.trim(), startup),
269
- dshxVersion: runtime.dshxVersion,
270
- creatorBridgeVersion: runtime.bridgeVersion,
271
- dshxContract: runtime.contractId,
272
- dshxCapabilities: runtime.capabilities,
273
- ...((args[0] === 'activate-new-client' || args[0] === 'hot-reload')
274
- ? { hostPid: process.pid, hostPort: Number(args[5]) }
275
- : {}),
276
- })
290
+ resolveResult(result(code, stdout, stderr, startup))
277
291
  })
278
292
  })
279
293
  }
@@ -292,7 +306,7 @@ export function runDshx(args, exec, options = {}) {
292
306
  const port = options.hostPort ?? currentWebPort()
293
307
  const receipt = readDelivery(root, exec.agent.id)
294
308
  const delivery = deliveryStatus(receipt, { port })
295
- if (args[0] === 'status' && delivery?.state === 'RUNTIME_VERIFICATION_REQUIRED') {
309
+ if (!exec?.signal?.aborted && args[0] === 'status' && delivery?.state === 'RUNTIME_VERIFICATION_REQUIRED') {
296
310
  try { delivery.runtimeProof = await verifyDeliveryClient(receipt, port, options.getWebStartupUrl?.(port)) }
297
311
  catch { delivery.runtimeProof = { state: 'RUNTIME_PROOF_FAILED', behavior: 'UNVERIFIED' } }
298
312
  }
@@ -401,7 +415,7 @@ function pluginMessage(text) {
401
415
  id: randomUUID(),
402
416
  role: 'user',
403
417
  content: Object.freeze([{ type: 'text', text }]),
404
- source: Object.freeze({ kind: 'plugin', plugin: 'dsh-creator-mode-plus' }),
418
+ source: Object.freeze({ kind: 'plugin:dsh-creator-mode-plus' }),
405
419
  })
406
420
  }
407
421
 
@@ -444,10 +458,14 @@ export async function releaseCreatorClaim(agent, options = {}) {
444
458
  forgetCreatorClaim({ agent })
445
459
  }
446
460
 
447
- /** Register fail-contained recovery delivery on the preset-scoped Agent lifecycle. */
461
+ /**
462
+ * Register fail-contained recovery delivery on the preset-scoped Agent lifecycle.
463
+ * `agent/created` replaced `agent/session-start`. The listener stays synchronous:
464
+ * a recovery failure must not reject serial session creation.
465
+ */
448
466
  export function installCreatorRecovery(ctx, options = {}) {
449
467
  if (typeof ctx.on !== 'function') return
450
- ctx.on('agent/session-start', ({ agent }) => {
468
+ ctx.on('agent/created', ({ agent }) => {
451
469
  void deliverCreatorRecovery(agent, options).catch((error) => {
452
470
  const message = `dsh-creator-mode-plus: recovery delivery failed: ${error instanceof Error ? error.message : String(error)}`
453
471
  if (ctx.logger?.warn) ctx.logger.warn(message)
package/src/safety.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { CORE_SOURCE_IMMUTABLE, PLUGIN_ONLY_RULE, creatorCoreMutationReason } from './core-boundary.js'
1
2
  /** Creator Mode+ claim tracking and last-mile destructive-shell guard. */
2
3
 
3
4
  const claimedPlugins = new WeakMap()
@@ -72,8 +73,53 @@ export function creatorDestructiveCommandReason(exec, explicitPluginId) {
72
73
  return undefined
73
74
  }
74
75
 
75
- /** Install the preset-scoped monotonic guard when the Host exposes it. */
76
- export function installCreatorSafetyGuard(ctx) {
77
- if (typeof ctx?.tools?.guard !== 'function') return
78
- ctx.tools.guard(exec => creatorDestructiveCommandReason(exec))
76
+ /** Install the required preset-scoped monotonic guard through public Host APIs. */
77
+ export function installCreatorSafetyGuard(ctx, resolveHarness = () => process.env.DSHX_HARNESS) {
78
+ if (typeof ctx?.tools?.guard !== 'function') throw new Error(`${CORE_SOURCE_IMMUTABLE}: public tools.guard is required for Creator Mode+`)
79
+ // Keep the guard installed even while a dependency is unavailable. Only
80
+ // declared, live dependency scopes may read services; agent.ctx has no such
81
+ // declarations. Re-read each service per call so replacement and per-session
82
+ // policy changes are observed without retaining an old service generation.
83
+ function serviceReader(name) {
84
+ let binding
85
+ ctx.inject?.([name], scope => {
86
+ const current = { scope, active: true }
87
+ binding = current
88
+ scope.effect(() => () => {
89
+ current.active = false
90
+ if (binding === current) binding = undefined
91
+ })
92
+ })
93
+ return () => {
94
+ if (!binding?.active) throw new Error(`${name} service is unavailable`)
95
+ binding.scope.fiber.assertActive()
96
+ return binding.scope[name]
97
+ }
98
+ }
99
+ const sandboxPolicy = serviceReader('sandboxPolicy')
100
+ const shell = serviceReader('shell')
101
+ ctx.tools.guard(exec => {
102
+ const destructive = creatorDestructiveCommandReason(exec)
103
+ if (destructive) return destructive
104
+ if (!['write', 'edit', 'write_file', 'edit_file', 'delete_file', 'move_file', 'copy_file', 'apply_patch', 'bash', 'terminal_open', 'terminal_send'].includes(exec?.name)) return undefined
105
+ try {
106
+ const root = resolveHarness()
107
+ if (!root) return `${CORE_SOURCE_IMMUTABLE}: cannot identify the Harness before a write operation`
108
+ let policy
109
+ if (['bash', 'terminal_open', 'terminal_send'].includes(exec.name)) {
110
+ try {
111
+ policy = sandboxPolicy().resolve(exec.agent ? { session: exec.agent.session } : {})
112
+ if (policy && exec.name === 'bash') policy = { ...policy, shellConfined: shell().sandboxMode !== undefined }
113
+ } catch (error) {
114
+ policy = { unavailableReason: error instanceof Error ? error.message : String(error) }
115
+ }
116
+ }
117
+ return creatorCoreMutationReason(exec, root, policy)
118
+ } catch (error) {
119
+ return `${CORE_SOURCE_IMMUTABLE}: cannot validate write boundary: ${error.message}`
120
+ }
121
+ })
122
+ ctx.inject?.(['systemPrompt'], scope => {
123
+ scope.systemPrompt.section({ name: 'creator:plugin-only', order: 1000, text: PLUGIN_ONLY_RULE })
124
+ })
79
125
  }
package/src/takeover.js CHANGED
@@ -76,7 +76,7 @@ function titleOf(session) {
76
76
  for (let i = events.length - 1; i >= 0; i--) if (events[i].type === 'session/title' && typeof events[i].data?.title === 'string') return events[i].data.title
77
77
  return '未命名对话'
78
78
  }
79
- function ownedJobs(jobs, agent) { return jobs?.list(agent).filter(job => job.ownerSession === agent.id && liveJob(job)) ?? [] }
79
+ function ownedJobs(jobs, agent) { return jobs?.list(agent.id).filter(job => job.owner === agent.id && liveJob(job)) ?? [] }
80
80
  function busy(ctx, agents, broker) {
81
81
  const jobs = get(ctx, 'jobs'), terminals = get(ctx, 'terminals')
82
82
  return agents.some(agent => agent.status !== 'idle' || broker.active.has(agent.id) || ownedJobs(jobs, agent).length || terminals?.hasOwned(agent))
@@ -100,8 +100,8 @@ async function drain(ctx, owner, broker, signal) {
100
100
  const waits = []
101
101
  for (const agent of group) {
102
102
  for (const job of ownedJobs(jobs, agent)) {
103
- jobs.kill(job.id, agent, 'Creator+ user-confirmed takeover')
104
- waits.push(jobs.wait(job.id, 20_000, agent, signal).then(result => { if (liveJob(result)) throw new Error('CREATOR_BACKGROUND_JOB_STILL_RUNNING') }))
103
+ jobs.kill(job.id, agent.id, 'Creator+ user-confirmed takeover')
104
+ waits.push(jobs.wait(job.id, 20_000, agent.id, signal).then(result => { if (liveJob(result)) throw new Error('CREATOR_BACKGROUND_JOB_STILL_RUNNING') }))
105
105
  }
106
106
  for (const terminal of terminals?.list(agent) ?? []) waits.push(terminals.kill(agent, terminal.id, 'Creator+ user-confirmed takeover'))
107
107
  waits.push(agent.whenIdle())