mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
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:
1 parent
65ef1ea70b
commit
d4d50f730d
8 files changed
+39
-42
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user