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 argparse
|
||||||
import ast
|
import ast
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
@ -47,6 +48,7 @@ DYNAMIC_SQL_VALUE = "__teleo_dynamic_sql_value__"
|
||||||
UNREVIEWED_DYNAMIC_SQL = "__teleo_unreviewed_dynamic_sql__"
|
UNREVIEWED_DYNAMIC_SQL = "__teleo_unreviewed_dynamic_sql__"
|
||||||
REVIEWED_SQL_INTERPOLATION_CALLS = frozenset({"sql_array", "sql_integer", "sql_json_array", "sql_literal"})
|
REVIEWED_SQL_INTERPOLATION_CALLS = frozenset({"sql_array", "sql_integer", "sql_json_array", "sql_literal"})
|
||||||
QUERY_RUNNERS = frozenset({"run_psql", "psql_json_lines"})
|
QUERY_RUNNERS = frozenset({"run_psql", "psql_json_lines"})
|
||||||
|
REVIEWED_RUNTIME_SOURCE_SHA256 = "5f98218622803e0a1bbedc1e23e538d1f152ecb516e31a387a5eb39080aaf2cd"
|
||||||
DYNAMIC_CALL_BUILTINS = frozenset({"eval", "exec", "globals", "locals"})
|
DYNAMIC_CALL_BUILTINS = frozenset({"eval", "exec", "globals", "locals"})
|
||||||
READ_CAPABILITY = "read"
|
READ_CAPABILITY = "read"
|
||||||
STAGE_PROPOSAL_CAPABILITY = "stage-proposal"
|
STAGE_PROPOSAL_CAPABILITY = "stage-proposal"
|
||||||
|
|
@ -83,6 +85,15 @@ class QueryContractError(ValueError):
|
||||||
"""The runtime SQL cannot be safely reduced to an exact read contract."""
|
"""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:
|
def _is_reviewed_sql_interpolation(node: ast.expr) -> bool:
|
||||||
return (
|
return (
|
||||||
isinstance(node, ast.Call)
|
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."""
|
"""Return every statically rendered SQL statement and its capability."""
|
||||||
|
|
||||||
tree = ast.parse(source)
|
tree = ast.parse(source)
|
||||||
|
|
@ -319,14 +334,20 @@ def extract_runtime_query_literals(source: str) -> tuple[tuple[int, str, str], .
|
||||||
|
|
||||||
if not queries:
|
if not queries:
|
||||||
raise QueryContractError("runtime source contains no SQL query literals")
|
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]))
|
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."""
|
"""Return canonical SQL literals actually passed to the runtime query runners."""
|
||||||
|
|
||||||
queries: list[tuple[int, str]] = []
|
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)
|
normalized = _normalize_sql_identifier_syntax(sql, line)
|
||||||
if CANONICAL_RELATION.search(normalized):
|
if CANONICAL_RELATION.search(normalized):
|
||||||
queries.append((line, 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."""
|
"""Mechanically derive canonical relation columns from runtime SQL."""
|
||||||
|
|
||||||
observed: dict[tuple[str, str], set[str]] = defaultdict(set)
|
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]] = {}
|
aliases: dict[str, tuple[str, str]] = {}
|
||||||
explicit_starts: set[int] = set()
|
explicit_starts: set[int] = set()
|
||||||
for match in ALIASED_CANONICAL_RELATION.finditer(sql):
|
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))
|
joined = ", ".join(sorted(unqualified))
|
||||||
raise QueryContractError(f"line {line}: unqualified SQL identifiers prevent exact derivation: {joined}")
|
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()}
|
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)
|
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(
|
@pytest.mark.parametrize(
|
||||||
"query",
|
"query",
|
||||||
(
|
(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue