Skip to content

Trust vocabulary

Wardline ships exactly three importable decorators. They are the only way you declare trust in your own code; everything else in the lattice is inferred by the engine. This page documents each one precisely — what it declares, its exact signature and allowed arguments, and a usage example.

The three decorators

Import all three from one module:

from wardline.decorators import trusted, trust_boundary, external_boundary

These are the exact names exported by wardline.decorators. The canonical names are also discoverable without importing Wardline at all, via wardline vocab, which prints:

version: wardline-generic-1
entries:
- canonical_name: external_boundary
  group: 1
  attrs: {}
- canonical_name: trust_boundary
  group: 1
  attrs:
    _wardline_to_level: TaintState
- canonical_name: trusted
  group: 1
  attrs:
    _wardline_level: TaintState

All three belong to group 1. trust_boundary stamps a _wardline_to_level marker; trusted stamps a _wardline_level marker; external_boundary carries no level attribute.

These are static-analysis markers — no runtime behavior

Each decorator stamps _wardline_* attributes onto the function object and returns the function unchanged. They do not wrap, validate, sanitise, log, or alter the function's behavior at runtime in any way. The analyzer's DecoratorTaintSourceProvider reads the markers from the AST during a scan; at runtime your decorated function behaves exactly as if the decorator were not there. Adding a decorator can never change what your program does — it only changes what Wardline can prove about it.

The four declarable tiers

You only ever write four of the eight lattice tiers directly. The decorators accept these names (as a TaintState enum member or its string form):

Tier Declared by Meaning
INTEGRAL @trusted (the default) Fully trusted data your code produces and relies on. The most-trusted tier.
ASSURED @trusted(level="ASSURED") or @trust_boundary(to_level="ASSURED") Trusted after validation — one notch below integral.
GUARDED @trust_boundary(to_level="GUARDED") Partially checked — passed a shape/format guard but not fully assured.
EXTERNAL_RAW @external_boundary Raw, untrusted data crossing into the system from outside.

The remaining four tiers (UNKNOWN_RAW, UNKNOWN_GUARDED, UNKNOWN_ASSURED, MIXED_RAW) are never written by you — the engine infers them for undecorated, unresolved, or provenance-conflicting code. They are documented in the trust model.

Each decorator only accepts the subset of tiers that makes sense for it; passing a tier outside that subset is an error (see the per-decorator sections below).

external_boundary

Declares: an external entry point. Its return value carries raw, untrusted data (EXTERNAL_RAW). This is your source — the function where data crosses into the system from the outside world.

Signature:

def external_boundary[F: Callable[..., Any]](fn: F) -> F

It takes no arguments — apply it bare, never called. There is no level to set: the return tier is always EXTERNAL_RAW.

Usage:

from wardline.decorators import external_boundary

@external_boundary
def read_request_body(request) -> str:
    return request.body          # tracked as EXTERNAL_RAW

Anything read_request_body returns is treated as untrusted from here on. If it reaches a function declared to produce trusted data without a validation boundary in between, Wardline reports a finding.

Anti-pattern: do not call it with parentheses or arguments — @external_boundary() or @external_boundary(level=...) is wrong. external_boundary is the bare-form decorator; it has nothing to parameterise.

trust_boundary

Declares: a validation/sanitisation boundary. The function takes input that may be untrusted and raises its trust on the way out, to the tier you name in to_level. This is the only sanctioned way to legitimise untrusted data.

Signature:

def trust_boundary(*, to_level: TaintState | str) -> Callable[[Any], Any]

to_level is keyword-only and required — there is no default. It accepts only two tiers:

  • GUARDED — partially checked (passed a shape/format guard).
  • ASSURED — trusted after validation.

Passing any other tier (for example INTEGRAL or EXTERNAL_RAW) is rejected: the boundary can only raise trust to GUARDED or ASSURED, not to fully integral and not to a raw tier.

Usage:

from wardline.decorators import trust_boundary

@trust_boundary(to_level="GUARDED")
def parse_user_id(raw: str) -> int:
    return int(raw)              # return tracked as GUARDED, not raw

Once data passes through parse_user_id, Wardline treats the result as GUARDED rather than EXTERNAL_RAW, so it can flow into code expecting guarded input without a finding.

Anti-pattern: do not omit to_level (@trust_boundary bare, or @trust_boundary(), raises because to_level is required), and do not name a tier outside {GUARDED, ASSURED}. A boundary that claims to produce INTEGRAL from raw input is exactly the over-trust Wardline exists to catch — so it is disallowed at the declaration site.

trusted

Declares: a trusted producer or sink — a function that both operates on and returns trusted data. Use it on internal code that you assert never handles raw, untrusted input.

Signature (two forms):

@overload
def trusted[F: Callable[..., Any]](fn: F, /) -> F: ...
@overload
def trusted(*, level: TaintState | str = ...) -> Callable[[Any], Any]: ...

trusted supports both the bare form and the parameterised form:

  • Bare @trusted — declares the default tier, INTEGRAL.
  • Parameterised @trusted(level="ASSURED") — declares ASSURED instead.

level is keyword-only and accepts only two tiers: INTEGRAL (the default) or ASSURED. Passing any other tier (for example GUARDED or EXTERNAL_RAW) is rejected — a trusted producer is, by definition, integral or assured, not merely guarded and never raw.

Usage — both forms:

from wardline.decorators import trusted

@trusted
def build_audit_record(event) -> dict:
    return {"event": event}      # declares it produces INTEGRAL data

@trusted(level="ASSURED")
def normalise_config(cfg: dict) -> dict:
    return dict(cfg)             # declares it produces ASSURED data

If anything EXTERNAL_RAW reaches the return path of build_audit_record without a trust_boundary in between, Wardline reports that the function declares INTEGRAL but actually returns untrusted data.

Anti-pattern: do not pass a non-trusted tier such as @trusted(level="GUARDED") or @trusted(level="EXTERNAL_RAW"). Those tiers belong to trust_boundary and external_boundary respectively; trusted is only for INTEGRAL or ASSURED.

How the three fit together

A typical flow uses one of each: external_boundary marks where untrusted data enters, trust_boundary marks where it is validated and its trust is raised, and trusted marks the code downstream that is entitled to assume trusted input.

from wardline.decorators import external_boundary, trust_boundary, trusted

@external_boundary
def read_id(request) -> str:
    return request.args["id"]            # EXTERNAL_RAW

@trust_boundary(to_level="GUARDED")
def validate_id(raw: str) -> int:
    return int(raw)                      # raises to GUARDED

@trusted(level="ASSURED")
def load_user(user_id: int):
    ...                                  # entitled to ASSURED input

Wardline reads these declarations and the data flow between them, then reports any path where untrusted data reaches a more-trusting declaration without crossing a boundary first. For the conceptual model behind the tiers and how trust combines along a flow, see the trust model.