regorus 0.14.0

A fast, lightweight Rego (OPA policy language) interpreter
Documentation
# Regorus CSharp

**Regorus** is

  - *Rego*-*Rus(t)*  - A fast, light-weight [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/)
   interpreter written in Rust.
  - *Rigorous* - A rigorous enforcer of well-defined Rego semantics.

See main [Regorus page](https://github.com/microsoft/regorus) for more details about the project.

# Building

## Github Actions

The simplest way to build a Nuget for Regorus' C# bindings is to use Github Actions. The action to do so is named `bindings/csharp` and is defined in `.github/workflows/test-csharp.yml`.

There are two ways to trigger a Nuget build.
1. Runs are triggered automatically whenever a push or pull request is made to the `main` branch.
2. A run can be triggered manually by navigating to the action in the Github UI and clicking `Run workflow`. This option allows you to generate a Nuget for any branch, which is useful when testing the integration of in-progress changes to Regorus with other projects. Nuget files that are generated via this flow will have a `manualtrigger` suffix appended to their version number, making it easy to distinguish them from Nugets generated using the `main` branch.
![Image displaying the run workflow button](docs/images/readme/manuallytriggeringrun.png)

Once the workflow run completes, the generated Nuget can be downloaded by following these steps:
1. Open the run.
2. Click on `Build Regorus nuget` on the left.
3. Expand the `Upload Regorus nuget` step.
4. Click the `Artifact download URL` link at the bottom.
5. Save and extract the downloaded zip file to find the `.nupkg` file.
![Image displaying the download URL link](docs/images/readme/downloadnuget.png)

## Local

The `cargo xtask` runner provides helpers for local builds:

1. `cargo xtask ffi` builds the `bindings/ffi` crate for the host platform in debug mode. Add `--target <triple>` (repeatable) to cross-compile, or `--release` to produce optimised artefacts. Results land under `bindings/ffi/target/<triple>/<profile>`.
2. `cargo xtask nuget` reuses those artefacts to pack the C# library. It defaults to debug builds for the host but accepts `--target`, `--release`, `--artifacts-dir <path>` to reuse existing binaries, and `--enforce-artifacts` to require every officially supported platform.
3. `cargo xtask test-csharp` ensures a NuGet is available (rebuilding when required or when `--force-nuget` is passed) and then runs `Regorus.Tests`, `TestApp`, and `TargetExampleApp` against it. The command accepts the same build flags as `cargo xtask nuget`.

## Memory Usage Safeguards

The C# bindings expose allocator-backed memory tracking utilities via the static `Regorus.MemoryLimits` helper. Typical usage:

```csharp
// Restrict total allocations to 128 MiB for the process
Regorus.MemoryLimits.SetGlobalMemoryLimit(128 * 1024 * 1024);

// Optional: tune how frequently each thread flushes its allocation counters
Regorus.MemoryLimits.SetThreadFlushThresholdOverride(256 * 1024);

// Engine operations throw InvalidOperationException with the allocator message if the budget is exceeded
using var engine = new Regorus.Engine();
var veryLargeJson = new string('x', 128 * 1024);
try
{
    engine.SetInputJson(veryLargeJson);
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Allocator reported: {ex.Message}");
}

// Restore defaults once done
Regorus.MemoryLimits.SetGlobalMemoryLimit(null);
Regorus.MemoryLimits.SetThreadFlushThresholdOverride(null);
```

See bindings/csharp/Regorus.Tests/RegorusTests.cs for scenario coverage and bindings/csharp/TargetExampleApp/Program.cs for end-to-end usage.

## RVM Usage Example

The RVM API lets you compile a program from modules/entrypoints and execute it in a VM:

```csharp
using Regorus;

const string Policy = """
package demo
default allow = false
allow if {
  input.user == "alice"
  some role in data.roles[input.user]
  role == "admin"
}
""";

const string Data = """
{ "roles": { "alice": ["admin"] } }
""";

const string Input = """
{ "user": "alice" }
""";

var modules = new[] { new PolicyModule("demo.rego", Policy) };
var entryPoints = new[] { "data.demo.allow" };

using var program = Program.CompileFromModules(Data, modules, entryPoints);
var listing = program.GenerateListing();

using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetDataJson(Data);
vm.SetInputJson(Input);

var result = vm.Execute();
Console.WriteLine($"allow: {result}");
```

### RVM instruction budget

Use `SetMaxInstructions` to configure the maximum number of dispatched RVM
bytecode instructions for a VM:

```csharp
vm.SetMaxInstructions(25_000);
```

The default is 25,000 and zero permits no dispatches. A fresh execution or
`LoadProgram` resets the consumed count, while `Resume` preserves it across
suspendable execution. Changing the maximum while suspended replaces the limit
without resetting consumption. Exhaustion is reported as the existing generic
`InvalidOperationException`; the C# setter does not expose a consumed-count
getter. Values that cannot fit the native pointer width are rejected instead
of being truncated.

### Per-execution memory budget

RVM run-to-completion and suspendable evaluation can use an optional memory
budget. Each accepted initial `Execute` or `ExecuteEntryPoint` call snapshots
the configured budget; `Resume` continues the same budget, including when it
runs on a different thread. Program compilation, program loading, and prior
`SetDataJson`, `SetInputJson`, and `SetContextJson` calls occur before and
outside that budget. Changing or clearing the configuration affects only the
next initial execution.

```csharp
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetDataJson(Data);
vm.SetInputJson(Input);
vm.SetMemoryBudgetConfig(new MemoryBudgetConfig(16 * 1024 * 1024));

try
{
    var result = vm.Execute();
}
catch (RegorusMemoryBudgetExceededException ex)
{
    Console.WriteLine(ex.Message);
}
```

The native execution budget includes resume C-string copying and JSON parsing, VM resume work, result JSON serialization, and Rust `CString` allocation before the native call returns. Managed UTF-8 decoding and C# `string` allocation after that return are not charged.

Run-to-completion accounting observes live-byte deltas; suspendable accounting
accumulates the requested size of each successful allocation and reallocation
across the initial execution and all resumes. Frees do not refund suspendable
usage. Both are cooperative budgets, not allocation-time peak-memory caps. A
single instruction, builtin, native serialization, or `CString` allocation
may exceed the limit before the next checkpoint; configure headroom for that
overshoot.

The VM retains suspendable usage as a cumulative scalar and selects it only
during each synchronous segment, so a resume may migrate threads without
resetting consumption. Native resume parsing, inner resume, and immediate
serialization share one accounting window.
Successful reallocation charges the full new requested size to the selected
execution, while failed reallocation charges nothing. Run-to-completion
accounting keeps its existing execution-thread baseline and downward ratchet.
See the [RVM memory budget documentation](../../docs/limits/memory_budget.md)
for the detailed accounting model.

Terminal budget or runtime failures invalidate retained results and clear execution state. Missing/unexpected resume input and malformed JSON are retryable while a valid continuation remains, but allocation requests consumed by malformed native input are not refunded and may exhaust the budget on a later valid resume. Native status 11 and `RegorusMemoryBudgetUnsupportedException` remain reserved for compatibility and are not used to reject suspendable execution. The process-wide limit exposed by `MemoryLimits` remains a separate safeguard.

## RVM with Registered Host-Await Builtins

Host-await builtins let you register custom function names at compile time.
When the VM encounters a call to one of these functions, it suspends execution
so the host can resolve the call externally and resume with a value.

### Suspendable mode (resolve one call at a time)

```csharp
using Regorus;

const string Policy = """
package demo
import rego.v1

default allow := false

allow if {
  account := get_account({"id": input.account_id})
  account.status == "active"
}
""";

var modules = new[] { new PolicyModule("demo.rego", Policy) };
var entryPoints = new[] { "data.demo.allow" };
var builtins = new[] { new HostAwaitBuiltin("get_account", 1) };

using var program = Program.CompileFromModules("{}", modules, entryPoints, builtins);
using var vm = new Rvm();
vm.SetExecutionMode(ExecutionMode.Suspendable);
vm.LoadProgram(program);
vm.SetInputJson("""{"account_id": "acct-42"}""");

// First Execute suspends when get_account() is called
vm.Execute();

// Inspect which builtin suspended and what argument was passed
var identifier = vm.GetHostAwaitIdentifier();  // "get_account"
var argument = vm.GetHostAwaitArgument();       // [{"id":"acct-42"}]

// Resolve externally, then resume
var result = vm.Resume("""{"status": "active", "name": "Alice"}""");
Console.WriteLine($"allow: {result}");  // true
```

### Run-to-completion mode (pre-load responses)

```csharp
using Regorus;

const string Policy = """
package demo
import rego.v1

default greeting := "unknown"

greeting := msg if {
  msg := translate(input.lang)
}
""";

var modules = new[] { new PolicyModule("demo.rego", Policy) };
var entryPoints = new[] { "data.demo.greeting" };
var builtins = new[] { new HostAwaitBuiltin("translate", 1) };

using var program = Program.CompileFromModules("{}", modules, entryPoints, builtins);
using var vm = new Rvm();
vm.SetExecutionMode(ExecutionMode.RunToCompletion);
vm.LoadProgram(program);
vm.SetInputJson("""{"lang": "es"}""");

// Queue responses before execution. SetHostAwaitResponses atomically replaces
// ALL prior responses for every identifier — pass every identifier the policy
// may invoke in a single call.
vm.SetHostAwaitResponses(new Dictionary<string, IReadOnlyList<string>>
{
    ["translate"] = new[] { "\"hola\"" },
});

var result = vm.Execute();
Console.WriteLine($"greeting: {result}");  // "hola"
```

## Azure RBAC Condition Evaluation

Evaluate Azure RBAC condition expressions directly with a JSON evaluation context:

```csharp
using Regorus;

const string Condition = "@Resource[owner] StringEquals 'alice'";
const string ContextJson = """
{
  "principal": {
    "id": "user-1",
    "principal_type": "User",
    "custom_security_attributes": {}
  },
  "resource": {
    "id": "/subscriptions/s1",
    "resource_type": "Microsoft.Storage/storageAccounts",
    "scope": "/subscriptions/s1",
    "attributes": {
      "owner": "alice",
      "confidential": true
    }
  },
  "request": {
    "action": "Microsoft.Storage/storageAccounts/read",
    "data_action": null,
    "attributes": {
      "clientIP": "10.0.0.1"
    }
  },
  "environment": {
    "is_private_link": null,
    "private_endpoint": null,
    "subnet": null,
    "utc_now": "2023-05-01T12:00:00Z"
  },
  "action": "Microsoft.Storage/storageAccounts/read",
  "suboperation": null
}
""";

var allowed = RbacEngine.EvaluateCondition(Condition, ContextJson);
Console.WriteLine($"RBAC condition allowed: {allowed}");
```

## Azure Policy JSON Evaluation

Compile and evaluate Azure Policy JSON `policyRule` definitions directly — no Rego translation required.
The `AzurePolicyCompiler` compiles JSON policy rules into RVM programs that can be executed with the `Rvm` engine.

```csharp
using Regorus;

// 1. Load alias definitions for the resource provider
const string AliasesJson = """
[{
    "namespace": "Microsoft.Storage",
    "resourceTypes": [{
        "resourceType": "storageAccounts",
        "aliases": [{
            "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
            "defaultPath": "properties.supportsHttpsTrafficOnly",
            "paths": []
        }]
    }]
}]
""";

using var registry = AliasRegistry.FromJson(AliasesJson);

// 2. Compile a JSON policy rule (the native Azure Policy language)
const string PolicyRule = """
{
    "if": {
        "allOf": [
            { "field": "type", "equals": "Microsoft.Storage/storageAccounts" },
            { "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", "equals": false }
        ]
    },
    "then": { "effect": "deny" }
}
""";

using var program = AzurePolicyCompiler.CompilePolicyRule(registry, PolicyRule);

// 3. Normalize an ARM resource and evaluate
var armResource = """
{
    "type": "Microsoft.Storage/storageAccounts",
    "name": "mystorage",
    "properties": { "supportsHttpsTrafficOnly": false }
}
""";
var envelope = registry.NormalizeAndWrap(armResource);

using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetInputJson(envelope!);

var result = vm.ExecuteEntryPoint("main");
// result: {"effect": "deny"} for non-compliant, "<undefined>" for compliant
Console.WriteLine($"Policy result: {result}");
```

**Context-dependent policies:** If your policy uses context functions like
`subscription()`, `resourceGroup()`, or `requestContext()`, you must also set
the VM context separately:

```csharp
// The context JSON from NormalizeAndWrap is in the input envelope,
// but must also be provided to the VM's ambient context:
vm.SetContextJson(contextJson);
```

You can also compile full policy definitions (with parameters) using
`AzurePolicyCompiler.CompilePolicyDefinition()`. See
`bindings/csharp/Regorus.Tests/AzurePolicyCompilerTests.cs` for comprehensive examples.