The Hive's rules
Laboratory. The Hive's rule language exists to press on the language and to drive Quantum OS; it carries no stability promise and may change in any release (
SUPPORT_TIERS.md).
A q:application type="hive" is the rules of the Hive, the set of daemons that watch a Quantum OS system (os/ in the repository): what the Hive offers when something happens, and what its actuator may do at all, in tags. It compiles to two files the daemons load: rules.toml, which hive-synapse reads to turn an event into offers an administrator may approve, and whitelist.toml, which hive-actuator checks every action against before running it. There is nothing to compute in a hive: no statements, no expressions, and text where a tag goes is a compile error.
quantum run hive.q --out projects/hive/hive # writes rules.toml and whitelist.toml
python scripts/generate-hive-config.py # the OS's own: os/config/hive/hive.q -> os/config/hive/Two things are checked before anything runs. A {field} in an offer is a field the event has, of the right type, and it is filled in by hive-synapse from the event itself, never from the model's text. And an offer the file's own whitelist would refuse does not compile, so an administrator never approves something that fails.
The shape of a hive
<q:application id="hive" type="hive">
<qh:on event="service-crash">
<qh:offer title="Restart {service}"
description="{unit} stopped unexpectedly. Run `systemctl restart {service}`."
action="restart-service" service="{service}" />
<qh:offer title="Show recent logs for {service}"
action="run-command" cmd="journalctl" args="-n 50 -u {service}" />
</qh:on>
<qh:on event="command-not-found">
<qh:offer title="Install {package}" action="install-package" package="{package}" />
</qh:on>
<qh:allow command="journalctl" args="-n 50 -u *" /> <!-- `*`: any one argument -->
<qh:allow service="nginx postgresql redis-server" />
<qh:block service="hive-* systemd-* dbus sshd" /> <!-- a block wins over an allow -->
<qh:block package="systemd libc6 apt dpkg" /> <!-- never removed -->
<qh:limit action="install-package" per-minute="5" />
</q:application>The rule on service-crash compiles to this, in rules.toml, with every parameter a string that hive-synapse types once the fields are filled in:
[[rules]]
event = "service-crash"
[[rules.offers]]
title = "Restart {service}"
description = "{unit} stopped unexpectedly. Run `systemctl restart {service}`."
action = "restart-service"
service = "{service}"Attribute types
| Type | What it is |
|---|---|
text | a string; {field} placeholders allowed |
args | words separated by spaces, each a placeholder or not |
int | a number, or a {field} of a field that is one |
bool | true or false |
words | one or more names separated by spaces |
enum | one of the values listed |
The tags
qh:on
What the Hive offers when an event happens: the qh:offers inside, in order. One per event.
Goes inside <q:application>.
| Attribute | Type | Default | What it is |
|---|---|---|---|
event= | enum: service-crash, log-anomaly, port-conflict, disk-low, memory-pressure, command-observed, command-not-found | required |
qh:offer
One thing an administrator may approve for the actuator to do. title= and description= are shown on the dashboard; action= names what runs, with the attributes that action takes. Any of them may hold {field}, a field of the event, filled in from the event itself.
Goes inside <qh:on>.
| Attribute | Type | Default | What it is |
|---|---|---|---|
title= | text | required | |
description= | text | `` | |
action= | enum: run-command, start-service, stop-service, restart-service, reload-service, enable-service, disable-service, install-package, remove-package, update-package-lists, upgrade-packages, clean-package-cache, kill-port-process, create-snapshot, list-snapshots | required |
Plus the attributes of its action=, listed under the actions.
qh:allow
What the actuator may do at all: its whitelist, which fails closed. Exactly one of command= (with args=: * is any one argument; the argument count must match), service=, path=, dataset=, action= (a permission with no list: kill-port-process, open-url, firewall, set-hostname, set-keyboard, set-screen-level, set-network) or delete-snapshot= (the snapshot names the Hive may destroy, a pattern).
Goes inside <q:application>.
| Attribute | Type | Default | What it is |
|---|---|---|---|
command= | text | one program | |
args= | args | `` | its arguments, with command= |
service= | words | services it may start, stop, restart, reload, enable, disable | |
path= | words | where files may be created, moved, copied, deleted (patterns) | |
dataset= | words | ZFS datasets it may snapshot (patterns) | |
action= | enum: kill-port-process, open-url, firewall, set-hostname, set-keyboard, set-screen-level, set-network | ||
delete-snapshot= | text | a pattern of snapshot names |
qh:block
What the actuator may never do, whatever is allowed: exactly one of service= (patterns), package= (never removed; patterns) or path= (patterns).
Goes inside <q:application>.
| Attribute | Type | Default | What it is |
|---|---|---|---|
service= | words | ||
package= | words | ||
path= | words |
qh:limit
At most this many of an action a minute, under the actuator's overall limit.
Goes inside <q:application>.
| Attribute | Type | Default | What it is |
|---|---|---|---|
action= | enum: run-command, create-snapshot, restore-snapshot, list-snapshots, delete-snapshot, show-notification, open-file, open-url, install-package, remove-package, update-package-lists, upgrade-packages, start-service, stop-service, restart-service, reload-service, enable-service, disable-service, create-directory, move-file, copy-file, delete-file, change-permissions, kill-port-process, add-firewall-rule, reboot, shutdown, suspend, clean-package-cache, clean-system, set-hostname, set-keyboard, set-screen-level, set-network | required | |
per-minute= | int | required |
The events
What a qh:on can be on: the observations hive-cortex passes to hive-synapse (HiveMessage in os/crates/hive-common/src/messages.rs), and the fields an offer may hold as {field}.
service-crash
A unit stopped unexpectedly (hive-nerve-journal saw it in the journal). (ServiceCrash)
| Field | Type | What it is |
|---|---|---|
{unit} | text | the unit, as systemd names it (nginx.service) |
{service} | text | the service name the actuator uses (nginx); absent when the unit is not a .service, and the offers that need it are left out |
{exit_code} | int | how it exited |
log-anomaly
A unit logs an unusual number of errors. (LogAnomaly)
| Field | Type | What it is |
|---|---|---|
{unit} | text | the unit, as systemd names it |
{service} | text | the service name, as in service-crash |
port-conflict
A program could not bind a port another one holds (hive-nerve-network). (PortConflict)
| Field | Type | What it is |
|---|---|---|
{port} | int | the port |
{requester} | text | the program that failed to bind |
{holder} | text | the program that holds the port |
disk-low
A filesystem is nearly full. (DiskSpaceLow)
| Field | Type | What it is |
|---|---|---|
{mount} | text | the mount point |
{percent} | int | how full, in percent |
{free_mb} | int | megabytes free |
memory-pressure
The system is running low on memory. (MemoryPressure)
| Field | Type | What it is |
|---|---|---|
{free_mb} | int | megabytes available |
{total_mb} | int | megabytes in all |
command-observed
A user ran a command the Hive judges dangerous (hive-nerve-process, from the audit log). (CommandObserved)
| Field | Type | What it is |
|---|---|---|
{cmd} | text | the command line |
{program} | text | the program |
command-not-found
A user ran a program that is not installed, and a package of that name exists (grove). (CommandNotFound)
| Field | Type | What it is |
|---|---|---|
{command} | text | what the user typed |
{package} | text | the package that would provide it |
The actions
What a qh:offer can name: the Action variants hive-actuator runs (os/crates/hive-common/src/actions.rs), each under the kebab-case of its name, with the attributes it takes.
run-command
Runs a command. It must match a <qh:allow command=> of this file, argument for argument. (Action::RunCommand)
| Attribute | Type | Default | What it is |
|---|---|---|---|
cmd= | text | required | the program |
args= | args | `` | its arguments |
start-service
Starts a service (systemctl start). (Action::StartService)
| Attribute | Type | Default | What it is |
|---|---|---|---|
service= | text | required |
stop-service
Stops a service. (Action::StopService)
| Attribute | Type | Default | What it is |
|---|---|---|---|
service= | text | required |
restart-service
Restarts a service. (Action::RestartService)
| Attribute | Type | Default | What it is |
|---|---|---|---|
service= | text | required |
reload-service
Reloads a service. (Action::ReloadService)
| Attribute | Type | Default | What it is |
|---|---|---|---|
service= | text | required |
enable-service
Enables a service at boot, and starts it now unless now=false. (Action::EnableService)
| Attribute | Type | Default | What it is |
|---|---|---|---|
service= | text | required | |
now= | bool | True |
disable-service
Disables a service at boot, and stops it now unless now=false. (Action::DisableService)
| Attribute | Type | Default | What it is |
|---|---|---|---|
service= | text | required | |
now= | bool | True |
install-package
Installs a package through PackageKit, after a safety snapshot. (Action::InstallPackage)
| Attribute | Type | Default | What it is |
|---|---|---|---|
package= | text | required |
remove-package
Removes a package (never one a <qh:block package=> names), after a safety snapshot; dangerous, so the dashboard asks twice. (Action::RemovePackage)
| Attribute | Type | Default | What it is |
|---|---|---|---|
package= | text | required | |
purge= | bool | False | remove its configuration too |
update-package-lists
Refreshes the package lists. (Action::UpdatePackageLists)
Takes no attribute.
upgrade-packages
Upgrades packages, after a safety snapshot. (Action::UpgradePackages)
| Attribute | Type | Default | What it is |
|---|---|---|---|
packages= | words | `` | which; none means all |
clean-package-cache
Cleans the package cache. (Action::CleanPackageCache)
Takes no attribute.
kill-port-process
Ends the process holding a port; needs <qh:allow action="kill-port-process"/>. (Action::KillPortProcess)
| Attribute | Type | Default | What it is |
|---|---|---|---|
port= | int | required | |
signal= | int | the signal, 15 when not given |
create-snapshot
Takes a ZFS snapshot of a dataset a <qh:allow dataset=> names. (Action::CreateSnapshot)
| Attribute | Type | Default | What it is |
|---|---|---|---|
dataset= | text | required | |
name= | text | required | |
recursive= | bool | True |
list-snapshots
Lists ZFS snapshots. (Action::ListSnapshots)
| Attribute | Type | Default | What it is |
|---|---|---|---|
dataset= | text | of one dataset; all when not given |
What qh:limit may name
Every Action variant, in kebab-case: run-command, create-snapshot, restore-snapshot, list-snapshots, delete-snapshot, show-notification, open-file, open-url, install-package, remove-package, update-package-lists, upgrade-packages, start-service, stop-service, restart-service, reload-service, enable-service, disable-service, create-directory, move-file, copy-file, delete-file, change-permissions, kill-port-process, add-firewall-rule, reboot, shutdown, suspend, clean-package-cache, clean-system, set-hostname, set-keyboard, set-screen-level, set-network.
What qh:allow action= may switch on
kill-port-process, open-url, firewall, set-hostname, set-keyboard, set-screen-level, set-network. Off unless the file switches it on.
What is refused
A compile error names the line. The compiler refuses:
- a tag, an attribute, an event or an action the schema does not list;
- a
{field}that is not a field of the rule's event, or a text field where a number goes; - an offer the file's own whitelist would refuse: a command no
qh:allow command=permits (an argument that is a{field}needs a*in its place), a service no allow permits or a block names, a package a block names, a dataset no allow permits, or a permission the file does not switch on; - a
q:statement, an expression, or text where a tag goes; - two
qh:onon one event, aqh:onwith nothing to offer, aqh:allowof two things at once, aqh:limitgiven twice or of less than one a minute.
What the compiler cannot check, the daemons do. hive-synapse refuses a rules.toml that fails the same checks and uses the rules its build was made with; an offer whose value is not what its action takes at run time (a port that is not a number, a unit that is not a service) is left out, never guessed. hive-actuator checks every action against the whitelist, which fails closed, and rebooting, shutting down and suspending have no switch at all.
How it reaches the daemons
rules.toml and whitelist.toml are installed as /etc/hive/rules.toml and /etc/hive/whitelist.toml. The OS's own files are written from os/config/hive/hive.q by scripts/generate-hive-config.py, committed as written and checked in CI, and hive-synapse embeds the committed rules.toml as its built-in rules, for a system without the file. The Hive lab (os/crates/hive-lab) plays a tape per rule against the real hive-cortex and hive-synapse in a fraction of a second; the VM lab (os/scripts/lab/vm) follows the same rules through the real system.
The rules Quantum OS ships
os/config/hive/hive.q, whole: the five rules and the whitelist the daemons run with.
<!--
The Hive's rules, in Quantum: what the Hive offers when something happens,
and what the actuator may do at all. `quantum run hive.q` (from the
repository's root: python scripts/generate-hive-config.py) writes
rules.toml, which hive-synapse loads, and whitelist.toml, which
hive-actuator loads. Both are committed next to this file and checked
against it in CI; edit this file, never them.
A {field} is filled in by hive-synapse from the event itself, never from
the model's text. An offer this file's own whitelist would refuse does not
compile. docs/targets/hive.md is the reference.
-->
<q:application id="hive" type="hive">
<!-- ================================================================ offers -->
<qh:on event="service-crash">
<qh:offer title="Restart {service}"
description="{unit} stopped unexpectedly. Run `systemctl restart {service}`."
action="restart-service" service="{service}" />
<qh:offer title="Show recent logs for {service}"
description="The last 50 journal lines from {service}, to see why it failed."
action="run-command" cmd="journalctl" args="-n 50 -u {service}" />
</qh:on>
<qh:on event="log-anomaly">
<qh:offer title="Show recent logs for {service}"
description="The last 50 journal lines from {service}, to see why it failed."
action="run-command" cmd="journalctl" args="-n 50 -u {service}" />
</qh:on>
<qh:on event="disk-low">
<qh:offer title="Show disk usage"
description="See how full {mount} and the other mounts are."
action="run-command" cmd="df" args="-h" />
</qh:on>
<qh:on event="command-not-found">
<qh:offer title="Install {package}"
description="`{command}` isn't installed; the {package} package provides it."
action="install-package" package="{package}" />
</qh:on>
<qh:on event="port-conflict">
<qh:offer title="Show listening ports"
description="See which process is holding port {port}."
action="run-command" cmd="ss" args="-tlnp" />
</qh:on>
<!-- ============================================================= whitelist -->
<!-- Commands: each matched argument for argument; `*` is any one argument. -->
<qh:allow command="df" args="-h" />
<qh:allow command="free" args="-h" />
<qh:allow command="uptime" />
<qh:allow command="whoami" />
<qh:allow command="id" />
<qh:allow command="uname" args="-a" />
<qh:allow command="ss" args="-tlnp" />
<qh:allow command="ip" args="addr" />
<qh:allow command="ip" args="route" />
<qh:allow command="systemctl" args="status *" />
<qh:allow command="systemctl" args="is-active *" />
<qh:allow command="journalctl" args="-n 50 -u *" />
<!-- The machine's settings a user asks for through the Settings app
(hive request) and an administrator approves on the screen: its name
(hostnamectl), its keyboard (us or br), its screen's level, an
interface's address (netplan; dangerous: Enter twice).
Two a minute each: a setting is not something to flap. -->
<qh:allow action="set-hostname" />
<qh:allow action="set-keyboard" />
<qh:allow action="set-screen-level" />
<qh:allow action="set-network" />
<qh:limit action="set-hostname" per-minute="2" />
<qh:limit action="set-keyboard" per-minute="2" />
<qh:limit action="set-screen-level" per-minute="2" />
<qh:limit action="set-network" per-minute="2" />
<qh:allow command="zpool" args="status" />
<qh:allow command="zfs" args="list" />
<qh:allow command="zfs" args="list -t snapshot" />
<!-- Services the actuator may start, stop, restart, reload, enable and
disable; a block wins over an allow, and the Hive and the units that
keep the machine reachable are never touched. -->
<qh:allow service="nginx apache2 postgresql mysql mariadb redis-server mongodb docker containerd
NetworkManager cups bluetooth" />
<qh:block service="hive-* systemd-* dbus sshd" />
<!-- Packages: any valid name may be installed (the per-minute limit below
bounds it); these are never removed. -->
<qh:block package="systemd linux-image-* grub-* hive-* zfs-* libc6 apt dpkg" />
<!-- Files: where they may be created, moved, copied, deleted and have their
permissions changed, and where never. -->
<qh:allow path="/home/* /tmp/* /var/tmp/*" />
<qh:block path="/home/*/.ssh/* /home/*/.gnupg/* /etc/* /usr/* /bin/* /sbin/* /lib/* /boot/* /root/*" />
<!-- ZFS: the datasets it may snapshot, and the snapshots it may destroy
(only the Hive's own). -->
<qh:allow dataset="rpool/ROOT/* rpool/home/*" />
<qh:allow delete-snapshot="hive-*" />
<!-- Permissions with no list behind them. Rebooting, shutting down and
suspending have no switch: never. -->
<qh:allow action="kill-port-process" />
<qh:allow action="open-url" />
<!-- At most this many a minute, under the actuator's overall limit. -->
<qh:limit action="kill-port-process" per-minute="10" />
<qh:limit action="install-package" per-minute="5" />
<qh:limit action="remove-package" per-minute="3" />
<qh:limit action="create-directory" per-minute="20" />
<qh:limit action="move-file" per-minute="20" />
<qh:limit action="copy-file" per-minute="20" />
<qh:limit action="delete-file" per-minute="20" />
<qh:limit action="change-permissions" per-minute="20" />
<qh:limit action="create-snapshot" per-minute="10" />
<qh:limit action="restore-snapshot" per-minute="10" />
<qh:limit action="delete-snapshot" per-minute="10" />
</q:application>