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.
@@ -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
+ }