syntax = "proto3";

package grackle;

// ─── Common Types ───────────────────────────────────────────

message Empty {}

message EnvironmentId {
  string id = 1;
}

message SessionId {
  string id = 1;
}

message KillAgentRequest {
  string id = 1;
  bool graceful = 2;  // false = SIGKILL (immediate), true = SIGTERM (advisory)
}

// ─── Enums ──────────────────────────────────────────────────

enum EventType {
  EVENT_TYPE_UNSPECIFIED = 0;
  EVENT_TYPE_TEXT = 1;
  EVENT_TYPE_TOOL_USE = 2;
  EVENT_TYPE_TOOL_RESULT = 3;
  EVENT_TYPE_ERROR = 4;
  EVENT_TYPE_STATUS = 5;
  EVENT_TYPE_SYSTEM = 6;
  EVENT_TYPE_FINDING = 7;
  EVENT_TYPE_SUBTASK_CREATE = 8;
  EVENT_TYPE_USER_INPUT = 9;
  EVENT_TYPE_SIGNAL = 10;
  EVENT_TYPE_USAGE = 11;
  EVENT_TYPE_ESCALATION = 12;
}

enum TaskStatus {
  TASK_STATUS_UNSPECIFIED = 0;
  TASK_STATUS_NOT_STARTED = 1;
  TASK_STATUS_WORKING = 3;
  TASK_STATUS_PAUSED = 4;
  TASK_STATUS_COMPLETE = 5;
  TASK_STATUS_FAILED = 6;
}

enum WorkspaceStatus {
  WORKSPACE_STATUS_UNSPECIFIED = 0;
  WORKSPACE_STATUS_ACTIVE = 1;
  WORKSPACE_STATUS_ARCHIVED = 2;
}

enum ClaudeProviderMode {
  CLAUDE_PROVIDER_MODE_UNSPECIFIED = 0;
  CLAUDE_PROVIDER_MODE_OFF = 1;
  CLAUDE_PROVIDER_MODE_SUBSCRIPTION = 2;
  CLAUDE_PROVIDER_MODE_API_KEY = 3;
}

enum ProviderToggle {
  PROVIDER_TOGGLE_UNSPECIFIED = 0;
  PROVIDER_TOGGLE_OFF = 1;
  PROVIDER_TOGGLE_ON = 2;
}

// ─── Environment ────────────────────────────────────────────

message Environment {
  string id = 1;
  string display_name = 2;
  string adapter_type = 3;
  string adapter_config = 4; // JSON
  bool bootstrapped = 6;
  string status = 7;
  string last_seen = 8;
  string env_info = 9; // JSON from PowerLine GetInfo
  string created_at = 10;
  string github_account_id = 11;
}

message EnvironmentList {
  repeated Environment environments = 1;
}

message AddEnvironmentRequest {
  string display_name = 1;
  string adapter_type = 2;
  string adapter_config = 3; // JSON
  string github_account_id = 5;
}

message ProvisionEvent {
  string stage = 1;
  string message = 2;
  double progress = 3; // 0.0 - 1.0
}

message ProvisionEnvironmentRequest {
  string id = 1;
  bool force = 2; // When true, skip reconnect, kill active sessions, and force full provision
}

message UpdateEnvironmentRequest {
  string id = 1;
  optional string display_name = 2;
  optional string adapter_config = 3; // JSON
  optional string github_account_id = 4;
}

// ─── GitHub Accounts ────────────────────────────────────────

/** A GitHub account registered with Grackle (token is never returned). */
message GitHubAccount {
  string id = 1;
  string label = 2;
  string username = 3;
  bool is_default = 4;
  string created_at = 5;
}

message GitHubAccountList {
  repeated GitHubAccount accounts = 1;
}

message AddGitHubAccountRequest {
  string label = 1;
  string token = 2;
  string username = 3; // Optional; resolved server-side if empty
  bool is_default = 4;
}

message UpdateGitHubAccountRequest {
  string id = 1;
  optional string label = 2;
  optional string token = 3;
  optional bool is_default = 4;
}

message RemoveGitHubAccountRequest {
  string id = 1;
}

message ImportGitHubAccountsResponse {
  int32 imported = 1;
  repeated string usernames = 2;
}

// ─── Session ────────────────────────────────────────────────

