Files
my-pi/pi-permission-system/src/pattern-suggest.ts
T

162 lines
5.4 KiB
TypeScript

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) };
}