From 1955dd9da26615d5bf455faa33d6171f22c59af1 Mon Sep 17 00:00:00 2001 From: RTCartist Date: Thu, 4 Jun 2026 23:26:42 +0800 Subject: [PATCH 1/5] fix: accept legacy ACP optionIds for backward compatibility Older ACP clients (e.g. Python ACP SDK 0.8.x) send different optionId values than the current TypeScript SDK: `approve` instead of `approve_once`, and `approve_for_session` instead of `approve_always`. Add a compatibility mapping in `permissionResponseToApprovalResponse` so these legacy ids are normalized to their canonical equivalents before the decision switch, preventing valid approvals from being silently rejected. Closes #426 --- packages/acp-adapter/src/approval.ts | 14 +++++++++++++- packages/acp-adapter/test/approval.test.ts | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/acp-adapter/src/approval.ts b/packages/acp-adapter/src/approval.ts index 8e183fdae..121f98ac2 100644 --- a/packages/acp-adapter/src/approval.ts +++ b/packages/acp-adapter/src/approval.ts @@ -22,6 +22,17 @@ export const APPROVE_ONCE_OPTION_ID = 'approve_once'; export const APPROVE_ALWAYS_OPTION_ID = 'approve_always'; export const REJECT_OPTION_ID = 'reject'; +/** + * Legacy option ids sent by older ACP clients (e.g. Python ACP SDK 0.8.x). + * Mapped to the canonical ids so that clients built against the older SDK + * continue to work without modification. + */ +const LEGACY_OPTION_ID_MAP: Record = { + approve: APPROVE_ONCE_OPTION_ID, + approve_for_session: APPROVE_ALWAYS_OPTION_ID, + reject: REJECT_OPTION_ID, +}; + /** * Phase 13.2 plan_review optionId namespace. Picked deliberately so the * `plan_*` prefix never collides with the canonical `approve_*` / @@ -151,7 +162,8 @@ export function permissionResponseToApprovalResponse( if (response.outcome.outcome === 'cancelled') { return { decision: 'cancelled' }; } - const optionId = response.outcome.optionId; + const rawOptionId = response.outcome.optionId; + const optionId = LEGACY_OPTION_ID_MAP[rawOptionId] ?? rawOptionId; if (req?.display.kind === 'plan_review') { return mapPlanReviewOptionId(req.display, optionId); } diff --git a/packages/acp-adapter/test/approval.test.ts b/packages/acp-adapter/test/approval.test.ts index 309466697..3a8001d07 100644 --- a/packages/acp-adapter/test/approval.test.ts +++ b/packages/acp-adapter/test/approval.test.ts @@ -175,6 +175,21 @@ describe('permissionResponseToApprovalResponse', () => { expect(result).toEqual({ decision: 'rejected' }); }); + it('maps legacy "approve" → { decision: approved } (Python ACP SDK compat)', () => { + const result = permissionResponseToApprovalResponse(undefined, { + outcome: { outcome: 'selected', optionId: 'approve' }, + }); + expect(result).toEqual({ decision: 'approved' }); + expect(result.scope).toBeUndefined(); + }); + + it('maps legacy "approve_for_session" → { decision: approved, scope: session } (Python ACP SDK compat)', () => { + const result = permissionResponseToApprovalResponse(undefined, { + outcome: { outcome: 'selected', optionId: 'approve_for_session' }, + }); + expect(result).toEqual({ decision: 'approved', scope: 'session' }); + }); + it('defensively maps an unknown optionId to { decision: rejected }', () => { const result = permissionResponseToApprovalResponse(undefined, { outcome: { outcome: 'selected', optionId: 'unknown_option_id' }, From 0bbf492ec77798fbeb5bf17a247b4709d20992bf Mon Sep 17 00:00:00 2001 From: RTCartist Date: Thu, 4 Jun 2026 23:42:53 +0800 Subject: [PATCH 2/5] chore: add changeset for legacy ACP optionId compatibility fix --- .changeset/legacy-acp-optionid-compat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/legacy-acp-optionid-compat.md diff --git a/.changeset/legacy-acp-optionid-compat.md b/.changeset/legacy-acp-optionid-compat.md new file mode 100644 index 000000000..af012847e --- /dev/null +++ b/.changeset/legacy-acp-optionid-compat.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix ACP custom client tools being auto-rejected when using legacy Python ACP SDK optionId values (`approve`, `approve_for_session`) From 9bba5139ea70cba6e45c0b0fe58c955a0b7a8c4a Mon Sep 17 00:00:00 2001 From: RTCartist Date: Fri, 5 Jun 2026 00:23:48 +0800 Subject: [PATCH 3/5] fix: apply legacy optionId mapping only in the canonical branch Move the legacy-to-canonical normalization below the plan_review check so that plan_review prompts pass the raw optionId directly to mapPlanReviewOptionId. Legacy clients predate plan_review, but this ordering is defensively correct: a stray canonical id should not be silently rewritten before the plan mapper sees it. --- packages/acp-adapter/src/approval.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/acp-adapter/src/approval.ts b/packages/acp-adapter/src/approval.ts index 121f98ac2..6491d2f6b 100644 --- a/packages/acp-adapter/src/approval.ts +++ b/packages/acp-adapter/src/approval.ts @@ -163,10 +163,10 @@ export function permissionResponseToApprovalResponse( return { decision: 'cancelled' }; } const rawOptionId = response.outcome.optionId; - const optionId = LEGACY_OPTION_ID_MAP[rawOptionId] ?? rawOptionId; if (req?.display.kind === 'plan_review') { - return mapPlanReviewOptionId(req.display, optionId); + return mapPlanReviewOptionId(req.display, rawOptionId); } + const optionId = LEGACY_OPTION_ID_MAP[rawOptionId] ?? rawOptionId; switch (optionId) { case APPROVE_ONCE_OPTION_ID: return { decision: 'approved' }; From 8eb18476afcfd38e135f7d8f2907531f1c63d0e8 Mon Sep 17 00:00:00 2001 From: RTCartist Date: Fri, 5 Jun 2026 02:03:36 +0800 Subject: [PATCH 4/5] chore(changeset): include @moonshot-ai/acp-adapter in legacy optionId fix Source changes live in `packages/acp-adapter/src/approval.ts`, so the adapter package needs to bump alongside `@moonshot-ai/kimi-code` to match the repo convention used by prior internal-package changes (see e.g. .changeset/add-update-config-skill.md). --- .changeset/legacy-acp-optionid-compat.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/legacy-acp-optionid-compat.md b/.changeset/legacy-acp-optionid-compat.md index af012847e..2da8e7b52 100644 --- a/.changeset/legacy-acp-optionid-compat.md +++ b/.changeset/legacy-acp-optionid-compat.md @@ -1,4 +1,5 @@ --- +"@moonshot-ai/acp-adapter": patch "@moonshot-ai/kimi-code": patch --- From 416f9b72f31ccc6b6483437612d8560510d86219 Mon Sep 17 00:00:00 2001 From: RTCartist Date: Fri, 5 Jun 2026 02:40:08 +0800 Subject: [PATCH 5/5] fix: preserve selectedLabel for legacy ACP optionIds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a legacy client (Python ACP SDK 0.8.x) returns `approve` or `approve_for_session`, `permissionResponseToApprovalResponse` correctly routes the decision but `attachSelectedLabel` then looks up the raw legacy id against the canonical option table and misses, dropping the human-readable label. PermissionResult hooks and approval records would therefore lose context such as "Approve for this session" — unlike the canonical path. Reuse `LEGACY_OPTION_ID_MAP` in `attachSelectedLabel` to normalize the optionId before the lookup, matching the canonical path. --- packages/acp-adapter/src/approval.ts | 9 ++++++- .../acp-adapter/test/approval-display.test.ts | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/acp-adapter/src/approval.ts b/packages/acp-adapter/src/approval.ts index 6491d2f6b..874116002 100644 --- a/packages/acp-adapter/src/approval.ts +++ b/packages/acp-adapter/src/approval.ts @@ -322,7 +322,14 @@ export function attachSelectedLabel( ) { return approval; } - const matched = options.find((o) => o.optionId === outcome.optionId); + // Normalize legacy ids (Python ACP SDK 0.8.x sends `approve` / + // `approve_for_session` / `reject`) to their canonical equivalents + // before the lookup. Without this, legacy approvals are correctly + // routed by `permissionResponseToApprovalResponse` but drop the + // human-readable `selectedLabel` (e.g. "Approve for this session"), + // so PermissionResult hooks and approval records lose context. + const normalizedOptionId = LEGACY_OPTION_ID_MAP[outcome.optionId] ?? outcome.optionId; + const matched = options.find((o) => o.optionId === normalizedOptionId); if (!matched) return approval; return { ...approval, selectedLabel: matched.name }; } diff --git a/packages/acp-adapter/test/approval-display.test.ts b/packages/acp-adapter/test/approval-display.test.ts index bbedb07e5..cec2ce548 100644 --- a/packages/acp-adapter/test/approval-display.test.ts +++ b/packages/acp-adapter/test/approval-display.test.ts @@ -259,6 +259,30 @@ describe('attachSelectedLabel', () => { expect(result).toEqual({ decision: 'rejected', selectedLabel: 'Reject' }); }); + it('attaches "Approve once" when the legacy "approve" optionId is selected', () => { + const approval: ApprovalResponse = { decision: 'approved' }; + const result = attachSelectedLabel( + { outcome: { outcome: 'selected', optionId: 'approve' } }, + approval, + options, + ); + expect(result).toEqual({ decision: 'approved', selectedLabel: 'Approve once' }); + }); + + it('attaches "Approve for this session" when the legacy "approve_for_session" optionId is selected', () => { + const approval: ApprovalResponse = { decision: 'approved', scope: 'session' }; + const result = attachSelectedLabel( + { outcome: { outcome: 'selected', optionId: 'approve_for_session' } }, + approval, + options, + ); + expect(result).toEqual({ + decision: 'approved', + scope: 'session', + selectedLabel: 'Approve for this session', + }); + }); + it('returns the input unchanged when the optionId is unknown', () => { const approval: ApprovalResponse = { decision: 'rejected' }; const result = attachSelectedLabel(