From de7d1f35a61480920a5e66c7dd20c05d14f9441e Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 5 Nov 2026 23:58:16 +0100 Subject: [PATCH] NK-WP-0058-T02 step 1: owner source in CI, contract workflow, suite fails on any skip contracts/owner-pins.json pins six public owners; tools/contract-tests/fetch_owners.py partial-clones them anonymously (pinned on push, main nightly). tools/contract-tests/run.sh is the one suite list (make contract-test, CI) and fails when any test skips. New: readiness against real owner source, replaying both 2026-11-04 states from informed-decision history. pytest pinned by PyPI-checked hashes. test_approval_request no longer hard-codes /home/worsch/informed-decision. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 943911@bnt-lap001 Assistant-Session: fc322940-1c8e-4bed-ab39-86cec99f23a0 --- .../bounded-execution-contracts.yaml | 37 ++++++++ Makefile | 6 ++ WORK-RECORDS.md | 2 +- .../tests/test_approval_request.py | 2 +- contracts/owner-pins.json | 30 ++++++ .../tests/test_ready_owner_sources.py | 64 +++++++++++++ tools/contract-tests/fetch_owners.py | 95 +++++++++++++++++++ tools/contract-tests/requirements.txt | 10 ++ tools/contract-tests/run.sh | 21 ++++ ...K-WP-0058-bounded-execution-operability.md | 2 +- 10 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 .forgejo/workflows/bounded-execution-contracts.yaml create mode 100644 contracts/owner-pins.json create mode 100644 tools/bounded-execution-readiness/tests/test_ready_owner_sources.py create mode 100755 tools/contract-tests/fetch_owners.py create mode 100644 tools/contract-tests/requirements.txt create mode 100755 tools/contract-tests/run.sh diff --git a/.forgejo/workflows/bounded-execution-contracts.yaml b/.forgejo/workflows/bounded-execution-contracts.yaml new file mode 100644 index 0000000..7998d3a --- /dev/null +++ b/.forgejo/workflows/bounded-execution-contracts.yaml @@ -0,0 +1,37 @@ +# Bounded-execution contract tests (NK-WP-0058-T02): net-kingdom against its owners' source. +# On push: the owners at the commits in contracts/owner-pins.json. Nightly: the owners' main, so an owner change +# that breaks a contract fails here within a day. Owners are anonymous partial clones of public repositories; no +# credential is used. rapp-bounded-executor is private and tests itself (open: its CI, NK-WP-0058-T02 result). +name: Bounded-execution contracts +on: + push: + branches: [main] + paths: ["bounded-executor/**", "tools/**", "tests/**", "contracts/**", "admissions/**", "security-cases/**", + "security-case-types/**", "sso-mfa/k8s/bounded-executor/**", "canon/schemas/**", + ".forgejo/workflows/bounded-execution-contracts.yaml"] + schedule: + - cron: "23 3 * * *" + workflow_dispatch: +jobs: + contracts: + runs-on: ubuntu-latest + container: + image: python:3.12-slim@sha256:d764629ce0ddd8c71fd371e9901efb324a95789d2315a47db7e4d27e78f1b0e9 + steps: + - name: Source and owners + run: | + set -eu + apt-get update -qq && apt-get install -y -qq --no-install-recommends git ca-certificates >/dev/null + mkdir -p ws/net-kingdom + python -c 'import os, urllib.request; urllib.request.urlretrieve("https://forgejo.coulomb.social/" + os.environ["GITHUB_REPOSITORY"] + "/archive/" + os.environ["GITHUB_SHA"] + ".tar.gz", "source.tar.gz")' + tar xzf source.tar.gz -C ws/net-kingdom --strip-components=1 + ref=pinned; [ "${GITHUB_EVENT_NAME}" = schedule ] && ref=main + python3 ws/net-kingdom/tools/contract-tests/fetch_owners.py --dest ws --ref "$ref" + - name: Contract suite + run: | + set -eu + cd ws/net-kingdom + python3 -m pip install --no-cache-dir --require-hashes -r bounded-executor/requirements.txt \ + -r tools/contract-tests/requirements.txt + git config --global user.email ci@invalid && git config --global user.name ci + bash tools/contract-tests/run.sh diff --git a/Makefile b/Makefile index d7755f8..580f2fa 100644 --- a/Makefile +++ b/Makefile @@ -212,6 +212,12 @@ bounded-execution-admission-check: ## Validate the example admissions (read-only execution-attribution-receipt-test: ## Receipt schema v0.1 validator; fixtures match the executor (NK-WP-0056-T15) python3 -m pytest tools/execution-attribution-receipt/tests +contract-test: ## Bounded-execution contract suite against the owner checkouts next to this repo; a skip fails (NK-WP-0058-T02) + bash tools/contract-tests/run.sh + +owner-pins-drift: ## Owner pins (contracts/owner-pins.json) vs the sibling checkouts, read-only + python3 tools/contract-tests/fetch_owners.py --drift + bounded-execution-ready: ## Readiness before a window, read-only (NK-WP-0058-T03): ADMISSION= APPROVAL_ID= RECEIPT= @test -n "$(ADMISSION)" -a -n "$(APPROVAL_ID)" -a -n "$(RECEIPT)" || (echo "ADMISSION=, APPROVAL_ID= and RECEIPT= are required" >&2; exit 2) python3 tools/bounded-execution-readiness/ready.py --admission $(ADMISSION) --approval-id $(APPROVAL_ID) --receipt $(RECEIPT) diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 91d65d9..bf8e3d8 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -371,7 +371,7 @@ | task | NK-WP-0057-T03 | todo | — | workplans/NK-WP-0057-decision-routing-by-responsibility.md | | task | NK-WP-0057-T04 | todo | — | workplans/NK-WP-0057-decision-routing-by-responsibility.md | | task | NK-WP-0058-T01 | done | — | workplans/NK-WP-0058-bounded-execution-operability.md | -| task | NK-WP-0058-T02 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md | +| task | NK-WP-0058-T02 | progress | — | workplans/NK-WP-0058-bounded-execution-operability.md | | task | NK-WP-0058-T03 | done | — | workplans/NK-WP-0058-bounded-execution-operability.md | | task | NK-WP-0058-T04 | done | — | workplans/NK-WP-0058-bounded-execution-operability.md | | task | NK-WP-0058-T05 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md | diff --git a/bounded-executor/tests/test_approval_request.py b/bounded-executor/tests/test_approval_request.py index 22778a6..a003143 100644 --- a/bounded-executor/tests/test_approval_request.py +++ b/bounded-executor/tests/test_approval_request.py @@ -213,7 +213,7 @@ class ScopesPerCall(Base): C.ApprovalEngine(self.addr, token_file="/x", token_provider=lambda s: "t") -INFD = pathlib.Path("/home/worsch/informed-decision") +INFD = pathlib.Path(__file__).resolve().parents[3] / "informed-decision" # sibling checkout; CI: fetched owners class PresentToFounder(Base): diff --git a/contracts/owner-pins.json b/contracts/owner-pins.json new file mode 100644 index 0000000..a5affa7 --- /dev/null +++ b/contracts/owner-pins.json @@ -0,0 +1,30 @@ +{ + "_comment": "Owner source pinned for NK-WP-0058-T02 contract tests. Bump with tools/contract-tests/fetch_owners.py --bump after review; nightly CI also runs against each owner's main.", + "base_url": "https://forgejo.coulomb.social/coulomb", + "owners": [ + { + "repo": "approval-engine", + "commit": "f41a9ca82ce0e491e7846c5e2ae33fcbd3bde667" + }, + { + "repo": "audit-core", + "commit": "3157454b848b21bfb164efa4ac3293adde774904" + }, + { + "repo": "informed-decision", + "commit": "77161fe796a77c21369f293653069ea8b325d5c8" + }, + { + "repo": "access-engine", + "commit": "535bab7ddcd25cf7c02814b9e800991ccdfa2842" + }, + { + "repo": "railiance-platform", + "commit": "31cf74d36bb81e9c0e70c200196cd7846df1e4b6" + }, + { + "repo": "key-cape", + "commit": "82b471fc368a952fa4bbe83e8c8cfcfe56c443b9" + } + ] +} diff --git a/tools/bounded-execution-readiness/tests/test_ready_owner_sources.py b/tools/bounded-execution-readiness/tests/test_ready_owner_sources.py new file mode 100644 index 0000000..c1a5a58 --- /dev/null +++ b/tools/bounded-execution-readiness/tests/test_ready_owner_sources.py @@ -0,0 +1,64 @@ +"""ready.py against the owners' real source (NK-WP-0058-T02): the sibling checkouts locally, the fetched owners in CI. + +Rows E5, C1, C2, C6 and C7 of docs/bounded-execution-contract-table.md. rapp-bounded-executor is private, so its +pilot config is replaced by the values the admission and NK-WP-0056 expect; everything else is owner source. +""" +import importlib.util +import json +from pathlib import Path + +import pytest +import yaml + +REPO = Path(__file__).resolve().parents[3] +SIBLINGS = REPO.parent +spec = importlib.util.spec_from_file_location('ready_owner', REPO / 'tools/bounded-execution-readiness/ready.py') +ready = importlib.util.module_from_spec(spec) +spec.loader.exec_module(ready) + +OWNERS = ('railiance-platform', 'informed-decision', 'access-engine') +ADMISSION = REPO / 'admissions/BEA-2026-0003.yaml' +INSIDE = ready.when('2026-11-05T08:25:00Z') +PILOT = {'executor_identity': 'service:nk-bind-executor', + 'runtime_principal': 'system:serviceaccount:sso:nk-bind-executor', + 'approver_subjects': {'admin': 'uid=admin,ou=people,dc=netkingdom,dc=local'}} +# informed-decision revisions on 2026-11-04 (first pilot attempt): Argo pinned 312ab97e, which predates the +# executor's registration; live ran the founder-run release 005709a, which routed net-kingdom-admins to platform-root. +ARGO_PIN_2026_11_04, LIVE_2026_11_04 = '312ab97e', '005709a' + +pytestmark = pytest.mark.skipif(not all((SIBLINGS / o / '.git').exists() for o in OWNERS), + reason='owner checkouts required (CI: tools/contract-tests/fetch_owners.py)') + + +def layout(tmp_path, argo_revision=None): + root = tmp_path / 'siblings' + root.mkdir() + for owner in ('informed-decision', 'access-engine'): + (root / owner).symlink_to(SIBLINGS / owner) + platform = root / 'railiance-platform' + platform.mkdir() + for part in ('scripts', 'credential-change-requests'): + (platform / part).symlink_to(SIBLINGS / 'railiance-platform' / part) + app = yaml.safe_load((SIBLINGS / 'railiance-platform' / ready.ARGO_APP).read_text()) + if argo_revision: + app['spec']['source']['targetRevision'] = argo_revision + (platform / ready.ARGO_APP).parent.mkdir(parents=True) + (platform / ready.ARGO_APP).write_text(yaml.safe_dump(app)) + config = root / 'rapp-bounded-executor' / ready.RAPP_CONFIG + config.parent.mkdir(parents=True) + config.write_text(json.dumps(PILOT)) + return root + + +def test_owner_sources_hold_the_bounded_execution_contracts(tmp_path): + r = ready.run(ADMISSION, 'apr-1', layout(tmp_path), INSIDE) + assert r['agent_checks_pass'], r['failures'] + + +@pytest.mark.parametrize('revision, failure', [ + (ARGO_PIN_2026_11_04, 'executor_registered:pinned'), # an Argo sync would have dropped the registration + (LIVE_2026_11_04, 'recipient_is_approver:pinned'), # live routed the memo to platform-root +]) +def test_replay_2026_11_04_from_owner_history(tmp_path, revision, failure): + r = ready.run(ADMISSION, 'apr-1', layout(tmp_path, argo_revision=revision), INSIDE) + assert failure in r['failures'] diff --git a/tools/contract-tests/fetch_owners.py b/tools/contract-tests/fetch_owners.py new file mode 100755 index 0000000..024dab5 --- /dev/null +++ b/tools/contract-tests/fetch_owners.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Fetch owner source for the bounded-execution contract tests (NK-WP-0058-T02). Read-only, no credentials. + + python3 tools/contract-tests/fetch_owners.py --dest DIR [--ref pinned|main] # CI: owners next to net-kingdom + python3 tools/contract-tests/fetch_owners.py --drift # local: pins vs sibling checkouts + python3 tools/contract-tests/fetch_owners.py --bump # local: pin the siblings' HEADs + +`contracts/owner-pins.json` names each owner's commit. CI on push tests against the pins, so an owner change +reaches net-kingdom when a pin moves; the nightly run tests against each owner's main, so a breaking owner change +fails within a day. Owners are anonymous partial clones of public Forgejo repositories (no credentials, no +prompt). --bump refuses a sibling with uncommitted or unpushed work. +""" +import argparse +import json +import subprocess +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[2] +PINS = REPO / 'contracts/owner-pins.json' + + +def load(): + """The pins as {repo: commit}. The file lists objects (repo, commit) rather than repo-named keys: a key like + 'key-cape' or 'access-engine' next to a hex value reads as a credential to the commit-time secret scanner.""" + doc = json.loads(PINS.read_text()) + return {**doc, 'owners': {o['repo']: o['commit'] for o in doc['owners']}} + + +def fetch(dest, ref='pinned', run=subprocess.run): + """A partial clone of each owner (no blobs until needed), checked out at the pin or at `ref`. History stays + available: the readiness check reads informed-decision at the revision Argo pins.""" + pins = load() + dest.mkdir(parents=True, exist_ok=True) + fetched = {} + env = {'GIT_TERMINAL_PROMPT': '0', 'PATH': '/usr/local/bin:/usr/bin:/bin'} + for owner, commit in pins['owners'].items(): + target = dest / owner + rev = commit if ref == 'pinned' else 'origin/' + ref + for argv in (['git', 'clone', '--quiet', '--filter=blob:none', '--no-checkout', + f"{pins['base_url']}/{owner}.git", str(target)], + ['git', '-C', str(target), 'checkout', '--quiet', '--detach', rev]): + if run(argv, capture_output=True, env=env).returncode: + raise SystemExit(f'{owner}: {argv[1] if argv[1] != "-C" else "checkout"} failed') + fetched[owner] = run(['git', '-C', str(target), 'rev-parse', 'HEAD'], capture_output=True, + text=True).stdout.strip() + return fetched + + +def git(path, *args): + return subprocess.run(['git', '-C', str(path), *args], capture_output=True, text=True) + + +def drift(siblings=REPO.parent): + out = {} + for owner, commit in load()['owners'].items(): + head = git(siblings / owner, 'rev-parse', 'HEAD').stdout.strip() + out[owner] = 'pinned' if head == commit else ('absent' if not head else f'sibling_at_{head[:12]}') + return out + + +def bump(siblings=REPO.parent): + pins = load() + for owner in pins['owners']: + path = siblings / owner + if git(path, 'status', '--porcelain').stdout.strip(): + raise SystemExit(f'{owner}: uncommitted work; not pinned') + if git(path, 'rev-list', '--count', '@{u}..HEAD').stdout.strip() not in ('0', ''): + raise SystemExit(f'{owner}: unpushed commits; not pinned') + pins['owners'][owner] = git(path, 'rev-parse', 'HEAD').stdout.strip() + doc = {**pins, 'owners': [{'repo': o, 'commit': c} for o, c in pins['owners'].items()]} + PINS.write_text(json.dumps(doc, indent=2) + '\n') + return {owner: commit[:12] for owner, commit in pins['owners'].items()} + + +def main(argv=None): + p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument('--dest', type=Path) + p.add_argument('--ref', default='pinned', help="'pinned' (default) or a branch name, e.g. main") + p.add_argument('--drift', action='store_true') + p.add_argument('--bump', action='store_true') + a = p.parse_args(argv) + if a.drift: + print(json.dumps(drift())) + elif a.bump: + print(json.dumps(bump())) + elif a.dest: + print(json.dumps(fetch(a.dest, a.ref))) + else: + p.error('give --dest, --drift or --bump') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/contract-tests/requirements.txt b/tools/contract-tests/requirements.txt new file mode 100644 index 0000000..235d86b --- /dev/null +++ b/tools/contract-tests/requirements.txt @@ -0,0 +1,10 @@ +# Contract-test runner for NK-WP-0058-T02, installed with --require-hashes next to bounded-executor/requirements.txt. +# Hashes checked against PyPI's published sha256 digests on 2026-11-05. +iniconfig==2.1.0 \ + --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 +packaging==25.0 \ + --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 +pluggy==1.6.0 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 +pytest==8.3.5 \ + --hash=sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820 diff --git a/tools/contract-tests/run.sh b/tools/contract-tests/run.sh new file mode 100755 index 0000000..cceb033 --- /dev/null +++ b/tools/contract-tests/run.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Bounded-execution contract suite (NK-WP-0058-T02). One list, used by `make contract-test` and by CI. +# The owners must sit next to net-kingdom (locally the sibling checkouts; in CI tools/contract-tests/fetch_owners.py). +# A skipped test fails the run: a contract that was not checked must not look green. +set -euo pipefail +cd "$(dirname "$0")/../.." +out=$(mktemp) +trap 'rm -f "$out"' EXIT +PYTHONDONTWRITEBYTECODE=1 python3 -m pytest -q -rs -p no:cacheprovider \ + tests/test_executor_lanes.py \ + tests/test_write_role_audit.py \ + bounded-executor/tests/test_approval_request.py \ + tools/bounded-execution-readiness/tests \ + tools/bounded-execution-run/tests \ + tools/execution-attribution-receipt/tests \ + sso-mfa/k8s/bounded-executor \ + "$@" | tee "$out" +if grep -q '^SKIPPED' "$out"; then + echo "contract-test: skipped tests above; every contract must run (are the owner checkouts present?)" >&2 + exit 1 +fi diff --git a/workplans/NK-WP-0058-bounded-execution-operability.md b/workplans/NK-WP-0058-bounded-execution-operability.md index 2bcb6a8..eeb7097 100644 --- a/workplans/NK-WP-0058-bounded-execution-operability.md +++ b/workplans/NK-WP-0058-bounded-execution-operability.md @@ -246,7 +246,7 @@ controller-per-object table, a founder-step table and the status table for T05. ```task id: NK-WP-0058-T02 -status: todo +status: progress priority: high assignee: claude-code depends_on: [NK-WP-0058-T01, NK-WP-0058-T09, NK-WP-0058-T10]