Follow-up from #358 (which enforced _in on the filter + check paths and closed #224).
Today the check/insert path deliberately honors only _eq and _in; #358 added config-load + API-boundary rejection of _neq/_gt/_lt and of a mixed _eq+_in pair on check. That was the right minimal call, but it leaves the operator vocabulary asymmetric between filter (all five operators) and check (two), and it leaves the SDK type more permissive than the server. This issue tracks standardizing that — split into three independent pieces because they carry very different risk.
Background: why check is scoped narrow (not arbitrary)
The comparison happens in a different place on each path:
- filter pushes
>/</!= down to ClickHouse as SQL, so the column's type drives a correct typed comparison.
- check evaluates in Go, in-process, via
fmt.Sprint string equality (internal/api/ingest.go, valueInSet). _eq/_in reduce cleanly to string equality/membership; ordered operators do not — "9" > "100" is true lexically, false numerically.
So this is not "wire up the missing operators" — the ordered operators need a typed-comparison implementation that does not exist on the insert path today.
Scope
Notes
- The API boundary already enforces whatever
validateRolePerms decides (Store.Put → Validate, plus the POST /v1/admin/policy/validate dry-run), so any operator change here is picked up by the SDK/gh callers for free — no separate endpoint work.
- Keep the
access-control.mdx operator table + field-reference rows in sync with whatever ships (per AGENTS.md §Documentation Sync).
Follow-up from #358 (which enforced
_inon the filter + check paths and closed #224).Today the
check/insert path deliberately honors only_eqand_in; #358 added config-load + API-boundary rejection of_neq/_gt/_ltand of a mixed_eq+_inpair oncheck. That was the right minimal call, but it leaves the operator vocabulary asymmetric betweenfilter(all five operators) andcheck(two), and it leaves the SDK type more permissive than the server. This issue tracks standardizing that — split into three independent pieces because they carry very different risk.Background: why
checkis scoped narrow (not arbitrary)The comparison happens in a different place on each path:
>/</!=down to ClickHouse as SQL, so the column's type drives a correct typed comparison.fmt.Sprintstring equality (internal/api/ingest.go,valueInSet)._eq/_inreduce cleanly to string equality/membership; ordered operators do not —"9" > "100"is true lexically, false numerically.So this is not "wire up the missing operators" — the ordered operators need a typed-comparison implementation that does not exist on the insert path today.
Scope
Add
_neqto the check path. Cheap and safe: it's_eqinverted on the samefmt.Sprintstring form — "inserted value must not equal the claim value," reject-only, no auto-inject. Rounds out the equality family with near-zero risk. Drop the_neqbranch from thevalidateRolePermscheck-path rejection and add enforcement inprocessRecord+ tests.Split
PolicyCheckfromPolicyFilterin the SDK. TodaycheckreusesPolicyFilter(clients/ts/src/types.ts), so TypeScript acceptscheck: { amount: { _gt: "…" } }(and both_eq+_inon one column) that the server rejects at.set()/.validate()with a 400. Givecheckits own type reflecting the enforced schema (_eq/_intoday,_neqonce the box above lands) so the types stop advertising operators the server refuses. Low-risk type-honesty fix; the server stays source of truth.Scope
_gt/_lton the check path — gated on real demand. Legit multi-tenant/quota patterns exist ("insertedamount≤ your tier's cap," "timestamp≥ a claim floor"), but this requires type-aware ordered comparison (numeric / date / string) on the insert path — string comparison would be a latent correctness-and-security bug in a guard. This piece supersedes the Policy filter/check:_inaccepted but never enforced;checkhonors only_eq#224/fix(policy): enforce _in on filter and check paths #358 decision to reject those operators, so treat it as a deliberate design change, not a gap-fill. Don't start without a concrete use case.Notes
validateRolePermsdecides (Store.Put→Validate, plus thePOST /v1/admin/policy/validatedry-run), so any operator change here is picked up by the SDK/ghcallers for free — no separate endpoint work.access-control.mdxoperator table + field-reference rows in sync with whatever ships (per AGENTS.md §Documentation Sync).