Skip to content

Rules & Policies

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 labelFeaturesStatusesRule types
business_rulesBusiness rulesai_traffic, logs, ai_coding, cortexOn, Monitor only, Offai_judge, all deterministic types, framework
logging_rulesLogging rulescodeOn, Offcustom_log
scan_profileScan profilecodeOn, Offcatalogue; 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 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.

WireUI wordTone + iconMeaning
enforcingOngreen, shieldRules act with their configured action: Block blocks, Flag flags
evaluateMonitor only (business_rules only)blue, eyeThe backend caps every block to notify (below)
offOffgrey, pauseThe 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 merged mode to inline_blocking or failMode to fail_closed, and its legacy policyText is not appended; judgeModel, sampleRate and rulesEnabled merge 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.

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):

  1. Off → Off: “Checks nothing, anywhere.”
  2. No features → Not running (amber): “It applies to nothing yet. Choose where it applies.”
  3. 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_projects for the same case (below).
  4. 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 (backend 01b2d44d), so a policy whose rules are all disabled is no longer “On” in the list and “Not running” in the editor.
  5. Only pattern rules, with “Run these rules” off → Not running: “Its only rules are pattern rules, and “Run these rules” is off.”
  6. Monitor only → Monitor only: “Records what it would block. Blocks nothing.”
  7. On, with mode: async_observe and 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.”
  8. 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: workspace (“Whole workspace”, the default) or projects (“Selected projects”, with projectIds; 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). A logging_rules policy sent with features: [] is saved with ["code"], its only feature (measured 2026-10-22).
  • Narrowing exists on two features only:
    • relayKeyIds narrows ai_traffic; empty means every key, including future keys.
    • collectorIds narrows logs; 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 01b2d44d and 955701d5, 2026-10-21, dev and prod). A PUT /{id} that omits (or sends null for) scope, projectIds, features, rules, relayKeyIds or collectorIds keeps the stored value; an explicit [] clears a target list, and features: [] is still refused. Before, omitting scope widened a project-scoped policy to the whole workspace (BE-4) and omitting rules deleted 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 (UI label)ai_trafficlogsai_codingcortexcode
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.

LimitValueAnswer
One pattern1,000 characters“rules[i].config.pattern must be at most 1000 characters”
All patterns of one policy20,000 characters“the patterns of one policy may total at most 20000 characters”
ShapeNo 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)”
LimitDefaultWhen it is reachedSetting (0 or security.public-rate-limit.enabled=false = off)
Writes (POST, PUT, PATCH, DELETE), per credential1,200 an hour, refilled continuously429 with Retry-Aftersecurity.public-rate-limit.policy-write-per-hour
Tests (/{id}/test, /effective/test) and /preview-logging-rule, per credential600 an hour429 with Retry-Aftersecurity.public-rate-limit.policy-test-per-hour
Pattern checks running at once, per backend instance4waits up to 5 s, then 503 policy_validation_busy, Retry-After: 5LOGRITE_POLICY_REGEX_PERMITS
Logging-rule interpretations running at once, per backend instance8waits up to 5 s, then 503 logging_rule_compile_busy, Retry-After: 5LOGRITE_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-typed settings field).
  • 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.

FeatureProject id used
ai_trafficThe request’s X-Warden-Project-Id if it belongs to the workspace, else the legacy relay_compliance_binding.project_id, else none
logsNone — project-scoped policies never apply
ai_codingThe plugin’s .logrite/config.json projectId, sent as ?projectId= on the policy fetch
cortexThe /chatv2 body projectId, validated by the backend; /chat and /chat/sync have none
codeThe 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.

