Skip to content

Custom policy DSL

Ampora's custom policies and tenant-specific lint rules use a small expression language designed to be readable, fail-closed, and non-Turing-complete.

Why a small DSL

  • Auditable: you can read a one-liner. You cannot easily read 200 lines of imperative Python.
  • Sandboxed: no IO, no recursion, no mutation. The interpreter cannot reach the network or disk.
  • Bounded: the recursive-descent parser caps nesting depth and the evaluator has a 50 ms wall-clock budget. A pathological expression cannot DoS the rollout engine.

Grammar (informal)

expr        := or_expr
or_expr     := and_expr ('or' and_expr)*
and_expr    := not_expr ('and' not_expr)*
not_expr    := 'not' not_expr | comparison
comparison  := value op value
            | value 'in' '(' value (',' value)* ')'
            | value 'matches' regex
            | value 'has-key' identifier
op          := '=' | '!=' | '<' | '<=' | '>' | '>='
value       := literal | path | function-call
literal     := number | string | boolean | null
path        := identifier ('.' identifier | '[' literal ']')*
function-call := identifier '(' value (',' value)* ')'

Variables you can reference

Inside a policy body, the available context is:

Reference Meaning
agent.description The agent's agent_description JSONB
agent.capabilities List of capability strings
agent.group The current group's name
config.yaml The Configuration's parsed AST as a tree
config.exporters Convenience: list of (alias, type, props) tuples
config.processors Same shape for processors
config.receivers Same shape for receivers
config.pipelines Map: signal → list of (receivers, processors, exporters)
tenant Current tenant name

Inside a lint rule body, only config.* is available — lint runs without an agent context.

Functions

A small set of safe functions:

Function What it does
len(x) Length of a string or list
lower(s), upper(s) Case conversion
starts-with(s, prefix), ends-with(s, suffix) String prefix/suffix tests
regex(pattern) A compiled regex; use with matches
count(list, condition) Count items in a list matching a sub-expression
any(list, condition), all(list, condition) Aggregates

Examples

"No exporter to a non-EU endpoint"

all (e in config.exporters where e.type = "otlp")
    where ends-with(e.props.endpoint, ".eu.example.com")

Reads as: "for every otlp exporter, the endpoint must end in .eu.example.com." A counter-example anywhere returns false, the policy denies.

"Hostmetrics interval ≥ 60s"

all (r in config.receivers where r.type = "hostmetrics")
    where r.props.collection_interval >= "60s"

"No debug exporter in production"

agent.description.environment = "prod"
    and any (e in config.exporters where e.type = "debug")
        => deny

Note the => deny — by default the engine reads the expression as "this must hold or deny"; the explicit => deny form is for the "must NOT hold" case.

"The agent must be tagged with service.name"

agent.description has-key "service.name"

"Pipelines must include batch"

all (p in config.pipelines["traces"])
    where any (proc in p.processors where proc.type = "batch")

Evaluation guarantees

  • Order: expressions are evaluated left-to-right, short-circuit on and / or.
  • Type: arithmetic on mismatched types returns the unit value and the expression evaluates to false.
  • Errors: a regex compilation error, a missing key in a strict context, etc. → policy denies (fail-closed).
  • Time: 50 ms hard ceiling on a single evaluation. Beyond → deny.

Testing your expression

The policy editor has an Evaluate panel where you can paste a sample agent + config JSON and see the result. Drafts also run in shadow mode against the live system; the audit log surfaces every "would-deny" so you can dry-run before activating.

Versioning

Policies are versioned. Saving a draft creates a new version; the approver sees a diff between the previous Active version and the new draft. Older versions remain in the audit history.

Beyond the DSL

If your governance need genuinely cannot be expressed in the DSL — e.g. it requires reaching out to an external service — file a feature request. We will consider extending the function set or, for very specific cases, adding a typed extension point. We do not intend to make the DSL Turing-complete.