Concepts

Registries

Define the closed predicate vocabulary that policies are allowed to use.

Registries define the vocabulary a Datalog policy is allowed to use. A Datalog rule can mention predicates such as owns, shared_with, blocked, or allow, but the Datalog engine does not know what these names mean by itself. The application must first declare which predicates exist, how many arguments they accept, and how they may be used.

In Maelys DL, this is a core security boundary:

ElementRole
Domain registryLists the domains declared by the application. Each domain defines the predicates a policy may use.
Policy sourceContains the rules and policy facts written using that vocabulary.
ManifestSelects the domain and policy files, checks their integrity, and restricts which predicates may be queried.

For programming details, see C Stable registries, Python registries and TypeScript registries.

Why registries exist

A Datalog engine can evaluate rules generically, but applications need their own vocabulary. For example, a document-access application might need:

DOMAIN
owns(User, Document)
shared_with(User, Document)
blocked(User)
allow(User, Document)

A graph application might need:

DOMAIN
edge(From, To)
reachable(From, To)

A decision engine might need:

DOMAIN
safe(Request)
allow(Request)
deny(Request)

The Datalog engine should not hard-code any of these names. Instead, each application registers a domain that declares the predicates policies may use. For the application, the important object is the domain definition: it lists the predicates that a policy may use. Registering that definition makes the domain available to policy loaders. A manifest may then restrict which of its QUERY predicates callers can inspect, but cannot add predicates.

Where registries fit

The application registers a domain such as documents, declaring owns/2, blocked/1, and allow/2 once. When a policy selects documents, the loader installs those same declarations in a predicate registry belonging to the loaded policy, freezes it, and checks the policy source against it. This is an implementation step, not a second vocabulary for the application to maintain.

ARCHITECTUREFrom a registered domain to a validated policy
    • Domain Registryapp registers once
    • Load policyselect documents
    • Predicate Registrycopied + frozen
    • Parse policyunknown names rejected

For policy A or B, the loader repeats the last three stages. The application registers documents only once; it never defines a second vocabulary for each policy.

Architectural model

Three layers govern how a policy is loaded and evaluated.

LayerRole
Domain RegistryVocabulary authority. Defines predicate names, arities, and kind flags.
Policy sourceDatalog logic. Uses predicates declared by the selected domain.
ManifestDeployment contract. Binds policies to domains, verifies integrity, and controls public observation through the Public Query Whitelist.

Predicate kinds at a glance

Three roles describe where facts come from; QUERY is a separate observation permission. A predicate has one origin and may additionally be query-enabled.

RoleMeaning
EDBRuntime facts supplied by the application.
IDBFacts derived by rules.
POLICY_FACTBase facts written directly in policy source.
QUERYObservation capability: when combined with EDB, POLICY_FACT, or IDB, permits inspection through the query API.

For example, IDB|QUERY declares a derived relation that may be queried. QUERY never stands alone: it does not say where the facts come from. The C predicate flags describe the corresponding declarations.

Domain-closed vocabulary

Maelys DL uses a domain-closed vocabulary model. That means every predicate used by a policy must be accepted by the selected domain. A policy cannot invent predicates during loading, and a manifest cannot add predicates to the domain. For example, this policy:

DATALOG
helper(X) :- safe(X).
allow(X) :- helper(X).

requires the selected domain to declare:

DOMAIN
safe/1    EDB
helper/1  IDB
allow/1   IDB|QUERY

If helper/1 is not declared by the domain, loading fails.

Example domain: document access

Suppose an application wants to evaluate document access rules. The application wants this vocabulary:

PredicateArityKindMeaning
owns(User, Document)2EDBRuntime fact saying a user owns a document.
shared_with(User, Document)2EDBRuntime fact saying a document was shared with a user.
sensitivity_level(User, Level)2EDBRuntime fact saying a user has a sensitivity level.
blocked(User)1POLICY_FACTFact declared by the policy source.
allow(User, Document)2IDB + QUERYDerived access decision.

A policy in this domain might contain:

DATALOG
blocked("mallory").
allow(User, Document) :-
    owns(User, Document),
    not(blocked(User)),
    sensitivity_level(User, Level),
    Level >= 3.
allow(User, Document) :-
    shared_with(User, Document),
    not(blocked(User)),
    sensitivity_level(User, Level),
    Level >= 3.

The extra sensitivity_level/2 guard adds a numeric threshold: ownership or sharing alone is not enough, the caller must also meet the minimum level.

