Agent eligibility check · The decision corpus

Architecture

docs/ARCHITECTURE.md · no id · revision ? · /

This is the development record, published as it was written. It is amended by revision, including where the work went wrong. Nothing here has been rewritten for the web.

Architecture

Decided 2026-09-19 by Rook, the project's coordinator, within the delegation in RECORD.md. The decisions were taken and written before Rook had a name — the naming convention for actors arrived the same day — so earlier revisions of this page credited them to a Perspicuity role word. Rook normalised that label at revision 5 of the project record; the facts are unchanged. Each decision names the alternatives that were considered, so a later reader can tell what was rejected and why.

The shape

                 Caddy  (already running on the host)
                   |
     +-------------+-------------------------------+
     |                                             |
  static files                                  /api/*
  public/, build/r/<id>/                            |
  (no application process)                    eligibility/server.py
                                                    |
                                              SQLite queue (WAL)
                                                    |
                                            eligibility/worker.py
                                                    |
                                   fetch -> checks -> report -> build/

Reading a report involves no application code at all. Caddy serves it from disk.

Decisions

D1. Python 3.11+, standard library only

The runtime has no third-party dependencies. This is the single most important decision here, and it is chosen for three reasons.

must be able to read the whole thing and re-run it. A dependency tree defeats that.

any machine with Python. No virtualenv, no install step, no lockfile.

supply-chain exposure on a service that fetches untrusted URLs.

Rejected: TypeScript/Bun (matches janus and balcony-solar in this workspace, but adds a runtime and a package graph to a tool whose value is being inspectable) and Django (matches find-my-next-bite, but the persistence here is one queue and one index — a framework and a migration system would be almost all overhead).

One honest exception. Gunicorn is the process manager in production, so the *deployment* has one third-party package even though the *application* has none. That distinction is enforced rather than asserted: scripts/check_stdlib_only.py parses every module in eligibility/, tests/ and scripts/ and fails on any import outside the standard library, and CI runs it. A zero-install deployment is also possible by running the standard-library server, at the cost of a less battle-tested front end.

D2. Static-first delivery

Caddy serves the landing page, the rubric and every finished report directly from disk. Only /api/* reaches the application. Reports are pre-rendered at audit time.

Rejected: render on request. It puts an application process in the read path of every report, which buys nothing — a report is a dated artefact and must not change when it is read, and pre-rendering makes that structural rather than promised.

D3. The audit is asynchronous

POST /api/audit enqueues a domain and returns an id immediately. The client polls GET /api/audit/<id>. A worker drains the queue and writes the report.

Fetching a stranger's website is slow, hangs, and is the obvious way to exhaust the service. Keeping it out of the request path is both a safety property and the reason the endpoint stays responsive. This also means a crash in the fetcher cannot take down submissions.

D4. The rubric is code, and the documentation is generated

eligibility/checks.py is the catalogue. make rubric regenerates docs/RUBRIC.md from it.

A published rubric that is maintained separately from the implementation will drift, and the drift will be invisible. Generating it makes "the rubric you read is the rubric that ran" a structural guarantee. Each check declares basis as evidence or judgment so a reader can see which statements are sourced and which are our opinion.

D5. One snapshot, pure checks

An audit performs exactly one bounded fetch pass — the page, robots.txt, sitemap.xml, llms.txt, and headers — into a Snapshot. Every check is a pure function of that snapshot.

This makes checks trivial to test without a network, makes an audit reproducible from a stored snapshot, and keeps all network risk inside one module.

D6. Safety is in the fetcher, and it is tested

eligibility/fetch.py is the only code that opens a socket, and it enforces:

reserved and cloud-metadata ranges — on the initial request and on every redirect;

Refusing on *resolved address* rather than on the hostname string is what makes this meaningful; the name-based version is trivially bypassed by a DNS record.

D7. SQLite with write-ahead logging

The queue, the report index and the rate-limit counters live in one SQLite database. This matches the store choice already made for find-my-next-bite and needs no server.

D8. No accounts, no cookies, no tracking

Cycle one has no sign-in, sets no cookie, and collects nothing about a visitor. A submitted domain is recorded because the report is about it; nothing identifies the submitter.

D9. Reports are private by default

A report lives at an unguessable id and is not listed, not indexed and not linked by us. The supplier publishes it if they choose. A report about someone's weaknesses is their information, and the default has to reflect that.

D10. Fix files are generated offline from the snapshot

eligibility/generate.py produces llms.txt, JSON-LD and robots directives from data already fetched. No generation step calls out to anything.

D11. Tests and CI use the standard library

unittest, run by scripts/ci.sh, which is also what GitHub Actions runs. One command locally and in CI, no test-framework dependency.

D12. Deployment mirrors the existing host pattern

/opt/agent-eligibility/releases/<revision> with a current symlink, a systemd unit and Gunicorn, matching find-my-next-bite. Templates live in ops/. Publication is David's release word; nothing here deploys itself.

What is deliberately absent

one of the things it checks for in others.

be able to change a pass/fail result.

How this record connects

It builds on or points to: RECORD.

It is referenced by: Agent instructions, Actors, Make the record conventions followable and enforce the actor rule.