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:
- 27 — a real LLM plans over the tellmesh URI surface.
- 30 — control a computer from another over the urirun node/host
mesh (no RDP; least-privilege, policy-gated routes over HTTP).
- 24 — adopt the
../tellmesh/*libraries
(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
- no
--yes→ commands are not dispatched (queries still run); add--yesto execute. --dry-run→ plan and show, dispatch nothing.--json→ machine-readable trace.- model/key come from
examples/.env; with no key it falls back to a deterministic
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
- The node keeps
~/.urirun-node/authorized_keys. The first enrollment on a fresh
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).
/deploythen authenticates by an ed25519 signature over the request
(purpose + timestamp + body hash, ±300 s) — urirun host deploy --identity KEY.
- Token auth still works in parallel: set
URIRUN_NODE_TOKENon the node and pass
--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)
--code FILEships a handler module the node writes to~/.urirun-node/deploy/and
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.
- Token:
urirun node serve --generate-token(whatnode_serve.shuses) mints a
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.
- Security:
/deployis off unless a token is set, and every call must send the
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:
- host side — live trace +
generated/host-run-<ts>.log+generated/run-log.md/.json. - node side — before/after each step the host writes to
log://lenovo/session/command/write,
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}}'
- live — watch the office node act in real time (every
/run+ every error) as the
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
| intent | URI(s) the LLM picks |
|---|---|
| log into a website / fill a form | browser://lenovo/page/open, browser://lenovo/form/command/submit |
| download / open a page, read it | browser://lenovo/page/open, browser://lenovo/page/query/dom |
| type / hotkeys / mouse on the box | him://lenovo/keyboard/command/type-text, …/keyboard/command/hotkey, him://lenovo/mouse/command/click |
| open / save / export a document | urioffice://lenovo/document/command/open, …/command/export-pdf |
| screenshot / OCR-driven clicking | kvm://lenovo/monitor/{n}/query/screenshot, kvm://lenovo/task/command/click-text |
| run anything on the machine | shell://lenovo/process/command/run |
| screen frames | screen://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
tellmesh_bridge.py— adapt tellmesh(payload, context)handlers to urirun's
local-function convention; build_bindings() emits the office bindings.
build_node_registry.py— office bindings + base (health/log/proc) → compiled registry.node_serve.sh— run on the node: build + (re)serve on:8765.office_agent.py/office_cli.sh— run on the host: NL → LLM plan → dispatch →
both-sides log.
test_office.py— offline end-to-end test (fake node + heuristic planner).
Test
python3 -m pytest test_office.py -q # or: python3 test_office.py