MAQPNADocs

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#

  1. Label tools. Policies mark tools as taint sources (toolLabels[].taints), exfiltration sinks (toolLabels[].sinks) or cleaners (toolLabels[].clearTaints, "*" for all). MCPServer.spec.toolLabels adds labels per tool. Labels of every policy that applies to the caller are unioned. A label is 1–63 characters, without commas or whitespace.
  2. 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.
  3. Match rules. A rule with sessionTaints: {any: [untrusted-input]} and sinks: [external] matches calls to sink tools in a tainted session. tools/list uses the same evaluation, so tools the caller would be denied are hidden.
  4. 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 gets ext.taintAdded. Untrusted registry servers add untrusted-input unless the tool's toolLabels entry says it is not a source; dataClass: confidential|restricted adds data-class:<class>.
  5. Taint from other components. The maqpna-web fetch tool (labels from web.taintLabels, default untrusted-input), untrusted A2A targets and injection guards (default label prompt-injection-suspected) call TaintSession directly; each addition is audited as an observe record.
  6. Clear. Cleaner tools remove matching labels only after an HTTP 2xx response (ext.taintCleared). An admin can clear with POST /v1/sessions/{id}/taint:clear (observe record, reason taint_cleared…, approver = the admin). Labels expire taintRetentionSeconds (default 86400) after the last tainted call, never before the session's token expires.
  7. 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 (source fork:<parent>), so forking never sheds taint.
  8. Publish. With activity.enabled, the gateway writes the labels to the AgentSession annotation maqpna.com/taints (comma-separated, empty when clean). Snapshots record it so forks can inherit it.
  9. Persist. Labels live in memory, journaled to taintStorePath (fsync per change, compacted at start) or to the shared Postgres journal taint.

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 upstreamURL servers and static upstreams are 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 file backend, each replica has its own taint store; use stateBackend: postgres for shared taint.