ifURI examples
← all examples

30 — control one computer from another, without RDP (urirun mesh)

RDP gives a remote operator the *whole* machine. urirun gives the controller only the specific routes a node chooses to expose, each policy-gated — least-privilege remote control over plain HTTP. No desktop, no full session: the controller calls named URIs, the node runs them and returns JSON.

computer B (node)                         computer A (controller / host)
  urirun node serve  --allow sys/sh/log     urirun host add-node B http://B:8765
     │  exposes:                                 │
     │   sys://B/runtime/query/info              │  POST /run {uri, payload}
     │   sh://B/command/run  (enum-whitelisted)  ├──────────────────────────►  runs on B,
     │   log://B/session/...                     │  ◄──────────────────────────  returns JSON

On each computer (the real workflow)

# on every machine you want to control (the "nodes"):
curl -fsSL https://get.urirun.com/node.sh | bash -s -- --name officepc --service
#   --service installs a boot service; it serves a small registry over HTTP on :8765

# on the controlling machine (the "host"):
curl -fsSL https://get.urirun.com/host.sh | bash -s -- --name studio \
     --add-node officepc=http://192.168.1.20:8765
HC=~/.urirun-host/mesh.json     # host.sh stores the mesh config here — always pass it

# control it (no RDP). ALWAYS pass --config, or host commands look in ./.urirun and show (none):
urirun host nodes  --config "$HC"     # registered nodes
urirun host routes --config "$HC"     # the exact URIs each node exposes
urirun node list --host 192.168.1.20 --ports 8765-8815   # every node running on a machine
curl -s http://192.168.1.20:8765/routes | python3 -m json.tool   # ask the node directly

# call a route the default node actually serves (env/proc/shell/log), namespaced under
# the node's --name. `node.sh --name officepc` -> routes live under `…://officepc/…`:
curl -s -X POST http://192.168.1.20:8765/run -H 'Content-Type: application/json' \
     -d '{"uri":"shell://officepc/command/uname","payload":{}}'
curl -s -X POST http://192.168.1.20:8765/run -H 'Content-Type: application/json' \
     -d '{"uri":"env://officepc/runtime/query/health","payload":{}}'

# shut down on each machine:
urirun node stop --all                         # stop every node running on this machine
systemctl --user disable --now urirun-node     # a --service node respawns on kill; disable it

> Two gotchas that bite first-timers > 1. urirun host nodes with no --config reads ./.urirun/mesh.json and prints (none). > The mesh host.sh created is at ~/.urirun-host/mesh.json — pass --config "$HC". > 2. A node's routes are namespaced under its own --name (defaulting to the machine's > *hostname*), not the alias you used in --add-node alias=URL. If you ran node.sh > without --name, run curl http://NODE:8765/routes to see the real scheme/name, e.g. > shell://lenovo/command/uname. The default node has no sh://…/command/run route — > that scheme belongs to this example's own mesh_local.sh registry below.

The node's security boundary is its --allow globs + each route's input schema. A controller can only call exposed routes, and only with permitted arguments — e.g. shell://…/command/which takes a binary param, and the local-demo's sh://…/command/run accepts an enum of whitelisted commands, so rm -rf / is rejected before it ever runs.

Re-provision a node from the host — no SSH (POST /deploy)

A running node can be re-provisioned over the mesh: push a new registry (and even the handler code) and the node hot-swaps what it serves, no restart. get.urirun.com/node.sh turns this on by default. Auth is SSH-key based, like ssh-copy-id — no shared token:

uri-copy-id 192.168.1.20                       # enroll your ~/.ssh/id_ed25519 on the node
#   first key on a fresh node = trust-on-first-use; later keys must be signed by an enrolled one
urirun host copy-id --all --config "$HC"       # …or onto every node in the mesh at once

# now push routes/code, signed with your key (no token to remember):
urirun host deploy officepc --config "$HC" \
  --bindings my.bindings.json --identity ~/.ssh/id_ed25519

A node started with --key-auth keeps ~/.urirun-node/authorized_keys and verifies an ed25519 signature on each /deploy; GET /health reports "deploy"/"keyAuth". A shared token still works as an alternative (node serve --admin-token … + host deploy --token). Example 31 uses exactly this to push a whole office/desktop URI surface onto a remote node and drive it from natural language.

