app

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

commit 5186bd45d9a368a8ae28c76d36bab45569325a26
parent dc8d3c8995f5665f0ed58b63328f46271babbf2f
Author: triesap <tyson@radroots.org>
Date:   Mon, 10 Aug 2026 19:22:55 +0000

ci: return workflow authority to local orchestration

- remove GitHub workflow definitions from the standalone capsule
- make portable Make targets the verification-lane authority
- fail closed on every tracked workflow root inside the OSS repository
- align public security and architecture contracts with local execution

Diffstat:
D.github/workflows/package.yml | 57---------------------------------------------------------
D.github/workflows/source.yml | 46----------------------------------------------
MAGENTS.md | 21+++++++++++----------
Mapp/desktop/src/test/kotlin/org/harvestcircle/architecture/ProductNamespaceGuardTest.kt | 3---
Mbuild.gradle.kts | 2+-
MbuildSrc/src/main/kotlin/org/harvestcircle/gradle/FoundationBoundaryAudit.kt | 11+----------
MbuildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt | 67+++++++++++++++----------------------------------------------------
Dconfig/verification/lanes-v1.properties | 24------------------------
Aconfig/verification/lanes-v2.properties | 18++++++++++++++++++
Mdocs/decisions/ADR-0008-public-specs-and-ci.md | 13+++++++++----
Mspec/harvestcircle_mvp_v1/ACCEPTANCE_CRITERIA.md | 2+-
Mspec/harvestcircle_mvp_v1/SECURITY_AND_PRIVACY.md | 2+-
12 files changed, 57 insertions(+), 209 deletions(-)

diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml @@ -1,57 +0,0 @@ -name: Package verification - -on: - pull_request: - push: - branches: - - dev - workflow_dispatch: - -permissions: - contents: read - -jobs: - package: - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - - macos-latest - - windows-latest - runs-on: ${{ matrix.os }} - steps: - - name: Check out source - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - fetch-depth: 0 - persist-credentials: false - - name: Install Java 21 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 - with: - distribution: temurin - java-version: "21" - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 - with: - toolchain: 1.97.1 - components: clippy,rustfmt - - name: Install cargo-deny - uses: taiki-e/install-action@7f4eb899022d8fe70b20c4f3de697aa85c309026 # v2 - with: - tool: cargo-deny - - name: Install Windows package tools - if: runner.os == 'Windows' - shell: pwsh - run: choco install make wixtoolset --no-progress --yes - - name: Export source provenance - shell: bash - run: | - radroots_revision=$(sed -n 's/^canonical_radroots_revision = "\([0-9a-f]\{40\}\)"$/\1/p' core/provenance/studio-import-v1.toml) - test ${#radroots_revision} -eq 40 - echo "HARVESTCIRCLE_BUILD_SOURCE_COMMIT=$GITHUB_SHA" >> "$GITHUB_ENV" - echo "HARVESTCIRCLE_BUILD_SOURCE_DIRTY=false" >> "$GITHUB_ENV" - echo "HARVESTCIRCLE_BUILD_RADROOTS_REVISION=$radroots_revision" >> "$GITHUB_ENV" - echo "SOURCE_DATE_EPOCH=$(git show -s --format=%ct "$GITHUB_SHA")" >> "$GITHUB_ENV" - - name: Verify host package - run: make package-check diff --git a/.github/workflows/source.yml b/.github/workflows/source.yml @@ -1,46 +0,0 @@ -name: Source verification - -on: - pull_request: - push: - branches: - - dev - workflow_dispatch: - -permissions: - contents: read - -jobs: - source: - runs-on: ubuntu-latest - steps: - - name: Check out source - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - fetch-depth: 0 - persist-credentials: false - - name: Install Java 21 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 - with: - distribution: temurin - java-version: "21" - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 - with: - toolchain: 1.97.1 - components: clippy,rustfmt - - name: Install cargo-deny - uses: taiki-e/install-action@7f4eb899022d8fe70b20c4f3de697aa85c309026 # v2 - with: - tool: cargo-deny - - name: Export source provenance - shell: bash - run: | - radroots_revision=$(sed -n 's/^canonical_radroots_revision = "\([0-9a-f]\{40\}\)"$/\1/p' core/provenance/studio-import-v1.toml) - test ${#radroots_revision} -eq 40 - echo "HARVESTCIRCLE_BUILD_SOURCE_COMMIT=$GITHUB_SHA" >> "$GITHUB_ENV" - echo "HARVESTCIRCLE_BUILD_SOURCE_DIRTY=false" >> "$GITHUB_ENV" - echo "HARVESTCIRCLE_BUILD_RADROOTS_REVISION=$radroots_revision" >> "$GITHUB_ENV" - echo "SOURCE_DATE_EPOCH=$(git show -s --format=%ct "$GITHUB_SHA")" >> "$GITHUB_ENV" - - name: Verify source - run: make source-check diff --git a/AGENTS.md b/AGENTS.md @@ -58,16 +58,17 @@ local artifacts, absolute host paths, or an enclosing monorepo layout. Local product crates are workspace path dependencies; shared Radroots packages remain immutable public Git dependencies. - 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`, + contributor and security guidance, and qualification evidence. 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. + and `docs/qualification/**`. 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/**` and `spec/**` paths are forbidden. All + `.github/**` and `.act/**` paths are forbidden; local workflow orchestration + belongs to the consuming monorepo's governed `.act/**` surface and must call + this capsule's standalone Make targets. Generated UniFFI Kotlin and native libraries are derived build output. Change the local canonical Rust producer contract/generator first, regenerate into @@ -128,8 +129,8 @@ blockers exactly. Verify exact manifests/lock agreement, generated binding and native artifact freshness when affected, compatibility and architecture guards, license/source policy, zero forbidden roots, `git diff --check`, and final -status and diff. Do not treat parent-only workflow proof as a substitute for -standalone repository validation. +status and diff. Parent local-`act` proof is integration evidence only: it must +execute, and never replace, the standalone repository validation commands. ## Git and external gates diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/architecture/ProductNamespaceGuardTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/architecture/ProductNamespaceGuardTest.kt @@ -127,9 +127,6 @@ class ProductNamespaceGuardTest { if (relative == "spec/harvestcircle_mvp_v1/UI_SURFACE_MAP.md") { inspected = inspected.replace("round_${legacyProduct}_screen", "") } - if (relative == ".github/workflows/source.yml" || relative == ".github/workflows/package.yml") { - inspected = inspected.replace(provenanceException, "") - } if (inspected.lowercase().contains(legacyProduct)) { add("$relative: legacy product name in tracked text") } diff --git a/build.gradle.kts b/build.gradle.kts @@ -14,7 +14,7 @@ plugins { val productCoordinatesFile = layout.projectDirectory.file("config/product/harvestcircle-v1.properties") val ffiCompatibilityBaselineFile = layout.projectDirectory.file("core/compatibility/harvestcircle-ffi-v4.properties") -val verificationLanesFile = layout.projectDirectory.file("config/verification/lanes-v1.properties") +val verificationLanesFile = layout.projectDirectory.file("config/verification/lanes-v2.properties") val legacyProduct = "stu" + "dio" val verifyProductCoordinates by tasks.registering(VerifyProductCoordinates::class) { diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/FoundationBoundaryAudit.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/FoundationBoundaryAudit.kt @@ -192,9 +192,6 @@ private class FoundationBoundaryAudit( if (relative == "spec/harvestcircle_mvp_v1/UI_SURFACE_MAP.md") { inspected = inspected.replace("round_${legacyProduct}_screen", "") } - if (relative == ".github/workflows/source.yml" || relative == ".github/workflows/package.yml") { - inspected = inspected.replace("core/provenance/$legacyProduct-import-v1.toml", "") - } if (inspected.lowercase().contains(legacyProduct)) { findings += "$relative: legacy product name outside the exact provenance allowlist" } @@ -254,8 +251,6 @@ private class FoundationBoundaryAudit( "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", - ".github/workflows/source.yml", - ".github/workflows/package.yml", ) (requiredPublicFiles - inventory.toSet()).sorted().forEach { relative -> findings += "$relative: required public repository file is missing" @@ -320,14 +315,10 @@ private class FoundationBoundaryAudit( "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" + normalized.startsWith("docs/qualification/") private fun isText(relative: String): Boolean { val path = Path.of(relative) diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt @@ -12,17 +12,13 @@ import org.gradle.api.tasks.TaskAction object VerificationLanes { private fun expected(environmentPrefix: String) = linkedMapOf( - "schema" to "harvestcircle.verification-lanes.v1", - "orchestration" to "github-actions", + "schema" to "harvestcircle.verification-lanes.v2", + "orchestration" to "standalone-make", "source.command" to "make source-check", - "source.runner" to "linux", - "source.workflow" to ".github/workflows/source.yml", - "source.permissions" to "contents:read", + "source.runner" to "host", "source.credentials" to "none", "package.command" to "make package-check", "package.runners" to "linux,macos,windows", - "package.workflow" to ".github/workflows/package.yml", - "package.permissions" to "contents:read", "package.credentials" to "none", "provenance.commit" to environmentPrefix + "BUILD_SOURCE_COMMIT", "provenance.dirty" to environmentPrefix + "BUILD_SOURCE_DIRTY", @@ -30,11 +26,9 @@ object VerificationLanes { "provenance.epoch" to "SOURCE_DATE_EPOCH", "signing.command" to "make signing-check", "signing.runner" to "macos", - "signing.permissions" to "contents:read", "signing.credentials" to "signing", "notarization.command" to "make notarization-check", "notarization.runner" to "macos", - "notarization.permissions" to "contents:read", "notarization.credentials" to "notarization", ) @@ -81,13 +75,8 @@ abstract class VerifyVerificationLanes : DefaultTask() { val environmentPrefix = ProductCoordinates.load(productManifestFile.get().asFile)["environment.prefix"] val policy = VerificationLanes.parse(source, environmentPrefix) - check(policy.size == 24) - check(runCatching { VerificationLanes.parse(source + "source.permissions=write", environmentPrefix) }.isFailure) - check( - runCatching { - VerificationLanes.parse(source.replace("contents:read", "contents:write"), environmentPrefix) - }.isFailure, - ) + check(policy.size == 18) + check(runCatching { VerificationLanes.parse(source + "source.workflow=forbidden", environmentPrefix) }.isFailure) check( runCatching { VerificationLanes.parse(source.replace("credentials=none", "credentials=all"), environmentPrefix) @@ -95,46 +84,20 @@ abstract class VerifyVerificationLanes : DefaultTask() { ) check( runCatching { - VerificationLanes.parse(source.replace("source.runner=linux", "source.runner=macos"), environmentPrefix) + VerificationLanes.parse(source.replace("source.runner=host", "source.runner=remote"), environmentPrefix) }.isFailure, ) val root = repositoryRoot.get().asFile.toPath() - val sourceWorkflow = root.resolve(policy.getValue("source.workflow")).toFile().readText() - val packageWorkflow = root.resolve(policy.getValue("package.workflow")).toFile().readText() - verifyWorkflow(sourceWorkflow, policy.getValue("source.command")) - verifyWorkflow(packageWorkflow, policy.getValue("package.command")) - check(sourceWorkflow.contains("runs-on: ubuntu-latest")) - listOf("ubuntu-latest", "macos-latest", "windows-latest").forEach { runner -> - check(packageWorkflow.contains("- $runner")) { "Package workflow is missing $runner" } - } - listOf( - policy.getValue("provenance.commit"), - policy.getValue("provenance.dirty"), - policy.getValue("provenance.radroots"), - policy.getValue("provenance.epoch"), - ).forEach { variable -> - check(sourceWorkflow.contains(variable)) { "Source workflow is missing provenance variable $variable" } - check(packageWorkflow.contains(variable)) { "Package workflow is missing provenance variable $variable" } - } - } - - private fun verifyWorkflow( - source: String, - command: String, - ) { - check(Regex("(?m)^permissions:\\s*\\n\\s{2}contents: read$").containsMatchIn(source)) { - "Workflow permissions must be contents: read" - } - check(source.contains("run: $command")) { "Workflow does not invoke $command" } - check(source.contains("persist-credentials: false")) { "Checkout credentials must not persist" } - val actionPins = Regex("(?m)^\\s*uses:\\s+[^@\\s]+@([0-9a-f]{40})(?:\\s+#.*)?$").findAll(source).toList() - check(actionPins.isNotEmpty()) { "Workflow does not use any pinned actions" } - check(source.lineSequence().filter { "uses:" in it }.count() == actionPins.size) { - "Every workflow action must use a full immutable commit SHA" + val makefile = root.resolve("Makefile").toFile().readText() + listOf("source.command", "package.command", "signing.command", "notarization.command").forEach { key -> + val command = policy.getValue(key) + val target = command.removePrefix("make ") + check(command == "make $target" && Regex("(?m)^${Regex.escape(target)}:").containsMatchIn(makefile)) { + "Verification lane $key does not name a standalone Make target" + } } - val forbidden = listOf("contents: write", "id-token: write", "pull-requests: write", "secrets.", "publish", "deploy") - forbidden.forEach { token -> - check(!source.contains(token, ignoreCase = true)) { "Workflow contains forbidden capability $token" } + check(policy.values.none { ".github/" in it || ".act/" in it }) { + "Standalone verification policy must not reference an orchestration root" } } } diff --git a/config/verification/lanes-v1.properties b/config/verification/lanes-v1.properties @@ -1,24 +0,0 @@ -schema=harvestcircle.verification-lanes.v1 -orchestration=github-actions -source.command=make source-check -source.runner=linux -source.workflow=.github/workflows/source.yml -source.permissions=contents:read -source.credentials=none -package.command=make package-check -package.runners=linux,macos,windows -package.workflow=.github/workflows/package.yml -package.permissions=contents:read -package.credentials=none -provenance.commit=HARVESTCIRCLE_BUILD_SOURCE_COMMIT -provenance.dirty=HARVESTCIRCLE_BUILD_SOURCE_DIRTY -provenance.radroots=HARVESTCIRCLE_BUILD_RADROOTS_REVISION -provenance.epoch=SOURCE_DATE_EPOCH -signing.command=make signing-check -signing.runner=macos -signing.permissions=contents:read -signing.credentials=signing -notarization.command=make notarization-check -notarization.runner=macos -notarization.permissions=contents:read -notarization.credentials=notarization diff --git a/config/verification/lanes-v2.properties b/config/verification/lanes-v2.properties @@ -0,0 +1,18 @@ +schema=harvestcircle.verification-lanes.v2 +orchestration=standalone-make +source.command=make source-check +source.runner=host +source.credentials=none +package.command=make package-check +package.runners=linux,macos,windows +package.credentials=none +provenance.commit=HARVESTCIRCLE_BUILD_SOURCE_COMMIT +provenance.dirty=HARVESTCIRCLE_BUILD_SOURCE_DIRTY +provenance.radroots=HARVESTCIRCLE_BUILD_RADROOTS_REVISION +provenance.epoch=SOURCE_DATE_EPOCH +signing.command=make signing-check +signing.runner=macos +signing.credentials=signing +notarization.command=make notarization-check +notarization.runner=macos +notarization.credentials=notarization diff --git a/docs/decisions/ADR-0008-public-specs-and-ci.md b/docs/decisions/ADR-0008-public-specs-and-ci.md @@ -1,11 +1,16 @@ -# ADR-0008: Public specs and CI are repository requirements +# ADR-0008: Public specs and local verification 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. +Durable specs, decisions, and qualification evidence belong in the repository. +The standalone capsule owns portable Make, Gradle, and Cargo verification +commands, but it does not own workflow definitions under `.github/**` or +`.act/**`. -Make/Gradle/Cargo remain the implementation authority. CI invokes them. +The consuming monorepo may invoke those commands through governed local-only +workflows under its root `.act/**` surface. Those workflows add orchestration, +not build behavior, and are not a substitute for running the standalone +commands directly. diff --git a/spec/harvestcircle_mvp_v1/ACCEPTANCE_CRITERIA.md b/spec/harvestcircle_mvp_v1/ACCEPTANCE_CRITERIA.md @@ -2,7 +2,7 @@ The foundation is complete when: -- public specs/governance and CI exist; +- public specs/governance and portable standalone verification exist; - product/provenance digests are canonical; - coordinate values have one authority; - Kotlin compatibility expectations are generated; diff --git a/spec/harvestcircle_mvp_v1/SECURITY_AND_PRIVACY.md b/spec/harvestcircle_mvp_v1/SECURITY_AND_PRIVACY.md @@ -10,4 +10,4 @@ - no silent provider fallback; - no private event through public sinks; - unknown protocol versions fail closed; -- public CI uses least privilege. +- local workflow orchestration is parent-owned, credential-free, and fail-closed.