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

View File

@@ -669,3 +669,27 @@ describe('reference stability (the memo contract)', () => {
expect(resolved.pending).toBe(after.pending)
})
})
describe('permissions / setPermission', () => {
it('passes the select read and switch through with the session id', async () => {
const { api, session } = makeSession()
api.onPermissions = () => Promise.resolve(ok({ options: [{ value: 'workspace-write', name: 'workspace-write' }], currentValue: 'workspace-write' }))
const read = await session.permissions()
expect(read.ok).toBe(true)
if (read.ok) expect(read.value.currentValue).toBe('workspace-write')
expect(api.callsOf('session.permissions')).toMatchObject([{ sessionId: SID }])
const switched = await session.setPermission('danger-full-access')
expect(switched.ok).toBe(true)
if (switched.ok) expect(switched.value.currentValue).toBe('danger-full-access')
expect(api.callsOf('session.setPermission')).toMatchObject([{ sessionId: SID, value: 'danger-full-access' }])
})
it('folds transport failures into the error branch', async () => {
const { api, session } = makeSession()
api.onPermissions = () => Promise.reject(new Error('down'))
api.onSetPermission = () => Promise.reject(new Error('down'))
expect((await session.permissions()).ok).toBe(false)
expect((await session.setPermission('x')).ok).toBe(false)
})
})