Configure

Channels, models and group policy

Establish a working model connection first, then assign model roles and platform channels, and finally narrow triggers, hours, users and tool boundaries per group.

Platforms

Eight platforms can be online at once

Every enabled bot profile runs in parallel, and replies always return to the channel they came from. Credentials are entered per platform on the Bots page; leaving a secret field blank keeps the stored value, and filling it in replaces it.

How messages arrive decides your deployment

What actually shapes deployment is not the platform itself but how messages reach you. The first four connect outbound from Diana, so a home connection or an internal network works as-is. Feishu and WeCom can only POST events to you, which means a publicly reachable HTTPS address.

PlatformHow it connectsPublic address
OneBot v11Reverse WebSocket, the adapter connects to DianaNo
TelegramBot API long polling, outbound from DianaNo
QQ official botOpen Platform WebSocket gateway, outbound from DianaNo
DingTalkStream mode connection, outbound from DianaNo
Feishu / LarkEvent subscription callback, pushed to DianaYes
WeComCustom-app callback, pushed to DianaYes
WeChatTencent iLink Bot QR login and long polling, outbound from DianaNo
iMessageBlueBubbles Server on a Mac pushes webhooks; optional polling fallbackNo, the Mac only needs to reach Diana

Credentials per platform

OneBot v11

Adapters
SnowLuma recommended
Connection
The adapter dials Diana's reverse WebSocket
Address
ws://HOST:18080/onebot/v11/ws
Features
Group lists, CQ codes, quotes, group files, local media

Telegram

Credential
Bot token from BotFather
Connection
Outbound long polling over the Bot API
Public
No webhook or public entry point needed
Network
Supports a proxy and a self-hosted Bot API

QQ official bot

Credentials
AppID and AppSecret from the Open Platform
Connection
Exchanges an access token, then dials the gateway
Sandbox
Available for bots that are not published yet
Capabilities
Receives text, images, video, voice (transcribed by the platform) and files, including media inside merged chat records. Sends text, images, video and voice
Scope
Group chats push only messages that mention the bot by default; the group owner can widen the bot's group message scope to all messages

DingTalk

Credentials
Client ID and Client Secret of the app
Connection
Stream mode; no HTTP callback to configure
Replies
Prefers the session webhook, which costs no quota
Scope
Only group messages that mention the bot

Feishu / Lark

Credentials
App ID and App Secret of the custom app
Verification
Verification Token from the event subscription page
Encryption
Encrypt Key, when encrypted delivery is enabled
Endpoint
Lark uses open.larksuite.com

WeCom

Credentials
Corp ID, AgentId and app Secret
Verification
Token and EncodingAESKey from "receive messages"
Required
Without both, it can send but not receive
Format
AgentId must be numeric

WeChat

Credentials
Nothing to type: save the bot, then scan the QR code under Connection; the credentials are written after you confirm
Connection
Tencent iLink Bot API (ilinkai.weixin.qq.com) long polling; the cursor and per-contact context_token live under weixin/ in the data directory, so a restart picks up where it left off
Capabilities
Private chats only. Receives text, images (downloaded from the CDN and decrypted), quoted text and server-side voice transcripts; files and videos arrive as placeholders. Sends text and images; video, voice and files are not supported yet
Proactive messages
Only to people who have messaged the bot first, because a reply must carry the context_token from their latest message. Reminders, owner notices and audit alerts to someone who never wrote fail with a clear error
Expiry
When the server reports an expired login (errcode -14), sending and receiving pause for an hour with the reason shown in the status; re-scan if it persists. Other rejections (risk control, rate limits) show the server error verbatim and back off

iMessage

Prerequisite
An always-on Mac signed in to an Apple ID, running BlueBubbles Server
Credentials
Server URL and password; the webhook token is generated on save
Callback
/api/channels/imessage/callback?token=…
Limits
Quoted replies need the Private API; no group moderation. See the iMessage setup guide (Chinese)

Callback URLs

Feishu and WeCom need the addresses below entered in their own consoles. HOST must be reachable from the platform's servers, over HTTPS.

https://HOST/api/channels/feishu/callback
https://HOST/api/channels/wecom/callback

