feat: add onPermissionRequest callback for host-driven per-call gating#299
Open
DaniAkash wants to merge 4 commits intoopenclaw:mainfrom
Open
feat: add onPermissionRequest callback for host-driven per-call gating#299DaniAkash wants to merge 4 commits intoopenclaw:mainfrom
DaniAkash wants to merge 4 commits intoopenclaw:mainfrom
Conversation
Adds an optional `onPermissionRequest` hook to `AcpRuntimeOptions` and
`AcpClientOptions`. When set, every per-call permission request the
agent emits is routed through the host's callback first; returning a
decision short-circuits the existing mode-based resolver, returning
`undefined` (or throwing) falls through to it.
Existing CLI users and programmatic consumers see zero behavior change
— the option is undefined by default, and the mode-based path is
preserved verbatim. Hosts that want to render approve/deny UI can now
opt in without reaching into acpx internals.
The wire shape:
type AcpPermissionRequest = {
sessionId: string;
raw: RequestPermissionRequest; // full ACP request, untouched
inferredKind: ToolKind | "other"; // best-effort classification
}
type AcpPermissionDecision =
| { outcome: "approve_once" }
| { outcome: "approve_always" }
| { outcome: "deny" }
| { outcome: "cancel" };
A new `decisionToResponse` helper in `src/permissions.ts` maps the
decision to the right ACP `RequestPermissionResponse` shape based on
the options the agent advertised — hosts never see `optionId` strings.
The existing `inferToolKind` is now exported so hosts can render
kind-aware UI without re-implementing the keyword classifier.
The client gate in `handlePermissionRequest` runs the callback before
the existing mode-based resolver, exposes a per-session `AbortSignal`
the callback can honor, and falls through cleanly on `undefined` /
thrown errors. The signal aborts when the session is cancelled so
in-flight host UI can short-circuit. Throws are logged and don't take
down the turn.
Driving consumer: `acpx-ai-provider` needs this to allow approve /
deny options on the Vercel AI SDK so it can be integrated with UI
components built with the Vercel AI SDK
(https://www.npmjs.com/package/acpx-ai-provider). Other tools can
leverage it the same way.
Tests: 5 new unit cases in `test/permissions.test.ts` cover
`decisionToResponse` option-mapping; 4 new integration cases in
`test/client.test.ts` cover the gate (decision honored / undefined
fallthrough / thrown error fallthrough / abort signal propagation).
`pnpm run check` passes locally — 613 tests, coverage 84.16% lines /
78.46% branches / 88.98% functions, all above thresholds.
- drop JSDoc blocks on new types and helpers to match the surrounding zero-JSDoc convention used by `permissionModeSatisfies`, `resolvePermissionRequest`, `classifyPermissionDecision`, etc. - inline the callback signature in `AcpClientOptions`, `AcpRuntimeOptions`, and `WithConnectedSessionOptions` to mirror the inline shape used by `onSessionUpdate`, `onClientOperation`, etc.; drop the `AcpOnPermissionRequest` type alias and its re-exports. - rename decision outcomes from `approve_once | approve_always | deny | cancel` to `allow_once | allow_always | reject_once | reject_always | cancel`, mapping 1:1 to the SDK's `PermissionOption["kind"]` strings. This removes the parallel vocabulary and lets `decisionToResponse` collapse to a single fallback-order table lookup. - expose `inferredKind` as `ToolKind | undefined` (matching `inferToolKind`'s own return convention) instead of substituting `"other"`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds an optional
onPermissionRequestcallback toAcpRuntimeOptionsandAcpClientOptions. When set, the host receives every per-call permission request the agent emits and can resolve it with its own UI; when unset (the default), behavior is identical to today's mode-based resolver.Why
Today's permission surface (
permissionMode+nonInteractivePermissions) is the right primitive for CLI use, but it's coarse for UI-driven hosts. A browser extension or IDE integration that wants to render an inline approve/deny card forapply_patch/ shell / delete has to either:approve-alland skip approval entirely (unsafe), orapprove-reads+nonInteractivePermissions: "deny"and watch every write get silently denied with no signal to the user.A callback hook gives those hosts a clean, opt-in way to gate per-call without changing any defaults. The mode-based resolver remains the fallback — it still runs when the callback returns
undefined, throws, or isn't set.This change is purely additive: zero behavior change for any existing consumer.
Wire shape
The host receives:
…and returns:
…or
undefinedto fall through. The decision outcomes map 1:1 to the SDK'sPermissionOption["kind"]strings, so a newdecisionToResponsehelper insrc/permissions.tsresolves them against whatever options the agent advertised — hosts never seeoptionIdstrings. The existinginferToolKindhelper is now exported so hosts can render kind-aware UI without re-implementing the keyword classifier.The gate sits at the top of
AcpClient.handlePermissionRequest, before the existingresolvePermissionRequestcall. Throws inside the host callback are caught + logged + fall through to the mode-based resolver, so a buggy host UI can't take down an agent turn. A per-sessionAbortSignalis exposed to the callback and aborts when the session is cancelled or the client is reset.Tests
test/permissions.test.tscoveringdecisionToResponseoption mapping (allow_once/allow_alwayspreference order, fallback when one allow kind is absent,reject_once→reject_alwaysfallback,cancelalways cancels, missing-option cancel).test/client.test.tscovering the gate (decision honored over mode,undefinedfallthrough, thrown error fallthrough, abort signal aborts on session cancel).pnpm run check(format + typecheck + lint + build + viewer + test:coverage) passes locally — 613 tests, coverage 84.09 / 78.46 / 88.98 (above 83 / 76 / 86 thresholds).Changelog
Entry added under
## Unreleased→### Changesper AGENTS.md.Driving consumer
acpx-ai-providerneeds this to allow approve / deny options on the Vercel AI SDK so it can be integrated with UI components built with the Vercel AI SDK. Other tools can leverage it the same way.