yuzu

software_actions

What it doesLists upgradable packages and counts installed software
Version1.1.0
KindCollector · read-only · gathered (device.software_actions.list_upgradable, device.software_actions.installed_count)
PlatformsWindows ✅ · macOS ✅ · Linux ✅
Actionsinstalled_count (definition device.software_actions.installed_count) · list_upgradable (definition device.software_actions.list_upgradable)
Securitysecurable SoftwareDeployment · operation Read · risk Low · dispatch ReadOnly · approval gate None
Rolesexecute: endpoint-admin, endpoint-operator · author: content-author

How it works

list_upgradable asks the host’s own package tool what has an update waiting and reports one row per package. On Windows that tool is winget upgrade (60-second deadline), on macOS softwareupdate -l (60 seconds), and on Linux apt list --upgradable (20 seconds). On Linux, apt is authoritative when it runs clean: its answer, rows or “up to date”, ends the query. yum or dnf check-update (20 seconds) is tried only when apt is absent or did not give a usable answer. installed_count returns one number: a native count of Uninstall registry subkeys on Windows (no subprocess), pkgutil --pkgs receipts on macOS, and dpkg-query or rpm -qa packages on Linux (20-second deadline for the subprocess legs). Every tool is run as a plain argument list through the agent’s shared subprocess runner, which enforces a time limit, never through a shell.

A query that did not complete never looks clean. If the tool is missing, cannot start, times out, is cut short or fails with nothing parsed, the result carries a failure status and no count| or “up to date” row. The “System is up to date” row is written only when a tool ran clean and listed nothing. Neither action is a patch-compliance check or a software inventory: there is no severity, no publisher, no install date and no per-user breakdown here (see installed_apps for inventory).

flowchart LR
  OP["Operator or workflow"] --> SRV["Server: SoftwareDeployment Read"]
  SRV -->|"gRPC mTLS"| HOST["Agent plugin host"] --> EX["software_actions.execute"]
  EX --> WIN["Windows: winget upgrade for list_upgradable, Uninstall registry subkey count for installed_count"]
  EX --> MAC["macOS: softwareupdate -l for list_upgradable, pkgutil --pkgs for installed_count"]
  EX --> LIN["Linux: apt, else yum or dnf for list_upgradable, dpkg-query else rpm for installed_count"]
  WIN --> ROWS["Rows and typed result status"]
  MAC --> ROWS
  LIN --> ROWS
  ROWS --> RS[("Server response store")] --> API["REST /api/responses and MCP"]

What you can decide with it

WindowsmacOSLinux
list_upgradable askswinget upgrade --accept-source-agreements (60 s)softwareupdate -l (60 s)apt list --upgradable (20 s), else yum or dnf check-update (20 s)
What it coversApplications winget can match to one of its sourcesApple’s own software-update list; no third-party, Homebrew or other app query is madeOS packages known to the host’s apt or yum/dnf; no snap, flatpak or language-package query
Where it worksExpected to work only when the agent runs in a user session (not measured). Winget is installed per user, and that per-user launcher does not exist when the agent runs as LocalSystem (the built-in all-powerful Windows service account); the Windows capture, taken as SYSTEM, fails with subprocess_runner:spawn_errorMeasured as an ordinary user; nothing in the plugin needs moreMeasured as root in a container; the plugin uses no sudo
Row shapename, current version, available versionApple’s update label, with - for both versionsapt: name, current, available. yum or dnf: name, -, available
installed_count countsSubkeys of the 64-bit Uninstall registry key (captured: 124)Installer receipts listed by pkgutil --pkgs (captured: 68)Installed packages from dpkg-query (including held ones), else lines of rpm -qa (captured: 206)
QuestionUseAnswer and limits
Does winget, Apple or the Linux package manager see an update waiting?list_upgradableYes: one row per package. No: the upgradable|none|System is up to date|- row, which means that one tool ran clean and listed nothing. A failure status with no data row means unknown, not clean
Roughly how much software is on this host, and has it changed?installed_countA trend on one host. The units differ per OS (registry subkeys, receipts, packages), so counts do not compare across Windows, macOS and Linux, and a Windows count excludes 32-bit applications
Is this host patched?NeitherNot answerable. The lists are one tool per OS with its own scope, apt reads the package lists already on the host and the plugin never refreshes them, nothing is graded by severity or security, and on Windows the action does not run under the service account. Use windows_updates for missing-patch state, vuln_scan for CVE matching and installed_apps for a real inventory

