Skip to content

OmnyGrid/omnyserver

Repository files navigation

omnyserver

pub package Null Safety Dart CI GitHub Tag New Commits Last Commits Pull Requests Code size License

A distributed server-orchestration platform written in pure Dart. A central Hub manages a fleet of Node agents — a cloud-like view of every server's status, resources, capabilities, services and deployments, driven from one place.

Nodes dial the Hub outbound over WebSocket-on-TLS (wss) — the same identity-centric, NAT-friendly model as its sibling omnyshell: you address servers by node identity, not host:port. A node behind NAT needs no inbound port, and the Hub needs exactly one: the node channel and the REST API share a single TLS listener.

                       :8443 (TLS)
        ┌──────────────────┴──────────────────┐
        │  /node      → node control channel  │
        │  /shell     → OmnyShell broker      │
   ────►│  /api/v1    → REST API              │◄────  CLI / REST client
        │  /metrics   → Prometheus            │
        └──────────────────┬──────────────────┘
                          HUB
                ▲          ▲          ▲
              wss│       wss│       wss│      (outbound; NAT-friendly)
              Node       Node       Node
omnyserver cert gen --out certs
omnyserver hub start  --cert certs/server.crt --key certs/server.key \
                      --api-token api-secret --grant node-account:node-token:node
omnyserver node start --hub wss://hub:8443 --id worker-01 \
                      --principal node-account --token node-token --ca certs/ca.crt
omnyserver nodes list --api https://hub:8443 --ca certs/ca.crt --token api-secret

Remote shell, from the same Hub

Add --shell and the Hub also brokers OmnyShell sessions — same port, same certificate, same credentials. A node becomes shell-capable with --with-shell, in the same process:

omnyserver hub start  … --shell
omnyserver node start … --with-shell --shell-label allow-roles=admin

omnyshell exec worker-01 'uptime' --hub wss://hub:8443/shell \
                                  --principal alice --token admin-token --ca certs/ca.crt

A standalone OmnyShell node or service can attach to the same Hub just as well — point it at the shell mount:

omnyshell service install node --hub wss://hub:8443/shell --id worker-01 --token …

Everything is available both as first-class Dart APIs and as the omnyserver CLI and a versioned REST API (/api/v1) ready for a future Web UI. Runs on Linux, macOS and Windows.

Built on omnyhub — the transport, node registry, heartbeat watchdog, RPC correlation and HTTP routing are the framework's, so OmnyServer is only what is actually its own.

API Documentation

See the API Documentation for the full list of classes and APIs.

Features

  • Hub orchestrator — node registration, discovery, authentication, live monitoring, audit and event aggregation, with a live model of every node.
  • Node agents — connect, heartbeat, report status & capabilities, execute commands/formulas/presets, and reconnect automatically.
  • Live monitoring — CPU, memory, storage, OS, processes and logs, designed for real-time dashboards.
  • Capability detection — Docker, Podman, Dart, Python, Java, Node.js, Git, SSH, CUDA, Metal, OpenCL — detected dynamically.
  • Formulas & presets — idempotent, cross-platform install/manage procedures, composed into presets, with desired-state reconciliation.
  • Runs as an OS serviceomnyserver service install hub|node installs the Hub or an agent as a systemd unit, a launchd job or a Windows scheduled task, so it survives a reboot. See Run as an OS service.
  • Security — token & Ed25519 public-key auth, role-based authorization, content-derived identity, audit log; RBAC/multi-tenant ready.
  • Persistence — repository abstractions with in-memory, JSON-directory and SQLite backends (PostgreSQL/distributed ready).
  • HTTP API & metrics — versioned REST API with OpenAPI, structured errors, and Prometheus/OpenTelemetry-ready /metrics, served on the Hub's own TLS port beside the node channel — one port to open, one certificate to manage.
  • EventsNodeConnected, HeartbeatReceived, FormulaFinished, PresetApplied, … with subscriptions and streaming.
  • Remote shell — the Hub can also broker OmnyShell sessions on the same port and credentials (--shell), and a node can be both an OmnyServer agent and an OmnyShell node in one process (--with-shell).

Architecture

