Pin reviewed Leoclean runtime SQL source
This commit is contained in:
parent
87580c219f
commit
6e4287987a
2 changed files with 59 additions and 4 deletions
|
|
@ -12,6 +12,7 @@ from __future__ import annotations
|
|||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
|
@ -47,6 +48,7 @@ DYNAMIC_SQL_VALUE = "__teleo_dynamic_sql_value__"
|
|||
UNREVIEWED_DYNAMIC_SQL = "__teleo_unreviewed_dynamic_sql__"
|
||||
REVIEWED_SQL_INTERPOLATION_CALLS = frozenset({"sql_array", "sql_integer", "sql_json_array", "sql_literal"})
|
||||
QUERY_RUNNERS = frozenset({"run_psql", "psql_json_lines"})
|
||||
REVIEWED_RUNTIME_SOURCE_SHA256 = "5f98218622803e0a1bbedc1e23e538d1f152ecb516e31a387a5eb39080aaf2cd"
|
||||
DYNAMIC_CALL_BUILTINS = frozenset({"eval", "exec", "globals", "locals"})
|
||||
READ_CAPABILITY = "read"
|
||||
STAGE_PROPOSAL_CAPABILITY = "stage-proposal"
|
||||
|
|
@ -83,6 +85,15 @@ class QueryContractError(ValueError):
|
|||
"""The runtime SQL cannot be safely reduced to an exact read contract."""
|
||||
|
||||
|
||||
def _assert_exact_runtime_source(source: str) -> None:
|
||||
observed = hashlib.sha256(source.encode("utf-8")).hexdigest()
|
||||
if observed != REVIEWED_RUNTIME_SOURCE_SHA256:
|
||||
raise QueryContractError(
|
||||
"runtime query surface differs from the exact reviewed runtime source: "
|
||||
f"expected={REVIEWED_RUNTIME_SOURCE_SHA256}, observed={observed}"
|
||||
)
|
||||
|
||||
|
||||
def _is_reviewed_sql_interpolation(node: ast.expr) -> bool:
|
||||
return (
|
||||
isinstance(node, ast.Call)
|
||||
|
|
@ -216,7 +227,11 @@ def _is_reviewed_psql_json_lines_forwarder(
|
|||
)
|
||||
|
||||
|
||||
def extract_runtime_query_literals(source: str) -> tuple[tuple[int, str, str], ...]:
|
||||
def extract_runtime_query_literals(
|
||||
source: str,
|
||||
*,
|
||||
enforce_source_pin: bool = True,
|
||||
) -> tuple[tuple[int, str, str], ...]:
|
||||
"""Return every statically rendered SQL statement and its capability."""
|
||||
|
||||
tree = ast.parse(source)
|
||||
|
|
@ -319,14 +334,20 @@ def extract_runtime_query_literals(source: str) -> tuple[tuple[int, str, str], .
|
|||
|
||||
if not queries:
|
||||
raise QueryContractError("runtime source contains no SQL query literals")
|
||||
if enforce_source_pin:
|
||||
_assert_exact_runtime_source(source)
|
||||
return tuple(sorted(queries, key=lambda item: item[0]))
|
||||
|
||||
|
||||
def extract_canonical_query_literals(source: str) -> tuple[tuple[int, str], ...]:
|
||||
def extract_canonical_query_literals(
|
||||
source: str,
|
||||
*,
|
||||
enforce_source_pin: bool = True,
|
||||
) -> tuple[tuple[int, str], ...]:
|
||||
"""Return canonical SQL literals actually passed to the runtime query runners."""
|
||||
|
||||
queries: list[tuple[int, str]] = []
|
||||
for line, sql, _capability in extract_runtime_query_literals(source):
|
||||
for line, sql, _capability in extract_runtime_query_literals(source, enforce_source_pin=enforce_source_pin):
|
||||
normalized = _normalize_sql_identifier_syntax(sql, line)
|
||||
if CANONICAL_RELATION.search(normalized):
|
||||
queries.append((line, normalized))
|
||||
|
|
@ -564,7 +585,7 @@ def derive_runtime_read_columns(source: str) -> dict[tuple[str, str], frozenset[
|
|||
"""Mechanically derive canonical relation columns from runtime SQL."""
|
||||
|
||||
observed: dict[tuple[str, str], set[str]] = defaultdict(set)
|
||||
for line, sql in extract_canonical_query_literals(source):
|
||||
for line, sql in extract_canonical_query_literals(source, enforce_source_pin=False):
|
||||
aliases: dict[str, tuple[str, str]] = {}
|
||||
explicit_starts: set[int] = set()
|
||||
for match in ALIASED_CANONICAL_RELATION.finditer(sql):
|
||||
|
|
@ -600,6 +621,7 @@ def derive_runtime_read_columns(source: str) -> dict[tuple[str, str], frozenset[
|
|||
joined = ", ".join(sorted(unqualified))
|
||||
raise QueryContractError(f"line {line}: unqualified SQL identifiers prevent exact derivation: {joined}")
|
||||
|
||||
_assert_exact_runtime_source(source)
|
||||
return {relation: frozenset(columns) for relation, columns in observed.items()}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -672,6 +672,39 @@ def test_runtime_query_contract_rejects_a_new_unallowlisted_column() -> None:
|
|||
query_contract.verify_runtime_read_columns(mutated, expected)
|
||||
|
||||
|
||||
def test_runtime_query_contract_pins_the_exact_reviewed_runtime_source() -> None:
|
||||
runtime_path = Path(__file__).resolve().parents[1] / "hermes-agent" / "leoclean-bin" / "cloudsql_memory_tool.py"
|
||||
runtime_source = runtime_path.read_text(encoding="utf-8")
|
||||
|
||||
assert hashlib.sha256(runtime_source.encode("utf-8")).hexdigest() == query_contract.REVIEWED_RUNTIME_SOURCE_SHA256
|
||||
|
||||
|
||||
def test_runtime_query_contract_rejects_recursive_proposal_execution_cardinality_mutation() -> None:
|
||||
runtime_path = Path(__file__).resolve().parents[1] / "hermes-agent" / "leoclean-bin" / "cloudsql_memory_tool.py"
|
||||
runtime_source = runtime_path.read_text(encoding="utf-8")
|
||||
recursive_sql = ''' sql = """
|
||||
with recursive spam as (
|
||||
select 1 as n, null::jsonb as proposal
|
||||
union all
|
||||
select n + 1, kb_stage.stage_leoclean_proposal(
|
||||
'revise_claim', 'telegram', 'receipt:1', 'reason', '{}'::jsonb
|
||||
)
|
||||
from spam
|
||||
where n < 10
|
||||
)
|
||||
select proposal from spam;
|
||||
"""
|
||||
'''
|
||||
function_start = runtime_source.index("def propose_core_change")
|
||||
sql_start = runtime_source.index(' sql = f"""\n', function_start)
|
||||
sql_end = runtime_source.index('"""\n rows = psql_json_lines(', sql_start) + len('"""\n')
|
||||
mutated = runtime_source[:sql_start] + recursive_sql + runtime_source[sql_end:]
|
||||
assert mutated != runtime_source
|
||||
|
||||
with pytest.raises(query_contract.QueryContractError, match="exact reviewed runtime source"):
|
||||
query_contract.extract_runtime_query_literals(mutated)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"query",
|
||||
(
|
||||
|
|
|
|||
Loading…
Reference in a new issue