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:
@@ -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.
|
||||
@@ -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`
|
||||
|
||||
@@ -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}`. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user