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/AGENTS.md +19 -3
- package/CHANGELOG.md +42 -1
- package/README.en.md +29 -5
- package/README.md +29 -5
- package/cordis.patch.yml +3 -0
- package/docs/bridge-contract.md +27 -39
- 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 +25 -10
- 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 +19 -2
- package/preset/skills/creator-mode-plus/maintenance.md +9 -12
- 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/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]
|
|
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]
|
|
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
|
|
259
|
+
const profileAccess = options.createProfileAccess?.(args, exec, runtime.root)
|
|
241
260
|
return new Promise((resolveResult, reject) => {
|
|
242
|
-
const
|
|
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
|
|
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
|
-
/**
|
|
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/
|
|
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
|
|
76
|
-
export function installCreatorSafetyGuard(ctx) {
|
|
77
|
-
if (typeof ctx?.tools?.guard !== 'function')
|
|
78
|
-
|
|
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.
|
|
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())
|