diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84e52aa..c18a985 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,12 +19,18 @@ permissions: jobs: account-unification-tests: if: ${{ github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) }} - runs-on: ubuntu-latest + # Fail closed until the isolated group has eligible disposable capacity. + # Do not substitute persistent control runners or a hosted fallback. + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] defaults: run: working-directory: services/account_unification steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" @@ -52,9 +58,15 @@ jobs: realm-config-validates: if: ${{ github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) }} - runs-on: ubuntu-latest + # Fail closed until the isolated group has eligible disposable capacity. + # Do not substitute persistent control runners or a hosted fallback. + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" @@ -72,9 +84,15 @@ jobs: compose-config-validates: if: ${{ github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) }} - runs-on: ubuntu-latest + # Fail closed until the isolated group has eligible disposable capacity. + # Do not substitute persistent control runners or a hosted fallback. + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Validate docker-compose run: docker compose -f docker-compose.yml config >/dev/null env: diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f68965d..243fa97 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -221,14 +221,21 @@ explicitly documented deployment-controller responsibility. ## Automation boundaries -- The hourly PR steward advances only trusted same-repository PRs with exact-head - approvals and required Checks. +- Protected PR maintenance belongs to the organization's central + `pr-review-merge-scheduler.yml`; Keyverse's local hourly steward was removed + in #140. Exact-head approvals and required Checks remain mandatory. The local + product-development workflow does not own review or merge authority. - The hourly product-development workflow runs OpenCode through `NVIDIA_NIM_API_KEY`, not Copilot Agent Tasks or `COPILOT_GITHUB_TOKEN`. - The model workspace has no Git metadata, GitHub credential, Actions OIDC, publication token, or upstream NIM credential. - Generated text patches are bounded, digest-sealed, independently verified on a fresh checkout, and published only as a draft PR. +- Repository `ci.yml` proposes the dedicated `CWL CI isolated` group plus + self-hosted/Linux/x64/isolation labels for all three credential-free CI jobs. + Checkout does not persist credentials. This source configuration is not proof + of registered disposable capacity, network isolation, cleanup, or executed + Checks; operator acceptance remains required before public PR execution. - Existing review agents and their credential system remain independent. - Neither automation path may self-approve, bypass protection, merge unverified work, tag, or publish a release. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4639f1a..65a0e1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,16 @@ Keep a Changelog, and releases use semantic versioning. ### Fixed +- Proposed dedicated-group isolated self-hosted routing for the three local CI + jobs after a billing lock prevented hosted job startup. Checkout no longer + persists its token; all test/coverage/build gates remain unchanged. Actual + isolated runner eligibility and execution remain rollout prerequisites. + +- Removed the obsolete local PR-steward tests after #140 centralized PR + maintenance. Added an ownership regression and aligned architecture and + operator guidance without restoring local merge automation or changing + production identity behavior. + - Prevented relying-party inventory from silently accepting a KV key/body identity mismatch, rejected unsafe live or `Location`-derived client UUIDs, and aligned exact client discovery with Keycloak's documented diff --git a/docs/doctoring/ci-isolated-runner-routing.md b/docs/doctoring/ci-isolated-runner-routing.md new file mode 100644 index 0000000..89606a3 --- /dev/null +++ b/docs/doctoring/ci-isolated-runner-routing.md @@ -0,0 +1,67 @@ +# Local CI isolated-runner routing + +**Date:** 2026-10-04 +**Status:** Proposed source routing; runtime eligibility and protected integration pending + +## Measured cause and bounded repair + +PR143 head `0066598098263bd0064e199c9ddb6279913e1ba0` Ready event produced +CI run `37183133580`. GraphQL check annotations for each of the three jobs +state that the job did not start because the account was locked for a billing +issue. That is pre-execution admission failure, not a product test failure. + +The existing organization self-hosted migration direction supplies a safe +source contract: `.github#2565` at `ab0c865989012c88d2c10c717f6649a76ea49e27` +uses dedicated group `CWL CI isolated` plus isolation/platform labels. The +current Keyverse proposal uses that exact group and Linux/x64 capability shape, +not a label-only match or privileged central control pool. All three check +names, command blocks, Python 3.12 contract, pins, permissions and event guards +are retained. Checkout explicitly disables credential persistence. + +Two source-contract tests failed before changes: hosted selector instead of +exact group/labels, and missing `persist-credentials: false`. Both pass after +changes. These static controls prove intended source selection, not successful +GitHub runner assignment or execution. Existing full gates must run on the +complete final candidate and hosted current head. + +## Authority and operation boundary + +GitHub documents groups as runner-access organization boundaries; labels narrow +capability selection within the group. Neither selected labels nor YAML proves +machine isolation. Public/fork PR code must not use persistent privileged +runners. `persist-credentials: false` prevents checkout from retaining its +read token for later Git use; it is not a claim that a runner is credential-free +or that trusted GitHub actions never receive their normal job token. + +Existing operator issue `linux-cluster-ops#326` retains disposable capacity, +private-service denial, clean-per-job state, registration custody, minimum +repository access, tool-image and real canary requirements. Its observed +credential-free point probes do not establish complete kernel-enforced denial +or a registered eligible pool. The proposal must remain Draft until those +operational boundaries and fresh current-head checks/review are satisfied. +No runner registration, relabeling, ACL expansion, billing purchase, credential +change, protected-gate weakening or release is performed by this source repair. + +The separate hourly product-development workflow is unchanged and is not part +of this three-job routing claim; do not claim organization-wide migration. + +## References — APA 7th + +GitHub. (n.d.). *Managing access to self-hosted runners using groups*. GitHub +Docs. Retrieved October 4, 2026, from +https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/manage-access + +GitHub. (n.d.). *Choosing the runner for a job*. GitHub Docs. Retrieved October +4, 2026, from +https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/choose-the-runner-for-a-job + +GitHub. (n.d.). *Checkout* [Source code documentation]. Retrieved October 4, +2026, from https://github.com/actions/checkout/blob/main/README.md + +ContextualWisdomLab. (2026). *All self-hosted runner routing* (.github PR #2565, +source snapshot `ab0c865989012c88d2c10c717f6649a76ea49e27`). +https://github.com/ContextualWisdomLab/.github/pull/2565 + +ContextualWisdomLab. (2026). *Register isolated ContextualWisdomLab Actions runner +on s1 (not b1-b5)* (linux-cluster-ops issue #326). +https://github.com/ContextualWisdomLab/linux-cluster-ops/issues/326 diff --git a/docs/doctoring/pr-governance-ownership.md b/docs/doctoring/pr-governance-ownership.md new file mode 100644 index 0000000..ce11635 --- /dev/null +++ b/docs/doctoring/pr-governance-ownership.md @@ -0,0 +1,88 @@ +# PR governance ownership after local steward retirement + +**Date:** 2026-10-03 +**Scope:** local verification of existing PR #143/#154 remediation; not protected promotion + +## Root cause and retained owner + +Protected source `7d9151cd2da260e118020c938c7358e2ee75d541` includes +#140's deletion of `.github/workflows/hourly-pr-steward.yml`, but retains five +tests that open that deleted file. The complete local account-unification +suite reproduced five `FileNotFoundError` failures before any repair. + +PR #154 at `85deb471a2e5de412e0f378eadd537750bb8476c` removes the obsolete +module. PR #143 at `dc2ca97ae7a33d4660d0179dd6050b57e9faa076` additionally +updates the operating guide. This local candidate reuses #143's two-path +repair and adds ownership regressions, architecture clarification, and this +record; it does not create another PR or adopt either remote branch's authority. +The operator wording is narrowed: observing central workflow source does not +prove its dispatch or effective protection on Keyverse. + +## Interpretation and regression boundary + +- **Repository policy:** central `pr-review-merge-scheduler.yml` owns PR + maintenance; local product authoring must not become another merge owner. +- **Vendor contract:** reusable workflows are called at job level and use + `workflow_call`. A reusable definition's existence is not execution evidence. +- **Protection boundary:** local tests do not replace required hosted Checks, + independent current-head review, unresolved-thread disposition, or protection. +- **Measured RED:** both new ownership tests failed before the repair: the + retired test remained and architecture did not name the central owner. +- **Measured GREEN:** the replacement tests pass after the repair. They check + that the retired workflow/test stays absent, the local product workflow stays + present, and current operating/architecture prose names central ownership. +- **Harness correction:** a case-sensitive `exact-head` assertion rejected + sentence-initial `Exact-head`; only the assertion was made case-insensitive. +- **Review correction:** independent review rejected the initial candidate + because publication-race guidance still depended on a subsequent hourly + steward. A third test reproduced that contradiction before prose was changed + to the normal protected PR path under central governance. The replacement + makes no claim that central dispatch has executed. +- **Hosted review sensitivity correction (2026-10-04):** CodeRabbit noted + that central-owner wording could coexist with the former statement that the + hourly PR steward advances trusted PRs. Two copied-document controls first + passed unchanged guidance, then restored that contradiction; both initially + failed because the contract did not reject it. The contract now rejects the + exact obsolete active-owner statement in both documents while preserving + historical retirement references. These controls do not claim general + natural-language contradiction detection or central execution evidence. +- **Non-goals:** no production Python, credential, workflow, review gate, + passwordless policy, SCIM contract, or release version is changed. This is + static ownership and local regression evidence, not live login, Keycloak, + PostgreSQL, or central scheduler acceptance. + +## Inventory and product-priority decision + +Read-only GitHub inventory on 2026-10-03 returned 29 distinct open PRs and +11 distinct issues (the Issues API also returns PRs, which were excluded). +PRD-FR-001 through PRD-FR-010 and gap G0 retain passwordless, verified-email, +side-effect-free preflight, exact reconciliation, and protected queue rules. +The baseline's August inventory is historical, not today's queue. + +The existing CI repair takes priority over opening a new product-gap proposal. +Key-vault, authorization, consumer subject/signer trust, and immutable release +requests (#152, #102/#103, #155, #156, #158, #160) remain separate unresolved +owner scopes; this repair does not claim to satisfy them. Likewise issue #131's +orchestrator routing work remains on its existing candidate paths. + +GitHub CLI authentication failed. Public API/browser reads and normal git fetch +worked without changing credentials. No remote write, approval, merge, release, +production deployment, or credential migration was performed. + +## References — APA 7th + +GitHub. (n.d.). *Reuse workflows*. GitHub Docs. Retrieved October 3, 2026, from +https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows + +GitHub. (n.d.). *About protected branches*. GitHub Docs. Retrieved October 3, +2026, from +https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches + +ContextualWisdomLab. (2026). *Remove redundant hourly-pr-steward workflow* +(Keyverse PR #140; commit `d8ff4eded4f82ad5e72deec940cd73e1583b4640`). +https://github.com/ContextualWisdomLab/keyverse/pull/140 + +ContextualWisdomLab. (2026). *Remove stale hourly-pr-steward test left behind by +#140* (Keyverse PR #143; reviewed source snapshot +`dc2ca97ae7a33d4660d0179dd6050b57e9faa076`). +https://github.com/ContextualWisdomLab/keyverse/pull/143 diff --git a/docs/operations/hourly-product-development.md b/docs/operations/hourly-product-development.md index abb366a..8449322 100644 --- a/docs/operations/hourly-product-development.md +++ b/docs/operations/hourly-product-development.md @@ -1,12 +1,14 @@ # Hourly product-development loop Keyverse separates protected pull-request maintenance from autonomous product -development. The schedules are offset so the merge loop has time to settle the -repository before a new product slice is considered. +development. Protected PR maintenance is owned by the organization's central +`pr-review-merge-scheduler.yml`, not a second Keyverse-local merge loop. +Keyverse's former hourly steward was removed in #140. Current-head review, +required Checks, and branch protection remain mandatory; local test success +alone does not prove central dispatch, approval, or merge readiness. | Minute (UTC) | Workflow | Responsibility | | --- | --- | --- | -| `17 * * * *` | `hourly-pr-steward.yml` | Update trusted PR branches, require approval and required Checks, then arm exact-head auto-merge. | | `41 * * * *` | `hourly-product-development.yml` | When the PR queue is empty and exact `main` is healthy, use OpenCode with NVIDIA NIM to produce one bounded buyer-visible draft PR. | The development scheduler never approves or merges its own work and never @@ -14,6 +16,22 @@ publishes a release. The existing review-agent workflows and their credentials remain unchanged. Review, repair, revalidation, and merge stay owned by the normal protected PR path. +## Local CI admission prerequisite + +The local `ci.yml` routes its three jobs through the dedicated `CWL CI isolated` +runner group with `self-hosted`, `linux`, `x64`, and `cwlab-ci-isolated` labels. +There is no hosted fallback or persistent control-runner substitution, and each +checkout uses `persist-credentials: false`. The commands, Python 3.12 contract, +coverage thresholds, action pins, and Draft/closed-PR admission remain unchanged. + +This is proposed source routing, not activated capacity. Before public PR jobs +execute, the existing isolated-runner operator must prove selected-repository +eligibility, disposable per-job state, host/private-network denial, tool-image +acceptance, and cleanup. Central `.github#2565` and `linux-cluster-ops#326` retain +those existing owner gates. Do not relabel privileged runners, widen access, +change billing, or reuse a token merely to drain the queue. Required Checks and +independent current-head approval remain mandatory for protected merge. + ## Architecture The workflow uses three jobs with different trust levels. @@ -199,8 +217,10 @@ a fresh checkout. It creates one run-unique branch named Workflow concurrency serializes scheduled runs, but GitHub does not provide an atomic compare-base-and-create-PR operation. If another actor opens a PR in the -final network interval, branch protection and the subsequent hourly steward -remain authoritative. During a duplicate-publication incident, revoke +final network interval, branch protection and the normal protected PR path +under central PR governance remain authoritative. This does not establish that +central dispatch has executed for the new head. During a duplicate-publication +incident, revoke `OPENCODE_PRODUCT_DEVELOPMENT_TOKEN`, close all but one draft, preserve the Actions logs and artifacts, add a reproducing contract test, and only then restore the token. diff --git a/services/account_unification/tests/test_ci_isolated_runner_contract.py b/services/account_unification/tests/test_ci_isolated_runner_contract.py new file mode 100644 index 0000000..793f60e --- /dev/null +++ b/services/account_unification/tests/test_ci_isolated_runner_contract.py @@ -0,0 +1,32 @@ +"""Fail-closed routing contracts for the repository's credential-free CI.""" +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[3] + + +def test_product_ci_uses_only_the_dedicated_isolated_runner_group() -> None: + """Public PR code must never run on persistent privileged control capacity.""" + workflow = yaml.safe_load((ROOT / ".github/workflows/ci.yml").read_text()) + assert set(workflow["jobs"]) == { + "account-unification-tests", "realm-config-validates", "compose-config-validates" + } + assert workflow["permissions"] == {"contents": "read"} + for job in workflow["jobs"].values(): + assert job["runs-on"] == { + "group": "CWL CI isolated", + "labels": ["self-hosted", "linux", "x64", "cwlab-ci-isolated"], + } + + +def test_isolated_ci_checkout_does_not_persist_its_read_token() -> None: + """Checked-out PR test code must not inherit a stored GitHub credential.""" + workflow = yaml.safe_load((ROOT / ".github/workflows/ci.yml").read_text()) + for job in workflow["jobs"].values(): + checkouts = [ + step for step in job["steps"] + if str(step.get("uses", "")).startswith("actions/checkout@") + ] + assert len(checkouts) == 1 + assert checkouts[0].get("with", {}).get("persist-credentials") is False diff --git a/services/account_unification/tests/test_hourly_pr_steward.py b/services/account_unification/tests/test_hourly_pr_steward.py deleted file mode 100644 index 910133e..0000000 --- a/services/account_unification/tests/test_hourly_pr_steward.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Static contract tests for the hourly protected PR steward.""" -from __future__ import annotations - -from pathlib import Path - - -def _workflow_source() -> str: - """Return the repository's hourly PR stewardship workflow source.""" - repository_root = Path(__file__).resolve().parents[3] - return ( - repository_root / ".github" / "workflows" / "hourly-pr-steward.yml" - ).read_text(encoding="utf-8") - - -def _permissions_block(source: str, marker: str, terminator: str) -> str: - """Return one indentation-sensitive workflow permissions block.""" - block_start = source.index(marker) - block_end = source.index(terminator, block_start) - return source[block_start:block_end] - - -def test_hourly_steward_runs_once_per_hour_with_bounded_concurrency() -> None: - """The schedule is hourly and overlapping steward runs are serialized.""" - workflow = _workflow_source() - assert 'cron: "17 * * * *"' in workflow - assert "group: hourly-pr-steward" in workflow - assert "cancel-in-progress: false" in workflow - assert "timeout-minutes: 10" in workflow - - -def test_hourly_steward_uses_read_only_workflow_token_defaults() -> None: - """Only the steward job receives its narrowly required write scopes.""" - workflow = _workflow_source() - top_level_permissions = _permissions_block( - workflow, - "permissions:\n", - "\nconcurrency:", - ) - job_permissions = _permissions_block( - workflow, - " permissions:\n", - " steps:", - ) - - assert "contents: read" in top_level_permissions - assert "write" not in top_level_permissions - assert "contents: write" in job_permissions - assert "pull-requests: write" in job_permissions - assert "checks: read" in job_permissions - assert "security-events: write" not in workflow - assert "actions: write" not in workflow - - -def test_hourly_steward_is_fail_closed_on_trust_review_and_checks() -> None: - """Untrusted, unapproved, pending, or failed pull requests remain untouched.""" - workflow = _workflow_source() - assert 'head_owner" != "ContextualWisdomLab"' in workflow - assert 'trusted_author" != "true"' in workflow - assert 'review_decision" != "APPROVED"' in workflow - assert 'gh pr checks "$number" --repo "$REPOSITORY" --required' in workflow - assert "--admin" not in workflow - - -def test_hourly_steward_invalidates_old_evidence_after_branch_update() -> None: - """A branch update exits the current iteration before merging stale evidence.""" - workflow = _workflow_source() - update_position = workflow.index("gh pr update-branch") - continue_position = workflow.index("continue", update_position) - approval_position = workflow.index('review_decision" != "APPROVED"') - assert update_position < continue_position < approval_position - - -def test_hourly_steward_binds_auto_merge_to_the_checked_head() -> None: - """GitHub auto-merge is armed only for the enumerated exact head SHA.""" - workflow = _workflow_source() - assert '--auto \\' in workflow - assert '--squash \\' in workflow - assert '--match-head-commit "$head_sha"' in workflow diff --git a/services/account_unification/tests/test_pr_governance_ownership.py b/services/account_unification/tests/test_pr_governance_ownership.py new file mode 100644 index 0000000..8a977c8 --- /dev/null +++ b/services/account_unification/tests/test_pr_governance_ownership.py @@ -0,0 +1,75 @@ +"""Guard the boundary between central PR governance and local development.""" +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[3] + + +def test_retired_local_steward_has_no_source_or_test_owner() -> None: + """Do not restore an obsolete workflow or tests that require its source.""" + assert not (ROOT / ".github/workflows/hourly-pr-steward.yml").exists() + assert not ( + ROOT / "services/account_unification/tests/test_hourly_pr_steward.py" + ).exists() + assert (ROOT / ".github/workflows/hourly-product-development.yml").is_file() + + +def test_operations_and_architecture_name_central_pr_governance() -> None: + """Operator guidance must distinguish central review from local authoring.""" + for path in ("ARCHITECTURE.md", "docs/operations/hourly-product-development.md"): + text = " ".join((ROOT / path).read_text(encoding="utf-8").split()) + assert "`pr-review-merge-scheduler.yml`" in text, path + assert "exact-head" in text.lower(), path + assert ( + "the hourly pr steward advances only trusted same-repository prs" + not in text.lower() + ), path + operations = (ROOT / "docs/operations/hourly-product-development.md").read_text( + encoding="utf-8" + ) + assert "| `17 * * * *` |" not in operations + assert "| `41 * * * *` | `hourly-product-development.yml` |" in operations + + +def test_publication_races_do_not_depend_on_retired_hourly_steward() -> None: + """Race recovery must use the surviving protected PR governance path.""" + text = " ".join( + (ROOT / "docs/operations/hourly-product-development.md") + .read_text(encoding="utf-8").split() + ) + race_section = text.split("## Publication and race handling", 1)[1].split( + "## First activation", 1 + )[0] + assert "hourly steward" not in race_section + assert "protected PR path" in race_section + assert "central PR governance" in race_section + + +@pytest.mark.parametrize( + "document", ["ARCHITECTURE.md", "docs/operations/hourly-product-development.md"] +) +def test_ownership_regression_rejects_restored_steward_prose( + tmp_path, monkeypatch, document +) -> None: + """Contradictory ownership must fail even when central guidance remains.""" + paths = ("ARCHITECTURE.md", "docs/operations/hourly-product-development.md") + for path in paths: + target = tmp_path / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text((ROOT / path).read_text(encoding="utf-8"), encoding="utf-8") + monkeypatch.setattr(sys.modules[__name__], "ROOT", tmp_path) + # The unchanged positive contract must pass before the contradiction is added. + test_operations_and_architecture_name_central_pr_governance() + target = tmp_path / document + target.write_text( + target.read_text(encoding="utf-8") + + "\nThe hourly PR steward advances only trusted same-repository PRs " + "with exact-head approvals and required Checks.\n", + encoding="utf-8", + ) + with pytest.raises(AssertionError): + test_operations_and_architecture_name_central_pr_governance()