verifycraft 0.1.0__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.
- verifycraft/__init__.py +48 -0
- verifycraft/cli.py +211 -0
- verifycraft/contracts/__init__.py +31 -0
- verifycraft/contracts/models.py +344 -0
- verifycraft/core/__init__.py +31 -0
- verifycraft/core/evaluator.py +173 -0
- verifycraft/core/fingerprint.py +270 -0
- verifycraft/core/policy.py +112 -0
- verifycraft/core/runner.py +275 -0
- verifycraft/engine.py +450 -0
- verifycraft/py.typed +1 -0
- verifycraft-0.1.0.dist-info/METADATA +286 -0
- verifycraft-0.1.0.dist-info/RECORD +16 -0
- verifycraft-0.1.0.dist-info/WHEEL +4 -0
- verifycraft-0.1.0.dist-info/entry_points.txt +2 -0
- verifycraft-0.1.0.dist-info/licenses/LICENSE +201 -0
verifycraft/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""VerifyCraft: Composable review, testing, and verification capabilities for AI agents."""
|
|
2
|
+
|
|
3
|
+
from verifycraft.contracts.models import (
|
|
4
|
+
CheckSpec,
|
|
5
|
+
CheckType,
|
|
6
|
+
Evidence,
|
|
7
|
+
ExecutionMode,
|
|
8
|
+
ExecutionPolicy,
|
|
9
|
+
ExpectedObservation,
|
|
10
|
+
Finding,
|
|
11
|
+
Hypothesis,
|
|
12
|
+
ResourceUsage,
|
|
13
|
+
Verdict,
|
|
14
|
+
VerificationInput,
|
|
15
|
+
VerificationResult,
|
|
16
|
+
)
|
|
17
|
+
from verifycraft.core.fingerprint import FingerprintError
|
|
18
|
+
from verifycraft.core.policy import PolicyViolationError
|
|
19
|
+
from verifycraft.core.runner import (
|
|
20
|
+
CheckRunner,
|
|
21
|
+
FakeRunner,
|
|
22
|
+
TrustedPytestRunner,
|
|
23
|
+
)
|
|
24
|
+
from verifycraft.engine import VerificationEngine
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"CheckRunner",
|
|
30
|
+
"CheckSpec",
|
|
31
|
+
"CheckType",
|
|
32
|
+
"Evidence",
|
|
33
|
+
"ExecutionMode",
|
|
34
|
+
"ExecutionPolicy",
|
|
35
|
+
"ExpectedObservation",
|
|
36
|
+
"FakeRunner",
|
|
37
|
+
"Finding",
|
|
38
|
+
"FingerprintError",
|
|
39
|
+
"Hypothesis",
|
|
40
|
+
"PolicyViolationError",
|
|
41
|
+
"ResourceUsage",
|
|
42
|
+
"TrustedPytestRunner",
|
|
43
|
+
"Verdict",
|
|
44
|
+
"VerificationEngine",
|
|
45
|
+
"VerificationInput",
|
|
46
|
+
"VerificationResult",
|
|
47
|
+
"__version__",
|
|
48
|
+
]
|
verifycraft/cli.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Command line interface for VerifyCraft.
|
|
2
|
+
|
|
3
|
+
Provides a thin CLI entry point over the exact same VerificationEngine.
|
|
4
|
+
Does not duplicate any verification or evaluation business logic.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from verifycraft import __version__
|
|
15
|
+
from verifycraft.contracts.models import (
|
|
16
|
+
CheckSpec,
|
|
17
|
+
CheckType,
|
|
18
|
+
ExecutionMode,
|
|
19
|
+
ExecutionPolicy,
|
|
20
|
+
ExpectedObservation,
|
|
21
|
+
Hypothesis,
|
|
22
|
+
Verdict,
|
|
23
|
+
VerificationInput,
|
|
24
|
+
VerificationResult,
|
|
25
|
+
)
|
|
26
|
+
from verifycraft.engine import VerificationEngine
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _format_summary(result: VerificationResult) -> str:
|
|
30
|
+
lines = [
|
|
31
|
+
"=" * 60,
|
|
32
|
+
f" VerifyCraft Verification Report ({result.schema_version})",
|
|
33
|
+
"=" * 60,
|
|
34
|
+
f" Verdict: {result.verdict.value.upper()}",
|
|
35
|
+
f" Fingerprint: {result.fingerprint}",
|
|
36
|
+
f" Wall Time: {result.resource_usage.wall_time_seconds:.4f}s",
|
|
37
|
+
]
|
|
38
|
+
if result.blocked_reason:
|
|
39
|
+
lines.append(f" Blocked Reason: {result.blocked_reason}")
|
|
40
|
+
if result.inconclusive_reason:
|
|
41
|
+
lines.append(f" Inconclusive: {result.inconclusive_reason}")
|
|
42
|
+
|
|
43
|
+
if result.findings:
|
|
44
|
+
lines.append("-" * 60)
|
|
45
|
+
lines.append(" Findings:")
|
|
46
|
+
for f in result.findings:
|
|
47
|
+
lines.append(f" [{f.verdict.value}] Hypothesis: {f.hypothesis_id}")
|
|
48
|
+
lines.append(f" Reason: {f.reason}")
|
|
49
|
+
|
|
50
|
+
if result.evidence:
|
|
51
|
+
lines.append("-" * 60)
|
|
52
|
+
lines.append(f" Evidence Collected ({len(result.evidence)} items):")
|
|
53
|
+
for evi in result.evidence:
|
|
54
|
+
lines.append(f" Evidence ID: {evi.evidence_id} (exit code: {evi.exit_code})")
|
|
55
|
+
if evi.output_truncated:
|
|
56
|
+
lines.append(" [Output truncated due to size limits]")
|
|
57
|
+
if evi.stdout.strip():
|
|
58
|
+
sample = evi.stdout.strip()[:300]
|
|
59
|
+
lines.append(f" Output sample:\n{sample}")
|
|
60
|
+
lines.append("=" * 60)
|
|
61
|
+
return "\n".join(lines)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
VERDICT_EXIT_CODES: dict[Verdict, int] = {
|
|
65
|
+
Verdict.SUPPORTED: 0,
|
|
66
|
+
Verdict.REFUTED: 2,
|
|
67
|
+
Verdict.INCONCLUSIVE: 3,
|
|
68
|
+
Verdict.BLOCKED: 4,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
73
|
+
parser = argparse.ArgumentParser(
|
|
74
|
+
prog="verifycraft",
|
|
75
|
+
description="VerifyCraft: Composable review, testing, and verification capabilities.",
|
|
76
|
+
epilog=(
|
|
77
|
+
"Exit codes:\n"
|
|
78
|
+
" 0 Hypothesis SUPPORTED by observable evidence\n"
|
|
79
|
+
" 1 Operational / CLI usage error\n"
|
|
80
|
+
" 2 Hypothesis REFUTED by evidence\n"
|
|
81
|
+
" 3 Verification INCONCLUSIVE\n"
|
|
82
|
+
" 4 Verification BLOCKED (policy or safety boundary)\n"
|
|
83
|
+
),
|
|
84
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument("-v", "--version", action="version", version=f"verifycraft {__version__}")
|
|
87
|
+
|
|
88
|
+
subparsers = parser.add_subparsers(dest="subcommand", help="Available subcommands")
|
|
89
|
+
|
|
90
|
+
# verifycraft verify
|
|
91
|
+
verify_parser = subparsers.add_parser(
|
|
92
|
+
"verify", help="Verify a hypothesis against an approved check."
|
|
93
|
+
)
|
|
94
|
+
verify_parser.add_argument(
|
|
95
|
+
"--input",
|
|
96
|
+
type=Path,
|
|
97
|
+
help="Path to JSON file containing VerificationInput specification.",
|
|
98
|
+
)
|
|
99
|
+
verify_parser.add_argument(
|
|
100
|
+
"--repo",
|
|
101
|
+
default=".",
|
|
102
|
+
help="Repository path (default: current directory).",
|
|
103
|
+
)
|
|
104
|
+
verify_parser.add_argument(
|
|
105
|
+
"--target",
|
|
106
|
+
help="Target test file (e.g. tests/test_regression.py).",
|
|
107
|
+
)
|
|
108
|
+
verify_parser.add_argument(
|
|
109
|
+
"--claim",
|
|
110
|
+
help="Hypothesis claim description.",
|
|
111
|
+
)
|
|
112
|
+
verify_parser.add_argument(
|
|
113
|
+
"--match-pattern",
|
|
114
|
+
action="append",
|
|
115
|
+
default=[],
|
|
116
|
+
help="Expected pattern in check output to support the hypothesis.",
|
|
117
|
+
)
|
|
118
|
+
verify_parser.add_argument(
|
|
119
|
+
"--mode",
|
|
120
|
+
choices=["dry_run", "trusted_local", "fake"],
|
|
121
|
+
default="dry_run",
|
|
122
|
+
help="Execution policy mode (default: dry_run).",
|
|
123
|
+
)
|
|
124
|
+
verify_parser.add_argument(
|
|
125
|
+
"--timeout",
|
|
126
|
+
type=float,
|
|
127
|
+
default=30.0,
|
|
128
|
+
help="Execution timeout ceiling in seconds (default: 30.0).",
|
|
129
|
+
)
|
|
130
|
+
verify_parser.add_argument(
|
|
131
|
+
"--untrusted",
|
|
132
|
+
action="store_true",
|
|
133
|
+
help="Flag that the code under test is from an untrusted source.",
|
|
134
|
+
)
|
|
135
|
+
verify_parser.add_argument(
|
|
136
|
+
"--format",
|
|
137
|
+
choices=["summary", "json"],
|
|
138
|
+
default="summary",
|
|
139
|
+
help="Output format (summary or json).",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return parser
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
146
|
+
parser = build_parser()
|
|
147
|
+
args = parser.parse_args(argv if argv is not None else sys.argv[1:])
|
|
148
|
+
|
|
149
|
+
if not args.subcommand:
|
|
150
|
+
parser.print_help()
|
|
151
|
+
sys.exit(0)
|
|
152
|
+
|
|
153
|
+
if args.subcommand == "verify":
|
|
154
|
+
# Load or construct VerificationInput
|
|
155
|
+
if args.input:
|
|
156
|
+
input_path = args.input.resolve()
|
|
157
|
+
try:
|
|
158
|
+
if not input_path.is_file():
|
|
159
|
+
raise FileNotFoundError(f"File not found or not a regular file: {args.input}")
|
|
160
|
+
raw_json = input_path.read_text(encoding="utf-8")
|
|
161
|
+
verification_input = VerificationInput.model_validate_json(raw_json)
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
print(f"Error reading input JSON: {exc}", file=sys.stderr)
|
|
164
|
+
sys.exit(1)
|
|
165
|
+
else:
|
|
166
|
+
if not args.target or not args.claim:
|
|
167
|
+
print(
|
|
168
|
+
"Error: Either --input <file> or both --target and --claim must be specified.",
|
|
169
|
+
file=sys.stderr,
|
|
170
|
+
)
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
|
|
173
|
+
hyp = Hypothesis(
|
|
174
|
+
id="hyp-cli",
|
|
175
|
+
claim=args.claim,
|
|
176
|
+
expected_observation=ExpectedObservation(
|
|
177
|
+
expected_outcome="fail",
|
|
178
|
+
expected_exit_codes=[1],
|
|
179
|
+
match_patterns=args.match_pattern,
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
spec = CheckSpec(
|
|
183
|
+
check_id="chk-cli",
|
|
184
|
+
check_type=CheckType.PYTEST,
|
|
185
|
+
target_path=args.target,
|
|
186
|
+
)
|
|
187
|
+
verification_input = VerificationInput(
|
|
188
|
+
repo_path=args.repo,
|
|
189
|
+
hypothesis=hyp,
|
|
190
|
+
check_spec=spec,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
policy = ExecutionPolicy(
|
|
194
|
+
mode=ExecutionMode(args.mode),
|
|
195
|
+
timeout_seconds=args.timeout,
|
|
196
|
+
untrusted_code=args.untrusted,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
engine = VerificationEngine()
|
|
200
|
+
result = engine.verify(verification_input, policy=policy)
|
|
201
|
+
|
|
202
|
+
if args.format == "json":
|
|
203
|
+
print(result.to_json(indent=2))
|
|
204
|
+
else:
|
|
205
|
+
print(_format_summary(result))
|
|
206
|
+
|
|
207
|
+
sys.exit(VERDICT_EXIT_CODES.get(result.verdict, 1))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
main()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Contracts module exporting all public data contracts for VerifyCraft."""
|
|
2
|
+
|
|
3
|
+
from verifycraft.contracts.models import (
|
|
4
|
+
CheckSpec,
|
|
5
|
+
CheckType,
|
|
6
|
+
Evidence,
|
|
7
|
+
ExecutionMode,
|
|
8
|
+
ExecutionPolicy,
|
|
9
|
+
ExpectedObservation,
|
|
10
|
+
Finding,
|
|
11
|
+
Hypothesis,
|
|
12
|
+
ResourceUsage,
|
|
13
|
+
Verdict,
|
|
14
|
+
VerificationInput,
|
|
15
|
+
VerificationResult,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CheckSpec",
|
|
20
|
+
"CheckType",
|
|
21
|
+
"Evidence",
|
|
22
|
+
"ExecutionMode",
|
|
23
|
+
"ExecutionPolicy",
|
|
24
|
+
"ExpectedObservation",
|
|
25
|
+
"Finding",
|
|
26
|
+
"Hypothesis",
|
|
27
|
+
"ResourceUsage",
|
|
28
|
+
"Verdict",
|
|
29
|
+
"VerificationInput",
|
|
30
|
+
"VerificationResult",
|
|
31
|
+
]
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Public typed contracts for VerifyCraft.
|
|
2
|
+
|
|
3
|
+
All models are versioned, JSON-serializable Pydantic v2 models.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
11
|
+
from typing import Any, Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Verdict(StrEnum):
|
|
17
|
+
"""Evidence status of a hypothesis.
|
|
18
|
+
|
|
19
|
+
Verdicts represent verifiable evidence outcomes, not probabilistic model confidence.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
SUPPORTED = "supported"
|
|
23
|
+
REFUTED = "refuted"
|
|
24
|
+
INCONCLUSIVE = "inconclusive"
|
|
25
|
+
BLOCKED = "blocked"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ExecutionMode(StrEnum):
|
|
29
|
+
"""Permitted execution mode for verification."""
|
|
30
|
+
|
|
31
|
+
DRY_RUN = "dry_run"
|
|
32
|
+
FAKE = "fake"
|
|
33
|
+
TRUSTED_LOCAL = "trusted_local"
|
|
34
|
+
ISOLATED_CONTAINER = "isolated_container"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CheckType(StrEnum):
|
|
38
|
+
"""Approved verification check types."""
|
|
39
|
+
|
|
40
|
+
PYTEST = "pytest"
|
|
41
|
+
STATIC = "static"
|
|
42
|
+
FAKE = "fake"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ExpectedObservation(BaseModel):
|
|
46
|
+
"""Explicit expected observation used to evaluate whether evidence supports or refutes a claim.
|
|
47
|
+
|
|
48
|
+
A command exiting successfully is NOT automatically evidence supporting a hypothesis.
|
|
49
|
+
Verdicts must be derived from matching actual observations against these explicit expectations.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
model_config = ConfigDict(frozen=True)
|
|
53
|
+
|
|
54
|
+
expected_exit_codes: list[int] = Field(
|
|
55
|
+
default_factory=lambda: [1],
|
|
56
|
+
description="Exit codes indicating expected behavior (e.g. [1] for expected test failure).",
|
|
57
|
+
)
|
|
58
|
+
match_patterns: list[str] = Field(
|
|
59
|
+
default_factory=list,
|
|
60
|
+
description="Patterns that must appear in stdout/stderr to support the claim.",
|
|
61
|
+
)
|
|
62
|
+
contradict_patterns: list[str] = Field(
|
|
63
|
+
default_factory=list,
|
|
64
|
+
description="Substrings or regex patterns that, if found, refute the claim.",
|
|
65
|
+
)
|
|
66
|
+
expected_outcome: Literal["fail", "pass", "error"] = Field(
|
|
67
|
+
default="fail",
|
|
68
|
+
description="Expected check outcome (e.g. 'fail' when verifying a regression).",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Hypothesis(BaseModel):
|
|
73
|
+
"""An explicit, testable claim regarding a code change or regression."""
|
|
74
|
+
|
|
75
|
+
model_config = ConfigDict(frozen=True)
|
|
76
|
+
|
|
77
|
+
id: str = Field(description="Unique identifier for this hypothesis.")
|
|
78
|
+
claim: str = Field(description="Human-readable description of the claim.")
|
|
79
|
+
failure_mechanism: str | None = Field(
|
|
80
|
+
default=None,
|
|
81
|
+
description="Proposed mechanism of failure (e.g. 'ZeroDivisionError on empty cart').",
|
|
82
|
+
)
|
|
83
|
+
affected_locations: list[str] = Field(
|
|
84
|
+
default_factory=list,
|
|
85
|
+
description="Source code or test locations implicated (e.g. ['src/pricing.py:42']).",
|
|
86
|
+
)
|
|
87
|
+
expected_observation: ExpectedObservation = Field(
|
|
88
|
+
default_factory=ExpectedObservation,
|
|
89
|
+
description="Explicit observation required to confirm or refute this hypothesis.",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CheckSpec(BaseModel):
|
|
94
|
+
"""Approved verification check specification.
|
|
95
|
+
|
|
96
|
+
Arbitrary shell strings are strictly prohibited. Every check must be an approved type
|
|
97
|
+
with bounded, validated arguments.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
model_config = ConfigDict(frozen=True)
|
|
101
|
+
|
|
102
|
+
check_id: str = Field(description="Unique identifier for this check specification.")
|
|
103
|
+
check_type: CheckType = Field(
|
|
104
|
+
default=CheckType.PYTEST,
|
|
105
|
+
description="Approved verification runner type.",
|
|
106
|
+
)
|
|
107
|
+
target_path: str = Field(
|
|
108
|
+
description="Relative path to test or target file (e.g. 'tests/test_regression.py').",
|
|
109
|
+
)
|
|
110
|
+
args: list[str] = Field(
|
|
111
|
+
default_factory=list,
|
|
112
|
+
description="Allowlisted arguments passed to runner (e.g. ['-k', 'test_negative']).",
|
|
113
|
+
)
|
|
114
|
+
env_overrides: dict[str, str] = Field(
|
|
115
|
+
default_factory=dict,
|
|
116
|
+
description="Approved environment variable overrides.",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@field_validator("target_path")
|
|
120
|
+
@classmethod
|
|
121
|
+
def validate_target_path(cls, path_str: str) -> str:
|
|
122
|
+
"""Ensure target_path is relative and does not escape workspace via traversal."""
|
|
123
|
+
clean = path_str.strip()
|
|
124
|
+
if not clean:
|
|
125
|
+
raise ValueError("target_path must not be empty")
|
|
126
|
+
clean_file = clean.split("::")[0].strip()
|
|
127
|
+
is_abs = (
|
|
128
|
+
Path(clean_file).is_absolute()
|
|
129
|
+
or PureWindowsPath(clean_file).is_absolute()
|
|
130
|
+
or PurePosixPath(clean_file).is_absolute()
|
|
131
|
+
or clean_file.startswith(("/", "\\"))
|
|
132
|
+
or bool(PureWindowsPath(clean_file).drive)
|
|
133
|
+
)
|
|
134
|
+
if is_abs:
|
|
135
|
+
raise ValueError(f"target_path must be relative, not absolute: {path_str}")
|
|
136
|
+
has_traversal = (
|
|
137
|
+
".." in Path(clean_file).parts
|
|
138
|
+
or ".." in PureWindowsPath(clean_file).parts
|
|
139
|
+
or ".." in PurePosixPath(clean_file).parts
|
|
140
|
+
)
|
|
141
|
+
if has_traversal:
|
|
142
|
+
raise ValueError(f"target_path must not contain '..' path traversal: {path_str}")
|
|
143
|
+
return clean
|
|
144
|
+
|
|
145
|
+
@field_validator("args")
|
|
146
|
+
@classmethod
|
|
147
|
+
def validate_args_security(cls, args: list[str]) -> list[str]:
|
|
148
|
+
"""Prohibit dangerous flags that might escape check execution safety."""
|
|
149
|
+
forbidden_substrings = [
|
|
150
|
+
";",
|
|
151
|
+
"&&",
|
|
152
|
+
"||",
|
|
153
|
+
"|",
|
|
154
|
+
"`",
|
|
155
|
+
"$",
|
|
156
|
+
"\n",
|
|
157
|
+
"\r",
|
|
158
|
+
"--override-ini",
|
|
159
|
+
"-o",
|
|
160
|
+
"--import-mode",
|
|
161
|
+
]
|
|
162
|
+
for arg in args:
|
|
163
|
+
for forbidden in forbidden_substrings:
|
|
164
|
+
if forbidden in arg:
|
|
165
|
+
raise ValueError(f"Prohibited pattern '{forbidden}' in check argument: {arg}")
|
|
166
|
+
return args
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class ExecutionPolicy(BaseModel):
|
|
170
|
+
"""Policy governing verification execution boundaries, budgets, and security."""
|
|
171
|
+
|
|
172
|
+
model_config = ConfigDict(frozen=True)
|
|
173
|
+
|
|
174
|
+
mode: ExecutionMode = Field(
|
|
175
|
+
default=ExecutionMode.DRY_RUN,
|
|
176
|
+
description="Execution mode. Defaults to dry_run (non-executing) for safety.",
|
|
177
|
+
)
|
|
178
|
+
timeout_seconds: float = Field(
|
|
179
|
+
default=30.0,
|
|
180
|
+
ge=0.1,
|
|
181
|
+
le=300.0,
|
|
182
|
+
description="Maximum execution wall-clock time in seconds.",
|
|
183
|
+
)
|
|
184
|
+
max_output_bytes: int = Field(
|
|
185
|
+
default=64 * 1024,
|
|
186
|
+
ge=1024,
|
|
187
|
+
le=5 * 1024 * 1024,
|
|
188
|
+
description="Maximum captured output bytes before truncation.",
|
|
189
|
+
)
|
|
190
|
+
untrusted_code: bool = Field(
|
|
191
|
+
default=False,
|
|
192
|
+
description="Whether code originates from an untrusted source (e.g. public PR).",
|
|
193
|
+
)
|
|
194
|
+
allow_network: bool = Field(
|
|
195
|
+
default=False,
|
|
196
|
+
description=(
|
|
197
|
+
"Whether network access is permitted. NOTE: In 'trusted_local' mode, "
|
|
198
|
+
"this is an advisory constraint and does not enforce kernel-level network "
|
|
199
|
+
"isolation. Network isolation requires 'isolated_container' mode."
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
allowed_check_types: list[CheckType] = Field(
|
|
203
|
+
default_factory=lambda: [CheckType.PYTEST, CheckType.FAKE],
|
|
204
|
+
description="Allowlist of approved check types.",
|
|
205
|
+
)
|
|
206
|
+
env_allowlist: list[str] = Field(
|
|
207
|
+
default_factory=lambda: [
|
|
208
|
+
"PATH",
|
|
209
|
+
"PYTHONPATH",
|
|
210
|
+
"PYTHONHOME",
|
|
211
|
+
"SYSTEMROOT",
|
|
212
|
+
"TMPDIR",
|
|
213
|
+
"TEMP",
|
|
214
|
+
"TMP",
|
|
215
|
+
"LANG",
|
|
216
|
+
"LC_ALL",
|
|
217
|
+
],
|
|
218
|
+
description="Environment variable names permitted to pass to the runner process.",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class ResourceUsage(BaseModel):
|
|
223
|
+
"""Execution resource accounting."""
|
|
224
|
+
|
|
225
|
+
model_config = ConfigDict(frozen=True)
|
|
226
|
+
|
|
227
|
+
wall_time_seconds: float = Field(default=0.0, ge=0.0)
|
|
228
|
+
memory_bytes: int | None = Field(default=None, ge=0)
|
|
229
|
+
attempts: int = Field(default=1, ge=0)
|
|
230
|
+
output_bytes: int = Field(default=0, ge=0)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class Evidence(BaseModel):
|
|
234
|
+
"""Observable, structured evidence collected from a check execution."""
|
|
235
|
+
|
|
236
|
+
model_config = ConfigDict(frozen=True)
|
|
237
|
+
|
|
238
|
+
evidence_id: str = Field(description="Unique evidence identifier.")
|
|
239
|
+
check_id: str = Field(description="ID of the CheckSpec that produced this evidence.")
|
|
240
|
+
evidence_type: str = Field(description="Evidence source type (e.g. 'pytest_execution').")
|
|
241
|
+
exit_code: int = Field(description="Process exit code.")
|
|
242
|
+
stdout: str = Field(default="", description="Captured standard output.")
|
|
243
|
+
stderr: str = Field(default="", description="Captured standard error.")
|
|
244
|
+
output_truncated: bool = Field(
|
|
245
|
+
default=False, description="Whether output was truncated due to size limits."
|
|
246
|
+
)
|
|
247
|
+
fingerprint: str = Field(
|
|
248
|
+
description="Repository revision and diff fingerprint at time of collection."
|
|
249
|
+
)
|
|
250
|
+
collected_at: datetime = Field(
|
|
251
|
+
default_factory=lambda: datetime.now(UTC),
|
|
252
|
+
description="Timestamp when evidence was captured.",
|
|
253
|
+
)
|
|
254
|
+
resource_usage: ResourceUsage = Field(
|
|
255
|
+
default_factory=ResourceUsage,
|
|
256
|
+
description="Resource usage during evidence collection.",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class Finding(BaseModel):
|
|
261
|
+
"""A claim evaluated against collected evidence."""
|
|
262
|
+
|
|
263
|
+
model_config = ConfigDict(frozen=True)
|
|
264
|
+
|
|
265
|
+
hypothesis_id: str = Field(description="ID of the hypothesis.")
|
|
266
|
+
verdict: Verdict = Field(
|
|
267
|
+
description="Evidence verdict: supported, refuted, inconclusive, or blocked."
|
|
268
|
+
)
|
|
269
|
+
reason: str = Field(description="Explanation of how the verdict was derived from evidence.")
|
|
270
|
+
evidence_ids: list[str] = Field(
|
|
271
|
+
default_factory=list,
|
|
272
|
+
description="IDs of evidence supporting or refuting this finding.",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class VerificationInput(BaseModel):
|
|
277
|
+
"""Input payload for a verification request."""
|
|
278
|
+
|
|
279
|
+
model_config = ConfigDict(frozen=True)
|
|
280
|
+
|
|
281
|
+
repo_path: str = Field(description="Path to repository root.")
|
|
282
|
+
revision: str = Field(default="HEAD", description="Repository revision or commit hash.")
|
|
283
|
+
diff: str | None = Field(
|
|
284
|
+
default=None, description="Unified diff representing changes under review."
|
|
285
|
+
)
|
|
286
|
+
hypothesis: Hypothesis = Field(description="The regression hypothesis to verify.")
|
|
287
|
+
check_spec: CheckSpec = Field(description="Approved check to execute.")
|
|
288
|
+
target_files: list[str] = Field(
|
|
289
|
+
default_factory=list,
|
|
290
|
+
description="Specific files targeted by this verification.",
|
|
291
|
+
)
|
|
292
|
+
metadata: dict[str, Any] = Field(
|
|
293
|
+
default_factory=dict,
|
|
294
|
+
description="Arbitrary caller-supplied metadata.",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
@field_validator("repo_path")
|
|
298
|
+
@classmethod
|
|
299
|
+
def validate_repo_path(cls, path_str: str) -> str:
|
|
300
|
+
"""Ensure repo_path is not empty."""
|
|
301
|
+
if not path_str or not path_str.strip():
|
|
302
|
+
raise ValueError("repo_path must not be empty")
|
|
303
|
+
return path_str
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class VerificationResult(BaseModel):
|
|
307
|
+
"""Top-level result produced by the verification engine."""
|
|
308
|
+
|
|
309
|
+
model_config = ConfigDict(frozen=True)
|
|
310
|
+
|
|
311
|
+
schema_version: str = Field(default="verifycraft.v0", description="Contract schema version.")
|
|
312
|
+
verdict: Verdict = Field(description="Overall verification verdict.")
|
|
313
|
+
findings: list[Finding] = Field(
|
|
314
|
+
default_factory=list, description="Findings evaluated by the engine."
|
|
315
|
+
)
|
|
316
|
+
evidence: list[Evidence] = Field(
|
|
317
|
+
default_factory=list, description="Evidence gathered during verification."
|
|
318
|
+
)
|
|
319
|
+
resource_usage: ResourceUsage = Field(
|
|
320
|
+
default_factory=ResourceUsage,
|
|
321
|
+
description="Aggregate resource usage.",
|
|
322
|
+
)
|
|
323
|
+
blocked_reason: str | None = Field(
|
|
324
|
+
default=None,
|
|
325
|
+
description="Reason verification was blocked, if applicable.",
|
|
326
|
+
)
|
|
327
|
+
inconclusive_reason: str | None = Field(
|
|
328
|
+
default=None,
|
|
329
|
+
description="Reason verification was inconclusive, if applicable.",
|
|
330
|
+
)
|
|
331
|
+
fingerprint: str = Field(description="Change fingerprint for which this result is valid.")
|
|
332
|
+
created_at: datetime = Field(
|
|
333
|
+
default_factory=lambda: datetime.now(UTC),
|
|
334
|
+
description="Timestamp when result was produced.",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def to_json(self, indent: int | None = 2) -> str:
|
|
338
|
+
"""Serialize result to a versioned JSON string."""
|
|
339
|
+
return self.model_dump_json(indent=indent)
|
|
340
|
+
|
|
341
|
+
@classmethod
|
|
342
|
+
def from_json(cls, json_str: str) -> VerificationResult:
|
|
343
|
+
"""Deserialize result from a JSON string."""
|
|
344
|
+
return cls.model_validate_json(json_str)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Core verification components for VerifyCraft."""
|
|
2
|
+
|
|
3
|
+
from verifycraft.core.evaluator import evaluate_hypothesis
|
|
4
|
+
from verifycraft.core.fingerprint import (
|
|
5
|
+
FingerprintError,
|
|
6
|
+
compute_fingerprint,
|
|
7
|
+
is_evidence_stale,
|
|
8
|
+
)
|
|
9
|
+
from verifycraft.core.policy import (
|
|
10
|
+
PolicyViolationError,
|
|
11
|
+
sanitize_environment,
|
|
12
|
+
validate_execution_authorization,
|
|
13
|
+
)
|
|
14
|
+
from verifycraft.core.runner import (
|
|
15
|
+
CheckRunner,
|
|
16
|
+
FakeRunner,
|
|
17
|
+
TrustedPytestRunner,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"CheckRunner",
|
|
22
|
+
"FakeRunner",
|
|
23
|
+
"FingerprintError",
|
|
24
|
+
"PolicyViolationError",
|
|
25
|
+
"TrustedPytestRunner",
|
|
26
|
+
"compute_fingerprint",
|
|
27
|
+
"evaluate_hypothesis",
|
|
28
|
+
"is_evidence_stale",
|
|
29
|
+
"sanitize_environment",
|
|
30
|
+
"validate_execution_authorization",
|
|
31
|
+
]
|