inspect-swe 0.2.1__py3-none-any.whl → 0.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
inspect_swe/__init__.py CHANGED
@@ -1,4 +1,6 @@
1
1
  from ._claude_code.claude_code import claude_code
2
+ from ._claude_code.install.download import download_claude_code
3
+ from ._util.sandbox import SandboxPlatform
2
4
 
3
5
  try:
4
6
  from ._version import __version__
@@ -6,4 +8,4 @@ except ImportError:
6
8
  __version__ = "unknown"
7
9
 
8
10
 
9
- __all__ = ["claude_code", "__version__"]
11
+ __all__ = ["claude_code", "download_claude_code", "SandboxPlatform", "__version__"]
@@ -1,3 +1,5 @@
1
+ from typing import Literal
2
+
1
3
  from inspect_ai.agent import (
2
4
  Agent,
3
5
  AgentState,
@@ -5,16 +7,44 @@ from inspect_ai.agent import (
5
7
  sandbox_agent_bridge,
6
8
  )
7
9
  from inspect_ai.model import ChatMessageSystem, ChatMessageUser
8
- from inspect_ai.util import sandbox
10
+ from inspect_ai.util import sandbox as sandbox_env
11
+
12
+ from inspect_swe._claude_code.install.install import ensure_claude_code_installed
9
13
 
10
14
 
11
15
  @agent
12
- def claude_code() -> Agent:
16
+ def claude_code(
17
+ version: Literal["auto", "sandbox", "stable", "latest"] | str = "auto",
18
+ user: str | None = None,
19
+ sandbox: str | None = None,
20
+ ) -> Agent:
21
+ """Claude Code agent.
22
+
23
+ Agent that uses [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) running in a sandbox.
24
+
25
+ The agent can either use a version of Claude Code installed in the sandbox, or can download a version and install it in the sandbox (see docs on `version` option below for details).
26
+
27
+ Args:
28
+ version: Version of claude code to use. One of:
29
+ - "auto": Use any available version of claude code in the sandbox, otherwise download the current stable version.
30
+ - "sandbox": Use the version of claude code in the sandbox (raises `RuntimeError` if claude is not available in the sandbox)
31
+ - "stable": Download and use the current stable version of claude code.
32
+ - "latest": Download and use the very latest version of claude code.
33
+ - "x.x.x": Download and use a specific version of claude code.
34
+ user: User to execute claude code with.
35
+ sandbox: Optional sandbox environment name.
36
+ """
37
+
13
38
  async def execute(state: AgentState) -> AgentState:
14
39
  async with sandbox_agent_bridge(state) as bridge:
40
+ # ensure claude is installed and get binary location
41
+ claude_binary = await ensure_claude_code_installed(
42
+ version, user, sandbox_env(sandbox)
43
+ )
44
+
15
45
  # base options
16
46
  cmd = [
17
- "claude",
47
+ claude_binary,
18
48
  "--print", # run without interactions
19
49
  "--dangerously-skip-permissions",
20
50
  "--model", # use current inspect model
@@ -35,7 +65,7 @@ def claude_code() -> Agent:
35
65
  cmd.append(prompt)
36
66
 
37
67
  # execute the agent
38
- result = await sandbox().exec(
68
+ result = await sandbox_env(sandbox).exec(
39
69
  cmd=cmd,
40
70
  env={
41
71
  "ANTHROPIC_BASE_URL": f"http://localhost:{bridge.port}",
@@ -44,6 +74,7 @@ def claude_code() -> Agent:
44
74
  "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
45
75
  "IS_SANDBOX": "1",
46
76
  },
77
+ user=user,
47
78
  )
48
79
 
49
80
  if result.success:
File without changes
@@ -0,0 +1,58 @@
1
+ from pathlib import Path
2
+
3
+ from inspect_swe._util.sandbox import SandboxPlatform
4
+
5
+ from ..._util.appdirs import package_cache_dir
6
+ from ..._util.checksum import verify_checksum
7
+
8
+
9
+ def read_cached_claude_code_binary(
10
+ version: str, platform: SandboxPlatform, expected_checksum: str | None
11
+ ) -> bytes | None:
12
+ # no cached binary
13
+ cache_path = _claude_code_cached_binary(version, platform)
14
+ if not cache_path.exists():
15
+ return None
16
+
17
+ # read binary
18
+ with open(cache_path, "rb") as f:
19
+ binary_data = f.read()
20
+
21
+ if expected_checksum is None or verify_checksum(binary_data, expected_checksum):
22
+ cache_path.touch()
23
+ return binary_data
24
+ else:
25
+ cache_path.unlink()
26
+ return None
27
+
28
+
29
+ def write_cached_claude_code_binary(
30
+ binary_data: bytes, version: str, platform: SandboxPlatform
31
+ ) -> None:
32
+ binary_path = _claude_code_cached_binary(version, platform)
33
+
34
+ with open(binary_path, "wb") as f:
35
+ f.write(binary_data)
36
+
37
+ _cleanup_claude_code_binary_cache(keep_count=3)
38
+
39
+
40
+ def _cleanup_claude_code_binary_cache(keep_count: int = 5) -> None:
41
+ # get all cached binaries
42
+ cache_files = list(_claude_code_cached_binary_dir().glob("claude-*"))
43
+ if len(cache_files) <= keep_count:
44
+ return
45
+
46
+ # remove oldest
47
+ cache_files.sort(key=lambda f: f.stat().st_atime)
48
+ files_to_remove = cache_files[:-keep_count]
49
+ for file_path in files_to_remove:
50
+ file_path.unlink()
51
+
52
+
53
+ def _claude_code_cached_binary_dir() -> Path:
54
+ return package_cache_dir("claude-code-downloads")
55
+
56
+
57
+ def _claude_code_cached_binary(version: str, platform: SandboxPlatform) -> Path:
58
+ return _claude_code_cached_binary_dir() / f"claude-{version}-{platform}"
@@ -0,0 +1,111 @@
1
+ import re
2
+ from typing import Literal
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from ..._util._async import run_coroutine
7
+ from ..._util.checksum import verify_checksum
8
+ from ..._util.download import download_file, download_text_file
9
+ from ..._util.sandbox import SandboxPlatform
10
+ from ..._util.trace import trace
11
+ from .cache import (
12
+ read_cached_claude_code_binary,
13
+ write_cached_claude_code_binary,
14
+ )
15
+
16
+
17
+ def download_claude_code(
18
+ version: Literal["stable", "latest"] | str, platform: SandboxPlatform
19
+ ) -> None:
20
+ """Download Claude Code.
21
+
22
+ Download a version of Claude Code. This version will be added to the cache of downloaded versions (which retains the 5 most recently downloaded versions).
23
+
24
+ Use this if you need to ensure that a specific version of Claude Code is downloaded in advance (e.g. if you are going to run your evaluations offline). After downloading, explicit requests for the downloaded version (e.g. `claude_code(version="1.0.98")`) will not require network access.
25
+
26
+ Args:
27
+ version: Version to download ("stable", "latest", or an explicit version number).
28
+ platform: Target platform ("linux-x64", "linux-arm64", "linux-x64-musl", or "linux-arm64-musl")
29
+ """
30
+ run_coroutine(download_claude_code_async(version, platform))
31
+
32
+
33
+ async def download_claude_code_async(
34
+ version: Literal["stable", "latest"] | str, platform: SandboxPlatform
35
+ ) -> bytes:
36
+ # determine version and checksum
37
+ gcs_bucket = await _claude_code_gcs_bucket()
38
+ version = await _claude_code_version(gcs_bucket, version)
39
+ manifest = await _claude_code_manifest(gcs_bucket, version)
40
+ expected_checksum = _checksum_for_platform(manifest, platform)
41
+
42
+ # check the cache
43
+ binary_data = read_cached_claude_code_binary(version, platform, expected_checksum)
44
+ if binary_data is None:
45
+ # not in cache, download and verify checksum
46
+ binary_url = f"{gcs_bucket}/{version}/{platform}/claude"
47
+ binary_data = await download_file(binary_url)
48
+ if not verify_checksum(binary_data, expected_checksum):
49
+ raise ValueError("Checksum verification failed")
50
+
51
+ # save to cache
52
+ write_cached_claude_code_binary(binary_data, version, platform)
53
+
54
+ # trace
55
+ trace(f"Downloaded claude code binary: {version} ({platform})")
56
+ else:
57
+ trace(f"Used claude code binary from cache: {version} ({platform})")
58
+
59
+ # return data
60
+ return binary_data
61
+
62
+
63
+ async def _claude_code_gcs_bucket() -> str:
64
+ INSTALL_SCRIPT_URL = "https://claude.ai/install.sh"
65
+ script_content = await download_text_file(INSTALL_SCRIPT_URL)
66
+ pattern = r'GCS_BUCKET="(https://storage\.googleapis\.com/[^"]+)"'
67
+ match = re.search(pattern, script_content)
68
+ if match is not None:
69
+ gcs_bucket = match.group(1)
70
+ return gcs_bucket
71
+ else:
72
+ raise RuntimeError("Unable to determine GCS bucket for claude code.")
73
+
74
+
75
+ async def _claude_code_version(gcs_bucket: str, target: str) -> str:
76
+ # validate target
77
+ target_pattern = r"^(stable|latest|[0-9]+\.[0-9]+\.[0-9]+(-[^[:space:]]+)?)$"
78
+ if re.match(target_pattern, target) is None:
79
+ raise RuntimeError(
80
+ "Invalid version target (must be 'stable', 'latest', or a semver version number)"
81
+ )
82
+
83
+ # resolve target alias if required
84
+ if target in ["stable", "latest"]:
85
+ version_url = f"{gcs_bucket}/{target}"
86
+ version = await download_text_file(version_url)
87
+ return version
88
+ else:
89
+ return target
90
+
91
+
92
+ class PlatformInfo(BaseModel):
93
+ checksum: str
94
+ size: int
95
+
96
+
97
+ class Manifest(BaseModel):
98
+ version: str
99
+ platforms: dict[str, PlatformInfo]
100
+
101
+
102
+ async def _claude_code_manifest(gcs_bucket: str, version: str) -> Manifest:
103
+ manifest_url = f"{gcs_bucket}/{version}/manifest.json"
104
+ manifest_json = await download_text_file(manifest_url)
105
+ return Manifest.model_validate_json(manifest_json)
106
+
107
+
108
+ def _checksum_for_platform(manifest: Manifest, platform: SandboxPlatform) -> str:
109
+ if platform not in manifest.platforms:
110
+ raise RuntimeError(f"Platform '{platform}' not found in manifest.")
111
+ return manifest.platforms[platform].checksum
@@ -0,0 +1,58 @@
1
+ from typing import Literal
2
+
3
+ from inspect_ai.util import SandboxEnvironment, concurrency
4
+ from inspect_ai.util import sandbox as sandbox_env
5
+
6
+ from inspect_swe._claude_code.install.cache import read_cached_claude_code_binary
7
+ from inspect_swe._util.trace import trace
8
+
9
+ from ..._util.sandbox import bash_command, detect_sandbox_platform, sandbox_exec
10
+ from .download import download_claude_code_async
11
+
12
+
13
+ async def ensure_claude_code_installed(
14
+ version: Literal["auto", "sandbox", "stable", "latest"] | str = "auto",
15
+ user: str | None = None,
16
+ sandbox: SandboxEnvironment | None = None,
17
+ ) -> str:
18
+ # resolve sandbox
19
+ sandbox = sandbox or sandbox_env()
20
+
21
+ # look in the sandbox first if we need to
22
+ if version == "auto" or version == "sandbox":
23
+ result = await sandbox.exec(bash_command("which claude"), user=user)
24
+ if result.success:
25
+ claude_binary = result.stdout.strip()
26
+ trace(f"Using claude code installed in sandbox: {claude_binary}")
27
+ return claude_binary
28
+
29
+ # if version == "sandbox" and we don't find it that's an error
30
+ if version == "sandbox":
31
+ raise RuntimeError("unable to locate claude code in sandbox")
32
+
33
+ # otherwise set to "stable"
34
+ version = "stable"
35
+
36
+ # detect the sandbox target platform
37
+ platform = await detect_sandbox_platform(sandbox)
38
+
39
+ # use concurrency so multiple samples don't attempt the same download all at once
40
+ async with concurrency("claude-install", 1, visible=False):
41
+ # if a specific version is requested, first try to read it directly from the cache
42
+ if version not in ["stable", "latest"]:
43
+ claude_binary_bytes: bytes | None = read_cached_claude_code_binary(
44
+ version, platform, None
45
+ )
46
+ if claude_binary_bytes is not None:
47
+ trace(f"Used claude code binary from cache: {version} ({platform})")
48
+
49
+ # download the binary
50
+ if claude_binary_bytes is None:
51
+ claude_binary_bytes = await download_claude_code_async(version, platform)
52
+
53
+ # write it into the container and return it
54
+ claude_binary = f"/opt/claude-{version}-{platform}"
55
+ await sandbox.write_file(claude_binary, claude_binary_bytes)
56
+ await sandbox_exec(sandbox, f"chmod +x {claude_binary}")
57
+ await sandbox_exec(sandbox, f"{claude_binary} config list", user=user)
58
+ return claude_binary
File without changes
@@ -0,0 +1,54 @@
1
+ import asyncio
2
+ from typing import Coroutine, Literal, TypeVar, cast
3
+
4
+ import nest_asyncio # type: ignore
5
+ import sniffio
6
+
7
+ from .platform import running_in_notebook
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ def run_coroutine(coroutine: Coroutine[None, None, T]) -> T:
13
+ if current_async_backend() == "trio":
14
+ raise RuntimeError("run_coroutine cannot be used with trio")
15
+
16
+ if running_in_notebook():
17
+ init_nest_asyncio()
18
+ return asyncio.run(coroutine)
19
+ else:
20
+ try:
21
+ # this will throw if there is no running loop
22
+ asyncio.get_running_loop()
23
+
24
+ # initialiase nest_asyncio then we are clear to run
25
+ init_nest_asyncio()
26
+ return asyncio.run(coroutine)
27
+
28
+ except RuntimeError:
29
+ # No running event loop so we are clear to run
30
+ return asyncio.run(coroutine)
31
+
32
+
33
+ _initialised_nest_asyncio: bool = False
34
+
35
+
36
+ def init_nest_asyncio() -> None:
37
+ global _initialised_nest_asyncio
38
+ if not _initialised_nest_asyncio:
39
+ nest_asyncio.apply()
40
+ _initialised_nest_asyncio = True
41
+
42
+
43
+ def current_async_backend() -> Literal["asyncio", "trio"] | None:
44
+ try:
45
+ return _validate_backend(sniffio.current_async_library().lower())
46
+ except sniffio.AsyncLibraryNotFoundError:
47
+ return None
48
+
49
+
50
+ def _validate_backend(backend: str) -> Literal["asyncio", "trio"]:
51
+ if backend in ["asyncio", "trio"]:
52
+ return cast(Literal["asyncio", "trio"], backend)
53
+ else:
54
+ raise RuntimeError(f"Unknown async backend: {backend}")
@@ -0,0 +1,20 @@
1
+ from pathlib import Path
2
+
3
+ from inspect_ai._util.constants import PKG_NAME
4
+ from platformdirs import user_cache_path, user_data_path
5
+
6
+
7
+ def package_data_dir(subdir: str | None) -> Path:
8
+ data_dir = user_data_path(PKG_NAME)
9
+ if subdir:
10
+ data_dir = data_dir / subdir
11
+ data_dir.mkdir(parents=True, exist_ok=True)
12
+ return data_dir
13
+
14
+
15
+ def package_cache_dir(subdir: str | None) -> Path:
16
+ cache_dir = user_cache_path(PKG_NAME)
17
+ if subdir:
18
+ cache_dir = cache_dir / subdir
19
+ cache_dir.mkdir(parents=True, exist_ok=True)
20
+ return cache_dir
@@ -0,0 +1,6 @@
1
+ import hashlib
2
+
3
+
4
+ def verify_checksum(data: bytes, expected_checksum: str) -> bool:
5
+ actual_checksum = hashlib.sha256(data).hexdigest()
6
+ return actual_checksum == expected_checksum
@@ -0,0 +1 @@
1
+ PKG_NAME = "inspect_swe"
@@ -0,0 +1,12 @@
1
+ import httpx
2
+
3
+
4
+ async def download_file(url: str) -> bytes:
5
+ async with httpx.AsyncClient() as client:
6
+ response = await client.get(url, follow_redirects=True)
7
+ response.raise_for_status()
8
+ return response.content
9
+
10
+
11
+ async def download_text_file(url: str) -> str:
12
+ return (await download_file(url)).decode("utf-8")
@@ -0,0 +1,15 @@
1
+ from typing import no_type_check
2
+
3
+
4
+ @no_type_check
5
+ def running_in_notebook() -> bool:
6
+ try:
7
+ from IPython import get_ipython # type: ignore
8
+
9
+ if "IPKernelApp" not in get_ipython().config:
10
+ return False
11
+ except ImportError:
12
+ return False
13
+ except AttributeError:
14
+ return False
15
+ return True
@@ -0,0 +1,59 @@
1
+ from typing import Literal, TypeAlias, cast
2
+
3
+ from inspect_ai.util import SandboxEnvironment
4
+
5
+ SandboxPlatform: TypeAlias = Literal[
6
+ "linux-x64", "linux-arm64", "linux-x64-musl", "linux-arm64-musl"
7
+ ]
8
+
9
+
10
+ async def detect_sandbox_platform(sandbox: SandboxEnvironment) -> SandboxPlatform:
11
+ # Get OS
12
+ os_name = await sandbox_exec(sandbox, "uname -s")
13
+ if os_name == "Linux":
14
+ os_type = "linux"
15
+ else:
16
+ raise ValueError(f"Unsupported OS: {os_name}")
17
+
18
+ # Get architecture
19
+ arch = await sandbox_exec(sandbox, "uname -m")
20
+ if arch in ["x86_64", "amd64"]:
21
+ arch_type = "x64"
22
+ elif arch in ["arm64", "aarch64"]:
23
+ arch_type = "arm64"
24
+ else:
25
+ raise ValueError(f"Unsupported architecture: {arch}")
26
+
27
+ # Check for musl on Linux
28
+ if os_type == "linux":
29
+ # Check for musl libc
30
+ musl_check_cmd = (
31
+ "if [ -f /lib/libc.musl-x86_64.so.1 ] || "
32
+ "[ -f /lib/libc.musl-aarch64.so.1 ] || "
33
+ "ldd /bin/ls 2>&1 | grep -q musl; then "
34
+ "echo 'musl'; else echo 'glibc'; fi"
35
+ )
36
+ libc_type = await sandbox_exec(sandbox, musl_check_cmd)
37
+ if libc_type == "musl":
38
+ platform = f"linux-{arch_type}-musl"
39
+ else:
40
+ platform = f"linux-{arch_type}"
41
+ else:
42
+ platform = f"{os_type}-{arch_type}"
43
+
44
+ return cast(SandboxPlatform, platform)
45
+
46
+
47
+ def bash_command(cmd: str) -> list[str]:
48
+ return ["bash", "--login", "-c", cmd]
49
+
50
+
51
+ async def sandbox_exec(
52
+ sandbox: SandboxEnvironment, cmd: str, user: str | None = None
53
+ ) -> str:
54
+ result = await sandbox.exec(bash_command(cmd), user=user)
55
+ if not result.success:
56
+ raise RuntimeError(
57
+ f"Error executing sandbox command {','.join(cmd)}: {result.stderr}"
58
+ )
59
+ return result.stdout.strip()
@@ -0,0 +1,7 @@
1
+ from logging import getLogger
2
+
3
+ logger = getLogger(__file__)
4
+
5
+
6
+ def trace(message: str) -> None:
7
+ logger.info(f"[Inspect SWE] {message}")
inspect_swe/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.2.1'
32
- __version_tuple__ = version_tuple = (0, 2, 1)
31
+ __version__ = version = '0.2.3'
32
+ __version_tuple__ = version_tuple = (0, 2, 3)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inspect_swe
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Software engineering agents for Inspect AI.
5
5
  Project-URL: Documentation, https://meridianlabs-ai.github.io/inspect_swe/
6
6
  Project-URL: Source Code, https://github.com/meridianlabs-ai/inspect_swe
@@ -9,7 +9,12 @@ Author: Meridian Labs
9
9
  License: MIT License
10
10
  License-File: LICENSE
11
11
  Requires-Python: >=3.10
12
+ Requires-Dist: httpx
12
13
  Requires-Dist: inspect-ai>=0.3.125
14
+ Requires-Dist: nest-asyncio
15
+ Requires-Dist: platformdirs
16
+ Requires-Dist: pydantic>=2.11.4
17
+ Requires-Dist: sniffio
13
18
  Requires-Dist: typing-extensions>=4.9.0
14
19
  Provides-Extra: dev
15
20
  Requires-Dist: anthropic; extra == 'dev'
@@ -0,0 +1,24 @@
1
+ inspect_swe/__init__.py,sha256=Jg2VYr_eK8_fOXA4Oj0UAQj-g-RxDJuXrIhxKhassko,335
2
+ inspect_swe/_registry.py,sha256=jM37ysrY39Ufd67GRKbiwfSViOLlm-82lm_JEaWKshw,97
3
+ inspect_swe/_version.py,sha256=kBRz0P2plw1eVdIpt70W6m1LMbEIhLY3RyOfVGdubaI,704
4
+ inspect_swe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ inspect_swe/_claude_code/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ inspect_swe/_claude_code/claude_code.py,sha256=YfxNLgohMMhAohLdclgGyLsfcjocwgmMyOxl2-HlepA,3297
7
+ inspect_swe/_claude_code/install/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ inspect_swe/_claude_code/install/cache.py,sha256=k08bCxGq-iYVpO16LNQhPjxTM9p2iecpqMjqYd2WBss,1708
9
+ inspect_swe/_claude_code/install/download.py,sha256=QKlFuDqCV55coTumIjyTXt2MU-vUQg8qPL3z3LHIUq8,4132
10
+ inspect_swe/_claude_code/install/install.py,sha256=cJP2JOUZNfPphz0eWbzrY7ULjSUU_SbSlPy3QecBltw,2430
11
+ inspect_swe/_util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ inspect_swe/_util/_async.py,sha256=cL8_Smmj2Es41TefceGDYLyVaO7gZ56VJcA4oByrWfQ,1520
13
+ inspect_swe/_util/appdirs.py,sha256=V3o1ERdSYLjKP-m4O1T_Hvkx0UsP2HdfvsshLSQgP6E,562
14
+ inspect_swe/_util/checksum.py,sha256=i-_GhtgCFd5eFj3PPJiGSCHDhZdPcIPNwiqddX93Sls,186
15
+ inspect_swe/_util/constants.py,sha256=xKvGgaJ0MwNbdzaken5HMbxYyKBEw_3VrBwCgkvAIWo,25
16
+ inspect_swe/_util/download.py,sha256=cCUau4ZBOKezpotJV5-v3JY_5CuYDZ-VcWlLf_EyNL0,340
17
+ inspect_swe/_util/platform.py,sha256=wm4efIFfdyTeaV2oxOXVvYl1u22MHX3jQMERHJMgv7A,339
18
+ inspect_swe/_util/sandbox.py,sha256=RixiEY1asFHa8HTsAHAxYXcPL-mUMgprQke1-TRbWYE,1812
19
+ inspect_swe/_util/trace.py,sha256=mFHmBKn2F8iJP9PpTHaCseMHnTMz3ErRx6RCKV83rZk,139
20
+ inspect_swe-0.2.3.dist-info/METADATA,sha256=yod5MyJGNjnpnlPCPczXyXMfx5BXhBrHJDoIkcTGpDI,1658
21
+ inspect_swe-0.2.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
+ inspect_swe-0.2.3.dist-info/entry_points.txt,sha256=OzpvUhd7M3T2Rog4MjwJAxIKeX5ljiR0mVYM9GefBKg,49
23
+ inspect_swe-0.2.3.dist-info/licenses/LICENSE,sha256=Hi3UDcbD6yCKZ1mcgt7pprzSG0rDEnSrbrm3XinyiDA,1070
24
+ inspect_swe-0.2.3.dist-info/RECORD,,
@@ -1,341 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- import hashlib
4
- import json
5
- import os
6
- import re
7
- import subprocess
8
- import tempfile
9
- import urllib.request
10
- from pathlib import Path
11
- from typing import Optional, cast
12
-
13
- # Constants
14
- INSTALL_SCRIPT_URL = "https://claude.ai/install.sh"
15
- CACHE_DIR = Path.home() / ".claude" / "downloads"
16
- # Fallback GCS bucket in case we can't fetch from install.sh
17
- FALLBACK_GCS_BUCKET = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases"
18
-
19
-
20
- def run_docker_exec(container_name: str, command: str) -> str:
21
- """Execute a command in the Docker container and return output."""
22
- cmd = ["docker", "exec", container_name, "bash", "-c", command]
23
- result = subprocess.run(cmd, capture_output=True, text=True, check=True)
24
- return result.stdout.strip()
25
-
26
-
27
- def detect_platform(container_name: str) -> str:
28
- """Detect the platform (OS and architecture) of the container."""
29
- # Get OS
30
- os_name = run_docker_exec(container_name, "uname -s")
31
- if os_name == "Darwin":
32
- os_type = "darwin"
33
- elif os_name == "Linux":
34
- os_type = "linux"
35
- else:
36
- raise ValueError(f"Unsupported OS: {os_name}")
37
-
38
- # Get architecture
39
- arch = run_docker_exec(container_name, "uname -m")
40
- if arch in ["x86_64", "amd64"]:
41
- arch_type = "x64"
42
- elif arch in ["arm64", "aarch64"]:
43
- arch_type = "arm64"
44
- else:
45
- raise ValueError(f"Unsupported architecture: {arch}")
46
-
47
- # Check for musl on Linux
48
- if os_type == "linux":
49
- # Check for musl libc
50
- musl_check_cmd = (
51
- "if [ -f /lib/libc.musl-x86_64.so.1 ] || "
52
- "[ -f /lib/libc.musl-aarch64.so.1 ] || "
53
- "ldd /bin/ls 2>&1 | grep -q musl; then "
54
- "echo 'musl'; else echo 'glibc'; fi"
55
- )
56
- libc_type = run_docker_exec(container_name, musl_check_cmd)
57
- if libc_type == "musl":
58
- platform = f"linux-{arch_type}-musl"
59
- else:
60
- platform = f"linux-{arch_type}"
61
- else:
62
- platform = f"{os_type}-{arch_type}"
63
-
64
- return platform
65
-
66
-
67
- def download_file(url: str) -> bytes:
68
- """Download a file from the given URL and return its contents."""
69
- with urllib.request.urlopen(url) as response:
70
- return cast(bytes, response.read())
71
-
72
-
73
- def get_gcs_bucket_from_install_script() -> str:
74
- """Fetch the install.sh script and extract the GCS_BUCKET URL.
75
-
76
- Falls back to hardcoded URL if extraction fails.
77
- """
78
- try:
79
- print("Fetching install script to discover GCS bucket...")
80
- script_content = download_file(INSTALL_SCRIPT_URL).decode("utf-8")
81
-
82
- # Look for GCS_BUCKET= line in the script
83
- # Pattern matches: GCS_BUCKET="https://storage.googleapis.com/..."
84
- pattern = r'GCS_BUCKET="(https://storage\.googleapis\.com/[^"]+)"'
85
- match = re.search(pattern, script_content)
86
-
87
- if match:
88
- gcs_bucket = match.group(1)
89
- print(f"Discovered GCS bucket: {gcs_bucket}")
90
- return gcs_bucket
91
- else:
92
- print("Could not extract GCS bucket from install script, using fallback")
93
- return FALLBACK_GCS_BUCKET
94
-
95
- except Exception as e:
96
- print(f"Error fetching install script: {e}, using fallback")
97
- return FALLBACK_GCS_BUCKET
98
-
99
-
100
- def validate_target(target: str) -> bool:
101
- """Validate the target parameter format."""
102
- pattern = r"^(stable|latest|[0-9]+\.[0-9]+\.[0-9]+(-[^[:space:]]+)?)$"
103
- return bool(re.match(pattern, target))
104
-
105
-
106
- def get_version(gcs_bucket: str, target: str = "stable") -> str:
107
- """Get the actual version to install based on the target."""
108
- if not validate_target(target):
109
- raise ValueError(f"Invalid target: {target}")
110
-
111
- # Always download stable version first (it has the most up-to-date installer)
112
- stable_url = f"{gcs_bucket}/stable"
113
- stable_version = download_file(stable_url).decode("utf-8").strip()
114
-
115
- if target == "stable" or target == stable_version:
116
- return stable_version
117
- elif target == "latest":
118
- # For latest, we'd need to check the latest version
119
- # For now, we'll use stable as the implementation
120
- return stable_version
121
- else:
122
- # Specific version requested
123
- return target
124
-
125
-
126
- def get_checksum_from_manifest(manifest_json: str, platform: str) -> str:
127
- """Extract the checksum for the given platform from the manifest."""
128
- manifest = json.loads(manifest_json)
129
-
130
- if "platforms" not in manifest:
131
- raise ValueError("Invalid manifest: missing platforms")
132
-
133
- if platform not in manifest["platforms"]:
134
- raise ValueError(f"Platform {platform} not found in manifest")
135
-
136
- checksum = manifest["platforms"][platform].get("checksum")
137
-
138
- if not checksum or not re.match(r"^[a-f0-9]{64}$", checksum):
139
- raise ValueError(f"Invalid checksum for platform {platform}")
140
-
141
- return str(checksum)
142
-
143
-
144
- def verify_checksum(data: bytes, expected_checksum: str) -> bool:
145
- """Verify the SHA256 checksum of the data."""
146
- actual_checksum = hashlib.sha256(data).hexdigest()
147
- return actual_checksum == expected_checksum
148
-
149
-
150
- def get_cached_binary_path(version: str, platform: str) -> Path:
151
- """Get the path where a binary would be cached."""
152
- return CACHE_DIR / f"claude-{version}-{platform}"
153
-
154
-
155
- def get_cached_binary(
156
- version: str, platform: str, expected_checksum: str
157
- ) -> Optional[bytes]:
158
- """
159
- Check if we have a cached binary and verify its checksum.
160
-
161
- Returns the binary data if valid, None otherwise.
162
- """
163
- cache_path = get_cached_binary_path(version, platform)
164
-
165
- if not cache_path.exists():
166
- return None
167
-
168
- try:
169
- with open(cache_path, "rb") as f:
170
- binary_data = f.read()
171
-
172
- # Verify the cached binary still has the correct checksum
173
- if verify_checksum(binary_data, expected_checksum):
174
- # Update access time so this file is considered "recently used"
175
- cache_path.touch()
176
- print(f"Using cached binary from {cache_path}")
177
- return binary_data
178
- else:
179
- print("Cached binary checksum mismatch, will re-download")
180
- cache_path.unlink() # Remove invalid cache file
181
- return None
182
- except Exception as e:
183
- print(f"Error reading cached binary: {e}")
184
- return None
185
-
186
-
187
- def cleanup_old_cache_files(keep_count: int = 3) -> None:
188
- """
189
- Remove old cached binaries, keeping only the most recent ones.
190
-
191
- Keeps the specified number of most recently accessed files.
192
- """
193
- if not CACHE_DIR.exists():
194
- return
195
-
196
- # Get all claude binary files in cache
197
- cache_files = list(CACHE_DIR.glob("claude-*"))
198
-
199
- if len(cache_files) <= keep_count:
200
- return # Nothing to clean up
201
-
202
- # Sort by access time (most recently accessed last)
203
- cache_files.sort(key=lambda f: f.stat().st_atime)
204
-
205
- # Remove oldest files
206
- files_to_remove = cache_files[:-keep_count]
207
- for file_path in files_to_remove:
208
- try:
209
- file_size_mb = file_path.stat().st_size / (1024 * 1024)
210
- file_path.unlink()
211
- print(f"Removed old cache file: {file_path.name} ({file_size_mb:.1f} MB)")
212
- except Exception as e:
213
- print(f"Error removing cache file {file_path}: {e}")
214
-
215
-
216
- def save_to_cache(binary_data: bytes, version: str, platform: str) -> None:
217
- """Save a binary to the cache directory and clean up old files."""
218
- CACHE_DIR.mkdir(parents=True, exist_ok=True)
219
- cache_path = get_cached_binary_path(version, platform)
220
-
221
- with open(cache_path, "wb") as f:
222
- f.write(binary_data)
223
-
224
- print(f"Saved binary to cache: {cache_path}")
225
-
226
- # Clean up old cache files, keeping only the 3 most recent
227
- cleanup_old_cache_files(keep_count=3)
228
-
229
-
230
- def transfer_binary(container_name: str, binary_data: bytes, target_path: str) -> None:
231
- """Transfer binary data to the container."""
232
- # Use a temporary file and docker cp
233
- with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
234
- tmp_file.write(binary_data)
235
- tmp_file_path = tmp_file.name
236
-
237
- try:
238
- # Copy file to container
239
- subprocess.run(
240
- ["docker", "cp", tmp_file_path, f"{container_name}:{target_path}"],
241
- check=True,
242
- )
243
- finally:
244
- # Clean up temporary file
245
- os.unlink(tmp_file_path)
246
-
247
-
248
- def install_claude(container_name: str, binary_path: str) -> None:
249
- """Install claude binary and verify it works."""
250
- # Copy binary to /usr/local/bin for system-wide access
251
- run_docker_exec(container_name, f"cp {binary_path} /usr/local/bin/claude")
252
- run_docker_exec(container_name, "chmod +x /usr/local/bin/claude")
253
-
254
- # Clean up the temporary binary
255
- run_docker_exec(container_name, f"rm -f {binary_path}")
256
-
257
- # Verify installation and initialize config
258
- try:
259
- # Check version
260
- version_output = run_docker_exec(container_name, "claude --version")
261
- print(f"Claude installed successfully: {version_output}")
262
-
263
- # Initialize config files/directories by running config list
264
- run_docker_exec(container_name, "claude config list")
265
- print("Claude configuration initialized")
266
-
267
- except subprocess.CalledProcessError as e:
268
- print(f"Warning: Could not verify claude installation: {e}")
269
- raise ValueError("Claude installation verification failed") from e
270
-
271
-
272
- def main(container_name: str, target: str = "stable") -> None:
273
- """Main function to orchestrate the Claude installation."""
274
- print(f"Installing Claude Code in container: {container_name}")
275
- print(f"Target: {target}")
276
-
277
- # Step 0: Get GCS bucket URL
278
- gcs_bucket = get_gcs_bucket_from_install_script()
279
-
280
- # Step 1: Detect platform
281
- print("Detecting platform...")
282
- platform = detect_platform(container_name)
283
- print(f"Platform: {platform}")
284
-
285
- # Step 2: Get version
286
- print("Determining version...")
287
- version = get_version(gcs_bucket, target)
288
- print(f"Version: {version}")
289
-
290
- # Step 3: Download and parse manifest
291
- print("Downloading manifest...")
292
- manifest_url = f"{gcs_bucket}/{version}/manifest.json"
293
- manifest_json = download_file(manifest_url).decode("utf-8")
294
-
295
- # Step 4: Get checksum for platform
296
- print("Extracting checksum...")
297
- expected_checksum = get_checksum_from_manifest(manifest_json, platform)
298
-
299
- # Step 5: Check cache or download binary
300
- binary_data = get_cached_binary(version, platform, expected_checksum)
301
-
302
- if binary_data is None:
303
- # Not in cache or invalid, need to download
304
- print(f"Downloading Claude binary for {platform}...")
305
- binary_url = f"{gcs_bucket}/{version}/{platform}/claude"
306
- binary_data = download_file(binary_url)
307
-
308
- # Step 6: Verify checksum
309
- print("Verifying checksum...")
310
- if not verify_checksum(binary_data, expected_checksum):
311
- raise ValueError("Checksum verification failed")
312
- print("Checksum verified successfully")
313
-
314
- # Save to cache for future use
315
- save_to_cache(binary_data, version, platform)
316
- else:
317
- print("Checksum already verified for cached binary")
318
-
319
- # Step 7: Transfer binary to container
320
- print("Transferring binary to container...")
321
- binary_path = f"/tmp/claude-{version}-{platform}"
322
- transfer_binary(container_name, binary_data, binary_path)
323
-
324
- # Step 8: Install
325
- print("Installing Claude Code...")
326
- install_claude(container_name, binary_path)
327
-
328
- print("\n✅ Installation complete!")
329
-
330
-
331
- if __name__ == "__main__":
332
- # Test code - replace with your actual container name
333
- test_container = "inspect-intervention-izedw74-default-1"
334
-
335
- # You can test with different targets
336
- # main(test_container, "stable")
337
- # main(test_container, "latest")
338
- # main(test_container, "1.0.0")
339
-
340
- # Default test
341
- main(test_container, "stable")
@@ -1,12 +0,0 @@
1
- inspect_swe/__init__.py,sha256=6F52dddUoPvJA7RugtyDaswUqliDGgeaTK_OXWplvI0,185
2
- inspect_swe/_registry.py,sha256=jM37ysrY39Ufd67GRKbiwfSViOLlm-82lm_JEaWKshw,97
3
- inspect_swe/_version.py,sha256=vYqoJTG51NOUmYyL0xt8asRK8vUT4lGAdal_EZ59mvw,704
4
- inspect_swe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- inspect_swe/_claude_code/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- inspect_swe/_claude_code/claude_code.py,sha256=iE_-6Wv0m7hO1Tj-d21K8iZHgBIcTcfSqLHPVS1whMM,1788
7
- inspect_swe/_claude_code/install_claude.py,sha256=g5nHIY-JVKDQFgm0IIhpCsCX5B6MadYj8-CtKpKU4YE,11796
8
- inspect_swe-0.2.1.dist-info/METADATA,sha256=56Fjle-9IWwx-k0AHCbxPPH7443KE1JqdSX4_s_dFCc,1526
9
- inspect_swe-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- inspect_swe-0.2.1.dist-info/entry_points.txt,sha256=OzpvUhd7M3T2Rog4MjwJAxIKeX5ljiR0mVYM9GefBKg,49
11
- inspect_swe-0.2.1.dist-info/licenses/LICENSE,sha256=Hi3UDcbD6yCKZ1mcgt7pprzSG0rDEnSrbrm3XinyiDA,1070
12
- inspect_swe-0.2.1.dist-info/RECORD,,