mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-09 01:49:49 -10:00
fix(uninstall): adopt receipt-less curl installs instead of refusing them (Refs #7091, CW-7525)
The installer deployed at codewhale.net/install.sh predates the receipt
and writes no .codewhale-install.sha256. web/public/install.sh on main
already writes one. As a result, `codewhale uninstall` refused every copy
installed through the main channel ("No command ownership receipt").
When there is no receipt, uninstall now adopts the copy, but only if:
- its directory is an install.sh default (~/.local/bin,
~/.local/codewhale/bin, ~/.local/codewhale-release.*, or
%USERPROFILE%\bin on Windows), and
- each command is a regular file owned by this user and byte-identical
to the running program (the release ships codewhale and codew as one
binary).
The preview lists exactly those files and changes nothing. --yes writes
the receipt with GuardedFile::stage + move_to_vacant (no clobber), then
runs the normal receipted path, which re-verifies each hash before it
unlinks anything. A modified or foreign codew is left out and reported.
Any other unreceipted directory is still preserved.
Verified on macOS arm64, temp HOMEs only, binary 16bb0efa6079:
- Live script, run as `curl -fsSL https://codewhale.net/install.sh | sh`
with CODEWHALE_RELEASE_BASE_URL set to a file:// release of the
candidate binary: 12 passed, 0 failed. Covered: default dir with no
receipt; preview changes nothing; --yes removes both commands and
leaves no receipt or leftovers; data and an unrelated file preserved;
live reinstall then a second removal; ~/.local/codewhale/bin adopted; a
modified codew preserved and reported; a custom CODEWHALE_INSTALL_DIR
refused; a foreign codew preserved byte-identical.
- Receipted installs from web/public/install.sh (preview, --yes, repeat,
reinstall, two copies, tampered alias, manual copy, cargo, npm, brew,
scoop, winget and nsis layouts, purge): 27 passed, 0 failed.
The adoption hunk is applied to main's uninstall.rs as-is. The build
that ran is the shared tree, which also carries another writer's
uncommitted install-cleanup work in the same file. That work is not
included here.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
4b3053d166
commit
804b7fca89
4 files changed
+85
-5
No files matched your search
+1
-1
@@ -33,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- **Image-heavy session recovery:** ordinary image reads use a 256 KiB delivery budget. Requests measure serialized bytes; over the local budget, older images are omitted from the outbound copy first, with receipts. An initial HTTP 413 is retried exactly once without image attachments, each replaced by an omission note. Saved images and executed tool actions remain intact. (CW-7550)
|
||||
|
||||
- **Uninstall on every install route:** `codewhale uninstall` previews receipt-owned release files and `--yes` removes only checked files, with the running command last. Both Unix installers and the Windows archive installer record command ownership; self-update renews existing receipts on both. On Windows the running `codewhale.exe` is renamed and deleted once the command exits. Package-managed copies are left alone, and the command prints their exact removal command: npm with its prefix, Cargo with its root, the Homebrew formula, `scoop uninstall codewhale`, `winget uninstall --id HunterBown.CodeWhale`, or the Windows setup's `Uninstall.exe`. Every other `codewhale`/`codew` still on PATH is listed. User data is kept unless you add `--purge-data`, which previews and then deletes only `~/.codewhale` (or a `CODEWHALE_HOME` named `.codewhale`) and `~/.deepseek/snapshots`, refusing symlinks, home ancestors and directories owned by another user. Windows removal is compile-checked but not yet exercised on a Windows host. (Refs #7091)
|
||||
- **Uninstall on every install route:** `codewhale uninstall` previews receipt-owned release files and `--yes` removes only checked files, with the running command last. Both Unix installers and the Windows archive installer record command ownership; self-update renews existing receipts on both. On Windows the running `codewhale.exe` is renamed and deleted once the command exits. Package-managed copies are left alone, and the command prints their exact removal command: npm with its prefix, Cargo with its root, the Homebrew formula, `scoop uninstall codewhale`, `winget uninstall --id HunterBown.CodeWhale`, or the Windows setup's `Uninstall.exe`. Every other `codewhale`/`codew` still on PATH is listed. User data is kept unless you add `--purge-data`, which previews and then deletes only `~/.codewhale` (or a `CODEWHALE_HOME` named `.codewhale`) and `~/.deepseek/snapshots`, refusing symlinks, home ancestors and directories owned by another user. A copy from an older `curl | sh` installer that wrote no receipt is adopted on `--yes` when it sits in an install.sh default directory: the commands byte-identical to the running program get a receipt and are removed, and anything else is preserved. Windows removal is compile-checked but not yet exercised on a Windows host. (Refs #7091)
|
||||
|
||||
- **GitHub Action needs an explicit provider:** the Action's `provider` input no longer defaults to the `codewhale` account relay. Name your BYOK provider (or `codewhale` for a separately verified account machine key); a missing provider stops with `configuration_missing` before any download or model call. The repository's own (disabled) review workflow now pins CLI `v0.10.2` by default instead of `v0.10.0`.
|
||||
- **Composer history honors `CODEWHALE_HOME`:** with `CODEWHALE_HOME` set, prompt history reads and writes `$CODEWHALE_HOME/composer_history.jsonl` instead of sharing `~/.codewhale` across isolated homes; an invalid `CODEWHALE_HOME` keeps no history rather than falling back to your real home. (#7069)
|
||||
|
||||
@@ -167,8 +167,43 @@ fn remove(yes: bool) -> Result<()> {
|
||||
identity.path.display()
|
||||
);
|
||||
}
|
||||
let ownership = CommandReceipt::capture(&identity.path)?.with_context(|| format!(
|
||||
"No command ownership receipt beside {}. This legacy/manual copy is preserved. Inspect its exact path and provenance before removing it manually; reinstalling into a fresh directory creates a receipted install.", identity.path.display()))?;
|
||||
let ownership = match CommandReceipt::capture(&identity.path)? {
|
||||
Some(ownership) => ownership,
|
||||
None => {
|
||||
let (directory, entries) = curl_install_entries(&identity).with_context(|| format!(
|
||||
"No command ownership receipt beside {}. This legacy/manual copy is preserved. Inspect its exact path and provenance before removing it manually; reinstalling into a fresh directory creates a receipted install.", identity.path.display()))?;
|
||||
println!(
|
||||
"No ownership receipt yet: {} is a curl-installer location and these commands are byte-identical to this Codewhale {}.",
|
||||
directory.display(),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
if !yes {
|
||||
for (name, _) in &entries {
|
||||
println!("Would record and remove {}", directory.join(name).display());
|
||||
}
|
||||
println!(
|
||||
"No files were changed. Run this executable again with uninstall --yes to record a receipt for exactly these files and remove them."
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let text: String = entries
|
||||
.iter()
|
||||
.map(|(name, digest)| format!("{digest} {name}\n"))
|
||||
.collect();
|
||||
let mut staged = GuardedFile::stage(&directory, text.as_bytes(), false)?;
|
||||
staged.verify_owned()?;
|
||||
staged.move_to_vacant(RECEIPT).with_context(|| {
|
||||
format!(
|
||||
"could not record a receipt in {}; staged bytes retained at {}",
|
||||
directory.display(),
|
||||
directory.join(staged.name()).display()
|
||||
)
|
||||
})?;
|
||||
println!("Recorded an install receipt in {}.", directory.display());
|
||||
CommandReceipt::capture(&identity.path)?
|
||||
.context("the adopted ownership receipt disappeared; nothing was removed")?
|
||||
}
|
||||
};
|
||||
if ownership.hashes.get(&primary_name) != Some(&identity.file_hash) {
|
||||
bail!("The receipt does not own the selected executable; nothing was removed");
|
||||
}
|
||||
@@ -342,6 +377,50 @@ fn manager_command(method: InstallMethod, executable: &Path) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn curl_install_entries(
|
||||
identity: &UpdateExecutableIdentity,
|
||||
) -> Option<(PathBuf, Vec<(String, String)>)> {
|
||||
let directory = identity.path.parent()?.canonicalize().ok()?;
|
||||
let home = dirs::home_dir()?.canonicalize().ok()?;
|
||||
let local = home.join(".local");
|
||||
let known = directory == local.join("bin")
|
||||
|| directory == local.join("codewhale").join("bin")
|
||||
|| (cfg!(windows) && directory == home.join("bin"))
|
||||
|| (directory.parent() == Some(local.as_path())
|
||||
&& directory
|
||||
.file_name()?
|
||||
.to_str()?
|
||||
.starts_with("codewhale-release."));
|
||||
if !known {
|
||||
return None;
|
||||
}
|
||||
let entries: Vec<(String, String)> = COMMANDS
|
||||
.iter()
|
||||
.filter(|name| {
|
||||
let path = directory.join(name);
|
||||
std::fs::symlink_metadata(&path)
|
||||
.is_ok_and(|meta| meta.is_file() && owned_by_user(&meta))
|
||||
&& std::fs::read(&path).is_ok_and(|bytes| sha256_hex(&bytes) == identity.file_hash)
|
||||
})
|
||||
.map(|name| ((*name).to_string(), identity.file_hash.clone()))
|
||||
.collect();
|
||||
entries
|
||||
.iter()
|
||||
.any(|(name, _)| identity.path.file_name() == Some(std::ffi::OsStr::new(name)))
|
||||
.then_some((directory, entries))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn owned_by_user(meta: &std::fs::Metadata) -> bool {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
meta.uid() == codewhale_config::private_directory::PrivateDirectory::current_user_id()
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn owned_by_user(_meta: &std::fs::Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn windows_installer_uninstaller(executable: &Path) -> Option<PathBuf> {
|
||||
let bin = executable.parent()?;
|
||||
if !bin
|
||||
|
||||
@@ -33,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- **Image-heavy session recovery:** ordinary image reads use a 256 KiB delivery budget. Requests measure serialized bytes; over the local budget, older images are omitted from the outbound copy first, with receipts. An initial HTTP 413 is retried exactly once without image attachments, each replaced by an omission note. Saved images and executed tool actions remain intact. (CW-7550)
|
||||
|
||||
- **Uninstall on every install route:** `codewhale uninstall` previews receipt-owned release files and `--yes` removes only checked files, with the running command last. Both Unix installers and the Windows archive installer record command ownership; self-update renews existing receipts on both. On Windows the running `codewhale.exe` is renamed and deleted once the command exits. Package-managed copies are left alone, and the command prints their exact removal command: npm with its prefix, Cargo with its root, the Homebrew formula, `scoop uninstall codewhale`, `winget uninstall --id HunterBown.CodeWhale`, or the Windows setup's `Uninstall.exe`. Every other `codewhale`/`codew` still on PATH is listed. User data is kept unless you add `--purge-data`, which previews and then deletes only `~/.codewhale` (or a `CODEWHALE_HOME` named `.codewhale`) and `~/.deepseek/snapshots`, refusing symlinks, home ancestors and directories owned by another user. Windows removal is compile-checked but not yet exercised on a Windows host. (Refs #7091)
|
||||
- **Uninstall on every install route:** `codewhale uninstall` previews receipt-owned release files and `--yes` removes only checked files, with the running command last. Both Unix installers and the Windows archive installer record command ownership; self-update renews existing receipts on both. On Windows the running `codewhale.exe` is renamed and deleted once the command exits. Package-managed copies are left alone, and the command prints their exact removal command: npm with its prefix, Cargo with its root, the Homebrew formula, `scoop uninstall codewhale`, `winget uninstall --id HunterBown.CodeWhale`, or the Windows setup's `Uninstall.exe`. Every other `codewhale`/`codew` still on PATH is listed. User data is kept unless you add `--purge-data`, which previews and then deletes only `~/.codewhale` (or a `CODEWHALE_HOME` named `.codewhale`) and `~/.deepseek/snapshots`, refusing symlinks, home ancestors and directories owned by another user. A copy from an older `curl | sh` installer that wrote no receipt is adopted on `--yes` when it sits in an install.sh default directory: the commands byte-identical to the running program get a receipt and are removed, and anything else is preserved. Windows removal is compile-checked but not yet exercised on a Windows host. (Refs #7091)
|
||||
|
||||
- **GitHub Action needs an explicit provider:** the Action's `provider` input no longer defaults to the `codewhale` account relay. Name your BYOK provider (or `codewhale` for a separately verified account machine key); a missing provider stops with `configuration_missing` before any download or model call. The repository's own (disabled) review workflow now pins CLI `v0.10.2` by default instead of `v0.10.0`.
|
||||
- **Composer history honors `CODEWHALE_HOME`:** with `CODEWHALE_HOME` set, prompt history reads and writes `$CODEWHALE_HOME/composer_history.jsonl` instead of sharing `~/.codewhale` across isolated homes; an invalid `CODEWHALE_HOME` keeps no history rather than falling back to your real home. (#7069)
|
||||
|
||||
+2
-1
@@ -882,7 +882,8 @@ codewhale auth clear --provider deepseek
|
||||
| Windows setup (NSIS) | Settings > Apps > CodeWhale > Uninstall, or `%LOCALAPPDATA%\Programs\CodeWhale\Uninstall.exe`. `codewhale uninstall` prints this path. |
|
||||
| WinGet | `winget uninstall --id HunterBown.CodeWhale` |
|
||||
| Scoop | `scoop uninstall codewhale` |
|
||||
| old unreceipted installer or manual (§3) | Inspect the exact command paths and provenance before removing them manually; automatic uninstall preserves them. |
|
||||
| older `curl \| sh` install without a receipt | `~/.local/bin/codewhale uninstall --yes`: in an install.sh default directory (`~/.local/bin`, `~/.local/codewhale/bin`, `~/.local/codewhale-release.*`, or `%USERPROFILE%\bin` on Windows) it records a receipt for the commands that are byte-identical to the running program, then removes only those. |
|
||||
| manual copy elsewhere (§3) | Inspect the exact command paths and provenance before removing them manually; automatic uninstall preserves them. |
|
||||
| npm | `npm uninstall -g codewhale` (`codewhale uninstall` prints the `--prefix` for this copy) |
|
||||
| Cargo | `cargo uninstall codewhale-cli` (`codewhale uninstall` prints the owning `--root`) |
|
||||
| Homebrew | `brew uninstall codewhale && brew untap Hmbown/deepseek-tui` (also removes its node dependency; `codewhale uninstall` prints the owning formula) |
|
||||
|
||||
Reference in new issue
Block a user