Rules of engagement
Allowed actions, exclusions, and what the scope guard does and does not enforce.
Rules of engagement exist in two forms.
The document is a markdown file rendered in the first phase of the run from the engagement you submitted. It is not sent to the model: the agent receives only the target list and the in/out scope lists, not this document. What becomes of the document itself — storage, the report, the manifest — is on The document below.
The scope guard is code inside the container. It inspects every command
before it is executed and every URL before it is fetched, and refuses what falls
outside the scope. It does not read the document. Every call to the three tools
that reach the network — execute_command, http_request and
browser_screenshot — goes through it, so the agent cannot skip the check. It
can still issue a command the check does not understand, which is the limit
described below.
What you set¶
An engagement's rules carry three fields: a maximum duration in minutes, a list
of allowed actions and a list of denied actions. The wizard offers four actions
— scan, enumerate, exploit, post_exploit — as toggles, and turning one
off moves it from the allowed list to the denied list. The duration field
carries min 30 and max 480. Those are attributes on the HTML input; neither
the create schema nor the agent validates the number that arrives.
All three are written into the document.
| Rule | In the document | Enforced by the guard |
|---|---|---|
| In-scope list | yes | yes, on every command and every URL |
| Out-of-scope list | yes | yes, by exact name or by address |
| Allowed actions | yes | no — the guard never reads the list |
| Denied actions | yes | only dos, and only on the command's first token |
| Maximum duration | yes | no |
The document also carries three standing prohibitions that are text and nothing
else: no denial of service, no social engineering of employees outside written
authorization, no testing against out-of-scope assets. The third is enforced by
the guard; the first is enforced only to the extent the dos tool list below
covers it; the second is not enforced at all.
What the guard enforces¶
For each command the guard extracts two things: IPv4 addresses matching a
dotted-quad pattern, and hostnames. A hostname counts only in two shapes — the
host component of a URL, wherever that URL appears, including inside a quoted
inline script; and a shell token that is entirely a hostname, optionally with a
:port suffix. Dotted tokens inside longer strings do not count, which is what
keeps index-DVwWyWM7.js and urllib.request from being read as domains. The
command's own program name is discarded before matching.
Each extracted target is then checked, out-of-scope first:
- An address on the out-of-scope list, or inside an out-of-scope network, is refused. Otherwise it must fall inside an in-scope network.
- A name equal to an out-of-scope entry is refused. Otherwise it must match an
in-scope entry by shell-style glob —
*.example.commatchessub.example.com, and a bareexample.commatches only itself.
Out-of-scope names are compared by exact equality, not by glob. A wildcard on the out-of-scope list excludes nothing.
URLs are checked the same way, by host. HTTP redirects are followed one hop at a time, up to five hops, and each hop is re-checked, so a redirect to an out-of-scope host is refused rather than followed. Past five hops the request stops and the response is annotated instead.
For a browser screenshot, navigation requests are checked at the network layer and aborted when they fall out of scope, and the final resolved URL is checked again before the picture is taken. Subresources the page loads are not checked.
A refused command is not a failed step. The refusal is handed back to the model
as the result of its tool call, prefixed BLOCKED:, and it is free to try
something else. A command_blocked event goes to the progress stream at the
same time.
A command with no recognisable target is allowed through
If the guard finds no address and no hostname in a command, it allows it as
a local command. That is what lets whoami and id run. It also means the
guard is blind to any target it cannot see in the command string: an IPv6
literal, which its address pattern does not match; a host held in a shell
variable or read from a file; a hostname assembled at runtime.
The guard is a scope check on well-formed command strings, and it is the only scope control in the run. The container does not add one: the agent container is started with no network configuration, so it runs on Docker's default network with unrestricted egress.
Denied actions¶
The guard maps actions to tools through one table, and that table has a single
entry: dos, matching hping3, slowloris, goldeneye and xerxes. The
match is on the command's first token, compared for equality. hping3 --flood
10.0.0.5 is caught. sudo hping3 …, /usr/bin/hping3 … and an hping3 after
a pipe are not: the first token is sudo, /usr/bin/hping3 and the first
command in the pipeline.
Denying exploit or post_exploit therefore puts a line in the document's
Prohibited Actions section and blocks no command. The allowed-actions list is
not consulted by the guard at all, and neither list reaches the model: both
exist only in the rendered document, which the agent never receives.
The duration is advisory¶
Duration: up to N minutes from start is written into section 3 of the
document. Nothing reads the value again. No timer stops the run at that mark.
Four hard ceilings are enforced inside the container, and none of them is a clock on the run as a whole.
Two end the run:
- 500 steps. The orchestrator's main loop exits at 500 iterations, counting every step in every phase.
- Five consecutive failing steps. A step that raises increments a counter; a step that completes resets it. At five the run aborts. A blocked command completes, so refusals do not accumulate towards this.
Two end something smaller:
- 300 seconds per command. Each command runs under a five-minute wall-clock
timeout — the only clock in the list, and it bounds one command rather than
the run. On expiry the process is killed and the step returns exit code
-1,Command timed outon stderr, and an empty stdout: whatever the command had already printed is discarded rather than truncated. The run continues. The value is the executor's default, and the agent constructs its executor without overriding it. - Ten steps of threat modeling. That phase advances once its step count reaches its cap, whether or not the agent declared it done. The cap is a field on the agent's own config model, and no interface sets it, so it is ten.
Hitting the 500-step ceiling or the consecutive-error abort ends the run where it stands. What survives, and what does not, is on When a run fails.
The document¶
The rendered document is stored on the engagement when the run returns, and reproduced whole, as preformatted text, in the Rules of Engagement section of the PDF report. It covers, in order: the engagement id and generation timestamp, the framework, the authorized targets with their kinds, both scope lists, the testing window, the authorized and prohibited actions, whether credentials were supplied, the methodology it claims to follow, and what the report will contain.
Rendering is also where scope overlap is caught: if an out-of-scope network encloses an in-scope one or the reverse, or a domain string appears verbatim on both lists, the render raises and the run aborts in its first phase. See Scope for what that costs.
The create request accepts an optional credentials list, but the
engagement does not keep it, so Section 6 always reads "No credentials
provided — black-box testing only".
A digest of the scope and the rules is written into the manifest of a sealed run, so the locker records what the run was authorized to do alongside what it did.
Last updated 2026-09-24