fix(FN-1952): remove stale task artifacts
This commit is contained in:
@@ -1,903 +0,0 @@
|
||||
# Authentication Research Report: Nodes and Dashboard
|
||||
|
||||
**Task:** FN-1783
|
||||
**Date:** 2026-04-14
|
||||
**Type:** Research Only — No Implementation
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Fusion currently operates with **minimal authentication boundaries**. Nodes in the mesh network store API keys but only validate them on a single endpoint (`POST /api/mesh/sync`), while the dashboard and all other API routes are completely open. This creates a security gap, especially for `fn serve` which binds to `0.0.0.0` by default.
|
||||
|
||||
This report documents the **current authentication posture**, analyzes **three options for completing node-to-node authentication**, and **three options for adding dashboard/API authentication**. The report provides concrete recommendations and implementation considerations.
|
||||
|
||||
**Key Findings:**
|
||||
- Node auth uses Bearer tokens with `Authorization` headers but validation is limited to one endpoint
|
||||
- Dashboard has **no authentication whatsoever** — all API routes are open
|
||||
- `fn serve` binds to all network interfaces, making it accessible to anyone on the network
|
||||
- Existing infrastructure (rate-limiting, error helpers) can be leveraged for auth implementation
|
||||
- A layered approach (separate node auth from dashboard auth) is recommended
|
||||
|
||||
---
|
||||
|
||||
## 1. Current State Analysis
|
||||
|
||||
### 1.1 Node Authentication
|
||||
|
||||
#### Where API Keys Are Stored
|
||||
- **`NodeConfig.apiKey`** is stored in the `nodes` table (SQLite in `~/.pi/fusion/fusion-central.db`)
|
||||
- Defined in `packages/core/src/types.ts`:
|
||||
```typescript
|
||||
interface NodeConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "local" | "remote";
|
||||
url?: string;
|
||||
apiKey?: string; // Stored plaintext
|
||||
status: "offline" | "online" | "error";
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### How API Keys Are Sent
|
||||
- **`NodeConnection.test()`** (`packages/core/src/node-connection.ts`):
|
||||
```typescript
|
||||
const response = await fetch(healthUrl, {
|
||||
method: "GET",
|
||||
headers: options.apiKey
|
||||
? { Authorization: `Bearer ${options.apiKey}` }
|
||||
: undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
```
|
||||
|
||||
- **`CentralCore.checkNodeHealth()`** (`packages/core/src/central-core.ts`):
|
||||
```typescript
|
||||
const response = await fetch(healthUrl, {
|
||||
method: "GET",
|
||||
headers: node.apiKey ? { Authorization: `Bearer ${node.apiKey}` } : undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
```
|
||||
|
||||
#### Where API Keys Are Validated
|
||||
- **Only `POST /api/mesh/sync`** validates the Bearer token:
|
||||
```typescript
|
||||
// In central-core.ts mesh sync handler
|
||||
const senderNode = await this.getNode(senderNodeId);
|
||||
if (!senderNode || senderNode.apiKey !== bearerToken) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
```
|
||||
|
||||
#### Critical Gaps
|
||||
|
||||
| Endpoint | Validates Token? | Notes |
|
||||
|-----------|-------------------|-------|
|
||||
| `GET /api/health` | **NO** | Returns status without auth check |
|
||||
| `GET /api/mesh/state` | **NO** | Returns mesh topology |
|
||||
| `GET /api/nodes` | **NO** | Lists all registered nodes |
|
||||
| `GET /api/nodes/:id` | **NO** | Returns node details including API keys |
|
||||
| `POST /api/mesh/sync` | **YES** | Only endpoint that validates |
|
||||
| `GET /api/nodes/:id/metrics` | **NO** | Returns system metrics |
|
||||
| `GET /api/nodes/:id/version` | **NO** | Returns version info |
|
||||
| `POST /api/nodes/:id/sync-plugins` | **NO** | Plugin sync endpoint |
|
||||
|
||||
**Security Impact:** Anyone who knows a remote node's URL can:
|
||||
- Query its health and system metrics
|
||||
- List all registered nodes and their API keys
|
||||
- Trigger plugin sync operations
|
||||
- Access the mesh topology
|
||||
|
||||
### 1.2 Dashboard Authentication
|
||||
|
||||
#### Current State
|
||||
- **No authentication** on any dashboard or API route
|
||||
- `fn dashboard` binds to `localhost` (somewhat protected by network isolation)
|
||||
- `fn serve` binds to `0.0.0.0` (**network-accessible!**)
|
||||
- `AuthStorage` is **only for AI provider credentials** (OAuth/API keys for OpenAI, Anthropic, etc.)
|
||||
- No session management, cookies, or API key validation
|
||||
|
||||
#### Relevant Code
|
||||
|
||||
**Dashboard startup** (`packages/cli/src/commands/dashboard.ts`):
|
||||
```typescript
|
||||
const server = app.listen(selectedPort); // Binds to localhost by default
|
||||
```
|
||||
|
||||
**Serve startup** (`packages/cli/src/commands/serve.ts`):
|
||||
```typescript
|
||||
const selectedHost = opts.host ?? "0.0.0.0"; // Network-accessible!
|
||||
const server = app.listen(selectedPort, selectedHost);
|
||||
```
|
||||
|
||||
**Health endpoint** (`packages/dashboard/src/server.ts`):
|
||||
```typescript
|
||||
app.get("/api/health", (_req, res) => {
|
||||
res.json({
|
||||
status: "ok",
|
||||
version: process.env.npm_package_version ?? "0.4.0",
|
||||
uptime: Math.floor(process.uptime()),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Rate limiting exists but no auth** (`packages/dashboard/src/rate-limit.ts`):
|
||||
```typescript
|
||||
export const RATE_LIMITS = {
|
||||
api: { windowMs: 60_000, max: 100 },
|
||||
mutation: { windowMs: 60_000, max: 30 },
|
||||
sse: { windowMs: 60_000, max: 10 },
|
||||
};
|
||||
```
|
||||
|
||||
**Error helpers available** (`packages/dashboard/src/api-error.ts`):
|
||||
```typescript
|
||||
export function unauthorized(message: string): ApiError {
|
||||
return new ApiError(401, message);
|
||||
}
|
||||
```
|
||||
|
||||
#### Critical Gaps
|
||||
|
||||
| Area | Gap |
|
||||
|------|-----|
|
||||
| Browser access | No login required to access dashboard |
|
||||
| API access | No API key required for programmatic access |
|
||||
| `fn serve` | Bound to `0.0.0.0` — anyone on network can access |
|
||||
| Credentials storage | No user credential storage |
|
||||
| Session management | No sessions or cookies |
|
||||
| Multi-user | Single-user only — no user isolation |
|
||||
|
||||
---
|
||||
|
||||
## 2. Node Authentication Options
|
||||
|
||||
### Option A: Shared Secret / Static API Key
|
||||
|
||||
**Concept:** Reuse the existing `apiKey` field but validate it on *all* node-facing endpoints.
|
||||
|
||||
#### Implementation Approach
|
||||
1. Add Bearer token validation middleware
|
||||
2. Apply to all `/api/nodes/`, `/api/mesh/`, `/api/health` endpoints
|
||||
3. Support both `Authorization: Bearer <token>` header and `?api_key=<token>` query param for browser compatibility
|
||||
|
||||
#### Security Properties
|
||||
- **Strength:** Basic protection against casual access
|
||||
- **Weakness:** Static keys can be leaked, no rotation mechanism
|
||||
- **Replay:** Vulnerable to replay attacks if tokens are captured
|
||||
|
||||
#### Operational Complexity
|
||||
- **Key Generation:** Simple UUID or random string
|
||||
- **Key Storage:** Already exists in `nodes` table
|
||||
- **Key Rotation:** Manual process — requires updating all nodes
|
||||
- **Revocation:** Not supported — keys are eternal
|
||||
- **Onboarding:** Share key out-of-band (secure channel required)
|
||||
|
||||
#### Compatibility with Current Architecture
|
||||
- **Minimal changes** — leverages existing `apiKey` field
|
||||
- `NodeConnection` already sends Bearer tokens
|
||||
- `CentralCore` already validates on one endpoint
|
||||
|
||||
#### Endpoints to Protect
|
||||
```typescript
|
||||
// All of these need Bearer token validation:
|
||||
GET /api/health
|
||||
GET /api/mesh/state
|
||||
GET /api/mesh/peer-exchange
|
||||
POST /api/mesh/sync
|
||||
GET /api/nodes
|
||||
POST /api/nodes
|
||||
GET /api/nodes/:id
|
||||
PATCH /api/nodes/:id
|
||||
DELETE /api/nodes/:id
|
||||
GET /api/nodes/:id/metrics
|
||||
GET /api/nodes/:id/version
|
||||
POST /api/nodes/:id/sync-plugins
|
||||
GET /api/nodes/:id/compatibility
|
||||
```
|
||||
|
||||
#### Impact on Existing Code
|
||||
```typescript
|
||||
// packages/dashboard/src/server.ts
|
||||
app.get("/api/health", (req, res) => {
|
||||
const token = req.headers.authorization?.replace("Bearer ", "") ||
|
||||
req.query.api_key;
|
||||
|
||||
// Validate against configured admin key or node registry
|
||||
if (!validateToken(token)) {
|
||||
return unauthorized(res, "Invalid API key");
|
||||
}
|
||||
// ... rest of handler
|
||||
});
|
||||
```
|
||||
|
||||
#### Works For
|
||||
- `fn serve` — Yes
|
||||
- `fn dashboard` — Yes (separate auth layer recommended)
|
||||
|
||||
---
|
||||
|
||||
### Option B: Mutual TLS (mTLS)
|
||||
|
||||
**Concept:** Nodes present client certificates validated by the server. Uses TLS client authentication.
|
||||
|
||||
#### Implementation Approach
|
||||
1. Generate CA for signing node certificates
|
||||
2. Each node gets a signed client certificate
|
||||
3. Server validates client certificate on TLS handshake
|
||||
4. Certificate CN/SAN identifies the node
|
||||
|
||||
#### Security Properties
|
||||
- **Strength:** Strong — certificates are cryptographically verified
|
||||
- **Weakness:** Complex PKI infrastructure
|
||||
- **Replay:** Certificates can be revoked but revocation checking adds latency
|
||||
|
||||
#### Operational Complexity
|
||||
- **Key Generation:** Certificate authority + per-node certificates
|
||||
- **Key Storage:** Certificate files on each node
|
||||
- **Key Rotation:** Certificate renewal with CA re-signing
|
||||
- **Revocation:** Certificate revocation lists (CRLs) or OCSP
|
||||
- **Onboarding:** Certificate issuance workflow required
|
||||
|
||||
#### Compatibility with Current Architecture
|
||||
- **Major changes** — requires TLS infrastructure
|
||||
- Node connection code needs certificate loading
|
||||
- Central registry needs certificate pinning
|
||||
|
||||
#### Additional Considerations
|
||||
```typescript
|
||||
// Server TLS configuration
|
||||
const server = https.createServer({
|
||||
cert: serverCert,
|
||||
key: serverKey,
|
||||
requestCert: true, // Request client certificate
|
||||
rejectUnauthorized: true, // Reject invalid client certs
|
||||
}, app);
|
||||
|
||||
// Node connection
|
||||
const response = await fetch(healthUrl, {
|
||||
cert: nodeCertificate,
|
||||
key: nodePrivateKey,
|
||||
ca: caCertificate, // Trust our CA
|
||||
});
|
||||
```
|
||||
|
||||
#### Works For
|
||||
- `fn serve` — Yes
|
||||
- `fn dashboard` — Yes (separate auth layer recommended)
|
||||
- **Not suitable for:** Quick setup, development environments
|
||||
|
||||
---
|
||||
|
||||
### Option C: JWT or Signed Requests
|
||||
|
||||
**Concept:** Short-lived tokens signed with a shared secret or asymmetric key. Includes timestamp and optional claims.
|
||||
|
||||
#### Implementation Approach
|
||||
1. Generate a signing secret (HMAC-SHA256) or keypair (RSA/Ed25519)
|
||||
2. Issue tokens with expiration (e.g., 1 hour)
|
||||
3. Include node ID in token claims
|
||||
4. Validate signature + expiration on each request
|
||||
5. Token refresh mechanism for long-running operations
|
||||
|
||||
#### Security Properties
|
||||
- **Strength:** Strong — cryptographic signatures + time-based expiry
|
||||
- **Replay:** Limited by token lifetime; use nonces for additional protection
|
||||
- **Rotation:** Shared secret rotation with grace period
|
||||
|
||||
#### Operational Complexity
|
||||
- **Key Generation:** Simple secret or keypair
|
||||
- **Key Storage:** Shared secret per-node or public key registry
|
||||
- **Key Rotation:** Secret rotation with token invalidation
|
||||
- **Revocation:** Token expiration handles revocation; for immediate revocation, maintain denylist
|
||||
- **Onboarding:** Token issuance workflow
|
||||
|
||||
#### Compatibility with Current Architecture
|
||||
- **Moderate changes** — add token generation/validation
|
||||
- Can reuse existing `apiKey` as shared secret
|
||||
- `NodeConnection` needs token generation
|
||||
|
||||
#### Token Structure
|
||||
```typescript
|
||||
interface NodeToken {
|
||||
nodeId: string;
|
||||
issuedAt: number; // Unix timestamp
|
||||
expiresAt: number; // Unix timestamp
|
||||
nonce?: string; // For replay protection
|
||||
}
|
||||
|
||||
// JWT payload example:
|
||||
// {
|
||||
// "nodeId": "node_abc123",
|
||||
// "iat": 1713000000,
|
||||
// "exp": 1713003600,
|
||||
// "jti": "unique-token-id"
|
||||
// }
|
||||
|
||||
// Signed with HMAC-SHA256 using shared secret
|
||||
```
|
||||
|
||||
#### Works For
|
||||
- `fn serve` — Yes
|
||||
- `fn dashboard` — Yes (separate auth layer recommended)
|
||||
|
||||
---
|
||||
|
||||
### Node Auth Options Comparison
|
||||
|
||||
| Aspect | Option A: Static API Key | Option B: mTLS | Option C: JWT |
|
||||
|--------|-------------------------|----------------|---------------|
|
||||
| **Implementation Effort** | Low | High | Medium |
|
||||
| **Security Level** | Basic | Strong | Strong |
|
||||
| **Key Rotation** | Manual | CA-based | Graceful |
|
||||
| **Replay Protection** | None | Certificate revocation | Token lifetime |
|
||||
| **Infrastructure** | None | PKI required | Signing only |
|
||||
| **Node Onboarding** | Share key | Certificate issuance | Token issuance |
|
||||
| **Audit Trail** | Basic | Strong (certificates) | Strong (tokens) |
|
||||
| **Compatibility** | High | Low | Medium |
|
||||
|
||||
---
|
||||
|
||||
## 3. Dashboard Authentication Options
|
||||
|
||||
### Option A: Static API Key
|
||||
|
||||
**Concept:** A single `FUSION_API_KEY` environment variable or config setting. All requests must include it in an `Authorization: Bearer` header.
|
||||
|
||||
#### Implementation Approach
|
||||
1. Add `FUSION_API_KEY` to environment/config
|
||||
2. Create auth middleware checking `Authorization` header
|
||||
3. Apply to all `/api/*` routes except `/api/health`
|
||||
4. Provide `?api_key=` query param fallback for browser convenience
|
||||
|
||||
#### Security Properties
|
||||
- **Strength:** Basic protection against unauthorized access
|
||||
- **Weakness:** Single key — if leaked, full access
|
||||
- **No multi-user support**
|
||||
|
||||
#### User Experience
|
||||
- **Setup:** Set one environment variable
|
||||
- **Login:** Include header in all requests
|
||||
- **CLI:** `fn` commands need to send the key
|
||||
|
||||
#### Frontend Impact
|
||||
```typescript
|
||||
// Dashboard API wrapper
|
||||
const api = {
|
||||
async fetch(url, options = {}) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
'Authorization': `Bearer ${getApiKey()}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
#### CLI Impact
|
||||
```bash
|
||||
# CLI commands need to send credentials
|
||||
fn task list --api-key $FUSION_API_KEY
|
||||
|
||||
# Or configured in settings
|
||||
fn config set api-key <key>
|
||||
```
|
||||
|
||||
#### Multi-Project Considerations
|
||||
- Single global key for all projects
|
||||
- Could add per-project keys in the future
|
||||
|
||||
#### Works For
|
||||
- `fn serve` — Yes (primary use case)
|
||||
- `fn dashboard` — Yes
|
||||
- **Recommended for:** Headless deployments, single-user setups
|
||||
|
||||
---
|
||||
|
||||
### Option B: Session-Based Login
|
||||
|
||||
**Concept:** Username/password stored in SQLite, session cookies for the React frontend, and CSRF protection.
|
||||
|
||||
#### Implementation Approach
|
||||
1. Add `users` table to project database
|
||||
2. Password hashing with bcrypt/argon2
|
||||
3. Session cookie with secure/httpOnly flags
|
||||
4. CSRF token in requests
|
||||
5. Login/logout endpoints
|
||||
6. Login page UI
|
||||
|
||||
#### Security Properties
|
||||
- **Strength:** Strong — proven session pattern
|
||||
- **Multi-user:** Yes
|
||||
- **Password storage:** Hashed, salted
|
||||
|
||||
#### User Experience
|
||||
- **Setup:** Create admin user on first run
|
||||
- **Login:** Username/password form
|
||||
- **Session:** Cookie-based, auto-renew
|
||||
|
||||
#### Frontend Impact
|
||||
```typescript
|
||||
// Login page component
|
||||
interface LoginPage {
|
||||
username: string;
|
||||
password: string;
|
||||
onLogin: (creds) => Promise<void>;
|
||||
}
|
||||
|
||||
// Protected route wrapper
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { user, loading } = useAuth();
|
||||
if (loading) return <Spinner />;
|
||||
if (!user) return <Navigate to="/login" />;
|
||||
return children;
|
||||
};
|
||||
```
|
||||
|
||||
#### Database Schema
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
passwordHash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user', -- 'admin', 'user'
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
expiresAt TEXT NOT NULL,
|
||||
createdAt TEXT NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES users(id)
|
||||
);
|
||||
```
|
||||
|
||||
#### Multi-Project Considerations
|
||||
- Users are per-project
|
||||
- Could add global users in the future
|
||||
- Dashboard settings need admin-only access
|
||||
|
||||
#### Works For
|
||||
- `fn dashboard` — Yes (primary use case)
|
||||
- `fn serve` — Yes
|
||||
- **Recommended for:** Team environments, multiple users
|
||||
|
||||
---
|
||||
|
||||
### Option C: OAuth / SSO Integration
|
||||
|
||||
**Concept:** Delegate authentication to external identity providers (GitHub, Google, generic OIDC).
|
||||
|
||||
#### Implementation Approach
|
||||
1. OAuth 2.0 / OIDC flow
|
||||
2. Support GitHub OAuth (common for developer tools)
|
||||
3. Support generic OIDC for enterprise
|
||||
4. Callback URL configuration
|
||||
5. User provisioning on first login
|
||||
|
||||
#### Security Properties
|
||||
- **Strength:** Strong — delegated to identity providers
|
||||
- **Multi-user:** Yes
|
||||
- **SSO:** Enterprise ready
|
||||
|
||||
#### User Experience
|
||||
- **Setup:** Register OAuth application, configure client ID/secret
|
||||
- **Login:** "Sign in with GitHub" button
|
||||
- **Session:** Managed by dashboard
|
||||
|
||||
#### Callback URL Challenges
|
||||
```typescript
|
||||
// For fn serve on remote host:
|
||||
// - User configures callback URL during OAuth app registration
|
||||
// - Must be publicly accessible
|
||||
// - Example: https://fusion.example.com/api/auth/callback/github
|
||||
|
||||
// For localhost development:
|
||||
// - Use ngrok or similar for callback
|
||||
// - Configure FUSION_PUBLIC_URL for correct callback
|
||||
```
|
||||
|
||||
#### Frontend Impact
|
||||
```typescript
|
||||
// OAuth login button
|
||||
<Button onClick={() => window.location.href = '/api/auth/github'}>
|
||||
<GitHubIcon /> Sign in with GitHub
|
||||
</Button>
|
||||
|
||||
// Protected routes
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { user } = useAuth();
|
||||
if (!user) return <Navigate to="/login" />;
|
||||
return children;
|
||||
};
|
||||
```
|
||||
|
||||
#### Multi-Project Considerations
|
||||
- Users are typically global (single sign-on)
|
||||
- Project permissions can be managed separately
|
||||
- Works well for enterprise deployments
|
||||
|
||||
#### Works For
|
||||
- `fn dashboard` — Yes (primary use case)
|
||||
- `fn serve` — Complex (requires public callback URL)
|
||||
|
||||
---
|
||||
|
||||
### Dashboard Auth Options Comparison
|
||||
|
||||
| Aspect | Option A: Static API Key | Option B: Session Login | Option C: OAuth/SSO |
|
||||
|--------|-------------------------|----------------------|---------------------|
|
||||
| **Implementation Effort** | Low | Medium | High |
|
||||
| **Security Level** | Basic | Strong | Strong |
|
||||
| **Multi-User** | No | Yes | Yes |
|
||||
| **User Management** | None | Built-in | External (IdP) |
|
||||
| **Setup Complexity** | Low | Medium | High |
|
||||
| **Callback URL Needed** | No | No | Yes |
|
||||
| **Enterprise Ready** | No | Partial | Yes |
|
||||
| **CLI Support** | Native | Cookie/Token | Token |
|
||||
|
||||
---
|
||||
|
||||
## 4. Recommendations
|
||||
|
||||
### 4.1 Recommended Node Auth: Option A (Shared Secret) + Option C (JWT) Hybrid
|
||||
|
||||
**Recommendation:** Start with Option A (Static API Key) for simplicity, then evolve to Option C (JWT) for better security.
|
||||
|
||||
#### Phase 1: Static API Key Validation
|
||||
- **Effort:** Low — add middleware to existing routes
|
||||
- **Impact:** Immediately secures all node endpoints
|
||||
- **Migration:** Update `NodeConnection` to send keys, existing keys work
|
||||
|
||||
#### Phase 2: JWT Migration
|
||||
- **Effort:** Medium — add token generation/validation
|
||||
- **Benefits:** Expiring tokens, replay protection, auditability
|
||||
- **Compatibility:** Can issue JWTs using existing `apiKey` as secret
|
||||
|
||||
#### Key Design Decisions
|
||||
1. **Protect all node endpoints** — not just `/api/mesh/sync`
|
||||
2. **Keep `/api/health` public** — health checks for load balancers
|
||||
3. **Support both header and query param** — flexibility for different clients
|
||||
4. **Document key rotation** — operational runbook
|
||||
|
||||
### 4.2 Recommended Dashboard Auth: Option A (Static API Key) for Headless, Option B (Sessions) for Dashboard
|
||||
|
||||
**Recommendation:** Use different auth strategies for different use cases.
|
||||
|
||||
#### For `fn serve` (Headless Deployments)
|
||||
- **Primary:** Static API key (`FUSION_API_KEY`)
|
||||
- **Rationale:** Simple, matches node auth, works well for scripts/CI
|
||||
- **CLI support:** `fn --api-key <key> task list`
|
||||
|
||||
#### For `fn dashboard` (Browser UI)
|
||||
- **Primary:** Session-based login (Option B)
|
||||
- **Fallback:** Static API key for API access
|
||||
- **Rationale:** Better UX for human users, natural multi-user support
|
||||
|
||||
#### Unified Approach
|
||||
Consider a unified auth system:
|
||||
1. Static key for API access (CLI, scripts)
|
||||
2. Session login for browser access
|
||||
3. API key can be converted to session
|
||||
|
||||
### 4.3 Should Node Auth and Dashboard Auth Be the Same?
|
||||
|
||||
**Answer: No — they serve different purposes.**
|
||||
|
||||
| Dimension | Node Auth | Dashboard Auth |
|
||||
|-----------|-----------|---------------|
|
||||
| **Client type** | Machine (other nodes) | Human (browser) |
|
||||
| **Credential type** | API key/token | Username/password or OAuth |
|
||||
| **Session length** | Minutes to hours | Hours to days |
|
||||
| **Transport** | Headers only | Cookies + headers |
|
||||
| **Trust model** | Shared secrets | Interactive login |
|
||||
|
||||
**Exception:** A single static API key could work for both, but with different validation paths:
|
||||
- Node: `Authorization: Bearer <key>` validated server-side
|
||||
- Dashboard: Configured in settings, sent with API requests
|
||||
|
||||
---
|
||||
|
||||
## 5. Prerequisites for Implementation
|
||||
|
||||
### 5.1 Database Changes
|
||||
|
||||
```sql
|
||||
-- New table for dashboard users (if session auth)
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
passwordHash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- New table for sessions
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
expiresAt TEXT NOT NULL,
|
||||
createdAt TEXT NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- Add token fields to nodes table (for JWT support)
|
||||
ALTER TABLE nodes ADD COLUMN tokenSecret TEXT;
|
||||
ALTER TABLE nodes ADD COLUMN tokenIssuedAt TEXT;
|
||||
```
|
||||
|
||||
### 5.2 Settings Changes
|
||||
|
||||
```typescript
|
||||
// packages/core/src/types.ts
|
||||
interface ProjectSettings {
|
||||
// ... existing fields ...
|
||||
|
||||
// New auth settings
|
||||
dashboardApiKey?: string; // For API access
|
||||
requireDashboardAuth?: boolean; // Enable/disable auth
|
||||
sessionDurationMs?: number; // Session timeout
|
||||
allowedOrigins?: string[]; // CORS origins
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 CLI Changes
|
||||
|
||||
```typescript
|
||||
// New CLI flags
|
||||
interface GlobalOptions {
|
||||
// ... existing ...
|
||||
apiKey?: string; // --api-key flag
|
||||
}
|
||||
|
||||
// Config location for API key
|
||||
~/.pi/fusion/settings.json: {
|
||||
"apiKey": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Environment Variables
|
||||
|
||||
```bash
|
||||
# For fn serve
|
||||
FUSION_API_KEY= # Required API key
|
||||
FUSION_REQUIRE_AUTH=true # Enable auth enforcement
|
||||
FUSION_PUBLIC_URL=https://... # For OAuth callbacks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Security Pitfalls to Avoid
|
||||
|
||||
### 6.1 SSE/WebSocket Security
|
||||
|
||||
**Issue:** SSE endpoints and WebSockets need special handling.
|
||||
|
||||
```typescript
|
||||
// SSE heartbeat - maintain auth context
|
||||
app.get("/api/events", (req, res) => {
|
||||
// Validate token, then maintain context
|
||||
const token = req.headers.authorization?.replace("Bearer ", "");
|
||||
// Store validated token in res.locals for use in connection
|
||||
});
|
||||
|
||||
// WebSocket - validate on upgrade
|
||||
server.on("upgrade", (req, socket, head) => {
|
||||
const token = parseTokenFromUrl(req.url);
|
||||
if (!validateToken(token)) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
// ... handle upgrade
|
||||
});
|
||||
```
|
||||
|
||||
### 6.2 File Service Routes
|
||||
|
||||
**Issue:** File upload/download routes bypass normal API.
|
||||
|
||||
```typescript
|
||||
// Protect all file routes
|
||||
app.post("/api/files/upload", authMiddleware, uploadHandler);
|
||||
app.get("/api/files/*", authMiddleware, downloadHandler);
|
||||
```
|
||||
|
||||
### 6.3 Plugin Routes
|
||||
|
||||
**Issue:** Plugin routes need consistent auth.
|
||||
|
||||
```typescript
|
||||
// All plugin routes mounted under /api/plugins/:id/*
|
||||
app.use("/api/plugins", authMiddleware, pluginRouter);
|
||||
```
|
||||
|
||||
### 6.4 Rate Limiting + Auth
|
||||
|
||||
**Issue:** Don't count auth failures against rate limits.
|
||||
|
||||
```typescript
|
||||
// Auth failures should be rate-limited separately
|
||||
const authLimiter = rateLimit({ max: 5, windowMs: 60_000 });
|
||||
const apiLimiter = rateLimit({ max: 100, windowMs: 60_000 });
|
||||
|
||||
app.post("/api/auth/login", authLimiter, loginHandler);
|
||||
app.get("/api/*", apiLimiter, authMiddleware, apiHandler);
|
||||
```
|
||||
|
||||
### 6.5 Health Endpoint
|
||||
|
||||
**Issue:** Load balancers need health checks without auth.
|
||||
|
||||
```typescript
|
||||
// Keep health endpoint public
|
||||
app.get("/api/health", healthHandler);
|
||||
|
||||
// Separate liveness vs readiness
|
||||
app.get("/api/health/live", liveHandler); // Can auth fail
|
||||
app.get("/api/health/ready", readyHandler); // Requires auth
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Open Questions / Risks
|
||||
|
||||
### 7.1 Node Auth Questions
|
||||
|
||||
1. **Should local node auth be required?** Local dashboard calls from browser may need different treatment than remote node-to-node calls.
|
||||
|
||||
2. **Token distribution:** How do remote nodes get their initial API key? Manual configuration? Bootstrap flow?
|
||||
|
||||
3. **Key rotation during operation:** If we rotate the shared secret, how do we update all nodes without downtime?
|
||||
|
||||
4. **Cross-project node access:** If a node serves multiple projects, should auth be per-project or global?
|
||||
|
||||
### 7.2 Dashboard Auth Questions
|
||||
|
||||
1. **First-run experience:** How does the first user log in if there's no auth configured yet?
|
||||
|
||||
2. **CLI + browser hybrid:** If I use the dashboard in browser AND the CLI, should they share sessions?
|
||||
|
||||
3. **API access for tools:** Should programmatic API access (from other tools/scripts) use the same auth as browser sessions?
|
||||
|
||||
4. **Guest/read-only access:** Do we need role-based access control (RBAC) for different permission levels?
|
||||
|
||||
### 7.3 Risks
|
||||
|
||||
1. **Breaking existing setups:** Adding auth to a running system requires migration strategy
|
||||
2. **Key management:** Storing keys securely, key rotation, key loss recovery
|
||||
3. **Multi-project complexity:** Different auth domains per project vs. global auth
|
||||
4. **Performance:** Auth validation adds latency to every request
|
||||
|
||||
---
|
||||
|
||||
## 8. Appendix: Affected Files and Endpoints
|
||||
|
||||
### 8.1 Files to Modify
|
||||
|
||||
#### Core Infrastructure
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `packages/core/src/types.ts` | Add auth-related types |
|
||||
| `packages/core/src/central-core.ts` | Add token validation to node endpoints |
|
||||
| `packages/dashboard/src/server.ts` | Add auth middleware, protect routes |
|
||||
| `packages/dashboard/src/routes.ts` | Add login/logout endpoints |
|
||||
| `packages/dashboard/src/api-error.ts` | Already has `unauthorized()` |
|
||||
|
||||
#### CLI Commands
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `packages/cli/src/commands/serve.ts` | Read `FUSION_API_KEY`, enforce auth |
|
||||
| `packages/cli/src/commands/dashboard.ts` | Add auth flag support |
|
||||
|
||||
#### Middleware
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `packages/dashboard/src/rate-limit.ts` | Already exists, may need extension |
|
||||
| `packages/dashboard/src/auth-middleware.ts` | **New file** — Bearer token validation |
|
||||
|
||||
### 8.2 Endpoints Requiring Protection
|
||||
|
||||
#### Node Endpoints (New Protection)
|
||||
```
|
||||
GET /api/health # Keep public or protect?
|
||||
GET /api/mesh/state
|
||||
POST /api/mesh/sync # Already protected
|
||||
GET /api/nodes
|
||||
POST /api/nodes
|
||||
GET /api/nodes/:id
|
||||
PATCH /api/nodes/:id
|
||||
DELETE /api/nodes/:id
|
||||
GET /api/nodes/:id/metrics
|
||||
GET /api/nodes/:id/version
|
||||
POST /api/nodes/:id/sync-plugins
|
||||
GET /api/nodes/:id/compatibility
|
||||
```
|
||||
|
||||
#### Dashboard Endpoints (New Protection)
|
||||
```
|
||||
# All /api/* routes except /api/health
|
||||
GET /api/tasks
|
||||
POST /api/tasks
|
||||
GET /api/tasks/:id
|
||||
PATCH /api/tasks/:id
|
||||
DELETE /api/tasks/:id
|
||||
# ... all other task, agent, mission, plugin endpoints
|
||||
```
|
||||
|
||||
#### New Auth Endpoints
|
||||
```
|
||||
POST /api/auth/login # Session login
|
||||
POST /api/auth/logout # Session logout
|
||||
GET /api/auth/me # Current user info
|
||||
POST /api/auth/setup # First-run user setup
|
||||
```
|
||||
|
||||
### 8.3 New Files Required
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `packages/dashboard/src/auth-middleware.ts` | Token validation middleware |
|
||||
| `packages/dashboard/src/auth-routes.ts` | Login/logout/register endpoints |
|
||||
| `packages/dashboard/app/pages/Login.tsx` | Login page UI |
|
||||
| `packages/dashboard/app/hooks/useAuth.ts` | Auth state hook |
|
||||
| `packages/dashboard/app/contexts/AuthContext.tsx` | Auth context provider |
|
||||
|
||||
---
|
||||
|
||||
## 9. Implementation Phases
|
||||
|
||||
### Phase 1: Node Auth (Low Effort, High Impact)
|
||||
1. Add Bearer token validation middleware
|
||||
2. Protect all `/api/nodes/` and `/api/mesh/` endpoints
|
||||
3. Test with existing `NodeConnection` code
|
||||
4. Document key management
|
||||
|
||||
### Phase 2: Dashboard API Key (Medium Effort)
|
||||
1. Add `FUSION_API_KEY` environment variable
|
||||
2. Protect all `/api/` routes with key validation
|
||||
3. Add `--api-key` flag to CLI
|
||||
4. Update dashboard to send key with requests
|
||||
|
||||
### Phase 3: Dashboard Sessions (Higher Effort)
|
||||
1. Add users/sessions tables
|
||||
2. Create login page
|
||||
3. Implement session management
|
||||
4. Add protected route wrapper
|
||||
5. Implement CSRF protection
|
||||
|
||||
### Phase 4: Advanced Auth (Future)
|
||||
1. OAuth integration
|
||||
2. Role-based access control
|
||||
3. Audit logging
|
||||
4. Token refresh mechanisms
|
||||
|
||||
---
|
||||
|
||||
## 10. References
|
||||
|
||||
### Code References
|
||||
- `packages/core/src/node-connection.ts` — Node connection + Bearer token sending
|
||||
- `packages/core/src/central-core.ts` — Node registry + mesh sync validation
|
||||
- `packages/dashboard/src/server.ts` — Server creation + route mounting
|
||||
- `packages/dashboard/src/api-error.ts` — `unauthorized()` helper
|
||||
- `packages/dashboard/src/rate-limit.ts` — Rate limiting infrastructure
|
||||
- `packages/cli/src/commands/serve.ts` — `fn serve` startup
|
||||
- `packages/cli/src/commands/dashboard.ts` — `fn dashboard` startup
|
||||
|
||||
### Authentication Patterns
|
||||
- Bearer token: RFC 6750
|
||||
- JWT: RFC 7519
|
||||
- Session cookies: OWASP Session Management Cheat Sheet
|
||||
- Password hashing: OWASP Password Storage Cheat Sheet
|
||||
- mTLS: RFC 5246 / TLS 1.3
|
||||
|
||||
---
|
||||
|
||||
*Report generated as part of FN-1783 research task.*
|
||||
@@ -1,232 +0,0 @@
|
||||
# Security Audit Report — Fusion Monorepo
|
||||
|
||||
**Task:** FN-1784
|
||||
**Date:** 2026-04-14
|
||||
**Auditor:** AI Agent
|
||||
**Scope:** `@fusion/core`, `@fusion/dashboard`, `@fusion/engine`, `@gsxdsm/fusion`
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The Fusion monorepo demonstrates a **generally strong security posture** with several well-implemented security controls. Key strengths include robust path traversal prevention in file operations, HMAC-SHA256 webhook signature verification with timing-safe comparison, git ref validation to prevent command injection, async execution of user-configured commands with timeouts, and parameterized SQLite queries throughout the database layer.
|
||||
|
||||
The audit identified **3 findings requiring attention**: one **Medium** severity issue (lack of request body size limits on text-based endpoints), and two **Info/Low** severity observations related to rate limiting coverage and potential information disclosure through error messages. No Critical or High severity vulnerabilities were identified.
|
||||
|
||||
The codebase demonstrates security awareness through:
|
||||
- Path boundary validation via `validatePath()` in `file-service.ts`
|
||||
- Git ref sanitization via `isValidBranchName()` and `isValidGitRef()` in `routes.ts`
|
||||
- Proper use of `timingSafeEqual` for HMAC verification in `github-webhooks.ts`
|
||||
- Async `exec` with timeouts for user-configured commands in `executor.ts`
|
||||
- Parameterized SQL queries throughout `store.ts`
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### [MEDIUM] Missing Request Body Size Limits on Text-Based API Endpoints
|
||||
|
||||
- **Severity:** Medium
|
||||
- **Category:** Rate Limiting / DoS Resilience
|
||||
- **Location:** `packages/dashboard/src/routes.ts:2800-2840`
|
||||
- **Description:** Several text input endpoints accept large strings without enforcing content-length limits, potentially enabling resource exhaustion attacks via oversized payloads.
|
||||
- **Evidence:**
|
||||
- `POST /tasks/:id/comments` accepts `text` up to 2000 characters (validated at lines 2786-2788), but no raw request body size limit
|
||||
- `PUT /tasks/:id/documents/:key` accepts `content` up to 100000 characters (validated at line 3350), but no raw request body size limit
|
||||
- `POST /memory` (PUT route) accepts arbitrary string content without size validation
|
||||
- The multer middleware only limits file uploads (`5MB` at line 95), not JSON/text payloads
|
||||
- **Recommendation:** Add global JSON body size limits via Express middleware configuration:
|
||||
```typescript
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
```
|
||||
Also consider adding explicit `content-length` validation headers where oversized payloads could cause denial of service.
|
||||
|
||||
---
|
||||
|
||||
### [LOW] Rate Limiting Gaps on Bulk Operations
|
||||
|
||||
- **Severity:** Low
|
||||
- **Category:** Rate Limiting / DoS Resilience
|
||||
- **Location:** `packages/dashboard/src/routes.ts:2710-2770`
|
||||
- **Description:** Certain endpoints that create multiple resources or perform bulk operations lack specific rate limiting, potentially enabling abuse.
|
||||
- **Evidence:**
|
||||
- `POST /planning/start-breakdown` generates multiple tasks without per-session rate limiting
|
||||
- `POST /tasks/batch-update-models` updates multiple tasks but uses the standard mutation rate limit (30 req/min)
|
||||
- `POST /api/ai/summarize-title` endpoint lacks specific rate limiting despite AI service costs
|
||||
- `POST /api/subtasks/start-streaming` has no per-session rate limit
|
||||
- **Recommendation:** Consider adding endpoint-specific rate limits for expensive operations:
|
||||
```typescript
|
||||
const summarizationLimit = rateLimit({ windowMs: 60_000, max: 10 });
|
||||
app.post('/api/ai/summarize-title', summarizationLimit, ...);
|
||||
```
|
||||
The existing rate limiter in `rate-limit.ts` supports custom configurations per endpoint.
|
||||
|
||||
---
|
||||
|
||||
### [INFO] In-Memory Rate Limiter Memory Footprint
|
||||
|
||||
- **Severity:** Info
|
||||
- **Category:** Rate Limiting / DoS Resilience
|
||||
- **Location:** `packages/dashboard/src/rate-limit.ts:55-70`
|
||||
- **Description:** The sliding-window rate limiter stores all client records in memory without persistent cleanup of expired entries beyond periodic garbage collection.
|
||||
- **Evidence:**
|
||||
```typescript
|
||||
const clients = new Map<string, ClientRecord>();
|
||||
const cleanup = setInterval(() => { /* cleanup expired */ }, windowMs);
|
||||
```
|
||||
Under high traffic with many unique IPs, the client map could grow unbounded between cleanup cycles.
|
||||
- **Recommendation:** The current implementation is acceptable for typical usage patterns. If Fusion serves high-traffic deployments with thousands of unique client IPs, consider:
|
||||
1. Adding a maximum map size with LRU eviction
|
||||
2. Using Redis-backed rate limiting for distributed deployments
|
||||
3. Reducing the cleanup interval
|
||||
|
||||
---
|
||||
|
||||
### [INFO] SQL Error Messages in Development Mode
|
||||
|
||||
- **Severity:** Info
|
||||
- **Category:** Information Disclosure
|
||||
- **Location:** `packages/dashboard/src/server.ts:380-400`
|
||||
- **Description:** Error messages from SQLite/database errors are returned to clients in development mode, which could leak schema information.
|
||||
- **Evidence:**
|
||||
```typescript
|
||||
const message = process.env.NODE_ENV === "production"
|
||||
? fallbackMessage
|
||||
: err instanceof Error && err.message
|
||||
? err.message
|
||||
: fallbackMessage;
|
||||
```
|
||||
When `NODE_ENV !== "production"`, raw error messages are returned, which may include SQLite constraint violations, table names, or query details.
|
||||
- **Recommendation:** The production fallback is correctly implemented. For additional defense-in-depth:
|
||||
1. Consider sanitizing error messages in `store.ts` to strip SQL-specific details before throwing
|
||||
2. Add a test that verifies production mode strips SQL error details
|
||||
3. Log full error details server-side for debugging while returning generic messages to clients
|
||||
|
||||
---
|
||||
|
||||
### [INFO] Webhook Replay Attack Prevention
|
||||
|
||||
- **Severity:** Info
|
||||
- **Category:** Webhook Security
|
||||
- **Location:** `packages/dashboard/src/github-webhooks.ts:65-90`
|
||||
- **Description:** The webhook verification correctly validates HMAC signatures but does not implement explicit replay attack prevention (e.g., timestamp validation or nonce tracking).
|
||||
- **Evidence:** `verifyWebhookSignature()` only checks that the signature matches, not whether the webhook is a replay of a previously processed event.
|
||||
- **Recommendation:** GitHub webhooks include an `X-Hub-Signature-256` header that provides cryptographic integrity. For most use cases, the HMAC verification is sufficient. If replay attacks are a concern:
|
||||
1. Store recent webhook event IDs with a TTL (e.g., last 5 minutes)
|
||||
2. Reject webhooks with duplicate event IDs within the TTL window
|
||||
3. Note: GitHub already timestamps webhooks and recommends rejecting requests older than 5 minutes
|
||||
|
||||
---
|
||||
|
||||
## Positive Security Controls
|
||||
|
||||
The following security controls are implemented well and should be maintained:
|
||||
|
||||
### 1. Path Traversal Prevention in File Operations
|
||||
- **Location:** `packages/dashboard/src/file-service.ts:170-210`
|
||||
- **Implementation:** The `validatePath()` function performs comprehensive path validation:
|
||||
- Rejects null bytes and absolute paths
|
||||
- Resolves paths against base directory
|
||||
- Verifies resolved path starts with base using `relative()` comparison
|
||||
- Blocks `../` traversal attempts
|
||||
|
||||
### 2. Git Ref Sanitization
|
||||
- **Location:** `packages/dashboard/src/routes.ts:700-725`, `packages/dashboard/src/routes.ts:770-800`
|
||||
- **Implementation:** `isValidBranchName()` and `isValidGitRef()` validate all user-supplied git references against allowlist patterns:
|
||||
- Blocks shell metacharacters (`;`, `<`, `>`, `&`, `|`, backticks, `$`, etc.)
|
||||
- Prevents whitespace and option-prefixed values
|
||||
- Validates against reserved git ref names
|
||||
|
||||
### 3. HMAC-SHA256 Webhook Signature Verification
|
||||
- **Location:** `packages/dashboard/src/github-webhooks.ts:65-90`
|
||||
- **Implementation:** Uses `timingSafeEqual` for constant-time signature comparison, preventing timing attacks:
|
||||
```typescript
|
||||
if (!timingSafeEqual(signatureBuffer, expectedBuffer)) {
|
||||
return { valid: false, error: "Signature mismatch" };
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Async Command Execution with Timeouts
|
||||
- **Location:** `packages/engine/src/executor.ts:1-30`
|
||||
- **Implementation:** User-configured commands (`testCommand`, `buildCommand`, `setupScript`) use async `exec` with explicit timeouts, preventing process stalls:
|
||||
```typescript
|
||||
import { promisify } from "node:util";
|
||||
const execAsync = promisify(exec);
|
||||
// Used with timeout in all user-command execution paths
|
||||
```
|
||||
|
||||
### 5. Parameterized SQLite Queries
|
||||
- **Location:** `packages/core/src/store.ts:600-700`
|
||||
- **Implementation:** All database operations use parameterized queries with `?` placeholders:
|
||||
```typescript
|
||||
this.db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
||||
```
|
||||
This prevents SQL injection throughout the data layer.
|
||||
|
||||
### 6. Multi-Project Path Validation
|
||||
- **Location:** `packages/core/src/central-core.ts` (project registration)
|
||||
- **Implementation:** Project registration validates absolute paths, preventing path traversal during project registration. Path validation ensures registered projects are within expected directories.
|
||||
|
||||
### 7. Checkout Leasing Conflict Detection
|
||||
- **Location:** `packages/dashboard/src/routes.ts:3900-3950`
|
||||
- **Implementation:** Checkout conflicts return HTTP 409 with structured error response:
|
||||
```typescript
|
||||
res.status(409).json({
|
||||
error: "Task is already checked out",
|
||||
currentHolder: err.currentHolderId,
|
||||
taskId: err.taskId,
|
||||
});
|
||||
```
|
||||
This prevents race conditions in task ownership.
|
||||
|
||||
---
|
||||
|
||||
## Recommendations Summary
|
||||
|
||||
Ranked by priority for follow-up tasks:
|
||||
|
||||
### 1. Add Global JSON Body Size Limits (Medium Priority)
|
||||
**File:** `packages/dashboard/src/server.ts`
|
||||
**Action:** Configure `express.json()` with explicit size limits to prevent oversized payload DoS attacks.
|
||||
**Effort:** Low — single line configuration change.
|
||||
|
||||
### 2. Add Endpoint-Specific Rate Limits for Expensive AI Operations (Low Priority)
|
||||
**Files:** `packages/dashboard/src/routes.ts`, `packages/dashboard/src/ai-summarize.ts`
|
||||
**Action:** Add specific rate limits for AI endpoints (`/api/ai/summarize-title`, `/planning/*`, `/subtasks/*`) to prevent abuse of expensive model calls.
|
||||
**Effort:** Low — existing rate limiter infrastructure can be reused.
|
||||
|
||||
### 3. Sanitize Database Error Messages (Low Priority)
|
||||
**File:** `packages/core/src/store.ts`
|
||||
**Action:** Wrap database operations to sanitize error messages before throwing, stripping SQL-specific details (table names, constraint names, column names) for production builds.
|
||||
**Effort:** Medium — requires wrapping error handling across the store.
|
||||
|
||||
### 4. Consider Webhook Replay Prevention (Informational)
|
||||
**File:** `packages/dashboard/src/github-webhooks.ts`
|
||||
**Action:** If webhook replay attacks are a threat model concern, implement event ID tracking with TTL. For most deployments, HMAC verification alone is sufficient.
|
||||
**Effort:** Medium — requires persistent storage for event IDs.
|
||||
|
||||
### 5. Document Security Considerations in Project Memory (Informational)
|
||||
**File:** `.fusion/memory.md`
|
||||
**Action:** Add a "Security Considerations" section documenting the security architecture, trusted boundaries, and known security-related patterns for future developers.
|
||||
**Effort:** Low — documentation only.
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Files Reviewed
|
||||
|
||||
| Package | File | Purpose |
|
||||
|---------|------|---------|
|
||||
| `@fusion/dashboard` | `src/routes.ts` | API route definitions, input validation, git operations |
|
||||
| `@fusion/dashboard` | `src/server.ts` | Server initialization, middleware, WebSocket setup |
|
||||
| `@fusion/dashboard` | `src/file-service.ts` | File read/write/move/delete operations |
|
||||
| `@fusion/dashboard` | `src/rate-limit.ts` | Rate limiting configuration |
|
||||
| `@fusion/dashboard` | `src/github-webhooks.ts` | Webhook signature verification |
|
||||
| `@fusion/core` | `src/store.ts` | TaskStore, SQLite operations, path handling |
|
||||
| `@fusion/core` | `src/plugin-loader.ts` | Plugin loading, dynamic imports, hook isolation |
|
||||
| `@fusion/engine` | `src/executor.ts` | Agent execution, worktree creation, tool boundaries |
|
||||
| `@fusion/engine` | `src/merger.ts` | Merge logic, git operations |
|
||||
| `@fusion/cli` | `src/commands/serve.ts` | Headless node startup, `/api/health` endpoint |
|
||||
|
||||
---
|
||||
|
||||
*Report generated by AI security audit agent for FN-1784*
|
||||
@@ -1,949 +0,0 @@
|
||||
# Cross-Node Project and Board Management Design Document
|
||||
|
||||
**Task:** FN-1833
|
||||
**Date:** 2026-04-14
|
||||
**Status:** Design Document
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Executive Summary](#1-executive-summary)
|
||||
2. [Architecture Overview](#2-architecture-overview)
|
||||
3. [How the Dashboard Connects to Nodes](#3-how-the-dashboard-connects-to-nodes)
|
||||
4. [How Projects Tie Into Nodes](#4-how-projects-tie-into-nodes)
|
||||
5. [How the Board Works Across Nodes](#5-how-the-board-works-across-nodes)
|
||||
6. [Task Dependency Chain](#6-task-dependency-chain)
|
||||
7. [Answers to Key Questions](#7-answers-to-key-questions)
|
||||
8. [Design Recommendations](#8-design-recommendations)
|
||||
9. [Gap Analysis](#9-gap-analysis)
|
||||
10. [Verification Checklist](#10-verification-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
This document describes the cross-node project and board management architecture in Fusion. The system enables a single dashboard instance to connect to and manage projects across multiple Fusion nodes (local or remote). The architecture uses a **proxy-based model** where the local dashboard server forwards API requests to remote nodes.
|
||||
|
||||
**Current State:**
|
||||
- The frontend proxy infrastructure exists (`proxyApi()` in `packages/dashboard/app/api.ts`, `useRemoteNodeData()` hook)
|
||||
- Node registration and connection testing exists (`CentralCore.connectToRemoteNode()`)
|
||||
- Project-to-node assignment exists (`CentralCore.assignProjectToNode()`)
|
||||
- **Critical Gap:** No backend proxy routes exist — the Express server has no `/api/proxy/:nodeId/*` handlers
|
||||
|
||||
**The path forward** is to implement the backend proxy routes (FN-1802/FN-1806), wire up project-node assignment through registration (FN-1803), and complete the remaining integration work.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture Overview
|
||||
|
||||
### 2.1 Three-Tier Model
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ Browser Dashboard │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ NodeContext (useNodeContext) │ │
|
||||
│ │ - currentNode: NodeConfig | null │ │
|
||||
│ │ - isRemote: currentNode !== null │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────────┐ ┌────────────────────┐ │
|
||||
│ │ App.tsx │ │ App.tsx │ │
|
||||
│ │ (Local Mode) │ │ (Remote Mode) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ useTasks() ──────┼───────┼─ useRemoteNodeData│ │
|
||||
│ │ useProjects() │ │ .projects │ │
|
||||
│ │ EventSource() │ │ .tasks │ │
|
||||
│ └────────────────────┘ │ │ │
|
||||
│ │ EventSource() │ │
|
||||
│ │ /api/events │ │
|
||||
│ └────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────┴────────────────┐
|
||||
│ │
|
||||
Local API Requests Proxy API Requests
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────────────────────────────────┐ ┌────────────────────────────────────┐
|
||||
│ Local Dashboard Server │ │ Local Dashboard Server │
|
||||
│ │ │ │
|
||||
│ /api/tasks ─────────────────────│ │ /api/proxy/:nodeId/* ────────────│
|
||||
│ /api/projects │ │ │
|
||||
│ /api/events │ │ (Forward to remote node) │
|
||||
│ (TaskStore) │ │ │
|
||||
│ │ │ /api/proxy/:nodeId/health │
|
||||
│ │ │ /api/proxy/:nodeId/projects │
|
||||
│ │ │ /api/proxy/:nodeId/tasks │
|
||||
│ │ │ /api/proxy/:nodeId/events │
|
||||
│ │ │ /api/proxy/:nodeId/project-health │
|
||||
│ │ │ │
|
||||
└────────────────────────────────────┘ └────────────────────────────────────┘
|
||||
│
|
||||
│ HTTP Request
|
||||
│ (with optional
|
||||
│ Authorization header)
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ Remote Fusion Node │
|
||||
│ (fn serve --host <host>) │
|
||||
│ │
|
||||
│ /api/health │
|
||||
│ /api/projects │
|
||||
│ /api/tasks │
|
||||
│ /api/events │
|
||||
│ (TaskStore) │
|
||||
│ │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Key Components
|
||||
|
||||
| Component | File | Purpose |
|
||||
|-----------|------|---------|
|
||||
| `NodeContext` | `packages/dashboard/app/context/NodeContext.tsx` | Tracks current node in React context |
|
||||
| `proxyApi()` | `packages/dashboard/app/api.ts` (line 2176) | Rewrites URLs to `/api/proxy/:nodeId/...` |
|
||||
| `withNodeId()` | `packages/dashboard/app/api.ts` (line 2162) | URL rewriting helper |
|
||||
| `useRemoteNodeData()` | `packages/dashboard/app/hooks/useRemoteNodeData.ts` | Fetches remote node data |
|
||||
| `useRemoteNodeEvents()` | `packages/dashboard/app/hooks/useRemoteNodeEvents.ts` | Subscribes to remote SSE |
|
||||
| `CentralCore` | `packages/core/src/central-core.ts` | Node registry, project registry |
|
||||
| `NodeConnection` | `packages/core/src/node-connection.ts` | Remote node connection testing |
|
||||
| `serve.ts` | `packages/cli/src/commands/serve.ts` | Headless node server |
|
||||
|
||||
### 2.3 Data Flow
|
||||
|
||||
**Local Mode:**
|
||||
1. Dashboard → `useTasks()`, `useProjects()`
|
||||
2. Fetch `GET /api/tasks`, `GET /api/projects`
|
||||
3. Subscribe `EventSource("/api/events")`
|
||||
4. TaskStore returns local data
|
||||
|
||||
**Remote Mode:**
|
||||
1. Dashboard → `setCurrentNode(node)` in NodeContext
|
||||
2. `isRemote = true` in App.tsx
|
||||
3. Fetch `useRemoteNodeData(nodeId)` → `proxyApi("/tasks", { nodeId })`
|
||||
4. URL rewritten to `/api/proxy/:nodeId/tasks`
|
||||
5. Backend forwards to remote node at `node.url`
|
||||
6. Subscribe `EventSource("/api/proxy/:nodeId/events")`
|
||||
7. Backend forwards SSE stream from remote node
|
||||
|
||||
---
|
||||
|
||||
## 3. How the Dashboard Connects to Nodes
|
||||
|
||||
### 3.1 Connection Lifecycle
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Node Connection Flow │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
1. User Registration
|
||||
┌─────────────────┐
|
||||
│ NodesView │
|
||||
│ AddNodeModal │
|
||||
└────────┬────────┘
|
||||
│ POST /api/nodes
|
||||
│ { name, host, port, secure?, apiKey? }
|
||||
▼
|
||||
2. Connection Test (server-side)
|
||||
┌─────────────────────────────────────────┐
|
||||
│ CentralCore.connectToRemoteNode() │
|
||||
│ ├── NodeConnection.test() │
|
||||
│ │ GET {url}/api/health │
|
||||
│ │ Returns { status, version, name } │
|
||||
│ └── Validates response │
|
||||
└────────────────┬────────────────────────┘
|
||||
│ Success
|
||||
▼
|
||||
3. Node Registration
|
||||
┌─────────────────────────────────────────┐
|
||||
│ CentralCore.registerNode() │
|
||||
│ ├── Store in central DB │
|
||||
│ │ nodes.url = resolved URL │
|
||||
│ │ nodes.apiKey = provided key │
|
||||
│ ├── Set status = "offline" │
|
||||
│ └── Emit "node:registered" │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
▼
|
||||
4. User Views Node
|
||||
┌─────────────────┐
|
||||
│ NodeCard │
|
||||
│ click handler │
|
||||
└────────┬────────┘
|
||||
│ setCurrentNode(node)
|
||||
▼
|
||||
5. Context Switch
|
||||
┌─────────────────────────────────────────┐
|
||||
│ NodeContext │
|
||||
│ ├── currentNode = node │
|
||||
│ ├── currentNodeId = node.id │
|
||||
│ ├── isRemote = true │
|
||||
│ └── localStorage.setItem(...) │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
▼
|
||||
6. App Switches Data Source
|
||||
┌─────────────────────────────────────────┐
|
||||
│ App.tsx (lines 71-74) │
|
||||
│ effectiveProjects = remoteData.projects │
|
||||
│ effectiveTasks = remoteData.tasks │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
▼
|
||||
7. Proxy Requests Begin
|
||||
┌─────────────────────────────────────────┐
|
||||
│ proxyApi("/tasks", { nodeId }) │
|
||||
│ └── withNodeId("/tasks", nodeId) │
|
||||
│ → "/api/proxy/{nodeId}/tasks" │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
▼
|
||||
8. [BLOCKED] Backend Forwards
|
||||
┌─────────────────────────────────────────┐
|
||||
│ [FN-1802] /api/proxy/:nodeId/* │
|
||||
│ ├── Fetch node.url + path │
|
||||
│ ├── Add Authorization header │
|
||||
│ ├── Forward request │
|
||||
│ └── Return response │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Node Types
|
||||
|
||||
| Type | Description | URL | API Key |
|
||||
|------|-------------|-----|---------|
|
||||
| `local` | Dashboard's own node | N/A (not accessible remotely) | N/A |
|
||||
| `remote` | Other Fusion nodes | `http(s)://host:port` | Optional |
|
||||
|
||||
### 3.3 Authentication
|
||||
|
||||
Remote nodes can be protected with API keys:
|
||||
|
||||
```typescript
|
||||
// In CentralCore.connectToRemoteNode() or node-connection.ts:
|
||||
const response = await fetch(healthUrl, {
|
||||
headers: node.apiKey
|
||||
? { Authorization: `Bearer ${node.apiKey}` }
|
||||
: undefined,
|
||||
});
|
||||
```
|
||||
|
||||
The API key is stored in the central DB (`nodes.apiKey`) and injected on every proxied request.
|
||||
|
||||
### 3.4 Health Checking
|
||||
|
||||
`CentralCore.checkNodeHealth()` (line 1064) verifies node availability:
|
||||
|
||||
```typescript
|
||||
const healthUrl = new URL("/api/health", node.url).toString();
|
||||
const response = await fetch(healthUrl, {
|
||||
headers: node.apiKey ? { Authorization: `Bearer ${node.apiKey}` } : undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
nextStatus = response.ok ? "online" : "offline";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. How Projects Tie Into Nodes
|
||||
|
||||
### 4.1 Project-Node Assignment Model
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Project Assignment Rules │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────┐
|
||||
│ RegisteredProject │
|
||||
│ │
|
||||
│ nodeId?: string │ ───► Points to a node in the registry
|
||||
│ │ (null/undefined = unassigned)
|
||||
└────────┬──────────┘
|
||||
│
|
||||
│
|
||||
├──► nodeId = "node_abc123" (Remote Node)
|
||||
│ ┌─────────────────────────────────────────────┐
|
||||
│ │ Project runs on REMOTE node_abc123 │
|
||||
│ │ │
|
||||
│ │ Dashboard shows this project when │
|
||||
│ │ viewing node_abc123 │
|
||||
│ └─────────────────────────────────────────────┘
|
||||
│
|
||||
├──► nodeId = "node_local" (Local Node)
|
||||
│ ┌─────────────────────────────────────────────┐
|
||||
│ │ Project runs on LOCAL node │
|
||||
│ │ │
|
||||
│ │ Dashboard shows this project when │
|
||||
│ │ viewing local node │
|
||||
│ └─────────────────────────────────────────────┘
|
||||
│
|
||||
└──► nodeId = null | undefined (Unassigned)
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Project runs on LOCAL in-process runtime │
|
||||
│ │
|
||||
│ NOT shown when viewing remote nodes │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 Assignment Routing Logic
|
||||
|
||||
From `packages/dashboard/app/utils/nodeProjectAssignment.ts`:
|
||||
|
||||
```typescript
|
||||
export function isProjectRoutedToNode(project: ProjectInfo, node: NodeInfo): boolean {
|
||||
if (node.type === "remote") {
|
||||
// Remote nodes: only explicit assignment counts
|
||||
return project.nodeId === node.id;
|
||||
}
|
||||
|
||||
// Local nodes: explicit assignment OR unassigned (null/undefined)
|
||||
if (project.nodeId === node.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unassigned projects run on local in-process runtime
|
||||
if (project.nodeId === undefined || project.nodeId === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Project Registry API
|
||||
|
||||
**Current API:**
|
||||
|
||||
| Method | Endpoint | Accepts nodeId? |
|
||||
|--------|----------|-----------------|
|
||||
| GET | `/api/projects` | N/A (list all) |
|
||||
| POST | `/api/projects` | ❌ No |
|
||||
| PATCH | `/api/projects/:id` | ❌ No |
|
||||
| POST | `/api/projects/:id/assign-node` | ✅ Yes (dedicated endpoint) |
|
||||
| DELETE | `/api/projects/:id/node` | ✅ Yes (dedicated endpoint) |
|
||||
|
||||
**The Problem:**
|
||||
- `CentralCore.registerProject()` (line 230) does NOT accept `nodeId` as input
|
||||
- The `nodeId` column exists in the `projects` table but is not set during registration
|
||||
- Users must call `assignProjectToNode()` separately after registration
|
||||
|
||||
**Routes.ts Line 12715:**
|
||||
```typescript
|
||||
const project = await central.registerProject({
|
||||
name: name.trim(),
|
||||
path: path.trim(),
|
||||
isolationMode,
|
||||
// nodeId is NOT accepted here
|
||||
});
|
||||
```
|
||||
|
||||
### 4.4 Node Management API
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/nodes` | List all nodes |
|
||||
| POST | `/api/nodes` | Register remote node (calls `connectToRemoteNode`) |
|
||||
| GET | `/api/nodes/:id` | Get node detail |
|
||||
| PATCH | `/api/nodes/:id` | Update node config |
|
||||
| DELETE | `/api/nodes/:id` | Unregister node |
|
||||
| POST | `/api/nodes/:id/health-check` | Trigger health check |
|
||||
| GET | `/api/mesh/state` | Get mesh state |
|
||||
|
||||
---
|
||||
|
||||
## 5. How the Board Works Across Nodes
|
||||
|
||||
### 5.1 Data Flow
|
||||
|
||||
**Local Mode (App.tsx lines 79-83):**
|
||||
```typescript
|
||||
const { tasks, createTask, moveTask, ... } = useTasks(
|
||||
currentProject ? { projectId: currentProject.id, searchQuery: searchQuery || undefined } : { searchQuery: searchQuery || undefined }
|
||||
);
|
||||
```
|
||||
|
||||
**Remote Mode (App.tsx lines 67-72):**
|
||||
```typescript
|
||||
const remoteData = useRemoteNodeData(currentNodeId, {
|
||||
projectId: currentProject?.id,
|
||||
searchQuery: searchQuery || undefined
|
||||
});
|
||||
const remoteEvents = useRemoteNodeEvents(currentNodeId);
|
||||
|
||||
// Use remote data when in remote mode
|
||||
const effectiveProjects = isRemote && remoteData.projects.length > 0 ? remoteData.projects : projects;
|
||||
const effectiveTasks = isRemote && remoteData.tasks.length > 0 ? remoteData.tasks : [];
|
||||
```
|
||||
|
||||
### 5.2 API Functions
|
||||
|
||||
From `packages/dashboard/app/api-node.ts`:
|
||||
|
||||
```typescript
|
||||
export async function fetchRemoteNodeHealth(nodeId: string): Promise<RemoteNodeHealth> {
|
||||
return proxyApi<RemoteNodeHealth>("/health", { nodeId });
|
||||
}
|
||||
|
||||
export async function fetchRemoteNodeProjects(nodeId: string): Promise<ProjectInfo[]> {
|
||||
return proxyApi<ProjectInfo[]>("/projects", { nodeId });
|
||||
}
|
||||
|
||||
export async function fetchRemoteNodeTasks(
|
||||
nodeId: string,
|
||||
projectId: string,
|
||||
searchQuery?: string,
|
||||
): Promise<Task[]> {
|
||||
const params = new URLSearchParams({ projectId });
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
params.set("q", searchQuery.trim());
|
||||
}
|
||||
return proxyApi<Task[]>(`/tasks?${params.toString()}`, { nodeId });
|
||||
}
|
||||
|
||||
export async function fetchRemoteNodeProjectHealth(
|
||||
nodeId: string,
|
||||
projectId: string,
|
||||
): Promise<ProjectHealth> {
|
||||
return proxyApi<ProjectHealth>(`/project-health?projectId=${encodeURIComponent(projectId)}`, {
|
||||
nodeId,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 URL Rewriting
|
||||
|
||||
From `packages/dashboard/app/api.ts` (lines 2162-2179):
|
||||
|
||||
```typescript
|
||||
export function withNodeId(path: string, nodeId?: string, localNodeId?: string): string {
|
||||
if (!nodeId || nodeId === localNodeId) return path;
|
||||
// Rewrite path to proxy endpoint: /tasks -> /proxy/:nodeId/tasks
|
||||
const apiPrefix = "/api";
|
||||
const pathWithoutPrefix = path.startsWith(apiPrefix) ? path.slice(apiPrefix.length) : path;
|
||||
return `/proxy/${encodeURIComponent(nodeId)}${pathWithoutPrefix}`;
|
||||
}
|
||||
|
||||
export function proxyApi<T>(path: string, opts?: RequestInit & { nodeId?: string; localNodeId?: string }): Promise<T> {
|
||||
const { nodeId, localNodeId, ...fetchOpts } = opts ?? {};
|
||||
const resolvedPath = withNodeId(path, nodeId, localNodeId);
|
||||
return api<T>(resolvedPath, fetchOpts);
|
||||
}
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- `proxyApi("/tasks", { nodeId: "node_abc" })` → `/api/proxy/node_abc/tasks`
|
||||
- `proxyApi("/api/health", { nodeId: "node_xyz" })` → `/api/proxy/node_xyz/health`
|
||||
- `proxyApi("/tasks", {})` → `/api/tasks` (no rewrite)
|
||||
|
||||
### 5.4 SSE Proxy
|
||||
|
||||
From `packages/dashboard/app/hooks/useRemoteNodeEvents.ts` (lines 137-142):
|
||||
|
||||
```typescript
|
||||
// Build SSE URL
|
||||
const encodedNodeId = encodeURIComponent(nodeId);
|
||||
const esUrl = `/api/proxy/${encodedNodeId}/events`;
|
||||
const eventSource = new EventSource(esUrl);
|
||||
```
|
||||
|
||||
**Reconnection Logic:**
|
||||
- 3-second reconnect delay on error
|
||||
- 45-second heartbeat timeout
|
||||
- Cleanup on unmount
|
||||
|
||||
### 5.5 Parallel Data Fetching
|
||||
|
||||
From `packages/dashboard/app/hooks/useRemoteNodeData.ts` (lines 66-79):
|
||||
|
||||
```typescript
|
||||
// Fetch health and projects in parallel
|
||||
const promises: Promise<unknown>[] = [
|
||||
fetchRemoteNodeHealth(nodeId),
|
||||
fetchRemoteNodeProjects(nodeId),
|
||||
];
|
||||
|
||||
// Add tasks and project health fetches if projectId is provided
|
||||
if (projectId) {
|
||||
promises.push(fetchRemoteNodeTasks(nodeId, projectId, searchQuery));
|
||||
promises.push(fetchRemoteNodeProjectHealth(nodeId, projectId));
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Task Dependency Chain
|
||||
|
||||
The following tasks must be implemented in dependency order:
|
||||
|
||||
### 6.1 Critical Path
|
||||
|
||||
| # | Task | Description | Blocked By |
|
||||
|---|------|-------------|------------|
|
||||
| 1 | **FN-1802** | Generic proxy route (`/api/proxy/:nodeId/*`) | — |
|
||||
| 2 | **FN-1806** | Specific proxy routes (health, projects, tasks, events) | FN-1802 |
|
||||
| 3 | **FN-1803** | Node-aware project registration + directory browsing | FN-1802 |
|
||||
| 4 | **FN-1804** | Frontend node selector for project creation | FN-1803 |
|
||||
| 5 | **FN-1805** | Wire peer exchange and discovery in runtimes | FN-1802 |
|
||||
| 6 | **FN-1736** | Comprehensive project scoping review | FN-1804 |
|
||||
| 7 | **FN-1733** | Fix project pause/resume to control engines | FN-1803 |
|
||||
| 8 | **FN-1662** | Fix project health stats for non-first projects | — |
|
||||
|
||||
### 6.2 FN-1802: Generic Proxy Route
|
||||
|
||||
**Goal:** Implement a catch-all proxy route that forwards any request to a remote node.
|
||||
|
||||
**Design:**
|
||||
```typescript
|
||||
// packages/dashboard/src/routes.ts
|
||||
router.all("/proxy/:nodeId/*", async (req, res) => {
|
||||
const { nodeId } = req.params;
|
||||
const node = await central.getNode(nodeId);
|
||||
if (!node || node.type !== "remote") {
|
||||
return res.status(404).json({ error: "Node not found" });
|
||||
}
|
||||
|
||||
// Build target URL
|
||||
const targetPath = req.params[0]; // The wildcard part
|
||||
const targetUrl = new URL(`/${targetPath}`, node.url);
|
||||
|
||||
// Copy query string
|
||||
req.url.split("?")[1] && targetUrl.search = req.url.split("?")[1];
|
||||
|
||||
// Forward request
|
||||
const response = await fetch(targetUrl.toString(), {
|
||||
method: req.method,
|
||||
headers: {
|
||||
...req.headers,
|
||||
...(node.apiKey && { Authorization: `Bearer ${node.apiKey}` }),
|
||||
},
|
||||
body: ["POST", "PUT", "PATCH"].includes(req.method) ? req.body : undefined,
|
||||
});
|
||||
|
||||
// Copy response
|
||||
res.status(response.status).json(await response.json());
|
||||
});
|
||||
```
|
||||
|
||||
### 6.3 FN-1806: Specific Proxy Routes
|
||||
|
||||
**Goal:** Implement specific proxy routes with proper typing and handling.
|
||||
|
||||
**Routes needed:**
|
||||
- `GET /api/proxy/:nodeId/health` → Remote `/api/health`
|
||||
- `GET /api/proxy/:nodeId/projects` → Remote `/api/projects`
|
||||
- `GET /api/proxy/:nodeId/tasks` → Remote `/api/tasks`
|
||||
- `GET /api/proxy/:nodeId/events` → Remote SSE `/api/events`
|
||||
- `GET /api/proxy/:nodeId/project-health` → Remote `/api/project-health`
|
||||
|
||||
### 6.4 FN-1803: Node-Aware Project Registration
|
||||
|
||||
**Goal:** Accept `nodeId` in `POST /api/projects` and wire `browseDirectory` for remote nodes.
|
||||
|
||||
**Changes needed:**
|
||||
1. `routes.ts` line 12684: Accept `nodeId` in POST body
|
||||
2. Call `central.registerProject()` with `nodeId`
|
||||
3. `browse-directory` route: proxy to remote node's filesystem
|
||||
|
||||
### 6.5 FN-1804: Frontend Node Selector
|
||||
|
||||
**Goal:** Add node picker to project creation UI.
|
||||
|
||||
**UI Changes:**
|
||||
- New Project modal: Add node dropdown
|
||||
- Show which node a project is assigned to
|
||||
- Allow changing node assignment
|
||||
|
||||
### 6.6 FN-1805: Peer Exchange & Discovery Wiring
|
||||
|
||||
**Goal:** Start `PeerExchangeService` and `startDiscovery()` in runtimes.
|
||||
|
||||
**Current State:**
|
||||
- `PeerExchangeService` exists in `packages/engine/src/peer-exchange-service.ts`
|
||||
- `CentralCore.startDiscovery()` exists (line 1267)
|
||||
- **Neither is wired in `serve.ts` or dashboard runtime**
|
||||
|
||||
**Changes needed:**
|
||||
1. `InProcessRuntime.start()`: Start `PeerExchangeService`
|
||||
2. `runServe()`: Call `central.startDiscovery(config)`
|
||||
3. `runDashboard()`: Call `central.startDiscovery(config)`
|
||||
|
||||
---
|
||||
|
||||
## 7. Answers to Key Questions
|
||||
|
||||
### 7.1 Does the dashboard proxy via the API?
|
||||
|
||||
**Yes, partially.**
|
||||
|
||||
The **frontend** has complete proxy infrastructure:
|
||||
- `proxyApi()` rewrites URLs to `/api/proxy/:nodeId/...`
|
||||
- `useRemoteNodeData()` fetches via proxy
|
||||
- `useRemoteNodeEvents()` subscribes to proxy SSE
|
||||
|
||||
The **backend** is missing the proxy routes:
|
||||
- `routes.ts` has NO `/proxy/` routes
|
||||
- Requests to `/api/proxy/:nodeId/*` return 404
|
||||
- This is the critical gap blocking all remote node viewing
|
||||
|
||||
### 7.2 Can the dashboard connect to any node?
|
||||
|
||||
**Yes, with the right conditions.**
|
||||
|
||||
Any Fusion node that exposes `GET /api/health` with the expected response can be registered:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.2.3",
|
||||
"uptime": 3600
|
||||
}
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
1. Node must be reachable (network access)
|
||||
2. Node must respond to `/api/health`
|
||||
3. Node can be on any host:port combination
|
||||
4. Optional API key authentication supported
|
||||
|
||||
**Connection test flow:**
|
||||
1. `NodeConnection.test()` hits `/api/health`
|
||||
2. Validates response has `status` field
|
||||
3. Returns `{ success, url, nodeInfo }`
|
||||
4. `CentralCore.connectToRemoteNode()` registers if successful
|
||||
|
||||
### 7.3 How does this tie into projects?
|
||||
|
||||
**Projects have an optional `nodeId` field.**
|
||||
|
||||
**Assignment rules:**
|
||||
- `nodeId = remote node ID` → Project runs on that remote node
|
||||
- `nodeId = local node ID` → Project runs on local node
|
||||
- `nodeId = null/undefined` → Project runs on local in-process runtime
|
||||
|
||||
**Dashboard behavior:**
|
||||
- When viewing a **remote node**: Shows only projects explicitly assigned to that node
|
||||
- When viewing the **local node**: Shows projects assigned to local + all unassigned projects
|
||||
|
||||
**Current gaps:**
|
||||
1. `POST /api/projects` doesn't accept `nodeId`
|
||||
2. No UI for selecting node during project creation
|
||||
3. `browseDirectory` only serves local filesystem
|
||||
|
||||
---
|
||||
|
||||
## 8. Design Recommendations
|
||||
|
||||
### 8.1 Implement Generic Wildcard Proxy (FN-1802)
|
||||
|
||||
**Recommendation:** Implement a catch-all proxy route rather than individual routes.
|
||||
|
||||
**Rationale:**
|
||||
- Simpler: One route handles all paths
|
||||
- Maintainable: No need to add routes for new endpoints
|
||||
- Complete: Any API endpoint works transparently
|
||||
|
||||
**Caveats to handle:**
|
||||
- SSE streams need special handling (upgrade, streaming)
|
||||
- Request body streaming for large payloads
|
||||
- Timeout handling for long requests
|
||||
- Error propagation from remote node
|
||||
|
||||
### 8.2 SSE Proxy with Proper Cleanup
|
||||
|
||||
**Recommendation:** Implement proper SSE proxy with resource cleanup.
|
||||
|
||||
**Requirements:**
|
||||
- Abort request when client disconnects
|
||||
- 45-second heartbeat timeout (as in `useRemoteNodeEvents`)
|
||||
- Clean up EventSource on client disconnect
|
||||
- Handle partial chunk streaming
|
||||
|
||||
**Implementation pattern:**
|
||||
```typescript
|
||||
router.get("/proxy/:nodeId/events", async (req, res) => {
|
||||
const node = await getNode(req.params.nodeId);
|
||||
|
||||
// Set SSE headers
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
});
|
||||
|
||||
// Create EventSource to remote
|
||||
const eventSource = new EventSource(`${node.url}/api/events`);
|
||||
|
||||
// Forward events
|
||||
eventSource.onmessage = (event) => {
|
||||
res.write(`data: ${event.data}\n\n`);
|
||||
};
|
||||
|
||||
// Cleanup on client disconnect
|
||||
req.on("close", () => {
|
||||
eventSource.close();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 8.3 Explicit Project-Node Assignment
|
||||
|
||||
**Recommendation:** Keep assignment explicit (user chooses), not automatic.
|
||||
|
||||
**Rationale:**
|
||||
- Clarity: Users know where their projects run
|
||||
- Control: No surprise migrations
|
||||
- Safety: Can't accidentally assign to wrong node
|
||||
|
||||
**UI pattern:**
|
||||
- Node dropdown in New Project modal
|
||||
- "Assigned to: {nodeName}" badge on project cards
|
||||
- Confirmation when changing assignment
|
||||
|
||||
### 8.4 Read-Only Remote Browsing Initially
|
||||
|
||||
**Recommendation:** Implement read-only remote project viewing first.
|
||||
|
||||
**Rationale:**
|
||||
- Lower risk: No accidental writes to remote
|
||||
- Simpler: No conflict resolution needed
|
||||
- Usable: Users can inspect remote state
|
||||
|
||||
**Future scope:**
|
||||
- Task creation on remote nodes
|
||||
- Task mutations (move, update, complete)
|
||||
- Real-time collaboration
|
||||
|
||||
### 8.5 Forward Search Query in Proxy
|
||||
|
||||
**Note:** The `searchQuery` parameter is forwarded in `fetchRemoteNodeTasks()`:
|
||||
|
||||
```typescript
|
||||
const params = new URLSearchParams({ projectId });
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
params.set("q", searchQuery.trim());
|
||||
}
|
||||
return proxyApi<Task[]>(`/tasks?${params.toString()}`, { nodeId });
|
||||
```
|
||||
|
||||
This satisfies the FN-1529 requirement for search query propagation.
|
||||
|
||||
---
|
||||
|
||||
## 9. Gap Analysis
|
||||
|
||||
### 9.1 Critical Gaps (Blocking Remote Viewing)
|
||||
|
||||
| Gap | File | Line | Impact |
|
||||
|-----|------|------|--------|
|
||||
| No proxy routes | `routes.ts` | N/A | Remote viewing completely broken |
|
||||
| No SSE proxy | `routes.ts` | N/A | No real-time updates for remote |
|
||||
|
||||
### 9.2 Project-Node Assignment Gaps
|
||||
|
||||
| Gap | File | Line | Impact |
|
||||
|-----|------|------|--------|
|
||||
| No nodeId in POST /projects | `routes.ts` | 12684 | Can't assign at creation |
|
||||
| No nodeId in registerProject | `central-core.ts` | 230 | API doesn't support it |
|
||||
| No node selector UI | `App.tsx` | — | Users can't choose node |
|
||||
| browse-directory not proxied | `routes.ts` | 12591 | Can't browse remote filesystem |
|
||||
|
||||
### 9.3 Background Service Gaps
|
||||
|
||||
| Gap | File | Line | Impact |
|
||||
|-----|------|------|--------|
|
||||
| PeerExchangeService not started | `serve.ts` | — | No peer sync |
|
||||
| startDiscovery not wired | `serve.ts` | — | No mDNS discovery |
|
||||
| PeerExchangeService not started | `dashboard.ts` | — | No peer sync |
|
||||
| startDiscovery not wired | `dashboard.ts` | — | No mDNS discovery |
|
||||
|
||||
### 9.4 Project Scoping Gaps (FN-1736)
|
||||
|
||||
| Gap | Description |
|
||||
|-----|-------------|
|
||||
| SSE filtering | SSE events not filtered by projectId |
|
||||
| WebSocket filtering | WebSocket broadcasts not project-scoped |
|
||||
| Background services | Some services not aware of project context |
|
||||
|
||||
---
|
||||
|
||||
## 10. Verification Checklist
|
||||
|
||||
### 10.1 File Verification
|
||||
|
||||
- [x] `packages/core/src/central-core.ts` — Node registry, project registry, mesh state
|
||||
- [x] `packages/core/src/node-connection.ts` — Connection testing
|
||||
- [x] `packages/core/src/types.ts` — Type definitions
|
||||
- [x] `packages/core/src/central-db.ts` — Database schema with `nodeId` column
|
||||
- [x] `packages/dashboard/app/context/NodeContext.tsx` — React context
|
||||
- [x] `packages/dashboard/app/api.ts` — `proxyApi()`, `withNodeId()`
|
||||
- [x] `packages/dashboard/app/api-node.ts` — Remote node API functions
|
||||
- [x] `packages/dashboard/app/hooks/useRemoteNodeData.ts` — Data fetching
|
||||
- [x] `packages/dashboard/app/hooks/useRemoteNodeEvents.ts` — SSE subscription
|
||||
- [x] `packages/dashboard/app/App.tsx` — Local/remote switching (lines 67-83)
|
||||
- [x] `packages/dashboard/app/utils/nodeProjectAssignment.ts` — Routing rules
|
||||
- [x] `packages/dashboard/src/routes.ts` — API routes
|
||||
- [x] `packages/cli/src/commands/serve.ts` — Headless node mode
|
||||
- [x] `packages/engine/src/peer-exchange-service.ts` — Peer sync service
|
||||
|
||||
### 10.2 No Proxy Routes
|
||||
|
||||
```bash
|
||||
$ grep -rn "/proxy" packages/dashboard/src/routes.ts
|
||||
# (no output)
|
||||
```
|
||||
|
||||
Confirmed: No proxy routes exist in `routes.ts`.
|
||||
|
||||
### 10.3 nodeId in Projects Table
|
||||
|
||||
```bash
|
||||
$ grep -n "nodeId" packages/core/src/central-db.ts
|
||||
# Line 34: nodeId TEXT,
|
||||
```
|
||||
|
||||
Confirmed: `nodeId` column exists in projects table schema.
|
||||
|
||||
### 10.4 CentralCore.registerProject Input
|
||||
|
||||
```typescript
|
||||
// central-core.ts line 230
|
||||
async registerProject(input: {
|
||||
name: string;
|
||||
path: string;
|
||||
isolationMode?: IsolationMode;
|
||||
settings?: ProjectSettings;
|
||||
// nodeId is NOT accepted
|
||||
}): Promise<RegisteredProject>
|
||||
```
|
||||
|
||||
Confirmed: `nodeId` is not in the input type.
|
||||
|
||||
### 10.5 PeerExchangeService Not Wired
|
||||
|
||||
```bash
|
||||
$ grep -rn "PeerExchangeService" packages/cli/src/commands/*.ts
|
||||
# (no output)
|
||||
```
|
||||
|
||||
Confirmed: `PeerExchangeService` is not instantiated in CLI commands.
|
||||
|
||||
### 10.6 Discovery Not Wired
|
||||
|
||||
```bash
|
||||
$ grep -rn "startDiscovery" packages/cli/src/commands/*.ts
|
||||
# (no output)
|
||||
```
|
||||
|
||||
Confirmed: `startDiscovery()` is not called in CLI commands.
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Relevant Type Definitions
|
||||
|
||||
### A.1 NodeConfig
|
||||
|
||||
```typescript
|
||||
export interface NodeConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "local" | "remote";
|
||||
url?: string;
|
||||
apiKey?: string;
|
||||
status: NodeStatus;
|
||||
capabilities?: AgentCapability[];
|
||||
systemMetrics?: SystemMetrics;
|
||||
knownPeers?: string[];
|
||||
maxConcurrent: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
### A.2 RegisteredProject
|
||||
|
||||
```typescript
|
||||
export interface RegisteredProject {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
status: ProjectStatus;
|
||||
isolationMode: IsolationMode;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastActivityAt?: string;
|
||||
nodeId?: string; // Optional - points to assigned node
|
||||
settings?: ProjectSettings;
|
||||
}
|
||||
```
|
||||
|
||||
### A.3 NodeMeshState
|
||||
|
||||
```typescript
|
||||
export interface NodeMeshState {
|
||||
nodeId: string;
|
||||
nodeName: string;
|
||||
nodeUrl: string | undefined;
|
||||
status: NodeStatus;
|
||||
metrics: SystemMetrics | null;
|
||||
lastSeen: string;
|
||||
connectedAt: string;
|
||||
knownPeers: PeerNode[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Central Database Schema
|
||||
|
||||
```sql
|
||||
-- projects table
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
isolationMode TEXT NOT NULL DEFAULT 'in-process',
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
lastActivityAt TEXT,
|
||||
nodeId TEXT, -- Optional foreign key to nodes.id
|
||||
settings TEXT
|
||||
);
|
||||
|
||||
-- nodes table
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
type TEXT NOT NULL CHECK (type IN ('local', 'remote')),
|
||||
url TEXT,
|
||||
apiKey TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'offline',
|
||||
capabilities TEXT,
|
||||
systemMetrics TEXT,
|
||||
knownPeers TEXT,
|
||||
maxConcurrent INTEGER NOT NULL DEFAULT 2,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix C: Related Documentation
|
||||
|
||||
- `.fusion/memory.md` — Section "Peer Gossip Protocol (FN-1224)"
|
||||
- `.fusion/memory.md` — Section "Node Plugin Sync (FN-1246/FN-1518)"
|
||||
- `.fusion/memory.md` — Section "FN-1529: Search Query Propagation"
|
||||
- `.fusion/memory.md` — Section "FN-1522: Task State Reconciliation Pattern"
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** 2026-04-14
|
||||
@@ -1,626 +0,0 @@
|
||||
# Agent Sandbox Research Findings: FN-1839
|
||||
|
||||
*Revised FN-1859: Original AgentOS section (agentos-project/agentos) replaced with Rivet Agent OS (rivet.dev/agent-os) research.*
|
||||
|
||||
**Research Date:** 2026-04-14 (original), 2026-04-15 (revision)
|
||||
**Task:** Research AgentOS and Alternative Sandbox Technologies for Fusion Agent Execution Isolation
|
||||
**Author:** Research Agent
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
**Can Rivet Agent OS integrate with Fusion?** **Potentially viable with moderate effort** — Rivet Agent OS is a Rust-based in-process agent runtime with ~6ms cold starts (92x faster than cloud sandboxes), WebAssembly and V8 isolate security, and npm package deployment. It natively supports Claude Code, Pi, and other coding agents via the Agent Communication Protocol (ACP). Integration would require adapting Fusion's tool surface (read_file, edit_file, bash, spawn_agent) to agentOS host tools, but the architectural fit is reasonable given agentOS's host tool model.
|
||||
|
||||
**Top Recommended Sandbox Option for Fusion:** **Docker containers with seccomp profiles** for the short-to-medium term, with **Rivet Agent OS** as a compelling alternative that offers similar isolation without container overhead.
|
||||
|
||||
**Key Trade-off:** Adding Rivet Agent OS introduces a Rust native dependency alongside Node.js, but provides near-zero cold starts (~6ms vs 100–300ms for ChildProcessRuntime), granular filesystem/network permissions, and WebAssembly-based isolation without requiring Docker. The tradeoff is worthwhile for teams wanting stronger security boundaries with lower latency.
|
||||
|
||||
---
|
||||
|
||||
## 2. Fusion's Current Isolation Model
|
||||
|
||||
Fusion implements a two-tier isolation model via the `ProjectRuntime` interface defined in `packages/engine/src/project-runtime.ts`.
|
||||
|
||||
### 2.1 InProcessRuntime
|
||||
|
||||
Defined in `packages/engine/src/runtimes/in-process-runtime.ts`, this is Fusion's **default** execution mode. All components — `TaskStore`, `Scheduler`, `TaskExecutor`, `WorktreePool`, `HeartbeatMonitor`, `PluginRunner` — share the same Node.js event loop and memory space.
|
||||
|
||||
```
|
||||
TaskExecutor (executor.ts)
|
||||
├── createKbAgent() → pi-coding-agent session
|
||||
├── Custom tools: read_file, edit_file, bash, spawn_agent, task_*, plugin_*
|
||||
├── Session runs in: git worktree directory (.worktrees/{name}/)
|
||||
└── HeartbeatMonitor → 30-second check cycle
|
||||
```
|
||||
|
||||
**Isolation guarantees:** None beyond git worktree directory isolation. A buggy or malicious agent could:
|
||||
- Read/write any file the Node.js process has access to
|
||||
- Exfiltrate data via network
|
||||
- Fork processes that outlive the session
|
||||
|
||||
**Startup overhead:** ~0ms (same process, no IPC)
|
||||
|
||||
### 2.2 ChildProcessRuntime
|
||||
|
||||
Defined in `packages/engine/src/runtimes/child-process-runtime.ts`, this forks a **separate Node.js process** running an internal `InProcessRuntime`. Communication uses `IpcHost`/`IpcWorker` defined in `packages/engine/src/ipc/ipc-protocol.ts`.
|
||||
|
||||
**IPC Protocol (from `ipc-protocol.ts`):**
|
||||
- Commands: `START_RUNTIME`, `STOP_RUNTIME`, `GET_STATUS`, `GET_METRICS`, `PING`
|
||||
- Events: `TASK_CREATED`, `TASK_MOVED`, `TASK_UPDATED`, `ERROR_EVENT`, `HEALTH_CHANGED`
|
||||
- 10-second command timeout, 5-second health-check ping interval
|
||||
|
||||
**Security model:** Process-level isolation (separate memory space). The child process runs the full `InProcessRuntime`, meaning the agent still has filesystem and network access within the forked process.
|
||||
|
||||
**Startup overhead:** ~100–300ms (fork + IPC handshake + InProcessRuntime initialization)
|
||||
|
||||
**Health monitoring:**
|
||||
- 3 missed heartbeats → restart attempt
|
||||
- Exponential backoff: 1s, 5s, 15s delays
|
||||
- Max 3 restart attempts before transitioning to `errored`
|
||||
|
||||
### 2.3 Git Worktree Isolation
|
||||
|
||||
Each task receives a dedicated git worktree (`.worktrees/{name}/`) created via `git worktree add`. Branches are named `fusion/{task-id}`. This provides:
|
||||
- **Filesystem isolation at directory level** — changes are isolated to the worktree
|
||||
- **No protection against:** reading parent project files, accessing `.fusion/`, network egress, process spawning
|
||||
|
||||
The worktree is configured in `TaskExecutor.execute()` (`packages/engine/src/executor.ts`, ~line 800) and cleaned up on task completion/failure.
|
||||
|
||||
### 2.4 Tool Surface
|
||||
|
||||
The executor's agent tools (`packages/engine/src/executor.ts`, ~lines 150–300) include:
|
||||
|
||||
| Tool | Purpose | Risk |
|
||||
|------|---------|------|
|
||||
| `read_file` | Read project files | FS escape potential |
|
||||
| `edit_file` | Modify project files | FS escape potential |
|
||||
| `bash` | Execute shell commands | Full system access |
|
||||
| `spawn_agent` | Fork child agents | Process escape potential |
|
||||
| `task_*` | Task board operations | Internal API access |
|
||||
| `plugin_*` | Plugin tool execution | Depends on plugin |
|
||||
| `review_step` | Trigger code review | Read-only |
|
||||
|
||||
The `bash` tool is the highest-risk tool — it executes arbitrary shell commands in the worktree context with access to the Node.js process environment.
|
||||
|
||||
### 2.5 PluginRunner Isolation
|
||||
|
||||
Defined in `packages/engine/src/plugin-runner.ts`, the `PluginRunner` bridges plugins to the agent tool surface. Key isolation properties:
|
||||
- **Hook timeout:** 5-second default (`DEFAULT_HOOK_TIMEOUT_MS`)
|
||||
- **Error isolation:** One plugin's crash doesn't propagate to others
|
||||
- **No filesystem/network sandboxing** for plugin tool execution
|
||||
|
||||
---
|
||||
|
||||
## 3. Rivet Agent OS Deep Dive
|
||||
|
||||
### 3.1 What is Rivet Agent OS?
|
||||
|
||||
**Project:** `rivet-dev/agent-os` (GitHub)
|
||||
**Language:** Rust
|
||||
**Maturity:** 2,694 stars, 110 forks, Apache 2.0 license
|
||||
**Created:** 2024-02-07, Last push: 2026-04-14
|
||||
**Purpose:** "A portable open-source operating system for agents. ~6 ms coldstarts, 32x cheaper than sandboxes. Powered by WebAssembly and V8 isolates."
|
||||
**Website:** https://rivet.dev/agent-os/
|
||||
**GitHub:** https://github.com/rivet-dev/agent-os
|
||||
|
||||
Rivet Agent OS is fundamentally different from the original researched `agentos-project/agentos`. It is a Rust-based in-process agent runtime designed for production AI coding agents, not a Python RL research framework.
|
||||
|
||||
### 3.2 Architecture
|
||||
|
||||
agentOS is built on an **in-process operating system kernel written in JavaScript/Rust**. Three runtimes mount into the kernel:
|
||||
|
||||
1. **WebAssembly**: POSIX utilities (coreutils, grep, sed, etc.) compiled to WASM
|
||||
2. **V8 isolates**: JavaScript/TypeScript agent code runs in sandboxed V8 contexts
|
||||
|
||||
The kernel manages:
|
||||
- Virtual filesystem
|
||||
- Process table
|
||||
- Pipes and PTYs
|
||||
- Virtual network stack
|
||||
|
||||
Everything runs inside the kernel — nothing executes on the host directly.
|
||||
|
||||
**Key deployment model:** npm package (`@rivet-dev/agent-os`) — can be deployed via Rivet Cloud, self-hosted, Railway, Vercel, Kubernetes, or any container platform.
|
||||
|
||||
### 3.3 Agent Support
|
||||
|
||||
agentOS supports multiple coding agents via the **Agent Communication Protocol (ACP)**:
|
||||
- **Pi** (primary)
|
||||
- **Claude Code** (in progress)
|
||||
- **Codex** (in progress)
|
||||
- **OpenCode** (in progress)
|
||||
- **Amp** (in progress)
|
||||
|
||||
This is relevant because Fusion's pi-coding-agent uses Pi-compatible tools, making agentOS a natural fit.
|
||||
|
||||
### 3.4 Security Model
|
||||
|
||||
agentOS uses **WebAssembly and V8 isolates** for security — "the same isolation technology trusted by browsers worldwide."
|
||||
|
||||
Security features include:
|
||||
- **Deny-by-default permissions** for filesystem, network, process, and environment access
|
||||
- **Programmatic network control**: Allow, deny, or proxy any outbound connection
|
||||
- **Resource limits**: Set precise CPU and memory limits per agent
|
||||
- **Isolated private network**: Each agent runs in its own network namespace
|
||||
|
||||
### 3.5 Performance Benchmarks
|
||||
|
||||
| Metric | agentOS | Fastest Sandbox (E2B) | Speedup |
|
||||
|--------|---------|------------------------|---------|
|
||||
| Cold start p50 | 4.8 ms | 440 ms | **92x faster** |
|
||||
| Cold start p95 | 5.6 ms | 950 ms | **170x faster** |
|
||||
| Cold start p99 | 6.1 ms | 3,150 ms | **516x faster** |
|
||||
|
||||
| Workload | agentOS | Cheapest Sandbox (Daytona) | Reduction |
|
||||
|----------|---------|----------------------------|----------|
|
||||
| Full coding agent | ~131 MB | ~1,024 MB | **8x smaller** |
|
||||
| Simple shell command | ~22 MB | ~1,024 MB | **47x smaller** |
|
||||
|
||||
### 3.6 Integration with Sandboxes
|
||||
|
||||
Importantly, agentOS "pairs seamlessly with sandboxes for heavier workloads" — it can spin up a full sandbox on demand (E2B, Daytona, etc.) and mount the sandbox's filesystem when the workload needs it (browsers, native binaries, dev servers). This makes it an orchestration layer rather than a replacement for all sandboxing.
|
||||
|
||||
### 3.7 Integration Analysis (8 Dimensions)
|
||||
|
||||
| Dimension | Analysis |
|
||||
|----------|---------|
|
||||
| **Security Boundary** | Strong — WebAssembly + V8 isolates, deny-by-default permissions, per-agent network namespaces. Agent code runs in sandboxed contexts with no direct host access. |
|
||||
| **Startup Overhead** | ~6ms cold start (92x faster than cloud sandboxes). This is 17–50x faster than Fusion's ChildProcessRuntime (100–300ms). Aligns well with heartbeat cycles. |
|
||||
| **Filesystem Access** | Virtual filesystem with mount capabilities for S3, Google Drive, SQLite, host directories. Could mount `.worktrees/{task-id}` as a host directory. The `.fusion/` directory could be restricted to read-only or excluded. |
|
||||
| **Network Access** | Programmable allow/deny/proxy for outbound connections. Could allow LLM API endpoints (api.anthropic.com, api.openai.com) while blocking other egress. Matches Fusion's security needs. |
|
||||
| **IPC / Tooling Compatibility** | Host tools model — Fusion's tool surface (read_file, edit_file, bash, spawn_agent) would need to be reimplemented as agentOS host tools. This is feasible but requires effort. The pi-coding-agent tools are Node.js native; they would need a bridge to agentOS's JavaScript host tool API. |
|
||||
| **Cross-Platform** | Excellent — npm package runs on Linux, macOS, Windows. "Just an npm package. No Kubernetes operators, no sidecar containers." Works on Rivet Cloud or self-hosted. |
|
||||
| **Operational Complexity** | Moderate — adds Rust native dependency alongside Node.js. The npm package model keeps deployment similar to existing Fusion. No Docker daemon required. |
|
||||
| **Recommended Integration Point** | New `AgentOsRuntime` implementing `ProjectRuntime` interface. Replace or supplement `ChildProcessRuntime` for in-process sandbox execution with agentOS handling the isolate lifecycle. |
|
||||
|
||||
### 3.8 Verdict
|
||||
|
||||
**Viable for Fusion with moderate implementation effort.** Rivet Agent OS offers compelling advantages:
|
||||
- Near-zero cold starts (~6ms) vs ChildProcessRuntime (~100–300ms)
|
||||
- WebAssembly + V8 isolate security without Docker dependency
|
||||
- Native support for Pi-compatible agents
|
||||
- npm package deployment model
|
||||
|
||||
**Key challenges:**
|
||||
- Tool surface reimplementation: Fusion's pi-coding-agent tools (read_file, edit_file, bash, etc.) are Node.js native. They would need a bridge to agentOS's host tool API.
|
||||
- Node.js compatibility: agentOS runs agents in V8 isolates with WASM POSIX utilities. Full Node.js compatibility (npm packages, native modules) requires the sandbox extension for heavy workloads.
|
||||
- Project maturity: While actively maintained (2,694 stars, recent commits), this is still early-stage technology.
|
||||
|
||||
**Integration complexity:** Medium. The architectural fit is reasonable, but the tool bridge requires custom implementation.
|
||||
|
||||
---
|
||||
|
||||
## 4. Alternative Technology Evaluations
|
||||
|
||||
### 4.1 gVisor (Google's Application Kernel)
|
||||
|
||||
**Project:** `google/gvisor` (18,091 stars)
|
||||
**Language:** Go
|
||||
**Purpose:** Userspace kernel ("runsc") that intercepts system calls, providing a stronger isolation boundary than containers without a VM overhead.
|
||||
|
||||
#### Analysis
|
||||
|
||||
| Dimension | Analysis |
|
||||
|----------|---------|
|
||||
| **Security Boundary** | Strong — intercepts all system calls, runs in user space. Prevents kernel exploits, filesystem escapes, privilege escalation. |
|
||||
| **Startup Overhead** | ~100ms (similar to ChildProcessRuntime). Container-style: snapshot/restore. |
|
||||
| **Filesystem Access** | `/proc` filtering, capability dropping, seccomp. Read-only host filesystem by default. |
|
||||
| **Network Access** | Network namespace isolation. Can allow specific outbound HTTPS. |
|
||||
| **IPC / Tooling Compatibility** | Would run the full Fusion executor inside gVisor. Compatible with Node.js tools. |
|
||||
| **Cross-Platform** | Linux-only (kernel-level). macOS requires Linux VM. |
|
||||
| **Operational Complexity** | Moderate — requires `runsc` installed on host. Docker integration available. |
|
||||
| **Integration Point** | Replace `ChildProcessRuntime` fork with gVisor container spawn. |
|
||||
|
||||
**Verdict:** **Partially viable.** Best security/performance tradeoff for Linux deployments. Requires significant operational changes.
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Firecracker MicroVMs (AWS)
|
||||
|
||||
**Project:** `firecracker-microvm/firecracker` (33,706 stars)
|
||||
**Language:** Rust
|
||||
**Purpose:** Lightweight VMs (~125ms startup, ~5MB memory overhead) for serverless computing.
|
||||
|
||||
#### Analysis
|
||||
|
||||
| Dimension | Analysis |
|
||||
|----------|---------|
|
||||
| **Security Boundary** | Strongest — full VM isolation with hardware virtualization (KVM). |
|
||||
| **Startup Overhead** | ~125ms cold start, ~100ms with microVM snapshot/resume. |
|
||||
| **Filesystem Access** | Rootfs + scratch space. Can mount project directory read-write. |
|
||||
| **Network Access** | TAP/TUN devices. Full network namespace control. |
|
||||
| **IPC / Tooling Compatibility** | Would run Fusion executor in VM. Requires custom IPC bridge. |
|
||||
| **Cross-Platform** | Linux with KVM only. macOS requires nested virtualization or VM. |
|
||||
| **Operational Complexity** | High — requires KVM, VM management infrastructure, snapshot storage. |
|
||||
| **Integration Point** | Replaces `ChildProcessRuntime` with VM spawn. |
|
||||
|
||||
**Verdict:** **Viable but high complexity.** Best isolation available, but operational burden is significant. Better suited for multi-tenant SaaS than single-developer `fn serve` deployments.
|
||||
|
||||
---
|
||||
|
||||
### 4.3 WebAssembly / WASI (Wasmtime, Wasmer)
|
||||
|
||||
**Project:** `bytecodealliance/wasmtime` (17,885 stars)
|
||||
**Language:** Rust
|
||||
**Purpose:** Sandboxed execution for untrusted code with linear memory model.
|
||||
|
||||
#### Analysis
|
||||
|
||||
| Dimension | Analysis |
|
||||
|----------|---------|
|
||||
| **Security Boundary** | Strong — WASI provides controlled I/O, no direct syscall access. Memory-safe linear model. |
|
||||
| **Startup Overhead** | Very low — ~1–10ms. Wasm modules compile to native code via JIT. |
|
||||
| **Filesystem Access** | WASI filesystem API provides directory capability grants. Can restrict to specific paths. |
|
||||
| **Network Access** | WASI sockets API (experimental). HTTP via WASI-http. Limited but improving. |
|
||||
| **IPC / Tooling Compatibility** | Incompatible with Node.js native tools. Agent tools (bash, read_file, etc.) would need WASI-native implementations or proxying. |
|
||||
| **Cross-Platform** | Excellent — runs on Linux, macOS, Windows, browsers. Single binary distribution. |
|
||||
| **Operational Complexity** | Low — single `wasmtime` binary. WASI support in Node.js via `wasmer-js` or `@aspect-run/wasi`. |
|
||||
| **Integration Point** | Not viable as a full executor sandbox — Fusion's tools are Node.js native. Could sandbox individual `bash` commands via WASI. |
|
||||
|
||||
**Verdict:** **Not viable for full executor isolation.** The tool compatibility gap is fundamental — Fusion's agent tools are Node.js native and cannot run inside Wasm. Could be used for isolated `bash` tool execution.
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Docker / OCI Containers with Security Profiles
|
||||
|
||||
**Project:** Standard OCI runtime
|
||||
**Purpose:** Industry-standard containerization with seccomp, AppArmor, and capability filtering.
|
||||
|
||||
#### Analysis
|
||||
|
||||
| Dimension | Analysis |
|
||||
|----------|---------|
|
||||
| **Security Boundary** | Good — seccomp profile can block dangerous syscalls (ptrace, mount, etc.). Capability dropping limits privileges. |
|
||||
| **Startup Overhead** | ~200–500ms cold start, ~50ms with container reuse (Docker reuse driver). |
|
||||
| **Filesystem Access** | Bind mounts for project root and worktrees. Read-only for system directories. |
|
||||
| **Network Access** | Docker bridge network. Can allow specific outbound HTTPS with `--network=container` or custom bridge. |
|
||||
| **IPC / Tooling Compatibility** | Full compatibility — runs Fusion executor as-is inside container. |
|
||||
| **Cross-Platform** | Works on Linux. On macOS/Windows, requires Docker Desktop or OrbStack. |
|
||||
| **Operational Complexity** | Moderate — Docker daemon required. `fn serve` would need Docker socket access. |
|
||||
| **Integration Point** | Replace `ChildProcessRuntime` fork with `docker run`. |
|
||||
|
||||
**Verdict:** **Viable and practical.** Best balance of compatibility, security, and operational familiarity. Recommended short-term option.
|
||||
|
||||
---
|
||||
|
||||
### 4.5 Linux Namespace Jails (bubblewrap, firejail, nsjail)
|
||||
|
||||
**Projects:**
|
||||
- `netblue30/firejail` (7,288 stars) — Linux namespaces and seccomp-bpf sandbox
|
||||
- `projectdiscovery/nuclei` (nsjail integration)
|
||||
|
||||
#### Analysis
|
||||
|
||||
| Dimension | Analysis |
|
||||
|----------|---------|
|
||||
| **Security Boundary** | Moderate — namespace isolation without VM overhead. Cannot block kernel exploits within same user namespace. |
|
||||
| **Startup Overhead** | Very low — ~10–50ms. No container image overhead. |
|
||||
| **Filesystem Access** | Overlay filesystem, per-namespace mounts. Can whitelist specific paths. |
|
||||
| **Network Access** | Network namespace isolation available. |
|
||||
| **IPC / Tooling Compatibility** | Full compatibility — runs as subprocess with namespace isolation. |
|
||||
| **Cross-Platform** | Linux-only. No macOS support. |
|
||||
| **Operational Complexity** | Low — single binary (`firejail` or `bubblewrap`). No daemon required. |
|
||||
| **Integration Point** | Sandbox individual `bash` tool invocations rather than full executor. |
|
||||
|
||||
**Verdict:** **Partially viable.** Good for hardening individual operations without full container overhead. Cannot match gVisor's security boundary.
|
||||
|
||||
---
|
||||
|
||||
### 4.6 Node.js Isolated-VM / V8 Isolate Sandboxing
|
||||
|
||||
**Projects:**
|
||||
- `nodejs/isolated-vm` (archived, no longer maintained)
|
||||
- Community successors and `v8isolate` experiments
|
||||
|
||||
#### Analysis
|
||||
|
||||
| Dimension | Analysis |
|
||||
|----------|---------|
|
||||
| **Security Boundary** | Strong within V8 — memory isolation, no direct syscall access. Weakness: native addons can escape. |
|
||||
| **Startup Overhead** | Very low — V8 isolate creation is ~10–50ms. |
|
||||
| **Filesystem Access** | No built-in filesystem access — must be explicitly provided. |
|
||||
| **Network Access** | No built-in network — must be explicitly provided. |
|
||||
| **IPC / Tooling Compatibility** | Not compatible with Fusion's pi-coding-agent tools which require Node.js native APIs. |
|
||||
| **Cross-Platform** | Node.js runs anywhere. |
|
||||
| **Operational Complexity** | Low — no external dependencies. |
|
||||
| **Integration Point** | Could sandbox individual JS expression evaluations, not full agent sessions. |
|
||||
|
||||
**Verdict:** **Not viable for full executor isolation.** The pi-coding-agent tool surface is Node.js native and cannot run inside a V8 isolate.
|
||||
|
||||
---
|
||||
|
||||
### 4.7 E2B (Cloud-Based Code Execution Sandbox)
|
||||
|
||||
**Project:** `e2b-dev/infra` (1,022 stars)
|
||||
**Service:** e2b.dev cloud sandbox
|
||||
**Purpose:** Cloud-hosted sandboxed execution environments for AI agents.
|
||||
|
||||
#### Analysis
|
||||
|
||||
| Dimension | Analysis |
|
||||
|----------|---------|
|
||||
| **Security Boundary** | Strong — managed cloud VMs with filesystem and network isolation. |
|
||||
| **Startup Overhead** | ~500ms–2s (cloud VM spawn + agent initialization). |
|
||||
| **Filesystem Access** | Managed filesystem with workspace sync. |
|
||||
| **Network Access** | Controlled via firewall rules. HTTPS outbound allowed. |
|
||||
| **IPC / Tooling Compatibility** | Requires cloud API integration. Fusion would need to proxy agent sessions to E2B API. |
|
||||
| **Cross-Platform** | Universal — web API. Works with any Fusion deployment. |
|
||||
| **Operational Complexity** | High — external service dependency, authentication, cost per execution. |
|
||||
| **Integration Point** | New `CloudSandboxRuntime` implementing `ProjectRuntime` interface, proxying to E2B API. |
|
||||
|
||||
**Verdict:** **Viable but external dependency.** Best for teams already using E2B or wanting zero operational overhead. Requires significant architecture change.
|
||||
|
||||
---
|
||||
|
||||
### 4.8 Modal (Serverless Python/Container Execution)
|
||||
|
||||
**Project:** `modal-labs/modal-client` (459 stars)
|
||||
**Service:** modal.com serverless platform
|
||||
**Purpose:** Serverless container execution for Python with GPU support.
|
||||
|
||||
#### Analysis
|
||||
|
||||
| Dimension | Analysis |
|
||||
|----------|---------|
|
||||
| **Security Boundary** | Strong — containers with network and filesystem isolation. |
|
||||
| **Startup Overhead** | ~500ms–2s (container cold start). |
|
||||
| **Filesystem Access** | Volume mounts for persistent storage. |
|
||||
| **Network Access** | Controlled outbound via Modal's network configuration. |
|
||||
| **IPC / Tooling Compatibility** | Python-first. Node.js support limited. Fusion would need Modal-compatible wrapper. |
|
||||
| **Cross-Platform** | Universal — web API. |
|
||||
| **Operational Complexity** | High — external service, Python focus, cost per execution. |
|
||||
| **Integration Point** | Not viable — Python/Modal runtime doesn't match Fusion's TypeScript/Node.js architecture. |
|
||||
|
||||
**Verdict:** **Not viable.** Modal's Python-first design doesn't match Fusion's TypeScript/Node.js runtime.
|
||||
|
||||
---
|
||||
|
||||
## 5. Comparative Summary Table
|
||||
|
||||
| Technology | Startup Latency | Security Strength | Cross-Platform | Complexity | Fusion Fit Score (1–5) |
|
||||
|------------|----------------|-------------------|-----------------|------------|------------------------|
|
||||
| **Rivet Agent OS** | ~6ms | Strong (Wasm + V8) | Excellent (npm) | Moderate | 4 — Strong candidate |
|
||||
| **gVisor** | ~100ms | Strong (syscall interception) | Linux only | Moderate | 4 — Strong candidate |
|
||||
| **Firecracker VMs** | ~125ms | Very Strong (VM) | Linux/KVM only | High | 3 — Powerful but complex |
|
||||
| **WebAssembly/WASI** | ~10ms | Strong (linear memory) | Excellent | Low | 2 — Tool incompatibility |
|
||||
| **Docker + seccomp** | ~200–500ms | Good (syscall filtering) | Linux + Docker Desktop | Moderate | 5 — Recommended |
|
||||
| **Namespace jails (firejail)** | ~10–50ms | Moderate (namespace) | Linux only | Low | 3 — Good for hardening |
|
||||
| **V8 isolates** | ~10–50ms | Strong (V8) | Universal | Low | 2 — Tool incompatibility |
|
||||
| **E2B cloud** | ~500ms–2s | Strong (VM) | Universal | High | 3 — External dependency |
|
||||
| **Modal** | ~500ms–2s | Strong (container) | Universal | High | 1 — Python-focused |
|
||||
|
||||
**Highlighted Recommendation:** Docker + seccomp (Score: 5) for short-term with maximum compatibility; Rivet Agent OS (Score: 4) as a compelling alternative with near-zero cold starts and no Docker dependency.
|
||||
|
||||
---
|
||||
|
||||
## 6. Integration Recommendations
|
||||
|
||||
### 6.1 Short-Term (1–2 Sprints)
|
||||
|
||||
**Option A: Docker-based ChildProcessRuntime replacement**
|
||||
|
||||
Add a new `DockerRuntime` class that replaces `ChildProcessRuntime` with container-based isolation:
|
||||
|
||||
```typescript
|
||||
// packages/engine/src/runtimes/docker-runtime.ts
|
||||
class DockerRuntime implements ProjectRuntime {
|
||||
async spawnContainer(config: ProjectRuntimeConfig): Promise<void> {
|
||||
const worktreeMount = `${config.workingDirectory}/.worktrees`;
|
||||
const fusionMount = `${config.workingDirectory}/.fusion`;
|
||||
|
||||
await execAsync(
|
||||
`docker run --rm ` +
|
||||
`-v ${worktreeMount}:/project/.worktrees:rw ` +
|
||||
`-v ${fusionMount}:/project/.fusion:ro ` +
|
||||
`--network=bridge ` +
|
||||
`--security-opt seccomp=default.json ` +
|
||||
`--cap-drop=ALL ` +
|
||||
`fusion-executor:latest node /app/entrypoint.js`,
|
||||
{ cwd: config.workingDirectory }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Changes to Fusion:**
|
||||
1. Add `packages/engine/src/runtimes/docker-runtime.ts`
|
||||
2. Update `HybridExecutor.addProject()` to accept `isolationMode: "docker"`
|
||||
3. Add Docker image build (`Dockerfile`) for executor
|
||||
4. Add seccomp profile generator for allowed syscalls
|
||||
|
||||
**Pre-requisites:**
|
||||
- Docker daemon running on host
|
||||
- `fusion/executor` Docker image published
|
||||
|
||||
**Effort:** ~2 sprints for MVP
|
||||
|
||||
---
|
||||
|
||||
**Option B: Firejail-based bash tool hardening**
|
||||
|
||||
Sandbox individual `bash` tool invocations rather than the full executor:
|
||||
|
||||
```typescript
|
||||
// In executor.ts — bash tool implementation
|
||||
async executeBash(command: string, worktreePath: string): Promise<string> {
|
||||
// Wrap bash execution in firejail
|
||||
const sandboxedCommand = [
|
||||
'firejail',
|
||||
`--quiet`,
|
||||
`--noprofile`,
|
||||
`--noroot`,
|
||||
`--private=/tmp/fusion-$$`,
|
||||
`--read-only=/home`,
|
||||
`--read-only=/root`,
|
||||
`--read-only=/bin`,
|
||||
`--read-only=/usr`,
|
||||
`--network=none`, // Or --network=eth0 for outbound-only
|
||||
`bash`, `-c`, command
|
||||
].join(' ');
|
||||
|
||||
return execAsync(sandboxedCommand, { cwd: worktreePath });
|
||||
}
|
||||
```
|
||||
|
||||
**Changes to Fusion:**
|
||||
1. Modify `createBashTool()` in `packages/engine/src/executor.ts`
|
||||
2. Add firejail availability check with fallback to unsandboxed bash
|
||||
3. Document firejail installation requirement
|
||||
|
||||
**Effort:** ~0.5 sprints
|
||||
|
||||
---
|
||||
|
||||
### 6.2 Medium-Term (Architectural Change)
|
||||
|
||||
**Option: New `IsolationMode` enum value with pluggable runtime factory**
|
||||
|
||||
Refactor `ProjectRuntime` interface to support runtime factories:
|
||||
|
||||
```typescript
|
||||
// packages/core/src/types.ts
|
||||
export type IsolationMode =
|
||||
| "in-process"
|
||||
| "child-process"
|
||||
| "docker" // NEW
|
||||
| "gvisor" // NEW
|
||||
| "custom"; // NEW: user-provided runtime class
|
||||
|
||||
// packages/engine/src/project-runtime.ts
|
||||
export interface ProjectRuntimeFactory {
|
||||
create(config: ProjectRuntimeConfig): ProjectRuntime;
|
||||
}
|
||||
```
|
||||
|
||||
**Changes to Fusion:**
|
||||
1. Extend `IsolationMode` in `packages/core/src/types.ts`
|
||||
2. Add `RuntimeFactoryRegistry` in `packages/engine/src/runtimes/`
|
||||
3. Update `HybridExecutor` to use factory based on `isolationMode`
|
||||
4. Add DockerRuntime and GvisorRuntime implementations
|
||||
5. Update `ProjectSettings` interface with sandbox-specific config
|
||||
|
||||
**Effort:** ~3–4 sprints
|
||||
|
||||
---
|
||||
|
||||
### 6.3 Long-Term (Ideal State)
|
||||
|
||||
**Full gVisor integration with capability-based filesystem access:**
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Fusion Engine (Host) │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ gVisor Sandbox (runsc) │ │
|
||||
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Fusion Executor (Node.js) │ │ │
|
||||
│ │ │ ├── pi-coding-agent session │ │ │
|
||||
│ │ │ ├── TaskExecutor │ │ │
|
||||
│ │ │ └── Tool layer (read_file, edit_file, bash) │ │ │
|
||||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ Allowed mounts: │ │
|
||||
│ │ - /project/.worktrees/{task-id} (read-write) │ │
|
||||
│ │ - /project/{allowed-paths} (read-only) │ │
|
||||
│ │ - /tmp (tmpfs, read-write) │ │
|
||||
│ │ │ │
|
||||
│ │ Network: │ │
|
||||
│ │ - Allow: api.anthropic.com:443, api.openai.com:443 │ │
|
||||
│ │ - Allow: github.com:443 │ │
|
||||
│ │ - Block: all other egress │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Changes to Fusion:**
|
||||
1. Implement gVisor-based `GvisorRuntime` with custom seccomp profile
|
||||
2. Add filesystem capability grants to worktree access
|
||||
3. Implement network egress allowlist in gVisor config
|
||||
4. Add gVisor snapshot/resume for fast container reuse
|
||||
5. Update `fn serve` to validate gVisor availability
|
||||
|
||||
**Effort:** ~6+ sprints (requires significant engineering investment)
|
||||
|
||||
---
|
||||
|
||||
## 7. Risks & Open Questions
|
||||
|
||||
### 7.1 What Would Need Prototyping Before Committing
|
||||
|
||||
1. **Docker runtime performance benchmark:** Measure actual startup latency vs `ChildProcessRuntime` baseline (100–300ms target)
|
||||
|
||||
2. **gVisor syscall compatibility:** Some Node.js operations require syscalls that gVisor may not intercept correctly. Test with Fusion's actual workload.
|
||||
|
||||
3. **Agent tool compatibility inside sandbox:** Verify pi-coding-agent tools work correctly when executed from within a container/gVisor
|
||||
|
||||
4. **Network egress allowlist for LLM APIs:** Prototype configuration for allowing only specific HTTPS endpoints
|
||||
|
||||
5. **Worktree access patterns:** Test git operations (commit, push, pull) from within the sandbox
|
||||
|
||||
### 7.2 Performance Benchmarks Needed
|
||||
|
||||
| Metric | Current (ChildProcessRuntime) | Docker | Rivet Agent OS | gVisor | Firecracker |
|
||||
|--------|-------------------------------|--------|----------------|--------|-------------|
|
||||
| Cold start latency | 100–300ms | ~200–500ms | ~6ms | ~100ms | ~125ms |
|
||||
| Memory overhead | 0 | 50–100MB | ~131MB | 10–50MB | 5MB |
|
||||
| Concurrent sandbox limit | N/A | ~10–50 containers | ~100+ isolates | ~50–100 | ~100+ |
|
||||
|
||||
### 7.3 Security Audit Considerations
|
||||
|
||||
1. **Seccomp profile completeness:** Audit allowed syscalls to ensure no privilege escalation paths
|
||||
2. **Capability review:** Verify `CAP_SYS_ADMIN`, `CAP_NET_ADMIN`, etc. are properly dropped
|
||||
3. **Container escape vectors:** Review Docker/gVisor known CVEs and maintain update cadence
|
||||
4. **Network egress verification:** Ensure LLM API endpoints are the only allowed outbound destinations
|
||||
|
||||
### 7.4 Open Questions
|
||||
|
||||
1. **How should worktree cleanup work?** Currently `git worktree remove` happens on the host. With containers or agentOS, the worktree exists inside the sandbox. Need to decide: snapshot-and-copy-out vs. bind-mount from host.
|
||||
|
||||
2. **Plugin tool execution:** Plugin tools run via `PluginRunner`. Should they also be sandboxed? Current design assumes trusted plugins.
|
||||
|
||||
3. **Heartbeat timing with sandbox overhead:** The 30-second heartbeat check cycle assumes `ChildProcessRuntime` startup. With Rivet Agent OS (~6ms cold start), heartbeat timing may be less of a concern, but still needs benchmarking.
|
||||
|
||||
4. **macOS deployment story:** gVisor and namespace jails are Linux-only. What's the strategy for macOS users? Docker Desktop as common denominator? Rivet Agent OS works on macOS via npm, making it a viable option here.
|
||||
|
||||
5. **fn serve headless deployment:** `fn serve` is designed for remote machines. Running Docker inside Docker or requiring Docker-in-Docker is complex. What's the container runtime strategy for remote nodes? Rivet Agent OS may simplify this as it doesn't require Docker.
|
||||
|
||||
6. **Tool bridge complexity:** How much effort is required to implement Fusion's pi-coding-agent tools as agentOS host tools? This is the critical path item for Rivet Agent OS integration.
|
||||
|
||||
7. **Node.js compatibility:** Rivet Agent OS runs agents in V8 isolates with WASM POSIX utilities. Full Node.js compatibility (npm packages, native modules) requires the sandbox extension. What's the fallback for npm-heavy tasks?
|
||||
|
||||
---
|
||||
|
||||
## 8. Conclusion
|
||||
|
||||
Fusion's current `ChildProcessRuntime` provides process-level isolation (separate memory space) but does not restrict filesystem or network access. For use cases requiring stronger security boundaries, several viable options exist:
|
||||
|
||||
| Priority | Option | When to Choose |
|
||||
|---------|--------|---------------|
|
||||
| **1** | Docker + seccomp | Teams already using Docker, need moderate security improvement, want familiar tooling |
|
||||
| **2** | Rivet Agent OS | Teams wanting near-zero cold starts (~6ms), no Docker dependency, WebAssembly-based security |
|
||||
| **3** | gVisor | Linux deployments requiring strong syscall isolation, willing to manage `runsc` dependency |
|
||||
| **4** | Firejail hardening | Quick win for individual `bash` tool hardening, minimal operational changes |
|
||||
| **5** | Firecracker | Maximum isolation required, team has VM management infrastructure |
|
||||
|
||||
**Rivet Agent OS is a viable option** for Fusion — it is a Rust-based in-process agent runtime with compelling performance (6ms cold starts, 92x faster than cloud sandboxes) and WebAssembly/V8 isolate security. It natively supports Pi-compatible coding agents and offers an npm package deployment model.
|
||||
|
||||
**Recommended path:** Start with Docker-based isolation (Option A in 6.1) for rapid iteration and maximum compatibility, then consider Rivet Agent OS integration for teams wanting lower latency and no Docker dependency.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- `packages/engine/src/project-runtime.ts` — `ProjectRuntime` interface
|
||||
- `packages/engine/src/runtimes/child-process-runtime.ts` — Current fork-based isolation
|
||||
- `packages/engine/src/runtimes/in-process-runtime.ts` — Default in-process runtime
|
||||
- `packages/engine/src/ipc/ipc-protocol.ts` — IPC command/event types
|
||||
- `packages/engine/src/executor.ts` — Agent session creation, tools, worktree management
|
||||
- `packages/engine/src/plugin-runner.ts` — Plugin tool execution, hook timeout isolation
|
||||
- `packages/core/src/types.ts` — `IsolationMode` type, `ProjectSettings` interface
|
||||
- `rivet-dev/agent-os` — https://github.com/rivet-dev/agent-os
|
||||
- `google/gvisor` — https://github.com/google/gvisor
|
||||
- `firecracker-microvm/firecracker` — https://github.com/firecracker-microvm/firecracker
|
||||
- `bytecodealliance/wasmtime` — https://github.com/bytecodealliance/wasmtime
|
||||
- `netblue30/firejail` — https://github.com/netblue30/firejail
|
||||
- `e2b-dev/infra` — https://github.com/e2b-dev/infra
|
||||
Reference in New Issue
Block a user