Compile rules

The compile rules decide what an expression may contain: for each of the ten binary operators, whether an expression may use it as Python does, may not use it at all, or has it replaced by a function of your own; how deep a comprehension may go; and the shapes of expression that are never allowed. Search2o Cloud applies the rules when a draft is validated or published. A changed rule does not touch an agent that is already published. Developers edit the rules in the UI under Guardrails.

Python operators

The operators are +, -, *, /, //, %, **, <<, >> and @. Each is set to one of three options.

  • Allow. The operator works as Python defines it. This is the default for every operator.
  • Deny. An expression that uses the operator fails validation, with the path of the command that holds the expression.
  • Rewrite. The operator is replaced by a call to a function you name. Choosing rewrite asks for the function name.

Rewriting an operator

Every use of a rewritten operator becomes a call to the named function with the operator's two operands: a ** b becomes bounded_pow(a, b). Write the function yourself, install the package on every agent server, and put the function on the allowlist under the name you enter.

The function keeps the operator's shape: two arguments, the left operand and the right operand. When the settings are saved, the agent server checks each named function: it must be on the allowlist, it must accept two arguments, and it must return a result for two sample numbers. If a function fails the check, the save is refused and the reason is shown. For matrix multiplication, @, only the two-argument shape is checked.

Rewriting is how an expensive operation is kept within limits. A function that stands in for ** can refuse an exponent above a limit, and a function that stands in for * can refuse a result that would not fit in 64 bits.

Comprehensions

A comprehension may be nested only so deep, and a single comprehension may have only so many for clauses. Both limits are settings below.

FieldTypeDefaultDescription
operatorsOperatorMappingModelWhich Python operators agent expressions may use, and how each is evaluated.
maxComprehensionDepthinteger4How deeply comprehensions may be nested in an expression.
maxGeneratorsInComprehensioninteger3How many 'for' clauses a single comprehension may have.

What validation refuses

Besides a denied operator, validation refuses these in any expression:

  • any character outside ASCII letters, digits, punctuation and the space;
  • import, in any form;
  • any name that starts with an underscore, whether a variable, an attribute, a function or a keyword argument, so no private or dunder members;
  • .format and .format_map on any value: the format-string mini-language reads attributes at run time, where the checks above cannot see them. An f-string does the same job and is checked like any expression. The check is on the attribute name alone and is made before anything runs, so a class of your own with a legitimate format method is refused too. To make such a method callable, bind it to a module-level name in your package, for example render_report = Report.format, add that name to the allowlist, and call render_report(report, ...);
  • lambda;
  • the assignment expression :=;
  • a call to anything other than a name or an attribute, such as (f)() or fs[0]();
  • a comprehension deeper, or with more for clauses, than the settings allow.

Each refusal is reported with the path of the command that holds the expression.

When a rule changes

The rules are applied to a draft, at validation and at publishing, and at no other time. A published agent keeps the rules it was published under. A changed rule reaches that agent only when a developer creates a draft from it and publishes the draft again.