The bind kernel
At the centre of the feature is a bind kernel that builds the live tool: it takes a base tool and fixes some of its arguments, producing a hidden transform of the original. The baked values fill in behind the scenes, so a caller supplies only the arguments that were left open. Two optional jq maps reshape the call on its way through: an input map builds the base tool’s arguments from what the caller sends, and an output map reshapes the base tool’s result (see Input and output maps). The base tool is any registered tool that is neither a preset nor derived from one — a management operation projected as a tool included, so you can save a configured variant of a live-ops action just as you would any other tool. A tool derived from a preset is an extension branch the preset produces (such as<preset>_<extension>); the platform knows each branch’s origin, and create
refuses it with a 400 naming the preset it comes from. A stored record whose base
is a preset or a preset’s branch is quarantined at load, whatever order the
records load in.
The store view
A preset persists through a typed store view over the platform’s generic versioned-document store, taggedkind="preset". Every preset is a versioned,
store-backed record — there is one tier, not two. Create writes a durable store
row and then registers the live tool; every save appends a new version, one version
is active, and you can roll back to an earlier one. A preset always rides the
versioning spine.
The contract owns the PresetStore protocol and the PresetBody model; the
skeleton owns the concrete view and the bind kernel.
Across the fleet
A preset mutation lands locally — the store write plus the live rebind — and then broadcasts on the worker bus so every worker sharing the manifest re-reads the active body and rebinds. In a single-process deployment the local apply is the whole fleet. The store write and the local rebind have already landed by the time the broadcast runs, so the response is the plain preset record; a preset broadcast that does not converge is not embedded in that response but surfaced server-side as a loud non-convergence ERROR log that names the workers the change did not reach —departed, timed_out, or missing. Re-run the mutation,
or a fleet reload_config, to reconverge the named workers.
Conflicted records
A preset whose stored body can no longer bind — its base tool or an extension it names was removed since it was authored — is quarantined rather than dropped. It stays visible in the listing withconflicted: true and a human-readable
conflicted_reason explaining the cause (null when the record is healthy). A
conflicted record is delete-only: save-version, rollback, and rename all reject
it with a 409, and only delete clears it (a hard store removal that touches no
registration). This field is the only place the cause is reported — resolve a
conflict by deleting the record and re-creating it once its base is back.
A preset is quarantined when its base tool’s MCP server or an extension it names is
removed. Unpicking its base tool from the server’s selection is refused instead (a
409 naming the preset; see Unpicking a used
tool), so a pick never quarantines a preset.
A stored preset’s name is never mounted as an MCP tool, on any worker, at boot or on a
reload: the server’s advertised-tools row for that name reports preset_conflict as
its unbound_reason. A preset refused because “its name is occupied by an existing
tool” collides with a tool outside MCP (a manifest or plugin tool).
Plugin-shipped defaults
Beyond the tools, extensions, and agents a plugin can carry, a plugin may also ship default presets — preset definitions it declares in code, which the platform seeds at boot. A declared default is created when it is absent and upgraded in place when the plugin’s shipped definition changes across releases, so a fresh install and an upgrade both land the current default without any operator step. The seeding is one-directional and deferential: a seeded default stays plugin-owned only while it is untouched, and the first time an operator edits it the preset becomes operator-owned and the plugin never overwrites it again. A seed likewise fills a default’s display name, tags, or folder only where the organization overlay leaves them unset, never over an operator’s own label.Baked values are a may-run boundary
A preset’s bakedfixed_kwargs are a may-run boundary, not a may-read one.
They are hidden from framework-manufactured surfaces — the tool input schema and
validation-error messages — so a caller authorized only to run the preset does not
see them through those channels.
Every preset read door — get a preset, get a version, list the versions, the MCP
read tool, and tai presets show / tai presets versions — masks any value baked
under a secret-typed parameter of the base tool: a leaf the base tool’s input
schema types as a SecretStr, a format: password string, or a write-only field.
A literal baked under a non-secret parameter is returned as authored, so the
may-run caveat below still stands for non-secret sensitive config. An !ENV marker
is left verbatim — it is a reference, not a
stored credential — and the backup export stays verbatim. When the base tool cannot
be resolved (its plugin is absent), the read fails closed and masks every baked
value.
Secret references in baked kwargs
Afixed_kwargs string value written !ENV ${VAR} is a secret reference, not a
stored credential. It is the same !ENV marker a manifest value and a Studio secret
field carry — referenced, not embedded
— sitting at a baked kwarg:
examples/presets/secret_reference_preset.yaml
!ENV ${SERVICE_API_TOKEN}, not the token.
The bind kernel resolves each reference once, from the server’s environment, as it
builds the live tool: it reads VAR and bakes the value wrapped so the framework
masks it wherever a run is recorded — traces, results, error text. The resolved
value reaches only the base tool’s own call, never a stored surface.
A reference may sit anywhere in a baked value — a top-level kwarg, a list element, a
field of a nested object, or a dictionary value. The bind kernel walks the base
tool’s own input schema alongside the baked kwargs and reveals the resolved value to the
tool by the tool argument binding rule
(a wrapped value as a tool argument):
revealed wherever the schema types that position with a concrete leaf type (a str field, a
list[str] element, a typed model field, a dict[str, str] value, a union whose every branch
is typed), and kept wrapped — masked wherever the run is recorded — wherever the schema leaves
the position open.
An unset referenced variable never binds silently. A write that references a
variable the server does not have fails loudly with a 400 naming the variable and
the kwarg, and a stored preset whose variable is absent at boot is
quarantined rather than bound empty, with the missing
variable named in the boot log.
A reference may carry a default — !ENV ${VAR:fallback} — used when the variable is
absent. A default is opt-in non-secret config: it is stored, versioned, and exported
in the clear like any other baked value, so never write a credential as a default.
In the Studio, the create-preset and save-version dialogs author these references as
typed rows that pick an existing environment key — see the
fixed kwargs editor.
Managing presets
Create, version, roll back, and rename presets from the CLI:validate— a dry-run that reports whether a draft would be accepted, running the exact pre-write checks the create (new name) or save-version (existing name) route runs, without writing anything. A malformed request is a400; a valid or invalid draft is both a200carrying avalidverdict.referees— the holders preview: every live holder the givenaction(renameordelete) would be refused on, the same gather that action’s gate reads, so a UI (or you) can preflight it. The list is the full union: the other presets whose authored-agent composition names this one as a tool, plus every behavioral holder that wires against the name — a schedule that fires it, a hook that targets it, a conversation route pointing at it, a tool-extensions map entry carrying it, a parked interaction whose resume continuation is it, and any plugin-held reference a plugin declares through the rename-referee seam. A rename or a delete also moves or removes every extension branch tool the preset produces, so the union covers references to those branch names too (a preset composing<preset>_<extension>, a schedule firing it, and so on); a holder of a branch names the branch. Each holder is one entry: itskind, the featuretagits own doors are registered under, itsname, thelocatorthat places the reference inside it (a version, a node id), and a one-linesummary. The list is the caller’s view (see Holders a role cannot read).rename— a preset’s name is its live tool name, so a rename rebinds the tool (new name bound before the old is torn down). Rename integrity is enforced atomically at this one gate: it runs create’s name pre-checks on the new name and blocks with a409listing every holder — the same full referee union above (referencing presets and behavioral wiring alike) — so you update those references first rather than have the rename silently strand them. It also rejects a conflicted record (409) and a no-op rename (400). Display metadata is not a behavioral reference: a tool’s organization overlay follows the name across a rename rather than blocking it.delete— removes the preset and each extension branch tool bound off it. The delete, its preview and every delete referee read all of those names, and the delete is refused with a409listing every holder of any of them. Once nothing holds them, every registered delete cascade runs for each name, and then the record is removed. The delete reads no preset body, so it works on a record the reference walk cannot read.set-version-tags— replace one version’stagsannotation. Tags are labels on an immutable version body, so this never rebinds the live tool.
Holders a role cannot read
Every holder list shown to a caller — the rename, delete and detach previews and refusals, the unpicking refusal on a manifest write, and the stored-object delete refusal — shows each holder by name only when the caller’s role may read the holder’stag. The
holders of one kind and tag the role cannot read come back as one count holder in
their place: its kind and tag, name set to the tag, a locator of only {"count": n},
and the summary <kind>: <n> not readable with your role, for example
schedule: 1 not readable with your role. A refusal that holds such a count ends by naming
who can clear it: ask someone with access to 'schedules' to remove those references first
when every holder is counted, or remove those references first; the others need someone with access to 'schedules' when some are named. An admin, and every caller when access
control is off, reads every holder by name; with access control on and no caller bound,
every holder is counted. The gate refuses on the full list either way: a count holder blocks
the action as the holders it stands for do.
Plugin referees
A plugin that holds references to a tool name registers a rename referee withtai42_app.tools.register_rename_referee and a delete referee with
tai42_app.tools.register_delete_referee, both when its module loads. A referee is a
coroutine called once per check with a frozenset of every tool name the action moves or
removes — the preset and each extension branch tool bound off it — and it answers a dict
from asked name to the list of ReferenceHolder entries holding that name. A name it holds
nothing on is left out or maps to []; a key that was not asked, or any other answer, fails
the action. A referee only reads: the preview calls it too. Every ReferenceHolder carries
the tag of the feature its holder lives under, the tag a role’s grant map names, so the
caller’s view can count the holders that role cannot read:
Every stored reference is checked at write
Every write door refuses a body that names a tool or a stored template the deployment does not serve: create, save-version, rollback and validate of a preset, as well as the hook, schedule, conversation-route, per-target config and private-tool-set doors. The refusal is one400 naming every unresolved reference, one line each, such as
base tool 'ghost' is not a registered tool,
tool_kwargs.tool_names entry 'ghost' is not a registered tool (an agent’s tool list) or
stored id 'prompts/missing' at tool_kwargs > params[1].system_prompt is not a stored template. An inline preset inside fixed_kwargs is walked as well.
A base tool whose kwargs compose other tools declares a tool_refs extractor through the
tools facet. The extractor is called with any kwargs blob its base receives and returns
ComposedToolRef entries; each entry’s site (the base tool’s own words for where the
name sits) is printed after the name, as in composed tool 'ghost' at tool_kwargs > step 'fetch' is not a registered tool. A blob that names no tool answers no entries; a blob
of the wrong shape raises a value-free ValueError, answered as a 400. Any other fault
in the extractor is a 500 naming the base tool.
A stored body the walk cannot read answers stored preset '<name>': <fault> as a 500 on
every door that reads it, with code stored-preset-unreadable and preset the row’s
name in the body, until the row is deleted.
Per-base-tool write validators
Those “pre-write checks” are extensible per base tool. A base-tool plugin can register a validator against its ownbase_tool name so that every preset written
over it is checked before it persists — the pinning preset for the
external-confirmation ask,
for instance, is the kind of constraint such a validator can enforce.
A plugin registers one when its tool module loads:
base_tool
on every write — create, save-version, and rollback — and the validate
dry-run door runs the same check without persisting. The registry is rebuilt on
each start(), so a reload re-imports the tool modules and re-registers cleanly;
registering the same base_tool twice within one load raises loudly rather than
silently swapping a base tool’s write gate.
Declarable per-tool input schema
A preset can declare the exposed named tool’s own typed input schema when its base tool supports one — a base tool registers support for it at load, the sameregister_*-at-load pattern as a write validator. When it does, the bind kernel
routes the caller’s validated object into the base tool’s invocation. Most base
tools declare no support: their typed schema is fixed. The shipped one that does
is sandbox_exec — see the
sandbox concept.
The declared input_schema is part of the versioned body, not a create-only
field: it is set at create and updated through the save-version door just like
the baked kwargs, so a schema change is a new version you can roll back to an earlier
one.
When a preset bakes the base tool’s payload argument itself, that baked object is
treated as a set of partial defaults rather than an absolute constant: the
caller’s validated object deep-merges over it, caller-wins-per-key — nested
objects merge recursively, while a list or scalar the caller supplies replaces the
baked one (lists never concatenate). So a preset can pin default fields while still
letting a caller override any of them. Every other baked kwarg stays a fixed, hidden
constant.
A preset’s extensions branch the preset itself. A branch over a preset with an
input_schema (and so one with an input map) takes the schema’s properties as its named
arguments and runs the preset: its validation, its payload routing or input map, its fixed
arguments and its output rules all hold on the branch. Such a schema must be closed
("additionalProperties": false), name its properties as identifiers, and use no root
combinator or $ref; otherwise the write is refused with input_schema cannot be presented to the preset's extensions: <reason>. A missing or unknown argument on the branch reads the
branch’s own argument error; a wrong value reads the preset’s caller input does not match the preset's input_schema: ….
Input and output maps
A preset version’s body may carry two optional jq programs,input_map and
output_map. Each is a jq text, or a TemplatedText rendered to one. Both are part of
the versioned body: set at create, changed or cleared on a new version, and checked on
every write, so a map that does not compile is refused before anything is stored.
- The input map turns the caller’s object into the base tool’s arguments.
.is the caller’s input, validated first against the preset’sinput_schema; an input map needs that schema, because the schema describes what the caller sends, and a write with an input map and no input schema is refused. The stored fixed kwargs are readable as$fixed. The map must produce an object. The fixed kwargs are then merged over it, and a key both set is refused (preset '<name>' input map sets <key>, which the preset fixes; a fixed value cannot be overridden), so a fixed value is never overridden by the map. - The output map reshapes the base tool’s result.
.is the result, and the map reads no variables. The preset’soutput_schemathen validates what the map produced, or the raw result when there is no map.
output_schema when one is set, no output schema when
an output map reshapes the result without one, and the base tool’s own schema otherwise;
its advertised input schema is the authored input_schema.
A result the base tool returns as an error passes through untouched: no map and no schema
runs over it. A preset over a base tool whose call fails returns the base’s error envelope
(isError: true) to an in-process caller, as a direct call of the base does. An output map
never runs over a result that carries a secret: the call is refused (see
secret values). An agent’s call of a preset runs the preset as
every other door does, with its maps, its schemas and its fixed arguments.
A call that parks on an ask is mapped where its result reappears: the output stage
applies to the result a resume hands back, at every level of a nested run, so a caller
reads the same mapped value whether the ask was answered live or parked and resumed. A
stage that fails on resume ends the run FAILED, where the same failure on a live call is
a tool error.
State binding on a version
A preset version’s body may carry astate_binding — the door-side binding that
injects an input-purpose template jq’s value before the tool runs and applies an
update-purpose op batch after it. The update jq reads the preset’s output, so with an
output map it reads the mapped result. The binding is part of the versioned body, so it is
validated and attached on every write — create, save-version, and rollback alike. A rollback
is not a bare re-point: it validates and attaches the target version’s binding exactly as create
and save-version do. Attaching is attach-on-use and idempotent, so a template that was merely
detached since the version was authored — but whose document still exists — is re-attached
automatically and the rollback succeeds. The rollback is refused and nothing is re-pointed
only when the named template document no longer exists (it was deleted, a 404) or its
attach path is occupied (a 409) — re-create the template document first (a merely detached
template needs nothing). A save-version that carries the binding forward attaches nothing: the
carried binding was checked at its own save. The statuses every binding refusal answers are
listed under saving a binding.
Admin registration tier
A base tool can declare the authorization tier that fences it. Declaring the adminfenced (or secret) tier gates both authoring a preset over the base tool —
enforced at the shared preset chokepoint on every authoring door — and running
it, so a preset authored over a fenced base inherits the fence at run time. The
default stays the presets’ own write for a base tool with no declaration;
sandbox_exec declares fenced, because registering and running a runnable code
tool is an admin act. See fenced tools and the
sandbox concept.
See the create-and-version-a-preset guide
for the full workflow, the versioning spine for the
store presets ride on, and the CLI reference for the
complete tai presets command set.
