Full-depth rename per the approved plan; the 'claw' product vocabulary (claws, /claws routes, clawId, Claw Chat) stays — it is now the brand. - Display brand: Clawmates (manifest, titles, hero, login/rail logo 'clawmates'); default host app.clawmates.work; registry ghcr.io/clawmates - Crates tc-* -> cm-* (16 crates + all imports); binaries clawmates-server/broker/bundler; images clawmates/*; env prefix CLAWMATES_* (+ CM_TEST_DATABASE_URL / CM_LIVE_LLM); config clawmates.toml; helm chart deploy/helm/clawmates with clawmates-* resources; db names clawmates*; sockets /run/clawmates; cookie cm_session; kind cluster clawmates-test; seccomp node profile clawmates-agent-profile.json - All 9 Playwright brand assertions updated in lockstep; historical spec document left untouched as the only remaining 'TeamClaw' - Local env migrated: dev pg clawmates-dev-pg/clawmates_dev, shared test server clawmates-test-pg, kind cluster recreated with image + profile, compose images rebuilt under clawmates/* Verified end to end: 161 Rust + 68 frontend tests, 29 Playwright journeys, 4 live kind tests, helm/install/LOC/placeholder gates, and the clean-room install rehearsal serving the clawmates login page from a signed bundle of the rebuilt images. Co-Authored-By: Claude Fable 5 <[email protected]>
114 lines
3.6 KiB
Rust
114 lines
3.6 KiB
Rust
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
use clawmates_bundler::{assemble, generate_keypair, verify, BundleError};
|
|
|
|
struct Fixture {
|
|
root: PathBuf,
|
|
public_key: PathBuf,
|
|
bundle: PathBuf,
|
|
}
|
|
|
|
fn fixture() -> Fixture {
|
|
let root = std::env::temp_dir().join(format!("tc-bundle-{}", uuid::Uuid::now_v7()));
|
|
fs::create_dir_all(&root).unwrap();
|
|
let private_key = root.join("release.key");
|
|
let public_key = root.join("release.pub");
|
|
generate_keypair(&private_key, &public_key).unwrap();
|
|
|
|
fs::write(root.join("server.tar"), b"pretend image bytes").unwrap();
|
|
fs::write(root.join("compose.yml"), b"services: {}").unwrap();
|
|
fs::write(root.join("0001_init.sql"), b"CREATE TABLE t ();").unwrap();
|
|
|
|
let bundle = root.join("bundle");
|
|
assemble(
|
|
&bundle,
|
|
"1.2.3",
|
|
&[
|
|
(root.join("server.tar"), "images/server.tar".into()),
|
|
(
|
|
root.join("compose.yml"),
|
|
"compose/docker-compose.yml".into(),
|
|
),
|
|
(
|
|
root.join("0001_init.sql"),
|
|
"migrations/0001_init.sql".into(),
|
|
),
|
|
],
|
|
&private_key,
|
|
)
|
|
.unwrap();
|
|
Fixture {
|
|
root,
|
|
public_key,
|
|
bundle,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn assembled_bundles_verify_offline() {
|
|
let fx = fixture();
|
|
// 3 artifacts + manifest.json.
|
|
assert_eq!(verify(&fx.bundle, &fx.public_key).unwrap(), 4);
|
|
|
|
let manifest: serde_json::Value =
|
|
serde_json::from_str(&fs::read_to_string(fx.bundle.join("manifest.json")).unwrap())
|
|
.unwrap();
|
|
assert_eq!(manifest["version"], "1.2.3");
|
|
assert_eq!(manifest["artifacts"].as_array().unwrap().len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn a_tampered_artifact_is_detected() {
|
|
let fx = fixture();
|
|
fs::write(fx.bundle.join("images/server.tar"), b"EVIL image bytes").unwrap();
|
|
let err = verify(&fx.bundle, &fx.public_key).unwrap_err();
|
|
assert!(matches!(err, BundleError::ChecksumMismatch(p) if p == "images/server.tar"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_tampered_checksum_list_fails_the_signature() {
|
|
let fx = fixture();
|
|
// Attacker edits both the artifact AND its checksum line — the
|
|
// signature over checksums.txt catches it.
|
|
let listing = fs::read_to_string(fx.bundle.join("checksums.txt")).unwrap();
|
|
fs::write(
|
|
fx.bundle.join("checksums.txt"),
|
|
listing.replace("images/server.tar", "images/sneaky.tar"),
|
|
)
|
|
.unwrap();
|
|
let err = verify(&fx.bundle, &fx.public_key).unwrap_err();
|
|
assert!(matches!(err, BundleError::BadSignature));
|
|
}
|
|
|
|
#[test]
|
|
fn the_wrong_public_key_is_refused() {
|
|
let fx = fixture();
|
|
let other_private = fx.root.join("other.key");
|
|
let other_public = fx.root.join("other.pub");
|
|
generate_keypair(&other_private, &other_public).unwrap();
|
|
let err = verify(&fx.bundle, &other_public).unwrap_err();
|
|
assert!(matches!(err, BundleError::BadSignature));
|
|
}
|
|
|
|
#[test]
|
|
fn a_missing_artifact_is_reported() {
|
|
let fx = fixture();
|
|
fs::remove_file(fx.bundle.join("migrations/0001_init.sql")).unwrap();
|
|
let err = verify(&fx.bundle, &fx.public_key).unwrap_err();
|
|
assert!(matches!(err, BundleError::Missing(p) if p.contains("0001_init.sql")));
|
|
}
|
|
|
|
#[test]
|
|
fn the_public_key_rederives_from_the_signing_key() {
|
|
let fx = fixture();
|
|
let derived = fx.root.join("derived.pub");
|
|
clawmates_bundler::derive_public_key(&fx.root.join("release.key"), &derived).unwrap();
|
|
assert_eq!(
|
|
fs::read_to_string(&derived).unwrap(),
|
|
fs::read_to_string(&fx.public_key).unwrap()
|
|
);
|
|
// And it verifies the bundle, same as the original.
|
|
assert_eq!(verify(&fx.bundle, &derived).unwrap(), 4);
|
|
}
|