VS Code ACP-Shaped Permission Bridge Plan
Status: implemented — landed as
runtime/vscodeagent/approval.go(ApprovalBroker) oversession/request_permission; kept as the design record.
Problem
The VS Code bridge currently treats runtime approvals as a notification plus a later approvalRespond request. That is too weak for Contenox HITL semantics. If the editor fails to render or answer the approval UI, the runtime can keep reasoning and choose another tool path that the active policy may allow. The approval boundary is therefore not represented as a blocking protocol operation.
The fix is not to hardcode tool names in the extension. Tool identity and arguments already exist in the runtime hitlservice.ApprovalRequest and task events. The VS Code extension should render the permission request it receives and return a decision. The runtime remains the policy owner.
ACP Lessons To Reuse
libacp.AgentSideConnection.callis the correct shape for permission flow: allocate a request id, store a pending response channel under a mutex, write the request, block on response/context/connection close, and remove the pending entry on every exit path.session/request_permissionis a reverse request from agent/runtime to client/editor. It carriessessionId, a structuredtoolCall, and explicitoptions.runtime/acpsvc.Transport.AskApprovalconverts a ContenoxApprovalRequestinto ACPRequestPermissionRequest, callsRequestPermission, and treats cancelled or malformed outcomes as not approved.permPendinginruntime/acpsvcsuppresses normal tool-call updates while a permission card owns that tool call. This avoids a pending/completed tool card racing or replacing the approval UI.normalizeToolCallNotificationpreserves monotonic tool status. A laterpendingupdate must not rewind an already in-progress/completed card.- ACP does not require the editor to know the complete tool catalog. It renders the concrete tool call supplied by the runtime.
Target VS Code Bridge Dialect
Keep contenox vscode-agent --stdio and the current Content-Length JSON-RPC framing for the extension process. Replace the active approval subprotocol with an ACP-shaped reverse JSON-RPC request:
{
"jsonrpc": "2.0",
"id": 42,
"method": "session/request_permission",
"params": {
"sessionId": "contenox-session-id",
"toolCall": {
"toolCallId": "call-1",
"title": "local_shell.local_shell: python3",
"kind": "execute",
"status": "pending",
"rawInput": {"command": "python3"}
},
"options": [
{"optionId": "allow", "name": "Allow", "kind": "allow_once"},
{"optionId": "deny", "name": "Deny", "kind": "reject_once"}
]
}
}
The extension responds:
{"jsonrpc":"2.0","id":42,"result":{"outcome":{"outcome":"selected","optionId":"allow"}}}
or:
{"jsonrpc":"2.0","id":42,"result":{"outcome":{"outcome":"cancelled"}}}
Implementation Steps
- Add shared ACP permission builders in Go so
acpsvcandvscodeagentuse the samelibacp.RequestPermissionRequestconstruction for titles, raw input, diff content, and tool kind. - Add server-initiated request support to
runtime/vscodeagent.Server: numeric ids, pending response map, response routing inRun, context cleanup, and fail-closed behavior on write/close/cancel. - Change
ApprovalBrokerto callsession/request_permissionsynchronously instead of sendingapprovalRequestednotifications. Delete the oldapprovalRespondcompatibility path once the TypeScript bridge uses the blocking request. - Add VS Code
BridgeClienthandling for incoming JSON-RPC requests. Dispatchsession/request_permission, call an active chat approval handler, and send a JSON-RPC response. If no handler exists or the UI fails, answercancelled. - Wire the chat turn’s
toolInvocationTokeninto the active approval handler for the duration of the turn. This keeps native VS Code approval UI tied to the current chat request and prevents approvals outside a chat turn from silently succeeding. - Add a VS Code agent permission-pending guard mirroring ACP so normal
toolCallnotifications do not render over the approval request for the same session/tool call. - Patch shipped interactive HITL policies so
local_shell.local_shelldefaults toapprove, notallow. A permissive shell fallback defeats the whole HITL boundary. - Add focused tests:
- Go: server-initiated request routing, approval allow/deny/cancel, close/cancel cleanup, and tool-call suppression while permission is pending.
- TypeScript:
BridgeClienthandlessession/request_permission, responds selected/cancelled, and fails closed without a handler. - Policy: dev/default/acp policies do not auto-allow a plain shell command such as
python3.
Manual Smoke Test
- Install/reload the extension build.
- Select an interactive HITL policy.
- Ask
@contenoxto create a file in the workspace. A permission card should block before execution. - Deny it. The file must not appear, and the assistant must report that the tool was denied.
- Ask
@contenoxto create a file in/tmpor$HOME. A shell fallback must also ask for approval, not silently runpython3. - Check
~/.contenox/vscode-telemetry.logforruntime.server_request.start,chat.approval.requested, andchat.approval.respondedentries.
Non-Goals
- Do not replace
vscode-agent --stdiowith the full ACP process in this pass. - Do not make the extension discover and enforce the tool catalog. Policy and tool identity stay in the runtime.
- Do not rely on telemetry as enforcement. Telemetry proves what happened; the blocking request is the enforcement.