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
|
|
@ -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?",
|
||||
|
|
|
|||
|
|
@ -692,6 +692,7 @@
|
|||
"auditClearDialogTitle": "Clear audit log?",
|
||||
"auditClearDialogBody": "Wipes every audit event on the active channel and seeds a fresh chain.reset marker carrying reviewer + reason. Refused on beta / production. Irreversible.",
|
||||
"auditClearReviewerLabel": "Reviewer",
|
||||
"auditClearReviewerHelper": "Recorded as an unchecked claim (prefix \"unverified:\") — the hub stores this name as sent.",
|
||||
"auditClearReasonLabel": "Reason (recorded in chain.reset marker)",
|
||||
"auditClearReasonHelper": "Required — auditors will read this.",
|
||||
"auditClearLogButton": "Clear log",
|
||||
|
|
@ -1120,6 +1121,13 @@
|
|||
"approvalsRequestFallback": "Approval required for this step",
|
||||
"approvalsFlowStepMeta": "Flow: {flow} · Step: {step}",
|
||||
"approvalsIntroHelp": "Paused processes (flows) waiting for your decision. Each card shows which process is holding at which step and what data it puts in front of you — Approve resumes it, Reject stops it with your reason.",
|
||||
"approvalsReviewerLabel": "You are deciding as",
|
||||
"approvalsReviewerUnverifiedPill": "not verified",
|
||||
"approvalsReviewerRecordedAs": "Recorded as: {value}",
|
||||
"approvalsReviewerNoteAnonymous": "This hub accepts calls without credentials, so a decision cannot be proven to be anyone's. Studio therefore marks the name inside the record itself as an unchecked claim.",
|
||||
"approvalsReviewerNoteAuthenticated": "The hub checks your access but records the name exactly as Studio sends it. Until the hub derives the identity from the checked access itself, Studio marks the name inside the record as an unchecked claim.",
|
||||
"approvalsReviewerNoteUnknown": "Studio could not read this hub's access settings (that needs admin rights). The name is stored exactly as sent and is therefore marked inside the record as an unchecked claim.",
|
||||
"approvalsReviewerUnverifiedTooltip": "Unchecked claim: this name comes from the deciding Studio, not from the hub.",
|
||||
"approvalsRejectDialogHelp": "Rejecting stops the process at this step and is recorded with your reason in the audit trail.",
|
||||
"approvalsRejectReasonHelper": "Required — recorded in the audit log.",
|
||||
"approvalsBatchNoDataTitle": "Approve without review data?",
|
||||
|
|
|
|||
|
|
@ -2384,6 +2384,12 @@ abstract class AppLocalizations {
|
|||
/// **'Reviewer'**
|
||||
String get auditClearReviewerLabel;
|
||||
|
||||
/// No description provided for @auditClearReviewerHelper.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recorded as an unchecked claim (prefix \"unverified:\") — the hub stores this name as sent.'**
|
||||
String get auditClearReviewerHelper;
|
||||
|
||||
/// No description provided for @auditClearReasonLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -3560,6 +3566,48 @@ abstract class AppLocalizations {
|
|||
/// **'Paused processes (flows) waiting for your decision. Each card shows which process is holding at which step and what data it puts in front of you — Approve resumes it, Reject stops it with your reason.'**
|
||||
String get approvalsIntroHelp;
|
||||
|
||||
/// No description provided for @approvalsReviewerLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You are deciding as'**
|
||||
String get approvalsReviewerLabel;
|
||||
|
||||
/// No description provided for @approvalsReviewerUnverifiedPill.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'not verified'**
|
||||
String get approvalsReviewerUnverifiedPill;
|
||||
|
||||
/// No description provided for @approvalsReviewerRecordedAs.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recorded as: {value}'**
|
||||
String approvalsReviewerRecordedAs(Object value);
|
||||
|
||||
/// No description provided for @approvalsReviewerNoteAnonymous.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This hub accepts calls without credentials, so a decision cannot be proven to be anyone\'s. Studio therefore marks the name inside the record itself as an unchecked claim.'**
|
||||
String get approvalsReviewerNoteAnonymous;
|
||||
|
||||
/// No description provided for @approvalsReviewerNoteAuthenticated.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The hub checks your access but records the name exactly as Studio sends it. Until the hub derives the identity from the checked access itself, Studio marks the name inside the record as an unchecked claim.'**
|
||||
String get approvalsReviewerNoteAuthenticated;
|
||||
|
||||
/// No description provided for @approvalsReviewerNoteUnknown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Studio could not read this hub\'s access settings (that needs admin rights). The name is stored exactly as sent and is therefore marked inside the record as an unchecked claim.'**
|
||||
String get approvalsReviewerNoteUnknown;
|
||||
|
||||
/// No description provided for @approvalsReviewerUnverifiedTooltip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unchecked claim: this name comes from the deciding Studio, not from the hub.'**
|
||||
String get approvalsReviewerUnverifiedTooltip;
|
||||
|
||||
/// No description provided for @approvalsRejectDialogHelp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1351,6 +1351,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get auditClearReviewerLabel => 'Prüfer';
|
||||
|
||||
@override
|
||||
String get auditClearReviewerHelper =>
|
||||
'Wird als ungeprüfte Angabe festgehalten (Präfix „unverified:“) — der Hub übernimmt den Namen unverändert.';
|
||||
|
||||
@override
|
||||
String get auditClearReasonLabel =>
|
||||
'Begründung (im chain.reset-Marker festgehalten)';
|
||||
|
|
@ -2062,6 +2066,33 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get 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.';
|
||||
|
||||
@override
|
||||
String get approvalsReviewerLabel => 'Sie entscheiden als';
|
||||
|
||||
@override
|
||||
String get approvalsReviewerUnverifiedPill => 'nicht überprüft';
|
||||
|
||||
@override
|
||||
String approvalsReviewerRecordedAs(Object value) {
|
||||
return 'Wird gespeichert als: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get 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.';
|
||||
|
||||
@override
|
||||
String get 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.';
|
||||
|
||||
@override
|
||||
String get 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.';
|
||||
|
||||
@override
|
||||
String get approvalsReviewerUnverifiedTooltip =>
|
||||
'Ungeprüfte Angabe: Dieser Name stammt vom entscheidenden Studio, nicht vom Hub.';
|
||||
|
||||
@override
|
||||
String get approvalsRejectDialogHelp =>
|
||||
'Die Ablehnung stoppt den Vorgang an diesem Schritt und wird mit Ihrer Begründung im Prüfprotokoll festgehalten.';
|
||||
|
|
|
|||
|
|
@ -1364,6 +1364,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get auditClearReviewerLabel => 'Reviewer';
|
||||
|
||||
@override
|
||||
String get auditClearReviewerHelper =>
|
||||
'Recorded as an unchecked claim (prefix \"unverified:\") — the hub stores this name as sent.';
|
||||
|
||||
@override
|
||||
String get auditClearReasonLabel => 'Reason (recorded in chain.reset marker)';
|
||||
|
||||
|
|
@ -2066,6 +2070,33 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get approvalsIntroHelp =>
|
||||
'Paused processes (flows) waiting for your decision. Each card shows which process is holding at which step and what data it puts in front of you — Approve resumes it, Reject stops it with your reason.';
|
||||
|
||||
@override
|
||||
String get approvalsReviewerLabel => 'You are deciding as';
|
||||
|
||||
@override
|
||||
String get approvalsReviewerUnverifiedPill => 'not verified';
|
||||
|
||||
@override
|
||||
String approvalsReviewerRecordedAs(Object value) {
|
||||
return 'Recorded as: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get approvalsReviewerNoteAnonymous =>
|
||||
'This hub accepts calls without credentials, so a decision cannot be proven to be anyone\'s. Studio therefore marks the name inside the record itself as an unchecked claim.';
|
||||
|
||||
@override
|
||||
String get approvalsReviewerNoteAuthenticated =>
|
||||
'The hub checks your access but records the name exactly as Studio sends it. Until the hub derives the identity from the checked access itself, Studio marks the name inside the record as an unchecked claim.';
|
||||
|
||||
@override
|
||||
String get approvalsReviewerNoteUnknown =>
|
||||
'Studio could not read this hub\'s access settings (that needs admin rights). The name is stored exactly as sent and is therefore marked inside the record as an unchecked claim.';
|
||||
|
||||
@override
|
||||
String get approvalsReviewerUnverifiedTooltip =>
|
||||
'Unchecked claim: this name comes from the deciding Studio, not from the hub.';
|
||||
|
||||
@override
|
||||
String get approvalsRejectDialogHelp =>
|
||||
'Rejecting stops the process at this step and is recorded with your reason in the audit trail.';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue