From 6f66f92e72d535102aec8361cfcdb75b39344558 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 10 Mar 2026 21:52:31 -0700 Subject: [PATCH] fix: prevent permission reply from silently escalating "once" to "always" Remove allow_always from the fallback chain when the user replies "once", aligning with the ACP spec which says "map by option kind first" with no fallback for allow_once. Also fix Inspector to use rawSend, revert hydration guard to accept empty configOptions, and handle respondPermission errors by rejecting the pending promise. Co-Authored-By: Claude Opus 4.6 --- docs/inspector.mdx | 1 + frontend/packages/inspector/src/App.tsx | 9 ++++----- sdks/typescript/src/client.ts | 13 ++++++++++--- sdks/typescript/tests/integration.test.ts | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/inspector.mdx b/docs/inspector.mdx index 06318b2..cc5f3d0 100644 --- a/docs/inspector.mdx +++ b/docs/inspector.mdx @@ -34,6 +34,7 @@ console.log(url); - Event JSON inspector - Prompt testing - Request/response debugging +- Interactive permission prompts (approve, always-allow, or reject tool-use requests) - Process management (create, stop, kill, delete, view logs) - Interactive PTY terminal for tty processes - One-shot command execution diff --git a/frontend/packages/inspector/src/App.tsx b/frontend/packages/inspector/src/App.tsx index bae03d0..b5f01a7 100644 --- a/frontend/packages/inspector/src/App.tsx +++ b/frontend/packages/inspector/src/App.tsx @@ -870,7 +870,7 @@ export default function App() { // Apply mode if selected if (!skipPostCreateConfig && config.agentMode) { try { - await session.send("session/set_mode", { modeId: config.agentMode }); + await session.rawSend("session/set_mode", { modeId: config.agentMode }); } catch { // Mode application is best-effort } @@ -886,7 +886,7 @@ export default function App() { (opt) => opt.category === "model" && opt.type === "select" && typeof opt.id === "string" ); if (modelOption && config.model !== modelOption.currentValue) { - await session.send("session/set_config_option", { + await session.rawSend("session/set_config_option", { optionId: modelOption.id, value: config.model, }); @@ -1241,7 +1241,6 @@ export default function App() { })); const title = params?.toolCall?.title ?? params?.toolCall?.toolCallId ?? "Permission request"; const resolved = resolvedPermissions.get(permissionId); - const isPending = pendingPermissionIds.has(permissionId); entries.push({ id: event.id, eventId: event.id, @@ -1252,7 +1251,7 @@ export default function App() { title, description: params?.toolCall?.description, options, - resolved: resolved != null || !isPending, + resolved: resolved != null || sdkPermissionId == null, selectedOptionId: resolved, }, }); @@ -1288,7 +1287,7 @@ export default function App() { } return entries; - }, [events, pendingPermissionIds, resolvedPermissions]); + }, [events, resolvedPermissions]); useEffect(() => { return () => { diff --git a/sdks/typescript/src/client.ts b/sdks/typescript/src/client.ts index 3cb27db..3d9857f 100644 --- a/sdks/typescript/src/client.ts +++ b/sdks/typescript/src/client.ts @@ -1262,7 +1262,7 @@ export class SandboxAgent { } private async hydrateSessionConfigOptions(sessionId: string, snapshot: SessionRecord): Promise { - if (snapshot.configOptions !== undefined && snapshot.configOptions.length > 0) { + if (snapshot.configOptions !== undefined) { return snapshot; } @@ -1440,7 +1440,14 @@ export class SandboxAgent { throw new Error(`permission '${permissionId}' not found`); } - const response = permissionReplyToResponse(permissionId, pending.request, reply); + let response: RequestPermissionResponse; + try { + response = permissionReplyToResponse(permissionId, pending.request, reply); + } catch (error) { + pending.reject(error instanceof Error ? error : new Error(String(error))); + this.pendingPermissionRequests.delete(permissionId); + throw error; + } this.resolvePendingPermission(permissionId, response); } @@ -2704,7 +2711,7 @@ function permissionReplyToResponse( ): RequestPermissionResponse { const preferredKinds: PermissionOptionKind[] = reply === "once" - ? ["allow_once", "allow_always"] + ? ["allow_once"] : reply === "always" ? ["allow_always", "allow_once"] : ["reject_once", "reject_always"]; diff --git a/sdks/typescript/tests/integration.test.ts b/sdks/typescript/tests/integration.test.ts index 9b644c9..2ce0948 100644 --- a/sdks/typescript/tests/integration.test.ts +++ b/sdks/typescript/tests/integration.test.ts @@ -512,7 +512,7 @@ describe("Integration: TypeScript SDK flat session API", () => { const session = await sdk.createSession({ agent: "mock" }); - await expect(session.send("session/cancel")).rejects.toThrow( + await expect(session.rawSend("session/cancel")).rejects.toThrow( "Use destroySession(sessionId) instead.", ); await expect(sdk.rawSendSessionMethod(session.id, "session/cancel", {})).rejects.toThrow(