@team-agent/installer 0.1.11 → 0.2.1

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 (113) hide show
  1. package/crates/team-agent-core/src/lib.rs +50 -5
  2. package/package.json +1 -1
  3. package/schemas/team.schema.json +1 -0
  4. package/src/team_agent/approvals/__init__.py +65 -0
  5. package/src/team_agent/approvals/constants.py +6 -0
  6. package/src/team_agent/approvals/parsing.py +176 -0
  7. package/src/team_agent/approvals/runtime_prompts.py +171 -0
  8. package/src/team_agent/approvals/status.py +165 -0
  9. package/src/team_agent/cli/__init__.py +137 -0
  10. package/src/team_agent/cli/commands.py +339 -0
  11. package/src/team_agent/cli/e2e.py +202 -0
  12. package/src/team_agent/cli/helpers.py +137 -0
  13. package/src/team_agent/cli/parser.py +477 -0
  14. package/src/team_agent/compiler.py +98 -33
  15. package/src/team_agent/coordinator/__init__.py +53 -0
  16. package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
  17. package/src/team_agent/coordinator/lifecycle.py +334 -0
  18. package/src/team_agent/coordinator/metadata.py +61 -0
  19. package/src/team_agent/coordinator/paths.py +17 -0
  20. package/src/team_agent/diagnose/__init__.py +48 -0
  21. package/src/team_agent/diagnose/checks.py +101 -0
  22. package/src/team_agent/diagnose/health.py +241 -0
  23. package/src/team_agent/diagnose/preflight.py +194 -0
  24. package/src/team_agent/diagnose/quick_start.py +233 -0
  25. package/src/team_agent/display/__init__.py +61 -0
  26. package/src/team_agent/display/close.py +147 -0
  27. package/src/team_agent/display/ghostty.py +77 -0
  28. package/src/team_agent/display/worker_window.py +110 -0
  29. package/src/team_agent/display/workspace.py +473 -0
  30. package/src/team_agent/launch/__init__.py +41 -0
  31. package/src/team_agent/launch/bootstrap.py +85 -0
  32. package/src/team_agent/launch/config.py +106 -0
  33. package/src/team_agent/launch/core.py +291 -0
  34. package/src/team_agent/launch/requirements.py +57 -0
  35. package/src/team_agent/leader/__init__.py +320 -0
  36. package/src/team_agent/lifecycle/__init__.py +5 -0
  37. package/src/team_agent/lifecycle/agents.py +226 -0
  38. package/src/team_agent/lifecycle/operations.py +321 -0
  39. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +39 -0
  40. package/src/team_agent/lifecycle/start.py +363 -0
  41. package/src/team_agent/mcp_server/__init__.py +42 -0
  42. package/src/team_agent/mcp_server/__main__.py +7 -0
  43. package/src/team_agent/mcp_server/contracts.py +148 -0
  44. package/src/team_agent/mcp_server/normalize.py +257 -0
  45. package/src/team_agent/mcp_server/server.py +150 -0
  46. package/src/team_agent/mcp_server/tools.py +205 -0
  47. package/src/team_agent/message_store/__init__.py +23 -0
  48. package/src/team_agent/message_store/agent_health.py +109 -0
  49. package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
  50. package/src/team_agent/message_store/result_watchers.py +102 -0
  51. package/src/team_agent/message_store/schema.py +266 -0
  52. package/src/team_agent/messaging/__init__.py +1 -0
  53. package/src/team_agent/messaging/activity_detector.py +190 -0
  54. package/src/team_agent/messaging/delivery.py +138 -0
  55. package/src/team_agent/messaging/deps.py +263 -0
  56. package/src/team_agent/messaging/idle_alerts.py +323 -0
  57. package/src/team_agent/messaging/internal_delivery.py +46 -0
  58. package/src/team_agent/messaging/leader.py +317 -0
  59. package/src/team_agent/messaging/leader_panes.py +343 -0
  60. package/src/team_agent/messaging/owner_bypass.py +29 -0
  61. package/src/team_agent/messaging/result_delivery.py +300 -0
  62. package/src/team_agent/messaging/results.py +456 -0
  63. package/src/team_agent/messaging/scheduler.py +428 -0
  64. package/src/team_agent/messaging/send.py +500 -0
  65. package/src/team_agent/messaging/session_drift.py +94 -0
  66. package/src/team_agent/messaging/tmux_io.py +337 -0
  67. package/src/team_agent/messaging/tmux_prompt.py +229 -0
  68. package/src/team_agent/orchestrator/__init__.py +376 -0
  69. package/src/team_agent/orchestrator/plan.py +122 -0
  70. package/src/team_agent/orchestrator/state.py +128 -0
  71. package/src/team_agent/profiles/__init__.py +82 -0
  72. package/src/team_agent/profiles/constants.py +19 -0
  73. package/src/team_agent/profiles/core.py +407 -0
  74. package/src/team_agent/profiles/helpers.py +69 -0
  75. package/src/team_agent/profiles/provider_env.py +188 -0
  76. package/src/team_agent/profiles/smoke.py +201 -0
  77. package/src/team_agent/provider_cli/__init__.py +43 -0
  78. package/src/team_agent/provider_cli/adapter.py +167 -0
  79. package/src/team_agent/provider_cli/base.py +48 -0
  80. package/src/team_agent/provider_cli/claude.py +457 -0
  81. package/src/team_agent/provider_cli/codex.py +319 -0
  82. package/src/team_agent/provider_cli/copilot.py +8 -0
  83. package/src/team_agent/provider_cli/fake.py +39 -0
  84. package/src/team_agent/provider_cli/gemini.py +95 -0
  85. package/src/team_agent/provider_cli/opencode.py +8 -0
  86. package/src/team_agent/provider_cli/prompt.py +62 -0
  87. package/src/team_agent/provider_cli/registry.py +18 -0
  88. package/src/team_agent/provider_cli/unsupported.py +32 -0
  89. package/src/team_agent/providers.py +67 -949
  90. package/src/team_agent/quality_gates.py +104 -0
  91. package/src/team_agent/restart/__init__.py +34 -0
  92. package/src/team_agent/restart/orchestration.py +328 -0
  93. package/src/team_agent/restart/selection.py +89 -0
  94. package/src/team_agent/restart/snapshot.py +70 -0
  95. package/src/team_agent/runtime.py +809 -5892
  96. package/src/team_agent/rust_core.py +22 -5
  97. package/src/team_agent/sessions/__init__.py +25 -0
  98. package/src/team_agent/sessions/capture.py +93 -0
  99. package/src/team_agent/sessions/inventory.py +44 -0
  100. package/src/team_agent/sessions/resume.py +135 -0
  101. package/src/team_agent/spec.py +3 -1
  102. package/src/team_agent/state.py +218 -4
  103. package/src/team_agent/status/__init__.py +63 -0
  104. package/src/team_agent/status/approvals.py +52 -0
  105. package/src/team_agent/status/compact.py +158 -0
  106. package/src/team_agent/status/constants.py +18 -0
  107. package/src/team_agent/status/inbox.py +28 -0
  108. package/src/team_agent/status/peek.py +117 -0
  109. package/src/team_agent/status/queries.py +168 -0
  110. package/src/team_agent/terminal.py +57 -0
  111. package/src/team_agent/cli.py +0 -858
  112. package/src/team_agent/mcp_server.py +0 -579
  113. package/src/team_agent/profiles.py +0 -882
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib
4
+
5
+ from team_agent.profiles.constants import *
6
+ from team_agent.profiles.core import (
7
+ _agent_profile_dir,
8
+ _profile_lookup_dir,
9
+ _safe_init_command,
10
+ _safe_inspection_command,
11
+ compact_profile_check,
12
+ doctor_profile,
13
+ effective_model,
14
+ ensure_profile_secret_boundary,
15
+ ensure_profile_secret_boundary_dir,
16
+ gitignore_patterns,
17
+ init_profile,
18
+ known_profiles,
19
+ load_profile,
20
+ parse_env_file,
21
+ prepare_agent_profile_launch,
22
+ profile_dir,
23
+ required_profile_keys,
24
+ show_profile,
25
+ smoke_check_agent_profile,
26
+ validate_agent_profile,
27
+ )
28
+ from team_agent.profiles.helpers import (
29
+ _alternate_value,
30
+ _common_missing_values,
31
+ _format_profile_check_failure,
32
+ _is_secret_key,
33
+ _safe_codex_provider_id,
34
+ _safe_plain_profile_value,
35
+ _safe_profile_value,
36
+ _strip_env_value,
37
+ )
38
+ from team_agent.profiles.provider_env import (
39
+ _claude_project_keys,
40
+ _compatible_api_network_exports,
41
+ _compatible_claude_config_dir,
42
+ _ensure_compatible_claude_config,
43
+ _profile_proxy_mode,
44
+ _provider_command_overrides,
45
+ _provider_env_exports,
46
+ _provider_env_unsets,
47
+ _read_json_object,
48
+ _write_json,
49
+ _write_runtime_env_file,
50
+ ensure_compatible_claude_mcp_config,
51
+ )
52
+ from team_agent.profiles.smoke import (
53
+ _anthropic_compatible_smoke,
54
+ _anthropic_messages_url,
55
+ _http_json_smoke,
56
+ _openai_chat_url,
57
+ _openai_compatible_smoke,
58
+ _proxy_info_for_endpoint,
59
+ _proxy_url_from_env,
60
+ _redact_proxy_url,
61
+ _redacted_endpoint,
62
+ _temporary_profile_network_env,
63
+ )
64
+
65
+ _REQUIRED_EXPORTS = (
66
+ "profile_dir",
67
+ "init_profile",
68
+ "doctor_profile",
69
+ "show_profile",
70
+ "load_profile",
71
+ "parse_env_file",
72
+ "validate_agent_profile",
73
+ "smoke_check_agent_profile",
74
+ "prepare_agent_profile_launch",
75
+ "effective_model",
76
+ "ensure_compatible_claude_mcp_config",
77
+ )
78
+ for _name in _REQUIRED_EXPORTS:
79
+ if _name not in globals():
80
+ raise ImportError(f"team_agent.profiles missing export: {_name}")
81
+
82
+ __all__ = [name for name in globals() if not name.startswith("__")]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+
6
+ AUTH_MODES = {"subscription", "official_api", "compatible_api"}
7
+ PROFILE_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
8
+ SECRET_KEYS = {"API_KEY", "AUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "OPENAI_API_KEY", "GEMINI_API_KEY"}
9
+ PROXY_ENV_KEYS = ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", "https_proxy", "http_proxy", "all_proxy", "NO_PROXY", "no_proxy")
10
+ CA_ENV_KEYS = ("NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE")
11
+ COMPATIBLE_API_NETWORK_ENV_KEYS = PROXY_ENV_KEYS + CA_ENV_KEYS
12
+ PROFILE_SECRET_BOUNDARY_TEXT = """# Team Agent Profile Secret Boundary
13
+
14
+ Do not read, print, grep, cat, sed, copy, summarize, or open raw `*.env` files in this directory.
15
+ These files may contain API keys or auth tokens and must stay out of agent context.
16
+
17
+ Use `team-agent profile show <name> --workspace . --json` or `team-agent profile doctor <name> --workspace . --json`
18
+ for redacted status. If a required value is missing, ask the human user to edit the local profile file.
19
+ """
@@ -0,0 +1,407 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from team_agent.profiles.constants import AUTH_MODES, PROFILE_KEY_RE, PROFILE_SECRET_BOUNDARY_TEXT
7
+ from team_agent.profiles.helpers import (
8
+ _alternate_value,
9
+ _common_missing_values,
10
+ _format_profile_check_failure,
11
+ _is_secret_key,
12
+ _safe_profile_value,
13
+ _strip_env_value,
14
+ )
15
+ from team_agent.rust_core import redact_text
16
+
17
+
18
+ def profile_dir(workspace: Path) -> Path:
19
+ return workspace / ".team" / "current" / "profiles"
20
+
21
+ def _profile_lookup_dir(workspace: Path, profiles_dir: Path | str | None = None) -> Path:
22
+ return Path(profiles_dir).resolve() if profiles_dir else profile_dir(workspace)
23
+
24
+ def _agent_profile_dir(agent: dict[str, Any]) -> Path | None:
25
+ value = agent.get("_profile_dir")
26
+ return Path(str(value)).resolve() if value else None
27
+
28
+ def _safe_inspection_command(name: str, profiles_dir: Path | str | None = None) -> str:
29
+ if profiles_dir:
30
+ return f"team-agent profile show {name} --team {Path(profiles_dir).resolve().parent} --json"
31
+ return f"team-agent profile show {name} --workspace . --json"
32
+
33
+ def _safe_init_command(name: str, auth_mode: str, profiles_dir: Path | str | None = None) -> str:
34
+ if profiles_dir:
35
+ return f"team-agent profile init {name} --auth-mode {auth_mode} --team {Path(profiles_dir).resolve().parent}"
36
+ return f"team-agent profile init {name} --auth-mode {auth_mode}"
37
+
38
+ def ensure_profile_secret_boundary_dir(directory: Path) -> Path:
39
+ directory.mkdir(parents=True, exist_ok=True)
40
+ first_path = directory / "AGENTS.md"
41
+ for filename in ("AGENTS.md", "CLAUDE.md"):
42
+ path = directory / filename
43
+ if not path.exists():
44
+ path.write_text(PROFILE_SECRET_BOUNDARY_TEXT, encoding="utf-8")
45
+ return first_path
46
+
47
+ def ensure_profile_secret_boundary(workspace: Path) -> Path:
48
+ directory = profile_dir(workspace)
49
+ return ensure_profile_secret_boundary_dir(directory)
50
+
51
+ def init_profile(workspace: Path, name: str, auth_mode: str, profiles_dir: Path | str | None = None) -> dict[str, Any]:
52
+ if auth_mode not in AUTH_MODES:
53
+ raise ValueError(f"unknown auth_mode: {auth_mode}")
54
+ directory = _profile_lookup_dir(workspace, profiles_dir)
55
+ directory.mkdir(parents=True, exist_ok=True)
56
+ ensure_profile_secret_boundary_dir(directory)
57
+ path = directory / f"{name}.env"
58
+ template_path = directory / f"{name}.example.env"
59
+ if auth_mode == "subscription":
60
+ body = f"AUTH_MODE=subscription\nPROFILE_NAME={name}\n"
61
+ elif auth_mode == "official_api":
62
+ body = f"AUTH_MODE=official_api\nPROFILE_NAME={name}\nAPI_KEY=\nMODEL=\n"
63
+ else:
64
+ body = f"AUTH_MODE=compatible_api\nPROFILE_NAME={name}\nBASE_URL=\nAPI_KEY=\nMODEL=\n"
65
+ created_profile = False
66
+ created_template = False
67
+ if not path.exists():
68
+ path.write_text(body, encoding="utf-8")
69
+ try:
70
+ path.chmod(0o600)
71
+ except OSError:
72
+ pass
73
+ created_profile = True
74
+ if not template_path.exists():
75
+ template_path.write_text(body, encoding="utf-8")
76
+ created_template = True
77
+ safe_inspection = _safe_inspection_command(name, directory if profiles_dir else None)
78
+ return {
79
+ "ok": True,
80
+ "profile": name,
81
+ "auth_mode": auth_mode,
82
+ "path": str(path),
83
+ "template_path": str(template_path),
84
+ "created_profile": created_profile,
85
+ "created_template": created_template,
86
+ "secret_written": False,
87
+ "safe_inspection_command": safe_inspection,
88
+ "raw_file_read_allowed_for_agents": False,
89
+ "instruction": (
90
+ f"Fill {path} locally; do not paste API keys into chat or role docs. "
91
+ f"Agents must inspect this profile only with `{safe_inspection}`."
92
+ ),
93
+ }
94
+
95
+ def doctor_profile(workspace: Path, name: str, profiles_dir: Path | str | None = None) -> dict[str, Any]:
96
+ directory = _profile_lookup_dir(workspace, profiles_dir)
97
+ loaded = load_profile(workspace, name, directory)
98
+ real_path = directory / f"{name}.env"
99
+ example_path = directory / f"{name}.example.env"
100
+ exists = bool(loaded.get("exists"))
101
+ values = loaded.get("values", {})
102
+ keys_present = [key for key, value in values.items() if value]
103
+ auth_mode = values.get("AUTH_MODE")
104
+ return {
105
+ "ok": exists,
106
+ "profile": name,
107
+ "path": str(loaded.get("path") or real_path),
108
+ "template_path": str(example_path),
109
+ "credential_present": real_path.exists(),
110
+ "auth_mode": auth_mode,
111
+ "keys_present": sorted(keys_present),
112
+ "secret_keys_present": sorted(key for key in keys_present if _is_secret_key(key)),
113
+ "redaction_engine": redact_text(" ".join(f"{key}=present" for key in keys_present)).get("engine"),
114
+ "secret_values_printed": False,
115
+ "safe_for_agent_context": True,
116
+ "raw_file_read_allowed_for_agents": False,
117
+ "safe_inspection_command": _safe_inspection_command(name, directory if profiles_dir else None),
118
+ "suggestion": None if exists else f"Run {_safe_init_command(name, 'subscription', directory if profiles_dir else None)}.",
119
+ }
120
+
121
+ def show_profile(workspace: Path, name: str, profiles_dir: Path | str | None = None) -> dict[str, Any]:
122
+ loaded = load_profile(workspace, name, profiles_dir)
123
+ values: dict[str, str] = loaded.get("values", {})
124
+ auth_mode = values.get("AUTH_MODE")
125
+ safe_values = {
126
+ key: _safe_profile_value(key, value)
127
+ for key, value in sorted(values.items())
128
+ }
129
+ missing = _common_missing_values(auth_mode, values)
130
+ return {
131
+ "ok": bool(loaded.get("exists")),
132
+ "profile": name,
133
+ "credential_present": bool(loaded.get("credential_present")),
134
+ "auth_mode": auth_mode,
135
+ "values": safe_values,
136
+ "keys_present": sorted(key for key, value in values.items() if value),
137
+ "secret_keys_present": sorted(key for key, value in values.items() if value and _is_secret_key(key)),
138
+ "missing_common": missing,
139
+ "safe_for_agent_context": True,
140
+ "secret_values_printed": False,
141
+ "raw_file_read_allowed_for_agents": False,
142
+ "instruction": (
143
+ "Use this redacted output for diagnostics. Do not read .team/*/profiles/*.env "
144
+ "or .team/runtime/provider-env/*.env into agent context."
145
+ ),
146
+ }
147
+
148
+ def known_profiles(team_dir: Path) -> set[str]:
149
+ directory = team_dir / "profiles"
150
+ if not directory.exists():
151
+ return set()
152
+ names = set()
153
+ for path in directory.glob("*.env"):
154
+ names.add(path.name.removesuffix(".env").removesuffix(".example"))
155
+ for path in directory.glob("*.example.env"):
156
+ names.add(path.name.removesuffix(".example.env"))
157
+ return names
158
+
159
+ def gitignore_patterns() -> list[str]:
160
+ return [".team/*/profiles/*.env", "!.team/*/profiles/*.example.env", ".team/runtime/provider-env/*.env"]
161
+
162
+ def load_profile(workspace: Path, name: str, profiles_dir: Path | str | None = None) -> dict[str, Any]:
163
+ directory = _profile_lookup_dir(workspace, profiles_dir)
164
+ real_path = directory / f"{name}.env"
165
+ example_path = directory / f"{name}.example.env"
166
+ path = real_path if real_path.exists() else example_path
167
+ if not path.exists():
168
+ return {
169
+ "exists": False,
170
+ "profile": name,
171
+ "path": str(real_path),
172
+ "template_path": str(example_path),
173
+ "credential_present": False,
174
+ "values": {},
175
+ }
176
+ values = parse_env_file(path)
177
+ return {
178
+ "exists": True,
179
+ "profile": name,
180
+ "path": str(path),
181
+ "template_path": str(example_path),
182
+ "credential_present": real_path.exists(),
183
+ "values": values,
184
+ }
185
+
186
+ def parse_env_file(path: Path) -> dict[str, str]:
187
+ values: dict[str, str] = {}
188
+ for raw in path.read_text(encoding="utf-8").splitlines():
189
+ line = raw.strip()
190
+ if not line or line.startswith("#"):
191
+ continue
192
+ if line.startswith("export "):
193
+ line = line[len("export ") :].strip()
194
+ if "=" not in line:
195
+ continue
196
+ key, value = line.split("=", 1)
197
+ key = key.strip()
198
+ if not PROFILE_KEY_RE.fullmatch(key):
199
+ continue
200
+ values[key] = _strip_env_value(value.strip())
201
+ return values
202
+
203
+ def validate_agent_profile(workspace: Path, agent: dict[str, Any]) -> dict[str, Any]:
204
+ profile = agent.get("profile")
205
+ auth_mode = str(agent.get("auth_mode") or "subscription")
206
+ profiles_dir = _agent_profile_dir(agent)
207
+ result = {
208
+ "ok": True,
209
+ "agent_id": agent.get("id"),
210
+ "provider": agent.get("provider"),
211
+ "profile": profile,
212
+ "auth_mode": auth_mode,
213
+ "path": None,
214
+ "credential_present": False,
215
+ "keys_present": [],
216
+ "missing_required": [],
217
+ "secret_values_printed": False,
218
+ }
219
+ if not profile:
220
+ return result
221
+ loaded = load_profile(workspace, str(profile), profiles_dir)
222
+ result["path"] = loaded.get("path")
223
+ result["credential_present"] = bool(loaded.get("credential_present"))
224
+ if not loaded.get("exists"):
225
+ result["ok"] = False
226
+ result["reason"] = "profile_missing"
227
+ result["suggestion"] = (
228
+ f"Run {_safe_init_command(str(profile), auth_mode, profiles_dir)}, then ask the human user "
229
+ "to fill the generated local profile file."
230
+ )
231
+ return result
232
+ values: dict[str, str] = loaded.get("values", {})
233
+ file_auth_mode = values.get("AUTH_MODE")
234
+ if file_auth_mode and file_auth_mode != auth_mode:
235
+ result["ok"] = False
236
+ result["reason"] = "auth_mode_mismatch"
237
+ result["file_auth_mode"] = file_auth_mode
238
+ result["suggestion"] = (
239
+ f"Set AUTH_MODE={auth_mode} in the local profile or update the role doc. "
240
+ f"Inspect safely with `{_safe_inspection_command(str(profile), profiles_dir)}`."
241
+ )
242
+ return result
243
+ keys_present = sorted(key for key, value in values.items() if value)
244
+ result["keys_present"] = keys_present
245
+ role_model = str(agent.get("model") or "") or None
246
+ profile_model = values.get("MODEL") or values.get("ANTHROPIC_MODEL")
247
+ effective = role_model or profile_model
248
+ result["effective_model"] = effective
249
+ result["model_source"] = "role" if role_model else ("profile" if profile_model else None)
250
+ if auth_mode == "compatible_api" and role_model and profile_model and role_model != profile_model:
251
+ result["ok"] = False
252
+ result["reason"] = "model_mismatch"
253
+ result["suggestion"] = (
254
+ f"Role model {role_model!r} does not match the profile MODEL value; "
255
+ f"inspect safely with `{_safe_inspection_command(str(profile), profiles_dir)}`, "
256
+ "then remove model from the role doc or make both values identical."
257
+ )
258
+ return result
259
+ required = required_profile_keys(str(agent.get("provider") or ""), auth_mode)
260
+ missing = [key for key in required if not values.get(key) and not _alternate_value(values, key)]
261
+ if auth_mode == "compatible_api" and not effective:
262
+ missing.append("MODEL")
263
+ result["missing_required"] = missing
264
+ if missing:
265
+ result["ok"] = False
266
+ result["reason"] = "profile_required_values_missing"
267
+ result["suggestion"] = (
268
+ f"Ask the human user to fill {', '.join(missing)} in local profile {profile}; "
269
+ f"inspect safely with `{_safe_inspection_command(str(profile), profiles_dir)}`. "
270
+ "Do not paste API keys into chat."
271
+ )
272
+ return result
273
+
274
+ def smoke_check_agent_profile(workspace: Path, agent: dict[str, Any], timeout: float = 8.0) -> dict[str, Any]:
275
+ auth_mode = str(agent.get("auth_mode") or "subscription")
276
+ profile = agent.get("profile")
277
+ result = {
278
+ "ok": True,
279
+ "agent_id": agent.get("id"),
280
+ "provider": agent.get("provider"),
281
+ "profile": profile,
282
+ "auth_mode": auth_mode,
283
+ "status": "not_required",
284
+ "secret_values_printed": False,
285
+ }
286
+ if auth_mode != "compatible_api" or not profile:
287
+ return result
288
+ loaded = load_profile(workspace, str(profile), _agent_profile_dir(agent))
289
+ values: dict[str, str] = loaded.get("values", {})
290
+ if str(values.get("PROFILE_SMOKE", "true")).lower() in {"0", "false", "no", "off"}:
291
+ result["status"] = "skipped_by_profile"
292
+ return result
293
+ validation = validate_agent_profile(workspace, agent)
294
+ if not validation.get("ok"):
295
+ return {**result, **validation, "ok": False, "status": "profile_invalid"}
296
+ provider = str(agent.get("provider") or "")
297
+ model = effective_model(agent, workspace)
298
+ from team_agent.profiles.smoke import _anthropic_compatible_smoke, _openai_compatible_smoke
299
+
300
+ if provider in {"claude", "claude_code"}:
301
+ return _anthropic_compatible_smoke(values, model, result, timeout)
302
+ if provider == "codex":
303
+ return _openai_compatible_smoke(values, model, result, timeout)
304
+ result["status"] = "unsupported_provider_smoke_skipped"
305
+ return result
306
+
307
+ def required_profile_keys(provider: str, auth_mode: str) -> list[str]:
308
+ if auth_mode == "subscription":
309
+ return []
310
+ if provider in {"claude", "claude_code"}:
311
+ if auth_mode == "official_api":
312
+ return ["API_KEY"]
313
+ if auth_mode == "compatible_api":
314
+ return ["BASE_URL", "API_KEY"]
315
+ if provider == "codex":
316
+ if auth_mode == "official_api":
317
+ return ["API_KEY"]
318
+ if auth_mode == "compatible_api":
319
+ return ["BASE_URL", "API_KEY"]
320
+ if provider == "gemini_cli" and auth_mode in {"official_api", "compatible_api"}:
321
+ return ["API_KEY"]
322
+ return []
323
+
324
+ def compact_profile_check(check: dict[str, Any]) -> dict[str, Any]:
325
+ keys = [
326
+ "agent_id",
327
+ "provider",
328
+ "profile",
329
+ "auth_mode",
330
+ "ok",
331
+ "status",
332
+ "reason",
333
+ "credential_present",
334
+ "keys_present",
335
+ "missing_required",
336
+ "effective_model",
337
+ "model_source",
338
+ "suggestion",
339
+ "http_status",
340
+ "endpoint",
341
+ "error",
342
+ "proxy_configured",
343
+ "proxy_scheme",
344
+ "proxy_url",
345
+ "proxy_source",
346
+ "proxy_mode",
347
+ ]
348
+ return {key: check.get(key) for key in keys if key in check}
349
+
350
+ def prepare_agent_profile_launch(workspace: Path, agent: dict[str, Any]) -> dict[str, Any] | None:
351
+ profile = agent.get("profile")
352
+ if not profile:
353
+ return None
354
+ check = validate_agent_profile(workspace, agent)
355
+ if not check.get("ok"):
356
+ raise ValueError(_format_profile_check_failure(check))
357
+ loaded = load_profile(workspace, str(profile), _agent_profile_dir(agent))
358
+ values: dict[str, str] = loaded.get("values", {})
359
+ auth_mode = str(agent.get("auth_mode") or values.get("AUTH_MODE") or "subscription")
360
+ provider = str(agent.get("provider") or "")
361
+ from team_agent.profiles.provider_env import (
362
+ _compatible_api_network_exports,
363
+ _compatible_claude_config_dir,
364
+ _profile_proxy_mode,
365
+ _provider_command_overrides,
366
+ _provider_env_exports,
367
+ _provider_env_unsets,
368
+ _write_runtime_env_file,
369
+ )
370
+ from team_agent.profiles.constants import COMPATIBLE_API_NETWORK_ENV_KEYS
371
+
372
+ exports = _provider_env_exports(provider, auth_mode, values)
373
+ claude_projects_root = None
374
+ if provider in {"claude", "claude_code"} and auth_mode == "compatible_api":
375
+ claude_config_dir = _compatible_claude_config_dir(workspace, str(agent["id"]))
376
+ exports["CLAUDE_CONFIG_DIR"] = str(claude_config_dir)
377
+ claude_projects_root = str(claude_config_dir / "projects")
378
+ exports.update(_compatible_api_network_exports(auth_mode, values))
379
+ unsets = _provider_env_unsets(provider, auth_mode)
380
+ if auth_mode == "compatible_api" and _profile_proxy_mode(values) == "direct":
381
+ unsets.extend(COMPATIBLE_API_NETWORK_ENV_KEYS)
382
+ exports = {key: value for key, value in exports.items() if key not in COMPATIBLE_API_NETWORK_ENV_KEYS}
383
+ overrides = _provider_command_overrides(str(agent.get("provider") or ""), auth_mode, values, agent)
384
+ env_file = None
385
+ if exports or unsets:
386
+ env_file = _write_runtime_env_file(workspace, str(agent["id"]), exports, unsets)
387
+ return {
388
+ "profile": str(profile),
389
+ "auth_mode": auth_mode,
390
+ "path": loaded.get("path"),
391
+ "credential_present": loaded.get("credential_present"),
392
+ "env_file": str(env_file) if env_file else None,
393
+ "env_keys": sorted(exports),
394
+ "env_unset": sorted(unsets),
395
+ "claude_projects_root": claude_projects_root,
396
+ "command_overrides": overrides,
397
+ "secret_values_printed": False,
398
+ }
399
+
400
+ def effective_model(agent: dict[str, Any], workspace: Path | None = None) -> str | None:
401
+ if agent.get("model"):
402
+ return str(agent["model"])
403
+ if workspace is None or not agent.get("profile"):
404
+ return None
405
+ loaded = load_profile(workspace, str(agent["profile"]), _agent_profile_dir(agent))
406
+ values: dict[str, str] = loaded.get("values", {})
407
+ return values.get("MODEL") or values.get("ANTHROPIC_MODEL")
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import urllib.parse
5
+ from typing import Any
6
+
7
+ from team_agent.rust_core import redact_text
8
+ from team_agent.profiles.constants import SECRET_KEYS
9
+
10
+
11
+ def _format_profile_check_failure(check: dict[str, Any]) -> str:
12
+ agent_id = check.get("agent_id") or "unknown"
13
+ profile = check.get("profile") or "-"
14
+ reason = check.get("reason") or "profile_invalid"
15
+ suggestion = check.get("suggestion") or f"Inspect safely with `team-agent profile show {profile} --workspace . --json`."
16
+ return f"profile validation failed for {agent_id} profile {profile}: {reason}. {suggestion}"
17
+
18
+ def _alternate_value(values: dict[str, str], key: str) -> str | None:
19
+ alternates = {
20
+ "BASE_URL": ["ANTHROPIC_BASE_URL", "OPENAI_BASE_URL"],
21
+ "API_KEY": ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "AUTH_TOKEN", "OPENAI_API_KEY", "GEMINI_API_KEY"],
22
+ }
23
+ for candidate in alternates.get(key, []):
24
+ if values.get(candidate):
25
+ return values[candidate]
26
+ return None
27
+
28
+ def _strip_env_value(value: str) -> str:
29
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
30
+ return value[1:-1]
31
+ return value
32
+
33
+ def _safe_codex_provider_id(value: str) -> bool:
34
+ return re.fullmatch(r"[A-Za-z0-9_-]+", value) is not None
35
+
36
+ def _is_secret_key(key: str) -> bool:
37
+ upper = key.upper()
38
+ return upper in SECRET_KEYS or "KEY" in upper or "TOKEN" in upper or "SECRET" in upper
39
+
40
+ def _safe_profile_value(key: str, value: str) -> dict[str, Any]:
41
+ if _is_secret_key(key):
42
+ return {"present": bool(value), "redacted": True}
43
+ return {"present": bool(value), "redacted": False, "value": _safe_plain_profile_value(value)}
44
+
45
+ def _common_missing_values(auth_mode: str | None, values: dict[str, str]) -> list[str]:
46
+ if auth_mode == "compatible_api":
47
+ required = ["BASE_URL", "API_KEY", "MODEL"]
48
+ elif auth_mode == "official_api":
49
+ required = ["API_KEY"]
50
+ else:
51
+ required = []
52
+ missing = []
53
+ for key in required:
54
+ if key == "MODEL":
55
+ if not (values.get("MODEL") or values.get("ANTHROPIC_MODEL")):
56
+ missing.append(key)
57
+ continue
58
+ if not values.get(key) and not _alternate_value(values, key):
59
+ missing.append(key)
60
+ return missing
61
+
62
+ def _safe_plain_profile_value(value: str) -> str:
63
+ parsed = urllib.parse.urlparse(value)
64
+ if parsed.scheme and parsed.netloc:
65
+ host = parsed.hostname or ""
66
+ port = f":{parsed.port}" if parsed.port else ""
67
+ auth = "[redacted]@" if parsed.username or parsed.password else ""
68
+ value = urllib.parse.urlunparse((parsed.scheme, f"{auth}{host}{port}", parsed.path, "", "", ""))
69
+ return str(redact_text(value).get("text") or "")