TableHolds
policyworkspace_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_ruleOne item: rule_key (unique per policy), type, name, text, config (jsonb), action, severity, enabled, sort_order
policy_assignmenttarget_type (project / relay_key / collector) + target_id, unique per policy
policy_versionSnapshot 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 in businessRuleIds. NL judge policies carry the policy_rule.id, recorded in judge.policyIds.

  • Collector logs and the dry runs (/{id}/test, /effective/test): the analyzer’s evaluator puts the same p<policyId>.<ruleKey> on every matchedRules[] entry and every deterministic finding as ruleId (analyzer 18d874cf). PolicyFindingMapper attributes a finding by its own ruleId, then by matchedRules[].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 (backend 2dcff896). 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 two regex rules from different policies matching one line overwrote each other and one flag survived. Now:

    • each flag stores ruleId (keyword) and policyId (long). For a deterministic rule ruleId is p<policyId>.<ruleKey> and policyId is parsed from it; for a plain-English rule ruleId is the judge policy id (policy_rule.id) and policyId comes from the logs bundle’s nlPolicies[].policyId (backend 1987c06f + analyzer 639e749d, 2026-10-21; null before); a framework finding has neither;
    • each flag’s severity is the rule’s own (medium when the rule has none): every bundle rule carries severity, omitted when null (backend 936dad6e, analyzer 639e749d). Before, every collector flag read medium;
    • 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 no ruleId), so two rules matching the same line are two flags;
    • GET /api/v1/agents/{id}/flags returns ruleId, policyId and policyName — 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.

  • AI coding: business findings are business.rule-<ruleKey>. Migrated rules keep their old warden_rule.id as 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}/status accepts an optional revision and 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, backend e7e56ff4).
  • 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).

MethodPathPurpose
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}/restoreRestore 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/overviewcounts (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/effectivefeature, projectId, relayKeyId, collectorId → {policies}
GET/effective/allPer 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/testSame over the effective bundle (replaces the collector dry run)
GET/{id}/activity?days=7Matches in warden-requests-<ws> and coding_issue (days 1–30)
GET/activityHub audit feed; page, size, policyId, actor, from, to. Each item carries revision and, for policy_* actions, the version’s origin
GET/export?ids=1,2Downloads logrite-policies-<yyyyMMdd>.json; exportedAt has microsecond precision
POST/importCreates 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 (audit warden_suppression_removed).
  • GET /api/v1/warden/library/frameworks now returns enabled and enabledFeatures per framework. All 30 library frameworks are accepted everywhere through FrameworkCatalog.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.

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.

  • AI traffic — WardenBindingResolver resolves the key and project, then effective(ws, "ai_traffic", project, keyId, null) → PolicyBundleBuilder (most-restrictive wins, policies absorbed in id order). ruleSetId on the wire is the first policy id. The legacy compliance-profile fallback is gone.
    • Duplicate rules (same type and config in two policies) keep one bundle entry; since backend 1987c06f (2026-10-21, dev and prod) the stricter action wins and the entry takes that rule’s id, action and severity. 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 lastUsedAt only 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.
  • Collector logs — effective(ws, "logs", null, null, agentId), always notify-only. The bundle’s nlPolicies[] carry policyId, and every rule its severity.
  • 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 and ACTIVE) 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-hint links), 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.

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.

/policies/<section>, with a left sub-nav (a <select> below lg):

SectionPathWhat it does
Overview/policiesA 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/policiesBusiness 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/exceptionsFalse positives (revoke), muted AI-coding issues (unmute), anomaly display filters
Alerts & monitors/policies/alertsMonitors (create/edit/dry run), notification channels, Discord (AI Warden)
Retention/policies/retentionRetention policy, AI-coding session retention, collector tiers, and index lifecycle (ISM) under Advanced
Activity/policies/activityAudit 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).

PolicyUnifyMigrator (@Order(60), run on ApplicationReadyEvent through StartupMigrationRunner.runOnce) migrates each store under its own marker. Each step is idempotent through policy.legacy_ref.

MarkerDoes
policy-unify-v1-businessEach 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-scanEach 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-loggingEach custom_log_rule → a logging_rules policy with one custom_log item (rule_key = the old rule id)
policy-unify-v1-verifyCompares 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-v1Only 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.

  • 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, every complianceProfileId) and relay-key ruleSetId/ruleSetIds; RelayKeyView now carries read-only policyIds.
  • Role floors warden-rules and custom-rule-write.
  • The Business Rules modal, RuleSetsView, customRulesApi.js and the MCP warden_rules tool (replaced by policies; judge moved to warden_relay).