fieldmesh

Modules as systemd services (spec v0.1 §8.3; open-questions Q11)#

Each module runs as its own sandboxed service. Its only way to reach the farm's data is the module host's unix socket, and the host answers only what the module's grant allows (sdk/modhost.py).

unit what it is
fieldmesh-host.service the host, over the farm's log file, reading back every two seconds what other processes append to it: readings, grants, revocations
fieldmesh-module@.service one module: fieldmesh-module@water, fieldmesh-module@soilwater
fieldmesh-server.service the SensorThings data service, read-only over the farm's log, tokens required (server/)
fieldmesh-module@notify.service.d/ the notifier's one exception: the network, for its push server, and its ntfy tokens as credentials (modules/notify/)
fieldmesh-module@twin.service.d/ and fieldmesh-module@twin.timer the covered-crop frost watch (modules/twin/): one run each evening at 17:30, in GreenLight's own environment, no network
fieldmesh-module@soilwater.service.d/ and fieldmesh-module@soilwater.timer the soil-moisture advice (modules/soilwater/): one look at every bed at 04:00, before the morning's watering, and again at 13:00
fieldmesh-module@board.service.d/ the farm page: its own state directory for the pages, served on this machine only (modules/board/)

Installing a module#

sudo useradd --system fieldmesh && sudo groupadd --system fieldmesh-modules
sudo cp -r . /opt/fieldmesh                      # the repository, read-only to services
sudo install -d -m 0700 /etc/fieldmesh/modules

# The owner's act (§8.2): grant the module its points. Only the token's hash is logged.
sudo python3 /opt/fieldmesh/sdk/modhost.py grant --log /var/lib/fieldmesh/farm.jsonl \
    --manifest /opt/fieldmesh/modules/water/module.json \
    --datastream <trough series id> --datastream <trough series id> \
    --token-out /etc/fieldmesh/modules/water.token
sudoedit /etc/fieldmesh/modules/water.json        # {"troughs": [{"datastream": …, "capacity_l": 400}]}

sudo cp deploy/systemd/*.service /etc/systemd/system/
sudo systemctl enable --now fieldmesh-host fieldmesh-module@water

# Taking it back: within two seconds, the next call it makes is refused.
sudo python3 /opt/fieldmesh/sdk/modhost.py revoke --log /var/lib/fieldmesh/farm.jsonl <grant id>

The notifier and the board#

Both read every module's decisions, which an owner grants by installing them: their manifests ask for it, and the grant record says so (§8.3). The notifier is granted no reading; the board only the series its page shows, named in its grant.

sudo python3 /opt/fieldmesh/sdk/modhost.py grant --log /var/lib/fieldmesh/farm.jsonl \
    --manifest /opt/fieldmesh/modules/notify/module.json --token-out /etc/fieldmesh/modules/notify.token
sudoedit /etc/fieldmesh/modules/notify.json      # the roster: people, routes, channels
# optional, if the ntfy server asks for tokens (modules/notify/README.md):
sudoedit /etc/fieldmesh/modules/notify.ntfy-token /etc/fieldmesh/modules/notify.ntfy-ack-token
sudo cp -r deploy/systemd/fieldmesh-module@notify.service.d /etc/systemd/system/
sudo systemctl enable --now fieldmesh-module@notify

sudo python3 /opt/fieldmesh/sdk/modhost.py grant --log /var/lib/fieldmesh/farm.jsonl \
    --manifest /opt/fieldmesh/modules/board/module.json \
    --datastream <each series the page shows> --token-out /etc/fieldmesh/modules/board.token
sudoedit /etc/fieldmesh/modules/board.json       # farm, time zone, languages, names, series
sudo cp -r deploy/systemd/fieldmesh-module@board.service.d /etc/systemd/system/
sudo systemctl enable --now fieldmesh-module@board   # http://127.0.0.1:8080/

The covered-crop frost watch runs once each evening rather than staying up:

sudo python3 -m venv /opt/fieldmesh/modules/twin/env
sudo /opt/fieldmesh/modules/twin/env/bin/pip install -r /opt/fieldmesh/modules/twin/requirements.txt
sudo python3 /opt/fieldmesh/sdk/modhost.py grant --log /var/lib/fieldmesh/farm.jsonl \
    --manifest /opt/fieldmesh/modules/twin/module.json \
    --property http://vocab.nerc.ac.uk/standard_name/air_temperature/ \
    --property http://vocab.nerc.ac.uk/standard_name/relative_humidity/ \
    --property http://vocab.nerc.ac.uk/standard_name/wind_speed/ \
    --property http://vocab.nerc.ac.uk/standard_name/cloud_area_fraction/ \
    --token-out /etc/fieldmesh/modules/twin.token
sudoedit /etc/fieldmesh/modules/twin.json        # {"lat": …, "lon": …, "cover": "thermic", "tunnel": "<id>"}
sudo cp -r deploy/systemd/fieldmesh-module@twin.service.d deploy/systemd/fieldmesh-module@twin.timer /etc/systemd/system/
sudo systemctl enable --now fieldmesh-module@twin.timer

Run as such a process on a copy of McAllister Farm's own forecast log, sandboxed without network and capped at 512 MB, it read the night through the host and logged its decision. That night took 5 seconds and 132 MB. systemd-analyze accepts the unit and its timer, and scores the unit 0.4. The timer itself has not fired under a system manager.

The notifier is the one module given the network, so systemd-analyze security scores its unit at 1.3 ("OK"), and the board, which serves on this machine only, at 1.2; a module with no network scores 0.4. Run live under a user manager with the notifier's settings, it read its roster and token as credentials, fell back to no ntfy token when that file was missing, and published to a real ntfy server through the host. Not checked: SMS through ModemManager from the service's own user, which polkit decides; check it on the first gateway with a SIM.

What the sandbox does, and what was checked#

The module service has its own throwaway user, no network (address families other than unix sockets refused), a read-only system and no home, no way to gain privileges, a system-call filter, and limits on memory, CPU and processes, so a runaway module cannot starve the foundation. Its token and configuration arrive as systemd credentials, readable by it alone and gone when it stops.

WASI's capability sandbox is the stated target (Q11), adopted when the Python toolchain is ready; the grant model does not change when the sandbox does.