With more than one bot on the same platform, append the profile id to tell them apart, for example /api/channels/feishu/callback/<profile-id>. With a single bot the short address is enough.

These paths bypass console login

Platform servers cannot carry your session, so each is verified by its own scheme instead: WeCom checks msg_signature, decrypts with WXBizMsgCrypt and confirms the corp id inside the payload; Feishu checks the Verification Token and, when an Encrypt Key is set, also verifies X-Lark-Signature before decrypting. The callback URL itself is public information and is not a credential, so do fill in the verification secrets.

Platform differences

  • Member-level thresholds only apply to OneBot v11. Other platforms have no such concept and the option is hidden for them.
  • Voice messages and mentions rely on OneBot CQ codes and degrade gracefully elsewhere: the text is still sent, but nobody is pinged.
  • Local media is fetched by the adapter from Diana's /media/resolver on OneBot; Telegram cannot reach a local address, so files are uploaded directly as multipart instead.
  • QQ official bots push only messages that mention the bot by default. Once the group owner sets the bot's group message scope to all messages under the group settings, unprompted participation works there too. DingTalk pushes only messages that mention the bot, so those features never trigger there.
  • Feishu, WeCom and DingTalk currently handle text only. Images and files arrive as download credentials that have to be exchanged separately, which is not wired up yet.

Context across channels

Each bot keeps its own platform, account, owner, persona and model assignments. Replies always go back to the source channel. Conversation context is always isolated by bot profile; matching group or user IDs never merge sessions. Message relays remain available. Cross-platform memory is a separate opt-in on both bots and only retrieves non-sensitive public group facts and summaries, not raw conversations or personal memories.

The legacy isolate_platform_contexts setting is ignored and its endpoint has been removed. Previously shared history is retained in its original namespace, without automatic reassignment to individual bots. New messages use each bot's isolated sessions.

Models

Sync the model list before setting a default

A provider holds the base URL, API key and protocol. The top of the page syncs the model catalogue from the server and the bottom picks a default; the bot configuration then binds each role to a specific model.

RoleUsed forHow to test it
ChatFinal replies and agent reasoningCheck phrasing, tool calls and long context.
VisionPhotos, screenshots, historical imagesSend a real image and check small-text OCR, UI structure and multi-image relations.
IntentUnprompted replies, suppression, relationship checksCheck latency, stable structured output and edge cases.
Image generationText-to-image and image editingIt must actually return an image — a text-only connectivity check is not enough.
On by default

"Detect other bots and stay quiet" is enabled by default to stop bots talking to each other in a loop. Turn it off only when you have a concrete reason.

OAuth sign-in

A profile's "Credential" setting can use OAuth instead of an API key. The console usually runs on a server and your browser is not necessarily on the same machine, so the callback does not have to land back locally: click "Sign in", finish the authorization in your own browser, then paste the whole callback URL from the address bar back in (just the code also works). The token is refreshed before it expires; if the profile also holds an API key, a failed refresh falls back to it rather than taking the whole profile down.

Providers are configuration, not code. OpenRouter ships built in; anything else goes under "Custom provider" with its authorize URL, token URL, client ID and scopes — suitable for a self-hosted gateway or any service not shipped here. Both URLs must be https; loopback addresses (127.0.0.1) may use http.

How the token is then presented to the model API is configuration too. The default Authorization: Bearer <token> suits almost every provider; not all of them, though — Anthropic answers an OAuth token in Authorization with a 401 "OAuth authentication is currently not supported." and wants the token in x-api-key with no prefix. So a custom provider can set the token header, the token prefix and a set of extra headers under "Advanced". When the token goes somewhere other than the default, the auth header the SDK writes itself is stripped so one request never carries two credentials.

The request body format of the token exchange is chosen under "Advanced" too. RFC 6749 §4.1.3 requires application/x-www-form-urlencoded at the token endpoint, so that is the default; JSON is a dialect some services speak instead, and the built-in OpenRouter only accepts JSON. Send the wrong one and the server usually answers with a bare "missing parameter" rather than saying the whole body failed to parse, which sends you looking at the client ID. Custom providers saved before this option existed keep sending JSON so a working setup does not suddenly stop exchanging tokens; newly added ones use the spec default.

Providers needing a first-party client ID are not preinstalled

That use misrepresents the client's identity to the authorization server and goes beyond what a consumer subscription itself licenses. Whether to do it is the operator's own call — it can be entered under "Custom provider".

Context and timeouts

llm:
  context_window_tokens: 65536
  max_context_tokens: 65536
  max_output_tokens: 2048
  timeout: 60000ms

Images consume vision tokens too. When vision quality is poor, first confirm that the role really is bound to a vision model, that the original image made it into the request, and that images were not dropped by count or token budget — before raising the detail level or the context ceiling.

Group policy

Configured per group, as actually joined

Group management syncs every group the bot has joined through the OneBot get_group_list call, not just the ones with local configuration. It falls back to the groups stored in SQLite only when the connection is unavailable.

Private chats are disabled by default, including ordinary messages and private notices from the owner. Enable all users, owner only or a whitelist under Bot → Behavior → Admission and limits when needed. Disabled messages are silently ignored without queueing, preprocessing or model calls; group chats and console login pairing are unaffected. Explicitly saved admission modes remain effective; missing or invalid modes disable private chats.

Enabled state

Stopping one group does not affect other groups or direct messages.

Triggers and unprompted replies

Explicit mentions, quotes, trigger words and semantic routing are evaluated in layers; an explicit mention outranks a quote.

Reply hours

Same-day and overnight windows with IANA time zones. This is group policy and never enters the model prompt.

Text and image input budget

After reserving output, safety overhead and other media, estimated text and image tokens each receive half the remaining budget and may borrow unused capacity. Requests that fit are unchanged. Text overages are first handled without a model call: the oldest chat history is dropped turn by turn (replaced by a short omission note), then older large Agent tool results are clipped to their head and tail, down to 85% of the text limit so the next few tool results still fit. Later Agent steps of the same reply reuse the same cut point, keeping the prompt prefix stable. System rules, the current question, the last three turns, memory and summaries, the two newest tool results and tool-call pairing are never touched. Only if the request still does not fit are the longest older messages summarized by a model, at most once per text per reply. Only image overages trigger numbered visual descriptions, which then count toward text usage. Failed descriptions retain the originals; image detail may be reduced only for image overages. If the request still cannot fit, images are dropped first and the remainder is left to the provider's context trim instead of failing the turn. Logged token counts are estimates, not provider billing usage.

Reply layout

Disabling multiple messages joins the reply into one message, compresses oversized content and disables automatic merged forwarding. Paragraph line breaks are configured independently: an unset value preserves existing behavior, while disabling them compacts ordinary prose without damaging lists, code, tables or quotations. Both options support group inheritance and explicit current-turn overrides without persisting those overrides. Models continue to use explicit message and line markers; raw newlines do not control layout.

Member admission

Blacklist (default) replies to everyone but the muted accounts; whitelist replies only to the listed accounts and sends nothing at all to the rest, not even the quiet-hours notice. The owner is exempt from the whitelist.

Muted accounts

The group list and the global list are a union: adding someone here never drops the global mute list. Level and hours, by contrast, are taken over wholesale by "set reply rules for this group".

Persona and context

Group-level prompts, history depth and per-message reply limits can override bot defaults. The existing max_reply_chars field and default of 3500 remain, but now apply per message on every platform, not to the combined length of all parts. Generation prompts include the limit. When splitting is allowed, complete paragraphs, sentences or sections are grouped first; only parts that remain oversized are compressed, leaving other parts unchanged. At most two compression calls are allowed per reply. Failure sends no partial result, and content is never hard-truncated. An explicit single-message request instead compresses the whole reply. Model output token budgets and delivery pacing still apply.

Tool overrides

Plugin settings and credentials are shared globally, with no sharing-mode switch. Each bot retains its own enabled state. Saving or resetting plugin settings affects all bots. Migration selects the first complete configured value set in saved bot order, keeps the other configurations as backups, and shows the source bot. Credentials from different accounts are never combined. Existing non-secret group overrides remain supported. RSS and GitHub subscriptions support multiple destinations, each selecting its own bot and group or private chat. Each subscription is checked once before delivery to its destinations; existing subscriptions are not automatically merged.

