@opencode-cloud/core 15.2.0 → 16.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 = "16.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
 
@@ -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": "16.0.0",
4
4
  "description": "Core NAPI bindings for opencode-cloud (internal package)",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -452,61 +452,12 @@ RUN git config --global init.defaultBranch main \
452
452
  && git config --global diff.colorMoved default
453
453
 
454
454
  # 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
455
+ COPY --chown=opencode:opencode packages/core/src/docker/files/starship.toml /home/opencode/.config/starship.toml
479
456
 
480
457
  # 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
458
+ COPY --chown=opencode:opencode packages/core/src/docker/files/bashrc.extra /home/opencode/.bashrc.extra
459
+ RUN cat /home/opencode/.bashrc.extra >> /home/opencode/.bashrc \
460
+ && rm /home/opencode/.bashrc.extra
510
461
 
511
462
  # -----------------------------------------------------------------------------
512
463
  # Stage 2: opencode build
@@ -535,7 +486,7 @@ USER opencode
535
486
  # commit on the main branch of https://github.com/pRizz/opencode.
536
487
  # Update it by running: ./scripts/update-opencode-commit.sh
537
488
  # Build opencode from source (BuildKit cache mounts disabled for now)
538
- RUN OPENCODE_COMMIT="d61147bea10a1bc7ba93c6bfa0b7d8fe55a561b5" \
489
+ RUN OPENCODE_COMMIT="41731edead75a52aceac0b48c22474bdeafc746c" \
539
490
  && rm -rf /tmp/opencode-repo \
540
491
  && git clone --depth 1 https://github.com/pRizz/opencode.git /tmp/opencode-repo \
541
492
  && cd /tmp/opencode-repo \
@@ -575,18 +526,8 @@ ENV PATH="/opt/opencode/bin:${PATH}"
575
526
  # This allows opencode to authenticate users via PAM (same users as Cockpit)
576
527
  # NOTE: Requires root privileges to write to /etc/pam.d/
577
528
  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
529
+ COPY packages/core/src/docker/files/pam/opencode /etc/pam.d/opencode
530
+ RUN chmod 644 /etc/pam.d/opencode
590
531
 
591
532
  # Verify PAM config file exists
592
533
  RUN ls -la /etc/pam.d/opencode && cat /etc/pam.d/opencode
@@ -596,38 +537,7 @@ RUN ls -la /etc/pam.d/opencode && cat /etc/pam.d/opencode
596
537
  # -----------------------------------------------------------------------------
597
538
  # Create opencode-broker service for PAM authentication
598
539
  # 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
540
+ COPY packages/core/src/docker/files/opencode-broker.service /etc/systemd/system/opencode-broker.service
631
541
 
632
542
  # Enable opencode-broker service
633
543
  RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
@@ -638,23 +548,7 @@ RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
638
548
  # -----------------------------------------------------------------------------
639
549
  # Create opencode as a systemd service for Cockpit integration
640
550
  # 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
551
+ COPY packages/core/src/docker/files/opencode.service /etc/systemd/system/opencode.service
658
552
 
659
553
  # Enable opencode service to start at boot (manual symlink since systemctl doesn't work during build)
660
554
  RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
@@ -664,15 +558,9 @@ RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
664
558
  # opencode Configuration
665
559
  # -----------------------------------------------------------------------------
666
560
  # 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 \
561
+ RUN mkdir -p /home/opencode/.config/opencode
562
+ COPY --chown=opencode:opencode packages/core/src/docker/files/opencode.jsonc /home/opencode/.config/opencode/opencode.jsonc
563
+ RUN chown -R opencode:opencode /home/opencode/.config/opencode \
676
564
  && chmod 644 /home/opencode/.config/opencode/opencode.jsonc
677
565
 
678
566
  # Verify config file exists
@@ -684,18 +572,8 @@ RUN ls -la /home/opencode/.config/opencode/opencode.jsonc && cat /home/opencode/
684
572
  # Supports both tini (default, works everywhere) and systemd (for Cockpit on Linux)
685
573
  # Set USE_SYSTEMD=1 environment variable to use systemd init
686
574
  # 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
