omnyserver 0.17.0 copy "omnyserver: ^0.17.0" to clipboard
omnyserver: ^0.17.0 copied to clipboard

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 [...]

omnyserver #

pub package Null Safety Dart CI codecov 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.
  • Server Blueprints — declare what a whole machine is, composed from the presets you already share. Each resource says what it should be, so applying twice does nothing, drift is the same check with the apply left off, and a resource you delete from the blueprint is removed from the node. See Blueprints.
  • 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, BlueprintApplied, … 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.

Blueprints — what a server is #

A blueprint declares a whole machine, composed from the presets you already have. Write it in YAML or JSON; the format is fixed when you author it, so a YAML blueprint comes back as the YAML you wrote, comments and all.

# builder.yaml
blueprint: builder
name: Build host
includes: [dev-tools]            # a preset, shared with everything else
resources:
  - { type: formula, name: nmap, ensure: installed }
  - { type: formula, name: docker, ensure: running }
omnyserver blueprint save builder.yaml
omnyserver blueprint assign builder --label role=build   # declare; runs nothing
omnyserver blueprint plan worker-01                      # what would change?
omnyserver blueprint apply --label role=build            # make it so

Each resource says what it should be, not what to do to it. That one choice is why applying twice is a no-op, why drift detection is the same comparison with the apply left off, and why uninstalling is ensure: absent rather than a separate command.

includes names presets and does not copy them, so fixing a shared piece reaches every machine built on it — the next plan on each will say so. blueprint resolved builder shows the flattened result, every resource naming which document it came from and what it overrode.

The node does the planning, because that is where the machine is. A plan built from what the Hub last heard is a plan built from intentions, and a node is something other people also touch.

blueprint plan exits 2 when a node has drifted, so it works as a check in a pipeline without parsing output.

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',
);

// One method per endpoint, returning what it means.
final nodes = await client.nodes(labels: ['env=prod']);
final drift = await client.drift('web-1');          // null if nothing declared
final result = await client.reconcile('web-1');
print('${result.changed} changed on ${nodes.length} nodes');

The raw get/post/put/delete are still there for an endpoint the client does not model yet — and for a test asserting the wire shape, which a typed decoder would paper over.

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

A fleet of servers, in Docker #

example/docker_fleet/ brings up one Hub, three nodes and the dashboard as separate containers — two bare hosts and one with the Dart SDK, so they advertise different capabilities because their machines differ:

docker compose -f example/docker_fleet/compose.yaml up --build -d

open http://localhost:8080                        # the dashboard
dart run example/docker_fleet/fleet_tour.dart     # the same fleet, over the API

docker compose -f example/docker_fleet/compose.yaml down -v

The dashboard gives you the fleet, a node's live status, formulas, declared state, the audit trail and a terminal on any node. Its Library screen is where blueprints and presets are read and written: a blueprint opens on the document somebody authored — YAML, comments intact — with a Resolved view beside it showing what a node is actually sent and where each resource came from, and assignment is by label with every matched node named before anything is declared. Nothing runs from the Library; a node is reconciled from its own page, where Dry run will tell you what that would do first.

The tour walks the same ground over the REST API: label selectors, a formula that succeeds on one host and fails on another, desired state and drift, issuing and revoking a credential.

compose.service.yaml beside it runs the same fleet the way a server does — under systemd, installed with omnyserver service install, so each role is a unit you can systemctl status. See its README.

Blueprints, in Docker #

example/docker_blueprints/ starts three empty containers and turns them into two web servers and a build host, because a document said they should be:

docker compose -f example/docker_blueprints/compose.yaml up --build -d

open http://localhost:8080                             # watch it happen
dart run example/docker_blueprints/blueprint_tour.dart

docker compose -f example/docker_blueprints/compose.yaml down -v

One preset shared by two blueprints, assigned by label, planned by each node against its own machine, and applied with real apt-get. Then the parts that only a declarative model can show: applying again changes nothing; dropping a resource from a blueprint removes it from the node; dropping a resource the machine already had leaves it alone; and editing the shared preset drifts every machine built on it without either blueprint being touched.

The container fleet #

test/docker/ runs a Hub and its nodes as real containers on a real network, driven by docker_commander: a TLS handshake against a certificate issued for the hostname the node dials, capability detection against two different host images, a Hub process that is killed and restarted, and the CLI run from a third container.

They are tagged docker, build an image first, and take minutes rather than seconds, so they are kept out of the default run:

dart test -t docker     # just the container fleet
dart test -x docker     # everything else (what CI's matrix runs)

Without a reachable Docker daemon they report as skipped, not failed.

Author #

Graciliano M. Passos: gmpassos@GitHub.

License #

Apache License - Version 2.0.

0
likes
125
points
171
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Repository (GitHub)
View/report issues

License

Apache-2.0 (license)

Dependencies

args, cryptography, dart_service_manager, http, meta, omnyhub, omnyshell, path, sqlite3, uuid, web, yaml

More

Packages that depend on omnyserver