PLUGIN PROTOCOL
Sensor Plugin Protocol v1
A plugin is an executable plus a JSON manifest. Device Agent launches the
command directly (never through a shell) with ZYVOR_PLUGIN_PROTOCOL=v1 and
expects one JSON object on stdout.
Single-reading form¶
{
"sensor": "cabinet-temperature",
"value": 31.2,
"kind": "temperature",
"unit": "celsius",
"quality": "good",
"labels": { "bus": "/dev/i2c-1", "address": "0x48" }
}
The daemon wraps that response in a canonical envelope containing the plugin name, sensor ID, collection time, success state, normalized readings and raw plugin output. The canonical envelope is used by REST, the dashboard and Nodra.
A plugin may instead return a readings array when one transaction produces
multiple measurements:
{
"sensor": "environment-1",
"quality": "good",
"readings": [
{ "name": "temperature", "kind": "temperature", "value": 31.2, "unit": "celsius" },
{ "name": "humidity", "kind": "humidity", "value": 62.1, "unit": "percent" }
]
}
Manifest¶
{
"name": "i2c-temperature",
"version": "0.1.1",
"command": "/usr/lib/zyvor-device-agent/plugins/i2c_temperature.py",
"args": ["--bus", "/dev/i2c-1", "--address", "0x48"],
"capabilities": ["temperature", "i2c"],
"sensor_id": "cabinet-temperature",
"enabled": true,
"poll_interval_seconds": 5,
"publish_to_nodra": true
}
Safety rules¶
By default Device Agent requires an absolute plugin command, rejects commands that are not executable regular files, rejects world-writable executables and caps captured stdout. A hung plugin is killed after the configured timeout.
Device Agent does not scan I2C addresses automatically. Some I2C devices react badly to probing. The included LM75/TMP102 reference plugin only touches the bus and address explicitly configured in its manifest.
The process boundary is deliberate. High-rate protocols belong in Nodra adapters; Device Agent plugins are for bounded hardware/sensor sampling.
Optional hardening (v0.1.4+, all opt-in)¶
[plugins]
run_as_uid = 1500
run_as_gid = 1500
allowed_owners = ["1500"]
allowed_directories = ["/usr/lib/zyvor-device-agent/plugins"]
max_memory_bytes = 67108864
max_cpu_seconds = 5
max_processes = 4
seccomp_enabled = true
run_as_uid/run_as_gid(Linux only): drop the plugin subprocess to this identity before exec instead of inheriting the daemon's own (today: root). Seedocs/HARDWARE_PERMISSIONS.mdfor the device-group implications.allowed_owners: reject plugin commands not owned by one of these uids (numeric) or usernames.allowed_directories: reject plugin commands whose canonicalized path isn't under one of these prefixes — resolved through symlinks, so this can't be bypassed with a../traversal or a symlink pointing elsewhere.max_memory_bytes/max_cpu_seconds/max_processes(Linux only):RLIMIT_AS/RLIMIT_CPU/RLIMIT_NPROCapplied to the subprocess — the last one in particular guards against a plugin that fork-bombs.seccomp_enabled(Linux only): installs a seccomp-bpf filter in the subprocess that returnsEPERMfor a fixed denylist of dangerous syscalls (ptrace,mount/umount2/pivot_root,reboot/kexec_load,init_module/finit_module/delete_module,acct,swapon/swapoff,bpf,perf_event_open,keyctl/add_key/request_key,setns,unshare) and allows everything else. Deliberately a denylist, not an allowlist — plugins are arbitrary external scripts/binaries with unknowable syscall needs, so a strict allowlist isn't safe to ship as a default. Layered on top of the identity drop and rlimits above as defense-in-depth, not a replacement for them.
All of these default to unrestricted/unlimited/disabled, matching pre-v0.1.4 behavior.