feat: vendor permission system source

This commit is contained in:
云服务部-叶林立
2026-08-19 14:35:19 +08:00
parent 198584daf8
commit 410c50a3e5
809 changed files with 157793 additions and 139 deletions
@@ -0,0 +1,193 @@
import { classifyToolKind, isMcpCheck } from "./access-intent/tool-kind";
import type { ToolInputFormatterLookup } from "./tool-input-formatter-registry";
import {
serializeRedactedToolInputPreview,
serializeToolInputPreview,
TOOL_INPUT_PREVIEW_MAX_LENGTH,
TOOL_TEXT_SUMMARY_MAX_LENGTH,
truncateInlineText,
} from "./tool-input-preview";
import {
formatEditInputForPrompt,
formatReadInputForPrompt,
formatWriteInputForPrompt,
getPromptPath,
} from "./tool-input-prompt-formatters";
import type { PermissionCheckResult } from "./types";
import { getNonEmptyString, toRecord } from "./value-guards";
export interface ToolPreviewFormatterOptions {
toolInputPreviewMaxLength: number;
toolTextSummaryMaxLength: number;
}
/**
* The built-in `ToolPreviewFormatterOptions`.
*
* Takes no config: `toolInputPreviewMaxLength` and `toolTextSummaryMaxLength`
* are subsumed by the renderer budgets (`promptMaxRows` / `promptFieldMaxWidth`,
* ADR 0011 §5), so an operator's values no longer take effect. The constants
* remain because they still shape a *prompt* preview; what the review log
* persists is bounded by `reviewLogFieldMaxWidth` at the writer instead.
*/
export function resolveToolPreviewLimits(): ToolPreviewFormatterOptions {
return {
toolInputPreviewMaxLength: TOOL_INPUT_PREVIEW_MAX_LENGTH,
toolTextSummaryMaxLength: TOOL_TEXT_SUMMARY_MAX_LENGTH,
};
}
/**
* Formats tool inputs for permission prompts and review logs.
*
* Accepts configurable limits in its constructor — the single injection
* point for preview-length configuration (#266).
*/
export class ToolPreviewFormatter {
constructor(
private readonly options: ToolPreviewFormatterOptions,
private readonly customFormatters?: ToolInputFormatterLookup,
) {}
// ── Prompt formatting ───────────────────────────────────────────────────
/**
* Collapse whitespace, trim, and truncate a string to fit inline.
* An explicit `maxLength` overrides the constructor default.
*/
sanitizeInlineText(value: string, maxLength?: number): string {
const limit = maxLength ?? this.options.toolTextSummaryMaxLength;
const normalized = value.replace(/\s+/g, " ").trim();
return normalized ? truncateInlineText(normalized, limit) : "empty text";
}
/** Serialize `input` to inline JSON and truncate at `toolInputPreviewMaxLength`. */
formatJsonInputForPrompt(input: unknown): string {
const inline = serializeToolInputPreview(input);
return inline
? `with input ${truncateInlineText(inline, this.options.toolInputPreviewMaxLength)}`
: "";
}
/** Format search-tool (grep/find/ls) input for a permission prompt. */
formatSearchInputForPrompt(
toolName: string,
input: Record<string, unknown>,
): string {
const parts: string[] = [];
const path = getPromptPath(input);
const pattern = getNonEmptyString(input.pattern);
const glob = getNonEmptyString(input.glob);
if (pattern) {
parts.push(`pattern '${this.sanitizeInlineText(pattern)}'`);
}
if (glob) {
parts.push(`glob '${this.sanitizeInlineText(glob)}'`);
}
if (path) {
parts.push(`path '${path}'`);
} else if (
toolName === "find" ||
toolName === "grep" ||
toolName === "ls"
) {
parts.push("current working directory");
}
return parts.length > 0 ? `for ${parts.join(", ")}` : "";
}
/**
* Format any tool input for display in a permission ask-prompt.
*
* Dispatches to the appropriate pure formatter for known tools
* and falls back to inline JSON for everything else.
*/
formatToolInputForPrompt(toolName: string, input: unknown): string {
const inputRecord = toRecord(input);
const custom = this.customFormatters?.get(toolName);
if (custom) {
const rendered = custom(inputRecord);
if (rendered !== undefined) {
return rendered;
}
}
switch (toolName) {
case "edit":
return formatEditInputForPrompt(inputRecord);
case "write":
return formatWriteInputForPrompt(inputRecord);
case "read":
return formatReadInputForPrompt(inputRecord);
case "find":
case "grep":
case "ls":
return this.formatSearchInputForPrompt(toolName, inputRecord);
case "mcp":
// The MCP target is already a request fact on the prompt payload.
// When no custom formatter is registered (or it declines), produce no
// additional preview rather than leaking the raw event JSON.
return "";
default:
return this.formatJsonInputForPrompt(input);
}
}
// ── Log formatting ──────────────────────────────────────────────────────
/**
* Serialize `input` to inline JSON for the review log, masking
* sensitive-keyed values.
*
* Unbounded here: the writer narrows every field it persists to
* `reviewLogFieldMaxWidth`, so a second bound at the producer would be a
* limit the operator cannot see or change.
*/
formatGenericToolInputForLog(input: unknown): string | undefined {
const inline = serializeRedactedToolInputPreview(input);
return inline ? `input ${inline}` : undefined;
}
/** Derive a loggable input preview string for the review log. */
getToolInputPreviewForLog(
result: PermissionCheckResult,
input: unknown,
pathBearingTools: ReadonlySet<string>,
): string | undefined {
if (classifyToolKind(result.toolName) === "bash" || isMcpCheck(result)) {
return undefined;
}
if (pathBearingTools.has(result.toolName)) {
return this.formatToolInputForPrompt(result.toolName, input) || undefined;
}
return this.formatGenericToolInputForLog(input);
}
/** Build the structured log context object for a permission review log entry. */
getPermissionLogContext(
result: PermissionCheckResult,
input: unknown,
pathBearingTools: ReadonlySet<string>,
): {
command?: string;
target?: string;
toolInputPreview?: string;
origin?: string;
} {
return {
command: result.command,
target: result.target,
toolInputPreview: this.getToolInputPreviewForLog(
result,
input,
pathBearingTools,
),
origin: result.origin,
};
}
}