Compare commits

..
Author SHA1 Message Date
CodeWhale Bot 6ec8176f97 chore(changelog): document maintainer and cron secret rules 2026-11-13 05:34:06 -08:00
CodeWhale Bot d544046fb3 web: refuse periodic repeated patterns in maintainer and cron secrets
Update secretStrongEnough to reject values that repeat a pattern with a period up to half the secret length, closing repeated short strings that pass the 8-distinct-character floor while leaving random hex, UUID and base64 secrets accepted. Update the 503 unavailability messages, operator docs and weak-secret test cases accordingly.

Checks: web npm test (7 node + 1316 vitest in 92 files), npx tsc --noEmit, npm run lint, npm run check:docs and npm run check pass.
2026-11-13 05:34:05 -08:00
CodeWhale BotandClaude Sonnet 5.5 06a5f9bbe1 web: accept hex and UUID secrets, and refuse secrets that contain lone surrogates
The distinct-character floor for the maintainer token and the cron secret is now 8
instead of 16, so a random hex secret (16 possible symbols) or a UUID is accepted.
A secret containing an unpaired UTF-16 surrogate is refused, since such a value is
encoded as the replacement character when it is compared. The documentation states
the new floor and gives a command for generating a secret.

Checks: web npm test 7 node + 1316 vitest in 92 files, tsc, lint and check:docs pass.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-13 05:34:03 -08:00
CodeWhale BotandClaude Sonnet 5.5 bed977ba04 web: bind admin sessions to the maintainer token and apply one strength rule to both secrets
A session now records a SHA-256 fingerprint of the MAINTAINER_TOKEN it was
issued under, and validation requires that fingerprint to match the token
currently configured. Changing the token therefore ends every open admin
session, sessions stored without a fingerprint are no longer accepted, and
a session is only issued when the configured token meets the minimum. The
draft post route, merch operator route, merch interest list and admin page
pass the configured token to validation; the extra length checks added at
those gates are dropped because validation now covers them.

The minimum is one rule shared by MAINTAINER_TOKEN and CRON_SECRET: 32 to
512 characters, counted by code point, with at least 16 distinct
characters. 512 matches the sign-in form's submit limit. A CRON_SECRET that
does not meet it leaves the manual cron trigger answering 503 with the
requirement, before the limiter or the comparison. The admin page logs the
requirement when the configured token is too weak, so operators can find it
in the Worker logs.

Tests: session binding and legacy records (community-agent), rotation at
login and at the merch interest list, weak token and weak cron secret
patterns, and the exact minimum. Web suite 1314 passed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-13 05:34:02 -08:00
CodeWhale BotandClaude Sonnet 5.5 b6b3013e7e web: rate limit the manual cron trigger and require a 32-character maintainer token
The manual /api/cron trigger now consults the shared rate limiter under its
own key before comparing the submitted secret, answers 429 with Retry-After
when the budget is spent, and answers 503 when the limiter is missing or
fails, so the secret is never compared without a bound on attempts.

Maintainer sign-in treats a MAINTAINER_TOKEN shorter than 32 characters as
unavailable: the login route answers 503 with a message naming the minimum,
and the admin page, the draft post route, the merch operator route and the
merch interest list apply the same check so an existing session stops being
honoured while the configured token is too short.

Tests: cron route (5), admin login (6, two new), merch interest (one new);
existing fixtures that used short tokens now use 32+ characters.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-13 05:34:00 -08:00
44 changed files with 355 additions and 201 deletions

No files matched your search

+3 -3
View File
@@ -48,7 +48,7 @@
tags: cnb:arch:amd64
cpus: 16
docker:
image: rust:1.100.0-bookworm
image: rust:1.88-bookworm
# codewhale-cnb-bridge GitHub App credentials from the CNB KeyStore
# (codewhale.net/codewhale-ci-secrets, github-bridge.yml), injected as
# environment variables. Values are never printed.
@@ -112,7 +112,7 @@
tags: cnb:arch:amd64
cpus: 16
docker:
image: rust:1.100.0-bookworm
image: rust:1.88-bookworm
stages:
- name: install release dependencies
script: |
@@ -163,7 +163,7 @@ main:
$:
tag_push:
- docker:
image: rust:1.100.0-bookworm
image: rust:1.88-bookworm
stages:
- name: build linux x64 release assets (static)
# The static full-LTO link can also outlive CNB's default no-output
+1 -1
View File
@@ -1,4 +1,4 @@
ARG RUST_VERSION=1.100.0
ARG RUST_VERSION=1.88
FROM rust:${RUST_VERSION}-slim-bookworm
# Native DBus headers and pkg-config are required by libdbus-sys during
+1 -1
View File
@@ -4,7 +4,7 @@
"context": "..",
"dockerfile": "Dockerfile",
"args": {
"RUST_VERSION": "1.100.0"
"RUST_VERSION": "1.88"
}
},
"customizations": {
+2 -72
View File
@@ -248,7 +248,7 @@ assert.match(nightly, /startsWith\(matrix\.target, 'aarch64-'\).*runner\.arch ==
const nightlyArmMuslSetup = namedStep(nightly, "Install Linux ARM64 musl toolchain");
assert.match(nightlyArmMuslSetup, /matrix\.target == 'aarch64-unknown-linux-musl'/);
assert.match(nightlyArmMuslSetup, /apt-get install -y binutils musl-tools/);
assert.match(nightlyArmMuslSetup, /rustup target add aarch64-unknown-linux-musl/);
assert.match(nightlyArmMuslSetup, /rustup target add --toolchain stable aarch64-unknown-linux-musl/);
const nightlyArmStaticSmoke = namedStep(
nightly,
"Verify static Linux ARM64 binary and launch",
@@ -373,7 +373,7 @@ assert.deepEqual([...new Set(valuesForKey(artifacts, "target"))].sort(), expecte
const releaseMuslBuild = namedStep(artifacts, "Build static Linux binaries (musl)");
assert.match(releaseMuslBuild, /endsWith\(matrix\.target, '-unknown-linux-musl'\)/);
assert.match(releaseMuslBuild, /apt-get install -y binutils musl-tools/);
assert.match(releaseMuslBuild, /rustup target add \$\{\{ matrix\.target \}\}/);
assert.match(releaseMuslBuild, /rustup target add --toolchain stable \$\{\{ matrix\.target \}\}/);
assert.match(
releaseMuslBuild,
/cargo build --profile dist --locked --target \$\{\{ matrix\.target \}\} -p codewhale-cli/,
@@ -934,76 +934,6 @@ assert.ok(
"release parity must allow the full CI suite's cold-build budget",
);
const pinnedToolchain = read("rust-toolchain.toml").match(/^channel\s*=\s*"([^"]+)"/m)[1];
assert.match(pinnedToolchain, /^\d+\.\d+\.\d+$/, "rust-toolchain.toml must pin an exact Rust release");
const msrv = read("Cargo.toml").match(/^rust-version\s*=\s*"([^"]+)"/m)[1];
assert.match(msrv, /^\d+\.\d+$/, "Cargo.toml must declare a minor MSRV");
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const stripWorkflowComments = (source) =>
source
.split("\n")
.map((line) => line.replace(/(^|\s)#.*$/, ""))
.join("\n");
const workflowDir = path.join(repoRoot, ".github", "workflows");
const workflowSources = new Map();
for (const file of fs.readdirSync(workflowDir).filter((name) => /\.ya?ml$/.test(name)).sort()) {
workflowSources.set(file, stripWorkflowComments(read(`.github/workflows/${file}`)));
}
const ciSource = workflowSources.get("ci.yml");
const msrvHeader = /^ msrv:[ \t]*$/m.exec(ciSource);
assert.ok(msrvHeader, "ci.yml must keep an MSRV job");
const msrvStart = msrvHeader.index;
const afterMsrvHeader = msrvStart + msrvHeader[0].length;
const nextJob = /^ \S/m.exec(ciSource.slice(afterMsrvHeader));
const msrvEnd = nextJob ? afterMsrvHeader + nextJob.index : ciSource.length;
const selectorValue = (value) => value.trim().replace(/^(["'])(.*)\1$/, "$2");
const inMsrvJob = (index) => index >= msrvStart && index < msrvEnd;
for (const [file, source] of workflowSources) {
const selectors = [
...[...source.matchAll(/dtolnay\/rust-toolchain@([^\s]+)/g)].map((m) => ["action ref", m[1], m.index]),
...[...source.matchAll(/^[ \t]+toolchain:[ \t]*([^\r\n]*)$/gm)].map((m) => ["toolchain input", selectorValue(m[1]), m.index]),
...[...source.matchAll(/^[ \t]+rust-version:[ \t]*([^\r\n]*)$/gm)].map((m) => ["rust-version input", selectorValue(m[1]), m.index]),
...[...source.matchAll(/^[ \t]+RUSTUP_TOOLCHAIN:[ \t]*([^\r\n]*)$/gm)].map((m) => ["RUSTUP_TOOLCHAIN", selectorValue(m[1]), m.index]),
...[...source.matchAll(/\bcargo\s+\+\s*["']?([^\s"']+)/g)].map((m) => ["cargo + selector", m[1], m.index]),
];
for (const [kind, value, index] of selectors) {
if (kind === "action ref" && (/^[0-9a-f]{40}$/.test(value) || value === "master")) continue;
const expected = file === "ci.yml" && inMsrvJob(index) ? msrv : pinnedToolchain;
assert.equal(value, expected, `${file}: ${kind} ${value} must equal ${expected}`);
}
assert.doesNotMatch(source, /rustup (?:target|component) add --toolchain/, `${file}: rustup adds must follow rust-toolchain.toml`);
}
const msrvJob = ciSource.slice(msrvStart, msrvEnd);
assert.match(msrvJob, new RegExp(`RUSTUP_TOOLCHAIN: "${escapeRegExp(msrv)}"`), "the MSRV job must override rust-toolchain.toml for every step");
assert.match(msrvJob, new RegExp(`toolchain: "?${escapeRegExp(msrv)}"?`));
assert.match(msrvJob, new RegExp(`cargo \\+${escapeRegExp(msrv)} check --workspace --locked`));
assert.doesNotMatch(msrvJob, new RegExp(`\\b${escapeRegExp(pinnedToolchain)}\\b`), "the MSRV job must not select the pinned release toolchain");
const rootDockerfile = read("Dockerfile");
const devcontainerDockerfile = read(".devcontainer/Dockerfile");
const devcontainerConfig = read(".devcontainer/devcontainer.json");
const tencentCnb = read("deploy/tencent-lighthouse/cnb/cnb.yml.example");
const pinnedTag = `${pinnedToolchain}-bookworm`;
for (const [label, source] of [
["Dockerfile", rootDockerfile],
[".devcontainer/Dockerfile", devcontainerDockerfile],
]) {
assert.match(source, new RegExp(`^ARG RUST_VERSION=${escapeRegExp(pinnedToolchain)}$`, "m"), `${label} must default RUST_VERSION to the pinned toolchain`);
assert.match(source, /^FROM .*\brust:\$\{RUST_VERSION\}/m, `${label} must build from the RUST_VERSION image`);
}
assert.match(devcontainerConfig, new RegExp(`"RUST_VERSION":\\s*"${escapeRegExp(pinnedToolchain)}"`), ".devcontainer/devcontainer.json must pass the pinned RUST_VERSION build arg");
for (const [label, source] of [
[".cnb.yml", cnb],
["deploy/tencent-lighthouse/cnb/cnb.yml.example", tencentCnb],
]) {
const tags = [...stripWorkflowComments(source).matchAll(/^[ \t]*image:[ \t]*([^\r\n]*)$/gm)]
.map((m) => selectorValue(m[1]))
.filter((value) => value.startsWith("rust:"))
.map((value) => value.slice("rust:".length));
assert.ok(tags.length > 0, `${label} must declare a Rust build image`);
assert.deepEqual([...new Set(tags)], [pinnedTag], `${label}: Rust build images must match rust-toolchain.toml`);
}
console.log(
"Workflow contracts OK: 6-target/12-asset single-runtime nightly and exact-head 8-target/38-asset release candidate.",
);
+5 -5
View File
@@ -23,17 +23,17 @@ jobs:
continue-on-error: ${{ matrix.checks == 'advisories' }}
steps:
- uses: actions/checkout@v7
# rust-toolchain.toml pins `channel = "1.100.0"` with no target, so
# rust-toolchain.toml pins `channel = "stable"` with no target, so
# inside the action's alpine (musl) container rustup resolves it to
# `1.100.0-x86_64-unknown-linux-musl`, which the container does not
# `stable-x86_64-unknown-linux-musl` — which the container does not
# have. Without provisioning, the action's `rustup show` step prints
# "error: override toolchain '1.100.0-x86_64-unknown-linux-musl' is not
# "error: override toolchain 'stable-x86_64-unknown-linux-musl' is not
# installed" and then auto-installs mid-run (network-dependent ~12s
# detour, hard failure if the download stalls). `rust-version` makes the
# entrypoint run `rustup default 1.100.0` (same musl host triple) before
# entrypoint run `rustup default stable` (same musl host triple) before
# cargo-deny touches the workspace, so the toolchain file resolves to an
# already-installed toolchain on every run.
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check ${{ matrix.checks }}
rust-version: 1.100.0
rust-version: stable
+11 -13
View File
@@ -203,7 +203,7 @@ jobs:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@1.100.0
- uses: dtolnay/rust-toolchain@stable
- uses: actions/[email protected]
with:
node-version: 22
@@ -428,7 +428,7 @@ jobs:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.100.0
toolchain: stable
- uses: mozilla-actions/[email protected]
id: sccache
# The GitHub Actions cache backend is main-only, mirroring
@@ -487,7 +487,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
if: needs.changes.outputs.heavy == 'true'
with:
toolchain: 1.100.0
toolchain: stable
components: rustfmt, clippy
- uses: mozilla-actions/[email protected]
id: sccache
@@ -728,8 +728,6 @@ jobs:
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
timeout-minutes: 45
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: "1.89"
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@master
@@ -753,7 +751,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@1.100.0
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/[email protected]
id: sccache
# Main-only GitHub Actions cache backend; see the Safety gate job.
@@ -827,11 +825,11 @@ jobs:
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
shell: pwsh
run: ./scripts/installer/installer-path-regression.tests.ps1 -AllowUserPathMutation
- uses: dtolnay/rust-toolchain@1.100.0
- uses: dtolnay/rust-toolchain@stable
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
- name: Install macOS LLVM tools
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
run: rustup component add llvm-tools
run: rustup component add llvm-tools --toolchain stable
- name: Omit macOS build debug metadata
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
shell: bash
@@ -1224,7 +1222,7 @@ jobs:
if: needs.changes.outputs.heavy == 'true'
shell: pwsh
run: ./scripts/installer/update-user-path.tests.ps1
- uses: dtolnay/rust-toolchain@1.100.0
- uses: dtolnay/rust-toolchain@stable
if: needs.changes.outputs.heavy == 'true'
- name: Check every workspace target compiles on Windows
if: needs.changes.outputs.heavy == 'true'
@@ -1269,7 +1267,7 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@1.100.0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
cache-bin: false
@@ -1307,7 +1305,7 @@ jobs:
run: echo "No executable Rust changes detected; preserving required npm wrapper smoke context."
- uses: actions/checkout@v7
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
- uses: dtolnay/rust-toolchain@1.100.0
- uses: dtolnay/rust-toolchain@stable
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
- uses: mozilla-actions/[email protected]
id: sccache
@@ -1366,7 +1364,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@1.100.0
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/[email protected]
id: sccache
# Main-only GitHub Actions cache backend; see the Safety gate job.
@@ -1425,7 +1423,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@1.100.0
- uses: dtolnay/rust-toolchain@stable
- name: Install Linux system dependencies
if: runner.os == 'Linux'
run: |
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.100.0
toolchain: stable
components: rustfmt
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
+2 -2
View File
@@ -67,7 +67,7 @@ jobs:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.100.0
toolchain: stable
targets: ${{ matrix.target }}
- uses: mozilla-actions/[email protected]
id: sccache
@@ -99,7 +99,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y binutils musl-tools
rustup target add aarch64-unknown-linux-musl
rustup target add --toolchain stable aarch64-unknown-linux-musl
- name: Build
shell: bash
# Nightly artifacts are disposable smoke binaries (14-day retention),
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
steps:
- uses: actions/checkout@v7
- name: Install Rust target
uses: dtolnay/rust-toolchain@1.100.0
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-unknown-linux-ohos
- name: Install OpenHarmony native SDK
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
node-version: '22'
cache: npm
cache-dependency-path: pet/package-lock.json
- uses: dtolnay/rust-toolchain@1.100.0
- uses: dtolnay/rust-toolchain@stable
- run: npm --prefix pet ci --ignore-scripts
- run: npm --prefix pet run check
- run: npm --prefix pet run sync
@@ -60,7 +60,7 @@ jobs:
cache-dependency-path: pet/package-lock.json
- run: npm --prefix pet ci --ignore-scripts
- run: npm --prefix pet run check
- uses: dtolnay/rust-toolchain@1.100.0
- uses: dtolnay/rust-toolchain@stable
if: runner.os == 'macOS'
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
if: runner.os == 'macOS'
+2 -2
View File
@@ -170,7 +170,7 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master 2026-07-18
with:
toolchain: 1.100.0
toolchain: stable
targets: ${{ matrix.target }}
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
if: runner.os == 'Linux'
@@ -215,7 +215,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y binutils musl-tools
rustup target add ${{ matrix.target }}
rustup target add --toolchain stable ${{ matrix.target }}
cargo build --profile dist --locked --target ${{ matrix.target }} -p codewhale-cli
- name: Configure Android NDK linker
if: matrix.target == 'aarch64-linux-android' && runner.os == 'Linux'
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
package-manager-cache: false
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # stable 2026-07-18
with:
toolchain: 1.100.0
toolchain: stable
- name: Match dispatch to the requested commit
id: source
shell: bash
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master 2026-07-18
with:
toolchain: 1.100.0
toolchain: stable
components: clippy, rustfmt
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
id: sccache
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@1.100.0
- uses: dtolnay/rust-toolchain@stable
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Run cargo audit
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
with:
ref: ${{ inputs.expected_sha }}
fetch-depth: 1
- uses: dtolnay/rust-toolchain@1.100.0
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest
- name: Run focused tests
shell: bash
+1 -1
View File
@@ -7,7 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- **Source-build toolchain:** Rust 1.100.0 is pinned consistently across local source builds, CI, Docker, Dev Containers and CNB recipes. The minimum supported Rust version remains 1.89.
- **Plugin index hooks:** hooks with the same name and event now have distinct row IDs, so clients can keep every configured hook visible.
- **Renaming sessions with Chinese titles:** the session picker no longer rejects a Chinese, Japanese, Korean or emoji title of more than about 33 characters as "Title must be 1–100 characters". It now counts characters, as the message says and as `/rename` and the Runtime API already did, so any title up to 100 characters is accepted. Thanks [@luochen211](https://github.com/luochen211) (PR 7322).
@@ -22,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Plugin index (Runtime API):** a new `GET /v1/apps/plugin-index` route lists plugins, skills, connectors, hooks and hand-written commands with kind, source and state, and `/v1/commands` rows say which plugin (`plugin_id`) added a command and whether a command came from the project (`workspace_sourced`) (CW-8092).
- **Model commands and routing:** `codewhale model list` now lists every custom endpoint you have configured, under its own name. `codewhale model resolve` no longer refuses a model on a custom endpoint, router or local server whose model list has not been fetched yet; once a list is fetched, a model missing from it is still refused. Automatic routing marks a context window as verified only when the route itself supplies it, and no longer reports a guessed output limit.
- **ACP:** when a plugin tool asks the core to run a tool that needs approval, the editor now gets a `session/request_permission` on the plugin tool's call that names the nested tool and its input; before, the prompt ended with an internal error (CW-8050).
- **Website maintainer and cron secrets:** the manual cron trigger is rate limited, admin sign-in and the cron secret require a 32 to 512 character secret with at least 8 distinct characters that is not a repeated pattern, and changing the maintainer token signs out existing admin sessions.
- **README and contributor credit:** the repository front page now opens the way the website does: the line "The model can be anyone's. The agent should be yours.", one sentence on what Codewhale is, the install command and one picture, with reference detail moved into the linked guides; the 18 translated READMEs follow the same structure and say they are machine-drafted. The contributor wall and the website now count the same 433 people (407 GitHub accounts and the commit authors whose email is not linked to an account), where the website had counted accounts only. `DEPENDENCY_SECURITY.md` moved into `docs/dependency-maintenance.md`, and `.github/SUPPORT.md` says where to ask for help.
- **Website Plugins page:** The website's Plugins page now describes what the released version does, and a labelled section says what 0.10.3 adds: one Plugins place, Standard and Creator mode, and the Permissions control.
- **First-time guide on the website:** a new docs page, "Use Codewhale for the first time" (`/docs/first-time`, English and Simplified Chinese), walks someone who has never used a terminal through opening one, installing, choosing the folder Codewhale works in and when that choice matters, opening a terminal in a folder from Finder or File Explorer, connecting a model, a first task, and stopping, undoing and quitting. The install page gains a short "New to the terminal?" block for macOS and Windows that links to it, and `llms.txt` carries the same steps.
+1 -1
View File
@@ -12,7 +12,7 @@
# Or mount an env file:
# docker run --rm -it --env-file .env codewhale
ARG RUST_VERSION=1.100.0
ARG RUST_VERSION=1.88
# ── Stage 1: Build ────────────────────────────────────────────────────
FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-slim-bookworm AS builder
+1 -1
View File
@@ -7,7 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- **Source-build toolchain:** Rust 1.100.0 is pinned consistently across local source builds, CI, Docker, Dev Containers and CNB recipes. The minimum supported Rust version remains 1.89.
- **Plugin index hooks:** hooks with the same name and event now have distinct row IDs, so clients can keep every configured hook visible.
- **Renaming sessions with Chinese titles:** the session picker no longer rejects a Chinese, Japanese, Korean or emoji title of more than about 33 characters as "Title must be 1–100 characters". It now counts characters, as the message says and as `/rename` and the Runtime API already did, so any title up to 100 characters is accepted. Thanks [@luochen211](https://github.com/luochen211) (PR 7322).
@@ -22,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Plugin index (Runtime API):** a new `GET /v1/apps/plugin-index` route lists plugins, skills, connectors, hooks and hand-written commands with kind, source and state, and `/v1/commands` rows say which plugin (`plugin_id`) added a command and whether a command came from the project (`workspace_sourced`) (CW-8092).
- **Model commands and routing:** `codewhale model list` now lists every custom endpoint you have configured, under its own name. `codewhale model resolve` no longer refuses a model on a custom endpoint, router or local server whose model list has not been fetched yet; once a list is fetched, a model missing from it is still refused. Automatic routing marks a context window as verified only when the route itself supplies it, and no longer reports a guessed output limit.
- **ACP:** when a plugin tool asks the core to run a tool that needs approval, the editor now gets a `session/request_permission` on the plugin tool's call that names the nested tool and its input; before, the prompt ended with an internal error (CW-8050).
- **Website maintainer and cron secrets:** the manual cron trigger is rate limited, admin sign-in and the cron secret require a 32 to 512 character secret with at least 8 distinct characters that is not a repeated pattern, and changing the maintainer token signs out existing admin sessions.
- **README and contributor credit:** the repository front page now opens the way the website does: the line "The model can be anyone's. The agent should be yours.", one sentence on what Codewhale is, the install command and one picture, with reference detail moved into the linked guides; the 18 translated READMEs follow the same structure and say they are machine-drafted. The contributor wall and the website now count the same 433 people (407 GitHub accounts and the commit authors whose email is not linked to an account), where the website had counted accounts only. `DEPENDENCY_SECURITY.md` moved into `docs/dependency-maintenance.md`, and `.github/SUPPORT.md` says where to ask for help.
- **Website Plugins page:** The website's Plugins page now describes what the released version does, and a labelled section says what 0.10.3 adds: one Plugins place, Standard and Creator mode, and the Permissions control.
- **First-time guide on the website:** a new docs page, "Use Codewhale for the first time" (`/docs/first-time`, English and Simplified Chinese), walks someone who has never used a terminal through opening one, installing, choosing the folder Codewhale works in and when that choice matters, opening a terminal in a folder from Finder or File Explorer, connecting a model, a first task, and stopping, undoing and quitting. The install page gains a short "New to the terminal?" block for macOS and Windows that links to it, and `llms.txt` carries the same steps.
@@ -15,7 +15,7 @@ main:
npm test
- docker:
image: rust:1.100.0-bookworm
image: rust:1.88-bookworm
stages:
- name: release version check
script: |
+1 -1
View File
@@ -17,7 +17,7 @@ they do not cost context on machines that will never hit them.
worktrees). Those spawned shells only see the repo's `rust-toolchain.toml`
override while inside `/workspace`, so without a global default they fail with
"rustup could not choose a version of rustc to run". The update script runs
`rustup default 1.100.0` to fix this.
`rustup default stable` to fix this.
- **Known env-specific test failures at `/workspace` (not code bugs):** because
the checkout sits directly under `/`, two `codewhale-tui` subagent tests fail
here — `git_repo_root_reports_attempted_paths_when_no_repo_found` (cannot
+11 -11
View File
@@ -49,8 +49,8 @@ cargo build --release --locked -p codewhale-cli
target/release/codewhale --version
```
See the Cargo section for build prerequisites; use the repository's pinned Rust
1.100.0 (declared minimum 1.89). Windows uses `target/release/codewhale.exe`
See the Cargo section for build prerequisites; use the repository's stable Rust
selection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`
and needs MSVC. A development build is separate from a signed release package.
For mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).
The [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is
@@ -543,25 +543,25 @@ cargo install --path crates/cli --locked # installs ~/.cargo/bin/codewhale
Things to know (observed):
* The repo contains `rust-toolchain.toml` (`channel = "1.100.0"`). With rustup,
the first `cargo` command inside the checkout **silently downloads Rust
1.100.0** (about 250 MB) if you do not have it, whatever your default is.
* The workspace treats every compiler warning as an error. The declared
minimum is Rust 1.89, which CI's MSRV job checks, but release binaries and
the repository's own checks use 1.100.0. Without rustup, use rustc 1.89 or
newer.
* The repo contains `rust-toolchain.toml` (`channel = "stable"`). The first
`cargo` command inside the checkout **silently downloads the latest stable
toolchain** (about 250 MB), whatever your default is.
* The workspace treats every compiler warning as an error. With Rust 1.89
the build **fails** after about 11 minutes with 8
`error: this lint expectation is unfulfilled` errors in `codewhale-tui`. Use
the stable toolchain the repo selects. Don't pass an older `+toolchain`.
* The workspace release profile uses thin LTO. On a 4-vCPU / 15 GB VM, the
`codewhale-tui` crate alone compiled for more than an hour, peaking at
6–8 GB of RAM. Budget 16 GB or more, and expect this to be the slowest install
path by far. `target/` grew past 1.2 GB.
Tested result: **works** on Rust 1.98.1, the version the repository selected when this was measured.
Tested result: **works** on the repo-selected stable Rust (1.98.1).
* `Finished release profile … in 83m 12s`. A Nix build competed for CPU and
memory for most of that time, so treat it as an upper bound.
* `target/` ended at **3.1 GB**. Delete it afterwards with `cargo clean`.
* The binary reports `codewhale 0.10.0 (dev)`, and `exec` worked.
* Cargo prints `warning: default toolchain implicitly overridden with
1.100.0-x86_64-unknown-linux-gnu by rustup toolchain file`, which is harmless.
stable-x86_64-unknown-linux-gnu by rustup toolchain file`, which is harmless.
* **Cargo builds provide no `codew`.**
**Upgrade:** re-run the same `cargo install … --force` (for crates.io, you can
+1 -1
View File
@@ -16,7 +16,7 @@
子智能体(subagent)工作树)。这些被拉起的 shell 只有在 `/workspace` 内部
才能看到仓库的 `rust-toolchain.toml` 覆盖设置,所以没有全局默认值时,
它们会以“rustup could not choose a version of rustc to run”失败。
更新脚本会运行 `rustup default 1.100.0` 来修复这一点。
更新脚本会运行 `rustup default stable` 来修复这一点。
- **`/workspace` 上已知的环境相关测试失败(不是代码缺陷):** 因为检出目录
直接位于 `/` 之下,有两个 `codewhale-tui` 子智能体测试会在这里失败——
`git_repo_root_reports_attempted_paths_when_no_repo_found`(无法在不可写的
+5 -5
View File
@@ -28,7 +28,7 @@ cargo build --release --locked -p codewhale-cli
target/release/codewhale --version
```
构建前置条件见下文 Cargo 章节;使用仓库固定的 Rust 1.100.0,最低声明版本
构建前置条件见下文 Cargo 章节;使用仓库选定的 stable Rust,最低声明版本
为 1.89。首次构建可能耗时较长。Windows 的可执行文件为
`target/release/codewhale.exe`,并需要 MSVC 工具链。开发构建不等于已签名发布包。
@@ -404,15 +404,15 @@ cargo install --path crates/cli --locked # installs ~/.cargo/bin/codewhale
需要知道的事(实际观察到的):
* 仓库里有 `rust-toolchain.toml`(`channel = "1.100.0"`)。使用 rustup 时,如果本机没有 Rust 1.100.0,在检出目录里运行的第一条 `cargo` 命令,无论你的默认工具链是什么,都会**悄悄下载 Rust 1.100.0**(约 250 MB)。
* workspace 把所有编译器警告都当作错误。声明的最低版本是 Rust 1.89,CI 的 MSRV 任务会检查它;但发布二进制和仓库自身的检查使用 1.100.0。不使用 rustup 时,请使用 1.89 或更新的 rustc。
* 仓库里有 `rust-toolchain.toml`(`channel = "stable"`)。在检出目录里运行的第一条 `cargo` 命令,无论你的默认工具链是什么,都会**悄悄下载最新的 stable 工具链**(约 250 MB)。
* workspace 把所有编译器警告都当作错误。用 Rust 1.89 时,构建会在大约 11 分钟后**失败**,在 `codewhale-tui` 中出现 8 个 `error: this lint expectation is unfulfilled`。请使用仓库选定的 stable 工具链,不要传入更旧的 `+toolchain`。
* workspace 的 release profile 使用 thin LTO。在 4 vCPU / 15 GB 的虚拟机上,仅 `codewhale-tui` 这一个 crate 就编译了一个多小时,内存峰值 6–8 GB。请准备 16 GB 或更多内存,并预期这是迄今最慢的安装路径。`target/` 增长超过了 1.2 GB。
测试结果:用 Rust 1.98.1(测量时仓库选定的版本)**可行**。
测试结果:用仓库选定的 stable Rust(1.98.1)**可行**。
* `Finished release profile … in 83m 12s`。其间大部分时间有一个 Nix 构建在争抢 CPU 和内存,所以请把它当作上限。
* `target/` 最终达到 **3.1 GB**。之后用 `cargo clean` 删除。
* 该二进制报告 `codewhale 0.10.0 (dev)`,`exec` 可用。
* Cargo 会打印 `warning: default toolchain implicitly overridden with 1.100.0-x86_64-unknown-linux-gnu by rustup toolchain file`,这是无害的。
* Cargo 会打印 `warning: default toolchain implicitly overridden with stable-x86_64-unknown-linux-gnu by rustup toolchain file`,这是无害的。
* **Cargo 构建不提供 `codew`。**
**升级:** 重新运行同样的 `cargo install … --force`(对 crates.io 可以加 `--version X.Y.Z`)。`codewhale update` 会拒绝处理 Cargo 安装。**卸载:** `cargo uninstall codewhale-cli`,然后见 §12。
+1 -2
View File
@@ -1,3 +1,2 @@
[toolchain]
channel = "1.100.0"
components = ["rustfmt", "clippy"]
channel = "stable"
+9 -2
View File
@@ -9,14 +9,21 @@ GITHUB_TOKEN=
GITHUB_REPO=codewhale-hq/CodeWhale
# Optional — required to manually invoke /api/cron
# (cloudflare cron triggers don't need this; they set cf-cron).
# (cloudflare cron triggers don't need this; they set cf-cron). When set it must
# be 32 to 512 characters with at least 8 distinct, and a shorter value repeated
# is refused; otherwise the trigger stays disabled.
# Generate one with: openssl rand -base64 33
CRON_SECRET=
# Optional — defaults to deepseek-flash.
DEEPSEEK_MODEL=deepseek-flash
DEEPSEEK_BASE_URL=https://api.deepseek.com
# Admin panel auth. Set to a random secret; enter it in the /admin login form.
# Admin panel auth. Set to a random secret of 32 to 512 characters with at
# least 8 distinct characters, and a shorter value repeated is refused (anything
# else leaves admin sign-in disabled); enter it in the /admin login form.
# Changing it signs out open admin sessions.
# Generate one with: openssl rand -base64 33
MAINTAINER_TOKEN=
# GitHub PAT for posting comments via /admin. Needs issues:write scope.
+5 -3
View File
@@ -87,8 +87,8 @@ form. The form posts to `/api/admin/login`; the token never goes in the URL.
|---|---|---|
| `DEEPSEEK_API_KEY` | Yes | DeepSeek API key for the community agent |
| `GITHUB_TOKEN` | Optional | Fine-grained PAT for GitHub API (raises rate limit) |
| `CRON_SECRET` | Optional | Shared secret for manual cron invocation |
| `MAINTAINER_TOKEN` | Optional | Auth token for /admin panel |
| `CRON_SECRET` | Optional | Shared secret for manual cron invocation (32 to 512 characters, at least 8 distinct, and a shorter value repeated is refused; otherwise the trigger stays disabled); attempts are rate limited (5 per minute per location) |
| `MAINTAINER_TOKEN` | Optional | Auth token for /admin panel; 32 to 512 characters with at least 8 distinct, and a shorter value repeated is refused (otherwise sign-in stays disabled); changing it signs out open sessions |
| `MAINTAINER_GITHUB_PAT` | Optional | GitHub PAT with `issues:write` scope for posting comments |
## Initial deployment
@@ -103,7 +103,9 @@ One-time setup before the first `npm run deploy`:
Copy the returned `id` values into the matching `CURATED_KV` and
`NEXT_INC_CACHE_KV` bindings in `cloudflare.config.ts`.
2. **Set secrets:**
2. **Set secrets:** generate `MAINTAINER_TOKEN` and `CRON_SECRET` with
`openssl rand -base64 33` (any random value of 32 to 512 characters with at
least 8 distinct and not a shorter value repeated works; `openssl rand -hex 32` is fine).
```bash
npx wrangler secret put DEEPSEEK_API_KEY
npx wrangler secret put MAINTAINER_TOKEN
+2 -2
View File
@@ -20,10 +20,10 @@ Env (mirrors `.env.example`):
| `DEEPSEEK_API_KEY` | DeepSeek platform key (`sk-...`) | only for the `/api/cron` tasks (summarization + community agent) |
| `GITHUB_TOKEN` | Fine-grained PAT, public-repo read scope | optional (raises rate limit 60 → 5000 req/h) |
| `GITHUB_REPO` | Defaults to `codewhale-hq/CodeWhale` | optional |
| `CRON_SECRET` | Shared secret for manual `/api/cron` invocation | optional (Cloudflare cron triggers don't need it) |
| `CRON_SECRET` | Shared secret for manual `/api/cron` invocation (32-512 characters, 8+ distinct; a shorter value repeated is refused)| optional (Cloudflare cron triggers don't need it) |
| `DEEPSEEK_MODEL` | Defaults to `deepseek-flash` | optional |
| `DEEPSEEK_BASE_URL` | Defaults to `https://api.deepseek.com` | optional |
| `MAINTAINER_TOKEN` | Admin panel auth; enter it in the `/admin` login form | only for `/admin` |
| `MAINTAINER_TOKEN` | Admin panel auth (32-512 characters, 8+ distinct; a shorter value repeated is refused; changing it signs out sessions); enter it in the `/admin` login form | only for `/admin` |
| `MAINTAINER_GITHUB_PAT` | PAT with `issues:write`, for posting comments via `/admin` | only for `/admin` posting |
| `NEXT_PUBLIC_GITEE_ENABLED` | Set to `1` once the Gitee mirror exists; blank hides Gitee links | optional |
+4 -2
View File
@@ -4,6 +4,7 @@ import { ConnectionBanner } from "@/components/connection-banner";
import { EmptyState, ErrorState } from "@/components/surface-state";
import { WhalePose } from "@/components/whale-pose";
import { getAgentEnv, listDrafts, validateSession, type AgentDraft } from "@/lib/community-agent";
import { MAINTAINER_TOKEN_UNAVAILABLE, maintainerTokenUsable } from "@/lib/maintainer-token";
import { AdminClient } from "./admin-client";
export const dynamic = "force-dynamic";
@@ -77,7 +78,8 @@ export default async function AdminPage({
const env = await getAgentEnv();
if (!env.MAINTAINER_TOKEN) {
if (!maintainerTokenUsable(env.MAINTAINER_TOKEN)) {
if (env.MAINTAINER_TOKEN) console.warn(MAINTAINER_TOKEN_UNAVAILABLE);
return (
<div className="route-state">
<ConnectionBanner locale={locale} />
@@ -98,7 +100,7 @@ export default async function AdminPage({
const cookieStore = await cookies();
const sid = cookieStore.get("mt_sid")?.value;
const authed = await validateSession(env.CURATED_KV, sid);
const authed = await validateSession(env.CURATED_KV, sid, env.MAINTAINER_TOKEN);
if (!authed) {
return <LoginForm locale={locale} error={err === "1"} />;
+5 -5
View File
@@ -1,12 +1,12 @@
import { NextResponse } from "next/server";
import { getAgentEnv, safeEqual, createSession } from "@/lib/community-agent";
import { MAINTAINER_TOKEN_UNAVAILABLE, MAX_SECRET_CHARS, maintainerTokenUsable } from "@/lib/maintainer-token";
import { FormBodyError, readBoundedUrlEncodedForm } from "@/lib/bounded-form";
export const dynamic = "force-dynamic";
const ALLOWED_LOCALES = new Set(["en", "zh"]);
const MAX_LOGIN_BODY_BYTES = 4_096;
const MAX_TOKEN_CHARS = 512;
function pickLocale(value: string | null | undefined): string {
if (!value) return "en";
@@ -18,8 +18,8 @@ export async function POST(req: Request) {
const url = new URL(req.url);
const localeFromQuery = pickLocale(url.searchParams.get("locale"));
if (!env.MAINTAINER_TOKEN) {
return new NextResponse("Not configured", {
if (!maintainerTokenUsable(env.MAINTAINER_TOKEN)) {
return new NextResponse(MAINTAINER_TOKEN_UNAVAILABLE, {
status: 503,
headers: { "Cache-Control": "no-store" },
});
@@ -58,7 +58,7 @@ export async function POST(req: Request) {
}
const submitted = form.get("token") ?? "";
const locale = pickLocale(form.get("locale") ?? localeFromQuery);
if (submitted.length > MAX_TOKEN_CHARS) {
if (submitted.length > MAX_SECRET_CHARS) {
return new NextResponse("Token too long", {
status: 413,
headers: { "Cache-Control": "no-store" },
@@ -73,7 +73,7 @@ export async function POST(req: Request) {
});
}
const sid = await createSession(env.CURATED_KV);
const sid = await createSession(env.CURATED_KV, env.MAINTAINER_TOKEN);
if (!sid) {
return new NextResponse("Session storage unavailable", {
status: 503,
+1 -1
View File
@@ -9,7 +9,7 @@ export async function POST(request: Request) {
try {
const authEnv = await getAgentEnv();
const sid = request.headers.get("cookie")?.split(";").map(c => c.trim()).find(c => c.startsWith("mt_sid="))?.slice(7);
if (!authEnv.MAINTAINER_TOKEN || !await validateSession(authEnv.CURATED_KV, sid)) throw new MerchError(401, "unauthorized", "Maintainer sign-in is required.");
if (!await validateSession(authEnv.CURATED_KV, sid, authEnv.MAINTAINER_TOKEN)) throw new MerchError(401, "unauthorized", "Maintainer sign-in is required.");
const env = await merchEnv(), config = parseConfig(env.MERCH_CONFIG_JSON);
if (!config || request.headers.get("origin") !== new URL(config.siteOrigin).origin || !request.headers.get("content-type")?.startsWith("application/json")) throw new MerchError(403, "request_blocked", "Use the approved store origin.");
const raw = await readBoundedBody(request, 8192);
+1 -1
View File
@@ -39,7 +39,7 @@ async function checkAuth(req: Request, env: CommunityAgentEnv): Promise<{ ok: bo
}
}
if (!sid || !(await validateSession(env.CURATED_KV, sid))) {
if (!sid || !(await validateSession(env.CURATED_KV, sid, env.MAINTAINER_TOKEN))) {
return { ok: false, status: 401, error: "unauthorized" };
}
return { ok: true };
+26 -1
View File
@@ -4,6 +4,7 @@ import type { AgentEnv } from "@/lib/community-agent-tasks";
import { runFactsDrift } from "@/lib/facts-drift";
import { runLinkCheck, runSemanticDrift } from "@/lib/content-watch";
import { safeEqual } from "@/lib/community-agent";
import { CRON_SECRET_UNAVAILABLE, secretStrongEnough } from "@/lib/maintainer-token";
export const dynamic = "force-dynamic";
@@ -30,9 +31,33 @@ export async function GET(req: Request) {
);
}
if (!secretStrongEnough(env.CRON_SECRET)) {
console.warn(CRON_SECRET_UNAVAILABLE);
return NextResponse.json(
{ error: CRON_SECRET_UNAVAILABLE },
{ status: 503, headers: { "Cache-Control": "no-store" } }
);
}
try {
if (!env.ADMIN_LOGIN_LIMITER) throw new Error("Missing cron limiter");
const { success } = await env.ADMIN_LOGIN_LIMITER.limit({ key: "codewhale-web:cron-secret" });
if (!success) {
return NextResponse.json(
{ error: "too many attempts" },
{ status: 429, headers: { "Cache-Control": "no-store", "Retry-After": "60" } }
);
}
} catch {
return NextResponse.json(
{ error: "manual trigger temporarily unavailable" },
{ status: 503, headers: { "Cache-Control": "no-store", "Retry-After": "60" } }
);
}
const auth = req.headers.get("x-cron-secret") ?? "";
if (!(await safeEqual(auth, env.CRON_SECRET))) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
return NextResponse.json({ error: "unauthorized" }, { status: 401, headers: { "Cache-Control": "no-store" } });
}
const { searchParams } = new URL(req.url);
+10 -10
View File
@@ -10,8 +10,8 @@
},
{
"path": "docs/INSTALL.md",
"sha256": "40eac00d7c85c2114d2e0adf470f651b61cee33aaf6a667f3af2f8f4d8715070",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's pinned Rust\n1.100.0 (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recLine truncated
"sha256": "35bc2795284622177f1be7f85544174a4cbd9d2881e52fbdedb6f1262b636815",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: rLine truncated
},
{
"path": "docs/MODES.md",
@@ -36,8 +36,8 @@
"sources": [
{
"path": "docs/INSTALL.md",
"sha256": "40eac00d7c85c2114d2e0adf470f651b61cee33aaf6a667f3af2f8f4d8715070",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's pinned Rust\n1.100.0 (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recLine truncated
"sha256": "35bc2795284622177f1be7f85544174a4cbd9d2881e52fbdedb6f1262b636815",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: rLine truncated
}
]
},
@@ -52,8 +52,8 @@
},
{
"path": "docs/INSTALL.md",
"sha256": "40eac00d7c85c2114d2e0adf470f651b61cee33aaf6a667f3af2f8f4d8715070",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's pinned Rust\n1.100.0 (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recLine truncated
"sha256": "35bc2795284622177f1be7f85544174a4cbd9d2881e52fbdedb6f1262b636815",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: rLine truncated
},
{
"path": "docs/KEYBINDINGS.md",
@@ -68,8 +68,8 @@
"sources": [
{
"path": "docs/INSTALL.md",
"sha256": "40eac00d7c85c2114d2e0adf470f651b61cee33aaf6a667f3af2f8f4d8715070",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's pinned Rust\n1.100.0 (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recLine truncated
"sha256": "35bc2795284622177f1be7f85544174a4cbd9d2881e52fbdedb6f1262b636815",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: rLine truncated
},
{
"path": "docs/PROVIDERS.md",
@@ -387,8 +387,8 @@
"sources": [
{
"path": "docs/INSTALL.md",
"sha256": "40eac00d7c85c2114d2e0adf470f651b61cee33aaf6a667f3af2f8f4d8715070",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's pinned Rust\n1.100.0 (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recLine truncated
"sha256": "35bc2795284622177f1be7f85544174a4cbd9d2881e52fbdedb6f1262b636815",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: rLine truncated
},
{
"path": "docs/OPERATIONS_RUNBOOK.md",
+10 -4
View File
@@ -14,7 +14,7 @@ vi.mock("@/lib/community-agent", async (importOriginal) => {
import { POST } from "@/app/api/admin/login/route";
import { createSession, safeEqual, validateSession } from "@/lib/community-agent";
const FIXTURE_TOKEN = "local-admin-integration-fixture";
const FIXTURE_TOKEN = "local-admin-integration-fixture-0123456789";
const sessions = new Map<string, string>();
const kv = {
put: vi.fn(async (key: string, value: string) => { sessions.set(key, value); }),
@@ -57,9 +57,9 @@ describe("admin login with real credential and session helpers", () => {
const sid = response.cookies.get("mt_sid")?.value;
expect(sid).toMatch(/^[A-Za-z0-9_-]{43}$/);
expect(sessions.size).toBe(1);
expect(await validateSession(sessionKv, sid)).toBe(true);
expect(await validateSession(sessionKv, sid, FIXTURE_TOKEN)).toBe(true);
expect(safeEqual).toHaveBeenCalledExactlyOnceWith(FIXTURE_TOKEN, FIXTURE_TOKEN);
expect(createSession).toHaveBeenCalledExactlyOnceWith(sessionKv);
expect(createSession).toHaveBeenCalledExactlyOnceWith(sessionKv, FIXTURE_TOKEN);
const cookie = response.headers.get("Set-Cookie")?.toLowerCase();
expect(cookie).toContain("httponly");
expect(cookie).toContain("secure");
@@ -69,7 +69,13 @@ describe("admin login with real credential and session helpers", () => {
expect.any(String), expect.any(String), { expirationTtl: 86400 },
]);
sessions.clear();
expect(await validateSession(sessionKv, sid)).toBe(false);
expect(await validateSession(sessionKv, sid, FIXTURE_TOKEN)).toBe(false);
});
it("stops honouring a session after the maintainer token is rotated", async () => {
const sid = (await POST(request())).cookies.get("mt_sid")?.value;
expect(await validateSession(sessionKv, sid, FIXTURE_TOKEN)).toBe(true);
expect(await validateSession(sessionKv, sid, "rotated-integration-fixture-9876543210")).toBe(false);
});
it("rejects a wrong credential without creating a session", async () => {
+38 -4
View File
@@ -5,6 +5,9 @@ const mocks = vi.hoisted(() => ({
vi.mock("@/lib/community-agent", () => mocks);
import { POST } from "@/app/api/admin/login/route";
const STRONG_TOKEN = "local-fixture-token-0123456789-abcdefghij";
const LONE_SURROGATES = Array.from({ length: 16 }, (_, index) => String.fromCharCode(0xd800 + index)).join("").repeat(2);
function request(token = "wrong", ip = "127.0.0.1") {
return new Request("https://codewhale.net/api/admin/login?locale=en", {
method: "POST",
@@ -14,7 +17,7 @@ function request(token = "wrong", ip = "127.0.0.1") {
}
beforeEach(() => {
vi.resetAllMocks();
mocks.getAgentEnv.mockResolvedValue({ MAINTAINER_TOKEN: "configured", ADMIN_LOGIN_LIMITER: { limit: mocks.limit }, CURATED_KV: {} });
mocks.getAgentEnv.mockResolvedValue({ MAINTAINER_TOKEN: STRONG_TOKEN, ADMIN_LOGIN_LIMITER: { limit: mocks.limit }, CURATED_KV: {} });
mocks.limit.mockResolvedValue({ success: true });
mocks.safeEqual.mockResolvedValue(false);
mocks.createSession.mockResolvedValue("session-id");
@@ -35,7 +38,7 @@ describe("admin login attempt boundary", () => {
});
it("does not parse or compare credentials after refusal", async () => {
mocks.limit.mockResolvedValue({ success: false });
const req = request("configured");
const req = request(STRONG_TOKEN);
const read = vi.spyOn(req, "text");
expect((await POST(req)).status).toBe(429);
expect(read).not.toHaveBeenCalled();
@@ -43,16 +46,47 @@ describe("admin login attempt boundary", () => {
expect(mocks.createSession).not.toHaveBeenCalled();
});
it("fails closed if the binding is missing or unavailable", async () => {
mocks.getAgentEnv.mockResolvedValueOnce({ MAINTAINER_TOKEN: "configured" });
mocks.getAgentEnv.mockResolvedValueOnce({ MAINTAINER_TOKEN: STRONG_TOKEN });
expect((await POST(request())).status).toBe(503);
mocks.limit.mockRejectedValue(new Error("binding unavailable"));
expect((await POST(request())).status).toBe(503);
expect(mocks.safeEqual).not.toHaveBeenCalled();
expect(mocks.createSession).not.toHaveBeenCalled();
});
it("refuses to run with a configured token below the minimum strength", async () => {
const weakTokens = [
"x", "configured", "a".repeat(31), "a".repeat(32), " ".repeat(40), "password".repeat(4),
"Password1".repeat(4), "0123456".repeat(5), "\u{1F433}".repeat(16), "L".repeat(600), LONE_SURROGATES,
];
for (const weak of weakTokens) {
mocks.getAgentEnv.mockResolvedValueOnce({ MAINTAINER_TOKEN: weak, ADMIN_LOGIN_LIMITER: { limit: mocks.limit }, CURATED_KV: {} });
mocks.safeEqual.mockResolvedValue(true);
const response = await POST(request(weak));
expect(response.status).toBe(503);
expect(response.headers.get("Cache-Control")).toBe("no-store");
expect(await response.text()).toContain("at least 32 characters");
}
expect(mocks.limit).not.toHaveBeenCalled();
expect(mocks.safeEqual).not.toHaveBeenCalled();
expect(mocks.createSession).not.toHaveBeenCalled();
});
it("accepts a token of exactly the minimum length", async () => {
const exact = "abcdefghijklmnopqrstuvwxyz012345";
expect(exact).toHaveLength(32);
mocks.getAgentEnv.mockResolvedValueOnce({ MAINTAINER_TOKEN: exact, ADMIN_LOGIN_LIMITER: { limit: mocks.limit }, CURATED_KV: {} });
mocks.safeEqual.mockResolvedValue(true);
expect((await POST(request(exact))).status).toBe(303);
});
it("accepts a 32-character hex secret with fewer than 16 distinct characters", async () => {
const hex = "3f9a3f9a1c7e1c7e3f9a1c7e5b2d5b2d";
expect(new Set(hex).size).toBeLessThan(16);
mocks.getAgentEnv.mockResolvedValueOnce({ MAINTAINER_TOKEN: hex, ADMIN_LOGIN_LIMITER: { limit: mocks.limit }, CURATED_KV: {} });
mocks.safeEqual.mockResolvedValue(true);
expect((await POST(request(hex))).status).toBe(303);
});
it("retains the authenticated session and redirect after an allowed attempt", async () => {
mocks.safeEqual.mockResolvedValue(true);
const response = await POST(request("configured"));
const response = await POST(request(STRONG_TOKEN));
expect(response.status).toBe(303);
expect(response.headers.get("Location")).toBe("https://codewhale.net/en/admin");
const cookie = response.headers.get("Set-Cookie");
+1 -1
View File
@@ -144,7 +144,7 @@ function stubAdminEnv(kv: FakeKv, lock?: FakeDraftClaimLock) {
mocks.getAgentEnv.mockResolvedValue({
CURATED_KV: kv,
...(lock ? { DRAFT_CLAIM_LOCK: lock } : {}),
MAINTAINER_TOKEN: "configured",
MAINTAINER_TOKEN: "local-fixture-token-0123456789-abcdefghij",
MAINTAINER_GITHUB_PAT: "ghp_test",
GITHUB_REPO: "codewhale-hq/CodeWhale",
});
+46 -23
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { logUsage, validateSession } from "./community-agent";
import { createSession, logUsage, validateSession } from "./community-agent";
describe("validateSession", () => {
let mockKV: {
@@ -20,65 +20,88 @@ describe("validateSession", () => {
const VALID_SID = "a".repeat(40);
const SESSION_PREFIX = "session:admin:";
const TOKEN = "local-fixture-token-0123456789-abcdefghij";
const ROTATED = "rotated-fixture-token-9876543210-zyxwvutsrq";
it("should return false if kv is undefined", async () => {
const result = await validateSession(undefined, VALID_SID);
const result = await validateSession(undefined, VALID_SID, TOKEN);
expect(result).toBe(false);
});
it("should return false if sid is undefined or null", async () => {
const kv = mockKV as unknown as Parameters<typeof validateSession>[0];
expect(await validateSession(kv, undefined)).toBe(false);
expect(await validateSession(kv, null)).toBe(false);
expect(await validateSession(kv, undefined, TOKEN)).toBe(false);
expect(await validateSession(kv, null, TOKEN)).toBe(false);
});
it("should return false if sid is empty", async () => {
const kv = mockKV as unknown as Parameters<typeof validateSession>[0];
expect(await validateSession(kv, "")).toBe(false);
expect(await validateSession(kv, "", TOKEN)).toBe(false);
});
it("should return false if sid length is less than 40", async () => {
const kv = mockKV as unknown as Parameters<typeof validateSession>[0];
expect(await validateSession(kv, "a".repeat(39))).toBe(false);
expect(await validateSession(kv, "a".repeat(39), TOKEN)).toBe(false);
});
it("should return false if sid length is greater than 64", async () => {
const kv = mockKV as unknown as Parameters<typeof validateSession>[0];
expect(await validateSession(kv, "a".repeat(65))).toBe(false);
expect(await validateSession(kv, "a".repeat(65), TOKEN)).toBe(false);
});
it("should return false if sid contains invalid characters", async () => {
const kv = mockKV as unknown as Parameters<typeof validateSession>[0];
// Length 40 but contains invalid char '!'
expect(await validateSession(kv, "a".repeat(39) + "!")).toBe(false);
expect(await validateSession(kv, "a".repeat(39) + "!", TOKEN)).toBe(false);
});
it("should return false if session is not found in KV", async () => {
const kv = mockKV as unknown as Parameters<typeof validateSession>[0];
mockKV.get.mockResolvedValue(null);
const result = await validateSession(kv, VALID_SID);
const result = await validateSession(kv, VALID_SID, TOKEN);
expect(result).toBe(false);
expect(mockKV.get).toHaveBeenCalledWith(SESSION_PREFIX + VALID_SID);
});
it("should return true if session is found in KV", async () => {
const kv = mockKV as unknown as Parameters<typeof validateSession>[0];
mockKV.get.mockResolvedValue(JSON.stringify({ createdAt: Date.now() }));
const result = await validateSession(kv, VALID_SID);
expect(result).toBe(true);
expect(mockKV.get).toHaveBeenCalledWith(SESSION_PREFIX + VALID_SID);
it("should return true for a session minted under the configured token", async () => {
const store = new Map<string, string>();
const kv = {
get: async (key: string) => store.get(key) ?? null,
put: async (key: string, value: string) => { store.set(key, value); },
} as unknown as Parameters<typeof validateSession>[0];
const sid = await createSession(kv, TOKEN);
expect(sid).toMatch(/^[A-Za-z0-9_-]{43}$/);
expect(await validateSession(kv, sid, TOKEN)).toBe(true);
});
it("should work with a 64 character sid", async () => {
const kv = mockKV as unknown as Parameters<typeof validateSession>[0];
mockKV.get.mockResolvedValue("{}");
it("should refuse the same session once the configured token changes", async () => {
const store = new Map<string, string>();
const kv = {
get: async (key: string) => store.get(key) ?? null,
put: async (key: string, value: string) => { store.set(key, value); },
} as unknown as Parameters<typeof validateSession>[0];
const sid = await createSession(kv, TOKEN);
expect(await validateSession(kv, sid, ROTATED)).toBe(false);
expect(await validateSession(kv, sid, "short")).toBe(false);
expect(await validateSession(kv, sid, undefined)).toBe(false);
expect(await validateSession(kv, sid, TOKEN)).toBe(true);
});
const sid = "b".repeat(64);
const result = await validateSession(kv, sid);
expect(result).toBe(true);
expect(mockKV.get).toHaveBeenCalledWith(SESSION_PREFIX + sid);
it("should refuse a stored record with no token fingerprint or unparseable content", async () => {
const kv = mockKV as unknown as Parameters<typeof validateSession>[0];
for (const raw of [JSON.stringify({ createdAt: Date.now() }), "{}", "not json", JSON.stringify({ tokenFingerprint: 7 })]) {
mockKV.get.mockResolvedValue(raw);
expect(await validateSession(kv, VALID_SID, TOKEN)).toBe(false);
}
});
it("should not mint a session under a weak token", async () => {
const kv = mockKV as unknown as Parameters<typeof createSession>[0];
for (const weak of [undefined, "", "short", "a".repeat(32)]) {
expect(await createSession(kv, weak)).toBeNull();
}
expect(mockKV.put).not.toHaveBeenCalled();
});
});
+26 -6
View File
@@ -12,6 +12,7 @@
import type { DraftClaimLockNamespace, DraftClaimLockStub, DraftLockAction } from "./draft-claim-lock";
import { maintainerTokenUsable } from "./maintainer-token";
import { requestChat, type ChatMessage, type DeepSeekEnv } from "./deepseek";
export type { DeepSeekEnv } from "./deepseek";
@@ -722,6 +723,7 @@ export async function deleteDraft(kv: KVNamespace | undefined, key: string): Pro
const SESSION_PREFIX = "session:admin:";
const SESSION_TTL_SEC = 60 * 60 * 24; // 24h
const SESSION_TOKEN_DOMAIN = "codewhale-web:admin-session:v1:";
function toBase64Url(bytes: Uint8Array): string {
let s = "";
@@ -738,21 +740,39 @@ export async function safeEqual(a: string, b: string): Promise<boolean> {
return diff === 0;
}
export async function createSession(kv: KVNamespace | undefined): Promise<string | null> {
if (!kv) return null;
async function tokenFingerprint(token: string): Promise<string> {
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(SESSION_TOKEN_DOMAIN + token)));
let hex = "";
for (const byte of digest) hex += byte.toString(16).padStart(2, "0");
return hex;
}
export async function createSession(kv: KVNamespace | undefined, token: string | undefined): Promise<string | null> {
if (!kv || !maintainerTokenUsable(token)) return null;
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
const sid = toBase64Url(bytes);
const value = JSON.stringify({ createdAt: Date.now() });
const value = JSON.stringify({ createdAt: Date.now(), tokenFingerprint: await tokenFingerprint(token) });
await kv.put(SESSION_PREFIX + sid, value, { expirationTtl: SESSION_TTL_SEC });
return sid;
}
export async function validateSession(kv: KVNamespace | undefined, sid: string | undefined | null): Promise<boolean> {
if (!kv || !sid) return false;
export async function validateSession(
kv: KVNamespace | undefined,
sid: string | undefined | null,
token: string | undefined,
): Promise<boolean> {
if (!kv || !sid || !maintainerTokenUsable(token)) return false;
if (!/^[A-Za-z0-9_-]{40,64}$/.test(sid)) return false;
const raw = await kv.get(SESSION_PREFIX + sid);
return raw !== null;
if (raw === null) return false;
let stored: unknown;
try {
stored = (JSON.parse(raw) as { tokenFingerprint?: unknown }).tokenFingerprint;
} catch {
return false;
}
return typeof stored === "string" && (await safeEqual(stored, await tokenFingerprint(token)));
}
export async function deleteSession(kv: KVNamespace | undefined, sid: string | undefined | null): Promise<void> {
+76
View File
@@ -0,0 +1,76 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({ getEnv: vi.fn(), safeEqual: vi.fn(), limit: vi.fn() }));
vi.mock("@/lib/kv", () => ({ getEnv: mocks.getEnv }));
vi.mock("@/lib/community-agent", () => ({ safeEqual: mocks.safeEqual }));
vi.mock("@/lib/facts-drift", () => ({ runFactsDrift: vi.fn() }));
vi.mock("@/lib/content-watch", () => ({ runLinkCheck: vi.fn(), runSemanticDrift: vi.fn() }));
import { GET } from "@/app/api/cron/route";
function request(secret = "wrong", ip = "127.0.0.1") {
return new Request("https://codewhale.net/api/cron?task=nope", {
headers: { "x-cron-secret": secret, "CF-Connecting-IP": ip },
});
}
beforeEach(() => {
vi.resetAllMocks();
mocks.getEnv.mockResolvedValue({ CRON_SECRET: "configured-cron-secret-0123456789-abcdef", ADMIN_LOGIN_LIMITER: { limit: mocks.limit } });
mocks.limit.mockResolvedValue({ success: true });
mocks.safeEqual.mockResolvedValue(false);
});
describe("manual cron trigger attempt boundary", () => {
it("bounds repeated guesses independently of the secret and forged IP", async () => {
let attempts = 0;
mocks.limit.mockImplementation(async () => ({ success: ++attempts <= 5 }));
for (let index = 0; index < 7; index++) {
const response = await GET(request(`guess-${index}`, `192.0.2.${index}`));
expect(response.status).toBe(index < 5 ? 401 : 429);
if (index >= 5) expect(response.headers.get("Retry-After")).toBe("60");
expect(response.headers.get("Cache-Control")).toBe("no-store");
}
expect(mocks.safeEqual).toHaveBeenCalledTimes(5);
expect(mocks.limit.mock.calls.map(([options]) => options.key)).toEqual(Array(7).fill("codewhale-web:cron-secret"));
});
it("does not compare the secret after refusal", async () => {
mocks.limit.mockResolvedValue({ success: false });
expect((await GET(request("configured-cron-secret-0123456789-abcdef"))).status).toBe(429);
expect(mocks.safeEqual).not.toHaveBeenCalled();
});
it("fails closed if the binding is missing or unavailable", async () => {
mocks.getEnv.mockResolvedValueOnce({ CRON_SECRET: "configured-cron-secret-0123456789-abcdef" });
expect((await GET(request())).status).toBe(503);
mocks.limit.mockRejectedValue(new Error("binding unavailable"));
expect((await GET(request())).status).toBe(503);
expect(mocks.safeEqual).not.toHaveBeenCalled();
});
it("stays disabled without a configured secret and never consults the limiter", async () => {
mocks.getEnv.mockResolvedValue({ ADMIN_LOGIN_LIMITER: { limit: mocks.limit } });
expect((await GET(request())).status).toBe(503);
expect(mocks.limit).not.toHaveBeenCalled();
});
it("stays disabled for a secret below the minimum strength and never consults the limiter or compares", async () => {
const loneSurrogates = Array.from({ length: 16 }, (_, index) => String.fromCharCode(0xd800 + index)).join("").repeat(2);
const weak = ["x", "a".repeat(32), " ".repeat(40), "password".repeat(4), "Password1".repeat(4), "\u{1F433}".repeat(16), "0123456".repeat(5), "L".repeat(600), loneSurrogates];
for (const secret of weak) {
mocks.getEnv.mockResolvedValueOnce({ CRON_SECRET: secret, ADMIN_LOGIN_LIMITER: { limit: mocks.limit } });
const response = await GET(request());
expect(response.status).toBe(503);
expect(response.headers.get("Cache-Control")).toBe("no-store");
expect(JSON.stringify(await response.json())).toContain("at least 32 characters");
}
expect(mocks.limit).not.toHaveBeenCalled();
expect(mocks.safeEqual).not.toHaveBeenCalled();
});
it("accepts a 32-character hex secret with fewer than 16 distinct characters", async () => {
const hex = "3f9a3f9a1c7e1c7e3f9a1c7e5b2d5b2d";
mocks.getEnv.mockResolvedValueOnce({ CRON_SECRET: hex, ADMIN_LOGIN_LIMITER: { limit: mocks.limit } });
mocks.safeEqual.mockResolvedValue(true);
expect((await GET(request(hex))).status).toBe(400);
});
it("still reaches task validation after an allowed, correct attempt", async () => {
mocks.safeEqual.mockResolvedValue(true);
expect((await GET(request("configured-cron-secret-0123456789-abcdef"))).status).toBe(400);
});
});
+1
View File
@@ -32,6 +32,7 @@ export async function readBoundedKvJson(kv: KVStreamNamespace | undefined, key:
interface CloudflareEnv extends MerchEnv {
DRAFT_CLAIM_LOCK?: DraftClaimLockNamespace;
CURATED_KV?: KVNamespace & KVStreamNamespace;
ADMIN_LOGIN_LIMITER?: { limit(options: { key: string }): Promise<{ success: boolean }> };
MERCH_INTEREST_LIMITER?: { limit(options: { key: string }): Promise<{ success: boolean }> };
MERCH_INTEREST_SITE_ORIGIN?: string;
DEEPSEEK_API_KEY?: string;
+25
View File
@@ -0,0 +1,25 @@
export const MIN_SECRET_CHARS = 32;
export const MAX_SECRET_CHARS = 512;
export const MIN_SECRET_DISTINCT_CHARS = 8;
const LONE_SURROGATE = /\p{Cs}/u;
export const MAINTAINER_TOKEN_UNAVAILABLE =
`Maintainer sign-in is unavailable: MAINTAINER_TOKEN must be set to a secret of at least ${MIN_SECRET_CHARS} characters ` +
`(at most ${MAX_SECRET_CHARS}, with at least ${MIN_SECRET_DISTINCT_CHARS} distinct characters; a shorter value repeated is refused).`;
export const CRON_SECRET_UNAVAILABLE =
`manual trigger disabled: CRON_SECRET must be a secret of at least ${MIN_SECRET_CHARS} characters ` +
`(at most ${MAX_SECRET_CHARS}, with at least ${MIN_SECRET_DISTINCT_CHARS} distinct characters; a shorter value repeated is refused)`;
export function secretStrongEnough(value: string | undefined): value is string {
if (typeof value !== "string" || value.length > MAX_SECRET_CHARS || LONE_SURROGATE.test(value)) return false;
const chars = [...value];
if (chars.length < MIN_SECRET_CHARS || new Set(chars).size < MIN_SECRET_DISTINCT_CHARS) return false;
for (let period = 1; period <= value.length / 2; period++) {
if (value.slice(period) === value.slice(0, value.length - period)) return false;
}
return true;
}
export const maintainerTokenUsable = secretStrongEnough;
+8 -2
View File
@@ -155,8 +155,8 @@ describe("durable merch interest registration", () => {
describe("private merch interest list and deletion", () => {
async function signedIn(): Promise<{ auth: CommunityAgentEnv; headers: Record<string, string> }> {
const auth = { CURATED_KV: env.CURATED_KV, MAINTAINER_TOKEN: "local-maintainer-fixture" };
const sid = await createSession(auth.CURATED_KV);
const auth = { CURATED_KV: env.CURATED_KV, MAINTAINER_TOKEN: "local-fixture-token-0123456789-abcdefghij" };
const sid = await createSession(auth.CURATED_KV, auth.MAINTAINER_TOKEN);
return { auth, headers: { Cookie: "mt_sid=" + sid } };
}
it("requires a real stored maintainer session for reading or deleting contacts", async () => {
@@ -165,6 +165,12 @@ describe("private merch interest list and deletion", () => {
expect(list).not.toHaveBeenCalled();
expect(remove).not.toHaveBeenCalled();
});
it.each(["short", "rotated-fixture-token-9876543210-zyxwvutsrq"])("refuses an existing session once the configured maintainer token changes to %s", async (replacement) => {
const { auth, headers } = await signedIn();
const response = await handleAdminMerchInterest(new Request("https://codewhale.net/api/admin/merch/interest", { headers }), env, { ...auth, MAINTAINER_TOKEN: replacement });
expect(response.status).toBe(401);
expect(list).not.toHaveBeenCalled();
});
it("paginates only the private prefix and deletes an opaque id with same-origin CSRF", async () => {
await handleMerchInterest(request(), env);
await handleMerchInterest(request({ ...input(), email: "[email protected]" }), env);
+1 -1
View File
@@ -138,7 +138,7 @@ export async function handleMerchInterest(request: Request, env?: MerchInterestE
}
async function requireAdmin(request: Request, auth: CommunityAgentEnv) {
const sid = request.headers.get("cookie")?.split(";").map(cookie => cookie.trim()).find(cookie => cookie.startsWith("mt_sid="))?.slice(7);
if (!auth.MAINTAINER_TOKEN || !await validateSession(auth.CURATED_KV, sid)) throw new InterestError(401, "unauthorized", "Maintainer sign-in is required.");
if (!await validateSession(auth.CURATED_KV, sid, auth.MAINTAINER_TOKEN)) throw new InterestError(401, "unauthorized", "Maintainer sign-in is required.");
}
export async function handleAdminMerchInterest(request: Request, env?: MerchInterestEnv, authEnv?: CommunityAgentEnv): Promise<Response> {
try {
+2 -2
View File
@@ -73,7 +73,7 @@ describe("public security boundaries", () => {
kv.values.set("agent:state:latest", JSON.stringify({ state: "running" }));
securityMocks.getAgentEnv.mockResolvedValue({
CURATED_KV: kv,
MAINTAINER_TOKEN: "configured",
MAINTAINER_TOKEN: "local-fixture-token-0123456789-abcdefghij",
});
const response = await adminPost(new Request("https://codewhale.net/api/admin/post", {
@@ -121,7 +121,7 @@ describe("public security boundaries", () => {
securityMocks.getAgentEnv.mockResolvedValue({
CURATED_KV: kv,
MAINTAINER_TOKEN: "configured",
MAINTAINER_TOKEN: "local-fixture-token-0123456789-abcdefghij",
MAINTAINER_GITHUB_PAT: "ghp_test",
GITHUB_REPO: "codewhale-hq/CodeWhale",
});