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.
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#
| Exception | Raised when | What to do |
|---|---|---|
CommunityEditionRestrictionError | compile_custom_rule() on the Community license. | Use a playbook with rules and a compiled pack; see Playbook schema. |
EnclaveCapabilityError | compile_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. |
CodonRuntimeError | evaluate() 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. |
LicenseExpiredError | A compiled pack's license passed its grace period while the engine was running. | Deploy a renewed license with packs compiled for it. |
DeveloperQuotaExceededError | A 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. |
KeyInvalidError | A license key string is malformed or its signature does not match. | Check the key you were issued. |
KeyExpiredError | Every key for the pack is past its expiry plus grace period. | Install the renewal. |
KeyNotFoundError | No key covers the pack. | Check which pack the key was issued for. |
StreamBlockedError | stream_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.
| Exception | Raised when | Example message |
|---|---|---|
LicenseSignatureInvalidError | The 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. |
LicenseNotYetValidError | The current time is before valid_from. | License is not yet valid (starts at …). |
LicenseExpiredError | The current time is after valid_until plus the grace period. | License expired on YYYY-MM-DD and grace period has elapsed. |
LicenseEntitlementError | The 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#
| Exception | Raised when |
|---|---|
FileNotFoundError | The .hxpack or license path passed to load_pack() does not exist (Policy pack not found: …, License file not found: …). |
UnsupportedPackDeclarationError | At 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. |
CompilerError | The 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#
| Status | Cause |
|---|---|
| 200 | Evaluated. Check action and trigger severities; a 200 can be a block. |
| 400 | Unknown domain in POST /v1/decision, or a request with license_key to a service that serves a compiled pack. |
| 401 | The service has a bearer token and the request did not carry it. GET /v1/health never needs it. |
| 403 | license_data failed verification, or the license does not allow the pack or solver. |
| 422 | The request body failed validation. |
| 500 | An 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.
| Path | What happens | Guard |
|---|---|---|
| WebSocket stream after a blocked token | The 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 block | When 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 fields | Only 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 groups | Only 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 pack | Accepted 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 token | Any 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.