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

  1. Session Resolution: Get or create session via SessionManager
  2. Early Session Event: Emit SessionEvent with session metadata
  3. Agent Loading: Resolve agent config from vault or defaults
  4. Working Directory: Resolve to absolute path for SDK operations
  5. Context Building: Load attachments, files, images for prompt
  6. System Prompt: Build from CLAUDE.md hierarchy or agent config
  7. Permission Handler: Create with session and callbacks
  8. SDK Query: Stream events from Claude Agent SDK
  9. Event Processing: Transform SDK events to SSE events
  10. 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 Design: SQLite stores metadata only (pointers). SDK JSONL files are the source of truth for messages. SessionManager links them together for portability.

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 escalation
  • rm -rf / - delete root filesystem
  • rm -rf ~ - delete home directory
  • :(){:|:&};: - fork bomb
  • mkfs - format filesystems
  • dd if= - direct disk access
  • chmod -R 777 / - permission changes on root

Suggested Grants

Returns graduated permission options from specific to broad:

  1. File only: path/to/file.md
  2. Folder: folder/*
  3. Recursive: folder/**/*
  4. Root folder: root/**/*
  5. 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 files
  • context_tokens: Token count of context
  • context_truncated: Whether context was truncated
  • agent_name: Name of agent being used
  • available_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

Design Note: New sessions are finalized immediately when SDK provides session ID (not at end). This allows the session to appear in the chat list even if the user navigates away mid-stream.
# 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.