Letting the Bot Change Itself
Tell the bot in a DM "add a /ping command that replies pong" or "set the weather plugin's default city to Shanghai". The bot drafts the change, trial-runs it in a sandbox, and sends you an approval card. Approve once and it takes effect on its own — hot reload when possible, an automatic restart when not — and then it reports back in the conversation you started from: "applied" or "failed, rolled back".
Design and defaults: ADR 0040 (Chinese). Runtime details for every step: Default Behavior.
Who can ask
- The bot's owner (endpoint owner /
trigger.masters) and IM approvers (approverIdswhileai.agent.toolHardGate.imApprovalis on). - Everyone else — group members, group admins, trusted members, strangers in DMs — does not see this capability: the bot will not draft for them, and a forced call is refused and audited.
- At most 1 change waits for approval per conversation and at most 3 per instance; when full, the bot asks you to settle existing cards first.
How a change flows
- Draft: the bot hands complete file contents and a field-level config patch to the host, which stages them in
.zhin/changes/. Nothing in production is touched and nothing takes effect yet. - Trial (next section): if it fails, no card is sent; the bot gets the errors and drafts again.
- Approval: the card always goes to the owner's DM — even if you asked in a business group, and saying "OK" in a business group does not count. It lists the files, lines added / removed, config paths touched, whether a reload or a restart is needed, a bounded diff excerpt (at most 40 lines / 2 KB; when cut it says the full content is in the Console), and the trial verdict including the names of skipped checks. If you have not seen the full content, open the Console "self-change" page before approving — do not approve blind. Reply
/approve <card id>or/deny <card id>in the DM, or settle it through the Console approval RPCs. Unanswered cards expire after 24 hours. - Apply: plugin-only changes hot reload; changes that need a restart (for example host sections such as
httpordatabase) trigger a controlled restart followed by a health check. - Report: back in the original conversation ("applied", noting a restart if one happened); if that conversation is unreachable (group gone, bot removed or muted) the report goes to the owner's DM.
You approve this exact content: the approval is bound to a content hash, and a mismatch after approval is refused.
Sandbox first, then publish
Code the bot writes never goes straight into production. Before the card is sent the host automatically runs:
- Static checks: syntax, default-export shape for convention directories, forbidden imports,
SKILL.md/agent.jsonparsing; - Sandbox load: assembles the drafted plugin in a throwaway child process with no real platform and no model, and checks that its commands / tools are discovered;
- The plugin's own tests: in an isolated, offline, read-only, resource-limited container.
When the machine cannot run a check (no Docker, the plugin has no tests, …) it is recorded as skipped: this does not block submission, but the card's trial verdict shows how many checks were skipped and names them (e.g. "passed (2 skipped); skipped: sandbox_load commands/ping.ts, …"). Look twice before approving such a card.
Trial passed, one human approval, written to the production directories — that is "publishing". There is no second approval.
When something goes wrong
- Health check fails after apply: the bot rolls back once from its snapshot and reports "failed, rolled back" with the reason.
- Still unhealthy after the rollback: an instance-level emergency stop opens, only the owner's DM is told, and there are no more automatic restarts until you deal with it (the evidence is kept until you mark it handled in the Console).
- A configuration save/application is unconfirmed, or a newer revision has replaced it: the flow stops and keeps evidence for manual verification. It does not retry the operation or overwrite another edit. Here,
failedmeans the flow stopped, not that the configuration is known to be unapplied. This outcome does not enable emergency stop by itself. - During recovery, the bot checks the last durably acknowledged revision. If it cannot prove ownership, it keeps the older records for manual verification.
- Upgrading from a build before #1146: an
applying/verifyingrecord that carries a configuration patch but no durably acknowledged revision (configRevision) cannot prove ownership. Recovery does not continue, restore, reload or restart it; the record becomesfailedwith evidence kept for manual verification. - A configuration receipt that reports a group as
restart-pendingwhile the save says no restart is required is inconsistent and is treated as unconfirmed (stop and verify manually), not as a pending restart. - Record status vs. decision outcome: for
change_config_unconfirmedandchange_config_supersededthe change record ends asfailed(automatic continuation stopped, manual verification required), while the matching decision event keeps outcomedefer— the result is not known to be an error or a denial, it is waiting on a human. - Process killed mid-apply: the next start checks durable progress and configuration ownership. Confirmed operations retain snapshot rollback and health checks; unconfirmed writes or applications retain evidence for manual verification.
- Process exited right after approval: on the next start, approvals younger than one hour are applied; older ones expire and the owner gets "approval expired, please draft again".
Automatic restarts need the supervisor
Automatic restarts rely on the zhin start supervisor. Without it (running the runtime with bare node or some container entrypoints) or with --once, the bot tells you this instance cannot apply such changes automatically, and changes that need a restart are not submitted for approval. Hot-reload-only changes are unaffected.
What no grant can loosen
- Every approval is a fresh human approval:
bypass, auto-review, the owner role, remembered grants anddanger-full-accesscannot approve for you. - The bot may only write plugin convention directories (
commands/,middlewares/,handlers/,tools/,components/,skills/,agents/, in the project root or a local plugin inside the project), the rootAGENTS.mdandskills/**; root config can only be patched field by field, never overwritten as a file. - Never writable:
node_modules, lockfiles,.env*,.zhin/and other host files.
The configuration allowlist is unchanged: the self-change tool can directly patch only ai.trigger.ambient within Host settings, and the default CLI still marks it pending restart. Model provider/model fields are outside this tool's Host allowlist. A trusted Host receipt confirming every pure configuration change applied avoids a redundant restart; approval and the pre-write supervisor gate remain unchanged.
Config the bot cannot change
Any setting that would loosen security or widen the tool surface is refused at draft time — no card is sent. For example:
- under
ai.agent: the dangerous-tool hard gate (toolHardGate, including the IM approver list), exec approval and exec security (execApprovalMode,execSecurity,execPreset,execAllowlist, …), the owner shell, injection guard, the owner's quota exemption, schedule security; ai.decisions.approval,http.token/http.tokens,a2aremote executors and callbacks,mcp/ai.mcpServers(a new MCP server means new tools), and more.
The full list is in Default Behavior. Edit these in the config file yourself.
Reviewing the full diff before approving
- Console "self-change" page (ships with the
zhinjs/consolerelease; the Host RPCs it uses are in this version): lists pending and recent changes; the detail view (needsops:approve) shows a unified diff per file (the baseline is the current production file, or the rollback snapshot once applying started; at most 64 KB combined across all files, cut by line and marked; later files are marked as not shown), the redacted config patch, every trial check (skipped ones highlighted) and staging usage. You can approve / reject there, and mark an emergency-stopped change as handled. Once a change ends its bodies are cleaned up under the staging retention rules and the detail says so rather than inventing a diff. - IM approval card: carries a bounded excerpt only and never config values; the excerpt is not the full content (#1142).
Not supported yet
- Creating a new plugin package (a new
plugins/<name>/package.jsonand entry) and installing npm dependencies: out of scope for now — install dependencies and create the plugin yourself, then ask the bot to add commands to it. - Changing the zhin framework itself, approving several changes together, pushing changes across instances, a visual editor.