/ types / permissions.ts
permissions.ts
  1  /**
  2   * Pure permission type definitions extracted to break import cycles.
  3   *
  4   * This file contains only type definitions and constants with no runtime dependencies.
  5   * Implementation files remain in src/utils/permissions/ but can now import from here
  6   * to avoid circular dependencies.
  7   */
  8  
  9  import { feature } from 'bun:bundle'
 10  import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
 11  
 12  // ============================================================================
 13  // Permission Modes
 14  // ============================================================================
 15  
 16  export const EXTERNAL_PERMISSION_MODES = [
 17    'acceptEdits',
 18    'bypassPermissions',
 19    'default',
 20    'dontAsk',
 21    'plan',
 22  ] as const
 23  
 24  export type ExternalPermissionMode = (typeof EXTERNAL_PERMISSION_MODES)[number]
 25  
 26  // Exhaustive mode union for typechecking. The user-addressable runtime set
 27  // is INTERNAL_PERMISSION_MODES below.
 28  export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'
 29  export type PermissionMode = InternalPermissionMode
 30  
 31  // Runtime validation set: modes that are user-addressable (settings.json
 32  // defaultMode, --permission-mode CLI flag, conversation recovery).
 33  export const INTERNAL_PERMISSION_MODES = [
 34    ...EXTERNAL_PERMISSION_MODES,
 35    ...(feature('TRANSCRIPT_CLASSIFIER') ? (['auto'] as const) : ([] as const)),
 36  ] as const satisfies readonly PermissionMode[]
 37  
 38  export const PERMISSION_MODES = INTERNAL_PERMISSION_MODES
 39  
 40  // ============================================================================
 41  // Permission Behaviors
 42  // ============================================================================
 43  
 44  export type PermissionBehavior = 'allow' | 'deny' | 'ask'
 45  
 46  // ============================================================================
 47  // Permission Rules
 48  // ============================================================================
 49  
 50  /**
 51   * Where a permission rule originated from.
 52   * Includes all SettingSource values plus additional rule-specific sources.
 53   */
 54  export type PermissionRuleSource =
 55    | 'userSettings'
 56    | 'projectSettings'
 57    | 'localSettings'
 58    | 'flagSettings'
 59    | 'policySettings'
 60    | 'cliArg'
 61    | 'command'
 62    | 'session'
 63  
 64  /**
 65   * The value of a permission rule - specifies which tool and optional content
 66   */
 67  export type PermissionRuleValue = {
 68    toolName: string
 69    ruleContent?: string
 70  }
 71  
 72  /**
 73   * A permission rule with its source and behavior
 74   */
 75  export type PermissionRule = {
 76    source: PermissionRuleSource
 77    ruleBehavior: PermissionBehavior
 78    ruleValue: PermissionRuleValue
 79  }
 80  
 81  // ============================================================================
 82  // Permission Updates
 83  // ============================================================================
 84  
 85  /**
 86   * Where a permission update should be persisted
 87   */
 88  export type PermissionUpdateDestination =
 89    | 'userSettings'
 90    | 'projectSettings'
 91    | 'localSettings'
 92    | 'session'
 93    | 'cliArg'
 94  
 95  /**
 96   * Update operations for permission configuration
 97   */
 98  export type PermissionUpdate =
 99    | {
100        type: 'addRules'
101        destination: PermissionUpdateDestination
102        rules: PermissionRuleValue[]
103        behavior: PermissionBehavior
104      }
105    | {
106        type: 'replaceRules'
107        destination: PermissionUpdateDestination
108        rules: PermissionRuleValue[]
109        behavior: PermissionBehavior
110      }
111    | {
112        type: 'removeRules'
113        destination: PermissionUpdateDestination
114        rules: PermissionRuleValue[]
115        behavior: PermissionBehavior
116      }
117    | {
118        type: 'setMode'
119        destination: PermissionUpdateDestination
120        mode: ExternalPermissionMode
121      }
122    | {
123        type: 'addDirectories'
124        destination: PermissionUpdateDestination
125        directories: string[]
126      }
127    | {
128        type: 'removeDirectories'
129        destination: PermissionUpdateDestination
130        directories: string[]
131      }
132  
133  /**
134   * Source of an additional working directory permission.
135   * Note: This is currently the same as PermissionRuleSource but kept as a
136   * separate type for semantic clarity and potential future divergence.
137   */
138  export type WorkingDirectorySource = PermissionRuleSource
139  
140  /**
141   * An additional directory included in permission scope
142   */
143  export type AdditionalWorkingDirectory = {
144    path: string
145    source: WorkingDirectorySource
146  }
147  
148  // ============================================================================
149  // Permission Decisions & Results
150  // ============================================================================
151  
152  /**
153   * Minimal command shape for permission metadata.
154   * This is intentionally a subset of the full Command type to avoid import cycles.
155   * Only includes properties needed by permission-related components.
156   */
157  export type PermissionCommandMetadata = {
158    name: string
159    description?: string
160    // Allow additional properties for forward compatibility
161    [key: string]: unknown
162  }
163  
164  /**
165   * Metadata attached to permission decisions
166   */
167  export type PermissionMetadata =
168    | { command: PermissionCommandMetadata }
169    | undefined
170  
171  /**
172   * Result when permission is granted
173   */
174  export type PermissionAllowDecision<
175    Input extends { [key: string]: unknown } = { [key: string]: unknown },
176  > = {
177    behavior: 'allow'
178    updatedInput?: Input
179    userModified?: boolean
180    decisionReason?: PermissionDecisionReason
181    toolUseID?: string
182    acceptFeedback?: string
183    contentBlocks?: ContentBlockParam[]
184  }
185  
186  /**
187   * Metadata for a pending classifier check that will run asynchronously.
188   * Used to enable non-blocking allow classifier evaluation.
189   */
190  export type PendingClassifierCheck = {
191    command: string
192    cwd: string
193    descriptions: string[]
194  }
195  
196  /**
197   * Result when user should be prompted
198   */
199  export type PermissionAskDecision<
200    Input extends { [key: string]: unknown } = { [key: string]: unknown },
201  > = {
202    behavior: 'ask'
203    message: string
204    updatedInput?: Input
205    decisionReason?: PermissionDecisionReason
206    suggestions?: PermissionUpdate[]
207    blockedPath?: string
208    metadata?: PermissionMetadata
209    /**
210     * If true, this ask decision was triggered by a bashCommandIsSafe_DEPRECATED security check
211     * for patterns that splitCommand_DEPRECATED could misparse (e.g. line continuations, shell-quote
212     * transformations). Used by bashToolHasPermission to block early before splitCommand_DEPRECATED
213     * transforms the command. Not set for simple newline compound commands.
214     */
215    isBashSecurityCheckForMisparsing?: boolean
216    /**
217     * If set, an allow classifier check should be run asynchronously.
218     * The classifier may auto-approve the permission before the user responds.
219     */
220    pendingClassifierCheck?: PendingClassifierCheck
221    /**
222     * Optional content blocks (e.g., images) to include alongside the rejection
223     * message in the tool result. Used when users paste images as feedback.
224     */
225    contentBlocks?: ContentBlockParam[]
226  }
227  
228  /**
229   * Result when permission is denied
230   */
231  export type PermissionDenyDecision = {
232    behavior: 'deny'
233    message: string
234    decisionReason: PermissionDecisionReason
235    toolUseID?: string
236  }
237  
238  /**
239   * A permission decision - allow, ask, or deny
240   */
241  export type PermissionDecision<
242    Input extends { [key: string]: unknown } = { [key: string]: unknown },
243  > =
244    | PermissionAllowDecision<Input>
245    | PermissionAskDecision<Input>
246    | PermissionDenyDecision
247  
248  /**
249   * Permission result with additional passthrough option
250   */
251  export type PermissionResult<
252    Input extends { [key: string]: unknown } = { [key: string]: unknown },
253  > =
254    | PermissionDecision<Input>
255    | {
256        behavior: 'passthrough'
257        message: string
258        decisionReason?: PermissionDecision<Input>['decisionReason']
259        suggestions?: PermissionUpdate[]
260        blockedPath?: string
261        /**
262         * If set, an allow classifier check should be run asynchronously.
263         * The classifier may auto-approve the permission before the user responds.
264         */
265        pendingClassifierCheck?: PendingClassifierCheck
266      }
267  
268  /**
269   * Explanation of why a permission decision was made
270   */
271  export type PermissionDecisionReason =
272    | {
273        type: 'rule'
274        rule: PermissionRule
275      }
276    | {
277        type: 'mode'
278        mode: PermissionMode
279      }
280    | {
281        type: 'subcommandResults'
282        reasons: Map<string, PermissionResult>
283      }
284    | {
285        type: 'permissionPromptTool'
286        permissionPromptToolName: string
287        toolResult: unknown
288      }
289    | {
290        type: 'hook'
291        hookName: string
292        hookSource?: string
293        reason?: string
294      }
295    | {
296        type: 'asyncAgent'
297        reason: string
298      }
299    | {
300        type: 'sandboxOverride'
301        reason: 'excludedCommand' | 'dangerouslyDisableSandbox'
302      }
303    | {
304        type: 'classifier'
305        classifier: string
306        reason: string
307      }
308    | {
309        type: 'workingDir'
310        reason: string
311      }
312    | {
313        type: 'safetyCheck'
314        reason: string
315        // When true, auto mode lets the classifier evaluate this instead of
316        // forcing a prompt. True for sensitive-file paths (.claude/, .git/,
317        // shell configs) — the classifier can see context and decide. False
318        // for Windows path bypass attempts and cross-machine bridge messages.
319        classifierApprovable: boolean
320      }
321    | {
322        type: 'other'
323        reason: string
324      }
325  
326  // ============================================================================
327  // Bash Classifier Types
328  // ============================================================================
329  
330  export type ClassifierResult = {
331    matches: boolean
332    matchedDescription?: string
333    confidence: 'high' | 'medium' | 'low'
334    reason: string
335  }
336  
337  export type ClassifierBehavior = 'deny' | 'ask' | 'allow'
338  
339  export type ClassifierUsage = {
340    inputTokens: number
341    outputTokens: number
342    cacheReadInputTokens: number
343    cacheCreationInputTokens: number
344  }
345  
346  export type YoloClassifierResult = {
347    thinking?: string
348    shouldBlock: boolean
349    reason: string
350    unavailable?: boolean
351    /**
352     * API returned "prompt is too long" — the classifier transcript exceeded
353     * the context window. Deterministic (same transcript → same error), so
354     * callers should fall back to normal prompting rather than retry/fail-closed.
355     */
356    transcriptTooLong?: boolean
357    /** The model used for this classifier call */
358    model: string
359    /** Token usage from the classifier API call (for overhead telemetry) */
360    usage?: ClassifierUsage
361    /** Duration of the classifier API call in ms */
362    durationMs?: number
363    /** Character lengths of the prompt components sent to the classifier */
364    promptLengths?: {
365      systemPrompt: number
366      toolCalls: number
367      userPrompts: number
368    }
369    /** Path where error prompts were dumped (only set when unavailable due to API error) */
370    errorDumpPath?: string
371    /** Which classifier stage produced the final decision (2-stage XML only) */
372    stage?: 'fast' | 'thinking'
373    /** Token usage from stage 1 (fast) when stage 2 was also run */
374    stage1Usage?: ClassifierUsage
375    /** Duration of stage 1 in ms when stage 2 was also run */
376    stage1DurationMs?: number
377    /**
378     * API request_id (req_xxx) for stage 1. Enables joining to server-side
379     * api_usage logs for cache-miss / routing attribution. Also used for the
380     * legacy 1-stage (tool_use) classifier — the single request goes here.
381     */
382    stage1RequestId?: string
383    /**
384     * API message id (msg_xxx) for stage 1. Enables joining the
385     * tengu_auto_mode_decision analytics event to the classifier's actual
386     * prompt/completion in post-analysis.
387     */
388    stage1MsgId?: string
389    /** Token usage from stage 2 (thinking) when stage 2 was run */
390    stage2Usage?: ClassifierUsage
391    /** Duration of stage 2 in ms when stage 2 was run */
392    stage2DurationMs?: number
393    /** API request_id for stage 2 (set whenever stage 2 ran) */
394    stage2RequestId?: string
395    /** API message id (msg_xxx) for stage 2 (set whenever stage 2 ran) */
396    stage2MsgId?: string
397  }
398  
399  // ============================================================================
400  // Permission Explainer Types
401  // ============================================================================
402  
403  export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH'
404  
405  export type PermissionExplanation = {
406    riskLevel: RiskLevel
407    explanation: string
408    reasoning: string
409    risk: string
410  }
411  
412  // ============================================================================
413  // Tool Permission Context
414  // ============================================================================
415  
416  /**
417   * Mapping of permission rules by their source
418   */
419  export type ToolPermissionRulesBySource = {
420    [T in PermissionRuleSource]?: string[]
421  }
422  
423  /**
424   * Context needed for permission checking in tools
425   * Note: Uses a simplified DeepImmutable approximation for this types-only file
426   */
427  export type ToolPermissionContext = {
428    readonly mode: PermissionMode
429    readonly additionalWorkingDirectories: ReadonlyMap<
430      string,
431      AdditionalWorkingDirectory
432    >
433    readonly alwaysAllowRules: ToolPermissionRulesBySource
434    readonly alwaysDenyRules: ToolPermissionRulesBySource
435    readonly alwaysAskRules: ToolPermissionRulesBySource
436    readonly isBypassPermissionsModeAvailable: boolean
437    readonly strippedDangerousRules?: ToolPermissionRulesBySource
438    readonly shouldAvoidPermissionPrompts?: boolean
439    readonly awaitAutomatedChecksBeforeDialog?: boolean
440    readonly prePlanMode?: PermissionMode
441  }