Skip to content

Yimo/feat/host fragment - #3967

Open
poorwym wants to merge 15 commits into
mainfrom
yimo/feat/host_fragment
Open

Yimo/feat/host fragment#3967
poorwym wants to merge 15 commits into
mainfrom
yimo/feat/host_fragment

Conversation

@poorwym

@poorwym poorwym commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

This PR adds the core infrastructure for deploying a DimOS Blueprint across multiple Hosts:

  1. Use .hosted() to describe where a module should run;
  2. Automatically place modules based on Host name, tags, state, load, and code revision;
  3. Compile one Blueprint into multiple immutable HostFragment objects;
  4. Route cross-Host streams through run-scoped Zenoh topics;
  5. Support run/Host-scoped module RPC and coordinator RPC;
  6. Start each Fragment in an isolated child process and manage its start, status, stop, and logs.

.hosted() API

app = autoconnect(
    Camera.blueprint().hosted(host="edge-a"),
    Detector.blueprint().hosted(tags={"gpu"}),
    ResultSink.blueprint(),  # No constraint: stays on the controller
)
blueprint.hosted()                    # Any available remote Host
blueprint.hosted(host="edge-a")     # Exact Host ID or unique name
blueprint.hosted(tags={"gpu"})       # Host must contain all tags
blueprint.hosted(local=True)          # Force placement on the controller

.hosted() can be applied to a single module or a composed Blueprint. A composed Blueprint becomes one placement unit and is assigned to a single Host.

Core Behavior

  • Modules without placement constraints stay on the controller, preserving existing single-Host behavior;
  • Cross-Host streams use dimos/runs/<run-id>/streams/<stream>/<message-type>;
  • Each Fragment is bound to its run_id, generation, host_id, application revision, and SHA-256 digest;
  • A Host validates the target Host, epoch, schema, digest, and code revision;
  • Starting the same Fragment again is idempotent; stopping requires a matching run, generation, and digest;
  • Automatic placement co-locates modules connected by module references. Explicit assignments can still be used to exercise cross-Host module RPC.

This PR does not include code distribution, failover, migration, replicas, or resource-aware scheduling. The standard dimos run command is also not yet connected to the full discovery → placement → dispatch workflow.


Validation on yimo/temp/host_e2e

This branch adds a Host client, scenario runner, and Docker Compose environment on top of the feature PR.

1. Switch Branches

git switch yimo/temp/host_e2e

2. Run the Three Automated Scenarios

experimental/hosted/compose/run.sh all
Scenario Module distribution What it validates
basic source → edge-a; transform → compute-a; sink → controller Exact name, tags, local default, and cross-Host streams
replay replay → replay-a; transform → compute-a; sink → controller SQLite Replay across Hosts
sim MuJoCo → sim-a; odom probe → compute-a; sink → controller Real simulation and typed odometry across Hosts

Compose topology

flowchart LR
    C["verify-* / Controller"] -->|"discover + lifecycle RPC"| Z["Zenoh Router"]
    Z <--> E["edge-host\nedge-a / edge"]
    Z <--> G["compute-host\ncompute-a / compute,gpu"]
    Z <--> R["replay-host\nreplay-a / replay"]
    Z <--> S["sim-host\nsim-a / sim"]
    E <-->|"run-scoped streams"| G
    G <-->|"run-scoped streams"| C
    R <-->|"run-scoped streams"| G
    S <-->|"run-scoped streams"| G
Loading

The Compose file has three layers:

  1. Router: one dedicated Zenoh router provides the shared discovery, RPC, and data fabric;
  2. Execution Hosts: edge-host, compute-host, replay-host, and sim-host each run an independent HostDaemon with a stable name and tags;
  3. Scenario controller: one profile-specific verify-* container owns the original Blueprint, discovers Hosts, resolves placement, compiles Fragments, and checks the final application result.

All services use the same locally built image, so every Host has the same code and application revision. They share the same runtime configuration: Zenoh transport, client mode, explicit connection to tcp/router:7447, two local workers, and no multicast discovery. sim-host and the simulation controllers also receive additional shared memory for MuJoCo.

Control plane and data plane

The controller uses the router to discover Host liveliness tokens and call Host RPCs such as describe, start, and stop. This is the control plane.

After deployment, application messages do not pass through the controller as a proxy. The Fragment compiler pins every boundary stream to a run-scoped Zenoh key, and the modules exchange data directly through the router. This is the data plane.

Controller Blueprint
  -> discover Host descriptors
  -> resolve .hosted() constraints
  -> compile one HostFragment per selected Host
  -> start consumer Fragments
  -> start producer Fragments
  -> wait for ValidationSink
  -> stop all Fragments

