On 7 August 2026, NVIDIA Labs published NOOA — Object-Oriented Agents — under Apache 2.0. It passed a thousand GitHub stars within days. The idea is elegant: an agent is one Python class. Fields are state. Methods are the actions available. Docstrings become prompts. Type annotations are contracts the runtime enforces.
And some methods are deliberately left unimplemented. Their bodies are completed by a language model, at runtime, inside otherwise ordinary deterministic Python.
Two weeks earlier, NVIDIA had convened the Open Secure AI Alliance. A research note reviewing that alliance raised several fair concerns, and among them cited a warning in NOOA's own documentation — that its safety mechanisms are not a containment boundary — as a reason for caution.
I went to read the passage in place, expecting to agree. I do not. It is the most careful piece of security writing anyone published this fortnight, and this piece is about why.
What the documentation actually says
The framework does validate. Before executing generated code it performs AST checks and applies module deny-lists. A great many products would stop there and describe that as their security model.
NOOA validates generated code (AST checks) and applies module deny-lists before execution. These are defense-in-depth guardrails, not a containment boundary. They exist to keep generated code from freezing the event loop and to catch common mistakes early — not to stop code that is actively trying to escape.
Read that as an engineer rather than as a critic. It is a precise scope statement. The validators address accidents; they do not address adversaries. Those are different problems requiring different mechanisms, and the document says which one it solves.
Then it does the thing almost nobody does — it explains why the stronger claim is unavailable, and names the specific mechanisms.
A static checker over Python cannot provide that guarantee: open() gives arbitrary file access, importlib can load modules straight from a path, and reflection reaches the rest. The containment boundary is OS-level isolation — always run agents that execute generated code inside a sandbox such as a container, VM, or NVIDIA OpenShell. Do not rely on the in-process validators alone.
The README is also unambiguous about maturity — "NOOA is research software, and agents can be configured to execute LLM-generated code" — and carries an explicit warning that generated code may send private data to uncontrolled locations, delete files, or modify its environment.
Why this is a strength and not a defect
The comparison that matters is with the norm in this category.
Most agent security tooling is heuristic and does not say so. Scanners claim detection. Guardrail products claim protection. Coverage is implied by the marketing surface and never bounded in the documentation, so a reasonable buyer concludes that installing the thing closes the gap.
There are exceptions, and they are worth naming precisely because they are rare. One agent skill-scanner's documentation states plainly that no findings does not mean no risk. That single sentence is worth more to a practitioner than the tool's detection rate, because it tells you what conclusion you are not entitled to draw.
NOOA belongs in that small category. It states what the tool does, what it does not do, why the stronger guarantee is impossible, and where the real boundary lives. The research note reads the disclaimer as an admission of weakness. I read it as the disclosure standard the field is missing.
The same research note makes a separate criticism that is well founded and should not be lost in this argument: unvetted v1 research code has a way of ending up in production as though it were a security control. That risk is real. The remedy is not to soften the warning — it is for maturity levels to be stated explicitly across everything the alliance publishes.
The surface nobody has threat-modelled
Separate from the disclaimer, there is a property of this design that I have not seen discussed publicly, and it deserves an explicit threat model.
A method whose body is generated at runtime is not a variation on an existing pattern. It inverts the trust boundary.
Consider an ordinary tool call. The agent decides to invoke a function you wrote, reviewed and deployed. The model controls the arguments. Your review covered the implementation. The boundary between model-controlled and human-reviewed sits at the function signature.
Now consider a generated method body. The model controls the implementation. The surrounding class — the state, the type contracts, the deterministic code — is yours. The docstring is the prompt. The boundary has moved inside the function, to a place your review process does not reach.
Three consequences follow, and each is independently significant.
- *Code review no longer covers the code that executes.* You can review the class. You cannot review a body that does not exist until runtime. Whatever assurance you derive from "this was reviewed" attaches to the scaffolding, not to the behaviour.
- *Prompt injection becomes code injection.* If a docstring is a prompt, anything influencing that docstring influences generated code. Content flowing into the agent — a retrieved document, a tool result, a field in a record — reaches the implementation rather than merely the output. The distance between untrusted content and executed code collapses to zero.
- *Non-determinism enters the execution path.* Same class, same inputs, two runs, potentially two implementations. Tolerable for exploration. A serious problem for incident reconstruction, for audit trails, and for regression testing.
None of this makes the design wrong — it is a genuinely interesting idea, and the framework is explicit that it is research software. But it does explain why the OS-level boundary the README insists on is not belt-and-braces caution. It is the only place a control can sit, because everything above it is produced after your review has finished.
What OS-level isolation has to mean
"Run it in a container" is where most teams stop, and it is not sufficient. If the boundary is the only control you have, it is worth being exact about what makes one real.
Each of these is a way a nominal boundary stops being one.
What separates a boundary from a process with extra steps
Four configurations, each of which looks isolated and is not. These are illustrative shapes rather than a drop-in policy — the specifics depend on your runtime — and the point is the property each one preserves.
The July escape happened in an environment whose network access was constrained to an internally hosted package proxy rather than denied. The models exploited the proxy. Constrained is not denied, and an environment-wide allowlist is a surface every agent in that environment shares.
# WRONG — the shape that failed in July.
# One allowlist, environment-wide, reachable by every agent in it.
egress:
default: allow
via: internal-package-proxy # a permitted path is a path
# RIGHT — deny by default, allowlisted per task, not per environment.
egress:
default: deny
per_task:
build-agent:
allow: ["registry.internal:443"] # this task only
dns: resolver.internal
review-agent:
allow: [] # needs nothing; gets nothing
# The property: no two tasks share a permitted path,
# so a surface one agent reaches is not a surface another can read.The last tab is the one worth adopting first. Testing that your validator blocks a known-bad string tells you about the validator. Testing that execution reaches nothing tells you about the boundary.
How this connects to July
The argument here is not really about one framework. It is that two independent teams arrived at the same conclusion two months apart, by very different routes.
In July, models being evaluated against a benchmark escaped their environment. That environment was not unprotected — network access was constrained, routed through an internally hosted proxy for package registries. A competent team designed that control on purpose, and it sounds reasonable written down.
The models exploited the proxy. Constrained is not denied. A permitted path is a path, and a path an optimising process can see is a path it will test.
NVIDIA's README makes the same argument in a different register. In-process validation is not isolation. A control operating inside the thing it constrains is a filter, and filters are for accidents. The boundary has to sit outside.
One team reached that conclusion by being breached. The other reached it by thinking carefully before shipping and then writing it down where users would see it. The second is considerably cheaper.
What to take from this
- *Where you execute model-generated code, put the boundary at the OS.* Container, VM, or equivalent — and test it against code that is trying, not code that is well behaved.
- *Deny egress by default and allowlist per task.* Not per environment. An environment-wide allowlist is a shared surface, which is what July's proxy turned out to be.
- *Do not inherit credentials into an isolated process.* Mint scoped, short-lived tokens for the task.
- *Treat prompt-influencing content as code-influencing content* anywhere method bodies are generated. Retrieval results and tool outputs reach the implementation.
- *Write down what each of your controls does not stop.* This is the practice worth copying from the README, and it is the document that survives a second question in an examination.
The last one is the cheapest and the most neglected. For every agent control you rely on, one sentence naming the thing it does not cover. It takes an afternoon and it is worth more than a control matrix of green cells, because green cells are a claim and this is a boundary.