1. Expanso + Jev
  2. Example 03 of 10

Jev decides how sensitive a record is. Expanso does the masking.

Every record gets sensitivity, PII and credential presence judged inline — restricted quarantines with an alert, the rest tokenize, mask, or pass through.

Where the record goes

Every stage on the left is Expanso, and it is deterministic. The record crosses to Jev once, for the one question a rule cannot answer, and comes straight back. Line numbers link to the YAML below.

SourcePOST /records
Expanso nodeone pipeline, 6 steps, deterministic
  1. Receive
  2. Shape
  3. Ask Jev
  4. Gate
  5. Mask
  6. Route
Jev judgment
  • sensitivity
  • contains_pii
  • contains_credentials
  • quarantine
  • tokenize
  • mask
  • pass
One record through this pipeline. Which decision each pass lands in cycles in order here; in the pipeline the gate picks it from Jev's answers.
  1. An HTTP server input accepts records on POST /records.

  2. Parses the POST body if it arrived as a string, then stamps received_at. It applies the same fixed rules every time, with no model involved; the timestamp is the one value that differs.

  3. Expanso packs the whole record and the typed questions into one request. Jev answers. If the call fails, a catch substitutes empty answers marked jev-unavailable, and the pipeline keeps going.

    • sensitivitychoice
    • contains_piinoul
    • contains_credentialsnoul
  4. Fixed thresholds over Jev’s answers choose one of four policies: quarantine, tokenize, mask, or pass.

  5. Applies the chosen policy to email, ssn and api_key: a SHA-256 hash to tokenize, the literal MASKED to mask, QUARANTINED for keys. The transformation itself involves no judgment.

  6. A switch output writes quarantined records to one file and everything else to another.

What Jev is asked

Jev, judgment

The pipeline sends the record with 3 typed questions. Jev answers each one with a value the pipeline can compare against a number.

  • sensitivitychoice

    What is the sensitivity class of this record?

    public · internal · confidential · restricted

  • contains_piinoul

    Does this record contain personally identifiable information?

  • contains_credentialsnoul

    Does this record contain passwords, API keys, tokens, or other secrets?

What Expanso does with the answers

Expanso, deterministic

Fixed thresholds, checked in order. The first rule that matches sets the route. These are the expressions in the pipeline, not a summary of them.

  1. $creds >= 0.7 || $sens == "restricted"quarantine · Likely credentials, or a restricted record.
  2. $sens == "confidential"tokenize · Confidential: hash the identifiers.
  3. $pii >= 0.5mask · Probably contains PII.
  4. elsepass · Forwarded unchanged.

If Jev is unreachable: pass

With no answers, sensitivity defaults to internal and both scores to 0, so the record is forwarded unmasked. As written, this example fails open. Change the defaults before relying on it.

The pipeline

This is the example's own pipeline file, unmodified. Violet marks the lines Expanso runs on its own. Orange marks the handoff, and the darker orange band is the HTTP call to Jev itself.

03-sensitivity-masking.yaml
Expanso, deterministicJev, judgment
name: jev-sensitivity-masking
type: pipeline
description: Semantic sensitivity classifier with Jev — every record gets a sensitivity class and PII/credential judgment at the edge, then the matching masking policy is applied before data leaves the source.
namespace: production
labels:
  category: data-security
  pattern: ai-decision
  model: jev

config:
  input:
    http_server:
      address: "0.0.0.0:8080"
      path: /records
      allowed_verbs: ["POST"]

  pipeline:
    processors:
      - mapping: |
          # http_server already parses JSON bodies; only parse raw strings
          root = if this.type() == "string" { this.parse_json() } else { this }

      - mapping: |
          root = this
          root.received_at = now()

      # ── Ask Jev: what is this record, semantically? ──
      - mutation: |
          meta jev_start = timestamp_unix_milli()

      - branch:
          request_map: |
            root = {
              "state": this.string(),
              "model": "jev-latest",
              "questions": {
                "sensitivity": {
                  "type": "choice",
                  "instructions": "What is the sensitivity class of this record?",
                  "criteria": {
                    "public": "Safe to share broadly, no personal or secret data",
                    "internal": "Company-internal, not for external sharing",
                    "confidential": "Sensitive business or personal data, restricted distribution",
                    "restricted": "Highly sensitive: regulated data, secrets, legal hold"
                  }
                },
                "contains_pii": {
                  "type": "noul",
                  "instructions": "Does this record contain personally identifiable information (names, emails, SSNs, addresses, phone numbers)?"
                },
                "contains_credentials": {
                  "type": "noul",
                  "instructions": "Does this record contain passwords, API keys, tokens, or other secrets?"
                }
              }
            }
          processors:
            - http:
                url: "${JEV_API_URL:https://api.typesafe.ai/v1/systemone}"
                verb: POST
                headers:
                  Content-Type: application/json
                  Authorization: "Bearer ${TYPESAFE_API_KEY}"
                timeout: 2s
                retries: 1
            - catch:
              - mapping: |
                  root = {"answers": {}, "model": "jev-unavailable"}
          result_map: |
            root.jev = {
              "answers": this.answers,
              "model": this.model.or("jev-latest"),
              "ms": timestamp_unix_milli() - metadata("jev_start")
            }

      # ── Pick the policy Jev's judgment calls for ──
      - mapping: |
          root = this
          let sens = this.jev.answers.sensitivity.choice.or("internal")
          let pii = this.jev.answers.contains_pii.noul.or(0)
          let creds = this.jev.answers.contains_credentials.noul.or(0)

          root.jev_decision = if $creds >= 0.7 || $sens == "restricted" {
            "quarantine"
          } else if $sens == "confidential" {
            "tokenize"
          } else if $pii >= 0.5 {
            "mask"
          } else {
            "pass"
          }

      # ── Apply the policy before data leaves the source ──
      - mapping: |
          root = this
          root.email = if this.jev_decision == "tokenize" {
            this.email.or("").hash("sha256").encode("hex")
          } else if this.jev_decision == "mask" {
            "MASKED"
          } else {
            this.email
          }
          root.ssn = if this.jev_decision == "tokenize" {
            this.ssn.or("").hash("sha256").encode("hex")
          } else if this.jev_decision == "mask" || this.jev_decision == "quarantine" {
            "MASKED"
          } else {
            this.ssn
          }
          root.api_key = if this.jev_decision == "quarantine" {
            "QUARANTINED"
          } else {
            this.api_key
          }
          root.policy_applied = this.jev_decision

  output:
    broker:
      pattern: fan_out
      outputs:
        - stdout:
            codec: lines
        - switch:
            cases:
              # Production: swap for http_client -> security alerting webhook
              - check: this.jev_decision == "quarantine"
                output:
                  file:
                    path: ./data/jev-sensitivity-masking/quarantine-${! now().ts_format("2006-01-02") }.jsonl
                    codec: lines
              - check: "true"
                output:
                  file:
                    path: ./data/jev-sensitivity-masking/forwarded-${! now().ts_format("2006-01-02") }.jsonl
                    codec: lines

135 lines. Copy and Download both give you the file byte for byte.

What you need

  • Expanso Edge installed, to validate and run the pipeline.
  • A Jev endpoint. The pipeline posts to JEV_API_URL, and falls back to https://api.typesafe.ai/v1/systemone when that variable is unset.
  • A key for that endpoint in TYPESAFE_API_KEY. The pipeline sends it as a bearer token and has no default for it.

What it proves

  • Input. POST /records on port 8080.
  • Output. Local files under ./data/jev-sensitivity-masking/. A comment in the YAML marks where a security alerting webhook replaces the quarantine file in production.
  • Scope. This example ships as a pipeline file and sample records. Its README marks the live runtime (event generator and dashboard) as coming next, so what is published here is the pipeline itself.
  • Revision. The file shown is the example as of commit 517c38f of its repository, which is still being developed.

Sample records

These are two of the records that ship with this example. The rest carry credential-shaped values, there so the example has something to catch, and are not reproduced here.

{"record_id":"rec-001","name":"Jane Doe","email":"[email protected]","ssn":"123-45-6789","plan":"team-annual"}
{"record_id": "rec-002", "name": "System Bot", "event": "nightly backup completed in 42s"}

Questions about this example.

receive, shape, gate, mask, route. Each of those stages applies the same fixed rules every time, with no model involved. Expanso also sets the thresholds that turn Jev's answers into a route.

Run the deterministic half on your own nodes.

Expanso Edge runs these pipelines where the data is created. The first five nodes are free.