Skip to content

In-app AI assistant chat ​

A chat panel embedded in the dashboard, backed by the control plane's own internal/ai engine: it reads your apps, deploys, logs, metrics, and diagnostics through the same tool surface the MCP server exposes, and it can propose actions, but every action that would change anything always pauses for an explicit confirmation click first. This is the same AI non-goal the project plan states in CLAUDE.md section 2: "AI is a read-and-suggest layer on top of the API, nothing more." It is not a different implementation of that rule, it is the same engine and the same confirmation gate docs/ai-assistant.md describes for levelrail-mcp, surfaced as a first-party dashboard panel and CLI command instead of (or alongside) an external MCP client.

This is a separate feature from levelrail-mcp. Read docs/ai-assistant.md if you want to point an external MCP-compatible assistant (Claude Desktop, Claude Code, or your own) at this control plane instead of, or in addition to, the chat described here.

Relevant packages: internal/ai, internal/api/ai_chat.go, internal/api/ai_settings.go, web/src/components/AiChatPanel.tsx, cmd/levelrail-cli/ai.go.

Enabling it ​

Two independent switches, both required:

  1. The experimental flag. The in-app chat is gated behind ai-chat (see docs/experimental-features.md):

    APP_EXPERIMENTAL=ai-chat

    Off (the default), every route under /api/v1/ai/sessions and /api/v1/settings/ai-assistant answers 404, the dashboard hides the AI assistant nav entry and the /ai-assistant page, and the CLI hides ai chat/ai sessions from --help and completion.

  2. A BYOK (bring your own key) provider, model, and API key. The flag alone is not enough; nothing runs until a key is configured too:

    bash
    levelrail-cli settings ai-assistant set --model claude-sonnet-4-5 --api-key sk-ant-...

    Only anthropic is supported server-side today (store.AIProviderAnthropic); the key is stored via the same envelope encryption every other secret in this platform uses (section 4.10 of CLAUDE.md) and is never returned or logged. levelrail-cli settings ai-assistant get reports only whether one is configured, never the key itself. The dashboard's Settings → AI assistant page does the same thing through a form.

With both set, the dashboard's AI assistant page (and command palette entry) appears, and levelrail-cli ai chat stops returning the experimental-disabled error.

What it can do ​

  • Read, always. Logs, metrics, deploy history, node status, certificates, and crashloop diagnosis, through the same read-only MCP tool classification docs/ai-assistant.md's "Modes and toolsets" section describes. These run automatically, with no confirmation, the moment the model asks for them.
  • Propose, never execute directly. A mutating tool call (deploy, rollback, restart, an env change, and so on) is recorded as a pending confirmation and streamed to the UI as a tool_call_proposed event. It does not run until a human clicks Approve (dashboard) or runs levelrail-cli ai sessions resolve <session> <confirmation-id> --approve (CLI). Rejecting it is just as explicit a path (--reject), and the model sees "User declined to run this action" as the result either way, so it can explain and move on rather than retry blindly.
  • Never in the reconciliation path. This bounds what the feature is allowed to become, not just what it does today: the assistant only ever calls the platform's own versioned REST API (internal/apiclient), the exact same path any external MCP client or the CLI already uses. It has no privileged internal entry point into the reconciler, the Docker client, or the database, matching the project plan's non-goal in section 2 ("No AI in the reconciliation path") and the architecture note in section 4.11.

Untrusted text handling (log content, deploy output, commit messages) and the confirmation gate's exact rules (what counts as "tainted," which annotations make a tool auto-run) are the same mechanism docs/ai-assistant.md's "Untrusted text and the assistant's confirmation gate" section documents in detail; this page doesn't repeat it.

Using it ​

Dashboard ​

AI assistant in the global nav (hidden unless the flag is on) opens a chat panel: a message composer, a transcript with the assistant's replies rendered as markdown (code blocks, lists, links; raw HTML and images are stripped, since a reply can echo untrusted tool output), and a confirmation card with Approve/Reject buttons for any pending mutating tool call. History lists past sessions so you can resume or delete one; New chat starts a fresh one.

CLI ​

bash
# One-shot: start a session, send a message, stream the reply to stdout.
levelrail-cli ai chat "why did the last deploy of web fail?"

# Continue the same conversation (the first run printed its session id to stderr).
levelrail-cli ai chat "roll it back then" --session aisess_abc123

# A mutating call proposed above pauses; approve or reject it explicitly.
levelrail-cli ai sessions resolve aisess_abc123 aiconf_xyz789 --approve

# Manage sessions.
levelrail-cli ai sessions list
levelrail-cli ai sessions get aisess_abc123
levelrail-cli ai sessions delete aisess_abc123

--json on any ai subcommand switches to one JSON object per stream event (or the plain resource for sessions list/get/delete), the same --json/--output/--query convention every other levelrail-cli command follows.

REST API ​

POST /api/v1/ai/sessions, GET /api/v1/ai/sessions, GET/DELETE /api/v1/ai/sessions/{id}, POST /api/v1/ai/sessions/{id}/messages (Server-Sent Events: text_delta, tool_call_proposed, tool_result, done), and POST /api/v1/ai/sessions/{id}/confirmations/{confirmation_id} (same SSE framing, for resolving a pending call). All of it requires AbilityRoot: a confirmed message can execute any mutating tool the platform exposes once approved, the same blast radius a root-scoped token would need to reach those actions directly, so this is not a surface to hand a lower-privileged token.

Scope and limits ​

  • Single admin user, no per-user session isolation beyond the existing root-only gate. Anyone who can authenticate as root can see and act on any session.
  • One provider. Anthropic only; no model routing or fallback.
  • No reconciler access. Worth repeating: this is architecturally the same constraint as the MCP server, not a weaker one. See "What it can do" above.
  • Not yet covered by real-infrastructure testing. test/e2e/ai_chat_test.go proves the full session lifecycle (create, send a message with an auto-executed read tool call, get, list, delete, and the flag-off 404) over real HTTP against a real router and a real ai.Engine, but with a scripted fake ai.Provider, never a live call to Anthropic's API. See docs/feature-status.md for the current evidence tier.

See also ​

  • ai-assistant.md: levelrail-mcp, the standalone MCP server for an external AI client, and the confirmation/untrusted-text mechanism this chat shares with it.
  • experimental-features.md: the APP_EXPERIMENTAL switch and what "off" means everywhere.
  • identity-and-access.md: API tokens and abilities (ai CLI commands and /api/v1/ai/... routes need root).
  • cli-reference.md: full flag reference for ai chat and ai sessions.

Released under the Apache 2.0 License.