feat(acp): --full-access flag for serve --acp, accepted after the subcommand

Probing the installed 0.10.2 binary over ACP showed three things:
- `codewhale serve --acp` runs under Ask;
- `codewhale --yolo serve --acp` runs under Full Access;
- `codewhale serve --acp --yolo` is rejected ("unexpected argument", exit 2).
That last form is the one editors write in their agent config. Founder
decision: Full Access is yolo (ApprovalMode::Bypass). A configured
`sandbox_mode = "danger-full-access"` must not widen Ask or Auto-Review, and
no state may have the sandbox off while the agent still asks.

Changes:
- `--full-access` is the visible flag; `--yolo` stays an alias
  (runtime_options.rs).
- The dispatcher lifts `--full-access` and `--yolo` out of the `serve`
  passthrough arguments (`capture_serve_full_access`), stopping at a `--`
  separator. It sets the same runtime option as the global flag, so the
  value still flows through the CODEWHALE_YOLO env layer, and managed
  config above that layer still wins. Setting `config.yolo` after load
  would have bypassed a managed `yolo = false`.
- `serve --acp` writes one stderr line naming the ACP permission and
  sandbox it will run with, computed with `sandbox_policy_for_turn`. When
  Full Access is off, the line says how to turn it on.
- The ACP hint and the approval_policy "never" error name `--full-access`.
- `SandboxPolicy::config_value()` replaces a duplicated match in
  `apply_requirements`.
- docs/MODES.md, docs/RUNTIME_API.md and CHANGELOG are updated.

Verified:
- `scripts/dev-cargo.sh build -p codewhale-cli --locked`: built, 0
  warnings.
- The built binary was probed over ACP. For each launch, a fresh
  CODEWHALE_HOME and runtime dir; then `initialize`, `session/new`, and a
  read of the `permission` configOption:
  - `serve --acp`: ask. stderr: "ACP permission: ask; sandbox:
    workspace-write. For Full Access start with `codewhale serve --acp
    --full-access` ...".
  - `--full-access serve --acp`: full-access, with sandbox
    danger-full-access.
  - `serve --acp --full-access`: full-access.
  - `serve --acp --yolo`: full-access.
- nextest codewhale-cli, filtered: 36 passed, including
  `clap_command_definition_is_consistent` and
  `yolo_flag_writes_only_the_codewhale_env_var`.
- nextest codewhale-tui, filtered to acp_server, requirements,
  sandbox::policy and runtime_options: 61 passed (38 acp_server, 6
  requirements).
- Not run: clippy, Windows, hosted CI.

The Seatbelt half of #4955 is not done.

Refs #6992 #4955

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-24 22:27:30 -07:00
1 parent 8b5baa7a5f
commit 135319aa1a
8 files changed
+88 -20

No files matched your search

