cli/proto/orchestrator/v1/plugin_registry.proto
Alex Dunmow 2f065e3ed4 feat: ninja CLI in its own repo (block/cli, WO-WZ-023)
The ninja developer CLI moves out of block/core into its own module
git.dev.alexdunmow.com/block/cli. Pins block/core@v0.18.2 for abi/v1, the
plugin.mod parser, and DESCRIBE guest builds; vendors its own orchestrator
registry client (internal/api, generated from a vendored plugin_registry.proto)
since it cannot import block/core/internal. Produces byte-identical v1
artifacts (verified: art-deco codeless SHA256-identical to the pre-move build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 22:39:32 +08:00

457 lines
15 KiB
Protocol Buffer

syntax = "proto3";
package orchestrator.v1;
import "google/protobuf/timestamp.proto";
option go_package = "git.dev.alexdunmow.com/block/cms/orchestrator/internal/api/orchestrator/v1;orchestratorv1";
// PluginScopeService manages @scope namespaces.
service PluginScopeService {
rpc CreateScope(CreateScopeRequest) returns (CreateScopeResponse);
rpc ListMyScopes(ListMyScopesRequest) returns (ListMyScopesResponse);
rpc GetScope(GetScopeRequest) returns (GetScopeResponse);
rpc ListMyPlugins(ListMyPluginsRequest) returns (ListMyPluginsResponse);
}
// PluginRegistryService is the read-side public API plus CreatePlugin.
service PluginRegistryService {
rpc CreatePlugin(CreatePluginRequest) returns (CreatePluginResponse);
rpc GetPlugin(GetPluginRequest) returns (GetPluginResponse);
rpc ListPlugins(ListPluginsRequest) returns (ListPluginsResponse);
rpc GetVersion(GetVersionRequest) returns (GetVersionResponse);
rpc ResolveInstall(ResolveInstallRequest) returns (ResolveInstallResponse);
rpc ListCategories(ListCategoriesRequest) returns (ListCategoriesResponse);
rpc ListTags(ListTagsRequest) returns (ListTagsResponse);
rpc SubmitForReview(SubmitForReviewRequest) returns (SubmitForReviewResponse);
// Private-plugin RPCs. All require the caller to be a member of the target
// account; the server resolves account membership from the bearer token.
rpc ListPrivatePlugins(ListPrivatePluginsRequest) returns (ListPrivatePluginsResponse);
rpc DeletePrivatePlugin(DeletePrivatePluginRequest) returns (DeletePrivatePluginResponse);
rpc DeletePrivatePluginVersion(DeletePrivatePluginVersionRequest) returns (DeletePrivatePluginVersionResponse);
rpc ListPrivatePluginInstallSites(ListPrivatePluginInstallSitesRequest) returns (ListPrivatePluginInstallSitesResponse);
// Instance-authenticated variants used by the CMS install/update flow. A CMS
// instance calls these with its per-account X-CMS-Secret header plus its
// instance_id; the server resolves the owning account from the instance and
// authorises private plugins owned by that account. No end-user JWT is
// required (the calling instance, not a user, is the trust unit).
rpc ListPrivatePluginsForInstance(ListPrivatePluginsForInstanceRequest) returns (ListPrivatePluginsResponse);
rpc ResolveInstallForInstance(ResolveInstallForInstanceRequest) returns (ResolveInstallResponse);
}
// PluginModerationService is the superadmin-only side: lists pending submissions
// and decides them. The lifecycle is: scope member CreatePlugin → PublishVersion(s)
// → SubmitForReview → admin Approve / Reject / RequestChanges.
service PluginModerationService {
rpc ListPendingReviews(ListPendingReviewsRequest) returns (ListPendingReviewsResponse);
rpc ApproveSubmission(ApproveSubmissionRequest) returns (ApproveSubmissionResponse);
rpc RejectSubmission(RejectSubmissionRequest) returns (RejectSubmissionResponse);
rpc RequestChanges(RequestChangesRequest) returns (RequestChangesResponse);
}
// PluginPublishService is called by the ninja CLI to publish a version.
service PluginPublishService {
rpc PublishVersion(PublishVersionRequest) returns (PublishVersionResponse);
}
// PluginAuthService handles OAuth device flow used by ninja CLI.
service PluginAuthService {
rpc StartDevice(StartDeviceRequest) returns (StartDeviceResponse);
rpc PollDevice(PollDeviceRequest) returns (PollDeviceResponse);
rpc ApproveDevice(ApproveDeviceRequest) returns (ApproveDeviceResponse);
rpc DenyDevice(DenyDeviceRequest) returns (DenyDeviceResponse);
rpc GetDeviceStatus(GetDeviceStatusRequest) returns (GetDeviceStatusResponse);
rpc Whoami(WhoamiRequest) returns (WhoamiResponse);
// ListMyAccounts is the CLI-facing account picker, returning the bare
// minimum needed for `ninja login` / `ninja account list`. The CLI in
// core/cmd/ninja consumes only this proto file; pulling in accounts.proto
// and AccountService would force core to also generate bindings for the
// full Account message and collide with the orchestrator's own copy at
// proto-registration time.
rpc ListMyAccountsForCLI(ListMyAccountsForCLIRequest) returns (ListMyAccountsForCLIResponse);
}
// --- Shared messages ---
message Scope {
string id = 1;
string slug = 2;
string display_name = 3;
google.protobuf.Timestamp created_at = 4;
}
// PluginVisibility is the lifecycle/access state of a plugin in the registry.
// PRIVATE plugins are scoped to a single account; PUBLIC plugins are visible
// to all. UNDER_REVIEW / REJECTED / TAKEN_DOWN are public-registry moderation
// states and do not apply to private plugins.
enum PluginVisibility {
PLUGIN_VISIBILITY_UNSPECIFIED = 0;
PLUGIN_VISIBILITY_PRIVATE = 1;
PLUGIN_VISIBILITY_UNDER_REVIEW = 2;
PLUGIN_VISIBILITY_PUBLIC = 3;
PLUGIN_VISIBILITY_REJECTED = 4;
PLUGIN_VISIBILITY_TAKEN_DOWN = 5;
}
message Plugin {
string id = 1;
string scope_slug = 2;
string name = 3;
PluginVisibility visibility = 4;
bool premium = 5;
string description = 6;
string homepage_url = 7;
repeated string categories = 8;
google.protobuf.Timestamp updated_at = 9;
string kind = 10;
string display_name = 11;
// owner_account_id is set for private plugins and identifies the account
// that owns the plugin. Empty for public plugins.
string owner_account_id = 12;
// latest_version is the most recent non-yanked version string published
// for this plugin (e.g. "0.2.3"). Empty when the plugin has no versions
// yet. Computed server-side as a LATERAL lookup on registry_versions so
// listing endpoints don't N+1.
string latest_version = 13;
repeated string tags = 14;
// preview_image_url is the PUBLIC, unsigned, long-lived image URL for this
// plugin's latest non-yanked version preview (a PNG screenshot), usable
// directly as an <img src>. Empty when no preview has been published yet —
// the wizard card then degrades to display_name + tags (spec §5.2).
string preview_image_url = 15;
}
message Category {
string slug = 1;
string display_name = 2;
string description = 3;
int32 sort_order = 4;
}
message Version {
string id = 1;
string plugin_id = 2;
string version = 3;
google.protobuf.Timestamp published_at = 6;
bool yanked = 7;
string sdk_constraint = 8;
string source_archive_sha256 = 9;
int64 source_archive_size = 10;
// preview_image_url is the public unsigned URL of this specific version's
// preview PNG, or empty when this version shipped no preview.png.
string preview_image_url = 11;
// abi_version is the wasm ABI major version this artifact was built against
// (WO-WZ-011). 0 means a legacy source archive (compiled in-container), not a
// .bnp. Instances filter incompatible artifacts on this before downloading.
uint32 abi_version = 12;
}
message Requirement {
string scope_slug = 1;
string name = 2;
string constraint_expr = 3;
}
// --- Scope service ---
message CreateScopeRequest { string slug = 1; string display_name = 2; }
message CreateScopeResponse { Scope scope = 1; }
message ListMyScopesRequest {}
message ListMyScopesResponse { repeated Scope scopes = 1; }
message GetScopeRequest { string slug = 1; }
message GetScopeResponse { Scope scope = 1; repeated Plugin plugins = 2; }
message ListMyPluginsRequest {}
message ListMyPluginsResponse { repeated Plugin plugins = 1; }
// --- Registry service ---
message CreatePluginRequest {
string scope_slug = 1;
string name = 2;
string description = 3;
string kind = 4;
repeated string categories = 5;
string display_name = 6;
// visibility is the requested visibility of the plugin. UNSPECIFIED defers
// to the registry's default (public for explicit scopes, private for the
// "@private" sentinel).
PluginVisibility visibility = 7;
// active_account_id is required when visibility = PRIVATE; the server
// verifies the caller is a member of this account before creating the
// private plugin under it.
string active_account_id = 8;
}
message CreatePluginResponse {
Plugin plugin = 1;
}
message GetPluginRequest {
string scope_slug = 1;
string name = 2;
// active_account_id is required when scope_slug = "private" so the server
// can resolve (account_id, name) instead of (scope_id, name). Ignored
// otherwise.
string active_account_id = 3;
}
message GetPluginResponse {
Plugin plugin = 1;
repeated Version versions = 2;
map<string,string> channels = 3;
}
message ListPluginsRequest {
int32 limit = 1;
int32 offset = 2;
string query = 3;
string kind = 4;
repeated string categories = 5;
repeated string tags = 6;
}
message ListPluginsResponse { repeated Plugin plugins = 1; }
message ListCategoriesRequest {}
message ListCategoriesResponse { repeated Category categories = 1; }
message ListTagsRequest {
// Optional kind filter: "plugin", "theme", or "" for both. Lets the browse
// UI show theme-specific tags when the user filters by kind=theme.
string kind = 1;
// Cap the response. 0 → server default (100).
int32 limit = 2;
}
message ListTagsResponse {
repeated TagCount tags = 1;
}
message TagCount {
string tag = 1;
int32 count = 2;
}
// --- Private plugin RPC messages ---
message ListPrivatePluginsRequest {
// account_id selects which account's private plugins to list. The caller
// must be a member of this account.
string account_id = 1;
}
message ListPrivatePluginsResponse {
repeated PrivatePluginSummary plugins = 1;
}
// PrivatePluginSummary is the row shape used by the publisher dashboard and
// by the CMS "Private" installer tab. It bundles a Plugin with the latest
// version per channel and an installation count.
message PrivatePluginSummary {
Plugin plugin = 1;
// channel_versions maps channel name -> latest version string published on
// that channel (e.g. "latest" -> "0.3.0", "beta" -> "0.4.0-beta.1").
map<string,string> channel_versions = 2;
// installed_site_count is the number of CMS sites in the owning account
// that currently have any version of this plugin installed. Used to gate
// delete-plugin in the dashboard.
int32 installed_site_count = 3;
}
message DeletePrivatePluginRequest {
string account_id = 1;
string plugin_name = 2;
}
message DeletePrivatePluginResponse {}
message DeletePrivatePluginVersionRequest {
string account_id = 1;
string plugin_name = 2;
string version = 3;
}
message DeletePrivatePluginVersionResponse {}
message ListPrivatePluginInstallSitesRequest {
string account_id = 1;
string plugin_name = 2;
}
message ListPrivatePluginInstallSitesResponse {
repeated PrivatePluginInstallSite sites = 1;
}
message PrivatePluginInstallSite {
string instance_id = 1;
string site_display_name = 2;
string installed_version = 3;
string pinned_channel = 4;
}
message GetVersionRequest {
string scope_slug = 1;
string plugin_name = 2;
string version = 3;
}
message GetVersionResponse {
Version version = 1;
repeated Requirement requires = 2;
string readme_md = 3;
string changelog_md = 4;
}
message ResolveInstallRequest {
string scope_slug = 1;
string plugin_name = 2;
string version_or_channel = 3;
// active_account_id is required when scope_slug = "private" so the server
// resolves (account_id, name) and verifies the caller's account
// membership. Ignored for public scopes.
string active_account_id = 4;
}
message ResolveInstallResponse {
string version_id = 1;
string scope_slug = 2;
string plugin_name = 3;
string version = 4;
string channel_pinned = 5;
string archive_url = 6;
string archive_sha256 = 7;
string sdk_constraint = 8;
repeated Requirement requires = 9;
bool yanked = 10;
repeated string warnings = 11;
// abi_version is the wasm ABI major of the resolved artifact (WO-WZ-011); 0
// for a legacy source archive. The installing instance rejects an artifact
// whose abi_version its loader cannot run.
uint32 abi_version = 12;
}
// --- Instance-authenticated request shapes ---
// The instance proves its identity with the per-account X-CMS-Secret header;
// instance_id selects the instance whose owning account scopes the call. No
// active_account_id / user JWT is needed. These cover only private plugins —
// public plugins use the anonymous ListPlugins / ResolveInstall.
message ListPrivatePluginsForInstanceRequest {
string instance_id = 1;
}
message ResolveInstallForInstanceRequest {
string instance_id = 1;
string plugin_name = 2;
string version_or_channel = 3;
}
// --- Review / moderation ---
message PendingReview {
string review_id = 1;
string plugin_id = 2;
string scope_slug = 3;
string plugin_name = 4;
string plugin_display_name = 5;
string submitted_by_user_id = 6;
google.protobuf.Timestamp submitted_at = 7;
}
message SubmitForReviewRequest {
string plugin_id = 1;
}
message SubmitForReviewResponse {
// visibility is "under_review" for non-admin authors, "public" when a superadmin
// submits (they bypass the queue entirely). String form is preserved here for
// backwards compatibility with existing handlers; the canonical enum is
// PluginVisibility on the Plugin message.
string visibility = 1;
}
message ListPendingReviewsRequest {
int32 limit = 1;
int32 offset = 2;
}
message ListPendingReviewsResponse {
repeated PendingReview reviews = 1;
}
message ApproveSubmissionRequest {
string plugin_id = 1;
string reason = 2; // optional note for audit log
}
message ApproveSubmissionResponse {}
message RejectSubmissionRequest {
string plugin_id = 1;
string reason = 2; // required
}
message RejectSubmissionResponse {}
message RequestChangesRequest {
string plugin_id = 1;
string reason = 2; // required — author sees this verbatim
}
message RequestChangesResponse {}
// --- Publish service ---
message PublishVersionRequest {
string plugin_id = 1;
string version = 2;
string channel = 3;
bytes archive = 4;
string readme_md = 5;
string changelog_md = 6;
}
message PublishVersionResponse {
Version version = 1;
string archive_url = 2;
repeated string warnings = 3;
}
// --- Auth service (device flow) ---
message StartDeviceRequest { repeated string scopes = 1; }
message StartDeviceResponse {
string device_code = 1;
string user_code = 2;
string verification_uri = 3;
int32 interval_seconds = 4;
int32 expires_in_seconds = 5;
}
message PollDeviceRequest { string device_code = 1; }
message PollDeviceResponse {
string access_token = 1;
string status = 2;
}
message ApproveDeviceRequest { string user_code = 1; }
message ApproveDeviceResponse {}
message DenyDeviceRequest { string user_code = 1; }
message DenyDeviceResponse {}
message GetDeviceStatusRequest { string user_code = 1; }
message GetDeviceStatusResponse {
bool valid = 1;
bool authorized = 2;
bool denied = 3;
string client_id = 4;
}
message WhoamiRequest {}
message WhoamiResponse {
string user_id = 1;
string email = 2;
string display_name = 3;
}
// MyAccount is the slim account row returned to the CLI by
// PluginAuthService.ListMyAccounts. The orchestrator's full Account message
// lives in accounts.proto and carries billing, plan, status, etc.; CLI
// account-switching needs none of that, so this shape stays minimal and
// avoids forcing core to generate bindings for accounts.proto.
message MyAccount {
string id = 1;
string slug = 2;
string name = 3;
}
message ListMyAccountsForCLIRequest {}
message ListMyAccountsForCLIResponse {
repeated MyAccount accounts = 1;
}