Rule Writing Guide

This guide covers common patterns, priority strategies, and debugging techniques for the rule engine.

Basic Format

Rules are created as JSON via the HTTP API or MCP tools. Every rule requires an id, filter, and at least one action:

// Minimal rule: add a logging header to all /api/ requests
{
  "id": "log-api",
  "name": "Log API requests",
  "active": true,
  "stage": "RequestHeaders",
  "priority": 0,
  "termination": "Continue",
  "filter": {
    "type": "Url",
    "config": { "mode": "Contains", "value": "/api/" }
  },
  "actions": [
    {
      "type": "AddRequestHeader",
      "config": { "name": "X-Logged", "value": "true" }
    }
  ]
}

Common Patterns

1. Block Trackers

{
  "id": "block-tracking",
  "name": "Block analytics trackers",
  "active": true, "stage": "RequestHeaders",
  "priority": 100, "termination": "Stop",
  "filter": {
    "type": "Or",
    "config": [
      { "type": "Url", "config": { "mode": "Contains", "value": "google-analytics.com" }},
      { "type": "Url", "config": { "mode": "Contains", "value": "doubleclick.net" }},
      { "type": "Url", "config": { "mode": "Contains", "value": "facebook.com/tr" }}
    ]
  },
  "actions": [{ "type": "Drop" }]
}

2. Mock API Responses

{
  "id": "mock-api",
  "name": "Mock /api/users",
  "active": true, "stage": "RequestHeaders",
  "priority": 50, "termination": "Stop",
  "filter": {
    "type": "And",
    "config": [
      { "type": "Path", "config": { "mode": "Exact", "value": "/api/users" }},
      { "type": "Method", "config": { "mode": "Exact", "value": "GET" }}
    ]
  },
  "actions": [{
    "type": "MockResponse",
    "config": {
      "status": 200,
      "headers": { "Content-Type": "application/json" },
      "body": { "type": "Text", "value": "[{\"id\":1,\"name\":\"Alice\"}]" }
    }
  }]
}

3. Environment Switching (Map Remote)

{
  "id": "map-staging",
  "name": "Route to staging backend",
  "active": true, "stage": "RequestHeaders",
  "priority": 50, "termination": "Stop",
  "filter": {
    "type": "Host",
    "config": { "mode": "Exact", "value": "api.example.com" }
  },
  "actions": [{
    "type": "MapRemote",
    "config": {
      "url": "https://staging-api.example.com{{request.path}}",
      "preserve_host": false
    }
  }]
}

4. Inject Auth Headers

{
  "id": "inject-auth",
  "active": true, "stage": "RequestHeaders",
  "priority": 10, "termination": "Continue",
  "filter": {
    "type": "Host",
    "config": { "mode": "Contains", "value": "internal" }
  },
  "actions": [{
    "type": "UpdateRequestHeader",
    "config": { "name": "Authorization", "value": "Bearer internal-token", "add_if_missing": true }
  }]
}

5. CORS Headers

{
  "id": "cors-headers",
  "active": true, "stage": "ResponseHeaders",
  "priority": 100, "termination": "Continue",
  "filter": { "type": "All" },
  "actions": [
    { "type": "AddResponseHeader", "config": { "name": "Access-Control-Allow-Origin", "value": "*" }},
    { "type": "AddResponseHeader", "config": { "name": "Access-Control-Allow-Methods", "value": "GET,POST,PUT,DELETE" }}
  ]
}

0.5.1 note: ResponseHeaders changes appear in flow records but are not yet written back to the actual HTTP response in CLI mode.

6. Response Body Replace

{
  "id": "redact-secrets",
  "active": true, "stage": "ResponseBody",
  "priority": 100, "termination": "Continue",
  "filter": {
    "type": "ResponseHeader",
    "config": { "name": "Content-Type", "value": { "mode": "Contains", "value": "json" }}
  },
  "actions": [{
    "type": "TransformResponseBody",
    "config": {
      "type": "RegexReplace",
      "config": { "pattern": "\"token\":\\s*\"[^\"]+\"", "replacement": "\"token\":\"***\"" }
    }
  }]
}

7. IP Blacklist (Connect Stage)

{
  "id": "block-ips",
  "active": true, "stage": "Connect",
  "priority": 100, "termination": "Stop",
  "filter": { "type": "SrcIp", "config": "10.0.0.0/8" },
  "actions": [{ "type": "Drop" }]
}

8. Rate Limiting

{
  "id": "rate-limit",
  "active": true, "stage": "RequestHeaders",
  "priority": 90, "termination": "Continue",
  "filter": { "type": "All" },
  "actions": [{
    "type": "RateLimit",
    "config": { "key": "{{ip}}", "limit": 60, "window_ms": 60000 }
  }]
}

9. Cross-Rule Variables

// Rule 1: extract API version into variable
{
  "id": "extract-version", "priority": 100,
  "stage": "RequestHeaders",
  "filter": { "type": "Path", "config": { "mode": "Regex", "value": "/api/v(\\d+)/" }},
  "actions": [{ "type": "SetVariable", "config": { "name": "api_version", "value": "v1" }}]
}
// Rule 2: read variable in response header
{
  "id": "add-version-header", "priority": 50,
  "stage": "ResponseHeaders",
  "filter": { "type": "All" },
  "actions": [{ "type": "AddResponseHeader", "config": { "name": "X-Api-Version", "value": "{{variable.api_version}}" }}]
}

10. WebSocket Message Filtering

{
  "id": "ws-mock",
  "active": true, "stage": "WebSocketMessage",
  "priority": 50, "termination": "Stop",
  "filter": {
    "type": "WebSocketMessage",
    "config": { "mode": "Contains", "value": "heartbeat" }
  },
  "actions": [{
    "type": "MockWebSocketMessage",
    "config": { "direction": "Incoming", "message": "{\"type\":\"heartbeat\",\"ts\":\"mocked\"}" }
  }]
}

Priority Strategy

Priority determines rule evaluation order. Recommended ranges:

RangePurposeTypical termination
100-90Block/security rulesStop
89-50Routing rules (Mock/MapRemote/Redirect)Stop
49-10Modification rules (Headers/Body)Continue
9-0Observation rules (Tag/Log/Inspect)Continue

Termination Guide

  • Stop: Use when subsequent rules are meaningless after this one. Block, MockResponse, MapRemote should typically Stop
  • Continue: Modification rules and observation rules should Continue to allow stacking multiple rule effects

Rule-Script Integration

RelayCore exclusive — rules and scripts observe each other, enabling logic neither engine can achieve alone.

0.5.1 notes: scripts run before rules in onRequestHeaders — read flow.rule_variables in onRequest. Script { action: "respond" } is not supported yet; use rule MockResponse / Drop instead.

Integration Matrix

DirectionMechanism1.0
Rule→RuleSetVariable + Mustache
Rule→Scriptflow.matched_rules / rule_variables
Script→ScriptsharedState same isolate
Script→RulesetTag / setVariable1.x

Pattern 1: Rule Extraction → Script Decision

Rules extract variables, scripts make complex decisions:

// Rules
[{"id":"extract-api-version","stage":"RequestHeaders","priority":100,
  "filter":{"type":"Path","config":{"mode":"Regex","value":"/api/v(\d+)/"}},
  "actions":[{"type":"SetVariable","config":{"name":"api_version","value":"v1"}}]},
 {"id":"extract-auth-info","stage":"RequestHeaders","priority":90,
  "filter":{"type":"RequestHeader","config":{"name":"Authorization",
    "value":{"mode":"Contains","value":"Bearer"}}},
  "actions":[{"type":"SetVariable","config":{"name":"auth_type","value":"bearer"}}]}]

// Script
globalThis.onRequest = (ctx, flow) => {
  const v = flow.rule_variables["api_version"];
  const a = flow.rule_variables["auth_type"];
  if (v === "v1" && a !== "bearer") {
    flow.tags.push("deprecated-api");
    // 410 via MockResponse rule; { action: "respond" } is 1.x roadmap
  }
  if (v === "v2") {
    const key = flow.network.client_ip + ":v2";
    const counts = sharedState.get("_v2counts") || {};
    counts[key] = (counts[key] || 0) + 1;
    sharedState.set("_v2counts", counts);
    flow.layer.data.request.headers.push(["X-Rate-Count", String(counts[key])]);
  }
  return flow;
};

Pattern 2: Rule Tagging → Script Inheritance

// Rule
{"id":"tag-slow","stage":"RequestHeaders","priority":50,
 "filter":{"type":"Or","config":[
   {"type":"Path","config":{"mode":"Contains","value":"/report"}},
   {"type":"Path","config":{"mode":"Contains","value":"/export"}}]},
 "actions":[
   {"type":"Tag","config":{"key":"type","value":"slow"}},
   {"type":"SetVariable","config":{"name":"trace","value":"1"}}]}

// Script
globalThis.onResponse = (ctx, flow) => {
  if (!flow.matched_rules.includes("tag-slow")) return;
  if (flow.layer.type !== "Http" || !flow.layer.data.response) return;
  const dur = Date.now() - new Date(flow.start_time).getTime();
  flow.layer.data.response.headers.push(["X-Server-Timing", "total;dur=" + dur]);
  sharedState.delete(flow.id);
};

Pattern 3: Rule Rate Limit + Script Cache Fallback

Scripts can cache successful responses; returning cached bodies or 429 requires { action: "respond" } (1.x roadmap). In 0.5.1, cache and tag only:

// Rule
{"id":"rate-limit","stage":"RequestHeaders","priority":100,
 "filter":{"type":"All"},
 "actions":[
   {"type":"RateLimit","config":{"key":"{{ip}}","limit":100,"window_ms":60000}},
   {"type":"SetVariable","config":{"name":"limited","value":"1"}}]}

// Script
function pathname(url) {
  const start = url.indexOf("://");
  const slash = start >= 0 ? url.indexOf("/", start + 3) : url.indexOf("/");
  return (slash >= 0 ? url.slice(slash) : "/").split("?")[0];
}
globalThis.onResponse = (ctx, flow) => {
  if (flow.layer.type !== "Http" || !flow.layer.data.response) return;
  const res = flow.layer.data.response;
  if (res.status >= 300) return;
  const path = pathname(flow.layer.data.request.url);
  const cache = sharedState.get("_cache") || {};
  cache[path] = { status: res.status, headers: res.headers, body: res.body, cachedAt: Date.now() };
  sharedState.set("_cache", cache);
};
globalThis.onRequest = (ctx, flow) => {
  if (flow.rule_variables["limited"] !== "1") return flow;
  const path = pathname(flow.layer.data.request.url);
  const cache = sharedState.get("_cache") || {};
  const c = cache[path];
  if (c && Date.now() - c.cachedAt < 300000) flow.tags.push("cache-hit");
  return flow;
};

Pattern 4: Script→Rule (1.x Roadmap)

// 1.x: script injects tag for subsequent rules to auto-block
globalThis.onRequest = (ctx, flow) => {
  const body = flow.layer.data.request.body;
  const raw = body ? atob(body.content || "") : "";
  if (raw.includes("DROP TABLE")) {
    ctx.setTag("attack", "sqli");     // 1.x
    ctx.setVariable("why", "SQLi");   // 1.x
  }
};

Debugging

View Matched Rules

// In a script
globalThis.onResponse = (ctx, flow) => {
  console.log("Matched rules:", flow.matched_rules);
  console.log("Rule variables:", flow.rule_variables);
};

Validate Rules

relay-core-cli rules validate rules.json

List Rules via API

curl http://127.0.0.1:8082/api/v1/rules | python3 -m json.tool

Common Issues

  • Rule not firing: Check "active": true and that stage matches the traffic stage
  • Inspect/breakpoints not working in CLI: The Inspect action requires a desktop UI (Tauri/RelayCraft) to display the interception dialog. In CLI mode, Inspect is a no-op — rules with Inspect actions will not pause traffic
  • Action order: Actions execute in array order; terminal actions (MockResponse etc.) prevent subsequent actions from running
  • MapLocal file access: Requires sandbox_root config; emits deprecation warning without it, will be enforced in 1.1
  • Large body performance: ResponseBody filter caches the full response; prefer Path filter for large files
  • ResponseHeaders not applied to clients: In 0.5.1 CLI, AddResponseHeader updates flow records but not curl/browser responses yet
  • MapRemote loops: Avoid forwarding traffic back to RelayCore's own proxy port