Agent Plugins
Each plugin runs inside the agent process on managed endpoints and exposes one or more actions that the server can invoke via instructions. This document covers what is common to every plugin — the load/allowlist/signing model, the stub plugins, and the operational recipes. The reference for each plugin lives next to its code in agents/plugins/<name>/README.md: what it does, per-OS capability and mechanism, privileges, the data contract, real captured output on every supported OS, and known gaps.
Plugin index
Each plugin that has adopted the README standard (docs/plugin-readme-standard.md) is listed here with a link to its page beside the code. The table is generated by tools/plugin-doc-gen; do not hand-edit it.
The echo plugin was named tar before 2026-10 (tar.dll, tar.so, tar.dylib); there is no alias, and the agent rejects a leftover tar plugin file. See the Echo section of Upgrading.
71 of 71 plugins document themselves this way; the rest are described in the prose below until their README lands.
| Plugin | Platforms | What it does | Docs |
|---|---|---|---|
agent_actions | ✅ ✅ ✅ | Agent runtime actions — set log level, query agent info | README |
agent_logging | ✅ ✅ ✅ | Remote agent log access — retrieve log tail, list key agent files | README |
antivirus | ✅ ✅ ✅ | Antivirus product detection, status, and Defender exclusions | README |
app_control | ✅ ⛔ ⛔ | Read-only WDAC and AppLocker application-control policy posture as configured (Windows-only) | README |
app_usage | ✅ 🟡 ✅ | Read-only machine-scope app usage inventory derived from Echo’s usage fold (no pid, command line, or user names in output) | README |
asset_tags | ✅ ✅ ✅ | Structured asset tag awareness — syncs server-assigned tags locally and detects changes | README |
autoruns | ✅ ✅ ✅ | Enumerates persistence sources (what starts automatically) across Windows, Linux and macOS | README |
bitlocker | ✅ ✅ ✅ | Disk encryption status — BitLocker, LUKS, FileVault | README |
browser_inventory | 🟡 🟡 ✅ | Chromium-family browser and profile inventory | README |
browser_policy | 🟡 🟡 ✅ | Enterprise-managed Chrome, Chromium and Edge browser policy inventory | README |
certificates | ✅ ✅ ✅ | Certificate inventory and management for system stores | README |
chargen | ✅ ✅ ✅ | RFC 864 character generator — streams rotating ASCII lines | README |
content_dist | ✅ ✅ ✅ | Content staging — download, verify, execute, and manage staged files (no shell-out) | README |
device_identity | ✅ ✅ ✅ | Reports device hostname, domain membership, and AD organizational unit | README |
diagnostics | ✅ ✅ ✅ | Agent diagnostics — log level, certificates, connection info | README |
discovery | ✅ ✅ 🟡 | Network device discovery — ARP scan and ping sweep | README |
disk_actions | ✅ 🟡 🟡 | Reports physical drive health and the mapping between drives and the logical volumes they back | README |
disk_space | ✅ ✅ ✅ | Reports free / total disk space for a single volume | README |
echo | ✅ ✅ ✅ | Echo activity record — continuous system state change tracking | README |
event_logs | ✅ ✅ ✅ | Queries system event logs for errors and filtered events | README |
example | ✅ ✅ ✅ | Reference example plugin — responds to ‘ping’ | README |
execution_artifacts | ✅ ⛔ ⛔ | Windows execution-evidence sources: ShimCache, Amcache InventoryApplicationFile, and Prefetch. Forensics-gated, single-target, default-off | README |
filesystem | ✅ ✅ ✅ | Filesystem operations — exists, list_dir, file_hash, search_dir, text ops (admin-only) | README |
filesystem_posture | 🟡 ✅ 🟡 | Reports mounted filesystems, per-mount quota-subsystem state, and snapshot-capable volumes | README |
firewall | ✅ ✅ ✅ | Firewall status and rule listing | README |
firmware_posture | ✅ 🟡 🟡 | Reports firmware/BIOS vendor, version, release date and update-pending posture | README |
hardware | ✅ ✅ ✅ | Reports hardware inventory: manufacturer, model, BIOS, CPU, memory, disks, drivers, chassis, baseboard, swap, machine id | README |
http_client | ✅ ✅ ✅ | HTTP client — download files, GET/HEAD requests with hash verification (no shell-out) | README |
installed_apps | ✅ ✅ ✅ | Inventories installed applications and queries by name | README |
interaction | ✅ ✅ ✅ | Desktop user interaction — notifications, message boxes, input dialogs, surveys, DND | README |
ioc | ✅ ✅ ✅ | Indicator of Compromise checking — match IOCs against local endpoint state | README |
license_scan | ✅ ✅ 🟡 | Detects software licences across OS, vendor and per-user surfaces | README |
local_security_policy | 🟡 🟡 🟡 | Reports local password, lockout and audit policy posture and sudoers content | README |
mgmt_posture | 🟡 ✅ 🟡 | Management-plane posture: which plane controls the device (facts only) | README |
msi_packages | ✅ ✅ ⛔ | Enumerates installed packages and product codes (Windows MSI / macOS pkgutil) | README |
netprobe | ✅ ✅ ✅ | Active network measurement: ICMP/TCP round-trip time, jitter, loss, and DNS resolution timing to operator-chosen targets | README |
netstat | ✅ ✅ ✅ | Enumerates active network connections and listening sockets | README |
network_actions | ✅ ✅ ✅ | Network actions — DNS flush and ping | README |
network_config | ✅ ✅ ✅ | Reports network adapter configuration, IP addresses, DNS servers, proxy settings, and the routing table | README |
network_diag | ✅ ✅ ✅ | Network diagnostics — listening ports and established connections | README |
os_info | ✅ ✅ ✅ | Reports OS name, version, build, architecture, and system uptime | README |
peripherals | ✅ ✅ ✅ | USB, PCI and Thunderbolt/USB4 device inventory | README |
pkg_inventory | 🟡 ✅ ✅ | Machine-scope package-manager inventory (managers, packages and store_packages); per-user package stores are out of scope, deferred to the user-session helper | README |
platform_security | ✅ ✅ ✅ | Secure Boot and code-integrity enforcement posture (read-only) | README |
power_health | ✅ ✅ 🟡 | Battery, thermal, and power-plan inventory, plus a gated power-plan switch | README |
printing | ✅ ✅ ✅ | Printer/job inventory plus a single narrowly-scoped clear_queue cancellation | README |
privacy_permissions | 🟡 🟡 🟡 | Per-app sensitive-permission grants — camera, microphone, location, full-disk-access equivalents (read-only) | README |
processes | ✅ ✅ ✅ | Process listing — enumerate and query running processes | README |
procfetch | ✅ ✅ ✅ | Enumerates running processes with SHA-1 hashes of executables | README |
quarantine | ✅ ✅ ✅ | Device network isolation (quarantine) with per-IP whitelisting | README |
rdp_control | ✅ ⛔ ⛔ | Remote Desktop control — enable/disable RDP (registry + firewall + TermService) | README |
registry | ✅ 🟡 🟡 | Windows Registry — get, set (six types, verified), delete value, delete key (optionally recursive), enumerate, per-user reads and writes, bulk apply with an undo manifest | README |
remote_access_audit | 🟡 ✅ 🟡 | SSH inbound-access audit: sshd_config as written, per-account authorized_keys fingerprints, sshd service state; never key comments, option values or paths | README |
runtimes | 🟡 🟡 ✅ | Installed .NET and JVM runtime inventory | README |
sccm | ✅ ⛔ ⛔ | Reports SCCM/ConfigMgr client status, version, and site assignment | README |
script_exec | ✅ ✅ ✅ | Executes commands and scripts with streaming output (admin-only) | README |
services | ✅ ✅ ✅ | System services — enumerate, query, and configure service startup types | README |
shares | ✅ 🟡 ✅ | Shares this host serves over SMB/NFS: names, paths and a few flags, never ACLs | README |
software_actions | ✅ ✅ ✅ | Lists upgradable packages and counts installed software | README |
status | ✅ ✅ ✅ | Reports agent version, system info, health, modules, connection, switch, and config | README |
storage | ✅ ✅ ✅ | Persistent key-value storage — set, get, delete, list, clear | README |
system_hardening | ✅ ✅ ✅ | Exploit-mitigation and kernel-hardening posture (read-only) | README |
tags | ✅ ✅ ✅ | Device tagging — set, get, delete, list tags for scope evaluation | README |
update_source_trust | 🟡 🟡 ✅ | Package and update-source trust posture (facts only) | README |
users | ✅ ✅ ✅ | Reports logged-on users, sessions, local accounts, admin group members, group membership, primary user, and session history | README |
vuln_scan | ✅ ✅ ✅ | Host vulnerability scanning — CVE matching and configuration compliance checks | README |
wifi | ✅ 🟡 🟡 | Scans visible WiFi networks and reports current connection status | README |
windows_optional_features | ✅ ⛔ ⛔ | Windows optional OS feature state (enabled/disabled/pending) via the DISM API | README |
windows_updates | ✅ ✅ ✅ | Updates/packages: installed, available, pending-reboot, patch connectivity | README |
wmi | ✅ ⛔ ⛔ | Windows Management Instrumentation — WQL queries and instance enumeration | README |
wol | ✅ ✅ ✅ | Sends Wake-on-LAN magic packets and checks host reachability | README |
Platform cells are Windows · macOS · Linux (✅ supported · 🟡 constrained or planned · ⛔ unsupported).
Table of Contents
Stub Plugins (Planned)
The following plugins are defined in the roadmap but not yet implemented. They are listed here for planning purposes.
| Plugin | Description | Target Phase |
|---|---|---|
patch | Patch deployment agent-side automation (scanning, downloading, installing). Server-side, PatchManager keeps patch and deployment records only: deployment execution was removed (ADR-0062, #3669) and nothing populates patch inventory (#3676), so GET /api/patches is always empty and GET /api/patches/deployments lists only recorded deployments; removal of those routes is tracked by #6326. | Phase 7 (7.8) |
Plugin Architecture
Overview
All agent plugins share a common architecture based on a stable C ABI defined in sdk/include/yuzu/plugin.h. This ABI is designed to remain binary-compatible across compiler versions and agent upgrades, so plugins can be updated independently of the agent binary. The ABI is currently at version 4 (YUZU_PLUGIN_ABI_VERSION); the agent still loads plugins built against any version back to YUZU_PLUGIN_ABI_VERSION_MIN (1) — a major-version bump does not mean existing plugins need recompiling. See sdk/README.md’s ABI compatibility table for the full version-by-version breakdown.
A C++23 CRTP wrapper (sdk/include/yuzu/plugin.hpp) provides ergonomic C++ authoring on top of the C ABI. The YUZU_PLUGIN_EXPORT macro handles symbol visibility and the export table automatically.
Plugin Lifecycle
- The agent scans its plugin directory at startup. If an allowlist is configured, each library’s SHA-256 hash is verified before loading (see Plugin Allowlist below). If a code-signing trust bundle is configured, the library’s CMS detached signature is verified against it as well (see Plugin Code Signing below) — both checks run before
dlopen/LoadLibraryso tampered or untrusted binaries never execute code. - Each shared library (
.dllon Windows,.soon Linux,.dylibon macOS) is loaded dynamically. Plugin names must be unique within the directory: once a library is loaded and its declared name read, a second file declaring a name an earlier file in the same scan already claimed is unloaded again before itsinitruns and rejected with aduplicate plugin namereason; the first file the directory walk encountered keeps the name. - The agent calls the plugin’s
initfunction, passing a context with configuration and callbacks. - When the server sends an instruction targeting a plugin action, the agent dispatches it to the correct plugin.
- The plugin executes the action and returns results via the output callback.
Plugin Allowlist
The agent can verify plugin shared libraries against a SHA-256 allowlist before loading them. This prevents loading of tampered or unauthorized plugins.
Configuration:
yuzu-agent --plugin-allowlist /etc/yuzu/plugin-allowlist.txt
# or via environment variable:
YUZU_PLUGIN_ALLOWLIST=/etc/yuzu/plugin-allowlist.txt yuzu-agent
Allowlist file format (compatible with sha256sum output):
a1b2c3d4e5f6... libhardware.so
f6e5d4c3b2a1... libnetwork_info.so
9876543210ab... libos_info.so
Generate the allowlist from your verified plugin builds:
sha256sum /opt/yuzu/plugins/*.so > /etc/yuzu/plugin-allowlist.txt
Behavior:
- If
--plugin-allowlistis not set, all plugins in the plugin directory are loaded (default). - If set, only plugins listed in the allowlist file are loaded, and only if their hash matches.
- Plugins not in the allowlist or with a hash mismatch are logged as errors and skipped.
- The allowlist is checked before
dlopen/LoadLibrary, so tampered binaries never execute code.
Plugin Code Signing
The agent can verify a CMS detached signature for each plugin shared library and reject any plugin whose signature does not chain to an operator-configured trust anchor. This is the provenance layer that complements the allowlist’s integrity layer: the allowlist binds this filename to this hash on this host, and the signature binds this file content to a CA-issued signing identity. With both enabled, an attacker who drops a malicious .so plus a fresh .sig is rejected because their cert does not chain to the trusted CA.
Trust anchor. A standard PEM file containing one or more X.509 root or intermediate certificates the operator chooses to trust. The bundle is deployment-format-agnostic — it can hold a public CA root, an internal-CA root, or (when shipped) the Yuzu self-managed CA root. The verifier does not care which authority issued the signing cert as long as the chain validates against the bundle.
Signature file convention. For each plugin file <plugin>.so (or .dll / .dylib), the agent looks for a sibling <plugin>.so.sig containing a PEM-armoured CMS detached signature over the plugin file bytes. Inspect a signature with:
openssl cms -inform pem -text -noout -in chargen.so.sig
Configuration. There are two paths — server-managed (recommended for fleets) and agent-local (for one-off hosts or air-gapped environments).
Server-managed (Settings → Plugin Code Signing). An admin operator goes to Settings > Plugins and updates (or the Settings page) and uploads the PEM trust bundle through the Plugin Code Signing card; replacing a loaded bundle asks for confirmation. The recorded “Require signed plugins” flag is served with the bundle; each agent enforces its own --plugin-require-signature. The server validates the PEM (counts certs, computes SHA-256, surfaces parse errors immediately), persists it atomically (temp file + rename) under the server’s cert directory, and exposes it at GET /api/v2/agent/plugin-policy (PluginSigning:Read, Administrator-only and unreachable by any MCP token at any tier — see docs/user-manual/rest-api.md “Settings”; returns JSON containing the bundle PEM + the require flag, nested under a data envelope). The deprecated GET /api/v1/agent/plugin-policy (flat body, require_admin gate) still works during its announced removal window — see server-admin.md’s vNEXT note. The card also has a “Require signed plugins” checkbox that flips the require flag and a “Remove trust bundle” button that disables signing for new agent starts. Every change emits an audit_event with action plugin_signing.bundle.uploaded / plugin_signing.bundle.cleared / plugin_signing.require.changed.
In this mode, agents are configured by curling the policy endpoint (with an admin token) into a local file at startup:
curl -fsSL -H "Authorization: Bearer $YUZU_ADMIN_TOKEN" \
https://server.example.com:8443/api/v2/agent/plugin-policy \
| jq -r .data.trust_bundle_pem > /etc/yuzu/plugin-trust-bundle.pem
yuzu-agent --plugin-trust-bundle /etc/yuzu/plugin-trust-bundle.pem \
--plugin-require-signature
Important — fail-closed when require is set. If you pass
--plugin-require-signaturewithout--plugin-trust-bundle(or with an empty path), the agent refuses to start rather than silently fail-open. This is intentional: the prior behaviour would have skipped the entire signing block and quietly loaded unsigned plugins while the operator believed enforcement was active.
Important — Yuzu-shipped plugins are not signed yet. This release ships the verifier; it does not yet sign the in-tree
agents/plugins/. If you turn on Require with a trust bundle anchored only at your own CA, every Yuzu-bundled plugin will be rejected at next agent restart. Use the transitional mode (bundle uploaded, Require off) until you have signed every plugin your fleet uses, including the in-tree ones. The Settings card displays this warning inline.
Windows admins. The curl/jq recipe above assumes a Unix host; equivalent PowerShell using
Invoke-RestMethod+ConvertFrom-Jsonworks against the same endpoint. Place the resulting PEM atC:\ProgramData\Yuzu\certs\plugin-trust-bundle.pemand pass the path via--plugin-trust-bundle.
(Fully automatic agent-side fetch is a planned follow-up; today’s contract is “the bundle is hosted on the server, the agent is told where the local copy lives via CLI flags”.)
Agent-local CLI flags:
yuzu-agent \
--plugin-trust-bundle /etc/yuzu/plugin-trust-bundle.pem \
--plugin-require-signature
| Flag | Effect |
|---|---|
--plugin-trust-bundle <path> | Enables signature verification. PEM file with one or more CA certs. Must be a regular file of at most 1 MiB, and on Windows not held open by another process that denies shared reading; otherwise every signed plugin is refused as untrusted (since 0.14.1, #5249). Env: YUZU_PLUGIN_TRUST_BUNDLE. |
--plugin-require-signature | When set, plugins without a .sig sibling are rejected. When unset (default), unsigned plugins are allowed (transitional mode for ops rolling out signing). Env: YUZU_PLUGIN_REQUIRE_SIGNATURE. |
--verify-plugin-signature <path> | Packaging-time check, not a runtime setting. Verifies one plugin’s CMS signature sidecar against --plugin-trust-bundle using the agent’s own verifier, then exits without starting the agent: exit 0 if it verifies, non-zero with Plugin signature rejected: <reason> otherwise. Requires --plugin-trust-bundle (exits non-zero if it is missing). Hidden from --help; used by the macOS package build scripts. No environment variable. |
Signing a plugin (operator workflow, internal CA — recommended):
The server’s internal CA now issues code-signing leaves directly (gap-matrix #10): the operator still generates their own key and CSR locally — the server never sees the private key (CSR-custody model) — and submits only the CSR to POST /api/v1/ca/issue-code-signing (Security:Write). The server signs it with usage hard-pinned to codeSigning only (never clientAuth/serverAuth), so there is no -extfile/EKU step to get right by hand — the constraint the old self-signed recipe enforced manually is now enforced server-side unconditionally.
# One-time: generate your own signing keypair + CSR. The server never
# sees signer.key.
openssl ecparam -genkey -name prime256v1 -out signer.key
openssl req -new -key signer.key -out signer.csr \
-subj "/CN=Plugin Signer"
# Submit the CSR; `label` becomes the leaf's CN and must match
# ^[A-Za-z0-9._-]{1,64}$. Returns the signed leaf plus the issuer chain.
curl -s -X POST -H "X-Yuzu-Token: $TOKEN" -H 'Content-Type: application/json' \
-d "{\"csr_pem\": $(jq -Rs . < signer.csr), \"label\": \"plugin-signer-2026\"}" \
https://localhost:8443/api/v1/ca/issue-code-signing > signer_issuance.json
jq -r .certificate_pem < signer_issuance.json > signer.pem
jq -r .chain_pem < signer_issuance.json > signer_chain.pem
# Per-plugin: sign with the leaf, include the issuer chain via -certfile.
openssl cms -sign -binary \
-signer signer.pem -inkey signer.key -certfile signer_chain.pem \
-in chargen.so -outform pem -out chargen.so.sig
The agent trust bundle is the already-public CA root — GET /api/v1/ca/root — no separate distribution step for a self-signed CA is needed. Fetch it once (as in the server-managed trust-bundle recipe above) and ship the .sig files alongside each .so.
Bring-your-own-CA (alternative, for operators not using the internal CA): the verifier’s code path is CA-agnostic and accepts any chain that validates against the configured trust bundle, so an existing external PKI (or a throwaway self-signed CA) still works — generate ca.key/ca.pem, issue a signing leaf with extendedKeyUsage=codeSigning (REQUIRED — the agent’s verifier enforces X509_PURPOSE_CODE_SIGN, rejecting a leaf without this EKU even if it chains to the trusted CA), sign with openssl cms -sign as above, and distribute your own ca.pem as the trust bundle instead of the internal CA root.
Behavior:
- If
--plugin-trust-bundleis not set, signature checking is off entirely (default). - If set + plugin has a
.sig→ must verify, else rejected. - If set + plugin has no
.sig+--plugin-require-signaturenot set → loaded (transitional mode). - If set + plugin has no
.sig+--plugin-require-signatureset → rejected. - Verification happens before
dlopen/LoadLibrary. Cert validity (notBefore/notAfter), digest match over file bytes, and chain anchor are all enforced. - Rejection reason is recorded as a stable label on
yuzu_agent_plugin_rejected_total:signature_missing,signature_invalid, orsignature_untrusted_chain— alert distinctly on each.
Self-managed CA — shipped for code signing. The internal CA’s POST /api/v1/ca/issue-code-signing (above) already lets operators sign plugins against the same server-managed CA used for mTLS — the CA root is published at the already-public GET /api/v1/ca/root. The verifier itself did not change to support this: the same code path accepts public-CA, internal-CA, and internal-CA-issued codeSigning certs. A separate server-side key-minting CLI (the server generating the signer’s private key on the operator’s behalf) was deliberately NOT built — CSR-custody keeps the private key off the server end to end.
Not yet supported (follow-up work):
- CRL / OCSP revocation checking at plugin-load time. Revoking a code-signing leaf issued via
POST /api/v1/ca/issue-code-signing(viaPOST /api/v1/ca/revoke) is recorded in the issued-cert inventory and republishes the CRL, but the agent’s plugin-load verifier (this section) does not consult the CRL — so revocation does not yet reach agents at plugin-load time, exactly like a hand-rolled external CA today. Revoke a compromised signing cert by removing its issuing CA from the trust bundle or rotating to a new CA (or, for an internal-CA-issued leaf, by letting it expire — see below). Tracked follow-up:#4234. - Signer-cert expiry has an operational consequence worth calling out explicitly:
CMS_verifychecks the signer leaf’snotBefore/notAfterat verification time, so once a code-signing leaf expires its plugins stop loading at the next agent restart (existing running plugin instances are unaffected until then) — track signer expiry (not_afterfrom the issuance response) and re-sign/re-issue before it lapses. - Authenticode-signed Windows DLLs. The agent uses the same CMS PEM format on every platform; Authenticode is a separate signature format and is not currently consumed.
Output Format
Plugins emit results by calling write_output() with pipe-delimited (|) fields. Each call writes one row of output. The server parses these rows according to the result schema declared in the plugin’s InstructionDefinition YAML.
Example:
write_output("hostname|Windows 11 Pro|10.0.26200|x64");
Configuration Access
Plugins can read configuration values set by the server using get_config(key). This allows the server to pass parameters (filter strings, thresholds, target paths) to plugin actions at execution time.
Data Storage
Each plugin can persist state locally. Per-plugin data is stored at:
<agent_data_dir>/<plugin_name>.json
For example, the tags plugin stores its tag set in tags.json within the agent’s data directory. This file survives agent restarts and upgrades.
Writing a New Plugin
Use the example plugin as a starting template. The minimum steps are:
- Create a new directory under
agents/plugins/<your_plugin>/. - Implement the C ABI entry points (or use the C++ CRTP wrapper).
- Add the plugin to
meson.build. - Write an
InstructionDefinitionYAML file incontent/definitions/following theyuzu.io/v1alpha1DSL spec. - Register plugin actions in the Substrate Primitive Reference table (
docs/yaml-dsl-spec.md, section 14). - Document the plugin (
docs/plugin-readme-standard.md): writeagents/plugins/<your_plugin>/README.mdfromdocs/templates/plugin-README.md; add the optionaldescription,values,exampleandplatformskeys to the definition’sresult.columns; on every OS where an action is supported or constrained, capture a sample with the builttools/plugin-captureon that OS (plugin-capture <plugin library> --os <os> --host-class <bare-metal|vm|container> --action <name> … --out agents/plugins/<your_plugin>/docs/samples/<os>.txt; the standard’s “Capturing on each OS” section has the per-host recipes, including the Linux container — a three-OS plugin needs all three before the PR is raised; a leg no capture host can honestly run is recorded[not captured] <class>: <reason>), thenpython3 tools/plugin-doc-gen/plugin_doc_gen.py --stamp <your_plugin> <os>to write the leg-hash; finallypython3 tools/plugin-doc-gen/plugin_doc_gen.py --all(ormeson compile -C <builddir> docs-regen) so the generated fences, this index, the site navigation and the plugin’scontent/plugin-docsmanifest are current, and--checkto confirm — it runs every generator-side check; thedocssuite adds the ratchet and the manifest-key binding. LowerRATCHET_BASELINE_MISSINGintests/test_plugin_readmes.pyby one in the same PR (the gate fails until you do, and tells you the number). Thedocstest suite and the docs-lint touch rule enforce this.
For the full DSL specification, see docs/yaml-dsl-spec.md. For a walkthrough, see docs/getting-started.md.