@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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/emit.rs +33 -11
- package/crates/team-agent/src/cli/status_port.rs +2 -2
- package/crates/team-agent/src/event_log.rs +3 -3
- package/crates/team-agent/src/lib.rs +1 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -13
- package/crates/team-agent/src/mcp_server/wire.rs +3 -1
- package/crates/team-agent/src/redaction.rs +185 -0
- package/crates/team-agent/src/tmux_backend.rs +1 -0
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -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
|
|
15
|
-
CmdOutput::Json(value)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
624
|
-
let
|
|
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
|
-
|
|
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, "[
|
|
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
|
|
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.
|
|
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.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
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",
|