From 96ba8f8ca78cd1e98736af5b231e26329280caf7 Mon Sep 17 00:00:00 2001 From: Gordon Beeming Date: Wed, 24 Jun 2026 08:04:30 +1000 Subject: [PATCH 1/2] feat: add Claude Code as a CLI provider with published images Register Claude Code (@anthropic-ai/claude-code) as a selectable provider alongside GitHub Copilot, with its own claude-* Docker image family. - ClaudeCodeTool/ClaudeAuthProvider/ClaudeModelProvider + ToolRegistry entry - Auth reuses the host login: mounts ~/.claude and ~/.claude.json, and on macOS re-seeds ~/.claude/.credentials.json from the Keychain each run so the token stays valid (the host rotates it). ANTHROPIC_API_KEY and a setup-token CLAUDE_CODE_OAUTH_TOKEN take precedence. - Claude manages its own model selection, so the Copilot-oriented model.conf is not forced on it. - Airlock default rules are now per-tool; Claude allows the Anthropic hosts. - claude-cli snippet, 11 claude-* images.json entries and generated Dockerfiles. - publish.yml builds and tags the claude-* family (claude-latest is the default). - README/docs and unit tests. Co-authored-by: Claude Co-authored-by: GitButler --- .github/workflows/publish.yml | 71 +++- README.md | 33 +- app/Commands/Run/RunCommand.cs | 15 +- app/CopilotHere.csproj | 1 + app/Infrastructure/AirlockRunner.cs | 27 +- app/Infrastructure/AppPaths.cs | 20 ++ app/Infrastructure/ICliTool.cs | 16 + app/Infrastructure/ToolRegistry.cs | 1 + app/Tools/ClaudeAuthProvider.cs | 253 +++++++++++++++ app/Tools/ClaudeCodeTool.cs | 135 ++++++++ app/Tools/ClaudeModelProvider.cs | 41 +++ app/Tools/EchoTool.cs | 6 + app/Tools/GitHubCopilotTool.cs | 9 +- docker/generated/Dockerfile.claude-default | 89 ++++++ docker/generated/Dockerfile.claude-dotnet | 151 +++++++++ docker/generated/Dockerfile.claude-dotnet-10 | 125 ++++++++ docker/generated/Dockerfile.claude-dotnet-8 | 114 +++++++ docker/generated/Dockerfile.claude-dotnet-9 | 114 +++++++ .../Dockerfile.claude-dotnet-playwright | 192 +++++++++++ .../generated/Dockerfile.claude-dotnet-rust | 181 +++++++++++ docker/generated/Dockerfile.claude-golang | 118 +++++++ docker/generated/Dockerfile.claude-java | 154 +++++++++ docker/generated/Dockerfile.claude-playwright | 130 ++++++++ docker/generated/Dockerfile.claude-rust | 119 +++++++ docker/images.json | 44 +++ docker/snippets/claude-cli.Dockerfile | 6 + .../tools/claude/default-airlock-rules.json | 26 ++ .../claude/default-docker-broker-rules.json | 66 ++++ docs/ci-cd-pipeline.md | 2 + docs/docker-images.md | 9 + .../AirlockConfigTests.cs | 3 +- .../ClaudeCodeToolTests.cs | 302 ++++++++++++++++++ .../ContainerRuntimeConfigTests.cs | 1 + .../DockerBrokerConfigTests.cs | 3 +- tests/CopilotHere.UnitTests/EchoToolTests.cs | 1 + .../GitHubCopilotToolTests.cs | 1 + .../CopilotHere.UnitTests/ImageConfigTests.cs | 3 +- .../CopilotHere.UnitTests/ModelConfigTests.cs | 3 +- .../MountDeduplicationTests.cs | 3 +- .../MountsConfigTests.cs | 3 +- .../ToolRegistryTests.cs | 12 + 41 files changed, 2579 insertions(+), 24 deletions(-) create mode 100644 app/Tools/ClaudeAuthProvider.cs create mode 100644 app/Tools/ClaudeCodeTool.cs create mode 100644 app/Tools/ClaudeModelProvider.cs create mode 100644 docker/generated/Dockerfile.claude-default create mode 100644 docker/generated/Dockerfile.claude-dotnet create mode 100644 docker/generated/Dockerfile.claude-dotnet-10 create mode 100644 docker/generated/Dockerfile.claude-dotnet-8 create mode 100644 docker/generated/Dockerfile.claude-dotnet-9 create mode 100644 docker/generated/Dockerfile.claude-dotnet-playwright create mode 100644 docker/generated/Dockerfile.claude-dotnet-rust create mode 100644 docker/generated/Dockerfile.claude-golang create mode 100644 docker/generated/Dockerfile.claude-java create mode 100644 docker/generated/Dockerfile.claude-playwright create mode 100644 docker/generated/Dockerfile.claude-rust create mode 100644 docker/snippets/claude-cli.Dockerfile create mode 100644 docker/tools/claude/default-airlock-rules.json create mode 100644 docker/tools/claude/default-docker-broker-rules.json create mode 100644 tests/CopilotHere.UnitTests/ClaudeCodeToolTests.cs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c689801..3d0c2bd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -393,6 +393,7 @@ jobs: outputs: repo_name: ${{ steps.repo.outputs.name }} copilot_version: ${{ steps.versions.outputs.copilot_version }} + claude_version: ${{ steps.versions.outputs.claude_version }} playwright_version: ${{ steps.versions.outputs.playwright_version }} csharp_ls_version: ${{ steps.versions.outputs.csharp_ls_version }} dotnet_8_version: ${{ steps.versions.outputs.dotnet_8_version }} @@ -409,6 +410,9 @@ jobs: COPILOT_VERSION=$(npm view @github/copilot version) echo "copilot_version=$COPILOT_VERSION" >> $GITHUB_OUTPUT + CLAUDE_VERSION=$(npm view @anthropic-ai/claude-code version) + echo "claude_version=$CLAUDE_VERSION" >> $GITHUB_OUTPUT + PLAYWRIGHT_VERSION=$(npm view playwright version) echo "playwright_version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT @@ -469,6 +473,41 @@ jobs: - image: java dockerfile: ./docker/generated/Dockerfile.java build_args: "COPILOT_VERSION=$COPILOT_VERSION" + - image: claude-default + dockerfile: ./docker/generated/Dockerfile.claude-default + tags_extra: | + ghcr.io/${{ needs.prepare-versions.outputs.repo_name }}:claude-latest + build_args: "CLAUDE_VERSION=$CLAUDE_VERSION" + - image: claude-rust + dockerfile: ./docker/generated/Dockerfile.claude-rust + build_args: "CLAUDE_VERSION=$CLAUDE_VERSION" + - image: claude-golang + dockerfile: ./docker/generated/Dockerfile.claude-golang + build_args: "CLAUDE_VERSION=$CLAUDE_VERSION" + - image: claude-playwright + dockerfile: ./docker/generated/Dockerfile.claude-playwright + build_args: "CLAUDE_VERSION=$CLAUDE_VERSION\nPLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" + - image: claude-dotnet-8 + dockerfile: ./docker/generated/Dockerfile.claude-dotnet-8 + build_args: "CLAUDE_VERSION=$CLAUDE_VERSION\nDOTNET_SDK_8_VERSION=$DOTNET_8_VERSION" + - image: claude-dotnet-9 + dockerfile: ./docker/generated/Dockerfile.claude-dotnet-9 + build_args: "CLAUDE_VERSION=$CLAUDE_VERSION\nDOTNET_SDK_9_VERSION=$DOTNET_9_VERSION" + - image: claude-dotnet-10 + dockerfile: ./docker/generated/Dockerfile.claude-dotnet-10 + build_args: "CLAUDE_VERSION=$CLAUDE_VERSION\nDOTNET_SDK_10_VERSION=$DOTNET_10_VERSION\nCSHARP_LS_VERSION=$CSHARP_LS_VERSION" + - image: claude-dotnet + dockerfile: ./docker/generated/Dockerfile.claude-dotnet + build_args: "CLAUDE_VERSION=$CLAUDE_VERSION\nDOTNET_SDK_8_VERSION=$DOTNET_8_VERSION\nDOTNET_SDK_9_VERSION=$DOTNET_9_VERSION\nDOTNET_SDK_10_VERSION=$DOTNET_10_VERSION\nCSHARP_LS_VERSION=$CSHARP_LS_VERSION" + - image: claude-dotnet-playwright + dockerfile: ./docker/generated/Dockerfile.claude-dotnet-playwright + build_args: "CLAUDE_VERSION=$CLAUDE_VERSION\nDOTNET_SDK_8_VERSION=$DOTNET_8_VERSION\nDOTNET_SDK_9_VERSION=$DOTNET_9_VERSION\nDOTNET_SDK_10_VERSION=$DOTNET_10_VERSION\nPLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION\nCSHARP_LS_VERSION=$CSHARP_LS_VERSION" + - image: claude-dotnet-rust + dockerfile: ./docker/generated/Dockerfile.claude-dotnet-rust + build_args: "CLAUDE_VERSION=$CLAUDE_VERSION\nDOTNET_SDK_8_VERSION=$DOTNET_8_VERSION\nDOTNET_SDK_9_VERSION=$DOTNET_9_VERSION\nDOTNET_SDK_10_VERSION=$DOTNET_10_VERSION\nCSHARP_LS_VERSION=$CSHARP_LS_VERSION" + - image: claude-java + dockerfile: ./docker/generated/Dockerfile.claude-java + build_args: "CLAUDE_VERSION=$CLAUDE_VERSION" steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -491,6 +530,7 @@ jobs: run: | ARGS='${{ matrix.build_args }}' ARGS="${ARGS//\$COPILOT_VERSION/${{ needs.prepare-versions.outputs.copilot_version }}}" + ARGS="${ARGS//\$CLAUDE_VERSION/${{ needs.prepare-versions.outputs.claude_version }}}" ARGS="${ARGS//\$PLAYWRIGHT_VERSION/${{ needs.prepare-versions.outputs.playwright_version }}}" ARGS="${ARGS//\$DOTNET_8_VERSION/${{ needs.prepare-versions.outputs.dotnet_8_version }}}" ARGS="${ARGS//\$DOTNET_9_VERSION/${{ needs.prepare-versions.outputs.dotnet_9_version }}}" @@ -507,22 +547,35 @@ jobs: IMAGE="${{ matrix.image }}" SHA="${{ github.sha }}" - TAGS="ghcr.io/${REPO}:copilot-${IMAGE} - ghcr.io/${REPO}:copilot-${IMAGE}-sha-${SHA}" + # Claude images carry the tool in their matrix key (claude-); + # Copilot images use the bare variant (default, rust, …). Derive the + # published tag prefix and the variant from that. + if [[ "$IMAGE" == claude-* ]]; then + PRIMARY="claude-${IMAGE#claude-}" + else + PRIMARY="copilot-${IMAGE}" + fi - # Add variant shorthand tag (e.g., :rust, :dotnet) - if [[ "$IMAGE" != "default" ]]; then + TAGS="ghcr.io/${REPO}:${PRIMARY} + ghcr.io/${REPO}:${PRIMARY}-sha-${SHA}" + + # Add variant shorthand tag (e.g., :rust, :dotnet) for Copilot only. + # The bare :variant tags point at the Copilot images, so Claude does + # not claim them. + if [[ "$IMAGE" != "default" && "$IMAGE" != claude-* ]]; then TAGS="${TAGS} ghcr.io/${REPO}:${IMAGE}" fi - # Add extra tags (e.g., :latest for the default image) + # Add extra tags (e.g., :latest / :claude-latest for the default images) EXTRA_TAGS="${{ matrix.tags_extra }}" if [[ -n "$EXTRA_TAGS" ]]; then TAGS="${TAGS} ${EXTRA_TAGS}" fi + echo "cache_ref=ghcr.io/${REPO}:${PRIMARY}" >> $GITHUB_OUTPUT + echo "tags<> $GITHUB_OUTPUT echo "$TAGS" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT @@ -537,7 +590,7 @@ jobs: tags: ${{ steps.tags.outputs.tags }} labels: project=copilot_here build-args: ${{ steps.args.outputs.build_args }} - cache-from: type=registry,ref=ghcr.io/${{ needs.prepare-versions.outputs.repo_name }}:copilot-${{ matrix.image }},mode=max + cache-from: type=registry,ref=${{ steps.tags.outputs.cache_ref }},mode=max cache-to: type=inline # Full airlock + DinD smoke test. Spins up a real airlock compose project, @@ -689,6 +742,12 @@ jobs: echo "- \`dotnet-playwright\`, \`copilot-dotnet-playwright\`" >> $GITHUB_STEP_SUMMARY echo "- \`dotnet-rust\`, \`copilot-dotnet-rust\`" >> $GITHUB_STEP_SUMMARY echo "- \`java\`, \`copilot-java\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Claude Code variants (claude- prefix):**" >> $GITHUB_STEP_SUMMARY + echo "- \`claude-latest\` (default)" >> $GITHUB_STEP_SUMMARY + echo "- \`claude-rust\`, \`claude-golang\`, \`claude-playwright\`, \`claude-java\`" >> $GITHUB_STEP_SUMMARY + echo "- \`claude-dotnet\`, \`claude-dotnet-8\`, \`claude-dotnet-9\`, \`claude-dotnet-10\`" >> $GITHUB_STEP_SUMMARY + echo "- \`claude-dotnet-playwright\`, \`claude-dotnet-rust\`" >> $GITHUB_STEP_SUMMARY else echo "**Images:** No changes or skipped publishing" >> $GITHUB_STEP_SUMMARY fi diff --git a/README.md b/README.md index 4c28c15..9774258 100644 --- a/README.md +++ b/README.md @@ -407,7 +407,7 @@ A reasonable question once you've seen [Docker Sandboxes (`sbx`)](https://docs.d | Nested Docker | Opt-in brokered socket (`--dind`). Image allowlist, endpoint allowlist, body inspection | Each sandbox has its own isolated Docker engine built in | | Secrets | Reuses the host `gh` CLI credentials per run | `sbx secret set`; proxy injects headers so values never enter the VM | | Persistence | Ephemeral container, persisted *config* in `.copilot_here/` and `~/.config/copilot_here/` | Sandboxes persist across runs. Installed packages, images, and history survive restarts| -| Agents supported today| GitHub Copilot CLI. Multi-tool scaffolding is in-tree (`--set-tool`, `ToolRegistry`), more agents on the roadmap | `claude`, `codex`, `copilot`, `gemini`, `kiro`, `opencode`, `shell`, `docker-agent` | +| Agents supported today| GitHub Copilot CLI and Claude Code, switchable with `--set-tool`; more agents on the roadmap | `claude`, `codex`, `copilot`, `gemini`, `kiro`, `opencode`, `shell`, `docker-agent` | | Platforms | macOS, Linux, Windows (PowerShell 5.1 and 7+) | macOS, Linux, Windows | | Status | Open source, .NET 10 Native AOT | Docker-maintained, experimental | @@ -713,9 +713,38 @@ copilot_yolo --no-cleanup "generate a README for this project" ``` +## πŸ€– Choosing a CLI Provider + +`copilot_here` runs more than one AI coding CLI inside the sandbox. GitHub Copilot CLI is the default; Claude Code (`@anthropic-ai/claude-code`) is also built in. Switch between them with `--set-tool`: + +```bash +copilot_here --list-tools # list the installed providers +copilot_here --set-tool claude # switch globally +copilot_here --set-tool-local claude # switch for the current project only +copilot_here --show-tool # show the active provider +``` + +The active provider decides which image family runs and how your arguments reach the underlying CLI. + +### Authenticating Claude Code + +Claude Code reuses your host login, the way Copilot reuses your `gh` token. Log in once on your machine: + +```bash +claude # complete the OAuth login +``` + +`copilot_here` mounts `~/.claude` into the container so Claude Code reads your `~/.claude/.credentials.json` and refreshes the token in place, which means sessions run as long as your login stays valid. On Linux the host app already uses that file, so it's shared directly. macOS keeps the login in the Keychain instead, so each run re-seeds `~/.claude/.credentials.json` from the Keychain (full credentials including the refresh token, written `0600`) before launching. It re-seeds every run on purpose: your host app rotates the refresh token when it refreshes, and a one-time seed would quickly go stale and fail with a 401. Your host Claude app keeps using the Keychain throughout. + +There's one trade-off to know about: because the sandbox and your host share the same login, a refresh triggered inside a long sandbox session rotates the token server-side and can leave the Keychain copy stale, so your host Claude occasionally needs a one-off `claude` re-login. To sidestep all of that, set `ANTHROPIC_API_KEY` in your environment; it takes precedence and never expires. A long-lived `CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token` is also honored if set. + +Your `~/.claude.json` (model preference, MCP servers, settings) is mounted too, so the sandbox runs with your real Claude setup. Claude Code manages its own model through that config and its `/model` picker, so it ignores the shared `--set-model`/`model.conf` value. Pick a Claude model with `/model` inside a session, or pass `--model ` for a single run. + +Under airlock, Claude's default rules allow `api.anthropic.com`, `console.anthropic.com`, and `statsig.anthropic.com`. These are a starting point and may need tweaking with `--edit-airlock-rules` for MCP servers or other hosts your workflow reaches. + ## 🐳 Docker Image Variants -This project provides multiple Docker image variants for different development scenarios. All images include the GitHub Copilot CLI and inherit the base security and authentication features. +This project provides multiple Docker image variants for different development scenarios. Each variant is published for both providers: the `copilot-*` family ships the GitHub Copilot CLI, and the matching `claude-*` family ships Claude Code. The variant you select stays the same; the active provider picks the right family. All images inherit the base security and authentication features. ### Available Images diff --git a/app/Commands/Run/RunCommand.cs b/app/Commands/Run/RunCommand.cs index 0762a4a..d87e6ad 100644 --- a/app/Commands/Run/RunCommand.cs +++ b/app/Commands/Run/RunCommand.cs @@ -369,8 +369,12 @@ public void Configure(RootCommand root) var imageName = ctx.ActiveTool.GetImageName(imageTag); DebugLogger.Log($"Selected image: {imageName}"); - // Determine model (CLI overrides config) - var effectiveModel = model ?? ctx.ModelConfig.Model; + // Determine model (CLI overrides config). Tools that keep their own model + // preference (e.g. Claude Code) ignore the persisted model.conf β€” its + // values target another provider and would be rejected β€” but an explicit + // per-run --model still applies. + var configModel = ctx.ActiveTool.ManagesOwnModelSelection ? null : ctx.ModelConfig.Model; + var effectiveModel = model ?? configModel; if (!string.IsNullOrEmpty(effectiveModel)) { if (!ctx.ActiveTool.SupportsModels) @@ -840,6 +844,13 @@ internal static List BuildDockerArgs( "-e", $"COPILOT_HERE_SESSION_INFO={sessionInfo}" }; + // Tool-specific extra config mounts (e.g. Claude Code's ~/.claude.json). + foreach (var (hostPath, containerPath) in ctx.ActiveTool.GetAdditionalConfigMounts(ctx.Paths)) + { + args.Add("-v"); + args.Add($"{ConvertToDockerPath(hostPath)}:{containerPath}"); + } + // Brokered Docker socket: mount the host-side broker UDS into the container, // or expose it via host.docker.internal on Windows where TCP is the only option. if (broker is not null) diff --git a/app/CopilotHere.csproj b/app/CopilotHere.csproj index 7befe41..df9dede 100644 --- a/app/CopilotHere.csproj +++ b/app/CopilotHere.csproj @@ -58,6 +58,7 @@ + diff --git a/app/Infrastructure/AirlockRunner.cs b/app/Infrastructure/AirlockRunner.cs index efc6e1c..a1583b1 100644 --- a/app/Infrastructure/AirlockRunner.cs +++ b/app/Infrastructure/AirlockRunner.cs @@ -135,7 +135,7 @@ public static int Run( // needs host.docker.internal in the airlock allowlist: it talks to // tcp://proxy:2375 directly, and the proxy container forwards to the // host broker via its own external network leg (see proxy-entrypoint.sh). - var processedConfigPath = ProcessNetworkConfig(rulesPath, ctx.Paths); + var processedConfigPath = ProcessNetworkConfig(rulesPath, ctx.Paths, ctx.ActiveTool); if (processedConfigPath is null) { Console.WriteLine("❌ Failed to process network config"); @@ -210,15 +210,22 @@ private static string GenerateSessionId() } /// - /// Gets the embedded default airlock rules from assembly resources. + /// Gets the embedded default airlock rules for the active tool. Each provider + /// allows a different set of hosts (GitHub for Copilot, Anthropic for Claude), + /// so the inherited defaults must follow the active tool β€” otherwise switching + /// tools leaves the wrong allowlist and the API host is rejected. /// - private static NetworkConfig? GetDefaultRules() + private static NetworkConfig? GetDefaultRules(ICliTool tool) { try { var assembly = Assembly.GetExecutingAssembly(); - var resourceName = "CopilotHere.Resources.github-copilot-default-airlock-rules.json"; - + var resourceName = tool.Name switch + { + "claude" => "CopilotHere.Resources.claude-default-airlock-rules.json", + _ => "CopilotHere.Resources.github-copilot-default-airlock-rules.json", + }; + using var stream = assembly.GetManifestResourceStream(resourceName); if (stream is null) return null; @@ -232,7 +239,7 @@ private static string GenerateSessionId() } } - private static string? ProcessNetworkConfig(string rulesPath, AppPaths paths) + private static string? ProcessNetworkConfig(string rulesPath, AppPaths paths, ICliTool tool) { try { @@ -244,7 +251,7 @@ private static string GenerateSessionId() // If inherit_default_rules is true, merge with default rules if (userConfig.InheritDefaultRules) { - var defaultRules = GetDefaultRules(); + var defaultRules = GetDefaultRules(tool); if (defaultRules is not null) { // Add default rules that don't conflict with user rules @@ -347,6 +354,12 @@ private static void SetupLogsDirectory(AppPaths paths, string rulesPath) extraMounts.AppendLine($" - {composePath}:{containerPath}:{mode}"); } + // Tool-specific extra config mounts (e.g. Claude Code's ~/.claude.json). + foreach (var (hostPath, containerPath) in ctx.ActiveTool.GetAdditionalConfigMounts(ctx.Paths)) + { + extraMounts.AppendLine($" - {ConvertToComposePath(hostPath)}:{containerPath}"); + } + // Build Docker broker substitutions for DinD on the airlock network. // // Two transport flavours depending on whether the host broker listens diff --git a/app/Infrastructure/AppPaths.cs b/app/Infrastructure/AppPaths.cs index 21b4ab4..f06d12d 100644 --- a/app/Infrastructure/AppPaths.cs +++ b/app/Infrastructure/AppPaths.cs @@ -15,6 +15,13 @@ public sealed record AppPaths /// Path to copilot CLI config directory (~/.config/copilot-cli-docker). public required string CopilotConfigPath { get; init; } + /// + /// Path to the host's Claude Code config directory (~/.claude). Unlike the + /// Copilot path, this is the user's real Claude directory, so an existing + /// `claude` login carries into the sandbox via the bind mount. + /// + public required string ClaudeConfigPath { get; init; } + /// Path to local .copilot_here config directory. public required string LocalConfigPath { get; init; } @@ -37,6 +44,7 @@ public static AppPaths Resolve() var globalConfigPath = Path.Combine(userHome, ".config", "copilot_here"); var copilotConfigPath = Path.Combine(userHome, ".config", "copilot-cli-docker"); + var claudeConfigPath = Path.Combine(userHome, ".claude"); // Ensure config directories exist try @@ -61,6 +69,17 @@ public static AppPaths Resolve() Console.Error.WriteLine($" Copilot session data may not persist."); } + try + { + Directory.CreateDirectory(claudeConfigPath); + } + catch (Exception ex) + { + Console.Error.WriteLine($"⚠️ Warning: Cannot create Claude config directory: {claudeConfigPath}"); + Console.Error.WriteLine($" {ex.Message}"); + Console.Error.WriteLine($" Claude Code session data may not persist."); + } + // Calculate container work directory string containerWorkDir; if (currentDir.StartsWith(userHome)) @@ -86,6 +105,7 @@ public static AppPaths Resolve() CurrentDirectory = currentDir, UserHome = userHome, CopilotConfigPath = copilotConfigPath, + ClaudeConfigPath = claudeConfigPath, LocalConfigPath = Path.Combine(currentDir, ".copilot_here"), GlobalConfigPath = globalConfigPath, ContainerWorkDir = containerWorkDir diff --git a/app/Infrastructure/ICliTool.cs b/app/Infrastructure/ICliTool.cs index 0fee35e..8da40c1 100644 --- a/app/Infrastructure/ICliTool.cs +++ b/app/Infrastructure/ICliTool.cs @@ -73,6 +73,14 @@ public interface ICliTool /// Absolute container path (e.g., "/home/appuser/.copilot") string GetContainerConfigPath(); + /// + /// Additional hostβ†’container bind mounts beyond the primary config directory + /// (e.g. Claude Code's ~/.claude.json, which lives in HOME rather than inside + /// ~/.claude). Only existing host paths are returned, so a missing file is + /// skipped instead of being mounted as an empty directory. + /// + IReadOnlyList<(string HostPath, string ContainerPath)> GetAdditionalConfigMounts(AppPaths paths); + /// /// Gets the authentication provider for this tool /// @@ -100,6 +108,14 @@ public interface ICliTool /// bool SupportsModels { get; } + /// + /// Indicates the tool keeps its own persistent model preference (e.g. Claude + /// Code's own config and /model picker). When true, the shared model.conf is + /// NOT auto-passed as --model β€” its values are oriented at another provider + /// and would be rejected. An explicit per-run --model still applies. + /// + bool ManagesOwnModelSelection { get; } + /// /// Indicates if the tool supports YOLO mode (unrestricted access) /// diff --git a/app/Infrastructure/ToolRegistry.cs b/app/Infrastructure/ToolRegistry.cs index b5b8df0..2276201 100644 --- a/app/Infrastructure/ToolRegistry.cs +++ b/app/Infrastructure/ToolRegistry.cs @@ -10,6 +10,7 @@ public static class ToolRegistry private static readonly Dictionary> _tools = new() { ["github-copilot"] = new Lazy(() => new GitHubCopilotTool()), + ["claude"] = new Lazy(() => new ClaudeCodeTool()), ["echo"] = new Lazy(() => new EchoTool()), }; diff --git a/app/Tools/ClaudeAuthProvider.cs b/app/Tools/ClaudeAuthProvider.cs new file mode 100644 index 0000000..e505ff3 --- /dev/null +++ b/app/Tools/ClaudeAuthProvider.cs @@ -0,0 +1,253 @@ +using System.Diagnostics; +using System.Text.Json; +using CopilotHere.Infrastructure; + +namespace CopilotHere.Tools; + +/// +/// Authentication provider for Claude Code. +/// +/// The sandbox reuses your real host login so sessions can run long. Claude Code +/// reads OAuth credentials from ~/.claude/.credentials.json and refreshes them in +/// place using the stored refresh token. We mount the host ~/.claude into the +/// container, so on Linux that file is shared directly and refresh just works. +/// +/// macOS keeps the login in the Keychain rather than a file, so a bind-mount +/// carries nothing on its own. We seed ~/.claude/.credentials.json from the +/// Keychain (full credentials, including the refresh token) so the container has +/// a self-refreshing copy. We deliberately do NOT also export +/// CLAUDE_CODE_OAUTH_TOKEN, because an explicit token would override the file and +/// disable refresh β€” capping sessions at the access token's short lifetime. +/// +/// An explicit ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in the environment +/// still wins, for anyone who prefers API-key billing or a long-lived +/// `claude setup-token`. +/// +public sealed class ClaudeAuthProvider : IAuthProvider +{ + private const string ApiKeyEnvVar = "ANTHROPIC_API_KEY"; + private const string OAuthTokenEnvVar = "CLAUDE_CODE_OAUTH_TOKEN"; + private const string KeychainService = "Claude Code-credentials"; + + public (bool isValid, string? error) ValidateAuth() + { + DebugLogger.Log("ClaudeAuthProvider.ValidateAuth called"); + + // Skip validation in test mode + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("COPILOT_HERE_TEST_MODE"))) + { + DebugLogger.Log("Test mode detected, skipping validation"); + return (true, null); + } + + if (HasEnv(ApiKeyEnvVar)) + { + DebugLogger.Log("ANTHROPIC_API_KEY present"); + return (true, null); + } + + if (HasEnv(OAuthTokenEnvVar)) + { + DebugLogger.Log("CLAUDE_CODE_OAUTH_TOKEN present"); + return (true, null); + } + + if (EnsureHostCredentialsAvailable()) + { + DebugLogger.Log("Host Claude Code credentials available for the sandbox"); + return (true, null); + } + + return (false, + "❌ Claude Code is not authenticated.\n" + + " Log in once on the host with `claude` (the sandbox reuses that login),\n" + + " or set ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN in your environment."); + } + + public string GetToken() + { + return Environment.GetEnvironmentVariable(ApiKeyEnvVar) + ?? Environment.GetEnvironmentVariable(OAuthTokenEnvVar) + ?? string.Empty; + } + + public string[] GetRequiredScopes() + { + // Claude Code has no scope concept like the gh token does. + return []; + } + + public string GetElevateTokenCommand() + { + return "claude # run once on the host to log in, or set ANTHROPIC_API_KEY"; + } + + public Dictionary GetEnvironmentVars() + { + var vars = new Dictionary(); + + // An explicit API key or OAuth token wins and passes straight through. + var apiKey = Environment.GetEnvironmentVariable(ApiKeyEnvVar); + if (!string.IsNullOrEmpty(apiKey)) + { + vars[ApiKeyEnvVar] = apiKey; + return vars; + } + + var oauthEnv = Environment.GetEnvironmentVariable(OAuthTokenEnvVar); + if (!string.IsNullOrEmpty(oauthEnv)) + { + vars[OAuthTokenEnvVar] = oauthEnv; + return vars; + } + + // Otherwise make sure the mounted ~/.claude has the full credentials file so + // the container can authenticate and refresh on its own. No token env var is + // exported β€” that would shadow the file and stop refresh. + EnsureHostCredentialsAvailable(); + return vars; + } + + // === PRIVATE HELPER METHODS === + + private static bool HasEnv(string name) => + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(name)); + + private static string? CredentialsFilePath() + { + var userHome = Environment.GetEnvironmentVariable("HOME") + ?? Environment.GetEnvironmentVariable("USERPROFILE") + ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + return string.IsNullOrEmpty(userHome) + ? null + : Path.Combine(userHome, ".claude", ".credentials.json"); + } + + /// + /// Ensures ~/.claude/.credentials.json holds usable credentials for the + /// container. + /// + /// On Linux the host app uses that file directly, so it's already the shared, + /// self-refreshing store β€” we just confirm it exists. + /// + /// On macOS the login lives in the Keychain, which the host app keeps fresh + /// (including rotating the refresh token). We therefore re-seed the file from + /// the Keychain on every run, not just once: a stale seed would carry a + /// refresh token the host has already rotated away, and the container would + /// fail with a 401. We only rewrite when the contents actually changed. + /// Returns true when credentials are available to the sandbox. + /// + private static bool EnsureHostCredentialsAvailable() + { + var path = CredentialsFilePath(); + if (path is null) + { + return false; + } + + if (!OperatingSystem.IsMacOS()) + { + // Linux/Windows store the login in the file; if it's absent the user + // hasn't logged in. + return File.Exists(path); + } + + var json = ReadKeychainCredentials(); + if (string.IsNullOrEmpty(json) || ExtractAccessToken(json) is null) + { + // Keychain unavailable (locked, no login) β€” fall back to any existing file. + return File.Exists(path); + } + + try + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + if (!File.Exists(path) || File.ReadAllText(path) != json) + { + File.WriteAllText(path, json); + TryRestrictPermissions(path); + DebugLogger.Log("Refreshed ~/.claude/.credentials.json from the macOS Keychain"); + } + return true; + } + catch (Exception ex) + { + DebugLogger.Log($"Failed to write credentials file: {ex.Message}"); + // A previous good seed is better than nothing. + return File.Exists(path); + } + } + + private static void TryRestrictPermissions(string path) + { + if (OperatingSystem.IsWindows()) + { + return; + } + + try + { + // Owner read/write only β€” the file holds an OAuth refresh token. + File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite); + } + catch (Exception ex) + { + DebugLogger.Log($"Could not tighten credentials file permissions: {ex.Message}"); + } + } + + private static string? ReadKeychainCredentials() + { + try + { + var startInfo = new ProcessStartInfo("security", + $"find-generic-password -s \"{KeychainService}\" -w") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process is null) return null; + + var stdout = process.StandardOutput.ReadToEnd(); + process.StandardError.ReadToEnd(); + process.WaitForExit(); + + return process.ExitCode == 0 ? stdout.Trim() : null; + } + catch (Exception ex) + { + DebugLogger.Log($"Keychain read failed: {ex.Message}"); + return null; + } + } + + /// + /// Both the Keychain payload and the Linux credentials file use the shape + /// { "claudeAiOauth": { "accessToken": "...", "refreshToken": "...", ... } }. + /// Used only to confirm the payload is the login we expect before writing it. + /// + private static string? ExtractAccessToken(string json) + { + try + { + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("claudeAiOauth", out var oauth) + && oauth.TryGetProperty("accessToken", out var token) + && token.ValueKind == JsonValueKind.String) + { + return token.GetString(); + } + } + catch (JsonException ex) + { + DebugLogger.Log($"Credentials JSON parse failed: {ex.Message}"); + } + + return null; + } +} diff --git a/app/Tools/ClaudeCodeTool.cs b/app/Tools/ClaudeCodeTool.cs new file mode 100644 index 0000000..5fcff70 --- /dev/null +++ b/app/Tools/ClaudeCodeTool.cs @@ -0,0 +1,135 @@ +using CopilotHere.Infrastructure; + +namespace CopilotHere.Tools; + +/// +/// Claude Code CLI tool provider (Anthropic's @anthropic-ai/claude-code). +/// +public sealed class ClaudeCodeTool : ICliTool +{ + private readonly IAuthProvider _authProvider = new ClaudeAuthProvider(); + private readonly IModelProvider _modelProvider = new ClaudeModelProvider(); + + public string Name => "claude"; + + public string DisplayName => "Claude Code"; + + public string GetImageName(string tag) + { + // If the tag is an absolute image reference (contains '/'), use it as-is + if (ContainerRunner.IsAbsoluteImageReference(tag)) + return tag; + + // Image name format: ghcr.io/gordonbeeming/copilot_here:claude-{variant} + // Tag matches what users invoke: "claude-dotnet", "claude-latest", etc. + const string imagePrefix = "ghcr.io/gordonbeeming/copilot_here"; + var imageTag = string.IsNullOrEmpty(tag) ? "claude-latest" : $"claude-{tag}"; + return $"{imagePrefix}:{imageTag}"; + } + + public string GetDockerfile() + { + return "docker/tools/claude/Dockerfile"; + } + + public List BuildCommand(CommandContext context) + { + var args = new List { "claude" }; + + // YOLO mode: skip all permission prompts inside the sandbox + if (context.IsYolo) + { + args.Add("--dangerously-skip-permissions"); + } + + // Add model if specified (Claude Code accepts aliases like "opus" or full IDs) + if (!string.IsNullOrEmpty(context.Model)) + { + args.Add("--model"); + args.Add(context.Model); + } + + // Add user arguments. With no further args, `claude` launches its + // interactive TUI, so there is no separate interactive/banner flag to add. + args.AddRange(context.UserArgs); + + return args; + } + + public List GetYoloModeFlags() + { + return ["--dangerously-skip-permissions"]; + } + + public string? GetInteractiveFlag() + { + // Claude Code is interactive by default when invoked with no prompt args. + return null; + } + + public IAuthProvider GetAuthProvider() + { + return _authProvider; + } + + public IModelProvider GetModelProvider() + { + return _modelProvider; + } + + public string[] GetRequiredDependencies() + { + // Auth flows through the mounted ~/.claude credentials and/or + // ANTHROPIC_API_KEY, so the only host dependency is the container runtime. + return ["docker"]; + } + + public string GetConfigDirName() + { + return ".claude"; + } + + public string? GetSessionDataPath() + { + return null; + } + + public string GetHostConfigPath(AppPaths paths) + { + return paths.ClaudeConfigPath; + } + + public string GetContainerConfigPath() + { + return "/home/appuser/.claude"; + } + + public IReadOnlyList<(string HostPath, string ContainerPath)> GetAdditionalConfigMounts(AppPaths paths) + { + // Claude Code's main config (model preference, MCP servers, settings) lives + // in ~/.claude.json β€” a sibling of ~/.claude, not inside it β€” so it needs its + // own mount. Skip it when absent so we never create a stray host file. + var hostJson = Path.Combine(paths.UserHome, ".claude.json"); + if (File.Exists(hostJson)) + { + return [(hostJson, "/home/appuser/.claude.json")]; + } + + return []; + } + + public string GetDefaultNetworkRulesPath() + { + return "docker/tools/claude/default-airlock-rules.json"; + } + + public bool SupportsModels => true; + + // Claude Code persists its own model choice (its config + /model picker), so + // don't push the shared model.conf at it β€” those values target Copilot. + public bool ManagesOwnModelSelection => true; + + public bool SupportsYoloMode => true; + + public bool SupportsInteractiveMode => true; +} diff --git a/app/Tools/ClaudeModelProvider.cs b/app/Tools/ClaudeModelProvider.cs new file mode 100644 index 0000000..415fb51 --- /dev/null +++ b/app/Tools/ClaudeModelProvider.cs @@ -0,0 +1,41 @@ +using CopilotHere.Infrastructure; + +namespace CopilotHere.Tools; + +/// +/// Model provider for Claude Code. +/// Claude Code accepts both short aliases (opus/sonnet/haiku) and fully-qualified +/// model IDs, and has no Copilot-style "pass an invalid model to list the valid +/// ones" probe, so the list is a curated static set rather than a container query. +/// +public sealed class ClaudeModelProvider : IModelProvider +{ + // Aliases resolve to Anthropic's current models; full IDs are accepted too. + private static readonly List KnownModels = + [ + "opus", + "sonnet", + "haiku", + "claude-opus-4-8", + "claude-sonnet-4-6", + "claude-haiku-4-5", + ]; + + public Task> ListAvailableModels(Infrastructure.AppContext ctx) + { + DebugLogger.Log("ClaudeModelProvider.ListAvailableModels called"); + return Task.FromResult(new List(KnownModels)); + } + + public (bool isValid, string? error) ValidateModel(string model) + { + // Don't pre-reject unknown IDs β€” Anthropic ships new models between releases, + // and Claude Code validates the model itself at runtime. Only block empties. + if (string.IsNullOrWhiteSpace(model)) + { + return (false, "Model name cannot be empty"); + } + + return (true, null); + } +} diff --git a/app/Tools/EchoTool.cs b/app/Tools/EchoTool.cs index ab84503..7616093 100644 --- a/app/Tools/EchoTool.cs +++ b/app/Tools/EchoTool.cs @@ -171,6 +171,11 @@ public string GetContainerConfigPath() return "/home/appuser/.copilot"; } + public IReadOnlyList<(string HostPath, string ContainerPath)> GetAdditionalConfigMounts(Infrastructure.AppPaths paths) + { + return []; + } + public Infrastructure.IAuthProvider GetAuthProvider() { return new EchoAuthProvider(); @@ -192,6 +197,7 @@ public string GetDefaultNetworkRulesPath() } public bool SupportsModels => true; + public bool ManagesOwnModelSelection => false; public bool SupportsYoloMode => true; public bool SupportsInteractiveMode => true; } diff --git a/app/Tools/GitHubCopilotTool.cs b/app/Tools/GitHubCopilotTool.cs index 5e0ac4d..4f03114 100644 --- a/app/Tools/GitHubCopilotTool.cs +++ b/app/Tools/GitHubCopilotTool.cs @@ -112,13 +112,20 @@ public string GetContainerConfigPath() return "/home/appuser/.copilot"; } + public IReadOnlyList<(string HostPath, string ContainerPath)> GetAdditionalConfigMounts(AppPaths paths) + { + return []; + } + public string GetDefaultNetworkRulesPath() { return "docker/tools/github-copilot/default-airlock-rules.json"; } public bool SupportsModels => true; - + + public bool ManagesOwnModelSelection => false; + public bool SupportsYoloMode => true; public bool SupportsInteractiveMode => true; diff --git a/docker/generated/Dockerfile.claude-default b/docker/generated/Dockerfile.claude-default new file mode 100644 index 0000000..9518b6a --- /dev/null +++ b/docker/generated/Dockerfile.claude-default @@ -0,0 +1,89 @@ +# Auto-generated from docker/images.json - DO NOT EDIT MANUALLY +# To modify, edit docker/snippets/*.Dockerfile or docker/images.json +# then run: pwsh docker/generate-dockerfiles.ps1 + +# Use a slim Node.js base image, which gives us `npm`. +FROM node:20-slim + +# --- snippet: system-packages --- +# Set non-interactive frontend to avoid prompts during package installation. +ENV DEBIAN_FRONTEND=noninteractive + +# Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + curl \ + git \ + gosu \ + gpg \ + nano \ + software-properties-common \ + wget \ + xdg-utils \ + zsh \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: docker-cli --- +# Install Docker CLI to support brokered Docker socket scenarios. +# Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: powershell --- +# Install PowerShell - architecture-specific approach +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "amd64" ]; then \ + # AMD64: Use Microsoft's APT repository + wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y powershell \ + && rm -rf /var/lib/apt/lists/*; \ + elif [ "$ARCH" = "arm64" ]; then \ + # ARM64: Download and install from GitHub releases + wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/powershell-7.4.6-linux-arm64.tar.gz -O /tmp/powershell.tar.gz \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 \ + && chmod +x /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \ + && rm /tmp/powershell.tar.gz; \ + fi + +# --- snippet: lsp-typescript --- +# Install TypeScript Language Server for code intelligence +ARG TYPESCRIPT_VERSION=latest +ARG TYPESCRIPT_LANGUAGE_SERVER_VERSION=latest +RUN npm install -g typescript@${TYPESCRIPT_VERSION} typescript-language-server@${TYPESCRIPT_LANGUAGE_SERVER_VERSION} + +# Write LSP config fragment for TypeScript +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "fileExtensions": { ".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" } } } }' \ + > /etc/copilot/lsp-config.d/typescript.json + +# --- snippet: claude-cli --- +# ARG for the Claude Code CLI version - passed from build process +# This ensures cache invalidation when a new version is available +ARG CLAUDE_VERSION=latest + +# Install Anthropic's Claude Code CLI via npm. +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION} + +# --- snippet: entrypoint --- +# Set the working directory for the container. +WORKDIR /work + +# Copy the entrypoint script into the container and make it executable. +COPY docker/shared/entrypoint.sh /usr/local/bin/ +COPY docker/shared/entrypoint-airlock.sh /usr/local/bin/ +COPY docker/session-info.sh /usr/local/bin/session-info +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-airlock.sh /usr/local/bin/session-info + +# The entrypoint script will handle user creation and command execution. +ENTRYPOINT [ "entrypoint.sh" ] + +# The default command to run if none is provided. +CMD [ "copilot", "--banner" ] diff --git a/docker/generated/Dockerfile.claude-dotnet b/docker/generated/Dockerfile.claude-dotnet new file mode 100644 index 0000000..73094eb --- /dev/null +++ b/docker/generated/Dockerfile.claude-dotnet @@ -0,0 +1,151 @@ +# Auto-generated from docker/images.json - DO NOT EDIT MANUALLY +# To modify, edit docker/snippets/*.Dockerfile or docker/images.json +# then run: pwsh docker/generate-dockerfiles.ps1 + +# Use a slim Node.js base image, which gives us `npm`. +FROM node:20-slim + +# --- snippet: system-packages --- +# Set non-interactive frontend to avoid prompts during package installation. +ENV DEBIAN_FRONTEND=noninteractive + +# Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + curl \ + git \ + gosu \ + gpg \ + nano \ + software-properties-common \ + wget \ + xdg-utils \ + zsh \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: docker-cli --- +# Install Docker CLI to support brokered Docker socket scenarios. +# Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: powershell --- +# Install PowerShell - architecture-specific approach +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "amd64" ]; then \ + # AMD64: Use Microsoft's APT repository + wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y powershell \ + && rm -rf /var/lib/apt/lists/*; \ + elif [ "$ARCH" = "arm64" ]; then \ + # ARM64: Download and install from GitHub releases + wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/powershell-7.4.6-linux-arm64.tar.gz -O /tmp/powershell.tar.gz \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 \ + && chmod +x /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \ + && rm /tmp/powershell.tar.gz; \ + fi + +# --- snippet: dotnet-prereqs --- +# Install .NET SDK prerequisites and ICU libraries +RUN apt-get update && apt-get install -y \ + wget \ + ca-certificates \ + libicu-dev \ + && rm -rf /var/lib/apt/lists/* + +# Add .NET to PATH +ENV PATH="/usr/share/dotnet:${PATH}" +ENV DOTNET_ROOT="/usr/share/dotnet" + +# --- snippet: dotnet8 --- +ARG DOTNET_SDK_8_VERSION + +# Install .NET 8 SDK (latest) +RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && if [ -z "$DOTNET_SDK_8_VERSION" ]; then \ + ./dotnet-install.sh --channel 8.0 --install-dir /usr/share/dotnet; \ + else \ + ./dotnet-install.sh --version $DOTNET_SDK_8_VERSION --install-dir /usr/share/dotnet; \ + fi \ + && rm dotnet-install.sh + +# --- snippet: dotnet9 --- +ARG DOTNET_SDK_9_VERSION + +# Install .NET 9 SDK (latest) +RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && if [ -z "$DOTNET_SDK_9_VERSION" ]; then \ + ./dotnet-install.sh --channel 9.0 --install-dir /usr/share/dotnet; \ + else \ + ./dotnet-install.sh --version $DOTNET_SDK_9_VERSION --install-dir /usr/share/dotnet; \ + fi \ + && rm dotnet-install.sh + +# --- snippet: dotnet10 --- +ARG DOTNET_SDK_10_VERSION + +# Install .NET 10 SDK (latest) +RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && if [ -z "$DOTNET_SDK_10_VERSION" ]; then \ + ./dotnet-install.sh --channel 10.0 --install-dir /usr/share/dotnet; \ + else \ + ./dotnet-install.sh --version $DOTNET_SDK_10_VERSION --install-dir /usr/share/dotnet; \ + fi \ + && rm dotnet-install.sh + +# --- snippet: lsp-typescript --- +# Install TypeScript Language Server for code intelligence +ARG TYPESCRIPT_VERSION=latest +ARG TYPESCRIPT_LANGUAGE_SERVER_VERSION=latest +RUN npm install -g typescript@${TYPESCRIPT_VERSION} typescript-language-server@${TYPESCRIPT_LANGUAGE_SERVER_VERSION} + +# Write LSP config fragment for TypeScript +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "fileExtensions": { ".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" } } } }' \ + > /etc/copilot/lsp-config.d/typescript.json + +# --- snippet: lsp-csharp --- +# Install C# Language Server for code intelligence +# Install directly to a shared location so appuser can access it +ARG CSHARP_LS_VERSION=0.22.0 +RUN dotnet tool install csharp-ls --tool-path /usr/local/bin --version ${CSHARP_LS_VERSION} + +# Write LSP config fragment for C# +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "csharp": { "command": "csharp-ls", "args": [], "fileExtensions": { ".cs": "csharp", ".csx": "csharp" } } } }' \ + > /etc/copilot/lsp-config.d/csharp.json + +# --- snippet: claude-cli --- +# ARG for the Claude Code CLI version - passed from build process +# This ensures cache invalidation when a new version is available +ARG CLAUDE_VERSION=latest + +# Install Anthropic's Claude Code CLI via npm. +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION} + +# --- snippet: entrypoint --- +# Set the working directory for the container. +WORKDIR /work + +# Copy the entrypoint script into the container and make it executable. +COPY docker/shared/entrypoint.sh /usr/local/bin/ +COPY docker/shared/entrypoint-airlock.sh /usr/local/bin/ +COPY docker/session-info.sh /usr/local/bin/session-info +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-airlock.sh /usr/local/bin/session-info + +# The entrypoint script will handle user creation and command execution. +ENTRYPOINT [ "entrypoint.sh" ] + +# The default command to run if none is provided. +CMD [ "copilot", "--banner" ] diff --git a/docker/generated/Dockerfile.claude-dotnet-10 b/docker/generated/Dockerfile.claude-dotnet-10 new file mode 100644 index 0000000..e9cc602 --- /dev/null +++ b/docker/generated/Dockerfile.claude-dotnet-10 @@ -0,0 +1,125 @@ +# Auto-generated from docker/images.json - DO NOT EDIT MANUALLY +# To modify, edit docker/snippets/*.Dockerfile or docker/images.json +# then run: pwsh docker/generate-dockerfiles.ps1 + +# Use a slim Node.js base image, which gives us `npm`. +FROM node:20-slim + +# --- snippet: system-packages --- +# Set non-interactive frontend to avoid prompts during package installation. +ENV DEBIAN_FRONTEND=noninteractive + +# Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + curl \ + git \ + gosu \ + gpg \ + nano \ + software-properties-common \ + wget \ + xdg-utils \ + zsh \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: docker-cli --- +# Install Docker CLI to support brokered Docker socket scenarios. +# Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: powershell --- +# Install PowerShell - architecture-specific approach +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "amd64" ]; then \ + # AMD64: Use Microsoft's APT repository + wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y powershell \ + && rm -rf /var/lib/apt/lists/*; \ + elif [ "$ARCH" = "arm64" ]; then \ + # ARM64: Download and install from GitHub releases + wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/powershell-7.4.6-linux-arm64.tar.gz -O /tmp/powershell.tar.gz \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 \ + && chmod +x /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \ + && rm /tmp/powershell.tar.gz; \ + fi + +# --- snippet: dotnet-prereqs --- +# Install .NET SDK prerequisites and ICU libraries +RUN apt-get update && apt-get install -y \ + wget \ + ca-certificates \ + libicu-dev \ + && rm -rf /var/lib/apt/lists/* + +# Add .NET to PATH +ENV PATH="/usr/share/dotnet:${PATH}" +ENV DOTNET_ROOT="/usr/share/dotnet" + +# --- snippet: dotnet10 --- +ARG DOTNET_SDK_10_VERSION + +# Install .NET 10 SDK (latest) +RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && if [ -z "$DOTNET_SDK_10_VERSION" ]; then \ + ./dotnet-install.sh --channel 10.0 --install-dir /usr/share/dotnet; \ + else \ + ./dotnet-install.sh --version $DOTNET_SDK_10_VERSION --install-dir /usr/share/dotnet; \ + fi \ + && rm dotnet-install.sh + +# --- snippet: lsp-typescript --- +# Install TypeScript Language Server for code intelligence +ARG TYPESCRIPT_VERSION=latest +ARG TYPESCRIPT_LANGUAGE_SERVER_VERSION=latest +RUN npm install -g typescript@${TYPESCRIPT_VERSION} typescript-language-server@${TYPESCRIPT_LANGUAGE_SERVER_VERSION} + +# Write LSP config fragment for TypeScript +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "fileExtensions": { ".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" } } } }' \ + > /etc/copilot/lsp-config.d/typescript.json + +# --- snippet: lsp-csharp --- +# Install C# Language Server for code intelligence +# Install directly to a shared location so appuser can access it +ARG CSHARP_LS_VERSION=0.22.0 +RUN dotnet tool install csharp-ls --tool-path /usr/local/bin --version ${CSHARP_LS_VERSION} + +# Write LSP config fragment for C# +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "csharp": { "command": "csharp-ls", "args": [], "fileExtensions": { ".cs": "csharp", ".csx": "csharp" } } } }' \ + > /etc/copilot/lsp-config.d/csharp.json + +# --- snippet: claude-cli --- +# ARG for the Claude Code CLI version - passed from build process +# This ensures cache invalidation when a new version is available +ARG CLAUDE_VERSION=latest + +# Install Anthropic's Claude Code CLI via npm. +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION} + +# --- snippet: entrypoint --- +# Set the working directory for the container. +WORKDIR /work + +# Copy the entrypoint script into the container and make it executable. +COPY docker/shared/entrypoint.sh /usr/local/bin/ +COPY docker/shared/entrypoint-airlock.sh /usr/local/bin/ +COPY docker/session-info.sh /usr/local/bin/session-info +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-airlock.sh /usr/local/bin/session-info + +# The entrypoint script will handle user creation and command execution. +ENTRYPOINT [ "entrypoint.sh" ] + +# The default command to run if none is provided. +CMD [ "copilot", "--banner" ] diff --git a/docker/generated/Dockerfile.claude-dotnet-8 b/docker/generated/Dockerfile.claude-dotnet-8 new file mode 100644 index 0000000..d5a6a9c --- /dev/null +++ b/docker/generated/Dockerfile.claude-dotnet-8 @@ -0,0 +1,114 @@ +# Auto-generated from docker/images.json - DO NOT EDIT MANUALLY +# To modify, edit docker/snippets/*.Dockerfile or docker/images.json +# then run: pwsh docker/generate-dockerfiles.ps1 + +# Use a slim Node.js base image, which gives us `npm`. +FROM node:20-slim + +# --- snippet: system-packages --- +# Set non-interactive frontend to avoid prompts during package installation. +ENV DEBIAN_FRONTEND=noninteractive + +# Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + curl \ + git \ + gosu \ + gpg \ + nano \ + software-properties-common \ + wget \ + xdg-utils \ + zsh \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: docker-cli --- +# Install Docker CLI to support brokered Docker socket scenarios. +# Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: powershell --- +# Install PowerShell - architecture-specific approach +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "amd64" ]; then \ + # AMD64: Use Microsoft's APT repository + wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y powershell \ + && rm -rf /var/lib/apt/lists/*; \ + elif [ "$ARCH" = "arm64" ]; then \ + # ARM64: Download and install from GitHub releases + wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/powershell-7.4.6-linux-arm64.tar.gz -O /tmp/powershell.tar.gz \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 \ + && chmod +x /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \ + && rm /tmp/powershell.tar.gz; \ + fi + +# --- snippet: dotnet-prereqs --- +# Install .NET SDK prerequisites and ICU libraries +RUN apt-get update && apt-get install -y \ + wget \ + ca-certificates \ + libicu-dev \ + && rm -rf /var/lib/apt/lists/* + +# Add .NET to PATH +ENV PATH="/usr/share/dotnet:${PATH}" +ENV DOTNET_ROOT="/usr/share/dotnet" + +# --- snippet: dotnet8 --- +ARG DOTNET_SDK_8_VERSION + +# Install .NET 8 SDK (latest) +RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && if [ -z "$DOTNET_SDK_8_VERSION" ]; then \ + ./dotnet-install.sh --channel 8.0 --install-dir /usr/share/dotnet; \ + else \ + ./dotnet-install.sh --version $DOTNET_SDK_8_VERSION --install-dir /usr/share/dotnet; \ + fi \ + && rm dotnet-install.sh + +# --- snippet: lsp-typescript --- +# Install TypeScript Language Server for code intelligence +ARG TYPESCRIPT_VERSION=latest +ARG TYPESCRIPT_LANGUAGE_SERVER_VERSION=latest +RUN npm install -g typescript@${TYPESCRIPT_VERSION} typescript-language-server@${TYPESCRIPT_LANGUAGE_SERVER_VERSION} + +# Write LSP config fragment for TypeScript +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "fileExtensions": { ".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" } } } }' \ + > /etc/copilot/lsp-config.d/typescript.json + +# --- snippet: claude-cli --- +# ARG for the Claude Code CLI version - passed from build process +# This ensures cache invalidation when a new version is available +ARG CLAUDE_VERSION=latest + +# Install Anthropic's Claude Code CLI via npm. +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION} + +# --- snippet: entrypoint --- +# Set the working directory for the container. +WORKDIR /work + +# Copy the entrypoint script into the container and make it executable. +COPY docker/shared/entrypoint.sh /usr/local/bin/ +COPY docker/shared/entrypoint-airlock.sh /usr/local/bin/ +COPY docker/session-info.sh /usr/local/bin/session-info +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-airlock.sh /usr/local/bin/session-info + +# The entrypoint script will handle user creation and command execution. +ENTRYPOINT [ "entrypoint.sh" ] + +# The default command to run if none is provided. +CMD [ "copilot", "--banner" ] diff --git a/docker/generated/Dockerfile.claude-dotnet-9 b/docker/generated/Dockerfile.claude-dotnet-9 new file mode 100644 index 0000000..c8fe102 --- /dev/null +++ b/docker/generated/Dockerfile.claude-dotnet-9 @@ -0,0 +1,114 @@ +# Auto-generated from docker/images.json - DO NOT EDIT MANUALLY +# To modify, edit docker/snippets/*.Dockerfile or docker/images.json +# then run: pwsh docker/generate-dockerfiles.ps1 + +# Use a slim Node.js base image, which gives us `npm`. +FROM node:20-slim + +# --- snippet: system-packages --- +# Set non-interactive frontend to avoid prompts during package installation. +ENV DEBIAN_FRONTEND=noninteractive + +# Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + curl \ + git \ + gosu \ + gpg \ + nano \ + software-properties-common \ + wget \ + xdg-utils \ + zsh \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: docker-cli --- +# Install Docker CLI to support brokered Docker socket scenarios. +# Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: powershell --- +# Install PowerShell - architecture-specific approach +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "amd64" ]; then \ + # AMD64: Use Microsoft's APT repository + wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y powershell \ + && rm -rf /var/lib/apt/lists/*; \ + elif [ "$ARCH" = "arm64" ]; then \ + # ARM64: Download and install from GitHub releases + wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/powershell-7.4.6-linux-arm64.tar.gz -O /tmp/powershell.tar.gz \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 \ + && chmod +x /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \ + && rm /tmp/powershell.tar.gz; \ + fi + +# --- snippet: dotnet-prereqs --- +# Install .NET SDK prerequisites and ICU libraries +RUN apt-get update && apt-get install -y \ + wget \ + ca-certificates \ + libicu-dev \ + && rm -rf /var/lib/apt/lists/* + +# Add .NET to PATH +ENV PATH="/usr/share/dotnet:${PATH}" +ENV DOTNET_ROOT="/usr/share/dotnet" + +# --- snippet: dotnet9 --- +ARG DOTNET_SDK_9_VERSION + +# Install .NET 9 SDK (latest) +RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && if [ -z "$DOTNET_SDK_9_VERSION" ]; then \ + ./dotnet-install.sh --channel 9.0 --install-dir /usr/share/dotnet; \ + else \ + ./dotnet-install.sh --version $DOTNET_SDK_9_VERSION --install-dir /usr/share/dotnet; \ + fi \ + && rm dotnet-install.sh + +# --- snippet: lsp-typescript --- +# Install TypeScript Language Server for code intelligence +ARG TYPESCRIPT_VERSION=latest +ARG TYPESCRIPT_LANGUAGE_SERVER_VERSION=latest +RUN npm install -g typescript@${TYPESCRIPT_VERSION} typescript-language-server@${TYPESCRIPT_LANGUAGE_SERVER_VERSION} + +# Write LSP config fragment for TypeScript +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "fileExtensions": { ".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" } } } }' \ + > /etc/copilot/lsp-config.d/typescript.json + +# --- snippet: claude-cli --- +# ARG for the Claude Code CLI version - passed from build process +# This ensures cache invalidation when a new version is available +ARG CLAUDE_VERSION=latest + +# Install Anthropic's Claude Code CLI via npm. +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION} + +# --- snippet: entrypoint --- +# Set the working directory for the container. +WORKDIR /work + +# Copy the entrypoint script into the container and make it executable. +COPY docker/shared/entrypoint.sh /usr/local/bin/ +COPY docker/shared/entrypoint-airlock.sh /usr/local/bin/ +COPY docker/session-info.sh /usr/local/bin/session-info +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-airlock.sh /usr/local/bin/session-info + +# The entrypoint script will handle user creation and command execution. +ENTRYPOINT [ "entrypoint.sh" ] + +# The default command to run if none is provided. +CMD [ "copilot", "--banner" ] diff --git a/docker/generated/Dockerfile.claude-dotnet-playwright b/docker/generated/Dockerfile.claude-dotnet-playwright new file mode 100644 index 0000000..ef73286 --- /dev/null +++ b/docker/generated/Dockerfile.claude-dotnet-playwright @@ -0,0 +1,192 @@ +# Auto-generated from docker/images.json - DO NOT EDIT MANUALLY +# To modify, edit docker/snippets/*.Dockerfile or docker/images.json +# then run: pwsh docker/generate-dockerfiles.ps1 + +# Use a slim Node.js base image, which gives us `npm`. +FROM node:20-slim + +# --- snippet: system-packages --- +# Set non-interactive frontend to avoid prompts during package installation. +ENV DEBIAN_FRONTEND=noninteractive + +# Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + curl \ + git \ + gosu \ + gpg \ + nano \ + software-properties-common \ + wget \ + xdg-utils \ + zsh \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: docker-cli --- +# Install Docker CLI to support brokered Docker socket scenarios. +# Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: powershell --- +# Install PowerShell - architecture-specific approach +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "amd64" ]; then \ + # AMD64: Use Microsoft's APT repository + wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y powershell \ + && rm -rf /var/lib/apt/lists/*; \ + elif [ "$ARCH" = "arm64" ]; then \ + # ARM64: Download and install from GitHub releases + wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/powershell-7.4.6-linux-arm64.tar.gz -O /tmp/powershell.tar.gz \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 \ + && chmod +x /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \ + && rm /tmp/powershell.tar.gz; \ + fi + +# --- snippet: dotnet-prereqs --- +# Install .NET SDK prerequisites and ICU libraries +RUN apt-get update && apt-get install -y \ + wget \ + ca-certificates \ + libicu-dev \ + && rm -rf /var/lib/apt/lists/* + +# Add .NET to PATH +ENV PATH="/usr/share/dotnet:${PATH}" +ENV DOTNET_ROOT="/usr/share/dotnet" + +# --- snippet: dotnet8 --- +ARG DOTNET_SDK_8_VERSION + +# Install .NET 8 SDK (latest) +RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && if [ -z "$DOTNET_SDK_8_VERSION" ]; then \ + ./dotnet-install.sh --channel 8.0 --install-dir /usr/share/dotnet; \ + else \ + ./dotnet-install.sh --version $DOTNET_SDK_8_VERSION --install-dir /usr/share/dotnet; \ + fi \ + && rm dotnet-install.sh + +# --- snippet: dotnet9 --- +ARG DOTNET_SDK_9_VERSION + +# Install .NET 9 SDK (latest) +RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && if [ -z "$DOTNET_SDK_9_VERSION" ]; then \ + ./dotnet-install.sh --channel 9.0 --install-dir /usr/share/dotnet; \ + else \ + ./dotnet-install.sh --version $DOTNET_SDK_9_VERSION --install-dir /usr/share/dotnet; \ + fi \ + && rm dotnet-install.sh + +# --- snippet: dotnet10 --- +ARG DOTNET_SDK_10_VERSION + +# Install .NET 10 SDK (latest) +RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && if [ -z "$DOTNET_SDK_10_VERSION" ]; then \ + ./dotnet-install.sh --channel 10.0 --install-dir /usr/share/dotnet; \ + else \ + ./dotnet-install.sh --version $DOTNET_SDK_10_VERSION --install-dir /usr/share/dotnet; \ + fi \ + && rm dotnet-install.sh + +# --- snippet: playwright --- +ARG PLAYWRIGHT_VERSION=latest + +# Install Playwright dependencies +# Using Playwright's system dependencies approach +RUN apt-get update && apt-get install -y \ + # Playwright system dependencies for Chromium + libnss3 \ + libnspr4 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libdbus-1-3 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxfixes3 \ + libxrandr2 \ + libgbm1 \ + libpango-1.0-0 \ + libcairo2 \ + libasound2 \ + libatspi2.0-0 \ + libxshmfence1 \ + && rm -rf /var/lib/apt/lists/* + +# Install Playwright globally via npm +RUN npm install -g playwright@${PLAYWRIGHT_VERSION} + +# Set NODE_PATH so Node.js can find globally installed packages +ENV NODE_PATH=/usr/local/lib/node_modules + +# Install all browsers (Chromium, Firefox, WebKit) in a system-wide location +# This avoids UID mismatch issues when container runs with different user IDs +ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright +RUN npx playwright install --with-deps + +# Make playwright browsers directory world-writable so any UID can access/update them +RUN chmod -R 777 /ms-playwright + +# --- snippet: lsp-typescript --- +# Install TypeScript Language Server for code intelligence +ARG TYPESCRIPT_VERSION=latest +ARG TYPESCRIPT_LANGUAGE_SERVER_VERSION=latest +RUN npm install -g typescript@${TYPESCRIPT_VERSION} typescript-language-server@${TYPESCRIPT_LANGUAGE_SERVER_VERSION} + +# Write LSP config fragment for TypeScript +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "fileExtensions": { ".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" } } } }' \ + > /etc/copilot/lsp-config.d/typescript.json + +# --- snippet: lsp-csharp --- +# Install C# Language Server for code intelligence +# Install directly to a shared location so appuser can access it +ARG CSHARP_LS_VERSION=0.22.0 +RUN dotnet tool install csharp-ls --tool-path /usr/local/bin --version ${CSHARP_LS_VERSION} + +# Write LSP config fragment for C# +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "csharp": { "command": "csharp-ls", "args": [], "fileExtensions": { ".cs": "csharp", ".csx": "csharp" } } } }' \ + > /etc/copilot/lsp-config.d/csharp.json + +# --- snippet: claude-cli --- +# ARG for the Claude Code CLI version - passed from build process +# This ensures cache invalidation when a new version is available +ARG CLAUDE_VERSION=latest + +# Install Anthropic's Claude Code CLI via npm. +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION} + +# --- snippet: entrypoint --- +# Set the working directory for the container. +WORKDIR /work + +# Copy the entrypoint script into the container and make it executable. +COPY docker/shared/entrypoint.sh /usr/local/bin/ +COPY docker/shared/entrypoint-airlock.sh /usr/local/bin/ +COPY docker/session-info.sh /usr/local/bin/session-info +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-airlock.sh /usr/local/bin/session-info + +# The entrypoint script will handle user creation and command execution. +ENTRYPOINT [ "entrypoint.sh" ] + +# The default command to run if none is provided. +CMD [ "copilot", "--banner" ] diff --git a/docker/generated/Dockerfile.claude-dotnet-rust b/docker/generated/Dockerfile.claude-dotnet-rust new file mode 100644 index 0000000..100b98e --- /dev/null +++ b/docker/generated/Dockerfile.claude-dotnet-rust @@ -0,0 +1,181 @@ +# Auto-generated from docker/images.json - DO NOT EDIT MANUALLY +# To modify, edit docker/snippets/*.Dockerfile or docker/images.json +# then run: pwsh docker/generate-dockerfiles.ps1 + +# Use a slim Node.js base image, which gives us `npm`. +FROM node:20-slim + +# --- snippet: system-packages --- +# Set non-interactive frontend to avoid prompts during package installation. +ENV DEBIAN_FRONTEND=noninteractive + +# Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + curl \ + git \ + gosu \ + gpg \ + nano \ + software-properties-common \ + wget \ + xdg-utils \ + zsh \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: docker-cli --- +# Install Docker CLI to support brokered Docker socket scenarios. +# Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: powershell --- +# Install PowerShell - architecture-specific approach +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "amd64" ]; then \ + # AMD64: Use Microsoft's APT repository + wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y powershell \ + && rm -rf /var/lib/apt/lists/*; \ + elif [ "$ARCH" = "arm64" ]; then \ + # ARM64: Download and install from GitHub releases + wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/powershell-7.4.6-linux-arm64.tar.gz -O /tmp/powershell.tar.gz \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 \ + && chmod +x /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \ + && rm /tmp/powershell.tar.gz; \ + fi + +# --- snippet: dotnet-prereqs --- +# Install .NET SDK prerequisites and ICU libraries +RUN apt-get update && apt-get install -y \ + wget \ + ca-certificates \ + libicu-dev \ + && rm -rf /var/lib/apt/lists/* + +# Add .NET to PATH +ENV PATH="/usr/share/dotnet:${PATH}" +ENV DOTNET_ROOT="/usr/share/dotnet" + +# --- snippet: dotnet8 --- +ARG DOTNET_SDK_8_VERSION + +# Install .NET 8 SDK (latest) +RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && if [ -z "$DOTNET_SDK_8_VERSION" ]; then \ + ./dotnet-install.sh --channel 8.0 --install-dir /usr/share/dotnet; \ + else \ + ./dotnet-install.sh --version $DOTNET_SDK_8_VERSION --install-dir /usr/share/dotnet; \ + fi \ + && rm dotnet-install.sh + +# --- snippet: dotnet9 --- +ARG DOTNET_SDK_9_VERSION + +# Install .NET 9 SDK (latest) +RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && if [ -z "$DOTNET_SDK_9_VERSION" ]; then \ + ./dotnet-install.sh --channel 9.0 --install-dir /usr/share/dotnet; \ + else \ + ./dotnet-install.sh --version $DOTNET_SDK_9_VERSION --install-dir /usr/share/dotnet; \ + fi \ + && rm dotnet-install.sh + +# --- snippet: dotnet10 --- +ARG DOTNET_SDK_10_VERSION + +# Install .NET 10 SDK (latest) +RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && if [ -z "$DOTNET_SDK_10_VERSION" ]; then \ + ./dotnet-install.sh --channel 10.0 --install-dir /usr/share/dotnet; \ + else \ + ./dotnet-install.sh --version $DOTNET_SDK_10_VERSION --install-dir /usr/share/dotnet; \ + fi \ + && rm dotnet-install.sh + +# --- snippet: rust --- +# Install Rust dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + pkg-config \ + libssl-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Rust toolchain to system-wide location +ENV RUSTUP_HOME=/usr/local/rustup +ENV CARGO_HOME=/usr/local/cargo +ENV PATH="/usr/local/cargo/bin:${PATH}" + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + +# Make cargo registry writable by all users (for downloading crates) +RUN chmod -R a+rwX /usr/local/cargo + +# Verify installation +RUN rustc --version && cargo --version + +# --- snippet: lsp-typescript --- +# Install TypeScript Language Server for code intelligence +ARG TYPESCRIPT_VERSION=latest +ARG TYPESCRIPT_LANGUAGE_SERVER_VERSION=latest +RUN npm install -g typescript@${TYPESCRIPT_VERSION} typescript-language-server@${TYPESCRIPT_LANGUAGE_SERVER_VERSION} + +# Write LSP config fragment for TypeScript +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "fileExtensions": { ".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" } } } }' \ + > /etc/copilot/lsp-config.d/typescript.json + +# --- snippet: lsp-csharp --- +# Install C# Language Server for code intelligence +# Install directly to a shared location so appuser can access it +ARG CSHARP_LS_VERSION=0.22.0 +RUN dotnet tool install csharp-ls --tool-path /usr/local/bin --version ${CSHARP_LS_VERSION} + +# Write LSP config fragment for C# +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "csharp": { "command": "csharp-ls", "args": [], "fileExtensions": { ".cs": "csharp", ".csx": "csharp" } } } }' \ + > /etc/copilot/lsp-config.d/csharp.json + +# --- snippet: lsp-rust --- +# Install Rust Analyzer for code intelligence +RUN rustup component add rust-analyzer + +# Write LSP config fragment for Rust +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "rust": { "command": "rust-analyzer", "args": [], "fileExtensions": { ".rs": "rust" } } } }' \ + > /etc/copilot/lsp-config.d/rust.json + +# --- snippet: claude-cli --- +# ARG for the Claude Code CLI version - passed from build process +# This ensures cache invalidation when a new version is available +ARG CLAUDE_VERSION=latest + +# Install Anthropic's Claude Code CLI via npm. +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION} + +# --- snippet: entrypoint --- +# Set the working directory for the container. +WORKDIR /work + +# Copy the entrypoint script into the container and make it executable. +COPY docker/shared/entrypoint.sh /usr/local/bin/ +COPY docker/shared/entrypoint-airlock.sh /usr/local/bin/ +COPY docker/session-info.sh /usr/local/bin/session-info +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-airlock.sh /usr/local/bin/session-info + +# The entrypoint script will handle user creation and command execution. +ENTRYPOINT [ "entrypoint.sh" ] + +# The default command to run if none is provided. +CMD [ "copilot", "--banner" ] diff --git a/docker/generated/Dockerfile.claude-golang b/docker/generated/Dockerfile.claude-golang new file mode 100644 index 0000000..1071537 --- /dev/null +++ b/docker/generated/Dockerfile.claude-golang @@ -0,0 +1,118 @@ +# Auto-generated from docker/images.json - DO NOT EDIT MANUALLY +# To modify, edit docker/snippets/*.Dockerfile or docker/images.json +# then run: pwsh docker/generate-dockerfiles.ps1 + +# Use a slim Node.js base image, which gives us `npm`. +FROM node:20-slim + +# --- snippet: system-packages --- +# Set non-interactive frontend to avoid prompts during package installation. +ENV DEBIAN_FRONTEND=noninteractive + +# Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + curl \ + git \ + gosu \ + gpg \ + nano \ + software-properties-common \ + wget \ + xdg-utils \ + zsh \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: docker-cli --- +# Install Docker CLI to support brokered Docker socket scenarios. +# Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: powershell --- +# Install PowerShell - architecture-specific approach +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "amd64" ]; then \ + # AMD64: Use Microsoft's APT repository + wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y powershell \ + && rm -rf /var/lib/apt/lists/*; \ + elif [ "$ARCH" = "arm64" ]; then \ + # ARM64: Download and install from GitHub releases + wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/powershell-7.4.6-linux-arm64.tar.gz -O /tmp/powershell.tar.gz \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 \ + && chmod +x /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \ + && rm /tmp/powershell.tar.gz; \ + fi + +# --- snippet: golang --- +# Install Go +# Using official Go installation method +ENV GOLANG_VERSION=1.26.3 +ENV GOPATH=/usr/local/go-workspace +ENV PATH="/usr/local/go/bin:${GOPATH}/bin:${PATH}" + +# Install Go from official tarball +RUN curl -fsSL "https://go.dev/dl/go${GOLANG_VERSION}.linux-$(dpkg --print-architecture).tar.gz" -o go.tar.gz \ + && tar -C /usr/local -xzf go.tar.gz \ + && rm go.tar.gz + +# Create GOPATH and make it writable by all users +RUN mkdir -p ${GOPATH} \ + && chmod -R a+rwX ${GOPATH} + +# Verify installation +RUN go version + +# --- snippet: lsp-typescript --- +# Install TypeScript Language Server for code intelligence +ARG TYPESCRIPT_VERSION=latest +ARG TYPESCRIPT_LANGUAGE_SERVER_VERSION=latest +RUN npm install -g typescript@${TYPESCRIPT_VERSION} typescript-language-server@${TYPESCRIPT_LANGUAGE_SERVER_VERSION} + +# Write LSP config fragment for TypeScript +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "fileExtensions": { ".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" } } } }' \ + > /etc/copilot/lsp-config.d/typescript.json + +# --- snippet: lsp-golang --- +# Install gopls (Go Language Server) for code intelligence +ARG GOPLS_VERSION=latest +RUN go install golang.org/x/tools/gopls@${GOPLS_VERSION} + +# Write LSP config fragment for Go +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "go": { "command": "gopls", "args": ["serve"], "fileExtensions": { ".go": "go" } } } }' \ + > /etc/copilot/lsp-config.d/golang.json + +# --- snippet: claude-cli --- +# ARG for the Claude Code CLI version - passed from build process +# This ensures cache invalidation when a new version is available +ARG CLAUDE_VERSION=latest + +# Install Anthropic's Claude Code CLI via npm. +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION} + +# --- snippet: entrypoint --- +# Set the working directory for the container. +WORKDIR /work + +# Copy the entrypoint script into the container and make it executable. +COPY docker/shared/entrypoint.sh /usr/local/bin/ +COPY docker/shared/entrypoint-airlock.sh /usr/local/bin/ +COPY docker/session-info.sh /usr/local/bin/session-info +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-airlock.sh /usr/local/bin/session-info + +# The entrypoint script will handle user creation and command execution. +ENTRYPOINT [ "entrypoint.sh" ] + +# The default command to run if none is provided. +CMD [ "copilot", "--banner" ] diff --git a/docker/generated/Dockerfile.claude-java b/docker/generated/Dockerfile.claude-java new file mode 100644 index 0000000..a71c39b --- /dev/null +++ b/docker/generated/Dockerfile.claude-java @@ -0,0 +1,154 @@ +# Auto-generated from docker/images.json - DO NOT EDIT MANUALLY +# To modify, edit docker/snippets/*.Dockerfile or docker/images.json +# then run: pwsh docker/generate-dockerfiles.ps1 + +# Use a slim Node.js base image, which gives us `npm`. +FROM node:20-slim + +# --- snippet: system-packages --- +# Set non-interactive frontend to avoid prompts during package installation. +ENV DEBIAN_FRONTEND=noninteractive + +# Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + curl \ + git \ + gosu \ + gpg \ + nano \ + software-properties-common \ + wget \ + xdg-utils \ + zsh \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: docker-cli --- +# Install Docker CLI to support brokered Docker socket scenarios. +# Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: powershell --- +# Install PowerShell - architecture-specific approach +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "amd64" ]; then \ + # AMD64: Use Microsoft's APT repository + wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y powershell \ + && rm -rf /var/lib/apt/lists/*; \ + elif [ "$ARCH" = "arm64" ]; then \ + # ARM64: Download and install from GitHub releases + wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/powershell-7.4.6-linux-arm64.tar.gz -O /tmp/powershell.tar.gz \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 \ + && chmod +x /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \ + && rm /tmp/powershell.tar.gz; \ + fi + +# --- snippet: java --- +# Install Java (Eclipse Temurin JDK 25), Maven, and Gradle +# Using Eclipse Temurin - widely used, well-maintained OpenJDK distribution + +# Add Eclipse Temurin repository and install JDK +RUN apt-get update && apt-get install -y gnupg unzip \ + && wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor -o /usr/share/keyrings/adoptium.gpg \ + && echo "deb [signed-by=/usr/share/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb $(. /etc/os-release && echo $VERSION_CODENAME) main" > /etc/apt/sources.list.d/adoptium.list \ + && apt-get update && apt-get install -y temurin-25-jdk \ + && rm -rf /var/lib/apt/lists/* + +# Set JAVA_HOME dynamically based on architecture (ENV can't use command substitution) +RUN ln -s /usr/lib/jvm/temurin-25-jdk-$(dpkg --print-architecture) /usr/lib/jvm/temurin-25-jdk +ENV JAVA_HOME=/usr/lib/jvm/temurin-25-jdk +ENV PATH="${JAVA_HOME}/bin:${PATH}" + +# Install Maven +ARG MAVEN_VERSION=3.9.9 +RUN curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" -o maven.tar.gz \ + && tar -C /usr/local -xzf maven.tar.gz \ + && ln -s /usr/local/apache-maven-${MAVEN_VERSION}/bin/mvn /usr/local/bin/mvn \ + && rm maven.tar.gz + +# Install Gradle +ARG GRADLE_VERSION=8.12 +RUN curl -fsSL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" -o gradle.zip \ + && unzip -d /usr/local gradle.zip \ + && ln -s /usr/local/gradle-${GRADLE_VERSION}/bin/gradle /usr/local/bin/gradle \ + && rm gradle.zip + +# Install PlantUML +ARG PLANTUML_VERSION=1.2025.2 +RUN curl -fsSL "https://github.com/plantuml/plantuml/releases/download/v${PLANTUML_VERSION}/plantuml-${PLANTUML_VERSION}.jar" -o /usr/local/lib/plantuml.jar \ + && echo '#!/bin/sh\nexec java -jar /usr/local/lib/plantuml.jar "$@"' > /usr/local/bin/plantuml \ + && chmod +x /usr/local/bin/plantuml + +# Install Graphviz (required by PlantUML for many diagram types) +RUN apt-get update && apt-get install -y graphviz \ + && rm -rf /var/lib/apt/lists/* + +# Make Maven and Gradle caches writable by all users +RUN mkdir -p /home/appuser/.m2 /home/appuser/.gradle \ + && chmod -R a+rwX /home/appuser/.m2 /home/appuser/.gradle + +# Verify installations +RUN java --version && mvn --version && gradle --version && plantuml -version + +# --- snippet: lsp-typescript --- +# Install TypeScript Language Server for code intelligence +ARG TYPESCRIPT_VERSION=latest +ARG TYPESCRIPT_LANGUAGE_SERVER_VERSION=latest +RUN npm install -g typescript@${TYPESCRIPT_VERSION} typescript-language-server@${TYPESCRIPT_LANGUAGE_SERVER_VERSION} + +# Write LSP config fragment for TypeScript +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "fileExtensions": { ".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" } } } }' \ + > /etc/copilot/lsp-config.d/typescript.json + +# --- snippet: lsp-java --- +# Install Eclipse JDT Language Server for Java code intelligence +ARG JDTLS_VERSION=1.43.0 +ARG JDTLS_TIMESTAMP=202412191447 +RUN mkdir -p /usr/local/share/jdtls \ + && curl -fsSL "https://download.eclipse.org/jdtls/milestones/${JDTLS_VERSION}/jdt-language-server-${JDTLS_VERSION}-${JDTLS_TIMESTAMP}.tar.gz" -o jdtls.tar.gz \ + && tar -C /usr/local/share/jdtls -xzf jdtls.tar.gz \ + && rm jdtls.tar.gz + +# Create launcher script +RUN echo '#!/bin/sh\nexec java \\\n -Declipse.application=org.eclipse.jdt.ls.core.id1 \\\n -Dosgi.bundles.defaultStartLevel=4 \\\n -Declipse.product=org.eclipse.jdt.ls.core.product \\\n -Dlog.level=ALL \\\n -noverify \\\n --add-modules=ALL-SYSTEM \\\n --add-opens java.base/java.util=ALL-UNNAMED \\\n --add-opens java.base/java.lang=ALL-UNNAMED \\\n -jar /usr/local/share/jdtls/plugins/org.eclipse.equinox.launcher_*.jar \\\n -configuration /usr/local/share/jdtls/config_linux \\\n -data /tmp/jdtls-workspace \\\n "$@"' > /usr/local/bin/jdtls \ + && chmod +x /usr/local/bin/jdtls + +# Write LSP config fragment for Java +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "java": { "command": "jdtls", "args": [], "fileExtensions": { ".java": "java" } } } }' \ + > /etc/copilot/lsp-config.d/java.json + +# --- snippet: claude-cli --- +# ARG for the Claude Code CLI version - passed from build process +# This ensures cache invalidation when a new version is available +ARG CLAUDE_VERSION=latest + +# Install Anthropic's Claude Code CLI via npm. +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION} + +# --- snippet: entrypoint --- +# Set the working directory for the container. +WORKDIR /work + +# Copy the entrypoint script into the container and make it executable. +COPY docker/shared/entrypoint.sh /usr/local/bin/ +COPY docker/shared/entrypoint-airlock.sh /usr/local/bin/ +COPY docker/session-info.sh /usr/local/bin/session-info +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-airlock.sh /usr/local/bin/session-info + +# The entrypoint script will handle user creation and command execution. +ENTRYPOINT [ "entrypoint.sh" ] + +# The default command to run if none is provided. +CMD [ "copilot", "--banner" ] diff --git a/docker/generated/Dockerfile.claude-playwright b/docker/generated/Dockerfile.claude-playwright new file mode 100644 index 0000000..29a3c67 --- /dev/null +++ b/docker/generated/Dockerfile.claude-playwright @@ -0,0 +1,130 @@ +# Auto-generated from docker/images.json - DO NOT EDIT MANUALLY +# To modify, edit docker/snippets/*.Dockerfile or docker/images.json +# then run: pwsh docker/generate-dockerfiles.ps1 + +# Use a slim Node.js base image, which gives us `npm`. +FROM node:20-slim + +# --- snippet: system-packages --- +# Set non-interactive frontend to avoid prompts during package installation. +ENV DEBIAN_FRONTEND=noninteractive + +# Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + curl \ + git \ + gosu \ + gpg \ + nano \ + software-properties-common \ + wget \ + xdg-utils \ + zsh \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: docker-cli --- +# Install Docker CLI to support brokered Docker socket scenarios. +# Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: powershell --- +# Install PowerShell - architecture-specific approach +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "amd64" ]; then \ + # AMD64: Use Microsoft's APT repository + wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y powershell \ + && rm -rf /var/lib/apt/lists/*; \ + elif [ "$ARCH" = "arm64" ]; then \ + # ARM64: Download and install from GitHub releases + wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/powershell-7.4.6-linux-arm64.tar.gz -O /tmp/powershell.tar.gz \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 \ + && chmod +x /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \ + && rm /tmp/powershell.tar.gz; \ + fi + +# --- snippet: playwright --- +ARG PLAYWRIGHT_VERSION=latest + +# Install Playwright dependencies +# Using Playwright's system dependencies approach +RUN apt-get update && apt-get install -y \ + # Playwright system dependencies for Chromium + libnss3 \ + libnspr4 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libdbus-1-3 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxfixes3 \ + libxrandr2 \ + libgbm1 \ + libpango-1.0-0 \ + libcairo2 \ + libasound2 \ + libatspi2.0-0 \ + libxshmfence1 \ + && rm -rf /var/lib/apt/lists/* + +# Install Playwright globally via npm +RUN npm install -g playwright@${PLAYWRIGHT_VERSION} + +# Set NODE_PATH so Node.js can find globally installed packages +ENV NODE_PATH=/usr/local/lib/node_modules + +# Install all browsers (Chromium, Firefox, WebKit) in a system-wide location +# This avoids UID mismatch issues when container runs with different user IDs +ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright +RUN npx playwright install --with-deps + +# Make playwright browsers directory world-writable so any UID can access/update them +RUN chmod -R 777 /ms-playwright + +# --- snippet: lsp-typescript --- +# Install TypeScript Language Server for code intelligence +ARG TYPESCRIPT_VERSION=latest +ARG TYPESCRIPT_LANGUAGE_SERVER_VERSION=latest +RUN npm install -g typescript@${TYPESCRIPT_VERSION} typescript-language-server@${TYPESCRIPT_LANGUAGE_SERVER_VERSION} + +# Write LSP config fragment for TypeScript +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "fileExtensions": { ".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" } } } }' \ + > /etc/copilot/lsp-config.d/typescript.json + +# --- snippet: claude-cli --- +# ARG for the Claude Code CLI version - passed from build process +# This ensures cache invalidation when a new version is available +ARG CLAUDE_VERSION=latest + +# Install Anthropic's Claude Code CLI via npm. +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION} + +# --- snippet: entrypoint --- +# Set the working directory for the container. +WORKDIR /work + +# Copy the entrypoint script into the container and make it executable. +COPY docker/shared/entrypoint.sh /usr/local/bin/ +COPY docker/shared/entrypoint-airlock.sh /usr/local/bin/ +COPY docker/session-info.sh /usr/local/bin/session-info +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-airlock.sh /usr/local/bin/session-info + +# The entrypoint script will handle user creation and command execution. +ENTRYPOINT [ "entrypoint.sh" ] + +# The default command to run if none is provided. +CMD [ "copilot", "--banner" ] diff --git a/docker/generated/Dockerfile.claude-rust b/docker/generated/Dockerfile.claude-rust new file mode 100644 index 0000000..f8f9b2d --- /dev/null +++ b/docker/generated/Dockerfile.claude-rust @@ -0,0 +1,119 @@ +# Auto-generated from docker/images.json - DO NOT EDIT MANUALLY +# To modify, edit docker/snippets/*.Dockerfile or docker/images.json +# then run: pwsh docker/generate-dockerfiles.ps1 + +# Use a slim Node.js base image, which gives us `npm`. +FROM node:20-slim + +# --- snippet: system-packages --- +# Set non-interactive frontend to avoid prompts during package installation. +ENV DEBIAN_FRONTEND=noninteractive + +# Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +RUN apt-get update && apt-get install -y \ + apt-transport-https \ + curl \ + git \ + gosu \ + gpg \ + nano \ + software-properties-common \ + wget \ + xdg-utils \ + zsh \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: docker-cli --- +# Install Docker CLI to support brokered Docker socket scenarios. +# Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. +RUN mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update && apt-get install -y docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# --- snippet: powershell --- +# Install PowerShell - architecture-specific approach +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "amd64" ]; then \ + # AMD64: Use Microsoft's APT repository + wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y powershell \ + && rm -rf /var/lib/apt/lists/*; \ + elif [ "$ARCH" = "arm64" ]; then \ + # ARM64: Download and install from GitHub releases + wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/powershell-7.4.6-linux-arm64.tar.gz -O /tmp/powershell.tar.gz \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 \ + && chmod +x /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \ + && rm /tmp/powershell.tar.gz; \ + fi + +# --- snippet: rust --- +# Install Rust dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + pkg-config \ + libssl-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Rust toolchain to system-wide location +ENV RUSTUP_HOME=/usr/local/rustup +ENV CARGO_HOME=/usr/local/cargo +ENV PATH="/usr/local/cargo/bin:${PATH}" + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + +# Make cargo registry writable by all users (for downloading crates) +RUN chmod -R a+rwX /usr/local/cargo + +# Verify installation +RUN rustc --version && cargo --version + +# --- snippet: lsp-typescript --- +# Install TypeScript Language Server for code intelligence +ARG TYPESCRIPT_VERSION=latest +ARG TYPESCRIPT_LANGUAGE_SERVER_VERSION=latest +RUN npm install -g typescript@${TYPESCRIPT_VERSION} typescript-language-server@${TYPESCRIPT_LANGUAGE_SERVER_VERSION} + +# Write LSP config fragment for TypeScript +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "typescript": { "command": "typescript-language-server", "args": ["--stdio"], "fileExtensions": { ".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" } } } }' \ + > /etc/copilot/lsp-config.d/typescript.json + +# --- snippet: lsp-rust --- +# Install Rust Analyzer for code intelligence +RUN rustup component add rust-analyzer + +# Write LSP config fragment for Rust +RUN mkdir -p /etc/copilot/lsp-config.d && \ + echo '{ "lspServers": { "rust": { "command": "rust-analyzer", "args": [], "fileExtensions": { ".rs": "rust" } } } }' \ + > /etc/copilot/lsp-config.d/rust.json + +# --- snippet: claude-cli --- +# ARG for the Claude Code CLI version - passed from build process +# This ensures cache invalidation when a new version is available +ARG CLAUDE_VERSION=latest + +# Install Anthropic's Claude Code CLI via npm. +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION} + +# --- snippet: entrypoint --- +# Set the working directory for the container. +WORKDIR /work + +# Copy the entrypoint script into the container and make it executable. +COPY docker/shared/entrypoint.sh /usr/local/bin/ +COPY docker/shared/entrypoint-airlock.sh /usr/local/bin/ +COPY docker/session-info.sh /usr/local/bin/session-info +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-airlock.sh /usr/local/bin/session-info + +# The entrypoint script will handle user creation and command execution. +ENTRYPOINT [ "entrypoint.sh" ] + +# The default command to run if none is provided. +CMD [ "copilot", "--banner" ] diff --git a/docker/images.json b/docker/images.json index 859f64e..84a8504 100644 --- a/docker/images.json +++ b/docker/images.json @@ -43,6 +43,50 @@ "java": { "base": "node:20-slim", "snippets": ["system-packages", "docker-cli", "powershell", "java", "lsp-typescript", "lsp-java", "copilot-cli", "entrypoint"] + }, + "claude-default": { + "base": "node:20-slim", + "snippets": ["system-packages", "docker-cli", "powershell", "lsp-typescript", "claude-cli", "entrypoint"] + }, + "claude-dotnet": { + "base": "node:20-slim", + "snippets": ["system-packages", "docker-cli", "powershell", "dotnet-prereqs", "dotnet8", "dotnet9", "dotnet10", "lsp-typescript", "lsp-csharp", "claude-cli", "entrypoint"] + }, + "claude-dotnet-8": { + "base": "node:20-slim", + "snippets": ["system-packages", "docker-cli", "powershell", "dotnet-prereqs", "dotnet8", "lsp-typescript", "claude-cli", "entrypoint"] + }, + "claude-dotnet-9": { + "base": "node:20-slim", + "snippets": ["system-packages", "docker-cli", "powershell", "dotnet-prereqs", "dotnet9", "lsp-typescript", "claude-cli", "entrypoint"] + }, + "claude-dotnet-10": { + "base": "node:20-slim", + "snippets": ["system-packages", "docker-cli", "powershell", "dotnet-prereqs", "dotnet10", "lsp-typescript", "lsp-csharp", "claude-cli", "entrypoint"] + }, + "claude-playwright": { + "base": "node:20-slim", + "snippets": ["system-packages", "docker-cli", "powershell", "playwright", "lsp-typescript", "claude-cli", "entrypoint"] + }, + "claude-rust": { + "base": "node:20-slim", + "snippets": ["system-packages", "docker-cli", "powershell", "rust", "lsp-typescript", "lsp-rust", "claude-cli", "entrypoint"] + }, + "claude-golang": { + "base": "node:20-slim", + "snippets": ["system-packages", "docker-cli", "powershell", "golang", "lsp-typescript", "lsp-golang", "claude-cli", "entrypoint"] + }, + "claude-dotnet-playwright": { + "base": "node:20-slim", + "snippets": ["system-packages", "docker-cli", "powershell", "dotnet-prereqs", "dotnet8", "dotnet9", "dotnet10", "playwright", "lsp-typescript", "lsp-csharp", "claude-cli", "entrypoint"] + }, + "claude-dotnet-rust": { + "base": "node:20-slim", + "snippets": ["system-packages", "docker-cli", "powershell", "dotnet-prereqs", "dotnet8", "dotnet9", "dotnet10", "rust", "lsp-typescript", "lsp-csharp", "lsp-rust", "claude-cli", "entrypoint"] + }, + "claude-java": { + "base": "node:20-slim", + "snippets": ["system-packages", "docker-cli", "powershell", "java", "lsp-typescript", "lsp-java", "claude-cli", "entrypoint"] } } } diff --git a/docker/snippets/claude-cli.Dockerfile b/docker/snippets/claude-cli.Dockerfile new file mode 100644 index 0000000..7f1cb3c --- /dev/null +++ b/docker/snippets/claude-cli.Dockerfile @@ -0,0 +1,6 @@ +# ARG for the Claude Code CLI version - passed from build process +# This ensures cache invalidation when a new version is available +ARG CLAUDE_VERSION=latest + +# Install Anthropic's Claude Code CLI via npm. +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_VERSION} diff --git a/docker/tools/claude/default-airlock-rules.json b/docker/tools/claude/default-airlock-rules.json new file mode 100644 index 0000000..bf97507 --- /dev/null +++ b/docker/tools/claude/default-airlock-rules.json @@ -0,0 +1,26 @@ +{ + "enabled": true, + "enable_logging": false, + "allowed_rules": [ + { + "host": "api.anthropic.com", + "allowed_paths": [ + "/v1/*", + "/api/*" + ] + }, + { + "host": "console.anthropic.com", + "allowed_paths": [ + "/v1/oauth/token", + "/api/*" + ] + }, + { + "host": "statsig.anthropic.com", + "allowed_paths": [ + "/v1/*" + ] + } + ] +} diff --git a/docker/tools/claude/default-docker-broker-rules.json b/docker/tools/claude/default-docker-broker-rules.json new file mode 100644 index 0000000..2926389 --- /dev/null +++ b/docker/tools/claude/default-docker-broker-rules.json @@ -0,0 +1,66 @@ +{ + "enabled": true, + "inherit_default_rules": true, + "mode": "enforce", + "enable_logging": false, + "body_inspection": { + "reject_privileged": true, + "reject_host_namespaces": true, + "reject_forbidden_binds": true, + "reject_dangerous_capabilities": true, + "allowed_images": [] + }, + "allowed_endpoints": [ + { "method": "GET", "path": "/_ping" }, + { "method": "HEAD", "path": "/_ping" }, + { "method": "GET", "path": "/version" }, + { "method": "GET", "path": "/info" }, + { "method": "GET", "path": "/containers/json" }, + { "method": "POST", "path": "/containers/create" }, + { "method": "GET", "path": "/containers/*/json" }, + { "method": "POST", "path": "/containers/*/start" }, + { "method": "POST", "path": "/containers/*/stop" }, + { "method": "POST", "path": "/containers/*/kill" }, + { "method": "POST", "path": "/containers/*/restart" }, + { "method": "POST", "path": "/containers/*/pause" }, + { "method": "POST", "path": "/containers/*/unpause" }, + { "method": "POST", "path": "/containers/*/wait" }, + { "method": "POST", "path": "/containers/*/rename" }, + { "method": "POST", "path": "/containers/*/update" }, + { "method": "POST", "path": "/containers/*/resize" }, + { "method": "DELETE", "path": "/containers/*" }, + { "method": "GET", "path": "/containers/*/logs" }, + { "method": "GET", "path": "/containers/*/stats" }, + { "method": "GET", "path": "/containers/*/top" }, + { "method": "GET", "path": "/containers/*/changes" }, + { "method": "GET", "path": "/containers/*/archive" }, + { "method": "PUT", "path": "/containers/*/archive" }, + { "method": "HEAD", "path": "/containers/*/archive" }, + { "method": "POST", "path": "/containers/*/exec" }, + { "method": "POST", "path": "/exec/*/start" }, + { "method": "POST", "path": "/exec/*/resize" }, + { "method": "GET", "path": "/exec/*/json" }, + { "method": "POST", "path": "/containers/*/attach" }, + { "method": "GET", "path": "/containers/*/attach/ws" }, + { "method": "GET", "path": "/images/json" }, + { "method": "POST", "path": "/images/create" }, + { "method": "GET", "path": "/images/**/json" }, + { "method": "GET", "path": "/images/**/history" }, + { "method": "DELETE", "path": "/images/**" }, + { "method": "POST", "path": "/images/prune" }, + { "method": "POST", "path": "/build" }, + { "method": "POST", "path": "/build/prune" }, + { "method": "GET", "path": "/networks" }, + { "method": "GET", "path": "/networks/*" }, + { "method": "POST", "path": "/networks/create" }, + { "method": "DELETE", "path": "/networks/*" }, + { "method": "POST", "path": "/networks/*/connect" }, + { "method": "POST", "path": "/networks/*/disconnect" }, + { "method": "POST", "path": "/networks/prune" }, + { "method": "GET", "path": "/volumes" }, + { "method": "GET", "path": "/volumes/*" }, + { "method": "POST", "path": "/volumes/create" }, + { "method": "DELETE", "path": "/volumes/*" }, + { "method": "POST", "path": "/volumes/prune" } + ] +} diff --git a/docs/ci-cd-pipeline.md b/docs/ci-cd-pipeline.md index 77fbec4..f99ba2b 100644 --- a/docs/ci-cd-pipeline.md +++ b/docs/ci-cd-pipeline.md @@ -86,6 +86,8 @@ All images are published to GitHub Container Registry (`ghcr.io`): - .NET image + Chromium browser dependencies - Tags: `dotnet-playwright`, `dotnet-playwright-sha-` +Each variant is built once per provider. The `copilot-*` tags ship the GitHub Copilot CLI (pinned to the version from `npm view @github/copilot version`); the matching `claude-*` tags ship Claude Code (pinned to `npm view @anthropic-ai/claude-code version`), with `claude-latest` as the default Claude image. The `build-images` matrix carries one entry per variant per provider, so adding a variant means adding both rows. + ## Running Tests Locally ### All Tests (Recommended) diff --git a/docs/docker-images.md b/docs/docker-images.md index 9e06457..1cb0980 100644 --- a/docs/docker-images.md +++ b/docs/docker-images.md @@ -2,6 +2,15 @@ This repository publishes multiple Docker image variants to optimize for size and functionality: +## Provider Image Families + +Every variant below is published twice β€” once per CLI provider: + +- **`copilot-*`** (and the unprefixed `latest`/`dotnet`/`rust`/… shorthands) ship the GitHub Copilot CLI. This is the default provider. +- **`claude-*`** ship Claude Code (`@anthropic-ai/claude-code`). The default Claude image is `claude-latest`. + +The variants are otherwise identical: same base, same toolchains, and the same security features. The tag prefix is what swaps the CLI. `copilot_here` selects the right family from the active provider (`--set-tool`); you choose the *variant* the same way regardless of provider. The examples below use the Copilot family; substitute the `claude-` prefix for the Claude equivalents (`claude-dotnet`, `claude-rust`, …). + ## Base Image: `latest` **Tag:** `ghcr.io/gordonbeeming/copilot_here:latest` diff --git a/tests/CopilotHere.UnitTests/AirlockConfigTests.cs b/tests/CopilotHere.UnitTests/AirlockConfigTests.cs index eea2327..b5fbb02 100644 --- a/tests/CopilotHere.UnitTests/AirlockConfigTests.cs +++ b/tests/CopilotHere.UnitTests/AirlockConfigTests.cs @@ -27,7 +27,8 @@ public void Setup() LocalConfigPath = localConfigPath, CurrentDirectory = _tempDir, ContainerWorkDir = "/work", - CopilotConfigPath = Path.Combine(_tempDir, ".copilot") + CopilotConfigPath = Path.Combine(_tempDir, ".copilot"), + ClaudeConfigPath = Path.Combine(_tempDir, ".claude") }; } diff --git a/tests/CopilotHere.UnitTests/ClaudeCodeToolTests.cs b/tests/CopilotHere.UnitTests/ClaudeCodeToolTests.cs new file mode 100644 index 0000000..a48ebfd --- /dev/null +++ b/tests/CopilotHere.UnitTests/ClaudeCodeToolTests.cs @@ -0,0 +1,302 @@ +using CopilotHere.Infrastructure; +using CopilotHere.Tools; +using TUnit.Core; + +namespace CopilotHere.Tests; + +public class ClaudeCodeToolTests +{ + private readonly ClaudeCodeTool _tool = new(); + + [Test] + public async Task Name_ReturnsCorrectValue() + { + await Assert.That(_tool.Name).IsEqualTo("claude"); + } + + [Test] + public async Task DisplayName_ReturnsCorrectValue() + { + await Assert.That(_tool.DisplayName).IsEqualTo("Claude Code"); + } + + [Test] + public async Task GetImageName_WithLatest_ReturnsCorrectFormat() + { + var imageName = _tool.GetImageName("latest"); + await Assert.That(imageName).IsEqualTo("ghcr.io/gordonbeeming/copilot_here:claude-latest"); + } + + [Test] + public async Task GetImageName_WithDotnet_ReturnsCorrectFormat() + { + var imageName = _tool.GetImageName("dotnet"); + await Assert.That(imageName).IsEqualTo("ghcr.io/gordonbeeming/copilot_here:claude-dotnet"); + } + + [Test] + public async Task GetImageName_WithEmptyTag_ReturnsDefault() + { + var imageName = _tool.GetImageName(""); + await Assert.That(imageName).IsEqualTo("ghcr.io/gordonbeeming/copilot_here:claude-latest"); + } + + [Test] + public async Task GetImageName_WithAbsoluteImage_ReturnsAsIs() + { + var imageName = _tool.GetImageName("myregistry.io/custom-claude:v1"); + await Assert.That(imageName).IsEqualTo("myregistry.io/custom-claude:v1"); + } + + [Test] + public async Task GetImageName_WithLocalTaggedImageNoSlash_ReturnsAsIs() + { + var imageName = _tool.GetImageName("my-local-image:dev"); + await Assert.That(imageName).IsEqualTo("my-local-image:dev"); + } + + [Test] + public async Task GetDefaultNetworkRulesPath_ReturnsCorrectPath() + { + var path = _tool.GetDefaultNetworkRulesPath(); + await Assert.That(path).IsEqualTo("docker/tools/claude/default-airlock-rules.json"); + } + + [Test] + public async Task BuildCommand_BasicMode_ContainsClaude() + { + var context = NewContext(); + + var command = _tool.BuildCommand(context); + + await Assert.That(command).IsNotEmpty(); + await Assert.That(command[0]).IsEqualTo("claude"); + } + + [Test] + public async Task BuildCommand_BasicMode_HasNoExtraFlags() + { + // With no yolo/model/args, the command is just `claude` (interactive TUI). + var context = NewContext(); + + var command = _tool.BuildCommand(context); + + await Assert.That(command.Count).IsEqualTo(1); + await Assert.That(command).DoesNotContain("--dangerously-skip-permissions"); + } + + [Test] + public async Task BuildCommand_WithYoloMode_AddsSkipPermissionsFlag() + { + var context = NewContext(isYolo: true); + + var command = _tool.BuildCommand(context); + + await Assert.That(command[0]).IsEqualTo("claude"); + await Assert.That(command).Contains("--dangerously-skip-permissions"); + } + + [Test] + public async Task BuildCommand_WithModel_AddsModelFlag() + { + var context = NewContext(model: "opus"); + + var command = _tool.BuildCommand(context); + + await Assert.That(command).Contains("--model"); + await Assert.That(command).Contains("opus"); + } + + [Test] + public async Task BuildCommand_WithUserArgs_PassesThroughArgs() + { + var context = NewContext(userArgs: ["-p", "fix the bug"]); + + var command = _tool.BuildCommand(context); + + await Assert.That(command).Contains("-p"); + await Assert.That(command).Contains("fix the bug"); + } + + [Test] + public async Task BuildCommand_ComplexScenario_AllFeaturesEnabled() + { + var context = NewContext( + userArgs: ["-p", "ship it"], + isYolo: true, + model: "claude-opus-4-8"); + + var command = _tool.BuildCommand(context); + + await Assert.That(command[0]).IsEqualTo("claude"); + await Assert.That(command).Contains("--dangerously-skip-permissions"); + await Assert.That(command).Contains("--model"); + await Assert.That(command).Contains("claude-opus-4-8"); + await Assert.That(command).Contains("-p"); + await Assert.That(command).Contains("ship it"); + } + + [Test] + public async Task GetYoloModeFlags_ReturnsCorrectFlags() + { + var flags = _tool.GetYoloModeFlags(); + + await Assert.That(flags).IsNotEmpty(); + await Assert.That(flags).Contains("--dangerously-skip-permissions"); + } + + [Test] + public async Task GetInteractiveFlag_ReturnsNull() + { + var flag = _tool.GetInteractiveFlag(); + await Assert.That(flag).IsNull(); + } + + [Test] + public async Task GetConfigDirName_ReturnsCorrectValue() + { + var dirName = _tool.GetConfigDirName(); + await Assert.That(dirName).IsEqualTo(".claude"); + } + + [Test] + public async Task GetSessionDataPath_ReturnsNull() + { + var path = _tool.GetSessionDataPath(); + await Assert.That(path).IsNull(); + } + + [Test] + public async Task GetHostConfigPath_UsesClaudeConfigPath() + { + var paths = new AppPaths + { + CurrentDirectory = "/tmp/project", + UserHome = "/tmp/home", + CopilotConfigPath = "/tmp/home/.config/copilot-cli-docker", + ClaudeConfigPath = "/tmp/home/.claude", + LocalConfigPath = "/tmp/project/.copilot_here", + GlobalConfigPath = "/tmp/home/.config/copilot_here", + ContainerWorkDir = "/work/tmp/project" + }; + + var path = _tool.GetHostConfigPath(paths); + + await Assert.That(path).IsEqualTo(paths.ClaudeConfigPath); + } + + [Test] + public async Task GetContainerConfigPath_ReturnsClaudeDirectory() + { + var path = _tool.GetContainerConfigPath(); + await Assert.That(path).IsEqualTo("/home/appuser/.claude"); + } + + [Test] + public async Task GetRequiredDependencies_ContainsDockerWithoutGh() + { + var deps = _tool.GetRequiredDependencies(); + + await Assert.That(deps).Contains("docker"); + await Assert.That(deps).DoesNotContain("gh"); + } + + [Test] + public async Task GetAuthProvider_ReturnsNonNull() + { + await Assert.That(_tool.GetAuthProvider()).IsNotNull(); + } + + [Test] + public async Task GetModelProvider_ReturnsNonNull() + { + await Assert.That(_tool.GetModelProvider()).IsNotNull(); + } + + [Test] + public async Task SupportsModels_ReturnsTrue() + { + await Assert.That(_tool.SupportsModels).IsTrue(); + } + + [Test] + public async Task ManagesOwnModelSelection_ReturnsTrue() + { + // Claude Code keeps its own model preference, so the shared model.conf must + // not be auto-passed as --model. + await Assert.That(_tool.ManagesOwnModelSelection).IsTrue(); + } + + [Test] + public async Task GetAdditionalConfigMounts_WithClaudeJson_MountsIt() + { + var home = Directory.CreateTempSubdirectory("claude-home-"); + try + { + await File.WriteAllTextAsync(Path.Combine(home.FullName, ".claude.json"), "{}"); + var paths = PathsWithHome(home.FullName); + + var mounts = _tool.GetAdditionalConfigMounts(paths); + + await Assert.That(mounts.Count).IsEqualTo(1); + await Assert.That(mounts[0].HostPath).IsEqualTo(Path.Combine(home.FullName, ".claude.json")); + await Assert.That(mounts[0].ContainerPath).IsEqualTo("/home/appuser/.claude.json"); + } + finally + { + home.Delete(recursive: true); + } + } + + [Test] + public async Task GetAdditionalConfigMounts_WithoutClaudeJson_ReturnsEmpty() + { + var home = Directory.CreateTempSubdirectory("claude-home-"); + try + { + var mounts = _tool.GetAdditionalConfigMounts(PathsWithHome(home.FullName)); + await Assert.That(mounts).IsEmpty(); + } + finally + { + home.Delete(recursive: true); + } + } + + private static AppPaths PathsWithHome(string home) => new() + { + CurrentDirectory = "/tmp/project", + UserHome = home, + CopilotConfigPath = Path.Combine(home, ".config", "copilot-cli-docker"), + ClaudeConfigPath = Path.Combine(home, ".claude"), + LocalConfigPath = "/tmp/project/.copilot_here", + GlobalConfigPath = Path.Combine(home, ".config", "copilot_here"), + ContainerWorkDir = "/work/tmp/project" + }; + + [Test] + public async Task SupportsYoloMode_ReturnsTrue() + { + await Assert.That(_tool.SupportsYoloMode).IsTrue(); + } + + [Test] + public async Task SupportsInteractiveMode_ReturnsTrue() + { + await Assert.That(_tool.SupportsInteractiveMode).IsTrue(); + } + + private static CommandContext NewContext( + List? userArgs = null, + bool isYolo = false, + string? model = null) => new() + { + UserArgs = userArgs ?? [], + IsYolo = isYolo, + IsInteractive = userArgs is null || userArgs.Count == 0, + Model = model, + ImageTag = "latest", + Mounts = [], + Environment = new Dictionary() + }; +} diff --git a/tests/CopilotHere.UnitTests/ContainerRuntimeConfigTests.cs b/tests/CopilotHere.UnitTests/ContainerRuntimeConfigTests.cs index daf22fc..40f457b 100644 --- a/tests/CopilotHere.UnitTests/ContainerRuntimeConfigTests.cs +++ b/tests/CopilotHere.UnitTests/ContainerRuntimeConfigTests.cs @@ -31,6 +31,7 @@ public void Setup() CurrentDirectory = _projectDir, UserHome = _tempDir, CopilotConfigPath = Path.Combine(_tempDir, ".config", "copilot-cli-docker"), + ClaudeConfigPath = Path.Combine(_tempDir, ".claude"), LocalConfigPath = localConfigPath, GlobalConfigPath = globalConfigPath, ContainerWorkDir = "/workspace" diff --git a/tests/CopilotHere.UnitTests/DockerBrokerConfigTests.cs b/tests/CopilotHere.UnitTests/DockerBrokerConfigTests.cs index 7d46fc6..3364c41 100644 --- a/tests/CopilotHere.UnitTests/DockerBrokerConfigTests.cs +++ b/tests/CopilotHere.UnitTests/DockerBrokerConfigTests.cs @@ -27,7 +27,8 @@ public void Setup() LocalConfigPath = localConfigPath, CurrentDirectory = _tempDir, ContainerWorkDir = "/work", - CopilotConfigPath = Path.Combine(_tempDir, ".copilot") + CopilotConfigPath = Path.Combine(_tempDir, ".copilot"), + ClaudeConfigPath = Path.Combine(_tempDir, ".claude") }; } diff --git a/tests/CopilotHere.UnitTests/EchoToolTests.cs b/tests/CopilotHere.UnitTests/EchoToolTests.cs index d6a8691..3cfee6a 100644 --- a/tests/CopilotHere.UnitTests/EchoToolTests.cs +++ b/tests/CopilotHere.UnitTests/EchoToolTests.cs @@ -301,6 +301,7 @@ public async Task GetHostConfigPath_UsesCopilotConfigPath() CurrentDirectory = "/tmp/project", UserHome = "/tmp/home", CopilotConfigPath = "/tmp/home/.config/copilot-cli-docker", + ClaudeConfigPath = "/tmp/home/.claude", LocalConfigPath = "/tmp/project/.copilot_here", GlobalConfigPath = "/tmp/home/.config/copilot_here", ContainerWorkDir = "/work/tmp/project" diff --git a/tests/CopilotHere.UnitTests/GitHubCopilotToolTests.cs b/tests/CopilotHere.UnitTests/GitHubCopilotToolTests.cs index 8398a05..cce0e31 100644 --- a/tests/CopilotHere.UnitTests/GitHubCopilotToolTests.cs +++ b/tests/CopilotHere.UnitTests/GitHubCopilotToolTests.cs @@ -336,6 +336,7 @@ public async Task GetHostConfigPath_UsesCopilotConfigPath() CurrentDirectory = "/tmp/project", UserHome = "/tmp/home", CopilotConfigPath = "/tmp/home/.config/copilot-cli-docker", + ClaudeConfigPath = "/tmp/home/.claude", LocalConfigPath = "/tmp/project/.copilot_here", GlobalConfigPath = "/tmp/home/.config/copilot_here", ContainerWorkDir = "/work/tmp/project" diff --git a/tests/CopilotHere.UnitTests/ImageConfigTests.cs b/tests/CopilotHere.UnitTests/ImageConfigTests.cs index 3c55c39..cc8ca4e 100644 --- a/tests/CopilotHere.UnitTests/ImageConfigTests.cs +++ b/tests/CopilotHere.UnitTests/ImageConfigTests.cs @@ -27,7 +27,8 @@ public void Setup() LocalConfigPath = localConfigPath, CurrentDirectory = _tempDir, ContainerWorkDir = "/work", - CopilotConfigPath = Path.Combine(_tempDir, ".copilot") + CopilotConfigPath = Path.Combine(_tempDir, ".copilot"), + ClaudeConfigPath = Path.Combine(_tempDir, ".claude") }; } diff --git a/tests/CopilotHere.UnitTests/ModelConfigTests.cs b/tests/CopilotHere.UnitTests/ModelConfigTests.cs index df8bfab..a0f7e60 100644 --- a/tests/CopilotHere.UnitTests/ModelConfigTests.cs +++ b/tests/CopilotHere.UnitTests/ModelConfigTests.cs @@ -27,7 +27,8 @@ public void Setup() LocalConfigPath = localConfigPath, CurrentDirectory = _tempDir, ContainerWorkDir = "/work", - CopilotConfigPath = Path.Combine(_tempDir, ".copilot") + CopilotConfigPath = Path.Combine(_tempDir, ".copilot"), + ClaudeConfigPath = Path.Combine(_tempDir, ".claude") }; } diff --git a/tests/CopilotHere.UnitTests/MountDeduplicationTests.cs b/tests/CopilotHere.UnitTests/MountDeduplicationTests.cs index 324e5a4..ac719bd 100644 --- a/tests/CopilotHere.UnitTests/MountDeduplicationTests.cs +++ b/tests/CopilotHere.UnitTests/MountDeduplicationTests.cs @@ -28,7 +28,8 @@ public void Setup() LocalConfigPath = localConfigPath, CurrentDirectory = _tempDir, ContainerWorkDir = "/work", - CopilotConfigPath = Path.Combine(_tempDir, ".copilot") + CopilotConfigPath = Path.Combine(_tempDir, ".copilot"), + ClaudeConfigPath = Path.Combine(_tempDir, ".claude") }; } diff --git a/tests/CopilotHere.UnitTests/MountsConfigTests.cs b/tests/CopilotHere.UnitTests/MountsConfigTests.cs index 1ba7858..431c648 100644 --- a/tests/CopilotHere.UnitTests/MountsConfigTests.cs +++ b/tests/CopilotHere.UnitTests/MountsConfigTests.cs @@ -27,7 +27,8 @@ public void Setup() LocalConfigPath = localConfigPath, CurrentDirectory = _tempDir, ContainerWorkDir = "/work", - CopilotConfigPath = Path.Combine(_tempDir, ".copilot") + CopilotConfigPath = Path.Combine(_tempDir, ".copilot"), + ClaudeConfigPath = Path.Combine(_tempDir, ".claude") }; } diff --git a/tests/CopilotHere.UnitTests/ToolRegistryTests.cs b/tests/CopilotHere.UnitTests/ToolRegistryTests.cs index 21ac4ff..0e2dbe0 100644 --- a/tests/CopilotHere.UnitTests/ToolRegistryTests.cs +++ b/tests/CopilotHere.UnitTests/ToolRegistryTests.cs @@ -17,6 +17,18 @@ public async Task Get_ValidTool_GitHubCopilot_ReturnsCorrectTool() await Assert.That(tool.DisplayName).IsEqualTo("GitHub Copilot CLI"); } + [Test] + public async Task Get_ValidTool_Claude_ReturnsCorrectTool() + { + // Act + var tool = ToolRegistry.Get("claude"); + + // Assert + await Assert.That(tool).IsNotNull(); + await Assert.That(tool.Name).IsEqualTo("claude"); + await Assert.That(tool.DisplayName).IsEqualTo("Claude Code"); + } + [Test] public async Task Get_ValidTool_Echo_ReturnsCorrectTool() { From eccc317725a2721865cdd421289e44b673324038 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 08:26:20 +0000 Subject: [PATCH 2/2] chore(docker): regenerate Dockerfiles after snippet/config change --- docker/generated/Dockerfile.claude-default | 20 +++++++++++++++++++ docker/generated/Dockerfile.claude-dotnet | 20 +++++++++++++++++++ docker/generated/Dockerfile.claude-dotnet-10 | 20 +++++++++++++++++++ docker/generated/Dockerfile.claude-dotnet-8 | 20 +++++++++++++++++++ docker/generated/Dockerfile.claude-dotnet-9 | 20 +++++++++++++++++++ .../Dockerfile.claude-dotnet-playwright | 20 +++++++++++++++++++ .../generated/Dockerfile.claude-dotnet-rust | 20 +++++++++++++++++++ docker/generated/Dockerfile.claude-golang | 20 +++++++++++++++++++ docker/generated/Dockerfile.claude-java | 20 +++++++++++++++++++ docker/generated/Dockerfile.claude-playwright | 20 +++++++++++++++++++ docker/generated/Dockerfile.claude-rust | 20 +++++++++++++++++++ 11 files changed, 220 insertions(+) diff --git a/docker/generated/Dockerfile.claude-default b/docker/generated/Dockerfile.claude-default index 9518b6a..b509b0d 100644 --- a/docker/generated/Dockerfile.claude-default +++ b/docker/generated/Dockerfile.claude-default @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,30 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3 \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin β€” otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.claude-dotnet b/docker/generated/Dockerfile.claude-dotnet index 73094eb..42f1c49 100644 --- a/docker/generated/Dockerfile.claude-dotnet +++ b/docker/generated/Dockerfile.claude-dotnet @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,30 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3 \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin β€” otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.claude-dotnet-10 b/docker/generated/Dockerfile.claude-dotnet-10 index e9cc602..f031423 100644 --- a/docker/generated/Dockerfile.claude-dotnet-10 +++ b/docker/generated/Dockerfile.claude-dotnet-10 @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,30 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3 \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin β€” otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.claude-dotnet-8 b/docker/generated/Dockerfile.claude-dotnet-8 index d5a6a9c..0cee5ae 100644 --- a/docker/generated/Dockerfile.claude-dotnet-8 +++ b/docker/generated/Dockerfile.claude-dotnet-8 @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,30 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3 \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin β€” otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.claude-dotnet-9 b/docker/generated/Dockerfile.claude-dotnet-9 index c8fe102..478838c 100644 --- a/docker/generated/Dockerfile.claude-dotnet-9 +++ b/docker/generated/Dockerfile.claude-dotnet-9 @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,30 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3 \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin β€” otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.claude-dotnet-playwright b/docker/generated/Dockerfile.claude-dotnet-playwright index ef73286..e396e5e 100644 --- a/docker/generated/Dockerfile.claude-dotnet-playwright +++ b/docker/generated/Dockerfile.claude-dotnet-playwright @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,30 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3 \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin β€” otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.claude-dotnet-rust b/docker/generated/Dockerfile.claude-dotnet-rust index 100b98e..0fd4d67 100644 --- a/docker/generated/Dockerfile.claude-dotnet-rust +++ b/docker/generated/Dockerfile.claude-dotnet-rust @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,30 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3 \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin β€” otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.claude-golang b/docker/generated/Dockerfile.claude-golang index 1071537..e255b3c 100644 --- a/docker/generated/Dockerfile.claude-golang +++ b/docker/generated/Dockerfile.claude-golang @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,30 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3 \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin β€” otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.claude-java b/docker/generated/Dockerfile.claude-java index a71c39b..4d27f3a 100644 --- a/docker/generated/Dockerfile.claude-java +++ b/docker/generated/Dockerfile.claude-java @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,30 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3 \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin β€” otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.claude-playwright b/docker/generated/Dockerfile.claude-playwright index 29a3c67..8620afb 100644 --- a/docker/generated/Dockerfile.claude-playwright +++ b/docker/generated/Dockerfile.claude-playwright @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,30 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3 \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin β€” otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker. diff --git a/docker/generated/Dockerfile.claude-rust b/docker/generated/Dockerfile.claude-rust index f8f9b2d..74551c7 100644 --- a/docker/generated/Dockerfile.claude-rust +++ b/docker/generated/Dockerfile.claude-rust @@ -10,6 +10,8 @@ FROM node:20-slim ENV DEBIAN_FRONTEND=noninteractive # Install git, curl, gpg, gosu, nano, xdg-utils, zsh, and related utilities for the entrypoint script and testing. +# Python tooling (pip + venv + pipx) ships here so every image can install pip-distributed +# CLIs; Debian 12 enforces PEP 668, so `pipx install ` is the supported path. RUN apt-get update && apt-get install -y \ apt-transport-https \ curl \ @@ -17,12 +19,30 @@ RUN apt-get update && apt-get install -y \ gosu \ gpg \ nano \ + pipx \ + python3 \ + python3-pip \ + python3-venv \ software-properties-common \ wget \ xdg-utils \ zsh \ && rm -rf /var/lib/apt/lists/* +# pipx installs apps into the runtime user's ~/.local/bin, so that dir needs to be on PATH +# for pipx-installed CLIs (e.g. `pipx install apm`) to be reachable without a per-session +# `pipx ensurepath`. The entrypoint always runs as appuser with HOME=/home/appuser. +# Append (not prepend): the entrypoint runs as root and resolves tools like getent/groupadd/ +# node via PATH before dropping to appuser, so a system binary must always win over anything +# in the user-writable (and possibly bind-mounted) ~/.local/bin β€” otherwise a planted binary +# there could execute as root. Appending keeps pipx apps reachable while system paths stay +# authoritative; pipx app names don't collide with system binaries. +# ENV covers the exec'd command and non-login shells; the profile.d drop-in re-adds it for +# login shells, which otherwise reset PATH via /etc/profile (Debian zsh sources it too). +ENV PATH="${PATH}:/home/appuser/.local/bin" +RUN echo 'case ":${PATH}:" in *:/home/appuser/.local/bin:*) ;; *) PATH="${PATH:+${PATH}:}/home/appuser/.local/bin" ;; esac' \ + > /etc/profile.d/copilot-local-bin.sh + # --- snippet: docker-cli --- # Install Docker CLI to support brokered Docker socket scenarios. # Only the CLI is installed; the daemon is the host's, mediated by copilot_here's broker.