DOCUMENTATION

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_bones or describe_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.
  • read mode refuses every write. A Write row is refused with refusedRule: read-mode unless 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_profile and export_mocap_animation are gated.
  • A texture endpoint's save flag is gated inside the handler. On import_texture, set_texture_settings, reimport_asset, set_asset_thumbnail and write_thumbnail_texture, an explicit save: true is refused in scratch and read mode. 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 in scratch mode 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_collection and build_metahuman_identity with read, and set_world_partition_runtime_hash with dryRun.
  • No-write dry runs pass the gate. import_asset with preview (and dryRun, its alias), import_vdb_as_volume_texture with dryRun, invoke_editor_command and send_editor_key with dryRun and no confirm, and render_sequence with dryRun (an alias of planOnly).
  • 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_collision and actors_to_instances (actors); set_user_asset_tags and migrate_assets (assets); create_asset and import_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_many needs nothing extra: its actors, however chosen, become one batch of ops, and batch previews over the cap.
  • Actor addressing is forgiving. Where an endpoint declares only actorPath, a request that sends actor is read as actorPath (and actors as actorPaths), and the other way around. The reply says keyAliased. edit_skeleton_bones, split_skeletal_mesh and set_skin_weights take path for mesh.

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 all forms of reapply_foliage_type, unmap_input_key, set_skin_weights, delete_material_expression and set_mesh_section_flags each 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_hidden writes only the running play world, which is not kept.
  • ai_team takes a list of actors only for its read action. Writing takes one actor.
  • read_datatable with file writes an output file the project does not load, so it is Observe, with its file confined to the project outside full mode.