Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 5 additions & 32 deletions cli/templates/devcontainer/Dockerfile.app
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ RUN rm -f /etc/sudoers.d/vscode

COPY --chmod=755 sandcat/scripts/app-init.sh /usr/local/bin/app-init.sh
COPY --chmod=755 sandcat/scripts/app-user-init.sh /usr/local/bin/app-user-init.sh
COPY --chmod=644 sandcat/scripts/java-env.sh /etc/profile.d/sandcat-java.sh
COPY --chown=vscode:vscode sandcat/tmux.conf /home/vscode/.tmux.conf

USER vscode
Expand All @@ -39,47 +40,19 @@ ENV LANG="en_US.UTF-8"

# __DEVBOX_INSTALL__

# If a JDK is installed via devbox (java or scala stack, or a user entry
# in devbox.tools.json), bake JAVA_HOME and JAVA_TOOL_OPTIONS into .bashrc
# so VS Code's env probe picks them up before the entrypoint runs. Without
# JAVA_HOME, JVM tooling like Metals fails to find the JDK.
#
# JDK-distribution-agnostic detection: devbox always exposes bin/java in
# its profile as a symlink to the actual JDK inside /nix/store/. Following
# that symlink and stripping bin/ gives the canonical JAVA_HOME for
# whichever distribution the user picked (openjdk, temurin-bin-*, jdk,
# jetbrains.jdk*, or any future one) — no hardcoded layout assumptions.
# Every valid JDK derivation has $JAVA_HOME/lib/security/cacerts inside.
#
# JAVA_TOOL_OPTIONS points to a trust store copy that app-user-init.sh
# populates with the mitmproxy CA at runtime; until then it holds the
# default Java CAs (harmless).
RUN JAVA_BIN="$HOME/.local/share/devbox/global/default/.devbox/nix/profile/default/bin/java"; \
if [ -e "$JAVA_BIN" ]; then \
DEVBOX_JAVA="$(dirname $(dirname $(readlink -f "$JAVA_BIN")))"; \
dir="$HOME/.local/share/sandcat"; mkdir -p "$dir"; \
ln -sfn "$DEVBOX_JAVA" "$dir/java-home"; \
{ echo ''; \
echo '# sandcat-java-env'; \
echo '[ -L "$HOME/.local/share/sandcat/java-home" ] && export JAVA_HOME="$HOME/.local/share/sandcat/java-home"'; \
echo '[ -f "$HOME/.local/share/sandcat/cacerts" ] && export JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=$HOME/.local/share/sandcat/cacerts -Djavax.net.ssl.trustStorePassword=changeit"'; \
} >> "$HOME/.bashrc"; \
fi

# __AGENT_DOCKER_HOME_PREP__

USER root
# Snapshot the image-side /home/vscode state that the agent-home volume
# will mask at runtime (devbox profile with symlinks into /nix/store, the
# sandcat helper dir with java-home + baseline cacerts, and .bashrc env
# hooks). app-init.sh rsyncs this back into the volume when the snapshot
# sandcat helper dir with java-home + baseline cacerts, and .bashrc). app-init.sh
# rsyncs this back into the volume when the snapshot
# hash changes, so rebuilds that add/remove packages or switch JDKs take
# effect without `docker compose down -v` (which would wipe auth Claude
# Code and force the IDE backend to re-upload).
#
# The hash covers merged devbox.json + .bashrc — the two files that
# capture "what packages devbox installed" and "what env we bake". Any
# stack/tools/Java change flips at least one of them; unchanged rebuilds
# The hash covers merged devbox.json + .bashrc. Any stack or tools change
# flips at least one of them; unchanged rebuilds
# leave the hash stable so app-init.sh skips the sync entirely.
RUN mkdir -p /opt/sandcat/snapshots \
&& cp -a /home/vscode/.local/share/devbox /opt/sandcat/snapshots/devbox \
Expand Down
7 changes: 6 additions & 1 deletion cli/templates/devcontainer/sandcat/scripts/app-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ else
fi

