1
+import { randomBytes } from "crypto";
2
+import * as path from "path";
3
import * as vscode from "vscode";
4
import { AcpClient, PermissionRequest, PermissionResponse, SessionUpdate } from "./acp/client";
5
import { defaultAgentKey, listAgents, permissionMode, resolveAgent } from "./agents";
211
private async handlePermission(request: PermissionRequest): Promise<PermissionResponse> {
212
const mode = permissionMode();
213
const options = request.options ?? [];
212
- const allowOption = options.find((o) => /allow|yes|approve/i.test(o.name ?? o.optionId));
213
- const denyOption = options.find((o) => /deny|no|reject|cancel/i.test(o.name ?? o.optionId));
214
+ // Classify by the ACP `kind` field; the option *name* is agent-controlled
215
+ // free text ("Allow now" would otherwise match /no/), so it is only a
216
+ // word-bounded fallback for agents that omit `kind`.
217
+ const byKind = (kinds: string[]) => options.find((o) => o.kind !== undefined && kinds.includes(o.kind));
218
+ const byName = (re: RegExp) => options.find((o) => re.test(o.name ?? o.optionId));
219
+ const allowOption = byKind(["allow_once", "allow_always"]) ?? byName(/\b(allow|yes|approve)\b/i);
220
+ const denyOption = byKind(["reject_once", "reject_always"]) ?? byName(/\b(deny|no|reject|cancel)\b/i);
221
222
if (mode === "allow" && allowOption) {
223
return { outcome: { outcome: "selected", optionId: allowOption.optionId } };
263
return `The agent wants to ${what}. Allow?`;
264
}
265
259
- private async readFile(params: { path: string }): Promise<string> {
266
+ private async readFile(params: { path: string; line?: number; limit?: number }): Promise<string> {
267
const uri = this.resolveUri(params.path);
268
+ await this.ensureFsAccess(uri, "read");
269
const bytes = await vscode.workspace.fs.readFile(uri);
262
- return Buffer.from(bytes).toString("utf8");
270
+ let text = Buffer.from(bytes).toString("utf8");
271
+ const { line, limit } = params;
272
+ if (typeof line === "number" || typeof limit === "number") {
273
+ const lines = text.split("\n");
274
+ const start = typeof line === "number" ? Math.max(0, line - 1) : 0;
275
+ const end = typeof limit === "number" ? start + limit : lines.length;
276
+ text = lines.slice(start, end).join("\n");
277
+ }
278
+ return text;
279
}
280
281
private async writeFile(params: { path: string; content: string }): Promise<void> {
282
const uri = this.resolveUri(params.path);
283
+ await this.ensureFsAccess(uri, "write");
284
await vscode.workspace.fs.writeFile(uri, Buffer.from(params.content, "utf8"));
285
}
286
287
+ /**
288
+ * Confine the agent's fs/* channel to the workspace. These requests carry no
289
+ * ACP permission prompt of their own, so paths outside every workspace
290
+ * folder follow `sigit.permission.mode`: allow silently, deny outright, or
291
+ * ask the user.
292
+ */
293
+ private async ensureFsAccess(uri: vscode.Uri, action: "read" | "write"): Promise<void> {
294
+ if (this.isInWorkspace(uri)) {
295
+ return;
296
+ }
297
+ const mode = permissionMode();
298
+ if (mode === "allow") {
299
+ return;
300
+ }
301
+ if (mode === "prompt") {
302
+ const allow = "Allow";
303
+ const choice = await vscode.window.showWarningMessage(
304
+ `The agent wants to ${action} a file outside the workspace: ${uri.fsPath}`,
305
+ { modal: true },
306
+ allow
307
+ );
308
+ if (choice === allow) {
309
+ return;
310
+ }
311
+ }
312
+ throw new Error(`Access denied: ${action} outside the workspace (${uri.fsPath})`);
313
+ }
314
+
315
+ private isInWorkspace(uri: vscode.Uri): boolean {
316
+ const folders = vscode.workspace.workspaceFolders ?? [];
317
+ return folders.some((folder) => {
318
+ const rel = path.relative(folder.uri.fsPath, uri.fsPath);
319
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
320
+ });
321
+ }
322
+
323
private resolveUri(p: string): vscode.Uri {
324
if (p.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(p)) {
325
return vscode.Uri.file(p);
445
}
446
447
function getNonce(): string {
395
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
396
- let text = "";
397
- for (let i = 0; i < 32; i++) {
398
- text += chars.charAt(Math.floor(Math.random() * chars.length));
399
- }
400
- return text;
448
+ return randomBytes(16).toString("hex");
449
}