feat: live HubClient with generated proto bindings

Proto codegen wired in. tools/generate.sh wraps protoc against
../fai_platform/proto/, including the well-known types (Empty,
Struct, Timestamp) so they are generated alongside fai's own
messages — protobuf 4.x doesn't ship them as importable Dart
types, so we have to generate them ourselves.

HubClient now exposes typed methods backed by real RPCs:
- healthy() — Hub.Health probe (returns false on connect refused).
- listCapabilities() — HubAdmin.ListCapabilities.
- eventLog() — HubAdmin.EventLog with type filter.
- listApprovals() — HubAdmin.ListApprovals (status filter).
- approve() / reject() — HubAdmin.DecideApproval.

Generated bindings under lib/src/generated/ are excluded from
analyze (out of our style control). protoc_plugin pinned to
22.2.0 in tools/generate.sh comment because newer plugins
generate code against protobuf 6.x, while grpc 4.x still
constrains us to protobuf 4.x.

Bumps fai_dart_sdk 0.1.0 -> 0.2.0.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-05 16:24:16 +02:00
parent d405a6a9bd
commit da10820959
26 changed files with 5364 additions and 28 deletions

View file

@ -1,11 +1,14 @@
// HubClient typed wrapper around the FI Hub gRPC service.
// HubClient typed Dart wrapper around the FI gRPC surface.
//
// Status (2026-05-05): scaffold. The generated proto bindings
// from `fai/platform`'s proto/fai/v1/ directory will populate
// the actual RPC implementations under `lib/src/generated/`
// once the codegen tooling is wired (planned: a small
// `tools/generate.sh` script that runs `protoc` with
// `protoc-gen-dart` against the platform repo's protos).
// Holds a single grpc.ClientChannel keyed by [HubEndpoint]. Each
// public method maps onto exactly one RPC on Hub or HubAdmin, so
// callers don't have to know which service hosts which call.
import 'package:grpc/grpc.dart';
import 'generated/fai/v1/hub.pb.dart' as pb;
import 'generated/fai/v1/hub.pbgrpc.dart' as grpc_hub;
import 'generated/google/protobuf/empty.pb.dart' show Empty;
/// Connection coordinates for a hub endpoint.
class HubEndpoint {
@ -29,15 +32,126 @@ class HubEndpoint {
String toString() => '${secure ? "https" : "http"}://$host:$port';
}
/// Stub client interface. The real implementation will arrive
/// when proto codegen is wired in. Studio uses this type as the
/// future-stable surface; the mock data layer in
/// `fai_studio/lib/data/mock.dart` is the placeholder until then.
/// Re-exports of generated protobuf types so callers don't have
/// to import the `generated/` directory directly.
typedef CapabilityEntry = pb.CapabilityEntry;
typedef CapabilityList = pb.CapabilityList;
typedef LoggedEvent = pb.LoggedEvent;
typedef PendingApprovalEntry = pb.PendingApprovalEntry;
/// Typed client for the FI Hub gRPC surface. Construct once,
/// reuse for the process lifetime, call [close] on shutdown.
class HubClient {
final HubEndpoint endpoint;
final ClientChannel _channel;
final grpc_hub.HubClient _hub;
final grpc_hub.HubAdminClient _admin;
HubClient({this.endpoint = const HubEndpoint()});
HubClient({this.endpoint = const HubEndpoint()})
: _channel = ClientChannel(
endpoint.host,
port: endpoint.port,
options: ChannelOptions(
credentials: endpoint.secure
? const ChannelCredentials.secure()
: const ChannelCredentials.insecure(),
),
),
_hub = grpc_hub.HubClient(
ClientChannel(
endpoint.host,
port: endpoint.port,
options: ChannelOptions(
credentials: endpoint.secure
? const ChannelCredentials.secure()
: const ChannelCredentials.insecure(),
),
),
),
_admin = grpc_hub.HubAdminClient(
ClientChannel(
endpoint.host,
port: endpoint.port,
options: ChannelOptions(
credentials: endpoint.secure
? const ChannelCredentials.secure()
: const ChannelCredentials.insecure(),
),
),
);
/// Closes the underlying channel. No-op while stubbed.
Future<void> close() async {}
/// Liveness probe. Returns true when the hub responds with
/// SERVING. False on any failure (includes connection refused).
Future<bool> healthy() async {
try {
final r = await _hub.health(Empty());
return r.state == pb.HealthStatus_State.SERVING;
} catch (_) {
return false;
}
}
/// All capabilities provided by installed modules.
Future<List<CapabilityEntry>> listCapabilities() async {
final r = await _admin.listCapabilities(Empty());
return r.capabilities;
}
/// Recent events from the audit log, newest-first. Pass an
/// empty `types` list for all event types.
Future<List<LoggedEvent>> eventLog({
int limit = 100,
List<String> types = const [],
}) async {
final req = pb.EventLogRequest()
..limit = limit
..eventTypes.addAll(types);
final r = await _admin.eventLog(req);
return r.events;
}
/// Pending approvals filtered by status. Defaults to all.
Future<List<PendingApprovalEntry>> listApprovals({
List<String> statuses = const [],
int limit = 100,
}) async {
final req = pb.ListApprovalsRequest()
..limit = limit
..statuses.addAll(statuses);
final r = await _admin.listApprovals(req);
return r.entries;
}
/// Approve a pending approval. The flow that posted it resumes
/// on its next polling tick.
Future<void> approve({
required String approvalId,
required String reviewer,
}) async {
final req = pb.DecideApprovalRequest()
..approvalId = approvalId
..reviewer = reviewer
..decision = pb.DecideApprovalRequest_Decision.APPROVE;
await _admin.decideApproval(req);
}
/// Reject a pending approval. The flow fails with the supplied
/// reason on its next polling tick.
Future<void> reject({
required String approvalId,
required String reviewer,
required String reason,
}) async {
final req = pb.DecideApprovalRequest()
..approvalId = approvalId
..reviewer = reviewer
..reason = reason
..decision = pb.DecideApprovalRequest_Decision.REJECT;
await _admin.decideApproval(req);
}
/// Closes the gRPC channel. Idempotent.
Future<void> close() async {
await _channel.shutdown();
}
}