Expand description
Looks up one named record in a repository this deployment does not hold, and caches the answer.
The policy gate decides a write from the subject alone: the account, its
kind, handle and depth, the client, the collection, the action and the
diff. Three rules the floor wants — an author’s app.bsky.feed.threadgate
and app.bsky.feed.postgate, an app.bsky.graph.block, and a declared
community.lexicon.preference.ai — each need one more fact, and it is the
same fact for all three: the value of a public record in somebody else’s
repository. This crate is that capability, built once, for the evaluators
that will ask for it.
§Why its own crate
It performs network I/O, and the two crates that would otherwise host it
cannot. didbot-policy is the policy core: no I/O, no async, no clock,
and Evaluator::evaluate takes a subject and nothing else precisely so
a predicate cannot reach the network from inside a decision.
didbot-pds holds no HTTP client and no async runtime either — the
operator poll’s fetch lives in didbot-serve for exactly that reason.
Putting the reader in either would give both a reqwest dependency they
are deliberately without. So it is a crate of its own, depending on
didbot-http and didbot-identity and on neither policy crate, which
leaves an evaluator free to depend on it without didbot-serve coming
along.
§How an evaluator reaches it
Two phases, because the decision channel is synchronous and the fetch is
not. didbot-pds’s write queue calls the gate inside a synchronous
critical section, so nothing under it can await.
- Prefetch. The write path, which has a runtime, calls
ForeignRecords::prefetchwith the records this write needs. It resolves and fetches what the cache does not already hold, bounded byLimits::record_deadlineper record andLimits::prefetch_deadlinefor the call. - Read. An evaluator holding an
Arc<ForeignRecords>from its own construction callsForeignRecords::read, which takes a lock, reads the cache and returns. It performs no I/O and does not await, so it is callable from inside a decision.
That split is what keeps the regex and Cedar engines untouched: they hold no reader, ask for no records, and their evaluation is unchanged.
§What an unknown means
Answer has three variants and the third one matters most. Present
and Absent are both things the repository said. Answer::Unknown is
everything else — not fetched yet, past the deadline, refused, a DID that
does not resolve, a server that did not answer — and this reader never
turns one into a refusal or into a permit. The evaluator that asked
decides what an unknown means for its own rule, because only it knows
whether the conservative reading of its rule is to deny or to stand
aside.
That leaves Staleness and EvalError to the evaluator too. A reader
whose answers may be up to Limits::present_ttl old is a BestEffort
view, and an evaluator built on it declares a max_lag it can live with;
this crate reports no lag of its own and raises no EvalError::Stale,
because a cache entry inside its TTL is not a stale view — it is the
view, and the TTL is the propagation delay plan/ai-preference.md asks
to have written down rather than discovered.
§Memory and network, declared
- At most
Limits::entriesanswers are held, each at mostMAX_RECORD_BYTESof JSON, so the cache costs at most those multiplied together. At the defaults that is 1024 records of 64 KiB, 64 MiB in the worst case and around a megabyte for records of the size a gate or a preference actually is. - One reference costs at most two requests: the DID document, then
com.atproto.repo.getRecordfor one record, orcom.atproto.repo.listRecordsfor a whole collection. Both read a bounded body, and a collection read takes at mostMAX_COLLECTION_RECORDSrecords inside that same bound. - One prefetch fetches at most
Limits::per_writerecords, each bounded byLimits::record_deadline, and the whole call byLimits::prefetch_deadline. - The cache is memory only. Nothing here writes to disk, and a restart begins with an empty cache — which costs one fetch per record and can never resurrect an answer somebody has since changed.
Modules§
- fetch 🔒
- The one round trip behind a cache miss, and what this reader will not make it for.
- record 🔒
- How one record in somebody else’s repository is addressed.
Structs§
- Counts
- What this reader has done since the process started, for the operator watching the numbers move.
- Entry 🔒
- One cached answer and when it stops being usable.
- Foreign
Records - The reader: a bounded cache of foreign records, over one
Fetch. - Limits
- What a
ForeignRecordsis bounded by. Every field is a parameter a deployment sets on its own, with its default and its reasonable range on the constant it comes from. - Network
- The real fetch: resolve the DID to its
#atproto_pdsendpoint, thencom.atproto.repo.getRecordthere. - NoLocal
Repositories - A deployment that holds nothing, for a caller with no registry to ask — a test, or a reader pointed only at the public network.
- Record
Ref - What a reader addresses: a DID, a collection, and either one record key or the whole collection.
Enums§
- Answer
- What this reader knows about one reference.
- Fetched
- What one round trip established.
- RefError
- Why a string is not a record this reader can address.
- Unknown
- Why a record’s value is not known.
Constants§
- DEFAULT_
ABSENT_ TTL - How long “that repository holds nothing at that key” is reused, absent configuration.
- DEFAULT_
ENTRIES - How many answers are held at once, absent configuration.
- DEFAULT_
PER_ WRITE - How many records one write may need, absent configuration.
- DEFAULT_
PREFETCH_ DEADLINE - How long one prefetch may take in total, absent configuration.
- DEFAULT_
PRESENT_ TTL - How long a record this reader read is reused, absent configuration.
- DEFAULT_
RECORD_ DEADLINE - How long one record’s fetch may take, absent configuration.
- DEFAULT_
UNKNOWN_ TTL - How long an unknown is reused, absent configuration.
- MAX_
COLLECTION_ RECORDS - The most records one collection read takes from a repository.
- MAX_
NAMED_ ACCOUNTS - The most accounts one write may name while a policy that reads something about each of them applies to it.
- MAX_
RECORD_ BYTES - The most of one record’s JSON this reader holds in memory.
Traits§
- Fetch
- One round trip to a repository this deployment does not hold.
- Local
Repositories - What this deployment already holds, so the reader never fetches it over the network.
Functions§
- outcome_
of 🔒 - The word a fetch’s outcome is logged under.
Type Aliases§
- Fetch
Future - A future a
Fetchimplementation hands back, boxed so the trait stays object-safe and a reader can be held asArc<dyn Fetch>.