This example also declares "mallory" in the domain's atoms, because that string is written in policy source. This permits the constant; it does not insert blocked("mallory"). The policy fact itself does that. By contrast, request values such as "alice" and "roadmap.pdf" need not be declared as atoms when they are supplied only in the EDB.

In this example:

PredicateWho provides it?
owns/2Runtime caller
shared_with/2Runtime caller
sensitivity_level/2Runtime caller
blocked/1Policy source
allow/2Datalog solver

The application supplies runtime facts such as:

DATALOG
owns("alice", "roadmap.pdf").
shared_with("bob", "roadmap.pdf").
sensitivity_level("alice", 4).
sensitivity_level("bob", 3).

The policy declares policy facts such as:

DATALOG
blocked("mallory").

The solver derives facts such as:

DATALOG
allow("alice", "roadmap.pdf").
allow("bob", "roadmap.pdf").

Both users satisfy the Level >= 3 guard, so the solver still derives allow/2 for each of them. A user with a lower sensitivity level would not derive allow/2 even if the ownership or sharing fact were present.

Whether callers may publicly inspect allow/2 depends on the loading path:

Loading pathPublic query behavior
Manifest loadingallow/2 must be listed in the manifest Public Query Whitelist.
Inline loadingAny predicate declared QUERY by the domain is accessible.

Two registries, one declared vocabulary

ARCHITECTUREOne domain, two independent loaded policies
    • Domain registryregistered once by the application

      • Domain: documentsowns/2, blocked/1, allow/2
    • Loaded policy A

      • Predicate registry Acopy of documents, frozen
    • Loaded policy B

      • Predicate registry Bcopy of documents, frozen

The loader copies the same declarations into each policy. A and B do not register new domains or share a mutable predicate registry.

TermWhat it means for an application
Domain definitionThe declaration you provide: a domain name and its allowed predicates, arities, flags, and optional policy-source atoms.
Domain registryThe engine's catalog of registered domain definitions. A policy selects one domain by name.
Predicate registryThe loaded policy's working copy of the selected domain's predicate declarations. It is frozen before the source is parsed. The loader creates and freezes it for you; it is not a second declaration to maintain.

For example, two policies can both select documents. Each loaded policy gets its own predicate registry for validation, but both use the vocabulary of the same registered domain. Loading a second policy does not require registering the domain again and cannot silently add a new predicate.

Predicate identity and freeze

A predicate name identifies one relation with one arity: if owns/2 is declared, the same domain cannot also use owns/1 for a different shape. Give different relations different names. A loaded policy receives the selected domain's declarations in an open predicate registry; before parsing, that registry is frozen. The parser then rejects unknown names and wrong arities, and a manifest cannot extend the frozen vocabulary.

Freezing is a loader step, not an extra call required by the public domain API. A domain defines the available vocabulary; a manifest selects a domain and restricts public observation.

What you have learned

KEEP THIS MODEL

Declare the vocabulary once
  • The domain is your vocabulary declaration

    Give it a name, list the predicates with their arity and origin, and declare any policy-source atoms. Stable C takes a declaration table; Advanced C also supports an installer callback.

  • The domain registry makes that declaration reusable

    It is the catalog in which the application registers domains. Registering the same declaration again is accepted; a different declaration under the same name is rejected, not silently substituted.

  • The predicate registry is created for you

    Each loaded policy receives a frozen copy of the predicate declarations of its selected domain. You do not maintain another vocabulary: unknown predicate names and wrong arities fail loading.

  • Origin and query permission answer different questions

    EDB means application-supplied facts, POLICY_FACT means facts written in the policy, and IDB means derived facts. QUERY permits observation and must accompany one of these origins.

  • The loading path determines the observation checks

    A manifest loads a policy set with metadata and SHA-256 verification; its queries list restricts which query-enabled predicates may be inspected. Inline loading takes one in-memory policy with no manifest whitelist, so all domain QUERY predicates are accessible.

  • A declaration table works with either loading path

    Register the domain first, then load its policy through a manifest or an inline source. The current SDK has no combined register-and-load call.

  • A manifest never creates predicates

    It selects an existing domain and may narrow public queries, but cannot extend the vocabulary of that domain. Its deprecated idb_predicates field is accepted for compatibility and has no effect.

Decision ruleThe domain defines what a policy may say. The policy defines how to reason. The manifest narrows what callers may inspect.