返回 CodeWhale
hooks.ts
根目录 / crates / tui / extension-host / src / shims / hooks.ts
1 /** A pre-execute mod proposes changes; Rust owns folding and admission. */
2 import { isJson } from '../json.ts'
3 import type { HookCallPayload, HookVerdictWire } from '../protocol.ts'
4 import type { OwnedEntry, OwnerBase } from './owned.ts'
5
6 export interface LocalHook<O extends OwnerBase> extends OwnedEntry<O> {
7 callback: (exec: Readonly<HookExecution>, next: () => Promise<HookVerdictWire>) => unknown
8 }
9
10 /** DSH's call-view spelling, without its agent/runtime/token authorities. */
11 export interface HookExecution {
12 readonly name: string
13 readonly callId: string
14 readonly arguments: unknown
15 readonly signal: AbortSignal
16 readonly workspace: string
17 readonly mode: string
18 readonly model: string
19 }
20
21 function freezeJson(value: any): any {
22 if (value && typeof value === 'object') {
23 for (const child of Object.values(value)) freezeJson(child)
24 Object.freeze(value)
25 }
26 return value
27 }
28
29 export function hookExecution(payload: HookCallPayload, signal: AbortSignal): Readonly<HookExecution> {
30 return Object.freeze({
31 name: payload.name,
32 callId: payload.call_id,
33 arguments: freezeJson(payload.input),
34 signal,
35 workspace: payload.workspace,
36 mode: payload.mode,
37 model: payload.model,
38 })
39 }
40
41 /** Unsupported or malformed decisions throw, so the Rust strict hook fails closed. */
42 export function hookVerdict(value: unknown, onAllow: () => void): HookVerdictWire {
43 if (value === undefined || value === null) return { kind: 'abstain' }
44 if (!isJson(value) || typeof value !== 'object' || value === null || Array.isArray(value)) {
45 throw new TypeError('pre-execute listener must return a JSON verdict')
46 }
47 const result = value as Record<string, any>
48 switch (result.kind) {
49 case 'allow':
50 onAllow()
51 return { kind: 'abstain' }
52 case 'abstain':
53 return { kind: 'abstain' }
54 case 'deny':
55 if (typeof result.reason !== 'string') throw new TypeError('deny needs a reason')
56 return { kind: 'deny', reason: result.reason }
57 case 'ask':
58 if (result.reason !== undefined && typeof result.reason !== 'string') throw new TypeError('ask reason must be text')
59 return { kind: 'ask', reason: result.reason ?? 'Requested by a pre-execute listener' }
60 case 'annotate':
61 if (typeof result.text !== 'string') throw new TypeError('annotate needs text')
62 return { kind: 'annotate', text: result.text }
63 case 'revise':
64 if (!result.input || typeof result.input !== 'object' || Array.isArray(result.input)) {
65 throw new TypeError('revise needs a JSON object input')
66 }
67 return { kind: 'revise', input: result.input }
68 default:
69 throw new TypeError(`unsupported pre-execute verdict ${JSON.stringify(result.kind)}`)
70 }
71 }
72
72 lines TYPESCRIPT