Files
kandev/docs/decisions/2026-07-31-authenticated-plugin-actions.md
T

3.9 KiB

ADR-2026-07-31-authenticated-plugin-actions: Authenticated Plugin Actions

Status: accepted Date: 2026-07-31 Area: backend, frontend, protocol, security

Context

The frontend plugin host currently maps plugin fetches toward /api/plugins/:id/..., while the host plugin endpoint is an externally callable webhook proxy. Reusing that public callback surface for browser commands would bypass normal user authentication, resource authorization, request limits, and cancellation controls.

Repository-provider plugins need browser-triggered connection, browse, review, and watch operations without turning each provider into a first-party HTTP route.

Decision

The manifest declares plugin action keys, each with canonical field scope set to workspace, task, or repository, plus a bounded body size. Kandev continues to read the prerelease resource_scope spelling for package compatibility, but new manifests and serialized records use scope. Kandev exposes one authenticated route: POST /api/plugins/:id/actions/:key. It rejects inactive plugins and undeclared actions; normal Kandev authentication and server-side authorization verify every referenced resource before the host dispatches Plugin.HandleAction.

The host derives task/workspace relationships itself, forwards verified actor and resource context separately from untrusted action JSON, applies a hard timeout and cancellation, caps request/response bodies, and permits only a small response-header allowlist. The frontend contract is host.api.invokeAction(...); plugin code must not use public webhook paths for authenticated browser work. Resource selectors use the documented camelCase JSON envelope (workspaceId, taskId, repositoryId). Callers may pass an AbortSignal; registry/provider teardown propagates it through the browser request and backend context.

PluginActionResponse.status is an optional HTTP status projection. Zero keeps the legacy 200 OK; otherwise the host accepts statuses from 200 through 599. This lets provider-neutral adapters preserve safe domain outcomes such as invalid input, authentication expiry, conflicts, and rate limits without turning them into transport failures. Retry-After joins the response-header allowlist for throttling responses. Invalid statuses fail closed as 502, while plugin transport/runtime failures remain 503 and do not expose internal error text.

The Go SDK defines stable typed action categories (ActionErrorCode) and shared HTTP projection (ActionErrorHTTPStatus). Plugins preserve causes with CategorizeActionError/CategorizeActionErrorWithRetry and map only typed provider or domain failures. Message-substring classification is outside the contract because it changes under wrapping/localization and can misreport authorization, conflict, or retry behavior.

/api/plugins/:id/webhooks/:key remains public only for provider callbacks/events. OAuth callbacks use signed, expiring, single-use state and PKCE.

Consequences

Future plugins gain one reusable browser-to-plugin RPC seam without Kandev learning provider endpoints or payloads. Plugin authors declare each action up front and must design against explicit resource scope and bounded input/output. The host gains a security-critical authorization boundary, but no provider-specific handler branch. Callers can distinguish retryable and user-correctable domain outcomes while the host continues to redact unexpected implementation failures.

Alternatives Considered

  • Reuse public webhook proxy for browser calls: rejected because callback endpoints do not establish user identity or authorize Kandev resources.
  • Add provider-specific REST routes in the host: rejected because every connector would pull provider payloads and authentication into core.
  • Let frontend bundles call plugin processes directly: rejected because managed plugin transport is not a browser-accessible authorization boundary.