Compare commits

..

5 commits

Author SHA1 Message Date
flemming-it
f481eee687 feat(client): GrpcWebClientChannel on web targets via conditional import
Some checks failed
Security / Security check (push) Failing after 1s
Splits channel construction into two files selected at
compile-time via `if (dart.library.html)`:

  channel_factory_io.dart   — native ClientChannel (HTTP/2)
  channel_factory_web.dart  — GrpcWebClientChannel.xhr (HTTP/1.1)

HubClient._channel is now `ClientChannelBase` so both
implementations satisfy the same field type. dart:html-only
imports (`package:grpc/grpc_web.dart`) stay isolated from the
native compile path.

Pairs with the hub-side `fai_grpc_web` adapter crate
(fai/platform 0d07892) — `tonic-web` on the same port as
native gRPC, so one `fai serve` listener handles both
surfaces.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-05-26 13:34:26 +02:00
flemming-it
5604e5eaf3 chore(proto): regenerate after InvokePluginTranslate RPC
Adds HubClient.invokePluginTranslate as a typed method.
Same shape as invokePluginTheme: capability + args in,
typed response out.

Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-05-25 23:02:21 +02:00
flemming-it
4a582f2b8d chore(proto): regenerate after InvokePluginTheme RPC
Tracks fai/platform's new gRPC RPC + matching request /
response messages. Adds HubClient.invokePluginTheme as a
typed Dart method.

Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-05-25 21:49:39 +02:00
flemming-it
6ebec5e3ae chore(proto): regenerate after CapabilityEntry.kind + accepts_mime
Tracks the platform's hub.proto v0.11 update — adds the new
fields to the generated Dart bindings:

- `CapabilityEntry.kind` (string) — wasm / builtin / federated.
  Studio uses it to gate the Install button so built-ins like
  `system.approval` and federated MCP/n8n tools never get
  offered an install path that doesn't exist.
- `ModuleInfoResponse.accepts_mime` (repeated string) — MIME
  allow-list declared in `module.yaml`. Studio reads it for
  file-picker filtering.

No hand-edited code in this commit — `tools/generate.sh`
output only.

Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-05-25 12:35:45 +02:00
flemming-it
0879a95aba feat(hub-client): GetInstalledModuleDocs + per-file MIME
- New getInstalledModuleDocs(name, locale) wraps the new gRPC
  RPC; callers get text + source path + a not-installed flag so
  Studio can decide whether to show a Documentation section at
  all.
- runSavedFlow gains an optional fileMimeTypes map keyed
  parallel to fileInputs. Hard-coded application/octet-stream
  remains as the fallback, but text.extract and other
  MIME-gating modules now get an accurate type when the caller
  knows one.
- fetchModuleDocs is marked @Deprecated; it still works for
  older consumers but Studio no longer calls it.
- Proto bindings regenerated against fai/platform proto.

Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-05-25 11:17:31 +02:00
6 changed files with 763 additions and 24 deletions

View file

@ -0,0 +1,21 @@
// Native channel factory plain gRPC over HTTP/2.
// Selected on every non-web target (macOS, Linux, Windows, iOS,
// Android) via the conditional import in hub_client.dart.
import 'package:grpc/grpc.dart';
import 'package:grpc/grpc_connection_interface.dart';
import 'hub_client.dart' show HubEndpoint;
ClientChannelBase createChannel(HubEndpoint endpoint) {
return ClientChannel(
endpoint.host,
port: endpoint.port,
options: ChannelOptions(
credentials: endpoint.secure
? const ChannelCredentials.secure()
: const ChannelCredentials.insecure(),
idleTimeout: const Duration(minutes: 5),
),
);
}

View file

@ -0,0 +1,17 @@
// Web channel factory gRPC-Web over HTTP/1.1.
// Selected on dart:html targets (Flutter web, Dart-for-web) via
// the conditional import in hub_client.dart. The hub must be
// running with `tonic-web` enabled see fai_platform docs
// architecture/protocol-surfaces.md.
import 'package:grpc/grpc_connection_interface.dart';
import 'package:grpc/grpc_web.dart';
import 'hub_client.dart' show HubEndpoint;
ClientChannelBase createChannel(HubEndpoint endpoint) {
final scheme = endpoint.secure ? 'https' : 'http';
return GrpcWebClientChannel.xhr(
Uri.parse('$scheme://${endpoint.host}:${endpoint.port}'),
);
}

View file

