Veefore docs

Risk engine

Thirteen named signals, weighted into an explainable score that policy can test — with no model you cannot interrogate.

Risk runs as stage 8 of the pipeline, before approval, because a policy rule can test the risk score and the signals that produced it. The output is a level, a score from 0 to 100, and the list of signals that fired with their individual contributions.

Explainable by construction

There is no opaque model here. Every score decomposes into named signals, each with a weight you set and a human-readable explanation of what it observed.

That is a requirement, not a preference. A refusal an operator cannot explain to the person it affected is a refusal that gets overridden, and then switched off. "Blocked, risk score 87" is useless. "Blocked: amount is 4.2× this agent's trailing median, and the destination has never been contacted before" is something you can act on.

#The signals

Signal Fires when
UNUSUAL_AMOUNT The amount is far outside this agent's own history
UNUSUAL_DESTINATION The destination is rare or newly seen for this agent
VELOCITY_ANOMALY Action rate is well above the agent's baseline
TIME_ANOMALY Activity at an hour this agent does not normally act
NEW_TOOL First use of this tool by this agent
NEW_RESOURCE First touch of this resource
NEW_RECIPIENT First time acting against this recipient
IDENTITY_ANOMALY Something inconsistent about the identity presentation
CREDENTIAL_ANOMALY Unusual credential use — stale, over-leased, unexpected scope
DELEGATION_ANOMALY An unusual chain: unexpected depth, or a path not seen before
POLICY_VIOLATION Recent refusals by this agent
TRUST_EVIDENCE Required trust evidence is missing, expiring or expired
BEHAVIORAL_DEVIATION The overall pattern departs from the agent's established behaviour

Several of these are baseline signals — they compare against what this agent has done before, not against a global average. A finance agent that moves large amounts all day has a high baseline and does not trip UNUSUAL_AMOUNT; a read-only agent that suddenly moves one does.

That also means a brand-new agent trips the "new" signals on almost everything it does. That is correct behaviour and worth planning for: run new agents in OBSERVE or SIMULATE long enough to establish a baseline before promoting.

#What a risk assessment returns

json
{
  "level": "HIGH",
  "score": 72,
  "signals": [
    {
      "type": "UNUSUAL_AMOUNT",
      "weight": 30,
      "explanation": "Amount 120000 is 4.2× this agent's trailing median of 28500."
    },
    {
      "type": "UNUSUAL_DESTINATION",
      "weight": 22,
      "explanation": "payments-eu.partner.example.com has not been contacted by this agent before."
    },
    {
      "type": "TIME_ANOMALY",
      "weight": 20,
      "explanation": "03:14 UTC is outside this agent's usual 08:00–19:00 activity."
    }
  ]
}

On a decision, explanation.risk.top_signals carries the three highest-weighted explanations, so the reason a request was escalated is in the response the agent already received.

#Using risk in policy

This is the point of computing it.

json
{
  "id": "rule-high-risk-approval",
  "name": "High risk needs a human",
  "priority": 600,
  "enabled": true,
  "when": {
    "kind": "condition",
    "id": "c-risk-score",
    "field": "risk.score",
    "operator": "gte",
    "value": 70
  },
  "then": {
    "decision": "REQUIRE_APPROVAL",
    "reason": "Risk score is at or above the review threshold.",
    "approval": { "routingMode": "SINGLE", "requiredApprovals": 1, "ttlSeconds": 900 }
  }
}

Testing a specific signal is often better than testing the score, because it says what you actually mean:

json
{
  "id": "rule-new-destination-large-amount",
  "name": "Block large amounts to unseen destinations",
  "priority": 820,
  "enabled": true,
  "when": {
    "kind": "group",
    "id": "g-new-dest-large",
    "operator": "and",
    "children": [
      {
        "kind": "condition",
        "id": "c-signal-new-dest",
        "field": "risk.signals",
        "operator": "contains",
        "value": "UNUSUAL_DESTINATION"
      },
      { "kind": "condition", "id": "c-large", "field": "amount", "operator": "gt", "value": 50000 }
    ]
  },
  "then": {
    "decision": "BLOCK",
    "reason": "Large amounts to a destination this agent has never used are refused."
  }
}

Three fields are available: risk.level, risk.score and risk.signals. The velocity and history counters the signals are built from are exposed separately too — velocity.actions_last_minute, history.tool_used_before and the rest — so you can write a rule on the raw observation rather than on the derived signal when that is clearer.

#Tuning

http
PUT /v1/risk/configuration

Signal weights and level thresholds are yours. Tuning is an audited change requiring risk:configure.

Approach it from the evidence rather than from intuition:

  1. Run in OBSERVE or SIMULATE and let real traffic accumulate.
  2. Open the Risk Center and look at the score distribution and the signal breakdown.
  3. A signal firing on nearly everything is not discriminating — lower its weight or fix the underlying baseline.
  4. A signal that never fires is either weighted too low or genuinely not relevant to your traffic.
  5. Set thresholds where the distribution has a natural gap.

Raising a threshold is a security change

Moving the HIGH threshold from 70 to 85 silently removes approval requirements from every rule that tests it. It is audited for exactly that reason. Simulate before and after, and look at what stops being escalated.

#Risk events

Every assessment that crosses a threshold writes a risk event with its signal evidence attached.

http
GET /v1/risk/events
GET /v1/risk/overview

The overview is what the Risk Center renders: counts by level, the signal breakdown, the highest-risk tools, the destinations in play, policy violations and credential anomalies. Events can be acknowledged with risk:acknowledge, so "we have looked at this" is recorded rather than remembered.

Identity, authority, policy and enforcement for autonomous AI agents.