575
+ COPY packages/core/src/docker/files/entrypoint.sh /usr/local/bin/entrypoint.sh
576
+ RUN chmod +x /usr/local/bin/entrypoint.sh
699
577
 
700
578
  # Note: Don't set USER here - entrypoint needs root to use runuser
701
579
  # The tini mode drops privileges to opencode user via runuser
@@ -706,7 +584,7 @@ RUN printf '%s\n' \
706
584
  # Check that opencode main page responds
707
585
  # Works for both tini and systemd modes
708
586
  HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
709
- CMD curl -f -H "Accept: text/html" http://localhost:3000/ || exit 1
587
+ CMD ["sh", "-c", "OPENCODE_PORT=\"${OPENCODE_PORT:-${PORT:-3000}}\"; curl -f -H \"Accept: text/html\" \"http://localhost:${OPENCODE_PORT}/\" || exit 1"]
710
588
 
711
589
  # -----------------------------------------------------------------------------
712
590
  # 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,172 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ log() {
5
+ echo "[opencode-cloud] $*"
6
+ }
7
+
8
+ OPENCODE_PORT="${OPENCODE_PORT:-${PORT:-3000}}"
9
+ OPENCODE_HOST="${OPENCODE_HOST:-0.0.0.0}"
10
+ export OPENCODE_PORT OPENCODE_HOST
11
+
12
+ detect_droplet() {
13
+ local hint="${OPENCODE_CLOUD_ENV:-}"
14
+ if [ -n "${hint}" ]; then
15
+ hint="$(printf "%s" "${hint}" | tr "[:upper:]" "[:lower:]")"
16
+ if [[ "${hint}" == *digitalocean* || "${hint}" == *droplet* ]]; then
17
+ return 0
18
+ fi
19
+ fi
20
+ curl -fsS --connect-timeout 1 --max-time 1 http://169.254.169.254/metadata/v1/id >/dev/null 2>&1
21
+ }
22
+
23
+ collect_non_persistent_paths() {
24
+ local -a paths=(
25
+ "/home/opencode/workspace"
26
+ "/home/opencode/.local/share/opencode"
27
+ "/home/opencode/.local/state/opencode"
28
+ "/home/opencode/.config/opencode"
29
+ "/var/lib/opencode-users"
30
+ )
31
+ local -a non_persistent=()
32
+ local fs_type
33
+ for path in "${paths[@]}"; do
34
+ fs_type="$(stat -f -c %T "${path}" 2>/dev/null || true)"
35
+ case "${fs_type}" in
36
+ ""|overlay|overlayfs|tmpfs|ramfs|squashfs)
37
+ non_persistent+=("${path}")
38
+ ;;
39
+ esac
40
+ done
41
+ if [ ${#non_persistent[@]} -eq 0 ]; then
42
+ return 1
43
+ fi
44
+ printf "%s\n" "${non_persistent[@]}"
45
+ return 0
46
+ }
47
+
48
+ non_persistent_paths="$(collect_non_persistent_paths || true)"
49
+ if [ -n "${non_persistent_paths}" ]; then
50
+ log "================================================================="
51
+ log "WARNING: Persistence is not configured for one or more paths."
52
+ log "Data loss is likely if the container is recreated or updated."
53
+ log "Non-persistent paths:"
54
+ while IFS= read -r path; do
55
+ log " - ${path}"
56
+ done <<< "${non_persistent_paths}"
57
+ if detect_droplet; then
58
+ log "Detected DigitalOcean Docker Droplet environment."
59
+ log "By default, Docker Droplets do not configure volumes or persistence."
60
+ log "You will almost certainly lose data if you are not careful."
61
+ fi
62
+ log "Configure persistence: https://github.com/pRizz/opencode-cloud#readme"
63
+ log "================================================================="
64
+ fi
65
+
66
+ log "----------------------------------------------------------------------"
67
+ log "If you created this container via opencode-cloud CLI, add users with:"
68
+ log " occ user add (or: opencode-cloud user add)"
69
+ log "Learn more: occ --help (or: opencode-cloud --help)"
70
+ log "Docs: https://github.com/pRizz/opencode-cloud#readme"
71
+ log "----------------------------------------------------------------------"
72
+
73
+ if [ "${USE_SYSTEMD:-}" = "1" ]; then
74
+ exec /sbin/init
75
+ else
76
+ # Ensure broker socket directory exists
77
+ install -d -m 0755 /run/opencode
78
+
79
+ # Ensure user records directory exists (ephemeral unless mounted)
80
+ install -d -m 0700 /var/lib/opencode-users
81
+
82
+ restore_users() {
83
+ shopt -s nullglob
84
+ local records=(/var/lib/opencode-users/*.json)
85
+ if [ ${#records[@]} -eq 0 ]; then
86
+ return 1
87
+ fi
88
+ for record in "${records[@]}"; do
89
+ local username password_hash locked
90
+ username="$(jq -r ".username // empty" "${record}")"
91
+ password_hash="$(jq -r ".password_hash // empty" "${record}")"
92
+ locked="$(jq -r ".locked // false" "${record}")"
93
+ if [ -z "${username}" ]; then
94
+ log "Skipping invalid user record: ${record}"
95
+ continue
96
+ fi
97
+ if ! id -u "${username}" >/dev/null 2>&1; then
98
+ log "Creating user: ${username}"
99
+ useradd -m -s /bin/bash "${username}"
100
+ fi
101
+ if [ -n "${password_hash}" ]; then
102
+ usermod -p "${password_hash}" "${username}"
103
+ fi
104
+ if [ "${locked}" = "true" ]; then
105
+ passwd -l "${username}" >/dev/null
106
+ else
107
+ passwd -u "${username}" >/dev/null || true
108
+ fi
109
+ log "Restored user: ${username}"
110
+ done
111
+ return 0
112
+ }
113
+
114
+ persist_user_record() {
115
+ local username="$1"
116
+ local shadow_hash
117
+ shadow_hash="$(getent shadow "${username}" | cut -d: -f2)"
118
+ if [ -z "${shadow_hash}" ]; then
119
+ log "Failed to read shadow hash for ${username}"
120
+ return 1
121
+ fi
122
+ local status locked
123
+ status="$(passwd -S "${username}" | tr -s " " | cut -d" " -f2)"
124
+ locked="false"
125
+ if [ "${status}" = "L" ]; then
126
+ locked="true"
127
+ fi
128
+ local record_path="/var/lib/opencode-users/${username}.json"
129
+ umask 077
130
+ jq -n --arg username "${username}" --arg hash "${shadow_hash}" --argjson locked "${locked}" '{username:$username,password_hash:$hash,locked:$locked}' > "${record_path}"
131
+ chmod 600 "${record_path}"
132
+ log "Persisted user record: ${username}"
133
+ }
134
+
135
+ bootstrap_user() {
136
+ local username="${OPENCODE_BOOTSTRAP_USER:-}"
137
+ local password="${OPENCODE_BOOTSTRAP_PASSWORD:-}"
138
+ local password_hash="${OPENCODE_BOOTSTRAP_PASSWORD_HASH:-}"
139
+ if [ -z "${username}" ]; then
140
+ return 1
141
+ fi
142
+ if [ -z "${password_hash}" ] && [ -z "${password}" ]; then
143
+ log "OPENCODE_BOOTSTRAP_USER is set but no password or hash provided"
144
+ exit 1
145
+ fi
146
+ if ! id -u "${username}" >/dev/null 2>&1; then
147
+ log "Creating bootstrap user: ${username}"
148
+ useradd -m -s /bin/bash "${username}"
149
+ fi
150
+ if [ -n "${password_hash}" ]; then
151
+ usermod -p "${password_hash}" "${username}"
152
+ else
153
+ echo "${username}:${password}" | chpasswd
154
+ fi
155
+ persist_user_record "${username}"
156
+ log "Bootstrap user ready: ${username}"
157
+ return 0
158
+ }
159
+
160
+ if restore_users; then
161
+ log "User records restored"
162
+ else
163
+ if ! bootstrap_user; then
164
+ log "No persisted users and no bootstrap user configured"
165
+ fi
166
+ fi
167
+
168
+ log "Starting opencode on ${OPENCODE_HOST}:${OPENCODE_PORT}"
169
+ /usr/local/bin/opencode-broker &
170
+ # Use runuser to switch to opencode user without password prompt
171
+ exec runuser -u opencode -- sh -lc "cd /home/opencode/workspace && /opt/opencode/bin/opencode web --port ${OPENCODE_PORT} --hostname ${OPENCODE_HOST}"
172
+ fi
@@ -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,49 @@ 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/opencode-broker.service",
901
+ include_bytes!("files/opencode-broker.service"),
902
+ 0o644,
903
+ )?;
904
+ append_bytes(
905
+ &mut tar,
906
+ "packages/core/src/docker/files/opencode.service",
907
+ include_bytes!("files/opencode.service"),
908
+ 0o644,
909
+ )?;
910
+ append_bytes(
911
+ &mut tar,
912
+ "packages/core/src/docker/files/pam/opencode",
913
+ include_bytes!("files/pam/opencode"),
914
+ 0o644,
915
+ )?;
916
+ append_bytes(
917
+ &mut tar,
918
+ "packages/core/src/docker/files/opencode.jsonc",
919
+ include_bytes!("files/opencode.jsonc"),
920
+ 0o644,
921
+ )?;
922
+ append_bytes(
923
+ &mut tar,
924
+ "packages/core/src/docker/files/starship.toml",
925
+ include_bytes!("files/starship.toml"),
926
+ 0o644,
927
+ )?;
928
+ append_bytes(
929
+ &mut tar,
930
+ "packages/core/src/docker/files/bashrc.extra",
931
+ include_bytes!("files/bashrc.extra"),
932
+ 0o644,
933
+ )?;
897
934
  tar.finish()?;
898
935
 
899
936
  // Finish gzip encoding
@@ -904,11 +941,30 @@ fn create_build_context() -> Result<Vec<u8>, std::io::Error> {
904
941
  Ok(archive_buffer)
905
942
  }
906
943
 
944
+ fn append_bytes<W: Write>(
945
+ tar: &mut TarBuilder<W>,
946
+ path: &str,
947
+ contents: &[u8],
948
+ mode: u32,
949
+ ) -> Result<(), std::io::Error> {
950
+ let mut header = tar::Header::new_gnu();
951
+ header.set_path(path)?;
952
+ header.set_size(contents.len() as u64);
953
+ header.set_mode(mode);
954
+ header.set_cksum();
955
+
956
+ tar.append(&header, contents)?;
957
+ Ok(())
958
+ }
959
+
907
960
  #[cfg(test)]
908
961
  mod tests {
909
962
  use super::*;
910
963
  use bollard::models::ImageSummary;
964
+ use flate2::read::GzDecoder;
911
965
  use std::collections::HashMap;
966
+ use std::io::Cursor;
967
+ use tar::Archive;
912
968
 
913
969
  fn make_image_summary(
914
970
  id: &str,
@@ -944,6 +1000,26 @@ mod tests {
944
1000
  assert_eq!(context[1], 0x8b, "should be gzip compressed");
945
1001
  }
946
1002
 
1003
+ #[test]
1004
+ fn build_context_includes_docker_assets() {
1005
+ let context = create_build_context().expect("should create context");
1006
+ let cursor = Cursor::new(context);
1007
+ let decoder = GzDecoder::new(cursor);
1008
+ let mut archive = Archive::new(decoder);
1009
+ let mut found = false;
1010
+
1011
+ for entry in archive.entries().expect("should read archive entries") {
1012
+ let entry = entry.expect("should read entry");
1013
+ let path = entry.path().expect("should read entry path");
1014
+ if path == std::path::Path::new("packages/core/src/docker/files/entrypoint.sh") {
1015
+ found = true;
1016
+ break;
1017
+ }
1018
+ }
1019
+
1020
+ assert!(found, "entrypoint asset should be in the build context");
1021
+ }
1022
+
947
1023
  #[test]
948
1024
  fn default_tag_is_latest() {
949
1025
  assert_eq!(IMAGE_TAG_DEFAULT, "latest");