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
10 changes: 5 additions & 5 deletions .github/workflows/real-model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1058,12 +1058,12 @@ jobs:
# BOTH harness binaries: claude (ClaudeCodeHarness) drives the blessed behavioral gate; codex (CodexHarness)
# drives the informational behavioral proof + the deterministic real-binary proofs. FATAL install (a missing
# binary must RED the lane, never let a gating arm self-skip green — "skip ≠ pass"). Pins match the worker image
# ARGs + the harness DefaultVersion consts the CLI surface is verified against (claude 2.1.193 / codex 0.142.2),
# ARGs + the harness DefaultVersion consts the CLI surface is verified against (claude 2.1.263 / codex 0.142.2),
# so a future CLI flag change is a visible lane break to fix, not a silent harness regression.
run: |
# One retry each to absorb a transient npm-registry blip (ECONNRESET / 5xx) without false-red-ing the lane; a
# genuinely missing/yanked binary still fails after the retry → reds (the install is fatal, not flaky).
npm install -g '@anthropic-ai/claude-code@2.1.193' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.193'; }
npm install -g '@anthropic-ai/claude-code@2.1.263' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.263'; }
npm install -g '@openai/codex@0.142.2' || { sleep 5; npm install -g '@openai/codex@0.142.2'; }
claude --version
codex --version
Expand Down Expand Up @@ -1206,7 +1206,7 @@ jobs:
- name: Install the real coding-agent CLI (Claude Code)
# FATAL install (a missing binary must RED the lane, never let the gating arm self-skip green — "skip ≠ pass").
run: |
npm install -g '@anthropic-ai/claude-code@2.1.193' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.193'; }
npm install -g '@anthropic-ai/claude-code@2.1.263' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.263'; }
claude --version
git --version

Expand Down Expand Up @@ -1333,7 +1333,7 @@ jobs:
- name: Install the real coding-agent CLI (Claude Code)
# FATAL install (a missing binary must RED the lane, never let the gating agent-feed arm self-skip green — "skip ≠ pass").
run: |
npm install -g '@anthropic-ai/claude-code@2.1.193' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.193'; }
npm install -g '@anthropic-ai/claude-code@2.1.263' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.263'; }
claude --version
git --version

Expand Down Expand Up @@ -1460,7 +1460,7 @@ jobs:
# drives the hooks.json Stop hook. FATAL install (a missing binary must RED the lane, never let it self-skip
# green — "skip ≠ pass"). Pins match the injection lane's own pinned versions.
run: |
npm install -g '@anthropic-ai/claude-code@2.1.193' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.193'; }
npm install -g '@anthropic-ai/claude-code@2.1.263' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.263'; }
npm install -g '@openai/codex@0.142.2' || { sleep 5; npm install -g '@openai/codex@0.142.2'; }
claude --version
codex --version
Expand Down
9 changes: 7 additions & 2 deletions backend/Dockerfile.worker
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,14 @@ RUN SHA="${SOURCE_REVISION_ID:-$(git rev-parse HEAD 2>/dev/null || echo unknown)
# 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
#
# The Node major must satisfy the STRICTEST `engines.node` of the two pinned CLIs: claude-code 2.1.263 declares
# >=22 (2.1.193 declared >=18), codex 0.142.2 declares >=16. On node:20 npm still installs claude-code, but only
# because EBADENGINE is a WARNING — an `engine-strict` npm, or a future release that actually uses node-22 syntax
# in its install script, turns that warning into a failed image build. Track the floor rather than the warning.
FROM node:22-bookworm-slim AS agent-cli
ARG CODEX_CLI_VERSION=0.142.2
ARG CLAUDE_CODE_VERSION=2.1.193
ARG CLAUDE_CODE_VERSION=2.1.263
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
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public sealed class ClaudeCodeHarness : IAgentHarness, IAgentHarnessContractGene
private const string AnthropicProvider = "Anthropic";

/// <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";
internal const string DefaultVersion = "2.1.263";

