https://github.com/srcfl/hugin-agent
Thin local agent for Hugin (https://hugin.sourceful-labs.net) — scans LAN, talks Modbus, runs Lua drivers. Open + auditable.
https://github.com/srcfl/hugin-agent
agent ems energy hugin localhost lua modbus
Last synced: 22 days ago
JSON representation
Thin local agent for Hugin (https://hugin.sourceful-labs.net) — scans LAN, talks Modbus, runs Lua drivers. Open + auditable.
- Host: GitHub
- URL: https://github.com/srcfl/hugin-agent
- Owner: srcfl
- License: mit
- Created: 2026-05-01T17:58:42.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-12T11:56:25.000Z (about 1 month ago)
- Last Synced: 2026-05-12T13:33:58.797Z (about 1 month ago)
- Topics: agent, ems, energy, hugin, localhost, lua, modbus
- Language: Go
- Size: 52.7 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# hugin-agent
> Thin local helper for the [Hugin web app](https://hugin.sourceful-labs.net) — scans your LAN, talks Modbus, runs Lua drivers. Open. Stateless. Auditable.
## Why this exists
Hugin's web app at `hugin.sourceful-labs.net` lets you write Lua drivers for energy devices with the help of an AI. To actually **test** a draft against the real hardware on your network, the browser needs a way out — it can't speak Modbus TCP, it can't sweep ARP, it can't talk to your inverter.
This binary is that way out. It listens on `localhost:19090`, exposes five JSON endpoints, and does exactly the things the browser can't:
- Sweep your LAN for devices (TCP probe + ARP)
- Read Modbus holding-registers from an IP
- Execute a forty-two-watts-style Lua driver in a sandbox and stream the emissions back
Nothing else. No state stored on disk. No telemetry. No auto-update. No hidden background work. The whole binary is auditable in 30 minutes.
If you already run [forty-two-watts](https://github.com/frahlg/forty-two-watts), you don't need this — 42w can play the agent role itself (same protocol).
## Install
### macOS / Linux — Homebrew
```bash
brew install srcfl/tap/hugin-agent
hugin-agent
```
(Homebrew strips macOS quarantine automatically — no Gatekeeper warning.)
### Windows — Scoop
```powershell
scoop bucket add srcfl https://github.com/srcfl/scoop-bucket
scoop install hugin-agent
hugin-agent
```
### Docker
Published as a multi-arch image on every release tag. Use as a
sidecar in your existing compose stack (e.g. forty-two-watts):
```yaml
# docker-compose.hugin.yml
services:
hugin-agent:
image: ghcr.io/srcfl/hugin-agent:latest
restart: unless-stopped
network_mode: host # see Modbus devices on the host LAN
volumes:
- hugin-agent-data:/var/lib/hugin-agent
volumes:
hugin-agent-data:
```
```bash
docker compose -f docker-compose.hugin.yml up -d
docker logs hugin-agent # the pairing URL is printed on stderr
```
`/var/lib/hugin-agent/creds.json` persists NATS pairing across
restarts. Without `network_mode: host` the agent can still talk to
the workbench (publish port 19090) but won't see Modbus devices on
the user's LAN — pick whichever fits your security model.
### From source (any platform with Go 1.25+)
```bash
go install github.com/srcfl/hugin-agent/cmd/hugin-agent@latest
hugin-agent
```
### Pre-built tarball
Direct downloads at [github.com/srcfl/hugin-agent/releases](https://github.com/srcfl/hugin-agent/releases). On macOS, you'll need to run `xattr -d com.apple.quarantine /path/to/hugin-agent` to bypass Gatekeeper for the unsigned binary — this is exactly what Homebrew does for you.
### Audit before you run
```bash
git clone https://github.com/srcfl/hugin-agent
cd hugin-agent
# read everything in cmd/ and internal/ — it's small
go build ./cmd/hugin-agent
./hugin-agent
```
## Pair with Hugin
When you start the agent it prints something like:
```
Hugin agent v0.1.0
Listening on http://127.0.0.1:19090
Pairing token: a3f9c2d8e7b1f0a4c5d6e8f7a1b2c3d4
Pair this agent with the web app:
1. Open https://hugin.sourceful-labs.net/settings.html
2. Paste the URL above + this token
3. Click 'Test connection' then 'Save pairing'
```
Open the URL, paste both fields, save. The web app stores the pair in your browser's `localStorage` (never sent to Sourceful's servers). After that, the chat page at `hugin.sourceful-labs.net/chat.html` shows a green "agent" pill in the header and unlocks "Scan my LAN" + "Test latest driver" buttons.
## Flags
| Flag | Default | Notes |
|-------------|-------------|------------------------------------------------------------|
| `--host` | `127.0.0.1` | Use `0.0.0.0` only if you're running this on a Pi and want LAN access. |
| `--port` | `19090` | |
| `--token` | (random) | Persistent token if you want to skip re-pairing on restart |
Same values via env: `HUGIN_AGENT_HOST`, `HUGIN_AGENT_PORT`, `HUGIN_AGENT_TOKEN`.
## Protocol (v1)
All endpoints return JSON. Auth: every protected endpoint expects `Authorization: Bearer ` matching the pairing token.
### `GET /v1/info` (no auth)
```json
{
"name": "hugin-agent",
"version": "0.1.0",
"protocol_version": 1,
"capabilities": ["scan", "probe", "run-lua"]
}
```
### `GET /v1/health` (no auth)
```json
{ "status": "ok", "ts": "2026-05-02T08:30:00Z" }
```
### `POST /v1/scan` (auth)
Sweeps the local subnet for devices.
Request:
```json
{ "cidr": "192.168.1.0/24", "ports": [502, 80, 1883], "deep_probe": false }
```
Response:
```json
{
"devices": [
{
"ip": "192.168.1.50",
"mac": "AA:BB:CC:DD:EE:FF",
"vendor_oui": "Sungrow",
"open_ports": [502, 80],
"modbus": { "fc43_make": "Sungrow", "fc43_model": "SH10RT" },
"http_banner": "..."
}
],
"errors": []
}
```
If `deep_probe: true`, devices on TCP-502 also get a Modbus FC43 device-identification probe.
### `POST /v1/probe` (auth)
Reads holding-registers from a specific Modbus device.
Request:
```json
{
"ip": "192.168.1.50",
"protocol": "modbus",
"modbus": {
"port": 502,
"slave_id": 1,
"registers": [
{ "addr": 16384, "count": 2, "kind": "i32_be" },
{ "addr": 16386, "count": 1, "kind": "u16" }
]
}
}
```
`kind` is one of: `u16`, `i16`, `u32_be`, `u32_le`, `i32_be`, `i32_le`, `f32_be`, `f32_le`.
### `POST /v1/run-lua` (auth)
Executes a forty-two-watts Lua driver and returns whatever it emits.
Request:
```json
{
"lua_source": "DRIVER = {...} function driver_init(c) ... end function driver_poll() ... end",
"config": { "host": "192.168.1.50", "port": 502, "slave_id": 1 },
"actions": ["init", "poll"],
"duration_ms": 30000
}
```
`actions` is run in order; defaults to `["init", "poll"]`. The Lua VM is sandboxed (no `os`/`io`/`debug`, no `require`/`load`/`dofile`); the only way out is the `host.*` table.
Response:
```json
{
"ok": true,
"emissions": [
{ "ts": "...", "channel": "meter", "data": { "power_w": -2500 } }
],
"metrics": {},
"logs": ["[info] driver_init done"],
"errors": []
}
```
## Security model
**What the agent CAN do** with the token someone has stolen from your machine:
- Read your Modbus / HTTP-discoverable devices on the LAN it's bound to
- Send Modbus reads (no writes — `/v1/probe` is read-only; Lua can write but only to the host you give it)
- Execute Lua source you POST it (sandboxed)
**What the agent CAN'T do:**
- Reach the public internet (we never make outbound calls except the ones drivers explicitly issue via `host.http_get` / `host.http_post`)
- Persist anything to disk
- Auto-update itself
- Phone home with telemetry
- Run arbitrary Go code (only sandboxed Lua)
**Recommendations:**
- Default bind is `127.0.0.1` (only the same machine). Don't change it unless you understand the trade-off.
- Don't share your pairing token. Generate a new random one with each restart by leaving `--token` empty.
- The web app at `hugin.sourceful-labs.net` stores the token in browser localStorage — clear it from settings if you stop using Hugin.
## License
MIT. See [LICENSE](LICENSE).
## Issues, questions
[github.com/srcfl/hugin-agent/issues](https://github.com/srcfl/hugin-agent/issues)