Shannon Session Workspaces

April 3, 2026 · View on GitHub

Overview

Session workspaces provide isolated filesystem environments for each user session. All file operations are scoped to the session's workspace directory, ensuring that different sessions cannot access each other's files.

This isolation is critical for:

  • Multi-tenant security: Preventing data leakage between users
  • Reproducibility: Each session starts with a clean workspace
  • Resource management: Per-session disk quotas prevent runaway usage

Local vs EKS Execution

Shannon uses different Python sandbox backends depending on the environment:

Local (Docker Compose)EKS (Production)
python_executor backendWASI sandbox (Python.wasm via wasmtime)Firecracker microVMs
Python packagesStandard library onlyFull data science (pandas, numpy, scipy, torch, etc.)
File write to /workspace/Yes, when session_id is present (WASI preopened dir)Yes, via EFS-backed ext4 block device
File persistencefile_* tools + python_executor write to session dir on Docker volumefile_* tools + python_executor both write to EFS (with bi-directional sync)
IsolationWASI capability-based (wasmtime preopened dirs)Hardware-level VM isolation

This document describes the local Docker Compose path.

Role Requirements for File Tools

Important: File tools are gated by role. You must specify the appropriate role in the request context to access file operations.

RoleFile Tools Available
developerfile_read, file_write, file_list, bash, python_executor
generalist (default)file_read, file_list, bash (no file_write)
criticfile_read, file_list, bash

To write files, you must use role: "developer":

{
  "query": "Create a file called test.txt with content 'Hello'",
  "session_id": "my-session-123",
  "context": {
    "role": "developer"
  }
}

Without role: "developer", the agent cannot use file_write and the request will either fail or the LLM won't have the tool available.

Example: File Persistence Across Requests

# First request - create file (requires developer role)
curl -X POST http://localhost:8080/api/v1/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Write '\''test data'\'' to data.txt",
    "session_id": "persist-test",
    "context": {"role": "developer"}
  }'

# Second request - read same file (same session_id)
curl -X POST http://localhost:8080/api/v1/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Read data.txt and show contents",
    "session_id": "persist-test",
    "context": {"role": "developer"}
  }'

Files persist within a session - subsequent requests with the same session_id can access previously created files.

Architecture

Directory Structure

/tmp/shannon-sessions/              # SHANNON_SESSION_WORKSPACES_DIR (base)
├── session-abc123/                 # Session A's workspace
│   ├── code/
│   │   └── main.py
│   ├── data/
│   │   └── input.csv
│   └── output.txt
├── session-def456/                 # Session B's workspace
│   └── results.json
└── session-xyz789/                 # Session C's workspace
    └── notes.md

Each session gets a dedicated subdirectory named after its session_id. The workspace is created automatically on first file operation.

Component Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                           Tool Request                                       │
│  {"tool": "file_write", "path": "data.txt", "content": "Hello"}             │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│                    Python llm-service (file_ops.py)                          │
│                                                                              │
│  Check: is_sandbox_enabled()?                                                │
│    ├── Yes → Proxy to Rust agent-core via gRPC                              │
│    └── No  → Execute locally with Python path validation                     │
└─────────────────────────────────────────────────────────────────────────────┘

            ┌───────────────────────┴───────────────────────┐
            │                                               │
            ▼ (WASI mode)                                   ▼ (Python mode)
┌───────────────────────────────┐           ┌───────────────────────────────┐
│   Rust agent-core             │           │   Python Path Validation       │
│   (sandbox_service.rs)        │           │                                │
│                               │           │   - Resolve canonical path     │
│   - WorkspaceManager          │           │   - Check against allowlist    │
│   - SafeCommand execution     │           │   - Execute if permitted       │
│   - Capability-based security │           │                                │
└───────────────────────────────┘           └───────────────────────────────┘

Security Model

Shannon implements two security layers for file operations. One or both may be active depending on configuration.

Phase 2: Python Path Validation (Fallback)

When SHANNON_USE_WASI_SANDBOX=0, file operations are handled directly in Python with path validation:

  1. Path canonicalization: All paths are resolved using Path.resolve() to eliminate symlinks and .. components
  2. Allowlist enforcement: Resolved paths must fall within allowed directories:
    • Session workspace: {SHANNON_SESSION_WORKSPACES_DIR}/{session_id}/
    • Optional shared workspace: SHANNON_WORKSPACE
    • Dev mode only: Current working directory (if SHANNON_DEV_ALLOW_CWD=1)
  3. Symlink protection: Symlinks that point outside allowed directories are rejected

Example of blocked path traversal:

# Request: {"path": "../../../etc/passwd"}
# Resolved: /etc/passwd
# Result: DENIED (not within session workspace)

Phase 3: WASI Sandbox (Default, Enhanced Security)

When SHANNON_USE_WASI_SANDBOX=1 (default in docker-compose), file operations are proxied to the Rust agent-core service which provides additional security:

  1. Capability-based security: The WASI runtime only grants access to the session workspace directory
  2. Memory limits: Sandbox execution is constrained by memory limits
  3. Native command implementation: Shell commands (ls, cat, etc.) are implemented in pure Rust, not by spawning shell processes
  4. Metacharacter blocking: Shell metacharacters are rejected at parse time

