Files
kandev/docs/plans/plugins/HOST-DATA-API.proto
T
João Salavisa 0df2bbac0a feat(plugins): typed capability-gated Host data API (ADR 0043) (#1753)
* docs(plugins): draft Host data API proto (read + write DTOs)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(plugins): ADR + spec + plan for the Host data API

Plugins access kandev data via capability-gated Host gRPC RPCs returning
hand-mapped DTOs, never the DB directly. ADR 0042.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(plugins): add Host data API RPCs and DTOs to plugin proto

Extends the existing service Host (per ADR 0042) with read RPCs
(ListTasks/GetTask, ListWorkspaces, ListWorkflows, ListWorkflowSteps,
ListAgentProfiles, ListRepositories, ListSessions, ListSessionCodeStats)
and declares the deferred phase-2 write RPCs (CreateTask, UpdateTask,
CreateComment) so the contract is stable for later tasks. Regenerates
plugin.pb.go/plugin_grpc.pb.go via `make -C apps/backend proto`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(plugins): Host data API SDK accessors + session LOC aggregation

Wave 1-2 of ADR 0042: pkg/pluginsdk gains typed reader accessors (host.Tasks(),
host.Sessions().CodeStats(), host.Workflows(), ...) over the extended Host gRPC
service, with proto<->native DTO conversions and an UnimplementedHostData seam
for the kandev-side impl. internal/analytics gains the per-session LOC
aggregation those readers surface. Backend handlers + capability gating land in
the next wave.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(plugins): capability-gated Host data read handlers (ADR 0042)

Implement pluginHost Tasks/Sessions/Workspaces/Workflows/AgentProfiles/
Repositories readers: each gated on api_read:<resource> (PermissionDenied
otherwise), backed by the service layer via narrow injected interfaces, and
hand-mapped to the pluginsdk DTOs. Sessions().CodeStats routes through the
analytics aggregation; acp_session_id preserves the metadata->resume_token
fallback. Wired through plugins.Service.SetDataSources + backendapp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(plugins): wire test for Host data reads + docs (ADR 0042 accepted)

End-to-end test over the real go-plugin transport: session data round-trips,
per-resource api_read gating returns PermissionDenied over the wire, cursor
pagination. Document the Host data API + SDK accessors in GRPC-CONTRACT.md,
reconcile the spec to shipped code, mark ADR 0042 accepted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(plugins): full session history + snapshot profile name in data API

Sessions().List now fetches tasks with includeArchived=true — an archived
task's sessions are still real sessions, and CodeStats already made the
same call at the SQL layer, so the two session reads were inconsistent
(26 of 359 sessions visible on a real instance). Tasks().List keeps
archived tasks excluded.

Session gains agent_profile_name (proto field 10) sourced from the
profile snapshot, so sessions of since-uninstalled agents keep a human
label — the AgentProfiles registry only lists live agents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(plugins): renumber Host data API ADR 0042 -> 0043

main merged its own ADR 0042 (project shell output); renumber this one to the
next free slot and update all references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(plugins): address Host data API review (perf, pagination, filters, proto)

- paginate before DTO conversion so per-session ACP lookups run <=limit times
- fetch all source pages (no silent 1000/workspace truncation); accurate HasMore
- stable id tiebreaker on paginated sorts
- AND SessionFilter.TaskIDs with WorkspaceIDs; exclude config-mode sessions from
  CodeStats (shared dialect.ExcludeConfigModePredicate) so List/CodeStats match
- Get returns codes.NotFound consistently in-process and over the wire
- wrap bare-DTO RPC responses (GetTaskResponse etc.) for Buf response uniqueness

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(plugins): mark Host data API plan tasks done

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 13:43:58 +01:00

223 lines
9.6 KiB
Protocol Buffer

// PROPOSED — kandev.plugin.v1 Host data API (draft for review, not wired yet).
//
// Extends the existing capability-gated Host service so plugins read (and, in a
// second phase, 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: ["<resource>"]`; writes require
// `api_write: ["<resource>"]`. 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:<resource>
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 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:<resource>. Route through service methods so
// the corresponding task.* events fire; this is the whole reason not to let
// plugins write the DB.
rpc CreateTask(CreateTaskRequest) returns (CreateTaskResponse);
rpc UpdateTask(UpdateTaskRequest) returns (UpdateTaskResponse);
rpc CreateComment(CreateCommentRequest) returns (CreateCommentResponse);
}
// ── 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;
}
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; }
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 Repository {
string id = 1;
string workspace_id = 2;
string name = 3; // owner/repo slug when known, else name
optional string default_branch = 4;
}
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 (phase 2; 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:<id>" server-side; not settable here.
}
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; }
message Comment {
string id = 1;
string task_id = 2;
string body = 3;
string source = 4; // "plugin:<id>"
string created_at = 5;
}
message CreateCommentRequest { string task_id = 1; string body = 2; }
message CreateCommentResponse { Comment comment = 1; }