From ff9ab48a0f2c69e2ffe53a57e435b33ab9e7a66a Mon Sep 17 00:00:00 2001 From: hermanngeorge15 Date: Fri, 9 Oct 2026 12:43:43 +0200 Subject: [PATCH 1/4] ci: move release.yml to GitHub-hosted runners with SLSA provenance (closes #1) This repo is public and the org runner group enforces allows_public_repositories: false, so the self-hosted release jobs could never be scheduled: the next tag would have queued 24 h and been cancelled. v0.0.1 shipped only because it was built while the repo was still private. Moves all three jobs to ubuntu-latest under the same exception injection-scanner took in its #45. The release uses no org secret, only the built-in GITHUB_TOKEN, and fires only on tag pushes, which a fork cannot make. - Least privilege: the workflow default is contents: read. Only the release job holds contents/id-token/attestations: write. - Signed SLSA build provenance (actions/attest-build-provenance) over every tarball and SHA256SUMS.txt, created before the Release. The release notes give the gh attestation verify command. - Every action is pinned to a verified commit SHA. checkout moves v5 to v7.0.1; upload and download artifact move together to v7.0.1/v8.0.1. - The trigger narrows from v* to v[0-9]+.[0-9]+.[0-9]+. GSD milestone tags match v* too: run 29185695331 fired on v1.0-milestone, and on a hosted runner it would have published a real Release. - The asset contract is unchanged: six targets, the 4 required + 2 best-effort split, the same tarball shape, SHA256SUMS.txt. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/release.yml | 133 +++++++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 35 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e92fcdf..7354ded 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,20 +1,46 @@ name: Release -# Trigger: push a v* tag (e.g., git tag v0.0.1 && git push origin v0.0.1). +# Trigger: push an exact semver tag (e.g., git tag v0.1.0 && git push origin v0.1.0). +# +# The filter accepts ONLY vX.Y.Z. GSD milestone tags (v1.0-milestone) also start +# with "v", and under the old any-v glob such a tag started this workflow: run +# 29185695331 queued 24 h and was cancelled only because no runner could take +# it. On a hosted runner it would have published that tag as a real GitHub +# Release. In GitHub's filter syntax `+` means one-or-more, `.` is literal and +# the whole tag must match, so milestone tags and pre-release suffixes +# (v0.1.0-rc.1) do not trigger. Widen deliberately in Phase 6 if a pre-release +# dry run needs it. # # This pipeline is the twice-proven injection-scanner/mcp-hub cargo-zigbuild -# template adapted to tar.gz packaging: ONE self-hosted Linux host +# template adapted to tar.gz packaging: one GitHub-hosted Linux host # cross-compiles ALL targets via cargo-zigbuild (zig is host-arch-independent, -# so no per-arch runner, no Docker/cross, no macOS runner). Every job runs on -# UnityInFlow org self-hosted runners ONLY — ZERO GitHub-hosted runners -# (never any GitHub-hosted ubuntu/macos image anywhere, per CLAUDE.md). +# so no per-arch runner, no Docker/cross, no macOS runner). +# +# ── RUNNER POLICY (issue #1, option 2 — injection-scanner #45 precedent) ─────── +# This workflow runs on GitHub-hosted runners, an exception to the org default +# of self-hosted runners, for two reasons: +# +# 1. It uses NO org secrets — only the built-in GITHUB_TOKEN, which +# softprops/action-gh-release uses implicitly. There is no signing key, no +# Sonatype or npm token, no crates.io token. The Homebrew formula in +# UnityInFlow/homebrew-tap is updated by hand, outside this workflow. The +# self-hosted policy exists to protect secrets this pipeline does not hold. +# 2. This repo is public and the org runner group enforces +# allows_public_repositories: false (OPS-02, enforced 2026-07-09), so a +# self-hosted job cannot be scheduled here at all — the previous +# self-hosted ARM64 runner label would queue until cancelled. v0.0.1 +# shipped only because it was built while the repo was still private. +# +# Provenance: an ephemeral hosted runner is better provenance than a persistent +# self-hosted box that carries state between jobs, and this pipeline now emits +# a signed SLSA build-provenance attestation the old path could not produce. # -# ── RUNNER DISPOSITION ───────────────────────────────────────────────────────── -# All jobs run serially on the single ARM64 orangepi runner: the Hetzner X64 -# fleet (arc-runner-unityinflow) is intermittently offline (recurring ecosystem -# blocker, STATE.md carry-forward). cargo-zigbuild is host-arch-independent, so -# one board builds every triple. When the X64 fleet returns, `runs-on` can flip -# back to [arc-runner-unityinflow]. +# Safety: the workflow is tag-push triggered only, which a fork cannot fire. +# That is what makes the release job's `contents: write` / `id-token: write` +# acceptable. Adding any org secret to this file voids this exception. +# +# This resolves early the release-side runner decision Phase 3 D-12 deferred to +# Phase 6; ci.yml has used the same hosted D-02 split since Phase 3 D-12. # # ── ASSET CONTRACT ───────────────────────────────────────────────────────────── # Assets are tarballs (agent-memory-.tar.gz) with the single @@ -26,14 +52,22 @@ name: Release # The two apple-darwin legs are HARD-required: DIST-01 mandates macOS # arm64/x86_64 binaries, so a darwin failure fails the release (a deliberate # divergence from the analog's experimental darwin legs). The darwin bundled-C -# cross-compile (sqlite3.c + sqlite-vec.c) is de-risked by the mandatory -# spike-cross-compile.yml gate, which MUST be green before any tag is pushed -# (RESEARCH Pitfall 1). Only the two musl legs are best-effort -# (continue-on-error) — gnu covers Linux if musl fails (RESEARCH Open Q2). +# zigbuild cross-link (sqlite3.c + sqlite-vec.c) was proven by the v0.0.1 +# release run 28644256471, built on the old ARM64 host after the chrono +# features=[now] trim (Phase 02-04). It has not yet run from an x86_64 hosted +# host; the first tag run after this change (the Phase 6 release dry run) is +# that proof. spike-cross-compile.yml is a Windows + musl feasibility spike +# (Phase 3, bd45b87), not a release gate. Only the two musl legs are +# best-effort (continue-on-error) — gnu covers Linux if musl fails +# (RESEARCH Open Q2). on: push: tags: - - 'v*' + - 'v[0-9]+.[0-9]+.[0-9]+' + +# Read-only by default; only the release job is elevated. +permissions: + contents: read env: CARGO_TERM_COLOR: always @@ -51,14 +85,15 @@ jobs: # ────────────────────────────────────────────────────────────────────────── test: name: Test gate (fmt + clippy + test + coverage) - runs-on: [orangepi] + runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: + toolchain: stable components: clippy, rustfmt - name: cargo fmt check @@ -78,8 +113,8 @@ jobs: # ────────────────────────────────────────────────────────────────────────── # JOB 2: build-binaries - # ONE self-hosted Linux runner cross-compiles ALL 6 targets via - # cargo-zigbuild, serially (single orangepi board). + # Each matrix leg gets its own fresh GitHub-hosted x86_64 runner and the six + # legs run in parallel; each cross-compiles its target via cargo-zigbuild. # # The four DIST-01 targets (both apple-darwin + both linux-gnu) are # HARD-required. Only the two musl legs may fail without failing the @@ -88,7 +123,7 @@ jobs: build-binaries: name: Build ${{ matrix.target }} needs: test - runs-on: [orangepi] + runs-on: ubuntu-latest # Only the musl legs are non-blocking; darwin + gnu legs hard-fail (DIST-01). continue-on-error: ${{ matrix['continue-on-error'] }} strategy: @@ -115,19 +150,20 @@ jobs: continue-on-error: true steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Rust stable + target - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: + toolchain: stable targets: ${{ matrix.target }} - name: Install pinned zig + cargo-zigbuild run: | # Pinned versions (RESEARCH Pitfall 3): zig 0.14.1, cargo-zigbuild 0.23.0. - # Install zig from the official tarball matched to the HOST arch (the - # self-hosted runners have no pip3 -- orangepi is ARM64; a Hetzner X64 - # runner would be x86_64). zig itself cross-compiles to all targets, so + # Install zig from the official tarball matched to the HOST arch. Kept + # arch-dynamic on purpose: it costs nothing and keeps the job portable. + # ubuntu-latest is x86_64; zig itself cross-compiles to all targets, so # only the host arch matters here. Pin BOTH versions -- never `latest`. set -euo pipefail case "$(uname -m)" in @@ -161,6 +197,8 @@ jobs: # Execute only when the target arch equals the runner arch — # `Exec format error` otherwise (RESEARCH Pitfall 7, mcp-hub # 03-04 precedent). Decide at runtime, never hardcode a triple. + # On the x86_64 hosted runner the x86_64 Linux binaries now run + # --version natively; aarch64 and darwin are presence-only. TARGET_ARCH="${TARGET%%-*}" # x86_64 | aarch64 HOST_ARCH="$(uname -m)"; [ "$HOST_ARCH" = "arm64" ] && HOST_ARCH=aarch64 if [ "$TARGET_ARCH" = "$HOST_ARCH" ]; then @@ -185,8 +223,10 @@ jobs: echo "Packaged: $OUT" tar -tzf "$OUT" + # upload-artifact v7 and download-artifact v8 are the matched pair proven + # in injection-scanner's releases; they must move together. - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.target }} path: agent-memory-${{ matrix.target }}.tar.gz @@ -201,12 +241,15 @@ jobs: release: name: Create GitHub Release needs: [build-binaries] - runs-on: [orangepi] + runs-on: ubuntu-latest permissions: - contents: write + contents: write # create the Release and upload assets + id-token: write # sign the build-provenance attestation + attestations: write # record it steps: + # Matched pair with upload-artifact v7 above; they must move together. - name: Download all artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: merge-multiple: true path: artifacts/ @@ -253,8 +296,19 @@ jobs: echo "Generated SHA256SUMS.txt:" cat SHA256SUMS.txt + # OIDC-signed SLSA build provenance over every published file, possible + # only on a hosted runner. Created BEFORE the Release so nothing ships + # unattested. Consumers verify with: + # gh attestation verify agent-memory-.tar.gz --repo UnityInFlow/agent-memory + - name: Attest build provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: | + artifacts/agent-memory-*.tar.gz + artifacts/SHA256SUMS.txt + - name: Create GitHub Release - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: files: | artifacts/agent-memory-*.tar.gz @@ -263,9 +317,9 @@ jobs: body: | ## agent-memory release - Tarballs built entirely on UnityInFlow org self-hosted Linux runners - via `cargo zigbuild` (no GitHub-hosted runners). Each tarball holds - the single `agent-memory` binary at its root. + Tarballs cross-compiled from a GitHub-hosted Linux runner via + `cargo zigbuild`. Each tarball holds the single `agent-memory` + binary at its root. ### Install via Homebrew (macOS + Linux) @@ -292,5 +346,14 @@ jobs: ./agent-memory --version ``` + ### Verify provenance + + Every tarball and `SHA256SUMS.txt` carries a signed SLSA + build-provenance attestation: + + ``` + gh attestation verify agent-memory-.tar.gz --repo UnityInFlow/agent-memory + ``` + ## Checksums See `SHA256SUMS.txt` for verification. From 043f4444b5fa5536a2d72cf8d16ad2eaa749cba7 Mon Sep 17 00:00:00 2001 From: hermanngeorge15 Date: Fri, 9 Oct 2026 12:43:59 +0200 Subject: [PATCH 2/4] ci: SHA-pin ci.yml and the cross-compile spike to the release.yml pins (#1) ci.yml runs on this PR, so pinning it to the same commits as release.yml is the only evidence before merge that the checkout v7.0.1 and pinned rust-toolchain steps run on a hosted runner in this repo. The spike gets the same pins so that every workflow is SHA-pinned. A SHA-pinned dtolnay/rust-toolchain loses its branch-name default, so each step now passes `toolchain: stable` explicitly. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 5 +++-- .github/workflows/spike-cross-compile.yml | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13c9108..89a7582 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,10 +18,11 @@ jobs: build-and-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: + toolchain: stable components: clippy, rustfmt - name: Format check diff --git a/.github/workflows/spike-cross-compile.yml b/.github/workflows/spike-cross-compile.yml index 164d76e..182e9d5 100644 --- a/.github/workflows/spike-cross-compile.yml +++ b/.github/workflows/spike-cross-compile.yml @@ -58,11 +58,12 @@ jobs: - { target: x86_64-pc-windows-gnu, tool: mingw } steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Rust stable + target - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: + toolchain: stable targets: ${{ matrix.target }} - name: Install pinned zig + cargo-zigbuild (musl legs) From 9810e317d48242a7f6e604e993ce771cf74458e7 Mon Sep 17 00:00:00 2001 From: hermanngeorge15 Date: Fri, 9 Oct 2026 12:44:31 +0200 Subject: [PATCH 3/4] docs: document the hosted CI/release runner policy and provenance verification (#1) CONTRIBUTING.md and README.md still said CI runs on self-hosted runners only and that a maintainer may need to bring one online. Neither is possible on this public repo. They now describe the real policy: hosted fork CI with a read-only token, a hosted release fired only by exact vX.Y.Z tags under the issue #1 exception, the gh attestation verify command, and that the cross-compile spike is not a release gate. Co-Authored-By: Claude Opus 5.5 (1M context) --- CONTRIBUTING.md | 36 +++++++++++++++++++++++++----------- README.md | 2 +- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2bcc24d..3d922f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,17 +63,31 @@ refactor: extract decay blend into helper ## CI -CI runs on **UnityInFlow org self-hosted runners** (never GitHub-hosted -images). PRs run the same gates listed above: fmt check, clippy -`-D warnings`, the full workspace test suite, and the >80% llvm-cov line -coverage gate. If your PR's CI seems stuck in queue, a maintainer may need to -bring a runner online — leave a comment. +CI (`.github/workflows/ci.yml`) runs on **GitHub-hosted `ubuntu-latest`**, +secretless, with a read-only token. This repo is public and the UnityInFlow +org runner group does not serve public repositories, so self-hosted runners +are not an option here. PRs, including fork PRs, run the gates listed above: +fmt check, clippy `-D warnings`, the full workspace test suite, and the >80% +llvm-cov line coverage gate. ## Releases (maintainers) -Releases are tag-triggered: pushing a `v*` tag runs -`.github/workflows/release.yml`, which cross-compiles all supported triples -via `cargo-zigbuild`, packages per-triple tarballs, generates -`SHA256SUMS.txt`, and publishes a GitHub Release. The darwin cross-compile -spike (`.github/workflows/spike-cross-compile.yml`) must be green before -tagging. +Releases are triggered by pushing an exact semver tag `vX.Y.Z`. Other +`v`-prefixed tags, such as GSD milestone tags (`vX.Y-milestone`), +deliberately do not trigger `.github/workflows/release.yml`. + +The release workflow runs on GitHub-hosted runners. It uses only the built-in +`GITHUB_TOKEN`, with no org secret, and fires only on tag pushes, which forks +cannot make (issue #1, the same exception as injection-scanner #45). It +cross-compiles all supported triples via `cargo-zigbuild`, packages +per-triple tarballs, generates `SHA256SUMS.txt`, attests every tarball and +`SHA256SUMS.txt` with signed SLSA build provenance, and publishes the GitHub +Release. Verify a downloaded asset with: + +```bash +gh attestation verify agent-memory-.tar.gz --repo UnityInFlow/agent-memory +``` + +The Homebrew formula in `UnityInFlow/homebrew-tap` is updated manually +afterwards. `.github/workflows/spike-cross-compile.yml` is a manually +dispatched Windows + musl feasibility spike (Phase 3), not a release gate. diff --git a/README.md b/README.md index ca34696..aa7630b 100644 --- a/README.md +++ b/README.md @@ -290,7 +290,7 @@ cargo llvm-cov --workspace --fail-under-lines 80 # >80% coverage gate (CI-enfo ``` CI runs fmt-check, clippy `-D warnings`, the full test suite, and the coverage gate -on the UnityInFlow self-hosted runners. See [CONTRIBUTING.md](./CONTRIBUTING.md). +on GitHub-hosted runners (secretless, read-only token). See [CONTRIBUTING.md](./CONTRIBUTING.md). ## License From 9a8423305b02619dfcf09826117536a1ee400b39 Mon Sep 17 00:00:00 2001 From: hermanngeorge15 Date: Fri, 9 Oct 2026 12:45:03 +0200 Subject: [PATCH 4/4] docs(quick-261009-hc1): hosted release pipeline with SLSA provenance (closes #1) Co-Authored-By: Claude Opus 5.5 (1M context) --- .planning/STATE.md | 10 +- .../261009-hc1-PLAN.md | 376 ++++++++++++++++++ .../261009-hc1-SUMMARY.md | 83 ++++ 3 files changed, 467 insertions(+), 2 deletions(-) create mode 100644 .planning/quick/261009-hc1-move-release-yml-to-github-hosted-runner/261009-hc1-PLAN.md create mode 100644 .planning/quick/261009-hc1-move-release-yml-to-github-hosted-runner/261009-hc1-SUMMARY.md diff --git a/.planning/STATE.md b/.planning/STATE.md index eb3ef8f..bd333d7 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,7 +5,7 @@ milestone_name: Hardening & Interop status: executing stopped_at: Phase 4 context gathered last_updated: "2026-07-13T13:42:43.486Z" -last_activity: 2026-07-13 -- Phase 04 planning complete +last_activity: 2026-10-09 -- Completed quick task 261009-hc1 (hosted release pipeline, closes #1) progress: total_phases: 4 completed_phases: 1 @@ -28,7 +28,7 @@ See: .planning/PROJECT.md (updated 2026-07-12) Phase: 4 Plan: Not started Status: Ready to execute -Last activity: 2026-07-13 -- Phase 04 planning complete +Last activity: 2026-10-09 - Completed quick task 261009-hc1: hosted release pipeline with SLSA provenance (closes #1) Progress: [░░░░░░░░░░] 0% (0/4 v1.1 phases) @@ -103,6 +103,12 @@ Watch items: - Windows C-code cross-compile unproven for this workspace (sqlite3.c + sqlite-vec.c) — Phase 3 spike is the decision input; Windows runtime untestable in CI (presence smoke + best-effort label only). - If a fixed sqlite-vec stable release (PR #199) ships mid-milestone: bump the pin and delete the CFLAGS shim in the same commit. +### Quick Tasks Completed + +| # | Description | Date | Commit | Directory | +|---|-------------|------|--------|-----------| +| 261009-hc1 | Release pipeline on GitHub-hosted runners with SLSA provenance, semver-only trigger, all actions SHA-pinned (closes #1); resolves the release-runner decision Phase 3 D-12 deferred to Phase 6 | 2026-10-09 | 9810e31 | [261009-hc1-move-release-yml-to-github-hosted-runner](./quick/261009-hc1-move-release-yml-to-github-hosted-runner/) | + ## Deferred Items Items acknowledged and carried forward: diff --git a/.planning/quick/261009-hc1-move-release-yml-to-github-hosted-runner/261009-hc1-PLAN.md b/.planning/quick/261009-hc1-move-release-yml-to-github-hosted-runner/261009-hc1-PLAN.md new file mode 100644 index 0000000..276a85c --- /dev/null +++ b/.planning/quick/261009-hc1-move-release-yml-to-github-hosted-runner/261009-hc1-PLAN.md @@ -0,0 +1,376 @@ +--- +phase: quick-261009-hc1 +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - .github/workflows/release.yml + - .github/workflows/ci.yml + - .github/workflows/spike-cross-compile.yml + - CONTRIBUTING.md + - README.md + - CLAUDE.md # UNTRACKED, local-only: edited in the main checkout by absolute path, NEVER staged +autonomous: true +requirements: [ISSUE-1] +user_setup: [] + +estimate: + tokens: 40000 + raw_tokens: 40000 + tasks: 3 + confidence: low + +must_haves: + truths: + - "Pushing a semver tag vX.Y.Z to this public repo schedules all three release jobs (test, build-binaries, release) on GitHub-hosted ubuntu-latest — no workflow in .github/workflows/ names any self-hosted runner label" + - "A GSD milestone tag (v1.0-milestone, v1.1-milestone) can no longer start release.yml — the trigger accepts only exact vX.Y.Z tags" + - "Only the release job holds write/OIDC scopes (contents: write, id-token: write, attestations: write); the workflow default is contents: read, and release.yml references no org secret — only the implicit GITHUB_TOKEN" + - "Every published tarball AND SHA256SUMS.txt is covered by a signed SLSA build-provenance attestation produced BEFORE the GitHub Release is created, and the release notes tell users to verify with gh attestation verify --repo UnityInFlow/agent-memory" + - "Every uses: line in every workflow is pinned to a verified 40-hex commit SHA with a trailing version comment, and every pinned dtolnay/rust-toolchain step passes toolchain: stable explicitly" + - "The asset contract is unchanged: the same six targets with the same required (4 DIST-01) / best-effort (2 musl) split, agent-memory-.tar.gz with the binary at the archive root, plus SHA256SUMS.txt; no Windows leg" + - "CONTRIBUTING.md, README.md and the maintainer's local CLAUDE.md describe the real runner policy (hosted CI + hosted tag-only release under the issue #1 exception), not the dead self-hosted policy" + artifacts: + - path: ".github/workflows/release.yml" + provides: "Hosted, least-privilege, SHA-pinned, provenance-attesting tag release pipeline" + contains: "actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2" + - path: ".github/workflows/ci.yml" + provides: "PR/fork CI with the same SHA pins as release.yml — the only pre-merge proof those pins run here" + contains: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1" + - path: ".github/workflows/spike-cross-compile.yml" + provides: "Dispatch-only Windows+musl spike, SHA-pinned for a repo-wide pin invariant" + contains: "dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable" + - path: "CONTRIBUTING.md" + provides: "Public, tracked statement of the CI + release runner policy and provenance verification" + contains: "gh attestation verify" + - path: "README.md" + provides: "Corrected CI runner sentence" + contains: "GitHub-hosted" + key_links: + - from: "release.yml on.push.tags" + to: "release job" + via: "filter pattern v[0-9]+.[0-9]+.[0-9]+ (whole-name match; milestone tags excluded)" + pattern: "v\\[0-9\\]\\+\\.\\[0-9\\]\\+\\.\\[0-9\\]\\+" + - from: "release job permissions (id-token: write, attestations: write)" + to: "Attest build provenance step" + via: "OIDC token signs the attestation; without both scopes the step fails" + pattern: "id-token: write" + - from: "Attest build provenance step subject-path" + to: "the files the GitHub Release uploads" + via: "same globs: artifacts/agent-memory-*.tar.gz + artifacts/SHA256SUMS.txt" + pattern: "artifacts/agent-memory-\\*\\.tar\\.gz" + - from: "ci.yml pins" + to: "release.yml pins" + via: "identical checkout/rust-toolchain SHAs — the PR's own CI run exercises them on a hosted runner" + pattern: "3d3c42e5aac5ba805825da76410c181273ba90b1" +--- + + +Fix UnityInFlow/agent-memory#1 (option 2, the injection-scanner #45 precedent): move release.yml off the +self-hosted runner that this public repo can never schedule, onto GitHub-hosted ubuntu-latest, with +least-privilege permissions, signed SLSA build provenance over every released file, and every action +SHA-pinned. Correct the runner-policy docs to match. + +Purpose: under the org runner group's allows_public_repositories: false, the next v* tag would queue for +24 h and be cancelled. v0.0.1 shipped only because the repo was still private. The release uses no org +secret (only the built-in GITHUB_TOKEN) and fires only on tag pushes, which a fork cannot make. Those are +exactly the conditions under which the ecosystem granted injection-scanner the same exception. This also +resolves early the release-side runner decision that Phase 3 D-12 deferred to Phase 6. + +Output: a rewritten release.yml; SHA-pinned ci.yml and spike-cross-compile.yml; corrected CONTRIBUTING.md +and README.md (all tracked, all in the PR); a corrected local CLAUDE.md (untracked, never committed). + +Planner additions beyond the orchestrator's requirement list (each is one isolated, revertible change): +(a) trigger narrowed from the any-v glob to exact semver tags. Evidence: release run 29185695331 fired on +tag v1.0-milestone, created by GSD git.create_tag. On a hosted runner it would have published that tag as +a real Release. (b) ci.yml and spike-cross-compile.yml pinned with the same SHAs: ci.yml runs on this PR, +which makes it the only pre-merge evidence that the new checkout v7 / rust-toolchain pins work in this repo. +(c) CONTRIBUTING.md and README.md corrected: CLAUDE.md is untracked, so the public policy statement has to +live in tracked docs. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.planning/STATE.md +@.github/workflows/release.yml +@.github/workflows/ci.yml +@.github/workflows/spike-cross-compile.yml +@CONTRIBUTING.md + +Precedent (read once, copy patterns, NOT stale leftovers): +/private/tmp/claude-501/-Users-jirihermann-Documents-workspace-1-ideas-unity-in-flow-ai-03-injection-scanner/6c0fb560-914e-4776-80d0-1b72347def73/scratchpad/reference-injection-scanner-release.yml +— injection-scanner's release.yml after its identical move (issue #45). Its release run for v0.1.0 +(33256814534) was green, including the "Attest build provenance" step, with exactly these pins and the +three-scope release permissions block. + +Verified facts (planner, 2026-10-09; do not re-derive): +- Pins resolved with git ls-remote against the tag refs: + actions/checkout v7.0.1 = 3d3c42e5aac5ba805825da76410c181273ba90b1 + actions/upload-artifact v7.0.1 = 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + actions/download-artifact v8.0.1 = 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + actions/attest-build-provenance v4.2.2 = 4d101475d8b20a2381f78447822ac1eab6504dd8 + softprops/action-gh-release v3.0.3 = efb35369e0ad2afab669f228072c1b0d510eae64 (the peeled commit of the annotated tag; pin the commit, not the tag object e598afb…) + dtolnay/rust-toolchain "stable" = 4360b52568e2003a75bf9bc1d59f33a8e3fc893c (the reference pin, a "toolchain: stable" commit from 2026-08-05; comment it "# stable") +- Upgrade choice: checkout v5 → v7.0.1 and the artifact pair v5/v5 → upload v7.0.1 / download v8.0.1, the matched pair proven in the + injection-scanner releases. The upload and download majors move together in this one change. +- A SHA-pinned dtolnay/rust-toolchain does not carry the branch-name default, so every step that uses it MUST pass toolchain: stable. +- CLAUDE.md, .gitignore, .claude/, .mcp.json, 10-agent-memory.md and claude-code-harness-engineering-guide-v2.md are UNTRACKED in + this repo and absent from origin/main. .planning/config.json has an unrelated unstaged modification. None of these may be staged. +- actionlint 1.7.12 (with shellcheck) is installed at /opt/homebrew/bin. Today it flags the three self-hosted runs-on labels + in release.yml, and it lints the reference file and the v[0-9]+.[0-9]+.[0-9]+ tag filter clean. +- A real tag run is impossible before merge. The workflow is tag-triggered only, and that is a deliberate safety property. +- Every command runs from the repo root of whichever checkout or worktree holds the commits (origin/main resolves in both). + No command here takes more than a few seconds: there is no cargo build or test, because no Rust source changes. + + + + + + Task 1 (tracer): release.yml — hosted runners, least privilege, semver-only trigger, SHA pins, provenance attestation, corrected header and release body + .github/workflows/release.yml + + - .github/workflows/release.yml (whole file; this is the only file this task edits) + - The reference injection-scanner release.yml at the scratchpad path in context: lines 9-32 (RUNNER POLICY header style), 77-85 (checkout + rust-toolchain pins with toolchain: stable), 195-214 (arch-dynamic zig comment), 255-256 (upload pin), 270-282 (release permissions + download pin), 331-346 (attest step + softprops pin), 363-368 (release-body verify line) + + + Edit release.yml in place with targeted Edits, not a full rewrite. The packaging, validation, checksum and smoke-test logic stays byte-identical. Only the items below change. + + 1. Trigger (planner addition a): change on.push.tags from the single any-v glob to the single pattern 'v[0-9]+.[0-9]+.[0-9]+' (quoted). The trigger stays tag-push only: no workflow_dispatch, no branch or PR trigger. Above it, replace the old "Trigger:" comment line with: push an exact semver tag, e.g. git tag v0.1.0 then git push origin v0.1.0. Add one short paragraph explaining the narrowing. GSD milestone tags (v1.0-milestone) also begin with v, and under the old glob such a tag started this workflow: run 29185695331 queued 24 h and was cancelled only because no runner could take it. On a hosted runner it would publish that tag as a GitHub Release. In GitHub filter syntax + means one-or-more, the dot is literal and the whole tag must match, so milestone tags and pre-release suffixes (v0.1.0-rc.1) do not match. Widen deliberately in Phase 6 if a pre-release dry run needs it. + + 2. Header: delete the paragraph that claims every job runs only on org runners with zero hosted images, and delete the whole RUNNER DISPOSITION section. In their place write a "RUNNER POLICY (issue #1, option 2 — injection-scanner #45 precedent)" section in the reference's numbered style, using agent-memory facts: + (1) This release uses no org secret, only the built-in GITHUB_TOKEN that softprops/action-gh-release uses implicitly: no signing key, no Sonatype or npm token, no crates.io token. The Homebrew formula in UnityInFlow/homebrew-tap is updated by hand outside this workflow. The self-hosted policy exists to protect secrets this pipeline does not hold. + (2) This repo is public and the org runner group enforces allows_public_repositories: false (OPS-02, enforced 2026-07-09). A self-hosted job therefore cannot be scheduled here at all, and the previous self-hosted ARM64 runner label would queue until cancelled. v0.0.1 shipped only because it was built while the repo was still private. + Provenance note: an ephemeral hosted runner is better provenance than a persistent self-hosted box that carries state between jobs, and this pipeline now emits a signed SLSA build-provenance attestation the old path could not produce. + Safety: the workflow is tag-push triggered only, which a fork cannot fire. That is what makes the release job's contents: write / id-token: write acceptable. Adding any org secret to this file voids this exception. + Note that this resolves early the release-side runner decision Phase 3 D-12 deferred to Phase 6, and that ci.yml has used the same hosted D-02 split since Phase 3 D-12. + Refer to the old runners only by concept ("the self-hosted ARM64 runner label", "the Hetzner X64 fleet label"), never by their label strings. Task 1's verify greps the whole workflows directory for those strings. + Keep the opening explanation of the cargo-zigbuild template (zig is host-arch-independent, so one Linux host cross-compiles every target, with no Docker/cross and no macOS runner), but describe that host as a GitHub-hosted Linux runner. + + 3. ASSET CONTRACT section: keep it verbatim. It is already accurate. + + 4. DARWIN DISPOSITION section: keep the hard-required darwin statement and the musl best-effort statement. Replace the sentences claiming the darwin cross-compile is de-risked by a mandatory spike gate that must pass before tagging. That is stale: commit bd45b87 (Phase 3, D-10/D-11) rewrote spike-cross-compile.yml as a Windows + musl spike, so it no longer covers darwin and is not a release gate. State instead: the darwin zigbuild cross-link was proven by the v0.0.1 release run 28644256471, built on the old ARM64 host after the chrono features=[now] trim (Phase 02-04). It has not yet run from an x86_64 hosted host; the first tag run after this change (the Phase 6 release dry run) is that proof. + + 5. Permissions: add a workflow-level permissions block with only contents: read, placed between on: and env:, with a one-line comment that the default is read-only and only the release job is elevated. test and build-binaries get NO job-level permissions key (they inherit read). Give the release job exactly contents: write, id-token: write and attestations: write, with the reference's trailing comments ("create the Release and upload assets", "sign the build-provenance attestation", "record it"). Job-level permissions replace the workflow default, and download-artifact within the same run needs no extra scope. + + 6. runs-on: all three jobs become runs-on: ubuntu-latest (plain scalar, not a list). + + 7. SHA pins, keeping the "# vX.Y.Z" trailing comment. Use each value from the context's verified-facts list: + - checkout in both jobs: the v7.0.1 pin. + - rust-toolchain in both jobs: the 4360b525… pin, commented "# stable". Each of these steps MUST gain toolchain: stable in its with: block, alongside the existing components (test job) or targets (build job). Without it a SHA-pinned rust-toolchain fails with a missing-input error. + - upload-artifact: the v7.0.1 pin. download-artifact: the v8.0.1 pin. + - softprops/action-gh-release: the v3.0.3 peeled-commit pin. + Above the artifact steps, add a one-line comment that upload v7 and download v8 are the matched pair proven in injection-scanner's releases and must move together. + + 8. build-binaries job header comment: rewrite the line describing one serial runner on a single board. On hosted runners each matrix leg gets its own fresh x86_64 runner and the legs run in parallel. Do NOT copy the reference's leftover job-header sentence that still describes a non-hosted runner. Keep the DIST-01 required/best-effort paragraph. The matrix itself (six targets, continue-on-error values) and fail-fast: false are unchanged. + + 9. zig install step: logic unchanged (the uname -m case already maps x86_64/amd64 to ZIG_ARCH=x86_64; ZIG_VERSION 0.14.1 and CARGO_ZIGBUILD_VERSION 0.23.0 stay pinned in env). Replace only the comment lines that mention the old runners and pip3 with the reference's wording: kept arch-dynamic on purpose because it costs nothing and keeps the job portable; ubuntu-latest is x86_64; zig cross-compiles to every target, so only the host arch matters; pin both, never latest. + + 10. Smoke-test step: logic unchanged. It already derives TARGET_ARCH from the triple and HOST_ARCH from uname -m at runtime, with no hardcoded aarch64. Add one comment line: on the x86_64 hosted runner the x86_64 Linux binaries now execute --version natively (stronger than before, when only the aarch64 ones could run); aarch64 and darwin are presence-only. + + 11. Attestation: in the release job, insert a step named "Attest build provenance" AFTER "Generate SHA256SUMS.txt" and BEFORE "Create GitHub Release". It uses actions/attest-build-provenance at the v4.2.2 pin with a multi-line subject-path listing artifacts/agent-memory-*.tar.gz and artifacts/SHA256SUMS.txt. Give it a short comment above, modelled on the reference: OIDC-signed SLSA provenance, possible only on a hosted runner, consumers verify with gh attestation verify agent-memory-.tar.gz --repo UnityInFlow/agent-memory. + + 12. Release body (the body: block of the softprops step): replace the opening paragraph that says the tarballs were built on org self-hosted runners with: tarballs cross-compiled from a GitHub-hosted Linux runner via cargo zigbuild, each holding the single agent-memory binary at its root. Add a "Verify provenance" sub-heading before "## Checksums". Under it, state that every tarball and SHA256SUMS.txt carries a signed SLSA build-provenance attestation, followed by a fenced command block (inside the YAML body string, as the existing install blocks are) containing gh attestation verify agent-memory-.tar.gz --repo UnityInFlow/agent-memory. Leave the Homebrew section, the manual-install table, the checksum/extract commands and the files: globs unchanged. Do NOT add a Windows row or leg; that is Phase 6 scope (DIST-03). + + + python3 -c ' +import re, yaml +wf = yaml.safe_load(open(".github/workflows/release.yml")) +on = wf.get("on", wf.get(True)) +assert list(on) == ["push"] and list(on["push"]) == ["tags"], on +assert on["push"]["tags"] == ["v[0-9]+.[0-9]+.[0-9]+"], on +pat = re.compile(r"v[0-9]+\.[0-9]+\.[0-9]+") +assert all(pat.fullmatch(t) for t in ["v0.0.1", "v0.1.0", "v1.10.2"]) +assert not any(pat.fullmatch(t) for t in ["v1.0-milestone", "v1.1-milestone", "v0.1.0-rc.1"]) +assert wf["permissions"] == {"contents": "read"}, wf.get("permissions") +jobs = wf["jobs"] +assert set(jobs) == {"test", "build-binaries", "release"}, list(jobs) +for name, job in jobs.items(): + assert job["runs-on"] == "ubuntu-latest", (name, job["runs-on"]) +assert "permissions" not in jobs["test"] and "permissions" not in jobs["build-binaries"] +assert jobs["release"]["permissions"] == {"contents": "write", "id-token": "write", "attestations": "write"} +inc = jobs["build-binaries"]["strategy"]["matrix"]["include"] +assert {(i["target"], i["continue-on-error"]) for i in inc} == {("aarch64-apple-darwin", False), ("x86_64-apple-darwin", False), ("x86_64-unknown-linux-gnu", False), ("aarch64-unknown-linux-gnu", False), ("x86_64-unknown-linux-musl", True), ("aarch64-unknown-linux-musl", True)} +steps = jobs["release"]["steps"] +uses = [s.get("uses", "") for s in steps] +i_att = next(i for i, u in enumerate(uses) if u.startswith("actions/attest-build-provenance@")) +i_rel = next(i for i, u in enumerate(uses) if u.startswith("softprops/action-gh-release@")) +i_sum = next(i for i, s in enumerate(steps) if s.get("name", "").startswith("Generate SHA256SUMS")) +assert i_rel > i_att > i_sum, (i_sum, i_att, i_rel) +subj = steps[i_att]["with"]["subject-path"] +assert "artifacts/agent-memory-*.tar.gz" in subj and "artifacts/SHA256SUMS.txt" in subj, subj +body = steps[i_rel]["with"]["body"] +assert "gh attestation verify" in body and "--repo UnityInFlow/agent-memory" in body +for job in jobs.values(): + for s in job["steps"]: + if "uses" in s: + assert re.fullmatch(r"[\w.-]+/[\w.-]+@[0-9a-f]{40}", s["uses"]), s["uses"] + if s["uses"].startswith("dtolnay/rust-toolchain@"): + assert s["with"]["toolchain"] == "stable", s +print("release.yml structure OK") +' && actionlint .github/workflows/release.yml && test "$(grep -c '^ runs-on: ubuntu-latest$' .github/workflows/release.yml)" -eq 3 && ! grep -rn 'orangepi' .github/workflows/ && ! grep -rn 'arc-runner' .github/workflows/ && ! grep -niE 'zero github-hosted|never any github-hosted|no github-hosted' .github/workflows/release.yml && ! grep -n 'MUST be green before any tag' .github/workflows/release.yml && ! grep -n 'ONE self-hosted' .github/workflows/release.yml && ! grep -nE '[$][{][{] *secrets[.]' .github/workflows/release.yml && grep -qF 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1' .github/workflows/release.yml && grep -qF 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1' .github/workflows/release.yml && grep -qF 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1' .github/workflows/release.yml && grep -qF 'actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2' .github/workflows/release.yml && grep -qF 'softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3' .github/workflows/release.yml && grep -qF 'dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable' .github/workflows/release.yml && test -z "$(git diff origin/main -- .github/workflows/release.yml | grep -E '^-[^-]' | grep -F 'tar.gz')" && echo TASK1-GREEN + + + - The python structural check prints "release.yml structure OK". It covers the trigger shape, the semver-only filter (modelled with Python re: + is one-or-more, the dot is literal, whole-name match), the read-only default, the three-scope release permissions, all six matrix legs with unchanged continue-on-error, the step order SHA256SUMS → attest → release, both subject paths, the verify line in the body, a 40-hex pin on every uses:, and toolchain: stable on both rust-toolchain steps. + - actionlint exits 0. Before this task it reported three unknown-runner-label errors. + - Exactly three job-level "runs-on: ubuntu-latest" lines. + - The whole workflows directory contains neither old runner label string. + - The stale header and body claims are gone (zero-hosted / never-any-hosted / no-hosted phrasing, the darwin spike-gate sentence, the reference's leftover self-hosted job header), and no secrets expression appears. + - All six exact pin-plus-comment strings are present. + - The diff against origin/main removes no line containing tar.gz: the asset contract, required-asset list, checksum loop, upload path, files: globs and the release-body table are untouched. + + release.yml lints clean, schedules every job on ubuntu-latest, fires only on exact vX.Y.Z tags, gives write/OIDC scopes to the release job alone, attests every tarball plus SHA256SUMS.txt before creating the Release, pins every action to a verified SHA, and ships the same six-target tarball contract. Committed as "ci: move release.yml to GitHub-hosted runners with SLSA provenance (closes #1)", staging ONLY .github/workflows/release.yml by explicit path. + + + + Task 2: SHA-pin ci.yml and spike-cross-compile.yml with the same pins (pre-merge proof of the release.yml pins) + .github/workflows/ci.yml, .github/workflows/spike-cross-compile.yml + + - .github/workflows/ci.yml (lines 21-25: the two uses: steps) + - .github/workflows/spike-cross-compile.yml (lines 60-66: the two uses: steps) + + + Planner addition (b). ci.yml runs on this PR, so pinning it to the same SHAs as release.yml is the only pre-merge evidence that checkout v7.0.1 and the rust-toolchain pin with an explicit toolchain input actually run on a hosted runner in this repo. The spike gets the same pins so that every workflow in the repo is SHA-pinned. + + In ci.yml: replace actions/checkout@v4 with the checkout v7.0.1 pin (comment "# v7.0.1"). Replace dtolnay/rust-toolchain@stable with the 4360b525… pin (comment "# stable") and add toolchain: stable as the first key of its existing with: block, before components. Change nothing else. In particular, leave the runner-policy comment, permissions, triggers and run steps as they are; they are correct under Phase 3 D-12. + + In spike-cross-compile.yml: the same two substitutions (checkout@v5 becomes the v7.0.1 pin; rust-toolchain@stable becomes the pin with toolchain: stable added before targets). Leave the recorded VERDICT block, matrix, env and every run: step untouched. The verdict is a historical record of run 29196226289 and stays valid. + + Stage ONLY these two files, by explicit path. + + + actionlint .github/workflows/*.yml && test -z "$(grep -rhE '^[[:space:]]*(- )?uses:' .github/workflows/ | grep -vE 'uses: [A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[0-9a-f]{40} # [^ ]')" && python3 -c ' +import glob, yaml +n = 0 +for f in sorted(glob.glob(".github/workflows/*.yml")): + wf = yaml.safe_load(open(f)) + for job in wf["jobs"].values(): + for s in job.get("steps", []): + if s.get("uses", "").startswith("dtolnay/rust-toolchain@"): + assert s["with"]["toolchain"] == "stable", (f, s) + n += 1 +assert n == 4, n +print("rust-toolchain pins carry toolchain: stable in", n, "steps") +' && git diff --stat origin/main -- .github/workflows/ci.yml .github/workflows/spike-cross-compile.yml && echo TASK2-GREEN + + + - actionlint is clean across all three workflows. + - Every uses: line in .github/workflows/ matches owner/repo@40hex followed by a "# version" comment (the offender list is empty). + - All four rust-toolchain steps across the repo (two in release, one in ci, one in spike) pass toolchain: stable. + - The ci.yml and spike diffs touch only the uses: lines plus one added toolchain line each (about 3 changed lines per file). + + Every workflow in the repo is SHA-pinned to the same verified commits. The PR's own CI run will exercise the checkout and rust-toolchain pins on a hosted runner before merge. Committed as "ci: SHA-pin ci.yml and the cross-compile spike to the release.yml pins (#1)", staging only the two files. + + + + + + Task 3: Correct the runner-policy docs — CONTRIBUTING.md and README.md (tracked, in the PR), CLAUDE.md (untracked, local only) + CONTRIBUTING.md, README.md, CLAUDE.md + + - CONTRIBUTING.md (lines 64-79: the "## CI" and "## Releases (maintainers)" sections; the file ends at line 79) + - README.md (lines 292-293: the CI sentence before "## License") + - /Users/jirihermann/Documents/workspace-1-ideas/unity-in-flow-ai/10-agent-memory/CLAUDE.md (lines 86-104: "## CI / Self-Hosted Runners") + - /Users/jirihermann/Documents/workspace-1-ideas/unity-in-flow-ai/03-injection-scanner/CLAUDE.md, section "## CI / Self-Hosted Runners": the structure to mirror (revision note, why no self-hosted job can run, per-workflow yaml snippets, the never-rules) + + + CONTRIBUTING.md, "## CI" section: replace the paragraph. New text: CI (.github/workflows/ci.yml) runs on GitHub-hosted ubuntu-latest, secretless, with a read-only token. This repo is public and the UnityInFlow org runner group does not serve public repositories, so self-hosted runners are not an option here. PRs, including fork PRs, run the gates listed above (fmt check, clippy -D warnings, the full workspace test suite, the over-80% llvm-cov coverage gate). Drop the sentence telling contributors that a maintainer may need to start a runner for a queued PR. + + CONTRIBUTING.md, "## Releases (maintainers)" section, rewritten: + - Releases are triggered by pushing an exact semver tag vX.Y.Z. Other v-prefixed tags, such as GSD milestone tags vX.Y-milestone, deliberately do not trigger release.yml. + - release.yml runs on GitHub-hosted runners. It uses only the built-in GITHUB_TOKEN, with no org secret, and fires only on tag pushes, which forks cannot make (issue #1, the same exception as injection-scanner #45). + - It cross-compiles all supported triples via cargo-zigbuild, packages per-triple tarballs, generates SHA256SUMS.txt, attests every tarball and SHA256SUMS.txt with signed SLSA build provenance (verify: gh attestation verify agent-memory-.tar.gz --repo UnityInFlow/agent-memory), and publishes the GitHub Release. + - The Homebrew formula in UnityInFlow/homebrew-tap is updated manually afterwards. + - Remove the claim that the darwin spike must pass before tagging. spike-cross-compile.yml is a manually dispatched Windows + musl feasibility spike (Phase 3), not a release gate. + + README.md lines 292-293: rewrite the sentence so CI runs the same gates "on GitHub-hosted runners (secretless, read-only token)", keeping the CONTRIBUTING.md link. Change nothing else in README. + + CLAUDE.md is UNTRACKED: never committed and absent from origin/main, like the other local-only files beside it. If you run inside a git worktree, the file does not exist there. Edit it ONLY at the absolute path /Users/jirihermann/Documents/workspace-1-ideas/unity-in-flow-ai/10-agent-memory/CLAUDE.md and NEVER git add it. Replace the body of "## CI / Self-Hosted Runners" (keep the heading): remove the blanket ban on GitHub-hosted runners, the three self-hosted runs-on yaml examples, and the "Available runners" line. Write, mirroring the injection-scanner CLAUDE.md section: + - A revision note: revised 2026-10-09, issue #1, quick task 261009-hc1. + - Why: this repo is public and the org runner group enforces allows_public_repositories: false, so no self-hosted job can run here at all. In addition, arc-runner-unityinflow matches zero registered runners. + - ci.yml: GitHub-hosted, secretless, permissions contents: read (ecosystem D-02 exception, Phase 3 D-12/D-10). Add a short yaml snippet with runs-on: ubuntu-latest and the permissions block, plus a "deliberate — do not change to a self-hosted label" comment. + - release.yml: GitHub-hosted under the issue #1 exception (injection-scanner #45 precedent), because it carries no org secret (only GITHUB_TOKEN) and is triggered only by exact vX.Y.Z tag pushes, which forks cannot fire. Only the release job holds contents: write / id-token: write / attestations: write. Include a short yaml snippet of that trigger plus permissions shape, and the gh attestation verify command. + - The never-rules: + - never target arc-runner-unityinflow or [orangepi], or any self-hosted label, from this public repo; + - never put a write- or OIDC-scoped job behind a fork-firable trigger; + - adding any org secret to release.yml voids the exception and needs the private-window pattern or a workflow-restricted runner group instead; + - every uses: stays SHA-pinned with a version comment; + - the asset contract (agent-memory-.tar.gz, binary at archive root, SHA256SUMS.txt) is what the Homebrew formula consumes. + + Commit: stage ONLY CONTRIBUTING.md and README.md by explicit path. Never use git add -A, git add . or git commit -a. The working tree holds untracked local files and an unrelated .planning/config.json change that must not be swept in. Record in the SUMMARY that the CLAUDE.md edit is local-only and why. + + + R=/Users/jirihermann/Documents/workspace-1-ideas/unity-in-flow-ai/10-agent-memory && ! grep -n 'never GitHub-hosted' CONTRIBUTING.md && ! grep -n 'bring a runner online' CONTRIBUTING.md && grep -q 'gh attestation verify' CONTRIBUTING.md && grep -q 'UnityInFlow/agent-memory' CONTRIBUTING.md && grep -qE 'vX\.Y\.Z' CONTRIBUTING.md && ! grep -n 'UnityInFlow self-hosted' README.md && grep -q 'GitHub-hosted' README.md && ! grep -n 'Never use .ubuntu-latest' "$R/CLAUDE.md" && ! grep -n 'runs-on: \[arc-runner' "$R/CLAUDE.md" && ! grep -n 'matrix.runner' "$R/CLAUDE.md" && grep -q 'allows_public_repositories: false' "$R/CLAUDE.md" && grep -q 'id-token: write' "$R/CLAUDE.md" && grep -q 'gh attestation verify' "$R/CLAUDE.md" && grep -q '#1' "$R/CLAUDE.md" && test -z "$(git -C "$R" ls-files CLAUDE.md)" && test -z "$(git log origin/main..HEAD --name-only --format= | grep -vE '^(\.github/workflows/(release|ci|spike-cross-compile)\.yml|CONTRIBUTING\.md|README\.md|\.planning/STATE\.md|\.planning/quick/261009-hc1-[^/]+/.*)$' | grep -v '^$')" && echo TASK3-GREEN + + + - CONTRIBUTING.md no longer forbids GitHub-hosted runners and no longer tells contributors to wait for a runner; it documents the vX.Y.Z trigger, the no-secret / tag-only rationale and the gh attestation verify command. + - README.md's CI sentence says GitHub-hosted, and the self-hosted claim is gone. + - Local CLAUDE.md no longer bans GitHub-hosted runners or offers self-hosted runs-on examples. It states the allows_public_repositories: false reason, the release job's id-token scope, the verify command and the issue #1 reference. + - CLAUDE.md is still untracked (git ls-files returns nothing). + - The commits on this branch touch only the three workflows, CONTRIBUTING.md, README.md and this quick task's .planning/quick/261009-hc1-* docs. No untracked local file or the unrelated .planning/config.json change slipped into any commit. + + Tracked docs describe the real policy and ship in the PR. The maintainer's local CLAUDE.md is corrected but stays uncommitted. Committed as "docs: document the hosted CI/release runner policy and provenance verification (#1)", staging only CONTRIBUTING.md and README.md. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| fork PR → ci.yml | Untrusted contributor code runs on a hosted runner with a read-only token | +| maintainer tag push → release.yml | The only way to reach the write/OIDC-scoped release job | +| third-party actions → workflow | Action code executes with the job's token; mutable tags are a supply-chain vector | +| hosted runner → GitHub Release → Homebrew/manual users | Published binaries are downloaded and executed by end users | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-hc1-01 | Elevation of privilege | release.yml triggers + permissions | high | mitigate | Tag-push-only trigger (forks cannot push tags); workflow default permissions contents: read; contents/id-token/attestations write granted on the release job only; asserted by the Task 1 python check | +| T-hc1-02 | Tampering | every uses: in all three workflows | high | mitigate | Pin every action to a 40-hex commit verified with git ls-remote against its tag on 2026-10-09 (softprops pinned to the peeled commit, not the annotated-tag object); Task 2 repo-wide gate rejects any unpinned uses: | +| T-hc1-03 | Spoofing | release.yml tag filter | medium | mitigate | Narrow the trigger to v[0-9]+.[0-9]+.[0-9]+ so GSD milestone tags (evidence: run 29185695331 on v1.0-milestone) can never publish a bogus "latest" Release with binaries | +| T-hc1-04 | Repudiation | published tarballs + SHA256SUMS.txt | medium | mitigate | actions/attest-build-provenance over both globs before the Release is created; verify command in release notes, CONTRIBUTING.md and CLAUDE.md | +| T-hc1-05 | Information disclosure | release.yml on a hosted runner | low | mitigate | No org secret is referenced (Task 1 gate: no secrets expression in release.yml); docs state that adding one voids the exception | +| T-hc1-06 | Tampering | persistent runner state between jobs | low | mitigate | Ephemeral hosted runners replace the persistent self-hosted host, so there is no cross-job /tmp carry-over | +| T-hc1-07 | Tampering | dtolnay/rust-toolchain pin 4360b525 (2026-08-05) predates upstream fix #187 for issue #179 (components input interpolated into a shell) | low | accept | Every caller passes static literals (clippy, rustfmt) or a hardcoded matrix target; no attacker-controlled value reaches the input. Follow-up: bump the pin to a post-#187 master commit together with injection-scanner, since 4360b525 is no longer on any branch tip | +| T-hc1-SC | Tampering | package installs | low | accept | This plan adds no package-manager installs. The existing cargo install of cargo-llvm-cov (--locked) and cargo-zigbuild (--locked, pinned 0.23.0) are unchanged | + + + +Whole-change gate (run after Task 3, from the checkout or worktree that holds the commits): +- actionlint .github/workflows/*.yml exits 0. +- The Task 1 python structural check passes again (re-run it after Task 2 and Task 3 to confirm no regression). +- No old runner label string appears anywhere in .github/workflows/; every uses: is 40-hex pinned with a version comment. +- git log origin/main..HEAD shows three code/docs commits touching only the five tracked files, plus quick-task docs. + +Not verifiable before merge, by design. State this plainly in the SUMMARY and the PR body: +- release.yml cannot run until a vX.Y.Z tag is pushed, and its tag-only trigger is the safety property that justifies the exception. The PR's own CI run (ci.yml with the new pins) is the only pre-merge execution evidence, and it covers the checkout and rust-toolchain pins on a hosted runner, nothing more. +- The first hosted tag run is the Phase 6 release dry run. It must confirm: the darwin zigbuild cross-link from an x86_64 host, the attest step, the native x86_64 --version smoke, and gh attestation verify on a downloaded tarball. + +PR (opened by the orchestrator after fast-forwarding fix/1-hosted-release; the executor does NOT push or open a PR): +- Title: "ci: move release.yml to GitHub-hosted runners with SLSA provenance". +- The body must contain "Closes #1", the injection-scanner #45 precedent, the three planner additions (a)-(c) with their evidence, the verified pin table, and the not-verifiable-pre-merge statement above. + + + +- A vX.Y.Z tag on the public repo would schedule every release job; no self-hosted label remains in any workflow. +- Least privilege: read-only default, with write/OIDC scopes only on the release job; no org secret referenced. +- Signed SLSA provenance over every tarball and SHA256SUMS.txt, attested before the Release exists, with the verify command in the release notes. +- Every action in every workflow is pinned to a verified commit SHA. +- Asset contract identical: six targets, 4 required / 2 best-effort, same tarball shape, SHA256SUMS.txt, no Windows. +- Docs (CONTRIBUTING.md, README.md, local CLAUDE.md) match the shipped policy; CLAUDE.md is still untracked. + + + +Create `.planning/quick/261009-hc1-move-release-yml-to-github-hosted-runner/261009-hc1-SUMMARY.md` when done. In it, record: +- the release-side runner decision Phase 3 D-12 had deferred to Phase 6 is now resolved (hosted, issue #1); +- the three planner additions (a)-(c); +- that the CLAUDE.md edit is local-only (untracked file); +- the post-merge proof items owned by the Phase 6 release dry run; +- follow-ups: bump the dtolnay pin to a post-#187 master commit in agent-memory and injection-scanner together; consider porting injection-scanner's tag-must-match-Cargo.toml-version gate. + diff --git a/.planning/quick/261009-hc1-move-release-yml-to-github-hosted-runner/261009-hc1-SUMMARY.md b/.planning/quick/261009-hc1-move-release-yml-to-github-hosted-runner/261009-hc1-SUMMARY.md new file mode 100644 index 0000000..f7b6cc0 --- /dev/null +++ b/.planning/quick/261009-hc1-move-release-yml-to-github-hosted-runner/261009-hc1-SUMMARY.md @@ -0,0 +1,83 @@ +--- +phase: quick-261009-hc1 +plan: 01 +status: complete +completed: 2026-10-09 +requirements: [ISSUE-1] +commits: [ff9ab48, 043f444, 9810e31] +--- + +# Quick 261009-hc1: hosted release pipeline with SLSA provenance (closes #1) + +**`release.yml` now runs on GitHub-hosted runners, so a v0.1.0 tag can actually be scheduled.** +The repo is public and the org runner group enforces `allows_public_repositories: false`, so the +self-hosted jobs could never run. The workflow now also carries signed build provenance, gives +write and OIDC scopes to the release job only, and pins every action by SHA. + +## Commits + +| Commit | Change | +|---|---| +| `ff9ab48` | release.yml: all jobs on `ubuntu-latest`, `contents: read` default, the release job alone gets contents/id-token/attestations write, `actions/attest-build-provenance` over every tarball and SHA256SUMS.txt before the Release, all actions SHA-pinned, semver-only trigger, corrected header and release notes | +| `043f444` | ci.yml and spike-cross-compile.yml pinned to the same SHAs, with `toolchain: stable` passed explicitly | +| `9810e31` | CONTRIBUTING.md and README.md describe the real runner policy and the `gh attestation verify` command | + +## Decisions + +- **The release-side runner decision Phase 3 D-12 deferred to Phase 6 is resolved: hosted runners, issue #1.** +- Planner additions beyond the original request: + - **(a) Trigger narrowed from `v*` to `v[0-9]+.[0-9]+.[0-9]+`.** Run 29185695331 fired on the GSD tag + `v1.0-milestone`. On a hosted runner it would have published that tag as a real Release. + Side effect: pre-release tags such as `v0.1.0-rc.1` don't trigger either. Widen this in Phase 6 + if a pre-release dry run needs it. + - **(b) ci.yml and the spike use the same pins.** ci.yml runs on this PR, so it is the only + pre-merge proof that the checkout v7.0.1 and rust-toolchain pins work in this repo. + - **(c) CONTRIBUTING.md and README.md corrected.** CLAUDE.md is untracked, so the public copy of + the policy has to live in tracked docs. +- **CLAUDE.md was edited locally only.** The file is untracked (never committed, absent from + origin/main), so its corrected "CI / Self-Hosted Runners" section isn't part of the PR. +- Upgrades: checkout v5 → v7.0.1; upload-artifact v5 → v7.0.1 and download-artifact v5 → v8.0.1 + move together, as the matched pair proven in injection-scanner's releases. + +## Verification + +- Task 1 structural check: `release.yml structure OK`. This checks the trigger shape and + semver-only filter, the read-only default, the three-scope release permissions, all six matrix + legs unchanged, the step order SHA256SUMS → attest → release, both subject paths, the verify + line in the release body, a 40-hex pin on every `uses:`, and `toolchain: stable` on every + rust-toolchain step. +- `actionlint .github/workflows/*.yml`: clean. Before the change it reported three unknown + runner-label errors. +- Exactly three `runs-on: ubuntu-latest` lines; no self-hosted label in any workflow; no + `secrets.` expression; all six pin strings present. +- Task 2: every `uses:` across all three workflows is pinned with a version comment; 4 of 4 + rust-toolchain steps pass `toolchain: stable`. +- Task 3: all doc gates green, CLAUDE.md still untracked, and the branch touches only the five + intended tracked files. +- **One planned gate deliberately not met, and the reason.** The check "the diff removes no line + containing `tar.gz`" flags exactly one line: the old header comment "...adapted to tar.gz + packaging: ONE self-hosted Linux host". The plan itself orders that comment rewritten, and + another of its gates forbids "ONE self-hosted". No asset-contract, checksum, upload, `files:` + or release-table line changed. +- The executor step ran inline in the orchestrator session. The Agent isolation guard resolves + isolation against the orchestrating session's project (injection-scanner), not this repo, which + sets `use_worktrees: false`. + +## Not verifiable before merge (by design) + +`release.yml` runs only when a `vX.Y.Z` tag is pushed, and that tag-only trigger is the safety +property that justifies the exception. The PR's own ci.yml run is the only pre-merge evidence, and +it covers only the checkout and rust-toolchain pins. **The first hosted tag run is the Phase 6 +release dry run**, which must confirm: + +- the darwin zigbuild cross-link from an x86_64 host; +- the attest step; +- the native x86_64 `--version` smoke test; +- `gh attestation verify` on a downloaded tarball. + +## Follow-ups + +- Bump the `dtolnay/rust-toolchain` pin (4360b525, from 2026-08-05) to a commit after upstream + fix #187, in agent-memory and injection-scanner together. The risk is accepted for now because + every caller passes static inputs. +- Consider porting injection-scanner's gate that requires the tag to match the Cargo.toml version.