Repository navigation
Releases: zyvorai/zyvor-device-agent
Release list
v0.2.0
Trusted hardware passport and flight recorder. Upgrade notes:
docs/UPGRADE-0.2.0.md.
Breaking
- Default listen is
127.0.0.1:9188. Non-loopback +auth.mode = "none"is
refused unlessserver.allow_unauthenticated_remote = true. - Long-lived bearer tokens are no longer accepted from
?token=query strings.
Camera<img>uses short-lived stream tickets; SSE uses authenticatedfetch(). identity.policy = required|preferred|software(defaultpreferred).
Production TPM boards userequired.
Added
- Device passport (
GET /api/v1/passport,agentctl passport/verify). - Segmented flight recorder and
GET /api/v1/recorder. - Redacted support bundles (
agentctl support-bundle). - Signed Fleet inventory sibling fields (digest, boot id, sequence, signature).
- Privilege-separated
bus-helperover authenticated UDS (--features privsep). - Enrollment renew / revocation check and lab enrollment server fixture.
- Safe signed remediation catalogue (no shell).
- Rules-based diagnostics findings; Edge AI stays HTTP 501.
- Hardware profile directory layout, qualify/package commands, verified
hardware registry (Minew intentionally unsigned). - Plugin SDK v2 schema and optional WASI feature.
- Six-screen field-engineer dashboard and first-run wizard.
- Zero-touch installer flags (
--enrollment-token) andagentctl commission.
Qualification
- HIL runner records RS485 and power-cycle evidence when operators attach logs.
- Minewing physical checklist remains unsigned;
minewing_claimable=false
until a physical signed run.
v0.1.6
Production-hardening release: arm64 native packages, emulator CI, Minewing
HIL harness (silicon sign-off still operator-gated), qualify/production docs.
- Minewing HIL runner (
scripts/hil/run-minewing-hil.sh,docs/HIL.md) with
fail-closed checklist signing (DA_HIL_SIGN=1only when claimable). - Native arm64
.deb/.rpmpackaging: CIpackages-arm64on
ubuntu-24.04-arm, release matrix for amd64+arm64,scripts/package-deb-rpm.sh. - Emulator CI scaffolding:
scripts/emulator/smokes for vcan CAN
capture, swtpm TPM identity, and v4l2loopback camera; CI jobs
emulator-vcan/emulator-swtpm/emulator-v4l2; qualify rows +
docs/EMULATOR_CI.md. - Opt-in non-root systemd unit example + udev rules (
packaging/systemd/
packaging/udev); HARDWARE_PERMISSIONS documents the swap path. - Bring-up SKU profile
minewing-gw1-r1(aligned with Zyvor OTA board profile). - Nodra MQTTS via
[nodra.tls](CA + optional client cert); plain MQTT documented as trusted-LAN only. make qualifysoftware matrix + PRODUCTION / QUALIFICATION / TEST-REPORT docs.- HTTPS-aware
verify-deployment.sh/deploy-remote.shhealth checks (ZYVOR_DEVICE_AGENT_TLS/_CA). - Document arm64 production install as signed tarball or multi-arch OCI (amd64 packages unchanged).
- Rename Aether → Axiom in positioning docs.
v0.1.5
-
Add optional camera support (
--features camera, off by default like
hotplug/tpm2): live snapshot and live MJPEG video stream from an
explicitly-allowlisted/dev/video*, followingcan_capture.rs's
opt-in/rate-limited/never-fatal posture — starts the device-discovery and
live-viewing half ofdocs/ROADMAP.md's "Edge AI bridge" line item
(local inference/accelerator support stays a separate, still-unscoped
future increment). New[camera]config (top-level, not nested under
[industrial]), newsrc/camera_capture.rs(one dedicated thread per
configured camera; prefers native MJPG passthrough, falls back to a
pure-Rust YUYV→RGB→JPEG software encode viajpeg-encoderfor cameras
without it; bounded retry-with-backoff on error, unlike CAN capture's
"fail once" policy, since USB replug is common). New endpoints
GET /api/v1/camera,GET /api/v1/camera/{id}/snapshot,
GET /api/v1/camera/{id}/stream(multipart/x-mixed-replace, renders in
a plain<img>tag, no WebRTC/signaling) — the latter two get the same
?token=query-auth carve-out the SSE routes already have, generalized
to a path-prefix list (src/auth/mod.rs's new
QUERY_TOKEN_PATH_PREFIXES) since camera ids are dynamic. Nodra
publishing (camera.devices[].publish_to_nodra) sends only
CameraCaptureStatushealth/presence, never frame bytes — a deliberate
boundary documented with a code comment onnodra::publisher_loopso a
future contributor doesn't "complete the parallel" with CAN's per-frame
forwarding arm.v4l(defaultv4l2feature — raw kernel ioctls, no
libv4l.so) andjpeg-encoderare both pure Rust;v4lis additionally
gated totarget_os = "linux"(V4L2 itself is Linux-only), with a no-op
spawn()stub everywhere else mirroringhardware::hotplug's pattern.
The YUYV→RGB→JPEG pipeline has real unit-test coverage that runs
cross-platform under--features camera(no Linux/hardware needed) by
gating that one sub-module onany(target_os = "linux", test)rather
than requiring Linux outright.packaging/systemd/zyvor-device-agent.service
needed noDeviceAllow=change — confirmedDevicePolicy=autowith zero
entries already permits root's/dev/video*access, documented with a
comment so nobody "fixes" it unnecessarily. This dev machine is macOS, so
the real V4L2 ioctl path (as opposed to the cross-platform encode-pipeline
unit tests) is unverified here — it needs a Linux box with either a real
camera or av4l2loopbackvirtual device to test end to end. -
Add optional native TLS for the TCP listener:
server.tls.enabled(off
by default) serves plain HTTPS — no client certificate ever required,
unlikeauth.mode = "mtls"— so an ordinary browser can reach
https://<host>:9188/directly. If no cert exists at
server.tls.cert_path/key_path, one is generated automatically on
first start (src/tls.rs,rcgen::generate_simple_self_signed, SANs:
localhost/127.0.0.1/::1/hostname/detected local IP), mirroring
../fabric'szyvor-fabricd::tlsmodule; a real cert can simply be
mounted at the same paths instead, and an existing cert/key there is
never overwritten. Independent ofauth.mode: TLS and bearer/mtls/none
auth compose (e.g.server.tls.enabled = true+auth.mode = "bearer"
gives an encrypted transport with a required token, the recommended
combination for a non-loopback bind).auth.mode = "mtls"still takes
its own separate TLS path (auth::mtls::load_server_config) when set,
since that one ties TLS to client-certificate verification.
v0.1.4
-
Publish multi-arch (
linux/amd64+linux/arm64) container images to
ghcr.io/zyvorai/device-agenton every tagged release, keyless-signed
with cosign and attested for build provenance, alongside the existing
.deb/.rpm/tarball artifacts — newcontainerjob in
.github/workflows/release.yml.ci.yml's existing per-push build+smoke-test
job (--load, local only) is unchanged; this is the first job that
actually publishes an artifact. Adds a systemd-supervised alternative to
the bare-metal deployment path (packaging/container/zyvor-device-agent-container.service,
Podman-based) and a new tutorial,
docs/guides/07-container-deployment.md, covering device/bus passthrough
flags, config/state volumes, and running it under systemd — aimed at
small ARM64 edge devices where an on-device Rust/Node build is
impractical and the amd64-only.deb/.rpmpackages don't apply yet. -
Add a numbered
docs/guides/tutorial series (getting started;
configuration and the API; securing the agent; industrial buses; writing
a sensor plugin; deploying to production) and a README banner/badge
header, replacing the plain-text architecture diagram with an SVG in the
same visual family as the new banner. Docs only — no behavior change.
Every command, endpoint, config key, and CLI flag referenced in the new
guides was cross-checked against the current source
(src/api.rs's route table,config/device-agent.example.toml,
scripts/deploy-remote.sh's flag parsing) rather than written from
memory. -
Add optional Linux hotplug event source:
--features hotplug(off by
default) opens a rawNETLINK_KOBJECT_UEVENTsocket (netlink-sys, pure
Rust) alongside the existing polling inventory refresh, and a kernel
uevent for a tracked bus subsystem (gpio/i2c/spidev/net/usb/
tty) triggers an immediatehardware::collect_inventory+
state.update_inventoryre-scan instead of waiting for the next poll
tick - reusing the existing SSE hardware-change stream and threshold
evaluation as-is. Deliberately notudev/tokio-udev: those need
libudev.soat runtime, which the container image/.deb/.rpmdon't
otherwise depend on. Opening the socket is never fatal - a warning is
logged once and the daemon falls back to polling-only. New
src/hardware/hotplug.rs;rust-native's CI job gains a second
clippy/test/build pass with--features hotplug(no extra system
library needed, unliketpm2, so it didn't need its own job). Verified
for real: sent a synthetic but correctly-formatted uevent over the real
netlink multicast group and confirmed the daemon's inventory-refresh
timestamp jumps immediately (well under the poll interval, which was
set to 300s for the test) for a tracked subsystem (i2c) and does not
move for an untracked one (cpu), proving both the trigger and the
filter are real, not just compiled. -
Add optional TPM2-backed mTLS identity:
identity.backend = "tpm"
(--features tpm2, off by default, must never affect a plaincargo build) generates and signs the mTLS private key inside a TPM2 via
tss-esapi'sTransientKeyContextinstead of a PKCS#8 file on disk -
what's persisted toauth.mtls.key_fileis a small JSON envelope (the
TPM's public key plus an encrypted private-key blob only that TPM can
unwrap), and every signature (CSR at enroll time, every TLS handshake
while serving) re-opens a TPM session rather than loading a private key
into process memory. Falls back to a software key at runtime (with a
warning) if the TPM can't be opened, since most dev/test boxes have none.
Fixed to ECC P-256/ECDSA-SHA256, the combination every TPM2 chip and
swtpmsupport well. Newsrc/identitymodule:DeviceIdentitywraps
either backend behind the samercgen::SigningKey(CSR generation) and
rustls::sign::SigningKey(TLS handshake signing) interfaces the
existing enroll/mtls code already used, so neither needed a rewrite -
only a backend behind them changed. CI gains a dedicatedrust-tpm2job
(installslibtss2-dev, builds/clippies/tests with--features tpm2);
rust-native's clippy step drops--all-featuresso it no longer
silently depends on that job's system library. Live-verified against
swtpm(the TPM2 emulator used where no physical TPM is available):
ranenrollfor real withidentity.backend = "tpm", confirmed the
persisted key file is the JSON envelope (not a PKCS#8 key) andidentity
reportsbackend: tpm, then startedserveinauth.mode = "mtls"and
confirmed a real mTLS handshake succeeds - i.e. the TLS signature the
emulated TPM produced verifies against the certificate's public key. See
docs/TPM2_IDENTITY.md. -
Add
auth.mode = "mtls"andzyvor-device-agent enroll/identity:
client-side enrollment generates a keypair and CSR, submits them to
enrollment.server_urlwith a single-use token, and persists the issued
certificate/key.servethen terminates TLS itself (viaaxum-server's
rustls integration) using that identity and, when
auth.mtls.require_client_cert = true, rejects any connection without a
client certificate signed byclient_ca_fileat the handshake. Device
Agent implements only this client side of the protocol - Fleet's
enrollment-token system today issues an opaque bearer token, not a signed
certificate, and a production CA (key custody, revocation, rotation) is a
separate, security-sensitive project of its own; see
docs/MTLS_ENROLLMENT.md. New dependencies:rcgen(CSR generation),
reqwest(rustls-backed, for submitting the CSR),axum-server+rustls
(serving mTLS),rustls-pemfileandx509-parser(reading the resulting
identity back foridentity). Automatic rotation and CRL/OCSP revocation
are explicit non-goals for this milestone - reissue manually with
enroll --force. TPM2/secure-element-backed key storage is a separate,
optional follow-up. -
Add signed
.deb/.rpmpackages (amd64) to the release pipeline, built
viacargo-deb/cargo-generate-rpmfrom new[package.metadata.deb]/
[package.metadata.generate-rpm]tables inCargo.toml(both fully
Cargo-metadata-driven, no separate packaging manifest). Asset layout
mirrorsscripts/install.sh. Installing the package never enables or
starts the systemd unit —cargo-deb'ssystemd-unitsintegration is
configured withenable = false/start = false, and the generated
.debcarries no maintainer scripts at all as a result; the rpm's only
scriptlet issystemctl daemon-reload./etc/zyvor/device-agent.toml
is a conffile (dpkg) /%config(noreplace)(rpm), so a locally-modified
config survives both an upgrade and a straight reinstall, and
dpkg -r/rpm -eleave it and the profiles/plugin manifests on disk —
all verified for real: built both packages on the reference Linux host,
dpkg -i'd the.debon top of an already-running manually-deployed
instance (confirmed--force-confold's "Keeping old config file as
default" preserves an operator edit), thendpkg -r'd it and confirmed
the config/profiles remained; the.rpmwas verified via an isolated
rpm --rootinstall (file layout, permissions, no enable/start
scriptlet) since this host runs a Debian-family package manager, not
rpm, day to day. arm64 packages aren't built yet — cross-packaging
wasn't validated in this pass, so it's tracked as a follow-up rather
than blocking amd64 on it. -
Add config hot-reload:
SIGHUPre-reads the config file and applies
auth.*,thresholds.*,plugins.*,fleet.*and the parts of
industrial.*/nodra.*read fresh per-request/tick, without a restart.
server.listen/unix_socket/dashboard_dir(bound once at startup) and
the Nodra MQTT connection/CAN-capture socket set (opened once at their own
startup) still need a restart —SIGHUPlogs a clear warning when one of
those changed. A config file that fails to parse is logged and ignored,
keeping the daemon on its last-known-good config.AppState.configmoves
from a plain field to anarc_swap::ArcSwap<Config>to make this possible. -
Add opt-in
plugins.seccomp_enabled(Linux only): installs a seccomp-bpf
denylist in the plugin subprocess (ptrace,mount/umount2/
pivot_root,reboot/kexec_load, module loading/unloading,acct,
swapon/swapoff,bpf,perf_event_open,keyctl/add_key/
request_key,setns,unshare→EPERM, everything else allowed) on
top of the existing identity drop and rlimits, as defense-in-depth against
a compromised or malicious plugin binary. -
Add bearer-token API auth (
auth.mode = "bearer"); every route except
/api/v1/healthis unauthenticated by default (mode = "none") — same as before. -
Add an additional Unix-domain-socket API listener with kernel peer-credential
(uid/gid) RBAC, alongside the existing TCP listener. -
Add opt-in plugin privilege drop (
plugins.run_as_uid/run_as_gid) so sensor
plugin subprocesses no longer have to inherit the daemon's root identity. -
scripts/deploy-remote.sh --auth-mode bearergenerates and installs a bearer
token the same way../fabrichandles its admin password; refuses to bind a
non-loopback address with no auth configured. -
Add
docs/HARDWARE_PERMISSIONS.mdcovering GPIO/I2C/SPI/CAN device-node
permissions for dropped-privilege plugins. -
CI: the
containerjob now actually boots the built amd64 and arm64 (via QEMU)
images and checks--version/doctorrun correctly, instead of only
cross-building them. -
Release pipeline: generate CycloneDX SBOMs (Rust + dashboard), sign checksums
and SBOMs with keylesscosign, and attach a SLSA build-provenance attestation. -
Add opt-in plugin hardening:
allowed_owners/allowed_directoriescommand
allowlists, andmax_memory_bytes/max_cpu_seconds/max_processesrlimits. -
Fix: the bundled dashboard now suppo...