Tune the reward, catch the collapse, and keep the same tripwires armed when the policy runs on the arm.
Where tools like Rerun and Foxglove give you read-only observability, OmniLoop lets you write back: halt on exception, mutate live variables, step the loop, and replay the trace — without restarting the target process. The same tripwires, limits and black box follow a policy from a training run in sim onto the machine that runs it; what changes is what a halt is allowed to do.
Four packages share one name-space, so pick by the language your loop is written in. They ship in lockstep — the same version number across all of them is always a compatible set.
| Your loop is… | Install | Import |
|---|---|---|
| Python (RL, training, ROS 2 nodes) | pip install omniloop |
from omniloop import TrainingLoop |
| Rust | cargo add omniloop |
use omniloop::{Config, HaltPolicy}; |
| C or C++ | build omniloop-sdk-cpp with CMake |
#include <omniloop/omniloop.hpp> |
| (building a binding yourself) | cargo add omniloop-core |
the engine: IPC, registry, journal, LoopRunner |
omniloop-core is the shared engine, not an SDK — it has no loop ergonomics of
its own. If you are writing a control loop rather than a language binding, you
want one of the first three.
# 1. Install OmniLoop Python SDK into your environment
pip install omniloop
# 2. Launch the telemetry server & visualizer dashboard
omniloop upTraining a policy. Reward shaping is the iteration loop, and every hypothesis otherwise costs a full run. Bind the reward terms instead:
from omniloop.integrations.isaaclab import IsaacLabIntegration
# Reward term weights become live sliders, bound to the reward manager
ol = IsaacLabIntegration(
env, runner,
reward_terms=["track_lin_vel", "action_rate", "energy"],
)
ol.loop.watch_all_nonfinite(True) # freeze on the first NaN
ol.loop.watch("ep_rew_mean", "less_than", -25.0) # and on reward collapse
for iteration in range(num_iterations):
with ol.tick(): # applies edits, evaluates tripwires
runner.learn(num_steps)
ol.log(ep_rew_mean=r)Three hypotheses in one eight-hour run instead of three runs — and when it goes NaN at hour fourteen the loop freezes with its memory intact rather than dying, with the flight recorder holding the frames that explain it.
Running it on the machine. The same tripwires, in the language the controller is written in, with a halt that hands over instead of stopping:
omniloop::Config cfg;
cfg.halt_policy = omniloop::HaltPolicy::Handoff; // never stop actuating
omniloop::Loop loop(cfg);
loop.tunable("kp", kp_, 0.0, 500.0);
loop.limit("kp", 0.0, 400.0); // hard envelope, in-process
loop.watch_array_within("joint_torque", -100.0, 100.0);
while (running_)
loop.step([&]{ controller_.update(dt); }, // normal body
[&]{ damping_.hold(dt); }); // fallback while haltedTrainingLoop also works standalone over any config dataclass — see
the reference below.
Neither of those is your job? omniloop-core has no concept of a robot, a
reward or an episode — the domain lives entirely in the optional adapters. If
your work is a repeating tick over state that is expensive to rebuild and fails
by going quietly numeric, it fits, whatever the field is called. See
Is OmniLoop for me? for the
five preconditions that actually decide it, and
Computational Steering
plus examples/simulation/steered_solver.py
for a worked non-ML case. The Python entry point is exported as Loop as well
as TrainingLoop — one class, two names.
- Live State Mutation: Mutate learning rates, PID gains, and controller bounds in real time over shared-memory IPC (<1ms latency).
- Halt on Exception & Tripwires: Catch unhandled exceptions or NaN safety violations automatically and freeze process memory for inspection instead of crashing.
- Safe halt for hardware: A halt does not have to mean "stop commanding the machine". Declare
halt_policy="handoff"and the loop keeps running at rate, diverting to your fallback controller — with a deadman for the operator who never comes back. See Halt & Step. - C, C++ and Rust control loops:
omniloop-sdk-cppbinds your existingdoublemembers zero-copy; theomniloopRust crate does the same through handles that satisfy the borrow checker. Both are thin shells over one engine, so they cannot disagree about what a halt means. See C++ SDK and Rust SDK. - Deterministic Replay & Timeline Forking: Step through
.omniv2 trace files tick-by-tick and hot-patch state into new execution runs. - Drivable by an AI agent: An MCP server (
omniloop mcp) exposes the same control plane to Claude, Cursor or any MCP host — read the exception, file, line and ±5 lines of source behind a frozen loop, test a fix live, and get back a config diff. Read-only by default. - Read-only builds for hardware:
-DOMNILOOP_READONLY=ONcompiles the write path out. Telemetry, tripwires, the flight recorder and journaling all still work; there is no code path that reaches an actuator.ol_is_readonly_build()reports it, so "could anything on the wire command this machine" is a property of the binary rather than a policy — which is the form a safety review can accept. See SAFETY.md. - A robot is not one process: each instrumented node publishes into its own segment pair (one writer per seqlock, which is what keeps a read cheap and tear-free), and one relay fans them into one dashboard on one time axis —
omniloop up --watch arm_left,arm_right,estimator. Mutate and halt stay per-loop, because aiming a Halt at whichever node published last is the worst possible affordance on something that can move. - Ecosystem Adapters: One-line integrations for Stable-Baselines3, Isaac Lab (
rsl_rl), ROS 2, LeRobot, Optuna, TensorBoard, and Rerun.
Warning
Security Notice: OmniLoop OSS is designed as a local, single-user developer tool and does not implement authentication, encryption, or access controls. Because it can trigger subprocess execution and modify active process memory via raw pointers:
- Never expose the visualizer port (default 8000/5173) to the public internet or untrusted networks.
- Always run OmniLoop bound to localhost (
127.0.0.1). - Do not run this tool in shared environments where local users cannot be fully trusted. See SECURITY.md for more details, and SAFETY.md before it goes near anything that can move — what OmniLoop guarantees, what it explicitly does not, and the pre-hardware checklist, in one signable page.
This differs in places from the original design targets (which aspired to use FlatBuffers and iceoryx2 directly in the core engine).
omniloop-core(Rust): shared-memory IPC engine using theshared_memorycrate — a single-slot seqlock for telemetry (with sequence-number dedup so stale frames aren't re-delivered) and a length-prefixed ring buffer for commands, both hand-rolled (not iceoryx2 yet). Payloads are UTF-8 JSON.registry.rsholds the live variable map and the halt/step state; each registered pointer declares its own type (f64orbool) at registration time, so dispatch is data-driven rather than matching on hardcoded variable names, and evaluates watchpoints (conditional-halt tripwires, incl. a global NaN watch) on every state sync.journal.rsimplements the versioned.omniv2 container: append-only recording (batched, explicit-flush) of telemetry frames and typed event records (mutations/control/lifecycle/hash checkpoints), each stamped with a tick + monotonic/wall timestamps and CRC-32-protected, with a sparse index for O(log n) seeking; plus an always-onFlightRecorderblack box and journal-vs-journal divergence detection. The IPC engine exports channel health counters (frames published/delivered/skipped, torn reads, ring-full rejections).ipc/wire.rsandipc/dispatch.rshold the command vocabulary and its meaning, andrunner.rsholds the loop engine every SDK wraps — all shared — a protocol with two implementations has two behaviours, and "why is this loop frozen" is not a question two SDKs may answer differently.registry/halt.rsholds the safe-halt policy and the deadman. See TRACEABILITY.md for the full spec and APIs.omniloop-sdk-rust(crateomniloop): the safe Rust API overLoopRunner. Where the C++ SDK binds a pointer to your variable, this hands outTunable/Readouthandles — OmniLoop owns the storage, so a tick can be open while your body reads state, a dangling binding is unrepresentable, and no caller writesunsafe. Handles are!Sendat compile time, because a control loop and its state live on one thread.omniloop-sdk-cpp(Rust + C ABI + header-only C++17): the SDK for control loops that are not Python. Exposes a stable C ABI (include/omniloop/omniloop.h) and an RAII wrapper (omniloop.hpp) over the same core: variables are bound by pointer so the tick path reads and writes the caller's own memory with no copy, frames are rendered into a buffer reserved at setup, and the wholetick_begin/halt_poll/tick_endpath performs no heap allocation once warm. Safe-halt handoff is first-class here because it is what makes the control plane usable on something that can move. Built with CMake (just build-cpp).omniloop-sdk-python(PyO3): Python bindings (OmniLoopTracker,StateJournal,JournalPlayer) over the core engine, plusomniloop/instrument.py, which provides the@track_loopdecorator that wraps a control loop: checks halt state each tick, catches exceptions and freezes the loop instead of crashing it, and publishes telemetry/mutations. Also providesdeclare_param()/load_param_schema()for declaring which parameters the dashboard should render controls for (see below) — most existing training/control loops integrate by callingwait_if_halted()/get_mutations()/publish_telemetry_raw()directly rather than using@track_loop, since the decorator owns its own loop and doesn't fit around existing loop structure.omniloop-server(Starlette,omniloop-server/main.py): polls the shared memory telemetry channel and rebroadcasts it over a/wswebsocket to connected dashboards; accepts mutation and replay-control messages from the dashboard and forwards them back into the command queue; relays any declared parameter schema to newly-connecting/existing dashboard clients.omniloop-dashboard(React + Vite): the IDE-style visualizer — source tree, 3D WebGL / Three.js workbench with URDF robot mesh loading, interactive state matrix, and a deterministic-time-travel visual timeline fork tree graph, driven entirely over the websocket above. Renders a dedicated section per declared parameter group automatically, alongside built-in PID-tuning demo controls.examples: domain-categorized runnable examples (examples/rl/,examples/robotics/,examples/core/,examples/configs/) and test suites (examples/tests/) (test_integration.py) exercising session isolation, halt/resume, live memory mutation, and exception interception end-to-end.
Important
The journal is the record; the live stream is a sample. Telemetry travels
over a single-slot "latest value wins" channel that the server polls at 10 Hz,
so a loop publishing faster than that overwrites its own frames and the
dashboard sees a subset. That is deliberate — the alternative is back-pressure
on a control loop — and it is reported rather than hidden: coverage in the
Diagnostics panel, omniloop doctor, and get_channel_health all say what
fraction actually arrived. A fast fallback controller under
halt_policy="handoff" can push coverage into single digits. The .omni
journal is written by the loop itself and is complete regardless; analyse
traces, not the stream.
OmniLoop offers two kinds of replay. Scrub replay moves through recorded
telemetry frame by frame in the dashboard, honouring the journal's recorded
timestamps. Deterministic replay (TrainingLoop.replay()) re-executes the
loop, feeding back recorded inputs and verifying a state hash per tick, with
ReplayReport naming the first divergence. Determinism covers what the journal
records — mutations, IPC commands, recorded seeds — not unseeded RNG,
wall-clock reads, thread scheduling, or GPU kernel non-associativity.
fork_timeline is a third thing again: it relaunches the originating script
with a frame's state hot-patched, under an isolated session so it cannot disturb
the run it branched from. Because it relaunches your script, your script has to
tolerate the flags it is relaunched with — --init-state <json>, plus
--replay-journal and --fork-tick when a journal is available. An
argparse script that does not declare them exits before any of your code runs,
so make it forkable in two lines:
import omniloop
ap = argparse.ArgumentParser(parents=[omniloop.fork_arguments()])
args = ap.parse_args()
for name, value in omniloop.fork_state(args).items(): # {} on a normal run
setattr(cfg, name, value)The dashboard reports a fork that dies on startup, and tells you why if it has to fall back to a bundled mock.
Install the SDK into your training environment. OmniLoop is a PyO3 native module, so either build it in place or install a wheel:
# Editable dev install (needs a Rust toolchain + maturin):
pip install maturin
cd omniloop-sdk-python && maturin develop # or: pip install -e .
# Or build a redistributable wheel once, install it anywhere (no Rust needed
# in the target env):
cd omniloop-sdk-python && maturin build --release
pip install target/wheels/omniloop-*.whlInstalling the package also puts an omniloop command on your PATH.
Each framework integration declares its dependency as an optional extra, so pull
in only what you use — pip install omniloop[sb3], omniloop[tensorboard],
omniloop[rerun], omniloop[mcap], omniloop[rl-games], omniloop[lerobot], or
omniloop[all-integrations] for the bundle. (The ROS 2 and rsl_rl adapters have
no PyPI extra by design: rclpy comes from a ROS 2 distribution, and rsl_rl
from your Isaac Lab install — use them from those environments.)
Bring up the visualizer & manage developer channels with the CLI:
# Run the stack
omniloop up # telemetry server + dashboard together
omniloop up --no-dashboard # server only
omniloop up --session my_run # isolate a concurrent instance (OMNILOOP_SESSION_ID)
# Diagnose the channel
omniloop status # live / stale / absent, plus server health
omniloop doctor # in-depth diagnostics & publisher collision checks
omniloop doctor --json # same, for CI (non-zero exit on any fault)
omniloop clean --dry-run # show which stale /dev/shm segments would go
# Read what a run recorded
omniloop inspect run.omni # one-command summary of an .omni journal
omniloop replay run.omni --events-only # step through what actually happened
omniloop why run.omni --tick 4350 # the causal chain behind a tick
omniloop diff recorded.omni replayed.omni # first tick where two runs disagree
omniloop export-mcap run.omni # export to .mcap (Foxglove, Rerun, ROS 2)
omniloop completion bash # shell completions (also zsh, fish)Every command is scriptable: 0 healthy, 1 fault, 2 journal damaged but a
prefix was recovered, 3 runs diverged. Add --json for structured output.
Full flag reference: docs/reference/cli.
omniloop up works from any directory — the wheel carries a compiled dashboard,
which the telemetry server serves itself on the same port (default
http://127.0.0.1:8000). No checkout, no npm, no second port.
Note
Working from a checkout: if you are developing the dashboard itself, run
omniloop up from inside the repository (or export
OMNILOOP_REPO_ROOT=/path/to/OmniLoop) and it will start the Vite dev server
on :5173 with hot reload instead of serving the packaged build.
Tip
Journals in a subdirectory: the dashboard's File → Load Trace browser
scans the server's working directory, its examples/ subdirectory and its
parent. If you keep runs somewhere else — runs/ is the usual choice — point
OMNILOOP_TRACE_DIR at it, or they will not be listed:
OMNILOOP_TRACE_DIR=$PWD/runs omniloop upPass an absolute journal= path either way: the server resolves a
recorded journal path against its own working directory, not your loop's.
See MANUAL_TESTING.md for the full step-by-step walkthrough (including triggering and recovering from a live safety exception), and the individual server/dashboard commands if you'd rather start them separately.
For an existing loop with its own control flow (RL training, or anything else),
the least-effort path is TrainingLoop. It derives the dashboard schema from
your existing config dataclass (no YAML to maintain), coerces incoming
mutations to the right type, and binds each one back to the live object it
controls — so you don't hand-write a schema, a mutation if-ladder, and a
telemetry payload and keep all three in sync:
from omniloop import TrainingLoop
loop = TrainingLoop.from_dataclass(
cfg.ppo, # your existing config object
tunable=["learning_rate", "entropy_coef", "clip_param"],
bind={"learning_rate": optimizer}, # writes optimizer.param_groups[*]["lr"]
bounds={"learning_rate": (1e-5, 1e-2, 1e-5)}, # optional slider-range overrides
journal="run.omni", # optional: record for replay
)
for it in range(num_iterations):
with loop.tick(): # halt/step barrier + apply edits
train_one_iteration(...)
loop.log(reward_mean=r, policy_loss=pl) # read-only dashboard readoutsThat's the whole integration. tick() blocks while the dashboard has the loop
halted, applies any parameter edits (to both your config and the bound live
object — setting cfg.learning_rate alone does nothing once an optimizer has
captured it), then publishes a telemetry frame on exit. If the iteration
raises, the loop freezes for inspection instead of crashing the process
(the same safety-interception behaviour as @track_loop); pass
freeze_on_exception=False to re-raise instead. An edit whose value won't
coerce to the parameter's type is skipped and reported, never fatal.
Add limits= for the values that can actually destroy a run. A slider's
bounds= range is a UI hint — the number box beside it will submit anything —
whereas limits= is a hard envelope enforced on every edit, including
programmatic ones, and every clamp is journaled:
loop = TrainingLoop.from_dataclass(
cfg.ppo,
tunable=["learning_rate"],
bind={"learning_rate": optimizer},
bounds={"learning_rate": (1e-5, 1e-2, 1e-5)}, # slider range
limits={"learning_rate": (1e-5, 5e-3)}, # above ~5e-3 Adam diverges
)Watch the outcome rather than hand-validating hyperparameters — let the loop freeze the instant a bad choice shows up, with the journal holding the edit that caused it:
loop.watch_all_nonfinite(True) # halt on the first NaN/inf
loop.watch("policy_loss", "greater_than", 50.0) # halt when the loss diverges
loop.watch("reward_mean", "less_than", -25.0) # halt on reward collapsebind targets can be a torch-style optimizer (anything with .param_groups), a
callable fn(value), or an (obj, "attr") tuple. For runnable references see
examples/rl/mock_rl_training.py (a toy loop whose
reward_mean/policy_loss visibly react to dragging the sliders live, showcasing
a real-world integration pattern guarded so training still runs when OmniLoop isn't installed),
and examples/tests/test_training_loop.py (a
dependency-free unit test of the binding/coercion/freeze behaviour).
TrainingLoop is a thin layer over the SDK primitives, which you can still call
directly if you need finer control (or don't have a config object to derive
from):
from omniloop import tracker, load_param_schema
load_param_schema("my_params.yaml") # or use declare_param(...) in code
for epoch in range(num_epochs):
tracker.wait_if_halted() # pause point
mutations = tracker.get_mutations() # dashboard edits since last tick
if "learning_rate" in mutations:
lr = float(mutations["learning_rate"])
...
tracker.publish_telemetry_raw(json.dumps({ # full current state each tick
"learning_rate": lr, "reward_mean": reward_mean, ...
}))declare_param(...) (in code) or load_param_schema("params.yaml") (YAML)
tells the dashboard what sliders/toggles/readouts to render for your
parameters — no App.jsx edits needed. This is what TrainingLoop calls under
the hood; reach for it directly when you're using the raw primitives instead of
TrainingLoop, or don't have a config object to derive a schema from.
examples/configs/rl_params.yaml is a complete reference
schema in the YAML format below.
YAML format:
params:
- name: learning_rate # key used in get_mutations()/publish_telemetry_raw
type: float # float | int | bool | string
kind: slider # slider | toggle | readonly | text
min: 0.00001
max: 0.01
step: 0.00001
default: 0.0003
label: "Learning Rate" # optional, defaults to `name`
group: "Optimizer" # optional section heading in the dashboard
- name: reward_mean
type: float
kind: readonly # display-only metric, no mutation controlOnce declared, the schema rides along automatically on every
publish_telemetry_raw() call (under a reserved __schema__ key) — there's
no separate "publish the schema" step, and no risk of it being missed by the
telemetry channel's single-slot "latest wins" semantics.
First-party adapters live in omniloop.integrations
so common ML/robotics stacks get live tuning + halt-on-exception with little or
no loop surgery. Each imports its third-party dependency lazily, so
import omniloop never requires torch / SB3 / rclpy / rerun to be installed —
import only the adapter you use.
| Adapter | Import | What it does |
|---|---|---|
| Stable-Baselines3 | from omniloop.integrations.sb3 import OmniLoopCallback |
Pass callback=OmniLoopCallback() to model.learn(...). Binds LR to policy.optimizer (and pins SB3's schedules so edits stick), streams ep_rew_mean, freezes on exception. |
| LeRobot | from omniloop.integrations.lerobot import OmniLoopCallback |
Wrap training steps; exposes learning rate and policy hyperparameters as live sliders, logs training loss/metrics, and freezes training on exception. |
| rsl_rl / rl_games (Isaac Lab) | from omniloop.integrations.rsl_rl import instrument_rsl_rl |
instrument_rsl_rl(runner) wraps the algorithm's update() in place — no library edits, no with block. Also instrument_rl_games(agent). |
| TensorBoard | from omniloop.integrations.tensorboard import OmniLoopSummaryWriter |
Wrap your SummaryWriter; every add_scalar also becomes a dashboard readout. Zero loop edits if you already log to TensorBoard. |
| ROS 2 | from omniloop.integrations.ros2 import OmniLoopRosBridge |
Subscribe topics as telemetry; push dashboard edits as ROS 2 parameters. The sim→real control plane. |
| GPU / JAX sims (Isaac Gym, Brax, MJX) | from omniloop.integrations.gpu_snapshot import SnapshotReflector |
Reduce device tensors to host scalar summaries (mean/min/max) for telemetry — the reflection path for state with no stable host pointer. |
| Rerun / Foxglove / MCAP | from omniloop.integrations.rerun import RerunSink, mcap_to_journal |
loop.add_sink(RerunSink()) mirrors frames into Rerun; mcap_to_journal(...) imports an MCAP log into an .omni journal for Replay. |
| LTTng | from omniloop.integrations.lttng import lttng_to_journal |
Imports babeltrace2-decoded kernel tracepoints (sched_switch, IRQ/softirq/hrtimer events, ...) as journal event records, so OS-level scheduling jitter can be correlated with control-loop ticks in Replay. |
The dashboard renders whatever these declare — no App.jsx edits. New CPU
simulators plug into zero-copy reflection via a BackendAdapter in
omniloop/backends.py (MuJoCo ships
built in). See examples/tests/
for dependency-free references exercising every adapter.
pip install 'omniloop[mcp]'
omniloop mcp --list-tools # what the surface exposes
omniloop mcp # stdio MCP server, read-only
omniloop mcp --allow-control # + live mutation, halt/step, watchpointsThe same control plane the dashboard drives, exposed as MCP tools so an agent can close the debugging loop without a human at the screen:
describe_params → set_watchpoint → wait_for_halt (blocks) → get_halt_context
→ why(tick) → set_variable (returns a correlation_id) → effects_of(cid)
→ resume → verify_replay → export_tuned_config
omniloop_get_halt_context is the one that earns the server: the tripped
watchpoint, the exception, the file, the line and the ±5 lines of source
around it, the traceback, and the state frame at the freeze — everything a
coding agent needs to open the file and fix it.
Two properties matter more than the tool count. It is read-only by default
(--allow-control is opt-in, because the failure mode of a misconfigured agent
with write access to a robot control loop is not a bad commit), and it fails
honestly — a clamped mutation comes back as applied: false, a damaged
journal is marked partial, a subsampled channel reports its coverage, and an
unverifiable replay says cannot_verify instead of "diverged at tick 0".
Mutation rights come with a real envelope: omniloop_set_bounds installs a hard
[min, max] inside the target process, where the Rust core clamps every
write into registered memory — including writes the loop's own code makes — and
the target publishes back what it is actually enforcing, so "the limit is armed"
is something an agent verifies rather than assumes.
Full surface, configuration and safety notes: MCP Server reference.
Recorded traces are more than telemetry stacks — the full trace layer is documented in TRACEABILITY.md:
- Event-typed journaling: every parameter mutation (old → new, with
provenance), freeze-on-exception (with traceback), watchpoint trip, and
session lifecycle event is recorded alongside telemetry — a
.omnifile answers "what changed and why", andexport_tuned_config_from_journal("run.omni")recovers the final tuned values of a session after the fact. .omniv2 container: versioned + CRC-32-protected records, ticks and timestamps stamped in the Rust core, sparse index for O(log n)seek_to_tick(). Pre-v2 files remain readable.- Watchpoints:
tracker.add_watchpoint("reward_mean", "less_than", -50)ortracker.set_watch_all_nonfinite(True)halt the loop from inside the Rust core the instant a predicate fires — a data breakpoint for loop state. - Flight recorder: an always-on black box of the last N frames + events,
auto-dumped to
omniloop_blackbox_*.omnion freeze-on-exception, so the crash you didn't journal still has a trace. - Channel self-observability:
tracker.stats(),GET /stats, andomniloop doctorexpose frames published/delivered/skipped, torn reads, and command-ring health per process side. - Replay divergence detection: deterministic state-hash checkpoints in
the journal;
find_divergence(a, b)pinpoints the first tick two runs disagree. The hash covers loop state only — provenance (__sys_*, captured source, timestamps, status fields) is excluded, since it differs between a recording and its replay by construction.
All correctness bugs identified in the initial pre-release audit have been resolved, and the security-hardening items it flagged have since landed:
- Websocket origin + auth: browser handshakes are checked against an
Origin allow-list (extend via
OMNILOOP_ALLOWED_ORIGINS), and settingOMNILOOP_AUTH_TOKENrequires clients to authenticate (first-message, query param, or bearer token). - Trace loading: journal names are resolved only against a fixed
allow-list of directories — the working directory, its
examplessubdirectory, their parent directory (to support running from a package subdirectory), and any explicitly configuredOMNILOOP_TRACE_DIR— as bare filenames (no separators, no.., no absolute paths); path traversal outside those roots is rejected. - Timeline forking: subprocess spawning is enabled only when the server
is bound to loopback, and can be forced off with
OMNILOOP_ENABLE_FORK=0.
The tool remains a local, single-user developer tool — see the security notice above and SECURITY.md.
Dual-licensed under either of: