target-revenue/workplans/TREV-WP-0009-target-revenue-control-plane.md
tegwick 7986e62f31 Extend WP-0006 auth for per-human sub-credentials (WP-0009-T02)
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: licensor_id couldn't
simply become non-unique, since phase_manifests, extensions, and
breach_records all FK to licensors(licensor_id), which only worked
because that column used to be unique. Introduced licensor_identities
(one row per tenant) as the new FK target for all four tables, with an
ensure_licensor_identity trigger auto-creating the identity on first
credential insert - so existing code (including every earlier test
fixture) needed no changes.

registry.py: Licensor gains credential_label/rights; RIGHTS_TIERS +
has_right() ordinal helper (enforcement is Control Plane's job, T03/
T04, not this task's); issue_sub_credential/revoke_sub_credential
(revocation via a SECURITY DEFINER function, matching
set_extension_status's existing pattern - trf_app has no UPDATE grant
on licensors); authenticate() rejects revoked credentials identically
to unrecognized ones.

Attribution scoped honestly: ledger_entry.schema.json stays unmodified
(frozen Stage 0 surface, additionalProperties:false) - per-entry human
attribution is a hosting-layer-only column
(ledger_entries.submitted_by_token, ledger.get_ledger_attribution()),
recorded alongside but never inside the signed entry payload. Narrower
than "the signature names the human," but 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 (append_entry's INSERT now references the new
column). New tests/test_licensor_credentials.py (8 tests): multi-
credential resolution, duplicate-label rejection, revocation and its
idempotence, invalid-rights rejection, the has_right helper, per-entry
attribution recorded and not leaking into exported ledger JSON, and
DB-level UPDATE rejection. Full suite: 84 offline, 41 with Docker (up
from 30); no stray containers left running.
2026-07-30 14:26:33 +02:00

183 lines
8.6 KiB
Markdown

---
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: todo
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.
## 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.