* 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>
25 KiB
Backend (Go) — architecture and conventions
Scoped guidance for apps/backend/. Repo-wide rules (commit format, code-quality limits, etc.) live in the root AGENTS.md.
Package Structure
apps/backend/
├── cmd/
│ ├── kandev/ # Main backend binary entry point
│ ├── agentctl/ # Agentctl binary (runs inside containers or standalone)
│ └── mock-agent/ # Mock agent for testing
├── internal/
│ ├── agent/
│ │ ├── runtime/ # Agent runtime: single seam for Launch/Resume/Stop/observe
│ │ │ ├── lifecycle/ # Agent instance management (moved from agent/lifecycle)
│ │ │ ├── agentctl/ # HTTP client for talking to agentctl (moved from agentctl/client)
│ │ │ └── routingerr/ # Provider error classifier + sanitizer + ProviderProber registry
│ │ ├── agents/ # Agent type implementations
│ │ ├── controller/ # Agent control operations
│ │ ├── credentials/ # Agent credential management
│ │ ├── discovery/ # Agent discovery
│ │ ├── docker/ # Docker-specific agent logic
│ │ ├── dto/ # Agent data transfer objects
│ │ ├── executor/ # Executor types, checks, and service
│ │ ├── handlers/ # Agent event handlers
│ │ ├── registry/ # Agent type registry and defaults
│ │ ├── settings/ # Agent settings
│ │ ├── mcpconfig/ # MCP server configuration
│ │ └── remoteauth/ # Remote auth catalog and method IDs for remote executors/UI
│ ├── auth/ # Opt-in user authentication + per-user scoping
│ │ ├── authn/ # Request-identity plumbing (context + gin helpers, RequireAdmin)
│ │ ├── httpmw/ # Global enforcement middleware (allowlist, synthetic identity)
│ │ ├── httpapi/ # /api/v1/auth/* + /api/v1/users endpoints
│ │ └── store/ # auth_identities, auth_sessions, auth_api_tokens, auth_invites
│ ├── agentctl/
│ │ └── server/ # agentctl HTTP server
│ │ ├── acp/ # ACP protocol implementation
│ │ ├── adapter/ # Protocol adapters + transport/ (ACP, Codex, OpenCode, Copilot, Amp)
│ │ ├── api/ # HTTP endpoints
│ │ ├── config/ # agentctl configuration
│ │ ├── instance/ # Multi-instance management
│ │ ├── mcp/ # MCP server integration
│ │ ├── process/ # Agent subprocess management
│ │ ├── shell/ # Shell session management
│ │ └── utility/ # agentctl utilities
│ ├── orchestrator/ # Task execution coordination
│ │ ├── dto/ # Orchestrator data transfer objects
│ │ ├── executor/ # Launches agents via lifecycle manager
│ │ ├── handlers/ # Orchestrator event handlers
│ │ ├── messagequeue/ # Message queue for agent prompts
│ │ ├── queue/ # Task queue
│ │ ├── scheduler/ # Task scheduling
│ │ └── watcher/ # Event handlers
│ ├── task/
│ │ ├── controller/ # Task HTTP/WS controllers
│ │ ├── dto/ # Task data transfer objects
│ │ ├── events/ # Task event types
│ │ ├── handlers/ # Task event handlers
│ │ ├── models/ # Task, Session, Executor, Message models
│ │ ├── repository/ # Database access (SQLite)
│ │ └── service/ # Task business logic
│ ├── office/ # Autonomous agent management (Office feature)
│ │ ├── agents/ # Agent instance CRUD + auth guards
│ │ ├── approvals/ # Approval requests and decisions
│ │ ├── channels/ # External integration channels (webhooks)
│ │ ├── config/ # Config sync (DB ↔ filesystem)
│ │ ├── configloader/ # Filesystem config reader/writer
│ │ ├── costs/ # Cost tracking and budget policies
│ │ ├── dashboard/ # Dashboard API, issues, activity, live runs
│ │ ├── infra/ # GC, reconciliation
│ │ ├── labels/ # Task labels
│ │ ├── onboarding/ # Workspace onboarding wizard API
│ │ ├── projects/ # Project management
│ │ ├── repository/ # Office SQLite persistence
│ │ ├── runtime/ # Agent run context, capabilities, and runtime action surface
│ │ ├── routines/ # Scheduled recurring tasks
│ │ ├── routing/ # Provider routing: resolver, validators, catalogue, backoff, agent-overrides
│ │ ├── scheduler/ # Wakeup scheduler (duplicate of service scheduler features)
│ │ ├── service/ # Core office service (wakeups, event subscribers, execution policy)
│ │ ├── shared/ # Shared interfaces and activity logging
│ │ ├── skills/ # Skill injection and materialization
│ │ └── workspaces/ # Workspace deletion handler
│ ├── events/ # Event bus for internal pub/sub
│ ├── gateway/ # WebSocket gateway
│ ├── github/ # GitHub API integration (PRs, reviews, webhooks)
│ ├── common/ # Shared utilities, config, logger
│ ├── integration/ # External integrations
│ ├── integrations/ # Shared shapes for third-party integrations
│ │ ├── healthpoll/ # Reusable 90s auth-health Poller (used by jira, linear)
│ │ └── secretadapter/ # Upsert-style adapter over secrets.SecretStore
│ ├── jira/ # Jira/Atlassian Cloud integration (config, REST client, poller)
│ ├── linear/ # Linear integration (config, GraphQL client, poller)
│ ├── lsp/ # LSP server
│ ├── mcp/ # MCP protocol support
│ ├── health/ # Health check endpoints
│ ├── notifications/ # Notification system
│ ├── persistence/ # Persistence layer
│ ├── prompts/ # Prompt management
│ ├── repoclone/ # Repository cloning for remote executors
│ ├── scriptengine/ # Script placeholder resolution and interpolation
│ ├── secrets/ # Secret management
│ ├── sprites/ # Sprites AI integration
│ ├── sysprompt/ # System prompt injection
│ ├── tools/ # Tool integrations
│ ├── user/ # User management
│ ├── utility/ # Shared utility functions
│ ├── workflow/ # Workflow engine
│ │ ├── engine/ # Typed state-machine engine
│ │ ├── models/ # Workflow step, template, and history models
│ │ ├── repository/ # Workflow persistence (SQLite)
│ │ └── service/ # Workflow CRUD, step resolution, and sync apply
│ ├── workflowsync/ # GitHub workflow sync (per-workspace repo config, poller, force sync)
│ └── worktree/ # Git worktree management for workspace isolation
Key Concepts
Orchestrator coordinates task execution:
- Receives task start/stop/resume requests via WebSocket
- Delegates to lifecycle manager for agent operations
- Handles event-driven state transitions via workflow engine
- Located in
internal/orchestrator/
Watcher Dispatch Coordinator (internal/orchestrator/watcher_dispatch.go) is the single pipeline that turns a freshly-observed external issue (Linear, Jira, future) into a Kandev task. Bus subscribers for each integration forward the event to WatcherDispatchCoordinator.Dispatch with a per-integration WatcherSource implementation (source_linear.go, source_jira.go). Source methods carry the integration-specific bits (reserve dedup, build task request, attach task ID, release, auto-start params); the coordinator owns the cross-cutting pipeline (create task, decide auto-start, error/release handling). Add a new watcher = implement WatcherSource + register a one-line bus subscriber. Do NOT add another createXIssueTask mirror.
Workflow Engine (internal/workflow/engine/) provides typed state-machine evaluation:
Engine.HandleTrigger()evaluates step actions for triggers (on_enter, on_turn_start, on_turn_complete, on_exit)TransitionStoreinterface abstracts persistence (implemented byorchestrator.workflowStore)CallbackRegistrymaps action kinds to callbacks (plan mode, auto-start, context reset)- First-transition-wins: multiple transition actions in one trigger, first eligible wins
EvaluateOnlymode: engine evaluates without persisting, caller orchestrates on_exit → DB → on_enterRequiresApprovalon actions: transitions requiring review gating are skipped- Idempotent by
OperationID; session-scoped data bag viaMachineState.Data
Agent Runtime (internal/agent/runtime/) is the single seam for launching, resuming, stopping, and observing agent executions. ADR 0004 introduced this in Phase 1 of task-model-unification. The public surface is runtime.Runtime (runtime.go); a thin facade (facade.go) delegates to a Backend (satisfied by *lifecycle.Manager).
Convention: only internal/agent/runtime/ (and code that pre-dates Phase 1 migration) may import runtime/lifecycle or runtime/agentctl directly. New consumers — workflow engine actions, cron-driven trigger handlers, future task-tier callers — should depend on runtime.Runtime. Existing call sites are migrated through later phases of task-model-unification.
Lifecycle Manager (internal/agent/runtime/lifecycle/) manages agent instances under the runtime:
Manager(manager.go,manager_*.go) - central coordinator for agent lifecycleExecutorBackendinterface (executor_backend.go) - abstracts execution environment (Docker, Standalone, Sprites, Remote Docker)ExecutionStore(execution_store.go) - thread-safe in-memory execution trackingsession.go- ACP session initialization and resumestreams.go- WebSocket stream connections to agentctlprocess_runner.go- agent process launch and managementprofile_resolver.go- resolves agent profiles/settings
agentctl client (internal/agent/runtime/agentctl/) is the HTTP/WS client used by the lifecycle manager to talk to a running agentctl instance. It is a runtime-tier package and should not be imported outside internal/agent/runtime/.
agentctl is an HTTP server that:
- Runs inside Docker containers or as standalone process
- Manages agent subprocess via stdin/stdout (ACP protocol)
- Exposes workspace operations (shell, git, files)
- Supports multiple concurrent instances on different ports
Standalone agentctl is launched in its own process group so terminal Ctrl+C is handled by the backend lifecycle manager first. Do not make standalone agentctl share the backend's foreground process group; that bypasses supervised shutdown and can leak ACP subprocesses.
Executor Types (database model):
local_pc- Standalone process on hostlocal_docker- Docker container on hostsprites- Sprites cloud environmentremote_docker,remote_vps,k8s- Planned
Remote SSH executor platforms: Treat supported remote OS/arch values as an end-to-end contract. Platform probe/normalization, lifecycle support checks, agentctl helper resolution, platform default shell, SSH readiness endpoints, frontend response types, and tests must stay aligned. Preserve raw unsupported platform details in user-facing errors, but use normalized values for supported-platform matching. Keep shell defaults platform-aware: Darwin defaults to zsh, Linux defaults to bash, unless an explicit shell is saved.
Opt-in authentication & per-user scoping (internal/auth/): auth is OFF by default; the global middleware (auth/httpmw, installed after CORS in backendapp.buildHTTPServer) then injects a synthetic admin identity for the pre-auth single user, so behavior is unchanged. Enablement is the features.auth runtime flag (KANDEV_FEATURES_AUTH, Settings > System > Feature Toggles) — the auth service derives its mode from cfg.Features.Auth (disabled / setup = flag-on-no-admin / enabled = flag-on-admin-exists); there is no separate auth.mode setting. When enabled, requests authenticate via a kandev_session cookie (opaque token, SHA-256 at rest, DB-backed) or a kandev_pat_* bearer token. Scoping rules:
- Identity travels in the request context (
authn.IdentityFromContext). No identity = internal caller (pollers, event bus, MCP stream) = unscoped. Synthetic identity = auth disabled = unscoped. - Workspaces are per-user (
workspaces.owner_id); the task service filters/denies at the service layer with*NotFoundsentinels (no existence leak). Unowned rows (owner_id='') stay visible until the setup wizard claims them for the admin. - New user-facing service entry points must apply scoping — call the
authorize*helpers intask/service/service_access.go(or the same pattern) when adding routes that read or mutate workspace-scoped data. - WS: clients carry their identity; dispatched actions and subscriptions are scoped; workspace-carrying events route via
Hub.BroadcastToWorkspace. A newhub.Broadcast(global) call site needs a//ws:globaljustification comment. - Self-authenticating webhooks (automation, office channels, plugin webhooks) and
/health,/api/v1/features,/api/v1/app-statestay public — the allowlist lives inauth/httpmw/middleware.gowith a pinning test.
Execution Flow
Client (WS) → Orchestrator → Lifecycle Manager → ExecutorBackend (container/process) → agentctl
↓
Client (WS) ← Orchestrator ← Lifecycle Manager ←──── stream updates (WS) ──────── agent subprocess
- Orchestrator receives
session.launchvia WS - Lifecycle Manager creates executor instance (container or process)
- agentctl starts inside the instance, agent subprocess is configured and started
- Agent events stream back via WS through the chain
Session Resume: TaskSession.ACPSessionID stored for resume; ExecutorRunning tracks active state; on restart RecoverInstances() reconnects.
Provider Pattern: Packages expose Provide(cfg, log) (*impl, cleanup, error) for DI. Returns implementation, cleanup function, and error. Cleanup called during graceful shutdown.
Worktrees: internal/worktree/Manager provides workspace isolation. Each session can have its own worktree (branch) to prevent conflicts between concurrent agents.
Worktree file materialization: copy_files (Repository.CopyFiles) is a comma-separated spec of repository-relative gitignored paths/doublestar globs seeded into each new worktree. Copy is the default; the exact terminal :symlink suffix (for example .env.local:symlink) creates a relative host-worktree link so source changes propagate live. Other colons stay literal (config:dev, .env:); use ::symlink to copy a literal path ending in :symlink. Malformed reserved syntax is rejected at save time. Parsing and materialization live in internal/worktree/copyfiles/ (ParseSpecs, ValidateSpec, Copy); duplicates and overlapping matches are first-entry-wins. Manager.copyConfiguredFiles runs before setup during worktree creation. Source and destination containment checks reject traversal and symlinked destination parents. Failures are non-fatal warnings. Windows link creation is best-effort. Remote executors cannot link to the host, so Parse/Plan preserve literal colon paths but turn symlink-mode entries into copied bytes delivered through WriteEntries.
Executor default scripts: Default prepare scripts are in internal/agent/runtime/lifecycle/default_scripts.go; internal/scriptengine/ handles placeholder resolution.
Conventions
- Provider pattern for DI; stderr for logs, stdout for ACP only.
- Pass context through chains; event bus for cross-component comm.
- Repository provider identity: Provider-backed repositories are keyed by workspace, provider, normalized
provider_hostorigin, full owner/namespace, and name. Persistprovider_hostwhen importing or resolving a remote; do not infer self-managed GitLab rows from owner/name alone. Legacy rows with an empty host have unknown identity and must fail closed for provider write/link operations. - Execution access: Workspace-oriented handlers (files, shell, inference, ports, vscode, LSP) MUST use
GetOrEnsureExecution(ctx, sessionID)— it recovers from backend restarts by creating executions on-demand. Only useGetExecutionBySessionIDfor operations that require a running agent process (prompt, cancel, mode). - Task lifecycle events: Any code path that mutates a task row must publish via the event bus (
task.created/task.updated/task.deleted) — either by going throughService.CreateTask/UpdateTask/DeleteTask/ArchiveTask, or by callingpublishTaskEvent(or one of thePublish*helpers inservice_events.go) directly. Walkingrepository.TaskRepositorystraight bypasses event publishing and breaks WS-driven UI like the All-Workflows kanban view.HandoffService's cascade methods learned this the hard way — they now require aTaskEventPublisherwired viaSetTaskEventPublisher. New cascade / bulk / cleanup paths must follow the same pattern. - Testing: Prefer
testing/synctest(Go 1.24+) overtime.Sleepfor time-dependent tests. Usesynctest.Testto wrap tests with tickers or timeouts — it advances fake time instantly when all goroutines are idle. Whensynctestis not feasible (e.g., tests spawning external processes likegit), use channel-based synchronization (<-started, non-blockingselect) instead of sleep-based waits. Reservetime.Sleeponly for integration tests that need real subprocess execution time.- Test cleanup: Register
t.Cleanupimmediately after creating resources that need teardown (adapters,io.Pipewriters, background goroutines) — before anyt.Fatal/t.Fatalfpath. Late cleanup registration leaks pipes and goroutines on early failure. - Joining production goroutines in tests: When code spawns untracked goroutines (e.g.
fireWakeup), don't rely on arbitrary sleeps. Join via an observable side effect — e.g. block onEventTypeCompletefroma.updatesChafter unblocking the fake agent. Use short timeouts (~100ms) for in-process negative assertions; reserve multi-second waits for subprocess/integration tests only. - Path/security tests: Avoid using the real filesystem root as a fixture root. Build fake absolute roots under
t.TempDir()withfilepath.Join; this keeps tests portable across Windows, POSIX, and privileged cloud executors. - Filesystem permission tests: Assert permission-denied behavior only after probing that the current executor enforces the permission bit change. Root-like Sprite executors may bypass
chmodrestrictions. - Full test output: For local full-suite pass/fail validation, prefer plain
go test -race ./....go test -json ./...can emit very large JSONL streams; if a wrapper or tracing tool truncates the stream mid-record, downstream JSON parsing may fail even when Go tests passed. Use JSON output mainly for CI artifacts or test-report tooling that explicitly requires it.
- Test cleanup: Register
Goroutine ownership and leak testing
Every long-running goroutine must have a single owner with explicit start and stop semantics:
- Lifecycle: the type that spawns the goroutine also exposes
Start(ctx)/Stop()(or equivalent).Startregisters on async.WaitGroup;Stopcancels the goroutine's context (or closes astopCh) andwg.Wait()s for drain. Idempotent on both ends.internal/integrations/healthpoll,internal/jira,internal/linear, andinternal/githubpollers are the canonical shape. - E2E reset invariant:
seedData/backend are worker-scoped, so any workspace-scoped state a global poller reads (for examplegithub_review_watches) must be deleted incmd/kandev/e2e_reset.gobefore task deletion — otherwise the poller recreates rows mid-reset and later tests see duplicates. Add aDelete...ByWorkspacecascade when introducing a new poller-backed entity. - Cancellation: the goroutine selects on
ctx.Done()(orstopCh) in every long wait. Never usetime.Sleepin a retry/backoff loop — usetime.NewTimerinside aselectthat also watches the shutdown signal (seelifecycle.StreamManager.sleepOrStop). - Detached helpers: event handlers and short-lived
go func()calls ininternal/orchestrator/andinternal/agent/runtime/lifecycle/must accept a cancellable context (or check the owning type's shutdown signal) and return promptly when it fires. - Leak testing: packages that spawn goroutines add
goleak.VerifyTestMain(m)in a per-packageTestMain. New packages of this kind must follow suit. When a third-party background goroutine genuinely can't be drained, suppress it withgoleak.IgnoreTopFunction(...)and leave a comment explaining why. Currently instrumented:internal/gateway/websocket/,internal/agent/runtime/lifecycle/,internal/agentctl/server/process/,internal/orchestrator/,internal/github/,internal/jira/,internal/linear/,internal/integrations/healthpoll/.
Backups
- On every SQLite boot,
persistence.Providereadskandev_meta.kandev_version. If the stored version differs from the binary version (or any user tables exist but no version is recorded), it takes aVACUUM INTOsnapshot into<data-dir>/backups/before running migrations. - Retention: 2 backups kept (newest two by mtime); older ones are pruned after the snapshot succeeds.
- Postgres: backup is skipped with a log line. Use
pg_dumpfor Postgres backups. - Boot aborts if the backup fails — the pool is closed and
Providereturns an error. - After all repos complete
initSchema,cmd/kandev/storage.go:recordSchemaVersionwrites the current binary version intokandev_meta(non-fatal; a failure just means the next boot will take a fresh snapshot). - Migration logging:
db.MigrateLogger.Apply(name, stmt)— success logs Info, "already exists" / "duplicate column name" is silently swallowed, anything else logs Warn but never returns an error (preserving the existing swallow-error contract). - Schema replay handling: use
internal/dbhelpers such asIsDuplicateColumnError/IsAlreadyExistsErrorinstead of local error-string matching. When adding or changing startup schema code, include fresh-DB plus same-DB replay tests for SQLite; add the same env-gated Postgres replay coverage when the path supports Postgres. Seedocs/decisions/0027-replayable-schema-migrations.md.
Schema & migrations (SQLite repository)
initSchema() in internal/task/repository/sqlite/base_schema.go runs the init*Schema (CREATE TABLE) steps before runMigrations(). The table-creation DDL uses CREATE TABLE IF NOT EXISTS, so on an existing database it is a no-op and never adds columns to a table that is already present.
Rule: when you add a column to an existing table, add it only via an idempotent ADD COLUMN migration in runMigrations() (base_migrations.go), never by editing the table's CREATE TABLE alone. Anything that references that new column — an index, a backfill UPDATE, a partial-index predicate — must live in runMigrations() after the ADD COLUMN, not in the init*Schema DDL. Putting a CREATE INDEX ... (new_col) in the schema-init block crashes existing DBs with no such column: new_col, because schema init runs before the migration that adds the column.
You may still list the column in the CREATE TABLE so fresh DBs get it inline, but the migration is the source of truth for evolution and must stand alone. New columns also need: the struct field in models/, the DTO field + ToAPI in pkg/api/v1/, and every CreateX/UpdateX/bulk write in the repo that should set it.
Code-quality limits
Enforced by apps/backend/.golangci.yml (errors on new code only):
- Functions: ≤80 lines, ≤50 statements
- Cyclomatic complexity: ≤15 · Cognitive complexity: ≤30
- Nesting depth: ≤5 · Naked returns only in functions ≤30 lines
- No duplicated blocks (≥150 tokens) · Repeated strings → constants (≥3 occurrences)
When you hit a limit, extract a helper function. Prefer composition over growing a single function.
When a PR fixup touches backend code, run the CI-style changed-file linter locally from apps/backend with the PR base SHA before pushing, because CI enforces changed-file complexity thresholds:
golangci-lint run ./... --new-from-rev="<base-sha>" --timeout=5m
Further scoped notes
internal/agentctl/AGENTS.md— agentctl server route groups, adapter model, ACP protocolinternal/agentctl/server/api/AGENTS.md— reverse-proxy body rewriting (Accept-Encoding), iframe-blocking header strippinginternal/integrations/AGENTS.md— playbook for adding a new third-party integration (Jira/Linear pattern)cmd/mock-agent/AGENTS.md— predefined/e2e:<name>scenarios vs inlinee2e:...scripts, recipe for adding a scenario, and the rebuild-before-e2e requirement