feat(wiring): §6 bookends (prepare_input/screen_output) + full public API + README v0.1 (TDD)
Module 11 (final) — the top-level wiring. The library never makes the model call (no SDK imported by the core), so the public surface is the toolkit plus two library-side bookends around the caller's tool-less transform (Form 1, chosen with the operator over an export-only toolkit and a full orchestrator — the bookends fit existing pipelines with least friction, encode the two halves the library can stand for, and impose no control flow): - prepare_input(text, source=INPUT) -> PreparedInput(fenced, nonce, report): §6 steps 1-2, sanitize THEN fence (carrier can never smuggle a forged delimiter). Merged report carries both steps' findings; renders no disposition. - screen_output(text, policy, *, provenance, transform_failed) -> DispositionResult: §6 steps 6-7, scan_output under guard() so a scanner error fails CLOSED (FAIL_SECURE, never a silent persist). transform_failed routes the compound forced-fallback rule. - __all__ exports the full framework-agnostic surface: detectors, result types, disposition machinery + presets, contract asserters, the grounding seam. Docs: README refreshed from the stale "brief / pre-implementation" line to a v0.1 alpha status with a Form-1 quickstart, the §6 adopt-this checklist, and an honest -limitations section (structural unsolvability at the text layer; semantic poisoning invisible to lexicon+entropy; text-only, no multimodal). CHANGELOG seeded; CLAUDE.md remote/status lines corrected (remote IS set, no longer brief-stage). 10 wiring tests (public surface, prepare_input compose, screen_output fail-closed + compound). 189 green (showcase + corpora follow). 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:
parent
1e63643157
commit
0af8f68cae
5 changed files with 410 additions and 10 deletions
|
|
@ -5,7 +5,141 @@ packages the ingestion-side security contract — sanitize -> fence -> tool-less
|
|||
quarantined transform -> per-stage capability isolation -> scan output before
|
||||
persist -> fail-secure — as composable, stdlib-first, framework-agnostic code.
|
||||
|
||||
The public API is wired up as modules land; see docs/PLAN.md for the build order.
|
||||
The library never makes the model call itself (no SDK is imported by the core),
|
||||
so the public surface is a **toolkit plus two bookends** around the caller's
|
||||
tool-less transform (BRIEF §6):
|
||||
|
||||
prepared = prepare_input(untrusted_content) # §6 steps 1-2: sanitize + fence
|
||||
output = your_model(prepared.fenced) # §6 step 3: tool-less, caller's job
|
||||
decision = screen_output(output, policy) # §6 steps 6-7: scan + dispose
|
||||
|
||||
if decision.disposition is Disposition.FAIL_SECURE:
|
||||
raise SystemExit # halt + alert; never persist (§6 steps 7-8)
|
||||
|
||||
The individual detectors (:func:`sanitize`, :func:`scan_lexicon`,
|
||||
:func:`scan_output`, ...), the contract asserters (:func:`assert_tool_less`,
|
||||
:func:`scoped_env`, ...) and the disposition machinery are all exported for
|
||||
pipelines that compose the checklist themselves. See ``docs/PLAN.md`` for the
|
||||
build order and ``docs/BRIEF.md`` §6 for the contract this wiring encodes.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .report import Finding, Report, Severity, Source, severity_rank
|
||||
from .sanitize import sanitize, SanitizeResult
|
||||
from .entropy import scan_entropy, EntropyResult, DecodedBlob
|
||||
from .lexicon import scan_lexicon, load_lexicon, LexiconPattern
|
||||
from .fence import fence, FenceResult
|
||||
from .neutralize import neutralize, NeutralizeResult
|
||||
from .output import scan_output, scan_secret_egress
|
||||
from .disposition import (
|
||||
decide,
|
||||
guard,
|
||||
Policy,
|
||||
Trust,
|
||||
Provenance,
|
||||
Disposition,
|
||||
DispositionResult,
|
||||
PRESET_TRUSTED_SOURCE,
|
||||
PRESET_USER_UPLOAD,
|
||||
)
|
||||
from .contract import (
|
||||
assert_tool_less,
|
||||
assert_credential_allowlist,
|
||||
credential_env_names,
|
||||
scoped_env,
|
||||
ContractViolation,
|
||||
)
|
||||
from .grounding import (
|
||||
SourceGroundingCheck,
|
||||
no_grounding_check,
|
||||
DEFAULT_GROUNDING_CHECK,
|
||||
)
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
|
||||
# --- §6 bookends: the two library-side halves around the transform ---------
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PreparedInput:
|
||||
"""Untrusted content made ready for a tool-less transform (§6 steps 1-2).
|
||||
|
||||
``fenced`` is the sanitized, spotlight-fenced text to hand the model.
|
||||
``nonce`` is the per-call fence delimiter — a caller may check for it in the
|
||||
output to detect a fence breakout. ``report`` merges the sanitize and fence
|
||||
findings so the caller can gate on the *input* too, not only the output.
|
||||
"""
|
||||
|
||||
fenced: str
|
||||
nonce: str
|
||||
report: Report
|
||||
|
||||
|
||||
def prepare_input(text: str, source: Source = Source.INPUT) -> PreparedInput:
|
||||
"""Sanitize then fence untrusted ``text`` (BRIEF §6 steps 1-2).
|
||||
|
||||
Strips carrier classes first (:func:`sanitize`), then spotlight-fences the
|
||||
cleaned payload in a randomized per-call delimiter (:func:`fence`) — sanitize
|
||||
*before* fence so a carrier can never smuggle a forged delimiter. The merged
|
||||
report carries both steps' findings; ``prepare_input`` itself renders no
|
||||
disposition (design principle 4 — the caller decides).
|
||||
"""
|
||||
sanitized = sanitize(text, source)
|
||||
fenced = fence(sanitized.text, source)
|
||||
report = Report()
|
||||
report.extend(sanitized.report.findings)
|
||||
report.extend(fenced.report.findings)
|
||||
return PreparedInput(fenced=fenced.text, nonce=fenced.nonce, report=report)
|
||||
|
||||
|
||||
def screen_output(
|
||||
text: str,
|
||||
policy: Policy,
|
||||
*,
|
||||
provenance: Provenance = Provenance.PROSE,
|
||||
transform_failed: bool = False,
|
||||
) -> DispositionResult:
|
||||
"""Scan the emitted ``text`` and dispose it, failing *closed* (§6 steps 6-7).
|
||||
|
||||
Runs :func:`scan_output` (lexicon + entropy + decode-and-rescan + secret
|
||||
egress) under :func:`guard`, so a scanner that errors on crafted input yields
|
||||
``FAIL_SECURE`` rather than an auto-persist — an un-scannable artifact is
|
||||
never committed. ``transform_failed=True`` (the caller's model call raised or
|
||||
fell back) plus any finding is treated as a probable forced-fallback attack
|
||||
and also halts, regardless of trust tier.
|
||||
"""
|
||||
return guard(
|
||||
lambda: scan_output(text, source=Source.OUTPUT),
|
||||
policy,
|
||||
provenance=provenance,
|
||||
transform_failed=transform_failed,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
# shared types
|
||||
"Finding", "Report", "Severity", "Source", "severity_rank",
|
||||
# input-side detectors + result types
|
||||
"sanitize", "SanitizeResult",
|
||||
"scan_entropy", "EntropyResult", "DecodedBlob",
|
||||
"scan_lexicon", "load_lexicon", "LexiconPattern",
|
||||
"fence", "FenceResult",
|
||||
"neutralize", "NeutralizeResult",
|
||||
# output-side
|
||||
"scan_output", "scan_secret_egress",
|
||||
# disposition
|
||||
"decide", "guard", "Policy", "Trust", "Provenance",
|
||||
"Disposition", "DispositionResult",
|
||||
"PRESET_TRUSTED_SOURCE", "PRESET_USER_UPLOAD",
|
||||
# contract asserters
|
||||
"assert_tool_less", "assert_credential_allowlist",
|
||||
"credential_env_names", "scoped_env", "ContractViolation",
|
||||
# grounding seam
|
||||
"SourceGroundingCheck", "no_grounding_check", "DEFAULT_GROUNDING_CHECK",
|
||||
# §6 bookends
|
||||
"prepare_input", "screen_output", "PreparedInput",
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue