Commit Graph

1638 Commits

Author SHA1 Message Date
João Salavisa 07038018ac fix(plugins): unload previous version before reloading on plugin update (#1861)
* fix(plugins): unload previous version before reloading on plugin update

The install/update path reloaded a plugin without first unloading the
previous version's registrations, unlike disable/uninstall. Because the
slot registry only appends, this ran initialize() twice and left two
main-top-bar slot entries, duplicating the plugin's widget icon until
a page refresh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): cache-bust plugin bundle import so updates re-execute

unloadPlugin(id, {evictCache: true}) drops the cached bundle registration
on install/update, but a same-tab re-import() of the identical bundle URL
returns the browser's already-evaluated ES module without re-running its
top-level registerKandevPlugin() call — leaving an updated plugin active
but missing from the UI. Key the bundle URL on the plugin's installed
version (?v=<version>) on both the frontend (toActivePlugin) and the
backend boot payload (bootActivePlugins) so a version bump forces a real
re-import while an unchanged version stays cache-friendly across boots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(plugins): update bundleUrl assertions for cache-bust query param

toActivePlugin now appends a ?v=<version> cache-busting query param to
bundleUrl; these two assertions still expected the bare path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): only evict bundle cache on install/update, not on enable

loadIfActive unconditionally evicted the cached bundle registration, which
broke the disable->enable cycle: since bundleUrl doesn't change on plain
enable, the browser's ES module cache returned the already-evaluated module
without re-running registerKandevPlugin, leaving the plugin's slots/routes
unregistered until a full page refresh.

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>
2026-07-22 13:45:18 +01:00
José Almeida 24f2fba183 fix: restore line expansion for multi-repo diffs (#1856)
* fix: restore line expansion for multi-repo diffs

* test: clarify review file grouping

* fix: require repository scope for ref reads
2026-07-22 09:10:54 +01:00
João Salavisa 6d5fc4794b fix(plugins): select utility agents per plugin (#1855)
* fix(plugins): select utility agents per plugin

* fix(plugins): address utility agent review feedback

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-22 01:50:40 +01:00
João Salavisa 78cae96bb1 feat(plugins): host-side conversation reads + utility-agent invoke (#1852)
* feat(plugins): add capability-gated Host reader for conversation content

Plugins can now read historical user/agent message content over the Host
data API, gated by a new `api_read:messages` capability (ADR 0047). This
unblocks "summarize yesterday"-style plugins that the live `message.added`
bus event alone cannot serve.

- proto: `ListMessages` RPC + `Message`/`MessageFilter` on `service Host`.
- SDK: `Host.Messages() MessageReader`, Go-native `Message`/`MessageFilter`.
- host: `api_read:messages` accessor + denied reader; content sanitized via
  `sysprompt.StripSystemContent` (no raw system prompts); RFC3339 `since`/
  `until` → `InvalidArgument` on bad input.
- data: new `ListMessagesForPlugin` filtered query (session/task ids, type,
  created_at range) through the task service, never the DB file.
- tests: in-process + wire capability gating, content stripping, filter/time
  passthrough, pagination, and the SQLite query.
- docs: ADR 0047, spec, plugins-manifest, plugins-authoring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(plugins): add Host.InvokeUtilityAgent for settings-selectable utility agent

Plugins can now delegate a one-shot LLM step to an operator-configured agent,
gated by a new `agent_invoke` capability (ADR 0048), so a plugin needs no
provider API key of its own.

- setting: new `utility_agent_profile_id` user setting (Settings > System >
  Utility Agent), a profile dropdown cloned from the existing utility-agent
  path; selectable in the UI.
- capability: boolean `agent_invoke`, enforced like `state`/`secrets`.
- proto/SDK: `InvokeUtilityAgent(prompt) -> text` RPC and
  `Host.InvokeUtilityAgent(ctx, prompt)`.
- backend: reuses the sessionless host-utility inference tier (ADR 0002) via a
  thin adapter — no agent loop, no task/session/workspace; resolves the
  configured profile to (agent type, model). Unconfigured / deleted profile ->
  gRPC FailedPrecondition.
- tests: capability gating, completion path (asserts the configured profile is
  used and text returned), no-utility-agent and stale-profile error paths,
  SDK/wire round-trips, and the settings apply/trim behavior.
- docs: ADR 0048, spec, plugins-manifest, plugins-authoring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): resolve utility-agent deps live and bound message filter size

Address CI + review feedback on the host plugin capabilities:

- docs: register the new /settings/system/utility-agent route in
  docs/public/coverage.json (fixes the "Validate public docs" CI failure).
- utility agent (ADR 0048): hostUtilityMgr is wired late in boot — after
  StartActivePlugins has already spawned boot-active plugins — so snapshotting
  the utility deps in hostForPlugin stranded those hosts with nil deps and made
  InvokeUtilityAgent return Unimplemented for their lifetime. Read the deps live
  from the Service (mutex-guarded) at call time instead; add a regression test
  for late wiring reaching an already-spawned host.
- messages (ADR 0047): cap the combined session/task/type filter values at 400
  so an oversized filter fails fast with InvalidArgument instead of hitting
  SQLite's host-parameter limit at query execution; add a test.

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>
2026-07-22 00:20:09 +01:00
João Salavisa c4a856dc10 fix(backend): reject disallowed cross-origin state changes in CORS (#1850)
* fix(backend): reject disallowed cross-origin state changes in CORS

The CORS middleware only aborted disallowed origins on the OPTIONS
preflight, then called c.Next() for every other method. A CORS-safelisted
request (e.g. a multipart/form-data POST, which skips preflight) from a
malicious page therefore still executed the handler's side effect even
though the browser could not read the response.

Against a loopback-bound backend this is a drive-by CSRF primitive: any
web page the victim visits could POST to a mutating endpoint such as
/api/plugins/install, which unpacks and spawns the uploaded plugin binary
(RCE), with no authentication layer in front of it.

Now a disallowed cross-origin request is rejected with 403 for the
preflight and every state-changing method; only CORS-safe methods
(GET/HEAD) fall through without CORS headers, so the browser still cannot
read the response and no state is changed. No-Origin (CLI/curl) and
allowed same/loopback-origin requests are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(backend): block all disallowed cross-origin requests in CORS

Addresses review: GET is not universally side-effect-free here. Plugin
webhooks are registered for GET as well as POST
(/api/plugins/:id/webhooks/:key -> Service.InvokeWebhook), so the earlier
GET/HEAD whitelist still let a disallowed-origin browser GET trigger that
unauthenticated side effect via drive-by CSRF.

Reject every method (including GET/HEAD) with 403 for a disallowed origin.
This loses nothing legitimate: a response the browser could read
cross-origin needs CORS headers we only emit for allowed origins.
Non-browser clients (CLI/curl) and resource loads (<img>/<script>) send no
Origin and take the wildcard branch, so they are unaffected.

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>
2026-07-21 23:55:13 +01:00
Carlos Florêncio 7fd99c3f33 feat: complete GitLab integration parity (#1832) 2026-07-21 23:24:22 +01:00
Carlos Florêncio 5499eadf19 fix(github): keep draft pull requests out of merge-ready state (#1851) 2026-07-21 23:22:49 +01:00
José Almeida c645885cb5 fix(tasks): preserve inherited workspaces on subtask deletion (#1840)
* fix(tasks): preserve inherited workspaces on subtask deletion

* fix(tasks): avoid archive workspace ownership transfer

* fix(tasks): serialize shared environment handoff

* test(tasks): cover blocked shared environment handoff

* test(tasks): cover last-member environment cleanup

* fix(tasks): roll back shared environment ownership
2026-07-21 22:37:02 +01:00
Alassane 21b0003b32 fix: surface orphaned tasks in pipeline view after step deletion (#1809) 2026-07-21 21:37:50 +01:00
Carlos Florêncio be56eff1a2 fix: reset office session state when execution profile changes (#1846) 2026-07-21 21:37:08 +01:00
Carlos Florêncio ea6bfcd15f fix(web): scope and label clarification shortcuts (#1847) 2026-07-21 20:41:41 +01:00
Carlos Florêncio d144c16fc2 fix(web): reconcile dockview size before restoring layout (#1843) (#1845) 2026-07-21 19:24:11 +01:00
Carlos Florêncio 4b6d603f6d feat(github): explain repository scope (#1842) 2026-07-21 19:24:02 +01:00
João Salavisa 330afb7901 feat(plugins): add main-top-bar slot for the default app top bar (#1841)
* feat(plugins): add main-top-bar slot for the default app top bar

Plugins could only contribute to the session/chat top bar via the
chat-top-bar slot. Add a main-top-bar slot, mounted on the default
app top bar (Home / Kanban / Tasks) across desktop, tablet, and
mobile headers, so plugins can surface workspace-scoped status or
actions outside of a task session.

slotProps: { workspaceId, workspaceLabel, currentPage } — no task or
session ids, since the bar isn't scoped to a task.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: narrow main-top-bar currentPage slot prop to a literal union

Addresses Claude Code Review feedback on PR #1841: MainTopBarSlotProps.currentPage
was typed as a plain string even though every caller only ever passes
"kanban" | "tasks" (matching KanbanHeaderProps.currentPage). Narrow the
plugin-facing contract to match so plugin authors get real type safety.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(e2e): cover the main-top-bar plugin slot

Extend the kandev-plugin-e2e fixture bundle to also register a
main-top-bar slot component, and assert it renders on the default
app top bar (Home) alongside the existing task-sidebar coverage.

Verified locally: apps/web e2e plugins.spec.ts passes, plugin-fixture
Go tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 17:18:34 +01:00
José Almeida b78cd1ce40 fix(settings): order plugins before system (#1844) 2026-07-21 16:32:55 +01:00
github-actions[bot] a43cd19902 release: 0.81.0 2026-07-21 13:09:32 +00:00
João Salavisa 2921553d40 feat(plugins): render plugin-settings slot at top of settings page (#1838)
Move the plugin-settings slot above the schema-driven settings card so a
plugin's live status/health card is the first content under the header,
instead of sitting between the settings and manifest cards. Docs updated to
match.

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:07:54 +01:00
José Almeida 8f2fe7a8e6 feat(web): add agent message comments (#1835)
* feat(web): add agent message comments

* fix(web): harden agent message comments

* fix(web): address message comment review findings

* fix(web): allow comments on legacy agent messages

* fix(web): address latest message comment review

* fix(web): address follow-up message comment review

* fix(web): address remaining message comment review

* fix(web): keep failed mobile comments open

* fix(web): preserve selections over message comments
2026-07-21 13:32:19 +01:00
João Salavisa 30e6cba810 feat(plugins): add owner-scoped plugin-settings slot for inline plugin UI (#1836)
* feat(plugins): add plugin-settings slot to per-plugin settings page

Add a named "plugin-settings" host slot so an installed plugin can render
its own inline UI (e.g. a live integration/health status card) on its
per-plugin settings page (Settings > Plugins > <plugin>), between the
schema-driven settings form and the manifest card.

The slot renders on every plugin's detail page, so the host forwards the
current plugin's identity via slotProps ({ pluginId, status }); registered
components must gate on slotProps.pluginId to avoid rendering on another
plugin's page. Documented in PLUGIN-API.md and mirrored in types.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(plugins): make plugin-settings slot owner-scoped in the host

Instead of forcing every plugin author to early-return unless
slotProps.pluginId matches its own id, the host now filters the
"plugin-settings" slot by the owning plugin. The registry already records
the owning pluginId per registration, so PluginSlot gains an optional
ownerPluginId prop backed by a new getSlotComponentsForPlugin() getter,
and the plugin-detail page passes the current plugin's id.

A plugin's settings card now renders only on its own settings page, with
no gating boilerplate in the plugin. Docs/types updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(plugins): add plugin-settings slot usage mockups

Add illustrative mockups of the plugin-settings slot rendered on a plugin's
settings page (Provider Usage health card) plus a strip of possible usages
(healthy, degraded, and a generic integration), referenced from PLUGIN-API.md.
Clearly labeled as mockups, not real product screenshots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): reset error boundary per owner in owner-scoped slots

Address review feedback on PR #1836:

- The PluginErrorBoundary key was `${name}-${index}`, identical across
  plugins for an owner-scoped slot. Navigating /settings/plugins/A -> B in
  the same settings shell (without remounting PluginSlot) reused A's errored
  boundary and silently hid B's healthy card. Include ownerPluginId in the
  key so each owner gets an independent boundary; add a regression test.
- Trim the verbose multi-line slot comment in plugin-detail to one line per
  the repo's comment convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(plugins): document plugin-settings slot; drop mockup binaries

- Public docs: add the owner-scoped `plugin-settings` slot to the Named
  slots table and a "Plugin settings page" section in plugins-authoring.md,
  and mention inline status cards in plugins.md's per-plugin settings.
- Remove the illustrative mockup PNGs (and their PLUGIN-API.md embeds) that
  were committed to the branch — image binaries should not merge into main;
  PR screenshots belong on a throwaway media ref instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(plugins): condense owner-key boundary comment to one line

Address review nit — the repo prefers one short comment line.

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>
2026-07-21 11:53:43 +01:00
Carlos Florêncio c2a678b023 feat: add Azure DevOps integration (#1778) 2026-07-21 08:40:26 +01:00
Carlos Florêncio d7c24184cd fix(agent): refresh opencode model cache (#1830) 2026-07-21 08:39:42 +01:00
João Salavisa ea28ab0e7a fix(web): keep plugin row action buttons pinned to the header (#1833)
Disable/Uninstall shifted from top-right to bottom-left depending on
whether the plugin description wrapped to multiple lines, because the
description and category badges shared the same flex-wrap row as the
action buttons. Move them below the fixed-height title row so the
buttons no longer move.

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 00:13:39 +01:00
Carlos Florêncio 2130c28686 fix(repositories): allow explicit paths outside discovery roots (#1825) 2026-07-20 19:49:41 +01:00
João Salavisa b277e7f1d8 feat: add chat-top-bar plugin slot (#1827)
* feat: add chat-top-bar plugin slot

Adds a new named plugin UI slot, chat-top-bar, rendered in the session
top bar beside the CPU/DB metrics and document/editor/debug controls.
Mirrors the existing chat-input-actions slot pattern: a wrapper
component (TaskTopBarPluginActions) assembles slotProps from the store
and renders <PluginSlot name="chat-top-bar" />, forwarding
{ taskId, taskTitle, workspaceId, activeSessionId, sessionIds }.

Updates PLUGIN-API.md, the public authoring guide, and the
PluginSlotName JSDoc to document the new slot. No backend changes
needed since slot names are an open string type.

* fix: address PR review — mobile chat-top-bar slot, gate by archived state

- Wire the chat-top-bar plugin slot into the mobile session top bar
  (session-mobile-top-bar.tsx), matching desktop's slotProps shape.
- Gate the plugin cluster behind !isArchived in TopBarRight, matching
  every other interactive control in that group (port-forward, PR/MR
  buttons, layout preset, editors menu).
- Extract RemoteExecutorIndicator, ApproveButton, and MobileTopBarDialogs
  to keep the mobile top bar under lint complexity/length limits.

* fix: gate mobile chat-top-bar plugin slot by archived state

Threads isArchived through TaskLayout -> SessionMobileLayout ->
SessionMobileTopBar -> MobileTopBarActions so the mobile top bar
hides plugin content on archived tasks, matching the desktop
TopBarRight behavior fixed in the previous commit.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-20 18:52:56 +01:00
Carlos Florêncio 15949959b0 fix: preserve authoritative archived task state (#1826) 2026-07-20 18:04:11 +01:00
José Almeida f9956a90d5 docs(skills): define Kandev mobile UI language (#1820)
* docs(skills): define Kandev mobile UI language

* docs(skills): restore mobile fixture import guidance
2026-07-20 17:34:59 +01:00
José Almeida 3479fb601e fix(web): refresh walkthrough ranges on Monaco model changes (#1822)
* fix(web): refresh walkthrough ranges on Monaco model changes

* fix(web): avoid recentering walkthrough on line edits

* test(backend): isolate executor session snapshots

* test(backend): deep-copy executor session snapshots
2026-07-20 17:34:15 +01:00
github-actions[bot] 801300645d release: 0.80.0 2026-07-20 15:05:49 +00:00
dk-blackfuel d185f13d13 fix: clear stale archived state when unarchiving from task detail top bar (#1816) 2026-07-20 16:05:10 +01:00
Carlos Florêncio ace3f56475 fix: show github review and issue watches after strictmode remount (#1823) 2026-07-20 15:34:29 +01:00
João Salavisa 91bdee3d3a feat(plugins): show source repo link on installed plugin list and settings (#1821)
* feat(plugins): show source repo link on installed plugin list and settings

Installed plugins carried no repository URL — a "Repo" link existed only
on marketplace catalog cards, whose repo_url is registry-derived from
plugins.yaml and never reaches an installed record. Let the manifest
declare an optional repo_url so sideloaded and directly-installed plugins
surface the same link.

- manifest: add optional RepoURL (repo_url); validate http(s)-only at
  registration so a javascript: scheme can't reach the operator's session.
- Record embeds the manifest, so repo_url flows through JSON to the
  frontend PluginRecord with no extra plumbing.
- frontend: extract the shared, guarded PluginRepoLink (+ isHttpUrl) from
  the marketplace row and render it on the installed plugin row and the
  plugin detail header.
- fixture + docs: exercise repo_url in the e2e fixture manifest and
  document the field in the public manifest reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): harden repo_url handling per review

- PluginRepoLink accepts string|undefined|null and isHttpUrl null-checks
  before trim, so it can't throw on an absent repo_url (CodeRabbit); drop
  the now-redundant truthiness guards at the call sites.
- validateRepoURL trims m.RepoURL in place so the stored/serialised value
  matches what was validated (Greptile).

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>
2026-07-20 14:03:52 +01:00
Carlos Florêncio 77529e1eab fix: preserve shared worktrees during task cleanup (#1819) 2026-07-20 13:53:48 +01:00
João Salavisa 18ba644388 fix(plugins): serialize empty sync-result arrays instead of null (#1817)
* fix(plugins): serialize empty sync-result arrays instead of null

Sync's result slices (Added/Installed/Missing/Errors) stayed Go nil when
nothing changed, and encoding/json marshals a nil slice as null. The
frontend's SyncResult type declares these as non-nullable arrays and reads
result.added.length unconditionally, so a routine "everything up to date"
sync crashed with "can't access property 'length', e.added is null".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(plugins): initialize sync-result slices at construction

Per review feedback (Claude, Greptile): normalizing nil slices at each
return site in sync() left the invariant fragile — a future early return
would silently reintroduce the null-serialization bug. Initialize
SyncResult's slices non-nil at the struct literal instead, and append the
scan results onto them.

Also tighten the regression test to check exact JSON field values instead
of a raw strings.Contains("null") substring search.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 12:50:18 +01:00
Carlos Florêncio ba75922fa1 feat: add configurable task layout profiles (#1799) 2026-07-20 12:09:06 +01:00
João Salavisa 4d1787f484 fix: harden agentctl bind, git clone args, and powershell sound escaping (#1811)
* fix: harden agentctl bind, git clone args, and powershell sound escaping

Three LOW-severity defense-in-depth hardening fixes with PoC + regression tests:

- notifications: escapePowerShell now neutralizes backtick and `$` in addition
  to `"`, defusing `$(...)` sub-expression and `$var` injection through an
  operator-supplied SoundFile path interpolated into the powershell `-c` script.
- office/skills: git clone for git-sourced skills now passes a `--`
  end-of-options separator before the source locator (matching configloader),
  so a `-`-prefixed locator can never be parsed as a git flag.
- agentctl: new Config.ListenHost() binds the control server and per-instance
  listeners to loopback only when no AuthToken is configured (auth disabled),
  so the unauthenticated command/shell/process routes are never reachable
  beyond localhost. The normal nonce-handshake flow (incl. Docker) always has
  a token and keeps binding all interfaces.

* test: make PoC tests flip and drop indexOf coupling

Address review feedback on the security-hardening PoCs:

- system_provider PoC now demonstrates the PowerShell sub-expression injection
  against a legacyEscapePowerShell copy of the unfixed function and asserts the
  current escaper defuses it, so the PoC genuinely flips instead of passing
  trivially against fixed code (a substring match on `$(` matched the escaped
  form too).
- materialization PoC + regression test use stdlib slices.Index instead of a
  custom indexOf defined in the PoC file, removing the coupling where deleting
  the PoC file would break the regression test.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-20 11:32:43 +01:00
Carlos Florêncio f212258907 perf: scale workspace git status for large worktrees (#1814) 2026-07-20 11:04:59 +01:00
Carlos Florêncio 110c2efcb1 fix: report storage usage and reclaim archived workspaces (#1813) 2026-07-20 09:35:57 +01:00
Carlos Florêncio 33aa7af67e feat: configure profiles for MCP-created tasks (#1797) 2026-07-19 23:18:06 +01:00
Carlos Florêncio 84a9ca0822 fix: ignore echoed user ACP chunks (#1804) 2026-07-19 22:09:49 +01:00
Carlos Florêncio 00f8c6b202 fix: repair incomplete code-server installations (#1800) 2026-07-19 22:09:40 +01:00
José Almeida f7f1d10996 feat: add parent-controlled task stopping via MCP (#1803)
* feat: add parent-controlled task stopping via MCP

* fix: harden parent task stop lifecycle

* fix: preserve rejected task rollback state
2026-07-19 20:23:46 +01:00
João Salavisa bc2f310e73 feat(plugins): add plugin marketplace with curated registry and updates (#1798)
* feat(plugins): add plugin marketplace with curated registry and updates

Discover and install plugins from inside kandev instead of only by URL/upload.

- Backend marketplace subpackage: SQLite-backed sources, catalog fetch/merge
  (dedup, install_state, degraded-source handling), and the /api/plugins/
  marketplace[/sources] API. Install reuses the existing install-by-URL pipeline.
- Frontend: Settings > Plugins gains a Browse tab (search/category/star sort),
  source management, one-click install, and an Update button on the Installed
  tab when the catalog has a newer version. Plugins may ship an icon (manifest
  `icon` -> catalog icon_url) with a letter-tile fallback.
- Registry infra: plugin-registry/ (curated plugins.yaml + schema.json) and two
  GitHub Actions (zero-dependency Node build-index.mjs -> index.json on Pages,
  daily star refresh). Ships empty at launch; the empty catalog is valid.
- Spec: docs/specs/plugins/marketplace.md. No download telemetry; stars only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): address marketplace PR review feedback

- CodeQL: stub fetch routers keyed off a path segment, not a hostname substring.
- Nullable stars end to end: a failed star lookup stays unknown (not 0) and
  sorts last, instead of decoding null -> 0 and corrupting ranking.
- Fetch marketplace sources concurrently and dedupe identical in-flight
  downloads (singleflight), so a slow source can't serialize the Browse tab.
- Register /marketplace routes before the /:id wildcard (gin tree ordering).
- Duplicate source URL returns 409 ErrDuplicateSource, not a raw SQL error.
- Browse: filter category client-side so the category dropdown never collapses.
- marketplaceInstall swallows after toasting (no unhandled promise rejection).
- Source badge keys off source_id, not a name substring.
- build-index: read block-sequence manifest categories, not just inline.
- Registry index workflow validates on pull_request; Pages deploy gated to
  push/schedule/dispatch. Spec: align package_sha256 (advisory in v1) + sort enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): guard catalog repo_url against javascript: href injection

repo_url comes from an untrusted index.json, so a malicious/compromised source
could set a javascript: URL that runs in the operator's session when the Repo
link is clicked (rel/target don't block it). Only render the anchor for http(s)
schemes. Adds regression tests for the scheme guard and null-star rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): require https for remote sources; test oversized download

- Reject remote plain-http marketplace sources (MITM catalog-poisoning risk);
  loopback http stays allowed for local dev/e2e fixtures.
- Add the missing oversized-response assertion to TestDownloadRejectsNon200AndOversized
  so the maxIndexBytes memory bound has coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): detach source download from singleflight leader's context

Two concurrent Browse opens after a cache Refresh share one singleflight
download; if the leader's request context is canceled (it navigates away), the
waiters must not inherit that cancellation. Use context.WithoutCancel so values
are preserved but the leader can't cancel everyone; the client's 20s timeout
still bounds the download.

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>
2026-07-19 17:10:53 +01:00
João Salavisa ac8401b709 fix: harden submodule init against malicious .gitmodules (#1802)
* fix: harden submodule init against malicious .gitmodules

initSubmodules ran a bare `git submodule update --init --recursive` in a
worktree whose HEAD is attacker-controlled during a fork-PR review task,
with no protocol restriction, no hooks disable, and no cmd.Env. Attacker
`.gitmodules` could pick a command-executing transport (`ext::sh -c`,
CVE-2018-17456), a local `file://` path, or a plain-http URL (cloud
metadata SSRF).

Harden the single sink so all call sites benefit:
- protocol.allow=never default-denies every transport; only https/ssh are
  re-enabled (blocks file://, git://, plain http:// SSRF).
- protocol.ext.allow=never hard-pins the RCE transport off; on the command
  line it outranks any ambient protocol.ext.allow=always.
- core.hooksPath=/dev/null neutralizes attacker-planted hooks.
- GIT_TERMINAL_PROMPT=0 stops attacker URLs from hanging on a prompt.

Residual: attacker-chosen https/ssh URLs are still fetched (transports real
submodules require); fully closing that needs gating submodule init off for
untrusted worktrees, tracked separately.

* fix: address submodule hardening review feedback

- Reuse newNonInteractiveGitCmd so submodule init inherits the full
  non-interactive env set (GIT_SSH_COMMAND=ssh -oBatchMode=yes, SSH/GCM
  askpass suppression, WaitDelay). Prevents an attacker ssh:// submodule
  URL from hanging the review runner on host-key/passphrase prompts until
  the context deadline (GIT_TERMINAL_PROMPT=0 alone only covers git's HTTP
  prompt, not SSH).
- Correct the file:// comment: command-line -c outranks repo/global git
  config, so ambient protocol.file.allow=always does NOT re-enable file
  submodules here. This is intentional for untrusted trees; a trusted
  opt-in must be an explicit per-repo -c, not implicit host config.
- Add a file:// URL variant to the transport-block test alongside the
  bare-path form (CVE-2022-39253 classification differs by git version).

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-19 16:12:56 +01:00
João Salavisa 19c3771cf0 fix: validate destination parent chain in copyfiles byte copy (#1801)
* fix: validate destination parent chain in copyfiles byte copy

copyState.copyFile wrote into the target worktree via os.MkdirAll +
os.Create with no destination-parent symlink validation, unlike the
sibling paths that were hardened for exactly this: symlinkMatch calls
validateParentChain and remote writeOneEntry calls secureDestination.

A worktree whose HEAD ships a directory segment as a symlink (a fork-PR
head can set a tree entry to mode 120000 pointing at an absolute host
path) let MkdirAll follow the link and os.Create write the source file's
bytes outside the worktree as the backend user — a zero-click,
content-driven arbitrary-file-write reachable on the PR-review path
(copyConfiguredFiles -> copyfiles.Copy during worktree creation).

Route the byte copy through the same secureDestination hardening: it
runs validateParentChain, creates the parent dirs, and re-confirms
containment via EvalSymlinks before returning a destination guaranteed
inside the worktree. A rejected destination emits a non-fatal warning,
mirroring symlinkMatch. The leaf os.Lstat skip-if-exists behavior is
preserved (only a symlinked parent was the escape vector).

Add byte-copy regression tests: a symlinked parent is rejected and
nothing is written outside the worktree, a leaf symlink is still
skipped, and a clean nested copy still works.

* fix: canonicalize copyfiles target root before containment checks

copyFile passed the raw s.targetDir into secureDestination, which
EvalSymlinks-resolves the created parent and then Rel-checks it against
that root. When the worktree is reached through a symlinked ancestor
(macOS /tmp -> /private/tmp, or a user-symlinked tasks dir) the canonical
parent looks like it escapes the non-canonical root, so every legitimate
byte copy is silently rejected.

Resolve targetDir once in Copy (falling back to the raw path when it does
not exist yet), mirroring the EvalSymlinks WriteEntries already performs.
Add a regression test that routes the target through a symlinked ancestor
and asserts the copy still lands at the real location.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-19 16:12:44 +01:00
Carlos Florêncio 5fb1923a9a feat: add explicit save for settings (#1686) 2026-07-19 14:21:36 +01:00
José Almeida 1a42c07abe fix: keep queued messages parked after agent cancellation (#1796) 2026-07-19 13:50:03 +01:00
João Salavisa 79a4534449 fix: gate gitlab/linear fetches on integration being configured (#1789)
* fix: gate gitlab/linear fetches on integration being configured

The GitLab and Linear pages fired merge-request/issue searches on mount
regardless of connection state, producing console errors when the
integration was unconfigured (two 500s on /gitlab, one 503 on /linear)
even though the UI already rendered the "not connected/configured" notice.

Gate the search fetch on the same connected/configured condition that
decides whether the results list renders:
- useGitLabSearch takes an `enabled` flag (converted to an options object
  to stay within the 5-param lint limit, matching useGitHubSearch); the
  page enables it only when status is loaded and GitLab is connected.
- Linear's inline issue-search hook is extracted to
  useLinearIssueSearch with an `enabled` flag, enabled only when the
  workspace is loaded, configured, and available.

Adds gating/wiring tests for both hooks.

* refactor: gate gitlab search at the effect level too

Mirror the Linear hook (which guards both its run callback and its debounce
effect) by adding the enabled guard to useGitLabSearch's fetch effect and
enabled to its dep array, so a disabled integration never schedules a fetch.
The callback keeps its own guard to also cover the exported refresh().

Addresses PR review feedback on the enabled-gate asymmetry.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-19 13:39:58 +01:00
Carlos Florêncio 4fdd679269 fix: attribute model configuration to turns (#1784) 2026-07-19 12:46:40 +01:00
João Salavisa 4b1320ffb0 feat(plugins): add chat-input-actions slot for plugin toolbar icons (#1794)
* feat(plugins): add chat-input-actions slot for plugin toolbar icons

Let plugins render icon buttons into the chat composer toolbar, beside
the model picker, mic, and send button, via the existing named-slot
mechanism (registerComponent). The host mounts the slot in both the
desktop and mobile toolbars and forwards { taskId, taskTitle,
activeSessionId, sessionIds } as slotProps — the active session plus
every kandev session id on the task. Resolving a session id to an
agent/ACP transcript id is left to the plugin (server-side, via the Host
data API); the host only propagates ids.

Docs (PLUGIN-API.md, public plugins/plugins-authoring) document the new
slot and its props.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): read store optionally in chat-input-actions slot

ChatInputPluginActions read the app store via useAppStore, which throws
outside a StateProvider — breaking existing toolbar tests that render the
composer in isolation. Switch to useOptionalAppStore (fallback to no
sessions) so the slot never hard-requires a provider.

Also: gate the slot behind hideAgentControls to match the other
session-specific toolbar controls, drop a redundant String() coercion,
tidy a dangling docs reference, and cover the taskId=null path.

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>
2026-07-19 12:26:50 +01:00
Carlos Florêncio 1dc96a92c6 fix: restore multi-repo review diffs (#1795) 2026-07-19 12:25:36 +01:00