/** * Verification control-plane policy -- deterministic gate that prevents * non-guardian and unverified_channel actors from invoking channel * verification endpoints conversationally via tools. * * Protected endpoints: * /v1/channel-verification-sessions * /v1/channel-verification-sessions/resend * /v1/channel-verification-sessions/status * /v1/channel-verification-sessions/revoke */ import { resolveCapabilities } from "../runtime/capabilities.js"; const VERIFICATION_ENDPOINT_PATHS = [ "/v1/channel-verification-sessions", "/v1/channel-verification-sessions/resend", "/v1/channel-verification-sessions/status", "/v1/channel-verification-sessions/revoke", ] as const; /** * Broad regex that catches any path targeting the verification control-plane, * even if the exact sub-path differs from the hardcoded list above. * Anchored on a path separator so it won't match inside unrelated words. */ const VERIFICATION_PATH_REGEX = /\/v1\/channel-verification-sessions/; /** Tools whose `input.command` (string) may contain verification endpoint paths. */ const COMMAND_TOOLS = new Set(["bash", "host_bash"]); /** Tools whose `input.url` (string) may contain verification endpoint paths. */ const URL_TOOLS = new Set(["network_request", "web_fetch"]); /** * Normalize a string to defeat common URL obfuscation techniques before matching: * - Decode percent-encoded characters (e.g. %2F → /) * - Collapse consecutive slashes into a single slash (preserving protocol://) * - Lowercase everything */ function normalizeForMatching(value: string): string { let normalized = value; // Iteratively decode percent-encoding to handle double-encoding (%252F → %2F → /) // Use per-sequence replacement instead of decodeURIComponent to avoid a single // malformed sequence (e.g. %ZZ) preventing all other valid sequences from decoding. let prev = ""; while (prev !== normalized) { prev = normalized; normalized = normalized.replace(/%[0-9a-fA-F]{2}/g, (match) => { try { return decodeURIComponent(match); } catch { return match; } }); } // Collapse consecutive slashes (but preserve the double slash in protocol e.g. https://) normalized = normalized.replace(/(?, ): boolean { if (COMMAND_TOOLS.has(toolName)) { const command = input.command; if (typeof command === "string") { // Primary: exact/normalized path matching if (containsVerificationEndpointPath(command)) { return true; } // Fallback: detect shell-expanded construction of verification paths if (containsVerificationFragments(command)) { return true; } } } if (URL_TOOLS.has(toolName)) { const url = input.url; if (typeof url === "string" && containsVerificationEndpointPath(url)) { return true; } } return false; } /** * Enforce the verification control-plane policy: if the invocation targets a * verification control-plane endpoint and the actor is not a guardian, deny. */ export function enforceVerificationControlPlanePolicy( toolName: string, input: Record, trustClass: string, ): { denied: boolean; reason?: string } { if (!isVerificationControlPlaneInvocation(toolName, input)) { return { denied: false }; } if (resolveCapabilities(trustClass).canUseVerificationControlPlane) { return { denied: false }; } return { denied: true, reason: "Guardian verification control-plane actions are restricted to guardian users. This is a security restriction \u2014 please wait for the designated guardian to perform this action.", }; }