442 lines
13 KiB
TypeScript
442 lines
13 KiB
TypeScript
/**
|
|
* Pure permission type definitions extracted to break import cycles.
|
|
*
|
|
* This file contains only type definitions and constants with no runtime dependencies.
|
|
* Implementation files remain in src/utils/permissions/ but can now import from here
|
|
* to avoid circular dependencies.
|
|
*/
|
|
|
|
import { feature } from 'bun:bundle'
|
|
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
|
|
|
|
// ============================================================================
|
|
// Permission Modes
|
|
// ============================================================================
|
|
|
|
export const EXTERNAL_PERMISSION_MODES = [
|
|
'acceptEdits',
|
|
'bypassPermissions',
|
|
'default',
|
|
'dontAsk',
|
|
'plan',
|
|
] as const
|
|
|
|
export type ExternalPermissionMode = (typeof EXTERNAL_PERMISSION_MODES)[number]
|
|
|
|
// Exhaustive mode union for typechecking. The user-addressable runtime set
|
|
// is INTERNAL_PERMISSION_MODES below.
|
|
export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'
|
|
export type PermissionMode = InternalPermissionMode
|
|
|
|
// Runtime validation set: modes that are user-addressable (settings.json
|
|
// defaultMode, --permission-mode CLI flag, conversation recovery).
|
|
export const INTERNAL_PERMISSION_MODES = [
|
|
...EXTERNAL_PERMISSION_MODES,
|
|
...(feature('TRANSCRIPT_CLASSIFIER') ? (['auto'] as const) : ([] as const)),
|
|
] as const satisfies readonly PermissionMode[]
|
|
|
|
export const PERMISSION_MODES = INTERNAL_PERMISSION_MODES
|
|
|
|
// ============================================================================
|
|
// Permission Behaviors
|
|
// ============================================================================
|
|
|
|
export type PermissionBehavior = 'allow' | 'deny' | 'ask'
|
|
|
|
// ============================================================================
|
|
// Permission Rules
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Where a permission rule originated from.
|
|
* Includes all SettingSource values plus additional rule-specific sources.
|
|
*/
|
|
export type PermissionRuleSource =
|
|
| 'userSettings'
|
|
| 'projectSettings'
|
|
| 'localSettings'
|
|
| 'flagSettings'
|
|
| 'policySettings'
|
|
| 'cliArg'
|
|
| 'command'
|
|
| 'session'
|
|
|
|
/**
|
|
* The value of a permission rule - specifies which tool and optional content
|
|
*/
|
|
export type PermissionRuleValue = {
|
|
toolName: string
|
|
ruleContent?: string
|
|
}
|
|
|
|
/**
|
|
* A permission rule with its source and behavior
|
|
*/
|
|
export type PermissionRule = {
|
|
source: PermissionRuleSource
|
|
ruleBehavior: PermissionBehavior
|
|
ruleValue: PermissionRuleValue
|
|
}
|
|
|
|
// ============================================================================
|
|
// Permission Updates
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Where a permission update should be persisted
|
|
*/
|
|
export type PermissionUpdateDestination =
|
|
| 'userSettings'
|
|
| 'projectSettings'
|
|
| 'localSettings'
|
|
| 'session'
|
|
| 'cliArg'
|
|
|
|
/**
|
|
* Update operations for permission configuration
|
|
*/
|
|
export type PermissionUpdate =
|
|
| {
|
|
type: 'addRules'
|
|
destination: PermissionUpdateDestination
|
|
rules: PermissionRuleValue[]
|
|
behavior: PermissionBehavior
|
|
}
|
|
| {
|
|
type: 'replaceRules'
|
|
destination: PermissionUpdateDestination
|
|
rules: PermissionRuleValue[]
|
|
behavior: PermissionBehavior
|
|
}
|
|
| {
|
|
type: 'removeRules'
|
|
destination: PermissionUpdateDestination
|
|
rules: PermissionRuleValue[]
|
|
behavior: PermissionBehavior
|
|
}
|
|
| {
|
|
type: 'setMode'
|
|
destination: PermissionUpdateDestination
|
|
mode: ExternalPermissionMode
|
|
}
|
|
| {
|
|
type: 'addDirectories'
|
|
destination: PermissionUpdateDestination
|
|
directories: string[]
|
|
}
|
|
| {
|
|
type: 'removeDirectories'
|
|
destination: PermissionUpdateDestination
|
|
directories: string[]
|
|
}
|
|
|
|
/**
|
|
* Source of an additional working directory permission.
|
|
* Note: This is currently the same as PermissionRuleSource but kept as a
|
|
* separate type for semantic clarity and potential future divergence.
|
|
*/
|
|
export type WorkingDirectorySource = PermissionRuleSource
|
|
|
|
/**
|
|
* An additional directory included in permission scope
|
|
*/
|
|
export type AdditionalWorkingDirectory = {
|
|
path: string
|
|
source: WorkingDirectorySource
|
|
}
|
|
|
|
// ============================================================================
|
|
// Permission Decisions & Results
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Minimal command shape for permission metadata.
|
|
* This is intentionally a subset of the full Command type to avoid import cycles.
|
|
* Only includes properties needed by permission-related components.
|
|
*/
|
|
export type PermissionCommandMetadata = {
|
|
name: string
|
|
description?: string
|
|
// Allow additional properties for forward compatibility
|
|
[key: string]: unknown
|
|
}
|
|
|
|
/**
|
|
* Metadata attached to permission decisions
|
|
*/
|
|
export type PermissionMetadata =
|
|
| { command: PermissionCommandMetadata }
|
|
| undefined
|
|
|
|
/**
|
|
* Result when permission is granted
|
|
*/
|
|
export type PermissionAllowDecision<
|
|
Input extends { [key: string]: unknown } = { [key: string]: unknown },
|
|
> = {
|
|
behavior: 'allow'
|
|
updatedInput?: Input
|
|
userModified?: boolean
|
|
decisionReason?: PermissionDecisionReason
|
|
toolUseID?: string
|
|
acceptFeedback?: string
|
|
contentBlocks?: ContentBlockParam[]
|
|
}
|
|
|
|
/**
|
|
* Metadata for a pending classifier check that will run asynchronously.
|
|
* Used to enable non-blocking allow classifier evaluation.
|
|
*/
|
|
export type PendingClassifierCheck = {
|
|
command: string
|
|
cwd: string
|
|
descriptions: string[]
|
|
}
|
|
|
|
/**
|
|
* Result when user should be prompted
|
|
*/
|
|
export type PermissionAskDecision<
|
|
Input extends { [key: string]: unknown } = { [key: string]: unknown },
|
|
> = {
|
|
behavior: 'ask'
|
|
message: string
|
|
updatedInput?: Input
|
|
decisionReason?: PermissionDecisionReason
|
|
suggestions?: PermissionUpdate[]
|
|
blockedPath?: string
|
|
metadata?: PermissionMetadata
|
|
/**
|
|
* If true, this ask decision was triggered by a bashCommandIsSafe_DEPRECATED security check
|
|
* for patterns that splitCommand_DEPRECATED could misparse (e.g. line continuations, shell-quote
|
|
* transformations). Used by bashToolHasPermission to block early before splitCommand_DEPRECATED
|
|
* transforms the command. Not set for simple newline compound commands.
|
|
*/
|
|
isBashSecurityCheckForMisparsing?: boolean
|
|
/**
|
|
* If set, an allow classifier check should be run asynchronously.
|
|
* The classifier may auto-approve the permission before the user responds.
|
|
*/
|
|
pendingClassifierCheck?: PendingClassifierCheck
|
|
/**
|
|
* Optional content blocks (e.g., images) to include alongside the rejection
|
|
* message in the tool result. Used when users paste images as feedback.
|
|
*/
|
|
contentBlocks?: ContentBlockParam[]
|
|
}
|
|
|
|
/**
|
|
* Result when permission is denied
|
|
*/
|
|
export type PermissionDenyDecision = {
|
|
behavior: 'deny'
|
|
message: string
|
|
decisionReason: PermissionDecisionReason
|
|
toolUseID?: string
|
|
}
|
|
|
|
/**
|
|
* A permission decision - allow, ask, or deny
|
|
*/
|
|
export type PermissionDecision<
|
|
Input extends { [key: string]: unknown } = { [key: string]: unknown },
|
|
> =
|
|
| PermissionAllowDecision<Input>
|
|
| PermissionAskDecision<Input>
|
|
| PermissionDenyDecision
|
|
|
|
/**
|
|
* Permission result with additional passthrough option
|
|
*/
|
|
export type PermissionResult<
|
|
Input extends { [key: string]: unknown } = { [key: string]: unknown },
|
|
> =
|
|
| PermissionDecision<Input>
|
|
| {
|
|
behavior: 'passthrough'
|
|
message: string
|
|
decisionReason?: PermissionDecision<Input>['decisionReason']
|
|
suggestions?: PermissionUpdate[]
|
|
blockedPath?: string
|
|
/**
|
|
* If set, an allow classifier check should be run asynchronously.
|
|
* The classifier may auto-approve the permission before the user responds.
|
|
*/
|
|
pendingClassifierCheck?: PendingClassifierCheck
|
|
}
|
|
|
|
/**
|
|
* Explanation of why a permission decision was made
|
|
*/
|
|
export type PermissionDecisionReason =
|
|
| {
|
|
type: 'rule'
|
|
rule: PermissionRule
|
|
}
|
|
| {
|
|
type: 'mode'
|
|
mode: PermissionMode
|
|
}
|
|
| {
|
|
type: 'subcommandResults'
|
|
reasons: Map<string, PermissionResult>
|
|
}
|
|
| {
|
|
type: 'permissionPromptTool'
|
|
permissionPromptToolName: string
|
|
toolResult: unknown
|
|
}
|
|
| {
|
|
type: 'hook'
|
|
hookName: string
|
|
hookSource?: string
|
|
reason?: string
|
|
}
|
|
| {
|
|
type: 'asyncAgent'
|
|
reason: string
|
|
}
|
|
| {
|
|
type: 'sandboxOverride'
|
|
reason: 'excludedCommand' | 'dangerouslyDisableSandbox'
|
|
}
|
|
| {
|
|
type: 'classifier'
|
|
classifier: string
|
|
reason: string
|
|
}
|
|
| {
|
|
type: 'workingDir'
|
|
reason: string
|
|
}
|
|
| {
|
|
type: 'safetyCheck'
|
|
reason: string
|
|
// When true, auto mode lets the classifier evaluate this instead of
|
|
// forcing a prompt. True for sensitive-file paths (.claude/, .git/,
|
|
// shell configs) — the classifier can see context and decide. False
|
|
// for Windows path bypass attempts and cross-machine bridge messages.
|
|
classifierApprovable: boolean
|
|
}
|
|
| {
|
|
type: 'other'
|
|
reason: string
|
|
}
|
|
|
|
// ============================================================================
|
|
// Bash Classifier Types
|
|
// ============================================================================
|
|
|
|
export type ClassifierResult = {
|
|
matches: boolean
|
|
matchedDescription?: string
|
|
confidence: 'high' | 'medium' | 'low'
|
|
reason: string
|
|
}
|
|
|
|
export type ClassifierBehavior = 'deny' | 'ask' | 'allow'
|
|
|
|
export type ClassifierUsage = {
|
|
inputTokens: number
|
|
outputTokens: number
|
|
cacheReadInputTokens: number
|
|
cacheCreationInputTokens: number
|
|
}
|
|
|
|
export type YoloClassifierResult = {
|
|
thinking?: string
|
|
shouldBlock: boolean
|
|
reason: string
|
|
unavailable?: boolean
|
|
/**
|
|
* API returned "prompt is too long" — the classifier transcript exceeded
|
|
* the context window. Deterministic (same transcript → same error), so
|
|
* callers should fall back to normal prompting rather than retry/fail-closed.
|
|
*/
|
|
transcriptTooLong?: boolean
|
|
/** The model used for this classifier call */
|
|
model: string
|
|
/** Token usage from the classifier API call (for overhead telemetry) */
|
|
usage?: ClassifierUsage
|
|
/** Duration of the classifier API call in ms */
|
|
durationMs?: number
|
|
/** Character lengths of the prompt components sent to the classifier */
|
|
promptLengths?: {
|
|
systemPrompt: number
|
|
toolCalls: number
|
|
userPrompts: number
|
|
}
|
|
/** Path where error prompts were dumped (only set when unavailable due to API error) */
|
|
errorDumpPath?: string
|
|
/** Which classifier stage produced the final decision (2-stage XML only) */
|
|
stage?: 'fast' | 'thinking'
|
|
/** Token usage from stage 1 (fast) when stage 2 was also run */
|
|
stage1Usage?: ClassifierUsage
|
|
/** Duration of stage 1 in ms when stage 2 was also run */
|
|
stage1DurationMs?: number
|
|
/**
|
|
* API request_id (req_xxx) for stage 1. Enables joining to server-side
|
|
* api_usage logs for cache-miss / routing attribution. Also used for the
|
|
* legacy 1-stage (tool_use) classifier — the single request goes here.
|
|
*/
|
|
stage1RequestId?: string
|
|
/**
|
|
* API message id (msg_xxx) for stage 1. Enables joining the
|
|
* tengu_auto_mode_decision analytics event to the classifier's actual
|
|
* prompt/completion in post-analysis.
|
|
*/
|
|
stage1MsgId?: string
|
|
/** Token usage from stage 2 (thinking) when stage 2 was run */
|
|
stage2Usage?: ClassifierUsage
|
|
/** Duration of stage 2 in ms when stage 2 was run */
|
|
stage2DurationMs?: number
|
|
/** API request_id for stage 2 (set whenever stage 2 ran) */
|
|
stage2RequestId?: string
|
|
/** API message id (msg_xxx) for stage 2 (set whenever stage 2 ran) */
|
|
stage2MsgId?: string
|
|
}
|
|
|
|
// ============================================================================
|
|
// Permission Explainer Types
|
|
// ============================================================================
|
|
|
|
export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH'
|
|
|
|
export type PermissionExplanation = {
|
|
riskLevel: RiskLevel
|
|
explanation: string
|
|
reasoning: string
|
|
risk: string
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tool Permission Context
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Mapping of permission rules by their source
|
|
*/
|
|
export type ToolPermissionRulesBySource = {
|
|
[T in PermissionRuleSource]?: string[]
|
|
}
|
|
|
|
/**
|
|
* Context needed for permission checking in tools
|
|
* Note: Uses a simplified DeepImmutable approximation for this types-only file
|
|
*/
|
|
export type ToolPermissionContext = {
|
|
readonly mode: PermissionMode
|
|
readonly additionalWorkingDirectories: ReadonlyMap<
|
|
string,
|
|
AdditionalWorkingDirectory
|
|
>
|
|
readonly alwaysAllowRules: ToolPermissionRulesBySource
|
|
readonly alwaysDenyRules: ToolPermissionRulesBySource
|
|
readonly alwaysAskRules: ToolPermissionRulesBySource
|
|
readonly isBypassPermissionsModeAvailable: boolean
|
|
readonly strippedDangerousRules?: ToolPermissionRulesBySource
|
|
readonly shouldAvoidPermissionPrompts?: boolean
|
|
readonly awaitAutomatedChecksBeforeDialog?: boolean
|
|
readonly prePlanMode?: PermissionMode
|
|
}
|