omnyshell 1.17.0
omnyshell: ^1.17.0 copied to clipboard
Secure, Hub-centric remote shell platform written in pure Dart. Clients connect to a Hub by node identity (not host:port); the Hub authenticates, authorizes and brokers encrypted sessions to Nodes ove [...]
omnyshell #
A secure, Hub-centric remote shell platform written in pure Dart.
Inspired by SSH, but instead of connecting to a host:port you connect to a
Hub by node identity. The Hub discovers nodes, authenticates and
authorizes principals, and brokers an encrypted session to the right node —
which may be behind NAT, since nodes dial the Hub outbound.
Traditional SSH OmnyShell
Client ──► Host:Port Client ──► Hub ──► Node
omnyshell connect worker-prod-01
omnyshell exec database-server "uname -a"
All transport is WebSocket-on-TLS (wss) — there is no plaintext or raw
TCP mode. Authentication is pluggable (Ed25519 public keys or bearer tokens),
authorization is enforced by the Hub, and the whole platform is available both
as first-class Dart APIs and as the omnyshell CLI.
API Documentation #
See the API Documentation for the full list of classes and APIs.
Features #
- Hub-centric. Connect by node identity, not by network location. The Hub is service discovery, authentication, authorization, session broker and tunnel coordinator in one.
- Secure by default. Every connection is WebSocket-on-TLS. There is no insecure mode. Login is replay-resistant (the Hub challenges each connection with a single-use nonce that public-key clients must sign).
- Pluggable authentication.
Authenticatorcontract withPublicKeyAuthenticator(Ed25519,authorized_keys-style) andTokenAuthenticator(bearer), or compose both. - Persisted login.
omnyshell loginauthenticates to a Hub once and saves the session to~/.omnyshell/credentials.json(mode600), so every other command runs without credential flags. Sessions are keyed by Hub URL with a remembered default, so you can switch between Hubs;omnyshell logoutclears one or all of them. - Role-based authorization. The Hub authorizes every session open; the
bundled
RoleBasedAuthorizerfails closed. - NAT-friendly tunnels. Nodes dial the Hub outbound and hold a persistent connection; the Hub multiplexes sessions over it and relays bytes.
- Real-time interactive shells & exec. Streaming stdin/stdout/stderr, exit
code propagation, terminal resize and interrupt signals, plus an extensible
local
:commandsystem. Theconnectprompt is a full line editor with persistent per-node history, prefix-aware history search, andssh-style TAB completion of commands and remote paths. - File transfer.
:download/:uploadmove files and directories over a separate parallel Hub connection, with GZip-compressed, resumable, SHA-256-verified streaming — and optional on-node--gz/--zip/--tar.gzarchiving. - TCP tunnels / port forwarding. Expose an internal TCP port — a connected
node's, or your own machine's — on a public port of the Hub, so external
clients reach e.g. a localhost HTTP server through
hub-host:PUBLIC_PORT. Bytes ride the existing multiplexedwssconnection (no extra plane); the Hub binds the public listener within an operator-configured range (--tunnel-port-range 20000-20100, fail-closed when unset) and authorizes each open with the sameRoleBasedAuthorizer. Useomnyshell tunnel open <node> <port>(or--local <port>), the in-session:tunnel <port>command, andomnyshell tunnel list/close. Built ontcp_tunnel'sPortRange. - Drive mounts (OmnyDrive).
omnyshell drivemounts a local directory — or a git repository — onto a path on a connected node and keeps the two in sync over the samewsstransport. Built on OmnyDrive: content-addressed manifests, explicit conflict detection (never a silent merge), one-shotsyncor livewatch, and per-mount read-only/read-write control. Mount state persists in~/.omnyshell/mounts.json. - Reliable. Heartbeats with a Clock-driven watchdog, automatic node reconnect with exponential backoff, and end-to-end backpressure.
- Observable. Structured audit log, hub metrics, and a discovery API.
- Three first-class APIs + a CLI. Embed a Hub, a Node or a Client, or run
the
omnyshellbinary — all built on the same shared core. - Tested. Unit, integration and end-to-end coverage over real
wssloopback connections.
Architecture #
OmnyShell Core (protocol + domain)
│
┌─────────────────┼─────────────────┐
│ │ │
Hub API Node API Client API
│ │ │
└─────────────────┼─────────────────┘
│
CLI
Clients and nodes both speak one multiplexed protocol over a single wss
connection. Control messages travel as JSON on WebSocket text frames; stream
data (stdin/stdout/stderr) travels as binary frames behind a compact 10-byte
header — SSH-channel-style multiplexing. The Hub relays a session by rewriting
the channel id between the client and node ends, never inspecting the bytes.
lib/
├── omnyshell.dart # shared protocol + domain contracts
├── omnyshell_hub.dart # Hub composition root
├── omnyshell_node.dart # Node runtime
├── omnyshell_client.dart # Client SDK
└── src/
├── domain/ # value objects, entities, auth & backend contracts
├── protocol/ # frames, control messages, codec, channels, mux
├── infrastructure/ # wss transport, process backend, authenticators
├── application/ # node runtime, hub broker, client runtime, CLI logic
└── shared/ # errors, clock, id/bytes helpers, JSON helpers
Getting started #
dependencies:
omnyshell: ^1.0.0
OmnyShell uses dart:io for TLS, sockets and process execution, so it runs on
any non-web Dart target. A TLS server certificate is required to run a Hub.
Usage #
Local development quick start #
The Hub needs a TLS certificate and key (there is no plaintext mode). For local use, generate a throwaway dev CA + server certificate and start a Hub:
omnyshell cert gen # writes certs/{ca,server}.{crt,key} (built-in)
./run-hub.sh # generates certs if missing, then starts the Hub
omnyshell cert gen builds a local CA and a Hub server certificate signed by it
(--out directory, --host to add SAN entries, --force to regenerate). It is
the built-in equivalent of the tool/gen-dev-certs.sh script (which remains for
repo checkouts); both shell out to openssl.
run-hub.sh starts a Hub on wss://127.0.0.1:8443 with two demo grants
(alice:s3cr3t:admin and noded:nodetok:node). In other shells, attach a node
and run a command — pass --ca certs/ca.crt so the dev certificate is trusted:
dart run bin/omnyshell.dart node start --hub wss://127.0.0.1:8443 \
--id local-01 --label allow-roles=admin \
--principal noded --token nodetok --ca certs/ca.crt
dart run bin/omnyshell.dart exec local-01 "uname -a" --hub wss://127.0.0.1:8443 \
--principal alice --token s3cr3t --ca certs/ca.crt
dart run bin/omnyshell.dart connect local-01 --hub wss://127.0.0.1:8443 \
--principal alice --token s3cr3t --ca certs/ca.crt
Why a CA, not a bare self-signed cert? A self-signed leaf certificate used as its own trust anchor is rejected by Dart's TLS stack when a client verifies it.
tool/gen-dev-certs.shtherefore creates a small local CA and a server certificate signed by it (with thekeyCertSign/serverAuthusages Dart requires). Clients trust the CA via--ca certs/ca.crt. For production, use a certificate from a real CA. There is no insecure/skip-verify mode.
If you only need the Hub to start (e.g. for embedding tests), a single self-signed certificate is enough, since the Hub only presents it:
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365 \
-subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
dart run bin/omnyshell.dart hub start --cert cert.pem --key key.pem \
--grant-token "alice:s3cr3t:admin"
Run a Hub #
omnyshell hub start \
--host 0.0.0.0 --port 8443 \
--cert server.crt --key server.key \
--grant-token "alice:s3cr3t:admin" \
--authorized-keys ./authorized_keys
authorized_keys lines are principal base64-ed25519-key role1,role2 Name.
Run a Node #
omnyshell node start \
--hub wss://hub.example.com:8443 \
--id worker-prod-01 \
--label env=prod \
--principal node-account --token "$NODE_TOKEN" \
--ca server.crt
Interactive sessions are served on a real pseudo-terminal allocated by the
system script(1) utility — no FFI and no native library to install. The
child shell gets a genuine tty at the client's requested geometry, so
full-screen programs such as nano, vim and htop work. Select the backend
with --pty-backend:
omnyshell node start --pty-backend script # default: system script(1), no native lib
omnyshell node start --pty-backend none # pipe-based shell, env-var geometry only
The script backend honours only the initial geometry — it cannot
propagate live resize (SIGWINCH) to the remote terminal. (A native FFI
backend with live-resize support exists but is currently disabled pending a
fix to an upstream crash.) On platforms where script is unavailable (e.g.
Windows), the node transparently falls back to a pipe-based shell and conveys
the initial geometry via the TERM/COLUMNS/LINES environment variables.
Node environment profile (~/.omnyshell/profile.yaml)
Sessions run the node's shell non-interactively, so no rc file (.zshrc,
.bashrc, …) is sourced and $PATH starts bare. The node instead applies an
env profile at ~/.omnyshell/profile.yaml to every shell and exec session:
# ~/.omnyshell/profile.yaml — managed by `omnyshell node`
env:
PATH: "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
EDITOR: vim # add your own vars; they survive PATH sync
Values may reference others with ${VAR} (expanded against the node's
environment, e.g. PATH: "/opt/bin:${PATH}").
On an interactive node start, the node derives PATH from your shell rc
(it runs your login+interactive shell and reads the resulting PATH) and, when
it differs from the stored profile, shows the change and asks before writing:
Detected PATH from your shell profile (~/.zshrc):
+ /opt/homebrew/bin
+ ~/.cargo/bin
Update ~/.omnyshell/profile.yaml? [y/N]
Disable that prompt with --no-profile-sync, or point at a different file with
--profile <path>. A non-interactive start (e.g. as a service) never
modifies the profile — it loads the existing file and prints a hint. Refresh the
profile on demand with:
omnyshell node profile sync # prompts before writing
omnyshell node profile sync --yes # write without prompting
Run as a system service #
Install the Hub or Node as a native OS service (systemd on Linux, launchd on
macOS, the Service Control Manager on Windows) so it starts at boot and is
restarted on failure. This wraps
dart_service_manager: the flags
you pass to service install are the exact hub start / node start flags, and
they are captured into the service definition.
# Install + start (user scope — no elevated privileges needed):
omnyshell service install hub \
--cert server.crt --key server.key \
--grant-token "alice:s3cret:admin"
omnyshell service install node \
--hub wss://hub.example.com:8443 \
--id worker-prod-01 --label env=prod \
--principal node-account --token "$NODE_TOKEN" --ca server.crt
# Inspect what would be installed without touching the system:
omnyshell service install hub --cert server.crt --key server.key \
--grant-token "alice:s3cret:admin" --dry-run
# Lifecycle (role is hub|node):
omnyshell service status hub
omnyshell service stop hub
omnyshell service start hub
omnyshell service restart hub
omnyshell service uninstall hub
- Scope. Installs to the current user by default. Add
--systemto install machine-wide (requiressudo/Administrator). Under--systemthe service runs withOMNYSHELL_HOME=/var/lib/omnyshell(override with--data-dir) so it has a stable home for its UID/state files. - Path flags are absolutized (
--cert,--key,--ca,--authorized-keys) at install time, so relative paths work regardless of the service's working directory. - Flags are captured at install time. To change them later, re-run with
service install --force <role> …, orservice reconfigure <role> …(which preserves the running state). - Secrets: tokens passed as flags are stored in the generated unit/plist. Restrict access to that file, or keep tokens out of the command line by other means, on shared machines.
- Windows runs a private copy. On Windows the service runs through Task
Scheduler (not the SCM, which kills a plain console app with error 1053). A
dart pub global activateinstall runs asdart <snapshot>, and Windows locks that pub-cache snapshot while the service runs — soservice installstages a private copy under%LOCALAPPDATA%\OmnyShell\bin(or%OMNYSHELL_HOME%\bin) and points the task there. This lets youpub global activatea new version freely; eachservice installthen refreshes the copy to the currently-installed version (re-runservice install --force <role>to pick up an upgrade).
Log in once #
omnyshell login authenticates to a Hub (verifying the credentials with a real
handshake) and saves the session locally, so the commands below don't need
--hub, --principal, --token/--key or --ca every time:
omnyshell login --hub wss://hub.example.com:8443 \
--principal alice --token "$TOKEN" --ca server.crt
omnyshell logout # forget the current Hub
omnyshell logout --hub wss://...:8443 # forget a specific Hub
omnyshell logout --all # forget every saved session
The session is written to ~/.omnyshell/credentials.json (mode 600). Logins
are keyed by Hub URL with a remembered default, and explicit credential flags
always override the saved session. For key-based login, pass --key instead of
--token; the saved session references the seed file by path rather than
copying the secret.
If you log in with --insecure-skip-verify (TLS verification disabled, for
self-signed/dev hubs), login asks whether to remember it for that Hub:
Store --insecure-skip-verify so future commands to wss://… also skip TLS verification? [y/N]
Answer y to persist it on the saved session, so later commands reuse the
insecure setting without re-passing the flag; answer n (the default, and what
a non-interactive login assumes) to keep it one-off. Re-running login without
the flag, or logout, clears the remembered setting.
Connect, exec and discover #
After login, run any client command with no credential flags:
omnyshell connect worker-prod-01
omnyshell exec worker-prod-01 "uname -a"
omnyshell exec worker-prod-01 "make build" --cwd /srv/app # set the working dir
omnyshell nodes list
omnyshell whoami
Or pass credentials explicitly (and target another Hub) on any single command:
omnyshell connect worker-prod-01 --hub wss://hub.example.com:8443 \
--principal alice --token "$TOKEN" --ca server.crt
Run against a local directory (run) #
omnyshell run is "edit locally, build remotely, get the results back" in one
command: it mounts a local directory onto the node (pushing the files up), runs
a command inside that directory, then syncs whatever the command produced
back down to local. It wraps the Drive mount
machinery, so it rides the same authenticated wss session — no extra ports.
# Mount the current directory, build remotely, sync the build output back.
omnyshell run worker-prod-01 "make build"
# Use a specific local directory and sync back periodically while it runs.
omnyshell run worker-prod-01 "pytest" --dir ./project --sync-interval 5
# Co-mount a sibling dependency so it stays reachable by its local relative path.
omnyshell run worker-prod-01 "make" --with ../dependency-project
# Throwaway run: tear the mount down and delete the node copy when done.
omnyshell run worker-prod-01 "make" --unmount --clean-remote
When the command needs a sibling directory that the project references by a
relative path (e.g. a build that reads ../dependency-project), add `--with