+7
View File
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- `--full-access` is now the documented flag for Full Access (`--yolo` stays an
alias), and `serve` accepts it after the subcommand: `codewhale serve --acp
--yolo` used to fail with "unexpected argument". `serve --acp` also writes one
stderr line naming the permission and sandbox it runs with, and the ACP
permission option's Full Access hint names the working command. A configured
`sandbox_mode = "danger-full-access"` still never widens Ask or Auto-Review
(#6992).
- Preview: `dispatch --computer` starts an account cloud conversation that can
also be opened in desktop. `--thread` continues or inspects it; `--stop`,
`--approve` and `--deny` control its current task. Explicit `--key-stdin`
+18
View File
@@ -364,6 +364,7 @@ Forwarded serve options:
--cors-origin <URL> Additional CORS origin to allow (repeatable)
--auth-token <TOKEN> Require this bearer token for /v1/* runtime API routes
--insecure Disable runtime API auth when no token is configured
--full-access Run with Full Access: no approval prompts, no Codewhale sandbox (alias: --yolo)
`codewhale serve --http` and `codewhale serve --mobile` remain compatibility
aliases for `codewhale app-server --http` and `codewhale app-server --mobile`.
@@ -2283,6 +2284,7 @@ fn run() -> Result<()> {
);
preserve_exec_separator(&mut cli, &argv);
capture_exec_startup_options(&mut cli)?;
capture_serve_full_access(&mut cli);
// The detached log proxy must not depend on user config parsing: its job
// is to frame child output and publish a terminal receipt even when the
@@ -2986,6 +2988,22 @@ fn preserve_exec_separator(cli: &mut Cli, argv: &[impl AsRef<std::ffi::OsStr>])
}
}
fn capture_serve_full_access(cli: &mut Cli) {
let Some(Commands::Serve(args)) = cli.command.as_mut() else {
return;
};
let before = args.args.len();
let separator = args.args.iter().position(|arg| arg == "--").unwrap_or(before);
let mut index = 0;
args.args.retain(|arg| {
index += 1;
index > separator || !matches!(arg.as_str(), "--full-access" | "--yolo")
});
if args.args.len() != before {
cli.runtime_options.yolo = true;
}
}
/// Admit exec's recognized startup options through the same Clap definitions
/// before the one runtime override capture. Unknown forwarded options and all
/// tokens after `--` retain their order and their existing exec meaning.
+37 -9
View File
@@ -3,6 +3,7 @@
//! This module keeps only connection configuration, canonical bindings and
//! JSON-RPC correlation. Secure cross-process attachment is not yet qualified.
use crate::config::{Config, ProviderKind};
use crate::core::authority::{SandboxNetworkAccess, sandbox_policy_for_turn};
#[cfg(test)]
use crate::runtime_threads::RuntimeThreadManagerConfig;
use crate::runtime_threads::{CreateThreadRequest, RuntimeThreadManager, UpdateThreadRequest};
@@ -11,7 +12,7 @@ use codewhale_config::AppMode;
use codewhale_execpolicy::ApprovalMode;
use serde_json::{Value, json};
use std::collections::{HashMap, VecDeque};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(test)]
@@ -32,6 +33,7 @@ pub async fn run_acp_server(
config_profile: Option<String>,
) -> Result<()> {
ensure_acp_approval_posture(&config)?;
eprintln!("{}", acp_startup_posture(&config, &default_cwd));
crate::runtime_api::run_http_server(
config,
default_cwd,
@@ -628,12 +630,7 @@ impl AcpServer {
Ok(())
}
fn permission_value(&self) -> &'static str {
match acp_approval_mode(&self.config) {
ApprovalMode::Bypass => "full-access",
ApprovalMode::Auto => "auto-review",
ApprovalMode::Never => "never",
ApprovalMode::Suggest => "ask",
}
acp_permission_value(&self.config)
}
async fn session_configuration(
&self,
@@ -839,7 +836,38 @@ impl AcpServer {
Ok(self.current_model())
}
}
const ACP_FULL_ACCESS_HINT: &str = "Start the server with `codewhale --yolo serve --acp`, or set `yolo = true` in config.toml. Full Access also turns off Codewhale's own sandbox unless sandbox_mode tightens it; Plan stays read-only.";
const ACP_FULL_ACCESS_HINT: &str = "Start the server with `codewhale serve --acp --full-access` (alias `--yolo`), or set `yolo = true` in config.toml. Full Access also turns off Codewhale's own sandbox unless sandbox_mode tightens it; Plan stays read-only.";
fn acp_permission_value(config: &Config) -> &'static str {
match acp_approval_mode(config) {
ApprovalMode::Bypass => "full-access",
ApprovalMode::Auto => "auto-review",
ApprovalMode::Never => "never",
ApprovalMode::Suggest => "ask",
}
}
fn acp_startup_posture(config: &Config, workspace: &Path) -> String {
let approval = acp_approval_mode(config);
let sandbox = sandbox_policy_for_turn(
acp_mode(config),
approval,
config.sandbox_mode.as_deref(),
workspace,
SandboxNetworkAccess::from_config(config.sandbox_network_access),
);
let mut line = format!(
"ACP permission: {}; sandbox: {}.",
acp_permission_value(config),
sandbox.config_value()
);
if approval != ApprovalMode::Bypass {
line.push_str(
" For Full Access start with `codewhale serve --acp --full-access` or set `yolo = true`; sandbox_mode alone never turns it on.",
);
}
line
}
fn acp_mode(config: &Config) -> AppMode {
if config.sandbox_mode.as_deref() == Some("read-only") {
@@ -869,7 +897,7 @@ fn acp_approval_mode(config: &Config) -> ApprovalMode {
fn ensure_acp_approval_posture(config: &Config) -> Result<()> {
anyhow::ensure!(
acp_approval_mode(config) != ApprovalMode::Never,
"approval_policy \"never\" is not supported by `codewhale serve --acp`: ACP sessions run under Ask, Auto-Review or Full Access. Set approval_policy = \"on-request\" for this server, or start it with --yolo for Full Access."
"approval_policy \"never\" is not supported by `codewhale serve --acp`: ACP sessions run under Ask, Auto-Review or Full Access. Set approval_policy = \"on-request\" for this server, or start it with --full-access for Full Access."
);
Ok(())
}
+3 -8
View File
@@ -11114,7 +11114,6 @@ fn apply_requirements(config: &mut Config) -> Result<()> {
.map(str::to_ascii_lowercase)
.unwrap_or_else(|| {
use crate::core::authority::{SandboxNetworkAccess, sandbox_policy_for_turn};
use crate::sandbox::SandboxPolicy;
use codewhale_execpolicy::ApprovalMode;
// Sandbox requirements lock approval posture at startup,
@@ -11124,18 +11123,14 @@ fn apply_requirements(config: &mut Config) -> Result<()> {
.as_deref()
.and_then(ApprovalMode::from_config_value)
.unwrap_or_default();
match sandbox_policy_for_turn(
sandbox_policy_for_turn(
codewhale_config::AppMode::Agent,
approval,
None,
Path::new("."),
SandboxNetworkAccess::from_config(config.sandbox_network_access),
) {
SandboxPolicy::ReadOnly { .. } => "read-only",
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write",
SandboxPolicy::DangerFullAccess => "danger-full-access",
SandboxPolicy::ExternalSandbox { .. } => "external-sandbox",
}
)
.config_value()
.to_string()
});
if !requirements
+4 -3
View File
@@ -45,8 +45,9 @@ pub struct RuntimeOptions {
/// Disable a feature for this run (repeatable).
#[arg(long = "disable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
pub disable: Vec<String>,
/// Legacy compatibility alias for Act + Full Access.
#[arg(long, hide = true)]
/// Run with Full Access: no approval prompts and no Codewhale sandbox
/// (the same as `yolo = true`; `--yolo` is an alias).
#[arg(long = "full-access", alias = "yolo")]
pub yolo: bool,
/// Maximum number of concurrent sub-agents (1-128; default 64).
#[arg(long)]
@@ -97,7 +98,7 @@ impl RuntimeOptions {
flag!(skip_onboarding, "--skip-onboarding");
flag!(fresh, "--fresh");
flag!(no_project_config, "--no-project-config");
flag!(yolo, "--yolo");
flag!(yolo, "--full-access");
flag!(verbose, "--verbose");
flag!(remote_control, "--remote-control");
if self.mouse_capture && self.no_mouse_capture {
+9
View File
@@ -105,6 +105,15 @@ impl Default for SandboxPolicy {
}
impl SandboxPolicy {
pub fn config_value(&self) -> &'static str {
match self {
SandboxPolicy::ReadOnly { .. } => "read-only",
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write",
SandboxPolicy::DangerFullAccess => "danger-full-access",
SandboxPolicy::ExternalSandbox { .. } => "external-sandbox",
}
}
pub fn read_only() -> Self {
SandboxPolicy::ReadOnly {
network_access: false,
+1
View File
@@ -354,6 +354,7 @@ Run `codewhale --help` for the canonical list. Common flags:
- `-r, --resume <ID|PREFIX|latest>`: resume a saved session
- `-c, --continue`: resume the most recent session in this workspace
- `--max-subagents <N>`: clamp to `1..=128`
- `--full-access` (alias `--yolo`): start with Full Access, no approval prompts and no Codewhale sandbox; the same as `yolo = true`. `serve --acp --full-access` and `--full-access serve --acp` are equivalent.
- `--mouse-capture` / `--no-mouse-capture`: opt in or out of internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. Mouse capture is enabled by default on non-Windows terminals and on Windows Terminal/ConEmu/Cmder so drag selection copies only transcript text, removes visual wrap-column line breaks from paragraphs, and stays scoped to the transcript pane; hold Shift while dragging or use `--no-mouse-capture` for raw terminal selection. It defaults off on legacy Windows console (CMD without `WT_SESSION` / `ConEmuPID`) and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where the terminal advertises mouse support but forwards SGR mouse events as raw text (#878, #898). Use `--mouse-capture` to opt in anywhere it's defaulted off. Raw terminal selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.
- `--profile <NAME>`: select config profile
- `--config <PATH>`: config file path
+9
View File
@@ -785,6 +785,15 @@ posture are server-owned read-only options; the editor cannot widen them.
`approval_policy = "never"` is not an ACP posture, so the server refuses to
start with it instead of serving it as Ask.
Full Access is `yolo`. Start it with `codewhale serve --acp --full-access`
(the flag may also come before `serve`; `--yolo` is an alias) or set
`yolo = true` in config. A configured `sandbox_mode = "danger-full-access"` does
not turn it on: that value can only narrow the sandbox the permission picks, so
under Ask or Auto-Review it changes nothing, and `ask` is not a config key. At
startup the server writes one line to stderr naming the permission and sandbox it
will run with, so an editor's agent log shows which posture a launch command
produced.
Tool updates begin `pending`. `in_progress` means Core reached final dispatch;
`completed`/`failed` and typed image blocks come from the actual Core result.
`session/request_permission` correlates a private JSON-RPC ID to the same