Security properties:

  • No shell injection possible (commands are parsed, not executed via shell)
  • No network access from sandbox
  • Memory-limited execution
  • No access to system files; only /workspace/ (session dir) is exposed

Configuration

Environment Variables

VariableDefaultDescription
SHANNON_USE_WASI_SANDBOX0Enable WASI sandbox mode. Set to 1, true, or yes to enable.
SHANNON_SESSION_WORKSPACES_DIR/tmp/shannon-sessionsBase directory for all session workspaces
SHANNON_MAX_WORKSPACE_SIZE_MB100Per-session disk quota in megabytes
SHANNON_WORKSPACE(unset)Optional shared workspace accessible to all sessions
SHANNON_DEV_ALLOW_CWD0Dev only: allow access to current working directory
SHANNON_ALLOW_GLOBAL_TMP0Allow access to global /tmp (disabled by default for isolation)

Docker Compose Configuration

Both agent-core and llm-service must share the same session workspaces volume:

volumes:
  shannon-sessions:

services:
  agent-core:
    volumes:
      - shannon-sessions:/tmp/shannon-sessions
    environment:
      - SHANNON_SESSION_WORKSPACES_DIR=/tmp/shannon-sessions

  llm-service:
    volumes:
      - shannon-sessions:/tmp/shannon-sessions
    environment:
      - SHANNON_USE_WASI_SANDBOX=${SHANNON_USE_WASI_SANDBOX:-0}
      - SHANNON_SESSION_WORKSPACES_DIR=/tmp/shannon-sessions

Using File Tools

file_read

Read contents of a file from the session workspace.

Parameters:

ParameterTypeRequiredDefaultDescription
pathstringyes-Path to the file (relative to workspace)
encodingstringnoutf-8File encoding (utf-8, ascii, latin-1)
max_size_mbintegerno10Maximum file size to read (1-100 MB)

Example:

{
  "tool": "file_read",
  "path": "data/config.json"
}

Response:

{
  "success": true,
  "output": {"key": "value"},
  "metadata": {
    "path": "/tmp/shannon-sessions/my-session/data/config.json",
    "size_bytes": 42,
    "encoding": "utf-8",
    "file_type": ".json"
  }
}

JSON and YAML files are automatically parsed into structured data.

file_write

Write content to a file in the session workspace.

Parameters:

ParameterTypeRequiredDefaultDescription
pathstringyes-Path where to write the file
contentstringyes-Content to write
modestringnooverwriteoverwrite or append
encodingstringnoutf-8File encoding
create_dirsbooleannofalseCreate parent directories if needed

Example:

{
  "tool": "file_write",
  "path": "output/results.txt",
  "content": "Analysis complete.\nTotal items: 42",
  "create_dirs": true
}

Response:

{
  "success": true,
  "output": "/tmp/shannon-sessions/my-session/output/results.txt",
  "metadata": {
    "path": "/tmp/shannon-sessions/my-session/output/results.txt",
    "size_bytes": 35,
    "mode": "overwrite",
    "encoding": "utf-8",
    "created_dirs": true
  }
}

file_list

List files in a directory within the session workspace.

Parameters:

ParameterTypeRequiredDefaultDescription
pathstringyes-Directory path to list
patternstringno*Glob pattern (e.g., *.txt, *.py)
recursivebooleannofalseInclude subdirectories
include_hiddenbooleannofalseInclude hidden files (.xxx)

Example:

{
  "tool": "file_list",
  "path": ".",
  "pattern": "*.py",
  "recursive": true
}

Response:

{
  "success": true,
  "output": [
    {"name": "main.py", "path": "main.py", "is_file": true, "size_bytes": 1024},
    {"name": "utils.py", "path": "lib/utils.py", "is_file": true, "size_bytes": 512}
  ],
  "metadata": {
    "directory": "/tmp/shannon-sessions/my-session",
    "pattern": "*.py",
    "recursive": true,
    "file_count": 2,
    "dir_count": 0
  }
}

Sandbox Commands (WASI Mode)

When SHANNON_USE_WASI_SANDBOX=1, these safe commands are available for direct execution. Commands are implemented natively in Rust (not via shell), eliminating shell injection risks.

Read Operations

CommandDescriptionExample
lsList directory contentsls -la data/
catPrint file contentscat config.json
headPrint first N lineshead -n 10 log.txt
tailPrint last N linestail -n 20 log.txt
wcCount lines/words/byteswc data.csv
pwdPrint working directorypwd

Write Operations

CommandDescriptionExample
mkdirCreate directorymkdir -p output/reports
rmRemove file/directoryrm -r temp/
cpCopy filecp source.txt backup.txt
mvMove/rename filemv old.txt new.txt
touchCreate empty filetouch marker.txt

Search Operations

CommandDescriptionExample
findFind files by namefind . -name "*.py"
grepSearch for patterngrep -i error log.txt

Utilities

CommandDescriptionExample
echoPrint textecho "Hello World"