Starting consumers first prevents short-lived replay or source messages from being published before their remote subscribers are ready.

Scenario module graphs

Each profile changes only the scenario controller; the router and four Host services keep the same topology.

basic:
  BasicSource@edge-a
    -> BasicTransform@compute-a
    -> ValidationSink@controller

replay:
  ReplaySource@replay-a
    -> ReplayTransform@compute-a
    -> ValidationSink@controller

sim:
  MujocoSimModule@sim-a
    -> SimOdomProbe@compute-a
    -> ValidationSink@controller

basic exercises exact-name placement, tag placement, the local default, and two Host boundaries. replay replaces the synthetic source with a real SQLite-backed replay. sim replaces it with a real headless MuJoCo process and a typed PoseStamped odometry stream.

The ValidationSink is deliberately local and is the end-to-end assertion point. It writes a result only after all required values arrive. The controller then prints the resolved placement and application result, stops each Fragment using its run/generation/digest identity, and exits with the scenario status. The shell runner propagates that status through --exit-code-from and removes the Compose containers and network.

The scenarios can also be run individually:

experimental/hosted/compose/run.sh basic
experimental/hosted/compose/run.sh replay
experimental/hosted/compose/run.sh sim

3. Run the Visual Scenario

experimental/hosted/compose/run.sh visual

After VISUAL:READY appears, open:

http://localhost:9878/?url=rerun%2Bhttp%3A%2F%2Flocalhost%3A9877%2Fproxy

The visual profile extends the simulation graph with a controller-local Rerun bridge:

MovingMujocoSimModule@sim-a
  ├─ odom -> SimOdomProbe@compute-a -> ValidationSink@controller
  └─ odom -> RerunBridgeModule@controller -> gRPC :9877 -> Web Viewer :9878

MuJoCo updates the robot's real root pose and velocity on sim-a. The same run-scoped odometry stream is consumed by the probe for an automated assertion and by Rerun for visualization. Static ground and target geometry are logged by the bridge, while the robot body is attached to the live odometry transform.

Unlike the finite profiles, visual does not exit after validation. VISUAL:READY means Host discovery, placement, Fragment startup, cross-Host odometry, and the application-level assertion have already succeeded; the controller is kept alive so the browser can remain connected.

The blue robot should move continuously in a circle, with the yellow arrow rotating to show its direction of travel. The red target and ground remain fixed, making motion easy to verify.

image image

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This change adds distributed Host lifecycle control, deployment supervision, and hosted fragment compilation.

Two reproduced failures need correction before merging:

  • The Host start endpoint deserializes unauthenticated requests before validating lifecycle metadata.
  • A failed, stale startup can terminate a replacement deployment that uses the same run ID.
  • Hosted fragment compilation also appears incompatible with the coordinator’s class-reference fallback behavior.

T-Rex validation blocked

The focused class-reference compatibility check could not execute because the environment is missing the plum dependency required to import the coordinator path.

Confidence Score: 1/5

Not safe to merge: the lifecycle control endpoint permits unsafe deserialization, and a stale startup failure can stop a replacement deployment.

The review contains three blocking findings, including one security issue. Two were reproduced through focused execution; the remaining compiler compatibility issue is supported by the differing resolver behavior but could not complete its focused execution check.

Files Needing Attention: dimos/cli/commands/host.py, dimos/hosted/daemon.py, and dimos/hosted/fragment_compiler.py

Security Review

The Host start lifecycle endpoint accepts Zenoh RPC payloads that are pickle-deserialized before epoch or fragment validation, without an authentication or authorization boundary at dispatch. A peer that can access the shared fabric can trigger arbitrary pickle reconstruction in the Host process.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced a proof for a posted P1 finding, including the Host RPC validation source and two related RPC outputs.
  • T-Rex produced a second proof for a posted P1 finding, including the stale startup reproduction source, the reproduction command, and the replacement termination output.
  • T-Rex attempted general contract validation but the run was blocked when importing the coordinator dependency graph raised a ModuleNotFoundError for plum.
  • T-Rex documented the blocked status with exact observed behavior, noting that both coordinator and hosted attempts exited before running the comparison and no uploaded artifacts were returned in this session.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "add .hosted() api for auto placement" | Re-trigger Greptile

cleanup.callback(rpc.stop)
control_name = HOST_CONTROL_RPC_NAME.format(host_id=host_id)
rpc.serve_rpc(daemon.describe, f"{control_name}/describe") # type: ignore[arg-type]
rpc.serve_rpc(daemon.start, f"{control_name}/start") # type: ignore[arg-type]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Authenticate Host Start Requests