OmnyServer is a single package with role-based export libraries over a layered lib/src (domain → application → infrastructure, plus protocol and shared).

lib/
  omnyserver.dart        core: models, protocol, contracts
  omnyserver_hub.dart    Hub runtime + server infra + persistence + HTTP API + metrics
  omnyserver_node.dart   Node agent + monitors + capabilities + formulas + services
  omnyserver_cli.dart    CLI as a library (buildRunner, HubApiClient)
  src/
    domain/         value objects, entities, Formula, auth & repository contracts, events
    application/    OmnyServerHub, NodeAgent, NodeFormulaService, EventAggregator
    infrastructure/ auth, monitors, capabilities, persistence, http, metrics
    protocol/       handshake messages + codec, operation payloads
    shared/         errors, json helpers, clock, ids

See doc/architecture.md, doc/protocol.md, doc/security.md, doc/formulas.md and doc/deployment.md.

Getting started

dependencies:
  omnyserver: ^0.3.0

Or install the CLI:

dart pub global activate omnyserver

Supported platforms: Linux, macOS, Windows. Requires the Dart SDK 3.12+.

Usage

Quick start (embedded Hub + Node)

The fastest way to see the whole stack work is the embedded example, which starts a Hub and a Node in one process and prints the node's live status:

dart run example/omnyserver_embedded_example.dart
final hub = OmnyServerHub(HubConfig(
  host: '127.0.0.1', port: 0,
  securityContext: SecurityContext()
    ..useCertificateChain(certs.serverCert)
    ..usePrivateKey(certs.serverKey),
  authenticator: TokenAuthenticator({
    'node-token': TokenGrant(principal: PrincipalId('node-account'), roles: {'node'}),
  }),
));
await hub.start();

final agent = NodeAgent(NodeAgentConfig(
  hubUri: Uri.parse('wss://127.0.0.1:${hub.port}'),
  nodeId: 'demo-node',
  credentials: const TokenCredentialProvider(principal: 'node-account', token: 'node-token'),
  securityContext: SecurityContext(withTrustedRoots: false)..setTrustedCertificates(certs.caCert),
  statusProvider: const SystemMonitor().snapshot,
  capabilityProvider: CapabilityScanner.standard().scan,
));
await agent.start();

// The node pushes a status snapshot as it registers, but it still has to reach
// the Hub — give it a moment before reading.
await Future<void>.delayed(const Duration(seconds: 1));

final status = hub.getStatus(NodeId('demo-node'));
print('${status?.cpu.coreCount} cores, ${status?.cpu.usagePercent}% used');

Run a Hub

dart run bin/omnyserver.dart cert gen --out certs
./run-hub.sh

The Hub only speaks wss and requires a certificate chain + key. Dart's TLS stack rejects a bare self-signed leaf, so cert gen builds a CA → leaf chain; nodes trust the CA with --ca.

With a real certificate (LetsEncrypt, cert-manager, a mounted secret), point the Hub at the directory instead of the two files:

omnyserver hub start --tls-dir /etc/letsencrypt/live/hub.example.com \
                     --api-token api-secret --grant node-account:node-token:node

--tls-dir reads fullchain.pem + privkey.pem and re-checks them periodically: when the certificate is renewed the Hub rebinds its listener with the fresh one — no restart, and established connections drain on the old listener. It replaces --cert/--key; passing both is an error.

Run a Node

dart run bin/omnyserver.dart node start \
  --hub wss://hub:8443 --id worker-01 \
  --principal node-account --token node-token --ca certs/ca.crt

Run as an OS service

hub start and node start run in the foreground and die with your terminal. To keep one running, install it as a native service — a systemd unit on Linux, a launchd job on macOS, a scheduled task on Windows:

omnyserver service install hub  --tls-dir /etc/letsencrypt/live/hub.example.com \
                                --api-token api-secret \
                                --grant node-account:node-token:node
omnyserver service install node --hub wss://hub:8443 --id worker-01 \
                                --token node-token --ca certs/ca.crt

There is no separate daemon and no config file: service install takes the same flags as hub start / node start, and bakes this executable plus those flags into the service definition. Whatever omnyserver hub start … would have done in your terminal is what the service does at boot. Relative paths are resolved before they are baked in, so the unit does not depend on where you ran the install from.

