main
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cc33b2a813 |
fix(plugins): support direct agent profile invocation (#2870)
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com> |
||
|
|
975c03fe3b | fix(plugins): resolve first-use repository task selections on server (#3068) | ||
|
|
355fc601ea | chore(plugins): add Bitbucket to marketplace (#2672) | ||
|
|
f218880ecb | feat(plugins): add source-control extension contracts (#2117) | ||
|
|
4257307063 |
feat: expose plugin agent tools through task MCP (#2552)
Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
e68d3f1d8f | docs(plugins): improve plugin authoring guidance (#2164) | ||
|
|
279e15b318 |
feat(plugins): implement Host data API write RPCs (CreateTask/UpdateTask/SendMessage) (#1970)
* feat(plugins): implement Host data API write RPCs (CreateTask/UpdateTask/CreateComment) The api_write manifest capability was a stored no-op. Wire the deferred phase-2 write RPCs (ADR 0043) so closed-loop plugins (Sentry->task, Slack slash-command task creation, refactor fan-out) can create and update tasks and post comments. - SDK: Host.Tasks() gains Create/Update; new Host.Comments() writer; CreateTaskInput/UpdateTaskInput/Comment DTOs (pkg/pluginsdk). - Gating: api_write:tasks gates Create/UpdateTask, api_write:comments gates CreateComment; undeclared -> gRPC PermissionDenied, per-method and independent of the read caps on the same resource. - Writes route through the first-party service layer (task service for tasks, office service for comments) via backendapp adapters, never a repository -- so task.*/comment-created events fire and WS updates. Server stamps source="plugin:<id>"; CreateTask resolves workspace/ workflow defaults and best-effort honors start_agent; UpdateTask uses a conservative title/description/state/workflow_step_id mask. - Fixture plugins exercise a CreateTask+CreateComment round-trip; in-process, real-broker wire, and subprocess tests cover the round-trip and per-capability denial. - Docs: spec, GRPC-CONTRACT, HOST-DATA-API.proto, proto comments moved writes deferred -> implemented; ADR 0043 amended with the concrete service-layer routing decision. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(plugins): send a message to a task session via Host SendMessage (replaces CreateComment) The office TaskComment write was the wrong primitive — it's an office/ dashboard construct, not a way to message the agent working a task, and it only did anything under office reactivity. Replace the CreateComment write RPC with SendMessage, which delivers a prompt to a task session through the orchestrator's real delivery path (the same one message_task uses). - Proto: swap CreateComment/Comment for SendMessage(SendMessageRequest) returns SendMessageResponse{session_id, status}; regenerated stubs. - SDK: Host.Comments() removed; Host.Messages().Send(taskID, sessionID, text) added (pairs with Messages().List), returning MessageDispatch. - Gating: api_write:comments dropped; SendMessage gated api_write:messages (independent of api_read:messages on the same accessor, like Tasks()). - Delivery: a new pluginsTaskMessengerAdapter (backendapp) resolves the target session (explicit id verified against the task, or the primary session), records a user message stamped source="plugin:<id>", then dispatches by state — running → queue; idle/completed → prompt (resume if the agent process is gone); never-started → launch; failed dispatch deletes the recorded message. Wired via SetWriteDeps (live-read). - Tests: adapter state-machine unit tests (queue/start/prompt/resume/ delete-on-failure/belongs-to-task/no-primary/terminal); SDK, in-process gating, wire, and subprocess round-trips updated to SendMessage. - Docs: spec, GRPC-CONTRACT, HOST-DATA-API.proto, proto comments, and the ADR 0043 amendment updated from comment-create to message-send. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(plugins): address PR review — nil guards, detached contexts, state validation Follow-up fixes from the #1970 review (Claude + CodeRabbit bots): - grpcHostServer.CreateTask/UpdateTask/SendMessage: nil-guard the returned value before toProto() so a custom/test Host returning (nil, nil) yields a gRPC Internal error, not a server panic (matches GetTask's pattern). - host.go: fix stale server doc comment (Comments()/CreateComment → Messages()/SendMessage). - startTaskBestEffort and the post-resume wait+retry now run on context.WithoutCancel(ctx): the task/resume is already committed, so a plugin's incoming deadline must not silently abort the launch/resume. - pluginsTaskWriterAdapter.UpdateTask validates the state string against the known task-state enum (excluding orchestrator-owned SCHEDULING) and returns InvalidArgument, so a plugin typo can't forward a bogus state to the service. - Tests: add the resume-wait-timeout path (message deleted so a retry can't duplicate), and a dedicated pluginsTaskWriterAdapter test covering source→metadata mapping and state validation (both adapters now depend on narrow interfaces for testability). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(plugins): launch start_agent asynchronously so CreateTask doesn't block Review nit follow-up: startTaskBestEffort called StartTask synchronously, so with the detached context the plugin's CreateTask RPC blocked on the orchestrator's full inline launch path (office lookup, state update, workflow step move, task fetch) with no bound — and the "asynchronous" comment was wrong. Move the fire-and-forget into pluginsTaskStarterAdapter (backendapp, no goleak), mirroring MCP's launchAutoStartTask: the launch runs in a goroutine on a context.WithTimeout(context.WithoutCancel(ctx), AgentLaunchTimeout), and StartTask returns immediately. The plugin host stays a simple synchronous call (its taskStarter contract now documents "returns promptly, launches async"). Adds adapters_plugin_starter_test.go: launch invoked, returns before the launch completes, detaches from a cancelled caller context, and swallows launch errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(plugins): document write validation + SendMessage rollback; harden starter test cleanup Addresses the final PR #1970 review nits: - adapters_plugin_starter_test.go: register the block-channel release via t.Cleanup (sync.Once-guarded) before the goroutine can block, per AGENTS.md. - GRPC-CONTRACT.md: document CreateTask/UpdateTask validation + error rules (title required, id required, NotFound on missing task, invalid-state reject). - spec.md: document that a failed SendMessage dispatch deletes the recorded message, plus the required-field/NotFound/terminal-session rules. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1ba81a7cd0 |
refactor(plugins): remove the unused tools feature (#1776)
Plugin `tools[]` overlapped with MCP, kandev's established mechanism for exposing tools to agents (`internal/mcp/`), and the `InvokeTool` path was never wired into agent tool sets — the `listTools` handler was listing-only. Rather than maintain a half-built parallel surface, remove it. If plugins ever contribute agent tools, they should feed through MCP, not a separate `InvokeTool` RPC. Removes: - proto `InvokeTool` RPC + `ToolRequest`/`ToolResponse`/`ToolContext` messages (regenerated `*.pb.go` / `*_grpc.pb.go`) - pkg/pluginsdk `InvokeTool` from the `Plugin` interface and `UnimplementedPlugin`, the Tool* Go-native types, and the `RemotePlugin`/`GRPCServer` dispatch - internal/plugins `Service.InvokeTool`, the `listTools` handler + `GET /api/plugins/tools` route, `PluginToolDTO`, and the manifest `Tools` field + tool-name uniqueness validation - frontend `PluginTool`/`PluginToolDTO` types, `listPluginTools`, and the manifest-card tools list - fixture/e2e `InvokeTool` echo impl and the `tools:` manifest block Test doubles that used the "crash" tool to exercise crash-restart now use a "crash" webhook key instead. Internal specs (GRPC-CONTRACT, spec) drop the tool RPC/feature; docs/public was already updated in #1774. Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fae2303db3 |
feat(plugins): per-plugin settings pages driven by manifest config_schema (#1761)
* feat(plugins): per-plugin settings pages driven by manifest config_schema Settings > Plugins rows now open /settings/plugins/:id — a detail page showing the manifest plus a settings form generated from the plugin author's config_schema. Secret fields (secret: true / format: password) are masked on the operator API and kept on masked re-submits; a running plugin restarts on save and reads its config (cleartext secrets included) through the new Host GetConfig gRPC RPC. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): write plugin config file owner-only (0600) Operator config can now carry cleartext secret values (config_schema fields marked secret), so <id>.config.yml must not be world-readable — match the 0600 mode the record file already uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(plugins): vault-backed plugin secrets — Get/Set/DeleteSecret RPCs and encrypted config secrets Plugins get vault-backed secret primitives on the Host API (GetSecret/SetSecret/DeleteSecret, capability-gated, namespaced to plugin:<id>㊙️<key> so a plugin can only touch its own entries; purged on uninstall). Secret config_schema fields now live in kandev's AES-256-GCM vault too — <id>.config.yml persists only a vault reference, and the Host GetConfig RPC resolves it back to cleartext for the plugin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(plugins): serve PR screenshots from the media/pr-1761-screenshots ref instead of the repo The PR body now points at raw URLs pinned to an unmerged media ref, so the binaries never land in main (PRs are squash-merged; only the branch tip tree merges). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): address review feedback on settings validation and save flow - integer form fields reject non-integral input instead of parseInt-truncating - enum selections keep their schema-declared types (numeric enums round-trip) - backend enum validation normalizes int-vs-float64 before comparison - maskSecrets masks any non-zero secret value regardless of runtime type - save flow splits PATCH from refetch: a refetch failure no longer reports a successful save as an error, and typed secrets are masked on screen - optional enum fields gain a 'Not set' option so they can be cleared - resolveConfigSecrets copies before mutating; restart-failure SetStatus errors are logged; SDK test fake returns a non-nil empty config map - add missing tests: usePluginConfigForm hook, getPluginConfig client, integer/enum regression cases Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): harden secret config invariants from review - reject non-string values on declared secret fields at validation time, closing the path where a numeric/boolean secret bypassed the vault and persisted cleartext in the config file - delete removed secret fields' vault entries only AFTER the config commit succeeds, so a failed SetConfig never leaves the still-current config referencing a deleted vault entry - uninstall fails closed: the plugin's vault namespace is purged before any destructive step, and a cleanup failure aborts the uninstall (retryable) instead of letting a reinstall inherit stale secrets Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): failure-atomic config secrets, quiesced uninstall purge, ctx propagation - UpdateConfig is now failure-atomic for secret overwrites: storeConfigSecrets snapshots each vault entry before writing and returns a rollback that restores prior values (or deletes newly-created ones). A vault.Set failure mid-loop or a failed SetConfig commit rolls back, so a request reported as failed never changes the value a still-current config ref resolves to. - Uninstall stops the plugin process BEFORE purging its vault namespace, so the plugin can no longer race the cleanup with a SetSecret between ListIDs and the deletes; the purge stays fail-closed and retryable. - Thread the request context through Uninstall -> deletePluginSecrets instead of context.Background(), per the pass-context-through-chains convention. - Consolidate the 'secret not found:' prefix check into isSecretNotFound (host.go + service rollback share one definition). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): detach rollback context, fail closed without a vault, reconcile uninstall status - vault rollback closures now run on context.WithoutCancel(ctx): a request cancelled mid-save (browser closed) can no longer abort the rollback and leave the vault holding new values while the unchanged config still points at them (Greptile P1). - storeConfigSecrets fails closed (errSecretVaultRequired) when a plugin declares secret config fields but no vault is wired, instead of silently persisting the secret in cleartext. - vaultRestoreFunc distinguishes not-found from a genuine Reveal error: a backend read failure aborts the update before writing rather than risk a rollback that deletes a real secret; rollback now returns errors, joined into UpdateConfig's failure so an inconsistent vault is surfaced, not just logged. - Uninstall reconciles lifecycle status when a secret-purge failure aborts after the process was stopped: a previously-running plugin transitions to error and notifies observers instead of being reported active with no process running. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): detach removed-secret cleanup ctx and clear stale save dirty flag - cleanupRemovedConfigSecrets now runs on context.WithoutCancel(ctx), matching the rollback path, so a client disconnect right after the config commit can no longer cancel the delete and orphan now-unreferenced vault entries. - tests: assert GetConfig succeeds before treating an empty map as proof that nothing persisted (a swallowed read error would be a false pass). - web: on a post-save refetch failure, rebase initialValues onto the masked snapshot so the saved form no longer reads as dirty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): fail tests on discarded GetConfig errors, drop setState-in-updater - config_test: capture and fatal on GetConfig errors at the four remaining positive-assertion sites (a nil from a failed read would panic on the next map access instead of giving a clear failure), matching the earlier fix. - web hook: compute the masked post-save snapshot from the current values and apply it to setValues + setInitialValues directly, instead of calling setInitialValues inside a setValues functional updater (a React anti-pattern that runs twice under StrictMode). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): skip validation of a secret field's own vault ref - validateConfigSchema takes the plugin id and skips a secret field whose value is that field's config vault ref (an internal storage marker, already validated when first set). Without this, a field declaring both secret: true and enum: [...] 400s on every save that keeps the mask, because the enum check saw the vault ref instead of a user value. Freshly entered secret values are still validated; a foreign plugin's ref is not skipped. Covered by unit + service-level regression tests. - surface mid-loop vault rollback failures in storeConfigSecrets via a warning instead of discarding them (double-fault observability). - web: integer inputs get step=1 so browsers flag non-integral input in place (serializeConfigValues remains the backstop). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(plugins): split config secret tests into config_secrets_test.go config_test.go had grown past the 800-line backend file-length limit (revive), failing CI lint. Move the vault-backed config and plugin-scoped secret tests (plus their fake-vault/store helpers) into a sibling file; no test logic changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e145d48f22 |
feat: add plugin system with grpc runtime and native ui plugins (#1742)
* feat: add plugin system with grpc runtime and native ui plugins - go plugin backends ship as release tarballs (per-platform binaries, checksums) that kandev installs from url/upload or filesystem sideload+sync, then spawns and supervises via hashicorp/go-plugin (unix socket / loopback tcp+automtls) - strict typed grpc contract (kandev.plugin.v1): DeliverEvent/InvokeTool/HandleWebhook plus a capability-gated host service (state, secrets, emit); author sdk in pkg/pluginsdk; packaging cli in cmd/plugin-pack - native frontend bundles: window.registerKandevPlugin + registry/host loader with dynamic routes, nav items, slots and ws handlers; plugins settings page with install dialog, sync button and enable/disable/uninstall - gated behind the plugins feature flag (prod off, dev/e2e on); covered by unit and integration tests plus playwright e2e against a real packaged fixture plugin Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): hydrate base app context on plugin routes Plugin-owned SPA routes classify as RouteUnknown, so the boot payload shipped only feature flags — no workspaces, workflows, or kanban context. Native plugin UI (e.g. host.ui.TaskCreateDialog) then had an empty store. Hydrate the same base context as the Home route for unknown SPA routes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(plugins): expose TaskCreateDialog on host.ui Lets a native plugin open kandev's real create-task modal (prefilled via initialValues) instead of POSTing directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): harden filesystem/URL sinks against CodeQL findings Add sink-level guards on the plugin trust boundary: securejoin containment on archive extraction (zip-slip), plugin-id validation before every fs_store path, http/https scheme validation on install-from-URL, and non-interpolated ws-handler error logging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): address review findings (races, resource caps, lifecycle) - serialize per-plugin startup to close a TOCTOU double-spawn race, and let a plugin restart after exhausting attempts - bound plugin startup with an explicit 30s timeout - reject backslash paths and cap decompressed bytes by actual size in pkgtar; only mark a package Signed when a verifier actually ran and passed - delete plugin_state on uninstall; validate webhook keys against the manifest and cap webhook body size; restart an active plugin when a new version installs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): sanitize plugin-id path sinks for CodeQL Pin the plugin id to a single path segment with filepath.Base at recordPath/ configPath and pkgtar.Remove — a barrier the taint analysis recognizes. Behaviour-identical for valid ids (already validated by safePluginID); clears the go/path-injection alerts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): inline plugin-id validation at path sinks for CodeQL filepath.Base does not strip '..', so it is not a path-injection barrier. Call the recognized safePluginID / id-pattern guard inline in Delete/GetConfig/ SetConfig and pkgtar.Remove — the same barrier that already cleared the Get sink. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): address code-review findings (backend) - add capability-gating tests for the Host RPC service (was untested) - close a stop-during-start orphan race so Disable/Uninstall can't leave a supervised subprocess behind - on failed upgrade, remove only the new version dir (not all versions + data); restart the prior version - validate manifest version and reject ids ending in .config (record-file alias) - make Deliverer.Refresh non-blocking and cancel in-flight delivery on worker stop - clamp out-of-range webhook status, skip-and-warn corrupt records in List, write records atomically, semver-aware sideload version pick Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): address code-review findings (frontend) - wrap plugin routes + settings routes in a shared PluginErrorBoundary so a throwing plugin page shows a fallback instead of white-screening the SPA - fix disable/re-enable in one session: keep the cached registration and re-run initialize instead of relying on a cached ES-module re-import - prefix root-relative bundle/style URLs with apiBaseUrl (split-origin/Tauri) - surface partial-install warnings via toast.warning instead of a bare success - dedupe/remove injected style links on unload; gate PluginNavItems on the flag Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugins): serialize per-plugin lifecycle + guard config writes - add a per-plugin keyed lock in Service so concurrent Enable/Disable/Install/ Uninstall/UpdateConfig for one id are serialized (no status-machine race) - take s.mu in SetConfig/GetConfig and write config atomically - bound each plugin initialize() with a timeout so one hung bundle can't stall the rest of the boot load Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(plugins): split service_test.go to satisfy file-length lint Move the install/URL test group into service_install_test.go; no logic change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: build and ship the e2e fixture plugin package to E2E shards The plugin E2E specs' global-setup requires apps/backend/.build/kandev-plugin-e2e-1.0.0.tar.gz. Build it in the E2E build job, upload it as an artifact, and download it in both the regular and container shard jobs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |