From 1eaa36b1a0080f932f34692dcf852c3cdaceb900 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Tue, 5 May 2026 22:05:40 +0200 Subject: [PATCH] feat: HubClient.verifyEventChain + listServices Mirrors the two new HubAdmin RPCs in fai_platform 0.10.44. Studio's Doctor page consumes these directly. Generated proto bindings refreshed to include VerifyEventChainResponse, ServiceList, DeclaredService. Bumps fai_dart_sdk 0.2.0 -> 0.3.0. Signed-off-by: flemming-it --- lib/src/generated/fai/v1/hub.pb.dart | 196 +++++++++++++++++++++++ lib/src/generated/fai/v1/hub.pbgrpc.dart | 46 ++++++ lib/src/generated/fai/v1/hub.pbjson.dart | 45 ++++++ lib/src/hub_client.dart | 16 ++ 4 files changed, 303 insertions(+) diff --git a/lib/src/generated/fai/v1/hub.pb.dart b/lib/src/generated/fai/v1/hub.pb.dart index 5765c2a..935b637 100644 --- a/lib/src/generated/fai/v1/hub.pb.dart +++ b/lib/src/generated/fai/v1/hub.pb.dart @@ -2052,6 +2052,202 @@ class DecideApprovalResponse extends $pb.GeneratedMessage { void clearStatus() => $_clearField(2); } +class VerifyEventChainResponse extends $pb.GeneratedMessage { + factory VerifyEventChainResponse({ + $fixnum.Int64? total, + $fixnum.Int64? verified, + $core.String? tamperedAt, + }) { + final $result = create(); + if (total != null) { + $result.total = total; + } + if (verified != null) { + $result.verified = verified; + } + if (tamperedAt != null) { + $result.tamperedAt = tamperedAt; + } + return $result; + } + VerifyEventChainResponse._() : super(); + factory VerifyEventChainResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory VerifyEventChainResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'VerifyEventChainResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'total') + ..aInt64(2, _omitFieldNames ? '' : 'verified') + ..aOS(3, _omitFieldNames ? '' : 'tamperedAt') + ..hasRequiredFields = false + ; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + VerifyEventChainResponse clone() => VerifyEventChainResponse()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + VerifyEventChainResponse copyWith(void Function(VerifyEventChainResponse) updates) => super.copyWith((message) => updates(message as VerifyEventChainResponse)) as VerifyEventChainResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static VerifyEventChainResponse create() => VerifyEventChainResponse._(); + VerifyEventChainResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static VerifyEventChainResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static VerifyEventChainResponse? _defaultInstance; + + /// Total events scanned. + @$pb.TagNumber(1) + $fixnum.Int64 get total => $_getI64(0); + @$pb.TagNumber(1) + set total($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasTotal() => $_has(0); + @$pb.TagNumber(1) + void clearTotal() => $_clearField(1); + + /// Number of events verified before any tamper detection. + /// Equal to total on a clean chain. + @$pb.TagNumber(2) + $fixnum.Int64 get verified => $_getI64(1); + @$pb.TagNumber(2) + set verified($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasVerified() => $_has(1); + @$pb.TagNumber(2) + void clearVerified() => $_clearField(2); + + /// Empty when no tampering was detected; otherwise the + /// event_id of the first inconsistent row. + @$pb.TagNumber(3) + $core.String get tamperedAt => $_getSZ(2); + @$pb.TagNumber(3) + set tamperedAt($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasTamperedAt() => $_has(2); + @$pb.TagNumber(3) + void clearTamperedAt() => $_clearField(3); +} + +class ServiceList extends $pb.GeneratedMessage { + factory ServiceList({ + $core.Iterable? services, + }) { + final $result = create(); + if (services != null) { + $result.services.addAll(services); + } + return $result; + } + ServiceList._() : super(); + factory ServiceList.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ServiceList.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ServiceList', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create) + ..pc(1, _omitFieldNames ? '' : 'services', $pb.PbFieldType.PM, subBuilder: DeclaredService.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ServiceList clone() => ServiceList()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ServiceList copyWith(void Function(ServiceList) updates) => super.copyWith((message) => updates(message as ServiceList)) as ServiceList; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ServiceList create() => ServiceList._(); + ServiceList createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ServiceList getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ServiceList? _defaultInstance; + + @$pb.TagNumber(1) + $pb.PbList get services => $_getList(0); +} + +class DeclaredService extends $pb.GeneratedMessage { + factory DeclaredService({ + $core.String? name, + $core.String? endpoint, + $core.String? healthPath, + $core.Iterable<$core.String>? tags, + }) { + final $result = create(); + if (name != null) { + $result.name = name; + } + if (endpoint != null) { + $result.endpoint = endpoint; + } + if (healthPath != null) { + $result.healthPath = healthPath; + } + if (tags != null) { + $result.tags.addAll(tags); + } + return $result; + } + DeclaredService._() : super(); + factory DeclaredService.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DeclaredService.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DeclaredService', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'name') + ..aOS(2, _omitFieldNames ? '' : 'endpoint') + ..aOS(3, _omitFieldNames ? '' : 'healthPath') + ..pPS(4, _omitFieldNames ? '' : 'tags') + ..hasRequiredFields = false + ; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeclaredService clone() => DeclaredService()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeclaredService copyWith(void Function(DeclaredService) updates) => super.copyWith((message) => updates(message as DeclaredService)) as DeclaredService; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeclaredService create() => DeclaredService._(); + DeclaredService createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DeclaredService getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DeclaredService? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get name => $_getSZ(0); + @$pb.TagNumber(1) + set name($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasName() => $_has(0); + @$pb.TagNumber(1) + void clearName() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get endpoint => $_getSZ(1); + @$pb.TagNumber(2) + set endpoint($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasEndpoint() => $_has(1); + @$pb.TagNumber(2) + void clearEndpoint() => $_clearField(2); + + /// Empty defaults to "/". + @$pb.TagNumber(3) + $core.String get healthPath => $_getSZ(2); + @$pb.TagNumber(3) + set healthPath($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasHealthPath() => $_has(2); + @$pb.TagNumber(3) + void clearHealthPath() => $_clearField(3); + + @$pb.TagNumber(4) + $pb.PbList<$core.String> get tags => $_getList(3); +} + class StoreEntry extends $pb.GeneratedMessage { factory StoreEntry({ $core.String? name, diff --git a/lib/src/generated/fai/v1/hub.pbgrpc.dart b/lib/src/generated/fai/v1/hub.pbgrpc.dart index 9727f7b..16765b9 100644 --- a/lib/src/generated/fai/v1/hub.pbgrpc.dart +++ b/lib/src/generated/fai/v1/hub.pbgrpc.dart @@ -171,6 +171,14 @@ class HubAdminClient extends $grpc.Client { '/fai.v1.HubAdmin/DecideApproval', ($0.DecideApprovalRequest value) => value.writeToBuffer(), ($core.List<$core.int> value) => $0.DecideApprovalResponse.fromBuffer(value)); + static final _$verifyEventChain = $grpc.ClientMethod<$1.Empty, $0.VerifyEventChainResponse>( + '/fai.v1.HubAdmin/VerifyEventChain', + ($1.Empty value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.VerifyEventChainResponse.fromBuffer(value)); + static final _$listServices = $grpc.ClientMethod<$1.Empty, $0.ServiceList>( + '/fai.v1.HubAdmin/ListServices', + ($1.Empty value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.ServiceList.fromBuffer(value)); HubAdminClient(super.channel, {super.options, super.interceptors}); @@ -236,6 +244,20 @@ class HubAdminClient extends $grpc.Client { $grpc.ResponseFuture<$0.DecideApprovalResponse> decideApproval($0.DecideApprovalRequest request, {$grpc.CallOptions? options}) { return $createUnaryCall(_$decideApproval, request, options: options); } + + /// Walk the event-log hash chain and return whether tampering + /// was detected. Surfaced by the CLI as `fai admin verify-events` + /// and by Studio's health page. + $grpc.ResponseFuture<$0.VerifyEventChainResponse> verifyEventChain($1.Empty request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$verifyEventChain, request, options: options); + } + + /// List host services declared in the operator config. Each + /// entry carries name + endpoint; reachability is operator-side + /// (on-demand `fai service status` or Studio probe). + $grpc.ResponseFuture<$0.ServiceList> listServices($1.Empty request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$listServices, request, options: options); + } } @$pb.GrpcServiceName('fai.v1.HubAdmin') @@ -327,6 +349,20 @@ abstract class HubAdminServiceBase extends $grpc.Service { false, ($core.List<$core.int> value) => $0.DecideApprovalRequest.fromBuffer(value), ($0.DecideApprovalResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$1.Empty, $0.VerifyEventChainResponse>( + 'VerifyEventChain', + verifyEventChain_Pre, + false, + false, + ($core.List<$core.int> value) => $1.Empty.fromBuffer(value), + ($0.VerifyEventChainResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$1.Empty, $0.ServiceList>( + 'ListServices', + listServices_Pre, + false, + false, + ($core.List<$core.int> value) => $1.Empty.fromBuffer(value), + ($0.ServiceList value) => value.writeToBuffer())); } $async.Future<$0.CapabilityList> listCapabilities_Pre($grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async { @@ -377,6 +413,14 @@ abstract class HubAdminServiceBase extends $grpc.Service { return decideApproval($call, await $request); } + $async.Future<$0.VerifyEventChainResponse> verifyEventChain_Pre($grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async { + return verifyEventChain($call, await $request); + } + + $async.Future<$0.ServiceList> listServices_Pre($grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async { + return listServices($call, await $request); + } + $async.Future<$0.CapabilityList> listCapabilities($grpc.ServiceCall call, $1.Empty request); $async.Future<$0.ModuleInfoResponse> moduleInfo($grpc.ServiceCall call, $0.ModuleInfoRequest request); $async.Future<$0.CheckPermissionResponse> checkPermission($grpc.ServiceCall call, $0.CheckPermissionRequest request); @@ -389,4 +433,6 @@ abstract class HubAdminServiceBase extends $grpc.Service { $async.Future<$0.InstallModuleResponse> installModule($grpc.ServiceCall call, $0.InstallModuleRequest request); $async.Future<$0.ApprovalList> listApprovals($grpc.ServiceCall call, $0.ListApprovalsRequest request); $async.Future<$0.DecideApprovalResponse> decideApproval($grpc.ServiceCall call, $0.DecideApprovalRequest request); + $async.Future<$0.VerifyEventChainResponse> verifyEventChain($grpc.ServiceCall call, $1.Empty request); + $async.Future<$0.ServiceList> listServices($grpc.ServiceCall call, $1.Empty request); } diff --git a/lib/src/generated/fai/v1/hub.pbjson.dart b/lib/src/generated/fai/v1/hub.pbjson.dart index 984d83a..419fb92 100644 --- a/lib/src/generated/fai/v1/hub.pbjson.dart +++ b/lib/src/generated/fai/v1/hub.pbjson.dart @@ -534,6 +534,51 @@ final $typed_data.Uint8List decideApprovalResponseDescriptor = $convert.base64De 'ChZEZWNpZGVBcHByb3ZhbFJlc3BvbnNlEh8KC2FwcHJvdmFsX2lkGAEgASgJUgphcHByb3ZhbE' 'lkEhYKBnN0YXR1cxgCIAEoCVIGc3RhdHVz'); +@$core.Deprecated('Use verifyEventChainResponseDescriptor instead') +const VerifyEventChainResponse$json = { + '1': 'VerifyEventChainResponse', + '2': [ + {'1': 'total', '3': 1, '4': 1, '5': 3, '10': 'total'}, + {'1': 'verified', '3': 2, '4': 1, '5': 3, '10': 'verified'}, + {'1': 'tampered_at', '3': 3, '4': 1, '5': 9, '10': 'tamperedAt'}, + ], +}; + +/// Descriptor for `VerifyEventChainResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List verifyEventChainResponseDescriptor = $convert.base64Decode( + 'ChhWZXJpZnlFdmVudENoYWluUmVzcG9uc2USFAoFdG90YWwYASABKANSBXRvdGFsEhoKCHZlcm' + 'lmaWVkGAIgASgDUgh2ZXJpZmllZBIfCgt0YW1wZXJlZF9hdBgDIAEoCVIKdGFtcGVyZWRBdA=='); + +@$core.Deprecated('Use serviceListDescriptor instead') +const ServiceList$json = { + '1': 'ServiceList', + '2': [ + {'1': 'services', '3': 1, '4': 3, '5': 11, '6': '.fai.v1.DeclaredService', '10': 'services'}, + ], +}; + +/// Descriptor for `ServiceList`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List serviceListDescriptor = $convert.base64Decode( + 'CgtTZXJ2aWNlTGlzdBIzCghzZXJ2aWNlcxgBIAMoCzIXLmZhaS52MS5EZWNsYXJlZFNlcnZpY2' + 'VSCHNlcnZpY2Vz'); + +@$core.Deprecated('Use declaredServiceDescriptor instead') +const DeclaredService$json = { + '1': 'DeclaredService', + '2': [ + {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'}, + {'1': 'endpoint', '3': 2, '4': 1, '5': 9, '10': 'endpoint'}, + {'1': 'health_path', '3': 3, '4': 1, '5': 9, '10': 'healthPath'}, + {'1': 'tags', '3': 4, '4': 3, '5': 9, '10': 'tags'}, + ], +}; + +/// Descriptor for `DeclaredService`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List declaredServiceDescriptor = $convert.base64Decode( + 'Cg9EZWNsYXJlZFNlcnZpY2USEgoEbmFtZRgBIAEoCVIEbmFtZRIaCghlbmRwb2ludBgCIAEoCV' + 'IIZW5kcG9pbnQSHwoLaGVhbHRoX3BhdGgYAyABKAlSCmhlYWx0aFBhdGgSEgoEdGFncxgEIAMo' + 'CVIEdGFncw=='); + @$core.Deprecated('Use storeEntryDescriptor instead') const StoreEntry$json = { '1': 'StoreEntry', diff --git a/lib/src/hub_client.dart b/lib/src/hub_client.dart index bf71f62..fbb1fae 100644 --- a/lib/src/hub_client.dart +++ b/lib/src/hub_client.dart @@ -38,6 +38,8 @@ typedef CapabilityEntry = pb.CapabilityEntry; typedef CapabilityList = pb.CapabilityList; typedef LoggedEvent = pb.LoggedEvent; typedef PendingApprovalEntry = pb.PendingApprovalEntry; +typedef DeclaredService = pb.DeclaredService; +typedef VerifyEventChainResponse = pb.VerifyEventChainResponse; /// Typed client for the F∆I Hub gRPC surface. Construct once, /// reuse for the process lifetime, call [close] on shutdown. @@ -150,6 +152,20 @@ class HubClient { await _admin.decideApproval(req); } + /// Walk the event-log hash chain on the hub. Returns total / + /// verified counts and an `tamperedAt` id (empty when clean). + Future verifyEventChain() { + return _admin.verifyEventChain(Empty()); + } + + /// Host services declared in the operator config. Each entry + /// is a name + endpoint + tags; reachability is up to the + /// caller (typically a follow-up HTTP probe). + Future> listServices() async { + final r = await _admin.listServices(Empty()); + return r.services; + } + /// Closes the gRPC channel. Idempotent. Future close() async { await _channel.shutdown();