Compare commits

..

No commits in common. "main" and "bun" have entirely different histories.
main ... bun

4 changed files with 99 additions and 109 deletions

View File

@ -27,17 +27,17 @@ Use this agent to review code changes before committing.
**Trigger:** Before creating commits or PRs. **Trigger:** Before creating commits or PRs.
## entity-specialist ## metrics-specialist
Use this agent when working on entity configuration or the entity system. Use this agent when working on system metric collection.
**Responsibilities:** **Responsibilities:**
- Understand entity types (sensor, binary_sensor, switch, light, button) - Understand Linux /proc and /sys interfaces
- Design shell commands for state polling and actions - Know cross-platform metric collection strategies
- Ensure proper Home Assistant discovery payloads - Ensure metrics are properly typed and documented
- Validate entity configuration options - Validate metric units and normalization
**Context:** Systant uses a unified "entity" system where all metrics and controls are defined as entities in TOML config. Each entity has a `state_command` and optionally `on_command`/`off_command`/`press_command` depending on type. **Context:** Systant collects CPU, memory, disk, and network metrics. Metrics should be normalized (percentages 0-100, bytes for sizes) and include metadata for Home Assistant discovery.
## mqtt-specialist ## mqtt-specialist
@ -50,19 +50,20 @@ Use this agent when working on MQTT publishing or Home Assistant integration.
- Handle connection lifecycle (connect, reconnect, disconnect) - Handle connection lifecycle (connect, reconnect, disconnect)
- Design topic hierarchies for commands and events - Design topic hierarchies for commands and events
**Context:** Systant publishes to MQTT with Home Assistant auto-discovery. Topics follow the pattern `systant/{hostname}/{entity_id}/state` for state updates, `systant/{hostname}/{entity_id}/set` for switch/light commands, and `homeassistant/{type}/{hostname}_{entity_id}/config` for discovery. **Context:** Systant publishes to MQTT with Home Assistant auto-discovery. Topics follow the pattern `systant/{hostname}/{metric_type}`. Command topics use `systant/{hostname}/command/{action}`.
## nix-specialist ## events-specialist
Use this agent when working on Nix packaging or the home-manager module. Use this agent when working on the event/command system.
**Responsibilities:** **Responsibilities:**
- Maintain the Nix flake and package definition - Design secure command execution with allowlists
- Update the home-manager module options - Implement event handlers and action dispatching
- Handle fixed-output derivations for npm dependencies - Ensure proper input validation and sanitization
- Ensure cross-system compatibility - Handle timeouts and error reporting
- Consider security implications of remote command execution
**Context:** Systant is packaged as a Nix flake with a home-manager module. The package uses a two-phase build: FOD for `bun install`, then `bun build --compile` for the binary. The home-manager module creates a systemd user service. **Context:** Systant listens for MQTT commands and executes configured actions. Security is paramount - all commands must be validated against an allowlist, inputs sanitized, and execution sandboxed where possible.
## debug-investigator ## debug-investigator

View File

