Safety classification
Every built-in MifBridge endpoint has one classification row that says whether it reads or writes, how undo treats it, whether the safety gate refuses it, whether it previews, and which requests to it write nothing. Read this to understand why a call was allowed or refused, and how to ask an endpoint for its row.
One row, read by everything
The plugin keeps exactly one row per built-in endpoint, sorted by name. The safety gate, read mode, asset locks, the change cap, describe_endpoint and self_audit all read that one table, so they cannot disagree about an endpoint. A row looks like this:
delete_level_actor Write Blanket Open NoPreview "actorPaths" "dryRun"
list_bones Read NoTxn Open NoPreview "" ""
save_package Write NoTxn Gated NoPreview "" ""
Ask any endpoint for its row with describe_endpoint. It answers a safety block:
safety {access, transaction, gate, previewable, capKeys, readForms, writeModes}
readOnly in that answer means the call does not write (access is not Write). It does not mean the call needs no undo entry.
The columns
| Column | Values | Meaning |
|---|---|---|
| access | Read, Observe, Write |
what a call does to things you keep. Read changes nothing (it may load assets and make transient objects). Observe changes only transient editor view state (camera, selection, open tabs, debug drawing, a capture or trace session) or writes an output file the project does not load (a screenshot, an export, a recording). Write is everything else: assets, actors, levels, settings, config files, a compiled class, a Play-In-Editor world, a child process. When in doubt, a row is Write |
| transaction | NoTxn, SelfTxn, Blanket |
the undo policy: no transaction, the handler opens its own, or one transaction around the whole call. This is kept separate from access on purpose: save_package is a Write with no transaction |
| gate | Gated, InHandler, Open |
Gated endpoints are refused by name in scratch and read mode, because they persist to disk where no path check helps, take the editor loop, or run outside the process. InHandler endpoints apply a write-mode rule themselves because the write depends on a parameter (for example set_cvar with persist, set_property with saveConfig, or a file path confined to the project). Open endpoints are not refused by name |
| preview | Preview, NoPreview |
takes the bulk preview handshake: dryRun, then previewId, then commit |
| capKeys | key names or none | for a direct multi-target write that has no preview: the request keys whose entries each count as one target for the change cap. A list key counts its entries and a string key counts one |
| readForms | request shapes or none | the shapes of request that make a Write endpoint write nothing |
The full list of gated endpoints is in Safety and write modes.
Read forms
Some endpoints write in one form and only read in another. A request that matches a read form is treated as a read by the gate, read mode, asset locks and the change cap. A form is a comma-separated list of conditions that must all hold, and | separates alternatives:
| Condition | Matches when |
|---|---|
key |
the key is a JSON true |
!key |
the key is absent, null, false, an empty string or an empty array |
key=value |
the key is that string, ignoring case. A JSON boolean sent with that value also matches. An absent key never matches |
dryRun |
any spelling of dry run the endpoint accepts |
Examples of forms in the table: set_skin_weights is a read when read is set and weight is absent. evaluate_dataflow evaluates by default, so its form is !set,evaluate=false. set_widget_navigation is a read without rules. import_asset previews with preview, and localization_run polls when op is absent.
What each consumer does with a row
| Consumer | Reads | Rule |
|---|---|---|
| undo transactions | transaction | NoTxn endpoints run without one, so a read leaves no empty undo entry |
| safety gate | gate, access, readForms, preview | In scratch: refuse a Gated row unless the request is a preview or matches a read form. In read: also refuse every Write row that is not a preview or a read form. In full: nothing by name |
| asset locks | access, readForms, preview | only a write request is checked against another session's locks. Reads are never refused by a lock |
| change cap | capKeys, preview, readForms | a direct write with capKeys is counted and refused over the cap, with the same message batch uses |
describe_endpoint |
every column | reported as the safety block above |
self_audit |
transaction, gate, access | each row reports its bucket and access, and unclassified[] lists a registered built-in with no row, which also makes healthy false |
batch |
gate, preview | each inner op is judged as a direct call would be |
In read mode, batch is the one Write row that is not judged whole, because it writes only through its ops. Each op is checked before any runs. A batch of reads runs. A batch holding one write runs nothing and names that op.
If a registered built-in somehow has no row, the dispatcher treats it as Write, Blanket and Open: the strictest reading for read mode and locks, and the undo policy every unlisted endpoint had before.
Extension endpoints
An endpoint registered by an extension has no table row. Its descriptor already declares a bucket, a safety and change-cap keys, and the lookup builds its row from them. Access is Read when the bucket is ReadOnly and the endpoint is not marked Unsafe, otherwise Write. The transaction comes from the bucket. The gate is Gated when the endpoint is Unsafe. capKeys come from its ChangeCapKeys. It previews when it registered a planner.
What the classification means for your work
- Asset locks no longer refuse reads. A pure read, such as
list_bonesordescribe_state_tree, runs even when another session holds a lock on the asset. The lock check is about writes. - Reads run without an undo entry. They no longer add an empty transaction per call.
readmode refuses every write. A Write row is refused withrefusedRule: read-modeunless the request is a bulk preview or matches a read form. Read and Observe rows run.- The gate covers writers that used to slip past it.
package_project(unless it only reads),migrate_assets,localization_run,localization_target,editor_utility_startup,add_water_collision_profileandexport_mocap_animationare gated. - A texture endpoint's save flag is gated inside the handler. On
import_texture,set_texture_settings,reimport_asset,set_asset_thumbnailandwrite_thumbnail_texture, an explicitsave: trueis refused inscratchandreadmode. A save the endpoint would make by default is skipped there, and the reply says so (saveSkipped,saveSkippedBy, a warning). Refusing the default call would have refused every texture import inscratchmode for a save nobody asked for. - Some reads that sit behind write names are read forms. Examples:
paint_mesh_texture_colors,apply_clothing_data,fracture_geometry_collection,edit_metahuman_instance,build_metahuman_collectionandbuild_metahuman_identitywithread, andset_world_partition_runtime_hashwithdryRun. - No-write dry runs pass the gate.
import_assetwithpreview(anddryRun, its alias),import_vdb_as_volume_texturewithdryRun,invoke_editor_commandandsend_editor_keywithdryRunand noconfirm, andrender_sequencewithdryRun(an alias ofplanOnly). - Direct multi-target writes are capped. Through capKeys:
delete_level_actor,set_property_many,set_actor_transform,set_actor_label,set_actor_editor_hidden,remove_component,rerun_actor_construction(actorPaths);set_collisionandactors_to_instances(actors);set_user_asset_tagsandmigrate_assets(assets);create_assetandimport_texture(items);set_texture_settings(paths);edit_mesh_instances(instances). A selection made by filter (classFilter,nameContains,folder) has a size only the handler knows, so those handlers count the resolved set after the selection and before the first write.set_property_manyneeds nothing extra: its actors, however chosen, become onebatchof ops, andbatchpreviews over the cap. - Actor addressing is forgiving. Where an endpoint declares only
actorPath, a request that sendsactoris read asactorPath(andactorsasactorPaths), and the other way around. The reply sayskeyAliased.edit_skeleton_bones,split_skeletal_meshandset_skin_weightstakepathformesh.
When a classification is wrong
| Case | Effect |
|---|---|
| a writer classified as a read | read mode and locks would let it through. The default for a doubtful row is Write, and the scratch watch still reports every real package a call dirtied and refuses, after the fact, a write into a protected root |
| a read classified as a write | read mode refuses a read and a lock refuses a reader. That is annoying but safe, and the refusal names the row |
| a read form that writes | the gate and read mode would pass a write. Each form is added only after reading the handler's branch for that shape, and the forms are few |
Some cases are deliberately left as they are:
- The
allforms ofreapply_foliage_type,unmap_input_key,set_skin_weights,delete_material_expressionandset_mesh_section_flagseach change one asset (a foliage type, an input mapping context, a mesh, a material), which the change cap counts as one target however many of its parts change. set_pie_actor_hiddenwrites only the running play world, which is not kept.ai_teamtakes a list ofactorsonly for its read action. Writing takes one actor.read_datatablewithfilewrites an output file the project does not load, so it is Observe, with its file confined to the project outsidefullmode.