* fix(terminal): expose executor-profile env vars to terminal shells
PR #1971 got the profile env into the agent subprocess and the repository
setup script, but the terminal panel was still a gap: the local user shell
was started with `InteractiveStartRequest.Env` unset, so it inherited only
the backend process environment, and agentctl's per-terminal shells were
started from `shell.DefaultConfig` with no env overrides at all.
Local/worktree path: resolve the task environment's executor profile at
terminal start (revealing secret-backed entries) and pass it through
UserShellOptions.Env into the deferred PTY start. Resolution happens per
terminal rather than being cached at launch, so a terminal opened before
the agent ever runs still gets the vars.
Remote path (Docker/Sprites/SSH): StartTerminalShell now folds the agent
instance's env into the shell config, so a terminal inside the workspace
sees what the agent sees. SSH is unchanged in scope — its instance env is
still only the credential allowlist, not arbitrary profile variables.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(terminal): resolve terminal env from the session's executor profile
Address PR review. The resolver read executor_profile_id off the
task_environments row, but persistTaskEnvironment's reuse branch never
refreshes that column — it keeps whatever the first session stamped on it.
buildLaunchAgentRequest resolves the agent's env from
session.ExecutorProfileID instead, so a task environment reused by a later
session that picked a different profile of the same executor type would give
the terminal stale secrets and omit the ones the agent just received.
Prefer the session's profile and fall back to the environment row only when
the session recorded none. Also document why mergeAgentEnvIntoShellConfig
needs no defensive copy and why StartEnvForTesting is exported.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(terminal): give env-row fallback subtests distinct session IDs
Address PR review nit: both subtests shared the hardcoded "session-3" at
the call site, so the error-path case read as a repeat of the first.
Move the session ID into the table as session-no-profile / session-error.
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(plugins): implement Host data API write RPCs (CreateTask/UpdateTask/CreateComment)
The api_write manifest capability was a stored no-op. Wire the deferred
phase-2 write RPCs (ADR 0043) so closed-loop plugins (Sentry->task,
Slack slash-command task creation, refactor fan-out) can create and
update tasks and post comments.
- SDK: Host.Tasks() gains Create/Update; new Host.Comments() writer;
CreateTaskInput/UpdateTaskInput/Comment DTOs (pkg/pluginsdk).
- Gating: api_write:tasks gates Create/UpdateTask, api_write:comments
gates CreateComment; undeclared -> gRPC PermissionDenied, per-method
and independent of the read caps on the same resource.
- Writes route through the first-party service layer (task service for
tasks, office service for comments) via backendapp adapters, never a
repository -- so task.*/comment-created events fire and WS updates.
Server stamps source="plugin:<id>"; CreateTask resolves workspace/
workflow defaults and best-effort honors start_agent; UpdateTask uses
a conservative title/description/state/workflow_step_id mask.
- Fixture plugins exercise a CreateTask+CreateComment round-trip;
in-process, real-broker wire, and subprocess tests cover the round-trip
and per-capability denial.
- Docs: spec, GRPC-CONTRACT, HOST-DATA-API.proto, proto comments moved
writes deferred -> implemented; ADR 0043 amended with the concrete
service-layer routing decision.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(plugins): send a message to a task session via Host SendMessage (replaces CreateComment)
The office TaskComment write was the wrong primitive — it's an office/
dashboard construct, not a way to message the agent working a task, and
it only did anything under office reactivity. Replace the CreateComment
write RPC with SendMessage, which delivers a prompt to a task session
through the orchestrator's real delivery path (the same one message_task
uses).
- Proto: swap CreateComment/Comment for SendMessage(SendMessageRequest)
returns SendMessageResponse{session_id, status}; regenerated stubs.
- SDK: Host.Comments() removed; Host.Messages().Send(taskID, sessionID,
text) added (pairs with Messages().List), returning MessageDispatch.
- Gating: api_write:comments dropped; SendMessage gated api_write:messages
(independent of api_read:messages on the same accessor, like Tasks()).
- Delivery: a new pluginsTaskMessengerAdapter (backendapp) resolves the
target session (explicit id verified against the task, or the primary
session), records a user message stamped source="plugin:<id>", then
dispatches by state — running → queue; idle/completed → prompt (resume
if the agent process is gone); never-started → launch; failed dispatch
deletes the recorded message. Wired via SetWriteDeps (live-read).
- Tests: adapter state-machine unit tests (queue/start/prompt/resume/
delete-on-failure/belongs-to-task/no-primary/terminal); SDK, in-process
gating, wire, and subprocess round-trips updated to SendMessage.
- Docs: spec, GRPC-CONTRACT, HOST-DATA-API.proto, proto comments, and the
ADR 0043 amendment updated from comment-create to message-send.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(plugins): address PR review — nil guards, detached contexts, state validation
Follow-up fixes from the #1970 review (Claude + CodeRabbit bots):
- grpcHostServer.CreateTask/UpdateTask/SendMessage: nil-guard the returned
value before toProto() so a custom/test Host returning (nil, nil) yields a
gRPC Internal error, not a server panic (matches GetTask's pattern).
- host.go: fix stale server doc comment (Comments()/CreateComment →
Messages()/SendMessage).
- startTaskBestEffort and the post-resume wait+retry now run on
context.WithoutCancel(ctx): the task/resume is already committed, so a
plugin's incoming deadline must not silently abort the launch/resume.
- pluginsTaskWriterAdapter.UpdateTask validates the state string against the
known task-state enum (excluding orchestrator-owned SCHEDULING) and returns
InvalidArgument, so a plugin typo can't forward a bogus state to the service.
- Tests: add the resume-wait-timeout path (message deleted so a retry can't
duplicate), and a dedicated pluginsTaskWriterAdapter test covering
source→metadata mapping and state validation (both adapters now depend on
narrow interfaces for testability).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(plugins): launch start_agent asynchronously so CreateTask doesn't block
Review nit follow-up: startTaskBestEffort called StartTask synchronously, so
with the detached context the plugin's CreateTask RPC blocked on the
orchestrator's full inline launch path (office lookup, state update, workflow
step move, task fetch) with no bound — and the "asynchronous" comment was wrong.
Move the fire-and-forget into pluginsTaskStarterAdapter (backendapp, no goleak),
mirroring MCP's launchAutoStartTask: the launch runs in a goroutine on a
context.WithTimeout(context.WithoutCancel(ctx), AgentLaunchTimeout), and
StartTask returns immediately. The plugin host stays a simple synchronous call
(its taskStarter contract now documents "returns promptly, launches async").
Adds adapters_plugin_starter_test.go: launch invoked, returns before the launch
completes, detaches from a cancelled caller context, and swallows launch errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(plugins): document write validation + SendMessage rollback; harden starter test cleanup
Addresses the final PR #1970 review nits:
- adapters_plugin_starter_test.go: register the block-channel release via
t.Cleanup (sync.Once-guarded) before the goroutine can block, per AGENTS.md.
- GRPC-CONTRACT.md: document CreateTask/UpdateTask validation + error rules
(title required, id required, NotFound on missing task, invalid-state reject).
- spec.md: document that a failed SendMessage dispatch deletes the recorded
message, plus the required-field/NotFound/terminal-session rules.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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: 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
* 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>
* 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>
* feat: bind HTTP server to multiple addresses
Support a list of bind addresses so the backend can serve on loopback and
a tailnet IP at once, without exposing every interface via 0.0.0.0.
- Config: server.host now parses a comma-separated list; a new server.hosts
YAML array takes precedence when set. Entries are trimmed, deduped, and
validated (IP or hostname); a wildcard entry collapses the set.
- Startup: one net.Listener per resolved address serving one shared
http.Server; the single-listener path is the one-element case.
- Bind-failure policy: all-fail is fatal; partial-fail warns, serves the
good addresses, and runs a cancellable background retry loop that binds
late-arriving addresses (e.g. a tailnet IP after tailscaled starts).
- Graceful shutdown stops the retry loop before closing all listeners;
covered by goleak.
- Readiness probe picks a reachable bound address, preferring loopback.
- Add ServerConfig.NonLoopbackBinds and config.IsLoopbackHost so the
sibling app-auth task can gate fail-closed auth on off-box binds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: preserve KANDEV_SERVER_HOST over config server.hosts
server.host now wins over the server.hosts array when set, because Host
carries the KANDEV_SERVER_HOST env override and env must beat a config-file
server.hosts (env-over-file precedence and the desktop loopback contract).
The viper default for server.host is empty so an unset host is
distinguishable from an explicit one; server.hosts is used only when host is
unset. Adds regression tests covering the env-override and hosts-when-unset
paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: harden multi-bind host parsing and listener lifecycle
Address review feedback:
- Validate every host before wildcard collapse so an invalid entry is
rejected regardless of order; canonicalize IPs so equivalent forms
(::1 and 0:0:0:0:0:0:0:1) dedupe and unspecified longhand IPs count as
the wildcard.
- Probe the IPv6 loopback (::1) for an IPv6 wildcard listener instead of
127.0.0.1, so readiness doesn't stall on an IPv6-only socket.
- Make serverListeners.Stop race-free via sync.Once.
- Register t.Cleanup for the blocker listener before the fatal-assertion
path in the partial-failure test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>