Skip to content

Fathom -- Getting Started

Fathom is a deterministic reasoning runtime built on CLIPS via clipspy. Define rules in YAML, evaluate in microseconds, get auditable results with zero hallucinations.

Installation

Requires Python 3.12 or later.

uv add fathom-rules

The core dependencies are clipspy, pyyaml, and pydantic -- they install automatically with the package. No other external packages are required.

Create Your First Template

Templates define the shape of facts. Create a file templates/agent.yaml:

templates:
  - name: agent
    description: "An AI agent requesting access or action"
    slots:
      - name: id
        type: string
        required: true
      - name: clearance
        type: symbol
        required: true
        allowed_values: [unclassified, cui, confidential, secret, top-secret]
      - name: purpose
        type: symbol
        required: true
      - name: session_id
        type: string
        required: true

  - name: data_request
    description: "A request to access a data source"
    slots:
      - name: agent_id
        type: string
        required: true
      - name: target
        type: string
        required: true
      - name: classification
        type: symbol
        required: true
      - name: action
        type: symbol
        allowed_values: [read, write, delete]
        default: read

Each slot has a type (string, symbol, float) and optional constraints (required, allowed_values, default). Fathom validates facts against these schemas at assertion time.

Register a Classification Hierarchy

To compare clearance against data classification we need an ordered set of levels. Declare it in hierarchies/clearance.yaml:

name: clearance
levels:
  - unclassified
  - cui
  - confidential
  - secret
  - top-secret

Then tie the hierarchy to the rule operators with a classification function in functions/clearance.yaml:

functions:
  - name: clearance
    type: classification
    params: [a, b]
    hierarchy_ref: clearance.yaml

Loading this function registers the below(...), meets_or_exceeds(...), and within_scope(...) operators so rules can compare levels.

Write Your First Rule

Rules define pattern-matching logic that fires when facts match conditions. Create rules/access-control.yaml:

ruleset: access-control
version: "1.0"
module: governance

rules:
  - name: deny-insufficient-clearance
    description: "Deny access when agent clearance is below data classification"
    salience: 10
    when:
      - template: data_request
        alias: $data
        conditions:
          - slot: classification
            expression: "meets_or_exceeds(unclassified)"
      - template: agent
        conditions:
          - slot: id
            expression: "equals($data.agent_id)"
          - slot: clearance
            expression: "below($data.classification)"
    then:
      action: deny
      reason: "Agent clearance is below the data classification (no read up)"
      log: full

when is a list of fact patterns. Each pattern names a template, an optional alias (used for cross-fact references like $data.classification), and a list of conditions. Each condition pairs a slot with an expression -- a literal operator like equals(...) / in([...]), or a classification operator like below(...) / meets_or_exceeds(...). Pattern ordering matters: the pattern that binds an alias must appear before any pattern that references it, so data_request (alias $data) is listed before the agent pattern that uses $data.classification.

salience controls firing priority -- higher values fire first. The evaluator uses last-write-wins on __fathom_decision facts, so the rule firing LAST wins. Deny rules should have LOWER salience than allow rules so they fire last and override any prior allow, enforcing fail-closed behavior.

Set Up Modules

Modules namespace rules into isolated groups with controlled execution order. Create modules/modules.yaml:

modules:
  - name: governance
    description: "Action-level governance rules"

focus_order: [governance]

The focus_order list determines evaluation sequence -- when you add more modules, rules in earlier modules fire before rules in later ones. Each rule file declares which module it belongs to via the module field.

Run Your First Evaluation

from fathom import Engine

engine = Engine()
engine.load_templates("templates/")
engine.load_modules("modules/")
engine.load_functions("functions/")
engine.load_rules("rules/access-control.yaml")

engine.assert_fact("agent", {
    "id": "agent-alpha",
    "clearance": "secret",
    "purpose": "threat-analysis",
    "session_id": "sess-001"
})

engine.assert_fact("data_request", {
    "agent_id": "agent-alpha",
    "target": "hr_records",
    "classification": "top-secret",
    "action": "read"
})

result = engine.evaluate()

print(result.decision)     # "deny"
print(result.reason)       # "Agent clearance is below the data classification (no read up)"
print(result.rule_trace)   # ["governance::deny-insufficient-clearance"]
print(result.module_trace) # ["governance"]
print(result.duration_us)  # ~90 (microseconds; varies by machine)

load_functions must run before load_rules, because the rules reference the below(...) operator the classification function registers. The full project layout is:

templates/agent.yaml          agent + data_request schemas
hierarchies/clearance.yaml     ordered classification levels
functions/clearance.yaml       registers below(...) / meets_or_exceeds(...)
modules/modules.yaml           single 'governance' module
rules/access-control.yaml      the deny rule

The agent's secret clearance is below the request's top-secret classification, so the deny rule fires. If the agent's clearance instead met or exceeded the classification, no deny rule would match and the engine would fall through to its fail-closed default (decision = "deny", reason = "default decision (no rules fired)"). To grant access you would add an explicit allow rule with higher salience -- see example 03 for the full read/write Bell-LaPadula pattern.

The result object contains five fields:

  • decision -- the action taken (allow, deny, escalate, etc.)
  • reason -- human-readable explanation with interpolated variables
  • rule_trace -- ordered list of rules that fired, prefixed by module name
  • module_trace -- ordered list of modules that were evaluated
  • duration_us -- evaluation time in microseconds

Working memory persists across evaluations within a session. This enables cumulative reasoning ("agent accessed PII from 3 sources -- deny the 4th") and temporal patterns that stateless engines cannot express.

Next Steps

Do Not

  • Do not skip template definitions and assert untyped facts -- Fathom validates at assertion time.
  • Do not set deny rules to higher salience than allow rules -- deny must have lower salience so it fires last and wins under last-write-wins (fail-closed).
  • Do not assume working memory resets between evaluate() calls within the same session -- it persists.
  • Do not use raw CLIPS constructs when YAML equivalents exist -- YAML is the primary authoring interface.