From 70f04af438ffa4ec07066e07a06e5be5cf4a2b8d Mon Sep 17 00:00:00 2001 From: flemming-it Date: Wed, 22 Jul 2026 23:56:03 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20declareService=20=E2=80=94=20persist=20?= =?UTF-8?q?a=20host=20service=20via=20the=20new=20RPC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: flemming-it --- CHANGELOG.md | 3 + lib/src/generated/chain/v1/hub.pb.dart | 148 +++++++++++++++++++++ lib/src/generated/chain/v1/hub.pbgrpc.dart | 32 +++++ lib/src/generated/chain/v1/hub.pbjson.dart | 31 +++++ lib/src/hub_client.dart | 19 +++ 5 files changed, 233 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d8781f..5de393f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ for the authoritative per-release detail. ## Unreleased +- `declareService()` — persist a host service into the operator + config via the new DeclareService RPC (returns restartRequired). + - Regenerated stubs: `StoreEntry.maintainers` (maintainer display strings from the store index). diff --git a/lib/src/generated/chain/v1/hub.pb.dart b/lib/src/generated/chain/v1/hub.pb.dart index 92f11ec..475c67b 100644 --- a/lib/src/generated/chain/v1/hub.pb.dart +++ b/lib/src/generated/chain/v1/hub.pb.dart @@ -4956,6 +4956,154 @@ class ServiceList extends $pb.GeneratedMessage { $pb.PbList get services => $_getList(0); } +/// Declare a host service in the operator config (`services:`). +/// The hub persists it to ~/.chain/config.yaml with the same +/// round-trip semantics as the MCP/n8n writers (every other key +/// stays intact). Services are read at daemon start, so the +/// response says honestly that a restart is required. +class DeclareServiceRequest extends $pb.GeneratedMessage { + factory DeclareServiceRequest({ + $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; + } + + DeclareServiceRequest._(); + + factory DeclareServiceRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory DeclareServiceRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'DeclareServiceRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'chain.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.') + DeclareServiceRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeclareServiceRequest copyWith( + void Function(DeclareServiceRequest) updates) => + super.copyWith((message) => updates(message as DeclareServiceRequest)) + as DeclareServiceRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeclareServiceRequest create() => DeclareServiceRequest._(); + @$core.override + DeclareServiceRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static DeclareServiceRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static DeclareServiceRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get name => $_getSZ(0); + @$pb.TagNumber(1) + set name($core.String value) => $_setString(0, value); + @$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 value) => $_setString(1, value); + @$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 value) => $_setString(2, value); + @$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 DeclareServiceResponse extends $pb.GeneratedMessage { + factory DeclareServiceResponse({ + $core.bool? restartRequired, + }) { + final result = create(); + if (restartRequired != null) result.restartRequired = restartRequired; + return result; + } + + DeclareServiceResponse._(); + + factory DeclareServiceResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory DeclareServiceResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'DeclareServiceResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'chain.v1'), + createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'restartRequired') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeclareServiceResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + DeclareServiceResponse copyWith( + void Function(DeclareServiceResponse) updates) => + super.copyWith((message) => updates(message as DeclareServiceResponse)) + as DeclareServiceResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeclareServiceResponse create() => DeclareServiceResponse._(); + @$core.override + DeclareServiceResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static DeclareServiceResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static DeclareServiceResponse? _defaultInstance; + + /// True until the hub learns to hot-reload `services:` — the + /// caller should surface "takes effect after a daemon restart". + @$pb.TagNumber(1) + $core.bool get restartRequired => $_getBF(0); + @$pb.TagNumber(1) + set restartRequired($core.bool value) => $_setBool(0, value); + @$pb.TagNumber(1) + $core.bool hasRestartRequired() => $_has(0); + @$pb.TagNumber(1) + void clearRestartRequired() => $_clearField(1); +} + class DeclaredService extends $pb.GeneratedMessage { factory DeclaredService({ $core.String? name, diff --git a/lib/src/generated/chain/v1/hub.pbgrpc.dart b/lib/src/generated/chain/v1/hub.pbgrpc.dart index 6fdc916..5a4754d 100644 --- a/lib/src/generated/chain/v1/hub.pbgrpc.dart +++ b/lib/src/generated/chain/v1/hub.pbgrpc.dart @@ -622,6 +622,15 @@ class HubAdminClient extends $grpc.Client { return $createUnaryCall(_$listServices, request, options: options); } + /// Declare a new host service in the operator config. Fails with + /// ALREADY_EXISTS when a service of that name is configured. + $grpc.ResponseFuture<$0.DeclareServiceResponse> declareService( + $0.DeclareServiceRequest request, { + $grpc.CallOptions? options, + }) { + return $createUnaryCall(_$declareService, request, options: options); + } + /// Compare the running hub's version against the channel release /// manifest. Surfaced by the CLI as `chain update check` and by /// Studio's Doctor page as a "v0.x.y available" pill. Network @@ -1021,6 +1030,11 @@ class HubAdminClient extends $grpc.Client { '/chain.v1.HubAdmin/ListServices', ($1.Empty value) => value.writeToBuffer(), $0.ServiceList.fromBuffer); + static final _$declareService = + $grpc.ClientMethod<$0.DeclareServiceRequest, $0.DeclareServiceResponse>( + '/chain.v1.HubAdmin/DeclareService', + ($0.DeclareServiceRequest value) => value.writeToBuffer(), + $0.DeclareServiceResponse.fromBuffer); static final _$checkUpdate = $grpc.ClientMethod<$1.Empty, $0.CheckUpdateResponse>( '/chain.v1.HubAdmin/CheckUpdate', @@ -1393,6 +1407,15 @@ abstract class HubAdminServiceBase extends $grpc.Service { false, ($core.List<$core.int> value) => $1.Empty.fromBuffer(value), ($0.ServiceList value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.DeclareServiceRequest, + $0.DeclareServiceResponse>( + 'DeclareService', + declareService_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.DeclareServiceRequest.fromBuffer(value), + ($0.DeclareServiceResponse value) => value.writeToBuffer())); $addMethod($grpc.ServiceMethod<$1.Empty, $0.CheckUpdateResponse>( 'CheckUpdate', checkUpdate_Pre, @@ -1842,6 +1865,15 @@ abstract class HubAdminServiceBase extends $grpc.Service { $async.Future<$0.ServiceList> listServices( $grpc.ServiceCall call, $1.Empty request); + $async.Future<$0.DeclareServiceResponse> declareService_Pre( + $grpc.ServiceCall $call, + $async.Future<$0.DeclareServiceRequest> $request) async { + return declareService($call, await $request); + } + + $async.Future<$0.DeclareServiceResponse> declareService( + $grpc.ServiceCall call, $0.DeclareServiceRequest request); + $async.Future<$0.CheckUpdateResponse> checkUpdate_Pre( $grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async { return checkUpdate($call, await $request); diff --git a/lib/src/generated/chain/v1/hub.pbjson.dart b/lib/src/generated/chain/v1/hub.pbjson.dart index d59b5a2..3359860 100644 --- a/lib/src/generated/chain/v1/hub.pbjson.dart +++ b/lib/src/generated/chain/v1/hub.pbjson.dart @@ -1303,6 +1303,37 @@ final $typed_data.Uint8List serviceListDescriptor = $convert.base64Decode( 'CgtTZXJ2aWNlTGlzdBI1CghzZXJ2aWNlcxgBIAMoCzIZLmNoYWluLnYxLkRlY2xhcmVkU2Vydm' 'ljZVIIc2VydmljZXM='); +@$core.Deprecated('Use declareServiceRequestDescriptor instead') +const DeclareServiceRequest$json = { + '1': 'DeclareServiceRequest', + '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 `DeclareServiceRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List declareServiceRequestDescriptor = $convert.base64Decode( + 'ChVEZWNsYXJlU2VydmljZVJlcXVlc3QSEgoEbmFtZRgBIAEoCVIEbmFtZRIaCghlbmRwb2ludB' + 'gCIAEoCVIIZW5kcG9pbnQSHwoLaGVhbHRoX3BhdGgYAyABKAlSCmhlYWx0aFBhdGgSEgoEdGFn' + 'cxgEIAMoCVIEdGFncw=='); + +@$core.Deprecated('Use declareServiceResponseDescriptor instead') +const DeclareServiceResponse$json = { + '1': 'DeclareServiceResponse', + '2': [ + {'1': 'restart_required', '3': 1, '4': 1, '5': 8, '10': 'restartRequired'}, + ], +}; + +/// Descriptor for `DeclareServiceResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List declareServiceResponseDescriptor = + $convert.base64Decode( + 'ChZEZWNsYXJlU2VydmljZVJlc3BvbnNlEikKEHJlc3RhcnRfcmVxdWlyZWQYASABKAhSD3Jlc3' + 'RhcnRSZXF1aXJlZA=='); + @$core.Deprecated('Use declaredServiceDescriptor instead') const DeclaredService$json = { '1': 'DeclaredService', diff --git a/lib/src/hub_client.dart b/lib/src/hub_client.dart index 311a133..2b8b53d 100644 --- a/lib/src/hub_client.dart +++ b/lib/src/hub_client.dart @@ -423,6 +423,25 @@ class HubClient { return r.services; } + /// Declare a host service in the operator config (`services:`). + /// Returns whether a daemon restart is required for it to take + /// effect (true today — services are read at start). Throws + /// ALREADY_EXISTS when the name is taken. + Future declareService({ + required String name, + required String endpoint, + String healthPath = '', + List tags = const [], + }) async { + final r = await _admin.declareService(pb.DeclareServiceRequest( + name: name, + endpoint: endpoint, + healthPath: healthPath, + tags: tags, + )); + return r.restartRequired; + } + /// Compare local hub version vs. release-channel manifest. /// Network failures land in `manifestReachable=false` rather /// than as exceptions — Studio renders that case as a quiet