diff --git a/.forgejo/workflows/ci-smoke.yaml b/.forgejo/workflows/ci-smoke.yaml index f706aa2..908adfc 100644 --- a/.forgejo/workflows/ci-smoke.yaml +++ b/.forgejo/workflows/ci-smoke.yaml @@ -1,4 +1,4 @@ -# CI smoke — package tests prove hub-core library health on push. +# Local source, enforcement, inventory and installed-package gates. name: CI Smoke on: @@ -22,23 +22,21 @@ jobs: pytest-smoke: runs-on: self-hosted steps: - - name: Run hub-core pytest + - name: Test enforcement, inventory and installed package run: | set -eu - REF="${GITHUB_SHA:-main}" - SHORT="${REF:0:7}" - ROOT="${HOME}/ci-hub-core-${SHORT}" - rm -rf "${ROOT}" - mkdir -p "${ROOT}" - wget -qO /tmp/hub-core.tar.gz \ - "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz" - tar xzf /tmp/hub-core.tar.gz -C "${ROOT}" --strip-components=1 - cd "${ROOT}" + REF="${GITHUB_SHA:?commit SHA required}" + CI_WORKDIR="$(mktemp -d)" + trap 'rm -rf -- "$CI_WORKDIR"' EXIT + wget -qO "$CI_WORKDIR/source.tar.gz" \ + "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${REF}.tar.gz" + mkdir "$CI_WORKDIR/source" + tar xzf "$CI_WORKDIR/source.tar.gz" -C "$CI_WORKDIR/source" --strip-components=1 + cd "$CI_WORKDIR/source" if ! command -v uv >/dev/null 2>&1; then pip install --user uv export PATH="${HOME}/.local/bin:${PATH}" fi - uv sync - uv run python -c "import hub_core; print(hub_core.__version__)" - uv run python -m pytest -q - echo "hub-core pytest smoke ok @ ${SHORT}" \ No newline at end of file + uv sync --locked --group dev --extra runtime + make ci-check + echo "hub-core source, enforcement, inventory and package checks passed @ ${REF}" diff --git a/Makefile b/Makefile index 29ba8af..e9e608c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: install test runtime-run conformance container-build ecosystem-regression +.PHONY: install test runtime-run conformance container-build ecosystem-regression inventory-check package-check ci-check UV ?= uv ECOSYSTEM_REGRESSION ?= /home/worsch/the-custodian/scripts/hub-ecosystem-regression.sh @@ -9,7 +9,7 @@ install: $(UV) sync test: - $(UV) run python -m pytest -q + $(UV) run --locked --extra runtime python -m pytest -q runtime-run: $(UV) run --extra runtime hub-core api @@ -22,3 +22,13 @@ container-build: ecosystem-regression: bash $(ECOSYSTEM_REGRESSION) + +# Local equivalents of the required CI gates. +inventory-check: + $(UV) run --locked --extra runtime python tools/build_access_inventory.py --inventory docs/platform-access-inventory.json --check + +package-check: + $(UV) build --out-dir dist/ci + @set -eu; set -- dist/ci/*.whl; test "$$#" -eq 1; $(UV) run --no-project --with "$$1" python -I tools/check_installed_package.py + +ci-check: test inventory-check package-check diff --git a/docs/conformance.md b/docs/conformance.md index 9c7a448..85c7835 100644 --- a/docs/conformance.md +++ b/docs/conformance.md @@ -46,3 +46,27 @@ absorption slices that are not part of the T04 minimal vertical. Tenant isolation also remains open because the 0.1 runtime has no tenant identity or authorization context yet. These gaps must not be interpreted as passing; the harness reports only the implemented profile above. + +## Access enforcement and CI gates + +`make ci-check` runs the test suite, checks the reviewed access inventory for +source drift, builds distributions and validates an installed wheel outside the +checkout's import path. Forgejo runs these gates for `main` pushes and manual +runs, using the full commit SHA and a unique temporary checkout. CI installs the +locked development and runtime dependencies first. Individual gates are +`make test`, `make inventory-check` and `make package-check`. + +`tests/test_enforced_conformance.py` runs all twelve existing Tier 2/3 checks +through an explicitly enforced runtime using a real signed IAM JWT and synthetic +owner facts, policy and audit. It verifies event attribution against authorization +records. Additional journeys establish valid state, deny both reads and writes +for anonymous/invalid credentials, revoked entitlement, policy denial and +policy/audit outages, then independently read back unchanged stored messages. +These tests run in the ordinary suite; they need no external owner checkout. + +The installed-wheel gate validates runtime/security imports, packaged action and +browser route coverage, contract fixtures/schemas and the migration template. +It makes no owner requests and starts no service. The separate optional Audit +Core interoperability suite still requires `HUB_CORE_AUDIT_CORE_SOURCE` and is +not silently represented as covered by ordinary CI. Local CI-equivalent success +is not a deployed Forgejo receipt or live owner/platform acceptance. diff --git a/tests/test_enforced_conformance.py b/tests/test_enforced_conformance.py new file mode 100644 index 0000000..5c2e9db --- /dev/null +++ b/tests/test_enforced_conformance.py @@ -0,0 +1,89 @@ +"""Disposable enforcement-mode journeys; synthetic owners, real signed IAM token. + +These exercise the complete Tier 2/3 workload through AccessBoundary. They do +not establish issuer registration, deployed custody or platform acceptance. +""" +import asyncio +import json +import time +from dataclasses import replace +from uuid import uuid4 + +import pytest +from cryptography.hazmat.primitives.asymmetric import rsa +from fastapi.testclient import TestClient + +from hub_core.conformance import ConformanceHarness +from hub_core.runtime.app import create_app +from hub_core.runtime.config import RuntimeSettings +from test_access_boundary import Owners +from test_access_identity import setup + + +@pytest.fixture(scope='module') +def signing_key(): + return rsa.generate_private_key(public_exponent=65537, key_size=2048) + + +@pytest.fixture +def enforced(signing_key): + token, identity, _, upstream = setup(signing_key) + owners = Owners() + owners.facts = replace(owners.facts, producer_addresses=frozenset({'hub:ops-hub'})) + async def current_facts(actor, resource): + return replace(owners.facts, checked_at=time.time()) + owners.resolve = current_facts + controller = owners.controller() + controller.identity = identity + app = create_app(settings=RuntimeSettings(environment='test', access_mode='enforce'), + access_controller=controller) + with TestClient(app, base_url='https://hub.example', headers={'Authorization': 'Bearer ' + token}) as client: + yield client, owners + asyncio.run(upstream.aclose()) + + +def test_tier_2_and_3_workload_passes_through_enforcement(enforced): + client, owners = enforced + report = ConformanceHarness(client).run() + assert report.passed, report.to_dict() + assert report.passed_count == 12 + assert owners.requests + assert all(r.actor.subject == 'immutable-root' for r in owners.requests) + records = {r['correlation_id']: r for r in owners.records if r['outcome'] == 'authorized'} + for family in ('progress', 'interaction'): + result = client.get('/ports/projections/' + family + '_events') + event = result.json()['data']['items'][0] + attribution = event['payload']['_hub_access'] + record = records[attribution['correlation_id']] + assert record['subject'] == 'immutable-root' + assert 'Projection rebuild conformance fixture.' not in json.dumps(owners.records) + + +@pytest.mark.parametrize('failure,status', [('anonymous',401), ('invalid',401), + ('revoked',403), ('policy_denied',403), ('policy_outage',503), ('audit_outage',503)]) +def test_denied_reads_and_writes_leave_no_business_mutation(enforced, failure, status): + client, owners = enforced + assert ConformanceHarness(client).run().passed + before = client.get('/ports/projections/messages').json()['data']['items'] + saved = client.headers['authorization'] + if failure == 'anonymous': + del client.headers['authorization'] + elif failure == 'invalid': + client.headers['authorization'] = 'Bearer invalid' + elif failure == 'revoked': + owners.facts = replace(owners.facts, root_entitled=False) + elif failure == 'policy_denied': + owners.allow = False + elif failure == 'policy_outage': + owners.policy_down = True + else: + owners.audit_down = True + message = {'schema_version':'0.1.0', 'correlation_id':str(uuid4()), + 'from_address':'hub:ops-hub', 'to_addresses':['agent:conformance'], + 'body':'must never commit'} + assert client.get('/ports/projections/messages').status_code == status + assert client.post('/ports/messaging/messages', json=message).status_code == status + client.headers['authorization'] = saved + owners.facts = replace(owners.facts, root_entitled=True) + owners.allow, owners.policy_down, owners.audit_down = True, False, False + assert client.get('/ports/projections/messages').json()['data']['items'] == before diff --git a/tools/check_installed_package.py b/tools/check_installed_package.py new file mode 100644 index 0000000..caa2df0 --- /dev/null +++ b/tools/check_installed_package.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Check an installed wheel outside the checkout's import path (use python -I). + +Run via: uv run --no-project --with dist/ python -I tools/check_installed_package.py +No startup, owner requests, credentials or database connection are needed. +""" +import json +from importlib.resources import files +from pathlib import Path + +import hub_core +from hub_core.conformance import ConformanceHarness +from hub_core.runtime.app import create_app +from hub_core.runtime.config import RuntimeSettings +from hub_core.runtime.inbox_projection import create_inbox_projection_router +from hub_core.security.boundary import iter_routes, route_key +from hub_core.security.browser import BROWSER_ROUTES, create_browser_router + + +def main(): + checkout = Path(__file__).resolve().parents[1] + installed = Path(hub_core.__file__).resolve() + if installed.is_relative_to(checkout): + raise RuntimeError('package smoke imported checkout instead of installed wheel') + catalog = json.loads(files('hub_core.security').joinpath('routes.json').read_text())['routes'] + app = create_app(settings=RuntimeSettings(environment='test', access_mode='enforce')) + routes = [*iter_routes(app), *iter_routes(create_inbox_projection_router())] + missing = [route_key(route, method) for route in routes if hasattr(route, 'methods') + for method in route.methods if route.path != '/healthz' and route_key(route, method) not in catalog] + if missing: + raise RuntimeError('installed action catalog is incomplete: ' + ', '.join(missing)) + browser = {(method, route.path) for route in iter_routes(create_browser_router()) + for method in route.methods} + if browser != BROWSER_ROUTES: + raise RuntimeError('installed browser route coverage differs') + # Constructor loads packaged contract fixtures; these checks load schemas. + harness = ConformanceHarness(None) + harness._schema_validate() + harness._no_secrets() + if not files('hub_core.migrations').joinpath('script.py.mako').is_file(): + raise RuntimeError('installed migration template missing') + print('Installed wheel: imports, action catalog, browser routes, contract resources and migration template pass') + + +if __name__ == '__main__': + main() diff --git a/workplans/HUB-WP-0012-netkingdom-platform-root-access.md b/workplans/HUB-WP-0012-netkingdom-platform-root-access.md index 6e823c1..b88e777 100644 --- a/workplans/HUB-WP-0012-netkingdom-platform-root-access.md +++ b/workplans/HUB-WP-0012-netkingdom-platform-root-access.md @@ -325,6 +325,28 @@ T01–T04 remain `progress`; issuer registration/MFA, owner-facts composition, MCP consumer adoption, operation-outcome auditing and platform conformance remain open. No production listener or entitlement changed. +## Conformance and CI continuation — 2026-09-28 + +Added enforcement-mode Tier 2/3 conformance journeys with signed IAM tokens and +explicit synthetic owners. Denial/revocation/outage cases exercise reads and +writes and verify unchanged business state after restoring access. Event +attribution is joined to recorded authorization decisions. + +Forgejo now runs `make ci-check`: the complete ordinary test suite, access +inventory drift checks, distribution builds and an isolated installed-wheel +resource/import check. CI checks out the full commit into a unique temporary +directory and installs locked development/runtime dependencies. The optional +owner-source interoperability suite remains separately identified. + +See [conformance documentation](../docs/conformance.md#access-enforcement-and-ci-gates). +Validation: local `make ci-check` passed with **330 tests**, one explicitly +optional owner-source module skipped, inventory coverage (165 Hub surfaces), +distribution builds and installed-wheel checks. Final locked runtime dependency +sync and workflow YAML/shell syntax checks also pass. + +These are local source/release gates, not live platform or remote CI acceptance; +T01–T04 remain `progress` and T06 remains open. + ## Acceptance checkpoints - [x] Architecture/source/runtime review captured; new implementation owner is hub-core