feat(app): --readonly flag blocks every write op with a READONLY badge#7
feat(app): --readonly flag blocks every write op with a READONLY badge#7posquit0 wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a --readonly CLI flag and a corresponding read-only mode for the TUI application. When enabled, this mode blocks all write operations—such as edit forms, status pickers, and destructive palette commands—and displays a canonical toast message while showing a 'READONLY' warning badge in the status row. The feedback identifies a potential bypass where shared.OpenScreenMsg lacks the isWriteScreen read-only check, which could allow navigation to write screens in read-only mode.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| case SwitchScreenMsg: | ||
| if s, ok := screenFromName(msg.Target); ok { | ||
| if isWriteScreen(s) { | ||
| if blocked, cmd := m.blockedByReadOnly(); blocked { | ||
| return m, cmd | ||
| } | ||
| } |
There was a problem hiding this comment.
While SwitchScreenMsg and ScreenChangeMsg are properly guarded with isWriteScreen checks, shared.OpenScreenMsg (handled right below at line 798) also resolves targets via screenFromName (which can return write screens like ScreenUserEdit or ScreenGroupEdit) but lacks the isWriteScreen read-only check. To prevent potential bypasses where a cross-screen drill-down could navigate to a write screen in read-only mode, please add the same guard to shared.OpenScreenMsg.
QW-2 — new global --readonly CLI flag threads a ReadOnly bool through
cmd/ota → app.Deps and gates every UI write-flow entry point in the
App Shell reducer (edit forms, status pickers, palette write commands
like :reset-password / :unlock / :reset-mfa, and any palette route
targeting a *Edit screen). Blocked ops surface a single canonical
toast ("read-only mode — write ops disabled") and the chrome stamps
a Warning-toned READONLY badge for the session so operators see the
guardrail at a glance.
Read flows (list, detail, search, filter, logs tail) stay 100%
functional — this is a pure UI gate, no business-logic changes.
The OpenScreenMsg cross-screen drill-down path resolved targets via
screenFromName, which happily returns write screens like ScreenUserEdit
and ScreenGroupEdit. Without the isWriteScreen / blockedByReadOnly guard
that SwitchScreenMsg and ScreenChangeMsg already apply, a drill-down
targeting "user-edit" bypassed --readonly and pushed the edit form onto
the nav stack. Apply the same gate here and add a regression test that
dispatches OpenScreenMsg{Target:"user-edit"} under ReadOnly:true and
asserts the active screen does not change plus a read-only toast fires.
f794d04 to
607e499
Compare
Summary
--readonlyCLI flag threads aReadOnlybool throughcmd/ota→app.Depsand gates every UI write-flow entry point in the App Shell reducer (edit forms, all status pickers, palette write commands like:reset-password/:unlock/:reset-mfa, and any palette route targeting a*Editscreen).read-only mode — write ops disabled) via the existingtoastCmdInfopath.[READONLY: on]badge for the session so operators see the guardrail at a glance.Implementation notes
Model.blockedByReadOnly()is the single decision point every gate calls; returns(bool, tea.Cmd)so the pattern stays uniform across handlers.isWriteScreen(Screen)covers the palette /SwitchScreenMsgpaths for:edit,:group-edit, etc."on"because bareValuecollapses to a Muted[KEY]rendering and drops the Tone — seeshared.formatStatusBadge.Test plan
go build ./...cleango test ./... -race -count=1cleaninternal/app/readonly_test.goregression:OpenUserEditMsgandOpenStatusPickerMsgblocked with toast, palette:reset-passwordblocked, chrome View stamps the badge, default (ReadOnly=false) omits the badge.cmd/ota/readonly_test.gosmoke:run --readonly --versionexits 0 (flag registered, no "flag provided but not defined").