ifURI examples
← all examples

31 — drive a remote computer's office work from natural language (LLM → URI → mesh)

You type an office task in plain language on your host; an LLM (liteLLM, config from examples/.env) turns it into a URI plan; urirun delegates each step to a remote node (192.168.188.201, the default urirun port :8765) over the mesh; and the run is logged on both sides — the host's trace *and* the node's own log — so you see exactly what happened on each machine.

It combines three earlier examples into one operational setup:

mesh (no RDP; least-privilege, policy-gated routes over HTTP).

(urihim, urikvm, uribrowser, urioffice, uriscreen, urishell) into the registry the node serves.

 HOST (studio)                                   NODE (lenovo @ 192.168.188.201:8765)
 ──────────────                                  ────────────────────────────────────
 "open example.com and type the invoice no."     urirun node serve  (office registry)
        │  examples/.env (OPENROUTER_API_KEY,       exposes, policy-gated:
        │                 LLM_MODEL)                  browser://lenovo/page/open
        ▼                                             him://lenovo/keyboard/command/type-text
  GET /health + /routes  ◄──── the node's live ─────  kvm://lenovo/monitor/.../screenshot
  (the action space)            action space          urioffice://lenovo/document/command/open
        │                                              screen://lenovo/monitor/0/query/frame
        ▼                                              shell://lenovo/process/command/run
  LLM plans [{uri,payload,why}]                        log://lenovo/session/...   ◄─┐
        │                                                                           │
        ▼  POST /run {uri,payload}                                                  │
  dispatch each step ───────────────────────────────►  node runs it, returns JSON   │
        │  + log each step on the node ─────────────────────────────────────────────┘
        ▼
  generated/run-log.md + host-run-*.log   ◄── read the node's own log back (both sides agree)

Two machines, two commands

1. On the node (the machine you want to control — 192.168.188.201)

Copy this example dir (and the ../tellmesh checkout) to the node, then:

./node_serve.sh
#   name=lenovo, binds 0.0.0.0:8765, ALLOW_REAL=1 (drives the real mouse/keyboard/
#   browser/LibreOffice). Builds the registry from the tellmesh manifests and serves it.

ALLOW_REAL=0 ./node_serve.sh        # safe mock mode (handlers return without touching the box)
NODE_NAME=officepc ./node_serve.sh  # different node name / URI host segment

It picks ../tellmesh/urisys-node/.venv/bin/python (it has urirun + uricontrol + the connectors); override with PY=…, and the checkout locations with IFURI_DIR=… TELLMESH_DIR=…. The node prints routes: 34 and the served schemes (browser, env, him, kvm, log, proc, screen, shell, urioffice).

> The bridge. Real tellmesh handlers are handler(payload, context); urirun's > local-function adapter calls fn(payload). tellmesh_bridge.py reads each > manifest, imports the real handler, and exposes a fn(payload) wrapper that > forwards a *persistent* context (mock state, config, the allow_real flag) — so the > whole tellmesh office surface executes in-process under the urirun node, unmodified.

2. On the host (your machine)

./office_cli.sh "open https://example.com and take a screenshot" --yes
./office_cli.sh "type 'Faktura 07/2026 zatwierdzona' then capture monitor 0" --yes
NODE_URL=http://192.168.188.201:8765 ./office_cli.sh "list the top processes" --yes

heuristic so the loop still runs. The configured LLM_MODEL is an image-preview model that plans JSON fine (like example 27) but cannot do MCP tool-calling — point URIRUN_OFFICE_MODEL at a stronger model if you want richer plans.

Reach the node from OUTSIDE the LAN — mesh.urirun.com relay

The host commands above point NODE_URL at http://192.168.188.201:8765, which only works when host and node share a LAN. When the node sits behind NAT/firewall (or its SSH is closed and /deploy is off — GET /health shows "deploy":false), reach it through the mesh-urirun-com relay (mesh.urirun.com): a tiny store-and-forward broker where both sides make only outbound HTTPS — no inbound ports, no port-forwarding, no VPN/RDP.

