Loading a rule pack¶
Fathom supports two ways to ship rules into an Engine: as a directory tree
you maintain alongside your application, or as an installable Python package
discovered at runtime through a setuptools entry point. Both routes funnel
through the same loaders, so the YAML you author looks identical either way —
only the delivery mechanism differs.
Load from a directory¶
Engine.from_rules(path, **kwargs) is a classmethod that returns a configured
Engine. Any keyword arguments are forwarded to the Engine(...) constructor.
from fathom.engine import Engine
engine = Engine.from_rules("examples/01-hello-allow-deny")
result = engine.evaluate()
The loader tries two discovery strategies, in order:
- Subdirectory convention (preferred). If the pack directory contains any
of
templates/,modules/,functions/, orrules/, each present subdirectory is passed to the matchingengine.load_templates,load_modules,load_functions, orload_rulesmethod. - Key-inspection fallback. If none of those subdirectories exist, every
*.yamlfile directly underpathis opened and routed by its top-level key:templates→ templates loader,modulesorfocus_order→ modules loader,functions→ functions loader,rulesorruleset→ rules loader.
Both strategies load in the same fixed order: templates → modules →
functions → rules. The order matters because templates define the slot
schemas that rules bind against, modules establish the namespaces that rules
live in, and functions may be referenced from a rule's left-hand side via the
raw-CLIPS test: clause — so everything a rule depends on must be compiled
before the rule itself.
Directory layout¶
For anything larger than a toy example, use the subdirectory convention. The
reference shape is what examples/01-hello-allow-deny ships with:
my-pack/
├── templates/
│ └── *.yaml
├── modules/
│ └── *.yaml
├── functions/
│ └── *.yaml
└── rules/
└── *.yaml
Every YAML file under a given subdirectory is loaded. The glob is *.yaml
only — files ending in .yml are not picked up by from_rules, so stick
to the long extension. Empty or missing subdirectories are fine; the loader
simply skips them.
The key-inspection fallback is handy for tiny single-file packs where
splitting into subdirectories would be overkill, but it's strictly less
expressive: it only scans the top level of path (no recursion) and each
file must declare exactly one top-level key that the loader recognises.
Load a directory into an engine you already have¶
from_rules is a constructor: it hands you a new Engine. When the engine
already exists — a long-lived process, a pack uploaded at runtime, a second
pack added to the first — use Engine.load_pack_dir(path):
from fathom.engine import Engine
engine = Engine(audit_sink=my_sink)
engine.load_pack_dir("/srv/packs/kssi/v1")
engine.load_pack_dir("/srv/packs/tenant-overrides")
It accepts both layouts above, loads in the same fixed order, and is the same
implementation from_rules and load_pack use — so there is one place that
knows the order rather than one per caller.
Three things it does that hand-rolling the four load_* calls does not:
- Loading the same directory twice is a no-op. Identity is the resolved path, so a relative and an absolute spelling are one pack, not two.
- A second pack may not redefine a template the first registered. The
check runs before anything is built, so a rejected pack leaves the engine
untouched. This is the same check
load_packapplies. - A directory holding nothing it recognises is an error, not an engine with no rules in it. Pointing one level too high used to succeed silently.
What it does not do is resolve PACK_DEPENDENCIES: a directory has no
module to declare them on, so a pack that needs another one loaded first must
have that one loaded by its caller. from_rules keeps its older, laxer
behaviour on an empty directory — it returns an empty engine rather than
raising, because FleetEngine builds session engines that way.
Load a distributed pack via entry point¶
Once a pack is packaged as a Python distribution, load it by name:
Engine.load_pack delegates to RulePackLoader, which walks the
fathom.packs entry-point group, imports the registered module, resolves its
on-disk location via module.__path__, and then runs the same
templates/ → modules/ → functions/ → rules/ subdirectory load as
from_rules does.
To expose your own pack, add an entry to your pyproject.toml:
Here my_package/rules/ is an importable package directory that contains the
familiar templates/, modules/, functions/, and rules/ subdirectories
full of YAML. After pip install, any process with Fathom installed can call
engine.load_pack("my-pack").
Packs shipped with Fathom¶
Fathom currently ships seven first-party rule packs, registered under the same
entry-point group in its own pyproject.toml:
owasp-agenticssvcnist-800-53hipaacmmcschema-denoisingconflict-detection
The first five render decisions. The last two do not: their rules only assert facts, and the host reads those facts or writes its own rules over them.
schema-denoisingfilters an upstream extraction stream down to the relation types that appear often enough to trust, leavingaligned_factfacts behind.conflict-detectionreports contradictions between claims asconflictfacts. It does not resolve them: what to do about a contradiction is a policy question, so it belongs in your rules rather than in the pack.
Pack dependencies¶
A pack module may declare PACK_DEPENDENCIES, a tuple of pack names to load
first:
load_pack resolves those before loading the pack itself. cmmc uses this:
it builds on nist-800-53, so engine.load_pack("cmmc") loads both.
Loading the same pack twice is a no-op¶
load_pack tracks which packs an Engine already holds and skips a repeat
load rather than re-running the loaders (which CLIPS would reject with
[CSTRCPSR4] Cannot redefine deftemplate … while it is in use). This is what
makes shared dependencies safe: loading nist-800-53 and then cmmc does
not load NIST twice.
The tracking is per-Engine, and Engine.reload_rules() clears it
(engine.py calls packs.forget_packs). A reload swaps in a fresh CLIPS
environment and discards the rule registry, so a previously-loaded pack's
rules are gone; keeping the record across that made the obvious repair — load
the pack again — a silent no-op that returned successfully while the rules
stayed absent. Clearing it means the retry is attempted, and fails loudly if
it cannot succeed. The same applies to load_pack_dir, which shares the
record.
Packs that define the same template¶
Two packs may not define the same template name with different definitions in
one Engine. load_pack checks for this before loading anything and
raises CompilationError:
The check runs first, so the second pack is not half-loaded: the engine is left exactly as it was. Identical definitions are allowed and simply reused.
This affects real combinations today — hipaa and nist-800-53/cmmc both
define data_transfer, audit_event and access_request with different
slots, so they cannot share an Engine. Load them into separate Engine
instances and evaluate against each.
Error handling¶
If you pass a name that isn't registered under fathom.packs,
RulePackLoader.discover raises CompilationError with
construct="pack:<name>". The same error is raised if the registered module
has no resolvable path (neither __path__ nor __file__), which in practice
only happens for exotic namespace-package setups.
When to use which¶
Engine.from_rules(path)— your application owns the rules, they live in a directory inside the repo (or a mounted volume), and you want the fastest edit-reload loop. Best for development, single-tenant deployments, and environment-specific overrides.engine.load_pack(name)— the rules are a redistributable asset consumed by multiple applications, need independent versioning, and can be published alongside your Python wheels. Enablespip install compliance-packstyle workflows and clean upgrades.
Nothing stops you from mixing both in one Engine: call load_pack for a
shared baseline, then load_rules or load_templates on a local directory
for application-specific overlays.
Related reading¶
- Python SDK reference — full
EngineAPI, including everyload_*method used here. - Writing rules — YAML authoring conventions for the files inside a pack.
- YAML schema reference — the top-level keys
(
templates,modules,functions,rules) that the key-inspection fallback looks for.