diff --git a/docs/usage/cli.mdx b/docs/usage/cli.mdx index 699fb1cb..032fd522 100644 --- a/docs/usage/cli.mdx +++ b/docs/usage/cli.mdx @@ -17,7 +17,7 @@ strix (--target | --target-list ) [options] When the target is an API spec, Strix copies it into the agent's workspace and authorizes the base URLs it declares (including those resolved from a Postman environment) as in-scope hosts - so the agent reads the contract and tests the full declared surface instead of discovering endpoints by crawling. Pair the spec with the deployed base URL (e.g. `--target ./openapi.yaml --target https://api.example.com`) so the agent has a reachable host to attack. - A local directory is mounted into the sandbox live and **writable**, so the agent edits your real files (`.git` excepted). Commit or stash first. + By default, a local directory is mounted into the sandbox live and **writable**, so the agent edits your real files (`.git` excepted). Commit or stash first, or use `--read-only-local-targets` with a separate `--workspace-mount` for immutable evidence. @@ -44,6 +44,19 @@ strix (--target | --target-list ) [options] [Workspace files](/usage/instructions#workspace-files). + + Mount every local-code target read-only as immutable evidence. Backends that + cannot enforce read-only bind mounts reject the run instead of silently + making the target writable. Pair this option with `--workspace-mount` when + the agent should produce remediation changes. + + + + Mount an existing host directory as a writable working area separate from + scan targets. It grants no assessment scope. The path must not be the same as, + contain, or be contained by a read-only local target. + + Scan depth: `quick`, `standard`, or `deep`. diff --git a/strix/interface/cli_args.py b/strix/interface/cli_args.py index b10fd8d3..10df679e 100644 --- a/strix/interface/cli_args.py +++ b/strix/interface/cli_args.py @@ -376,11 +376,15 @@ Examples: "--resume picks up where the prior run left off, including the " "original target list." ) - if args.workspace_mount or args.read_only_local_targets: + if args.workspace_mount: parser.error( - "Cannot combine --resume with --workspace-mount or " - "--read-only-local-targets. Resume restores the original " - "containment configuration." + "Cannot combine --resume with --workspace-mount. Resume restores " + "the original containment configuration." + ) + if args.read_only_local_targets: + parser.error( + "Cannot combine --resume with --read-only-local-targets. Resume " + "restores the original containment configuration." ) _load_resume_state(args, parser) agents_path = runtime_state_dir(run_dir_for(args.resume)) / "agents.json" diff --git a/strix/interface/scan_setup.py b/strix/interface/scan_setup.py index 7410b514..6e5a7fee 100644 --- a/strix/interface/scan_setup.py +++ b/strix/interface/scan_setup.py @@ -216,6 +216,8 @@ def _same_location(left: Path, right: Path) -> bool: def _reject_workspace_overlap(workspace: Path, local_sources: list[dict[str, Any]]) -> None: workspace = workspace.expanduser().resolve() for source in local_sources: + # Only immutable evidence needs an alias guard. Two writable views do + # not weaken the containment contract. if not source.get("read_only"): continue evidence = Path(str(source["source_path"])).expanduser().resolve() diff --git a/tests/test_cli_target_list.py b/tests/test_cli_target_list.py index e713df18..3d26e0e9 100644 --- a/tests/test_cli_target_list.py +++ b/tests/test_cli_target_list.py @@ -97,15 +97,19 @@ def test_parse_arguments_rejects_resume_with_containment_overrides( if flag == "workspace": workspace = tmp_path / "remediation" workspace.mkdir() - argv.extend(["--workspace-mount", str(workspace)]) + expected_flag = "--workspace-mount" + argv.extend([expected_flag, str(workspace)]) else: - argv.append("--read-only-local-targets") + expected_flag = "--read-only-local-targets" + argv.append(expected_flag) monkeypatch.setattr(sys, "argv", argv) with pytest.raises(SystemExit): cli_main.parse_arguments() - assert "Resume restores the original containment configuration" in capsys.readouterr().err + error = capsys.readouterr().err + assert expected_flag in error + assert "Resume restores the original containment configuration" in error def _write_run_record(runs_dir: Path, run_name: str, record: dict[str, Any]) -> None: