1
Moderation-Engine
Hermes Agent edited this page 2026-10-21 10:57:02 +00:00

Moderation Engine

A rule engine that acts on VK activity without the model in the loop. It exists because a community needs consistent, instant moderation that does not depend on a language model being awake, in budget, or in the mood.

Rule shape

interface Rule {
  id: string                      // stable, human-readable: 'spam-links', 'welcome-ban'
  enabled: boolean
  match: {
    type: 'keyword' | 'regex' | 'newMember' | 'link' | 'externalLink' | 'repeat'
    pattern?: string              // keyword list (comma-separated) or regex source
    flags?: string                // regex flags, default 'iu'
    scope: Array<'message' | 'comment' | 'wallPost' | 'boardReply'>
  }
  action: {
    kind: 'ban' | 'delete' | 'warn' | 'notify'
    durationH?: number            // 0 or absent = permanent
    reason?: number               // VK ban reason code
    comment?: string              // shown in the ban list
    notifyText?: string           // for 'notify'
  }
  exempt?: {
    userIds?: number[]
    minAdminLevel?: number        // never act on users at or above this level
    ignoreStaff?: boolean         // default true
  }
}

Match types

Type Behaviour
keyword case-insensitive substring over a comma-separated list; word-boundary aware for Cyrillic and Latin
regex user-supplied source + flags; compiled once at load, invalid regex fails the rule with a clear error (not a silent skip)
newMember fires only when the author joined within pattern minutes (default 10) — the classic spam-bot signature
link any URL in the text
externalLink a URL whose host is not in the community's links block
repeat the same user posted ≥ N times (N from pattern) within 10 minutes

Evaluation order

Rules evaluate in declaration order. First match wins for actions, but warn rules accumulate: a user can collect warnings from multiple rules before hitting the threshold.

event arrives
  → exempt check (userIds, admin level, staff)
  → for each enabled rule whose scope includes the event type:
       match? → execute action
              → write audit
              → if action is 'warn': increment counter, check threshold
  → deliver event to agent (always, with an auto-moderated prefix if anything fired)

Warn threshold

moderation.warnThreshold (default 3). Counters live in ctx.storage under vk.warn.<userId> with a rolling 30-day window. On reaching the threshold:

  1. The user is banned for durationH from the threshold-crossing rule, or permanently if unset.
  2. The counter resets.
  3. An audit record notes trigger: 'warn-threshold'.

Where it runs

Two entry points, same engine:

  1. Inbound — on every LongPoll event before delivery to the agent (events/inbound.ts).
  2. Outbound — as a tools/pre-execute listener, checking posts/comments the agent itself is about to create. A rule that would ban a user also blocks the agent from posting content that matches a delete rule — the agent cannot launder banned content through its own tools.

The engine never calls the model. It calls VK API directly through the same client, so rate limiting and error handling are shared.

Storage

Key Content
vk.rules Rule[] — the full rule set
vk.warn.<userId> { count: number; firstAt: number; lastAt: number }
vk.audit append-only array of audit records (bounded to last 10 000, rotated)

Rules are edited through vk_moderation_rules, not by hand-editing storage. Every write is validated against the Rule schema before it lands.

Audit record

interface AuditRecord {
  at: number                        // unix ms
  source: 'tool' | 'rule' | 'longpoll'
  tool?: string                     // e.g. 'vk_ban_manage'
  ruleId?: string
  action: string
  target?: { userIds?: number[]; postId?: number; commentId?: number; topicId?: number }
  before?: unknown                  // for settings changes
  after?: unknown
  ok: boolean
  error?: string
  token: '<redacted>'               // never the real token
}

The audit log is written from tools/result (a final observer) for tool calls and directly by the engine for rule actions. It is append-only from the plugin's perspective — no tool deletes audit entries.

Safety invariants

  1. Never act on staff. exempt.ignoreStaff defaults to true; the engine resolves admin_level for the target before acting and skips anyone at or above minAdminLevel (default 1).
  2. Never act on the community's own id or on ids in moderation.exemptUserIds.
  3. Never ban without a reason code — VK requires one for visible bans, and an unexplained ban is a support ticket.
  4. delete is preferred over ban for first offences unless the rule says otherwise; deletion is reversible, bans are not (from the user's view).
  5. Dry-run mode. moderation.enabled: true plus safety.dryRunDefault: true means rules evaluate and audit but do not execute. Use this to tune a rule set against a week of real traffic before turning it on.

Example rule set

[
  {
    "id": "spam-links",
    "enabled": true,
    "match": { "type": "externalLink", "scope": ["message", "comment", "wallPost"] },
    "action": { "kind": "delete", "comment": "Внешние ссылки запрещены" },
    "exempt": { "ignoreStaff": true }
  },
  {
    "id": "new-member-crypto",
    "enabled": true,
    "match": { "type": "keyword", "pattern": "крипт,казино,ставки,заработок", "scope": ["message", "comment"] },
    "action": { "kind": "ban", "durationH": 720, "reason": 1, "comment": "Спам" },
    "exempt": { "ignoreStaff": true }
  },
  {
    "id": "first-post-welcome",
    "enabled": true,
    "match": { "type": "newMember", "pattern": "10", "scope": ["wallPost"] },
    "action": { "kind": "notify", "notifyText": "Проверьте первый пост новичка" }
  }
]

Limitations

  • No ML classification. Keyword and regex only — deliberate: predictable, auditable, cheap.
  • No cross-community reputation. Each community's engine is independent.
  • No automatic appeal flow. A banned user must contact the community through other means.
  • Rule ordering matters and is not visible in the UI beyond the list order. Keep rule ids descriptive.