message Session {
  string id = 1;
  string environment_id = 2;
  string runtime = 3;
  string runtime_session_id = 4;
  string prompt = 5;
  string model = 6;
  string status = 7;
  string log_path = 8;
  int32 turns = 9;
  string started_at = 10;
  string suspended_at = 11;
  string ended_at = 12;
  string error = 13;
  string task_id = 14;
  string persona_id = 15;
  int32 input_tokens = 16;
  int32 output_tokens = 17;
  int32 cost_millicents = 18; // int32 sufficient: max ~$21,474 (2^31-1 millicents); avoids BigInt in TS clients
  int32 pipe_fd = 19; // fd assigned to parent for pipe IPC (0 = no pipe)
  string end_reason = 20; // Why the session stopped: "completed", "killed", "interrupted"
}

message SessionList {
  repeated Session sessions = 1;
}

message SessionFilter {
  string environment_id = 1;     // empty = all
  string status = 2;             // empty = all
}

message SpawnRequest {
  string environment_id = 1;
  string prompt = 2;
  int32 max_turns = 4;
  string branch = 6; // Branch for worktree (empty = skip)
  string system_context = 7; // Prepended to prompt (task desc + findings)
  string persona_id = 8; // Persona to use (falls back to app default persona)
  string working_directory = 9; // Working directory / repo root (non-task spawns)
  string pipe = 10; // "sync", "async", "detach", or "" (no pipe)
  string parent_session_id = 11; // Caller's session ID (for pipe setup)
  string workspace_id = 12; // Workspace scope for the session (used to associate MCP tool calls)
}

message ResumeRequest {
  string session_id = 1;
}

message InputMessage {
  string session_id = 1;
  string text = 2;
}

// ─── IPC ────────────────────────────────────────────────────

message WaitForPipeRequest {
  string session_id = 1;       // Parent's session ID
  int32 fd = 2;                // fd to wait on
}

message WaitForPipeResponse {
  string content = 1;          // Message content from child
  string sender_session_id = 2;
}

message WriteToFdRequest {
  string session_id = 1;       // Caller's session ID
  int32 fd = 2;                // fd to write to
  string message = 3;          // Message content
}

message CloseFdRequest {
  string session_id = 1;       // Caller's session ID
  int32 fd = 2;                // fd to close
}

message CloseFdResponse {
  bool stopped = 1;            // True if the child was stopped (last fd closed)
}

message SessionFds {
  repeated FdInfo fds = 1;
}

message FdInfo {
  int32 fd = 1;
  string stream_name = 2;
  string permission = 3;
  string delivery_mode = 4;
  bool owned = 5;              // true = opened via spawn, false = inherited
  string target_session_id = 6; // the other session on this stream
}

message CreateStreamRequest {
  string session_id = 1;          // Creator's session ID
  string name = 2;                // Human-readable label (must be unique)
  bool self_echo = 3;             // When true, publishers receive their own messages echoed back (chatroom mode)
}

message CreateStreamResponse {
  string stream_id = 1;           // Opaque stream identifier
  int32 fd = 2;                   // Creator's fd (rw permission, async delivery)
}

message AttachStreamRequest {
  string session_id = 1;          // Caller's session ID (must hold an fd to the stream)
  int32 fd = 2;                   // Caller's fd on the stream
  string target_session_id = 3;   // Session to grant access to
  string permission = 4;          // "r", "w", or "rw" (must not exceed caller's)
  string delivery_mode = 5;       // "sync", "async", or "detach"
}

message AttachStreamResponse {
  int32 fd = 1;                   // The fd allocated for the target session
}

// ─── Stream Listing ──────────────────────────────────────────

message StreamSubscriberInfo {
  string subscription_id = 1;
  string session_id = 2;
  int32 fd = 3;
  string permission = 4;         // "r", "w", or "rw"
  string delivery_mode = 5;      // "sync", "async", or "detach"
  bool created_by_spawn = 6;
}

message StreamInfo {
  string id = 1;
  string name = 2;
  int32 subscriber_count = 3;
  int32 message_buffer_depth = 4;
  repeated StreamSubscriberInfo subscribers = 5;
  bool self_echo = 6;             // Whether publishers receive their own messages echoed back
}

message ListStreamsResponse {
  repeated StreamInfo streams = 1;
}

// ─── Streaming Events ───────────────────────────────────────

