target-revenue/workplans/TREV-WP-0009-target-revenue-control-plane.md
tegwick 885da0a1cb Implement Control Plane backend: rights enforcement + audit log (WP-0009-T03)
migrations/0006_control_plane.sql: control_plane_audit_log (append-only,
no UPDATE/DELETE for trf_app) and control_plane_proposed_entries (the
Contributor tier's "propose, don't append" workflow from concept §2) -
review decisions go through a review_proposed_entry() SECURITY DEFINER
function, same governance-action pattern as
set_extension_status/revoke_credential, not a direct UPDATE.

src/target_revenue/control_plane.py is the enforcement layer concept
§2 called for: register_phase/append_development_credit require
Operator+; propose_ledger_entry requires Contributor+ and stores a
pending proposal without touching the real Ledger; approve_proposed_entry
(Operator+) appends it under the *reviewer's own* credential/attribution
(not the original proposer's - the reviewer is who's authorizing it into
the real Ledger, while the proposer stays on record in the proposal row
and audit log); reject_proposed_entry (Operator+) discards it. issue_/
revoke_user_credential (Admin+) wrap registry.py's T02 functions with
the same rights check and audit logging. Every action funnels through
record_audit_event, independent of the Trust Service's own signed
records.

tests/test_control_plane.py (12 tests): rights enforcement at each
tier boundary, the full propose -> approve -> appended-under-reviewer
flow, propose -> reject -> nothing appended, double-review rejection,
audit log content/attribution, DB-level UPDATE rejection on both new
tables. Full suite: 84 offline (unchanged), 53 with Docker (up from
41); no stray containers left running.
2026-07-30 15:08:59 +02:00

10 KiB

id type title domain repo status owner topic_slug created updated state_hub_workstream_id
TREV-WP-0009 workplan Target Revenue Control Plane infotech target-revenue active claude infotech 2026-07-30 2026-07-30 0e8dfd43-a742-4458-9e9b-514a9284821b

Target Revenue Control Plane

An interactive UI over the hosted Trust Service (workplans/TREV-WP-0006-trust-service-implementation.md, finished), usable by a human user with appropriate rights acting as the binky tenant. The specific, named requirement driving this workplan: make it possible to interactively register Phases and create Development Credit ledger entries, rather than requiring raw API calls or the scripts/trf_onboard.py CLI.

Split from the former combined TREV-WP-0009-control-plane-and-effort-calculator.md (2026-07-30) — the Development Effort Calculator is a separate concern with its own formula decision and implementation arc; it now lives in workplans/TREV-WP-0010-development-effort-calculator.md. The two remain related (the Control Plane's Phase-registration flow is expected to use the Calculator's output, per T04 below) but are sequenced and reviewed independently.

Does not include: declaring any real Phase for any repo, or changing the Trust Service's core guarantees (determinism, append-only, no discretionary conversion authority) — the Control Plane is a client of the existing hosted service, bound by the same rules any other client is.

Concept: specs/TargetRevenueControlPlaneConcept.md.

User rights model — decision (human gate)

id: TREV-WP-0009-T01
status: done
priority: high
human_accept_required: true
human_accepted_by: Bernd
human_accepted_at: "2026-07-30"
state_hub_task_id: "177117e7-955b-4f12-b17b-75ee3f0357c4"

specs/TargetRevenueControlPlaneConcept.md §2 proposes a four-tier rights model (Viewer/Contributor/Operator/Admin) and recommends option (b) for the auth-attribution question: the Control Plane holds the one binky Licensor token server-side and layers its own human-user auth/audit log in front of it, rather than requiring a WP-0006 schema change for per-human sub-credentials (option (a)). Confirm the rights tiers and the (a)-vs-(b) choice, or propose a refinement. Agents may prepare a recommendation and leave this todo.

Accepted 2026-07-30 by the maintainer (Bernd): the four rights tiers are confirmed as proposed. Option (a) is adopted, not (b) — the Control Plane will issue per-human-user sub-credentials at the Trust Service layer, mapped to the same underlying binky Licensor identity, so that the Trust Service's own signed ledger records can attest to the specific human who acted, not merely "the binky Licensor did this." This means a real, scoped extension to WP-0006's already-finished licensors/token auth model is now a firm prerequisite, not a hypothetical branch — see T02 below, added specifically for this reason.

WP-0006 auth extension: per-human sub-credentials

id: TREV-WP-0009-T02
status: done
priority: high
state_hub_task_id: "52ae3a7a-6b55-4694-8bc1-cb0e32a4fe31"

Extend migrations/0001_registries.sql's licensors/token model and src/target_revenue/registry.authenticate so a single Licensor identity (binky) can have multiple, individually-issued, individually-revocable sub-credentials, each resolving to the same licensor_id for phase- ownership checks (registry.py, ledger.py) but distinguishable in the returned Licensor/signing context so a ledger entry's signature (or an accompanying attributable field) can reflect which sub-credential signed it. This is a change to WP-0006's finished, tested auth layer — treat it with the same care as any change to already-shipped, tested code: new tests proving existing single-token behavior is unaffected, plus new tests for the sub-credential path. Does not change the Ledger's append-only guarantees or the hash-chain/signature scheme itself, only who may authenticate as binky and how that's distinguished.

Result: migrations/0005_licensor_credentials.sqllicensors can now hold multiple rows per licensor_id (credential_label, rights tier, issued_by, revoked_at). Real structural finding along the way: licensor_id could not simply become non-unique, because phase_manifests, extensions, and breach_records all carry a foreign key to licensors(licensor_id), which only worked because that column used to be unique — a FK target must be unique. Introduced a new licensor_identities table (one row per tenant) as the FK target for all four tables instead, with an ensure_licensor_identity trigger that auto-creates the identity row on first credential insert (so existing code that inserts directly into licensors — including every earlier test fixture — needed no changes), plus registry.create_licensor_identity for callers that want tenant onboarding as its own explicit step. registry.py gained Licensor.credential_label/.rights, RIGHTS_TIERS/has_right() (an ordinal helper — enforcing what each tier may do is Control Plane's own job, T03/T04, not this task's), issue_sub_credential/ revoke_sub_credential (revocation via a revoke_credential() SECURITY DEFINER function, matching set_extension_status's existing pattern — trf_app has no UPDATE grant on licensors). authenticate() now rejects a revoked credential identically to an unrecognized one.

Attribution, scoped honestly: ledger_entry.schema.json was deliberately left unmodified (frozen Stage 0 normative surface, additionalProperties: false) — per-entry human attribution is instead a hosting-layer-only column, ledger_entries.submitted_by_token, recorded alongside but never inside the signed entry payload (ledger.get_ledger_attribution). This means the claim is narrower than "the signature itself names the human": the cryptographic signature is unchanged and still only attests to the entry content and chain; the database additionally knows which credential submitted each entry, queryable but not portable/exported with the entry itself. Exactly the "(or an accompanying attributable field)" alternative this task's own description anticipated.

All four Docker-gated test files that append Ledger entries needed migration 0005 added to their setup (ledger.append_entry's INSERT now references the new column) — done for test_registry_hosting.py/test_ledger_hosting.py/ test_hosted_conformance.py/test_onboarding_hosted.py. New tests/test_licensor_credentials.py (8 tests): multi-credential resolution to the same licensor_id, duplicate-active-label rejection, revocation and its idempotence, invalid-rights rejection, the has_right ordinal helper, per-entry attribution recorded and not leaking into the exported ledger JSON, and DB-level UPDATE rejection on licensors. Full suite: 84 passing offline (unchanged), 41 passing with Docker (up from 30); no stray containers left running.

Control Plane backend: auth layer and audit log

id: TREV-WP-0009-T03
status: done
priority: high
state_hub_task_id: "86a58e61-dccd-4678-b694-22a9eaea2b3c"

Using T02's sub-credential extension, implement the Control Plane's own human-user authentication/authorization layer (issuing and managing sub-credentials per the four rights tiers) and its own audit log (concept §5) — which human user took which action, timestamped, alongside the Trust Service's own signed record id for that action. This is the piece that must exist before any write-capable UI flow (T04) can be built responsibly.

Result: migrations/0006_control_plane.sql adds control_plane_audit_log (append-only, no UPDATE/DELETE grant for trf_app) and control_plane_proposed_entries (the Contributor tier's "propose, don't append" workflow from concept §2 — review decisions go through a review_proposed_entry() SECURITY DEFINER function, same governance-action pattern as set_extension_status/revoke_credential, not a direct UPDATE). src/target_revenue/control_plane.py is the enforcement layer concept §2 called for: register_phase/ append_development_credit require Operator+; propose_ledger_entry requires Contributor+ and stores a pending proposal rather than touching the real Ledger; approve_proposed_entry (Operator+) appends it — under the reviewer's own credential/attribution, not the original proposer's, since the reviewer is the one authorizing it into the real Ledger, while the proposer stays on record in the proposal row and audit log; reject_proposed_entry (Operator+) discards it with nothing appended. issue_user_credential/revoke_user_credential (Admin+) wrap registry.py's T02 functions with the same rights check and audit logging. Every action funnels through record_audit_event — actor credential label, action, Phase, and (where applicable) the resulting Trust Service record id, independent of the Trust Service's own signed records.

tests/test_control_plane.py (12 tests): rights enforcement at each tier boundary (Viewer can't register, Contributor can't append directly or review, non-Admin can't issue credentials), the full propose → approve → appended-under-reviewer's-credential flow, propose → reject → nothing appended, double-review rejection, audit log content and attribution, and DB-level UPDATE rejection on both new tables. Full suite: 84 passing offline (unchanged), 53 passing with Docker (up from 41); no stray containers left running.

Control Plane interactive UI: Phase registration and Development Credit entry

id: TREV-WP-0009-T04
status: todo
priority: high
state_hub_task_id: "01b295f2-8f97-4fc4-a14e-6f68aa85458d"

Implement the interactive flows themselves, using T03's auth/audit layer and the existing hosted Trust Service API (service/app.py) plus T02's sub-credential extension as the backend:

  • Phase registration, informed by workplans/TREV-WP-0010-development-effort-calculator.md's output once available (not blocking — a manual target_basis entry path must work standalone too, since the Calculator may not be ready first);
  • the priority flow: interactively creating a development-credit ledger entry against a selected, already-registered Phase (concept §3's step-by-step description) — select Phase, fill amount/currency/ recognized-date/evidence-reference/Monetization-Extension, submit, immediately show the updated GET /phases/{id}/metrics result;
  • read-only views (Phase status, Ledger, Attestation, Breach/Compliance Record history) — already public/unauthenticated at the Trust Service layer, so these need no new backend work, only UI.