causal-continuity-engine 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.
- causal_continuity_engine/__init__.py +21 -0
- causal_continuity_engine/api.py +1264 -0
- causal_continuity_engine/capabilities.py +548 -0
- causal_continuity_engine/capsule.py +996 -0
- causal_continuity_engine/cli.py +1338 -0
- causal_continuity_engine/core.py +468 -0
- causal_continuity_engine/engine.py +4354 -0
- causal_continuity_engine/evidence.py +971 -0
- causal_continuity_engine/extraction.py +263 -0
- causal_continuity_engine/github.py +657 -0
- causal_continuity_engine/graph.py +809 -0
- causal_continuity_engine/invalidation.py +1040 -0
- causal_continuity_engine/lamport.py +306 -0
- causal_continuity_engine/learning.py +466 -0
- causal_continuity_engine/memory.py +610 -0
- causal_continuity_engine/ontology.py +108 -0
- causal_continuity_engine/partial.py +309 -0
- causal_continuity_engine/policy.py +940 -0
- causal_continuity_engine/proof.py +920 -0
- causal_continuity_engine/py.typed +1 -0
- causal_continuity_engine/redaction.py +94 -0
- causal_continuity_engine/resume.py +653 -0
- causal_continuity_engine/store.py +1174 -0
- causal_continuity_engine/verifiers.py +1006 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/SPEC.md +515 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/benchmarks/__init__.py +0 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/benchmarks/continuitybench/__init__.py +0 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/benchmarks/continuitybench/run.py +146 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/benchmarks/continuitybench/scenarios.py +544 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.anchor.v1.json +62 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.capsule.v1.json +112 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.continuity-receipt.v1.json +315 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.event.v1.json +73 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.proof-predicate.v1.json +35 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.proof.v1.json +242 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.recovery.v1.json +109 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.resume.v1.json +332 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/__init__.py +1 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/schema_validation.py +61 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_api_contract.py +1051 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_cli.py +664 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_conformance.py +758 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_distribution_environment.py +26 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_engine_e2e.py +286 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_github_redaction.py +193 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_instrument_validation.py +535 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_invalidation_resume.py +205 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_learning_partial_api.py +380 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_memory_extraction.py +151 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions.py +406 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round10_release.py +842 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round11_deep_integrity.py +831 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round12_release_blockers.py +206 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round13_jcs.py +212 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round14_state_integrity.py +1056 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round15_capsule_invalidation_github.py +251 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round15_public_boundaries.py +561 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round15_storage_boundaries.py +707 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round16_cli_api_core_capabilities.py +285 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round2.py +388 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round3.py +490 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round4.py +557 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round5.py +583 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round6.py +559 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round7.py +579 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round8_boundaries.py +558 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round8_engine.py +843 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round8_portability.py +2250 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round8_state.py +705 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round9_integrity.py +885 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round9_scope_freshness.py +179 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_release_boundary_validation.py +676 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_release_controls.py +840 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_sqlite_lifecycle.py +52 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_store_graph.py +172 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_trust.py +398 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/action_intent_wrong_type.json +170 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/actor_wrong_type.json +170 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/created_at_invalid_calendar_date.json +172 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/created_at_noncanonical_offset.json +172 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/empty_required_authoritative_failure.json +114 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/empty_required_authoritative_pass.json +107 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/empty_required_no_results.json +99 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/empty_required_self_asserted_pass.json +105 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/generate.py +498 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/incomplete_failed_check.json +168 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/index.json +240 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/input_item_wrong_type.json +163 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/invalid_result_value.json +170 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/lamport_fingerprint_spoof.json +1458 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/lamport_no_registry.json +1454 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/lamport_truncated_key.json +1454 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/missing_required_field.json +157 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/non_binary64_integer.json +174 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/policy_decision_wrong_type.json +146 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/retry_cannot_launder_a_failure.json +197 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/self_asserted_cannot_satisfy.json +173 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/self_assertion_cannot_poison_authoritative_pass.json +194 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/status_contradicts_contents.json +173 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/summary_contradicts_contents.json +170 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/tamper_statement.json +172 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/tamper_statement_resealed.json +172 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/task_id_under_unrelated_field.json +172 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unbound_task.json +165 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unknown_action_intent_field.json +173 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unknown_algorithm.json +172 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unknown_schema_version.json +170 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unknown_top_level_field.json +175 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unverified_forged_body_no_key.json +170 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unverified_hmac_no_key.json +170 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/valid_hmac.json +172 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/valid_hmac_jcs_edges.json +186 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/valid_hmac_unicode.json +172 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/valid_lamport.json +1458 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/wrong_project.json +173 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/verifiers/__init__.py +0 -0
- causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/verifiers/verify_proof.py +1109 -0
- causal_continuity_engine-0.1.0.dist-info/METADATA +720 -0
- causal_continuity_engine-0.1.0.dist-info/RECORD +124 -0
- causal_continuity_engine-0.1.0.dist-info/WHEEL +5 -0
- causal_continuity_engine-0.1.0.dist-info/entry_points.txt +2 -0
- causal_continuity_engine-0.1.0.dist-info/licenses/LICENSE.txt +202 -0
- causal_continuity_engine-0.1.0.dist-info/licenses/NOTICE +6 -0
- causal_continuity_engine-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
"""Instrument validation for the completion gate (ADR-056).
|
|
2
|
+
|
|
3
|
+
A verifier that only ever reports PASS is not evidence of anything. This file
|
|
4
|
+
therefore exercises negative controls at the completion boundary.
|
|
5
|
+
|
|
6
|
+
CCE's completion gate has accumulated twenty independent rejection paths
|
|
7
|
+
across repeated adversarial review rounds. Every one is exercised by a test that
|
|
8
|
+
proves it CAN fire. Nothing until now proved that each fires **for the
|
|
9
|
+
reason it exists** and that the others stay quiet — which matters here more
|
|
10
|
+
than most places, because three of this project's worst defects were
|
|
11
|
+
controls that existed, were tested, and did not run in the path that
|
|
12
|
+
decided (ADR-014, ADR-033/044, ADR-043).
|
|
13
|
+
|
|
14
|
+
So this harness works the other way round from the rest of the suite. It
|
|
15
|
+
builds one known-good completion, plants a specific defect, and asserts:
|
|
16
|
+
|
|
17
|
+
1. the gate that SHOULD catch it does, identified by name; and
|
|
18
|
+
2. the baseline — unmodified — completes cleanly, so a gate that fires on
|
|
19
|
+
everything is exposed rather than mistaken for rigour.
|
|
20
|
+
|
|
21
|
+
A defect caught by the WRONG gate is reported as a mismatch rather than a
|
|
22
|
+
pass. That is not pedantry: it means the model of which control covers what
|
|
23
|
+
is wrong, and a control believed to cover something it does not is exactly
|
|
24
|
+
how a gap survives a review round.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import shlex
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
import pytest
|
|
34
|
+
|
|
35
|
+
from causal_continuity_engine.core import digest_obj
|
|
36
|
+
from causal_continuity_engine.engine import Engine
|
|
37
|
+
|
|
38
|
+
PRJ = "prj_instrument"
|
|
39
|
+
REPOSITORY_ID = 18018
|
|
40
|
+
PRECHECK_SPENDER = "tsk_111111111111111111111111"
|
|
41
|
+
RACING_SPENDER = "tsk_222222222222222222222222"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _python_command(source: str) -> str:
|
|
45
|
+
return shlex.join([sys.executable, "-c", source])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
PASS_COMMAND = _python_command("raise SystemExit(0)")
|
|
49
|
+
FAIL_COMMAND = _python_command("raise SystemExit(1)")
|
|
50
|
+
|
|
51
|
+
#: Each gate's identifying phrase, taken from the message it raises. Keyed by
|
|
52
|
+
#: the ADR that introduced it so a renamed message fails loudly here.
|
|
53
|
+
GATES = {
|
|
54
|
+
"wrong_target": "is not a task in the requested project", # ADR-068
|
|
55
|
+
"open_invalidation": "unresolved invalidation control state", # CI-005
|
|
56
|
+
"already_complete": "is already verified", # ADR-074
|
|
57
|
+
"quarantined": "is quarantined", # ADR-050
|
|
58
|
+
"not_completable": "is not completable", # ADR-074
|
|
59
|
+
"no_proof": "none was provided", # PA-005
|
|
60
|
+
"malformed": "malformed proof envelope", # ADR-068
|
|
61
|
+
"tampered": "invalid or tampered", # PA-002
|
|
62
|
+
"not_verified": "not 'verified'", # PA-005
|
|
63
|
+
"wrong_project": "was issued for", # ADR-018
|
|
64
|
+
"wrong_intent": "proof intent is", # ADR-068
|
|
65
|
+
"unbound_task": "does not name task", # ADR-018
|
|
66
|
+
"already_spent": f"already used to complete {PRECHECK_SPENDER}",
|
|
67
|
+
"claim_race": f"already used to complete {RACING_SPENDER}",
|
|
68
|
+
"not_current": "no longer describes", # ADR-043
|
|
69
|
+
"no_verifiers": "no required verifiers", # ADR-045
|
|
70
|
+
"policy_moved": "policy now requires", # ADR-054
|
|
71
|
+
"policy_denied": "policy decision is not 'allow'", # ADR-068
|
|
72
|
+
"low_grade": "evidence grade", # ADR-027
|
|
73
|
+
"state_changed": "task state changed", # ADR-069
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _reads_deliverable() -> str:
|
|
78
|
+
return _python_command(
|
|
79
|
+
"import pathlib,sys; "
|
|
80
|
+
"sys.exit(0 if pathlib.Path('deliverable.py').read_text() else 1)")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _good(tmp_path, **overrides):
|
|
84
|
+
"""A completion that should be accepted, and the pieces to corrupt."""
|
|
85
|
+
(tmp_path / "deliverable.py").write_text("def f(): return 1\n")
|
|
86
|
+
cfg = {
|
|
87
|
+
"max_autonomy_level": 2,
|
|
88
|
+
"require_proof_for": ["task_complete"],
|
|
89
|
+
"min_evidence_grade": "C",
|
|
90
|
+
"required_verifiers": [{
|
|
91
|
+
"name": "unit-tests", "command": _reads_deliverable(),
|
|
92
|
+
"expect_fail_command": FAIL_COMMAND,
|
|
93
|
+
"artifacts": ["deliverable.py"]}],
|
|
94
|
+
**overrides,
|
|
95
|
+
}
|
|
96
|
+
engine = Engine(workdir=tmp_path)
|
|
97
|
+
engine.create_project(
|
|
98
|
+
"p", project_id=PRJ, repository_id=REPOSITORY_ID, config=cfg)
|
|
99
|
+
engine.policy.grant(project_id=PRJ, level=2, granted_by="lead")
|
|
100
|
+
engine.policy.set_project_config(PRJ, cfg)
|
|
101
|
+
task = engine.graph.put_node(
|
|
102
|
+
entity_type="task", tenant_id=engine.tenant_id, project_id=PRJ,
|
|
103
|
+
data={"title": "ship the exporter"}, status="open")
|
|
104
|
+
proof = engine.attest_action(
|
|
105
|
+
PRJ, intent_type="task_complete", intent_statement="exporter done",
|
|
106
|
+
actor={"agent": "test"}, action_type="run_verifier",
|
|
107
|
+
continuity={"task_ids": [task.id]})
|
|
108
|
+
return engine, task, proof, cfg
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
#: Exception types a REJECTION may use. Narrowing this to PermissionError
|
|
112
|
+
#: alone would make a gate that raised anything else look like a harness
|
|
113
|
+
#: error rather than a control firing, and it would go uncounted below —
|
|
114
|
+
#: an instrument blind to the very thing it exists to measure (ADR-067).
|
|
115
|
+
REJECTION_TYPES = (PermissionError, ValueError)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _attempt(engine, task_id, proof):
|
|
119
|
+
"""Try the completion. Returns the gate name that fired, or None."""
|
|
120
|
+
try:
|
|
121
|
+
engine.complete_task(PRJ, task_id, proof=proof)
|
|
122
|
+
return None
|
|
123
|
+
except REJECTION_TYPES as exc:
|
|
124
|
+
message = str(exc)
|
|
125
|
+
fired = [name for name, phrase in GATES.items() if phrase in message]
|
|
126
|
+
if not fired:
|
|
127
|
+
pytest.fail(f"a {type(exc).__name__} rejection fired with an "
|
|
128
|
+
f"unrecognised message: {message}")
|
|
129
|
+
if len(fired) > 1:
|
|
130
|
+
pytest.fail(f"ambiguous gate message matches {fired}: {message}")
|
|
131
|
+
return fired[0]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _reseal(proof: dict) -> dict:
|
|
135
|
+
"""Recompute the digest so the tamper is internally consistent.
|
|
136
|
+
|
|
137
|
+
Under HMAC an outsider cannot re-sign, so this isolates the SIGNATURE
|
|
138
|
+
check from the digest check: a resealed body still fails on the signature
|
|
139
|
+
alone, which is the property being asserted.
|
|
140
|
+
"""
|
|
141
|
+
body = {k: v for k, v in proof.items()
|
|
142
|
+
if k not in ("signature", "proof_digest")}
|
|
143
|
+
proof["proof_digest"] = digest_obj(body)
|
|
144
|
+
return proof
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _trusted_reseal(engine: Engine, proof: dict) -> dict:
|
|
148
|
+
"""Issue a modified envelope with the engine's actual trusted key.
|
|
149
|
+
|
|
150
|
+
This is used only for semantic gates whose input must remain both
|
|
151
|
+
internally consistent and authentic; otherwise the tamper gate would
|
|
152
|
+
correctly intercept the fixture first.
|
|
153
|
+
"""
|
|
154
|
+
issued = _reseal(json.loads(json.dumps(proof)))
|
|
155
|
+
issued["signature"] = engine.signer.sign(issued)
|
|
156
|
+
return issued
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ─────────────────────────── the mutation table ───────────────────────────
|
|
160
|
+
# Each entry plants one defect and names the single gate that must catch it.
|
|
161
|
+
|
|
162
|
+
class TestBaselineCompletesCleanly:
|
|
163
|
+
def test_unmodified_completion_is_accepted(self, tmp_path):
|
|
164
|
+
"""If this fails, every result below is meaningless."""
|
|
165
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
166
|
+
assert _attempt(engine, task.id, proof) is None, \
|
|
167
|
+
"the known-good case is rejected: no gate result below can be trusted"
|
|
168
|
+
engine.close()
|
|
169
|
+
|
|
170
|
+
def test_the_good_proof_is_actually_verified(self, tmp_path):
|
|
171
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
172
|
+
assert proof["status"] == "verified"
|
|
173
|
+
assert proof["evidence_context"]["mutation"]["bound"] is True
|
|
174
|
+
engine.close()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TestEachDefectTripsItsOwnGate:
|
|
178
|
+
def test_target_is_not_a_task_in_the_requested_project(self, tmp_path):
|
|
179
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
180
|
+
assumption = engine.graph.put_node(
|
|
181
|
+
entity_type="assumption", tenant_id=engine.tenant_id,
|
|
182
|
+
project_id=PRJ, data={"statement": "not executable work"},
|
|
183
|
+
status="active")
|
|
184
|
+
assert _attempt(engine, assumption.id, proof) == "wrong_target"
|
|
185
|
+
engine.close()
|
|
186
|
+
|
|
187
|
+
def test_a_different_claim_cannot_overwrite_verified_completion(self, tmp_path):
|
|
188
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
189
|
+
assert _attempt(engine, task.id, proof) is None
|
|
190
|
+
version = engine.graph.get(task.id)["version"]
|
|
191
|
+
# An exact retry is a no-op, so transport retries remain safe.
|
|
192
|
+
assert engine.complete_task(PRJ, task.id, proof=proof)["version"] == version
|
|
193
|
+
replacement = engine.attest_action(
|
|
194
|
+
PRJ, intent_type="task_complete", intent_statement="replacement",
|
|
195
|
+
actor={"agent": "test"}, action_type="run_verifier",
|
|
196
|
+
continuity={"task_ids": [task.id]})
|
|
197
|
+
assert _attempt(engine, task.id, replacement) == "already_complete"
|
|
198
|
+
completed = engine.graph.get(task.id)
|
|
199
|
+
assert completed["version"] == version
|
|
200
|
+
assert completed["data"]["completion_evidence"] == proof["proof_id"]
|
|
201
|
+
engine.close()
|
|
202
|
+
|
|
203
|
+
def test_quarantined_task(self, tmp_path):
|
|
204
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
205
|
+
engine.partial.quarantine(task.id, actor="cce", reason="ambiguous output")
|
|
206
|
+
assert _attempt(engine, task.id, proof) == "quarantined"
|
|
207
|
+
engine.close()
|
|
208
|
+
|
|
209
|
+
def test_blocked_task_cannot_bypass_state_gate_when_proof_is_optional(
|
|
210
|
+
self, tmp_path):
|
|
211
|
+
engine, task, proof, _ = _good(tmp_path, require_proof_for=[])
|
|
212
|
+
engine.graph.put_node(
|
|
213
|
+
entity_type="task", tenant_id=engine.tenant_id,
|
|
214
|
+
project_id=PRJ, node_id=task.id, status="blocked", data={})
|
|
215
|
+
assert _attempt(engine, task.id, None) == "not_completable"
|
|
216
|
+
assert engine.graph.get(task.id)["status"] == "blocked"
|
|
217
|
+
engine.close()
|
|
218
|
+
|
|
219
|
+
def test_no_proof_at_all(self, tmp_path):
|
|
220
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
221
|
+
assert _attempt(engine, task.id, None) == "no_proof"
|
|
222
|
+
engine.close()
|
|
223
|
+
|
|
224
|
+
def test_required_proof_field_is_missing(self, tmp_path):
|
|
225
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
226
|
+
malformed = json.loads(json.dumps(proof))
|
|
227
|
+
malformed.pop("proof_id")
|
|
228
|
+
assert _attempt(engine, task.id, malformed) == "malformed"
|
|
229
|
+
engine.close()
|
|
230
|
+
|
|
231
|
+
def test_body_tampered_without_resealing(self, tmp_path):
|
|
232
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
233
|
+
forged = json.loads(json.dumps(proof))
|
|
234
|
+
forged["action_intent"]["statement"] = "something else entirely"
|
|
235
|
+
assert _attempt(engine, task.id, forged) == "tampered"
|
|
236
|
+
engine.close()
|
|
237
|
+
|
|
238
|
+
def test_body_tampered_and_resealed_still_fails_on_the_signature(self, tmp_path):
|
|
239
|
+
"""The case that decides whether the signature check is decoration.
|
|
240
|
+
|
|
241
|
+
The digest is recomputed so it agrees with the altered body; only the
|
|
242
|
+
signature can catch it. This is the resealed-tamper negative control.
|
|
243
|
+
"""
|
|
244
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
245
|
+
forged = _reseal(json.loads(json.dumps(proof)))
|
|
246
|
+
forged["action_intent"]["statement"] = "something else entirely"
|
|
247
|
+
forged = _reseal(forged)
|
|
248
|
+
assert _attempt(engine, task.id, forged) == "tampered"
|
|
249
|
+
engine.close()
|
|
250
|
+
|
|
251
|
+
def test_status_forged_to_verified(self, tmp_path):
|
|
252
|
+
engine, task, proof, cfg = _good(
|
|
253
|
+
tmp_path, required_verifiers=[{
|
|
254
|
+
"name": "unit-tests", "command": FAIL_COMMAND}])
|
|
255
|
+
assert proof["status"] == "failed"
|
|
256
|
+
assert _attempt(engine, task.id, proof) == "not_verified"
|
|
257
|
+
engine.close()
|
|
258
|
+
|
|
259
|
+
def test_proof_from_another_project(self, tmp_path):
|
|
260
|
+
"""A GENUINELY signed proof from elsewhere, not a JSON edit.
|
|
261
|
+
|
|
262
|
+
Editing project_id in the body breaks the digest, so a naive swap is
|
|
263
|
+
caught by the tamper gate and never reaches the scope check — which
|
|
264
|
+
is why this mutation has to mint a real foreign proof to test what it
|
|
265
|
+
claims to test.
|
|
266
|
+
"""
|
|
267
|
+
engine, task, proof, cfg = _good(tmp_path)
|
|
268
|
+
other = Engine(workdir=tmp_path, signer=engine.signer) # same key
|
|
269
|
+
other.create_project("o", project_id="prj_elsewhere", config=cfg)
|
|
270
|
+
other.policy.grant(project_id="prj_elsewhere", level=2, granted_by="l")
|
|
271
|
+
other.policy.set_project_config("prj_elsewhere", cfg)
|
|
272
|
+
foreign_task = other.graph.put_node(
|
|
273
|
+
entity_type="task", tenant_id=other.tenant_id,
|
|
274
|
+
project_id="prj_elsewhere", data={"title": "their task"},
|
|
275
|
+
status="open")
|
|
276
|
+
foreign = other.attest_action(
|
|
277
|
+
"prj_elsewhere", intent_type="task_complete",
|
|
278
|
+
intent_statement="theirs", actor={"agent": "test"},
|
|
279
|
+
action_type="run_verifier",
|
|
280
|
+
continuity={"task_ids": [foreign_task.id]})
|
|
281
|
+
assert _attempt(engine, task.id, foreign) == "wrong_project"
|
|
282
|
+
other.close()
|
|
283
|
+
engine.close()
|
|
284
|
+
|
|
285
|
+
def test_proof_for_another_intent(self, tmp_path):
|
|
286
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
287
|
+
wrong_intent = engine.attest_action(
|
|
288
|
+
PRJ, intent_type="pr_ready", intent_statement="ready for review",
|
|
289
|
+
actor={"agent": "test"}, action_type="run_verifier",
|
|
290
|
+
continuity={"task_ids": [task.id]})
|
|
291
|
+
assert wrong_intent["status"] == "verified"
|
|
292
|
+
assert _attempt(engine, task.id, wrong_intent) == "wrong_intent"
|
|
293
|
+
engine.close()
|
|
294
|
+
|
|
295
|
+
def test_proof_that_names_no_task(self, tmp_path):
|
|
296
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
297
|
+
unbound = engine.attest_action(
|
|
298
|
+
PRJ, intent_type="task_complete", intent_statement="unbound",
|
|
299
|
+
actor={"agent": "test"}, action_type="run_verifier")
|
|
300
|
+
assert _attempt(engine, task.id, unbound) == "unbound_task"
|
|
301
|
+
engine.close()
|
|
302
|
+
|
|
303
|
+
def test_proof_replayed_onto_a_second_task(self, tmp_path):
|
|
304
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
305
|
+
second = engine.graph.put_node(
|
|
306
|
+
entity_type="task", tenant_id=engine.tenant_id, project_id=PRJ,
|
|
307
|
+
node_id=PRECHECK_SPENDER, data={"title": "another"},
|
|
308
|
+
status="open")
|
|
309
|
+
both = engine.attest_action(
|
|
310
|
+
PRJ, intent_type="task_complete", intent_statement="both",
|
|
311
|
+
actor={"agent": "test"}, action_type="run_verifier",
|
|
312
|
+
continuity={"task_ids": [task.id, second.id]})
|
|
313
|
+
assert _attempt(engine, second.id, both) is None
|
|
314
|
+
assert _attempt(engine, task.id, both) == "already_spent"
|
|
315
|
+
engine.close()
|
|
316
|
+
|
|
317
|
+
def test_proof_is_claimed_by_a_peer_after_the_precheck(
|
|
318
|
+
self, tmp_path, monkeypatch):
|
|
319
|
+
"""The final atomic claim must reject a precheck/claim race."""
|
|
320
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
321
|
+
|
|
322
|
+
def peer_won_claim(project_id, proof_id, task_id):
|
|
323
|
+
return RACING_SPENDER
|
|
324
|
+
|
|
325
|
+
monkeypatch.setattr(engine, "_claim_proof", peer_won_claim)
|
|
326
|
+
assert _attempt(engine, task.id, proof) == "claim_race"
|
|
327
|
+
assert engine.graph.get(task.id)["status"] == "open"
|
|
328
|
+
engine.close()
|
|
329
|
+
|
|
330
|
+
def test_deliverable_changed_after_attestation(self, tmp_path):
|
|
331
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
332
|
+
(tmp_path / "deliverable.py").write_text("def f(): return 999\n")
|
|
333
|
+
assert _attempt(engine, task.id, proof) == "not_current"
|
|
334
|
+
engine.close()
|
|
335
|
+
|
|
336
|
+
def test_invalidation_fired_after_attestation(self, tmp_path):
|
|
337
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
338
|
+
asm = engine.graph.put_node(
|
|
339
|
+
entity_type="assumption", tenant_id=engine.tenant_id,
|
|
340
|
+
project_id=PRJ, data={"statement": "schema is stable"},
|
|
341
|
+
status="active", criticality="critical")
|
|
342
|
+
engine.invalidation.fire(
|
|
343
|
+
tenant_id=engine.tenant_id, project_id=PRJ, target_node_id=asm.id,
|
|
344
|
+
trigger_type="contradictory_evidence", trigger_confidence=0.95)
|
|
345
|
+
assert _attempt(engine, task.id, proof) == "open_invalidation"
|
|
346
|
+
engine.close()
|
|
347
|
+
|
|
348
|
+
def test_policy_declares_no_verifiers(self, tmp_path):
|
|
349
|
+
engine, task, proof, cfg = _good(tmp_path)
|
|
350
|
+
engine.policy.set_project_config(
|
|
351
|
+
PRJ, {**cfg, "required_verifiers": []})
|
|
352
|
+
assert _attempt(engine, task.id, proof) == "no_verifiers"
|
|
353
|
+
engine.close()
|
|
354
|
+
|
|
355
|
+
def test_policy_tightened_after_attestation(self, tmp_path):
|
|
356
|
+
engine, task, proof, cfg = _good(tmp_path)
|
|
357
|
+
engine.policy.set_project_config(PRJ, {
|
|
358
|
+
**cfg, "required_verifiers": cfg["required_verifiers"] + [
|
|
359
|
+
{"name": "security-scan", "command": PASS_COMMAND}]})
|
|
360
|
+
assert _attempt(engine, task.id, proof) == "policy_moved"
|
|
361
|
+
engine.close()
|
|
362
|
+
|
|
363
|
+
def test_authentic_proof_records_policy_denial(self, tmp_path):
|
|
364
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
365
|
+
denied = json.loads(json.dumps(proof))
|
|
366
|
+
denied["policy_decision"] = {
|
|
367
|
+
"decision": "deny",
|
|
368
|
+
"reason": "trusted issuer recorded a denied action",
|
|
369
|
+
}
|
|
370
|
+
denied = _trusted_reseal(engine, denied)
|
|
371
|
+
assert _attempt(engine, task.id, denied) == "policy_denied"
|
|
372
|
+
engine.close()
|
|
373
|
+
|
|
374
|
+
def test_evidence_below_the_grade_floor(self, tmp_path):
|
|
375
|
+
"""A check the claimant chose, rather than one the policy pinned."""
|
|
376
|
+
engine, task, proof, cfg = _good(
|
|
377
|
+
tmp_path, required_verifiers=["unit-tests"]) # bare name: unpinned
|
|
378
|
+
from causal_continuity_engine.verifiers import VerifierSpec
|
|
379
|
+
weak = engine.attest_action(
|
|
380
|
+
PRJ, intent_type="task_complete", intent_statement="weak",
|
|
381
|
+
actor={"agent": "test"}, action_type="run_verifier",
|
|
382
|
+
verifier_specs=[VerifierSpec(name="unit-tests",
|
|
383
|
+
command=PASS_COMMAND)],
|
|
384
|
+
continuity={"task_ids": [task.id]})
|
|
385
|
+
assert _attempt(engine, task.id, weak) == "low_grade"
|
|
386
|
+
engine.close()
|
|
387
|
+
|
|
388
|
+
def test_task_changes_during_validation(self, tmp_path, monkeypatch):
|
|
389
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
390
|
+
original = engine.proof_currency
|
|
391
|
+
|
|
392
|
+
def plant_change(project_id, task_id, envelope):
|
|
393
|
+
result = original(project_id, task_id, envelope)
|
|
394
|
+
engine.graph.put_node(
|
|
395
|
+
entity_type="task", tenant_id=engine.tenant_id,
|
|
396
|
+
project_id=PRJ, node_id=task.id, status="quarantined",
|
|
397
|
+
data={"quarantine_reason": "arrived during validation"})
|
|
398
|
+
return result
|
|
399
|
+
|
|
400
|
+
monkeypatch.setattr(engine, "proof_currency", plant_change)
|
|
401
|
+
assert _attempt(engine, task.id, proof) == "state_changed"
|
|
402
|
+
# Rejection rolls the planted in-transaction mutation back too.
|
|
403
|
+
assert engine.graph.get(task.id)["status"] == "open"
|
|
404
|
+
engine.close()
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class TestNoGateFiresSpuriously:
|
|
408
|
+
"""The other half of instrument validation, which CCE had none of: a
|
|
409
|
+
control that fires on everything is not a control."""
|
|
410
|
+
|
|
411
|
+
@pytest.mark.parametrize("harmless", [
|
|
412
|
+
"add_an_unrelated_task",
|
|
413
|
+
"compose_a_resume_packet",
|
|
414
|
+
"run_an_evidence_probe",
|
|
415
|
+
"record_an_audit_entry",
|
|
416
|
+
"ingest_an_unrelated_event",
|
|
417
|
+
])
|
|
418
|
+
def test_harmless_activity_does_not_block_completion(self, tmp_path, harmless):
|
|
419
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
420
|
+
if harmless == "add_an_unrelated_task":
|
|
421
|
+
engine.graph.put_node(
|
|
422
|
+
entity_type="task", tenant_id=engine.tenant_id, project_id=PRJ,
|
|
423
|
+
data={"title": "unrelated"}, status="open")
|
|
424
|
+
elif harmless == "compose_a_resume_packet":
|
|
425
|
+
engine.resume_packet(PRJ)
|
|
426
|
+
elif harmless == "run_an_evidence_probe":
|
|
427
|
+
engine.probe_evidence(PRJ)
|
|
428
|
+
elif harmless == "record_an_audit_entry":
|
|
429
|
+
engine.store.audit(actor="someone", action="unrelated.thing")
|
|
430
|
+
elif harmless == "ingest_an_unrelated_event":
|
|
431
|
+
engine.ingest_github(PRJ, "issues", "unrelated-1", {
|
|
432
|
+
"action": "opened",
|
|
433
|
+
"issue": {"number": 99, "title": "T",
|
|
434
|
+
"body": "An unrelated observation.", "state": "open",
|
|
435
|
+
"labels": [], "author_association": "OWNER",
|
|
436
|
+
"created_at": "2026-07-31T10:00:00Z"},
|
|
437
|
+
"repository": {"id": REPOSITORY_ID, "full_name": "o/r"}})
|
|
438
|
+
assert _attempt(engine, task.id, proof) is None, \
|
|
439
|
+
f"{harmless} spuriously blocked an otherwise valid completion"
|
|
440
|
+
engine.close()
|
|
441
|
+
|
|
442
|
+
def test_a_non_critical_invalidation_elsewhere_does_not_block(self, tmp_path):
|
|
443
|
+
"""Only invalidations that touch this task, or critical ones, count."""
|
|
444
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
445
|
+
unrelated = engine.graph.put_node(
|
|
446
|
+
entity_type="assumption", tenant_id=engine.tenant_id,
|
|
447
|
+
project_id=PRJ, data={"statement": "the office wifi is fast"},
|
|
448
|
+
status="active", criticality="low")
|
|
449
|
+
engine.invalidation.fire(
|
|
450
|
+
tenant_id=engine.tenant_id, project_id=PRJ,
|
|
451
|
+
target_node_id=unrelated.id, trigger_type="dependency_drift",
|
|
452
|
+
trigger_confidence=0.95)
|
|
453
|
+
assert _attempt(engine, task.id, proof) is None, \
|
|
454
|
+
"an unrelated low-severity invalidation blocked completion"
|
|
455
|
+
engine.close()
|
|
456
|
+
|
|
457
|
+
def test_uncontested_atomic_proof_claim_does_not_block(
|
|
458
|
+
self, tmp_path, monkeypatch):
|
|
459
|
+
"""Exercise the claim-race boundary with its harmless result."""
|
|
460
|
+
engine, task, proof, _ = _good(tmp_path)
|
|
461
|
+
original = engine._claim_proof
|
|
462
|
+
calls = []
|
|
463
|
+
|
|
464
|
+
def observe_claim(project_id, proof_id, task_id):
|
|
465
|
+
calls.append((project_id, proof_id, task_id))
|
|
466
|
+
return original(project_id, proof_id, task_id)
|
|
467
|
+
|
|
468
|
+
monkeypatch.setattr(engine, "_claim_proof", observe_claim)
|
|
469
|
+
assert _attempt(engine, task.id, proof) is None
|
|
470
|
+
assert calls == [(PRJ, proof["proof_id"], task.id)]
|
|
471
|
+
engine.close()
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class TestGateCoverageIsComplete:
|
|
475
|
+
"""If a new rejection path is added without a negative control, say so.
|
|
476
|
+
|
|
477
|
+
This class is the instrument for the instrument. Its own three defects —
|
|
478
|
+
a substring count that a comment could inflate, no check that a NAMED
|
|
479
|
+
gate is actually exercised, and a blind spot for any exception type but
|
|
480
|
+
one — were the kind that leave a harness reporting full coverage of
|
|
481
|
+
something it stopped covering (ADR-067).
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
@staticmethod
|
|
485
|
+
def _rejection_count() -> int:
|
|
486
|
+
"""Rejections in complete_task, counted from the PARSED function.
|
|
487
|
+
|
|
488
|
+
Counting `source.count("raise PermissionError")` counted text: a
|
|
489
|
+
comment or a string literal mentioning the phrase inflated it, and a
|
|
490
|
+
gate raised through a helper did not appear at all. The AST counts
|
|
491
|
+
raise statements.
|
|
492
|
+
"""
|
|
493
|
+
import ast
|
|
494
|
+
import inspect
|
|
495
|
+
import textwrap
|
|
496
|
+
|
|
497
|
+
from causal_continuity_engine import engine as engine_module
|
|
498
|
+
tree = ast.parse(textwrap.dedent(
|
|
499
|
+
inspect.getsource(engine_module.Engine._complete_task_locked)))
|
|
500
|
+
return sum(
|
|
501
|
+
1 for node in ast.walk(tree)
|
|
502
|
+
if isinstance(node, ast.Raise)
|
|
503
|
+
and isinstance(node.exc, ast.Call)
|
|
504
|
+
and isinstance(node.exc.func, ast.Name)
|
|
505
|
+
and node.exc.func.id in {t.__name__ for t in REJECTION_TYPES})
|
|
506
|
+
|
|
507
|
+
def test_every_rejection_path_has_a_named_gate(self):
|
|
508
|
+
raises = self._rejection_count()
|
|
509
|
+
assert raises == len(GATES), (
|
|
510
|
+
f"complete_task has {raises} rejection paths but GATES names "
|
|
511
|
+
f"{len(GATES)}. Add the new gate here with a planted defect that "
|
|
512
|
+
f"trips it, or this harness silently stops covering it.")
|
|
513
|
+
|
|
514
|
+
def test_every_named_gate_has_a_planted_defect_that_trips_it(self):
|
|
515
|
+
"""A name in GATES with no test behind it is coverage on paper.
|
|
516
|
+
|
|
517
|
+
The count above would still pass: GATES and the rejection paths would
|
|
518
|
+
agree while one of them was never exercised.
|
|
519
|
+
"""
|
|
520
|
+
import inspect
|
|
521
|
+
source = inspect.getsource(TestEachDefectTripsItsOwnGate)
|
|
522
|
+
unexercised = sorted(name for name in GATES
|
|
523
|
+
if f'== "{name}"' not in source)
|
|
524
|
+
assert not unexercised, (
|
|
525
|
+
f"named but never planted: {unexercised}. Each gate needs a test "
|
|
526
|
+
f"that plants its defect and asserts THAT gate catches it.")
|
|
527
|
+
|
|
528
|
+
def test_the_counter_is_not_fooled_by_text(self):
|
|
529
|
+
"""Validates the instrument's own instrument: the AST counter must
|
|
530
|
+
ignore a mention of the phrase that is not a raise."""
|
|
531
|
+
import ast
|
|
532
|
+
source = 'def f():\n x = "raise PermissionError" # raise ValueError\n'
|
|
533
|
+
tree = ast.parse(source)
|
|
534
|
+
assert not [n for n in ast.walk(tree) if isinstance(n, ast.Raise)], \
|
|
535
|
+
"the counter would credit a comment or string as a rejection path"
|