message SessionEvent {
  string session_id = 1;
  EventType type = 2;
  string timestamp = 3;
  string content = 4;
  string raw = 5; // JSON, optional original payload
}

message SessionEventList {
  string session_id = 1;
  repeated SessionEvent events = 2;
}

// ─── Tokens ─────────────────────────────────────────────────

message TokenEntry {
  string name = 1;
  string type = 2;
  string env_var = 3;
  string file_path = 4;
  string value = 5;
  string expires_at = 6;
}

message TokenList {
  repeated TokenInfo tokens = 1;
}

message TokenInfo {
  string name = 1;
  string type = 2;
  string env_var = 3;
  string file_path = 4;
  string expires_at = 5;
  // No value — never expose to clients
}

message TokenName {
  string name = 1;
}

// ─── Credential Providers ──────────────────────────────────

message CredentialProviderConfig {
  ClaudeProviderMode claude = 1;
  ProviderToggle github = 2;
  ProviderToggle copilot = 3;
  ProviderToggle codex = 4;
  ProviderToggle goose = 5;
}

message SetCredentialProviderRequest {
  string provider = 1;  // "claude", "github", "copilot", "codex", or "goose"
  string value = 2;     // "off", "on", "subscription", "api_key"
}

// ─── Workspaces ─────────────────────────────────────────────

message Workspace {
  string id = 1;
  string name = 2;
  string description = 3;
  string repo_url = 4;
  WorkspaceStatus status = 6;
  string created_at = 7;
  string updated_at = 8;
  bool use_worktrees = 9; // When false, agents work in the main checkout (no worktree dirs created)
  string working_directory = 10; // Working directory / repo root (e.g. /workspaces/my-repo)
  string default_persona_id = 11; // Default persona for tasks in this workspace
  repeated string linked_environment_ids = 13; // Linked environments (read-only, set via Link/Unlink RPCs)
  int32 token_budget = 14; // Aggregate token cap across all tasks; 0 = unlimited
  int32 cost_budget_millicents = 15; // Aggregate cost cap in millicents; 0 = unlimited
}

message WorkspaceList {
  repeated Workspace workspaces = 1;
}

message ListWorkspacesRequest {
  string environment_id = 1; // empty = all
}

message CreateWorkspaceRequest {
  string name = 1;
  string description = 2;
  string repo_url = 3;
  optional bool use_worktrees = 5; // Default true when omitted
  optional string working_directory = 6; // Working directory / repo root (e.g. /workspaces/my-repo)
  optional string default_persona_id = 7; // Default persona for tasks in this workspace
  string environment_id = 8; // Required: initial environment to link
  optional int32 token_budget = 9; // Aggregate token cap; 0 = unlimited
  optional int32 cost_budget_millicents = 10; // Aggregate cost cap in millicents; 0 = unlimited
}

message UpdateWorkspaceRequest {
  string id = 1;
  optional string name = 2;
  optional string description = 3;
  optional string repo_url = 4;
  optional bool use_worktrees = 6; // When false, disables worktree isolation for this workspace
  optional string working_directory = 7; // Working directory / repo root (e.g. /workspaces/my-repo)
  optional string default_persona_id = 8; // Default persona for tasks in this workspace
  optional int32 token_budget = 10; // Aggregate token cap; 0 = unlimited
  optional int32 cost_budget_millicents = 11; // Aggregate cost cap in millicents; 0 = unlimited
}

message WorkspaceId {
  string id = 1;
}

message LinkEnvironmentRequest {
  string workspace_id = 1;
  string environment_id = 2;
}

message UnlinkEnvironmentRequest {
  string workspace_id = 1;
  string environment_id = 2;
}

// ─── Tasks ─────────────────────────────────────────────────

message Task {
  string id = 1;
  optional string workspace_id = 2;
  string title = 3;
  string description = 4;
  TaskStatus status = 5;
  string branch = 6;
  string latest_session_id = 8; // computed on read from session history
  repeated string depends_on = 9;
  string started_at = 11;
  string completed_at = 12;
  string created_at = 14;
  string updated_at = 15;
  int32 sort_order = 16;
  string parent_task_id = 17; // empty for root tasks
  int32 depth = 18; // 0 for root, parent.depth + 1
  repeated string child_task_ids = 19; // populated by server on read
  bool can_decompose = 20;
  string default_persona_id = 22; // Default persona for this task (overrides workspace default)
  string workpad = 23; // Persistent structured context (JSON) that survives across sessions
  string schedule_id = 24; // ID of the schedule that created this task (empty for manually-created tasks)
  int32 token_budget = 25; // Total token cap (input + output); 0 = unlimited
  int32 cost_budget_millicents = 26; // Cost cap in millicents ($0.00001 units); 0 = unlimited
}

