Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/guides/setup-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Setup preview contract

`verdict setup --dry-run --json` produces a mutation-free `setup_plan`.
Every proposed action includes its reason, security impact, postcondition, and
exact undo description. The preview does not read configuration contents,
probe optional tools, contact the network, or access credentials.

An existing configuration is preserved as a read-only action. A missing
configuration is presented as a consent-required creation action. Applying
either action remains a separate transactional boundary; the preview itself
does not authorize or perform mutations.
39 changes: 39 additions & 0 deletions tests/test_setup_plan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Tests for auditable, mutation-free setup plans."""

from __future__ import annotations

from pathlib import Path

from verdict.setup_plan import build_setup_plan


def test_setup_actions_explain_reason_security_postcondition_and_undo(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))

report = build_setup_plan().to_dict()
action = report["actions"][0]

for field in ("reason", "security_impact", "postcondition", "undo"):
assert isinstance(action[field], str)
assert action[field]
assert report["mutation_free"] is True
assert not (tmp_path / "config").exists()


def test_existing_config_preview_explains_noop_without_reading_contents(
tmp_path: Path, monkeypatch
) -> None:
config_dir = tmp_path / "config" / "verdict"
config_dir.mkdir(parents=True)
config_path = config_dir / "verdict.yaml"
config_path.write_text("private: do-not-read\n", encoding="utf-8")
before = config_path.read_bytes()
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))

action = build_setup_plan().to_dict()["actions"][0]

assert action["action_id"] == "preserve-config"
assert "byte-for-byte" in action["postcondition"]
assert config_path.read_bytes() == before
16 changes: 16 additions & 0 deletions verdict/setup_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class SetupAction:
kind: str
target: str
description: str
reason: str
security_impact: str
postcondition: str
undo: str
reversible: bool
requires_consent: bool

Expand All @@ -36,6 +40,10 @@ def to_dict(self) -> dict[str, object]:
"kind": self.kind,
"target": self.target,
"description": self.description,
"reason": self.reason,
"security_impact": self.security_impact,
"postcondition": self.postcondition,
"undo": self.undo,
"reversible": self.reversible,
"requires_consent": self.requires_consent,
}
Expand Down Expand Up @@ -124,6 +132,10 @@ def build_setup_plan() -> SetupPlan:
kind="inspect",
target="config",
description="Preserve the existing Verdict configuration unchanged.",
reason="An existing configuration must not be overwritten implicitly.",
security_impact="No change; existing configuration and credential references remain untouched.",
postcondition="The existing configuration remains byte-for-byte unchanged.",
undo="No action required; this is a read-only inspection.",
reversible=True,
requires_consent=False,
),
Expand All @@ -135,6 +147,10 @@ def build_setup_plan() -> SetupPlan:
kind="write_config",
target="config",
description="Create a minimal Verdict configuration after explicit consent.",
reason="A missing configuration is required before an operator can apply setup.",
security_impact="Creates only local configuration; credentials remain environment/keyring references.",
postcondition="A validated minimal configuration exists at the planned target.",
undo="Remove the newly created configuration after confirming it was not modified.",
reversible=True,
requires_consent=True,
),
Expand Down