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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion backend/Dockerfile.worker
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,14 @@ RUN dotnet restore CodeSpace.sln
RUN dotnet publish src/CodeSpace.Api/CodeSpace.Api.csproj -c Release -o /app --no-restore

# ── 2. The Node-based harness CLIs, version-pinned (override at build time with --build-arg) ──
# These two ARG defaults are the SINGLE SOURCE OF TRUTH for the pinned CLI versions. A test
# (HarnessVersionPinTests) asserts CodexHarness.DefaultVersion / ClaudeCodeHarness.DefaultVersion match these exact
# strings, so the harness-reported version can NEVER silently drift from what the worker actually installs — bump
# here and the test fails until the C# constants follow. `deploy/sync-local-harnesses.sh` installs these same pins
# locally, so a dev box matches the worker. Keep all three (this file · the harness consts · a local install) in lockstep.
FROM node:20-bookworm-slim AS agent-cli
ARG CODEX_CLI_VERSION=0.142.2
ARG CLAUDE_CODE_VERSION=2.1.191
ARG CLAUDE_CODE_VERSION=2.1.193
RUN npm install -g "@openai/codex@${CODEX_CLI_VERSION}" "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}"

# ── 3. Runtime: agent-execution + isolation deps + the Node runtime & CLIs + the published app ──
Expand Down
29 changes: 29 additions & 0 deletions backend/deploy/sync-local-harnesses.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Sync the LOCAL agent-CLI harnesses to the EXACT versions the worker image pins — so a dev box runs the same codex
# /claude the worker does, and the harness-reported version (CodexHarness/ClaudeCodeHarness.DefaultVersion, pinned to
# these same ARGs by HarnessVersionPinTests) stays honest. The single source of truth is backend/Dockerfile.worker.
#
# backend/deploy/sync-local-harnesses.sh
#
# After a version bump, the lockstep is: edit the Dockerfile ARG → update the matching DefaultVersion C# const (the
# pin test enforces this) → run this script on every dev box. Nothing to remember beyond "bump the ARG, run this".
set -euo pipefail

DOCKERFILE="$(cd "$(dirname "$0")/.." && pwd)/Dockerfile.worker"
[ -f "$DOCKERFILE" ] || { echo "✗ $DOCKERFILE not found"; exit 1; }

codex_v=$(grep -oE 'ARG CODEX_CLI_VERSION=[^[:space:]]+' "$DOCKERFILE" | head -1 | cut -d= -f2)
claude_v=$(grep -oE 'ARG CLAUDE_CODE_VERSION=[^[:space:]]+' "$DOCKERFILE" | head -1 | cut -d= -f2)
echo "Worker-pinned versions: codex=$codex_v claude=$claude_v"

npm install -g "@openai/codex@${codex_v}" "@anthropic-ai/claude-code@${claude_v}"

# Verify the EFFECTIVE (PATH) binaries match — a native claude/codex install can shadow the npm one.
check() { # $1=binary $2=expected
local got; got=$("$1" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "missing")
if [ "$got" = "$2" ]; then echo "✓ $1 $got"; else
echo "⚠ $1 on PATH is $got, expected $2 — a native install may be shadowing npm. Update it (e.g. 'claude update') or fix PATH order."
fi
}
check codex "$codex_v"
check claude "$claude_v"
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ public sealed class ClaudeCodeHarness : IAgentHarness, IModelCredentialProjector

private const string AnthropicProvider = "Anthropic";

private const string DefaultVersion = "2.1.0";
/// <summary>The pinned Claude Code CLI version — MUST match <c>CLAUDE_CODE_VERSION</c> in <c>backend/Dockerfile.worker</c> (the single source of truth); a pin test fails if they drift.</summary>
internal const string DefaultVersion = "2.1.193";

private const string DefaultCommand = "claude";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public sealed class CodexHarness : IAgentHarness, IModelCredentialProjector, IMc
/// </summary>
public const string ConfigHomeEnvVar = "CODEX_HOME";

private const string DefaultVersion = "0.2.0";
/// <summary>The pinned Codex CLI version — MUST match <c>CODEX_CLI_VERSION</c> in <c>backend/Dockerfile.worker</c> (the single source of truth); a pin test fails if they drift.</summary>
internal const string DefaultVersion = "0.142.2";

private const string DefaultCommand = "codex";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ public void Version_uses_the_default_then_the_env_override()
try
{
System.Environment.SetEnvironmentVariable(CodexHarness.VersionEnvVar, null);
new CodexHarness().Version.ShouldBe("0.2.0");
new CodexHarness().Version.ShouldBe(CodexHarness.DefaultVersion); // tracks the Dockerfile pin (HarnessVersionPinTests)

System.Environment.SetEnvironmentVariable(CodexHarness.VersionEnvVar, "9.9.9");
new CodexHarness().Version.ShouldBe("9.9.9");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using CodeSpace.Core.Services.Agents.Harnesses.Claude;
using CodeSpace.Core.Services.Agents.Harnesses.Codex;
using Shouldly;

namespace CodeSpace.UnitTests.Workflows;

/// <summary>
/// Pins each harness's <c>DefaultVersion</c> constant to the SINGLE SOURCE OF TRUTH — the
/// <c>CODEX_CLI_VERSION</c> / <c>CLAUDE_CODE_VERSION</c> ARG in <c>backend/Dockerfile.worker</c> (the version the
/// worker image actually installs). A bump in the Dockerfile that isn't mirrored into the C# constant (or vice
/// versa) FAILS here, so the harness-reported version can never silently drift from what the worker runs. The third
/// surface — a developer's local install — is synced from the same ARG by <c>deploy/sync-local-harnesses.sh</c>.
/// </summary>
[Trait("Category", "Unit")]
public class HarnessVersionPinTests
{
[Fact]
public void Codex_default_version_matches_the_worker_dockerfile_pin() =>
DockerfileArg("CODEX_CLI_VERSION").ShouldBe(CodexHarness.DefaultVersion);

[Fact]
public void Claude_default_version_matches_the_worker_dockerfile_pin() =>
DockerfileArg("CLAUDE_CODE_VERSION").ShouldBe(ClaudeCodeHarness.DefaultVersion);

private static string DockerfileArg(string name)
{
var content = File.ReadAllText(LocateWorkerDockerfile());
var match = Regex.Match(content, $@"ARG\s+{Regex.Escape(name)}=(\S+)");

match.Success.ShouldBeTrue($"ARG {name} not found in backend/Dockerfile.worker");
return match.Groups[1].Value;
}

private static string LocateWorkerDockerfile()
{
for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent)
{
var candidate = Path.Combine(dir.FullName, "backend", "Dockerfile.worker");
if (File.Exists(candidate)) return candidate;
}

throw new FileNotFoundException("backend/Dockerfile.worker not found walking up from " + AppContext.BaseDirectory);
}
}
Loading