feat(store,doctor): surface the hub's trust + exposure data (0.77.0)
Some checks failed
Security / Security check (push) Failing after 1s

The trust-gate dialog replaces its generic 'no per-entry status yet
(alpha)' note with the hub's classified verification statement —
pinned store key / trusted publishers (green), installs without
signature checking (amber), install would be refused (red), bridge
entry (neutral) — via one shared describeInstallVerification mapping.
Against a pre-0.23 hub the field is empty and the old honest wording
stays (pinned by test).

Information architecture: policy-off is a GLOBAL fact, so it appears
as ONE ChainInlineHelp notice above the store grid instead of a
warning pill on every card (card noise); only 'blocked' — a genuine
per-source anomaly — earns a card pill. Doctor's host services show
the hub-classified network reach per endpoint (local only / private
network / publicly reachable with a protect-it hint / reach unknown).

Verified end-to-end against the live dev hub (guide harness): the
wire field arrives, the store page shows exactly one policy notice
and quiet cards; trust-gate variants captured via the dialog
harness. Suite 123 green.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-21 13:14:13 +02:00
parent 87afa4dc05
commit 2f076ccf29
15 changed files with 732 additions and 13 deletions

View file

@ -14,6 +14,7 @@ import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import '../data/error_presentation.dart';
import '../data/hub.dart';
import '../data/install_verification.dart';
import '../data/system_actions.dart';
import '../data/today_story_loader.dart';
import '../l10n/app_localizations.dart';
@ -383,6 +384,23 @@ class _StorePageState extends State<StorePage> {
setState(() => _showStudio = v),
),
),
// ONE page-level notice when the hub says
// installs run unverified (policy off)
// a global fact, so it appears once here
// instead of as a pill on every card. Old
// hubs report nothing => nothing shows.
if (raw.any(
(i) => i.installVerification == 'unverified',
))
Padding(
padding: const EdgeInsets.only(
bottom: ChainSpace.md,
),
child: ChainInlineHelp(
icon: Icons.gpp_maybe_outlined,
text: l.storePolicyUnverifiedNotice,
),
),
if (_aiThinking ||
_aiAnswer != null ||
_aiError != null) ...[
@ -2136,6 +2154,7 @@ class _StoreCardState extends State<_StoreCard> {
const SizedBox(width: ChainSpace.xs),
],
_ProvenancePill(item: item),
_VerificationPill(item: item),
const Spacer(),
if (_hasUpdate)
FilledButton.icon(
@ -3809,6 +3828,39 @@ class _ProvenancePill extends StatelessWidget {
}
}
/// Signature-verification warning pill. Same philosophy as the
/// provenance pill: the GOOD path (signature checked at install)
/// stays quiet, and so does the policy-off case that one is a
/// GLOBAL property and lives as ONE notice above the grid, not as
/// a repeated badge on every card. Only "blocked" (this source
/// has no applicable key material although the policy demands
/// signatures) is a genuine per-source anomaly worth a badge.
class _VerificationPill extends StatelessWidget {
final StoreItem item;
const _VerificationPill({required this.item});
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
if (item.installVerification != 'blocked') {
return const SizedBox.shrink();
}
final info = describeInstallVerification(item.installVerification, l)!;
return Padding(
padding: const EdgeInsets.only(left: ChainSpace.xs),
child: Tooltip(
message: info.body,
child: ChainPill(
label: l.verifPillBlocked,
tone: ChainPillTone.danger,
icon: Icons.gpp_bad_outlined,
),
),
);
}
}
/// Store-side version picker. Same shape as the module-sheet
/// version picker but lives here so the Store page doesn't take
/// a dependency on the module-sheet's private widgets. The