Skip to content

docs(email-security): the read budget, and why free text needs a window [HOLD until the gateway ships to prod] - #389

Open
maximelb wants to merge 5 commits into
masterfrom
mailsec-read-governor-docs
Open

docs(email-security): the read budget, and why free text needs a window [HOLD until the gateway ships to prod]#389
maximelb wants to merge 5 commits into
masterfrom
mailsec-read-governor-docs

Conversation

@maximelb

@maximelb maximelb commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Documents the behaviour landing in lc_api-go#944 (security review L-12), board ticket "[Pre-GA] No read-cost governor on the mailsec read surface".

Two reads on the Email Security surface recompute rather than serving a cached or seekable answer, and both are now bounded per organization: GET /coverage over an explicit window, and GET /messages carrying q. Neither was documented as costing anything, and the filter table's "Free text over the message's identifying fields" gave no hint that q is matched row by row while every other filter in that table is a lookup.

  • api-reference.md — a ## Read budgets section: which two reads, the 2,400/hour figure, the 429 body, rate_bucket: mailsec_read as what distinguishes it from the raw-download refusal on the same surface, Retry-After, and the two cheapest ways to get a refused read served. The GET /coverage and GET /messages rows now point at it.
  • messages.md### Free text needs a window: what q actually costs, exactly what counts as a bound (since, the five lookup filters, a single verdict) and what does not (until alone, and the residuals), and the 512-character cap.
  • cli.mdcoverage with no window is memoized and is the right shape for a poller; --window-days recomputes and is budgeted.
  • automation.md — the same note where the docs already recommend scripted coverage polling.

The q examples are HTTP rather than CLI on purpose: q has no CLI flag today (a parity gap filed separately).

Anchors checked by hand: api-reference.md#read-budgets (new), #free-text-needs-a-window (new, depth 3 so within toc_depth), and #get-messagesmsg_uuideml, which detections.md already links to.

Public repo — not merging this myself. Ready for review whenever it suits.

Two reads recompute rather than serving a cached or seekable answer, and both
are now bounded per organization: coverage over an explicit window, and a
message search carrying q. Neither was documented as costing anything, and
'Free text over the message's identifying fields' gave no hint that q is
matched row by row while every other filter in that table is a lookup.

- api-reference: a Read budgets section with the refusal shape, what makes it
  distinguishable from the EML download's 429 on the same surface, and the two
  cheapest ways to get a refused read served.
- messages: 'Free text needs a window' — what q actually costs, exactly what
  counts as a bound and what does not (and why an until alone does not), and
  the 512-character cap.
- cli: coverage with no window is memoized and is the right shape for a poller;
  --window-days recomputes and is budgeted.
- automation: the same, where the docs recommend scripted coverage polling.

Examples for q are HTTP rather than CLI on purpose: q has no CLI flag today.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lcbill
lcbill previously approved these changes Sep 8, 2026
An independent review of the gateway PR caught that the 429's own advice —
'add a mailbox, sender, campaign or IOC filter' — did not work, because every
free-text search was counted. It is true now: a search riding one of those
filters is an index lookup and is exempt entirely, and the docs say so rather
than describing it as merely cheaper. A bare verdict is deliberately not an
exemption, and the reason (the verdict index is keyed verdict-then-time, so
benign is most of a tenant's mail) is stated where a reader would otherwise
assume symmetry with the other filters.

The number moved from 2400 to 7200 after the sizing was re-derived against the
console's real 300ms search debounce.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lcbill
lcbill previously approved these changes Sep 8, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lcbill
lcbill previously approved these changes Sep 8, 2026
@maximelb

maximelb commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

HOLD — merge with the gateway prod release.

This documents behaviour that does not exist on api.limacharlie.io yet. The rule it describes (a bare q is refused; the 512-character cap; the read budget) ships in lc_api-go#944, which is going to legion-exp only for now. The docs site publishes on merge, so merging this before the gateway reaches production would tell customers their working ?q=… calls are refused while they still succeed — and the 429 shape and rate_bucket would document a budget that is not being charged.

Reviewed and green (markdownlint, link check, rendered build). Please merge it in the same wave as the lc_api-go production release, not before.

@maximelb maximelb changed the title docs(email-security): the read budget, and why free text needs a window docs(email-security): the read budget, and why free text needs a window [HOLD until the gateway ships to prod] Sep 8, 2026
Independent review.

- The EML admonition was titled 'deliberately the only one that is [rate-limited]'
  and closed with 'No other Email Security route is rate-limited' — which this
  same PR makes untrue eighty lines below. It is now about the posture that IS
  unique to that route (failing closed), and it points at the read budget and
  says why that one fails OPEN instead.
- 'Every other filter in the table above is a lookup' was wrong and contradicted
  by this same section twenty lines later: state, direction, user_reported,
  min_score, sender_root_domain, a multi-valued verdict and since/until are all
  residuals. The six that really are lookups are named.
- messages.md offered a single verdict as a way to satisfy the requirement and
  then listed only the five index filters as exempt from the budget, leaving a
  reader to assume symmetry. It says now that a single verdict satisfies the
  rule and does not exempt the search, and why.
- api-reference said q is 'subject to the read budget' unconditionally; a q
  riding an index filter is not counted at all.
- 'a once-a-minute poll costs almost nothing' — the memo TTL is exactly 60
  seconds and it is per pod, so that poll lands on the expiry most times. The
  claim that survives is the one the paragraph is actually for: it is not
  budgeted. Not that it is free.
- 'roughly half' was 40%, and the three rate-limit headers are in SECONDS while
  the body's period is a duration string.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lcbill
lcbill previously approved these changes Sep 8, 2026
…d POST /hunts (#399)

The read-budget section documented in documentation#389 covers the two GET reads,
which are priced in database work. The rule backtest is priced in something else
entirely: it re-reads every stored message in its window — fetch, decrypt,
decompress, parse, evaluate — so it carries its own budget, its own rate_bucket
and its own refill window.

Says what a client actually needs: that mailsec_post_read is NOT mailsec_read
and must not be retried on the other's advice, that 6 backtests per 10 minutes
is the allowance across every credential in the organization, that it fails open
like its sibling, and that no narrower request shape takes a backtest out of the
budget the way a filtered search leaves the query budget.

Also states why POST /hunts is counted while it still refuses: budgeting it on
the day the engine lands would be a change of contract for clients that shipped
against it first.

Stacked on documentation#389 because it extends that PR's Read budgets section.
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.

2 participants