Files
letta-code/src/permissions/checker.ts
2025-10-24 21:19:24 -07:00

279 lines
7.6 KiB
TypeScript

// src/permissions/checker.ts
// Main permission checking logic
import { resolve } from "node:path";
import { cliPermissions } from "./cli";
import {
matchesBashPattern,
matchesFilePattern,
matchesToolPattern,
} from "./matcher";
import { permissionMode } from "./mode";
import { sessionPermissions } from "./session";
import type {
PermissionCheckResult,
PermissionDecision,
PermissionRules,
} from "./types";
/**
* Tools that don't require approval within working directory
*/
const WORKING_DIRECTORY_TOOLS = ["Read", "Glob", "Grep"];
/**
* Check permission for a tool execution.
*
* Decision logic:
* 1. Check deny rules from settings (first match wins) → DENY
* 2. Check CLI disallowedTools (--disallowedTools flag) → DENY
* 3. Check permission mode (--permission-mode flag) → ALLOW or DENY
* 4. Check CLI allowedTools (--allowedTools flag) → ALLOW
* 5. For Read/Glob/Grep within working directory → ALLOW
* 6. Check session allow rules (first match wins) → ALLOW
* 7. Check allow rules from settings (first match wins) → ALLOW
* 8. Check ask rules from settings (first match wins) → ASK
* 9. Fall back to default behavior for tool → ASK or ALLOW
*
* @param toolName - Name of the tool (e.g., "Read", "Bash", "Write")
* @param toolArgs - Tool arguments (contains file paths, commands, etc.)
* @param permissions - Loaded permission rules
* @param workingDirectory - Current working directory
*/
type ToolArgs = Record<string, unknown>;
export function checkPermission(
toolName: string,
toolArgs: ToolArgs,
permissions: PermissionRules,
workingDirectory: string = process.cwd(),
): PermissionCheckResult {
// Build permission query string
const query = buildPermissionQuery(toolName, toolArgs);
// Get session rules
const sessionRules = sessionPermissions.getRules();
// Check deny rules FIRST (highest priority - overrides everything including working directory)
if (permissions.deny) {
for (const pattern of permissions.deny) {
if (matchesPattern(toolName, query, pattern, workingDirectory)) {
return {
decision: "deny",
matchedRule: pattern,
reason: "Matched deny rule",
};
}
}
}
// Check CLI disallowedTools (second highest priority - overrides all allow rules)
const disallowedTools = cliPermissions.getDisallowedTools();
for (const pattern of disallowedTools) {
if (matchesPattern(toolName, query, pattern, workingDirectory)) {
return {
decision: "deny",
matchedRule: `${pattern} (CLI)`,
reason: "Matched --disallowedTools flag",
};
}
}
// Check permission mode (applies before CLI allow rules but after deny rules)
const modeOverride = permissionMode.checkModeOverride(toolName);
if (modeOverride) {
const currentMode = permissionMode.getMode();
return {
decision: modeOverride,
matchedRule: `${currentMode} mode`,
reason: `Permission mode: ${currentMode}`,
};
}
// Check CLI allowedTools (third priority - overrides settings but not deny rules)
const allowedTools = cliPermissions.getAllowedTools();
for (const pattern of allowedTools) {
if (matchesPattern(toolName, query, pattern, workingDirectory)) {
return {
decision: "allow",
matchedRule: `${pattern} (CLI)`,
reason: "Matched --allowedTools flag",
};
}
}
// After checking CLI overrides, check if Read/Glob/Grep within working directory
if (WORKING_DIRECTORY_TOOLS.includes(toolName)) {
const filePath = extractFilePath(toolArgs);
if (
filePath &&
isWithinAllowedDirectories(filePath, permissions, workingDirectory)
) {
return {
decision: "allow",
reason: "Within working directory",
};
}
}
// Check session allow rules (higher precedence than persisted allow)
if (sessionRules.allow) {
for (const pattern of sessionRules.allow) {
if (matchesPattern(toolName, query, pattern, workingDirectory)) {
return {
decision: "allow",
matchedRule: `${pattern} (session)`,
reason: "Matched session allow rule",
};
}
}
}
// Check persisted allow rules
if (permissions.allow) {
for (const pattern of permissions.allow) {
if (matchesPattern(toolName, query, pattern, workingDirectory)) {
return {
decision: "allow",
matchedRule: pattern,
reason: "Matched allow rule",
};
}
}
}
// Check ask rules
if (permissions.ask) {
for (const pattern of permissions.ask) {
if (matchesPattern(toolName, query, pattern, workingDirectory)) {
return {
decision: "ask",
matchedRule: pattern,
reason: "Matched ask rule",
};
}
}
}
// Fall back to tool defaults
return {
decision: getDefaultDecision(toolName),
reason: "Default behavior for tool",
};
}
/**
* Extract file path from tool arguments
*/
function extractFilePath(toolArgs: ToolArgs): string | null {
// Different tools use different parameter names
if (typeof toolArgs.file_path === "string" && toolArgs.file_path.length > 0) {
return toolArgs.file_path;
}
if (typeof toolArgs.path === "string" && toolArgs.path.length > 0) {
return toolArgs.path;
}
if (
typeof toolArgs.notebook_path === "string" &&
toolArgs.notebook_path.length > 0
) {
return toolArgs.notebook_path;
}
return null;
}
/**
* Check if file path is within allowed directories
* (working directory + additionalDirectories)
*/
function isWithinAllowedDirectories(
filePath: string,
permissions: PermissionRules,
workingDirectory: string,
): boolean {
const absolutePath = resolve(workingDirectory, filePath);
// Check if within working directory
if (absolutePath.startsWith(workingDirectory)) {
return true;
}
// Check additionalDirectories
if (permissions.additionalDirectories) {
for (const dir of permissions.additionalDirectories) {
const resolvedDir = resolve(workingDirectory, dir);
if (absolutePath.startsWith(resolvedDir)) {
return true;
}
}
}
return false;
}
/**
* Build permission query string for a tool execution
*/
function buildPermissionQuery(toolName: string, toolArgs: ToolArgs): string {
switch (toolName) {
case "Read":
case "Write":
case "Edit":
case "Glob":
case "Grep": {
// File tools: "ToolName(path/to/file)"
const filePath = extractFilePath(toolArgs);
return filePath ? `${toolName}(${filePath})` : toolName;
}
case "Bash": {
// Bash: "Bash(command with args)"
const command =
typeof toolArgs.command === "string" ? toolArgs.command : "";
return `Bash(${command})`;
}
default:
// Other tools: just the tool name
return toolName;
}
}
/**
* Check if query matches a permission pattern
*/
function matchesPattern(
toolName: string,
query: string,
pattern: string,
workingDirectory: string,
): boolean {
// File tools use glob matching
if (["Read", "Write", "Edit", "Glob", "Grep"].includes(toolName)) {
return matchesFilePattern(query, pattern, workingDirectory);
}
// Bash uses prefix matching
if (toolName === "Bash") {
return matchesBashPattern(query, pattern);
}
// Other tools use simple name matching
return matchesToolPattern(toolName, pattern);
}
/**
* Get default decision for a tool (when no rules match)
*/
function getDefaultDecision(toolName: string): PermissionDecision {
// Tools that default to auto-allow
const autoAllowTools = ["Read", "Glob", "Grep", "TodoWrite"];
if (autoAllowTools.includes(toolName)) {
return "allow";
}
// Everything else defaults to ask
return "ask";
}