import { PATH_BEARING_TOOLS } from "./access-intent/path-surfaces"; import { prefix, stripBashCommentLines } from "./bash-arity"; import { deriveApprovalPattern } from "./session-rules"; /** The suggestion returned for a "Yes, for this session" dialog option. */ export interface SessionApprovalSuggestion { /** The permission surface this approval applies to. */ surface: string; /** The wildcard pattern to store as a session rule. */ pattern: string; /** Human-readable label for the "for session" dialog option. */ label: string; } /** * Suggest a bash session-approval pattern from a command string. * * Uses the arity table (`src/bash-arity.ts`) to identify the semantically * meaningful prefix tokens for the command, then produces a wildcard pattern: * * - Single bare token (no args): exact command (`ls`). * - Arity prefix covers all tokens: trailing wildcard (`npm run build*`). * - Arity prefix shorter than token list: space + wildcard (`git checkout *`). * - Unknown command: first token + space wildcard (`mytool *`). */ export function suggestBashPattern(command: string): string { const trimmed = command.trim(); if (!trimmed) return ""; // Strip leading shell comment lines so the suggestion is based on the // actual command, not a `# description` prefix agents often prepend. const stripped = stripBashCommentLines(trimmed); if (!stripped) return ""; const tokens = stripped.split(/\s+/); if (tokens.length === 1) return stripped; const meaningful = prefix(tokens); if (meaningful.length >= tokens.length) { return `${stripped}*`; } return `${meaningful.join(" ")} *`; } /** * Suggest an MCP session-approval pattern from a resolved target string. * * - Qualified target (`server:tool`) → `server:*` * - Munged target (`server_tool`) → `server_*` * - Bare target (no separator) → `*` */ export function suggestMcpPattern(target: string): string { const trimmed = target.trim(); const colonIndex = trimmed.indexOf(":"); if (colonIndex > 0) { return `${trimmed.slice(0, colonIndex)}:*`; } const underscoreIndex = trimmed.indexOf("_"); if (underscoreIndex > 0) { return `${trimmed.slice(0, underscoreIndex)}_*`; } return "*"; } /** Scope labels for the forwarded-approval two-step scope select. */ export interface ForwardedScopeLabels { /** Least-privilege default: record on the requesting subagent only. */ subagentLabel: string; /** Record on the serving node — covers the parent and all subagents. */ servingSessionLabel: string; } /** * Build the two scope labels shown when a human grants a forwarded request * "for this session." * * The subagent option names the requester (least privilege); the whole-session * option restates the surface + pattern being granted session-wide. */ export function buildForwardedScopeLabels( agentName: string | null, surface: string, pattern: string, ): ForwardedScopeLabels { const subagentLabel = agentName ? `This subagent ('${agentName}') only` : "This subagent only"; return { subagentLabel, servingSessionLabel: `The whole session — allow ${surface} "${pattern}" for parent and all subagents`, }; } /** Surface-aware human-readable labels for the session-approval option. */ function buildLabel(pattern: string, surface: string): string { switch (surface) { case "bash": return `Yes, allow bash "${pattern}" for this session`; case "mcp": return `Yes, allow mcp tool "${pattern}" for this session`; case "skill": return `Yes, allow skill "${pattern}" for this session`; case "external_directory": return `Yes, allow access to external directory "${pattern}" for this session`; case "path": return `Yes, allow path "${pattern}" for this session`; default: // Path-bearing tools with a specific path pattern show the pattern. if (PATH_BEARING_TOOLS.has(surface) && pattern !== "*") { return `Yes, allow ${surface} "${pattern}" for this session`; } // Tool surfaces with catch-all or extension tools. return `Yes, allow tool "${surface}" for this session`; } } /** * Suggest a session-approval pattern for the given permission surface and value. * * Returns a `SessionApprovalSuggestion` with the surface, the wildcard pattern * to store in `SessionRules`, and a human-readable dialog label. * * `value` is expected to be the canonical (cwd-resolved, absolute) path for * path surfaces — callers resolve it before suggesting, so the derived pattern * matches the policy values a later tool call produces. */ export function suggestSessionPattern( surface: string, value: string, ): SessionApprovalSuggestion { let pattern: string; switch (surface) { case "bash": pattern = suggestBashPattern(value); break; case "mcp": pattern = suggestMcpPattern(value); break; case "skill": pattern = value; break; case "external_directory": pattern = deriveApprovalPattern(value); break; case "path": pattern = deriveApprovalPattern(value); break; default: // Path-bearing tools: derive a directory-scoped pattern from the path. if (PATH_BEARING_TOOLS.has(surface) && value !== "*") { pattern = deriveApprovalPattern(value); break; } // Extension tools / fallback. pattern = "*"; break; } return { surface, pattern, label: buildLabel(pattern, surface) }; }