Guardrails — scoping what an agent may do
A model that only writes text needs its output checked. A model that runs shell commands, edits files and calls APIs needs its actions scoped, before they happen, by something that is not the model.
In contenox that scope is six declarations. None of them is a setting you tick; each is a file you write, diff and review like any other change.
| What it decides | Where you declare it |
|---|---|
| Which model answers | model: in the agent declaration; execute_config.model / provider in an authored chain |
| Which tools exist at all | tools: in the declaration; execute_config.tools allowlist in an authored chain |
| Where it may act | workspace roots, and the sandbox |
| What runs, asks, or is refused | the envelope — HITL policy |
| What content gets through | a route task — moderation gate |
| What it may spend | compute bounds in the envelope |
1. Which model answers
A declaration’s model: pins the model for that agent; routing stays on your
configured default when the field is absent. In an authored chain a task names
its model and provider per step. Nothing negotiates that at runtime, and an
envelope can pin it further: modelAllowlist and backendAllowlist in the
compute block mean a unit cannot switch to a model you did not name — see
sovereignty for why that matters when the inference
has to stay on your hardware.
2. Which tools exist at all
A declaration’s tools: line is the allowlist most agents use. Omitting it
inherits every tool, so name them to narrow it.
Behind that, and in a chain you write yourself, the field is
execute_config.tools — an allowlist whose default is the important part:
Absent or
null= none. The task has no tools until this field explicitly grants some.
[] is no tools, ["*"] is all, ["*","!local_shell"] is all-except. There is
also hide_tools to suppress specific tools from both the registry and any
client-passed set, and tools_policies to constrain a provider before it runs —
local_shell: { "_allowed_commands": "git,go,ls" }.
A tool the task was never granted is not a tool the model can be argued into calling.
3. Where it may act
Sessions run only inside the workspace roots you configured — the launch
directory, roots granted with contenox workspace add, and any passed for that
run. Never the runtime’s own config, database or policies.
Every agent-reachable shell gets a scrubbed environment, so credentials in your shell are not credentials in the agent’s. The reasoning is in the threat model: the process on the other end of an external agent connection is not one you can trust.
4. What runs, asks, or is refused
The envelope is evaluated before every tool call. Three verdicts:
allow— runs silentlyapprove— pauses and waits for a persondeny— refused
A call no rule matches takes default_action. Set that to approve and
anything you did not think of asks instead of proceeding — which is the point of
declaring rules rather than enumerating threats.
Rules match on the tool and its arguments, with conditions like
command_prefix_allowlist, so git and go test can pass while everything
else on the same shell stops. The full grammar is in
HITL policies, and the format has a
published JSON Schema you can point your
editor at.
It fails closed in three directions. An unknown default_action, or a typo
inside compute or trusted_binaries, refuses to load the policy rather than
silently disarming it. A policy file that cannot be read falls back to asking
about everything, including reads. A broken envelope stops work; it never
quietly widens it.
5. What content gets through
Content moderation is not a subsystem here, it is a task. A route task
classifies with a cheap model and the transition sends the request to the real
chain or to a rejection:
{ "id": "moderate", "handler": "route",
"transition": { "branches": [
{ "operator": "equals", "when": "safe", "goto": "simple-chat" },
{ "operator": "equals", "when": "unsafe", "goto": "reject_request" },
{ "operator": "default", "goto": "simple-chat" } ] } }
That is the moderation gate, shipped as
examples/simple-chat-with-moderation.json. Put the same task after the
generating task instead of before it and you have output moderation — the
mechanism does not change, only its position. You pick the classifier model, and
you can read what it decides on.
6. What it may spend
compute caps turns, tool calls and tokens. A unit that crosses a bound ends as
stuck rather than running on. attention decides who may answer a unit’s
question — by default a human, never the agent itself.
None of this asks the model to behave
Every declaration above is enforced outside the model, before the effect lands. A tool that was never granted cannot be argued into existence; a path outside the workspace roots is not reachable by a better prompt; a call the envelope denies does not run. That is what makes them guardrails rather than instructions.
Next
- HITL policies — the envelope format in full.
- The agent sandbox — the filesystem and exec fence.
- The threat model — why the fence exists.
- Nested permission bomb — why inheriting your own permissions is the bug.