DocsCore conceptsTrust, taint and provenance

Trust, taint and provenance

An AI agent that reads a GitHub issue written by a stranger has just taken instructions from a stranger. Nothing in the agent's architecture distinguishes "the user asked me to" from "a comment in the issue asked me to".

Memnox does not try to detect that. Detection is a losing arms race against natural language. Instead it tracks where the context came from and raises the bar for what may be done afterwards.

Two words, kept apart

Trusted is about the author: does this person resolve to a known human in this workspace?

Tainted is about the session: has this agent read anything that was not ground truth?

An event is tainted at ingestion; a session becomes tainted by reading one.

Classification is actor-aware

It is not enough to ask what kind of source something is. The same GitHub issue is trustworthy from a maintainer and not from a passer-by, so classification looks at both.

Source

github_file, github_symbol, github_line_chunk

Ground truth, never tainted

extracted_decision

Ground truth, it already passed a human

GitHub issue or comment from OWNER / MEMBER / COLLABORATOR

Trusted

The same issue from NONE

Tainted

Slack message from a workspace member

Trusted

Slack message from outside the workspace

Tainted

Email, documents, third-party chat

Always tainted

Anything else

Falls back to a source-authority threshold

Derivatives ending in _enriched inherit their base classification. An LLM rewrite cannot launder taint, summarizing a hostile document produces a tainted summary.

Taint attaches to the session

Not to strings. Once an agent's session has read untrusted content, the session stays tainted for the store's TTL, and taint merges monotonically, it can be added, never removed.

There is no scrubbing step, because there is no reliable way to know which of the model's later tokens were shaped by what it read.

What a tainted session may not do

Privileged actions from a tainted session require a human:

file.write · shell.execute · deploy.* · database.* · mcp.* · data.export · *.delete

Two are non-overridable: project.delete and database.drop are blocked outright, and no approval, routine or break-glass, lifts the block.

Fail-closed, deliberately

Every other advisor fails silent: if it cannot run, it raises nothing, because a broken advisor must not block work.

Provenance is the exception. If the session taint store cannot be read, the session is treated as tainted. The reasoning: an unreadable taint store is exactly the situation in which you least want to assume innocence.

Reporting taint

The caller reports provenance on the check:

ts
await memnox.check({
  action: 'file.write',
  target: 'src/config.ts',
  sessionId: runId,
  taint: [{ sourceType: 'github_issue_comment', actor: 'NONE' }],
});

The MCP firewall and the editor hooks do this for you. A custom loop that reads external content and does not report it is the one gap in the model, and it is a gap you opened, not one Memnox has.

Running it across pods

Without Redis the taint store is per-process, which means a session tainted on one pod is clean on the next. --redis-url moves it into Redis with a lock-guarded read-merge-write and a 7-day TTL.

It is never reconstructed from the audit log. An audit event records that an action happened, not what the model read.