Platform delivery

QQ/OneBot merge thresholds package the parts into a forward card without shortening the content. Telegram ignores those thresholds and never automatically concatenates separate messages. It sends normal parts or one message according to the user's choice and validates rendered platform capacity. Oversized parts are compressed first; content such as code that cannot be safely reduced still fails rather than being corrupted. Other platforms never attempt OneBot forwarding.

Persona

A persona is one SOUL.md

Each bot's persona is a single Markdown document: who she is, what she cares about, how she talks, what she looks like. It is placed verbatim at the very front of the system prompt, with no wrapper. Edit it under Bots → Persona; a group can replace it wholesale in group management, or leave it empty to follow the bot.

Diana's way of writing it is to give reasons, not rules: rules always miss cases nobody foresaw, while a character who understands why can work out situations the document never mentions. Only the few things that are costly to get wrong become hard lines. Write about her in the owner's voice ("we"), say that her identity holds under pressure, and describe her appearance. Output format, message splitting, tools, time and which group she is in need not be written — the runtime adds those. See docs/soul-md.md in the repository for the full guide.

Persona library

The library is a source to apply from, not a live binding: picking one fills its text into the editor, and it takes effect when you save the bot. Editing the library entry later never silently changes a bot you already configured.

  • Six built-ins (Diana, human-like, catgirl, assistant, girlfriend, boyfriend) ship with the program and are read-only; apply one and save your own copy to change it.
  • Every entry exports as name.md; importing a .md takes the name from the first-line heading, or from the file name.
  • Import only adds: same name with different content becomes "Name (2)", same name with identical text is skipped; the library holds up to 50 entries.

Upgrading

Persona files are .md only; older JSON / YAML persona files no longer import. Nothing already stored is lost: on upgrade, the soul layer, self-reference, sentence-ending particles and action-description switch in bot configurations and in the library are rendered into the same text the runtime used to inject and folded into SOUL.md, so the model reads the same instructions — they are just visible and editable now. Prompt overrides and criteria that older library personas carried no longer travel with a persona and are dropped; each bot's own prompt overrides and criteria are kept as they are.

SillyTavern character cards

Import also accepts SillyTavern character cards: V1/V2/V3 JSON files and PNGs with an embedded card (the chara/ccv3 tEXt chunk). The card's description, personality, scenario, example dialogue and greeting are composed into one document ({{char}}/{{user}} macros are expanded in place; past the 4000-character budget, later sections are dropped whole). An embedded character_book is merged into the world book along the way.

Reply mode (how readily it joins a chat), refusal wording and model assignments are not part of the persona: they describe how this bot runs in a group, not who she is.

World and interaction

World book and human-like behaviors

The world book is the lore book of the world your bot lives in, maintained under Bots → Persona → World book. Entries are organised as chapters on a tree, and the path itself becomes context ("Zhijiang / Harbor: …"). Each node declares how it is injected: always-on entries ride along on every turn and describe the skeleton of the world; entries with trigger keywords only enter the context when recent conversation touches them, so details never waste tokens. Title-only nodes act as folders and inject nothing; disabling a chapter disables everything under it.

There is one book shared by all bots; each bot's own switch decides whether it uses it (on by default — an empty book injects nothing anyway). The whole book exports to JSON and imports back; parent-child links are rebuilt from the references inside the file, and like persona files, IDs are machine-local state and are reassigned on import.

Import is also compatible with SillyTavern world books: a world info file's top-level entries, or the character_book embedded in a character card, can be selected directly. Fields map by meaning — blue-light constant becomes always-on, green-light keys become trigger keywords, comment becomes the title (falling back to the first key or the start of the content), disable becomes disabled — sorted by order and placed at the root. Secondary keys (keysecondary, AND ANY logic) carry over too: the entry then requires a primary hit plus any secondary hit. Other secondary logics (NOT ANY etc.) invert the meaning, so those entries fall back to primary keys only. Recursive scanning, probability and insertion position have no counterpart here and are ignored; injection follows the local rules.

Human-like behaviors

