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.
Ormos requires Go 1.25.
Run the latest release directly:
go run github.com/nicodes/ormos@latestOr install it:
go install github.com/nicodes/ormos@latest
ormosPrebuilt 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/ormosOn 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.
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/ormositself — 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.jsonin a dotfiles repo, point--configat 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.keywould 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
}allowedRootsconfines 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.deniedPortsalways wins.terminalsDisabledrefuses 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.
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.
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.
go test -race ./...
go vet ./...
go run . --versionThe repository is intentionally limited to the public agent and shared protocol. The hosted backend, application, and deployment configuration are not part of this repository.