Bridging external applications

Some applications MixOS runs are not MixOS's own: they are built elsewhere and offer their own automation port. MixOS never forks them to add Bus verbs. Instead, bridged connects to an application's control channel and answers on the Bus for it, so agents and scripts drive it the same way as any native app.

One bridged instance serves one application, under the application's own name. An instance named fakeapp answers fakeapp.call, fakeapp.commands, fakeapp.execute, fakeapp.inspect, fakeapp.screenshot and fakeapp.status.

What the application must offer

A line-delimited JSON control channel on loopback TCP:

  1. Optionally, an auth line first on every connection: {"id":"auth","method":"auth","params":{"token":"<64 hex>"}}, or with auth: "key", the same line with params.key. A key is written by the application to a file after it starts, and may be rotated; the bridge reads that file on every connect.
  2. Then one request per line, {"id":N,"method":"...","params":{...}}.
  3. One reply per line, {"id":N,"ok":true,"result":...} or {"id":N,"ok":false,"error":"..."}, matched to its request by id.

The named verbs use the methods engine.commands, engine.execute, ui.inspect and ui.screenshot; <app>.call passes any other method through. The full contract is in the bridge spec.

Setting up an instance

Write the instance config, /etc/mixos/bridge/<app>.conf.mix (or $MIXOS_ETC/bridge/<app>.conf.mix for a session). The file name is the instance name:

port: 47801,
token_file: "/home/user/.config/fakeapp/control.token"

