Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

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.

License

Rulea is licensed under the Apache License 2.0. See LICENSE for the full license text.

Policy quality tools (2.1 alpha)

These tools run locally without a Rulease server or database:

rulea lint policy.rulea
rulea test policy.rulea cases.json
rulea diff baseline.rulea candidate.rulea
rulea simulate baseline.rulea candidate.rulea contexts.json

lint() returns diagnostics with code, severity (INFO, WARNING, ERROR), message and rule. It reports compiler errors, missing reasons, constant boolean conditions, disabled rules and provably shadowed first-match or priority members. lint(source, entrypoints=[...]) additionally finds unused rules relative to explicit entry points. It does not claim general theorem proving or infer that public entry points are unused.

Test cases retain the existing {rule, context, result} format, or use:

[{"name":"minor","rule":"checkout.adult","context":{"age":17},
  "expect":{"result":"DENY","reason":"minimum_age",
            "trace_contains":["age >= 18"],"decision_count":1}}]

decision_count includes the root and recursive child decisions. children assertions match a descendant by exact rule, result, and/or reason fields. run_suite(..., schema=...) validates contexts against JSON Schema 2020-12. Remote schema references are rejected; validation never retrieves a URL.

semantic_diff() reports added/removed declarations and exact changed fields. behavioral_diff() compares results, reasons and child decisions on identical inputs. Its changed case indices refer to the caller's input sequence; the report does not copy private contexts. Compilation errors abort the comparison.

Metadata

Release files for rulea 2.1.0a1

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.1.0a1
File Size Uploaded Provenance
rulea-2.1.0a1.tar.gz 31.6 kB With provenance Details

Built distribution (wheel)

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

Total release size: 55.7 kB

Release files / rulea-2.1.0a1.tar.gz

Download URL rulea-2.1.0a1.tar.gz
Size 31.6 kB
Tags Source
SHA-256 checksum
How to use checksums
4cab37676d0f0911d4424836a603f1f5e0abfbaace240996ddc3cd5f1b2d006c
BLAKE2b-256 checksum
How to use checksums
83a6bbe98b42dbe68625b2c67ab16576e8232dc148d6c2a4470867be805cb731
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 22, 2026.

Transparency log

Release files / rulea-2.1.0a1-py3-none-any.whl

Download URL rulea-2.1.0a1-py3-none-any.whl
Size 24.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
90e0315f6b3784c30494b062d280a2650c58c50eb06426efa8a6b4f42b67a62f
BLAKE2b-256 checksum
How to use checksums
ff3f77720ff90ebddc12db585597bd26a4120442c4f8a68b842c24abf0220748
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 22, 2026.

Transparency log

Security for rulea 2.1.0a1

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.1.0a1.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 22, 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