app

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

commit b6ab1d4101763281e49762924d1f6b2ac8db2384
parent 1b3f190e3c463fb7eecf46aaf93727057f1ab057
Author: triesap <tyson@radroots.org>
Date:   Mon, 10 Aug 2026 03:47:57 +0000

ci: publish repository-owned verification lanes

- split source, package, signing, and notarization readiness behind Make and Gradle
- validate runner, command, permission, and credential isolation in machine policy
- keep external orchestration thin and forge agnostic under the forbidden-root invariant
- verify source, bindings, licenses, host packaging, and both integration lanes

Diffstat:
MMakefile | 16++++++++++++++--
Mapp/desktop/build.gradle.kts | 16++++++++++++++--
Mbuild.gradle.kts | 8++++++++
AbuildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig/verification/lanes-v1.properties | 18++++++++++++++++++
5 files changed, 122 insertions(+), 4 deletions(-)

diff --git a/Makefile b/Makefile @@ -5,10 +5,10 @@ CARGO ?= cargo CARGO_MANIFEST := core/Cargo.toml EXTBUILD ?= $(if $(shell cargo extbuild --version 2>/dev/null),cargo extbuild run --) -.PHONY: help doctor lock metadata format format-fix lint test check build bindings dev run audit licenses package release-check clean +.PHONY: help doctor lock metadata format format-fix lint test check build bindings dev run audit licenses package source-check package-check signing-check notarization-check release-check clean help: - @printf '%s\n' doctor lock metadata format format-fix lint test check build bindings dev run audit licenses package release-check clean + @printf '%s\n' doctor lock metadata format format-fix lint test check build bindings dev run audit licenses package source-check package-check signing-check notarization-check release-check clean doctor: $(if $(strip $(EXTBUILD)),cargo extbuild doctor,@:) @@ -66,6 +66,18 @@ licenses: doctor package: check $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:verifyHostPackage +source-check: check bindings licenses + $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:sourceReadiness + +package-check: source-check + $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:packageReadiness + +signing-check: doctor + $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:signingReadiness + +notarization-check: doctor + $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:notarizationReadiness + release-check: doctor $(EXTBUILD) $(CARGO) audit --file core/Cargo.lock $(EXTBUILD) $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml advisories licenses sources diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts @@ -895,9 +895,21 @@ val verifyReleaseBuildProvenance by tasks.registering(VerifyReleaseBuildProvenan radrootsRevision.set(buildRadrootsRevision) sourceDateEpoch.set(buildSourceDateEpoch) } +val sourceReadiness by tasks.registering { + dependsOn(":verifyProductCoordinates", ":verifyVerificationLanes", ":app:shared:check", "check", verifyUniFfiBindings) +} +val packageReadiness by tasks.registering { + dependsOn(verifyHostPackage, verifyReleaseBuildProvenance) +} +val signingReadiness by tasks.registering { + dependsOn(verifyMacOsDeveloperIdSignature) +} +val notarizationReadiness by tasks.registering { + dependsOn(verifyMacOsNotarization) +} tasks.register("releaseReadiness") { - dependsOn("checkLicense", "dependencyCheckAnalyze", verifyHostPackage, verifyReleaseBuildProvenance) + dependsOn("checkLicense", "dependencyCheckAnalyze", sourceReadiness, packageReadiness) if (isMacOsHost) { - dependsOn(verifyMacOsDeveloperIdSignature, verifyMacOsNotarization) + dependsOn(signingReadiness, notarizationReadiness) } } diff --git a/build.gradle.kts b/build.gradle.kts @@ -1,4 +1,5 @@ import org.harvestcircle.gradle.VerifyProductCoordinates +import org.harvestcircle.gradle.VerifyVerificationLanes plugins { alias(libs.plugins.kotlin.multiplatform) apply false @@ -10,6 +11,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 legacyProduct = "stu" + "dio" val verifyProductCoordinates by tasks.registering(VerifyProductCoordinates::class) { @@ -30,6 +32,12 @@ val verifyProductCoordinates by tasks.registering(VerifyProductCoordinates::clas ) } +val verifyVerificationLanes by tasks.registering(VerifyVerificationLanes::class) { + group = "verification" + description = "Validates forge-agnostic verification lanes and least-privilege policy." + policyFile.set(verificationLanesFile) +} + providers.environmentVariable("EXT_BUILD_GRADLE_BUILD_DIR").orNull?.let { extBuildGradleRoot -> layout.buildDirectory.set(file(extBuildGradleRoot).resolve("root")) } diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt @@ -0,0 +1,68 @@ +package org.harvestcircle.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +object VerificationLanes { + private val expected = + linkedMapOf( + "schema" to "harvestcircle.verification-lanes.v1", + "orchestration" to "external-forge-agnostic", + "source.command" to "make source-check", + "source.runner" to "linux", + "source.permissions" to "contents:read", + "source.credentials" to "none", + "package.command" to "make package-check", + "package.runners" to "linux,macos,windows", + "package.permissions" to "contents:read", + "package.credentials" to "none", + "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", + ) + + fun parse(source: String): Map<String, String> { + val parsed = linkedMapOf<String, String>() + source.trimEnd('\n', '\r').lineSequence().forEachIndexed { index, raw -> + val line = raw.trim() + require(line.isNotEmpty() && !line.startsWith('#')) { + "Verification lane policy contains an empty or comment line at ${index + 1}" + } + val separator = line.indexOf('=') + require(separator > 0 && separator < line.lastIndex && line.indexOf('=', separator + 1) == -1) { + "Verification lane policy contains malformed syntax at ${index + 1}" + } + val key = line.substring(0, separator) + val value = line.substring(separator + 1) + require(parsed.put(key, value) == null) { "Duplicate verification lane key: $key" } + } + require(parsed.keys == expected.keys) { "Verification lane keys do not match the authority" } + require(parsed == expected) { "Verification lane values do not match the authority" } + return parsed + } +} + +abstract class VerifyVerificationLanes : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val policyFile: RegularFileProperty + + @TaskAction + fun verify() { + val source = policyFile.get().asFile.readText() + check(VerificationLanes.parse(source).size == 18) + check(runCatching { VerificationLanes.parse(source + "source.permissions=write") }.isFailure) + check(runCatching { VerificationLanes.parse(source.replace("contents:read", "contents:write")) }.isFailure) + check(runCatching { VerificationLanes.parse(source.replace("credentials=none", "credentials=all")) }.isFailure) + check(runCatching { VerificationLanes.parse(source.replace("source.runner=linux", "source.runner=macos")) }.isFailure) + } +} diff --git a/config/verification/lanes-v1.properties b/config/verification/lanes-v1.properties @@ -0,0 +1,18 @@ +schema=harvestcircle.verification-lanes.v1 +orchestration=external-forge-agnostic +source.command=make source-check +source.runner=linux +source.permissions=contents:read +source.credentials=none +package.command=make package-check +package.runners=linux,macos,windows +package.permissions=contents:read +package.credentials=none +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