@team-agent/installer 0.1.0

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.
Files changed (36) hide show
  1. package/README.md +201 -0
  2. package/crates/team-agent-core/Cargo.toml +12 -0
  3. package/crates/team-agent-core/src/lib.rs +287 -0
  4. package/crates/team-agent-core/src/main.rs +152 -0
  5. package/examples/team.spec.yaml +206 -0
  6. package/examples/team_state.md +35 -0
  7. package/npm/install.mjs +266 -0
  8. package/package.json +28 -0
  9. package/pyproject.toml +18 -0
  10. package/schemas/result-envelope.schema.json +76 -0
  11. package/schemas/team.schema.json +241 -0
  12. package/scripts/install.py +88 -0
  13. package/scripts/run_regression_tests.py +79 -0
  14. package/skills/team-agent/SKILL.md +173 -0
  15. package/src/team_agent/__init__.py +3 -0
  16. package/src/team_agent/__main__.py +5 -0
  17. package/src/team_agent/cli.py +857 -0
  18. package/src/team_agent/compiler.py +269 -0
  19. package/src/team_agent/coordinator.py +62 -0
  20. package/src/team_agent/errors.py +10 -0
  21. package/src/team_agent/events.py +37 -0
  22. package/src/team_agent/fake_worker.py +80 -0
  23. package/src/team_agent/mcp_server.py +579 -0
  24. package/src/team_agent/message_store.py +497 -0
  25. package/src/team_agent/paths.py +45 -0
  26. package/src/team_agent/permissions.py +123 -0
  27. package/src/team_agent/profiles.py +882 -0
  28. package/src/team_agent/providers.py +1045 -0
  29. package/src/team_agent/routing.py +84 -0
  30. package/src/team_agent/runtime.py +5213 -0
  31. package/src/team_agent/rust_core.py +156 -0
  32. package/src/team_agent/simple_yaml.py +236 -0
  33. package/src/team_agent/spec.py +308 -0
  34. package/src/team_agent/state.py +112 -0
  35. package/src/team_agent/task_graph.py +80 -0
  36. package/templates/team_state.md +32 -0
