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.
@@ -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
- ctx.tools.register({
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
- const id = pluginId(args.name)
260
- return runClaimedDshx(id, ['check', id], exec, { ...authOptions, hostPort: currentWebPort() })
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
- ctx.tools.register({
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
- const id = pluginId(args.name)
281
- return runClaimedDshx(id, [
282
- 'activation-plan', id, '--change', choice(args.change, CHANGES, 'change surface'),
283
- ], exec, { ...authOptions, hostPort: currentWebPort() })
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
- ctx.tools.register({
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
- const id = pluginId(args.name)
301
- const port = currentWebPort()
302
- return runClaimedDshx(id, [
303
- 'activate-new-client', id, '--profile', 'web', '--port', String(port),
304
- ], exec, { ...authOptions, hostPort: port })
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
- ctx.tools.register({
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
- const port = currentWebPort()
347
- return runClaimedDshx(id, [
348
- 'hot-reload', id, '--profile', 'web', '--port', String(port), '--json',
349
- ], exec, { ...authOptions, hostPort: port })
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
  }