Skip to content

fix(schema): attest authoritative inference receipts - #3830

Merged
Sinity merged 3 commits into
masterfrom
feature/fix/schema-gate-enforcement
Aug 5, 2026
Merged

fix(schema): attest authoritative inference receipts#3830
Sinity merged 3 commits into
masterfrom
feature/fix/schema-gate-enforcement

Conversation

@Sinity

@Sinity Sinity commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

Bind schema inference and commit authority to a signed receipt from an external, non-archive ground-truth source.

Problem

The prior gate could accept archive/blob data as its own external ground truth, accept empty evidence, and permit schema commit or the legacy operator CLI to bypass receipt authorization. A receipt hash could also be recomputed after evidence replacement.

Solution

Reject archive-local evidence roots, require nonempty verified evidence, sign the canonical receipt with durable authority material, and require authorized receipts for generation and commit entrypoints. Add mutation-sensitive tests for every bypass path.

Verification

  • devtools test tests/unit/maintenance/test_schema_inference_gate.py passed: 74 tests.
  • devtools verify --quick passed before this branch was published; pre-push records its current quick baseline.

Ref polylogue-tnqqt

Summary by CodeRabbit

  • New Features

    • Added authorization receipts for schema generation and schema commits.
    • Added validation for receipt freshness, integrity, archive identity, schema compatibility, and successful status.
    • Added protections against concurrent changes during schema generation.
  • Bug Fixes

    • Prevented schema generation using stale, replaced, mismatched, or invalid authorization receipts.
    • Rejected unauthorized external data locations within managed archive areas.
  • Tests

    • Expanded coverage for authorization failures, stale data, immutable receipts, empty archives, and concurrent writers.

Sinity and others added 2 commits August 5, 2026 18:58
Problem: schema generation could run without proving that the archive was quiesced and that every schema tier matched the packaged schema.

What changed: add an archive-wide offline lease, content-bound immutable PASS receipts, live archive and tier identity validation, and a required receipt to the schema-generate command. Add command-route attack tests for missing, mismatched, replaced, stale, stale-tier, and concurrent-writer cases.

Compatibility/migration: schema-generate now requires --schema-inference-receipt. Existing aggregate generation receipts remain separate and are not authoritative gate receipts.

Co-Authored-By: Claude <noreply@anthropic.com>
Schema package persistence now consumes a fresh, HMAC-attested gate
receipt. The gate rejects empty evidence and archive-owned blob paths from
being used as external ground truth, while both persisting CLI paths hold
the archive exclusion lease through generation.

Verification: devtools test focused schema gate and command suites; devtools
verify --quick.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Sinity, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c846bfcd-a12c-443c-95d9-bfe5ba15081b

📥 Commits

Reviewing files that changed from the base of the PR and between 6f504cf and 92618fc.

📒 Files selected for processing (10)
  • devtools/schema_commit.py
  • devtools/schema_generate.py
  • polylogue/maintenance/schema_inference_gate.py
  • polylogue/schemas/operator/schema_inference.py
  • tests/infra/schema_inference.py
  • tests/unit/core/test_schema_generation.py
  • tests/unit/devtools/test_schema_commit_command.py
  • tests/unit/devtools/test_schema_inference_gate.py
  • tests/unit/devtools/test_schema_lab_commands.py
  • tests/unit/maintenance/test_schema_inference_gate.py
📝 Walkthrough

Walkthrough

Schema inference now uses immutable, attested receipts. The gate validates archive and tier identities, schema compatibility, freshness, paths, and quiescence before authorizing schema generation. Persistent CLI operations require a receipt.

Changes

Schema authorization flow

Layer / File(s) Summary
Archive identity and gate verdict
polylogue/maintenance/schema_inference_gate.py
The gate records archive and tier identities, validates external ground-truth roots, requires reconciled source data, and records schema and lease metadata.
Immutable receipt validation
polylogue/maintenance/schema_inference_gate.py, tests/unit/maintenance/test_schema_inference_gate.py
The gate writes immutable attested receipts and validates their digest, freshness, path, archive identity, tier identity, schema identity, and PASS status.
CLI authorization enforcement
devtools/schema_commit.py, devtools/schema_generate.py, polylogue/schemas/operator/schema_inference.py, tests/unit/devtools/*
Schema commit and generation require receipt paths for persistent operations. Generation runs inside the archive authorization context. Tests cover missing, stale, replaced, mismatched, and concurrent-writer receipts.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SchemaGate
  participant Archive
  participant Receipt
  participant SchemaCLI
  participant SchemaGenerator
  SchemaGate->>Archive: acquire exclusive quiescence
  SchemaGate->>Receipt: write immutable attested receipt
  SchemaCLI->>Receipt: validate receipt and archive identities
  Receipt-->>SchemaCLI: return authorization result
  SchemaCLI->>SchemaGenerator: generate schemas within authorization
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the schema change and the use of authoritative inference receipts.
Description check ✅ Passed The description covers the summary, problem, solution, and verification, but omits the required changelog entry for security and CLI changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/fix/schema-gate-enforcement

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@devtools/schema_generate.py`:
- Around line 119-120: Introduce a shared
resolve_authorized_archive_root(config) helper beside
authorize_schema_generation that returns the configured archive root while
preserving _ConfigStub compatibility. Replace the duplicated inline derivation
in devtools/schema_generate.py lines 119-120, devtools/schema_commit.py lines
106-107, and polylogue/schemas/operator/schema_inference.py line 142 with this
helper before authorization; retain the separate --db-path containment check in
schema_inference.py.

In `@polylogue/maintenance/schema_inference_gate.py`:
- Around line 530-565: Avoid recomputing tier file hashes during a single
schema-gate operation: update the identity collection flow around
_tier_schema_identity, _run_schema_inference_gate_locked, and
validate_schema_inference_gate_receipt to reuse content digests keyed by path
and file metadata (st_dev, st_ino, st_size, and st_mtime_ns), or collect them
once and pass them into the final snapshot. Preserve the existing identity and
validation results while ensuring each unchanged tier file is hashed only once
per gate run.
- Around line 1257-1262: Update authorize_schema_generation to enforce
single-use authorization rather than only validating the receipt: after
successful validation, atomically consume or claim its receipt_nonce in a
durable store, or require the validated receipt to be removed/renamed so
subsequent calls fail. Preserve schema_inference_quiescence and ensure failed
validation does not consume the authorization.

In `@polylogue/schemas/operator/schema_inference.py`:
- Around line 127-137: Update the parser definition for schema_inference_receipt
to mark --schema-inference-receipt as required, matching the existing argparse
pattern in related commands. Remove the redundant None check and ensure any
remaining command error output in this flow is written to stderr, consistent
with the other devtools commands.

In `@tests/unit/devtools/test_schema_commit_command.py`:
- Around line 28-32: Update the _allow_schema_generation test stub to record
each authorization call and its receipt argument, then assert non-dry-run commit
tests enter the context exactly once with the command-line receipt path. Add an
assertion that the dry-run path does not enter authorization, covering the with
authorize_schema_generation(...) flow in the commit command.

In `@tests/unit/devtools/test_schema_lab_commands.py`:
- Line 32: Move the shared _seed_archive helper out of
tests.unit.maintenance.test_schema_inference_gate into a shared fixture module
under tests/infra, then update both test_schema_lab_commands.py and
test_schema_inference_gate.py to import it from that module. Remove the
cross-package import so devtools test collection does not load the maintenance
test module.
- Around line 383-403: Update
test_schema_generate_rejects_replaced_or_stale_receipt to assert the exact
expected error for each mutation instead of accepting a disjunction of messages.
Preserve the replaced-receipt case for attestation failure, and test staleness
using a genuinely signed receipt passed to
validate_schema_inference_gate_receipt with an advanced now value beyond
RECEIPT_TTL (while respecting RECEIPT_CLOCK_SKEW), so the freshness-window logic
is exercised directly.
- Around line 355-365: Make
test_schema_generate_valid_route_consumes_fresh_gate_receipt deterministic by
requesting the frozen_clock fixture and patching the runtime module’s datetime
source used to evaluate receipt freshness. Set the frozen time consistently with
the authoritative receipt so the test does not depend on wall-clock timing; use
fixed_now only for expected timestamp construction where applicable.

In `@tests/unit/maintenance/test_schema_inference_gate.py`:
- Around line 186-193: Update the test helper _run so its default ground-truth
root mapping is applied only when ground_truth_roots is omitted, not when it is
explicitly an empty dict; use an optional/sentinel distinction while preserving
the existing default mapping for callers that do not supply the argument. This
ensures test_empty_archive_cannot_authorize_schema_inference exercises an
actually empty root set.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 653242b1-da6d-457a-8a33-c89e0b11a85d

📥 Commits

Reviewing files that changed from the base of the PR and between 3be104e and 6f504cf.

📒 Files selected for processing (7)
  • devtools/schema_commit.py
  • devtools/schema_generate.py
  • polylogue/maintenance/schema_inference_gate.py
  • polylogue/schemas/operator/schema_inference.py
  • tests/unit/devtools/test_schema_commit_command.py
  • tests/unit/devtools/test_schema_lab_commands.py
  • tests/unit/maintenance/test_schema_inference_gate.py

Comment thread devtools/schema_generate.py Outdated
Comment on lines 530 to 565
def _tier_schema_identity(archive_root: Path, location: ArchiveLocation) -> dict[str, object]:
tiers: dict[str, object] = {}
for tier, spec in ARCHIVE_TIER_SPECS.items():
path = location.active_index_path if tier is ArchiveTier.INDEX else archive_root / spec.filename
identity = (
location.active_index if tier is ArchiveTier.INDEX else location.configured_tier(cast(Any, tier.value))
)
path = identity.resolved_path
expected_schema_sha256 = _canonical_schema_digest(tier)
entry: dict[str, object] = {
"path": str(path),
"stable_id": identity.stable_id,
"expected_user_version": spec.version,
"durability": spec.durability,
"exists": path.exists(),
"actual_user_version": None,
"content_sha256": None,
"schema_sha256": None,
"expected_schema_sha256": expected_schema_sha256,
}
if path.exists():
try:
with open_readonly_connection(path) as conn:
entry["actual_user_version"] = int(conn.execute("PRAGMA user_version").fetchone()[0])
entry["schema_sha256"] = _sqlite_schema_digest(conn)
except sqlite3.Error as exc:
entry["error"] = str(exc)
try:
entry["content_sha256"] = _file_sha256(path)
except OSError as exc:
entry["error"] = str(exc)
entry["matches_expected"] = entry["actual_user_version"] == spec.version
if expected_schema_sha256 is not None:
entry["matches_expected"] = bool(entry["matches_expected"]) and (
entry["schema_sha256"] == expected_schema_sha256
)
tiers[tier.value] = entry

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Full-file hashing of every tier repeats on each identity collection.

_tier_schema_identity computes _file_sha256 for every tier file. _run_schema_inference_gate_locked calls it twice (Line 1282 and Line 1347), and validate_schema_inference_gate_receipt calls it again on each authorized CLI run (Line 1238). On a large archive this reads every tier database three or more times per operation.

Cache the per-path digest by (path, st_dev, st_ino, st_size, st_mtime_ns) inside one gate run, or collect content hashes once and reuse them for the final identity snapshot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@polylogue/maintenance/schema_inference_gate.py` around lines 530 - 565, Avoid
recomputing tier file hashes during a single schema-gate operation: update the
identity collection flow around _tier_schema_identity,
_run_schema_inference_gate_locked, and validate_schema_inference_gate_receipt to
reuse content digests keyed by path and file metadata (st_dev, st_ino, st_size,
and st_mtime_ns), or collect them once and pass them into the final snapshot.
Preserve the existing identity and validation results while ensuring each
unchanged tier file is hashed only once per gate run.

Comment on lines +1257 to +1262
@contextmanager
def authorize_schema_generation(archive_root: Path, receipt_path: Path) -> Iterator[dict[str, object]]:
"""Hold quiescence while authorizing one schema-generation scan."""

with schema_inference_quiescence(archive_root):
yield validate_schema_inference_gate_receipt(receipt_path, archive_root=archive_root)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for any consumption/replay tracking for receipt_nonce.
rg -n -C4 'receipt_nonce|consumed|nonce' --glob '*.py' -g '!tests/**'

Repository: Sinity/polylogue

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## schema_inference_gate.py outline"
ast-grep outline polylogue/maintenance/schema_inference_gate.py --view expanded | sed -n '1,220p'

echo "## relevant function definitions"
sed -n '1180,1395p' polylogue/maintenance/schema_inference_gate.py | cat -n | sed -n '1,230p'

echo "## search receipt_nonce in all tracked Python files"
git ls-files '*.py' | xargs rg -n -C3 'receipt_nonce|consumed|nonce' || true

Repository: Sinity/polylogue

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## exact production receipt_nonce occurrences"
git grep -n -C3 'receipt_nonce' -- '*.py' '!(tests/*)' || true

echo "## exact authorized schema generation contexts"
rg -n -C4 'authorize_schema_generation\(' --glob '*.py' tests || true

echo "## local receipt_nonce validation code block"
ast-grep run --pattern 'def validate_schema_inference_gate_receipt(...\n{
  $$$
}' --lang python polylogue/maintenance/schema_inference_gate.py || true

Repository: Sinity/polylogue

Length of output: 2145


Other (CWE-294): Authentication Bypass by Capture-replay

Reachability: Internal

Reachability path
● Entry
  tests/unit/devtools/test_schema_commit_command.py
│
▼
● Hop
  devtools/schema_commit.py:76
  main
│
▼
● Sink
  polylogue/maintenance/schema_inference_gate.py

Do not call authorize_schema_generation single-use without consuming the receipt.

authorize_schema_generation says it authorizes one scan, but it only calls validate_schema_inference_gate_receipt. That validator accepts the same PASS receipt on every invocation until RECEIPT_TTL expires. The generated receipt_nonce is not checked or recorded, so it does not prevent replay. Either enforce single use (consume the nonce/claim in a durable store or require callers to remove/rename the receipt after successful authorization) or update the docstring to allow reuse while the receipt is fresh and the archive identity is unchanged.
[semantic_category:broken_auth]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@polylogue/maintenance/schema_inference_gate.py` around lines 1257 - 1262,
Update authorize_schema_generation to enforce single-use authorization rather
than only validating the receipt: after successful validation, atomically
consume or claim its receipt_nonce in a durable store, or require the validated
receipt to be removed/renamed so subsequent calls fail. Preserve
schema_inference_quiescence and ensure failed validation does not consume the
authorization.

Comment thread polylogue/schemas/operator/schema_inference.py Outdated
Comment on lines +28 to +32
@contextmanager
def _allow_schema_generation(*_args: object, **_kwargs: object) -> Iterator[dict[str, object]]:
yield {}


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test proves that the commit path enters the authorization context.

_allow_schema_generation accepts any arguments and yields. Every non-dry-run test patches it, so removing the with authorize_schema_generation(...) block in devtools/schema_commit.py Line 107 would keep all these tests green. The only new negative test covers the missing --schema-inference-receipt argument, which is checked before authorization.

Record the call in the stub and assert it, and assert that a dry-run does not enter it.

💚 Proposed fix to pin the authorization call
+_authorized: list[tuple[object, ...]] = []
+
+
 `@contextmanager`
 def _allow_schema_generation(*_args: object, **_kwargs: object) -> Iterator[dict[str, object]]:
+    _authorized.append(_args)
     yield {}

Then assert len(_authorized) == 1 and that the recorded receipt path matches the argument passed on the command line.

Also applies to: 52-56

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/devtools/test_schema_commit_command.py` around lines 28 - 32,
Update the _allow_schema_generation test stub to record each authorization call
and its receipt argument, then assert non-dry-run commit tests enter the context
exactly once with the command-line receipt path. Add an assertion that the
dry-run path does not enter authorization, covering the with
authorize_schema_generation(...) flow in the commit command.

Comment thread tests/unit/devtools/test_schema_lab_commands.py Outdated
Comment on lines +355 to +365
def test_schema_generate_valid_route_consumes_fresh_gate_receipt(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
root, receipt = _authoritative_schema_gate(tmp_path, "valid")
monkeypatch.setattr(schema_generate, "get_config", lambda: _ConfigStub(root / "index.db", root))
monkeypatch.setattr(schema_generate, "infer_schema", _successful_inference)

assert schema_generate.main(["--provider", "chatgpt", "--schema-inference-receipt", str(receipt)]) == 0
assert "Generated schema package set for chatgpt" in capsys.readouterr().out

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the frozen_clock fixture and the uses_real_clock marker convention.
rg -n -C6 'def frozen_clock|uses_real_clock' --glob '*.py'

Repository: Sinity/polylogue

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tracked Python files with schema_lab_commands =="
git ls-files | rg '(^tests/.*schema_lab_commands\.py$|schema_generate|inference|receipt|frozen_clock|uses_real_clock)' || true

echo
echo "== target file snippet =="
target="tests/unit/devtools/test_schema_lab_commands.py"
if [ -f "$target" ]; then
  wc -l "$target"
  sed -n '320,420p' "$target" | cat -n -v
else
  echo "missing $target"
fi

echo
echo "== search target tests without glob =="
rg -n -C4 'test_schema_generate_valid_route_consumes_fresh_gate_receipt|test_schema_generate.*stale|validate_schema_inference_gate_receipt|schema_inference_receipt|generated_at|RECEIPT_TTL|time\.time|datetime\.now|utcnow|frozen_clock|uses_real_clock' -g '*.py' || true

Repository: Sinity/polylogue

Length of output: 8327


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository root files =="
git show refs/remotes/origin/HEAD:pyproject.toml >/tmp/pyproject-check.txt 2>/dev/null || true
if [ -f pyproject.toml ]; then
  sed -n '1,220p' pyproject.toml | cat -n -v
fi

echo
echo "== tests/unit/devtools/test_schema_inference_gate.py =="
wc -l tests/unit/devtools/test_schema_inference_gate.py
sed -n '1,260p' tests/unit/devtools/test_schema_inference_gate.py | cat -n -v

echo
echo "== tests/unit/infra/test_frozen_clock.py =="
wc -l tests/unit/infra/test_frozen_clock.py
sed -n '1,220p' tests/unit/infra/test_frozen_clock.py | cat -n -v

echo
echo "== tests/infra/frozen_clock.py =="
wc -l tests/infra/frozen_clock.py
sed -n '1,220p' tests/infra/frozen_clock.py | cat -n -v

echo
echo "== direct references in schema inference gate and schema generate =="
for f in devtools/schema_inference_gate.py devtools/schema_generate.py tests/infra/frozen_clock.py tests/unit/infra/test_frozen_clock.py; do
  echo "-- $f --"
  rg -n -C3 'datetime\.now|time\.time|utcnow|utcfromtimestamp|epoch|now\(|tz_utc|UTC|RECEIPT_TTL|generated_at|frozen_clock|uses_real_clock|pytest' "$f" || true
done

Repository: Sinity/polylogue

Length of output: 36148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo test conftest files =="
git ls-files | rg '(^|/)conftest\.py$' || true

echo
echo "== search uses_real_clock across tracked Python files only =="
python3 - <<'PY'
import subprocess, pathlib
files = subprocess.check_output(["git", "ls-files", "*.py"], text=True).splitlines()
matches = []
for f in files:
    txt = pathlib.Path(f).read_text(errors="ignore")
    if "uses_real_clock" in txt or "frozen_clock" in txt:
        matches.append((f, [(i, txt.splitlines()[i-1]) for i, line in enumerate(txt.splitlines(), start=1) if "uses_real_clock" in line or "frozen_clock" in line]))
for f, hits in matches:
    print(f"-- {f} --")
    for line, text in hits[:40]:
        print(f"{line}: {text}")
    if len(hits) > 40:
        print(f"... +{len(hits)-40}")
PY

echo
echo "== target test file includes and fixture signature context =="
sed -n '1,50p' tests/unit/devtools/test_schema_lab_commands.py | cat -n -v
sed -n '336,370p' tests/unit/devtools/test_schema_lab_commands.py | cat -n -v
sed -n '388,410p' tests/unit/devtools/test_schema_lab_commands.py | cat -n -v

Repository: Sinity/polylogue

Length of output: 29297


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tests/conftest.py =="
sed -n '1,130p' tests/conftest.py | cat -n -v

echo
echo "== tests/unit/infra/test_clock_guard.py =="
sed -n '1,90p' tests/unit/infra/test_clock_guard.py | cat -n -v

echo
echo "== pytest runtime source for frozen_clock guard =="
python3 - <<'PY'
import pathlib
fp = pathlib.Path("tests/unit/infra/test_clock_guard.py")
text = fp.read_text(errors="ignore")
lines = text.splitlines()
for i, line in enumerate(lines, 1):
    if "frozen_clock" in line or "uses_real_clock" in line or "check_time" in line:
        start=max(1,i-8); end=min(len(lines),i+12)
        print(f"\n--- around {i} ---")
        for n in range(start,end+1):
            print(f"{n}: {lines[n-1]}")
PY

Repository: Sinity/polylogue

Length of output: 21822


Use frozen_clock for timestamp-sensitive receipt tests.

test_schema_generate_valid_route_consumes_fresh_gate_receipt passes only because the receipt is evaluated immediately, and the stale branch at lines 395–396 relies on real wall-clock time being past the fixed 2020 timestamp. Keep datetime control deterministic by requesting frozen_clock and patching the relevant runtime module, or use fixed_now where the test only constructs expected timestamps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/devtools/test_schema_lab_commands.py` around lines 355 - 365, Make
test_schema_generate_valid_route_consumes_fresh_gate_receipt deterministic by
requesting the frozen_clock fixture and patching the runtime module’s datetime
source used to evaluate receipt freshness. Set the frozen time consistently with
the authoritative receipt so the test does not depend on wall-clock timing; use
fixed_now only for expected timestamp construction where applicable.

Source: Coding guidelines

Comment on lines +383 to +403
@pytest.mark.parametrize("mutation", ["replaced", "stale"])
def test_schema_generate_rejects_replaced_or_stale_receipt(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
mutation: str,
capsys: pytest.CaptureFixture[str],
) -> None:
root, receipt = _authoritative_schema_gate(tmp_path, mutation)
payload = json.loads(receipt.read_text(encoding="utf-8"))
if mutation == "replaced":
payload["receipt_nonce"] = "attacker-replacement"
else:
payload["generated_at"] = datetime(2020, 1, 1, tzinfo=UTC).isoformat()
payload["receipt_sha256"] = schema_inference_gate_receipt_digest(payload)
receipt.write_text(json.dumps(payload, sort_keys=True), encoding="utf-8")
monkeypatch.setattr(schema_generate, "get_config", lambda: _ConfigStub(root / "index.db", root))
monkeypatch.setattr(schema_generate, "infer_schema", pytest.fail)

assert schema_generate.main(["--provider", "chatgpt", "--schema-inference-receipt", str(receipt)]) == 1
error = capsys.readouterr().err
assert "digest mismatch" in error or "stale" in error or "attestation" in error

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

The "stale" case fails on attestation, so the receipt TTL stays untested.

validate_schema_inference_gate_receipt verifies the HMAC attestations at Line 1226 of polylogue/maintenance/schema_inference_gate.py, before the freshness window at Line 1233. The "stale" branch rewrites generated_at and recomputes receipt_sha256, but it cannot recompute the attestation MACs, so verification fails at the attestation step. RECEIPT_TTL and RECEIPT_CLOCK_SKEW are never exercised.

The disjunction at Line 403 hides this. It accepts three different messages, so each parametrized case passes for the wrong reason.

Assert the exact expected failure per case, and cover the TTL through validate_schema_inference_gate_receipt(..., now=...) with a genuinely signed receipt and an advanced now.

🧰 Tools
🪛 ast-grep (0.45.0)

[info] 396-396: use jsonify instead of json.dumps for JSON output
Context: json.dumps(payload, sort_keys=True)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/devtools/test_schema_lab_commands.py` around lines 383 - 403,
Update test_schema_generate_rejects_replaced_or_stale_receipt to assert the
exact expected error for each mutation instead of accepting a disjunction of
messages. Preserve the replaced-receipt case for attestation failure, and test
staleness using a genuinely signed receipt passed to
validate_schema_inference_gate_receipt with an advanced now value beyond
RECEIPT_TTL (while respecting RECEIPT_CLOCK_SKEW), so the freshness-window logic
is exercised directly.

Comment thread tests/unit/maintenance/test_schema_inference_gate.py
Resolve the authoritative archive root consistently across schema entrypoints, require the legacy writer's signed gate receipt at argument parsing, and make an explicit empty ground-truth mapping stay empty.

The expiry assertion validates a real signed receipt at a controlled time, and the shared archive fixture removes test-to-test private imports.

Co-Authored-By: Codex <noreply@openai.com>
@Sinity
Sinity merged commit 506d0f9 into master Aug 5, 2026
3 checks passed
@Sinity
Sinity deleted the feature/fix/schema-gate-enforcement branch August 5, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant