Skip to content

Commit 6669a38

Browse files
authored
feat: increase base permissions for agents (#665)
- Defined a set of base tools that are always allowed (`TodoWrite`, `Glob`, `Grep`, etc.) - Implemented mode-specific tool permissions (e.g., `acceptEdits` mode allows `Edit`, `Write`, and `NotebookEdit` tools) - Also make the MCP tools check for these permissions This needs a bigger refactor so the flow of data for toolcalls/MCP tools is more clear, but this gets the basic concepts in
1 parent 2414593 commit 6669a38

File tree

6 files changed

+352
-264
lines changed

6 files changed

+352
-264
lines changed

apps/twig/src/renderer/features/sessions/stores/sessionStore.ts

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -276,31 +276,6 @@ function subscribeToChannel(taskRunId: string) {
276276
draft.sessions[taskRunId].pendingPermissions = newPermissions;
277277
}
278278
});
279-
280-
const auth = useAuthStore.getState();
281-
if (auth.client && session.taskId) {
282-
const storedEntry: StoredLogEntry = {
283-
type: "notification",
284-
timestamp: new Date().toISOString(),
285-
notification: {
286-
method: "_array/permission_request",
287-
params: payload,
288-
},
289-
};
290-
try {
291-
await auth.client.appendTaskRunLog(session.taskId, taskRunId, [
292-
storedEntry,
293-
]);
294-
log.info("Permission request persisted to logs", {
295-
taskRunId,
296-
toolCallId: payload.toolCall.toolCallId,
297-
});
298-
} catch (error) {
299-
log.warn("Failed to persist permission request to logs", {
300-
error,
301-
});
302-
}
303-
}
304279
} else {
305280
log.warn("Session not found for permission request", {
306281
taskRunId,
@@ -1291,36 +1266,6 @@ const useStore = create<SessionStore>()(
12911266
optionId,
12921267
hasCustomInput: !!customInput,
12931268
});
1294-
1295-
// Persist permission response to logs for recovery tracking
1296-
const auth = useAuthStore.getState();
1297-
if (auth.client) {
1298-
const storedEntry: StoredLogEntry = {
1299-
type: "notification",
1300-
timestamp: new Date().toISOString(),
1301-
notification: {
1302-
method: "_array/permission_response",
1303-
params: {
1304-
toolCallId,
1305-
optionId,
1306-
...(customInput && { customInput }),
1307-
},
1308-
},
1309-
};
1310-
try {
1311-
await auth.client.appendTaskRunLog(taskId, session.taskRunId, [
1312-
storedEntry,
1313-
]);
1314-
log.info("Permission response persisted to logs", {
1315-
taskId,
1316-
toolCallId,
1317-
});
1318-
} catch (persistError) {
1319-
log.warn("Failed to persist permission response to logs", {
1320-
error: persistError,
1321-
});
1322-
}
1323-
}
13241269
} catch (error) {
13251270
log.error("Failed to respond to permission", {
13261271
taskId,
@@ -1362,36 +1307,6 @@ const useStore = create<SessionStore>()(
13621307
});
13631308

13641309
log.info("Permission cancelled", { taskId, toolCallId });
1365-
1366-
// Persist permission cancellation to logs for recovery tracking
1367-
const auth = useAuthStore.getState();
1368-
if (auth.client) {
1369-
const storedEntry: StoredLogEntry = {
1370-
type: "notification",
1371-
timestamp: new Date().toISOString(),
1372-
notification: {
1373-
// TODO: Migrate to twig
1374-
method: "_array/permission_response",
1375-
params: {
1376-
toolCallId,
1377-
optionId: "_cancelled",
1378-
},
1379-
},
1380-
};
1381-
try {
1382-
await auth.client.appendTaskRunLog(taskId, session.taskRunId, [
1383-
storedEntry,
1384-
]);
1385-
log.info("Permission cancellation persisted to logs", {
1386-
taskId,
1387-
toolCallId,
1388-
});
1389-
} catch (persistError) {
1390-
log.warn("Failed to persist permission cancellation to logs", {
1391-
error: persistError,
1392-
});
1393-
}
1394-
}
13951310
} catch (error) {
13961311
log.error("Failed to cancel permission", {
13971312
taskId,

apps/twig/src/renderer/features/sessions/utils/parseSessionLogs.ts

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/// <reference path="../../../types/electron.d.ts" />
22

3-
import type {
4-
RequestPermissionRequest,
5-
SessionNotification,
3+
import {
4+
CLIENT_METHODS,
5+
type RequestPermissionRequest,
6+
type SessionNotification,
67
} from "@agentclientprotocol/sdk";
78
import { trpcVanilla } from "@/renderer/trpc";
89

@@ -108,44 +109,85 @@ export type PermissionRequest = RequestPermissionRequest & {
108109
receivedAt: number;
109110
};
110111

112+
type SessionUpdate = {
113+
sessionUpdate?: string;
114+
toolCallId?: string;
115+
status?: string;
116+
};
117+
118+
type NotificationMsg = StoredLogEntry["notification"];
119+
120+
function getSessionUpdate(msg: NotificationMsg): SessionUpdate | null {
121+
if (msg?.method !== "session/update") return null;
122+
return (msg.params as { update?: SessionUpdate })?.update ?? null;
123+
}
124+
125+
function getPermissionToolCallId(msg: NotificationMsg): string | null {
126+
if (msg?.method !== CLIENT_METHODS.session_request_permission) return null;
127+
return (msg.params as RequestPermissionRequest)?.toolCall?.toolCallId ?? null;
128+
}
129+
130+
function isTerminalStatus(status?: string): boolean {
131+
return (
132+
status === "in_progress" || status === "completed" || status === "failed"
133+
);
134+
}
135+
111136
/**
112137
* Scan log entries to find pending permission requests.
113-
* Returns permission requests that don't have a matching response.
138+
* A permission is pending if:
139+
* 1. We have a session/request_permission for a toolCallId
140+
* 2. No subsequent tool_call_update
141+
* 3. No assistant messages after the permission request (conversation hasn't moved on)
114142
*/
115143
export function findPendingPermissions(
116144
entries: StoredLogEntry[],
117145
): Map<string, PermissionRequest> {
118-
const requests = new Map<string, StoredLogEntry>();
119-
const responses = new Set<string>();
120-
121-
for (const entry of entries) {
122-
const method = entry.notification?.method;
123-
const params = entry.notification?.params as
124-
| Record<string, unknown>
125-
| undefined;
126-
127-
// TODO: Migrate to twig
128-
if (method === "_array/permission_request" && params?.toolCallId) {
129-
requests.set(params.toolCallId as string, entry);
146+
const permissionRequests = new Map<
147+
string,
148+
{ entry: StoredLogEntry; index: number }
149+
>();
150+
const resolvedToolCalls = new Set<string>();
151+
let lastAssistantMessageIndex = -1;
152+
153+
entries.forEach((entry, i) => {
154+
const msg = entry.notification;
155+
156+
const permissionToolCallId = getPermissionToolCallId(msg);
157+
if (permissionToolCallId) {
158+
permissionRequests.set(permissionToolCallId, { entry, index: i });
130159
}
131160

132-
// TODO: Migrate to twig
133-
if (method === "_array/permission_response" && params?.toolCallId) {
134-
responses.add(params.toolCallId as string);
161+
const update = getSessionUpdate(msg);
162+
if (!update) return;
163+
164+
const isResolvedToolCall =
165+
update.sessionUpdate === "tool_call_update" &&
166+
update.toolCallId &&
167+
isTerminalStatus(update.status);
168+
169+
if (isResolvedToolCall) {
170+
resolvedToolCalls.add(update.toolCallId!);
135171
}
136-
}
137172

138-
const pending = new Map<string, PermissionRequest>();
139-
for (const [toolCallId, entry] of requests) {
140-
if (!responses.has(toolCallId)) {
141-
const params = entry.notification?.params as RequestPermissionRequest;
142-
pending.set(toolCallId, {
143-
...params,
144-
receivedAt: entry.timestamp
145-
? new Date(entry.timestamp).getTime()
146-
: Date.now(),
147-
});
173+
if (update.sessionUpdate === "assistant_message") {
174+
lastAssistantMessageIndex = i;
148175
}
176+
});
177+
178+
const pending = new Map<string, PermissionRequest>();
179+
for (const [toolCallId, { entry, index }] of permissionRequests) {
180+
const isResolved = resolvedToolCalls.has(toolCallId);
181+
const isStale = lastAssistantMessageIndex > index;
182+
if (isResolved || isStale) continue;
183+
184+
const params = entry.notification?.params as RequestPermissionRequest;
185+
pending.set(toolCallId, {
186+
...params,
187+
receivedAt: entry.timestamp
188+
? new Date(entry.timestamp).getTime()
189+
: Date.now(),
190+
});
149191
}
150192

151193
return pending;

packages/agent/src/adapters/base-acp-agent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ export abstract class BaseAcpAgent implements Agent {
3535
protected session!: BaseSession;
3636
protected sessionId!: string;
3737
client: AgentSideConnection;
38-
protected logger: Logger;
39-
protected fileContentCache: { [key: string]: string } = {};
38+
logger: Logger;
39+
fileContentCache: { [key: string]: string } = {};
4040

4141
constructor(client: AgentSideConnection) {
4242
this.client = client;

0 commit comments

Comments
 (0)