message TaskList {
  repeated Task tasks = 1;
}

message ListTasksRequest {
  string workspace_id = 1;
  string search = 2;      // optional case-insensitive substring filter on title/description
  string status = 3;      // optional status filter (e.g. "not_started", "working", "complete")
}

message SearchTasksRequest {
  string query = 1;        // fuzzy search query (required)
  string workspace_id = 2; // optional workspace scope
  int32 limit = 3;         // max results to return; 0 means use default (10)
  string status = 4;       // optional status filter (e.g. "not_started", "working", "complete")
}

message SearchTaskResult {
  Task task = 1;
  double relevance_score = 2; // relevance score 0.0–1.0, higher is more relevant
}

message SearchTasksResponse {
  repeated SearchTaskResult results = 1;
}

message CreateTaskRequest {
  optional string workspace_id = 1;
  string title = 2;
  string description = 3;
  repeated string depends_on = 5;
  string parent_task_id = 6;
  optional bool can_decompose = 7;
  optional string default_persona_id = 9; // Default persona for this task
  optional int32 token_budget = 10; // Total token cap (input + output); 0 = unlimited
  optional int32 cost_budget_millicents = 11; // Cost cap in millicents; 0 = unlimited
}

message TaskId {
  string id = 1;
}

message UpdateTaskRequest {
  string id = 1;
  string title = 2;
  string description = 3;
  TaskStatus status = 4;
  repeated string depends_on = 6;
  string session_id = 8; // Optional: late-bind an existing session to this task
  optional string default_persona_id = 9; // Default persona for this task
  optional int32 token_budget = 10; // Total token cap (input + output); 0 = unlimited
  optional int32 cost_budget_millicents = 11; // Cost cap in millicents; 0 = unlimited
}

message StartTaskRequest {
  string task_id = 1;
  string persona_id = 4; // Persona override (falls back to task/workspace/app default)
  string environment_id = 5; // Select environment at start time
  string notes = 6; // Feedback/instructions for retry (included in system context)
  string pipe = 7; // "sync", "async", "detach", or "" (no pipe)
  string parent_session_id = 8; // Caller's session ID (for pipe setup)
}

message SetWorkpadRequest {
  string task_id = 1;
  string workpad = 2; // JSON string with status, summary, and extra fields
}

// ─── Findings ──────────────────────────────────────────────

message Finding {
  string id = 1;
  string workspace_id = 2;
  string task_id = 3;
  string session_id = 4;
  string category = 5;
  string title = 6;
  string content = 7;
  repeated string tags = 8;
  string created_at = 9;
}

message FindingList {
  repeated Finding findings = 1;
}

message PostFindingRequest {
  string workspace_id = 1;
  string task_id = 2;
  string session_id = 3;
  string category = 4;
  string title = 5;
  string content = 6;
  repeated string tags = 7;
}

message QueryFindingsRequest {
  // Optional: when empty, returns findings across all workspaces.
  string workspace_id = 1;
  repeated string categories = 2;
  repeated string tags = 3;
  int32 limit = 4;
}

message GetFindingRequest {
  string id = 1;
}

// ─── Escalations ───────────────────────────────────────────

message Escalation {
  string id = 1;
  string workspace_id = 2;
  string task_id = 3;
  string title = 4;
  string message = 5;
  string source = 6;
  string urgency = 7;
  string status = 8;
  string created_at = 9;
  string delivered_at = 10;
  string acknowledged_at = 11;
  string task_url = 12;
}

message EscalationList {
  repeated Escalation escalations = 1;
}

message CreateEscalationRequest {
  string workspace_id = 1;
  string task_id = 2;
  string title = 3;
  string message = 4;
  string urgency = 5;
}

message ListEscalationsRequest {
  string workspace_id = 1;
  string status = 2;
  int32 limit = 3;
}

message AcknowledgeEscalationRequest {
  string id = 1;
}

// ─── Personas ──────────────────────────────────────────────

