feat(web): permission presets and approval answering for the web UI

The web host now composes the sandboxed product path (sandbox-local +
sandbox-policy behind bash-sandbox/fs-sandbox, with user-approval and
permission on top); BootHostOptions.sandbox carries the deployment
defaults (workspace-write + ask).

createApiProxy owns the approval pending registry: a ctx.approval ask
becomes an answerable approval/requested mux frame with a stable rpcId,
replayed verbatim on every mux open until settled; respond routes by the
echoed rpcId, validates the ApprovalResponsePayload audit correlation,
and broadcasts approval/resolved; the ask's abort signal withdraws the
question as cancelled.

session.permissions / session.setPermission project ctx.permission into
a protocol-owned PermissionOption select; idle switches are held
last-write-wins and
flushed into the next prompted turn (the ACP bridge's anchoring
pattern). The shared hasOpenTurn fold moved to dsh-session,
deduplicating the private copies in user-approval, the ACP bridge, and
the proxy.

Client, per the designer draft: a pending approval takes over the
composer (ApprovalPanel replaces the InputBar — amber strip,
justification headline, paired command, one-shot refuse/allow, keyed by
rpcId so a queued second approval remounts live; the resolved frame
restores the composer); the sidebar session row shows an amber
waiting-approval dot that outranks the running ring (manager-tracked
approvalId set, idempotent under mux-open replays, cleared per
connection generation, lit for uninstantiated sessions too); the
permission selector is a composer bottom-row chip over an invisible
native select, with a presentation-only title-case transform
(workspace-write renders as Workspace Write; wire names untouched). Question placeholders stay in the message flow. The
connection fixture mirrors the host behavior for keyless browser
acceptance.
This commit is contained in:
Turtle
2026-07-24 13:39:00 +08:00
parent 0133e80767
commit f0410d592d
69 changed files with 1744 additions and 139 deletions
@@ -19,7 +19,9 @@ import {
sessionCreateValueSchema,
sessionHistoryValueSchema,
sessionListValueSchema,
sessionPermissionsValueSchema,
sessionPromptValueSchema,
sessionSetPermissionValueSchema,
} from '../api/sessions.schema.ts'
/**
@@ -44,6 +46,8 @@ export interface IApiClient {
history(payload: RequestPayload<'session.history'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.history'>>>
prompt(payload: RequestPayload<'session.prompt'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.prompt'>>>
cancel(payload: RequestPayload<'session.cancel'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.cancel'>>>
permissions(payload: RequestPayload<'session.permissions'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.permissions'>>>
setPermission(payload: RequestPayload<'session.setPermission'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'session.setPermission'>>>
}
host: {
describe(payload: RequestPayload<'host.describe'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'host.describe'>>>
@@ -66,6 +70,8 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseV
'session.history': sessionHistoryValueSchema,
'session.prompt': sessionPromptValueSchema,
'session.cancel': sessionCancelValueSchema,
'session.permissions': sessionPermissionsValueSchema,
'session.setPermission': sessionSetPermissionValueSchema,
'host.describe': hostDescribeValueSchema,
}
@@ -247,6 +253,8 @@ export abstract class AbstractApiClient implements IApiClient {
history: (payload, signal) => this.callUnary('session.history', payload, signal),
prompt: (payload, signal) => this.callUnary('session.prompt', payload, signal),
cancel: (payload, signal) => this.callUnary('session.cancel', payload, signal),
permissions: (payload, signal) => this.callUnary('session.permissions', payload, signal),
setPermission: (payload, signal) => this.callUnary('session.setPermission', payload, signal),
}
readonly host: IApiClient['host'] = {
@@ -19,7 +19,9 @@ import {
sessionCreateRequestSchema,
sessionHistoryRequestSchema,
sessionListRequestSchema,
sessionPermissionsRequestSchema,
sessionPromptRequestSchema,
sessionSetPermissionRequestSchema,
} from '../api/sessions.schema.ts'
import { hostDescribeRequestSchema } from '../api/host.schema.ts'
@@ -43,6 +45,8 @@ const UNARY_ROUTES: UnaryRoutes = {
'session.history': { schema: sessionHistoryRequestSchema, invoke: (api, r) => api.sessions.history(r) },
'session.prompt': { schema: sessionPromptRequestSchema, invoke: (api, r) => api.sessions.prompt(r) },
'session.cancel': { schema: sessionCancelRequestSchema, invoke: (api, r) => api.sessions.cancel(r) },
'session.permissions': { schema: sessionPermissionsRequestSchema, invoke: (api, r) => api.sessions.permissions(r) },
'session.setPermission': { schema: sessionSetPermissionRequestSchema, invoke: (api, r) => api.sessions.setPermission(r) },
'host.describe': { schema: hostDescribeRequestSchema, invoke: (api, r) => api.host.describe(r) },
}