Auto mode: re-key GovernanceSurface + privilege clamp off server-derived execution_mode
Problem / context
!249463 (merged) keys the local-surface GovernanceSurface and the privilege clamp off the client-sent environment, which is spoofable: a client can assert its own environment to pick a different rule column or dodge the clamp (an unrecognized/absent value falls through to :web, the most permissive path). Per ADR-008, before governance flags go GA the surface and clamp must derive from non-spoofable, server-derived signals.
The substrate exists: !248339 (merged) shipped the sealed FlowExecutionAuthorizer::Classification, and !250574 (merged) persists it as the execution_mode column.
Design (resolved 2026-08-20)
Two server-derived signals, kept separate (not collapsed into one):
execution_mode(binary: client / background) = trust + interactivity.client= a human is present / interactive (includes the interactive web UI);background= unattended (CI). Drives the privilege clamp.surface(3-way: web / local / background) = which policy column applies (web_access/local_access/background_access). Kept 3-way so the web UI retains its own distinct policy column (web_access), separate from CLI/IDE (local_access).
This chunk does NOT collapse surface selection onto the binary execution_mode. It makes the existing 3-way surface selection server-derived (removing the spoofable client-environment dependency) and uses execution_mode as the trust clamp. web_access stays a live, distinct column.
Verified code facts: GovernanceSurface maps web/ambient to :web (→ web_access, has ask) unless the background flag is on and the flow is allowlisted, then :background (→ background_access, ask→allow); ide/chat/chat_partial → local_access. Naming trap: environment: web/ambient are labeled "background environments" (web = legacy ambient), NOT the interactive web UI. Interactive Duo Chat comes in as chat/chat_partial → local_access today.
Caveat: the interactive web UI must be derivable server-side as the web surface (and as client execution_mode), not via spoofable client environment or caller_can_execute (the web path currently hardwires caller_can_execute: false).
Scope (Rails)
- Make the 3-way surface selection server-derived instead of reading client
environment. Precursor call sites to retarget: thegovernance_surface = (workflow && workflow.environment) || params[:environment].presence || :webline inworkflows.rb; theenvironmentthreading inworkflow_context_generation_service.rb/create_and_start_workflow_service.rb. - Use the persisted
execution_mode(from !250574 (merged)) as the trust clamp inupdate_agent_privileges_service.rb/create_workflow_service.rb. - Read the signals at the
:wstoken mint (which reads the persisted workflow row, not the creation-time classification). - Consolidate the duplicated surface-resolution logic into a single authority, removing the scattered
|| :webfallbacks (this also resolves the external-surface split-brain, #618093). - Preserve
web_accessas a live, distinct column; do not fold web intolocal_access. - Handle pre-flip rows: flag-off client-executable runs persist
execution_mode: background(see the !250574 (merged) discussion onnilvsbackground).
Dependencies
- !250574 (merged) (persisted
execution_mode) must land first. - Pre-GA blocker on rollout #613544; overlaps with #618093 (external-surface split-brain).
Acceptance criteria
- Surface selection and the privilege clamp derive from server-side signals, not client
environment. - A client asserting
environmentcan no longer dodge the clamp or pick its own column. web_access,local_access,background_accessall remain reachable; web keeps a distinct policy column.- A single surface-resolution authority; no scattered
|| :webfallbacks. - Signals read correctly at the
:wsmint; pre-flipbackgroundrows handled.