# ON the node (192.168.188.201) — bridge the local node to the relay (outbound only).
# MESH_NODE is the relay queue name; the URIs you call must match what the node serves
# (its routes target NODE_NAME, here `lenovo`).
MESH_RELAY=https://mesh.urirun.com MESH_NODE=lenovo MESH_TOKEN=$SECRET \
  LOCAL_NODE=http://127.0.0.1:8765 ../../mesh-urirun-com/clients/mesh-node.sh

# ON the host (anywhere on the internet) — drive it through the relay:
MESH_RELAY=https://mesh.urirun.com MESH_TOKEN=$SECRET \
  ../../mesh-urirun-com/clients/mesh-run.sh lenovo 'screen://lenovo/monitor/0/query/frame' '{}'

Why this matters here: the bridge only needs the node's local /run (already served) plus outbound HTTPS — so it works even with deploy:false and closed SSH, bypassing the uri-copy-id//deploy enrollment entirely. The node's own --allow still gates every job; the relay never executes anything.

Run the NL→LLM office loop THROUGH the relay (transparent)

office_agent.py POSTs {uri, payload} to NODE_URL/run. mesh-urirun-com's mesh-proxy.py is a local /run endpoint that tunnels to the relay — so just point NODE_URL at it and the whole loop runs over mesh.urirun.com, no other change:

# host: start the transparent proxy (enqueue+poll the relay behind a local /run)
MESH_RELAY=https://mesh.urirun.com MESH_TOKEN=$SECRET \
  python3 ../../mesh-urirun-com/clients/mesh-proxy.py --node lenovo --port 8090 &

# now the natural-language office loop drives the remote node through the relay:
NODE_URL=http://127.0.0.1:8090 ./office_cli.sh "list the top processes" --yes

The same proxy serves urirun host ask / flow:// via URI_SERVICE_MAP ({"lenovo":"http://127.0.0.1:8090"}) — see the relay README.

Provision the node FROM the host (no SSH) — POST /deploy

You don't have to log into the node to change what it serves. urirun nodes expose a token-gated POST /deploy that accepts a registry and the handler code, then hot-swaps the served surface live (no restart). So after the node runs once with an admin token, the host drives everything over the mesh:

# on the node — /deploy is ON by default with SSH-key auth (no shared secret):
./node_serve.sh
#   deploy /api : ENABLED (ssh-key: run 'uri-copy-id' from the host)

# from the host — enroll your SSH key once, then push the office bindings + bridge code:
./deploy_from_host.sh
#   == enroll SSH key (uri-copy-id; trust-on-first-use) ==   ← first run only
#   == push … ==  -> node routeCount jumps 7 -> 34, no restart. Re-run any time.

SSH-style auth — uri-copy-id (no tokens)

Managing many nodes by shared token is a pain, so urirun also does public-key auth like SSH, reusing your ~/.ssh/id_ed25519:

uri-copy-id 192.168.188.201          # ssh-copy-id for urirun: enroll your key on the node
#   (equiv: urirun host copy-id 192.168.188.201 --identity ~/.ssh/id_ed25519)
urirun host deploy lenovo --bindings node-office.bindings.json \
  --code tellmesh_bridge.py --identity ~/.ssh/id_ed25519     # signed, no token

node is trust-on-first-use (claims the node, no secret); afterwards adding a key must be signed by an already-enrolled key. Start the node with --key-auth (node_serve.sh and get.urirun.com/node.sh do this by default).

(purpose + timestamp + body hash, ±300 s) — urirun host deploy --identity KEY.

--token. The node needs the cryptography package for key-auth (node.sh installs it; otherwise pip install cryptography).

Under the hood it is a first-class urirun command:

urirun host deploy lenovo --bindings node-office.bindings.json \
  --code tellmesh_bridge.py --env TELLMESH_DIR=~/github/tellmesh \
  --allow 'him://lenovo/**' --allow 'browser://lenovo/**' --token secret
#   (lenovo resolves from the host mesh config, or pass a full URL)

imports lazily — that is how tellmesh_bridge.py gets onto the node. Heavy packages the bridge imports (urihim, urikvm, …) must already be installed on the node; --env TELLMESH_DIR=… points the bridge at them.

random token and persists it to ~/.urirun-node/admin-token (0600), reused across restarts so the host's token stays valid; --admin-token TOKEN / URIRUN_NODE_TOKEN pins your own; --admin-token auto is the generate-or-reuse shorthand.

