Builds the Control Plane's browser UI (login, dashboard, Phase registration, Development Credit entry/proposal/review, credential admin, audit log) as a FastAPI + Jinja2 app over the already-finished T03 backend, rather than from scratch — whynot-design's Lit web components are vendored as static assets (source commit 4b62cffc, v0.4.1), with lit itself resolved via an esm.sh CDN import map. Session auth re-checks the credential token against the database on every request rather than trusting the session cookie's cached rights, so a mid-session revocation takes effect immediately. 9 new Docker-gated HTTP-level tests via FastAPI's TestClient (no browser-automation tool available, so real rendering of the <wn-*> components was never visually verified). All four WP-0009 tasks are now done; workplan marked finished. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
257 lines
13 KiB
Markdown
257 lines
13 KiB
Markdown
---
|
||
id: TREV-WP-0009
|
||
type: workplan
|
||
title: "Target Revenue Control Plane"
|
||
domain: infotech
|
||
repo: target-revenue
|
||
status: finished
|
||
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: done
|
||
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.
|
||
|
||
**Result:** Built on `whynot-design` (vendored, source commit
|
||
`4b62cffc86496d587ac8d48a8e199624bc4a5c1f`, v0.4.1 — see
|
||
`src/target_revenue/service/static/whynot-design/VENDORED.md` for what is
|
||
vendored versus resolved via an `esm.sh` CDN import map for the `lit`
|
||
peer dependency, a disclosed v0 trade-off) rather than from scratch, per
|
||
the explicit direction to check feasibility first. New FastAPI app
|
||
`src/target_revenue/service/control_plane_app.py`, session-based sign-in
|
||
(paste-a-credential-token, `itsdangerous`-signed cookie via Starlette's
|
||
`SessionMiddleware`, re-authenticated against the database on every
|
||
request so a mid-session revocation takes effect immediately, never
|
||
trusted from the cookie alone), seven Jinja2 templates under
|
||
`service/control_plane_templates/` (`base`, `login`, `dashboard`,
|
||
`phase_new`, `phase_detail`, `proposals`, `admin_credentials`, `audit`),
|
||
all wired to real `control_plane.py`/`registry.py`/`ledger.py`/`metrics.py`
|
||
calls — no mock data. Routes cover: login/logout; dashboard (list Phases
|
||
for the signed-in tenant); Phase registration (Operator+); Phase detail
|
||
(status/metrics + Ledger table); the priority flow — append a
|
||
Development Credit entry directly (Operator+/Admin) or submit it for
|
||
review (Contributor), same form, button label switches on rights;
|
||
proposal review (approve/reject, Operator+); credential issue/revoke
|
||
(Admin); and the Control Plane's own audit log (any signed-in user).
|
||
Rights gating is enforced identically to T03's backend — the UI layer
|
||
adds no new authorization logic, it only redirects with a flash message
|
||
when a route requires more than the signed-in credential's tier.
|
||
|
||
Tests: `tests/test_control_plane_app.py`, 9 Docker-gated tests via
|
||
FastAPI's `TestClient` (same disposable-Postgres-per-module pattern as
|
||
`test_control_plane.py`) — covers unauthenticated redirect to `/login`,
|
||
invalid-token rejection, successful login/dashboard, rights-gated route
|
||
redirects (viewer blocked from Phase registration and admin pages), the
|
||
full Operator register-Phase-then-append-entry flow, the
|
||
Contributor-proposes/Operator-reviews flow, and admin credential
|
||
issuance. Explicit scope disclosure: these are HTTP-level tests against
|
||
the FastAPI app, not real-browser tests — no browser-automation tool is
|
||
available in this environment, so the `<wn-*>` web components' actual
|
||
rendering/interactivity was never visually verified, only the
|
||
server-rendered HTML/session/redirect behavior around them. Full suite:
|
||
84 passing offline (unchanged), 146 passing with Docker (up from 53); no
|
||
stray containers left running.
|
||
|
||
All four WP-0009 tasks (T01–T04) are now done — this workplan is
|
||
finished.
|