Add custom rules
You add your own rules to a decision: first with the in-process compile_custom_rule(), then with regex rules in a playbook that you compile into your own pack. The starting point is the built-in example pack (data protection), which knows regulated data such as SSNs and card numbers but not your internal identifiers.
About the example
This tutorial uses the built-in data-protection pack so you can run everything without writing a pack first. The same calls work for any decision pack: eligibility, limits, routing and so on. See Core concepts.
What you'll build#
- A check that shows how
compile_custom_rule()behaves on the Community tier. - A playbook,
internal_ids.yaml, with two regex rules: block internal project codes such asPROJ-ZEUS-9X, and flag ticket numbers such asTCK-004211. - A compiled pack,
internal_ids.hxpack, that you run and serve withhelixor-pack.
Prerequisites#
- The runtime installed, which includes the
helixor-packcommand. See Installation. - Your Developer license at
~/.helixor/helixor.lic. See Licensing. Check it withhelixor-pack inspect-license: it must listcustom_playbook_compilationunder features. - Read Playbooks for what a compiled pack executes in 0.2.1.
Steps#
- Try
compile_custom_rule()on CommunityThe built-in example pack is static on the Community tier. Asking it to learn a rule raises
CommunityEditionRestrictionError:from helixor_runtime import CommunityEditionRestrictionError, HelixorEngine engine = HelixorEngine() print("tier:", engine.tier) text = "Status for PROJ-ZEUS-9X is green." print("before:", engine.evaluate(text).action) try: engine.compile_custom_rule("Block any text containing 'PROJ-ZEUS'") except CommunityEditionRestrictionError as exc: print(f"{type(exc).__name__}: {exc}")tier: COMMUNITY before: permit_clean_payload CommunityEditionRestrictionError: Cannot compile custom policies in Free Community Edition. The bundled regulatory PII artifact is static and perpetual. To compile custom domain rules from natural language (e.g. internal customer IDs, loyalty numbers, or specialized regex), register for free at https://helixor.ai/register.
The exception class is exported from
helixor_runtime, like every runtime error, and derives fromHelixorRuntimeError. - Know what
compile_custom_rule()actually matchesDespite the wording of that message, the instruction is not interpreted as natural language or as a regular expression. On a Developer engine the call does this:
- It takes the first quoted string in the instruction,
'PROJ-ZEUS'above. If there is no quoted string, it takes the last word. - It adds a fatal rule with ID
RULE-CUSTOM-<SLUG>(hereRULE-CUSTOM-PROJ_ZEUS) and actionblock_custom_rule_<slug>(hereblock_custom_rule_proj_zeus). - It matches by case-sensitive substring.
PROJ-ZEUS-9Xmatches;proj-zeusdoes not."Block internal ticket numbers like TCK"matches any text containingTCK.
Every rule runs on every payload, and the first fatal match decides the action. Text with
PROJ-ZEUSand an email address returnsblock_custom_rule_proj_zeus, with both rules intriggers.Known issues in 0.2.1
- The public API has no engine on which
compile_custom_rule()succeeds.HelixorEngine()is Community and raisesCommunityEditionRestrictionError. An engine fromHelixorEngine.load_pack()runs a sealed pack whose rules are fixed when it is compiled, so the call raisesEnclaveCapabilityError. - A custom-rule match is never redacted:
remedy.clean_textstill contains it.
Put the rules you depend on in a playbook, below, and compile it into a pack.
Fixed in the next release:
compile_custom_rule()is removed fromHelixorEngine, so this step changes. On every engine the call raisesRemovedCapabilityError(exported fromhelixor_runtime, and anEnclaveCapabilityError) with this message:RemovedCapabilityError: HelixorEngine.compile_custom_rule was removed: no public engine could run it (the Community enclave is static and a sealed .hxpack is immutable). Declare the rule in the playbook's `rules:` section (type regex or luhn_checksum, action block or redact) and compile it with `helixor-pack compile`; the pack engine reports and redacts its matches.
- It takes the first quoted string in the instruction,
- Write a playbook with regex rules
A
rulesentry withtype: regexis compiled into your pack and runs on every payload, next to the built-in checks. The returned action isblock_<id>foraction: blockandredact_<id>foraction: redact, so choose IDs that read well as actions and list those actions for reviewers. Any otheractionortype, or a pattern that is not a valid regular expression, fails compilation withUnsupportedPackDeclarationError.pack_id: custom.internal_ids.v1 version: 1.0.0 name: Internal identifier guard kind: internal_policy description: "Blocks internal project codes and flags ticket numbers." actions: - permit_clean_payload - block_internal_project_code - redact_ticket_id - block_glba_ssn_leakage - block_pci_dss_pan_leakage - block_hipaa_phi_leakage - redact_and_permit_contact_pii rules: - id: internal_project_code type: regex pattern: '\bPROJ-[A-Z]+-\d+[A-Z]?\b' action: block - id: ticket_id type: regex pattern: '\bTCK-\d{6}\b' action: redact default_action: permit_clean_payloadactionslists every outcome the pack can return, including the five of the built-in checks that run in every compiled pack. 0.2.1 does not check the list; the next release rejects a list that leaves one out. Quote regex patterns with single quotes so YAML does not interpret the backslashes, and quote anydescriptionthat contains a colon followed by a space; otherwise the file does not parse. - Compile it with your Developer license
helixor-pack compile --playbook internal_ids.yaml \ --license ~/.helixor/helixor.lic \ --out internal_ids.hxpack
Compiling playbook 'internal_ids.yaml' locally... Successfully compiled sealed binary pack (873 bytes): internal_ids.hxpack
The size depends on your license and the runtime version.
The pack is encrypted to your license: it only unseals with the license it was compiled with. The size varies with the playbook.
- Run it
helixor-pack rununseals the pack in memory, evaluates one text and prints the full decision as JSON.helixor-pack run --pack internal_ids.hxpack \ --license ~/.helixor/helixor.lic \ --text "Status for PROJ-ZEUS-9X is green."
{ "pack_id": "custom.internal_ids.v1", "action": "block_internal_project_code", "invariants_passed": false, "reason": "Pack rule 'internal_project_code' triggered (block)", "triggers": [ { "rule_id": "internal_project_code", "law": "internal_policy", "severity": "FATAL", "matched_items": [ "PROJ-ZEUS-9X" ] } ], "remedy": { "clean_text": "[BLOCKED: Prohibited Content]", "redactions_count": 0, "redacted_categories": [] }, "latency_us": 43.83, "tokens_spent": 0, "egress_bytes": 0, "receipt_hash": "hx_proof_e2bfe6bb7f774b235dca3d4a", "license_status": "ACTIVE", "active_key_id": "ep1_2026", "active_epoch": 1, "edition": "DEVELOPER", "upgrade_notice": null }Now run it against four inputs.
summarize.pyprints the action, rule IDs andclean_textfrom each result:import json import sys d = json.load(sys.stdin) print(f" {d['action']} {[t['rule_id'] for t in d['triggers']]} {d['remedy']['clean_text']!r}")for text in "Status for PROJ-ZEUS-9X is green." \ "See TCK-004211 for the fix." \ "Email ops@example.com about PROJ-ZEUS-9X." \ "Lunch is at noon."; do echo "$text" helixor-pack run --pack internal_ids.hxpack --license ~/.helixor/helixor.lic --text "$text" \ | python summarize.py doneStatus for PROJ-ZEUS-9X is green. block_internal_project_code ['internal_project_code'] '[BLOCKED: Prohibited Content]' See TCK-004211 for the fix. redact_ticket_id ['ticket_id'] 'See [REDACTED_TICKET_ID] for the fix.' Email ops@example.com about PROJ-ZEUS-9X. block_internal_project_code ['internal_project_code', 'RULE-GDPR-EMAIL-REDACT'] 'Email [REDACTED_EMAIL] about PROJ-ZEUS-9X.' Lunch is at noon. permit_clean_payload [] 'Lunch is at noon.'
Read the results: the redact rule replaces the ticket number with
[REDACTED_TICKET_ID]. In the third input both your rule and the built-in email check fire; both are reported, and your fatal rule decides the action. The result carries your license state:editionisDEVELOPERandlicense_statusisACTIVE.Known issues in 0.2.1
- A blocked value can stay in
clean_text. A block rule replaces the whole text with[BLOCKED: Prohibited Content]only when nothing else was redacted. In the third input the email is redacted, soclean_textstill containsPROJ-ZEUS-9X. Never forwardclean_textfrom a result whose action starts withblock. - Receipts from a compiled pack hash
clean_text, not the input. Every input that a block rule replaces with[BLOCKED: Prohibited Content]gets the same receipt. Log your own request ID next to it.
Fixed in the next release. Any fired block rule withholds the whole text, so the third input's
clean_textis[BLOCKED: Prohibited Content]. Receipts hash the input with the pack version, so each input gets its own; the values differ from the ones above. See Receipts. - A blocked value can stay in
- Serve it
To call the pack from other processes, serve it on loopback. The HTTP and WebSocket clients from Serve decisions to other languages work unchanged.
helixor-pack serve --pack internal_ids.hxpack \ --license ~/.helixor/helixor.lic \ --host 127.0.0.1 --port 18734
To use the pack in your own Python process instead, load it with
HelixorEngine.load_pack("internal_ids.hxpack", license_file=...)and checkengine.pack_idbefore you take traffic.
How it works#
helixor-pack compile parses the playbook, checks that your license is entitled to compile it, and encrypts the compiled form with AES-256-GCM using a key from your license. The pack ID and license ID are bound into the encryption, so the pack will not unseal with another license. At run time the CLI verifies the license signature and dates, unseals the pack in memory, and evaluates your rules and the example pack's built-in checks. Every check runs and every match is reported in triggers. The first fatal match decides the action, with your rules checked before the built-in ones; if nothing fatal matched, the first warning decides.
From your playbook, rules of type regex or luhn_checksum execute. The compiler rejects any codons or hard_rules other than the example pack's built-in ones with UnsupportedPackDeclarationError, rather than sealing declarations that would never run; see Playbooks and the playbook schema. Patterns run on every payload with no timeout, so avoid nested quantifiers.
If the CLI cannot find a license it exits with status 1. Without --license it looks for ./helixor.lic, then ~/.helixor/helixor.lic. The flags are in the CLI reference, and the error types in Errors.