Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

OmniLoop

PyPI crates.io MIT Apache-2.0 Docs

The control plane for physical AI and robot control loops

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.


📦 Which package do I want?

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.

⚡ Quickstart

# 1. Install OmniLoop Python SDK into your environment
pip install omniloop

# 2. Launch the telemetry server & visualizer dashboard
omniloop up

💡 A Short Taste

Training 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 halted

TrainingLoop 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.


🚀 Key Capabilities

  • 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-cpp binds your existing double members zero-copy; the omniloop Rust 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 .omni v2 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=ON compiles 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.

🛠️ Current Architecture (as implemented)

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 the shared_memory crate — 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.rs holds the live variable map and the halt/step state; each registered pointer declares its own type (f64 or bool) 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.rs implements the versioned .omni v2 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-on FlightRecorder black 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.rs and ipc/dispatch.rs hold the command vocabulary and its meaning, and runner.rs holds 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.rs holds the safe-halt policy and the deadman. See TRACEABILITY.md for the full spec and APIs.
  • omniloop-sdk-rust (crate omniloop): the safe Rust API over LoopRunner. Where the C++ SDK binds a pointer to your variable, this hands out Tunable/Readout handles — OmniLoop owns the storage, so a tick can be open while your body reads state, a dangling binding is unrepresentable, and no caller writes unsafe. Handles are !Send at 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 whole tick_begin/halt_poll/tick_end path 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, plus omniloop/instrument.py, which provides the @track_loop decorator 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 provides declare_param()/load_param_schema() for declaring which parameters the dashboard should render controls for (see below) — most existing training/control loops integrate by calling wait_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 /ws websocket 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.


💻 Getting Started & Installation

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-*.whl

Installing 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 up

Pass 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.


🔌 Plugging in your own training/control code

Recommended: TrainingLoop (derive-and-bind)

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 readouts

That'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 collapse

bind 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).

Lower-level: the raw primitives

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, ...
    }))

Declaring dashboard controls

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 control

Once 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.


🧩 Framework Integrations

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.


🤖 Driving OmniLoop from an AI agent (MCP)

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, watchpoints

The 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.


🔍 Traceability & Replay

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 .omni file answers "what changed and why", and export_tuned_config_from_journal("run.omni") recovers the final tuned values of a session after the fact.
  • .omni v2 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) or tracker.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_*.omni on freeze-on-exception, so the crash you didn't journal still has a trace.
  • Channel self-observability: tracker.stats(), GET /stats, and omniloop doctor expose 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.

🛡️ Security Hardening & Isolation

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 setting OMNILOOP_AUTH_TOKEN requires 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 examples subdirectory, their parent directory (to support running from a package subdirectory), and any explicitly configured OMNILOOP_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.


📄 License

Dual-licensed under either of:

See LICENSE and NOTICE for details.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

4 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages