GET /v1/rules

The tenant's secret ruleset — the patterns an agent-side integration uses to mask credentials on the host, before the model request leaves it. This is the one endpoint that takes no event: it is a configuration feed, fetched at startup and refreshed when the heartbeat says the id moved.

GET {base_url}/v1/rules
Authorization: Bearer ogr_<key>
If-None-Match: "<ruleset id>"

Why an integration fetches patterns instead of shipping them

An agent-side plugin is open source and installed on developer laptops. Shipping the pattern set inside it hands every rule to everyone, including the tenant's own custom rules — which are the ones that describe that customer's credential shapes. So the plugin ships no patterns: it fetches the composed ruleset for the org its API key proves, caches it on the host 0600, and refreshes when the id changes.

The masking itself is lossless and can be done blind: a secret is opaque, so the model has no use for its bytes and replacing them costs nothing. (This is why the same treatment is deliberately not applied to PII, whose values carry semantics a model may legitimately need.)

Response — 200

{
  "ruleset": {
    "id": "rs_8f21c0",
    "generated_at": "2026-09-14T03:12:44.117Z",
    "family": "secrets",
    "dialect": "ogr-re-1",
    "rules": [
      {
        "key": "entity_api_key",
        "tier": "strong",
        "flags": "i",
        "patterns": [{ "id": "gitlab", "re": "glpat-[A-Za-z0-9_-]{20,}" }],
        "examples": { "match": ["glpat-ABCDEFGHIJKLMNOPQRST"], "nomatch": ["glpat-short"] }
      }
    ]
  }
}
FieldMeaning
idThe ruleset's identity. The heartbeat echoes it as rules.id; when it differs from what you hold, refetch
generated_atWhen this composition was produced
dialectThe regex dialect the patterns are written in — ogr-re-1, a deliberately small subset so every implementation agrees on what a pattern means
rules[].tierstrong — the shape is a credential by construction; heuristic — a shape ordinary text produces by accident (password = …). An integration may mask both: a reversible mask that over-masks is cheap, where a finding that over-reports is not
rules[].groupThe 1-based capturing group that IS the span; absent means the whole match
rules[].reject_valueWhat this rule refuses to call a credential, applied to the span after the pattern matches. An implementation that ignores it over-masks
rules[].examplesStrings the rule MUST match and MUST NOT match

⚠️ examples are not documentation — run them. Every rule carries its own test vectors, so an implementation of ogr-re-1 can verify itself against the ruleset it was just served, on the host, before masking anything with it. A rule whose examples fail in your engine is a rule your engine reads differently from the runtime's, and masking with it would produce placeholders the runtime cannot account for.

Polling: ETag / 304

The response carries ETag: "<id>" and Cache-Control: private, max-age=60. Send If-None-Match and a 304 costs one round trip and no body:

curl -s -D- -o/dev/null $OGR_RUNTIME/v1/rules \
  -H "Authorization: Bearer $OGR_API_KEY" \
  -H 'If-None-Match: "rs_8f21c0"'
# HTTP/1.1 304 Not Modified
# ETag: "rs_8f21c0"

Better still, do not poll: the heartbeat already carries rules.id, so a running integration learns of a change within one beat.

What the integration reports back

A masked request carries the optional redaction field on the event — which rules fired and which placeholders were minted. It is a success record, not a finding: the values never left the host. If the runtime then finds a secret anyway, that is a miss with a name — same ruleset and our pattern found it, a stale ruleset, or a shape no rule covers yet.

⚠️ No report means no diagnosis, and that is deliberate. An integration that never masked and one that masked and missed look identical on the wire unless the report is there; both get the ordinary finding.

Errors

StatusBodyMeaning
401{"error": "unauthorized"}Missing, malformed, unknown, revoked or expired key
304(empty)Your If-None-Match matches the current ruleset