Diagrams

Linux list_upgradable decision flow

flowchart TD
  A{"apt present?"} -->|"yes"| R["Run apt list --upgradable, 20 s"]
  A -->|"no"| Y{"yum or dnf present?"}
  R --> C1{"Ran clean, exit 0, not truncated?"}
  C1 -->|"yes"| D1["Done: apt rows, or the up-to-date row. yum and dnf are not tried"]
  C1 -->|"no"| Y
  Y -->|"yes"| YR["Run check-update, 20 s"]
  Y -->|"no"| G{"apt was tried and failed?"}
  YR --> C2{"Ran clean, exit 0 or 100?"}
  C2 -->|"yes, rows"| D2["Rows returned. CONSTRAINED apt_list_upgradable_failed if apt had failed"]
  C2 -->|"yes, no rows"| F{"apt was tried and failed?"}
  C2 -->|"no"| D4["No data row. Status: the runner failure or yum_check_update_failed"]
  F -->|"yes"| D5["No data row. Status: the apt failure"]
  F -->|"no"| D6["Up-to-date row"]
  G -->|"yes"| D5
  G -->|"no"| D8["UNAVAILABLE no_supported_package_manager"]

apt is the only source consulted on a Debian-family host that runs it cleanly, so an unrelated dnf problem can never overwrite apt’s answer. The fallback to yum or dnf exists for RPM hosts and for hosts where apt failed; in that second case a clean but empty yum answer is still reported as the apt failure, never as “up to date”. A yum or dnf exit code of 100 means updates are available and is treated as success.

OS capability

ActionWindowsmacOSLinux
installed_count✅ supported · rung 1 · native Reg*W subkey count of the Uninstall key✅ supported · rung 2 · pkgutil —pkgs via bounded argv runner✅ supported · rung 2 · dpkg-query/rpm via bounded argv runner
list_upgradable🟡 constrained · rung 2 · winget via bounded argv runner✅ supported · rung 2 · softwareupdate -l via bounded argv runner✅ supported · rung 2 · apt/yum check-update via bounded argv runner

Declared limits per leg (descriptor fallback text, verbatim):

Privileges and prerequisites

OSRuns asExtra grant neededMeasuredIf the read is refused
WindowsAgent service account: LocalSystem today, not yet the intended virtual service account (docs/agent-privilege-model.md); no row for this plugin thereinstalled_count reads HKLM\...\Uninstall natively and needs nothing extra. list_upgradable needs %LOCALAPPDATA% (the current user’s application-data folder) and a user-session context: winget is a per-user launcher stub (a Windows “App Execution Alias”) that does not exist when the agent runs as LocalSystem2026-09-07 on bare metal as SYSTEM: installed_count returned 124, list_upgradable failed with subprocess_runner:spawn_errorlist_upgradable: UNAVAILABLE / partial with subprocess_runner:spawn_error or software_actions:localappdata_unset. installed_count: UNAVAILABLE / partial with software_actions:registry_query_failed
macOSAgent daemon as root: the shipped LaunchDaemon has no UserName key (docs/agent-privilege-model.md). The plugin does not drop privilegeNone: softwareupdate -l and pkgutil --pkgs ran for an ordinary user2026-09-07 on macOS 26.6.2 as an ordinary non-root user, below the production identityUNAVAILABLE / partial with software_actions:softwareupdate_not_present or software_actions:softwareupdate_failed; installed_count: software_actions:pkgutil_not_present or software_actions:pkgutil_query_failed
LinuxAgent daemon, dedicated unprivileged account (yuzu-agent on the packaged unit), never root by design (docs/agent-privilege-model.md)None in the plugin: it uses no sudo. apt list --upgradable, dpkg-query and rpm -qa are local reads. yum or dnf check-update can contact repositories, and whether an unprivileged account may refresh their metadata is the tool’s own behaviour, not measured here2026-09-06 in a container as root (euid 0), a more privileged identity than productionlist_upgradable: UNAVAILABLE / partial with software_actions:no_supported_package_manager, software_actions:apt_list_upgradable_failed or software_actions:yum_check_update_failed; installed_count: software_actions:dpkg_query_failed, software_actions:rpm_query_failed or software_actions:no_supported_package_manager

