* feat(plugins): add auth capability for plugin-provided OIDC/SAML login
Plugins can now authenticate a visitor against an external IdP (OIDC/SAML)
and contribute a login button to the pre-auth screen:
- new `auth` manifest capability + a capability-gated X-Kandev-Auth-Login
webhook-response directive; the host mints the kandev_session (ADR 0050),
the plugin never receives the raw token. Plugin-set Set-Cookie is dropped.
- auth.Service.AuthenticateExternal: existing-identity / link-by-email / JIT
member provisioning; refuses to auto-link to an admin; verified-email is
the plugin's contract.
- auth_providers manifest block + plugins.SSOProviders() populate the
anonymous boot payload (auth.ssoProviders); the login page renders a
"Continue with <provider>" button per provider.
- ADR 0050 + plugin-manifest / plugin-authoring / auth spec docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: exclude publish_review_findings_kandev from public-docs coverage
The native code-review agent (#1957) added the publish_review_findings_kandev
MCP tool but did not update docs/public/coverage.json; that PR did not touch
docs/public so the path-filtered validator never ran. It's an internal review
reporting tool, not a user-facing feature, so record it as an exclusion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(plugins,auth): address review on SSO directive and JIT provisioning
- takeAuthLoginDirective strips every case variant of X-Kandev-Auth-Login and
rejects (403) when a plugin sends more than one, so no variant leaks to the
browser and login never uses a nondeterministically-picked assertion.
- provisionExternalUser rolls back the just-created user when the identity
insert fails (transient error or a lost UNIQUE(provider,subject) race) and
resolves to the winning identity, so no account is left without a login and
no email is reserved by an unusable row. Adds userstore.DeleteUser.
- auth spec: mark the login-page SSO buttons + provider discovery as landed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: drop redundant coverage exclusion; main now covers publish_review_findings_kandev
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>
* feat(review): navigate to code-review findings from the Changes panel
The findings count in the review top bar was a passive badge, so a user
had to scroll the diff to hunt for each inline finding. Turn it into a
navigator: it opens a popover listing every open finding grouped by file
(most-severe first), and clicking one jumps to the finding's card in the
diff, scrolls it into view, and briefly flashes it.
- lib/review/navigation.ts: navigateToFinding selects the finding's file,
emits an event, then polls across frames (files render lazily) for the
card and scrolls + flashes it. Reuses the composite <repo>\x00<path>
key so cross-repo same-named files stay distinct.
- review-findings-overview + review-findings-button: the grouped list and
its click-to-open popover trigger, mirroring the FixCommentsButton /
ReviewCommentsOverview pattern. The badge moves out of ReviewRunButton
into the top bar, keeping the review-open-count testid and copy.
- inline-review-finding: stamps data-review-finding-id + scroll margin so
navigation can target the card.
- unanchored-findings-banner: auto-expands when a navigation targets one
of its stale findings so that card renders and can be scrolled to.
- globals.css: review-finding-flash highlight, respecting reduced motion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(review): harden finding navigation from code-review feedback
Address automated review of the findings navigator:
- navigation.ts: scope the finding lookup to the open review dialog so a
duplicate card rendered by the Changes panel behind it is not scrolled
to instead; re-emit the navigate event each poll frame so a lazily
mounted stale-findings banner still expands (a one-shot event fired
before the section rendered was missed); honor prefers-reduced-motion
for the scroll, matching the flash animation.
- review-dialog-surface: clear the sidebar file filter before selecting a
finding's file, so a finding in a filtered-out file still mounts.
- review-findings-button: surface a warning when the card can't be reached
instead of discarding the promise silently.
- review-findings-overview: compute the file directory once per row.
- Tests: unit coverage for the banner auto-expand and the findings button
(null render + navigate), plus a mobile-* e2e that taps the navigator
and verifies the target card is reached.
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>
* 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>
* 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>
* 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>
* 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
* 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>
* 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>
* 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>
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>
* 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>
* feat(web): improve mobile task and kanban navigation
* feat(web): refine mobile task navigation
* fix(web): address mobile navigation review feedback
* test(web): cover task title field modes
* fix(web): keep mobile board navigator open across workflows
* fix(web): address mobile workflow review feedback
* 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>
* 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>