feat(approvals,audit): record the reviewer as the unchecked claim it is

The hub copies the reviewer string a client sends straight into
decided_by (DecideApproval, ClearEventLog); nothing on the wire ties
it to the authenticated caller. Studio filled it from the OS account,
so an export read like non-repudiation while being an arbitrary
client claim — the legal finding of the 2026-07-26 usertest panel.

The real fix is hub-side (derive decided_by from CALLER_IDENTITY);
that contract is written down in docs/reviewer-identity.md and needs
a hub release. Until then Studio does the one thing it can do
honestly and marks its own claim as a claim, inside the record:

- data/reviewer_identity.dart is the single place that produces and
  reads the value; wire() is idempotent, so page and HubService may
  both normalise. Every write path funnels through HubService, so no
  surface can send a bare handle.
- The inbox states before the decision who will be recorded, what
  that attribution is worth on this hub (from AuthStatus), and the
  literal string that lands in decided_by. An unreadable auth policy
  stays unreadable — never optimistic.
- Reading back: a marked value shows its plain name plus an
  unchecked flag; an unmarked one (legacy row, CLI decision, or a
  future hub-derived identity) is not classified either way.
- The audit wipe seeds the same kind of marked attribution into its
  chain.reset marker.

When the hub starts deriving the value it overwrites the field and
the prefix disappears by itself — no Studio release needed.

Guards: reviewer_identity_test (the value) and
approvals_reviewer_identity_test (every surface that writes or
renders it, against the hermetic fake hub). Visual proof for both
themes via the dialog-shot harness.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-08-03 23:50:34 +02:00
parent 415f8a7ddb
commit ebc668d28d
15 changed files with 1129 additions and 41 deletions

View file

@ -674,6 +674,7 @@
"auditClearDialogTitle": "Audit-Log löschen?",
"auditClearDialogBody": "Löscht jedes Audit-Event auf dem aktiven Kanal und seedet einen neuen chain.reset-Marker mit Prüfer und Begründung. Auf beta / production verweigert. Nicht umkehrbar.",
"auditClearReviewerLabel": "Prüfer",
"auditClearReviewerHelper": "Wird als ungeprüfte Angabe festgehalten (Präfix „unverified:“) — der Hub übernimmt den Namen unverändert.",
"auditClearReasonLabel": "Begründung (im chain.reset-Marker festgehalten)",
"auditClearReasonHelper": "Erforderlich — Auditoren werden das lesen.",
"auditClearLogButton": "Log löschen",
@ -1102,6 +1103,13 @@
"approvalsRequestFallback": "Freigabe für diesen Schritt erforderlich",
"approvalsFlowStepMeta": "Flow: {flow} · Schritt: {step}",
"approvalsIntroHelp": "Hier warten pausierte Vorgänge (Flows) auf Ihre Entscheidung. Jede Karte zeigt, welcher Vorgang an welchem Schritt hält und welche Daten er Ihnen vorlegt — Freigeben setzt ihn fort, Ablehnen stoppt ihn mit Ihrer Begründung.",
"approvalsReviewerLabel": "Sie entscheiden als",
"approvalsReviewerUnverifiedPill": "nicht überprüft",
"approvalsReviewerRecordedAs": "Wird gespeichert als: {value}",
"approvalsReviewerNoteAnonymous": "Dieser Hub nimmt Aufrufe ohne Zugangsdaten an — eine Entscheidung lässt sich damit niemandem nachweisen. Studio kennzeichnet den Namen deshalb im Eintrag selbst als ungeprüfte Angabe.",
"approvalsReviewerNoteAuthenticated": "Der Hub prüft Ihren Zugang, übernimmt den Namen aber unverändert von Studio. Bis der Hub die Identität aus dem geprüften Zugang selbst ableitet, kennzeichnet Studio den Namen im Eintrag als ungeprüfte Angabe.",
"approvalsReviewerNoteUnknown": "Studio konnte die Zugangs-Einstellungen dieses Hubs nicht lesen (dafür braucht es Admin-Rechte). Der Name wird unverändert übernommen und deshalb im Eintrag als ungeprüfte Angabe gekennzeichnet.",
"approvalsReviewerUnverifiedTooltip": "Ungeprüfte Angabe: Dieser Name stammt vom entscheidenden Studio, nicht vom Hub.",
"approvalsRejectDialogHelp": "Die Ablehnung stoppt den Vorgang an diesem Schritt und wird mit Ihrer Begründung im Prüfprotokoll festgehalten.",
"approvalsRejectReasonHelper": "Pflichtfeld — wird im Prüfprotokoll (Audit-Log) festgehalten.",
"approvalsBatchNoDataTitle": "Ohne Prüfdaten freigeben?",