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, RemovedCapabilityError also from AttributeError, DecisionBudgetExceededError from RuntimeError, BeliefSnapshotError from ValueError.
Evaluation#
| Exception | Raised when | What to do |
|---|---|---|
RemovedCapabilityError | compile_custom_rule() on any engine: the method is removed from HelixorEngine (0.3.0). 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. |
SalesEngineUnavailableError, code SALES_ENGINE_NOT_IN_RUNTIME | Code uses helixor_runtime.HelixorSalesEngine, including from helixor_runtime import HelixorSalesEngine. The sales engine is not part of the runtime (removed in 0.3.1; in 0.3.0 it always failed with FileNotFoundError). Also an ImportError. | Write the decision as a playbook, compile it with helixor-pack compile and load it with HelixorEngine.load_pack(). |
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 | 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 | 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. The built-in example pack and compiled packs both enforce the license's max_decisions_per_minute and max_decisions_lifetime: past a limit a compiled pack raises this error unless the license's overage policy is TRUE_UP, which records the overage and keeps deciding. (In 0.2.1 only the built-in pack enforced the limits.) | 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. The authority key used for 0.2.0 and early 0.2.1 builds is revoked, and since 0.3.0 the runtime rejects licenses it signed. | 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 '…'. |
LicenseNotFoundError (also a FileNotFoundError), code LICENSE_NOT_FOUND | No license in the search order, or an explicit path or HELIXOR_LICENSE_FILE names a missing file. New in 0.3.1; the message lists the locations checked and where to request a license. | LICENSE_NOT_FOUND: no Helixor license file was found. |
LicenseLocationAmbiguousError, code LICENSE_LOCATION_AMBIGUOUS | One search directory holds both helixor.hxlic and the older helixor.lic. New in 0.3.1. | LICENSE_LOCATION_AMBIGUOUS: both … and … exist; the runtime will not guess which license to use. |
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 passed to load_pack() does not exist (Policy pack not found: …). A missing license raises LicenseNotFoundError, which is also a FileNotFoundError. |
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.Changed in 0.3.1: a missing license raises LicenseNotFoundError, not PackUnsealError. |
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.
Licensing service: sign-up and issuance#
Planned Sign-up, registration requests and license issuance check your acceptance of the Helixor Software License Agreement. These errors come from the Helixor licensing service, not from the runtime.
| Status | Code | Cause | What to do |
|---|---|---|---|
| 422 | LICENSE_AGREEMENT_NOT_ACCEPTED | The request has no agreement_id or agreement_sha256, or the account has no recorded acceptance of the current agreement version when a license is issued or renewed. | Review the agreement and accept it: tick the checkbox, type the acceptance phrase, or pass --accept-license-agreement <sha256>. |
| 422 | LICENSE_AGREEMENT_MISMATCH | The agreement_id or agreement_sha256 is not the current agreement version, usually because a new version was published after you reviewed it. | Fetch GET /api/v1/legal/license-agreement, review the current text and accept it. |
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.3.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.3.0 closed these paths that were open in 0.2.1: a WebSocket stream continuing after a blocked token, a blocked value left in a compiled pack's clean_text, a dict with several text fields evaluated on one field, a capturing group reported and redacted instead of the whole match, and compiled packs ignoring actions, default_action and budget.
| Path | What happens | Guard |
|---|---|---|
invariants and remedies in a compiled pack | Accepted and stored, but not executed. (actions, default_action and budget are enforced; see Playbook schema.) | 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.