llm-ingestion-pipeline-secu.../tests/test_contract.py
Kjell Tore Guttormsen 9b75bf0bb7 feat(contract): tool-less + credential-allowlist + env-scoping quarantine asserters (TDD) [skip-docs]
Module 9 of the build order — the differentiator. Where the other modules
detect and report, these are pure functions that ENFORCE an invariant and
raise: the write-time analogue of runtime least-privilege. They harden the
call a pipeline makes around its quarantined transform; the library makes no
model call itself.

Three asserts, matching the reusable-contract checklist (BRIEF §6 steps 3-4):
- assert_tool_less(request): the model request carries no tool surface —
  tools/functions/tool_choice/function_call/mcp_servers, populated. Covers
  both Anthropic and OpenAI request shapes; empty/None is genuinely tool-less.
- assert_credential_allowlist(env, allowed): the process env holds no
  credential beyond this stage's allowlist (subset = least-privilege; the
  enrichment stage sees only the model key, never the publish credential).
- scoped_env(env, allowed): the isolation primitive — strips off-allowlist
  credentials, keeps PATH etc.; the assert then passes by construction.

Generalized from claude-code-llm-wiki tools/wiki_ingest/enrich.py
assert_quarantine (a pipeline-specific gate) into framework-agnostic pieces.
Two deliberate safety choices inherited from the reference: credential
detection is name-based (env VALUES are never read — value-scanning is the
output module's job); and a raised ContractViolation names only the offending
KEY, never a value, so it is safe to route to an alert channel (minimal-alert,
BRIEF §6 step 8). CREDENTIAL_NAME_RE ported verbatim from the proven reference.

23 new tests; 161 green total.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyRCQMocjZ6SmSQ6JidJ2k
2026-07-04 20:44:20 +02:00

163 lines
6 KiB
Python

"""Tests for contract — the write-time quarantine asserters (BRIEF §5/§6, PLAN §93).
These are the differentiator: pure functions that RAISE (unlike detectors, which
return a Report). They are the write-time analogue of runtime least-privilege —
they harden the call the pipeline makes; they make no model call themselves.
Reference: claude-code-llm-wiki tools/wiki_ingest/enrich.py assert_quarantine —
generalized here into framework-agnostic, reusable pieces.
"""
from __future__ import annotations
import pytest
from llm_ingestion_guard.contract import (
ContractViolation,
assert_tool_less,
assert_credential_allowlist,
credential_env_names,
scoped_env,
)
# --- ContractViolation shape -----------------------------------------------
def test_contract_violation_is_exception_with_code_and_details():
exc = ContractViolation("boom", code="tool_present", details=("tools",))
assert isinstance(exc, Exception)
assert exc.code == "tool_present"
assert exc.details == ("tools",)
assert "boom" in str(exc)
# --- (a) tool-less assert ---------------------------------------------------
def test_tool_less_passes_when_no_tool_keys():
# a bare messages request with no tool surface is tool-less.
assert assert_tool_less({"model": "claude-opus-4-8", "messages": []}) is None
@pytest.mark.parametrize("request_kwargs", [
{},
{"tools": []},
{"tools": None},
{"tool_choice": None},
{"functions": []},
{"mcp_servers": []},
])
def test_tool_less_passes_on_absent_or_empty_tool_surface(request_kwargs):
# empty/None tool surface is genuinely tool-less; only a *populated* one raises.
assert assert_tool_less(request_kwargs) is None
@pytest.mark.parametrize("request_kwargs,offending", [
({"tools": [{"name": "run_shell"}]}, "tools"),
({"functions": [{"name": "exec"}]}, "functions"), # OpenAI legacy shape
({"function_call": {"name": "exec"}}, "function_call"), # OpenAI legacy shape
({"tool_choice": {"type": "tool", "name": "x"}}, "tool_choice"),
({"mcp_servers": [{"url": "https://x"}]}, "mcp_servers"), # Anthropic MCP connector
])
def test_tool_less_raises_on_populated_tool_surface(request_kwargs, offending):
with pytest.raises(ContractViolation) as excinfo:
assert_tool_less(request_kwargs)
exc = excinfo.value
assert exc.code == "tool_present"
assert offending in exc.details
assert offending in str(exc)
def test_tool_less_reports_all_offending_keys_sorted():
with pytest.raises(ContractViolation) as excinfo:
assert_tool_less({"tools": [{"name": "a"}], "mcp_servers": [{"url": "u"}]})
assert excinfo.value.details == ("mcp_servers", "tools")
# --- (b) per-stage credential allowlist ------------------------------------
def test_credential_env_names_detects_credential_shaped_keys():
env = {
"ANTHROPIC_API_KEY": "x",
"FORGEJO_PUBLISH_TOKEN": "x",
"DB_PASSWORD": "x",
"MY_SECRET_VALUE": "x",
"PATH": "/usr/bin",
"HOME": "/home/u",
"LANG": "en_US.UTF-8",
"SECRETARY_NAME": "x", # not a credential — must not false-positive
}
assert credential_env_names(env) == (
"ANTHROPIC_API_KEY",
"DB_PASSWORD",
"FORGEJO_PUBLISH_TOKEN",
"MY_SECRET_VALUE",
)
def test_credential_allowlist_passes_when_only_allowlisted_credential_present():
env = {"ANTHROPIC_API_KEY": "sk-ant-xxx", "PATH": "/usr/bin", "HOME": "/home/u"}
assert assert_credential_allowlist(env, ("ANTHROPIC_API_KEY",)) is None
def test_credential_allowlist_passes_on_subset_holds_fewer_than_allowed():
# least-privilege is "holds no MORE than the allowlist"; holding fewer is safe.
env = {"PATH": "/usr/bin"}
assert assert_credential_allowlist(env, ("ANTHROPIC_API_KEY", "OTHER_KEY")) is None
def test_credential_allowlist_raises_on_off_allowlist_credential():
env = {"ANTHROPIC_API_KEY": "x", "FORGEJO_PUBLISH_TOKEN": "x"}
with pytest.raises(ContractViolation) as excinfo:
assert_credential_allowlist(env, ("ANTHROPIC_API_KEY",))
exc = excinfo.value
assert exc.code == "credential_leak"
assert exc.details == ("FORGEJO_PUBLISH_TOKEN",)
assert "FORGEJO_PUBLISH_TOKEN" in str(exc)
def test_credential_allowlist_never_leaks_the_secret_value_in_the_message():
# minimal-alert (BRIEF §6.8): the value must never appear in the raised message.
secret = "ghp_" + "a" * 36
env = {"PUBLISH_TOKEN": secret}
with pytest.raises(ContractViolation) as excinfo:
assert_credential_allowlist(env, ())
assert secret not in str(excinfo.value)
assert secret not in "".join(excinfo.value.details)
def test_credential_allowlist_reports_multiple_leaks_sorted():
env = {"AWS_SECRET_ACCESS_KEY": "x", "GITHUB_TOKEN": "x", "ANTHROPIC_API_KEY": "x"}
with pytest.raises(ContractViolation) as excinfo:
assert_credential_allowlist(env, ("ANTHROPIC_API_KEY",))
assert excinfo.value.details == ("AWS_SECRET_ACCESS_KEY", "GITHUB_TOKEN")
# --- (c) capability isolation (env scoping) --------------------------------
def test_scoped_env_strips_off_allowlist_credentials_keeps_the_rest():
env = {
"ANTHROPIC_API_KEY": "keep",
"FORGEJO_PUBLISH_TOKEN": "drop",
"PATH": "/usr/bin",
"HOME": "/home/u",
}
scoped = scoped_env(env, ("ANTHROPIC_API_KEY",))
assert scoped == {"ANTHROPIC_API_KEY": "keep", "PATH": "/usr/bin", "HOME": "/home/u"}
def test_scoped_env_does_not_mutate_input():
env = {"ANTHROPIC_API_KEY": "x", "FORGEJO_PUBLISH_TOKEN": "y"}
original = dict(env)
scoped_env(env, ("ANTHROPIC_API_KEY",))
assert env == original
def test_scoped_env_output_always_satisfies_the_allowlist_assert():
# the isolation guarantee: scoping then asserting always passes, by construction.
env = {
"ANTHROPIC_API_KEY": "x",
"FORGEJO_PUBLISH_TOKEN": "x",
"AWS_SECRET_ACCESS_KEY": "x",
"PATH": "/usr/bin",
}
allowed = ("ANTHROPIC_API_KEY",)
assert assert_credential_allowlist(scoped_env(env, allowed), allowed) is None