Manage the nodes on a machine

urirun node list                 # every running node here (any port; probes listening sockets)
urirun node stop --port 8766     # stop one instance (repeatable)
urirun node stop --all           # stop them all (node.sh's free-port fallback breeds duplicates)

Manage a node's environment — as URIs (node://, no script)

Start a node with --manage and it exposes admin-gated node:// management routes, so you provision it the URI way — install the packages a node needs (connectors, the tellmesh office surface, cryptography) into its own venv, over the mesh, no SSH:

# on the node: needs admin auth (token or key); served from a separate, always-gated registry
urirun node serve … --key-auth --manage        # or --admin-token …

# from the host (signed with your enrolled key, or --token):
urirun run node://officepc/runtime/query/info        --execute   # python / venv / urirun version
urirun run node://officepc/package/query/list   --payload '{"match":"urirun"}' --execute
urirun run node://officepc/package/command/install \
        --payload '{"spec":["uricontrol","urirun-connector-time-tools"]}' --execute
urirun run node://officepc/connector/command/install --payload '{"id":"http-check"}' --execute
URIdoes
node://N/runtime/query/infointerpreter, venv, urirun version
node://N/package/query/listinstalled packages (match filter)
node://N/package/command/installpip install a spec (PyPI / git+… / path) into the node's venv
node://N/connector/command/installinstall urirun-connector-<id>

Every node:// call is gated by the admin token / enrolled SSH key (never the open /run allow-list), since installing is arbitrary code execution. This closes the loop: a node missing a connector is fixed with a URI, not a login.

Watch a node's activity live (no SSH)

A node streams every /run and every error as Server-Sent Events on GET /events, so you can follow what a remote machine is doing in real time — over plain HTTP, nothing to tail:

HC=~/.urirun-host/mesh.json
urirun host watch node-b --config "$HC"                  # live stream, formatted
urirun host watch node-b --scheme sh,error --config "$HC"   # only these schemes
urirun host watch node-b --follow --config "$HC"         # reconnect + replay on drop
curl -N http://192.168.1.20:8765/events                  # raw SSE, no CLI
data: {"event":"run","uri":"sys://node-b/runtime/query/info","ok":true, ...}
data: {"event":"error","uri":"error://local/E-a7d90355/...","message":"Route not found: ...", ...}

GET /health reports events (subscriber count); for the node's own stdout use tail -f ~/.urirun-node/node.log (--background) or journalctl --user -u urirun-node -f (--service). Example 32 drives scenarios while watching this stream.

Run the whole thing locally (two nodes + a host)

./mesh_local.sh

It simulates two computers as local nodes and a controller, end to end:

== control node-b FROM the controller, over HTTP (no RDP) ==
  [b] sys info      -> {"host":"…","platform":"Linux-…","cwd":"…"}
  [b] run 'uname -a' -> Linux … #35-Ubuntu …
  [a] write log     -> {"wrote":"hello from the controller"}
  [a] read log      -> {"lines":["{…\"text\": \"hello from the controller\"}"]}

== least-privilege check: a non-whitelisted command is REFUSED by the node ==
  rm -rf / -> false 'rm -rf /' is not one of ['uname -a', 'uptime', 'id', 'date', 'df -h /']

== discover running nodes on this machine ('urirun node list') ==
  PORT   NAME    ROUTES  DEPLOY  EXEC  URL
  34489  node-b  4       False   True  http://127.0.0.1:34489
  58503  node-a  4       False   True  http://127.0.0.1:58503

== shut them down with 'urirun node stop' (SIGTERM -> port freed) ==
  port 58503: stopped (pids [...])
  port 34489: stopped (pids [...])

Why this beats RDP for automation

cannot do anything the node didn't publish.

(see examples 25–29 for flows and agents over exactly these routes).

mesh-urirun-com repo (mesh.urirun.com) routes the same /run calls through a small PHP relay so both sides only need outbound HTTP — no inbound ports, no port-forwarding.

Files

Files

.gitignoreREADME.mdmesh_local.shnode.bindings.template.jsontest_mesh.py.benchmarks/

View on GitHub →