Add layered settings, and fix a migration mechanism that silently lied

Two things, because the second could not be built on the first.

The schema stamp was part of the same execute_batch as the CREATE TABLE IF NOT
EXISTS statements, and it wrote unconditionally. Opening an older file therefore
added no columns but flipped the version forward anyway; opening a *newer* file
stamped it back down and then wrote rows the newer build could not read. Both
produced a database whose recorded version was a lie, and every future schema
change would have inherited it.

Now the version is read before anything is applied, migrations are ordered and
forward-only inside one transaction, a newer file is refused with SchemaTooNew
rather than downgraded, and a supported version raised without a step to reach
it fails at the first open instead of claiming success. Tested by stamping a
file at version 99 and asserting both the refusal and that the stamp is left
untouched.

lumbridge-settings resolves compiled default -> settings.toml -> environment.
The environment sits above the file deliberately: decision 0016 calls
LUMBRIDGE_CLAUDE_OAUTH=0 "one switch off", and a switch a config file can
silently re-enable is not a switch. A pinned value renders disabled and names
the variable, rather than accepting an edit that would do nothing.

Every field carries a WriteAuthority. Routing all writes through Configure is
the obvious design and would hand a layout-only agent the program every future
pane launches — the guarantee decision 0006 exists to make. Anything naming a
program, path or destination is Human-only, asserted by a test that reads the
path rather than trusting the author.

Four paths are permanently not settings, with the reason recorded beside each
and a test asserting their absence: the usage endpoint URL, the credentials
path, the client identity, and the shell program. A configuration file that can
redirect where an access token is sent is a credential exfiltration path with a
friendly name.

Environment access is a trait rather than std::env, because the workspace forbids
unsafe, set_var is unsafe in Rust 2024, and the layering rule has to be testable
without mutating the process running the test.

Verified live with LUMBRIDGE_CLAUDE_OAUTH=0: the account-endpoint row reads off,
greyed, "pinned by LUMBRIDGE_CLAUDE_OAUTH". The Advanced page names every file,
endpoint and child process Lumbridge touches and states that nothing is sent
anywhere else — as a fact, not as a toggle nobody can flip.