@ -336,6 +336,7 @@ class CapabilityEntry extends $pb.GeneratedMessage {
$core.String? version, $core.String? version,
$core.String? moduleName, $core.String? moduleName,
$core.String? moduleVersion, $core.String? moduleVersion,
$core.String? kind,
}) { }) {
final $result = create(); final $result = create();
if (capability != null) { if (capability != null) {
@ -350,6 +351,9 @@ class CapabilityEntry extends $pb.GeneratedMessage {
if (moduleVersion != null) { if (moduleVersion != null) {
$result.moduleVersion = moduleVersion; $result.moduleVersion = moduleVersion;
} }
if (kind != null) {
$result.kind = kind;
}
return $result; return $result;
} }
CapabilityEntry._() : super(); CapabilityEntry._() : super();
@ -361,6 +365,7 @@ class CapabilityEntry extends $pb.GeneratedMessage {
..aOS(2, _omitFieldNames ? '' : 'version') ..aOS(2, _omitFieldNames ? '' : 'version')
..aOS(3, _omitFieldNames ? '' : 'moduleName') ..aOS(3, _omitFieldNames ? '' : 'moduleName')
..aOS(4, _omitFieldNames ? '' : 'moduleVersion') ..aOS(4, _omitFieldNames ? '' : 'moduleVersion')
..aOS(5, _omitFieldNames ? '' : 'kind')
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -418,6 +423,24 @@ class CapabilityEntry extends $pb.GeneratedMessage {
$core.bool hasModuleVersion() => $_has(3); $core.bool hasModuleVersion() => $_has(3);
@$pb.TagNumber(4) @$pb.TagNumber(4)
void clearModuleVersion() => $_clearField(4); void clearModuleVersion() => $_clearField(4);
/// Where this capability comes from. Studio gates install
/// affordances on this only "wasm" caps have a bundle to
/// install. Empty string is treated as "wasm" for
/// backwards-compat with older hubs.
/// "wasm" declared by an installed WASM module
/// "builtin" implemented in the hub itself
/// (e.g. system.approval)
/// "federated" discovered from an external MCP/n8n server;
/// not installable via the module-store path
@$pb.TagNumber(5)
$core.String get kind => $_getSZ(4);
@$pb.TagNumber(5)
set kind($core.String v) { $_setString(4, v); }
@$pb.TagNumber(5)
$core.bool hasKind() => $_has(4);
@$pb.TagNumber(5)
void clearKind() => $_clearField(5);
} }
class ModuleInfoRequest extends $pb.GeneratedMessage { class ModuleInfoRequest extends $pb.GeneratedMessage {
@ -471,6 +494,7 @@ class ModuleInfoResponse extends $pb.GeneratedMessage {
$core.Iterable<$4.CapabilityRef>? provides, $core.Iterable<$4.CapabilityRef>? provides,
$core.Iterable<$core.String>? permissions, $core.Iterable<$core.String>? permissions,
$core.String? directory, $core.String? directory,
$core.Iterable<$core.String>? acceptsMime,
}) { }) {
final $result = create(); final $result = create();
if (name != null) { if (name != null) {
@ -488,6 +512,9 @@ class ModuleInfoResponse extends $pb.GeneratedMessage {
if (directory != null) { if (directory != null) {
$result.directory = directory; $result.directory = directory;
} }
if (acceptsMime != null) {
$result.acceptsMime.addAll(acceptsMime);
}
return $result; return $result;
} }
ModuleInfoResponse._() : super(); ModuleInfoResponse._() : super();
@ -500,6 +527,7 @@ class ModuleInfoResponse extends $pb.GeneratedMessage {
..pc<$4.CapabilityRef>(3, _omitFieldNames ? '' : 'provides', $pb.PbFieldType.PM, subBuilder: $4.CapabilityRef.create) ..pc<$4.CapabilityRef>(3, _omitFieldNames ? '' : 'provides', $pb.PbFieldType.PM, subBuilder: $4.CapabilityRef.create)
..pPS(4, _omitFieldNames ? '' : 'permissions') ..pPS(4, _omitFieldNames ? '' : 'permissions')
..aOS(5, _omitFieldNames ? '' : 'directory') ..aOS(5, _omitFieldNames ? '' : 'directory')
..pPS(6, _omitFieldNames ? '' : 'acceptsMime')
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -552,6 +580,14 @@ class ModuleInfoResponse extends $pb.GeneratedMessage {
$core.bool hasDirectory() => $_has(4); $core.bool hasDirectory() => $_has(4);
@$pb.TagNumber(5) @$pb.TagNumber(5)
void clearDirectory() => $_clearField(5); void clearDirectory() => $_clearField(5);
/// MIME allow-list for bytes-shaped inputs, copied verbatim
/// from `module.yaml`'s `accepts_mime:` field. Empty means
/// "no declared restriction". Studio uses this to pre-filter
/// the OS file-picker so operators don't pick a file the
/// module would reject.
@$pb.TagNumber(6)
$pb.PbList<$core.String> get acceptsMime => $_getList(5);
} }
class CheckPermissionRequest extends $pb.GeneratedMessage { class CheckPermissionRequest extends $pb.GeneratedMessage {
@ -2055,6 +2091,416 @@ class FetchModuleDocsResponse extends $pb.GeneratedMessage {
void clearSourceUrl() => $_clearField(3); void clearSourceUrl() => $_clearField(3);
} }
class GetInstalledModuleDocsRequest extends $pb.GeneratedMessage {
factory GetInstalledModuleDocsRequest({
$core.String? name,
$core.String? locale,
}) {
final $result = create();
if (name != null) {
$result.name = name;
}
if (locale != null) {
$result.locale = locale;
}
return $result;
}
GetInstalledModuleDocsRequest._() : super();
factory GetInstalledModuleDocsRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory GetInstalledModuleDocsRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GetInstalledModuleDocsRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'name')
..aOS(2, _omitFieldNames ? '' : 'locale')
..hasRequiredFields = false
;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetInstalledModuleDocsRequest clone() => GetInstalledModuleDocsRequest()..mergeFromMessage(this);
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetInstalledModuleDocsRequest copyWith(void Function(GetInstalledModuleDocsRequest) updates) => super.copyWith((message) => updates(message as GetInstalledModuleDocsRequest)) as GetInstalledModuleDocsRequest;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static GetInstalledModuleDocsRequest create() => GetInstalledModuleDocsRequest._();
GetInstalledModuleDocsRequest createEmptyInstance() => create();
static $pb.PbList<GetInstalledModuleDocsRequest> createRepeated() => $pb.PbList<GetInstalledModuleDocsRequest>();
@$core.pragma('dart2js:noInline')
static GetInstalledModuleDocsRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<GetInstalledModuleDocsRequest>(create);
static GetInstalledModuleDocsRequest? _defaultInstance;
/// Capability id (e.g. `text.extract`) or module-manifest name
/// (e.g. `text-extract`). The hub maps capability module via
/// the registry; falls back to treating the name as a
/// directory name.
@$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);
/// Preferred locale code (e.g. "de", "en"). When set and not
/// "en", `MODULE.<locale>.md` is tried first; otherwise the
/// generic `MODULE.md` is used. Empty string = no preference.
@$pb.TagNumber(2)
$core.String get locale => $_getSZ(1);
@$pb.TagNumber(2)
set locale($core.String v) { $_setString(1, v); }
@$pb.TagNumber(2)
$core.bool hasLocale() => $_has(1);
@$pb.TagNumber(2)
void clearLocale() => $_clearField(2);
}
class GetInstalledModuleDocsResponse extends $pb.GeneratedMessage {
factory GetInstalledModuleDocsResponse({
$core.String? text,
$core.String? sourcePath,
$core.bool? notInstalled,
}) {
final $result = create();
if (text != null) {
$result.text = text;
}
if (sourcePath != null) {
$result.sourcePath = sourcePath;
}
if (notInstalled != null) {
$result.notInstalled = notInstalled;
}
return $result;
}
GetInstalledModuleDocsResponse._() : super();
factory GetInstalledModuleDocsResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory GetInstalledModuleDocsResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GetInstalledModuleDocsResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'text')
..aOS(2, _omitFieldNames ? '' : 'sourcePath')
..aOB(3, _omitFieldNames ? '' : 'notInstalled')
..hasRequiredFields = false
;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetInstalledModuleDocsResponse clone() => GetInstalledModuleDocsResponse()..mergeFromMessage(this);
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetInstalledModuleDocsResponse copyWith(void Function(GetInstalledModuleDocsResponse) updates) => super.copyWith((message) => updates(message as GetInstalledModuleDocsResponse)) as GetInstalledModuleDocsResponse;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static GetInstalledModuleDocsResponse create() => GetInstalledModuleDocsResponse._();
GetInstalledModuleDocsResponse createEmptyInstance() => create();
static $pb.PbList<GetInstalledModuleDocsResponse> createRepeated() => $pb.PbList<GetInstalledModuleDocsResponse>();
@$core.pragma('dart2js:noInline')
static GetInstalledModuleDocsResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<GetInstalledModuleDocsResponse>(create);
static GetInstalledModuleDocsResponse? _defaultInstance;
/// Markdown body. Empty when the module is installed but ships
/// no inline docs Studio uses this to hide its docs button.
@$pb.TagNumber(1)
$core.String get text => $_getSZ(0);
@$pb.TagNumber(1)
set text($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasText() => $_has(0);
@$pb.TagNumber(1)
void clearText() => $_clearField(1);
/// Absolute path on the hub host that the body was read from,
/// for "loaded from /path/MODULE.md" hints in Studio. Empty
/// when text is empty.
@$pb.TagNumber(2)
$core.String get sourcePath => $_getSZ(1);
@$pb.TagNumber(2)
set sourcePath($core.String v) { $_setString(1, v); }
@$pb.TagNumber(2)
$core.bool hasSourcePath() => $_has(1);
@$pb.TagNumber(2)
void clearSourcePath() => $_clearField(2);
/// True when the module wasn't installed at all. Distinct from
/// "installed but no inline docs" so Studio can show different
/// affordances ("install first" vs "no inline docs available").
@$pb.TagNumber(3)
$core.bool get notInstalled => $_getBF(2);
@$pb.TagNumber(3)
set notInstalled($core.bool v) { $_setBool(2, v); }
@$pb.TagNumber(3)
$core.bool hasNotInstalled() => $_has(2);
@$pb.TagNumber(3)
void clearNotInstalled() => $_clearField(3);
}
class InvokePluginThemeRequest extends $pb.GeneratedMessage {
factory InvokePluginThemeRequest({
$core.String? capability,
$core.String? brightness,
}) {
final $result = create();
if (capability != null) {
$result.capability = capability;
}
if (brightness != null) {
$result.brightness = brightness;
}
return $result;
}
InvokePluginThemeRequest._() : super();
factory InvokePluginThemeRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory InvokePluginThemeRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'InvokePluginThemeRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'capability')
..aOS(2, _omitFieldNames ? '' : 'brightness')
..hasRequiredFields = false
;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
InvokePluginThemeRequest clone() => InvokePluginThemeRequest()..mergeFromMessage(this);
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
InvokePluginThemeRequest copyWith(void Function(InvokePluginThemeRequest) updates) => super.copyWith((message) => updates(message as InvokePluginThemeRequest)) as InvokePluginThemeRequest;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static InvokePluginThemeRequest create() => InvokePluginThemeRequest._();
InvokePluginThemeRequest createEmptyInstance() => create();
static $pb.PbList<InvokePluginThemeRequest> createRepeated() => $pb.PbList<InvokePluginThemeRequest>();
@$core.pragma('dart2js:noInline')
static InvokePluginThemeRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<InvokePluginThemeRequest>(create);
static InvokePluginThemeRequest? _defaultInstance;
/// Capability name the operator asked for, e.g.
/// `studio.theme.solarized`. The hub maps this to the
/// installed plugin directory.
@$pb.TagNumber(1)
$core.String get capability => $_getSZ(0);
@$pb.TagNumber(1)
set capability($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasCapability() => $_has(0);
@$pb.TagNumber(1)
void clearCapability() => $_clearField(1);
/// "light" or "dark" the plugin returns a ColorScheme
/// for this brightness. Plugins that don't recognise the
/// value return PluginError::declined and the hub
/// surfaces that as a friendly error.
@$pb.TagNumber(2)
$core.String get brightness => $_getSZ(1);
@$pb.TagNumber(2)
set brightness($core.String v) { $_setString(1, v); }
@$pb.TagNumber(2)
$core.bool hasBrightness() => $_has(1);
@$pb.TagNumber(2)
void clearBrightness() => $_clearField(2);
}
class InvokePluginTranslateRequest extends $pb.GeneratedMessage {
factory InvokePluginTranslateRequest({
$core.String? capability,
$core.String? text,
$core.String? fromLocale,
$core.String? toLocale,
}) {
final $result = create();
if (capability != null) {
$result.capability = capability;
}
if (text != null) {
$result.text = text;
}
if (fromLocale != null) {
$result.fromLocale = fromLocale;
}
if (toLocale != null) {
$result.toLocale = toLocale;
}
return $result;
}
InvokePluginTranslateRequest._() : super();
factory InvokePluginTranslateRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory InvokePluginTranslateRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'InvokePluginTranslateRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'capability')
..aOS(2, _omitFieldNames ? '' : 'text')
..aOS(3, _omitFieldNames ? '' : 'fromLocale')
..aOS(4, _omitFieldNames ? '' : 'toLocale')
..hasRequiredFields = false
;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
InvokePluginTranslateRequest clone() => InvokePluginTranslateRequest()..mergeFromMessage(this);
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
InvokePluginTranslateRequest copyWith(void Function(InvokePluginTranslateRequest) updates) => super.copyWith((message) => updates(message as InvokePluginTranslateRequest)) as InvokePluginTranslateRequest;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static InvokePluginTranslateRequest create() => InvokePluginTranslateRequest._();
InvokePluginTranslateRequest createEmptyInstance() => create();
static $pb.PbList<InvokePluginTranslateRequest> createRepeated() => $pb.PbList<InvokePluginTranslateRequest>();
@$core.pragma('dart2js:noInline')
static InvokePluginTranslateRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<InvokePluginTranslateRequest>(create);
static InvokePluginTranslateRequest? _defaultInstance;
/// Capability name (e.g. `studio.translate.ollama`).
@$pb.TagNumber(1)
$core.String get capability => $_getSZ(0);
@$pb.TagNumber(1)
set capability($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasCapability() => $_has(0);
@$pb.TagNumber(1)
void clearCapability() => $_clearField(1);
/// Text to translate. UTF-8, arbitrary length the plugin
/// tolerates.
@$pb.TagNumber(2)
$core.String get text => $_getSZ(1);
@$pb.TagNumber(2)
set text($core.String v) { $_setString(1, v); }
@$pb.TagNumber(2)
$core.bool hasText() => $_has(1);
@$pb.TagNumber(2)
void clearText() => $_clearField(2);
/// BCP-47 source locale. Empty = let the plugin auto-detect.
@$pb.TagNumber(3)
$core.String get fromLocale => $_getSZ(2);
@$pb.TagNumber(3)
set fromLocale($core.String v) { $_setString(2, v); }
@$pb.TagNumber(3)
$core.bool hasFromLocale() => $_has(2);
@$pb.TagNumber(3)
void clearFromLocale() => $_clearField(3);
/// BCP-47 target locale. Required; the host always knows it.
@$pb.TagNumber(4)
$core.String get toLocale => $_getSZ(3);
@$pb.TagNumber(4)
set toLocale($core.String v) { $_setString(3, v); }
@$pb.TagNumber(4)
$core.bool hasToLocale() => $_has(3);
@$pb.TagNumber(4)
void clearToLocale() => $_clearField(4);
}
class InvokePluginTranslateResponse extends $pb.GeneratedMessage {
factory InvokePluginTranslateResponse({
$core.String? translated,
}) {
final $result = create();
if (translated != null) {
$result.translated = translated;
}
return $result;
}
InvokePluginTranslateResponse._() : super();
factory InvokePluginTranslateResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory InvokePluginTranslateResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'InvokePluginTranslateResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'translated')
..hasRequiredFields = false
;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
InvokePluginTranslateResponse clone() => InvokePluginTranslateResponse()..mergeFromMessage(this);
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
InvokePluginTranslateResponse copyWith(void Function(InvokePluginTranslateResponse) updates) => super.copyWith((message) => updates(message as InvokePluginTranslateResponse)) as InvokePluginTranslateResponse;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static InvokePluginTranslateResponse create() => InvokePluginTranslateResponse._();
InvokePluginTranslateResponse createEmptyInstance() => create();
static $pb.PbList<InvokePluginTranslateResponse> createRepeated() => $pb.PbList<InvokePluginTranslateResponse>();
@$core.pragma('dart2js:noInline')
static InvokePluginTranslateResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<InvokePluginTranslateResponse>(create);
static InvokePluginTranslateResponse? _defaultInstance;
/// Translated text. Returns verbatim what the plugin
/// emits no normalisation by the hub.
@$pb.TagNumber(1)
$core.String get translated => $_getSZ(0);
@$pb.TagNumber(1)
set translated($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasTranslated() => $_has(0);
@$pb.TagNumber(1)
void clearTranslated() => $_clearField(1);
}
class InvokePluginThemeResponse extends $pb.GeneratedMessage {
factory InvokePluginThemeResponse({
$core.String? brightness,
$core.Iterable<$core.int>? tokens,
}) {
final $result = create();
if (brightness != null) {
$result.brightness = brightness;
}
if (tokens != null) {
$result.tokens.addAll(tokens);
}
return $result;
}
InvokePluginThemeResponse._() : super();
factory InvokePluginThemeResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory InvokePluginThemeResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'InvokePluginThemeResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'brightness')
..p<$core.int>(2, _omitFieldNames ? '' : 'tokens', $pb.PbFieldType.KU3)
..hasRequiredFields = false
;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
InvokePluginThemeResponse clone() => InvokePluginThemeResponse()..mergeFromMessage(this);
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
InvokePluginThemeResponse copyWith(void Function(InvokePluginThemeResponse) updates) => super.copyWith((message) => updates(message as InvokePluginThemeResponse)) as InvokePluginThemeResponse;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static InvokePluginThemeResponse create() => InvokePluginThemeResponse._();
InvokePluginThemeResponse createEmptyInstance() => create();
static $pb.PbList<InvokePluginThemeResponse> createRepeated() => $pb.PbList<InvokePluginThemeResponse>();
@$core.pragma('dart2js:noInline')
static InvokePluginThemeResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<InvokePluginThemeResponse>(create);
static InvokePluginThemeResponse? _defaultInstance;
/// Echo of the brightness input. Plugins are expected to
/// round-trip it; if they don't the client treats that as
/// a contract violation.
@$pb.TagNumber(1)
$core.String get brightness => $_getSZ(0);
@$pb.TagNumber(1)
set brightness($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasBrightness() => $_has(0);
@$pb.TagNumber(1)
void clearBrightness() => $_clearField(1);
/// 14 ARGB tokens in Material 3 order:
/// primary, on_primary, secondary, on_secondary,
/// tertiary, on_tertiary, error, on_error,
/// surface, on_surface, surface_variant,
/// on_surface_variant, outline, outline_variant.
/// Studio merges any missing slot (well-behaved plugins
/// send all 14) with its built-in fallback before
/// applying the scheme.
@$pb.TagNumber(2)
$pb.PbList<$core.int> get tokens => $_getList(1);
}
class ListApprovalsRequest extends $pb.GeneratedMessage { class ListApprovalsRequest extends $pb.GeneratedMessage {
factory ListApprovalsRequest({ factory ListApprovalsRequest({
$core.Iterable<$core.String>? statuses, $core.Iterable<$core.String>? statuses,

View file

@ -175,6 +175,18 @@ class HubAdminClient extends $grpc.Client {
'/fai.v1.HubAdmin/FetchModuleDocs', '/fai.v1.HubAdmin/FetchModuleDocs',
($0.FetchModuleDocsRequest value) => value.writeToBuffer(), ($0.FetchModuleDocsRequest value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.FetchModuleDocsResponse.fromBuffer(value)); ($core.List<$core.int> value) => $0.FetchModuleDocsResponse.fromBuffer(value));
static final _$getInstalledModuleDocs = $grpc.ClientMethod<$0.GetInstalledModuleDocsRequest, $0.GetInstalledModuleDocsResponse>(
'/fai.v1.HubAdmin/GetInstalledModuleDocs',
($0.GetInstalledModuleDocsRequest value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.GetInstalledModuleDocsResponse.fromBuffer(value));
static final _$invokePluginTheme = $grpc.ClientMethod<$0.InvokePluginThemeRequest, $0.InvokePluginThemeResponse>(
'/fai.v1.HubAdmin/InvokePluginTheme',
($0.InvokePluginThemeRequest value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.InvokePluginThemeResponse.fromBuffer(value));
static final _$invokePluginTranslate = $grpc.ClientMethod<$0.InvokePluginTranslateRequest, $0.InvokePluginTranslateResponse>(
'/fai.v1.HubAdmin/InvokePluginTranslate',
($0.InvokePluginTranslateRequest value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.InvokePluginTranslateResponse.fromBuffer(value));
static final _$listApprovals = $grpc.ClientMethod<$0.ListApprovalsRequest, $0.ApprovalList>( static final _$listApprovals = $grpc.ClientMethod<$0.ListApprovalsRequest, $0.ApprovalList>(
'/fai.v1.HubAdmin/ListApprovals', '/fai.v1.HubAdmin/ListApprovals',
($0.ListApprovalsRequest value) => value.writeToBuffer(), ($0.ListApprovalsRequest value) => value.writeToBuffer(),
@ -356,6 +368,37 @@ class HubAdminClient extends $grpc.Client {
return $createUnaryCall(_$fetchModuleDocs, request, options: options); return $createUnaryCall(_$fetchModuleDocs, request, options: options);
} }
/// Read the inline-docs (MODULE.md / MODULE.<locale>.md) of an
/// installed module from disk. No network fetch, no provider-
/// specific URL probing the docs ship in the .fai bundle and
/// live next to module.wasm on disk after install. Returns an
/// empty body when the module is installed but ships no inline
/// docs; Studio uses that to hide the docs button entirely.
$grpc.ResponseFuture<$0.GetInstalledModuleDocsResponse> getInstalledModuleDocs($0.GetInstalledModuleDocsRequest request, {$grpc.CallOptions? options}) {
return $createUnaryCall(_$getInstalledModuleDocs, request, options: options);
}
/// Invoke an installed Studio plugin's `theme` hook. Returns
/// the plugin's proposed Material 3 ColorScheme for the
/// brightness the caller asked for ("light" or "dark"). The
/// first end-to-end RPC of the Studio-plugin subsystem; the
/// `translate` and `output-view` hooks get their own RPCs as
/// the matching Studio surfaces ship.
$grpc.ResponseFuture<$0.InvokePluginThemeResponse> invokePluginTheme($0.InvokePluginThemeRequest request, {$grpc.CallOptions? options}) {
return $createUnaryCall(_$invokePluginTheme, request, options: options);
}
/// Invoke an installed Studio plugin's `translate` hook.
/// The first user surface that consumes this is Studio's
/// FaiEnBadge when an operator has a translate plugin
/// installed, server-supplied English text (MCP tool
/// descriptions, n8n endpoint names) gets translated
/// on-the-fly into the active locale instead of carrying
/// the honest-but-rough [EN] badge.
$grpc.ResponseFuture<$0.InvokePluginTranslateResponse> invokePluginTranslate($0.InvokePluginTranslateRequest request, {$grpc.CallOptions? options}) {
return $createUnaryCall(_$invokePluginTranslate, request, options: options);
}
/// List system.approval@^0 entries, optionally filtered by status. /// List system.approval@^0 entries, optionally filtered by status.
$grpc.ResponseFuture<$0.ApprovalList> listApprovals($0.ListApprovalsRequest request, {$grpc.CallOptions? options}) { $grpc.ResponseFuture<$0.ApprovalList> listApprovals($0.ListApprovalsRequest request, {$grpc.CallOptions? options}) {
return $createUnaryCall(_$listApprovals, request, options: options); return $createUnaryCall(_$listApprovals, request, options: options);
@ -639,6 +682,27 @@ abstract class HubAdminServiceBase extends $grpc.Service {
false, false,
($core.List<$core.int> value) => $0.FetchModuleDocsRequest.fromBuffer(value), ($core.List<$core.int> value) => $0.FetchModuleDocsRequest.fromBuffer(value),
($0.FetchModuleDocsResponse value) => value.writeToBuffer())); ($0.FetchModuleDocsResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.GetInstalledModuleDocsRequest, $0.GetInstalledModuleDocsResponse>(
'GetInstalledModuleDocs',
getInstalledModuleDocs_Pre,
false,
false,
($core.List<$core.int> value) => $0.GetInstalledModuleDocsRequest.fromBuffer(value),
($0.GetInstalledModuleDocsResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.InvokePluginThemeRequest, $0.InvokePluginThemeResponse>(
'InvokePluginTheme',
invokePluginTheme_Pre,
false,
false,
($core.List<$core.int> value) => $0.InvokePluginThemeRequest.fromBuffer(value),
($0.InvokePluginThemeResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.InvokePluginTranslateRequest, $0.InvokePluginTranslateResponse>(
'InvokePluginTranslate',
invokePluginTranslate_Pre,
false,
false,
($core.List<$core.int> value) => $0.InvokePluginTranslateRequest.fromBuffer(value),
($0.InvokePluginTranslateResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.ListApprovalsRequest, $0.ApprovalList>( $addMethod($grpc.ServiceMethod<$0.ListApprovalsRequest, $0.ApprovalList>(
'ListApprovals', 'ListApprovals',
listApprovals_Pre, listApprovals_Pre,
@ -875,6 +939,18 @@ abstract class HubAdminServiceBase extends $grpc.Service {
return fetchModuleDocs($call, await $request); return fetchModuleDocs($call, await $request);
} }
$async.Future<$0.GetInstalledModuleDocsResponse> getInstalledModuleDocs_Pre($grpc.ServiceCall $call, $async.Future<$0.GetInstalledModuleDocsRequest> $request) async {
return getInstalledModuleDocs($call, await $request);
}
$async.Future<$0.InvokePluginThemeResponse> invokePluginTheme_Pre($grpc.ServiceCall $call, $async.Future<$0.InvokePluginThemeRequest> $request) async {
return invokePluginTheme($call, await $request);
}
$async.Future<$0.InvokePluginTranslateResponse> invokePluginTranslate_Pre($grpc.ServiceCall $call, $async.Future<$0.InvokePluginTranslateRequest> $request) async {
return invokePluginTranslate($call, await $request);
}
$async.Future<$0.ApprovalList> listApprovals_Pre($grpc.ServiceCall $call, $async.Future<$0.ListApprovalsRequest> $request) async { $async.Future<$0.ApprovalList> listApprovals_Pre($grpc.ServiceCall $call, $async.Future<$0.ListApprovalsRequest> $request) async {
return listApprovals($call, await $request); return listApprovals($call, await $request);
} }
@ -992,6 +1068,9 @@ abstract class HubAdminServiceBase extends $grpc.Service {
$async.Future<$0.InstallModuleResponse> installModule($grpc.ServiceCall call, $0.InstallModuleRequest request); $async.Future<$0.InstallModuleResponse> installModule($grpc.ServiceCall call, $0.InstallModuleRequest request);
$async.Future<$0.UninstallModuleResponse> uninstallModule($grpc.ServiceCall call, $0.UninstallModuleRequest request); $async.Future<$0.UninstallModuleResponse> uninstallModule($grpc.ServiceCall call, $0.UninstallModuleRequest request);
$async.Future<$0.FetchModuleDocsResponse> fetchModuleDocs($grpc.ServiceCall call, $0.FetchModuleDocsRequest request); $async.Future<$0.FetchModuleDocsResponse> fetchModuleDocs($grpc.ServiceCall call, $0.FetchModuleDocsRequest request);
$async.Future<$0.GetInstalledModuleDocsResponse> getInstalledModuleDocs($grpc.ServiceCall call, $0.GetInstalledModuleDocsRequest request);
$async.Future<$0.InvokePluginThemeResponse> invokePluginTheme($grpc.ServiceCall call, $0.InvokePluginThemeRequest request);
$async.Future<$0.InvokePluginTranslateResponse> invokePluginTranslate($grpc.ServiceCall call, $0.InvokePluginTranslateRequest request);
$async.Future<$0.ApprovalList> listApprovals($grpc.ServiceCall call, $0.ListApprovalsRequest 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.DecideApprovalResponse> decideApproval($grpc.ServiceCall call, $0.DecideApprovalRequest request);
$async.Future<$0.VerifyEventChainResponse> verifyEventChain($grpc.ServiceCall call, $1.Empty request); $async.Future<$0.VerifyEventChainResponse> verifyEventChain($grpc.ServiceCall call, $1.Empty request);

View file

@ -143,6 +143,7 @@ const CapabilityEntry$json = {
{'1': 'version', '3': 2, '4': 1, '5': 9, '10': 'version'}, {'1': 'version', '3': 2, '4': 1, '5': 9, '10': 'version'},
{'1': 'module_name', '3': 3, '4': 1, '5': 9, '10': 'moduleName'}, {'1': 'module_name', '3': 3, '4': 1, '5': 9, '10': 'moduleName'},
{'1': 'module_version', '3': 4, '4': 1, '5': 9, '10': 'moduleVersion'}, {'1': 'module_version', '3': 4, '4': 1, '5': 9, '10': 'moduleVersion'},
{'1': 'kind', '3': 5, '4': 1, '5': 9, '10': 'kind'},
], ],
}; };
@ -150,7 +151,8 @@ const CapabilityEntry$json = {
final $typed_data.Uint8List capabilityEntryDescriptor = $convert.base64Decode( final $typed_data.Uint8List capabilityEntryDescriptor = $convert.base64Decode(
'Cg9DYXBhYmlsaXR5RW50cnkSHgoKY2FwYWJpbGl0eRgBIAEoCVIKY2FwYWJpbGl0eRIYCgd2ZX' 'Cg9DYXBhYmlsaXR5RW50cnkSHgoKY2FwYWJpbGl0eRgBIAEoCVIKY2FwYWJpbGl0eRIYCgd2ZX'
'JzaW9uGAIgASgJUgd2ZXJzaW9uEh8KC21vZHVsZV9uYW1lGAMgASgJUgptb2R1bGVOYW1lEiUK' 'JzaW9uGAIgASgJUgd2ZXJzaW9uEh8KC21vZHVsZV9uYW1lGAMgASgJUgptb2R1bGVOYW1lEiUK'
'Dm1vZHVsZV92ZXJzaW9uGAQgASgJUg1tb2R1bGVWZXJzaW9u'); 'Dm1vZHVsZV92ZXJzaW9uGAQgASgJUg1tb2R1bGVWZXJzaW9uEhIKBGtpbmQYBSABKAlSBGtpbm'
'Q=');
@$core.Deprecated('Use moduleInfoRequestDescriptor instead') @$core.Deprecated('Use moduleInfoRequestDescriptor instead')
const ModuleInfoRequest$json = { const ModuleInfoRequest$json = {
@ -173,6 +175,7 @@ const ModuleInfoResponse$json = {
{'1': 'provides', '3': 3, '4': 3, '5': 11, '6': '.fai.v1.CapabilityRef', '10': 'provides'}, {'1': 'provides', '3': 3, '4': 3, '5': 11, '6': '.fai.v1.CapabilityRef', '10': 'provides'},
{'1': 'permissions', '3': 4, '4': 3, '5': 9, '10': 'permissions'}, {'1': 'permissions', '3': 4, '4': 3, '5': 9, '10': 'permissions'},
{'1': 'directory', '3': 5, '4': 1, '5': 9, '10': 'directory'}, {'1': 'directory', '3': 5, '4': 1, '5': 9, '10': 'directory'},
{'1': 'accepts_mime', '3': 6, '4': 3, '5': 9, '10': 'acceptsMime'},
], ],
}; };
@ -181,7 +184,7 @@ final $typed_data.Uint8List moduleInfoResponseDescriptor = $convert.base64Decode
'ChJNb2R1bGVJbmZvUmVzcG9uc2USEgoEbmFtZRgBIAEoCVIEbmFtZRIYCgd2ZXJzaW9uGAIgAS' 'ChJNb2R1bGVJbmZvUmVzcG9uc2USEgoEbmFtZRgBIAEoCVIEbmFtZRIYCgd2ZXJzaW9uGAIgAS'
'gJUgd2ZXJzaW9uEjEKCHByb3ZpZGVzGAMgAygLMhUuZmFpLnYxLkNhcGFiaWxpdHlSZWZSCHBy' 'gJUgd2ZXJzaW9uEjEKCHByb3ZpZGVzGAMgAygLMhUuZmFpLnYxLkNhcGFiaWxpdHlSZWZSCHBy'
'b3ZpZGVzEiAKC3Blcm1pc3Npb25zGAQgAygJUgtwZXJtaXNzaW9ucxIcCglkaXJlY3RvcnkYBS' 'b3ZpZGVzEiAKC3Blcm1pc3Npb25zGAQgAygJUgtwZXJtaXNzaW9ucxIcCglkaXJlY3RvcnkYBS'
'ABKAlSCWRpcmVjdG9yeQ=='); 'ABKAlSCWRpcmVjdG9yeRIhCgxhY2NlcHRzX21pbWUYBiADKAlSC2FjY2VwdHNNaW1l');
@$core.Deprecated('Use checkPermissionRequestDescriptor instead') @$core.Deprecated('Use checkPermissionRequestDescriptor instead')
const CheckPermissionRequest$json = { const CheckPermissionRequest$json = {
@ -532,6 +535,94 @@ final $typed_data.Uint8List fetchModuleDocsResponseDescriptor = $convert.base64D
'ChdGZXRjaE1vZHVsZURvY3NSZXNwb25zZRIdCgplcnJvcl9raW5kGAEgASgJUgllcnJvcktpbm' 'ChdGZXRjaE1vZHVsZURvY3NSZXNwb25zZRIdCgplcnJvcl9raW5kGAEgASgJUgllcnJvcktpbm'
'QSEgoEdGV4dBgCIAEoCVIEdGV4dBIdCgpzb3VyY2VfdXJsGAMgASgJUglzb3VyY2VVcmw='); 'QSEgoEdGV4dBgCIAEoCVIEdGV4dBIdCgpzb3VyY2VfdXJsGAMgASgJUglzb3VyY2VVcmw=');
@$core.Deprecated('Use getInstalledModuleDocsRequestDescriptor instead')
const GetInstalledModuleDocsRequest$json = {
'1': 'GetInstalledModuleDocsRequest',
'2': [
{'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'},
{'1': 'locale', '3': 2, '4': 1, '5': 9, '10': 'locale'},
],
};
/// Descriptor for `GetInstalledModuleDocsRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List getInstalledModuleDocsRequestDescriptor = $convert.base64Decode(
'Ch1HZXRJbnN0YWxsZWRNb2R1bGVEb2NzUmVxdWVzdBISCgRuYW1lGAEgASgJUgRuYW1lEhYKBm'
'xvY2FsZRgCIAEoCVIGbG9jYWxl');
@$core.Deprecated('Use getInstalledModuleDocsResponseDescriptor instead')
const GetInstalledModuleDocsResponse$json = {
'1': 'GetInstalledModuleDocsResponse',
'2': [
{'1': 'text', '3': 1, '4': 1, '5': 9, '10': 'text'},
{'1': 'source_path', '3': 2, '4': 1, '5': 9, '10': 'sourcePath'},
{'1': 'not_installed', '3': 3, '4': 1, '5': 8, '10': 'notInstalled'},
],
};
/// Descriptor for `GetInstalledModuleDocsResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List getInstalledModuleDocsResponseDescriptor = $convert.base64Decode(
'Ch5HZXRJbnN0YWxsZWRNb2R1bGVEb2NzUmVzcG9uc2USEgoEdGV4dBgBIAEoCVIEdGV4dBIfCg'
'tzb3VyY2VfcGF0aBgCIAEoCVIKc291cmNlUGF0aBIjCg1ub3RfaW5zdGFsbGVkGAMgASgIUgxu'
'b3RJbnN0YWxsZWQ=');
@$core.Deprecated('Use invokePluginThemeRequestDescriptor instead')
const InvokePluginThemeRequest$json = {
'1': 'InvokePluginThemeRequest',
'2': [
{'1': 'capability', '3': 1, '4': 1, '5': 9, '10': 'capability'},
{'1': 'brightness', '3': 2, '4': 1, '5': 9, '10': 'brightness'},
],
};
/// Descriptor for `InvokePluginThemeRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List invokePluginThemeRequestDescriptor = $convert.base64Decode(
'ChhJbnZva2VQbHVnaW5UaGVtZVJlcXVlc3QSHgoKY2FwYWJpbGl0eRgBIAEoCVIKY2FwYWJpbG'
'l0eRIeCgpicmlnaHRuZXNzGAIgASgJUgpicmlnaHRuZXNz');
@$core.Deprecated('Use invokePluginTranslateRequestDescriptor instead')
const InvokePluginTranslateRequest$json = {
'1': 'InvokePluginTranslateRequest',
'2': [
{'1': 'capability', '3': 1, '4': 1, '5': 9, '10': 'capability'},
{'1': 'text', '3': 2, '4': 1, '5': 9, '10': 'text'},
{'1': 'from_locale', '3': 3, '4': 1, '5': 9, '10': 'fromLocale'},
{'1': 'to_locale', '3': 4, '4': 1, '5': 9, '10': 'toLocale'},
],
};
/// Descriptor for `InvokePluginTranslateRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List invokePluginTranslateRequestDescriptor = $convert.base64Decode(
'ChxJbnZva2VQbHVnaW5UcmFuc2xhdGVSZXF1ZXN0Eh4KCmNhcGFiaWxpdHkYASABKAlSCmNhcG'
'FiaWxpdHkSEgoEdGV4dBgCIAEoCVIEdGV4dBIfCgtmcm9tX2xvY2FsZRgDIAEoCVIKZnJvbUxv'
'Y2FsZRIbCgl0b19sb2NhbGUYBCABKAlSCHRvTG9jYWxl');
@$core.Deprecated('Use invokePluginTranslateResponseDescriptor instead')
const InvokePluginTranslateResponse$json = {
'1': 'InvokePluginTranslateResponse',
'2': [
{'1': 'translated', '3': 1, '4': 1, '5': 9, '10': 'translated'},
],
};
/// Descriptor for `InvokePluginTranslateResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List invokePluginTranslateResponseDescriptor = $convert.base64Decode(
'Ch1JbnZva2VQbHVnaW5UcmFuc2xhdGVSZXNwb25zZRIeCgp0cmFuc2xhdGVkGAEgASgJUgp0cm'
'Fuc2xhdGVk');
@$core.Deprecated('Use invokePluginThemeResponseDescriptor instead')
const InvokePluginThemeResponse$json = {
'1': 'InvokePluginThemeResponse',
'2': [
{'1': 'brightness', '3': 1, '4': 1, '5': 9, '10': 'brightness'},
{'1': 'tokens', '3': 2, '4': 3, '5': 13, '10': 'tokens'},
],
};
/// Descriptor for `InvokePluginThemeResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List invokePluginThemeResponseDescriptor = $convert.base64Decode(
'ChlJbnZva2VQbHVnaW5UaGVtZVJlc3BvbnNlEh4KCmJyaWdodG5lc3MYASABKAlSCmJyaWdodG'
'5lc3MSFgoGdG9rZW5zGAIgAygNUgZ0b2tlbnM=');
@$core.Deprecated('Use listApprovalsRequestDescriptor instead') @$core.Deprecated('Use listApprovalsRequestDescriptor instead')
const ListApprovalsRequest$json = { const ListApprovalsRequest$json = {
'1': 'ListApprovalsRequest', '1': 'ListApprovalsRequest',

View file

@ -6,8 +6,10 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:grpc/grpc.dart'; import 'package:grpc/grpc_connection_interface.dart';
import 'channel_factory_io.dart'
if (dart.library.html) 'channel_factory_web.dart' as channel_factory;
import 'generated/fai/v1/common.pb.dart' as pb_common; import 'generated/fai/v1/common.pb.dart' as pb_common;
import 'generated/fai/v1/hub.pb.dart' as pb; import 'generated/fai/v1/hub.pb.dart' as pb;
import 'generated/fai/v1/hub.pbgrpc.dart' as grpc_hub; import 'generated/fai/v1/hub.pbgrpc.dart' as grpc_hub;
@ -59,6 +61,9 @@ typedef ClearEventLogResponse = pb.ClearEventLogResponse;
typedef DaemonPathsResponse = pb.DaemonPathsResponse; typedef DaemonPathsResponse = pb.DaemonPathsResponse;
typedef UninstallModuleResponse = pb.UninstallModuleResponse; typedef UninstallModuleResponse = pb.UninstallModuleResponse;
typedef FetchModuleDocsResponse = pb.FetchModuleDocsResponse; typedef FetchModuleDocsResponse = pb.FetchModuleDocsResponse;
typedef GetInstalledModuleDocsResponse = pb.GetInstalledModuleDocsResponse;
typedef InvokePluginThemeResponse = pb.InvokePluginThemeResponse;
typedef InvokePluginTranslateResponse = pb.InvokePluginTranslateResponse;
typedef ListMcpClientsResponse = pb.ListMcpClientsResponse; typedef ListMcpClientsResponse = pb.ListMcpClientsResponse;
typedef McpClientStatus = pb.McpClientStatus; typedef McpClientStatus = pb.McpClientStatus;
typedef McpClientConfigEntry = pb.McpClientConfigEntry; typedef McpClientConfigEntry = pb.McpClientConfigEntry;
@ -75,9 +80,15 @@ typedef SubmitResponse = pb.SubmitResponse;
/// Typed client for the FI Hub gRPC surface. Construct once, /// Typed client for the FI Hub gRPC surface. Construct once,
/// reuse for the process lifetime, call [close] on shutdown. /// reuse for the process lifetime, call [close] on shutdown.
///
/// The channel type is picked at compile time:
/// * native targets plain HTTP/2 gRPC via [ClientChannel].
/// * web targets HTTP/1.1 gRPC-Web via [GrpcWebClientChannel].
/// Requires the hub to be running with the
/// `tonic-web` layer enabled.
class HubClient { class HubClient {
final HubEndpoint endpoint; final HubEndpoint endpoint;
final ClientChannel _channel; final ClientChannelBase _channel;
final grpc_hub.HubClient _hub; final grpc_hub.HubClient _hub;
final grpc_hub.HubAdminClient _admin; final grpc_hub.HubAdminClient _admin;
@ -86,17 +97,7 @@ class HubClient {
_admin = grpc_hub.HubAdminClient(_channel); _admin = grpc_hub.HubAdminClient(_channel);
factory HubClient({HubEndpoint endpoint = const HubEndpoint()}) { factory HubClient({HubEndpoint endpoint = const HubEndpoint()}) {
final channel = ClientChannel( return HubClient._(endpoint, channel_factory.createChannel(endpoint));
endpoint.host,
port: endpoint.port,
options: ChannelOptions(
credentials: endpoint.secure
? const ChannelCredentials.secure()
: const ChannelCredentials.insecure(),
idleTimeout: const Duration(minutes: 5),
),
);
return HubClient._(endpoint, channel);
} }
/// Liveness probe. Returns true when the hub responds with /// Liveness probe. Returns true when the hub responds with
@ -240,6 +241,10 @@ class HubClient {
/// when set, so Studio doesn't have to handle credentials. /// when set, so Studio doesn't have to handle credentials.
/// Errors come back inside [FetchModuleDocsResponse.errorKind] /// Errors come back inside [FetchModuleDocsResponse.errorKind]
/// (not as exceptions) so the UI can render fallbacks. /// (not as exceptions) so the UI can render fallbacks.
///
/// Deprecated: prefer [getInstalledModuleDocs], which reads
/// `MODULE.md`/`MODULE.<locale>.md` shipped inside the bundle.
@Deprecated('Use getInstalledModuleDocs (reads from bundle, no network)')
Future<FetchModuleDocsResponse> fetchModuleDocs( Future<FetchModuleDocsResponse> fetchModuleDocs(
String name, { String name, {
String locale = '', String locale = '',
@ -249,6 +254,80 @@ class HubClient {
); );
} }
/// Read the installed module's `MODULE.md` (or `MODULE.<locale>.md`)
/// from disk under `~/.fai/modules/<module>/`. No network. Works in
/// air-gapped installs. The response distinguishes three states via
/// [GetInstalledModuleDocsResponse.notInstalled] and an empty
/// [GetInstalledModuleDocsResponse.text]:
///
/// - text non-empty docs found, render them
/// - text empty + notInstalled=false installed but bundle had
/// no inline docs
/// - text empty + notInstalled=true module is not installed
Future<GetInstalledModuleDocsResponse> getInstalledModuleDocs(
String name, {
String locale = '',
}) {
return _admin.getInstalledModuleDocs(
pb.GetInstalledModuleDocsRequest(name: name, locale: locale),
);
}
/// Invoke an installed Studio plugin's `theme` hook and
/// receive a Material 3 ColorScheme for the brightness
/// ("light" or "dark") the caller asked for.
///
/// [capability] is the full Studio-plugin capability name
/// (e.g. "studio.theme.solarized"). [brightness] must be
/// "light" or "dark"; other values surface as
/// `INVALID_ARGUMENT` from the hub. A plugin that doesn't
/// recognise the brightness returns Declined, which the
/// hub maps to `FAILED_PRECONDITION` Studio's
/// friendly-error mapper renders it with a recovery hint.
///
/// The first end-to-end RPC of the Studio-plugin
/// subsystem; `translate` and `output-view` hooks get
/// their own methods once the matching Studio surfaces
/// ship.
Future<InvokePluginThemeResponse> invokePluginTheme({
required String capability,
required String brightness,
}) {
return _admin.invokePluginTheme(
pb.InvokePluginThemeRequest(
capability: capability,
brightness: brightness,
),
);
}
/// Invoke an installed Studio plugin's `translate` hook and
/// receive translated text. [fromLocale] may be empty
/// (auto-detect); [toLocale] must be a BCP-47 code (the
/// host always knows the target).
///
/// Errors surface as gRPC exceptions:
/// * NOT_FOUND plugin not installed.
/// * FAILED_PRECONDITION plugin returned Declined or
/// Misconfigured (e.g. unreachable
/// LLM endpoint).
/// * INTERNAL generic catch-all.
Future<InvokePluginTranslateResponse> invokePluginTranslate({
required String capability,
required String text,
required String toLocale,
String fromLocale = '',
}) {
return _admin.invokePluginTranslate(
pb.InvokePluginTranslateRequest(
capability: capability,
text: text,
fromLocale: fromLocale,
toLocale: toLocale,
),
);
}
/// List every configured MCP server with the last-discovery /// List every configured MCP server with the last-discovery
/// snapshot. Drives Studio's MCP-clients editor. /// snapshot. Drives Studio's MCP-clients editor.
Future<ListMcpClientsResponse> listMcpClients() { Future<ListMcpClientsResponse> listMcpClients() {
@ -489,27 +568,33 @@ class HubClient {
/// flow that mixes text and binary inputs (e.g. extract /// flow that mixes text and binary inputs (e.g. extract
/// taking a `document: bytes`) flows through one RPC: /// taking a `document: bytes`) flows through one RPC:
/// * [textInputs] string values wrapped as text Payloads /// * [textInputs] string values wrapped as text Payloads
/// * [fileInputs] raw bytes wrapped as bytes Payloads, /// * [fileInputs] raw bytes wrapped as bytes Payloads.
/// with `application/octet-stream` MIME /// MIME type defaults to
/// type. Caller can override the MIME by /// `application/octet-stream`; clients that
/// passing a [bytePayloads] entry instead. /// know the real type pass it via
/// Both maps default to empty so existing text-only callers /// [fileMimeTypes] (same key). Modules
/// typically gate on MIME e.g.
/// `text.extract` rejects octet-stream so
/// accurate types matter.
/// All maps default to empty so existing text-only callers
/// are backward-compatible. Keys must not collide between /// are backward-compatible. Keys must not collide between
/// the maps; on collision the bytes-shaped entry wins (the /// [textInputs] and [fileInputs]; on collision the bytes-shaped
/// less-common case is more likely to be the operator's /// entry wins (the less-common case is more likely to be the
/// intent). /// operator's intent).
Future<SubmitResponse> runSavedFlow({ Future<SubmitResponse> runSavedFlow({
required String name, required String name,
Map<String, String> textInputs = const {}, Map<String, String> textInputs = const {},
Map<String, Uint8List> fileInputs = const {}, Map<String, Uint8List> fileInputs = const {},
Map<String, String> fileMimeTypes = const {},
}) async { }) async {
final req = pb.RunSavedFlowRequest()..name = name; final req = pb.RunSavedFlowRequest()..name = name;
for (final entry in textInputs.entries) { for (final entry in textInputs.entries) {
req.inputs[entry.key] = pb_common.Payload()..text = entry.value; req.inputs[entry.key] = pb_common.Payload()..text = entry.value;
} }
for (final entry in fileInputs.entries) { for (final entry in fileInputs.entries) {
final mime = fileMimeTypes[entry.key] ?? 'application/octet-stream';
final bytes = pb_common.Bytes() final bytes = pb_common.Bytes()
..mimeType = 'application/octet-stream' ..mimeType = mime
..data = entry.value; ..data = entry.value;
req.inputs[entry.key] = pb_common.Payload()..bytes = bytes; req.inputs[entry.key] = pb_common.Payload()..bytes = bytes;
} }