From ee4cf14cbc0ffb34b457181a02302f2cd4255915 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 29 Jul 2026 22:00:34 +0200 Subject: [PATCH] Define and implement multi-repo onboarding mechanism (WP-0006-T07) specs/TrustServiceOnboarding.md defines the mechanism: a Phase Manifest file is committed to the declaring repo (durable, independently foldable forever) and separately registered with the hosted service; once registered, the Ledger's live authoritative copy is the hosted service only, not a second competing file. Licensor token bootstrapping is explicitly out of scope here (a WP-0008-T01 governance action). scripts/trf_onboard.py: a dependency-light CLI (stdlib urllib + target_revenue.validation only, no FastAPI/psycopg needed to onboard a Phase) with validate/register-phase/append-entry/status subcommands. The Licensor token is read only from a named environment variable, never accepted as a literal argument. tests/test_trf_onboard.py (4 tests, no network/Docker) proves invalid-manifest and missing-token-env cases fail before any HTTP attempt, by monkeypatching the request function to raise if called. tests/test_onboarding_hosted.py (1 Docker-gated test) runs an actual uvicorn server on a real socket and drives the full register -> append -> status round trip through the CLI as an external repo would invoke it. --- README.md | 2 +- scripts/trf_onboard.py | 136 ++++++++++++++ specs/TrustServiceOnboarding.md | 106 +++++++++++ tests/test_onboarding_hosted.py | 173 ++++++++++++++++++ tests/test_trf_onboard.py | 87 +++++++++ ...EV-WP-0006-trust-service-implementation.md | 28 ++- 6 files changed, 530 insertions(+), 2 deletions(-) create mode 100644 scripts/trf_onboard.py create mode 100644 specs/TrustServiceOnboarding.md create mode 100644 tests/test_onboarding_hosted.py create mode 100644 tests/test_trf_onboard.py diff --git a/README.md b/README.md index 0f08eb1..0cd5366 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ The concept's §13 now defines a **Global Contingency Share Determination Rule** | [TREV-WP-0003](workplans/TREV-WP-0003-normative-core-extraction.md) | Extract stable normative core docs — **finished**, reviewed and accepted 2026-07-29 | | [TREV-WP-0004](workplans/TREV-WP-0004-global-jurisdiction-research.md) | Global jurisdictional research backing the License/CUA candidates — **finished**, T10 synthesis accepted 2026-07-29 with alpha/beta working defaults (full legal review deferred until out of beta — see `SCOPE.md` §1) | | [TREV-WP-0005](workplans/TREV-WP-0005-enforcement-network-research.md) | Enforcement Network legal feasibility research — **finished**, T10 synthesis accepted 2026-07-29 on the same alpha/beta basis (Japan's Article 12 risk remains explicitly unresolved) | -| [TREV-WP-0006](workplans/TREV-WP-0006-trust-service-implementation.md) | Hosted Trust Service reference implementation (PRD Phase 4b) — active; T01 (PRD), T02 (ADR-0002, accepted), T03 (registries), T04 (Ledger append API), T05 (Metrics), T06 (Conversion Attestation) done; T07 (multi-repo onboarding) next | +| [TREV-WP-0006](workplans/TREV-WP-0006-trust-service-implementation.md) | Hosted Trust Service reference implementation (PRD Phase 4b) — active; T01 (PRD), T02 (ADR-0002, accepted), T03 (registries), T04 (Ledger append API), T05 (Metrics), T06 (Conversion Attestation), T07 (onboarding mechanism, `specs/TrustServiceOnboarding.md` + `scripts/trf_onboard.py`) done; T08 (hosted conformance suite) next | | [TREV-WP-0007](workplans/TREV-WP-0007-degeneration-policy-and-canonical-profiles.md) | Degeneration policy + canonical monetization profile catalog — active, not yet started | | [TREV-WP-0008](workplans/TREV-WP-0008-governance-and-pilot-rollout.md) | Governance formalization + pilot rollout across `coulomb-loop`/`net-kingdom`/`helix-forge`/`railiance-*` — active, not yet started; real Phase declarations gated behind T05 | diff --git a/scripts/trf_onboard.py b/scripts/trf_onboard.py new file mode 100644 index 0000000..b882c2b --- /dev/null +++ b/scripts/trf_onboard.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Trust Service onboarding CLI (WP-0006-T07). + +Mechanism only — see `specs/TrustServiceOnboarding.md` for the full +process this wraps. Deliberately dependency-light: stdlib `urllib` for +HTTP (no `requests`/`httpx` requirement) plus `target_revenue.validation` +(already a core, non-service dependency) for offline pre-flight checks — +a repo onboarding a Phase should not need to install FastAPI/psycopg just +to register one. + +Every subcommand that submits data validates it offline first, with the +same check the hosted service itself runs at registration +(`validation.py`), so a rejection is caught locally with an identical +field-by-field diff instead of only being discovered via a failed network +call (specs/TrustServiceOnboarding.md §3 steps 2 and 4). +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import urllib.error +import urllib.request +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "src")) + +from target_revenue import validation # noqa: E402 + + +def _load(path: str) -> dict: + with open(path, encoding="utf-8") as f: + return json.load(f) + + +def _request(url: str, method: str, token: str | None = None, body: dict | None = None) -> dict: + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Content-Type": "application/json"} + if token: + headers["Authorization"] = f"Bearer {token}" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + try: + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8") + raise SystemExit(f"{method} {url} -> HTTP {exc.code}: {detail}") from exc + + +def _token_from_env(env_var: str) -> str: + token = os.environ.get(env_var) + if not token: + raise SystemExit( + f"environment variable {env_var!r} is not set — the Licensor " + "API token must never be passed on the command line or " + "committed to the repo (specs/TrustServiceOnboarding.md §2)" + ) + return token + + +def cmd_validate(args: argparse.Namespace) -> None: + manifest = _load(args.manifest) + try: + validation.validate_phase_manifest(manifest) + except validation.ConformanceError as exc: + print("REJECTED:", "; ".join(exc.errors), file=sys.stderr) + raise SystemExit(1) + print(f"OK: {manifest['phase']['id']} is schema-conformant") + + +def cmd_register_phase(args: argparse.Namespace) -> None: + manifest = _load(args.manifest) + try: + validation.validate_phase_manifest(manifest) + except validation.ConformanceError as exc: + print("REJECTED (offline check, no network call made):", "; ".join(exc.errors), file=sys.stderr) + raise SystemExit(1) + token = _token_from_env(args.token_env) + result = _request(f"{args.url}/phases", "POST", token=token, body=manifest) + print(json.dumps(result, indent=2)) + + +def cmd_append_entry(args: argparse.Namespace) -> None: + entry = _load(args.entry) + token = _token_from_env(args.token_env) + result = _request( + f"{args.url}/phases/{args.phase_id}/ledger", "POST", token=token, body=entry + ) + print(json.dumps(result, indent=2)) + + +def cmd_status(args: argparse.Namespace) -> None: + result = _request(f"{args.url}/phases/{args.phase_id}/metrics", "GET") + print(json.dumps(result, indent=2)) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="trf_onboard", description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + + p_validate = sub.add_parser("validate", help="offline-validate a Phase Manifest file, no network call") + p_validate.add_argument("manifest") + p_validate.set_defaults(func=cmd_validate) + + p_register = sub.add_parser("register-phase", help="validate offline, then POST /phases") + p_register.add_argument("--url", required=True, help="Trust Service base URL") + p_register.add_argument("--token-env", required=True, help="env var holding the Licensor API token") + p_register.add_argument("--manifest", required=True) + p_register.set_defaults(func=cmd_register_phase) + + p_append = sub.add_parser("append-entry", help="POST a Ledger entry for an already-registered Phase") + p_append.add_argument("--url", required=True) + p_append.add_argument("--token-env", required=True) + p_append.add_argument("--phase-id", required=True) + p_append.add_argument("--entry", required=True) + p_append.set_defaults(func=cmd_append_entry) + + p_status = sub.add_parser("status", help="GET /phases/{id}/metrics (public, no token)") + p_status.add_argument("--url", required=True) + p_status.add_argument("--phase-id", required=True) + p_status.set_defaults(func=cmd_status) + + return parser + + +def main(argv: list[str] | None = None) -> None: + parser = build_parser() + args = parser.parse_args(argv) + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/specs/TrustServiceOnboarding.md b/specs/TrustServiceOnboarding.md new file mode 100644 index 0000000..00edc64 --- /dev/null +++ b/specs/TrustServiceOnboarding.md @@ -0,0 +1,106 @@ +# Trust Service Onboarding: Multi-Repo Registration Mechanism + +Status: Draft v0.1 +Date: 2026-07-29 +Workplan: `workplans/TREV-WP-0006-trust-service-implementation.md` T07 +Primary artifacts: `specs/PhaseManifestSpecification.md`, `specs/TargetLedgerSpecification.md`, `specs/TrustServiceProductRequirementsDocument.md`, `docs/adr/ADR-0002-hosted-trust-service-stack.md`, `scripts/trf_onboard.py` + +**Scope note:** this document defines the **mechanism** by which any repo +registers a Phase against a running hosted Trust Service instance. It does +**not** select which repos or Phases go first — that is +`workplans/TREV-WP-0008-governance-and-pilot-rollout.md` T02 (repo/Phase +survey) and T03 (draft pilot manifests), which use this mechanism rather +than redefine it. It also does not authorize any real Phase to go live — +that remains gated behind WP-0008-T05 regardless of how well this +mechanism works. + +--- + +## 1. What a repo commits to itself vs. submits to the Trust Service + +A recurring confusion this document exists to close: a Phase Manifest is +**not** solely a Trust Service database row. It has two authoritative +copies with different roles, and both are required, not just one: + +| Artifact | Lives in | Role | +|---|---|---| +| Phase Manifest file (e.g. `trf/phase-.json`) | The declaring repo's own version control, committed | The repo's own durable, git-history-backed record of what it declared and when — survives independent of any Trust Service instance's uptime or even existence (TSD §1.2's offline-verifiability property applies here too: this file alone, plus its ledger export, must remain independently foldable forever). | +| The same Phase Manifest, registered | The hosted Trust Service's `phase_manifests` table (`migrations/0001_registries.sql`) | The multi-tenant, queryable, publicly-servable copy other parties (Customers, auditors, Enforcement Partners) read without needing access to the declaring repo at all. | +| Target Ledger entries | The hosted Trust Service's `ledger_entries` table only | Per ADR-0002, the Ledger's authoritative live copy is the hosted service once a Phase is registered there — a repo does **not** also maintain a second, competing ledger file post-registration (that would reintroduce exactly the "which copy is authoritative" ambiguity Stage 0's single-golden-fixture model never had to answer). A repo *may* keep periodic exports for its own archival/offline-verification purposes (§4 below), but those are copies, not a second source of truth. | +| Extension registrations, Conversion Attestation | The hosted Trust Service, per T03/T06 | Same reasoning as the Ledger. | + +**Rule of thumb:** anything that must exist and remain meaningful even if +this Trust Service instance is retired, replaced, or briefly down belongs +committed to the repo (the Manifest declaration itself). Anything that +must be a single, live, multi-party-queryable authority belongs +exclusively in the hosted service once registered (the Ledger). + +## 2. Licensor bootstrapping (out of band, not part of this mechanism) + +Before any repo can register a Phase, its Licensor identity must already +be resolved (`workplans/TREV-WP-0008-governance-and-pilot-rollout.md` T01) +and a row must exist in the `licensors` table with an issued API token +(`migrations/0001_registries.sql`). This document assumes that step has +already happened; it is a governance action, not a mechanical one, and is +explicitly out of this task's scope. The token itself must never be +committed to the repo — it is a deployment secret (CI secret store, +environment variable at declaration time), referenced by name in the +repo's own tooling, never by value. + +## 3. Step-by-step mechanism + +1. **Author the Phase Manifest locally**, conforming to + `specs/PhaseManifestSpecification.md` and + `schemas/phase_manifest.schema.json`, following the shape already + established by `examples/phase-001/manifest.json`. +2. **Validate offline before doing anything else** — + `python -m target_revenue.validation` equivalent via + `scripts/trf_onboard.py validate ` (wraps + `validation.validate_phase_manifest`, the same check the hosted + service itself runs at registration, so a rejection is caught locally + with the identical field-by-field diff rather than discovered only via + a failed network call). +3. **Commit the Manifest file to the repo** before registering it — this + preserves a git-history record of intent to declare, independent of + whether registration succeeds on the first attempt. +4. **Register**: `scripts/trf_onboard.py register-phase --url --token-env --manifest `. Performs the same offline validation as step 2 again (defense in depth — a manifest edited after step 2 but before this step must still be caught), then `POST /phases`. +5. **Append Ledger entries as commercial payments are recognized.** In + practice this step is automated by whatever billing/payment pipeline + the product line already uses, not run manually per payment — + `scripts/trf_onboard.py append-entry --url ... --token-env ... --phase-id ... --entry ` is the mechanism that pipeline calls, or a template for it. +6. **Monitor via the public, unauthenticated read endpoints** — + `scripts/trf_onboard.py status --url ... --phase-id ...` (wraps + `GET /phases/{id}/metrics`) — no token needed for this step, since + metrics are public per FR-9/FR-10. +7. **Conversion and Attestation require no onboarding action at all.** + Per WP-0006-T06, the moment the ledger fold reaches + `Outstanding Target = 0`, conversion is already true; the Trust Service + publishes the Attestation on first observation. Nothing in this + mechanism gates on, waits for, or triggers that separately. + +## 4. Offline verifiability remains intact after hosting + +Any party — including the declaring repo itself, independent of the +Trust Service's continued availability — can export a Phase's current +Ledger via `GET /phases/{id}/ledger` (public) and independently recompute +Outstanding Target with `target_revenue.fold.fold_outstanding_target` +against the committed Manifest file. This is the same offline-first +property `docs/adr/ADR-0002-hosted-trust-service-stack.md` and +`specs/TrustServiceProductRequirementsDocument.md` §3 require the hosted +service to preserve, not a new guarantee invented for onboarding — this +section just makes explicit that a repo can exercise it as part of its own +routine (e.g., a periodic CI job that exports and re-verifies), not only a +theoretical possibility. + +## 5. Explicit non-goals + +- Selecting which of `coulomb-loop`, `net-kingdom`, `helix-forge`, or a + `railiance-*` repo registers first, or which Milestone Release qualifies + — `workplans/TREV-WP-0008-governance-and-pilot-rollout.md` T02/T03. +- Resolving Licensor identity — WP-0008-T01. +- Authorizing any registration performed via this mechanism to bind real + money or a real License header change — WP-0008-T05, unaffected by this + mechanism existing or working correctly. +- A CI/CD template for automated payment-pipeline integration — a + reasonable future deliverable once a real pilot Phase's billing + pipeline is known, not invented speculatively here. diff --git a/tests/test_onboarding_hosted.py b/tests/test_onboarding_hosted.py new file mode 100644 index 0000000..493e32c --- /dev/null +++ b/tests/test_onboarding_hosted.py @@ -0,0 +1,173 @@ +"""End-to-end test: scripts/trf_onboard.py against a real, running hosted +Trust Service instance (WP-0006-T07), not just FastAPI's in-process +TestClient — this is the one test in the suite that exercises an actual +HTTP round trip over a real socket, since the onboarding CLI uses +`urllib` against a real URL rather than an ASGI transport. + +Same ephemeral, disposable Postgres-via-Docker pattern as the other +hosted tests (never the shared state-hub instance). +""" + +from __future__ import annotations + +import json +import shutil +import subprocess +import threading +import time +import uuid +from pathlib import Path + +import pytest + +psycopg = pytest.importorskip("psycopg") +pytest.importorskip("fastapi") +uvicorn = pytest.importorskip("uvicorn") + +from conftest import golden_manifest # noqa: E402 + +REPO_ROOT = Path(__file__).resolve().parents[1] +MIGRATIONS = [ + REPO_ROOT / "migrations" / "0001_registries.sql", + REPO_ROOT / "migrations" / "0002_ledger.sql", + REPO_ROOT / "migrations" / "0003_attestations.sql", +] + +pytestmark = pytest.mark.skipif( + shutil.which("docker") is None, reason="docker not available" +) + + +@pytest.fixture(scope="module") +def pg_container(): + name = f"trf-test-pg-onboard-{uuid.uuid4().hex[:8]}" + subprocess.run( + [ + "docker", "run", "--rm", "-d", + "--name", name, + "-e", "POSTGRES_PASSWORD=postgres", + "-e", "POSTGRES_DB=target_revenue_test", + "-p", "127.0.0.1::5432", + "postgres:16-alpine", + ], + check=True, capture_output=True, + ) + try: + port_out = subprocess.run( + ["docker", "port", name, "5432/tcp"], check=True, capture_output=True, text=True + ).stdout.strip() + host_port = port_out.split(":")[-1] + dsn = f"host=127.0.0.1 port={host_port} dbname=target_revenue_test user=postgres password=postgres" + + for _ in range(60): + try: + with psycopg.connect(dsn, connect_timeout=1): + break + except psycopg.OperationalError: + time.sleep(0.5) + else: + raise RuntimeError("postgres container did not become ready in time") + + with psycopg.connect(dsn) as conn: + for migration in MIGRATIONS: + conn.execute(migration.read_text(encoding="utf-8")) + conn.commit() + token = "test-token-onboard" + conn.execute( + "INSERT INTO licensors (token, licensor_id) VALUES (%s, %s)", + (token, "onboard-corp"), + ) + conn.commit() + + app_dsn = ( + f"host=127.0.0.1 port={host_port} dbname=target_revenue_test " + f"user=trf_app password=changeme-in-deployment" + ) + yield {"app_dsn": app_dsn, "token": token} + finally: + subprocess.run(["docker", "stop", name], capture_output=True) + + +@pytest.fixture() +def live_server(pg_container, monkeypatch): + monkeypatch.setenv("TRF_DATABASE_URL", pg_container["app_dsn"]) + monkeypatch.setenv("TRF_SIGNING_KEY_HEX", "22" * 32) + from target_revenue.service import app as app_module + + if hasattr(app_module.app.state, "pool"): + app_module.app.state.pool.close() + del app_module.app.state.pool + if hasattr(app_module.app.state, "signing_key"): + del app_module.app.state.signing_key + + config = uvicorn.Config(app_module.app, host="127.0.0.1", port=0, log_level="warning") + server = uvicorn.Server(config) + thread = threading.Thread(target=server.run, daemon=True) + thread.start() + for _ in range(100): + if getattr(server, "started", False): + break + time.sleep(0.05) + else: + raise RuntimeError("uvicorn server did not start in time") + + port = server.servers[0].sockets[0].getsockname()[1] + try: + yield f"http://127.0.0.1:{port}" + finally: + server.should_exit = True + thread.join(timeout=5) + + +def test_onboard_cli_registers_phase_and_appends_entry_against_live_server( + live_server, pg_container, tmp_path, monkeypatch, capsys +): + import importlib.util + + spec = importlib.util.spec_from_file_location("trf_onboard", REPO_ROOT / "scripts" / "trf_onboard.py") + trf_onboard = importlib.util.module_from_spec(spec) + spec.loader.exec_module(trf_onboard) + + manifest = golden_manifest() + manifest["phase"]["id"] = manifest["phase"]["id"] + "-onboard-" + uuid.uuid4().hex[:6] + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + + monkeypatch.setenv("TRF_ONBOARD_TEST_TOKEN", pg_container["token"]) + + trf_onboard.main( + [ + "register-phase", + "--url", live_server, + "--token-env", "TRF_ONBOARD_TEST_TOKEN", + "--manifest", str(manifest_path), + ] + ) + + entry = { + "id": "trsl:entry:onboardcli0001", + "phase": manifest["phase"]["id"], + "type": "development-credit", + "amount": 1000, + "currency": "USD", + "recognized_at": "2026-08-01T00:00:00Z", + "evidence_reference": "confidential:evidence:onboardcli0001", + "extension": {"id": "trsl:extension:development-license", "version": "1.0"}, + } + entry_path = tmp_path / "entry.json" + entry_path.write_text(json.dumps(entry), encoding="utf-8") + + trf_onboard.main( + [ + "append-entry", + "--url", live_server, + "--token-env", "TRF_ONBOARD_TEST_TOKEN", + "--phase-id", manifest["phase"]["id"], + "--entry", str(entry_path), + ] + ) + + capsys.readouterr() # discard register-phase/append-entry output + trf_onboard.main(["status", "--url", live_server, "--phase-id", manifest["phase"]["id"]]) + status = json.loads(capsys.readouterr().out) + assert status["facts"]["cumulative_development_credit"] == 1000 diff --git a/tests/test_trf_onboard.py b/tests/test_trf_onboard.py new file mode 100644 index 0000000..133ea4a --- /dev/null +++ b/tests/test_trf_onboard.py @@ -0,0 +1,87 @@ +"""Pure, offline tests for scripts/trf_onboard.py (WP-0006-T07). + +No network call is made in these tests — they verify the offline +pre-flight validation short-circuits before any HTTP attempt, and that a +missing token env var fails loudly rather than silently sending an +unauthenticated request. +""" + +from __future__ import annotations + +import importlib.util +import json +import sys +from pathlib import Path + +import pytest +from conftest import golden_manifest + +REPO_ROOT = Path(__file__).resolve().parents[1] +_SPEC = importlib.util.spec_from_file_location("trf_onboard", REPO_ROOT / "scripts" / "trf_onboard.py") +trf_onboard = importlib.util.module_from_spec(_SPEC) +_SPEC.loader.exec_module(trf_onboard) + + +def test_validate_accepts_golden_manifest(tmp_path, capsys): + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text(json.dumps(golden_manifest()), encoding="utf-8") + + trf_onboard.main(["validate", str(manifest_path)]) + + assert "OK" in capsys.readouterr().out + + +def test_validate_rejects_broken_manifest(tmp_path): + broken = golden_manifest() + del broken["phase"]["initial_target"] + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text(json.dumps(broken), encoding="utf-8") + + with pytest.raises(SystemExit): + trf_onboard.main(["validate", str(manifest_path)]) + + +def test_register_phase_rejects_offline_before_any_network_call(tmp_path, monkeypatch): + """A non-conformant manifest must be caught by the offline check — + _request must never be called at all.""" + broken = golden_manifest() + del broken["phase"]["initial_target"] + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text(json.dumps(broken), encoding="utf-8") + + def _boom(*args, **kwargs): + raise AssertionError("no network call should have been made") + + monkeypatch.setattr(trf_onboard, "_request", _boom) + monkeypatch.setenv("TRF_TEST_TOKEN", "irrelevant") + + with pytest.raises(SystemExit): + trf_onboard.main( + [ + "register-phase", + "--url", "http://example.invalid", + "--token-env", "TRF_TEST_TOKEN", + "--manifest", str(manifest_path), + ] + ) + + +def test_register_phase_requires_token_env_to_be_set(tmp_path, monkeypatch): + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text(json.dumps(golden_manifest()), encoding="utf-8") + monkeypatch.delenv("TRF_TEST_TOKEN_UNSET", raising=False) + + def _boom(*args, **kwargs): + raise AssertionError("no network call should have been made without a token") + + monkeypatch.setattr(trf_onboard, "_request", _boom) + + with pytest.raises(SystemExit, match="TRF_TEST_TOKEN_UNSET"): + trf_onboard.main( + [ + "register-phase", + "--url", "http://example.invalid", + "--token-env", "TRF_TEST_TOKEN_UNSET", + "--manifest", str(manifest_path), + ] + ) diff --git a/workplans/TREV-WP-0006-trust-service-implementation.md b/workplans/TREV-WP-0006-trust-service-implementation.md index 4e2c767..25f8a43 100644 --- a/workplans/TREV-WP-0006-trust-service-implementation.md +++ b/workplans/TREV-WP-0006-trust-service-implementation.md @@ -273,7 +273,7 @@ passing under the Docker-gated suite; no stray containers left running. ```task id: TREV-WP-0006-T07 -status: todo +status: done priority: medium state_hub_task_id: "44c5846e-1f37-41fe-8ac0-fc7d88628e9f" ``` @@ -287,6 +287,32 @@ Trust Service (the registration call). Coordinate with the actual first repos — this task defines the mechanism, not the pilot selection. +**Result:** `specs/TrustServiceOnboarding.md` defines the mechanism: a +Phase Manifest file is committed to the declaring repo (durable, +git-history-backed, independently foldable forever) *and* registered with +the hosted service (the multi-tenant, publicly-servable copy); once +registered, the Ledger's live authoritative copy is the hosted service +only, not a second competing file — a repo may keep periodic exports for +its own offline re-verification, but those are copies, not a source of +truth. Licensor token bootstrapping is explicitly out of this mechanism's +scope (a WP-0008-T01 governance action). `scripts/trf_onboard.py` +implements it as a dependency-light CLI (stdlib `urllib` + the existing +`target_revenue.validation`, no FastAPI/psycopg required to onboard a +Phase) with four subcommands: `validate` (offline only, no network), +`register-phase` (validates offline before ever attempting the network +call), `append-entry`, and `status` (public, no token). The Licensor API +token is read only from an environment variable named on the command +line, never accepted as a literal argument or committed. `tests/test_trf_onboard.py` +(4 tests, no network, no Docker) proves both invalid-manifest and +missing-token-env cases fail before any HTTP attempt is made — asserted by +monkeypatching the request function to raise if called at all. +`tests/test_onboarding_hosted.py` (1 Docker-gated test) is the one test in +the suite that runs an actual `uvicorn` server on a real socket (rather +than FastAPI's in-process TestClient, since the CLI genuinely speaks HTTP) +and drives the full register → append → status round trip through the CLI +exactly as an external repo would invoke it. Full suite: 49 passing +offline, 71 passing with Docker; no stray containers left running. + ## Conformance test suite at hosted scale ```task