feat(store,doctor): surface the hub's trust + exposure data (0.77.0)
Some checks failed
Security / Security check (push) Failing after 1s
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:
parent
87afa4dc05
commit
2f076ccf29
15 changed files with 732 additions and 13 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -6,6 +6,28 @@ lockstep.
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Added — real trust + exposure data (0.77.0)
|
||||
|
||||
- **Per-entry install verification.** The hub now reports (since
|
||||
0.23) how an install of each store entry would be verified under
|
||||
the CURRENT policy — computed with the same resolvers the install
|
||||
gate enforces, so display and enforcement cannot disagree. The
|
||||
trust-gate dialog replaces its generic "no per-entry status yet
|
||||
(alpha)" note with the classified statement: signature checked
|
||||
via pinned store key / trusted publishers (green), installs
|
||||
without signature checking (amber, policy off), install would be
|
||||
refused (red), or bridge entry without a bundle signature. Store
|
||||
cards stay quiet on the good path and wear an "unsigned"/
|
||||
"blocked" warning pill only when installs are unverified —
|
||||
same philosophy as the provenance pill. Against a pre-0.23 hub
|
||||
everything keeps the old honest wording.
|
||||
- **Service exposure on the doctor page.** Every declared host
|
||||
service shows its network reach as classified by the hub from
|
||||
the endpoint host (local only / private network / publicly
|
||||
reachable / reach unknown — no DNS lookups). Publicly reachable
|
||||
endpoints get the warning tone with a "make sure it is
|
||||
protected" hint.
|
||||
|
||||
### Added — explain it where it happens (0.76.0)
|
||||
|
||||
- **In-place help pattern.** New `ChainInlineHelp` (a one-sentence
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:chain_studio/data/hub.dart' show StoreItem;
|
||||
import 'package:chain_studio/l10n/app_localizations.dart';
|
||||
import 'package:chain_studio/pages/federation.dart';
|
||||
import 'package:chain_studio/widgets/chain_install_confirm.dart';
|
||||
|
||||
import '../test/support/fake_hub.dart';
|
||||
|
||||
|
|
@ -41,6 +43,31 @@ Future<void> _shot(WidgetTester tester, String name) async {
|
|||
print('dialog-shot: ${file.path}');
|
||||
}
|
||||
|
||||
StoreItem _storeItem(String verification) => StoreItem(
|
||||
name: 'text.extract',
|
||||
taglineEn: 'Extract clean text',
|
||||
taglineDe: 'Reinen Text extrahieren',
|
||||
descriptionEn: '',
|
||||
descriptionDe: '',
|
||||
category: 'text',
|
||||
tags: const [],
|
||||
requiresCapabilities: const [],
|
||||
requiresServices: const [],
|
||||
license: 'Apache-2.0',
|
||||
repository: '',
|
||||
bestVersion: '0.1.0',
|
||||
status: 'alpha',
|
||||
installed: false,
|
||||
featured: false,
|
||||
iconUrl: '',
|
||||
screenshotUrls: const [],
|
||||
docsUrl: '',
|
||||
kind: 'native',
|
||||
provider: '',
|
||||
source: 'bundled',
|
||||
installVerification: verification,
|
||||
);
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
|
|
@ -76,4 +103,38 @@ void main() {
|
|||
findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
// Trust gate with the hub-reported verification statement — the
|
||||
// pinned-key (good) and policy-off (warning) variants.
|
||||
for (final (name, verification) in [
|
||||
('trust-gate-pinned', 'pinned-key'),
|
||||
('trust-gate-unverified', 'unverified'),
|
||||
]) {
|
||||
testWidgets('$name — dark', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
installFakeHub();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
themeMode: ThemeMode.dark,
|
||||
theme: ThemeData.light(useMaterial3: true),
|
||||
darkTheme: ThemeData.dark(useMaterial3: true),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: const Locale('de'),
|
||||
builder: (context, child) =>
|
||||
RepaintBoundary(key: _shotKey, child: child),
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: ChainInstallConfirmDialog(
|
||||
item: _storeItem(verification),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await _shot(tester, '$name-dark');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/// Studio's own build version. Bump on every UI release so the
|
||||
/// running app self-identifies.
|
||||
const String kStudioVersion = '0.76.0';
|
||||
const String kStudioVersion = '0.77.0';
|
||||
|
||||
const String kProductName = 'Ch∆In Studio';
|
||||
const String kVendorName = 'Flemming.AI (F∆I)';
|
||||
|
|
|
|||
|
|
@ -937,6 +937,7 @@ class HubService {
|
|||
source: e.source,
|
||||
canonicalCategory: e.canonicalCategory,
|
||||
canonicalCategoryLabel: e.canonicalCategoryLabel,
|
||||
installVerification: e.installVerification,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
|
@ -1289,7 +1290,12 @@ class HubService {
|
|||
services: services
|
||||
.map(
|
||||
(s) =>
|
||||
ServiceEntry(name: s.name, endpoint: s.endpoint, tags: s.tags),
|
||||
ServiceEntry(
|
||||
name: s.name,
|
||||
endpoint: s.endpoint,
|
||||
tags: s.tags,
|
||||
exposure: s.exposure,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
update: UpdateStatus(
|
||||
|
|
@ -1386,10 +1392,16 @@ class ServiceEntry {
|
|||
final String endpoint;
|
||||
final List<String> tags;
|
||||
|
||||
/// Network reach of [endpoint] as classified by the hub
|
||||
/// (host part only, no DNS): "loopback" / "private" / "public" /
|
||||
/// "unknown". Empty from pre-0.23 hubs.
|
||||
final String exposure;
|
||||
|
||||
const ServiceEntry({
|
||||
required this.name,
|
||||
required this.endpoint,
|
||||
required this.tags,
|
||||
this.exposure = '',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2233,6 +2245,15 @@ class StoreItem {
|
|||
/// Human-readable label for [canonicalCategory] (e.g. "Data & Formats").
|
||||
final String canonicalCategoryLabel;
|
||||
|
||||
/// How an install of this entry would be verified under the
|
||||
/// hub's CURRENT policy — computed hub-side with the same
|
||||
/// resolvers the install gate enforces, so this can never
|
||||
/// disagree with what actually happens:
|
||||
/// "pinned-key" / "trusted-publishers" / "unverified" /
|
||||
/// "blocked" / "federated". Empty from pre-0.23 hubs — the UI
|
||||
/// keeps its generic note then.
|
||||
final String installVerification;
|
||||
|
||||
/// True iff this entry is a Studio plugin/theme rather than a flow
|
||||
/// module — drives the "Modules | Studio & Themes" store segment.
|
||||
bool get isStudioPlugin => canonicalCategory == 'studio-themes';
|
||||
|
|
@ -2264,5 +2285,6 @@ class StoreItem {
|
|||
this.source = '',
|
||||
this.canonicalCategory = '',
|
||||
this.canonicalCategoryLabel = '',
|
||||
this.installVerification = '',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
65
lib/data/install_verification.dart
Normal file
65
lib/data/install_verification.dart
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// Presentation of `StoreItem.installVerification` — the hub's
|
||||
// per-entry statement of how an install would be verified under
|
||||
// the CURRENT policy (computed hub-side with the same resolvers
|
||||
// the install gate enforces). One mapping, used by the trust-gate
|
||||
// dialog, the module detail sheet and the store card, so every
|
||||
// surface tells the same story.
|
||||
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
enum VerificationTone { good, neutral, warning, danger }
|
||||
|
||||
class InstallVerificationInfo {
|
||||
final String label;
|
||||
final String body;
|
||||
final VerificationTone tone;
|
||||
|
||||
const InstallVerificationInfo({
|
||||
required this.label,
|
||||
required this.body,
|
||||
required this.tone,
|
||||
});
|
||||
}
|
||||
|
||||
/// Maps the wire value to its localized presentation. Returns null
|
||||
/// for an empty/unknown value (pre-0.23 hub) — callers keep their
|
||||
/// generic wording then instead of guessing.
|
||||
InstallVerificationInfo? describeInstallVerification(
|
||||
String wire,
|
||||
AppLocalizations l,
|
||||
) {
|
||||
switch (wire) {
|
||||
case 'pinned-key':
|
||||
return InstallVerificationInfo(
|
||||
label: l.verifPinnedKey,
|
||||
body: l.verifPinnedKeyBody,
|
||||
tone: VerificationTone.good,
|
||||
);
|
||||
case 'trusted-publishers':
|
||||
return InstallVerificationInfo(
|
||||
label: l.verifTrustedPublishers,
|
||||
body: l.verifTrustedPublishersBody,
|
||||
tone: VerificationTone.good,
|
||||
);
|
||||
case 'unverified':
|
||||
return InstallVerificationInfo(
|
||||
label: l.verifUnverified,
|
||||
body: l.verifUnverifiedBody,
|
||||
tone: VerificationTone.warning,
|
||||
);
|
||||
case 'blocked':
|
||||
return InstallVerificationInfo(
|
||||
label: l.verifBlocked,
|
||||
body: l.verifBlockedBody,
|
||||
tone: VerificationTone.danger,
|
||||
);
|
||||
case 'federated':
|
||||
return InstallVerificationInfo(
|
||||
label: l.verifFederated,
|
||||
body: l.verifFederatedBody,
|
||||
tone: VerificationTone.neutral,
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1189,6 +1189,12 @@
|
|||
}
|
||||
},
|
||||
"doctorSummaryDeclared": "deklariert",
|
||||
"svcExposureLoopback": "nur lokal",
|
||||
"svcExposurePrivate": "privates Netz",
|
||||
"svcExposurePublic": "öffentlich erreichbar",
|
||||
"svcExposureUnknown": "Reichweite unbekannt",
|
||||
"svcExposurePublicHint": "Dieser Dienst-Endpunkt liegt auf einer öffentlichen Adresse — prüfen Sie, ob er abgesichert ist (TLS, Auth, Firewall).",
|
||||
"svcExposureUnknownHint": "Hostname statt IP-Adresse — der Hub löst zur Einstufung bewusst keine Namen auf.",
|
||||
"doctorLinkStore": "Store öffnen",
|
||||
"doctorLinkApprovals": "Freigaben öffnen",
|
||||
"doctorLinkAudit": "Protokoll öffnen",
|
||||
|
|
@ -1622,6 +1628,19 @@
|
|||
"installConfirmNeedsCapabilities": "Benötigte Fähigkeiten",
|
||||
"installConfirmTrustTitle": "Vertrauen & Sicherheit",
|
||||
"installConfirmTrustBody": "Das Modul läuft in einer Sandbox: Es darf nur auf die Netzwerk-Endpunkte, Dateien und Umgebungsvariablen zugreifen, die es selbst deklariert — der Hub setzt diese Liste durch. Die vollständige Berechtigungsliste sehen Sie nach der Installation in den Modul-Details.",
|
||||
"storePolicyUnverifiedNotice": "Die Signaturpflicht ist in der Hub-Richtlinie ausgeschaltet — Installationen werden nicht kryptografisch geprüft. Der Installations-Dialog zeigt den Status je Modul; für geprüfte Installationen security.require_signatures aktivieren.",
|
||||
"verifPillUnverified": "unsigniert",
|
||||
"verifPillBlocked": "blockiert",
|
||||
"verifPinnedKey": "Signatur wird geprüft — hinterlegter Store-Schlüssel",
|
||||
"verifPinnedKeyBody": "Der Hub verifiziert das Paket beim Installieren gegen den fest hinterlegten Schlüssel dieser Quelle. Ein manipuliertes Paket wird abgelehnt.",
|
||||
"verifTrustedPublishers": "Signatur wird geprüft — vertrauenswürdige Herausgeber",
|
||||
"verifTrustedPublishersBody": "Der Hub verifiziert die Signatur beim Installieren gegen die Liste vertrauenswürdiger Herausgeber. Ein Paket ohne gültige Signatur wird abgelehnt.",
|
||||
"verifUnverified": "Wird ohne Signaturprüfung installiert",
|
||||
"verifUnverifiedBody": "Die Signaturpflicht ist in der Hub-Richtlinie ausgeschaltet — dieses Paket wird beim Installieren nicht kryptografisch geprüft. Für geprüfte Installationen security.require_signatures aktivieren.",
|
||||
"verifBlocked": "Installation würde abgelehnt",
|
||||
"verifBlockedBody": "Die Richtlinie verlangt Signaturen, aber für diese Quelle ist kein Schlüsselmaterial hinterlegt — der Hub würde die Installation verweigern.",
|
||||
"verifFederated": "Über Integration angebunden — keine Paket-Signatur",
|
||||
"verifFederatedBody": "Dieser Eintrag ist kein installierbares Paket, sondern läuft über eine konfigurierte Integration (z. B. MCP oder n8n). Eine Paket-Signatur gibt es hier nicht; es gilt das Vertrauen in die Integration.",
|
||||
"installConfirmSignatureNote": "Signaturen prüft der Hub beim Installieren, sofern die Signaturpflicht im Sicherheitsprofil aktiv ist. Einen Signaturstatus vorab zeigt der Store-Index derzeit noch nicht an (Alpha).",
|
||||
"settingsSidebarPinnedTitle": "Navigation immer ausgeklappt",
|
||||
"settingsSidebarPinnedBody": "Zeigt die Beschriftungen der Seitenleiste dauerhaft an, statt sie nur beim Überfahren mit der Maus einzublenden.",
|
||||
|
|
|
|||
|
|
@ -1213,6 +1213,12 @@
|
|||
}
|
||||
},
|
||||
"doctorSummaryDeclared": "declared",
|
||||
"svcExposureLoopback": "local only",
|
||||
"svcExposurePrivate": "private network",
|
||||
"svcExposurePublic": "publicly reachable",
|
||||
"svcExposureUnknown": "reach unknown",
|
||||
"svcExposurePublicHint": "This service endpoint sits on a public address — make sure it is protected (TLS, auth, firewall).",
|
||||
"svcExposureUnknownHint": "Hostname instead of an IP address — the hub deliberately does not resolve names to classify.",
|
||||
"doctorLinkStore": "Open the store",
|
||||
"doctorLinkApprovals": "Open approvals",
|
||||
"doctorLinkAudit": "Open the audit log",
|
||||
|
|
@ -1646,6 +1652,19 @@
|
|||
"installConfirmNeedsCapabilities": "Required capabilities",
|
||||
"installConfirmTrustTitle": "Trust & security",
|
||||
"installConfirmTrustBody": "The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.",
|
||||
"storePolicyUnverifiedNotice": "Signature enforcement is switched off in the hub policy — installs are not cryptographically verified. The install dialog shows the per-module status; enable security.require_signatures for verified installs.",
|
||||
"verifPillUnverified": "unsigned",
|
||||
"verifPillBlocked": "blocked",
|
||||
"verifPinnedKey": "Signature checked — pinned store key",
|
||||
"verifPinnedKeyBody": "The hub verifies the bundle at install time against this source's pinned key. A tampered bundle is refused.",
|
||||
"verifTrustedPublishers": "Signature checked — trusted publishers",
|
||||
"verifTrustedPublishersBody": "The hub verifies the signature at install time against the trusted-publisher list. A bundle without a valid signature is refused.",
|
||||
"verifUnverified": "Installs without signature checking",
|
||||
"verifUnverifiedBody": "Signature enforcement is switched off in the hub policy — this bundle is not cryptographically verified at install time. Enable security.require_signatures for verified installs.",
|
||||
"verifBlocked": "An install would be refused",
|
||||
"verifBlockedBody": "The policy requires signatures, but no key material applies to this source — the hub would refuse the install.",
|
||||
"verifFederated": "Connected via an integration — no bundle signature",
|
||||
"verifFederatedBody": "This entry is not an installable bundle; it runs through a configured integration (e.g. MCP or n8n). There is no bundle signature — trust follows the integration.",
|
||||
"installConfirmSignatureNote": "The hub verifies signatures at install time whenever the security profile requires them. The store index does not yet show a per-entry signature status up front (alpha).",
|
||||
"settingsSidebarPinnedTitle": "Keep the navigation expanded",
|
||||
"settingsSidebarPinnedBody": "Shows the sidebar labels permanently instead of only while hovering with the mouse.",
|
||||
|
|
|
|||
|
|
@ -3692,6 +3692,42 @@ abstract class AppLocalizations {
|
|||
/// **'declared'**
|
||||
String get doctorSummaryDeclared;
|
||||
|
||||
/// No description provided for @svcExposureLoopback.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'local only'**
|
||||
String get svcExposureLoopback;
|
||||
|
||||
/// No description provided for @svcExposurePrivate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'private network'**
|
||||
String get svcExposurePrivate;
|
||||
|
||||
/// No description provided for @svcExposurePublic.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'publicly reachable'**
|
||||
String get svcExposurePublic;
|
||||
|
||||
/// No description provided for @svcExposureUnknown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'reach unknown'**
|
||||
String get svcExposureUnknown;
|
||||
|
||||
/// No description provided for @svcExposurePublicHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This service endpoint sits on a public address — make sure it is protected (TLS, auth, firewall).'**
|
||||
String get svcExposurePublicHint;
|
||||
|
||||
/// No description provided for @svcExposureUnknownHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hostname instead of an IP address — the hub deliberately does not resolve names to classify.'**
|
||||
String get svcExposureUnknownHint;
|
||||
|
||||
/// No description provided for @doctorLinkStore.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -4898,6 +4934,84 @@ abstract class AppLocalizations {
|
|||
/// **'The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.'**
|
||||
String get installConfirmTrustBody;
|
||||
|
||||
/// No description provided for @storePolicyUnverifiedNotice.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Signature enforcement is switched off in the hub policy — installs are not cryptographically verified. The install dialog shows the per-module status; enable security.require_signatures for verified installs.'**
|
||||
String get storePolicyUnverifiedNotice;
|
||||
|
||||
/// No description provided for @verifPillUnverified.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'unsigned'**
|
||||
String get verifPillUnverified;
|
||||
|
||||
/// No description provided for @verifPillBlocked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'blocked'**
|
||||
String get verifPillBlocked;
|
||||
|
||||
/// No description provided for @verifPinnedKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Signature checked — pinned store key'**
|
||||
String get verifPinnedKey;
|
||||
|
||||
/// No description provided for @verifPinnedKeyBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The hub verifies the bundle at install time against this source\'s pinned key. A tampered bundle is refused.'**
|
||||
String get verifPinnedKeyBody;
|
||||
|
||||
/// No description provided for @verifTrustedPublishers.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Signature checked — trusted publishers'**
|
||||
String get verifTrustedPublishers;
|
||||
|
||||
/// No description provided for @verifTrustedPublishersBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The hub verifies the signature at install time against the trusted-publisher list. A bundle without a valid signature is refused.'**
|
||||
String get verifTrustedPublishersBody;
|
||||
|
||||
/// No description provided for @verifUnverified.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Installs without signature checking'**
|
||||
String get verifUnverified;
|
||||
|
||||
/// No description provided for @verifUnverifiedBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Signature enforcement is switched off in the hub policy — this bundle is not cryptographically verified at install time. Enable security.require_signatures for verified installs.'**
|
||||
String get verifUnverifiedBody;
|
||||
|
||||
/// No description provided for @verifBlocked.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'An install would be refused'**
|
||||
String get verifBlocked;
|
||||
|
||||
/// No description provided for @verifBlockedBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The policy requires signatures, but no key material applies to this source — the hub would refuse the install.'**
|
||||
String get verifBlockedBody;
|
||||
|
||||
/// No description provided for @verifFederated.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connected via an integration — no bundle signature'**
|
||||
String get verifFederated;
|
||||
|
||||
/// No description provided for @verifFederatedBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This entry is not an installable bundle; it runs through a configured integration (e.g. MCP or n8n). There is no bundle signature — trust follows the integration.'**
|
||||
String get verifFederatedBody;
|
||||
|
||||
/// No description provided for @installConfirmSignatureNote.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -2149,6 +2149,26 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get doctorSummaryDeclared => 'deklariert';
|
||||
|
||||
@override
|
||||
String get svcExposureLoopback => 'nur lokal';
|
||||
|
||||
@override
|
||||
String get svcExposurePrivate => 'privates Netz';
|
||||
|
||||
@override
|
||||
String get svcExposurePublic => 'öffentlich erreichbar';
|
||||
|
||||
@override
|
||||
String get svcExposureUnknown => 'Reichweite unbekannt';
|
||||
|
||||
@override
|
||||
String get svcExposurePublicHint =>
|
||||
'Dieser Dienst-Endpunkt liegt auf einer öffentlichen Adresse — prüfen Sie, ob er abgesichert ist (TLS, Auth, Firewall).';
|
||||
|
||||
@override
|
||||
String get svcExposureUnknownHint =>
|
||||
'Hostname statt IP-Adresse — der Hub löst zur Einstufung bewusst keine Namen auf.';
|
||||
|
||||
@override
|
||||
String get doctorLinkStore => 'Store öffnen';
|
||||
|
||||
|
|
@ -2886,6 +2906,54 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get installConfirmTrustBody =>
|
||||
'Das Modul läuft in einer Sandbox: Es darf nur auf die Netzwerk-Endpunkte, Dateien und Umgebungsvariablen zugreifen, die es selbst deklariert — der Hub setzt diese Liste durch. Die vollständige Berechtigungsliste sehen Sie nach der Installation in den Modul-Details.';
|
||||
|
||||
@override
|
||||
String get storePolicyUnverifiedNotice =>
|
||||
'Die Signaturpflicht ist in der Hub-Richtlinie ausgeschaltet — Installationen werden nicht kryptografisch geprüft. Der Installations-Dialog zeigt den Status je Modul; für geprüfte Installationen security.require_signatures aktivieren.';
|
||||
|
||||
@override
|
||||
String get verifPillUnverified => 'unsigniert';
|
||||
|
||||
@override
|
||||
String get verifPillBlocked => 'blockiert';
|
||||
|
||||
@override
|
||||
String get verifPinnedKey =>
|
||||
'Signatur wird geprüft — hinterlegter Store-Schlüssel';
|
||||
|
||||
@override
|
||||
String get verifPinnedKeyBody =>
|
||||
'Der Hub verifiziert das Paket beim Installieren gegen den fest hinterlegten Schlüssel dieser Quelle. Ein manipuliertes Paket wird abgelehnt.';
|
||||
|
||||
@override
|
||||
String get verifTrustedPublishers =>
|
||||
'Signatur wird geprüft — vertrauenswürdige Herausgeber';
|
||||
|
||||
@override
|
||||
String get verifTrustedPublishersBody =>
|
||||
'Der Hub verifiziert die Signatur beim Installieren gegen die Liste vertrauenswürdiger Herausgeber. Ein Paket ohne gültige Signatur wird abgelehnt.';
|
||||
|
||||
@override
|
||||
String get verifUnverified => 'Wird ohne Signaturprüfung installiert';
|
||||
|
||||
@override
|
||||
String get verifUnverifiedBody =>
|
||||
'Die Signaturpflicht ist in der Hub-Richtlinie ausgeschaltet — dieses Paket wird beim Installieren nicht kryptografisch geprüft. Für geprüfte Installationen security.require_signatures aktivieren.';
|
||||
|
||||
@override
|
||||
String get verifBlocked => 'Installation würde abgelehnt';
|
||||
|
||||
@override
|
||||
String get verifBlockedBody =>
|
||||
'Die Richtlinie verlangt Signaturen, aber für diese Quelle ist kein Schlüsselmaterial hinterlegt — der Hub würde die Installation verweigern.';
|
||||
|
||||
@override
|
||||
String get verifFederated =>
|
||||
'Über Integration angebunden — keine Paket-Signatur';
|
||||
|
||||
@override
|
||||
String get verifFederatedBody =>
|
||||
'Dieser Eintrag ist kein installierbares Paket, sondern läuft über eine konfigurierte Integration (z. B. MCP oder n8n). Eine Paket-Signatur gibt es hier nicht; es gilt das Vertrauen in die Integration.';
|
||||
|
||||
@override
|
||||
String get installConfirmSignatureNote =>
|
||||
'Signaturen prüft der Hub beim Installieren, sofern die Signaturpflicht im Sicherheitsprofil aktiv ist. Einen Signaturstatus vorab zeigt der Store-Index derzeit noch nicht an (Alpha).';
|
||||
|
|
|
|||
|
|
@ -2153,6 +2153,26 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get doctorSummaryDeclared => 'declared';
|
||||
|
||||
@override
|
||||
String get svcExposureLoopback => 'local only';
|
||||
|
||||
@override
|
||||
String get svcExposurePrivate => 'private network';
|
||||
|
||||
@override
|
||||
String get svcExposurePublic => 'publicly reachable';
|
||||
|
||||
@override
|
||||
String get svcExposureUnknown => 'reach unknown';
|
||||
|
||||
@override
|
||||
String get svcExposurePublicHint =>
|
||||
'This service endpoint sits on a public address — make sure it is protected (TLS, auth, firewall).';
|
||||
|
||||
@override
|
||||
String get svcExposureUnknownHint =>
|
||||
'Hostname instead of an IP address — the hub deliberately does not resolve names to classify.';
|
||||
|
||||
@override
|
||||
String get doctorLinkStore => 'Open the store';
|
||||
|
||||
|
|
@ -2882,6 +2902,52 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get installConfirmTrustBody =>
|
||||
'The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.';
|
||||
|
||||
@override
|
||||
String get storePolicyUnverifiedNotice =>
|
||||
'Signature enforcement is switched off in the hub policy — installs are not cryptographically verified. The install dialog shows the per-module status; enable security.require_signatures for verified installs.';
|
||||
|
||||
@override
|
||||
String get verifPillUnverified => 'unsigned';
|
||||
|
||||
@override
|
||||
String get verifPillBlocked => 'blocked';
|
||||
|
||||
@override
|
||||
String get verifPinnedKey => 'Signature checked — pinned store key';
|
||||
|
||||
@override
|
||||
String get verifPinnedKeyBody =>
|
||||
'The hub verifies the bundle at install time against this source\'s pinned key. A tampered bundle is refused.';
|
||||
|
||||
@override
|
||||
String get verifTrustedPublishers => 'Signature checked — trusted publishers';
|
||||
|
||||
@override
|
||||
String get verifTrustedPublishersBody =>
|
||||
'The hub verifies the signature at install time against the trusted-publisher list. A bundle without a valid signature is refused.';
|
||||
|
||||
@override
|
||||
String get verifUnverified => 'Installs without signature checking';
|
||||
|
||||
@override
|
||||
String get verifUnverifiedBody =>
|
||||
'Signature enforcement is switched off in the hub policy — this bundle is not cryptographically verified at install time. Enable security.require_signatures for verified installs.';
|
||||
|
||||
@override
|
||||
String get verifBlocked => 'An install would be refused';
|
||||
|
||||
@override
|
||||
String get verifBlockedBody =>
|
||||
'The policy requires signatures, but no key material applies to this source — the hub would refuse the install.';
|
||||
|
||||
@override
|
||||
String get verifFederated =>
|
||||
'Connected via an integration — no bundle signature';
|
||||
|
||||
@override
|
||||
String get verifFederatedBody =>
|
||||
'This entry is not an installable bundle; it runs through a configured integration (e.g. MCP or n8n). There is no bundle signature — trust follows the integration.';
|
||||
|
||||
@override
|
||||
String get installConfirmSignatureNote =>
|
||||
'The hub verifies signatures at install time whenever the security profile requires them. The store index does not yet show a per-entry signature status up front (alpha).';
|
||||
|
|
|
|||
|
|
@ -1159,6 +1159,9 @@ class _ServicesPanel extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_ServiceExposurePill(
|
||||
exposure: snapshot.services[i].exposure,
|
||||
),
|
||||
for (final tag in snapshot.services[i].tags) ...[
|
||||
ChainPill(label: tag, tone: ChainPillTone.neutral),
|
||||
const SizedBox(width: ChainSpace.xs),
|
||||
|
|
@ -1173,6 +1176,43 @@ class _ServicesPanel extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Network-reach pill per declared service, from the hub's
|
||||
/// endpoint classification (`DeclaredService.exposure`). Public
|
||||
/// endpoints get the warning tone — a host service reachable from
|
||||
/// outside is the one thing an operator should notice here.
|
||||
/// Empty (pre-0.23 hub) renders nothing.
|
||||
class _ServiceExposurePill extends StatelessWidget {
|
||||
final String exposure;
|
||||
|
||||
const _ServiceExposurePill({required this.exposure});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final (label, tone, hint) = switch (exposure) {
|
||||
'loopback' => (l.svcExposureLoopback, ChainPillTone.neutral, ''),
|
||||
'private' => (l.svcExposurePrivate, ChainPillTone.neutral, ''),
|
||||
'public' => (
|
||||
l.svcExposurePublic,
|
||||
ChainPillTone.warning,
|
||||
l.svcExposurePublicHint,
|
||||
),
|
||||
'unknown' => (
|
||||
l.svcExposureUnknown,
|
||||
ChainPillTone.neutral,
|
||||
l.svcExposureUnknownHint,
|
||||
),
|
||||
_ => ('', ChainPillTone.neutral, ''),
|
||||
};
|
||||
if (label.isEmpty) return const SizedBox.shrink();
|
||||
final pill = ChainPill(label: label, tone: tone);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: ChainSpace.xs),
|
||||
child: hint.isEmpty ? pill : Tooltip(message: hint, child: pill),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UpdateBanner extends StatefulWidget {
|
||||
final UpdateStatus status;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,14 +5,16 @@
|
|||
// quick fix) now routes through this dialog, which shows what
|
||||
// the hub actually knows before anything is downloaded — origin
|
||||
// store, version, license, maturity, required services — plus
|
||||
// an honest note on when signature verification happens. No
|
||||
// fake "verified" badge: per-entry signature status is not in
|
||||
// the store index yet, and the dialog says so instead of
|
||||
// pretending.
|
||||
// the per-entry verification statement the hub computes with the
|
||||
// SAME resolvers the install gate enforces (pinned key / trusted
|
||||
// publishers / policy off / blocked / federated). Against a
|
||||
// pre-0.23 hub the field is empty and the dialog keeps its
|
||||
// generic when-verification-happens note instead of guessing.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data/hub.dart';
|
||||
import '../data/install_verification.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/tokens.dart';
|
||||
|
||||
|
|
@ -184,12 +186,65 @@ class ChainInstallConfirmDialog extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: ChainSpace.xs),
|
||||
Text(
|
||||
l.installConfirmSignatureNote,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
height: 1.4,
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final info = describeInstallVerification(
|
||||
item.installVerification,
|
||||
l,
|
||||
);
|
||||
if (info == null) {
|
||||
// Pre-0.23 hub: keep the generic note.
|
||||
return Text(
|
||||
l.installConfirmSignatureNote,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
height: 1.4,
|
||||
),
|
||||
);
|
||||
}
|
||||
final color = switch (info.tone) {
|
||||
VerificationTone.good => ChainColors.success,
|
||||
VerificationTone.warning => ChainColors.warning,
|
||||
VerificationTone.danger => theme.colorScheme.error,
|
||||
VerificationTone.neutral =>
|
||||
theme.colorScheme.onSurfaceVariant,
|
||||
};
|
||||
final icon = switch (info.tone) {
|
||||
VerificationTone.good => Icons.verified_outlined,
|
||||
VerificationTone.warning =>
|
||||
Icons.warning_amber_outlined,
|
||||
VerificationTone.danger => Icons.gpp_bad_outlined,
|
||||
VerificationTone.neutral => Icons.link_outlined,
|
||||
};
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 14, color: color),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
info.label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
info.body,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
name: chain_studio
|
||||
description: "Ch∆In Studio — desktop GUI for the Ch∆In hub"
|
||||
publish_to: 'none'
|
||||
version: 0.76.0
|
||||
version: 0.77.0
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.0-200.1.beta
|
||||
|
|
|
|||
116
test/install_verification_test.dart
Normal file
116
test/install_verification_test.dart
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// Presentation of the hub's per-entry install-verification
|
||||
// statement. The mapping is the single source for the trust-gate
|
||||
// dialog, the store card pill and the detail sheet — these tests
|
||||
// pin the wire contract and the honest-fallback rule (unknown or
|
||||
// empty => null => callers keep their generic wording).
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:chain_studio/data/hub.dart';
|
||||
import 'package:chain_studio/data/install_verification.dart';
|
||||
import 'package:chain_studio/l10n/app_localizations.dart';
|
||||
import 'package:chain_studio/widgets/chain_install_confirm.dart';
|
||||
|
||||
StoreItem _item({String verification = ''}) => StoreItem(
|
||||
name: 'text.extract',
|
||||
taglineEn: '',
|
||||
taglineDe: '',
|
||||
descriptionEn: '',
|
||||
descriptionDe: '',
|
||||
category: 'text',
|
||||
tags: const [],
|
||||
requiresCapabilities: const [],
|
||||
requiresServices: const [],
|
||||
license: 'Apache-2.0',
|
||||
repository: '',
|
||||
bestVersion: '0.1.0',
|
||||
status: 'alpha',
|
||||
installed: false,
|
||||
featured: false,
|
||||
iconUrl: '',
|
||||
screenshotUrls: const [],
|
||||
docsUrl: '',
|
||||
kind: 'native',
|
||||
provider: '',
|
||||
source: 'bundled',
|
||||
installVerification: verification,
|
||||
);
|
||||
|
||||
Widget _host(Widget child) => MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: const Locale('de'),
|
||||
home: Scaffold(body: child),
|
||||
);
|
||||
|
||||
Future<AppLocalizations> _l10n(WidgetTester tester) async {
|
||||
late AppLocalizations l;
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
Builder(
|
||||
builder: (context) {
|
||||
l = AppLocalizations.of(context)!;
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
return l;
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('wire values map to tones; unknown maps to null', (
|
||||
tester,
|
||||
) async {
|
||||
final l = await _l10n(tester);
|
||||
expect(
|
||||
describeInstallVerification('pinned-key', l)!.tone,
|
||||
VerificationTone.good,
|
||||
);
|
||||
expect(
|
||||
describeInstallVerification('trusted-publishers', l)!.tone,
|
||||
VerificationTone.good,
|
||||
);
|
||||
expect(
|
||||
describeInstallVerification('unverified', l)!.tone,
|
||||
VerificationTone.warning,
|
||||
);
|
||||
expect(
|
||||
describeInstallVerification('blocked', l)!.tone,
|
||||
VerificationTone.danger,
|
||||
);
|
||||
expect(
|
||||
describeInstallVerification('federated', l)!.tone,
|
||||
VerificationTone.neutral,
|
||||
);
|
||||
// Pre-0.23 hub / future value: no guessing.
|
||||
expect(describeInstallVerification('', l), isNull);
|
||||
expect(describeInstallVerification('something-new', l), isNull);
|
||||
});
|
||||
|
||||
testWidgets('trust gate states the classified verification', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_host(ChainInstallConfirmDialog(item: _item(verification: 'unverified'))),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('Wird ohne Signaturprüfung installiert'),
|
||||
findsOneWidget,
|
||||
);
|
||||
// The generic alpha note must be gone when the hub reports.
|
||||
expect(
|
||||
find.textContaining('zeigt der Store-Index derzeit noch nicht'),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('trust gate keeps the generic note for old hubs', (tester) async {
|
||||
await tester.pumpWidget(_host(ChainInstallConfirmDialog(item: _item())));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.textContaining('zeigt der Store-Index derzeit noch nicht'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue