Add comprehensive documentation and GitHub Copilot configuration: - docs/knowledge/: 17 deep-dive knowledge files covering value semantics, RVM architecture, builtins, FFI boundary, feature composition, error handling migration, policy evaluation security, Rego semantics, interpreter/compiler architecture, Azure Policy/RBAC, engine API, time builtins, language extension guide, tooling architecture, causality/partial eval, Rego compiler, Azure Policy aliases, and telemetry/diagnostics - .github/agents/: 16 role-specific AI agent definitions (red-teamer, semantics-expert, architect, performance-engineer, test-engineer, verification-engineer, security-auditor, reliability-engineer, support-engineer, ci-engineer, refactorer, api-steward, program-manager, demo-engineer, dx-engineer, tech-lead) - .github/skills/: 6 workflow skill definitions (thorough-review, design-alternatives, add-builtin, opa-conformance, security-review, verification) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
5.7 KiB
Knowledge: Error Handling Migration
Deep knowledge about regorus's error handling patterns and the ongoing
migration from anyhow to thiserror. Read this before adding error
handling to new code or modifying existing error paths.
Current State
The codebase has two error handling approaches coexisting:
Legacy: anyhow (widespread)
Most of the codebase uses anyhow::Result with bail!() and anyhow!():
use anyhow::{anyhow, bail, Result};
fn eval_something(&mut self) -> Result<Value> {
let v = map.get("key").ok_or_else(|| anyhow!("missing key"))?;
if condition_fails {
bail!("evaluation failed: {reason}");
}
Ok(value)
}
Found in: src/interpreter.rs, src/engine.rs, src/parser.rs,
src/lexer.rs, src/value.rs, src/number.rs, src/builtins/, and most
other modules.
Target: thiserror (RVM leads)
The RVM uses strongly typed error enums:
use thiserror::Error;
#[derive(Error, Debug, Clone, PartialEq)]
pub enum VmError {
#[error("Execution stopped: exceeded maximum instruction limit of {limit} after {executed} instructions (pc={pc})")]
InstructionLimitExceeded { limit: usize, executed: usize, pc: usize },
#[error("Register index {index} out of bounds (pc={pc}, register_count={register_count})")]
RegisterIndexOutOfBounds { index: u8, pc: usize, register_count: usize },
// ... 30+ variants covering every VM error case
}
pub type Result<T> = core::result::Result<T, VmError>;
Found in: src/rvm/vm/errors.rs
The VmError Pattern (Reference Implementation)
Key design principles visible in VmError:
1. Every variant carries context:
InstructionLimitExceeded { limit: usize, executed: usize, pc: usize }
Not just "limit exceeded" — includes the limit, actual count, and program counter.
2. Program counter in every variant:
// Every single variant includes `pc: usize`
RegisterNotObject { register: u8, value: Value, pc: usize },
LiteralIndexOutOfBounds { index: u16, pc: usize },
This is a debugging aid — every error can be traced to the exact instruction.
3. Exhaustive coverage: 30+ variants covering every known error case. No catch-all "Other(String)".
4. Derives Clone and PartialEq:
#[derive(Error, Debug, Clone, PartialEq)]
Clone enables error propagation without ownership transfer. PartialEq enables testing error conditions precisely.
5. Type alias for ergonomics:
pub type Result<T> = core::result::Result<T, VmError>;
6. Bridge from anyhow:
impl From<anyhow::Error> for VmError {
fn from(err: anyhow::Error) -> Self {
VmError::ArithmeticError { message: format!("{}", err), pc: 0 }
}
}
This allows the RVM to call into legacy code that returns anyhow::Result.
Migration Strategy
For New Code
Always use thiserror. Define a module-specific error enum:
use thiserror::Error;
#[derive(Error, Debug, Clone, PartialEq)]
pub enum MySubsystemError {
#[error("invalid input: {0}")]
InvalidInput(String),
#[error("resource limit exceeded: {current} > {limit}")]
ResourceLimitExceeded { current: usize, limit: usize },
}
pub type Result<T> = core::result::Result<T, MySubsystemError>;
For Existing Code
When modifying existing functions that use anyhow:
- Within the same module: continue with
anyhowfor consistency - At module boundaries: consider wrapping
anyhow::Errorin a typed variant - Incremental migration: converting a whole module at once is better than mixing styles within a single module
Bridge Pattern
When typed-error code calls anyhow code (or vice versa):
// Typed → anyhow (automatic via anyhow's From impl)
fn caller() -> anyhow::Result<Value> {
typed_function()?; // VmError auto-converts to anyhow::Error
Ok(value)
}
// Anyhow → typed (explicit conversion needed)
fn caller() -> Result<Value, VmError> {
anyhow_function().map_err(|e| VmError::Internal {
message: format!("{}", e),
pc: current_pc,
})?;
Ok(value)
}
Error Message Guidelines
For OPA Conformance
Builtin error messages must match OPA exactly — the conformance test suite compares literally. When implementing builtins, check the OPA Go source.
For Internal Errors
- Include enough context to diagnose without a debugger
- Include identifiers (register index, PC, rule name, etc.)
- Don't include sensitive data (user input, policy content)
- Use structured fields, not string formatting:
// ✗ Bad
#[error("register {0} out of bounds at pc {1}")]
RegisterOutOfBounds(u8, usize),
// ✓ Good — named fields are self-documenting
#[error("register index {index} out of bounds (pc={pc}, register_count={register_count})")]
RegisterIndexOutOfBounds { index: u8, pc: usize, register_count: usize },
Panic Safety Connection
Error handling is the front line of panic safety. The deny lints forbid
unwrap(), expect(), panic!(), etc. Every fallible operation must return
Result. This is not just style — in daemon mode, a panic crashes the service.
The error migration makes this stronger: with typed errors, every failure mode
is enumerated and the compiler ensures all are handled. With anyhow, errors
are opaque and may be accidentally swallowed.
no_std Compatibility
Both anyhow and thiserror support no_std with default-features = false:
anyhow = { version = "1.0", default-features = false }
thiserror = { version = "2.0", default-features = false }
Error types must use alloc::string::String instead of std::string::String
and avoid std::io::Error without a feature gate.