/** * Plugin LLM facade — trust-gating, credential redaction, and full sandboxing. * * Exposes {@link pluginLlmComplete} as the single entry point for plugins * that need LLM access. Before dispatching, the facade: * 1. Resolves the plugin manifest to obtain its access constraints. * 2. Validates the resolved role config against `allowedModels` / * `allowedProviders` allow-lists and throws {@link PluginModelGateError} * on denial. * 3. Validates the request against the plugin's {@link PluginPermissionDescriptor} * (max tokens, allowed roles). Throws {@link PluginDeniedError} on violation. * 4. Checks the in-memory token-bucket rate limiter. Throws * {@link PluginRateLimitedError} with `retryAfterMs` when exhausted. * 5. Delegates to `getLlmExecutor('plugin').auxiliary(...)`. * 6. On error: redacts credential patterns from message + stack before * re-throwing as {@link PluginLlmError}. * * Filesystem ACL is enforced via {@link validateFsAccess} — call this before * executing any file_read / file_write tool requested by a plugin. Returns * `true` when the path is within the plugin's declared glob patterns. * * @module llm/plugin-facade * @task T9305 * @task T9313 * @epic T9261 T-LLM-CRED-CENTRALIZATION * @see ADR-072 §6 plugin facade */ import type { SendOptions } from '@cleocode/contracts/llm/interfaces.js'; import type { NormalizedResponse, TransportMessage } from '@cleocode/contracts/llm/normalized-response.js'; /** * Filesystem access control list for a plugin. * * Default deny — the plugin MUST explicitly declare scope via `read` / `write` * glob patterns. Patterns are matched against the path using Node's built-in * `path.matchesGlob` (`**` crosses directory boundaries). * * @example * ```ts * { read: ['/tmp/plugin-scratch/**'], write: ['/tmp/plugin-scratch/**'] } * ``` */ export interface PluginFsAcl { /** Glob patterns the plugin is allowed to read from. */ readonly read: readonly string[]; /** Glob patterns the plugin is allowed to write to. */ readonly write: readonly string[]; } /** * Per-plugin rate-limit configuration for the in-memory token-bucket. * * The bucket holds up to `burst` tokens. `tokensPerSecond` tokens are added * each second (fractionally). Each LLM call costs one token. When the bucket * is empty the call is rejected with {@link PluginRateLimitedError}. */ export interface PluginRateLimitConfig { /** * Sustained token refill rate (tokens per second). * Must be positive. Example: `2` allows 2 calls/s sustained. */ readonly tokensPerSecond: number; /** * Maximum burst capacity (tokens). * Must be ≥ 1. The bucket starts full at this value. */ readonly burst: number; } /** * Permission descriptor for a plugin manifest entry (Phase 5). * * All fields are optional — omitted fields are unconstrained (except * `fsAccess` which defaults to full deny when omitted). */ export interface PluginPermissionDescriptor { /** * Maximum number of output tokens allowed per call. * Rejected with {@link PluginDeniedError} when `opts.maxTokens` exceeds this. */ readonly maxTokens?: number; /** * Allow-list of CLEO role names the plugin may invoke under. * Empty or omitted = unconstrained. */ readonly allowedRoles?: readonly string[]; /** * Filesystem access control list. * Defaults to fully-denied when omitted (`{ read: [], write: [] }`). */ readonly fsAccess?: PluginFsAcl; /** In-memory token-bucket rate-limit configuration. */ readonly rateLimit?: PluginRateLimitConfig; } /** * Minimal plugin manifest entry used by the facade for access-control checks. * * The full manifest schema lives in the plugin registry (future work). This * shape is intentionally minimal — only the fields the facade reads. */ export interface PluginManifestEntry { /** Unique plugin identifier. */ readonly pluginId: string; /** Allow-list of model identifiers this plugin may call. Empty = unconstrained. */ readonly allowedModels: readonly string[]; /** Allow-list of provider identifiers this plugin may use. Empty = unconstrained. */ readonly allowedProviders: readonly string[]; /** Phase 5 permission descriptor (optional). */ readonly permissions?: PluginPermissionDescriptor; } /** * Register a plugin manifest entry in the in-process registry. * * Overwrites any existing entry for the same `pluginId` and resets that * plugin's rate-limit bucket (config may have changed). * * @param entry - The plugin manifest entry to register. */ export declare function registerPluginManifest(entry: PluginManifestEntry): void; /** * Clear all plugin manifest entries from the in-process registry. * * Also resets all rate-limit buckets. Useful in test teardown to reset * inter-test state. */ export declare function clearPluginRegistry(): void; /** * Validate whether a plugin may access a filesystem path for the given operation. * * Default deny — the plugin must explicitly declare the path via glob patterns * in its `permissions.fsAccess` descriptor. An empty glob list means no access * on that axis. * * Patterns are matched using Node's built-in `path.matchesGlob` (Node ≥ 22). * * @param pluginId - Plugin requesting filesystem access. * @param path - Filesystem path being accessed. * @param operation - `'read'` or `'write'`. * @returns `true` when the path matches at least one declared pattern; `false` otherwise. * * @example * ```ts * // Returns true when '/tmp/scratch/out.txt' matches '/tmp/scratch/**' * validateFsAccess('my-plugin', '/tmp/scratch/out.txt', 'write'); * ``` */ export declare function validateFsAccess(pluginId: string, path: string, operation: 'read' | 'write'): boolean; /** * Execute a single-turn LLM completion on behalf of a plugin. * * This is the sole sanctioned LLM entry point for plugin code. It enforces * five gates in order before dispatching: * 1. Model allow-list — {@link PluginModelGateError} on violation. * 2. Provider allow-list — {@link PluginModelGateError} on violation. * 3. Permission descriptor (maxTokens, allowedRoles) — {@link PluginDeniedError}. * 4. Token-bucket rate limit — {@link PluginRateLimitedError} with `retryAfterMs`. * 5. Filesystem ACL enforcement — when `opts.toolCall` is a file tool, auto-validates * the path against the plugin's `permissions.fsAccess` allow-list * ({@link PluginDeniedError} on denial). Pass `toolCall` whenever you are * about to dispatch a `file_read` / `file_write` tool on behalf of the plugin. * * All executor errors are caught and re-thrown as {@link PluginLlmError} with * credentials redacted from the message and stack. * * @param pluginId - Unique identifier of the calling plugin. * @param messages - Conversation messages in provider-neutral form. * @param opts - Optional per-call send options forwarded to the executor. * @returns A promise resolving to a {@link NormalizedResponse}. * @throws {@link PluginModelGateError} when the requested model/provider is * not in the plugin's allow-list. * @throws {@link PluginDeniedError} when the request violates the plugin's * permission descriptor (`maxTokens` cap, `allowedRoles`, or filesystem ACL). * @throws {@link PluginRateLimitedError} when the plugin's token bucket is * exhausted. Check `err.retryAfterMs` for the backoff duration. * @throws {@link PluginLlmError} for all other executor failures (credentials * are redacted from the error message before re-throwing). * * @example * ```ts * // Enforce fs-ACL when a plugin requests a file_read tool: * await pluginLlmComplete('my-plugin', messages, { * toolCall: { name: 'file_read', path: '/tmp/scratch/data.json' }, * }); * ``` */ export declare function pluginLlmComplete(pluginId: string, messages: TransportMessage[], opts?: SendOptions & { /** Model override — validated against the plugin's allowedModels list. */ readonly model?: string; /** Provider override — validated against the plugin's allowedProviders list. */ readonly provider?: string; /** Max output tokens — validated against the plugin's permissions.maxTokens cap. */ readonly maxTokens?: number; /** Role name — validated against the plugin's permissions.allowedRoles list. */ readonly role?: string; /** * Filesystem tool invocation to validate before dispatch (Gate 5, T9313). * * When supplied and the tool name is a recognised file-access tool * (`file_read`, `file_write`, etc.), the path is automatically validated * against the plugin's `permissions.fsAccess` allow-list. * Throws {@link PluginDeniedError} when denied. */ readonly toolCall?: { readonly name: string; readonly path: string; }; }): Promise; //# sourceMappingURL=plugin-facade.d.ts.map