// v1.13.17-cross-repo-reads: tool the model uses to request read access to // a path outside its session's primary project root. When the model emits // view_file("/opt/forks/foo/go.mod") under a session scoped to /opt/boocode, // pathGuard's error message hints at this tool. The model then emits // request_read_access(path="/opt/forks/foo/go.mod", // reason="investigating foo to write the design doc") // The tool's execute does cheap up-front validation: if the requested path // can't possibly be granted under the current whitelist + repo-shape rules, // it returns a denial immediately without prompting the user. Otherwise, the // tool-phase pause branch (parallel of ask_user_input) stores a pending // sentinel and waits for the user's allow/deny via the grant_read_access // endpoint. // // The execute body never directly mutates state; the grant endpoint owns // the persistence path. This keeps the tool-side logic side-effect-free // (it's just a request) and matches ask_user_input's "server-side no-op // fallback, pause happens in tool-phase" shape. import { z } from 'zod'; import type { ToolDef } from './tools.js'; const RequestReadAccessInput = z.object({ path: z.string().min(1), reason: z.string().min(1).max(500), }); type RequestReadAccessInputT = z.infer; export const requestReadAccess: ToolDef = { name: 'request_read_access', description: "Ask the user for read-only access to a path outside the current " + "session's project scope. Use when a previous read tool (view_file, " + 'list_dir, grep, find_files) was refused with a path-escapes-project ' + 'error and the path is plausibly under another known repository (e.g. ' + '/opt/forks/foo). Provide a short reason describing why you need the ' + "access. Pauses the conversation until the user picks Allow or Deny; " + 'the next assistant turn sees the result. On Allow, the tool result ' + 'is "granted: " — subsequent reads under that root succeed for ' + 'the rest of the session. On Deny, the tool result is "denied". Do ' + 'not call this for paths that are already inside the project root.', inputSchema: RequestReadAccessInput, jsonSchema: { type: 'function', function: { name: 'request_read_access', description: "Ask the user for read-only access to a path outside the session's " + 'project scope. Pauses the conversation until the user picks Allow ' + 'or Deny. Subsequent reads under the granted root succeed for the ' + 'rest of the session.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Absolute path the model wants to read. Must be under the ' + "server's PROJECT_ROOT_WHITELIST (default /opt) and outside " + "the session's primary project root.", }, reason: { type: 'string', description: 'Short rationale (<=500 chars) shown to the user explaining ' + 'why the access is needed. The user uses this to decide.', }, }, required: ['path', 'reason'], additionalProperties: false, }, }, }, // Server-side no-op. The "execution" of request_read_access is the // pause-and-resume cycle managed by tool-phase.ts + the grant endpoint. // The inference loop catches this tool name BEFORE executeToolCall fires // and inserts a pending sentinel instead — this fallback only runs if // something bypasses that branch, in which case we surface the pending // shape so downstream code can still detect it. Mirrors ask_user_input. async execute(input) { return { _pending: true, path: input.path, reason: input.reason }; }, };