Compare commits
2 commits
a56da84b7a
...
582038e009
| Author | SHA1 | Date | |
|---|---|---|---|
| 582038e009 | |||
| 941140581c |
3 changed files with 147 additions and 5 deletions
123
.gitignore
vendored
123
.gitignore
vendored
|
|
@ -0,0 +1,123 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
@ -8,9 +8,12 @@ the host.
|
||||||
|
|
||||||
- Arch Linux–based image
|
- Arch Linux–based image
|
||||||
- Runs as the host user (same username, UID, GID)
|
- Runs as the host user (same username, UID, GID)
|
||||||
|
- **Per-project isolation**: Each project gets its own container (identified by project path hash)
|
||||||
|
- **Shared persistent home**: All containers mount the same home directory from XDG_DATA_HOME, allowing tools to persist across projects
|
||||||
|
- **Sudo access**: OpenCode agent can install project-specific dependencies that persist in the stopped container
|
||||||
|
- **Hard linking support**: Can hard link files like `~/.gitconfig` to share configurations with containers
|
||||||
- Mounts only the current project directory (same absolute path inside container)
|
- Mounts only the current project directory (same absolute path inside container)
|
||||||
- Persists OpenCode state in XDG_DATA_HOME/opencode-container/container-home directory
|
- **Security boundary**: No access to SSH keys, passwords, or full `$HOME` (intentionally prevents remote code pushes)
|
||||||
- No access to SSH keys, passwords, or full `$HOME`
|
|
||||||
- Simple shell function (`opencode`) to launch interactively
|
- Simple shell function (`opencode`) to launch interactively
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(message)s',
|
||||||
|
stream=sys.stderr
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class OpenCodeContainer:
|
class OpenCodeContainer:
|
||||||
IMAGE = "opencode-container:latest"
|
IMAGE = "opencode-container:latest"
|
||||||
|
|
@ -45,6 +53,14 @@ class OpenCodeContainer:
|
||||||
# Ensure container home directory exists
|
# Ensure container home directory exists
|
||||||
self.container_home_path.mkdir(parents=True, exist_ok=True)
|
self.container_home_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Check if this project already has a running container
|
||||||
|
if self.container_exists() and self.container_running():
|
||||||
|
logger.error(f"Project '{self.project_path.name}' already has a running OpenCode container.")
|
||||||
|
logger.error(f"Container name: {self.container_name}")
|
||||||
|
logger.error("Wait for the current instance to finish or manually stop it with:")
|
||||||
|
logger.error(f" docker stop {self.container_name}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Pre-create project directory structure to prevent root-owned directories
|
# Pre-create project directory structure to prevent root-owned directories
|
||||||
try:
|
try:
|
||||||
relative_path = self.project_path.relative_to(Path.home())
|
relative_path = self.project_path.relative_to(Path.home())
|
||||||
|
|
@ -60,7 +76,7 @@ class OpenCodeContainer:
|
||||||
self.create_container()
|
self.create_container()
|
||||||
|
|
||||||
if not self.start_container():
|
if not self.start_container():
|
||||||
print("Recreating container due to failed start")
|
logger.warning("Recreating container due to failed start")
|
||||||
self.remove_container()
|
self.remove_container()
|
||||||
self.create_container()
|
self.create_container()
|
||||||
if not self.start_container():
|
if not self.start_container():
|
||||||
|
|
@ -83,7 +99,7 @@ class OpenCodeContainer:
|
||||||
).returncode == 0
|
).returncode == 0
|
||||||
|
|
||||||
def build_image(self) -> None:
|
def build_image(self) -> None:
|
||||||
print(f"Building image '{self.IMAGE}' with user {self.host_username} ({self.host_uid}:{self.host_gid})")
|
logger.info(f"Building image '{self.IMAGE}' with user {self.host_username} ({self.host_uid}:{self.host_gid})")
|
||||||
self._run([
|
self._run([
|
||||||
"docker", "build",
|
"docker", "build",
|
||||||
"--build-arg", f"USERNAME={self.host_username}",
|
"--build-arg", f"USERNAME={self.host_username}",
|
||||||
|
|
@ -98,7 +114,7 @@ class OpenCodeContainer:
|
||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
def create_container(self) -> None:
|
def create_container(self) -> None:
|
||||||
print(f"Creating container '{self.container_name}'")
|
logger.info(f"Creating container '{self.container_name}'")
|
||||||
|
|
||||||
self._run([
|
self._run([
|
||||||
"docker", "create",
|
"docker", "create",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue