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
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local task LIFETIME, never a permission or a human-consent detector.
|
|
3
|
+
*
|
|
4
|
+
* Producer integration: declare public Cordis inject ['agents', 'sessions'];
|
|
5
|
+
* create this tracker in that producer's effect-owned context BEFORE a turn.
|
|
6
|
+
* Only the real Creator execute closure may pass its native Tools execution.
|
|
7
|
+
* The caller must separately prove that closure and obtain official human
|
|
8
|
+
* once/task/remember consent. No event, source.kind, taskId, sessionId, token
|
|
9
|
+
* copied into JSON, or successful capture() authorizes any operation.
|
|
10
|
+
*
|
|
11
|
+
* Supported scope: one observed AgentLoop turn, one freshly delivered direct
|
|
12
|
+
* user input, one exact Agent + Session + producer lifetime. The first public
|
|
13
|
+
* agent/turn-stopping ends it, even if a goal/Grok listener steers another step
|
|
14
|
+
* before turn/end. Automatic continuation is NOT presumed to be the same task.
|
|
15
|
+
* Any new inbox input or user/message AFTER capture revokes that handle; only
|
|
16
|
+
* another fresh direct input can anchor a new handle. Ordinary tool results,
|
|
17
|
+
* system prompt updates and uninterrupted tool steps do not renew or revoke it.
|
|
18
|
+
*
|
|
19
|
+
* A fresh input must be observed on Session's live committed inbox splice,
|
|
20
|
+
* claimed by the real AgentLoop, admitted in its pre-step, and committed as an
|
|
21
|
+
* append user/message. Constructor seed/fork/history and plugin inputs cannot
|
|
22
|
+
* supply an anchor. FirstLiveSeq is a freshness boundary, NOT consent proof.
|
|
23
|
+
* Starting/reloading this tracker mid-turn fails closed; no history adoption.
|
|
24
|
+
*
|
|
25
|
+
* Public seams used (current checkout): Session.firstLiveSeq/eventAt/seq/id;
|
|
26
|
+
* Agent.session/ctx/status; agents.get/sessions.get/list; session/created,
|
|
27
|
+
* session/event/disposed;
|
|
28
|
+
* agent/inbox/claimed/discarded, agent/pre-step, agent/turn-stopping,
|
|
29
|
+
* agent/status/disposed/error; tools/pre-execute and tools/execute waterfalls;
|
|
30
|
+
* producer Context.effect and Fiber.assertActive. No AgentLoop private phase,
|
|
31
|
+
* synthetic task id, polling, persistence, wall-clock task guessing or new tool.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export const DEVELOPMENT_TASK_SCOPE = 'creator-host-turn-direct-input-v1'
|
|
35
|
+
const MAX_PENDING_INPUTS = 256
|
|
36
|
+
const MAX_CLAIMED_INPUTS = 256
|
|
37
|
+
const object = value => value !== null && typeof value === 'object'
|
|
38
|
+
const direct = message => object(message) && message.role === 'user' && message.source?.kind === 'user'
|
|
39
|
+
const signalLike = signal => object(signal) && typeof signal.aborted === 'boolean'
|
|
40
|
+
&& typeof signal.addEventListener === 'function' && typeof signal.removeEventListener === 'function'
|
|
41
|
+
|
|
42
|
+
export class DevelopmentTaskError extends Error {
|
|
43
|
+
constructor(code) {
|
|
44
|
+
super(code)
|
|
45
|
+
this.name = 'DevelopmentTaskError'
|
|
46
|
+
this.code = code
|
|
47
|
+
this.eligible = false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const fail = code => { throw new DevelopmentTaskError(code) }
|
|
51
|
+
|
|
52
|
+
/** Returns { capture(exec), revalidate(handle, exec), dispose() }; no grant API. */
|
|
53
|
+
export function createDevelopmentTaskTracker(ctx) {
|
|
54
|
+
if (!ctx?.root || typeof ctx.on !== 'function' || typeof ctx.effect !== 'function'
|
|
55
|
+
|| typeof ctx.fiber?.assertActive !== 'function'
|
|
56
|
+
|| typeof ctx.agents?.get !== 'function' || typeof ctx.sessions?.get !== 'function'
|
|
57
|
+
|| typeof ctx.sessions?.list !== 'function') {
|
|
58
|
+
fail('TASK_LIFECYCLE_UNAVAILABLE')
|
|
59
|
+
}
|
|
60
|
+
const root = ctx.root
|
|
61
|
+
const states = new Map()
|
|
62
|
+
const observed = new WeakMap()
|
|
63
|
+
// Existing history is a baseline, not replayable live input. seq is a
|
|
64
|
+
// public append offset; reading it does not adopt any historical task.
|
|
65
|
+
for (const session of ctx.sessions.list()) observed.set(session, session.seq - 1)
|
|
66
|
+
const pending = new WeakMap()
|
|
67
|
+
const executions = new WeakMap()
|
|
68
|
+
const issued = new WeakMap()
|
|
69
|
+
const disposers = []
|
|
70
|
+
let disposed = false
|
|
71
|
+
|
|
72
|
+
function registered(agent, session = agent?.session) {
|
|
73
|
+
return object(agent) && object(session) && agent.session === session
|
|
74
|
+
&& agent.id === session.id && agent.ctx?.root === root
|
|
75
|
+
&& ctx.agents.get(agent.id) === agent && ctx.sessions.get(session.id) === session
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function revokeHandle(state, code) {
|
|
79
|
+
const record = state.task
|
|
80
|
+
state.task = undefined
|
|
81
|
+
if (record && !record.controller.signal.aborted) record.controller.abort(new DevelopmentTaskError(code))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function close(state, code) {
|
|
85
|
+
if (!state || state.closed) return
|
|
86
|
+
state.closed = true
|
|
87
|
+
state.anchor = undefined
|
|
88
|
+
state.claimed.clear()
|
|
89
|
+
state.admitted.clear()
|
|
90
|
+
state.detachSignal?.()
|
|
91
|
+
// Keep a closed turn tombstone until its turn/end: stopping listeners may
|
|
92
|
+
// continue the SAME turn. No later pre-step can resurrect this lifetime.
|
|
93
|
+
revokeHandle(state, code)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function inputChanged(state) {
|
|
97
|
+
if (!state || state.closed) return
|
|
98
|
+
if (state.task) {
|
|
99
|
+
state.anchor = undefined // before abort observers can reenter capture()
|
|
100
|
+
revokeHandle(state, 'TASK_NEW_INPUT')
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pendingOf(session) {
|
|
105
|
+
let inputs = pending.get(session)
|
|
106
|
+
if (!inputs) pending.set(session, inputs = new Map())
|
|
107
|
+
return inputs
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function onSessionEvent(session, event) {
|
|
111
|
+
if (disposed || !object(session) || !object(event)
|
|
112
|
+
|| !Number.isSafeInteger(event.seq) || event.seq < session.firstLiveSeq
|
|
113
|
+
|| event.seq !== session.seq - 1 || session.eventAt(event.seq) !== event) return
|
|
114
|
+
const previous = observed.get(session)
|
|
115
|
+
if (previous !== undefined && event.seq <= previous) return
|
|
116
|
+
if (previous !== undefined && event.seq !== previous + 1) {
|
|
117
|
+
close(states.get(session), 'TASK_EVENT_GAP')
|
|
118
|
+
pending.delete(session)
|
|
119
|
+
observed.set(session, event.seq)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
// Session publishes AFTER committing. An earlier listener may reenter the
|
|
123
|
+
// consumer before this listener runs: current() refuses that unseen tail.
|
|
124
|
+
try { applySessionEvent(session, event) }
|
|
125
|
+
catch {
|
|
126
|
+
close(states.get(session), 'TASK_EVENT_INVALID')
|
|
127
|
+
pending.delete(session)
|
|
128
|
+
} finally { observed.set(session, event.seq) }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function applySessionEvent(session, event) {
|
|
132
|
+
const agent = ctx.agents.get(session.id)
|
|
133
|
+
if (!registered(agent, session)) return
|
|
134
|
+
let state = states.get(session)
|
|
135
|
+
if (event.type === 'agent/inbox/spliced') {
|
|
136
|
+
for (const message of event.data.inserted) {
|
|
137
|
+
inputChanged(state)
|
|
138
|
+
// A new direct input makes even an uncaptured previous anchor stale.
|
|
139
|
+
if (direct(message) && state) state.anchor = undefined
|
|
140
|
+
if (!direct(message) || typeof message.id !== 'string' || message.id.length > 4096) continue
|
|
141
|
+
const inputs = pendingOf(session)
|
|
142
|
+
if (inputs.size >= MAX_PENDING_INPUTS) inputs.delete(inputs.keys().next().value)
|
|
143
|
+
inputs.set(message.id, event.seq)
|
|
144
|
+
}
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
if (event.type === 'turn/start') {
|
|
148
|
+
close(state, 'TASK_TURN_REPLACED')
|
|
149
|
+
if (agent.status !== 'running') return
|
|
150
|
+
state = {
|
|
151
|
+
agent, session, agentContext: agent.ctx, start: event, turn: event.data.turn,
|
|
152
|
+
signal: undefined, detachSignal: undefined, closed: false, task: undefined,
|
|
153
|
+
anchor: undefined, claimed: new Map(), admitted: new Set(), step: undefined,
|
|
154
|
+
}
|
|
155
|
+
states.set(session, state)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
if (!state || state.agent !== agent) return
|
|
159
|
+
if (event.type === 'turn/end') {
|
|
160
|
+
close(state, 'TASK_TURN_ENDED')
|
|
161
|
+
states.delete(session)
|
|
162
|
+
} else if (event.type === 'session/end-seed') {
|
|
163
|
+
close(state, 'TASK_HISTORY_BOUNDARY')
|
|
164
|
+
} else if (event.type === 'step/start') {
|
|
165
|
+
state.step = event
|
|
166
|
+
} else if (event.type === 'user/message') {
|
|
167
|
+
inputChanged(state)
|
|
168
|
+
if (direct(event.data)) state.anchor = undefined
|
|
169
|
+
if (!state.closed && direct(event.data) && state.admitted.delete(event.data.id)
|
|
170
|
+
&& event.surfaceOp === 'append' && event.sourceEventSeqs === undefined
|
|
171
|
+
&& state.step?.data.turn === state.turn && state.step.seq < event.seq) {
|
|
172
|
+
state.anchor = event
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function onClaimed({ agent, message, turn }) {
|
|
178
|
+
if (disposed || !registered(agent)) return
|
|
179
|
+
const session = agent.session
|
|
180
|
+
const inputs = pending.get(session)
|
|
181
|
+
const insertionSeq = inputs?.get(message.id)
|
|
182
|
+
inputs?.delete(message.id)
|
|
183
|
+
const state = states.get(session)
|
|
184
|
+
if (!state || state.closed || state.agent !== agent || state.turn !== turn
|
|
185
|
+
|| insertionSeq === undefined || !direct(message)) return
|
|
186
|
+
if (state.claimed.size >= MAX_CLAIMED_INPUTS) {
|
|
187
|
+
close(state, 'TASK_TRACKING_LIMIT')
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
state.claimed.set(message.id, insertionSeq)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function onPreStep({ agent, turn, messages, signal }, next) {
|
|
194
|
+
const state = object(agent) && states.get(agent.session)
|
|
195
|
+
if (!disposed && state && !state.closed && registered(agent)
|
|
196
|
+
&& state.agent === agent && state.turn === turn && signalLike(signal)) {
|
|
197
|
+
if (state.signal && state.signal !== signal) close(state, 'TASK_TURN_SIGNAL_CHANGED')
|
|
198
|
+
if (!state.closed && !state.signal) {
|
|
199
|
+
state.signal = signal
|
|
200
|
+
const abort = () => close(state, 'TASK_TURN_CANCELLED')
|
|
201
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
202
|
+
state.detachSignal = () => signal.removeEventListener('abort', abort)
|
|
203
|
+
}
|
|
204
|
+
if (signal.aborted) close(state, 'TASK_TURN_CANCELLED')
|
|
205
|
+
if (!state.closed) {
|
|
206
|
+
state.admitted = new Set(messages.filter(message => direct(message)
|
|
207
|
+
&& state.claimed.has(message.id)).map(message => message.id))
|
|
208
|
+
state.claimed.clear()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return next() // observation only; never return allow/enter ourselves
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function onPreExecute(exec, next) {
|
|
215
|
+
const state = object(exec.agent) && states.get(exec.agent.session)
|
|
216
|
+
if (!disposed && state && !state.closed && registered(exec.agent)
|
|
217
|
+
&& state.agent === exec.agent && typeof exec.token === 'symbol'
|
|
218
|
+
&& exec.signal === state.signal && !state.signal?.aborted && state.anchor) {
|
|
219
|
+
executions.set(exec, {
|
|
220
|
+
state, token: exec.token, agent: exec.agent, input: state.anchor,
|
|
221
|
+
callerSignal: exec.signal, phase: 'pre',
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
return next() // preserve native deny/ask/guards and other listeners
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function onExecute(exec, next) {
|
|
228
|
+
const observation = executions.get(exec)
|
|
229
|
+
if (observation?.phase === 'pre') observation.phase = 'dispatch'
|
|
230
|
+
try { return await next() }
|
|
231
|
+
finally { if (observation) observation.phase = 'closed' }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function onDisposedAgent({ agent }) {
|
|
235
|
+
if (!object(agent)) return
|
|
236
|
+
close(states.get(agent.session), 'TASK_AGENT_DISPOSED')
|
|
237
|
+
states.delete(agent.session)
|
|
238
|
+
pending.delete(agent.session)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function dispose() {
|
|
242
|
+
if (disposed) return
|
|
243
|
+
disposed = true
|
|
244
|
+
for (const state of states.values()) close(state, 'TASK_PRODUCER_DISPOSED')
|
|
245
|
+
states.clear()
|
|
246
|
+
for (const off of disposers.splice(0).reverse()) {
|
|
247
|
+
try { off() } catch { /* revoked first; a stale queued listener is inert */ }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function current(exec) {
|
|
252
|
+
if (disposed) fail('TASK_PRODUCER_DISPOSED')
|
|
253
|
+
try { ctx.fiber.assertActive() } catch { dispose(); fail('TASK_PRODUCER_DISPOSED') }
|
|
254
|
+
const observation = object(exec) && executions.get(exec)
|
|
255
|
+
if (!observation) fail('TASK_EXECUTION_UNOBSERVED')
|
|
256
|
+
if (observation.phase !== 'dispatch') fail('TASK_EXECUTION_NOT_ACTIVE')
|
|
257
|
+
const state = observation.state
|
|
258
|
+
if (observed.get(state.session) !== state.session.seq - 1) fail('TASK_EVENT_PENDING')
|
|
259
|
+
if (state.closed || states.get(state.session) !== state) fail('TASK_TURN_ENDED')
|
|
260
|
+
if (exec.token !== observation.token || exec.agent !== observation.agent
|
|
261
|
+
|| !registered(state.agent, state.session) || state.agent.ctx !== state.agentContext
|
|
262
|
+
|| state.agent.status !== 'running') fail('TASK_AGENT_MISMATCH')
|
|
263
|
+
if (state.signal?.aborted || observation.callerSignal.aborted || exec.signal?.aborted) {
|
|
264
|
+
close(state, 'TASK_TURN_CANCELLED')
|
|
265
|
+
fail('TASK_TURN_CANCELLED')
|
|
266
|
+
}
|
|
267
|
+
if (!state.anchor || state.anchor !== observation.input) fail('TASK_INPUT_CHANGED')
|
|
268
|
+
if (state.session.eventAt(state.start.seq) !== state.start
|
|
269
|
+
|| state.session.eventAt(state.anchor.seq) !== state.anchor) fail('TASK_HISTORY_CHANGED')
|
|
270
|
+
// New queued direct input already supersedes this task, even before the
|
|
271
|
+
// next step consumes it. Do not guess that multiple queued prompts agree.
|
|
272
|
+
if ([...state.agent.inbox.nextTurn, ...state.agent.inbox.nextStep].some(direct)) {
|
|
273
|
+
state.anchor = undefined
|
|
274
|
+
revokeHandle(state, 'TASK_NEW_INPUT')
|
|
275
|
+
fail('TASK_INPUT_CHANGED')
|
|
276
|
+
}
|
|
277
|
+
return state
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function capture(exec) {
|
|
281
|
+
const state = current(exec)
|
|
282
|
+
if (state.task) return state.task.handle
|
|
283
|
+
const controller = new AbortController()
|
|
284
|
+
const handle = Object.freeze({
|
|
285
|
+
scope: DEVELOPMENT_TASK_SCOPE,
|
|
286
|
+
sessionId: state.session.id,
|
|
287
|
+
turn: state.turn,
|
|
288
|
+
turnStartSeq: state.start.seq,
|
|
289
|
+
userInputSeq: state.anchor.seq,
|
|
290
|
+
signal: controller.signal,
|
|
291
|
+
evidenceBoundary: 'lifecycle-only-no-authorization',
|
|
292
|
+
})
|
|
293
|
+
const record = { state, handle, input: state.anchor, controller }
|
|
294
|
+
state.task = record
|
|
295
|
+
issued.set(handle, record)
|
|
296
|
+
return handle
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function revalidate(handle, exec) {
|
|
300
|
+
const record = object(handle) && issued.get(handle)
|
|
301
|
+
if (!record) fail('TASK_HANDLE_UNISSUED')
|
|
302
|
+
if (record.controller.signal.aborted) fail(record.controller.signal.reason.code)
|
|
303
|
+
const state = current(exec)
|
|
304
|
+
if (record.state !== state || state.task !== record || record.input !== state.anchor) {
|
|
305
|
+
fail('TASK_HANDLE_MISMATCH')
|
|
306
|
+
}
|
|
307
|
+
return handle
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
disposers.push(ctx.on('session/created', session => {
|
|
312
|
+
if (!disposed && !observed.has(session) && ctx.sessions.get(session.id) === session) {
|
|
313
|
+
observed.set(session, session.seq - 1)
|
|
314
|
+
}
|
|
315
|
+
}))
|
|
316
|
+
disposers.push(ctx.on('session/event', onSessionEvent))
|
|
317
|
+
disposers.push(ctx.on('agent/inbox/claimed', onClaimed))
|
|
318
|
+
disposers.push(ctx.on('agent/inbox/discarded', ({ agent, message }) => pending.get(agent.session)?.delete(message.id)))
|
|
319
|
+
disposers.push(ctx.on('agent/pre-step', onPreStep, { prepend: true }))
|
|
320
|
+
disposers.push(ctx.on('tools/pre-execute', onPreExecute, { prepend: true }))
|
|
321
|
+
disposers.push(ctx.on('tools/execute', onExecute, { prepend: true }))
|
|
322
|
+
disposers.push(ctx.on('agent/turn-stopping', ({ agent, turn }) => {
|
|
323
|
+
const state = states.get(agent.session)
|
|
324
|
+
if (state?.turn === turn) close(state, 'TASK_TURN_STOPPING')
|
|
325
|
+
}, { prepend: true }))
|
|
326
|
+
disposers.push(ctx.on('agent/status', ({ agent, status }) => {
|
|
327
|
+
if (status !== 'running') close(states.get(agent.session), 'TASK_AGENT_IDLE')
|
|
328
|
+
}))
|
|
329
|
+
disposers.push(ctx.on('agent/error', ({ agent }) => close(states.get(agent.session), 'TASK_AGENT_ERROR')))
|
|
330
|
+
disposers.push(ctx.on('agent/disposed', onDisposedAgent))
|
|
331
|
+
disposers.push(ctx.on('session/disposed', session => {
|
|
332
|
+
close(states.get(session), 'TASK_SESSION_DISPOSED')
|
|
333
|
+
states.delete(session)
|
|
334
|
+
pending.delete(session)
|
|
335
|
+
}))
|
|
336
|
+
ctx.effect(() => dispose, 'creator development task lifetime')
|
|
337
|
+
} catch (error) {
|
|
338
|
+
dispose()
|
|
339
|
+
throw error
|
|
340
|
+
}
|
|
341
|
+
return Object.freeze({ capture, revalidate, dispose })
|
|
342
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createDesktopProfileBridge } from './desktop-profile.js'
|
|
1
2
|
import { installTakeoverFence, requestTakeover } from './takeover.js'
|
|
2
3
|
/** Creator Mode+ model tools backed by fixed dshx operations. */
|
|
3
4
|
|
|
@@ -7,8 +8,13 @@ import {
|
|
|
7
8
|
runClaimedDshx,
|
|
8
9
|
runClientFailureDshx,
|
|
9
10
|
runDshx,
|
|
11
|
+
resolveHarnessRoot,
|
|
10
12
|
} from './runner.js'
|
|
11
13
|
import { healCreatorPlusPresets } from './preset-015.js'
|
|
14
|
+
import { createDevelopmentExecutionAuthority } from './development-execution.js'
|
|
15
|
+
import { createDevelopmentTaskTracker } from './development-tasks.js'
|
|
16
|
+
import { createDevelopmentPolicyTracker } from './development-policy.js'
|
|
17
|
+
import { createDevelopmentInvocations } from './development-invocation.js'
|
|
12
18
|
import {
|
|
13
19
|
forgetCreatorClaim,
|
|
14
20
|
installCreatorSafetyGuard,
|
|
@@ -22,7 +28,7 @@ export {
|
|
|
22
28
|
} from './compatibility.js'
|
|
23
29
|
|
|
24
30
|
export const name = 'dsh-creator-mode-plus'
|
|
25
|
-
export const inject = ['tools', 'webServer', 'connection', 'agents', 'sessions', 'userQuestions']
|
|
31
|
+
export const inject = ['tools', 'webServer', 'connection', 'agents', 'sessions', 'userQuestions', 'profileContext']
|
|
26
32
|
|
|
27
33
|
const CLIENT_FAILURE_PATH = '/dsh-creator-mode-plus/client-failure'
|
|
28
34
|
const MAX_CLIENT_FAILURE_BYTES = 16 * 1024
|
|
@@ -48,7 +54,14 @@ const output = {
|
|
|
48
54
|
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
function isSameOrigin(req) {
|
|
57
|
+
function isSameOrigin(req, options, port) {
|
|
58
|
+
// Electron checks dsh-app origin, then forwards an authenticated request
|
|
59
|
+
// without Origin. Require a script-only marker and the owned loopback route.
|
|
60
|
+
if (options.hostProfile === 'desktop' && req.headers.origin === undefined
|
|
61
|
+
&& req.headers['x-dsh-creator-client'] === '1'
|
|
62
|
+
&& req.headers['content-type'] === 'application/json'
|
|
63
|
+
&& req.headers.host === `127.0.0.1:${port}`
|
|
64
|
+
&& ['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(req.socket?.remoteAddress)) return true
|
|
52
65
|
const origin = req.headers.origin
|
|
53
66
|
const authority = req.headers.host
|
|
54
67
|
if (typeof origin !== 'string' || typeof authority !== 'string') return false
|
|
@@ -97,7 +110,7 @@ function clientFailureHandler(webServer, options) {
|
|
|
97
110
|
res.end()
|
|
98
111
|
return
|
|
99
112
|
}
|
|
100
|
-
if (!isSameOrigin(req)) {
|
|
113
|
+
if (!isSameOrigin(req, options, webServer.port)) {
|
|
101
114
|
res.writeHead(403)
|
|
102
115
|
res.end('forbidden')
|
|
103
116
|
return
|
|
@@ -174,7 +187,7 @@ export function installClientFailureRoute(ctx, options = {}) {
|
|
|
174
187
|
}
|
|
175
188
|
|
|
176
189
|
/** Register file-backed Creator Mode+ operations for one preset scope. */
|
|
177
|
-
export function apply(ctx) {
|
|
190
|
+
export function apply(ctx, config = {}) {
|
|
178
191
|
console.log('[dsh-creator-mode-plus] loaded')
|
|
179
192
|
try {
|
|
180
193
|
const healed = healCreatorPlusPresets()
|
|
@@ -184,12 +197,25 @@ export function apply(ctx) {
|
|
|
184
197
|
} catch (error) {
|
|
185
198
|
ctx.logger?.warn?.(`dsh-creator-mode-plus: persona heal failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
186
199
|
}
|
|
187
|
-
const authOptions = { getWebStartupUrl: port => typeof ctx.connection.authenticatedUrl === 'function' ? ctx.connection.authenticatedUrl(`http://127.0.0.1:${port}/`) : undefined }
|
|
200
|
+
const authOptions = { ...createDesktopProfileBridge(ctx), getWebStartupUrl: port => typeof ctx.connection.authenticatedUrl === 'function' ? ctx.connection.authenticatedUrl(`http://127.0.0.1:${port}/`) : undefined }
|
|
188
201
|
installCreatorRecovery(ctx, authOptions)
|
|
189
202
|
installClientFailureRoute(ctx, authOptions)
|
|
190
|
-
installCreatorSafetyGuard(ctx)
|
|
203
|
+
installCreatorSafetyGuard(ctx, () => resolveHarnessRoot())
|
|
191
204
|
installTakeoverFence(ctx)
|
|
192
205
|
|
|
206
|
+
// The sealed executor is a separate, undeployed integration. Enable it only
|
|
207
|
+
// in an explicitly configured development preset; ordinary Creator sessions
|
|
208
|
+
// keep the reviewed fixed bridge and the Host's existing approval stack.
|
|
209
|
+
const developmentExecution = config.developmentExecution === true
|
|
210
|
+
const ownedDefinitions = new Map()
|
|
211
|
+
const tasks = developmentExecution ? createDevelopmentTaskTracker(ctx) : undefined
|
|
212
|
+
const policy = developmentExecution ? createDevelopmentPolicyTracker(ctx) : undefined
|
|
213
|
+
let authority, invocations
|
|
214
|
+
const registerOwned = definition => {
|
|
215
|
+
ownedDefinitions.set(definition.name, definition)
|
|
216
|
+
return ctx.tools.register(definition)
|
|
217
|
+
}
|
|
218
|
+
|
|
193
219
|
ctx.tools.register({
|
|
194
220
|
name: 'dshx_claim_plugin',
|
|
195
221
|
description: 'Claim one plugin for this Creator+ session and arm the external Guardian before editing. Different sessions may claim different plugins concurrently.',
|
|
@@ -203,7 +229,7 @@ export function apply(ctx) {
|
|
|
203
229
|
output,
|
|
204
230
|
execute(args, exec) {
|
|
205
231
|
const id = pluginId(args.name)
|
|
206
|
-
return runDshx(['creator', 'claim', id], exec, { ...authOptions, hostPort: currentWebPort() }).then((result) => {
|
|
232
|
+
return runDshx(['creator', 'claim', id], exec, { ...authOptions, hostPort: (authOptions.hostPort ?? currentWebPort()) }).then((result) => {
|
|
207
233
|
if (result.exitCode === 0) rememberCreatorClaim(exec, id)
|
|
208
234
|
return result
|
|
209
235
|
})
|
|
@@ -217,7 +243,7 @@ export function apply(ctx) {
|
|
|
217
243
|
parameters: { type: 'object', properties: { name: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' } }, required: ['name'], additionalProperties: false },
|
|
218
244
|
timeoutMs: 360_000,
|
|
219
245
|
output,
|
|
220
|
-
execute: (args, exec) => requestTakeover(ctx, pluginId(args.name), exec, { ...authOptions, hostPort: currentWebPort() }),
|
|
246
|
+
execute: (args, exec) => requestTakeover(ctx, pluginId(args.name), exec, { ...authOptions, hostPort: (authOptions.hostPort ?? currentWebPort()) }),
|
|
221
247
|
presentCall: args => ({ card: 'generic', title: `申请接管 ${args.name}`, kind: 'edit', rawInput: args.name }),
|
|
222
248
|
})
|
|
223
249
|
|
|
@@ -239,12 +265,12 @@ export function apply(ctx) {
|
|
|
239
265
|
const id = pluginId(args.name)
|
|
240
266
|
return runClaimedDshx(id, [
|
|
241
267
|
'creator', 'scaffold', id, choice(args.kind, KINDS, 'plugin kind'),
|
|
242
|
-
], exec, { ...authOptions, hostPort: currentWebPort() })
|
|
268
|
+
], exec, { ...authOptions, hostPort: (authOptions.hostPort ?? currentWebPort()) })
|
|
243
269
|
},
|
|
244
270
|
presentCall: args => ({ card: 'generic', title: `dshx scaffold ${args.name}`, kind: 'edit', rawInput: args }),
|
|
245
271
|
})
|
|
246
272
|
|
|
247
|
-
|
|
273
|
+
registerOwned({
|
|
248
274
|
name: 'dshx_check',
|
|
249
275
|
description: 'Run DSHX v0.7 external-plugin checks, including client Cordis service injection and the built-client handoff. Passing proves SOURCE_BUILT only, not live activation.',
|
|
250
276
|
parameters: {
|
|
@@ -255,14 +281,21 @@ export function apply(ctx) {
|
|
|
255
281
|
},
|
|
256
282
|
timeoutMs: 60_000,
|
|
257
283
|
output,
|
|
258
|
-
execute(args, exec) {
|
|
259
|
-
|
|
260
|
-
|
|
284
|
+
async execute(args, exec) {
|
|
285
|
+
if (!developmentExecution) return runClaimedDshx(pluginId(args.name), ['check', pluginId(args.name)], exec, { ...authOptions, hostPort: (authOptions.hostPort ?? currentWebPort()) })
|
|
286
|
+
const owner = authority.enter(exec, args)
|
|
287
|
+
let call
|
|
288
|
+
try {
|
|
289
|
+
call = invocations.open(owner, exec, policy.capture(exec))
|
|
290
|
+
const id = pluginId(args.name)
|
|
291
|
+
if (call.route === 'sealed') return await call.invokeSealed()
|
|
292
|
+
return await runClaimedDshx(id, ['check', id], call.legacyExecution(), { ...authOptions, hostPort: (authOptions.hostPort ?? currentWebPort()) })
|
|
293
|
+
} finally { if (call) call.close(); else authority.revoke(owner) }
|
|
261
294
|
},
|
|
262
295
|
presentCall: args => ({ card: 'generic', title: `dshx check ${args.name}`, kind: 'read', rawInput: args.name }),
|
|
263
296
|
})
|
|
264
297
|
|
|
265
|
-
|
|
298
|
+
registerOwned({
|
|
266
299
|
name: 'dshx_activation_plan',
|
|
267
300
|
description: 'Select the activation method for one changed component: patch, manifest, preset, client, new-client, server, or artifact.',
|
|
268
301
|
parameters: {
|
|
@@ -276,16 +309,26 @@ export function apply(ctx) {
|
|
|
276
309
|
},
|
|
277
310
|
timeoutMs: 60_000,
|
|
278
311
|
output,
|
|
279
|
-
execute(args, exec) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
'activation-plan', id, '--change', choice(args.change, CHANGES, 'change surface'),
|
|
283
|
-
|
|
312
|
+
async execute(args, exec) {
|
|
313
|
+
if (!developmentExecution) {
|
|
314
|
+
const id = pluginId(args.name)
|
|
315
|
+
return runClaimedDshx(id, ['activation-plan', id, '--change', choice(args.change, CHANGES, 'change surface')], exec, { ...authOptions, hostPort: (authOptions.hostPort ?? currentWebPort()) })
|
|
316
|
+
}
|
|
317
|
+
const owner = authority.enter(exec, args)
|
|
318
|
+
let call
|
|
319
|
+
try {
|
|
320
|
+
call = invocations.open(owner, exec, policy.capture(exec))
|
|
321
|
+
const id = pluginId(args.name)
|
|
322
|
+
if (call.route === 'sealed') return await call.invokeSealed()
|
|
323
|
+
return await runClaimedDshx(id, [
|
|
324
|
+
'activation-plan', id, '--change', choice(args.change, CHANGES, 'change surface'),
|
|
325
|
+
], call.legacyExecution(), { ...authOptions, hostPort: (authOptions.hostPort ?? currentWebPort()) })
|
|
326
|
+
} finally { if (call) call.close(); else authority.revoke(owner) }
|
|
284
327
|
},
|
|
285
328
|
presentCall: args => ({ card: 'generic', title: `dshx plan ${args.change}`, kind: 'read', rawInput: args }),
|
|
286
329
|
})
|
|
287
330
|
|
|
288
|
-
|
|
331
|
+
registerOwned({
|
|
289
332
|
name: 'dshx_activate_new_client',
|
|
290
333
|
description: 'Activate one checked my-plugins Web client in the DSHX v0.7 safe order: profile link, resolution proof, watched-patch transaction, then current-Host manifest proof.',
|
|
291
334
|
parameters: {
|
|
@@ -296,12 +339,22 @@ export function apply(ctx) {
|
|
|
296
339
|
},
|
|
297
340
|
timeoutMs: 90_000,
|
|
298
341
|
output,
|
|
299
|
-
execute(args, exec) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
342
|
+
async execute(args, exec) {
|
|
343
|
+
if (!developmentExecution) {
|
|
344
|
+
const id = pluginId(args.name), port = (authOptions.hostPort ?? currentWebPort())
|
|
345
|
+
return runClaimedDshx(id, ['activate-new-client', id, '--profile', 'web', '--port', String(port)], exec, { ...authOptions, hostPort: port })
|
|
346
|
+
}
|
|
347
|
+
const owner = authority.enter(exec, args)
|
|
348
|
+
let call
|
|
349
|
+
try {
|
|
350
|
+
call = invocations.open(owner, exec, policy.capture(exec))
|
|
351
|
+
const id = pluginId(args.name)
|
|
352
|
+
if (call.route === 'sealed') return await call.invokeSealed()
|
|
353
|
+
const port = (authOptions.hostPort ?? currentWebPort())
|
|
354
|
+
return await runClaimedDshx(id, [
|
|
355
|
+
'activate-new-client', id, '--profile', 'web', '--port', String(port),
|
|
356
|
+
], call.legacyExecution(), { ...authOptions, hostPort: port })
|
|
357
|
+
} finally { if (call) call.close(); else authority.revoke(owner) }
|
|
305
358
|
},
|
|
306
359
|
presentCall: args => ({ card: 'generic', title: `dshx activate ${args.name}`, kind: 'edit', rawInput: args.name }),
|
|
307
360
|
})
|
|
@@ -319,7 +372,7 @@ export function apply(ctx) {
|
|
|
319
372
|
output,
|
|
320
373
|
async execute(args, exec) {
|
|
321
374
|
const id = pluginId(args.name)
|
|
322
|
-
const port = currentWebPort()
|
|
375
|
+
const port = (authOptions.hostPort ?? currentWebPort())
|
|
323
376
|
const result = await runClaimedDshx(id, ['creator', 'remove', id], exec, { ...authOptions, hostPort: port })
|
|
324
377
|
if (result.exitCode === 0) forgetCreatorClaim(exec)
|
|
325
378
|
return result
|
|
@@ -327,7 +380,7 @@ export function apply(ctx) {
|
|
|
327
380
|
presentCall: args => ({ card: 'generic', title: `dshx remove ${args.name}`, kind: 'edit', rawInput: args.name }),
|
|
328
381
|
})
|
|
329
382
|
|
|
330
|
-
|
|
383
|
+
registerOwned({
|
|
331
384
|
name: 'dshx_hot_reload',
|
|
332
385
|
description: 'Activate the code changes of one already-loaded, checked server plugin. The requested behavior remains RUNTIME_VERIFICATION_REQUIRED until exercised. Use the fixed plugin ID; runtime provenance and cleanup are checked internally.',
|
|
333
386
|
parameters: {
|
|
@@ -338,15 +391,25 @@ export function apply(ctx) {
|
|
|
338
391
|
},
|
|
339
392
|
timeoutMs: 90_000,
|
|
340
393
|
output,
|
|
341
|
-
execute(args, exec) {
|
|
394
|
+
async execute(args, exec) {
|
|
342
395
|
const id = pluginId(args.name)
|
|
343
396
|
if (HOT_RELOAD_INFRASTRUCTURE.has(id)) {
|
|
344
397
|
throw new Error(`dshx_hot_reload cannot replace its executing infrastructure plugin: ${id}`)
|
|
345
398
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
'hot-reload', id, '--profile', 'web', '--port', String(port), '--json',
|
|
349
|
-
|
|
399
|
+
if (!developmentExecution) {
|
|
400
|
+
const port = (authOptions.hostPort ?? currentWebPort())
|
|
401
|
+
return runClaimedDshx(id, ['hot-reload', id, '--profile', 'web', '--port', String(port), '--json'], exec, { ...authOptions, hostPort: port })
|
|
402
|
+
}
|
|
403
|
+
const owner = authority.enter(exec, args)
|
|
404
|
+
let call
|
|
405
|
+
try {
|
|
406
|
+
call = invocations.open(owner, exec, policy.capture(exec))
|
|
407
|
+
if (call.route === 'sealed') return await call.invokeSealed()
|
|
408
|
+
const port = (authOptions.hostPort ?? currentWebPort())
|
|
409
|
+
return await runClaimedDshx(id, [
|
|
410
|
+
'hot-reload', id, '--profile', 'web', '--port', String(port), '--json',
|
|
411
|
+
], call.legacyExecution(), { ...authOptions, hostPort: port })
|
|
412
|
+
} finally { if (call) call.close(); else authority.revoke(owner) }
|
|
350
413
|
},
|
|
351
414
|
presentCall: args => ({ card: 'generic', title: `dshx hot reload ${args.name}`, kind: 'edit', rawInput: args.name }),
|
|
352
415
|
})
|
|
@@ -358,7 +421,7 @@ export function apply(ctx) {
|
|
|
358
421
|
timeoutMs: 60_000,
|
|
359
422
|
output,
|
|
360
423
|
execute(_args, exec) {
|
|
361
|
-
return runDshx(['browser', 'open', '--json'], exec, { ...authOptions, hostPort: currentWebPort() })
|
|
424
|
+
return runDshx(['browser', 'open', '--json'], exec, { ...authOptions, hostPort: (authOptions.hostPort ?? currentWebPort()) })
|
|
362
425
|
},
|
|
363
426
|
presentCall: () => ({ card: 'generic', title: 'Open authenticated DSH browser', kind: 'edit', rawInput: {} }),
|
|
364
427
|
})
|
|
@@ -370,8 +433,12 @@ export function apply(ctx) {
|
|
|
370
433
|
timeoutMs: 30_000,
|
|
371
434
|
output,
|
|
372
435
|
execute(_args, exec) {
|
|
373
|
-
return runDshx(['status'], exec, { ...authOptions, hostPort: currentWebPort() })
|
|
436
|
+
return runDshx(['status'], exec, { ...authOptions, hostPort: (authOptions.hostPort ?? currentWebPort()) })
|
|
374
437
|
},
|
|
375
438
|
presentCall: () => ({ card: 'generic', title: 'dshx status', kind: 'read' }),
|
|
376
439
|
})
|
|
440
|
+
if (developmentExecution) {
|
|
441
|
+
authority = createDevelopmentExecutionAuthority(ctx, ownedDefinitions)
|
|
442
|
+
invocations = createDevelopmentInvocations(ctx, { authority, tasks, policy, getHarnessRoot: resolveHarnessRoot })
|
|
443
|
+
}
|
|
377
444
|
}
|