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>
457 lines
15 KiB
Protocol Buffer
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;
|
|
}
|