ormos
The public system agent for ormos, a personal remote-access
tool. It runs on a machine you own, makes one outbound encrypted connection to
the hosted relay, and serves terminal sessions and explicitly configured local
ports through that connection.
The agent never opens a listening socket. It does, however, accept authenticated
instructions from the Ormos relay to start shells and connect to local services,
so review the policy section below before leaving it running on an important
machine.
Linux and macOS, on amd64 and arm64. That is the whole list: CI tests and
releases ship exactly those four targets, and nothing else is claimed as
supported.
There is no Windows build and none is planned. The agent's job is to act on the
machine it runs on — allocate a PTY, poll it, read a terminal's foreground
process group, signal a shell that will not exit — which it does through
golang.org/x/sys/unix. The root package and every file in internal/system
carry //go:build (linux && !android) || (darwin && !ios), so go build . for
Windows reports that the package does not exist there rather than failing on a
list of missing syscalls.
The tag names the platforms rather than saying unix, which would also claim
the BSDs and Solaris. Nothing is tested there, so nothing is claimed there. The
!android and !ios halves matter for the same reason: Go sets the linux tag
on Android and the darwin tag on iOS, so a plain linux || darwin would build
the whole agent for both. The shared relay package is untagged and stays portable —
the hosted backend imports it.
CI tests on Linux and macOS, cross-compiles every shipped target on Linux, and
asserts that on every other platform Go supports the agent is not selected at
all. A release is gated on the
macOS tests too, since the Darwin archives are what people download.
Live port status is a Linux-only feature for now: the agent finds listening
ports by reading /proc/net/tcp, so on macOS the dashboard shows configured
ports without marking which are up.
Run or install
Ormos requires Go 1.25.
Run the latest release directly:
go run github.com/nicodes/ormos@latest
Or install it:
go install github.com/nicodes/ormos@latest
ormos
Prebuilt Linux and macOS archives, checksums, and build provenance are attached
to each GitHub release. Verify a
download before running it:
gh attestation verify ormos_Linux_x86_64.tar.gz --repo nicodes/ormos
On first run, Ormos shows a short pairing code and a URL. Approve the code in
the web app and the machine registers itself, saving only the resulting pairing
token. No credentials are typed into the terminal, and none are accepted as
flags or environment variables. Later runs reconnect using the saved pairing.
ormos run the system
ormos --config PATH use an isolated config and state directory
ormos --help show help
ormos --version print the version
The production relay is wss://api.ormos.dev. Set ORMOS_API_URL to use
another relay; plaintext URLs are accepted only for loopback development.
Local state and policy
State lives under ~/.config/ormos/:
| File |
Purpose |
config.json |
Pairing token and system identity; mode 0600 |
identity.key |
Long-lived terminal sealing key; mode 0600 |
policy.json |
Optional restrictions enforced by this machine |
sessions.log |
Local JSON-lines audit trail of relay requests |
sessions.log.1 |
The previous generation of that trail, kept across one roll |
--config /some/path/config.json moves all of these into that config file's
directory, which is useful for keeping development and production pairings
separate.
sessions.log is append-only but not unbounded: at 4 MiB it is renamed to
sessions.log.1, replacing any previous generation, and a fresh log starts. Two
files of recent history, never more. The roll is taken under a file lock, so two
agents sharing one state directory cannot roll each other's history away; when
the lock cannot be taken the roll waits for an uncontended write rather than
renaming unlocked — on a filesystem with no flock support at all, that means
the log grows past the bound. The live file's mode is corrected on every open;
the rolled file's is corrected once at startup and inherited from the live file
at each roll.
Whenever the agent reads config.json or identity.key it re-checks what it
finds there, so a copy restored from a backup or loosened by a stray chmod
does not quietly stay readable by other local users:
- group and other permission bits are cleared from the file, and from
~/.config/ormos itself — a writable state directory would let another local
user replace these files rather than read them;
- a symbolic link at either path is refused rather than followed. If you keep
config.json in a dotfiles repo, point --config at the real file instead of
symlinking it into place — a link is now a startup error, not a warning;
- a named pipe or directory at either path is refused too, rather than blocking
the agent forever on an open that never returns;
- a file owned by another user is refused outright. Adopting a planted
identity.key would publish the planter's public key as this machine's and
seal every terminal to a key they hold.
A correction to either file is reported on startup, because tightening a mode
does not undo the exposure. For identity.key the seal has no forward secrecy,
so whoever read it can decrypt captured traffic from past sessions and the only
real remedy is regenerating the key. For config.json the pairing token in it
is a bearer credential, so the remedy is signing out and pairing again.
Owner bits are left as they are, so a key deliberately made 0400 stays 0400.
An optional policy can limit what the relay may ask this machine to do:
{
"allowedRoots": ["~/code"],
"allowedPorts": [3000, 5173],
"deniedPorts": [],
"terminalsDisabled": false
}
allowedRoots confines terminal working directories after resolving
symlinks. An empty list allows any directory.
allowedPorts, when non-empty, is the only set of local ports the agent will
connect to. Otherwise built-in rules still reject privileged and well-known
service ports.
deniedPorts always wins.
terminalsDisabled refuses terminal sessions while retaining port previews.
Policy changes are re-read while the agent is running. The relay's configured
port list is an additional check, not a replacement for local policy.
Security model
The agent holds a per-system pairing token and an X25519 terminal key. Terminal
frames are sealed end to end between the browser and agent; the relay carries
the ciphertext but cannot read terminal contents. Port-preview traffic is
proxied in plaintext at the relay, and this is structural, not a missing
feature: the server terminates TLS to the browser's iframe, so it must hand
over plaintext however the bytes arrived from the agent. Terminals can be
sealed because xterm.js is JavaScript running in the page and can hold a key;
an iframe has no such hook — the browser's own HTTP stack loads the document
and its sub-resources.
A compromised or malicious relay can still request shells and local
connections within the limits of policy.json. Run the agent as an
unprivileged user and set explicit roots and ports when that boundary matters.
The local audit log is evidence for the machine owner, but it is not tamper-proof
against a shell running as the same user.
The shared Go package relay defines the control DTOs, tunnel
framing, and terminal sealing protocol used by both the public agent and the
private hosted backend.
Protocol compatibility
The v0.1 wire format is unchanged from the backend version it was extracted
from. Breaking protocol changes must be rolled out in this order:
- deploy backend support for both the currently released agent and the new
format;
- publish the new agent release;
- remove old-format support only after the compatibility window.
There is no automatic version negotiation in v0.1, so publishing a breaking
agent before compatible server support would strand users of @latest.
Development
go test -race ./...
go vet ./...
go run . --version
The repository is intentionally limited to the public agent and shared
protocol. The hosted backend, application, and deployment configuration are not
part of this repository.