Skip to content

Id boundary coercion does not reach where.id inside AND/OR/NOT branches or nested relation entries #1368

Description

@borisno2

Raised deliberately during PR #1366 (issue #1164, part of #1127). The implementer stopped rather than guess, which was the right call — the gap needs a semantic decision, not a patch.

Context

parseListId (packages/core/src/contract/id-boundary.ts, ADR-0048) is the contract-driven boundary coercion for ids. #1366 routed MCP's query where.id through it, joining update and delete, so an integer-keyed list validates an integer and a malformed id matches nothing instead of reaching the driver.

The coercion currently covers a where.id at the top level of the predicate: a bare value, in / notIn, and scalar operators.

What is not covered

  1. Ids inside AND / OR / NOT branches. { OR: [{ id: 'not-an-int' }, …] } is not walked, so the id never meets the coercion.
  2. Nested relation-entry where.id. A where on a relation refinement is not walked either.

The decision this needs

Coercing an id inside a boolean branch is not simply "recurse and apply". An unparseable id at the top level can match nothing safely, because the whole predicate then matches nothing. Inside an OR, "matches nothing" is the right reading for that disjunct — but the surrounding OR still matches on its other arms, so the caller gets results from a query containing a malformed id. Inside a NOT, "matches nothing" inverts to "matches everything", which widens the result set — the dangerous direction.

So the question is per-position, and worth answering explicitly:

  • Does a malformed id make its own leaf match nothing, or make the whole request a refusal?
  • If a leaf, what is the rule under NOT, where matching nothing inverts to matching everything?
  • Does a nested relation-entry where.id follow the same rule as a root one?

A refusal for the whole request is the conservative reading and the easiest to reason about; per-leaf matching is friendlier but needs the NOT case pinned down.

Interim safety

The uncovered paths are not known to be exploitable today — an uncoerced id reaches the driver as it did before this work. This issue is about closing the boundary consistently, not about a live hole. Confirm that claim as part of the fix rather than inheriting it.

Acceptance criteria

  • The behaviour of a malformed id is defined and documented for each position: top level, inside AND, inside OR, inside NOT, and in a nested relation-entry where
  • A malformed id can never widen a result set, in any position — the NOT case is explicitly tested
  • String-keyed lists keep working (contains and friends must not be coerced away)
  • Tests cover an int-keyed list for every position above
  • update / delete / query agree on the rule

Related

#1127 story 20 and ADR-0048 (the boundary coercion's definition, spec 1).


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions