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>
13 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 | finished | 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.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
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: 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 manualtarget_basisentry path must work standalone too, since the Calculator may not be ready first); - the priority flow: interactively creating a
development-creditledger 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 updatedGET /phases/{id}/metricsresult; - 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.