For an application whose port has no auth step, write auth: "none" and no token_file. For an application that writes its own key file after it starts, write auth: "key" with the same token_file (the key file's path). The instance starts before the key exists; calls answer rc 11 until it does. Then start the unit:

systemctl start bridged@fakeapp

The unit runs mix bridged.mix --start fakeapp. It checks the instance first, before the name joins the Bus, then runs mix --serve bridged.mix --name fakeapp as its child. A refused start exits 2, with the reason in the journal, and is never restarted. Any other failure (a crash, a broken install) is restarted a few times and then left failed. A normal systemctl stop ends the bridge with status 143, which the unit counts as clean. Run the bridge under the unit, not --start by hand: outside systemd, killing the --start process alone can leave the serve child running. A start is refused when:

  • the Mix binary is older than 0.112.1, which cannot bound writes (RUNTIME);
  • the name is not a valid Bus service name, or starts with mixos- (NAME_INVALID);
  • the name belongs to the Bus broker (noded, noded-*) (NAME_RESERVED);
  • the name is the first segment of a verb already registered to another component, such as settings (NAME_RESERVED);
  • the installed verb registry, /opt/mixos/share/bus/verbs.conf.mix, is missing or unreadable (REGISTRY: the check fails closed);
  • the config or the token file is missing or wrong, or the token file is readable by group or other (CONFIG, TOKEN).

Using it

send fakeapp fakeapp.status
send fakeapp fakeapp.commands
send fakeapp fakeapp.execute body='{"command":"file.new","params":{"width":640}}'
send fakeapp fakeapp.call body='{"method":"echo","params":{"a":1}}'
rcMeaning
0The app did it; the body is its result as JSON.
10The app refused; the body is its error text.
11The app could not be reached: connect, auth, transport or timeout, or the bridge is backing off after one.
12The request was wrong: a missing field, a body that is not a JSON object, or a request longer than max_line (1 MiB by default).

The bridge keeps one connection open. If the application goes away, calls answer rc 11, the bridge waits backoff_s (default 2 seconds), then reconnects and authenticates again on the next call. Each call's network work (connect, auth, writing the request, reading the reply) shares one timeout_s budget, so an application that stops reading, or reads too slowly, gets rc 11 at about timeout_s and a fresh connection after the backoff. Preparing the request (encoding a large body) cannot be cut short, so it can add to the time a call takes beyond timeout_s; the network step after it then fails at once. This needs Mix 0.112.1 or later.

Security

Read this before bridging an application.

  • Any mesh member can drive a bridged application. The Bus gives every member of the mesh access to every verb; there is no per-verb access control, and bridged adds none. Whatever the application's control channel allows, any mesh member can do through <app>.call: send it UI input, run its commands and, if it has file commands, read and write files. Bridge only applications you would let every mesh member drive.
  • File access is the application's to limit. An application that offers file I/O over its control channel should confine it to automation roots it is started with (read and write directories). The bridge passes paths through unchanged and cannot confine them.
  • The token stays on this node. It is read from token_file at start and sent only on the loopback connection. It never appears in status, in an error or in a log line; if the application echoes it in an error or a result, the bridge replaces it with [redacted]. The token file must be 0600 (a file group or other can read is refused) and readable by the unit's user (mixos).
  • Loopback only. The config refuses any host but 127.0.0.1 or localhost, and connects to 127.0.0.1 for either. An auth: "none" port is open to every local process while the application runs, Bus or no Bus; open it only for applications you bridge.
  • A local process can squat the port. If the application dies, any local process that binds its port before it comes back receives the token on the bridge's next reconnect, and can then pass itself off as the application to the bridge, and use the token against the real application once it returns. Loopback TCP has no way to prove who is listening; a unix-socket control channel, checked by owner, would close this, and is a possible future protocol option. Until then, bridge applications only on machines whose local users you trust.
  • The unit's environment is the administrator's. Keep /etc/mixos/bridge/<app>.env and the config writable only by root: whoever can change them (BRIDGED_MIX in particular) picks the code that runs as mixos.
  • The unit is sandboxed. It runs without capabilities or new privileges, sees the file system read-only (homes included) with private /tmp and /var/tmp, and may send IP traffic to loopback only. So keep the config and the token file out of /tmp and /var/tmp, where the unit cannot see them. A Bus that is not on loopback (for example a session broker at ws://<container-ip>:<port>) needs a drop-in, systemctl edit bridged@<app>, adding IPAddressAllow=<broker-ip> under [Service]. The loopback-only IP filter is cgroup BPF: systemd enforces it for this system unit and the serve child on kernels and hosts that support it, and ignores it where they do not (some container managers, for example). The systemd-analyze security score is computed from the unit file and does not prove the filter is enforced on a given host.
  • The name cannot shadow a system service. The start checks refuse a name that a registered verb already starts with, and the Bus itself refuses a name that is already registered.

Status

bridged is version 0.2.0 (unreleased); 0.2.0 adds auth: "key". Its six verbs are declared in the Bus verb registry, docs/docs/spec/bus/verbs.conf.mix (schema 3), as the bridged instance family <app>.<verb>. The static registry gate checks the family's owner and suffix. Installing the registry is a separate prerequisite: the scripts and the registry must come from the same revision, and the registry must be at /opt/mixos/share/bus/verbs.conf.mix or named by BRIDGED_VERBS in /etc/mixos/bridge/<app>.env. Otherwise every start is refused. This source tree is not a live deployment, and nothing in it installs the registry for you.

The family is declared with members: "instances", so the running instances are not listed. tools/verb_registry.mix --instances DIR checks only the private file-stem naming and collisions; it reads no config token. Mesh access is unchanged: the Bus still gives every member every verb. The runtime guard reads the verbs and families lists. It does not enforce the full canonical format (field order, spacing); the static gate does that.

Verb classes. <app>.call and <app>.execute are intent; <app>.commands, <app>.inspect and <app>.status are query. <app>.screenshot is intent: its focus option can bring the window forward, a change another caller sees, so it is not a query even though it only captures. See the instance verb families decision.

Source and tests

bridged is a Mix component (decision): no Cargo package, identity in component.mx. The source is services/bridged:

PathWhat
scripts/lib/bridge_core.mixthe stateless core: config, token, serve-name guard, line-JSON client
scripts/bridged.mixthe Bus service (mix --serve … --name <app>); --start <app> (the unit: checks, then the service as a child); --check <app> (the checks alone)
units/bridged@.serviceone unit per application; config /etc/mixos/bridge/<app>.conf.mix
tests/lib/fake_app.mixa stand-in application on tcp_listen (token, key or no auth, slow and dropping methods)
tests/bridge_core_test.mixoffline: config, token, guard, spec fixture, the client against stand-in apps
tests/bridged_test.mixevery verb over a private broker, the start refusals, --start (no child on a refusal, the child's exit code passed through), the app dying and coming back

Run the tests from the checkout root with Mix 0.112.1 or later. The mix binary the tests run under is BRIDGED_MIX, else MIXOS_TEST_MIX, else /opt/mixos/bin/mix. The Bus test needs a noded: --noded <path>, else MIXOS_TEST_NODED, else /opt/mixos/bin/noded. An empty variable counts as unset. tools/mix_tests.mix sets MIXOS_TEST_MIX, MIXOS_TEST_NODED and BRIDGED_MIX to the fresh build:

mix services/bridged/tests/bridge_core_test.mix
mix services/bridged/tests/bridged_test.mix

This page as Markdown · This page describes the current main branch of MixOS.