If an untrusted peer can reach the shared Zenoh fabric, it can send a request to this Host start endpoint. The request is pickle-deserialized before HostDaemon.start checks its epoch or fragment metadata, and there is no caller authentication or authorization boundary. A peer can therefore trigger arbitrary pickle reconstruction and execute code in the Host service.

How this was verified: A payload reducer executed before the lifecycle handler returned its stale-epoch validation error.

Knowledge Base Used:

Artifacts

Host RPC validation source

  • Focused validation source that sends the lifecycle request with a controlled pickle reducer, demonstrating that payload reconstruction occurs before lifecycle validation.

Direct lifecycle validation output

  • Direct stale-epoch invocation output establishes that the handler validation would reject the request after it is invoked.

Anonymous Host RPC output

  • Anonymous lifecycle request output shows the pickle reducer marker appears before the stale-epoch handler error, confirming unsafe deserialization occurs first.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment thread dimos/hosted/daemon.py
Comment on lines +161 to +164
error = self._wait_for_start(receive_ready)
if error is not None:
_terminate(process, self._stop_timeout)
kill_run_processes(fragment.run_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Guard Stale Startup Cleanup

A deployment can be stopped and replaced while its original start() call is still waiting for readiness. If that stale call then times out or receives EOF, this run-wide cleanup kills every process with the same DIMOS_RUN_ID, including the replacement deployment. The identity check on line 167 happens only after the sweep, so it cannot prevent the failure. Check that the deployment is still current before sweeping, or limit cleanup to the failed deployment's process tree.

Knowledge Base Used:

Artifacts

Stale startup reproduction source

  • Focused concurrency reproduction source that creates a stale startup and a replacement deployment sharing one run ID.

Stale startup reproduction command

  • Command wrapper used to execute the stale-start replacement scenario.

Replacement termination output

  • Execution output records the stale cleanup sweep terminating the replacement PID and the replacement ending in a failed state.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +522 to +531
if not possible:
disabled = any(
atom.module in blueprint.disabled_modules_tuple and _matches_reference(atom, requested)
for atom in blueprint.blueprints
)
if reference.optional or disabled:
return None
raise ValueError(
f"Module reference {consumer.name}.{reference.name} has no provider in the Blueprint"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Preserve Class Reference Fallback

When a required class-typed module reference has no provider in the blueprint, the fragment compiler raises an error. The existing coordinator instead preserves the class-reference fallback and accepts the same local blueprint. Hosted compilation can therefore reject existing graphs that previously built successfully. Keep this resolver aligned with _resolve_single_ref, or explicitly validate and document the compatibility break.

Knowledge Base Used:

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
4931 1 4930 183
View the top 1 failed test(s) by shortest run time
dimos.codebase_checks.test_import_from_source::test_import_from_source
Stack Traces | 9.71s run time
def test_import_from_source() -> None:
        """Fail if any name is imported from a module that only re-imported it."""
        violations = find_reexport_imports()
        if violations:
            listing = "\n".join(
                f"  - {p.relative_to(DIMOS_PROJECT_ROOT)}:{line}: `{name}` imported from "
                f"{src}, but defined in {origin}"
                for p, line, name, src, origin in sorted(violations)
            )
>           raise AssertionError(
                f"Found import(s) that pull a name from a re-exporter:\n{listing}\n\n"
                "Import each name straight from the module that defines it (shown above). "
                "If a module re-exports a name on purpose, mark its import with "
                "`# noqa: F401` or the `from x import Y as Y` form, and that re-export "
                "will be allowed."
            )
E           AssertionError: Found import(s) that pull a name from a re-exporter:
E             - .../cli/commands/host.py:319: `FRAGMENT_SCHEMA_VERSION` imported from dimos.hosted.daemon, but defined in dimos.hosted.fragment
E             - dimos/hosted/test_daemon.py:27: `HostFragment` imported from dimos.hosted.daemon, but defined in dimos.hosted.fragment
E           
E           Import each name straight from the module that defines it (shown above). If a module re-exports a name on purpose, mark its import with `# noqa: F401` or the `from x import Y as Y` form, and that re-export will be allowed.

listing    = '  - .../cli/commands/host.py:319: `FRAGMENT_SCHEMA_VERSION` imported from dimos.hosted.daemon, but defined in dimos...dimos/hosted/test_daemon.py:27: `HostFragment` imported from dimos.hosted.daemon, but defined in dimos.hosted.fragment'
violations = [(PosixPath('/home/runner/work/dimos/dimos/.../cli/commands/host.py'), 319, 'FRAGMENT_SCHEMA_VERSION', 'dimos.hosted....../dimos/hosted/test_daemon.py'), 27, 'HostFragment', 'dimos.hosted.daemon', 'dimos.hosted.fragment')]

dimos/codebase_checks/test_import_from_source.py:148: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant