Download packages/acp-server/src/approval.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 9.92 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/acp-server/src/approval.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/acp-server/src/approval.ts
-
curl -L -o approval.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/acp-server/src/approval.ts
9.92 kB
| /** | |
| * ACP `session/request_permission` ↔ agent-core-v2 approval mappers. | |
| * | |
| * Pure functions that translate an `ApprovalRequest` (raised by the engine's | |
| * `AgentPermissionGate` and surfaced through the `interaction` kernel) into the | |
| * ACP `PermissionOption[]` + `ToolCallUpdate` surfaced to the client, and the | |
| * client's `RequestPermissionResponse` back into an `ApprovalResponse`. Kept | |
| * free of IO so the mappings stay unit-testable without a live connection. | |
| */ | |
| import type { | |
| PermissionOption, | |
| RequestPermissionResponse, | |
| ToolCallContent, | |
| ToolCallUpdate, | |
| } from '@agentclientprotocol/sdk'; | |
| import type { | |
| SessionApprovalRequest as ApprovalRequest, | |
| SessionApprovalResponse as ApprovalResponse, | |
| } from '@moonshot-ai/agent-core-v2'; | |
| import { displayBlockToAcpContent } from './convert'; | |
| import { acpToolCallId } from './events-map'; | |
| /** | |
| * Canonical option ids surfaced to the ACP client. | |
| * | |
| * The wire-level `PermissionOption.optionId` is opaque to the client (it | |
| * round-trips back in `RequestPermissionResponse.outcome.optionId`), so the | |
| * host is free to pick any stable string. These literals are the single source | |
| * of truth on both the build- and the parse-side; tests import them rather | |
| * than re-typing the strings. | |
| */ | |
| export const APPROVE_ONCE_OPTION_ID = 'approve_once'; | |
| export const APPROVE_ALWAYS_OPTION_ID = 'approve_always'; | |
| export const REJECT_OPTION_ID = 'reject'; | |
| /** | |
| * `plan_review` optionId namespace. Picked deliberately so the `plan_*` prefix | |
| * never collides with the canonical `approve_*` / `reject` namespace nor with | |
| * the question bridge's `q{n}_*` namespace. | |
| * | |
| * - `plan_opt_<i>` — one per `display.options[i]` (rendered as `allow_once` | |
| * so the user can pick A / B / C without re-entering the prompt). | |
| * - `plan_approve` — fallback approve when `display.options` is absent or | |
| * has fewer than two entries. | |
| * - `plan_revise` / `plan_reject_and_exit` — the two reject-side exits. | |
| */ | |
| export const PLAN_APPROVE_OPTION_ID = 'plan_approve'; | |
| export const PLAN_REVISE_OPTION_ID = 'plan_revise'; | |
| export const PLAN_REJECT_AND_EXIT_OPTION_ID = 'plan_reject_and_exit'; | |
| function planOptOptionId(i: number): string { | |
| return `plan_opt_${i}`; | |
| } | |
| /** | |
| * The three canonical permission options surfaced to the ACP client for a | |
| * non-`plan_review` approval prompt. | |
| * | |
| * Order is load-bearing: ACP clients render options top-to-bottom, so | |
| * allow-once is the primary action, allow-always the secondary, and reject the | |
| * terminal/dangerous action that should be hardest to click by accident. | |
| */ | |
| const CANONICAL_OPTIONS: readonly PermissionOption[] = [ | |
| { optionId: APPROVE_ONCE_OPTION_ID, name: 'Approve once', kind: 'allow_once' }, | |
| { | |
| optionId: APPROVE_ALWAYS_OPTION_ID, | |
| name: 'Approve for this session', | |
| kind: 'allow_always', | |
| }, | |
| { optionId: REJECT_OPTION_ID, name: 'Reject', kind: 'reject_once' }, | |
| ]; | |
| /** | |
| * Build the {@link PermissionOption}[] surfaced to the ACP client for an | |
| * approval prompt. | |
| * | |
| * When the request's display block carries `kind: 'plan_review'`, the options | |
| * expand to one `allow_once` per `display.options[i]` (A / B / C) — or a | |
| * single `plan_approve` fallback when the policy did not supply ≥ 2 discrete | |
| * options — plus the two `reject_once` exits `Revise` and `Reject and Exit`. | |
| * | |
| * For every other display kind, returns the canonical 3-option list. | |
| */ | |
| export function approvalRequestToPermissionOptions( | |
| req: ApprovalRequest, | |
| ): readonly PermissionOption[] { | |
| if (req.display.kind !== 'plan_review') { | |
| return CANONICAL_OPTIONS; | |
| } | |
| const display = req.display; | |
| const approveOptions: PermissionOption[] = | |
| display.options !== undefined && display.options.length >= 2 | |
| ? display.options.map((opt, i) => ({ | |
| optionId: planOptOptionId(i), | |
| name: opt.label, | |
| kind: 'allow_once' as const, | |
| })) | |
| : [{ optionId: PLAN_APPROVE_OPTION_ID, name: 'Approve', kind: 'allow_once' as const }]; | |
| return [ | |
| ...approveOptions, | |
| { optionId: PLAN_REVISE_OPTION_ID, name: 'Revise', kind: 'reject_once' as const }, | |
| { | |
| optionId: PLAN_REJECT_AND_EXIT_OPTION_ID, | |
| name: 'Reject and Exit', | |
| kind: 'reject_once' as const, | |
| }, | |
| ]; | |
| } | |
| /** | |
| * Translate an ACP {@link RequestPermissionResponse} into an engine | |
| * {@link ApprovalResponse}. | |
| * | |
| * Decision mapping (canonical / non-plan_review path): | |
| * - `cancelled` outcome → `decision: 'cancelled'`. | |
| * - `approve_once` → `decision: 'approved'` (no scope, one-shot). | |
| * - `approve_always` → `decision: 'approved'` with `scope: 'session'` so the | |
| * engine installs a session-runtime allow rule for subsequent invocations. | |
| * - `reject` → `decision: 'rejected'`. | |
| * - Legacy Python kimi-cli (< v0.9.0) ids `approve` / `approve_for_session` | |
| * map like `approve_once` / `approve_always` so custom ACP clients built | |
| * against the old SDK are not silently rejected. | |
| * - Any other optionId → defensive `rejected` (rejecting is strictly safer | |
| * than approving for an unknown id). | |
| * | |
| * For `plan_review`, the `plan_opt_<i>` / `plan_approve` / `plan_revise` / | |
| * `plan_reject_and_exit` optionIds map directly to the discriminator, and the | |
| * matched option's label is attached as `selectedLabel` so the downstream | |
| * policy can drive its branch off a stable string. | |
| */ | |
| export function permissionResponseToApprovalResponse( | |
| req: ApprovalRequest, | |
| response: RequestPermissionResponse, | |
| ): ApprovalResponse { | |
| if (response.outcome.outcome === 'cancelled') { | |
| return { decision: 'cancelled' }; | |
| } | |
| const optionId = response.outcome.optionId; | |
| if (req.display.kind === 'plan_review') { | |
| return mapPlanReviewOptionId(req.display, optionId); | |
| } | |
| switch (optionId) { | |
| case APPROVE_ONCE_OPTION_ID: | |
| // Legacy Python kimi-cli (< v0.9.0) used 'approve' as the allow-once | |
| // optionId. Keep accepting it so custom ACP clients built against the | |
| // old SDK are not silently rejected. | |
| case 'approve': | |
| return { decision: 'approved' }; | |
| case APPROVE_ALWAYS_OPTION_ID: | |
| // Legacy Python kimi-cli (< v0.9.0) used 'approve_for_session' as the | |
| // allow-always optionId. Same backward-compatibility rationale as the | |
| // 'approve' branch above. | |
| case 'approve_for_session': | |
| return { decision: 'approved', scope: 'session' }; | |
| case REJECT_OPTION_ID: | |
| return { decision: 'rejected' }; | |
| default: | |
| // Unknown optionId — defensive fallback. Reject is safer than approve. | |
| return { decision: 'rejected' }; | |
| } | |
| } | |
| function mapPlanReviewOptionId( | |
| display: Extract<ApprovalRequest['display'], { kind: 'plan_review' }>, | |
| optionId: string, | |
| ): ApprovalResponse { | |
| if (optionId === PLAN_APPROVE_OPTION_ID) { | |
| return { decision: 'approved' }; | |
| } | |
| if (optionId === PLAN_REVISE_OPTION_ID) { | |
| return { decision: 'rejected', selectedLabel: 'Revise' }; | |
| } | |
| if (optionId === PLAN_REJECT_AND_EXIT_OPTION_ID) { | |
| return { decision: 'rejected', selectedLabel: 'Reject and Exit' }; | |
| } | |
| const match = /^plan_opt_(\d+)$/.exec(optionId); | |
| if (match) { | |
| const i = Number(match[1]); | |
| const opts = display.options; | |
| if (opts !== undefined && Number.isInteger(i) && i >= 0 && i < opts.length) { | |
| return { decision: 'approved', selectedLabel: opts[i]!.label }; | |
| } | |
| return { decision: 'rejected' }; | |
| } | |
| return { decision: 'rejected' }; | |
| } | |
| /** | |
| * Build the ACP {@link ToolCallUpdate} that scopes a permission request to a | |
| * specific in-flight tool call. | |
| * | |
| * The `toolCallId` is the prefixed ACP wire id `${turnId}:${rawId}` — matching | |
| * the id format used by all other tool_call/tool_call_update notifications — | |
| * so the client can correlate the approval prompt with the tool card it | |
| * already rendered. If `req.turnId` is `undefined` the raw id is used as a | |
| * defensive fallback (in practice approvals always fire after | |
| * `tool.call.started`, so the fallback is effectively unreachable). | |
| * | |
| * Content shape: | |
| * - If `req.display` produces a diff-bearing entry, prepend it so the diff / | |
| * plan card is the headline of the approval prompt. | |
| * - Always append a human-readable action summary | |
| * (`"Requesting approval to ${req.action}"`) so the prompt is never empty. | |
| */ | |
| export function buildPermissionToolCallUpdate(req: ApprovalRequest): ToolCallUpdate { | |
| const rawId = req.toolCallId ?? req.toolName; | |
| const toolCallId = req.turnId !== undefined ? acpToolCallId(req.turnId, rawId) : rawId; | |
| const content: ToolCallContent[] = []; | |
| const headlineEntry = displayBlockToAcpContent(req.display); | |
| if (headlineEntry !== null) { | |
| content.push(headlineEntry); | |
| } | |
| content.push({ | |
| type: 'content', | |
| content: { type: 'text', text: `Requesting approval to ${req.action}` }, | |
| }); | |
| return { | |
| toolCallId, | |
| title: req.toolName, | |
| content, | |
| }; | |
| } | |
| /** | |
| * Look up the matched {@link PermissionOption}'s display name for the given | |
| * response and return a new {@link ApprovalResponse} carrying `selectedLabel`. | |
| * Returns the input unchanged when the outcome was `cancelled`, the optionId | |
| * is unknown, or it is in the `plan_*` namespace (the plan_review branch | |
| * attaches `selectedLabel` inside the mapper already). | |
| * | |
| * Pure: returns a fresh object (never mutates the input). | |
| */ | |
| export function attachSelectedLabel( | |
| response: RequestPermissionResponse, | |
| approval: ApprovalResponse, | |
| options: readonly PermissionOption[], | |
| ): ApprovalResponse { | |
| const outcome = response.outcome; | |
| if (outcome.outcome !== 'selected') return approval; | |
| if ( | |
| outcome.optionId.startsWith('plan_opt_') || | |
| outcome.optionId === PLAN_APPROVE_OPTION_ID || | |
| outcome.optionId === PLAN_REVISE_OPTION_ID || | |
| outcome.optionId === PLAN_REJECT_AND_EXIT_OPTION_ID | |
| ) { | |
| return approval; | |
| } | |
| const matched = options.find((o) => o.optionId === outcome.optionId); | |
| if (!matched) return approval; | |
| return { ...approval, selectedLabel: matched.name }; | |
| } | |