Disclaimer
This is a draft discussion paper circulated for peer review. It is independent work published in a personal capacity and does not constitute official guidance or policy of any government body. It does not mandate or recommend specific controls for any agency, system, or project. Views and analysis are the author's own.
Part II-A: Python Language Binding Reference¶
This section provides the Python-specific binding reference for the Wardline framework specification. It covers the annotation vocabulary, interface contract, enforcement mechanisms, and residual risks specific to Python. The parent specification (Part I) governs; this binding implements.
Normative status. Section A.3 (interface contract) is normative. All other sections are non-normative — they provide design rationale, worked examples, and implementation guidance.
A.1 Design history¶
This section is non-normative.
The wardline concept emerged through three iterations of structured adversarial deliberation using prompted AI agent teams. Iteration 1 asked whether Python's permissive defaults could be addressed at the language level (a stricter dialect, a transpiler, or a runtime enforcement layer). An agent team concluded unanimously that the approach was not viable — ecosystem orphaning, the adoption cliff, and the maintenance burden were each independently fatal.
Iteration 2 — operating with knowledge of the first team's conclusions but no other constraint — independently generated the concept that became the semantic boundary enforcer: a standalone AST-based analysis layer that extends Python's existing annotation machinery rather than creating a parallel system. The creative pivot — from "change the language" to "analyse the language" — was generated by the agent team, not by the human operator. This team's design was implemented as a pattern-matching enforcement gate and deployed in production on the case study codebase.
Iteration 3 used seven specialist agent perspectives to refine the design. Binary taint tracking was rejected by all seven agents; the team replaced it with the two-dimensional model tracking trust classification and validation status as orthogonal dimensions — now formalised in the parent specification.
Feasibility finding. The deployed predecessor validates the approach at the pattern-matching level: automated detection of common agentic code failure modes is technically feasible for Python, compatible with existing development workflows, and buildable at modest cost relative to typical internal tooling.
Posture: reference implementation, not product. The Python enforcement regime described in this document is not a single product. It is a composition of existing ecosystem tools — ruff for syntactic pattern detection, mypy for type-layer tier diagnostics — and a reference implementation that covers the analysis surface no existing tool addresses: tier-aware taint-flow tracking, structural verification, and governance orchestration. The specification is the durable artefact. The reference implementation proves the specification is implementable. But the specification is designed so that any tool author can implement a compatible Wardline-Core scanner, Wardline-Type plugin, or Wardline-Governance orchestrator independently. The regime matures when a governing body owns the specification, not the code.
A.2 Python language evaluation¶
This section is non-normative. It models how future binding authors should assess their language against the framework specification's evaluation criteria.
| Criterion | Assessment | Detail |
|---|---|---|
| Annotation expressiveness | Strong | Decorators can express all 17 annotation groups. Decorators are metadata-only, visible to ast.parse() without execution, and compose naturally. |
| Parse tree access | Strong | The ast standard library module ships with every CPython install. Zero external dependencies for full AST analysis. |
| Type system metadata | Moderate | typing.Annotated (Python 3.9+) enables field-level tier marking. However, type hints are optional and not enforced at runtime. |
| Structural typing | Moderate | typing.Protocol (Python 3.8+) enables structural subtyping. @runtime_checkable Protocols support isinstance() checks. However, Protocols are not widely adopted for trust semantics, and mypy adoption remains optional. |
| Runtime object model | Strong | Descriptors (__get__/__set__/__set_name__), __init_subclass__, and metaclasses provide rich runtime structural enforcement. Standard CPython machinery, not extensions. |
| Class hierarchy enforcement | Strong | __init_subclass__ fires at class definition time (import time), not at instance creation time. Violations caught at module load. |
| Serialisation boundary control | Weak | Static analysis cannot cross serialisation boundaries. json.loads() returns dict regardless of what was serialised. Descriptors provide partial runtime coverage, but the fundamental blind spot remains. |
| Tooling ecosystem | Strong | mypy, pyright, ruff, bandit, and extensive AST tooling. SARIF is the standard interchange format. |
A.2.1 Where Python falls short¶
Three significant limitations shape this language binding:
No linear types. Python cannot prevent aliasing of validated data. A ValidatedRecord can be assigned to multiple variables, passed to functions that mutate it, or stored in containers that mix tiers — and the type system cannot track which aliases are still valid. Languages with ownership models (Rust) or linear types can prevent this at the type level; Python cannot.
No compile-time enforcement. Type checking in Python is optional. A codebase can use typing.Annotated for tier marking and Protocol for trust-typed signatures, but nothing prevents a developer (or agent) from ignoring these annotations entirely. The AST scanner compensates by checking decorator contracts at CI time, but there is a gap between authoring time and CI feedback.
No ownership model. Python cannot prevent lower-tier data from being aliased as Tier 1 at runtime without active enforcement mechanisms (descriptors, runtime checks). In a language with ownership semantics, a tier promotion would consume the original value, making it structurally impossible to reference the pre-promotion data.
These limitations define the ceiling of assurance achievable through this language binding. Adopters should understand that Python's assurance ceiling is lower than what a language with ownership semantics and mandatory type checking could provide.
A.2.2 Ecosystem tool coverage¶
| Conformance Profile | Candidate Tool | Implementation Path | Fit |
|---|---|---|---|
| Advisory fast path (non-conformant) | ruff | Custom rule plugin — syntactic pattern matching for PY-WL-001 through PY-WL-005 | Strong — ruff's per-file AST rule architecture matches pattern detection directly. Advisory only: no manifest, no tier-graded SARIF |
| Wardline-Core (authoritative) | Bespoke scanner | Two-pass AST analysis with taint tracking, manifest consumption, SARIF output | Required — no existing tool provides tier-aware severity grading |
| Wardline-Type | mypy plugin | Plugin using mypy's type analysis hooks to understand Annotated tier metadata |
Strong — mypy's plugin API supports custom type metadata and diagnostic hooks |
| Wardline-Type (baseline) | pyright (no plugin) | Standard typing.Protocol and typing.Annotated mechanisms |
Moderate — structural conformance but no tier-flow analysis |
| Wardline-Governance | Bespoke CLI | Manifest validation, fingerprint baseline, SARIF aggregation, control-law reporting | Required — governance orchestration is wardline-specific |
A.3 Interface contract (Wardline-Core)¶
This section is normative.
Any tool that implements Wardline-Core rules for the Python regime — whether a ruff plugin, the reference scanner, a Semgrep rule pack, or a future competing implementation — MUST satisfy the following interface contract:
-
Manifest consumption. The tool MUST consume the wardline manifest (
wardline.yamland any overlays) and validate the manifest against the framework's JSON Schemas before producing findings. A tool that produces findings without validating the manifest is non-conformant. (Note:wardline.yamlis the trust topology manifest.wardline.tomlis the scanner's operational configuration. The two files serve different purposes.) -
Decorator discovery. The tool MUST discover wardline decorator syntax from the target codebase's AST — identifying which functions carry which decorators and extracting their arguments. The tool SHALL NOT rely on runtime introspection or dynamic attribute inspection as the primary discovery mechanism. The canonical decorator names and their argument schemas are defined in A.4.
-
Schema default recognition. The tool MUST recognise
schema_default()as a PY-WL-001 suppression marker. Calls wrapped inschema_default()where the default value matches the overlay's declared approved default are governed by the overlay declaration, not by PY-WL-001. -
SARIF output. The tool MUST produce findings in SARIF v2.1.0 with the wardline-specific property bags defined in the parent specification. The Python regime requires the following mandatory property bag keys on each
resultobject:Key Type Description wardline.rulestring Binding rule ID (e.g., PY-WL-001)wardline.taintStatestring Canonical taint state token (e.g., AUDIT_TRAIL)wardline.enclosingTierinteger Tier of the enclosing context (1, 2, 3, or 4) wardline.severitystring ERROR,WARNING, orSUPPRESSwardline.exceptionabilitystring UNCONDITIONAL,STANDARD,RELAXED, orTRANSPARENTwardline.analysisLevelinteger Analysis level that produced the finding (1, 2, or 3) wardline.exceptedboolean Whether an active exception covers this finding wardline.annotationGroupsinteger array Annotation groups active on the enclosing function -
Rule declaration. The tool MUST declare which rules it implements and MUST maintain golden corpus specimens for those rules.
-
Verification mode. The tool SHOULD support the
--verification-modeoutput profile for deterministic byte-identical output against the golden corpus.
A tool that satisfies this contract and implements at least one PY-WL rule is a partial Wardline-Core tool. A tool that implements all nine binding rules (PY-WL-001 through PY-WL-009) with tier-aware severity grading is a complete Wardline-Core tool.
Rule mapping. The nine Python binding rules derive from the eight framework rules (Part I) as follows. WL-001 splits into two binding rules because Python has two distinct access-with-fallback idioms (dict.get() and getattr()); all other framework rules map one-to-one with a numbering offset:
| Python Rule | Framework Rule | Pattern |
|---|---|---|
| PY-WL-001 | WL-001 (split) | Dict key access with fallback default (.get(), .setdefault(), collections.defaultdict) |
| PY-WL-002 | WL-001 (split) | Attribute access with fallback default (getattr() with default, hasattr() guards) |
| PY-WL-003 | WL-002 | Existence-checking as structural gate (if key in dict, hasattr() guards as validation proxy) |
| PY-WL-004 | WL-003 | Broad exception handlers swallowing errors (except Exception, bare except) |
| PY-WL-005 | WL-004 | Catching exceptions silently — no action taken in handler (except: pass, bare except with no re-raise or logging) |
| PY-WL-006 | WL-005 | Audit-critical writes inside broad exception handlers |
| PY-WL-007 | WL-006 | Runtime type-checking internal data (tier-dependent — suppressed at Tier 4) |
| PY-WL-008 | WL-007 | Validation boundary with no rejection path (structural verification) |
| PY-WL-009 | WL-008 | Semantic validation without prior shape validation (validation ordering) |
PY-WL-001 through PY-WL-005 are syntactic patterns detectable by per-file AST analysis (the ruff advisory path). PY-WL-006 through PY-WL-009 require semantic context — audit-path awareness, tier classification, structural verification, or validation ordering — and are implemented by the reference scanner.
Tools that detect wardline-relevant patterns without satisfying this contract — e.g., ruff rules that match .get() calls without consuming the manifest — are advisory tools, not Wardline-Core tools. Advisory tools provide useful early-warning feedback but their findings are not governance-grade.
A.4 Annotation vocabulary: design principles, mapping table, and rationale¶
This section is non-normative except where explicitly stated.
A.4.1 Design principles¶
Parasitic, not parallel. The decorators extend Python's existing machinery. They are standard decorators importable from a PyPI package. No custom syntax, no runtime overhead beyond attribute assignment, no framework lock-in.
Sparse annotation, dense inference. Developers annotate boundaries — where trust changes, where failure modes matter, where ordering is required. The scanner infers everything between boundaries. Target: ~50–100 decorators for an 80k-line codebase in the initial annotation pass.
Library, not framework. The decorator vocabulary is a reusable PyPI package. Projects pick what they need via wardline.toml configuration — unused decorator groups are ignored by the scanner.
Decorators as machine-readable institutional knowledge. Each decorator converts a prose-level institutional constraint ("audit records must not have fabricated defaults") into a machine-checkable declaration.
Coding posture per tier. The parent specification's authority tier model implies distinct programming styles:
| Tier | Posture | Philosophy |
|---|---|---|
| Tier 4 | Sceptical programming | Treat everything as hostile sludge. Validate structure first, normalise, reject. |
| Tier 3 | Guarded programming | Structure is trustworthy. Direct field access is safe; validate domain constraints before using values in business logic. |
| Tier 2 | Confident programming | Structure and domain meaning are trustworthy within the declared bounded context. Guard only cross-cutting concerns. |
| Tier 1 | Offensive programming (assert invariants; never silently recover) | Assume invariants, detonate on breach. Anomalies must surface immediately as faults. |
Minimum Python version: 3.12+. The scanner targets Python 3.12+ only. ast.Constant is the canonical node at this floor; ast.Match (3.10+) and ast.unparse() (3.9+) are available.
A.4.2 Decorator mapping table¶
The 17 annotation groups are defined as language-agnostic semantic requirements in the framework specification. This table provides the Python-specific decorator syntax. Decorators set _wardline_* metadata attributes on the decorated callable; they do almost nothing at runtime.
| # | Group | Python Decorator(s) | Signature / Parameters | Scanner Checks |
|---|---|---|---|---|
| 1 | Authority Tier Flow | @external_boundary |
(none) | Return value tagged TIER_4. Auto-detected for known external call sites but explicit annotation preferred. |
| 1 | @validates_shape |
(none) | Body must contain rejection path (WL-007). T4 → T3 constructor. | |
| 1 | @validates_semantic |
(none) | Body must contain rejection path (WL-007). Inputs must trace to @validates_shape output (WL-008/PY-WL-009). T3 → T2 constructor. Bounded context declared in overlay. |
|
| 1 | @validates_external |
(none) | Combined T4 → T2. Body must contain rejection path (WL-007). Must perform both structural and semantic checks. | |
| 1 | @tier1_read |
(none) | Body bans: .get() with defaults, getattr() with fallbacks, hasattr(), broad except. Return tagged TIER_1. |
|
| 1 | @audit_writer |
(none) | Call-site bans: enclosing swallowing except. Audit must dominate telemetry on shared execution paths. Fallback paths that bypass the audit call produce a finding. Return tagged TIER_1. |
|
| 1 | @authoritative_construction |
(none) | Same body restrictions as @tier1_read. Semantically equivalent to @audit_writer but for non-audit authoritative artefacts. Return tagged TIER_1. |
|
| 2 | Audit Primacy | @audit_critical |
(none) | Superset of @audit_writer — call sites must not have fallback paths that skip the audit call. |
| 3 | Plugin/Component Contract | @system_plugin |
(none) | Body bans top-level broad except. Allows narrower except for external calls and row-value operations. "Wrap your external calls; let your own bugs crash." |
| 4 | Data Provenance | @int_data |
(none) | AUDIT_TRAIL body restrictions. Return value is UNKNOWN_RAW unless composed with @restoration_boundary. Allow-list/deny-list on call targets. |
| 5 | Schema Contracts | @all_fields_mapped(source=Class) |
source: the class whose fields must all appear in the body |
Verifies every field on source appears as attribute access on the parameter. |
| 5 | @output_schema(fields=[...]) |
fields: list of output field names |
Field collision detection at call sites. | |
| 5 | (access-site) | schema_default(expr) |
Wraps a .get() expression |
Suppression marker for PY-WL-001. Scanner verifies overlay declaration, default value match, and validation boundary context. Part of Wardline-Core interface contract. |
| 6 | Layer Boundaries | @layer(N) |
N: integer layer number |
Import direction enforcement. Upward imports are findings. Hybrid: default layer from directory path via wardline.toml, decorator overrides per symbol. |
| 7 | Template Safety | @parse_at_init |
(none) | Call sites must be in __init__, __post_init__, or setup methods. Calls from per-row methods are findings. |
| 8 | Secret Handling | @handles_secrets |
(none) | Return tagged SECRET (orthogonal taint dimension). SECRET reaching logger, print, persistence without hashing is a finding. |
| 9 | Operation Semantics | @idempotent |
(none) | First state-modifying call must be preceded by existence/dedup guard. |
| 9 | @atomic |
(none) | Multiple state-modifying calls must be within transaction context. | |
| 9 | @compensatable(rollback=fn) |
rollback: reference to rollback function |
Rollback function must exist with compatible signature. | |
| 10 | Failure Mode | @fail_closed |
(none) | Same body restrictions as @tier1_read. Carries implicit @must_propagate. Severity lookups use AUDIT_TRAIL. |
| 10 | @fail_open |
(none) | Explicitly permits graceful degradation patterns. Composition requirement: must carry a trust classification decorator (WARNING if alone). | |
| 10 | @emits_or_explains |
(none) | Every return/exit path must reach an emit call or an explain/logging call. | |
| 10 | @exception_boundary |
(none) | Authorises exception handling from high-stakes call sites. Still subject to PY-WL-005. Placement governed via wardline.toml (lenient/controlled/strict modes). |
|
| 10 | @must_propagate |
(none) | Exceptions must propagate to an @exception_boundary. No intermediate catch-and-continue. |
|
| 10 | @preserve_cause |
(none) | Every raise X(...) in except blocks must include from clause. One-hop traversal into helper calls. |
|
| 11 | Data Sensitivity | @handles_pii(fields=[...]) |
fields: list of PII field names |
Named fields must not reach logger, error messages, unprotected persistence. |
| 11 | @handles_classified(level=str) |
level: classification level (e.g., "PROTECTED") |
No mixing with lower classification levels. No downgrading without @declassifies. |
|
| 11 | @declassifies(from_level=str, to_level=str) |
from_level, to_level: classification levels |
Body must contain rejection path. CODEOWNERS-protected. | |
| 12 | Determinism | @deterministic |
(none) | Body ban on non-deterministic stdlib calls (random, uuid4, datetime.now, set iteration). |
| 12 | @time_dependent |
(none) | Suppresses @deterministic-style findings. |
|
| 13 | Concurrency/Ordering | @thread_safe |
(none) | Body must protect shared mutable state or be pure. |
| 13 | @ordered_after(name) |
name: function name that must precede |
At call sites where both functions appear, named function must lexically precede. | |
| 13 | @not_reentrant |
(none) | Call graph cycle detection through the decorated function. | |
| 14 | Access/Attribution | @requires_identity |
(none) | Identity-typed parameter must appear in @audit_writer/@audit_critical call within body. |
| 14 | @privileged_operation |
(none) | Authorisation check must precede state-modifying call. | |
| 15 | Lifecycle/Scope | @test_only |
(none) | No production module may import this symbol. |
| 15 | @deprecated_by(date=str, replacement=str) |
date: expiry date; replacement: replacement function |
Post-expiry: blocking. Pre-expiry: advisory. | |
| 15 | @feature_gated(flag=str) |
flag: feature flag name |
Static reference counting; stale flag detection. | |
| 16 | Generic Trust Boundary | @trust_boundary(from_tier=N, to_tier=M) |
from_tier, to_tier: integers 1–4 |
Parameterised tier transition. Promotion requires rejection path. Skip-promotions to T1 are schema-invalid. |
| 16 | @data_flow(consumes=N, produces=M) |
consumes, produces: integers 1–4 |
Descriptive-only documentation marker. No enforcement. Advisory if produces > consumes. | |
| 17 | Restoration Boundaries | @restoration_boundary(...) |
restored_tier: int; institutional_provenance: str (opt); structural_evidence: bool; semantic_evidence: bool (opt); integrity_evidence: str (opt) |
Body must satisfy WL-007. Evidence must support claimed tier per the evidence matrix in the framework specification. Scanner demotes effective taint state when evidence is insufficient. |
Group 1 aliases and Group 16 equivalences. Group 1 decorators are convenience aliases for common Group 16 configurations:
| Group 1 Decorator | Equivalent Group 16 | Notes |
|---|---|---|
@external_boundary |
Sources TIER_4 (no from_tier) |
|
@validates_shape |
@trust_boundary(from_tier=4, to_tier=3) |
|
@validates_semantic |
@trust_boundary(from_tier=3, to_tier=2) |
|
@validates_external |
@trust_boundary(from_tier=4, to_tier=2) |
|
@tier1_read |
Sources TIER_1 (no from_tier) |
|
@audit_writer |
@trust_boundary(from_tier=2, to_tier=1) + call-site enforcement |
Audit-ordering semantics not expressible via @trust_boundary alone |
@authoritative_construction |
@trust_boundary(from_tier=2, to_tier=1) |
A.4.3 Non-obvious design rationale¶
Why Python uses decorator stacking. Python decorators compose naturally via stacking — @int_data above @restoration_boundary produces a function with both Group 4 body restrictions and Group 17 evidence verification. This is standard Python idiom. Each decorator sets its own _wardline_* metadata attributes independently. The scanner reads all attributes and applies each group's rules. No decorator needs awareness of the others in the stack.
Which groups share decorators and why. Groups 1 and 16 share the tier-transition concept — Group 1 decorators are aliases for common Group 16 configurations. This means @validates_shape and @trust_boundary(from_tier=4, to_tier=3) are semantically identical. The aliases exist for readability: most codebases use the Group 1 names; Group 16 exists for non-standard transitions. Groups 8 and 11 share the sensitivity.py module because both deal with data sensitivity (secrets vs. classification levels) and use the same taint propagation engine with different taint dimensions. Groups 9 and 10 share operations.py because both deal with function-level behavioural contracts.
functools.wraps preservation. When a wardline decorator is applied to a function, _wardline_* metadata attributes are set on the wrapper function. If the function is subsequently wrapped by another decorator that uses functools.wraps, the metadata attributes are preserved because functools.wraps copies the __wrapped__ attribute and updates __dict__. The scanner resolves decorated functions through __wrapped__ chains to find wardline metadata, ensuring decorators from third-party libraries (e.g., Flask's @route, pytest's @fixture) do not hide wardline annotations.
Metaclass and descriptor implications. Python's descriptor protocol (__get__/__set__/__set_name__) provides runtime structural enforcement that complements the static analysis layer. The key design decision: AuthoritativeField descriptors raise on access-before-set, making fabricated defaults structurally impossible for Tier 1 data at the Python object model level. This catches violations that the AST scanner cannot reach (dynamic dispatch, cross-module indirection, generated code). The limitation: __dict__ manipulation bypasses the descriptor's __set__ sentinel check — a fundamental constraint of Python's descriptor protocol. Similarly, __init_subclass__ fires at class definition time (import time) to enforce that subclass methods carry wardline decorators, but composition-based delegation (creating unannotated helper classes within annotated method bodies) bypasses this enforcement.
How @validates_external relates to the decomposed validators. @validates_external (combined T4→T2) performs both shape and semantic validation in a single function body. The model treats this as two logical transitions (T4→T3→T2) occurring within one function. The scanner must establish that the body performs both structural and semantic checks. The decomposed form (@validates_shape + @validates_semantic on separate functions) is preferred for large validators where the structural and semantic concerns are distinct. The combined form is appropriate when both checks are simple enough to co-locate without confusion. Stacking @validates_shape + @validates_semantic on the same function is contradictory (SCN-021) — use @validates_external for the combined case.
Body evaluation context for validation boundaries. At the first analysis level, the scanner evaluates pattern rules within validation boundary bodies using the severity lookups of the input tier — the tier the validator operates on, not the tier it produces:
| Decorator | Input tier | Body evaluation severity | PY-WL-003 (existence-checking) suppression |
|---|---|---|---|
@validates_shape |
TIER_4 | EXTERNAL_RAW | Yes — existence-checking is the purpose of shape validation |
@validates_semantic |
TIER_3 | SHAPE_VALIDATED | No — structural guarantees already established |
@validates_external |
TIER_4 | EXTERNAL_RAW | Yes — encompasses shape validation |
@fail_closed strictness ordering. When decorators compose, severity dominates exceptionability: ERROR > WARNING > SUPPRESS regardless of exceptionability class. Within the same severity, exceptionability is ordered UNCONDITIONAL > STANDARD > RELAXED. When @fail_closed is composed with a validation boundary decorator, body pattern rules fire at ERROR severity but exceptionability is capped at STANDARD — the findings remain governable. UNCONDITIONAL exceptionability is reserved for AUDIT_TRAIL-native contexts.
Exception translation boundaries. The failure mode decorators (@fail_closed, @fail_open, @emits_or_explains) govern what happens within a function. The exception propagation decorators (@exception_boundary, @must_propagate, @preserve_cause) govern what happens to exceptions after they leave the function. Without @exception_boundary, a @fail_closed function correctly raises on failure — and then a caller three frames up catches with except Exception: use_default(), defeating the @fail_closed intent entirely. @exception_boundary declares which functions are architecturally authorised to make terminal policy decisions about exceptions from high-stakes paths. @must_propagate carries implicit from @fail_closed and @audit_critical.
Contradictory combination detection (SCN-021). The scanner detects mutually exclusive decorator combinations as ERROR findings. The 29 detected combinations (26 contradictory, 3 suspicious) are:
| # | Combination | Type | Rationale |
|---|---|---|---|
| 1 | @fail_open + @fail_closed |
Contradictory | Mutually exclusive failure modes |
| 2 | @fail_open + @tier1_read |
Contradictory | Tier 1 requires offensive programming — fail-open is structurally incompatible |
| 3 | @fail_open + @audit_writer |
Contradictory | Audit writes must not silently degrade |
| 4 | @fail_open + @authoritative_construction |
Contradictory | Authoritative artefacts must not have fallback construction paths |
| 5 | @fail_open + @audit_critical |
Contradictory | Audit-critical paths must not have fallback paths |
| 6 | @external_boundary + @int_data |
Contradictory | External and internal data sources are mutually exclusive |
| 7 | @external_boundary + @tier1_read |
Contradictory | External data is Tier 4; Tier 1 reads are internal |
| 8 | @external_boundary + @authoritative_construction |
Contradictory | External data cannot be directly authoritative |
| 9 | @validates_shape + @validates_semantic |
Contradictory | Use @validates_external for combined T4→T2 |
| 10 | @validates_shape + @tier1_read |
Contradictory | Shape validation produces T3, not T1 |
| 11 | @validates_semantic + @external_boundary |
Contradictory | Semantic validation operates on T3 input, not T4 |
| 12 | @exception_boundary + @must_propagate |
Contradictory | Exception boundaries terminate; must-propagate requires forwarding |
| 13 | @idempotent + @compensatable |
Contradictory | Idempotent operations need no compensation |
| 14 | @deterministic + @time_dependent |
Contradictory | Time-dependent operations are inherently non-deterministic |
| 15 | @deterministic + @external_boundary |
Contradictory | External calls are non-deterministic by definition |
| 16 | @tier1_read + @restoration_boundary |
Contradictory | Tier 1 reads access existing authoritative data; restoration reconstructs from raw representation |
| 17 | @audit_writer + @restoration_boundary |
Contradictory | Audit writes create new records; restoration reconstructs existing ones |
| 18 | @fail_closed + @emits_or_explains |
Contradictory | Fail-closed raises on failure; emits-or-explains requires structured error output |
| 19 | @audit_critical + @fail_open |
Contradictory | (Alias of #5 — caught regardless of decorator ordering) |
| 20 | @validates_external + @validates_shape |
Contradictory | @validates_external already encompasses shape validation |
| 21 | @validates_external + @validates_semantic |
Contradictory | @validates_external already encompasses semantic validation |
| 22 | @int_data + @validates_shape |
Contradictory | Internal data uses restoration boundaries, not the T4→T3 shape-validation mechanism designed for external input |
| 23 | @preserve_cause + @exception_boundary |
Contradictory | (Alias of #12 — @preserve_cause implies propagation) |
| 24 | @compensatable + @audit_writer |
Contradictory | Audit writes must not be compensated (reversed) |
| 25 | @data_flow(produces=...) + @external_boundary |
Contradictory | External boundaries produce T4 data; data-flow produces declared-tier data |
| 26 | @system_plugin + @tier1_read |
Contradictory | Plugins receive external input; Tier 1 reads are internal |
| 27 | @fail_open + @deterministic |
Suspicious | Fail-open with fallback defaults may produce non-deterministic output |
| 28 | @compensatable + @deterministic |
Suspicious | Compensation introduces state changes that may affect determinism |
| 29 | @time_dependent + @idempotent |
Suspicious | Time-dependent operations may not be idempotent across invocations |
Severity matrix. The Python binding extends the parent specification's 8×8 severity matrix to 9×8 by splitting WL-001 into PY-WL-001 and PY-WL-002. No cell values are changed — the added PY-WL-002 row inherits WL-001's severity matrix entries with no additional SUPPRESS cells. The total is 26 UNCONDITIONAL cells (25 inherited from the framework matrix plus PY-WL-002's AUDIT_TRAIL entry). The Java binding (Part II-B) documents two cells where Java's type system structurally prevents certain violations, warranting SUPPRESS; Python's weaker type system does not support equivalent structural guarantees, so no cells are modified.
A.5 Type system and runtime enforcement¶
This section is non-normative.
Python uses three complementary enforcement mechanisms beyond the AST scanner:
Type system enforcement via typing.Annotated. Tier metadata is embedded in type hints: Annotated[str, Tier1, FailFast]. The Tier1, Tier2, Tier3, Tier4, and FailFast markers are annotation-only. They serve as documentation, scanner input (field-level tier classification), and mypy plugin integration (tier-flow checking). The mypy plugin's unique contribution is understanding Annotated[str, Tier1] as carrying tier metadata and flagging where a Tier4-annotated value flows to a Tier1-annotated parameter without validation.
Structural typing via typing.Protocol. Protocols encode trust requirements as structural types — e.g., ValidatedRecord requires _wardline_validated: bool and _wardline_trust_tier: Literal[1, 2]. Protocols use integer tier values (not string tokens) because Python's Literal types provide compile-time tier discrimination that mypy/pyright enforce statically. @runtime_checkable Protocols enable isinstance() checks as scaffolding for accidental tier mismatches, though they check structural conformance, not semantic authority.
Runtime structural enforcement via descriptors and __init_subclass__. AuthoritativeField descriptors raise on access-before-set, making fabricated defaults structurally impossible for Tier 1 fields. __init_subclass__ enforcement requires subclass methods to carry wardline decorators, firing at import time. Both are standard CPython OOP machinery shipped in the wardline-decorators package.
| Layer | When It Fires | What It Catches | Coverage |
|---|---|---|---|
| AST scanner | CI time | Pattern violations across entire codebase | Broad. Known false-negative surface (undecorated intermediaries, dynamic dispatch). |
| Type system (Protocols) | Development time (IDE) | Tier mismatches at function call sites | Narrow — only where mypy/pyright enabled. Optional. |
| Runtime (descriptors) | Access time | Fabricated defaults on Tier 1 fields | Narrow but absolute within scope. |
Runtime (__init_subclass__) |
Import time | Unclassified methods on high-assurance base classes | Narrow but absolute within scope. |
A.6 Regime composition matrix¶
This section is non-normative.
The Python enforcement regime composes existing ecosystem tools with a reference implementation to achieve Wardline-Full conformance per the framework specification.
| Capability | Best Home | Profile | Rationale |
|---|---|---|---|
| Syntactic pattern detection (PY-WL-001 through PY-WL-005) | ruff rules | Advisory (not conformant) | Pure AST pattern match. Fast, fires at IDE time. Advisory only: no manifest, no tier-graded SARIF. |
| Tier-aware severity grading (all WL rules) | Reference scanner | Wardline-Core (authoritative) | Requires manifest consumption and decorator metadata for context-sensitive grading. |
| Taint-flow tracking between declared boundaries | Reference scanner | Wardline-Core | No existing tool consumes the manifest's trust topology. |
| Context-dependent rules (PY-WL-006 through PY-WL-009) | Reference scanner | Wardline-Core | Requires semantic context: audit-path awareness, tier classification, structural verification, validation ordering. |
| Type-layer tier-mismatch diagnostics | mypy plugin | Wardline-Type | Extends mypy's existing type-flow analysis with tier metadata. |
| Runtime tier enforcement | Decorator library | Foundation | Python-native OOP machinery; ships with decorator vocabulary. |
| Manifest validation, schema checking | wardline CLI | Wardline-Governance | Validates wardline.yaml, overlays, exception registers. |
| Fingerprint baseline management | wardline CLI | Wardline-Governance | Tracks annotation surface changes. |
| SARIF aggregation across regime tools | wardline CLI | Wardline-Governance | Combines per-tool SARIF into regime-level output. |
| Control-law state reporting | wardline CLI | Wardline-Governance | Reports normal/alternate/direct based on tool success. |
The regime is temporally layered: ruff catches patterns while the developer types (advisory); the reference scanner grades them with tier-aware severity at CI time (authoritative). These are not redundant — they are layered by speed and precision.
Anti-recommendations. Do not force tier-aware taint analysis into ruff (its architecture is per-file, per-rule). Do not make mypy own governance artefacts. Do not use Semgrep as the normative rule source unless it can consume the wardline manifest faithfully. Do not build a pyright plugin until the mypy plugin is proven.
Stable interoperability surfaces. Third-party tools target: (1) manifest schema, (2) decorator metadata conventions, (3) rule identifiers and semantics, (4) golden corpus format, (5) SARIF property bags, (6) conformance profile vocabulary, (7) regime composition contract. If these interfaces are stable, any tool author can build wardline-compatible tooling without coordination with the specification's maintainers.
A.7 Residual risks¶
This section is non-normative. Assessors evaluating a Python wardline deployment should review these risks alongside the framework-level residual risks in the framework specification, particularly risk 12 (evasion surface trajectory) — as annotation coverage grows, coding-level risk falls but governance risk rises, and the risks below should be read in that context.
A.7.1 Dynamic dispatch and metaprogramming¶
Python's dynamic nature creates fundamental blind spots for static analysis. getattr(obj, method_name)(), **kwargs forwarding, decorator factories that modify function signatures, and metaclass-generated methods are all invisible to the AST scanner. The scanner tags these as UNKNOWN — conservative but noisy in heavily dynamic codebases. Projects with heavy dynamic dispatch should use the coverage metric to measure whether the UNKNOWN surface is acceptable.
A.7.2 Taint laundering via serialisation¶
The serialisation boundary creates a fundamental blind spot for trust-escalation paths (a Python-specific instantiation of ACF-T1, authority tier conflation). Tier 4 data written to a database by one code path and read back through an @int_data-decorated function or a @restoration_boundary function may be classified at a higher tier than its actual provenance warrants. This is a structural limitation of static analysis in any language — no analysis depth can cross the serialisation boundary.
The @restoration_boundary decorator partially mitigates this by requiring declared provenance evidence categories and verifying that the function body contains corresponding checks. However, the correctness of the evidence remains governance-verified. @int_data without @restoration_boundary provides no evidence verification at all — the return value is tagged UNKNOWN_RAW.
Blast radius. The most cost-effective circumvention paths in the design are composed declarations — @int_data + @restoration_boundary with dishonest or incorrect evidence claims. The allow-list, deny-list, provenance justification, and fingerprint baseline are compensating controls.
A.7.3 Combined false-negative surface¶
Two analysis approximations compound at the first analysis level: the two-hop undecorated heuristic misses deeper indirection chains, and the function-level taint approximation treats all values within a function body under the enclosing function's trust classification. When both apply simultaneously, the scanner may miss tier-flow violations. The combined effect is multiplicative, not additive. Variable-level tracking (level 2) and transitive call-graph inference (level 3) eliminate these false-negative surfaces respectively.
A.7.4 Decorator selection correctness¶
The scanner verifies structural consistency given a declared decorator. It cannot verify that the decorator choice itself is correct. An agent that applies @fail_open to a function that should be @fail_closed produces no structural finding. Partial mitigations: contradictory-combination detection (SCN-021), suspicious-combination advisories, body-behaviour contradiction advisories, and baseline ratification with selection confirmation. The irreducible residual: choosing the correct decorator requires understanding business context — a semantic judgement the scanner cannot make.
A.7.5 Governance decay¶
The governance model specifies rigorous human gates: CODEOWNERS review, temporal separation, baseline ratification, provenance justification. Every one of these is a human activity. Under deadline pressure, each gate becomes a candidate for rubber-stamping. The scanner cannot verify the quality of the human judgement that governs the scanner's own trust topology. The governance capacity mechanisms defined in the framework specification — particularly the expedited governance ratio — provide quantitative signals that can detect governance decay before it reaches systemic rubber-stamping.
A.7.6 Fingerprint baseline deletion¶
Deleting wardline.fingerprint.json resets the entire governance history. If the scanner silently re-establishes the baseline, any injected misannotations become the accepted baseline with no diff. Compensating controls: the scanner distinguishes initial establishment from deletion by checking VCS history; CODEOWNERS protection on governance artefacts.
A.7.7 Runtime structural enforcement bypass¶
The AuthoritativeField descriptor stores values as obj.__dict__["_authoritative_{name}"]. Direct __dict__ manipulation bypasses the descriptor's __set__ sentinel check — a fundamental limitation of Python's descriptor protocol. Compensating controls: AST scanner rules, fingerprint baseline, and supplementary __dict__-access advisory findings.
A.7.8 Third-party library taint accuracy¶
Python applications commonly depend on third-party libraries for data processing, validation, and serialisation — Pydantic, marshmallow, pandas, requests, and similar packages. These libraries execute in-process but are outside the wardline's annotation surface. The framework's dependency_taint declarations (§13.1.2) allow the overlay to assign taint states to third-party function return values, but the accuracy of those declarations depends on governance review, not machine verification.
Two Python-specific concerns sharpen this risk. First, Pydantic model defaults on fields that participate in tier-classified data flows are subject to the Group 5 scanning requirement (§6, SHOULD). When a Pydantic model is defined in a third-party library, the enforcement tool's ability to scan those defaults depends on whether it analyses installed package source — which is binding-specific and not specified in the §A.3 interface contract. Library-defined Pydantic defaults that escape scanning create a gap at exactly the point where §6 Group 5 says they SHOULD be caught. Second, the two-hop call-graph heuristic (§8.1) that enables WL-007 delegation analysis may or may not follow calls into third-party library source depending on the scanner's resolution of installed packages. A @validates_shape function whose body delegates to a library function (e.g., return my_library.validate(raw)) satisfies WL-007 only if the scanner follows the delegation and finds a rejection path in the library's source. If the scanner does not resolve library internals, the delegation appears to have no rejection path.
Compensating controls: dependency_taint declarations with version pinning; the application's own validation boundaries as the terminal control; governance review of taint declarations when dependency versions change; the two-hop heuristic as a best-effort mechanism for delegation resolution into available source.
A.8 Worked example with SARIF output¶
This section is non-normative. It demonstrates decorators in context through the full tier lifecycle — from raw external input to authoritative artefact — proving implementability.
Scenario. A government risk assessment system receives partner data from an external API, validates it, and produces an authoritative risk assessment record for the audit trail.
Data flow:
External API response (T4)
→ parse_partner_response() → PartnerDTO (T3)
→ validate_partner_semantics() → ValidatedPartner (T2)
→ create_risk_assessment() → RiskAssessment (T1)
Step 1: External boundary — receiving raw data (T4)
from wardline import external_boundary
@external_boundary
def fetch_partner_data(partner_id: str) -> dict:
"""Returns T4 raw data from external partner API."""
response = requests.get(f"{PARTNER_API_URL}/{partner_id}")
response.raise_for_status()
return response.json()
Return value tagged EXTERNAL_RAW. No field access, no defaults, no assumptions about structure.
Step 2: Shape validation — establishing structure (T4 → T3)
from dataclasses import dataclass
from wardline import validates_shape, schema_default
@dataclass(frozen=True)
class PartnerDTO:
"""T3 — shape-validated. Safe to handle; values unchecked."""
partner_id: str
name: str
country_code: str
classification: str
risk_indicators: list[str]
@validates_shape
def parse_partner_response(raw: dict) -> PartnerDTO:
"""T4 → T3. Establishes structural contract."""
required = {"partner_id", "name", "country_code", "security_classification"}
missing = required - raw.keys()
if missing:
raise SchemaError(f"Missing fields: {missing}")
for field in ("partner_id", "name", "country_code", "security_classification"):
if not isinstance(raw[field], str):
raise SchemaError(
f"{field}: expected str, got {type(raw[field]).__name__}"
)
indicators = schema_default(raw.get("risk_indicators", []))
if not isinstance(indicators, list):
raise SchemaError(
f"risk_indicators: expected list, got {type(indicators).__name__}"
)
for i in indicators:
if not isinstance(i, str):
raise SchemaError(
f"risk_indicators item: expected str, got {type(i).__name__}"
)
return PartnerDTO(
partner_id=raw["partner_id"],
name=raw["name"],
country_code=raw["country_code"],
classification=raw["security_classification"],
risk_indicators=indicators,
)
Note: risk_indicators is optional-by-contract — the external API may omit it. The schema_default() wrapper links this .get() to the overlay declaration for this data source, which declares the field as optional with an approved default of []. Without schema_default(), the .get() would fire PY-WL-001 at ERROR/STANDARD severity.
Step 3: Semantic validation — establishing domain fitness (T3 → T2)
from wardline import validates_semantic
@dataclass(frozen=True)
class ValidatedPartner:
"""T2 — semantically validated for landscape recording and reporting."""
partner_id: str
name: str
country_code: str
classification: str
risk_indicators: tuple[str, ...]
# bounded_context declared in overlay: consumers:
# ["record_to_landscape", "generate_partner_report"]
@validates_semantic
def validate_partner_semantics(dto: PartnerDTO) -> ValidatedPartner:
"""T3 → T2. Domain fitness for landscape and reporting consumers."""
if dto.country_code not in VALID_COUNTRY_CODES:
raise DomainValidationError(
f"Unrecognised country code: {dto.country_code!r}"
)
if dto.classification not in VALID_CLASSIFICATION_LEVELS:
raise DomainValidationError(
f"Invalid classification: {dto.classification!r}"
)
if not dto.name.strip():
raise DomainValidationError("Partner name is empty")
if len(dto.name) > MAX_PARTNER_NAME_LENGTH:
raise DomainValidationError(
f"Name exceeds {MAX_PARTNER_NAME_LENGTH} characters"
)
for indicator in dto.risk_indicators:
if indicator not in KNOWN_RISK_INDICATORS:
raise DomainValidationError(
f"Unknown risk indicator: {indicator!r}"
)
return ValidatedPartner(
partner_id=dto.partner_id,
name=dto.name.strip(),
country_code=dto.country_code,
classification=dto.classification,
risk_indicators=tuple(dto.risk_indicators),
)
Step 4: Trusted construction — creating institutional authority (T2 → T1)
from wardline import authoritative_construction
@authoritative_construction
def create_risk_assessment(
partner: ValidatedPartner,
context: AuditContext,
) -> RiskAssessment:
"""T2 → T1. Produces an authoritative risk assessment.
This is an institutional act, not a data transformation."""
return RiskAssessment(
assessment_id=generate_assessment_id(),
partner_id=partner.partner_id,
partner_name=partner.name,
risk_level=compute_risk_level(partner),
classification=partner.classification,
assessed_by=context.identity,
assessed_at=context.timestamp,
)
The complete call chain:
def assess_partner(partner_id: str, context: AuditContext) -> RiskAssessment:
"""Full pipeline: T4 → T3 → T2 → T1."""
raw = fetch_partner_data(partner_id) # T4
dto = parse_partner_response(raw) # T3
validated = validate_partner_semantics(dto) # T2
assessment = create_risk_assessment( # T1
validated, context
)
return assessment
Each line is a tier transition. Each function has one decorator declaring one transition. A reviewer can read this pipeline and trace the tier at every step.
Corresponding SARIF output. If the monolith version of this code were scanned — e.g., a function that uses raw_data.get("security_classification", "OFFICIAL") in a Tier 1 context — the scanner would produce:
{
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "wardline-scanner",
"version": "0.2.0",
"rules": [{
"id": "PY-WL-001",
"shortDescription": {
"text": "Dictionary key access with fallback default"
},
"defaultConfiguration": { "level": "error" }
}]
}
},
"results": [{
"ruleId": "PY-WL-001",
"level": "error",
"message": {
"text": "Fabricated default on tier-sensitive path: .get(\"security_classification\", \"OFFICIAL\")"
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": "src/adapters/partner_adapter.py"
},
"region": {
"startLine": 42,
"snippet": {
"text": "raw_data.get(\"security_classification\", \"OFFICIAL\")"
}
}
},
"logicalLocations": [{
"fullyQualifiedName": "myproject.adapters.partner_adapter.process_partner_update",
"kind": "function"
}]
}],
"properties": {
"wardline.rule": "PY-WL-001",
"wardline.taintState": "AUDIT_TRAIL",
"wardline.severity": "ERROR",
"wardline.exceptionability": "UNCONDITIONAL",
"wardline.analysisLevel": 1,
"wardline.enclosingTier": 1,
"wardline.annotationGroups": [1],
"wardline.excepted": false
}
}],
"properties": {
"wardline.manifestHash": "sha256:a1b2c3d4e5f6...",
"wardline.coverageRatio": 0.73,
"wardline.controlLaw": "normal",
"wardline.deterministic": true
}
}]
}
The SARIF output carries: the binding rule ID (PY-WL-001), the taint state of the enclosing context (AUDIT_TRAIL), the tier-graded severity (ERROR), the exceptionability class (UNCONDITIONAL — this finding cannot be excepted), and the analysis level. An assessor reading this finding knows immediately: a fabricated default was detected in a Tier 1 (audit trail) context, it is an unconditional error, and no exception can suppress it.
Agent guidance note. When generating code that interacts with wardline-annotated boundaries, agents should determine the input tier, the expected output tier, and the required transition — then apply the most specific decorator. If the tier is unknown, leave the function unannotated; UNKNOWN is safer than a wrong declaration. Full agent guidance is maintained as a living document outside the specification.
Annotation change impact preview. Python binding implementations SHOULD support annotation change impact preview using the SARIF metadata defined in the framework specification. When a developer modifies a tier assignment or decorator — e.g., changing @validates_shape to @validates_external, or promoting a module from Tier 3 to Tier 2 — the tool shows the cascade: newly applicable pattern rules, resolved findings, severity changes, and affected modules. The primary span is the changed annotation; secondary spans (carried in SARIF relatedLocations) are code locations whose compliance status changes. This gives developers and reviewers a before-and-after view of a governance change before it is committed, reducing the risk of annotation changes that inadvertently widen the enforcement surface or silently resolve findings that should remain visible.
A.9 Adoption strategy¶
This section is non-normative.
Adoption follows a phased model. Each phase is independently valuable.
| Phase | Components | Coverage |
|---|---|---|
| 1: Decorators + advisory ruff rules | wardline-decorators, wardline-ruff |
IDE-time and pre-commit advisory warnings for PY-WL-001 through PY-WL-005 at uniform severity. No manifest required. Lowest-cost entry point. |
| 2: Manifest + reference scanner | Add wardline.yaml, wardline-scanner |
Tier-aware severity grading for all nine binding rules. Taint-flow tracking. SARIF output. Governance-grade findings begin here. |
| 3: Type-system enforcement | wardline-mypy |
Development-time tier-mismatch diagnostics. Requires typing.Annotated tier annotations on data models. |
| 4: Runtime structural enforcement | AuthoritativeField descriptors, __init_subclass__ bases |
Specific high-risk paths become structurally impossible to violate. Deploy on audit records and decision products first. |
| 5: Full regime governance | wardline-cli |
Fingerprint baseline tracking, exception register management, SARIF aggregation, control-law state reporting. Provides governance evidence for independent assessment. |
Annotation budget. Target ~50–100 decorators for an 80k-line codebase in the initial annotation pass — external boundaries (15–25), validators (15–25), audit writers/readers (10–15), sensitive data handlers (5–10), layer declarations (module-level). Phases are a recommended ordering, not a mandatory sequence; a project may skip Phase 3 (no mypy) and still achieve Phases 1, 2, 4, and 5.
A.10 Error handling and control law¶
This section is non-normative.
Scanner error handling.
| Scenario | Behaviour |
|---|---|
| Syntax error in Python file | Skip; WARNING. Escalate to ERROR if file is in Tier 1 module. |
| Unresolvable import | Tag UNKNOWN in symbol table. |
| Unrecognised decorator | Ignore. |
Invalid wardline.yaml |
Exit non-zero. Do not scan. |
Missing wardline.yaml |
Exit non-zero. Do not scan. |
Missing wardline.toml |
Run with defaults (all groups enabled). Advisory. |
| Baseline file missing (initial) | Record current surface as baseline. |
| Baseline file missing (deleted) | ERROR, exit non-zero. Do not silently re-establish. |
| File exceeds size limit | Skip; WARNING. Escalate to ERROR if Tier 1 module. |
Exit codes: 0 (no ERROR findings), 1 (at least one ERROR finding), 2 (internal error), 3 (direct law — regime cannot produce meaningful enforcement output; wardline regime only).
Regime-level control-law state transitions.
| Scenario | Control Law | Impact |
|---|---|---|
| All configured tools ran successfully | Normal | Full enforcement |
| ruff plugin unavailable or failed | Alternate | Advisory fast-path absent; reference scanner provides authoritative coverage at CI time |
| mypy plugin unavailable or failed | Alternate | Type-layer diagnostics absent; no compensating tool at development time |
| Reference scanner unavailable | Alternate (severe) | Authoritative analysis absent; ruff provides advisory coverage for five of nine rules only |
| Manifest validation failed | Direct | Trust topology unavailable; no governance-grade findings possible |
| wardline CLI itself unavailable | Direct | No regime orchestration, SARIF aggregation, or control-law reporting |
The distinction between alternate and direct law follows the framework specification: alternate means degraded but running; direct means no meaningful enforcement output. Changes to wardline policy artefacts MUST NOT proceed under direct-law bypass.