commit d0d70bceec6704386de3983c553c897794421ad0 parent 1f8e5728f4815961d7dac0545c2b03464991b592 Author: triesap <tyson@radroots.org> Date: Mon, 10 Aug 2026 16:37:43 +0000 repo: publish the HarvestCircle specifications and governance - add the approved public product and contributor documentation - record the accepted public repository and foundation decisions - permit only enumerated public documentation and workflow roots - audit public sources for symlinks, secrets, and generated output Diffstat:
16 files changed, 432 insertions(+), 10 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md @@ -57,10 +57,17 @@ local artifacts, absolute host paths, or an enclosing monorepo layout. implicit sibling override, dirty source cache, or unrecorded native binary. Local product crates are workspace path dependencies; shared Radroots packages remain immutable public Git dependencies. -- Human specifications, decisions, runbooks, migration history, qualification - records, and execution evidence are parent-owned and absent from standalone - clones. Physical or tracked `docs/**`, `.github/**`, and `.act/**` roots are - forbidden, including symlinks. Public commands remain forge agnostic. +- The repository tracks durable public product specifications, decisions, + contributor and security guidance, qualification evidence, and thin CI + wrappers. Approved public surfaces are `README.md`, `NOTICE`, + `CONTRIBUTING.md`, `SECURITY.md`, `LICENSE`, `LICENSES/**`, + `spec/harvestcircle_mvp_v1/**`, `docs/decisions/**`, + `docs/qualification/**`, and `.github/workflows/{source,package}.yml`. + These roots are inspected by the same namespace, secret, generated-output, + credential, and symlink rules as source code. Internal handoffs, RCLDs, + migration narratives, and execution records remain parent-owned. Other + `docs/**`, `spec/**`, `.github/**`, and all `.act/**` paths are forbidden. + CI must remain a thin wrapper around repository-owned Make targets. Generated UniFFI Kotlin and native libraries are derived build output. Change the local canonical Rust producer contract/generator first, regenerate into diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing + +## Start with the specification + +Read: + +```text +spec/harvestcircle_mvp_v1/ +AGENTS.md +``` + +before changing product behaviour or architecture. + +## Development flow + +1. Use the current development branch policy. +2. Make one coherent change at a time. +3. Add or update tests. +4. Run focused checks. +5. Run: + +```sh +make format +make lint +make test +make check +``` + +6. Do not hand-edit generated UniFFI code. +7. Do not include secrets or private event plaintext. +8. Document intentional architecture deviations. + +## Commit style + +```text +<scope>: <imperative summary> +``` + +## Public contracts + +Changes to FFI, product coordinates, storage migrations, or future Radroots +event contracts require explicit compatibility review. diff --git a/NOTICE b/NOTICE @@ -0,0 +1,16 @@ +HarvestCircle + +Copyright © 2026 HarvestCircle contributors. + +This repository was derived from Radroots Studio application work and imports +product-specific Rust foundations whose provenance is recorded in: + +```text +core/provenance/studio-import-v1.toml +``` + +Canonical reusable Radroots dependencies remain sourced from the public +`radrootslabs/lib` repository at exact immutable revisions. + +Third-party licence notices are governed by the checked-in dependency and +licence verification configuration. diff --git a/README.md b/README.md @@ -0,0 +1,59 @@ +# HarvestCircle + +HarvestCircle is an open-source Nostr application for coordinating local-food +buying circles. + +The project is in early desktop development. It is not ready for real +commercial use. + +## Current foundation + +- Kotlin Multiplatform shared application code; +- Compose Desktop host; +- product-specific Rust core through UniFFI; +- local Nostr identity creation and import; +- operating-system keyring custody; +- configurable Nostr relay bootstrap; +- compatibility-gated native startup; +- reproducible source and package verification. + +## Sovereign direction + +The MVP is designed to work without a managed HarvestCircle account or API. + +Future work adds canonical Radroots collective-market contracts, private buyer +commitments, a selectable open reference authority, pickup, and proof. + +## Build + +Prerequisites include JDK 21, Rust 1.97.1, and platform packaging tools. + +```sh +make doctor +make check +make build +make package +``` + +## Development branch + +Active implementation currently proceeds on `dev`. + +## Specifications + +The durable product contract is under: + +```text +spec/harvestcircle_mvp_v1/ +``` + +## Security + +Do not submit secret keys, nsec values, signer secrets, or decrypted private +contracts in issues or logs. + +See `SECURITY.md`. + +## Licence + +HarvestCircle is licensed under GPL-3.0-only. See `LICENSE` and `LICENSES/`. diff --git a/SECURITY.md b/SECURITY.md @@ -0,0 +1,35 @@ +# Security policy + +## Reporting + +Use GitHub private vulnerability reporting when it is available. + +Do not publish secret keys, nsec values, NIP-46 secrets, decrypted private +contracts, or exploitable operational details in a public issue. + +## Scope + +Security-sensitive areas include: + +- key generation and recovery; +- operating-system keyring custody; +- FFI compatibility; +- native library loading; +- database migrations and recovery; +- operation replay; +- relay transport; +- private Nostr events; +- package provenance; +- dependency policy. + +## Expectations + +Reports should include: + +- affected commit; +- affected platform; +- reproduction steps that do not expose real secrets; +- expected and observed behaviour; +- impact. + +The project makes no production-readiness claim during the alpha phase. diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/FoundationBoundaryAudit.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/FoundationBoundaryAudit.kt @@ -69,7 +69,7 @@ abstract class VerifyFoundationBoundaries : DefaultTask() { ) { val fixtures = listOf( - ".github/workflows/source.yml" to "name: source", + ".github/ISSUE_TEMPLATE/bug.md" to "# Bug report", "app/shared/src/commonMain/kotlin/org/harvestcircle/application/Leak.kt" to ("import org.harvestcircle." + "ffi.BuildInfoDto"), "app/desktop/src/main/kotlin/org/harvestcircle/desktop/Blocking.kt" to @@ -86,6 +86,17 @@ abstract class VerifyFoundationBoundaries : DefaultTask() { }.isFailure, ) { "Foundation audit accepted negative fixture $path" } } + val symlinkPath = "spec/harvestcircle_mvp_v1/escape.md" + check( + runCatching { + FoundationBoundaryAudit( + root, + paths + symlinkPath, + overrides = mapOf(symlinkPath to "outside"), + symbolicLinks = setOf(symlinkPath), + ).verify() + }.isFailure, + ) { "Foundation audit accepted symlink fixture $symlinkPath" } val provenancePath = "core/provenance/" + "stu" + "dio-import-v1.toml" val altered = root.resolve(provenancePath).readText().replace("09065a610d95e57acdc895a14c07580fa099e7c3", "0".repeat(40)) check( @@ -100,6 +111,7 @@ private class FoundationBoundaryAudit( private val root: Path, paths: List<String>, private val overrides: Map<String, String> = emptyMap(), + private val symbolicLinks: Set<String> = emptySet(), ) { private val inventory = paths.distinct().sorted() private val legacyProduct = "stu" + "dio" @@ -107,8 +119,9 @@ private class FoundationBoundaryAudit( private val legacyRepository = "https://github.com/radrootslabs/${legacyProduct}_app" private val temporaryNamespace = listOf("org", "radroots", "harvestcircle").joinToString(".") private val textExtensions = - setOf("gradle", "json", "kt", "kts", "lock", "md", "properties", "rs", "sql", "toml", "xml", "yaml", "yml") - private val textNames = setOf(".gitattributes", ".gitignore", "AGENTS.md", "Makefile", "gradlew", "gradlew.bat") + setOf("gradle", "json", "kt", "kts", "lock", "md", "properties", "rs", "sql", "toml", "txt", "xml", "yaml", "yml") + private val textNames = + setOf(".gitattributes", ".gitignore", "AGENTS.md", "LICENSE", "Makefile", "NOTICE", "gradlew", "gradlew.bat") fun verify() { val findings = mutableListOf<String>() @@ -128,11 +141,15 @@ private class FoundationBoundaryAudit( findings: MutableList<String>, ) { val normalized = relative.lowercase() - if (normalized.startsWith("docs/") || normalized.startsWith("spec/") || - normalized.startsWith(".github/") || normalized.startsWith(".act/") + if ((normalized.startsWith("docs/") || normalized.startsWith("spec/") || + normalized.startsWith(".github/") || normalized.startsWith(".act/")) && + !isApprovedPublicPath(normalized) ) { findings += "$relative: forbidden repository root" } + if (relative in symbolicLinks || Files.isSymbolicLink(root.resolve(relative))) { + findings += "$relative: symbolic links are not allowed in public sources" + } if (normalized.startsWith("core/target/") || normalized.contains("/build/") || normalized.contains("generated/uniffi") || normalized.endsWith(".dylib") || normalized.endsWith(".so") || normalized.endsWith(".dll") || normalized.endsWith(".class") @@ -163,7 +180,17 @@ private class FoundationBoundaryAudit( findings: MutableList<String>, ) { if (relative != provenancePath) { - val inspected = if (relative == "core/Cargo.toml") source.replace(legacyRepository, "") else source + var inspected = if (relative == "core/Cargo.toml") source.replace(legacyRepository, "") else source + if (relative == "NOTICE") { + val legacyDisplayName = legacyProduct.replaceFirstChar { it.uppercase() } + inspected = + inspected + .replace("Radroots $legacyDisplayName application work", "") + .replace("core/provenance/$legacyProduct-import-v1.toml", "") + } + if (relative == "spec/harvestcircle_mvp_v1/UI_SURFACE_MAP.md") { + inspected = inspected.replace("round_${legacyProduct}_screen", "") + } if (inspected.lowercase().contains(legacyProduct)) { findings += "$relative: legacy product name outside the exact provenance allowlist" } @@ -205,6 +232,28 @@ private class FoundationBoundaryAudit( } private fun verifyExactContracts(findings: MutableList<String>) { + val requiredPublicFiles = + setOf( + "README.md", + "NOTICE", + "CONTRIBUTING.md", + "SECURITY.md", + "LICENSE", + "LICENSES/GPL-3.0-only.txt", + "spec/harvestcircle_mvp_v1/PRODUCT_SPEC.md", + "spec/harvestcircle_mvp_v1/ARCHITECTURE.md", + "spec/harvestcircle_mvp_v1/IDENTITY_AND_BOOTSTRAP.md", + "spec/harvestcircle_mvp_v1/UI_SURFACE_MAP.md", + "spec/harvestcircle_mvp_v1/UI_COPY_CONTRACT.md", + "spec/harvestcircle_mvp_v1/SECURITY_AND_PRIVACY.md", + "spec/harvestcircle_mvp_v1/ACCEPTANCE_CRITERIA.md", + "docs/decisions/ADR-0008-public-specs-and-ci.md", + "docs/decisions/ADR-0009-canonical-manifest-digests.md", + "docs/decisions/ADR-0010-gap-aware-snapshot-delivery.md", + ) + (requiredPublicFiles - inventory.toSet()).sorted().forEach { relative -> + findings += "$relative: required public repository file is missing" + } val cargo = text("core/Cargo.toml") if (cargo.lineSequence().count { it.trim() == "repository = \"$legacyRepository\"" } != 1) { findings += "core/Cargo.toml: legacy repository allowlist must be exact" @@ -249,6 +298,23 @@ private class FoundationBoundaryAudit( ) } + private fun isApprovedPublicPath(normalized: String): Boolean = + normalized in + setOf( + "spec", + "spec/harvestcircle_mvp_v1", + "docs", + "docs/decisions", + "docs/qualification", + ".github", + ".github/workflows", + ) || + normalized.startsWith("spec/harvestcircle_mvp_v1/") || + normalized.startsWith("docs/decisions/") || + normalized.startsWith("docs/qualification/") || + normalized == ".github/workflows/source.yml" || + normalized == ".github/workflows/package.yml" + private fun isText(relative: String): Boolean { val path = Path.of(relative) return path.extension in textExtensions || path.name in textNames diff --git a/docs/decisions/ADR-0008-public-specs-and-ci.md b/docs/decisions/ADR-0008-public-specs-and-ci.md @@ -0,0 +1,11 @@ +# ADR-0008: Public specs and CI are repository requirements + +Status: Accepted + +HarvestCircle is an open-source, spec-anchored project intended for public +review and an OpenSats application. + +Durable specs, decisions, qualification evidence, and CI wrappers belong in +the repository. + +Make/Gradle/Cargo remain the implementation authority. CI invokes them. diff --git a/docs/decisions/ADR-0009-canonical-manifest-digests.md b/docs/decisions/ADR-0009-canonical-manifest-digests.md @@ -0,0 +1,10 @@ +# ADR-0009: Manifest digests use canonical semantic serialization + +Status: Accepted + +Raw checkout bytes are not portable across line-ending policies. + +Product and provenance digests are computed from parsed and canonicalized +content. + +LF policy remains a defense in depth, not the digest authority. diff --git a/docs/decisions/ADR-0010-gap-aware-snapshot-delivery.md b/docs/decisions/ADR-0010-gap-aware-snapshot-delivery.md @@ -0,0 +1,9 @@ +# ADR-0010: Snapshot streams are conflated and gap-aware + +Status: Accepted + +Application changes contain complete snapshots. + +The transport may conflate intermediate values, but must preserve the latest +state, check delivery failure, validate predecessor revisions, and resnapshot +on gaps. diff --git a/spec/harvestcircle_mvp_v1/ACCEPTANCE_CRITERIA.md b/spec/harvestcircle_mvp_v1/ACCEPTANCE_CRITERIA.md @@ -0,0 +1,16 @@ +# Acceptance criteria + +The foundation is complete when: + +- public specs/governance and CI exist; +- product/provenance digests are canonical; +- coordinate values have one authority; +- Kotlin compatibility expectations are generated; +- change delivery is gap-aware; +- Git sources require rev; +- BuildInfo/readiness is complete; +- scopes close safely; +- signer-binding access is future-safe; +- relay destinations are explicit; +- architecture decisions are documented; +- complete qualification evidence is public. diff --git a/spec/harvestcircle_mvp_v1/ARCHITECTURE.md b/spec/harvestcircle_mvp_v1/ARCHITECTURE.md @@ -0,0 +1,22 @@ +# Architecture + +```text +app:shared + KMP common application models, presenters, shared Compose + ↓ platform-neutral runtime interface +app:desktop + desktop host, generated UniFFI adapter, JNA/AWT, packaging + ↓ +Rust HarvestCircle application/runtime/storage/Nostr/FFI crates + ↓ +canonical public Radroots libraries +``` + +Rust owns canonical identity, persistence, operation, Nostr, compatibility, +and future commercial protocol state. + +Kotlin shared code owns presentation state and platform-neutral use cases. + +Generated FFI types remain in the desktop adapter. + +No silent fallback, duplicated commercial model, or UI-thread blocking. diff --git a/spec/harvestcircle_mvp_v1/IDENTITY_AND_BOOTSTRAP.md b/spec/harvestcircle_mvp_v1/IDENTITY_AND_BOOTSTRAP.md @@ -0,0 +1,27 @@ +# Identity and bootstrap + +Preserve: + +- local Nostr identity generation; +- one-use recovery; +- acknowledgment before persistence; +- local secret import; +- OS-keyring custody; +- activation, switching, sign-out, removal, and repair. + +Identity and signer binding are separate. + +Current signer binding: + +```text +LocalKeyring +``` + +Future: + +```text +RemoteNip46 +ReadOnly +``` + +Future variants are not implemented during foundation completion. diff --git a/spec/harvestcircle_mvp_v1/PRODUCT_SPEC.md b/spec/harvestcircle_mvp_v1/PRODUCT_SPEC.md @@ -0,0 +1,29 @@ +# HarvestCircle MVP v1 + +HarvestCircle coordinates one local-food buying-circle round between a farm +and nearby buyers without requiring a central product marketplace. + +The eventual desktop MVP proves: + +- one signed farm produce-box round; +- private buyer maximum-price commitments; +- authority admission and deterministic clearing; +- private allocation; +- pay-at-pickup fulfilment; +- buyer acknowledgment; +- inspectable Nostr/Radroots evidence. + +Initial commercial constraints: + +- one farm; +- one standardized produce box; +- two price levels; +- one pickup window; +- CAD; +- one or two boxes per buyer; +- pay at pickup; +- farm-provided comparison; +- no custody, delivery, or multi-farm cart. + +The current foundation phase preserves local Nostr identity security and builds +the KMP/Rust platform required for that MVP. diff --git a/spec/harvestcircle_mvp_v1/SECURITY_AND_PRIVACY.md b/spec/harvestcircle_mvp_v1/SECURITY_AND_PRIVACY.md @@ -0,0 +1,13 @@ +# Security and privacy + +- no plaintext secret fallback; +- one-use generated recovery; +- bounded and cleared import buffers; +- no secret logs or diagnostics; +- compatibility before storage open; +- exact native artifact; +- UUIDv7 durable operation IDs; +- no silent provider fallback; +- no private event through public sinks; +- unknown protocol versions fail closed; +- public CI uses least privilege. diff --git a/spec/harvestcircle_mvp_v1/UI_COPY_CONTRACT.md b/spec/harvestcircle_mvp_v1/UI_COPY_CONTRACT.md @@ -0,0 +1,24 @@ +# UI copy contract + +Voice: + +- calm; +- factual; +- concise; +- sentence case; +- no exclamation marks; +- no hype or artificial urgency; +- explicit actor and state; +- explicit public/private visibility; +- explicit uncertainty. + +Use: + +- Nostr identity +- buying circle +- commitment admitted +- delivery incomplete +- farm-provided comparison +- inspect proof + +Do not claim independently verified savings in the MVP. diff --git a/spec/harvestcircle_mvp_v1/UI_SURFACE_MAP.md b/spec/harvestcircle_mvp_v1/UI_SURFACE_MAP.md @@ -0,0 +1,36 @@ +# UI surface reference + +The future desktop MVP uses: + +- dashboard layout with fixed sidebar/top bar/main header; +- canvas layout for focused workflows; +- nested views rather than one long scrolling dashboard page. + +Locked screen keys: + +```text +bootstrap_screen +signer_connection_screen +startup_recovery_screen +runtime_recovery_screen +protocol_compatibility_screen +signing_review_screen +proof_chain_screen +fulfillment_issue_screen +personal_today_screen +explore_screen +farm_profile_screen +circle_screen +activity_screen +commitment_screen +allocation_screen +farm_overview_screen +round_studio_screen +live_round_screen +pickup_desk_screen +round_outcome_screen +network_screen +settings_screen +``` + +Foundation completion does not implement these product screens.