Skip to content

abuse-enforcement-service: fail closed when the allowlist store errors - #7

Merged
Pitchfork-and-Torch merged 1 commit into
mainfrom
cursor/allowlist-fail-closed-96f3
Sep 4, 2026
Merged

Pitchfork-and-Torch merged 1 commit into
mainfrom
cursor/allowlist-fail-closed-96f3

Conversation

@Pitchfork-and-Torch

Copy link
Copy Markdown
Owner

Problem

In abuse-enforcement-service, the allowlist is the exemption check that runs before any enforcement rule can fire. A Manhattan GET failure during that check was swallowed inside ManhattanAllowlist::get_entity and returned as None. fetch_user_allowlist / fetch_entity_allowlist read None as is_allowlisted: false, so run_enforcement_inner continued into the rule pipeline for an account or post that may have been exempt.

The neighbouring Gizmoduck and credibility fetches on the same path behave differently: they abort with ? and the score is retried. Only the exemption lookup failed toward enforcement.

Change

Make the allowlist lookup behave like the other critical fetches.

  • ManhattanAllowlist::get / get_entity return anyhow::Result<Option<_>>. Ok(None) means the store confirmed the key is absent. A GET error, or a stored entry that cannot be decoded, is returned as Err instead of None.
  • fetch_user_allowlist / fetch_entity_allowlist return Result and propagate the error. Only a confirmed absence maps to "not allowlisted". Shared conversion lives in a small allowlist_facts helper.
  • run_enforcement_inner uses ? on the allowlist lookups. A store error now aborts the attempt and the score lands in the existing retry queue (exponential backoff, MAX_RETRIES, then dropped without enforcing) instead of proceeding to rules.
  • Admin handlers: GET /allowlist/{id} and GET /allowlist/{type}/{id} return 500 on a read error instead of 404 (OpenAPI responses updated). Bulk upsert reports a failed pre-read as a per-row error rather than guessing add. The DELETE audit before_json snapshot stays best-effort and never blocks the delete.

No change to behaviour when the store is healthy. No rule, config, or action changes.

Before / after

Scenario Before After
Allowlisted, store healthy skip (user_in_allowlist) skip (user_in_allowlist)
Not allowlisted, store healthy proceed to rules proceed to rules
Store GET fails proceed to rules (could reach act_suspend) Err → retry queue; never enforces on this attempt
Stored entry undecodable proceed to rules Err → retry queue

Tests

  • Added unit tests in strato.rs for the lookup-to-facts conversion, including one asserting that a store error is not turned into is_allowlisted=false.
  • This service depends on internal xai_* crates that are not in the repo, so it cannot be built here. The changed allowlist.rs / strato.rs code was compiled verbatim in a scratch crate against stubbed store/facts types (same tracing, anyhow, serde_json usage) and driven through a scripted store: 9/9 tests pass and the before/after table above is its output. Only Manhattan GET failed increments MANHATTAN_ERRORS_TOTAL; the decode-failure case does not.
  • allowlist.rs and strato.rs are rustfmt-clean. Remaining rustfmt diffs in lib.rs / service.rs are pre-existing on main and untouched.

Upstream

The touched regions are identical on xai-org/x-algorithm main and this commit cherry-picks onto it cleanly. A full sync of this fork's main with upstream is not part of this PR: it conflicts in four files owned by the fork's own commits (README.md, home-mixer/candidate_hydrators/vf_candidate_hydrator.rs, home-mixer/params/param.rs, home-mixer/scorers/ranking_scorer.rs).

Open in Web Open in Cursor 

The allowlist is an exemption check that runs before any rule can fire.
Until now a Manhattan GET failure during that check was swallowed inside
ManhattanAllowlist::get_entity and returned as None, which the fetch
helpers read as is_allowlisted: false. Enforcement then continued into the
rule pipeline for an account or post that may have been exempt, while the
neighbouring Gizmoduck and credibility fetches on the same path abort with
`?` and are retried.

Make the allowlist lookup behave like those fetches:

- ManhattanAllowlist::get / get_entity return anyhow::Result<Option<_>>.
  Ok(None) means the store confirmed the key is absent. A GET error or an
  undecodable stored entry is returned as Err instead of None.
- fetch_user_allowlist / fetch_entity_allowlist return Result and
  propagate the error. Only a confirmed absence maps to "not allowlisted".
- run_enforcement_inner uses `?` on the allowlist lookups, so a store
  error aborts the attempt and the score lands in the existing retry
  queue (backoff, then dropped without enforcing) rather than proceeding
  to rules.
- Admin handlers: GET /allowlist/{id} and GET /allowlist/{type}/{id}
  return 500 on a read error instead of 404; bulk upsert reports a failed
  pre-read as a per-row error; the DELETE audit snapshot stays best-effort.

Adds unit tests for the lookup-to-facts conversion, including one that
asserts a store error is not turned into is_allowlisted=false.

Co-authored-by: Jon Bailey <Pitchfork-and-Torch@users.noreply.github.com>
@Pitchfork-and-Torch
Pitchfork-and-Torch merged commit b4a3553 into main Sep 4, 2026
@Pitchfork-and-Torch
Pitchfork-and-Torch deleted the cursor/allowlist-fail-closed-96f3 branch September 4, 2026 11:07
cursor Bot pushed a commit that referenced this pull request Sep 8, 2026
#7)

The allowlist is an exemption check that runs before any rule can fire.
Until now a Manhattan GET failure during that check was swallowed inside
ManhattanAllowlist::get_entity and returned as None, which the fetch
helpers read as is_allowlisted: false. Enforcement then continued into the
rule pipeline for an account or post that may have been exempt, while the
neighbouring Gizmoduck and credibility fetches on the same path abort with
`?` and are retried.

Make the allowlist lookup behave like those fetches:

- ManhattanAllowlist::get / get_entity return anyhow::Result<Option<_>>.
  Ok(None) means the store confirmed the key is absent. A GET error or an
  undecodable stored entry is returned as Err instead of None.
- fetch_user_allowlist / fetch_entity_allowlist return Result and
  propagate the error. Only a confirmed absence maps to "not allowlisted".
- run_enforcement_inner uses `?` on the allowlist lookups, so a store
  error aborts the attempt and the score lands in the existing retry
  queue (backoff, then dropped without enforcing) rather than proceeding
  to rules.
- Admin handlers: GET /allowlist/{id} and GET /allowlist/{type}/{id}
  return 500 on a read error instead of 404; bulk upsert reports a failed
  pre-read as a per-row error; the DELETE audit snapshot stays best-effort.

Adds unit tests for the lookup-to-facts conversion, including one that
asserts a store error is not turned into is_allowlisted=false.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jon Bailey <Pitchfork-and-Torch@users.noreply.github.com>
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