mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
cdb8485ec2
* feat(probe): pipeline wiring for #655 PR-C — trigger, consent surface, freshness, transmission ledger - claim_verification_protocol.md gains the Claim-Standing Probe Offer section: user-requested advisory view after the Phase E Claim Registry at Stage 2.5/4.5; HIGH-IMPACT-only trigger; ambiguity ineligible until researcher confirmation; never part of the integrity result (gate 12 boundary). - scripts/build_claim_standing_query_plan.py: §3.1 eligibility (gate 1) + §3.2 propose/bind consent surface (gate 2); emits schema-valid query-plan 1.0/1.1 or an explicit not_checked declination on cancel. - scripts/check_claim_standing_freshness.py: §7 probe-identity drift -> stale with closed reasons (gate 13); corrupt digests are errors. - shared/contracts/claim_standing/transmission_ledger.schema.json + scripts/check_claim_standing_transmissions.py: closed §6 per-event accounting; consented allowlist/roster fail-closed (gate 14). - 82 new tests across four files; CI manifest + integration-guard markers, protocol doc, contracts README, CHANGELOG aligned. Zero-spend: no live index, model, or network call anywhere; every probe surface still says STANCE CLASSIFICATION UNMEASURED. #655 stays open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeJS63dMYTADV8FPR4Mx1m * fix(probe): PR-C review round 2 — consent binds full projection, honest declinations, freshness coverage, transmission completeness Applies the converged three-track findings (codex 5 P1 + 2 P2, security 2 P2 + 6 P3, simplify/reuse/altitude): - Gate 1: Stage 2.5 HIGH-IMPACT tier alone is the registry witness; E1 now records the five-part high_impact_basis; basis provenance recorded on the consent surface, never in the registry. - Gate 2: consent surface embeds the complete consentable-plan projection (surface hash covers every receipt-bound field; bind asserts digest equality); absence/invalidation/cancel each produce an explicit local not_checked declination (consent_absent/consent_invalidated/ consent_cancelled); bind --output is consent-gated to the derived .query-plan.json path. - Gate 13: freshness consumes the runner's single expected_identity authority, requires the record's ledger, compares consented provider/model, and declares assessed/unassessed bindings. - Gate 14: schema + self-digest validation of both inputs, duplicate attempt and off-target index refusal, explicit stance-transmission requirement, stance-record cross-check, event-kind fixed field-by-field, conservative-accounting semantics documented; uniqueItems on events. - Gate 12: attribute-level containment of the discovery module, no direct write calls, CLI paths included in the no-file-creation run; doc pins consolidated into the #655 integration guard (incl. Phase E surface and a forbidden-stale-sentence check). - Substrate: validator memoization, shared export-consent helpers (trailing-separator refusal), exported trigger constants pinned to the plan schema; falsey decisions refused, deep copies via copy.deepcopy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeJS63dMYTADV8FPR4Mx1m * fix(probe): PR-C review round 3 — intake invariants in gate 14, cancel-flow fidelity, complete artifact roster codex R2 (1 P1 + 4 P2 + 1 P3) and security R2 (6 P3) convergence: - Gate 14: adopt substrate.validate_input so a re-sealed retrieval input omitting a planned attempt is refused; stance-record cross-check now schema-validates + digest-replays the record and compares per-row prompt hash and result state (closed row->event mapping). - Gate 2: an explicit cancel records consent_cancelled even after a retrieval_plus_stance surface was proposed (checked before the surface hash comparison); falsey filter lists refused at proposal; bind CLI exits 3 on declinations. - Consent surface: complete consented-artifact roster from new substrate.ARTIFACT_SUFFIXES (test-pinned to every owning module's suffix constant); trailing-separator authorized paths refused at proposal, in validate_plan, and via the shared helper now used by discovery and the renderer too. - Freshness: fail-closed parity guard between the runner's expected_identity and the stale-reason map; eligibility verdict gains a dispatchable flag distinct from the tier witness. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeJS63dMYTADV8FPR4Mx1m * fix(probe): PR-C review round 4 — failed stance calls bind their prompt hash codex R3's remaining P2: a judge_timeout / judge_error / oversized-output row left prompt_sha256 null, so the gate-14 cross-check could not hold a tampered failure event to the hash actually sent. The runner now binds prompt_sha256 + assessment_input_sha256 into the row BEFORE the transport call; validate_stance_record replays those hashes on transport-reaching failure rows (and requires them null on undispatched abstract_missing rows); the transmission cross-check requires a non-null, exactly-matching prompt hash for every transport-reaching row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeJS63dMYTADV8FPR4Mx1m * test(probe): close the remaining capability-scan evasion forms Dotted from-imports of discovery symbols, discovery-alias rebinding, and getattr() dynamic lookup now fail the gate-12 wiring scan (security R2 P3-6 residue). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeJS63dMYTADV8FPR4Mx1m * chore: ignore the maintainer-local deliverables/ directory Local working documents are not part of the public project; keep the directory permanently out of version control on every checkout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeJS63dMYTADV8FPR4Mx1m --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
257 lines
8.7 KiB
Python
257 lines
8.7 KiB
Python
"""Mutation tests for the static #655 Track A integration guard."""
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from scripts import check_claim_standing_candidate_ledger_integration as guard
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
COPIED = tuple(
|
|
dict.fromkeys(
|
|
(
|
|
*(guard.SCHEMA_DIR / name for name in guard.SCHEMAS),
|
|
guard.RUNTIME,
|
|
guard.PROTOCOL,
|
|
guard.PHASE_E_PROTOCOL,
|
|
guard.DESIGN,
|
|
guard.CONTRACT_README,
|
|
guard.MANIFEST,
|
|
guard.WORKFLOW,
|
|
guard.CHANGELOG,
|
|
*guard.FIXTURES,
|
|
*guard.RESOLVER_HASHES,
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
def _tree(tmp_path: Path) -> Path:
|
|
root = tmp_path / "repo"
|
|
for relative in COPIED:
|
|
target = root / relative
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(ROOT / relative, target)
|
|
return root
|
|
|
|
|
|
def _replace(root: Path, relative: Path, old: str, new: str) -> None:
|
|
path = root / relative
|
|
text = path.read_text(encoding="utf-8")
|
|
assert old in text
|
|
path.write_text(text.replace(old, new), encoding="utf-8")
|
|
|
|
|
|
def test_current_repository_passes() -> None:
|
|
assert guard.run_checks(ROOT) == []
|
|
|
|
|
|
def test_schema_identity_and_closure_are_pinned(tmp_path: Path) -> None:
|
|
root = _tree(tmp_path)
|
|
schema = guard.SCHEMA_DIR / "query_plan.schema.json"
|
|
_replace(root, schema, "claim-standing-query-plan/1.0", "claim-standing-query-plan/2.0")
|
|
_replace(root, schema, '"additionalProperties": false', '"additionalProperties": true')
|
|
errors = guard.run_checks(root)
|
|
assert any("schema_version drifted" in error for error in errors)
|
|
assert any("root must remain closed" in error for error in errors)
|
|
|
|
|
|
def test_shared_contract_definitions_and_track_boundary_are_pinned(tmp_path: Path) -> None:
|
|
root = _tree(tmp_path)
|
|
candidate = guard.SCHEMA_DIR / "candidate_ledger.schema.json"
|
|
_replace(root, candidate, '"returned_count": { "type": "integer"', '"returned_count": { "type": "number"')
|
|
errors = guard.run_checks(root)
|
|
assert any("shared attempt/attempt definitions drifted" in error for error in errors)
|
|
root = _tree(tmp_path / "boundary")
|
|
query = guard.SCHEMA_DIR / "query_plan.schema.json"
|
|
_replace(root, query, '"created_at": { "$ref":', '"stance": { "type": "string" },\n "created_at": { "$ref":')
|
|
assert any("Track B/live fields entered Track A" in error for error in guard.run_checks(root))
|
|
|
|
|
|
def test_network_or_dispatch_capability_is_rejected(tmp_path: Path) -> None:
|
|
root = _tree(tmp_path)
|
|
path = root / guard.RUNTIME
|
|
path.write_text("import requests\n" + path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
errors = guard.run_checks(root)
|
|
assert any("non-allowlisted transport/model/process-capable imports" in error for error in errors)
|
|
_replace(root, guard.RUNTIME, 'add_parser("build"', 'add_parser("dispatch"')
|
|
assert any("forbidden capability marker" in error for error in guard.run_checks(root))
|
|
|
|
|
|
def test_non_allowlisted_and_dynamic_imports_are_rejected(tmp_path: Path) -> None:
|
|
root = _tree(tmp_path / "direct")
|
|
path = root / guard.RUNTIME
|
|
path.write_text("import httpx\n" + path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
errors = guard.run_checks(root)
|
|
assert any("non-allowlisted" in error and "httpx" in error for error in errors)
|
|
|
|
root = _tree(tmp_path / "dynamic")
|
|
path = root / guard.RUNTIME
|
|
path.write_text(
|
|
"_process_module = __import__('subprocess')\n"
|
|
+ path.read_text(encoding="utf-8"),
|
|
encoding="utf-8",
|
|
)
|
|
errors = guard.run_checks(root)
|
|
assert any("dynamic import, introspection" in error for error in errors)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("payload", "symbol"),
|
|
[
|
|
(
|
|
"from sys import modules as registry\n"
|
|
"registry['os'].spawnl(0, '/bin/true', 'true')\n",
|
|
"sys.modules",
|
|
),
|
|
(
|
|
"from sys import _getframe as frame\n"
|
|
"_scope = frame().f_globals\n",
|
|
"sys._getframe",
|
|
),
|
|
],
|
|
)
|
|
def test_from_import_requires_an_exact_per_module_symbol_allowlist(
|
|
tmp_path: Path, payload: str, symbol: str
|
|
) -> None:
|
|
root = _tree(tmp_path)
|
|
path = root / guard.RUNTIME
|
|
path.write_text(payload + path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
errors = guard.run_checks(root)
|
|
assert any("non-allowlisted" in error and symbol in error for error in errors)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"payload",
|
|
[
|
|
"_runner = eval\n_runner('1 + 1')\n",
|
|
"sys.modules['os'].system('true')\n",
|
|
(
|
|
"getattr(globals()['__built' + 'ins__'], "
|
|
"'__im' + 'port__')('socket')\n"
|
|
),
|
|
],
|
|
)
|
|
def test_alias_introspection_and_allowed_module_capability_bypasses_are_rejected(
|
|
tmp_path: Path, payload: str
|
|
) -> None:
|
|
root = _tree(tmp_path)
|
|
path = root / guard.RUNTIME
|
|
path.write_text(payload + path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
errors = guard.run_checks(root)
|
|
assert any("dynamic import, introspection" in error for error in errors)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"payload",
|
|
[
|
|
"argparse._os.spawnl(0, '/bin/true', 'true')\n",
|
|
"hashlib.__spec__.loader.load_module('os')\n",
|
|
(
|
|
"import argparse as parser_alias\n"
|
|
"parser_alias._os.spawnl(0, '/bin/true', 'true')\n"
|
|
),
|
|
"_escaped_module = argparse\n",
|
|
"import hashlib as hash_alias\n_escaped_module = hash_alias\n",
|
|
"_escaped_attribute = hashlib.sha256\n",
|
|
"_escaped_stream = sys.stderr\n",
|
|
"os.system('true')\n",
|
|
"os.O_EXCL = 0\n",
|
|
"del os.O_NOFOLLOW\n",
|
|
],
|
|
)
|
|
def test_direct_module_uses_are_exact_and_cannot_escape(
|
|
tmp_path: Path, payload: str
|
|
) -> None:
|
|
root = _tree(tmp_path)
|
|
path = root / guard.RUNTIME
|
|
path.write_text(payload + path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
errors = guard.run_checks(root)
|
|
assert any("exact current-use" in error for error in errors)
|
|
|
|
|
|
def test_safe_direct_module_alias_is_tracked_and_allowed(tmp_path: Path) -> None:
|
|
root = _tree(tmp_path)
|
|
_replace(root, guard.RUNTIME, "import hashlib", "import hashlib as hash_alias")
|
|
path = root / guard.RUNTIME
|
|
path.write_text(
|
|
path.read_text(encoding="utf-8").replace(
|
|
"hashlib.sha256", "hash_alias.sha256"
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
assert guard.run_checks(root) == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"payload",
|
|
[
|
|
"hashlib = object()\n",
|
|
"def escape(os):\n return os.open('/tmp/x', 0)\n",
|
|
"from pathlib import Path as json\n",
|
|
],
|
|
)
|
|
def test_direct_module_bindings_cannot_be_shadowed(
|
|
tmp_path: Path, payload: str
|
|
) -> None:
|
|
root = _tree(tmp_path)
|
|
path = root / guard.RUNTIME
|
|
path.write_text(payload + path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
errors = guard.run_checks(root)
|
|
assert any("cannot be shadowed" in error for error in errors)
|
|
|
|
|
|
def test_existing_resolver_change_is_rejected(tmp_path: Path) -> None:
|
|
root = _tree(tmp_path)
|
|
path = root / next(iter(guard.RESOLVER_HASHES))
|
|
path.write_text(path.read_text(encoding="utf-8") + "\n# drift\n", encoding="utf-8")
|
|
assert any("existing resolver changed" in error for error in guard.run_checks(root))
|
|
|
|
|
|
def test_documentation_manifest_and_workflow_markers_are_required(tmp_path: Path) -> None:
|
|
root = _tree(tmp_path)
|
|
_replace(root, guard.PROTOCOL, "STANCE CLASSIFICATION UNMEASURED", "MEASURED")
|
|
_replace(
|
|
root,
|
|
guard.MANIFEST,
|
|
'id = "655-claim-standing-candidate-ledger"',
|
|
'id = "removed"',
|
|
)
|
|
_replace(
|
|
root,
|
|
guard.WORKFLOW,
|
|
"Check claim-standing candidate-ledger integration (#655)",
|
|
"removed",
|
|
)
|
|
errors = guard.run_checks(root)
|
|
assert any(str(guard.PROTOCOL) in error for error in errors)
|
|
assert any(str(guard.MANIFEST) in error for error in errors)
|
|
assert any(str(guard.WORKFLOW) in error for error in errors)
|
|
|
|
|
|
def test_phase_e_probe_offer_markers_are_required(tmp_path: Path) -> None:
|
|
root = _tree(tmp_path)
|
|
_replace(
|
|
root,
|
|
guard.PHASE_E_PROTOCOL,
|
|
"`ALL` is not permission to probe every claim",
|
|
"ALL rows may be probed",
|
|
)
|
|
errors = guard.run_checks(root)
|
|
assert any(str(guard.PHASE_E_PROTOCOL) in error for error in errors)
|
|
|
|
|
|
def test_stale_deferred_wiring_sentence_is_forbidden(tmp_path: Path) -> None:
|
|
root = _tree(tmp_path)
|
|
path = root / guard.PROTOCOL
|
|
path.write_text(
|
|
path.read_text(encoding="utf-8")
|
|
+ "\nA dedicated contract is deferred to the pipeline-wiring slice.\n",
|
|
encoding="utf-8",
|
|
)
|
|
errors = guard.run_checks(root)
|
|
assert any("must not appear" in error for error in errors)
|