Skip to main content

Storm Pulse Agent

CI License: AGPL-3.0 Typed: mypy strict

Secure server management agent for Storm Developments. Connects outbound to a Django dashboard over WebSocket with mTLS, pushes system metrics, and executes whitelisted deploy commands. Zero listening ports.

How It Works

  1. Agent connects outbound to the dedicated Pulse transport at wss://pulse.stormdevelopments.ca/ws/pulse/. Caddy requires and verifies the agent's client certificate before proxying the WebSocket to Django.
  2. Sends a register message (including its available commands list), then pushes metrics every 15s (CPU, memory, disk, load, containers).
  3. Dashboard sends HMAC-signed commands. Agent verifies signature, nonce, and expiry before executing.
  4. Commands run via subprocess.run(shell=False) against a strict whitelist. Custom commands can be added via config with optional overridable parameters (regex-validated). No shell injection possible.

Read the Protocol Specification for exact information.

Security

Five layers, each independent:

  • Network -- No inbound ports. Agent initiates all connections.
  • Transport -- mTLS with per-agent certs from a private CA.
  • Application -- HMAC-SHA256 + nonce + expiry on every command.
  • Execution -- Whitelisted commands only. Absolute paths. shell=False. Config placeholders from local config only; runtime params are regex-validated.
  • OS -- Rootless by default: a sudo-less operator user against rootless Docker, no host root, no docker group. Systemd sandboxing. (A legacy system-mode install under a dedicated system user is still supported.)

See the Security Architecture wiki page for the full design. Found a vulnerability? SECURITY.md has the reporting path.

Setup

Requires Python 3.12+. Three runtime deps: websockets, psutil, cryptography.

Install from PyPI:

pip install storm-pulse-agent

For full setup instructions (operator user, permissions, systemd, firewall), see the Setup Guide.

Storm Developments uses two deliberately separate endpoints:

Purpose Endpoint Client certificate
One-time enrollment https://stormdevelopments.ca/api/enroll/ Not required; the new agent does not have one yet
Agent WebSocket wss://pulse.stormdevelopments.ca/ws/pulse/ Required and verified by Caddy

Current dashboards return the dedicated WebSocket URL with the enrollment credentials, and stormpulse init uses it as the prompt default. Verify that the prompt shows pulse.stormdevelopments.ca; agents talking to an older dashboard retain the historical same-host derivation as a compatibility fallback.

Install modes. stormpulse init auto-detects which to use:

  • User mode (rootless), the default on hardened boxes. Runs as a sudo-less operator user against rootless Docker. Config and creds under ~/.config/stormpulse/, data under ~/.local/share/stormpulse/, a systemd user unit. No host root, no docker group, no system user.
  • System mode (legacy). Runs under a dedicated stormpulse system user with a system unit; config and creds under /etc/stormpulse/. Used only where rootless Docker is not present.

Already on a system install? stormpulse migrate-to-rootless converts it in place.

CLI

stormpulse enroll ENDPOINT AGENT_ID TOKEN [--creds-dir DIR] [--force]
stormpulse init [--creds-dir DIR] [--user | --system] [--force]
stormpulse migrate-to-rootless [--force]
stormpulse run [CONFIG]
stormpulse status [CONFIG]
stormpulse signoff status [CONFIG]
stormpulse signoff unseal [CONFIG] [--confirm-hostname HOSTNAME]
stormpulse signoff seal [CONFIG]
stormpulse garage init [--config PATH] [--garage-config PATH] [--force]
stormpulse caddy init [--config PATH] [--force]
stormpulse logging init [--config PATH]
stormpulse update [--source {pip,git}] [--branch BRANCH] [--version VERSION] [--no-restart]
stormpulse --version

enroll -- One-time enrollment. Generates an EC P-256 keypair, sends a CSR to the dashboard, writes the signed cert + CA cert + HMAC key to the credentials directory (~/.config/stormpulse/ for a rootless user-mode install, /etc/stormpulse/ for a legacy system install; override with --creds-dir). The private key never leaves the machine.

init -- Interactive setup wizard. Auto-detects the install mode (rootless user mode when it finds a rootless Docker socket, legacy system mode otherwise; force with --user / --system). Generates config, creates the matching systemd unit (user unit or system unit), sets permissions. Run after enrollment. Auto-detects Garage installations and running Docker containers and offers to enable integration / log shipping.

migrate-to-rootless -- Converts an existing legacy system install to rootless user mode in place. Preserves the agent's cryptographic identity so the dashboard sees the same agent. Use --force to overwrite user-mode files left by a previous migration.

run -- Starts the agent. Connects to the dashboard, sends heartbeats and metrics, executes commands. Reconnects automatically with exponential backoff.

status -- Local inspection. Shows version, agent ID, config path, dashboard URL, certificate expiry, nonce DB entry count, and whether the agent process is running. No network required.

