You let a coding agent loose on a repository. It reads what it needs, then writes β a
file two directories up, a ~/.config it had no business in, a /tmp script it runs
next. When you look at how, the answer is dull: the agent's write tool called
File.WriteAllText on whatever path the model produced. Nothing stopped it because
nothing was there to stop it. The model was asked, in the system prompt, to stay in the
repository. Asking is not a boundary.
This is the problem I think is worth a .NET framework's first page, ahead of everything
else a framework does. Not because the rest is unimportant β 6 orchestration modes, 16
LLM providers and a RAG pipeline that publishes its own benchmark are further down this
article β but because the rest is now table stakes, and this is not. The question to ask
of any agent stack in 2026 is not what can the agent do but what can it not do, and who
decided. Orkeon is a .NET 10 framework, MIT, whose answer to that question is enforced by
a mount table and a compiler rather than by a prompt. Here is what that looks like, in the
order you would meet it.
A boundary in front of the model, not behind it
An agent in Orkeon never sees a disk path. It sees virtual paths β /workspace,
/output β and each one is a mount you declared, with the rights you gave it. The
grammar is Docker-style: <physical>:<virtual>:<rights>, rights one of ro, rw, rwnd
(read, write, create β never delete), anything else a FormatException at boot. A path
that matches no mount is refused before any byte reaches the disk, by the file-system
service the tool is forced to go through, and the refusal is written for its reader,
which is a model:
Tool result << file_write [FAIL]: Error: No mount found for virtual path '/etc/hello.md'.
Available mounts: /output (writable), /crew (read-only)
It lists the mounts the agent is allowed to know about, annotated with rights, so a
tool-calling loop can correct itself on the next turn instead of retrying the same string.
A write to a read-only mount fails with a different sentence, ending on Mounts granting Write: /output. β and physical paths are redacted from every message, internal mounts
included, so hiding a folder does not leak it back through an error.
You can watch this happen in two minutes with no API key. From a clone of the repository,
with Ollama and the .NET 10 SDK installed:
ollama pull qwen2.5:1.5b
dotnet tool install -g Orkeon.Scripting.Cli --prerelease
mkdir -p out && orkeon run examples/quickstart/crew.yaml --mount ./out:/output:rw
One agent, one tool, one task: write three lines to /output/hello.md. ./out/hello.md
appears. That is the whole demo, except for what it demonstrates β the agent never saw
./out; it saw /output, the only mount you passed, mounted rw (the runner adds
/crew, read-only: the crew's own definition). Now open examples/quickstart/crew.yaml,
replace every /output/hello.md (three of them) with /etc/hello.md, and run it again. You get the refusal above, measured with that very crew. The block is
executed literally by CI on every change to the README or the source (quickstart.yml
extracts it from the README and runs it against a pinned Ollama on a GitHub runner), so if
it stops working the build goes red before you find out. The tool itself comes from nuget.org β no extra
feed, no token.
qwen2.5:1.5b is 986 MB and calls a tool the way a tool is called, five runs out of five
under Ollama 0.34.0. The README first shipped with llama3.2:1b, which wrote the tool-call
envelope as its answer, with a broken JSON string, every time under that Ollama version;
two runtime guards and a model change came out of that first CI run. The boundary is the
point, not the prose.
Why the framework cannot cheat either
A mount table is worth nothing if one tool, one loader or one cache calls
File.ReadAllText on the model's string instead. So the rule is not a code-review
convention. It is a Roslyn analyzer, Orkeon.Compliance.Vfs, attached to every project
under src/ by one ItemGroup in Directory.Build.props, with seven diagnostics β
ORKVFS001 to ORKVFS007 β every one of them an error:
var text = File.ReadAllText(path);
// error ORKVFS001: Direct call to 'System.IO.File.ReadAllText' β
// route filesystem access through IFileSystemService
System.IO.File.*, System.IO.Directory.*, new FileStream/FileInfo/DirectoryInfo on
a string, Path.GetFullPath, new FileSystemWatcher, new StreamReader/StreamWriter on
a path β and a seventh rule about a shape: a nullable IFileSystemService? field or
parameter, the one that reopens the door to if (_fs is null) { β¦System.IOβ¦ }. The
consequence for an agent is that the 79 built-in tools it may call, and the loaders and
caches behind them, reach your disk only through the mount table you declared β a property
of the compiler, not of a review β with one exception the next section owns, the process
shell_command spawns. Outside the VFS implementation itself and four bootstrap files
exempted by path, every place the rule was stepped around carries a
[SuppressVfsCompliance("reason")] with a mandatory reason string, and a grep counts
them: 50 in the whole source tree, none in the tool projects, none in the RAG projects. Part 3, The System.IO you cannot
call, counts every one and says where each sits.
The part I would want as a reader: you can have the same rule in your own project,
without the rest of Orkeon. The analyzer is a standalone NuGet package with no Orkeon
dependency:
<ItemGroup>
<PackageReference Include="Orkeon.Compliance.Vfs" Version="[ANALYZER-VERSION]" PrivateAssets="all" />
</ItemGroup>
dotnet build, and every direct System.IO call in that project is an ORKVFS error
(the package is netstandard2.0, compiled against Roslyn 4.8.0, so it loads on any SDK from
.NET 8 on β the ".NET 10 only" below does not apply to it) β
the line an agent, or a colleague in a hurry, would have slipped in does not build. Opt a
symbol out with an attribute you declare yourself (the analyzer matches it by name, so
there is nothing to reference), or lower a rule to warning in .editorconfig like any
Roslyn diagnostic. One honest note, because it is the kind of thing this article exists to
say out loud: the package has shipped twice before this version β 1.0.0-rc.3 to GitHub
Packages only, 1.0.0-rc.4 to nuget.org β and both were compiled against a Roslyn newer
than a stock SDK's compiler, which skips such an analyzer with a warning and then
silence; a fresh project with File.ReadAllText built clean. The version above is the
first one that loads. If you install an earlier one, you get nothing, and nothing tells
you.
The rest of the boundary
The filesystem is the boundary you meet first. Three more sit beside it, and the honest
framing is what each one bounds and what it leaves open.
A sandbox for code. code_interpreter is never exposed as a tool an agent can call
unless you expose it yourself. shell_command is different: it ships registered, with a
read-only allowlist (ls, cat, grep, read-only gitβ¦), and read-only includes cat
β the mount table rewrites arguments that start with a mount and leaves /etc/passwd
verbatim for the kernel to answer. The mitigation is the repository's own: compose your
host without AddOrkeonCodeTools(), or hand the tool an allowlist of your own; orkeon forge already removes both tools from the catalogue when it trials a generated crew.
Part 3 has the whole of it.
A budget for autonomy. The autonomous orchestration mode lets an agent delegate,
spawn sub-agents and call tools until it decides it is done β a polite way of saying it
can burn your budget while you are at lunch. So AgentExecutionBudget bounds five
dimensions at once, and exhausting any one throws β the agent stops there instead of
quietly continuing:
#pragma warning disable ORKEXP002 // autonomous orchestration is [Experimental]
var budget = new AgentExecutionBudget
{
MaxToolCalls = 15,
MaxDelegationDepth = 2, // AβBβC is depth 2
MaxWallTime = TimeSpan.FromMinutes(5),
MaxTokensConsumed = 16_000,
MaxSpawnedAgents = 3,
};
#pragma warning restore ORKEXP002
The pragma is not decoration. AgentExecutionBudget carries [Experimental("ORKEXP002")],
so without it this block is a compiler error β ExperimentalAttribute, the one door the
stability commitment leaves open, refusing the very code this article shows you.
Everything else sits in 35 PublicAPI.Shipped.txt baselines, 29,878 entries, where an
undeclared change breaks the framework's own build. The opt-in is your acknowledgement
that these five dimensions may move in any release, minor versions included. And two of
the five do not yet count what their names say on the orchestrator's path β and exhausting
one there ends the run as a success whose metadata is the only trace; part 5, Six ways
to run a crew, is honest about both.
Circuit breakers for loops. The graph orchestration mode allows cycles on purpose β
retrieve, judge, retrieve again β and bounds them with a retry count and a breaker
(transitions, visits per node, total duration). A tripped breaker returns what was
produced and what was paid for, rather than a stack trace.
Trust you can check without trusting me
A framework that puts a boundary in front of the model should not ask you to take its
own bytes on faith. Every Orkeon artefact β the nine packages on nuget.org, the CLI
archives, the .deb, the MSIs β is built by a public GitHub Actions workflow and
attested: GitHub signs a statement that this exact file was produced by this workflow,
at this commit, in this repository. Publishing to nuget.org goes through Trusted
Publishing, so there is no long-lived API key anywhere to leak or rotate. A CycloneDX SBOM
ships beside each release from rc.4 on, attested with the archives it describes. You verify all of it with
gh attestation verify and sha256sum, and the one surprise β nuget.org re-signs every
package it accepts, so the bytes you download are not the bytes that were attested, and a
standard-library script in the repository recovers them, in three commands β is
documented with the digests it produced. Part 2,
Publish a package whose origin can be verified, is that chain, command by command. It
proves where the bytes came from. It proves nothing about whether they are any good; that
is what the rest of the series is for.
Forge a team from a need
You do not have to write the crew. Describe the need, and orkeon forge interviews you,
drafts the team, renders it (YAML or a TypeScript .ork.ts), validates it, runs it in a
sandbox on a sample you gave it, judges the result against acceptance criteria in your
words, and asks for your verdict β then promotes the result into an ordinary folder you
can commit:
orkeon init # once: pick a provider and a model (Ollama included)
orkeon forge "a team that triages the issues of a GitHub repository every morning"
orkeon forge list # every session on disk, resumable
orkeon forge promote <slug> --to ./crews # adopt the crew that passed
The cycle is brief β blueprint β render β validate β test β diagnose β verdict, with
sessions you can resume, edit and re-test. The sandbox is in-process β workspace mounted
read-only, writes confined to the session directory, the two execution tools removed from
the catalogue β and the judge never invents a tick: when no model can grade, the verdict
says judge: deterministic. It needs a configured model; without one it stops at the door
with FORGE-LLM-UNAVAILABLE and points you to orkeon init. Part 4, From a sentence to
a crew, walks the whole cycle and its limits, then the three surfaces it renders β YAML,
.ork.ts, C# β for when you would rather write the crew yourself.
What is in the box, and what is not
At 1.0.0-rc.4: 16 LLM providers behind one capability model β fourteen vendors, from
OpenAI and Anthropic to Ollama, Mistral, DeepSeek, Qwen and Gemini, plus the two
aggregators OpenRouter and Mammouth AI β 79 built-in tool classes and an MCP client and
server for the rest, 6 memory stores, 6 orchestration modes, a staged RAG pipeline with a
corrective loop, 105 runnable examples with a CI-checked inventory, and observability that
speaks the OpenTelemetry GenAI conventions, so a run shows up in an Aspire dashboard as
invoke_agent / chat / execute_tool spans. If you are already on Microsoft Agent
Framework, you do not have to leave it to try this: Orkeon.Interop.AgentFramework wraps
a crew as an AIAgent and takes an AIAgent as an Orkeon agent's brain or as one of its
tools, and the shipped example runs both directions on a local model.
Now the other list, because you would find it anyway:
- .NET 10 only. There is no
net8.0build, and none is promised. A .NET 11 preview
job runs in CI, non-blocking. - No production references yet. The architecture is production-shaped β checkpoint
and resume, structured logs, a frozen API β but nobody has run this at scale in front of
paying users. Pilot first. - The scope is frozen. 16 providers, 79 tools, 6 stores, until real users ask
for more:CONTRIBUTING.mdsays so, in a section titled exactly that, and turns down the
17th provider before the issue is opened. An OpenAI-compatible endpoint needs no
provider at all β pointBaseUrlat it. - The desktop app is Windows-only. Orkeon Studio is WPF. The cross-platform
observability surface is the Aspire dashboard, and that was a decision, not an accident. - A single maintainer. That is the honest headcount, and the reason for the freeze.
MIT means the code outlives that fact;SUPPORT.mdhas a section titled If the project
stops that names no one and needs no one.
Try it
# The boundary, in two minutes β the block above
ollama pull qwen2.5:1.5b
dotnet tool install -g Orkeon.Scripting.Cli --prerelease
mkdir -p out && orkeon run examples/quickstart/crew.yaml --mount ./out:/output:rw
# The analyzer, in your own project β no Orkeon dependency
dotnet add package Orkeon.Compliance.Vfs --version [ANALYZER-VERSION]
# The framework, in an app
dotnet add package Orkeon --prerelease
dotnet add package Orkeon.Tools --prerelease
The repository is at github.com/Orkeon/orkeon β the docs, the examples, the API
reference, and the issue tracker, which is where to bring a crew that behaved strangely,
a boundary described more confidently than the code supports, or a design decision you
would have made differently. An actual escape β a path the mount table let through, an
allowlist bypass β goes through the private channel SECURITY.md describes, not a public
issue. That feedback is what turns rc.4 into 1.0.0.
Have a goat day π
Join the conversation.