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:
parent
415f8a7ddb
commit
ebc668d28d
15 changed files with 1129 additions and 41 deletions
|
|
@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
|||
import '../data/error_presentation.dart';
|
||||
import '../data/friendly_error.dart';
|
||||
import '../data/hub.dart';
|
||||
import '../data/reviewer_identity.dart';
|
||||
import '../data/workspace.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/theme.dart';
|
||||
|
|
@ -1258,7 +1259,9 @@ class _ExplanationPanel extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Outcome of the clear-audit confirmation dialog.
|
||||
/// Outcome of the clear-audit confirmation dialog. [reviewer] is
|
||||
/// already the wire value — the marked, unchecked claim that lands
|
||||
/// in the `chain.reset` marker (see `data/reviewer_identity.dart`).
|
||||
class _ClearOutcome {
|
||||
final String reviewer;
|
||||
final String reason;
|
||||
|
|
@ -1266,10 +1269,11 @@ class _ClearOutcome {
|
|||
}
|
||||
|
||||
/// Two-field confirmation dialog for "clear the audit log".
|
||||
/// Reviewer defaults to the OS user (closest stable identity
|
||||
/// without an auth backend); reason has no default so the
|
||||
/// operator has to type *something* — the chain.reset marker
|
||||
/// must carry context.
|
||||
/// Reviewer prefills with the local handle (the OS account is a
|
||||
/// label, not an identity — [HubService.clearEventLog] marks it as
|
||||
/// an unchecked claim on the wire, and the field's helper says so);
|
||||
/// reason has no default so the operator has to type *something* —
|
||||
/// the chain.reset marker must carry context.
|
||||
class _ClearAuditDialog extends StatefulWidget {
|
||||
const _ClearAuditDialog();
|
||||
|
||||
|
|
@ -1291,11 +1295,7 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final user =
|
||||
Platform.environment['USER'] ??
|
||||
Platform.environment['USERNAME'] ??
|
||||
'operator';
|
||||
_reviewer = TextEditingController(text: '$user@studio');
|
||||
_reviewer = TextEditingController(text: ReviewerIdentity.localHandle);
|
||||
_reason = TextEditingController();
|
||||
}
|
||||
|
||||
|
|
@ -1329,6 +1329,8 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
|||
controller: _reviewer,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.auditClearReviewerLabel,
|
||||
helperText: l.auditClearReviewerHelper,
|
||||
helperMaxLines: 3,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
|
|
@ -1365,7 +1367,7 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
|||
: () => Navigator.pop(
|
||||
context,
|
||||
_ClearOutcome(
|
||||
reviewer: _reviewer.text.trim(),
|
||||
reviewer: ReviewerIdentity.wire(_reviewer.text),
|
||||
reason: _reason.text.trim(),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue