helixordevelopers

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.

Tier 1 · Developer30 minutesIntermediate

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 as PROJ-ZEUS-9X, and flag ticket numbers such as TCK-004211.
  • A compiled pack, internal_ids.hxpack, that you run and serve with helixor-pack.

Prerequisites#

  • The runtime installed, which includes the helixor-pack command. See Installation.
  • Your Developer license at ~/.helixor/helixor.lic. See Licensing. Check it with helixor-pack inspect-license: it must list custom_playbook_compilation under features.
  • Read Playbooks for what a compiled pack executes in 0.2.1.

Steps#

  1. Try compile_custom_rule() on Community

    The 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 from HelixorRuntimeError.

  2. Know what compile_custom_rule() actually matches

    Despite 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:

    1. It takes the first quoted string in the instruction, 'PROJ-ZEUS' above. If there is no quoted string, it takes the last word.
    2. It adds a fatal rule with ID RULE-CUSTOM-<SLUG> (here RULE-CUSTOM-PROJ_ZEUS) and action block_custom_rule_<slug> (here block_custom_rule_proj_zeus).
    3. It matches by case-sensitive substring. PROJ-ZEUS-9X matches; proj-zeus does not. "Block internal ticket numbers like TCK" matches any text containing TCK.

    Every rule runs on every payload, and the first fatal match decides the action. Text with PROJ-ZEUS and an email address returns block_custom_rule_proj_zeus, with both rules in triggers.

    Known issues in 0.2.1

    • The public API has no engine on which compile_custom_rule() succeeds. HelixorEngine() is Community and raises CommunityEditionRestrictionError. An engine from HelixorEngine.load_pack() runs a sealed pack whose rules are fixed when it is compiled, so the call raises EnclaveCapabilityError.
    • A custom-rule match is never redacted: remedy.clean_text still 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 from HelixorEngine, so this step changes. On every engine the call raises RemovedCapabilityError (exported from helixor_runtime, and an EnclaveCapabilityError) 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.
    
  3. Write a playbook with regex rules

    A rules entry with type: regex is compiled into your pack and runs on every payload, next to the built-in checks. The returned action is block_<id> for action: block and redact_<id> for action: redact, so choose IDs that read well as actions and list those actions for reviewers. Any other action or type, or a pattern that is not a valid regular expression, fails compilation with UnsupportedPackDeclarationError.

    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_payload
    

    actions lists 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 any description that contains a colon followed by a space; otherwise the file does not parse.

  4. 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.

  5. Run it

    helixor-pack run unseals 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.py prints the action, rule IDs and clean_text from 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
    done
    
    Status 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: edition is DEVELOPER and license_status is ACTIVE.

    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, so clean_text still contains PROJ-ZEUS-9X. Never forward clean_text from a result whose action starts with block.
    • 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_text is [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.

  6. 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 check engine.pack_id before 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.

Next steps#