matching X-Urirun-Token. It can add executable routes and push code, so treat the token like a deploy key and use it only on a trusted LAN. GET /health reports "deploy": true|false.

> Chicken-and-egg: the *first* time still needs the node started with a token (and, for > the office handlers, the tellmesh runtime present). After that, every update — > new bindings, new allow policy, new handler code — is host-driven over HTTP.

Managing many nodes

urirun node list                       # every running node on THIS machine (any port)
urirun node list --host 192.168.188.201 --ports 8765-8820   # probe a remote range
urirun node stop --port 8766           # stop one instance (repeatable)
urirun node stop --all                 # stop every running node on this machine
uri-copy-id 192.168.188.201            # enroll your key on one node
urirun host copy-id --all              # …or on every node in the mesh config

node.sh's free-port fallback (8765 busy → next port) is why duplicates appear; node list finds them all, node stop clears them. (systemd --user services respawn on kill — disable those with systemctl --user disable --now urirun-node.)

Both-sides logging (the point of the setup)

Every run is recorded twice, and the host reads the node's log back so you can confirm they agree:

so the node's own log (~/.urirun-node/notes.jsonl) records the delegated task, every dispatched URI + payload, and every result. Read it independently any time:

curl -s -X POST http://192.168.188.201:8765/run -H 'Content-Type: application/json' \
  -d '{"uri":"log://lenovo/session/query/recent","payload":{"limit":20}}'

agent drives it, over plain HTTP, no SSH:

urirun host watch lenovo --config ~/.urirun-host/mesh.json   # or: curl -N http://192.168.188.201:8765/events
# data: {"event":"run","uri":"him://lenovo/keyboard/command/type-text","ok":true, ...}
# data: {"event":"error","uri":"error://local/E-…","message":"Route not found: kvm.display.query", ...}

A real run delegated to the live node, end to end:

  [0] browser://lenovo/page/open            -> ok {"url":"https://example.com","title":...}
  [1] him://lenovo/keyboard/command/type-text -> ok {"typed":"Faktura 07/2026 zatwierdzona","chars":28}
  [2] screen://lenovo/monitor/0/query/frame -> ok {... frame ...}   (needs ALLOW_REAL=1 on the node)

== node-side log (read back from the node) ==
  node: [host] new task: "open https://example.com ..." (3 steps, planner=llm)
  node: [host->node] step 0: browser://lenovo/page/open payload={"url":"https://example.com"}
  node: [node] step 0 ok: {"url":"https://example.com",...}
  ...

Office tasks this covers

intentURI(s) the LLM picks
log into a website / fill a formbrowser://lenovo/page/open, browser://lenovo/form/command/submit
download / open a page, read itbrowser://lenovo/page/open, browser://lenovo/page/query/dom
type / hotkeys / mouse on the boxhim://lenovo/keyboard/command/type-text, …/keyboard/command/hotkey, him://lenovo/mouse/command/click
open / save / export a documenturioffice://lenovo/document/command/open, …/command/export-pdf
screenshot / OCR-driven clickingkvm://lenovo/monitor/{n}/query/screenshot, kvm://lenovo/task/command/click-text
run anything on the machineshell://lenovo/process/command/run
screen framesscreen://lenovo/monitor/0/query/frame

OS-independent by construction: the host only ever speaks URI + JSON payload; what a URI *does* on the node is the node's connector + its allow_real driver (xdotool/ydotool, a real browser, LibreOffice). The same plan drives Linux, macOS or Windows nodes unchanged.

Safety / policy

The node's --allow globs are the security boundary (here: open — every office scheme, including shell://lenovo/process/command/run, may execute). Tighten it by editing node_serve.sh (e.g. drop the shell allow, or only allow query routes). Each route also validates its payload against the input schema before running. The host never holds node secrets — secret:// resolution is off on the node by default.

Files

local-function convention; build_bindings() emits the office bindings.

both-sides log.

Test

python3 -m pytest test_office.py -q     # or: python3 test_office.py

Files

.gitignoreREADME.mdbuild_node_registry.pydeploy_from_host.shnode_serve.shoffice_agent.pyoffice_cli.shtellmesh_bridge.pytest_office.py.benchmarks/data/

View on GitHub →