--- id: TREV-WP-0009 type: workplan title: "Target Revenue Control Plane" domain: infotech repo: target-revenue status: active owner: claude topic_slug: infotech created: "2026-07-30" updated: "2026-07-30" state_hub_workstream_id: "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) ```task 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 ```task 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.sql` — `licensors` 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 ```task 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 ```task 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.