# Loopback relay (0.5.5) Loopback is a remote-control harness. Three parts, all over WebSocket: - relay: this server; only routes messages, holds no state on disk. - agent: runs on a target machine (e.g. a Windows kiosk); dials this relay and registers. - mcp bridge: an MCP server on the operator's machine; connects here, lists agents, attaches to one, drives it. Relay WebSocket URL: wss://loopback.elevated.app Health: https://loopback.elevated.app/healthz Auth: every agent and controller must present the shared token (ask the relay operator; it is not published here). ## Run an agent on a target machine # Download and run on the kiosk (PowerShell): Invoke-WebRequest https://loopback.elevated.app/download/loopback-agent-windows-x64.exe -OutFile loopback-agent.exe .\loopback-agent.exe --relay wss://loopback.elevated.app --token --root C:\kiosk-app ## Set up the MCP bridge (operator machine, e.g. Claude Code) The bridge is launched over stdio by your MCP client; it takes NO command-line arguments and is configured entirely through environment variables: LOOPBACK_RELAY_URL = wss://loopback.elevated.app (this is also the built-in default) LOOPBACK_TOKEN = (required; ask the relay operator) LOOPBACK_CLIENT = (optional; auto-attach to one agent) Step 1 - download the bridge for the operator's OS: Windows (PowerShell): Invoke-WebRequest https://loopback.elevated.app/download/loopback-mcp-windows-x64.exe -OutFile loopback-mcp.exe macOS: curl -L https://loopback.elevated.app/download/loopback-mcp-darwin-arm64 -o loopback-mcp && chmod +x loopback-mcp && xattr -d com.apple.quarantine loopback-mcp 2>/dev/null || true Step 2 - register it with Claude Code (one command): claude mcp add loopback -e LOOPBACK_RELAY_URL=wss://loopback.elevated.app -e LOOPBACK_TOKEN= -- /loopback-mcp (on Windows use the full path to loopback-mcp.exe) ...or add it to a project .mcp.json: { "mcpServers": { "loopback": { "command": "/loopback-mcp", "env": { "LOOPBACK_RELAY_URL": "wss://loopback.elevated.app", "LOOPBACK_TOKEN": "" } } } } Step 3 - use it. The bridge exposes these tools; typical flow: 1. remote_list_hosts -> see which kiosks are online 2. remote_use -> attach to one (auto-attaches if only one, or if LOOPBACK_CLIENT set) 3. remote_screenshot -> see the screen; remote_run / remote_read_file -> inspect; remote_click/type/key -> fix Signing notes: - macOS binaries are ad-hoc signed, so they launch on Apple Silicon; a downloaded copy is still quarantined, so run `xattr -d com.apple.quarantine ` once (shown above). - Windows binaries are not code-signed; SmartScreen may warn on first interactive launch (Unblock-File the download, or deploy via the scheduled task, which does not prompt). The relay default is already this URL, so LOOPBACK_TOKEN is usually the only value you must supply. Kiosk agent runs headless: on Windows, `loopback-agent --install --token --user ` builds a no-console tray host and a logon task in one step (`--uninstall` to remove). The agent shows a tray icon (green = connected) and its log rotates so it cannot fill the drive. On locked-down networks the socket resets periodically; the agent auto-reconnects. Windows kiosk notes (ANY multi-desktop kiosk, vendor-neutral): - remote_screenshot and remote_click/type/key act on the current INPUT desktop via a compiled native helper, so they reach a passenger UI running on its own Win32 desktop even though the agent is on Default. Pass `desktop` to target a named desktop. - A long-lived service that would inherit the call's stdio (e.g. a platform launcher .bat) must be started with remote_run background:true, or the call hangs. - HOW you make a given desktop the input desktop is platform-specific (see below). ## Platform-specific: Elevation CUSS2 (ElevatedAI) ONLY — activating an airline IMPORTANT: everything in THIS section is EXCLUSIVE to Elevation's CUSS2 platform (a.k.a. ElevatedAI / c2-platform). Other CUSS vendors' platforms use different activation APIs, desktop layouts, tenant ids and scripts. Do NOT assume any of the below on a non-Elevation kiosk: first identify which platform is installed/running, then use that platform's own mechanism (its docs, config, or local API). On an Elevation CUSS2 kiosk (c2-platform in C:\cuss2): each airline (a "tenant") runs on its own Windows desktop; the common-use launch screen is the CLA desktop. Only the ACTIVE tenant's desktop is the input desktop, so remote_screenshot and remote_click/type/key reach an airline app ONLY after that airline is made ACTIVE. A FRESH kiosk has ONLY the CUSS2 platform (in C:\cuss2) and its local control API — there are NO helper scripts in C:\kiosk-app or anywhere else by default. Do NOT assume any .ps1 helper exists. Drive the platform through its control API; if you want a helper, WRITE it to the kiosk yourself with remote_write_file, then run it with remote_run (shell=powershell). Control API: ws://[::1]:22222/cla (IPv6 loopback ONLY — 127.0.0.1 does NOT connect). JSON text frames. The platform also sends periodic {"type":"ping"} frames — ignore them. Flow: -> {"type":"hello","client":"...","version":"1"} <- {"type":"hello_ack","airlines":[{"id":"FTT:FFT","tenantId":"FTT","state":"AVAILABLE","desktop":"..."},...]} (this is how you LIST airlines + live state; there is no separate list call) Launch/activate an airline (NO auth): -> {"type":"launch","airlineId":"","lang":"en"} Restart a stuck airline (auth REQUIRED): -> {"type":"tester_auth","requestId":"a1","pin":""} then after <- {"type":"tester_auth","status":"OK"}: -> {"type":"tester_restart_tenant","requestId":"r1","tenantId":""} The tester PIN is deliberately NOT in this doc — get it from the relay operator and pass it at runtime; never hardcode it into a file left on the kiosk. Windows PowerShell speaks this API via System.Net.WebSockets. On a fresh kiosk, write this helper once with remote_write_file (e.g. to C:\cuss2\cla-control.ps1), then invoke it: --- cla-control.ps1 --- param([ValidateSet('list','launch','restart')][string]$Action='list',[string]$TenantId,[string]$Pin,[string]$Url='ws://[::1]:22222/cla') $ErrorActionPreference='Stop' $ws=New-Object System.Net.WebSockets.ClientWebSocket;$cts=New-Object System.Threading.CancellationTokenSource function Send($o){$j=$o|ConvertTo-Json -Compress;$b=[Text.Encoding]::UTF8.GetBytes($j);$ws.SendAsync([ArraySegment[byte]]::new($b),'Text',$true,$cts.Token).Wait()} function Recv($ms){$buf=New-Object byte[] 65536;$sb=New-Object Text.StringBuilder do{$t=$ws.ReceiveAsync([ArraySegment[byte]]::new($buf),$cts.Token);if(-not $t.Wait($ms)){return $null};$r=$t.Result;if($r.MessageType -eq 'Close'){return $null};[void]$sb.Append([Text.Encoding]::UTF8.GetString($buf,0,$r.Count))}while(-not $r.EndOfMessage) $sb.ToString()} $ws.ConnectAsync([Uri]$Url,$cts.Token).Wait() Send @{type='hello';client='cla-control';version='1'} $ack=$null;$d=(Get-Date).AddSeconds(5) while((Get-Date) -lt $d){$m=Recv 1000;if($m){$o=$m|ConvertFrom-Json;if($o.type -eq 'hello_ack'){$ack=$o;break}}} if(-not $ack){Write-Host 'NO_HELLO_ACK (platform stopped? start it first)';exit 1} if($Action -eq 'list'){$ack.airlines|ForEach-Object{Write-Host ("{0} tenant={1} state={2} desktop={3}" -f $_.id,$_.tenantId,$_.state,$_.desktop)};exit 0} $t=$ack.airlines|Where-Object{$_.tenantId -eq $TenantId}|Select-Object -First 1 if(-not $t){Write-Host "UNKNOWN_TENANT $TenantId";exit 2} if($Action -eq 'launch'){Send @{type='launch';airlineId=$t.id;lang='en'}} elseif($Action -eq 'restart'){ Send @{type='tester_auth';requestId='a1';pin=$Pin};$ok=$false;$d=(Get-Date).AddSeconds(5) while((Get-Date) -lt $d){$m=Recv 1000;if(-not $m){continue};if($m -match '"type":"ping"'){continue};$o=$m|ConvertFrom-Json;if($o.type -eq 'tester_auth'){$ok=($o.status -eq 'OK');break}} if(-not $ok){Write-Host 'AUTH_FAILED (bad PIN?)';exit 3};Send @{type='tester_restart_tenant';requestId='r1';tenantId=$TenantId}} $d=(Get-Date).AddSeconds(18);while((Get-Date) -lt $d){$m=Recv 1000;if($m -and ($m -notmatch '"type":"ping"')){Write-Host "<< $m"}} try{$ws.CloseAsync('NormalClosure','bye',$cts.Token).Wait(2000)|Out-Null}catch{} Write-Host 'DONE' --- end cla-control.ps1 --- List: powershell -ExecutionPolicy Bypass -File C:\cuss2\cla-control.ps1 -Action list Launch: powershell -ExecutionPolicy Bypass -File C:\cuss2\cla-control.ps1 -Action launch -TenantId FTT Restart: powershell -ExecutionPolicy Bypass -File C:\cuss2\cla-control.ps1 -Action restart -TenantId FTT -Pin After launching an airline its desktop becomes the input desktop, so remote_screenshot / remote_click / remote_type then act on that airline (kiosk screen is 1280x1024; coordinates are screen pixels). Exit codes: 0 ok, 1 platform not reachable, 2 unknown tenant, 3 bad PIN. Start / stop the whole platform (these .bat files DO ship with the platform, in C:\cuss2): - START (do it EXACTLY like this): remote_run with command = C:\cuss2\startCUSS.bat cwd = C:\cuss2 <-- REQUIRED, see below background = true <-- REQUIRED, see below Why cwd MUST be C:\cuss2: startCUSS.bat just runs `start c2-platform-windows-x64.exe` with NO `cd`, so the platform inherits the caller's working directory and resolves its config + cache relative to it. Launched from anywhere else (e.g. the loopback agent's own folder like C:\cuss2\loopback) the platform DOES start but reads the WRONG config/cache and misbehaves — this is the #1 reason agents fail to bring the platform up. Forcing cwd=C:\cuss2 fixes it. Why background:true: a foreground remote_run of startCUSS.bat never returns and HANGS the call. (If the agent is path-jailed, C:\cuss2 must be inside its --root or the cwd is rejected — run the agent with --root C:\cuss2.) Ready in ~10-25s; then -Action list shows the tenants AVAILABLE. - STOP (graceful, returns the screen to the Default desktop): remote_run command C:\cuss2\stopCUSS.bat (cwd C:\cuss2 is safest here too). If -Action list returns NO_HELLO_ACK the platform is stopped — start it first. Airline (tenant) state machine: STOPPED -> INITIALIZE -> UNAVAILABLE -> AVAILABLE -> ACTIVE. - launch works ONLY from AVAILABLE; launching a STOPPED/UNAVAILABLE tenant returns WRONG_APPLICATION_STATE. A tenant becomes AVAILABLE only once its airline web app (Chrome, served by the airline backend) has started and registered. -Action list shows each state. - tester_restart_tenant only relaunches a tenant the platform is already managing; for a STOPPED/never-launched tenant it returns OK but is a no-op and cannot revive an app whose backend never connected — reload the whole platform (stopCUSS.bat then startCUSS.bat) or fix the backend instead. ## Tools the bridge exposes remote_list_hosts, remote_use, remote_info, remote_read_file, remote_write_file (append:true to append), remote_edit_file, remote_list_dir, remote_stat, remote_mkdir, remote_remove, remote_move, remote_glob, remote_grep, remote_run (bash/sh/zsh/cmd/powershell/pwsh; background:true = launch detached, return now), remote_screenshot (PNG/JPEG; Windows captures the current input desktop; optional desktop=), remote_download (stream large files/logs to the operator), remote_click, remote_type, remote_key (drive the UI; Windows injects on the input desktop; optional desktop=). Connection: the agent and bridge keep the link alive automatically and reconnect in under 2s if it drops, so a brief reconnect in the logs is normal, not an error. ## Downloads - agent macOS arm64: https://loopback.elevated.app/download/loopback-agent-darwin-arm64 (59.1 MB) - agent macOS x64: https://loopback.elevated.app/download/loopback-agent-darwin-x64 (66.2 MB) - agent Windows x64: https://loopback.elevated.app/download/loopback-agent-windows-x64.exe (82.3 MB) - mcp macOS arm64: https://loopback.elevated.app/download/loopback-mcp-darwin-arm64 (59.4 MB) - mcp macOS x64: https://loopback.elevated.app/download/loopback-mcp-darwin-x64 (66.5 MB) - mcp Windows x64: https://loopback.elevated.app/download/loopback-mcp-windows-x64.exe (82.8 MB)