@opencode-cloud/core
Advanced tools
+1
-1
| [package] | ||
| name = "opencode-cloud-core" | ||
| version = "4.0.0" | ||
| version = "4.0.1" | ||
| edition = "2024" | ||
@@ -5,0 +5,0 @@ rust-version = "1.88" |
+1
-1
| { | ||
| "name": "@opencode-cloud/core", | ||
| "version": "4.0.0", | ||
| "version": "4.0.1", | ||
| "description": "Core NAPI bindings for opencode-cloud (internal package)", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
+9
-2
@@ -11,4 +11,9 @@ # opencode-cloud | ||
| > [!WARNING] | ||
| > This project is a work in progress and evolving rapidly. Use with caution. | ||
| A production-ready toolkit for deploying and managing [opencode](https://github.com/anomalyco/opencode) as a persistent cloud service, **sandboxed inside a Docker container** for isolation and security. | ||
| This project uses the opencode fork at https://github.com/pRizz/opencode, which adds additional authentication and security features. | ||
| ## Quick install (cargo) | ||
@@ -23,8 +28,10 @@ | ||
| [](https://console.aws.amazon.com/cloudformation/home#/stacks/create/review?templateURL=https://raw.githubusercontent.com/pRizz/opencode-cloud/main/infra/aws/cloudformation/opencode-cloud-quick.yaml) | ||
| [](https://console.aws.amazon.com/cloudformation/home#/stacks/create/review?templateURL=https://opencode-cloud-templates.s3.us-east-2.amazonaws.com/cloudformation/opencode-cloud-quick.yaml) | ||
| Quick deploy provisions a private EC2 instance behind a public ALB with HTTPS. | ||
| **A domain name is required** for ACM certificate validation. | ||
| **A Route53 hosted zone ID is required** for automated DNS validation. | ||
| Docs: `docs/deploy/aws.md` (includes teardown steps) | ||
| Docs: `docs/deploy/aws.md` (includes teardown steps and S3 hosting setup for forks) | ||
| Credentials: `docs/deploy/aws.md#retrieving-credentials` | ||
@@ -31,0 +38,0 @@ ## Features |
+16
-94
@@ -107,4 +107,2 @@ # ============================================================================= | ||
| dnsutils=1:9.18.* \ | ||
| # Reverse proxy for opencode UI + API | ||
| nginx=1.24.* \ | ||
| # Compression | ||
@@ -509,3 +507,3 @@ zip=3.0-* \ | ||
| # - opencode-broker build | ||
| # - nginx config for single-endpoint UI + API proxy | ||
| # - opencode web build + runtime | ||
| # - PAM configuration + systemd services | ||
@@ -522,3 +520,3 @@ # - opencode config file | ||
| # Build opencode from source (BuildKit cache mounts disabled for now) | ||
| RUN OPENCODE_COMMIT="3a4eccc7e883575e0d5a508f46036a9f243c06e8" \ | ||
| RUN OPENCODE_COMMIT="9b91eb17f5ca1b0ee99cfaa0b4c87da6dbe9e784" \ | ||
| && rm -rf /tmp/opencode-repo \ | ||
@@ -532,9 +530,11 @@ && git clone --depth 1 https://github.com/pRizz/opencode.git /tmp/opencode-repo \ | ||
| && bun install --frozen-lockfile \ | ||
| && bun run packages/opencode/script/build.ts --single \ | ||
| && cd packages/app \ | ||
| && bun run build \ | ||
| && cd packages/opencode \ | ||
| && export VITE_OPENCODE_SERVER_URL="http://localhost:3000" \ | ||
| && bun run build-single-ui \ | ||
| && rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun \ | ||
| && cd /tmp/opencode-repo \ | ||
| && mkdir -p /home/opencode/.local/share/opencode/bin \ | ||
| && mkdir -p /home/opencode/.local/share/opencode/ui \ | ||
| && cp /tmp/opencode-repo/packages/opencode/dist/opencode-*/bin/opencode /home/opencode/.local/share/opencode/bin/opencode \ | ||
| && cp -R /tmp/opencode-repo/packages/opencode/dist/opencode-*/ui/. /home/opencode/.local/share/opencode/ui/ \ | ||
| && chown -R opencode:opencode /home/opencode/.local/share/opencode \ | ||
@@ -547,10 +547,2 @@ && chmod +x /home/opencode/.local/share/opencode/bin/opencode \ | ||
| # Copy UI assets to standard web root (requires root) | ||
| USER root | ||
| RUN mkdir -p /var/www/opencode \ | ||
| && cp -R /tmp/opencode-repo/packages/app/dist/. /var/www/opencode/ \ | ||
| && chown -R root:root /var/www/opencode \ | ||
| && chmod 755 /var/www /var/www/opencode \ | ||
| && chmod -R a+rX /var/www/opencode | ||
| # ----------------------------------------------------------------------------- | ||
@@ -577,45 +569,2 @@ # opencode-broker Installation | ||
| # ----------------------------------------------------------------------------- | ||
| # Nginx Reverse Proxy for UI + API | ||
| # ----------------------------------------------------------------------------- | ||
| # Serve the built UI from /var/www/opencode and proxy all 3000 traffic to backend. | ||
| RUN rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf 2>/dev/null || true \ | ||
| && printf '%s\n' \ | ||
| 'server {' \ | ||
| ' listen 3000;' \ | ||
| ' server_name _;' \ | ||
| '' \ | ||
| ' location / {' \ | ||
| ' proxy_pass http://127.0.0.1:3001;' \ | ||
| ' proxy_http_version 1.1;' \ | ||
| ' proxy_set_header Upgrade $http_upgrade;' \ | ||
| ' proxy_set_header Connection "upgrade";' \ | ||
| ' proxy_set_header Host $host;' \ | ||
| ' proxy_set_header X-Real-IP $remote_addr;' \ | ||
| ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \ | ||
| ' proxy_set_header X-Forwarded-Proto $scheme;' \ | ||
| ' }' \ | ||
| '}' \ | ||
| '' \ | ||
| 'server {' \ | ||
| ' listen 3002;' \ | ||
| ' server_name _;' \ | ||
| '' \ | ||
| ' root /var/www/opencode;' \ | ||
| ' index index.html;' \ | ||
| '' \ | ||
| ' location /assets/ {' \ | ||
| ' try_files $uri =404;' \ | ||
| ' }' \ | ||
| '' \ | ||
| ' location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|ico|webp|woff2?|ttf|map)$ {' \ | ||
| ' try_files $uri =404;' \ | ||
| ' }' \ | ||
| '' \ | ||
| ' location / {' \ | ||
| ' try_files $uri $uri/ /index.html;' \ | ||
| ' }' \ | ||
| '}' \ | ||
| > /etc/nginx/conf.d/opencode.conf | ||
| # ----------------------------------------------------------------------------- | ||
| # PAM Configuration | ||
@@ -699,3 +648,3 @@ # ----------------------------------------------------------------------------- | ||
| # ----------------------------------------------------------------------------- | ||
| # Create opencode as a systemd service for Cockpit integration (backend only) | ||
| # Create opencode as a systemd service for Cockpit integration | ||
| # NOTE: Requires root privileges to write to /etc/systemd/system/ | ||
@@ -712,3 +661,3 @@ USER root | ||
| 'WorkingDirectory=/home/opencode/workspace' \ | ||
| 'ExecStart=/home/opencode/.local/share/opencode/bin/opencode --port 3001 --hostname 0.0.0.0' \ | ||
| 'ExecStart=/home/opencode/.local/share/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0' \ | ||
| 'Restart=always' \ | ||
@@ -726,40 +675,11 @@ 'RestartSec=5' \ | ||
| # Nginx service for serving UI + proxying API | ||
| RUN printf '%s\n' \ | ||
| '[Unit]' \ | ||
| 'Description=Nginx reverse proxy for opencode UI' \ | ||
| 'After=network.target opencode.service' \ | ||
| '' \ | ||
| '[Service]' \ | ||
| 'Type=simple' \ | ||
| 'ExecStart=/usr/sbin/nginx -g "daemon off;"' \ | ||
| 'ExecReload=/usr/sbin/nginx -s reload' \ | ||
| 'Restart=always' \ | ||
| 'RestartSec=5' \ | ||
| '' \ | ||
| '[Install]' \ | ||
| 'WantedBy=multi-user.target' \ | ||
| > /etc/systemd/system/opencode-nginx.service | ||
| # Enable nginx service | ||
| RUN mkdir -p /etc/systemd/system/multi-user.target.wants \ | ||
| && ln -sf /etc/systemd/system/opencode-nginx.service /etc/systemd/system/multi-user.target.wants/opencode-nginx.service | ||
| # Prevent the distro nginx service from also starting (port 3000 conflict) | ||
| RUN rm -f /etc/systemd/system/multi-user.target.wants/nginx.service \ | ||
| && ln -sf /dev/null /etc/systemd/system/nginx.service | ||
| # ----------------------------------------------------------------------------- | ||
| # opencode Configuration | ||
| # ----------------------------------------------------------------------------- | ||
| # Create opencode.jsonc config file with PAM authentication enabled and UI URL | ||
| # Create opencode.jsonc config file with PAM authentication enabled | ||
| RUN mkdir -p /home/opencode/.config/opencode \ | ||
| && printf '%s\n' \ | ||
| '{' \ | ||
| ' // Container UI served via nginx on 3002' \ | ||
| ' "auth": {' \ | ||
| ' "enabled": true' \ | ||
| ' },' \ | ||
| ' "server": {' \ | ||
| ' "uiUrl": "http://localhost:3002"' \ | ||
| ' }' \ | ||
@@ -788,5 +708,7 @@ '}' \ | ||
| 'else' \ | ||
| ' # Ensure broker socket directory exists' \ | ||
| ' install -d -m 0755 /run/opencode' \ | ||
| ' /usr/local/bin/opencode-broker &' \ | ||
| ' # Use runuser to switch to opencode user without password prompt' \ | ||
| ' runuser -u opencode -- /home/opencode/.local/share/opencode/bin/opencode --port 3001 --hostname 0.0.0.0 &' \ | ||
| ' exec /usr/sbin/nginx -g "daemon off;"' \ | ||
| ' exec runuser -u opencode -- sh -lc "cd /home/opencode/workspace && /home/opencode/.local/share/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0"' \ | ||
| 'fi' \ | ||
@@ -825,6 +747,6 @@ > /usr/local/bin/entrypoint.sh && chmod +x /usr/local/bin/entrypoint.sh | ||
| # Expose opencode ports (3000/3001/3002) and Cockpit (9090) | ||
| EXPOSE 3000 3001 3002 9090 | ||
| # Expose opencode web (3000) and Cockpit (9090) | ||
| EXPOSE 3000 9090 | ||
| # Hybrid init: entrypoint script chooses tini or systemd based on USE_SYSTEMD env | ||
| ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] |
+24
-4
@@ -56,2 +56,10 @@ //! Health check module for OpenCode service | ||
| fn format_host(bind_addr: &str) -> String { | ||
| if bind_addr.contains(':') && !bind_addr.starts_with('[') { | ||
| format!("[{bind_addr}]") | ||
| } else { | ||
| bind_addr.to_string() | ||
| } | ||
| } | ||
| /// Check health by querying OpenCode's /global/health endpoint | ||
@@ -61,4 +69,5 @@ /// | ||
| /// Returns an error for connection issues, timeouts, or non-200 responses. | ||
| pub async fn check_health(port: u16) -> Result<HealthResponse, HealthError> { | ||
| let url = format!("http://127.0.0.1:{port}/global/health"); | ||
| pub async fn check_health(bind_addr: &str, port: u16) -> Result<HealthResponse, HealthError> { | ||
| let host = format_host(bind_addr); | ||
| let url = format!("http://{host}:{port}/global/health"); | ||
@@ -100,6 +109,7 @@ let client = reqwest::Client::builder() | ||
| client: &DockerClient, | ||
| bind_addr: &str, | ||
| port: u16, | ||
| ) -> Result<ExtendedHealthResponse, HealthError> { | ||
| // Get basic health info | ||
| let health = check_health(port).await?; | ||
| let health = check_health(bind_addr, port).await?; | ||
@@ -161,3 +171,3 @@ // Get container stats | ||
| // Port 1 should always refuse connection | ||
| let result = check_health(1).await; | ||
| let result = check_health("127.0.0.1", 1).await; | ||
| assert!(result.is_err()); | ||
@@ -169,2 +179,12 @@ match result.unwrap_err() { | ||
| } | ||
| #[test] | ||
| fn format_host_wraps_ipv6() { | ||
| assert_eq!(format_host("::1"), "[::1]"); | ||
| } | ||
| #[test] | ||
| fn format_host_preserves_ipv4() { | ||
| assert_eq!(format_host("127.0.0.1"), "127.0.0.1"); | ||
| } | ||
| } |
+64
-18
@@ -180,2 +180,4 @@ //! Docker image build and pull operations | ||
| last_buildkit_vertex_id: Option<String>, | ||
| export_vertex_id: Option<String>, | ||
| export_vertex_name: Option<String>, | ||
| buildkit_logs_by_vertex_id: HashMap<String, String>, | ||
@@ -203,2 +205,4 @@ vertex_name_by_vertex_id: HashMap<String, String>, | ||
| last_buildkit_vertex_id: None, | ||
| export_vertex_id: None, | ||
| export_vertex_name: None, | ||
| buildkit_logs_by_vertex_id: HashMap::new(), | ||
@@ -260,20 +264,30 @@ vertex_name_by_vertex_id: HashMap::new(), | ||
| update_buildkit_vertex_names(&mut state.vertex_name_by_vertex_id, status); | ||
| let (vertex_id, vertex_name) = | ||
| match select_latest_buildkit_vertex(status, &state.vertex_name_by_vertex_id) { | ||
| Some((vertex_id, vertex_name)) => (vertex_id, vertex_name), | ||
| None => { | ||
| let Some(log_entry) = latest_logs.last() else { | ||
| return; | ||
| }; | ||
| let name = state | ||
| .vertex_name_by_vertex_id | ||
| .get(&log_entry.vertex_id) | ||
| .cloned() | ||
| .or_else(|| state.last_buildkit_vertex.clone()) | ||
| .unwrap_or_else(|| format_vertex_fallback_label(&log_entry.vertex_id)); | ||
| (log_entry.vertex_id.clone(), name) | ||
| } | ||
| }; | ||
| update_export_vertex_from_logs( | ||
| &latest_logs, | ||
| &state.vertex_name_by_vertex_id, | ||
| &mut state.export_vertex_id, | ||
| &mut state.export_vertex_name, | ||
| ); | ||
| let (vertex_id, vertex_name) = match select_latest_buildkit_vertex( | ||
| status, | ||
| &state.vertex_name_by_vertex_id, | ||
| state.export_vertex_id.as_deref(), | ||
| state.export_vertex_name.as_deref(), | ||
| ) { | ||
| Some((vertex_id, vertex_name)) => (vertex_id, vertex_name), | ||
| None => { | ||
| let Some(log_entry) = latest_logs.last() else { | ||
| return; | ||
| }; | ||
| let name = state | ||
| .vertex_name_by_vertex_id | ||
| .get(&log_entry.vertex_id) | ||
| .cloned() | ||
| .or_else(|| state.last_buildkit_vertex.clone()) | ||
| .unwrap_or_else(|| format_vertex_fallback_label(&log_entry.vertex_id)); | ||
| (log_entry.vertex_id.clone(), name) | ||
| } | ||
| }; | ||
| record_buildkit_logs(state, &latest_logs, &vertex_id, &vertex_name); | ||
| state.last_buildkit_vertex_id = Some(vertex_id); | ||
| state.last_buildkit_vertex_id = Some(vertex_id.clone()); | ||
| if state.last_buildkit_vertex.as_deref() != Some(&vertex_name) { | ||
@@ -285,3 +299,7 @@ state.last_buildkit_vertex = Some(vertex_name.clone()); | ||
| vertex_name | ||
| } else if let Some(log_entry) = latest_logs.last() { | ||
| } else if let Some(log_entry) = latest_logs | ||
| .iter() | ||
| .rev() | ||
| .find(|entry| entry.vertex_id == vertex_id) | ||
| { | ||
| format!("{vertex_name} ยท {}", log_entry.message) | ||
@@ -367,3 +385,13 @@ } else { | ||
| vertex_name_by_vertex_id: &HashMap<String, String>, | ||
| export_vertex_id: Option<&str>, | ||
| export_vertex_name: Option<&str>, | ||
| ) -> Option<(String, String)> { | ||
| if let Some(export_vertex_id) = export_vertex_id { | ||
| let name = export_vertex_name | ||
| .map(str::to_string) | ||
| .or_else(|| vertex_name_by_vertex_id.get(export_vertex_id).cloned()) | ||
| .unwrap_or_else(|| format_vertex_fallback_label(export_vertex_id)); | ||
| return Some((export_vertex_id.to_string(), name)); | ||
| } | ||
| let mut best_runtime: Option<(u32, String, String)> = None; | ||
@@ -431,2 +459,20 @@ let mut fallback: Option<(String, String)> = None; | ||
| fn update_export_vertex_from_logs( | ||
| latest_logs: &[BuildkitLogEntry], | ||
| vertex_name_by_vertex_id: &HashMap<String, String>, | ||
| export_vertex_id: &mut Option<String>, | ||
| export_vertex_name: &mut Option<String>, | ||
| ) { | ||
| if let Some(entry) = latest_logs | ||
| .iter() | ||
| .rev() | ||
| .find(|log| log.message.trim_start().starts_with("exporting to image")) | ||
| { | ||
| *export_vertex_id = Some(entry.vertex_id.clone()); | ||
| if let Some(name) = vertex_name_by_vertex_id.get(&entry.vertex_id) { | ||
| *export_vertex_name = Some(name.clone()); | ||
| } | ||
| } | ||
| } | ||
| fn record_buildkit_logs( | ||
@@ -433,0 +479,0 @@ state: &mut BuildLogState, |
@@ -31,7 +31,15 @@ # opencode-cloud-sandbox | ||
| ``` | ||
| docker run --rm -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -p 9090:9090 ghcr.io/prizz/opencode-cloud-sandbox:latest | ||
| docker run --rm -it -p 3000:3000 -p 9090:9090 ghcr.io/prizz/opencode-cloud-sandbox:latest | ||
| ``` | ||
| The opencode web UI is available at `http://localhost:3000`. The backend is reachable at `http://localhost:3001`, and the static UI is served at `http://localhost:3002`. Cockpit runs on `http://localhost:9090`. | ||
| The opencode web UI is available at `http://localhost:3000`. Cockpit runs on `http://localhost:9090`. | ||
| ## opencode build and serve flow | ||
| The Docker image builds opencode directly from the fork and runs the web server without nginx: | ||
| 1. `cd packages/opencode` | ||
| 2. `bun run build` to generate `packages/opencode/dist` | ||
| 3. Run the server with `./bin/opencode web` | ||
| ## Source | ||
@@ -38,0 +46,0 @@ |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
270
2.66%310120
-0.02%