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>
The versions in .pre-commit-config.yaml are stricter than the ones uv resolves,
so these were invisible to `make lint`:
- ruff 0.11.13 reports N802 for `ast.NodeVisitor`'s `visit_*` methods; 0.15.20
exempts them. The names are dispatch keys and cannot be lowercased, so add the
per-file ignore next to the existing stdlib-handler precedent.
- mypy 1.17.1 does not narrow `str` to `SafetyMode` through a membership test
against the tuple. Return the matched element instead.
- bandit does not read `# noqa`; pair it with `# nosec` as the other 33 sites do.
The trailing-whitespace hook rewrites the built viewer bundle, stripping a space
and a tab that are content inside a highlight.js character class. Exclude the
generated assets rather than let a hook corrupt shipped JS.
Upstream removed every "Strix" literal from model-facing text; this branch
predated that and reintroduced one in the prompt template and one as an agent
name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only the terminal renderers learned about the `blocked` status. `apply_patch`
and `repeat_request` are also refused by the safety runtime — `apply_patch`
unconditionally in observe mode, `repeat_request` unconditionally in guarded —
and both rendered as though nothing had happened. A blocked patch was
byte-identical to one that was applied.
Extract the verdict line into `safetyBlockLine` in the Go renderers and a
`SafetyBlock` component in the viewer, then call it from the patch and
repeat-request renderers as well. The viewer's terminal renderer was reading
the envelope's `error`, which is a fixed string, so it now reads
`safety.reason` like the TUI already did.
Go tests assert a blocked patch no longer matches an applied one.
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>
An exception escaping a non-interactive cycle re-raised before the status
handling, so a dying child stayed 'running' and its parent waited out the
timeout on a completion report the child could no longer send. Set the
terminal status and wake the parent on the way out too.
A tool call for a name Strix does not register raised ModelBehaviorError
from the SDK turn resolver, which nothing retries: the root agent's raise
tore down the whole scan and a sub-agent died before its status was set.
Opt into the SDK's tool_not_found_behavior="return_error_to_model" so the
unknown call comes back as a tool result and the agent self-corrects.
The setting landed in openai-agents 0.19.0, which requires openai>=2.45,
so both pins move.
* perf(cli): fast startup — lazy heavy imports + onedir standalone build
* perf(cli): drop legacy single-file compat from install/self-update
* perf(cli): simplify — drop constants module and extra lazy-import refactors
* refactor(update): strix --update just re-runs the install script
* perf(cli): drop packaging/install/update changes; deepen lazy imports instead
Reverts the onedir build, install.sh, and self-update changes so release
mechanics stay untouched. Startup cost is addressed purely by deferring
heavy imports (agents/openai, config.models, report state/writer, docker)
until a scan actually runs; DEFAULT_MAX_TURNS moves to strix.config.settings
so argparse no longer pulls the agents SDK.
---------
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
One tool was doing three jobs (wait on the user, wait on other agents, and
- wrongly - wait for a long-running command), so the driver had to guess which
one an agent meant and used parent_id as the proxy: the root waits for a human,
everyone else waits for agents. That proxy is wrong, since the user can message
any agent from the TUI's agent tree.
Tool identity now carries the intent, and the coordinator records it as a
wait_kind that survives snapshot/restore:
respond_to_user -> wait_kind="user", never auto-resumed (root or not)
wait_for_agents -> wait_kind="agents", auto-resumed on a 300s timer
recovery exhaust -> wait_kind="stalled"
respond_to_user fuses the message and the yield into one call, so there is no
way to answer and then forget to stop - the two-step that gpt-4o-mini skipped
2/2 in live testing. Plain text still renders as before.
Auto-resume is also bounded now: an agent that re-parks after every timeout
burned a model turn every 300s for the rest of the scan (and, since parked
children notify their parent, spammed the parent's inbox on the same cycle).
After _MAX_IDLE_AUTO_RESUMES it stays parked until a real message arrives.
The user can message any agent from the TUI, not only the root, so the
justification is that the parent is an agent with no other way to learn
the child parked - not that the child has no human resumer.
Parking is self-service only for the root, which the user is watching.
A parked child owes its parent a report it can no longer send, so the
parent would wait out its full timeout for nothing.
A mutual wait between two agents resolves only when both hit their cap,
so the ceiling is the worst-case idle burn. Name the constants instead of
repeating the literal, and align the interactive auto-resume timeout.