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"] }
}
]
}
}
| Field | Meaning |
|---|---|
id | The ruleset's identity. The heartbeat echoes it as rules.id; when it differs from what you hold, refetch |
generated_at | When this composition was produced |
dialect | The regex dialect the patterns are written in — ogr-re-1, a deliberately small subset so every implementation agrees on what a pattern means |
rules[].tier | strong — 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[].group | The 1-based capturing group that IS the span; absent means the whole match |
rules[].reject_value | What this rule refuses to call a credential, applied to the span after the pattern matches. An implementation that ignores it over-masks |
rules[].examples | Strings 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
| Status | Body | Meaning |
|---|---|---|
401 | {"error": "unauthorized"} | Missing, malformed, unknown, revoked or expired key |
304 | (empty) | Your If-None-Match matches the current ruleset |