cernora 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.
- cernora/__init__.py +94 -0
- cernora/__main__.py +5 -0
- cernora/adapter.py +31 -0
- cernora/cli/__init__.py +1 -0
- cernora/cli/main.py +121 -0
- cernora/cli/profiles.py +25 -0
- cernora/composition/__init__.py +1 -0
- cernora/composition/gating.py +126 -0
- cernora/composition/scoring.py +18 -0
- cernora/conformance.py +195 -0
- cernora/core/__init__.py +1 -0
- cernora/core/canonical.py +65 -0
- cernora/core/case.py +62 -0
- cernora/core/errors.py +13 -0
- cernora/core/evidence.py +124 -0
- cernora/core/evidence_bundle_v2.py +391 -0
- cernora/core/gate.py +72 -0
- cernora/core/identity.py +69 -0
- cernora/core/score.py +41 -0
- cernora/evaluation/__init__.py +1 -0
- cernora/evaluation/contracts.py +209 -0
- cernora/evaluation/imported_case.py +255 -0
- cernora/evaluation/package.py +370 -0
- cernora/examples/__init__.py +1 -0
- cernora/examples/coding_task/__init__.py +11 -0
- cernora/examples/coding_task/__main__.py +20 -0
- cernora/examples/coding_task/adapter.py +402 -0
- cernora/examples/coding_task/resources/candidates/backend-v1.json +1 -0
- cernora/examples/coding_task/resources/candidates/fail-closed-v1.json +1 -0
- cernora/examples/coding_task/resources/candidates/frontend-v1.json +1 -0
- cernora/examples/coding_task/workflow.py +115 -0
- cernora/examples/offline_workflow/__init__.py +13 -0
- cernora/examples/offline_workflow/__main__.py +21 -0
- cernora/examples/offline_workflow/adapter.py +390 -0
- cernora/examples/offline_workflow/resources/completed-export.json +1 -0
- cernora/examples/offline_workflow/workflow.py +106 -0
- cernora/ingestion/__init__.py +1 -0
- cernora/ingestion/contracts_v2.py +192 -0
- cernora/ingestion/errors.py +31 -0
- cernora/ingestion/package_v2.py +952 -0
- cernora/profile.py +56 -0
- cernora/profile_loader.py +98 -0
- cernora/profile_workspace.py +324 -0
- cernora/profiles/coding_task/__init__.py +318 -0
- cernora/profiles/coding_task/resources/checks/backend-v1.json +1 -0
- cernora/profiles/coding_task/resources/checks/fail-closed-v1.json +1 -0
- cernora/profiles/coding_task/resources/checks/frontend-v1.json +1 -0
- cernora/profiles/coding_task/resources/profile.json +44 -0
- cernora/profiles/offline_workflow/__init__.py +270 -0
- cernora/profiles/offline_workflow/resources/lookup-result.json +1 -0
- cernora/profiles/offline_workflow/resources/profile.json +38 -0
- cernora/py.typed +1 -0
- cernora/resources.py +27 -0
- cernora/schemas/__init__.py +1 -0
- cernora/schemas/case-profile-v1.schema.json +71 -0
- cernora/schemas/evidence-bundle-v2.schema.json +321 -0
- cernora/schemas/evidence-v1.schema.json +109 -0
- cernora/schemas/gate-decision-v1.schema.json +55 -0
- cernora/schemas/import-manifest-v2.schema.json +38 -0
- cernora/schemas/import-receipt-v2.schema.json +169 -0
- cernora/schemas/imported-evaluation-authority-v1.schema.json +218 -0
- cernora/schemas/imported-evaluation-manifest-v1.schema.json +55 -0
- cernora/schemas/imported-evaluation-receipt-v1.schema.json +469 -0
- cernora/schemas/score-v1.schema.json +37 -0
- cernora-0.1.0.dist-info/METADATA +299 -0
- cernora-0.1.0.dist-info/RECORD +69 -0
- cernora-0.1.0.dist-info/WHEEL +4 -0
- cernora-0.1.0.dist-info/entry_points.txt +2 -0
- cernora-0.1.0.dist-info/licenses/LICENSE +201 -0
cernora/__init__.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Cernora: evidence-bound evaluation for tool-using agents."""
|
|
2
|
+
|
|
3
|
+
from cernora.adapter import AdaptedBundle, Adapter, CompletedExport
|
|
4
|
+
from cernora.conformance import (
|
|
5
|
+
AdapterConformance,
|
|
6
|
+
ConformanceError,
|
|
7
|
+
ProfileConformance,
|
|
8
|
+
check_adapter_conformance,
|
|
9
|
+
check_profile_conformance,
|
|
10
|
+
)
|
|
11
|
+
from cernora.core.case import (
|
|
12
|
+
Case,
|
|
13
|
+
CaseInput,
|
|
14
|
+
CaseProfile,
|
|
15
|
+
FixtureReference,
|
|
16
|
+
GatePolicy,
|
|
17
|
+
ScorerPolicy,
|
|
18
|
+
)
|
|
19
|
+
from cernora.core.evidence import (
|
|
20
|
+
AnswerClaim,
|
|
21
|
+
Artifact,
|
|
22
|
+
Evidence,
|
|
23
|
+
EvidenceReference,
|
|
24
|
+
Failure,
|
|
25
|
+
ProcessResult,
|
|
26
|
+
StructuredAnswer,
|
|
27
|
+
ToolAction,
|
|
28
|
+
)
|
|
29
|
+
from cernora.core.evidence_bundle_v2 import EvidenceBundleV2
|
|
30
|
+
from cernora.core.gate import GateDecision
|
|
31
|
+
from cernora.core.identity import (
|
|
32
|
+
ComponentIdentity,
|
|
33
|
+
ExternalProducerIdentity,
|
|
34
|
+
component_identity,
|
|
35
|
+
external_producer_identity,
|
|
36
|
+
)
|
|
37
|
+
from cernora.core.score import Score, ScoreObservation
|
|
38
|
+
from cernora.evaluation.package import evaluate_imported_case, read_imported_evaluation
|
|
39
|
+
from cernora.ingestion.contracts_v2 import AuthorityBoundImportPackageV2
|
|
40
|
+
from cernora.ingestion.package_v2 import import_evidence_bundle_v2
|
|
41
|
+
from cernora.profile import Profile, ProfileAssessment, ProfileEvaluationContext
|
|
42
|
+
from cernora.profile_loader import ProfileLoadError, load_local_profile
|
|
43
|
+
from cernora.profile_workspace import ProfileInitResult, ProfileWorkspaceError, init_profile
|
|
44
|
+
from cernora.resources import PUBLIC_SCHEMAS, read_public_schema
|
|
45
|
+
|
|
46
|
+
__version__ = "0.1.0"
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"PUBLIC_SCHEMAS",
|
|
50
|
+
"AdaptedBundle",
|
|
51
|
+
"Adapter",
|
|
52
|
+
"AdapterConformance",
|
|
53
|
+
"AnswerClaim",
|
|
54
|
+
"Artifact",
|
|
55
|
+
"AuthorityBoundImportPackageV2",
|
|
56
|
+
"Case",
|
|
57
|
+
"CaseInput",
|
|
58
|
+
"CaseProfile",
|
|
59
|
+
"CompletedExport",
|
|
60
|
+
"ComponentIdentity",
|
|
61
|
+
"ConformanceError",
|
|
62
|
+
"Evidence",
|
|
63
|
+
"EvidenceBundleV2",
|
|
64
|
+
"EvidenceReference",
|
|
65
|
+
"ExternalProducerIdentity",
|
|
66
|
+
"Failure",
|
|
67
|
+
"FixtureReference",
|
|
68
|
+
"GateDecision",
|
|
69
|
+
"GatePolicy",
|
|
70
|
+
"ProcessResult",
|
|
71
|
+
"Profile",
|
|
72
|
+
"ProfileAssessment",
|
|
73
|
+
"ProfileConformance",
|
|
74
|
+
"ProfileEvaluationContext",
|
|
75
|
+
"ProfileInitResult",
|
|
76
|
+
"ProfileLoadError",
|
|
77
|
+
"ProfileWorkspaceError",
|
|
78
|
+
"Score",
|
|
79
|
+
"ScoreObservation",
|
|
80
|
+
"ScorerPolicy",
|
|
81
|
+
"StructuredAnswer",
|
|
82
|
+
"ToolAction",
|
|
83
|
+
"__version__",
|
|
84
|
+
"check_adapter_conformance",
|
|
85
|
+
"check_profile_conformance",
|
|
86
|
+
"component_identity",
|
|
87
|
+
"evaluate_imported_case",
|
|
88
|
+
"external_producer_identity",
|
|
89
|
+
"import_evidence_bundle_v2",
|
|
90
|
+
"init_profile",
|
|
91
|
+
"load_local_profile",
|
|
92
|
+
"read_imported_evaluation",
|
|
93
|
+
"read_public_schema",
|
|
94
|
+
]
|
cernora/__main__.py
ADDED
cernora/adapter.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Preview completed-export Adapter interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class CompletedExport:
|
|
12
|
+
"""One already terminal ordinary-file export supplied by a third party."""
|
|
13
|
+
|
|
14
|
+
root: Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class AdaptedBundle:
|
|
19
|
+
"""Paths to one normalized EvidenceBundle v2 and its sibling artifacts."""
|
|
20
|
+
|
|
21
|
+
bundle_path: Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@runtime_checkable
|
|
25
|
+
class Adapter(Protocol):
|
|
26
|
+
"""Normalize completed local files without execution, credentials or network."""
|
|
27
|
+
|
|
28
|
+
def adapt(self, completed_export: CompletedExport, output: Path) -> AdaptedBundle: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ["AdaptedBundle", "Adapter", "CompletedExport"]
|
cernora/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Cernora command-line interface."""
|
cernora/cli/main.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Public completed-evidence CLI with stable fail-closed exit classes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, NoReturn
|
|
9
|
+
|
|
10
|
+
from cernora import __version__
|
|
11
|
+
from cernora.cli.profiles import BUILTIN_PROFILE_SELECTORS, load_builtin_profile
|
|
12
|
+
from cernora.conformance import ConformanceError, check_profile_conformance
|
|
13
|
+
from cernora.core.canonical import canonical_json
|
|
14
|
+
from cernora.core.errors import ContractError
|
|
15
|
+
from cernora.evaluation.package import evaluate_imported_case
|
|
16
|
+
from cernora.ingestion.errors import IngestionConfigurationError, IngestionIntegrityError
|
|
17
|
+
from cernora.ingestion.package_v2 import import_evidence_bundle_v2
|
|
18
|
+
from cernora.profile import Profile
|
|
19
|
+
from cernora.profile_loader import ProfileLoadError, load_local_profile
|
|
20
|
+
from cernora.profile_workspace import ProfileWorkspaceError, init_profile
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UsageParser(argparse.ArgumentParser):
|
|
24
|
+
def error(self, message: str) -> NoReturn:
|
|
25
|
+
self.print_usage(sys.stderr)
|
|
26
|
+
print(f"cernora: error: {message}", file=sys.stderr)
|
|
27
|
+
raise SystemExit(2)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parser() -> argparse.ArgumentParser:
|
|
31
|
+
root = UsageParser(prog="cernora")
|
|
32
|
+
root.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
33
|
+
commands = root.add_subparsers(dest="command", required=True)
|
|
34
|
+
|
|
35
|
+
profile = commands.add_parser("profile")
|
|
36
|
+
profile_commands = profile.add_subparsers(dest="profile_command", required=True)
|
|
37
|
+
init = profile_commands.add_parser("init")
|
|
38
|
+
init.add_argument("name")
|
|
39
|
+
init.add_argument("--output", type=Path)
|
|
40
|
+
validate = profile_commands.add_parser("validate")
|
|
41
|
+
_add_profile_selector(validate)
|
|
42
|
+
|
|
43
|
+
evidence = commands.add_parser("evidence")
|
|
44
|
+
evidence_commands = evidence.add_subparsers(dest="evidence_command", required=True)
|
|
45
|
+
evidence_import = evidence_commands.add_parser("import")
|
|
46
|
+
_add_profile_selector(evidence_import)
|
|
47
|
+
evidence_import.add_argument("--bundle", type=Path, required=True)
|
|
48
|
+
evidence_import.add_argument("--output", type=Path, required=True)
|
|
49
|
+
evidence_evaluate = evidence_commands.add_parser("evaluate")
|
|
50
|
+
_add_profile_selector(evidence_evaluate)
|
|
51
|
+
evidence_evaluate.add_argument("--import-root", type=Path, required=True)
|
|
52
|
+
evidence_evaluate.add_argument("--output", type=Path, required=True)
|
|
53
|
+
return root
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _add_profile_selector(command: argparse.ArgumentParser) -> None:
|
|
57
|
+
selection = command.add_mutually_exclusive_group(required=True)
|
|
58
|
+
selection.add_argument("--profile", choices=BUILTIN_PROFILE_SELECTORS)
|
|
59
|
+
selection.add_argument("--profile-path", type=Path)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _load_selected_profile(args: argparse.Namespace) -> Profile:
|
|
63
|
+
if args.profile_path is not None:
|
|
64
|
+
return load_local_profile(args.profile_path)
|
|
65
|
+
return load_builtin_profile(args.profile)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _emit(value: Any) -> None:
|
|
69
|
+
sys.stdout.buffer.write(canonical_json(value) + b"\n")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv: list[str] | None = None) -> int:
|
|
73
|
+
args = parser().parse_args(argv)
|
|
74
|
+
result: Any
|
|
75
|
+
try:
|
|
76
|
+
if args.command == "profile" and args.profile_command == "init":
|
|
77
|
+
result = init_profile(args.name, output=args.output)
|
|
78
|
+
code = 0
|
|
79
|
+
else:
|
|
80
|
+
profile = _load_selected_profile(args)
|
|
81
|
+
if args.command == "profile" and args.profile_command == "validate":
|
|
82
|
+
check_profile_conformance(profile)
|
|
83
|
+
result = profile.authority
|
|
84
|
+
code = 0
|
|
85
|
+
elif args.command == "evidence" and args.evidence_command == "import":
|
|
86
|
+
result = import_evidence_bundle_v2(
|
|
87
|
+
profile=profile,
|
|
88
|
+
bundle_path=args.bundle,
|
|
89
|
+
output=args.output,
|
|
90
|
+
)
|
|
91
|
+
code = 0
|
|
92
|
+
elif args.command == "evidence":
|
|
93
|
+
result = evaluate_imported_case(
|
|
94
|
+
profile=profile,
|
|
95
|
+
import_root=args.import_root,
|
|
96
|
+
output=args.output,
|
|
97
|
+
)
|
|
98
|
+
code = {"pass": 0, "fail": 1, "inconclusive": 3}[result.case_outcome]
|
|
99
|
+
except (
|
|
100
|
+
ConformanceError,
|
|
101
|
+
IngestionConfigurationError,
|
|
102
|
+
ProfileLoadError,
|
|
103
|
+
ProfileWorkspaceError,
|
|
104
|
+
) as exc:
|
|
105
|
+
print(str(exc), file=sys.stderr)
|
|
106
|
+
return 2
|
|
107
|
+
except IngestionIntegrityError as exc:
|
|
108
|
+
print(str(exc), file=sys.stderr)
|
|
109
|
+
return 3
|
|
110
|
+
except (ContractError, OSError, ValueError, KeyError) as exc:
|
|
111
|
+
print(str(exc), file=sys.stderr)
|
|
112
|
+
return 2 if args.command == "profile" else 3
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
print(f"evaluation failed closed: {type(exc).__name__}", file=sys.stderr)
|
|
115
|
+
return 3
|
|
116
|
+
_emit(result)
|
|
117
|
+
return code
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
raise SystemExit(main())
|
cernora/cli/profiles.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Explicit CLI wiring for wheel-bundled public Profiles."""
|
|
2
|
+
|
|
3
|
+
from cernora.profile import Profile
|
|
4
|
+
|
|
5
|
+
BUILTIN_PROFILE_SELECTORS = ("builtin:coding-task", "builtin:offline-workflow")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_builtin_profile(selector: str) -> Profile:
|
|
9
|
+
"""Instantiate one explicitly selected built-in Profile.
|
|
10
|
+
|
|
11
|
+
This closed CLI switch is not an SDK registry or discovery mechanism.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
if selector == "builtin:offline-workflow":
|
|
15
|
+
from cernora.profiles.offline_workflow import OfflineWorkflowProfile
|
|
16
|
+
|
|
17
|
+
return OfflineWorkflowProfile()
|
|
18
|
+
if selector == "builtin:coding-task":
|
|
19
|
+
from cernora.profiles.coding_task import CodingTaskProfile
|
|
20
|
+
|
|
21
|
+
return CodingTaskProfile()
|
|
22
|
+
raise ValueError(f"unknown built-in Profile selector: {selector}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ["BUILTIN_PROFILE_SELECTORS", "load_builtin_profile"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Evaluator-owned Score and Gate composition."""
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Deterministic fail-closed GateDecision composition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Sequence
|
|
6
|
+
|
|
7
|
+
from cernora.composition.scoring import index_scores
|
|
8
|
+
from cernora.core.evidence import (
|
|
9
|
+
EvidenceReference,
|
|
10
|
+
Failure,
|
|
11
|
+
evidence_reference_sort_key,
|
|
12
|
+
)
|
|
13
|
+
from cernora.core.gate import GateDecision
|
|
14
|
+
from cernora.core.identity import component_identity
|
|
15
|
+
from cernora.core.score import Score
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compose_gate(
|
|
19
|
+
*,
|
|
20
|
+
decision_id: str,
|
|
21
|
+
policy_version: str,
|
|
22
|
+
required_score_ids: Sequence[str],
|
|
23
|
+
required_observations: Sequence[str],
|
|
24
|
+
scores: Iterable[Score],
|
|
25
|
+
) -> GateDecision:
|
|
26
|
+
"""Compose a decision without converting invalid/missing observations into pass."""
|
|
27
|
+
|
|
28
|
+
indexed = index_scores(scores)
|
|
29
|
+
blocking: list[str] = []
|
|
30
|
+
inconclusive: list[str] = []
|
|
31
|
+
evidence: dict[tuple[str, str, str | None], EvidenceReference] = {}
|
|
32
|
+
|
|
33
|
+
for score_id in required_score_ids:
|
|
34
|
+
score = indexed.get(score_id)
|
|
35
|
+
if score is None:
|
|
36
|
+
inconclusive.append(f"missing required score: {score_id}")
|
|
37
|
+
continue
|
|
38
|
+
observations = {item.observation_id: item for item in score.observations}
|
|
39
|
+
for observation in score.observations:
|
|
40
|
+
for reference in observation.evidence_references:
|
|
41
|
+
evidence[(reference.evidence_id, reference.locator, reference.sha256)] = reference
|
|
42
|
+
for observation_id in required_observations:
|
|
43
|
+
required = observations.get(observation_id)
|
|
44
|
+
if required is None:
|
|
45
|
+
inconclusive.append(f"missing required observation {score_id}/{observation_id}")
|
|
46
|
+
elif required.applicability == "invalid":
|
|
47
|
+
inconclusive.append(
|
|
48
|
+
f"invalid observation {score_id}/{required.observation_id}: {required.reason}"
|
|
49
|
+
)
|
|
50
|
+
elif required.applicability == "not_applicable":
|
|
51
|
+
inconclusive.append(
|
|
52
|
+
f"required observation not applicable {score_id}/{required.observation_id}: "
|
|
53
|
+
f"{required.reason}"
|
|
54
|
+
)
|
|
55
|
+
elif required.applicability == "observed" and required.value is not True:
|
|
56
|
+
blocking.append(f"failed observation {score_id}/{required.observation_id}")
|
|
57
|
+
|
|
58
|
+
present_score_ids = tuple(score_id for score_id in required_score_ids if score_id in indexed)
|
|
59
|
+
references = tuple(sorted(evidence.values(), key=evidence_reference_sort_key))
|
|
60
|
+
scorer_identities = tuple(
|
|
61
|
+
component_identity("scorer", indexed[score_id].scorer_version)
|
|
62
|
+
for score_id in present_score_ids
|
|
63
|
+
)
|
|
64
|
+
input_digests = tuple(
|
|
65
|
+
sorted({reference.sha256 for reference in references if reference.sha256 is not None})
|
|
66
|
+
)
|
|
67
|
+
if inconclusive:
|
|
68
|
+
message = "; ".join(inconclusive)
|
|
69
|
+
return GateDecision(
|
|
70
|
+
schema_version="agent.evaluator.gate-decision/v1",
|
|
71
|
+
decision_id=decision_id,
|
|
72
|
+
decision="inconclusive",
|
|
73
|
+
policy_version=policy_version,
|
|
74
|
+
policy_identity=component_identity("gate_policy", policy_version),
|
|
75
|
+
blocking_reasons=tuple(inconclusive),
|
|
76
|
+
score_ids=present_score_ids,
|
|
77
|
+
evidence_references=references,
|
|
78
|
+
infrastructure_failure="required_score_invalid_or_missing",
|
|
79
|
+
eligible=False,
|
|
80
|
+
failure=Failure(
|
|
81
|
+
domain="scorer",
|
|
82
|
+
code="required_score_invalid_or_missing",
|
|
83
|
+
message=message,
|
|
84
|
+
evidence_references=references,
|
|
85
|
+
),
|
|
86
|
+
scorer_identities=scorer_identities,
|
|
87
|
+
input_digests=input_digests,
|
|
88
|
+
harness_contribution="blocks_harness",
|
|
89
|
+
)
|
|
90
|
+
if blocking:
|
|
91
|
+
message = "; ".join(blocking)
|
|
92
|
+
return GateDecision(
|
|
93
|
+
schema_version="agent.evaluator.gate-decision/v1",
|
|
94
|
+
decision_id=decision_id,
|
|
95
|
+
decision="fail",
|
|
96
|
+
policy_version=policy_version,
|
|
97
|
+
policy_identity=component_identity("gate_policy", policy_version),
|
|
98
|
+
blocking_reasons=tuple(blocking),
|
|
99
|
+
score_ids=present_score_ids,
|
|
100
|
+
evidence_references=references,
|
|
101
|
+
eligible=True,
|
|
102
|
+
failure=Failure(
|
|
103
|
+
domain="agent",
|
|
104
|
+
code="required_observation_failed",
|
|
105
|
+
message=message,
|
|
106
|
+
evidence_references=references,
|
|
107
|
+
),
|
|
108
|
+
scorer_identities=scorer_identities,
|
|
109
|
+
input_digests=input_digests,
|
|
110
|
+
harness_contribution="eligible_evaluation",
|
|
111
|
+
)
|
|
112
|
+
return GateDecision(
|
|
113
|
+
schema_version="agent.evaluator.gate-decision/v1",
|
|
114
|
+
decision_id=decision_id,
|
|
115
|
+
decision="pass",
|
|
116
|
+
policy_version=policy_version,
|
|
117
|
+
policy_identity=component_identity("gate_policy", policy_version),
|
|
118
|
+
blocking_reasons=(),
|
|
119
|
+
score_ids=present_score_ids,
|
|
120
|
+
evidence_references=references,
|
|
121
|
+
eligible=True,
|
|
122
|
+
failure=None,
|
|
123
|
+
scorer_identities=scorer_identities,
|
|
124
|
+
input_digests=input_digests,
|
|
125
|
+
harness_contribution="eligible_evaluation",
|
|
126
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Generic score helpers shared by profile adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
from cernora.core.score import Score
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def index_scores(scores: Iterable[Score]) -> dict[str, Score]:
|
|
11
|
+
"""Index scores by ID and reject ambiguous duplicate identities."""
|
|
12
|
+
|
|
13
|
+
indexed: dict[str, Score] = {}
|
|
14
|
+
for score in scores:
|
|
15
|
+
if score.score_id in indexed:
|
|
16
|
+
raise ValueError(f"duplicate score ID: {score.score_id}")
|
|
17
|
+
indexed[score.score_id] = score
|
|
18
|
+
return indexed
|
cernora/conformance.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Preview conformance helpers for public Profile and Adapter authors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from cernora.adapter import AdaptedBundle, Adapter, CompletedExport
|
|
11
|
+
from cernora.core.canonical import canonical_json
|
|
12
|
+
from cernora.core.case import CaseProfile
|
|
13
|
+
from cernora.core.evidence_bundle_v2 import (
|
|
14
|
+
decode_evidence_bundle_v2,
|
|
15
|
+
verify_artifact_payloads_v2,
|
|
16
|
+
)
|
|
17
|
+
from cernora.profile import Profile
|
|
18
|
+
|
|
19
|
+
_MAX_CONFORMANCE_FILE_BYTES = 16_000_000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConformanceError(ValueError):
|
|
23
|
+
"""A public Profile or Adapter does not satisfy the Preview contract."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ProfileConformance:
|
|
28
|
+
"""Validated public identity summary for one Profile."""
|
|
29
|
+
|
|
30
|
+
profile_id: str
|
|
31
|
+
profile_version: str
|
|
32
|
+
projection_version: str
|
|
33
|
+
case_ids: tuple[str, ...]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class AdapterConformance:
|
|
38
|
+
"""Validated identity summary for one canonical Adapter output."""
|
|
39
|
+
|
|
40
|
+
bundle_id: str
|
|
41
|
+
bundle_sha256: str
|
|
42
|
+
artifact_ids: tuple[str, ...]
|
|
43
|
+
bundle_path: Path
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_profile_conformance(candidate: object) -> ProfileConformance:
|
|
47
|
+
"""Validate the static public Profile contract without evaluating evidence.
|
|
48
|
+
|
|
49
|
+
Local Profile Python is trusted code. This helper validates its exposed authority
|
|
50
|
+
and identity; a real import/evaluation remains the behavioral acceptance test.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
if not isinstance(candidate, Profile):
|
|
54
|
+
raise ConformanceError("object does not implement the Cernora Profile contract")
|
|
55
|
+
authority = candidate.authority
|
|
56
|
+
if not isinstance(authority, CaseProfile):
|
|
57
|
+
raise ConformanceError("Profile authority must be a CaseProfile")
|
|
58
|
+
projection_version = candidate.projection_version
|
|
59
|
+
if not isinstance(projection_version, str) or not projection_version:
|
|
60
|
+
raise ConformanceError("Profile projection_version must be a non-empty string")
|
|
61
|
+
case_ids = tuple(case.case_id for case in authority.cases)
|
|
62
|
+
if not case_ids or len(case_ids) != len(set(case_ids)):
|
|
63
|
+
raise ConformanceError("Profile authority must contain uniquely identified Cases")
|
|
64
|
+
try:
|
|
65
|
+
canonical_json(authority)
|
|
66
|
+
except (TypeError, ValueError) as exc:
|
|
67
|
+
raise ConformanceError("Profile authority must have canonical JSON bytes") from exc
|
|
68
|
+
return ProfileConformance(
|
|
69
|
+
profile_id=authority.profile_id,
|
|
70
|
+
profile_version=authority.profile_version,
|
|
71
|
+
projection_version=projection_version,
|
|
72
|
+
case_ids=case_ids,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_ordinary_file(path: Path) -> bytes:
|
|
77
|
+
try:
|
|
78
|
+
before = path.lstat()
|
|
79
|
+
except OSError as exc:
|
|
80
|
+
raise ConformanceError("Adapter output contains an unreadable path") from exc
|
|
81
|
+
if not stat.S_ISREG(before.st_mode):
|
|
82
|
+
raise ConformanceError("Adapter output files must be ordinary files")
|
|
83
|
+
descriptor = -1
|
|
84
|
+
try:
|
|
85
|
+
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
|
86
|
+
opened = os.fstat(descriptor)
|
|
87
|
+
if (before.st_dev, before.st_ino) != (opened.st_dev, opened.st_ino):
|
|
88
|
+
raise ConformanceError("Adapter output changed while it was inspected")
|
|
89
|
+
if opened.st_size > _MAX_CONFORMANCE_FILE_BYTES:
|
|
90
|
+
raise ConformanceError("Adapter output file exceeds the conformance size limit")
|
|
91
|
+
with os.fdopen(descriptor, "rb", closefd=True) as stream:
|
|
92
|
+
descriptor = -1
|
|
93
|
+
payload = stream.read(_MAX_CONFORMANCE_FILE_BYTES + 1)
|
|
94
|
+
if len(payload) > _MAX_CONFORMANCE_FILE_BYTES:
|
|
95
|
+
raise ConformanceError("Adapter output file exceeds the conformance size limit")
|
|
96
|
+
return payload
|
|
97
|
+
except OSError as exc:
|
|
98
|
+
raise ConformanceError("Adapter output contains an unreadable path") from exc
|
|
99
|
+
finally:
|
|
100
|
+
if descriptor >= 0:
|
|
101
|
+
os.close(descriptor)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _closed_output_files(root: Path) -> dict[str, bytes]:
|
|
105
|
+
try:
|
|
106
|
+
root_info = root.lstat()
|
|
107
|
+
except OSError as exc:
|
|
108
|
+
raise ConformanceError("Adapter output root is missing or unreadable") from exc
|
|
109
|
+
if stat.S_ISLNK(root_info.st_mode) or not stat.S_ISDIR(root_info.st_mode):
|
|
110
|
+
raise ConformanceError("Adapter output root must be an ordinary directory")
|
|
111
|
+
files: dict[str, bytes] = {}
|
|
112
|
+
for path in sorted(root.rglob("*")):
|
|
113
|
+
try:
|
|
114
|
+
info = path.lstat()
|
|
115
|
+
except OSError as exc:
|
|
116
|
+
raise ConformanceError("Adapter output contains an unreadable path") from exc
|
|
117
|
+
if stat.S_ISLNK(info.st_mode):
|
|
118
|
+
raise ConformanceError("Adapter output must not contain symbolic links")
|
|
119
|
+
if stat.S_ISDIR(info.st_mode):
|
|
120
|
+
continue
|
|
121
|
+
if not stat.S_ISREG(info.st_mode):
|
|
122
|
+
raise ConformanceError("Adapter output must contain only ordinary files")
|
|
123
|
+
relative = path.relative_to(root).as_posix()
|
|
124
|
+
files[relative] = _read_ordinary_file(path)
|
|
125
|
+
return files
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def check_adapter_conformance(
|
|
129
|
+
candidate: object,
|
|
130
|
+
completed_export: CompletedExport,
|
|
131
|
+
output: Path,
|
|
132
|
+
) -> AdapterConformance:
|
|
133
|
+
"""Run one Adapter and verify its closed canonical EvidenceBundle v2 tree.
|
|
134
|
+
|
|
135
|
+
The caller supplies a disposable, non-existing output path. Determinism should be
|
|
136
|
+
tested by invoking this helper repeatedly with equivalent completed exports.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
if not isinstance(candidate, Adapter):
|
|
140
|
+
raise ConformanceError("object does not implement the Cernora Adapter contract")
|
|
141
|
+
if output.exists() or output.is_symlink():
|
|
142
|
+
raise ConformanceError("Adapter conformance output must not already exist")
|
|
143
|
+
try:
|
|
144
|
+
result = candidate.adapt(completed_export, output)
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
raise ConformanceError("Adapter rejected the supplied completed export") from exc
|
|
147
|
+
if not isinstance(result, AdaptedBundle):
|
|
148
|
+
raise ConformanceError("Adapter must return a Cernora AdaptedBundle")
|
|
149
|
+
try:
|
|
150
|
+
expected_bundle_path = (output / "bundle.json").resolve(strict=True)
|
|
151
|
+
actual_bundle_path = result.bundle_path.resolve(strict=True)
|
|
152
|
+
except OSError as exc:
|
|
153
|
+
raise ConformanceError("Adapter did not return a readable bundle path") from exc
|
|
154
|
+
if actual_bundle_path != expected_bundle_path:
|
|
155
|
+
raise ConformanceError("Adapter bundle path must be <output>/bundle.json")
|
|
156
|
+
|
|
157
|
+
files = _closed_output_files(output)
|
|
158
|
+
bundle_bytes = files.get("bundle.json")
|
|
159
|
+
if bundle_bytes is None:
|
|
160
|
+
raise ConformanceError("Adapter output is missing bundle.json")
|
|
161
|
+
try:
|
|
162
|
+
bundle = decode_evidence_bundle_v2(bundle_bytes)
|
|
163
|
+
except (TypeError, ValueError) as exc:
|
|
164
|
+
raise ConformanceError("Adapter bundle.json is not strict EvidenceBundle v2") from exc
|
|
165
|
+
if bundle_bytes != canonical_json(bundle):
|
|
166
|
+
raise ConformanceError("Adapter bundle.json is not canonical JSON")
|
|
167
|
+
|
|
168
|
+
expected_files = {"bundle.json", *(artifact.path for artifact in bundle.artifacts)}
|
|
169
|
+
if set(files) != expected_files:
|
|
170
|
+
raise ConformanceError("Adapter output does not match the Bundle artifact set")
|
|
171
|
+
payloads = {artifact.artifact_id: files[artifact.path] for artifact in bundle.artifacts}
|
|
172
|
+
try:
|
|
173
|
+
verify_artifact_payloads_v2(bundle, payloads)
|
|
174
|
+
except (TypeError, ValueError) as exc:
|
|
175
|
+
raise ConformanceError("Adapter artifact payloads do not match the Bundle") from exc
|
|
176
|
+
if bundle.terminal.answer is not None:
|
|
177
|
+
answer_id = bundle.terminal.answer.artifact.artifact_id
|
|
178
|
+
if payloads[answer_id] != bundle.terminal.answer.content.encode("utf-8"):
|
|
179
|
+
raise ConformanceError("Adapter terminal answer bytes do not match its artifact")
|
|
180
|
+
|
|
181
|
+
return AdapterConformance(
|
|
182
|
+
bundle_id=bundle.bundle_id,
|
|
183
|
+
bundle_sha256=bundle.bundle_sha256,
|
|
184
|
+
artifact_ids=tuple(artifact.artifact_id for artifact in bundle.artifacts),
|
|
185
|
+
bundle_path=result.bundle_path,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
__all__ = [
|
|
190
|
+
"AdapterConformance",
|
|
191
|
+
"ConformanceError",
|
|
192
|
+
"ProfileConformance",
|
|
193
|
+
"check_adapter_conformance",
|
|
194
|
+
"check_profile_conformance",
|
|
195
|
+
]
|
cernora/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Stable Cernora contracts and deterministic primitives."""
|