docs(public): align release facts with runtime

Derive website sandbox facts from explicit runtime capability markers instead of source filenames. Document actual platform wrappers, narrow model and audit claims, and make two-package/three-command installs exact.

Verify release assets under their manifest filenames before renaming them, with an executable contract test for every Unix snippet. Refresh localized README receipts and keep source-candidate toolCount at 66.

Signed-off-by: Hunter B <hmbown@gmail.com>
This commit is contained in:
Hunter B
2026-07-21 18:38:16 -07:00
parent bdadd2250e
commit 4b80259c67
32 changed files with 595 additions and 458 deletions
+8 -6
View File
@@ -1,7 +1,7 @@
<!-- source: README.md sha256:acc243c2dc85 -->
<!-- source: README.md sha256:a14f7d3aa7d1 -->
# Codewhale
**Un runtime. Todos los modelos. Tu máquina.**
**Un runtime. Modelos alojados y locales compatibles. Tu máquina.**
Codewhale es un agente de código para tu terminal. Funciona con modelos
alojados y locales compatibles; los modelos abiertos primero. Le das un proveedor, un modelo y una
@@ -17,10 +17,12 @@ mitad de tarea con `/model`. Usa la TUI para el trabajo interactivo y
vienen de la ruta real. Un precio desconocido se muestra como desconocido,
nunca como $0.
- **Seguro por construcción.** El modo Plan es de solo lectura. Las
aprobaciones controlan cada llamada riesgosa. El sandbox del sistema
operativo resiste — Seatbelt, Landlock, seccomp, bwrap. El
`constitution.json` de un repo se compila en bloqueos de escritura que ni
siquiera Full Access puede saltarse.
aprobaciones controlan cada llamada riesgosa. Codewhale solo informa un
sandbox de comandos del sistema operativo cuando realmente envuelve el
comando: Seatbelt en macOS cuando está disponible y bubblewrap opcional en
Linux cuando está instalado. Windows actualmente informa que no hay
sandbox. El `constitution.json` de un repo se compila en bloqueos de
escritura que ni siquiera Full Access puede saltarse.
- **Trabajo que sobrevive.** Los fleets registran cada paso en un libro mayor
de solo agregado; `fleet resume` retoma donde te detuviste. Cada turno deja
un recibo que puedes inspeccionar.
+8 -5
View File
@@ -1,7 +1,7 @@
<!-- source: README.md sha256:acc243c2dc85 -->
<!-- source: README.md sha256:a14f7d3aa7d1 -->
# Codewhale
**ひとつのランタイム。あらゆるモデル。あなたのマシン。**
**ひとつのランタイム。対応するホスト型・ローカルモデル。あなたのマシン。**
Codewhale はターミナルで動くコーディングエージェントです。対応するホスト型・
ローカルモデルで動作し、オープンモデルを最優先します。プロバイダ、モデル、タスクを渡すと、
@@ -18,9 +18,12 @@ CI には `codewhale exec` を。Rust 製、MIT ライセンス、あなたの
実際のルートに由来します。不明な価格は不明と表示され、$0 とは決して
表示されません。
- **構造として安全。** Plan モードは読み取り専用。リスクのある呼び出しは
すべて承認でゲートされます。OS サンドボックスが守ります — Seatbelt、
Landlock、seccomp、bwrap。リポジトリの `constitution.json` は書き込み
ホールドへとコンパイルされ、Full Access でもスキップできません。
すべて承認でゲートされます。Codewhale が OS コマンドサンドボックスを
表示するのは、実際にコマンドをラップするときだけです。macOS では利用可能な
Seatbelt、Linux ではインストール済みで明示的に有効化した bubblewrap を使い、
Windows は現在サンドボックスなしと表示します。リポジトリの
`constitution.json` は書き込みホールドへとコンパイルされ、Full Access でも
スキップできません。
- **消えない作業。** Fleet はすべてのステップを追記専用の台帳に記録し、
`fleet resume` で止めたところから再開できます。どのターンも検証できる
レシートを残します。
+7 -5
View File
@@ -1,7 +1,7 @@
<!-- source: README.md sha256:acc243c2dc85 -->
<!-- source: README.md sha256:a14f7d3aa7d1 -->
# Codewhale
**하나의 런타임. 모든 모델. 당신의 컴퓨터.**
**하나의 런타임. 지원되는 호스팅 및 로컬 모델. 당신의 컴퓨터.**
Codewhale은 터미널에서 쓰는 코딩 에이전트입니다. 지원되는 호스팅 및 로컬
모델과 함께 동작하며, 오픈 모델을 우선합니다. 프로바이더, 모델, 작업을 지정하면 코드를 읽고,
@@ -17,9 +17,11 @@ Codewhale은 터미널에서 쓰는 코딩 에이전트입니다. 지원되는
알 수 없는 가격은 알 수 없음으로 표시됩니다 — 절대 $0으로 표시되지
않습니다.
- **구조적으로 안전.** Plan 모드는 읽기 전용입니다. 위험한 호출은 모두
승인을 거칩니다. OS 샌드박스가 지켜냅니다 — Seatbelt, Landlock, seccomp,
bwrap. 저장소의 `constitution.json`은 Full Access조차 건너뛸 수 없는 쓰기
홀드로 컴파일됩니다.
승인을 거칩니다. Codewhale은 명령이 실제로 래핑될 때만 OS 명령 샌드박스를
표시합니다. macOS에서는 사용 가능한 Seatbelt를, Linux에서는 설치되어 있고
명시적으로 활성화한 bubblewrap을 사용하며, Windows는 현재 샌드박스 없음으로
표시합니다. 저장소의 `constitution.json`은 Full Access조차 건너뛸 수 없는
쓰기 홀드로 컴파일됩니다.
- **사라지지 않는 작업.** Fleet은 모든 단계를 추가 전용 원장에 기록하고,
`fleet resume`은 멈춘 지점부터 이어갑니다. 매 턴마다 확인할 수 있는
영수증이 남습니다.
+6 -4
View File
@@ -1,6 +1,6 @@
# Codewhale
**One runtime. Every model. Your machine.**
**One runtime. Supported hosted and local models. Your machine.**
Codewhale is a coding agent for your terminal. It works with supported hosted
and local models; open models first. Give it a provider, a model, and a task: it reads your
@@ -15,9 +15,11 @@ runs on your machine.
and one toolset. Context budgets and prices come from the real route. An
unknown price shows as unknown, never as $0.
- **Safe by construction.** Plan mode is read-only. Approvals gate every
risky call. The OS sandbox holds — Seatbelt, Landlock, seccomp, bwrap.
A repo's `constitution.json` compiles into write holds that even Full
Access cannot skip.
risky call. Codewhale reports an OS command sandbox only when it actually
wraps the command: Seatbelt on macOS when available, and opt-in bubblewrap
on Linux when installed. Windows currently reports none. A repo's
`constitution.json` compiles into write holds that even Full Access cannot
skip.
- **Work that survives.** Fleets record every step in an append-only
ledger; `fleet resume` picks up where you stopped. Every turn leaves a
receipt you can inspect.
+8 -5
View File
@@ -1,7 +1,7 @@
<!-- source: README.md sha256:acc243c2dc85 -->
<!-- source: README.md sha256:a14f7d3aa7d1 -->
# Codewhale
**Um runtime. Todos os modelos. Sua máquina.**
**Um runtime. Modelos hospedados e locais compatíveis. Sua máquina.**
O Codewhale é um agente de código para o seu terminal. Funciona com modelos
hospedados e locais compatíveis; modelos abertos em primeiro lugar. Você informa um provedor, um
@@ -17,9 +17,12 @@ sua máquina.
um único conjunto de ferramentas. Orçamentos de contexto e preços vêm da
rota real. Um preço desconhecido aparece como desconhecido, nunca como $0.
- **Seguro por construção.** O modo Plan é somente leitura. Aprovações
controlam cada chamada arriscada. O sandbox do sistema operacional segura —
Seatbelt, Landlock, seccomp, bwrap. O `constitution.json` de um repositório
é compilado em bloqueios de escrita que nem o Full Access consegue pular.
controlam cada chamada arriscada. O Codewhale só informa um sandbox de
comandos do sistema operacional quando ele realmente envolve o comando:
Seatbelt no macOS quando disponível e bubblewrap opcional no Linux quando
instalado. O Windows atualmente informa que não há sandbox. O
`constitution.json` de um repositório é compilado em bloqueios de escrita
que nem o Full Access consegue pular.
- **Trabalho que sobrevive.** Fleets registram cada passo em um livro-razão
de apenas inclusão; `fleet resume` retoma de onde você parou. Cada turno
deixa um recibo que você pode inspecionar.
+7 -5
View File
@@ -1,7 +1,7 @@
<!-- source: README.md sha256:acc243c2dc85 -->
<!-- source: README.md sha256:a14f7d3aa7d1 -->
# Codewhale
**Một runtime. Mọi model. Máy của bạn.**
**Một runtime. Các model hosted và local được hỗ trợ. Máy của bạn.**
Codewhale là một coding agent cho terminal của bạn. Hoạt động với các model
hosted và local được hỗ trợ; ưu tiên model mở. Đưa cho nó một provider, một model và một nhiệm vụ: nó đọc
@@ -16,9 +16,11 @@ bằng Rust, giấy phép MIT, chạy trên máy của bạn.
runtime và một bộ công cụ. Ngân sách ngữ cảnh và giá lấy từ route thật; giá
chưa rõ hiển thị là chưa rõ, không bao giờ là $0.
- **An toàn từ thiết kế.** Chế độ Plan chỉ đọc. Mọi lệnh gọi rủi ro đều qua
phê duyệt. Sandbox của hệ điều hành giữ vững — Seatbelt, Landlock, seccomp,
bwrap. `constitution.json` của repo được biên dịch thành các chốt chặn ghi
mà ngay cả Full Access cũng không thể bỏ qua.
phê duyệt. Codewhale chỉ báo sandbox lệnh của hệ điều hành khi lệnh thực sự
được bọc: Seatbelt trên macOS khi khả dụng và bubblewrap trên Linux khi đã
cài đặt và bật rõ ràng. Windows hiện báo không có sandbox. `constitution.json`
của repo được biên dịch thành các chốt chặn ghi mà ngay cả Full Access cũng
không thể bỏ qua.
- **Công việc không mất.** Fleet ghi lại từng bước vào sổ cái chỉ ghi thêm;
`fleet resume` tiếp tục từ chỗ bạn dừng. Mỗi lượt đều để lại một biên nhận
bạn có thể kiểm tra.
+3 -3
View File
@@ -1,13 +1,13 @@
<!-- source: README.md sha256:acc243c2dc85 -->
<!-- source: README.md sha256:a14f7d3aa7d1 -->
# Codewhale
**一个运行时。所有模型。你的机器。**
**一个运行时。支持的托管与本地模型。你的机器。**
Codewhale 是运行在终端里的编程智能体。适配受支持的托管与本地模型;开放模型优先。给它一个 provider、一个模型和一个任务:它会读你的代码、改文件、跑命令、检查自己的工作,在任务完成或需要你介入时停下。任务中途用 `/model` 切换模型。交互式工作用 TUI,脚本和 CI 用 `codewhale exec`。Rust 编写,MIT 许可,运行在你自己的机器上。
**为什么选 Codewhale**
- **不被锁定。** DeepSeek、Claude、GPT、Kimi、GLM 等 30 多家 provider,以及你自己的 vLLM、SGLang、Ollama——无需 key——都跑在同一套运行时和同一套工具之上。上下文预算与价格取自真实路由;价格未知时显示未知,绝不显示 $0。
- **安全靠构造。** Plan 模式只读。审批把关每一次高风险调用。操作系统级沙箱守住底线——Seatbelt、Landlock、seccomp、bwrap。仓库的 `constitution.json` 会编译成写入拦截,连 Full Access 也无法跳过。
- **安全靠构造。** Plan 模式只读。审批把关每一次高风险调用。Codewhale 只有在命令实际经由 OS 沙箱包装器运行时才报告沙箱:macOS 上是可用时启用的 Seatbelt,Linux 上是已安装且显式启用的 bubblewrapWindows 当前报告无 OS 沙箱。仓库的 `constitution.json` 会编译成写入拦截,连 Full Access 也无法跳过。
- **工作不丢失。** Fleet 把每一步记录在只追加的账本里;`fleet resume` 从你停下的地方继续。每一轮都留下可查验的回执。
它诞生于 `deepseek-tui`。社区需要更多 provider,于是我们造了一个把模型当组件、而不是当产品的运行时。
+1 -1
View File
@@ -46,7 +46,7 @@ You will receive status updates at each phase. If the timeline slips, we will co
### In scope (what counts)
- Remote code execution through crafted prompts or model responses
- Sandbox escape — breaking out of the YOLO-mode workspace boundary or shell `cwd` confinement
- Sandbox escape — breaking out of an active Seatbelt/bubblewrap wrapper or a declared workspace boundary
- Credential leak — exfiltration of API keys, tokens, or environment secrets
- Arbitrary file read/write outside the intended workspace (`PathEscape` bypass)
- SSRF via `fetch_url` or `web_search` against internal network endpoints
+7 -4
View File
@@ -172,7 +172,9 @@ drives turns through Chat Completions.
- `mod.rs` - Sandbox type definitions
- `policy.rs` - Sandbox policy configuration
- `seatbelt.rs` - macOS Seatbelt profile generation
- `bwrap.rs` - opt-in Linux bubblewrap command wrapper
- `landlock.rs` - Linux Landlock detection and future helper contract
- `seccomp.rs` - dormant Linux seccomp implementation; not wired into commands
- `windows.rs` - Windows helper contract; not advertised until a Job
Object process-containment helper exists
@@ -218,7 +220,7 @@ drives turns through Chat Completions.
2. Tool registry looks up handler
3. Pre-execution hooks run
4. Approval requested when the effective permission posture and policy require it
5. Tool executed (possibly sandboxed on macOS)
5. Tool executed (possibly wrapped by Seatbelt on macOS or opt-in bubblewrap on Linux)
6. Post-execution hooks run
7. Result metadata is retained on runtime item records
8. **LSP post-edit hook**: after a `File` write, edit, or patch action (including a replay-only legacy alias), the engine runs `run_post_edit_lsp_hook()` when LSP is enabled to collect diagnostics
@@ -296,9 +298,10 @@ command = "echo 'Running tool: $TOOL_NAME'"
holds. Side-effectful MCP tools use the same boundary.
3. **Extensibility**: MCP, skills, and hooks allow customization without code changes
4. **Cross-platform**: Core works on Linux/macOS/Windows. Sandbox guarantees
are platform-specific: macOS Seatbelt is the active policy path; Linux and
Windows require helper enforcement before they should be treated as full OS
sandboxing.
are platform-specific: macOS uses Seatbelt when available; Linux uses an
installed bubblewrap executable only when explicitly enabled; Windows has
no advertised OS command sandbox. Landlock, seccomp, and the Windows helper
contract are not wired into command execution.
5. **Minimal dependencies**: Careful dependency selection for build speed
6. **Local-first runtime API**: HTTP/SSE endpoints are intended for trusted localhost access and are served by the `crates/tui` runtime today
+4 -3
View File
@@ -1285,9 +1285,10 @@ If you are upgrading from older releases:
auto-approval.
- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.
- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.
Platform support is not identical. macOS uses Seatbelt for policy
enforcement. Linux support is helper-gated around Landlock. Windows does not
currently advertise an OS sandbox; the planned Windows helper contract starts
Platform support is not identical. macOS uses Seatbelt when its runtime
probe succeeds. Linux uses bubblewrap only when `prefer_bwrap = true` and
`/usr/bin/bwrap` is executable; without that opt-in it reports no OS command
sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts
with process-tree containment only and must not be described as read-only
filesystem isolation, workspace-write enforcement, network blocking,
registry isolation, or AppContainer isolation until those are implemented.
+1 -1
View File
@@ -75,7 +75,7 @@ notes, and relevant issue/PR comments.
visibility in the phase strip (PR #4474)
- **[Fred Leitz / fleitz](https://github.com/fleitz)** — canonical `Bash`
no-`cwd` workspace resolution and its regression test, keeping isolated
sub-agent commands inside their selected worktree (PR #4673 / #4674)
sub-agent commands inside their selected worktree (PR #4673; issue #4674)
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — exact
Vim-space regression reproduction and verification that the v0.9.1 input
path already contains the needed global binding (PR #4477)
+1 -1
View File
@@ -41,7 +41,7 @@ logs and adapter logs are stored under `.codewhale/fleet/` and
### Interactive and persistent status
CodeWhale has two similarly named status surfaces with different scopes:
Codewhale has two similarly named status surfaces with different scopes:
- In the TUI, `/fleet status` (or `/subagents`) shows the sub-agents attached
to the current interactive session. It does not read the persistent Fleet
+16 -12
View File
@@ -182,7 +182,7 @@ codewhale-tui --version
Known limitations:
- Commands inherit Android's per-app UID, SELinux, and seccomp protections and
any permissions granted to Termux. Codewhale's additional Landlock/bwrap
any permissions granted to Termux. Codewhale's opt-in bubblewrap
child-process sandbox is Linux-only and is not built on Android, so approved
commands receive no Codewhale-specific filesystem narrowing.
- The Termux build has no supported Android Keystore or desktop Secret Service
@@ -244,7 +244,7 @@ npm install -g codewhale
codewhale --version # prints the published version that was installed
```
`postinstall` downloads the right pair of binaries from the matching GitHub
`postinstall` downloads the matching three binaries from the GitHub
release, verifies a SHA-256 manifest, and exposes `codewhale`, `codew`, and
`codewhale-tui` on your `PATH`.
@@ -274,8 +274,9 @@ Useful environment variables:
## 4. Install via Cargo (any Tier-1 Rust target)
If GitHub releases are slow, blocked, or you're on an unsupported architecture,
install from crates.io directly. Both crates are required — the dispatcher
delegates to the TUI runtime at runtime.
install from crates.io directly. Two Cargo packages are required:
`codewhale-cli` installs the `codewhale` and `codew` commands, while
`codewhale-tui` installs the `codewhale-tui` command used by the dispatcher.
```bash
# Requires Rust 1.88+ (https://rustup.rs)
@@ -481,7 +482,7 @@ explicitly. Replace `X.Y.Z` with the version you want to restore.
# npm wrapper, only for versions that were published to npm
npm install -g codewhale@X.Y.Z
# Cargo install path; both crates are required
# Cargo path: two packages provide codewhale + codew + codewhale-tui
cargo install codewhale-cli --version X.Y.Z --locked --force
cargo install codewhale-tui --version X.Y.Z --locked --force
```
@@ -752,7 +753,8 @@ The legacy `DEEPSEEK_TUI_RELEASE_BASE_URL` name is still accepted.
`codewhale update` normally contacts GitHub Releases for metadata and binary
assets. On networks where GitHub is blocked or unreliable, use the CNB source
mirror instead and install both binaries from the release tag:
mirror instead and install both Cargo packages from the release tag. Together,
they provide the `codewhale`, `codew`, and `codewhale-tui` commands:
To check the latest release without downloading or replacing binaries, run
`codewhale update --check`.
@@ -786,8 +788,9 @@ The package requires the Cargo feature called `edition2024`, but that feature
is not stabilized in this version of Cargo
```
Install current stable Rust through rustup, then rerun the two Cargo install
commands from [Section 4](#4-install-via-cargo-any-tier-1-rust-target). For
Install current stable Rust through rustup, then rerun the two Cargo package
install commands from [Section 4](#4-install-via-cargo-any-tier-1-rust-target).
Together they provide `codewhale`, `codew`, and `codewhale-tui`. For
mainland China networks, this rsproxy-based sequence has been verified to work:
```bash
@@ -817,7 +820,8 @@ sudo apt-get install -y build-essential pkg-config libdbus-1-dev
WSL2 uses the same Linux source-build path as Ubuntu. If `cargo install
codewhale-tui --locked` fails while compiling the keyring or D-Bus secret
storage crates, install the Linux build dependencies inside the WSL distro,
then rerun both Cargo install commands:
then rerun the two Cargo package install commands. Together they install
`codewhale`, `codew`, and `codewhale-tui`:
```bash
sudo apt-get update
@@ -921,9 +925,9 @@ Use one of these paths:
3. Install via Cargo, which builds locally and does not download GitHub release
assets. See [Section 4](#4-install-via-cargo-any-tier-1-rust-target).
4. Download both `codewhale` and `codewhale-tui` manually from the
[Releases page](https://github.com/Hmbown/CodeWhale/releases), place them
in a directory on `PATH`, and make them executable. See
4. Download all three matching `codewhale`, `codew`, and `codewhale-tui`
binaries from the [Releases page](https://github.com/Hmbown/CodeWhale/releases),
place them in a directory on `PATH`, and make them executable. See
[Section 6](#6-manual-download-from-github-releases).
---
+144 -221
View File
@@ -1,271 +1,194 @@
# Sandbox threat model
Codewhale executes shell commands spawned by AI reasoning. The sandbox
module restricts what those commands can do to the host system. This
document describes what each platform's sandbox actually enforces,
what is best-effort, and what is explicitly out of scope.
Codewhale can launch shell commands proposed by a model. Approval policy,
workspace-aware tools, and an operating-system command wrapper are separate
controls: an approval is not a sandbox, and selecting `workspace-write` does
not prove that the current platform has an OS wrapper available.
This document describes only behavior wired into the command execution path.
## Platform overview
| Mechanism | Platform | Type | Status |
| Mechanism | Platform | Selection | What Codewhale reports |
|---|---|---|---|
| Seatbelt | macOS | Mandatory access control | Enforced |
| Landlock | Linux | Filesystem access control | Enforced |
| seccomp BPF | Linux | Syscall filter | Enforced |
| Process hardening | Linux | Kernel prctl / rlimit | Enforced |
| Bubblewrap (bwrap) | Linux | Namespace isolation | Optional |
| Windows Job Object | Windows | Process-tree containment | v1 (PR #2220) |
| Seatbelt (`sandbox-exec`) | macOS | Automatic when the runtime probe succeeds | `macos-seatbelt` |
| Bubblewrap (`/usr/bin/bwrap`) | Linux | `prefer_bwrap = true` and the file is executable | `linux-bwrap` |
| No OS wrapper | Linux without usable opt-in bwrap | Default | `none` |
| No OS wrapper | Windows | Current implementation | `none` |
| OpenSandbox-compatible service | Any supported host | `sandbox_backend = "opensandbox"` | External execution path |
## Threat model: what each layer addresses
The repository contains Landlock and seccomp implementation modules, plus a
future Windows helper contract. They are not wired into child-command launch
in v0.9.1, so Codewhale does not advertise them as active sandboxes. Detecting
a Landlock-capable kernel is not enough to claim that a command was restricted.
### 1. Process hardening (Linux only)
## macOS: Seatbelt
**When it runs:** Before any threads are spawned, before Tokio boots,
before any data is loaded into memory.
Codewhale probes `/usr/bin/sandbox-exec` by running a minimal profile. When the
probe succeeds and the selected `SandboxPolicy` requests a sandbox, the child
command is wrapped with a generated Seatbelt profile.
**What it does:**
The profile can provide:
- `PR_SET_DUMPABLE=0` — prevents ptrace, makes `/proc/<pid>/` root-owned
- `PR_SET_NO_NEW_PRIVS=1` — irreversible; no child can ever gain privileges
- `RLIMIT_CORE=0` — no core dumps, so sensitive data never hits disk
- broad filesystem reads;
- writes limited by the selected policy, including the workspace and specific
runtime/cache paths needed by supported tools;
- network access only when the policy enables it.
**What it protects against:**
- Process inspection via ptrace/strace/gdb
- Privilege escalation via setuid/setgid/fscaps
- Core dumps leaking API keys, tokens, prompt content
If the probe fails or `sandbox-exec` is unavailable, Codewhale reports no OS
sandbox and launches the command without a Seatbelt wrapper. It does not set a
Seatbelt marker on that fallback.
**What it does NOT protect against:**
- A compromised child reading its parent's `/proc/<pid>/mem` (already blocked
by `PR_SET_DUMPABLE=0` making `/proc/<pid>/` root-owned)
- Kernel exploits that bypass prctl
## Linux: opt-in bubblewrap
### 2. Landlock (Linux, kernel 5.13+)
Linux command sandboxing is opt-in. Set the top-level configuration key:
**When it runs:** Applied to each child process at spawn time via a
helper script or `landlock_restrict_self`. Only restrictable by the
process itself — parent cannot force Landlock on a child.
```toml
prefer_bwrap = true
```
**What it does:**
- Restricts filesystem access to a whitelist of paths
- Handles: `EXECUTE`, `READ_FILE`, `READ_DIR`, `WRITE_FILE`, `REMOVE_DIR`,
`REMOVE_FILE`, `MAKE_DIR`, `MAKE_REG`, `MAKE_SYM`, `TRUNCATE`
Codewhale selects bubblewrap only when `/usr/bin/bwrap` is a regular executable
file. The wrapper derives its mounts and network namespace from the resolved
`SandboxPolicy`:
**What it protects against:**
- Reading files outside the workspace (e.g., `/etc/passwd`, `~/.ssh`)
- Writing to system directories (`/usr`, `/bin`, `/lib`)
- Creating or deleting files in protected locations
```text
/usr/bin/bwrap \
--unshare-all \
[--share-net] \
--ro-bind / / \
--bind <writable-root> <writable-root> ... \
--ro-bind <protected-descendant> <protected-descendant> ... \
--chdir <cwd> \
-- <program> <args>
```
**What it does NOT protect against:**
- Network access (Landlock is filesystem-only)
- Process inspection (use seccomp for this)
- Reading files that are already mapped (Landlock applies at `open()` time)
That gives the child a read-only root view. For `workspace-write`, every safe,
existing policy root is mounted read-write: the working directory, configured
additional roots, `/tmp` and `TMPDIR` unless excluded, and verified Git
worktree metadata roots. Existing `.codewhale` and `.deepseek` descendants are
remounted read-only after their writable parent. Missing paths, non-directory
paths, and `/` are not promoted to writable mounts.
**Detection:** `detect_denial()` checks stderr for `Permission denied`,
`Operation not permitted`, `EACCES`, `EPERM`.
For `read-only`, there are no writable binds, so the working directory remains
inside the read-only root view. `--unshare-all` isolates the network namespace
by default. Codewhale adds `--share-net` only when the policy's
`network_access` is true. `danger-full-access` and `external-sandbox` bypass the
local wrapper entirely.
### 3. seccomp BPF (Linux only)
If the user does not opt in, or `/usr/bin/bwrap` is missing or non-executable,
Codewhale reports `none` and launches the command without a Linux OS wrapper.
There is no marker-only Landlock fallback.
**When it runs:** Installed via `prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER)`
on the child process.
Install bubblewrap separately when this opt-in fits the workflow:
**What it does:**
- Whitelist of ~100 safe syscalls (file I/O, memory, process, IPC,
synchronization, signals, time)
- **Explicitly denied:** `ptrace`, `mount`, `umount2`, `kexec_load`,
`kexec_file_load`, `init_module`, `finit_module`, `delete_module`,
`bpf`, `reboot`, `swapon`, `swapoff`, `pivot_root`,
`setuid`/`setgid`/`setreuid`/`setregid`/`setresuid`/`setresgid`,
`personality`
- Any syscall not on the whitelist → `SECCOMP_RET_KILL_PROCESS` (SIGSYS)
**What it protects against:**
- Process hijacking via ptrace
- Mounting filesystems (bypassing Landlock read-only restrictions)
- Loading kernel modules
- Loading BPF programs (would bypass seccomp itself!)
- Rebooting the system
- Privilege changes via setuid/setgid
**What it does NOT protect against:**
- Legitimate use of allowed syscalls for malicious purposes
- Side-channel attacks via allowed syscalls (e.g., timing)
**Detection:** `detect_denial()` checks exit code 31 (SIGSYS) or stderr
for `Bad system call`, `bad system call`, `SIGSYS`, `seccomp`.
### 4. Bubblewrap / bwrap (Linux, optional)
**When it runs:** If `/usr/bin/bwrap` is present AND the config key
`[sandbox] prefer_bwrap = true` is set. Runs as an outer wrapper around
the child command.
**What it does:**
- Creates a new mount namespace with `--unshare-all`
- Read-only bind-mounts the entire root filesystem
- Bind-mounts the workspace directory with read-write access
- Changes into the workspace with `--chdir`
**What it protects against:**
- Any filesystem write outside the workspace (stronger than Landlock alone
because it's enforced at the namespace level, not just filesystem access)
- Accidental modification of system files
**What it does NOT protect against:**
- Network access (bwrap does not create a network namespace by default with
`--unshare-all`; the child still has full network access)
- Process inspection
- Memory attacks
**Installation:** User must install bubblewrap themselves:
- Ubuntu/Debian: `apt install bubblewrap`
- Fedora: `dnf install bubblewrap`
- Arch: `pacman -S bubblewrap`
Codewhale does NOT vendor bwrap.
Codewhale does not vendor bubblewrap.
**Fallback:** If bwrap is not installed, the sandbox falls back to Landlock
only.
## Windows: no advertised OS sandbox
### 5. Seatbelt (macOS)
The Windows command path currently reports no OS sandbox. The source tree has
a future helper contract for Job Object process-tree cleanup, but it is not
wired into selection and must not be described as any of the following:
**When it runs:** Applied via the `sandbox-exec` wrapper command. The
seatbelt profile is generated dynamically based on the `SandboxPolicy`.
- read-only filesystem or workspace-write enforcement;
- network blocking;
- registry isolation;
- restricted-token or AppContainer isolation.
**What it does:**
- Restricts filesystem access based on the policy profile
- Can restrict network access (when `network_access: false`)
Windows host permissions and approval policy still apply, but they are not a
Codewhale OS command sandbox.
**What it protects against:**
- Reading/writing files outside allowed paths
- Network connections (when configured)
## Linux process hardening is not a command sandbox
**What it does NOT protect against:**
- Process inspection (Seatbelt does not block ptrace)
- Syscall-level attacks
At startup on Linux, Codewhale best-effort applies `PR_SET_DUMPABLE=0`,
`PR_SET_NO_NEW_PRIVS=1`, and `RLIMIT_CORE=0` to its own process. Each failure is
logged and startup continues. These controls reduce process-inspection,
privilege-escalation, and core-dump risk; they do not create filesystem or
network isolation for a child command and are not listed as a sandbox backend.
**Detection:** Checks stderr for `file-write` and `network` denial patterns.
## External OpenSandbox execution
### 6. Windows Job Object (v1, PR #2220)
**When it runs:** Applied at process spawn time via
`PROC_THREAD_ATTRIBUTE_JOB_LIST` and restricted token assignment.
**What it does (v1):**
- Job Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` — all child
processes terminate when the parent exits
- Memory cap: 1 GB per process, 2 GB per job
- Active process limit: 64
- UI restrictions: no desktop handle access
- Restricted token: drops Administrators group SID, sets medium-low
integrity level
**What is deferred (v2):**
- WFP (Windows Filtering Platform) firewall rules — network is open in v1
- Filesystem ACL integration at spawn time (deferred)
- AppContainer isolation
- Registry key isolation
**Detection:** Checks stderr for `Access is denied`, `STATUS_ACCESS_DENIED`,
`ERROR_ACCESS_DENIED`, `ERROR_PRIVILEGE_NOT_HELD`,
`ERROR_ACCESS_DISABLED_BY_POLICY`, and integrity/AppContainer patterns.
## Defense in depth
The Linux sandbox applies layers in order:
```
Process hardening (prctl) ← before threads
Landlock (filesystem) ← at child spawn
seccomp BPF (syscalls) ← at child spawn
bwrap (namespace isolation) ← optional outer wrapper
```
Each layer addresses a different threat surface. seccomp cannot protect the
filesystem (that's Landlock's job). Landlock cannot stop ptrace (that's
seccomp + PR_SET_DUMPABLE). bwrap adds namespace-level isolation that
neither Landlock nor seccomp can provide.
## Configuration
Relevant config keys in `~/.codewhale/config.toml`:
When `sandbox_backend = "opensandbox"` is configured, shell execution is sent
to the configured OpenSandbox-compatible HTTP endpoint instead of starting a
local child. Codewhale validates the request/response contract, but isolation
guarantees belong to the configured service and its operator.
```toml
# Sandbox policy mode
sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-access | external-sandbox
# Linux bubblewrap passthrough
prefer_bwrap = false # requires `bubblewrap` package installed
# External sandbox backend
sandbox_backend = "none" # "none" or "opensandbox"
sandbox_backend = "opensandbox"
sandbox_url = "http://localhost:8080"
sandbox_api_key = "YOUR_API_KEY"
```
Environment variable overrides:
`sandbox_backend = "none"` (or omitting the key) keeps local execution.
- `DEEPSEEK_SANDBOX_MODE``sandbox_mode`
- `DEEPSEEK_PREFER_BWRAP=true``prefer_bwrap`
- `DEEPSEEK_SANDBOX_BACKEND``sandbox_backend`
- `DEEPSEEK_SANDBOX_URL``sandbox_url`
- `DEEPSEEK_SANDBOX_API_KEY``sandbox_api_key`
## Policies and fallbacks
## Detecting sandbox denials
The local `sandbox_mode` values are:
When a command fails, the sandbox manager checks for denial patterns:
```toml
sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-access | external-sandbox
```
| Platform | Denial mechanism | Exit code | Stderr patterns |
|---|---|---|---|
| macOS Seatbelt | sandbox-exec violation | non-zero | `file-write`, `network` |
| Linux Landlock | EACCES / EPERM | non-zero | `Permission denied`, `Operation not permitted` |
| Linux seccomp | SIGSYS (31) | 31 or 159 | `Bad system call`, `SIGSYS` |
| Linux bwrap | Mount/namespace failure | non-zero | varies |
| Windows | Access denied / privilege | non-zero | `Access is denied`, `ERROR_PRIVILEGE_NOT_HELD` |
- `read-only` and `workspace-write` are enforced by Seatbelt or bubblewrap only
when that wrapper is selected and available.
- `danger-full-access` deliberately bypasses the local OS wrapper.
- `external-sandbox` declares that execution is already externally isolated
and bypasses a second local wrapper.
- When no wrapper is selected, the shell command runs without Codewhale OS
isolation. Approval rules and workspace-aware native file tools remain
separate controls.
The `was_denied()` method on `SandboxManager` aggregates all platform-specific
checks. The `denial_message()` method returns a human-readable explanation.
Canonical environment overrides exist for `sandbox_mode` and the external
backend:
- `CODEWHALE_SANDBOX_MODE`
- `CODEWHALE_SANDBOX_BACKEND`
- `CODEWHALE_SANDBOX_URL`
- `CODEWHALE_SANDBOX_API_KEY`
There is no `CODEWHALE_PREFER_BWRAP` environment override; use the top-level
`prefer_bwrap` config key.
## Diagnostics and failure attribution
`codewhale setup --status`, `codewhale doctor`, `codewhale doctor --json`, and
the `diagnostics` tool report the locally available wrapper after applying the
resolved bubblewrap preference. An individual command can still bypass that
wrapper when its policy does not request sandboxing. On Linux, merely finding
a Landlock syscall or a bwrap source module does not make
`sandbox_available` true.
Denial attribution is intentionally conservative:
- Seatbelt uses its wrapper-specific denial patterns.
- Bubblewrap setup errors must be prefixed by `bwrap:`; a read-only-filesystem
error from the bwrap filesystem view can also identify the boundary.
- A child command's generic `Permission denied` or `Operation not permitted`
is not, by itself, proof that Codewhale's sandbox blocked it.
- Unsandboxed command failures are never labeled sandbox denials.
## Limitations
### What the sandbox does NOT protect against
- Availability is checked before launch; the selected wrapper can still fail
because of host policy, container restrictions, or a race after the probe.
- Bubblewrap ignores a configured writable root if it is missing, is not a
directory, or canonicalizes to `/`; a path can also disappear between policy
resolution and wrapper launch.
- Seatbelt profiles are generated at runtime and must be tested against the
commands they are expected to support.
- No current local wrapper is advertised on Windows.
- An external sandbox backend is only as strong as its configured service.
- No sandbox protects against kernel vulnerabilities or all resource-exhaustion
and side-channel attacks.
- **Network attacks** — only macOS Seatbelt can block network; Linux and
Windows v1 leave network open
- **Memory attacks** — no platform prevents a child process from reading
its own memory or exploiting memory corruption bugs
- **Timing side channels** — allowed syscalls on Linux can be used for
timing-based information leaks
- **Resource exhaustion** — the Linux job object limits memory and process
count, but does not limit CPU, file descriptors, or disk I/O
- **Kernel vulnerabilities** — if the kernel itself has a vulnerability,
the sandbox cannot prevent exploitation (this applies to all platforms)
- **Supply chain** — if the child process downloads and executes untrusted
code, the sandbox limits what that code can do, but does not prevent the
download
## Implementation references
### Platform-specific gaps
- **Linux:** Landlock only protects filesystem access. seccomp adds syscall
filtering but uses a whitelist that may need updates for new syscalls.
- **macOS:** Seatbelt profiles are generated at runtime. A misconfigured
profile could be too permissive.
- **Windows v1:** No filesystem ACL enforcement at spawn time. Network is
fully open. Job Object is process-tree only.
## Related
- `crates/tui/src/sandbox/` — implementation
- `crates/config/src/lib.rs` — config keys
- `crates/tui/src/tools/diagnostics.rs``diagnostics` tool reports
`sandbox_available`, `sandbox_type`, `bwrap_available`, `cgroup_version`
- `config.example.toml` — annotated config reference
- Issue #2180 — this document
- Issue #2182 — seccomp filter implementation
- Issue #2183 — process hardening
- Issue #2184 — bwrap passthrough
- Issue #2185 — Windows Job Object v1
- Issue #2186 — SandboxExecutor trait unification
- Issue #2187 — sandbox parity tests
- `crates/tui/src/sandbox/mod.rs` — truthful selection and public capability markers
- `crates/tui/src/sandbox/seatbelt.rs` — macOS wrapper and availability probe
- `crates/tui/src/sandbox/bwrap.rs` — Linux opt-in wrapper
- `crates/tui/src/sandbox/process_hardening.rs` — Linux parent-process hardening
- `crates/tui/src/sandbox/backend.rs` — external backend selection
- `crates/tui/src/tools/diagnostics.rs` — machine-readable diagnostics
+8 -7
View File
@@ -33,9 +33,10 @@ Codewhale's security model has three distinct layers on Android:
user has granted to Termux. See the
[Android application sandbox](https://source.android.com/docs/security/app-sandbox)
and [Termux filesystem layout](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout).
2. **Codewhale's per-command sandbox backend** — Seatbelt (macOS) or
Landlock/bwrap (Linux) can further narrow what a child command may access.
Codewhale does not currently provide that additional layer on Android.
2. **Codewhale's per-command sandbox backend** — Seatbelt (macOS) or the
opt-in bubblewrap wrapper (Linux) can further narrow what a child command
may access. Codewhale does not currently provide that additional layer on
Android.
3. **Codewhale's own gates** — workspace trust, approval prompts,
`allow_shell`/`disallowed-tools`, and the file-tool permission system.
These share the cross-platform application code path; their Android
@@ -43,15 +44,15 @@ Codewhale's security model has three distinct layers on Android:
### Codewhale sandbox backend: none
Codewhale's existing Seatbelt and Landlock/bwrap integrations do not target
Codewhale's existing Seatbelt and Linux bubblewrap integrations do not target
Android. Consequently, `codewhale doctor --json` reports the sandbox as
`{"available": false, "kind": null}` on Android. That status describes the
absence of an additional Codewhale child-process sandbox; it does not mean
Android or Termux provides no OS isolation.
- `get_platform_sandbox()` returns `None` on Android.
- No Linux-only sandbox modules (Landlock, bwrap) are compiled into the
Android build — they are `#[cfg(target_os = "linux")]`-gated and Rust
- No Linux-only bubblewrap wrapper is compiled into the Android build — it is
`#[cfg(target_os = "linux")]`-gated and Rust
treats `android` as a distinct target from `linux`.
- Shell commands retain Termux's Android app boundary but receive no
Codewhale-specific filesystem narrowing. Treat every location available to
@@ -95,7 +96,7 @@ skipped entirely on Android (Bionic libc).
| Feature | Status | Notes |
|---------|--------|-------|
| Android app sandbox | ✅ inherited | Per-app UID plus Android platform protections |
| Codewhale command sandbox | ❌ unavailable | No Landlock/bwrap/Seatbelt backend on Android |
| Codewhale command sandbox | ❌ unavailable | No bubblewrap/Seatbelt backend on Android |
| Codewhale keyring backend | ❌ unavailable | Falls back to file-backed secrets |
| Approvals / gates | ⚠️ implemented | Device QA pending |
| File tools | ⚠️ implemented | Device QA pending |
+10 -2
View File
@@ -17,9 +17,14 @@
"version": "0.9.1",
"providerCount": 34,
"toolCount": 66,
"sandboxBackends": [
"seatbelt (macOS, when available)",
"bubblewrap (Linux, opt-in when installed)"
],
"sources": [
"Cargo.toml",
"crates/tui/src/config.rs",
"crates/tui/src/sandbox/mod.rs",
"web/scripts/derive-facts.mjs"
]
},
@@ -102,7 +107,8 @@
"telemetry": "no Codewhale product telemetry or mandatory hosted relay",
"account": "no account required for the local runtime",
"plan": "Plan is read-only",
"sandbox": "sandbox implementations and limitations are platform-specific",
"sandbox": "Seatbelt is used on macOS when available; Linux bubblewrap is opt-in and must be installed; Windows currently reports no OS sandbox",
"audit": "sensitive events append best-effort to $CODEWHALE_HOME/audit.log (default ~/.codewhale/audit.log); write failures are logged",
"usage": "provider token and cache usage is shown locally when available",
"sources": [
"docs/PROVIDERS.md",
@@ -110,7 +116,9 @@
"docs/SANDBOX.md",
"docs/RUNTIME_API.md",
"docs/WEB.md",
"web/app/[locale]/faq/page.tsx"
"crates/tui/src/audit.rs",
"web/app/[locale]/faq/page.tsx",
"web/app/[locale]/install/page.tsx"
]
},
"platforms": {
+1 -1
View File
@@ -143,7 +143,7 @@ default model, Node engines) are never hand-written into pages:
1. **Build time** — `scripts/derive-facts.mjs` runs as `prebuild` (and before
`npm run dev`), parses the parent repo (`Cargo.toml`, `crates/tui/src/config.rs`,
`crates/tui/src/sandbox/`, `npm/codewhale/package.json`) and writes
`crates/tui/src/sandbox/mod.rs`, `npm/codewhale/package.json`) and writes
`lib/facts.generated.ts`. Never edit that file by hand.
2. **Published release** — `data/latest-published-release.json` records the
latest GitHub Release separately from the source candidate. Install commands
+4 -4
View File
@@ -225,9 +225,9 @@ default_text_model = "openrouter/deepseek/deepseek-v4-pro"`}
provider you select receives the prompt, project context, tool definitions,
and tool results required for that turn. Use a loopback local-model route to
keep model inference local.
Sandbox backends: <strong>seatbelt</strong> (macOS), <strong>landlock</strong> (Linux), restricted tokens (Windows).
OS command sandboxing is platform-specific: Codewhale uses <strong>Seatbelt</strong> on macOS when available. On Linux it uses <strong>bubblewrap</strong> only when <code className="inline">prefer_bwrap = true</code> and <code className="inline">/usr/bin/bwrap</code> is executable; otherwise commands have no Codewhale OS wrapper. Windows currently reports no OS sandbox.
Workspace boundaries default to <code className="inline">--workspace</code>. <code className="inline">/trust</code> lifts them.
Permission posture is configurable per session. All credential/approval/elevation events are written to <code className="inline">~/.codewhale/audit.log</code>.
Permission posture is configurable per session. Sensitive credential, approval, and elevation events are appended best-effort to <code className="inline">$CODEWHALE_HOME/audit.log</code> (default <code className="inline">~/.codewhale/audit.log</code>); write failures are logged.
</>
),
sources: ["SECURITY.md", "docs/PROVIDERS.md", "docs/RUNTIME_API.md"],
@@ -572,9 +572,9 @@ default_text_model = "openrouter/deepseek/deepseek-v4-pro"`}
Codewhale Codewhale
provider
prompt使
<strong>seatbelt</strong>macOS<strong>landlock</strong>LinuxWindows
OS macOS 使 <strong>Seatbelt</strong>Linux <code className="inline">prefer_bwrap = true</code> <code className="inline">/usr/bin/bwrap</code> 使 <strong>bubblewrap</strong> Codewhale OS Windows OS
<code className="inline">--workspace</code><code className="inline">/trust</code>
姿// <code className="inline">~/.codewhale/audit.log</code>
姿 <code className="inline">$CODEWHALE_HOME/audit.log</code> <code className="inline">~/.codewhale/audit.log</code>
</>
),
sources: ["SECURITY.md", "docs/PROVIDERS.md", "docs/RUNTIME_API.md"],
+23 -15
View File
@@ -59,27 +59,27 @@ const FROM_SOURCE = `git clone https://github.com/Hmbown/CodeWhale
cd CodeWhale
cargo build --release --locked
# Install both binaries from the local checkout
cargo install --path crates/cli --locked # codewhale
# Install two Cargo packages; together they provide three commands
cargo install --path crates/cli --locked # codewhale + codew
cargo install --path crates/tui --locked # codewhale-tui`;
const CONFIG_TREE = `~/.codewhale/
const CONFIG_TREE = `$CODEWHALE_HOME/ (default: ~/.codewhale/)
├── config.toml api keys, model, hooks, profiles
├── mcp.json MCP server definitions
├── skills/ user skills (each with SKILL.md)
├── sessions/ checkpoints + offline queue
├── tasks/ background task store
└── audit.log credential / approval / elevation audit trail
└── audit.log best-effort credential / approval / elevation events
./.codewhale/ project-scoped config (optional, per-repo)`;
const CONFIG_TREE_ZH = `~/.codewhale/
const CONFIG_TREE_ZH = `$CODEWHALE_HOME/(默认:~/.codewhale/
├── config.toml API 密钥、模型、钩子、配置集
├── mcp.json MCP 服务器定义
├── skills/ 用户技能(每个含 SKILL.md)
├── sessions/ 检查点 + 离线队列
├── tasks/ 后台任务存储
└── audit.log 凭证 / 审批 / 提权审计日志
└── audit.log 尽力写入的凭证 / 审批 / 提权事件
./.codewhale/ 项目级配置(可选,每个仓库)`;
@@ -189,8 +189,10 @@ codewhale doctor`;
<code className="inline">curl</code>
npm {" "}
<code className="inline">npm update -g codewhale</code>
Cargo <code className="inline">cargo install</code> {" "}
<code className="inline">--force</code>
Cargo package <code className="inline">cargo install</code> {" "}
<code className="inline">--force</code><code className="inline">codewhale-cli</code>
<code className="inline">codewhale</code> <code className="inline">codew</code>
<code className="inline">codewhale-tui</code>
Homebrew tap <code className="inline">brew upgrade deepseek-tui</code>
</>
) : (
@@ -199,9 +201,12 @@ codewhale doctor`;
installed with <code className="inline">install.sh</code>, re-run the same{" "}
<code className="inline">curl</code> command to overwrite the binaries.
If you installed via a package manager, prefer it instead: npm users run{" "}
<code className="inline">npm update -g codewhale</code>; cargo users re-run both{" "}
<code className="inline">cargo install</code> commands with{" "}
<code className="inline">--force</code>; the legacy Homebrew tap updates with{" "}
<code className="inline">npm update -g codewhale</code>; cargo users re-run the two
package <code className="inline">cargo install</code> commands with{" "}
<code className="inline">--force</code> (<code className="inline">codewhale-cli</code>
provides <code className="inline">codewhale</code> and <code className="inline">codew</code>;
<code className="inline">codewhale-tui</code> provides the command of the same name);
the legacy Homebrew tap updates with{" "}
<code className="inline">brew upgrade deepseek-tui</code>.
</>
)}
@@ -349,16 +354,19 @@ codewhale doctor`;
<p className="mt-3 text-sm text-ink-soft leading-relaxed max-w-2xl">
{isZh ? (
<>
<code className="inline">codewhale</code> <code className="inline">codewhale-tui</code> <code className="inline">~/.cargo/bin</code>
Cargo package <code className="inline">codewhale</code>
<code className="inline">codew</code> <code className="inline">codewhale-tui</code>
<code className="inline">~/.cargo/bin</code>
Rust 1.88+Linux <code className="inline">pkg-config</code> {" "}
<code className="inline">libdbus-1-dev</code> Rust访{" "}
<a href="https://rustup.rs" className="body-link">rustup.rs</a>
</>
) : (
<>
Compiles and installs <code className="inline">codewhale</code> and{" "}
<code className="inline">codewhale-tui</code> to{" "}
<code className="inline">~/.cargo/bin</code>. Requires Rust 1.88+; install via{" "}
The two Cargo packages install three commands
<code className="inline">codewhale</code>, <code className="inline">codew</code>, and{" "}
<code className="inline">codewhale-tui</code>to <code className="inline">~/.cargo/bin</code>.
Requires Rust 1.88+; install via{" "}
<a href="https://rustup.rs" className="body-link">rustup.rs</a> if you don&apos;t have it.
On Linux, install build dependencies such as{" "}
<code className="inline">pkg-config</code> and{" "}
+2 -2
View File
@@ -47,8 +47,8 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
path: "/",
locale,
title: isZh
? "Codewhale — 一个运行时,所有模型,你的机器"
: "Codewhale — One runtime. Every model. Your machine.",
? "Codewhale — 一个运行时,支持的托管与本地模型,你的机器"
: "Codewhale — One runtime. Supported hosted and local models. Your machine.",
description: isZh
? "开源终端编程智能体,适配受支持的托管与本地模型,开放模型优先。TUI、CLI 与本地工具——运行在你自己的机器上。Rust 编写,MIT 许可。"
: "Open-source terminal coding agent for supported hosted and local models, open models first. TUI, CLI, and local tools — runs on your machine. Rust, MIT.",
+2 -2
View File
@@ -72,7 +72,7 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
<>
<br />
<br />
<span></span>
</>
@@ -80,7 +80,7 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
<>
One runtime.
<br />
Every model.
Supported hosted and local models.
<br />
<span>Your machine.</span>
</>
+2 -2
View File
@@ -26,7 +26,7 @@ const tracksEn = [
{ title: "Sub-agent parallel execution", note: "agent; 64 concurrent sessions by default, configurable to 128, with bounded result handles" },
{ title: "RLM batched processing", note: "Persistent sandboxed Python REPL with 116 cheap parallel children for long-input analysis" },
{ title: "Three operating modes", note: "Plan (read-only), Act (execution), Operate (Fleet/Workflow orchestration); orthogonal Ask / Auto-Review / Full Access posture" },
{ title: "Per-platform sandbox", note: "seatbelt (macOS), landlock (Linux); Windows containment via restricted tokens (limited)" },
{ title: "OS command sandbox", note: "Seatbelt on macOS when available; opt-in bubblewrap on Linux when installed. Windows currently reports no OS sandbox." },
{ title: "Durable sessions + tasks", note: "Save, resume, rollback; background task queue with replayable timelines under ~/.codewhale/tasks/" },
{ title: "Bidirectional MCP", note: "Consume tools from external servers; expose as server via `codewhale mcp`; ~/.codewhale/mcp.json" },
{ title: "Skills + unified slash palette", note: "~/.codewhale/skills/ auto-loading; /help, /mode, /status, /config, /trust, /feedback" },
@@ -84,7 +84,7 @@ const tracksZh = [
{ title: "子 Agent 并行执行", note: "agent;默认 64 个并发会话,可配置到 128 个,通过 var_handle 有界读取结果" },
{ title: "RLM 批量处理", note: "持久沙箱 Python REPL,支持 1–16 路廉价并行子调用,处理长文本分析" },
{ title: "三种运行模式", note: "Plan(只读调查)、Act(执行)与 OperateFleet / Workflow 编排);Ask、Auto-Review 与 Full Access 权限姿态独立设置" },
{ title: "跨平台沙箱", note: "seatbeltmacOS)、landlockLinux);Windows 通过受限令牌实现基础隔离(功能有限)" },
{ title: "OS 命令沙箱", note: "macOS 在可用时使用 SeatbeltLinux 在安装后可显式启用 bubblewrap。Windows 当前报告无 OS 沙箱。" },
{ title: "持久化会话 + 后台任务", note: "保存、恢复、回滚;后台任务队列,可回放时间线,位于 ~/.codewhale/tasks/" },
{ title: "双向 MCP 协议", note: "消费外部服务器工具;通过 `codewhale mcp` 暴露为服务器;~/.codewhale/mcp.json" },
{ title: "技能 + 统一命令面板", note: "~/.codewhale/skills/ 自动加载;/help、/mode、/status、/config、/trust、/feedback" },
+1 -85
View File
@@ -6,93 +6,9 @@ import {
type Arch,
type UserAgentArchitecture,
} from "@/lib/install-platform";
import { SNIPPETS, VERIFY } from "@/lib/install-binary-snippets";
import { InstallCodeBlock } from "./install-code-block";
function windowsSnippet(arch: "x64" | "arm64"): string {
return `# PowerShell
$ErrorActionPreference = "Stop"
$dest = "$Env:USERPROFILE\\bin"
New-Item -ItemType Directory -Force $dest | Out-Null
$manifest = Invoke-WebRequest https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
Invoke-WebRequest \`
-Uri https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-windows-${arch}.exe \`
-OutFile "$dest\\codewhale.exe"
Invoke-WebRequest \`
-Uri https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-tui-windows-${arch}.exe \`
-OutFile "$dest\\codewhale-tui.exe"
$expected = @{}
$manifest.Content -split "\`n" | ForEach-Object {
$parts = $_.Trim() -split "\\s+"
if ($parts.Length -ge 2) { $expected[$parts[1]] = $parts[0].ToUpperInvariant() }
}
if ((Get-FileHash "$dest\\codewhale.exe" -Algorithm SHA256).Hash -ne $expected["codewhale-windows-${arch}.exe"]) { throw "codewhale.exe checksum mismatch" }
if ((Get-FileHash "$dest\\codewhale-tui.exe" -Algorithm SHA256).Hash -ne $expected["codewhale-tui-windows-${arch}.exe"]) { throw "codewhale-tui.exe checksum mismatch" }
$Env:Path = "$dest;$Env:Path"`;
}
function windowsVerify(arch: "x64" | "arm64"): string {
return `# PowerShell
$manifest = Invoke-WebRequest https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
$expected = @{}
$manifest.Content -split "\`n" | ForEach-Object {
$parts = $_.Trim() -split "\\s+"
if ($parts.Length -ge 2) { $expected[$parts[1]] = $parts[0].ToUpperInvariant() }
}
if ((Get-FileHash "$Env:USERPROFILE\\bin\\codewhale.exe" -Algorithm SHA256).Hash -ne $expected["codewhale-windows-${arch}.exe"]) { throw "codewhale.exe checksum mismatch" }
if ((Get-FileHash "$Env:USERPROFILE\\bin\\codewhale-tui.exe" -Algorithm SHA256).Hash -ne $expected["codewhale-tui-windows-${arch}.exe"]) { throw "codewhale-tui.exe checksum mismatch" }`;
}
const SNIPPETS: Record<Arch, string> = {
"macos-arm64": `curl -fsSL -O https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
curl -fsSL -o codewhale \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-macos-arm64
curl -fsSL -o codewhale-tui \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-tui-macos-arm64
grep -E ' (codewhale|codewhale-tui)-macos-arm64$' codewhale-artifacts-sha256.txt | shasum -a 256 -c -
chmod +x codewhale codewhale-tui
xattr -d com.apple.quarantine codewhale codewhale-tui 2>/dev/null || true
sudo mv codewhale codewhale-tui /usr/local/bin/`,
"macos-x64": `curl -fsSL -O https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
curl -fsSL -o codewhale \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-macos-x64
curl -fsSL -o codewhale-tui \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-tui-macos-x64
grep -E ' (codewhale|codewhale-tui)-macos-x64$' codewhale-artifacts-sha256.txt | shasum -a 256 -c -
chmod +x codewhale codewhale-tui
xattr -d com.apple.quarantine codewhale codewhale-tui 2>/dev/null || true
sudo mv codewhale codewhale-tui /usr/local/bin/`,
"linux-x64": `curl -fsSL -O https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
curl -fsSL -o codewhale \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-linux-x64
curl -fsSL -o codewhale-tui \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-tui-linux-x64
grep -E ' (codewhale|codewhale-tui)-linux-x64$' codewhale-artifacts-sha256.txt | sha256sum -c -
chmod +x codewhale codewhale-tui
sudo mv codewhale codewhale-tui /usr/local/bin/`,
"linux-arm64": `curl -fsSL -O https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
curl -fsSL -o codewhale \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-linux-arm64
curl -fsSL -o codewhale-tui \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-tui-linux-arm64
grep -E ' (codewhale|codewhale-tui)-linux-arm64$' codewhale-artifacts-sha256.txt | sha256sum -c -
chmod +x codewhale codewhale-tui
sudo mv codewhale codewhale-tui /usr/local/bin/`,
"windows-x64": windowsSnippet("x64"),
"windows-arm64": windowsSnippet("arm64"),
};
const VERIFY: Record<Arch, string> = {
"macos-arm64": `grep -E ' (codewhale|codewhale-tui)-macos-arm64$' codewhale-artifacts-sha256.txt | shasum -a 256 -c -`,
"macos-x64": `grep -E ' (codewhale|codewhale-tui)-macos-x64$' codewhale-artifacts-sha256.txt | shasum -a 256 -c -`,
"linux-x64": `grep -E ' (codewhale|codewhale-tui)-linux-x64$' codewhale-artifacts-sha256.txt | sha256sum -c -`,
"linux-arm64": `grep -E ' (codewhale|codewhale-tui)-linux-arm64$' codewhale-artifacts-sha256.txt | sha256sum -c -`,
"windows-x64": windowsVerify("x64"),
"windows-arm64": windowsVerify("arm64"),
};
const LABELS: Record<Arch, string> = {
"macos-arm64": "macOS · Apple Silicon",
"macos-x64": "macOS · Intel",
+4 -1
View File
@@ -74,7 +74,10 @@ function freshFacts(overrides: Partial<RepoFacts> = {}): RepoFacts {
sourceCommittedAt: null,
version: "0.8.64",
crates: ["cli", "config", "tui"],
sandboxBackends: ["landlock (Linux)", "seatbelt (macOS)"],
sandboxBackends: [
"seatbelt (macOS, when available)",
"bubblewrap (Linux, opt-in when installed)",
],
providers: [
{ id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" },
{ id: "anthropic", label: "Anthropic", env: "ANTHROPIC_API_KEY" },
+2 -2
View File
@@ -158,8 +158,8 @@ export const DOC_TOPICS: DocTopic[] = [
slug: "sandbox",
label: { en: "Sandbox & Approval", zh: "沙箱与审批" },
description: {
en: "seatbelt (macOS), landlock (Linux), Windows containment, and approval policies.",
zh: "seatbeltmacOS)、landlockLinux)、Windows 隔离和审批策略。",
en: "Available Seatbelt (macOS), opt-in bubblewrap (Linux), platform gaps, and approval policies.",
zh: "可用的 SeatbeltmacOS)、显式启用的 bubblewrapLinux)、平台缺口和审批策略。",
},
repoSource: "docs/SANDBOX.md",
hasPage: false,
+14 -4
View File
@@ -29,16 +29,18 @@ function installGitHubFixture(toolCountSource: string | null): void {
}),
);
}
if (url.includes("/contents/crates/tui/src/sandbox?")) {
return response(JSON.stringify([{ name: "seatbelt.rs", type: "file" }]));
}
const rawPath = url.split(`/${REVISION}/`)[1];
const sources: Record<string, string> = {
"Cargo.toml": 'version = "0.9.2"\nmembers = ["crates/tui"]',
"crates/tui/src/config.rs":
'pub enum ApiProvider {\n Deepseek,\n}\nconst DEFAULT_TEXT_MODEL: &str = "remote-model";',
"crates/tui/src/config/models.rs": "",
"crates/tui/src/sandbox/mod.rs": `
pub const PUBLIC_SANDBOX_BACKENDS: &[&str] = &[
"seatbelt (macOS, when available)",
"bubblewrap (Linux, opt-in when installed)",
];
`,
"npm/codewhale/package.json": JSON.stringify({ engines: { node: ">=18" } }),
LICENSE: "MIT License\n",
};
@@ -67,6 +69,14 @@ describe("deriveFactsFromRemote", () => {
expect(facts?.sourceRevision).toBe(REVISION);
expect(facts?.version).toBe("0.9.2");
expect(facts?.toolCount).toBe(73);
expect(facts?.sandboxBackends).toEqual([
"seatbelt (macOS, when available)",
"bubblewrap (Linux, opt-in when installed)",
]);
const fetchMock = vi.mocked(fetch);
expect(
fetchMock.mock.calls.some(([input]) => String(input).includes("/contents/")),
).toBe(false);
});
it("fails derivation when the exact revision has no valid tool count", async () => {
+11 -36
View File
@@ -51,29 +51,6 @@ async function fetchText(
}
}
async function fetchListing(
dir: string,
revision: string,
ghToken?: string,
): Promise<string[] | null> {
// Use GitHub Contents API to list a directory.
const url = `https://api.github.com/repos/Hmbown/CodeWhale/contents/${dir}?ref=${revision}`;
const headers: Record<string, string> = {
"Accept": "application/vnd.github+json",
"User-Agent": "codewhale-web-drift",
"X-GitHub-Api-Version": "2022-11-28",
};
if (ghToken) headers["Authorization"] = `Bearer ${ghToken}`;
try {
const r = await fetch(url, { headers });
if (!r.ok) return null;
const arr = (await r.json()) as { name: string; type: string }[];
return arr.filter((e) => e.type === "file").map((e) => e.name);
} catch {
return null;
}
}
async function fetchSourceMarker(ghToken?: string): Promise<SourceMarker | null> {
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
@@ -184,16 +161,12 @@ function deriveDefaultModel(cfg: string): string | null {
return m ? m[1] : null;
}
function deriveSandboxBackends(files: string[]): string[] {
const map: Record<string, string> = {
seatbelt: "seatbelt (macOS)",
landlock: "landlock (Linux)",
};
return files
.map((f) => f.replace(/\.rs$/, ""))
.filter((name) => !["mod", "policy", "backend", "opensandbox", "windows"].includes(name))
.sort()
.map((name) => map[name] ?? name);
function deriveSandboxBackends(source: string): string[] {
const marker = source.match(
/pub const PUBLIC_SANDBOX_BACKENDS\s*:\s*&\[&str\]\s*=\s*&\[([\s\S]*?)\];/,
);
if (!marker) return [];
return [...marker[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]);
}
async function fetchLatestPublishedRelease(
@@ -262,11 +235,11 @@ export async function deriveFactsFromRemote(ghToken?: string): Promise<RepoFacts
const source = await fetchSourceMarker(ghToken);
if (!source) return null;
const [cargo, configRs, configModels, sandboxFiles, npmPkg, licText, generatedFacts, latestPublishedRelease] = await Promise.all([
const [cargo, configRs, configModels, sandboxSource, npmPkg, licText, generatedFacts, latestPublishedRelease] = await Promise.all([
fetchText("Cargo.toml", source.revision, ghToken),
fetchText("crates/tui/src/config.rs", source.revision, ghToken),
fetchText("crates/tui/src/config/models.rs", source.revision, ghToken),
fetchListing("crates/tui/src/sandbox", source.revision, ghToken),
fetchText("crates/tui/src/sandbox/mod.rs", source.revision, ghToken),
fetchText("npm/codewhale/package.json", source.revision, ghToken),
fetchText("LICENSE", source.revision, ghToken),
fetchText("web/lib/facts.generated.ts", source.revision, ghToken),
@@ -288,7 +261,9 @@ export async function deriveFactsFromRemote(ghToken?: string): Promise<RepoFacts
sourceCommittedAt: source.committedAt,
version: deriveVersion(cargo),
crates: deriveCrates(cargo),
sandboxBackends: sandboxFiles ? deriveSandboxBackends(sandboxFiles) : BUILD_FACTS.sandboxBackends,
sandboxBackends: sandboxSource
? deriveSandboxBackends(sandboxSource)
: BUILD_FACTS.sandboxBackends,
providers: deriveProvidersFromConfig(configRs),
defaultModel: deriveDefaultModel(`${configRs}\n${configModels ?? ""}`),
nodeEngines: (() => {
+3 -6
View File
@@ -27,7 +27,7 @@ export interface RepoFacts {
}
export const FACTS: RepoFacts = {
"generatedAt": "2026-07-21T23:08:04.820Z",
"generatedAt": "2026-07-22T00:51:16.685Z",
"sourceRevision": null,
"sourceCommittedAt": null,
"version": "0.9.1",
@@ -52,11 +52,8 @@ export const FACTS: RepoFacts = {
"workflow-js"
],
"sandboxBackends": [
"bwrap",
"landlock (Linux)",
"process_hardening",
"seatbelt (macOS)",
"seccomp"
"seatbelt (macOS, when available)",
"bubblewrap (Linux, opt-in when installed)"
],
"providers": [
{
+124
View File
@@ -0,0 +1,124 @@
import type { Arch } from "./install-platform";
function windowsSnippet(arch: "x64" | "arm64"): string {
return `# PowerShell
$ErrorActionPreference = "Stop"
$dest = "$Env:USERPROFILE\\bin"
New-Item -ItemType Directory -Force $dest | Out-Null
$manifest = Invoke-WebRequest https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
Invoke-WebRequest \`
-Uri https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-windows-${arch}.exe \`
-OutFile "$dest\\codewhale.exe"
Invoke-WebRequest \`
-Uri https://github.com/Hmbown/CodeWhale/releases/latest/download/codew-windows-${arch}.exe \`
-OutFile "$dest\\codew.exe"
Invoke-WebRequest \`
-Uri https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-tui-windows-${arch}.exe \`
-OutFile "$dest\\codewhale-tui.exe"
$expected = @{}
$manifest.Content -split "\`n" | ForEach-Object {
$parts = $_.Trim() -split "\\s+"
if ($parts.Length -ge 2) { $expected[$parts[1]] = $parts[0].ToUpperInvariant() }
}
if ((Get-FileHash "$dest\\codewhale.exe" -Algorithm SHA256).Hash -ne $expected["codewhale-windows-${arch}.exe"]) { throw "codewhale.exe checksum mismatch" }
if ((Get-FileHash "$dest\\codew.exe" -Algorithm SHA256).Hash -ne $expected["codew-windows-${arch}.exe"]) { throw "codew.exe checksum mismatch" }
if ((Get-FileHash "$dest\\codewhale-tui.exe" -Algorithm SHA256).Hash -ne $expected["codewhale-tui-windows-${arch}.exe"]) { throw "codewhale-tui.exe checksum mismatch" }
$Env:Path = "$dest;$Env:Path"`;
}
function windowsVerify(arch: "x64" | "arm64"): string {
return `# PowerShell
$manifest = Invoke-WebRequest https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
$expected = @{}
$manifest.Content -split "\`n" | ForEach-Object {
$parts = $_.Trim() -split "\\s+"
if ($parts.Length -ge 2) { $expected[$parts[1]] = $parts[0].ToUpperInvariant() }
}
if ((Get-FileHash "$Env:USERPROFILE\\bin\\codewhale.exe" -Algorithm SHA256).Hash -ne $expected["codewhale-windows-${arch}.exe"]) { throw "codewhale.exe checksum mismatch" }
if ((Get-FileHash "$Env:USERPROFILE\\bin\\codew.exe" -Algorithm SHA256).Hash -ne $expected["codew-windows-${arch}.exe"]) { throw "codew.exe checksum mismatch" }
if ((Get-FileHash "$Env:USERPROFILE\\bin\\codewhale-tui.exe" -Algorithm SHA256).Hash -ne $expected["codewhale-tui-windows-${arch}.exe"]) { throw "codewhale-tui.exe checksum mismatch" }`;
}
export const SNIPPETS: Record<Arch, string> = {
"macos-arm64": `curl -fsSL -O https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
curl -fsSL -O \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-macos-arm64
curl -fsSL -O \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codew-macos-arm64
curl -fsSL -O \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-tui-macos-arm64
grep -E ' (codewhale|codew|codewhale-tui)-macos-arm64$' codewhale-artifacts-sha256.txt | shasum -a 256 -c -
chmod +x codewhale-macos-arm64 codew-macos-arm64 codewhale-tui-macos-arm64
xattr -d com.apple.quarantine codewhale-macos-arm64 codew-macos-arm64 codewhale-tui-macos-arm64 2>/dev/null || true
sudo mv codewhale-macos-arm64 /usr/local/bin/codewhale
sudo mv codew-macos-arm64 /usr/local/bin/codew
sudo mv codewhale-tui-macos-arm64 /usr/local/bin/codewhale-tui`,
"macos-x64": `curl -fsSL -O https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
curl -fsSL -O \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-macos-x64
curl -fsSL -O \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codew-macos-x64
curl -fsSL -O \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-tui-macos-x64
grep -E ' (codewhale|codew|codewhale-tui)-macos-x64$' codewhale-artifacts-sha256.txt | shasum -a 256 -c -
chmod +x codewhale-macos-x64 codew-macos-x64 codewhale-tui-macos-x64
xattr -d com.apple.quarantine codewhale-macos-x64 codew-macos-x64 codewhale-tui-macos-x64 2>/dev/null || true
sudo mv codewhale-macos-x64 /usr/local/bin/codewhale
sudo mv codew-macos-x64 /usr/local/bin/codew
sudo mv codewhale-tui-macos-x64 /usr/local/bin/codewhale-tui`,
"linux-x64": `curl -fsSL -O https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
curl -fsSL -O \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-linux-x64
curl -fsSL -O \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codew-linux-x64
curl -fsSL -O \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-tui-linux-x64
grep -E ' (codewhale|codew|codewhale-tui)-linux-x64$' codewhale-artifacts-sha256.txt | sha256sum -c -
chmod +x codewhale-linux-x64 codew-linux-x64 codewhale-tui-linux-x64
sudo mv codewhale-linux-x64 /usr/local/bin/codewhale
sudo mv codew-linux-x64 /usr/local/bin/codew
sudo mv codewhale-tui-linux-x64 /usr/local/bin/codewhale-tui`,
"linux-arm64": `curl -fsSL -O https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
curl -fsSL -O \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-linux-arm64
curl -fsSL -O \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codew-linux-arm64
curl -fsSL -O \\
https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-tui-linux-arm64
grep -E ' (codewhale|codew|codewhale-tui)-linux-arm64$' codewhale-artifacts-sha256.txt | sha256sum -c -
chmod +x codewhale-linux-arm64 codew-linux-arm64 codewhale-tui-linux-arm64
sudo mv codewhale-linux-arm64 /usr/local/bin/codewhale
sudo mv codew-linux-arm64 /usr/local/bin/codew
sudo mv codewhale-tui-linux-arm64 /usr/local/bin/codewhale-tui`,
"windows-x64": windowsSnippet("x64"),
"windows-arm64": windowsSnippet("arm64"),
};
function unixVerify(platform: string, checksumCommand: string): string {
return `curl -fsSL -O https://github.com/Hmbown/CodeWhale/releases/latest/download/codewhale-artifacts-sha256.txt
verify_binary() {
asset="$1"
installed="$2"
expected=$(awk -v asset="$asset" '$2 == asset { print $1 }' codewhale-artifacts-sha256.txt)
actual=$(${checksumCommand} "$installed" | awk '{ print $1 }')
if [ -z "$expected" ] || [ "$actual" != "$expected" ]; then
echo "$installed checksum mismatch" >&2
return 1
fi
}
verify_binary codewhale-${platform} /usr/local/bin/codewhale
verify_binary codew-${platform} /usr/local/bin/codew
verify_binary codewhale-tui-${platform} /usr/local/bin/codewhale-tui`;
}
export const VERIFY: Record<Arch, string> = {
"macos-arm64": unixVerify("macos-arm64", "shasum -a 256"),
"macos-x64": unixVerify("macos-x64", "shasum -a 256"),
"linux-x64": unixVerify("linux-x64", "sha256sum"),
"linux-arm64": unixVerify("linux-arm64", "sha256sum"),
"windows-x64": windowsVerify("x64"),
"windows-arm64": windowsVerify("arm64"),
};
+1 -1
View File
@@ -6,7 +6,7 @@ export const SITE_URL = "https://codewhale.net";
export const SITE_NAME = "Codewhale";
/** The one-line product identity, used as the default OG image alt text. */
export const IDENTITY_PHRASE = "One runtime. Every model. Your machine.";
export const IDENTITY_PHRASE = "One runtime. Supported hosted and local models. Your machine.";
/** Shared OG card rendered by app/opengraph-image.tsx (1200×630 PNG). */
const OG_IMAGE = {
+151 -2
View File
@@ -1,7 +1,19 @@
import { createHash } from "node:crypto";
import { existsSync, readFileSync, statSync } from "node:fs";
import {
chmodSync,
existsSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";
import { FACTS } from "./facts.generated";
import { SNIPPETS } from "./install-binary-snippets";
const root = new URL("../../", import.meta.url);
@@ -13,7 +25,12 @@ type PublicSurfaceMatrix = {
license: string;
terminology: Record<string, string>;
};
sourceCandidate: { version: string; providerCount: number; toolCount: number };
sourceCandidate: {
version: string;
providerCount: number;
toolCount: number;
sandboxBackends: string[];
};
latestPublishedRelease: {
tag: string;
version: string;
@@ -94,6 +111,7 @@ describe("public surface contracts", () => {
expect(matrix.sourceCandidate.version).toBe(FACTS.version);
expect(matrix.sourceCandidate.providerCount).toBe(FACTS.providers.length);
expect(matrix.sourceCandidate.toolCount).toBe(FACTS.toolCount);
expect(matrix.sourceCandidate.sandboxBackends).toEqual(FACTS.sandboxBackends);
expect({
tag: matrix.latestPublishedRelease.tag,
version: matrix.latestPublishedRelease.version,
@@ -160,6 +178,137 @@ describe("public surface contracts", () => {
expect(changelog).not.toContain("compare/v0.9.1...HEAD");
});
it("distinguishes two Cargo packages from the three installed commands", () => {
const installDoc = text("docs/INSTALL.md");
const installPage = text("web/app/[locale]/install/page.tsx");
expect(installDoc).toContain("Two Cargo packages are required");
expect(installDoc).toContain(
"`codewhale-cli` installs the `codewhale` and `codew` commands",
);
expect(installDoc).toContain(
"Download all three matching `codewhale`, `codew`, and `codewhale-tui`",
);
expect(installPage).toContain(
"# Install two Cargo packages; together they provide three commands",
);
expect(installPage).toContain("# codewhale + codew");
expect(installPage).toContain("The two Cargo packages install three commands");
expect(installPage).not.toContain("Install both binaries");
expect(installDoc).not.toContain("install both binaries from the release tag");
for (const platform of [
"macos-arm64",
"macos-x64",
"linux-arm64",
"linux-x64",
] as const) {
expect(SNIPPETS[platform], platform).toContain(`codew-${platform}`);
expect(SNIPPETS[platform], platform).toContain(
`sudo mv codew-${platform} /usr/local/bin/codew`,
);
}
for (const arch of ["x64", "arm64"] as const) {
expect(SNIPPETS[`windows-${arch}`], arch).toContain(`codew-windows-${arch}.exe`);
expect(SNIPPETS[`windows-${arch}`], arch).toContain(
'Get-FileHash "$dest\\codew.exe"',
);
}
});
it("checks Unix release assets under their manifest filenames before renaming", () => {
const scratch = mkdtempSync(join(tmpdir(), "codewhale-install-checksum-"));
const mockBin = join(scratch, "bin");
const curlPath = join(mockBin, "curl");
const checksumPath = join(mockBin, "checksum");
try {
const mkdir = spawnSync("/bin/mkdir", ["-p", mockBin]);
expect(mkdir.status, mkdir.stderr.toString()).toBe(0);
writeFileSync(
curlPath,
`#!/bin/sh
output=""
url=""
while [ "$#" -gt 0 ]; do
case "$1" in
-o) shift; output="$1" ;;
http*) url="$1" ;;
esac
shift
done
[ -n "$output" ] || output=$(basename "$url")
if [ "$output" = codewhale-artifacts-sha256.txt ]; then
for platform in macos-arm64 macos-x64 linux-arm64 linux-x64; do
for binary in codewhale codew codewhale-tui; do
printf 'fixture-hash %s-%s\\n' "$binary" "$platform"
done
done > "$output"
else
printf 'fixture payload for %s\\n' "$output" > "$output"
fi
`,
);
writeFileSync(
checksumPath,
`#!/bin/sh
while [ "$#" -gt 0 ]; do shift; done
while read -r _hash filename; do
if [ ! -f "$filename" ]; then
echo "manifest target missing: $filename" >&2
exit 1
fi
done
`,
);
chmodSync(curlPath, 0o755);
chmodSync(checksumPath, 0o755);
for (const command of ["shasum", "sha256sum"]) {
const link = spawnSync("/bin/ln", ["-s", checksumPath, join(mockBin, command)]);
expect(link.status, link.stderr.toString()).toBe(0);
}
for (const platform of [
"macos-arm64",
"macos-x64",
"linux-arm64",
"linux-x64",
] as const) {
const lines = SNIPPETS[platform].split("\n");
const checksumLine = lines.findIndex((line) => line.includes(" -c -"));
expect(checksumLine, platform).toBeGreaterThan(-1);
const result = spawnSync(
"/bin/bash",
["-o", "pipefail", "-eu", "-c", lines.slice(0, checksumLine + 1).join("\n")],
{
cwd: scratch,
env: { ...process.env, PATH: `${mockBin}:${process.env.PATH ?? ""}` },
},
);
expect(result.status, `${platform}: ${result.stderr.toString()}`).toBe(0);
}
} finally {
rmSync(scratch, { recursive: true, force: true });
}
});
it("qualifies the resolved audit path and best-effort persistence", () => {
const installPage = text("web/app/[locale]/install/page.tsx");
expect(matrix.trust.audit).toContain("best-effort");
expect(matrix.trust.audit).toContain("$CODEWHALE_HOME");
expect(installPage).toContain(
"const CONFIG_TREE = `$CODEWHALE_HOME/ (default: ~/.codewhale/)",
);
expect(installPage).toContain(
"best-effort credential / approval / elevation events",
);
expect(installPage).toContain("尽力写入的凭证 / 审批 / 提权事件");
expect(installPage).not.toContain(
"audit.log credential / approval / elevation audit trail",
);
});
it("keeps modes, permission postures, and idle shortcuts exact", () => {
const modes = text("docs/MODES.md");
const keys = text("docs/KEYBINDINGS.md");
+11 -10
View File
@@ -5,7 +5,7 @@
*
* Sources of truth:
* - <repo>/Cargo.toml → version, workspace crates
* - <repo>/crates/tui/src/sandbox/*.rs → sandbox backends
* - <repo>/crates/tui/src/sandbox/mod.rs → enforced sandbox markers
* - <repo>/crates/tui/src/config.rs → provider list (ApiProvider enum), DEFAULT_TEXT_MODEL
* - <repo>/npm/codewhale/package.json → node engines
* - <repo>/crates/tui/src/tools/*.rs → tool count (ToolSpec impls)
@@ -42,15 +42,16 @@ export function deriveCrates() {
}
export function deriveSandboxBackends() {
const dir = join(REPO_ROOT, "crates/tui/src/sandbox");
if (!existsSync(dir)) return [];
const files = readdirSync(dir)
.filter((f) => f.endsWith(".rs"))
.map((f) => f.replace(/\.rs$/, ""))
.filter((f) => !["mod", "policy", "backend", "opensandbox", "windows"].includes(f))
.sort();
const map = { seatbelt: "seatbelt (macOS)", landlock: "landlock (Linux)" };
return files.map((f) => map[f] ?? f);
const source = read("crates/tui/src/sandbox/mod.rs");
return source ? deriveSandboxBackendsFromSource(source) : [];
}
export function deriveSandboxBackendsFromSource(source) {
const marker = source.match(
/pub const PUBLIC_SANDBOX_BACKENDS\s*:\s*&\[&str\]\s*=\s*&\[([\s\S]*?)\];/,
);
if (!marker) return [];
return [...marker[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]);
}
/**