Open Source · Rust · AGPL-3.0

orno

The agent loop can't step outside the box.

A runtime contract for LLM agents in CI. You declare the ceiling in YAML — iterations, tools, effects, resources — and orno stops the agent the moment it tries to cross it.

5
Strictness axes
1
Binary, no server
AGPL
3.0 licensed
01 — The problem

An unbounded agent loop in CI is a liability.

One bad prompt and the loop burns a weekend of tokens, reaches endpoints it had no business touching, and leaves you nothing to audit afterwards. orno wraps the loop in a contract and enforces it at runtime — nothing to wire up in code, no place to quietly disable the limits.

02 — The contract

Five axes. All enforced at runtime.

Scroll — the field highlights each axis in turn.

01

Bounded iteration

Caps the agent-loop turns so it can never spin forever.

max_iterations
02

Bounded tool surface

Declares exactly which tools, MCP calls, and subagents the model may reach.

allowed_tools
03

Bounded effects

Gates mutating operations, network access, and which domains are in reach.

mutations · network
04

Bounded resources

Hard ceilings on total tokens, total tool calls, and subagent depth.

tokens · calls · depth
05

Bounded non-determinism

Every LLM call is recorded — replay is exact, byte for byte.

record · replay
03 — How enforcement works

Declare it once. orno holds the line.

step 01

Declare in YAML

Set the contract — iterations, tool surface, effects, resources — in one file.

step 02

Enforce at runtime

Every agent node checks all five axes live. No policy library to keep in sync.

step 03

Breach → stop

Cross a limit and the node terminates with the matching event on the log.

04 — Why teams reach for it

One binary, one file

No server, no database, no scheduler to babysit. Drop it on a runner and call it.

Two streams, no parser

NDJSON events on stdout, tracing on stderr. Pipe straight into jq, Splunk, or Datadog.

A pinned GitHub Action

Pin DoctorMozg/orno@v0 in the workflow and the run stays reproducible.

The whole surface is one agent block and one policy block.

The policy lives next to the agent that runs under it. One node, same file, same PR diff. There's no separate policy file to drift out of sync and no library to import. The pills below are the keys you'll actually write: policy.max_iterations, allowed_tools at the agent level, policy.allow_mutations, policy.allow_network, policy.allowed_domains, policy.max_total_tokens, policy.max_tool_calls, policy.max_subagent_depth — each one a single line, each one glossed.

Three verbs you run before merge. One of them never calls the model.

validate loads the policy surface and fails on a bad contract, with zero LLM and zero network. plan gives you a static DAG preview with the worst-case ceiling, no tokens spent. run is the one that executes: NDJSON on stdout, tracing on stderr, non-zero exit on a breach. orno also ships schema, replay, and completions, so nothing about the tool is hiding from you.

A pipeline is a DAG of nodes, not one loop.

A kind: agent node runs the strict loop with all five axes enforced live, its output flowing on as nodes.<id>.output. A kind: shell node runs a deterministic subprocess that agent policy doesn't touch, exposing nodes.<id>.stdout, .stderr, and .exit_code — plain glue between the agent steps. Bound the non-deterministic parts hard, keep the deterministic parts plain, and keep both in one file.

Record once, replay byte for byte.

A flaky agent failure turns into a deterministic local repro instead of a "works on the third rerun" mystery, and the recording doubles as your audit trail. --record-bundle captures every LLM call, every tool call, and the YAML into one bundle you replay offline with orno replay — no keys, no network. Want finer control? --record-tape and --replay-tape pin the LLM calls; --record-tool-tape and --replay-tool-tape pin the tool and MCP calls.

A counter in your code lives in one place. A contract lives at every node.

Sure, you can cap iterations with a counter. It sits in one function until someone comments it out under deadline. orno's limits are declarative config instead, enforced at every agent node on every run and reviewed in the same PR diff. No policy library to drift, no off switch buried three commits back, and effects and tool surface denied by default until you name them. orno is honest about where it earns its keep: an agent that never calls a tool is a template engine over a chat completion. The case for it starts at two-plus turns with tool calls in between.

One static binary. Drop it on the runner.

A single line installs it: curl --proto '=https' --tlsv1.2 -fsSL https://raw.githubusercontent.com/DoctorMozg/orno/master/install.sh | bash. You get one static binary for x86_64-unknown-linux-gnu, aarch64-apple-darwin, or x86_64-pc-windows-msvc, with no toolchain at run time — Rust 1.95+ is only needed to build from source. Set ORNO_VERSION to pin a release, ORNO_INSTALL_DIR to relocate it.

Auditable by design

Guardrails you can read.

Open source under AGPL-3.0. The contract is right there in the YAML — no hidden policy, no place to switch the limits off.

View on GitHub
AGPL-3.0 · Rust · orno