message McpServerConfig {
  string name = 1; // Server name (e.g. "grackle", "github")
  string command = 2; // Executable to run
  repeated string args = 3; // CLI arguments
  repeated string tools = 4; // Per-server tool allowlist (empty = all)
}

message ToolConfig {
  repeated string allowed_tools = 1; // Allowlist (empty = allow all)
  repeated string disallowed_tools = 2; // Blocklist (takes precedence)
}

// Wrapper for allowed MCP tools with proto3 presence tracking.
// When the message field is absent → "don't change" (preserve existing).
// When present with empty tools → "clear to default" (revert to full set).
// When present with tools → "replace with this list".
message AllowedMcpTools {
  repeated string tools = 1;
}

message Persona {
  string id = 1;
  string name = 2;
  string description = 3;
  string system_prompt = 4;
  ToolConfig tool_config = 5;
  string runtime = 6;
  string model = 7;
  int32 max_turns = 8;
  repeated McpServerConfig mcp_servers = 9;
  string created_at = 10;
  string updated_at = 11;
  string type = 12; // "agent" (default) or "script"
  string script = 13; // Script source code (for script personas)
  repeated string allowed_mcp_tools = 14; // Persona-scoped MCP tool allowlist (empty = default set)
}

message PersonaList {
  repeated Persona personas = 1;
}

message PersonaId {
  string id = 1;
}

message CreatePersonaRequest {
  string name = 1;
  string description = 2;
  string system_prompt = 3;
  ToolConfig tool_config = 4;
  string runtime = 5;
  string model = 6;
  int32 max_turns = 7;
  repeated McpServerConfig mcp_servers = 8;
  string type = 9; // "agent" (default) or "script"
  string script = 10; // Script source code (for script personas)
  repeated string allowed_mcp_tools = 11; // Persona-scoped MCP tool allowlist
}

message UpdatePersonaRequest {
  string id = 1;
  string name = 2;
  string description = 3;
  string system_prompt = 4;
  ToolConfig tool_config = 5;
  string runtime = 6;
  string model = 7;
  int32 max_turns = 8;
  repeated McpServerConfig mcp_servers = 9;
  string type = 10; // "agent" or "script"
  string script = 11; // Script source code (for script personas)
  AllowedMcpTools allowed_mcp_tools = 13; // Presence-aware wrapper (absent = keep, empty = clear, populated = replace)
}

// ─── Schedules ─────────────────────────────────────────────

message Schedule {
  string id = 1;
  string title = 2;
  string description = 3;
  string schedule_expression = 4; // Interval shorthand ("30s", "5m") or cron expression ("0 9 * * MON")
  string persona_id = 5;
  string workspace_id = 7; // Empty = system-level schedule
  string parent_task_id = 8; // Empty = ROOT_TASK_ID
  bool enabled = 9;
  string last_run_at = 10;
  string next_run_at = 11;
  int32 run_count = 12;
  string created_at = 13;
  string updated_at = 14;
}

message ScheduleId {
  string id = 1;
}

message ScheduleList {
  repeated Schedule schedules = 1;
}

message CreateScheduleRequest {
  string title = 1;
  string description = 2;
  string schedule_expression = 3;
  string persona_id = 4;
  string workspace_id = 6; // Empty = system-level
  string parent_task_id = 7; // Empty = ROOT_TASK_ID
}

message UpdateScheduleRequest {
  string id = 1;
  optional string title = 2;
  optional string description = 3;
  optional string schedule_expression = 4;
  optional string persona_id = 5;
  optional bool enabled = 7;
}

message ListSchedulesRequest {
  string workspace_id = 1; // Empty = all
}

// ─── Codespaces ────────────────────────────────────────────

message CodespaceInfo {
  string name = 1;
  string repository = 2;
  string state = 3;
  string git_status = 4;
}

message CodespaceList {
  repeated CodespaceInfo codespaces = 1;
  string error = 2; // non-fatal error (e.g. gh CLI not available)
}

/** Optional filter for ListCodespaces — leave github_account_id empty for the default account. */
message ListCodespacesRequest {
  string github_account_id = 1;
}

message CreateCodespaceRequest {
  string repo = 1;
  string machine = 2; // optional machine type
}

message CreateCodespaceResponse {
  string name = 1;
  string repository = 2;
}

// ─── Docker ─────────────────────────────────────────────────

