Skip to main content

rulea v2

Readable rules. Predictable decisions.

rulea is an embeddable decision language for Python. Applications collect facts; rulea returns an explainable decision. Applications perform any resulting actions. The core has no runtime dependencies and never executes Python eval or policy actions.

Requires Python 3.9+. Install with pip install . (development: pip install pytest).

from rulea import RuleEngine

engine = RuleEngine("examples/documents.rulea")
decision = engine.decide("documents.edit", {
    "user": {"id": 100},
    "resource": {"owner": 100, "locked": False, "status": "draft"},
})
if decision.allowed:
    pass  # application performs the operation
print(decision.to_dict())

Policy language

version 42
namespace documents

schema:
    user.id: int
    resource.owner: int
    resource.locked: bool

rule owner:
    when: user.id == resource.owner
    reason: "Only the owner may edit"

rule editable:
    when: matches("owner") and not resource.locked
    reason: "Owner required and document must be unlocked"

rule locked:
    when: resource.locked
    effect: DENY
    reason: "Document is locked"

ruleset edit:
    rules: locked, editable
    strategy: deny_overrides
    default: DENY

Declarations begin at column zero; fields are indented. Conditions occupy one line (parentheses are supported). Whole-line comments and blank lines are accepted. A policy has at most one namespace and version. Version defaults to 1. Rule fields: required when; optional reason, effect (ALLOW by default), priority (0), enabled (true), description, and comma-separated tags. Reasons may be quoted strings or plain text. Unknown/duplicate fields and obsolete action/extends fields are load errors, not silently ignored.

Expressions support plain fact paths (user.id or user["id"]), JSON scalar literals, list/tuple literals, and, or, not, chained comparisons (== != < <= > >=), membership (in, not in) and matches("rule"). Boolean operators short-circuit and require actual booleans. No arithmetic, comprehensions, arbitrary calls, methods, private attributes, assignment or Python imports are available. Expression size, AST depth and node count are bounded. Fact dictionaries/lists/scalars must be exact built-in JSON types; arbitrary objects and subclasses are rejected before their methods can be invoked. Nonfinite floats and deeply nested facts are rejected. Inputs are copied, never mutated.

Decisions and conflicts

Decision and its trace entries are immutable data. The result is one of:

Result Meaning
ALLOW Permission granted
DENY Permission denied
NOT_APPLICABLE Unknown/disabled rule, or unmatched rule inside a ruleset
ERROR Missing/invalid facts or evaluation failure

A direct rule evaluation converts a false condition to DENY. Within a ruleset, a false condition is NOT_APPLICABLE, allowing other rules to decide. A matching rule returns its declared effect. decision.allowed and bool(decision) are true only for ALLOW; engine.check(name, context) preserves the legacy (bool, reason_or_none) interface and fails closed for all other results. Unknown rule names are never treated as explicit denial internally.

Rulesets require all three fields: rules, strategy, default.

Strategy Selection
deny_overrides Evaluate all; matching DENY wins over ALLOW
allow_overrides Evaluate all; matching ALLOW wins over DENY
priority Evaluate highest numeric priority first; first applicable wins
first_match Evaluate in declared order; first applicable wins

ERROR takes precedence among evaluated rules, including with allow_overrides. For priority ties and same-effect matches, declaration order wins. No applicable rule uses the explicitly declared default (ALLOW, DENY or NOT_APPLICABLE). Short-circuited rules are not evaluated. Each ruleset decision contains the strategy, winning rule (or default), and all evaluated child decisions.

The formal v2 predicate reference is matches("rule"); allow() is rejected. A predicate is the boolean when condition, not a Decision. Only rules whose effect is ALLOW (including the default) can serve as predicate references. Referencing an effect: DENY rule is a load-time PolicyError, even under not or inside an unreachable branch. Explicit denials belong in rulesets.

Evaluation Condition true Condition false
Direct rule, effect ALLOW ALLOW DENY
Direct rule, effect DENY DENY DENY
Ruleset member, effect ALLOW ALLOW NOT_APPLICABLE
Ruleset member, effect DENY DENY NOT_APPLICABLE
matches() of enabled ALLOW rule True False

Unknown references fail loading. A disabled ALLOW predicate returns False; not matches() can invert that boolean. Errors propagate as ERROR, never as False. Negation of a predicate failure is intentional and cannot invert an explicit DENY because DENY rules cannot be referenced. For example, a block condition can be declared as an ALLOW predicate and used via not matches(), while an actual effect: DENY rule must be combined through a ruleset. Child decisions in composition traces use ALLOW/NOT_APPLICABLE for matched/ unmatched predicates; they are not independent direct-rule evaluations.

Composition resolves an unqualified name in the current namespace first, then as an absolute name. Only rules can be referenced, not rulesets. Unknown references and cycles fail during loading. Each rule is evaluated at most once per request. A referenced ERROR propagates through matches, including beneath not; it cannot become an accidental permission grant.

Schemas, modules and replay

An optional schema maps full fact paths to string, int, float, bool, list or null. With any schema present, every expression fact path must be declared. Unknown paths and incompatible comparison types fail at load time. At evaluation all schema fields are required with exact types (bool is not int). Extra JSON facts are accepted. List element types are currently not declared. Without a schema, missing facts are ERROR at runtime. Schema validation is intentionally small; it does not prove all runtime operations valid.

Policy imports do not load files, packages or remote content. The host provides a name-to-source mapping; imported modules should declare their own namespace:

engine = RuleEngine.from_string(
    'import shared\nrule edit:\n    when: matches("common.owner")\n',
    modules={"shared": 'namespace common\nrule owner:\n    when: user.id == resource.owner\n'},
)

