diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000000..dbd43187c2 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,5 @@ +# Source validation only; labels do not attest runtime isolation. +self-hosted-runner: + labels: + - cwlab-ci-isolated + - cwlab-control diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 356244f7fc..1be7ae0449 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -60,7 +60,9 @@ jobs: name: Detect CodeQL languages # Draft PRs get no runner; ready_for_review re-runs this on the same head. if: (github.event.action != 'closed') && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '"ubuntu-24.04"') }} + runs-on: + group: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read @@ -164,7 +166,9 @@ jobs: # dependency exactly; the only case where it's genuinely skipped is a # closed PR, where this job being implicitly skipped too is fine because # closed PRs need no required check. - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '"ubuntu-24.04"') }} + runs-on: + group: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} # Verdict reads have exceeded five minutes; retain the ten-minute control budget. timeout-minutes: 10 permissions: @@ -367,7 +371,9 @@ jobs: && github.event.pull_request.state != 'closed' && needs.detect-languages.result == 'success' && needs.detect-languages.outputs.code == 'true' - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '"ubuntu-24.04"') }} + runs-on: + group: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read diff --git a/.github/workflows/package-description-boundary.yml b/.github/workflows/package-description-boundary.yml index 1bd4cc9926..8241893628 100644 --- a/.github/workflows/package-description-boundary.yml +++ b/.github/workflows/package-description-boundary.yml @@ -61,7 +61,11 @@ permissions: jobs: build-distribution: - runs-on: ubuntu-latest + # Caller-controlled build backends run only on the disposable isolated pool. + # Never fall back to a GitHub-hosted runner for untrusted package code. + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 10 steps: - name: Check out the calling repository @@ -127,7 +131,11 @@ jobs: package-description-boundary: needs: build-distribution if: ${{ always() }} - runs-on: ubuntu-latest + # The trusted inspector also stays on the isolated pool. Missing capacity + # must remain an incomplete check, never a hosted fallback or silent pass. + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 10 steps: - name: Reject a failed input producer diff --git a/docs/doctoring/package-description-boundary.md b/docs/doctoring/package-description-boundary.md index 1121809d96..fb6f52296f 100644 --- a/docs/doctoring/package-description-boundary.md +++ b/docs/doctoring/package-description-boundary.md @@ -167,3 +167,18 @@ metadata descriptions must be identical or the gate fails closed. Published Mark links using GitHub's `blob/main`, `tree/main`, `master`, or `develop` forms are blocking `mutable-release-link` findings; release tags and exact commits remain valid. + +## Runner boundary + +Both jobs in `.github/workflows/package-description-boundary.yml` now require +`CWL CI isolated` with `self-hosted`, `linux`, `x64`, and +`cwlab-ci-isolated` labels. The producer executes caller-controlled packaging +code; the inspector executes the trusted central gate. Neither job has a +GitHub-hosted fallback. Missing isolated capacity therefore remains incomplete +hosted evidence and cannot become a silent pass. + +This is a package-gate slice of the broader all-workflow migration owned by +`ContextualWisdomLab/.github#2565`. It does not provision runners, widen group +access, or prove disposable cleanup. A real caller can be accepted only after +operator-provided group access, isolated capacity, cleanup canary, and fresh +exact-head terminal checks. diff --git a/tests/test_audit_org_codeql_coverage.py b/tests/test_audit_org_codeql_coverage.py index 0ffc8fa749..194540ad53 100644 --- a/tests/test_audit_org_codeql_coverage.py +++ b/tests/test_audit_org_codeql_coverage.py @@ -263,9 +263,13 @@ def test_null_latest_analysis_is_not_coverage() -> None: def test_audit_codeql_coverage_defaults_now_to_current_time() -> None: - repositories = [covered_by_recent_analysis("DefaultNowRepo", days_ago=0)] + """Exercise the real default clock with a genuinely current analysis.""" + repository = covered_by_recent_analysis("DefaultNowRepo", days_ago=0) + repository["latest_codeql_analysis"]["created_at"] = datetime.now( + timezone.utc + ).isoformat() - assert audit.audit_codeql_coverage(repositories) == [] + assert audit.audit_codeql_coverage([repository]) == [] def test_load_payload_reads_from_stdin(monkeypatch) -> None: @@ -299,7 +303,17 @@ def test_main_fail_path_reports_gaps_from_stdin(monkeypatch, capsys) -> None: assert "FAIL: 1 repositories have no CodeQL coverage" in captured.err -def test_main_pass_path_reports_from_file_arg(tmp_path, capsys) -> None: +def test_main_pass_path_reports_from_file_arg(tmp_path, capsys, monkeypatch) -> None: + """Keep the CLI freshness witness bound to the fixture's reference clock.""" + class FixedDatetime(datetime): + """Supply the same reference instant used by the analysis fixture.""" + + @classmethod + def now(cls, tz=None): + """Return the fixture clock in the caller's requested timezone.""" + return NOW.astimezone(tz) if tz is not None else NOW.replace(tzinfo=None) + + monkeypatch.setattr(audit, "datetime", FixedDatetime) payload_path = tmp_path / "repositories.json" payload_path.write_text( json.dumps([covered_by_default_setup("ELUNVERA"), covered_by_recent_analysis("Orgmetra")]), diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index 82569e89c7..c19cac190f 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -1147,10 +1147,10 @@ def test_codeql_moved_head_requires_forward_ancestry(tmp_path: Path) -> None: assert enforce.returncode != 0 -def test_codeql_control_routing_keeps_pr_workflows_hosted() -> None: +def test_codeql_control_routing_keeps_pr_workflows_isolated() -> None: """Only the trusted main revision may request the restricted control group.""" workflow = WORKFLOW_PATH.read_text(encoding="utf-8") - selectors = [line.strip() for line in workflow.splitlines() if line.strip().startswith("runs-on:")] + selectors = [line.strip() for line in workflow.splitlines() if line.startswith(" group:")] trusted = "ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main" assert len(selectors) == 3 for selector in selectors: @@ -1158,7 +1158,10 @@ def test_codeql_control_routing_keeps_pr_workflows_hosted() -> None: choices = re.findall(r"'([^']*)'", selector) assert choices[0] == trusted assert json.loads(choices[1]) == {"group": "CWL central control", "labels": ["self-hosted", "linux", "x64"]} - assert json.loads(choices[2]) == "ubuntu-24.04" + assert json.loads(choices[2]) == { + "group": "CWL CI isolated", + "labels": ["self-hosted", "linux", "x64", "cwlab-ci-isolated"], + } def test_codeql_pr_rejects_invalid_required_run_time(tmp_path: Path) -> None: diff --git a/tests/test_package_description_boundary.py b/tests/test_package_description_boundary.py index f2adf26a3b..78bb267712 100644 --- a/tests/test_package_description_boundary.py +++ b/tests/test_package_description_boundary.py @@ -341,6 +341,20 @@ def test_workflow_is_callable_only() -> None: assert set(_workflow()[True]) == {"workflow_call"} +def test_workflow_runner_is_central_isolated_self_hosted() -> None: + """Both jobs reject GitHub-hosted fallback and use the isolated pool.""" + jobs = _workflow()["jobs"] + expected = { + "group": "CWL CI isolated", + "labels": ["self-hosted", "linux", "x64", "cwlab-ci-isolated"], + } + assert jobs["build-distribution"]["runs-on"] == expected + assert jobs["package-description-boundary"]["runs-on"] == expected + text = _WORKFLOW.read_text(encoding="utf-8") + assert "ubuntu-latest" not in text + assert "ubuntu-24.04" not in text + + def test_workflow_checks_out_the_gate_at_its_own_commit() -> None: """The caller must run the gate revision it pinned, not whatever main holds.""" steps = _workflow()["jobs"]["package-description-boundary"]["steps"] diff --git a/tests/test_required_review_runner_image_contract.py b/tests/test_required_review_runner_image_contract.py index ab7ede7aa4..b299244ad7 100644 --- a/tests/test_required_review_runner_image_contract.py +++ b/tests/test_required_review_runner_image_contract.py @@ -95,10 +95,17 @@ def test_opencode_review_dispatch_uses_explicit_supported_image(self) -> None: unittest.main() -def test_codeql_pr_routes_trusted_main_to_control_and_pr_revisions_to_hosted() -> None: - """Separate short metadata work from model work without granting PR runner access.""" +def test_codeql_pr_routes_trusted_main_to_control_and_pr_revisions_to_isolated() -> None: + """Keep trusted workflow control separate from isolated PR execution.""" workflow = Path(".github/workflows/codeql-pr.yml").read_text() - assert workflow.count('"group":"CWL central control"') == 3 - assert workflow.count("github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main'") == 3 - assert workflow.count("|| '\"ubuntu-24.04\"'") == 3 + trusted = "github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main'" + selectors = [line for line in workflow.splitlines() if line.startswith(" group:")] + labels = [line for line in workflow.splitlines() if line.startswith(" labels:")] + assert len(selectors) == len(labels) == 3 + for selector, label in zip(selectors, labels, strict=True): + assert trusted in selector and trusted in label + assert '"group":"CWL central control"' in selector + assert '"group":"CWL CI isolated"' in selector + assert '"cwlab-ci-isolated"' in selector and '"cwlab-ci-isolated"' in label + assert '"ubuntu-24.04"' not in selector + label assert '"group":"CWL MCP remediation"' not in workflow diff --git a/tests/test_sandboxed_web_e2e.py b/tests/test_sandboxed_web_e2e.py index 1b1cdf3722..e43aed95e0 100644 --- a/tests/test_sandboxed_web_e2e.py +++ b/tests/test_sandboxed_web_e2e.py @@ -1581,6 +1581,8 @@ def test_isolated_command_resolves_repo_local_launcher_against_sandboxed_cwd(mon resolution instead goes through the sandboxed-``cwd``-relative path this fix adds, not a PATH search. """ + # Model the guest home independently of the host scratch directory. + monkeypatch.setattr(Path, "home", lambda: tmp_path / "synthetic-home") monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) sandbox = tmp_path / "sandbox" repo = sandbox / "repo" @@ -1613,6 +1615,8 @@ def test_isolated_command_resolves_bare_command_via_relative_path_entry(monkeypa ``shutil.which`` to ``None`` here proves resolution instead falls through to the cwd-anchored ``PATH`` search this fix adds. """ + # Model the guest home independently of the host scratch directory. + monkeypatch.setattr(Path, "home", lambda: tmp_path / "synthetic-home") monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) sandbox = tmp_path / "sandbox" repo = sandbox / "repo" @@ -1646,6 +1650,8 @@ def test_isolated_command_translates_absolute_workspace_launcher_to_sandbox_moun that does not exist inside the sandbox, where only ``SANDBOX_MOUNT`` is bound, so the command would fail to launch there unchanged. """ + # Model the guest home independently of the host scratch directory. + monkeypatch.setattr(Path, "home", lambda: tmp_path / "synthetic-home") monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) sandbox = tmp_path / "sandbox" repo = sandbox / "repo" @@ -1706,6 +1712,8 @@ def test_isolated_command_rejects_relative_path_entry_escaping_sandbox(monkeypat def test_isolated_command_rejects_repo_local_path_traversal(monkeypatch, tmp_path): """A repo-local launcher path that lexically escapes the sandbox root is still rejected.""" + # Model the guest home independently of the host scratch directory. + monkeypatch.setattr(Path, "home", lambda: tmp_path / "synthetic-home") monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) sandbox = tmp_path / "sandbox" repo = sandbox / "repo" @@ -1726,6 +1734,8 @@ def test_isolated_command_rejects_repo_local_path_traversal(monkeypatch, tmp_pat def test_isolated_command_rejects_explicit_external_path(monkeypatch, tmp_path): """An explicit absolute path outside the workspace and bind roots is still rejected.""" + # Model the guest home independently of the host scratch directory. + monkeypatch.setattr(Path, "home", lambda: tmp_path / "synthetic-home") monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) sandbox = tmp_path / "sandbox" repo = sandbox / "repo" diff --git a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py index 42efce621e..d6cd65c9de 100644 --- a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py +++ b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py @@ -49,12 +49,12 @@ def test_hourly_review_repair_uses_explicit_supported_image(self) -> None: self.assertIn("labels: [self-hosted, linux, x64]", workflow) def test_codeql_pr_uses_explicit_supported_image(self) -> None: - """Require trusted-main control routing and Ubuntu fallback for all three jobs.""" + """Require trusted-main control routing and isolated fallback for all three jobs.""" workflow = CODEQL_PR.read_text(encoding="utf-8") self.assertNotIn("runs-on: ubuntu-latest", workflow) selectors = [ line.strip() for line in workflow.splitlines() - if line.strip().startswith("runs-on:") + if line.startswith(" group:") ] self.assertEqual(len(selectors), 3) for selector in selectors: @@ -64,7 +64,7 @@ def test_codeql_pr_uses_explicit_supported_image(self) -> None: ) self.assertIn('"group":"CWL central control"', selector) self.assertIn('"labels":["self-hosted","linux","x64"]', selector) - self.assertIn("|| '\"ubuntu-24.04\"'", selector) + self.assertIn("|| '{\"group\":\"CWL CI isolated\",\"labels\":[\"self-hosted\",\"linux\",\"x64\",\"cwlab-ci-isolated\"]}'", selector) def test_codeql_scan_dispatch_uses_explicit_supported_image(self) -> None: """Require validation, scan, and attempt wake jobs in the dedicated group."""