Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitguardian.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# GitGuardian scan configuration.
# paths-ignore entries are deliberate, documented test fixtures — never real
# credentials. Each entry must name its reason inline.
paths-ignore:
# Deliberately public TEST-ONLY Ed25519 keypair committed as a fixture for the
# signed cloud-facts envelope tests (Rust + web). It is never pinned in
# crates/config/src/cloud_facts/keys.rs or web/lib/cloud-facts/keys.ts, signs
# nothing outside tests, and exists so the suites share one stable fixture.
# Introduced in 9e83f8184 with its rationale in the commit message.
- docs/cloud-facts/fixtures/test-only-signing-key.pem
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Cloud facts channel (`facts/v1`), behind `[cloud_facts].enabled` (default
**off**): a signed (Ed25519, keys pinned in the binary), versioned, ETag +
TTL cached overlay for model catalog deltas, provider defaults, release
truth, and one-line announcements. Bundled facts remain the floor; a
missing, rejected, or out-of-scope payload changes nothing, and the fetch
is never a startup dependency. New crate `codewhale-cloud-facts`,
`codewhale_config::cloud_facts`, catalog layer 15
(`CatalogSource::CloudFacts`), `/status` rows `Catalog:` and
`Cloud facts:`. Website route `/api/facts/v1/<channel>` (Supabase
`facts_*` tables, anon read-only, RLS forced) and the authoring tool
`web/scripts/facts-publish.mjs`. Note: codewhale.net is served by a
Cloudflare Worker (OpenNext), not Vercel as the PRD states; the route is
host-agnostic. See [CLOUD_FACTS.md](docs/CLOUD_FACTS.md).
- Website: the public site moves to the Tideline deep-ocean design language
(dark by default with an opt-in light documentation sheet, palette grounded
in the TUI's WHALE_* tokens) and the new whale brand mark across the favicon,
Expand Down
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"crates/app-server",
"crates/build-support",
"crates/cli",
"crates/cloud-facts",
"crates/command-contract",
"crates/config",
"crates/core",
Expand Down
16 changes: 16 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ check_for_updates = true
check_interval_hours = 24
# update_uri = "https://internal.mirror.example/codewhale/releases/latest"

# ─────────────────────────────────────────────────────────────────────────────────
# Cloud facts (docs/CLOUD_FACTS.md) — OFF by default
# ─────────────────────────────────────────────────────────────────────────────────
# A signed, versioned overlay of facts that move faster than releases (model
# catalog deltas, provider defaults, release truth, one-line notices). Bundled
# facts are always the floor; a rejected or unreachable payload changes nothing.
# Env: CODEWHALE_CLOUD_FACTS=1|0 overrides `enabled`; CODEWHALE_DISABLE_CLOUD_FACTS=1
# is a hard kill switch; CODEWHALE_CLOUD_FACTS_URL / _CHANNEL / _PATH override
# the endpoint, channel, or read a local envelope (air-gapped). `/status` shows
# the provenance ("Cloud facts:" row).
[cloud_facts]
enabled = false
channel = "stable"
ttl_hours = 6
# url = "https://codewhale.net/api/facts/v1/{channel}"

# ─────────────────────────────────────────────────────────────────────────────────
# Hotbar slots (#2061 / #2064)
# ─────────────────────────────────────────────────────────────────────────────────
Expand Down
24 changes: 24 additions & 0 deletions crates/cloud-facts/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "codewhale-cloud-facts"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Fetch + verified disk cache for the Codewhale cloud facts channel (facts/v1)"

[lints]
workspace = true

[dependencies]
codewhale-config = { path = "../config", version = "0.9.11" }
codewhale-release = { path = "../release", version = "0.9.11" }
reqwest.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tracing.workspace = true

[dev-dependencies]
tempfile.workspace = true
66 changes: 66 additions & 0 deletions crates/cloud-facts/examples/fetch_live.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! Dogfood/proof harness: run the cloud facts client once against a real
//! endpoint with an isolated `CODEWHALE_HOME` and print the resulting status.
//!
//! ```sh
//! CODEWHALE_HOME=$(mktemp -d) CODEWHALE_CLOUD_FACTS=1 \
//! CODEWHALE_CLOUD_FACTS_URL=http://localhost:3000/api/facts/v1/{channel} \
//! cargo run -p codewhale-cloud-facts --example fetch_live
//! ```
//!
//! The flag stays off by default; this example only honours the env override.

use codewhale_cloud_facts::{Settings, maybe_load_persisted_cache, refresh, status};
use codewhale_config::catalog::now_unix;
use codewhale_config::cloud_facts::overlay;

#[tokio::main(flavor = "current_thread")]
async fn main() {
let settings = Settings::default().resolve();
println!(
"enabled={} channel={} url={}",
settings.enabled,
settings.channel,
settings.url()
);
println!(
"cache={}",
codewhale_cloud_facts::cache_path().map_or("<none>".into(), |p| p.display().to_string())
);
let seeded = maybe_load_persisted_cache(&settings);
println!("seeded_from_disk={seeded:?}");
println!("before: {}", status().label(now_unix()));
match refresh(&settings, true).await {
Ok(outcome) => println!("refresh: {outcome:?}"),
Err(err) => println!("refresh error: {err}"),
}
let st = status();
println!("after: {}", st.label(now_unix()));
println!(
"status_json={}",
serde_json::to_string(&st).unwrap_or_default()
);
if let Some(facts) = overlay::overlay() {
println!(
"overlay: channel={} v{} key={} sha256={} patches={} defaults={} announcements={} dropped={}",
facts.channel,
facts.facts_version,
facts.key_id,
facts.sha256,
facts.models.len(),
facts.provider_defaults.len(),
facts.announcements.len(),
facts.dropped.len()
);
if let Some(release) = &facts.release {
println!(
"release: latest={} yanked={:?}",
release.latest, release.yanked
);
}
for a in &facts.announcements {
println!("announcement[{}] {:?}: {}", a.id, a.level, a.text);
}
} else {
println!("overlay: none (bundled facts in use)");
}
}
Loading
Loading