P3: cut the config to sandbox and config to core edges

Two of the six remaining config-cluster edges to other tui modules, both one-way:

- Config::read_denylist and Config::effective_sandbox_denied_read_paths are deleted.
  ReadDenylist::from_config(&Config) in sandbox/read_guard.rs builds the same list
  from the same three fields, and its four callers use it directly (exec_agent,
  runtime_threads x2, ui/frame) plus lib.rs, which now asks the list for
  subtree_paths(). The deny-list doc comment moved with the constructor.
- apply_requirements no longer calls core::authority::sandbox_policy_for_turn just to
  name the Agent baseline. ApprovalMode::agent_baseline_sandbox_mode() in
  codewhale-execpolicy returns "danger-full-access" for Full Access and
  "workspace-write" otherwise, apply_requirements reads it, and
  sandbox_policy_for_turn derives its non-Plan default from the same method, so the
  baseline is still defined once. The network argument the old call passed never
  affected the returned config value. The comment that said "the engine's resolver" now
  says where the baseline comes from.

Measured after this change (re-run, not carried over): largest SCC still 79 modules
(this cut does not fragment it); config now has 7 outbound module edges instead of 9.
Remaining non-cluster edges from config/config_persistence: plugins::providers x5,
provider_readiness x2, hooks::HooksConfig x1, codex_model_cache::model_roster_for x1.
tui production lines with inline tests stripped: 640,313 (old metric incl. inline tests:
948,634+); runtime 10,109; the monolith is unchanged in size.

Verified locally (macOS; no hosted CI): CLI build exit 0; nextest -p codewhale-execpolicy
-p codewhale-tui --lib focused set: 992 run, 992 passed, 0 failed, 14546 skipped (read_guard
26, read_denylist 1, sandbox 125, requirements 6, sandbox_policy_for_turn 1, config::tests
423, authority 202, execpolicy 236); workspace clippy with CI's exact flags exits 0 on the
final tree (two needless borrows it found in lib.rs and ui/frame.rs were fixed after the
test run; those are token-only changes, covered by clippy's compile, tests not re-run for
them); fmt clean; dead-code 209, blocking 543/165, boundary 121.

Not verified: a live sandboxed shell under the OS wrapper (only the existing unit tests that
build the deny-list and the policy table), Linux/Windows, hosted CI.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Sonnet 5.5 committed 2026-10-25 11:32:46 -07:00
1 parent 65ef1ea70b
commit d4d50f730d
8 files changed
+39 -42

No files matched your search