@@ -0,0 +1,882 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ import json
5
+ import os
6
+ import re
7
+ import shlex
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from team_agent.rust_core import redact_text
15
+
16
+ AUTH_MODES = {"subscription", "official_api", "compatible_api"}
17
+ PROFILE_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
18
+ SECRET_KEYS = {"API_KEY", "AUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "OPENAI_API_KEY", "GEMINI_API_KEY"}
19
+ PROXY_ENV_KEYS = ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", "https_proxy", "http_proxy", "all_proxy", "NO_PROXY", "no_proxy")
20
+ CA_ENV_KEYS = ("NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE")
21
+ COMPATIBLE_API_NETWORK_ENV_KEYS = PROXY_ENV_KEYS + CA_ENV_KEYS
22
+ PROFILE_SECRET_BOUNDARY_TEXT = """# Team Agent Profile Secret Boundary
23
+
24
+ Do not read, print, grep, cat, sed, copy, summarize, or open raw `*.env` files in this directory.
25
+ These files may contain API keys or auth tokens and must stay out of agent context.
26
+
27
+ Use `team-agent profile show <name> --workspace . --json` or `team-agent profile doctor <name> --workspace . --json`
28
+ for redacted status. If a required value is missing, ask the human user to edit the local profile file.
29
+ """
30
+
31
+
32
+ def profile_dir(workspace: Path) -> Path:
33
+ return workspace / ".team" / "current" / "profiles"
34
+
35
+
36
+ def _profile_lookup_dir(workspace: Path, profiles_dir: Path | str | None = None) -> Path:
37
+ return Path(profiles_dir).resolve() if profiles_dir else profile_dir(workspace)
38
+
39
+
40
+ def _agent_profile_dir(agent: dict[str, Any]) -> Path | None:
41
+ value = agent.get("_profile_dir")
42
+ return Path(str(value)).resolve() if value else None
43
+
44
+
45
+ def _safe_inspection_command(name: str, profiles_dir: Path | str | None = None) -> str:
46
+ if profiles_dir:
47
+ return f"team-agent profile show {name} --team {Path(profiles_dir).resolve().parent} --json"
48
+ return f"team-agent profile show {name} --workspace . --json"
49
+
50
+
51
+ def _safe_init_command(name: str, auth_mode: str, profiles_dir: Path | str | None = None) -> str:
52
+ if profiles_dir:
53
+ return f"team-agent profile init {name} --auth-mode {auth_mode} --team {Path(profiles_dir).resolve().parent}"
54
+ return f"team-agent profile init {name} --auth-mode {auth_mode}"
55
+
56
+
57
+ def ensure_profile_secret_boundary_dir(directory: Path) -> Path:
58
+ directory.mkdir(parents=True, exist_ok=True)
59
+ first_path = directory / "AGENTS.md"
60
+ for filename in ("AGENTS.md", "CLAUDE.md"):
61
+ path = directory / filename
62
+ if not path.exists():
63
+ path.write_text(PROFILE_SECRET_BOUNDARY_TEXT, encoding="utf-8")
64
+ return first_path
65
+
66
+
67
+ def ensure_profile_secret_boundary(workspace: Path) -> Path:
68
+ directory = profile_dir(workspace)
69
+ return ensure_profile_secret_boundary_dir(directory)
70
+
71
+
72
+ def init_profile(workspace: Path, name: str, auth_mode: str, profiles_dir: Path | str | None = None) -> dict[str, Any]:
73
+ if auth_mode not in AUTH_MODES:
74
+ raise ValueError(f"unknown auth_mode: {auth_mode}")
75
+ directory = _profile_lookup_dir(workspace, profiles_dir)
76
+ directory.mkdir(parents=True, exist_ok=True)
77
+ ensure_profile_secret_boundary_dir(directory)
78
+ path = directory / f"{name}.env"
79
+ template_path = directory / f"{name}.example.env"
80
+ if auth_mode == "subscription":
81
+ body = f"AUTH_MODE=subscription\nPROFILE_NAME={name}\n"
82
+ elif auth_mode == "official_api":
83
+ body = f"AUTH_MODE=official_api\nPROFILE_NAME={name}\nAPI_KEY=\nMODEL=\n"
84
+ else:
85
+ body = f"AUTH_MODE=compatible_api\nPROFILE_NAME={name}\nBASE_URL=\nAPI_KEY=\nMODEL=\n"
86
+ created_profile = False
87
+ created_template = False
88
+ if not path.exists():
89
+ path.write_text(body, encoding="utf-8")
90
+ try:
91
+ path.chmod(0o600)
92
+ except OSError:
93
+ pass
94
+ created_profile = True
95
+ if not template_path.exists():
96
+ template_path.write_text(body, encoding="utf-8")
97
+ created_template = True
98
+ safe_inspection = _safe_inspection_command(name, directory if profiles_dir else None)
99
+ return {
100
+ "ok": True,
101
+ "profile": name,
102
+ "auth_mode": auth_mode,
103
+ "path": str(path),
104
+ "template_path": str(template_path),
105
+ "created_profile": created_profile,
106
+ "created_template": created_template,
107
+ "secret_written": False,
108
+ "safe_inspection_command": safe_inspection,
109
+ "raw_file_read_allowed_for_agents": False,
110
+ "instruction": (
111
+ f"Fill {path} locally; do not paste API keys into chat or role docs. "
112
+ f"Agents must inspect this profile only with `{safe_inspection}`."
113
+ ),
114
+ }
115
+
116
+
117
+ def doctor_profile(workspace: Path, name: str, profiles_dir: Path | str | None = None) -> dict[str, Any]:
118
+ directory = _profile_lookup_dir(workspace, profiles_dir)
119
+ loaded = load_profile(workspace, name, directory)
120
+ real_path = directory / f"{name}.env"
121
+ example_path = directory / f"{name}.example.env"
122
+ exists = bool(loaded.get("exists"))
123
+ values = loaded.get("values", {})
124
+ keys_present = [key for key, value in values.items() if value]
125
+ auth_mode = values.get("AUTH_MODE")
126
+ return {
127
+ "ok": exists,
128
+ "profile": name,
129
+ "path": str(loaded.get("path") or real_path),
130
+ "template_path": str(example_path),
131
+ "credential_present": real_path.exists(),
132
+ "auth_mode": auth_mode,
133
+ "keys_present": sorted(keys_present),
134
+ "secret_keys_present": sorted(key for key in keys_present if _is_secret_key(key)),
135
+ "redaction_engine": redact_text(" ".join(f"{key}=present" for key in keys_present)).get("engine"),
136
+ "secret_values_printed": False,
137
+ "safe_for_agent_context": True,
138
+ "raw_file_read_allowed_for_agents": False,
139
+ "safe_inspection_command": _safe_inspection_command(name, directory if profiles_dir else None),
140
+ "suggestion": None if exists else f"Run {_safe_init_command(name, 'subscription', directory if profiles_dir else None)}.",
141
+ }
142
+
143
+
144
+ def show_profile(workspace: Path, name: str, profiles_dir: Path | str | None = None) -> dict[str, Any]:
145
+ loaded = load_profile(workspace, name, profiles_dir)
146
+ values: dict[str, str] = loaded.get("values", {})
147
+ auth_mode = values.get("AUTH_MODE")
148
+ safe_values = {
149
+ key: _safe_profile_value(key, value)
150
+ for key, value in sorted(values.items())
151
+ }
152
+ missing = _common_missing_values(auth_mode, values)
153
+ return {
154
+ "ok": bool(loaded.get("exists")),
155
+ "profile": name,
156
+ "credential_present": bool(loaded.get("credential_present")),
157
+ "auth_mode": auth_mode,
158
+ "values": safe_values,
159
+ "keys_present": sorted(key for key, value in values.items() if value),
160
+ "secret_keys_present": sorted(key for key, value in values.items() if value and _is_secret_key(key)),
161
+ "missing_common": missing,
162
+ "safe_for_agent_context": True,
163
+ "secret_values_printed": False,
164
+ "raw_file_read_allowed_for_agents": False,
165
+ "instruction": (
166
+ "Use this redacted output for diagnostics. Do not read .team/*/profiles/*.env "
167
+ "or .team/runtime/provider-env/*.env into agent context."
168
+ ),
169
+ }
170
+
171
+
172
+ def known_profiles(team_dir: Path) -> set[str]:
173
+ directory = team_dir / "profiles"
174
+ if not directory.exists():
175
+ return set()
176
+ names = set()
177
+ for path in directory.glob("*.env"):
178
+ names.add(path.name.removesuffix(".env").removesuffix(".example"))
179
+ for path in directory.glob("*.example.env"):
180
+ names.add(path.name.removesuffix(".example.env"))
181
+ return names
182
+
183
+
184
+ def gitignore_patterns() -> list[str]:
185
+ return [".team/*/profiles/*.env", "!.team/*/profiles/*.example.env", ".team/runtime/provider-env/*.env"]
186
+
187
+
188
+ def load_profile(workspace: Path, name: str, profiles_dir: Path | str | None = None) -> dict[str, Any]:
189
+ directory = _profile_lookup_dir(workspace, profiles_dir)
190
+ real_path = directory / f"{name}.env"
191
+ example_path = directory / f"{name}.example.env"
192
+ path = real_path if real_path.exists() else example_path
193
+ if not path.exists():
194
+ return {
195
+ "exists": False,
196
+ "profile": name,
197
+ "path": str(real_path),
198
+ "template_path": str(example_path),
199
+ "credential_present": False,
200
+ "values": {},
201
+ }
202
+ values = parse_env_file(path)
203
+ return {
204
+ "exists": True,
205
+ "profile": name,
206
+ "path": str(path),
207
+ "template_path": str(example_path),
208
+ "credential_present": real_path.exists(),
209
+ "values": values,
210
+ }
211
+
212
+
213
+ def parse_env_file(path: Path) -> dict[str, str]:
214
+ values: dict[str, str] = {}
215
+ for raw in path.read_text(encoding="utf-8").splitlines():
216
+ line = raw.strip()
217
+ if not line or line.startswith("#"):
218
+ continue
219
+ if line.startswith("export "):
220
+ line = line[len("export ") :].strip()
221
+ if "=" not in line:
222
+ continue
223
+ key, value = line.split("=", 1)
224
+ key = key.strip()
225
+ if not PROFILE_KEY_RE.fullmatch(key):
226
+ continue
227
+ values[key] = _strip_env_value(value.strip())
228
+ return values
229
+
230
+
231
+ def validate_agent_profile(workspace: Path, agent: dict[str, Any]) -> dict[str, Any]:
232
+ profile = agent.get("profile")
233
+ auth_mode = str(agent.get("auth_mode") or "subscription")
234
+ profiles_dir = _agent_profile_dir(agent)
235
+ result = {
236
+ "ok": True,
237
+ "agent_id": agent.get("id"),
238
+ "provider": agent.get("provider"),
239
+ "profile": profile,
240
+ "auth_mode": auth_mode,
241
+ "path": None,
242
+ "credential_present": False,
243
+ "keys_present": [],
244
+ "missing_required": [],
245
+ "secret_values_printed": False,
246
+ }
247
+ if not profile:
248
+ return result
249
+ loaded = load_profile(workspace, str(profile), profiles_dir)
250
+ result["path"] = loaded.get("path")
251
+ result["credential_present"] = bool(loaded.get("credential_present"))
252
+ if not loaded.get("exists"):
253
+ result["ok"] = False
254
+ result["reason"] = "profile_missing"
255
+ result["suggestion"] = (
256
+ f"Run {_safe_init_command(str(profile), auth_mode, profiles_dir)}, then ask the human user "
257
+ "to fill the generated local profile file."
258
+ )
259
+ return result
260
+ values: dict[str, str] = loaded.get("values", {})
261
+ file_auth_mode = values.get("AUTH_MODE")
262
+ if file_auth_mode and file_auth_mode != auth_mode:
263
+ result["ok"] = False
264
+ result["reason"] = "auth_mode_mismatch"
265
+ result["file_auth_mode"] = file_auth_mode
266
+ result["suggestion"] = (
267
+ f"Set AUTH_MODE={auth_mode} in the local profile or update the role doc. "
268
+ f"Inspect safely with `{_safe_inspection_command(str(profile), profiles_dir)}`."
269
+ )
270
+ return result
271
+ keys_present = sorted(key for key, value in values.items() if value)
272
+ result["keys_present"] = keys_present
273
+ role_model = str(agent.get("model") or "") or None
274
+ profile_model = values.get("MODEL") or values.get("ANTHROPIC_MODEL")
275
+ effective = role_model or profile_model
276
+ result["effective_model"] = effective
277
+ result["model_source"] = "role" if role_model else ("profile" if profile_model else None)
278
+ if auth_mode == "compatible_api" and role_model and profile_model and role_model != profile_model:
279
+ result["ok"] = False
280
+ result["reason"] = "model_mismatch"
281
+ result["suggestion"] = (
282
+ f"Role model {role_model!r} does not match the profile MODEL value; "
283
+ f"inspect safely with `{_safe_inspection_command(str(profile), profiles_dir)}`, "
284
+ "then remove model from the role doc or make both values identical."
285
+ )
286
+ return result
287
+ required = required_profile_keys(str(agent.get("provider") or ""), auth_mode)
288
+ missing = [key for key in required if not values.get(key) and not _alternate_value(values, key)]
289
+ if auth_mode == "compatible_api" and not effective:
290
+ missing.append("MODEL")
291
+ result["missing_required"] = missing
292
+ if missing:
293
+ result["ok"] = False
294
+ result["reason"] = "profile_required_values_missing"
295
+ result["suggestion"] = (
296
+ f"Ask the human user to fill {', '.join(missing)} in local profile {profile}; "
297
+ f"inspect safely with `{_safe_inspection_command(str(profile), profiles_dir)}`. "
298
+ "Do not paste API keys into chat."
299
+ )
300
+ return result
301
+
302
+
303
+ def smoke_check_agent_profile(workspace: Path, agent: dict[str, Any], timeout: float = 8.0) -> dict[str, Any]:
304
+ auth_mode = str(agent.get("auth_mode") or "subscription")
305
+ profile = agent.get("profile")
306
+ result = {
307
+ "ok": True,
308
+ "agent_id": agent.get("id"),
309
+ "provider": agent.get("provider"),
310
+ "profile": profile,
311
+ "auth_mode": auth_mode,
312
+ "status": "not_required",
313
+ "secret_values_printed": False,
314
+ }
315
+ if auth_mode != "compatible_api" or not profile:
316
+ return result
317
+ loaded = load_profile(workspace, str(profile), _agent_profile_dir(agent))
318
+ values: dict[str, str] = loaded.get("values", {})
319
+ if str(values.get("PROFILE_SMOKE", "true")).lower() in {"0", "false", "no", "off"}:
320
+ result["status"] = "skipped_by_profile"
321
+ return result
322
+ validation = validate_agent_profile(workspace, agent)
323
+ if not validation.get("ok"):
324
+ return {**result, **validation, "ok": False, "status": "profile_invalid"}
325
+ provider = str(agent.get("provider") or "")
326
+ model = effective_model(agent, workspace)
327
+ if provider in {"claude", "claude_code"}:
328
+ return _anthropic_compatible_smoke(values, model, result, timeout)
329
+ if provider == "codex":
330
+ return _openai_compatible_smoke(values, model, result, timeout)
331
+ result["status"] = "unsupported_provider_smoke_skipped"
332
+ return result
333
+
334
+
335
+ def required_profile_keys(provider: str, auth_mode: str) -> list[str]:
336
+ if auth_mode == "subscription":
337
+ return []
338
+ if provider in {"claude", "claude_code"}:
339
+ if auth_mode == "official_api":
340
+ return ["API_KEY"]
341
+ if auth_mode == "compatible_api":
342
+ return ["BASE_URL", "API_KEY"]
343
+ if provider == "codex":
344
+ if auth_mode == "official_api":
345
+ return ["API_KEY"]
346
+ if auth_mode == "compatible_api":
347
+ return ["BASE_URL", "API_KEY"]
348
+ if provider == "gemini_cli" and auth_mode in {"official_api", "compatible_api"}:
349
+ return ["API_KEY"]
350
+ return []
351
+
352
+
353
+ def compact_profile_check(check: dict[str, Any]) -> dict[str, Any]:
354
+ keys = [
355
+ "agent_id",
356
+ "provider",
357
+ "profile",
358
+ "auth_mode",
359
+ "ok",
360
+ "status",
361
+ "reason",
362
+ "credential_present",
363
+ "keys_present",
364
+ "missing_required",
365
+ "effective_model",
366
+ "model_source",
367
+ "suggestion",
368
+ "http_status",
369
+ "endpoint",
370
+ "error",
371
+ "proxy_configured",
372
+ "proxy_scheme",
373
+ "proxy_url",
374
+ "proxy_source",
375
+ "proxy_mode",
376
+ ]
377
+ return {key: check.get(key) for key in keys if key in check}
378
+
379
+
380
+ def prepare_agent_profile_launch(workspace: Path, agent: dict[str, Any]) -> dict[str, Any] | None:
381
+ profile = agent.get("profile")
382
+ if not profile:
383
+ return None
384
+ check = validate_agent_profile(workspace, agent)
385
+ if not check.get("ok"):
386
+ raise ValueError(_format_profile_check_failure(check))
387
+ loaded = load_profile(workspace, str(profile), _agent_profile_dir(agent))
388
+ values: dict[str, str] = loaded.get("values", {})
389
+ auth_mode = str(agent.get("auth_mode") or values.get("AUTH_MODE") or "subscription")
390
+ provider = str(agent.get("provider") or "")
391
+ exports = _provider_env_exports(provider, auth_mode, values)
392
+ claude_projects_root = None
393
+ if provider in {"claude", "claude_code"} and auth_mode == "compatible_api":
394
+ claude_config_dir = _compatible_claude_config_dir(workspace, str(agent["id"]))
395
+ exports["CLAUDE_CONFIG_DIR"] = str(claude_config_dir)
396
+ claude_projects_root = str(claude_config_dir / "projects")
397
+ exports.update(_compatible_api_network_exports(auth_mode, values))
398
+ unsets = _provider_env_unsets(provider, auth_mode)
399
+ if auth_mode == "compatible_api" and _profile_proxy_mode(values) == "direct":
400
+ unsets.extend(COMPATIBLE_API_NETWORK_ENV_KEYS)
401
+ exports = {key: value for key, value in exports.items() if key not in COMPATIBLE_API_NETWORK_ENV_KEYS}
402
+ overrides = _provider_command_overrides(str(agent.get("provider") or ""), auth_mode, values, agent)
403
+ env_file = None
404
+ if exports or unsets:
405
+ env_file = _write_runtime_env_file(workspace, str(agent["id"]), exports, unsets)
406
+ return {
407
+ "profile": str(profile),
408
+ "auth_mode": auth_mode,
409
+ "path": loaded.get("path"),
410
+ "credential_present": loaded.get("credential_present"),
411
+ "env_file": str(env_file) if env_file else None,
412
+ "env_keys": sorted(exports),
413
+ "env_unset": sorted(unsets),
414
+ "claude_projects_root": claude_projects_root,
415
+ "command_overrides": overrides,
416
+ "secret_values_printed": False,
417
+ }
418
+
419
+
420
+ def effective_model(agent: dict[str, Any], workspace: Path | None = None) -> str | None:
421
+ if agent.get("model"):
422
+ return str(agent["model"])
423
+ if workspace is None or not agent.get("profile"):
424
+ return None
425
+ loaded = load_profile(workspace, str(agent["profile"]), _agent_profile_dir(agent))
426
+ values: dict[str, str] = loaded.get("values", {})
427
+ return values.get("MODEL") or values.get("ANTHROPIC_MODEL")
428
+
429
+
430
+ def _provider_env_exports(provider: str, auth_mode: str, values: dict[str, str]) -> dict[str, str]:
431
+ if auth_mode == "subscription":
432
+ return {}
433
+ if provider in {"claude", "claude_code"}:
434
+ exports: dict[str, str] = {}
435
+ base_url = values.get("ANTHROPIC_BASE_URL") or values.get("BASE_URL")
436
+ api_key = values.get("ANTHROPIC_API_KEY") or values.get("API_KEY")
437
+ auth_token = values.get("ANTHROPIC_AUTH_TOKEN") or values.get("AUTH_TOKEN")
438
+ model = values.get("ANTHROPIC_MODEL") or values.get("MODEL")
439
+ if base_url:
440
+ exports["ANTHROPIC_BASE_URL"] = base_url
441
+ if auth_mode == "official_api" and api_key:
442
+ exports["ANTHROPIC_API_KEY"] = api_key
443
+ if auth_token or (auth_mode == "compatible_api" and api_key):
444
+ exports["ANTHROPIC_AUTH_TOKEN"] = auth_token or api_key
445
+ if model:
446
+ exports["ANTHROPIC_MODEL"] = model
447
+ return exports
448
+ if provider == "codex":
449
+ exports = {}
450
+ api_key = values.get("OPENAI_API_KEY") or values.get("API_KEY")
451
+ if api_key:
452
+ exports["TEAM_AGENT_PROVIDER_API_KEY"] = api_key
453
+ exports.setdefault("OPENAI_API_KEY", api_key)
454
+ if values.get("BASE_URL"):
455
+ exports["OPENAI_BASE_URL"] = values["BASE_URL"]
456
+ return exports
457
+ if provider == "gemini_cli":
458
+ api_key = values.get("GEMINI_API_KEY") or values.get("API_KEY")
459
+ return {"GEMINI_API_KEY": api_key} if api_key else {}
460
+ return {}
461
+
462
+
463
+ def _provider_env_unsets(provider: str, auth_mode: str) -> list[str]:
464
+ unsets: list[str] = []
465
+ if provider in {"claude", "claude_code"}:
466
+ if auth_mode == "compatible_api":
467
+ unsets.append("ANTHROPIC_API_KEY")
468
+ if auth_mode == "official_api":
469
+ unsets.append("ANTHROPIC_AUTH_TOKEN")
470
+ if provider == "codex" and auth_mode == "compatible_api":
471
+ unsets.extend(["OPENAI_API_KEY", "OPENAI_BASE_URL"])
472
+ if provider == "gemini_cli" and auth_mode == "compatible_api":
473
+ unsets.append("GEMINI_API_KEY")
474
+ return sorted(set(unsets))
475
+
476
+
477
+ def _provider_command_overrides(provider: str, auth_mode: str, values: dict[str, str], agent: dict[str, Any]) -> dict[str, Any]:
478
+ overrides: dict[str, Any] = {}
479
+ model = agent.get("model") or values.get("MODEL") or values.get("ANTHROPIC_MODEL")
480
+ if model:
481
+ overrides["model"] = str(model)
482
+ if provider == "codex":
483
+ codex_profile = values.get("CODEX_PROFILE") or values.get("NATIVE_PROFILE")
484
+ if codex_profile:
485
+ overrides["codex_profile"] = codex_profile
486
+ configs: list[str] = []
487
+ model_provider = values.get("MODEL_PROVIDER")
488
+ base_url = values.get("BASE_URL")
489
+ if auth_mode == "compatible_api" and model_provider and base_url and _safe_codex_provider_id(model_provider):
490
+ configs.append(f'model_provider="{model_provider}"')
491
+ prefix = f"model_providers.{model_provider}"
492
+ configs.append(f'{prefix}.base_url="{base_url}"')
493
+ configs.append(f'{prefix}.env_key="TEAM_AGENT_PROVIDER_API_KEY"')
494
+ if values.get("WIRE_API"):
495
+ configs.append(f'{prefix}.wire_api="{values["WIRE_API"]}"')
496
+ if values.get("PROVIDER_NAME"):
497
+ configs.append(f'{prefix}.name="{values["PROVIDER_NAME"]}"')
498
+ if configs:
499
+ overrides["codex_config"] = configs
500
+ return overrides
501
+
502
+
503
+ def _write_runtime_env_file(workspace: Path, agent_id: str, exports: dict[str, str], unsets: list[str]) -> Path:
504
+ directory = workspace / ".team" / "runtime" / "provider-env"
505
+ directory.mkdir(parents=True, exist_ok=True)
506
+ path = directory / f"{agent_id}.env"
507
+ lines = [f"unset {key}" for key in sorted(unsets)]
508
+ lines.extend(f"export {key}={shlex.quote(value)}" for key, value in sorted(exports.items()))
509
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
510
+ try:
511
+ path.chmod(0o600)
512
+ except OSError:
513
+ pass
514
+ return path
515
+
516
+
517
+ def _compatible_claude_config_dir(workspace: Path, agent_id: str) -> Path:
518
+ directory = workspace / ".team" / "runtime" / "provider-config" / agent_id / "claude"
519
+ directory.mkdir(parents=True, exist_ok=True)
520
+ _ensure_compatible_claude_config(directory, workspace)
521
+ return directory
522
+
523
+
524
+ def ensure_compatible_claude_mcp_config(workspace: Path, agent_id: str, mcp_config: dict[str, Any]) -> None:
525
+ if not mcp_config:
526
+ return
527
+ directory = _compatible_claude_config_dir(workspace, agent_id)
528
+ state_path = directory / ".claude.json"
529
+ state = _read_json_object(state_path)
530
+ projects = state.setdefault("projects", {})
531
+ if not isinstance(projects, dict):
532
+ projects = {}
533
+ state["projects"] = projects
534
+ for project_key in _claude_project_keys(workspace):
535
+ project = projects.setdefault(project_key, {})
536
+ if not isinstance(project, dict):
537
+ project = {}
538
+ projects[project_key] = project
539
+ project["hasTrustDialogAccepted"] = True
540
+ project.setdefault("projectOnboardingSeenCount", 1)
541
+ project.setdefault("allowedTools", [])
542
+ project.setdefault("mcpContextUris", [])
543
+ project.setdefault("enabledMcpjsonServers", [])
544
+ project.setdefault("disabledMcpjsonServers", [])
545
+ project.setdefault("hasClaudeMdExternalIncludesApproved", False)
546
+ project.setdefault("hasClaudeMdExternalIncludesWarningShown", False)
547
+ servers = project.setdefault("mcpServers", {})
548
+ if not isinstance(servers, dict):
549
+ servers = {}
550
+ project["mcpServers"] = servers
551
+ servers.update(mcp_config)
552
+ _write_json(state_path, state)
553
+
554
+
555
+ def _ensure_compatible_claude_config(directory: Path, workspace: Path) -> None:
556
+ settings_path = directory / "settings.json"
557
+ settings = _read_json_object(settings_path)
558
+ settings.setdefault("theme", "auto")
559
+ settings.setdefault("skipDangerousModePermissionPrompt", True)
560
+ _write_json(settings_path, settings)
561
+
562
+ state_path = directory / ".claude.json"
563
+ state = _read_json_object(state_path)
564
+ state["hasCompletedOnboarding"] = True
565
+ state.setdefault("lastOnboardingVersion", "2.1.0")
566
+ state.setdefault("firstStartTime", "1970-01-01T00:00:00.000Z")
567
+ state.setdefault("numStartups", 0)
568
+ projects = state.get("projects")
569
+ if not isinstance(projects, dict):
570
+ projects = {}
571
+ state["projects"] = projects
572
+ for project_key in _claude_project_keys(workspace):
573
+ project = projects.get(project_key)
574
+ if not isinstance(project, dict):
575
+ project = {}
576
+ projects[project_key] = project
577
+ project["hasTrustDialogAccepted"] = True
578
+ project.setdefault("projectOnboardingSeenCount", 1)
579
+ _write_json(state_path, state)
580
+
581
+
582
+ def _claude_project_keys(workspace: Path) -> list[str]:
583
+ keys = [str(workspace)]
584
+ try:
585
+ resolved = str(workspace.resolve())
586
+ except OSError:
587
+ resolved = None
588
+ if resolved and resolved not in keys:
589
+ keys.append(resolved)
590
+ return keys
591
+
592
+
593
+ def _read_json_object(path: Path) -> dict[str, Any]:
594
+ try:
595
+ data = json.loads(path.read_text(encoding="utf-8"))
596
+ except (OSError, json.JSONDecodeError):
597
+ return {}
598
+ return data if isinstance(data, dict) else {}
599
+
600
+
601
+ def _write_json(path: Path, data: dict[str, Any]) -> None:
602
+ path.parent.mkdir(parents=True, exist_ok=True)
603
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
604
+ try:
605
+ path.chmod(0o600)
606
+ except OSError:
607
+ pass
608
+
609
+
610
+ def _anthropic_compatible_smoke(
611
+ values: dict[str, str],
612
+ model: str | None,
613
+ base_result: dict[str, Any],
614
+ timeout: float,
615
+ ) -> dict[str, Any]:
616
+ base_url = values.get("ANTHROPIC_BASE_URL") or values.get("BASE_URL")
617
+ api_key = values.get("ANTHROPIC_API_KEY") or values.get("API_KEY")
618
+ auth_token = values.get("ANTHROPIC_AUTH_TOKEN") or values.get("AUTH_TOKEN")
619
+ if not base_url or not (api_key or auth_token) or not model:
620
+ return {**base_result, "ok": False, "status": "smoke_failed", "reason": "missing_base_url_api_key_or_model"}
621
+ endpoint = _anthropic_messages_url(base_url)
622
+ payload = {
623
+ "model": model,
624
+ "max_tokens": 1,
625
+ "messages": [{"role": "user", "content": "ping"}],
626
+ }
627
+ headers = {
628
+ "content-type": "application/json",
629
+ "anthropic-version": values.get("ANTHROPIC_VERSION") or "2023-06-01",
630
+ }
631
+ headers["authorization"] = f"Bearer {auth_token or api_key}"
632
+ return _http_json_smoke(endpoint, payload, headers, values, base_result, timeout)
633
+
634
+
635
+ def _openai_compatible_smoke(
636
+ values: dict[str, str],
637
+ model: str | None,
638
+ base_result: dict[str, Any],
639
+ timeout: float,
640
+ ) -> dict[str, Any]:
641
+ base_url = values.get("OPENAI_BASE_URL") or values.get("BASE_URL")
642
+ api_key = values.get("OPENAI_API_KEY") or values.get("API_KEY")
643
+ if not base_url or not api_key or not model:
644
+ return {**base_result, "ok": False, "status": "smoke_failed", "reason": "missing_base_url_api_key_or_model"}
645
+ endpoint = _openai_chat_url(base_url)
646
+ payload = {
647
+ "model": model,
648
+ "max_tokens": 1,
649
+ "messages": [{"role": "user", "content": "ping"}],
650
+ }
651
+ headers = {"content-type": "application/json", "authorization": f"Bearer {api_key}"}
652
+ return _http_json_smoke(endpoint, payload, headers, values, base_result, timeout)
653
+
654
+
655
+ def _http_json_smoke(
656
+ endpoint: str,
657
+ payload: dict[str, Any],
658
+ headers: dict[str, str],
659
+ values: dict[str, str],
660
+ base_result: dict[str, Any],
661
+ timeout: float,
662
+ ) -> dict[str, Any]:
663
+ proxy_info = _proxy_info_for_endpoint(endpoint, values)
664
+ request = urllib.request.Request(
665
+ endpoint,
666
+ data=json.dumps(payload).encode("utf-8"),
667
+ headers=headers,
668
+ method="POST",
669
+ )
670
+ try:
671
+ with _temporary_profile_network_env(values):
672
+ response_ctx = urllib.request.urlopen(request, timeout=timeout)
673
+ with response_ctx as response:
674
+ status = int(getattr(response, "status", 200))
675
+ body = response.read(1024).decode("utf-8", errors="replace")
676
+ except urllib.error.HTTPError as exc:
677
+ body = exc.read(4096).decode("utf-8", errors="replace")
678
+ return {
679
+ **base_result,
680
+ **proxy_info,
681
+ "ok": False,
682
+ "status": "smoke_failed",
683
+ "reason": "http_error",
684
+ "http_status": exc.code,
685
+ "endpoint": _redacted_endpoint(endpoint),
686
+ "error": redact_text(body or str(exc)).get("text"),
687
+ }
688
+ except Exception as exc:
689
+ reason = "proxy_connectivity_failed" if proxy_info.get("proxy_configured") else "request_failed"
690
+ return {
691
+ **base_result,
692
+ **proxy_info,
693
+ "ok": False,
694
+ "status": "smoke_failed",
695
+ "reason": reason,
696
+ "endpoint": _redacted_endpoint(endpoint),
697
+ "error": redact_text(str(exc)).get("text"),
698
+ "suggestion": (
699
+ "Proxy is configured for this request; allow the profile BASE_URL through the proxy or disable the proxy for Team Agent startup."
700
+ if proxy_info.get("proxy_configured")
701
+ else "Check BASE_URL network connectivity from this machine."
702
+ ),
703
+ }
704
+ if 200 <= status < 300:
705
+ return {
706
+ **base_result,
707
+ **proxy_info,
708
+ "ok": True,
709
+ "status": "smoke_passed",
710
+ "http_status": status,
711
+ "endpoint": _redacted_endpoint(endpoint),
712
+ }
713
+ return {
714
+ **base_result,
715
+ **proxy_info,
716
+ "ok": False,
717
+ "status": "smoke_failed",
718
+ "reason": "unexpected_status",
719
+ "http_status": status,
720
+ "endpoint": _redacted_endpoint(endpoint),
721
+ "error": redact_text(body).get("text"),
722
+ }
723
+
724
+
725
+ def _anthropic_messages_url(base_url: str) -> str:
726
+ base = base_url.rstrip("/")
727
+ if base.endswith("/messages"):
728
+ return base
729
+ if base.endswith("/v1"):
730
+ return f"{base}/messages"
731
+ return f"{base}/v1/messages"
732
+
733
+
734
+ def _openai_chat_url(base_url: str) -> str:
735
+ base = base_url.rstrip("/")
736
+ if base.endswith("/chat/completions"):
737
+ return base
738
+ if base.endswith("/v1"):
739
+ return f"{base}/chat/completions"
740
+ return f"{base}/v1/chat/completions"
741
+
742
+
743
+ def _redacted_endpoint(endpoint: str) -> str:
744
+ return endpoint.split("?", 1)[0]
745
+
746
+
747
+ def _proxy_info_for_endpoint(endpoint: str, values: dict[str, str]) -> dict[str, Any]:
748
+ parsed = urllib.parse.urlparse(endpoint)
749
+ if _profile_proxy_mode(values) == "direct":
750
+ return {"proxy_configured": False, "proxy_mode": "direct"}
751
+ profile_env = _compatible_api_network_exports("compatible_api", values)
752
+ proxy_url = _proxy_url_from_env(parsed.scheme, profile_env)
753
+ if proxy_url:
754
+ return {
755
+ "proxy_configured": True,
756
+ "proxy_scheme": parsed.scheme,
757
+ "proxy_url": _redact_proxy_url(proxy_url),
758
+ "proxy_source": "profile",
759
+ }
760
+ ambient_proxy_url = _proxy_url_from_env(parsed.scheme, os.environ)
761
+ if ambient_proxy_url:
762
+ return {
763
+ "proxy_configured": True,
764
+ "proxy_scheme": parsed.scheme,
765
+ "proxy_url": _redact_proxy_url(ambient_proxy_url),
766
+ "proxy_source": "ambient",
767
+ }
768
+ return {"proxy_configured": False}
769
+
770
+
771
+ def _proxy_url_from_env(scheme: str, env: Any) -> str | None:
772
+ upper = f"{scheme.upper()}_PROXY"
773
+ lower = f"{scheme.lower()}_proxy"
774
+ return env.get(upper) or env.get(lower) or env.get("ALL_PROXY") or env.get("all_proxy")
775
+
776
+
777
+ def _compatible_api_network_exports(auth_mode: str, values: dict[str, str]) -> dict[str, str]:
778
+ if auth_mode != "compatible_api" or _profile_proxy_mode(values) == "direct":
779
+ return {}
780
+ return {key: values[key] for key in COMPATIBLE_API_NETWORK_ENV_KEYS if values.get(key)}
781
+
782
+
783
+ @contextmanager
784
+ def _temporary_profile_network_env(values: dict[str, str]) -> Any:
785
+ profile_env = _compatible_api_network_exports("compatible_api", values)
786
+ direct = _profile_proxy_mode(values) == "direct"
787
+ touched_keys = COMPATIBLE_API_NETWORK_ENV_KEYS if direct else tuple(profile_env)
788
+ saved = {key: os.environ.get(key) for key in touched_keys}
789
+ try:
790
+ if direct:
791
+ for key in COMPATIBLE_API_NETWORK_ENV_KEYS:
792
+ os.environ.pop(key, None)
793
+ os.environ.update(profile_env)
794
+ yield
795
+ finally:
796
+ for key, value in saved.items():
797
+ if value is None:
798
+ os.environ.pop(key, None)
799
+ else:
800
+ os.environ[key] = value
801
+
802
+
803
+ def _profile_proxy_mode(values: dict[str, str]) -> str:
804
+ return str(values.get("PROXY_MODE") or values.get("NETWORK_MODE") or "inherit").strip().lower()
805
+
806
+
807
+ def _redact_proxy_url(proxy_url: str) -> str:
808
+ parsed = urllib.parse.urlparse(proxy_url)
809
+ if not parsed.netloc:
810
+ return proxy_url
811
+ host = parsed.hostname or ""
812
+ port = f":{parsed.port}" if parsed.port else ""
813
+ auth = "[redacted]@" if parsed.username or parsed.password else ""
814
+ return urllib.parse.urlunparse((parsed.scheme, f"{auth}{host}{port}", parsed.path, "", "", ""))
815
+
816
+
817
+ def _format_profile_check_failure(check: dict[str, Any]) -> str:
818
+ agent_id = check.get("agent_id") or "unknown"
819
+ profile = check.get("profile") or "-"
820
+ reason = check.get("reason") or "profile_invalid"
821
+ suggestion = check.get("suggestion") or f"Inspect safely with `team-agent profile show {profile} --workspace . --json`."
822
+ return f"profile validation failed for {agent_id} profile {profile}: {reason}. {suggestion}"
823
+
824
+
825
+ def _alternate_value(values: dict[str, str], key: str) -> str | None:
826
+ alternates = {
827
+ "BASE_URL": ["ANTHROPIC_BASE_URL", "OPENAI_BASE_URL"],
828
+ "API_KEY": ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "AUTH_TOKEN", "OPENAI_API_KEY", "GEMINI_API_KEY"],
829
+ }
830
+ for candidate in alternates.get(key, []):
831
+ if values.get(candidate):
832
+ return values[candidate]
833
+ return None
834
+
835
+
836
+ def _strip_env_value(value: str) -> str:
837
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
838
+ return value[1:-1]
839
+ return value
840
+
841
+
842
+ def _safe_codex_provider_id(value: str) -> bool:
843
+ return re.fullmatch(r"[A-Za-z0-9_-]+", value) is not None
844
+
845
+
846
+ def _is_secret_key(key: str) -> bool:
847
+ upper = key.upper()
848
+ return upper in SECRET_KEYS or "KEY" in upper or "TOKEN" in upper or "SECRET" in upper
849
+
850
+
851
+ def _safe_profile_value(key: str, value: str) -> dict[str, Any]:
852
+ if _is_secret_key(key):
853
+ return {"present": bool(value), "redacted": True}
854
+ return {"present": bool(value), "redacted": False, "value": _safe_plain_profile_value(value)}
855
+
856
+
857
+ def _common_missing_values(auth_mode: str | None, values: dict[str, str]) -> list[str]:
858
+ if auth_mode == "compatible_api":
859
+ required = ["BASE_URL", "API_KEY", "MODEL"]
860
+ elif auth_mode == "official_api":
861
+ required = ["API_KEY"]
862
+ else:
863
+ required = []
864
+ missing = []
865
+ for key in required:
866
+ if key == "MODEL":
867
+ if not (values.get("MODEL") or values.get("ANTHROPIC_MODEL")):
868
+ missing.append(key)
869
+ continue
870
+ if not values.get(key) and not _alternate_value(values, key):
871
+ missing.append(key)
872
+ return missing
873
+
874
+
875
+ def _safe_plain_profile_value(value: str) -> str:
876
+ parsed = urllib.parse.urlparse(value)
877
+ if parsed.scheme and parsed.netloc:
878
+ host = parsed.hostname or ""
879
+ port = f":{parsed.port}" if parsed.port else ""
880
+ auth = "[redacted]@" if parsed.username or parsed.password else ""
881
+ value = urllib.parse.urlunparse((parsed.scheme, f"{auth}{host}{port}", parsed.path, "", "", ""))
882
+ return str(redact_text(value).get("text") or "")