Skip to content

[FEATURE] Add read-only access mode to SessionManager for multi-tenant safety #2020

@mohanasudhan

Description

@mohanasudhan

Problem Statement

Problem Statement:
When a SessionManager is hooked into an Agent, it automatically registers callbacks for both read operations (restoring conversation/state on init) and write operations (persisting every new message, syncing agent state, redacting content). There is no way to decouple these — it's all-or-nothing.

In multi-tenant systems, this is a problem. A common pattern is: load a tenant's shared context or prior conversation into the agent (read), let the agent operate with short-term in-memory messages during the request (in-memory), but never write those ephemeral per-request interactions back to the persistent store (no write). Today this isn't possible without building a custom SessionManager subclass that no-ops every write method.

The coupling exists in two places:

  1. SessionManager.register_hooks() unconditionally registers callbacks for MessageAddedEvent (append + sync), AfterInvocationEvent (sync), and their multi-agent/bidi equivalents — alongside the read-path AgentInitializedEvent
  2. Agent._run_loop and Agent._execute_event_loop_cycle directly call self._session_manager.redact_latest_message() and self._session_manager.sync_agent() outside of the hook system, bypassing any hook-level filtering.

Proposed Solution

Proposed Solution:
Add an access_mode parameter to the SessionManager base class with an AccessMode enum (READ_WRITE, READ_ONLY), defaulting to READ_WRITE to preserve current behavior.

Changes:

  • Add AccessMode enum and access_mode property to SessionManager.
  • In register_hooks(), conditionally skip registration of write callbacks (append_message, sync_agent, sync_multi_agent, sync_bidi_agent, append_bidi_message) when access_mode is READ_ONLY. Read callbacks (initialize, initialize_multi_agent, initialize_bidi_agent) always register.
  • In Agent._run_loop and Agent._execute_event_loop_cycle, guard the direct redact_latest_message() and sync_agent() calls behind not self._session_manager.is_read_only.
  • Pass access_mode through RepositorySessionManager, FileSessionManager, and S3SessionManager constructors.
  • Export AccessMode from strands.session.

Usage:

from strands import Agent
from strands.session import S3SessionManager, AccessMode

agent = Agent(
session_manager=S3SessionManager(
session_id="tenant-123",
bucket="my-bucket",
access_mode=AccessMode.READ_ONLY,
)
)

Agent loads conversation history from S3 but never writes back

Additional considerations for follow-up:

  • Granular control (e.g., read-only messages but writable state) could be added later as separate mode values.
  • In read-only mode, guardrail-triggered redactions won't persist — the original unredacted message stays in storage. This should be documented as a known trade-off.
  • Conversation manager state (e.g., removed_message_count) won't sync in read-only mode, so subsequent session loads may re-read previously trimmed messages. This should also be documented.

Use Case

  1. Multi-tenant customer support agent — A SaaS platform runs one agent per incoming customer request. The agent loads the tenant's shared knowledge base and prior conversation history from S3 on init, but each request is ephemeral. You don't want thousands of concurrent request-level interactions polluting the shared tenant session. Read-only mode lets the agent consume context without writing back.
  2. Dry-run / preview mode — During development or QA, you want to test an agent against a real production session to see how it would respond, without actually modifying the stored conversation. A developer points the agent at a production session ID with READ_ONLY and runs prompts against it safely.
  3. Shared context across agent replicas — In a horizontally scaled deployment, multiple agent instances serve the same tenant concurrently. One designated "writer" agent (or a separate ingestion pipeline) owns the session writes. All other replicas load the session read-only to avoid write conflicts, race conditions, and duplicate messages in storage.

Alternatives Solutions

Alternative: ReadOnlySessionManager wrapper class
Instead of adding a mode flag to the base class, introduce a standalone wrapper that takes any SessionManager and no-ops all write methods while delegating all read methods to the inner instance.

from strands.session import ReadOnlySessionManager, S3SessionManager

inner = S3SessionManager(session_id="tenant-123", bucket="my-bucket")
agent = Agent(session_manager=ReadOnlySessionManager(inner))

How it works:

  • initialize(), initialize_multi_agent(), initialize_bidi_agent() → delegated to inner session manager (reads happen normally)
  • append_message(), sync_agent(), redact_latest_message(), sync_multi_agent(), sync_bidi_agent(), append_bidi_message() → no-op (writes silently skipped)
  • register_hooks() is inherited unchanged from SessionManager — all hooks fire, but the write methods do nothing when called

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions