Docs / Developer / Kit content tools
Kit content tools
Kit has two different inventories:
- Runtime tools perform model-requested work.
- Slash commands navigate the terminal shell.
They are intentionally not mirrors. /tools explains content operations to a human, while model
discovery remains part of the tool loop.
Built lifecycle
The capability foundation and the eight listed lifecycle kinds are connected to the live Kit
session:
src/entities/capabilities/owns stable IDs, deterministic search, provider-safe names,
direct, deferred, or hidden exposure, and the shared provider-discovery descriptor contract.src/entities/lorebook/capabilities/owns pure lorebook settings and entry update, reorder,
enable, and remove previews.src/entities/character/capabilities/owns typed character identity, prompts, greetings,
metadata, presentation, media, links, variants, settings, and sealed behavior-script previews.- Persona, preset, standalone regex, media-pack, quick-reply, and HTML-document folders own their
corresponding semantic settings and lifecycle operations. - Character base-path and variant edits live in entity-layer operations shared by Kit and the
Workbench. Semantic field capabilities may target a named variant without exposing a raw patch
tool. - The Workbench lorebook session imports the same pure operations for its existing editing paths.
src/kit/capabilities/discovers drop-in capability modules, adapts them to Kit runtime tools,
joins content and non-content descriptors into one discovery catalog, keeps a complete dispatch
registry, and exposes only successful search or exact describe selections to later model calls.src/kit/changes/composes multiple operations for one target into one in-memory draft while
preserving the baseline,originalescrow, per-operation warnings, and platform impact.src/kit/results/owns bounded session-local observations and fail-closed JSON Pointer traversal.src/generated/capabilities.tsis generated from the same folders for the browser bundle and
Kit.bun run capabilities:checkrejects drift.- The loop asks for a fresh tool snapshot before every model request.
capability_findsearches and reveals up to five relevant typed workflows, browses a collapsed
domain to area to action hierarchy without revealing schemas, or describes and reveals one exact
operation for the next model step. An existing piece target is optional during discovery. Compact
calls may omitaction: a query means search, an exact ID means describe, and other calls browse.
A flat contentkindis accepted, andcontent/characterbrowse shorthand resolves to the
character kind rather than an empty area.studio.<kind>.createreveals a typed creation workflow for each of these supported kinds: character,
lorebook, persona, preset, regex, quick-reply set, sprite pack, and HTML document. Each builds a canonical preview and does not write
during composition.studio.piece.duplicatecopies one stored piece to a new id. A copy is a create, so it composes a
preview-only draft and reaches storage through the same create-only compare. It carriesbody,profilesandoriginalforward: a copy that dropped escrow would silently lose every
platform-native field the canonical model does not express.studio_deleteremoves one piece. It is the only Kit tool that destroys canonical content. It carries no discovery descriptor, becausecapabilities/runtime.tsrefuses any deferred workflow whose effect isapply; its sole
classification is the explicitdeleteentry in the safety-owned trust map, which sits at the
danger floor. It reads the target before removing it, reports a miss asstalewithout writing,
and its description states plainly that referencing pieces were not checked. The receipt repeats that
only for the kinds something can reference (lorebook, pack, regex).studio_transferpreviews a crossing: it serializes one stored piece through another platform's
adapter and reports what the crossing costs, without writing anything.studio_exportperforms the crossing and writes the file into a fresh<studio>/Export/<timestamp> Kit <id>/folder. The model
supplies no path component at all: it names a piece and a target format, and the filename is
derived from the piece id plus the adapter's own extension. Containment is therefore structural
rather than validated, which is what makes a model-callable file write safe. Writes are
create-only, because an export that silently replaced an earlier one could destroy a file already
sent to somebody and there is no revision history to recover it from. Text and binary carriers share
the exclusive-write owner, which syncs file contents and removes its own incomplete file on failure.- Both call one shared
convertStoredPiece, so a preview can never report different losses than the
write that follows it. The conversion graph is imported lazily so it stays out of Kit's cold start. macro_lookupanswers "what does this macro do here, and what is it on that platform?" from the
engines' own catalogs, so a model never has to guess an equivalent.block_lookupdoes the same one level up, for kinds of prompt block: what a tracker or an
assembler is for, how it is built, what silently goes wrong with it, and an editable starting
block.preset_verifyrenders a preset through the real engine and reports what did not resolve.
It is offered only where an engine lives: the checkout is the one pointed at in the studio
window's Settings > Studio (engines.jsonin the studio folder, read when the session is built)
or named byBUNZAI_ST_ROOT/BUNZAI_MARINARA_ROOT, which outranks it.rail_openputs a preset where this app shows presets, and a name that is a real piece of ANOTHER
kind now gets told which kind it is and where that kind opens, instead of a roll-call of every
preset in the studio. Asked to put a drawing on the rail it used to answer "no preset matches" and
list them all - true, and read as "that piece cannot be opened at all". The rail holds a preset's
blocks in evaluation order; nothing else has blocks.- Drawings have both halves now:
studio_htmldoc_createmakes one, andhtmldoc.document.update-
a deferred capability found by search ("edit the wireframe", "fix the layout") - patches the markup,
name, summary, tags or notes of one that exists. The edit half was missing at first, and the gap
was not cosmetic: create-only apply refuses an occupied id, correctly, so a model that had written
a better version of a drawing could neither save it nor honestly claim it had. regex_from_drawingandhtml_will_drawserve the design-once-render-always shape: somebody
draws the rich card, tracker or panel as anhtmldoc, then a regex renders it from whatever
compact form the model is asked to write, so the visual complexity lives in the script rather than
in every reply. The pattern is a judgement call and stays with its author; the REPLACEMENT is pure
transcription - flatten the markup to one line, double every literal$, swap each declared slot
for a capture reference - and at four thousand characters that is where a hand-written rule goes
quietly wrong.regex_from_drawingdoes that transcription, refuses a slot pointing past the
pattern's capture count, and with asampleruns the finished pair through the real engine,
reporting a pattern that matched nothing as a failure rather than as an unchanged clean run.html_will_drawreads any markup againstcore/render/seal-policy.ts- the same tag list the
sealed preview enforces - and names what would be stripped (form controls among them), which
http(s) reference cannot load, and which handler never fires. Both are reads; saving stays with
the create tools. Neither is a safety check: the sanitizer and the iframe CSP do the enforcing.folder_searchlists and reads files in a folder the user has shared with Kit, plus the studio
itself, which is a standing grant rather than something shared. It can do nothing else. Every path is resolved and then checked against the grants, so the resolved path is the one
used, and that resolved path is reconciled against its real path so a link cannot carry it out of
the granted tree. Traversal skips symlinked entries, and the walk is bounded in depth and entries.folder_importis the only door from a shared folder into the studio, and it goes one way. It
reads a file, parses it through the same three-way branch the studio's own inspect uses, and
proposes each canonical piece as a create draft. Its effect isdraft, notread, which keeps it
out of the read-only MCP posture where nothing would ask before applying - so on the Claude
subscription provider it is absent unless "let Kit make changes this session" was answered at
setup, because that answer is what starts the tool server with the full belt. A file that carries more
than one piece drafts all of them or none: a character card's body names its embedded book inknowledgeRefs, so importing the character alone would leave a reference to a piece that is not
there. An id already on the shelf is refused rather than replaced.rail_openputs a preset on the rail beside the conversation, the same view/railopens, and itsstatusaction reports what is on the rail right now. It is read-only: it changes nothing and so
needs no confirmation, which is what makes it usable mid-sentence. The rail it opens is still a
write surface, and edits made there meet the ordinary Gate. It refuses to choose between several
matching presets. What is on the rail also rides in the turn's ambient context, one line, so the
model knows without asking - it used to answer from what it had opened earlier and was wrong the
moment somebody opened a different preset themselves.regex_labis the regex workbench:trya pattern against real text,showwhere exactly it
matched,raceseveral candidates side by side,readone in plain words,linta whole set,
browserecipes, or build onefrom_examples. Read-only - it runs patterns and stores nothing;
saving belongs to the regex create capability. Regex is the only content in the studio that is
write-only without a preview, which is why the workbench exists.studio_artputs a piece's own card art on the user's screen. Read-only for the same reasonrail_openis: showing a picture changes nothing, so it needs no confirmation, and needing none is
what makes it usable mid-sentence. The BYTES never reach the model. The result carries a structuredshowand the shell resolves the picture itself, because a card image is often hundreds of
kilobytes and paying tokens to move something the model cannot see would be the worst trade
available. It refuses to choose between several matches, and it checks the piece actually carries
art before reporting that it showed any, so the model cannot describe a picture nobody is looking
at. A portrait stored as an asset-store or archive reference still resolves to nothing: those need
the asset files loaded beside the entity, which no caller does yet.ask_choiceoffers the user a list to pick from instead of writing options into a sentence. The
options travel as data on the tool result and the shell renders them, so a model cannot offer a
choice by claiming to have offered one. Picking fills the composer; it never sends.- A selected capability creates or composes a preview draft without saving. Its result also carries
a structured semantic review projection; the loop never parses a draft ID or field diff from
prose or JSON output. - A selected read-effect capability returns deterministic analysis and is rejected if its preview
changes canonical content. change_querylists drafts, returns one complete accumulated proposal, or validates canonical
shape and current revision without writing.change_discarddrops an in-memory draft.- When the model finishes composing a draft, the loop suppresses its final save-or-discard prose
and deterministically hands the accumulated review to the application-owned Gate. Several draft
operations can compose before this single handoff. change_applypauses at the Gate, compares the stored canonical revision, saves exactly once,
re-reads the piece, and returns an applied, stale, or failed receipt. Denying the draft review
dispatcheschange_discardwithout another model round.docs_querysearches the generated Bunzai documentation catalog, then reads only a
catalog-declared page or heading section. It cannot accept filesystem paths.studio_readreturns one complete normal canonical piece by default. Outline, JSON Pointer paths,
offsets, and bounded continuation remain available for genuinely oversized content.result_querystats, reads, or literally searches an oversized observation through an opaque
session handle.
The scheduler is live. Read-only batches run concurrently while retaining provider order in the
returned observations. Any batch containing a draft, apply, or unknown effect runs serially. The
loop has model-round, tool-call, elapsed-time, no-progress, and cancellation stops with recovery
guidance. An identical full-piece read repeating in nearby exploration stops after the second
observation instead of consuming the turn. A batch that exceeds the tool budget is not added to
provider history; cancellation records no-op results for declared calls it did not execute, preserving
tool-call/result pairing. The /tools command opens a target-aware Panel Deck:
choose a piece, an available semantic area, then an action. Its detail pane names platform
applicability and the preview-before-apply boundary. Compact terminals show one active pane at a
time instead of crushing three columns.
Backstage labels come from the loop state machine, not model prose. Live and sealed traces retain
capability discovery, reads, drafting, preview readiness, one-shot apply, verification, and the final
verified, stale, discarded, failed, cancelled, or stopped receipt. Text emitted alongside a provider
tool call stays in provider history but is not rendered as a conversational transcript line.
Capability contract
A content capability is a pure semantic operation. It declares:
- a stable dotted ID such as
lorebook.entries.update; - content kind, area, action, summary, aliases, and platform applicability;
- a Zod input schema;
- explicit
readordrafteffect and direct, deferred, or hidden exposure; - a concurrency key;
- a preview function that returns a complete canonical entity, exact changes, warnings, and
platform impact.
Preview never writes. The draft composer rejects a changed target identity or changed original
escrow. Read-effect capabilities may also return a structured observation, but their returned entity
must be canonically identical and their change list must be empty. The adapter enforces both rules
before returning analysis.
Apply and authorization
The safety subsystem owns access classification. It receives exact provider names from the validated
pure-content capability catalog; it does not trust general workflow metadata, runtime effects, or
name prefixes. Content read capabilities remain read-only, content draft capabilities and discard
are preview-only drafts, and applying is a durable write that pauses in the interactive Gate under
the default guarded mode. General workflow tools retain the writable Studio bridge, so discovery
metadata cannot classify them as safe; they remain unknown until a separate safety-owned exact-name
policy grants the appropriate access.
The Studio backend compares and publishes under one per-path write lock. Updates require the
expected revision. Creation uses a separate create-only comparison that refuses an occupied ID
rather than overwriting it or silently minting a sibling. A stale revision or create collision
writes nothing. After a successful publish, Kit re-reads the entity and verifies all
capability-owned canonical fields and non-Studio escrow before reporting applied. A save with an
unreadable or mismatched verification result reports failure and is not retried automatically.
The guarded-mode draft Gate is a semantic review card, not a generic tool confirmation and not a
verbal contract with the model. It shows the target and bounded before/after rows. Mouse controls
and y or Enter apply; mouse controls and n, d, or Escape discard. Autopilot and full-control
modes retain their documented write behavior.
Progressive exposure
The runtime registry contains every adapted capability and deferred workflow tool so dispatch can
resolve a selected operation. Each user turn starts with the direct belt: ask_choice,block_lookup, capability_find, change_apply, change_discard, change_query, docs_query,folder_import, folder_search, html_will_draw, macro_lookup, preset_copy_blocks,preset_verify, rail_open, regex_from_drawing, regex_lab, result_query, studio_art,studio_collections, studio_delete, studio_export, studio_graveyard, studio_graveyard_bury,studio_list, studio_read, and studio_search. preset_verify is the one entry that is
conditional: it declares available and takes itself out of the belt on a machine with no engine
checkout, so a person without one starts with twenty-five.
studio_delete and studio_export are direct rather than deferred for a structural reason, not a
convenience one. The
deferred path classifies a tool from its discovery metadata, and the runtime refuses to let that
path describe an apply effect at all, so a destructive workflow cannot be discovered into
existence. A tool that must be granted by exact name in the trust map has no discovery descriptor to
hide behind. Routing removal through the draft/apply path instead would require ChangeDraftMode to
grow a remove arm and ChangeDraft.proposed to become optional; that is a change to a core
authority and belongs in its own design pass.
One provider-discovery catalog accepts content, studio, transfer, and diagnostics domains.transfer is populated by studio_transfer; diagnostics is a declared but still empty slot, and
nothing should read its presence in the enum as a shipped workflow.
Content capability metadata projects into that catalog; a non-content HarnessTool supplies the
same validated discovery metadata beside its implementation. The live deferred inventory includes
the content catalog and typed creation for the eight listed lifecycle kinds under studio/lifecycle.
Lifecycle, transfer, and diagnostic workflows join the same router instead of adding another
meta-tool. Only restricted pure-content capabilities derive safe read or draft access from catalog
metadata. General workflow tools remain unknown to the Gate until separately classified. Each
creation tool has a separate safety-owned exact-name draft classification. A deferred apply
workflow is rejected until its plan supplies an explicit safety-owned access contract, so discovery
metadata cannot quietly downgrade a write. The complete tool registry also rejects duplicate
provider names before dispatch or schema publication.
A catalog search replaces the current deferred set with no more than five deterministically ranked
matches. A collapsed browse lists domains, areas, or the actions under one area without exposing
typed schemas. Describe selects one exact action and replaces the deferred set with that operation.
An optional piece target filters compatible kinds but is no longer required to discover studio-wide
workflows. Hidden descriptors never appear in search, browse, describe, or the provider snapshot.
The deferred set is cleared before the next user turn, while any preview draft remains available to
the direct query, apply, or discard tools.
This keeps a future catalog of more than one hundred semantic operations out of every prompt while
preserving typed inputs for the selected operation.
Creation discovery does not burden the opening prompt with six large schemas. Search reveals up to
five matching tools while an exact describe reveals one; the next provider round receives each selected JSON Schema with field
descriptions and defaults. Character creation documents portrait and additional-media roles.
Persona creation keeps the shelf-only brief distinct from injected first-person content and
documents its portrait. Pack creation documents item IDs, expression labels, resolvable references,
optional MIME types such as image/png, and default-face selection. A media ref may be a data URI,
an HTTP(S) URL, or an existing asset or archive reference. Lorebook creation accepts typed initial
entries and triggers. Preset creation accepts typed initial prompt blocks, samplers, and media
inlining settings. Regex creation accepts stored rule data and never executes it.
Canonical reads and result handles
studio_read defaults to the complete canonical entity. A normal piece up to 64,000 characters is
returned in that one observation. outline returns the selected value's immediate children as RFC
6901 JSON Pointers. read accepts one returned pointer plus character offset and limit, so a model
can traverse genuinely oversized content without guessing a path. Missing and malformed pointers
fail closed.
An unpaged piece over 64,000 characters is stored in the owning Kit session and returned as an
opaque result-N handle plus a 4,096-character peek. Other result-producing tools, including tools
imported from external MCP servers, retain the shared
4,096-character inline threshold. result_query provides stat, bounded read, and literal
case-insensitive search. Handles contain no path and disappear with the session.
Storage is capped at 24 results, 1,000,000 UTF-8 bytes per entry, and 4,000,000 UTF-8 bytes total.
Oldest entries are evicted first. Character counts remain available for paging. A value too large
for the store remains recoverable through direct studio_read offsets. Result reads cap at 12,000
characters and searches at 50 rows.
Complete draft review
Every operation stores its validated input, changes, warnings, and platform impact. The draft also
retains the aggregate warnings and impact while composing over one immutable baseline.change_query list returns compact counts and states. show returns the complete operations,
baseline, proposed entity, warnings, and platform impact; oversized proposals use the same result
handles, and explicit offsets remain available beyond the store cap. validate re-parses the
proposed canonical entity and compares the current stored revision without changing draft state or
writing. Operation rollback remains deliberately absent until deterministic replay is specified.
Character slice
studio.character.create is the character creation entry point. A query such ascreate new character withkind: character reveals it without requiring a pre-existing target. It accepts a safe optional ID
or derives one from the name, then previews a complete minimal canonical card with any supplied
tagline, description, personality, scenario, first message, examples, and tags. Applying uses an
atomic create-only write and a post-create verification read.
The existing-character bundle is:
| Capability | Previewed change |
|---|---|
character.identity.update |
Name, nickname, tagline, and casting-card identity |
character.prompts.update |
Persona prose, prompts, examples, and depth injections |
character.greetings.update |
First, alternate, and group-only greetings |
character.metadata.update |
Tags, attribution, rating, warnings, and creator notes |
character.presentation.update |
Palette, background, field order, spoilers, and links |
character.media.update |
Portrait, asset pack, visual kind, and face label |
character.links.update |
World, lorebook, and standalone behavior references |
character.variants.manage |
Add, remove, rename, mode, and explicit field inheritance |
character.settings.update |
Authored dials, bias, variables, and sealed behavior settings |
character.behavior-scripts.manage |
Add, overlay, remove, and reorder embedded script data |
All ten are preview-only drafts. Embedded regex and trigger payloads remain data; these operations
never execute them. Platform-native character extension fields that exist only inside original
escrow are not editable through this lifecycle. A semantic capability must not rewrite escrow to
implement a canonical edit. Format-owned capabilities can be added only when their adapter provides
a typed, non-escrow semantic home.
Lorebook slice
The current drop-ins are:
| Capability | Previewed change |
|---|---|
lorebook.settings.update |
Portable book settings |
lorebook.entries.update |
Basic fields on one entry |
lorebook.entries.reorder |
Authored entry order |
lorebook.entries.enable |
Enabled state for one or more entries |
lorebook.entries.remove |
Removal of one or more entries |
studio.lorebook.create creates the book and may include initial entries with literal or regex
triggers, activation mode, insertion position, depth, and role. Duplicate, separate entry-create,
trigger-specific update, placement-specific update, and broad bulk-operation capabilities remain
future additions. The Web UI keeps its selection, focus, generated-ID, and undo behavior locally.
Other canonical bundles
| Kind | Capabilities |
|---|---|
| Persona | Create, identity text, structured profile, injection, and presentation |
| Preset | Create, settings and samplers, prompt blocks, and groups |
| Regex | Create, set settings, and stored rule lifecycle |
| Pack | Create, pack settings, media items, and named groups |
| Quick reply | Create a typed set; Workbench provides the set editor |
| HTML document | Create and update the sealed drawing document |
These operations share pure entity-layer reducers with the Workbench where the corresponding editor
already exists. Regex and embedded behavior payloads remain sealed data and are never executed by a
capability.
Macro translation across engines
A preset crossing to another engine is TRANSLATED, not merely reported on. Refusing because one
token has no perfect twin leaves the job unfinished: the author asked for a preset they can load
somewhere else, and prose they wrote is worth more than a macro that was never going to fire.
The engine computes every equivalence and the model explains it, never the reverse. A model
reasoning from macro names gets this confidently wrong, because names collide across engines while
meanings do not follow them.
Hub and spoke, the same as formats
There is no engine-pair table. Project rule 1 forbids format-to-format conversion for macros exactly
as it does for codecs: five engines would mean twenty directed pairs, so each new engine would
multiply the work rather than add to it.
Instead each catalog declares what its OWN macros mean, using the canonical operations insrc/core/preset/macros/ops.ts. Every cross-engine answer is a join on that meaning, computed inequivalence.ts. Adding an engine is one annotation pass and every direction appears for free.
Ops attach per ENTRY, not per name, because one name can mean different things at different argument
counts. RoleCall documents {{random}} as 0-100, {{random::min::max}} as a range, and three or
more arguments as a pick; annotating the name would wrongly flag the portable three-argument form.
Annotation is deliberately partial. An entry with no op falls back to name matching, which is correct
for the majority of macros that agree across engines. Only divergence needs declaring.
What a crossing produces
| Verdict | Meaning |
|---|---|
portable |
Both engines perform the operation; the token is re-spelled in the target's punctuation |
collision |
Same NAME, different operation. Left as authored and flagged, never silently kept |
absent |
The target has no macro for the operation; removed, with same-family candidates |
flatten |
A block the target cannot express; the body is kept and the scaffolding removed |
{{random::1::10}} crossing RoleCall to SillyTavern is the worked example: RoleCall declaresrandom.range, SillyTavern declares random.pick, and the collision is derived. Nothing states that
pair anywhere.
Block flattening keeps the body and records the dropped condition as a {{// ...}} comment, which
the target does support, so the artifact documents its own losses. The first branch is taken, since
without an evaluator there is no basis to choose another.
The gate that keeps it honest
unannotatedDivergence() returns any name several engines share with differing shapes where at least
one side has not declared an op, and a test asserts that list is empty. A silent mistranslation
cannot be introduced by adding an engine and forgetting to declare a meaning.
Honest limits
- Risu (
[[name]]CBS) and Agnai (named slots) are not modeled at all; an unmodeled target reportschecked: false, which a caller must render as "unknown" and never as "clean". - SillyTavern's catalog is the only one with no oracle to verify against, so its annotations are the
least grounded. RoleCall, Lumiverse and Marinara are machine-checked against their real registries
byscripts/macro-oracle. - Flag-prefixed tokens (
{{#if}},{{!x}},{{~x}}) resolve to no name and are never flagged. The
same character means different things per engine, so stripping it would trade a false negative for
a false positive.
Bunzai documentation query
docs_query is one direct read-only meta-tool with four actions:
browsereturns catalog collections and pages from index metadata only (no Markdown bodies).
With no collection it lists root collections and root pages; with a collection it lists that
folder's immediate child collections and direct pages. Limit defaults to 12 and caps at 25;
offset paginates pages. Collection ids are slash-separated catalog prefixes and never raw
filesystem paths.outlinereturns one page's semantic overview and a collapsed H2 map from the generated index,
still without loading the Markdown body. Each H2 reports its child count. Passing one returned
section slug expands only that H2/H3 branch and its semantic summaries.searchlazily loads catalog-declared Markdown, splits it into heading-addressable chunks, and
ranks full text plus title, short summary, semantic summary, topics, and headings with an
in-memory BM25-style scorer;readaccepts one returned catalog ID and is the only action that returns authoritative source
prose. Omit the section slug to read the whole document; provide one returned slug for a targeted
nugget. Responses stay bounded at 1,000 to 12,000 characters and return a continuation offset, so
Kit can consume a long whole document over several calls instead of truncating it permanently.
Semantic summaries and topics are navigation aids. They are merged into the generated catalog only
from authored sidecars under docs/summaries/ that also have a current independent APPROVE
receipt under docs/summary-reviews/ bound to the page source hash and the semantic-summary hash.
Unapproved, revised, blocked, or stale sidecars never become navigation metadata. Discovery of pages
for authoring tools is folder-derived from docs/; authors do not hand-edit generated JSON to
register a new page.
New document flow:
- Add
docs/.../page.md bun run docs:summaries:scaffold -- <doc-id>- Author page and section summaries in the sidecar
bun run docs:summaries:stamp -- <doc-id>bun run docs:summaries:check -- <doc-id>(author validation)- Independent review:
bun run docs:summaries:review -- <doc-id> APPROVE --notes "..." --reviewer <id> bun run docs:indexand commit generated artifacts
Edited document flow: re-read changed ranges, update affected summaries, stamp, author-check, then
new independent review (never self-approve; never stamp without reviewing prose). Globaldocs:summaries:check fails until every page is APPROVED and projections are current.
The model must read source prose before relying on a detail. It should normally read the whole
document around a located answer so nearby qualifications, failure conditions, and current-versus-
planned distinctions remain visible. A section nugget is appropriate for a narrow lookup or limited
context. The shared corpus boundary validates the generated index, resolves only catalog-declared
Markdown beneath docs/, and caps each returned page. The index is built lazily on the first docs
query and requires no embedding model, vector database, server, background process, dependency, or
network call. The Studio docs reader and Kit use the same path-containment implementation.
Serving the belt to another client
bunzai mcp [studio-dir] [--read-only] serves the same tools over MCP on stdio, so any client that
speaks it (Claude Code, Cursor, Zed, Codex) can read and edit the studio. Register it once with the
client's own command; for Claude Code that is:
claude mcp add bunzai -- bunzai mcp
An explicit studio directory is the first non-flag argument; without one the server resolves the same
studio the CLI would. The subcommand exists so nothing has to know where Bunzai's source lives: a
compiled binary has no src/mcp/main.ts to point at, and a path that worked from a checkout failed
silently for anyone who installed rather than cloned, with the turn simply answering without tools.
The connecting client owns its permission policy. A person registering the full belt must review
that client's configuration; Bunzai cannot guarantee that another client prompts before each call.
Use --read-only to exclude write capabilities. Spawned by Kit for the Claude subscription provider,
the CLI is started withdontAsk, with tools: [], skills: [], and strictMcpConfig: true. allowedTools only grants
permission; it does not restrict availability. Only the explicitly supplied Bunzai bridge is exposed,
and other unapproved calls are denied. The bridge is read-only unless the person explicitly enables
its write option. Filtering builds the dispatcher from that reduced set, so a filtered tool is unknown
rather than executable. These configuration properties have isolated tests; real SDK execution is not
a security proof from this audit.
The belt comes from discoverTools(), the same folders-as-schema loader Kit's own loop uses, and the
schemas come from the tools' own Zod definitions. A file dropped into src/kit/tools appears in
every connected client with no edit here. Bunzai's stdio server implements its small JSON-RPC protocol
without the MCP SDK. The optional Claude provider does require that SDK as a peer; its patched version
is pinned explicitly because a transitive peer override did not update the installed package.
Wearing other people's belts
The other direction: MCP servers configured in Settings > Connections join Kit's own belt. Saving,
disabling, or removing a server synchronizes the live connections and invalidates the cached Kit
session, so the next turn rebuilds the exact tool catalog without restarting Bunzai. The config is~/.bunzai/mcp.json - home-keyed beside the vault, because an entry names a COMMAND
THIS MACHINE EXECUTES at the next session build, which is a fact about the machine and not about any
studio. It is written only through the host-only /api/mcp/* routes (a tailed-in device offered
that surface would hold remote code execution wearing a settings screen), parsed fail-closed per
entry with rejects named, and a server's env block - where API keys ride - goes in and never comes
back: listings carry key names only, and a save with env omitted keeps the stored values.
One connection manager per process (src/kit/mcp/connections.ts) spawns each enabled server once,
reuses it across sessions, restarts it when its recipe changes, and kills every child on exit -
sessions must never each spawn their own copy. A server that fails to connect is a named failure in
Settings, never a broken session; a tool call that hangs is an error result the model reads, not a
dead turn; and under the test runner nothing connects at all unless a test opts in by name
(BUNZAI_MCP=on), because session-building tests share the real ~/.bunzai.
External tools wear the name mcp_<server>_<tool> (folded to the belt's alphabet, collisions
dropped and counted) and carry the server's own JSON Schema to the model via schemaOverride -
round-tripping it through Zod would be lossy, and the far server is the real validator. Every such
tool is classified egress BY EXACT ENUMERATED NAME at the moment it joins the belt
(assembleBelt returns the tools and the trust entries from the same breath, so they cannot
drift): the danger tier that confirms at the Gate and can at most be allowed for a session. TheCatalogToolAccess seam deliberately cannot grant write, delete or exec, and an external
name that was never enumerated stays on the unknown floor - deny by absence. Effect is apply,
because "we cannot know what it does" must never skip an ask. The serving side never re-exports
them: bunzai mcp builds from discoverTools() alone, so two Bunzais pointed at each other do
not echo. External results cross the same bounded store as first-party observations: large values
return an opaque handle and peek, while values beyond the store cap are omitted with a named error.
Verification status
Unit and integration proof covers catalog parity, all discovery domains, compact discovery calls,
progressive exposure, eight-kind creation discovery and preview, media schema descriptions, whole-card
reads, bounded oversized result storage, JSON Pointer traversal, complete draft evidence,
read-effect isolation, canonical draft composition, create collision refusal, stale update refusal,
one-save apply, post-save verification, and a full fake-provider rename journey through the real
Studio store.
The lifecycle and transfer surfaces add: a delete that removes nothing on a miss and is classified at
the danger floor, a duplicate that carries original escrow and profiles onto the copy, structural
export containment against hostile ids and invented extensions, create-only export refusal on an
occupied name, and a RoleCall-preset-to-SillyTavern-preset journey end to end through the real
adapter registry onto a real filesystem.
Macro claims are checked rather than asserted. scripts/macro-oracle captures each engine's own
registry into committed fixtures and a parity test proves no catalog claims a macro its engine does
not have. A separate gate proves no shared name diverges in shape without a declared operation.
SillyTavern is the one modeled engine with no oracle, so its annotations rest on transcription alone.
Live terminal verification remains required: none of the above has been exercised in a running Kit
session against a real provider.