Integration fixes the merge required:
- guard tools after the strict-schema downgrade, so the copy
dataclasses.replace returns is the object the safety wrapper mutates
- await _ctx_client, which main made async for the Caido bootstrap handle
- pass main's extra_files through with the isolated local sources
- keep DEFAULT_SAFETY_MODE alongside main's new report/state imports
- rebuild the committed viewer bundle from the merged frontend sources
- pin the browser-session safety phrase in test_safety_prompt so it no
longer matches unrelated prompt text, and stamp safety_mode on the
workspace-file resume record
* add extra-files plumbing so orchestrators can drop single files into the sandbox workspace
* reject extra-file paths that collide with a local source tree
* add --workspace-file so CLI users can place files in the sandbox workspace
* reject repeated and control-character workspace paths
* revalidate persisted workspace files when resuming a run
* drop the workspace-file size limit
Guarded review was blocking or prompting on any non-empty hard gap even when the
reviewer had already determined the missing evidence was irrelevant — a file the
command only writes (an output that does not exist yet), a benign parser
misclassification, or a data file that is only read. The reviewer's own reason
would say the action is safe, then defer anyway.
- Reviewer prompt: a hard gap is missing evidence, not proof of danger. After
inspecting, the model judges whether the gap could change the action's effect —
allow when it cannot, block when it could hide a dangerous effect it cannot rule
out, and defer only for genuine ambiguity. It no longer blocks or defers merely
because a gap remains.
- Remove the code override that forced an incomplete-evidence "allow" into a defer
(interactive) or block. The reviewer's verdict now stands, gated by the existing
confidence threshold: an unsure allow still defers to the human.
Headless runs are unchanged — incomplete evidence still fails closed before the
reviewer, preserving the autonomous guarantee. Forced inspection and the
inspection-failure guard remain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
repeat_request replays a captured HTTP request with optional modifications.
Its effective bytes are fully determined before dispatch — the captured request
is immutable and the modification overlay is deterministic — so it no longer
needs a blanket deterministic block.
- Extract resolve_effective_request in the proxy tool so the tool and the safety
layer build the {method, url, headers, body} from the same function; the
reviewed request is byte-for-byte the one that is sent.
- compile_network_evidence freezes that request as an evidence packet; the
runtime routes repeat_request through the reviewer (and human approval when
guarded+interactive), sending only if allowed and failing closed when the
request cannot be resolved.
- Approval prompts now carry the real tool name (via _ExecReview.tool_name), so
a deferred repeat_request no longer shows as exec_command.
- Reviewer prompt notes the replayed-request shape.
Tests cover allow/block/unresolvable/deferred paths and the packet shape.
Full Python suite, ruff, and mypy strix/ pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve conflicts from main's mount-prompt refactor against the safety
approval UI:
- update.go / vulnerabilities.go: keep the variadic cornerPrompt + cornerButton
needed for the three-button safety prompt while adopting main's mount changes
(Mount/Skip labels, mountPromptBounds + labelHitAt, direct answerMountConfirmation).
- Reconcile main's new runner-lifecycle tests (interrupt, teardown) with the
now-default guarded mode by running them with safety off, matching their intent.
Full Python (1168) and Go suites, ruff, and mypy strix/ pass on the merge.
Engine + integration:
- Reviewer inspection now surfaces the real frozen source of an already-frozen
workspace script/dependency instead of an empty string, so workspace-resident
scripts resolve without a needless human defer.
- Guard effectful static tools via an explicit, documented set plus the SDK's
per-tool needs_approval signal; give the exec/stdin wrappers the same
idempotency guard as their sibling wrappers.
- Centralize DEFAULT_SAFETY_MODE and share one resume safety-mode rule between the
CLI and runner so the two cannot drift; type InspectionContext.runner, reuse
RUNTIME_STATE_DIR_NAME, and drop a dead workdir parameter and a write-only field.
TUI approval experience:
- Approve All drops the run into dangerous mode: it approves the pending call and
turns review off for the rest of the run, with a standing "review off" status flag.
- The status row shows the owning agent as paused while it waits on a decision.
- Redesigned prompt: a risk + tool header, a collapsible command/reason preview
that expands (e) and scrolls, and no internal digest, agent, or request ids.
Full Python (1138) and Go suites, ruff, and mypy strix/ pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A scan runs dozens of agents concurrently, each holding a sandbox session, a
browser session, a model client, and a SQLite handle. At the common 1024 soft
file-descriptor limit that budget is exhausted around ~32 agents, after which
SQLite can no longer open agents.db and agents fail en masse with
"unable to open database file" (an fd-exhaustion symptom, not DB corruption).
Measured directly: fd usage scales ~linearly with concurrent agents and hits
1024 at 32 agents; fds are reclaimed as agents finish, so this is a ceiling
problem, not a leak. Strix never set its own limit, inheriting whatever the
launching shell had (often 1024).
Raise RLIMIT_NOFILE toward the hard cap at scan start (best-effort, idempotent,
POSIX-guarded; warns when the hard cap is itself too low to lift without a
privileged operator). Runs no longer depend on the operator setting ulimit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* let an agent wait on what it already said
An agent that answers in plain text is nudged to call a tool, and the only tool
that hands control back takes a required message. So it says the same thing
twice: once as text the user has already read, once as the argument it had to
supply to stop. Seen on a run whose whole instruction was "hi" - a greeting, then
the same greeting again through respond_to_user.
message is optional now. The nudge arms the tool with the text that was
delivered and says not to repeat it, so an agent that has said its piece can park
on it with an empty call. Anything it does want to add it passes normally.
Parking still cannot leave the user on silence: an empty call is refused unless
something was actually said, and the arming is single use - execution clears it
as soon as a turn ends any other way.
The interactive prompt now also says to answer and stop in one respond_to_user
call, which is what avoids the nudge in the first place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* drop the worked example from the interactive prompt
"the user greeted you, asked something you can answer outright, or you need a
decision" was the run I had been reading, written into a rule that holds
whatever the reason. The rule is that replying and stopping is one call; listing
occasions only invites the model to check whether this is one of them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* drop the arming flag; an empty message just waits
Passing the delivered text from execution into the tool, and refusing an empty
call without it, was machinery guarding against an agent parking having said
nothing. That leaves the user looking at "waiting for your reply" with a cursor
in front of them - they type. It does not need a mechanism.
What is left is the default on message, and the nudge saying the text already
landed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* only offer waiting on words that were written
The nudge told every agent its text had already been delivered, but it fires
whenever a turn leaves the agent running, and a turn can end with no tool call
and no text at all - _final_output_preview has carried <none> and <empty>
branches all along. An agent that said nothing was being invited to wait on an
answer the user never received, leaving them at a bare prompt.
It now reads the turn: waiting on what was said is offered only when something
was, and otherwise the agent is told plainly that the user has read nothing and
to send its message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* leave the continuation nudge alone
Rewording it meant asserting from the outside whether the agent had spoken, and
the nudge fires whenever a turn leaves the agent running - text or no text. The
agent knows which it did without being told, so the guidance belongs in its
prompt, where the condition is its own to read.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* say it in the nudge, where the agent is reading
An agent stranded by the nudge reasons off the nudge. Told only to call
respond_to_user, it supplies a message, and since it has just answered in plain
text that message is the same answer again. The system prompt saying otherwise
sits thousands of tokens earlier and loses.
The clause goes on the line the agent acts on: call respond_to_user, with no
message if it has already said it. That reads true whatever the turn did,
including one that produced no text, because the agent is the one who knows
which — nothing here has to work it out from the outside.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* correct the nudge the internal-turn test asserts
The test expected "ended the autonomous Strix run", which strix.core.execution
does not inject; it says "ended the autonomous run". The classifier was right and
the test was not, so the suite failed on main while the behaviour it guards was
fine.
The sentence is written inline in another module and copied by hand into the
classifier and again into the test, which is how it drifted. A second test now
reads it back out of that module's source, joining the adjacent string literals
its line wrapping leaves behind, and fails if either nudge is no longer injected
verbatim. Reworded one and it reports which nudge went missing and what a resumed
scan would do about it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* read the nudges out of what the module can inject, not out of its text
Searching the source accepted the sentence anywhere in the file, so a stale copy
left behind in a comment would have kept the guard passing after the message it
guards had changed - the drift it exists to catch.
Parsing the module instead limits it to strings the code can actually inject.
Comments never reach the tree, docstrings are dropped as description rather than
behaviour, and adjacent literals are joined during parsing, which the line
wrapping needed and the regex was only approximating.
Checked by rewording the message and leaving the old wording in a comment: the
guard fails, where searching the text passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* make the working-directory prompt answer the mouse
Its Confirm and Cancel were drawn as buttons and did nothing when clicked: the
modal mouse handler had a case for every dialog except this one, so a click fell
through and the scan sat waiting on an answer the user believed they had given.
Only the keyboard could answer it.
The prompt is docked in a corner rather than centered, so it also needs its own
bounds; the centered ones every other dialog uses would have put the buttons in
the wrong place. Those bounds now come from the same placement cornerOverlay
draws with.
Two returns that hand back the model alongside a call that mutates it are now
sequenced explicitly. They work, but only because the compiler happens to
evaluate the call first, and one of them is what puts the prompt back in the
composer when the mount is declined.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* skip the mount instead of abandoning the scan
Declining the working-directory prompt threw the whole launch away and dropped
back to the start screen, which is a lot to lose for answering one question
about one directory. The two answers are now about the directory alone: mount it,
or run without it. The prompt is the whole of the input either way.
The buttons say which is which - Mount and Skip rather than Confirm and Cancel -
and the prompt says what skipping costs.
A run with neither target nor directory is a real run, so two things follow it.
It can be resumed: its instruction is what drives it, and that is in the run
record. And it tells the agent plainly that it has neither, because an agent
given no scope goes looking for the one it assumes it was meant to have.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Trace review showed the split rule firing on commands with no execution at all:
curl downloading a `.js` asset (`curl …/app.js > app.js`), glob and grep
patterns containing a script suffix, and running an inspectable workspace script
with an output redirect (`python3 probe.py > out.jsonl`). It blocked on any
interpreter segment or any token ending in a script suffix.
Block only the shapes whose executed code no artifact can describe: an
interpreter that reads from a pipe, stdin, or heredoc, and create-then-run where
a script-suffixed file is written (`> x.py`, `-o x.py`) and executed in the same
expression. Running an inspectable script with its output redirected or piped,
and downloading a script-named asset, now go to review — the script itself is
still read into the packet. Destructive-in-a-chain detection is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recon tools route their target list through a flag — `ffuf -w wordlist.txt`,
`httpx -l hosts.txt`, `nuclei --list targets.txt` — not the `<` redirect the
input-file collector already handled, so the reviewer kept blocking "probes
every host listed in hosts.txt" because the list was never in the packet. Parse
the value of the common list-file flags and collect it the same bounded way,
alongside redirect inputs. The value is only read when it resolves to a
workspace file, so a boolean `-l` (grep, wc) whose next token is not a file
collects nothing and never makes the packet incomplete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trace review of two guarded-mode runs found the unresolved-execution guard
firing on ordinary commands: curl fetching a .js asset, rg over a .py file, sed
and cat and cp on script-named files — 24 blocks in one run. The guard was meant
to fail closed on an unknown interpreter handed a script, but a read, transfer,
or text tool takes such a file as data, not as a program to run.
Exclude known read commands, HTTP clients, and a set of text/data tools from the
script-suffix branch, so only a genuinely unknown executable given a script still
fails closed. awk moves from the interpreter set to the data tools: its program
is an inline positional argument, not a -c or script file the entrypoint reader
can resolve.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two guarded-mode false-positives from a recon run.
A command that reads a workspace data file — `while read host; do dig "$host";
done < hosts_passive.txt` — reached the reviewer with an empty artifact list,
because the evidence compiler only collects script entrypoints and their Python
imports, never a data file consumed via input redirection. The reviewer, asked
whether the queried hosts were in scope, had no way to see them and fail-closed
on unresolved scope. Parse single `<` input redirections (not `<<` heredocs or
`<(` process substitution) and attach each workspace-resident file as an
artifact with role "input", bounded by max_artifact_bytes and flagged when
truncated. Files outside /workspace are not read.
Separately, the reviewer treated scope as the exact authorized host, so it
blocked resolving admin.fiuu.com under an authorized fiuu.com. State in the
prompt that an authorized domain covers its subdomains, and point the reviewer
at the new role "input" artifacts for scope checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two guarded-mode false-positives surfaced in real scan traces.
The reviewer blocked a boolean SQL injection probe
(`curl "…/login?username='+OR+'1'='1"`) for being an injection attempt at all,
though it is a read-only GET that changes nothing. The prompt said "allow only
non-destructive" but never established that in-scope offensive testing is the
tool's authorized purpose, so the model blocked on the technique. Rewrite the
guarded-mode guidance to judge by effect: in-scope injection probes, recon,
enumeration, and fuzzing pass, while destructive or persistent effects block —
with SQL spelled out (boolean/UNION/time-based read probes pass; DROP, DELETE,
INSERT, INTO OUTFILE, stacked statements, and command execution block).
Ambiguous evidence still fails closed, and every deterministic block, the
completeness gate, observe's passive-only rule, and scope enforcement are kept.
Separately the reviewer blocked a plain `curl` as "use of bash shell within a
curl command". The shell wrapper stamps `shell: bash` onto every exec_command
for execution, and the evidence packet passed that transport default straight
to the reviewer, which read it as the agent invoking a shell. Strip the
harness-injected transport keys (`shell`, `max_output_tokens`) from the packet's
original_arguments; the command itself is still parsed from `cmd`, so an
agent-authored `bash -c` payload is unaffected.
Note: the effect-based prompt also lets in-scope recon tools (nmap, subfinder,
ffuf, katana) through, which the old prompt blocked as "scanning" or "high
volume". That follows directly from judging by effect rather than technique.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
77c7b0d changed the message to "the autonomous run" in both execution.py and
live_view.py but left this case asserting the older wording, so the classifier
test has been failing on main since that commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Grouped browser verbs were classified by their verb alone. `tab` and `session`
sit in the passive set, so `tab new <url>` — documented as navigating — and
`session clear` earned a deterministic allow and executed unreviewed in guarded
mode and unblocked in observe, while `open <url>`, the same navigation, was
reviewed. Passivity is now decided from verb plus subcommand, and the packet
carries the result so observe mode stops maintaining a second copy of the rule
that could drift more permissive than guarded. The blocked-action list still
matches on the bare verb, so `auth login` keeps matching `auth`.
Interpreters were a seven-name allowlist, so `python3.12`, `/usr/bin/python3`,
`php`, and `deno` set no script path and produced a packet with no artifacts
that was still stamped complete — the exact shape the reviewer is told it may
allow. Recognize versioned and common interpreters so their sources are
actually collected, and fail closed when a command runs code that cannot be
resolved to an inspectable script.
`from pkg import payload` collected only the package initializer, because an
imported name was treated as an attribute and never as a submodule. Effectful
code in `pkg/payload.py` executed without appearing in the evidence.
Workspace staging runs twice per run and was not idempotent: the second pass
read the origin from `source_path`, which the first pass had already rewritten
to the copy. With the completion marker absent it cleared the destination and
then copied from that same emptied directory, silently handing the agent an
empty workspace. The origin is now read back from `original_source_path`.
Each fix is covered by a test that fails when the fix is reverted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `agent_browser` skill is always loaded, so its safety paragraph shipped to
`off`-mode agents. Its prohibitions do not hold there — Strix only assigns a
browser session in a safety mode, while multi-session browsing is a normal
documented workflow — so the paragraph misdescribed the tools those agents
have. Move it into the already mode-gated block in the system prompt, and pin
the gating in both directions.
Test changes:
- `test_observe_mode_blocks_browser_click` asserted nothing about observe mode.
The same call blocks in guarded for a different reason (no prior snapshot),
so the observe rule was never reached. Give it a snapshot and assert the
block's source and category, plus the passive-read inverse.
- Neither workspace-epoch bump was pinned; removing either left the suite
green. Both are now covered, along with the read-only case that must not
bump, and an end-to-end pairing where a patch during review invalidates a
script decision.
- Cover `invoke_mutating_tool`'s observe-block and off-mode paths, the
reviewer's low-confidence, block, missing-model and failed-inspection rules,
the inline `bash -c` source path, and the two dependency-budget guards.
- Assert browser sessions are disjoint across agents rather than freezing one
agent's command string.
- Fold the compound-separator and safety-config tests into the parametrized
cases that already covered them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Introduce a pre-execution safety layer that reviews effectful agent actions
against compiled, frozen evidence before they run.
`--safety-mode guarded` allows non-destructive interaction after review;
`--safety-mode observe` permits passive target interaction only. `off` stays
the default, so existing runs are unchanged.
Deterministic rules decide what they can on their own: destructive commands,
code-loading environment overrides, blocked browser actions, and mutating
requests in observe mode are refused without a model call, and a small set of
read-only commands is allowed outright. Everything else compiles an evidence
packet — command, scope, script source and its local import closure, prior
tool-call evidence, and browser snapshot context — for a bounded reviewer that
may make one isolated inspection call. Incomplete evidence fails closed.
In safety modes, user-owned local directories are copied into the run
directory so the originals are never mounted writable, while `.git`,
`.agents`, and `.codex` inside the copy stay read-only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(agents): collapse repeated waits queued inside one model turn
* fix(agents): state that one wait is enough in every prompt variant
---------
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
* fix(llm): cap the tool calls one assistant response may queue
* fix(llm): cap the subscription backend's responses too
---------
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>