- Python 79.9%
- HTML 14.5%
- CSS 2.4%
- Shell 1.7%
- JavaScript 1.5%
additive thread in run_forever; own Database() on WAL; shared deps with live allowlist via provider; clean stop on shutdown; flag default OFF so default behavior is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| .claude/commands | ||
| config | ||
| deploy | ||
| docs | ||
| scripts | ||
| src/lynceus | ||
| systemd | ||
| .gitignore | ||
| BACKLOG.md | ||
| CHANGELOG.md | ||
| install.sh | ||
| LICENSE | ||
| Makefile | ||
| pyproject.toml | ||
| README.md | ||
| SECURITY.md | ||
| uninstall.sh | ||
LYNCEUS IS IN ACTIVE DEVELOPMENT AND IS NOT COMPLETE. EXPECT ISSUES
Lynceus - Warden
Personal-use RF security monitoring: passive WiFi/Bluetooth observation, watchlist matching, alerting.
Project assertions. Read these first.
- Passive only. Lynceus never transmits, probes, injects, or associates. It only reads what Kismet has already heard.
- Read-only UI by design. The web UI surfaces state and never mutates configuration. Read-only is a security boundary, not a missing feature.
- No telemetry. Lynceus does not phone home. The only outbound connection is to the operator-configured ntfy broker.
- Probe SSID capture is OFF by default. Probe SSIDs reveal device WiFi history. Operators opt in explicitly during
lynceus-setup.
Project status
Personal-use RF security monitoring tool. v0.9.1.
This is not a hardened public product. It is a personal project, feature-complete for v0.9.1; the 0.9.1 fixes are hardware-verified on-device. Use it on hardware you control, in a jurisdiction where passive RF observation is legal, and read the source before trusting it with anything.
The full default suite passes at v0.9.1, with a small set of POSIX-only install.sh, file-mode, and symlink tests skipping on non-POSIX hosts. A separate diagnostic suite (pytest -m diagnostic) is run pre-push for pre-flight observation and is excluded from the default suite.
What Lynceus does
Lynceus is a small daemon plus read-only web UI that watches the WiFi and Bluetooth airspace around the operator and flags hardware of interest. It polls a local Kismet instance for sightings, persists them to SQLite, and matches each sighting against a curated watchlist (MAC addresses, OUIs, MAC ranges, BLE service UUIDs). Matches generate alerts that surface in the web UI and as push notifications via ntfy.
The threat model is simple: detect surveillance-relevant devices in the operator's environment — license-plate readers, body-worn cameras, drones, gunshot-detection nodes, known-bad hacking-tool hardware, AirTag-class trackers, and other RF-emitting equipment that's worth knowing about when it shows up. Lynceus is not a network attack tool, not a tracking tool, and not a substitute for situational awareness.
Features
- Argus integration. Watchlist metadata schema migration, dual-artifact CSV import via
lynceus-import-argus, optional metadata extension tolynceus-seed-watchlistYAML, alert-to-watchlist linkage in the alerts table, and a/watchlistUI page that surfaces vendor/category/confidence. Ntfy notification bodies are enriched with vendor and confidence. Per-Argus-record dedup gating (peer-collide + within-import dup) makes a no-op re-import idempotent (1 mutating SQL statement down from 99 against the bundled snapshot); the three-tier highest-severity-wins tiebreak (severity > confidence > CSV order) prevents a low-severity peer from silently downgrading a high-severity sibling. - Tier 1 enrichment. Probe SSID capture (opt-in, off by default), BLE friendly-name capture (on by default — BLE names are publicly broadcast), and an expanded BLE service UUID enrichment dictionary.
- Operator triage workflow on
/alerts. Filter dropdowns for severity, time window (relative buckets plus an absolutesince/untilrange that now accepts sub-dayYYYY-MM-DDTHH:MM[:SS]datetime strings in addition to date-only), acknowledged state, search, rule_type,has_note, andhas_action(any alert covered by an active snooze / permanent allowlist / non-archived watchful tracking). Per-alert snooze with operator-pickable durations (1h / 24h / 7d / 30d / forever), per-rule_type snooze on/rulesalongside a per-rule_type fires breakdown that surfaces type-level totals + last-fired + snooze state above the per-rule_name list (lockstep window dropdown), single + bulk acknowledge, a minimum keyboard shortcut set (/focus search,n/ppaginate,?toggle a non-modal help overlay,Escclose help / reset filters), and streaming CSV export at/alerts.csvthat mirrors the filtered set with full Argus-metadata join +action_takencolumn.mac_rangeparity across the alert-detail "Allowlisted" badge and thehas_actionfilter. - Watchful snooze (recurrence-aware tracking). A third snooze surface for MACs that should not page on every sighting but should escalate if they keep showing up. A
/watchfulpage lists tracked entries with status / state / window / MAC filters, a recurrence-digest section grouping escalations by ISO week, and per-entry action buttons: dismiss (archive), promote (allowlist + archive atomically), reset (walk back from escalated), investigate (flag + note, keep counting), confirmed-safe (close as benign, no allowlist write). Triage entry-point is the per-row "Watch" button on/alerts. A syntheticwatchful_recurrencerule_type alert at ntfy priority 4 fires on the 4th sighting (1 initial + 3 counted recurrences on a ≥24h gap debounce); unactioned entries auto-archive after 90 days with no observations. - Watchlist exploration.
/watchlistlist page with search (q), filters (pattern_type,severity,device_category), and pagination over the full ~23.4k-row corpus. Per-row detail page at/watchlist/<id>cross-links to matched alerts and surfaces full Argus provenance (vendor, source URL, source excerpt, FCC ID, geographic scope, first-seen / last-verified timestamps). Streaming CSV export at/watchlist.csvcovers the filtered set with the full metadata-join column projection. - Migration rollback (operator-facing, opt-in destructive flow). Every shipped DB migration ships a paired
NNN_<name>_down.sql. Thelynceus-validate rollback --target-version Nsubcommand walks the chain in descending order with interactive confirmation (--yesfor scripted use), reversing pure-CREATE migrations cleanly, ADD-COLUMN migrations via portable table-rebuild, and CHECK-relaxation migrations with informative abort on now-disallowed rows. Migration 010 (normalize_watchlist_patterns) is irreversible by design and skipped with a logged WARNING. Back up your DB before invoking rollback. - Dark mode (theme toggle). Persistent operator preference via
localStorage. Theme bootstrap script in the<head>resolves the active theme before first paint so navigating between pages never flashes the wrong palette. Toggle lives in the topnav on every page. - Ergonomic CLI tooling.
lynceus-quickstart(foreground daemon + UI + browser launch for dev/demo),lynceus-setup(interactive wizard with Kismet/ntfy probes and optional Argus import),lynceus-bootstrap-kismet(configures Kismet capture interfaces and adds the operator to thekismetgroup;--installalso adds the Kismet apt repo and installs the package on Debian/Ubuntu/Kali — idempotent, the one CLI that's allowed to touch the network),lynceus-validate(config preflight + migration rollback subcommand),lynceus-export-config(single-file YAML+rules+allowlist bundle for backup / sharing / diffing). - Read-only
/settingspage. Surfaces current capture state with prominent privacy treatment (recording warning when probe-SSID capture is on, privacy-mode indicator when off), Kismet/ntfy connection status, watchlist origin breakdown (Argus / YAML / bundled), and system info (version, DB path, log path). Sensitive values (Kismet API token, ntfy topic) are redacted server-side. No mutation endpoints. - Release packaging.
install.shand a thinuninstall.shwrapper (Linux only;--user/--system/--uninstall/--purge/--dry-run), hardened systemd units (lynceus.service,lynceus-ui.service,lynceus-refresh.service+lynceus-refresh.timer) withNoNewPrivileges,ProtectSystem=strict, restricted namespaces, and friends. Canonical XDG-aware DB-path conventions across the codebase. End-to-end fresh-host deployment runbook at docs/DEPLOYMENT.md and Kali Linux smoke checklist at docs/KALI_SMOKE_CHECKLIST.md. - Bundled threat data. A curated default watchlist (~41,500 Argus rows, exported 2026-06-03) ships in the wheel as package data and is auto-imported on first
lynceus-setuprun. Coverage includes MAC, OUI, MAC ranges, BLE manufacturer IDs, BLE service UUIDs, drone Remote-ID prefixes, and SSID rows (both exact and case-insensitive-substring matches — the bundledargus_ssidrule fires on Flock cameras, Penguin trackers, and the FS Ext Battery family out of the box). Refresh from the latest published export withlynceus-import-argus --from-github.
Installation
Linux (primary supported target).
git clone https://github.com/kevwillow/lynceus-warden
cd lynceus-warden
./install.sh --user
For production on a dedicated host, install system-wide:
sudo ./install.sh --system
Lynceus uses a dedicated Python venv to comply with PEP 668 (the externally-managed-environment policy on Debian/Ubuntu/Kali). The
lynceus-*commands are exposed via symlinks to the venv binaries; you don't need to activate the venv manually.
install.sh --user creates the venv at ~/.local/share/lynceus/.venv and symlinks the console scripts into ~/.local/bin/. install.sh --system does the same at /opt/lynceus/.venv with symlinks under /usr/local/bin/. Operators don't need to manage the venv themselves; the symlinks make the lynceus-* commands appear on PATH transparently. install.sh never adds --break-system-packages — the venv is the whole point.
The system mode also creates a lynceus system user (which owns /opt/lynceus), lays down /etc/lynceus, /var/lib/lynceus, and /var/log/lynceus, copies the systemd units into /etc/systemd/system, and runs daemon-reload. The units are not auto-enabled — that's an explicit step after lynceus-setup --system.
Run ./install.sh --help for the full flag list (--user, --system, --uninstall, --purge, --dry-run). --dry-run works without root and prints the planned commands so operators can preview the install before committing to it.
To reverse an install, pass the matching scope to --uninstall:
./install.sh --uninstall --user # reverse a --user install
sudo ./install.sh --uninstall --system # reverse a --system install
--purge extends --uninstall to also delete config / data / state. With --user that's ~/.config/lynceus, ~/.local/share/lynceus, and ~/.local/state/lynceus (the latter two contain lynceus.db and logs). With --system that's /etc/lynceus and /var/lib/lynceus. Without --purge, the install artifacts (venv, symlinks, systemd units) are removed and operator data is preserved.
For convenience, ./uninstall.sh is a thin wrapper that auto-detects the install scope by looking for the venv marker (~/.local/share/lynceus/.venv for --user, /opt/lynceus/.venv for --system) and execs install.sh --uninstall with the right flag. Pass --user / --system explicitly if both are present, or to override the auto-detection. --purge and --dry-run pass through.
Do NOT pipe install.sh through curl | bash. Lynceus is a security tool. An install method that doesn't let you read the script before running it directly contradicts the project's threat model. If you want a one-liner, write your own — none is shipped.
macOS. pip install -e . from a clone (or, on PEP-668-managed installs, inside your own venv). The Python tools (lynceus, lynceus-ui, lynceus-setup, lynceus-quickstart, lynceus-seed-watchlist, lynceus-import-argus) all work. There is no systemd integration; use launchd if you need a service.
Windows. Same as macOS: pip install -e . from a clone. Treated as works for development; production deployment is not supported. No installer, no service automation, no documentation for running unattended.
Troubleshooting
install.shfails withpython3 -m venverrors. Install the venv module via your distro's package manager and re-run:- Debian/Ubuntu/Kali:
sudo apt install python3-venv - Fedora/RHEL:
sudo dnf install python3-virtualenv - Arch: ships with
python(no separate package needed).
- Debian/Ubuntu/Kali:
lynceus-*commands not found after install. Confirm the install's bin directory is on yourPATH:--userinstall:~/.local/binmust be onPATH.install.sh --userprints a one-liner hint when it isn't.--systeminstall:/usr/local/binis onPATHfor normal login shells; if it isn't, fix your shell profile rather than working around it.
Getting started
For a step-by-step end-to-end install from a fresh Kali / Debian / Ubuntu host through working capture + alerting + web UI, see docs/DEPLOYMENT.md. The runbook covers prerequisites, install, Kismet bootstrap, configuration, validation, systemd enablement, and post-install smoke verification, with a "common issues" section for the operator- facing failure modes that surface most often.
Quick start
- Install Lynceus.
./install.sh --userfrom a clone. - Install Kismet (if you don't already have it). By default
sudo lynceus-bootstrap-kismetassumes Kismet is already present and just configures it — detects Wi-Fi monitor-capable + Bluetooth interfaces, patches/etc/kismet/kismet_site.conf(append-only, preserves your edits), and adds you to thekismetgroup. On Debian/Ubuntu/Kali, add--installto also add the official Kismet apt repo and install thekismetpackage first. Idempotent; safe to re-run.install.shitself remains offline by design — this helper is the one CLI that handles the network-using install step. On other distros, install Kismet manually per https://www.kismetwireless.net/packages/, then run the baresudo lynceus-bootstrap-kismetto configure capture + group. Then log out and back in (so thekismetgroup takes effect), start Kismet (sudo systemctl start kismetor foregroundkismet), open http://localhost:2501, set the admin password, and create an API key underSettings → API Keys(namelynceus, rolereadonly). - Configure Lynceus. Run
lynceus-setup(orlynceus-setup --webfor the browser-based wizard — same questions, friendlier for headless / SSH-tunneled hosts and operators new to YAML). Either flow probes Kismet and ntfy, generateslynceus.yaml, auto-locates the API key from~/.kismet/session.db(no copy-paste needed in the common case), and prompts explicitly for probe SSID capture with a privacy explanation (off unless you opt in). It also offers to add a Bluetooth capture source (when anhci*adapter is detected). The web wizard's Argus step asks how you'd like to load the Argus watchlist with four choices — Skip (default; Lynceus runs without a watchlist), Use bundled snapshot, Fetch from GitHub, or Import from file. The interactive CLI flow auto-imports the bundled snapshot (its legacy default). Press Enter at the ntfy prompt to skip notifications entirely. To refresh the Argus watchlist later, runlynceus-import-argus --from-github(or--input <path-to-csv>for an air-gapped host). - Run. For dev/demo,
lynceus-quickstartlaunches the daemon, the UI, and a browser tab in the foreground; Ctrl+C shuts it down. For production,sudo systemctl enable --now lynceus.service lynceus-ui.service. - Verify. Open the UI, watch sightings populate, browse
/watchlistfor the bundled threat data, and visit/settingsto confirm capture state, Kismet/ntfy connectivity, and the watchlist origin breakdown.
Configuration
lynceus-setup is the primary configuration tool. Re-run it with --reconfigure to rewrite an existing config; without that flag it refuses to clobber what's already there.
For operators who prefer a browser-driven setup — typically a headless Raspberry Pi reached via SSH port-forward, a desktop install where copying values from a wiki into a terminal feels awkward, or anyone new to YAML config — pass --web instead. lynceus-setup --web prints a loopback URL with a single-use setup token, hosts a multi-page wizard that asks the same questions as the CLI flow, validates input through the same Config constructor the daemon loads, streams live per-step progress while the apply runs, and exits cleanly when you click Done. Loopback-bound by default on port 8766 (overridable via --port); pass --bind 0.0.0.0 to opt into remote access. Every other CLI flag (--user, --system, --reconfigure, --skip-probes, --output) works identically with --web. The CLI flow is unchanged when --web is absent.
Configuration files live at XDG-aware paths:
--userinstall:~/.config/lynceus/lynceus.yaml(or$XDG_CONFIG_HOME/lynceus/lynceus.yaml).--systeminstall:/etc/lynceus/lynceus.yaml.
Operator-local severity tuning lives alongside the main config in severity_overrides.yaml. Vendor overrides, device-category severity bumps, a confidence-downgrade threshold, and the Argus CSV # meta: schema_version accept-list (argus_schema_version_accept_list, default ["25", "26"]) all go there; the file is read by lynceus-import-argus --override-file so changes propagate at next import.
To check current configuration without editing files, navigate to /settings in the web UI. Capture state, watchlist data status, and system info are visible there. The page is read-only — to change settings, run lynceus-setup --reconfigure.
Bundled threat data
Lynceus ships a default watchlist as package data inside the wheel: src/lynceus/data/default_watchlist.csv.
-
Source. Snapshot from Argus, the companion RF-signature project. Lynceus is not redistributing the full Argus corpus — what's bundled is a point-in-time snapshot.
-
Coverage. ~41,500 records (exported 2026-06-03) across
mac,oui,mac_range,ble_manufacturer_id,ble_uuid,drone_remote_id_prefix,ssid,ssid_pattern, andble_local_nameidentifier types. Categories includedrone,alpr,gunshot_detect,hacking_tool, andunknown. Argus v1.4.1 extends thedevice_categoryenum withautomotive_telematics(forward-compat — zero active rows at v1.4.1; Parrot Automotive arm + v1.4.2 cellular-IoT backlog will populate later). The bundledargus_ssidrule fires on Flock-class cameras, Penguin trackers, and the FS Ext Battery family out of the box. -
First-run. The web wizard's Argus step asks how you'd like to load the bundled snapshot — Skip, Bundled, GitHub, or File. Pick Bundled (or import via
lynceus-import-argus --input <path>post-setup) to land the snapshot in your DB. The interactive CLI wizard (lynceus-setupwithout--web) auto-imports the bundled snapshot on first run. -
Refresh. When a newer Argus export is available, refresh in place. The single-command path pulls the latest tagged release straight from GitHub:
lynceus-import-argus --from-githubThis fetches
exports/argus_export.csvfrom the latest release ofkevwillow/argus-dband runs the existing import. The pulled artifact is preserved at<data-dir>/argus-cache/<ref>__argus_export.csvso each refresh leaves an audit trail.--dbdefaults to the canonical path for the active scope (~/.local/share/lynceus/lynceus.dbfor--scope user,/var/lib/lynceus/lynceus.dbfor--scope system); pass--dbto override. Pin a specific ref with--ref v1.2.3(tag, branch, or commit), and override the source repo with--repo OWNER/NAMEif you maintain a fork.Air-gapped operators still pass a local file:
lynceus-import-argus --input <path-to-fresh-argus-export.csv>--from-githuband--inputare mutually exclusive; pass exactly one. The importer is idempotent and metadata-aware in both modes — re-running against the same input is safe.lynceus-import-argusis the only Lynceus CLI that touches the network;install.shand the daemon stay offline. -
Auto-refresh (system installs, opt-in).
install.sh --systemshipslynceus-refresh.timer+lynceus-refresh.serviceto/etc/systemd/system/but does not enable them — the timer is the only Lynceus surface that opts the host into a recurring outbound call, so it stays an explicit operator decision (keeps theinstall.shoffline invariant intact). Once enabled, the timer re-runslynceus-import-argus --scope system --from-githubweekly, comfortably faster than the default 30-day staleness threshold so the/settings"watchlist stale" badge stays cold:# Enable (weekly, default off): sudo systemctl enable --now lynceus-refresh.timer # Customize cadence — opens a drop-in override: sudo systemctl edit lynceus-refresh.timer # Disable: sudo systemctl disable --now lynceus-refresh.timerFailures (network outage, transient GitHub error) are logged via
journalctl -u lynceus-refresh.service; the next scheduled fire retries. The oneshot does notRestart=— tight retry loops on a sustained outage are worse than a missed window.
Running Lynceus
Production (Linux + systemd). Two services:
lynceus.service— the poller daemon.lynceus-ui.service— the read-only web UI.
Both units are hardened (NoNewPrivileges=yes, ProtectSystem=strict, restricted namespaces, etc.) and run as the lynceus system user. Logs land under /var/log/lynceus/. Database under /var/lib/lynceus/.
Development / demo. lynceus-quickstart. Foreground process group, browser auto-launch, Ctrl+C to shut everything down cleanly. Suitable for hacking on Lynceus or showing somebody what it does. Not suitable for unattended operation.
CLI surface.
| Command | Purpose |
|---|---|
lynceus |
Poller daemon (entry point of lynceus.service). |
lynceus-ui |
Web UI (entry point of lynceus-ui.service). |
lynceus-quickstart |
Foreground dev/demo launcher (daemon + UI + browser). |
lynceus-setup |
Interactive configuration wizard. Add --web for a browser-driven flow on loopback port 8766 (single-use token; same prompts, validated through the same Config constructor). |
lynceus-seed-watchlist |
Add watchlist entries from a YAML file. |
lynceus-import-argus |
Import an Argus CSV export. --from-github pulls the latest release from kevwillow/argus-db; --input <path> reads a local file. --db defaults to the canonical scope path. |
lynceus-validate |
Config preflight + DB migration rollback. The legacy no-subcommand form runs the read-only config validator; lynceus-validate rollback --target-version N reverses applied migrations with an opt-in confirmation flow. |
lynceus-bootstrap-kismet |
Configures Kismet capture interfaces + kismet group; --install also adds the apt repo + package on Debian/Ubuntu/Kali. The one CLI that touches the network during install. |
lynceus-export-config |
Bundle current config + rules + allowlist into a single YAML for backup / sharing / diffing. |
For at-a-glance configuration and connectivity verification while running, navigate to /settings in the web UI. It surfaces capture state, Kismet/ntfy reachability, watchlist origin breakdown, and system info — read-only.
Privacy / threat model
- Read-only UI is a security boundary. All configuration mutations happen out-of-band —
lynceus-setup, or by editing the YAML directly. Visibility (the/settingspage) supports operator awareness; mutability would erode the boundary, so it isn't there. - Probe SSID capture is OFF by default. A device's probe SSIDs are the SSIDs it has previously associated with — effectively a partial WiFi history. Capturing them by default would turn Lynceus into a passive surveillance tool aimed at bystanders. The setup wizard prompts explicitly, and
/settingsmakes the current state prominent: a "recording" warning when capture is on, a "privacy mode" indicator when it is off. - BLE friendly-name capture is ON by default. BLE friendly names are broadcast publicly with intent; capturing them does not breach any reasonable privacy expectation.
- GPS in evidence rows is OFF by default and records the OPERATOR's location. When alert-time evidence capture is enabled, the GPS fix Kismet provides is the receiver's location at alert time, not the observed device's. Persisting it would build a high-resolution movement log of the operator. The
evidence_store_gpsconfig flag is opt-in (defaultfalse) and is independent ofevidence_capture_enabled. When enabled, GPS values are retained perevidence_retention_days(default 90) along with the rest of the snapshot. - Sensitive values are redacted server-side. The Kismet API token and ntfy topic never appear in
/settings-rendered HTML. Reduces shoulder-surfing risk and keeps secrets out of the response stream. - No outbound telemetry. Lynceus does not phone home, ship analytics, or report to any external service. The only outbound connection is to the operator-configured ntfy broker.
- Operator responsibility. Passive WiFi/Bluetooth observation is legal in most US jurisdictions but rules vary. It is the operator's job to verify what is allowed where they live. Lynceus is not, and will not become, an active-attack tool.
Architecture
Two-process production deployment, single SQLite database between them.
+----------------+ poll +----------+ write +-----------+
| Kismet API |<----------| poller |---------->| SQLite |
+----------------+ +----+-----+ +-----+-----+
| ^
| rules engine | read
v |
+---+----+ +---+-----+
| ntfy |<-- alerts | webui |
+--------+ rendered | (FastAPI|
| Jinja2)|
+---------+
- Poller (
lynceus) — polls the Kismet REST API on a configurable interval, runs the rules engine over each sighting, persists sightings + alerts. - Rules engine — watchlist match (mac / oui / mac_range / ble service uuid / ble manufacturer id / ssid exact / ssid substring / drone remote-id prefix), allowlist suppression, AirTag-class tracker recognition, first-sighting heuristics, watchful recurrence tracking, per-rule_type and per-alert snooze gates.
- Database — SQLite, with versioned migrations and a canonical XDG-aware path resolution.
- Web UI (
lynceus-ui) — FastAPI + Jinja2 templates, read-only. Served by uvicorn. - Notifier — ntfy push for matched alerts. Null/recording backends exist for testing.
License & credits
License: MIT. See LICENSE.
Built on Kismet for radio capture and ntfy for push delivery.
How I built this
Lynceus-Warden && Argus-db is the result of many long days and longer nights of iterative work across multiple machines — Windows dev boxes for some scraping and most of the dev work, Linux dev machines and a Linux server for the database, orchestration, and most agent work. The build process spans research, scraping, validation, schema design, license posture, discipline framework, building the web UI and the audit trail that backs every promotion. The substantive growth from a 514-row baseline to over 41,000 active identifiers happened across many weeks of work; the architectural framework that makes those promotions trustworthy took longer.
Operator-led orchestration
I plan and orchestrate this project myself, using Claude chat as a strategic-planning collaborator, paperclipai as the database company orchestration layer, and Claude Code as the execution agent across multiple specialist roles (extraction worker, source worker, validator, database architect, orchestrator). I have final decision authority on everything that lands in this repo. Strategic direction, architectural decisions, source-admission disputes, license posture, schema changes, and discipline-framework evolution are all operator-ratified before they commit.
The AI agents are highly capable executors with substantial scoping autonomy inside the constraints I set. They surface findings, propose decompositions, escalate when something needs ratification, and run extensive verification work I couldn't do at scale manually. But they don't decide canonical contract. I do.
This was not vibe-coded. Argus has 21 documented amendments to its canonical contract and 14 sub-agent rules governing how the build process itself operates. Every active identifier traces back to a verifiable public source via the audit trail. The discipline framework exists precisely because building a surveillance-equipment identification database is the kind of work where "looks roughly right" isn't good enough — provenance, confidence, and false-positive resistance all need to be load-bearing, not afterthoughts.
Notable technical work
Two areas surfaced data that wasn't otherwise aggregated anywhere queryable:
Vendor app decompilation. I downloaded Android APKs of setup and admin apps published by surveillance-equipment vendors (Flock Safety being one substantive example) and analyzed the binaries for embedded identifier patterns — BLE service UUIDs, MAC address prefixes, vendor-specific protocol fields, default device names. Vendor setup apps need to recognize and connect to their own equipment, so they ship with the identifiers needed to do that. Decompiling public app-store binaries surfaced this information directly. This is legal reverse-engineering of publicly-distributed software, but it required actually doing the work rather than waiting for vendors to publish identifier schemas (they don't).
GitHub researcher-repo aggregation. Surveillance equipment has been studied by independent researchers for years — drone RID protocol work (alphafox02/DragonSync), cellular intercept detection (EFForg/rayhunter), BLE stalking-tracker research (seemoo-lab/AirGuard), FAA Remote ID database mirrors (jlrjr's wrapper), and more. The data exists across these projects but had never been pulled into a single queryable database with provenance discipline. Argus aggregates it: every identifier traces back to the specific researcher repo, the specific commit, the specific file path, with proper attribution under the original licenses. This is meta-research synthesis rather than primary discovery, but it makes a large amount of distributed researcher work actually usable.
The discipline framework
The most substantive thing I built isn't the database. It's the framework that makes the database verifiable.
Every active identifier carries source attribution, confidence scoring, source-type classification, and a chain of corroboration. The framework includes hard rules that prevent fabrication (every identifier must trace to a concrete public source), PII discipline (individual-attributed registrations stay held, not promoted), and downstream-consumer protection (downstream scanners receive only high-confidence canonical data). The framework evolved with the work — each substantive amendment is documented with case studies showing what went wrong (or could have gone wrong) and why the rule exists.
Building this with AI tools is what made it possible at the scale and velocity it happened. Building it deliberately, with operator-final-say discipline and a binding correctness framework, is what makes the output trustworthy.
Support the Project
This project was built as a hobby by one person, a couple computers, and a couple of LLMs. It burned through quite a bit of token cost and mass amounts of personal time — but it was worth it. If Lynceus saves you some time or you just think it's cool, consider tossing a few sats my way. No pressure, but coffee and compute aren't free.
-
Star this repo — it's free and it helps others find the project
-
Submit an issue or PR — bug reports and feature ideas welcome
-
Crypto donations — if you're feeling generous:
- BTC —
bc1qmtzjlc2cw2y45nea2jqf4deh946j8mq502zvsw - BTC (Unstoppable Domain) —
gurutech.blockchain - LTC —
ltc1qf32n038a90ulajlq6zz67r3n2myewpjlj2ej6w - ETH —
0x9bf3311c4721fe37f58913dc57c2bf1722dc8a0f - BCH —
bitcoincash:qr2l294kuve9cw48u7xek9nklhed066ycvjtj4ymq9 - SOL —
CuraE8usMpSrAhpY2QiWaQGoBjyJzkSaUNP6kRgAzscU
- BTC —
-
Contact — kev@gurutechnology.services