# Refresh image-managed home state (devbox profile, sandcat java-home
# symlink and baseline cacerts, .bashrc env hooks) into the agent-home
# symlink and baseline cacerts, .bashrc) into the agent-home
# volume when the image snapshot has changed since last start.
#
# The volume masks anything the Dockerfile writes under /home/vscode after
Expand Down Expand Up @@ -203,6 +203,11 @@ done
# shell so app-user-init still receives sandcat placeholders and env vars.
su - vscode -c '. /mitmproxy-config/sandcat.env 2>/dev/null || true; /usr/local/bin/app-user-init.sh'

# app-user-init.sh has now refreshed the Java trust store. Source the same
# profile script that login and interactive shells use so the agent process
# and every shell it starts inherit the Java settings too.
. /etc/profile.d/sandcat-java.sh

# Source all sandcat profile.d scripts from /etc/bash.bashrc so env vars
# are available in non-login shells (e.g. VS Code integrated terminals).
# Guard with a marker to avoid duplicating on container restart.
Expand Down
10 changes: 10 additions & 0 deletions cli/templates/devcontainer/sandcat/scripts/java-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash

sandcat_home="${SANDCAT_USER_HOME:-/home/vscode}"
if [ -L "$sandcat_home/.local/share/sandcat/java-home" ]; then
export JAVA_HOME="$sandcat_home/.local/share/sandcat/java-home"
fi
if [ -f "$sandcat_home/.local/share/sandcat/cacerts" ]; then
export JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=$sandcat_home/.local/share/sandcat/cacerts -Djavax.net.ssl.trustStorePassword=changeit"
fi
unset sandcat_home
12 changes: 12 additions & 0 deletions cli/test/init/stacks.bats
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ teardown() {
refute_output --partial "openjdk"
}

@test "Dockerfile installs the Java profile instead of .bashrc hooks" {
local dockerfile="$SCT_TEMPLATEDIR/devcontainer/Dockerfile.app"
local app_init="$SCT_TEMPLATEDIR/devcontainer/sandcat/scripts/app-init.sh"
run grep -F 'COPY --chmod=644 sandcat/scripts/java-env.sh /etc/profile.d/sandcat-java.sh' "$dockerfile"
assert_success
run grep -F '. /etc/profile.d/sandcat-java.sh' "$app_init"
assert_success

run grep -F '# sandcat-java-env' "$dockerfile"
assert_failure
}

@test "stack_extension returns extension ID for stacks with extensions" {
run stack_extension python
assert_output "ms-python.python"
Expand Down
25 changes: 25 additions & 0 deletions cli/test/run/run.bats
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,31 @@ teardown() {
unstub_all
}

@test "Java profile reaches non-login child shells" {
local sandcat_home="$BATS_TEST_TMPDIR/vscode"
local java_env="$SCT_ROOT/templates/devcontainer/sandcat/scripts/java-env.sh"
mkdir -p "$sandcat_home/.local/share/sandcat"
ln -s /nix/store/example-jdk "$sandcat_home/.local/share/sandcat/java-home"
touch "$sandcat_home/.local/share/sandcat/cacerts"

run env -i PATH="$PATH" SANDCAT_USER_HOME="$sandcat_home" bash --noprofile --norc -c \
". '$java_env'; bash --noprofile --norc -c 'printf \"%s\\n%s\" \"\$JAVA_HOME\" \"\$JAVA_TOOL_OPTIONS\"'"
assert_success
assert_output "$sandcat_home/.local/share/sandcat/java-home
-Djavax.net.ssl.trustStore=$sandcat_home/.local/share/sandcat/cacerts -Djavax.net.ssl.trustStorePassword=changeit"
}

@test "Java profile leaves non-JVM shells unconfigured" {
local sandcat_home="$BATS_TEST_TMPDIR/vscode"
local java_env="$SCT_ROOT/templates/devcontainer/sandcat/scripts/java-env.sh"
mkdir -p "$sandcat_home/.local/share/sandcat"

run env -i PATH="$PATH" SANDCAT_USER_HOME="$sandcat_home" bash --noprofile --norc -c \
". '$java_env'; printf '<%s>|<%s>' \"\${JAVA_HOME-}\" \"\${JAVA_TOOL_OPTIONS-}\""
assert_success
assert_output "<>|<>"
}

# --- warn_stale_home_volume ---

@test "no warning when volume does not exist (first run)" {
Expand Down
Loading