@opencode-cloud/core 3.3.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "opencode-cloud-core"
3
- version = "3.3.0"
3
+ version = "4.0.1"
4
4
  edition = "2024"
5
5
  rust-version = "1.88"
6
6
  license = "MIT"
package/README.md CHANGED
@@ -8,8 +8,13 @@
8
8
  [![MSRV](https://img.shields.io/badge/MSRV-1.85-blue.svg)](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
10
 
11
+ > [!WARNING]
12
+ > This project is a work in progress and evolving rapidly. Use with caution.
13
+
11
14
  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.
12
15
 
16
+ This project uses the opencode fork at https://github.com/pRizz/opencode, which adds additional authentication and security features.
17
+
13
18
  ## Quick install (cargo)
14
19
 
15
20
  ```bash
@@ -19,12 +24,14 @@ opencode-cloud --version
19
24
 
20
25
  ## Deploy to AWS
21
26
 
22
- [![Deploy to AWS](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](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)
27
+ [![Deploy to AWS](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](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)
23
28
 
24
29
  Quick deploy provisions a private EC2 instance behind a public ALB with HTTPS.
25
30
  **A domain name is required** for ACM certificate validation.
31
+ **A Route53 hosted zone ID is required** for automated DNS validation.
26
32
 
27
- Docs: `docs/deploy/aws.md` (includes teardown steps)
33
+ Docs: `docs/deploy/aws.md` (includes teardown steps and S3 hosting setup for forks)
34
+ Credentials: `docs/deploy/aws.md#retrieving-credentials`
28
35
 
29
36
  ## Features
30
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-cloud/core",
3
- "version": "3.3.0",
3
+ "version": "4.0.1",
4
4
  "description": "Core NAPI bindings for opencode-cloud (internal package)",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -104,8 +104,6 @@ RUN --mount=type=cache,target=/var/lib/apt/lists \
104
104
  netcat-openbsd=1.226-* \
105
105
  iputils-ping=3:20240117-* \
106
106
  dnsutils=1:9.18.* \
107
- # Reverse proxy for opencode UI + API
108
- nginx=1.24.* \
109
107
  # Compression
110
108
  zip=3.0-* \
111
109
  unzip=6.0-* \
@@ -505,7 +503,7 @@ RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshr
505
503
  # This block includes:
506
504
  # - opencode build (backend binary) + app build (frontend dist)
507
505
  # - opencode-broker build
508
- # - nginx config for single-endpoint UI + API proxy
506
+ # - opencode web build + runtime
509
507
  # - PAM configuration + systemd services
510
508
  # - opencode config file
511
509
  #
@@ -517,7 +515,7 @@ RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshr
517
515
  # Clone the fork and build opencode from source (as non-root user)
518
516
  # Pin to specific commit for reproducibility
519
517
  # Build opencode from source (BuildKit cache mounts disabled for now)
520
- RUN OPENCODE_COMMIT="34db1c9b23441b3b3a125c107149f346f9fdb96e" \
518
+ RUN OPENCODE_COMMIT="9b91eb17f5ca1b0ee99cfaa0b4c87da6dbe9e784" \
521
519
  && rm -rf /tmp/opencode-repo \
522
520
  && git clone --depth 1 https://github.com/pRizz/opencode.git /tmp/opencode-repo \
523
521
  && cd /tmp/opencode-repo \
@@ -526,13 +524,15 @@ RUN OPENCODE_COMMIT="34db1c9b23441b3b3a125c107149f346f9fdb96e" \
526
524
  && curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.5" \
527
525
  && export PATH="/home/opencode/.bun/bin:${PATH}" \
528
526
  && bun install --frozen-lockfile \
529
- && bun run packages/opencode/script/build.ts --single \
530
- && cd packages/app \
531
- && bun run build \
527
+ && cd packages/opencode \
528
+ && export VITE_OPENCODE_SERVER_URL="http://localhost:3000" \
529
+ && bun run build-single-ui \
532
530
  && rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun \
533
531
  && cd /tmp/opencode-repo \
534
532
  && mkdir -p /home/opencode/.local/share/opencode/bin \
533
+ && mkdir -p /home/opencode/.local/share/opencode/ui \
535
534
  && cp /tmp/opencode-repo/packages/opencode/dist/opencode-*/bin/opencode /home/opencode/.local/share/opencode/bin/opencode \
535
+ && cp -R /tmp/opencode-repo/packages/opencode/dist/opencode-*/ui/. /home/opencode/.local/share/opencode/ui/ \
536
536
  && chown -R opencode:opencode /home/opencode/.local/share/opencode \
537
537
  && chmod +x /home/opencode/.local/share/opencode/bin/opencode \
538
538
  && /home/opencode/.local/share/opencode/bin/opencode --version
@@ -540,14 +540,6 @@ RUN OPENCODE_COMMIT="34db1c9b23441b3b3a125c107149f346f9fdb96e" \
540
540
  # Add opencode to PATH
541
541
  ENV PATH="/home/opencode/.local/share/opencode/bin:${PATH}"
542
542
 
543
- # Copy UI assets to standard web root (requires root)
544
- USER root
545
- RUN mkdir -p /var/www/opencode \
546
- && cp -R /tmp/opencode-repo/packages/app/dist/. /var/www/opencode/ \
547
- && chown -R root:root /var/www/opencode \
548
- && chmod 755 /var/www /var/www/opencode \
549
- && chmod -R a+rX /var/www/opencode
550
-
551
543
  # -----------------------------------------------------------------------------
552
544
  # opencode-broker Installation
553
545
  # -----------------------------------------------------------------------------
@@ -568,49 +560,6 @@ RUN ls -la /usr/local/bin/opencode-broker \
568
560
  && test -x /usr/local/bin/opencode-broker \
569
561
  && echo "Broker installed"
570
562
 
571
- # -----------------------------------------------------------------------------
572
- # Nginx Reverse Proxy for UI + API
573
- # -----------------------------------------------------------------------------
574
- # Serve the built UI from /var/www/opencode and proxy all 3000 traffic to backend.
575
- RUN rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf 2>/dev/null || true \
576
- && printf '%s\n' \
577
- 'server {' \
578
- ' listen 3000;' \
579
- ' server_name _;' \
580
- '' \
581
- ' location / {' \
582
- ' proxy_pass http://127.0.0.1:3001;' \
583
- ' proxy_http_version 1.1;' \
584
- ' proxy_set_header Upgrade $http_upgrade;' \
585
- ' proxy_set_header Connection "upgrade";' \
586
- ' proxy_set_header Host $host;' \
587
- ' proxy_set_header X-Real-IP $remote_addr;' \
588
- ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \
589
- ' proxy_set_header X-Forwarded-Proto $scheme;' \
590
- ' }' \
591
- '}' \
592
- '' \
593
- 'server {' \
594
- ' listen 3002;' \
595
- ' server_name _;' \
596
- '' \
597
- ' root /var/www/opencode;' \
598
- ' index index.html;' \
599
- '' \
600
- ' location /assets/ {' \
601
- ' try_files $uri =404;' \
602
- ' }' \
603
- '' \
604
- ' location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|ico|webp|woff2?|ttf|map)$ {' \
605
- ' try_files $uri =404;' \
606
- ' }' \
607
- '' \
608
- ' location / {' \
609
- ' try_files $uri $uri/ /index.html;' \
610
- ' }' \
611
- '}' \
612
- > /etc/nginx/conf.d/opencode.conf
613
-
614
563
  # -----------------------------------------------------------------------------
615
564
  # PAM Configuration
616
565
  # -----------------------------------------------------------------------------
@@ -690,7 +639,7 @@ RUN mkdir -p /home/opencode/.npm \
690
639
  # -----------------------------------------------------------------------------
691
640
  # opencode systemd Service (2026-01-22)
692
641
  # -----------------------------------------------------------------------------
693
- # Create opencode as a systemd service for Cockpit integration (backend only)
642
+ # Create opencode as a systemd service for Cockpit integration
694
643
  # NOTE: Requires root privileges to write to /etc/systemd/system/
695
644
  USER root
696
645
  RUN printf '%s\n' \
@@ -702,7 +651,7 @@ RUN printf '%s\n' \
702
651
  'Type=simple' \
703
652
  'User=opencode' \
704
653
  'WorkingDirectory=/home/opencode/workspace' \
705
- 'ExecStart=/home/opencode/.local/share/opencode/bin/opencode --port 3001 --hostname 0.0.0.0' \
654
+ 'ExecStart=/home/opencode/.local/share/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0' \
706
655
  'Restart=always' \
707
656
  'RestartSec=5' \
708
657
  'Environment=PATH=/home/opencode/.local/share/opencode/bin:/home/opencode/.local/bin:/home/opencode/.cargo/bin:/home/opencode/.local/share/mise/shims:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \
@@ -715,44 +664,15 @@ RUN printf '%s\n' \
715
664
  RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
716
665
  && ln -sf /etc/systemd/system/opencode.service /etc/systemd/system/multi-user.target.wants/opencode.service
717
666
 
718
- # Nginx service for serving UI + proxying API
719
- RUN printf '%s\n' \
720
- '[Unit]' \
721
- 'Description=Nginx reverse proxy for opencode UI' \
722
- 'After=network.target opencode.service' \
723
- '' \
724
- '[Service]' \
725
- 'Type=simple' \
726
- 'ExecStart=/usr/sbin/nginx -g "daemon off;"' \
727
- 'ExecReload=/usr/sbin/nginx -s reload' \
728
- 'Restart=always' \
729
- 'RestartSec=5' \
730
- '' \
731
- '[Install]' \
732
- 'WantedBy=multi-user.target' \
733
- > /etc/systemd/system/opencode-nginx.service
734
-
735
- # Enable nginx service
736
- RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
737
- && ln -sf /etc/systemd/system/opencode-nginx.service /etc/systemd/system/multi-user.target.wants/opencode-nginx.service
738
-
739
- # Prevent the distro nginx service from also starting (port 3000 conflict)
740
- RUN rm -f /etc/systemd/system/multi-user.target.wants/nginx.service \
741
- && ln -sf /dev/null /etc/systemd/system/nginx.service
742
-
743
667
  # -----------------------------------------------------------------------------
744
668
  # opencode Configuration
745
669
  # -----------------------------------------------------------------------------
746
- # Create opencode.jsonc config file with PAM authentication enabled and UI URL
670
+ # Create opencode.jsonc config file with PAM authentication enabled
747
671
  RUN mkdir -p /home/opencode/.config/opencode \
748
672
  && printf '%s\n' \
749
673
  '{' \
750
- ' // Container UI served via nginx on 3002' \
751
674
  ' "auth": {' \
752
675
  ' "enabled": true' \
753
- ' },' \
754
- ' "server": {' \
755
- ' "uiUrl": "http://localhost:3002"' \
756
676
  ' }' \
757
677
  '}' \
758
678
  > /home/opencode/.config/opencode/opencode.jsonc \
@@ -776,9 +696,11 @@ RUN printf '%s\n' \
776
696
  'if [ "${USE_SYSTEMD}" = "1" ]; then' \
777
697
  ' exec /sbin/init' \
778
698
  'else' \
699
+ ' # Ensure broker socket directory exists' \
700
+ ' install -d -m 0755 /run/opencode' \
701
+ ' /usr/local/bin/opencode-broker &' \
779
702
  ' # Use runuser to switch to opencode user without password prompt' \
780
- ' runuser -u opencode -- /home/opencode/.local/share/opencode/bin/opencode --port 3001 --hostname 0.0.0.0 &' \
781
- ' exec /usr/sbin/nginx -g "daemon off;"' \
703
+ ' 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"' \
782
704
  'fi' \
783
705
  > /usr/local/bin/entrypoint.sh && chmod +x /usr/local/bin/entrypoint.sh
784
706
 
@@ -812,8 +734,8 @@ RUN echo "${OPENCODE_CLOUD_VERSION}" > /etc/opencode-cloud-version
812
734
  # -----------------------------------------------------------------------------
813
735
  WORKDIR /home/opencode/workspace
814
736
 
815
- # Expose opencode ports (3000/3001/3002) and Cockpit (9090)
816
- EXPOSE 3000 3001 3002 9090
737
+ # Expose opencode web (3000) and Cockpit (9090)
738
+ EXPOSE 3000 9090
817
739
 
818
740
  # Hybrid init: entrypoint script chooses tini or systemd based on USE_SYSTEMD env
819
741
  ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
@@ -28,10 +28,18 @@ docker pull ghcr.io/prizz/opencode-cloud-sandbox:latest
28
28
  Run the container:
29
29
 
30
30
  ```
31
- docker run --rm -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -p 9090:9090 ghcr.io/prizz/opencode-cloud-sandbox:latest
31
+ docker run --rm -it -p 3000:3000 -p 9090:9090 ghcr.io/prizz/opencode-cloud-sandbox:latest
32
32
  ```
33
33
 
34
- 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`.
34
+ The opencode web UI is available at `http://localhost:3000`. Cockpit runs on `http://localhost:9090`.
35
+
36
+ ## opencode build and serve flow
37
+
38
+ The Docker image builds opencode directly from the fork and runs the web server without nginx:
39
+
40
+ 1. `cd packages/opencode`
41
+ 2. `bun run build` to generate `packages/opencode/dist`
42
+ 3. Run the server with `./bin/opencode web`
35
43
 
36
44
  ## Source
37
45
 
@@ -53,12 +53,21 @@ pub enum HealthError {
53
53
  Timeout,
54
54
  }
55
55
 
56
+ fn format_host(bind_addr: &str) -> String {
57
+ if bind_addr.contains(':') && !bind_addr.starts_with('[') {
58
+ format!("[{bind_addr}]")
59
+ } else {
60
+ bind_addr.to_string()
61
+ }
62
+ }
63
+
56
64
  /// Check health by querying OpenCode's /global/health endpoint
57
65
  ///
58
66
  /// Returns the health response on success (HTTP 200).
59
67
  /// Returns an error for connection issues, timeouts, or non-200 responses.
60
- pub async fn check_health(port: u16) -> Result<HealthResponse, HealthError> {
61
- let url = format!("http://127.0.0.1:{port}/global/health");
68
+ pub async fn check_health(bind_addr: &str, port: u16) -> Result<HealthResponse, HealthError> {
69
+ let host = format_host(bind_addr);
70
+ let url = format!("http://{host}:{port}/global/health");
62
71
 
63
72
  let client = reqwest::Client::builder()
64
73
  .timeout(Duration::from_secs(5))
@@ -95,10 +104,11 @@ pub async fn check_health(port: u16) -> Result<HealthResponse, HealthError> {
95
104
  /// If container stats fail, still returns response with container_state = "unknown".
96
105
  pub async fn check_health_extended(
97
106
  client: &DockerClient,
107
+ bind_addr: &str,
98
108
  port: u16,
99
109
  ) -> Result<ExtendedHealthResponse, HealthError> {
100
110
  // Get basic health info
101
- let health = check_health(port).await?;
111
+ let health = check_health(bind_addr, port).await?;
102
112
 
103
113
  // Get container stats
104
114
  let container_name = super::CONTAINER_NAME;
@@ -155,11 +165,21 @@ mod tests {
155
165
  #[tokio::test]
156
166
  async fn test_health_check_connection_refused() {
157
167
  // Port 1 should always refuse connection
158
- let result = check_health(1).await;
168
+ let result = check_health("127.0.0.1", 1).await;
159
169
  assert!(result.is_err());
160
170
  match result.unwrap_err() {
161
171
  HealthError::ConnectionRefused => {}
162
172
  other => panic!("Expected ConnectionRefused, got: {other:?}"),
163
173
  }
164
174
  }
175
+
176
+ #[test]
177
+ fn format_host_wraps_ipv6() {
178
+ assert_eq!(format_host("::1"), "[::1]");
179
+ }
180
+
181
+ #[test]
182
+ fn format_host_preserves_ipv4() {
183
+ assert_eq!(format_host("127.0.0.1"), "127.0.0.1");
184
+ }
165
185
  }
@@ -177,6 +177,8 @@ struct BuildLogState {
177
177
  error_log_buffer_size: usize,
178
178
  last_buildkit_vertex: Option<String>,
179
179
  last_buildkit_vertex_id: Option<String>,
180
+ export_vertex_id: Option<String>,
181
+ export_vertex_name: Option<String>,
180
182
  buildkit_logs_by_vertex_id: HashMap<String, String>,
181
183
  vertex_name_by_vertex_id: HashMap<String, String>,
182
184
  }
@@ -199,6 +201,8 @@ impl BuildLogState {
199
201
  error_log_buffer_size,
200
202
  last_buildkit_vertex: None,
201
203
  last_buildkit_vertex_id: None,
204
+ export_vertex_id: None,
205
+ export_vertex_name: None,
202
206
  buildkit_logs_by_vertex_id: HashMap::new(),
203
207
  vertex_name_by_vertex_id: HashMap::new(),
204
208
  }
@@ -255,31 +259,45 @@ fn handle_buildkit_status(
255
259
  ) {
256
260
  let latest_logs = append_buildkit_logs(&mut state.buildkit_logs_by_vertex_id, status);
257
261
  update_buildkit_vertex_names(&mut state.vertex_name_by_vertex_id, status);
258
- let (vertex_id, vertex_name) =
259
- match select_latest_buildkit_vertex(status, &state.vertex_name_by_vertex_id) {
260
- Some((vertex_id, vertex_name)) => (vertex_id, vertex_name),
261
- None => {
262
- let Some(log_entry) = latest_logs.last() else {
263
- return;
264
- };
265
- let name = state
266
- .vertex_name_by_vertex_id
267
- .get(&log_entry.vertex_id)
268
- .cloned()
269
- .or_else(|| state.last_buildkit_vertex.clone())
270
- .unwrap_or_else(|| format_vertex_fallback_label(&log_entry.vertex_id));
271
- (log_entry.vertex_id.clone(), name)
272
- }
273
- };
262
+ update_export_vertex_from_logs(
263
+ &latest_logs,
264
+ &state.vertex_name_by_vertex_id,
265
+ &mut state.export_vertex_id,
266
+ &mut state.export_vertex_name,
267
+ );
268
+ let (vertex_id, vertex_name) = match select_latest_buildkit_vertex(
269
+ status,
270
+ &state.vertex_name_by_vertex_id,
271
+ state.export_vertex_id.as_deref(),
272
+ state.export_vertex_name.as_deref(),
273
+ ) {
274
+ Some((vertex_id, vertex_name)) => (vertex_id, vertex_name),
275
+ None => {
276
+ let Some(log_entry) = latest_logs.last() else {
277
+ return;
278
+ };
279
+ let name = state
280
+ .vertex_name_by_vertex_id
281
+ .get(&log_entry.vertex_id)
282
+ .cloned()
283
+ .or_else(|| state.last_buildkit_vertex.clone())
284
+ .unwrap_or_else(|| format_vertex_fallback_label(&log_entry.vertex_id));
285
+ (log_entry.vertex_id.clone(), name)
286
+ }
287
+ };
274
288
  record_buildkit_logs(state, &latest_logs, &vertex_id, &vertex_name);
275
- state.last_buildkit_vertex_id = Some(vertex_id);
289
+ state.last_buildkit_vertex_id = Some(vertex_id.clone());
276
290
  if state.last_buildkit_vertex.as_deref() != Some(&vertex_name) {
277
291
  state.last_buildkit_vertex = Some(vertex_name.clone());
278
292
  }
279
293
 
280
294
  let message = if progress.is_plain_output() {
281
295
  vertex_name
282
- } else if let Some(log_entry) = latest_logs.last() {
296
+ } else if let Some(log_entry) = latest_logs
297
+ .iter()
298
+ .rev()
299
+ .find(|entry| entry.vertex_id == vertex_id)
300
+ {
283
301
  format!("{vertex_name} · {}", log_entry.message)
284
302
  } else {
285
303
  vertex_name
@@ -360,7 +378,17 @@ fn update_buildkit_vertex_names(
360
378
  fn select_latest_buildkit_vertex(
361
379
  status: &BuildkitStatusResponse,
362
380
  vertex_name_by_vertex_id: &HashMap<String, String>,
381
+ export_vertex_id: Option<&str>,
382
+ export_vertex_name: Option<&str>,
363
383
  ) -> Option<(String, String)> {
384
+ if let Some(export_vertex_id) = export_vertex_id {
385
+ let name = export_vertex_name
386
+ .map(str::to_string)
387
+ .or_else(|| vertex_name_by_vertex_id.get(export_vertex_id).cloned())
388
+ .unwrap_or_else(|| format_vertex_fallback_label(export_vertex_id));
389
+ return Some((export_vertex_id.to_string(), name));
390
+ }
391
+
364
392
  let mut best_runtime: Option<(u32, String, String)> = None;
365
393
  let mut fallback: Option<(String, String)> = None;
366
394
 
@@ -423,6 +451,24 @@ fn format_vertex_fallback_label(vertex_id: &str) -> String {
423
451
  format!("vertex {short}")
424
452
  }
425
453
 
454
+ fn update_export_vertex_from_logs(
455
+ latest_logs: &[BuildkitLogEntry],
456
+ vertex_name_by_vertex_id: &HashMap<String, String>,
457
+ export_vertex_id: &mut Option<String>,
458
+ export_vertex_name: &mut Option<String>,
459
+ ) {
460
+ if let Some(entry) = latest_logs
461
+ .iter()
462
+ .rev()
463
+ .find(|log| log.message.trim_start().starts_with("exporting to image"))
464
+ {
465
+ *export_vertex_id = Some(entry.vertex_id.clone());
466
+ if let Some(name) = vertex_name_by_vertex_id.get(&entry.vertex_id) {
467
+ *export_vertex_name = Some(name.clone());
468
+ }
469
+ }
470
+ }
471
+
426
472
  fn record_buildkit_logs(
427
473
  state: &mut BuildLogState,
428
474
  latest_logs: &[BuildkitLogEntry],