Commit Graph

210 Commits

Author SHA1 Message Date
Clément Fiere 59d1cc7277 feat: add Slack-style unread divider to session transcript (#1922) 2026-07-31 14:42:14 +01:00
Carlos Florêncio 7ad1599b70 feat(web): show WebSocket connectivity warnings (#2083) 2026-07-31 14:23:16 +01:00
Carlos Florêncio 40702d9095 fix(settings): improve transcript navigation controls (#2064) 2026-07-30 20:32:34 +01:00
Clément Fiere 3eb8e4a59c feat: add transcript last prompt navigation (#1999) 2026-07-30 12:35:26 +01:00
José Almeida eab70052d2 fix(web): prevent blank screens during SPA recovery (#2024)
* fix(web): prevent blank screens during SPA recovery

* fix(web): address SPA recovery review feedback
2026-07-30 10:46:35 +01:00
Carlos Florêncio 6ba9f1b0fd feat: add quarantine retention lifecycle (#2049) 2026-07-30 10:44:51 +01:00
Carlos Florêncio f501346d15 feat: add visible WIP overflow queues (#2001) 2026-07-28 15:46:16 +01:00
Carlos Florêncio 7250cdbf61 feat: explain busy storage cleanup and allow override (#1993) 2026-07-28 09:23:42 +01:00
Carlos Florêncio 2bd2bae2ea feat: improve agent runtime update flow (#1982) 2026-07-27 19:34:13 +01:00
Carlos Florêncio d0d084bdc6 feat(agents): add user-managed runtime updates (#1950) 2026-07-27 12:21:47 +01:00
João Salavisa a7005e5d2f chore: revert agent subscription usage in settings and chat tooltip (#1940)
* chore: revert agent subscription usage in settings and chat tooltip

Reverts #1697 (55c8b461b). The subscription usage plugin now covers this,
so the built-in Settings > Agents section and the context-doughnut tooltip
rows are no longer needed.

Conflicts resolved to preserve later work on the same files:
- token-usage-display.tsx/.test.tsx: kept the pinnable tooltip (#1869),
  the rebuilt context-window layout (#1741), and the ACP/API source row
  (#1752); removed only SessionUsageRows and its imports.
- settings/agents/page.tsx: #1741 moved <AgentUsageSection /> above the
  pre-existing <Separator />, so the revert missed the JSX; removed it
  and kept the separator.
- agent-usage-section.tsx/.test.tsx: deleted (later changes were cosmetic
  tweaks to the section itself).

Office-side subscription surfaces (dashboard quota card, agent detail
QuotaCard, internal/agent/usage clients) predate #1697 and are untouched.

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

* fix(usage): keep #1697 client fixes when reverting its UI surface

#1697 bundled two things: the Settings > Agents section plus doughnut
tooltip rows, and a set of genuine bug fixes to the underlying provider
usage clients. Reverting it wholesale threw away the second group, which
is still live code behind the office dashboard quota card and the agent
detail QuotaCard.

Restores the client layer to its post-#1697 state (billing.go, cache.go,
client_claude.go, client_codex.go, fileutil.go, types.go and the two
client tests), fixing the defects CodeRabbit flagged on #1940:

- billing type is recomputed per call again instead of being frozen for
  the process lifetime by sync.OnceValue, so login/logout is observed
  without a backend restart
- usage cache regains fetch coalescing and short negative caching, so a
  provider outage no longer means one outbound call per request
- Claude usage parses float utilization and the richer limits[] array
  instead of failing to decode
- Claude token refresh patches only claudeAiOauth in a decoded map and
  writes atomically via temp+rename, preserving unknown keys in the
  credentials file the real CLI shares
- Codex reads tokens.access_token rather than a nonexistent flat token
  field, and its doc comment matches the ~/.codex/auth.json path

The removed UI/API surface stays reverted: GET /api/v1/agents/usage, the
host usage lister, the settings section, and the tooltip rows.

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

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:04:25 +01:00
Carlos Florêncio 75abf779ed feat(github): support per-workspace GitHub App registrations (#1810) 2026-07-26 10:39:08 +01:00
Carlos Florêncio a4d53bdf42 feat(web): remember task listing display preferences (#1944) 2026-07-25 19:30:52 +01:00
Carlos Florêncio 715a2033b2 fix: route update alerts through notification providers (#1938) 2026-07-25 11:07:06 +01:00
João Salavisa 919a39e49a feat(auth): opt-in authentication & multi-user segregation (#1930)
* feat(auth): add auth core — identities, sessions, PATs, invites, mode state machine

Phase 1 of opt-in authentication + multi-user segregation:
- internal/auth: argon2id password hashing (PHC), opaque session/PAT tokens
  (SHA-256 at rest), login rate limiter, auth-mode state machine
  (disabled/setup/enabled), setup-wizard promotion of the pre-auth
  default-user row into the admin account, invites, admin user management
  with last-admin guard.
- internal/auth/store: auth_identities (OIDC-ready), auth_sessions,
  auth_api_tokens, auth_invites tables (SQLite+Postgres dialects).
- internal/auth/authn: request-identity plumbing (gin + plain contexts),
  RequireAdmin middleware.
- users table: display_name/role/status columns via new MigrateLogger
  wiring; AccountRepository surface (consumer-scoped, keeps existing
  Repository fakes untouched).
- config: AuthConfig.Required/SessionTTLHours/CookieName
  (KANDEV_AUTH_REQUIRED), FeaturesConfig.Auth; profiles.yaml + runtime
  flag registry entry for KANDEV_FEATURES_AUTH.

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

* feat(auth): enforce opt-in auth over HTTP — middleware, auth API, boot payload

Phase 2 of opt-in authentication + multi-user segregation:
- internal/auth/httpmw: global gin middleware after CORS. Disabled mode
  injects the synthetic single-user admin identity (behavior unchanged);
  enabled mode resolves session cookie then PAT bearer, with an explicit
  allowlist (health, SPA bootstrap reads, credential endpoints,
  self-authenticating webhooks), deferral for WS/proxy upgrades and
  office agent JWTs, and SPA shell availability for the login page.
- internal/auth/httpapi: full auth surface — setup wizard, login/logout,
  me, password change, sessions, PATs, invites (mint/preview/accept),
  auth settings (enable/disable), admin user management.
- Boot payload: initialState.auth block always present; unauthenticated
  visitors on auth-enabled instances get features+auth only (no data).
- External MCP: PAT required once auth is enabled (open while disabled).
- Admin guards on destructive system routes and runtime-flag overrides.
- Startup warning when bound non-loopback with auth disabled
  (ServerConfig.NonLoopbackBinds fail-closed nudge).
- Wiring: auth store in Repositories, auth service in Services,
  ClaimUnownedWorkspaces backfill on the task repository.

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

* feat(auth): per-user workspace scoping across HTTP services

Phase 3 of opt-in authentication + multi-user segregation:
- Task service: workspaces, workflows, repositories, and tasks are scoped
  to the request identity (owner-only visibility; pre-auth unowned rows
  stay shared). Denials use *NotFound sentinels - no existence leak.
  Internal callers and the disabled-mode synthetic identity remain
  unscoped, preserving behavior exactly when auth is off.
- CreateWorkspace stamps the authenticated caller as owner.
- Session chokepoint: lifecycle Manager.GetOrEnsureExecution runs a
  settable session-access check covering shell/files/ports/vscode/LSP.
- Secrets: per-user user_id column, scoped CRUD/list, ClaimUnowned wired
  into the setup-wizard backfills.
- Boot payload inherits scoping automatically.

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

* feat(auth): scope WebSocket gateway to authenticated users

Phase 4 of opt-in authentication + multi-user segregation:
- Connection auth: /ws, /terminal, /lsp, /vscode, /port-proxy routes are
  guarded when auth is enabled - session cookie (same-origin upgrade),
  resolved identity from the HTTP middleware, or ?token=<PAT> for
  headerless programmatic clients; JSON 401 before any upgrade.
- Client identity: every WS client carries its authn.Identity; dispatched
  RPC actions (workspace.list, task CRUD, ...) run under a context
  carrying it, so WS RPC gets the same per-user scoping as HTTP.
- Subscription checks: task/session subscribe+focus verify workspace
  visibility via the task service; user topic subscriptions are pinned to
  the client's own user.
- Broadcast routing: new Hub.BroadcastToWorkspace routes workspace-scoped
  events (task/workflow/repo/office/session-state) to the owner's clients
  only, with safe fallbacks (unowned/unresolvable -> broadcast) so events
  never vanish; instance-wide events (executors, environments, agent
  profiles, system jobs) stay global by design.
- Replaced the long-standing discarded-token TODO in HandleConnection.

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

* docs(auth): add feature spec and backend architecture notes for opt-in auth

- docs/specs/auth/spec.md: product behavior (opt-in, setup wizard,
  per-user workspaces, sessions/PATs, invites, public surface, v1 limits)
- apps/backend/AGENTS.md: internal/auth package layout + scoping rules
  for new service entry points and WS broadcast call sites

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

* docs(auth): public documentation for opt-in authentication

- New docs/public/authentication.md: enabling, users & invites, PATs,
  public endpoints, limitations, disabling
- run-as-a-service.md: network-security note now points at the real
  authentication boundary instead of reverse-proxy-only guidance
- meta.json: nav entry under Configure Kandev

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

* docs(auth): ADR for opt-in authentication architecture

Records the load-bearing choices: mode state machine + synthetic identity
in disabled mode, opaque DB-backed credentials over JWTs, default-user
promotion at setup, service-layer scoping with no-admin-bypass, and the
explicit public allowlist.

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

* feat(web): auth UI — login, setup wizard, invites, users & account settings

Adds the frontend half of opt-in authentication: a pre-auth gate in
main.tsx that renders the setup wizard or login/invite pages instead of
the app shell whenever the boot payload's auth block says so, a new
`auth` store slice + feature flag mirroring the backend's StatePayload,
credentialed fetch (`credentials: "include"`) with a 401 handler that
clears the session and redirects to /login, and a full auth-api.ts
client for the /api/v1/auth and /api/v1/users routes.

Settings additions: a System > Users page (list/create/disable/promote
users, invite-link dialog) and System > Authentication page (enable/
disable with confirm + env-locked banner), gated on the `auth` feature
flag and admin role. A new Account sidebar group (Profile & Password,
API Tokens) is shown to signed-in users once auth is enabled.

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

* test(auth): E2E lifecycle suite + fix workspace event WS leak it caught

New dedicated Playwright auth project (backend restarts with
KANDEV_AUTH_REQUIRED=true, isolated from the tokenless default suite):
setup-mode boot gating, admin setup wizard, login/logout + bad
credentials, PATs, admin-surface authorization, anonymous WS rejection,
UI login flow, and the acceptance test - two-user segregation across
HTTP, boot payload, and WebSocket.

The segregation spec immediately caught a real leak: workspace.* event
payloads carry their ID as 'id' (the payload IS the workspace DTO), not
'workspace_id', so BroadcastToWorkspace fell back to broadcast-to-all
for workspace create/update/delete. Fixed with action-aware extraction
plus a unit regression test.

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

* fix(auth): close cross-user access gaps found in security audit

Two independent security audits of the auth branch found several
authorization gaps where session/workspace-scoped surfaces were not wired
to the new per-user chokepoint. All fixed:

- IDOR on reverse-proxy/terminal routes (vscode, port-proxy, terminal):
  these resolve executions via GetExecutionBySessionID /
  GetOrEnsureExecutionForEnvironment / EnsurePassthroughExecution, which
  skipped the session-access check that only GetOrEnsureExecution ran. Added
  the check inside the env/passthrough resolvers (before the cache
  short-circuit) and an explicit CheckSessionAccess gate in the two bare-lookup
  proxy handlers. An authenticated non-owner can no longer reach another
  user's IDE, forwarded ports, or shell.
- Cross-user agent-conversation read: ListMessages / ListMessagesPaginated /
  SearchMessages now authorize the session.
- Cross-user repository-script read/inject: script CRUD now authorizes the
  parent repository's workspace.
- Office HTTP surface: a group middleware enforces workspace ownership on
  :wsId routes for real users (agent-JWT callers keep their claim scoping).
- Login rate-limit bypass: SetTrustedProxies(nil) so a spoofed
  X-Forwarded-For can't rotate the limiter key on a directly-reachable
  backend.
- Invite double-provisioning race: MarkInviteUsed is now the atomic
  serialization point (consumed before user creation).
- Setup wizard: serialized with a mutex so concurrent first-visitors can't
  corrupt the promoted admin profile.
- Log sanitizer: explicit kandev_pat_ redaction rule.

Regression tests added for message/script scoping and the
execution-access-before-cache invariant. Auth E2E (incl. two-user
segregation) still green.

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

* fix(auth): scope repository active-session-count endpoint

GET /api/v1/repositories/:id/active-session-count went through
CountActiveSessionsByRepository, which was not identity-scoped - a
cross-user info leak of another owner's active session counts. Added the
same authorizeRepositoryID gate the script routes use.

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

* fix(auth): add json tags to auth store models — omit digests, fix token UI

The auth store structs (APIToken, Invite, LoginIdentity, Session) had db:
tags but no json: tags, so the handlers that serialize them directly sent
Go-cased keys (CreatedAt, Name) AND leaked the SHA-256/password digests to
clients. Two consequences:
- The API-tokens UI showed 'Invalid Date' and an empty name (frontend reads
  created_at/name).
- token_sha256 / password_hash were serialized in list responses.

Added json tags with snake_case keys the frontend reads and json:"-" on
every credential digest. Regression test asserts digests never serialize and
the snake_case timestamp/name keys are present. Also adds the
auth-screenshots capture spec (artifacts gitignored).

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

* feat(web): move auth enable into feature toggles; show current user in sidebar

Remove the standalone Authentication settings page and its dedicated
settings/get-auth-settings API surface now that enabling auth lives in the
Feature Toggles system, and drop env_required now that /auth/me no longer
returns it. Add a current-user chip with a logout action to the sidebar
footer, shown only in enabled auth mode.

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

* refactor(auth): make features.auth the single on/off control

Per review feedback: authentication configuration now lives entirely in
the existing Feature Toggles system rather than a separate Authentication
settings section.

- The auth mode is derived from the features.auth runtime flag
  (KANDEV_FEATURES_AUTH): off -> disabled, on+no-admin -> setup,
  on+admin -> enabled. Removed the parallel auth.mode system setting,
  the SetEnabled service method, GET/PATCH /api/v1/auth/settings, the
  AuthConfig.Required field, KANDEV_AUTH_REQUIRED, and the auth service's
  Settings-store dependency and EnvRequired/env_required.
- The features.auth registry entry is re-described as the actual enable
  switch (high-risk, restart-required) and defaults off in every profile
  (dev was previously true — that only revealed UI before; now it would
  enforce, so it must be an explicit opt-in).
- Setup no longer persists a mode; completing the wizard creates the admin
  and the mode auto-derives to enabled.
- Startup 'exposed without auth' warning + all docs (public guide,
  run-as-a-service, spec, ADR, AGENTS.md) point at the feature toggle.
- E2E: auth specs use KANDEV_FEATURES_AUTH; screenshot spec captures the
  Feature Toggles page instead of the removed Authentication page; the
  member-restriction assertion targets an admin invite route.
- Tests updated across service/httpmw/httpapi/profiles for the derived
  mode; full suite green (only pre-existing sandbox-FS failures remain).

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

* fix(auth): complete cross-user isolation — integrations, automation, plan/walkthrough, session reads

A follow-up isolation audit found that per-user scoping had been wired into
the task service, secrets, office, and the WS gateway — but several
workspace/task/session-scoped surfaces still resolved a caller-supplied ID
with no ownership gate. Closed all of them; cross-user access now returns
404 across the board.

- Integrations (jira/linear/sentry/slack/azure-devops/gitlab/github/
  workflow-sync): a global middleware authorizes the workspace_id (query, or
  gitlab's /workspaces/:id/ path) on those route groups. Previously a member
  who knew another user's workspace ID could read/overwrite their integration
  connection config and watches.
- Automation: workspace-authorizer seam gates get/list/create/update/delete/
  enable/disable, triggers, run history, and — critically — the webhook
  secret. Was fully cross-tenant over WS before.
- Task plans (PlanService) and walkthroughs (WalkthroughService): task-
  authorizer seam on every read/write. These bypassed the task service.
- Session reads that hit the repo directly — session turns, task context,
  git snapshots, file reviews — now call AuthorizeSession/TaskAccess.

Internal callers (pollers, schedulers, MCP) carry no identity and stay
unscoped; disabled mode is unaffected. Regression tests added
(plan/walkthrough scoping, automation authorizer matrix, integration path
extraction) and the two-user E2E now asserts integration routes are shielded.
Docs (spec, ADR, public guide) updated to state what is isolated and that
filesystem/agent-credential isolation remains an OS-level limitation.

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

* fix(web): current-user chip uses border hover, not accent fill

The sidebar current-user chip used hover:bg-accent (a purpleish fill).
Switch to the border-hover idiom used elsewhere (border border-transparent
-> hover:border-border) with the neutral hover:bg-muted/50 the adjacent
workspace picker uses, so the hover reads as a subtle outline instead of a
colored background. Screenshot spec now captures the chip hover state.

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

* fix(web): right-align current-user chip in the sidebar footer

Push the user chip to the far right of the expanded footer (ml-auto) so it
sits opposite the left-aligned icon buttons; collapsed mode keeps the
centered vertical stack unchanged.

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

* fix(auth): reject synthetic identity on management routes and scope office run broadcasts

Address code-review findings on the opt-in authentication PR:

- P1: while auth is disabled the middleware injects a synthetic admin
  (RoleAdmin), and RequireAdmin alone let it through — so anyone hitting a
  not-yet-enabled instance could mint a PAT or plant an admin that survives
  enablement and hijacks first-run setup. Add authn.RequireRealIdentity and
  guard the credential/session/user-management routes with it (public
  bootstrap routes stay reachable). Responds 404 while disabled.
- P1: office run events carry task_id but no workspace_id, so they fell back
  to a global broadcast and leaked across users. Resolve the workspace from
  the task and route via the new Hub.BroadcastToWorkspaceOrDrop, which fails
  closed (drops rather than global-broadcasts) when the workspace is unknown
  and auth is enforced.
- nit: type createAuthSlice as a single-arg creator and call it with just set.

Also register the authentication docs page and its settings routes in
docs/public/coverage.json so the public-docs validator passes.

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-25 00:10:22 +01:00
Clément Fiere f85b648d97 feat: notify user when a kandev update is detected (#1924)
* feat: notify user when a kandev update is detected

Adds a background-poller-driven update notification with a saved
preference for delivery channel (desktop / in-app banner / both) and
an enable/disable switch on the Updates settings page.

Backend:
- internal/system/updates: NotifySettings + NotifyStore persist the
  enable/channel preference on the existing install-wide settings
  table; notifyIfNewUpdate broadcasts a WS event the first time a
  newer release is observed, deduped durably via kandev_meta so a
  restart or repeat poll doesn't re-announce the same version.
- New GET/PUT /api/v1/system/updates/notification-settings endpoints.
- pkg/websocket: new system.update_available action.

Desktop (Tauri):
- native_notifications.rs: task_id is now optional (Option<String>,
  required only for session-scoped events) and the event allowlist
  accepts system.update_available: alongside the existing session
  events, so the update notification can use the native bridge.

Frontend:
- New UpdateNotificationsCard on the Updates settings page (Switch +
  channel Select), wired through the shared settings-save-provider
  floating Save/Discard action.
- useUpdateAvailableToast bridges the WS event to an in-app toast
  and/or the native/browser desktop notification, per the saved
  channel, deduped per version.

* docs: document the update notification channel setting

* fix: hydrate update notification settings and broadcast delivery ack

- Only mark a release notification as delivered when the broadcaster
  reports at least one connected client, closing a race where an
  update could be silently dropped before the first client connects.
- Hydrate updateNotificationSettings through the settings boot payload
  (loadSettingsInitialState -> hydrateState) instead of a component-
  level fetch, per the boot-payload -> store -> component data flow.
  Widen the hydration types (HydrationState) so callers can hydrate a
  partial system slice without fabricating the other system fields.
  UpdateNotificationsCard now only reads/writes the subscribed store
  state.
- Fix module-scope settings-fetch cache leaking across tests in
  use-update-available-toast.test.ts so the fetch-failure test
  actually exercises the catch/fallback path.
- Add API-client contract tests for fetchUpdateNotificationSettings
  and saveUpdateNotificationSettings (GET cache policy, PUT method
  and body).

* test: cover delayed hydration and boot-payload mapping for update notifications
2026-07-24 22:40:24 +01:00
Carlos Florêncio 65c303d37f fix: distinguish session notification events (#1918) 2026-07-24 16:39:43 +01:00
João Salavisa 5db3e48262 feat: add opt-in plugin auto-updater (#1903)
* feat: add opt-in plugin auto-updater

Automatically update installed plugins in the background, off by default
and opt-in. Builds on the existing verified install pipeline — an
auto-update is just an automatic reinstall of a newer catalog package_url.

Two-tier toggle on Settings > Plugins: an instance-wide default (SQLite
plugin_settings) plus a per-plugin override (tri-state auto_update on the
plugin record, carried forward on upgrade). The AutoUpdatePoller
(~90-min cadence + a boot sweep) upgrades only plugins that are currently
active, opted in, and report update_available; disabled/errored plugins
stay on their installed version.

New endpoints: GET/PUT /api/plugins/settings and
PUT /api/plugins/:id/auto-update.

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

* fix(plugins): recheck auto-update eligibility at install and fix test mock

Address PR #1903 review feedback:
- Re-check each plugin's current status + effective opt-in immediately before
  InstallFromURL (eligibleForAutoUpdate), closing the TOCTOU window between the
  candidate snapshot / catalog fetch and the install so a plugin disabled or
  opted out mid-sweep is never reinstalled and reactivated.
- Correct the marketplace spec to describe the real failure behavior (shared
  with manual updates) instead of promising a version rollback the install
  pipeline does not perform.
- Fix the frontend CI failure: the plugins page test mocked the whole
  plugins-api module without the new settings/auto-update functions, so
  useAutoUpdateSettings threw at mount.
- Add an aria-label to the per-plugin auto-update Switch and Reset control.
- Add a use-auto-update-settings hook test (load, failure, optimistic setDefault,
  rollback) and an eligibleForAutoUpdate unit 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-24 13:04:54 +01:00
João Salavisa 99dfbbde58 feat(plugins): add keybindings and modal window capabilities (#1895)
* feat(plugins): add keybindings and modal window capabilities

Add two independent plugin UI capabilities.

Keybindings: plugins declare keybindings in the manifest under
ui.keybindings ({id, default, description}), validated backend-side
(combo grammar + unique ids, requires a UI bundle) and exposed to the
frontend. Plugin JS binds a handler via registry.registerKeybinding(id,
handler); a global dispatcher fires it. Combos are user-overridable in
Settings > Keyboard Shortcuts (namespaced plugin:{pluginId}:{id}) with
conflict detection against core and other plugins.

Modals: host.openModal({title?, content, size?, dismissible?}) returns a
{ close() } handle and renders in a single host-owned Dialog isolated by
PluginErrorBoundary, cleaned up on plugin unload.

Fixture plugin, PLUGIN-API.md / spec, and a Playwright e2e demonstrate a
keybinding handler opening a modal.

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

* fix(plugins): address review feedback on keybinding dispatch and validation

Core shortcuts now take precedence over plugin keybindings on a colliding
combo (defaultPrevented cooperation between the two capture-phase
dispatchers), a throwing plugin handler no longer aborts dispatch to other
plugins, and the keybinding combo grammar is tightened: the backend validator
and frontend parser both drop the undocumented control/super modifier
aliases and reject shift+digit/symbol combos that can never actually fire
(KeyboardEvent.key reports the shifted character, not the token).

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

* fix(plugins): stop conflicting core shortcut from double-firing with plugin keybindings

The earlier defaultPrevented guard only covered the central useAppShortcuts
dispatcher. Per-component core shortcuts registered via useKeyboardShortcut
run in the bubble phase, after the capture-phase plugin keybinding dispatcher,
so a plugin-handled-and-prevented combo still triggered the core action too.
Guard useKeyboardShortcut (and use-editor-keybinds, another capture-phase core
listener) on event.defaultPrevented so exactly one action fires per combo.

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

* fix(plugins): make core shortcuts always win over plugin keybindings

Panel-search, task-switcher, and other core listeners never checked
event.defaultPrevented, so a plugin bound to the same combo (e.g. Cmd/Ctrl+F)
could still fire alongside them. Instead of patching every core listener,
usePluginShortcuts now computes the set of effective core shortcut combos
(registry, resolved through user overrides, plus FIND_IN_PANEL/SAVE) and
skips invoking any plugin handler whose combo is in that set, so the
matching core listener always handles the event exactly once.

Also adds a hook-level regression test for useEditorKeybinds' existing
defaultPrevented guard, and fixes a leaked capture-phase window listener in
use-keyboard-shortcut.test.ts that could bleed into later tests.

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

* fix(web): keep Cmd+K closing the command panel after shortcut-precedence guard

cmdk's built-in vim bindings intercept Ctrl/Cmd+K and +P as list-navigation
shortcuts and call preventDefault() before the event reaches our window-level
useKeyboardShortcut listeners. Once that hook started bailing on
event.defaultPrevented (added to fix core/plugin shortcut double-firing), the
palette's own Cmd+K close/reopen toggle got silently suppressed whenever focus
was inside the open panel. Disable cmdk's vim bindings on the command palette
so its Ctrl/Cmd+K and +P always reach the app-level toggle.

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

* docs(plugins): document keybindings and modal window plugin APIs

Fill in the public author-facing docs that the internal plugin-keybindings
and modal-window contract (PLUGIN-API.md, spec.md) already covers: the
registry.registerKeybinding / host.openModal JS surface, the ui.keybindings
manifest field with its combo grammar, and a capability mention on the
Plugins overview page.

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-24 11:45:28 +01:00
Mathieu Virbel b4239d885e feat(agents): add configurable command prefix for sandboxed ACP launch (#1888) 2026-07-24 10:29:37 +01:00
Carlos Florêncio 2751b932df perf: cache storage analysis results (#1911) 2026-07-24 09:55:40 +01:00
Carlos Florêncio 77ef32c86c feat: add simplified resource metrics display (#1904) 2026-07-23 21:24:10 +01:00
João Salavisa ac6d8eb3be feat: resolve @prompt references in workflow prompts (#1865)
* feat: resolve @prompt_name saved-prompt references in workflow step prompts

Workflow step prompts, the GitHub review-watch task prompt, and the CI
auto-fix prompt now resolve "@name" references to saved custom prompts
(Settings > Prompts) at invocation time, appending the referenced
content as hidden context while keeping the mention visible - matching
how chat messages and message_task already resolve @mentions. Editing
a saved prompt now propagates to every workflow step that references
it without needing to update each step individually.

Adds a Monaco "@" completion provider to the step-prompt and
review-watch editors, alongside the existing "{{placeholder}}"
provider.

* fix: address PR review findings on prompt-reference expansion

- Make AppendReferenceExpansions idempotent: a prompt that already
  contains a previously-appended expansion block is returned unchanged,
  since the block's own "### @name" headers would otherwise be
  re-scanned as fresh mentions on a second call.
- Skip "@name" expansion entirely for CLI-passthrough sessions: their
  prompts are written raw to a PTY with no <kandev-system> stripping,
  so the hidden block would otherwise leak into the terminal verbatim.

* fix: fail safe on passthrough lookup error in startTask

A session-lookup error while resolving isPassthrough for the Kandev
MCP wrap and prompt-reference expansion previously defaulted to
false (non-passthrough), risking a hidden <kandev-system> block
leaking into a real passthrough session's PTY on a transient error.
Extracted resolveIsPassthroughForLaunch, which now fails safe by
defaulting to true (skip injection) and logs a warning.

* fix: scope expansion idempotency marker to system blocks

The idempotency guard added in a prior commit checked for the
expansion marker anywhere in the prompt, which could falsely
suppress a legitimate first-time expansion if caller-controlled
text happened to contain that literal phrase. Add
sysprompt.HasSystemMarker, mirroring the existing HasKandevContext
pattern, so the marker is only recognized inside a formatter-
produced <kandev-system> block. Also strengthens a passthrough test
assertion that didn't actually distinguish the snapshot path from
the fail-safe default.

* fix: match exact expansion-block prefix in idempotency guard

The prior scoping fix accepted the marker anywhere inside any
<kandev-system> block, which could still false-positive against an
unrelated system block (e.g. a plan-mode prefix or MCP context
wrap) that happens to mention the marker phrase without it being
the block's own content. Check for the exact literal prefix this
package's formatter produces instead: the opening tag immediately
followed by the marker, with nothing in between.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-23 13:14:54 +01:00
José Almeida 9c16d337a2 feat(web): add global app status surface (#1869)
* feat(web): add app status bar

* feat(web): add app status bar visibility toggle

* feat(web): persist status bar item order

* fix(web): refine app status bar behavior

* fix(web): polish app status bar presentation

* test(web): split plugin slot coverage groups

* fix(web): address status surface review feedback

* fix(web): stabilize status bar overlay tests
2026-07-23 12:28:54 +01:00
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
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
Carlos Florêncio 4b6d603f6d feat(github): explain repository scope (#1842) 2026-07-21 19:24:02 +01: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
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
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
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 ba75922fa1 feat: add configurable task layout profiles (#1799) 2026-07-20 12:09:06 +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
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
Carlos Florêncio 5fb1923a9a feat: add explicit save for settings (#1686) 2026-07-19 14:21:36 +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
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 21f811d5ec feat: add idle storage maintenance (#1699) 2026-07-18 19:08:44 +01:00
João Salavisa 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>
2026-07-18 17:03:13 +01:00
João Salavisa 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>
2026-07-17 22:13:58 +01:00
Carlos Florêncio 34a031f9b9 feat(web): warn about workflow replay cycles (#1720) 2026-07-17 18:47:27 +01:00
João Salavisa 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>
2026-07-17 12:00:26 +01:00
Carlos Florêncio 685b17be26 fix(web): refresh agent capability status (#1736) 2026-07-16 21:45:35 +01:00
João Salavisa 3952e35c8d fix(web): keep settings section actions aligned on narrow screens (#1727)
The section header squeezed the actions toolbar next to the title, so
on smaller widths the Workflows toolbar wrapped with its second row
left-dangling (Add Workflow floating alone). The header now stacks the
actions under the title below sm, protects them from compression by
long descriptions, and right-aligns wrapped toolbar rows beside the
title.

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:21:19 +01:00
João Salavisa 2a1f7c2edf feat: sync workflows from a configured github repo (#1721)
* feat: sync workflows from a configured github repo

Per-workspace setting pointing at a GitHub repo directory of portable
workflow export files. A background poller fetches it on an interval and
creates, updates, or deletes the matching workflows; a force-sync endpoint
bypasses the content-hash check. Synced workflows carry source/source_path
provenance and coexist with manual ones; changes that cannot be applied
safely (steps or workflows that still hold tasks, ambiguous step names,
unparseable files) surface as warnings in the new settings section.

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

* feat(web): fill workflow sync form from a pasted github link

A new Repository link field parses plain repo URLs, SSH remotes, and
tree/blob links (which also carry branch and directory) into the
structured owner/repo/branch/path fields.

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

* feat(web): simplify workflow sync form to link, branch, and interval

The repository link is now the only repo input: owner, repo, and
directory resolve from the pasted URL (echoed as a summary under the
field), and a stored config renders back into a canonical tree link via
buildGitHubRepoUrl. Branch and poll interval stay individually editable.

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

* feat(web): collapse workflow sync into a button, dialog, and status card

The Workflows settings page now shows only a GitHub Sync button by
default; the link/branch/interval form lives in a dialog that closes on
successful save or removal. Once configured, a compact status card
(mirroring the GitHub connection-status card) shows the synced repo,
branch, directory, last result, warnings, and a Sync now action.

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

* feat(web): move github sync button into the workflows page header

The GitHub Sync trigger now stacks under the Workspace settings button
on the top right, and the extra separator around the sync area is gone —
unconfigured workspaces show no sync chrome at all, configured ones show
just the status card above the workflow list.

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

* feat(web): move github sync button next to the workflow actions

GitHub Sync now sits in the Workflows section toolbar alongside
Export All, Import, and Add Workflow; the status card renders inside the
section above the workflow list. The toolbar moved to its own component
to keep the page under the file-size limit.

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

* feat(web): separate sync status card from the workflow list

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

* fix(web): align sync status card with the workflow list gutter

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

* fix: repair local edits to synced workflows on periodic sync

Periodic syncs previously skipped applying when the repo content hash
was unchanged, so local UI edits to a synced workflow were only reverted
by a manual Sync now. Every sync now reconciles against the repo; the
applier compares each step against its definition and only writes (and
broadcasts) actual differences, so a no-drift sync stays silent.

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

* feat: make github-synced workflows read-only

Workflows with source=github reject UI mutations: workflow rename,
description, agent-profile, and delete return 409 from the task-domain
handlers; step create/update/delete/reorder and template application are
guarded in the workflow controller. The sync applier writes through the
service/repo layer and stays exempt. The frontend disables the card's
inputs, save/delete, and the step pipeline editor for synced workflows,
with a tooltip pointing at the synced repository. Drift repair on sync
remains as the backstop.

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

* feat: optional workflow sync polling and repo-derived branch picker

A per-workspace auto-sync toggle (poll_enabled, default on) controls
the background polling loop; when off the workspace only syncs via Sync
now. The dialog's branch field becomes a select fed by the repository's
actual branches (same loader the task-create dialog uses), falling back
to free text until branches load. Existing dev databases gain the new
column via an idempotent ALTER.

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

* feat: release synced workflows when the sync config is removed

Deleting a workspace's workflow sync configuration converts its
GitHub-sourced workflows back to manual ownership (source=manual), so
they become editable instead of being stranded read-only. Release runs
before the config row is deleted so a failure keeps the config for
retry. The settings page reloads after removal (and after syncs that
changed workflows) so cards unlock without a manual refresh.

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

* feat(web): drop branch selector and compact the auto-sync row

The pasted link already carries the branch (main by default), so the
dialog no longer needs a branch field — the resolved summary echoes it.
The auto-sync toggle is now one compact row with the interval inline and
a single short helper line.

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

* fix: address PR review feedback

Backend: fail-open read-only guard on workflow lookup errors (fixes MCP
step handler CI failures); poller Stop holds its mutex through drain;
per-workspace lock serializes syncs against config delete/replace;
rollback workflow creation when provenance stamping fails; idiomatic
duplicate-column migration via db.IsDuplicateColumnError; branch-name
validation and a 30-day interval cap; generic 500s with logged detail;
WS read-only rejections use CONFLICT with the real reason; e2e reset
aborts when sync-config cleanup fails; test hygiene (t.Cleanup,
SetMaxOpenConns).

Frontend: malformed percent escapes parse as invalid instead of
throwing; save disabled during initial load and for invalid intervals;
Remove disabled while saving; stale async config responses discarded;
delete-dialog confirms re-check read-only; focusable Synced badge;
explicit button types; failed polls labeled 'last attempt'; unique
warning list keys; test cleanup via afterEach.

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

* fix: guard workflow sync test fake against concurrent poller access

The poller invokes the fake applier from its own goroutine while the
synctest test asserts on call counts; -race caught the unguarded reads.
Also documents that the per-workspace lock is deliberately held across
the GitHub fetch.

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-16 08:31:20 +01:00
Carlos Florêncio d763b8c0c9 feat(desktop): add native app integrations (#1715) 2026-07-15 23:19:00 +01:00
Carlos Florêncio af6756bbbc feat: add archive confirmation preference (#1708) 2026-07-15 19:34:18 +01:00