signoff status / unseal / seal -- Manage the verify-block hatch on this host. The agent ships sealed: the dashboard cannot dispatch run_verify_block until the operator opens the hatch with signoff unseal, which requires typing the host's hostname back at the prompt (or --confirm-hostname HOSTNAME for automation). signoff seal closes the hatch in one keystroke. The dashboard never gets to seal or unseal: the operator on the host is the only authority. See the Security Architecture page for the threat model.

garage init -- Detects a Garage S3 node and appends a [garage] section to an existing stormpulse.toml. Auto-detects container name from docker-compose.yml. Use --force to overwrite an existing [garage] section.

caddy init -- Detects a Caddy reverse proxy and appends a [caddy] section to the agent config. Sanity-checks the Caddyfile for a Pulse-managed drop-in import line and parses TLS cert lifecycle events out of the Caddy admin API. Use --force to overwrite an existing [caddy] section.

logging init -- Detects running Docker containers and appends [[log_groups]] blocks for each, using source_type = "docker_stream" and the docker_raw parser. Skips containers already present in the config. See Log Shipping for details.

update -- Reinstalls the agent in place via pipx install --force. --source git (default) pulls from the official repo, optionally pinned to --branch; --source pip pulls the published release. --no-restart skips the post-install systemctl restart so you can stage the update without bouncing the agent.

Configuration

Run stormpulse init to generate a config interactively - see the Setup Guide. Key settings:

Section Field Description
agent id Unique identifier for this server
agent pulse_token UUID from the Server record in the dashboard
agent disabled_commands List of command names to remove from the registry (optional)
dashboard url Agent WebSocket URL. Storm Developments installs use wss://pulse.stormdevelopments.ca/ws/pulse/.
project project_dir Absolute path to the deployed project
project compose_file Absolute path to docker-compose.yml
project env_file Absolute path to .env file (optional, passed as --env-file to docker compose)
commands.* Custom commands (optional, see example config)
garage enabled Enable Garage S3 integration (optional, default: absent)
garage container_name Docker container name for Garage (e.g. garaged)
garage config_path Path to Garage config file
garage detector_interval_seconds How often to check for brand-new buckets (optional, default: 2.0)

Documentation

Develop

git clone https://git.stormdevelopments.ca/official-public/storm-pulse.git && cd storm-pulse
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
make check      # pytest + mypy (strict) + architecture/security invariants

make check needs no Docker, no network, and no credentials.

The wire tier

The default suite fakes the systems the agent drives. The wire tier runs against the real ones in throwaway containers, which is the only way to catch a dependency release that renames a JSON field, changes a status code, or reworks an error string.

One directory and one container per Integration, under tests/wire/<name>/. Each owns a <name>-up / test-<name>-wire pair:

make garage-up           # digest-pinned Garage on loopback (ports 3910/3913)
make test-garage-wire    # ~40 tests, ~12s
make garage-down

make test-wire           # every integration at once (needs every container up)

Each harness mints its own credentials on first use, so there is nothing to configure and no secret to source. It fails loudly, never skips, if the container is not up.

Testing a Garage upgrade before the fleet takes it:

GARAGE_IMAGE=dxflrs/garage:v2.4.0 make garage-up && make test-garage-wire

License

AGPL-3.0 - see LICENSE.

Copyright (c) 2026 Mathew Storm.

Metadata

Release files for storm-pulse-agent 0.4.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for storm-pulse-agent 0.4.1
File Size Uploaded
storm_pulse_agent-0.4.1.tar.gz 360.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for storm-pulse-agent 0.4.1
File Interpreter ABI Platform
storm_pulse_agent-0.4.1-py3-none-any.whl Python 3 none any Details

Total release size: 740.6 kB

Release files / storm_pulse_agent-0.4.1.tar.gz

Download URL storm_pulse_agent-0.4.1.tar.gz
Size 360.9 kB
Tags Source
SHA-256 checksum
How to use checksums
3564c737d18da128c7da0b61a3f9387eeb419841e9dc33cccd49f371e140e518
BLAKE2b-256 checksum
How to use checksums
78bb47245ffcfd4a91d7b94481e8a2a729255530796ad79789e88d1d790f5cf1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / storm_pulse_agent-0.4.1-py3-none-any.whl

Download URL storm_pulse_agent-0.4.1-py3-none-any.whl
Size 379.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2371cdf586f9886aa5a8acbcf2b526e5dbcb3465375f01b37205b9f1af23a61f
BLAKE2b-256 checksum
How to use checksums
f6f039127150d35c24af90dd50f1c4e5f840d30d8f79952e3d2af9829b54bca6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

0.5.0

2 release files

This release

0.4.1 This release

2 release files

0.4.0

2 release files

0.2.0

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page