Skip to content

Releases: zyvorai/zyvor-device-agent

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 10:57

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 unless server.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 authenticated fetch().
  • identity.policy = required|preferred|software (default preferred).
    Production TPM boards use required.

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-helper over 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) and agentctl 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

Choose a tag to compare

@github-actions github-actions released this 14 Sep 13:12

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=1 only when claimable).
  • Native arm64 .deb/.rpm packaging: CI packages-arm64 on
    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 qualify software matrix + PRODUCTION / QUALIFICATION / TEST-REPORT docs.
  • HTTPS-aware verify-deployment.sh / deploy-remote.sh health 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

Choose a tag to compare

@github-actions github-actions released this 12 Sep 13:59
  • 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*, following can_capture.rs's
    opt-in/rate-limited/never-fatal posture — starts the device-discovery and
    live-viewing half of docs/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]), new src/camera_capture.rs (one dedicated thread per
    configured camera; prefers native MJPG passthrough, falls back to a
    pure-Rust YUYV→RGB→JPEG software encode via jpeg-encoder for 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
    CameraCaptureStatus health/presence, never frame bytes — a deliberate
    boundary documented with a code comment on nodra::publisher_loop so a
    future contributor doesn't "complete the parallel" with CAN's per-frame
    forwarding arm. v4l (default v4l2 feature — raw kernel ioctls, no
    libv4l.so) and jpeg-encoder are both pure Rust; v4l is additionally
    gated to target_os = "linux" (V4L2 itself is Linux-only), with a no-op
    spawn() stub everywhere else mirroring hardware::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 on any(target_os = "linux", test) rather
    than requiring Linux outright. packaging/systemd/zyvor-device-agent.service
    needed no DeviceAllow= change — confirmed DevicePolicy=auto with 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 a v4l2loopback virtual 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,
    unlike auth.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's zyvor-fabricd::tls module; a real cert can simply be
    mounted at the same paths instead, and an existing cert/key there is
    never overwritten. Independent of auth.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

Choose a tag to compare

@github-actions github-actions released this 12 Sep 10:31
  • Publish multi-arch (linux/amd64 + linux/arm64) container images to
    ghcr.io/zyvorai/device-agent on every tagged release, keyless-signed
    with cosign and attested for build provenance, alongside the existing
    .deb/.rpm/tarball artifacts — new container job 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/.rpm packages 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 raw NETLINK_KOBJECT_UEVENT socket (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 immediate hardware::collect_inventory +
    state.update_inventory re-scan instead of waiting for the next poll
    tick - reusing the existing SSE hardware-change stream and threshold
    evaluation as-is. Deliberately not udev/tokio-udev: those need
    libudev.so at runtime, which the container image/.deb/.rpm don'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, unlike tpm2, 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 plain cargo build) generates and signs the mTLS private key inside a TPM2 via
    tss-esapi's TransientKeyContext instead of a PKCS#8 file on disk -
    what's persisted to auth.mtls.key_file is 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
    swtpm support well. New src/identity module: DeviceIdentity wraps
    either backend behind the same rcgen::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 dedicated rust-tpm2 job
    (installs libtss2-dev, builds/clippies/tests with --features tpm2);
    rust-native's clippy step drops --all-features so 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):
    ran enroll for real with identity.backend = "tpm", confirmed the
    persisted key file is the JSON envelope (not a PKCS#8 key) and identity
    reports backend: tpm, then started serve in auth.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" and zyvor-device-agent enroll/identity:
    client-side enrollment generates a keypair and CSR, submits them to
    enrollment.server_url with a single-use token, and persists the issued
    certificate/key. serve then terminates TLS itself (via axum-server's
    rustls integration) using that identity and, when
    auth.mtls.require_client_cert = true, rejects any connection without a
    client certificate signed by client_ca_file at 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-pemfile and x509-parser (reading the resulting
    identity back for identity). 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/.rpm packages (amd64) to the release pipeline, built
    via cargo-deb/cargo-generate-rpm from new [package.metadata.deb]/
    [package.metadata.generate-rpm] tables in Cargo.toml (both fully
    Cargo-metadata-driven, no separate packaging manifest). Asset layout
    mirrors scripts/install.sh. Installing the package never enables or
    starts the systemd unit — cargo-deb's systemd-units integration is
    configured with enable = false/start = false, and the generated
    .deb carries no maintainer scripts at all as a result; the rpm's only
    scriptlet is systemctl 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 -e leave it and the profiles/plugin manifests on disk —
    all verified for real: built both packages on the reference Linux host,
    dpkg -i'd the .deb on top of an already-running manually-deployed
    instance (confirmed --force-confold's "Keeping old config file as
    default" preserves an operator edit), then dpkg -r'd it and confirmed
    the config/profiles remained; the .rpm was verified via an isolated
    rpm --root install (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: SIGHUP re-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 — SIGHUP logs 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.config moves
    from a plain field to an arc_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/health is 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 bearer generates and installs a bearer
    token the same way ../fabric handles its admin password; refuses to bind a
    non-loopback address with no auth configured.

  • Add docs/HARDWARE_PERMISSIONS.md covering GPIO/I2C/SPI/CAN device-node
    permissions for dropped-privilege plugins.

  • CI: the container job now actually boots the built amd64 and arm64 (via QEMU)
    images and checks --version/doctor run correctly, instead of only
    cross-building them.

  • Release pipeline: generate CycloneDX SBOMs (Rust + dashboard), sign checksums
    and SBOMs with keyless cosign, and attach a SLSA build-provenance attestation.

  • Add opt-in plugin hardening: allowed_owners/allowed_directories command
    allowlists, and max_memory_bytes/max_cpu_seconds/max_processes rlimits.

  • Fix: the bundled dashboard now suppo...

Read more