feat(plugins): port agent_conversation Host contract from the maintained fork (#3672)

Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
Alassane
2026-09-15 18:17:41 -04:00
committed by GitHub
parent cb9752b1a2
commit 60aef37cbd
48 changed files with 6109 additions and 157 deletions
@@ -0,0 +1,59 @@
---
created: 2026-09-14
status: completed
requirements:
- REQ-PLUGINS-PLUGINS-001
system_design:
- ../../specs/plugins/system-design/plugins-01.md
legacy_specs: []
---
# Implementation Plan: AgentConversation Host Contract Port
## Overview
Port the maintained `agent_conversation` Host contract to canonical Kandev so a
plugin can manage one hidden conversation per plugin, workspace, and conversation
key through the existing typed gRPC Host channel. The source contract is
`yattdev/kandev` commit `612f46f7a4b206c7cd81863efc4202c123901542`; the target
branch is canonical `main` as it stood at merge base `753e5549e`.
## Scope
The port adds `EnsureAgentConversation`, `DispatchAgentConversation`, and
`DeleteAgentConversation` to `kandev.plugin.v1`, Go-native SDK access through
`pluginsdk.AgentConversations(host)`, and capability-gated Host wiring. `Ensure`
creates or repairs a hidden, workflowless ephemeral task/session; `Dispatch`
records durable occurrence keys so retries do not double-send; `Delete` removes
only conversations owned by the calling plugin. Installation, disable, and
uninstall paths clean up plugin-owned conversations. Quick Chat and its expiry
sweeper exclude those managed sessions.
The port preserves typed `PermissionDenied` for an undeclared
`agent_conversation` capability, replay-safe dispatch statuses, workspace
isolation, pagination, cancellation-safe cleanup, and the published manifest and
authoring guidance. It does not add workspace-agent-principal binding, a general
metadata query API, a user-facing UI, or a schema index unrelated to the scoped
contract.
## Compatibility and rollback
The RPCs and capability are additive. Plugins that do not declare
`capabilities.agent_conversation: true` retain their existing behavior; plugins
that do declare it receive a typed denial when an older Host does not implement
the contract. Rolling back the Host leaves existing managed ephemeral records
inert; normal plugin uninstall or the lifecycle cleanup path removes them.
## Verification
- Regenerate the proto with the repository-pinned toolchain.
- Run focused task-service, plugin, SDK, SQLite repository, and backend-app
tests; run build, vet, and touched-package lint.
- Cover capability denial, idempotent Ensure/Dispatch/Delete, occurrence claims,
cleanup under cancellation, cross-workspace isolation, lifecycle cleanup, and
managed-session Quick Chat and sweeper exclusion.
- Validate the public manifest and authoring references.
## Work orders
- [x] [Task 01: Port the AgentConversation Host contract](task-01-port-agent-conversation-host.md)
@@ -0,0 +1,80 @@
---
id: "01-port-agent-conversation-host"
title: "Port the AgentConversation Host contract"
status: completed
wave: 1
depends_on: []
plan: "plan.md"
requirements:
- REQ-PLUGINS-PLUGINS-001
acceptance_criteria:
- AC-PLUGINS-PLUGINS-001.3
- AC-PLUGINS-PLUGINS-001.7
- AC-PLUGINS-PLUGINS-001.11
system_design:
- ../../specs/plugins/system-design/plugins-01.md
---
# Task 01: Port the AgentConversation Host Contract
## Summary
Port the maintained AgentConversation Host contract into canonical Kandev. The
contract lets a plugin that declares `agent_conversation: true` ensure, dispatch
to, and delete hidden workspace conversations through the existing gRPC Host and
the Go-native SDK manager.
## In scope
- Add the additive `EnsureAgentConversation`, `DispatchAgentConversation`, and
`DeleteAgentConversation` protobuf RPCs and generated stubs.
- Expose typed descriptors, dispatch outcomes, and the optional
`pluginsdk.AgentConversations(host)` manager.
- Gate every RPC on `capabilities.agent_conversation`, returning typed
`PermissionDenied` when it is absent.
- Create, repair, dispatch, and delete plugin-owned hidden ephemeral task/session
records with workspace isolation and idempotent occurrence keys.
- Keep managed conversations out of Quick Chat restoration and expiry; remove
them during plugin lifecycle cleanup.
- Publish the capability and manager contract in manifest and authoring guides.
## Out of scope
- Workspace-agent-principal binding from the maintained fork's optional
`workspace_agent_principals` capability.
- A repository metadata predicate, general metadata index, or unrelated schema
migration.
- A user-facing SPA surface or a Coordinator plugin package change.
## Acceptance
- A plugin with `agent_conversation: true` can ensure a stable hidden
conversation, dispatch a prompt exactly once per occurrence key, and delete
only its own matching conversation.
- A plugin without that capability receives gRPC `PermissionDenied`; no direct
database, shell, REST, MCP, or prompt-derived authority is introduced.
- Managed records are isolated by workspace and plugin ownership, excluded from
Quick Chat and its sweeper, and removed on disable or uninstall.
- The public manifest and authoring documentation describe the capability,
manager, outcomes, and compatibility behavior.
## Verification
```bash
cd apps/backend && go test -tags fts5 -count=1 ./internal/task/service/ ./internal/task/repository/sqlite/ ./internal/plugins/... ./pkg/pluginsdk/ ./internal/backendapp/
cd apps/backend && go build ./... && go vet ./...
cd apps/backend && golangci-lint run ./internal/task/service/... ./internal/task/repository/sqlite/... ./internal/plugins/... ./pkg/pluginsdk/... ./internal/backendapp/...
node --test .github/scripts/pr-docs.test.cjs
git diff --check
```
## Results
- Ported the maintained contract from `612f46f7a4b206c7cd81863efc4202c123901542`
onto canonical-main APIs, including generated proto, SDK, Host, task service,
lifecycle wiring, tests, and public references.
- Added cancellation-safe compensation for dispatch claims and recorded messages,
SQLite cross-connection claim coverage, Quick Chat exclusion coverage, and
stale ephemeral metadata compatibility coverage.
- Focused and touched-package tests, build, vet, lint, documentation checks, and
diff validation passed at the delivery head before PR review follow-up.
+30 -1
View File
@@ -685,6 +685,7 @@ subscription vocabulary and wildcard rules are in the
| Interactions | Interactions().ListPending, Interactions().Get | api_read: interactions | Durable record of agent requests still owed a human answer; Get resolves resolved ones too |
| Interaction responses | Interactions().RespondToPermission, .AnswerClarification, .CancelClarification | api_write: interactions | Routed through the services the native UI drives; first terminal response wins |
| Agent invocation | InvokeUtilityAgent(ctx, prompt, options...) | agent_invoke: true | One-shot completion. No options, or an empty ProfileID, uses the current platform default. A non-empty ProfileID selects that exact eligible profile. An invalid explicit profile returns FailedPrecondition without fallback. The host does not read plugin configuration for selection. |
| Agent conversations | AgentConversations(host): Ensure, Dispatch, Delete | agent_conversation: true | Hidden workflowless ephemeral task/session per plugin, workspace, and conversation key; dispatch occurrence keys are durable and idempotent; uninstall removes every conversation owned by the plugin |
The Go signatures, filters, DTOs, and pagination types live in
apps/backend/pkg/pluginsdk/host.go and data_types.go. api_write task/message
@@ -861,6 +862,33 @@ Pending agent interactions are an additive, optional Host extension too;
discover it the same way with `pluginsdk.Interactions(host)`. See "Pending
agent interactions" below for the contract.
Managed agent conversations are another optional Host extension. They let a
backend plugin keep one hidden agent session for each workspace and stable
conversation key:
```go
conversations, ok := pluginsdk.AgentConversations(host)
if !ok {
return errors.New("this Kandev host does not support agent conversations")
}
descriptor, status, err := conversations.Ensure(ctx, pluginsdk.AgentConversationSpec{
WorkspaceID: workspaceID,
ConversationKey: "coordinator",
BasePrompt: "Review this workspace and report actionable changes.",
AgentProfileID: configuredProfileID,
})
```
Declare `agent_conversation: true` before using the manager. `Ensure` returns
`created`, `exists`, or `configuration_required`; the last result creates no
task or session. `Dispatch` returns `started`, `sent`, `duplicate_occurrence`,
or `skipped_busy`. Supply a stable occurrence key when a scheduled event may be
retried; a busy dispatch does not consume it. `Delete` is idempotent and only
removes conversations owned by the calling plugin. Disabling or uninstalling
the plugin removes all of that plugin's managed conversations across
workspaces.
**Host state** is a small key/value store kandev keeps for your plugin in
its own database. Each entry is addressed by a `(scope, scopeID, key)`
triple and holds a JSON object (`map[string]any`): `SetState` upserts one,
@@ -1005,7 +1033,8 @@ checked against your manifest's `capabilities` before the handler runs:
`GetState`/`SetState`/`DeleteState`/`ListState` require
`capabilities.state: true`; `GetSecret`/`SetSecret`/`DeleteSecret`/
`RevealSecret` require `capabilities.secrets: true`; `InvokeUtilityAgent`
requires `capabilities.agent_invoke: true`; each data-reader accessor requires
requires `capabilities.agent_invoke: true`; managed conversation operations
require `capabilities.agent_conversation: true`; each data-reader accessor requires
its resource in `capabilities.api_read` (e.g. `tasks`, `sessions`, `messages`,
`interactions`, `workspaces`, `workflows`, `agent_profiles`, `repositories`).
Calling one without the declared capability returns gRPC `PermissionDenied`
+2
View File
@@ -52,6 +52,7 @@ capabilities:
state: true
secrets: true
agent_invoke: true # gates Host.InvokeUtilityAgent
agent_conversation: true # gates managed AgentConversations Ensure/Dispatch/Delete
auth: true # gates external (OIDC/SAML) login; see ADR 0050
user_state: true # gates host.storage (per-user browser storage)
@@ -207,6 +208,7 @@ for a complete authoring path.
| `capabilities.state` | no | bool | Gates `Host.GetState`/`SetState`/`DeleteState`/`ListState`. Calling any of them without this set to `true` returns gRPC `PermissionDenied`. |
| `capabilities.secrets` | no | bool | Gates `Host.RevealSecret`/`GetSecret`/`SetSecret`/`DeleteSecret`. Calling any of them without this set to `true` returns gRPC `PermissionDenied`. |
| `capabilities.agent_invoke` | no | bool | Gates `Host.InvokeUtilityAgent`, a one-shot completion. No options, or an empty `UtilityAgentOptions.ProfileID`, uses the current default profile from Settings > Utility Agents. A non-empty profile ID selects that exact eligible global, enabled, non-CLI profile whose agent supports sessionless inference. The host does not read plugin configuration or utility-agent records for selection. Calling without this capability returns gRPC `PermissionDenied`; a missing, deleted, disabled, CLI-passthrough, workspace-scoped, or non-inference explicit profile returns gRPC `FailedPrecondition` without fallback. See [explicit plugin utility selection](../decisions/2026-09-14-explicit-plugin-utility-selection.md). |
| `capabilities.agent_conversation` | no | bool | Gates the optional `pluginsdk.AgentConversations(host)` manager. `Ensure` creates or repairs a hidden ephemeral task/session for one `(plugin, workspace, conversation key)`, `Dispatch` sends an idempotent prompt, and `Delete` removes the matching plugin-owned conversation. Calls without this capability return gRPC `PermissionDenied`. |
| `capabilities.auth` | no | bool | Lets the plugin log a visitor in against an external IdP (OIDC/SAML). Its webhook validates the token, then asserts the identity to Kandev via the `X-Kandev-Auth-Login` response header (`{provider, subject, email, display_name}`); Kandev mints the session and sets the cookie, so the plugin never sees the token. Requires authentication enabled; new users are provisioned as members, and Kandev never creates an admin nor auto-links to an existing admin account. **You MUST only assert an email the IdP verified as owned by the subject; a spoofed email claim is account takeover.** Highest-privilege capability; grant only to trusted plugins. See ADR 0050. |
| `capabilities.user_state` | no | bool | Gates `host.storage` (`get`/`set`/`delete`/`list`/`subscribe`), the authenticated per-user browser storage surface at `/api/plugins/{id}/user-state/...`. Unlike `capabilities.state` (the gRPC `Host.SetState` family, written by the plugin's own backend), this is reachable directly from the plugin's frontend bundle with no Go backend required; every read/write is scoped to the calling user. Calling the route without this capability returns `403`. See [Authoring a plugin](plugins-authoring.md) and the per-user-plugin-storage decision record. |
| `webhooks[].key` | yes | string | Must be unique within the manifest. Used in the relay path `POST /api/plugins/{id}/webhooks/{key}`. |
+6 -3
View File
@@ -203,8 +203,9 @@ binary an operator hasn't explicitly approved via install or Sync.
## Enable, disable, uninstall
- **Disable** stops the subprocess. Config and state are preserved; no
events or webhooks are delivered while disabled.
- **Disable** stops the subprocess and removes the plugin's managed agent
conversations. Config and other state are preserved; no events or webhooks
are delivered while disabled.
- **Enable** respawns the subprocess and re-completes the handshake. It is also
the manual recovery action for an `error` plugin; the Settings row and detail
page show the last failure diagnostic when one is available. A successful
@@ -308,7 +309,9 @@ disk on its first restart after upgrading to this version.
sessions, workspaces, workflows, agent profiles, repositories) is gated
individually via `api_read:<resource>`. Task create/update and message send
are independently gated by `api_write:tasks` and `api_write:messages` and
use Kandev's first-party service paths. An undeclared capability returns
use Kandev's first-party service paths. `agent_conversation` separately gates
plugin-owned hidden agent sessions and their Ensure, Dispatch, and Delete
lifecycle. An undeclared capability returns
gRPC `PermissionDenied` with a message naming the missing capability,
checked before the handler runs. `GetConfig` and `EmitEvent` are the only
ungated RPCs: a plugin can always read its own config (secrets included)