diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 42a43193d0..41ba931811 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -161,11 +161,12 @@ jobs: run: pnpm --filter @kandev/web typecheck # Unlike the eslint literal-string guard (scoped to already-migrated paths - # via i18nGuardFiles), these four checks scan components/app/hooks/lib + # via i18nGuardFiles), these five checks scan components/app/hooks/lib # repo-wide. Each one pins a class of i18n bug that fails silently at # runtime: a key with no catalog entry, tag markup rendered - # verbatim, an English plural ending passed as a value, and t() at module - # scope frozen at the boot locale. See docs/i18n.md. + # verbatim, an English plural ending passed as a value, t() at module + # scope frozen at the boot locale, and em dashes in public copy. See + # docs/i18n.md. - name: Run i18n checks run: pnpm --filter @kandev/web i18n:check diff --git a/.github/workflows/notify-docs.yml b/.github/workflows/notify-docs.yml index 8170c39b4f..14ae35a4a6 100644 --- a/.github/workflows/notify-docs.yml +++ b/.github/workflows/notify-docs.yml @@ -9,6 +9,8 @@ on: branches: [main] paths: - "docs/public/**" + - "apps/web/scripts/check-no-em-dash-ui.mjs" + - "apps/web/scripts/check-no-em-dash-ui.test.ts" - "scripts/validate-public-docs.mjs" - "scripts/validate-public-docs.test.mjs" - "scripts/notify-docs-workflow.test.mjs" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 11fdafc88e..75407e4ffb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -68,3 +68,13 @@ repos: language: system files: ^apps/web/(components|app|hooks|lib|src)/.*\.tsx?$|^apps/web/eslint\.i18n\.options\.mjs$ pass_filenames: false + + # Public copy hooks + - repo: local + hooks: + - id: public-copy-em-dash + name: Public copy em-dash guard + entry: node apps/web/scripts/check-no-em-dash-ui.mjs + language: system + pass_filenames: false + always_run: true diff --git a/apps/backend/internal/orchestrator/event_handlers_automation.go b/apps/backend/internal/orchestrator/event_handlers_automation.go index 852c1b7ea5..0620742272 100644 --- a/apps/backend/internal/orchestrator/event_handlers_automation.go +++ b/apps/backend/internal/orchestrator/event_handlers_automation.go @@ -135,7 +135,7 @@ func (s *Service) createAutomationTask(ctx context.Context, evt *automation.Auto title := s.resolveAutomationTaskTitle(a, evt) repositories := s.resolveAutomationRepository(ctx, a, evt) if len(repositories) == 0 { - errMsg := "no repository available — add a repository to the workspace" + errMsg := "no repository available; add a repository to the workspace" s.logger.Warn("automation skipped: "+errMsg, zap.String("automation_id", a.ID), zap.String("workspace_id", a.WorkspaceID)) diff --git a/apps/web/AGENTS.md b/apps/web/AGENTS.md index caa12b68b3..321f032a27 100644 --- a/apps/web/AGENTS.md +++ b/apps/web/AGENTS.md @@ -220,8 +220,9 @@ locale is active, and never updates on a switch — and the pseudo-locale cannot see it, because the text _is_ translated, just frozen. Store the key and resolve at render, or make the value a component. `check-module-scope-t.mjs` enforces it. -UI punctuation: avoid Unicode em dash (U+2014) in user-facing copy and locale values. -`pnpm run i18n:check` enforces periods, colons, commas, semicolons, and parentheses. +UI punctuation: avoid Unicode em dash (U+2014) in user-facing copy, locale values, +published documentation. `pnpm run i18n:check` and the `public-copy-em-dash` hook +enforce periods, colons, commas, semicolons, and parentheses across those sources. `pnpm lint` fails on hardcoded UI strings (`i18next/no-literal-string` is an **error**), but **only on the `i18nGuardFiles` allowlist** in diff --git a/apps/web/scripts/check-no-em-dash-ui.mjs b/apps/web/scripts/check-no-em-dash-ui.mjs index c7497061b3..0ec7fa7838 100644 --- a/apps/web/scripts/check-no-em-dash-ui.mjs +++ b/apps/web/scripts/check-no-em-dash-ui.mjs @@ -3,9 +3,9 @@ * Fail when user-visible UI copy contains a Unicode em dash. * * The check covers locale values, rendered web source strings, backend mock-agent - * response strings, and backend shared page catalogs. Historical changelog - * content is intentionally excluded because the changelog is generated release - * history and must remain immutable. + * response strings, backend shared page catalogs, and published Markdown. Historical + * changelog content is intentionally excluded because the changelog is generated + * release history and must remain immutable. * Comments are ignored in source files so this remains a copy check rather than * a style check for developer prose. */ @@ -19,6 +19,7 @@ const WEB_ROOT = path.resolve(import.meta.dirname, ".."); const REPO_ROOT = path.resolve(WEB_ROOT, "..", ".."); const SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx"]); const BACKEND_SOURCE_EXTENSIONS = new Set([".go"]); +const PUBLIC_DOC_EXTENSIONS = new Set([".md", ".mdx"]); const IGNORED_DIRECTORIES = new Set(["dist", "e2e", "node_modules"]); const CODE_STATE = "code"; const LINE_COMMENT_STATE = "line-comment"; @@ -194,6 +195,184 @@ export function findSourceViolations(source, file, root) { ); } +const PUBLIC_DOC_COMMENT_MARKERS = [ + { start: "" }, + { start: "{/*", end: "*/" }, +]; + +function readBacktickRun(source, index) { + let end = index; + while (source[end] === "`") end += 1; + return source.slice(index, end); +} + +function findBacktickRun(source, run, start) { + let cursor = source.indexOf(run, start); + while (cursor >= 0) { + const before = source[cursor - 1]; + const after = source[cursor + run.length]; + if (before !== "`" && after !== "`") return cursor; + cursor = source.indexOf(run, cursor + 1); + } + return -1; +} + +function readFence(line) { + const match = /^( {0,3})(`{3,}|~{3,})(.*)$/.exec(line); + if (!match || (match[2][0] === "`" && match[3].includes("`"))) return null; + return { marker: match[2][0], length: match[2].length }; +} + +function isClosingFence(line, fence) { + const match = /^( {0,3})(`{3,}|~{3,})\s*$/.exec(line); + return Boolean(match && match[2][0] === fence.marker && match[2].length >= fence.length); +} + +function findPublicDocComment(line, index) { + return PUBLIC_DOC_COMMENT_MARKERS.find(({ start }) => line.startsWith(start, index)); +} + +function maskPublicDocComment(line, index, endMarker) { + const end = line.indexOf(endMarker, index); + if (end < 0) { + return { + output: line.slice(index).replace(/[^\n]/g, " "), + nextIndex: line.length, + commentEnd: endMarker, + closed: false, + }; + } + + const endExclusive = end + endMarker.length; + return { + output: line.slice(index, endExclusive).replace(/[^\n]/g, " "), + nextIndex: endExclusive, + commentEnd: null, + closed: true, + }; +} + +function copyInlineCode(line, index, run) { + const end = findBacktickRun(line, run, index); + if (end < 0) { + return { output: line.slice(index), nextIndex: line.length, closed: false }; + } + + const endExclusive = end + run.length; + return { output: line.slice(index, endExclusive), nextIndex: endExclusive, closed: true }; +} + +function consumePublicDocToken(line, index, state) { + if (state.commentEnd) { + const consumed = maskPublicDocComment(line, index, state.commentEnd); + return { + ...consumed, + inlineCodeRun: null, + done: !consumed.closed, + }; + } + + if (state.inlineCodeRun) { + const consumed = copyInlineCode(line, index, state.inlineCodeRun); + return { + ...consumed, + commentEnd: null, + inlineCodeRun: consumed.closed ? null : state.inlineCodeRun, + done: !consumed.closed, + }; + } + + if (line[index] === "`") { + const run = readBacktickRun(line, index); + const consumed = copyInlineCode(line, index + run.length, run); + return { + output: line.slice(index, index + run.length) + consumed.output, + nextIndex: consumed.nextIndex, + commentEnd: null, + inlineCodeRun: consumed.closed ? null : run, + done: !consumed.closed, + }; + } + + const comment = findPublicDocComment(line, index); + if (comment) { + const consumed = maskPublicDocComment(line, index, comment.end); + return { + ...consumed, + inlineCodeRun: null, + done: !consumed.closed, + }; + } + + return { + output: line[index], + nextIndex: index + 1, + commentEnd: null, + inlineCodeRun: null, + done: false, + }; +} + +function maskPublicDocLine(line, state) { + let output = ""; + let cursor = 0; + let currentState = state; + + while (cursor < line.length) { + const consumed = consumePublicDocToken(line, cursor, currentState); + output += consumed.output; + currentState = { + commentEnd: consumed.commentEnd, + inlineCodeRun: consumed.inlineCodeRun, + }; + if (consumed.done) return { output, ...currentState }; + cursor = consumed.nextIndex; + } + + return { output, ...currentState }; +} + +function stripPublicDocCommentsPreservingLines(source) { + let output = ""; + let fence = null; + let state = { commentEnd: null, inlineCodeRun: null }; + + for (const line of source.split("\n")) { + if (fence) { + output += line; + if (isClosingFence(line, fence)) fence = null; + } else { + const openingFence = state.commentEnd || state.inlineCodeRun ? null : readFence(line); + if (openingFence) { + output += line; + fence = openingFence; + state = { commentEnd: null, inlineCodeRun: null }; + } else { + const masked = maskPublicDocLine(line, state); + output += masked.output; + state = { commentEnd: masked.commentEnd, inlineCodeRun: masked.inlineCodeRun }; + } + } + output += "\n"; + } + + return output.slice(0, -1); +} + +function containsPublicDocEmDash(value) { + return value.includes(EM_DASH); +} + +export function findPublicDocViolations(source, file, root) { + return stripPublicDocCommentsPreservingLines(source) + .split("\n") + .flatMap((line, index) => + containsPublicDocEmDash(line) + ? [{ kind: "public-doc", file: relativePath(file, root), line: index + 1 }] + : [], + ); +} + function listFiles(directory, predicate, files = []) { if (!fs.existsSync(directory)) return files; @@ -245,6 +424,19 @@ function scanCatalogDirectory(directory, root, violations) { } } +export function scanPublicDocsEmDashViolations({ + docsRoot = path.join(REPO_ROOT, "docs", "public"), + root = REPO_ROOT, +} = {}) { + const violations = []; + for (const file of listFiles(docsRoot, (candidate) => + PUBLIC_DOC_EXTENSIONS.has(path.extname(candidate).toLowerCase()), + )) { + violations.push(...findPublicDocViolations(fs.readFileSync(file, "utf8"), file, root)); + } + return violations; +} + export function scanUiEmDashViolations({ repoRoot = REPO_ROOT, webRoot = WEB_ROOT } = {}) { const violations = []; const sourceRoots = ["components", "app", "src", "hooks", "lib"].map((directory) => @@ -277,6 +469,13 @@ export function scanUiEmDashViolations({ repoRoot = REPO_ROOT, webRoot = WEB_ROO } } + violations.push( + ...scanPublicDocsEmDashViolations({ + docsRoot: path.join(repoRoot, "docs", "public"), + root: repoRoot, + }), + ); + return violations; } @@ -290,14 +489,14 @@ function formatViolation(violation) { function main() { const violations = scanUiEmDashViolations(); if (violations.length > 0) { - console.error(`✗ ${violations.length} UI em dash violation(s):\n`); + console.error(`✗ ${violations.length} public-copy em dash violation(s):\n`); for (const violation of violations) console.error(formatViolation(violation)); console.error("\nUse a period, colon, comma, semicolon, or parentheses in user-facing copy."); process.exitCode = 1; return; } - console.log("✓ no em dashes in UI strings or locale values."); + console.log("✓ no em dashes in public copy or locale values."); } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { diff --git a/apps/web/scripts/check-no-em-dash-ui.test.ts b/apps/web/scripts/check-no-em-dash-ui.test.ts index a254251ffc..f68f8be0f9 100644 --- a/apps/web/scripts/check-no-em-dash-ui.test.ts +++ b/apps/web/scripts/check-no-em-dash-ui.test.ts @@ -5,12 +5,17 @@ import { afterEach, describe, expect, it } from "vitest"; import { EM_DASH, containsEmDash, + findPublicDocViolations, findSourceViolations, scanUiEmDashViolations, stripCommentsPreservingStrings, } from "./check-no-em-dash-ui.mjs"; const temporaryDirectories: string[] = []; +const TEST_REPO_ROOT = "/repo"; +const PUBLIC_DOC_FILE = `${TEST_REPO_ROOT}/docs/public/guide.md`; +const PUBLIC_DOC_RELATIVE_FILE = "docs/public/guide.md"; +const PUBLIC_DOC_KIND = "public-doc"; afterEach(() => { for (const directory of temporaryDirectories.splice(0)) { @@ -35,6 +40,46 @@ describe("check-no-em-dash-ui", () => { expect(containsEmDash(String.raw`visible \u2014 copy`)).toBe(true); }); + it("ignores non-rendered Markdown and MDX comments", () => { + const source = [ + ``, + "", + "Visible copy", + `{/* hidden ${EM_DASH} MDX comment */}`, + `Visible ${EM_DASH} copy`, + ].join("\n"); + + expect(findPublicDocViolations(source, PUBLIC_DOC_FILE, TEST_REPO_ROOT)).toEqual([ + { kind: PUBLIC_DOC_KIND, file: PUBLIC_DOC_RELATIVE_FILE, line: 7 }, + ]); + }); + + it("checks actual em dashes instead of source escapes in public docs", () => { + const source = ["JSON escape \\u2014 stays literal.", `Visible ${EM_DASH} copy`].join("\n"); + + expect(findPublicDocViolations(source, PUBLIC_DOC_FILE, TEST_REPO_ROOT)).toEqual([ + { kind: PUBLIC_DOC_KIND, file: PUBLIC_DOC_RELATIVE_FILE, line: 2 }, + ]); + }); + + it("does not treat comment markers in Markdown code as comments", () => { + const source = [ + "Inline code `>A: get_task_conversation_kandev(B) to confirm spec ``` ### Agent A's actual tool calls (turn by turn) -**Turn 1 — propose the contract:** +**Turn 1: propose the contract:** ``` message_task_kandev( @@ -175,7 +175,7 @@ message_task_kandev( # → "sent" (Task B was idle) ``` -**Turn 2 — Task B's reply arrives as a new user turn in Task A's conversation:** +**Turn 2: Task B's reply arrives as a new user turn in Task A's conversation:** ``` # Incoming turn (user message in A's conversation): @@ -197,7 +197,7 @@ message_task_kandev( # → "queued" (Task B was running its own turn) ``` -**Turn 3 — verify before writing tests:** +**Turn 3: verify before writing tests:** ``` get_task_conversation_kandev( @@ -216,7 +216,7 @@ Task B processes each incoming message as a normal turn. When it receives Agent ## Good practices -**Be self-contained.** Each message should include enough context for the receiving agent to act without looking up prior conversation history — task ID, repo, branch, current status, and what you expect in return. +**Be self-contained.** Each message should include enough context for the receiving agent to act without looking up prior conversation history; task ID, repo, branch, current status, and what you expect in return. **One bounded question at a time.** Cross-task messages work best for specific, answerable requests ("does this schema cover your needs?"), not open-ended collaboration. Keep the negotiation short. @@ -239,10 +239,10 @@ These tools complement cross-task communication for common coordination patterns | `list_task_sessions_kandev` | List a task's sessions to find the `session_id` for the two tools above | | `list_related_tasks_kandev` | Discover parent / child / sibling / blocker task IDs | | `create_task_kandev` | Delegate work to a new subtask; returns the new task's ID | -| `spawn_session_kandev` | Start another session on an existing task; returns `{task_id, session_id, state}` — use the `session_id` field to message the new session directly | +| `spawn_session_kandev` | Start another session on an existing task; returns `{task_id, session_id, state}`; use the `session_id` field to message the new session directly | | `move_task_kandev` | Hand off a task to the next workflow step with an optional prompt for the receiving agent | | `create_task_plan_kandev` | Record an agreed implementation plan (both tasks can create/update their own plans) | -| `get_task_plan_kandev` | Read a task's plan — useful before messaging to share a structured proposal | +| `get_task_plan_kandev` | Read a task's plan; useful before messaging to share a structured proposal | | `step_complete_kandev` | Signal that the current workflow step is done (task-mode only) | | `ask_user_question_kandev` | Escalate to a human when agent negotiation cannot resolve a question | diff --git a/docs/public/architecture.md b/docs/public/architecture.md index 99d13eb92d..509e777f64 100644 --- a/docs/public/architecture.md +++ b/docs/public/architecture.md @@ -31,7 +31,7 @@ flowchart LR These adjacent launch layers have different jobs: -- `apps/backend/internal/launcher/` implements all four launch modes — `dev` (supervised `make -C apps/backend dev` backend + Vite dev-server child, state under `/.kandev-dev/`), installed `run`, `start`, and service behavior. +- `apps/backend/internal/launcher/` implements all four launch modes: `dev` (supervised `make -C apps/backend dev` backend + Vite dev-server child, state under `/.kandev-dev/`), installed `run`, `start`, and service behavior. - The published `apps/cli/bin/` npm shim selects the matching `@kdlbs/runtime-*` package and starts its native `kandev` binary. - `apps/desktop/` is a Tauri shell. Rust starts its bundled `kandev --headless` on an owned loopback origin and owns native windows, external links, notifications, and updates. The product UI is still the backend-served SPA. diff --git a/docs/public/authentication.md b/docs/public/authentication.md index 1cac0ff348..6de1151750 100644 --- a/docs/public/authentication.md +++ b/docs/public/authentication.md @@ -5,9 +5,9 @@ description: "Enable opt-in authentication, manage users and invites, and use pe # Authentication & Users -Kandev ships as a single-user local tool with authentication **disabled** — nothing changes for laptop installs. When several people share one Kandev server, enable authentication to give each person their own account and their own private workspaces. +Kandev ships as a single-user local tool with authentication **disabled**: nothing changes for laptop installs. When several people share one Kandev server, enable authentication to give each person their own account and their own private workspaces. -Authentication is a **runtime feature toggle** — the same system as the other feature flags — so there is no separate "Authentication" configuration page. +Authentication is a **runtime feature toggle**: the same system as the other feature flags, so there is no separate "Authentication" configuration page. ## Quick checklist @@ -20,7 +20,7 @@ Authentication is a **runtime feature toggle** — the same system as the other ## What changes when authentication is on - Everyone signs in with email + password. Browser sessions last 30 days (sliding) and can be revoked from `Settings > Account`. The signed-in user is shown in the bottom-left of the sidebar, with a log-out menu. -- **Workspaces become per-user.** You only see workspaces you own — including their tasks, sessions, repositories, terminals, previews, and live updates. Existing data is assigned to the admin created during setup. +- **Workspaces become per-user.** You only see workspaces you own, including their tasks, sessions, repositories, terminals, previews, and live updates. Existing data is assigned to the admin created during setup. - Secrets are per-user. A **Global** secret is user-global across that user's workspaces; a **Workspace** secret belongs to one of their workspaces. With authentication disabled, Global is install-global. Executors and agent profiles remain shared across the instance, so they can reference Global secrets only; repositories may bind Global or same-workspace secrets. - Admins manage users and instance settings, but do **not** see other users' workspaces. - Programmatic clients (external MCP, scripts) authenticate with personal access tokens. @@ -45,9 +45,9 @@ A server that listens on non-loopback interfaces with authentication disabled lo `Settings > System > Users` (admin only): -- **Invite links** — mint a tokenized URL (`/invite?token=…`) and share it out of band. Optional pinned email, member or admin role, single use, 7-day default expiry. No email server needed. -- **Direct creation** — create an account with a password yourself. -- **Disable / role changes** — disabling a user immediately revokes their sessions and tokens. The last active admin cannot be demoted or disabled. +- **Invite links**: mint a tokenized URL (`/invite?token=…`) and share it out of band. Optional pinned email, member or admin role, single use, 7-day default expiry. No email server needed. +- **Direct creation**: create an account with a password yourself. +- **Disable / role changes**: disabling a user immediately revokes their sessions and tokens. The last active admin cannot be demoted or disabled. Roles: `admin` (user management, authentication settings, destructive system operations, feature toggles) and `member` (everything else, scoped to their own workspaces). @@ -67,13 +67,13 @@ External MCP clients (Claude Code, Cursor connecting to `/mcp`) must be configur ## What is isolated -When authentication is on, everything in a workspace is private to its owner and returns "not found" to anyone else — even if they know the ID: workspaces, tasks, workflows, sessions, plans, walkthroughs, terminals, VS Code, port previews, git snapshots, Workspace secrets, repository bindings, **and the workspace's third-party integration settings (GitHub/GitLab/Jira/Linear/Sentry/Azure) and automations**. A user's Global secrets are also private to that user. Admins manage users but do not see other users' workspaces or secrets. +When authentication is on, everything in a workspace is private to its owner and returns "not found" to anyone else, even if they know the ID: workspaces, tasks, workflows, sessions, plans, walkthroughs, terminals, VS Code, port previews, git snapshots, Workspace secrets, repository bindings, **and the workspace's third-party integration settings (GitHub/GitLab/Jira/Linear/Sentry/Azure) and automations**. A user's Global secrets are also private to that user. Admins manage users but do not see other users' workspaces or secrets. Shared across the instance (by design): executors, agent profiles, environments, editors, prompts, and system pages. ## Limitations -- **Filesystem and agent credentials are not isolated.** Worktrees and repositories live under one `~/.kandev` tree owned by the OS user running the backend, and agent CLI logins (`gh auth`, `claude login`, provider API keys) authenticate as that OS user — so all app-users share the same on-disk agent credentials, and anyone with shell access to the server can read all files. Authentication isolates users' kandev *data* at the application layer, not the filesystem or per-user agent auth. For hard isolation of agent credentials, run a separate kandev instance per user (or use OS-level access control / sandboxed executors). -- One owner per workspace — no sharing or team workspaces yet. +- **Filesystem and agent credentials are not isolated.** Worktrees and repositories live under one `~/.kandev` tree owned by the OS user running the backend, and agent CLI logins (`gh auth`, `claude login`, provider API keys) authenticate as that OS user, so all app-users share the same on-disk agent credentials, and anyone with shell access to the server can read all files. Authentication isolates users' kandev *data* at the application layer, not the filesystem or per-user agent auth. For hard isolation of agent credentials, run a separate kandev instance per user (or use OS-level access control / sandboxed executors). +- One owner per workspace, no sharing or team workspaces yet. - Local accounts only for now; the account model is ready for OIDC/SSO later. - Authentication does not replace TLS. Terminate HTTPS in front of Kandev (the session cookie is marked `Secure` when the request arrives over TLS or `X-Forwarded-Proto: https`). diff --git a/docs/public/automation-and-mcp.md b/docs/public/automation-and-mcp.md index afddb4fd14..287775274a 100644 --- a/docs/public/automation-and-mcp.md +++ b/docs/public/automation-and-mcp.md @@ -48,15 +48,15 @@ Open **Settings > Workspaces > _Workspace_ > Automations** (`/settings/workspace 7. Choose a schedule and optional GitHub condition, or switch to webhook mode. 8. Save, use **Run now** on the automation's page, then read what it said before widening credentials or scope. -The form can save an empty agent, executor, or repository selection, but launch still needs a usable agent/executor and a repository. For scheduled, webhook, and manual work, an empty repository falls back to the workspace's first repository. If the workspace has none, the run fails with `no repository available — add a repository to the workspace`. A GitHub pull-request run instead checks out that PR's head branch and uses its base branch. +The form can save an empty agent, executor, or repository selection, but launch still needs a usable agent/executor and a repository. For scheduled, webhook, and manual work, an empty repository falls back to the workspace's first repository. If the workspace has none, the run fails with `no repository available; add a repository to the workspace`. A GitHub pull-request run instead checks out that PR's head branch and uses its base branch. ### What a firing produces -Every automation produces the same thing: an ordinary, persistent task tagged `origin = automation_run`. That origin — not `is_ephemeral` — is what keeps it off the kanban and out of task lists, which means the task keeps its worktree and stays repliable. Worktrees are retained for the ten most recent finished runs of each automation and reclaimed beyond that, so an older run stays readable but can no longer be answered. The trigger is the start signal, so the agent starts immediately rather than waiting for a workflow step's `auto_start_agent` action. +Every automation produces the same thing: an ordinary, persistent task tagged `origin = automation_run`. That origin (not `is_ephemeral`) is what keeps it off the kanban and out of task lists, which means the task keeps its worktree and stays repliable. Worktrees are retained for the ten most recent finished runs of each automation and reclaimed beyond that, so an older run stays readable but can no longer be answered. The trigger is the start signal, so the agent starts immediately rather than waiting for a workflow step's `auto_start_agent` action. A finished run parks in `WAITING_FOR_INPUT` rather than `COMPLETED`, so you can reply to it and the agent continues in the same session and worktree. A run is a thread, not a receipt. -There is no execution-mode choice. Earlier versions asked for **Task** or **Run** up front; the column was retained so existing rows need no migration, but it is no longer read, is accepted and ignored on the wire, and is omitted from responses. Automations created before the change behave like every other one, and cards already on a board are left alone — they are ordinary tasks now and can be archived by hand. +There is no execution-mode choice. Earlier versions asked for **Task** or **Run** up front; the column was retained so existing rows need no migration, but it is no longer read, is accepted and ignored on the wire, and is omitted from responses. Automations created before the change behave like every other one, and cards already on a board are left alone; they are ordinary tasks now and can be archived by hand. A run cannot wait for a permission response. Kandev rejects the request and marks the run failed. Use only a profile whose intended, constrained actions can complete without a prompt. @@ -83,7 +83,7 @@ The scheduler checks every 30 seconds and computes each expression's next calend Use the supplied presets: every 5, 15, or 30 minutes; hourly; every 6 hours; daily; or weekly. The backend also accepts `@every` followed by a Go duration, `@hourly`, `@daily`, `@weekly`, and step forms such as `*/10 * * * *` or `0 */6 * * *`. -The editor accepts arbitrary five-field cron text, including fixed calendar forms such as `30 8 * * *`, weekday ranges such as `15 9 * * 1-5`, and day-of-month schedules — all of which now fire at the correct time. A saved timezone (for example `America/New_York`) is honored, including that zone's daylight-saving transitions; an empty timezone means UTC, so schedules are deterministic regardless of the host clock. Scheduled runs are deduplicated per trigger per minute. +The editor accepts arbitrary five-field cron text, including fixed calendar forms such as `30 8 * * *`, weekday ranges such as `15 9 * * 1-5`, and day-of-month schedules, all of which now fire at the correct time. A saved timezone (for example `America/New_York`) is honored, including that zone's daylight-saving transitions; an empty timezone means UTC, so schedules are deterministic regardless of the host clock. Scheduled runs are deduplicated per trigger per minute. ### GitHub pull requests @@ -147,7 +147,7 @@ Webhook delivery has no event deduplication or filter-expression evaluator. Make ### Manual trigger -**Run now** on an automation's page — and the play action in the settings table — fires a run with trigger type `manual` and no deduplication. Use it to test repository/profile resolution and read what comes back. +**Run now** on an automation's page and the play action in the settings table fire a run with trigger type `manual` and no deduplication. Use either action to test repository/profile resolution and read what comes back. A trigger can succeed and still run nothing. A disabled automation, an already-fired dedup key, or a concurrency cap that is already reached all report **skipped with the reason** rather than claiming a fire. A cap skip writes a run row so the history explains itself; a disabled automation does not, since nothing was ever going to run. @@ -163,17 +163,17 @@ Trigger payloads are untrusted input. Do not let a PR body or webhook field sile ## Read what an automation has been doing -**Automations** in the sidebar lists the workspace's automations with a health dot; picking one opens it. **`/automations`** is the agenda across all of them — what fires next, and the recent runs of every automation in one feed. **`/automations/`** is one automation's conversation: it opens on the newest run's transcript, pins the standing instruction above it, and carries a reply box. Runs sit in a rail beside it, grouped Running / Completed, as a switcher between instances. Configuration is behind **Details** in that rail, because an automation is configured once and read continuously. +**Automations** in the sidebar lists the workspace's automations with a health dot. Picking one opens it. **`/automations`** is the agenda across all of them, what fires next, and the recent runs of every automation in one feed. **`/automations/`** is one automation's conversation: it opens on the newest run's transcript, pins the standing instruction above it, and carries a reply box. Runs sit in a rail beside it, grouped Running / Completed, as a switcher between instances. Configuration is behind **Details** in that rail, because an automation is configured once and read continuously. `/runs` still resolves to the same places, so older links keep working. ## Concurrency, history, and cleanup -Maximum concurrent runs defaults to 1 and cannot be less than 1. A run counts as active while its status is `task_created` **and** its task is neither deleted, archived, nor explicitly cancelled — the same definition the UI uses when it says an automation will not fire because a run is still open, so the reason shown and the cap causing it cannot disagree. When the cap is reached, Kandev records a `skipped` run and advances the schedule's evaluation time rather than retrying every 30 seconds. +Maximum concurrent runs defaults to 1 and cannot be less than 1. A run counts as active while its status is `task_created` **and** its task is neither deleted, archived, nor explicitly cancelled, the same definition the UI uses when it says an automation will not fire because a run is still open, so the reason shown and the cap causing it cannot disagree. When the cap is reached, Kandev records a `skipped` run and advances the schedule's evaluation time rather than retrying every 30 seconds. Run history can report `triggered`, `task_created`, `succeeded`, `failed`, `skipped`, `archived`, or `cancelled`. The last two are derived at read time, not stored: a `task_created` run whose task was deleted or whose primary session was cancelled reads as `cancelled`, and one whose task was archived reads as `archived`. That derivation is defined once and shared by every view, so two surfaces cannot disagree about the same run. -A run that produced a task opens its conversation. A run that never produced one — a skipped firing — is listed but inert; there is nothing to read. +A run that produced a task opens its conversation. A run that never produced one (a skipped firing) is listed but inert; there is nothing to read. Deleting one run also deletes its associated task. **Delete all runs** deletes all associated tasks and history for that automation and is irreversible. @@ -231,12 +231,12 @@ indicator during the wait. ### Create idempotency with `external_id` -A caller that cannot tell whether an earlier `create_task_kandev` call (or `POST /api/v1/tasks`) actually landed — a crash before recording the response, a webhook redelivery — can pass an `external_id` and retry safely instead of guessing. `external_id` is an opaque, caller-chosen string, case-sensitive and byte-exact, unique per workspace: two workspaces can each hold their own task for the same value, but a second create for a value already held in the same workspace never makes a second task there. It is validated and trimmed like other free-text fields; a value that is empty after trimming is treated as if the field were omitted. +A caller that cannot tell whether an earlier `create_task_kandev` call (or `POST /api/v1/tasks`) actually landed (a crash before recording the response or a webhook redelivery) can pass an `external_id` and retry safely instead of guessing. `external_id` is an opaque, caller-chosen string, case-sensitive and byte-exact, unique per workspace: two workspaces can each hold their own task for the same value, but a second create for a value already held in the same workspace never makes a second task there. It is validated and trimmed like other free-text fields; a value that is empty after trimming is treated as if the field were omitted. -Every create response — from both the MCP tool result and the REST response body — always carries two additional fields, whether or not the request included an `external_id`: +Every create response from both the MCP tool result and the REST response body always carries two additional fields, whether or not the request included an `external_id`: - `deduplicated`: `true` when the returned task already existed for that identity, `false` when this call created it. -- `creation_complete`: `false` only when the returned task is an existing one whose own create had not finished when observed and may still be running (it is not proof the other create is still alive — it may have crashed). Every other outcome reports `true`. +- `creation_complete`: `false` only when the returned task is an existing one whose own create had not finished when observed and may still be running (it is not proof the other create is still alive; it may have crashed). Every other outcome reports `true`.
Reading the four outcomes @@ -245,14 +245,14 @@ Every create response — from both the MCP tool result and the REST response bo | --- | --- | --- | | `false` | `true` | This call created the task. | | `true` | `true` | An earlier, finished create already holds this identity; that task is returned unchanged, with no new task, agent, or side effect of any kind. | -| `true` | `false` | An earlier create for this identity had not finished when observed and may still be running — or may have crashed. The task is returned as-is; nothing about it is started, modified, or assumed finished. | +| `true` | `false` | An earlier create for this identity had not finished when observed and may still be running, or may have crashed. The task is returned as-is; nothing about it is started, modified, or assumed finished. | | `false` | `true`, and `external_id` absent from the response | This call finished creating a task, but another actor released or reused the identity in the narrow window before settlement. The task exists and is otherwise normal; it simply is not holding that identity anymore. |
**Do not react to `creation_complete: false` by releasing the identity and retrying.** The other create may still be doing required work; releasing it out from under that work can produce two tasks for the same identity. `creation_complete: false` means "ask again later," not "safe to force." An operator who has independently confirmed a create is abandoned (not merely slow) can free its identity with `DELETE /api/v1/workspaces/:workspace_id/tasks/by-external-id?external_id=...`, which returns `204` and leaves the task itself untouched. `GET` the same path to look up the task currently holding an identity, including one still unsettled, without creating anything; it returns `404` when nothing holds it. -Deleting or archiving the task that holds an identity does not carry the identity forward: archiving leaves it in place, but deleting the task frees the identity for reuse by a later create. `external_id` cannot be changed after creation — update requests that include it leave the task's identity untouched. The WebSocket `task.create` action and the plugin host's `Tasks().Create` do not accept `external_id`; use `create_task_kandev` or `POST /api/v1/tasks` for idempotent creates. +Deleting or archiving the task that holds an identity does not carry the identity forward: archiving leaves it in place, but deleting the task frees the identity for reuse by a later create. `external_id` cannot be changed after creation; update requests that include it leave the task's identity untouched. The WebSocket `task.create` action and the plugin host's `Tasks().Create` do not accept `external_id`; use `create_task_kandev` or `POST /api/v1/tasks` for idempotent creates. A task session currently registers these tool groups: @@ -273,7 +273,7 @@ prompted to call it before any other work, even though the task already has a pr prompt. Use a short title phrase targeting about six words in sentence case rather than a sentence or progress update. The tool is omitted for ordinary tasks, tasks created while the setting was disabled, config sessions, -Office sessions, and every later session on the task—even if the owner fails before renaming it. A human +Office sessions, and every later session on the task, even if the owner fails before renaming it. A human rename wins if it happens first; a late owner call returns `title_not_pending`, while a non-owner call returns `title_not_owner`, without changing the title. @@ -425,7 +425,7 @@ External MCP exposes 33 tools in these groups: - executors: list executors and profiles; create, update, or delete executor profiles; - tasks: list, create, move, delete, archive, or update task state; list a task's sessions; and read task conversation. -The settings page's static **Available tools** preview currently counts 30 and omits `list_repositories_kandev`, `import_workflow_kandev`, and `get_task_conversation_kandev`. Treat the client's live `tools/list` response from the endpoint—not that preview—as authoritative. +The settings page's static **Available tools** preview currently counts 30 and omits `list_repositories_kandev`, `import_workflow_kandev`, and `get_task_conversation_kandev`. Treat the client's live `tools/list` response from the endpoint, not that preview, as authoritative. In external mode, `create_task_kandev` has no current task and does not accept the `parent_id: "self"` shorthand. Its registered top-level contract asks for a repository ID, repository URL (including a supported GitHub pull request or GitLab merge request URL), or local path; workspace and workflow resolve automatically only when unambiguous. The current handler can nevertheless accept an omitted repository and create repo-less work, which is a contract/implementation mismatch rather than a supported equivalent of the regular UI's **None** option. Supply an explicit repository locator for portable clients. A resolvable agent profile is required even with `start_agent: false`; otherwise `start_agent` defaults to true. To create a subtask, pass the full ID of an existing parent. diff --git a/docs/public/configuration.md b/docs/public/configuration.md index 241bc3cb8f..039f3c9395 100644 --- a/docs/public/configuration.md +++ b/docs/public/configuration.md @@ -108,7 +108,7 @@ An external NATS URL moves event traffic across the configured network and can e | `docker.defaultNetwork` | `KANDEV_DOCKER_DEFAULTNETWORK` | `kandev-network` | Accepted compatibility field; not wired into current executor networking. | | `docker.volumeBasePath` | `KANDEV_DOCKER_VOLUMEBASEPATH` | `/var/lib/kandev/volumes` on Unix; `%LOCALAPPDATA%\kandev\volumes` on Windows | Accepted compatibility field; not wired into current executor volume placement. | -The Docker socket is effectively root-equivalent on many hosts. Do not publish it or assume `docker.tlsVerify` secures a TCP daemon—it currently does not. Configure TLS through a supported Docker endpoint/environment and validate it independently, or keep the daemon local. See [Docker](./docker.md) and [Executors](./executors.md). +The Docker socket is effectively root-equivalent on many hosts. Do not publish it or assume `docker.tlsVerify` secures a TCP daemon; it currently does not. Configure TLS through a supported Docker endpoint/environment and validate it independently, or keep the daemon local. See [Docker](./docker.md) and [Executors](./executors.md). ### Core agent service @@ -388,7 +388,7 @@ Startup validation currently enforces: - logging level/format; and - positive `repositoryDiscovery.maxDepth`. -Other fields can pass configuration validation and still fail later—for example an unreachable NATS/PostgreSQL/Docker endpoint, unwritable log path, nonsensical timeout, or incompatible pool sizes. A field appearing in the schema does not prove its subsystem is available. +Other fields can pass configuration validation and still fail later, for example an unreachable NATS/PostgreSQL/Docker endpoint, unwritable log path, nonsensical timeout, or incompatible pool sizes. A field appearing in the schema does not prove its subsystem is available. If a value appears ignored: @@ -400,4 +400,4 @@ If a value appears ignored: Use `kandev --verbose` to surface startup errors. Do not use `--debug` merely to diagnose a YAML typo on an exposed machine; verbose logs are usually sufficient. -Variables used only to assemble/test the runtime—such as `KANDEV_WEB_DIST_DIR`, `KANDEV_DESKTOP_RUNTIME_DIR`, mock/E2E switches, supervisor socket/manifest values, and bootstrap nonces—are internal implementation contracts, not supported deployment configuration. `KANDEV_BUNDLE_DIR` is the narrow exception documented for installer/package integration in [CLI](./cli.md); end users should still let the installer set it. +Variables used only to assemble/test the runtime: such as `KANDEV_WEB_DIST_DIR`, `KANDEV_DESKTOP_RUNTIME_DIR`, mock/E2E switches, supervisor socket/manifest values, and bootstrap nonces, are internal implementation contracts, not supported deployment configuration. `KANDEV_BUNDLE_DIR` is the narrow exception documented for installer/package integration in [CLI](./cli.md); end users should still let the installer set it. diff --git a/docs/public/coordination.md b/docs/public/coordination.md index e31f7e6748..e4665fdf36 100644 --- a/docs/public/coordination.md +++ b/docs/public/coordination.md @@ -166,7 +166,7 @@ Additional messaging boundaries: Use `get_task_conversation_kandev` to read a primary or explicit session conversation. It supports limits, before/after cursors, ascending or descending order, and message-type filters. When a task has more than one session, use `list_task_sessions_kandev` to list them (newest first, flagging the primary and your own) and pass the `session_id` you want. Use `list_related_tasks_kandev` for the current or another same-workspace task to list its parent, direct children, siblings, stored blocker relationships, and associated GitHub pull requests. -Replies close the loop: the receiving agent calls `message_task_kandev` back with the originating task's ID, turning a one-way notification into a genuine bidirectional conversation. This enables multi-turn negotiations between agents — for example, agreeing on an API contract before both sides implement. See [Agent Communication](agent-communication.md) for delivery semantics, discovery patterns, and a worked negotiation example. +Replies close the loop: the receiving agent calls `message_task_kandev` back with the originating task's ID, turning a one-way notification into a genuine bidirectional conversation. This enables multi-turn negotiations between agents, for example, agreeing on an API contract before both sides implement. See [Agent Communication](agent-communication.md) for delivery semantics, discovery patterns, and a worked negotiation example. ## Wait for child tasks diff --git a/docs/public/desktop-app.md b/docs/public/desktop-app.md index cc2371b56f..e5a22f6734 100644 --- a/docs/public/desktop-app.md +++ b/docs/public/desktop-app.md @@ -115,7 +115,7 @@ Desktop inherits normal backend configuration. By default, persistent data lives Only one desktop instance runs per OS user/application scope. Launching the app a second time shows, unminimizes, and focuses the existing main window; it does not start a second backend. -Closing the main OS window or choosing **Quit Kandev** quits the application and stops the backend it owns. There is no tray/background mode or desktop autostart service. On Unix, shutdown sends a graceful termination and force-kills after five seconds if needed; on Windows it terminates the owned process tree. Active external executor resources may have their own lifecycle—check [Executors](./executors.md) before manual cleanup. +Closing the main OS window or choosing **Quit Kandev** quits the application and stops the backend it owns. There is no tray/background mode or desktop autostart service. On Unix, shutdown sends a graceful termination and force-kills after five seconds if needed; on Windows it terminates the owned process tree. Active external executor resources may have their own lifecycle, check [Executors](./executors.md) before manual cleanup. The application menu exposes New Task, Settings (`Cmd/Ctrl+,`), contextual Close (`Cmd/Ctrl+W`), zoom, full-screen, Help, update, and Quit actions. Contextual Close asks the web UI to close its top dialog or eligible file/diff/commit/preview tab; if nothing is closeable it does not shut down the window or backend. The desktop shell saves window geometry in its platform app-data directory and clamps restored geometry to an available display. diff --git a/docs/public/developer-tools.md b/docs/public/developer-tools.md index f4817a86bc..3b0a679298 100644 --- a/docs/public/developer-tools.md +++ b/docs/public/developer-tools.md @@ -161,7 +161,7 @@ For an idle, non-archived repository-backed task, **Files → Workspace actions The task **Files** panel browses, searches, opens, and edits task-worktree files. Kandev rejects file paths that escape the resolved worktree. A session with one worktree opens that worktree directly in a host editor. When a session has several worktrees, the editor button asks which repository or worktree to open, and each configured editor in the adjacent menu expands to the same repository-and-branch picker. Check that selection before launching an editor from a multi-repository task. Older API clients that omit `worktree_id` retain the first-worktree fallback. -Open the context menu on any file or folder in the Files tree — right-click on desktop, long-press on touch — for **Open in \**, which launches your default editor at that path instead of at the worktree root. When more than one editor is configured, **Open in other editor** lists the rest. When the tree is rooted above the worktrees — a multi-worktree task, or any task that has had sources attached — Kandev resolves the clicked path back to its own worktree, so no picker is needed. The action is hidden for entries that belong to no worktree, such as an attached plain folder, because the editor launch is resolved against a worktree. It is also hidden while several files are selected, because it applies to a single path. +Open the context menu on any file or folder in the Files tree: right-click on desktop or long-press on touch to see **Open in \**, which launches your default editor at that path instead of at the worktree root. When more than one editor is configured, **Open in other editor** lists the rest. When the tree is rooted above the worktrees (a multi-worktree task or any task that has had sources attached), Kandev resolves the clicked path back to its own worktree, so no picker is needed. The action is hidden for entries that belong to no worktree, such as an attached plain folder, because the editor launch is resolved against a worktree. It is also hidden while several files are selected, because it applies to a single path. **VS Code (Embedded)** runs code-server inside the active task environment and displays it in a workbench panel. Opening it starts code-server independently of the agent process. It launches with `--auth none` and binds `0.0.0.0` on a random port inside that runtime; network/firewall isolation is therefore important, especially for Local and SSH environments. If the binary is absent, agentctl attempts to download it into `~/.kandev/tools/code-server`; first use needs a supported execution platform and access to the [code-server release](https://github.com/coder/code-server/releases/tag/v4.130.0). Native Windows Local and Worktree sessions do not support this integration. Linux-backed Local Docker, Sprites, and supported SSH sessions can offer it even when the Kandev app runs on Windows. The task-detail topbar follows the active session's executor capability, never the visitor's browser platform; other configured editors remain available. Use the panel error and task-environment logs when installation, startup, or proxying fails. @@ -195,7 +195,7 @@ Language-server settings are part of **Settings > General > Editors**. Kandev cu - Python; - Kotlin through the official `kotlin-lsp`. Kotlin support is experimental because the upstream server is still alpha. -Auto-start and auto-install are off for every language by default. Enable only the languages used by the workspace, then save settings. Each language card keeps its installation command, prerequisites, and managed destination visible next to the auto-install setting, including for touch and keyboard users. Open the language-server control to inspect its status and use the explicit **Start**, **Stop**, or **Retry** action. Browser-local storage remembers manual enablement for that session and language. If you stop an auto-started server, it stays off for that session and language in the current browser tab—even if a matching editor reopens—until you explicitly start it again or reload the page. Kandev disconnects an unused server connection after two minutes. +Auto-start and auto-install are off for every language by default. Enable only the languages used by the workspace, then save settings. Each language card keeps its installation command, prerequisites, and managed destination visible next to the auto-install setting, including for touch and keyboard users. Open the language-server control to inspect its status and use the explicit **Start**, **Stop**, or **Retry** action. Browser-local storage remembers manual enablement for that session and language. If you stop an auto-started server, it stays off for that session and language in the current browser tab, even if a matching editor reopens, until you explicitly start it again or reload the page. Kandev disconnects an unused server connection after two minutes. **Status location** defaults to the active file's editor toolbar. On a fine-pointer desktop with **Show status bar** enabled under **Settings > Preferences > Appearance > Status Bar**, you can instead place it in that bar; the item follows the active supported file and is absent on unsupported files and non-file panels. Turning the status surface off moves the item back to the editor toolbar without overwriting its saved location. A touch-oriented tablet also keeps the saved preference but uses the 44 px editor-toolbar control and bottom drawer. The phone file viewer has no LSP control. diff --git a/docs/public/executors.md b/docs/public/executors.md index d98d4dab5c..418df3c2ae 100644 --- a/docs/public/executors.md +++ b/docs/public/executors.md @@ -280,7 +280,7 @@ Run **Test Connection**, independently verify the observed SHA256 host fingerpri The profile editor exposes remote shell and agent-readiness checks. Backend/API configuration also recognizes `ssh_workdir_root` (default `~/.kandev`) and `ssh_shell`; the current profile UI exposes `ssh_shell` but not a workdir-root field. -The remote-auth card is built from the currently enabled agents. Depending on an agent's declared methods, it can copy selected local credential files, resolve a stored secret into that agent's authentication environment variable, or run an agent-specific setup script on the remote host. GitHub can use an explicitly selected `GITHUB_TOKEN` secret as an unmanaged profile override; Kandev does not copy the host-active `gh` token. These transfers write sensitive material under the remote user's home and are best-effort—verify authentication on the remote after saving. Although the profile editor also stores Git name/email controls for SSH, the current SSH runtime does not apply them; configure Git identity on the remote host yourself. +The remote-auth card is built from the currently enabled agents. Depending on an agent's declared methods, it can copy selected local credential files, resolve a stored secret into that agent's authentication environment variable, or run an agent-specific setup script on the remote host. GitHub can use an explicitly selected `GITHUB_TOKEN` secret as an unmanaged profile override; Kandev does not copy the host-active `gh` token. These transfers write sensitive material under the remote user's home and are best-effort, verify authentication on the remote after saving. Although the profile editor also stores Git name/email controls for SSH, the current SSH runtime does not apply them; configure Git identity on the remote host yourself. diff --git a/docs/public/extending-kandev.md b/docs/public/extending-kandev.md index 0415d6628f..8be0f17f0c 100644 --- a/docs/public/extending-kandev.md +++ b/docs/public/extending-kandev.md @@ -81,7 +81,7 @@ Plugins are a peer extension mechanism to the seams above, aimed at extensions that should ship and version independently of a kandev release. A plugin backend is a Go binary that kandev spawns and supervises as a subprocess, communicating over a strict typed gRPC protocol -(`internal/plugins/`, `pkg/pluginsdk`) — it receives bus events and relays +(`internal/plugins/`, `pkg/pluginsdk`); it receives bus events and relays external webhooks, calling back into kandev through a capability-gated Host RPC service (state, secrets, read-only data, cross-plugin events). A plugin may additionally ship an optional **native frontend bundle** that the SPA loads at boot to register real routes, nav @@ -89,7 +89,7 @@ items, slot components, and WebSocket handlers, sharing kandev's own React instance and app store. Plugins are distributed as a signed-or-unsigned tarball and installed by URL, -manual upload, or filesystem sideload/sync — there is no manifest-paste +manual upload, or filesystem sideload/sync, there is no manifest-paste registration step and no credentials to issue. Plugins are part of the base product and are not a runtime feature toggle. See [Plugins](plugins.md) for the operator-facing install/operate flow, [Authoring a plugin](plugins-authoring.md) for the diff --git a/docs/public/feature-status.md b/docs/public/feature-status.md index 1e3100f484..d0d419c802 100644 --- a/docs/public/feature-status.md +++ b/docs/public/feature-status.md @@ -75,7 +75,7 @@ Released builds can lag this page. Check **Settings > System > About** or `kande | Language servers | Limited | Desktop Monaco maps TypeScript/JavaScript, Go, Rust, Python, and experimental Kotlin to task-host servers with diagnostics, completion, hover, navigation, signature help, and semantic tokens. Status distinguishes a launched process awaiting `initialize` from server-reported project work; slow Kotlin Gradle imports can take minutes, with no automatic timeout or universal percentage/ETA. Fine-pointer desktop can place active-file status in the editor toolbar or the opt-in status surface controlled by **Show status bar**; when that surface is off it falls back to the toolbar. Touch tablet uses the toolbar, and phone has no LSP control. Auto-start/installation default off; Kotlin is manual-only. Local PC and Local Docker are supported, while SSH, Sprites, remote Docker, and mobile LSP are not. | | Integrated shell terminals | Dependency-bound | Create, reopen, rename, and terminate are shipped. Shell settings affect new terminals, and executor/task-environment availability controls where the PTY runs. | | Prompts, keyboard shortcuts, notifications, appearance, and task actions | Supported | These are normal settings surfaces. Browser notification permission, custom command validity, and local shell/editor availability still apply to their effects. | -| Web UI localization | Limited | **Settings > General > Appearance** offers English, Português (Portugal), and 简体中文. The choice applies immediately, persists per browser through a `kandev_locale` cookie, and is negotiated from `Accept-Language` before defaulting to English when no cookie is set. Externalization is complete: every reachable web UI surface renders through the translation layer, and dates, numbers, and relative timestamps follow the selected locale. Per-language completeness still differs. Portuguese tracks English closely; Simplified Chinese is maintained out of band and lags, most visibly in the task workbench. An untranslated key falls back to English rather than failing. About fifteen shared `@kandev/ui` accessibility labels — mostly screen-reader-only text such as a dialog's **Close** — stay English even where the visible UI is translated. Desktop-shell and CLI surfaces, API/log diagnostics, and agent output were never in scope; user content such as task titles, chat, and diffs is never translated. Development and E2E builds list an extra **Pseudo (QA)** locale that production builds do not ship. | +| Web UI localization | Limited | **Settings > General > Appearance** offers English, Português (Portugal), and 简体中文. The choice applies immediately, persists per browser through a `kandev_locale` cookie, and is negotiated from `Accept-Language` before defaulting to English when no cookie is set. Externalization is complete: every reachable web UI surface renders through the translation layer, and dates, numbers, and relative timestamps follow the selected locale. Per-language completeness still differs. Portuguese tracks English closely; Simplified Chinese is maintained out of band and lags, most visibly in the task workbench. An untranslated key falls back to English rather than failing. About fifteen shared `@kandev/ui` accessibility labels (mostly screen-reader-only text such as a dialog's **Close**) stay English even where the visible UI is translated. Desktop-shell and CLI surfaces, API/log diagnostics, and agent output were never in scope; user content such as task titles, chat, and diffs is never translated. Development and E2E builds list an extra **Pseudo (QA)** locale that production builds do not ship. | @@ -103,7 +103,7 @@ Released builds can lag this page. Check **Settings > System > About** or `kande | Manual, preset/`@every` schedule, authenticated webhook, GitHub PR-open, and GitHub PR-merged automations | Limited | Every automation now produces the same kind of run; the Task/Run mode selector is withdrawn and runs have their own destination instead of the board. PR-open triggers need explicit repositories and ignore the saved Opened checkbox. PR-merged triggers watch task-linked pull requests, poll at up to one-minute latency, and bind their archive target to the event task; a manual run does not replay a merge. Webhooks silently retain only the first 1 MiB. **Play** remains clickable while disabled but produces no history. | | Fixed cron timezone behavior | Limited | The UI accepts fixed cron and timezone values, but the current interval scheduler does not honor the configured timezone and some fixed cron expressions may never fire. Prefer tested presets or `@every` intervals. | | GitHub push and CI automation triggers | In progress | Trigger types and UI labels exist as coming-soon/stub paths; they are not working production triggers. GitHub's separate opt-in PR CI-fix automation is shipped. | -| Automation runs | Limited | A firing creates a persistent task hidden from the board by its origin, starts immediately, and stays answerable — the old Run mode's inability to take user input is gone with the mode itself. Automations that predate this change stop producing kanban cards on their next firing. Worktrees are kept for the ten most recent finished runs per automation and reclaimed beyond that, so an older run is still readable but no longer repliable. | +| Automation runs | Limited | A firing creates a persistent task hidden from the board by its origin, starts immediately, and stays answerable; the old Run mode's inability to take user input is gone with the mode itself. Automations that predate this change stop producing kanban cards on their next firing. Worktrees are kept for the ten most recent finished runs per automation and reclaimed beyond that, so an older run is still readable but no longer repliable. | diff --git a/docs/public/index.md b/docs/public/index.md index 2f9573a365..ef8f0be819 100644 --- a/docs/public/index.md +++ b/docs/public/index.md @@ -9,10 +9,10 @@ Kandev lets you assign repository work to coding agents, review the result, and ## Start here -1. [Get started](use-kandev.md) — install Kandev, add a repository, and run a first task. -2. [Tasks and workflows](tasks-and-workflows.md) — create work, choose a workflow, and use plans. -3. [Sessions and review](sessions-and-review.md) — supervise an agent, inspect changes, and finish safely. -4. [Security and trust](security.md) — choose an appropriate access boundary before sharing Kandev with others. +1. [Get started](use-kandev.md); install Kandev, add a repository, and run a first task. +2. [Tasks and workflows](tasks-and-workflows.md); create work, choose a workflow, and use plans. +3. [Sessions and review](sessions-and-review.md); supervise an agent, inspect changes, and finish safely. +4. [Security and trust](security.md); choose an appropriate access boundary before sharing Kandev with others.
More guides for running work diff --git a/docs/public/integrations.md b/docs/public/integrations.md index 64dad7a3cb..9ec56897ad 100644 --- a/docs/public/integrations.md +++ b/docs/public/integrations.md @@ -307,7 +307,7 @@ server-owned templates that include only the linked PR's canonical URL; their text cannot be customized through the UI, HTTP, MCP, or storage. They report the observed event without prescribing an action; the task workflow and agent context determine the response. GitLab has the same auto-fix, auto-merge, and -lifecycle controls — see "Automate a linked merge request" below. Selecting a +lifecycle controls. See "Automate a linked merge request" below. Selecting a destination workflow step for a lifecycle event remains follow-up work for both providers. @@ -362,9 +362,9 @@ Kandev reuses the existing lightweight task MR poller, which checks linked MRs r **Auto-merge when ready** merges the linked MR only when it is open, not a draft, its pipeline is passing, it has no unresolved discussions, and GitLab's own merge-readiness verdict (`detailed_merge_status`, falling back to `merge_status` on older GitLab versions) agrees. An auto-fix dispatch in the same evaluation pass takes priority over an auto-merge attempt in that pass. -**Your review is requested** matches the GitLab account connected to the task's workspace against the MR's current reviewer list — GitLab has no separate "review requested" API event, so appearing as a reviewer is the signal. The first observation is a quiet baseline; only a later false-to-true transition to being a reviewer wakes the agent. Staying assigned across MR updates does not re-fire it; clearing the reviewer assignment and being re-added — for example, for a re-review after changes — rearms the next transition. +**Your review is requested** matches the GitLab account connected to the task's workspace against the MR's current reviewer list: GitLab has no separate "review requested" API event, so appearing as a reviewer is the signal. The first observation is a quiet baseline; only a later false-to-true transition to being a reviewer wakes the agent. Staying assigned across MR updates does not re-fire it; clearing the reviewer assignment and being re-added (for example, for a re-review after changes) rearms the next transition. -**MR merged** and **MR closed without merging** each notify once when the linked MR enters that terminal state; reopening and re-closing an MR re-arms the notification. Kandev delivers lifecycle notifications to the task's active promptable session, preferring the primary session, and does not interrupt a busy session — it queues the message for delivery when that session is available. Lifecycle messages report only the observed event and the MR's canonical URL and cannot be customized. +**MR merged** and **MR closed without merging** each notify once when the linked MR enters that terminal state; reopening and re-closing an MR re-arms the notification. Kandev delivers lifecycle notifications to the task's active promptable session, preferring the primary session, and does not interrupt a busy session; it queues the message for delivery when that session is available. Lifecycle messages report only the observed event and the MR's canonical URL and cannot be customized. For a GitLab Review Watch task with any MR lifecycle prompt enabled, the **Auto** cleanup policy retains the terminal task so lifecycle delivery can finish, matching the GitHub review-watch behavior described above. @@ -470,7 +470,7 @@ Linear polling is also bounded. **Default (Linear order)** reads one page of 50; Sentry configuration is workspace-specific and supports multiple named instances. This is useful when one Kandev workspace spans different Sentry organizations or self-hosted installations. -Create an instance with a unique name, base URL, and bearer authentication token. The default URL is `https://sentry.io`; replace it for self-hosted Sentry. A URL with no scheme becomes HTTPS. It must be a bare HTTP(S) host root—paths, queries, and fragments are rejected. The UI lists `org:read`, `project:read`, and `event:read` as the required read scopes. +Create an instance with a unique name, base URL, and bearer authentication token. The default URL is `https://sentry.io`; replace it for self-hosted Sentry. A URL with no scheme becomes HTTPS. It must be a bare HTTP(S) host root, paths, queries, and fragments are rejected. The UI lists `org:read`, `project:read`, and `event:read` as the required read scopes. On any saved edit, a blank token preserves the existing token, including when the URL changes. The pre-save **Test connection** candidate cannot reuse that stored token after a URL change, so paste the token to test the new URL before saving. diff --git a/docs/public/k8s.md b/docs/public/k8s.md index cd9e290bbd..94ee0d81d8 100644 --- a/docs/public/k8s.md +++ b/docs/public/k8s.md @@ -6,7 +6,7 @@ status: experimental # Kubernetes -Kandev ships example Kubernetes YAML in `k8s/`. It is a single-replica, persistent deployment example—not a Helm chart, operator, or supported high-availability topology. The release workflow publishes container images but does not apply these manifests to a cluster. Review and adapt every manifest before production use. +Kandev ships example Kubernetes YAML in `k8s/`. It is a single-replica, persistent deployment example, not a Helm chart, operator, or supported high-availability topology. The release workflow publishes container images but does not apply these manifests to a cluster. Review and adapt every manifest before production use. ## Quick path @@ -248,8 +248,8 @@ The `Recreate` strategy stops active local agents. SQLite migrations run on star This release includes a one-time task-worktree ownership schema cutover (see [Operations](operations.md)). The cutover rewrites the worktree ownership tables in one transaction and drops legacy schema. It requires: -- A verified pre-upgrade database backup. For SQLite, create and verify a manual snapshot **before** starting the upgrade — the automatic pre-migration snapshot is taken during startup of the new binary and cannot be verified beforehand. For PostgreSQL, use `pg_dump` (the cutover does not invoke it). -- All writers stopped during the cutover — with PostgreSQL, do not run a mixed-version fleet across the upgrade; the cutover takes a database advisory lock and fails closed if it cannot serialize. +- A verified pre-upgrade database backup. For SQLite, create and verify a manual snapshot **before** starting the upgrade; the automatic pre-migration snapshot is taken during startup of the new binary and cannot be verified beforehand. For PostgreSQL, use `pg_dump` (the cutover does not invoke it). +- All writers stopped during the cutover. With PostgreSQL, do not run a mixed-version fleet across the upgrade; the cutover takes a database advisory lock and fails closed if it cannot serialize. - One successful schema initializer: keep the deployment at a single replica during startup, and check `kubectl rollout status` before scaling out. If the initializer reports a worktree-ownership conflict, stop the rollout and @@ -258,7 +258,7 @@ Restore service with a compatible pre-cutover image, or deploy the migration hotfix and retry against the unchanged PVC. Do not run an older image against a database after the cutover has committed. -`kubectl rollout undo` changes the image, not the database schema. After the cutover the final schema is intentionally not readable by older binaries, so a binary downgrade requires restoring the matching pre-upgrade database backup — do not start an older image against the normalized database. Reapplying the checked-in `k8s/deployment.yaml` without the image transformation resets the image to `kandev:latest`, so keep your production customization in your own overlay or deployment repository. +`kubectl rollout undo` changes the image, not the database schema. After the cutover the final schema is intentionally not readable by older binaries, so a binary downgrade requires restoring the matching pre-upgrade database backup; do not start an older image against the normalized database. Reapplying the checked-in `k8s/deployment.yaml` without the image transformation resets the image to `kandev:latest`, so keep your production customization in your own overlay or deployment repository. ## Remove while retaining data diff --git a/docs/public/operations.md b/docs/public/operations.md index 9c2d37a055..f803562259 100644 --- a/docs/public/operations.md +++ b/docs/public/operations.md @@ -70,7 +70,7 @@ After startup it returns HTTP 200 with: {"status":"ok","service":"kandev","mode":"websocket+http","version":"1.2.3"} ``` -It returns HTTP 503 with `status: "starting"` (plus the same `version`) until routes, the agent registry, and the listener are ready. The supplied Kubernetes probes use this endpoint. `/health` is unauthenticated even when auth is enabled, so it's also the credential-free way for monitoring to read the running version — no need to authenticate to **System > About** just to check what build is deployed. +It returns HTTP 503 with `status: "starting"` (plus the same `version`) until routes, the agent registry, and the listener are ready. The supplied Kubernetes probes use this endpoint. `/health` is unauthenticated even when auth is enabled, so it's also the credential-free way for monitoring to read the running version; there is no need to authenticate to **System > About** just to check what build is deployed. For application diagnostics, open **Settings > System > Status** or request: @@ -144,7 +144,7 @@ Scheduled cleanup is disabled by default and runs only after the configured reso period. Orphaned task workspaces and rotated Go caches move into Kandev's quarantine before permanent deletion. Each entry shows its `delete_after` retention deadline: **Delete** and **Clear eligible** cannot remove it before that time. The deadline is the earliest safe deletion -time, not an exact promise—the first successful scheduled or full manual maintenance run after the +time, not an exact promise, the first successful scheduled or full manual maintenance run after the deadline performs the purge, subject to the idle gate and any preemption. Use **Clear eligible** to remove only entries whose deadlines have passed. It reports protected @@ -251,7 +251,7 @@ authoritative. Do not delete ownership rows by hand. Start a compatible pre-cutover binary to restore service, or deploy the migration hotfix and retry the upgrade against the unchanged database. -The normalized schema is intentionally incompatible with older binaries. To downgrade, stop all instances and restore the pre-upgrade backup — never start an older binary against a post-cutover database. SQLite restores from its automatic pre-migration snapshot; PostgreSQL restores your verified `pg_dump` backup. +The normalized schema is intentionally incompatible with older binaries. To downgrade, stop all instances and restore the pre-upgrade backup; never start an older binary against a post-cutover database. SQLite restores from the verified manual snapshot created before the upgrade; PostgreSQL restores your verified `pg_dump` backup. Switching `database.driver` does not migrate data. PostgreSQL and shared NATS remove two single-process data constraints, but they do not make Kandev horizontally scalable: WebSocket subscriptions, execution lifecycle/control state, and task workspaces remain process- or filesystem-local. The current product and supplied deployment validate one backend replica only; do not add replicas based on the database and event bus alone. @@ -442,10 +442,11 @@ Kandev warns when its live WebSocket connection has not recovered for three seco **Settings > System > Feature Toggles** currently exposes: -- **Office mode** — experimental, medium risk, and off in the production profile by default. -- **Claude background prompt handoff** — experimental, high risk, and off in every profile by default. Enabling it lets Claude Code accept another prompt after its foreground yields while recognized async subagent, `run_in_background` shell, or Monitor work remains active. ACP lifecycle gaps can misclassify activity or overlap prompts; use it only for controlled testing. -- **Unread divider** — a per-user setting at **Settings > General > Task Actions**. It defaults off, takes effect immediately, and controls both the Slack-style **New** divider and read-cursor updates while that user's transcript view is visible. -- **Debug mode** — high risk; enables diagnostic endpoints and agent-message logging that can contain sensitive content. +- **Office mode**: experimental, medium risk, and off in the production profile by default. +- **App status bar**: stable, low risk, and off in the production profile by default. Enabling it adds the desktop/tablet bar and phone Status entry after restart; disabling it again does not stop connections, metrics collection requested by other clients, or plugins. Urgent WebSocket connectivity warnings still remain visible while the feature is off. +- **Claude background prompt handoff**: experimental, high risk, and off in every profile by default. Enabling it lets Claude Code accept another prompt after its foreground yields while recognized async subagent, `run_in_background` shell, or Monitor work remains active. ACP lifecycle gaps can misclassify activity or overlap prompts; use it only for controlled testing. +- **Unread divider**: a per-user setting at **Settings > General > Task Actions**. It defaults off, takes effect immediately, and controls both the Slack-style **New** divider and read-cursor updates while that user's transcript view is visible. +- **Debug mode**: high risk; enables diagnostic endpoints and agent-message logging that can contain sensitive content. Each feature toggle requires restart. A value supplied explicitly by its environment variable locks the UI control; the debug toggle is also locked by explicit legacy/debug-message environment variables. Otherwise the UI stores an override in the database. The page can request restart only when the native local supervisor is available. A normal Unix `kandev` terminal launch is supervised; Desktop, a service, a container, a directly started backend, a deploy preview, or Windows requires a manual application restart. @@ -476,8 +477,8 @@ drawer mirrors it as the saved left sequence followed by the saved right sequenc ## Related pages -- [Configuration](configuration.md) — paths, database, logging, NATS, Docker, and security-sensitive environment variables -- [Executors](executors.md) — runtime lifecycle, credentials, cleanup, and isolation boundaries -- [Git operations](git-operations.md) — branches, worktrees, push, and pull-request behavior -- [Automation and MCP](automation-and-mcp.md) — external MCP routes and their current unauthenticated trust boundary -- [Windows support](windows-support.md) — Windows-native limitations and supported alternatives +- [Configuration](configuration.md); paths, database, logging, NATS, Docker, and security-sensitive environment variables +- [Executors](executors.md); runtime lifecycle, credentials, cleanup, and isolation boundaries +- [Git operations](git-operations.md); branches, worktrees, push, and pull-request behavior +- [Automation and MCP](automation-and-mcp.md); external MCP routes and their current unauthenticated trust boundary +- [Windows support](windows-support.md); Windows-native limitations and supported alternatives diff --git a/docs/public/plugins-authoring.md b/docs/public/plugins-authoring.md index 67f6a013e4..15a8b924fa 100644 --- a/docs/public/plugins-authoring.md +++ b/docs/public/plugins-authoring.md @@ -402,43 +402,39 @@ function useNoteValue(taskId, panelId) { // duplicating the read-and-commit logic without its guards. const guardRef = host.React.useRef({ cancelled: false, generation: 0 }); - const refresh = host.React.useCallback( - (options = {}) => { - const preserveValue = options.preserveValue ?? dirtyRef.current; - const guard = guardRef.current; - const generation = ++guard.generation; - return host.storage.get("task", taskId, "note").then( - (entry) => { - // Ignore a response that resolves after taskId changed, the panel - // unmounted, or a newer refresh started — otherwise a stale read - // (the previous task's note, or one of two overlapping reads that - // resolved out of order) can land in the current field. - if (guard.cancelled || generation !== guard.generation) return; - if (!preserveValue && !dirtyRef.current) - setValue(entry ? entry.value : ""); - updatedAtRef.current = entry ? entry.updatedAt : undefined; - setLoadedTaskId(taskId); - setReadError(false); - return true; - }, - () => { - // Do not mark the task loaded after a rejected read. Rendering an - // empty editor here would allow its first save to omit - // ifUnmodifiedSince and overwrite an existing note. Keep the panel - // in a retry state until an authoritative read succeeds. - if (guard.cancelled || generation !== guard.generation) return false; - setReadError(true); - return false; - }, - ); - }, - [taskId], - ); + const refresh = host.React.useCallback((options = {}) => { + const preserveValue = options.preserveValue ?? dirtyRef.current; + const guard = guardRef.current; + const generation = ++guard.generation; + return host.storage.get("task", taskId, "note").then( + (entry) => { + // Ignore a response that resolves after taskId changed, the panel + // unmounted, or a newer refresh started; otherwise a stale read + // (the previous task's note, or one of two overlapping reads that + // resolved out of order) can land in the current field. + if (guard.cancelled || generation !== guard.generation) return; + if (!preserveValue && !dirtyRef.current) setValue(entry ? entry.value : ""); + updatedAtRef.current = entry ? entry.updatedAt : undefined; + setLoadedTaskId(taskId); + setReadError(false); + return true; + }, + () => { + // Do not mark the task loaded after a rejected read. Rendering an + // empty editor here would allow its first save to omit + // ifUnmodifiedSince and overwrite an existing note. Keep the panel + // in a retry state until an authoritative read succeeds. + if (guard.cancelled || generation !== guard.generation) return false; + setReadError(true); + return false; + }, + ); + }, [taskId]); host.React.useEffect(() => { const guard = guardRef.current; guard.cancelled = false; - // Clear the previous task's value/timestamp synchronously — otherwise it + // Clear the previous task's value/timestamp synchronously; otherwise it // stays on screen (and could be sent as ifUnmodifiedSince under the new // taskId) until this effect's first refresh() resolves. setValue(""); @@ -448,7 +444,7 @@ function useNoteValue(taskId, panelId) { setReadError(false); refresh(); // Scope echo suppression to this panel instance rather than the shared - // per-tab default writer id — otherwise a second surface editing the + // per-tab default writer id; otherwise a second surface editing the // same note (a kanban shortcut, another panel) looks like this panel's // own echo and its write never arrives here. const unsubscribe = host.storage.subscribe( @@ -634,14 +630,14 @@ The same registration and panel identity drive both viewports, so a panel remain selected through a slow or failed reload and is closed only after a ready generation omits it or the plugin is explicitly disabled/uninstalled. -`writerId` is appended to the host's own per-tab id, not a full replacement — +`writerId` is appended to the host's own per-tab id, not a full replacement; a static value like `panelId` is the same across every tab that has that panel open, so two different tabs subscribing with the same raw `panelId` would otherwise suppress each other's real, cross-tab updates as if they were local echoes. Pass `ifUnmodifiedSince` (the `updatedAt` from the last `get`) to `set` and handle the resulting `409` by refetching, rather than silently discarding a concurrent edit. `host.storage.list` returns every entry under a -`(scope, scopeId)` pair with no pagination — fine for a handful of keys per +`(scope, scopeId)` pair with no pagination; fine for a handful of keys per task, not for an unbounded per-item collection. ### 4. Webhook receiver @@ -697,13 +693,13 @@ Host reader because event queues are bounded and delivery is best-effort. ### 6. Kanban-aware contribution `registerTaskMenuAction` adds an item to the kanban card's context/dropdown -menu — group `"edit"` nests it inside the `Edit` submenu, group `"primary"` +menu; group `"edit"` nests it inside the `Edit` submenu, group `"primary"` renders it as a flat, top-level item positioned between the "Move to"/"Send to workflow" submenus and the "Link" submenu. `task-card-indicators` (see the named slots table) is the matching read-only surface, rendered beside the PR status icon on every card. `task-card-tags` is a sibling read-only surface with the same `slotProps` shape, mounted in its -own row instead of that cramped title-row spot — reach for it when a +own row instead of that cramped title-row spot; reach for it when a contribution (e.g. a row of tag chips) needs its own width. Both `visible(context)` and `run(context)` receive the card's actual `presentation`: `"desktop"` on the desktop kanban and `"mobile"` on the phone kanban. Use it @@ -733,9 +729,9 @@ Kanban components directly. `registerTaskFilter` adds a client-side, multi-select filter section to the kanban board's display dropdown, next to the built-in Workflow and Repository sections. The plugin supplies its own options (including any "untagged"-style -sentinel — the host does not special-case option values) and a `matches` +sentinel; the host does not special-case option values) and a `matches` predicate; filtering runs entirely against tasks already loaded in the -board's in-memory state, with no backend query or persistence — selections +board's in-memory state, with no backend query or persistence; selections reset on reload. ```js @@ -750,7 +746,7 @@ registry.registerTaskFilter({ }); ``` -An empty selection is implicit "All" for that section — `matches` is only +An empty selection is implicit "All" for that section: `matches` is only called once at least one option is selected, and a `matches` that throws is caught, logged, and treated as non-matching for that task. @@ -843,7 +839,7 @@ repackaging. - **Reusing a raw surface id as writerId:** a static id like panelId is the same across every tab that has that surface open. Passed as-is to host.storage's writerId, it makes two different tabs look like the same - writer and suppresses real cross-tab updates as if they were local echoes — + writer and suppresses real cross-tab updates as if they were local echoes; the host already appends it to a per-tab id, so pass the surface id, not a fabricated combined string. - **Writing outside KANDEV_PLUGIN_DATA_DIR:** arbitrary files belong below the diff --git a/docs/public/plugins-manifest.md b/docs/public/plugins-manifest.md index 47903ce66b..57ad211ace 100644 --- a/docs/public/plugins-manifest.md +++ b/docs/public/plugins-manifest.md @@ -6,7 +6,7 @@ status: experimental # Plugin Manifest Reference -`manifest.yaml` is the authoritative description of a plugin — identity, +`manifest.yaml` is the authoritative description of a plugin; identity, runtime executables, capabilities, webhooks, config schema, and optional UI bundle. Kandev parses and validates it **before any plugin code runs**. See [Authoring a plugin](plugins-authoring.md) for the build @@ -50,9 +50,9 @@ capabilities: api_write: ["tasks"] # gates Tasks().Create/Update state: true secrets: true - agent_invoke: true # gates Host.InvokeUtilityAgent - auth: true # gates external (OIDC/SAML) login — see ADR 0050 - user_state: true # gates host.storage (per-user browser storage) + agent_invoke: true # gates Host.InvokeUtilityAgent + auth: true # gates external (OIDC/SAML) login, see ADR 0050 + user_state: true # gates host.storage (per-user browser storage) webhooks: - key: "slack-events" @@ -104,65 +104,65 @@ ui: # optional native frontend plugin
Complete field reference and validation rules -| Field | Required | Type | Notes | -| ------------------------------ | ------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | yes | string | Must match `^[a-z0-9][a-z0-9._-]*$` (lowercase alphanumeric, dots, underscores, hyphens; must start with a lowercase alphanumeric). Directory name under `~/.kandev/plugins/`. | -| `api_version` | yes | int | Must be exactly `1`. Any other value is rejected. | -| `version` | yes | string | Free-form; used as the version directory name (`~/.kandev/plugins///`). | -| `display_name` | no | string | Shown in Settings > Plugins. | -| `description` | no | string | Shown in Settings > Plugins. | -| `author` | no | string | Free-form. | -| `categories` | no | string[] | Each entry must be one of `connector`, `automation`, `tools`, `analytics`. Unknown values are rejected. | -| `icon` | no | string | Package-relative path (e.g. `icon.svg`) to an image the package ships, rendered on the [marketplace](plugins-marketplace.md) card and in plugin lists. The registry index-build resolves it to an absolute `icon_url`; for an installed plugin it is served from the extracted package. Omit it and the card falls back to a letter tile. | -| `repo_url` | no | string | Absolute `http(s)` URL to the plugin's source repository. Rendered as a "Repo" link in Settings > Plugins (both the installed list and the plugin detail). Any other scheme (e.g. `javascript:`) is rejected at registration. Distinct from the marketplace card's `repo_url`, which the registry derives from `plugins.yaml`; declare this in the manifest so sideloaded and directly-installed plugins also carry the link. | -| `runtime.type` | conditionally | string | `"binary"` is the only supported value. Setting it (vs. leaving it empty) makes the manifest **runtime-managed** — see "Managed vs. legacy" below. | -| `runtime.executables` | required when `runtime.type: binary` | map\ | Key is `-` (e.g. `linux-amd64`, `darwin-arm64`, `windows-amd64`); value is a clean, package-relative path under `server/` (no leading `/`, no `..` segments). At least one entry required; the running host's key must be present at install time. Windows values end in `.exe`. | -| `min_kandev_version` | no | string | Lowest Kandev version this plugin supports. Enforced at **install time**: installing onto an older Kandev fails with `requires kandev >= , running ` and nothing is registered. Already-installed plugins are never re-checked at load or start. A Kandev built from source reports the version `dev` and skips the check entirely, so local development is unaffected. Compare as dotted numeric segments (`0.78.0`) — a `v` prefix does not parse as a number and falls back to a string comparison. | -| `capabilities.events` | no | string[] | Bus subjects (or wildcard patterns) this plugin subscribes to. See "Event subscription vocabulary" below. | -| `capabilities.api_read` | no | string[] | Gates the Host data API's read-only accessors. Each entry is a resource name: `tasks`, `sessions`, `messages`, `workspaces`, `workflows`, `agent_profiles`, `repositories`. Calling the matching `Host` accessor (e.g. `Tasks()`) without its resource declared returns gRPC `PermissionDenied`. See "Host data API resource vocabulary" below. | -| `capabilities.api_write` | no | string[] | Gates Host data writes. Current resources are `tasks` (`Tasks().Create/Update`) and `messages` (`Messages().Send`); reads and writes are gated independently. | -| `capabilities.state` | no | bool | Gates `Host.GetState`/`SetState`/`DeleteState`/`ListState`. Calling any of them without this set to `true` returns gRPC `PermissionDenied`. | -| `capabilities.secrets` | no | bool | Gates `Host.RevealSecret`/`GetSecret`/`SetSecret`/`DeleteSecret`. Calling any of them without this set to `true` returns gRPC `PermissionDenied`. | -| `capabilities.agent_invoke` | no | bool | Gates `Host.InvokeUtilityAgent` — a one-shot completion run by the utility agent selected for this plugin. Declare a `utility_agent` config property with `type: string` and `format: utility-agent`; Settings > Plugins renders the picker. The selected utility agent resolves its own enabled ACP profile. Calling without this capability returns gRPC `PermissionDenied`; calling without a valid profile-backed selection returns gRPC `FailedPrecondition`. See ADR 0048. | -| `capabilities.auth` | no | bool | Lets the plugin log a visitor in against an external IdP (OIDC/SAML). Its webhook validates the token, then asserts the identity to Kandev via the `X-Kandev-Auth-Login` response header (`{provider, subject, email, display_name}`); Kandev mints the session and sets the cookie — the plugin never sees the token. Requires authentication enabled; new users are provisioned as members, and Kandev never creates an admin nor auto-links to an existing admin account. **You MUST only assert an email the IdP verified as owned by the subject — a spoofed email claim is account takeover.** Highest-privilege capability; grant only to trusted plugins. See ADR 0050. | -| `capabilities.user_state` | no | bool | Gates `host.storage` (`get`/`set`/`delete`/`list`/`subscribe`), the authenticated per-user browser storage surface at `/api/plugins/{id}/user-state/...`. Unlike `capabilities.state` (the gRPC `Host.SetState` family, written by the plugin's own backend), this is reachable directly from the plugin's frontend bundle with no Go backend required — every read/write is scoped to the calling user. Calling the route without this capability returns `403`. See [Authoring a plugin](plugins-authoring.md) and the per-user-plugin-storage decision record. | -| `webhooks[].key` | yes | string | Must be unique within the manifest. Used in the relay path `POST /api/plugins/{id}/webhooks/{key}`. | -| `webhooks[].description` | no | string | Free-form. | -| `webhooks[].method` | no | string | **Informational only** — kandev does not validate or enforce the inbound HTTP method against this value. | -| `webhooks[].access` | no | string | `public` (default) allows anonymous callers; `authenticated` requires a Kandev identity. Cookie-authenticated browser calls remain subject to Kandev's same-origin policy. | -| `webhooks[].max_body_bytes` | no | int | Request-body limit for this key. Defaults to 4 MiB. Public webhooks cannot exceed 4 MiB; authenticated webhooks can request up to 16 MiB. Plugins using this field should declare the first supporting `min_kandev_version`. | -| `auth_providers[]` | no | object[] | Login buttons this plugin contributes to the pre-auth login screen (needs `capabilities.auth`). Each is `{ id, display_name, initiate }`, where `initiate` names one of the plugin's `webhooks[].key` values — the button navigates to that webhook, which 302-redirects to the IdP. Surfaced anonymously in the boot payload as `auth.ssoProviders`. | -| `config_schema` | no | object | JSON-Schema-like object driving the settings form at **Settings > Plugins > ``** (`GET /api/plugins/{id}/config` and `PATCH /api/plugins/{id}`). See "Config schema validation and secret fields" below. | -| `ui.bundle` | no | string | Root-relative path (must start with `/`, e.g. `/ui/bundle.js`) to the plugin's native UI ES module, served at `GET /api/plugins/{id}/bundle`. | -| `ui.styles` | no | string[] | Root-relative CSS paths (each must start with `/`), served at `GET /api/plugins/{id}/ui/*` and injected as `` tags on load. | -| `ui.pages` | no | object[] | Optional declarative metadata accepted by the manifest model. The current frontend does not render these entries; a native bundle registers its supported routes/nav/slots at runtime, so most plugins omit `ui.pages`. | -| `ui.pages[].key` | yes\* | string | Stable identifier for the page (\*required when a page entry is present). | -| `ui.pages[].title` | yes\* | string | Display title. | -| `ui.pages[].path` | yes\* | string | Route path for the page. | -| `ui.pages[].surface` | yes\* | string | Metadata enum (`settings` · `task-panel` · `main-nav`) validated by the manifest parser. It is not a current frontend mount; use the registry hooks in the authoring guide. | -| `ui.keybindings` | no | object[] | Declares plugin keybindings bound at runtime via `registerKeybinding`. Requires `ui.bundle`. | -| `ui.keybindings[].id` | yes\* | string | Stable, plugin-local slug (_required when a keybinding entry is present). Must match `^[a-z0-9][a-z0-9-]_$`and be unique within this plugin's own`ui.keybindings`list — not globally; the effective shortcut is namespaced`plugin:{pluginId}:{id}`. | -| `ui.keybindings[].default` | yes\* | string | Default combo string. `+`-separated tokens: zero or more modifiers from `mod`, `ctrl`, `cmd`, `meta`, `alt`, `option`, `shift` (`mod` = ⌘ on macOS, Ctrl elsewhere) plus exactly one non-modifier key. `shift` may not combine with a digit or symbol key — the browser reports the shifted glyph for those keys, so the combo could never match. | -| `ui.keybindings[].description` | yes\* | string | Non-empty, human-readable label shown in **Settings > Keyboard Shortcuts**. | +| Field | Required | Type | Notes | +|---|---|---|---| +| `id` | yes | string | Must match `^[a-z0-9][a-z0-9._-]*$` (lowercase alphanumeric, dots, underscores, hyphens; must start with a lowercase alphanumeric). Directory name under `~/.kandev/plugins/`. | +| `api_version` | yes | int | Must be exactly `1`. Any other value is rejected. | +| `version` | yes | string | Free-form; used as the version directory name (`~/.kandev/plugins///`). | +| `display_name` | no | string | Shown in Settings > Plugins. | +| `description` | no | string | Shown in Settings > Plugins. | +| `author` | no | string | Free-form. | +| `categories` | no | string[] | Each entry must be one of `connector`, `automation`, `tools`, `analytics`. Unknown values are rejected. | +| `icon` | no | string | Package-relative path (e.g. `icon.svg`) to an image the package ships, rendered on the [marketplace](plugins-marketplace.md) card and in plugin lists. The registry index-build resolves it to an absolute `icon_url`; for an installed plugin it is served from the extracted package. Omit it and the card falls back to a letter tile. | +| `repo_url` | no | string | Absolute `http(s)` URL to the plugin's source repository. Rendered as a "Repo" link in Settings > Plugins (both the installed list and the plugin detail). Any other scheme (e.g. `javascript:`) is rejected at registration. Distinct from the marketplace card's `repo_url`, which the registry derives from `plugins.yaml`; declare this in the manifest so sideloaded and directly-installed plugins also carry the link. | +| `runtime.type` | conditionally | string | `"binary"` is the only supported value. Setting it (vs. leaving it empty) makes the manifest **runtime-managed**: see "Managed vs. legacy" below. | +| `runtime.executables` | required when `runtime.type: binary` | map\ | Key is `-` (e.g. `linux-amd64`, `darwin-arm64`, `windows-amd64`); value is a clean, package-relative path under `server/` (no leading `/`, no `..` segments). At least one entry required; the running host's key must be present at install time. Windows values end in `.exe`. | +| `min_kandev_version` | no | string | Lowest Kandev version this plugin supports. Enforced at **install time**: installing onto an older Kandev fails with `requires kandev >= , running ` and nothing is registered. Already-installed plugins are never re-checked at load or start. A Kandev built from source reports the version `dev` and skips the check entirely, so local development is unaffected. Compare as dotted numeric segments (`0.78.0`); a `v` prefix does not parse as a number and falls back to a string comparison. | +| `capabilities.events` | no | string[] | Bus subjects (or wildcard patterns) this plugin subscribes to. See "Event subscription vocabulary" below. | +| `capabilities.api_read` | no | string[] | Gates the Host data API's read-only accessors. Each entry is a resource name: `tasks`, `sessions`, `messages`, `workspaces`, `workflows`, `agent_profiles`, `repositories`. Calling the matching `Host` accessor (e.g. `Tasks()`) without its resource declared returns gRPC `PermissionDenied`. See "Host data API resource vocabulary" below. | +| `capabilities.api_write` | no | string[] | Gates Host data writes. Current resources are `tasks` (`Tasks().Create/Update`) and `messages` (`Messages().Send`); reads and writes are gated independently. | +| `capabilities.state` | no | bool | Gates `Host.GetState`/`SetState`/`DeleteState`/`ListState`. Calling any of them without this set to `true` returns gRPC `PermissionDenied`. | +| `capabilities.secrets` | no | bool | Gates `Host.RevealSecret`/`GetSecret`/`SetSecret`/`DeleteSecret`. Calling any of them without this set to `true` returns gRPC `PermissionDenied`. | +| `capabilities.agent_invoke` | no | bool | Gates `Host.InvokeUtilityAgent`; a one-shot completion run by the utility agent selected for this plugin. Declare a `utility_agent` config property with `type: string` and `format: utility-agent`; Settings > Plugins renders the picker. The selected utility agent resolves its own enabled ACP profile. Calling without this capability returns gRPC `PermissionDenied`; calling without a valid profile-backed selection returns gRPC `FailedPrecondition`. See ADR 0048. | +| `capabilities.auth` | no | bool | Lets the plugin log a visitor in against an external IdP (OIDC/SAML). Its webhook validates the token, then asserts the identity to Kandev via the `X-Kandev-Auth-Login` response header (`{provider, subject, email, display_name}`); Kandev mints the session and sets the cookie; the plugin never sees the token. Requires authentication enabled; new users are provisioned as members, and Kandev never creates an admin nor auto-links to an existing admin account. **You MUST only assert an email the IdP verified as owned by the subject; a spoofed email claim is account takeover.** Highest-privilege capability; grant only to trusted plugins. See ADR 0050. | +| `capabilities.user_state` | no | bool | Gates `host.storage` (`get`/`set`/`delete`/`list`/`subscribe`), the authenticated per-user browser storage surface at `/api/plugins/{id}/user-state/...`. Unlike `capabilities.state` (the gRPC `Host.SetState` family, written by the plugin's own backend), this is reachable directly from the plugin's frontend bundle with no Go backend required; every read/write is scoped to the calling user. Calling the route without this capability returns `403`. See [Authoring a plugin](plugins-authoring.md) and the per-user-plugin-storage decision record. | +| `webhooks[].key` | yes | string | Must be unique within the manifest. Used in the relay path `POST /api/plugins/{id}/webhooks/{key}`. | +| `webhooks[].description` | no | string | Free-form. | +| `webhooks[].method` | no | string | **Informational only**: kandev does not validate or enforce the inbound HTTP method against this value. | +| `webhooks[].access` | no | string | `public` (default) allows anonymous callers; `authenticated` requires a Kandev identity. Cookie-authenticated browser calls remain subject to Kandev's same-origin policy. | +| `webhooks[].max_body_bytes` | no | int | Request-body limit for this key. Defaults to 4 MiB. Public webhooks cannot exceed 4 MiB; authenticated webhooks can request up to 16 MiB. Plugins using this field should declare the first supporting `min_kandev_version`. | +| `auth_providers[]` | no | object[] | Login buttons this plugin contributes to the pre-auth login screen (needs `capabilities.auth`). Each is `{ id, display_name, initiate }`, where `initiate` names one of the plugin's `webhooks[].key` values: the button navigates to that webhook, which 302-redirects to the IdP. Surfaced anonymously in the boot payload as `auth.ssoProviders`. | +| `config_schema` | no | object | JSON-Schema-like object driving the settings form at **Settings > Plugins > ``** (`GET /api/plugins/{id}/config` and `PATCH /api/plugins/{id}`). See "Config schema validation and secret fields" below. | +| `ui.bundle` | no | string | Root-relative path (must start with `/`, e.g. `/ui/bundle.js`) to the plugin's native UI ES module, served at `GET /api/plugins/{id}/bundle`. | +| `ui.styles` | no | string[] | Root-relative CSS paths (each must start with `/`), served at `GET /api/plugins/{id}/ui/*` and injected as `` tags on load. | +| `ui.pages` | no | object[] | Optional declarative metadata accepted by the manifest model. The current frontend does not render these entries; a native bundle registers its supported routes/nav/slots at runtime, so most plugins omit `ui.pages`. | +| `ui.pages[].key` | yes* | string | Stable identifier for the page (*required when a page entry is present). | +| `ui.pages[].title` | yes* | string | Display title. | +| `ui.pages[].path` | yes* | string | Route path for the page. | +| `ui.pages[].surface` | yes* | string | Metadata enum (`settings` · `task-panel` · `main-nav`) validated by the manifest parser. It is not a current frontend mount; use the registry hooks in the authoring guide. | +| `ui.keybindings` | no | object[] | Declares plugin keybindings bound at runtime via `registerKeybinding`. Requires `ui.bundle`. | +| `ui.keybindings[].id` | yes* | string | Stable, plugin-local slug (*required when a keybinding entry is present). Must match `^[a-z0-9][a-z0-9-]*$` and be unique within this plugin's own `ui.keybindings` list, not globally; the effective shortcut is namespaced `plugin:{pluginId}:{id}`. | +| `ui.keybindings[].default` | yes* | string | Default combo string. `+`-separated tokens: zero or more modifiers from `mod`, `ctrl`, `cmd`, `meta`, `alt`, `option`, `shift` (`mod` = ⌘ on macOS, Ctrl elsewhere) plus exactly one non-modifier key. `shift` may not combine with a digit or symbol key; the browser reports the shifted glyph for those keys, so the combo could never match. | +| `ui.keybindings[].description` | yes* | string | Non-empty, human-readable label shown in **Settings > Keyboard Shortcuts**. | `ui.pages` is declarative manifest metadata only and is not currently rendered by the frontend. A native bundle's runtime nav items, icons, routes, named slots, and per-route title-bar chrome (`registerNavItem`, `registerRoute`, and `registerComponent`) are the supported JS SDK surface with no corresponding -`manifest.yaml` field — see [Authoring a plugin](plugins-authoring.md). +`manifest.yaml` field, see [Authoring a plugin](plugins-authoring.md). ## Managed vs. legacy manifests Setting `runtime.type: binary` makes a manifest **runtime-managed**: kandev spawns and supervises the declared executable itself. A managed manifest must **not** set `base_url` or an `endpoints` block (`health`/`events`/ -`webhooks` paths on a remote service) — those describe the old +`webhooks` paths on a remote service); those describe the old remote/operator-hosted tier, and validation rejects a managed manifest that sets them. A manifest with an empty `runtime.type` still _parses_ as a legacy remote manifest (with `base_url`/`endpoints` instead) and passes -`manifest.Validate()` on its own — but the **installer** rejects it: `pkgtar +`manifest.Validate()` on its own, but the **installer** rejects it: `pkgtar .Install` requires `manifest.IsManaged()` to be true (`runtime.type: binary`), so a legacy manifest can never actually be installed via `POST /api/plugins/install` or a filesystem sideload. The remote tier is @@ -176,7 +176,7 @@ ADR 0047): each entry must be one of `tasks`, `sessions`, `messages`, `workspaces`, `workflows`, `agent_profiles`, `repositories`. Declaring a resource grants the matching `Host` accessor (`Tasks()`, `Sessions()`, `Messages()`, `Workspaces()`, `Workflows()`, `AgentProfiles()`, -`Repositories()` — see [Authoring a plugin](plugins-authoring.md)); calling an +`Repositories()`, see [Authoring a plugin](plugins-authoring.md)); calling an accessor for an undeclared resource returns gRPC `PermissionDenied`. `capabilities.api_write` gates the current Host data writes: `tasks` gates `Tasks().Create`/`Update`, and `messages` gates `Messages().Send`. A plugin may @@ -187,18 +187,18 @@ write without the matching declaration returns gRPC `PermissionDenied`. one user/agent message per row (`id`, `session_id`, `task_id`, `turn_id`, `author_type`, `content`, `type`, `created_at`), filterable by session ids, task ids, a `created_at` time range (`since` inclusive / `until` exclusive, -RFC3339), and message `types`. Content is sanitized — kandev-injected +RFC3339), and message `types`. Content is sanitized; kandev-injected `` blocks are stripped, exactly like the `message.added` bus event, so raw system prompts are never exposed. `author_type` is `user` or `agent` (there is no `system` author). ## Config schema validation and secret fields -`config_schema` is not an arbitrary, purely descriptive JSON Schema — kandev +`config_schema` is not an arbitrary, purely descriptive JSON Schema; kandev validates submitted config against a specific subset of it before persisting: -- `required` (an array of property names) is enforced — a `PATCH` missing a +- `required` (an array of property names) is enforced; a `PATCH` missing a required property is rejected. - `type` (`string`, `boolean`, `number`, or `integer`) is checked against the submitted value. @@ -209,7 +209,7 @@ persisting: when the plugin must always have a selection; optional fields include a **Not set** choice. - A property with `secret: true`, or `format: "password"`, is treated as a - **secret field** and must be `type: string` (or untyped) — a non-string + **secret field** and must be `type: string` (or untyped); a non-string secret is rejected. Secret values are moved into kandev's encrypted vault; `GET /api/plugins/{id}/config` returns the literal mask `"********"` in their place, and resubmitting that mask unchanged is treated as "keep the @@ -219,7 +219,7 @@ persisting: backend validation effect. The plugin process itself always sees real, unmasked values (secrets -included) via the `GetConfig` Host RPC — masking only applies to the +included) via the `GetConfig` Host RPC; masking only applies to the operator-facing API/UI. ## Event subscription vocabulary @@ -231,18 +231,18 @@ one subject segment; every other segment must match literally; and the **pattern and subject must have the same number of dot-separated segments** to match at all. `task.*` matches `task.created` (2 segments each) but does **not** match a three-segment subject such as `shell.output.` -(`shell.*` would not match it either — 2 vs. 3 segments). +(`shell.*` would not match it either; 2 vs. 3 segments). Any subject kandev publishes on its internal event bus is a valid -subscription target — this is not a closed list scoped to any one feature +subscription target; this is not a closed list scoped to any one feature area. The table below groups every subject defined in `internal/events/types.go` by domain (some are further suffixed per-session, -e.g. `shell.output.`, `git.event.` — subscribe to the +e.g. `shell.output.`, `git.event.`; subscribe to the literal wildcard segment count that matches, e.g. `shell.output.*`). A delivered event's `event_type` (`pluginsdk.Event.EventType`) is the **concrete subject it was published on**, i.e. the same string your pattern -matched — so a plugin subscribed to `shell.output.*` reads the session id off +matched, so a plugin subscribed to `shell.output.*` reads the session id off `event_type` as its last segment. For an unsuffixed subject that string is just the subject itself (`task.created`). @@ -278,7 +278,7 @@ just the subject itself (`task.created`). | Linear | `linear.new_issue` | | Sentry | `sentry.new_issue` | | Office (autonomous agents) | `office.agent.created`, `office.agent.updated`, `office.agent.status_changed`, `office.skill.created`, `office.skill.updated`, `office.project.created`, `office.project.updated`, `office.approval.created`, `office.approval.resolved`, `office.comment.created`, `office.cost.recorded`, `office.run.queued`, `office.run.processed`, `office.run.event_appended` (per-run), `office.routine.triggered`, `office.inbox.item`, `office.task.status_changed`, `office.task.updated`, `office.task.decision_recorded`, `office.task.review_requested`, `office.provider.health_changed`, `office.route_attempt.appended`, `office.routing.settings_updated` | -| Cross-plugin | `plugin..` — published by `Host.EmitEvent`; subscribe with `plugin..*` to react to another plugin's events. | +| Cross-plugin | `plugin..`; published by `Host.EmitEvent`; subscribe with `plugin..*` to react to another plugin's events. | > Event list current as of 2026-07-16; regenerate from > `apps/backend/internal/events/types.go` if this page drifts from the code. @@ -287,7 +287,7 @@ just the subject itself (`task.created`). Once installed, kandev writes several fields onto the stored record alongside the parsed manifest. These are **kandev-owned runtime state, not -author-supplied manifest fields** — do not include them in `manifest.yaml`; +author-supplied manifest fields**: do not include them in `manifest.yaml`; they have no effect there and are overwritten on install: | Field | Meaning | diff --git a/docs/public/plugins-marketplace.md b/docs/public/plugins-marketplace.md index b0c4952040..73211b2f93 100644 --- a/docs/public/plugins-marketplace.md +++ b/docs/public/plugins-marketplace.md @@ -7,7 +7,7 @@ status: experimental # Plugin Marketplace The marketplace is a discoverable, curated catalog of kandev plugins you can -browse and install from inside the app — no tarball URL required. It is +browse and install from inside the app, no tarball URL required. It is assembled from one or more **sources**: kandev ships with the official source enabled by default, and you can add team or corporate sources alongside it. @@ -18,7 +18,7 @@ pipeline, enable/disable, and security posture work, see plugin](plugins-authoring.md). Like the rest of plugins, the marketplace ships in the base product with no -feature flag to turn on — **Settings > Plugins** is always available in the +feature flag to turn on; **Settings > Plugins** is always available in the sidebar. ## Quick path @@ -30,16 +30,16 @@ sidebar. > **Sideloading still works.** The marketplace only adds *discovery*. Installing > a plugin by URL or by uploading a `.tar.gz` is unchanged and always available, -> even with every source disabled or offline — see +> even with every source disabled or offline, see > [Plugins → Installing a plugin](plugins.md#installing-a-plugin). ## Using the marketplace **Settings > Plugins** has two tabs: -- **Installed** — the plugins on this instance, with Enable / Disable / +- **Installed**: the plugins on this instance, with Enable / Disable / Uninstall and an **Update** button when a newer version is available. -- **Browse** — the merged catalog across all enabled sources. +- **Browse**: the merged catalog across all enabled sources. ### Browse and install @@ -47,19 +47,19 @@ Open **Settings > Plugins > Browse**. Each plugin shows as a card with its name, description, author, categories, source repository link, latest version, and GitHub star count. To narrow the list: -- **Search** — type in the search box to match plugin name or description. -- **Category** — filter to a single category with the category dropdown. -- **Sort** — **Most stars** (default), **Recently updated** (by latest release +- **Search**: type in the search box to match plugin name or description. +- **Category**: filter to a single category with the category dropdown. +- **Sort**: **Most stars** (default), **Recently updated** (by latest release / repo activity), or **Name**. Stars are a **sort hint, not a quality score**. kandev collects no download or -usage telemetry, so there is no "most installed" metric — ranking is GitHub +usage telemetry, so there is no "most installed" metric; ranking is GitHub stars only, and "Recently updated" surfaces new or actively maintained plugins that high-star incumbents would otherwise bury. Click **Install** on a card to install it. This resolves the entry to its latest release tarball and runs the same verified install pipeline as -install-by-URL (`POST /api/plugins/install`) — see [Plugins → Installing a +install-by-URL (`POST /api/plugins/install`), see [Plugins → Installing a plugin](plugins.md#installing-a-plugin) for the exact steps and integrity checks. A card for a plugin you already have at the latest version shows **Installed** instead of an install button. @@ -75,7 +75,7 @@ installed, the **Installed** tab shows an **Update to v``** button on that plugin's row. Clicking it reinstalls the newer tarball through the normal pipeline; Enable, Disable, and Uninstall are unchanged. -Updates are never automatic — kandev surfaces the newer version and waits for +Updates are never automatic; kandev surfaces the newer version and waits for an explicit click. There are no update channels and no background auto-update. ### Add a team or corporate source @@ -100,7 +100,7 @@ Notes: configured source wins** (the official source is always first); later duplicates are hidden. - A source that is unreachable or serves malformed JSON is reported as - **degraded** — its entries are omitted, but the healthy sources still load. + **degraded**: its entries are omitted, but the healthy sources still load. - Configured sources persist across restarts; the fetched catalog itself is a short-lived cache and is re-fetched on demand. @@ -123,23 +123,23 @@ Package your plugin as usual (see [Authoring a plugin → Packaging](plugins-authoring.md#packaging)) and cut a GitHub **Release**. One asset is required; a second is optional: -- `-.tar.gz` (**required**) — the plugin package. It carries its +- `-.tar.gz` (**required**); the plugin package. It carries its own internal `checksums.txt` covering every packaged file, which the install pipeline verifies on extraction. -- `checksums.txt` (optional) — the sha256 of the tarball itself. Advisory +- `checksums.txt` (optional); the sha256 of the tarball itself. Advisory provenance: the catalog reserves a `package_sha256` field for it, but the index builder does not populate or enforce the digest yet, so it is included only for forward compatibility. The release must pass the standard package integrity gate. The [`kdlbs/kandev-plugin-template`](https://github.com/kdlbs/kandev-plugin-template) -starter repo is the recommended way to bootstrap a repo with the right layout — + starter repo is the recommended way to bootstrap a repo with the right layout. its `.github/workflows/release.yml` produces both assets automatically when you push a version tag. ### 2. Add an icon (optional) -Add an `icon:` field to your `manifest.yaml` — a **package-relative path** to an +Add an `icon:` field to your `manifest.yaml`; a **package-relative path** to an image your package ships (for example `icon.svg`). The catalog resolves it to your icon and renders it on the plugin card; without it, the card shows a letter tile. See the [Plugin manifest reference](plugins-manifest.md#field-reference). @@ -164,18 +164,18 @@ a PR that lists it. release package `-.tar.gz` to match, so the index resolves the right asset. `categories` here are free-form curation tags for catalog filtering (not the manifest's category enum). `featured` is a maintainer-only - pin — leave it out of submissions. The pointer-list shape is defined by + pin; leave it out of submissions. The pointer-list shape is defined by [`plugin-registry/schema.json`](https://github.com/kdlbs/kandev/blob/main/plugin-registry/schema.json). 2. Open a pull request. The registry index-build workflow runs on your PR (build + tests, no Pages deploy), resolving your entry against the GitHub API - — your repo must have a latest release that publishes a `.tar.gz` package + Your repo must have a latest release that publishes a `.tar.gz` package asset (an accompanying `checksums.txt` asset is optional). An entry whose repo has no release or no package asset is skipped. -3. A maintainer reviews and merges — maintainer approval is what gates the +3. A maintainer reviews and merges; maintainer approval is what gates the official catalog. The index-build workflow then picks up your entry and your plugin appears in the in-app catalog on the next build. -Ranking in the catalog is **GitHub stars only** — there is no download or usage +Ranking in the catalog is **GitHub stars only**: there is no download or usage telemetry to game. Full submission details are in [`plugin-registry/README.md`](https://github.com/kdlbs/kandev/blob/main/plugin-registry/README.md). @@ -191,11 +191,11 @@ Any URL that serves an `index.json` document of the catalog shape can be added as a marketplace source (see [Add a team or corporate source](#add-a-team-or-corporate-source)), and its plugins merge into the Browse tab alongside the official ones. This is the recommended path for a team or -corporate registry — no PR to the main repo. +corporate registry, no PR to the main repo. The `index.json` document is the fetch contract between a source and kandev. The simplest way to produce one is to copy the official registry's -`plugin-registry/` directory into your own repo — its `plugins.yaml` + +`plugin-registry/` directory into your own repo; its `plugins.yaml` + [`build-index.mjs`](https://github.com/kdlbs/kandev/blob/main/plugin-registry/build-index.mjs) build script (zero-dependency Node) + GitHub Action resolve each listed repo's latest release into a full catalog record and publish the generated diff --git a/docs/public/plugins.md b/docs/public/plugins.md index b5f39d65d6..b3cc08d56b 100644 --- a/docs/public/plugins.md +++ b/docs/public/plugins.md @@ -15,11 +15,11 @@ from the in-app catalog, see the [Plugin marketplace](plugins-marketplace.md). For building a plugin, see [Authoring a plugin](plugins-authoring.md). For the manifest schema, see [Plugin manifest reference](plugins-manifest.md). -Plugins are an operator-level, instance-wide capability — there is no +Plugins are an operator-level, instance-wide capability, there is no per-user plugin access. They ship in the base product with no feature flag to turn on: **Settings > Plugins** is always available in the sidebar. Because loaded plugin code runs with backend privileges, install only plugins you -trust — see [Security posture](#security-posture). +trust, see [Security posture](#security-posture). ## Quick path @@ -36,19 +36,19 @@ Kandev owns the whole process lifecycle: it extracts the package, spawns the binary, completes the go-plugin handshake, health-checks it (`Ping` every 30s), and restarts it on crash or repeated health-check failure (backoff, max 5 attempts). There is no separate operator-managed plugin process to run -or babysit — install a package and kandev does the rest. +or babysit; install a package and kandev does the rest. A native UI bundle can register a nav item that renders as a top-level sidebar entry or, when it declares itself part of the Integrations section, alongside kandev's first-party integration links in the main sidebar's -**Integrations** section — expect new entries to appear there once such a +**Integrations** section: expect new entries to appear there once such a plugin is installed and active. Bundles can also inject components into host-defined slots, including **icon buttons in the chat composer toolbar** (beside the model picker, mic, and send button), so an active plugin can add its own action right where you message an agent. A bundle can also declare **keybindings** (user-overridable at **Settings > Keyboard Shortcuts**, with core shortcuts always winning on a conflict) and open host-owned **modal -windows** from anywhere in its code — see [Authoring a +windows** from anywhere in its code: see [Authoring a plugin](plugins-authoring.md) for both. ### Global Status contributions @@ -82,12 +82,12 @@ Full-bleed routes that opt out of host topbar chrome own their Status trigger. ## Installing a plugin -The easiest way to install is from the in-app catalog — **Settings > Plugins > +The easiest way to install is from the in-app catalog: **Settings > Plugins > Browse**, then **Install** on a card (see the [Plugin marketplace](plugins-marketplace.md)). To install a plugin that is not in a configured catalog, open **Settings > Plugins** and click **Install plugin**. You can install from a URL (kandev downloads the tarball) or by uploading a -`.tar.gz` file directly. No credentials are ever shown or copied — installing a +`.tar.gz` file directly. No credentials are ever shown or copied; installing a plugin has nothing to reveal, unlike a webhook-secret/API-key registration flow. @@ -111,7 +111,7 @@ Either path runs the same pipeline: 1. Verify `checksums.txt` covers every other file in the tarball and every hash matches (always enforced). 2. Check for `checksums.txt.sig`. Signature verification is not currently - wired up, so every package — signed or not — installs and is reported + wired up, so every package, signed or not, installs and is reported as unsigned today (see "Signed vs. unsigned packages" below). 3. Parse and validate `manifest.yaml` **before any code runs**: schema, `id` pattern, the `categories` and UI-surface enums, and that @@ -128,7 +128,7 @@ Either path runs the same pipeline: is not stored verbatim. A successful install that failed to spawn returns HTTP 201 with a -`warning` field rather than failing outright — the package is installed, +`warning` field rather than failing outright. The package is installed, just not yet running. Once installed, the plugin appears in the list with its category, a status @@ -148,19 +148,19 @@ through the install endpoint. The **Sync** button in Settings > Plugins (and `POST /api/plugins/sync`) reconciles kandev's registry with what is actually on disk: -- **A dropped directory** — `~/.kandev/plugins///manifest.yaml` - placed manually with no existing record — is validated and registered +- **A dropped directory** (`~/.kandev/plugins///manifest.yaml`) + placed manually with no existing record is validated and registered with status **`disabled`**, never auto-enabled. Directory sideloads skip the checksum/integrity gate the URL/upload pipeline runs, so an operator must explicitly inspect and enable one. If more than one version directory exists for the same unregistered id, the lexically greatest version is registered and the others are reported as skipped. -- **A dropped tarball** — any `*.tar.gz` sitting directly in - `~/.kandev/plugins/` — is run through the same verified install pipeline +- **A dropped tarball** (any `*.tar.gz` sitting directly in + `~/.kandev/plugins/`) is run through the same verified install pipeline `POST /api/plugins/install` uses. On success the tarball file is deleted; on failure it is left in place and the failure is reported. -- **A missing install** — a registered record whose `install_path` no - longer exists on disk — is stopped (if running) and marked `error`. +- **A missing install** (a registered record whose `install_path` no + longer exists on disk) is stopped (if running) and marked `error`. At boot, kandev runs only the directory-sideload and missing-install steps (never the tarball-install step), as part of resuming plugins that were @@ -180,7 +180,7 @@ binary an operator hasn't explicitly approved via install or Sync. so the row/detail immediately shows the replacement reason and keeps the plugin in `error`. - **Uninstall** stops the subprocess and deletes the plugin's package, - registration record, and all persisted state — there is no grace period. + registration record, and all persisted state; there is no grace period. When a plugin is in `error`, its declared events remain buffered in the bounded 100-event/5-minute ring buffer. If that buffer overflows, kandev drops the @@ -194,7 +194,7 @@ A plugin can declare `config_schema` in its manifest, generating a settings form at **Settings > Plugins > ``** (also `GET /api/plugins/{id}/config` and `PATCH /api/plugins/{id}`). Fields marked `secret: true` or `format: "password"` (for example a GitHub PAT) are never returned in -cleartext to the operator UI — reads show a masked placeholder, and +cleartext to the operator UI. Reads show a masked placeholder, and resubmitting the form unchanged leaves the stored secret alone. The plugin process itself receives the real values via the `GetConfig` Host RPC. Saving config **restarts the running plugin** so it re-reads its config. @@ -203,8 +203,8 @@ references rather than cleartext for secret fields. ![A plugin's settings page: a schema-driven form with a masked API-token field, a toggle, and a text field, above a Manifest card showing the plugin's id, version, signing status, and capabilities.](../screenshots/plugin-settings-page.png) -A plugin can also render its own UI inline on this page — at the top, above the -settings form — via the `plugin-settings` slot, for example a live +A plugin can also render its own UI inline on this page, at the top, above the +settings form, via the `plugin-settings` slot, for example a live integration-health card ("CLI installed ✅ v0.45.2", "API token ✅ authenticated"). This is owner-scoped, so a plugin's card only ever appears on its own settings page. See [supported named @@ -215,11 +215,11 @@ slots](plugins-authoring.md#supported-named-slots) in the authoring guide. ## Signed vs. unsigned packages -Every package's `checksums.txt` is verified at install time — this integrity +Every package's `checksums.txt` is verified at install time: this integrity gate is always enforced. Signing (`checksums.txt.sig`, an ed25519 signature over `checksums.txt`) is a separate, optional layer, and its verification hook is not currently wired up in the shipped product: no signature is -cryptographically checked today, so every install — signed tarball or not — +cryptographically checked today, so every install, signed tarball or not, is currently treated and reported as unsigned. A signed package installs identically to an unsigned one; signing is not required in v1. @@ -234,7 +234,7 @@ identically to an unsigned one; signing is not required in v1. │ ├── manifest.yaml │ ├── server/plugin--[.exe] │ └── ui/bundle.js # optional - └── data/ # KANDEV_PLUGIN_DATA_DIR — shared across versions + └── data/ # KANDEV_PLUGIN_DATA_DIR; shared across versions ```
@@ -247,8 +247,8 @@ recorded and are cleared after successful recovery. ## Security posture - **Auth is the spawn relationship.** Kandev spawns the plugin subprocess - itself over a unix domain socket (macOS/Linux) or loopback TCP (Windows) — - never a routable network address — and secures it in both cases with the + itself over a unix domain socket (macOS/Linux) or loopback TCP (Windows), + never a routable network address, and secures it in both cases with the go-plugin handshake plus AutoMTLS. There is no `api_key`, `webhook_secret`, or HMAC signing anywhere in the contract. - **Capability-based access control.** A plugin can only call the Host RPCs @@ -259,7 +259,7 @@ recorded and are cleared after successful recovery. implemented task/message Host writes. An undeclared capability returns gRPC `PermissionDenied` with a message naming the missing capability, checked by a server interceptor before the handler runs. `GetConfig` and `EmitEvent` - are the only ungated RPCs — a plugin can always read its own config + are the only ungated RPCs: a plugin can always read its own config (secrets included) and emit events. - **Native UI plugins run in-origin with full app-store access.** This is an accepted tradeoff, not an oversight: a plugin bundle shares the kandev @@ -267,14 +267,14 @@ recorded and are cleared after successful recovery. first-party pages. In v1 only **active, operator-installed** plugins load; a failing bundle or `initialize` is caught and never breaks boot; slot components render behind error boundaries. Hard sandboxing (a worker or - realm boundary) is explicit future work — see below. + realm boundary) is explicit future work: see below. - **Package integrity is always checked; signing is optional.** See "Signed vs. unsigned packages" above. - **Curated marketplace, no auto-install.** The [Plugin marketplace](plugins-marketplace.md) adds one-click install from a catalog, but the official source is PR-curated (a plugin appears only after a maintainer approves it), install is always an explicit operator action, and - updates require an explicit click — there is no automatic discovery or + updates require an explicit click: there is no automatic discovery or background install. kandev collects no download or usage telemetry. Related: [Authoring a plugin](plugins-authoring.md), [Plugin manifest diff --git a/docs/public/run-as-a-service.md b/docs/public/run-as-a-service.md index 1901527a7e..b5f4e203c7 100644 --- a/docs/public/run-as-a-service.md +++ b/docs/public/run-as-a-service.md @@ -13,7 +13,7 @@ Stable is the default release channel. A verified Kandev-managed npm/npx user se the npm Nightly channel from **Settings → System → Updates**. Desktop, Homebrew, and system services remain Stable-only. -> **Network security:** the backend listens on `0.0.0.0` by default and ships with authentication **disabled**. Before allowing remote access, enable [opt-in authentication](authentication.md) (the **Authentication & users** feature toggle, or `KANDEV_FEATURES_AUTH=true`) and terminate TLS in a reverse proxy — authentication does not replace HTTPS. A server bound to non-loopback interfaces without authentication logs a startup warning. See [server configuration](configuration.md#root-and-server). +> **Network security:** the backend listens on `0.0.0.0` by default and ships with authentication **disabled**. Before allowing remote access, enable [opt-in authentication](authentication.md) (the **Authentication & users** feature toggle, or `KANDEV_FEATURES_AUTH=true`) and terminate TLS in a reverse proxy; authentication does not replace HTTPS. A server bound to non-loopback interfaces without authentication logs a startup warning. See [server configuration](configuration.md#root-and-server). ## Quick path @@ -158,7 +158,7 @@ On macOS, installation writes the plist, removes an already loaded job, bootstra If the target unit/plist already exists and lacks Kandev's managed marker, installation saves it as `.bak` before replacing it. Review that backup; uninstall does not restore or remove it. -The service manager starts the control plane only. Agents still need their configured executor dependencies and credentials—for example Docker access for a Docker profile or network reachability and keys for SSH. See [Executors](executors.md). +The service manager starts the control plane only. Agents still need their configured executor dependencies and credentials, for example Docker access for a Docker profile or network reachability and keys for SSH. See [Executors](executors.md). ## Operate and inspect diff --git a/docs/public/sessions-and-review.md b/docs/public/sessions-and-review.md index 082ca933d2..8a43394715 100644 --- a/docs/public/sessions-and-review.md +++ b/docs/public/sessions-and-review.md @@ -54,7 +54,7 @@ Right-click an agent tab on desktop to manage it. Available actions depend on it | **Set as Primary** | Makes a stoppable session the task's primary target | | **Stop** | Cancels the active agent turn for this session | | **Resume** | Attempts to continue a completed, failed, or cancelled session | -| **Delete** | Permanently removes the conversation; if it was primary, another session is promoted when possible. The task workspace and its files are kept — a later session reuses them | +| **Delete** | Permanently removes the conversation; if it was primary, another session is promoted when possible. The task workspace and its files are kept; a later session reuses them | | **Share** | Opens the publishing preview for an eligible session | | **Handoff** | Starts another session with a generated summary of this conversation | | **Close Others** | Closes other visible agent panels without deleting their sessions | @@ -115,7 +115,7 @@ direct shortcut. Open **Settings > General > Layouts** to configure reusable desktop workbench profiles. Select a tab in a built-in layout to reveal its nearby edit controls, arrange or remove tabs and splits, then use the floating **Save changes** control. Kandev keeps the built-in row visible, marks it **Customized**, and stores your override without requiring a duplicate. Choose **Reset** beside a customized built-in to restore its original definition. -**PR Details** is a reusable Layouts panel whose visibility follows the active task's review association. Without a linked GitHub pull request or GitLab merge request, the tab stays hidden—even when the selected layout includes it. Once a review is linked, Kandev adds PR Details as an inactive tab: beside **Agent** for the built-in Default, or in the group and tab position you configured in the Layouts editor. Closing that tab prevents it from reappearing automatically in the same session. Changing the default applies to task environments without a saved task-specific layout and **Reset Layout**, not a layout already saved for a task. Removing Terminal from the Default layout also prevents Kandev from creating its initial user shell. +**PR Details** is a reusable Layouts panel whose visibility follows the active task's review association. Without a linked GitHub pull request or GitLab merge request, the tab stays hidden, even when the selected layout includes it. Once a review is linked, Kandev adds PR Details as an inactive tab: beside **Agent** for the built-in Default, or in the group and tab position you configured in the Layouts editor. Closing that tab prevents it from reappearing automatically in the same session. Changing the default applies to task environments without a saved task-specific layout and **Reset Layout**, not a layout already saved for a task. Removing Terminal from the Default layout also prevents Kandev from creating its initial user shell. All panels for a task point at the same task environment. In a multi-repository task, check the repository label before editing, committing, or reviewing. A preview also requires the application to listen on a reachable interface and expose a forwarded port. @@ -167,7 +167,7 @@ Review compares each submodule with the gitlink commit recorded by its parent an When a task has multiple linked pull requests, use the PR selector in the Changes diff header or Review toolbar to inspect one PR revision at a time. The selection is scoped to that task for the current app session. Switching PRs replaces only the remote PR contribution; uncommitted and committed sources keep their normal precedence. Selecting a file from a specific PR row opens that exact PR revision, even when a sibling PR changes the same path. -When several pull requests are linked to a task, hover the PR control in the desktop top bar—or tap the PR status chip on mobile—to open the tabbed CI surface. Each PR tab has a **Remove from task** button. Removing a tab only detaches that Kandev task association; it does not close or modify the GitHub pull request, its branch or commits, the task repositories, or sibling PR associations. Explicitly linking that PR again restores the association. +When several pull requests are linked to a task, hover the PR control in the desktop top bar or tap the PR status chip on mobile to open the tabbed CI surface. Each PR tab has a **Remove from task** button. Removing a tab only detaches that Kandev task association; it does not close or modify the GitHub pull request, its branch or commits, the task repositories, or sibling PR associations. Explicitly linking that PR again restores the association.