Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,12 @@ tests/
├── ast-analyzer.basic.test.ts # AST analyzer tests (v0.5)
├── auto-fix-engine.test.ts # Auto-fix engine tests (v0.5)
├── merge-strategies.basic.test.ts # Merge strategies tests (v0.5)
├── logger-redaction.test.ts # Sensitive data redaction tests (v1.1)
├── e2b/ssh-key-injector.test.ts # SSH key injection tests (v1.1)
├── integration.test.ts # End-to-end integration tests (v0.5)
└── mcp-tools-smoke.test.ts # MCP tools smoke tests (v0.5)

Total: 441 tests, 100% passing, 87.5% function coverage
Total: 507 tests (441 + 66 new), 100% passing, 87.5% function coverage

vitest.config.ts # Test framework configuration (project root)
```
Expand Down Expand Up @@ -735,6 +737,62 @@ The PR will include:
- **Checklist:** Review todos for security, tests, changes
- **Branch:** Auto-generated (`e2b/[slug]-[timestamp]`) or custom

### SSH Key Injection (Private Repository Access)

**Overview:**
SSH key injection enables access to private Git repositories within E2B sandboxes. This is an opt-in security feature that requires explicit user consent.

**Usage:**
```bash
# Basic SSH key injection
parallel-cc sandbox-run --repo . --prompt "Clone private repo" \
--ssh-key ~/.ssh/id_ed25519

# Non-interactive (CI/CD) - requires explicit confirmation flag
parallel-cc sandbox-run --repo . --prompt "Build private deps" \
--ssh-key ~/.ssh/deploy_key --confirm-ssh-key --json

# With OAuth and git-live
parallel-cc sandbox-run --repo . --prompt "Update dependencies" \
--ssh-key ~/.ssh/id_ed25519 --auth-method oauth --git-live
```

**Security Flow:**
1. **Validation**: Key file existence, permissions (warns if not 600/400), format verification
2. **Security Warning**: Interactive prompt explaining risks (skippable with `--confirm-ssh-key`)
3. **Injection**: Key written to sandbox's `~/.ssh` with 600 permissions
4. **Known Hosts**: GitHub, GitLab, Bitbucket automatically added
5. **SSH Config**: StrictHostKeyChecking set to `accept-new`
6. **Cleanup**: Key removed from sandbox after execution (in finally block)

**Security Considerations:**
- SSH keys are transmitted over encrypted connection (E2B uses TLS)
- Keys are stored temporarily in sandbox memory/disk
- Keys are cleaned up after execution completes (even on errors)
- Passphrase-protected keys won't work (non-interactive mode)
- All key-related data is redacted from logs automatically

**Best Practices:**
- Use dedicated deploy keys with minimal permissions (read-only when possible)
- Rotate keys regularly
- Monitor key usage in your git provider's dashboard
- Prefer repository-specific deploy keys over personal SSH keys
- Never use production keys for development/testing

**Supported Key Types:**
- RSA (`id_rsa`)
- Ed25519 (`id_ed25519`) - Recommended
- ECDSA (`id_ecdsa`)
- DSA (`id_dsa`) - Deprecated, not recommended

**Troubleshooting:**
| Error | Solution |
|-------|----------|
| "Permission denied (publickey)" | Ensure key is added to GitHub/GitLab |
| "Bad permissions" | Run `chmod 600 ~/.ssh/id_*` |
| "Invalid key format" | Verify file is a private key (not .pub) |
| "Passphrase required" | Use a key without passphrase for automation |

### Requirements & Limitations

**Environment Variables:**
Expand Down Expand Up @@ -816,7 +874,8 @@ src/e2b/
├── sandbox-manager.ts # E2B sandbox lifecycle management
├── file-sync.ts # Upload/download with compression
├── claude-runner.ts # Autonomous Claude execution
└── output-monitor.ts # Real-time output streaming
├── output-monitor.ts # Real-time output streaming
└── ssh-key-injector.ts # SSH key injection for private repo access (v1.1)
```

