Files
sub2api/backend/pkg/pluginapi/v1/plugin.proto
T
shaw c63bd14a02 feat(plugin): generic host services + read-only status bridge channel
Extend the local plugin system's host<->plugin contract with generic,
capability-neutral facilities, so transport plugins can be built on top
without embedding plugin-specific logic in the host.

Host services (plugin dials a broker-provided HostService gRPC):
- per-plugin namespaced KV store (key/value size and TTL bounded)
- account directory: ListAccounts + ResolveOutboundIdentity, restricted to
  the accounts a plugin's declared capability already covers
- InitHostServices lifecycle hook; plugins that don't implement it run
  unchanged and are simply offered no host services

Read-only runtime status channel:
- HealthResponse gains an opaque, plugin-defined status_json produced
  passively (no config apply, no upstream call, no side effects)
- GET /admin/plugins/:id/status (ungated) and the plugin.status UI-bridge
  verb map to Health, so a status UI can poll live state without a step-up
  prompt or a "test" side effect
- config.test no longer force-shows a host success toast; the plugin UI owns
  result presentation, so plugins that read status do not spam toasts

Document the UI-bridge verb taxonomy (read vs mutate/test gating) and the
status contract under pkg/pluginapi.
2026-09-19 11:25:56 +08:00

225 lines
6.0 KiB
Protocol Buffer

syntax = "proto3";
package sub2api.plugin.v1;
option go_package = "github.com/Wei-Shaw/sub2api/pkg/pluginapi/v1;pluginv1";
service TransportPlugin {
rpc GetInfo(GetInfoRequest) returns (GetInfoResponse);
rpc Health(HealthRequest) returns (HealthResponse);
rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse);
rpc ApplyConfig(ApplyConfigRequest) returns (ApplyConfigResponse);
rpc TestConfig(TestConfigRequest) returns (TestConfigResponse);
rpc Forward(stream ForwardRequest) returns (stream ForwardResponse);
// InitHostServices hands the plugin a go-plugin broker stream id it can dial
// back to reach the host-provided HostService. It is an optional, generic
// lifecycle hook: plugins built against an older contract leave it
// Unimplemented and the host simply runs without offering host services.
rpc InitHostServices(InitHostServicesRequest) returns (InitHostServicesResponse);
}
// HostService is implemented by the host and dialed by the plugin over the
// go-plugin broker id delivered via InitHostServices. It exposes generic,
// capability-neutral facilities that any plugin may use; new facilities are
// added here as new RPCs without changing the transport contract version.
service HostService {
rpc KVGet(KVGetRequest) returns (KVGetResponse);
rpc KVSet(KVSetRequest) returns (KVSetResponse);
rpc KVDelete(KVDeleteRequest) returns (KVDeleteResponse);
rpc KVList(KVListRequest) returns (KVListResponse);
// Account directory: enumerate the accounts the plugin is bound to act on and
// resolve the outbound identity (credentials/headers/proxy) the host would
// attach to a live request. The host restricts both calls to accounts the
// requesting plugin's capability already covers.
rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse);
rpc ResolveOutboundIdentity(ResolveOutboundIdentityRequest) returns (ResolveOutboundIdentityResponse);
}
message GetInfoRequest {}
message GetInfoResponse {
string plugin_id = 1;
string plugin_version = 2;
uint32 protocol_version = 3;
uint32 transport_api_version = 4;
repeated string capabilities = 5;
}
message HealthRequest {}
message HealthResponse {
bool healthy = 1;
string message = 2;
// status_json is an optional plugin-defined JSON blob describing current runtime
// state (e.g. a status summary for the config UI). It MUST be produced passively
// — reading it must not apply config, reach upstream, or cause side effects — so
// the host can expose it via a lightweight, ungated status endpoint. Ignored by
// the host's health gating.
string status_json = 3;
}
message ValidateConfigRequest {
bytes config_json = 1;
}
message ValidateConfigResponse {
bool valid = 1;
string message = 2;
bytes normalized_config_json = 3;
}
message ApplyConfigRequest {
bytes config_json = 1;
}
message ApplyConfigResponse {
bool applied = 1;
string message = 2;
}
message TestConfigRequest {
bytes config_json = 1;
}
message TestConfigResponse {
bool success = 1;
string message = 2;
int64 latency_ms = 3;
// status_json is an optional plugin-defined JSON blob surfaced to the config UI
// alongside the test result (e.g. a runtime status summary). Ignored by the host.
string status_json = 4;
}
message HeaderValues {
repeated string values = 1;
}
message ForwardRequestStart {
string request_id = 1;
string method = 2;
string url = 3;
string host = 4;
map<string, HeaderValues> headers = 5;
string proxy_url = 6;
int64 account_id = 7;
int32 account_concurrency = 8;
string platform = 9;
string account_type = 10;
int64 content_length = 11;
bool has_body = 12;
}
message ForwardRequest {
oneof frame {
ForwardRequestStart start = 1;
bytes body_chunk = 2;
bool body_end = 3;
}
}
message ForwardResponseStart {
int32 status_code = 1;
string status = 2;
string protocol = 3;
int32 protocol_major = 4;
int32 protocol_minor = 5;
map<string, HeaderValues> headers = 6;
int64 content_length = 7;
}
message ForwardResponseEnd {
int64 bytes_received = 1;
int64 duration_ms = 2;
}
message ForwardResponseError {
string code = 1;
string message = 2;
bool request_sent = 3;
}
message ForwardResponse {
oneof frame {
ForwardResponseStart start = 1;
bytes body_chunk = 2;
ForwardResponseEnd end = 3;
ForwardResponseError error = 4;
}
}
message InitHostServicesRequest {
// host_service_id is the broker stream id the plugin dials to reach HostService.
uint32 host_service_id = 1;
// host_service_api_version lets the plugin reject a host it cannot talk to.
uint32 host_service_api_version = 2;
}
message InitHostServicesResponse {
// ready is true once the plugin has successfully dialed back the host service.
bool ready = 1;
string message = 2;
}
// KV messages back a namespaced, per-plugin persistent key-value store. The
// host derives the owning plugin from the runtime that serves the connection,
// so a plugin can never read or write another plugin's namespace.
message KVGetRequest {
string namespace = 1;
string key = 2;
}
message KVGetResponse {
bool found = 1;
bytes value = 2;
}
message KVSetRequest {
string namespace = 1;
string key = 2;
bytes value = 3;
// ttl_seconds > 0 sets an expiry; 0 stores the entry without expiry.
int64 ttl_seconds = 4;
}
message KVSetResponse {}
message KVDeleteRequest {
string namespace = 1;
string key = 2;
}
message KVDeleteResponse {}
message KVListRequest {
string namespace = 1;
string key_prefix = 2;
int32 limit = 3;
}
message KVListResponse {
repeated string keys = 1;
}
message ListAccountsRequest {
string platform = 1;
string account_type = 2;
}
message ListAccountsResponse {
repeated int64 account_ids = 1;
}
message ResolveOutboundIdentityRequest {
int64 account_id = 1;
}
message ResolveOutboundIdentityResponse {
bool found = 1;
int64 account_id = 2;
string platform = 3;
string account_type = 4;
string proxy_url = 5;
string token = 6;
map<string, HeaderValues> headers = 7;
}