Configuration
rules.svc boots from one YAML document, like every platform block. The document says where the
service listens, how it checks tokens, which tenants it serves and where each one’s product comes
from, and, under rules:, how the service bounds what the rules do. What rules a tenant runs is
not configuration: it is the product’s rules/ folder, read through meta.
Where each setting lives
Section titled “Where each setting lives”| Layer | What goes there | Owner |
|---|---|---|
| Boot document | listener, TLS, token issuers, the tenants served, the rules: block | the operator |
| Tenant node | the tenant’s meta source, its data.svc endpoint, rules.svc’s own pools | the operator |
The product’s rules/ folder | the rules, vocabulary files, static scopes in rules/rules.yaml | the product team |
The product’s rules/extend/access.yaml | who may run, evaluate, apply and author the rules | the product team |
| The request | the scope to run and the facts | the caller |
The boot document is .rules.yaml in the working directory unless -f (or --config) names
another, or comes from config.svc.
The boot document
Section titled “The boot document”name: ruleshost: 0.0.0.0port: 8094zerotrust: truesvccert: /etc/kisai/rules.crtsvckey: /etc/kisai/rules.keylog: level: infojwks: issuers: iam: "https://iam.example.com"
rules: # optional: every key has a default exec: timeout: 10s maxtimeout: 30s maxcycles: 5000 systemuser: # needed for data sources and background work identity: rules-system password: "vault://…" sweeps: enabled: false events: enabled: false secret: "vault://…"
hives: cluster: - path: "" tenants: # only these tenants are served - "acme:prod:erp:eu1" tenant: - path: "acme:prod:erp:eu1" name: eu1 active: true meta: url: "https://meta.example.com" data: "https://data.example.com" # the tenant's data.svc, for `data` sources datastores: main: # the product's data, for `seshat` sources dbpool: main rules: # rules.svc's own records: sweeps dbpool: rules dbpools: main: type: postgres dbserver: db.internal:5432 database: erp dbuser: rules_reader # a read-only role is enough password: "vault://…" schema: acme_prod_erp_eu1 # data.svc's schema for the tenant rules: type: postgres dbserver: db.internal:5432 database: erp dbuser: rules_owner password: "vault://…" schema: rules_acme_prod_erp_eu1The keys outside rules: are the ones every platform block reads: name, host, port,
zerotrust with svccert and svckey (TLS, on by default), log.level, jwks (the issuers whose
tokens are accepted), timeouts (read, write, readheader, shutdown), hives and
loadtenants (tenants warmed at boot).
The rules: block
Section titled “The rules: block”A closed key set. A key rules.svc does not read refuses boot, naming it (rules.svc does not read rules.exec.maxcycle), so a misspelt key never boots as its default. A cluster-configuration change
is checked the same way; a refused document changes nothing and the one in place stays.
| Key | Default | Meaning |
|---|---|---|
rules.exec.timeout | 10s | The longest one execution may run: a duration (10s) or seconds (10). Past it the call answers 504 rules-execution-timeout. A static scope’s limits.timeout replaces it for that scope |
rules.exec.maxtimeout | 30s | The ceiling on a static scope’s limits.timeout: a longer one is cut to it. An exec.timeout above it refuses boot |
rules.exec.maxcycles | 5000 | The most cycles one execution runs, and the ceiling on a static scope’s limits.maxcycles. An execution that needs more fails with 422 rules-execution-failed |
rules.evaluate.maxids | 100 | The most records one POST /rules/evaluate takes |
rules.body.maxbytes | 1048576 | The largest request body; a larger one answers 413 rules-body-too-large. A watched run’s draft has its own cap (4 MiB) |
rules.systemuser.identity, rules.systemuser.password | none | The system user, below. Both or neither |
rules.sweeps.enabled | false | Sweeps are off until an operator turns them on; a start answers 409 rules-sweeps-disabled until then |
rules.sweeps.pertenant | 1 | Sweeps a tenant may have queued or running at once; one more answers 429 rules-sweep-limit |
rules.sweeps.maxerrors | 100 | A sweep stops, failed, after this many records failed |
rules.sweeps.pagesize | 500 | Records a sweep reads, decides and records at a time |
rules.sweeps.retention | 720h | How long finished sweeps and what they recorded are kept; older ones are purged hourly |
rules.events.enabled | false | Takes data.svc’s deliveries at POST /rules/events; while off, that route answers 404 |
rules.events.secret | none | The secret data.svc signs each delivery with. enabled needs it |
A watched run (POST /rules/run) keeps its own bounds whatever the block says: 10 seconds and 2000
cycles.
The system user
Section titled “The system user”Some work has no caller to run as: a sweep after it starts, an evaluation a data change triggers,
and the checks rules.svc makes of a data source when a tenant’s rules load. That work runs as the
system user: a normal user of each tenant, created in iam with a password and no second
factor, whose identity and password rules.systemuser holds. rules.svc logs in as it at the
tenant’s iam, keeps the token until shortly before it expires, and logs in again.
- Its privileges are granted like any user’s and bound what background work can do. A sweep checks them again before every page, so revoking one stops the sweep.
- A product whose scopes read a
datasource does not load without a system user. Sweeps and events act as it, so they cannot run without one either. - Keep its password in vault (
vault://…), as for any credential in the document.
Tenants
Section titled “Tenants”rules.svc serves the tenants listed under the cluster node’s tenants:. For each, it builds a meta
client from the tenant node (meta.url, or meta.localdir for a product checked out on disk),
loads the product’s rules/ folder, and watches it.
| Tenant node key | Read for |
|---|---|
meta.url, meta.localdir | where the product, and so its rules/ folder, comes from |
data | the tenant’s data.svc, for scopes with a data source. When the tenant names none, rules.svc’s own data key is used |
datastores.<name> with dbpools.<pool> | for scopes with a seshat source: rules.svc’s own read pool on the tenant’s product data, naming data.svc’s schema for the tenant. A read-only database role is enough: a seshat source only reads |
datastores.rules with its pool | rules.svc’s own records (sweeps and what they recorded). rules.svc migrates this schema itself, and only its own tables |
| State | Answer |
|---|---|
| The tenant is not configured | 400 rules-tenant-not-served |
| Configured, its first load not yet done | 503 rules-tenant-not-loaded, with Retry-After |
| Loaded | served; a later load that fails keeps what was served, and GET /rulesets says why |
Readiness lists each tenant as a dependency (tenant:<key>) without gating on it, so one tenant’s
broken folder never takes the instance out of rotation for the others. A tenant removed from the
configuration keeps its rules until the process restarts.
The product’s rules/ folder
Section titled “The product’s rules/ folder”rules/ rules.yaml static scopes (optional) extend/ access.yaml who may do what (optional; holds no rules) invoices/ a folder of rules, any depth route.grl limits/large.grl vocab.yaml vocabulary for this folder; a subfolder's lists override its parent's common/ currency.grlA call runs a static scope of rules/rules.yaml by name, or a top folder with the folders under
it. Scopes is the full reference for rules.yaml;
Access for extend/access.yaml.
A load checks the whole folder before serving any of it, and fails, naming the file (and the rule and line for a rule), when:
- a
.grlsits directly inrules/or inrules/extend/; rules.yamlhas an unknown key, a wrong type, a bad or repeated name, a path that is not there, or a declaration that does not hold (Scopes);extend/access.yamlhas an unknown action or key, or an empty expression;- a rule does not parse, or calls a method that does not exist or with the wrong number of arguments;
- a static scope’s source cannot be opened or does not offer what the scope does with it.
kis rules check runs the same checks offline, except opening sources, and prints the same
errors, so a broken folder is caught before it is pushed.
Sweeps and events
Section titled “Sweeps and events”Both are off by default.
- Sweeps need
rules.sweeps.enabled, a system user, and the tenant’srulesdatastore. See Sweeps. - Events need
rules.events.enabledandsecret, a system user, and data.svc delivering to rules.svc: anevents.targetsentry in data.svc’s configuration withurl: https://rules.example.com/rules/events, the samesecret, and the topics of the entities your scopes react to. See Events.
Reloads
Section titled “Reloads”| What changed | What happens |
|---|---|
A file under a tenant’s rules/ | that tenant’s rules load again; served whole when the load succeeds, kept as they were when it fails |
| The cluster configuration | the rules: block is checked and installed again (a refused one changes nothing); the tenants list is applied |
| A tenant node | the tenant’s lifecycle restarts on its new settings |
A sweep that is running keeps the rule text it started with; an edit mid-sweep does not change it.
What refuses boot
Section titled “What refuses boot”- A key under
rules:that rules.svc does not read, or a value it cannot take: anexec.timeoutorexec.maxtimeoutthat is not a positive duration, anexec.timeoutoverexec.maxtimeout, a count or asweeps.retentionthat is not positive,events.enabledwithoutevents.secret, a system user with only one of identity and password. - Everything that refuses boot for every platform block: no
port, certificates missing whilezerotrustis on.
Continue with
Section titled “Continue with”- The rules service: the routes and what they answer.
- Access: who may do what.
- Rules over data: sources, evaluate, sweeps and events.
- Troubleshooting.