feat: Add ACP logout support (#816)

This commit is contained in:
Ben Brandt
2026-06-25 23:42:58 +02:00
committed by GitHub
parent e9163f8553
commit 0a0468c720
3 changed files with 58 additions and 4 deletions

View File

@@ -17,6 +17,7 @@ import {
ListSessionsResponse,
LoadSessionRequest,
LoadSessionResponse,
LogoutRequest,
methods,
ndJsonStream,
NewSessionRequest,
@@ -74,11 +75,13 @@ import {
} from "@anthropic-ai/claude-agent-sdk";
import { ContentBlockParam } from "@anthropic-ai/sdk/resources";
import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
import type { Stats } from "node:fs";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { promisify } from "node:util";
import packageJson from "../package.json" with { type: "json" };
import {
applyAskElicitationResponse,
@@ -109,6 +112,8 @@ import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./u
export const CLAUDE_CONFIG_DIR =
process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
const execFileAsync = promisify(execFile);
const MAX_TITLE_LENGTH = 256;
function sanitizeTitle(text: string): string {
@@ -855,6 +860,9 @@ export class ClaudeAcpAgent {
http: true,
sse: true,
},
auth: {
logout: {},
},
loadSession: true,
sessionCapabilities: {
additionalDirectories: {},
@@ -992,6 +1000,30 @@ export class ClaudeAcpAgent {
throw new Error("Method not implemented.");
}
async logout(_params: LogoutRequest): Promise<void> {
// Clear in-memory gateway credentials supplied via `authenticate`. The
// gateway method never touches the on-disk credential store, so dropping
// this reference is the whole logout for that path.
this.gatewayAuthRequest = undefined;
// For the Claude/Console login methods the credentials live in the native
// CLI's store (keychain or config dir), which only the binary can clear.
// `claude auth logout` is non-interactive and idempotent.
const cliPath = await claudeCliPath();
try {
await execFileAsync(cliPath, ["auth", "logout"]);
} catch (error) {
const stderr =
typeof error === "object" && error && "stderr" in error
? String((error as { stderr: unknown }).stderr).trim()
: undefined;
throw RequestError.internalError(
{ stderr: stderr || undefined },
`claude auth logout failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async prompt(params: PromptRequest): Promise<PromptResponse> {
const session = this.sessions[params.sessionId];
if (!session) {
@@ -4572,6 +4604,7 @@ export function runAcp() {
agent.setSessionConfigOption(ctx.params),
)
.onRequest(methods.agent.authenticate, (ctx) => agent.authenticate(ctx.params))
.onRequest(methods.agent.logout, (ctx) => agent.logout(ctx.params))
.onRequest(methods.agent.session.prompt, (ctx) =>
runPromptWithCancellation(agent, ctx.params, ctx.signal),
)

View File

@@ -4,10 +4,10 @@ import { resolveSettings } from "@anthropic-ai/claude-agent-sdk";
import { claudeCliPath, runAcp } from "./acp-agent.js";
import packageJson from "../package.json" with { type: "json" };
if (process.argv.includes("--version") || process.argv.includes("-v")) {
console.log(packageJson.version);
process.exit(0);
} else if (process.argv.includes("--cli")) {
// `--cli` is checked first so that `--version`/`-v` (and any other flags) are
// forwarded to the wrapped native CLI rather than swallowed by our own version
// handler below. Our version flag only applies when not delegating.
if (process.argv.includes("--cli")) {
const { spawn } = await import("node:child_process");
const args = process.argv.slice(2).filter((arg) => arg !== "--cli");
const child = spawn(await claudeCliPath(), args, { stdio: "inherit" });
@@ -37,6 +37,9 @@ if (process.argv.includes("--version") || process.argv.includes("-v")) {
console.error(err);
process.exit(1);
});
} else if (process.argv.includes("--version") || process.argv.includes("-v")) {
console.log(packageJson.version);
process.exit(0);
} else {
// Apply env vars from the managed-policy tier before any SDK call so the
// SDK subprocess inherits them. Going through resolveSettings (vs. a raw

View File

@@ -2523,6 +2523,24 @@ describe("stop reason propagation", () => {
});
});
describe("logout", () => {
function createMockAgent() {
const mockClient = {
sessionUpdate: async () => {},
} as unknown as AcpClient;
return new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
}
it("advertises the logout capability during initialize", async () => {
const agent = createMockAgent();
const response = await agent.initialize({
protocolVersion: 1,
clientCapabilities: {},
});
expect(response.agentCapabilities?.auth?.logout).toEqual({});
});
});
describe("session/close", () => {
function createMockAgent() {
const mockClient = {