/** A running Docker container that an environment can attach to. */
message DockerContainerInfo {
  string id = 1;
  string name = 2;
  string image = 3;
  string state = 4;  // e.g. "running"
  string status = 5; // human-readable status (e.g. "Up 3 minutes")
}

message DockerContainerList {
  repeated DockerContainerInfo containers = 1;
  string error = 2; // non-fatal error (e.g. docker CLI not available)
}

/** Request for ListDockerContainers. Empty for now; reserved for future filters. */
message ListDockerContainersRequest {
}

// ─── Settings ───────────────────────────────────────────────

message GetSettingRequest {
  string key = 1;
}

message SetSettingRequest {
  string key = 1;
  string value = 2;
}

message SettingResponse {
  string key = 1;
  string value = 2;
}

// ─── Pairing ────────────────────────────────────────────────

message PairingCodeResponse {
  string code = 1;
  string url = 2;
}

// ─── Usage ──────────────────────────────────────────────────

message GetUsageRequest {
  string scope = 1;              // "session" | "task" | "task_tree" | "workspace" | "environment"
  string id = 2;                 // ID of the scoped entity
}

message UsageStats {
  int32 input_tokens = 1;
  int32 output_tokens = 2;
  int32 cost_millicents = 3; // int32 sufficient: max ~$21,474 (2^31-1 millicents); avoids BigInt in TS clients
  int32 session_count = 4;
}

// ─── Knowledge Graph ─────────────────────────────────────────

message KnowledgeNodeProto {
  string id = 1;
  string kind = 2;
  string workspace_id = 3;
  string created_at = 4;
  string updated_at = 5;
  // Reference node fields
  string source_type = 6;
  string source_id = 7;
  string label = 8;
  // Native node fields
  string category = 9;
  string title = 10;
  string content = 11;
  repeated string tags = 12;
}

message KnowledgeEdgeProto {
  string from_id = 1;
  string to_id = 2;
  string type = 3;
  string metadata_json = 4;
  string created_at = 5;
}

message SearchKnowledgeRequest {
  string query = 1;
  int32 limit = 2;
  string workspace_id = 3;
}

message SearchKnowledgeResult {
  double score = 1;
  KnowledgeNodeProto node = 2;
  repeated KnowledgeEdgeProto edges = 3;
}

message SearchKnowledgeResponse {
  repeated SearchKnowledgeResult results = 1;
}

message GetKnowledgeNodeRequest {
  string id = 1;
}

message GetKnowledgeNodeResponse {
  KnowledgeNodeProto node = 1;
  repeated KnowledgeEdgeProto edges = 2;
}

message ExpandKnowledgeNodeRequest {
  string id = 1;
  int32 depth = 2;
  repeated string edge_types = 3;
}

message ExpandKnowledgeNodeResponse {
  repeated KnowledgeNodeProto nodes = 1;
  repeated KnowledgeEdgeProto edges = 2;
}

message ListRecentKnowledgeNodesRequest {
  int32 limit = 1;
  string workspace_id = 2;
}

message ListRecentKnowledgeNodesResponse {
  repeated KnowledgeNodeProto nodes = 1;
  repeated KnowledgeEdgeProto edges = 2;
}

message CreateKnowledgeNodeRequest {
  string title = 1;
  string content = 2;
  string category = 3;
  repeated string tags = 4;
  string workspace_id = 5;
}

message CreateKnowledgeNodeResponse {
  string id = 1;
}

// ─── Version Status ─────────────────────────────────────────

message VersionStatus {
  string current_version = 1;
  string latest_version = 2;
  bool update_available = 3;
  bool is_docker = 4;
}

// ─── Unified Event Streaming ────────────────────────────────

message DomainEvent {
  string id = 1;
  string type = 2;         // e.g. "task.created", "environment.changed"
  string timestamp = 3;
  string payload_json = 4; // JSON-encoded payload
}

message ServerEvent {
  oneof event {
    SessionEvent session_event = 1;
    DomainEvent domain_event = 2;
  }
}

// ─── Plugins ─────────────────────────────────────────────────

message PluginInfo {
  string name = 1;
  string description = 2;
  bool enabled = 3;    // DB-persisted desired state
  bool required = 4;   // true for core (cannot be disabled)
  bool loaded = 5;     // currently running in this server instance
}

message PluginList {
  repeated PluginInfo plugins = 1;
}

message SetPluginEnabledRequest {
  string name = 1;
  bool enabled = 2;
}
