1- """Env contract for the optional coding subagent."""
1+ """Environment contract for the optional coding subagent."""
22
3+ from __future__ import annotations
4+
5+ import logging
36import os
47from collections .abc import Mapping
8+ from dataclasses import dataclass
9+
10+ from coding .github_credentials import (
11+ GitHubAppProvider ,
12+ GitHubCredentialError ,
13+ GitHubCredentialProvider ,
14+ GitHubPatProvider ,
15+ )
516
6- # LangGraph always applies a limit (default 25). This is a safety stop for a
7- # stuck loop, not a budget for a real job.
817CODER_RECURSION_LIMIT = 500
18+ APP_ENV_NAMES = (
19+ "GITHUB_APP_ID" ,
20+ "GITHUB_APP_INSTALLATION_ID" ,
21+ "GITHUB_APP_PRIVATE_KEY_BASE64" ,
22+ )
23+
24+ logger = logging .getLogger (__name__ )
25+
26+
27+ @dataclass (frozen = True )
28+ class GitHubProviders :
29+ coding : GitHubCredentialProvider | None
30+ search : GitHubCredentialProvider | None
31+ error : str | None = None
32+ warning : str | None = None
933
1034
1135def _env (env : Mapping [str , str ] | None ) -> Mapping [str , str ]:
1236 return os .environ if env is None else env
1337
1438
15- def write_token (env : Mapping [str , str ] | None = None ) -> str | None :
16- source = _env (env )
17- for name in ("GITHUB_CODER_TOKEN" , "GITHUB_PERSONAL_ACCESS_TOKEN" ):
18- value = (source .get (name ) or "" ).strip ()
19- if value :
20- return value
21- return None
39+ def _value (source : Mapping [str , str ], name : str ) -> str :
40+ return (source .get (name ) or "" ).strip ()
2241
2342
24- def coding_enabled (env : Mapping [str , str ] | None = None ) -> bool :
43+ def github_providers (
44+ env : Mapping [str , str ] | None = None ,
45+ * ,
46+ client = None ,
47+ now = None ,
48+ ) -> GitHubProviders :
49+ """Select search and coding credentials without making network calls."""
2550 source = _env (env )
26- return bool ((source .get ("DAYTONA_API_KEY" ) or "" ).strip () and write_token (source ))
27-
28-
29- def allowed_repos (env : Mapping [str , str ] | None = None ) -> tuple [str , ...]:
30- raw = (_env (env ).get ("GITHUB_ALLOWED_REPOS" ) or "" ).strip ()
31- if not raw :
32- return ()
33- return tuple (part .strip () for part in raw .split ("," ) if part .strip ())
51+ search_pat = _value (source , "GITHUB_PERSONAL_ACCESS_TOKEN" )
52+ coder_pat = _value (source , "GITHUB_CODER_TOKEN" )
53+ app_values = tuple (_value (source , name ) for name in APP_ENV_NAMES )
54+ app_configured = any (app_values )
55+ app_complete = all (app_values )
56+
57+ search = GitHubPatProvider (search_pat , client = client ) if search_pat else None
58+
59+ if coder_pat and app_complete :
60+ return GitHubProviders (
61+ coding = None ,
62+ search = search ,
63+ error = (
64+ "GITHUB_CODER_TOKEN and complete GitHub App credentials are both "
65+ "configured; choose exactly one explicit coding method"
66+ ),
67+ )
68+ if app_configured and not app_complete :
69+ missing = ", " .join (
70+ name for name , value in zip (APP_ENV_NAMES , app_values ) if not value
71+ )
72+ return GitHubProviders (
73+ coding = None ,
74+ search = search ,
75+ warning = (
76+ "incomplete GitHub App credentials disable coding; missing " + missing
77+ ),
78+ )
79+
80+ coding : GitHubCredentialProvider | None
81+ if coder_pat :
82+ coding = GitHubPatProvider (coder_pat , client = client )
83+ elif app_complete :
84+ try :
85+ coding = GitHubAppProvider (
86+ app_id = app_values [0 ],
87+ installation_id = app_values [1 ],
88+ private_key_base64 = app_values [2 ],
89+ client = client ,
90+ now = now ,
91+ )
92+ except GitHubCredentialError as error :
93+ return GitHubProviders (coding = None , search = search , error = str (error ))
94+ elif search_pat :
95+ coding = search
96+ else :
97+ coding = None
98+
99+ return GitHubProviders (coding = coding , search = search or coding )
34100
35101
36- def repo_is_allowed (repo : str , env : Mapping [str , str ] | None = None ) -> bool :
37- rules = allowed_repos (env )
38- if not rules :
39- return True
40- owner , _ , name = repo .partition ("/" )
41- for rule in rules :
42- if rule .endswith ("/*" ):
43- if owner == rule [:- 2 ]:
44- return True
45- elif repo == rule :
46- return True
47- return False
102+ def coding_enabled (env : Mapping [str , str ] | None = None ) -> bool :
103+ source = _env (env )
104+ selection = github_providers (source )
105+ return bool (
106+ _value (source , "DAYTONA_API_KEY" )
107+ and selection .coding is not None
108+ and selection .error is None
109+ and selection .warning is None
110+ )
111+
112+
113+ def log_configuration_warnings (
114+ selection : GitHubProviders ,
115+ env : Mapping [str , str ] | None = None ,
116+ ) -> None :
117+ source = _env (env )
118+ if selection .error :
119+ logger .error ("[CODER] GitHub configuration error: %s" , selection .error )
120+ if selection .warning :
121+ logger .warning ("[CODER] %s" , selection .warning )
122+ if _value (source , "GITHUB_ALLOWED_REPOS" ):
123+ logger .warning (
124+ "[CODER] GITHUB_ALLOWED_REPOS is ignored; GitHub permissions now "
125+ "define repository access"
126+ )
48127
49128
50129def ttl_minutes (env : Mapping [str , str ] | None = None ) -> int :
51- raw = (_env (env ). get ( "DAYTONA_TTL_MINUTES" ) or "" ). strip ( )
130+ raw = _value (_env (env ), "DAYTONA_TTL_MINUTES" )
52131 try :
53132 value = int (raw )
54133 except ValueError :
@@ -57,5 +136,5 @@ def ttl_minutes(env: Mapping[str, str] | None = None) -> int:
57136
58137
59138def snapshot_id (env : Mapping [str , str ] | None = None ) -> str | None :
60- value = (_env (env ). get ( "DAYTONA_SNAPSHOT" ) or "" ). strip ( )
139+ value = _value (_env (env ), "DAYTONA_SNAPSHOT" )
61140 return value or None
0 commit comments