helixordevelopers

Errors and failure modes

What can go wrong, what the runtime raises when it does, and which failures stop a payload (fail closed) versus let it through (fail open). Read the fail-open section before you go to production.

Runtime 0.2.1

Catching runtime exceptions#

Every error the runtime raises derives from HelixorRuntimeError, and the classes on this page are exported from helixor_runtime, except the two subclasses LicenseRevokedAuthorityError and PlaybookParseError: catch their parents, LicenseSignatureInvalidError and CompilerError. Catch the base class around every evaluation, and never forward a payload that was not evaluated.

from helixor_runtime import DeveloperQuotaExceededError, HelixorRuntimeError

try:
    result = engine.evaluate(text)
except DeveloperQuotaExceededError:
    raise PayloadRejected("rate limited")      # back off and retry later
except HelixorRuntimeError as exc:
    log.error("helixor evaluation failed: %s", type(exc).__name__)
    raise PayloadRejected(type(exc).__name__) from exc   # never forward the payload

The exceptions that do not derive from it are FileNotFoundError and ValueError from load_pack() for a bad path or argument, and NativeRuntimeUnavailableError from the generated SDK client, which is not exported either. Some classes also derive from a built-in type for compatibility: StreamBlockedError from RuntimeError, UnsupportedPackDeclarationError from ValueError. In the next release: RemovedCapabilityError also from AttributeError, DecisionBudgetExceededError from RuntimeError, BeliefSnapshotError from ValueError.

Evaluation#

ExceptionRaised whenWhat to do
CommunityEditionRestrictionErrorcompile_custom_rule() on the Community license.Use a playbook with rules and a compiled pack; see Playbook schema.
EnclaveCapabilityErrorcompile_custom_rule() on an engine from load_pack(): a sealed pack's rules are fixed.Add the rule to the playbook and compile again.
RemovedCapabilityError (next release)compile_custom_rule() on any engine: the method is removed from HelixorEngine. An EnclaveCapabilityError that is also an AttributeError, so hasattr(engine, "compile_custom_rule") is False. The message says to declare the rule in the playbook.Add the rule to the playbook's rules and compile it.
CodonRuntimeErrorevaluate() got a dict with no text, message or payload key, a value under that key that is not a string, or a payload that is neither a string nor a dict.Pass a string.
AmbiguousPayloadError (next release)evaluate() got a dict with more than one key. A CodonRuntimeError; its keys attribute lists the dict's keys, sorted.Pass a string, or a dict with exactly one text key.
DecisionBudgetExceededError (next release)A compiled pack's decision took longer than its budget.max_latency_ms. The result is withheld. Attributes pack_id, budget_key, limit, measured.Treat the payload as not evaluated.
LicenseExpiredErrorA compiled pack's license passed its grace period while the engine was running.Deploy a renewed license with packs compiled for it.
DeveloperQuotaExceededErrorA Developer license's per-minute rate (a sliding 60-second window) or lifetime evaluation quota is exceeded, and the key's enforcement mode is fail-closed. In 0.2.1 only the built-in example pack enforces the limits. In the next release a compiled pack enforces the license's max_decisions_per_minute and max_decisions_lifetime too: past a limit it raises this error unless the license's overage policy is TRUE_UP, which records the overage and keeps deciding.Back off, or move to a license without limits. Treat the payload as not evaluated.
KeyInvalidErrorA license key string is malformed or its signature does not match.Check the key you were issued.
KeyExpiredErrorEvery key for the pack is past its expiry plus grace period.Install the renewal.
KeyNotFoundErrorNo key covers the pack.Check which pack the key was issued for.
StreamBlockedErrorstream_filter or astream_filter hit a fatal match with block_on_fatal=True. Message: Stream blocked by statutory invariant: <reason> (<action>); attributes action and reason.Stop sending the response; see Streaming.

evaluate() never raises because of what a string says. Empty text evaluates as clean.

Licenses#

All license file errors share the base class LicenseError. See License file for the checks.

ExceptionRaised whenExample message
LicenseSignatureInvalidErrorThe signature is missing, or does not verify because any signed field was changed.License digital signature verification failed: …
LicenseRevokedAuthorityError (a LicenseSignatureInvalidError)The license was signed by a revoked authority key. 0.2.1 rotated the license authority, so licenses issued for 0.2.0 are rejected.License was signed by revoked authority key … Request a reissued license signed by the current authority.
LicenseNotYetValidErrorThe current time is before valid_from.License is not yet valid (starts at …).
LicenseExpiredErrorThe current time is after valid_until plus the grace period.License expired on YYYY-MM-DD and grace period has elapsed.
LicenseEntitlementErrorThe license does not allow the pack (at compile or load) or a solver the pack declares.License '…' (DEVELOPER) is not entitled to execute pack '…'.

A license file that is not valid JSON, or is missing a section such as identity, fails with a JSON or key error rather than a LicenseError. Treat any error while loading a license as fatal.

Packs and compilation#

ExceptionRaised when
FileNotFoundErrorThe .hxpack or license path passed to load_pack() does not exist (Policy pack not found: …, License file not found: …).
UnsupportedPackDeclarationErrorAt compile or load time: the playbook declares codons or hard rules other than the built-in ones, a rule type other than regex or luhn_checksum, a rule action other than block or redact, a rule without id or pattern, or an invalid regular expression. The message lists every problem. At load time it surfaces as a PackUnsealError.
PackUnsealError Invalid .hxpack: file too small to contain valid header.
Invalid pack container magic bytes: …
Unsupported pack format version: …
Decryption failed: content key mismatch or corrupted pack envelope (the wrong license, a renewed license, or a modified file)
Integrity check failed: AST digest mismatch.
No license file provided. Specify license_file or set HELIXOR_LICENSE_FILE.
CompilerErrorThe playbook is not valid YAML (often an unquoted ": "), its root is not a mapping, pack_id is missing, or its solver declarations cannot be read (PlaybookParseError). With --remote: the authority returned an HTTP error, or could not be reached.

The CLI turns each of these into one line on standard error and exit code 1; see CLI.

HTTP status codes#

StatusCause
200Evaluated. Check action and trigger severities; a 200 can be a block.
400Unknown domain in POST /v1/decision, or a request with license_key to a service that serves a compiled pack.
401The service has a bearer token and the request did not carry it. GET /v1/health never needs it.
403license_data failed verification, or the license does not allow the pack or solver.
422The request body failed validation.
500An unhandled error, for example an invalid license_key sent to a service running the built-in pack (a key error).

WebSocket errors arrive as {"type": "error", "message": "…"}. An error inside a handler also closes the connection. Without the bearer token, the connection is closed with code 1008. See HTTP API.

Fails closed#

These checks stop processing with an error. No partial result is returned.

  • A license signature that is missing or invalid, or a license that is not yet valid or is past its grace period.
  • A pack the license does not allow, or a declared solver the license does not allow.
  • A pack sealed for a different license, a modified pack, or a failed integrity check.
  • A fatal match in stream_filter / astream_filter (raises) or in a streaming session (blocked chunk, nothing further emitted).
  • Developer quota or rate exceeded, when the key's enforcement mode is fail-closed.
  • load_pack() with a missing pack or license file.
  • A playbook or pack that declares something the engine does not execute, and a pack rule whose condition cannot be evaluated.
  • A dict payload the engine cannot read.
  • A service started on a non-loopback address without a token, or with a wildcard CORS origin.

Known fail-open paths in 0.2.1#

In these cases the runtime does not raise, and a payload or text can get through. Guard against each one in your integration. 0.2.1 closed the other paths listed for 0.2.0: load_pack with a missing file, a contact match overriding a custom block, compiled-pack rules skipped when a built-in check matched, invalid or unsupported rules skipped silently, redact rules that did not redact, license_key bypassing a served pack, and damaged text from short streaming chunks.

PathWhat happensGuard
WebSocket stream after a blocked tokenThe next stream_chunk starts a new session, and text after the fatal value is streamed back. Fixed in the next release: later chunks are refused with STREAM_BLOCKED.Close the connection on blocked: true.
remedy.clean_text of a compiled-pack blockWhen another rule redacted something, the blocked value stays in clean_text. Fixed in the next release: any fired block rule withholds the whole text.Never forward clean_text when the action starts with block or any trigger is FATAL.
A dict with several text fieldsOnly the first of text, message, payload that is present is evaluated; the others pass unchecked. Fixed in the next release: AmbiguousPayloadError.Evaluate each field, or pass a string.
A pack rules pattern with capturing groupsOnly the first group's text is reported and redacted. Fixed in the next release: the whole match is reported and redacted.Use non-capturing groups, (?:...).
invariants, remedies, default_action, actions and budget in a compiled packAccepted and stored, but not executed or enforced. In the next release actions, default_action and budget are enforced (see Playbook schema); invariants and remedies are still accepted and not executed.Express checks as rules; accept only the actions you declared.
A service with no tokenAny process that can reach the port can call it.Keep it on loopback, or set HELIXOR_SERVICE_TOKEN.

For how to design around these, see Reliability and Troubleshooting.