From 45a3a304997d0570c27f5eeb4b5707f514522d9a Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 26 May 2026 15:24:49 +0000 Subject: [PATCH 1/4] fix(opencode): ensure opencode.json is created in postStartCommand for volume mount resilience When /home/vscode/.config/opencode is a Docker volume mount, the build-time opencode.json created by install.sh is hidden by the empty volume. Move config creation to postStartCommand.sh so it runs at every container start. - Add directory creation, opencode.json write/merge, and chown to postStartCommand.sh - Fix detect_user fallback to match install.sh logic (root fallback instead of hardcoded vscode) - Add postStartCommand recreation test to all 6 test scripts - Document the decision in wiki/decisions/ - Update wiki/ conventions, features, architecture, index --- src/opencode/postStartCommand.sh | 44 +++++++++++++++++++++ test/opencode/autoupdate_disabled.sh | 8 ++++ test/opencode/autoupdate_enabled.sh | 8 ++++ test/opencode/custom_username.sh | 8 ++++ test/opencode/default.sh | 8 ++++ test/opencode/specific_version.sh | 8 ++++ test/opencode/test.sh | 8 ++++ wiki/architecture.md | 7 ++++ wiki/conventions.md | 9 ++++- wiki/decisions/poststart-config-creation.md | 22 +++++++++++ wiki/features.md | 6 ++- wiki/index.md | 3 +- 12 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 wiki/decisions/poststart-config-creation.md diff --git a/src/opencode/postStartCommand.sh b/src/opencode/postStartCommand.sh index 0a34e95..45f6313 100755 --- a/src/opencode/postStartCommand.sh +++ b/src/opencode/postStartCommand.sh @@ -5,6 +5,50 @@ echo_prefix="[opencode-poststart]" autoupdate="${AUTOUPDATE:-true}" +# ensure volume-mounted config directories exist and have correct ownership +detect_user() { + local user="${_REMOTE_USER:-${USERNAME:-}}" + if [ -z "$user" ]; then + user="$(getent passwd | awk -F: '$3 >= 1000 && $1 !~ /^(nobody|nfsnobody|daemon)$/ {print $1; exit 0}')" || true + fi + if [ -z "$user" ]; then + user="$(getent passwd | awk -F: '$3 == 0 && $1 == "root" {print $1; exit 0}')" || true + fi + [ -z "$user" ] && user="root" + echo "$user" +} + +TARGET_USER="$(detect_user)" +TARGET_HOME="$(getent passwd "$TARGET_USER" 2>/dev/null | cut -d: -f6)" +[ -z "$TARGET_HOME" ] && TARGET_HOME="/home/$TARGET_USER" + +# recreate directories masked by volume mounts +mkdir -p "${TARGET_HOME}/.config/opencode" +mkdir -p "${TARGET_HOME}/.local/share/opencode" +mkdir -p "${TARGET_HOME}/.local/state/opencode" + +# ensure opencode.json with lsp config exists +opencode_config="${TARGET_HOME}/.config/opencode/opencode.json" +if [ -f "$opencode_config" ]; then + if ! jq -e '.lsp' "$opencode_config" >/dev/null 2>&1; then + jq '. + {"lsp": {}}' "$opencode_config" > "${opencode_config}.tmp" && mv "${opencode_config}.tmp" "$opencode_config" + echo "$echo_prefix added 'lsp' key to existing opencode.json" + fi +else + cat > "$opencode_config" <<'EOF' +{ + "$schema": "https://opencode.ai/config.json", + "lsp": {} +} +EOF + echo "$echo_prefix created opencode.json with LSP enabled" +fi + +chown -R "${TARGET_USER}:${TARGET_USER}" \ + "${TARGET_HOME}/.config/opencode" \ + "${TARGET_HOME}/.local/share/opencode" \ + "${TARGET_HOME}/.local/state/opencode" 2>/dev/null || true + if ! command -v opencode >/dev/null 2>&1; then echo "$echo_prefix opencode not found, installing..." curl -fsSL https://opencode.ai/install | bash diff --git a/test/opencode/autoupdate_disabled.sh b/test/opencode/autoupdate_disabled.sh index 4a0fc3b..97e709c 100644 --- a/test/opencode/autoupdate_disabled.sh +++ b/test/opencode/autoupdate_disabled.sh @@ -15,4 +15,12 @@ check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG" check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0 +# test postStartCommand recreates config when missing (volume mount case) +OPENCODE_CONFIG_BAK="$OPENCODE_CONFIG" +rm -f "$OPENCODE_CONFIG_BAK" +AUTOUPDATE=false bash /usr/local/share/devcontainer-features/opencode-postStartCommand.sh || true +check "postStartCommand recreates opencode.json" test -f "$OPENCODE_CONFIG_BAK" +check "recreated opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG_BAK" +check "recreated opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG_BAK")" -eq 0 + reportResults \ No newline at end of file diff --git a/test/opencode/autoupdate_enabled.sh b/test/opencode/autoupdate_enabled.sh index 3b27772..861436a 100644 --- a/test/opencode/autoupdate_enabled.sh +++ b/test/opencode/autoupdate_enabled.sh @@ -17,4 +17,12 @@ check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG" check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0 +# test postStartCommand recreates config when missing (volume mount case) +OPENCODE_CONFIG_BAK="$OPENCODE_CONFIG" +rm -f "$OPENCODE_CONFIG_BAK" +AUTOUPDATE=false bash /usr/local/share/devcontainer-features/opencode-postStartCommand.sh || true +check "postStartCommand recreates opencode.json" test -f "$OPENCODE_CONFIG_BAK" +check "recreated opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG_BAK" +check "recreated opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG_BAK")" -eq 0 + reportResults \ No newline at end of file diff --git a/test/opencode/custom_username.sh b/test/opencode/custom_username.sh index d249541..74d8104 100755 --- a/test/opencode/custom_username.sh +++ b/test/opencode/custom_username.sh @@ -17,4 +17,12 @@ check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG" check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0 +# test postStartCommand recreates config when missing (volume mount case) +OPENCODE_CONFIG_BAK="$OPENCODE_CONFIG" +rm -f "$OPENCODE_CONFIG_BAK" +AUTOUPDATE=false bash /usr/local/share/devcontainer-features/opencode-postStartCommand.sh || true +check "postStartCommand recreates opencode.json" test -f "$OPENCODE_CONFIG_BAK" +check "recreated opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG_BAK" +check "recreated opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG_BAK")" -eq 0 + reportResults diff --git a/test/opencode/default.sh b/test/opencode/default.sh index d249541..74d8104 100755 --- a/test/opencode/default.sh +++ b/test/opencode/default.sh @@ -17,4 +17,12 @@ check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG" check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0 +# test postStartCommand recreates config when missing (volume mount case) +OPENCODE_CONFIG_BAK="$OPENCODE_CONFIG" +rm -f "$OPENCODE_CONFIG_BAK" +AUTOUPDATE=false bash /usr/local/share/devcontainer-features/opencode-postStartCommand.sh || true +check "postStartCommand recreates opencode.json" test -f "$OPENCODE_CONFIG_BAK" +check "recreated opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG_BAK" +check "recreated opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG_BAK")" -eq 0 + reportResults diff --git a/test/opencode/specific_version.sh b/test/opencode/specific_version.sh index 872d49c..1dd0182 100755 --- a/test/opencode/specific_version.sh +++ b/test/opencode/specific_version.sh @@ -21,4 +21,12 @@ check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG" check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0 +# test postStartCommand recreates config when missing (volume mount case) +OPENCODE_CONFIG_BAK="$OPENCODE_CONFIG" +rm -f "$OPENCODE_CONFIG_BAK" +AUTOUPDATE=false bash /usr/local/share/devcontainer-features/opencode-postStartCommand.sh || true +check "postStartCommand recreates opencode.json" test -f "$OPENCODE_CONFIG_BAK" +check "recreated opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG_BAK" +check "recreated opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG_BAK")" -eq 0 + reportResults diff --git a/test/opencode/test.sh b/test/opencode/test.sh index 5c30acb..3e2f4ae 100755 --- a/test/opencode/test.sh +++ b/test/opencode/test.sh @@ -19,4 +19,12 @@ check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG" check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0 +# test postStartCommand recreates config when missing (volume mount case) +OPENCODE_CONFIG_BAK="$OPENCODE_CONFIG" +rm -f "$OPENCODE_CONFIG_BAK" +AUTOUPDATE=false bash /usr/local/share/devcontainer-features/opencode-postStartCommand.sh || true +check "postStartCommand recreates opencode.json" test -f "$OPENCODE_CONFIG_BAK" +check "recreated opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG_BAK" +check "recreated opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG_BAK")" -eq 0 + reportResults diff --git a/wiki/architecture.md b/wiki/architecture.md index e423018..43c91c2 100644 --- a/wiki/architecture.md +++ b/wiki/architecture.md @@ -8,8 +8,15 @@ └── validate.yaml # Validate features src/ # Feature implementations ├── opencode/ +│ ├── devcontainer-feature.json # metadata + options +│ ├── install.sh # build-time setup +│ ├── postStartCommand.sh # runtime config (volume resilience) +│ ├── README.md +│ └── NOTES.md +├── agents-workspace/ │ ├── devcontainer-feature.json │ ├── install.sh +│ ├── postStartCommand.sh │ ├── README.md │ └── NOTES.md └── agency-agents/ diff --git a/wiki/conventions.md b/wiki/conventions.md index c9246cc..24e4f04 100644 --- a/wiki/conventions.md +++ b/wiki/conventions.md @@ -4,7 +4,14 @@ Each feature lives in `src//` with: - `devcontainer-feature.json` — metadata + options -- `install.sh` — entrypoint script +- `install.sh` — entrypoint script (build time) +- `postStartCommand.sh` — post-start script (runtime) + +## Volume Mount Resilience + +Config files and directories that must survive Docker volume mounts must be created or ensured in `postStartCommand.sh`, not only in `install.sh`. Volumes mounted at runtime replace build-time directories; `postStartCommand.sh` runs after volumes are mounted and can recreate any missing state. + +`install.sh` may still create these files at build time for the non-volume case, but `postStartCommand.sh` must be the authoritative source for runtime state. ## Options diff --git a/wiki/decisions/poststart-config-creation.md b/wiki/decisions/poststart-config-creation.md new file mode 100644 index 0000000..26bed9a --- /dev/null +++ b/wiki/decisions/poststart-config-creation.md @@ -0,0 +1,22 @@ +--- +title: PostStart config creation for volume mount resilience +date: 2026-05-26 +status: accepted +--- + +## Context + +The `opencode` feature creates `~/.config/opencode/opencode.json` (with `lsp: {}`) during `install.sh` at build time. When a Docker volume is mounted at `/home/vscode/.config/opencode`, the volume starts empty at runtime and hides the build-time file. The `postStartCommand.sh` did not recreate it, so LSP servers remained unconfigured. + +The `agents-workspace` feature demonstrates the correct pattern: `install.sh` only copies the postStart script, and all actual setup happens in `postStartCommand.sh` at runtime. + +## Decision + +Move `opencode.json` creation (and directory ownership fixes) from `install.sh` into `postStartCommand.sh`, which runs every container start. `install.sh` still creates the file at build time (harmless for non-volume cases), but `postStartCommand.sh` guarantees it exists even when a volume replaces the directory. + +## Consequences + +- Volume mounts no longer break LSP server configuration +- `postStartCommand.sh` is idempotent — if the file exists with `lsp`, it skips creation; if it exists without `lsp`, it merges it in +- Ownership is corrected at runtime in case the volume changes UID/GID mappings +- Tests now validate that `postStartCommand.sh` can recreate the config independently diff --git a/wiki/features.md b/wiki/features.md index 3022250..cf1d1d0 100644 --- a/wiki/features.md +++ b/wiki/features.md @@ -2,13 +2,17 @@ ## opencode -Installs opencode CLI and fixes volume-mounted directory permissions. +Installs opencode CLI and ensures volume-mounted config directories are writable. Options: - `username` (default: _REMOTE_USER) — user to install for - `version` (default: empty/latest) — install specific version - `autoupdate` (default: true) — auto-upgrade on container start +Behavior: +- `install.sh` installs the CLI, creates directories, writes `~/.config/opencode/opencode.json` with `lsp: {}`, and generates a fix-permissions script. +- `postStartCommand.sh` ensures `opencode.json` exists at every start (critical when `~/.config/opencode` is a volume mount), installing opencode if missing and running autoupgrade when enabled. + Idempotency: marker uses version if specified, else "latest" ## agency-agents diff --git a/wiki/index.md b/wiki/index.md index 1d37162..c05ad09 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -1,10 +1,11 @@ # Wiki - [architecture.md](architecture.md) — project structure and feature layout -- [conventions.md](conventions.md) — feature structure and option patterns +- [conventions.md](conventions.md) — feature structure, option patterns, volume mount resilience - [features.md](features.md) — available features and their purpose - [publishing.md](publishing.md) — release workflow and GHCR publishing - [testing.md](testing.md) — testing commands and CLI usage +- [decisions/poststart-config-creation.md](decisions/poststart-config-creation.md) — runtime config creation for volume mount support ## Operations From def7d0ab816d56a767c08f86db36dfb2209e5bde Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 26 May 2026 15:25:53 +0000 Subject: [PATCH 2/4] =?UTF-8?q?chore(opencode):=20bump=20version=200.3.6?= =?UTF-8?q?=20=E2=86=92=200.3.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/opencode/devcontainer-feature.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/opencode/devcontainer-feature.json b/src/opencode/devcontainer-feature.json index e71927d..fe287ac 100644 --- a/src/opencode/devcontainer-feature.json +++ b/src/opencode/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "opencode CLI", "id": "opencode", - "version": "0.3.6", + "version": "0.3.7", "description": "Installs the opencode AI coding agent CLI and ensures volume-mounted data directories are owned by the correct user.", "documentationURL": "https://opencode.ai", "options": { From 779bba76b21e9282de7c5d14fe39f41c564d29a5 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 26 May 2026 15:29:58 +0000 Subject: [PATCH 3/4] fix(opencode): align detect_user with install.sh shell validation postStartCommand.sh was returning _REMOTE_USER/USERNAME without validating the user exists in passwd. In CI, _REMOTE_USER=vscode is set even in root-only images (debian:latest), causing the config to be created in /home/vscode/ while tests check /root/. Copy the same shell-validation logic from install.sh: if the env-var user has */nologin, */false, or empty shell, fall back to the first UID>=1000 user or root. --- src/opencode/postStartCommand.sh | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/opencode/postStartCommand.sh b/src/opencode/postStartCommand.sh index 45f6313..be0673d 100755 --- a/src/opencode/postStartCommand.sh +++ b/src/opencode/postStartCommand.sh @@ -8,13 +8,22 @@ autoupdate="${AUTOUPDATE:-true}" # ensure volume-mounted config directories exist and have correct ownership detect_user() { local user="${_REMOTE_USER:-${USERNAME:-}}" - if [ -z "$user" ]; then - user="$(getent passwd | awk -F: '$3 >= 1000 && $1 !~ /^(nobody|nfsnobody|daemon)$/ {print $1; exit 0}')" || true - fi - if [ -z "$user" ]; then - user="$(getent passwd | awk -F: '$3 == 0 && $1 == "root" {print $1; exit 0}')" || true + local valid_user + + valid_user="$(getent passwd | awk -F: '$3 >= 1000 && $1 !~ /^(nobody|nfsnobody|daemon)$/ {print $1; exit 0}')" || true + [ -z "$valid_user" ] && valid_user="root" + + if [ -n "$user" ]; then + local user_shell + user_shell="$(getent passwd "$user" 2>/dev/null | cut -d: -f7)" || true + case "$user_shell" in + */nologin|*/false|"") + user="$valid_user" + ;; + esac fi - [ -z "$user" ] && user="root" + + [ -z "$user" ] && user="$valid_user" echo "$user" } From 102ce0cb5e37eb6adca90f911f8519afade92d95 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 26 May 2026 15:42:05 +0000 Subject: [PATCH 4/4] fix(opencode): align postStart user detection with install.sh via HOME --- src/opencode/postStartCommand.sh | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/opencode/postStartCommand.sh b/src/opencode/postStartCommand.sh index be0673d..99dd1a7 100755 --- a/src/opencode/postStartCommand.sh +++ b/src/opencode/postStartCommand.sh @@ -6,15 +6,19 @@ echo_prefix="[opencode-poststart]" autoupdate="${AUTOUPDATE:-true}" # ensure volume-mounted config directories exist and have correct ownership -detect_user() { - local user="${_REMOTE_USER:-${USERNAME:-}}" - local valid_user +# resolve the user and home matching where install.sh placed the config +TARGET_USER="" +TARGET_HOME="${HOME:-}" +if [ -n "$TARGET_HOME" ]; then + TARGET_USER="$(getent passwd | awk -F: -v h="$TARGET_HOME" '$6 == h {print $1; exit}')" || true +fi + +if [ -z "$TARGET_USER" ]; then + user="${_REMOTE_USER:-${USERNAME:-}}" valid_user="$(getent passwd | awk -F: '$3 >= 1000 && $1 !~ /^(nobody|nfsnobody|daemon)$/ {print $1; exit 0}')" || true [ -z "$valid_user" ] && valid_user="root" - if [ -n "$user" ]; then - local user_shell user_shell="$(getent passwd "$user" 2>/dev/null | cut -d: -f7)" || true case "$user_shell" in */nologin|*/false|"") @@ -22,14 +26,11 @@ detect_user() { ;; esac fi - [ -z "$user" ] && user="$valid_user" - echo "$user" -} - -TARGET_USER="$(detect_user)" -TARGET_HOME="$(getent passwd "$TARGET_USER" 2>/dev/null | cut -d: -f6)" -[ -z "$TARGET_HOME" ] && TARGET_HOME="/home/$TARGET_USER" + TARGET_USER="$user" + TARGET_HOME="$(getent passwd "$TARGET_USER" 2>/dev/null | cut -d: -f6)" + [ -z "$TARGET_HOME" ] && TARGET_HOME="/home/$TARGET_USER" +fi # recreate directories masked by volume mounts mkdir -p "${TARGET_HOME}/.config/opencode"