Orchestrator
The Orchestrator is the core execution controller for the Parachute Computer server, managing agent execution, session lifecycle, trust level enforcement, permission handling, and streaming responses to clients via Server-Sent Events (SSE).
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ API Router │
│ (FastAPI routes) │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌────────────────────────────────┐
│ Orchestrator │
│ (Agent Execution Controller) │
│ │
│ - run_streaming() │
│ - abort_stream() │
│ - grant/deny_permission() │
└────┬──────────────────────┬────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────────┐
│ SessionManager │ │ PermissionHandler │
│ │ │ │
│ - Session CRUD │ │ - check_permission() │
│ - SDK mapping │ │ - request_approval() │
│ - Transcript │ │ - grant/deny() │
│ loading │ │ - AskUserQuestion │
└────┬────────────┘ └────────┬─────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────────┐
│ Claude SDK │ │ Permission Handler │
│ Wrapper │ │ Callbacks (SSE) │
│ │ │ │
│ - query_streaming - on_denial │
│ - HOME override │ │ - on_request │
│ - Event convert │ │ - on_user_question │
└────┬────────────┘ └─────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Claude Agent SDK │
│ (Streaming from Claude) │
│ │
│ Session Storage: │
│ {vault}/.claude/projects/ │
└─────────────────────────────────┘
Key Components
| Component | File | Responsibility |
|---|---|---|
Orchestrator |
orchestrator.py | Execute agents, stream events, manage permissions |
SessionManager |
session_manager.py | CRUD sessions, load transcripts, map SDK to DB |
Claude SDK Wrapper |
claude_sdk.py | Async streaming, event conversion, vault-portable storage |
PermissionHandler |
permission_handler.py | Check tool access, request approval, handle questions |
run_streaming() Flow
The main entry point for processing chat messages. Returns an async generator of SSE events.
Execution Steps
- Session Resolution: Get or create session via SessionManager
- Early Session Event: Emit SessionEvent with session metadata
- Agent Loading: Resolve agent config from vault or defaults
- Working Directory: Resolve to absolute path for SDK operations
- Context Building: Load attachments, files, images for prompt
- System Prompt: Build from CLAUDE.md hierarchy or agent config
- Permission Handler: Create with session and callbacks
- SDK Query: Stream events from Claude Agent SDK
- Event Processing: Transform SDK events to SSE events
- Cleanup: Store final metrics and clean up active stream tracking
Event Flow
Client POST /api/chat
│
▼
run_streaming(message, session_id, ...)
│
├──► SessionEvent (session metadata)
│
├──► UserMessageEvent (echo user prompt)
│
├──► PromptMetadataEvent (transparency)
│
├──► InitEvent (tools available)
│
├──► TextEvent / ThinkingEvent / ToolUseEvent ...
│
├──► PermissionRequestEvent (if needed)
│
└──► DoneEvent (final response + metrics)
SessionManager
Manages session lifecycle with SQLite backend for metadata and SDK JSONL files for transcripts.
Pointer Architecture
Key Methods
| Method | Purpose |
|---|---|
get_or_create_session() |
Returns (session, resume_info, is_new) tuple |
finalize_session() |
Convert placeholder to real session after SDK provides ID |
resolve_working_directory() |
Convert relative/absolute path for SDK |
get_sdk_transcript_path() |
Calculate where SDK stored the JSONL |
load_sdk_messages_by_id() |
Load messages from SDK session file |
get_prior_conversation() |
Load context for imported sessions |
Session File Locations
Two locations checked in order:
1. Vault-based (new):
{vault}/.claude/projects/{encoded-cwd}/{session_id}.jsonl
2. Home-based (pre-migration):
~/.claude/projects/{encoded-cwd}/{session_id}.jsonl
Path encoding: /foo/bar/ → -foo-bar-
ResumeInfo
| Field | Description |
|---|---|
method |
"sdk_resume" or "new" |
is_new_session |
Whether this is a fresh session |
previous_message_count |
Messages from prior session |
sdk_session_available |
Whether SDK session exists |
Claude SDK Wrapper
Wraps the Claude Agent SDK to provide async streaming with vault-portable session storage.
HOME Override
# Sessions stored in vault instead of ~/.claude/
with _override_home(vault_path):
async for event in sdk_query(prompt=..., options=...):
yield _event_to_dict(event)
SDK Query Parameters
| Parameter | Purpose |
|---|---|
system_prompt |
Full custom prompt or None for preset |
system_prompt_append |
Content to append to preset |
use_claude_code_preset |
True unless custom/agent prompt |
setting_sources=["project"] |
Enables CLAUDE.md hierarchy loading |
cwd |
Working directory for file operations |
resume |
Session ID to resume, or None |
tools |
Allowed tools list |
mcp_servers |
MCP server configurations |
permission_mode="bypassPermissions" |
Checked via can_use_tool callback |
can_use_tool |
Permission handler's SDK callback |
Event Conversion
SDK events are converted to dictionaries for SSE streaming:
SDK Event Types → String mapping:
- UserMessage → "user"
- AssistantMessage → "assistant"
- SystemMessage → "system"
- ResultMessage → "result"
- StreamMessage → "stream"
Content extraction:
- Text blocks → {type: "text", text: ...}
- Thinking blocks → {type: "thinking", thinking: ...}
- Tool use blocks → {type: "tool_use", id, name, input}
PermissionHandler
Implements session-based permission checking for tool access with interactive user approval.
Tool Categories
| Category | Tools | Behavior |
|---|---|---|
| Always Allowed | MCP tools, WebSearch, WebFetch, Task | Bypass permission checks |
| Require Permission | Read, Glob, Grep, Write, Edit, Bash | Check session permissions |
| Always Denied | .env, credentials.json, private keys | Global deny list (even with trust mode) |
Permission Check Flow
check_permission(tool_name, input_data, tool_use_id)
│
├── Always allow? (MCP, web, task) → Allow
│
├── Dangerous bash? (sudo, rm -rf /) → Deny
│
├── Trust mode enabled? → Allow (except deny list)
│
├── Session permissions match? → Allow
│
└── Request approval → Wait for user response
│
┌───────┴───────┐
▼ ▼
Grant Deny
│
▼
Add pattern to session
Dangerous Commands (Always Blocked)
sudo- privilege escalationrm -rf /- delete root filesystemrm -rf ~- delete home directory:(){:|:&};:- fork bombmkfs- format filesystemsdd if=- direct disk accesschmod -R 777 /- permission changes on root
Suggested Grants
Returns graduated permission options from specific to broad:
- File only:
path/to/file.md - Folder:
folder/* - Recursive:
folder/**/* - Root folder:
root/**/* - Vault access:
**/*
AskUserQuestion Handling
_handle_ask_user_question(input_data, context)
│
├── Extract questions list
│
├── Generate request ID
│
├── Create UserQuestionRequest
│
├── Emit on_user_question callback (SSE event)
│
├── Wait for answers (300-second timeout)
│
└── Return updated input with answers
System Prompt Building
The _build_system_prompt method constructs the context for Claude:
Prompt Sources
| Source | Behavior |
|---|---|
| Custom prompt provided | Full override, return as-is |
| Agent with system_prompt | Full override from agent config |
| Vault agent | SDK loads CLAUDE.md hierarchy, build append only |
| Default | Claude Code preset with optional append |
Metadata Returned
prompt_source: "claude_code_preset", "custom", "agent"context_files: List of included context filescontext_tokens: Token count of contextcontext_truncated: Whether context was truncatedagent_name: Name of agent being usedavailable_agents: Agents discovered in vault
Stream Management
Active Streams
# QueryInterrupt for cancellation
interrupt = QueryInterrupt()
active_streams[session_id] = interrupt
# Check each event loop iteration
if interrupt.is_interrupted:
yield AbortedEvent(...)
break
Stream Control Methods
| Method | Purpose |
|---|---|
abort_stream(session_id) |
Signal interrupt for active stream |
has_active_stream(session_id) |
Check if session has active streaming |
get_active_stream_ids() |
Get all session IDs with active streams |
Early Session Finalization
# Capture session ID from InitEvent
if event.type == "init" and "session_id" in event.data:
captured_session_id = event.data["session_id"]
# Finalize immediately for new sessions
if is_new_session:
session = await session_manager.finalize_session(
placeholder, captured_session_id, model, title
)
# Update permission handler with real session ID
permission_handler.session_id = captured_session_id
Trust Level Enforcement
The orchestrator enforces trust levels per-session, determining how the agent is executed:
Trust Level Resolution:
│
├── "full" → Unrestricted SDK execution
│ All tools available, full filesystem access
│
├── "vault" → Restricted SDK execution
│ Tools limited to vault directory
│ Working directory must be within vault
│
└── "sandboxed" → Docker container execution
Fresh container per message
Isolated filesystem (vault mounted read-only)
Container destroyed after response
Trust level is stored in session metadata and persists across messages. Workspace trust levels act as a floor — sessions can't be less restrictive than their workspace.
Edge Cases Handled
Session Management
- Working directory doesn't exist: Falls back to vault root
- Pre-migration sessions: Searches both vault and home .claude/
- Imported sessions: Prior conversation injected as context
- Session ID collision: Creates new session with warning
Permission Handling
- Deny list supersedes trust mode: .env always blocked
- MCP tools always allowed: Provide structured vault access
- AskUserQuestion timeout: 5 minutes, then empty answers
- Permission request timeout: 2 minutes, then denied
Streaming
- Stream interrupted mid-tool: Partial response in AbortedEvent
- MCP loading failure: Continue without MCP, warnings logged
Design Patterns
Pointer Architecture
SQLite stores metadata only (pointers). SDK JSONL files are source of truth. Vault can move to different machine.
Callback-Driven Permissions
Permission checks emit callbacks → SSE events → Client responds via API → grant/deny methods.
Streaming Transparency
SSE events show all intermediate steps. PromptMetadataEvent shows what's included. Tools/thinking/results all streamed.
Graceful Degradation
MCP loading failure → continue without MCP. Working directory missing → fallback to vault. All failures logged but non-critical.