feat: declareService — persist a host service via the new RPC
Some checks failed
Security / Security check (push) Failing after 1s

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-22 23:56:03 +02:00
parent a0c5725578
commit 70f04af438
5 changed files with 233 additions and 0 deletions

View file

@ -6,6 +6,9 @@ for the authoritative per-release detail.
## Unreleased ## Unreleased
- `declareService()` — persist a host service into the operator
config via the new DeclareService RPC (returns restartRequired).
- Regenerated stubs: `StoreEntry.maintainers` (maintainer display - Regenerated stubs: `StoreEntry.maintainers` (maintainer display
strings from the store index). strings from the store index).

View file

@ -4956,6 +4956,154 @@ class ServiceList extends $pb.GeneratedMessage {
$pb.PbList<DeclaredService> get services => $_getList(0); $pb.PbList<DeclaredService> 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<DeclareServiceRequest>(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<DeclareServiceResponse>(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 { class DeclaredService extends $pb.GeneratedMessage {
factory DeclaredService({ factory DeclaredService({
$core.String? name, $core.String? name,

View file

@ -622,6 +622,15 @@ class HubAdminClient extends $grpc.Client {
return $createUnaryCall(_$listServices, request, options: options); 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 /// Compare the running hub's version against the channel release
/// manifest. Surfaced by the CLI as `chain update check` and by /// manifest. Surfaced by the CLI as `chain update check` and by
/// Studio's Doctor page as a "v0.x.y available" pill. Network /// 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', '/chain.v1.HubAdmin/ListServices',
($1.Empty value) => value.writeToBuffer(), ($1.Empty value) => value.writeToBuffer(),
$0.ServiceList.fromBuffer); $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 = static final _$checkUpdate =
$grpc.ClientMethod<$1.Empty, $0.CheckUpdateResponse>( $grpc.ClientMethod<$1.Empty, $0.CheckUpdateResponse>(
'/chain.v1.HubAdmin/CheckUpdate', '/chain.v1.HubAdmin/CheckUpdate',
@ -1393,6 +1407,15 @@ abstract class HubAdminServiceBase extends $grpc.Service {
false, false,
($core.List<$core.int> value) => $1.Empty.fromBuffer(value), ($core.List<$core.int> value) => $1.Empty.fromBuffer(value),
($0.ServiceList value) => value.writeToBuffer())); ($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>( $addMethod($grpc.ServiceMethod<$1.Empty, $0.CheckUpdateResponse>(
'CheckUpdate', 'CheckUpdate',
checkUpdate_Pre, checkUpdate_Pre,
@ -1842,6 +1865,15 @@ abstract class HubAdminServiceBase extends $grpc.Service {
$async.Future<$0.ServiceList> listServices( $async.Future<$0.ServiceList> listServices(
$grpc.ServiceCall call, $1.Empty request); $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( $async.Future<$0.CheckUpdateResponse> checkUpdate_Pre(
$grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async { $grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async {
return checkUpdate($call, await $request); return checkUpdate($call, await $request);

View file

@ -1303,6 +1303,37 @@ final $typed_data.Uint8List serviceListDescriptor = $convert.base64Decode(
'CgtTZXJ2aWNlTGlzdBI1CghzZXJ2aWNlcxgBIAMoCzIZLmNoYWluLnYxLkRlY2xhcmVkU2Vydm' 'CgtTZXJ2aWNlTGlzdBI1CghzZXJ2aWNlcxgBIAMoCzIZLmNoYWluLnYxLkRlY2xhcmVkU2Vydm'
'ljZVIIc2VydmljZXM='); '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') @$core.Deprecated('Use declaredServiceDescriptor instead')
const DeclaredService$json = { const DeclaredService$json = {
'1': 'DeclaredService', '1': 'DeclaredService',

View file

@ -423,6 +423,25 @@ class HubClient {
return r.services; 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<bool> declareService({
required String name,
required String endpoint,
String healthPath = '',
List<String> 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. /// Compare local hub version vs. release-channel manifest.
/// Network failures land in `manifestReachable=false` rather /// Network failures land in `manifestReachable=false` rather
/// than as exceptions Studio renders that case as a quiet /// than as exceptions Studio renders that case as a quiet