worldstate-check 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ """WorldState Check package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,15 @@
1
+ from .command import run_command_check
2
+ from .file import run_file_check
3
+ from .http import run_http_check
4
+ from .json_check import run_json_check
5
+ from .metric import run_metric_check
6
+ from .tcp import run_tcp_check
7
+
8
+ CHECK_RUNNERS = {
9
+ "file": run_file_check,
10
+ "json": run_json_check,
11
+ "metric": run_metric_check,
12
+ "http": run_http_check,
13
+ "tcp": run_tcp_check,
14
+ "command": run_command_check,
15
+ }
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any, Callable
5
+
6
+ from worldstate_check.models import CheckResult, CheckStatus, VerificationContext
7
+
8
+
9
+ def timed_result(
10
+ check: dict[str, Any],
11
+ fn: Callable[[], tuple[CheckStatus, str, Any, Any, dict[str, Any], str | None]],
12
+ ) -> CheckResult:
13
+ started = time.perf_counter()
14
+ status, summary, expected, observed, evidence, error = fn()
15
+ return CheckResult(
16
+ check_id=check["id"],
17
+ check_type=check["type"],
18
+ required=check.get("required", True),
19
+ status=status,
20
+ summary=summary,
21
+ expected=expected,
22
+ observed=observed,
23
+ evidence=evidence,
24
+ error=error,
25
+ duration_ms=(time.perf_counter() - started) * 1000,
26
+ )
27
+
28
+
29
+ def unknown(check: dict[str, Any], message: str, *, evidence: dict[str, Any] | None = None) -> CheckResult:
30
+ return CheckResult(
31
+ check_id=check["id"],
32
+ check_type=check["type"],
33
+ required=check.get("required", True),
34
+ status=CheckStatus.UNKNOWN,
35
+ summary=message,
36
+ evidence=evidence or {},
37
+ error=message,
38
+ )
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ from typing import Any
6
+
7
+ from worldstate_check.errors import PathBoundaryError
8
+ from worldstate_check.models import CheckStatus, VerificationContext
9
+ from worldstate_check.util import resolve_path
10
+
11
+ from .base import timed_result, unknown
12
+
13
+
14
+ def run_command_check(check: dict[str, Any], ctx: VerificationContext):
15
+ if not ctx.allow_command:
16
+ return unknown(check, "command checks are disabled; pass --allow-command for a trusted specification")
17
+ try:
18
+ cwd = resolve_path(ctx.root, check.get("cwd", "."), ctx.allow_outside_root)
19
+ except PathBoundaryError as exc:
20
+ return unknown(check, str(exc))
21
+
22
+ def evaluate():
23
+ max_bytes = check.get("max_output_bytes", 65_536)
24
+ timeout = float(check.get("timeout_seconds", 10.0))
25
+ env = {"PATH": os.environ.get("PATH", ""), "HOME": os.environ.get("HOME", "")}
26
+ try:
27
+ proc = subprocess.run(
28
+ check["argv"],
29
+ cwd=cwd,
30
+ env=env,
31
+ capture_output=True,
32
+ text=False,
33
+ shell=False,
34
+ timeout=timeout,
35
+ check=False,
36
+ )
37
+ except subprocess.TimeoutExpired:
38
+ return CheckStatus.FAIL, "verification command timed out", {"timeout_seconds": timeout}, None, {"cwd": str(cwd)}, None
39
+ except (OSError, ValueError) as exc:
40
+ return CheckStatus.UNKNOWN, "verification command could not be started", None, None, {"cwd": str(cwd)}, str(exc)
41
+
42
+ stdout_bytes = proc.stdout[: max_bytes + 1]
43
+ stderr_bytes = proc.stderr[: max_bytes + 1]
44
+ if len(stdout_bytes) > max_bytes or len(stderr_bytes) > max_bytes:
45
+ return (
46
+ CheckStatus.UNKNOWN,
47
+ "verification command output exceeds configured evidence limit",
48
+ None,
49
+ {"exit_code": proc.returncode},
50
+ {"cwd": str(cwd), "max_output_bytes": max_bytes},
51
+ None,
52
+ )
53
+ stdout = stdout_bytes.decode("utf-8", errors="replace")
54
+ stderr = stderr_bytes.decode("utf-8", errors="replace")
55
+ expected: dict[str, Any] = {}
56
+ observed: dict[str, Any] = {"exit_code": proc.returncode}
57
+ failures: list[str] = []
58
+ if "exit_code" in check:
59
+ expected["exit_code"] = check["exit_code"]
60
+ if proc.returncode != check["exit_code"]:
61
+ failures.append(f"exit_code={proc.returncode}")
62
+ if "stdout_contains" in check:
63
+ expected["stdout_contains"] = check["stdout_contains"]
64
+ matched = check["stdout_contains"] in stdout
65
+ observed["stdout_contains"] = matched
66
+ if not matched:
67
+ failures.append("required stdout text missing")
68
+ if "stderr_contains" in check:
69
+ expected["stderr_contains"] = check["stderr_contains"]
70
+ matched = check["stderr_contains"] in stderr
71
+ observed["stderr_contains"] = matched
72
+ if not matched:
73
+ failures.append("required stderr text missing")
74
+
75
+ evidence = {
76
+ "argv": check["argv"],
77
+ "cwd": str(cwd),
78
+ "stdout_excerpt": stdout[-2000:],
79
+ "stderr_excerpt": stderr[-2000:],
80
+ }
81
+ if failures:
82
+ return CheckStatus.FAIL, "; ".join(failures), expected, observed, evidence, None
83
+ return CheckStatus.PASS, "command postcondition satisfied", expected, observed, evidence, None
84
+
85
+ return timed_result(check, evaluate)
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from typing import Any
5
+
6
+ from worldstate_check.errors import PathBoundaryError
7
+ from worldstate_check.models import CheckStatus, VerificationContext
8
+ from worldstate_check.util import resolve_path
9
+
10
+ from .base import timed_result, unknown
11
+
12
+
13
+ def run_file_check(check: dict[str, Any], ctx: VerificationContext):
14
+ try:
15
+ path = resolve_path(ctx.root, check["path"], ctx.allow_outside_root)
16
+ except PathBoundaryError as exc:
17
+ return unknown(check, str(exc))
18
+
19
+ def evaluate():
20
+ exists = path.exists()
21
+ expected: dict[str, Any] = {}
22
+ observed: dict[str, Any] = {"exists": exists}
23
+ evidence: dict[str, Any] = {"path": str(path)}
24
+ failures: list[str] = []
25
+
26
+ if "exists" in check:
27
+ expected["exists"] = check["exists"]
28
+ if exists != check["exists"]:
29
+ failures.append(f"exists={exists}")
30
+
31
+ content: str | None = None
32
+ if any(k in check for k in {"contains", "not_contains"}):
33
+ if not exists or not path.is_file():
34
+ failures.append("file content unavailable")
35
+ else:
36
+ max_bytes = check.get("max_read_bytes", 1_048_576)
37
+ size = path.stat().st_size
38
+ evidence["size_bytes"] = size
39
+ if size > max_bytes:
40
+ return (
41
+ CheckStatus.UNKNOWN,
42
+ f"file exceeds max_read_bytes ({size} > {max_bytes})",
43
+ expected,
44
+ observed,
45
+ evidence,
46
+ "file too large for configured text assertion",
47
+ )
48
+ try:
49
+ content = path.read_text(encoding="utf-8")
50
+ except (OSError, UnicodeDecodeError) as exc:
51
+ return CheckStatus.UNKNOWN, "could not read file as UTF-8", expected, observed, evidence, str(exc)
52
+
53
+ if "contains" in check:
54
+ expected["contains"] = check["contains"]
55
+ matched = content is not None and check["contains"] in content
56
+ observed["contains"] = matched
57
+ if not matched:
58
+ failures.append("required text missing")
59
+
60
+ if "not_contains" in check:
61
+ expected["not_contains"] = check["not_contains"]
62
+ matched = content is not None and check["not_contains"] not in content
63
+ observed["not_contains"] = matched
64
+ if not matched:
65
+ failures.append("forbidden text present")
66
+
67
+ if "sha256" in check:
68
+ expected["sha256"] = check["sha256"].lower()
69
+ if not exists or not path.is_file():
70
+ failures.append("sha256 unavailable")
71
+ else:
72
+ digest = hashlib.sha256()
73
+ try:
74
+ with path.open("rb") as handle:
75
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
76
+ digest.update(chunk)
77
+ except OSError as exc:
78
+ return CheckStatus.UNKNOWN, "could not hash file", expected, observed, evidence, str(exc)
79
+ observed["sha256"] = digest.hexdigest()
80
+ if observed["sha256"] != expected["sha256"]:
81
+ failures.append("sha256 mismatch")
82
+
83
+ if failures:
84
+ return CheckStatus.FAIL, "; ".join(failures), expected, observed, evidence, None
85
+ return CheckStatus.PASS, "file postcondition satisfied", expected, observed, evidence, None
86
+
87
+ return timed_result(check, evaluate)
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import urllib.error
5
+ import urllib.request
6
+ from typing import Any
7
+
8
+ from worldstate_check.errors import SpecError
9
+ from worldstate_check.models import CheckStatus, VerificationContext
10
+ from worldstate_check.util import compare_value, extract_dotted
11
+
12
+ from .base import timed_result
13
+
14
+
15
+ def run_http_check(check: dict[str, Any], ctx: VerificationContext):
16
+ del ctx
17
+
18
+ def evaluate():
19
+ timeout = float(check.get("timeout_seconds", 3.0))
20
+ request = urllib.request.Request(check["url"], method="GET", headers={"User-Agent": "worldstate-check/0.1"})
21
+ evidence: dict[str, Any] = {"url": check["url"]}
22
+ try:
23
+ response = urllib.request.urlopen(request, timeout=timeout)
24
+ status = response.status
25
+ body_bytes = response.read(1_048_577)
26
+ except urllib.error.HTTPError as exc:
27
+ status = exc.code
28
+ body_bytes = exc.read(1_048_577)
29
+ except (urllib.error.URLError, TimeoutError, OSError) as exc:
30
+ return CheckStatus.FAIL, "HTTP endpoint could not be reached", None, None, evidence, str(exc)
31
+
32
+ if len(body_bytes) > 1_048_576:
33
+ return CheckStatus.UNKNOWN, "HTTP response exceeds 1 MiB evidence limit", None, status, evidence, None
34
+ body = body_bytes.decode("utf-8", errors="replace")
35
+ observed: dict[str, Any] = {"status": status}
36
+ expected: dict[str, Any] = {}
37
+ failures: list[str] = []
38
+
39
+ if "status" in check:
40
+ expected["status"] = check["status"]
41
+ if status != check["status"]:
42
+ failures.append(f"status={status}")
43
+ if "text_contains" in check:
44
+ expected["text_contains"] = check["text_contains"]
45
+ matched = check["text_contains"] in body
46
+ observed["text_contains"] = matched
47
+ if not matched:
48
+ failures.append("required response text missing")
49
+ if "json_field" in check:
50
+ try:
51
+ payload = json.loads(body)
52
+ value = extract_dotted(payload, check["json_field"])
53
+ matched, expected_value = compare_value(value, check["operator"], check)
54
+ except (json.JSONDecodeError, KeyError, ValueError, SpecError) as exc:
55
+ return CheckStatus.UNKNOWN, "HTTP JSON assertion could not be evaluated", expected, observed, evidence, str(exc)
56
+ expected["json"] = {"field": check["json_field"], "value": expected_value, "operator": check["operator"]}
57
+ observed["json"] = {"field": check["json_field"], "value": value}
58
+ if not matched:
59
+ failures.append("JSON response postcondition not satisfied")
60
+
61
+ evidence["body_bytes"] = len(body_bytes)
62
+ if failures:
63
+ return CheckStatus.FAIL, "; ".join(failures), expected, observed, evidence, None
64
+ return CheckStatus.PASS, "HTTP postcondition satisfied", expected, observed, evidence, None
65
+
66
+ return timed_result(check, evaluate)
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from worldstate_check.errors import PathBoundaryError, SpecError
6
+ from worldstate_check.models import CheckStatus, VerificationContext
7
+ from worldstate_check.util import compare_value, evidence_path, extract_dotted, read_text_limited, resolve_path, strict_json_loads
8
+
9
+ from .base import timed_result, unknown
10
+
11
+
12
+ def run_json_check(check: dict[str, Any], ctx: VerificationContext):
13
+ try:
14
+ path = resolve_path(ctx.root, check["path"], ctx.allow_outside_root)
15
+ except PathBoundaryError as exc:
16
+ return unknown(check, str(exc))
17
+
18
+ def evaluate():
19
+ evidence = {"path": evidence_path(path, ctx.root), "field": check["field"]}
20
+ try:
21
+ raw = read_text_limited(path, int(check.get("max_read_bytes", 1_048_576)))
22
+ data = strict_json_loads(raw)
23
+ except FileNotFoundError:
24
+ return CheckStatus.FAIL, "JSON file does not exist", None, None, evidence, None
25
+ except (OSError, UnicodeDecodeError, ValueError) as exc:
26
+ return CheckStatus.UNKNOWN, "could not parse JSON evidence", None, None, evidence, str(exc)
27
+ try:
28
+ observed = extract_dotted(data, check["field"])
29
+ except KeyError:
30
+ return CheckStatus.FAIL, f"JSON field not found: {check['field']}", None, None, evidence, None
31
+ try:
32
+ matched, expected = compare_value(observed, check["operator"], check)
33
+ except (ValueError, SpecError) as exc:
34
+ return CheckStatus.UNKNOWN, "JSON comparison could not be evaluated", None, observed, evidence, str(exc)
35
+ if matched:
36
+ return CheckStatus.PASS, "JSON postcondition satisfied", expected, observed, evidence, None
37
+ return CheckStatus.FAIL, "JSON postcondition not satisfied", expected, observed, evidence, None
38
+
39
+ return timed_result(check, evaluate)
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import io
5
+ import math
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from worldstate_check.errors import PathBoundaryError, SpecError
11
+ from worldstate_check.models import CheckStatus, VerificationContext
12
+ from worldstate_check.util import (
13
+ compare_value,
14
+ evidence_path,
15
+ extract_dotted,
16
+ freshness_age_seconds,
17
+ read_text_limited,
18
+ resolve_path,
19
+ strict_json_loads,
20
+ )
21
+
22
+ from .base import timed_result, unknown
23
+
24
+ DEFAULT_MAX_TELEMETRY_BYTES = 16 * 1024 * 1024
25
+
26
+
27
+ def run_metric_check(check: dict[str, Any], ctx: VerificationContext):
28
+ source = check["source"]
29
+ try:
30
+ path = resolve_path(ctx.root, source["path"], ctx.allow_outside_root)
31
+ except PathBoundaryError as exc:
32
+ return unknown(check, str(exc))
33
+
34
+ def evaluate():
35
+ evidence: dict[str, Any] = {"path": evidence_path(path, ctx.root), "source_type": source["type"]}
36
+ try:
37
+ observed, timestamp_value = _read_source(path, source)
38
+ except FileNotFoundError:
39
+ return CheckStatus.FAIL, "telemetry source does not exist", None, None, evidence, None
40
+ except (OSError, UnicodeDecodeError, csv.Error, KeyError, ValueError) as exc:
41
+ return CheckStatus.UNKNOWN, "could not read telemetry evidence", None, None, evidence, str(exc)
42
+
43
+ if "max_age_seconds" in source:
44
+ try:
45
+ age = freshness_age_seconds(timestamp_value, datetime.now(timezone.utc))
46
+ except (ValueError, OSError, OverflowError) as exc:
47
+ return CheckStatus.UNKNOWN, "could not evaluate telemetry freshness", None, observed, evidence, str(exc)
48
+ evidence["age_seconds"] = round(age, 3)
49
+ evidence["max_age_seconds"] = source["max_age_seconds"]
50
+ if age > float(source["max_age_seconds"]):
51
+ return (
52
+ CheckStatus.FAIL,
53
+ "telemetry is stale",
54
+ {"max_age_seconds": source["max_age_seconds"]},
55
+ {"age_seconds": round(age, 3), "value": observed},
56
+ evidence,
57
+ None,
58
+ )
59
+
60
+ try:
61
+ matched, expected = compare_value(observed, check["operator"], check)
62
+ except (ValueError, SpecError) as exc:
63
+ return CheckStatus.UNKNOWN, "metric comparison could not be evaluated", None, observed, evidence, str(exc)
64
+ if matched:
65
+ return CheckStatus.PASS, "metric postcondition satisfied", expected, observed, evidence, None
66
+ return CheckStatus.FAIL, "metric postcondition not satisfied", expected, observed, evidence, None
67
+
68
+ return timed_result(check, evaluate)
69
+
70
+
71
+ def _read_source(path: Path, source: dict[str, Any]) -> tuple[Any, Any]:
72
+ raw = read_text_limited(path, int(source.get("max_read_bytes", DEFAULT_MAX_TELEMETRY_BYTES)))
73
+ if source["type"] == "json":
74
+ data = strict_json_loads(raw)
75
+ observed = extract_dotted(data, source["field"])
76
+ timestamp = extract_dotted(data, source["timestamp_field"]) if "max_age_seconds" in source else None
77
+ return observed, timestamp
78
+
79
+ reader = csv.DictReader(io.StringIO(raw.lstrip("\ufeff")))
80
+ if reader.fieldnames is None:
81
+ raise ValueError("CSV telemetry source has no header")
82
+ if len(reader.fieldnames) != len(set(reader.fieldnames)):
83
+ raise ValueError("CSV telemetry source has duplicate column names")
84
+ row: dict[str, str | None] | None = None
85
+ for row in reader:
86
+ if None in row:
87
+ raise ValueError("CSV telemetry row has more values than the header")
88
+ if row is None:
89
+ raise ValueError("CSV telemetry source has no data rows")
90
+ if source["column"] not in row:
91
+ raise KeyError(source["column"])
92
+ observed = _coerce_scalar(row[source["column"]])
93
+ timestamp = row[source["timestamp_column"]] if "max_age_seconds" in source else None
94
+ return observed, timestamp
95
+
96
+
97
+ def _coerce_scalar(value: str | None) -> Any:
98
+ if value is None:
99
+ raise ValueError("CSV telemetry value is missing")
100
+ stripped = value.strip()
101
+ lowered = stripped.lower()
102
+ if lowered == "true":
103
+ return True
104
+ if lowered == "false":
105
+ return False
106
+ try:
107
+ if any(c in stripped for c in ".eE"):
108
+ value_float = float(stripped)
109
+ if not math.isfinite(value_float):
110
+ return stripped
111
+ return value_float
112
+ return int(stripped)
113
+ except ValueError:
114
+ return stripped
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import socket
4
+ from typing import Any
5
+
6
+ from worldstate_check.models import CheckStatus, VerificationContext
7
+
8
+ from .base import timed_result, unknown
9
+
10
+
11
+ def run_tcp_check(check: dict[str, Any], ctx: VerificationContext):
12
+ if not ctx.allow_network:
13
+ return unknown(check, "network checks are disabled; pass --allow-network for a trusted specification")
14
+
15
+ def evaluate():
16
+ expected = check.get("reachable", True)
17
+ timeout = float(check.get("timeout_seconds", 2.0))
18
+ evidence = {"host": check["host"], "port": check["port"]}
19
+ try:
20
+ with socket.create_connection((check["host"], check["port"]), timeout=timeout):
21
+ reachable = True
22
+ except OSError as exc:
23
+ reachable = False
24
+ evidence["connection_error"] = exc.__class__.__name__
25
+ status = CheckStatus.PASS if reachable == expected else CheckStatus.FAIL
26
+ summary = "TCP postcondition satisfied" if status is CheckStatus.PASS else "TCP postcondition not satisfied"
27
+ return status, summary, expected, reachable, evidence, None
28
+
29
+ return timed_result(check, evaluate)
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from . import __version__
9
+ from .demo import run_demo
10
+ from .engine import verify
11
+ from .errors import SpecError
12
+ from .loader import load_spec
13
+ from .models import VerificationContext, Verdict
14
+ from .report import render_text, report_payload, verify_json_report, write_json_report
15
+
16
+ EXIT_BY_VERDICT = {
17
+ Verdict.VERIFIED: 0,
18
+ Verdict.NOT_VERIFIED: 1,
19
+ Verdict.UNCERTAIN: 2,
20
+ }
21
+
22
+
23
+ def build_parser() -> argparse.ArgumentParser:
24
+ parser = argparse.ArgumentParser(
25
+ prog="worldstate-check",
26
+ description="Verify observed postconditions after an AI agent or autonomous system acts.",
27
+ )
28
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
29
+ sub = parser.add_subparsers(dest="command", required=True)
30
+
31
+ verify_parser = sub.add_parser("verify", help="evaluate a verification specification")
32
+ verify_parser.add_argument("spec", type=Path)
33
+ verify_parser.add_argument("--root", type=Path, help="verification root; defaults to the specification directory")
34
+ verify_parser.add_argument("--allow-outside-root", action="store_true", help="allow file paths outside the verification root")
35
+ verify_parser.add_argument("--allow-command", action="store_true", help="allow command checks in a trusted specification")
36
+ verify_parser.add_argument("--allow-network", action="store_true", help="allow HTTP and TCP checks in a trusted specification")
37
+ verify_parser.add_argument("--wait", type=float, default=0.0, metavar="SECONDS", help="retry until verified or the timeout expires")
38
+ verify_parser.add_argument("--poll-interval", type=float, default=0.5, metavar="SECONDS")
39
+ verify_parser.add_argument("--report", type=Path, help="write a JSON evidence report")
40
+ verify_parser.add_argument("--json", action="store_true", help="print the JSON report instead of text")
41
+
42
+ validate_parser = sub.add_parser("validate", help="validate a specification without evaluating it")
43
+ validate_parser.add_argument("spec", type=Path)
44
+
45
+ report_parser = sub.add_parser("verify-report", help="verify the SHA-256 digest of a saved evidence report")
46
+ report_parser.add_argument("report", type=Path)
47
+
48
+ init_parser = sub.add_parser("init", help="write a minimal starter specification")
49
+ init_parser.add_argument("path", type=Path, nargs="?", default=Path("worldstate.yaml"))
50
+
51
+ demo_parser = sub.add_parser("demo", help="run a deterministic synthetic example")
52
+ demo_parser.add_argument("--scenario", choices=["verified", "partial"], default="partial")
53
+ return parser
54
+
55
+
56
+ def main(argv: list[str] | None = None) -> int:
57
+ parser = build_parser()
58
+ args = parser.parse_args(argv)
59
+ try:
60
+ if args.command == "validate":
61
+ load_spec(args.spec)
62
+ print(f"VALID: {args.spec}")
63
+ return 0
64
+ if args.command == "verify-report":
65
+ try:
66
+ valid = verify_json_report(args.report)
67
+ except (OSError, json.JSONDecodeError) as exc:
68
+ print(f"ERROR: could not read report: {exc}", file=sys.stderr)
69
+ return 2
70
+ print(f"{'VALID' if valid else 'INVALID'}: {args.report}")
71
+ return 0 if valid else 1
72
+ if args.command == "init":
73
+ return _init_spec(args.path)
74
+ if args.command == "demo":
75
+ return EXIT_BY_VERDICT[run_demo(args.scenario)]
76
+ if args.command == "verify":
77
+ return _verify_command(args)
78
+ except SpecError as exc:
79
+ print(f"SPEC ERROR: {exc}", file=sys.stderr)
80
+ return 2
81
+ except (ValueError, OSError, RuntimeError) as exc:
82
+ print(f"ERROR: {exc}", file=sys.stderr)
83
+ return 2
84
+ return 2
85
+
86
+
87
+ def _verify_command(args: argparse.Namespace) -> int:
88
+ spec_path = args.spec.resolve()
89
+ spec = load_spec(spec_path)
90
+ root = (args.root.resolve() if args.root else spec_path.parent)
91
+ ctx = VerificationContext(
92
+ spec_path=spec_path,
93
+ root=root,
94
+ allow_outside_root=args.allow_outside_root,
95
+ allow_command=args.allow_command,
96
+ allow_network=args.allow_network,
97
+ )
98
+ report = verify(spec, ctx, wait_seconds=args.wait, poll_interval=args.poll_interval)
99
+ if args.report:
100
+ write_json_report(report, args.report)
101
+ if args.json:
102
+ print(json.dumps(report_payload(report), indent=2, sort_keys=True))
103
+ else:
104
+ print(render_text(report))
105
+ if args.report:
106
+ print(f"Report: {args.report.resolve()}")
107
+ return EXIT_BY_VERDICT[report.verdict]
108
+
109
+
110
+ def _init_spec(path: Path) -> int:
111
+ if path.exists():
112
+ print(f"ERROR: refusing to overwrite existing file: {path}", file=sys.stderr)
113
+ return 2
114
+ path.parent.mkdir(parents=True, exist_ok=True)
115
+ path.write_text(
116
+ """version: 1\ntask: verify-outcome\nchecks:\n - id: result-file\n type: file\n path: result.json\n exists: true\n\n - id: result-state\n type: json\n path: result.json\n field: status\n operator: eq\n value: complete\n""",
117
+ encoding="utf-8",
118
+ )
119
+ print(f"Created {path}")
120
+ return 0
121
+
122
+
123
+ if __name__ == "__main__":
124
+ raise SystemExit(main())
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ from .engine import verify
8
+ from .models import VerificationContext, Verdict
9
+ from .report import render_text
10
+
11
+
12
+ def run_demo(scenario: str = "partial") -> Verdict:
13
+ if scenario not in {"verified", "partial"}:
14
+ raise ValueError("scenario must be verified or partial")
15
+ with tempfile.TemporaryDirectory(prefix="worldstate-check-") as raw:
16
+ root = Path(raw)
17
+ (root / "command-ack.txt").write_text("SAFE_MODE_ACK\n", encoding="utf-8")
18
+ attitude = 1.8 if scenario == "verified" else 14.7
19
+ telemetry = {
20
+ "mode": "SAFE",
21
+ "payload": {"power": "off"},
22
+ "attitude": {"error_deg": attitude},
23
+ "battery": {"soc": 71.0},
24
+ }
25
+ (root / "telemetry.json").write_text(json.dumps(telemetry), encoding="utf-8")
26
+ spec = {
27
+ "version": 1,
28
+ "task": "enter-safe-mode",
29
+ "checks": [
30
+ {"id": "command-ack", "type": "file", "path": "command-ack.txt", "contains": "SAFE_MODE_ACK"},
31
+ {"id": "payload-power", "type": "json", "path": "telemetry.json", "field": "payload.power", "operator": "eq", "value": "off"},
32
+ {
33
+ "id": "attitude-error",
34
+ "type": "metric",
35
+ "source": {"type": "json", "path": "telemetry.json", "field": "attitude.error_deg"},
36
+ "operator": "lte",
37
+ "value": 3.0,
38
+ },
39
+ {
40
+ "id": "battery-soc",
41
+ "type": "metric",
42
+ "source": {"type": "json", "path": "telemetry.json", "field": "battery.soc"},
43
+ "operator": "gte",
44
+ "value": 25.0,
45
+ },
46
+ ],
47
+ }
48
+ report = verify(spec, VerificationContext(spec_path=root / "demo.yaml", root=root))
49
+ print(render_text(report))
50
+ return report.verdict