Shipped

This is not a proposal, not a PR under review, not a mailing list thread. MidnightBSD has merged and shipped a working age verification daemon. It is enabled by default.

Architecture Overview

The aged subsystem takes a fundamentally different approach from the Linux ecosystem's D-Bus/portal/systemd stack. There is no D-Bus. There is no systemd. There are no portals. This is a traditional Unix daemon with a Unix domain socket, a C library in libutil, and group-based access control — the BSD way.

aged(8) — The Daemon

A privileged daemon that manages age data for all users on the system. Stores records in a SQLite database at /var/db/aged/aged.db. Listens on a Unix domain socket at /var/run/aged/aged.sock. Starts before LOGIN in the boot sequence. Drops privileges to a dedicated aged user (UID 866) after binding the socket.

Enabled by default: aged_enable="YES" is set in rc.conf.

agectl(8) — The CLI

Command-line interface for interacting with the daemon. Any user can query their own age bracket. Root can set age or date of birth for any user and manage age group memberships.

agev(3) — The Library

Three C functions added to libutil, available to any application compiled against MidnightBSD: agev_get_age_bracket(), agev_set_age(), and agev_set_dob(). This is the application-facing API — the equivalent of what the Linux ecosystem is trying to build with xdg-desktop-portal.

Introduced in PR #310.

Age Groups — Unix Group Enforcement

Four Unix groups for age-based access control, used by MidnightBSD's mport package manager to gate package installation:

  • age4p (GID 856) — users aged 4 and older
  • age13p (GID 857) — users aged 13 and older
  • age16p (GID 858) — users aged 16 and older
  • age18p (GID 859) — users aged 18 and older

Introduced in PR #317 (March 26, 2026). That PR is flagged "AI-Assisted-by: gemini 2.5 pro".

How It Differs from Linux

The Linux age verification ecosystem is building a multi-layer stack: systemd stores the data, xdg-desktop-portal exposes the API, and desktop environments implement backends. The MidnightBSD approach is architecturally simpler and, by BSD standards, more conventional.

Linux (Proposed)

  • D-Bus IPC
  • systemd userdb storage
  • xdg-desktop-portal API
  • Desktop-specific backends
  • Flatpak permission model
  • JSON user records

MidnightBSD (Shipped)

  • Unix domain socket
  • SQLite database
  • C library in libutil
  • Unix group enforcement
  • rc.conf service management
  • Plaintext socket protocol

The practical consequence: on Linux, age reporting is still forming across multiple upstream projects and no distribution has shipped an integrated mechanism. On MidnightBSD, the entire stack — storage, API, enforcement — shipped in a single merge.

Socket Protocol

The daemon listens on /var/run/aged/aged.sock. The protocol is plaintext over a Unix domain socket. Behavior depends on the caller's privilege level.

Root (UID 0)

Root sends structured commands:

SET <uid> <type> <value>

Where type is either age or dob, and value is the integer age (2–125) or date of birth string.

Non-Root

Any data sent by a non-root client triggers a lookup of the caller's own UID. The daemon responds with the caller's age bracket. The content of the message is irrelevant — sending anything triggers the lookup.

Response Format

The daemon responds with a comma-delimited pair representing the lower and upper bounds of the age bracket:

Response Meaning
0,12 Under 13
13,15 13 – 15
16,17 16 – 17
18,-1 18+ (no upper bound)
-1,-1 Unknown (no age data provided)

Age Bracket Mapping

The four age brackets correspond to the categories required by AB 1043:

0,12

Under 13

Group: age4p

13,15

13 – 15

Group: age13p

16,17

16 – 17

Group: age16p

18,-1

18+

Group: age18p

Users with UIDs below 1000 (service accounts, root, daemon, etc.) default to the 18+ bracket if no age has been explicitly set. Age values must be between 2 and 125 inclusive.

The agev(3) Library API

Three functions in libutil, declared in <libutil.h>:

agev_get_age_bracket()

int agev_get_age_bracket(uid_t uid, int *lower, int *upper);

Connects to the aged socket, sends a lookup request for the given UID, and parses the comma-delimited response into lower and upper bounds.

agev_set_age()

int agev_set_age(uid_t uid, int age);

Sends SET <uid> age <age> to the daemon. Requires root.

agev_set_dob()

int agev_set_dob(uid_t uid, const char *dob);

Sends SET <uid> dob <dob> to the daemon. Requires root.

Because these functions are in libutil, any application compiled against MidnightBSD's base system can query age brackets with a single function call. No library to install, no service to discover, no IPC mechanism to negotiate. This is the simplest possible application-facing API.

Group-Based Enforcement

PR #317 added four Unix groups that the aged daemon manages based on the user's age bracket. The mport package manager checks group membership to gate package installation:

How It Works

When a user's age is set via agectl or the agev(3) library, the daemon calculates the appropriate bracket and adds the user to the corresponding groups. A 16-year-old would be added to age4p, age13p, and age16p but not age18p.

Packages in the mport repository can declare a minimum age group. A package marked as requiring age18p will not install for users who are not members of that group.

This is enforcement at the package manager level — not just data collection. The Linux ecosystem has not proposed anything equivalent. The D-Bus proposal and xdg-desktop-portal PR define query APIs; they do not restrict what software a user can install based on their reported age.

The Parsing Bug

Known Bug

agev_get_age_bracket() (introduced in PR #310) splits the daemon's response on '-' instead of ','. The daemon returns comma-delimited responses (e.g., "13,15"), but the library function calls strtok(response, "-").

This means 4 of the 5 possible response types are parsed incorrectly:

  • "0,12" — never split, parsed as a single token
  • "13,15" — never split, parsed as a single token
  • "16,17" — never split, parsed as a single token
  • "18,-1" — split on the - in -1, producing "18," and "1"
  • "-1,-1" — split on the first -, producing "" and "1," and "1"

The only response that would partially survive is "18,-1", and even that produces an incorrect upper bound of 1 instead of -1.

Self-Declaration Only

The aged daemon is purely self-declared. No identity verification, no document checks, no network calls. The user (or root on their behalf) states an age, and the system believes them.

The PR author acknowledged this directly:

"This will not comply with Brazil or the proposed law in New York. (they require ID checks)" — PR #302

This satisfies AB 1043's self-declaration model but is insufficient for Brazil Lei 15.211 (which bans self-declaration) and NY S8102A (which requires "commercially reasonable" verification).

Community Reception

PR #302 received two public comments, both negative:

  • "Orwellian"
  • "Lame"

Ageless Linux Position

Removal Available

MidnightBSD's aged daemon is the first age reporting implementation to ship. It is also the first for which Ageless Linux provides non-speculative removal instructions. The daemon can be disabled via rc.conf, the database deleted, and age group memberships removed.

Note: become-ageless.sh targets Debian-based Linux systems and does not support BSD. The MidnightBSD removal procedure is documented as manual steps.

Removal Guide Short-Circuit Guide

Source

PR #302 — aged daemon and agectl CLI (March 9, 2026)
PR #310 — agev(3) library (introduced parsing bug)
PR #317 — age groups (March 26, 2026; AI-assisted)
aged commits — full commit history