personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

providers/cli: constrain cogitate provider environments

Build cogitate CLI environments from provider-specific allowlists instead of copying the full parent environment, and require usable Vertex service-account credentials before spawning Gemini CLI. Provider call sites now pass provider names directly and tests cover cross-provider leakage, Vertex strict validation, and settings-file behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+310 -105
+222 -54
tests/test_cli_provider.py
··· 942 942 # --------------------------------------------------------------------------- 943 943 944 944 945 + def test_build_cogitate_env_allowlist_anthropic(): 946 + config = {"providers": {"auth": {"anthropic": "api_key"}}} 947 + with ( 948 + patch.dict( 949 + os.environ, 950 + { 951 + "PATH": "/bin", 952 + "HOME": "/home/test", 953 + "ANTHROPIC_API_KEY": "sk-ant", 954 + "CLAUDE_CONFIG_DIR": "/tmp/claude", 955 + "OPENAI_API_KEY": "sk-oai", 956 + "GOOGLE_API_KEY": "gk", 957 + "HTTPS_PROXY": "http://proxy", 958 + }, 959 + clear=True, 960 + ), 961 + patch("think.utils.get_config", return_value=config), 962 + ): 963 + env = build_cogitate_env("anthropic") 964 + 965 + assert env["ANTHROPIC_API_KEY"] == "sk-ant" 966 + assert env["CLAUDE_CONFIG_DIR"] == "/tmp/claude" 967 + assert env["PATH"] == "/bin" 968 + assert "OPENAI_API_KEY" not in env 969 + assert "GOOGLE_API_KEY" not in env 970 + assert "HTTPS_PROXY" not in env 971 + 972 + 973 + def test_build_cogitate_env_allowlist_openai(): 974 + config = {"providers": {"auth": {"openai": "api_key"}}} 975 + with ( 976 + patch.dict( 977 + os.environ, 978 + { 979 + "PATH": "/bin", 980 + "OPENAI_API_KEY": "sk-oai", 981 + "OPENAI_ORG_ID": "org", 982 + "ANTHROPIC_API_KEY": "sk-ant", 983 + "GOOGLE_API_KEY": "gk", 984 + "NODE_OPTIONS": "--inspect", 985 + }, 986 + clear=True, 987 + ), 988 + patch("think.utils.get_config", return_value=config), 989 + ): 990 + env = build_cogitate_env("openai") 991 + 992 + assert env["OPENAI_API_KEY"] == "sk-oai" 993 + assert env["OPENAI_ORG_ID"] == "org" 994 + assert "ANTHROPIC_API_KEY" not in env 995 + assert "GOOGLE_API_KEY" not in env 996 + assert "NODE_OPTIONS" not in env 997 + 998 + 999 + def test_build_cogitate_env_allowlist_google(): 1000 + config = { 1001 + "providers": { 1002 + "google_backend": "aistudio", 1003 + "auth": {"google": "api_key"}, 1004 + } 1005 + } 1006 + with ( 1007 + patch.dict( 1008 + os.environ, 1009 + { 1010 + "PATH": "/bin", 1011 + "GOOGLE_API_KEY": "gk", 1012 + "GEMINI_HOME": "/tmp/gemini", 1013 + "VERTEX_REGION": "global", 1014 + "OPENAI_API_KEY": "sk-oai", 1015 + "ANTHROPIC_API_KEY": "sk-ant", 1016 + "HTTPS_PROXY": "http://proxy", 1017 + }, 1018 + clear=True, 1019 + ), 1020 + patch("think.utils.get_config", return_value=config), 1021 + ): 1022 + env = build_cogitate_env("google") 1023 + 1024 + assert env["GOOGLE_API_KEY"] == "gk" 1025 + assert env["GEMINI_HOME"] == "/tmp/gemini" 1026 + assert env["VERTEX_REGION"] == "global" 1027 + assert "OPENAI_API_KEY" not in env 1028 + assert "ANTHROPIC_API_KEY" not in env 1029 + assert "HTTPS_PROXY" not in env 1030 + 1031 + 1032 + def test_build_cogitate_env_vertex_strict_validation_raises(): 1033 + config = { 1034 + "providers": { 1035 + "google_backend": "vertex", 1036 + "vertex_credentials": "/tmp/missing-sa.json", 1037 + "auth": {"google": "platform"}, 1038 + } 1039 + } 1040 + with ( 1041 + patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 1042 + patch("think.utils.get_config", return_value=config), 1043 + patch("os.path.exists", return_value=False), 1044 + pytest.raises( 1045 + ValueError, 1046 + match=( 1047 + r"^Vertex provider configured but no usable SA credentials at " 1048 + r"/tmp/missing-sa\.json$" 1049 + ), 1050 + ), 1051 + ): 1052 + build_cogitate_env("google") 1053 + 1054 + 945 1055 class TestBuildCogitateEnv: 946 1056 """Tests for build_cogitate_env — API key stripping for CLI subprocesses.""" 947 1057 ··· 952 1062 patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-secret"}, clear=False), 953 1063 patch("think.utils.get_config", return_value=config), 954 1064 ): 955 - env = build_cogitate_env("ANTHROPIC_API_KEY") 1065 + env = build_cogitate_env("anthropic") 956 1066 assert "ANTHROPIC_API_KEY" not in env 957 1067 958 1068 def test_explicit_platform_strips_key(self): ··· 962 1072 patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-secret"}, clear=False), 963 1073 patch("think.utils.get_config", return_value=config), 964 1074 ): 965 - env = build_cogitate_env("ANTHROPIC_API_KEY") 1075 + env = build_cogitate_env("anthropic") 966 1076 assert "ANTHROPIC_API_KEY" not in env 967 1077 968 1078 def test_api_key_mode_preserves_key(self): ··· 972 1082 patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-secret"}, clear=False), 973 1083 patch("think.utils.get_config", return_value=config), 974 1084 ): 975 - env = build_cogitate_env("ANTHROPIC_API_KEY") 1085 + env = build_cogitate_env("anthropic") 976 1086 assert env["ANTHROPIC_API_KEY"] == "sk-secret" 977 1087 978 1088 def test_missing_auth_section_strips_key(self): ··· 982 1092 patch.dict(os.environ, {"OPENAI_API_KEY": "sk-openai"}, clear=False), 983 1093 patch("think.utils.get_config", return_value=config), 984 1094 ): 985 - env = build_cogitate_env("OPENAI_API_KEY") 1095 + env = build_cogitate_env("openai") 986 1096 assert "OPENAI_API_KEY" not in env 987 1097 988 1098 def test_other_env_vars_preserved(self): ··· 996 1106 ), 997 1107 patch("think.utils.get_config", return_value=config), 998 1108 ): 999 - env = build_cogitate_env("ANTHROPIC_API_KEY") 1109 + env = build_cogitate_env("anthropic") 1000 1110 assert env["HOME"] == "/home/test" 1001 1111 1002 1112 def test_key_not_in_env_is_harmless(self): ··· 1006 1116 patch.dict(os.environ, {}, clear=False), 1007 1117 patch("think.utils.get_config", return_value=config), 1008 1118 ): 1009 - env = build_cogitate_env("GOOGLE_API_KEY") 1119 + env = build_cogitate_env("google") 1010 1120 assert "GOOGLE_API_KEY" not in env 1011 1121 1012 1122 def test_per_provider_independence(self): ··· 1027 1137 ), 1028 1138 patch("think.utils.get_config", return_value=config), 1029 1139 ): 1030 - ant_env = build_cogitate_env("ANTHROPIC_API_KEY") 1031 - oai_env = build_cogitate_env("OPENAI_API_KEY") 1140 + ant_env = build_cogitate_env("anthropic") 1141 + oai_env = build_cogitate_env("openai") 1032 1142 assert ant_env["ANTHROPIC_API_KEY"] == "sk-ant" 1033 1143 assert "OPENAI_API_KEY" not in oai_env 1034 1144 ··· 1037 1147 config = { 1038 1148 "providers": { 1039 1149 "google_backend": "vertex", 1150 + "vertex_credentials": "/tmp/fake-sa.json", 1040 1151 "auth": {"google": "platform"}, 1041 1152 } 1042 1153 } 1043 1154 with ( 1044 1155 patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 1045 1156 patch("think.utils.get_config", return_value=config), 1157 + patch("os.path.exists", return_value=True), 1158 + patch( 1159 + "builtins.open", 1160 + mock_open( 1161 + read_data='{"type": "service_account", "project_id": "my-gcp-project"}' 1162 + ), 1163 + ), 1164 + patch.object(Path, "exists", return_value=True), 1046 1165 ): 1047 - env = build_cogitate_env("GOOGLE_API_KEY") 1166 + env = build_cogitate_env("google") 1048 1167 assert env["GOOGLE_GENAI_USE_VERTEXAI"] == "true" 1049 1168 assert "GOOGLE_API_KEY" not in env 1050 1169 ··· 1061 1180 patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 1062 1181 patch("think.utils.get_config", return_value=config), 1063 1182 patch("os.path.exists", return_value=True), 1183 + patch( 1184 + "builtins.open", 1185 + mock_open( 1186 + read_data='{"type": "service_account", "project_id": "my-gcp-project"}' 1187 + ), 1188 + ), 1189 + patch.object(Path, "exists", return_value=True), 1064 1190 ): 1065 - env = build_cogitate_env("GOOGLE_API_KEY") 1191 + env = build_cogitate_env("google") 1066 1192 assert env["GOOGLE_GENAI_USE_VERTEXAI"] == "true" 1067 1193 assert env["GOOGLE_APPLICATION_CREDENTIALS"] == "/tmp/fake-sa.json" 1068 1194 assert "GOOGLE_API_KEY" not in env ··· 1079 1205 patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 1080 1206 patch("think.utils.get_config", return_value=config), 1081 1207 ): 1082 - env = build_cogitate_env("GOOGLE_API_KEY") 1208 + env = build_cogitate_env("google") 1083 1209 assert "GOOGLE_GENAI_USE_VERTEXAI" not in env 1084 1210 assert env["GOOGLE_API_KEY"] == "gk-test" 1085 1211 1086 1212 def test_auto_backend_detects_vertex(self): 1087 1213 """Auto backend with Vertex detection sets env vars.""" 1088 - config = {"providers": {"auth": {"google": "platform"}}} 1214 + config = { 1215 + "providers": { 1216 + "auth": {"google": "platform"}, 1217 + "vertex_credentials": "/tmp/fake-sa.json", 1218 + } 1219 + } 1089 1220 with ( 1090 1221 patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 1091 1222 patch("think.utils.get_config", return_value=config), 1092 1223 patch("think.providers.google._detect_backend", return_value="vertex"), 1224 + patch("os.path.exists", return_value=True), 1225 + patch( 1226 + "builtins.open", 1227 + mock_open( 1228 + read_data='{"type": "service_account", "project_id": "my-gcp-project"}' 1229 + ), 1230 + ), 1231 + patch.object(Path, "exists", return_value=True), 1093 1232 ): 1094 - env = build_cogitate_env("GOOGLE_API_KEY") 1233 + env = build_cogitate_env("google") 1095 1234 assert env["GOOGLE_GENAI_USE_VERTEXAI"] == "true" 1096 1235 assert "GOOGLE_API_KEY" not in env 1097 1236 ··· 1107 1246 patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-ant"}, clear=True), 1108 1247 patch("think.utils.get_config", return_value=config), 1109 1248 ): 1110 - env = build_cogitate_env("ANTHROPIC_API_KEY") 1249 + env = build_cogitate_env("anthropic") 1111 1250 assert "GOOGLE_GENAI_USE_VERTEXAI" not in env 1112 1251 assert env["ANTHROPIC_API_KEY"] == "sk-ant" 1113 1252 ··· 1130 1269 read_data='{"type": "service_account", "project_id": "my-gcp-project"}' 1131 1270 ), 1132 1271 ), 1272 + patch.object(Path, "exists", return_value=True), 1133 1273 ): 1134 - env = build_cogitate_env("GOOGLE_API_KEY") 1274 + env = build_cogitate_env("google") 1135 1275 assert env["GOOGLE_GENAI_USE_VERTEXAI"] == "true" 1136 1276 assert env["GOOGLE_APPLICATION_CREDENTIALS"] == "/tmp/fake-sa.json" 1137 1277 assert env["GOOGLE_CLOUD_PROJECT"] == "my-gcp-project" ··· 1139 1279 assert "GOOGLE_API_KEY" not in env 1140 1280 1141 1281 def test_vertex_backend_missing_creds_no_project(self): 1142 - """Vertex backend still sets location without explicit SA credentials.""" 1282 + """Vertex backend raises without explicit SA credentials.""" 1143 1283 config = { 1144 1284 "providers": { 1145 1285 "google_backend": "vertex", ··· 1149 1289 with ( 1150 1290 patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 1151 1291 patch("think.utils.get_config", return_value=config), 1292 + pytest.raises( 1293 + ValueError, 1294 + match=( 1295 + r"^Vertex provider configured but no usable SA credentials at None$" 1296 + ), 1297 + ), 1152 1298 ): 1153 - env = build_cogitate_env("GOOGLE_API_KEY") 1154 - assert "GOOGLE_CLOUD_PROJECT" not in env 1155 - assert env["GOOGLE_CLOUD_LOCATION"] == "global" 1299 + build_cogitate_env("google") 1156 1300 1157 1301 def test_vertex_backend_invalid_sa_json_no_project(self): 1158 - """Invalid SA JSON logs and skips project configuration.""" 1302 + """Invalid SA JSON raises before spawning Gemini CLI.""" 1159 1303 config = { 1160 1304 "providers": { 1161 1305 "google_backend": "vertex", ··· 1168 1312 patch("think.utils.get_config", return_value=config), 1169 1313 patch("os.path.exists", return_value=True), 1170 1314 patch("builtins.open", mock_open(read_data="not json")), 1315 + pytest.raises( 1316 + ValueError, 1317 + match=( 1318 + r"^Vertex provider configured but no usable SA credentials at " 1319 + r"/tmp/fake-sa\.json$" 1320 + ), 1321 + ), 1171 1322 ): 1172 - env = build_cogitate_env("GOOGLE_API_KEY") 1173 - assert "GOOGLE_CLOUD_PROJECT" not in env 1174 - assert env["GOOGLE_CLOUD_LOCATION"] == "global" 1323 + build_cogitate_env("google") 1175 1324 1176 1325 def test_vertex_backend_sa_missing_project_id(self): 1177 - """Missing project_id in SA JSON leaves project env unset.""" 1326 + """Missing project_id in SA JSON raises before spawning Gemini CLI.""" 1178 1327 config = { 1179 1328 "providers": { 1180 1329 "google_backend": "vertex", ··· 1194 1343 ) 1195 1344 ), 1196 1345 ), 1346 + pytest.raises( 1347 + ValueError, 1348 + match=( 1349 + r"^Vertex provider configured but no usable SA credentials at " 1350 + r"/tmp/fake-sa\.json$" 1351 + ), 1352 + ), 1197 1353 ): 1198 - env = build_cogitate_env("GOOGLE_API_KEY") 1199 - assert "GOOGLE_CLOUD_PROJECT" not in env 1200 - assert env["GOOGLE_CLOUD_LOCATION"] == "global" 1354 + build_cogitate_env("google") 1201 1355 1202 1356 def test_aistudio_clears_project_and_location(self): 1203 1357 """AI Studio clears inherited Vertex project context.""" ··· 1219 1373 ), 1220 1374 patch("think.utils.get_config", return_value=config), 1221 1375 ): 1222 - env = build_cogitate_env("GOOGLE_API_KEY") 1376 + env = build_cogitate_env("google") 1223 1377 assert "GOOGLE_CLOUD_PROJECT" not in env 1224 1378 assert "GOOGLE_CLOUD_LOCATION" not in env 1225 1379 ··· 1228 1382 config = { 1229 1383 "providers": { 1230 1384 "google_backend": "vertex", 1385 + "vertex_credentials": "/tmp/fake-sa.json", 1231 1386 "auth": {"google": "platform"}, 1232 1387 } 1233 1388 } ··· 1235 1390 patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 1236 1391 patch("think.utils.get_config", return_value=config), 1237 1392 patch("think.utils.get_journal", return_value="/fake/journal"), 1393 + patch("os.path.exists", return_value=True), 1394 + patch( 1395 + "builtins.open", 1396 + mock_open( 1397 + read_data='{"type": "service_account", "project_id": "my-gcp-project"}' 1398 + ), 1399 + ), 1238 1400 patch.object(Path, "exists", return_value=True), 1239 1401 ): 1240 - env = build_cogitate_env("GOOGLE_API_KEY") 1402 + env = build_cogitate_env("google") 1241 1403 assert ( 1242 1404 env["GEMINI_CLI_SYSTEM_SETTINGS_PATH"] 1243 1405 == "/fake/journal/.config/gemini-vertex-settings.json" ··· 1255 1417 patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 1256 1418 patch("think.utils.get_config", return_value=config), 1257 1419 ): 1258 - env = build_cogitate_env("GOOGLE_API_KEY") 1420 + env = build_cogitate_env("google") 1259 1421 assert "GEMINI_CLI_SYSTEM_SETTINGS_PATH" not in env 1260 1422 1261 1423 def test_aistudio_clears_inherited_system_settings_path(self): ··· 1277 1439 ), 1278 1440 patch("think.utils.get_config", return_value=config), 1279 1441 ): 1280 - env = build_cogitate_env("GOOGLE_API_KEY") 1442 + env = build_cogitate_env("google") 1281 1443 assert "GEMINI_CLI_SYSTEM_SETTINGS_PATH" not in env 1282 1444 1283 - def test_vertex_writes_settings_file_when_absent(self): 1445 + def test_vertex_writes_settings_file_when_absent(self, tmp_path): 1284 1446 """Vertex backend creates Gemini CLI system settings when missing.""" 1447 + creds_path = tmp_path / "fake-sa.json" 1448 + creds_path.write_text( 1449 + '{"type": "service_account", "project_id": "my-gcp-project"}', 1450 + encoding="utf-8", 1451 + ) 1452 + journal_path = tmp_path / "journal" 1285 1453 config = { 1286 1454 "providers": { 1287 1455 "google_backend": "vertex", 1456 + "vertex_credentials": str(creds_path), 1288 1457 "auth": {"google": "platform"}, 1289 1458 } 1290 1459 } 1291 1460 with ( 1292 1461 patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 1293 1462 patch("think.utils.get_config", return_value=config), 1294 - patch("think.utils.get_journal", return_value="/fake/journal"), 1295 - patch.object(Path, "exists", return_value=False), 1296 - patch("os.makedirs") as mock_mkdirs, 1297 - patch("builtins.open", mock_open()) as mock_file, 1298 - patch("os.chmod") as mock_chmod, 1463 + patch("think.utils.get_journal", return_value=str(journal_path)), 1299 1464 ): 1300 - env = build_cogitate_env("GOOGLE_API_KEY") 1301 - written = "".join(call.args[0] for call in mock_file().write.call_args_list) 1465 + env = build_cogitate_env("google") 1302 1466 assert env["GEMINI_CLI_SYSTEM_SETTINGS_PATH"] == ( 1303 - "/fake/journal/.config/gemini-vertex-settings.json" 1467 + str(journal_path / ".config" / "gemini-vertex-settings.json") 1468 + ) 1469 + written = (journal_path / ".config" / "gemini-vertex-settings.json").read_text( 1470 + encoding="utf-8" 1304 1471 ) 1305 - assert mock_mkdirs.called 1306 - assert mock_file.called 1307 1472 assert json.loads(written) == { 1308 1473 "security": {"auth": {"selectedType": "vertex-ai"}} 1309 1474 } 1310 - mock_chmod.assert_called_once_with( 1311 - "/fake/journal/.config/gemini-vertex-settings.json", 0o600 1312 - ) 1313 1475 1314 - def test_vertex_skips_settings_write_when_exists(self): 1476 + def test_vertex_skips_settings_write_when_exists(self, tmp_path): 1315 1477 """Vertex backend does not rewrite existing Gemini CLI settings.""" 1478 + creds_path = tmp_path / "fake-sa.json" 1479 + creds_path.write_text( 1480 + '{"type": "service_account", "project_id": "my-gcp-project"}', 1481 + encoding="utf-8", 1482 + ) 1483 + journal_path = tmp_path / "journal" 1484 + settings_path = journal_path / ".config" / "gemini-vertex-settings.json" 1485 + settings_path.parent.mkdir(parents=True) 1486 + settings_path.write_text('{"existing": true}', encoding="utf-8") 1316 1487 config = { 1317 1488 "providers": { 1318 1489 "google_backend": "vertex", 1490 + "vertex_credentials": str(creds_path), 1319 1491 "auth": {"google": "platform"}, 1320 1492 } 1321 1493 } 1322 1494 with ( 1323 1495 patch.dict(os.environ, {"GOOGLE_API_KEY": "gk-test"}, clear=True), 1324 1496 patch("think.utils.get_config", return_value=config), 1325 - patch("think.utils.get_journal", return_value="/fake/journal"), 1326 - patch.object(Path, "exists", return_value=True), 1327 - patch("builtins.open", mock_open()) as mock_file, 1497 + patch("think.utils.get_journal", return_value=str(journal_path)), 1328 1498 ): 1329 - env = build_cogitate_env("GOOGLE_API_KEY") 1330 - assert env["GEMINI_CLI_SYSTEM_SETTINGS_PATH"] == ( 1331 - "/fake/journal/.config/gemini-vertex-settings.json" 1332 - ) 1333 - mock_file.assert_not_called() 1499 + env = build_cogitate_env("google") 1500 + assert env["GEMINI_CLI_SYSTEM_SETTINGS_PATH"] == (str(settings_path)) 1501 + assert settings_path.read_text(encoding="utf-8") == '{"existing": true}'
+8
tests/test_google.py
··· 88 88 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 89 89 monkeypatch.setenv("GOOGLE_API_KEY", "x") 90 90 monkeypatch.setattr( 91 + "think.utils.get_config", 92 + lambda: {"providers": {"google_backend": "aistudio"}}, 93 + ) 94 + monkeypatch.setattr( 91 95 "think.providers.cli.shutil.which", 92 96 lambda name: "/usr/bin/gemini" if name == "gemini" else None, 93 97 ) ··· 166 170 167 171 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 168 172 monkeypatch.setenv("GOOGLE_API_KEY", "x") 173 + monkeypatch.setattr( 174 + "think.utils.get_config", 175 + lambda: {"providers": {"google_backend": "aistudio"}}, 176 + ) 169 177 monkeypatch.setattr("think.providers.cli.shutil.which", lambda _name: None) 170 178 171 179 ndjson_input = json.dumps(
+4
tests/test_google_thinking.py
··· 34 34 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 35 35 monkeypatch.setenv("GOOGLE_API_KEY", "x") 36 36 monkeypatch.setattr( 37 + "think.utils.get_config", 38 + lambda: {"providers": {"google_backend": "aistudio"}}, 39 + ) 40 + monkeypatch.setattr( 37 41 "think.providers.cli.shutil.which", 38 42 lambda name: "/usr/bin/gemini" if name == "gemini" else None, 39 43 )
+1 -1
think/providers/anthropic.py
··· 298 298 callback=callback, 299 299 aggregator=aggregator, 300 300 cwd=Path(cwd_value) if cwd_value else None, 301 - env=build_cogitate_env("ANTHROPIC_API_KEY"), 301 + env=build_cogitate_env("anthropic"), 302 302 ) 303 303 304 304 result = await runner.run()
+73 -48
think/providers/cli.py
··· 510 510 session_id = self.translate(event_data, self.aggregator, self.callback) 511 511 if session_id: 512 512 self.cli_session_id = session_id 513 + except QuotaExhaustedError: 514 + raise 513 515 except Exception: 514 516 LOG.exception("Error translating CLI event: %s", line[:200]) 515 517 ··· 619 621 # --------------------------------------------------------------------------- 620 622 621 623 622 - def build_cogitate_env(env_key: str) -> dict[str, str]: 623 - """Build environment dict for a cogitate CLI subprocess. 624 + _BASE_ALLOWLIST = [ 625 + "PATH", 626 + "HOME", 627 + "USER", 628 + "LOGNAME", 629 + "SHELL", 630 + "PWD", 631 + "TERM", 632 + "TMPDIR", 633 + "TZ", 634 + "LANG", 635 + "LC_*", 636 + "XDG_*", 637 + "SOLSTONE_*", 638 + "SOL_*", 639 + "SSL_CERT_FILE", 640 + "SSL_CERT_DIR", 641 + ] 624 642 625 - By default, strips the provider's API key so the CLI uses its own 626 - platform/account-based auth. Controlled by the ``providers.auth`` 627 - section in journal config: 643 + _PROVIDER_ALLOWLIST: dict[str, list[str]] = { 644 + "anthropic": ["ANTHROPIC_*", "CLAUDE_*"], 645 + "openai": ["OPENAI_*"], 646 + "google": ["GOOGLE_*", "GEMINI_*", "VERTEX_*"], 647 + } 628 648 629 - "providers": { 630 - "auth": { 631 - "anthropic": "platform" // default — strip key 632 - } 633 - } 649 + 650 + def _matches_env_pattern(key: str, pattern: str) -> bool: 651 + if pattern.endswith("*"): 652 + return key.startswith(pattern[:-1]) 653 + return key == pattern 654 + 634 655 635 - Values: ``"platform"`` (default) strips the key; ``"api_key"`` preserves it. 656 + def build_cogitate_env(provider_name: str) -> dict[str, str]: 657 + """Build environment dict for a cogitate CLI subprocess. 658 + 659 + The child environment is built from an allowlist. Patterns ending in ``*`` 660 + match the prefix before the star, including the exact prefix string. 661 + No other glob characters are supported. 636 662 637 663 Args: 638 - env_key: Environment variable name to consider stripping 639 - (e.g., ``"ANTHROPIC_API_KEY"``). 664 + provider_name: Provider name (``anthropic``, ``openai``, or ``google``). 640 665 641 666 Returns: 642 - Copy of ``os.environ`` with the key removed when auth mode is platform. 667 + Filtered environment for the provider CLI. 643 668 """ 669 + from think.providers import PROVIDER_METADATA 644 670 from think.utils import get_config 645 671 672 + if provider_name not in _PROVIDER_ALLOWLIST: 673 + valid = ", ".join(sorted(_PROVIDER_ALLOWLIST)) 674 + raise ValueError(f"Unsupported cogitate provider: {provider_name!r} ({valid})") 675 + 646 676 config = get_config() 647 - auth_config = config.get("providers", {}).get("auth", {}) 648 - 649 - # Determine provider name from env_key for config lookup 650 - # e.g., "ANTHROPIC_API_KEY" -> lookup auth_config for matching provider 651 - # We check all auth entries; default is "platform" for any missing provider 652 - auth_mode = "platform" 653 - for provider, mode in auth_config.items(): 654 - from think.providers import PROVIDER_METADATA 655 - 656 - meta = PROVIDER_METADATA.get(provider, {}) 657 - if meta.get("env_key") == env_key: 658 - auth_mode = mode 659 - break 677 + providers_config = config.get("providers", {}) 678 + auth_config = providers_config.get("auth", {}) 679 + auth_mode = auth_config.get(provider_name, "platform") 680 + env_key = PROVIDER_METADATA[provider_name]["env_key"] 681 + allowlist = _BASE_ALLOWLIST + _PROVIDER_ALLOWLIST[provider_name] 682 + env = { 683 + key: value 684 + for key, value in os.environ.items() 685 + if any(_matches_env_pattern(key, pattern) for pattern in allowlist) 686 + } 660 687 661 - env = os.environ.copy() 662 688 if auth_mode == "platform": 663 689 env.pop(env_key, None) 664 690 665 691 # Vertex AI / AI Studio: set backend env vars for Google provider 666 - if env_key == "GOOGLE_API_KEY": 667 - providers_config = config.get("providers", {}) 692 + if provider_name == "google": 668 693 google_backend = providers_config.get("google_backend", "auto") 669 694 670 695 # Determine effective backend ··· 685 710 env.pop("GOOGLE_API_KEY", None) 686 711 # SA credentials: set GOOGLE_APPLICATION_CREDENTIALS 687 712 creds_path = providers_config.get("vertex_credentials") 688 - if creds_path and os.path.exists(creds_path): 689 - env["GOOGLE_APPLICATION_CREDENTIALS"] = creds_path 690 - # Project context lets the Gemini CLI use Vertex instead of 691 - # falling back to AI Studio auth. 692 - try: 693 - with open(creds_path, encoding="utf-8") as _f: 694 - _sa_data = json.load(_f) 695 - if "project_id" in _sa_data: 696 - env["GOOGLE_CLOUD_PROJECT"] = _sa_data["project_id"] 697 - else: 698 - LOG.warning( 699 - "SA credentials at %s missing project_id", creds_path 700 - ) 701 - except (OSError, json.JSONDecodeError) as exc: 702 - LOG.warning( 703 - "Could not read project_id from %s: %s", creds_path, exc 704 - ) 705 - # else: GOOGLE_APPLICATION_CREDENTIALS may be inherited from env 713 + if not creds_path or not os.path.exists(creds_path): 714 + raise ValueError( 715 + f"Vertex provider configured but no usable SA credentials at {creds_path}" 716 + ) 717 + try: 718 + with open(creds_path, encoding="utf-8") as _f: 719 + _sa_data = json.load(_f) 720 + except (OSError, json.JSONDecodeError) as exc: 721 + raise ValueError( 722 + f"Vertex provider configured but no usable SA credentials at {creds_path}" 723 + ) from exc 724 + project_id = _sa_data.get("project_id") 725 + if not project_id: 726 + raise ValueError( 727 + f"Vertex provider configured but no usable SA credentials at {creds_path}" 728 + ) 729 + env["GOOGLE_APPLICATION_CREDENTIALS"] = creds_path 730 + env["GOOGLE_CLOUD_PROJECT"] = project_id 706 731 env["GOOGLE_CLOUD_LOCATION"] = "global" 707 732 from think.utils import get_journal 708 733
+1 -1
think/providers/google.py
··· 803 803 callback=callback, 804 804 aggregator=aggregator, 805 805 cwd=Path(cwd_value) if cwd_value else None, 806 - env=build_cogitate_env("GOOGLE_API_KEY"), 806 + env=build_cogitate_env("google"), 807 807 ) 808 808 809 809 result = await runner.run()
+1 -1
think/providers/openai.py
··· 220 220 callback=cb, 221 221 aggregator=aggregator, 222 222 cwd=Path(cwd_value) if cwd_value else None, 223 - env=build_cogitate_env("OPENAI_API_KEY"), 223 + env=build_cogitate_env("openai"), 224 224 ) 225 225 226 226 try: