1
0
Fork 0

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
This commit is contained in:
Kjell Tore Guttormsen 2026-07-04 20:44:20 +02:00
commit 9b75bf0bb7
2 changed files with 296 additions and 0 deletions

View file

@ -0,0 +1,133 @@
"""contract — the write-time quarantine asserters (BRIEF §5/§6, PLAN §93).
The *differentiator*. Where the other modules detect and report, these functions
enforce an invariant and **raise**: they are the write-time analogue of runtime
least-privilege. They harden the call a pipeline makes around the quarantined,
tool-less transform the library itself makes no model call.
Three asserts, matching the reusable-contract checklist (BRIEF §6, steps 3-4):
* **(a) tool-less transform** :func:`assert_tool_less` verifies the model
request carries zero tools/functions/MCP servers. A successful injection then
has nothing to act with (checklist step 3).
* **(b) per-stage credential allowlist** :func:`assert_credential_allowlist`
verifies the process env holds no credential beyond the ones this stage is
entitled to: the enrichment stage sees only the model key, never the publish
credential (checklist step 4).
* **(c) capability isolation** :func:`scoped_env` produces a correctly scoped
env so a hijacked stage cannot even read a credential it was never granted;
the assert then passes by construction.
Reference: ``claude-code-llm-wiki`` ``tools/wiki_ingest/enrich.py``
``assert_quarantine`` a pipeline-specific quarantine gate, generalized here
into framework-agnostic, reusable pieces.
Two safety choices are deliberate and inherited from the reference:
* Credential detection is **name-based**; env *values* are never read into the
assert. Value-scanning is :mod:`output`'s job for emitted text, not the env.
* A raised :class:`ContractViolation` names only the offending *key* never a
credential value so the exception is safe to route to an alert channel
(minimal-alert payloads, BRIEF §6, step 8).
"""
from __future__ import annotations
import re
from typing import Iterable, Mapping
class ContractViolation(Exception):
"""A write-time security-contract invariant was violated.
Carries a machine-readable :attr:`code` and a :attr:`details` tuple of the
offending identifiers (tool-surface keys or env credential names) so a caller
can build a minimal, content-free alert payload. The message and details
never contain a credential value.
"""
def __init__(self, message: str, *, code: str, details: tuple[str, ...] = ()):
super().__init__(message)
self.code = code
self.details = details
# --- (a) tool-less transform -----------------------------------------------
# Populated tool surface across Anthropic + OpenAI request shapes. An empty
# list / None is genuinely tool-less; only a *populated* key is a violation.
_TOOL_KEYS = ("tools", "functions", "tool_choice", "function_call", "mcp_servers")
def assert_tool_less(request: Mapping[str, object]) -> None:
"""Assert the model request carries no tool surface (checklist step 3).
Raises :class:`ContractViolation` (code ``"tool_present"``) if any of
``tools``, ``functions``, ``tool_choice``, ``function_call`` or
``mcp_servers`` is present *and populated*. ``request`` is the request
kwargs mapping framework-agnostic, no SDK type required.
"""
offending = tuple(sorted(k for k in _TOOL_KEYS if request.get(k)))
if offending:
raise ContractViolation(
"model request carries a tool surface: %s" % ", ".join(offending),
code="tool_present",
details=offending,
)
# --- (b) per-stage credential allowlist ------------------------------------
# Credential-shaped env-var names, matched on the underscore-delimited word
# boundary so ``SECRETARY_NAME`` does not false-positive. Ported verbatim from
# the reference pipeline's proven ``CREDENTIAL_NAME_RE``.
CREDENTIAL_NAME_RE = re.compile(
r"(^|_)(API_?KEY|APIKEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?)(_|$)"
)
def credential_env_names(env: Mapping[str, str]) -> tuple[str, ...]:
"""Return the sorted env-var names that look like credentials.
Name-based only: values are never read. ``PATH``/``HOME``/``LANG`` and other
non-credential vars are ignored.
"""
return tuple(sorted(
name for name in env if CREDENTIAL_NAME_RE.search(name.upper())
))
def assert_credential_allowlist(env: Mapping[str, str], allowed: Iterable[str]) -> None:
"""Assert the env holds no credential beyond ``allowed`` (checklist step 4).
Least-privilege: the credential set must be a *subset* of ``allowed``
holding fewer than allowed is safe; holding even one more raises
:class:`ContractViolation` (code ``"credential_leak"``). The raised message
names only the offending keys, never their values.
"""
allowlist = set(allowed)
leaked = tuple(
name for name in credential_env_names(env) if name not in allowlist
)
if leaked:
raise ContractViolation(
"env carries off-allowlist credential(s): %s" % ", ".join(leaked),
code="credential_leak",
details=leaked,
)
# --- (c) capability isolation (env scoping) --------------------------------
def scoped_env(env: Mapping[str, str], allowed: Iterable[str]) -> dict[str, str]:
"""Return a copy of ``env`` with off-allowlist credentials removed.
Non-credential vars (``PATH`` etc.) pass through so the scoped env still
works for a subprocess; only credential-shaped keys outside ``allowed`` are
dropped. The input is not mutated. By construction,
``assert_credential_allowlist(scoped_env(env, a), a)`` always passes a
hijacked stage cannot read a credential it was never granted.
"""
allowlist = set(allowed)
leaked = set(credential_env_names(env)) - allowlist
return {name: value for name, value in env.items() if name not in leaked}