feat(safety): add contextual action review with guarded and observe modes

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>
This commit is contained in:
oyasumi
2026-08-08 00:47:21 +00:00
co-authored by Claude Opus 5
parent f8a8801d56
commit 6e5bb2e76e
40 changed files with 3860 additions and 35 deletions
+27
View File
@@ -33,6 +33,10 @@ _LLM_ENV_KEYS = [
# RuntimeSettings
"STRIX_IMAGE",
"STRIX_RUNTIME_BACKEND",
# SafetySettings
"STRIX_SAFETY_MODE",
"STRIX_SAFETY_MODEL",
"STRIX_SAFETY_TIMEOUT",
# TelemetrySettings
"STRIX_TELEMETRY",
]
@@ -177,6 +181,29 @@ def test_apply_override_and_load_settings_round_trip(tmp_path: Path) -> None:
assert loader.load_settings() is settings
def test_safety_settings_load_from_config(tmp_path: Path) -> None:
path = tmp_path / "cli-config.json"
path.write_text(
json.dumps(
{
"env": {
"STRIX_SAFETY_MODE": "guarded",
"STRIX_SAFETY_MODEL": "openai/safety-model",
"STRIX_SAFETY_TIMEOUT": "12",
}
}
),
encoding="utf-8",
)
loader.apply_config_override(path)
settings = loader.load_settings().safety
assert settings.mode == "guarded"
assert settings.model == "openai/safety-model"
assert settings.timeout == 12
def test_apply_config_override_invalidates_cache(tmp_path: Path) -> None:
first = tmp_path / "first.json"
first.write_text(json.dumps({"env": {"STRIX_LLM": "first-model"}}), encoding="utf-8")