/** Package-owned approval audit-stream invariants. @module @deepseek-ai/dsh-user-approval/invariant */ import type { Context } from 'cordis' import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' import type { ApprovalRequestId } from './index.ts' import { APPROVAL_POLICIES } from './index.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-user-approval' const APPROVAL_OUTCOMES = ['allowed-once', 'rejected', 'cancelled', 'unavailable'] as const /** Cordis companion plugin name. */ export const name = 'user-approval-invariant' /** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] type ApprovalTransition = | { kind: 'asked'; id: ApprovalRequestId } | { kind: 'decided'; id: ApprovalRequestId } /** Validate one approval event against committed unmatched questions. */ function validateApprovalEvent( pending: ReadonlySet, event: SessionEvent, fail: InvariantFailure, ): ApprovalTransition | undefined { if (event.type === 'approval/asked') { if (event.data.toolName.length === 0) fail('approval/asked toolName must be non-empty') if (pending.has(event.data.id)) fail(`approval/asked repeated open id ${JSON.stringify(event.data.id)}`) return { kind: 'asked', id: event.data.id } } if (event.type === 'approval/decided') { if (!pending.has(event.data.id)) fail(`approval/decided has no matching approval/asked for id ${JSON.stringify(event.data.id)}`) if (!APPROVAL_OUTCOMES.includes(event.data.outcome)) { fail(`approval/decided carries unknown outcome ${JSON.stringify(event.data.outcome)}`) } return { kind: 'decided', id: event.data.id } } if (event.type === 'approval/policy' && !APPROVAL_POLICIES.includes(event.data.policy)) { fail(`approval/policy carries unknown policy ${JSON.stringify(event.data.policy)}`) } return undefined } /** Apply one accepted approval-pair transition. */ function applyApprovalTransition(pending: Set, transition: ApprovalTransition): void { if (transition.kind === 'asked') pending.add(transition.id) else pending.delete(transition.id) } /** Install audit pairing and closed-vocabulary checks. */ const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { const traces = new WeakMap>() const staged = new WeakMap() const seed = (session: Session): Set => { const pending = new Set() traces.set(session, pending) for (const event of session.events) { const transition = validateApprovalEvent(pending, event, fail) if (transition !== undefined) applyApprovalTransition(pending, transition) } return pending } const traceFor = (session: Session): Set => traces.get(session) ?? seed(session) for (const session of ctx.sessions.list()) seed(session) ctx.on('session/created', (session) => { seed(session) }, { global: true }) ctx.on('session/event', (session, event) => { if (event.type !== 'approval/asked' && event.type !== 'approval/decided') return const candidate = staged.get(event) /* v8 ignore next -- internal/dispatch stages every package-owned pair event */ if (candidate === undefined || candidate.session !== session) return fail('approval audit event published without pre-commit validation') staged.delete(event) applyApprovalTransition(traceFor(session), candidate.transition) }, { global: true }) ctx.on('internal/dispatch', (_mode, eventName, args) => { if (eventName !== 'session/event') return const [session, event] = args as [Session, SessionEvent] const transition = validateApprovalEvent(traceFor(session), event, fail) if (transition !== undefined) staged.set(event, { session, transition }) }, { global: true }) }, { inject: ['sessions'] }) /** * Register the approval invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))