mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
feat(exec): --add-dir, and a full usage journal no longer fails a turn
`codewhale exec --add-dir <path>` (repeatable) adds an external root the file tools may read and write, without Full Access. The path must exist and be a directory. EngineConfig carries the roots into the tool context. When the native usage journal reaches its per-owner cap, the request now runs with cost unknown instead of failing before dispatch on "native usage journal capacity reached". Acknowledged receipts are pruned first. Verification: cargo check -p codewhale-tui --lib and cargo check -p codewhale-engine --lib passed. No focused runtime test for --add-dir yet.
This commit is contained in:
1 parent
6a6d9bf298
commit
be43ea93fd
8 files changed
+46
-4
No files matched your search
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`codewhale exec --add-dir <path>`** (repeatable) lets the file tools read and write a sibling directory, a second checkout or a shared scratch root without turning on Full Access.
|
||||
- **A full usage journal no longer fails a turn before dispatch.** The request runs with cost unknown instead of stopping on “native usage journal capacity reached”.
|
||||
- **Reverting a file works in any posture.** Restoring a file the thread's own turns changed no longer requires `/trust` or Full Access; the exact snapshot and reviewed-hash checks still apply. **`fetch_url` can reach a local or private host you allowed** with `/network allow <host>`; cloud metadata stays blocked. Both refusals now name the setting.
|
||||
- **`codewhale exec` offers tools by default**, still under the configured sandbox and approval posture. `--no-tools` is the one-shot. A pipe on stdin is the prompt when no arguments are given, or is appended to them.
|
||||
- **A child no longer dies when usage settlement is not acknowledged.** The turn continues and cost stays unknown, matching the parent path.
|
||||
|
||||
@@ -730,6 +730,7 @@ pub async fn run_exec_agent(
|
||||
},
|
||||
verbosity: execution_config.verbosity.clone(),
|
||||
workspace_follow_symlinks: settings.workspace_follow_symlinks,
|
||||
extra_trusted_paths: execution_config.extra_trusted_paths.clone(),
|
||||
exec_policy_engine: execution_config.exec_policy_engine.clone(),
|
||||
terminal_chrome_enabled: false,
|
||||
advisor_config: execution_config
|
||||
|
||||
@@ -574,6 +574,9 @@ pub struct EngineConfig {
|
||||
/// itself must be inside the workspace). Mirrors the
|
||||
/// `workspace_follow_symlinks` setting.
|
||||
pub workspace_follow_symlinks: bool,
|
||||
/// Extra roots the file tools may read and write outside the workspace,
|
||||
/// set by `codewhale exec --add-dir`.
|
||||
pub extra_trusted_paths: Vec<PathBuf>,
|
||||
/// Ask-only permission rules loaded from sibling `permissions.toml`.
|
||||
pub exec_policy_engine: codewhale_execpolicy::ExecPolicyEngine,
|
||||
/// Whether turn startup may write terminal title/taskbar OSC sequences.
|
||||
@@ -696,6 +699,7 @@ impl Default for EngineConfig {
|
||||
verbosity: None,
|
||||
tools: None,
|
||||
workspace_follow_symlinks: false,
|
||||
extra_trusted_paths: Vec::new(),
|
||||
exec_policy_engine: codewhale_execpolicy::ExecPolicyEngine::new(Vec::new(), Vec::new()),
|
||||
terminal_chrome_enabled: true,
|
||||
session_checkpoint_backend: None,
|
||||
@@ -8471,6 +8475,11 @@ impl Engine {
|
||||
let trusted =
|
||||
codewhale_runtime::workspace_trust::WorkspaceTrust::load_for(&self.session.workspace);
|
||||
let mut trusted_external_paths = trusted.paths().to_vec();
|
||||
for path in &self.config.extra_trusted_paths {
|
||||
if !trusted_external_paths.iter().any(|trusted| trusted == path) {
|
||||
trusted_external_paths.push(path.clone());
|
||||
}
|
||||
}
|
||||
let clipboard_images_dir =
|
||||
codewhale_runtime::config::clipboard_images_dir(&self.session.workspace);
|
||||
if !trusted_external_paths
|
||||
|
||||
@@ -1479,6 +1479,7 @@ impl NativeUsageSource {
|
||||
let _scope = bind_test_cost_scope(slot.test_origin);
|
||||
with_runtime_usage_journal_mut(|journal| {
|
||||
let entry = journal.entry(owner.to_string()).or_default();
|
||||
entry.records.retain(|record| !record.is_acknowledged());
|
||||
let occupied = occupied_runtime_usage_sources(entry);
|
||||
if let Some(current) = entry
|
||||
.native_slots
|
||||
@@ -1508,10 +1509,12 @@ impl NativeUsageSource {
|
||||
if !occupied.contains(&slot.source_fingerprint)
|
||||
&& occupied.len() >= MAX_RUNTIME_USAGE_RECORDS_PER_OWNER
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::WouldBlock,
|
||||
"native usage journal capacity reached before dispatch",
|
||||
));
|
||||
tracing::warn!(
|
||||
owner,
|
||||
occupied = occupied.len(),
|
||||
"native usage journal is at capacity; this request continues without a retained receipt"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
slot.active
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
|
||||
@@ -14524,6 +14524,7 @@ source_usage_records: Default::default(),
|
||||
tools: (!isolated_chat).then(|| cfg.tools.clone()).flatten(),
|
||||
verbosity: cfg.verbosity.clone(),
|
||||
workspace_follow_symlinks: settings.workspace_follow_symlinks,
|
||||
extra_trusted_paths: Vec::new(),
|
||||
exec_policy_engine: cfg.exec_policy_engine.clone(),
|
||||
terminal_chrome_enabled: false,
|
||||
advisor_config: cfg
|
||||
|
||||
@@ -1981,6 +1981,10 @@ pub struct Config {
|
||||
>,
|
||||
#[serde(skip)]
|
||||
pub required_account_model_owner: Option<String>,
|
||||
/// Runtime-only extra roots the tool layer may read and write outside the
|
||||
/// workspace, set by `codewhale exec --add-dir`.
|
||||
#[serde(skip)]
|
||||
pub extra_trusted_paths: Vec<std::path::PathBuf>,
|
||||
/// Persisted exact-route declarations, separate from provider credentials.
|
||||
#[serde(
|
||||
default,
|
||||
@@ -10046,6 +10050,11 @@ fn apply_profile(config: ConfigFile, profile: Option<&str>) -> Result<Config> {
|
||||
fn merge_config(base: Config, override_cfg: Config) -> Config {
|
||||
Config {
|
||||
custom_models: override_cfg.custom_models.or(base.custom_models),
|
||||
extra_trusted_paths: if override_cfg.extra_trusted_paths.is_empty() {
|
||||
base.extra_trusted_paths
|
||||
} else {
|
||||
override_cfg.extra_trusted_paths
|
||||
},
|
||||
provider: override_cfg.provider.or(base.provider),
|
||||
telemetry: override_cfg.telemetry.or(base.telemetry),
|
||||
http_headers: override_cfg.http_headers.or(base.http_headers),
|
||||
|
||||
@@ -423,6 +423,9 @@ struct ExecArgs {
|
||||
/// surface under the configured sandbox and approval posture.
|
||||
#[arg(long, default_value_t = false)]
|
||||
no_tools: bool,
|
||||
/// Additional directory the file tools may read and write (repeatable).
|
||||
#[arg(long = "add-dir", value_name = "PATH")]
|
||||
add_dir: Vec<PathBuf>,
|
||||
/// Read the prompt from a file (`-` reads stdin) instead of argv, for
|
||||
/// prompts past the OS per-argument limit (~128 KiB on Linux).
|
||||
#[arg(long, value_name = "PATH", conflicts_with = "prompt")]
|
||||
@@ -2804,6 +2807,19 @@ async fn run_async_main_dispatch(
|
||||
"persisted Models.dev cache load did not complete; keeping bundled"
|
||||
);
|
||||
}
|
||||
if !args.add_dir.is_empty() {
|
||||
let mut extra = Vec::with_capacity(args.add_dir.len());
|
||||
for path in &args.add_dir {
|
||||
let canonical = path.canonicalize().with_context(|| {
|
||||
format!("--add-dir {} does not exist", path.display())
|
||||
})?;
|
||||
if !canonical.is_dir() {
|
||||
bail!("--add-dir {} is not a directory", path.display());
|
||||
}
|
||||
extra.push(canonical);
|
||||
}
|
||||
config.extra_trusted_paths = extra;
|
||||
}
|
||||
let prompt = resolve_exec_prompt(&args)?;
|
||||
let resume_session_id = resolve_exec_resume_session_id(&args, &workspace)?;
|
||||
validate_exec_tool_authority_resume(
|
||||
|
||||
@@ -1058,6 +1058,7 @@ pub(crate) fn build_engine_config(app: &App, config: &Config) -> EngineConfig {
|
||||
subagent_state_root: None,
|
||||
allow_shell: app.allow_shell,
|
||||
trust_mode: app.trust_mode,
|
||||
extra_trusted_paths: config.extra_trusted_paths.clone(),
|
||||
notes_path: config.notes_path(),
|
||||
mcp_config_path: config.mcp_config_path(),
|
||||
mcp_auth_context: Default::default(),
|
||||
|
||||
Reference in new issue
Block a user