@yarlisaisolutions/rule-cascade-server
A stateless HTTP service that implements spec/v1/rule-evaluation.openapi.yaml. Use it for callers that cannot embed a runtime. It keeps nothing between requests, so you run as many replicas as you need behind a load balancer.
A stateless HTTP service that implements spec/v1/rule-evaluation.openapi.yaml (spec/v1/rule-evaluation.openapi.yaml).
Use it for callers that cannot embed a runtime. It keeps nothing between requests, so you run as
many replicas as you need behind a load balancer.
npm ci && npm run build # at the repository root
RULE_SERVER_TOKEN=$(openssl rand -hex 32) RULES_DIR=examples/contracts node packages/server/dist/main.js| Variable | Meaning | Default |
|---|---|---|
RULES_DIR | Directory with the rulesets to serve; see below | ./rules |
PORT | Listen port | 8080 |
RULE_SERVER_TOKEN | Bearer token for evaluations, the server manifest and bundles | required |
RULE_SERVER_ALLOW_OPEN | 1 or true: start without a token and serve every endpoint openly | unset |
DRAIN_SECONDS | Time between "not ready" and shutdown on SIGTERM | 5 |
RULES_REFRESH | manual (SIGHUP only), interval or cron: also reload on a schedule when the files changed | manual |
RULES_REFRESH_INTERVAL | With interval: 30s, 5m, 1h or seconds; at least 1s | none |
RULES_REFRESH_CRON | With cron: minute hour day-of-month month day-of-week | none |
RULES_REFRESH_TZ | Time zone of the cron schedule | UTC |
RULE_SERVER_WORKERS | Evaluate on this many worker threads; 0 evaluates on the event loop | 0 |
RULE_EVALUATION_TIMEOUT_MS | With workers: answer 503 and replace the worker after this long | 5000 |
Scheduled refresh, the cache policy and the checksum URL are described in docs/caching.md; the worker pool and measurements in docs/performance.md.
The rules directory
The server loads two kinds of files from RULES_DIR:
| File | What happens at start-up and on reload |
|---|---|
*.ruleset.yaml, *.ruleset.yml, *.ruleset.json | Compiled here: schema validation, inheritance and every load check. Entity schemas are read from the same directory |
*.bundle.json | Read as a precompiled bundle. Only the bundle format is checked; the compiler that produced it already ran the load checks |
An optional cache-policy.yaml (or .yml, .json) sets the Cache-Control of client manifests
per ruleset: revalidate (the default), ttl or permanent. It is checked with the rulesets; a bad
one stops the start or refuses the reload. See docs/caching.md.
A ruleset id may be defined once, by one file. A source ruleset can only extend a parent that is also present as a source document, because a bundle does not hold what inheritance needs.
A bundle carries no title, scope or status. GET /rulesets therefore lists a ruleset that came from a
bundle with its id, version and checksum only, and a ?scope= filter does not match it.
Endpoints
| Method and path | Purpose | Needs the token |
|---|---|---|
GET /healthz | Liveness | no |
GET /readyz | Readiness; 503 while draining | no |
GET /rulesets?scope= | List rulesets, optionally under a hierarchy path | no |
GET /rulesets/{id}/manifest?channel=client | Client-safe manifest, with ETag | no |
GET /rulesets/{id}/manifest?channel=server | Full manifest for backend runtimes | yes |
GET /rulesets/{id}/bundle | The compiled bundle, for runtimes that evaluate in process; with ETag | yes |
GET /rulesets/{id}/rules/{ruleId} | One resolved rule | server-only rules: yes |
POST /evaluations | Evaluate an operation | yes |
The endpoints marked "yes" answer 401 without Authorization: Bearer <token>. The bundle contains
the server manifest, so it is protected exactly like it: the token is checked before the ruleset is
looked up and before a conditional request is answered. The endpoints marked "no" are public on
purpose: browsers fetch the client manifest directly, and load balancers probe the health endpoints.
The server fails closed. Without RULE_SERVER_TOKEN the process logs a fatal line and exits 1
before it loads any rules. To serve every endpoint without a token, which is only acceptable on a
private network, set RULE_SERVER_ALLOW_OPEN=1; the server then logs a warn line at start-up and
marks its listen line "auth":"open". An empty token counts as unset. Embedded, createRuleServer
throws unless it is given token or allowOpen: true.
curl -s localhost:8080/evaluations -H "Authorization: Bearer $RULE_SERVER_TOKEN" -H 'Content-Type: application/json' -d '{
"ruleset": "acme.payments.transfer", "entity": "Transfer", "operation": "create",
"data": {"type": "international", "amount": 500, "currency": "USD", "memo": "gift",
"beneficiary": {"name": "X", "country": "KP", "swiftCode": "ABCDKPPY"}}}'
# {"decision":"deny","findings":[{"code":"ORG-TRF-001","message":"Transfers to KP are not permitted.", ...}], ...}The request body is the evaluation request of the specification plus ruleset, and optionally
version and channel. The body is checked before the ruleset is looked up or anything is
evaluated, with the shape check every runtime applies (specification section 8). A malformed body is
answered with 400 and a problem document whose title says what is wrong, for example
'view' must be an object; an unknown ruleset or version is 404. Optional members may be null;
a null channel is the default channel, server. A body with a number too large for a double
(1e999) is refused with 400 as well. A body larger than 1 MiB (maxBodyBytes when embedded) is
413, and a path with malformed percent-encoding is 400.
trigger, view, resolutions and locale are passed to the runtime as given. A view narrows the evaluation to one place in the user interface, and a finding reports the
place its rule names as location:
curl -s localhost:8080/evaluations -H "Authorization: Bearer $RULE_SERVER_TOKEN" -H 'Content-Type: application/json' -d '{
"ruleset": "acme.payments.transfer", "entity": "Transfer", "operation": "create",
"view": {"component": "beneficiary-panel"},
"data": {"type": "international", "amount": 500, "currency": "USD", "memo": "gift",
"beneficiary": {"name": "X", "country": "ES"}}}'
# {"decision":"deny","findings":[{"code":"PAY-TRF-002", ..., "location":{"component":"beneficiary-panel"}}],"effects":[],"commands":[]}Without the view the same request also returns the fee that a rule of the amount-panel computes.
A runtime without a compiler downloads the bundle and evaluates it in process:
curl -s -H "Authorization: Bearer $RULE_SERVER_TOKEN" localhost:8080/rulesets/acme.payments.transfer/bundle -o transfer.bundle.json
# ETag: "sha256:c192...2c7e-bundle"; send it back as If-None-Match and the answer is 304 until the ruleset changesCustom operators
A ruleset may use custom operators (x-*), which are functions of the host application. The
rule-cascade-server command registers none: it serves such a ruleset, logs a warning that names the
missing operators at start-up and after every reload, and the rules that need them fail closed (the
evaluation denies with a RULE-EVALUATION-ERROR finding).
To supply operators, embed the server in a program of your own:
import { createRuleServer, RuleStore } from '@yarlisaisolutions/rule-cascade-server';
const store = new RuleStore('./rules');
const { server, missingOperators } = createRuleServer(store, {
token: process.env.RULE_SERVER_TOKEN, // required; createRuleServer throws without it unless allowOpen: true
operators: { 'x-luhn': (text) => typeof text === 'string' && luhn(text) },
});
missingOperators(); // {} when everything is registered, otherwise { '<ruleset id>': ['x-...'] }
server.listen(8080);An operator receives plain JSON values and returns a JSON value. It must be synchronous and pure. Every runtime that evaluates the ruleset needs the same operators: a browser that evaluates the client manifest registers them too.
Behaviour that matters in production
- Fails fast. If any ruleset fails a load check, or any bundle is unusable, the process exits before it listens.
- Fails closed. Without
RULE_SERVER_TOKEN(or the explicitRULE_SERVER_ALLOW_OPEN=1) the process exits1. - Scheduled refresh. With
RULES_REFRESH=intervalorcronthe server also checks the directory on a schedule and reloads, with the same safety, only when a file changed. Reloads and refusals are logged with theirtrigger; unchanged checks are not. - Safe reload.
SIGHUPreloads the directory. If the new rules fail to load, the server logsreload refused, still serving the previous ruleswith the reason and keeps serving the previous ones;/readyzkeeps reporting the previousloadedAt. - Graceful shutdown. On
SIGTERM(orSIGINT) readiness turns503at once; afterDRAIN_SECONDSthe server stops accepting connections, in-flight requests finish, and the process exits0. - Cacheable manifests and bundles. The ETag is the ruleset checksum plus the channel, or plus
bundle; revalidation returns304.If-None-Matchmay list several entity tags, use weak validators (W/"..."), or be*. Client manifests follow the cache policy; server manifests and bundles are alwaysprivate, no-cache. By default the client manifest is sent withCache-Control: public, max-age=0, must-revalidate, so every use is revalidated. A browser that usescreateManifestClientof the runtime keeps evaluating its last manifest while this endpoint is unreachable; the evaluation on the server remains the decision. - Checksum URLs.
GET /rulesets/{id}/manifest?channel=client&checksum=sha256:...answers the manifest only when that is the checksum being served (inpermanentmode withCache-Control: public, max-age=31536000, immutable), and409withCache-Control: no-storeand the servedchecksumin the problem body otherwise. This parameter is an extension of this server; it is not in the OpenAPI description. - Worker threads. With
RULE_SERVER_WORKERS=Nevaluations run on N worker threads with the same results; one that outlivesRULE_EVALUATION_TIMEOUT_MSis answered503and its worker replaced. Embedded, passworkers,evaluationTimeoutMsand, for custom operators,operatorsModule(an ES module exporting them) tocreateRuleServer, and callterminateWorkers()on shutdown. - Structured logs. One JSON line per request; health probes are not logged.
Container and Kubernetes files are in Dockerfile (packages/server/Dockerfile) and deploy/kubernetes.
rule-cascade (Python)
The Rule Cascade runtime for Python, and the reference implementation of the specification. It implements both conformance levels: it reads a bundle and evaluates (evaluator), and it loads source documents and produces bundles (compiler).
Agent tools
A dispatcher that runs the three tools of bindings/llm-tools.json against the rule server, so an AI agent checks an operation before it calls the API that performs it.