omnyserver service status  hub        # running
omnyserver service info    hub        # the parameters, and the command the OS runs
omnyserver service restart hub
omnyserver service stop    hub
omnyserver service uninstall hub

Two more, for the day after: service reconfigure hub --cors-origin … re-applies changed flags to the installed service, and service reinstall hub refreshes the binary while keeping the config it was installed with — which is how a fleet picks up a new release.

Use --dry-run to print the generated unit/plist without touching the system, and --system to install machine-wide (sudo on Linux/macOS, Administrator on Windows) rather than for your user alone.

Where it keeps its data. A service is supervised forever, so it persists by default. --data-dir names one root holding everything — credentials and identity at the top, and the Hub's fleet data (nodes, audit, metrics, desired state, issued grants) under hub/:

/var/lib/omnyserver/          # --system default (~/.omnyserver otherwise)
  hub/                        # the Hub's fleet data

Pass --ephemeral for a Hub that keeps nothing. Note that omnyserver hub start persists by default too, to ~/.omnyserver/hub — a Hub that forgot the fleet, the audit trail and every credential grant add issued, on every restart, was a footgun with no upside.

Linux, user scope. A user service runs under systemctl --user, which stops at logout unless lingering is enabled. install tries to enable it for you and tells you the exact command (sudo loginctl enable-linger <user>) if it cannot.

Flags become part of the service definition, which means --token, --api-token and --grant are written into a file readable by the user who installed it. Restrict it accordingly.

Discover and operate (via the REST API)

omnyserver nodes list                 --api https://hub:8443 --ca certs/ca.crt --token api-secret
omnyserver node status  worker-01     --api https://hub:8443 --ca certs/ca.crt --token api-secret
omnyserver node restart worker-01     --api https://hub:8443 --ca certs/ca.crt --token api-secret
omnyserver formula run docker worker-01 --action verify --api https://hub:8443 --ca certs/ca.crt --token api-secret
omnyserver preset apply docker-host.json worker-01 --api https://hub:8443 --ca certs/ca.crt --token api-secret

The CLI's operational commands call the Hub's HTTP API — exactly the surface any other client uses.

Every one of them takes either credential the Hub knows:

# The Hub's master API token — one shared secret, audited as "api".
omnyserver node status worker-01 --api https://hub:8443 --token api-secret

# Your own grant (--grant alice:admin-token:admin) — an identity the Hub
# verifies, so the audit trail names you and your roles decide what you may do.
omnyserver node status worker-01 --api https://hub:8443 \
                                 --principal alice --token admin-token

A grant's roles are checked against the Hub's Authorizer before it may touch the API at all, and the fail-closed default reserves it for admin — so node-account's token connects nodes and nothing more, even if it leaks.

Formulas and presets

Ask the Hub what nodes can actually do, rather than guessing into a free-text box:

omnyserver formula list
# FORMULA     NAME              ACTIONS
# dart        Dart SDK          install, update, uninstall, verify
# docker      Docker            install, update, start, stop, restart, uninstall, verify

Save a preset on the Hub once, and apply it by id everywhere:

omnyserver preset save docker-host.json
omnyserver preset apply docker-host --label env=prod

A preset file is whatever copy you happen to have; a saved preset is the one everybody agrees on. preset apply still accepts a file, for a one-off.

Addressing the fleet

Label a node when it starts, then select on the label:

omnyserver node start --id web-01 --label env=prod --label role=web
omnyserver nodes list  --label env=prod
omnyserver nodes list  --offline                      # the ones wanting attention

omnyserver formula run docker --label env=prod --action verify   # every prod node
omnyserver preset apply docker-host.json --all

formula run and preset apply take one node, --node (repeatable), --label, or --all, and report a result per node. A selector matching nothing is an error, not a quiet success — "applied to 0 nodes" reads like it worked.

Issuing credentials

Grants can be baked into the command line (--grant alice:admin-token:admin), or issued at runtime and revoked without restarting the Hub:

