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.
- package/AGENTS.md +15 -3
- package/CHANGELOG.md +35 -1
- package/README.en.md +29 -5
- package/README.md +29 -5
- package/cordis.patch.yml +3 -0
- package/docs/bridge-contract.md +25 -38
- 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 +24 -9
- 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 +11 -0
- package/preset/skills/creator-mode-plus/maintenance.md +5 -11
- 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,95 @@
|
|
|
1
|
+
/** Private, per-invocation adapter to the application's public profile APIs. */
|
|
2
|
+
import { randomBytes } from 'node:crypto'
|
|
3
|
+
import { readFileSync, realpathSync } from 'node:fs'
|
|
4
|
+
import { join, resolve, sep } from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
import { stringify } from 'yaml'
|
|
7
|
+
|
|
8
|
+
const PATH = '/dsh-creator-mode-plus/desktop-profile'
|
|
9
|
+
const REGISTRY = Symbol.for('dsh-creator-mode-plus.desktop-profile.v1')
|
|
10
|
+
const ID = /^[a-z][a-z0-9-]*$/
|
|
11
|
+
|
|
12
|
+
export function createDesktopProfileBridge(ctx, dependencies = {}) {
|
|
13
|
+
const profile = ctx.profileContext
|
|
14
|
+
if (profile?.name !== 'desktop') return {}
|
|
15
|
+
const anchorSuffix = '/node_modules/@deepseek-ai/dsh/package.json'
|
|
16
|
+
if (!profile.installAnchor.endsWith(anchorSuffix)) throw new Error('Unrecognized Desktop runtime install anchor')
|
|
17
|
+
const hostRoot = profile.installAnchor.slice(0, -anchorSuffix.length)
|
|
18
|
+
const server = ctx.webServer
|
|
19
|
+
const hostKey = ctx.root ?? server
|
|
20
|
+
globalThis[REGISTRY] ??= new WeakMap()
|
|
21
|
+
let shared = globalThis[REGISTRY].get(hostKey)
|
|
22
|
+
if (!shared) {
|
|
23
|
+
shared = { tickets: new Map(), owners: 0 }
|
|
24
|
+
shared.dispose = server.register({ kind: 'exact', path: PATH, handler: async (req, res) => {
|
|
25
|
+
const ticket = shared.tickets.get(req.headers['x-dshx-profile-ticket'])
|
|
26
|
+
if (req.method !== 'POST' || !ticket || ticket.signal?.aborted || Date.now() > ticket.expires) {
|
|
27
|
+
res.writeHead(403); res.end('desktop profile access expired'); return
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
let bytes = 0; const chunks = []
|
|
31
|
+
for await (const part of req) {
|
|
32
|
+
bytes += part.length
|
|
33
|
+
if (bytes > 4096) throw new Error('profile request too large')
|
|
34
|
+
chunks.push(part)
|
|
35
|
+
}
|
|
36
|
+
const request = JSON.parse(Buffer.concat(chunks).toString())
|
|
37
|
+
const result = await ticket.run(request)
|
|
38
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' })
|
|
39
|
+
res.end(JSON.stringify(result))
|
|
40
|
+
} catch (error) {
|
|
41
|
+
res.writeHead(400, { 'content-type': 'application/json' })
|
|
42
|
+
res.end(JSON.stringify({ code: 1, stdout: '', stderr: String(error.message ?? error) }))
|
|
43
|
+
}
|
|
44
|
+
} })
|
|
45
|
+
globalThis[REGISTRY].set(hostKey, shared)
|
|
46
|
+
}
|
|
47
|
+
shared.owners++
|
|
48
|
+
const owned = new Set()
|
|
49
|
+
ctx.effect(() => () => {
|
|
50
|
+
for (const token of owned) shared.tickets.delete(token)
|
|
51
|
+
if (--shared.owners === 0) { shared.dispose(); globalThis[REGISTRY].delete(hostKey) }
|
|
52
|
+
})
|
|
53
|
+
return {
|
|
54
|
+
hostProfile: 'desktop', hostPort: server.port,
|
|
55
|
+
hostRoot, hostHome: profile.home,
|
|
56
|
+
createProfileAccess(args, exec, root) {
|
|
57
|
+
const token = randomBytes(32).toString('hex')
|
|
58
|
+
let mutationUsed = false
|
|
59
|
+
const pluginId = args[0] === 'creator' ? args[2] : args[1]
|
|
60
|
+
const mutation = args[0] === 'activate-new-client' ? 'add'
|
|
61
|
+
: args[0] === 'creator' && args[1] === 'remove' ? 'remove' : undefined
|
|
62
|
+
const run = async request => {
|
|
63
|
+
if (request.operation === 'dump') {
|
|
64
|
+
const boot = dependencies.boot ?? await import('@deepseek-ai/dsh-app-boot')
|
|
65
|
+
const rows = boot.composeEntries([boot.readProfilePatches('dshx', profile)])
|
|
66
|
+
// Configuration values and credential references never leave the Host.
|
|
67
|
+
return { code: 0, stderr: '', stdout: stringify(rows.map(({ id, name, disabled }) => ({ id, ...name ? { name } : {}, ...disabled ? { disabled } : {} }))) }
|
|
68
|
+
}
|
|
69
|
+
if (!mutation || mutationUsed || request.operation !== mutation || request.pluginId !== pluginId || !ID.test(pluginId)) throw new Error('desktop profile operation not granted')
|
|
70
|
+
const claims = dependencies.claims ?? await import(pathToFileURL(join(root, 'tools/dshx/src/internal/creator-claims.mjs')).href)
|
|
71
|
+
claims.assertUnfenced(root, exec.agent.id, pluginId)
|
|
72
|
+
if (!claims.listClaims(root).some(row => row.sessionId === exec.agent.id && row.pluginId === pluginId)) throw new Error('desktop profile claim changed')
|
|
73
|
+
const source = realpathSync(join(root, 'my-plugins', pluginId))
|
|
74
|
+
if (source.split(sep).includes('node_modules') || ['apps', 'packages', 'vendor'].some(dir => source === join(root, dir) || source.startsWith(join(root, dir) + sep))) throw new Error('protected plugin source')
|
|
75
|
+
const pkg = JSON.parse(readFileSync(join(source, 'package.json'), 'utf8'))
|
|
76
|
+
if (pkg.name !== pluginId || pkg.dsh?.bundle !== undefined) throw new Error('desktop watched operation requires a matching plain plugin')
|
|
77
|
+
const manifest = JSON.parse(readFileSync(join(profile.dir, 'package.json'), 'utf8'))
|
|
78
|
+
if (manifest.dsh?.profile?.bundles?.includes(pluginId)) throw new Error('desktop bundle removal is external-only')
|
|
79
|
+
const existing = manifest.dependencies?.[pluginId]
|
|
80
|
+
if (existing && (!existing.startsWith('link:') || realpathSync(resolve(profile.dir, existing.slice(5))) !== source)) throw new Error('desktop profile dependency identity changed')
|
|
81
|
+
mutationUsed = true
|
|
82
|
+
const operations = dependencies.operations ?? await import('@deepseek-ai/dsh-plugin-manager/operations')
|
|
83
|
+
const pm = profile.packageManager
|
|
84
|
+
const result = await operations.runPluginCommand({ profile: profile.name, dir: profile.dir, installAnchor: profile.installAnchor, cwd: source, home: profile.home },
|
|
85
|
+
mutation === 'add' ? ['add', `link:${source}`] : ['remove', pluginId],
|
|
86
|
+
{ execution: 'service', ...pm, signal: exec.signal, outputBytes: 16384, activateNewBundles: false, lockWaitMs: 30000, idleTimeoutMs: 60000 })
|
|
87
|
+
return { code: result.exitCode, stdout: result.output, stderr: '' }
|
|
88
|
+
}
|
|
89
|
+
shared.tickets.set(token, { run, signal: exec.signal, expires: Date.now() + 120000 })
|
|
90
|
+
owned.add(token)
|
|
91
|
+
return { env: { DSHX_DESKTOP_PROFILE_ACCESS: JSON.stringify({ token, port: server.port, hostPid: process.pid }) },
|
|
92
|
+
dispose() { shared.tickets.delete(token); owned.delete(token) } }
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private Creator execute/request provenance, NOT execution permission.
|
|
3
|
+
*
|
|
4
|
+
* TRUSTED WIRING REQUIRED: only reviewed C apply code constructs this object
|
|
5
|
+
* from its own four definition objects and keeps the authority private. Only
|
|
6
|
+
* those real execute closures call enter(exec, args). A receives reader only,
|
|
7
|
+
* through reviewed private wiring, not an arbitrary sourceVerified callback.
|
|
8
|
+
* This public factory cannot prove who called it; same-process plugin code is
|
|
9
|
+
* not sandboxed. Tool names merely select the fixed operation vocabulary.
|
|
10
|
+
* Definition/execute identity, native exec/token identity and closure ownership
|
|
11
|
+
* are the premises. No name, callId, JSON marker or allowed-once proves them.
|
|
12
|
+
*
|
|
13
|
+
* Public seams: ctx.root/fiber.assertActive/effect/on; tools.get(name, agent),
|
|
14
|
+
* agents.get and sessions.get; Agent.ctx/session; tools/pre-execute, execute,
|
|
15
|
+
* result/change; agent/disposed and session/disposed. Inject tools/agents/sessions.
|
|
16
|
+
* Native pre signal is the caller signal. Tools may fuse a wrapper/timeout
|
|
17
|
+
* signal before the body. We retain BOTH and construct a request signal from
|
|
18
|
+
* them plus the private owner lifetime; no arbitrary supplied signal is bound.
|
|
19
|
+
*
|
|
20
|
+
* enter is single-use per native token. Owners are frozen empty objects, all
|
|
21
|
+
* state is private. makeApprovalRequest is single-use per owner and binds the
|
|
22
|
+
* exact frozen request, never its callId. inspect/revalidate/reader lookups are
|
|
23
|
+
* live reads, not transferable grants; repeat lookup immediately before use.
|
|
24
|
+
* Native Approval does NOT deduplicate repeated calls with the same request:
|
|
25
|
+
* question deduplication and one-use grant/ticket consumption belong elsewhere.
|
|
26
|
+
* Task/target binding is also a separate private C composition, not this API.
|
|
27
|
+
* Closing dispatch, cancellation, definition replacement or producer disposal
|
|
28
|
+
* invalidates the owner/request. No rules, approvals, execution method, disk,
|
|
29
|
+
* CLI or activation here. Even activate-new-client provenance says NOTHING
|
|
30
|
+
* about sealed-byte support or whether an operation can skip human review.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const OPERATIONS = Object.freeze({
|
|
34
|
+
dshx_check: 'check',
|
|
35
|
+
dshx_activation_plan: 'activation-plan',
|
|
36
|
+
dshx_hot_reload: 'hot-reload',
|
|
37
|
+
dshx_activate_new_client: 'activate-new-client',
|
|
38
|
+
})
|
|
39
|
+
const CHANGES = new Set(['patch', 'manifest', 'preset', 'client', 'new-client', 'server', 'artifact'])
|
|
40
|
+
const object = value => value !== null && typeof value === 'object'
|
|
41
|
+
const signal = value => value instanceof AbortSignal
|
|
42
|
+
|
|
43
|
+
export class DevelopmentExecutionError extends Error {
|
|
44
|
+
constructor(code) { super(code); this.name = 'DevelopmentExecutionError'; this.code = code; this.eligible = false }
|
|
45
|
+
}
|
|
46
|
+
const fail = code => { throw new DevelopmentExecutionError(code) }
|
|
47
|
+
|
|
48
|
+
/** ownedDefinitions is a trusted, private Map<fixed tool name, exact definition>. */
|
|
49
|
+
export function createDevelopmentExecutionAuthority(ctx, ownedDefinitions) {
|
|
50
|
+
if (!ctx?.root || typeof ctx.on !== 'function' || typeof ctx.effect !== 'function'
|
|
51
|
+
|| typeof ctx.fiber?.assertActive !== 'function' || typeof ctx.tools?.get !== 'function'
|
|
52
|
+
|| typeof ctx.agents?.get !== 'function' || typeof ctx.sessions?.get !== 'function') fail('EXECUTION_SERVICES_UNAVAILABLE')
|
|
53
|
+
if (!(ownedDefinitions instanceof Map) || ownedDefinitions.size !== 4) fail('EXECUTION_DEFINITIONS_INVALID')
|
|
54
|
+
const definitions = new Map()
|
|
55
|
+
for (const [name, definition] of ownedDefinitions) {
|
|
56
|
+
if (!Object.hasOwn(OPERATIONS, name) || !object(definition)
|
|
57
|
+
|| definition.name !== name || typeof definition.execute !== 'function') fail('EXECUTION_DEFINITIONS_INVALID')
|
|
58
|
+
definitions.set(name, { definition, execute: definition.execute, operation: OPERATIONS[name] })
|
|
59
|
+
}
|
|
60
|
+
const root = ctx.root
|
|
61
|
+
const observations = new WeakMap()
|
|
62
|
+
const owners = new WeakMap()
|
|
63
|
+
const requests = new WeakMap()
|
|
64
|
+
const active = new Set()
|
|
65
|
+
// Native Tools tokens are non-registered Symbols, valid weak keys in the
|
|
66
|
+
// current Host. Remember replay without retaining every completed call.
|
|
67
|
+
const usedTokens = new WeakSet()
|
|
68
|
+
const disposers = []
|
|
69
|
+
let disposed = false
|
|
70
|
+
|
|
71
|
+
function live(agent, session, agentCtx) {
|
|
72
|
+
return object(agent) && object(session) && agent.ctx === agentCtx && agentCtx?.root === root
|
|
73
|
+
&& agent.session === session && ctx.agents.get(agent.id) === agent
|
|
74
|
+
&& ctx.sessions.get(session.id) === session && agent.id === session.id
|
|
75
|
+
}
|
|
76
|
+
function close(record, code) {
|
|
77
|
+
if (!record || record.closed) return
|
|
78
|
+
record.closed = code
|
|
79
|
+
record.phase = 'closed'
|
|
80
|
+
active.delete(record)
|
|
81
|
+
record.callerSignal.removeEventListener('abort', record.onAbort)
|
|
82
|
+
record.bodySignal?.removeEventListener('abort', record.onAbort)
|
|
83
|
+
record.controller?.abort(new DevelopmentExecutionError(code))
|
|
84
|
+
}
|
|
85
|
+
function current(record) {
|
|
86
|
+
if (disposed) fail('EXECUTION_PRODUCER_DISPOSED')
|
|
87
|
+
try { ctx.fiber.assertActive() } catch { dispose(); fail('EXECUTION_PRODUCER_DISPOSED') }
|
|
88
|
+
if (!record) fail('EXECUTION_UNOBSERVED')
|
|
89
|
+
if (record.closed) fail(record.closed)
|
|
90
|
+
const { exec, definition, execute, agent, session, agentCtx } = record
|
|
91
|
+
let code
|
|
92
|
+
if (record.phase !== 'dispatch') code = 'EXECUTION_NOT_ACTIVE'
|
|
93
|
+
else if (!live(agent, session, agentCtx)) code = 'EXECUTION_AGENT_CHANGED'
|
|
94
|
+
else if (exec.token !== record.token || exec.agent !== agent || exec.name !== record.name
|
|
95
|
+
|| exec.callId !== record.callId || exec.rootCallId !== record.rootCallId || exec.parent !== record.parent
|
|
96
|
+
|| exec.arguments !== record.args) code = 'EXECUTION_IDENTITY_CHANGED'
|
|
97
|
+
else if (ctx.tools.get(record.name, agent) !== definition || definition.name !== record.name
|
|
98
|
+
|| definition.execute !== execute) code = 'EXECUTION_DEFINITION_CHANGED'
|
|
99
|
+
else if (record.callerSignal.aborted || !signal(exec.signal) || exec.signal.aborted
|
|
100
|
+
|| record.bodySignal?.aborted || record.controller?.signal.aborted) code = 'EXECUTION_CANCELLED'
|
|
101
|
+
else if (record.bodySignal && exec.signal !== record.bodySignal) code = 'EXECUTION_BODY_SIGNAL_CHANGED'
|
|
102
|
+
if (code) { close(record, code); fail(code) }
|
|
103
|
+
return record
|
|
104
|
+
}
|
|
105
|
+
function owned(owner) {
|
|
106
|
+
const record = object(owner) ? owners.get(owner) : undefined
|
|
107
|
+
if (!record) fail('EXECUTION_OWNER_UNISSUED')
|
|
108
|
+
return current(record)
|
|
109
|
+
}
|
|
110
|
+
function argumentsFor(record, args) {
|
|
111
|
+
if (args !== record.args || !object(args) || Array.isArray(args) || !Object.isFrozen(args)) fail('EXECUTION_ARGUMENTS_INVALID')
|
|
112
|
+
const keys = Reflect.ownKeys(args)
|
|
113
|
+
const expected = record.operation === 'activation-plan' ? ['name', 'change'] : ['name']
|
|
114
|
+
if (keys.length !== expected.length || keys.some(key => !expected.includes(key))) fail('EXECUTION_ARGUMENTS_INVALID')
|
|
115
|
+
for (const key of expected) {
|
|
116
|
+
const descriptor = Object.getOwnPropertyDescriptor(args, key)
|
|
117
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value') || typeof descriptor.value !== 'string') fail('EXECUTION_ARGUMENTS_INVALID')
|
|
118
|
+
}
|
|
119
|
+
if (!/^[a-z][a-z0-9-]*$/.test(args.name)
|
|
120
|
+
|| (record.operation === 'activation-plan' && !CHANGES.has(args.change))) fail('EXECUTION_ARGUMENTS_INVALID')
|
|
121
|
+
return Object.freeze({ operation: record.operation, pluginId: args.name,
|
|
122
|
+
...(record.operation === 'activation-plan' ? { change: args.change } : {}),
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function enter(exec, args) {
|
|
127
|
+
const record = current(object(exec) ? observations.get(exec) : undefined)
|
|
128
|
+
if (record.owner || usedTokens.has(record.token)) fail('EXECUTION_ALREADY_ENTERED')
|
|
129
|
+
// Burn the native token even if an attempted closure entry has bad args.
|
|
130
|
+
usedTokens.add(record.token)
|
|
131
|
+
let operation
|
|
132
|
+
try { operation = argumentsFor(record, args) }
|
|
133
|
+
catch (error) { close(record, 'EXECUTION_ARGUMENTS_INVALID'); throw error }
|
|
134
|
+
record.bodySignal = exec.signal
|
|
135
|
+
record.bodySignal.addEventListener('abort', record.onAbort, { once: true })
|
|
136
|
+
record.controller = new AbortController()
|
|
137
|
+
const owner = Object.freeze(Object.create(null))
|
|
138
|
+
record.owner = owner
|
|
139
|
+
record.signal = AbortSignal.any([record.callerSignal, record.bodySignal, record.controller.signal])
|
|
140
|
+
record.view = Object.freeze({
|
|
141
|
+
...operation, owner, agent: record.agent, session: record.session,
|
|
142
|
+
toolName: record.name, callId: record.callId, rootCallId: record.rootCallId,
|
|
143
|
+
token: record.token, callerSignal: record.callerSignal, bodySignal: record.bodySignal,
|
|
144
|
+
signal: record.signal, evidenceBoundary: 'owned-execution-only-no-authorization',
|
|
145
|
+
})
|
|
146
|
+
owners.set(owner, record)
|
|
147
|
+
current(record)
|
|
148
|
+
return owner
|
|
149
|
+
}
|
|
150
|
+
function inspect(owner) { return owned(owner).view }
|
|
151
|
+
function revalidate(owner) { owned(owner); return owner }
|
|
152
|
+
// Revoke-only private composition: policy/peer loss can cancel the exact
|
|
153
|
+
// already-issued request.signal. It cannot mint a new owner or reopen one.
|
|
154
|
+
function revoke(owner) {
|
|
155
|
+
const record = object(owner) ? owners.get(owner) : undefined
|
|
156
|
+
if (!record) fail('EXECUTION_OWNER_UNISSUED')
|
|
157
|
+
const wasLive = !record.closed
|
|
158
|
+
close(record, 'EXECUTION_REVOKED')
|
|
159
|
+
return wasLive
|
|
160
|
+
}
|
|
161
|
+
function makeApprovalRequest(owner, reason) {
|
|
162
|
+
const record = owned(owner)
|
|
163
|
+
if (record.request) fail('EXECUTION_REQUEST_ALREADY_BOUND')
|
|
164
|
+
if (reason !== undefined && (typeof reason !== 'string' || reason.length > 4096)) fail('EXECUTION_REQUEST_REASON_INVALID')
|
|
165
|
+
const request = Object.freeze({
|
|
166
|
+
agent: record.agent, toolName: record.name, callId: record.callId,
|
|
167
|
+
signal: record.signal, ...(reason === undefined ? {} : { reason }),
|
|
168
|
+
})
|
|
169
|
+
record.request = request
|
|
170
|
+
requests.set(request, record)
|
|
171
|
+
return request
|
|
172
|
+
}
|
|
173
|
+
function lookupRequest(request) {
|
|
174
|
+
const record = object(request) ? requests.get(request) : undefined
|
|
175
|
+
if (!record || record.request !== request) return undefined
|
|
176
|
+
try {
|
|
177
|
+
current(record)
|
|
178
|
+
if (request.agent !== record.agent || request.toolName !== record.name
|
|
179
|
+
|| request.callId !== record.callId || request.signal !== record.signal) return undefined
|
|
180
|
+
return record.view
|
|
181
|
+
} catch { return undefined }
|
|
182
|
+
}
|
|
183
|
+
function dispose() {
|
|
184
|
+
if (disposed) return
|
|
185
|
+
disposed = true
|
|
186
|
+
for (const record of [...active]) close(record, 'EXECUTION_PRODUCER_DISPOSED')
|
|
187
|
+
for (const remove of disposers.splice(0).reverse()) remove()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
disposers.push(ctx.on('tools/pre-execute', async (exec, next) => {
|
|
192
|
+
const selected = definitions.get(exec.name)
|
|
193
|
+
if (!disposed && selected && object(exec) && !observations.has(exec) && !usedTokens.has(exec.token)
|
|
194
|
+
&& typeof exec.token === 'symbol' && Symbol.keyFor(exec.token) === undefined
|
|
195
|
+
&& signal(exec.signal) && !exec.signal.aborted
|
|
196
|
+
&& live(exec.agent, exec.agent?.session, exec.agent?.ctx)
|
|
197
|
+
&& ctx.tools.get(exec.name, exec.agent) === selected.definition
|
|
198
|
+
&& selected.definition.execute === selected.execute) {
|
|
199
|
+
const record = {
|
|
200
|
+
...selected, exec, name: exec.name, token: exec.token, args: exec.arguments,
|
|
201
|
+
callId: exec.callId, rootCallId: exec.rootCallId, parent: exec.parent,
|
|
202
|
+
agent: exec.agent, session: exec.agent.session, agentCtx: exec.agent.ctx,
|
|
203
|
+
callerSignal: exec.signal, phase: 'pre', closed: undefined,
|
|
204
|
+
}
|
|
205
|
+
record.onAbort = () => close(record, 'EXECUTION_CANCELLED')
|
|
206
|
+
exec.signal.addEventListener('abort', record.onAbort, { once: true })
|
|
207
|
+
observations.set(exec, record)
|
|
208
|
+
active.add(record)
|
|
209
|
+
}
|
|
210
|
+
return next() // Observation only: never return allow/ask or alter policy.
|
|
211
|
+
}))
|
|
212
|
+
disposers.push(ctx.on('tools/execute', async (exec, next) => {
|
|
213
|
+
const record = observations.get(exec)
|
|
214
|
+
if (!record || record.closed || record.phase !== 'pre') return next()
|
|
215
|
+
record.phase = 'dispatch'
|
|
216
|
+
try { return await next() }
|
|
217
|
+
finally { close(record, 'EXECUTION_ENDED') }
|
|
218
|
+
}))
|
|
219
|
+
disposers.push(ctx.on('tools/result', exec => close(observations.get(exec), 'EXECUTION_ENDED')))
|
|
220
|
+
disposers.push(ctx.on('tools/change', () => {
|
|
221
|
+
for (const record of [...active]) {
|
|
222
|
+
if (ctx.tools.get(record.name, record.agent) !== record.definition
|
|
223
|
+
|| record.definition.execute !== record.execute) close(record, 'EXECUTION_DEFINITION_CHANGED')
|
|
224
|
+
}
|
|
225
|
+
}, { global: true }))
|
|
226
|
+
disposers.push(ctx.on('agent/disposed', ({ agent }) => {
|
|
227
|
+
for (const record of [...active]) if (record.agent === agent) close(record, 'EXECUTION_AGENT_DISPOSED')
|
|
228
|
+
}))
|
|
229
|
+
disposers.push(ctx.on('session/disposed', session => {
|
|
230
|
+
for (const record of [...active]) if (record.session === session) close(record, 'EXECUTION_SESSION_DISPOSED')
|
|
231
|
+
}))
|
|
232
|
+
ctx.effect(() => dispose)
|
|
233
|
+
} catch (error) { dispose(); throw error }
|
|
234
|
+
return Object.freeze({ enter, inspect, revalidate, revoke, makeApprovalRequest, lookupRequest,
|
|
235
|
+
reader: Object.freeze({ lookupRequest }), dispose,
|
|
236
|
+
})
|
|
237
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/** Private C execute composition. No model/HTTP owner enrollment, grant writer,
|
|
2
|
+
* script runner, argv or automatic approval fallback. Optional creatorExecution
|
|
3
|
+
* is the public Cordis seam; its captured peer is private to one C generation.
|
|
4
|
+
*/
|
|
5
|
+
import { types as utilTypes } from 'node:util'
|
|
6
|
+
import { createDevelopmentTargetResolver } from './development-target.js'
|
|
7
|
+
import { DevelopmentTaskError } from './development-tasks.js'
|
|
8
|
+
|
|
9
|
+
const object = value => value !== null && typeof value === 'object'
|
|
10
|
+
const signal = value => value instanceof AbortSignal
|
|
11
|
+
const PROTOCOL = 'creator-owned-execution-v1'
|
|
12
|
+
const SYNC = ['lookupTarget', 'readRegisteredTarget', 'prepare', 'release', 'dispose']
|
|
13
|
+
const METHODS = [...SYNC, 'authorize', 'dispatch']
|
|
14
|
+
export class DevelopmentInvocationError extends Error {
|
|
15
|
+
constructor(code, cause) { super(code, cause ? { cause } : undefined); this.name = 'DevelopmentInvocationError'; this.code = code; this.eligible = false }
|
|
16
|
+
}
|
|
17
|
+
const fail = code => { throw new DevelopmentInvocationError(code) }
|
|
18
|
+
function synchronous(value) {
|
|
19
|
+
if (utilTypes.isPromise(value)) { void Promise.prototype.then.call(value, undefined, () => {}); fail('INVOCATION_ASYNC_PRIVATE_READER') }
|
|
20
|
+
if (typeof value?.then === 'function') fail('INVOCATION_ASYNC_PRIVATE_READER')
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
function waitFor(promise, lifetime) {
|
|
24
|
+
if (lifetime.aborted) { void Promise.resolve(promise).catch(() => {}); return Promise.reject(new DevelopmentInvocationError('INVOCATION_CANCELLED')) }
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const abort = () => { detach(); reject(new DevelopmentInvocationError('INVOCATION_CANCELLED')) }
|
|
27
|
+
const detach = () => lifetime.removeEventListener('abort', abort)
|
|
28
|
+
lifetime.addEventListener('abort', abort, { once: true })
|
|
29
|
+
Promise.resolve(promise).then(value => { detach(); resolve(value) }, error => { detach(); reject(error) })
|
|
30
|
+
if (lifetime.aborted) abort()
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Called by real C apply only; exact owner/exec associations never leave here. */
|
|
35
|
+
export function createDevelopmentInvocations(ctx, { authority, tasks, policy, getHarnessRoot } = {}) {
|
|
36
|
+
if (!ctx?.root || typeof ctx.inject !== 'function' || typeof ctx.effect !== 'function' || typeof ctx.on !== 'function'
|
|
37
|
+
|| typeof getHarnessRoot !== 'function' || typeof authority?.revoke !== 'function'
|
|
38
|
+
|| typeof tasks?.capture !== 'function' || typeof policy?.revalidateFor !== 'function') fail('INVOCATION_SERVICES_UNAVAILABLE')
|
|
39
|
+
const root = ctx.root, producer = new AbortController(), byOwner = new WeakMap(), byExec = new WeakMap(), preBindings = new WeakMap(), opened = new WeakSet(), active = new Set(), connections = new Set()
|
|
40
|
+
let binding, disposed = false
|
|
41
|
+
|
|
42
|
+
function cleanup(frame) {
|
|
43
|
+
// May run again if cancellation occurred synchronously INSIDE prepare and
|
|
44
|
+
// its returned preparation was assigned only afterwards.
|
|
45
|
+
if (frame.preparation && !frame.released) {
|
|
46
|
+
frame.released = true
|
|
47
|
+
try { synchronous(frame.connection.methods.release(frame.owner, frame.preparation)) }
|
|
48
|
+
catch (error) { frame.cleanupError ??= error }
|
|
49
|
+
}
|
|
50
|
+
if (frame.compilation && !frame.inputReleased) {
|
|
51
|
+
frame.inputReleased = true
|
|
52
|
+
frame.targets.releaseCompilation(frame.compilation)
|
|
53
|
+
}
|
|
54
|
+
frame.targets?.dispose()
|
|
55
|
+
}
|
|
56
|
+
function close(frame) {
|
|
57
|
+
if (!frame.closed) {
|
|
58
|
+
frame.closed = true
|
|
59
|
+
active.delete(frame); byOwner.delete(frame.owner); byExec.delete(frame.exec)
|
|
60
|
+
for (const detach of frame.detach.splice(0)) detach()
|
|
61
|
+
authority.revoke(frame.owner) // Never abort the shared Agent/body signal.
|
|
62
|
+
}
|
|
63
|
+
cleanup(frame)
|
|
64
|
+
}
|
|
65
|
+
function watch(frame, lifetime) {
|
|
66
|
+
if (!signal(lifetime) || lifetime.aborted) { close(frame); fail('INVOCATION_CANCELLED') }
|
|
67
|
+
const cancel = () => close(frame)
|
|
68
|
+
lifetime.addEventListener('abort', cancel, { once: true })
|
|
69
|
+
frame.detach.push(() => lifetime.removeEventListener('abort', cancel))
|
|
70
|
+
}
|
|
71
|
+
function connectionLive(connection) {
|
|
72
|
+
if (!connection || !connection.active || disposed || producer.signal.aborted || !connection.binding.active
|
|
73
|
+
|| connection.signal.aborted || connection.peer.signal !== connection.signal) fail('INVOCATION_PEER_ENDED')
|
|
74
|
+
connection.binding.scope.fiber.assertActive()
|
|
75
|
+
}
|
|
76
|
+
function check(frame, target = true) {
|
|
77
|
+
if (!frame || frame.closed || disposed || producer.signal.aborted) fail('INVOCATION_CLOSED')
|
|
78
|
+
ctx.fiber.assertActive()
|
|
79
|
+
const source = authority.inspect(frame.owner), mode = policy.inspect(frame.policyHandle)
|
|
80
|
+
// Read-only exact-exec validation consumes no new pre witness. capture
|
|
81
|
+
// itself remains single-use, including for the same real native exec.
|
|
82
|
+
if (policy.revalidateFor(frame.policyHandle, frame.exec) !== frame.policyHandle || source.token !== frame.exec.token
|
|
83
|
+
|| source.agent !== frame.exec.agent || source.session !== frame.exec.agent.session
|
|
84
|
+
|| source.operation !== frame.operation || source.pluginId !== frame.pluginId
|
|
85
|
+
|| mode.policyEpoch !== frame.policyEpoch || mode.route !== frame.route || mode.signal !== frame.policySignal
|
|
86
|
+
|| source.signal !== frame.signal || frame.signal.aborted) fail('INVOCATION_BINDING_CHANGED')
|
|
87
|
+
if (frame.task) tasks.revalidate(frame.task, frame.exec)
|
|
88
|
+
if (frame.connection) connectionLive(frame.connection)
|
|
89
|
+
if (target && frame.snapshot) frame.targets.revalidate(frame.snapshot, frame.exec)
|
|
90
|
+
return source
|
|
91
|
+
}
|
|
92
|
+
function inspectOwner(connection, owner) {
|
|
93
|
+
const frame = object(owner) ? byOwner.get(owner) : undefined
|
|
94
|
+
if (!frame || frame.connection !== connection || !frame.request || !frame.snapshot) return undefined
|
|
95
|
+
try {
|
|
96
|
+
check(frame)
|
|
97
|
+
return Object.freeze({ exactNativeRequest: frame.request, binding: frame.snapshot.binding,
|
|
98
|
+
operation: frame.operation, policyEpoch: frame.policyEpoch, signal: frame.signal,
|
|
99
|
+
...(frame.task ? { taskHandle: frame.task, taskSignal: frame.task.signal } : {}) })
|
|
100
|
+
} catch { close(frame); return undefined }
|
|
101
|
+
}
|
|
102
|
+
function compilationInput(connection, owner, handle) {
|
|
103
|
+
const frame = object(owner) ? byOwner.get(owner) : undefined
|
|
104
|
+
if (!frame || frame.connection !== connection || frame.compilation !== handle || !frame.request) return undefined
|
|
105
|
+
try { check(frame, false); return frame.targets.compilationInput(handle, frame.exec) }
|
|
106
|
+
catch { close(frame); return undefined }
|
|
107
|
+
}
|
|
108
|
+
function endConnection(connection) {
|
|
109
|
+
if (!connection.active) return
|
|
110
|
+
connection.active = false
|
|
111
|
+
connection.signal?.removeEventListener('abort', connection.cancel)
|
|
112
|
+
for (const frame of [...active]) if (frame.connection === connection) close(frame)
|
|
113
|
+
connections.delete(connection)
|
|
114
|
+
try { synchronous(connection.methods?.dispose()) } catch { /* Its signal already prevents reuse. */ }
|
|
115
|
+
}
|
|
116
|
+
function connect(b) {
|
|
117
|
+
if (!b || !b.active || !b.facade || disposed) fail('INVOCATION_EXECUTOR_UNAVAILABLE')
|
|
118
|
+
b.scope.fiber.assertActive()
|
|
119
|
+
if (b.connection?.active && !b.connection.signal.aborted) {
|
|
120
|
+
connectionLive(b.connection)
|
|
121
|
+
return b.connection
|
|
122
|
+
}
|
|
123
|
+
// Reconnection is allowed only while OPENING A NEW invocation. Existing
|
|
124
|
+
// frames and their reader closures retain their original connection.
|
|
125
|
+
const connection = { binding: b, active: true }
|
|
126
|
+
const reader = Object.freeze({ root, signal: producer.signal,
|
|
127
|
+
inspectOwner: owner => inspectOwner(connection, owner),
|
|
128
|
+
compilationInput: (owner, handle) => compilationInput(connection, owner, handle) })
|
|
129
|
+
let peer
|
|
130
|
+
try {
|
|
131
|
+
peer = synchronous(b.connect.call(b.facade, reader))
|
|
132
|
+
if (!object(peer) || !signal(peer.signal) || peer.signal.aborted
|
|
133
|
+
|| !Array.isArray(peer.supportedOperations) || peer.supportedOperations.length !== 1
|
|
134
|
+
|| peer.supportedOperations[0] !== 'hot-reload') fail('INVOCATION_EXECUTOR_INVALID')
|
|
135
|
+
const methods = Object.create(null)
|
|
136
|
+
for (const name of METHODS) {
|
|
137
|
+
const method = peer[name]
|
|
138
|
+
if (typeof method !== 'function' || (SYNC.includes(name) && utilTypes.isAsyncFunction(method))) fail('INVOCATION_EXECUTOR_INVALID')
|
|
139
|
+
methods[name] = (...args) => method.apply(peer, args)
|
|
140
|
+
}
|
|
141
|
+
Object.assign(connection, { peer, signal: peer.signal, methods, supportedOperations: Object.freeze([...peer.supportedOperations]) })
|
|
142
|
+
connection.cancel = () => endConnection(connection)
|
|
143
|
+
connections.add(connection); b.connection = connection
|
|
144
|
+
connection.signal.addEventListener('abort', connection.cancel, { once: true })
|
|
145
|
+
connectionLive(connection)
|
|
146
|
+
return connection
|
|
147
|
+
} catch (error) {
|
|
148
|
+
connection.active = false
|
|
149
|
+
connections.delete(connection)
|
|
150
|
+
if (peer && typeof peer.dispose === 'function') { try { synchronous(peer.dispose()) } catch {} }
|
|
151
|
+
throw error instanceof DevelopmentInvocationError ? error : new DevelopmentInvocationError('INVOCATION_EXECUTOR_UNAVAILABLE', error)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const dependency = ctx.inject(['creatorExecution'], scope => {
|
|
155
|
+
if (disposed) return
|
|
156
|
+
const b = { scope, active: true }
|
|
157
|
+
try {
|
|
158
|
+
const facade = scope.creatorExecution
|
|
159
|
+
if (facade?.protocol === PROTOCOL && facade.root === root && typeof facade.connectProducer === 'function'
|
|
160
|
+
&& !utilTypes.isAsyncFunction(facade.connectProducer)) Object.assign(b, { facade, connect: facade.connectProducer })
|
|
161
|
+
} catch { /* Invalid optional service closes sealed only, never tool registration. */ }
|
|
162
|
+
binding = b
|
|
163
|
+
scope.effect(() => () => {
|
|
164
|
+
b.active = false
|
|
165
|
+
if (binding === b) binding = undefined
|
|
166
|
+
for (const connection of [...connections]) if (connection.binding === b) endConnection(connection)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
// Observe, but never connect/prepare/request, before an earlier A reviewer
|
|
170
|
+
// unwinds. A body may not borrow a service or existing peer installed while
|
|
171
|
+
// its original native pre was waiting. These are private activation objects,
|
|
172
|
+
// not repeatedly-created Cordis service proxies.
|
|
173
|
+
const stopPre = ctx.on('tools/pre-execute', (exec, next) => {
|
|
174
|
+
if (!disposed && !preBindings.has(exec)) {
|
|
175
|
+
const b = binding, connection = b?.connection
|
|
176
|
+
preBindings.set(exec, { binding: b,
|
|
177
|
+
connection: connection?.active && !connection.signal.aborted ? connection : undefined })
|
|
178
|
+
}
|
|
179
|
+
return next()
|
|
180
|
+
}, { prepend: true })
|
|
181
|
+
|
|
182
|
+
function open(owner, exec, policyHandle) {
|
|
183
|
+
if (disposed || opened.has(exec) || byOwner.has(owner)) fail('INVOCATION_ALREADY_OPENED')
|
|
184
|
+
const source = authority.inspect(owner), mode = policy.inspect(policyHandle)
|
|
185
|
+
if (policy.revalidateFor(policyHandle, exec) !== policyHandle || source.token !== exec.token || source.agent !== exec.agent
|
|
186
|
+
|| source.session !== exec.agent.session) fail('INVOCATION_BINDING_CHANGED')
|
|
187
|
+
opened.add(exec)
|
|
188
|
+
const frame = { owner, exec, policyHandle, route: mode.route, operation: source.operation, pluginId: source.pluginId,
|
|
189
|
+
policyEpoch: mode.policyEpoch, policySignal: mode.signal, signal: source.signal, detach: [], phase: 'open', closed: false }
|
|
190
|
+
byOwner.set(owner, frame); byExec.set(exec, frame); active.add(frame)
|
|
191
|
+
try {
|
|
192
|
+
watch(frame, mode.signal); watch(frame, producer.signal); watch(frame, source.signal)
|
|
193
|
+
try { frame.task = tasks.capture(exec) }
|
|
194
|
+
catch (error) { if (!(error instanceof DevelopmentTaskError)) throw error }
|
|
195
|
+
if (frame.task) watch(frame, frame.task.signal)
|
|
196
|
+
if (frame.route === 'sealed') {
|
|
197
|
+
const before = preBindings.get(exec)
|
|
198
|
+
if (!before || !before.binding || before.binding !== binding || !before.binding.active) fail('INVOCATION_PRE_BINDING_ENDED')
|
|
199
|
+
frame.connection = before.connection ?? connect(before.binding)
|
|
200
|
+
connectionLive(frame.connection)
|
|
201
|
+
watch(frame, frame.connection.signal)
|
|
202
|
+
} else if (frame.route !== 'legacy') fail('INVOCATION_POLICY_UNSUPPORTED')
|
|
203
|
+
check(frame, false)
|
|
204
|
+
return Object.freeze({ route: frame.route,
|
|
205
|
+
legacyExecution() {
|
|
206
|
+
check(frame, false)
|
|
207
|
+
if (frame.route !== 'legacy' || frame.phase !== 'open') fail('INVOCATION_LEGACY_FORBIDDEN')
|
|
208
|
+
frame.phase = 'legacy'
|
|
209
|
+
// A signal-replaced exec copy is ONLY an input to the old runner.
|
|
210
|
+
return Object.freeze({ ...exec, signal: frame.signal })
|
|
211
|
+
},
|
|
212
|
+
invokeSealed: () => invokeSealed(frame),
|
|
213
|
+
close() {
|
|
214
|
+
close(frame)
|
|
215
|
+
if (frame.cleanupError) throw new DevelopmentInvocationError('INVOCATION_CLEANUP_FAILED', frame.cleanupError)
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
} catch (error) { close(frame); throw error }
|
|
219
|
+
}
|
|
220
|
+
async function invokeSealed(frame) {
|
|
221
|
+
check(frame, false)
|
|
222
|
+
if (frame.route !== 'sealed' || frame.phase !== 'open') fail('INVOCATION_SEALED_REPLAY')
|
|
223
|
+
frame.phase = 'preparing'
|
|
224
|
+
const connection = frame.connection
|
|
225
|
+
if (!connection.supportedOperations.includes(frame.operation)) fail('INVOCATION_OPERATION_UNSUPPORTED')
|
|
226
|
+
frame.lease = synchronous(connection.methods.lookupTarget(frame.pluginId))
|
|
227
|
+
if (!object(frame.lease)) fail('INVOCATION_TARGET_UNREGISTERED')
|
|
228
|
+
check(frame, false)
|
|
229
|
+
frame.targets = createDevelopmentTargetResolver({ getHarnessRoot,
|
|
230
|
+
readRegisteredTarget: lease => {
|
|
231
|
+
check(frame, false)
|
|
232
|
+
if (lease !== frame.lease) return undefined
|
|
233
|
+
return synchronous(connection.methods.readRegisteredTarget(lease))
|
|
234
|
+
},
|
|
235
|
+
readCompilationSignals: original => {
|
|
236
|
+
if (original !== frame.exec || byExec.get(original) !== frame) return undefined
|
|
237
|
+
const source = check(frame, false)
|
|
238
|
+
return { ownerSignal: source.signal, policySignal: frame.policySignal }
|
|
239
|
+
}, producerSignal: producer.signal })
|
|
240
|
+
frame.compilation = frame.targets.captureCompilation(frame.exec, frame.lease)
|
|
241
|
+
frame.snapshot = frame.targets.compilationSnapshot(frame.compilation, frame.exec)
|
|
242
|
+
frame.request = authority.makeApprovalRequest(frame.owner, `Creator ${frame.operation}: ${frame.pluginId}; sealed in-process execution`)
|
|
243
|
+
check(frame)
|
|
244
|
+
frame.preparation = synchronous(connection.methods.prepare(frame.owner, frame.lease, frame.compilation))
|
|
245
|
+
if (!object(frame.preparation)) fail('INVOCATION_PREPARATION_INVALID')
|
|
246
|
+
check(frame)
|
|
247
|
+
frame.phase = 'authorizing'
|
|
248
|
+
const capability = await waitFor(connection.methods.authorize(frame.owner, frame.preparation), frame.signal)
|
|
249
|
+
check(frame)
|
|
250
|
+
if (!object(capability)) fail('INVOCATION_CAPABILITY_INVALID')
|
|
251
|
+
frame.phase = 'dispatching'
|
|
252
|
+
// The fixed peer performs its own final source/policy/capability consume.
|
|
253
|
+
// Its result is an explicit in-process managed receipt, never a CLI claim.
|
|
254
|
+
return await waitFor(connection.methods.dispatch(frame.owner, frame.preparation, capability), frame.signal)
|
|
255
|
+
}
|
|
256
|
+
function dispose() {
|
|
257
|
+
if (disposed) return
|
|
258
|
+
disposed = true
|
|
259
|
+
stopPre()
|
|
260
|
+
producer.abort(new DevelopmentInvocationError('INVOCATION_PRODUCER_DISPOSED'))
|
|
261
|
+
for (const frame of [...active]) close(frame)
|
|
262
|
+
for (const connection of [...connections]) endConnection(connection)
|
|
263
|
+
void Promise.resolve(dependency.dispose()).catch(() => {})
|
|
264
|
+
}
|
|
265
|
+
ctx.effect(() => dispose)
|
|
266
|
+
return Object.freeze({ open, dispose })
|
|
267
|
+
}
|