PRD: Modular Parachute Computer
Restructuring Parachute from a monolithic application into a modular personal computer platform.
Contents
- Vision & Principles
- Architecture Overview
- Parachute CLI
- Parachute Computer Core
- Module Manifest
- Brain Module & Interface System
- Para-ID System
- Module System
- Core Modules (Daily, Chat)
- Skills vs Modules
- Flutter App Architecture
- Agent Sandboxing
- Chat Sandbox Modes
- Chat Bot Integration
- Suggestions System
- Module Permission UI
- Maintenance Agent
- API Key Scoping
- Inter-Module Communication
- Hooks System
- Observability & Audit
- Claude Authentication
- Testing Strategy
- Migration Plan
- Decisions & Questions
Executive Summary
Parachute is evolving from a monolithic application into a modular personal computer platform. This PRD defines the architecture for:
- Parachute Computer - The foundational platform (Python server + management UI)
- Brain Module - Knowledge layer with published interface (optional, swappable)
- Module System - Discoverable modules that extend functionality (all modules are vault modules)
- Core Modules - Daily and Chat as the first installable modules
- Flutter App Architecture - Shared
parachute_corepackage with separate apps
Vision & Principles
The Core Insight
Parachute Computer should be a platform, not an application.
The current monolith tightly couples Chat, Daily, and Vault. This restructuring inverts the relationship: Parachute Computer becomes the foundation, and modules plug into it.
Guiding Principles
Vault-Centric
The user's vault (~/Parachute) is the source of truth. Modules, configurations, and data live there. The platform serves the vault.
Local-First
Everything runs on the user's machine. No cloud dependency. Data never leaves without explicit action.
Module Independence
Modules can be developed, tested, and deployed independently. A bug in one module shouldn't crash others.
Progressive Disclosure
The default experience is simple (Daily + Chat). Power users can add modules, configure permissions, build their own.
Interoperability
Modules communicate through standard interfaces (HTTP, MCP). No tight coupling between modules.
User Ownership
Users own their modules. They can inspect, modify, fork, and share them freely.
Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ Flutter Apps (Separate Packages) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────────┐ │
│ │ Parachute Daily │ │ Parachute Chat │ │ Parachute Computer │ │
│ │ (journaling) │ │ (conversation) │ │ (module management) │ │
│ └────────┬────────┘ └────────┬────────┘ └─────────────┬───────────────┘ │
│ │ │ │ │
│ ┌────────┴────────────────────┴─────────────────────────┴───────────────┐ │
│ │ parachute_core (Flutter package) │ │
│ │ • Server connection • Auth/API keys • Theming/Design tokens │ │
│ │ • Common widgets • Sync service • Transcription (optional) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
HTTP/SSE
│
┌─────────────────────────────────────┴───────────────────────────────────────┐
│ Parachute Computer (Python Server) │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Core Router (FastAPI) │ │
│ │ /api/brain/* → Brain (integrated) │ │
│ │ /api/modules/* → Module Management │ │
│ │ /api/daily/* → Daily Module │ │
│ │ /api/chat/* → Chat Module │ │
│ │ /api/{custom}/* → User-defined Modules │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ parachute.core (Python package) │ │
│ │ • Claude SDK wrapper • Module discovery & lifecycle │ │
│ │ • Brain (knowledge layer) • Database layer (SQLite) │ │
│ │ • File access (gated) • MCP infrastructure │ │
│ │ • Hooks system • Event bus │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Modules (all vault modules) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │ │
│ │ │ Daily │ │ Chat │ │ Telegram │ │ Custom │ │ │
│ │ │ (Parachute) │ │ (Parachute) │ │ Bot │ │ Modules │ │ │
│ │ │ verified │ │ verified │ │ user │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ Trusted modules in main container │ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Agent Sandboxes (Docker per untrusted agent) │ │
│ │ Claude SDK runs with scoped permissions per agent definition │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
~/Parachute (vault)
Key Architectural Changes
| Current | Future |
|---|---|
| Single monolithic Flutter app | Multiple apps sharing parachute_core package |
| Hardcoded features (Chat, Daily, Vault) | Discoverable modules that register routes |
| Lima VM for isolation | Bare metal + Docker containers for sandboxing |
| Settings screen for configuration | Parachute Computer UI for module management |
Parachute CLI
The parachute CLI is the primary interface for managing Parachute on any system (Mac, Linux, headless servers, Raspberry Pi).
Commands
# Server management
parachute start # Start server (default port 3333)
parachute start --port 3334 # Start on custom port
parachute stop # Stop server
parachute status # Show server status
# Module management
parachute modules list # List installed modules
parachute modules install <url> # Install from git repo
parachute modules install <path> --dev # Install from local path (symlink)
parachute modules install <url> --path python/ # Install subdirectory of repo
parachute modules remove <name> # Remove a module
parachute modules status # Show module status (loaded, pending approval, etc.)
parachute modules diff <name> # Show what changed in pending update
parachute modules approve <name> # Approve module update
# Configuration
parachute config # Show current config
parachute config set <key> <value> # Set config value
parachute vault # Show vault path
parachute vault set <path> # Set vault path
# Authentication
parachute auth status # Show auth status
parachute keys list # List API keys
parachute keys create <name> # Create new API key
parachute keys revoke <name> # Revoke API key
Module Installation Flow
$ parachute modules install https://github.com/OpenParachutePBC/parachute-brain
Cloning parachute-brain...
Validating manifest.yaml...
Name: brain
Version: 1.0.0
Provides: BrainInterface
Installing dependencies...
Computing hash: sha256:abc123...
✓ Module 'brain' installed and approved
Path: ~/.modules/brain/
Restart server to load: parachute restart
Implementation
The CLI lives in parachute-computer and is installed via pip:
pip install parachute-computer
# or
pipx install parachute-computer
This provides the parachute command globally, enabling management of Parachute on headless servers, Raspberry Pis, and any Linux/Mac system.
Parachute Computer Core
The core platform provides foundational services that all modules depend on.
Core Services
| Service | Purpose | Access |
|---|---|---|
| Claude SDK Wrapper | Unified interface to Claude Agent SDK with HOME override for vault-portable sessions | All modules |
| Permission Handler | Enforce read/write/bash permissions per module | All modules |
| Database Layer | SQLite operations (sessions, metadata, chunks) | Via core API |
| File Access | Gated filesystem operations respecting permissions | Per-module |
| MCP Infrastructure | Spawn and manage MCP servers for agent tools | Module-declared |
| Event Bus | Publish/subscribe for cross-module events | All modules |
| Module Discovery | Find, load, validate modules from config | Core only |
Configuration
Module registry at ~/.parachute/modules.yaml:
# All modules are vault modules - no "built-in" distinction
# Fresh vault starts empty, user installs what they want
modules:
daily:
enabled: true
source: parachute://daily # Official Parachute module
version: "1.0.0"
trust: verified # Signed by Parachute
hash: "sha256:abc123..." # Verified on load
chat:
enabled: true
source: parachute://chat
version: "1.0.0"
trust: verified
hash: "sha256:def456..."
telegram-bot:
enabled: true
source: local # User-created in vault
path: "Modules/telegram-bot"
trust: trusted # User explicitly trusts
hash: "sha256:789abc..." # Re-approve if changes
agents: # Agent-level sandboxing
responder:
paths: ["Brain/projects/client-x.md", "Work/client-x/"]
capabilities: [read]
admin:
paths: ["Brain/**"]
capabilities: [read, write]
experimental-module:
enabled: true
source: github://someuser/cool-module
version: "0.1.0"
trust: untrusted # Runs in separate container
hash: "sha256:xyz789..."
Module Manifest
Every module must have a manifest.yaml at its root defining its metadata, interfaces, agents, and capabilities.
# Required fields
name: brain
version: 1.0.0
description: Knowledge layer for Parachute
# Entry point - must export a class inheriting from Module
module: module.py
# What interfaces this module provides
provides:
- BrainInterface
# What interfaces this module can use (optional dependencies)
optional_requires:
- JournalInterface
- ConversationInterface
# Python dependencies (installed in module's virtualenv)
dependencies:
- pyyaml>=6.0
- aiofiles>=23.0
# Agents defined by this module
agents:
indexer:
description: "Indexes new content into the knowledge graph"
sandbox:
workspace_access: rw # none | ro | rw
allowed_paths:
- Brain/
network: none # none | bridge
memory: 512m
cpus: 1.0
pids_limit: 256
system_prompt: prompts/indexer.md
capabilities:
- Read
- Write
- Edit
# Hooks this module registers
hooks:
- event: daily.entry.created
handler: hooks:on_entry_created
async: true
# MCP tools exposed (callable by other modules' agents)
mcp_tools:
- name: brain_search
description: Search entities by keyword
handler: tools:brain_search
- name: brain_resolve
description: Resolve text to para_id
handler: tools:brain_resolve
# API routes (auto-prefixed with /api/{module_name}/)
routes:
prefix: /api/brain
Module Directory Structure
parachute-brain/
├── manifest.yaml # Required: module metadata
├── module.py # Required: exports BrainModule class
├── interface.py # Interface definition (BrainInterface)
├── store.py # Entity storage
├── search.py # Search implementation
├── tools.py # MCP tool handlers
├── hooks.py # Hook handlers
├── prompts/
│ └── indexer.md # Agent system prompts
├── tests/
│ └── test_brain.py
└── CLAUDE.md # Instructions for AI working on this module
Key Manifest Fields
| Field | Required | Description |
|---|---|---|
name |
Yes | Unique module identifier |
version |
Yes | Semantic version string |
module |
Yes | Python file exporting Module subclass |
provides |
No | Interfaces this module publishes |
optional_requires |
No | Interfaces this module can use (graceful if missing) |
agents |
No | Agent definitions with sandbox configs |
mcp_tools |
No | MCP tools exposed to other modules' agents |
Brain Module & Interface System
Brain is a module with a published interface, not baked into core. This enables users to upgrade, swap, or even skip Brain entirely. Modules depend on interfaces, not implementations.
Interface-Based Architecture
┌─────────────────────────────────────┐
│ Daily Module │
│ • Creates journal entries │
│ • Detects entity mentions │
└──────────────┬──────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ BRAIN MODULE (BrainInterface) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Entity │ │ Para-ID │ │ Relation │ │ Context │ │
│ │ Store │ │ System │ │ Graph │ │ Provider │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Publishes: BrainInterface (search, resolve, link, suggest) │
│ Other modules optionally depend on this interface │
│ │
│ ~/Parachute/Brain/ │
│ types.yaml # Dynamic entity type definitions │
│ entities/ # Markdown files per entity │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Chat Module │
│ • Loads context from Brain │
│ • Updates Brain after sessions │
└─────────────────────────────────────┘
The Daily → Brain → Chat Flow
Here's how knowledge flows through the system:
1. Daily Creates Entry
You journal: "Met with Kevin about the LVB curriculum. He suggested we add a module on prompt engineering."
2. Brain Detects Mentions
Agent (via hook) scans entry and finds:
Kevin→ resolves toperson-abc123(Kevin from Regen Hub)LVB→ resolves toproject-def456(Learn Vibe Build)prompt engineering→ no match, suggests creating topic
Brain auto-links the entry and optionally updates entity files with new context.
3. Chat Loads Context
Later, you open Chat and mention "working on LVB". A hook detects this and:
- Loads
Brain/projects/learn-vibe-build.mdinto context - Loads recent Daily entries mentioning LVB
- If the entity points to
~/Projects/lvb/, loads that project's AGENTS.md
4. Brain Updates from Chat
After the chat, a hook asks: "Update Brain with new information?" New facts about Kevin or LVB get written back to their entity files.
Entities Can Point to External Work
Project entities can reference folders outside the vault, enabling context-aware assistance:
---
para_id: project-parachute
type: project
status: active
work_path: ~/Projects/parachute # External folder
agents_path: ~/Projects/parachute/CLAUDE.md
aliases:
- Parachute
- parachute project
- the vault app
tags:
- open-source
- flutter
- python
---
# Parachute
Open & interoperable extended mind technology.
## Quick Context
- Flutter app + Python base server
- Working on modular architecture restructure
- See [[learn-vibe-build]] for related teaching
## Relationships
- [[aaron-gabriel]] - Founder
- [[kevin-regen-hub]] - Advisor on distribution
Dynamic Entity Types
Entity types are not hardcoded. Users start with sensible defaults but can create their own. Daily can suggest new types based on journal content.
types:
person:
icon: "👤"
folder: "people"
fields: [aliases, tags, relationships]
project:
icon: "📁"
folder: "projects"
fields: [status, work_path, aliases, tags, relationships]
topic:
icon: "💡"
folder: "topics"
fields: [aliases, tags]
resource:
icon: "📚"
folder: "resources"
fields: [url, aliases, tags]
# User-defined types
organization:
icon: "🏢"
folder: "organizations"
fields: [type, aliases, tags, relationships]
event:
icon: "📅"
folder: "events"
fields: [date, location, attendees]
Brain MCP Tools
Available to all modules and agents:
| Tool | Purpose |
|---|---|
brain-search |
Semantic search across all entities |
brain-resolve |
Resolve text mention to para_id (alias resolution) |
brain-link |
Create wiki-style link between entries and entities |
brain-suggest |
Suggest entities/types based on text content |
brain-types |
List/create/modify entity types |
brain-context |
Load entity files + related context for a topic |
Para-ID System
Para-IDs are stable UUIDs that enable cross-module references. Each ID is prefixed with its source module, making it clear where data lives while allowing portable references across the system.
Format
para:{module}:{12-char-alphanumeric}
Examples:
- para:brain:a1b2c3d4e5f6 → Brain entity (person, project, topic)
- para:daily:x7y8z9w0v1u2 → Daily journal entry
- para:chat:m3n4o5p6q7r8 → Chat session
Why Para-IDs Matter
- Cross-module linking: Daily entry can reference a Brain entity without knowing how Brain stores it
- Portable references: IDs survive if Brain switches from markdown files to a graph database
- Audit trail: Easy to trace what accessed what
- Stable pointers: Content can move/rename without breaking links
Module Responsibility
Each module maintains its own ID → location mapping:
class EntityStore:
def resolve(self, para_id: str) -> Optional[Path]:
"""Map para_id to actual file location."""
# para:brain:a1b2c3d4e5f6 → Brain/entities/people/kevin.md
record = self.db.get(para_id)
return Path(record.file_path) if record else None
def create(self, entity_type: str, name: str) -> str:
"""Create new entity, return para_id."""
para_id = f"para:brain:{generate_id()}"
file_path = f"Brain/entities/{entity_type}/{slugify(name)}.md"
self.db.insert(para_id=para_id, file_path=file_path)
return para_id
Cross-Module References
When Daily mentions a Brain entity, the [[Kevin]] syntax is resolved at runtime via Brain's resolve() function, and the para_id is stored in frontmatter for stable linking:
# Daily/entries/2024-01-15-morning.md
---
mentions:
- para:brain:a1b2c3d4e5f6 # Kevin
- para:brain:b2c3d4e5f6g7 # LVB project
---
Had a great call with [[Kevin]] about [[Learn Vibe Build]]...
Module System
All modules are vault modules. A fresh vault starts empty—Parachute Computer offers to install official modules (Daily, Chat) during onboarding, but they install into the vault just like any other module.
from parachute.core import Module, route, mcp_tool, event
from parachute.core import Module, route, mcp_tool, event
class DailyModule(Module):
"""Daily journaling and reflection module."""
name = "daily"
version = "1.0.0"
description = "Voice-first journaling with agent reflections"
# Declare required permissions
permissions = {
"read": ["Daily/**", "parachute/memory.md"],
"write": ["Daily/**"],
"bash": False,
"mcp": ["daily-journal", "daily-search"],
}
# HTTP Routes (exposed at /api/daily/*)
@route("GET", "/entries")
async def list_entries(self, date: str = None, limit: int = 50):
"""List journal entries."""
...
@route("POST", "/query")
async def query_streaming(self, message: str, session_id: str = None):
"""Stream a conversation about journal entries."""
async for event in self.core.query(
message=message,
session_id=session_id,
mcp_servers=self.get_mcp_servers(),
):
yield event
# MCP Tools (exposed to Claude agent)
@mcp_tool("daily-journal")
async def search_journals(self, query: str, date_from: str = None):
"""Search journal entries by content."""
...
# Event handlers
@event("daily.entry.created")
async def on_entry_created(self, entry_id: str, content: str):
"""Handle new journal entry - trigger reflection pipeline."""
# Check if Brain module is available (optional dependency)
brain = self.get_interface("BrainInterface")
if brain:
mentions = await brain.suggest(content)
for mention in mentions:
if mention.confidence > 0.8:
await brain.link(entry_id, mention.para_id)
await self.trigger_reflection_agent(entry_id)
Trusted Module Communication
Trusted modules can directly import and call each other's Python code (they run in the same process):
# In a trusted module, you can import other trusted modules directly
from parachute.core.brain import brain
from parachute.modules.daily import daily_module
# Direct function calls - no HTTP overhead
entities = await brain.search("Kevin")
entries = await daily_module.get_recent_entries(limit=5)
# This only works for trusted modules in the main container
# Untrusted modules must use the HTTP API
Module Discovery Flow
- Server starts and loads
~/.parachute/modules.yaml - For each enabled module:
- If
source: builtin→ Load fromparachute/modules/{name}/ - If
source: vault→ Load from{vault}/{path}/module.py
- If
- Validate module class against interface
- Check permissions against policy
- Register routes at
/api/{name}/* - Start MCP servers if declared
- Call
on_enable()lifecycle hook
Core Modules (Daily, Chat)
Official Parachute modules available for easy installation. These are vault modules like any other—just signed and verified by Parachute.
Daily Module Verified (Parachute)
Voice-first journaling with AI-powered reflections. The foundation of Parachute's "extended mind" experience.
Data Location: ~/Parachute/Daily/
Source: parachute://daily
MCP Tools: daily-search, daily-today, daily-create
Events: daily.entry.created, daily.reflection.completed
Chat Module Verified (Parachute)
Conversational AI interface with agentic capabilities. Session management, context loading, and Brain-aware conversations.
Data Location: ~/Parachute/Chat/
Source: parachute://chat
MCP Tools: chat-sessions, chat-context
Events: chat.session.created, chat.session.message
Example: User-Created Module
Here's what a Telegram bot module might look like—with agent-level sandboxing:
Telegram Bot User-Created
Custom bot that responds to Telegram messages with context from a specific project.
Location: ~/Parachute/Modules/telegram-bot/
from parachute.core import Module, route, agent
from parachute.core.brain import brain
class TelegramBotModule(Module):
name = "telegram-bot"
version = "0.1.0"
# Define agents with different sandbox scopes
agents = {
"responder": {
# This agent can only see one project
"paths": [
"Brain/projects/client-x.md",
"~/Work/client-x/"
],
"capabilities": ["read"],
},
"admin": {
# Admin agent has broader access
"paths": ["Brain/**"],
"capabilities": ["read", "write"],
}
}
@route("POST", "/webhook")
async def handle_telegram(self, message: dict):
# Use the scoped responder agent
response = await self.run_agent(
"responder",
f"Answer this question: {message['text']}"
)
return {"reply": response}
Skills vs Modules
Skills and modules serve different purposes. Understanding the distinction prevents confusion about where functionality should live.
| Aspect | Modules | Skills |
|---|---|---|
| What they are | Python packages extending Parachute | Claude Code agent skills (markdown instructions) |
| Language | Python | Markdown |
| Runs where | Server process | Inside agent sandbox |
| Provides | Endpoints, MCPs, interfaces | Agent instructions |
| Installed via | parachute modules install |
Copy to skills folder |
| Trust level | Fully trusted (main process) | Runs in agent's sandbox |
| Where they live | ~/.modules/ |
~/Parachute/.claude/skills/ or {project}/.claude/skills/ |
How They Interact
Modules control which skills agents can access:
agents = {
"workspace": AgentConfig(
sandbox=SandboxConfig(
workspace_access="rw",
allowed_paths=["${CWD}"],
),
# Agent can use workspace-level skills
allow_workspace_skills=True,
# Agent can use user-level skills
allow_user_skills=True,
),
"restricted": AgentConfig(
sandbox=SandboxConfig(workspace_access="none"),
# No access to any skills
allow_workspace_skills=False,
allow_user_skills=False,
),
}
~/Parachute/.claude/skills/remotion.md, not as a module. It's just agent instructions for using the Remotion CLI. If Remotion needed server-side infrastructure (like a render queue), that would be a module.
Flutter App Architecture
The current monolith becomes multiple packages sharing a core.
Current (Monolith)
app/
lib/
features/
chat/
daily/
vault/
settings/
core/
services/
providers/
Future (Modular)
app/
packages/
parachute_core/ ← Shared
parachute_daily/ ← Daily-specific
parachute_chat/ ← Chat-specific
parachute_computer/ ← Management UI
apps/
daily/ ← Standalone app
chat/ ← Standalone app
computer/ ← Full app
Package Contents
| Package | Contents | Optional? |
|---|---|---|
parachute_core |
Server connection, auth, theming, common widgets, sync | Required by all |
parachute_daily |
Journal UI, voice input, entry management | - |
parachute_chat |
Chat UI, session management, message bubbles | - |
parachute_computer |
Module management, permissions, settings | - |
Unified Shell Architecture
The shell application dynamically loads module UIs based on what's installed on the server.
class ParachuteApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ShellScaffold(
// Modules register their UI widgets
modules: [
if (moduleRegistry.hasModule('daily'))
ModuleTab(
name: 'Daily',
icon: Icons.today,
builder: () => DailyApp(), // From parachute-daily/flutter
),
if (moduleRegistry.hasModule('chat'))
ModuleTab(
name: 'Chat',
icon: Icons.chat,
builder: () => ChatApp(), // From parachute-chat/flutter
),
],
),
);
}
}
Each module's Flutter package exports a top-level widget that the shell loads:
class DailyApp extends StatelessWidget {
// This is the entry point the shell loads
// It receives server connection from app-core
}
pubspec.yaml Dependencies
dependencies:
flutter:
sdk: flutter
parachute_daily:
path: ../parachute-daily/flutter # or git reference
parachute_chat:
path: ../parachute-chat/flutter
Agent Sandboxing
Agents run in Docker containers with configurable isolation. This is based on OpenClaw's battle-tested sandboxing model.
Sandbox Configuration
Defined in module's manifest.yaml or at runtime:
@dataclass
class SandboxConfig:
# Filesystem access
workspace_access: str = "none" # none | ro | rw
allowed_paths: list[str] = [] # Paths within vault (e.g., ["Daily/", "Brain/"])
# Resource limits
memory: str = "512m"
cpus: float = 1.0
pids_limit: int = 256
# Network
network: str = "none" # none | bridge
# Security
read_only_root: bool = True
cap_drop: list[str] = ["ALL"]
Container Implementation
When an agent runs, parachute-computer:
- Creates a Docker container from
parachute-sandbox:latest(Debian slim) - Mounts vault paths as specified in
allowed_paths - Sets resource limits
- Runs Claude SDK inside the container
- Captures output and cleans up
FROM debian:bookworm-slim
# Install Claude CLI and dependencies
RUN apt-get update && apt-get install -y \
python3 python3-pip nodejs npm curl \
&& npm install -g @anthropic-ai/claude-code \
&& rm -rf /var/lib/apt/lists/*
# Non-root user
RUN useradd -m -u 1000 parachute
USER parachute
WORKDIR /workspace
Module Hash Verification
Protects against agents modifying their own module code:
modules:
telegram-bot:
source: local
path: "Modules/telegram-bot"
trust: trusted
hash: "sha256:a1b2c3d4e5f6..." # Computed on first load
# On server restart:
# 1. Compute hash of module.py
# 2. If hash differs from stored → require user re-approval
# 3. This prevents agents from modifying their own code
If a module's code changes (even a single character), the user must re-approve it before it loads. This prevents prompt injection attacks where an agent tries to modify its own permissions.
Trust Levels
| Level | Description | Behavior |
|---|---|---|
| Verified | Signed by Parachute (Daily, Chat) | Runs in main container, signature checked |
| Trusted | User explicitly trusts this module | Runs in main container, hash verified |
| Untrusted | Third-party or experimental | Separate container (future), hash verified |
Agent-Level Sandboxing
The real sandboxing happens when Claude SDK runs. Modules define agent types with scoped permissions:
class TelegramBotModule(Module):
# Module runs in main process (trusted)
# But its AGENTS run in sandboxed containers
agents = {
"responder": {
# Tightly scoped - only sees one project
"paths": [
"Brain/projects/client-x.md",
"~/Work/client-x/"
],
"capabilities": ["read"],
"bash": False,
"network": False,
},
"admin": {
# Broader access but still scoped
"paths": ["Brain/**", "Daily/**"],
"capabilities": ["read", "write"],
"bash": ["git", "npm"],
"network": ["api.telegram.org"],
}
}
async def handle_message(self, text: str):
# When this runs Claude SDK, it uses the "responder" sandbox
return await self.run_agent("responder", text)
Container Architecture
┌─────────────────────────────────────────────────────────────────┐ │ Host Machine │ │ │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ Main Container (Parachute Computer) │ │ │ │ • FastAPI server (port 3333) │ │ │ │ • Brain module (if installed) │ │ │ │ • All trusted modules (loaded as Python) │ │ │ │ • Module hash verification on load │ │ │ │ │ │ │ │ Volumes: │ │ │ │ /vault ← ~/Parachute (read-write) │ │ │ │ /app ← Server code (read-only) │ │ │ │ │ │ │ │ When Claude SDK runs → spawns agent sandbox │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ Agent Sandbox (per Claude SDK run) │ │ │ │ • Scoped filesystem access (per agent definition) │ │ │ │ • Scoped bash commands (if any) │ │ │ │ • Scoped network access (if any) │ │ │ │ • HOME=/vault for SDK session storage │ │ │ └───────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘
Permission Flow
- Module loads → Hash checked against stored value
- If hash changed → User must re-approve
- Module calls
run_agent()→ Core looks up agent sandbox definition - Claude SDK spawns → Runs with scoped permissions from agent definition
- Agent tries to access file → Permission handler checks against allowed paths
Chat Sandbox Modes
The Chat module defines three agent configurations with different trust levels.
| Mode | Trust | Sandbox | Use Case |
|---|---|---|---|
| Default | Low | Locked | General conversation (like Claude.ai) |
| Workspace | Medium | Scoped to cwd | Working in a project directory |
| Trusted | High | None | Power user, full access |
class ChatModule(Module):
name = "chat"
provides = ["ConversationInterface"]
optional_requires = ["BrainInterface", "JournalInterface"]
agents = {
"default": AgentConfig(
description="Safe general assistant (like Claude.ai)",
sandbox=SandboxConfig(
workspace_access="none",
network="bridge", # Can call MCPs
),
allowed_interfaces=["BrainInterface", "JournalInterface"],
capabilities=["Read"], # Can read vault via interfaces, not direct FS
system_prompt="prompts/default.md",
),
"workspace": AgentConfig(
description="Project-focused assistant with file access",
sandbox=SandboxConfig(
workspace_access="rw",
allowed_paths=["${CWD}"], # Scoped to selected directory
network="bridge",
),
capabilities=["Read", "Write", "Edit", "Bash"],
system_prompt="prompts/workspace.md",
load_cwd_claude_md=True, # Load CLAUDE.md from selected directory
),
"trusted": AgentConfig(
description="Full access (power user mode)",
sandbox=None, # No sandbox
requires_explicit_approval=True,
capabilities=["Read", "Write", "Edit", "Bash", "Computer"],
system_prompt="prompts/trusted.md",
),
}
System Prompt Composition
For each mode, system prompts compose differently:
| Mode | Prompt Components |
|---|---|
| Default | Chat module base prompt → Brain context → Journal context |
| Workspace | Workspace prompt → CLAUDE.md from cwd → Brain context → Project-specific context |
| Trusted | Trusted prompt → Full Brain access → All CLAUDE.md files in hierarchy |
Chat Bot Integration
The Chat module supports direct integration with external messaging platforms (Telegram, Discord), enabling conversations that can be viewed and managed from both the bot interface and the Parachute UI.
Concept
A Parachute session can be linked to an external bot conversation:
- One Parachute session = one Telegram DM, one Discord channel, or one group chat
- Messages flow through Parachute, which orchestrates Claude responses
- The bot is a lightweight interface; Parachute is the source of truth
- Users can "zoom into" any bot conversation in the Chat UI to see full thinking context
┌─────────────────────────────────────────────────────────────────┐
│ Parachute │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Chat Session │ ←→ │ Claude Agent │ ←→ │ Brain/Journal │ │
│ │ (SQLite) │ │ (sandboxed) │ │ Interfaces │ │
│ └──────┬──────┘ └──────────────┘ └──────────────────┘ │
│ │ │
│ │ linked_bot: telegram:123456 │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Bot Connectors │ │
│ │ ┌────────────┐ ┌─────────────┐ ┌────────────────────┐ │ │
│ │ │ Telegram │ │ Discord │ │ Future: Slack... │ │ │
│ │ └────────────┘ └─────────────┘ └────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Session Linking
Each chat session can optionally be linked to an external bot chat:
session:
id: "abc123"
title: "Project Discussion"
linked_bot:
platform: "telegram" # telegram | discord
chat_id: "123456789" # Platform-specific chat ID
chat_type: "dm" # dm | group
linked_at: "2024-01-15T10:30:00Z"
Bot Connector Architecture
Each platform has a connector that receives messages, finds/creates the linked Parachute session, passes the message to the Chat module, and sends Claude's response back through the platform.
class TelegramConnector:
"""Bridges Telegram messages to Parachute sessions."""
async def on_message(self, update: TelegramUpdate):
chat_id = update.message.chat.id
# Find or create linked session
session = await self.session_service.get_by_bot_link(
platform="telegram",
chat_id=str(chat_id)
)
if not session:
session = await self.session_service.create(
title=f"Telegram: {update.message.chat.title or 'DM'}",
linked_bot={
"platform": "telegram",
"chat_id": str(chat_id),
"chat_type": "group" if update.message.chat.type != "private" else "dm"
}
)
# Process through Chat module (respects session permissions)
response = await self.chat_module.send_message(
session_id=session.id,
content=update.message.text,
source="telegram"
)
# Send response back to Telegram
await self.bot.send_message(
chat_id=chat_id,
text=response.content
)
Per-Session Permissions
Each session has its own permission configuration, critical for bot-linked sessions where different chats need different access levels:
@dataclass
class SessionPermissions:
"""Per-session access control."""
# Folder access (paths within vault the agent can see)
allowed_folders: list[str] = field(default_factory=list)
# e.g., ["Projects/parachute/", "Brain/"]
# MCP tools this session can use
allowed_mcps: list[str] | None = None # None = all, [] = none
# e.g., ["brain_search", "brain_resolve"]
# Agent capabilities
capabilities: list[str] = field(default_factory=lambda: ["Read"])
# e.g., ["Read", "Write", "Edit", "Bash"]
# Sandbox config override (if None, uses mode default)
sandbox_override: SandboxConfig | None = None
System Prompt Awareness
The agent's system prompt is dynamically constructed to include its permissions context, so it "knows" what folders, tools, and capabilities it has access to. Permissions are enforced at both prompt level AND sandbox level—the agent literally cannot exceed its configured boundaries.
def build_system_prompt(session: Session, mode: str) -> str:
"""Compose system prompt with permission awareness."""
base_prompt = load_prompt(f"prompts/{mode}.md")
# Add folder access context
if session.permissions.allowed_folders:
folder_context = f"""
## Folder Access
You have access to the following folders in the vault:
{chr(10).join(f'- {f}' for f in session.permissions.allowed_folders)}
You cannot access files outside these folders.
"""
else:
folder_context = """
## Folder Access
You do not have direct filesystem access in this session.
Use available interfaces (Brain, Journal) to access information.
"""
return f"{base_prompt}\n\n{folder_context}"
Configuration UI
In the Chat app, users can configure per-session permissions:
┌─────────────────────────────────────────────────────────────────┐
│ Session: Project Discussion │
│ Linked: Telegram DM (@username) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Mode: [Workspace ▼] │
│ │
│ ─── Folder Access ───────────────────────────────────────────── │
│ [✓] Projects/parachute/ │
│ [✓] Brain/ │
│ [ ] Daily/ │
│ [ ] Full vault access │
│ │
│ ─── MCP Tools ───────────────────────────────────────────────── │
│ [✓] brain_search │
│ [✓] brain_resolve │
│ [ ] web_fetch │
│ [ ] All tools │
│ │
│ ─── Capabilities ────────────────────────────────────────────── │
│ [✓] Read files │
│ [✓] Write files │
│ [✓] Edit files │
│ [ ] Run commands (Bash) │
│ │
│ [Save Permissions] │
└─────────────────────────────────────────────────────────────────┘
Bot Setup (Settings)
Global bot configuration in the Settings screen:
┌─────────────────────────────────────────────────────────────────┐
│ Bot Integrations │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ─── Telegram ────────────────────────────────────────────────── │
│ Status: ● Connected │
│ Bot: @MyParachuteBot │
│ Token: •••••••••••••••••••• [Edit] [Remove] │
│ │
│ ─── Discord ─────────────────────────────────────────────────── │
│ Status: ○ Not configured │
│ [Add Discord Bot] │
│ │
│ ─── Default Permissions for New Bot Sessions ────────────────── │
│ Mode: [Default ▼] │
│ Folder Access: [None - use interfaces only] │
│ MCP Tools: [Brain tools only] │
│ │
└─────────────────────────────────────────────────────────────────┘
Use Cases
| Use Case | Description | Permissions |
|---|---|---|
| Personal Assistant via Telegram | DM your bot for quick queries, see full context in Parachute later | Default mode, Brain interfaces only |
| Project Channel | Link a Discord channel to a workspace-mode session | Workspace mode, project folder access |
| Shared Family Bot | Group chat with restricted permissions | Default mode, read-only Brain access |
| ClawdBot-Style Setup | Full-featured bot with thinking visible in Parachute UI | Workspace/Trusted mode, full access |
MVP Scope
- Telegram connector (single bot token in settings)
- Session linking (one session = one chat)
- Per-session folder access configuration
- Per-session MCP tool filtering
- Settings UI for bot configuration
- Session permissions UI in chat detail view
Suggestions System
Sandboxed agents can propose changes rather than executing them directly. This creates a trust-building loop where users (or orchestrator agents) approve suggestions before they take effect.
Why Suggestions?
- Safety: Sandboxed agents can't accidentally damage data
- Trust building: Log of approved/rejected suggestions helps calibrate agent behavior
- User control: Clear approval points for sensitive operations
- Orchestration: Parent agents can filter/combine child agent suggestions
Suggestion Hierarchy
Sandboxed Agent
│
│ suggests: "Update Kevin entity with new project"
↓
Orchestrator Agent (optional)
│
│ filters/batches suggestions
│ suggests: "3 entity updates from today's journal"
↓
User
│
│ reviews and approves/rejects
↓
Action Executed
Suggestion Data Structure
@dataclass
class Suggestion:
id: str
source_agent: str # Which agent proposed this
source_session: str # Which session it came from
timestamp: datetime
# What's being suggested
action: str # "create" | "update" | "delete" | "link"
target_module: str # "brain" | "daily" | "chat"
target_id: Optional[str] # para_id if updating existing
# The actual change
payload: dict # Module-specific data
reasoning: str # Why the agent thinks this is good
# Resolution
status: str # "pending" | "approved" | "rejected" | "modified"
resolved_by: Optional[str] # "user" | "orchestrator:{id}"
resolved_at: Optional[datetime]
modifications: Optional[dict] # If user tweaked the suggestion
User Approval UI
┌─────────────────────────────────────────────────────────────────┐
│ Pending Suggestions (3) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 📝 Update: Kevin [Brain] │ │
│ │ │ │
│ │ Add to "Recent Activity": │ │
│ │ "- 2024-01-15: Discussed LVB curriculum with Aaron" │ │
│ │ │ │
│ │ Source: Daily reflection • 2 min ago │ │
│ │ │ │
│ │ [Reject] [Edit] [Approve ✓] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ✨ Create: AI Safety (topic) [Brain] │ │
│ │ │ │
│ │ "Emerging area of interest based on journal mentions" │ │
│ │ │ │
│ │ Source: Daily reflection • 2 min ago │ │
│ │ │ │
│ │ [Reject] [Edit] [Approve ✓] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ [Approve All] [Review Later] │
└─────────────────────────────────────────────────────────────────┘
Module Permission UI
When installing or managing modules, users see clear permission screens similar to OAuth consent flows.
Installation Flow
┌─────────────────────────────────────────────────────────────────┐
│ Install Module: parachute-contacts │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 📇 Contacts Module v1.0.0 │
│ by OpenParachutePBC │
│ │
│ Sync and manage your contacts with AI-powered enrichment. │
│ │
│ ─── This module requests access to: ─────────────────────────── │
│ │
│ ✓ Create folder: Contacts/ │
│ ✓ Read/Write: Contacts/** │
│ ✓ Read: Brain/** (to link contacts to entities) │
│ │
│ ─── Agents this module can run: ─────────────────────────────── │
│ │
│ • contact_enricher │
│ - Network access: Yes (to fetch public info) │
│ - File access: Contacts/ only │
│ - Can suggest: Brain entity updates │
│ │
│ • contact_sync │
│ - Network access: Yes (CardDAV servers) │
│ - File access: Contacts/ only │
│ │
│ ─── MCP tools provided: ─────────────────────────────────────── │
│ │
│ • contacts_search - Search contacts by name/email │
│ • contacts_get - Get contact details │
│ • contacts_suggest_link - Suggest Brain entity link │
│ │
│ [Cancel] [Install & Approve] │
└─────────────────────────────────────────────────────────────────┘
Module Management View
┌─────────────────────────────────────────────────────────────────┐
│ Installed Modules │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 🧠 Brain v1.2.0 ✓ Active │ │
│ │ Knowledge layer with entity management │ │
│ │ │ │
│ │ Permissions: Read/Write Brain/**, MCP: brain_* │ │
│ │ Agents: indexer (sandboxed), linker (sandboxed) │ │
│ │ │ │
│ │ [Permissions] [Disable] [Remove] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 📔 Daily v1.0.0 ✓ Active │ │
│ │ Voice journaling and reflection │ │
│ │ │ │
│ │ Permissions: Read/Write Daily/**, Read Brain/** │ │
│ │ Agents: reflection (sandboxed) │ │
│ │ │ │
│ │ [Permissions] [Disable] [Remove] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ [+ Install Module] │
└─────────────────────────────────────────────────────────────────┘
Maintenance Agent
A special agent configuration for system administration tasks. It has broad local access but restricted network access, preventing accidental data exfiltration while allowing powerful local operations.
Purpose
- Fix configuration issues
- Manage modules
- Debug problems
- Update settings
- Clean up data
Configuration
agents = {
"maintenance": AgentConfig(
description="System maintenance with local-only access",
sandbox=SandboxConfig(
workspace_access="rw",
allowed_paths=[
".parachute/", # Config files
".modules/", # Module code (read for debugging)
"Chat/", # Can fix chat data
"Daily/", # Can fix daily data
"Brain/", # Can fix brain data
],
network="none", # NO network access - can't exfiltrate
memory="1g",
cpus=2.0,
),
capabilities=["Read", "Write", "Edit", "Bash"],
system_prompt="prompts/maintenance.md",
# Extra safety: require explicit user invocation
requires_explicit_invocation=True,
),
}
Invocation
The maintenance agent is invoked explicitly, not as a default mode:
# In chat
User: /maintenance
System: Starting maintenance agent (local access only, no network)...
User: Why aren't my daily entries syncing?
Agent: Let me check the Daily module configuration and recent logs...
# Via CLI
parachute maintenance "Check why brain search is slow"
API Key Scoping
API keys are scoped by module to provide fine-grained access control for multi-device access.
api_keys:
- key_hash: "sha256:abc123..."
name: "iPhone"
created: "2024-01-15T10:30:00Z"
scopes:
- module: "daily"
capabilities: ["read", "write"]
- module: "chat"
capabilities: ["read", "write"]
- module: "brain"
capabilities: ["read"] # Can query but not modify
- key_hash: "sha256:def456..."
name: "Shared Family Device"
scopes:
- module: "daily"
capabilities: ["read"] # Read-only access to journal
- key_hash: "sha256:ghi789..."
name: "Admin Device"
scopes:
- module: "*"
capabilities: ["read", "write", "admin"] # Full access + module management
Enforcement Flow
- Request arrives with
Authorization: Bearer <key> - Server hashes key (SHA-256), looks up in registry
- Extracts module from route (e.g.,
/api/daily/entries→daily) - Checks if key has scope for that module + capability
- Returns 403 Forbidden if not authorized
Special Scopes
| Scope | Description |
|---|---|
module: "*" |
Access all modules |
capabilities: ["admin"] |
Module management (approve upgrades, install/remove modules) |
| Localhost bypass | Configurable via auth_mode: remote|always|disabled |
Inter-Module Communication
Multiple patterns for modules to communicate, depending on trust level.
Trusted Modules: Direct Python
Trusted modules run in the same process and can import each other directly:
# In Daily module
from parachute.core.brain import brain
from parachute.modules.chat import chat_module
# Direct function calls - no serialization, no HTTP
mentions = await brain.suggest(entry_content)
recent_chats = await chat_module.get_sessions(topic="LVB")
All Modules: Standard Patterns
| Pattern | Use Case | Example |
|---|---|---|
| Direct Import | Trusted module → trusted module | from parachute.core.brain import brain |
| Internal HTTP | Untrusted module → any module | POST /api/brain/resolve |
| MCP Tools | Agent using module capabilities | Chat agent calls brain-search tool |
| Event Bus | Decoupled reactions | Hook listens for daily.entry.created |
Hooks System
User-configurable scripts triggered by system events. More flexible than hardcoded curator logic.
hooks/
├── on-session-start.py # Load context files, inject memory
├── on-session-end.py # Summarize to memory, cleanup
├── on-daily-entry.py # Trigger reflection, link mentions
├── on-context-limit.py # Memory flush before compaction
└── on-project-mention.py # Load project AGENTS.md
Hook Configuration
Hooks follow Claude Code's compatible format—stdin JSON, exit codes, stdout response:
hooks:
daily.entry.created:
- type: "command"
command: "python -m parachute_brain.hooks.detect_mentions"
timeout: 30
- type: "command"
command: "/path/to/custom/backup-script.sh"
async: true # Non-blocking
session.end:
- type: "command"
command: "python -m parachute.hooks.activity_summary"
Hook I/O
Hooks receive JSON on stdin and return results via exit code:
{
"event": "daily.entry.created",
"timestamp": "2024-01-15T10:30:00Z",
"vault_path": "/Users/aaron/Parachute",
"module": "daily",
"data": {
"entry_id": "2024-01-15-morning",
"entry_path": "/Users/aaron/Parachute/Daily/entries/2024-01-15-morning.md"
}
}
- Exit 0 = success (stdout parsed as JSON if present)
- Exit 2 = blocking error (stderr fed to caller, action blocked if blockable)
- Other = non-blocking error (logged, execution continues)
Hook Events
| Event | Trigger | Context Provided |
|---|---|---|
session.start |
New chat/daily session begins | session_id, module, working_directory |
session.end |
Session closes or times out | session_id, message_count, duration |
daily.entry.created |
New journal entry saved | entry_id, content, audio_path |
context.limit.approaching |
Context window 80% full | session_id, token_count, threshold |
brain.mention.detected |
Entity mentioned in text | source_id, para_id, confidence |
Observability & Audit
Track what's calling what across the system. This builds trust and enables debugging.
Event Log
Every significant action is logged:
@dataclass
class AuditEvent:
timestamp: datetime
event_type: str # "api_call" | "mcp_call" | "module_call" | "agent_start" | "suggestion"
# Source
source_type: str # "flutter_app" | "api_key" | "agent" | "module" | "hook"
source_id: str # Session ID, API key name, agent ID, etc.
# Target
target_module: str
target_endpoint: str # "/api/brain/search" or "brain.resolve()"
# Details
para_ids_involved: list[str] # Any para_ids referenced
duration_ms: int
success: bool
error: Optional[str]
Storage
~/Parachute/.parachute/
└── audit/
├── 2024-01-15.jsonl # One file per day
├── 2024-01-16.jsonl
└── ...
Benefits
| Benefit | Description |
|---|---|
| Debugging | "Why did this agent access that file?" |
| Trust | Users can see exactly what's happening |
| Performance | Identify slow endpoints or excessive calls |
| Security | Detect unusual access patterns |
Visualization (Future)
A knowledge graph view showing module interactions:
┌─────────────────────────────────────────────────────────────────┐
│ System Activity (Last 24h) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ │
│ │ Daily │ │
│ └────┬────┘ │
│ │ 47 calls │
│ ↓ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Brain │ ←────── │ Chat │ │
│ └─────────┘ 156 └─────────┘ │
│ ↑ calls │
│ │ 12 calls │
│ ┌────┴────┐ │
│ │ Flutter │ │
│ │ App │ │
│ └─────────┘ │
│ │
│ Recent: brain.search("Kevin") → 3 results (23ms) │
│ daily.create_entry() → para:daily:x7y8z9 (156ms) │
│ chat.send_message() → streaming (2.3s) │
│ │
└─────────────────────────────────────────────────────────────────┘
Claude Authentication
Parachute uses Claude's native authentication rather than managing API keys directly.
How It Works
Users authenticate with Claude directly:
# On the host machine (one-time setup)
claude login
This stores OAuth credentials in ~/.claude/.credentials.json. With the HOME override, credentials land in ~/Parachute/.claude/.credentials.json, making them portable with the vault.
Credential Discovery
The Claude SDK checks in order:
ANTHROPIC_API_KEYenvironment variableCLAUDE_CODE_OAUTH_TOKENenvironment variable- Stored credentials in
~/.claude/.credentials.json
Parachute relies on option 3 with the HOME override.
Headless / Remote Servers
For servers without a browser (Linux VPS, Raspberry Pi):
# On a machine with a browser
claude login
# Copy credentials to remote server
scp ~/.claude/.credentials.json user@server:~/Parachute/.claude/
Why Not API Keys?
- Simpler: Users don't manage a separate API key
- Consistent: Same auth model as Claude Code CLI
- Secure: OAuth tokens are scoped and refreshable
- Portable: Credentials move with vault
Testing Strategy
Comprehensive testing across all layers: unit, integration, and end-to-end.
Backend Testing (Python)
Framework: pytest + pytest-asyncio
parachute-computer/tests/
├── unit/
│ ├── test_module_loader.py
│ ├── test_interface_registry.py
│ └── test_sandbox.py
├── integration/
│ ├── test_module_lifecycle.py
│ └── test_hook_execution.py
└── api/
├── test_modules_api.py
└── test_health.py
Frontend Testing (Flutter)
Frameworks: Widget tests (built-in), Integration tests (built-in), Patrol for E2E
End-to-End Testing
Flow: Start test server → Run Flutter app → Execute Patrol tests
#!/bin/bash
set -e
# Start test server
VAULT_PATH=~/ParachuteTest PORT=3334 parachute start &
sleep 5
# Run backend tests
cd parachute-computer && pytest
cd parachute-brain && pytest
cd parachute-daily/python && pytest
cd parachute-chat/python && pytest
# Run Flutter tests
cd parachute-app-core
flutter test
patrol test --target integration_test/e2e_test.dart
# Cleanup
parachute stop
Agentic Testing
For dynamic testing beyond scripted scenarios, leverage Claude to explore the app:
claude --prompt "
You are testing the Parachute app running on localhost:3334.
Explore the Daily module:
1. Create a voice entry (simulate with text)
2. Verify it appears in the entry list
3. Check that Brain linking works if Brain is installed
4. Report any issues you find
"
CI/CD Pipeline
GitHub Actions workflow runs on every push:
- Backend job: Python tests on ubuntu-latest
- Frontend job: Flutter tests on macos-latest
- E2E job: Full integration after backend + frontend pass
Migration Plan
1-2 week sprint creating new repositories with selective code migration from existing system. Test vault at ~/ParachuteTest/ on port 3334.
~/Projects/parachute/. Existing system stays untouched as reference.
Repository Structure
github.com/OpenParachutePBC/
├── parachute-computer # Python server + module system (NO Brain!)
├── parachute-brain # Brain module - publishes BrainInterface
├── parachute-daily # Daily module (Python + Flutter)
├── parachute-chat # Chat module (Python + Flutter)
├── parachute-app-core # Shared Flutter package
└── parachute # (existing - reference only)
1 Foundation Days 1-3
Core infrastructure with dummy module validation.
- Create
parachute-computerrepo with FastAPI server skeleton - Implement Brain (entity store, para-id, types, search)
- Module base class with
@route,@mcp_tool,@eventdecorators - Module loader (reads
modules.yaml, validates hash, registers routes) - Create
parachute-app-corewith transcription services - Set up test vault at
~/ParachuteTest/
2 Daily Module Days 4-6
Daily working end-to-end with Brain integration.
- Create
parachute-dailyrepo (Python module + Flutter package) - Port voice transcription and entry management
- Implement minimal reflection agent (validates SDK integration)
- Daily → Brain flow: detect mentions, link entities
- Verify offline-first entry creation works
3 Chat Module Days 7-9
Chat working with Brain context loading.
- Create
parachute-chatrepo (Python module + Flutter package) - Port streaming chat and session management
- Brain → Chat flow: load context when entities mentioned
- Working directory / context folder selection
- Post-session Brain update hooks
4 Integration & Hardening Days 10-14
Full system with security features.
- Hooks system (discovery, event firing, default hooks)
- Agent-level sandboxing (scoped permissions for Claude SDK runs)
- Module hash verification (re-approve if code changes)
- System prompt composition (module + Brain + hooks)
- End-to-end validation: Daily → Brain → Chat flow
Test Environment
| Setting | Test | Production |
|---|---|---|
| Vault | ~/ParachuteTest/ |
~/Parachute/ |
| Server Port | 3334 | 3333 |
| Codebase | New repos | Existing parachute/ |
Decisions Made
Key architectural decisions resolved during planning.
Architecture
| Question | Decision |
|---|---|
| Brain: module or core? | Module. Publishes BrainInterface. Other modules optionally depend on interface, not implementation. Can be upgraded/swapped. |
| Module sandboxing level? | Agent-level. Modules are trusted Python. Sandboxing applies when Claude SDK runs. |
| Built-in vs third-party modules? | No distinction. All modules are vault modules. Parachute modules are just signed/verified. |
| Module communication? | Direct Python imports for trusted modules. HTTP/MCP for untrusted. |
| System prompts? | Layered. Module base + Brain entity context + hook injection. |
| Transcription location? | App-core. Used by both Daily and Chat, stays in shared package. |
| SDK session storage? | In vault. HOME=~/Parachute so .claude/ travels with data. |
Remaining Questions (Decide During Implementation)
- Module hot-reload: Can modules update without server restart?
- Relationship types: Should Brain relationships be typed or freeform?
- Reflection agent scope: What should the minimal reflection agent do exactly?