Bots → Behavior → Human-like adds three independent switches, all off by default:

  • Mood — the bot's mood rises and falls with how it is treated: compliments make it cheerful (lighter tone, more banter), abuse makes it glum (quieter, subdued), and a few quiet hours bring it back to neutral. It reuses the per-message relationship evaluation, costs no extra model call, only affects tone, and resets on restart.
  • Style learning — about once a day per group, a background model reads recent member messages (senders replaced by letters) and writes a note of up to 300 characters on how that group talks: catchphrases and in-jokes with their meaning, message length, punctuation and emoji habits, how people tease each other. The note is added to replies. It shows up when editing the group on the Groups page, where you can rewrite it (manual notes are never overwritten automatically) or relearn it (after a confirmation, since it replaces the current note). You can also turn the style off for just that group: it is neither learned nor added to replies, and the note is kept. Groups the bot is off in, or doesn't reply in, are skipped. When a member asks the bot to stop saying a particular word in the group (for example a catchphrase), long-term memory records it as a group rule: it isn't tied to the person who asked, it is added after the style note, and the bot follows it no matter who it is replying to, even if the note lists that word. This works whether or not style learning is on. A group rule lapses after 30 days by default (or sooner if the member named a shorter period, like "not today"); asking again extends it, and anyone in the group can lift it by saying the word is fine again. Requests to add a catchphrase or switch tone still apply only to the person who asked.
  • Poke replies — when poked (OneBot only), it replies with one short line in persona, matched to how close you are; repeated pokes within 90 seconds get one reply, and if the model is unavailable it stays silent instead of erroring.

Agent

The model only sees what this situation allows

The agent runner feeds structured tool results back to the model until it produces a final reply or hits the step limit. The tool registry narrows by platform, relationship permissions, group configuration and feature state.

Built-in web search

Web search ships with Diana and cannot be installed or removed. The model is told to search first for time-sensitive questions, fact checks, unfamiliar entities and recommendations.

Reminders and feeds

One-off and recurring tasks share the same persistence; the console shows the schedule, next run, source and status.

Messaging another conversation

When someone in a group asks to be sent something privately, cross_session_message delivers the full text into the direct chat right away; the owner can also use it to post into a named group. The destination can never be the current conversation, so it cannot stand in for an ordinary reply. It defaults to the person speaking; only the owner may name someone else or a group. Users outside private admission, blocked users and groups that are not admitted or are switched off get nothing; the same destination is never sent two in a row, and one originating session may send at most five in ten minutes. On QQ the friend roster decides between an ordinary direct message and a temporary session through the shared group; when neither works the text is parked and delivered automatically once the person adds the bot, expiring after seven days with at most three parked per person. The friend request itself still needs the owner's approval: having something to deliver never lets the bot admit someone on the owner's behalf.

MCP and skills

External extensions are discovered from configuration and constrained by working directory, allow lists, permissions and timeouts.

Image tools

Recognition, generation and editing are authorised separately; image generation is health-checked with a real generation request.

Host metrics

host_stats lets the bot answer "how much memory is in use", "is the CPU busy", "how hot is it right now": CPU model and load, load average, memory, disk, Diana's own usage, plus whatever temperature, power and battery readings the machine exposes. Read-only, no parameters, and it shares its collector with the console dashboard.

Temperatures come from /sys/class/hwmon, power from the RAPL energy counters, battery from /sys/class/power_supply — no dependency on lm-sensors or any other external command, and a container only needs /sys mounted. RAPL exposes cumulative energy rather than instantaneous power, so power needs the difference between two samples; the first call says so instead of inventing a number. On macOS powermetrics needs root and Windows has no general interface, so both report temperature and power as unavailable — a fabricated number is worse than no number. That holds for every field: anything that cannot be read is reported with the reason, never as a zero.

This tool is owner-only too: hostname, disk paths and hardware models should not be visible to everyone in a group.

Local files and commands: read / write / execute

Every local tool is confined to workspace under the data directory. The path is fixed and not configurable; absolute paths and ../ are rejected. The three tiers are independent:

TierToolsDefaultSwitch
Readread_file, list_files, grep, find_files, manage_files statonfollows the "built-in agent" switch
Writewrite_file, edit_file, save_to_workspace, manage_files move / copy / delete / mkdiron for new profiles"allow file writes"
Executerun_commandon for the owner when the sandbox worksavailable to the owner and WebUI admin sessions in standard mode; registered only when the command sandbox is effective, always offline

A newly created bot works out of the box: writes are on (confined to workspace under the data directory, so config and the database stay out of reach). run_command has no program allowlist, but it is registered only when the command sandbox actually works: commands stay in the workspace, cannot read credential files, are always offline, and are not reviewed before running. Without a usable sandbox, or with the sandbox set to off, the tool is not registered and the owner's commands go through host_access, which a model reviews before every execution; commands that need the network go there too. The rule is one sentence: anything that can change the machine or send data out gets reviewed. Member sessions and safe mode never register the tool.

"Allow plugin scripts and package installs to use the network" only affects install_package and the scripts and MCP servers a plugin declares. Their entry points are fixed at install time; they follow the sandbox setting and, without a sandbox, run with Diana's own process permissions, which the startup log reports.

The command allowlist has been removed

The agent_command_allowlist key is ignored on load and no longer written on save, so an empty allowlist no longer turns run_command off. The owner can already run any host command through host_access, which made the allowlist a duplicate restriction. To stop local execution entirely, switch to safe mode. The file-write default still applies to new profiles only: a write switch left off stays off after an upgrade, and "Fill in recommended defaults" on the same form only edits the form until you press Save.

write_file only writes text and refuses binary targets such as .png, .pdf, .mp4 or .zip; read_file likewise refuses binary files and points to view_image for pictures and manage_files stat for size and type. Images, audio, video, PDFs and archives go in through save_to_workspace, byte for byte: from a message in the current conversation (including images the bot itself sent), from a public URL (through the same private-address-blocking download cache as other fetches), or from an MCP media_id. The real type is sniffed from the bytes and the extension corrected (JPEG is always saved as .jpg). Without an explicit path, chat media and URLs land in downloads/ and the bot's own output in outputs/; existing files are never overwritten by default. When the owner has file writes on, images from image are still delivered as before and the original is also saved to outputs/. manage_files delete does not delete: it moves the entry into .trash/<timestamp>/, keeping its relative path; entries inside the trash cannot be moved or deleted again with manage_files, and nothing can be saved into it.

The workspace is split by purpose: tmp/ for drafts and intermediate files (cleaned after 1 day), downloads/ for downloads and files saved from chat and .agent-browser/ for screenshots (7 days), outputs/ for the bot's own output (30 days), and .trash/, purged 7 days after deletion, all by the daily storage maintenance. keep/<bot>/ is the long-term area and is never cleaned automatically: when the owner says to keep something, save_to_workspace is called with keep=true (or the file is moved into keep/ with manage_files) together with a short description. Each bot's long-term area is capped at 2 GB; beyond that saves are refused rather than evicting older files, and other bots' long-term areas are read-only. The index (path, description, source message, who saved it, when, size, type) lives in .diana/keep-index/ and the newest 20 entries are added to the tail of the owner's reply prompt. Loose files in the workspace root and coding/ repositories that no bot references and nobody touched for 30 days are only reported (log and WebUI), never deleted. Settings → System → Workspace lists files by area and lets you download, delete (to the trash) and empty the trash.

Each edit_file replacement must match exactly one place in the original file; several matches or none rejects the whole batch rather than editing the wrong spot. All replacements are located against the original file, not applied one after another — applying them in sequence would let a later replacement match what an earlier one just wrote, and that edit is invisible in the caller's input.

Identity does not stop prompt injection

These tiers are owner-only, but "the call happened in the owner's conversation" is not "the owner asked for this call": other people's messages share the context with the owner's, and the model decides to call a tool after reading them. The real boundary is these switches and the allowlist, not the identity check — open the tier you need and leave the rest closed.

Coding agent

Hand code changes to Claude Code / Codex and get a report back

The coding agent plugin (official.coding-agent) lets the bot hand a coding job to a coding CLI installed on the host: edit code, run tests and commit inside a persistent workspace, for minutes or for hours. You can ask for progress at any point, and when the run ends the bot reports back to the conversation that assigned it. Disabled by default — it changes code in the background and speaks up on its own, so enabling it is a decision you make, not one made for you.

