@team-agent/installer 0.3.1 → 0.3.3

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 (79) hide show
  1. package/Cargo.lock +34 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -1
  4. package/crates/team-agent/src/cli/adapters.rs +234 -26
  5. package/crates/team-agent/src/cli/diagnose.rs +144 -10
  6. package/crates/team-agent/src/cli/emit.rs +289 -54
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +1281 -196
  9. package/crates/team-agent/src/cli/status_port.rs +195 -46
  10. package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
  11. package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
  12. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
  13. package/crates/team-agent/src/cli/tests/run_delegation.rs +59 -3
  14. package/crates/team-agent/src/cli/types.rs +18 -0
  15. package/crates/team-agent/src/compiler.rs +15 -5
  16. package/crates/team-agent/src/coordinator/health.rs +95 -17
  17. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  18. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  19. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  20. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  21. package/crates/team-agent/src/coordinator/types.rs +15 -3
  22. package/crates/team-agent/src/db/schema.rs +37 -2
  23. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  24. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  25. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  26. package/crates/team-agent/src/fake_worker.rs +146 -3
  27. package/crates/team-agent/src/leader/start.rs +121 -23
  28. package/crates/team-agent/src/leader/types.rs +44 -1
  29. package/crates/team-agent/src/lib.rs +3 -0
  30. package/crates/team-agent/src/lifecycle/display.rs +645 -47
  31. package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
  32. package/crates/team-agent/src/lifecycle/mod.rs +2 -0
  33. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  34. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  35. package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
  36. package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
  37. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
  38. package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
  39. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  40. package/crates/team-agent/src/lifecycle/restart.rs +24 -1
  41. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
  43. package/crates/team-agent/src/lifecycle/types.rs +19 -0
  44. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  45. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  46. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  47. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  48. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  49. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  50. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  51. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  52. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  53. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  54. package/crates/team-agent/src/message_store.rs +21 -4
  55. package/crates/team-agent/src/messaging/delivery.rs +470 -59
  56. package/crates/team-agent/src/messaging/mod.rs +9 -6
  57. package/crates/team-agent/src/messaging/results.rs +353 -63
  58. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  59. package/crates/team-agent/src/messaging/send.rs +35 -3
  60. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  61. package/crates/team-agent/src/messaging/types.rs +11 -3
  62. package/crates/team-agent/src/os_probe.rs +119 -0
  63. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  64. package/crates/team-agent/src/packaging/tests.rs +23 -0
  65. package/crates/team-agent/src/provider/adapter.rs +564 -63
  66. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  67. package/crates/team-agent/src/provider/classify.rs +51 -4
  68. package/crates/team-agent/src/provider/helpers.rs +10 -1
  69. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  70. package/crates/team-agent/src/provider/types.rs +47 -0
  71. package/crates/team-agent/src/session_capture.rs +616 -0
  72. package/crates/team-agent/src/state/persist.rs +170 -1
  73. package/crates/team-agent/src/state/projection.rs +141 -8
  74. package/crates/team-agent/src/state/selector.rs +5 -2
  75. package/crates/team-agent/src/tmux_backend.rs +161 -64
  76. package/crates/team-agent/src/transport/test_support.rs +9 -0
  77. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  78. package/crates/team-agent/src/transport.rs +13 -2
  79. package/package.json +4 -4
