// kandev.plugin.v1 Host data API — folded into // apps/backend/proto/kandev/plugin/v1/plugin.proto and fully wired (reads and // writes both implemented). This file is the frozen design draft kept for // reference alongside the ADRs: a point-in-time SNAPSHOT that has NOT tracked // the contract since it was written. For the live shape, read // apps/backend/proto/kandev/plugin/v1/plugin.proto (wire contract) and // apps/backend/pkg/pluginsdk/data_types.go (Go SDK mirror) directly — do not // infer current fields from this file. Examples of known-diverged messages // (not exhaustive — other messages in this file may also have drifted): // `Task` (17 fields here vs 29 live), `WorkflowStep` (5 here vs 10 live — // missing `color`, `is_start_step`, `wip_limit`, `agent_profile_id`, // `on_enter_action_types`), and `Message` (absent here; the whole // `ListMessages` read path from ADR 0047 postdates this draft). // // Extends the existing capability-gated Host service so plugins read AND write // kandev data through a typed, versioned contract instead of touching the // SQLite file directly. These messages are a HAND-DEFINED public contract: the // backend maps internal models → these DTOs explicitly. Do NOT generate them // from domain structs — the mapping IS the decoupling. // // Enforcement (host side): each RPC checks the caller plugin's manifest // capabilities. Reads require `api_read: [""]`; writes require // `api_write: [""]`. Undeclared → gRPC PermissionDenied // "capability 'api_read:tasks' not declared". Writes go through the service // layer (which publishes task.* events), never the repository. syntax = "proto3"; package kandev.plugin.v1; import "google/protobuf/struct.proto"; // ── Added to the existing `service Host` (state/secrets/emit already there) ── service HostData { // Reads — capability api_read: rpc ListTasks(ListTasksRequest) returns (ListTasksResponse); rpc GetTask(GetTaskRequest) returns (GetTaskResponse); rpc ListWorkspaces(ListWorkspacesRequest) returns (ListWorkspacesResponse); rpc ListWorkflows(ListWorkflowsRequest) returns (ListWorkflowsResponse); rpc ListWorkflowSteps(ListWorkflowStepsRequest) returns (ListWorkflowStepsResponse); rpc ListAgentProfiles(ListAgentProfilesRequest) returns (ListAgentProfilesResponse); rpc ListExecutorProfiles(ListExecutorProfilesRequest) returns (ListExecutorProfilesResponse); rpc ListRepositories(ListRepositoriesRequest) returns (ListRepositoriesResponse); // Sessions + code stats — capability api_read:sessions. Driven by a real // plugin (kandev-plugin-agent-stats) that otherwise read task_sessions, // task_session_commits, and task_session_git_snapshots straight from the // SQLite file. SessionCodeStats is a COMPUTED, stable DTO — deliberately not // the raw commit/snapshot rows, whose JSON schema churns. rpc ListSessions(ListSessionsRequest) returns (ListSessionsResponse); rpc ListSessionCodeStats(ListSessionCodeStatsRequest) returns (ListSessionCodeStatsResponse); // Writes — capability api_write:. Route through service methods so // the corresponding task.* events fire; this is the whole reason not to let // plugins write the DB. // `ListExecutorProfiles` requires `api_read:executor_profiles`; preview/delete // capability is implicit in plugin-owned task provenance and is never conferred by // a task link stored in plugin state. rpc CreateTask(CreateTaskRequest) returns (CreateTaskResponse); rpc UpdateTask(UpdateTaskRequest) returns (UpdateTaskResponse); rpc SendMessage(SendMessageRequest) returns (SendMessageResponse); rpc PreviewPluginOwnedTaskTree(PreviewPluginOwnedTaskTreeRequest) returns (PreviewPluginOwnedTaskTreeResponse); rpc DeletePluginOwnedTaskTree(DeletePluginOwnedTaskTreeRequest) returns (DeletePluginOwnedTaskTreeResponse); } // ── Common ────────────────────────────────────────────────────────────────── // Pagination is opaque-cursor based so the server can change the underlying // ordering/store without breaking plugins. message Page { int32 limit = 1; // 0 → server default (e.g. 50); server caps the max string cursor = 2; // empty → first page; echo next_cursor to continue } message PageInfo { string next_cursor = 1; bool has_more = 2; } // Timestamps are RFC3339 strings (matches the JSON API + the Event envelope // convention), not protobuf Timestamp, to keep one time representation across // the whole plugin contract. Optional string fields use `optional` so absent // (NULL) is distinguishable from empty. // ── Task ──────────────────────────────────────────────────────────────────── message Task { string id = 1; string workspace_id = 2; string workflow_id = 3; string title = 4; string description = 5; string state = 6; // TaskState enum value as a string, stable string priority = 7; string created_by = 8; string created_at = 9; // RFC3339 string updated_at = 10; optional string started_at = 11; optional string completed_at = 12; optional string parent_id = 13; string identifier = 14; bool is_ephemeral = 15; repeated TaskRepository repositories = 16; google.protobuf.Struct metadata = 17; // free-form; plugin-tolerant } message TaskRepository { string id = 1; string repository_id = 2; string base_branch = 3; int32 position = 4; string checkout_branch = 5; } message TaskFilter { repeated string workspace_ids = 1; // empty → all the plugin may see repeated string workflow_ids = 2; repeated string states = 3; optional string parent_id = 4; bool include_ephemeral = 5; // default false (kanban-visible only) } message ListTasksRequest { TaskFilter filter = 1; Page page = 2; } message ListTasksResponse { repeated Task tasks = 1; PageInfo page_info = 2; } message GetTaskRequest { string id = 1; } message GetTaskResponse { Task task = 1; } // ── Workspace / Workflow / Steps ───────────────────────────────────────────── message Workspace { string id = 1; string name = 2; optional string description = 3; string owner_id = 4; optional string default_executor_id = 5; optional string default_agent_profile_id = 6; string created_at = 7; string updated_at = 8; } message ListWorkspacesRequest { Page page = 1; } message ListWorkspacesResponse { repeated Workspace workspaces = 1; PageInfo page_info = 2; } message Workflow { string id = 1; string workspace_id = 2; string name = 3; optional string description = 4; int32 sort_order = 5; string created_at = 6; string updated_at = 7; } message ListWorkflowsRequest { string workspace_id = 1; Page page = 2; } message ListWorkflowsResponse { repeated Workflow workflows = 1; PageInfo page_info = 2; } // DIVERGED from the live contract — see canonical // apps/backend/proto/kandev/plugin/v1/plugin.proto for fields 6-10 // (color, is_start_step, wip_limit, agent_profile_id, on_enter_action_types). message WorkflowStep { string id = 1; string workflow_id = 2; string name = 3; int32 position = 4; string stage_type = 5; // work | review | approval | custom } message ListWorkflowStepsRequest { string workflow_id = 1; } message ListWorkflowStepsResponse { repeated WorkflowStep steps = 1; } // ── Agent profiles / Repositories (read-only context) ──────────────────────── message AgentProfile { string id = 1; string agent_id = 2; string display_name = 3; string name = 4; string model = 5; string mode = 6; } message ListAgentProfilesRequest { Page page = 1; } message ListAgentProfilesResponse { repeated AgentProfile profiles = 1; PageInfo page_info = 2; } message ExecutorProfile { string id = 1; string display_name = 2; string executor_type = 3; } message ListExecutorProfilesRequest { Page page = 1; } message ListExecutorProfilesResponse { repeated ExecutorProfile profiles = 1; PageInfo page_info = 2; } message Repository { string id = 1; string workspace_id = 2; string name = 3; // owner/repo slug when known, else name optional string default_branch = 4; // Credential-free provider origin identity. Never expose local paths, // scripts, or credentials through this Host DTO. string source_type = 5; string provider_id = 6; string provider_repository_id = 7; string provider_host = 8; string owner_or_project = 9; string provider_name = 10; string remote_url = 11; // Opaque provider-defined connection scope. Together with the immutable // provider_repository_id, this is the durable upstream identity. string provider_scope = 12; } message ListRepositoriesRequest { string workspace_id = 1; Page page = 2; } message ListRepositoriesResponse { repeated Repository repositories = 1; PageInfo page_info = 2; } // ── Sessions ───────────────────────────────────────────────────────────────── // Identity + agent context for one agent run on a task. acp_session_id is the // join key an external usage tool (e.g. tokscale) uses to attribute tokens — so // kandev exposes it and stays out of the token business entirely. message Session { string id = 1; string task_id = 2; string agent_profile_id = 3; string agent_display_name = 4; // from the profile snapshot at run time string model = 5; // resolved model at run time string acp_session_id = 6; // external usage-attribution join key string state = 7; string started_at = 8; optional string ended_at = 9; string agent_profile_name = 10; // profile name from the snapshot at run time } message SessionFilter { repeated string task_ids = 1; repeated string workspace_ids = 2; repeated string states = 3; } message ListSessionsRequest { SessionFilter filter = 1; Page page = 2; } message ListSessionsResponse { repeated Session sessions = 1; PageInfo page_info = 2; } // Computed code metrics per session — the aggregate the agent-stats plugin // re-derived by hand from commits + git snapshots. Exposed as a stable shape so // plugins never touch task_session_commits / task_session_git_snapshots. message SessionCodeStats { string session_id = 1; int64 lines_added_committed = 2; // SUM of commit insertions int64 lines_deleted_committed = 3; // SUM of commit deletions int64 lines_added_peak_pending = 4; // peak uncommitted diff additions across snapshots int64 lines_deleted_peak_pending = 5; } message ListSessionCodeStatsRequest { SessionFilter filter = 1; Page page = 2; } message ListSessionCodeStatsResponse { repeated SessionCodeStats stats = 1; PageInfo page_info = 2; } // ── Writes (api_write) ─────────────────────────────────────────────────────── message CreateTaskRequest { string workspace_id = 1; string workflow_id = 2; optional string workflow_step_id = 3; string title = 4; string description = 5; optional string parent_id = 6; bool start_agent = 7; // kandev stamps source = "plugin:" server-side; not settable here. repeated PluginTaskRepository repositories = 8; optional PluginTaskLaunchOptions launch = 9; google.protobuf.Struct metadata = 10; // plugin namespace only; host reserves source. } message PluginTaskRepository { string repository_id = 1; // existing Kandev repository, or empty with remote below optional RemoteRepositoryDescriptor remote = 2; optional string base_branch = 3; optional string checkout_branch = 4; optional int64 pull_request_number = 5; } message RemoteRepositoryDescriptor { string provider_id = 1; string provider_host = 2; string owner_or_project = 3; string provider_repository_id = 4; string name = 5; string clone_url = 6; // exact credential-free URL; host never reconstructs it optional string default_branch = 7; optional string base_branch = 8; optional string head_branch = 9; optional int64 pull_request_number = 10; string provider_scope = 11; // credential-free configured provider instance } message PluginTaskLaunchOptions { optional string agent_profile_id = 1; optional string executor_profile_id = 2; optional string prompt = 3; optional string plan_mode = 4; } message CreateTaskResponse { Task task = 1; } message UpdateTaskRequest { string id = 1; optional string title = 2; optional string description = 3; optional string state = 4; optional string workflow_step_id = 5; } message UpdateTaskResponse { Task task = 1; } // SendMessage delivers a prompt to a task session (api_write:messages) through // the orchestrator — the same delivery path message_task uses. The server // records a user message stamped source = "plugin:" and resolves session_id // (empty → the task's primary session), queueing when the session is running // and resuming/starting it otherwise. message SendMessageRequest { string task_id = 1; string session_id = 2; // empty → the task's primary session string text = 3; } message SendMessageResponse { string session_id = 1; // the session the message was delivered to string status = 2; // "queued" | "sent" | "started" } // Deletion is owner-scoped. Host compares metadata.source with calling plugin ID; links // in plugin state are not deletion authority. Preview always precedes destructive UI. message PreviewPluginOwnedTaskTreeRequest { string root_task_id = 1; } message PreviewPluginOwnedTaskTreeResponse { repeated Task tasks = 1; } message DeletePluginOwnedTaskTreeRequest { string root_task_id = 1; } message DeletePluginOwnedTaskTreeResponse { repeated string deleted_task_ids = 1; }