diff --git a/.github/workflows/v2-ci-ordinary-settlement.yml b/.github/workflows/v2-ci-ordinary-settlement.yml index 2b83903..6864875 100644 --- a/.github/workflows/v2-ci-ordinary-settlement.yml +++ b/.github/workflows/v2-ci-ordinary-settlement.yml @@ -1,4 +1,4 @@ -name: V2 ordinary post-merge settlement (source only) +name: V2 ordinary post-merge settlement on: push: @@ -17,11 +17,11 @@ concurrency: jobs: preflight: name: ordinary-v2-secret-free-preflight - # Net remains inert until an immutable net-v1 CLI release and - # dedicated custody are installed and read back in one later source change. - if: ${{ false }} runs-on: ubuntu-latest timeout-minutes: 5 + outputs: + receipt_sha256: ${{ steps.receipt.outputs.sha256 }} + activation: ${{ steps.receipt.outputs.activation }} steps: - name: Check out exact protected source without persisted credentials uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -39,6 +39,10 @@ jobs: run: | set -euo pipefail python3 tools/v2-ci-ordinary-observe.py produce "$RUNNER_TEMP/ordinary-v2-preflight.json" + digest="$(sha256sum "$RUNNER_TEMP/ordinary-v2-preflight.json" | cut -d ' ' -f 1)" + activation="$(python3 -c 'import json,sys; print(str(json.load(open(sys.argv[1]))["activation"]).lower())' "$RUNNER_TEMP/ordinary-v2-preflight.json")" + echo "sha256=$digest" >> "$GITHUB_OUTPUT" + echo "activation=$activation" >> "$GITHUB_OUTPUT" - name: Retain public exact-run receipt uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: @@ -46,3 +50,79 @@ jobs: path: ${{ runner.temp }}/ordinary-v2-preflight.json retention-days: 30 if-no-files-found: error + + settle: + name: ordinary-v2-credential-settlement + needs: [preflight] + # Only the exact secret-free predecessor receipt can admit this job. + if: needs.preflight.outputs.activation == 'true' + environment: ordinary-v2 + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + PACKAGE_VERSION: 0.1.5 + # Immutable v0.1.5 GitHub release archive, independently read back before activation. + PACKAGE_SHA256: 3567a92825917a7d537f6c5c545d3a7947bc35edd666fc3a1898de3bf97267c9 + permissions: + actions: read + contents: read + checks: read + pull-requests: read + steps: + - name: Check out exact protected Net source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: false + - name: Download only this run's predecessor receipt + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: ordinary-v2-preflight-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/ordinary-v2-receipt + - name: Recheck the receipt and current public authority + shell: bash + env: + GH_TOKEN: ${{ github.token }} + EXPECTED_WORKFLOW_SHA: ${{ github.workflow_sha }} + EXPECTED_RECEIPT_SHA256: ${{ needs.preflight.outputs.receipt_sha256 }} + FSGG_V2_SOURCE_PROFILE: net-v1 + run: | + set -euo pipefail + receipt="$RUNNER_TEMP/ordinary-v2-receipt/ordinary-v2-preflight.json" + test "$(sha256sum "$receipt" | cut -d ' ' -f 1)" = "$EXPECTED_RECEIPT_SHA256" + python3 tools/v2-ci-ordinary-observe.py verify "$receipt" + - name: Set up the pinned .NET SDK + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 + with: + global-json-file: global.json + - name: Install only the pinned public Coordination CLI archive + shell: bash + run: | + set -euo pipefail + package_dir="$RUNNER_TEMP/ordinary-v2-package" + mkdir "$package_dir" + package="$package_dir/FS.GG.Coordination.Cli.$PACKAGE_VERSION.nupkg" + curl --fail --location --silent --show-error \ + "https://github.com/FS-GG/FS.GG.Coordination/releases/download/v$PACKAGE_VERSION/FS.GG.Coordination.Cli.$PACKAGE_VERSION.nupkg" \ + --output "$package" + test "$(sha256sum "$package" | cut -d ' ' -f 1)" = "$PACKAGE_SHA256" + config="$RUNNER_TEMP/ordinary-v2-local-only.config" + cat > "$config" < + CFG + dotnet tool install FS.GG.Coordination.Cli --version "$PACKAGE_VERSION" \ + --tool-path "$RUNNER_TEMP/ordinary-v2-cli" --configfile "$config" --no-cache + - name: Execute exactly one installed Net settlement attempt + shell: bash + env: + GH_TOKEN: ${{ github.token }} + EXPECTED_WORKFLOW_SHA: ${{ github.workflow_sha }} + FSGG_V2_SOURCE_PROFILE: net-v1 + FSGG_V2_PREFLIGHT_RECEIPT: ${{ runner.temp }}/ordinary-v2-receipt/ordinary-v2-preflight.json + V2_ORDINARY_APP_ID: ${{ secrets.V2_ORDINARY_APP_ID }} + V2_ORDINARY_APP_PRIVATE_KEY: ${{ secrets.V2_ORDINARY_APP_PRIVATE_KEY }} + V2_ORDINARY_AUTHORIZER_PRIVATE_KEY: ${{ secrets.V2_ORDINARY_AUTHORIZER_PRIVATE_KEY }} + run: | + set -euo pipefail + "$RUNNER_TEMP/ordinary-v2-cli/fsgg-coordination" ordinary-settlement execute diff --git a/docs/roadmaps/v2-ordinary-adoption.md b/docs/roadmaps/v2-ordinary-adoption.md index b1e36cb..7d346c3 100644 --- a/docs/roadmaps/v2-ordinary-adoption.md +++ b/docs/roadmaps/v2-ordinary-adoption.md @@ -1,7 +1,7 @@ # C3-NET-01 — Ordinary V2 receiver adoption -Status: source prepared and disabled. Dedicated custody is enrolled; CLI release, installation, and -activation remain pending. +Status: activation source prepared for the 0.1.5 CLI. Dedicated custody is enrolled. Installed +operation and its post-merge settlement proof remain pending until this change merges. FS.GG.Net is the fixed C3 source repository (`FS-GG/FS.GG.Net`, repository ID `1305845505`) under the code-owned `net-v1` profile. This change adds only repository-owned @@ -10,9 +10,10 @@ check, generated workspace content, or protected effect. ## Prepared source -- The receiver workflow is bound to protected-main pushes, but its only job has an unconditional - false guard. It uses read-only GitHub permissions, persists no checkout credential, and contains - no credential job, environment binding, secret reference, package download, or settlement command. +- The receiver workflow is bound to protected-main pushes. Its read-only, secret-free preflight + produces an exact-run receipt. The bounded credential job runs only when that receipt admits + activation, rechecks current Authority, verifies the pinned public package archive, and uses + only the dedicated `ordinary-v2` secrets. Both checkouts persist no credential. - The source pattern comes from Rendering receiver commit `8f4acd853566ea287abd15655c1aa5c4d3ceb403`; the observer retains its repaired Audio bytes, and the qualifier replaces only the code-owned source profile. Their SHA-256 digests are @@ -30,26 +31,57 @@ check, generated workspace content, or protected effect. - The shared policy ID remains `v2-ci-i1-ordinary-settlement-v1`; the shared Authority anchor retains App `5064713`, installation `164553252`, repository `FS-GG/FS.GG.Coordination.Authority` (`1351660651`), `contents:write`, metadata read, and the existing writer/integrity ruleset pins. -- Read-only API observation at `2026-09-28T12:22:05Z`, against Net main - `2fe9c00976b5d01c36fbd587c21135b650cf43b9`, found the `ordinary-v2` environment as ID +- Read-only API observation at `2026-09-28T15:00:36Z`, against Net main + `dfc04d994e955842a7e16597f7093ed14f1b5251`, found the `ordinary-v2` environment as ID `22920188172`, restricted to the single `main` branch policy ID `61287584`, with no reviewers. Protected custody bridge run `36419999006` succeeded and the environment now reads back the exact three dedicated ordinary-v2 secret names. -- No immutable published CLI release with `net-v1` support is selected. Version and package - SHA-256 remain null, and policy explicitly refuses activation until a served package digest is - independently verified. -- Net already pins .NET SDK `10.0.401` in the repository's tracked `global.json`. This - receiver leaves that pin unchanged and invokes no .NET setup while disabled. +- The immutable public `v0.1.5` release package pin is SHA-256 + `3567a92825917a7d537f6c5c545d3a7947bc35edd666fc3a1898de3bf97267c9`, from reviewed + Coordination source and exact tag `1268908d2d5a38d30a764c927f3e0591e53138aa`. The public + release asset matches the prepared archive byte for byte. Its readback records an identical + GitHub Packages archive and an identical nuget.org payload after NuGet signing; anonymous + public-only install and invocation passed before this activation source was pushed. +- Net already pins .NET SDK `10.0.401` in the repository's tracked `global.json`. The credential + job uses that pin after receipt and Authority verification. ## Installation boundary -Do not enable the preflight or add a credential job until one reviewed source change verifies all of: +This activation source was held locally until one reviewed change verified all of: 1. an immutable published Coordination CLI supports the exact `net-v1` source profile and its served package SHA-256 is pinned; 2. Net identity, exact current required-check population, producer mappings, and shared Authority binding are freshly read back. -The later activation must change policy status, installed state, package evidence, observer guard, -and the bounded credential job together. This disabled source cannot settle work and imports no V1 -admission or receiver state. +This candidate changes policy status, installed state, package evidence, observer guard, and the +bounded credential job together. It imports no V1 admission or receiver state. + +## Activation handoff (prepared on 2026-09-28) + +This activation candidate becomes one reviewed PR based on protected `main` only after the +public `FS.GG.Coordination.Cli` 0.1.5 release asset has its SHA-256 verified and the published +package implements `net-v1`. The package pin must identify that exact digest and source commit; +a local build or an intended release version is insufficient. + +Refresh Net's repository ID, protected-main head, required check names and App IDs, workflow IDs and +paths, and the `ordinary-v2` environment branch policy and three dedicated secret *names* from the +native API. Re-read the shared Authority binding against its current source. On 2026-09-28 the Net +readback matched repository ID `1305845505`, four required GitHub Actions App `15368` contexts, +workflow IDs `316245439`, `316890379`, `316890380`, environment ID `22920188172`, its sole `main` +branch policy ID `61287584`, and exactly the three names recorded above. These observations are a +baseline, not permission to use stale values at activation. + +The candidate enables the secret-free preflight, passes its receipt digest and activation result +to the `ordinary-v2` credential job, checks the public CLI archive SHA-256 before local-only +installation, and rechecks the same-run receipt and current protected policy, workflow, and anchor +before a settlement attempt. The policy and source tests bind the installed state, package pin, +receipt fence, exact secret inventory, and absence of request or manual trigger paths together. + +The clean source gate for this repository is the two Python receiver test files followed by +`dotnet restore FS.GG.Net.slnx --locked-mode`, `dotnet build FS.GG.Net.slnx -c Debug --no-restore`, +and `dotnet test FS.GG.Net.slnx -c Debug --no-build --no-restore`. If the shared NuGet cache raises +`NU1403` for `FSharp.Core 10.1.401`, use a fresh task-specific `NUGET_PACKAGES` directory; the +isolated locked restore, build, and both test assemblies passed at this handoff. Preserve all four +native required checks, merge the exact green PR head, read back the merged Authority, and verify +the ordinary `AlreadyComplete` rerun behavior before recording installed operation. diff --git a/policy/v2-ci-ordinary-settlement.json b/policy/v2-ci-ordinary-settlement.json index 11727cd..5ff59ff 100644 --- a/policy/v2-ci-ordinary-settlement.json +++ b/policy/v2-ci-ordinary-settlement.json @@ -1,7 +1,7 @@ { "schema": "fsgg.github.v2-ci-ordinary-settlement-policy/1", "policyId": "v2-ci-i1-ordinary-settlement-v1", - "status": "source-qualified-not-installed", + "status": "installed", "repository": "FS-GG/FS.GG.Net", "repositoryId": 1305845505, "selectedSource": { @@ -84,9 +84,9 @@ "allowedBranches": [ "main" ], - "installed": false, + "installed": true, "liveObservation": { - "observedAt": "2026-09-28T12:22:05Z", + "observedAt": "2026-09-28T15:00:36Z", "environmentPresent": true, "environmentId": 22920188172, "environmentNodeId": "EN_kwDOTdWfAc8AAAAFViZRDA", @@ -104,17 +104,19 @@ "V2_ORDINARY_APP_PRIVATE_KEY", "V2_ORDINARY_AUTHORIZER_PRIVATE_KEY" ], - "disposition": "dedicated-custody-enrolled-release-pending" + "disposition": "dedicated-custody-enrolled" } }, "packagePin": { - "status": "awaiting-published-net-profile-release", - "version": null, - "sha256": null, - "servedPackageVerified": false, + "status": "published-verified", + "version": "0.1.5", + "sha256": "3567a92825917a7d537f6c5c545d3a7947bc35edd666fc3a1898de3bf97267c9", + "servedPackageVerified": true, + "sourceCommit": "1268908d2d5a38d30a764c927f3e0591e53138aa", + "releaseTag": "v0.1.5", + "publisherRunId": 36437778484, "requiredCapability": "net-v1 selected source", - "requiredBeforeActivation": true, - "refusal": "no immutable published CLI release asset with net-v1 support or verified served SHA-256 is selected" + "requiredBeforeActivation": true }, "credentialInventory": [ { @@ -153,8 +155,8 @@ "readBackAt": "2026-09-28T12:22:05Z" }, "activationEvidence": { - "observedAt": "2026-09-28T11:55:40Z", - "sourceMainSha": "2fe9c00976b5d01c36fbd587c21135b650cf43b9", + "observedAt": "2026-09-28T15:00:36Z", + "sourceMainSha": "dfc04d994e955842a7e16597f7093ed14f1b5251", "sourceRepositoryId": 1305845505, "requiredCheckAppId": 15368, "requiredGateChecks": [ @@ -165,8 +167,8 @@ ] }, "activationPrerequisites": [ - "published immutable Coordination CLI with net-v1 support and verified package SHA-256", - "current source-check population and shared Authority binding are read back again" + "published immutable Coordination CLI with net-v1 support and verified package SHA-256: satisfied by 0.1.5 release and public feed readback", + "current source-check population and shared Authority binding are read back again before activation" ], "forbiddenCredentialReuse": [ "CALLABLE_ISOLATED_OPERATION_APP_PRIVATE_KEY", diff --git a/tests/v2-ci-ordinary-observe/run.py b/tests/v2-ci-ordinary-observe/run.py index ac517c9..323a80e 100644 --- a/tests/v2-ci-ordinary-observe/run.py +++ b/tests/v2-ci-ordinary-observe/run.py @@ -95,27 +95,33 @@ def api(path): with patch.object(MODULE, "api", side_effect=api): MODULE.current_authority("FS-GG/FS.GG.Net", policy) - def test_workflow_is_hard_disabled_and_has_no_credential_or_package_surface(self): + def test_workflow_prepares_bounded_settlement_with_exact_receipt_guard(self): workflow = (ROOT / ".github/workflows/v2-ci-ordinary-settlement.yml").read_text() self.assertIn(" push:\n branches: [main]", workflow) - self.assertIn(" if: ${{ false }}", workflow) + self.assertNotIn(" if: ${{ false }}", workflow) + self.assertIn("if: needs.preflight.outputs.activation == 'true'", workflow) + self.assertIn("environment: ordinary-v2", workflow) self.assertIn("FSGG_V2_SOURCE_PROFILE: net-v1", workflow) self.assertIn("persist-credentials: false", workflow) self.assertIn("python3 tools/v2-ci-ordinary-observe.py produce", workflow) - for forbidden in ( - "secrets.", "environment:", "ordinary-settlement execute", "PACKAGE_VERSION", - "PACKAGE_SHA256", "setup-dotnet", "global.json", "workflow_dispatch:", - "repository_dispatch:", "pull_request:", "pull_request_target:", - ): + self.assertIn("python3 tools/v2-ci-ordinary-observe.py verify", workflow) + self.assertIn("PACKAGE_VERSION: 0.1.5", workflow) + self.assertIn("PACKAGE_SHA256: 3567a92825917a7d537f6c5c545d3a7947bc35edd666fc3a1898de3bf97267c9", workflow) + self.assertIn("https://github.com/FS-GG/FS.GG.Coordination/releases/download/v$PACKAGE_VERSION/FS.GG.Coordination.Cli.$PACKAGE_VERSION.nupkg", workflow) + self.assertNotIn("api.nuget.org/v3-flatcontainer", workflow) + self.assertIn("ordinary-settlement execute", workflow) + for name in ("V2_ORDINARY_APP_ID", "V2_ORDINARY_APP_PRIVATE_KEY", "V2_ORDINARY_AUTHORIZER_PRIVATE_KEY"): + self.assertIn("${{ secrets." + name + " }}", workflow) + for forbidden in ("workflow_dispatch:", "repository_dispatch:", "pull_request:", "pull_request_target:", "V1_ADMISSION", "CALLABLE_ISOLATED_OPERATION"): self.assertNotIn(forbidden, workflow) - def test_policy_anchor_environment_and_unresolved_package_are_bounded(self): + def test_policy_anchor_environment_and_published_package_are_bounded(self): policy = json.loads((ROOT / "policy/v2-ci-ordinary-settlement.json").read_text()) anchor = json.loads((ROOT / "policy/v2-ci-ordinary-settlement-anchor.json").read_text()) self.assertEqual("v2-ci-i1-ordinary-settlement-v1", policy["policyId"]) self.assertEqual(policy["policyId"], anchor["policyId"]) - self.assertEqual("source-qualified-not-installed", policy["status"]) - self.assertFalse(policy["credentialJob"]["installed"]) + self.assertEqual("installed", policy["status"]) + self.assertTrue(policy["credentialJob"]["installed"]) observation = policy["credentialJob"]["liveObservation"] self.assertEqual(22920188172, observation["environmentId"]) self.assertEqual(61287584, observation["branchPolicyId"]) @@ -126,11 +132,12 @@ def test_policy_anchor_environment_and_unresolved_package_are_bounded(self): "V2_ORDINARY_APP_PRIVATE_KEY", "V2_ORDINARY_AUTHORIZER_PRIVATE_KEY", }, set(observation["secretNames"])) - self.assertEqual("awaiting-published-net-profile-release", + self.assertEqual("published-verified", policy["packagePin"]["status"]) - self.assertIsNone(policy["packagePin"]["version"]) - self.assertIsNone(policy["packagePin"]["sha256"]) - self.assertFalse(policy["packagePin"]["servedPackageVerified"]) + self.assertEqual("0.1.5", policy["packagePin"]["version"]) + self.assertEqual("3567a92825917a7d537f6c5c545d3a7947bc35edd666fc3a1898de3bf97267c9", + policy["packagePin"]["sha256"]) + self.assertTrue(policy["packagePin"]["servedPackageVerified"]) self.assertEqual(3, len(policy["credentialInventory"])) self.assertTrue(all(item["provisioned"] for item in policy["credentialInventory"])) self.assertEqual(5064713, anchor["writer"]["appId"]) diff --git a/tests/v2-ci-ordinary-qualification/run.py b/tests/v2-ci-ordinary-qualification/run.py index c0a6165..7fea65c 100644 --- a/tests/v2-ci-ordinary-qualification/run.py +++ b/tests/v2-ci-ordinary-qualification/run.py @@ -141,17 +141,18 @@ def test_failed_incomplete_stale_and_foreign_evidence_refuse(self): with self.assertRaisesRegex(MODULE.Refusal, "wrong repository"): self.qualify(associations=associations) - def test_policy_remains_disabled_with_explicit_release_and_custody_refusals(self): - self.assertEqual("source-qualified-not-installed", self.policy["status"]) - self.assertFalse(self.policy["credentialJob"]["installed"]) + def test_policy_installs_only_the_verified_net_profile_release(self): + self.assertEqual("installed", self.policy["status"]) + self.assertTrue(self.policy["credentialJob"]["installed"]) self.assertEqual(3, self.policy["credentialJob"]["liveObservation"]["secretCount"]) self.assertEqual(3, len(self.policy["credentialInventory"])) - self.assertEqual("awaiting-published-net-profile-release", - self.policy["packagePin"]["status"]) - self.assertIsNone(self.policy["packagePin"]["version"]) - self.assertIsNone(self.policy["packagePin"]["sha256"]) - self.assertIn("no immutable published CLI release", - self.policy["packagePin"]["refusal"]) + pin = self.policy["packagePin"] + self.assertEqual("published-verified", pin["status"]) + self.assertEqual("0.1.5", pin["version"]) + self.assertEqual("3567a92825917a7d537f6c5c545d3a7947bc35edd666fc3a1898de3bf97267c9", + pin["sha256"]) + self.assertEqual("1268908d2d5a38d30a764c927f3e0591e53138aa", pin["sourceCommit"]) + self.assertTrue(pin["servedPackageVerified"]) self.assertEqual(["OpenV2"], self.policy["unchangedGates"]) self.assertEqual({"v1Admission": False, "receiverStateImport": False}, self.policy["migration"])