Verdalia Relay#
Verdalia Relay lets workflows and assistants use services running on your own machine. Use it for local filesystem access, coding-agent sessions with OpenCode or Oh My Pi, Hue lights, or other Relay-backed tools without giving Canopy direct access to your computer.
Installation#
Relay is a single executable. Download it from the Downloads page, verify it, and unpack it.
Linux#
sha256sum -c SHA256SUMS
tar -xzf verdalia-relay-linux-x86_64.tar.gz
./verdalia-relay --help
Needs glibc 2.36 or newer and ALSA for audio playback. Both are present on any desktop Linux with working sound.
Windows#
Unpack the zip anywhere — there is no installer, and nothing is written outside the folder you unpack into and your user profile. Then, from PowerShell in that folder:
Get-FileHash verdalia-relay-windows-x86_64.zip -Algorithm SHA256
.\verdalia-relay.exe --help
Compare the printed hash against the one in SHA256SUMS. Windows 10 or newer, 64-bit. Nothing else to install: audio playback goes through WASAPI, which is part of Windows.
Two things behave differently on Windows, and both are noted where they matter below: configuration lives under %APPDATA%\verdalia-relay rather than ~/.config, and filesystem mount paths are Windows paths.
The executable is unsigned, so SmartScreen shows a warning the first time you run it. Verify the checksum above, then choose More info → Run anyway.
Configuration#
Relay keeps its settings in a single config.json, created with defaults on first run. There are no environment variables except RUST_LOG for log level.
| Platform | Location |
|---|---|
| Linux | ~/.config/verdalia-relay/config.json |
| Windows | %APPDATA%\verdalia-relay\config.json |
Settings are read once, when serve starts. A change takes effect the next time Relay starts.
Plugins#
Each capability Relay offers is a plugin with its own switch, and every switch is on by default. A plugin that is on still needs what it works with:
| Plugin | Name in commands | Needs |
|---|---|---|
| Hue | hue |
A paired bridge — see Hue Bridge |
| Filesystem | filesystem |
At least one mount |
| Coding agent | opencode_acp |
At least one mount, and OpenCode or Oh My Pi installed |
| Audio | audio |
A sound output device |
Filesystem and the coding agent wait while no mount exists; verdalia-relay status lists them as on, waiting for a filesystem mount. A request to a plugin that is off or waiting is answered with a 503 that names what to do.
Changing settings#
Run verdalia-relay config in a terminal to open the settings editor: sections on the left, their settings on the right. Arrow keys move, Space flips a switch, Enter edits a value, r resets it to its default, and s saves. In the Mounts section, a adds a mount and d removes one. The editor will not save a combination Relay would refuse to start with, and names the problem at the bottom of the screen.
The same settings are available as commands, for scripts and remote shells:
verdalia-relay config show [--format json] # every setting
verdalia-relay config get server.port
verdalia-relay config set server.port 9000 # lists are comma-separated
verdalia-relay config unset server.port # back to the default
verdalia-relay config disable audio # or: enable <plugin>
verdalia-relay config mount add projects ~/projects [--read-only]
verdalia-relay config mount remove projects
verdalia-relay config mount list
verdalia-relay config path # where config.json is
verdalia-relay config set --help lists every setting with what it does. A value that is out of range is refused and the file is left as it was. The dynamic section is written by verdalia-relay dynamic pair and is not settable here.
The config file#
Editing the file by hand works too. A config with one mount looks like this:
{
"version": 2,
"server": {
"host": "127.0.0.1",
"port": 8080,
"allowed_origins": [
"http://localhost:8081",
"https://api.verdalia.io"
],
"tls_cert_path": null,
"tls_key_path": null
},
"hue": { "enabled": true },
"filesystem": {
"enabled": true,
"mounts": [
{
"name": "workspace",
"path": "/absolute/path/to/mount",
"read_only": false
}
]
},
"opencode_acp": {
"enabled": true,
"agent": "opencode",
"opencode": { "command": "opencode", "args": ["acp"] },
"omp": { "command": "omp", "args": ["acp"] },
"max_sessions": 32
},
"dynamic": {
"enabled": false,
"api_base_url": null,
"websocket_url": null,
"relay_identity_id": null,
"connection_id": null,
"workspace_id": null,
"machine_id": null,
"private_key_path": null
},
"audio": {
"enabled": true,
"max_clip_bytes": 16777216,
"max_concurrent_clips": 8
}
}
Note: a config file without "version" is from an earlier Relay. The first command that reads it turns every plugin on and adds "version": 1; switch a plugin off again with verdalia-relay config disable <plugin>.
Server Configuration#
| Field | Default | Description |
|---|---|---|
host |
127.0.0.1 |
Bind address for the HTTP server. Use 0.0.0.0 for all interfaces (required when reverse-proxying). |
port |
8080 |
HTTP port. |
allowed_origins |
Canopy origins | CORS allowed origins. Empty array allows all origins (insecure). |
tls_cert_path |
null |
Path to TLS certificate file. Set alongside tls_key_path to enable HTTPS. |
tls_key_path |
null |
Path to TLS private key file. |
CLI flags --host and --port on the serve subcommand override config file values. Config file values override built-in defaults.
TLS: When both tls_cert_path and tls_key_path are set, the main server runs HTTPS. Otherwise HTTP. For production, consider reverse-proxying through Nginx or Caddy instead.
Filesystem Mounts#
Filesystem mounts define which local directories Relay exposes to authenticated requests. Add them with the editor's Mounts section or from the command line:
verdalia-relay config mount add workspace /home/user/projects
verdalia-relay config mount add docs /home/user/documents --read-only
A relative path is resolved against the current directory before it is stored. In the file, the same two mounts are:
{
"filesystem": {
"enabled": true,
"mounts": [
{ "name": "workspace", "path": "/home/user/projects", "read_only": false },
{ "name": "docs", "path": "/home/user/documents", "read_only": true }
]
}
}
Each mount requires:
- name — lower case, digits, hyphens, underscores only (e.g.
workspace,my-project) - path — absolute path to an existing directory. On Windows that means a drive letter, and because the config file is JSON, each backslash is doubled:
"C:\\Users\\you\\projects". - read_only — when
true, write operations (write, append, edit, create, delete, move) are rejected at the API level
Mount names appear in workflow nodes and assistant tools as selectable targets. With no mounts, the Filesystem and coding-agent plugins wait and answer 503.
Coding agent#
Relay can host coding-agent sessions over the Agent Client Protocol, each running inside one of the mounts, with either OpenCode or Oh My Pi (omp). A Relay runs one of them: agent picks which, and Agent Coding and the Relay ACP node list, start, and open that agent's sessions. It is on by default and starts working once a mount exists; turn it off with verdalia-relay config disable opencode_acp.
verdalia-relay config set opencode_acp.agent omp # or: opencode
{
"opencode_acp": {
"enabled": true,
"agent": "opencode",
"opencode": { "command": "opencode", "args": ["acp"] },
"omp": { "command": "omp", "args": ["acp"] },
"max_sessions": 32
}
}
| Field | Default | Description |
|---|---|---|
enabled |
true |
Run coding-agent sessions. Waits until at least one filesystem mount exists. |
agent |
opencode |
The agent every session runs: opencode or omp. |
opencode.command |
opencode |
OpenCode binary path or name. |
opencode.args |
["acp"] |
Arguments that start OpenCode in ACP mode. |
omp.command |
omp |
Oh My Pi binary path or name. |
omp.args |
["acp"] |
Arguments that start Oh My Pi in ACP mode. |
max_sessions |
32 |
Most agents running at once; a session keeps its agent until it is closed. Minimum: 1. |
The agent runs as your user with its own configuration and credentials: log in to OpenCode or Oh My Pi on this machine first. By default Oh My Pi asks before running a shell command; its approval mode decides (tools.approvalMode in its config, or add --approval-mode to omp.args). Relay names each new Oh My Pi session after your first message, and /rename <title> changes the name.
On Windows, set command to the full path of the executable, or to opencode.cmd. Relay
launches the command directly rather than through a shell, and a bare opencode only finds
opencode.exe — not the .cmd shim that npm-installed CLIs put on PATH.
Dynamic Relay Configuration#
The dynamic section is written and managed by the verdalia-relay dynamic pair command. It should not be edited manually.
{
"dynamic": {
"enabled": true,
"api_base_url": "https://api.verdalia.io",
"websocket_url": "wss://api.verdalia.io/ws/relay",
"relay_identity_id": "01J...",
"connection_id": "01J...",
"workspace_id": "01J...",
"machine_id": "01J...",
"private_key_path": "/home/user/.config/verdalia-relay/dynamic-relay-ed25519.key"
}
}
When enabled: true, all other fields must be populated. api_base_url must use https unless the host is localhost.
Connection Modes#
Relay has two connection modes:
| Mode | When to use |
|---|---|
| Static | Your Relay has a stable HTTPS URL (VPS, reverse-proxied host). Canopy sends signed HTTP requests to that URL. |
| Dynamic | Your Relay runs on a laptop, desktop, or home server behind NAT. Relay opens an outbound WebSocket connection to Canopy. |
Static Mode Setup#
- Configure and start Relay on your host.
- Expose it through a stable HTTPS URL (direct TLS, Nginx reverse proxy, Cloudflare Tunnel, or ngrok).
- In Canopy, open Settings → Connections and create a Verdalia Relay connection.
- Choose Static.
- Enter the Relay URL and API key.
- Save and test the connection.
Static mode uses signed HTTP requests. Each request includes:
Authorization: Bearer <api_key>— API key withrelay_prefixX-Relay-Signature— Base64 Ed25519 signatureX-Relay-Timestamp— Unix timestamp (within 5-minute window)X-Relay-Nonce(optional) — Per-request nonce for replay protection
The canonical signed payload is:
<METHOD>\n<path>\n<query>\n<timestamp>\n[<nonce>\n]<body_sha256_hex>
Dynamic Mode Setup#
Install
verdalia-relayon the target machine.In Canopy, open Settings → Connections and create a Verdalia Relay connection.
Choose Dynamic.
Save the connection, then click Generate pairing command.
Run the pairing command shown in Canopy:
verdalia-relay dynamic pair <pairing-code> --api-base-url https://api.verdalia.ioThis generates an Ed25519 keypair saved to
~/.config/verdalia-relay/dynamic-relay-ed25519.key, sends the public key and pairing code to Canopy, and writes thedynamicconfig section.Start Relay and leave it running:
verdalia-relay serveWait for the connection status in Canopy to show Online.
Once online, the Relay maintains a persistent WebSocket connection to Canopy's broker. If the local process stops or the machine sleeps, requests fail instead of being queued.
CLI Reference#
verdalia-relay <SUBCOMMAND>
Subcommands:
serve Start the HTTP server
key Manage API keys
dynamic Manage dynamic relay pairing
hue Manage the paired Hue Bridge and its lights
status Show relay configuration, plugins, and paired state
config Read and change relay settings; opens an editor when run in a terminal
help Print help
verdalia-relay serve#
Starts the Relay HTTP server.
verdalia-relay serve [--host <HOST>] [--port <PORT>]
| Flag | Overrides config |
|---|---|
--host |
server.host |
--port |
server.port |
verdalia-relay key#
Manage API keys for authentication.
# Create a new API key
verdalia-relay key create [--name <NAME>] [--public-key-stdin] [--format <table|json>]
# List existing key bindings
verdalia-relay key list [--format <table|json>]
# Revoke a key binding by ID
verdalia-relay key revoke <ID>
Key management commands require user confirmation before making changes. The API key (prefix relay_) is shown once at creation time and cannot be retrieved later.
key create: Creates a new authorized key binding. Prompts for the Canopy public key (or reads from stdin with --public-key-stdin). The Canopy public key is available in your workspace under Settings → Connections → Verdalia Relay.
verdalia-relay dynamic#
# Pair with Canopy
verdalia-relay dynamic pair <PAIRING-CODE> --api-base-url <URL> [--label <LABEL>] [--format <human|json>]
# Show pairing status
verdalia-relay dynamic status
Hue Bridge#
Pairing and light control live on the command line. Nothing here needs serve to be running,
and none of it goes through Canopy.
# Find a bridge, press its link button when prompted, and store the key
verdalia-relay hue pair
# Show the paired bridge and whether it answers
verdalia-relay hue status
# Lights, rooms, and scenes
verdalia-relay hue lights
verdalia-relay hue light <LIGHT-ID> --on --brightness 0.4
verdalia-relay hue light <LIGHT-ID> --kelvin 2700
verdalia-relay hue all off
verdalia-relay hue groups
verdalia-relay hue group <GROUP-ID> on
verdalia-relay hue scenes
verdalia-relay hue scene <SCENE-ID>
# Forget the bridge
verdalia-relay hue unpair
hue pair searches the network for bridges, asks which one to use when it finds several, and
then waits for the link button — retrying six times, five seconds apart. Pass --ip <ADDRESS>
to skip discovery. Every listing command takes --format json.
hue light with no state flags prints the light's current state instead of changing it. When
--brightness, --kelvin, or --rgb is given without --on or --off, the light keeps its
current switch.
Relay Status#
verdalia-relay status [--format <human|json>]
Shows the config and state paths, the API address and whether something is listening on it, whether each plugin is on, off, or waiting for a mount, dynamic pairing state, the paired Hue Bridge, and how many API keys exist.
Service Setup#
systemd (Linux)#
Relay reads its config, state, and Hue credentials from your home directory, so on a laptop or desktop it belongs in the user manager, where it runs as you.
1. Create ~/.config/systemd/user/verdalia-relay.service.
[Unit]
Description=Verdalia Relay
[Service]
Type=simple
ExecStart=%h/.local/bin/verdalia-relay serve
Restart=always
RestartSec=10
Environment=RUST_LOG=info
[Install]
WantedBy=default.target
Note: directive names are case-sensitive. execstart= is discarded as an unknown key and the
unit then refuses to load with Service has no ExecStart=.
Coding agents: a user service does not read your shell profile, so its PATH is usually only
/usr/local/bin:/usr/bin. An agent installed under your home directory, such as
~/.local/bin/omp, is then not found, and starting a session says so. Give the agent's full path:
verdalia-relay config set opencode_acp.omp.command ~/.local/bin/omp # or opencode_acp.opencode.command
2. Enable and start it.
systemctl --user daemon-reload
systemctl --user enable --now verdalia-relay
systemctl --user status verdalia-relay
The user manager starts at login and stops at logout, taking Relay with it. To run it from boot without logging in:
loginctl enable-linger $USER
Relay logs to stderr only, so journald holds them and RUST_LOG in the unit sets the level. There
is no log file.
journalctl --user -u verdalia-relay -f
As a system service#
On a host nobody logs into — a VPS or home server — install it system-wide instead and name the
user it runs as. Config and state come from that user's home, so pair it with the account that ran
dynamic pair and hue pair.
Create /etc/systemd/system/verdalia-relay.service:
[Unit]
Description=Verdalia Relay
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=your-user
ExecStart=/usr/local/bin/verdalia-relay serve
Restart=always
RestartSec=10
Environment=RUST_LOG=info
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now verdalia-relay
Task Scheduler (Windows)#
Relay is a console program, so the way to run it in the background at login is a scheduled task. From an elevated PowerShell, with the path to where you unpacked it:
$exe = "C:\Tools\verdalia-relay\verdalia-relay.exe"
$action = New-ScheduledTaskAction -Execute $exe -Argument "serve"
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
$settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
-ExecutionTimeLimit 0 -AllowStartIfOnBatteries
Register-ScheduledTask -TaskName "Verdalia Relay" -Action $action -Trigger $trigger `
-Settings $settings -RunLevel Limited
-RunLevel Limited matters: Relay reads its config and Hue credentials from %APPDATA%, so it
must run as you rather than as SYSTEM, which has a different profile and would not find them.
-ExecutionTimeLimit 0 stops Windows killing a long-running task after three days.
Start-ScheduledTask -TaskName "Verdalia Relay"
Get-ScheduledTaskInfo -TaskName "Verdalia Relay"
Relay logs to stderr and nothing else, and a scheduled task discards that. To keep logs, point the task at a wrapper that redirects them:
$action = New-ScheduledTaskAction -Execute "cmd.exe" `
-Argument "/c `"$exe`" serve >> `"$env:LOCALAPPDATA\verdalia-relay.log`" 2>&1"
Networking#
Nginx Reverse Proxy (Static Mode)#
server {
listen 443 ssl;
server_name relay.your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Cloudflare Tunnel#
cloudflared tunnel --url http://localhost:8080
ngrok#
ngrok http 8080
Environment Variables#
| Variable | Description |
|---|---|
RUST_LOG |
Log level filter. Default: info for serve, warn for every other command. |
All other configuration is in ~/.config/verdalia-relay/config.json; see Changing settings.
Identity Database#
Relay stores state in an SQLite database at ~/.config/verdalia-relay/state.db. This includes API key bindings with Ed25519 public key hashes and Hue Bridge credentials.
Use Relay in Workflows and Tools#
After the connection is saved, select it anywhere Canopy asks for a Relay connection:
- Relay Request workflow node
- Relay Filesystem workflow node
- Relay ACP workflow node
- Hue Light Control workflow node
- Relay filesystem assistant tools
The node or tool does not need to know whether the connection is static or dynamic. Choose the connection, configure the action, and run it.
Troubleshooting#
Dynamic Relay says unpaired
Create a new pairing code in Canopy and run the pairing command again on the local machine.
Dynamic Relay is offline
Make sure verdalia-relay serve is still running and the machine has network access to Canopy. Check the log output with RUST_LOG=debug.
A request fails after the machine sleeps
Start Relay again and wait for the connection to return online. Dynamic Relay does not queue requests while offline.
Static Relay fails to connect
Check that the URL is reachable from Canopy, TLS is valid, and the API key matches the key configured in Relay.
Canopy public key is rejected during key creation
The public key shown in your Canopy workspace under Connections → Verdalia Relay must be entered exactly as displayed. Use --public-key-stdin to pipe it directly.