Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
45 changes: 44 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1369,6 +1369,10 @@ Git Identity (for commits in sandbox):
PARALLEL_CC_GIT_USER Environment variable fallback
PARALLEL_CC_GIT_EMAIL Environment variable fallback

NPM Authentication (for private packages):
--npm-token <token> NPM token (or set PARALLEL_CC_NPM_TOKEN env var)
--npm-registry <url> Custom registry (default: registry.npmjs.org)

Examples:
# Default: uncommitted changes, review before committing
parallel-cc sandbox-run --repo . --prompt "Fix bug"
Expand All @@ -1380,7 +1384,13 @@ Examples:
parallel-cc sandbox-run --repo . --prompt "Fix #42" --branch feature/issue-42

# Override git identity for commits
parallel-cc sandbox-run --repo . --prompt "Fix bug" --git-user "CI Bot" --git-email "ci@example.com"`)
parallel-cc sandbox-run --repo . --prompt "Fix bug" --git-user "CI Bot" --git-email "ci@example.com"

# Private NPM packages
parallel-cc sandbox-run --repo . --prompt "Install deps" --npm-token "npm_xxx"

# Custom NPM registry
parallel-cc sandbox-run --repo . --prompt "Task" --npm-token "xxx" --npm-registry "https://npm.company.com"`)
.requiredOption('--repo <path>', 'Repository path')
.option('--prompt <text>', 'Prompt text to execute')
.option('--prompt-file <path>', 'Path to prompt file (e.g., PLAN.md, .apm/Implementation_Plan.md)')
Expand All @@ -1394,6 +1404,8 @@ Examples:
.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('--npm-token <token>', 'NPM authentication token for private packages (or set PARALLEL_CC_NPM_TOKEN env var)')
.option('--npm-registry <url>', 'Custom NPM registry URL (default: https://registry.npmjs.org)', 'https://registry.npmjs.org')
.option('--json', 'Output as JSON')
.action(async (options) => {
const coordinator = new Coordinator();
Expand Down Expand Up @@ -1732,6 +1744,37 @@ Examples:
}
}

// Configure NPM authentication if token provided
// Priority: CLI flag > environment variable
const npmToken = options.npmToken || process.env.PARALLEL_CC_NPM_TOKEN;
if (npmToken) {
if (!options.json) {
console.log(chalk.dim(' Configuring NPM authentication for private packages...'));
}

const npmConfigResult = await sandboxManager.configureNpmAuth(
sandbox,
npmToken,
options.npmRegistry
);

if (npmConfigResult) {

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.

The npmConfigResult check uses truthiness, which can treat failures as successes. Consider checking npmConfigResult.success to match the pattern used elsewhere and avoid false positives.

Suggested change
if (npmConfigResult) {
if (npmConfigResult.success) {

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

if (!options.json) {
console.log(chalk.green('✓ NPM authentication configured'));
// Don't log the registry URL if it's the default
if (options.npmRegistry !== 'https://registry.npmjs.org') {
console.log(chalk.dim(` Registry: ${options.npmRegistry}`));
}
}
} else {
// NPM config failure is non-blocking - warn but continue
if (!options.json) {
console.warn(chalk.yellow('⚠ NPM authentication configuration failed'));
console.warn(chalk.dim(' Continuing without private package access'));
}
}
}

// Create E2B session in database
const db = coordinator['db'];
db.createE2BSession({
Expand Down
93 changes: 93 additions & 0 deletions src/e2b/sandbox-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,99 @@ export class SandboxManager {
}
}

/**
* Configure NPM authentication in sandbox for private package access
*
* Creates ~/.npmrc file with authentication token for the specified registry.
* This enables npm/yarn/pnpm to install private packages.
*
* Security:
* - Token is never logged
* - Token is sanitized (newlines removed)
* - Registry URL is validated
*
* @param sandbox - E2B Sandbox instance
* @param npmToken - NPM authentication token
* @param npmRegistry - NPM registry URL (default: https://registry.npmjs.org)
* @returns boolean indicating success
*/
async configureNpmAuth(
sandbox: Sandbox,
npmToken: string,
npmRegistry: string = 'https://registry.npmjs.org'
): Promise<boolean> {
try {
// Validate sandbox
if (!sandbox || !sandbox.files) {
this.logger.error('Invalid sandbox: missing files API');
return false;
}

// Validate token
if (!npmToken || typeof npmToken !== 'string' || !npmToken.trim()) {
this.logger.error('Invalid NPM token: must be a non-empty string');
return false;
}

// Reject tokens containing newlines (injection attack prevention)
// Legitimate NPM tokens never contain newlines
if (/[\r\n]/.test(npmToken)) {
this.logger.error('Invalid NPM token: contains newline characters');
return false;
}

const sanitizedToken = npmToken.trim();

// Validate registry URL
let parsedUrl: URL;
try {
parsedUrl = new URL(npmRegistry);
} catch {
this.logger.error(`Invalid registry URL format: ${npmRegistry}`);
return false;
}

// Check protocol
if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') {
this.logger.error(`Invalid registry URL: must use http or https protocol`);
return false;
}

// Warn about insecure http
if (parsedUrl.protocol === 'http:') {
this.logger.warn('Using HTTP for NPM registry is insecure. Consider using HTTPS.');
}

// Build normalized registry (origin + optional path) without query/fragment
// This ensures auth scope and registry= line use the same normalized value
const cleanPath = parsedUrl.pathname && parsedUrl.pathname !== '/'
? parsedUrl.pathname.replace(/\/$/, '')
: '';
const registryHost = parsedUrl.host + cleanPath;
const normalizedRegistry = `${parsedUrl.protocol}//${registryHost}`;

// Build .npmrc content
// Use function replacement to avoid $ character interpretation in registryHost
const npmrcContent = [
`//registry.npmjs.org/:_authToken=${sanitizedToken}`.replace(
'registry.npmjs.org',
() => registryHost
),
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
`registry=${normalizedRegistry}`
].join('\n') + '\n';

// Write .npmrc file
await sandbox.files.write('/root/.npmrc', npmrcContent);

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.

Consider adding timeout protection to sandbox.files.write(...) so it can’t hang if the sandbox is unresponsive, similar to the Promise.race pattern used elsewhere.

Suggested change
await sandbox.files.write('/root/.npmrc', npmrcContent);
await Promise.race([
sandbox.files.write('/root/.npmrc', npmrcContent),
new Promise<void>((_, reject) =>
setTimeout(() => reject(new Error('Files write timeout after 10 seconds')), 10000)
)
]);

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


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.

The .npmrc path is hardcoded to /root/.npmrc, so non‑root sandboxes won’t load auth. Consider writing to $HOME/.npmrc (with a /root/.npmrc fallback) so npm tools can find it.

Suggested change
await sandbox.files.write(`${process.env.HOME ?? '/root'}/.npmrc`, npmrcContent);

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

this.logger.info('NPM authentication configured successfully');
return true;
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
this.logger.error(`Failed to configure NPM config: ${errorMsg}`);
return false;
}
}

/**
* Get sandbox cost estimation
*
Expand Down
Loading