mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-13 04:29:24 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf539b03a1 | ||
|
|
2d1ba71ed7 |
No files matched your search
@@ -48,7 +48,7 @@
|
||||
tags: cnb:arch:amd64
|
||||
cpus: 16
|
||||
docker:
|
||||
image: rust:1.88-bookworm
|
||||
image: rust:1.100.0-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.88-bookworm
|
||||
image: rust:1.100.0-bookworm
|
||||
stages:
|
||||
- name: install release dependencies
|
||||
script: |
|
||||
@@ -163,7 +163,7 @@ main:
|
||||
$:
|
||||
tag_push:
|
||||
- docker:
|
||||
image: rust:1.88-bookworm
|
||||
image: rust:1.100.0-bookworm
|
||||
stages:
|
||||
- name: build linux x64 release assets (static)
|
||||
# The static full-LTO link can also outlive CNB's default no-output
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ARG RUST_VERSION=1.88
|
||||
ARG RUST_VERSION=1.100.0
|
||||
FROM rust:${RUST_VERSION}-slim-bookworm
|
||||
|
||||
# Native DBus headers and pkg-config are required by libdbus-sys during
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"context": "..",
|
||||
"dockerfile": "Dockerfile",
|
||||
"args": {
|
||||
"RUST_VERSION": "1.88"
|
||||
"RUST_VERSION": "1.100.0"
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
|
||||
@@ -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 --toolchain stable aarch64-unknown-linux-musl/);
|
||||
assert.match(nightlyArmMuslSetup, /rustup target add 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 --toolchain stable \$\{\{ matrix\.target \}\}/);
|
||||
assert.match(releaseMuslBuild, /rustup target add \$\{\{ matrix\.target \}\}/);
|
||||
assert.match(
|
||||
releaseMuslBuild,
|
||||
/cargo build --profile dist --locked --target \$\{\{ matrix\.target \}\} -p codewhale-cli/,
|
||||
@@ -934,6 +934,76 @@ 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.",
|
||||
);
|
||||
@@ -23,17 +23,17 @@ jobs:
|
||||
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# rust-toolchain.toml pins `channel = "stable"` with no target, so
|
||||
# rust-toolchain.toml pins `channel = "1.100.0"` with no target, so
|
||||
# inside the action's alpine (musl) container rustup resolves it to
|
||||
# `stable-x86_64-unknown-linux-musl` — which the container does not
|
||||
# `1.100.0-x86_64-unknown-linux-musl`, which the container does not
|
||||
# have. Without provisioning, the action's `rustup show` step prints
|
||||
# "error: override toolchain 'stable-x86_64-unknown-linux-musl' is not
|
||||
# "error: override toolchain '1.100.0-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 stable` (same musl host triple) before
|
||||
# entrypoint run `rustup default 1.100.0` (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: stable
|
||||
rust-version: 1.100.0
|
||||
+13
-11
@@ -203,7 +203,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- uses: actions/[email protected]
|
||||
with:
|
||||
node-version: 22
|
||||
@@ -428,7 +428,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
- 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: stable
|
||||
toolchain: 1.100.0
|
||||
components: rustfmt, clippy
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
@@ -728,6 +728,8 @@ 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
|
||||
@@ -751,7 +753,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
@@ -825,11 +827,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@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
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 --toolchain stable
|
||||
run: rustup component add llvm-tools
|
||||
- name: Omit macOS build debug metadata
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
|
||||
shell: bash
|
||||
@@ -1222,7 +1224,7 @@ jobs:
|
||||
if: needs.changes.outputs.heavy == 'true'
|
||||
shell: pwsh
|
||||
run: ./scripts/installer/update-user-path.tests.ps1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
if: needs.changes.outputs.heavy == 'true'
|
||||
- name: Check every workspace target compiles on Windows
|
||||
if: needs.changes.outputs.heavy == 'true'
|
||||
@@ -1267,7 +1269,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-bin: false
|
||||
@@ -1305,7 +1307,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@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
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
|
||||
@@ -1364,7 +1366,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- uses: mozilla-actions/[email protected]
|
||||
id: sccache
|
||||
# Main-only GitHub Actions cache backend; see the Safety gate job.
|
||||
@@ -1423,7 +1425,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- name: Install Linux system dependencies
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
components: rustfmt
|
||||
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
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 --toolchain stable aarch64-unknown-linux-musl
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
- name: Build
|
||||
shell: bash
|
||||
# Nightly artifacts are disposable smoke binaries (14-day retention),
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Install Rust target
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@1.100.0
|
||||
with:
|
||||
targets: aarch64-unknown-linux-ohos
|
||||
- name: Install OpenHarmony native SDK
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: pet/package-lock.json
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- 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@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
if: runner.os == 'macOS'
|
||||
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master 2026-07-18
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
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 --toolchain stable ${{ matrix.target }}
|
||||
rustup target add ${{ 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'
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
package-manager-cache: false
|
||||
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # stable 2026-07-18
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
- name: Match dispatch to the requested commit
|
||||
id: source
|
||||
shell: bash
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master 2026-07-18
|
||||
with:
|
||||
toolchain: stable
|
||||
toolchain: 1.100.0
|
||||
components: clippy, rustfmt
|
||||
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
id: sccache
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
name: RU01 RU08 candidate images
|
||||
on:
|
||||
push:
|
||||
branches: [ci/codex-image-probe-CW-8205]
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
image:
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- image: release
|
||||
arch: amd64
|
||||
runner: ubuntu-24.04
|
||||
- image: release
|
||||
arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
- image: devcontainer
|
||||
arch: amd64
|
||||
runner: ubuntu-24.04
|
||||
- image: devcontainer
|
||||
arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
CANDIDATE_SHA: 2d1ba71ed7f4919f7baed5ec7b1dc99fccf6cd38
|
||||
IMAGE_KIND: ${{ matrix.image }}
|
||||
IMAGE_ARCH: ${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003
|
||||
- name: Qualify exact candidate container
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p receipts candidate
|
||||
git archive "$CANDIDATE_SHA" | tar -x -C candidate
|
||||
printf 'workflow_sha=%s\ncandidate_sha=%s\nimage=%s\narchitecture=%s\n' "$GITHUB_SHA" "$CANDIDATE_SHA" "$IMAGE_KIND" "$IMAGE_ARCH" | tee receipts/identity.txt
|
||||
uname -m | tee -a receipts/identity.txt
|
||||
docker version > receipts/docker-version.txt
|
||||
python3 scripts/ci/qualify-candidate-image.py 2>&1 | tee receipts/qualification.log
|
||||
if grep -E "syncing channel updates for .stable|downloading toolchain .stable" receipts/qualification.log; then
|
||||
exit 1
|
||||
fi
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
|
||||
if: always()
|
||||
with:
|
||||
name: candidate-image-${{ matrix.image }}-${{ matrix.arch }}
|
||||
path: receipts/
|
||||
if-no-files-found: error
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- name: Install cargo-audit
|
||||
run: cargo install cargo-audit
|
||||
- name: Run cargo audit
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.expected_sha }}
|
||||
fetch-depth: 1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@1.100.0
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- name: Run focused tests
|
||||
shell: bash
|
||||
|
||||
+1
-1
@@ -7,6 +7,7 @@ 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).
|
||||
@@ -21,7 +22,6 @@ 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
@@ -12,7 +12,7 @@
|
||||
# Or mount an env file:
|
||||
# docker run --rm -it --env-file .env codewhale
|
||||
|
||||
ARG RUST_VERSION=1.88
|
||||
ARG RUST_VERSION=1.100.0
|
||||
|
||||
# ── Stage 1: Build ────────────────────────────────────────────────────
|
||||
FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-slim-bookworm AS builder
|
||||
|
||||
@@ -7,6 +7,7 @@ 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).
|
||||
@@ -21,7 +22,6 @@ 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.88-bookworm
|
||||
image: rust:1.100.0-bookworm
|
||||
stages:
|
||||
- name: release version check
|
||||
script: |
|
||||
|
||||
@@ -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 stable` to fix this.
|
||||
`rustup default 1.100.0` 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
@@ -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 stable Rust
|
||||
selection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`
|
||||
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`
|
||||
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 = "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 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 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 the repo-selected stable Rust (1.98.1).
|
||||
Tested result: **works** on Rust 1.98.1, the version the repository selected when this was measured.
|
||||
* `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
|
||||
stable-x86_64-unknown-linux-gnu by rustup toolchain file`, which is harmless.
|
||||
1.100.0-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
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
子智能体(subagent)工作树)。这些被拉起的 shell 只有在 `/workspace` 内部
|
||||
才能看到仓库的 `rust-toolchain.toml` 覆盖设置,所以没有全局默认值时,
|
||||
它们会以“rustup could not choose a version of rustc to run”失败。
|
||||
更新脚本会运行 `rustup default stable` 来修复这一点。
|
||||
更新脚本会运行 `rustup default 1.100.0` 来修复这一点。
|
||||
- **`/workspace` 上已知的环境相关测试失败(不是代码缺陷):** 因为检出目录
|
||||
直接位于 `/` 之下,有两个 `codewhale-tui` 子智能体测试会在这里失败——
|
||||
`git_repo_root_reports_attempted_paths_when_no_repo_found`(无法在不可写的
|
||||
|
||||
@@ -28,7 +28,7 @@ cargo build --release --locked -p codewhale-cli
|
||||
target/release/codewhale --version
|
||||
```
|
||||
|
||||
构建前置条件见下文 Cargo 章节;使用仓库选定的 stable Rust,最低声明版本
|
||||
构建前置条件见下文 Cargo 章节;使用仓库固定的 Rust 1.100.0,最低声明版本
|
||||
为 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 = "stable"`)。在检出目录里运行的第一条 `cargo` 命令,无论你的默认工具链是什么,都会**悄悄下载最新的 stable 工具链**(约 250 MB)。
|
||||
* workspace 把所有编译器警告都当作错误。用 Rust 1.89 时,构建会在大约 11 分钟后**失败**,在 `codewhale-tui` 中出现 8 个 `error: this lint expectation is unfulfilled`。请使用仓库选定的 stable 工具链,不要传入更旧的 `+toolchain`。
|
||||
* 仓库里有 `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。
|
||||
* workspace 的 release profile 使用 thin LTO。在 4 vCPU / 15 GB 的虚拟机上,仅 `codewhale-tui` 这一个 crate 就编译了一个多小时,内存峰值 6–8 GB。请准备 16 GB 或更多内存,并预期这是迄今最慢的安装路径。`target/` 增长超过了 1.2 GB。
|
||||
|
||||
测试结果:用仓库选定的 stable Rust(1.98.1)**可行**。
|
||||
测试结果:用 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 stable-x86_64-unknown-linux-gnu by rustup toolchain file`,这是无害的。
|
||||
* Cargo 会打印 `warning: default toolchain implicitly overridden with 1.100.0-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
-1
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
channel = "1.100.0"
|
||||
@@ -0,0 +1,65 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
|
||||
def run(*args):
|
||||
print('+ ' + shlex.join(args), flush=True)
|
||||
subprocess.run(args, check=True)
|
||||
|
||||
|
||||
source = Path('candidate').resolve()
|
||||
arch = os.environ['IMAGE_ARCH']
|
||||
kind = os.environ['IMAGE_KIND']
|
||||
sha = os.environ['CANDIDATE_SHA']
|
||||
platform = 'linux/' + arch
|
||||
machine = {'amd64': 'x86_64', 'arm64': 'aarch64'}[arch]
|
||||
tag = 'candidate-probe:' + kind
|
||||
build = ['docker', 'buildx', 'build', '--platform', platform, '--load', '--progress=plain', '--tag', tag]
|
||||
if kind == 'release':
|
||||
run(*build, '--target', 'builder', '--build-arg', 'CODEWHALE_BUILD_SHA=' + sha, str(source))
|
||||
run('docker', 'run', '--rm', '--platform', platform, tag, '/bin/sh', '-ec',
|
||||
"rustc --version; rustc --version | grep '^rustc 1.100.0 '; rustup toolchain list | tee /tmp/toolchains; test \"$(wc -l < /tmp/toolchains)\" -eq 1; grep '^1.100.0-' /tmp/toolchains")
|
||||
run(*build, '--build-arg', 'CODEWHALE_BUILD_SHA=' + sha, str(source))
|
||||
run('docker', 'image', 'inspect', tag)
|
||||
run('docker', 'run', '--rm', '--platform', platform, '--entrypoint', '/bin/sh', tag, '-ec',
|
||||
f'test "$(uname -m)" = {machine}; test "$(id -un)" = codewhale; codewhale --version | grep -F 0.10.3; codew --version | grep -F 0.10.3; cmp /usr/local/bin/codewhale /usr/local/bin/codew')
|
||||
else:
|
||||
config = json.loads((source / '.devcontainer/devcontainer.json').read_text())
|
||||
recipe = source / '.devcontainer' / config['build']['dockerfile']
|
||||
context = source / '.devcontainer' / config['build']['context']
|
||||
for key, value in config['build'].get('args', {}).items():
|
||||
build.extend(['--build-arg', key + '=' + value])
|
||||
run(*build, '--file', str(recipe), str(context))
|
||||
run('docker', 'image', 'inspect', tag)
|
||||
command = ['docker', 'run', '--rm', '--platform', platform, '--user', config['remoteUser'],
|
||||
'--mount', f'type=bind,source={source},target=/workspaces/CodeWhale,readonly',
|
||||
'--workdir', '/workspaces/CodeWhale']
|
||||
for mount in config['mounts']:
|
||||
command.extend(['--mount', mount])
|
||||
for key, value in config['remoteEnv'].items():
|
||||
if value.startswith('${localEnv:'):
|
||||
continue
|
||||
command.extend(['--env', key + '=' + value])
|
||||
post_create = config['postCreateCommand']
|
||||
assert post_create == 'cargo build --locked', post_create
|
||||
probe = f'''set -eu
|
||||
id
|
||||
uname -m
|
||||
test "$(id -un)" = {shlex.quote(config['remoteUser'])}
|
||||
test "$(uname -m)" = {machine}
|
||||
rustup toolchain list | tee /tmp/toolchains-before
|
||||
test "$(wc -l < /tmp/toolchains-before)" -eq 1
|
||||
grep '^1.100.0-' /tmp/toolchains-before
|
||||
rustc --version
|
||||
rustc --version | grep '^rustc 1.100.0 '
|
||||
cargo --version
|
||||
rustup show active-toolchain
|
||||
{post_create}
|
||||
rustup toolchain list | tee /tmp/toolchains-after
|
||||
cmp /tmp/toolchains-before /tmp/toolchains-after
|
||||
'''
|
||||
run(*command, tag, '/bin/bash', '-lc', probe)
|
||||
print('PASS: ' + kind + ' ' + platform + ' candidate=' + sha, flush=True)
|
||||
+2
-9
@@ -9,21 +9,14 @@ 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). 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
|
||||
# (cloudflare cron triggers don't need this; they set cf-cron).
|
||||
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 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
|
||||
# Admin panel auth. Set to a random secret; enter it in the /admin login form.
|
||||
MAINTAINER_TOKEN=
|
||||
|
||||
# GitHub PAT for posting comments via /admin. Needs issues:write scope.
|
||||
|
||||
+3
-5
@@ -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 (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 |
|
||||
| `CRON_SECRET` | Optional | Shared secret for manual cron invocation |
|
||||
| `MAINTAINER_TOKEN` | Optional | Auth token for /admin panel |
|
||||
| `MAINTAINER_GITHUB_PAT` | Optional | GitHub PAT with `issues:write` scope for posting comments |
|
||||
|
||||
## Initial deployment
|
||||
@@ -103,9 +103,7 @@ 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:** 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).
|
||||
2. **Set secrets:**
|
||||
```bash
|
||||
npx wrangler secret put DEEPSEEK_API_KEY
|
||||
npx wrangler secret put MAINTAINER_TOKEN
|
||||
|
||||
+2
-2
@@ -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 (32-512 characters, 8+ distinct; a shorter value repeated is refused)| optional (Cloudflare cron triggers don't need it) |
|
||||
| `CRON_SECRET` | Shared secret for manual `/api/cron` invocation | 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 (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_TOKEN` | Admin panel auth; 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,7 +4,6 @@ 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";
|
||||
@@ -78,8 +77,7 @@ export default async function AdminPage({
|
||||
|
||||
const env = await getAgentEnv();
|
||||
|
||||
if (!maintainerTokenUsable(env.MAINTAINER_TOKEN)) {
|
||||
if (env.MAINTAINER_TOKEN) console.warn(MAINTAINER_TOKEN_UNAVAILABLE);
|
||||
if (!env.MAINTAINER_TOKEN) {
|
||||
return (
|
||||
<div className="route-state">
|
||||
<ConnectionBanner locale={locale} />
|
||||
@@ -100,7 +98,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, env.MAINTAINER_TOKEN);
|
||||
const authed = await validateSession(env.CURATED_KV, sid);
|
||||
|
||||
if (!authed) {
|
||||
return <LoginForm locale={locale} error={err === "1"} />;
|
||||
|
||||
@@ -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 (!maintainerTokenUsable(env.MAINTAINER_TOKEN)) {
|
||||
return new NextResponse(MAINTAINER_TOKEN_UNAVAILABLE, {
|
||||
if (!env.MAINTAINER_TOKEN) {
|
||||
return new NextResponse("Not configured", {
|
||||
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_SECRET_CHARS) {
|
||||
if (submitted.length > MAX_TOKEN_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, env.MAINTAINER_TOKEN);
|
||||
const sid = await createSession(env.CURATED_KV);
|
||||
if (!sid) {
|
||||
return new NextResponse("Session storage unavailable", {
|
||||
status: 503,
|
||||
|
||||
@@ -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 (!await validateSession(authEnv.CURATED_KV, sid, authEnv.MAINTAINER_TOKEN)) throw new MerchError(401, "unauthorized", "Maintainer sign-in is required.");
|
||||
if (!authEnv.MAINTAINER_TOKEN || !await validateSession(authEnv.CURATED_KV, sid)) 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);
|
||||
|
||||
@@ -39,7 +39,7 @@ async function checkAuth(req: Request, env: CommunityAgentEnv): Promise<{ ok: bo
|
||||
}
|
||||
}
|
||||
|
||||
if (!sid || !(await validateSession(env.CURATED_KV, sid, env.MAINTAINER_TOKEN))) {
|
||||
if (!sid || !(await validateSession(env.CURATED_KV, sid))) {
|
||||
return { ok: false, status: 401, error: "unauthorized" };
|
||||
}
|
||||
return { ok: true };
|
||||
|
||||
@@ -4,7 +4,6 @@ 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";
|
||||
|
||||
@@ -31,33 +30,9 @@ 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, headers: { "Cache-Control": "no-store" } });
|
||||
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"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
|
||||
"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
|
||||
},
|
||||
{
|
||||
"path": "docs/MODES.md",
|
||||
@@ -36,8 +36,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"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
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -52,8 +52,8 @@
|
||||
},
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"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
|
||||
"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
|
||||
},
|
||||
{
|
||||
"path": "docs/KEYBINDINGS.md",
|
||||
@@ -68,8 +68,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"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
|
||||
"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
|
||||
},
|
||||
{
|
||||
"path": "docs/PROVIDERS.md",
|
||||
@@ -387,8 +387,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/INSTALL.md",
|
||||
"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
|
||||
"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
|
||||
},
|
||||
{
|
||||
"path": "docs/OPERATIONS_RUNBOOK.md",
|
||||
|
||||
@@ -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-0123456789";
|
||||
const FIXTURE_TOKEN = "local-admin-integration-fixture";
|
||||
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, FIXTURE_TOKEN)).toBe(true);
|
||||
expect(await validateSession(sessionKv, sid)).toBe(true);
|
||||
expect(safeEqual).toHaveBeenCalledExactlyOnceWith(FIXTURE_TOKEN, FIXTURE_TOKEN);
|
||||
expect(createSession).toHaveBeenCalledExactlyOnceWith(sessionKv, FIXTURE_TOKEN);
|
||||
expect(createSession).toHaveBeenCalledExactlyOnceWith(sessionKv);
|
||||
const cookie = response.headers.get("Set-Cookie")?.toLowerCase();
|
||||
expect(cookie).toContain("httponly");
|
||||
expect(cookie).toContain("secure");
|
||||
@@ -69,13 +69,7 @@ describe("admin login with real credential and session helpers", () => {
|
||||
expect.any(String), expect.any(String), { expirationTtl: 86400 },
|
||||
]);
|
||||
sessions.clear();
|
||||
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);
|
||||
expect(await validateSession(sessionKv, sid)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a wrong credential without creating a session", async () => {
|
||||
|
||||
@@ -5,9 +5,6 @@ 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",
|
||||
@@ -17,7 +14,7 @@ function request(token = "wrong", ip = "127.0.0.1") {
|
||||
}
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mocks.getAgentEnv.mockResolvedValue({ MAINTAINER_TOKEN: STRONG_TOKEN, ADMIN_LOGIN_LIMITER: { limit: mocks.limit }, CURATED_KV: {} });
|
||||
mocks.getAgentEnv.mockResolvedValue({ MAINTAINER_TOKEN: "configured", ADMIN_LOGIN_LIMITER: { limit: mocks.limit }, CURATED_KV: {} });
|
||||
mocks.limit.mockResolvedValue({ success: true });
|
||||
mocks.safeEqual.mockResolvedValue(false);
|
||||
mocks.createSession.mockResolvedValue("session-id");
|
||||
@@ -38,7 +35,7 @@ describe("admin login attempt boundary", () => {
|
||||
});
|
||||
it("does not parse or compare credentials after refusal", async () => {
|
||||
mocks.limit.mockResolvedValue({ success: false });
|
||||
const req = request(STRONG_TOKEN);
|
||||
const req = request("configured");
|
||||
const read = vi.spyOn(req, "text");
|
||||
expect((await POST(req)).status).toBe(429);
|
||||
expect(read).not.toHaveBeenCalled();
|
||||
@@ -46,47 +43,16 @@ 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: STRONG_TOKEN });
|
||||
mocks.getAgentEnv.mockResolvedValueOnce({ MAINTAINER_TOKEN: "configured" });
|
||||
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(STRONG_TOKEN));
|
||||
const response = await POST(request("configured"));
|
||||
expect(response.status).toBe(303);
|
||||
expect(response.headers.get("Location")).toBe("https://codewhale.net/en/admin");
|
||||
const cookie = response.headers.get("Set-Cookie");
|
||||
|
||||
@@ -144,7 +144,7 @@ function stubAdminEnv(kv: FakeKv, lock?: FakeDraftClaimLock) {
|
||||
mocks.getAgentEnv.mockResolvedValue({
|
||||
CURATED_KV: kv,
|
||||
...(lock ? { DRAFT_CLAIM_LOCK: lock } : {}),
|
||||
MAINTAINER_TOKEN: "local-fixture-token-0123456789-abcdefghij",
|
||||
MAINTAINER_TOKEN: "configured",
|
||||
MAINTAINER_GITHUB_PAT: "ghp_test",
|
||||
GITHUB_REPO: "codewhale-hq/CodeWhale",
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { createSession, logUsage, validateSession } from "./community-agent";
|
||||
import { logUsage, validateSession } from "./community-agent";
|
||||
|
||||
describe("validateSession", () => {
|
||||
let mockKV: {
|
||||
@@ -20,88 +20,65 @@ 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, TOKEN);
|
||||
const result = await validateSession(undefined, VALID_SID);
|
||||
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, TOKEN)).toBe(false);
|
||||
expect(await validateSession(kv, null, TOKEN)).toBe(false);
|
||||
expect(await validateSession(kv, undefined)).toBe(false);
|
||||
expect(await validateSession(kv, null)).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, "", TOKEN)).toBe(false);
|
||||
expect(await validateSession(kv, "")).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), TOKEN)).toBe(false);
|
||||
expect(await validateSession(kv, "a".repeat(39))).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), TOKEN)).toBe(false);
|
||||
expect(await validateSession(kv, "a".repeat(65))).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) + "!", TOKEN)).toBe(false);
|
||||
expect(await validateSession(kv, "a".repeat(39) + "!")).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, TOKEN);
|
||||
const result = await validateSession(kv, VALID_SID);
|
||||
expect(result).toBe(false);
|
||||
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 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);
|
||||
});
|
||||
|
||||
it("should refuse a stored record with no token fingerprint or unparseable content", async () => {
|
||||
it("should return true if session is found in KV", 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);
|
||||
}
|
||||
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 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();
|
||||
it("should work with a 64 character sid", async () => {
|
||||
const kv = mockKV as unknown as Parameters<typeof validateSession>[0];
|
||||
mockKV.get.mockResolvedValue("{}");
|
||||
|
||||
const sid = "b".repeat(64);
|
||||
const result = await validateSession(kv, sid);
|
||||
expect(result).toBe(true);
|
||||
expect(mockKV.get).toHaveBeenCalledWith(SESSION_PREFIX + sid);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
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";
|
||||
|
||||
@@ -723,7 +722,6 @@ 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 = "";
|
||||
@@ -740,39 +738,21 @@ export async function safeEqual(a: string, b: string): Promise<boolean> {
|
||||
return diff === 0;
|
||||
}
|
||||
|
||||
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;
|
||||
export async function createSession(kv: KVNamespace | undefined): Promise<string | null> {
|
||||
if (!kv) return null;
|
||||
const bytes = new Uint8Array(32);
|
||||
crypto.getRandomValues(bytes);
|
||||
const sid = toBase64Url(bytes);
|
||||
const value = JSON.stringify({ createdAt: Date.now(), tokenFingerprint: await tokenFingerprint(token) });
|
||||
const value = JSON.stringify({ createdAt: Date.now() });
|
||||
await kv.put(SESSION_PREFIX + sid, value, { expirationTtl: SESSION_TTL_SEC });
|
||||
return sid;
|
||||
}
|
||||
|
||||
export async function validateSession(
|
||||
kv: KVNamespace | undefined,
|
||||
sid: string | undefined | null,
|
||||
token: string | undefined,
|
||||
): Promise<boolean> {
|
||||
if (!kv || !sid || !maintainerTokenUsable(token)) return false;
|
||||
export async function validateSession(kv: KVNamespace | undefined, sid: string | undefined | null): Promise<boolean> {
|
||||
if (!kv || !sid) return false;
|
||||
if (!/^[A-Za-z0-9_-]{40,64}$/.test(sid)) return false;
|
||||
const raw = await kv.get(SESSION_PREFIX + sid);
|
||||
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)));
|
||||
return raw !== null;
|
||||
}
|
||||
|
||||
export async function deleteSession(kv: KVNamespace | undefined, sid: string | undefined | null): Promise<void> {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,6 @@ 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;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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;
|
||||
@@ -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-fixture-token-0123456789-abcdefghij" };
|
||||
const sid = await createSession(auth.CURATED_KV, auth.MAINTAINER_TOKEN);
|
||||
const auth = { CURATED_KV: env.CURATED_KV, MAINTAINER_TOKEN: "local-maintainer-fixture" };
|
||||
const sid = await createSession(auth.CURATED_KV);
|
||||
return { auth, headers: { Cookie: "mt_sid=" + sid } };
|
||||
}
|
||||
it("requires a real stored maintainer session for reading or deleting contacts", async () => {
|
||||
@@ -165,12 +165,6 @@ 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);
|
||||
|
||||
@@ -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 (!await validateSession(auth.CURATED_KV, sid, auth.MAINTAINER_TOKEN)) throw new InterestError(401, "unauthorized", "Maintainer sign-in is required.");
|
||||
if (!auth.MAINTAINER_TOKEN || !await validateSession(auth.CURATED_KV, sid)) throw new InterestError(401, "unauthorized", "Maintainer sign-in is required.");
|
||||
}
|
||||
export async function handleAdminMerchInterest(request: Request, env?: MerchInterestEnv, authEnv?: CommunityAgentEnv): Promise<Response> {
|
||||
try {
|
||||
|
||||
@@ -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: "local-fixture-token-0123456789-abcdefghij",
|
||||
MAINTAINER_TOKEN: "configured",
|
||||
});
|
||||
|
||||
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: "local-fixture-token-0123456789-abcdefghij",
|
||||
MAINTAINER_TOKEN: "configured",
|
||||
MAINTAINER_GITHUB_PAT: "ghp_test",
|
||||
GITHUB_REPO: "codewhale-hq/CodeWhale",
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user