Subprocesses, all run as a plain argument list through the agent’s subprocess runner with a time limit and no shell: winget.exe (Windows list_upgradable, 60 s), softwareupdate (macOS list_upgradable, 60 s), apt and yum or dnf (Linux list_upgradable, 20 s each), dpkg-query or rpm (Linux installed_count, 20 s), pkgutil (macOS installed_count, 20 s). A tool that outlives its deadline is killed and reported as subprocess_runner:deadline. installed_count on Windows spawns nothing. Network: winget upgrade and softwareupdate -l reach their configured update sources, and yum or dnf check-update can; the other legs are local reads.

Data contract

Inputs

Neither action takes parameters.

Outputs

Pipe-delimited rows, one literal-discriminator field followed by the action’s own fields. list_upgradable writes one upgradable|... line per upgradable package, or a single sentinel row when the query ran clean and nothing is pending; installed_count writes exactly one count|N line, or none at all on a degraded read — a count|0 line is never emitted for a failed or absent query.

device.software_actions.installed_count — count

FieldTypeValuesAvailableExampleDescription
countint32-Windows, Linux, macOS124Non-negative count of installed packages or applications; the line is omitted entirely (never emitted as 0) when the underlying query failed. Values: non-negative integer.

device.software_actions.list_upgradable — package_name|current_version|available_version

FieldTypeValuesAvailableExampleDescription
package_namestring-Windows, Linux, macOSnoneThe package or application name; the literal ‘none’ on the clean-scan sentinel row when nothing is upgradable. Values: free text, or the literal none.
current_versionstring-Windows, Linux, macOSSystem is up to dateThe currently installed version, or ’-’ when not read; on the up-to-date sentinel row this field carries the literal sentence ‘System is up to date’ instead of a version. Values: free text, or -, or the literal System is up to date.
available_versionstring-Windows, Linux, macOS-The version available to upgrade to, or ’-’ when not applicable or not read. Values: free text, or -.

Result status

A clean read sets no result status (the agent records UNDECLARED / UNKNOWN, as the macOS and Linux samples show). Every failure below sets one and writes no count| line and no “up to date” row. Actions that fail return exit code 1. A partial result (CONSTRAINED) that still wrote data rows returns 0, so check the result status and not only the exit code: this covers winget_header_unrecognized, winget_rows_unmapped, winget_partial_exit, softwareupdate_partial_exit, and on Linux apt_list_upgradable_failed when yum or dnf found rows. A CONSTRAINED result with no rows written (for example winget_no_table) returns 1.

StatusCompletenessProvenanceWhen
UNDECLARED——A clean run: rows, the up-to-date row, or a count|N line
UNAVAILABLEPARTIALsoftware_actions:localappdata_unsetWindows list_upgradable: no %LOCALAPPDATA% in the agent’s environment, so the winget path cannot be built
UNAVAILABLEPARTIALsubprocess_runner:spawn_errorThe runner could not start the tool (for example winget under LocalSystem, as in the Windows sample)
CONSTRAINEDPARTIALsubprocess_runner:deadline / subprocess_runner:cancelled / subprocess_runner:signaledThe tool outlived its deadline and was killed, the run was cancelled, or the tool was killed by a signal
OKPARTIALsubprocess_runner:line_limitNever emitted by this plugin, which sets no line cap; named because the shared runner can produce it
UNAVAILABLEPARTIALsoftware_actions:winget_failed / software_actions:softwareupdate_failed / software_actions:apt_list_upgradable_failed / software_actions:yum_check_update_failedThe tool ran but the capture was unusable (truncated or a failing exit) or a failing exit parsed no rows; on Linux this is also the status when apt failed and yum or dnf found nothing
UNAVAILABLEPARTIALsoftware_actions:softwareupdate_not_present / software_actions:no_supported_package_managersoftwareupdate is missing, or on Linux neither apt nor yum or dnf is present
UNAVAILABLEPARTIALsoftware_actions:registry_query_failed / software_actions:dpkg_query_failed / software_actions:rpm_query_failed / software_actions:pkgutil_query_failed / software_actions:pkgutil_not_presentinstalled_count: the registry read or the package-manager query failed, or pkgutil is absent
CONSTRAINEDPARTIALsoftware_actions:winget_header_unrecognized / software_actions:winget_rows_unmapped / software_actions:winget_partial_exit / software_actions:winget_no_tablewinget’s table was found but its header could not be read (rows are name only), some rows were dropped as unmapped, winget exited non-zero but rows still parsed, or no recognisable table was found (the row is then upgradable|none|-|-)
CONSTRAINEDPARTIALsoftware_actions:softwareupdate_partial_exitsoftwareupdate -l exited non-zero but update labels still parsed
CONSTRAINEDPARTIALsoftware_actions:apt_list_upgradable_failedLinux: apt failed but yum or dnf returned rows; the rows are real and apt’s failure is flagged

Where the data goes

Sample output

Windows — captured: windows Microsoft Windows NT 10.0.26200.0 x64 · bare-metal · 2026-09-07 · SYSTEM · leg-hash c95c61957b6f

== action=list_upgradable
[result_status] UNAVAILABLE / PARTIAL / subprocess_runner:spawn_error
[rc] 1

== action=installed_count
count|124
[result_status] UNDECLARED / UNKNOWN

macOS — captured: macos macOS 26.6.2 arm64 · bare-metal · 2026-09-07 · euid 501 (jsmith) · leg-hash c95c61957b6f

== action=list_upgradable
upgradable|none|System is up to date|-
[result_status] UNDECLARED / UNKNOWN

== action=installed_count
count|68
[result_status] UNDECLARED / UNKNOWN

Linux — captured: linux Debian GNU/Linux 13 (trixie) aarch64 · container · 2026-09-06 · euid 0 · leg-hash c95c61957b6f

== action=list_upgradable
upgradable|none|System is up to date|-
[result_status] UNDECLARED / UNKNOWN

== action=installed_count
count|206
[result_status] UNDECLARED / UNKNOWN

Caveats and known gaps

  1. It cannot say whether a host is patched. Each OS answers from one tool with its own scope (winget’s sources, Apple’s update list, apt or yum/dnf), apt reads the package lists already on the host and the plugin never refreshes them, and no row carries a severity or a security flag. “Up to date” means that one tool listed nothing.
  2. The Windows list_upgradable leg does not work under the shipped service account. winget is installed per user, and its launcher does not exist when the agent runs as LocalSystem, so the Windows capture (taken as SYSTEM) is a subprocess_runner:spawn_error failure. The action is expected to work only when the agent runs in a user session (not measured; the SYSTEM capture fails). Even then it lists only what winget has a source for, not Windows Update.
  3. installed_count is a gauge, not an inventory, and its unit differs per OS. Windows counts every subkey of the 64-bit Uninstall key whether or not the subkey holds any values, and misses 32-bit applications and per-user installs; macOS counts installer receipts; Linux counts installed packages, held ones included. Compare a host with itself over time, never across operating systems.
  4. Row shapes carry some conventions to special-case. The up-to-date row puts a sentence in the current_version field, so key on the package name none. macOS rows hold Apple’s update label with - for both versions, and yum or dnf rows have no current version. A row upgradable|none|-|- with CONSTRAINED / winget_no_table means winget’s output was not recognised, not that the host is up to date.
  5. A failed query is never made to look clean, and that is an invariant. Failures write a typed status and no count|0 or up-to-date row, and a failed apt followed by a clean empty yum answer still reports the apt failure. Do not add a fallback that emits count|0 or the up-to-date row on error.

Source and tests