diff --git a/docs/adr/ADR-0002-hosted-trust-service-stack.md b/docs/adr/ADR-0002-hosted-trust-service-stack.md new file mode 100644 index 0000000..f5d9474 --- /dev/null +++ b/docs/adr/ADR-0002-hosted-trust-service-stack.md @@ -0,0 +1,211 @@ +--- +id: ADR-0002 +title: "Hosted Trust Service stack: storage, auth, and deployment target" +status: proposed +date: 2026-07-29 +decided_by: null +accepted_at: null +workstream: TREV-WP-0006 (trust-service-implementation) +alternatives_considered: [Go service, file/git-based ledger store, per-repo-hosted SQLite] +--- + +# ADR-0002 — Hosted Trust Service Stack (Storage, Auth, Deployment) + +## Status + +**Proposed, not yet accepted.** Per `docs/adr/ADR-0001-stage0-library-stack.md`'s +own Revisit trigger, this is that separate, later decision — hosting, +multi-tenant storage, and authentication were explicitly out of ADR-0001's +scope and are not inherited from it. This ADR requires explicit maintainer +sign-off (`workplans/TREV-WP-0006-trust-service-implementation.md` T02, +`human_accept_required: true`) before any of T03–T08 build against it. + +## Scope + +Three decisions, per T02's task description: + +1. Storage engine for the append-only Target Ledger at multi-repo scale. +2. Authentication/authorization model for Phase Registry and Ledger writes. +3. Hosting/deployment target. + +This ADR does **not** decide API framework internals beyond what's needed +to justify the storage/auth/hosting choices, does not draft the actual API +routes (`specs/TrustServiceProductRequirementsDocument.md` §6 previews +those, non-binding), and does not resolve the Breach/Compliance Record +component's ownership gap flagged in that same PRD §4 — that remains a +separate open item. + +## Context + +`specs/TrustServiceProductRequirementsDocument.md` (WP-0006-T01) specifies +what the hosted service must do: serve multiple Phases/Licensors +concurrently (TS-FR-8), enforce phase-scoped authenticated writes (TS-FR-3), +preserve WP-0002's append-only/hash-chained/deterministic-fold guarantees +under hosting (§3 of that PRD), and publish public facts alongside +gated confidential evidence (TS-FR-4). The immediate driver is +`workplans/TREV-WP-0008-governance-and-pilot-rollout.md`'s four target +product lines (`coulomb-loop`, `net-kingdom`, `helix-forge`, `railiance-*`), +all within the same Forgejo org — this is an internal-scale multi-tenant +problem (single-digit to low-double-digit Licensors at pilot stage), not an +internet-scale one. + +Existing infrastructure in this org, per the state hub's repo-goal records: + +- `railiance-infra` provisions and hardens a HostEurope server. +- `railiance-cluster` installs k3s, Helm, GitOps tooling, monitoring, and + "essential platform services" on that server, explicitly as reusable + baseline infrastructure for `coulomb`-org projects — not a repo-specific + deployment target only `railiance-cluster` itself may use. + +This means a hosting decision does not start from zero: a Kubernetes +baseline with GitOps deployment already exists and is the natural fit +target-revenue's own PRD (T02's task description) named as the first +candidate to investigate, not assume. + +## Decision + +**Storage: PostgreSQL**, one `target_ledger_entries` table partitioned +logically by `phase_id`, with database-level enforcement — not just +application-level convention — that settled entries cannot be updated or +deleted: + +- the application's database role has `INSERT`/`SELECT` only on this table, + no `UPDATE`/`DELETE` grant at all; +- a unique constraint on `(phase_id, entry_id)` and a foreign-key-shaped + check that `previous_entry_hash` matches the prior entry for that + `phase_id`, enforced by a trigger, not solely by application code; +- `phase_manifests` and `extensions` as separate tables, `JSONB` columns for + the less rigidly-typed fields (`target_basis`, extension contract + metadata) so schema evolution doesn't require a migration for every new + optional field. + +**Authentication: per-Licensor API token for write access, independent from +per-entry Ed25519 signing.** Two distinct guarantees, not one mechanism +doing both jobs: + +- each registered Licensor (one per product line at pilot stage, per + WP-0008-T01's Licensor-identity question) is issued a bearer token scoped + to write only within its own `phase_id` namespace — this is service-level + access control, revocable and rotatable without touching the ledger's + cryptographic history; +- every accepted Ledger entry is still signed with the Trust Service + instance's Ed25519 key over the canonical bytes (TSD §3.2, ADR-0001's + established crypto), independent of which token authenticated the write — + this is the integrity guarantee an external verifier checks, and it does + not depend on trusting the token layer; +- confidential-evidence read access (TS-FR-4) uses the same token + infrastructure with a separate, narrower read-grant scope, so a Licensor + can delegate temporary read access (e.g., to an Enforcement Partner + investigating an Alleged Violation, `specs/EnforcementNetworkConcept.md`) + without issuing a write-capable credential. + +**Hosting: `railiance-cluster`'s existing k3s deployment on the +`railiance-infra`-provisioned HostEurope server**, packaged as a +containerized service with a Helm chart following that cluster's existing +GitOps pattern, rather than provisioning separate infrastructure. PostgreSQL +runs as a dedicated StatefulSet in the same cluster (or against a shared +cluster Postgres instance, if `railiance-cluster` already operates one for +another service — an open item for T03 to confirm at implementation time, +not assumed here). + +**Application language: Python**, wrapping `src/target_revenue/` directly +rather than reimplementing the fold/hashing/validation logic in a second +language. A thin HTTP layer (a lightweight ASGI framework — the specific +package is an implementation detail for T03/T04, not fixed by this ADR) adds +the network boundary, auth, and persistence TS-FR-1–9 require; the pure fold +and canonical-serialization code that WP-0002 already tested (36 passing +tests) is called, not rewritten. + +## Rationale + +| Requirement | Choice | Why | +|---|---|---| +| Append-only enforcement must survive a hosting layer that could accidentally add an update path (TrustServicePRD §3 point 4) | Postgres with revoked `UPDATE`/`DELETE` grants + a trigger | Makes "no mutation of settled entries" a database-enforced fact, not just an API design intention that a future endpoint could quietly violate | +| Deterministic fold must match between hosted and offline computation (TrustServicePRD TS-NFR-1, WP-0006-T04's stated highest-risk property) | Reuse `src/target_revenue/fold.py` unchanged inside the hosted service | Eliminates the entire class of bug where a second, hand-written fold implementation drifts from the tested one | +| Multi-tenant isolation (TS-FR-8) must be a security property, not a convention | Per-Licensor token scoped at the database-query level (row-level filtering by `phase_id` ownership), not just at the API-route level | An API-layer-only check can be bypassed by a bug in one route; a query-level scope is harder to forget | +| Existing infra should be reused, not duplicated, for an internal-scale service | `railiance-cluster`'s k3s + GitOps baseline | That repo's own stated goal is to be reusable "coulomb infrastructure," and this avoids a second hosting decision this org would have to maintain | +| Ledger integrity must not depend on trusting the hosting/access-control layer | Ed25519 signature over canonical bytes, independent of the API token | Matches TrustServicePRD §3 point 2 (offline verifiability) — a party with an exported ledger and the public key can verify without trusting the token system at all | + +### Why not a Go service + +`docs/adr/ADR-0001-stage0-library-stack.md` names Go as fitting "a +long-lived, standalone, performance-sensitive service." This is not (yet) +that: pilot-stage throughput is a handful of Phases across four internal +product lines, not a high-throughput public API. Reusing the +already-tested Python fold avoids a second implementation of the exact +logic WP-0002 spent its test suite validating. Revisit if throughput, +latency, or team composition changes materially post-pilot. + +### Why not a file/git-based ledger store + +`workplans/TREV-WP-0008-governance-and-pilot-rollout.md` T05 already +countenances "an explicit interim manual/git-based ledger process" as an +acceptable **fallback** if the hosted service isn't ready in time for the +first pilot Phase. That is a reasonable stopgap precisely because it avoids +concurrent-writer problems by having only one thing (a human, or a very +small number of them) append at a time. It is a poor default for the actual +hosted service once multiple product lines write concurrently: a flat file +or git-commit-based ledger has no native transactional guarantee against a +lost-update race between two near-simultaneous append requests, which a +relational database's row-level locking handles as a solved problem. Keep +the git-based path available as WP-0008-T05's named fallback; do not build +the hosted service's primary storage on it. + +### Why not per-repo-hosted SQLite + +Would avoid running a shared database service, but reintroduces exactly the +isolation and concurrent-access problems a single shared Postgres instance +with per-tenant row scoping already solves, while adding an operational +question (where does each repo's SQLite file actually live, who backs it +up) that `railiance-cluster`'s existing platform-services remit is better +positioned to answer once, centrally. + +## Consequences + +### Positive + +- No new infrastructure to provision — reuses `railiance-infra`/ + `railiance-cluster`'s already-stated purpose. +- Ledger integrity (signature + hash chain) is decoupled from access + control (tokens), so revoking or rotating a Licensor's credentials never + touches the cryptographic history a third party would verify. +- Reusing `src/target_revenue/` means WP-0002's 36 tests continue to cover + the core fold/hash logic inside the hosted service, not just a + now-unused offline library. + +### Negative / risks + +- Single HostEurope server is a single point of failure. Acceptable for + alpha/beta pilot scale (small, internal, `coulomb`-org-only usage per + `SCOPE.md` §1's alpha/beta risk-acceptance framing) but must be revisited + before any external or higher-availability commitment. +- Database-level append-only enforcement (revoked grants + trigger) must + actually be tested as part of T04's conformance work, not assumed correct + because it sounds right on paper — a misconfigured role grant would + silently reintroduce the exact risk this ADR is designed to close. +- Whether `railiance-cluster` already runs a shared Postgres instance + suitable for reuse, or whether this service needs its own, is not + resolved here and is explicitly left to T03 to confirm before + implementation, not assumed by this ADR. + +### Compensating guardrails + +1. `src/target_revenue/fold.py` and `hashing.py` are imported, not + reimplemented, inside the hosted service's request handlers. +2. Database migrations must never grant the application role `UPDATE` or + `DELETE` on `target_ledger_entries`; any migration that does is a + defect, not a convenience fix, and should be caught in review. +3. T04's conformance testing must include an explicit test that attempts + to update or delete a settled entry through every available access path + (API, direct DB role) and asserts each is rejected. +4. T08's hosted conformance suite must assert hosted-computed and + offline-computed folds agree on the same exported ledger, per + TrustServicePRD TS-NFR-1. + +## Revisit trigger + +Reconsider this decision if: pilot usage moves beyond the four named +`coulomb`-org product lines to external, higher-throughput, or +higher-availability requirements; `railiance-cluster`'s k3s baseline is +retired or materially changed; or T03's implementation discovers the +single-Postgres-instance assumption doesn't hold operationally. diff --git a/workplans/TREV-WP-0006-trust-service-implementation.md b/workplans/TREV-WP-0006-trust-service-implementation.md index 61f0700..af1e678 100644 --- a/workplans/TREV-WP-0006-trust-service-implementation.md +++ b/workplans/TREV-WP-0006-trust-service-implementation.md @@ -92,6 +92,16 @@ assumed by default. **Human accept gate:** per the same policy as ADR-0001 — agents may draft, must not treat as accepted without explicit maintainer sign-off. +**Status: ready for review, not done.** Drafted at +`docs/adr/ADR-0002-hosted-trust-service-stack.md`: PostgreSQL (append-only +enforced at the database-grant/trigger level, not just convention) for +storage; per-Licensor API token for write/read-grant access control, +decoupled from the existing Ed25519 per-entry signature; `railiance-cluster`'s +existing k3s/Helm/GitOps baseline (on the `railiance-infra`-provisioned +HostEurope server) for hosting, rather than new infrastructure; Python +reusing `src/target_revenue/` directly behind a thin HTTP layer. Awaiting +maintainer sign-off before T03–T08 build against it. + ## Phase Registry and Extension Registry hosting ```task