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
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ Aim for high test coverage, especially for:
If you discover a security vulnerability, please report it responsibly:

1. **Do not** open a public GitHub issue
2. Email security concerns to: security@opencode-ai.com
2. Report via [GitHub Security Advisory](https://github.com/isupervillain/opencode-container-exec/security)
3. Allow time for the issue to be addressed before public disclosure

### Security Review
Expand Down Expand Up @@ -257,7 +257,7 @@ We follow [Semantic Versioning](https://semver.org/):

- **Issues**: Use GitHub issues for bugs and feature requests
- **Discussions**: Use GitHub Discussions for questions
- **Security**: Email security@opencode-ai.com for vulnerabilities
- **Security**: Report via [GitHub Security Advisory](https://github.com/isupervillain/opencode-container-exec/security)

## License

Expand Down
20 changes: 8 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ toggle.sh [action] [container]
## How it works

1. The plugin detects running devcontainers by checking the `devcontainer.local_folder` Docker label
2. It converts WSL paths to Windows WSL format (`\\wsl.localhost\Ubuntu\...`)
2. It converts WSL paths to Windows format (prefers `wslpath -w`, falls back to `\\wsl.localhost\<distro>\...`)
3. When enabled, bash commands are routed through `devcontainer exec --container-id`
4. State is persisted in `~/.config/opencode/container-mode.json`

Expand All @@ -212,6 +212,7 @@ toggle.sh [action] [container]
| Variable | Description | Default | Required |
|----------|-------------|---------|----------|
| `WSL_DISTRO_NAME` | WSL distribution name | `Ubuntu` | No |
| `WSL_DISTRO` | Alternate WSL distribution variable (fallback) | _unset_ | No |
| `HOME` | User home directory | `~` | No |
| `NODE_ENV` | Node environment | `production` | No |

Expand Down Expand Up @@ -283,14 +284,13 @@ Currently, the plugin uses automatic configuration. Future versions will support
```bash
echo $WSL_DISTRO_NAME
```
2. Set distribution name if not Ubuntu:
2. Set distribution name if not Ubuntu (dot/hyphen names are supported, e.g. `Ubuntu-24.04`):
```bash
export WSL_DISTRO_NAME="Debian"
export WSL_DISTRO_NAME="Ubuntu-24.04"
```
3. Verify path conversion:
```bash
# Should show Windows-style path
echo "\\\\wsl.localhost\\$WSL_DISTRO_NAME$(pwd)"
wslpath -w "$(pwd)"
```

#### 4. Permission errors
Expand Down Expand Up @@ -394,11 +394,7 @@ The plugin logs security events in development mode. For production issues:

### Reporting Vulnerabilities

If you discover a security vulnerability, please report it responsibly:

- **Do not** open a public GitHub issue
- Email: security@opencode-ai.com
- Allow time for the issue to be addressed before public disclosure
If you discover a security vulnerability, please report it responsibly by opening a private [GitHub Security Advisory](https://github.com/isupervillain/opencode-container-exec/security/advisaries/new).

## Contributing

Expand Down Expand Up @@ -440,11 +436,11 @@ See [CHANGELOG.md](CHANGELOG.md) for version history.

## License

MIT © [OpenCode AI](https://opencode.ai)
MIT © isupervillain

## Support

- **Documentation**: [README.md](README.md)
- **Issues**: [GitHub Issues](https://github.com/isupervillain/opencode-container-exec/issues)
- **Discussions**: [GitHub Discussions](https://github.com/isupervillain/opencode-container-exec/discussions)
- **Security**: security@opencode-ai.com
- **Security**: Report via [GitHub Security Advisory](https://github.com/isupervillain/opencode-container-exec/security)
72 changes: 52 additions & 20 deletions plugin/internal.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readFileSync, existsSync, mkdirSync, renameSync, openSync, writeSync, closeSync, chmodSync, lstatSync, realpathSync, mkdtempSync, rmSync, statSync } from "fs"
import { join, dirname, resolve, relative, isAbsolute } from "path"
import { execSync } from "child_process"
import { execSync, execFileSync } from "child_process"
import { fileURLToPath } from "url"

const __dirname = dirname(fileURLToPath(import.meta.url))
Expand Down Expand Up @@ -185,6 +185,16 @@ function validateWindowsPath(path) {
return /^[a-zA-Z0-9_\-./\\: ]+$/.test(path)
}

function getAutoDetectPathCandidates(winPath) {
const candidates = [winPath]
if (winPath.startsWith('\\\\wsl.localhost\\')) {
candidates.push(winPath.replace('\\\\wsl.localhost\\', '\\\\wsl$\\'))
} else if (winPath.startsWith('\\\\wsl$\\')) {
candidates.push(winPath.replace('\\\\wsl$\\', '\\\\wsl.localhost\\'))
}
return [...new Set(candidates)]
}

// State management with secure file operations
function getState() {
try {
Expand Down Expand Up @@ -359,23 +369,41 @@ function getWindowsPath(dir) {
logSecurityEvent('invalid_directory_path', { dir })
return null
}

const distro = process.env.WSL_DISTRO_NAME || "Ubuntu"

// Validate distro name
if (!/^[a-zA-Z0-9_-]+$/.test(distro)) {
logSecurityEvent('invalid_distro_name', { distro })
return null

const envDistro = process.env.WSL_DISTRO_NAME || process.env.WSL_DISTRO

// Prefer explicit distro environment when provided.
let winPath = null
if (!envDistro) {
// Fall back to wslpath for portability across distro naming variants.
try {
winPath = execFileSync('wslpath', ['-w', dir], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
}).trim()
} catch {
// Fallback for environments without wslpath
}
}

const winPath = `\\\\wsl.localhost\\${distro}${dir}`.replace(/\//g, "\\")


if (!winPath) {
const distro = envDistro || 'Ubuntu'

// Allow common distro naming including dots (e.g. Ubuntu-24.04)
if (!/^[a-zA-Z0-9_.-]+$/.test(distro)) {
logSecurityEvent('invalid_distro_name', { distro })
return null
}

winPath = `\\\\wsl.localhost\\${distro}${dir}`.replace(/\//g, '\\')
}

// Validate the resulting path
if (!validateWindowsPath(winPath)) {
logSecurityEvent('invalid_windows_path', { winPath })
return null
}

return winPath
}

Expand Down Expand Up @@ -412,15 +440,19 @@ function findContainer(selection, currentDir) {
return { error: "Invalid directory path for auto-detection" }
}

const autoContainer = runCommand(`docker ps -q --filter "label=devcontainer.local_folder=${winPath}"`)
.split("\n")
.map(id => id.trim())
.filter(Boolean)

if (autoContainer.length === 1) {
return { containerId: autoContainer[0] }
const autoContainer = getAutoDetectPathCandidates(winPath)
.flatMap((candidate) =>
runCommand(`docker ps -q --filter "label=devcontainer.local_folder=${candidate}"`)
.split("\n")
.map(id => id.trim())
.filter(Boolean)
)
const uniqueAutoContainers = [...new Set(autoContainer)]

if (uniqueAutoContainers.length === 1) {
return { containerId: uniqueAutoContainers[0] }
}
if (autoContainer.length > 1) {
if (uniqueAutoContainers.length > 1) {
return { error: "Multiple matching containers found for current directory. Use 'list' and select by number." }
}
return { error: "No devcontainer found for current directory. Use 'list' to see available containers." }
Expand Down
64 changes: 46 additions & 18 deletions scripts/toggle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,26 @@ get_windows_path() {
return 1
fi

local distro="${WSL_DISTRO_NAME:-Ubuntu}"

# Validate distro name
if [[ ! "$distro" =~ ^[a-zA-Z0-9_-]+$ ]]; then
log_security "invalid_distro_name" "distro=$distro"
return 1
local win_path=""

local env_distro="${WSL_DISTRO_NAME:-${WSL_DISTRO:-}}"

# Prefer explicit distro environment when provided.
if [[ -z "$env_distro" ]] && command -v wslpath &> /dev/null; then
win_path=$(wslpath -w "$dir" 2>/dev/null || true)
fi

if [[ -z "$win_path" ]]; then
local distro="${env_distro:-Ubuntu}"

# Allow common distro naming including dots (e.g. Ubuntu-24.04)
if [[ ! "$distro" =~ ^[a-zA-Z0-9_.-]+$ ]]; then
log_security "invalid_distro_name" "distro=$distro"
return 1
fi

win_path=$(printf '\\\\wsl.localhost\\%s%s' "$distro" "$dir" | sed 's|/|\\|g')
fi

local win_path
win_path=$(printf '\\\\wsl.localhost\\%s%s' "$distro" "$dir" | sed 's|/|\\|g')

# Validate the resulting path
if ! validate_windows_path "$win_path"; then
Expand All @@ -169,6 +179,17 @@ get_windows_path() {
return 0
}

get_auto_detect_paths() {
local base_path="$1"
printf '%s\n' "$base_path"

if [[ "$base_path" == \\\\wsl.localhost\\* ]]; then
printf '%s\n' "${base_path/\\\\wsl.localhost\\/\\\\wsl$\\}"
elif [[ "$base_path" == \\\\wsl$\\* ]]; then
printf '%s\n' "${base_path/\\\\wsl$\\/\\\\wsl.localhost\\}"
fi
}

# Create config dir if not exists with proper permissions
mkdir -p "$CONFIG_DIR"
chmod 700 "$CONFIG_DIR"
Expand Down Expand Up @@ -292,16 +313,23 @@ select_container() {
return 1
}

local auto_container_lines
# Use proper quoting to prevent injection
if ! auto_container_lines=$(docker ps -q --filter "label=devcontainer.local_folder=$win_path" 2>&1); then
echo "❌ Failed to auto-detect container: $auto_container_lines"
return 1
fi
local auto_containers=()
while IFS= read -r id; do
[ -n "$id" ] && auto_containers+=("$id")
done <<< "$auto_container_lines"
local candidate
while IFS= read -r candidate; do
local auto_container_lines
# Use proper quoting to prevent injection
if ! auto_container_lines=$(docker ps -q --filter "label=devcontainer.local_folder=$candidate" 2>&1); then
echo "❌ Failed to auto-detect container: $auto_container_lines"
return 1
fi

local id
while IFS= read -r id; do
if [[ -n "$id" ]] && [[ ! " ${auto_containers[*]} " =~ " ${id} " ]]; then
auto_containers+=("$id")
fi
done <<< "$auto_container_lines"
done < <(get_auto_detect_paths "$win_path")

if [ ${#auto_containers[@]} -eq 1 ]; then
# Validate the returned container ID
Expand Down
20 changes: 20 additions & 0 deletions test/unit/paths.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('Path Operations', () => {
describe('getWindowsPath()', () => {
// Save original environment
const originalEnv = process.env.WSL_DISTRO_NAME;
const originalWslDistro = process.env.WSL_DISTRO;

afterEach(() => {
// Restore environment
Expand All @@ -42,6 +43,12 @@ describe('Path Operations', () => {
} else {
delete process.env.WSL_DISTRO_NAME;
}

if (originalWslDistro) {
process.env.WSL_DISTRO = originalWslDistro;
} else {
delete process.env.WSL_DISTRO;
}
});

it('should convert WSL path to Windows format with default distro', () => {
Expand All @@ -55,6 +62,19 @@ describe('Path Operations', () => {
const result = getWindowsPath('/home/user/project');
assert.strictEqual(result, '\\\\wsl.localhost\\Debian\\home\\user\\project');
});

it('should accept distro names with dots', () => {
process.env.WSL_DISTRO_NAME = 'Ubuntu-24.04';
const result = getWindowsPath('/home/user/project');
assert.strictEqual(result, '\\\\wsl.localhost\\Ubuntu-24.04\\home\\user\\project');
});

it('should use WSL_DISTRO when WSL_DISTRO_NAME is not set', () => {
delete process.env.WSL_DISTRO_NAME;
process.env.WSL_DISTRO = 'Debian';
const result = getWindowsPath('/home/user/project');
assert.strictEqual(result, '\\\\wsl.localhost\\Debian\\home\\user\\project');
});

it('should handle nested paths', () => {
delete process.env.WSL_DISTRO_NAME;
Expand Down
Loading