app.engine.dsl_validator module

Structural validator for strategy ASTs.

Runs synchronously and without I/O. Called on every create/update so a malformed AST never lands in the database, and called again by the simulate service as a cheap idempotent guard - the second call is fast because the AST has already been parsed by Pydantic into plain dict/list/ scalar values.

The validator enforces three things, in order:

  1. Shape: every node has the required keys with the expected types (no surprise dict where a literal was expected, no missing when on a rule).

  2. Whitelist: compare.op, arith.op, field.path, and node.type must all appear in the corresponding allow-list in dsl_ast. The interpreter never invokes getattr on a node type string, but the validator is the first line of defence - by the time the AST reaches the handler table any unknown name has already been rejected with DslValidationError.

  3. Limits: a static node count and recursion depth are computed while walking. Both are bounded by configs.DSL_MAX_NODES and configs.DSL_MAX_DEPTH respectively, so an attacker can’t smuggle in a billion-node tree that would otherwise OOM the API.

Auto-assigned IDs: nodes are allowed to omit id (Blockly will provide them; hand-written JSON often skips them). The validator assigns a deterministic "<parent_id>.<type>.<index>" slug so the interpreter trace and any future error messages have a stable correlation key.

app.engine.dsl_validator.validate_ast(ast, *, max_depth=None, max_nodes=None)[source]

Validate ast in place (mutates only to fill in missing id fields).

Returns the same dict reference for convenience so callers can write ast = validate_ast(ast). Raises DslValidationError with a descriptive detail on any structural failure.

The limits default to configs.DSL_* but tests can override them to exercise the rejection paths without sending a megabyte of JSON.

Parameters:
  • ast (Any)

  • max_depth (int | None)

  • max_nodes (int | None)

Return type:

Dict[str, Any]