omnyserver hub start … --data-dir /var/lib/omnyserver   # or the grants die with it
omnyserver grant add bob --role viewer --note 'read-only dashboard'
omnyserver grant list
omnyserver grant revoke <id>        # the next request with that token fails

The Hub stores a hash, not the token. It is printed once, when issued, and cannot be shown again — so the Hub's storage is not a list of passwords, and a lost token is replaced rather than recovered. That is what the grant id is for.

Issuing and revoking are admin-only, so an operator can run the fleet but not mint itself an admin token.

--data-dir is not optional in production. Without it the Hub keeps nodes, the audit trail, metrics, declared state and issued credentials in memory only, and forgets all of it when it stops.

Desired state, and drift

Declare what a node is supposed to be, then ask — at any point later — whether it still is:

omnyserver state set docker-host.json --label env=prod   # declare; runs nothing
omnyserver state diff --label env=prod                   # has it drifted?
omnyserver state reconcile --label env=prod              # make it true again

Declaring is not applying. preset apply runs steps and tells you they succeeded; it cannot tell you anything a week later, after somebody logged into the machine and changed something by hand. A declaration keeps answering: state diff re-plans against what the node currently advertises, and an empty plan means no drift.

state reconcile runs exactly what the plan says is outstanding, so a converged node does nothing at all — idempotent, and safe on a timer. state diff exits non-zero when anything has drifted, so it works as a check in a pipeline.

Roles

Role May
viewer read the API: fleet, live status, history, events, audit
operator also act: restart, shut down, update, formulas, presets
admin everything
node enrol a node — and nothing else; it cannot reach the API at all

So a read-only dashboard link is a --grant bob:view-token:viewer, and a leaked node credential still cannot operate the fleet.

Long-running work

omnyserver formula run docker worker-01 --action install --async
# worker-01   dispatched — ops show 2f6eadcd-…

omnyserver ops list
omnyserver ops show 2f6eadcd-… --wait

formula run, preset apply and state reconcile answer synchronously, which is right for a verify and wrong for an install: the Hub gives up after its request timeout, the node carries on working, and you are told a failure that did not happen. --async hands back an id instead. The work is the same — only who waits for it changes.

Alerts

omnyserver hub start … --alert 'disk>90' --alert 'cpu>95 for 5m' \
                       --alert 'offline for 2m'
omnyserver alerts    # what is wrong right now; non-zero while anything is

Judged on the heartbeats the Hub already receives. for 5m is what separates an alert from a twitch: a node at 95% CPU for one heartbeat is a build running, and one at 95% for five minutes is a problem. An alert is announced once and resolved when it clears.

There are no default rules — a tool that invents its own thresholds is a tool that pages you at 3am about a disk it decided was too full.

Read a node's log without logging into it:

omnyserver node logs worker-01 -f

The Hub keeps a bounded tail (the last 500 lines per node, in memory) — for looking at a machine that is misbehaving now. It is not the audit trail, and not a substitute for shipping logs somewhere that keeps them. Nodes ship their log by default (node start --no-ship-logs opts out).

Watch the fleet, and read a node's history:

omnyserver events --follow                      # tail -f for the fleet (SSE)
omnyserver node metrics worker-01 --since 1h    # CPU / memory / disk over time
omnyserver audit                                # who did what, as the Hub verified it

node metrics reads history the Hub has been recording on every heartbeat all along — no extra configuration, and it works for any node that has been connected long enough to report twice.

omnyserver whoami answers what the Hub makes of your credentials:

omnyserver whoami --api https://hub:8443 --principal alice --token admin-token
# principal: alice
# roles:     admin

From a browser

The Hub's API is callable from a web app — that is what omnyserver_web, the dashboard, is built on. Two things are needed, and both are needed:

omnyserver hub start --cert certs/server.crt --key certs/server.key \
                     --api-token api-secret --grant alice:admin-token:admin \
                     --cors-origin https://dashboard.example.com
  • --cors-origin — a browser will not hand a page a cross-origin response unless the server says that origin may have it, and a dashboard is always a different origin than the Hub (in development too: webdev on :8080, Hub on :8443). Without it the app sees network errors and nothing else.
  • A publicly-trusted certificate, or one trusted at the OS/browser level. The browser owns the TLS stack; there is no in-page --insecure to offer, and a self-signed Hub simply will not load.

Client code imports the browser-safe barrel, and drives the very same HubApiClient the CLI does:

import 'package:omnyserver/omnyserver_client_web.dart';

final client = HubApiClient(
  Uri.parse('https://hub.example.com:8443'),
  principal: 'alice',
  token: 'admin-token',
);
final nodes = (await client.get('/nodes') as List)
    .map((n) => NodeDescriptor.fromJson((n as Map).cast()))
    .toList();

HTTP API

Served over HTTPS on the Hub's own port, alongside the node control channel — one TLS listener, two surfaces. Versioned under /api/v1:

Method & path Description
GET /nodes list registered nodes
GET /nodes/{id} node descriptor
GET /nodes/{id}/status live status snapshot
GET /nodes/{id}/capabilities advertised capabilities
POST /nodes/{id}/restart · /shutdown · /update node control
POST /nodes/{id}/formula run a formula action
POST /presets/apply apply a preset to a node
GET /events · /audit recent events / audit
GET /openapi.json OpenAPI document
GET /metrics (root) Prometheus exposition

How it works

OmnyServer is built on omnyhub: the transport, node registry, heartbeat watchdog, RPC correlation and HTTP routing are the framework's. What OmnyServer adds is what is actually its own — identity, capability detection, formulas, presets, reconciliation, auditing and persistence.

Connection flow

  1. A Node dials the Hub over wss at /node and sends Hello.
  2. The Hub issues a challenge nonce; the Node answers with a token or a signed nonce. On success the Hub returns the resolved principal and roles.
  3. The Node registers its descriptor (identity, platform, capabilities, labels). The Hub authorizes the registration — a node credential may enrol a node and nothing else — then adds it to the live registry and begins receiving heartbeats, each carrying a live status snapshot.
  4. The Hub dispatches operations (formula, preset, restart, …) as RPCs over the same connection and correlates the replies.

Security envelope

All transport is TLS. Authentication is pluggable (token / Ed25519 public key); authorization is role-based and fail-closed; identity is content-derived; every sensitive action is audited. See doc/security.md.

The HTTP API is reached with the Hub's --api-token, or with a grant's (principal, token) pair — --token plus --principal on the CLI, which the Hub verifies against the same grant store the node channel uses, so the audit trail names the operator and their roles decide what they may do. There is no third way in, and no way in without one of them: a Hub started with neither authenticates nobody, and says so. Only /healthz and /metrics are open, so a load balancer and a Prometheus scraper — neither of which carries a token — can still tell whether the Hub is alive.

The OmnyGrid ecosystem

OmnyServer is one of four packages sharing the same identity-centric, NAT-friendly model — nodes dial a Hub outbound, and you address them by identity rather than host:port.

Package What it does
omnyhub The HUB framework everything below is built on: transport, routing, auth, node registry and control plane.
omnyserver (this) Fleet orchestration — monitoring, capabilities, formulas, presets, desired-state reconciliation.
omnyshell Remote shell — SSH-like sessions brokered to a node by identity. An OmnyServer Hub can host its broker (--shell).
omnydrive File & git drive synchronization.

Roadmap

  • Remote agent self-update and OS-update orchestration.
  • Additional transports (gRPC, QUIC, message bus) behind omnyhub's Transport.
  • PostgreSQL and distributed persistence backends.
  • Richer reconciliation (dependency ordering, version comparison).
  • Web UI on top of the REST API; RBAC / multi-tenant authorization.
  • Kubernetes orchestration and AI-workload scheduling.

Running the example and tests

dart pub get
dart analyze
dart test
dart run example/omnyserver_embedded_example.dart

Author

Graciliano M. Passos: gmpassos@GitHub.

License

Apache License - Version 2.0.

About

Distributed server orchestration platform written in pure Dart. A central Hub manages a fleet of Node agents over WebSocket-on-TLS: live monitoring, capability detection, presets, formulas and desired-state reconciliation, exposed both as first-class Dart APIs and a REST HTTP API. Ships Hub, Node and CLI implementations.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages