MobKitConfig
Top-level configuration for the module runtime.ModuleConfig
RestartPolicy
DiscoverySpec
PreSpawnData
SDK and Gateway runtime options
The stock SDK gateway (rpc_gateway) consumes the runtime_options object of mobkit/init. The Python and TypeScript builders emit it from their setters; where a row names a setter, that is the only way the SDK reaches the key, and a key with no setter on an SDK can be sent only from a hand-built mobkit/init payload. mobkit_gateway refuses a runtime_options object rather than dropping it.
Unsupported nested fields fail initialization with
-32602 instead of being silently ignored.
Model fallback
Automatic model fallback is off by default. An enabled policy requires an explicit, non-empty chain; an empty chain never walks the model catalog. Meerkat owns retry eligibility, target admission, credentials, and committed model routing. MobKit forwards that policy without adding its own retry loop. The host file selected bymeerkat_config_path accepts [model_fallback].
Rust embedders can pass the same meerkat::Config to
UnifiedRuntimeBuilder::meerkat_config. In mob.toml, use
[runtime.model_fallback] for a mob default or
[profiles.<name>.model_fallback] on an inline profile:
enabled = false wins over an
enabled parent. Tables are not merged field by field, so a profile does not
silently borrow its parent’s chain. A realm-profile reference cannot carry a
sibling fallback override; put it on the referenced profile or the mob runtime.
Unknown fallback keys, use_catalog_default_chain, invalid policy values, and
enabled = true without a chain are errors. There is no separate
runtime_options.model_fallback gateway option.
Cross-provider fallback requires explicit cross_provider = true and a usable
authorized credential binding; omission does not authorize switching accounts.
Targets must pass Meerkat’s context, output-budget, tool and modality admission.
Transport failures and empty output are not default fallback triggers.
New fallback provenance can cause an unsafe restored route to be held with a
typed error. Historical sessions without that provenance are not inferred
to be fallback sessions, automatically reverted, or migrated. Existing explicit
operator model/provider resume masks retain their authority.
The host config setter affects builder-created member session services, not an
already-built .mob_spec() service. Native background memory engines currently
use their own default config (also fallback-off); this table does not configure
those engines. This release does not add fallback overlays, automatic reversion,
or a routing-repair CLI.
One-shot mob definition update
Python hosts use the supported builder ceremony on the single activation authorized to change the durable definition:mob_config and emits:
declare_spec_update(...) from
later ordinary restarts. A stale revision or different successor definition
raises Python StorageResolutionError; an exact retry returns the already
committed successor revision and converges its checked projection.
This is a cold-activation ceremony, not a hot reload. Fully shut down the prior
runtime before issuing the update: an already-running MobHandle keeps the
definition it was constructed with. The CAS and verified-resume fence serialize
the update with the next bootstrap, but they do not reconfigure an older live
process.
Self-hosted models and the Meerkat host config
Meerkat keeps three tables out of the mob definition and in the hostconfig.toml: [self_hosted] (serving endpoints and model aliases), [realm]
(backend, auth and binding profiles, including the credential binding every
provider = "self_hosted" member resolves through) and [models] (custom
model rows). A mob.toml profile refers to them by name only:
mob.toml
config.toml
[models.<id>] provider = "self_hosted"
is refused by Meerkat’s model registry: self-hosted aliases live under
[self_hosted.models] only.
[self_hosted] and [realm] belong in the host config, never in mob.toml.
MobDefinition::from_toml deserializes without deny_unknown_fields, so a
top-level [self_hosted] or [realm] table written into mob.toml used to
vanish without a diagnostic and resurface two layers later as a member that
could not build. Both binaries now refuse it at init, on the raw TOML text,
with one message:
-32602 on the mobkit/init request id
(mobkit_gateway when it loads config/mob.toml).
[models.<id>] is different from the two tables above. A profile’s model
must be catalogued, provider-annotated, or defined under the mob
definition’s own [models.<id>] to pass the model check: the check
(meerkat_mob::validate_definition, run at init on rpc_gateway and by
Meerkat at create time on both binaries) reads only the definition’s tables.
So a custom model a profile names by id belongs in mob.toml. A [models]
table in the host config still reaches the factory through
meerkat_config_path (a provider-annotated profile can use such a row), but
it does not satisfy the check, and a profile whose model exists only there and
carries no provider is refused at init as UnknownModel.
How the host config reaches each surface
The loader reads exactly the named file and merges it over
Config::default()
with Config::merge_toml_str, the step Meerkat applies to a discovered
.rkat/config.toml: no directory walk and no home-directory fallback, so what
the gateway loads is what the operator named. Adoption is never silent:
mobkit_gateway logs one INFO line naming the adopted file and whether the
init param or the workspace convention produced it. A file that cannot be read or
does not parse or does not validate refuses init (rpc_gateway: -32602,
runtime_options.meerkat_config_path: failed to read meerkat config <path>: ...,
meerkat config <path> is invalid: ... or
meerkat config <path> does not validate: ...; mobkit_gateway: -32602 on
the request id, init params: meerkat_config_path: ...), because an operator
who pointed the gateway at a file expects that file in force. Validation is
Meerkat’s own Config::validate against the canonical catalog, the step
rkat and rkat-rpc run after every load: a [self_hosted.models.<id>]
alias whose server is not declared, a [models.<id>] row colliding with a
catalog id, or a zero compaction threshold is refused at init instead of
killing the first member build. The gateway’s own
declarations stay layered on top: member_comms_address and compaction on
rpc_gateway, compaction on mobkit_gateway, and a profile’s own
auto_compact_threshold still wins per member.
The init model check
rpc_gateway checks every inline profile’s model before the mob exists, by
Meerkat’s own rule (meerkat_mob::validate_definition, narrowed to its
UnknownModel diagnostic): a model is acceptable when it is catalogued, or
defined under this definition’s [models.<id>], or provider-annotated
(provider = "self_hosted" with a self_hosted_server_id, for one). This is
the rule rkat mob validate documents and MobBuilder::create applies at
bootstrap. A refused model is -32602 with the profile named and, when a
catalog id is close, a Did you mean one of: ...? hint (the common cause is a
dated id such as claude-sonnet-4-5-20250514 for claude-sonnet-4-5). Only
that one diagnostic is adopted at init on purpose: the rest of the set
(missing skill refs, wiring, flows) is Meerkat’s to refuse at create time.
mobkit_gateway has no pre-check; Meerkat runs the same validation when it
creates the mob.
Provider-annotated profiles bypass the refusal. By Meerkat’s rule a
profile that sets provider = "..." is resolvable whatever its model string
says, so a dated or misspelled id on such a profile (every HomeCore profile
carries provider = "openai") passes init and surfaces at the first LLM call.
rpc_gateway does not add a second refusal authority for that case; it logs
one WARN per such profile at init naming the profile, the model and the closest
catalog ids (profiles.<p> model '<m>' is provider-annotated ... a typo surfaces at the first LLM call. Did you mean one of: ...?). A provider = "self_hosted"
alias is checked against the host config’s [self_hosted.models] instead and
warned about only when it is not registered there. Catalogued and
[models.<id>]-defined models warn about nothing.
Earlier releases compared the model string against the static catalog alone,
so a [models.<id>] custom model and every self-hosted alias were refused at
init on rpc_gateway while mobkit_gateway accepted them and then failed at
the first member build with “self-hosted model ’..’ is not registered in
config”; both binaries now agree, and with meerkat_config_path the member
builds.
Member role migrations
role_migrations is a top-level mobkit/init param, a sibling of
runtime_options rather than a field inside it. On rpc_gateway,
runtime_options is a closed allowlist and unknown keys are a hard error, so
nesting the declaration there fails initialization with
unsupported runtime_options fields: role_migrations.
A durable member whose role changed refuses to resume: Meerkat returns
MobError::MemberRoleMigrationRequired rather than silently restamping the
member’s durable role, comms name and binding together. The host clears that
refusal by declaring, for one boot, which exact identity is migrating and which
role it is migrating from.
.role_migrations([...]) accepts RoleMigrationDeclaration dataclasses or
plain dicts and validates both; a conflicting pair raises ValueError; leaving
the setter unused emits no role_migrations key at all. The TypeScript builder
exposes no equivalent setter.
The two gateway binaries, whose names differ by one word, refuse bad payloads
differently and install accepted ones under different conditions.
Declarations that reach neither identity plane arm nothing, because there is no
identity plane to migrate on. See
roster and member lifecycle for the operator
view of the refusal.
Mob profile tools ([profiles.*.tools])
The mob definition (.mob("config/mob.toml"), or the mob_config string of
mobkit/init) is Meerkat’s MobDefinition. Each
[profiles.<name>.tools] table is meerkat-mob’s ToolConfig, and every
boolean in it defaults to false. The table has no unknown-key check, so a
misspelled key (comm = true) is dropped and the flag it meant to set stays
false.
The
mob-communication skill is preloaded for every member regardless of these
flags. Agent-side spawns that inherit the parent’s tool filter open every
category, including comms, before the inherited allow-list is applied, so the
comms refusal is a host-spawn (mobkit/ensure_member) behaviour; an agent
spawn of a comms = false profile silently runs with comms on.
WorkGraph
rpc_gateway constructs a realm-scoped WorkGraphService on every launch
unless runtime_options.workgraph is false (the default is true, so a
launch that never mentions the key has one). The service backs the member
workgraph_* tools, the mob-executor attention overlays and the
mobkit/workgraph/* RPCs.
mobkit_gateway has no switch: its persistent mode attaches the durable store
from the workspace state dir (same degraded posture on open failure) and its
default ephemeral launch is memory-backed.
The namespace grant is issued by the gateway, not the host. Meerkat
refuses to build an agent that has WorkGraph tools and no host-issued
WorkGraphNamespaceGrant. Whenever MobKit installs the tools it takes the
grant from the service it just built, so the grant names the same scope the
tools are pinned to: realm mob.<mob definition id> (Meerkat’s canonical mob
realm, the one every other realm-scoped store for the mob uses; if it cannot
be formed the raw id is used and Meerkat’s own validation names both realms)
and namespace default. Every workgraph_* tool call has its realm_id and
namespace arguments overwritten with that scope and all_namespaces
removed, so a member cannot wander into another mob’s graph. The meerkat
WorkGraph guide’s “embedding hosts must issue the grant in the agent build” is
a Rust-embedder instruction; SDK hosts have nothing to issue. The tools stay
per-profile: a member sees them only when its profile sets
tools.workgraph = true, while the mobkit/workgraph/* RPCs are available
whenever the service exists.
Storage layout and durability
MobKitStorageLayout is the single path authority for storage roots and the
canonical top-level database locators; no gateway or builder surface derives
state-dir file names or resolves ambient roots ($XDG_STATE_HOME, $HOME)
on its own, and a CI gate (tests/storage_gate.rs) keeps it that way.
Feature code owns relative names beneath the layout’s roots (the blob
directory’s internal sharding, per-realm agent-memory files).
Canonical spellings (decided once; stores shared with Meerkat keep
Meerkat’s names, MobKit-owned files converge on *.sqlite3):
Canonical-name-first probing. Opening a slot resolves the canonical name
first, then probes the known legacy spellings in the same directory. Exactly
one spelling → it is used where it lies (no rename at open). Two spellings
of the same store → the gateway refuses to start (error
-32014, “file-name
twins”) and points at mobkit/storage/doctor; physical renames happen only
through the storage migrate verb, under the maintenance fence.
Durability classes and the declaration requirement. Every composed slot
resolves to a durability class (durable / scratch) and a resolution
(persistent / declared_ephemeral / non_persistent). A durable-class
slot may run non-persistent only as an explicit declaration
(runtime_options.runtime_store, runtime_options.event_log, the Rust
builder’s ephemeral_runtime_store(true) / ephemeral_blobs(true)) — never
as a silent fallback; undeclared gaps are startup errors that SDKs surface
as the typed StorageResolutionError (-32014). The sanctioned boot-without
degradations (schedule or workgraph store open failure) disable the feature
and appear as degraded slots.
Storage census surfaces. The resolved picture rides the storage object
on mobkit/status, mobkit/capabilities, and mobkit/storage/doctor:
blob_durability / blob_store_persistent (H1), session_store_incremental
(H2), and the per-slot slots array (domain, class, resolution,
backend, degraded, optional detail).
Changelog policy. Any storage file rename or table move gets the
binary-rename treatment: an explicit, loud changelog entry naming the old
and new spellings — operators read these files directly.
Identity bootstrap
- Rust
- Python
identity_bootstrap_mode setter is an explicit
identity-first configuration and therefore requires a roster provider for all
three modes. Leave the setter unused to retain a classic non-identity gateway;
a configured roster with no mode continues to use eager materialization.
mobkit/status_identity_bootstrap returns a non-blocking typed snapshot.
mobkit/wait_identity_bootstrap accepts target = "materialized" or
"startup_ready"; terminal broken identities return with ready = false
rather than leaving the barrier pending indefinitely. Provider or restore
failures that apply to the whole pass are reported in the snapshot’s optional
error field.
The stock persistent gateway also advertises a private shutdown handshake and
its bounded stdio_shutdown_horizon_ms in the mobkit/init result. SDK hosts
keep stdin and provider callbacks open until that handshake completes, then
close the process. The current 335-second horizon covers an already-admitted
provider callback, runtime event and mob drains, the final lease-release
callback, and bounded response-delivery/reaping margin. Provider operations
must resolve or reject within their public 120-second contract. The Python host
allows 125 seconds for event-loop completion before cancelling that callback;
the TypeScript host exposes the same cancellation as an optional
ProviderCallbackContext and suppresses late callback responses; the gateway
enforces a final 130-second hard wire deadline. Provider rejection,
timeout, or cancellation is pre-commit: an implementation must not commit a
replacement grant or other external authority after cancellation is observed.
Older and custom gateways that do not advertise the handshake retain the EOF
shutdown protocol.
If the gateway exhausts its runtime deadline, it reports incomplete cleanup;
the SDK still reaps the bounded child process and then surfaces the failure.
Rust hosts that dispatch JSON-RPC from an Arc<UnifiedRuntime> should call
handle_unified_rpc_json_arc (or the corresponding with_live_arc entrypoint).
That transfers runtime ownership to supervised identity mutations, so a
disconnected request cannot cancel a cross-mob operation between its commit
and rollback points. The borrowed dispatcher fails identity-owned cross-mob
mutations closed because it cannot provide that ownership guarantee.
mobkit_gateway identity roster
identity_roster is a top-level mobkit/init param of mobkit_gateway
(typed InitParams), the standalone counterpart of the SDK roster provider
that rpc_gateway reaches through .roster(...) callbacks; rpc_gateway has
no identity_roster key. It is an array of DurableAgentSpec objects and seeds the
desired identity roster for identity_first: true (the default):
At boot the seed becomes the
MutableRosterProvider and restore_flow runs
over it: the roster is validated, continuity is resolved for every listed
identity, and each non-Broken identity is created or resumed. Omitting the
param is an empty seed, so the gateway starts with no identities and
mobkit/ensure_member adds them at runtime (it upserts the same provider). The
seed is boot-scoped like role_migrations: it is never persisted, and an
identity missing from the next boot’s seed is not restored on that boot (its
continuity record is untouched). Under identity_first: false the param is
parsed and ignored, not refused.
ConsoleUiConfig
View-level configuration for the stock console.mobkit_gateway discovers config/console.toml in the conventional workspace layout; rpc_gateway accepts the same file via runtime_options.console_config_path, and the TypeScript builder exposes .consoleConfig("config/console.toml").
The config is normalized and projected through GET /console/experience as console_config.
Agent grouping selectors and badge fields support
labels.<key>, label:<key>, raw label keys, and direct fields such as group, subgroup, role, kind, identity, member_id, and agent_id.
ConsoleUiConfig is intentionally view-level. It controls presentation, ordering, labels, default pins, and links; it does not decide runtime authorization, routing, or whether an agent exists.AuthPolicy
AuthProvider
ConsolePolicy
For standalone
mobkit_gateway launches, pass console_read_only: true in the
mobkit/init params or set MOBKIT_CONSOLE_READ_ONLY=true. This setting is
part of the gateway resume fingerprint, so toggling it creates or resumes the
matching runtime instead of reusing a runtime launched with the opposite policy.
mobkit_gateway takes the HTTP exposure knobs as top-level mobkit/init
params rather than runtime_options: http_listen (HOST:PORT, default
127.0.0.1:0; also --http-listen or MOBKIT_HTTP_LISTEN_ADDR, the init
param winning), allow_remote (also --allow-remote or
MOBKIT_HTTP_ALLOW_REMOTE=1) and http_public_base_url. Its console is
always open, so a non-loopback http_listen is refused unless allow_remote
is set. Every init-param refusal on this binary (http_listen parse, the
exposure gate, http_public_base_url, meerkat_config_path, compaction,
a [self_hosted] table in mob.toml) answers -32602 on the request id, and
a listener bind failure -32603 on the request id. The listen address and the
advertised base both join the resume fingerprint like --control-listen, so a
relaunch that changes either creates a runtime with the declared values instead
of reporting the previous launch’s. The mirror rule holds on rpc_gateway: the
same four keys sent top-level there are refused with -32602 naming their
runtime_options.* home. See Deployment.
Console auth by surface
require_app_auth defaults to true, and the SDK gateway’s decision state
without auth_config trusts no signing key, so the default console is closed
to every caller (each request answers 401 missing_credentials or a token
failure). Opening it is an explicit, per-surface choice:
An open console is a deployment decision. Use it only behind a loopback bind
or a reverse proxy that authenticates; for anything shared, configure
auth_config (Python .auth(...), TypeScript .auth(...)) instead.
RuntimeOpsPolicy
LocalJsonMemoryBackendConfig
For SDK builds that boot through
rpc_gateway, state_path is derived from .persistent_state(path) as <path>/memory-ledger-state.json; callers provide only backend: "local_json" (and optionally health_check_endpoint). The legacy ElephantMemoryBackendConfig / backend: "elephant" shape is deprecated: it was always local JSON persistence plus a health check — no data ever reached Elephant — and it converts to this config on load. Real Elephant integration is the wire-boundary provider described in docs/design/memory-hub-roadmap.md.
Ownership of the assertion ledger. The file is a snapshot of runtime
state, not a log that other writers may append to. MobkitRuntimeHandle loads
it exactly once at bootstrap and keeps assertions and conflicts in memory as
the authority; every mobkit/memory/index (host calls, and the Steward’s
conflict signals when agent_memory.steward is enabled) serializes the entire
in-memory state and atomically replaces the file, with no re-read, lock or
modification check. A direct edit is therefore invisible until the next boot
and overwritten by the next flush; single-writer is a convention MobKit does
not enforce with a lock. Retention is capped at 4,096 assertions, oldest
evicted first, at index time and at load. At load, a structurally malformed
file (an entry missing assertion_id, entity, topic, store, fact or
indexed_at_ms) refuses the boot with MobkitRuntimeError::MemoryBackend,
while a well-formed entry whose entity, topic or store is not canonical, or
whose fact is empty, is dropped without a diagnostic. Append through
mobkit/memory/index, Python MobHandle.memory_index(...), TypeScript
handle.memoryIndex(...), or Rust MobkitRuntimeHandle::memory_index /
UnifiedRuntime::memory_index; there is no agent-facing tool, so a host that
wants members to write exposes one over the handle. This ledger is distinct
from the agent_memory injection and turn-surface ledgers below.
Agent memory injection
agent_memory is separate from mobkit/memory/*. It is hot identity-scoped memory plus prompt injection: during create/resume/materialize and ordinary identity-first turns, MobKit asks an AgentMemoryProvider for memories keyed by AgentIdentity and injects a bounded, quoted observation summary. With the current Meerkat 0.8.32 pairing, ambient per-turn memory requires runtime_mode = "turn_driven"; remove this restriction after the Meerkat 0.8.33 carrier change lands. The bundled gateway provider is one SQLite database per realm under <persistent_state>/agent-memory (store: "sqlite", the only live gateway store). The earlier per-identity markdown files are import-only: store: "markdown" is refused at init with a migration verdict, and pointing SQLite at the same directory imports a realm’s un-imported markdown files when that realm is first accessed and its SQLite connection is opened, preserving ids, tag content and timestamps and renaming each original <file>.imported rather than deleting it (tags are collated on insert, so tag ORDER is not preserved - assert on the set, not the sequence). The path is server-owned and cannot be overridden from SDK init input. Elephant remains an optional deeper knowledge backend for graph/document/truth/provenance use cases and is not required for this hot path.
With the SQLite store, build-time injection also composes a metadata index (record ids, kinds, titles, descriptions, human-phrased age) over the identity’s readable scopes, budgeted at ~8 KiB, ahead of the selected record bodies. Every injected record is logged to an injection ledger inside the realm database and counted in per-record usage statistics; explicit mobkit/agent_memory/recall reads are counted separately from ambient injection.
Inbound anti-spoofing is on by default (defang_inbound: true): every non-steer identity-first send is scanned for reserved memory-envelope markers (the injection header pattern, <mobkit_memory_observation> tags, [mem-token: prefixes) and matches are visibly neutralized before delivery, so a peer message or echoed web content cannot impersonate remembered context. A warning with a hit count is logged whenever defanging fires. Steer sends bypass injection and defanging entirely.
Rust builders can use .persistent_agent_memory_stack(config, engines) with .persistent_state(path) to install the full SQLite stack (store plus taint firewall plus the enabled judgment-plane engines). Callers that need a different authority use .agent_memory(provider, config). The former .persistent_agent_memory(...) markdown constructor is removed; legacy markdown is accepted only by the one-shot SQLite importer.
Gateway callers can write records with mobkit/agent_memory/remember, read them with mobkit/agent_memory/recall, and delete individual records with mobkit/agent_memory/forget. TypeScript callers can use handle.rememberAgentMemory(identity, { title, body, tags, realm }), handle.recallAgentMemory(identity, { realm, selection, queryText, queryTerms, maxEntries }), and handle.forgetAgentMemory(identity, memoryId, { realm }); Python callers can use handle.remember_agent_memory(identity, title=..., body=..., tags=..., realm=...), handle.recall_agent_memory(identity, realm=..., selection=..., query_text=..., query_terms=..., max_entries=...), and handle.forget_agent_memory(identity, memory_id, realm=...). Writes and deletes are identity-scoped, require the identity to be registered in the identity runtime, and writes become turn context on the next identity-first send, plus build context on materialization/resume/respawn/reset for that identity. A successful write means the configured hot provider accepted the record; it does not mean optional Elephant extraction/enrichment has completed. Bundled SQLite writes enforce the staged-memory validator’s record limits. The retired markdown parser is used only while importing existing files.
When selection = "always", the latest memories for the identity are injected. When selection = "contextual" (the default), build-time injection sends synthesized identity/profile/label query text and deterministic terms; per-turn injection sends the raw current identity-first message plus normalized terms. Common stopwords are ignored by the bundled provider and records below the relevance threshold are not injected. MobKit does not send app context or customizer-added instructions to the memory provider. Semantic/vector retrieval remains a provider concern rather than a MobKit runtime authority.
Per-turn injection is governed by per_turn_injection. When omitted, the gateway derives the effective policy from the selected memory backend’s library default: "budgeted" for the bundled SQLite store and "off" for the legacy, import-only markdown kind. Set "off" explicitly to disable ambient push. Selected memories travel as typed injected context immediately before the conversational input, rather than being fused into the user message, and Meerkat excludes that role from semantic-memory indexing. Budgeted injection applies an injection ladder: at most ~4 KiB rendered per record, 20 KiB per assembly, and 60 KiB cumulative per delivered session, with records offered at most once per session (cross-turn dedup). The turn-surface ledger records that preparation-time offer; the transcript’s typed injected-context append records delivery at activation. Build-time injection always applies the per-record and per-assembly caps.
Automatic build/turn injection uses recall_timeout_ms (default 500, max 30000) and recall_failure_policy (default "skip", or "fail"). With the default skip policy, provider errors and timeouts omit the memory block and allow delivery to continue. Explicit mobkit/agent_memory/recall requests still return provider errors.
Agent memory is identity-scoped and is not automatically cleared by session respawn or identity reset. Apps that require privacy reset semantics can call mobkit/agent_memory/forget for specific records or use an AgentMemoryProvider whose lifecycle policy clears memory alongside the app’s identity lifecycle. forget removes records from future recall and injection; it does not erase text already delivered into an active model context, so reset or respawn the identity after deletion when immediate context revocation matters.
BigQueryNaming
ReleaseMetadata
build_runtime_decision_state parses and validates this document on every
build, with or without console auth; nothing reads it at serve time, so the
check is release governance, not runtime policy. RuntimeDecisionState::local_console
fills the canonical value. A malformed document is a DecisionPolicyError::TomlParse
whose message names the release metadata JSON.
Trust manifest (mobkit.toml)
Constants
See also
- Modules — module configuration in context
- Authentication — auth policy usage
- Sessions — session store configuration
