mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 07:33:06 +08:00
纯宿主侧改动,向后兼容。 - 契约(pkg/pluginapi/v1/plugin.proto,已重新生成):ListAccounts 增补 repeated AccountInfo accounts(保留 account_ids 向后兼容),新增 metadata_json 只读快照字段。HostServiceAPIVersion 1→2(InitHostServices 运行时协商,加法式, 旧调用方忽略新字段即可)。 - 权限边界:可见范围由清单能力经 pluginCapabilityAccountScopeGrants 推导, ListAccounts 与 ResolveOutboundIdentity 均以该范围为权威边界。 - metadata_json 采用 denylist:剥离 Credentials 与含反向引用的关联对象 Groups/AccountGroups(避免 encoding/json 循环引用),其余字段自动透传, 数据模型新增非机密字段无需改宿主。 测试:范围/元数据/无凭据泄漏用例、denylist 反射护栏 + 循环引用安全断言、纵深过滤用例; build/vet/gofmt、-tags unit 与 -race 全绿。
275 lines
8.9 KiB
Protocol Buffer
275 lines
8.9 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
|
|
// (with full readable metadata) and resolve the outbound identity
|
|
// (credentials/headers/proxy) the host would attach to a live request. The
|
|
// host restricts both calls to the account scope the requesting plugin's
|
|
// declared capabilities cover; a plugin can never widen that scope. Metadata
|
|
// never carries raw credentials — those are only handed out by
|
|
// ResolveOutboundIdentity, the dedicated (equally sensitive) credential channel.
|
|
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 {
|
|
// account_ids is retained for plugins built against host_service_api_version 1.
|
|
// The host keeps filling it (the ids of the accounts in `accounts`) so those
|
|
// plugins keep working; new plugins should read `accounts` instead.
|
|
repeated int64 account_ids = 1;
|
|
// accounts carries the full readable metadata for every account in the
|
|
// plugin's scope — including active accounts that are currently NOT
|
|
// schedulable because they are paused (rate-limited / temp-unschedulable /
|
|
// overloaded). This lets a plugin make its own decisions (e.g. skip paused
|
|
// accounts) instead of hammering them. (Administratively disabled / expired
|
|
// accounts are already excluded upstream by the host.) Available when
|
|
// host_service_api_version >= 2.
|
|
repeated AccountInfo accounts = 2;
|
|
}
|
|
|
|
// AccountInfo is the host's readable, non-secret view of one account. It never
|
|
// contains credentials (token / refresh_token / cookies); use
|
|
// ResolveOutboundIdentity for those.
|
|
//
|
|
// The typed fields are a small, STABLE decision core. Everything else about the
|
|
// account — including any field added to the account model later — is carried in
|
|
// metadata_json, so new fields reach plugins WITHOUT a contract change, a host
|
|
// mapper edit, or a plugin rebuild. Only a newly added *secret* field would ever
|
|
// require a host change (a one-line denylist entry), which is the safe default.
|
|
message AccountInfo {
|
|
int64 id = 1;
|
|
string platform = 2;
|
|
string account_type = 3;
|
|
string name = 4;
|
|
// status is the coarse lifecycle state: active / disabled / error / expired.
|
|
string status = 5;
|
|
// schedulable is the host-authoritative decision (IsSchedulable) — false when
|
|
// the account is active but paused for ANY reason (rate limit,
|
|
// temp-unschedulable, overload, expiry auto-pause, quota). This is the primary
|
|
// signal a plugin should use to skip an account so it stops probing paused ones.
|
|
bool schedulable = 6;
|
|
bool is_shadow = 7;
|
|
// metadata_json is a host-produced JSON object holding the account's readable
|
|
// field set (status timestamps, rate-limit / overload / temp-unschedulable
|
|
// windows and reason, last-used, expiry, session window, group ids, priority,
|
|
// concurrency, quota dimension, extra config, proxy, and any future field).
|
|
// Keys are the account model's field names. Times are RFC3339 strings (JSON
|
|
// null when unset). Only two things are withheld: the raw Credentials blob
|
|
// (the refresh_token — a long-lived secret the outbound-identity RPC does not
|
|
// hand out; the short-lived access token and proxy URL come from that RPC), and
|
|
// the Groups/AccountGroups relation graphs (cyclic — group_ids conveys
|
|
// membership). Plugins read whatever keys they need; the host does not have to
|
|
// change when the account model gains a field.
|
|
bytes metadata_json = 20;
|
|
}
|
|
|
|
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;
|
|
}
|