Missing modules, cyclic imports, duplicate qualified names and conflicting schemas fail early. The engine snapshots and compiles all sources at load time. The CLI currently accepts self-contained policies; use the Python API for modules.

Each decision contains result, rule, reason, policy_version, policy_hash, engine_version, boolean expression trace, and nested decisions. Trace records evaluated expressions, including false conditions; short-circuited expressions are absent. It does not dump fact values. The SHA-256 hash includes exact source text and every imported module, while the human version is declared by the author. Archive sources, context and engine version to replay a decision. No clocks, randomness, environment variables or external services are consulted during evaluation. Deep composition exceeding the interpreter recursion limit returns ERROR rather than granting access.

Observability

events = []
engine = RuleEngine("examples/documents.rulea", on_event=events.append)

Events: policy_loaded, rule_evaluated, decision_produced, evaluation_failed. Hooks receive fresh event dictionaries; their exceptions are isolated from decisions. Events intentionally contain no timestamps or latencies: the host can measure elapsed time around decide and export metrics. Decisions contain no timing-dependent fields. Context, evaluation caches and traces are local to each call, permitting concurrent reads of a loaded engine. Treat loaded engines as immutable and replace the instance to reload policies. Hooks themselves must be thread-safe when used concurrently.

Development tools

rulea check examples/documents.rulea
rulea lint examples/documents.rulea
rulea test examples/documents.rulea examples/documents.cases.json
rulea explain examples/documents.rulea documents.edit --context '{"user":{"id":100},"resource":{"owner":100,"locked":false,"status":"draft"}}'
rulea fmt examples/documents.rulea --write
rulea benchmark examples/access.rulea can_view --context '{"user":{"is_authenticated":true}}'
python -m pytest -q

python -m rulea is also supported. check and lint run the same strict load-time validation. fmt conservatively normalizes indentation and trailing whitespace without reordering declarations. test reads a nonempty JSON array of {rule, context, result} cases. explain emits structured JSON. Exit codes: 0 success/ALLOW; 1 failed test or non-ALLOW explanation; 2 invalid policy/input or ERROR. Benchmark timing is tooling output, never decision data.

Migrating from v1

This is a major release. Existing simple one-line rules and check() remain usable; conditions now require booleans and undefined variables are errors. Fix rule-name mismatches (the legacy example declares can_edit, not edit). Replace extends with explicit matches() composition. Move every action to the host application; ActionRegistry has been removed. The permissive parser and legacy positional CLI are replaced by strict validation and subcommands. Python 3.8 support is dropped. No policy server, audit database, management UI, workflow engine or framework dependency is included in the core.

Public API contract (2.x)

Stable top-level exports are RuleEngine, Decision, Result, Trace, and PolicyError, plus __version__. Supported engine interfaces are the path constructor, from_string, decide, check, policy_version and policy_hash. Decision/Trace data fields, Decision.allowed, bool conversion and to_dict() are public. Result retains the four documented values. The DSL reference function is matches(); no decision-reference allow() alias is provided. Parser/compiler/evaluator modules, their ASTs and engine internals are not stable API. parse_rule_file is no longer a top-level export; its internal helper is not covered by 2.x compatibility guarantees. Use RuleEngine to parse and validate policies. This is a pre-2.0 syntax correction, not a 2.x rename.

Release procedure

The only publishing workflow is .github/workflows/python-publish.yml, triggered by a published GitHub Release. Main pushes and PRs only run checks. The workflow builds wheel and sdist, validates metadata, runs tests, installs the wheel in an isolated environment and executes examples before passing artifacts to a separate OIDC publishing job. There is no stored PyPI token or password fallback.

Before releasing, configure the rulea PyPI Trusted Publisher with owner tanuki276, repository rulea, workflow python-publish.yml, environment pypi. The GitHub pypi environment and any desired release protection rules must also be configured by a maintainer. Workflow YAML alone cannot verify that PyPI trusts the repository. See PyPI setup.

  1. Make the PR ready and require the test matrix plus distribution checks to pass.
  2. Review and merge. Match the Release tag (2.0.0 or v2.0.0) to project.version.
  3. Publish a GitHub Release from that commit; the single workflow publishes to PyPI.
  4. Inspect the publish job and verify the released package installation.

pyproject.toml is the sole metadata source. The existing LICENSE is preserved and included as package license metadata; the former MIT classifier did not match that file and has been removed. No license grant is changed by v2.

Metadata

Release files for rulea 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rulea 2.0.0
File Size Uploaded Provenance
rulea-2.0.0.tar.gz 23.1 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for rulea 2.0.0
File Interpreter ABI Platform
rulea-2.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 39.6 kB

Release files / rulea-2.0.0.tar.gz

Download URL rulea-2.0.0.tar.gz
Size 23.1 kB
Tags Source
SHA-256 checksum
How to use checksums
6553875e23c85b7154f3bbb95f63288f95f5809e2bbcaf0f6dbfc30d8d5b1846
BLAKE2b-256 checksum
How to use checksums
9a2cd8d7c4877836cffe6d62488679886fc04c7c39a43e0b44390e3f43982dc8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Release files / rulea-2.0.0-py3-none-any.whl

Download URL rulea-2.0.0-py3-none-any.whl
Size 16.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
153a6ae983c6aff3ae00a7514437f40fd6ed16dbe35a4f932216fc4194746b73
BLAKE2b-256 checksum
How to use checksums
fb6e72550a777c08796d24cbf6a6054f6646161e2f942e026f528cd26f4a23f9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Security for rulea 2.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for rulea-2.0.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page