@opencode-cloud/core 15.1.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 +1 -1
- package/README.md +33 -3
- package/package.json +1 -1
- package/src/docker/Dockerfile +15 -137
- package/src/docker/README.dockerhub.md +10 -2
- package/src/docker/files/README.md +4 -0
- package/src/docker/files/bashrc.extra +25 -0
- package/src/docker/files/entrypoint.sh +172 -0
- package/src/docker/files/opencode-broker.service +30 -0
- package/src/docker/files/opencode.jsonc +5 -0
- package/src/docker/files/opencode.service +15 -0
- package/src/docker/files/pam/opencode +9 -0
- package/src/docker/files/starship.toml +21 -0
- package/src/docker/image.rs +83 -7
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
[](https://github.com/pRizz/opencode-cloud/actions/workflows/ci.yml)
|
|
4
4
|
[](https://gitea.com/pRizz/opencode-cloud)
|
|
5
5
|
[](https://crates.io/crates/opencode-cloud)
|
|
6
|
-
[](https://github.com/pRizz/opencode-cloud/pkgs/container/opencode-cloud-sandbox)
|
|
7
6
|
[](https://hub.docker.com/r/prizz/opencode-cloud-sandbox)
|
|
8
7
|
[](https://hub.docker.com/r/prizz/opencode-cloud-sandbox)
|
|
8
|
+
[](https://github.com/pRizz/opencode-cloud/pkgs/container/opencode-cloud-sandbox)
|
|
9
9
|
[](https://docs.rs/opencode-cloud)
|
|
10
10
|
[](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)
|
|
11
11
|
[](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
|
+
[](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
package/src/docker/Dockerfile
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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="
|
|
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
|
-
|
|
579
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
-
|
|
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
|
|
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
|
|
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
|
|
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,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)"
|
package/src/docker/image.rs
CHANGED
|
@@ -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
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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");
|