Skip to content

Commit 8031ea3

Browse files
authored
Merge pull request #61 from agentic-labs/caching
Cache the checkout
2 parents cfc83ed + 5b978e0 commit 8031ea3

File tree

2 files changed

+130
-91
lines changed

2 files changed

+130
-91
lines changed

lsproxy/client.py

Lines changed: 5 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import json
22
import httpx
33
import time
4-
from typing import List, TYPE_CHECKING, Optional
4+
from typing import List, Optional
55

6-
# Only import type hints for Modal if type checking
7-
if TYPE_CHECKING:
8-
import modal
96

107
from .models import (
118
DefinitionResponse,
@@ -19,9 +16,6 @@
1916
IdentifierResponse,
2017
)
2118

22-
from .auth import create_jwt
23-
24-
2519
class Lsproxy:
2620
"""Client for interacting with the lsproxy API."""
2721

@@ -151,100 +145,20 @@ def initialize_with_modal(
151145
ImportError: If Modal or PyJWT are not installed
152146
ValueError: If repository cloning fails
153147
"""
148+
154149
try:
155-
import modal
156-
import secrets
150+
from .modal import ModalSandbox
157151
except ImportError:
158152
raise ImportError(
159153
"Modal and PyJWT are required for this feature. "
160154
"Install them with: pip install 'lsproxy-sdk[modal]'"
161155
)
162156

163-
app = modal.App.lookup("lsproxy-app", create_if_missing=True)
164-
165-
# Generate a secure random secret
166-
jwt_secret = secrets.token_urlsafe(32)
167-
168-
# Create JWT token with 24-hour expiration
169-
payload = {
170-
"sub": "lsproxy-client",
171-
"iat": int(time.time()),
172-
"exp": int(time.time()) + 86400, # 24 hour expiration
173-
}
174-
token = create_jwt(payload, jwt_secret)
175-
176-
lsproxy_image = modal.Image.from_registry(f"agenticlabs/lsproxy:{version}").env(
177-
{"JWT_SECRET": jwt_secret}
178-
)
179-
180-
sandbox_config = {
181-
"image": lsproxy_image,
182-
"app": app,
183-
"encrypted_ports": [4444],
184-
}
185-
186-
if timeout is not None:
187-
sandbox_config["timeout"] = timeout
188-
189-
print("Starting sandbox...")
190-
sandbox = modal.Sandbox.create(**sandbox_config)
191-
192-
tunnel_url = sandbox.tunnels()[4444].url
193-
194-
# Clone repository with token if provided
195-
print(f"Cloning {repo_url}...")
196-
if git_token:
197-
# Insert token into URL for private repo access
198-
url_parts = repo_url.split("://")
199-
if len(url_parts) != 2:
200-
raise ValueError("Invalid repository URL format")
201-
auth_url = f"{url_parts[0]}://x-access-token:{git_token}@{url_parts[1]}"
202-
clone_url = auth_url
203-
else:
204-
clone_url = repo_url
205-
206-
try:
207-
p = sandbox.exec(
208-
"git", "config", "--global", "--add", "safe.directory", "/mnt/workspace"
209-
)
210-
p.wait()
211-
212-
p = sandbox.exec(
213-
"git", "clone", clone_url, "--depth", "1", "/mnt/workspace"
214-
)
215-
exit_code = p.wait()
216-
if exit_code != 0:
217-
raise ValueError(
218-
"Failed to clone repository. Please check:\n"
219-
"- The repository URL is correct\n"
220-
"- You have access to the repository\n"
221-
"- If it's a private repository, you've provided a valid git token"
222-
)
223-
if sha is not None:
224-
# Checkout the specific commit
225-
p = sandbox.exec(
226-
"bash", "-c", f"cd /mnt/workspace && git fetch origin {sha}"
227-
)
228-
exit_code = p.wait()
229-
if exit_code != 0:
230-
raise ValueError(
231-
f"Failed to fetch SHA ({sha}). Please check it is valid"
232-
)
233-
234-
p = sandbox.exec(
235-
"bash", "-c", f"cd /mnt/workspace && git checkout {sha}"
236-
)
237-
p.wait()
238-
239-
except Exception as e:
240-
sandbox.terminate()
241-
raise ValueError(f"Repository cloning failed: {str(e)}")
242157

243-
# Start lsproxy
244-
p = sandbox.exec("lsproxy")
158+
sandbox = ModalSandbox(repo_url, git_token, sha, timeout, version)
245159

246160
# Wait for server to be ready
247-
client = cls(base_url=f"{tunnel_url}/v1", auth_token=token)
161+
client = cls(base_url=f"{sandbox.tunnel_url}/v1", auth_token=sandbox.jwt_token)
248162

249163
print("Waiting for server start up (make take a minute)...")
250164
for attempt in range(180):

lsproxy/modal.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import time
2+
import json
3+
import hashlib
4+
import subprocess
5+
6+
from typing import TYPE_CHECKING
7+
from .auth import create_jwt
8+
9+
# Only import type hints for Modal if type checking
10+
if TYPE_CHECKING:
11+
import modal
12+
13+
def create_named_modal_secret(
14+
env: dict[str, str | None],
15+
secret_name: str,
16+
):
17+
"""
18+
modal secret create [OPTIONS] SECRET_NAME KEYVALUES...
19+
20+
SECRET_NAME: [required]
21+
KEYVALUES...: Space-separated KEY=VALUE items [required]
22+
23+
1) filter out None values
24+
2) stable JSON stringify
25+
3) take SHA256 hash of the string to get the SECRET_NAME
26+
4) assemble KEYVALUES using shlex.quote
27+
28+
"""
29+
try:
30+
import modal
31+
except ImportError:
32+
raise ImportError(
33+
"Modal and PyJWT are required for this feature. "
34+
"Install them with: pip install 'lsproxy-sdk[modal]'"
35+
)
36+
filtered_env = {k: v for k, v in env.items() if v is not None}
37+
38+
# subprocess.run already handles quoting
39+
keyvalues = [f"{k}={v}" for k, v in filtered_env.items()]
40+
print(f"Creating Modal secret {secret_name} with {len(keyvalues)} keyvalues")
41+
result = subprocess.run(
42+
["modal", "secret", "create", "--force", secret_name, *keyvalues],
43+
capture_output=True,
44+
text=True,
45+
)
46+
if result.returncode != 0:
47+
raise Exception(f"Error creating Modal secret {secret_name}: {result.stderr}")
48+
return modal.Secret.from_name(secret_name)
49+
50+
class ModalSandbox:
51+
def __init__(self, repo_url: str, git_token: str, sha: str, timeout: int, version: str):
52+
try:
53+
import modal
54+
import secrets
55+
except ImportError:
56+
raise ImportError(
57+
"Modal and PyJWT are required for this feature. "
58+
"Install them with: pip install 'lsproxy-sdk[modal]'"
59+
)
60+
61+
app = modal.App.lookup("lsproxy-app", create_if_missing=True)
62+
63+
# Generate a secure random secret
64+
jwt_secret = secrets.token_urlsafe(32)
65+
66+
# Create JWT token with 24-hour expiration
67+
payload = {
68+
"sub": "lsproxy-client",
69+
"iat": int(time.time()),
70+
"exp": int(time.time()) + 86400, # 24 hour expiration
71+
}
72+
self.jwt_token = create_jwt(payload, jwt_secret)
73+
74+
75+
if git_token:
76+
# We want the github secret to be named, because that allows us to cache the layers (for the same sha) even as the token changes!
77+
repo_id = hashlib.md5(repo_url.encode()).hexdigest()
78+
gh_secret = create_named_modal_secret(
79+
{"GITHUB_IAT": git_token},
80+
f"gh-iat-token-{repo_id}"
81+
)
82+
url_parts = repo_url.split("://")
83+
lsproxy_image = modal.Image.from_registry(f"agenticlabs/lsproxy:{version}")
84+
lsproxy_image = lsproxy_image.run_commands(
85+
[
86+
"git config --global --add safe.directory /mnt/workspace",
87+
f"git clone --depth 1 {url_parts[0]}://x-access-token:$GITHUB_IAT@{url_parts[1]} /mnt/workspace"
88+
],
89+
secrets=[
90+
gh_secret
91+
], # sneaky, cache the layers (for the same sha) even as the token changes!
92+
)
93+
if sha:
94+
lsproxy_image = lsproxy_image.run_commands([
95+
f"cd /mnt/workspace && git fetch origin {sha} && git checkout {sha}"
96+
]
97+
)
98+
else:
99+
lsproxy_image = modal.Image.from_registry(f"agenticlabs/lsproxy:{version}").run_commands(
100+
[
101+
f"git clone --depth 1 {repo_url} /mnt/workspace"
102+
]
103+
)
104+
105+
jwt_secret = modal.Secret.from_dict({"JWT_SECRET": jwt_secret})
106+
sandbox_config = {
107+
"image": lsproxy_image,
108+
"app": app,
109+
"encrypted_ports": [4444],
110+
"secrets": [jwt_secret],
111+
}
112+
113+
if timeout is not None:
114+
sandbox_config["timeout"] = timeout
115+
116+
print("Starting sandbox...")
117+
118+
self.sandbox = modal.Sandbox.create(**sandbox_config)
119+
self.tunnel_url = self.sandbox.tunnels()[4444].url
120+
121+
# Start lsproxy
122+
p = self.sandbox.exec(f"lsproxy")
123+
124+
def terminate(self):
125+
self.sandbox.terminate()

0 commit comments

Comments
 (0)