Skip to content

Commit 2c5c606

Browse files
phernandezclaude
andauthored
feat: Implement cloud mount CLI commands for local file access (#306)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude <noreply@anthropic.com>
1 parent a1d7792 commit 2c5c606

8 files changed

Lines changed: 1108 additions & 68 deletions

File tree

justfile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ test-int:
1818
# Run all tests
1919
test: test-unit test-int
2020

21+
# Lint and fix code (calls fix)
22+
lint: fix
23+
2124
# Lint and fix code
22-
lint:
23-
uv run ruff check . --fix
25+
fix:
26+
uv run ruff check --fix --unsafe-fixes src tests
2427

2528
# Type check code
2629
typecheck:
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
---
2+
title: 'SPEC-7: POC to spike Tigris/Turso for local access to cloud data'
3+
type: spec
4+
permalink: specs/spec-7-poc-tigris-turso-local-access-cloud-data
5+
tags:
6+
- poc
7+
- tigris
8+
- turso
9+
- cloud-storage
10+
- architecture
11+
- proof-of-concept
12+
---
13+
14+
# SPEC-7: POC to spike Tigris/Turso for local access to cloud data
15+
16+
## Why
17+
18+
Current basic-memory-cloud architecture uses Fly volumes for tenant file storage, which creates several limitations:
19+
20+
1. **Storage Scalability**: Fly volumes require pre-provisioning and don't auto-scale with usage
21+
2. **Cost Model**: Volume pricing vs object storage pricing may be less favorable at scale
22+
3. **Local Development**: No way for users to mount their cloud tenant files locally for real-time editing
23+
4. **Multi-Region**: Volumes are region-locked, limiting global deployment flexibility
24+
5. **Backup/Disaster Recovery**: Object storage provides better durability and replication options
25+
26+
The core insight is that Basic Memory requires POSIX filesystem semantics but could benefit from object storage durability and accessibility. By combining:
27+
- **Tigris object storage** for file persistence (via rclone mount)
28+
- **Turso/libSQL** for SQLite indexing (replacing local .db files)
29+
30+
We could enable a revolutionary user experience: **local editing of cloud-stored files** while maintaining Basic Memory's existing filesystem assumptions.
31+
32+
## What
33+
34+
This specification defines a proof-of-concept to validate the technical feasibility of the Tigris/Turso architecture for basic-memory-cloud tenants.
35+
36+
**Affected Areas:**
37+
- **Storage Architecture**: Replace Fly volumes with Tigris object storage
38+
- **Database Architecture**: Replace local SQLite with Turso remote database
39+
- **Container Setup**: Add rclone mounting in tenant containers
40+
- **Local Development**: Enable local mounting of cloud tenant data
41+
- **Basic Memory Core**: Validate unchanged operation over mounted filesystems
42+
43+
**Key Components:**
44+
- **Tigris Storage**: S3-compatible object storage via Fly.io integration
45+
- **rclone NFS Mount**: Native NFS mounting without FUSE dependencies
46+
- **Turso Database**: Hosted libSQL for SQLite replacement
47+
- **Single-Tenant Model**: One bucket + one database per tenant (simplified isolation)
48+
49+
## How (High Level)
50+
51+
### Phase 1: Local POC Validation
52+
- [ ] Set up Tigris bucket with test data
53+
- [ ] Configure rclone NFS mount locally
54+
- [ ] Test Basic Memory operations over mounted filesystem
55+
- [ ] Measure performance characteristics and identify issues
56+
- [ ] Validate file watching, sync operations, and concurrent access patterns
57+
58+
### Phase 2: Database Migration
59+
- [ ] Set up Turso account and test database
60+
- [ ] Modify Basic Memory to accept external DATABASE_URL
61+
- [ ] Test all operations with remote SQLite via Turso
62+
- [ ] Validate performance and functionality parity
63+
64+
### Phase 3: Container Integration
65+
- [ ] Create container image with rclone + NFS support
66+
- [ ] Implement tenant-specific credential management
67+
- [ ] Test container startup with automatic mounting
68+
- [ ] Validate isolation between tenant containers
69+
70+
### Phase 4: Local Access Validation
71+
- [ ] Test local rclone mounting of tenant data
72+
- [ ] Validate real-time file editing experience
73+
- [ ] Test conflict resolution and sync behavior
74+
- [ ] Measure latency impact on user experience
75+
76+
### Architecture Overview
77+
```
78+
Local Development:
79+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
80+
│ Local rclone │───▶│ Tigris Bucket │◀───│ Tenant Container│
81+
│ NFS Mount │ │ (S3 storage) │ │ rclone mount │
82+
└─────────────────┘ └─────────────────┘ └─────────────────┘
83+
│ │
84+
▼ ▼
85+
┌─────────────────┐ ┌─────────────────┐
86+
│ Basic Memory │ │ Basic Memory │
87+
│ (local files) │ │ (mounted files) │
88+
└─────────────────┘ └─────────────────┘
89+
│ │
90+
▼ ▼
91+
┌─────────────────┐ ┌─────────────────┐
92+
│ Turso Database │◀───────────────────────────│ Turso Database │
93+
│ (shared index) │ │ (shared index) │
94+
└─────────────────┘ └─────────────────┘
95+
```
96+
97+
## How to Evaluate
98+
99+
### Success Criteria
100+
- [ ] **Filesystem Compatibility**: Basic Memory operates without modification over rclone-mounted Tigris storage
101+
- [ ] **Performance Acceptable**: File operations complete within 2x local filesystem latency
102+
- [ ] **Database Functionality**: All Basic Memory features work with Turso remote SQLite
103+
- [ ] **Container Reliability**: Tenant containers start successfully with automatic mounting
104+
- [ ] **Local Access**: Users can mount and edit cloud files locally with real-time sync
105+
- [ ] **Data Isolation**: Tenant data remains properly isolated using bucket/database separation
106+
107+
### Testing Procedure
108+
1. **Local Filesystem Test**:
109+
```bash
110+
# Mount Tigris bucket locally
111+
rclone nfsmount tigris:test-bucket ~/tigris-test --vfs-cache-mode writes
112+
113+
# Run Basic Memory operations
114+
cd ~/tigris-test && basic-memory sync --watch
115+
# Test: create notes, search, file watching, bulk operations
116+
```
117+
118+
2. **Database Migration Test**:
119+
```bash
120+
# Configure Turso connection
121+
export DATABASE_URL="libsql://test-db.turso.io?authToken=..."
122+
123+
# Test all MCP tools with remote database
124+
basic-memory tools # Test each tool functionality
125+
```
126+
127+
3. **Container Integration Test**:
128+
```dockerfile
129+
# Test container with rclone mounting
130+
FROM python:3.12
131+
RUN apt-get update && apt-get install -y rclone nfs-common
132+
# ... test startup and mounting process
133+
```
134+
135+
4. **Performance Benchmarking**:
136+
- File creation/read/write operations (target: <2x local latency)
137+
- Search query performance (target: comparable to local SQLite)
138+
- File watching responsiveness (target: events within 1-2 seconds)
139+
- Concurrent operation handling
140+
141+
### Risk Assessment
142+
**High Risk Items**:
143+
- [ ] NFS-over-S3 performance may be insufficient for real-time operations
144+
- [ ] File watching (`inotify`) over NFS may be unreliable
145+
- [ ] Network interruptions could cause filesystem errors
146+
- [ ] Concurrent access patterns might hit S3 rate limits
147+
148+
**Mitigation Strategies**:
149+
- Comprehensive performance testing before committing to architecture
150+
- Fallback plan to S3-native storage backend if filesystem approach fails
151+
- Extensive error handling and retry logic for network issues
152+
153+
### Metrics to Track
154+
- **Latency**: File operation response times (read/write/watch)
155+
- **Reliability**: Success rate of file operations over time
156+
- **Throughput**: Concurrent file operations and search queries
157+
- **User Experience**: Perceived performance for local mounting use case
158+
159+
## Notes
160+
161+
### Key Architectural Decisions
162+
- **Single tenant per bucket/database**: Simplifies isolation and credential management
163+
- **Maintain POSIX compatibility**: Preserve Basic Memory's existing filesystem assumptions
164+
- **NFS over FUSE**: Better compatibility and performance characteristics
165+
- **Turso for SQLite**: Leverages specialized remote SQLite expertise
166+
167+
### Alternative Approaches Considered
168+
- **S3-native storage backend**: Would require Basic Memory architecture changes
169+
- **Hybrid approach**: Local files + cloud sync (adds complexity)
170+
- **FUSE mounting**: More platform dependencies and kernel requirements
171+
172+
### Integration Points
173+
- [ ] Fly.io Tigris integration for bucket provisioning
174+
- [ ] Turso account setup and database provisioning
175+
- [ ] Container image modifications for rclone support
176+
- [ ] Credential management for tenant isolation
177+
178+
## Observations
179+
180+
- [architecture] Tigris/Turso split cleanly separates file storage from indexing concerns #storage-separation
181+
- [user-experience] Local mounting of cloud files could be revolutionary for knowledge management #local-cloud-hybrid
182+
- [compatibility] Maintaining POSIX filesystem assumptions preserves Basic Memory's local/cloud compatibility #architecture-preservation
183+
- [simplification] Single tenant per bucket eliminates complex multi-tenancy in storage layer #tenant-isolation
184+
- [risk] NFS-over-S3 performance characteristics are unproven for real-time operations #performance-risk
185+
- [benefit] Object storage pricing model could be more favorable than volume pricing #cost-optimization
186+
- [innovation] Real-time local editing of cloud-stored files addresses major SaaS limitation #competitive-advantage
187+
188+
## Relations
189+
190+
- implements [[SPEC-6 Explicit Project Parameter Architecture]]
191+
- requires [[Fly.io Tigris Integration]]
192+
- enables [[Local Cloud File Access]]
193+
- alternative_to [[Fly Volume Storage]]

src/basic_memory/cli/commands/cloud.py

Lines changed: 55 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -11,78 +11,25 @@
1111

1212
from basic_memory.cli.app import cloud_app
1313
from basic_memory.cli.auth import CLIAuth
14-
from basic_memory.config import ConfigManager
14+
from basic_memory.cli.commands.cloud.api_client import (
15+
CloudAPIError,
16+
get_cloud_config,
17+
make_api_request,
18+
get_authenticated_headers,
19+
)
20+
from basic_memory.cli.commands.cloud.mount_commands import (
21+
mount_cloud_files,
22+
setup_cloud_mount,
23+
show_mount_status,
24+
unmount_cloud_files,
25+
)
26+
from basic_memory.cli.commands.cloud.rclone_config import MOUNT_PROFILES
1527
from basic_memory.ignore_utils import load_gitignore_patterns, should_ignore_path
1628
from basic_memory.utils import generate_permalink
1729

1830
console = Console()
1931

2032

21-
class CloudAPIError(Exception):
22-
"""Exception raised for cloud API errors."""
23-
24-
pass
25-
26-
27-
def get_cloud_config() -> tuple[str, str, str]:
28-
"""Get cloud OAuth configuration from config."""
29-
config_manager = ConfigManager()
30-
config = config_manager.config
31-
return config.cloud_client_id, config.cloud_domain, config.cloud_host
32-
33-
34-
async def make_api_request(
35-
method: str,
36-
url: str,
37-
headers: Optional[dict] = None,
38-
json_data: Optional[dict] = None,
39-
timeout: float = 30.0,
40-
) -> httpx.Response:
41-
"""Make an API request to the cloud service."""
42-
headers = headers or {}
43-
auth_headers = await get_authenticated_headers()
44-
headers.update(auth_headers)
45-
# Add debug headers to help with compression issues
46-
headers.setdefault("Accept-Encoding", "identity") # Disable compression for debugging
47-
48-
async with httpx.AsyncClient(timeout=timeout) as client:
49-
try:
50-
console.print(f"[dim]Making {method} request to {url}[/dim]")
51-
console.print(f"[dim]Headers: {dict(headers)}[/dim]")
52-
53-
response = await client.request(method=method, url=url, headers=headers, json=json_data)
54-
55-
console.print(f"[dim]Response status: {response.status_code}[/dim]")
56-
console.print(f"[dim]Response headers: {dict(response.headers)}[/dim]")
57-
58-
response.raise_for_status()
59-
return response
60-
except httpx.HTTPError as e:
61-
console.print(f"[red]HTTP Error details: {e}[/red]")
62-
# Check if this is a response error with response details
63-
if hasattr(e, "response") and e.response is not None: # pyright: ignore [reportAttributeAccessIssue]
64-
response = e.response # type: ignore
65-
console.print(f"[red]Response status: {response.status_code}[/red]")
66-
console.print(f"[red]Response headers: {dict(response.headers)}[/red]")
67-
try:
68-
console.print(f"[red]Response text: {response.text}[/red]")
69-
except Exception:
70-
console.print("[red]Could not read response text[/red]")
71-
raise CloudAPIError(f"API request failed: {e}") from e
72-
73-
74-
async def get_authenticated_headers() -> dict[str, str]:
75-
"""Get authentication headers with JWT token."""
76-
client_id, domain, _ = get_cloud_config()
77-
auth = CLIAuth(client_id=client_id, authkit_domain=domain)
78-
token = await auth.get_valid_token()
79-
if not token:
80-
console.print("[red]Not authenticated. Please run 'tenant login' first.[/red]")
81-
raise typer.Exit(1)
82-
83-
return {"Authorization": f"Bearer {token}"}
84-
85-
8633
@cloud_app.command()
8734
def login():
8835
"""Authenticate with WorkOS using OAuth Device Authorization flow."""
@@ -407,3 +354,45 @@ def status() -> None:
407354
except Exception as e:
408355
console.print(f"[red]Unexpected error: {e}[/red]")
409356
raise typer.Exit(1)
357+
358+
359+
# Mount commands
360+
361+
362+
@cloud_app.command("setup")
363+
def setup() -> None:
364+
"""Set up local file access with automatic rclone installation and configuration."""
365+
setup_cloud_mount()
366+
367+
368+
@cloud_app.command("mount")
369+
def mount(
370+
profile: str = typer.Option(
371+
"balanced", help=f"Mount profile: {', '.join(MOUNT_PROFILES.keys())}"
372+
),
373+
path: Optional[str] = typer.Option(
374+
None, help="Custom mount path (default: ~/basic-memory-{tenant-id})"
375+
),
376+
) -> None:
377+
"""Mount cloud files locally for editing."""
378+
try:
379+
mount_cloud_files(profile_name=profile)
380+
except Exception as e:
381+
console.print(f"[red]Mount failed: {e}[/red]")
382+
raise typer.Exit(1)
383+
384+
385+
@cloud_app.command("unmount")
386+
def unmount() -> None:
387+
"""Unmount cloud files."""
388+
try:
389+
unmount_cloud_files()
390+
except Exception as e:
391+
console.print(f"[red]Unmount failed: {e}[/red]")
392+
raise typer.Exit(1)
393+
394+
395+
@cloud_app.command("mount-status")
396+
def mount_status() -> None:
397+
"""Show current mount status."""
398+
show_mount_status()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Cloud commands package."""

0 commit comments

Comments
 (0)