Skip to content
Closed
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
41 changes: 41 additions & 0 deletions cli/lib/devcontainer.bash
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ apply_template_placeholders() {
local file=$1
shift

if [[ ! -f "$file" ]]; then
echo "apply_template_placeholders: file not found: $file" >&2
return 1
fi

local tokens=() replacements=()
while [[ $# -ge 2 ]]; do
tokens+=("$1")
Expand Down Expand Up @@ -146,6 +151,11 @@ apply_inline_placeholders() {
local file=$1
shift

if [[ ! -f "$file" ]]; then
echo "apply_inline_placeholders: file not found: $file" >&2
return 1
fi

local tokens=() replacements=()
while [[ $# -ge 2 ]]; do
tokens+=("$1")
Expand All @@ -164,6 +174,37 @@ apply_inline_placeholders() {
mv "$tmpfile" "$file"
}

# Fail loudly if any unsubstituted __PLACEHOLDER__ tokens remain in the
# generated devcontainer files. Acts as a tripwire so the bug surfaces
# during init rather than at compose-up time, where stale placeholders
# silently mount empty directories or pass garbage flags to mitmproxy.
#
# Args:
# $1 - Path to the generated devcontainer directory
verify_no_placeholders() {
local devcontainer_dir=$1
local hits rc=0
# grep exit codes: 0 = match, 1 = no match, >1 = real error. We
# treat 1 as success (no leftover placeholders) and let >1 (e.g.
# the path doesn't exist) propagate as a hard failure.
#
# -I skips binary files (e.g. compiled .pyc bytecode that may
# travel alongside the addon sources). The pattern requires
# uppercase letters/digits so Python lowercase dunders like
# __name__ and __init__ in template scripts don't false-match.
hits=$(grep -IrnE '__[A-Z][A-Z0-9_]*__' "$devcontainer_dir") || rc=$?
if [[ "$rc" -gt 1 ]]; then
echo "verify_no_placeholders: grep failed (rc=$rc) for $devcontainer_dir" >&2
return "$rc"
fi
if [[ -n "$hits" ]]; then
echo "Unsubstituted template placeholders remain in generated files:" >&2
echo "$hits" >&2
return 1
fi
return 0
}

# Replaces provider-specific placeholders in generated templates.
# Args:
# $1 - Path to devcontainer directory
Expand Down
8 changes: 8 additions & 0 deletions cli/libexec/init/devcontainer
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ devcontainer() {
mkdir -p "$devcontainer_dir"
# Use rsync-style copy to include dotfiles (glob * skips them)
cp -R "$SCT_TEMPLATEDIR/devcontainer/." "$devcontainer_dir/"
# Strip any compiled-Python artefacts that may have been copied along
# with the addon sources. Stale .pyc files would otherwise be picked
# up by mitmproxy in preference to the .py source on the bind mount.
find "$devcontainer_dir" -depth \
\( -name '__pycache__' -type d -o -name '*.pyc' \) \
-exec rm -rf {} +

local rel_settings_file="../$settings_file"

Expand Down Expand Up @@ -116,6 +122,8 @@ devcontainer() {

customize_devcontainer_json "$devcontainer_dir/devcontainer.json" "$project_name"

verify_no_placeholders "$devcontainer_dir"

echo "Devcontainer dir created at ${devcontainer_dir#"$project_path"/}" | info
}

Expand Down
28 changes: 28 additions & 0 deletions cli/test/init/devcontainer.bats
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,31 @@ teardown() {
run grep '__STACK_EXTENSIONS__' "$DEVCONTAINER_JSON"
assert_success
}

@test "apply_template_placeholders fails when target file is missing" {
run apply_template_placeholders "$BATS_TEST_TMPDIR/missing.txt" "__X__" "y"
assert_failure
assert_output --partial "file not found"
}

@test "apply_inline_placeholders fails when target file is missing" {
run apply_inline_placeholders "$BATS_TEST_TMPDIR/missing.txt" "__X__" "y"
assert_failure
assert_output --partial "file not found"
}

@test "verify_no_placeholders passes when no placeholders remain" {
mkdir -p "$BATS_TEST_TMPDIR/dc"
echo "all good here" > "$BATS_TEST_TMPDIR/dc/file.txt"
run verify_no_placeholders "$BATS_TEST_TMPDIR/dc"
assert_success
}

@test "verify_no_placeholders fails when a placeholder remains" {
mkdir -p "$BATS_TEST_TMPDIR/dc"
echo "still has __AGENT_MITM_ADDON__ here" > "$BATS_TEST_TMPDIR/dc/file.txt"
run verify_no_placeholders "$BATS_TEST_TMPDIR/dc"
assert_failure
assert_output --partial "Unsubstituted template placeholders"
assert_output --partial "__AGENT_MITM_ADDON__"
}
40 changes: 40 additions & 0 deletions cli/test/init/regression.bats
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,46 @@ cursor_agent_compose_file_has_expected_content() {
assert_customization_volumes_core "$compose_file"
}

# Issue #59: init silently produced a devcontainer with unreplaced
# __AGENT_MITM_ADDON__ / __AGENT_USER_INIT__ / __MITM_HTTP2__ placeholders.
# verify_no_placeholders now fails init outright; this test pins that
# behavior for the claude+jetbrains+node combination from the bug report.
@test "devcontainer leaves no placeholders for claude+jetbrains+node" {
run devcontainer \
--settings-file "$SETTINGS_FILE" \
--project-path "$PROJECT_DIR" \
--agent "claude" \
--ide "jetbrains" \
--name "test-project" \
--stacks "node" \
--proxy "web"
assert_success

local scripts_dir="$PROJECT_DIR/.devcontainer/sandcat/scripts"
local f
for f in \
mitmproxy_addon_claude.py \
mitmproxy_addon_common.py \
wg-client-init.sh \
app-init.sh \
app-post-start.sh \
app-user-init.sh
do
[[ -s "$scripts_dir/$f" ]] || \
fail "expected non-empty $scripts_dir/$f"
done

# Pin the specific symptoms from #59: the bug report named these
# files and these tokens. Asserting via grep rather than relying
# solely on verify_no_placeholders means this test still fails if
# someone later removes the verify call from `devcontainer`.
local proxy_yml="$PROJECT_DIR/.devcontainer/sandcat/compose-proxy.yml"
run grep -nE '__(AGENT_MITM_ADDON|MITM_HTTP2|AGENT_MITM_STREAMING_FLAGS)__' "$proxy_yml"
assert_failure
run grep -nE '__AGENT_USER_INIT__' "$scripts_dir/app-user-init.sh"
assert_failure
}

@test "devcontainer end-to-end: creates devcontainer config for claude agent" {
export SANDCAT_MOUNT_CLAUDE_CONFIG="true"
export SANDCAT_ENABLE_DOTFILES="true"
Expand Down
Loading