uofa-demo / src /uofa_cli /interrogate /forbidden.py
cloudronin's picture
push build context (uofa source + packs + space app)
a28ec65 verified
Raw
History Blame Contribute Delete
5.2 kB
"""The interrogation firewall — the single source of truth for forbidden tokens.
SIP *measures*; it never *judges*. A SIP evidence bundle must therefore never
carry a verdict: no pass/fail, accepted, validated, credible, score, or any
other field that encodes a credibility decision. The credibility decision
belongs to the practitioner and the COU acceptance criteria, never to the
measurement instrument (``SIP_Evidence_Contract_Spec_v0_1.md`` §8).
This module is the ONE place the forbidden vocabulary is defined. Every other
enforcement point imports ``FORBIDDEN_TOKENS`` from here so the schema, the
``interrogate`` command output check, the CI ``firewall_guard.py`` script, and
the firewall tests can never drift from one another:
- ``specs/sip_evidence_bundle_schema.json`` rejects any bundle whose property
names intersect this list (and rejects unknown properties outright via
``additionalProperties: false``); a lockstep test asserts the schema's
root denylist equals this list.
- ``uofa interrogate`` asserts none of these tokens name a field in its output.
- ``dev/tools/scripts/firewall_guard.py`` scans the emitted surface for them.
- ``tests/interrogate/`` parametrizes over this list.
See ``AGENTS.md`` §12 (the interrogation firewall).
Matching is on **exact property names**, not substrings: ``decision`` is
forbidden as a bundle field, but ``parentModelSnapshot.parentDecision`` — the
recorded decision of the *parent* COU, carried as inherited provenance per
SIP §5.6 — is legitimate. Substring matching would false-positive on it.
"""
from __future__ import annotations
# Verdict / decision / rating vocabulary that must never name a field in a SIP
# evidence bundle. Exact, lowercase-or-camel as authored; see module docstring
# for why matching is exact-property-name, not substring.
FORBIDDEN_TOKENS: tuple[str, ...] = (
"verdict",
"passFail",
"pass_fail",
"accepted",
"rejected",
"validated",
"credible",
"credibilityIndex",
"decision",
"outcome",
"certify",
"certified",
"score",
"rating",
"approved",
)
def forbidden_property_names_constraint() -> dict:
"""Return the JSON-Schema fragment that bans forbidden property names.
The schema embeds this fragment (``propertyNames``) at the root and on
every freeform object. A test asserts the on-disk schema's root denylist
equals ``list(FORBIDDEN_TOKENS)`` so the two cannot drift.
"""
return {"not": {"enum": list(FORBIDDEN_TOKENS)}}
# The regions where decision/action content is legitimate (Addendum A4/A5,
# generalized for the pack-shaped legs): top-level blocks each governed by their
# OWN signature scope, not the denylist. ``engineerDecision`` is the original (a
# signed human decision); ``guardrailAction`` is the guardrail leg's output (§6);
# downstream verified-outcome labels can add more. Each is excluded from the
# measurement signature and exempt from the measurement-region denylist —
# additive content in its own scope, never mixed into the measurement region.
DECISION_BLOCK_KEY = "engineerDecision"
GUARDRAIL_BLOCK_KEY = "guardrailAction"
ACTION_REGION_KEYS: tuple[str, ...] = (DECISION_BLOCK_KEY, GUARDRAIL_BLOCK_KEY)
def find_forbidden_property_names(obj, _path: str = "$"):
"""Yield ``(json_path, token)`` for every forbidden property name in ``obj``.
Walks dicts/lists recursively, matching property *names* exactly (so
``parentDecision`` is never flagged for containing ``decision``). Used by
the command-level firewall check and the CI guard to inspect an emitted
bundle structurally rather than by substring grep.
"""
forbidden = set(FORBIDDEN_TOKENS)
if isinstance(obj, dict):
for key, value in obj.items():
if key in forbidden:
yield (f"{_path}.{key}", key)
yield from find_forbidden_property_names(value, f"{_path}.{key}")
elif isinstance(obj, list):
for index, item in enumerate(obj):
yield from find_forbidden_property_names(item, f"{_path}[{index}]")
def find_forbidden_in_measurement_region(bundle):
"""Forbidden property names anywhere EXCEPT the top-level action-region blocks.
The signature-scoped firewall (Addendum A5, superseding the flat denylist):
decision/action content is valid only inside a verifying-signed action-region
block (``ACTION_REGION_KEYS`` — ``engineerDecision``, ``guardrailAction``, …)
and is forbidden everywhere else — the "measurement region". This walker
enforces the "everywhere else" half. Only the *top-level* action-region blocks
are exempt; such a block smuggled deeper (e.g. inside ``measurements``) is
still fully scanned, so it cannot be used to bypass the firewall.
"""
if not isinstance(bundle, dict):
yield from find_forbidden_property_names(bundle)
return
forbidden = set(FORBIDDEN_TOKENS)
exempt = set(ACTION_REGION_KEYS)
for key, value in bundle.items():
if key in exempt:
continue
if key in forbidden:
yield (f"$.{key}", key)
yield from find_forbidden_property_names(value, f"$.{key}")