Rules & Policies
Overview
Section titled “Overview”Rules & Policies (/policies) is the workspace page where you set rules once and they apply
everywhere by default: AI traffic (the AI Warden relay), collector logs,
AI coding, Cortex chat, and every project’s code scans and log generation.
Any policy can be narrowed to selected projects, relay keys or collectors.
It replaces three separate stores — Warden rule sets (warden_rule_set and its child tables),
scan templates (project_analyzer_rules_config) and custom log rules (custom_log_rule) — with
one data model (policy, policy_rule, policy_assignment, policy_version) and one API
(/api/v1/policies*). The hub, the in-feature editors (AI Warden → Business Rules, a collector’s
Business Rules tab, AI Coding → Create rule, Configure Rules, Your own rules) and the MCP policies
tool all edit the same rows.
“Policy as code” is served by version history plus JSON export and import. There is no Git, pull request or bundle machinery.
| Kind (wire) | UI label | Features | Statuses | Rule types |
|---|---|---|---|---|
business_rules | Business rules | ai_traffic, logs, ai_coding, cortex | On, Monitor only, Off | ai_judge, all deterministic types, framework |
logging_rules | Logging rules | code | On, Off | custom_log |
scan_profile | Scan profile | code | On, Off | catalogue; scope must be projects with exactly one project |
A scan profile is what used to be a project’s “template” for one framework. Its settings hold
framework (general or a code-scannable library key), the server-resolved logTypeId and
generationOptions. Scan wire names stay: templateId, templates[] and
InitiateScanRequest.template(s) now carry scan-profile policy ids.
Status: On / Monitor only / Off
Section titled “Status: On / Monitor only / Off”Status is a separate axis from targeting. Every engine enforces it. The UI words changed on
2026-10-19; the wire values did not — the API, MCP and export files still speak enforcing,
evaluate and off.
| Wire | UI word | Tone + icon | Meaning |
|---|---|---|---|
enforcing | On | green, shield | Rules act with their configured action: Block blocks, Flag flags |
evaluate | Monitor only (business_rules only) | blue, eye | The backend caps every block to notify (below) |
off | Off | grey, pause | The policy applies nowhere. A policy with no features must be Off |
- Monitor only — per-item actions are capped directly in the bundle. A framework whose only
owners are Monitor-only policies is listed in the wire field
evaluateFrameworks(upper-cased); the relay and Cortex cap findings for those frameworks to notify. Collector logs and AI coding never block anyway, so Monitor only behaves like On there. - Monitor only never changes how the relay enforces (backend
1987c06f, 2026-10-21, dev and prod). A Monitor-only policy no longer raises the mergedmodetoinline_blockingorfailModetofail_closed, and its legacypolicyTextis not appended;judgeModel,sampleRateandrulesEnabledmerge as before. Until then, putting a policy in Monitor only on a key could switch that key to inline judging with fail-closed (BE-2). - A refused status names both words: “Monitor only (evaluate) is for business rules only. Logging rules and scan profiles are On (enforcing) or Off.”
statusChangedAt records the last change; the Overview raises evaluate_long after 14 days in
Monitor only.
What a policy is doing: “Not running”
Section titled “What a policy is doing: “Not running””The hub also shows what a policy is doing right now, which can differ from its status
(model/policyState.js, the first rule that applies wins; frontend 78b1737b, 2026-10-21):
- Off → Off: “Checks nothing, anywhere.”
- No features → Not running (amber): “It applies to nothing yet. Choose where it applies.”
- Selected projects, and every one of them was deleted → Not running: “Its projects were
deleted, so it runs nowhere. Pick a project, or apply it to the whole workspace.” The Overview
raises
policy_no_projectsfor the same case (below). - No enabled rules → Not running: “It has no rules yet.”, or “Every rule is turned off.” when
it holds rules that are all disabled. The list and the editor count the same thing: summaries
carry
enabledRuleCount(backend01b2d44d), so a policy whose rules are all disabled is no longer “On” in the list and “Not running” in the editor. - Only pattern rules, with “Run these rules” off → Not running: “Its only rules are pattern rules, and “Run these rules” is off.”
- Monitor only → Monitor only: “Records what it would block. Blocks nothing.”
- On, with
mode: async_observeand plain-English rules → On · judge afterwards: “Pattern rules act as set. The AI judge checks after the request is sent, so its Block is recorded, not enforced.” - Otherwise → On.
Amber is used only for problems. A state is never shown by colour alone: the word is always visible and the icon is decoration.
Scope, features and narrowing
Section titled “Scope, features and narrowing”- Scope:
workspace(“Whole workspace”, the default) orprojects(“Selected projects”, withprojectIds; every id must be a project in the workspace; at most 100 ids, “projectIds must list at most 100 ids — got N”). - Features:
ai_traffic(AI traffic),logs(Collector logs),ai_coding(AI coding),cortex(Cortex chat),code(Code scans & log generation). Alogging_rulespolicy sent withfeatures: []is saved with["code"], its only feature (measured 2026-10-22). - Narrowing exists on two features only:
relayKeyIdsnarrowsai_traffic; empty means every key, including future keys.collectorIdsnarrowslogs; empty means every collector.
- Collector logs × Selected projects is refused (400): “Collector logs can’t be limited to projects: collectors aren’t linked to projects. Narrow by collector instead.” Collectors belong to a workspace, not a project.
- An update keeps what it does not send (backend
01b2d44dand955701d5, 2026-10-21, dev and prod). APUT /{id}that omits (or sends null for)scope,projectIds,features,rules,relayKeyIdsorcollectorIdskeeps the stored value; an explicit[]clears a target list, andfeatures: []is still refused. Before, omittingscopewidened a project-scoped policy to the whole workspace (BE-4) and omittingrulesdeleted every rule (BE-37).
A policy applies to a request when it is not Off, its features contain the feature, and either the
scope is workspace or a project id is known and listed in the policy; for ai_traffic and
logs the narrowing list must be empty or contain the key / collector. PolicyResolver.effective
does this in three queries, ordered by policy id.
Rule type × feature support
Section titled “Rule type × feature support”| Rule type (UI label) | ai_traffic | logs | ai_coding | cortex | code |
|---|---|---|---|---|---|
ai_judge (Plain-English rule) | ✓ | ✓ (flag) | ✓ (flag) | ✓ | — |
content_regex / regex (Pattern) | ✓ | ✓ (flag) | ✓ (flag) | ✓ | — |
prompt_injection(_heuristic) (Prompt-injection check) | ✓ | ✓ (flag) | — | ✓ | — |
allowlist / model_allowlist / endpoint_allowlist / rate_limit / token_budget | ✓ | — | — | — | — |
framework (Framework) | ✓ | ✓ (flag) | ✓ (code categories) | ✓ | via scan profiles |
custom_log (Logging rule), catalogue (Framework rule setting) | — | — | — | — | ✓ |
rate_limit and token_budget are policy rules like any other: a hit with action Block answers
403 {"error":{"type":"warden_blocked","code":"policy_block"}}, not 429. The relay answers
429 warden_rate_limited only from its own limiters, which no policy controls: the per-key request
cap WARDEN_RATE_LIMIT_PER_MIN (rate_limited) and the opt-in per-tenant tokens-per-minute budget
WARDEN_TPM_BUDGET_PER_TENANT (token_budget_exceeded).
Saving a policy whose enabled rule does not run on some of its features returns a warnings[]
line, e.g. “Rate limit “burst” does not run on Collector logs.” The editor shows these above the
footer. Every read of one policy (GET /{id} and the status, update and restore answers) carries
the same list, computed from the stored rules and features (backend f47b5d0a); version snapshots
store none.
A rule must carry the configuration its engine reads, or the save answers 400 (backend 01b2d44d,
2026-10-21): “Pattern rule “X” needs a pattern.” (regex / content_regex with a blank
pattern), “Allowlist rule “X” needs values.”, “Rate limit rule “X” needs a limit (requests per
window).” (and a windowMs that is not a positive number of milliseconds is refused), “Token
budget rule “X” needs maxTokens.” The editor refuses an empty pattern before saving, with the same
sentence. Before, such rules saved, shipped to the relay and never matched.
Pattern size and shape are limited, and the limits are checked before any regex work (2026-10-23). Each one answers 400.
| Limit | Value | Answer |
|---|---|---|
| One pattern | 1,000 characters | “rules[i].config.pattern must be at most 1000 characters” |
| All patterns of one policy | 20,000 characters | “the patterns of one policy may total at most 20000 characters” |
| Shape | No repeat or alternation inside a repeated group, such as (a+)+, no backreference such as \1, and at most 3 open-ended repeats (+ or *) | “Pattern rule “X” can take exponential time on a long message” |
- Why these limits exist: before them, the save compiled the pattern with no length limit. A 1,000,000-character pattern held a backend thread at 100% CPU for minutes and timed out at the edge (524). A 100,000-character pattern took 15 s.
- Shape check and stored patterns: the shape check skips a pattern the policy already holds unchanged, so a save never fails over a rule nobody edited.
- Existing rules: the longest stored pattern on either estate was 90 characters. The only stored pattern with a refused shape was a QA probe.
- Logging rules: custom logging-rule targets keep their own tighter limit of 160 characters, with the same shape check.
- At run time the analyzer and the relay apply the same limits again (
regexGuard.ts), so a row that bypassed the API still cannot stall a service. See Pattern rules at run time on the AI Warden page.
Request limits on /api/v1/policies (2026-10-23)
Section titled “Request limits on /api/v1/policies (2026-10-23)”| Limit | Default | When it is reached | Setting (0 or security.public-rate-limit.enabled=false = off) |
|---|---|---|---|
| Writes (POST, PUT, PATCH, DELETE), per credential | 1,200 an hour, refilled continuously | 429 with Retry-After | security.public-rate-limit.policy-write-per-hour |
Tests (/{id}/test, /effective/test) and /preview-logging-rule, per credential | 600 an hour | 429 with Retry-After | security.public-rate-limit.policy-test-per-hour |
| Pattern checks running at once, per backend instance | 4 | waits up to 5 s, then 503 policy_validation_busy, Retry-After: 5 | LOGRITE_POLICY_REGEX_PERMITS |
| Logging-rule interpretations running at once, per backend instance | 8 | waits up to 5 s, then 503 logging_rule_compile_busy, Retry-After: 5 | LOGRITE_POLICY_COMPILE_PERMITS |
- Reads (GET) are never counted. A credential is the bearer token or API key, so two people in one workspace have separate budgets.
- The hourly figures were set from the
[api-audit]lines of 2026-10-15 to 2026-10-23. In the busiest hour, one credential made 536 writes and 206 tests on dev (acceptance harnesses), and 47 and 16 on prod. Both limits sit above every measured hour. - The two concurrency limits are separate because their costs differ: the slowest pattern body the API accepts takes about 4 s to check, and interpreting a logging rule calls the analyzer and a model. Interpretations can therefore never make a plain policy save wait.
- The backend now gives up on the analyzer’s rule routes after 75 s (connect 5 s). A timed-out interpretation saves the rule as a draft with “the assistant was unreachable”, as before. AI coding issue write-ups, which run in the background on the slower lane, wait up to 180 s.
- A body nested deeper than the JSON reader’s 256-level limit answers
400 malformed_request“The request body is beyond what the JSON reader accepts — …” (until 2026-10-23 it read as a wrong-typedsettingsfield). - The backend’s shared HTTP client (GitHub and Bitbucket API calls) gives up after 10 s to connect
and 30 s to read:
logrite.http.default-connect-timeout-ms,logrite.http.default-read-timeout-ms(0 = no limit). GitHub itself ends any REST request after 10 s.
Actions: the UI writes Block → block, Flag → notify, Record → record for patterns, otherwise
allow. On read, notify/allow_notify show as Flag and allow/record as Record.
How each feature learns its project
Section titled “How each feature learns its project”| Feature | Project id used |
|---|---|
ai_traffic | The request’s X-Warden-Project-Id if it belongs to the workspace, else the legacy relay_compliance_binding.project_id, else none |
logs | None — project-scoped policies never apply |
ai_coding | The plugin’s .logrite/config.json projectId, sent as ?projectId= on the policy fetch |
cortex | The /chatv2 body projectId, validated by the backend; /chat and /chat/sync have none |
code | The scan’s projectId; Gatekeeper uses the project of the newest scan of the repository in the caller’s workspace |
A foreign project id is dropped (PolicyResolver.validProject), so only workspace-scoped policies apply.
Data model
Section titled “Data model”| Table | Holds |
|---|---|
policy | workspace_id, kind, name, description, status, status_changed_at, scope, features (jsonb), source (custom/library), library_key, settings (jsonb), revision (optimistic lock), audit columns, legacy_ref (unique, migrator only) |
policy_rule | One item: rule_key (unique per policy), type, name, text, config (jsonb), action, severity, enabled, sort_order |
policy_assignment | target_type (project / relay_key / collector) + target_id, unique per policy |
policy_version | Snapshot of the resulting document per revision, with summary, actor and origin (console, api, mcp, migration, restore, import, system). No FK, so history survives deletion |
Hand-run DDL: migrations/2026-10-policies.sql. Rule rows are upserted by key — an existing key
keeps its row id, a new key inserts, a missing key deletes — so ids no longer churn on save.
Rule ids on the wire
-
Relay (and Cortex) bundle: each deterministic rule carries
"id": "p<policyId>.<ruleKey>"; capture docs record it inbusinessRuleIds. NL judge policies carry thepolicy_rule.id, recorded injudge.policyIds. -
Collector logs and the dry runs (
/{id}/test,/effective/test): the analyzer’s evaluator puts the samep<policyId>.<ruleKey>on everymatchedRules[]entry and every deterministic finding asruleId(analyzer18d874cf).PolicyFindingMapperattributes a finding by its ownruleId, then bymatchedRules[].ruleId, and only when the evaluator reported no id at all by the first enabled rule of that type; an id that names no rule leaves the finding unattributed (backend2dcff896). Before that a match was credited to the first rule of the same type in any policy. -
Collector flags (
agent-flags-<ws>) carry the id too since 2026-10-16. Until then the analyzer wrote a fixed field list without it, and the flag’s document id ended in the rule type, so tworegexrules from different policies matching one line overwrote each other and one flag survived. Now:- each flag stores
ruleId(keyword) andpolicyId(long). For a deterministic ruleruleIdisp<policyId>.<ruleKey>andpolicyIdis parsed from it; for a plain-English ruleruleIdis the judge policy id (policy_rule.id) andpolicyIdcomes from the logs bundle’snlPolicies[].policyId(backend1987c06f+ analyzer639e749d, 2026-10-21; null before); a framework finding has neither; - each flag’s
severityis the rule’s own (mediumwhen the rule has none): every bundle rule carriesseverity, omitted when null (backend936dad6e, analyzer639e749d). Before, every collector flag readmedium; - a line that matches a Block rule and a Flag rule gets both flags: the logs evaluator runs
preEvaluate(..., {collectAll: true})instead of stopping at the first block (AN-1); - the document id is
docId:decisionLayer:ruleId(falling back to the rule type when there is noruleId), so two rules matching the same line are two flags; GET /api/v1/agents/{id}/flagsreturnsruleId,policyIdandpolicyName— the name is looked up in one query per page, only among the caller’s workspace’s policies (null otherwise);- the collector’s Logs drawer shows a Policy row linking to
/policies/policies/<policyId>when the name is known.
Flags written before the change have neither field.
- each flag stores
-
AI coding: business findings are
business.rule-<ruleKey>. Migrated rules keep their oldwarden_rule.idas the key, so existing fingerprints and mutes still match.
Business-rules settings: mode (inline_blocking default / async_observe), failMode
(fail_open / fail_closed; the old open/closed aliases are gone), sampleRate,
scanScopes, deterministicRules (“Run these rules” in the editor’s Pattern and limit rules
section; a missing value counts as on), flagOnlyOnAiTraffic, judgeModel, and the preserved
legacy policyText.
All routes are under /api/v1/policies, served by PolicyController (CRUD and history),
PolicyHubController (hub reads and tests) and PolicyHubWriteController (transfer, frameworks,
scan-profile ensure). The workspace comes from the session JWT or an lgp_live_ API key. A read
key may call the GETs and the two test routes; any other write answers 403 “This API key is
read-only; …”. full and admin keys may write too, under the minting user’s role floor, with
origin api in the version history. Timestamps in a write answer equal the following GET
(microsecond precision, backend 8c747ee2).
Role floors (RoleFloors): policy-business-write = Maintainer for business_rules;
policy-code-write = Developer for logging_rules and scan_profile. Status changes and
restore use the same family; a Viewer can only read. A refusal names the floor (backend 7db0d5a1,
2026-10-21): 403 “You need the Maintainer role to change business rules.” or “You need the
Developer role to change logging rules and scan profiles.” (Maintainer/Developer is the
workspace’s configured floor). The old template-write floor is gone: the project template routes
(/api/v1/project/{projectId}/templates*, MCP project_admin) use policy-code-write.
The history origin is mcp for a connected AI client’s delegated token, api for an API key and
console for a session, for every write including delete (WriteOrigin, backend 01b2d44d;
deletes used to record console always, and mcp was never written).
Conflicts (409). Every 409 body is served by GlobalApiExceptionHandler, so a hub write (import,
framework enable) answers the same body as a PUT instead of a 500 (backend 01b2d44d):
- stale revision →
{"error":"stale_revision","currentRevision":N,"message":"This policy changed since you opened it. Reload to see the latest version."}.PATCH /{id}/statusaccepts an optionalrevisionand refuses a stale one with the same body. A DELETE that races an update reads the row under a write lock, so it waits and records the next revision instead of answering 500 or a generic 409 (RV-7, backende7e56ff4). - deleting a scan profile that scans still reference →
{"error":"scan_profile_in_use","scans":N, "message":"This scan profile is used by N scans. Turn it Off instead."}(the scan rows keep their FK to the profile). It used to answer a generic “This record already exists”.
Creates, imports and framework enables take a per-workspace advisory lock
(pg_advisory_xact_lock), so parallel creates with one name make one policy and the others are
refused as a duplicate name, instead of creating duplicates (BE-8).
| Method | Path | Purpose |
|---|---|---|
| GET | / (root) | List summaries in id order; query kind, feature, status, projectId, q. Without page: the bare array. With page (0-based): {items, total, page, size}, size 1–200, default 25 (backend 01b2d44d; one dev workspace held 658 policies). Each summary carries enabledRuleCount |
| GET | /{id} | One PolicyDocument |
| POST | / (root) | Create (201). An unknown kind: “Unknown kind ‘x’. Use business_rules, logging_rules or scan_profile.” |
| PUT | /{id} | Update; omitted fields keep their stored value (above); a stale revision → 409 stale_revision |
| PATCH | /{id}/status | {status, reason?, revision?} |
| DELETE | /{id} | 204; a referenced scan profile → 409 scan_profile_in_use |
| GET | /{id}/versions, /{id}/versions/{rev} | History list / one snapshot; also for a deleted policy of the caller’s workspace (policy_version.workspace_id) — the hub opens it read-only from Activity |
| POST | /{id}/versions/{rev}/restore | Restore a snapshot as a new revision (below) |
| POST | /preview-logging-rule | {title, text, projectId?} → {status, compiled, compileError, suggestions, reach?}. With projectId the rule is checked against that project’s latest scan: a method or file it does not contain is a DRAFT with a suggestion, an exact declared name in a rule naming its class (or a camelCase name) is stored as ^name$, and reach {count, sample, broad} says how many methods it matches (backend a91a625c…62af493c, on dev since 22 Oct; a name the author typed is always kept, and invented names beside real ones are left out with a note). The save runs the same check for a rule scoped to exactly one project, and a save of text that was just checked reuses that compile (24 h, backend 17738dc0) so Save stores what Check rule showed. A sentence that asks NOT to log is refused whatever the model labels it; a positive rule the model labels leak compiles (2f4ffd52). A plain typo in a method or class name is corrected, with a note in suggestions; a rule naming an area (“Audit money movements”) is shown that area’s declared methods and may list up to 30 of them (f3374ef7, cc348a1f) |
| GET | /overview | counts (byStatus — business and logging rules only, scan profiles excluded since backend d0feaaa5 — byFeature, and byKindStatus = kind × status with zeros), attention[], recent (last 5 changes) |
| GET | /effective | feature, projectId, relayKeyId, collectorId → {policies} |
| GET | /effective/all | Per relay key, collector, AI coding, Cortex and project, in one call. Each code.projects[] row carries projectType: compliance when every scan profile names a framework other than general, else generator (backend d0feaaa5) |
| POST | /{id}/test | {feature, input, projectId?, relayKeyId?, collectorId?} → {verdict, findings, wouldBlock, judge?}; one policy, no Monitor-only cap. Logging rules return the stored compile preview; scan profiles → 400 |
| POST | /effective/test | Same over the effective bundle (replaces the collector dry run) |
| GET | /{id}/activity?days=7 | Matches in warden-requests-<ws> and coding_issue (days 1–30) |
| GET | /activity | Hub audit feed; page, size, policyId, actor, from, to. Each item carries revision and, for policy_* actions, the version’s origin |
| GET | /export?ids=1,2 | Downloads logrite-policies-<yyyyMMdd>.json; exportedAt has microsecond precision |
| POST | /import | Creates each policy Off, workspace-wide, un-narrowed, with fresh rule keys; a clash gets ” (imported)”; scan profiles refused per item |
| POST | /frameworks/{libraryKey}/enable | {features, scope?, projectIds?, action?} (features required; a missing scope means workspace, backend 53019b44 — it used to answer 400) → {policy, scanProfiles:[{id, name, projectId}]}; upserts the library business-rules policy for the non-code features; code ensures one scan profile per project in scope, so a workspace with no projects gets scanProfiles: [] |
| POST | /frameworks/{libraryKey}/disable | {features?} → Off, or drop only those features |
| POST | /scan-profiles/ensure | {projectId, framework} → the project’s profile (Developer) |
Test verdicts (PolicyFindingMapper, backend 1702123b, 2026-10-21, dev and prod). The backend
sends feature in the evaluate body. On ai_traffic and cortex a matching Block rule answers
verdict: BLOCK; logs and ai_coding are notify-only and never answer BLOCK; a Monitor-only
policy’s block is a notify in /effective/test. Each finding carries the rule’s own severity
(medium when null). wouldBlock is exactly verdict == BLOCK, computed after those caps — before,
a block/high rule tested on ai_traffic answered NOTIFY/medium, and wouldBlock was true beside a
NOTIFY verdict. The evaluator’s judge field passes through: "ran", "timeout", or
"disabled" when the collector judge is off (feature: logs, AGENT_SCAN_JUDGE_MAX_LINES=0); the
hub’s Test a log line then says the plain-English rules were skipped.
Overview attention codes: policy_no_features (““X” is on but isn’t applied anywhere.”),
policy_no_projects (warn, with policyId and name: ““X” is limited to projects, but none of
them exists any more, so it isn’t running.” — On or Monitor-only, scope projects, no project left;
backend 955701d5), logging_rule_draft (“Logging rule “X” couldn’t be compiled, so it isn’t
running.”), relay_key_unprotected, collector_unprotected, evaluate_long (““X” has been in
Monitor only for N days.”), cortex_not_covered (info).
A save warning never prints a rule key: a rule with no name reads “An unnamed pattern rule does not run on …”.
Related routes elsewhere:
DELETE /api/v1/warden/violations/suppressions/{id}revokes a false-positive suppression (auditwarden_suppression_removed).GET /api/v1/warden/library/frameworksnow returnsenabledandenabledFeaturesper framework. All 30 library frameworks are accepted everywhere throughFrameworkCatalog.normalize.
Every write records a policy_version and a governance audit row (policy_created,
policy_updated, policy_status_changed, policy_deleted, policy_restored, policy_imported,
framework_enabled, framework_disabled). /api/v1/policies, /import,
/frameworks/*/enable and /scan-profiles/ensure honour Idempotency-Key.
Internal: Cortex resolve
Section titled “Internal: Cortex resolve”GET /internal/policies/resolve?workspaceId=&feature=cortex&projectId= (PolicyInternalController,
X-Warden-Internal-Token) returns the same WardenBindingResolveResponse the relay gets, built from
the effective cortex policies, plus policyNames: a map from string policy id and from
fw:<FW> (upper-cased, e.g. fw:HIPAA, fw:M-26-14) to the policy name, so a refusal can name
the policy whose rule fired. feature other than cortex is a 400.
Enforcement per feature
Section titled “Enforcement per feature”- AI traffic —
WardenBindingResolverresolves the key and project, theneffective(ws, "ai_traffic", project, keyId, null)→PolicyBundleBuilder(most-restrictive wins, policies absorbed in id order).ruleSetIdon the wire is the first policy id. The legacy compliance-profile fallback is gone.- Duplicate rules (same
typeandconfigin two policies) keep one bundle entry; since backend1987c06f(2026-10-21, dev and prod) the stricter action wins and the entry takes that rule’sid,actionandseverity. Before, the lower policy id won whatever its action, so a Monitor-only policy with a lower id cancelled an On policy’s identical Block (BE-1). - Monitor-only policies never raise
mode/failMode(see Status). - A resolve updates the relay key’s
lastUsedAtonly when the stored value is more than 60 s old (BE-14). - An edit reaches the relay and Cortex within about 60 s (their bundle caches); this is intended.
- Duplicate rules (same
- Collector logs —
effective(ws, "logs", null, null, agentId), always notify-only. The bundle’snlPolicies[]carrypolicyId, and every rule itsseverity. - AI coding —
CodingPolicyService.policy(ws, projectId); see AI Coding. - Cortex chat — prompt pre-check and answer check in the ai-service; see AI Service.
- Code — scan profiles drive the analyzer’s rules; logging rules (
custom_log, compiled andACTIVE) run on interactive scans and IDE and Gatekeeper scans. See Log Generator.- Logging rules are added to a scan profile’s rules. The built-in profile “Custom rules only”
(
custom_only) runs logging rules alone. - Each scan reports per rule what was written, merged or could not be placed (
customRules). - The value a rule names (its
capture) is logged for Java — from a parameter, a field one or two steps away (null-guarded, through type-hintlinks), a local at the exit, or the signed-in user; an audit-class rule also records whom the line is about (account number, customer id, user id, username, email or identifier — the first the method has). When a value exists only at the exit, the rule writes one line there carrying every value. match.excludeMethods(Class#method) lists methods the rule reaches but skips: constructors, exception types and handlers (unless the rule is about errors), templates, yes/no checks, extractors, configuration beans, private helpers and controller copies of service methods — only for names the author did not type, and never all of a rule’s methods.- See fixes and open gaps.
- Logging rules are added to a scan profile’s rules. The built-in profile “Custom rules only”
(
Version history, export and import
Section titled “Version history, export and import”Every write creates a policy_version. The editor’s History tab lists versions (summary, actor,
origin, time), shows the version summary with the raw JSON diff behind Show raw changes, and
restores one through a confirm. Activity → Policy opens the editor on History with that version
expanded — also for a deleted policy, read-only.
Restore (backend 01b2d44d and c926f121, 2026-10-21, dev and prod) writes the snapshot’s
settings verbatim; a key the old revision lacked takes the write default, never the current value.
A project that no longer exists is dropped with a warnings[] line “Project #N no longer exists;
it was removed from the restored policy.” When that was the revision’s only project the restore
answers 400 “Project #N no longer exists, and it was this revision’s only project. Restore it with
a different scope, or pick another revision.” Before, restore kept the current settings and failed
on any deleted project.
Export strips ids, revisions, timestamps, actors, warnings and all targeting (projectIds,
relayKeyIds, collectorIds), so a file moves cleanly between workspaces. Import is capped at 100
items and always mints fresh rule keys, so an imported policy never shares a key (and so an AI-coding
finding id business.rule-<ruleKey>) with an existing policy (backend 53019b44). Except logging
rules: a custom_log key is CustomRuleIds.mint(workspace, text, title), a hash, so an import or
copy with the same rule title and text gets the original’s key again (measured 2026-10-22, CR-F7 in
Log Generator). Refusing a
client-chosen key that another policy already uses was not shipped: dev ws 3103 already holds 2
such duplicates (prod 0).
In the hub (frontend 8f8bc77d): Export all exports business and logging rules only (scan
profiles cannot be imported); Import refuses a file over 100 items before sending it —
“Import takes at most 100 policies; this file has N.”; Duplicate creates the copy Off; and
saving any policy On that was not On before (a new draft included) asks the Turn-on confirm first.
The hub
Section titled “The hub”/policies/<section>, with a left sub-nav (a <select> below lg):
| Section | Path | What it does |
|---|---|---|
| Overview | /policies | A status strip (On / Monitor only / Off counts from byKindStatus, business and logging rules only), Where your rules run (one row per feature with a coverage badge — Covered, Monitor only, Gaps, Not covered, Turned off in settings, No … yet — from model/coverage.js, and one fix button), a grouped needs-attention list with one-click fixes, recent changes |
| Policies | /policies/policies | Business and logging rules, 25 a page from the server (?page=&size=; scan profiles on their own tab, whose count ignores the other filters); filters (kind, feature, status, scope; query params deep-link, e.g. ?feature=ai_coding, ?kind=logging_rules); each row shows what the policy is doing (On / Monitor only / Off / Not running / On · judge afterwards) and where it runs; a ⋯ menu for status, duplicate, export and delete; Starter policies gallery; editor drawer (Rules / Applies to / Test / History); Import and Export all |
| Frameworks | /policies/frameworks[/<libraryKey>] | All served frameworks (On/Off, region, rule count, features); detail drawer (Rules, Where it runs, Coverage, Evidence, Versions). Where it runs lists the policies that use the framework and is seeded from the library policy. The Turn-on toast reports what the server enabled — the policy’s features, plus Code scans only when scanProfiles came back, otherwise “Code scans & log generation needs a project: no project got a scan profile.” (frontend 9a37fa43) |
| Data protection | /policies/data-protection | “What is protected right now” strip, then Masking (log, AI Warden and Cortex redaction, raw text of personal-data blocks), How much is stored (sampling) and AI coding |
| Where rules apply | /policies/applies[?open=<feature>] | One collapsible card per feature with the same coverage badge as the Overview; per relay key, collector, AI coding, Cortex and project; adding a policy from a key or collector row narrows it to that row; collector dry run behind Test a log line; the Gatekeeper threshold |
| Exceptions | /policies/exceptions | False positives (revoke), muted AI-coding issues (unmute), anomaly display filters |
| Alerts & monitors | /policies/alerts | Monitors (create/edit/dry run), notification channels, Discord (AI Warden) |
| Retention | /policies/retention | Retention policy, AI-coding session retention, collector tiers, and index lifecycle (ISM) under Advanced |
| Activity | /policies/activity | Audit feed with filters; Time, Changed by, What, Policy, Origin |
Writes are gated in the UI by the same role floors (hooks/usePolicyPermissions.js): a Viewer sees
no New policy, no status items and a read-only editor (“You can view this policy. Changing business
rules needs the Maintainer role.”); a Developer can change logging rules and scan profiles only.
Since frontend 1f6356f4 / 8f8bc77d (2026-10-21) the same gate covers the in-feature doors: the
AI Warden Business rules embed, a collector’s Business rules tab, AI Coding → Create rule and the
Policies empty-state starter grid. The server still checks: a 403 reads “You need the Maintainer
role to change business rules.” (Developer for code kinds).
Monitors and lifecycle policies need Maintainer (backend 4efd5906, 2026-10-21, dev and prod).
A non-GET call to alerting monitors, destinations / notification channels or _plugins/_ism through
either OpenSearch proxy — /api/v1/analytics and /api/v1/dashboards (including api/alerting/*,
api/ism/*, api/notifications/*) — from a Viewer or Developer answers 403 “You need the
Maintainer role to change monitors and lifecycle policies.” The Alerts and Retention sections hide
create, edit and delete below Maintainer. Before, a Viewer could create and delete monitors and ISM
policies (UXB-8). The monitor list searches with from=0, size=10000 and scopes the hits to the
workspace before cutting the page (backend f703c00f); it used to read 100 monitors cluster-wide
and then filter, so a workspace could see none of its own (BE-33).
Migration from the legacy stores
Section titled “Migration from the legacy stores”PolicyUnifyMigrator (@Order(60), run on ApplicationReadyEvent through
StartupMigrationRunner.runOnce) migrates each store under its own marker. Each step is idempotent
through policy.legacy_ref.
| Marker | Does |
|---|---|
policy-unify-v1-business | Each warden_rule_set → a business_rules policy with the same id (legacy_ref = warden_rule_set:<id>). ai_traffic on iff the set had relay keys (they become relay_key assignments); logs on iff applies_to had logs or agent_rule_set rows existed (only then become collector assignments); ai_coding iff applies_to had coding; cortex always off. Rules keep rule_key = <warden_rule.id>; NL policies become ai_judge items nl-<id>; frameworks become framework items. The id sequence is reset afterwards |
policy-unify-v1-scan | Each project_analyzer_rules_config row → a scan_profile (legacy_ref = template:<id>); log_scan_result.template_id is re-pointed to the new ids and its FK now references policy(id) (fk_log_scan_result_scan_profile) |
policy-unify-v1-logging | Each custom_log_rule → a logging_rules policy with one custom_log item (rule_key = the old rule id) |
policy-unify-v1-verify | Compares counts per workspace and checks every log_scan_result.template_id has a policy; a mismatch logs [PolicyUnify] verification failed and throws, so it re-runs next boot |
policy-legacy-drop-v1 | Only when logrite.policy.drop-legacy-tables=true (env LOGRITE_POLICY_DROP_LEGACY_TABLES, default false) and verify has passed: drops the legacy tables and warden_relay_key.rule_set_id / compliance_profile_id; logs [PolicyUnify] legacy rule tables dropped |
Status (2026-10-16): dropped on both estates. LOGRITE_POLICY_DROP_LEGACY_TABLES=true was set
on the dev and prod backends after a read-only archive of the legacy tables (CSV of the 11 rule sets
of the legacy logrite tenant, which the migration skips by design, plus a pg_dump of every
legacy table per estate) was written to a mode-700 directory on the dev VM. The logrite tenant’s
active relay keys were revoked first — see AI Warden.
What was removed
Section titled “What was removed”- Routes:
/api/v1/warden/rulesets*,/api/v1/warden/agents/{agentId}/rulesets*,/api/v1/warden/agents/{agentId}/evaluate-log,/api/v1/warden/rulesets/{id}/evaluate-log,/api/v1/custom-rules*,POST /api/v1/config/analyzerrules(the catalogue GETs stay) and/api/v1/relay/compliance/*. - Compliance profiles (
warden_compliance_profile, everycomplianceProfileId) and relay-keyruleSetId/ruleSetIds;RelayKeyViewnow carries read-onlypolicyIds. - Role floors
warden-rulesandcustom-rule-write. - The Business Rules modal,
RuleSetsView,customRulesApi.jsand the MCPwarden_rulestool (replaced bypolicies;judgemoved towarden_relay).