## Coding Standards
Expand Down
107 changes: 105 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { SandboxManager } from './e2b/sandbox-manager.js';
import { createTarball, uploadToSandbox, downloadChangedFiles, scanForCredentials } from './e2b/file-sync.js';
import { executeClaudeInSandbox } from './e2b/claude-runner.js';
import { pushToRemoteAndCreatePR } from './e2b/git-live.js';
import { validateSSHKeyPath, injectSSHKey, cleanupSSHKey, getSecurityWarning } from './e2b/ssh-key-injector.js';
import { logger } from './logger.js';
import * as fs from 'fs/promises';
import { existsSync } from 'fs';
Expand Down Expand Up @@ -1391,6 +1392,8 @@ Examples:
.option('--target-branch <branch>', 'Target branch for PR when using --git-live (default: main)', 'main')
.option('--git-user <name>', 'Git user name for commits in sandbox (default: auto-detect from local git config)')
.option('--git-email <email>', 'Git user email for commits in sandbox (default: auto-detect from local git config)')
.option('--ssh-key <path>', 'Path to SSH private key for private repository access (e.g., ~/.ssh/id_ed25519)')
.option('--confirm-ssh-key', 'Skip interactive SSH key security warning (for non-interactive use)')
.option('--json', 'Output as JSON')
.action(async (options) => {
const coordinator = new Coordinator();
Expand Down Expand Up @@ -1504,6 +1507,62 @@ Examples:
process.exit(1);
}

// Validate SSH key if provided
let sshKeyValidation: Awaited<ReturnType<typeof validateSSHKeyPath>> | undefined;
if (options.sshKey) {
// Expand ~ to home directory
const sshKeyPath = options.sshKey.startsWith('~')
? path.join(os.homedir(), options.sshKey.slice(1))
: options.sshKey;

sshKeyValidation = await validateSSHKeyPath(sshKeyPath);

if (!sshKeyValidation.valid) {
if (options.json) {
console.log(JSON.stringify({
success: false,
error: `SSH key validation failed: ${sshKeyValidation.error}`,
keyPath: sshKeyPath
}));
} else {
console.error(chalk.red(`✗ SSH key validation failed: ${sshKeyValidation.error}`));
}
process.exit(1);
}

// Warn about permissions if needed
if (sshKeyValidation.permissionsWarning && !options.json) {
console.warn(chalk.yellow(`⚠ ${sshKeyValidation.permissionsWarning}`));
}

// Display security warning and get confirmation
if (!options.json && !options.confirmSshKey) {
console.log('');
console.log(chalk.yellow(getSecurityWarning(sshKeyPath)));
console.log('');

const answer = await promptUser('Do you want to proceed with SSH key injection? (y/n): ');
if (answer !== 'y' && answer !== 'yes') {
console.log(chalk.yellow('✓ Cancelled - SSH key will not be used'));
options.sshKey = undefined;
sshKeyValidation = undefined;
} else {
console.log(chalk.green('✓ Proceeding with SSH key injection'));
}
} else if (options.json && !options.confirmSshKey) {
// In JSON mode without --confirm-ssh-key, require explicit confirmation
console.log(JSON.stringify({
success: false,
error: 'SSH key injection requires explicit confirmation',
hint: 'Use --confirm-ssh-key flag to skip interactive prompt in non-interactive mode'
}));
process.exit(1);
}

// Update the key path to expanded version
options.sshKey = sshKeyPath;
}

// Read prompt
let prompt: string;
if (options.promptFile) {
Expand Down Expand Up @@ -1610,14 +1669,17 @@ Examples:
}

// Wrap tarball usage in try/finally for guaranteed cleanup
let sandbox: Awaited<ReturnType<typeof sandboxManager.createSandbox>>['sandbox'] | undefined;
let sshKeyInjected = false;
try {
// Step 4: Create sandbox and upload
if (!options.json) {
console.log(chalk.blue('\nStep 4/6: Creating E2B sandbox...'));
}

const { sandbox, sandboxId: createdSandboxId, status } = await sandboxManager.createSandbox(sessionId);
sandboxId = createdSandboxId; // Track for cleanup in catch block
const createResult = await sandboxManager.createSandbox(sessionId);
sandbox = createResult.sandbox;
sandboxId = createResult.sandboxId; // Track for cleanup in catch block

if (!options.json) {
console.log(chalk.green(`✓ Sandbox created: ${sandboxId}`));
Expand All @@ -1637,6 +1699,37 @@ Examples:
console.log(chalk.dim(` Duration: ${(uploadResult.duration / 1000).toFixed(1)}s`));
}

// Inject SSH key if provided (after sandbox creation and upload)
if (options.sshKey) {
if (!options.json) {
console.log(chalk.dim(' Injecting SSH key for private repository access...'));
}

const injectionResult = await injectSSHKey(sandbox, options.sshKey, logger);

if (!injectionResult.success) {
if (options.json) {
console.log(JSON.stringify({
success: false,
error: `SSH key injection failed: ${injectionResult.error}`,
keyPath: options.sshKey
}));
} else {
console.error(chalk.red(`✗ SSH key injection failed: ${injectionResult.error}`));
}
await sandboxManager.terminateSandbox(sandboxId);
process.exit(1);
}

sshKeyInjected = true;
if (!options.json) {
console.log(chalk.green(`✓ SSH key injected (${injectionResult.keyType || 'unknown'} type)`));
if (injectionResult.keyFingerprint) {
console.log(chalk.dim(` Fingerprint: ${injectionResult.keyFingerprint}`));
}
}
}

// Create E2B session in database
const db = coordinator['db'];
db.createE2BSession({
Expand Down Expand Up @@ -1913,6 +2006,16 @@ Examples:
}

} finally {
// Best-effort cleanup of SSH key from sandbox (before termination)
if (sshKeyInjected && sandbox) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--dry-run promises the sandbox stays active for inspection, but the finally block still removes the SSH key. Consider skipping SSH key cleanup when --dry-run so inspection can test private repo access.

Suggested change
if (sshKeyInjected && sandbox) {
if (!options.dryRun && sshKeyInjected && sandbox) {

🚀 Want me to fix this? Reply ex: "fix it for me".

try {
await cleanupSSHKey(sandbox, logger);
} catch (cleanupError) {
// Log but don't throw - don't mask original errors
logger.warn(`Failed to cleanup SSH key: ${cleanupError instanceof Error ? cleanupError.message : 'Unknown error'}`);
}
}

// Best-effort cleanup of tarball
try {
const tarballExists = await fs.access(tarballResult.path).then(() => true).catch(() => false);
Expand Down
Loading