Compile rules

The compile rules decide what an expression may contain: for each of the ten binary operators, whether an expression may use the operator as Python does, may not use the operator at all, or has the operator 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 only 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 GUI 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 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, and put the function on the allowlist under the name you enter, on every agent server.

The function must keep the operator's shape: two arguments, the left operand and the right operand, both numbers. A function that takes one argument cannot stand in for an operator. When the settings are saved, the agent server checks each named function: the function must be on the allowlist, and must accept two numbers. A function that fails the check refuses the save, and the reason is shown. Matrix multiplication, @, is not checked.

Rewriting is how an expensive operation is kept within bounds. 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 bounds 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, the validation pass 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 the agent was published under. A changed rule reaches that agent only when a developer creates a draft from the agent and publishes the draft again.