| ... | ... | @@ -3360,6 +3360,118 @@ describe("ClaudeAdapterLive", () => { |
| 3360 | 3360 | ); |
| 3361 | 3361 | }); |
| 3362 | 3362 | |
| 3363 | it.effect("acceptForSession returns session-scoped permission updates", () => { |
| 3364 | const harness = makeHarness(); |
| 3365 | return Effect.gen(function* () { |
| 3366 | const adapter = yield* ClaudeAdapter; |
| 3367 | |
| 3368 | const session = yield* adapter.startSession({ |
| 3369 | threadId: THREAD_ID, |
| 3370 | provider: ProviderDriverKind.make("claudeAgent"), |
| 3371 | runtimeMode: "approval-required", |
| 3372 | }); |
| 3373 | |
| 3374 | yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); |
| 3375 | |
| 3376 | yield* adapter.sendTurn({ |
| 3377 | threadId: session.threadId, |
| 3378 | input: "approve this for the session", |
| 3379 | attachments: [], |
| 3380 | }); |
| 3381 | yield* Stream.take(adapter.streamEvents, 1).pipe(Stream.runDrain); |
| 3382 | |
| 3383 | const createInput = harness.getLastCreateQueryInput(); |
| 3384 | const canUseTool = createInput?.options.canUseTool; |
| 3385 | assert.equal(typeof canUseTool, "function"); |
| 3386 | if (!canUseTool) { |
| 3387 | return; |
| 3388 | } |
| 3389 | |
| 3390 | const respondToNextRequest = Effect.gen(function* () { |
| 3391 | const requested = yield* Stream.runHead(adapter.streamEvents); |
| 3392 | assert.equal(requested._tag, "Some"); |
| 3393 | if (requested._tag !== "Some" || requested.value.type !== "request.opened") { |
| 3394 | return; |
| 3395 | } |
| 3396 | const runtimeRequestId = requested.value.requestId; |
| 3397 | assert.equal(typeof runtimeRequestId, "string"); |
| 3398 | if (runtimeRequestId === undefined) { |
| 3399 | return; |
| 3400 | } |
| 3401 | yield* adapter.respondToRequest( |
| 3402 | session.threadId, |
| 3403 | ApprovalRequestId.make(runtimeRequestId), |
| 3404 | "acceptForSession", |
| 3405 | ); |
| 3406 | yield* Stream.take(adapter.streamEvents, 1).pipe(Stream.runDrain); |
| 3407 | }); |
| 3408 | |
| 3409 | // MCP tools frequently arrive with no usable suggestion (Claude Code |
| 3410 | // sends an empty array); the decision must still stick for the session. |
| 3411 | const mcpPermissionPromise = canUseTool( |
| 3412 | "mcp__linear__create_issue", |
| 3413 | { title: "hello" }, |
| 3414 | { |
| 3415 | signal: new AbortController().signal, |
| 3416 | suggestions: [], |
| 3417 | toolUseID: "tool-use-mcp-1", |
| 3418 | }, |
| 3419 | ); |
| 3420 | yield* respondToNextRequest; |
| 3421 | const mcpPermission = (yield* Effect.promise(() => mcpPermissionPromise)) as PermissionResult; |
| 3422 | assert.equal(mcpPermission.behavior, "allow"); |
| 3423 | if (mcpPermission.behavior !== "allow") { |
| 3424 | return; |
| 3425 | } |
| 3426 | assert.deepEqual(mcpPermission.updatedPermissions, [ |
| 3427 | { |
| 3428 | type: "addRules", |
| 3429 | rules: [{ toolName: "mcp__linear__create_issue" }], |
| 3430 | behavior: "allow", |
| 3431 | destination: "session", |
| 3432 | }, |
| 3433 | ]); |
| 3434 | |
| 3435 | // Received suggestions are reused but rescoped to the session — |
| 3436 | // echoing "localSettings" would persist a session-only choice to disk. |
| 3437 | const bashPermissionPromise = canUseTool( |
| 3438 | "Bash", |
| 3439 | { command: "git status" }, |
| 3440 | { |
| 3441 | signal: new AbortController().signal, |
| 3442 | suggestions: [ |
| 3443 | { |
| 3444 | type: "addRules", |
| 3445 | rules: [{ toolName: "Bash", ruleContent: "git status" }], |
| 3446 | behavior: "allow", |
| 3447 | destination: "localSettings", |
| 3448 | }, |
| 3449 | ], |
| 3450 | toolUseID: "tool-use-bash-1", |
| 3451 | }, |
| 3452 | ); |
| 3453 | yield* respondToNextRequest; |
| 3454 | const bashPermission = (yield* Effect.promise( |
| 3455 | () => bashPermissionPromise, |
| 3456 | )) as PermissionResult; |
| 3457 | assert.equal(bashPermission.behavior, "allow"); |
| 3458 | if (bashPermission.behavior !== "allow") { |
| 3459 | return; |
| 3460 | } |
| 3461 | assert.deepEqual(bashPermission.updatedPermissions, [ |
| 3462 | { |
| 3463 | type: "addRules", |
| 3464 | rules: [{ toolName: "Bash", ruleContent: "git status" }], |
| 3465 | behavior: "allow", |
| 3466 | destination: "session", |
| 3467 | }, |
| 3468 | ]); |
| 3469 | }).pipe( |
| 3470 | Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| 3471 | Effect.provide(harness.layer), |
| 3472 | ); |
| 3473 | }); |
| 3474 | |
| 3363 | 3475 | it.effect("classifies Agent tools and read-only Claude tools correctly for approvals", () => { |
| 3364 | 3476 | const harness = makeHarness(); |
| 3365 | 3477 | return Effect.gen(function* () { |