@team-agent/installer 0.5.46 → 0.5.47

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.
package/Cargo.lock CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.46"
578
+ version = "0.5.47"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.46"
12
+ version = "0.5.47"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -11,16 +11,22 @@ use std::io::Write as _;
11
11
  pub fn emit(output: &CmdOutput, as_json: bool) -> Option<String> {
12
12
  match output {
13
13
  CmdOutput::None => None,
14
- CmdOutput::Human(text) => Some(text.clone()),
15
- CmdOutput::Json(value) if as_json => serde_json::to_string_pretty(&sort_json(value)).ok(),
16
- CmdOutput::Json(Value::Object(obj)) => {
17
- let lines: Vec<String> = obj
18
- .iter()
19
- .map(|(key, value)| format!("{key}: {}", human_value(value)))
20
- .collect();
21
- Some(lines.join("\n"))
14
+ CmdOutput::Human(text) => Some(crate::redaction::redact_external_text(text)),
15
+ CmdOutput::Json(value) => {
16
+ let value = crate::redaction::redact_external_value(value);
17
+ if as_json {
18
+ return serde_json::to_string_pretty(&sort_json(&value)).ok();
19
+ }
20
+ if let Value::Object(obj) = value {
21
+ let lines: Vec<String> = obj
22
+ .iter()
23
+ .map(|(key, value)| format!("{key}: {}", human_value(value)))
24
+ .collect();
25
+ Some(lines.join("\n"))
26
+ } else {
27
+ Some(human_value(&value))
28
+ }
22
29
  }
23
- CmdOutput::Json(value) => Some(human_value(value)),
24
30
  }
25
31
  }
26
32
 
@@ -620,8 +626,24 @@ fn emit_cli_error(command: &str, args: &[String], cwd: &Path, error: &CliError)
620
626
  }
621
627
  let normalized = normalize_cli_error(error);
622
628
  let payload_error = normalized.as_ref().unwrap_or(error);
623
- let _ = std::fs::write(&log_path, format!("{payload_error}\n"));
624
- let payload = payload_error.to_payload(&log_path, command);
629
+ let safe_error = crate::redaction::redact_external_text(&payload_error.to_string());
630
+ let _ = std::fs::write(&log_path, format!("{safe_error}\n"));
631
+ let mut payload = payload_error.to_payload(&log_path, command);
632
+ payload.error = safe_error;
633
+ payload.action = crate::redaction::redact_external_text(&payload.action);
634
+ payload.log = crate::redaction::redact_external_text(&payload.log);
635
+ payload.reason = payload
636
+ .reason
637
+ .map(|value| crate::redaction::redact_external_text(&value));
638
+ payload.session_name = payload
639
+ .session_name
640
+ .map(|value| crate::redaction::redact_external_text(&value));
641
+ payload.next_actions = payload.next_actions.map(|values| {
642
+ values
643
+ .into_iter()
644
+ .map(|value| crate::redaction::redact_external_text(&value))
645
+ .collect()
646
+ });
625
647
  if has_arg(args, "--json") {
626
648
  if let Ok(value) = serde_json::to_value(payload) {
627
649
  println!("{}", python_compact_json(&value));
@@ -89,7 +89,7 @@ pub fn status_scoped(
89
89
  );
90
90
  }
91
91
  let readiness = crate::cli::diagnose::wait_readiness(&readiness_state);
92
- let full = json!({
92
+ let full = crate::redaction::redact_external_value(&json!({
93
93
  "ok": true,
94
94
  "team": state.pointer("/leader/id").cloned().unwrap_or_else(|| json!("leader")),
95
95
  "session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
@@ -124,7 +124,7 @@ pub fn status_scoped(
124
124
  .tail(10)
125
125
  .map_err(|e| CliError::Runtime(e.to_string()))?,
126
126
  ),
127
- });
127
+ }));
128
128
  if compact {
129
129
  Ok(compact_status(full))
130
130
  } else {
@@ -147,7 +147,7 @@ impl EventLog {
147
147
  obj.insert(k, v);
148
148
  }
149
149
  }
150
- let event = Value::Object(obj);
150
+ let event = crate::redaction::redact_external_value(&Value::Object(obj));
151
151
  self.maybe_rotate()?;
152
152
  // 单次 write_all(line+"\n"):POSIX O_APPEND 对 <PIPE_BUF 写原子,避免并发写者交错(对抗 P1)。
153
153
  let mut bytes = to_python_json(&sort_value(&event)).into_bytes();
@@ -176,11 +176,11 @@ impl EventLog {
176
176
  let mut out = Vec::new();
177
177
  for line in &lines[start..] {
178
178
  match serde_json::from_str::<Value>(line) {
179
- Ok(v) => out.push(v),
179
+ Ok(v) => out.push(crate::redaction::redact_external_value(&v)),
180
180
  Err(_) => {
181
181
  let mut m = serde_json::Map::new();
182
182
  m.insert("raw".to_string(), Value::String((*line).to_string()));
183
- out.push(Value::Object(m));
183
+ out.push(crate::redaction::redact_external_value(&Value::Object(m)));
184
184
  }
185
185
  }
186
186
  }
@@ -56,6 +56,7 @@ pub use crate::db::message_store;
56
56
  // step 8 (provider) — ProviderAdapter trait + typed provider/turn-state/liveness 等(ROUND-0 骨架;
57
57
  // fn body unimplemented!(),P2 porter 落实现)。MUST-NOT-13:provider 调用全走 trait。
58
58
  pub mod provider;
59
+ mod redaction;
59
60
  /// unit-6 (Stage 2) compat shim. Physical home is now
60
61
  /// `crate::provider::session::capture`; this re-export keeps every
61
62
  /// `crate::session_capture::*` caller working without modification.
@@ -485,27 +485,17 @@ fn proxy_scheme(url: &str) -> Option<String> {
485
485
 
486
486
  fn redact_endpoint(raw: &str) -> String {
487
487
  let no_query = raw.split_once('?').map(|(head, _)| head).unwrap_or(raw);
488
- let Some((scheme, rest)) = no_query.split_once("://") else {
489
- return no_query.to_string();
490
- };
491
- let slash = rest.find('/').unwrap_or(rest.len());
492
- let authority = &rest[..slash];
493
- let path = &rest[slash..];
494
- if let Some((_, host)) = authority.rsplit_once('@') {
495
- format!("{scheme}://[redacted]@{host}{path}")
496
- } else {
497
- no_query.to_string()
498
- }
488
+ crate::redaction::redact_external_text(no_query)
499
489
  }
500
490
 
501
491
  fn redact_text(raw: &str, secrets: &[&str]) -> String {
502
492
  let mut out = raw.chars().take(512).collect::<String>();
503
493
  for secret in secrets {
504
494
  if !secret.is_empty() {
505
- out = out.replace(secret, "[redacted]");
495
+ out = out.replace(secret, "[REDACTED]");
506
496
  }
507
497
  }
508
- out
498
+ crate::redaction::redact_external_text(&out)
509
499
  }
510
500
 
511
501
  fn auth_mode_wire(auth_mode: AuthMode) -> &'static str {
@@ -204,6 +204,7 @@ fn run_stdio_loop_inner<R: BufRead, W: Write>(
204
204
  report.requests_read = report.requests_read.saturating_add(1);
205
205
  let frame = handle_stdin_line(tools, &line, report)?;
206
206
  if let Some(value) = frame {
207
+ let value = crate::redaction::redact_external_value(&value);
207
208
  serde_json::to_writer(&mut *writer, &value)?;
208
209
  writer.write_all(b"\n")?;
209
210
  writer.flush()?;
@@ -266,7 +267,8 @@ fn rpc_id_from_request(request: &Value) -> RpcId {
266
267
  }
267
268
 
268
269
  fn tool_call_result_value(is_error: bool, body: &Value) -> Value {
269
- let text = json_dumps_default(body);
270
+ let body = crate::redaction::redact_external_value(body);
271
+ let text = json_dumps_default(&body);
270
272
  let mut content = serde_json::Map::new();
271
273
  content.insert("type".to_string(), Value::String("text".to_string()));
272
274
  content.insert("text".to_string(), Value::String(text));
@@ -0,0 +1,185 @@
1
+ use std::sync::LazyLock;
2
+
3
+ use regex::{Captures, Regex};
4
+ use serde_json::Value;
5
+
6
+ const REDACTED: &str = "[REDACTED]";
7
+ const SENSITIVE_FAMILIES: [&str; 7] = [
8
+ "PROXY",
9
+ "TOKEN",
10
+ "KEY",
11
+ "PASSWORD",
12
+ "AUTH",
13
+ "SECRET",
14
+ "CREDENTIAL",
15
+ ];
16
+ const STRUCTURAL_KEYS: [&str; 13] = [
17
+ "active_team_key",
18
+ "cohort_key",
19
+ "dedupe_key",
20
+ "error_key",
21
+ "error_observation_key",
22
+ "last_check_key",
23
+ "last_error_observation_key",
24
+ "last_notified_key",
25
+ "last_suppressed_key",
26
+ "parent_team_key",
27
+ "runtime_team_key",
28
+ "team_key",
29
+ "team_state_key",
30
+ ];
31
+
32
+ static SHELL_ASSIGNMENT: LazyLock<Regex> = LazyLock::new(|| {
33
+ Regex::new(
34
+ r#"(?i)(?P<prefix>^|[\s\[,;('"])(?P<key>[a-z_][a-z0-9_]*)=(?P<value>'[^']*'|"[^"]*"|[^\s,\]\[};)]+)"#,
35
+ )
36
+ .expect("shell assignment redaction regex")
37
+ });
38
+
39
+ static URL_USERINFO: LazyLock<Regex> = LazyLock::new(|| {
40
+ Regex::new(r"(?P<scheme>[A-Za-z][A-Za-z0-9+.-]*://)[^\s/?#]+@")
41
+ .expect("URL userinfo redaction regex")
42
+ });
43
+
44
+ pub(crate) fn redact_external_value(value: &serde_json::Value) -> serde_json::Value {
45
+ match value {
46
+ Value::Object(object) => Value::Object(
47
+ object
48
+ .iter()
49
+ .map(|(key, value)| {
50
+ let value = if is_sensitive_env_key(key) {
51
+ Value::String(REDACTED.to_string())
52
+ } else {
53
+ redact_external_value(value)
54
+ };
55
+ (key.clone(), value)
56
+ })
57
+ .collect(),
58
+ ),
59
+ Value::Array(values) => Value::Array(values.iter().map(redact_external_value).collect()),
60
+ Value::String(text) => Value::String(redact_external_text(text)),
61
+ other => other.clone(),
62
+ }
63
+ }
64
+
65
+ pub(crate) fn redact_external_text(text: &str) -> String {
66
+ let assignments = SHELL_ASSIGNMENT.replace_all(text, |captures: &Captures<'_>| {
67
+ let key = captures.name("key").map_or("", |value| value.as_str());
68
+ if !is_sensitive_env_key(key) {
69
+ return captures[0].to_string();
70
+ }
71
+ let prefix = captures.name("prefix").map_or("", |value| value.as_str());
72
+ let value = captures.name("value").map_or("", |value| value.as_str());
73
+ let quote = match (value.as_bytes().first(), value.as_bytes().last()) {
74
+ (Some(b'\''), Some(b'\'')) => "'",
75
+ (Some(b'"'), Some(b'"')) => "\"",
76
+ _ => "",
77
+ };
78
+ let unquoted = value
79
+ .strip_prefix(quote)
80
+ .and_then(|value| value.strip_suffix(quote))
81
+ .unwrap_or(value);
82
+ let has_url_userinfo = URL_USERINFO.is_match(unquoted);
83
+ let redacted_url = URL_USERINFO
84
+ .replace_all(unquoted, "${scheme}[REDACTED]@")
85
+ .into_owned();
86
+ let safe_value = if has_url_userinfo {
87
+ &redacted_url
88
+ } else {
89
+ REDACTED
90
+ };
91
+ format!("{prefix}{key}={quote}{safe_value}{quote}")
92
+ });
93
+ URL_USERINFO
94
+ .replace_all(&assignments, "${scheme}[REDACTED]@")
95
+ .into_owned()
96
+ }
97
+
98
+ fn is_sensitive_env_key(key: &str) -> bool {
99
+ if STRUCTURAL_KEYS
100
+ .iter()
101
+ .any(|structural| key.eq_ignore_ascii_case(structural))
102
+ {
103
+ return false;
104
+ }
105
+ let key = key.to_ascii_uppercase();
106
+ SENSITIVE_FAMILIES
107
+ .iter()
108
+ .any(|family| key == *family || key.ends_with(&format!("_{family}")))
109
+ }
110
+
111
+ #[cfg(test)]
112
+ mod tests {
113
+ use super::*;
114
+ use serde_json::json;
115
+
116
+ #[test]
117
+ fn recursive_values_mask_env_families_without_erasing_wire_truth() {
118
+ let input = json!({
119
+ "HTTPS_PROXY": "proxy-secret",
120
+ "Copilot_Github_Token": "token-secret",
121
+ "nested": [{"db_password": "password-secret"}],
122
+ "team_key": "current",
123
+ "active_team_key": "current",
124
+ "dedupe_key": "restart:worker",
125
+ "auth_mode": "subscription",
126
+ "model_source": "role",
127
+ "model_stale": true,
128
+ });
129
+
130
+ let redacted = redact_external_value(&input);
131
+
132
+ assert_eq!(redacted["HTTPS_PROXY"], REDACTED);
133
+ assert_eq!(redacted["Copilot_Github_Token"], REDACTED);
134
+ assert_eq!(redacted["nested"][0]["db_password"], REDACTED);
135
+ assert_eq!(redacted["team_key"], "current");
136
+ assert_eq!(redacted["active_team_key"], "current");
137
+ assert_eq!(redacted["dedupe_key"], "restart:worker");
138
+ assert_eq!(redacted["auth_mode"], "subscription");
139
+ assert_eq!(redacted["model_source"], "role");
140
+ assert_eq!(redacted["model_stale"], true);
141
+ assert_eq!(redact_external_value(&redacted), redacted);
142
+ }
143
+
144
+ #[test]
145
+ fn text_masks_quoted_assignments_and_url_userinfo_idempotently() {
146
+ let input = "HTTPS_PROXY='https://user:pass@proxy.invalid:8443/path' http_proxy=other endpoint=https://user:pass@proxy.invalid:8443/path";
147
+ let expected = "HTTPS_PROXY='https://[REDACTED]@proxy.invalid:8443/path' http_proxy=[REDACTED] endpoint=https://[REDACTED]@proxy.invalid:8443/path";
148
+
149
+ let redacted = redact_external_text(input);
150
+
151
+ assert_eq!(redacted, expected);
152
+ assert_eq!(redact_external_text(&redacted), redacted);
153
+ }
154
+
155
+ #[test]
156
+ fn text_leaves_non_env_assignments_and_plain_urls_unchanged() {
157
+ let input = "team_key=current author=operator endpoint=https://proxy.invalid/health";
158
+ assert_eq!(redact_external_text(input), input);
159
+ }
160
+
161
+ #[test]
162
+ fn mixed_external_value_is_idempotent() {
163
+ let marker = "synthetic-redaction-unit-marker";
164
+ let credential_url = format!("https://demo-user:{marker}@proxy.invalid:8443/path");
165
+ let diagnostic = format!(
166
+ "subprocess exited 37: argv=[tmux, HTTPS_PROXY='{credential_url}']; endpoint={credential_url}"
167
+ );
168
+ let input = json!({
169
+ "HTTPS_PROXY": marker,
170
+ "ordinary_diagnostic": format!(
171
+ "HTTPS_PROXY='{credential_url}' http_proxy=\"{credential_url}\" OPENAI_API_KEY={marker} endpoint={credential_url}"
172
+ ),
173
+ "nested": [[diagnostic, credential_url]],
174
+ "team_key": "current",
175
+ });
176
+
177
+ let once = redact_external_value(&input);
178
+ let twice = redact_external_value(&once);
179
+ let text = once.to_string();
180
+
181
+ assert!(!text.contains(marker));
182
+ assert!(text.contains("https://[REDACTED]@proxy.invalid:8443/path"));
183
+ assert_eq!(twice, once);
184
+ }
185
+ }
@@ -1338,6 +1338,7 @@ fn strip_ansi_escapes_inplace(input: &str) -> String {
1338
1338
  }
1339
1339
 
1340
1340
  fn scrub_secrets(line: &str) -> String {
1341
+ let line = crate::redaction::redact_external_text(line);
1341
1342
  // Five shapes: sk-XXXX, ghp_XXXX, AKIAXXXX (16-char uppercase id), Bearer XXXX,
1342
1343
  // 32+ hex (token).
1343
1344
  let mut out = String::with_capacity(line.len());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.46",
3
+ "version": "0.5.47",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.5.46",
24
- "@team-agent/cli-darwin-x64": "0.5.46",
25
- "@team-agent/cli-linux-x64": "0.5.46"
23
+ "@team-agent/cli-darwin-arm64": "0.5.47",
24
+ "@team-agent/cli-darwin-x64": "0.5.47",
25
+ "@team-agent/cli-linux-x64": "0.5.47"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",