RiskEngine assigns every tool call one of four tiers, and escalate_at turns that tier into a gate: any permitted action at or above the threshold is routed to a human approver before it runs.
The four tiers
RiskTier is a string enum defined in infy/governance/types.py. The tiers are ordered by severity.
Severity ordering is exposed through
tier_at_least(tier, threshold), which returns True when tier is at or above threshold. The engine and the escalation gate both rely on this ranking.
How a tier is assigned
RiskEngine.assess(tool, args) resolves a tier from the tool’s static metadata. It checks three sources in order and returns the first that applies.
1
Explicit risk_tier wins
If the tool declares
risk_tier, that value is used verbatim: RiskTier(tool.risk_tier). This is the override. A tool author who knows the risk states it directly.2
Verb mapping
Otherwise, if the tool declares a
verb and that verb is known, the engine maps it to a tier (see the table below). The lookup is case-insensitive: tool.verb.upper() is matched.3
Side-effect default
If neither is set, the engine falls back to the tool’s
side_effect flag. A side-effecting tool becomes HIGH. A pure or read-only tool becomes LOW.Tool fields (from infy/tools.py) are all optional governance metadata: risk_tier, verb, and side_effect. If none are set on a tool, side_effect defaults to False, so an unannotated tool assesses as LOW.
The verb table
When a tool setsverb but not risk_tier, the engine uses this fixed mapping from infy/governance/risk.py.
A verb outside this table is ignored, and resolution falls through to the
side_effect default.
Profiling a tool
Attach the metadata when you define the tool. Any of the three inputs works. Pick the most specific one you can.When
risk_tier and verb disagree, risk_tier wins because it is checked first. In transfer_funds above, the explicit "critical" is used, not the DB verb’s HIGH.Risk gates the decision
Assessing a tier is only half the story. On its own, the tier is an annotation on the audit record.escalate_at, set on the Governance object, is what makes risk gate the run.
Inside Governance._authorize, the flow is:
tier = self.risk.assess(tool, args)scores the call.- The policy engine evaluates the request. A
FORBIDblocks it outright. - For a permitted request, approval is required when the policy attached a
REQUIRE_APPROVALobligation or whenescalate_at is not None and tier_at_least(tier, self.escalate_at).
escalate_at defaults to RiskTier.HIGH. So out of the box, every HIGH and CRITICAL action is sent to the approver, and the default approver is DenyAll. The practical effect: an unprofiled, side-effecting tool assesses as HIGH, hits the gate, and is denied by default unless a human approves it.
Where to go next
Policy
Deny-by-default authorization that runs before risk is scored.
Approval
Human-in-the-loop review, including durable approval that suspends a run.
Audit
The tamper-evident, hash-chained record where each tier lands.
Governance overview
How policy, risk, approval, and audit compose on the agent loop.