private const string DefaultCommand = "claude";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ namespace CodeSpace.Core.Services.Agents.Harnesses.Claude;
/// Reproduces Claude Code's transcript-file location so a CONTINUE can RESTORE a prior session's JSONL where the CLI
/// looks for it on <c>--resume</c>. Claude stores a session at
/// <c>&lt;CLAUDE_CONFIG_DIR&gt;/projects/&lt;sanitized-cwd&gt;/&lt;session-id&gt;.jsonl</c>. The sanitizer is a BYTE-FOR-BYTE
/// port of the real claude 2.1.193 encoder (extracted from the binary): replace every char outside <c>[A-Za-z0-9]</c>
/// port of the real claude 2.1.263 encoder (extracted from the binary): replace every char outside <c>[A-Za-z0-9]</c>
/// with <c>-</c>, and when the result exceeds 200 chars truncate to 200 and append <c>-&lt;base36 hash&gt;</c> of the
/// ORIGINAL cwd (so deep paths still map to a stable, collision-resistant dir). Pinned by <c>ClaudeTranscriptPathTests</c>
/// against ground-truth pairs produced by the real algorithm.
/// against ground-truth pairs produced by the real algorithm. Unchanged since the 2.1.193 pin this port was first taken
/// from — only the minifier's symbol names moved (<c>ab/Byu/hRe/pXe</c> became <c>RA/Te/gz/az</c>), and
/// <c>defaultPath()</c> still builds the segment as <c>RA(cwd)</c> under <c>projects/</c>.
/// <code>
/// function ab(e){let t=e.replace(/[^a-zA-Z0-9]/g,"-");if(t.length&lt;=200)return t;return `${t.slice(0,200)}-${Byu(e)}`}
/// function Byu(e){return Math.abs(hRe(e)).toString(36)}
/// function hRe(e){let t=0;for(let n=0;n&lt;e.length;n++)t=(t&lt;&lt;5)-t+e.charCodeAt(n)|0;return t}
/// function RA(e){let n=k(e);if(n.length&lt;=az)return n;return `${n.slice(0,az)}-${Te(e)}`} // az=200
/// function k(e){return e.replace(/[^a-zA-Z0-9]/g,"-")}
/// function Te(e){return Math.abs(gz(e)).toString(36)}
/// function gz(t){let e=0;for(let r=0;r&lt;t.length;r++)e=(e&lt;&lt;5)-e+t.charCodeAt(r)|0;return e}
/// </code>
/// <para><b>The sharpest P3 hazard</b>: the cwd MUST be the RESOLVED real path the agent process runs in — on macOS
/// <c>/var/…</c> resolves to <c>/private/var/…</c>, and under bubblewrap it is the <c>--chdir</c> host path. Encoding the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ public async Task A_plane_that_refuses_to_open_or_to_write_leaves_the_parse_path
[Theory]
[InlineData("codex-cli", "0.142.2", 1, "codex-cli/v1")]
[InlineData("codex-cli", "0.142.2", null, "codex-cli/v1")]
[InlineData("claude-code", "2.1.193", 2, "claude-code/v2")]
[InlineData("claude-code", "2.1.263", 2, "claude-code/v2")]
[InlineData("claude-code", "3.0.0", 2, "claude-code/v2")]
[InlineData("scripted", "9.9.9", 2, "scripted/v2")]
[InlineData("scripted", "2.0.0", null, "scripted/v1")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace CodeSpace.UnitTests.Workflows;
/// <summary>
/// 🟢 Unit: <see cref="ClaudeTranscriptPath"/> reproduces Claude Code's transcript-file location so a CONTINUE can
/// RESTORE a prior session's JSONL where the CLI looks for it on <c>--resume</c>. The known-pairs are the LOAD-BEARING
/// pin: they are REAL <c>~/.claude/projects</c> directory names observed on a machine running claude 2.1.193, so a
/// pin: they are REAL <c>~/.claude/projects</c> directory names observed on a machine running claude 2.1.263, so a
/// drift in the cwd→sanitized-dir encoding fails HERE at test time rather than as a silent failed real-CLI resume
/// (the sharpest P3 hazard — a mismatch lands the transcript under the wrong dir and <c>--resume</c> cold-starts with
/// no error).
Expand All @@ -15,14 +15,17 @@ namespace CodeSpace.UnitTests.Workflows;
public class ClaudeTranscriptPathTests
{
[Theory]
// Ground truth — a byte-exact port of the real claude 2.1.193 `ab()`: replace every char outside [A-Za-z0-9] with
// Ground truth — a byte-exact port of the real claude 2.1.263 `RA()`: replace every char outside [A-Za-z0-9] with
// '-' (so '/', '.', AND '_' all become '-'); alphanumerics + existing '-' survive (they map to themselves).
[InlineData("/Users/mars/Projects/CodeSpace", "-Users-mars-Projects-CodeSpace")] // real ~/.claude/projects dir
[InlineData("/Users/mars/Projects/CodeSpace/backend/src/CodeSpace.Core", "-Users-mars-Projects-CodeSpace-backend-src-CodeSpace-Core")] // real dir; the '.' in CodeSpace.Core → '-'
[InlineData("/private/var/folders/z7/qrtkqj255vs6dg3wjfkgcn380000gn/T/codespace-agent-workspaces/05e4e233e0c5482985cbddd01d1a72a4",
"-private-var-folders-z7-qrtkqj255vs6dg3wjfkgcn380000gn-T-codespace-agent-workspaces-05e4e233e0c5482985cbddd01d1a72a4")] // resolved agent-workspace cwd (/private, not /var)
[InlineData("/Users/john_doe/my_project", "-Users-john-doe-my-project")] // UNDERSCORE → '-' (the real binary does NOT preserve '_') — ground truth via the extracted ab()
[InlineData("/Users/john_doe/my_project", "-Users-john-doe-my-project")] // UNDERSCORE → '-' (the real binary does NOT preserve '_') — ground truth via the extracted RA()
[InlineData("/Users/a_b/x.y/z", "-Users-a-b-x-y-z")] // mixed '_' + '.' → '-'
// Observed live on 2.1.263: the real CLI, run from this cwd, created exactly this dir under its projects/ home.
// Covers the classes the pairs above miss — a SPACE and NON-ASCII (each UTF-16 unit → its own '-', so '项目' → '--').
[InlineData("/private/tmp/cs enc/my_project.v2/项目-x", "-private-tmp-cs-enc-my-project-v2----x")]
public void EncodeCwd_matches_the_real_claude_encoder(string cwd, string expected) =>
ClaudeTranscriptPath.EncodeCwd(cwd).ShouldBe(expected);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using CodeSpace.Core.Services.Agents.Harnesses.Claude;
using CodeSpace.Core.Services.Agents.Harnesses.Codex;
Expand All @@ -12,7 +13,9 @@ namespace CodeSpace.UnitTests.Workflows;
/// <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>.
/// surface — a developer's local install — is synced from the same ARG by <c>deploy/sync-local-harnesses.sh</c>. The
/// fourth is <c>.github/workflows/real-model.yml</c>'s EXACT-pinned installs, the lanes that verify the CLI surface
/// the harness argv targets; they must name the shipped version or the gate certifies a CLI nobody runs.
/// </summary>
[Trait("Category", "Unit")]
public class HarnessVersionPinTests
Expand All @@ -25,6 +28,25 @@ public void Codex_default_version_matches_the_worker_dockerfile_pin() =>
public void Claude_default_version_matches_the_worker_dockerfile_pin() =>
DockerfileArg("CLAUDE_CODE_VERSION").ShouldBe(ClaudeCodeHarness.DefaultVersion);

/// <summary>
/// The FOURTH surface: <c>real-model.yml</c>'s exact-pinned installs — the lanes whose whole job is to verify the
/// CLI surface the harness argv targets. An exact pin there that lags the worker's ARG means the gate certifies a
/// version the product does not ship, which is precisely the silent drift the other pins exist to prevent. Only
/// EXACT pins are asserted; the deliberately FLOATING <c>@~2.1.0</c> lanes are excluded by the version pattern, so
/// they keep tracking the newest 2.1.x without failing here.
/// </summary>
[Fact]
public void Claude_exact_workflow_pins_match_the_worker_dockerfile_pin()
{
var pinned = DockerfileArg("CLAUDE_CODE_VERSION");
var matches = Regex.Matches(File.ReadAllText(LocateRealModelWorkflow()), @"@anthropic-ai/claude-code@(\d+\.\d+\.\d+)");

matches.Count.ShouldBeGreaterThan(0, "real-model.yml must keep at least one exact-pinned claude-code install — the lane that verifies the CLI surface");

foreach (var version in matches.Select(m => m.Groups[1].Value).Distinct())
version.ShouldBe(pinned, $"an exact '@anthropic-ai/claude-code@{version}' install in .github/workflows/real-model.yml lags CLAUDE_CODE_VERSION in backend/Dockerfile.worker — the lane would certify a CLI the worker image never installs");
}

private static string DockerfileArg(string name)
{
var content = File.ReadAllText(LocateWorkerDockerfile());
Expand All @@ -34,14 +56,20 @@ private static string DockerfileArg(string name)
return match.Groups[1].Value;
}

private static string LocateWorkerDockerfile()
private static string LocateWorkerDockerfile() => LocateRepoFile("backend", "Dockerfile.worker");

private static string LocateRealModelWorkflow() => LocateRepoFile(".github", "workflows", "real-model.yml");

private static string LocateRepoFile(params string[] segments)
{
var relative = Path.Combine(segments);

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

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