From 30c246440116656d4d3dcac676eabe852b0f8b68 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Tue, 14 Jul 2026 18:24:43 +0000 Subject: [PATCH] [#177] Debug-gate LIVECAP_MODEL_BASE_URL; extend release-invariants to assert it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LIVECAP_MODEL_BASE_URL is documented as a #110 dev-run knob (exercise the download-failure fallback) but the env read was NOT #[cfg(debug_assertions)]- gated, so a release binary honored it — the same #161-class "dev-only env escape compiled into release" the #146 doc claims we don't ship. (Bounded: pointer_url stays pinned to the official HF_REPO, so the SHA-256 digest source can't move — hygiene, not integrity — but a shipped binary should expose no dev env knobs.) Gate it like dev_flags.rs / the #64 bleed dump: debug `blob_base_url()` reads the env; release `blob_base_url()` returns `base_url_or_default(None)` (the official repo) with the env read compiled out entirely. The pure `base_url_or_default` helper + its tests are unchanged; SHA-pinning untouched. The now-release-unused `MODEL_BASE_URL_ENV` const carries `#[cfg_attr(not(debug_assertions), allow(dead_code))]` — unused → no string emitted → the release binary carries no `LIVECAP_MODEL_BASE_URL` symbol. Extend the #176 release-invariants CI job's FORBIDDEN list with LIVECAP_MODEL_BASE_URL so a dropped guard is caught permanently. Proofs (local): debug example strings → present (knob live); release example strings → ABSENT; a seeded un-gated release read → PRESENT (gate would FAIL); `cargo test --release --no-run` + release clippy -D warnings clean; model tests 5/5. No new deps; no caption/audio content logged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 15 ++++++++------- crates/livecap-core/src/model.rs | 18 +++++++++++++++++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36e6fec..62119eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,13 +63,14 @@ jobs: - run: cargo test --workspace release-invariants: - # #176: turn the #161-class privacy invariant into a permanent gate. CI's + # #176/#177: turn the #161-class privacy invariant into a permanent gate. CI's # other jobs only compile/test the DEBUG profile, so a dropped - # `#[cfg(debug_assertions)]` guard (e.g. a bad merge on the #64 bleed dump or a - # #108 dev flag) is invisible to them and only surfaces at tag time. This job - # compiles livecap-app in RELEASE and asserts the debug-only env-escape NAMES - # are absent from the binary — they exist only inside `#[cfg(debug_assertions)]` - # items, so a release build compiles them out entirely (#146/#161). + # `#[cfg(debug_assertions)]` guard (e.g. a bad merge on the #64 bleed dump, a + # #108 dev flag, or the #177 model-base-url knob) is invisible to them and only + # surfaces at tag time. This job compiles livecap-app in RELEASE and asserts the + # debug-only env-escape NAMES are absent from the binary — they exist only + # inside `#[cfg(debug_assertions)]` items, so a release build compiles them out + # entirely (#146/#161/#177). runs-on: macos-14 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -98,7 +99,7 @@ jobs: # These env vars are read ONLY inside #[cfg(debug_assertions)] items, so a # release build must contain none of their names. A hit means a guard was # dropped and the shipped binary regained a covert env path (#161-class). - FORBIDDEN="LIVECAP_BLEED_DUMP_DIR LIVECAP_CAPTURE_VISIBLE LIVECAP_AUTOSTART LIVECAP_UI_PROBE" + FORBIDDEN="LIVECAP_BLEED_DUMP_DIR LIVECAP_CAPTURE_VISIBLE LIVECAP_AUTOSTART LIVECAP_UI_PROBE LIVECAP_MODEL_BASE_URL" fail=0 for name in $FORBIDDEN; do if strings "$BIN" | grep -q "$name"; then diff --git a/crates/livecap-core/src/model.rs b/crates/livecap-core/src/model.rs index c0dbab3..cb65f1d 100644 --- a/crates/livecap-core/src/model.rs +++ b/crates/livecap-core/src/model.rs @@ -33,13 +33,29 @@ const HF_REPO: &str = "https://huggingface.co/ggerganov/whisper.cpp"; /// source — [`pointer_url`] is pinned to the official [`HF_REPO`] — so an /// overridden host cannot serve a fake LFS pointer alongside a matching fake /// blob; any blob that does not match the OFFICIAL digest fails verification. +// Release builds never read this env var (#177), so the const is unused there — +// which is the point: an unused const emits no string, so the release binary +// carries no `LIVECAP_MODEL_BASE_URL` symbol (asserted by the release-invariants +// CI gate). The `allow(dead_code)` only silences the release-profile warning. +#[cfg_attr(not(debug_assertions), allow(dead_code))] const MODEL_BASE_URL_ENV: &str = "LIVECAP_MODEL_BASE_URL"; -/// The effective BLOB download base URL (env override or the official repo). +/// The effective BLOB download base URL. Debug builds honor the +/// [`MODEL_BASE_URL_ENV`] dev-run knob (#110 download-failure fallback); RELEASE +/// builds ALWAYS use the official [`HF_REPO`] — the env read is compiled out +/// entirely (#177, mirroring #146/#161), so a shipped binary exposes no such knob +/// and its model-download host can't be redirected. The pure +/// [`base_url_or_default`] core stays testable in both profiles. +#[cfg(debug_assertions)] fn blob_base_url() -> String { base_url_or_default(std::env::var(MODEL_BASE_URL_ENV).ok()) } +#[cfg(not(debug_assertions))] +fn blob_base_url() -> String { + base_url_or_default(None) +} + /// Pure core of [`blob_base_url`]: `None`/blank → the official repo. Trailing /// slashes are trimmed so the joined download URLs stay well-formed. fn base_url_or_default(overridden: Option) -> String {