Kidobo is a resolver-v3 Cargo workspace with one released package and three private implementation
crates. The root kidobo package remains the version and release source of truth; every internal crate
is fixed at 0.0.0 with publish = false.
Dependencies point inward:
kidobo (Clap, prompts, rendering, process setup)
├── kidobo-adapters (filesystem, HTTP, subprocesses, firewall, systemd)
└── kidobo-app (requests, outcomes, ports, use-case policy)
└── kidobo-core (pure deterministic domain computation)
kidobo-adapters
├── kidobo-app
└── kidobo-core
kidobo-core performs no filesystem, network, process, clock, or terminal I/O. It uses url::Url only
for deterministic configuration validation; HTTP clients and transport remain in kidobo-adapters.
kidobo-app owns command sequencing and failure policy. Its public-in-workspace interfaces are typed
requests, typed outcomes, focused capability ports, and internal source registries. It never depends on
adapters or Clap.
kidobo-adapters implements ports with bounded filesystem and HTTP reads, atomic writes, cache formats,
locks, bgpq4, ipset, iptables, and systemd. Adapter errors are mapped to application port errors at the
boundary.
The root package parses arguments, reads confirmations, renders human/TSV/JSON output, maps exit codes,
and composes production dependencies. CliIo supplies injected input, output, error, terminal, and color
capabilities for command-dispatch tests. Logger and SIGINT handler installation remain in the production
wrapper.
- Add request, outcome, and narrowly scoped ports to
kidobo-app. - Test orchestration with fake ports and an event ledger, including failure-stop behavior.
- Implement each side-effecting port in
kidobo-adaptersand test it with temporary paths or scripted executors. - Add only argument conversion, dependency composition, prompts, rendering, and exit mapping to the root package.
- Add binary acceptance coverage for compatibility-sensitive syntax or output.
Do not add a global runtime supertrait or universal virtual filesystem. A use case should receive only the capabilities it needs.
Sync and offline lookup use separate registries so lookup cannot acquire an HTTP client or command runner. Registration order is provider evaluation order, IDs must be unique, and application code applies required versus best-effort policy. Offline entries are sorted by source label and source line before lookup so output remains deterministic regardless of filesystem iteration order.
| Provider | Sync role and policy | Offline lookup |
|---|---|---|
| Local blocklist | Candidate, required | Local entries |
| Remote feeds | Candidate, best effort | Compatible cached entries only |
| Config safelist | Safelist, required | Included with valid config |
| GitHub metadata | External safelist, best effort with admission checks | Compatible cache only |
| ASN bans | Candidate, required with stale fallback | Configured ASN caches only |
To add a built-in source, implement the appropriate provider trait, assign a stable unique ID, register it in the relevant adapter registry builder, and add role, policy, ordering, fallback, and offline-boundary tests. Registries are internal Rust extension points, not plugin ABIs or user configuration.
The application sync use case explicitly preserves this order:
- Resolve paths and valid config, then acquire the nonblocking lock.
- Ensure sets and chains without activating new wiring.
- Load every registered source and apply provider failure policy.
- Admit externally controlled safelists without allowing them to empty a nonempty enabled-family baseline, then compute family-separated effective lists.
- Validate both enabled-family capacities before either swap.
- Promote every accepted staged cache manifest; any promotion failure aborts.
- Atomically replace IPv6, then IPv4.
- Activate and normalize fail-closed firewall wiring.
- Best-effort clean disabled-family artifacts.
The enforcement adapter retains the 31-character ipset name constraint, temporary restore-and-swap
replacement, per-set atomicity, and fail-closed chain and INPUT jump ordering. To normalize a family's
shared INPUT chain without numeric-position deletion, it temporarily creates kidobo-input-stage, points
that chain at kidobo-input, and activates the staging jump before replacing stable jumps by exact rule
specification. A successful activation leaves exactly one stable jump at position 1 and removes the staging
jump and chain. Cleanup handles both names so an interrupted prior activation cannot leave unmanaged
artifacts. Tests use recording ports and scripted command runners; development validation must never invoke
live firewall or systemd mutations.
The CLI signal handler supplies the application cancellation port. Workflows check it between operations, source workers stop taking new work and join active requests, and prompt input polls for cancellation. Sync checks cancellation before promotions and immediately before entering enforcement. The enforcement section has no cancellation checkpoints: family replacements, activation, and disabled-family cleanup retain their existing failure ordering. Full flush likewise completes every scoped cleanup attempt after starting. The CLI preserves operational diagnostics and returns 130 when SIGINT accompanies a failure. Subprocess supervision covers both child exit and pipe EOF under one deadline, including pipes inherited by descendants after their parent exits; output limits apply independently to stdout and stderr.
Validated remote-feed and GitHub metadata writes use a private generation adapter. A complete generation is
written and synced in a sibling staging directory and atomically published under its content SHA-256 ID.
The application selects it with an atomically replaced and synced current.json manifest only after all
source semantics and both enabled-family capacities pass, and before either set replacement. Remote feeds are stored below
v2/remote/<url-hash>/generations/<sha256>/; GitHub metadata is stored below
v2/github-meta/generations/<sha256>/. Each manifest names a current and optional previous generation.
Readers validate the manifest, generation identifiers, bounded members, configured URL, checksums, and GitHub category scope before accepting data. They try current, then previous, then the legacy flat-file layout. New writes use only v2, while legacy files remain readable and unchanged for compatibility. After a successful promotion, only the current and previous generations are retained; incomplete or unselected staging directories are never selected and are removed opportunistically by online cache work. Normal cache flush removes the containing remote-cache directory and therefore both layouts.
An unchanged current generation retains its previous manifest entry and skips redundant promotion. Existing same-ID content is verified before reuse. Corrupt or missing members are repaired from retained staging data only after admission, using atomic member replacement followed by whole-generation verification. Repair failure preserves the previous generation, and abandoned repairs remove only their staging directories. A cache staging failure uses a validated fallback with a warning; absence or rejection of that fallback is a hard error even for best-effort providers. Manifest promotion failures remain hard.
Remote feeds are parsed into an incrementally deduplicated set. For configured
maxelem = M, one feed accepts at most max(16384, min(2M, 1000000)) data
lines and max(4096, min(2M, 1000000)) unique canonical CIDRs. The combined
remote set accepts at most max(8192, min(4M, 2000000)) CIDRs. URLs are fetched
in deterministic chunks of at most five workers so memory retains one chunk
plus the bounded aggregate. Per-feed rejection uses a compatible selected
cache; aggregate rejection aborts before enforcement.
GitHub metadata is an externally controlled safelist. Fresh and cached batches
share the same 4,096-entry, IPv4 /8, IPv6 /16, and one-sixteenth-per-family
coverage envelope. The application separately rejects an otherwise valid batch
when it would empty an enabled family that remains nonempty after applying only
the operator's safe.ips baseline.
Use just check at each implementation checkpoint and just ci for final integration. The local CI recipe
checks formatting, lints, dependency policy, audits, and tests. just publish-release X.Y.Z prepares the
candidate in a temporary worktree, runs that gate, repeats the release-note check, then performs the
release-only coverage, rustdoc, isolated binary-exercise, and static compatibility gates. The exercises use a temporary runtime
root, loopback feed, fake privileged commands, and Debian 11/Alpine 3.22 containers. The publisher packages the tested static Linux x86_64 binary
and uses GitHub CLI to upload, verify, and publish the release. Run just release-notes-check after every
repository change.