Skip to content

Sessions & Taint

Sessions are the fundamental unit of conversation state in Triggerfish. Every session independently tracks a taint level -- a classification watermark that records the highest sensitivity of data accessed during the session. Taint drives the policy engine's output decisions: if a session is tainted at CONFIDENTIAL, no data from that session can flow to a channel classified below CONFIDENTIAL.

Session Taint Model

How Taint Works

When a session accesses data at a classification level, the entire session is tainted at that level. Taint follows three rules:

  1. Per-conversation: Each session has its own independent taint level
  2. Escalation only: Taint can increase, never decrease within a session
  3. Full reset clears everything: Taint AND conversation history are cleared together
Session starts:   taint = PUBLIC

Access internal wiki:
  Session taint:  PUBLIC -> INTERNAL

Access Salesforce:
  Session taint:  INTERNAL -> CONFIDENTIAL

Try to downgrade:
  Session taint:  CONFIDENTIAL -> CONFIDENTIAL  (no change, escalation only)

Full reset:
  Session taint:  -> PUBLIC (history also cleared)

SECURITY

Taint can never be selectively reduced. There is no mechanism to "un-taint" a session without clearing the entire conversation history. This prevents context leakage -- if the session remembers seeing confidential data, the taint must reflect that.

Why Taint Cannot Decrease

Even if the classified data is no longer displayed, the LLM's context window still contains it. The model may reference, summarize, or echo classified information in future responses. The only safe way to lower taint is to eliminate the context entirely -- which is exactly what a full reset does.

Session Types

Triggerfish manages several session types, each with independent taint tracking:

Session TypeDescriptionInitial TaintPersists Across Restarts
MainPrimary direct conversation with the ownerPUBLICYes
ChannelOne per connected channel (Telegram, Slack, etc.)PUBLICYes
BackgroundSpawned for autonomous tasks (cron, webhooks)PUBLICDuration of task
AgentPer-agent sessions for multi-agent routingPUBLICYes
GroupGroup chat sessionsPUBLICYes

INFO

Background sessions always start with PUBLIC taint, regardless of the parent session's taint level. This is by design -- cron jobs and webhook-triggered tasks should not inherit the taint of whichever session happened to spawn them.

Taint Escalation Example

Here is a complete flow showing taint escalation and the resulting policy block:

User: "Check my Salesforce pipeline"

  [PRE_CONTEXT_INJECTION]
  Input classified: PUBLIC (owner text message)
  Session taint: PUBLIC (unchanged)

  [PRE_TOOL_CALL: salesforce.query_opportunities]
  Permission check: ALLOW

  [Tool executes with user's delegated OAuth token]

  [POST_TOOL_RESPONSE]
  Salesforce data classified: CONFIDENTIAL
  Session taint: PUBLIC -> CONFIDENTIAL
  Lineage record created: lin_789xyz

  Agent: "You have 3 deals closing this week totaling $1.2M..."

User: "Send a message to my wife that I'll be late tonight"

  [PRE_TOOL_CALL: whatsapp.send_message]
  Permission check: ALLOW

  [PRE_OUTPUT]
  Session taint: CONFIDENTIAL
  Wife = EXTERNAL recipient
  WhatsApp personal = PUBLIC channel
  Effective classification: PUBLIC

  CONFIDENTIAL -> PUBLIC: BLOCKED

  Agent: "I can't send to external contacts in this session
          because we accessed confidential data.

          -> Reset session and send message
          -> Cancel"

User: [chooses reset and send]

  [SESSION_RESET]
  Archive lineage records
  Clear conversation history
  Session taint: CONFIDENTIAL -> PUBLIC

  [Message sent successfully]
  Agent: "Done. Message sent to your wife."

Full Reset Mechanism

A session reset is the only way to lower taint. It is a deliberate, destructive operation:

  1. Archive lineage records -- All lineage data from the session is preserved in audit storage
  2. Clear conversation history -- The entire context window is wiped
  3. Reset taint to PUBLIC -- The session starts fresh
  4. Require user confirmation -- The SESSION_RESET hook requires explicit confirmation before executing

After a reset, the session is indistinguishable from a brand-new session. The agent has no memory of the previous conversation. This is the only way to guarantee that classified data cannot leak through the LLM's context.

Inter-Session Communication

When an agent sends data between sessions using sessions_send, the same write-down rules apply:

Source Session TaintTarget Session ChannelDecision
PUBLICPUBLIC channelALLOW
CONFIDENTIALCONFIDENTIAL channelALLOW
CONFIDENTIALPUBLIC channelBLOCK
RESTRICTEDCONFIDENTIAL channelBLOCK

Session tools available to the agent:

ToolDescriptionTaint Impact
sessions_listList active sessions with filtersNo taint change
sessions_historyRetrieve transcript for a sessionTaint inherits from referenced session
sessions_sendSend message to another sessionSubject to write-down check
sessions_spawnCreate background task sessionNew session starts at PUBLIC
session_statusCheck current session state and metadataNo taint change

Data Lineage

Every data element processed by Triggerfish carries provenance metadata -- a complete record of where data came from, how it was transformed, and where it went. Lineage is the audit trail that makes classification decisions verifiable.

Lineage Record Structure

json
{
  "lineage_id": "lin_789xyz",
  "content_hash": "sha256:a1b2c3d4...",
  "origin": {
    "source_type": "integration",
    "source_name": "salesforce",
    "record_id": "opp_00123ABC",
    "record_type": "Opportunity",
    "accessed_at": "2025-01-29T10:23:45Z",
    "accessed_by": "user_456",
    "access_method": "plugin_query"
  },
  "classification": {
    "level": "CONFIDENTIAL",
    "reason": "source_system_default",
    "assigned_at": "2025-01-29T10:23:45Z",
    "can_be_downgraded": false
  },
  "transformations": [
    {
      "type": "extraction",
      "description": "Selected fields: name, amount, stage",
      "timestamp": "2025-01-29T10:23:46Z",
      "agent_id": "agent_123"
    },
    {
      "type": "summarization",
      "description": "LLM summarized 3 records into pipeline overview",
      "timestamp": "2025-01-29T10:23:47Z",
      "input_lineage_ids": ["lin_789xyz", "lin_790xyz", "lin_791xyz"],
      "agent_id": "agent_123"
    }
  ],
  "current_location": {
    "session_id": "sess_456",
    "context_position": "assistant_response_3"
  }
}

Lineage Tracking Rules

EventLineage Action
Data read from integrationCreate lineage record with origin
Data transformed by LLMAppend transformation, link input lineages
Data aggregated from multiple sourcesMerge lineage, classification = max(inputs)
Data sent to channelRecord destination, verify classification
Session resetArchive lineage records, clear from context

Aggregation Classification

When data from multiple sources is combined (e.g., an LLM summary of records from different integrations), the aggregated result inherits the maximum classification of all inputs:

Input 1: INTERNAL    (internal wiki)
Input 2: CONFIDENTIAL (Salesforce record)
Input 3: PUBLIC      (weather API)

Aggregated output classification: CONFIDENTIAL (max of inputs)

TIP

Enterprise deployments can configure optional downgrade rules for statistical aggregates (averages, counts, sums of 10+ records) or certified anonymized data. All downgrades require explicit policy rules, are logged with full justification, and are subject to audit review.

Audit Capabilities

Lineage enables four categories of audit queries:

  • Forward trace: "What happened to data from Salesforce record X?" -- follows data forward from origin to all destinations
  • Backward trace: "What sources contributed to this output?" -- traces an output back to all its source records
  • Classification justification: "Why is this marked CONFIDENTIAL?" -- shows the classification reason chain
  • Compliance export: Full chain of custody for legal or regulatory review

Taint Persistence

Session taint is persisted through the StorageProvider under the taint: namespace. This means taint survives daemon restarts -- a session that was CONFIDENTIAL before a restart is still CONFIDENTIAL after.

Lineage records are persisted under the lineage: namespace with compliance-driven retention (default 90 days).

Released under the MIT License.