Command Flags

CommandSupported Flags
ls-a (all), -l (long format)
head-n NUM (line count)
tail-n NUM (line count)
mkdir-p (create parents)
rm-r (recursive)
grep-i (ignore case)
find-name PATTERN

Blocked Shell Metacharacters

The following shell metacharacters are blocked to prevent command injection:

CharacterMeaningWhy Blocked
|PipePrevents chaining to other commands
;Command separatorPrevents running additional commands
&&AND operatorPrevents conditional execution
||OR operatorPrevents conditional execution
>Output redirectPrevents writing to arbitrary files
<Input redirectPrevents reading from arbitrary files
>>Append redirectPrevents appending to arbitrary files
$(Command substitutionPrevents embedded command execution
`Backtick substitutionPrevents embedded command execution
\nNewlinePrevents multi-line injection
\rCarriage returnPrevents injection via CR

Example of blocked command:

cat file.txt | grep secret     # REJECTED: pipe character
ls; rm -rf /                   # REJECTED: semicolon
echo $(whoami)                 # REJECTED: command substitution

Quota Enforcement

Each session workspace has a configurable disk quota (default: 100MB).

How Quota Works

  1. Before each write operation, the current workspace size is calculated
  2. If adding the new content would exceed the quota, the operation fails
  3. No partial writes occur - either the entire write succeeds or nothing is written

Checking Workspace Size

The WorkspaceManager provides a get_workspace_size() method that recursively calculates the total size of all files in a session workspace.

Error Response

When quota is exceeded:

{
  "success": false,
  "error": "Workspace quota exceeded: 102.5MB > 100MB limit"
}

Recommendations

  • Use file_list to audit workspace contents before large writes
  • Clean up temporary files when no longer needed
  • Request quota increase via configuration if legitimate needs exceed default

Session ID Validation

Session IDs are validated to prevent path traversal attacks:

RuleExample ValidExample Invalid
Alphanumeric, hyphen, underscore onlysession-123, user_abcsession/../etc
Max 128 characters(any <=128 char string)(>128 char string)
Cannot start with .session.1.hidden-session
Cannot contain ..my-sessionsession..test

Cleanup

Automatic Cleanup

Session workspaces are automatically cleaned up based on session TTL (Time-To-Live). When a session expires:

  1. The session record is marked for deletion
  2. The workspace directory and all contents are removed
  3. Cleanup runs periodically as a background process

Manual Cleanup

Administrators can manually delete a session workspace:

let mgr = WorkspaceManager::from_env();
mgr.delete_workspace("session-abc123")?;

Listing All Workspaces

To see all active session workspaces:

let mgr = WorkspaceManager::from_env();
let sessions = mgr.list_workspaces()?;
// Returns: ["session-abc123", "session-def456", ...]

Troubleshooting

File Not Found

Error: File not found: data.txt
  • Verify the file exists in the session workspace
  • Use file_list to see available files
  • Check that the path is relative to workspace root

Access Denied

Error: Reading /etc/passwd is not allowed. Use session workspace.
  • Absolute paths outside the workspace are rejected
  • Path traversal attempts (../) are blocked
  • Use paths relative to the workspace root

Sandbox Proto Not Available

Error: Sandbox proto not available
  • WASI sandbox is enabled but agent-core is not running
  • Check that agent-core container is healthy
  • Verify gRPC connectivity between services

Quota Exceeded

Error: Workspace quota exceeded
  • Clean up unused files with rm
  • Increase SHANNON_MAX_WORKSPACE_SIZE_MB if needed
  • Check for unexpectedly large files

Known Limitations

Session ID Requires role: developer

File tools (file_read, file_write, file_list) are gated by role. Without role: "developer" in the request context, the LLM won't have file_write available and may use python_executor instead (which can write to /workspace/ if a session_id is present).

Solution: Always include "context": {"role": "developer"} when submitting tasks that need file operations.

Python Executor vs File Tools

Both python_executor and file_* tools can read/write the session workspace when a session_id is present:

  • python_executor: Sees the workspace as /workspace/ inside the sandbox. On local WASI, the session directory is mounted via wasmtime's preopened dir with full read-write permissions (DirPerms::all()). On EKS, the Firecracker VM mounts the ext4 at /workspace/. Without a session_id, no /workspace mount exists and writes fail.
  • file_* tools: Write directly to the session directory on the host via the Rust SandboxService (gRPC). Always writable when a session_id is provided.

On EKS, both tools access the same EFS-backed ext4, with bi-directional sync between the session directory and the ext4 image.

Key Source Files

ComponentFilePurpose
Workspace Managerrust/agent-core/src/workspace.rsSession directory management
Safe Commandsrust/agent-core/src/safe_commands.rsNative command implementations
Sandbox Servicerust/agent-core/src/sandbox_service.rsgRPC service for sandbox operations
Python File Toolspython/llm-service/llm_service/tools/builtin/file_ops.pyFile read/write/list tools
Sandbox Clientpython/llm-service/llm_service/tools/builtin/sandbox_client.pygRPC client for sandbox proxy