Taint propagation#
Taint is a label on a session that has read untrusted content. It is MAQPNA's answer to indirect prompt injection: the gateway cannot tell whether the model was fooled, but it knows which content entered the session, and rules can then deny, or require approval for, tools that could leak data. Code: cmd/maqpna-gateway/taint.go, policy_governance.go, registry_governance.go, fork.go, pkg/taint.
Taint itself blocks nothing. It is an input to policy evaluation (Request.sessionTaints), matched by a rule's sessionTaints {any, all} together with the tool's sinks.
Sources and effects#
flowchart LR
subgraph Sources
S1["ToolPolicy toolLabels taints"]
S2["MCPServer trust untrusted<br/>(default)"]
S3["MCPServer dataClass<br/>confidential or restricted"]
S4["maqpna-web fetch results"]
S5["Untrusted A2A peer answers"]
S6["Injection guards"]
S7["Parent session (fork)"]
end
S1 --> T[("Taint store<br/>namespace/session to labels")]
S2 -- untrusted-input --> T
S3 -- "data-class:class" --> T
S4 -- untrusted-input --> T
S5 -- untrusted-input --> T
S6 -- prompt-injection-suspected --> T
S7 -- inherited labels --> T
T --> P["Policy evaluation:<br/>rule sessionTaints + sinks"]
T --> V["tools/list visibility"]
T --> A["AgentSession annotation<br/>maqpna.com/taints"]
C1["Cleaner tools clearTaints<br/>(after 2xx)"] -. remove .-> T
C2["Admin taint:clear (audited)"] -. remove .-> T
C3["Expiry taintRetentionSeconds"] -. remove .-> T
Sequence#
sequenceDiagram
autonumber
participant Ag as Agent
participant GW as Gateway
participant TS as Taint store
participant PE as Policy engine
participant GH as github MCP server
participant L as Audit ledger
Ag->>GW: tools/call github.get_issue
GW->>TS: current labels (none)
GW->>PE: decide with sessionTaints empty
PE-->>GW: allow
GW->>GH: forward
GH-->>GW: issue text (may hold an injection)
GW->>TS: add untrusted-input (source github.get_issue)
GW->>L: allow, ext.taintAdded untrusted-input
Ag->>GW: tools/call mail.send_email (sinks external)
GW->>TS: current labels (untrusted-input)
GW->>PE: decide with sessionTaints untrusted-input
PE-->>GW: require_approval (rule sessionTaints any untrusted-input, sinks external)
GW->>L: record with ext.taint untrusted-input
Step by step#
- Label tools. Policies mark tools as taint sources (
toolLabels[].taints), exfiltration sinks (toolLabels[].sinks) or cleaners (toolLabels[].clearTaints,"*"for all).MCPServer.spec.toolLabelsadds labels per tool. Labels of every policy that applies to the caller are unioned. A label is 1–63 characters, without commas or whitespace. - Read the session's labels before deciding. Every governed call looks up the session's current labels (with Postgres, the read refreshes from the database first, so a label set on one replica applies on another at the very next call) and passes them to the policy engine. They are recorded in
ext.taint. - Match rules. A rule with
sessionTaints: {any: [untrusted-input]}andsinks: [external]matches calls to sink tools in a tainted session.tools/listuses the same evaluation, so tools the caller would be denied are hidden. - Add labels after the call. Once a tool call to a source has been forwarded, whatever the upstream status (an error page can carry untrusted content too), its labels are added to
namespace/session, and the call's record getsext.taintAdded. Untrusted registry servers adduntrusted-inputunless the tool'stoolLabelsentry says it is not a source;dataClass: confidential|restrictedaddsdata-class:<class>. - Taint from other components. The
maqpna-webfetch tool (labels fromweb.taintLabels, defaultuntrusted-input), untrusted A2A targets and injection guards (default labelprompt-injection-suspected) callTaintSessiondirectly; each addition is audited as anobserverecord. - Clear. Cleaner tools remove matching labels only after an HTTP 2xx response (
ext.taintCleared). An admin can clear withPOST /v1/sessions/{id}/taint:clear(observerecord, reasontaint_cleared…, approver = the admin). Labels expiretaintRetentionSeconds(default 86400) after the last tainted call, never before the session's token expires. - Inherit on fork. The first time a forked session's token is seen, the fork is tainted with its inherited labels (
maqpna_inherited_taints) plus the parent's and root's current labels (sourcefork:<parent>), so forking never sheds taint. - Publish. With
activity.enabled, the gateway writes the labels to theAgentSessionannotationmaqpna.com/taints(comma-separated, empty when clean). Snapshots record it so forks can inherit it. - Persist. Labels live in memory, journaled to
taintStorePath(fsync per change, compacted at start) or to the shared Postgres journaltaint.
What you see#
maqpna taint list (from GET /v1/taints) and maqpna taint get show tainted sessions with their labels and sources; maqpna session describe shows a taint badge; the timeline marks calls that added taint with the flag taint_added. Metrics: maqpna_gateway_taint_events_total{op,label} and the gauge maqpna_gateway_tainted_sessions. See maqpna taint list, maqpna taint get and maqpna taint clear.
Edge cases#
- Implicit
upstreamURLservers and staticupstreamsare not registry servers, so they do not taint automatically; label them in a policy. - DLP-class-driven labels and session-end cleanup are not implemented (Planned).
- With several gateway replicas and the
filebackend, each replica has its own taint store; usestateBackend: postgresfor shared taint.