Skip to content

Add regulatory region to aged(8) and agectl(8)#327

Merged
laffer1 merged 5 commits intomasterfrom
feature/aged_region
Apr 4, 2026
Merged

Add regulatory region to aged(8) and agectl(8)#327
laffer1 merged 5 commits intomasterfrom
feature/aged_region

Conversation

@laffer1
Copy link
Copy Markdown
Member

@laffer1 laffer1 commented Apr 4, 2026

Allows users to set the region they're in. This changes the aged(8) behavior as follows:

Returns -2,-2 if the user has not defined the region or opted out.
In US-CA, it would return -1, -1 since they are in a region requiring it.

Also added all the places I think have laws in place or are in the process of doing so. We don't support all of them and won't be able to in some cases. We may need to handle this in the future so the daemon will have a mechanism to do so.

Scenarios:
It's likely worse to 'lie' about doing verification in regions that require more than attesting. So we default to -2,-2 in those.

EU wants to make everyone there have an app on their phone that must be queried to check their age.
UK just said that attestation is no longer valid this year. So the aged(8) isn't compliant there anyway. Use -2,-2
BR isn't fully compliant but I think it's required to "try" to do something also. keep daemon on

AI-Assisted-by: Gemini 2.5 pro

Summary by Sourcery

Add configurable regulatory regions to the age verification daemon and CLI, gating responses based on region and documenting the new behavior.

New Features:

  • Allow root to configure a global regulatory region for age verification via the aged settings table and the agectl -r option.
  • Introduce a special -2,-2 response code when age verification is not permitted in the configured region.

Enhancements:

  • Extend aged to persist a region setting in a new settings table and apply region-based policy when serving age queries.

CI:

  • Adjust the cross-bootstrap-tools GitHub Actions workflow to build only the amd64 target on ubuntu-24.04 and macos-latest.

Documentation:

  • Document the new agectl -r flag, valid region values, and the -2,-2 response semantics in the agectl(8) and aged(8) man pages.

laffer1 added 3 commits April 4, 2026 12:19
AI-Assisted-by: gemini pro
Signed-off-by: Lucas Holt <luke@foolishgames.com>
AI-Assisted-by: Gemini 2.5 pro
Signed-off-by: Lucas Holt <luke@foolishgames.com>
downloads anyway)

switch to strncmp

Signed-off-by: Lucas Holt <luke@foolishgames.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 4, 2026

Reviewer's Guide

Introduce a configurable regulatory region for the aged(8) daemon, expose a root-only region-setting interface in agectl(8), gate age lookups based on the configured region (returning -2,-2 when verification is not allowed or not configured), and document the new behavior; also simplify the cross-bootstrap-tools CI matrix.

Sequence diagram for agectl region setting and aged lookup behavior

sequenceDiagram
    actor Admin
    participant agectl
    participant aged_daemon
    participant sqlite_db

    Admin->>agectl: invoke with -r region
    agectl->>aged_daemon: connect UNIX socket
    agectl->>aged_daemon: send REG region

    aged_daemon->>sqlite_db: open aged.db
    aged_daemon->>sqlite_db: UPDATE settings SET value = region WHERE key = 'region'
    sqlite_db-->>aged_daemon: update result
    aged_daemon-->>agectl: OK
    agectl-->>Admin: display success

    Admin->>User: system configured for region

    actor User
    User->>agectl: invoke without args
    agectl->>aged_daemon: connect UNIX socket
    agectl->>aged_daemon: send GET

    aged_daemon->>sqlite_db: open aged.db
    aged_daemon->>aged_daemon: load_region_setting
    aged_daemon->>sqlite_db: SELECT value FROM settings WHERE key = 'region'
    sqlite_db-->>aged_daemon: region value
    aged_daemon->>aged_daemon: decide region_allowed

    alt region_allowed == 0 or region unset
        aged_daemon-->>agectl: -2,-2
    else region_allowed == 1
        aged_daemon->>sqlite_db: SELECT age FROM users WHERE uid = client_uid
        sqlite_db-->>aged_daemon: age or no row
        aged_daemon->>aged_daemon: derive age_range
        aged_daemon-->>agectl: age_range
    end

    agectl-->>User: print response
Loading

ER diagram for aged SQLite users and settings tables

erDiagram
    USERS {
        INTEGER uid PK
        INTEGER age
        TEXT dob
    }

    SETTINGS {
        TEXT key PK
        TEXT value
    }

    USERS ||..|| SETTINGS : uses_global_configuration
Loading

Flow diagram for aged GET handling with regulatory region gating

flowchart TD
    A["Client sends GET to aged"] --> B["Open SQLite aged.db"]
    B --> C["load_region_setting from settings table"]
    C --> D{current_region is non NULL and in allowed set?
US-CA, US-CO, US-IL,
BR, parental}
    D -- No --> E["Return -2,-2 to client"]
    D -- Yes --> F["Prepare SELECT age FROM users WHERE uid = client_uid"]
    F --> G{Row found?}
    G -- No --> H["age = -1"]
    G -- Yes --> I["age = stored value"]
    H --> J["If uid < 1000 and age == -1 then age = 18"]
    I --> J
    J --> K["Map age to range string via get_range"]
    K --> L["Return range string to client"]
Loading

File-Level Changes

Change Details Files
Add regulatory region storage and loading to aged(8) using a new settings table and per-request caching.
  • Define MAX_REGION_LEN and a global current_region pointer in the daemon.
  • Extend init_db() to create a settings table and insert an initial 'region' key with NULL value if missing.
  • Implement load_region_setting() to read the 'region' setting from SQLite into current_region on each client request, freeing previous value first.
usr.sbin/aged/aged.c
Add validation and persistence logic for regulatory region updates via a new REG command in aged(8).
  • Implement is_valid_region() with a fixed allow-list of region codes (US-* states, BR, DE, EU, UK, AU, JP, null, parental).
  • Handle a new root-only "REG " command in the main request loop, validating the region and updating the settings table, treating literal "null" as SQL NULL.
  • Log success or failure via syslog and respond with OK/ERR over the socket.
usr.sbin/aged/aged.c
Gate age query responses in aged(8) by regulatory region and introduce the -2,-2 response code when verification is not permitted.
  • Load the current region before handling each client request.
  • Determine if the configured region allows age verification (US-CA, US-CO, US-IL, BR, parental) and, if not allowed or unset, short-circuit the request with a "-2,-2" response.
  • Preserve existing age lookup logic (including defaulting system accounts to age 18) when the region is allowed.
usr.sbin/aged/aged.c
Expose a root-only CLI for setting the regulatory region in agectl(8).
  • Extend usage() text to describe the new -r option.
  • Add -r to getopt processing, introducing mode=3 for region updates that do not require a username argument.
  • When mode==3, send "REG " to the aged(8) socket instead of GET/SET commands; keep existing behavior for age/DOB operations.
usr.sbin/agectl/agectl.c
Document regulatory region support, valid region values, and the -2,-2 response semantics in the man pages.
  • Update agectl(8) to describe the -r region option, list valid region codes, add usage examples, and document -2,-2 as "Age verification is not permitted in the configured region."
  • Update aged(8) to describe the REG protocol, enumerate valid regions, and specify which regions allow GET operations to return an age range vs -2,-2.
usr.sbin/agectl/agectl.8
usr.sbin/aged/aged.8
Narrow the cross-bootstrap-tools CI matrix to fewer OS/architecture combinations.
  • Limit target_arch to amd64 only and reduce OS matrix to ubuntu-24.04 and macos-latest in cross-bootstrap-tools.yml.
.github/workflows/cross-bootstrap-tools.yml

Possibly linked issues

  • #0: The PR implements region-based behavior and -2,-2 outputs in non-required jurisdictions, matching the issue’s proposal.
  • #0: The PR adds regional configuration and behavior (including DE/EU) to aged(8), directly reflecting the issue’s Germany/EU law concerns.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@laffer1
Copy link
Copy Markdown
Member Author

laffer1 commented Apr 4, 2026

Relates to #318 request to have a -2,-2 code for folks who don't set it.

actions

Signed-off-by: Lucas Holt <luke@foolishgames.com>
Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The list of valid regions and which ones allow GET responses diverges between the code and the man pages (e.g., code checks "parental" and not "UK" in the allow-list, while the docs mention "UK" and "other"), so it would be good to make the documentation and the is_valid_region/region_allowed logic consistent.
  • is_valid_region() uses strncmp(region, valid_regions[i], MAX_REGION_LEN), which will treat any string with a valid prefix (e.g., US-CA-foo) as valid; consider using strcmp or checking length to require an exact match.
  • In the REG handler you call sqlite3_prepare_v2 without checking its return code before binding and stepping, unlike in load_region_setting; it would be safer to bail out on non-SQLITE_OK and avoid using an unprepared statement.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The list of valid regions and which ones allow GET responses diverges between the code and the man pages (e.g., code checks "parental" and not "UK" in the allow-list, while the docs mention "UK" and "other"), so it would be good to make the documentation and the `is_valid_region`/`region_allowed` logic consistent.
- `is_valid_region()` uses `strncmp(region, valid_regions[i], MAX_REGION_LEN)`, which will treat any string with a valid prefix (e.g., `US-CA-foo`) as valid; consider using `strcmp` or checking length to require an exact match.
- In the `REG` handler you call `sqlite3_prepare_v2` without checking its return code before binding and stepping, unlike in `load_region_setting`; it would be safer to bail out on non-`SQLITE_OK` and avoid using an unprepared statement.

## Individual Comments

### Comment 1
<location path="usr.sbin/aged/aged.c" line_range="226-233" />
<code_context>
 					syslog(LOG_INFO, "User information updated for uid %d", target_uid);
 					write(client_fd, "OK\n", 3);
 				}
+			} else if (client_uid == 0 && strncmp(buf, "REG ", 4) == 0) {
+				char *region = buf + 4;
+				char *newline = strchr(region, '\n');
+				if (newline) *newline = '\0';
+
+				if (is_valid_region(region)) {
+					sqlite3_stmt *stmt;
+					sqlite3_prepare_v2(db, "UPDATE settings SET value = ? WHERE key = 'region';", -1, &stmt, 0);
+					if (strcmp(region, "null") == 0) {
+						sqlite3_bind_null(stmt, 1);
</code_context>
<issue_to_address>
**issue (bug_risk):** Handle sqlite3_prepare_v2() failures before using stmt in the REG handler.

In this REG handler, the result of sqlite3_prepare_v2() is ignored. If prepare fails, stmt is invalid and subsequent sqlite3_bind_*/sqlite3_step/sqlite3_finalize calls invoke undefined behavior. Please capture and check rc, log on error, and return an error to the client without using stmt when rc != SQLITE_OK, following the pattern used in load_region_setting().
</issue_to_address>

### Comment 2
<location path="usr.sbin/aged/aged.c" line_range="314" />
<code_context>
 	return age;
 }

+static int
+is_valid_region(const char *region) {
+	/* places that require age verification or will be. Not all can be supported */
</code_context>
<issue_to_address>
**issue (bug_risk):** Align valid region values and allowed-region behavior with the documented interface.

The current sets are inconsistent: `valid_regions` has "null" and "parental" but not "other" (manpage says "null" and "other" only), `is_valid_region()` accepts many regions (US-AL, EU, UK, etc.), while the GET path only treats US-CA, US-CO, US-IL, BR, and "parental" as enabling age verification. Aged(8) says UK, BR, US-CA, US-CO, and US-IL allow GET and anything else returns "-2,-2". Please reconcile code and docs on: (1) the canonical list of region strings; (2) which ones actually enable age verification/GET; and (3) the intended meaning and support of "other" vs "parental".
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +226 to +233
} else if (client_uid == 0 && strncmp(buf, "REG ", 4) == 0) {
char *region = buf + 4;
char *newline = strchr(region, '\n');
if (newline) *newline = '\0';

if (is_valid_region(region)) {
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, "UPDATE settings SET value = ? WHERE key = 'region';", -1, &stmt, 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Handle sqlite3_prepare_v2() failures before using stmt in the REG handler.

In this REG handler, the result of sqlite3_prepare_v2() is ignored. If prepare fails, stmt is invalid and subsequent sqlite3_bind_*/sqlite3_step/sqlite3_finalize calls invoke undefined behavior. Please capture and check rc, log on error, and return an error to the client without using stmt when rc != SQLITE_OK, following the pattern used in load_region_setting().

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The set of valid region strings and the regions that permit age responses are inconsistent between code and manpages (e.g., code checks for "parental" and not "other" or "UK", while the docs list "other" and say UK should get ranges); it would be good to align the constants and behavior in is_valid_region, the region_allowed logic, and the documented values.
  • Using strncmp(..., MAX_REGION_LEN) for region comparisons can cause unintended matches for prefixes (e.g., US-CA-foo matching US-CA); consider switching to exact strcmp for both is_valid_region and the region_allowed checks so only precise region codes are accepted.
  • The daemon calls load_region_setting(db) for every client request even though the region is global state and only changes on REG updates; you could simplify and reduce database load by loading the region once at startup and refreshing it only when a REG command succeeds.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The set of valid region strings and the regions that permit age responses are inconsistent between code and manpages (e.g., code checks for "parental" and not "other" or "UK", while the docs list "other" and say UK should get ranges); it would be good to align the constants and behavior in `is_valid_region`, the `region_allowed` logic, and the documented values.
- Using `strncmp(..., MAX_REGION_LEN)` for region comparisons can cause unintended matches for prefixes (e.g., `US-CA-foo` matching `US-CA`); consider switching to exact `strcmp` for both `is_valid_region` and the `region_allowed` checks so only precise region codes are accepted.
- The daemon calls `load_region_setting(db)` for every client request even though the region is global state and only changes on `REG` updates; you could simplify and reduce database load by loading the region once at startup and refreshing it only when a `REG` command succeeds.

## Individual Comments

### Comment 1
<location path="usr.sbin/aged/aged.c" line_range="47-56" />
<code_context>
 #define SOCKET_PATH "/var/run/aged/aged.sock"
 #define DB_PATH "/var/db/aged/aged.db"
 #define RUN_USER "aged"
+#define MAX_REGION_LEN 10

 static int calculate_age(const char *);
</code_context>
<issue_to_address>
**issue (bug_risk):** Region comparison using strncmp/MAX_REGION_LEN can accept unintended prefixes.

Using `strncmp(region, valid_regions[i], MAX_REGION_LEN)` will accept any string that merely starts with a valid region (e.g., `"US-CA-TEST"`, `"BRASIL"`), which can misclassify inputs. Use an exact match instead (e.g., `strcmp(region, valid_regions[i]) == 0`), optionally preceded by a `strlen(region) > MAX_REGION_LEN` check if you need to enforce a maximum length.
</issue_to_address>

### Comment 2
<location path="usr.sbin/aged/aged.c" line_range="231-233" />
<code_context>
+				char *newline = strchr(region, '\n');
+				if (newline) *newline = '\0';
+
+				if (is_valid_region(region)) {
+					sqlite3_stmt *stmt;
+					sqlite3_prepare_v2(db, "UPDATE settings SET value = ? WHERE key = 'region';", -1, &stmt, 0);
+					if (strcmp(region, "null") == 0) {
+						sqlite3_bind_null(stmt, 1);
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Lack of error checking for sqlite3_prepare_v2/sqlite3_step in the `REG` handler may hide failures.

In this path the return values of `sqlite3_prepare_v2` and `sqlite3_step` are ignored, so failures (locked DB, bad schema, etc.) will still result in logging a successful update and returning `"OK\n"` even when nothing was written. Please check these return codes and handle errors (log via `syslog` and return an error to the client) instead of assuming success.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +47 to 56
#define MAX_REGION_LEN 10

static int calculate_age(const char *);
static void get_range(int, char *);
static void init_db(void);
static int is_valid_region(const char *);
static void load_region_setting(sqlite3 *);

static char *current_region = NULL;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Region comparison using strncmp/MAX_REGION_LEN can accept unintended prefixes.

Using strncmp(region, valid_regions[i], MAX_REGION_LEN) will accept any string that merely starts with a valid region (e.g., "US-CA-TEST", "BRASIL"), which can misclassify inputs. Use an exact match instead (e.g., strcmp(region, valid_regions[i]) == 0), optionally preceded by a strlen(region) > MAX_REGION_LEN check if you need to enforce a maximum length.

Comment on lines +231 to +233
if (is_valid_region(region)) {
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, "UPDATE settings SET value = ? WHERE key = 'region';", -1, &stmt, 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Lack of error checking for sqlite3_prepare_v2/sqlite3_step in the REG handler may hide failures.

In this path the return values of sqlite3_prepare_v2 and sqlite3_step are ignored, so failures (locked DB, bad schema, etc.) will still result in logging a successful update and returning "OK\n" even when nothing was written. Please check these return codes and handle errors (log via syslog and return an error to the client) instead of assuming success.

in a region allowing that.

Update documentation to be more accurate.

Signed-off-by: Lucas Holt <luke@foolishgames.com>
@laffer1 laffer1 merged commit fd00bb7 into master Apr 4, 2026
6 of 7 checks passed
@laffer1 laffer1 deleted the feature/aged_region branch April 4, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant