@opencode-cloud/core 15.2.0 → 17.0.0

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 = "15.2.0"
3
+ version = "17.0.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.89"
6
6
  license = "MIT"
package/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  [![CI](https://github.com/pRizz/opencode-cloud/actions/workflows/ci.yml/badge.svg)](https://github.com/pRizz/opencode-cloud/actions/workflows/ci.yml)
4
4
  [![Mirror](https://img.shields.io/badge/mirror-gitea-blue?logo=gitea)](https://gitea.com/pRizz/opencode-cloud)
5
5
  [![crates.io](https://img.shields.io/crates/v/opencode-cloud.svg)](https://crates.io/crates/opencode-cloud)
6
- [![GHCR](https://img.shields.io/badge/ghcr.io-sandbox-blue?logo=github)](https://github.com/pRizz/opencode-cloud/pkgs/container/opencode-cloud-sandbox)
7
6
  [![Docker Hub](https://img.shields.io/docker/v/prizz/opencode-cloud-sandbox?label=docker&sort=semver)](https://hub.docker.com/r/prizz/opencode-cloud-sandbox)
8
7
  [![Docker Pulls](https://img.shields.io/docker/pulls/prizz/opencode-cloud-sandbox)](https://hub.docker.com/r/prizz/opencode-cloud-sandbox)
8
+ [![GHCR](https://img.shields.io/badge/ghcr.io-sandbox-blue?logo=github)](https://github.com/pRizz/opencode-cloud/pkgs/container/opencode-cloud-sandbox)
9
9
  [![docs.rs](https://docs.rs/opencode-cloud/badge.svg)](https://docs.rs/opencode-cloud)
10
10
  [![MSRV](https://img.shields.io/badge/MSRV-1.85-blue.svg)](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)
11
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -51,6 +51,16 @@ Quick deploy provisions a private EC2 instance behind a public ALB with HTTPS.
51
51
  Docs: `docs/deploy/aws.md` (includes teardown steps and S3 hosting setup for forks)
52
52
  Credentials: `docs/deploy/aws.md#retrieving-credentials`
53
53
 
54
+ ## Deploy to DigitalOcean
55
+
56
+ [![Deploy on DigitalOcean](https://img.shields.io/badge/Deploy-DigitalOcean-0080FF?logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/opencode-cloud)
57
+
58
+ Marketplace one-click deploy provisions a Droplet that bootstraps opencode-cloud
59
+ on first boot (listing pending).
60
+
61
+ Manual Droplet setup: `docs/deploy/digitalocean-droplet.md`
62
+ Marketplace docs: `docs/deploy/digitalocean-marketplace.md`
63
+
54
64
  ## Features
55
65
 
56
66
  - **Sandboxed execution** - opencode runs inside a Docker container, isolated from your host system
@@ -77,12 +87,12 @@ The sandbox container image is named **`opencode-cloud-sandbox`** (not `opencode
77
87
 
78
88
  **Why use the CLI?** It configures volumes, ports, and upgrades safely, so you don’t have to manage `docker run` flags or image updates yourself.
79
89
 
80
- The image is published to both registries:
90
+ The image is published to both registries (Docker Hub is the primary distribution):
81
91
 
82
92
  | Registry | Image |
83
93
  |----------|-------|
84
- | GitHub Container Registry | [`ghcr.io/prizz/opencode-cloud-sandbox`](https://github.com/pRizz/opencode-cloud/pkgs/container/opencode-cloud-sandbox) |
85
94
  | Docker Hub | [`prizz/opencode-cloud-sandbox`](https://hub.docker.com/r/prizz/opencode-cloud-sandbox) |
95
+ | GitHub Container Registry | [`ghcr.io/prizz/opencode-cloud-sandbox`](https://github.com/pRizz/opencode-cloud/pkgs/container/opencode-cloud-sandbox) |
86
96
 
87
97
  Pull commands:
88
98
 
@@ -182,7 +192,7 @@ occ start --port 8080
182
192
  # Start and open browser
183
193
  occ start --open
184
194
 
185
- # Check service status
195
+ # Check service status (includes broker health: Healthy/Degraded/Unhealthy)
186
196
  occ status
187
197
 
188
198
  # View logs
@@ -194,7 +204,7 @@ occ logs -f
194
204
  # View opencode-broker logs (systemd/journald required)
195
205
  occ logs --broker
196
206
 
197
- # Dump opencode-broker logs (no follow)
207
+ # Troubleshoot broker health issues reported by `occ status`
198
208
  occ logs --broker --no-follow
199
209
 
200
210
  # Note: Broker logs require systemd/journald. This is enabled by default on supported Linux
@@ -238,6 +248,26 @@ occ mount clean --purge --force
238
248
  # Factory reset host (container, volumes, mounts, config/data)
239
249
  occ reset host --force
240
250
 
251
+ ### Container Mode
252
+
253
+ When `occ` runs inside the opencode container, it will auto-detect this and switch to **container runtime**.
254
+ Override if needed:
255
+
256
+ ```bash
257
+ occ --runtime host <command>
258
+ OPENCODE_RUNTIME=host occ <command>
259
+ ```
260
+
261
+ Supported commands in container runtime:
262
+ - `occ status`
263
+ - `occ logs`
264
+ - `occ user`
265
+ - `occ update opencode`
266
+
267
+ Notes:
268
+ - Host/Docker lifecycle commands are disabled in container runtime.
269
+ - `occ logs` and `occ update opencode` require systemd inside the container. If systemd is not available, run those commands from the host instead.
270
+
241
271
  ### Webapp-triggered update (command file)
242
272
 
243
273
  When running in foreground mode (for example via `occ install`, which uses `occ start --no-daemon`),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode-cloud/core",
3
- "version": "15.2.0",
3
+ "version": "17.0.0",
4
4
  "description": "Core NAPI bindings for opencode-cloud (internal package)",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,6 +1,12 @@
1
1
  # =============================================================================
2
2
  # opencode-cloud Container Image
3
3
  # =============================================================================
4
+ # IMPORTANT:
5
+ # Keep scripts/service/config assets in `packages/core/src/docker/files/`
6
+ # and COPY them into the image instead of embedding large inline shell blocks.
7
+ # When adding files, also update `packages/core/src/docker/image.rs`
8
+ # (`create_build_context`) so CLI-driven Docker builds include them.
9
+ #
4
10
  # A comprehensive development environment for AI-assisted coding with opencode.
5
11
  #
6
12
  # Features:
@@ -452,61 +458,12 @@ RUN git config --global init.defaultBranch main \
452
458
  && git config --global diff.colorMoved default
453
459
 
454
460
  # Starship configuration (minimal, fast prompt)
455
- RUN mkdir -p /home/opencode/.config \
456
- && printf '%s\n' \
457
- '# Minimal starship config for fast prompt' \
458
- 'format = """' \
459
- '$directory\' \
460
- '$git_branch\' \
461
- '$git_status\' \
462
- '$character"""' \
463
- '' \
464
- '[directory]' \
465
- 'truncation_length = 3' \
466
- 'truncate_to_repo = true' \
467
- '' \
468
- '[git_branch]' \
469
- 'format = "[$branch]($style) "' \
470
- 'style = "bold purple"' \
471
- '' \
472
- '[git_status]' \
473
- 'format = '"'"'([$all_status$ahead_behind]($style) )'"'"'' \
474
- '' \
475
- '[character]' \
476
- 'success_symbol = "[>](bold green)"' \
477
- 'error_symbol = "[>](bold red)"' \
478
- > /home/opencode/.config/starship.toml
461
+ COPY --chown=opencode:opencode packages/core/src/docker/files/starship.toml /home/opencode/.config/starship.toml
479
462
 
480
463
  # Shell aliases
481
- RUN printf '%s\n' \
482
- '' \
483
- '# Modern CLI aliases' \
484
- 'alias ls="eza --icons"' \
485
- 'alias ll="eza -l --icons"' \
486
- 'alias la="eza -la --icons"' \
487
- 'alias lt="eza --tree --icons"' \
488
- 'alias grep="rg"' \
489
- 'alias top="btop"' \
490
- '' \
491
- '# Git aliases' \
492
- 'alias g="git"' \
493
- 'alias gs="git status"' \
494
- 'alias gd="git diff"' \
495
- 'alias gc="git commit"' \
496
- 'alias gp="git push"' \
497
- 'alias gl="git pull"' \
498
- 'alias gco="git checkout"' \
499
- 'alias gb="git branch"' \
500
- 'alias lg="lazygit"' \
501
- '' \
502
- '# Docker aliases (for Docker-in-Docker)' \
503
- 'alias d="docker"' \
504
- 'alias dc="docker compose"' \
505
- '' \
506
- >> /home/opencode/.bashrc
507
-
508
- # Set up pipx path
509
- RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.bashrc
464
+ COPY --chown=opencode:opencode packages/core/src/docker/files/bashrc.extra /home/opencode/.bashrc.extra
465
+ RUN cat /home/opencode/.bashrc.extra >> /home/opencode/.bashrc \
466
+ && rm /home/opencode/.bashrc.extra
510
467
 
511
468
  # -----------------------------------------------------------------------------
512
469
  # Stage 2: opencode build
@@ -535,7 +492,7 @@ USER opencode
535
492
  # commit on the main branch of https://github.com/pRizz/opencode.
536
493
  # Update it by running: ./scripts/update-opencode-commit.sh
537
494
  # Build opencode from source (BuildKit cache mounts disabled for now)
538
- RUN OPENCODE_COMMIT="d61147bea10a1bc7ba93c6bfa0b7d8fe55a561b5" \
495
+ RUN OPENCODE_COMMIT="41731edead75a52aceac0b48c22474bdeafc746c" \
539
496
  && rm -rf /tmp/opencode-repo \
540
497
  && git clone --depth 1 https://github.com/pRizz/opencode.git /tmp/opencode-repo \
541
498
  && cd /tmp/opencode-repo \
@@ -575,18 +532,8 @@ ENV PATH="/opt/opencode/bin:${PATH}"
575
532
  # This allows opencode to authenticate users via PAM (same users as Cockpit)
576
533
  # NOTE: Requires root privileges to write to /etc/pam.d/
577
534
  USER root
578
- RUN printf '%s\n' \
579
- '# PAM configuration for OpenCode authentication' \
580
- '# Install to /etc/pam.d/opencode' \
581
- '' \
582
- '# Standard UNIX authentication' \
583
- 'auth required pam_unix.so' \
584
- 'account required pam_unix.so' \
585
- '' \
586
- '# Optional: Enable TOTP 2FA (uncomment when pam_google_authenticator is installed)' \
587
- '# auth required pam_google_authenticator.so' \
588
- > /etc/pam.d/opencode \
589
- && chmod 644 /etc/pam.d/opencode
535
+ COPY packages/core/src/docker/files/pam/opencode /etc/pam.d/opencode
536
+ RUN chmod 644 /etc/pam.d/opencode
590
537
 
591
538
  # Verify PAM config file exists
592
539
  RUN ls -la /etc/pam.d/opencode && cat /etc/pam.d/opencode
@@ -596,38 +543,7 @@ RUN ls -la /etc/pam.d/opencode && cat /etc/pam.d/opencode
596
543
  # -----------------------------------------------------------------------------
597
544
  # Create opencode-broker service for PAM authentication
598
545
  # NOTE: Requires root privileges to write to /etc/systemd/system/
599
- RUN printf '%s\n' \
600
- '[Unit]' \
601
- 'Description=OpenCode Authentication Broker' \
602
- 'Documentation=https://github.com/pRizz/opencode' \
603
- 'After=network.target' \
604
- '' \
605
- '[Service]' \
606
- 'Type=notify' \
607
- 'ExecStart=/usr/local/bin/opencode-broker' \
608
- 'ExecReload=/bin/kill -HUP $MAINPID' \
609
- 'Restart=always' \
610
- 'RestartSec=5' \
611
- '' \
612
- '# Security hardening' \
613
- 'NoNewPrivileges=false' \
614
- 'ProtectSystem=strict' \
615
- 'ProtectHome=read-only' \
616
- 'PrivateTmp=true' \
617
- 'ReadWritePaths=/run/opencode' \
618
- '' \
619
- '# Socket directory' \
620
- 'RuntimeDirectory=opencode' \
621
- 'RuntimeDirectoryMode=0755' \
622
- '' \
623
- '# Logging' \
624
- 'StandardOutput=journal' \
625
- 'StandardError=journal' \
626
- 'SyslogIdentifier=opencode-broker' \
627
- '' \
628
- '[Install]' \
629
- 'WantedBy=multi-user.target' \
630
- > /etc/systemd/system/opencode-broker.service
546
+ COPY packages/core/src/docker/files/opencode-broker.service /etc/systemd/system/opencode-broker.service
631
547
 
632
548
  # Enable opencode-broker service
633
549
  RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
@@ -638,23 +554,7 @@ RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
638
554
  # -----------------------------------------------------------------------------
639
555
  # Create opencode as a systemd service for Cockpit integration
640
556
  # NOTE: Requires root privileges to write to /etc/systemd/system/
641
- RUN printf '%s\n' \
642
- '[Unit]' \
643
- 'Description=opencode Web Interface' \
644
- 'After=network.target opencode-broker.service' \
645
- '' \
646
- '[Service]' \
647
- 'Type=simple' \
648
- 'User=opencode' \
649
- 'WorkingDirectory=/home/opencode/workspace' \
650
- 'ExecStart=/opt/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0' \
651
- 'Restart=always' \
652
- 'RestartSec=5' \
653
- 'Environment=PATH=/opt/opencode/bin:/home/opencode/.local/bin:/home/opencode/.cargo/bin:/home/opencode/.local/share/mise/shims:/home/opencode/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \
654
- '' \
655
- '[Install]' \
656
- 'WantedBy=multi-user.target' \
657
- > /etc/systemd/system/opencode.service
557
+ COPY packages/core/src/docker/files/opencode.service /etc/systemd/system/opencode.service
658
558
 
659
559
  # Enable opencode service to start at boot (manual symlink since systemctl doesn't work during build)
660
560
  RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
@@ -664,15 +564,9 @@ RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
664
564
  # opencode Configuration
665
565
  # -----------------------------------------------------------------------------
666
566
  # Create opencode.jsonc config file with PAM authentication enabled
667
- RUN mkdir -p /home/opencode/.config/opencode \
668
- && printf '%s\n' \
669
- '{' \
670
- ' "auth": {' \
671
- ' "enabled": true' \
672
- ' }' \
673
- '}' \
674
- > /home/opencode/.config/opencode/opencode.jsonc \
675
- && chown -R opencode:opencode /home/opencode/.config/opencode \
567
+ RUN mkdir -p /home/opencode/.config/opencode
568
+ COPY --chown=opencode:opencode packages/core/src/docker/files/opencode.jsonc /home/opencode/.config/opencode/opencode.jsonc
569
+ RUN chown -R opencode:opencode /home/opencode/.config/opencode \
676
570
  && chmod 644 /home/opencode/.config/opencode/opencode.jsonc
677
571
 
678
572
  # Verify config file exists
@@ -684,29 +578,23 @@ RUN ls -la /home/opencode/.config/opencode/opencode.jsonc && cat /home/opencode/
684
578
  # Supports both tini (default, works everywhere) and systemd (for Cockpit on Linux)
685
579
  # Set USE_SYSTEMD=1 environment variable to use systemd init
686
580
  # Note: Entrypoint runs as root to support both modes; tini mode drops to opencode user
687
- RUN printf '%s\n' \
688
- '#!/bin/bash' \
689
- 'if [ "${USE_SYSTEMD}" = "1" ]; then' \
690
- ' exec /sbin/init' \
691
- 'else' \
692
- ' # Ensure broker socket directory exists' \
693
- ' install -d -m 0755 /run/opencode' \
694
- ' /usr/local/bin/opencode-broker &' \
695
- ' # Use runuser to switch to opencode user without password prompt' \
696
- ' exec runuser -u opencode -- sh -lc "cd /home/opencode/workspace && /opt/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0"' \
697
- 'fi' \
698
- > /usr/local/bin/entrypoint.sh && chmod +x /usr/local/bin/entrypoint.sh
581
+ COPY packages/core/src/docker/files/entrypoint.sh /usr/local/bin/entrypoint.sh
582
+ RUN chmod +x /usr/local/bin/entrypoint.sh
699
583
 
700
584
  # Note: Don't set USER here - entrypoint needs root to use runuser
701
585
  # The tini mode drops privileges to opencode user via runuser
702
586
 
587
+ # Healthcheck script asset
588
+ COPY packages/core/src/docker/files/healthcheck.sh /usr/local/bin/healthcheck.sh
589
+ RUN chmod +x /usr/local/bin/healthcheck.sh
590
+
703
591
  # -----------------------------------------------------------------------------
704
592
  # Health Check
705
593
  # -----------------------------------------------------------------------------
706
- # Check that opencode main page responds
594
+ # Check broker readiness (process + socket) and opencode web response
707
595
  # Works for both tini and systemd modes
708
596
  HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
709
- CMD curl -f -H "Accept: text/html" http://localhost:3000/ || exit 1
597
+ CMD ["/usr/local/bin/healthcheck.sh"]
710
598
 
711
599
  # -----------------------------------------------------------------------------
712
600
  # opencode Artifacts
@@ -24,17 +24,25 @@ https://github.com/pRizz/opencode-cloud (mirror: https://gitea.com/pRizz/opencod
24
24
  Pull the image:
25
25
 
26
26
  ```
27
- docker pull ghcr.io/prizz/opencode-cloud-sandbox:latest
27
+ docker pull prizz/opencode-cloud-sandbox:latest
28
28
  ```
29
29
 
30
30
  Run the container:
31
31
 
32
32
  ```
33
- docker run --rm -it -p 3000:3000 ghcr.io/prizz/opencode-cloud-sandbox:latest
33
+ docker run --rm -it -p 3000:3000 prizz/opencode-cloud-sandbox:latest
34
34
  ```
35
35
 
36
36
  The opencode web UI is available at `http://localhost:3000`.
37
37
 
38
+ ## App Platform
39
+
40
+ - Set `http_port` to `3000` or provide `PORT`/`OPENCODE_PORT` so the health check hits the right port.
41
+ - App Platform storage is ephemeral. Workspace, config, and PAM users reset on redeploy unless you add external storage.
42
+ - Logs are visible in the App Platform UI without extra setup.
43
+ - Provide `OPENCODE_BOOTSTRAP_USER` with either `OPENCODE_BOOTSTRAP_PASSWORD` or `OPENCODE_BOOTSTRAP_PASSWORD_HASH` for first-boot access.
44
+ - App Platform supports Linux/AMD64 images and favors smaller image sizes.
45
+
38
46
  ## Install the opencode-cloud CLI
39
47
 
40
48
  Cargo:
@@ -0,0 +1,4 @@
1
+ This directory contains files copied into the Docker image via `Dockerfile` `COPY` lines.
2
+ When adding new files here, update the minimal build context in
3
+ `packages/core/src/docker/image.rs` (see `create_build_context` and its helper)
4
+ so `occ`/CLI builds include the new assets.
@@ -0,0 +1,25 @@
1
+
2
+ # Modern CLI aliases
3
+ alias ls="eza --icons"
4
+ alias ll="eza -l --icons"
5
+ alias la="eza -la --icons"
6
+ alias lt="eza --tree --icons"
7
+ alias grep="rg"
8
+ alias top="btop"
9
+
10
+ # Git aliases
11
+ alias g="git"
12
+ alias gs="git status"
13
+ alias gd="git diff"
14
+ alias gc="git commit"
15
+ alias gp="git push"
16
+ alias gl="git pull"
17
+ alias gco="git checkout"
18
+ alias gb="git branch"
19
+ alias lg="lazygit"
20
+
21
+ # Docker aliases (for Docker-in-Docker)
22
+ alias d="docker"
23
+ alias dc="docker compose"
24
+
25
+ export PATH="/home/opencode/.local/bin:$PATH"
@@ -0,0 +1,199 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ log() {
5
+ echo "[opencode-cloud] $*"
6
+ }
7
+
8
+ read_opencode_cloud_version() {
9
+ local version_file="/etc/opencode-cloud-version"
10
+ local version
11
+
12
+ if [ -r "${version_file}" ]; then
13
+ version="$(head -n 1 "${version_file}" 2>/dev/null | tr -d "\r\n")"
14
+ else
15
+ version=""
16
+ fi
17
+
18
+ if [ -z "${version}" ]; then
19
+ printf "dev"
20
+ else
21
+ printf "%s" "${version}"
22
+ fi
23
+ }
24
+
25
+ print_welcome_banner() {
26
+ local version
27
+ version="$(read_opencode_cloud_version)"
28
+
29
+ log "----------------------------------------------------------------------"
30
+ log "Welcome to opencode-cloud-sandbox"
31
+ log "You are running opencode-cloud v${version}"
32
+ log "For questions, problems, and feature requests, file an issue:"
33
+ log " https://github.com/pRizz/opencode-cloud/issues"
34
+ log "opencode-cloud runs opencode in a Docker sandbox; use occ/opencode-cloud CLI to manage users, mounts, and updates."
35
+ log "Quick start: occ user add <username> | occ status | occ logs -f"
36
+ log "Docs: https://github.com/pRizz/opencode-cloud#readme"
37
+ log "----------------------------------------------------------------------"
38
+ }
39
+
40
+ OPENCODE_PORT="${OPENCODE_PORT:-${PORT:-3000}}"
41
+ OPENCODE_HOST="${OPENCODE_HOST:-0.0.0.0}"
42
+ export OPENCODE_PORT OPENCODE_HOST
43
+
44
+ print_welcome_banner
45
+
46
+ detect_droplet() {
47
+ local hint="${OPENCODE_CLOUD_ENV:-}"
48
+ if [ -n "${hint}" ]; then
49
+ hint="$(printf "%s" "${hint}" | tr "[:upper:]" "[:lower:]")"
50
+ if [[ "${hint}" == *digitalocean* || "${hint}" == *droplet* ]]; then
51
+ return 0
52
+ fi
53
+ fi
54
+ curl -fsS --connect-timeout 1 --max-time 1 http://169.254.169.254/metadata/v1/id >/dev/null 2>&1
55
+ }
56
+
57
+ collect_non_persistent_paths() {
58
+ local -a paths=(
59
+ "/home/opencode/workspace"
60
+ "/home/opencode/.local/share/opencode"
61
+ "/home/opencode/.local/state/opencode"
62
+ "/home/opencode/.config/opencode"
63
+ "/var/lib/opencode-users"
64
+ )
65
+ local -a non_persistent=()
66
+ local fs_type
67
+ for path in "${paths[@]}"; do
68
+ fs_type="$(stat -f -c %T "${path}" 2>/dev/null || true)"
69
+ case "${fs_type}" in
70
+ ""|overlay|overlayfs|tmpfs|ramfs|squashfs)
71
+ non_persistent+=("${path}")
72
+ ;;
73
+ esac
74
+ done
75
+ if [ ${#non_persistent[@]} -eq 0 ]; then
76
+ return 1
77
+ fi
78
+ printf "%s\n" "${non_persistent[@]}"
79
+ return 0
80
+ }
81
+
82
+ non_persistent_paths="$(collect_non_persistent_paths || true)"
83
+ if [ -n "${non_persistent_paths}" ]; then
84
+ log "================================================================="
85
+ log "WARNING: Persistence is not configured for one or more paths."
86
+ log "Data loss is likely if the container is recreated or updated."
87
+ log "Non-persistent paths:"
88
+ while IFS= read -r path; do
89
+ log " - ${path}"
90
+ done <<< "${non_persistent_paths}"
91
+ if detect_droplet; then
92
+ log "Detected DigitalOcean Docker Droplet environment."
93
+ log "By default, Docker Droplets do not configure volumes or persistence."
94
+ log "You will almost certainly lose data if you are not careful."
95
+ fi
96
+ log "Configure persistence: https://github.com/pRizz/opencode-cloud#readme"
97
+ log "================================================================="
98
+ fi
99
+
100
+ if [ "${USE_SYSTEMD:-}" = "1" ]; then
101
+ exec /sbin/init
102
+ else
103
+ # Ensure broker socket directory exists
104
+ install -d -m 0755 /run/opencode
105
+
106
+ # Ensure user records directory exists (ephemeral unless mounted)
107
+ install -d -m 0700 /var/lib/opencode-users
108
+
109
+ restore_users() {
110
+ shopt -s nullglob
111
+ local records=(/var/lib/opencode-users/*.json)
112
+ if [ ${#records[@]} -eq 0 ]; then
113
+ return 1
114
+ fi
115
+ for record in "${records[@]}"; do
116
+ local username password_hash locked
117
+ username="$(jq -r ".username // empty" "${record}")"
118
+ password_hash="$(jq -r ".password_hash // empty" "${record}")"
119
+ locked="$(jq -r ".locked // false" "${record}")"
120
+ if [ -z "${username}" ]; then
121
+ log "Skipping invalid user record: ${record}"
122
+ continue
123
+ fi
124
+ if ! id -u "${username}" >/dev/null 2>&1; then
125
+ log "Creating user: ${username}"
126
+ useradd -m -s /bin/bash "${username}"
127
+ fi
128
+ if [ -n "${password_hash}" ]; then
129
+ usermod -p "${password_hash}" "${username}"
130
+ fi
131
+ if [ "${locked}" = "true" ]; then
132
+ passwd -l "${username}" >/dev/null
133
+ else
134
+ passwd -u "${username}" >/dev/null || true
135
+ fi
136
+ log "Restored user: ${username}"
137
+ done
138
+ return 0
139
+ }
140
+
141
+ persist_user_record() {
142
+ local username="$1"
143
+ local shadow_hash
144
+ shadow_hash="$(getent shadow "${username}" | cut -d: -f2)"
145
+ if [ -z "${shadow_hash}" ]; then
146
+ log "Failed to read shadow hash for ${username}"
147
+ return 1
148
+ fi
149
+ local status locked
150
+ status="$(passwd -S "${username}" | tr -s " " | cut -d" " -f2)"
151
+ locked="false"
152
+ if [ "${status}" = "L" ]; then
153
+ locked="true"
154
+ fi
155
+ local record_path="/var/lib/opencode-users/${username}.json"
156
+ umask 077
157
+ jq -n --arg username "${username}" --arg hash "${shadow_hash}" --argjson locked "${locked}" '{username:$username,password_hash:$hash,locked:$locked}' > "${record_path}"
158
+ chmod 600 "${record_path}"
159
+ log "Persisted user record: ${username}"
160
+ }
161
+
162
+ bootstrap_user() {
163
+ local username="${OPENCODE_BOOTSTRAP_USER:-}"
164
+ local password="${OPENCODE_BOOTSTRAP_PASSWORD:-}"
165
+ local password_hash="${OPENCODE_BOOTSTRAP_PASSWORD_HASH:-}"
166
+ if [ -z "${username}" ]; then
167
+ return 1
168
+ fi
169
+ if [ -z "${password_hash}" ] && [ -z "${password}" ]; then
170
+ log "OPENCODE_BOOTSTRAP_USER is set but no password or hash provided"
171
+ exit 1
172
+ fi
173
+ if ! id -u "${username}" >/dev/null 2>&1; then
174
+ log "Creating bootstrap user: ${username}"
175
+ useradd -m -s /bin/bash "${username}"
176
+ fi
177
+ if [ -n "${password_hash}" ]; then
178
+ usermod -p "${password_hash}" "${username}"
179
+ else
180
+ echo "${username}:${password}" | chpasswd
181
+ fi
182
+ persist_user_record "${username}"
183
+ log "Bootstrap user ready: ${username}"
184
+ return 0
185
+ }
186
+
187
+ if restore_users; then
188
+ log "User records restored"
189
+ else
190
+ if ! bootstrap_user; then
191
+ log "No persisted users and no bootstrap user configured"
192
+ fi
193
+ fi
194
+
195
+ log "Starting opencode on ${OPENCODE_HOST}:${OPENCODE_PORT}"
196
+ /usr/local/bin/opencode-broker &
197
+ # Use runuser to switch to opencode user without password prompt
198
+ exec runuser -u opencode -- sh -lc "cd /home/opencode/workspace && /opt/opencode/bin/opencode web --port ${OPENCODE_PORT} --hostname ${OPENCODE_HOST}"
199
+ fi
@@ -0,0 +1,13 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ opencode_port="${OPENCODE_PORT:-${PORT:-3000}}"
5
+
6
+ if [ -d /run/systemd/system ]; then
7
+ systemctl is-active --quiet opencode-broker.service
8
+ else
9
+ pgrep -x opencode-broker >/dev/null
10
+ fi
11
+
12
+ test -S /run/opencode/auth.sock
13
+ curl -fsS -H "Accept: text/html" "http://localhost:${opencode_port}/" >/dev/null
@@ -0,0 +1,30 @@
1
+ [Unit]
2
+ Description=OpenCode Authentication Broker
3
+ Documentation=https://github.com/pRizz/opencode
4
+ After=network.target
5
+
6
+ [Service]
7
+ Type=notify
8
+ ExecStart=/usr/local/bin/opencode-broker
9
+ ExecReload=/bin/kill -HUP $MAINPID
10
+ Restart=always
11
+ RestartSec=5
12
+
13
+ # Security hardening
14
+ NoNewPrivileges=false
15
+ ProtectSystem=strict
16
+ ProtectHome=read-only
17
+ PrivateTmp=true
18
+ ReadWritePaths=/run/opencode
19
+
20
+ # Socket directory
21
+ RuntimeDirectory=opencode
22
+ RuntimeDirectoryMode=0755
23
+
24
+ # Logging
25
+ StandardOutput=journal
26
+ StandardError=journal
27
+ SyslogIdentifier=opencode-broker
28
+
29
+ [Install]
30
+ WantedBy=multi-user.target
@@ -0,0 +1,5 @@
1
+ {
2
+ "auth": {
3
+ "enabled": true
4
+ }
5
+ }
@@ -0,0 +1,15 @@
1
+ [Unit]
2
+ Description=opencode Web Interface
3
+ After=network.target opencode-broker.service
4
+
5
+ [Service]
6
+ Type=simple
7
+ User=opencode
8
+ WorkingDirectory=/home/opencode/workspace
9
+ ExecStart=/bin/bash -lc "OPENCODE_PORT=${OPENCODE_PORT:-${PORT:-3000}}; OPENCODE_HOST=${OPENCODE_HOST:-0.0.0.0}; exec /opt/opencode/bin/opencode web --port ${OPENCODE_PORT} --hostname ${OPENCODE_HOST}"
10
+ Restart=always
11
+ RestartSec=5
12
+ Environment=PATH=/opt/opencode/bin:/home/opencode/.local/bin:/home/opencode/.cargo/bin:/home/opencode/.local/share/mise/shims:/home/opencode/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
13
+
14
+ [Install]
15
+ WantedBy=multi-user.target
@@ -0,0 +1,9 @@
1
+ # PAM configuration for OpenCode authentication
2
+ # Install to /etc/pam.d/opencode
3
+
4
+ # Standard UNIX authentication
5
+ auth required pam_unix.so
6
+ account required pam_unix.so
7
+
8
+ # Optional: Enable TOTP 2FA (uncomment when pam_google_authenticator is installed)
9
+ # auth required pam_google_authenticator.so
@@ -0,0 +1,21 @@
1
+ # Minimal starship config for fast prompt
2
+ format = """
3
+ $directory\
4
+ $git_branch\
5
+ $git_status\
6
+ $character"""
7
+
8
+ [directory]
9
+ truncation_length = 3
10
+ truncate_to_repo = true
11
+
12
+ [git_branch]
13
+ format = "[$branch]($style) "
14
+ style = "bold purple"
15
+
16
+ [git_status]
17
+ format = '([$all_status$ahead_behind]($style) )'
18
+
19
+ [character]
20
+ success_symbol = "[>](bold green)"
21
+ error_symbol = "[>](bold red)"
@@ -20,6 +20,7 @@ use futures_util::StreamExt;
20
20
  use http_body_util::{Either, Full};
21
21
  use std::collections::{HashMap, HashSet, VecDeque};
22
22
  use std::env;
23
+ use std::io::Write;
23
24
  use std::time::{SystemTime, UNIX_EPOCH};
24
25
  use tar::Builder as TarBuilder;
25
26
  use tracing::{debug, warn};
@@ -887,13 +888,55 @@ fn create_build_context() -> Result<Vec<u8>, std::io::Error> {
887
888
 
888
889
  // Add Dockerfile to archive
889
890
  let dockerfile_bytes = DOCKERFILE.as_bytes();
890
- let mut header = tar::Header::new_gnu();
891
- header.set_path("Dockerfile")?;
892
- header.set_size(dockerfile_bytes.len() as u64);
893
- header.set_mode(0o644);
894
- header.set_cksum();
895
-
896
- tar.append(&header, dockerfile_bytes)?;
891
+ append_bytes(&mut tar, "Dockerfile", dockerfile_bytes, 0o644)?;
892
+ append_bytes(
893
+ &mut tar,
894
+ "packages/core/src/docker/files/entrypoint.sh",
895
+ include_bytes!("files/entrypoint.sh"),
896
+ 0o644,
897
+ )?;
898
+ append_bytes(
899
+ &mut tar,
900
+ "packages/core/src/docker/files/healthcheck.sh",
901
+ include_bytes!("files/healthcheck.sh"),
902
+ 0o644,
903
+ )?;
904
+ append_bytes(
905
+ &mut tar,
906
+ "packages/core/src/docker/files/opencode-broker.service",
907
+ include_bytes!("files/opencode-broker.service"),
908
+ 0o644,
909
+ )?;
910
+ append_bytes(
911
+ &mut tar,
912
+ "packages/core/src/docker/files/opencode.service",
913
+ include_bytes!("files/opencode.service"),
914
+ 0o644,
915
+ )?;
916
+ append_bytes(
917
+ &mut tar,
918
+ "packages/core/src/docker/files/pam/opencode",
919
+ include_bytes!("files/pam/opencode"),
920
+ 0o644,
921
+ )?;
922
+ append_bytes(
923
+ &mut tar,
924
+ "packages/core/src/docker/files/opencode.jsonc",
925
+ include_bytes!("files/opencode.jsonc"),
926
+ 0o644,
927
+ )?;
928
+ append_bytes(
929
+ &mut tar,
930
+ "packages/core/src/docker/files/starship.toml",
931
+ include_bytes!("files/starship.toml"),
932
+ 0o644,
933
+ )?;
934
+ append_bytes(
935
+ &mut tar,
936
+ "packages/core/src/docker/files/bashrc.extra",
937
+ include_bytes!("files/bashrc.extra"),
938
+ 0o644,
939
+ )?;
897
940
  tar.finish()?;
898
941
 
899
942
  // Finish gzip encoding
@@ -904,11 +947,30 @@ fn create_build_context() -> Result<Vec<u8>, std::io::Error> {
904
947
  Ok(archive_buffer)
905
948
  }
906
949
 
950
+ fn append_bytes<W: Write>(
951
+ tar: &mut TarBuilder<W>,
952
+ path: &str,
953
+ contents: &[u8],
954
+ mode: u32,
955
+ ) -> Result<(), std::io::Error> {
956
+ let mut header = tar::Header::new_gnu();
957
+ header.set_path(path)?;
958
+ header.set_size(contents.len() as u64);
959
+ header.set_mode(mode);
960
+ header.set_cksum();
961
+
962
+ tar.append(&header, contents)?;
963
+ Ok(())
964
+ }
965
+
907
966
  #[cfg(test)]
908
967
  mod tests {
909
968
  use super::*;
910
969
  use bollard::models::ImageSummary;
970
+ use flate2::read::GzDecoder;
911
971
  use std::collections::HashMap;
972
+ use std::io::Cursor;
973
+ use tar::Archive;
912
974
 
913
975
  fn make_image_summary(
914
976
  id: &str,
@@ -944,6 +1006,39 @@ mod tests {
944
1006
  assert_eq!(context[1], 0x8b, "should be gzip compressed");
945
1007
  }
946
1008
 
1009
+ #[test]
1010
+ fn build_context_includes_docker_assets() {
1011
+ let context = create_build_context().expect("should create context");
1012
+ let cursor = Cursor::new(context);
1013
+ let decoder = GzDecoder::new(cursor);
1014
+ let mut archive = Archive::new(decoder);
1015
+ let mut found_entrypoint = false;
1016
+ let mut found_healthcheck = false;
1017
+
1018
+ for entry in archive.entries().expect("should read archive entries") {
1019
+ let entry = entry.expect("should read entry");
1020
+ let path = entry.path().expect("should read entry path");
1021
+ if path == std::path::Path::new("packages/core/src/docker/files/entrypoint.sh") {
1022
+ found_entrypoint = true;
1023
+ }
1024
+ if path == std::path::Path::new("packages/core/src/docker/files/healthcheck.sh") {
1025
+ found_healthcheck = true;
1026
+ }
1027
+ if found_entrypoint && found_healthcheck {
1028
+ break;
1029
+ }
1030
+ }
1031
+
1032
+ assert!(
1033
+ found_entrypoint,
1034
+ "entrypoint asset should be in the build context"
1035
+ );
1036
+ assert!(
1037
+ found_healthcheck,
1038
+ "healthcheck asset should be in the build context"
1039
+ );
1040
+ }
1041
+
947
1042
  #[test]
948
1043
  fn default_tag_is_latest() {
949
1044
  assert_eq!(IMAGE_TAG_DEFAULT, "latest");