mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 23:56:38 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9c9de775f | ||
|
|
4d2b3fdb1b | ||
|
|
135319aa1a |
No files matched your search
@@ -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`
|
||||
|
||||
+67
-2
@@ -196,6 +196,12 @@ enum CloudAgentsCommand {
|
||||
/// Assign an existing account Project to this Agent.
|
||||
#[arg(long)]
|
||||
project_id: Option<String>,
|
||||
/// Exact configured provider id saved in the whale's Fleet member.
|
||||
#[arg(long, requires = "model")]
|
||||
provider: Option<String>,
|
||||
/// Exact model id on that provider.
|
||||
#[arg(long, requires = "provider")]
|
||||
model: Option<String>,
|
||||
#[arg(long)]
|
||||
operation_key: String,
|
||||
},
|
||||
@@ -632,6 +638,23 @@ struct AccountAgent {
|
||||
#[serde(default)]
|
||||
project_id: String,
|
||||
revision: u64,
|
||||
#[serde(default)]
|
||||
configuration: Option<AgentConfiguration>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentConfiguration {
|
||||
#[serde(default)]
|
||||
revision: u64,
|
||||
#[serde(default)]
|
||||
model: Option<AgentModelRoute>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
struct AgentModelRoute {
|
||||
provider: String,
|
||||
model: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -1176,6 +1199,7 @@ impl<'a, T: CloudTransport> CloudClient<'a, T> {
|
||||
&self,
|
||||
name: &str,
|
||||
project_id: Option<&str>,
|
||||
route: Option<(&str, &str)>,
|
||||
operation_key: &str,
|
||||
) -> Result<AccountAgent> {
|
||||
let name = validate_named_text(name, "Agent name", 80)?;
|
||||
@@ -1184,6 +1208,12 @@ impl<'a, T: CloudTransport> CloudClient<'a, T> {
|
||||
"name": name,
|
||||
"operationKey": operation_key,
|
||||
});
|
||||
if let Some((provider, model)) = route {
|
||||
body["model"] = serde_json::json!({
|
||||
"provider": validate_named_text(provider, "Provider", 256)?,
|
||||
"model": validate_named_text(model, "Model", 256)?,
|
||||
});
|
||||
}
|
||||
if let Some(project_id) = project_id {
|
||||
body["projectId"] = serde_json::json!(validate_resource_id(project_id, "Project")?);
|
||||
}
|
||||
@@ -2313,16 +2343,33 @@ fn run_agents<T: CloudTransport, W: Write>(
|
||||
writeln!(out, "Codewhale Agents ({})", listing.agents.len())?;
|
||||
for agent in listing.agents {
|
||||
validate_resource_id(&agent.id, "Agent")?;
|
||||
writeln!(out, "{} — {}", printable(&agent.name), agent.id)?;
|
||||
match agent
|
||||
.configuration
|
||||
.as_ref()
|
||||
.and_then(|configuration| configuration.model.as_ref())
|
||||
{
|
||||
Some(route) => writeln!(
|
||||
out,
|
||||
"{} — {} ({}/{})",
|
||||
printable(&agent.name),
|
||||
agent.id,
|
||||
printable(&route.provider),
|
||||
printable(&route.model)
|
||||
)?,
|
||||
None => writeln!(out, "{} — {}", printable(&agent.name), agent.id)?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
CloudAgentsCommand::Create {
|
||||
name,
|
||||
project_id,
|
||||
provider,
|
||||
model,
|
||||
operation_key,
|
||||
} => {
|
||||
let agent = client.create_agent(&name, project_id.as_deref(), &operation_key)?;
|
||||
let route = provider.as_deref().zip(model.as_deref());
|
||||
let agent = client.create_agent(&name, project_id.as_deref(), route, &operation_key)?;
|
||||
validate_resource_id(&agent.id, "Agent")?;
|
||||
if let Some(project_id) = project_id.as_deref()
|
||||
&& agent.project_id != validate_resource_id(project_id, "Project")?
|
||||
@@ -2331,6 +2378,24 @@ fn run_agents<T: CloudTransport, W: Write>(
|
||||
}
|
||||
writeln!(out, "Agent: {}", printable(&agent.name))?;
|
||||
writeln!(out, "ID: {}", agent.id)?;
|
||||
if let Some(configuration) = agent.configuration.as_ref()
|
||||
&& let Some(saved) = configuration.model.as_ref()
|
||||
{
|
||||
if route.is_some_and(|(provider, model)| {
|
||||
provider != saved.provider || model != saved.model
|
||||
}) {
|
||||
bail!("The Codewhale service saved a different model than requested");
|
||||
}
|
||||
writeln!(
|
||||
out,
|
||||
"Model: {}/{} (Fleet revision {})",
|
||||
printable(&saved.provider),
|
||||
printable(&saved.model),
|
||||
configuration.revision
|
||||
)?;
|
||||
} else if route.is_some() {
|
||||
bail!("The Codewhale service did not save the requested model");
|
||||
}
|
||||
if !agent.project_id.is_empty() {
|
||||
writeln!(
|
||||
out,
|
||||
|
||||
@@ -58,6 +58,13 @@ pub(crate) struct ComputerOptions {
|
||||
pub prepare_model: bool,
|
||||
#[arg(long, hide = true)]
|
||||
pub expected_account: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "AGENT_ID",
|
||||
conflicts_with_all = ["thread", "cloud_provider", "cloud_model"],
|
||||
help = "Start a cloud conversation with this whale, using its saved model"
|
||||
)]
|
||||
pub whale: Option<String>,
|
||||
}
|
||||
|
||||
impl ComputerOptions {
|
||||
@@ -73,6 +80,7 @@ impl ComputerOptions {
|
||||
|| self.json
|
||||
|| self.configured_model
|
||||
|| self.prepare_model
|
||||
|| self.whale.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +177,7 @@ pub(super) fn run<T: CloudTransport, W: Write>(
|
||||
config: &ConfigStore,
|
||||
secrets: &Secrets,
|
||||
) -> Result<()> {
|
||||
let local = config.config.resolve_runtime_options_with_secrets(
|
||||
let mut local = config.config.resolve_runtime_options_with_secrets(
|
||||
&codewhale_config::CliRuntimeOverrides::default(),
|
||||
secrets,
|
||||
);
|
||||
@@ -242,13 +250,13 @@ pub(super) fn run<T: CloudTransport, W: Write>(
|
||||
if options.thread.is_none() && prompt.is_empty() {
|
||||
bail!("Give a task to dispatch, or continue with --thread <id>.");
|
||||
}
|
||||
if let Some(value) = &options.whale {
|
||||
validate_resource_id(value, "Whale")?;
|
||||
}
|
||||
let use_local_model = options.cloud_provider.is_none()
|
||||
&& !options.key_stdin
|
||||
&& (!prompt.is_empty() || options.prepare_model);
|
||||
if use_local_model {
|
||||
portable_model(&local, config)?;
|
||||
}
|
||||
if options.thread.is_none() && options.cloud_provider.is_none() {
|
||||
if options.thread.is_none() && options.cloud_provider.is_none() && options.whale.is_none() {
|
||||
options.cloud_provider = Some(local.provider.as_str().to_string());
|
||||
options.cloud_model = Some(local.model.clone());
|
||||
}
|
||||
@@ -316,6 +324,49 @@ pub(super) fn run<T: CloudTransport, W: Write>(
|
||||
bail!("This account conversation is unavailable.");
|
||||
}
|
||||
}
|
||||
let whale_id = options.whale.clone().or_else(|| {
|
||||
thread["agentId"]
|
||||
.as_str()
|
||||
.filter(|id| !id.is_empty() && thread["agentFleetRevision"].as_u64().is_some())
|
||||
.map(str::to_string)
|
||||
});
|
||||
let whale_route = match &whale_id {
|
||||
Some(id) => {
|
||||
validate_resource_id(id, "Whale")?;
|
||||
let agent = account.request(HttpMethod::Get, &format!("/api/agents/{id}"), None)?;
|
||||
let route = &agent["agent"]["configuration"]["model"];
|
||||
match (route["provider"].as_str(), route["model"].as_str()) {
|
||||
(Some(provider), Some(model)) if agent["agent"]["id"].as_str() == Some(id) => {
|
||||
validate_model_route(provider, model)?;
|
||||
Some((provider.to_string(), model.to_string()))
|
||||
}
|
||||
_ => bail!(
|
||||
"This whale has no saved model. Give it a configured model in Codewhale before dispatching."
|
||||
),
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if let Some((provider, model)) = &whale_route
|
||||
&& (use_local_model || options.prepare_model)
|
||||
{
|
||||
let kind = codewhale_config::ProviderKind::parse(provider).with_context(|| {
|
||||
format!(
|
||||
"Configure the {provider} provider in Codewhale before dispatching to this whale."
|
||||
)
|
||||
})?;
|
||||
local = config.config.resolve_runtime_options_with_secrets(
|
||||
&codewhale_config::CliRuntimeOverrides {
|
||||
provider: Some(kind),
|
||||
model: Some(model.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
secrets,
|
||||
);
|
||||
portable_model(&local, config)?;
|
||||
} else if use_local_model {
|
||||
portable_model(&local, config)?;
|
||||
}
|
||||
let selected = if options.computer.is_none() && options.thread.is_none() {
|
||||
let list = account.request(HttpMethod::Get, "/api/computers", None)?;
|
||||
let machines = list["computers"]
|
||||
@@ -369,7 +420,7 @@ pub(super) fn run<T: CloudTransport, W: Write>(
|
||||
}
|
||||
if options.thread.is_none() {
|
||||
let value = account.request(HttpMethod::Post, "/v1/threads", Some(json!({
|
||||
"title": prompt.chars().take(120).collect::<String>(), "agentId": "", "computerId": computer,
|
||||
"title": prompt.chars().take(120).collect::<String>(), "agentId": whale_id.as_deref().unwrap_or(""), "computerId": computer,
|
||||
"productMode": "chat", "mode": "chat", "billingMode": "byok_external", "requiresByok": true,
|
||||
"modelProvider": options.cloud_provider, "model": options.cloud_model, "operationKey": operation_key,
|
||||
})))?;
|
||||
@@ -427,6 +478,7 @@ pub(super) fn run<T: CloudTransport, W: Write>(
|
||||
let provider = options
|
||||
.cloud_provider
|
||||
.as_deref()
|
||||
.or(whale_route.as_ref().map(|(provider, _)| provider.as_str()))
|
||||
.or_else(|| latest.and_then(|turn| turn["effective_provider"].as_str()))
|
||||
.or_else(|| {
|
||||
latest_id
|
||||
@@ -438,6 +490,7 @@ pub(super) fn run<T: CloudTransport, W: Write>(
|
||||
let model = options
|
||||
.cloud_model
|
||||
.as_deref()
|
||||
.or(whale_route.as_ref().map(|(_, model)| model.as_str()))
|
||||
.or_else(|| latest.and_then(|turn| turn["effective_model"].as_str()))
|
||||
.or_else(|| {
|
||||
latest_id
|
||||
@@ -446,7 +499,7 @@ pub(super) fn run<T: CloudTransport, W: Write>(
|
||||
.flatten()
|
||||
})
|
||||
.unwrap_or("");
|
||||
let provider_id = if options.cloud_provider.is_some() {
|
||||
let provider_id = if options.cloud_provider.is_some() || whale_route.is_some() {
|
||||
None
|
||||
} else {
|
||||
latest
|
||||
|
||||
@@ -3163,7 +3163,11 @@ fn account_agents_refuse_foreign_threads_ambiguous_main_and_machine_keys() {
|
||||
.contains("interactive Codewhale account login")
|
||||
);
|
||||
assert!(transport.requests().is_empty());
|
||||
assert!(client.create_agent("Whale", None, "invalid/key").is_err());
|
||||
assert!(
|
||||
client
|
||||
.create_agent("Whale", None, None, "invalid/key")
|
||||
.is_err()
|
||||
);
|
||||
assert!(transport.requests().is_empty());
|
||||
|
||||
client
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -61,6 +61,12 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
let plan = update_plan_for_exe(¤t_exe);
|
||||
let channel = ReleaseChannel::from_beta_flag(beta);
|
||||
let current_version = env!("CARGO_PKG_VERSION");
|
||||
let build_version = env!("CODEWHALE_BUILD_VERSION");
|
||||
// The build script marks an unstamped checkout with `(dev)` and nothing
|
||||
// else: a release artifact carries its short SHA and a Cargo source
|
||||
// package carries the plain version. Only the checkout marker means the
|
||||
// installed file has no published artifact behind it.
|
||||
let unreleased_checkout_build = build_version.ends_with(" (dev)");
|
||||
let proxy = proxy_arg
|
||||
.as_deref()
|
||||
.map(validate_and_build_proxy)
|
||||
@@ -68,7 +74,7 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
|
||||
println!("Checking for {} updates...", channel.label());
|
||||
println!("Current binary: {}", current_exe.display());
|
||||
println!("Current version: v{current_version}");
|
||||
println!("Current version: v{build_version}");
|
||||
if legacy_binary {
|
||||
println!();
|
||||
println!("{}", legacy_binary_message(¤t_exe));
|
||||
@@ -94,7 +100,14 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
.with_context(update_network_fallback_hint)?;
|
||||
let latest_tag = &fetched.release.tag_name;
|
||||
println!("Latest {} release: {latest_tag}", channel.label());
|
||||
if update_is_needed(channel, current_version, latest_tag)? {
|
||||
let replace_same_version = unreleased_checkout_build
|
||||
&& compare_release_versions(current_version, latest_tag)? == Ordering::Equal;
|
||||
if update_is_needed(channel, current_version, latest_tag)? || replace_same_version {
|
||||
if replace_same_version {
|
||||
println!(
|
||||
"This build is not a published release ({build_version}); the {latest_tag} release artifact replaces it."
|
||||
);
|
||||
}
|
||||
if install_method.supports_self_update() && !protected_location {
|
||||
println!(
|
||||
"Update available. Run `{} update` to install {latest_tag}.",
|
||||
@@ -138,7 +151,9 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
fetched.source.describe()
|
||||
);
|
||||
}
|
||||
if !update_is_needed(channel, current_version, latest_tag)? {
|
||||
let replace_same_version = unreleased_checkout_build
|
||||
&& compare_release_versions(current_version, latest_tag)? == Ordering::Equal;
|
||||
if !update_is_needed(channel, current_version, latest_tag)? && !replace_same_version {
|
||||
if compare_release_versions(current_version, latest_tag)? == Ordering::Greater {
|
||||
println!(
|
||||
"Current build is newer than the latest published release; keeping v{current_version}. No downgrade or download performed."
|
||||
@@ -148,6 +163,11 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if replace_same_version {
|
||||
println!(
|
||||
"This build is not a published release ({build_version}); installing the {latest_tag} release artifact over it."
|
||||
);
|
||||
}
|
||||
|
||||
// Reject unrelated command paths before downloads or any sibling changes.
|
||||
for target in &plan.target_paths {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -54,6 +54,16 @@ configured model still matches the conversation before transferring a key.
|
||||
without returning a credential. `--prepare-model --thread ID` reconnects that
|
||||
model without starting a turn.
|
||||
|
||||
A whale is a named account collaborator with a saved provider and model.
|
||||
`codewhale account agents create NAME --provider PROVIDER --model MODEL
|
||||
--operation-key KEY` saves one without starting work or a computer, and
|
||||
`codewhale account agents list` shows each whale's saved model.
|
||||
`codewhale dispatch --whale AGENT_ID "your task"` starts a cloud conversation
|
||||
with that whale: the service takes the route from the whale's saved
|
||||
configuration, and your local configuration for that provider supplies the
|
||||
memory-only key. Later `--thread` follow-ups keep using the whale's current
|
||||
saved model; a changed whale applies to the next turn, not to a running one.
|
||||
|
||||
Every send prints a replay key before submission. After an uncertain response,
|
||||
repeat the original command and prompt with `--operation-key KEY`. Keep the
|
||||
original `--computer` target when retrying creation, or the original `--thread`
|
||||
|
||||
+17
-2
@@ -170,7 +170,9 @@ What it does (verified):
|
||||
* It **never uses sudo and never edits your shell profile.** It refuses to
|
||||
install into system or package-manager directories (`/usr/bin`,
|
||||
`~/.cargo/bin`, Homebrew, `node_modules`, `/nix/store`…) and refuses to
|
||||
overwrite a *different* existing `codewhale`.
|
||||
overwrite a *different* existing `codewhale` unless you ask for it with
|
||||
`CODEWHALE_FORCE_REINSTALL=1` (see [re-running the
|
||||
installer](#re-running-the-installer)).
|
||||
|
||||
Expected output:
|
||||
|
||||
@@ -258,8 +260,15 @@ codewhale doctor # diagnostics; see the note in §8 about what it does NO
|
||||
`Already installed: …` and exits 0.
|
||||
* Different version already installed: it **refuses**
|
||||
(`codewhale install: refusing to replace existing …/codewhale`), exits 1 and
|
||||
changes nothing. It downloads ~160 MB before refusing. Use
|
||||
changes nothing. It downloads the release before refusing. Use
|
||||
[`codewhale update`](#7-updating-and-rolling-back) instead.
|
||||
* Not a release build, at the same version (a local `cargo build` reports
|
||||
`0.10.2 (dev)`): `codewhale update` still reinstalls the release artifact
|
||||
over it, because that build has no release provenance to preserve.
|
||||
* Deliberate replacement: `CODEWHALE_FORCE_REINSTALL=1` replaces a differing
|
||||
`codewhale`/`codew` in the install directory and keeps each previous file
|
||||
beside it as `<name>.bak-<utc-timestamp>`. It never replaces a symlink or a
|
||||
non-regular file, and never replaces a file in a managed directory.
|
||||
|
||||
### Upgrade / uninstall
|
||||
|
||||
@@ -587,6 +596,12 @@ It updates `codewhale` **and** `codew` together. It took 10 s here. Run it
|
||||
again and you get `Already up to date; no download needed.` Other options:
|
||||
`--beta` and `--proxy <URL>`.
|
||||
|
||||
A build that is not a published release reports its provenance in
|
||||
`codewhale --version` (`0.10.2 (dev)` for a local build, a commit hash for a
|
||||
release). `codewhale update` compares the release, not just the version, so a
|
||||
same-version `(dev)` build is replaced by the release artifact instead of
|
||||
reporting "Already up to date" forever.
|
||||
|
||||
<a id="roll-back-to-a-previous-release"></a>
|
||||
|
||||
### Rolling back (e.g. to v0.9.13)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@
|
||||
[[feature]]
|
||||
id = "cloud-computer-dispatch"
|
||||
name = "Cloud computer conversations"
|
||||
summary = "Dispatch, inspect, continue and stop the same account cloud conversation from terminal and desktop."
|
||||
summary = "Dispatch, inspect, continue and stop the same account cloud conversation from terminal and desktop, including conversations with a saved whale."
|
||||
status = "experimental"
|
||||
surfaces = { tui = "preview" }
|
||||
since = "unreleased"
|
||||
|
||||
@@ -198,7 +198,9 @@ codewhale doctor # diagnostics; see the note in §8 about what it does NO
|
||||
### 重新运行安装器
|
||||
|
||||
* 已安装的就是同一版本:无害。它会打印 `Already installed: …` 并以 0 退出。
|
||||
* 已安装的是不同版本:它会**拒绝**(`codewhale install: refusing to replace existing …/codewhale`),以 1 退出,并且不改动任何东西。拒绝之前它已经下载了约 160 MB。请改用 [`codewhale update`](#7-更新与回滚)。
|
||||
* 已安装的是不同版本:它会**拒绝**(`codewhale install: refusing to replace existing …/codewhale`),以 1 退出,并且不改动任何东西。拒绝之前它已经下载了发布包。请改用 [`codewhale update`](#7-更新与回滚)。
|
||||
* 是同一版本但不是发布构建(本地 `cargo build` 会报告 `0.10.2 (dev)`):`codewhale update` 仍会用发布产物覆盖它,因为这种构建没有需要保留的发布溯源信息。
|
||||
* 主动替换:`CODEWHALE_FORCE_REINSTALL=1` 会替换安装目录中不同的 `codewhale`/`codew`,并把每个旧文件保留在同目录的 `<名称>.bak-<UTC 时间戳>`。它绝不替换符号链接或非普通文件,也绝不替换受管目录中的文件。
|
||||
|
||||
### 升级 / 卸载
|
||||
|
||||
@@ -447,6 +449,8 @@ codewhale update
|
||||
|
||||
它会把 `codewhale` **和** `codew` 一起更新,在测试机上用了 10 秒。再运行一次会得到 `Already up to date; no download needed.`。其他选项:`--beta` 和 `--proxy <URL>`。
|
||||
|
||||
不是发布构建的构建会在 `codewhale --version` 中显示自己的来源(本地构建是 `0.10.2 (dev)`,发布构建是提交哈希)。`codewhale update` 比较的是发布产物而不只是版本号,因此同版本的 `(dev)` 构建会被发布产物替换,而不是永远报告 "Already up to date"。
|
||||
|
||||
<a id="roll-back-to-a-previous-release"></a>
|
||||
|
||||
### 回滚(例如回到 v0.9.13)
|
||||
|
||||
+45
-22
@@ -95,7 +95,8 @@ preflight_glibc() {
|
||||
}
|
||||
|
||||
# This script installs an already checksum-verified archive. Existing different
|
||||
# binaries go through `codewhale update`, the sole version-aware updater.
|
||||
# binaries go through `codewhale update`, or through CODEWHALE_FORCE_REINSTALL=1
|
||||
# here when the installed file is not a published release artifact.
|
||||
case "$BIN_DIR" in
|
||||
/*) ;;
|
||||
*) echo "ERROR: PREFIX must be an absolute user path" >&2; exit 1 ;;
|
||||
@@ -119,8 +120,21 @@ check_destination() {
|
||||
destination_exists=1
|
||||
return 0
|
||||
fi
|
||||
if [[ "${CODEWHALE_FORCE_REINSTALL:-}" == 1 && ! -L "$dst" && -f "$dst" ]]; then
|
||||
local backup="$dst.bak-$(date -u +%Y%m%dT%H%M%SZ)" suffix=1
|
||||
while [[ -e "$backup" || -L "$backup" ]]; do
|
||||
backup="$dst.bak-$(date -u +%Y%m%dT%H%M%SZ)-$suffix"
|
||||
suffix=$((suffix + 1))
|
||||
done
|
||||
mv "$dst" "$backup"
|
||||
forced_backups+=("$backup")
|
||||
echo "Replacing $dst (previous file kept at $backup)"
|
||||
return 0
|
||||
fi
|
||||
echo "ERROR: refusing to replace existing $dst; no existing file was changed." >&2
|
||||
echo "For an existing direct Codewhale install, run its full path with 'update'." >&2
|
||||
echo "To replace it with this archive and keep the previous file beside it:" >&2
|
||||
echo ' CODEWHALE_FORCE_REINSTALL=1 ./install.sh' >&2
|
||||
echo "To migrate, install this verified archive into a fresh user prefix:" >&2
|
||||
echo ' mkdir -p "$HOME/.local"' >&2
|
||||
echo ' codewhale_prefix="$(mktemp -d "$HOME/.local/codewhale-release.XXXXXX")"' >&2
|
||||
@@ -133,26 +147,10 @@ check_destination() {
|
||||
|
||||
# The image is an explicit installer choice; presence never changes Core's
|
||||
# Node default. Archives carry notices/source beside any qualified image.
|
||||
payloads=(codewhale codew)
|
||||
if [[ "${CODEWHALE_INSTALL_COMPILED_HOST:-}" == 1 ]]; then
|
||||
for file in codewhale-extension-host codewhale-extension-host.LICENSES.txt codewhale-extension-host.relink-source.tar.gz codewhale-extension-host.release.json; do
|
||||
[[ -f "$SCRIPT_DIR/$file" && ! -L "$SCRIPT_DIR/$file" ]] || { echo "ERROR: compiled host requested but archive has no complete qualified payload ($file); use Node" >&2; exit 1; }
|
||||
payloads+=("$file")
|
||||
done
|
||||
fi
|
||||
# Validate every source and destination before the first write.
|
||||
for bin in "${payloads[@]}"; do
|
||||
src="$SCRIPT_DIR/$bin"
|
||||
[[ -f "$src" ]] || { echo "ERROR: $src not found in archive" >&2; exit 1; }
|
||||
mode=0644
|
||||
case "$bin" in codewhale|codew|codewhale-extension-host) mode=0755; preflight_glibc "$src" "${bin/codewhale-extension-host/compiled host}" ;; esac
|
||||
check_destination "$src" "$BIN_DIR/$bin" "$mode"
|
||||
done
|
||||
legacy_tui="$BIN_DIR/codewhale-tui"
|
||||
if [[ -e "$legacy_tui" || -L "$legacy_tui" ]]; then
|
||||
check_destination "$SCRIPT_DIR/codewhale" "$legacy_tui"
|
||||
fi
|
||||
|
||||
# Backups this run made under CODEWHALE_FORCE_REINSTALL. A run that fails after
|
||||
# moving a command aside puts it back, so a forced install that does not
|
||||
# complete never leaves a command missing from the prefix.
|
||||
forced_backups=()
|
||||
stage=""
|
||||
stage_dir=""
|
||||
# Commands this run published, as "source<TAB>destination" lines. If a later
|
||||
@@ -171,12 +169,37 @@ rollback_published() {
|
||||
done <<< "$published"
|
||||
}
|
||||
on_exit() {
|
||||
local status=$?
|
||||
local status=$? dst backup
|
||||
if [[ -n "$stage" ]]; then rm -f "$stage"; fi
|
||||
if [[ -n "$stage_dir" ]]; then rmdir "$stage_dir"; fi
|
||||
if [[ "$status" -ne 0 && -n "$published" ]]; then rollback_published; fi
|
||||
if [[ "$status" -ne 0 ]]; then
|
||||
for backup in "${forced_backups[@]}"; do
|
||||
[[ -e "$backup" ]] || continue
|
||||
dst="$(printf '%s' "$backup" | sed 's/\.bak-[0-9TZ]*\(-[0-9][0-9]*\)\{0,1\}$//')"
|
||||
if [[ ! -e "$dst" && ! -L "$dst" ]]; then
|
||||
mv "$backup" "$dst" && echo "Restored $dst: this install did not complete." >&2
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
trap on_exit EXIT
|
||||
payloads=(codewhale codew)
|
||||
if [[ "${CODEWHALE_INSTALL_COMPILED_HOST:-}" == 1 ]]; then
|
||||
for file in codewhale-extension-host codewhale-extension-host.LICENSES.txt codewhale-extension-host.relink-source.tar.gz codewhale-extension-host.release.json; do
|
||||
[[ -f "$SCRIPT_DIR/$file" && ! -L "$SCRIPT_DIR/$file" ]] || { echo "ERROR: compiled host requested but archive has no complete qualified payload ($file); use Node" >&2; exit 1; }
|
||||
payloads+=("$file")
|
||||
done
|
||||
fi
|
||||
# Validate every source and destination before the first write.
|
||||
for bin in "${payloads[@]}"; do
|
||||
src="$SCRIPT_DIR/$bin"
|
||||
[[ -f "$src" ]] || { echo "ERROR: $src not found in archive" >&2; exit 1; }
|
||||
mode=0644
|
||||
case "$bin" in codewhale|codew|codewhale-extension-host) mode=0755; preflight_glibc "$src" "${bin/codewhale-extension-host/compiled host}" ;; esac
|
||||
check_destination "$src" "$BIN_DIR/$bin" "$mode"
|
||||
done
|
||||
|
||||
install_binary() {
|
||||
local src="$1" dst="$2" mode="${3:-0755}"
|
||||
check_destination "$src" "$dst" "$mode"
|
||||
|
||||
+83
-47
@@ -16,6 +16,9 @@ Usage:
|
||||
Environment:
|
||||
CODEWHALE_INSTALL_DIR Install directory. Default: $HOME/.local/bin
|
||||
CODEWHALE_VERSION Release tag for a fresh directory. Default: latest
|
||||
CODEWHALE_FORCE_REINSTALL=1
|
||||
Replace differing codewhale/codew already in the install
|
||||
directory, keeping each old file beside it as .bak-<time>
|
||||
CODEWHALE_RELEASE_BASE_URL
|
||||
Custom release asset base URL ending in /download
|
||||
CODEWHALE_INSTALL_COMPILED_HOST=1
|
||||
@@ -223,9 +226,75 @@ cli_asset="codewhale-$target"
|
||||
shim_asset="codew-$target"
|
||||
manifest_asset="codewhale-artifacts-sha256.txt"
|
||||
|
||||
# Resolve the real directory before applying managed-prefix checks. Never use
|
||||
# sudo or allow an install directory symlink to obscure which files will change.
|
||||
case "$install_dir" in
|
||||
/*) ;;
|
||||
*) fail "CODEWHALE_INSTALL_DIR must be an absolute path" ;;
|
||||
esac
|
||||
[ ! -L "$install_dir" ] || fail "install directory is a symlink: $install_dir; choose a fresh user directory"
|
||||
mkdir -p "$install_dir" || fail "cannot create $install_dir; choose a writable user directory (no sudo is used)"
|
||||
install_dir="$(cd -P "$install_dir" && pwd)"
|
||||
case "$install_dir/" in
|
||||
/bin/*|/sbin/*|/usr/bin/*|/usr/sbin/*|/nix/store/*|/gnu/store/*|*/node_modules/*|*/Cellar/*|*/.linuxbrew/*|*/linuxbrew/*|*/.cargo/bin/*)
|
||||
fail "refusing managed/system directory $install_dir; use a fresh user directory"
|
||||
;;
|
||||
esac
|
||||
[ -w "$install_dir" ] || fail "$install_dir is not writable; choose a user directory (no sudo is used)"
|
||||
|
||||
tmpdir="$(mktemp -d 2>/dev/null || mktemp -d -t codewhale-install)"
|
||||
trap 'rm -rf "$tmpdir"' EXIT INT TERM
|
||||
|
||||
# Backups this run made under CODEWHALE_FORCE_REINSTALL, newest last. A run that
|
||||
# fails after moving a command aside puts it back, so a forced install that does
|
||||
# not complete never leaves the command missing from the install directory.
|
||||
forced_backups="$tmpdir/.forced-backups"
|
||||
: > "$forced_backups"
|
||||
|
||||
stage=""
|
||||
stage_dir=""
|
||||
# Commands this run published. If a later publication fails they are removed
|
||||
# again, but only while each is still the exact file this run wrote, so a
|
||||
# failed install leaves no half-installed pair and never touches a file that
|
||||
# was already installed.
|
||||
published="$tmpdir/.published"
|
||||
: > "$published"
|
||||
rollback_published() {
|
||||
while IFS= read -r name; do
|
||||
destination="$install_dir/$name"
|
||||
if [ ! -L "$destination" ] && [ -f "$destination" ] && cmp -s "$tmpdir/$name" "$destination"; then
|
||||
rm -f "$destination"
|
||||
say "Removed $destination: this install did not complete." >&2
|
||||
fi
|
||||
done < "$published"
|
||||
}
|
||||
restore_forced_backups() {
|
||||
[ -f "$forced_backups" ] || return 0
|
||||
while IFS= read -r backup; do
|
||||
[ -n "$backup" ] || continue
|
||||
[ -e "$backup" ] || continue
|
||||
# The name this run generated ends in .bak-<utc-stamp>[-<n>]; the
|
||||
# destination is everything before it.
|
||||
destination="$(printf '%s' "$backup" | sed 's/\.bak-[0-9TZ]*\(-[0-9][0-9]*\)\{0,1\}$//')"
|
||||
if [ ! -e "$destination" ] && [ ! -L "$destination" ]; then
|
||||
mv "$backup" "$destination" && say "Restored $destination: this install did not complete." >&2
|
||||
fi
|
||||
done < "$forced_backups"
|
||||
}
|
||||
on_exit() {
|
||||
status=$?
|
||||
if [ -n "$stage" ]; then rm -f "$stage"; fi
|
||||
if [ -n "$stage_dir" ]; then rmdir "$stage_dir"; fi
|
||||
if [ "$status" -ne 0 ]; then
|
||||
rollback_published
|
||||
restore_forced_backups
|
||||
fi
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap on_exit EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
say "Installing Codewhale for $target"
|
||||
say "Release assets: $release_base"
|
||||
say "Install dir: $install_dir"
|
||||
@@ -243,22 +312,6 @@ if command -v xattr >/dev/null 2>&1; then
|
||||
xattr -d com.apple.quarantine "$tmpdir/codewhale" "$tmpdir/codew" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Resolve the real directory before applying managed-prefix checks. Never use
|
||||
# sudo or allow an install directory symlink to obscure which files will change.
|
||||
case "$install_dir" in
|
||||
/*) ;;
|
||||
*) fail "CODEWHALE_INSTALL_DIR must be an absolute path" ;;
|
||||
esac
|
||||
[ ! -L "$install_dir" ] || fail "install directory is a symlink: $install_dir; choose a fresh user directory"
|
||||
mkdir -p "$install_dir" || fail "cannot create $install_dir; choose a writable user directory (no sudo is used)"
|
||||
install_dir="$(cd -P "$install_dir" && pwd)"
|
||||
case "$install_dir/" in
|
||||
/bin/*|/sbin/*|/usr/bin/*|/usr/sbin/*|/nix/store/*|/gnu/store/*|*/node_modules/*|*/Cellar/*|*/.linuxbrew/*|*/linuxbrew/*|*/.cargo/bin/*)
|
||||
fail "refusing managed/system directory $install_dir; use a fresh user directory"
|
||||
;;
|
||||
esac
|
||||
[ -w "$install_dir" ] || fail "$install_dir is not writable; choose a user directory (no sudo is used)"
|
||||
|
||||
check_destination() {
|
||||
destination="$1"
|
||||
source="$2"
|
||||
@@ -269,10 +322,24 @@ check_destination() {
|
||||
destination_exists=1
|
||||
return
|
||||
fi
|
||||
if [ "${CODEWHALE_FORCE_REINSTALL:-}" = 1 ] && [ ! -L "$destination" ] && [ -f "$destination" ]; then
|
||||
backup="$destination.bak-$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
suffix=1
|
||||
while [ -e "$backup" ] || [ -L "$backup" ]; do
|
||||
backup="$destination.bak-$(date -u +%Y%m%dT%H%M%SZ)-$suffix"
|
||||
suffix=$((suffix + 1))
|
||||
done
|
||||
mv "$destination" "$backup" || fail "cannot move $destination aside; it was not replaced"
|
||||
printf '%s\n' "$backup" >> "$forced_backups"
|
||||
say "Replacing $destination (previous file kept at $backup)"
|
||||
return
|
||||
fi
|
||||
cat >&2 <<EOF
|
||||
codewhale install: refusing to replace existing $destination.
|
||||
It may be a newer build, another installation, or a symlink. No existing file was changed.
|
||||
For an existing direct Codewhale install, run its full path with 'update'.
|
||||
To replace it with this release and keep the previous file beside it:
|
||||
curl -fsSL https://codewhale.net/install.sh | CODEWHALE_FORCE_REINSTALL=1 sh
|
||||
To migrate safely from a package manager or mixed installation, create a fresh directory:
|
||||
mkdir -p "\$HOME/.local"
|
||||
codewhale_install_dir="\$(mktemp -d "\$HOME/.local/codewhale-release.XXXXXX")"
|
||||
@@ -316,38 +383,7 @@ fi
|
||||
|
||||
check_destination "$install_dir/codewhale" "$tmpdir/codewhale"
|
||||
check_destination "$install_dir/codew" "$tmpdir/codew"
|
||||
legacy_tui="$install_dir/codewhale-tui"
|
||||
if [ -e "$legacy_tui" ] || [ -L "$legacy_tui" ]; then
|
||||
check_destination "$legacy_tui" "$tmpdir/codewhale"
|
||||
fi
|
||||
|
||||
stage=""
|
||||
stage_dir=""
|
||||
# Commands this run published. If a later publication fails they are removed
|
||||
# again, but only while each is still the exact file this run wrote, so a
|
||||
# failed install leaves no half-installed pair and never touches a file that
|
||||
# was already installed.
|
||||
published="$tmpdir/.published"
|
||||
: > "$published"
|
||||
rollback_published() {
|
||||
while IFS= read -r name; do
|
||||
destination="$install_dir/$name"
|
||||
if [ ! -L "$destination" ] && [ -f "$destination" ] && cmp -s "$tmpdir/$name" "$destination"; then
|
||||
rm -f "$destination"
|
||||
say "Removed $destination: this install did not complete." >&2
|
||||
fi
|
||||
done < "$published"
|
||||
}
|
||||
on_exit() {
|
||||
status=$?
|
||||
if [ -n "$stage" ]; then rm -f "$stage"; fi
|
||||
if [ -n "$stage_dir" ]; then rmdir "$stage_dir"; fi
|
||||
if [ "$status" -ne 0 ]; then rollback_published; fi
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap on_exit EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
install_binary() {
|
||||
source="$1"
|
||||
destination="$2"
|
||||
|
||||
Reference in new issue
Block a user