Skip to content

Commit b0c5241

Browse files
committed
feat(config,tui): confirmed opt-out for model-bound key redaction
The model boundary masks credential-shaped tool output before it reaches an upstream model (configured secrets plus a CredentialShaped structural pass). Editing files that contain real credentials has always been awkward because the model cannot quote those bytes back. This adds a deliberate, documented opt-out that treats lowering the boundary as a security decision: - config.toml gains [redaction] model_bound = "enabled" (default) | "disabled"; "disabled" only records a request. - After a restart the interactive TUI shows a full-screen startup gate (same explicit-key discipline as workspace trust: 1/Y confirm, 2/U keep, 3/N/Esc quit; Enter never confirms by reflex). - Confirming persists a receipt to ~/.codewhale/redaction-state.json and rebuilds the engine so its client applies the opt-out. Until a receipt exists every process - including headless/exec runs - stays masked. - The client opt-out covers structured ToolResult blocks only; routing/ classification summaries and durable goal-state text keep always-on redaction. - Gate copy is localized across all 15 shipped locale packs; enum, JSON, and ALL_MESSAGE_IDS stay in sync.
1 parent aedb88b commit b0c5241

28 files changed

Lines changed: 1417 additions & 4 deletions

config.example.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,33 @@ memory_path = "~/.codewhale/memory.md"
206206
# max_reprompts = 2
207207
# reprompt_message = "So, what's up ? Keep running !"
208208

209+
# ───────────────────────────────────────────────────────────────────────────
210+
# Model-bound key redaction ([redaction])
211+
# ───────────────────────────────────────────────────────────────────────────
212+
# Codewhale masks credential-looking values in tool output before it reaches
213+
# the model (the "model boundary"), so a file that contains a configured API
214+
# key, a bare provider token, or a credential-shaped opaque string never leaks
215+
# those bytes to the model. Leave this enabled unless the model must read and
216+
# edit files that contain real credentials.
217+
#
218+
# Disabling is a security decision, so it is never a plain flag:
219+
# * Set model_bound = "disabled" here, restart Codewhale, and the startup
220+
# gate asks twice - a first confirmation, then a red "are you really
221+
# sure?" stage. Only the second confirmation takes effect, and it applies
222+
# on later launches while model_bound stays "disabled".
223+
# * Going back to "enabled" - or rewriting config.toml after the
224+
# confirmation - invalidates it: requesting "disabled" again always
225+
# asks for a fresh confirmation.
226+
# * Until a confirmation exists - including in non-interactive/headless
227+
# runs, which never confirm anything - masking stays on regardless of
228+
# this key. Choosing "keep masking on" on the gate leaves the key
229+
# untouched, so the next launch asks again.
230+
# * The value is forgiving: false/"off" mean "disabled"; true/"on" mean
231+
# "enabled".
232+
# [redaction]
233+
# model_bound = "enabled" # mask keys before they reach the model (default)
234+
# model_bound = "disabled" # request the opt-out (restart + confirm required)
235+
209236
# Native tool catalog controls (#2076). By default only the core tool surface
210237
# is loaded into the model context; less common native tools are discoverable
211238
# through ToolSearch and loaded on first use.

crates/config/src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod provider;
1414
mod provider_defaults;
1515
mod provider_kind;
1616
pub mod provider_templates;
17+
pub mod redaction;
1718
pub mod resolve;
1819
pub mod route;
1920
pub mod settings_schema;
@@ -920,10 +921,29 @@ pub struct ConfigToml {
920921
/// [`WorkflowConfigToml::default`].
921922
#[serde(default)]
922923
pub workflow: Option<WorkflowConfigToml>,
924+
/// Model-bound credential redaction policy (`[redaction]`). When absent,
925+
/// masking is enabled — the shipped security default.
926+
#[serde(default, skip_serializing_if = "Option::is_none")]
927+
pub redaction: Option<crate::redaction::RedactionToml>,
923928
#[serde(flatten)]
924929
pub extras: BTreeMap<String, toml::Value>,
925930
}
926931

932+
impl ConfigToml {
933+
/// The requested model-bound masking mode, defaulting to enabled.
934+
///
935+
/// The request only takes effect once the interactive TUI has recorded a
936+
/// confirmation on its startup gate; see
937+
/// [`crate::redaction::effective_masking`].
938+
#[must_use]
939+
pub fn redaction_model_bound_masking(&self) -> crate::redaction::ModelBoundMasking {
940+
self.redaction
941+
.as_ref()
942+
.map(crate::redaction::RedactionToml::model_bound_masking)
943+
.unwrap_or_default()
944+
}
945+
}
946+
927947
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
928948
enum ProviderConfigField {
929949
ApiKey,

0 commit comments

Comments
 (0)