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
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Renderer/action visible/disabled predicates bypass the canonical CEL engine — home-grown JS evaluator diverges from server enforcement #2661
home-grown packages/core/src/evaluator/ExpressionEvaluator.ts — compiles the predicate as plain JavaScript (ExpressionCache.compile → fn(...))
❌ not CEL at all
Path B unwraps a { dialect: 'cel', source } envelope but then ignores the dialect and executes the source as JS (ExpressionEvaluator.evaluate, comment says "the underlying syntax is identical" — it isn't):
FormulaFunctions has notoday / daysFromNow / daysAgo / now — any date predicate an author writes per the platform's CEL catalog throws today is not defined → fail-open.
CEL-isms diverge under JS semantics: has(record.x), in, "a" in list, single-quoted strings inside ${} templating, == coercion (JS == is loose where CEL is typed), size(), matches(), isBlank() — none behave per the authoring contract that validateExpression / the server enforce.
Consequence class: an action visible predicate that the server-side ActionEngine (fail-closed, CEL) and build-time lint both accept can evaluate differently — or not at all — in the browser. The framework's whole guardrail program (ADR-0032, fix(console): resolve ctx.user-gated record-header actions for platform admins #1928 family, framework#3183) stops at this boundary.
Why now
framework#3205 just fixed record.due_date == today() at the single celEngine.evaluate chokepoint. Path A inherits that fix with a dependency bump (objectui#<bump-PR>). Path B inherits nothing — it isn't downstream of the engine.
Proposed direction
Converge Path B onto the canonical engine, exactly as fieldRules.ts already does:
In useCondition / SchemaRenderer / ExpressionProvider, when the predicate is a { dialect: 'cel' } envelope (or once spec normalizes these sites to CEL), route it to @objectstack/formula's ExpressionEngine.evaluate with { record, extra: scope } — same fail-open fallback semantics as today.
Keep the legacy ${...} JS-template path for plain strings during a deprecation window (it's a documented ObjectUI DX shorthand), but stop treating CEL envelopes as JS.
Reuse evalFieldPredicate's pattern (or extract a shared evalCelPredicate) so there is one client-side delegation point.
Bundle-size note: app-shell's celAuthoring.ts already lazy-loads @objectstack/formula via dynamic import() — the same trick applies if engine weight in the base renderer is a concern.
Acceptance
An action visible: { dialect: 'cel', source: 'record.status == "open" && record.due_date == today()' } evaluates identically in the browser and in the server ActionEngine (incl. the framework#3205 date-equality fix).
has(...), in, string equality, isBlank, today()-family all behave per the CEL catalog in component/action predicates.
Legacy ${...} string predicates unchanged.
fieldRules Path A untouched (already canonical).
Refs: framework ADR-0032, framework#1928, framework#3183, framework#3205; packages/core/src/evaluator/fieldRules.ts (the in-repo precedent).
Activity
chore(deps): align @objectstack/formula|lint|client to ^15.1.1 (spec …
fix(core): revert Error cause arg — package TS lib is < ES2022 (unbre…
feat(evaluator): route CEL-dialect component/action predicates to the…
fix(action): honor the spec disabled predicate on every action-render…
fix(core): ActionEngine 保留 CEL 信封,与渲染器判决一致 (objectstack-ai#3314) (obj…