ADR 0055: Agent-only mode; Chat becomes an internal read-only profile
- Status: Superseded by ADR 0052 / ADR 0053
- Date: 2026-08-05
Superseded. The mode concept this ADR removed returned as the Plan operating state (ADR 0052, superseded in turn by ADR 0053), so the product again exposes an
Agent | Planselector andchatmigrates toplan, notagent. Retained because the reasoning about a negative permission gate and about failing closed on an unknownmodevalue still holds, and because it records why the toggle was removed before it was reintroduced with a different contract.
Context
Since D003/D004 a session carried one of two tool profiles. agent got the full coding surface (Read/Bash/Edit/Write/Glob/Grep/BrowserPreview plus plugin tools); chat got a read-only subset (Read/Glob/Grep) that host-core hard-denies past every permission mode (D115).
The product is an agent desktop. chat was never a destination: it existed as a safety valve, but the UI advertised it as a peer choice through a top-bar segmented toggle, a Settings default-mode row, /chat-mode and /agent-mode palette commands, and four localized labels. That had three costs:
- A user who switched to Chat and later removed the toggle from their muscle memory could leave a session permanently unable to write files. With the toggle gone the session would have been stranded with no way back.
- Every tool, prompt-composition, and permission change had to be reasoned about twice, and the lazy tool-activation core sets (D185) forked on mode.
- The mode chip competed for space with the controls users do change — model, thinking level, permission mode.
Deleting the profile outright was not acceptable either: imported sessions and rows written by older builds carry a mode string the current UI does not produce, and the narrow tool surface is a security boundary worth keeping for anything that is not explicitly agent.
Decision
agentis the only mode the product exposes. The top-bar segmented toggle, the Settings default-mode row, both palette commands and their slash aliases, the.ct-mode*styles, and thesettings.mode*i18n keys are removed.newSessionalways requestsagent, and boot normalizes a storeddefaultModethat is notagent.chatis renamedread-only. The shared type becomesMode = "read-only" | "agent". Host-core'sSESSION_MODESis["agent", "read-only"], andnormalize_session_modefolds the pre-D188chatspelling intoread-onlyon every write path (session.create,session.configure,session.import), rejecting anything else. Callers store the normalized value, never the raw input.- The permission gate is negative.
PermissionManagerdenies whenmode != "agent"and the tool is outsideread_only_mode_allows(Read/Glob/Grepplusplugin_*). An unknown or legacymodestring therefore fails closed into the read-only surface instead of silently gaining Write/Edit/Bash. The hard deny continues to outrank every D115 permission mode, includingauto. - The error codes are renamed to
BASH_DISABLED_IN_READ_ONLYandWRITE_DISABLED_IN_READ_ONLY. Neither had a localized message, so the rename is confined topackages/shared/src/errors.ts, the host's code selection, and the specs. - Existing
chatrows are migrated toagentat open.boot_maintenancerunsUPDATE sessions SET mode = 'agent' WHERE mode = 'chat'and folds a storedapp.defaultModeofchatintoagentviajson_set. Both statements are idempotent; after the first open nothing matches.
Consequences
- No session can be stranded read-only: the only rows that can hold
read-onlyare ones a future import or external writer produces, and they keep the narrow, host-enforced surface by design. SCHEMA_VERSIONstays at 7.Database::openarchives-and-resets anything below the current version (D119), so a version bump would have destroyed the very sessions this change exists to rescue. The fix-up is a data repair inside the existing schema instead.- The mode string stays in the RPC contract (
tools.execute,session.configure,SessionSummary) and in thesessions.modecolumn. Removing it would be a protocol break for no gain, and the host still needs it to pick a tool profile. - The conversation top bar keeps only the model picker plus task actions; the composer chip row now leads with Thinking.
ModelSelectremains the writer of the session thinking level on a model switch. - Tool activation (D185) still has two core sets. The read-only one is simply unreachable from the UI.
Alternatives
Drop the read-only profile entirely
Rejected. It would leave imported and legacy rows with a mode the host does not recognize, and the natural fallback for an unrecognized value would be the full agent surface — a silent privilege widening at exactly the wrong moment.
Keep the value spelled chat
Rejected. The name described a product mode that no longer exists and read as a peer of Agent in code, logs, and error codes. read-only says what the profile actually is, which is what the remaining host-side enforcement is for.
Keep the toggle but hide it behind developer mode
Rejected. It preserves both the dual-toolset reasoning cost and the stranding failure mode, and a debug-only path into a security-relevant profile is worse than no path.
Bump the schema version to carry the migration
Rejected. v7 is a breaking reset, not a migration chain (D119): opening a pre-current database archives it and bootstraps a fresh file. A bump would discard the user's sessions to fix one column value in them.
References
packages/shared/src/types.ts,packages/shared/src/errors.tspackages/agent-runtime/src/runtime.tscrates/host-core/src/sessions.rs,crates/host-core/src/permissions.rscrates/host-core/src/db.rs,crates/host-core/src/rpc/mod.rsapps/desktop/src/components/ConversationTopbar.tsxapps/desktop/src/pages/SettingsPage.tsxapps/desktop/electron/main/builtin-commands.tsdocs/spec/03-runtime/03-tools-and-permissions.mddocs/spec/03-runtime/04-data-storage.mddocs/spec/03-runtime/08-error-codes.mddocs/spec/06-delivery/04-e2e-test-plan.md(E2E-018, E2E-088)- Decision D191; amended D003, D004, D115; superseded by D188 / D189
- ADR 0052, ADR 0053