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,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution-bound policy lifetime, NEVER source provenance or execution permission.
|
|
3
|
+
* A reviewed C producer owns this tracker privately. Source ownership is proved
|
|
4
|
+
* separately by development-execution's exact definitions/real execute closure.
|
|
5
|
+
*
|
|
6
|
+
* Observe BEFORE next(): an old A review can await on the pre-execute unwind.
|
|
7
|
+
* Never mutate exec.signal or abort the shared Agent signal. Every invalidation
|
|
8
|
+
* closes only our signal, which C must combine into its owned request/executor.
|
|
9
|
+
*
|
|
10
|
+
* Reads use an explicitly injected producer dependency scope, not undeclared
|
|
11
|
+
* agent.ctx services. Cordis service proxies are not stable identity objects;
|
|
12
|
+
* each actual inject activation gets a private binding + effect lifetime.
|
|
13
|
+
* Canonical policy switches are native committed Session events. Arbitrary
|
|
14
|
+
* same-process mutation outside those contracts is not a sandboxed threat model.
|
|
15
|
+
*/
|
|
16
|
+
const EVENTS = new Set(['permission/preset', 'sandbox/mode', 'approval/policy'])
|
|
17
|
+
const MODES = new Set(['read-only', 'workspace-write', 'danger-full-access'])
|
|
18
|
+
const LEGACY = Object.freeze({
|
|
19
|
+
'read-only': ['read-only', 'ask'],
|
|
20
|
+
'workspace-write': ['workspace-write', 'ask'],
|
|
21
|
+
'danger-full-access': ['danger-full-access', 'never'],
|
|
22
|
+
})
|
|
23
|
+
const object = value => value !== null && typeof value === 'object'
|
|
24
|
+
const isSignal = value => value instanceof AbortSignal
|
|
25
|
+
export class DevelopmentPolicyError extends Error {
|
|
26
|
+
constructor(code) { super(code); this.name = 'DevelopmentPolicyError'; this.code = code; this.eligible = false }
|
|
27
|
+
}
|
|
28
|
+
const fail = code => { throw new DevelopmentPolicyError(code) }
|
|
29
|
+
|
|
30
|
+
/** Requires the C producer to declare agents/sessions; optional policy dependencies are declared below. */
|
|
31
|
+
export function createDevelopmentPolicyTracker(ctx) {
|
|
32
|
+
if (!ctx?.root || typeof ctx.on !== 'function' || typeof ctx.inject !== 'function' || typeof ctx.effect !== 'function'
|
|
33
|
+
|| typeof ctx.fiber?.assertActive !== 'function' || typeof ctx.agents?.get !== 'function' || typeof ctx.sessions?.get !== 'function') fail('POLICY_SERVICES_UNAVAILABLE')
|
|
34
|
+
const root = ctx.root, records = new WeakMap(), handles = new WeakMap(), active = new Set(), off = []
|
|
35
|
+
let binding, disposed = false
|
|
36
|
+
function close(record, code) {
|
|
37
|
+
if (!record || record.closed) return
|
|
38
|
+
record.closed = code
|
|
39
|
+
active.delete(record)
|
|
40
|
+
for (const detach of record.detach.splice(0)) detach()
|
|
41
|
+
record.controller.abort(new DevelopmentPolicyError(code))
|
|
42
|
+
}
|
|
43
|
+
function liveAgent(agent, session = agent?.session) {
|
|
44
|
+
return object(agent) && object(session) && agent.ctx?.root === root && agent.session === session
|
|
45
|
+
&& ctx.agents.get(agent.id) === agent && ctx.sessions.get(session.id) === session
|
|
46
|
+
}
|
|
47
|
+
function read(b, agent, session) {
|
|
48
|
+
if (!b || !b.active || b.signal.aborted) fail('POLICY_BINDING_UNAVAILABLE')
|
|
49
|
+
b.ctx.fiber.assertActive()
|
|
50
|
+
if (!liveAgent(agent, session)) fail('POLICY_AGENT_CHANGED')
|
|
51
|
+
const presets = b.ctx.permissionPresets, approval = b.ctx.approval, sandbox = b.ctx.sandboxPolicy
|
|
52
|
+
if (typeof presets?.current !== 'function' || typeof presets?.resolve !== 'function'
|
|
53
|
+
|| typeof approval?.overrideOf !== 'function' || typeof sandbox?.resolve !== 'function') fail('POLICY_SERVICES_UNAVAILABLE')
|
|
54
|
+
const preset = presets.current(session)
|
|
55
|
+
const files = sandbox.resolve({ session })
|
|
56
|
+
const policy = approval.overrideOf(session) ?? approval.config.policy ?? 'ask'
|
|
57
|
+
if (!MODES.has(files.mode) || !['ask', 'never'].includes(policy)) fail('POLICY_STATE_UNSUPPORTED')
|
|
58
|
+
if (preset === 'custom') fail('POLICY_PRESET_UNSUPPORTED')
|
|
59
|
+
const spec = presets.resolve(preset) // Unknown presets never become legacy.
|
|
60
|
+
if (spec.sandbox !== files.mode || spec.approval !== policy) fail('POLICY_STATE_MISMATCH')
|
|
61
|
+
let route
|
|
62
|
+
if (preset === 'approve-for-me') {
|
|
63
|
+
if (policy !== 'ask') fail('POLICY_NEVER')
|
|
64
|
+
route = 'sealed'
|
|
65
|
+
} else {
|
|
66
|
+
const expected = Object.hasOwn(LEGACY, preset) && LEGACY[preset]
|
|
67
|
+
if (!expected || expected[0] !== files.mode || expected[1] !== policy) fail('POLICY_PRESET_UNSUPPORTED')
|
|
68
|
+
route = 'legacy'
|
|
69
|
+
}
|
|
70
|
+
return Object.freeze({ preset, sandbox: files.mode, approval: policy, route })
|
|
71
|
+
}
|
|
72
|
+
// An unresolved pre observation remains unresolved for that invocation even
|
|
73
|
+
// when a dependency appears later. Never adopt the newest binding on resume.
|
|
74
|
+
const dependency = ctx.inject(['permissionPresets', 'approval', 'sandboxPolicy', 'agents', 'sessions'], scope => {
|
|
75
|
+
if (disposed) return
|
|
76
|
+
const controller = new AbortController()
|
|
77
|
+
const b = { ctx: scope, active: true, signal: controller.signal, epochs: new WeakMap() }
|
|
78
|
+
binding = b
|
|
79
|
+
scope.effect(() => () => {
|
|
80
|
+
b.active = false
|
|
81
|
+
if (binding === b) binding = undefined
|
|
82
|
+
controller.abort(new DevelopmentPolicyError('POLICY_BINDING_ENDED'))
|
|
83
|
+
for (const record of [...active]) if (record.binding === b) close(record, 'POLICY_BINDING_ENDED')
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
off.push(() => { void Promise.resolve(dependency.dispose()).catch(() => {}) }) // witnesses are revoked before async child cleanup
|
|
87
|
+
function watch(record, signal, code) {
|
|
88
|
+
if (signal.aborted) { close(record, code); return }
|
|
89
|
+
const abort = () => close(record, code)
|
|
90
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
91
|
+
record.detach.push(() => signal.removeEventListener('abort', abort))
|
|
92
|
+
}
|
|
93
|
+
// Epoch identity is per real Session + this exact dependency activation, not
|
|
94
|
+
// per exec. Task rules may compare it across calls while each call still owns
|
|
95
|
+
// its separate pre witness and non-revivable cancellation signal.
|
|
96
|
+
function retireEpoch(b, session, epoch = b?.epochs.get(session)) {
|
|
97
|
+
if (!epoch) return
|
|
98
|
+
epoch.invalidated = true
|
|
99
|
+
for (const record of [...active]) if (record.binding === b && record.session === session && record.policyEpoch === epoch.token) close(record, 'POLICY_EPOCH_CHANGED')
|
|
100
|
+
}
|
|
101
|
+
function epochFor(b, session, state) {
|
|
102
|
+
const prior = b.epochs.get(session)
|
|
103
|
+
if (prior && !prior.invalidated) {
|
|
104
|
+
if (session.seq < prior.cursor || ['preset', 'sandbox', 'approval', 'route'].some(key => state[key] !== prior.state[key])) retireEpoch(b, session, prior)
|
|
105
|
+
// This scan also covers changes with ZERO active executions and a pre
|
|
106
|
+
// observer running before our session/event listener has received them.
|
|
107
|
+
for (let seq = prior.cursor; !prior.invalidated && seq < session.seq; seq++) if (EVENTS.has(session.eventAt(seq)?.type)) retireEpoch(b, session, prior)
|
|
108
|
+
if (!prior.invalidated) { prior.cursor = session.seq; return prior.token }
|
|
109
|
+
}
|
|
110
|
+
const epoch = { token: Object.freeze(Object.create(null)), cursor: session.seq, state, invalidated: false }
|
|
111
|
+
b.epochs.set(session, epoch)
|
|
112
|
+
return epoch.token
|
|
113
|
+
}
|
|
114
|
+
function current(record) {
|
|
115
|
+
if (!record) fail('POLICY_EXECUTION_UNOBSERVED')
|
|
116
|
+
if (record.closed) fail(record.closed)
|
|
117
|
+
if (disposed) { close(record, 'POLICY_PRODUCER_ENDED'); fail(record.closed) }
|
|
118
|
+
try { ctx.fiber.assertActive() } catch { close(record, 'POLICY_PRODUCER_ENDED'); fail(record.closed) }
|
|
119
|
+
const exec = record.exec
|
|
120
|
+
if (exec.token !== record.token || exec.agent !== record.agent || exec.name !== record.name
|
|
121
|
+
|| exec.arguments !== record.args || exec.callId !== record.callId || exec.rootCallId !== record.rootCallId
|
|
122
|
+
|| exec.parent !== record.parent || !liveAgent(record.agent, record.session)) close(record, 'POLICY_EXECUTION_CHANGED')
|
|
123
|
+
if (!record.closed && (record.callerSignal.aborted || exec.signal?.aborted)) close(record, 'POLICY_CANCELLED')
|
|
124
|
+
if (!record.closed && record.bodySignal && exec.signal !== record.bodySignal) close(record, 'POLICY_EXECUTION_CHANGED')
|
|
125
|
+
const epoch = record.binding?.epochs.get(record.session)
|
|
126
|
+
if (!record.closed && (epoch?.invalidated || epoch?.token !== record.policyEpoch || record.session.seq < record.floor)) close(record, 'POLICY_EPOCH_CHANGED')
|
|
127
|
+
// Native log membership also catches revalidation from an earlier observer,
|
|
128
|
+
// before our event callback has run. Endpoint equality cannot defeat ABA.
|
|
129
|
+
for (let seq = record.floor; !record.closed && seq < record.session.seq; seq++) {
|
|
130
|
+
if (EVENTS.has(record.session.eventAt(seq)?.type)) { retireEpoch(record.binding, record.session); close(record, 'POLICY_EPOCH_CHANGED') }
|
|
131
|
+
}
|
|
132
|
+
if (!record.closed) {
|
|
133
|
+
try {
|
|
134
|
+
const now = read(record.binding, record.agent, record.session)
|
|
135
|
+
if (['preset', 'sandbox', 'approval', 'route'].some(key => now[key] !== record.initial[key])) retireEpoch(record.binding, record.session)
|
|
136
|
+
} catch (error) { retireEpoch(record.binding, record.session); close(record, error instanceof DevelopmentPolicyError ? error.code : 'POLICY_BINDING_UNAVAILABLE') }
|
|
137
|
+
}
|
|
138
|
+
if (record.closed) fail(record.closed)
|
|
139
|
+
return record
|
|
140
|
+
}
|
|
141
|
+
function pre(exec, next) {
|
|
142
|
+
if (disposed || !object(exec) || records.has(exec) || !liveAgent(exec.agent)
|
|
143
|
+
|| typeof exec.token !== 'symbol' || !isSignal(exec.signal)) return next()
|
|
144
|
+
const record = {
|
|
145
|
+
exec, token: exec.token, agent: exec.agent, session: exec.agent.session, name: exec.name, args: exec.arguments,
|
|
146
|
+
callId: exec.callId, rootCallId: exec.rootCallId, parent: exec.parent, callerSignal: exec.signal,
|
|
147
|
+
binding, floor: exec.agent.session.seq, phase: 'pre', detach: [], controller: new AbortController(),
|
|
148
|
+
}
|
|
149
|
+
records.set(exec, record); active.add(record)
|
|
150
|
+
try {
|
|
151
|
+
record.initial = read(record.binding, record.agent, record.session)
|
|
152
|
+
if (!record.closed) record.policyEpoch = epochFor(record.binding, record.session, record.initial)
|
|
153
|
+
} catch (error) { retireEpoch(record.binding, record.session); close(record, error instanceof DevelopmentPolicyError ? error.code : 'POLICY_BINDING_UNAVAILABLE') }
|
|
154
|
+
if (!record.closed) {
|
|
155
|
+
watch(record, record.callerSignal, 'POLICY_CANCELLED')
|
|
156
|
+
if (!record.closed) watch(record, record.binding.signal, 'POLICY_BINDING_ENDED')
|
|
157
|
+
}
|
|
158
|
+
return next() // no allow/deny/ask decision: capture must fail if this witness expired
|
|
159
|
+
}
|
|
160
|
+
function capture(exec) {
|
|
161
|
+
const record = current(object(exec) && records.get(exec))
|
|
162
|
+
if (record.phase !== 'dispatch') fail('POLICY_EXECUTION_NOT_ACTIVE')
|
|
163
|
+
if (record.handle) fail('POLICY_ALREADY_CAPTURED')
|
|
164
|
+
if (!isSignal(exec.signal)) { close(record, 'POLICY_CANCELLED'); fail(record.closed) }
|
|
165
|
+
record.bodySignal = exec.signal
|
|
166
|
+
watch(record, record.bodySignal, 'POLICY_CANCELLED')
|
|
167
|
+
current(record)
|
|
168
|
+
const handle = Object.freeze(Object.create(null))
|
|
169
|
+
record.handle = handle; handles.set(handle, record)
|
|
170
|
+
record.view = Object.freeze({ ...record.initial, policyEpoch: record.policyEpoch, agent: record.agent, session: record.session,
|
|
171
|
+
signal: AbortSignal.any([record.callerSignal, record.bodySignal, record.controller.signal, record.binding.signal]),
|
|
172
|
+
evidenceBoundary: 'policy-lifetime-only-no-authorization',
|
|
173
|
+
})
|
|
174
|
+
return handle
|
|
175
|
+
}
|
|
176
|
+
function owned(handle) {
|
|
177
|
+
const record = object(handle) && handles.get(handle)
|
|
178
|
+
if (!record) fail('POLICY_HANDLE_UNISSUED')
|
|
179
|
+
current(record)
|
|
180
|
+
if (record.phase !== 'dispatch') fail('POLICY_EXECUTION_NOT_ACTIVE')
|
|
181
|
+
return record
|
|
182
|
+
}
|
|
183
|
+
function revalidateFor(handle, exec) {
|
|
184
|
+
const record = object(handle) && handles.get(handle)
|
|
185
|
+
if (!record) fail('POLICY_HANDLE_UNISSUED')
|
|
186
|
+
if (record.exec !== exec || records.get(exec) !== record) fail('POLICY_EXECUTION_CHANGED')
|
|
187
|
+
owned(handle)
|
|
188
|
+
return handle
|
|
189
|
+
}
|
|
190
|
+
function dispose() {
|
|
191
|
+
if (disposed) return
|
|
192
|
+
disposed = true
|
|
193
|
+
for (const record of [...active]) close(record, 'POLICY_PRODUCER_ENDED')
|
|
194
|
+
for (const stop of off.splice(0).reverse()) stop()
|
|
195
|
+
}
|
|
196
|
+
off.push(ctx.on('tools/pre-execute', pre, { prepend: true }))
|
|
197
|
+
off.push(ctx.on('tools/execute', async (exec, next) => {
|
|
198
|
+
const record = records.get(exec)
|
|
199
|
+
if (record && !record.closed) record.phase = 'dispatch'
|
|
200
|
+
try { return await next() } finally { close(record, 'POLICY_EXECUTION_ENDED') }
|
|
201
|
+
}))
|
|
202
|
+
off.push(ctx.on('tools/result', exec => close(records.get(exec), 'POLICY_EXECUTION_ENDED')))
|
|
203
|
+
off.push(ctx.on('session/event', (session, event) => {
|
|
204
|
+
if (!EVENTS.has(event.type) || session.eventAt(event.seq) !== event) return
|
|
205
|
+
const epoch = binding?.epochs.get(session)
|
|
206
|
+
if (epoch && event.seq >= epoch.cursor) retireEpoch(binding, session, epoch)
|
|
207
|
+
for (const record of [...active]) if (record.session === session && event.seq >= record.floor) close(record, 'POLICY_EPOCH_CHANGED')
|
|
208
|
+
}, { global: true }))
|
|
209
|
+
off.push(ctx.on('agent/disposed', ({ agent }) => { for (const record of [...active]) if (record.agent === agent) close(record, 'POLICY_AGENT_CHANGED') }, { global: true }))
|
|
210
|
+
off.push(ctx.on('session/disposed', session => {
|
|
211
|
+
retireEpoch(binding, session); binding?.epochs.delete(session)
|
|
212
|
+
for (const record of [...active]) if (record.session === session) close(record, 'POLICY_AGENT_CHANGED')
|
|
213
|
+
}, { global: true }))
|
|
214
|
+
ctx.effect(() => dispose)
|
|
215
|
+
return Object.freeze({ capture, inspect: handle => owned(handle).view, revalidate: handle => { owned(handle); return handle }, revalidateFor, dispose })
|
|
216
|
+
}
|