Refactor runtime around isolated bot instances
This commit is contained in:
+32
-14
@@ -2,11 +2,11 @@ import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import * as acp from "@agentclientprotocol/sdk";
|
||||
import type { AgentCapabilities, InitializeResponse, RequestPermissionRequest, RequestPermissionResponse, SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import type { RolePolicy } from "../config.js";
|
||||
import type { PermissionPolicy } from "../config.js";
|
||||
|
||||
export interface AcpClientOptions {
|
||||
initializeTimeoutMs: number;
|
||||
policy: RolePolicy;
|
||||
policy: PermissionPolicy;
|
||||
}
|
||||
|
||||
export class AcpClient {
|
||||
@@ -48,7 +48,12 @@ export class AcpClient {
|
||||
this.collecting = false;
|
||||
this.chunks = [];
|
||||
if (this.capabilities.sessionCapabilities?.resume) {
|
||||
await this.connection.agent.request(acp.methods.agent.session.resume, { sessionId, cwd, mcpServers: [] });
|
||||
try {
|
||||
await this.connection.agent.request(acp.methods.agent.session.resume, { sessionId, cwd, mcpServers: [] });
|
||||
} catch (error) {
|
||||
if (!this.capabilities.loadSession) throw error;
|
||||
await this.connection.agent.request(acp.methods.agent.session.load, { sessionId, cwd, mcpServers: [] });
|
||||
}
|
||||
} else if (this.capabilities.loadSession) {
|
||||
await this.connection.agent.request(acp.methods.agent.session.load, { sessionId, cwd, mcpServers: [] });
|
||||
} else {
|
||||
@@ -92,24 +97,37 @@ export class AcpClient {
|
||||
}
|
||||
}
|
||||
|
||||
export function decidePermission(request: RequestPermissionRequest, policy: RolePolicy): RequestPermissionResponse {
|
||||
if (policy.permissionMode === "deny") return reject(request);
|
||||
export function decidePermission(request: RequestPermissionRequest, policy: PermissionPolicy): RequestPermissionResponse {
|
||||
if (policy.mode === "deny") return reject(request);
|
||||
const allowOption = request.options.find((option) => option.kind === "allow_once") || request.options.find((option) => option.kind === "allow_always");
|
||||
if (!allowOption) return reject(request);
|
||||
if (policy.permissionMode === "auto") return { outcome: { outcome: "selected", optionId: allowOption.optionId } };
|
||||
if (policy.mode === "auto") return { outcome: { outcome: "selected", optionId: allowOption.optionId } };
|
||||
|
||||
const name = String(request.toolCall.name || request.toolCall.kind || "").toLowerCase();
|
||||
const title = String(request.toolCall.title || "").toLowerCase();
|
||||
const allowedTool = policy.allowedTools.some((tool) => name === tool.toLowerCase() || title.startsWith(tool.toLowerCase()));
|
||||
if (!allowedTool) return reject(request);
|
||||
if (name === "bash" || name === "terminal" || title.startsWith("bash") || title.startsWith("terminal")) {
|
||||
if (request.toolCall.rawInput === undefined || policy.allowedCommandPatterns.length === 0) return reject(request);
|
||||
const input = typeof request.toolCall.rawInput === "string" ? request.toolCall.rawInput : JSON.stringify(request.toolCall.rawInput);
|
||||
if (!policy.allowedCommandPatterns.some((pattern) => new RegExp(pattern).test(input))) return reject(request);
|
||||
const name = typeof request.toolCall.name === "string" ? request.toolCall.name.trim().toLowerCase() : "";
|
||||
if (!name || !policy.allowedTools.some((tool) => name === tool.trim().toLowerCase())) return reject(request);
|
||||
if (name === "bash" || name === "terminal") {
|
||||
if (policy.allowedCommandPatterns.length === 0) return reject(request);
|
||||
const input = commandInput(request.toolCall.rawInput);
|
||||
if (!input || !policy.allowedCommandPatterns.some((pattern) => fullMatch(pattern, input))) return reject(request);
|
||||
}
|
||||
return { outcome: { outcome: "selected", optionId: allowOption.optionId } };
|
||||
}
|
||||
|
||||
function commandInput(rawInput: unknown): string | undefined {
|
||||
if (typeof rawInput === "string") return rawInput;
|
||||
if (typeof rawInput === "object" && rawInput !== null && !Array.isArray(rawInput)) {
|
||||
const record = rawInput as Record<string, unknown>;
|
||||
if (Object.keys(record).some((key) => !["command", "timeout", "timeoutMs"].includes(key))) return undefined;
|
||||
return typeof record.command === "string" ? record.command : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function fullMatch(pattern: string, input: string): boolean {
|
||||
const match = new RegExp(pattern).exec(input);
|
||||
return match?.index === 0 && match[0] === input;
|
||||
}
|
||||
|
||||
function reject(request: RequestPermissionRequest): RequestPermissionResponse {
|
||||
const option = request.options.find((item) => item.kind === "reject_once") || request.options.find((item) => item.kind === "reject_always");
|
||||
return option ? { outcome: { outcome: "selected", optionId: option.optionId } } : { outcome: { outcome: "cancelled" } };
|
||||
|
||||
Reference in New Issue
Block a user