@ -16,50 +16,31 @@ Systant is a lightweight CLI tool written in Bun/TypeScript that:
### Architecture ### Architecture
``` ```
index.ts # CLI entry point index.ts # CLI entry point (yargs)
src/ src/
config.ts # TOML configuration loading commands/ # CLI command handlers
mqtt.ts # MQTT client, publishing, and HA discovery metrics/ # System metric collectors
entities.ts # Entity management (state polling, command handling) mqtt/ # MQTT client and publishing
events/ # MQTT event listeners and handlers
actions/ # Executable actions (shell, service, notify)
ha/ # Home Assistant discovery
config/ # Configuration loading
``` ```
### Entity System ### Event/Command System
Systant uses a unified "entity" concept that combines state monitoring and command handling. Entity types: Systant subscribes to MQTT topics and executes configured actions:
- **sensor**: Read-only numeric/string values (CPU usage, temperature, etc.)
- **binary_sensor**: Read-only on/off states (service running, etc.)
- **switch**: Controllable on/off with `on_command` and `off_command`
- **light**: Same as switch, for display/monitor control
- **button**: Press-only actions with `press_command`
Each entity is defined in TOML with shell commands:
```toml
[entities.cpu_usage]
type = "sensor"
state_command = "awk '/^cpu / {print int(($2+$4)*100/($2+$4+$5))}' /proc/stat"
unit = "%"
icon = "mdi:chip"
name = "CPU Usage"
[entities.headphones]
type = "switch"
state_command = "pactl get-default-sink | grep -q usb && echo ON || echo OFF"
on_command = "pactl set-default-sink alsa_output.usb-..."
off_command = "pactl set-default-sink alsa_output.pci-..."
```
### MQTT Topics
``` ```
systant/{hostname}/{entity_id}/state # State updates Topic: systant/{hostname}/command/{action}
systant/{hostname}/{entity_id}/set # Commands (for switch/light) Payload: { "args": [...], "timeout": 30 }
systant/{hostname}/{entity_id}/press # Button presses
systant/{hostname}/availability # Online/offline status Topic: systant/{hostname}/event/{event_name}
homeassistant/{type}/{hostname}_{id}/config # HA auto-discovery Payload: { ... event data ... }
``` ```
Actions are sandboxed and configurable via allowlists in the config file. Security is critical - never execute arbitrary commands without validation.
### Key Design Decisions ### Key Design Decisions
- **Single binary**: Compiles to standalone executable via `bun build --compile` - **Single binary**: Compiles to standalone executable via `bun build --compile`
@ -70,29 +51,10 @@ homeassistant/{type}/{hostname}_{id}/config # HA auto-discovery
## Tech Stack ## Tech Stack
- **Runtime**: Bun (not Node.js) - **Runtime**: Bun (not Node.js)
- **Config**: TOML (smol-toml) - **CLI**: yargs
- **MQTT**: mqtt.js - **Config**: TOML
- **Packaging**: Nix flake with home-manager module - **MQTT**: mqtt.js or Bun-native when available
- **Package**: Nix flake for reproducible builds
### NixOS/Home Manager Integration
```nix
# flake.nix inputs
inputs.systant.url = "git+ssh://...";
# Add overlay for pkgs.systant
nixpkgs.overlays = [ inputs.systant.overlays.default ];
# Import home-manager module
home-manager.sharedModules = [ inputs.systant.homeManagerModules.default ];
# In user config
services.systant = {
enable = true;
settings = { /* TOML as Nix attrset */ };
# or: configFile = ./systant.toml;
};
```
## Bun Conventions ## Bun Conventions
@ -143,17 +105,10 @@ Watch mode: `bun test --watch`
## Commands ## Commands
```bash ```bash
# Development bun run start # Run in development
bun run index.ts run --config systant.toml bun run dist # Build standalone binary
bun test # Run tests
# Build standalone binary bun test --watch # Watch mode
bun build index.ts --compile --outfile systant
# Nix build
nix build .#systant
# Run tests
bun test
``` ```
## Planning Protocol ## Planning Protocol

View File

@ -12,8 +12,8 @@
... ...
}: }:
{ {
# Home Manager module (system-independent) # NixOS module (system-independent)
homeManagerModules.default = import ./nix/nixos-module.nix; nixosModules.default = import ./nix/nixos-module.nix;
# Overlay to add systant to pkgs # Overlay to add systant to pkgs
overlays.default = final: prev: { overlays.default = final: prev: {

View File

@ -1,17 +1,11 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let let
cfg = config.services.systant; cfg = config.systant;
settingsFormat = pkgs.formats.toml { }; settingsFormat = pkgs.formats.toml { };
configFile =
if cfg.configFile != null
then cfg.configFile
else if cfg.settings != { }
then settingsFormat.generate "systant-config.toml" cfg.settings
else null;
in in
{ {
options.services.systant = { options.systant = {
enable = lib.mkEnableOption "systant system monitoring agent"; enable = lib.mkEnableOption "systant system monitoring agent";
package = lib.mkOption { package = lib.mkOption {
@ -59,30 +53,70 @@ in
} }
''; '';
}; };
user = lib.mkOption {
type = lib.types.str;
default = "systant";
description = "User account under which systant runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "systant";
description = "Group under which systant runs.";
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
home.packages = [ cfg.package ]; # Create systant user/group if using defaults
users.users.${cfg.user} = lib.mkIf (cfg.user == "systant") {
systemd.user.services.systant = { isSystemUser = true;
Unit = { group = cfg.group;
Description = "Systant system monitoring agent"; description = "Systant service user";
After = [ "network-online.target" ];
Wants = [ "network-online.target" ];
}; };
Service = { users.groups.${cfg.group} = lib.mkIf (cfg.group == "systant") { };
# Generate config file from settings if configFile not provided
environment.etc."systant/config.toml" = lib.mkIf (cfg.configFile == null && cfg.settings != { }) {
source = settingsFormat.generate "systant-config.toml" cfg.settings;
};
systemd.services.systant = {
description = "Systant system monitoring agent";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "simple"; Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = ExecStart =
if configFile != null let
then "${cfg.package}/bin/systant run --config ${configFile}" configPath =
else "${cfg.package}/bin/systant run"; if cfg.configFile != null
then cfg.configFile
else "/etc/systant/config.toml";
in
"${cfg.package}/bin/systant run --config ${configPath}";
Restart = "on-failure"; Restart = "on-failure";
RestartSec = "5s"; RestartSec = "5s";
};
Install = { # Hardening
WantedBy = [ "default.target" ]; NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
# Allow reading system metrics
ReadOnlyPaths = [
"/proc"
"/sys"
];
}; };
}; };
}; };