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
15 changes: 10 additions & 5 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh
RUN mkdir -p /home/vscode/.npm \
&& mkdir -p /home/vscode/.nuget/packages \
&& mkdir -p /home/vscode/.cache/pip \
&& mkdir -p /home/vscode/.cache/unity-helpers-devcontainer \
&& mkdir -p /home/vscode/.unity-test-project \
&& chown -R vscode:vscode /home/vscode/.npm /home/vscode/.nuget /home/vscode/.cache /home/vscode/.unity-test-project

Expand All @@ -301,17 +302,21 @@ RUN dotnet tool install -g csharpier --version 1.2.6
# added below (ENV for processes, profile.d for login shells).
RUN npm config set prefix "${HOME}/.local"

# Pre-install the AI coding agent CLIs so a freshly built/rebuilt image has them
# even before post-create runs. post-create.sh/post-start.sh refresh these to the
# latest versions (via .devcontainer/install-agent-clis.sh), so this layer is a
# fast offline-safe baseline rather than the source of truth.
# Pre-install the AI coding agent CLIs and local MCP runtime packages so a freshly
# built/rebuilt image has them even before post-create runs. post-create.sh and
# post-start.sh refresh these to the latest versions, so this layer is a fast
# offline-safe baseline rather than the source of truth.
RUN npm install -g --no-fund --no-audit \
"@openai/codex@latest" \
"opencode-ai@latest" \
"@nanocollective/nanocoder@latest" \
"@z_ai/mcp-server@latest" \
"mcp-remote@latest" \
&& "$(npm prefix -g)/bin/codex" --version \
&& "$(npm prefix -g)/bin/opencode" --version \
&& "$(npm prefix -g)/bin/nanocoder" --version
&& "$(npm prefix -g)/bin/nanocoder" --version \
&& test -x "$(npm prefix -g)/bin/zai-mcp-server" \
&& "$(npm prefix -g)/bin/mcp-remote" --version

# Switch back to root for remaining setup
USER root
Expand Down
9 changes: 6 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
"remoteEnv": {
"GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}",
"GH_TOKEN": "${localEnv:GH_TOKEN}",
"GITHUB_PERSONAL_ACCESS_TOKEN": "${localEnv:GITHUB_PERSONAL_ACCESS_TOKEN}",
"Z_AI_API_KEY": "${localEnv:Z_AI_API_KEY}",
// Credentials resolve from a 0600 cache; an EMPTY cache used to fall through to the editor's
// askpass, which raises a dialog on the owner's desktop that no credential helper can suppress
// (#450). This makes the fallback an error naming the fix instead. The Git extension sets its
Expand All @@ -65,12 +67,12 @@
// 1. Fix volume mount permissions (Docker named volumes are root-owned)
// 2. Restore .NET tools (CSharpier, etc.)
// 3. Install npm dependencies (markdownlint-cli, cspell, prettier)
// 4. Install/update AI coding agent CLIs (codex, opencode, nanocoder — latest via npm)
// 4. Install/update AI coding agent CLIs and MCP runtimes (latest via npm)
// 5. Check Codex authentication state (advisory)
// 6. Sync Unity MCP client configs from .env.local (all agents)
// 6. Sync Unity and shared MCP client configs (all agents)
// 7. Install git hooks
// 8. Mark workspace as safe directory
// 9. Pre-pull Unity Docker image (background, non-blocking)
// 9. Pre-pull Unity and GitHub MCP Docker images (background, non-blocking)
"postCreateCommand": "bash .devcontainer/post-create.sh",
// Runs on every container start to verify/repair agent CLI availability and
// re-assert volume ownership Docker may have reset (keeps `npm install` sudo-free).
Expand Down Expand Up @@ -467,6 +469,7 @@
"source=${localWorkspaceFolder}/.git,target=${containerWorkspaceFolder}/.git,type=bind,consistency=cached",
"source=unity-helpers-npm-cache,target=/home/vscode/.npm,type=volume",
"source=unity-helpers-pip-cache,target=/home/vscode/.cache/pip,type=volume",
"source=unity-helpers-agent-state,target=/home/vscode/.cache/unity-helpers-devcontainer,type=volume",
"source=unity-helpers-nuget-cache,target=/home/vscode/.nuget/packages,type=volume",
"source=unity-helpers-docker-cache,target=/var/lib/docker,type=volume",
"source=unity-helpers-unity-project,target=/home/vscode/.unity-test-project,type=volume"
Expand Down
39 changes: 22 additions & 17 deletions .devcontainer/install-agent-clis.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,34 @@
# =============================================================================
# install-agent-clis.sh
# -----------------------------------------------------------------------------
# Idempotently install the latest AI coding agent CLIs as user-global npm
# packages. This script is safe to run on every container start.
# Idempotently install the latest AI coding agent CLIs and MCP runtime packages
# as user-global npm packages. This script is safe to run on every container
# start.
#
# Agents installed:
# Packages installed:
# * @openai/codex (bin: codex)
# * opencode-ai (bin: opencode)
# * @nanocollective/nanocoder (bin: nanocoder)
# * @z_ai/mcp-server (bin: zai-mcp-server)
# * mcp-remote (bin: mcp-remote)
#
# Behavior:
# * Resolves each package's npm latest dist-tag with a bounded timeout.
# * Skips install when the current version already matches latest.
# * Installs into NPM_CONFIG_PREFIX (default: $HOME/.local) without sudo.
# * Retries transient failures with small backoff.
# * Never fails callers; offline and registry errors are non-fatal.
# * One missing agent never blocks installing the others.
# * One missing package never blocks installing the others.
# =============================================================================

set -euo pipefail

AGENTS=(
PACKAGES=(
"@openai/codex|codex"
"opencode-ai|opencode"
"@nanocollective/nanocoder|nanocoder"
"@z_ai/mcp-server|zai-mcp-server"
"mcp-remote|mcp-remote"
)
NPM_PREFIX="${NPM_CONFIG_PREFIX:-${HOME}/.local}"
VIEW_TIMEOUT_SECONDS="${CODEX_NPM_VIEW_TIMEOUT_SECONDS:-20}"
Expand Down Expand Up @@ -65,7 +70,7 @@ while [ "$#" -gt 0 ]; do
done

if ! command -v npm >/dev/null 2>&1; then
warn "npm not found; skipping agent CLI installs."
warn "npm not found; skipping agent CLI and MCP runtime installs."
exit 0
fi

Expand Down Expand Up @@ -101,19 +106,19 @@ latest_version() {
timeout "${VIEW_TIMEOUT_SECONDS}" npm view "${pkg}" version 2>/dev/null | tr -d '[:space:]' || true
}

# Returns 0 when the agent is installed and its binary resolves.
verify_agent() {
# Returns 0 when the package is installed and its binary resolves.
verify_package() {
local bin="$1"
command -v "${bin}" >/dev/null 2>&1
}

install_agent() {
install_package() {
local pkg="$1" bin="$2" latest="$3"
local attempt
for attempt in 1 2 3; do
if timeout "${INSTALL_TIMEOUT_SECONDS}" npm install -g "${pkg}@${latest}" --silent --no-fund --no-audit; then
hash -r 2>/dev/null || true
if verify_agent "${bin}"; then
if verify_package "${bin}"; then
log "${pkg} ready: $(${bin} --version 2>/dev/null | head -1 || echo "${latest}")"
return 0
fi
Expand All @@ -129,15 +134,15 @@ install_agent() {

failures=0

for agent in "${AGENTS[@]}"; do
pkg="${agent%%|*}"
bin="${agent##*|}"
for package in "${PACKAGES[@]}"; do
pkg="${package%%|*}"
bin="${package##*|}"

installed="$(installed_version "${pkg}")"
latest="$(latest_version "${pkg}")"

if [[ -z "${latest}" ]]; then
if [[ -n "${installed}" ]] && verify_agent "${bin}"; then
if [[ -n "${installed}" ]] && verify_package "${bin}"; then
log "Registry unreachable; keeping installed ${pkg}@${installed}."
else
warn "Registry unreachable and ${bin} is not installed; will retry later."
Expand All @@ -147,21 +152,21 @@ for agent in "${AGENTS[@]}"; do
fi

if [[ "${installed}" == "${latest}" ]]; then
if verify_agent "${bin}"; then
if verify_package "${bin}"; then
log "${pkg}@${installed} already up-to-date."
continue
fi
warn "${pkg}@${installed} is registered but ${bin} is not on PATH; reinstalling."
fi

log "Installing ${pkg}@${latest} (previously: ${installed:-not installed})"
if ! install_agent "${pkg}" "${bin}" "${latest}"; then
if ! install_package "${pkg}" "${bin}" "${latest}"; then
failures=$((failures + 1))
fi
done

if [ "${failures}" -gt 0 ]; then
warn "${failures} agent CLI(s) could not be verified; continuing without them."
warn "${failures} package(s) could not be verified; continuing without them."
fi

exit 0
49 changes: 33 additions & 16 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ VOLUME_DIRS=(
"/home/vscode/.npm"
"/home/vscode/.nuget/packages"
"/home/vscode/.cache/pip"
"/home/vscode/.cache/unity-helpers-devcontainer"
"/home/vscode/.unity-test-project"
"/home/vscode/.unity-test-project/.unity-license-cache/local-share-unity3d"
"/home/vscode/.unity-test-project/.unity-license-cache/config-unity3d"
Expand Down Expand Up @@ -117,12 +118,12 @@ else
log_ok "npm install succeeded (no lockfile)"
fi

# ── Step 4: Install/update AI coding agent CLIs ─────────────────────────────
# Ensures OpenAI Codex, OpenCode, and nanocoder are installed globally and
# current on first container create. Failures are non-fatal and retried again
# from post-start.
# ── Step 4: Install/update AI coding agents and MCP runtimes ─────────────────
# Ensures OpenAI Codex, OpenCode, nanocoder, Z.AI Vision MCP, and the remote MCP
# adapter are installed globally and current on first container create. Failures
# are non-fatal and retried again from post-start.

log_step "Installing AI coding agent CLIs (codex, opencode, nanocoder)"
log_step "Installing AI coding agent CLIs and MCP runtimes"

if bash "$SCRIPT_DIR/install-agent-clis.sh" --force-latest-check; then
for agent_bin in codex opencode nanocoder; do
Expand All @@ -132,6 +133,13 @@ if bash "$SCRIPT_DIR/install-agent-clis.sh" --force-latest-check; then
log_warn "$agent_bin CLI is not currently available (non-fatal). It will retry on next container start."
fi
done
for mcp_bin in zai-mcp-server mcp-remote; do
if command -v "$mcp_bin" >/dev/null 2>&1; then
log_ok "$mcp_bin MCP runtime is available"
else
log_warn "$mcp_bin MCP runtime is not currently available (non-fatal). It will retry on next container start."
fi
done
else
log_warn "Agent CLI installation failed (non-fatal). It will retry on next container start."
fi
Expand All @@ -147,16 +155,21 @@ else
log_warn "Codex is not logged in yet. Run: npm run codex:login"
fi

# ── Step 4c: Sync Unity MCP client configs ──────────────────────────────────
# ── Step 4c: Sync MCP client configs ────────────────────────────────────────
# Regenerates every agent MCP config (Claude Code, Cursor, VS Code, Codex,
# OpenCode, nanocoder) from .env.local so a rebuilt container never serves a
# stale endpoint to a newly installed agent. Best-effort: the bridge is
# usually running on the HOST, so discovery failing here is expected; the
# pinned endpoint is written anyway. Requires node_modules (Step 3 ran first).
# OpenCode, nanocoder) with Unity, GitHub, and Z.AI servers. Best-effort: the
# Unity bridge is usually running on the HOST, so discovery failing here is
# expected; the pinned endpoint and shared server launchers are written anyway.
# Requires node_modules (Step 3 ran first).

log_step "Syncing Unity MCP client configs"
log_step "Syncing MCP client configs"

MCP_REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
if node "${MCP_REPO_ROOT}/scripts/mcp/unity-mcp.mjs" configure-shared >/dev/null 2>&1; then
log_ok "GitHub and Z.AI MCP client configs written"
else
log_warn "Could not sync shared MCP client configs (non-fatal). Run: node scripts/mcp/unity-mcp.mjs configure-shared"
fi
if node "${MCP_REPO_ROOT}/scripts/mcp/unity-mcp.mjs" configure --no-discover >/dev/null 2>&1; then
log_ok "Unity MCP client configs written"
else
Expand Down Expand Up @@ -209,25 +222,29 @@ log_ok "Marked $WORKSPACE_DIR as safe directory"
# safe.directory entries (a "not absolute" warning per entry on every git command).
bash "$WORKSPACE_DIR/scripts/normalize-container-git-config.sh" || log_warn "Could not normalize container git config"

# ── Step 7: Pre-pull Unity Docker image (background) ─────────────────────────
# Pre-pulls the GameCI Unity Editor Docker image so that unity test/compile
# scripts can run immediately without waiting for the image download.
# ── Step 7: Pre-pull runtime Docker images (background) ─────────────────────
# Pre-pulls the GameCI Unity Editor and official GitHub MCP images so tests and
# the first GitHub tool call do not wait for image downloads.
# This runs in the background and is non-fatal (the image will be pulled
# on-demand if this step is skipped or fails).

log_step "Pre-pulling Unity Docker image (background)"
log_step "Pre-pulling runtime Docker images (background)"

UNITY_VERSION="${UNITY_VERSION:-2021.3.45f1}"
UNITY_IMAGE_VERSION="${UNITY_IMAGE_VERSION:-3}"
UNITY_IMAGE="unityci/editor:ubuntu-${UNITY_VERSION}-base-${UNITY_IMAGE_VERSION}"
GITHUB_MCP_IMAGE="${GITHUB_MCP_IMAGE:-ghcr.io/github/github-mcp-server:latest}"

if command -v docker >/dev/null 2>&1; then
# Pull in background so it doesn't block post-create
(docker pull "$UNITY_IMAGE" >/dev/null 2>&1 && \
log_ok "Unity image pulled: $UNITY_IMAGE") &
log_ok "Unity image pull started in background: $UNITY_IMAGE"
(docker pull "$GITHUB_MCP_IMAGE" >/dev/null 2>&1 && \
log_ok "GitHub MCP image pulled: $GITHUB_MCP_IMAGE") &
log_ok "GitHub MCP image pull started in background: $GITHUB_MCP_IMAGE"
else
log_warn "Docker not available yet (DinD may still be starting). Unity image will be pulled on first use."
log_warn "Docker not available yet (DinD may still be starting). Runtime images will be pulled on first use."
fi

# ── Done ─────────────────────────────────────────────────────────────────────
Expand Down
63 changes: 42 additions & 21 deletions .devcontainer/post-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
# Post-start setup script for the devcontainer.
# Runs as the remoteUser (vscode) after each container start.
#
# This script keeps the AI coding agent CLIs (OpenAI Codex, OpenCode, nanocoder)
# available across restarts, repairs volume mount ownership that Docker may have
# reset, and avoids unnecessary npm registry calls on every start.
# This script keeps the AI coding agent CLIs and MCP runtimes available across
# restarts, repairs volume mount ownership that Docker may have reset, and avoids
# unnecessary npm registry calls on every start.

set -euo pipefail

Expand Down Expand Up @@ -46,6 +46,7 @@ USER_OWNED_DIRS=(
"${HOME}/.local"
"${HOME}/.nuget/packages"
"${HOME}/.cache/pip"
"${HOME}/.cache/unity-helpers-devcontainer"
"${HOME}/.unity-test-project"
)

Expand Down Expand Up @@ -153,33 +154,53 @@ else
log_warn "Run: bash scripts/check-container-git-credentials.sh --fix"
fi

log_step "Verifying AI coding agent CLIs (codex, opencode, nanocoder)"
log_step "Verifying AI coding agent CLIs and MCP runtimes"

mkdir -p "$STATE_DIR"
load_failure_state

if retry_is_deferred; then
exit 0
fi
if ! retry_is_deferred; then
# Installer is non-fatal by design; verify command availability explicitly.
bash "$SCRIPT_DIR/install-agent-clis.sh" || true
all_packages_available=true
for agent_bin in codex opencode nanocoder; do
if command -v "$agent_bin" >/dev/null 2>&1 && timeout "${CODEX_VERSION_TIMEOUT_SECONDS}" "$agent_bin" --version >/dev/null 2>&1; then
log_ok "$agent_bin CLI is available"
else
all_packages_available=false
log_warn "$agent_bin CLI verification failed (non-fatal)."
fi
done
for mcp_bin in zai-mcp-server mcp-remote; do
if command -v "$mcp_bin" >/dev/null 2>&1; then
log_ok "$mcp_bin MCP runtime is available"
else
all_packages_available=false
log_warn "$mcp_bin MCP runtime verification failed (non-fatal)."
fi
done

# Installer is non-fatal by design; verify command availability explicitly.
bash "$SCRIPT_DIR/install-agent-clis.sh" || true
all_agents_available=true
for agent_bin in codex opencode nanocoder; do
if command -v "$agent_bin" >/dev/null 2>&1 && timeout "${CODEX_VERSION_TIMEOUT_SECONDS}" "$agent_bin" --version >/dev/null 2>&1; then
log_ok "$agent_bin CLI is available"
if [ "$all_packages_available" = true ]; then
clear_failure_state
log_ok "All agent CLIs and MCP runtimes are available"
else
all_agents_available=false
log_warn "$agent_bin CLI verification failed (non-fatal)."
record_failure_and_backoff
log_warn "Package verification failed (non-fatal). Re-run: bash .devcontainer/install-agent-clis.sh --force-latest-check"
fi
done
fi

log_step "Syncing MCP client configs"

if [ "$all_agents_available" = true ]; then
clear_failure_state
log_ok "All agent CLIs are available"
MCP_REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
if node "${MCP_REPO_ROOT}/scripts/mcp/unity-mcp.mjs" configure-shared >/dev/null 2>&1; then
log_ok "GitHub and Z.AI MCP client configs written"
else
log_warn "Could not sync shared MCP client configs (non-fatal). Run: node scripts/mcp/unity-mcp.mjs configure-shared"
fi
if node "${MCP_REPO_ROOT}/scripts/mcp/unity-mcp.mjs" configure --no-discover >/dev/null 2>&1; then
log_ok "Unity MCP client configs written"
else
record_failure_and_backoff
log_warn "Agent CLI verification failed (non-fatal). Re-run: bash .devcontainer/install-agent-clis.sh --force-latest-check"
log_warn "Could not sync Unity MCP client configs (non-fatal). Run: npm run unity:mcp:configure"
fi

exit 0
4 changes: 3 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,9 @@
"unredacted",
"unwritable",
"unanalyzed",
"denormal"
"denormal",
"zai",
"zread"
]
}
],
Expand Down
Loading
Loading