app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit a2038b3e25b9e34f0b8fd001f26a8ed10b5772cb
parent f97b1f1479dd3ab9382b03c1d1f4ae0f4aab1dd8
Author: triesap <tyson@radroots.org>
Date:   Sun,  9 Aug 2026 19:32:22 +0000

policy: enforce portable HarvestCircle checks

- reject legacy product names in tracked paths and source text
- preserve only the approved historical repository URL exception
- canonicalize secure database test roots and stabilize observer delivery
- verify governed and standalone contributor build lanes

Diffstat:
Aapp/desktop/src/test/kotlin/org/radroots/harvestcircle/architecture/LegacyProductIdentityGuardTest.kt | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/harvestcircle_application/src/snapshot.rs | 2+-
Mcore/crates/harvestcircle_ffi/src/observer.rs | 6++++--
Mcore/crates/harvestcircle_preferences/src/lib.rs | 2+-
Mcore/crates/harvestcircle_runtime/src/persistence.rs | 24++++++++++++++++++++----
Mcore/crates/harvestcircle_runtime/src/runtime_actor.rs | 6+++++-
Mcore/crates/harvestcircle_runtime/tests/restart_isolation.rs | 6+++++-
Mcore/crates/harvestcircle_storage/src/accounts.rs | 6+++++-
Mcore/crates/harvestcircle_storage/src/compatibility.rs | 6+++++-
Mcore/crates/harvestcircle_storage/src/db.rs | 6+++++-
Mcore/crates/harvestcircle_storage/src/recovery.rs | 6+++++-
Mcore/crates/harvestcircle_storage/src/repair.rs | 6+++++-
Mcore/crates/harvestcircle_storage/tests/redaction.rs | 6+++++-
13 files changed, 131 insertions(+), 16 deletions(-)

diff --git a/app/desktop/src/test/kotlin/org/radroots/harvestcircle/architecture/LegacyProductIdentityGuardTest.kt b/app/desktop/src/test/kotlin/org/radroots/harvestcircle/architecture/LegacyProductIdentityGuardTest.kt @@ -0,0 +1,65 @@ +package org.radroots.harvestcircle.architecture + +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.extension +import kotlin.io.path.name +import kotlin.io.path.readText +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class LegacyProductIdentityGuardTest { + @Test + fun trackedSourcesUseTheHarvestCircleNamingContract() { + val root = findRepositoryRoot() + val contract = root.resolve("AGENTS.md").readText() + assertTrue(contract.contains("`harvestcircle_*`")) + assertTrue(contract.contains("`org.radroots.harvestcircle`")) + assertTrue(contract.contains("`HarvestCircle*`")) + assertTrue(contract.contains("`HARVESTCIRCLE_*`")) + + val legacyProduct = "stu" + "dio" + val repositoryUrlException = "https://github.com/radrootslabs/" + legacyProduct + "_app" + val textExtensions = setOf("kt", "kts", "rs", "toml", "properties", "yml", "yaml", "md") + val textNames = setOf("Makefile", ".gitignore", "gradlew", "gradlew.bat") + val findings = + trackedFiles(root).flatMap { relative -> + buildList { + if (relative.lowercase().contains(legacyProduct)) { + add("$relative: legacy product name in tracked path") + } + + val path = root.resolve(relative) + if (path.extension in textExtensions || path.name in textNames) { + val inspected = path.readText().replace(repositoryUrlException, "") + if (inspected.lowercase().contains(legacyProduct)) { + add("$relative: legacy product name in tracked text") + } + } + } + } + + assertEquals(emptyList(), findings.sorted()) + } +} + +private fun trackedFiles(root: Path): List<String> { + val process = + ProcessBuilder("git", "-C", root.toString(), "ls-files", "-z") + .redirectErrorStream(true) + .start() + val output = process.inputStream.readAllBytes() + check(process.waitFor() == 0) { + "Unable to enumerate tracked HarvestCircle sources: ${output.toString(StandardCharsets.UTF_8)}" + } + return output + .toString(StandardCharsets.UTF_8) + .split('\u0000') + .filter(String::isNotEmpty) +} + +private fun findRepositoryRoot(): Path = + generateSequence(Path.of("").toAbsolutePath()) { it.parent } + .first { Files.isRegularFile(it.resolve("core/Cargo.toml")) && Files.isDirectory(it.resolve("app/desktop")) } diff --git a/core/crates/harvestcircle_application/src/snapshot.rs b/core/crates/harvestcircle_application/src/snapshot.rs @@ -77,7 +77,7 @@ impl RelayConfiguration { /// # Errors /// /// Returns a safe configuration error before runtime or network work when - /// the relay count exceeds the Studio policy. + /// the relay count exceeds the HarvestCircle policy. pub fn new(relays: Vec<RelayUrl>) -> Result<Self, SafeError> { if relays.len() > MAX_CONFIGURED_RELAYS { return Err(relay_limit_exceeded()); diff --git a/core/crates/harvestcircle_ffi/src/observer.rs b/core/crates/harvestcircle_ffi/src/observer.rs @@ -482,8 +482,10 @@ mod tests { } } + const OBSERVER_DELIVERY_TIMEOUT: Duration = Duration::from_secs(5); + async fn wait_for_snapshot_count(observer: &RecordingObserver, minimum: usize) { - tokio::time::timeout(Duration::from_secs(1), async { + tokio::time::timeout(OBSERVER_DELIVERY_TIMEOUT, async { while observer.snapshots.lock().expect("snapshots").len() < minimum { tokio::task::yield_now().await; } @@ -493,7 +495,7 @@ mod tests { } async fn wait_for_fresh_profile(observer: &RecordingObserver) { - tokio::time::timeout(Duration::from_secs(1), async { + tokio::time::timeout(OBSERVER_DELIVERY_TIMEOUT, async { loop { let fresh = observer .snapshots diff --git a/core/crates/harvestcircle_preferences/src/lib.rs b/core/crates/harvestcircle_preferences/src/lib.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MPL-2.0 -//! UI-neutral Studio preference state. +//! UI-neutral HarvestCircle preference state. //! //! This module carries forward the uniquely required preference behavior from //! source commit `6074a4745be361f21bb47d4778c74a14b2d57954`. It intentionally diff --git a/core/crates/harvestcircle_runtime/src/persistence.rs b/core/crates/harvestcircle_runtime/src/persistence.rs @@ -355,7 +355,11 @@ mod tests { #[test] fn persistent_bootstrap_handles_fresh_and_existing_signed_out_state() { let directory = tempdir().expect("directory"); - let path = directory.path().join("harvestcircle.sqlite3"); + let path = directory + .path() + .canonicalize() + .expect("canonical temporary directory") + .join("harvestcircle.sqlite3"); let public_key = account().public_key(); let secrets = InMemorySecretStore::default(); { @@ -388,7 +392,11 @@ mod tests { #[test] fn corrupt_database_fails_safely_without_recreation() { let directory = tempdir().expect("directory"); - let path = directory.path().join("harvestcircle.sqlite3"); + let path = directory + .path() + .canonicalize() + .expect("canonical temporary directory") + .join("harvestcircle.sqlite3"); fs::write(&path, b"not a sqlite database").expect("corrupt file"); let error = PersistentAppCore::open(&path, RelayConfiguration::default()) @@ -404,7 +412,11 @@ mod tests { #[test] fn persisted_generate_and_import_survive_restart_without_secret_bytes() { let directory = tempdir().expect("directory"); - let path = directory.path().join("harvestcircle.sqlite3"); + let path = directory + .path() + .canonicalize() + .expect("canonical temporary directory") + .join("harvestcircle.sqlite3"); let secrets = InMemorySecretStore::default(); let selected; { @@ -646,7 +658,11 @@ mod tests { #[test] fn bootstrap_recovery_completes_credential_deleted_removal_and_fallback() { let directory = tempdir().expect("directory"); - let path = directory.path().join("harvestcircle.sqlite3"); + let path = directory + .path() + .canonicalize() + .expect("canonical temporary directory") + .join("harvestcircle.sqlite3"); let secrets = InMemorySecretStore::default(); let first; let removed; diff --git a/core/crates/harvestcircle_runtime/src/runtime_actor.rs b/core/crates/harvestcircle_runtime/src/runtime_actor.rs @@ -1584,7 +1584,11 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn installation_identity_survives_file_backed_runtime_restart() { let directory = tempfile::tempdir().expect("temporary directory"); - let path = directory.path().join("harvestcircle.sqlite3"); + let path = directory + .path() + .canonicalize() + .expect("canonical temporary directory") + .join("harvestcircle.sqlite3"); let first = RuntimeActorHandle::open( &path, RelayConfiguration::default(), diff --git a/core/crates/harvestcircle_runtime/tests/restart_isolation.rs b/core/crates/harvestcircle_runtime/tests/restart_isolation.rs @@ -22,7 +22,11 @@ impl Clock for FixedClock { #[test] fn restart_restores_selection_and_keeps_account_namespaces_isolated() { let directory = tempdir().expect("temporary directory"); - let path = directory.path().join("harvestcircle.sqlite3"); + let path = directory + .path() + .canonicalize() + .expect("canonical temporary directory") + .join("harvestcircle.sqlite3"); let secrets = InMemorySecretStore::default(); let (owner_a, owner_b); diff --git a/core/crates/harvestcircle_storage/src/accounts.rs b/core/crates/harvestcircle_storage/src/accounts.rs @@ -305,10 +305,14 @@ mod tests { AccountCreatedAt, AccountIdentity, AccountLabel, AccountSummary, BindingAvailability, LocalSignerBinding, PublicKey, SafeErrorCode, UnixTimestamp, }; - use tempfile::tempdir; + use tempfile::{TempDir, tempdir_in}; use crate::Database; + fn tempdir() -> std::io::Result<TempDir> { + tempdir_in(std::env::temp_dir().canonicalize()?) + } + fn public_key(key_byte: u8) -> PublicKey { let value = match key_byte { 1 => "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df", diff --git a/core/crates/harvestcircle_storage/src/compatibility.rs b/core/crates/harvestcircle_storage/src/compatibility.rs @@ -332,7 +332,7 @@ pub(crate) const fn quarantined_storage_error() -> SafeError { #[cfg(test)] mod tests { use rusqlite::{Connection, params}; - use tempfile::tempdir; + use tempfile::{TempDir, tempdir_in}; use super::{ DatabasePreflight, PersistedIdentityIssueKind, column_exists, preflight, @@ -341,6 +341,10 @@ mod tests { use crate::Database; use harvestcircle_domain::{AccountIdentity, PublicKey, SafeErrorCode}; + fn tempdir() -> std::io::Result<TempDir> { + tempdir_in(std::env::temp_dir().canonicalize()?) + } + #[test] fn preflight_rejects_non_files_missing_schema_zero_version_and_unknown_tables() { let directory = tempdir().expect("temporary directory"); diff --git a/core/crates/harvestcircle_storage/src/db.rs b/core/crates/harvestcircle_storage/src/db.rs @@ -377,7 +377,7 @@ mod tests { use std::path::Path; use std::process::Command; - use tempfile::tempdir; + use tempfile::{TempDir, tempdir_in}; use harvestcircle_application::{AccountRepository, AppStateRepository}; use harvestcircle_domain::{PublicKey, SafeErrorCode}; @@ -388,6 +388,10 @@ mod tests { CURRENT_SCHEMA_VERSION, Database, configure, create_secure_directory, migrations, restrict_sqlite_sidecars, }; + + fn tempdir() -> std::io::Result<TempDir> { + tempdir_in(std::env::temp_dir().canonicalize()?) + } use crate::{DatabasePreflight, PersistedIdentityIssueKind, RepairAuthorization}; #[test] diff --git a/core/crates/harvestcircle_storage/src/recovery.rs b/core/crates/harvestcircle_storage/src/recovery.rs @@ -518,7 +518,7 @@ mod tests { use std::path::Path; use rusqlite::Connection; - use tempfile::tempdir; + use tempfile::{TempDir, tempdir_in}; use super::{ AUTHENTICATION_KEY_FILENAME, MANIFEST_FORMAT, MigrationRecovery, atomic_secure_write, @@ -527,6 +527,10 @@ mod tests { recovery_directory, replace_with_backup, secure_read, }; + fn tempdir() -> std::io::Result<TempDir> { + tempdir_in(std::env::temp_dir().canonicalize()?) + } + fn sqlite_database(path: &Path) { let connection = Connection::open(path).expect("open sqlite database"); connection diff --git a/core/crates/harvestcircle_storage/src/repair.rs b/core/crates/harvestcircle_storage/src/repair.rs @@ -265,7 +265,7 @@ mod tests { use std::io::Write; use rusqlite::Connection; - use tempfile::tempdir; + use tempfile::{TempDir, tempdir_in}; use super::{ REPAIR_DOMAIN, RepairAuthorization, RepairCandidate, authenticate, authenticate_candidate, @@ -274,6 +274,10 @@ mod tests { }; use crate::Database; + fn tempdir() -> std::io::Result<TempDir> { + tempdir_in(std::env::temp_dir().canonicalize()?) + } + fn quarantined_database(path: &std::path::Path) { drop(Database::open(path).expect("current database")); let connection = Connection::open(path).expect("open database"); diff --git a/core/crates/harvestcircle_storage/tests/redaction.rs b/core/crates/harvestcircle_storage/tests/redaction.rs @@ -6,7 +6,11 @@ use harvestcircle_domain::{ PublicKey, UnixTimestamp, }; use harvestcircle_storage::Database; -use tempfile::tempdir; +use tempfile::{TempDir, tempdir_in}; + +fn tempdir() -> std::io::Result<TempDir> { + tempdir_in(std::env::temp_dir().canonicalize()?) +} const SECRET_HEX: &str = "1111111111111111111111111111111111111111111111111111111111111111"; const SECRET_NSEC: &str = "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5";