Files
kandev/apps/backend/pkg/pluginsdk/host.go
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

594 lines
23 KiB
Go

// host.go implements the Host side of the kandev.plugin.v1.Host service
// (§3 of docs/plans/plugins/GRPC-CONTRACT.md) in both directions:
//
// - grpcHostClient: used inside the plugin subprocess. Wraps a
// pluginv1.HostClient dialed over the go-plugin broker (see serve.go)
// and satisfies the Go-native Host interface that Serve injects into
// the author's Plugin.
// - grpcHostServer: used inside kandev. Wraps kandev's own Go-native Host
// implementation (state store, secrets, event bus) and satisfies the
// generated pluginv1.HostServer interface so it can be registered on
// the broker-served grpc.Server that GRPCPlugin.GRPCClient spins up
// (see serve.go's "Host injection" section).
//
// Both directions share the same Go-native Host interface and the same
// proto conversion helpers in types.go, so kandev's runtime manager
// implements Host exactly once and gets both the client and server wiring
// for free via GRPCPlugin.
package pluginsdk
import (
"context"
pluginv1 "github.com/kandev/kandev/proto/kandev/plugin/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Host is the set of operations kandev exposes back to a running plugin,
// per §3's Host service. On the plugin side, Serve injects an
// SDK-provided implementation that proxies these calls to kandev over the
// go-plugin broker. On the kandev side, the runtime manager provides its
// own Go-native implementation of this same interface (backed by the real
// state store / secrets / event bus) and hands it to GRPCPlugin.Host; the
// SDK wraps it into the generated pluginv1.HostServer for registration.
type Host interface {
// GetState looks up a single state entry. found is false, err is nil
// when the key does not exist.
GetState(ctx context.Context, scope, scopeID, key string) (value map[string]any, found bool, err error)
// SetState upserts a single state entry.
SetState(ctx context.Context, scope, scopeID, key string, value map[string]any) error
// DeleteState removes a single state entry. Deleting a missing key is
// not an error.
DeleteState(ctx context.Context, scope, scopeID, key string) error
// ListState returns every state entry for a scope/scopeID pair.
ListState(ctx context.Context, scope, scopeID string) ([]StateEntry, error)
// RevealSecret resolves a secret reference to its cleartext value.
RevealSecret(ctx context.Context, ref string) (string, error)
// EmitEvent publishes a plugin-originated event onto kandev's bus.
EmitEvent(ctx context.Context, name string, payload map[string]any) error
// Tasks returns the reader for the Host data API's task RPCs
// (capability api_read:tasks per ADR 0043).
Tasks() TaskReader
// Sessions returns the reader for the Host data API's session and
// session-code-stats RPCs (capability api_read:sessions).
Sessions() SessionReader
// Workspaces returns the reader for the Host data API's workspace RPCs
// (capability api_read:workspaces).
Workspaces() WorkspaceReader
// Workflows returns the reader for the Host data API's workflow and
// workflow-step RPCs (capability api_read:workflows).
Workflows() WorkflowReader
// AgentProfiles returns the reader for the Host data API's agent
// profile RPCs (capability api_read:agent_profiles).
AgentProfiles() AgentProfileReader
// Repositories returns the reader for the Host data API's repository
// RPCs (capability api_read:repositories).
Repositories() RepositoryReader
}
// TaskReader is the read-only accessor behind Host.Tasks(), mirroring the
// Host data API's ListTasks/GetTask RPCs (ADR 0043). Write methods
// (CreateTask/UpdateTask) are deferred to a later phase and intentionally
// not part of this interface yet.
type TaskReader interface {
// List returns tasks matching filter, newest page first per page.
List(ctx context.Context, filter TaskFilter, page Page) ([]Task, *PageInfo, error)
// Get returns a single task by id.
Get(ctx context.Context, id string) (*Task, error)
}
// SessionReader is the read-only accessor behind Host.Sessions(), mirroring
// the Host data API's ListSessions/ListSessionCodeStats RPCs.
type SessionReader interface {
// List returns sessions matching filter.
List(ctx context.Context, filter SessionFilter, page Page) ([]Session, *PageInfo, error)
// CodeStats returns computed per-session code-change stats matching
// filter. SessionCodeStats is a stable, computed shape — never raw
// commit/snapshot rows.
CodeStats(ctx context.Context, filter SessionFilter, page Page) ([]SessionCodeStats, *PageInfo, error)
}
// WorkspaceReader is the read-only accessor behind Host.Workspaces(),
// mirroring the Host data API's ListWorkspaces RPC.
type WorkspaceReader interface {
List(ctx context.Context, page Page) ([]Workspace, *PageInfo, error)
}
// WorkflowReader is the read-only accessor behind Host.Workflows(),
// mirroring the Host data API's ListWorkflows/ListWorkflowSteps RPCs.
type WorkflowReader interface {
// List returns workflows for workspaceID.
List(ctx context.Context, workspaceID string, page Page) ([]Workflow, *PageInfo, error)
// ListSteps returns the steps for workflowID, in position order.
ListSteps(ctx context.Context, workflowID string) ([]WorkflowStep, error)
}
// AgentProfileReader is the read-only accessor behind Host.AgentProfiles(),
// mirroring the Host data API's ListAgentProfiles RPC.
type AgentProfileReader interface {
List(ctx context.Context, page Page) ([]AgentProfile, *PageInfo, error)
}
// RepositoryReader is the read-only accessor behind Host.Repositories(),
// mirroring the Host data API's ListRepositories RPC.
type RepositoryReader interface {
// List returns repositories for workspaceID.
List(ctx context.Context, workspaceID string, page Page) ([]Repository, *PageInfo, error)
}
// newHostClient wraps a *grpc.ClientConn (dialed over the go-plugin broker)
// as a Go-native Host implementation.
func newHostClient(conn *grpc.ClientConn) Host {
return &grpcHostClient{client: pluginv1.NewHostClient(conn)}
}
type grpcHostClient struct {
client pluginv1.HostClient
}
func (h *grpcHostClient) GetState(ctx context.Context, scope, scopeID, key string) (map[string]any, bool, error) {
resp, err := h.client.GetState(ctx, &pluginv1.GetStateRequest{Scope: scope, ScopeId: scopeID, Key: key})
if err != nil {
return nil, false, err
}
if !resp.GetFound() {
return nil, false, nil
}
value, err := structToMap(resp.GetValue())
if err != nil {
return nil, false, err
}
return value, true, nil
}
func (h *grpcHostClient) SetState(ctx context.Context, scope, scopeID, key string, value map[string]any) error {
protoValue, err := mapToStruct(value)
if err != nil {
return err
}
_, err = h.client.SetState(ctx, &pluginv1.SetStateRequest{Scope: scope, ScopeId: scopeID, Key: key, Value: protoValue})
return err
}
func (h *grpcHostClient) DeleteState(ctx context.Context, scope, scopeID, key string) error {
_, err := h.client.DeleteState(ctx, &pluginv1.DeleteStateRequest{Scope: scope, ScopeId: scopeID, Key: key})
return err
}
func (h *grpcHostClient) ListState(ctx context.Context, scope, scopeID string) ([]StateEntry, error) {
resp, err := h.client.ListState(ctx, &pluginv1.ListStateRequest{Scope: scope, ScopeId: scopeID})
if err != nil {
return nil, err
}
return stateEntriesFromProto(resp.GetEntries())
}
func (h *grpcHostClient) RevealSecret(ctx context.Context, ref string) (string, error) {
resp, err := h.client.RevealSecret(ctx, &pluginv1.RevealSecretRequest{Ref: ref})
if err != nil {
return "", err
}
return resp.GetValue(), nil
}
func (h *grpcHostClient) EmitEvent(ctx context.Context, name string, payload map[string]any) error {
protoPayload, err := mapToStruct(payload)
if err != nil {
return err
}
_, err = h.client.EmitEvent(ctx, &pluginv1.EmitEventRequest{EventName: name, Payload: protoPayload})
return err
}
func (h *grpcHostClient) Tasks() TaskReader { return grpcTaskReader{client: h.client} }
func (h *grpcHostClient) Sessions() SessionReader { return grpcSessionReader{client: h.client} }
func (h *grpcHostClient) Workspaces() WorkspaceReader { return grpcWorkspaceReader{client: h.client} }
func (h *grpcHostClient) Workflows() WorkflowReader { return grpcWorkflowReader{client: h.client} }
func (h *grpcHostClient) AgentProfiles() AgentProfileReader {
return grpcAgentProfileReader{client: h.client}
}
func (h *grpcHostClient) Repositories() RepositoryReader {
return grpcRepositoryReader{client: h.client}
}
var _ Host = (*grpcHostClient)(nil)
// grpcTaskReader implements TaskReader on the plugin side, calling the
// generated pluginv1.HostClient and converting proto<->Go-native.
type grpcTaskReader struct {
client pluginv1.HostClient
}
func (r grpcTaskReader) List(ctx context.Context, filter TaskFilter, page Page) ([]Task, *PageInfo, error) {
resp, err := r.client.ListTasks(ctx, &pluginv1.ListTasksRequest{Filter: filter.toProto(), Page: page.toProto()})
if err != nil {
return nil, nil, err
}
tasks, err := tasksFromProto(resp.GetTasks())
if err != nil {
return nil, nil, err
}
return tasks, pageInfoFromProto(resp.GetPageInfo()), nil
}
func (r grpcTaskReader) Get(ctx context.Context, id string) (*Task, error) {
resp, err := r.client.GetTask(ctx, &pluginv1.GetTaskRequest{Id: id})
if err != nil {
return nil, err
}
task, err := taskFromProto(resp.GetTask())
if err != nil {
return nil, err
}
return &task, nil
}
// grpcSessionReader implements SessionReader on the plugin side.
type grpcSessionReader struct {
client pluginv1.HostClient
}
func (r grpcSessionReader) List(ctx context.Context, filter SessionFilter, page Page) ([]Session, *PageInfo, error) {
resp, err := r.client.ListSessions(ctx, &pluginv1.ListSessionsRequest{Filter: filter.toProto(), Page: page.toProto()})
if err != nil {
return nil, nil, err
}
return sessionsFromProto(resp.GetSessions()), pageInfoFromProto(resp.GetPageInfo()), nil
}
func (r grpcSessionReader) CodeStats(ctx context.Context, filter SessionFilter, page Page) ([]SessionCodeStats, *PageInfo, error) {
resp, err := r.client.ListSessionCodeStats(ctx, &pluginv1.ListSessionCodeStatsRequest{Filter: filter.toProto(), Page: page.toProto()})
if err != nil {
return nil, nil, err
}
return sessionCodeStatsSliceFromProto(resp.GetStats()), pageInfoFromProto(resp.GetPageInfo()), nil
}
// grpcWorkspaceReader implements WorkspaceReader on the plugin side.
type grpcWorkspaceReader struct {
client pluginv1.HostClient
}
func (r grpcWorkspaceReader) List(ctx context.Context, page Page) ([]Workspace, *PageInfo, error) {
resp, err := r.client.ListWorkspaces(ctx, &pluginv1.ListWorkspacesRequest{Page: page.toProto()})
if err != nil {
return nil, nil, err
}
return workspacesFromProto(resp.GetWorkspaces()), pageInfoFromProto(resp.GetPageInfo()), nil
}
// grpcWorkflowReader implements WorkflowReader on the plugin side.
type grpcWorkflowReader struct {
client pluginv1.HostClient
}
func (r grpcWorkflowReader) List(ctx context.Context, workspaceID string, page Page) ([]Workflow, *PageInfo, error) {
resp, err := r.client.ListWorkflows(ctx, &pluginv1.ListWorkflowsRequest{WorkspaceId: workspaceID, Page: page.toProto()})
if err != nil {
return nil, nil, err
}
return workflowsFromProto(resp.GetWorkflows()), pageInfoFromProto(resp.GetPageInfo()), nil
}
func (r grpcWorkflowReader) ListSteps(ctx context.Context, workflowID string) ([]WorkflowStep, error) {
resp, err := r.client.ListWorkflowSteps(ctx, &pluginv1.ListWorkflowStepsRequest{WorkflowId: workflowID})
if err != nil {
return nil, err
}
return workflowStepsFromProto(resp.GetSteps()), nil
}
// grpcAgentProfileReader implements AgentProfileReader on the plugin side.
type grpcAgentProfileReader struct {
client pluginv1.HostClient
}
func (r grpcAgentProfileReader) List(ctx context.Context, page Page) ([]AgentProfile, *PageInfo, error) {
resp, err := r.client.ListAgentProfiles(ctx, &pluginv1.ListAgentProfilesRequest{Page: page.toProto()})
if err != nil {
return nil, nil, err
}
return agentProfilesFromProto(resp.GetProfiles()), pageInfoFromProto(resp.GetPageInfo()), nil
}
// grpcRepositoryReader implements RepositoryReader on the plugin side.
type grpcRepositoryReader struct {
client pluginv1.HostClient
}
func (r grpcRepositoryReader) List(ctx context.Context, workspaceID string, page Page) ([]Repository, *PageInfo, error) {
resp, err := r.client.ListRepositories(ctx, &pluginv1.ListRepositoriesRequest{WorkspaceId: workspaceID, Page: page.toProto()})
if err != nil {
return nil, nil, err
}
return repositoriesFromProto(resp.GetRepositories()), pageInfoFromProto(resp.GetPageInfo()), nil
}
// registerHostServer registers a grpc server that dispatches
// kandev.plugin.v1.Host RPCs to impl (kandev's Go-native Host
// implementation), converting proto<->Go-native types at the boundary.
func registerHostServer(s grpc.ServiceRegistrar, impl Host) {
pluginv1.RegisterHostServer(s, &grpcHostServer{impl: impl})
}
type grpcHostServer struct {
pluginv1.UnimplementedHostServer
impl Host
}
func (s *grpcHostServer) GetState(ctx context.Context, req *pluginv1.GetStateRequest) (*pluginv1.GetStateResponse, error) {
value, found, err := s.impl.GetState(ctx, req.GetScope(), req.GetScopeId(), req.GetKey())
if err != nil {
return nil, err
}
if !found {
return &pluginv1.GetStateResponse{Found: false}, nil
}
protoValue, err := mapToStruct(value)
if err != nil {
return nil, err
}
return &pluginv1.GetStateResponse{Found: true, Value: protoValue}, nil
}
func (s *grpcHostServer) SetState(ctx context.Context, req *pluginv1.SetStateRequest) (*pluginv1.SetStateResponse, error) {
value, err := structToMap(req.GetValue())
if err != nil {
return nil, err
}
if err := s.impl.SetState(ctx, req.GetScope(), req.GetScopeId(), req.GetKey(), value); err != nil {
return nil, err
}
return &pluginv1.SetStateResponse{}, nil
}
func (s *grpcHostServer) DeleteState(ctx context.Context, req *pluginv1.DeleteStateRequest) (*pluginv1.DeleteStateResponse, error) {
if err := s.impl.DeleteState(ctx, req.GetScope(), req.GetScopeId(), req.GetKey()); err != nil {
return nil, err
}
return &pluginv1.DeleteStateResponse{}, nil
}
func (s *grpcHostServer) ListState(ctx context.Context, req *pluginv1.ListStateRequest) (*pluginv1.ListStateResponse, error) {
entries, err := s.impl.ListState(ctx, req.GetScope(), req.GetScopeId())
if err != nil {
return nil, err
}
protoEntries := make([]*pluginv1.StateEntry, len(entries))
for i := range entries {
converted, err := entries[i].toProto()
if err != nil {
return nil, err
}
protoEntries[i] = converted
}
return &pluginv1.ListStateResponse{Entries: protoEntries}, nil
}
func (s *grpcHostServer) RevealSecret(ctx context.Context, req *pluginv1.RevealSecretRequest) (*pluginv1.RevealSecretResponse, error) {
value, err := s.impl.RevealSecret(ctx, req.GetRef())
if err != nil {
return nil, err
}
return &pluginv1.RevealSecretResponse{Value: value}, nil
}
func (s *grpcHostServer) EmitEvent(ctx context.Context, req *pluginv1.EmitEventRequest) (*pluginv1.EmitEventResponse, error) {
payload, err := structToMap(req.GetPayload())
if err != nil {
return nil, err
}
if err := s.impl.EmitEvent(ctx, req.GetEventName(), payload); err != nil {
return nil, err
}
return &pluginv1.EmitEventResponse{}, nil
}
// ── Host data API reads (ADR 0043) ──────────────────────────────────────
//
// Each method below dispatches to the injected Go-native impl's resource
// accessor (impl.Tasks(), impl.Sessions(), ...) and converts proto<->native
// at the boundary, exactly like GetState/ListState above. Capability
// gating and the real service-layer calls live in the impl kandev's
// runtime manager provides (internal/plugins), not here — this adapter
// only wires the RPC to whatever impl.Tasks()/impl.Sessions()/... does, so
// it compiles against any Host, including one that embeds
// UnimplementedHostData and returns Unimplemented for every accessor.
func (s *grpcHostServer) ListTasks(ctx context.Context, req *pluginv1.ListTasksRequest) (*pluginv1.ListTasksResponse, error) {
filter := taskFilterFromProto(req.GetFilter())
page := pageFromProto(req.GetPage())
tasks, pageInfo, err := s.impl.Tasks().List(ctx, filter, page)
if err != nil {
return nil, err
}
protoTasks, err := tasksToProto(tasks)
if err != nil {
return nil, err
}
return &pluginv1.ListTasksResponse{Tasks: protoTasks, PageInfo: pageInfo.toProto()}, nil
}
// GetTask dispatches to the impl's TaskReader.Get and wraps the result in
// GetTaskResponse (Buf RPC_RESPONSE_STANDARD_NAME/RPC_REQUEST_RESPONSE_UNIQUE:
// no bare-DTO RPC responses). The Host contract is that a missing task is a
// gRPC NotFound *error* from Get, never a (nil, nil) success — the nil check
// below is defense-in-depth for a Host implementation that doesn't follow
// that contract, so a plugin still gets NotFound instead of a zero-value
// GetTaskResponse.
func (s *grpcHostServer) GetTask(ctx context.Context, req *pluginv1.GetTaskRequest) (*pluginv1.GetTaskResponse, error) {
task, err := s.impl.Tasks().Get(ctx, req.GetId())
if err != nil {
return nil, err
}
if task == nil {
return nil, status.Error(codes.NotFound, "task not found")
}
protoTask, err := task.toProto()
if err != nil {
return nil, err
}
return &pluginv1.GetTaskResponse{Task: protoTask}, nil
}
func (s *grpcHostServer) ListWorkspaces(ctx context.Context, req *pluginv1.ListWorkspacesRequest) (*pluginv1.ListWorkspacesResponse, error) {
page := pageFromProto(req.GetPage())
workspaces, pageInfo, err := s.impl.Workspaces().List(ctx, page)
if err != nil {
return nil, err
}
return &pluginv1.ListWorkspacesResponse{Workspaces: workspacesToProto(workspaces), PageInfo: pageInfo.toProto()}, nil
}
func (s *grpcHostServer) ListWorkflows(ctx context.Context, req *pluginv1.ListWorkflowsRequest) (*pluginv1.ListWorkflowsResponse, error) {
page := pageFromProto(req.GetPage())
workflows, pageInfo, err := s.impl.Workflows().List(ctx, req.GetWorkspaceId(), page)
if err != nil {
return nil, err
}
return &pluginv1.ListWorkflowsResponse{Workflows: workflowsToProto(workflows), PageInfo: pageInfo.toProto()}, nil
}
func (s *grpcHostServer) ListWorkflowSteps(ctx context.Context, req *pluginv1.ListWorkflowStepsRequest) (*pluginv1.ListWorkflowStepsResponse, error) {
steps, err := s.impl.Workflows().ListSteps(ctx, req.GetWorkflowId())
if err != nil {
return nil, err
}
return &pluginv1.ListWorkflowStepsResponse{Steps: workflowStepsToProto(steps)}, nil
}
func (s *grpcHostServer) ListAgentProfiles(ctx context.Context, req *pluginv1.ListAgentProfilesRequest) (*pluginv1.ListAgentProfilesResponse, error) {
page := pageFromProto(req.GetPage())
profiles, pageInfo, err := s.impl.AgentProfiles().List(ctx, page)
if err != nil {
return nil, err
}
return &pluginv1.ListAgentProfilesResponse{Profiles: agentProfilesToProto(profiles), PageInfo: pageInfo.toProto()}, nil
}
func (s *grpcHostServer) ListRepositories(ctx context.Context, req *pluginv1.ListRepositoriesRequest) (*pluginv1.ListRepositoriesResponse, error) {
page := pageFromProto(req.GetPage())
repos, pageInfo, err := s.impl.Repositories().List(ctx, req.GetWorkspaceId(), page)
if err != nil {
return nil, err
}
return &pluginv1.ListRepositoriesResponse{Repositories: repositoriesToProto(repos), PageInfo: pageInfo.toProto()}, nil
}
func (s *grpcHostServer) ListSessions(ctx context.Context, req *pluginv1.ListSessionsRequest) (*pluginv1.ListSessionsResponse, error) {
filter := sessionFilterFromProto(req.GetFilter())
page := pageFromProto(req.GetPage())
sessions, pageInfo, err := s.impl.Sessions().List(ctx, filter, page)
if err != nil {
return nil, err
}
return &pluginv1.ListSessionsResponse{Sessions: sessionsToProto(sessions), PageInfo: pageInfo.toProto()}, nil
}
func (s *grpcHostServer) ListSessionCodeStats(ctx context.Context, req *pluginv1.ListSessionCodeStatsRequest) (*pluginv1.ListSessionCodeStatsResponse, error) {
filter := sessionFilterFromProto(req.GetFilter())
page := pageFromProto(req.GetPage())
stats, pageInfo, err := s.impl.Sessions().CodeStats(ctx, filter, page)
if err != nil {
return nil, err
}
return &pluginv1.ListSessionCodeStatsResponse{Stats: sessionCodeStatsSliceToProto(stats), PageInfo: pageInfo.toProto()}, nil
}
var _ pluginv1.HostServer = (*grpcHostServer)(nil)
// UnimplementedHostData is an embeddable default for the Host data API
// (ADR 0043) sub-accessors: Tasks/Sessions/Workspaces/Workflows/
// AgentProfiles/Repositories. Embed it in a Go-native Host implementation
// to satisfy the interface before wiring real data access — every method
// on every returned reader returns a gRPC Unimplemented error. Override
// individual accessor methods (e.g. define your own Tasks() on the
// embedding type) as real capability-gated, service-backed logic lands;
// unlike UnimplementedPlugin's per-RPC methods, these are per-resource
// accessors because each one fans out to multiple reader methods.
type UnimplementedHostData struct{}
func (UnimplementedHostData) Tasks() TaskReader { return unimplementedTaskReader{} }
func (UnimplementedHostData) Sessions() SessionReader { return unimplementedSessionReader{} }
func (UnimplementedHostData) Workspaces() WorkspaceReader { return unimplementedWorkspaceReader{} }
func (UnimplementedHostData) Workflows() WorkflowReader { return unimplementedWorkflowReader{} }
func (UnimplementedHostData) AgentProfiles() AgentProfileReader {
return unimplementedAgentProfileReader{}
}
func (UnimplementedHostData) Repositories() RepositoryReader {
return unimplementedRepositoryReader{}
}
func errUnimplementedHostData(resource string) error {
return status.Errorf(codes.Unimplemented, "pluginsdk: Host data API %q not implemented", resource)
}
type unimplementedTaskReader struct{}
func (unimplementedTaskReader) List(context.Context, TaskFilter, Page) ([]Task, *PageInfo, error) {
return nil, nil, errUnimplementedHostData("tasks")
}
func (unimplementedTaskReader) Get(context.Context, string) (*Task, error) {
return nil, errUnimplementedHostData("tasks")
}
type unimplementedSessionReader struct{}
func (unimplementedSessionReader) List(context.Context, SessionFilter, Page) ([]Session, *PageInfo, error) {
return nil, nil, errUnimplementedHostData("sessions")
}
func (unimplementedSessionReader) CodeStats(context.Context, SessionFilter, Page) ([]SessionCodeStats, *PageInfo, error) {
return nil, nil, errUnimplementedHostData("sessions")
}
type unimplementedWorkspaceReader struct{}
func (unimplementedWorkspaceReader) List(context.Context, Page) ([]Workspace, *PageInfo, error) {
return nil, nil, errUnimplementedHostData("workspaces")
}
type unimplementedWorkflowReader struct{}
func (unimplementedWorkflowReader) List(context.Context, string, Page) ([]Workflow, *PageInfo, error) {
return nil, nil, errUnimplementedHostData("workflows")
}
func (unimplementedWorkflowReader) ListSteps(context.Context, string) ([]WorkflowStep, error) {
return nil, errUnimplementedHostData("workflows")
}
type unimplementedAgentProfileReader struct{}
func (unimplementedAgentProfileReader) List(context.Context, Page) ([]AgentProfile, *PageInfo, error) {
return nil, nil, errUnimplementedHostData("agent_profiles")
}
type unimplementedRepositoryReader struct{}
func (unimplementedRepositoryReader) List(context.Context, string, Page) ([]Repository, *PageInfo, error) {
return nil, nil, errUnimplementedHostData("repositories")
}