+8
View File
@@ -44,6 +44,14 @@ impl ApprovalMode {
}
}
#[must_use]
pub fn agent_baseline_sandbox_mode(self) -> &'static str {
match self {
Self::Bypass => "danger-full-access",
Self::Auto | Self::Suggest | Self::Never => "workspace-write",
}
}
#[must_use]
pub fn cycle_permission_next(self) -> Self {
let Some(index) = Self::PERMISSION_CYCLE.iter().position(|mode| *mode == self) else {
+3 -36
View File
@@ -3239,31 +3239,6 @@ pub(crate) fn approval_policy_baseline_from_permission_posture(
// === Config Loading ===
impl Config {
/// The read deny-list in force for this config (S1).
///
/// Unions the built-in credential-store defaults (unless
/// `sandbox_read_denylist_defaults = false`) with
/// `sandbox_denied_read_paths`, minus `sandbox_read_denylist_exempt`.
/// Feeds both the in-process file-reading tools and the OS sandbox
/// wrappers. A `read_file` call refuses every rule; a shell `cat` refuses
/// only the rules scoped to the process, which excludes a tool's own auth
/// config.
#[must_use]
pub fn read_denylist(&self) -> crate::sandbox::read_guard::ReadDenylist {
crate::sandbox::read_guard::ReadDenylist::build(
self.sandbox_read_denylist_defaults.unwrap_or(true),
&self.sandbox_denied_read_paths,
&self.sandbox_read_denylist_exempt,
)
}
/// Every path the OS sandbox wrappers should deny reads under — the
/// deny-list's subtree rules, home-expanded and normalized.
#[must_use]
pub fn effective_sandbox_denied_read_paths(&self) -> Vec<std::path::PathBuf> {
self.read_denylist().subtree_paths()
}
/// Whether Linux shell commands prefer bubblewrap confinement.
///
/// On by default: an unset `prefer_bwrap` means sandboxed commands use
@@ -10822,14 +10797,14 @@ fn apply_requirements(config: &mut Config) -> Result<()> {
}
if !requirements.allowed_sandbox_modes.is_empty() {
// At config load there is no live turn mode yet. Check the Agent
// baseline with the engine's resolver; Plan can narrow it later.
// baseline that the engine's resolver derives from the same approval
// posture; Plan can narrow it later.
// Explicit settings retain their existing allow-list check.
let mode = config
.sandbox_mode
.as_deref()
.map(str::to_ascii_lowercase)
.unwrap_or_else(|| {
use crate::core::authority::{SandboxNetworkAccess, sandbox_policy_for_turn};
use codewhale_execpolicy::ApprovalMode;
// Sandbox requirements lock approval posture at startup,
@@ -10839,15 +10814,7 @@ fn apply_requirements(config: &mut Config) -> Result<()> {
.as_deref()
.and_then(ApprovalMode::from_config_value)
.unwrap_or_default();
sandbox_policy_for_turn(
codewhale_config::AppMode::Agent,
approval,
None,
Path::new("."),
SandboxNetworkAccess::from_config(config.sandbox_network_access),
)
.config_value()
.to_string()
approval.agent_baseline_sandbox_mode().to_string()
});
if !requirements
.allowed_sandbox_modes
+3 -1
View File
@@ -359,7 +359,9 @@ pub(crate) fn sandbox_policy_for_turn(
) -> SandboxPolicy {
let default = if mode == AppMode::Plan {
SandboxPolicy::read_only()
} else if approval_mode == ApprovalMode::Bypass {
} else if approval_mode.agent_baseline_sandbox_mode()
== SandboxPolicy::DangerFullAccess.config_value()
{
SandboxPolicy::DangerFullAccess
} else {
workspace_write_policy(workspace, network_access)
+1 -1
View File
@@ -659,7 +659,7 @@ pub(crate) async fn run_exec_agent(
read_only_roots: execution_config.bwrap_ro_roots.clone(),
device_roots: execution_config.bwrap_dev_roots.clone(),
},
read_denylist: execution_config.read_denylist(),
read_denylist: crate::sandbox::read_guard::ReadDenylist::from_config(&execution_config),
memory_enabled: execution_config.memory_enabled(),
memory_path: execution_config.memory_path(),
speech_output_dir: execution_config.speech_output_dir(),
+3 -1
View File
@@ -12633,7 +12633,9 @@ fn run_sandbox_command(config: &Config, args: SandboxArgs) -> Result<()> {
read_only_roots: config.bwrap_ro_roots.clone(),
device_roots: config.bwrap_dev_roots.clone(),
});
manager.set_denied_read_subpaths(config.effective_sandbox_denied_read_paths());
manager.set_denied_read_subpaths(
crate::sandbox::read_guard::ReadDenylist::from_config(config).subtree_paths(),
);
let exec_env = manager.prepare(&spec);
if spec.sandbox_policy.should_sandbox() && !exec_env.is_sandboxed() {
bail!(
+2 -2
View File
@@ -16668,7 +16668,7 @@ impl RuntimeThreadManager {
read_only_roots: cfg.bwrap_ro_roots.clone(),
device_roots: cfg.bwrap_dev_roots.clone(),
},
read_denylist: cfg.read_denylist(),
read_denylist: crate::sandbox::read_guard::ReadDenylist::from_config(&cfg),
memory_enabled: !isolated_chat && cfg.memory_enabled(),
memory_path: cfg.memory_path(),
speech_output_dir: cfg.speech_output_dir(),
@@ -17038,7 +17038,7 @@ impl RuntimeThreadManager {
read_only_roots: cfg.bwrap_ro_roots.clone(),
device_roots: cfg.bwrap_dev_roots.clone(),
},
cfg.effective_sandbox_denied_read_paths(),
crate::sandbox::read_guard::ReadDenylist::from_config(&cfg).subtree_paths(),
)
};
let mut active = self.active.lock().await;
+18
View File
@@ -217,6 +217,24 @@ impl ReadDenylist {
Self::default()
}
/// The read deny-list in force for a config (S1).
///
/// Unions the built-in credential-store defaults (unless
/// `sandbox_read_denylist_defaults = false`) with
/// `sandbox_denied_read_paths`, minus `sandbox_read_denylist_exempt`.
/// Feeds both the in-process file-reading tools and the OS sandbox
/// wrappers. A `read_file` call refuses every rule; a shell `cat` refuses
/// only the rules scoped to the process, which excludes a tool's own auth
/// config.
#[must_use]
pub(crate) fn from_config(config: &crate::config::Config) -> Self {
Self::build(
config.sandbox_read_denylist_defaults.unwrap_or(true),
&config.sandbox_denied_read_paths,
&config.sandbox_read_denylist_exempt,
)
}
/// Build the effective deny-list.
///
/// * `include_defaults` — apply the built-in credential-store set.
+1 -1
View File
@@ -1045,7 +1045,7 @@ pub(crate) fn build_engine_config(app: &App, config: &Config) -> EngineConfig {
read_only_roots: config.bwrap_ro_roots.clone(),
device_roots: config.bwrap_dev_roots.clone(),
},
read_denylist: config.read_denylist(),
read_denylist: crate::sandbox::read_guard::ReadDenylist::from_config(config),
memory_enabled: config.memory_enabled(),
memory_path: config.memory_path(),
speech_output_dir: config.speech_output_dir(),