@@ -0,0 +1,522 @@
1
+ use std::collections::BTreeMap;
2
+ use std::io::Write as _;
3
+ use std::path::Path;
4
+ use std::process::{Command, Stdio};
5
+ use std::time::Duration;
6
+
7
+ use serde_json::{json, Value};
8
+
9
+ use crate::lifecycle::profile_launch;
10
+ use crate::model::enums::{AuthMode, Provider};
11
+ use crate::model::yaml::Value as YamlValue;
12
+
13
+ pub(crate) const DEFAULT_PROFILE_SMOKE_TIMEOUT: Duration = Duration::from_secs(8);
14
+
15
+ pub(crate) fn profile_smoke_checks_for_agents(
16
+ workspace: &Path,
17
+ agents: &[YamlValue],
18
+ timeout: Duration,
19
+ ) -> Vec<Value> {
20
+ profile_smoke_checks_for_agents_with_profile_dir(workspace, agents, None, timeout)
21
+ }
22
+
23
+ pub(crate) fn profile_smoke_checks_for_agents_with_profile_dir(
24
+ workspace: &Path,
25
+ agents: &[YamlValue],
26
+ profile_dir: Option<&Path>,
27
+ timeout: Duration,
28
+ ) -> Vec<Value> {
29
+ agents
30
+ .iter()
31
+ .map(|agent| smoke_check_agent_profile(workspace, agent, profile_dir, timeout))
32
+ .collect()
33
+ }
34
+
35
+ pub(crate) fn smoke_check_agent_profile(
36
+ workspace: &Path,
37
+ agent: &YamlValue,
38
+ profile_dir: Option<&Path>,
39
+ timeout: Duration,
40
+ ) -> Value {
41
+ let agent_id = agent
42
+ .get("id")
43
+ .and_then(YamlValue::as_str)
44
+ .unwrap_or("agent")
45
+ .to_string();
46
+ let provider = agent
47
+ .get("provider")
48
+ .and_then(YamlValue::as_str)
49
+ .and_then(profile_launch::parse_provider)
50
+ .unwrap_or(Provider::Codex);
51
+ let auth_mode = agent
52
+ .get("auth_mode")
53
+ .and_then(YamlValue::as_str)
54
+ .and_then(profile_launch::parse_auth_mode)
55
+ .unwrap_or(AuthMode::Subscription);
56
+ let profile = agent
57
+ .get("profile")
58
+ .and_then(YamlValue::as_str)
59
+ .filter(|value| !value.is_empty());
60
+ let model = agent
61
+ .get("model")
62
+ .and_then(YamlValue::as_str)
63
+ .filter(|value| !value.is_empty());
64
+
65
+ if auth_mode != AuthMode::CompatibleApi {
66
+ return base_result(
67
+ &agent_id,
68
+ provider,
69
+ profile,
70
+ auth_mode,
71
+ true,
72
+ "not_required",
73
+ );
74
+ }
75
+ let Some(profile_name) = profile else {
76
+ return invalid_profile(&agent_id, provider, profile, auth_mode, "missing_profile");
77
+ };
78
+ let loaded = match profile_launch::load_profile(workspace, profile_name, profile_dir) {
79
+ Ok(loaded) => loaded,
80
+ Err(error) => {
81
+ return invalid_profile(&agent_id, provider, profile, auth_mode, &error.to_string());
82
+ }
83
+ };
84
+ if smoke_disabled(&loaded.values) {
85
+ let mut result = base_result(
86
+ &agent_id,
87
+ provider,
88
+ profile,
89
+ auth_mode,
90
+ true,
91
+ "skipped_by_profile",
92
+ );
93
+ merge_proxy_info(&mut result, &loaded.values);
94
+ return result;
95
+ }
96
+
97
+ let target = match SmokeTarget::from_profile(provider, model, &loaded.values) {
98
+ Ok(target) => target,
99
+ Err(reason) => {
100
+ let mut result = invalid_profile(&agent_id, provider, profile, auth_mode, reason);
101
+ merge_proxy_info(&mut result, &loaded.values);
102
+ return result;
103
+ }
104
+ };
105
+ let mut result = run_http_smoke(&agent_id, provider, profile, auth_mode, &target, timeout);
106
+ merge_proxy_info(&mut result, &loaded.values);
107
+ result
108
+ }
109
+
110
+ pub(crate) fn format_profile_smoke_failures(failures: &[Value]) -> String {
111
+ failures
112
+ .iter()
113
+ .filter_map(|failure| {
114
+ let agent = failure
115
+ .get("agent_id")
116
+ .and_then(Value::as_str)
117
+ .unwrap_or("agent");
118
+ let reason = failure
119
+ .get("reason")
120
+ .or_else(|| failure.get("error"))
121
+ .and_then(Value::as_str)
122
+ .unwrap_or("failed");
123
+ Some(format!("{agent}: {reason}"))
124
+ })
125
+ .collect::<Vec<_>>()
126
+ .join("; ")
127
+ }
128
+
129
+ struct SmokeTarget {
130
+ endpoint: String,
131
+ token: String,
132
+ model: String,
133
+ kind: SmokeKind,
134
+ }
135
+
136
+ enum SmokeKind {
137
+ Anthropic,
138
+ OpenAi,
139
+ }
140
+
141
+ impl SmokeTarget {
142
+ fn from_profile(
143
+ provider: Provider,
144
+ agent_model: Option<&str>,
145
+ values: &BTreeMap<String, String>,
146
+ ) -> Result<Self, &'static str> {
147
+ match provider {
148
+ Provider::Claude | Provider::ClaudeCode => {
149
+ let base = value_any(values, &["ANTHROPIC_BASE_URL", "BASE_URL"])
150
+ .ok_or("missing_base_url")?;
151
+ let token = value_any(
152
+ values,
153
+ &[
154
+ "ANTHROPIC_AUTH_TOKEN",
155
+ "AUTH_TOKEN",
156
+ "ANTHROPIC_API_KEY",
157
+ "API_KEY",
158
+ "BEARER_TOKEN",
159
+ ],
160
+ )
161
+ .ok_or("missing_api_key")?;
162
+ let model = value_any(values, &["ANTHROPIC_MODEL", "MODEL"])
163
+ .or(agent_model)
164
+ .ok_or("missing_model")?;
165
+ Ok(Self {
166
+ endpoint: anthropic_endpoint(base),
167
+ token: token.to_string(),
168
+ model: model.to_string(),
169
+ kind: SmokeKind::Anthropic,
170
+ })
171
+ }
172
+ Provider::Codex => {
173
+ let base = value_any(values, &["OPENAI_BASE_URL", "BASE_URL"])
174
+ .ok_or("missing_base_url")?;
175
+ let token =
176
+ value_any(values, &["OPENAI_API_KEY", "API_KEY"]).ok_or("missing_api_key")?;
177
+ let model = value_any(values, &["OPENAI_MODEL", "MODEL"])
178
+ .or(agent_model)
179
+ .ok_or("missing_model")?;
180
+ Ok(Self {
181
+ endpoint: openai_endpoint(base),
182
+ token: token.to_string(),
183
+ model: model.to_string(),
184
+ kind: SmokeKind::OpenAi,
185
+ })
186
+ }
187
+ Provider::GeminiCli | Provider::Fake => Err("unsupported_provider_smoke_skipped"),
188
+ }
189
+ }
190
+
191
+ fn body(&self) -> String {
192
+ let body = match self.kind {
193
+ SmokeKind::Anthropic => json!({
194
+ "model": self.model,
195
+ "max_tokens": 1,
196
+ "messages": [{"role": "user", "content": "ping"}],
197
+ }),
198
+ SmokeKind::OpenAi => json!({
199
+ "model": self.model,
200
+ "max_tokens": 1,
201
+ "messages": [{"role": "user", "content": "ping"}],
202
+ }),
203
+ };
204
+ body.to_string()
205
+ }
206
+ }
207
+
208
+ fn run_http_smoke(
209
+ agent_id: &str,
210
+ provider: Provider,
211
+ profile: Option<&str>,
212
+ auth_mode: AuthMode,
213
+ target: &SmokeTarget,
214
+ timeout: Duration,
215
+ ) -> Value {
216
+ match curl_post(target, timeout) {
217
+ Ok(response) if (200..300).contains(&response.http_status) => {
218
+ json!({
219
+ "ok": true,
220
+ "agent_id": agent_id,
221
+ "provider": provider_wire(provider),
222
+ "profile": profile,
223
+ "auth_mode": auth_mode_wire(auth_mode),
224
+ "status": "smoke_passed",
225
+ "http_status": response.http_status,
226
+ "endpoint": redact_endpoint(&target.endpoint),
227
+ "secret_values_printed": false,
228
+ })
229
+ }
230
+ Ok(response) => {
231
+ json!({
232
+ "ok": false,
233
+ "agent_id": agent_id,
234
+ "provider": provider_wire(provider),
235
+ "profile": profile,
236
+ "auth_mode": auth_mode_wire(auth_mode),
237
+ "status": "smoke_failed",
238
+ "reason": "http_error",
239
+ "http_status": response.http_status,
240
+ "endpoint": redact_endpoint(&target.endpoint),
241
+ "error": redact_text(&response.body, &[target.token.as_str()]),
242
+ "secret_values_printed": false,
243
+ })
244
+ }
245
+ Err(message) => {
246
+ json!({
247
+ "ok": false,
248
+ "agent_id": agent_id,
249
+ "provider": provider_wire(provider),
250
+ "profile": profile,
251
+ "auth_mode": auth_mode_wire(auth_mode),
252
+ "status": "smoke_failed",
253
+ "reason": "request_failed",
254
+ "endpoint": redact_endpoint(&target.endpoint),
255
+ "error": redact_text(&message, &[target.token.as_str()]),
256
+ "secret_values_printed": false,
257
+ })
258
+ }
259
+ }
260
+ }
261
+
262
+ struct CurlResponse {
263
+ http_status: u16,
264
+ body: String,
265
+ }
266
+
267
+ fn curl_post(target: &SmokeTarget, timeout: Duration) -> Result<CurlResponse, String> {
268
+ let body_path = temp_body_path();
269
+ std::fs::write(&body_path, target.body()).map_err(|e| e.to_string())?;
270
+ let result = curl_post_with_body_file(target, timeout, &body_path);
271
+ let _ = std::fs::remove_file(&body_path);
272
+ result
273
+ }
274
+
275
+ fn curl_post_with_body_file(
276
+ target: &SmokeTarget,
277
+ timeout: Duration,
278
+ body_path: &Path,
279
+ ) -> Result<CurlResponse, String> {
280
+ let mut child = Command::new("curl")
281
+ .arg("--config")
282
+ .arg("-")
283
+ .stdin(Stdio::piped())
284
+ .stdout(Stdio::piped())
285
+ .stderr(Stdio::piped())
286
+ .spawn()
287
+ .map_err(|e| format!("curl_spawn_failed: {e}"))?;
288
+ let Some(mut stdin) = child.stdin.take() else {
289
+ return Err("curl_stdin_unavailable".to_string());
290
+ };
291
+ stdin
292
+ .write_all(curl_config(target, timeout, body_path).as_bytes())
293
+ .map_err(|e| format!("curl_config_write_failed: {e}"))?;
294
+ drop(stdin);
295
+ let output = child
296
+ .wait_with_output()
297
+ .map_err(|e| format!("curl_wait_failed: {e}"))?;
298
+ let stdout = String::from_utf8_lossy(&output.stdout).to_string();
299
+ let stderr = String::from_utf8_lossy(&output.stderr).to_string();
300
+ if !output.status.success() {
301
+ let message = if stderr.trim().is_empty() {
302
+ stdout
303
+ } else {
304
+ stderr
305
+ };
306
+ return Err(format!("curl_failed: {}", message.trim()));
307
+ }
308
+ parse_curl_output(&stdout)
309
+ }
310
+
311
+ fn curl_config(target: &SmokeTarget, timeout: Duration, body_path: &Path) -> String {
312
+ let mut lines = vec![
313
+ "silent".to_string(),
314
+ "show-error".to_string(),
315
+ format!("max-time = \"{}\"", timeout.as_secs().max(1)),
316
+ "output = \"-\"".to_string(),
317
+ "write-out = \"\\n%{http_code}\"".to_string(),
318
+ "request = \"POST\"".to_string(),
319
+ format!("url = \"{}\"", curl_quote(&target.endpoint)),
320
+ "header = \"content-type: application/json\"".to_string(),
321
+ format!(
322
+ "header = \"authorization: Bearer {}\"",
323
+ curl_quote(&target.token)
324
+ ),
325
+ format!(
326
+ "data-binary = \"@{}\"",
327
+ curl_quote(&body_path.to_string_lossy())
328
+ ),
329
+ ];
330
+ if matches!(target.kind, SmokeKind::Anthropic) {
331
+ lines.push("header = \"anthropic-version: 2023-06-01\"".to_string());
332
+ }
333
+ format!("{}\n", lines.join("\n"))
334
+ }
335
+
336
+ fn parse_curl_output(stdout: &str) -> Result<CurlResponse, String> {
337
+ let Some((body, status)) = stdout.rsplit_once('\n') else {
338
+ return Err("curl_missing_http_status".to_string());
339
+ };
340
+ let http_status = status
341
+ .trim()
342
+ .parse::<u16>()
343
+ .map_err(|e| format!("curl_bad_http_status: {e}"))?;
344
+ Ok(CurlResponse {
345
+ http_status,
346
+ body: body.chars().take(4096).collect(),
347
+ })
348
+ }
349
+
350
+ fn temp_body_path() -> std::path::PathBuf {
351
+ let nanos = std::time::SystemTime::now()
352
+ .duration_since(std::time::UNIX_EPOCH)
353
+ .map(|duration| duration.as_nanos())
354
+ .unwrap_or(0);
355
+ std::env::temp_dir().join(format!(
356
+ "team-agent-profile-smoke-{}-{nanos}.json",
357
+ std::process::id()
358
+ ))
359
+ }
360
+
361
+ fn curl_quote(raw: &str) -> String {
362
+ raw.replace('\\', "\\\\")
363
+ .replace('"', "\\\"")
364
+ .replace('\n', "")
365
+ .replace('\r', "")
366
+ }
367
+
368
+ fn base_result(
369
+ agent_id: &str,
370
+ provider: Provider,
371
+ profile: Option<&str>,
372
+ auth_mode: AuthMode,
373
+ ok: bool,
374
+ status: &str,
375
+ ) -> Value {
376
+ json!({
377
+ "ok": ok,
378
+ "agent_id": agent_id,
379
+ "provider": provider_wire(provider),
380
+ "profile": profile,
381
+ "auth_mode": auth_mode_wire(auth_mode),
382
+ "status": status,
383
+ "secret_values_printed": false,
384
+ })
385
+ }
386
+
387
+ fn invalid_profile(
388
+ agent_id: &str,
389
+ provider: Provider,
390
+ profile: Option<&str>,
391
+ auth_mode: AuthMode,
392
+ reason: &str,
393
+ ) -> Value {
394
+ json!({
395
+ "ok": false,
396
+ "agent_id": agent_id,
397
+ "provider": provider_wire(provider),
398
+ "profile": profile,
399
+ "auth_mode": auth_mode_wire(auth_mode),
400
+ "status": "profile_invalid",
401
+ "reason": reason,
402
+ "secret_values_printed": false,
403
+ })
404
+ }
405
+
406
+ fn value_any<'a>(values: &'a BTreeMap<String, String>, keys: &[&str]) -> Option<&'a str> {
407
+ keys.iter()
408
+ .find_map(|key| profile_launch::profile_value_or_alternate(values, key))
409
+ .filter(|value| !value.is_empty())
410
+ }
411
+
412
+ fn smoke_disabled(values: &BTreeMap<String, String>) -> bool {
413
+ values
414
+ .get("PROFILE_SMOKE")
415
+ .map(|value| {
416
+ matches!(
417
+ value.trim().to_ascii_lowercase().as_str(),
418
+ "false" | "0" | "no" | "off"
419
+ )
420
+ })
421
+ .unwrap_or(false)
422
+ }
423
+
424
+ fn anthropic_endpoint(base: &str) -> String {
425
+ endpoint_with_suffix(base, "messages", "v1/messages")
426
+ }
427
+
428
+ fn openai_endpoint(base: &str) -> String {
429
+ endpoint_with_suffix(base, "chat/completions", "v1/chat/completions")
430
+ }
431
+
432
+ fn endpoint_with_suffix(base: &str, terminal: &str, suffix: &str) -> String {
433
+ let clean = base.trim().trim_end_matches('/');
434
+ if clean.ends_with(terminal) {
435
+ return clean.to_string();
436
+ }
437
+ if clean.ends_with("/v1") {
438
+ return format!("{clean}/{}", terminal.trim_start_matches('/'));
439
+ }
440
+ format!("{clean}/{}", suffix.trim_start_matches('/'))
441
+ }
442
+
443
+ fn merge_proxy_info(result: &mut Value, values: &BTreeMap<String, String>) {
444
+ let Some(obj) = result.as_object_mut() else {
445
+ return;
446
+ };
447
+ let mode = profile_launch::profile_proxy_mode(values);
448
+ obj.insert("proxy_mode".to_string(), json!(mode));
449
+ if let Some((key, url)) = proxy_value(values) {
450
+ obj.insert("proxy_configured".to_string(), json!(true));
451
+ obj.insert("proxy_source".to_string(), json!(key));
452
+ obj.insert("proxy_scheme".to_string(), json!(proxy_scheme(url)));
453
+ obj.insert("proxy_url".to_string(), json!(redact_endpoint(url)));
454
+ } else {
455
+ obj.insert("proxy_configured".to_string(), json!(false));
456
+ }
457
+ }
458
+
459
+ fn proxy_value(values: &BTreeMap<String, String>) -> Option<(&'static str, &str)> {
460
+ [
461
+ "HTTPS_PROXY",
462
+ "HTTP_PROXY",
463
+ "ALL_PROXY",
464
+ "https_proxy",
465
+ "http_proxy",
466
+ "all_proxy",
467
+ ]
468
+ .into_iter()
469
+ .find_map(|key| {
470
+ values
471
+ .get(key)
472
+ .filter(|value| !value.is_empty())
473
+ .map(|value| (key, value.as_str()))
474
+ })
475
+ }
476
+
477
+ fn proxy_scheme(url: &str) -> Option<String> {
478
+ url.split_once("://").map(|(scheme, _)| scheme.to_string())
479
+ }
480
+
481
+ fn redact_endpoint(raw: &str) -> String {
482
+ let no_query = raw.split_once('?').map(|(head, _)| head).unwrap_or(raw);
483
+ let Some((scheme, rest)) = no_query.split_once("://") else {
484
+ return no_query.to_string();
485
+ };
486
+ let slash = rest.find('/').unwrap_or(rest.len());
487
+ let authority = &rest[..slash];
488
+ let path = &rest[slash..];
489
+ if let Some((_, host)) = authority.rsplit_once('@') {
490
+ format!("{scheme}://[redacted]@{host}{path}")
491
+ } else {
492
+ no_query.to_string()
493
+ }
494
+ }
495
+
496
+ fn redact_text(raw: &str, secrets: &[&str]) -> String {
497
+ let mut out = raw.chars().take(512).collect::<String>();
498
+ for secret in secrets {
499
+ if !secret.is_empty() {
500
+ out = out.replace(secret, "[redacted]");
501
+ }
502
+ }
503
+ out
504
+ }
505
+
506
+ fn provider_wire(provider: Provider) -> &'static str {
507
+ match provider {
508
+ Provider::Claude => "claude",
509
+ Provider::ClaudeCode => "claude_code",
510
+ Provider::Codex => "codex",
511
+ Provider::GeminiCli => "gemini_cli",
512
+ Provider::Fake => "fake",
513
+ }
514
+ }
515
+
516
+ fn auth_mode_wire(auth_mode: AuthMode) -> &'static str {
517
+ match auth_mode {
518
+ AuthMode::Subscription => "subscription",
519
+ AuthMode::OfficialApi => "official_api",
520
+ AuthMode::CompatibleApi => "compatible_api",
521
+ }
522
+ }