Skip to content

Commit eb1647b

Browse files
authored
feat: CG-10779: Global session management + decoupled auth (#445)
1 parent 091848b commit eb1647b

File tree

26 files changed

+337
-332
lines changed

26 files changed

+337
-332
lines changed

.codegen/config.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ github_token = ""
33
openai_api_key = ""
44

55
[repository]
6-
organization_name = "codegen-sh"
7-
repo_name = "codegen-sdk"
6+
repo_path = ""
7+
repo_name = ""
8+
full_name = ""
9+
user_name = ""
10+
user_email = ""
11+
language = ""
812

913
[feature_flags.codebase]
1014
debug = false

src/codegen/cli/api/client.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
LookupOutput,
3535
PRLookupInput,
3636
PRLookupResponse,
37-
PRSchema,
3837
RunCodemodInput,
3938
RunCodemodOutput,
4039
RunOnPRInput,
@@ -57,9 +56,9 @@ class RestAPI:
5756

5857
_session: ClassVar[requests.Session] = requests.Session()
5958

60-
auth_token: str | None = None
59+
auth_token: str
6160

62-
def __init__(self, auth_token: str | None = None):
61+
def __init__(self, auth_token: str):
6362
self.auth_token = auth_token
6463

6564
def _get_headers(self) -> dict[str, str]:
@@ -133,11 +132,10 @@ def run(
133132
template_context: Context variables to pass to the codemod
134133
135134
"""
136-
session = CodegenSession()
137-
135+
session = CodegenSession.from_active_session()
138136
base_input = {
139137
"codemod_name": function.name,
140-
"repo_full_name": session.repo_name,
138+
"repo_full_name": session.config.repository.full_name,
141139
"codemod_run_type": run_type,
142140
}
143141

@@ -158,13 +156,13 @@ def run(
158156
RunCodemodOutput,
159157
)
160158

161-
def get_docs(self) -> dict:
159+
def get_docs(self) -> DocsResponse:
162160
"""Search documentation."""
163-
session = CodegenSession()
161+
session = CodegenSession.from_active_session()
164162
return self._make_request(
165163
"GET",
166164
DOCS_ENDPOINT,
167-
DocsInput(docs_input=DocsInput.BaseDocsInput(repo_full_name=session.repo_name)),
165+
DocsInput(docs_input=DocsInput.BaseDocsInput(repo_full_name=session.config.repository.full_name)),
168166
DocsResponse,
169167
)
170168

@@ -179,11 +177,12 @@ def ask_expert(self, query: str) -> AskExpertResponse:
179177

180178
def create(self, name: str, query: str) -> CreateResponse:
181179
"""Get AI-generated starter code for a codemod."""
182-
session = CodegenSession()
180+
session = CodegenSession.from_active_session()
181+
language = ProgrammingLanguage(session.config.repository.language)
183182
return self._make_request(
184183
"GET",
185184
CREATE_ENDPOINT,
186-
CreateInput(input=CreateInput.BaseCreateInput(name=name, query=query, language=session.language)),
185+
CreateInput(input=CreateInput.BaseCreateInput(name=name, query=query, language=language)),
187186
CreateResponse,
188187
)
189188

@@ -197,18 +196,24 @@ def identify(self) -> IdentifyResponse | None:
197196
)
198197

199198
def deploy(
200-
self, codemod_name: str, codemod_source: str, lint_mode: bool = False, lint_user_whitelist: list[str] | None = None, message: str | None = None, arguments_schema: dict | None = None
199+
self,
200+
codemod_name: str,
201+
codemod_source: str,
202+
lint_mode: bool = False,
203+
lint_user_whitelist: list[str] | None = None,
204+
message: str | None = None,
205+
arguments_schema: dict | None = None,
201206
) -> DeployResponse:
202207
"""Deploy a codemod to the Modal backend."""
203-
session = CodegenSession()
208+
session = CodegenSession.from_active_session()
204209
return self._make_request(
205210
"POST",
206211
DEPLOY_ENDPOINT,
207212
DeployInput(
208213
input=DeployInput.BaseDeployInput(
209214
codemod_name=codemod_name,
210215
codemod_source=codemod_source,
211-
repo_full_name=session.repo_name,
216+
repo_full_name=session.config.repository.full_name,
212217
lint_mode=lint_mode,
213218
lint_user_whitelist=lint_user_whitelist or [],
214219
message=message,
@@ -220,11 +225,11 @@ def deploy(
220225

221226
def lookup(self, codemod_name: str) -> LookupOutput:
222227
"""Look up a codemod by name."""
223-
session = CodegenSession()
228+
session = CodegenSession.from_active_session()
224229
return self._make_request(
225230
"GET",
226231
LOOKUP_ENDPOINT,
227-
LookupInput(input=LookupInput.BaseLookupInput(codemod_name=codemod_name, repo_full_name=session.repo_name)),
232+
LookupInput(input=LookupInput.BaseLookupInput(codemod_name=codemod_name, repo_full_name=session.config.repository.full_name)),
228233
LookupOutput,
229234
)
230235

@@ -244,7 +249,7 @@ def run_on_pr(self, codemod_name: str, repo_full_name: str, github_pr_number: in
244249
RunOnPRResponse,
245250
)
246251

247-
def lookup_pr(self, repo_full_name: str, github_pr_number: int) -> PRSchema:
252+
def lookup_pr(self, repo_full_name: str, github_pr_number: int) -> PRLookupResponse:
248253
"""Look up a PR by repository and PR number."""
249254
return self._make_request(
250255
"GET",

src/codegen/cli/auth/auth_session.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from dataclasses import dataclass
2+
from pathlib import Path
3+
4+
from codegen.cli.api.client import RestAPI
5+
from codegen.cli.auth.session import CodegenSession
6+
from codegen.cli.auth.token_manager import get_current_token
7+
from codegen.cli.errors import AuthError, NoTokenError
8+
9+
10+
@dataclass
11+
class User:
12+
full_name: str
13+
email: str
14+
github_username: str
15+
16+
17+
@dataclass
18+
class Identity:
19+
token: str
20+
expires_at: str
21+
status: str
22+
user: "User"
23+
24+
25+
class CodegenAuthenticatedSession(CodegenSession):
26+
"""Represents an authenticated codegen session with user and repository context"""
27+
28+
# =====[ Instance attributes ]=====
29+
_token: str | None = None
30+
31+
# =====[ Lazy instance attributes ]=====
32+
_identity: Identity | None = None
33+
34+
def __init__(self, token: str | None = None, repo_path: Path | None = None):
35+
# TODO: fix jank.
36+
# super().__init__(repo_path)
37+
self._token = token
38+
39+
@property
40+
def token(self) -> str | None:
41+
"""Get the current authentication token"""
42+
if self._token:
43+
return self._token
44+
return get_current_token()
45+
46+
@property
47+
def identity(self) -> Identity | None:
48+
"""Get the identity of the user, if a token has been provided"""
49+
if self._identity:
50+
return self._identity
51+
if not self.token:
52+
msg = "No authentication token found"
53+
raise NoTokenError(msg)
54+
55+
identity = RestAPI(self.token).identify()
56+
if not identity:
57+
return None
58+
59+
self._identity = Identity(
60+
token=self.token,
61+
expires_at=identity.auth_context.expires_at,
62+
status=identity.auth_context.status,
63+
user=User(
64+
full_name=identity.user.full_name,
65+
email=identity.user.email,
66+
github_username=identity.user.github_username,
67+
),
68+
)
69+
return self._identity
70+
71+
def is_authenticated(self) -> bool:
72+
"""Check if the session is fully authenticated, including token expiration"""
73+
return bool(self.identity and self.identity.status == "active")
74+
75+
def assert_authenticated(self) -> None:
76+
"""Raise an AuthError if the session is not fully authenticated"""
77+
if not self.identity:
78+
msg = "No identity found for session"
79+
raise AuthError(msg)
80+
if self.identity.status != "active":
81+
msg = "Current session is not active. API Token may be invalid or may have expired."
82+
raise AuthError(msg)

src/codegen/cli/auth/decorators.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@
44
import click
55
import rich
66

7+
from codegen.cli.auth.auth_session import CodegenAuthenticatedSession
78
from codegen.cli.auth.login import login_routine
8-
from codegen.cli.auth.session import CodegenSession
99
from codegen.cli.errors import AuthError, InvalidTokenError, NoTokenError
10+
from codegen.cli.rich.pretty_print import pretty_print_error
1011

1112

1213
def requires_auth(f: Callable) -> Callable:
1314
"""Decorator that ensures a user is authenticated and injects a CodegenSession."""
1415

1516
@functools.wraps(f)
1617
def wrapper(*args, **kwargs):
17-
session = CodegenSession()
18+
session = CodegenAuthenticatedSession.from_active_session()
19+
20+
# Check for valid session
21+
if not session.is_valid():
22+
pretty_print_error(f"The session at path {session.repo_path} is missing or corrupt.\nPlease run 'codegen init' to re-initialize the project.")
23+
raise click.Abort()
1824

1925
try:
2026
if not session.is_authenticated():

src/codegen/cli/auth/login.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
import rich_click as click
55

66
from codegen.cli.api.webapp_routes import USER_SECRETS_ROUTE
7-
from codegen.cli.auth.session import CodegenSession
7+
from codegen.cli.auth.auth_session import CodegenAuthenticatedSession
88
from codegen.cli.auth.token_manager import TokenManager
99
from codegen.cli.env.global_env import global_env
1010
from codegen.cli.errors import AuthError
1111

1212

13-
def login_routine(token: str | None = None) -> CodegenSession:
13+
def login_routine(token: str | None = None) -> CodegenAuthenticatedSession:
1414
"""Guide user through login flow and return authenticated session.
1515
1616
Args:
@@ -39,7 +39,7 @@ def login_routine(token: str | None = None) -> CodegenSession:
3939

4040
# Validate and store token
4141
token_manager = TokenManager()
42-
session = CodegenSession(_token)
42+
session = CodegenAuthenticatedSession(token=_token)
4343

4444
try:
4545
session.assert_authenticated()

0 commit comments

Comments
 (0)