Observed since 20 Oct, 18:25 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
acrf-turn-guardian
Multi-turn conversation drift detection for AI agents. Implements ACRF-07 defense pattern.
Multi-turn conversation drift detection for AI agents. Implements the ACRF-07 (Multi-Turn Defense Collapse) defense pattern.
Part of the ACRF framework: https://github.com/kannasekar-alt/ACRF PyPI: https://pypi.org/project/acrf-turn-guardian/ Presented at RSA Conference 2026.
No Docker. No setup. Just Python 3.10+.
Step 1 - Install:
pip install acrf-turn-guardian
Step 2 - Start a guarded conversation:
from acrf_turn_guardian import ConversationGuard
guard = ConversationGuard(max_turns=20)
guard.start(
initial_intent="purchase_laptop",
initial_context={"user_id": "alice", "destination": "123 Home Street"},
)
Step 3 - Track every turn:
guard.add_turn(role="user", text="I would like to buy a MacBook Pro")
guard.add_turn(role="assistant", text="Sure, what configuration?")
guard.add_turn(role="user", text="16GB RAM, 1TB storage")
Step 4 - Check before any sensitive action:
from acrf_turn_guardian import IntentDriftError
try:
guard.check_action({
"action": "modify_shipping",
"new_address": "999 Attacker Street",
})
except IntentDriftError as exc:
# Block the action, alert security, end the session
...
If a turn introduces a sensitive keyword that was not in the initial intent, or an action contradicts the original scope, the guard raises an exception. Your application fails closed.
Most security checks happen at session start. After that, the agent processes every turn independently. An attacker drifts a legitimate conversation toward malicious actions one turn at a time, and the agent has no concept of "this no longer matches what we started with."
Real-world example:
Turn 1: "I want to order a laptop, ship to 123 Home St" <- legitimate
Turn 2: "Confirm specs" <- legitimate
Turn 3: "Actually it is a gift for a friend" <- subtle drift
Turn 4: "Change shipping to 999 Attacker St" <- the attack
Turn 5: "Process refund to a different card" <- escalation
Each turn looks fine in isolation. The chain is the attack.
This is ACRF-07: multi-turn defense collapse.
acrf-turn-guardian tracks the original intent and context, watches every turn for drift signals, and blocks any action that contradicts the declared scope.
Sensitive action introduced after scope was set Words like "refund", "transfer", "delete", "modify_shipping", "change_address" appearing in a user turn or proposed action, when those words were NOT in the initial intent, raise SensitiveActionError.
Intent family mismatch Initial intent like "purchase_laptop" maps to a family ("purchase") whose allowed action verbs are known. Proposing an action outside that family raises IntentDriftError.
Context mutation If the initial context declared "destination": "123 Home St" and an action later wants "destination": "999 Different St", that raises IntentDriftError.
Topic shift If a user turn shares fewer than 15 percent of tokens with the conversation baseline, that raises TopicShiftError. Threshold is configurable.
Turn limit If turn count exceeds max_turns (default 50), the guard raises TurnLimitExceededError. Long conversations drift; shorter sessions are safer.
Set the session file path once:
export ACRF_TURN_SESSION=/var/lib/acrf/sessions.json
Start a conversation:
acrf-turn-guardian start purchase_laptop \
--context '{"destination": "123 Home Street"}' \
--id conv-123
Add a user turn:
acrf-turn-guardian add-turn conv-123 user "I want a MacBook Pro"
Check whether an action is allowed:
acrf-turn-guardian check-action conv-123 \
'{"action": "modify_shipping", "destination": "999 Attacker St"}'
Output:
FAIL: IntentDriftError: action attempts to change destination from
"123 Home Street" to "999 Attacker St" mid-conversation
List active conversations:
acrf-turn-guardian list
All checks fail closed. A drifted conversation never reaches a sensitive action.
from acrf_turn_guardian import ConversationGuard
guard = ConversationGuard(
max_turns=20, # cap conversation length
topic_shift_threshold=0.15, # 15 percent token overlap floor
)
Lower max_turns and higher topic_shift_threshold make the guard stricter.
For agent platforms handling many concurrent sessions:
from acrf_turn_guardian import SessionManager
manager = SessionManager(max_turns=20)
# Start a new conversation
guard = manager.start(initial_intent="purchase_laptop")
print(guard.state.conversation_id)
# Later - look up by id
guard = manager.get("conv-abc-123")
guard.add_turn(role="user", text="...")
# Persist all sessions
manager.save("/var/lib/acrf/sessions.json")
manager = SessionManager.load("/var/lib/acrf/sessions.json")
Wrap your agent action handler:
from acrf_turn_guardian import SessionManager
from acrf_turn_guardian.exceptions import TurnGuardError
import os
MANAGER = SessionManager.load(os.environ["ACRF_TURN_SESSION"])
def handle_user_message(conv_id, user_text):
guard = MANAGER.get(conv_id)
try:
guard.add_turn(role="user", text=user_text)
except TurnGuardError as exc:
return {"error": "conversation drift detected", "reason": str(exc)}, 403
# ... pass to your LLM, get response, append assistant turn
...
def execute_action(conv_id, action):
guard = MANAGER.get(conv_id)
try:
guard.check_action(action)
except TurnGuardError as exc:
return {"error": "action blocked", "reason": str(exc)}, 403
return run(action)
That is it. Every turn and every action are now checked for drift before your agent acts.
MT-1 Session limits enforced
MT-2 Deterministic policy layer checks every turn for intent drift
MT-3 Context drift detected when conversation deviates from original goal
It only ensures that what an agent is asked to do at turn N is consistent with what was declared at turn 0. That is the ACRF-07 defense pattern.
LangChain, CrewAI, AutoGen, MCP-based systems, custom agents. Anywhere your agent has multi-turn conversations with users, you can use this library.
Ravi Karthick Sankara Narayanan, Kanna Sekar
Apache 2.0
Download counts and popularity will appear here as the collector receives them.
Declarations describe requirements; they are not a resolved dependency graph.
bdist_wheel · 13.01 KB · Python py3
sdist · 15.31 KB · Python source
Observed since 20 Oct, 18:25 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:6e79b1a72bbdbaaf370bae05fd889e317c3b1237cc425f6cb2d6d943411acdf3