Commit Graph

1623 Commits

Author SHA1 Message Date
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
Carlos Florêncio b5dc6a261e fix: hide workspace ownership marker from git status (#1783) 2026-07-19 11:11:26 +01:00
João Salavisa cdd0bf8cbd feat: bind HTTP server to multiple addresses (#1792)
* feat: bind HTTP server to multiple addresses

Support a list of bind addresses so the backend can serve on loopback and
a tailnet IP at once, without exposing every interface via 0.0.0.0.

- Config: server.host now parses a comma-separated list; a new server.hosts
  YAML array takes precedence when set. Entries are trimmed, deduped, and
  validated (IP or hostname); a wildcard entry collapses the set.
- Startup: one net.Listener per resolved address serving one shared
  http.Server; the single-listener path is the one-element case.
- Bind-failure policy: all-fail is fatal; partial-fail warns, serves the
  good addresses, and runs a cancellable background retry loop that binds
  late-arriving addresses (e.g. a tailnet IP after tailscaled starts).
- Graceful shutdown stops the retry loop before closing all listeners;
  covered by goleak.
- Readiness probe picks a reachable bound address, preferring loopback.
- Add ServerConfig.NonLoopbackBinds and config.IsLoopbackHost so the
  sibling app-auth task can gate fail-closed auth on off-box binds.

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

* fix: preserve KANDEV_SERVER_HOST over config server.hosts

server.host now wins over the server.hosts array when set, because Host
carries the KANDEV_SERVER_HOST env override and env must beat a config-file
server.hosts (env-over-file precedence and the desktop loopback contract).
The viper default for server.host is empty so an unset host is
distinguishable from an explicit one; server.hosts is used only when host is
unset. Adds regression tests covering the env-override and hosts-when-unset
paths.

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

* fix: harden multi-bind host parsing and listener lifecycle

Address review feedback:
- Validate every host before wildcard collapse so an invalid entry is
  rejected regardless of order; canonicalize IPs so equivalent forms
  (::1 and 0:0:0:0:0:0:0:1) dedupe and unspecified longhand IPs count as
  the wildcard.
- Probe the IPv6 loopback (::1) for an IPv6 wildcard listener instead of
  127.0.0.1, so readiness doesn't stall on an IPv6-only socket.
- Make serverListeners.Stop race-free via sync.Once.
- Register t.Cleanup for the blocker listener before the fatal-assertion
  path in the partial-failure 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-19 10:36:44 +01:00
João Salavisa 8f23b8a19e fix: prevent command injection via untrusted git branch names (#1791)
* fix: prevent command injection via untrusted git branch names

Untrusted git branch names (e.g. a fork PR head branch) were substituted
unescaped into executor prepare scripts and run via `eval` in a shell,
yielding command injection (CRITICAL RCE) in the Docker/Sprites/remote
executors. git check-ref-format permits `;`, `|`, `$`, backticks and
`()`, so a branch like `$(touch pwned)` or `main;touch pwned` executed.

- scriptengine: shell-single-quote every data placeholder that lands in
  shell text (repository.path/name/branch/clone_url/ssh_url,
  workspace.path, worktree.base_path/path/branch/base_branch). Script
  fragments (setup_script, git.identity_setup, github.auth_setup,
  kandev.*) are left untouched.
- default_scripts: wrap each data placeholder in single quotes in the
  postlude, clone lines, and the Sprites echo (double quotes still allow
  $(...) substitution). `'<escaped>'` round-trips to the literal.
- orchestrator: defense-in-depth — reject a PR head branch that fails
  securityutil.IsValidBranchName before it reaches CheckoutBranch, while
  still creating the review task without a checkout.
- tests: PoC-derived regression asserting the injected command no longer
  runs through the real sh -c/eval sink, provider escaping unit tests,
  and ingestion-layer validation coverage.

* fix: harden branch-name escaping for legacy stored prepare scripts

Automated review (Codex, CodeRabbit) flagged that shell-single-quoting the
value while relying on the templates to add the surrounding quotes only
protects the templates we ship. Profiles snapshot the default prepare
script at creation time, so an OLD stored script referencing a placeholder
bare (e.g. `git clone --depth=1 --branch {{repository.branch}} ...`) still
resolved with an unquoted, injectable branch value.

- scriptengine: add shellQuote, which emits a fully self-contained
  single-quoted shell token (surrounding quotes included). Every data
  placeholder (repository.path/name/branch/clone_url/ssh_url,
  workspace.path, worktree.base_path/path/branch/base_branch) now carries
  its own quoting, so a bare `{{placeholder}}` in any template — including
  legacy stored scripts — is safe. shellSingleQuote is kept for the git
  identity fragment, which supplies its own quotes.
- default_scripts: reference data placeholders bare (the value self-quotes);
  the Sprites echo becomes printf with the values as separate args so a
  hostile URL/branch can't expand inside a double-quoted string.
- tests: assert providers emit fully-quoted tokens that round-trip through a
  real shell; cover base_path/path/repository.path/name/ssh_url; add a
  regression proving a legacy stored script with bare placeholders no longer
  executes injected commands.

* test: strengthen branch-injection regression assertions

Address Greptile review feedback on the new tests.

- assertNoUnquotedPayload now tracks real single-quote state char-by-char
  instead of "some quote before and after", so a payload in an unquoted
  context on a line that happens to contain other quotes is caught.
- Drop the unused wantHasRepoRow table field; repo-row presence is already
  asserted via len(got) != 1.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-19 10:33:29 +01:00
João Salavisa 410b1dd4d1 fix: prevent AppleScript injection RCE in macOS notifications (#1790)
* fix: prevent AppleScript injection RCE in macOS notifications

buildAppleScript escaped only the double-quote in notification title/body
and interpolated the result into an `osascript -e` string literal. A raw
trailing backslash defeated that escaping: `\"` became `\\"`, which
AppleScript reads as an escaped backslash plus a real closing delimiter,
so the `display notification "..."` literal terminated early and any
following text (e.g. `do shell script`) executed on the host.

Notification title/body come verbatim from untrusted external-integration
issue titles (Sentry/Linear/Jira) with no sanitization, so an attacker who
can raise an issue in a connected workspace could achieve RCE on the macOS
host when the office session reached WAITING_FOR_INPUT.

Replace string interpolation with argv passing: title/body are delivered as
`run` handler arguments (item 1/2 of argv) and never touch the AppleScript
source, so no escaping is required. Add a proof-of-concept test documenting
the legacy breakout and a regression guard asserting the fixed builder never
leaks attacker text into script fragments.

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

* fix: terminate osascript option parsing before notification argv

Passing untrusted title/body as trailing arguments still let a title that
looks like an osascript flag be consumed as an option: a title of "-e" would
turn the body into another script fragment, reopening the AppleScript
injection. Add a `--` option terminator before title/body so untrusted text
always lands in argv. Extend the regression tests to cover the flag-like
title vector.

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

* test: prove single-line AppleScript payload is host RCE, not just DoS

Add a single-line (Jira/Linear, no newline) PoC alongside the multi-line
Sentry payload. A newline statement separator is only needed for a SECOND
statement; the single-line payload instead injects an expression
`& (do shell script (...))` into the display-notification body argument, so
the command executes as a side effect while the line stays one valid
statement. Both shapes are host code execution. Extend the regression guard
to prove the argv fix neutralizes both.

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 10:32:55 +01:00
João Salavisa 370cee8f7f docs: add plugin system guide, authoring tutorial, and manifest reference (#1774)
* docs: add plugin system guide, authoring tutorial, and manifest reference

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

* docs: update plugin docs for host data API, config, and nav additions

Cherry-picked plugin docs (d853b66b8) were written before four follow-up
PRs merged: typed capability-gated Host data API (#1753, ADR 0043),
richer frontend SDK (#1759), per-plugin settings pages (#1761), and
sidebar Integrations nav items (#1767). Re-verify against the merged
code and bring the docs current:

- plugins.md: per-plugin settings/config page, expanded Host RPC list,
  updated capability vocabulary (api_read/api_write), sidebar nav note.
- plugins-authoring.md: GetConfig/GetSecret/SetSecret/DeleteSecret and
  the six Host data readers, registerRoute options/PluginPageChrome,
  NavItem icon/integrations section, host.api.baseUrl/navigate, and the
  full host.ui component list.
- plugins-manifest.md: corrected api_read/api_write semantics (now
  enforced, not reserved), config_schema validation/secret-masking
  behavior, and new Host data API / config-schema sections.
- configuration.md, feature-status.md: document the KANDEV_FEATURES_PLUGINS
  flag alongside the existing Office flag.
- coverage.json: new plugin-system coverage area so the public-docs
  validator accounts for the three new pages.

* docs: fix plugin config route, signing claim, and data-dir layout

Address automated review findings on PR #1774:
- plugins-manifest.md: PATCH route for config is /api/plugins/{id}, not
  /api/plugins/{id}/config (only GET uses the /config suffix).
- plugins-authoring.md: host.store is not read-only — setState is
  genuinely exposed and writable; correct the misleading inline comment.
- plugins.md: signature verification is not currently wired up in
  production (pkgtar.VerifySignature is nil, never assigned outside
  tests), so every package installs and is reported as unsigned today —
  correct the 'kandev verifies it' claim in both the install pipeline
  step and the signed/unsigned section.
- plugins.md: KANDEV_PLUGIN_DATA_DIR is <id>/data, a sibling of the
  <version>/ directory (shared across versions), not nested inside it —
  fix the on-disk layout diagram.

* docs: mark plugin docs as experimental

The plugin system is feature-flagged and off by default in production
(KANDEV_FEATURES_PLUGINS). Mark all three plugin pages with
status: experimental frontmatter, matching the k8s.md convention for a
page that is wholly about an experimental feature.

* docs: add plugin UI screenshots and a capture spec

Embed four operator-facing screenshots in the plugin docs (the Plugins
list, the install dialog, a per-plugin settings page with its manifest
card, and a native plugin route). Captured from an isolated e2e backend
with the kandev-plugin-hello reference example installed.

Add e2e/tests/plugins/plugins-docs-screenshots.spec.ts to regenerate them:
it reuses the plugins e2e harness (isolated backend, plugins flag on) and
is skipped unless CAPTURE_DOCS_MEDIA=1, so it adds no CI cost. Defaults to
the repo's own e2e fixture package; override DOCS_PLUGIN_* to point at the
polished hello example (as the committed captures were taken).

* docs: fix plugin screenshot paths for the landing publisher

The Cloudflare preview build failed because markdown images under
docs/public/media/ compile to build-time module imports that the docs
site can't resolve — media/ is reserved for <DocsVideo> assets. The
landing publisher only rewrites and copies images referenced under
screenshots/ (README convention). Move the four plugin captures to
docs/screenshots/plugin-*.png and reference them as ../screenshots/*.png,
and update the capture spec to write there.

* docs: convert plugin how-it-works diagram to mermaid

The hand-drawn ASCII pipeline rendered with broken alignment and stray
syntax highlighting on the docs site. Replace it with a mermaid flowchart
(same convention as architecture.md), which renders as a clean SVG.

* docs: give plugins its own nav section

* docs: clarify plugin tools are declared/listed, not agent-invocable yet

Plugin tools[] are validated and exposed via GET /api/plugins/tools, and
the gRPC InvokeTool transport is implemented and tested, but nothing in
production calls it — no agent, MCP bridge, or HTTP route. The docs
overstated this as agent-invocable. Correct the Plugin interface comment,
the manifest tools[] rows, the how-it-works diagram, and the example
plugin description, and add a note in both the authoring and manifest
pages that InvokeTool stays unused until the agent-tool wiring ships.

* docs: clarify webhook relay in how-it-works diagram

The diagram collapsed two hops into one node, reading as if the plugin
receives the external HTTP POST directly. Split it: an external caller
POSTs to kandev's webhook route, which relays the request to the plugin
over the gRPC HandleWebhook method (the plugin serves no HTTP itself).
Also label the DeliverEvent/InvokeTool/HandleWebhook edges by their real
proto method names.

* docs: note the plugin backend is any go-plugin gRPC executable, Go SDK only

* docs: remove plugin tools from the docs

Plugin tools[] overlap with MCP (kandev's established agent-tool surface)
and the invocation path was never wired to agents. Rather than document a
half-feature that implies plugins are an alternate MCP, drop it entirely:
the Plugin interface (InvokeTool), the manifest tools[] block and field
rows, the how-it-works diagram branch, and the example-plugin mention.
Removal from the backend code is tracked separately.

* docs: replace how-it-works mermaid with a designed diagram image

The mermaid flowchart rendered poorly. Replace it with a hand-authored
HTML/CSS architecture figure (install pipeline + the runtime gRPC
interactions), rendered to docs/screenshots/plugin-architecture.png. The
diagram source lives in the docs-screenshot capture spec (skipped unless
CAPTURE_DOCS_MEDIA=1) so it regenerates deterministically at 2x.

* docs: make the how-it-works figure a real box-and-arrow diagram

The previous figure read as a list of rows. Replace it with a hand-drawn
SVG architecture diagram: the install pipeline feeds a spawned plugin
subprocess, then kandev and the plugin communicate over one gRPC
connection (DeliverEvent, HandleWebhook, and the plugin's Host API
callbacks), with the external webhook caller and the browser SPA as
edge nodes. Source lives in the capture spec's ARCH_HTML.

* docs: make the how-it-works diagram a clean monochrome flowchart

Drop the multi-color component diagram for a calm top-down flowchart
(mermaid's shape, drawn properly): install → verify → extract → spawn,
branching into event delivery and webhook relay, converging on the Host
API callbacks, with an optional native-UI step. Grayscale, no accent
colors. Source stays in the capture spec's ARCH_HTML.

* docs: dark plugin diagram + space out the two page images

- Make the how-it-works flowchart a dark rounded card (matches the docs
  page in dark mode; reads as an embedded card in light). Rendered with
  omitBackground so the corners are transparent.
- Move the Settings > Plugins list screenshot out of the intro (where it
  sat right before the diagram) down into the "Installing a plugin"
  section, so text separates the two images.

* docs: update install-dialog screenshot for aligned footer actions

Recaptured after fix(plugins): align install-dialog footer actions
(#1780) — Cancel and Install now sit together in one right-aligned
footer row instead of the Install button being stranded under the
dropzone.

* docs: account for /settings/system/storage in coverage.json

The storage settings route (added by #1699) was never recorded in the
public-docs coverage inventory — the docs validator is path-filtered to
docs/public/** so it didn't run for that PR. This docs PR is the first to
trip it. Add the route to system-health-and-maintenance.

* docs: review fixes — remove stale tools refs, tighten accuracy

Review pass against the now-merged main (tools removed in #1776):
- extending-kandev.md: drop "serves agent-invocable tools" from the plugin
  overview (the feature no longer exists).
- plugins-manifest.md: the legacy endpoints block is health/events/webhooks
  only (no tools path); and the `signed` field is always false today since
  signature verification isn't wired.
- plugins.md: install validation covers the categories/UI-surface enums (not
  a "capability vocabulary"); AutoMTLS secures the channel on all platforms,
  not just Windows; and status vs signing are separate badges.

* docs: explain what Host state is (GetState/SetState)

The authoring guide listed the state RPCs but never said what 'state'
means. Add a plain-language explanation: a per-plugin, kandev-persisted
key/value store keyed by (scope, scopeID, key) holding JSON objects,
namespaced per plugin, scoped to instance/workspace/task/agent, and how it
differs from the writable data dir. Make the example a real increment.

* docs: note NavItem section:'settings' is not rendered

A NavItem with section:'settings' is accepted by the type but no sidebar
section renders it (plugin-nav-items renders main/default, the Integrations
sections render 'integrations'); annotate that settings pages use
registerSettingsRoute. Addresses repeated review feedback.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 00:56:32 +01:00
João Salavisa 7e386561f2 fix: treat git-describe builds as ahead of latest release (#1787)
* fix: treat git-describe builds as ahead of latest release

The update checker flagged "Update available" for local builds whose version
is a git-describe string like v0.79.0-60-g8fae44fb1, even though such a build
is 60 commits AHEAD of the v0.79.0 release. parseSemver treated the
"-60-g8fae44fb1" suffix as a pre-release of 0.79.0, which sorts below the
plain tag, so the comparator reported the build as older than latest.

Recognize the git-describe "-<N>-g<hash>[-dirty]" suffix as a commits-ahead
marker: such a build now sorts at or above its base tag and above any real
pre-release, so an ahead-of-latest build is no longer flagged as
update-available. Genuine pre-releases (rc.1, alpha.2) are unaffected.

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

* fix: require default-length hash for git-describe detection

Tighten the git-describe suffix detector to require the abbreviated commit
hash to be at least 7 hex digits (git's default core.abbrev). This prevents a
contrived pre-release such as v1.0.0-3-ga from being misclassified as a
git-describe "ahead" build. kandev's Makefile runs `git describe` with the
default abbreviation, so real builds always meet the floor.

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>
2026-07-19 00:54:04 +01:00
João Salavisa d2d19aa1b5 fix: stop async client component error on automations settings (#1788)
The /settings/workspace/[id]/automations page was a leftover Next.js
server component (async, awaiting a params promise in render). Mounted
as a client component in the Vite SPA, it logged React errors:
"is an async Client Component" and "suspended by an uncached promise".

Convert it to a plain synchronous client component taking a workspaceId
prop and delegating data loading to AutomationsListPage, matching the
sibling repositories/workflows routes. Update the call site to pass
workspaceId directly instead of a resolved params promise.

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 00:50:38 +01:00
João Salavisa ad3e4d4c4b fix: point About documentation link to kandev.ai/docs (#1786)
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-18 23:48:14 +01:00
Carlos Florêncio 8fae44fb12 feat: unify configuration chat sessions (#1695) 2026-07-18 22:58:18 +01:00
Carlos Florêncio e9634fa636 feat: allow detaching subtasks from parent tasks (#1781) 2026-07-18 21:19:05 +01:00
Carlos Florêncio 18841d6c00 fix: indent multi-repo change trees (#1779) 2026-07-18 21:11:17 +01:00
João Salavisa a54c7dc1f4 fix(plugins): align install-dialog footer actions (#1780)
* fix(plugins): align install-dialog footer actions

The install dialog rendered a per-tab "Install" button inside each tab
body (left-aligned, under the input/dropzone) while DialogFooter held
only Cancel (right-aligned), so the two actions landed on different
rows and looked broken.

Lift the tab, url, and file state into InstallPluginDialog and render a
single DialogFooter with Cancel (outline) + Install (primary). The
Install button's testid, handler, and disabled state derive from the
active tab, preserving both install-plugin-url-submit and
install-plugin-upload-submit selectors. Install is the sole primary so
the base DialogContent fires it on Enter.

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

* fix(plugins): reset install-dialog inputs on close

A successful install closes the dialog by flipping the controlled `open`
prop directly (usePluginActions.afterInstall -> closeInstallDialog),
which never routes through onOpenChange. With the tab/url/file state now
lifted into InstallPluginDialog, the previous reset-in-handleOpenChange
approach left a stale file/url selected the next time the dialog opened,
making an accidental re-upload possible.

Reset the lifted state from an effect keyed on `open` so every close path
(programmatic close, Cancel, Escape, overlay) clears it. Adds a
regression test covering upload-install -> reopen -> empty state.

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>
2026-07-18 19:50:54 +01:00
Carlos Florêncio 5a691ea8b2 chore: improve harness verification guidance (#1777) 2026-07-18 19:08:55 +01:00
Carlos Florêncio 21f811d5ec feat: add idle storage maintenance (#1699) 2026-07-18 19:08:44 +01:00
Clément Fiere 2586c564b1 fix(web): navigate to Settings on the first gear click from a session view (#1775) 2026-07-18 18:25:10 +01:00