You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while implementing #630. Narrowed by the PM on 2026-08-02: the sharing-rule half of this issue is closed by measurement (see below) and only the flow-start-condition half remains open.
Settled: sharing rules must NOT get has() guards
The original suggested work said "if either surface is exposed, add has(...) guards". For sharing rules that is now known to be actively harmful. Measured in #621 / PR #637 with the platform's own compileCelToFilter — the function plugin-sharing calls at seed time:
Sharing conditions are compiled to pushdown filters stored in sys_sharing_rule.criteria_json, not interpreted per record. Function calls as a class are outside the translatable subset. So adding a has() guard to a sharing rule makes it untranslatable, which makes the seeder skip it — turning seeded: 9, skipped: 0 into seeded: 0, skipped: 9, with the same quiet signature as the bug #621 was filed for, amplified from 2 rules to 9.
Further, sharing rules likely need no totality guard at all: a missing column in a where clause is NULL-compared by the database, not an abort. The abort behaviour belongs to the CEL interpreter, and sharing conditions never reach it.
This conclusion is pinned as an assertion in test/sharing-seeding.test.ts, so an attempt to add guards there turns CI red rather than shipping inert rules.
Still open: flow start conditions (9)
case_escalation, case_escalation_on_create, opportunity_approval, opportunity_approval_on_create, lead_assignment.check_hot, case_csat_followup, contact_welcome, opportunity_won_alert, task_urgent_alert — all read record fields with no has(...).
Whether that matters is unmeasured, and must not be inherited from either of the other two surfaces. The validation path aborts and skips; the sharing path compiles to SQL and never interprets. Flow start conditions are a third mechanism and could behave like either, or neither.
What to establish:
What record shape does a record-change trigger hand the start condition — the driver's post-write row (sparse on driver-memory / driver-mongodb), a re-read, or a merged {...previous, ...data}?
Are start conditions interpreted or compiled? If compiled to a filter like sharing rules, guards are wrong here too, for the same reason.
What happens on abort — does the flow silently not run (the same declared-≠-enforced shape), or does it error visibly?
Suggested work
Measure (1)–(3) against driver-memory, which is already sparse and reproduces the condition without any setup.
If they are not exposed: change nothing, and record why — a comment naming the record shape and evaluation mechanism the flow path actually uses — so the next person does not re-measure it. A correct "no action needed, here is the evidence" outcome is a complete result for this issue, not a failure.
Strict CEL aborts the whole predicate on a key that is not present. Note record.f != null aborts too — the != null guards already in the repo protect against dyn< null > < int, not against an absent key.
Found while implementing #630. Narrowed by the PM on 2026-08-02: the sharing-rule half of this issue is closed by measurement (see below) and only the flow-start-condition half remains open.
Settled: sharing rules must NOT get
has()guardsThe original suggested work said "if either surface is exposed, add
has(...)guards". For sharing rules that is now known to be actively harmful. Measured in #621 / PR #637 with the platform's owncompileCelToFilter— the functionplugin-sharingcalls at seed time:Sharing conditions are compiled to pushdown filters stored in
sys_sharing_rule.criteria_json, not interpreted per record. Function calls as a class are outside the translatable subset. So adding ahas()guard to a sharing rule makes it untranslatable, which makes the seeder skip it — turningseeded: 9, skipped: 0intoseeded: 0, skipped: 9, with the same quiet signature as the bug #621 was filed for, amplified from 2 rules to 9.Further, sharing rules likely need no totality guard at all: a missing column in a
whereclause is NULL-compared by the database, not an abort. The abort behaviour belongs to the CEL interpreter, and sharing conditions never reach it.This conclusion is pinned as an assertion in
test/sharing-seeding.test.ts, so an attempt to add guards there turns CI red rather than shipping inert rules.Still open: flow start conditions (9)
case_escalation,case_escalation_on_create,opportunity_approval,opportunity_approval_on_create,lead_assignment.check_hot,case_csat_followup,contact_welcome,opportunity_won_alert,task_urgent_alert— all read record fields with nohas(...).Whether that matters is unmeasured, and must not be inherited from either of the other two surfaces. The validation path aborts and skips; the sharing path compiles to SQL and never interprets. Flow start conditions are a third mechanism and could behave like either, or neither.
What to establish:
driver-memory/driver-mongodb), a re-read, or a merged{...previous, ...data}?Suggested work
driver-memory, which is already sparse and reproduces the condition without any setup.has(...)guards in the two shapes Script validations are silently SKIPPED when the merged record lacks the field key (update path) #630 settled on, and extend the sweep intest/object-validation-predicates.test.tsto cover flow start conditions.Background (from #630)
Strict CEL aborts the whole predicate on a key that is not present. Note
record.f != nullaborts too — the!= nullguards already in the repo protect againstdyn< null > < int, not against an absent key.isBlank(record.f)truerecord.f != nullfalserecord.f == "v"falsehas(record.f)falsetrue@objectstack/driver-sqlnull@objectstack/driver-sqlite-wasmnull@objectstack/driver-memory@objectstack/driver-mongodb