The tool is coding, owner-only, with seven operations: submit, status, tail (the last few actions), cancel, followup (continue in the same CLI session), list and workspaces.

When a job is still running a few seconds after dispatch, Diana itself posts a fixed-format acknowledgement with the job ID, agent, workspace and a summary of the instruction; the model does not write it. That message is the proof a job was really dispatched: if the model claims it handed work to Claude Code without calling the tool, no acknowledgement appears. Jobs that end within those seconds (an auth failure, a suspended account) get no acknowledgement; the dispatching turn replies with the result directly, and when that result is longer than 1,500 characters Diana posts the full text first so the model only states the conclusion. The final report starts with a header that mentions the requester. Results up to 1,500 characters follow in the same message; for longer reports the header carries only the opening lines and the full text follows as one merged-forward card (or as split messages when the bot has merged forwards off or the platform lacks them; a forward that gets no acknowledgement is treated as delivered rather than sent again). If the full text fails, the retry sends only the full text, not the header again. The job record keeps the full result, capped at 20,000 characters; status gives the model only the first 1,500 and sets result_truncated.

Settings

SettingWhat it does
Backend CLIclaude / codex / acp / custom command. Claude Code's stream-json yields an exact session ID, step-by-step progress and a structured result (model turns and cost included). acp connects ACP agents such as Gemini CLI or opencode: the executable is the agent command and the command template holds only the agent's own arguments (for example --acp); it gives structured progress and a final conclusion. A custom command falls back to reporting the tail of the log.
ExecutableEmpty means the backend's default name, resolved on PATH. Give an absolute path when the CLI is not on PATH.
Command templateEmpty uses the backend preset. Placeholders {{instruction}}, {{model}}, {{session}}, {{workspace}}; a placeholder with no value drops its argument along with the flag right before it, so an unset model never leaves a dangling --model. Arguments never go through a shell — quotes and semicolons in an instruction are just characters.
Workspace allowlistOne name=local path or name=git URL per line. Only workspaces registered here can be assigned work. A git URL is cloned to workspace/coding/<name> on first use and stays there.
Max runtime per run120 minutes by default, 1440 at most. On timeout the whole process group is killed; finished changes stay in the workspace.
Concurrent jobsGlobal limit. One job per workspace, always — not configurable: two agents editing one checkout overwrite each other.
API keyInjected as ANTHROPIC_API_KEY / OPENAI_API_KEY per backend. Left empty, the CLI's own stored credentials are used — note those belong to the system user running Diana, not to your laptop.

Jobs survive a Diana restart

The CLI starts in its own session (setsid), detached from Diana's process group, with output written to workspace/coding/.jobs/<job>.log and the job record as JSON beside it. On startup Diana scans that directory: a live process is picked back up, a finished one is concluded from its log and any owed report is delivered. The reported flag is only stored after the report goes through, so a failed delivery is retried on the next start rather than silently dropping an hour of work.

PIDs get reused, so re-attaching does not trust a live PID alone: a log quiet for more than 30 minutes ends the job as interrupted. Windows has no setsid equivalent, so jobs there do not survive a restart and are concluded as interrupted.

Authorization boundary

The coding CLI does not go through the built-in agent's command sandbox: it needs network access and repo-wide writes. The presets use --permission-mode auto, so routine changes go ahead on their own and anything that needs a nod goes through chat approval below. The real boundary is four things: only the owner can call it (the tool is absent from the non-owner allowlist and re-checks identity itself), work happens only in registered workspaces (workspace names reject path separators and ..), dangerous operations need the owner to reply with a code, and every call is written to the operation log.

Approving dangerous operations from chat

A job runs unattended, but "and then push it" deserves a human nod. Diana installs a PreToolUse hook for the coding CLI: before each command the hook asks whether it may proceed. Anything not on the dangerous list goes straight through; a match stops the job and Diana sends the owner this:

编码任务 code-3f2a1b(工作区 diana)要执行一个需要你点头的操作:
Bash:git push origin main

同意就回 7c1d9e,不同意回 b40f22。10 分钟内没人回我就当拒绝。

The owner types the code in chat and the job continues; the deny code or a timeout blocks the step, and the CLI decides whether to work around it or end with an error. status reports awaiting_approval along with both codes, so a message that scrolled past can be asked for again.

Why a code rather than the word "yes". Same reasoning as the extension-change confirmation code: the decision reduces to one structural match with no semantic inference, so no phrasing can throw it off in either direction. More importantly the code appears only in the owner's own message — page text, tool output or someone else's message can say "approved" as often as it likes and nothing is released. The code must appear as a standalone fragment, so one embedded in a longer hash does not count; it is consumed on use, so replaying the same message a second time does nothing.

The hook process and Diana talk through a file mailbox under the job directory (.jobs/approvals/) — no local port, no token: the hook is a child of the CLI, its environment is not Diana's to decide, and that directory is always reachable. A useful side effect is that a Diana restart mid-wait loses nothing — the request file is still there, the hook is still polling, and the question is asked again after startup. The hook calls back into Diana's own executable (internal subcommand __coding-approve) rather than a generated script, which avoids executable bits and interpreter paths differing per platform.

SettingWhat it does
Chat approval"Dangerous operations" (default) stops only on list matches; "all writes" asks once per edited file, which is noisy and mostly for trying things out; "off" runs unattended throughout.
Dangerous operation listOne command fragment per line, case-insensitive, a match requires approval. Defaults cover git push, git reset --hard, git clean -f, git tag -d, gh pr merge, gh pr create, gh release, npm publish, docker push, rm -rf, sudo. What earns a place: it cannot be taken back, or its effect reaches outside the workspace. Editing files, running tests and committing locally are not on it — those are implied by assigning the work.
Approval timeout10 minutes by default, then treated as a refusal. The job is stopped while waiting and that time counts against the max runtime, so raise both together.

This works with Claude Code and ACP agents. Claude Code goes through the PreToolUse hook; an ACP agent goes through the permission requests it sends itself, using the same mailbox and codes, so only operations the agent chooses to ask about can be approved. Codex and a custom command have no equivalent. With approval on and a backend that cannot do it, submitting fails outright instead of quietly allowing everything: silently not asking would be a lie.

A step blocked by auto is fixed by adding it to the dangerous list

The hook stays silent on anything off the list, leaving those to --permission-mode auto. In non-interactive mode nobody can answer the CLI's own prompt, so a step auto wants confirmed is simply refused, and the job log names it. When that happens, add the command fragment to the dangerous list: a match goes through chat approval, and once the owner replies the hook returns an explicit allow, which overrides auto's hesitation. To skip adding them one by one, switch the template to bypassPermissions — note Claude Code rejects that mode when running as root, and a Docker deployment is root by default.

When the approval path itself breaks (an unreadable policy file, say) the hook allows and the reason goes to Diana's operation log. That is deliberate: Diana's own plumbing failing should not strand a job halfway. What you truly do not want touched is kept out by the workspace allowlist, not by approval.

Identity does not stop prompt injection

As with the three local file and command tiers: "the call happened in the owner's conversation" is not "the owner asked for this call". Other people's messages share that context. The workspace allowlist is the gate that actually holds — register only repositories you are willing to let it change freely, and keep production config or secret repos out. To withhold push, switch the template to --permission-mode acceptEdits and block pushes with a git hook on those repositories.

Writing the instruction

The coding CLI cannot see this chat. When a change depends on what was said, the model puts the relevant quotes in the context parameter and they are prepended as background. Instructions cap at 8000 characters — they land in argv, which the OS limits per argument; longer material belongs in a file inside the workspace for the CLI to read itself.

Once a job finishes with a session ID, followup continues in that same session, so the CLI remembers the previous round. A running job takes no additions: a non-interactive coding CLI has no channel for mid-run input, and forcing one would just be a second process editing the same checkout.

Security

A public deployment needs HTTPS and origin limits

  • The console protects the API with a login session — change the initial administrator password immediately.
  • Configure a separate OneBot access token and keep both ends of the reverse WebSocket in sync.
  • Keep config.yaml, model API keys, bot tokens and platform cookies out of Git.
  • Debug mode records full context and tool arguments; enable it briefly and only in trusted environments.
  • Put the public entry point behind an HTTPS reverse proxy and restrict who can reach the admin surface.