File loading, comment-preserving writes and editable controls are not in this
pass; 0022 records why that order is the honest one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Metal Agent
2026-09-01 00:06:06 -07:00
co-authored by Claude Opus 5
parent 72887cb4ab
commit e5d7a3efd5
14 changed files with 1431 additions and 14 deletions
+236
View File
@@ -0,0 +1,236 @@
//! The settings pane.
//!
//! Four pages that describe code which actually runs. Every row says three
//! things a settings interface usually leaves out: where the value came from,
//! when a change takes effect, and who is allowed to make it.
//!
//! A control whose value was pinned by the environment is drawn disabled with
//! the variable named, rather than accepting an edit that would silently do
//! nothing — decision 0016 calls that switch "one switch off", and an interface
//! that lets a file or a click override it has quietly broken the promise.
use gpui::prelude::*;
use gpui::{div, px};
use lumbridge_settings::{Page, SETTINGS, SettingItem, Settings, WriteAuthority};
use crate::theme::ThemeColors;
/// Everything one row needs to say.
struct Row<'a> {
item: &'a SettingItem,
value: String,
origin: String,
editable: bool,
}
/// One row: what it is, where it came from, when it applies.
fn setting_row(row: &Row<'_>, theme: ThemeColors) -> gpui::AnyElement {
let Row {
item,
value,
origin,
editable,
} = row;
let (label, detail, takes_effect, authority) = (
item.label,
item.detail,
item.takes_effect.label(),
item.authority,
);
let editable = *editable;
div()
.flex()
.flex_col()
.gap(px(2.0))
.px_3()
.py_2()
.border_b_1()
.border_color(theme.border_quiet)
.child(
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.flex_1()
.min_w_0()
.truncate()
.text_sm()
.text_color(if editable { theme.text } else { theme.muted })
.child(label.to_owned()),
)
.child(
div()
.flex_none()
.px(px(5.0))
.rounded(px(3.0))
.text_xs()
.bg(theme.surface_raised)
.text_color(if editable { theme.accent } else { theme.muted })
.child(value.to_owned()),
),
)
.child(
div()
.text_xs()
.text_color(theme.muted)
.child(detail.to_owned()),
)
.child(
div()
.flex()
.gap_2()
.text_xs()
.text_color(theme.muted)
.child(origin.to_owned())
.child("·")
.child(takes_effect.to_owned())
.when(authority == WriteAuthority::Human, |view| {
view.child("·").child(
div()
.text_color(theme.attention)
.child("a person must set this"),
)
}),
)
.into_any_element()
}
/// Renders one page.
pub(crate) fn page(page: Page, settings: &Settings, theme: ThemeColors) -> gpui::AnyElement {
let value_for = |path: &str| -> (String, String, bool) {
match path {
"appearance.theme" => (
settings.theme.value.clone(),
settings.theme.origin.label(),
settings.theme.origin.is_editable(),
),
"appearance.accent" => (
settings.accent.value.clone(),
settings.accent.origin.label(),
settings.accent.origin.is_editable(),
),
"usage.claude_account_endpoint" => (
on_off(settings.claude_account_endpoint.value),
settings.claude_account_endpoint.origin.label(),
settings.claude_account_endpoint.origin.is_editable(),
),
"usage.claude_transcripts" => (
on_off(settings.claude_transcripts.value),
settings.claude_transcripts.origin.label(),
settings.claude_transcripts.origin.is_editable(),
),
"usage.codex_app_server" => (
on_off(settings.codex_app_server.value),
settings.codex_app_server.origin.label(),
settings.codex_app_server.origin.is_editable(),
),
"usage.claude_rate_limit_feed" => (
settings
.claude_rate_limit_feed
.value
.clone()
.unwrap_or_else(|| "wherever the bridge was installed".to_owned()),
settings.claude_rate_limit_feed.origin.label(),
settings.claude_rate_limit_feed.origin.is_editable(),
),
"terminal.allow_osc52_clipboard" => (
on_off(settings.allow_osc52_clipboard.value),
settings.allow_osc52_clipboard.origin.label(),
settings.allow_osc52_clipboard.origin.is_editable(),
),
_ => ("".to_owned(), "unknown".to_owned(), false),
}
};
let rows: Vec<gpui::AnyElement> = SETTINGS
.iter()
.filter(|item| item.page == page)
.map(|item| {
let (value, origin, editable) = value_for(item.path);
setting_row(
&Row {
item,
value,
origin,
editable,
},
theme,
)
})
.collect();
div()
.flex()
.flex_col()
.size_full()
.overflow_hidden()
.children(rows)
.when(page == Page::Advanced, |view| {
view.child(what_lumbridge_reads(settings, theme))
})
.into_any_element()
}
fn on_off(value: bool) -> String {
if value { "on" } else { "off" }.to_owned()
}
/// The Advanced page's disclosure.
///
/// Stated as fact rather than offered as a toggle. "No telemetry" is not a
/// setting anyone can turn on, so presenting it as one would be theatre.
fn what_lumbridge_reads(settings: &Settings, theme: ThemeColors) -> gpui::AnyElement {
let reads = [
"~/.claude/projects — session transcripts, four token counters per record",
"the status-line feed — the two subscription windows, written by the installed bridge",
"~/.claude/.credentials.json — the access token, only when the account endpoint is on",
"GET api.anthropic.com/api/oauth/usage — your own account, identified as lumbridge",
"codex app-server — launched as a child, asked only for its quota windows",
];
div()
.flex()
.flex_col()
.gap(px(2.0))
.px_3()
.py_2()
.child(
div()
.text_xs()
.text_color(theme.muted)
.child("WHAT LUMBRIDGE READS"),
)
.children(reads.into_iter().map(|line| {
div()
.text_xs()
.text_color(theme.muted)
.child(line.to_owned())
}))
.child(
div()
.mt_2()
.text_xs()
.text_color(theme.success)
.child("Nothing is sent anywhere else. There is no telemetry to turn off."),
)
.when(!settings.unknown.is_empty(), |view| {
view.child(
div()
.mt_2()
.text_xs()
.text_color(theme.attention)
.child(format!(
"{} key(s) in settings.toml were not understood by this build and were left alone: {}",
settings.unknown.len(),
settings
.unknown
.iter()
.map(|key| key.path.clone())
.collect::<Vec<_>>()
.join(", ")
)),
)
})
.into_any_element()
}