History

Deny AskUserQuestion after aborted waiting turn

- Return a deny permission result when the user cancels a pending Claude question - Add coverage for aborting AskUserQuestion while user input is pending

Julius Marminge committed 8 months ago

Commit fda3fa31 parent 004b6fe2 files · +77 −0Browse files

Unified · Split

2 files
  1. apps/server/src/provider/Layers/ClaudeAdapter.test.ts +68 −0
  2. apps/server/src/provider/Layers/ClaudeAdapter.ts +9 −0
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+68−0
@@ -2497,6 +2497,74 @@ describe("ClaudeAdapterLive", () => { · 2496 unmodified lines
24972497 );
24982498 });
24992499
2500+ it.effect("denies AskUserQuestion when the waiting turn is aborted", () => {
2501+ const harness = makeHarness();
2502+ return Effect.gen(function* () {
2503+ const adapter = yield* ClaudeAdapter;
2504+
2505+ const session = yield* adapter.startSession({
2506+ threadId: THREAD_ID,
2507+ provider: "claudeAgent",
2508+ runtimeMode: "approval-required",
2509+ });
2510+
2511+ yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain);
2512+
2513+ const createInput = harness.getLastCreateQueryInput();
2514+ const canUseTool = createInput?.options.canUseTool;
2515+ assert.equal(typeof canUseTool, "function");
2516+ if (!canUseTool) {
2517+ return;
2518+ }
2519+
2520+ const controller = new AbortController();
2521+ const permissionPromise = canUseTool(
2522+ "AskUserQuestion",
2523+ {
2524+ questions: [
2525+ {
2526+ question: "Continue?",
2527+ header: "Continue",
2528+ options: [{ label: "Yes", description: "Proceed" }],
2529+ multiSelect: false,
2530+ },
2531+ ],
2532+ },
2533+ {
2534+ signal: controller.signal,
2535+ toolUseID: "tool-ask-abort",
2536+ },
2537+ );
2538+
2539+ const requestedEvent = yield* Stream.runHead(adapter.streamEvents);
2540+ assert.equal(requestedEvent._tag, "Some");
2541+ if (requestedEvent._tag !== "Some" || requestedEvent.value.type !== "user-input.requested") {
2542+ assert.fail("Expected user-input.requested event");
2543+ return;
2544+ }
2545+ assert.equal(requestedEvent.value.threadId, session.threadId);
2546+
2547+ controller.abort();
2548+
2549+ const resolvedEvent = yield* Stream.runHead(adapter.streamEvents);
2550+ assert.equal(resolvedEvent._tag, "Some");
2551+ if (resolvedEvent._tag !== "Some" || resolvedEvent.value.type !== "user-input.resolved") {
2552+ assert.fail("Expected user-input.resolved event");
2553+ return;
2554+ }
2555+ assert.deepEqual(resolvedEvent.value.payload.answers, {});
2556+
2557+ const permissionResult = yield* Effect.promise(() => permissionPromise);
2558+ assert.deepEqual(permissionResult, {
2559+ behavior: "deny",
2560+ message: "User cancelled tool execution.",
2561+ } satisfies PermissionResult);
2562+ }).pipe(
2563+ Effect.provideService(Random.Random, makeDeterministicRandomService()),
2564+ Effect.provide(harness.layer),
2565+ );
2566+ });
2567+
25002568 it.effect("writes provider-native observability records when enabled", () => {
25012569 const nativeEvents: Array<{
25022570 event?: {
apps/server/src/provider/Layers/ClaudeAdapter.ts+9−0
@@ -2209,6 +2209,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { · 2208 unmodified lines
22092209 );
22102210
22112211 const answersDeferred = yield* Deferred.make<ProviderUserInputAnswers>();
2212+ let aborted = false;
22122213 const pendingInput: PendingUserInput = {
22132214 questions,
22142215 answers: answersDeferred,
@@ -2242,6 +2243,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { · 27 unmodified lines
22422243 if (!pendingUserInputs.has(requestId)) {
22432244 return;
22442245 }
2246+ aborted = true;
22452247 pendingUserInputs.delete(requestId);
22462248 Effect.runFork(Deferred.succeed(answersDeferred, {} as ProviderUserInputAnswers));
22472249 };
@@ -2272,6 +2274,13 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { · 24 unmodified lines
22722274 },
22732275 });
22742276
2277+ if (aborted) {
2278+ return {
2279+ behavior: "deny",
2280+ message: "User cancelled tool execution.",
2281+ } satisfies PermissionResult;
2282+ }
2283+
22752284 // Return the answers to the SDK in the expected format:
22762285 // { questions: [...], answers: { questionText: selectedLabel } }
22772286 return {

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t