Compare commits

...

2 commits

Author SHA1 Message Date
582038e009 Disallow running more than one instance of opercode
We don't want to let people run more than one instance of opencode
in one project directory, this will lead to chaos and then
they interfear with each other in weird ways like when one
stopps it crashes the other, etc.
2026-01-22 01:39:44 +09:00
941140581c Update readme with important features 2026-01-22 01:39:26 +09:00
3 changed files with 147 additions and 5 deletions

123
.gitignore vendored
View file

@ -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/

View file

@ -8,9 +8,12 @@ the host.
- Arch Linuxbased image - Arch Linuxbased 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

View file

@ -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",