@team-agent/installer 0.5.73 → 0.5.75
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/adapters.rs +19 -13
- package/crates/team-agent/src/cli/emit.rs +6 -3
- package/crates/team-agent/src/cli/mod.rs +151 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +28 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +3 -0
- package/crates/team-agent/src/cli/types.rs +6 -0
- package/crates/team-agent/src/compiler.rs +14 -1
- package/crates/team-agent/src/layout/worker_env.rs +366 -0
- package/crates/team-agent/src/leader/lease.rs +334 -53
- package/crates/team-agent/src/leader/registry.rs +177 -108
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +12 -1
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +2272 -128
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +16 -6
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +138 -13
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +5 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +3 -11
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +60 -0
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +31 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +25 -23
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +110 -31
- package/crates/team-agent/src/messaging/delivery.rs +226 -37
- package/crates/team-agent/src/messaging/leader_channel.rs +12 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +234 -0
- package/crates/team-agent/src/model/spec.rs +81 -7
- package/crates/team-agent/src/provider/adapters/pi.rs +14 -0
- package/crates/team-agent/src/state/persist.rs +433 -4
- package/crates/team-agent/src/state/repository.rs +45 -2
- package/crates/team-agent/src/tmux_backend/tests.rs +414 -7
- package/crates/team-agent/src/tmux_backend.rs +274 -26
- package/crates/team-agent/src/transport/test_support.rs +18 -3
- package/crates/team-agent/src/transport.rs +99 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +3 -0
- package/skills/team-agent/command-coverage.json +21 -0
- package/skills/team-agent/references/team-agent-operator.md +9 -2
|
@@ -54,14 +54,14 @@ fn worker_spawn_inherits_parent_process_env_for_proxy_and_ca() {
|
|
|
54
54
|
&launch_transport,
|
|
55
55
|
)
|
|
56
56
|
.expect("launch fixture should spawn worker");
|
|
57
|
-
let
|
|
57
|
+
let launch_spawn = launch_transport.single_spawn();
|
|
58
58
|
|
|
59
59
|
let restart_team = compiled_team_dir("restart", &[("worker_a", "Restart Worker")]);
|
|
60
60
|
seed_restart_state(&restart_team, "worker_a");
|
|
61
61
|
let restart_transport = RecordingTransport::new().with_session_present(false);
|
|
62
62
|
restart_with_transport(&restart_team, true, None, &restart_transport)
|
|
63
63
|
.expect("restart fixture should spawn worker");
|
|
64
|
-
let
|
|
64
|
+
let restart_spawn = restart_transport.single_spawn();
|
|
65
65
|
|
|
66
66
|
let add_team = compiled_team_dir("add-agent", &[("worker_a", "Existing Worker")]);
|
|
67
67
|
seed_running_add_agent_state(&add_team, "worker_a");
|
|
@@ -77,17 +77,15 @@ fn worker_spawn_inherits_parent_process_env_for_proxy_and_ca() {
|
|
|
77
77
|
&add_transport,
|
|
78
78
|
)
|
|
79
79
|
.expect("add-agent fixture should spawn worker");
|
|
80
|
-
let
|
|
80
|
+
let add_spawn = add_transport.single_spawn();
|
|
81
81
|
|
|
82
82
|
let failures = [
|
|
83
|
-
("quick-start launch",
|
|
84
|
-
("restart",
|
|
85
|
-
("add-agent",
|
|
83
|
+
("quick-start launch", launch_spawn, "worker_a"),
|
|
84
|
+
("restart", restart_spawn, "worker_a"),
|
|
85
|
+
("add-agent", add_spawn, "worker_b"),
|
|
86
86
|
]
|
|
87
87
|
.into_iter()
|
|
88
|
-
.flat_map(|(surface,
|
|
89
|
-
spawn_env_contract_failures(surface, &command_line, agent_id)
|
|
90
|
-
})
|
|
88
|
+
.flat_map(|(surface, spawn, agent_id)| spawn_env_contract_failures(surface, &spawn, agent_id))
|
|
91
89
|
.collect::<Vec<_>>();
|
|
92
90
|
|
|
93
91
|
assert!(
|
|
@@ -97,6 +95,61 @@ fn worker_spawn_inherits_parent_process_env_for_proxy_and_ca() {
|
|
|
97
95
|
);
|
|
98
96
|
}
|
|
99
97
|
|
|
98
|
+
#[test]
|
|
99
|
+
fn tmux_control_filter_uses_env_keys_not_substring_in_inherited_values() {
|
|
100
|
+
let mut env = BTreeMap::from([
|
|
101
|
+
("TA_SPAWN_ENV_CANARY".to_string(), "FOO".to_string()),
|
|
102
|
+
(
|
|
103
|
+
"HTTP_PROXY".to_string(),
|
|
104
|
+
"http://canary-proxy:1".to_string(),
|
|
105
|
+
),
|
|
106
|
+
(
|
|
107
|
+
"NODE_EXTRA_CA_CERTS".to_string(),
|
|
108
|
+
"/canary/ca.pem".to_string(),
|
|
109
|
+
),
|
|
110
|
+
("TEAM_AGENT_WORKSPACE".to_string(), "/ws".to_string()),
|
|
111
|
+
(
|
|
112
|
+
"TEAM_AGENT_OWNER_TEAM_ID".to_string(),
|
|
113
|
+
"teamdir".to_string(),
|
|
114
|
+
),
|
|
115
|
+
("TEAM_AGENT_AGENT_ID".to_string(), "worker_a".to_string()),
|
|
116
|
+
(
|
|
117
|
+
"HARNESS_PROBE".to_string(),
|
|
118
|
+
"printf 'LS_USR_BIN_TMUX=%s\\n'\n".to_string(),
|
|
119
|
+
),
|
|
120
|
+
]);
|
|
121
|
+
let spawn = RecordedSpawn {
|
|
122
|
+
argv: vec!["codex".to_string()],
|
|
123
|
+
env: env.clone(),
|
|
124
|
+
env_unset: Vec::new(),
|
|
125
|
+
session: SessionName::new("s"),
|
|
126
|
+
window: WindowName::new("w"),
|
|
127
|
+
pane_id: PaneId::new("%1"),
|
|
128
|
+
};
|
|
129
|
+
assert!(
|
|
130
|
+
spawn_env_contract_failures("harness-value", &spawn, "worker_a").is_empty(),
|
|
131
|
+
"inherited values may contain the letters TMUX= without leaking TMUX/TMUX_PANE keys"
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
env.insert(
|
|
135
|
+
"TMUX".to_string(),
|
|
136
|
+
"/tmp/tmux-canary/default,1".to_string(),
|
|
137
|
+
);
|
|
138
|
+
let leaked = RecordedSpawn {
|
|
139
|
+
env,
|
|
140
|
+
..spawn
|
|
141
|
+
};
|
|
142
|
+
let failures = spawn_env_contract_failures("key-leak", &leaked, "worker_a");
|
|
143
|
+
assert!(
|
|
144
|
+
failures.iter().any(|row| row.contains("present_keys=") && row.contains("TMUX")),
|
|
145
|
+
"actual TMUX key must still fail closed; failures={failures:?}"
|
|
146
|
+
);
|
|
147
|
+
assert!(
|
|
148
|
+
failures.iter().all(|row| !row.contains("command_line=")),
|
|
149
|
+
"failures must not dump command_line; failures={failures:?}"
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
100
153
|
#[test]
|
|
101
154
|
#[serial(env)]
|
|
102
155
|
fn worker_spawn_scrubs_cross_provider_process_identity_but_keeps_config_env() {
|
|
@@ -234,42 +287,67 @@ fn worker_spawn_stays_out_of_coordinator_tick_and_daemon_preserves_parent_env()
|
|
|
234
287
|
);
|
|
235
288
|
}
|
|
236
289
|
|
|
237
|
-
fn
|
|
290
|
+
fn spawn_env_has_tmux_control_assignment(spawn: &RecordedSpawn) -> bool {
|
|
291
|
+
spawn.env.contains_key("TMUX")
|
|
292
|
+
|| spawn.env.contains_key("TMUX_PANE")
|
|
293
|
+
|| spawn.argv.iter().any(|part| {
|
|
294
|
+
part.split_whitespace().any(|tok| {
|
|
295
|
+
tok == "TMUX" || tok.starts_with("TMUX=") || tok.starts_with("TMUX_PANE=")
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fn spawn_env_contract_failures(
|
|
301
|
+
surface: &str,
|
|
302
|
+
spawn: &RecordedSpawn,
|
|
303
|
+
agent_id: &str,
|
|
304
|
+
) -> Vec<String> {
|
|
238
305
|
let mut failures = Vec::new();
|
|
239
|
-
for expected in [
|
|
240
|
-
"TA_SPAWN_ENV_CANARY
|
|
241
|
-
"HTTP_PROXY
|
|
242
|
-
"NODE_EXTRA_CA_CERTS
|
|
243
|
-
"
|
|
244
|
-
"TEAM_AGENT_OWNER_TEAM_ID=teamdir",
|
|
306
|
+
for (key, expected) in [
|
|
307
|
+
("TA_SPAWN_ENV_CANARY", Some("FOO")),
|
|
308
|
+
("HTTP_PROXY", Some("http://canary-proxy:1")),
|
|
309
|
+
("NODE_EXTRA_CA_CERTS", Some("/canary/ca.pem")),
|
|
310
|
+
("TEAM_AGENT_OWNER_TEAM_ID", Some("teamdir")),
|
|
245
311
|
] {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
312
|
+
match (spawn.env.get(key).map(String::as_str), expected) {
|
|
313
|
+
(Some(actual), Some(wanted)) if actual == wanted => {}
|
|
314
|
+
(Some(_), Some(wanted)) => failures.push(format!(
|
|
315
|
+
"{surface}: `{key}` present but not `{wanted}`"
|
|
316
|
+
)),
|
|
317
|
+
_ => failures.push(format!(
|
|
318
|
+
"{surface}: missing `{key}` overlay/inherit"
|
|
319
|
+
)),
|
|
250
320
|
}
|
|
251
321
|
}
|
|
252
|
-
if
|
|
322
|
+
if spawn
|
|
323
|
+
.env
|
|
324
|
+
.get("TEAM_AGENT_WORKSPACE")
|
|
325
|
+
.is_none_or(|value| value.is_empty())
|
|
326
|
+
{
|
|
327
|
+
failures.push(format!("{surface}: missing `TEAM_AGENT_WORKSPACE` overlay"));
|
|
328
|
+
}
|
|
329
|
+
if spawn.env.get("TEAM_AGENT_AGENT_ID").map(String::as_str) != Some(agent_id) {
|
|
253
330
|
failures.push(format!(
|
|
254
|
-
"{surface}: missing `TEAM_AGENT_AGENT_ID={agent_id}` overlay
|
|
331
|
+
"{surface}: missing `TEAM_AGENT_AGENT_ID={agent_id}` overlay"
|
|
255
332
|
));
|
|
256
333
|
}
|
|
257
|
-
if
|
|
334
|
+
if spawn_env_has_tmux_control_assignment(spawn) {
|
|
335
|
+
let present: Vec<&str> = ["TMUX", "TMUX_PANE"]
|
|
336
|
+
.into_iter()
|
|
337
|
+
.filter(|key| spawn.env.contains_key(*key))
|
|
338
|
+
.collect();
|
|
258
339
|
failures.push(format!(
|
|
259
|
-
"{surface}: worker spawn must filter tmux control env from inherited parent env;
|
|
340
|
+
"{surface}: worker spawn must filter tmux control env from inherited parent env; present_keys={present:?}"
|
|
260
341
|
));
|
|
261
342
|
}
|
|
262
|
-
if !
|
|
343
|
+
if !spawn.argv.iter().any(|part| part == "codex") {
|
|
263
344
|
failures.push(format!(
|
|
264
|
-
"{surface}: provider executable must remain the command name `codex
|
|
345
|
+
"{surface}: provider executable must remain the command name `codex`"
|
|
265
346
|
));
|
|
266
347
|
}
|
|
267
|
-
if
|
|
268
|
-
.split_whitespace()
|
|
269
|
-
.any(|part| part.ends_with("/codex"))
|
|
270
|
-
{
|
|
348
|
+
if spawn.argv.iter().any(|part| part.ends_with("/codex")) {
|
|
271
349
|
failures.push(format!(
|
|
272
|
-
"{surface}: provider executable must not be hard-coded as an absolute codex path
|
|
350
|
+
"{surface}: provider executable must not be hard-coded as an absolute codex path"
|
|
273
351
|
));
|
|
274
352
|
}
|
|
275
353
|
failures
|
|
@@ -580,6 +658,7 @@ impl RecordingTransport {
|
|
|
580
658
|
self
|
|
581
659
|
}
|
|
582
660
|
|
|
661
|
+
#[allow(dead_code)]
|
|
583
662
|
fn single_spawn_command_line(&self) -> String {
|
|
584
663
|
self.single_spawn().command_line()
|
|
585
664
|
}
|
|
@@ -31,9 +31,9 @@ use crate::provider::wire::{
|
|
|
31
31
|
is_claude_family, parse_canonical_provider, parse_provider, provider_wire,
|
|
32
32
|
};
|
|
33
33
|
use crate::transport::{
|
|
34
|
-
submit_verification_wire, turn_verification_wire, CaptureRange,
|
|
35
|
-
|
|
36
|
-
Target, Transport, WindowName,
|
|
34
|
+
submit_verification_wire, turn_verification_wire, CaptureRange, CaptureSampleOutcome,
|
|
35
|
+
InjectPayload, InjectReport, InjectVerification, InputSurfaceProbe, Key, PaneId, PaneInfo,
|
|
36
|
+
QueryOutcome, SessionName, SubmitObserver, SubmitVerification, Target, Transport, WindowName,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
use super::helpers::{message_exists, MessageStatusShadow};
|
|
@@ -749,16 +749,20 @@ pub fn deliver_pending_message(
|
|
|
749
749
|
// are preserved byte-for-byte for grep compatibility; new keys are
|
|
750
750
|
// ADDITIONAL.
|
|
751
751
|
let submit_attempts_detail = render_submit_diagnostics(&inject_report);
|
|
752
|
-
|
|
753
|
-
"
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
752
|
+
let mut unverified = serde_json::json!({
|
|
753
|
+
"message_id": message_id,
|
|
754
|
+
"recipient": message.recipient,
|
|
755
|
+
"reason": reason,
|
|
756
|
+
"attempts": inject_report.attempts,
|
|
757
|
+
"submit_attempts_detail": submit_attempts_detail,
|
|
758
|
+
});
|
|
759
|
+
merge_inject_readback_fields(
|
|
760
|
+
&mut unverified,
|
|
761
|
+
&inject_report,
|
|
762
|
+
Some(&target),
|
|
763
|
+
resolved.metadata.as_ref(),
|
|
764
|
+
);
|
|
765
|
+
event_log.write("send.unverified", unverified)?;
|
|
762
766
|
if inject_report.attempts >= u32::from(SEND_RETRY_MAX_ATTEMPTS) {
|
|
763
767
|
store.mark(message_id, "failed", Some("send_unverified_exhausted"))?;
|
|
764
768
|
emit_send_failed_exhausted(
|
|
@@ -1070,7 +1074,7 @@ fn persist_submit_verification(workspace: &Path, message_id: &str, report: &Inje
|
|
|
1070
1074
|
if std::fs::create_dir_all(&dir).is_err() {
|
|
1071
1075
|
return;
|
|
1072
1076
|
}
|
|
1073
|
-
let body = serde_json::json!({
|
|
1077
|
+
let mut body = serde_json::json!({
|
|
1074
1078
|
"message_id": message_id,
|
|
1075
1079
|
"submit_verification": format!("{:?}", report.submit_verification),
|
|
1076
1080
|
"inject_verification": format!("{:?}", report.inject_verification),
|
|
@@ -1078,6 +1082,7 @@ fn persist_submit_verification(workspace: &Path, message_id: &str, report: &Inje
|
|
|
1078
1082
|
"submit_verified": inject_submit_verified(report),
|
|
1079
1083
|
"attempts": report.attempts,
|
|
1080
1084
|
});
|
|
1085
|
+
merge_inject_readback_fields(&mut body, report, None, None);
|
|
1081
1086
|
let _ = std::fs::write(dir.join("submit-verification.json"), format!("{body}\n"));
|
|
1082
1087
|
}
|
|
1083
1088
|
|
|
@@ -1914,6 +1919,116 @@ fn render_submit_diagnostics(report: &InjectReport) -> serde_json::Value {
|
|
|
1914
1919
|
)
|
|
1915
1920
|
}
|
|
1916
1921
|
|
|
1922
|
+
fn json_opt_bool(value: Option<bool>) -> serde_json::Value {
|
|
1923
|
+
match value {
|
|
1924
|
+
Some(flag) => serde_json::Value::Bool(flag),
|
|
1925
|
+
None => serde_json::Value::Null,
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
fn inject_pane_id(target: &Target) -> Option<&str> {
|
|
1930
|
+
match target {
|
|
1931
|
+
Target::Pane(pane) => Some(pane.as_str()),
|
|
1932
|
+
Target::SessionWindow { .. } => None,
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
fn merge_inject_readback_fields(
|
|
1937
|
+
event: &mut serde_json::Value,
|
|
1938
|
+
report: &InjectReport,
|
|
1939
|
+
target: Option<&Target>,
|
|
1940
|
+
metadata: Option<&TargetMetadata>,
|
|
1941
|
+
) {
|
|
1942
|
+
if let Some(metadata) = metadata {
|
|
1943
|
+
append_target_metadata(event, metadata);
|
|
1944
|
+
}
|
|
1945
|
+
let Some(obj) = event.as_object_mut() else {
|
|
1946
|
+
return;
|
|
1947
|
+
};
|
|
1948
|
+
let inject_pane = target.and_then(inject_pane_id);
|
|
1949
|
+
if let Some(pane) = inject_pane {
|
|
1950
|
+
obj.insert(
|
|
1951
|
+
"inject_target_pane_id".to_string(),
|
|
1952
|
+
serde_json::json!(pane),
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
let resolved_pane = metadata.map(|meta| meta.target_pane_id.as_str());
|
|
1956
|
+
obj.insert(
|
|
1957
|
+
"target_pane_matched".to_string(),
|
|
1958
|
+
json_opt_bool(match (inject_pane, resolved_pane) {
|
|
1959
|
+
(Some(left), Some(right)) => Some(left == right),
|
|
1960
|
+
_ => None,
|
|
1961
|
+
}),
|
|
1962
|
+
);
|
|
1963
|
+
let inject_endpoint = metadata.and_then(|meta| meta.tmux_endpoint.as_deref());
|
|
1964
|
+
// metadata.tmux_endpoint is the transport used to resolve; compare to
|
|
1965
|
+
// itself is tautological. Equality vs inject target id is the pane check
|
|
1966
|
+
// above. Socket equality is recorded when both sides exist on metadata.
|
|
1967
|
+
obj.insert(
|
|
1968
|
+
"resolved_tmux_endpoint_present".to_string(),
|
|
1969
|
+
serde_json::Value::Bool(inject_endpoint.is_some()),
|
|
1970
|
+
);
|
|
1971
|
+
let diag = report.submit_diagnostics.as_ref();
|
|
1972
|
+
obj.insert(
|
|
1973
|
+
"token_seen_before_paste".to_string(),
|
|
1974
|
+
json_opt_bool(diag.and_then(|d| d.token_seen_before_paste)),
|
|
1975
|
+
);
|
|
1976
|
+
obj.insert(
|
|
1977
|
+
"token_seen_after_paste".to_string(),
|
|
1978
|
+
json_opt_bool(diag.and_then(|d| d.token_seen_after_paste)),
|
|
1979
|
+
);
|
|
1980
|
+
obj.insert(
|
|
1981
|
+
"token_seen_after_enter".to_string(),
|
|
1982
|
+
json_opt_bool(diag.and_then(|d| d.token_seen_after_enter)),
|
|
1983
|
+
);
|
|
1984
|
+
obj.insert(
|
|
1985
|
+
"capture_ok_count".to_string(),
|
|
1986
|
+
serde_json::json!(diag.map(|d| d.capture_ok_count).unwrap_or(0)),
|
|
1987
|
+
);
|
|
1988
|
+
obj.insert(
|
|
1989
|
+
"capture_err_count".to_string(),
|
|
1990
|
+
serde_json::json!(diag.map(|d| d.capture_err_count).unwrap_or(0)),
|
|
1991
|
+
);
|
|
1992
|
+
obj.insert(
|
|
1993
|
+
"capture_sample_count".to_string(),
|
|
1994
|
+
serde_json::json!(diag.map(|d| d.capture_sample_count).unwrap_or(0)),
|
|
1995
|
+
);
|
|
1996
|
+
obj.insert(
|
|
1997
|
+
"last_capture_outcome".to_string(),
|
|
1998
|
+
serde_json::json!(diag
|
|
1999
|
+
.map(|d| d.last_capture_outcome.as_str())
|
|
2000
|
+
.unwrap_or(CaptureSampleOutcome::NotAttempted.as_str())),
|
|
2001
|
+
);
|
|
2002
|
+
obj.insert(
|
|
2003
|
+
"pane_command_basename".to_string(),
|
|
2004
|
+
diag.and_then(|d| d.pane_command_basename.as_deref())
|
|
2005
|
+
.map(|name| serde_json::json!(name))
|
|
2006
|
+
.unwrap_or(serde_json::Value::Null),
|
|
2007
|
+
);
|
|
2008
|
+
obj.insert(
|
|
2009
|
+
"pane_command_query".to_string(),
|
|
2010
|
+
serde_json::json!(diag
|
|
2011
|
+
.map(|d| d.pane_command_query.as_str())
|
|
2012
|
+
.unwrap_or(QueryOutcome::Unavailable.as_str())),
|
|
2013
|
+
);
|
|
2014
|
+
obj.insert(
|
|
2015
|
+
"input_surface".to_string(),
|
|
2016
|
+
serde_json::json!(diag
|
|
2017
|
+
.map(|d| d.input_surface.as_str())
|
|
2018
|
+
.unwrap_or(InputSurfaceProbe::Unavailable.as_str())),
|
|
2019
|
+
);
|
|
2020
|
+
obj.insert(
|
|
2021
|
+
"diag_target_pane_id".to_string(),
|
|
2022
|
+
diag.and_then(|d| d.target_pane_id.as_deref())
|
|
2023
|
+
.map(|pane| serde_json::json!(pane))
|
|
2024
|
+
.unwrap_or(serde_json::Value::Null),
|
|
2025
|
+
);
|
|
2026
|
+
obj.insert(
|
|
2027
|
+
"diag_target_pane_query_matched".to_string(),
|
|
2028
|
+
json_opt_bool(diag.and_then(|d| d.target_pane_query_matched)),
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
1917
2032
|
#[allow(clippy::too_many_arguments)]
|
|
1918
2033
|
fn emit_send_failed_exhausted(
|
|
1919
2034
|
workspace: &Path,
|
|
@@ -1944,21 +2059,22 @@ fn emit_send_failed_exhausted(
|
|
|
1944
2059
|
.and_then(|r| r.submit_diagnostics.as_ref())
|
|
1945
2060
|
.and_then(|d| d.attempts_detail.last())
|
|
1946
2061
|
.map(|a| a.pane_tail_excerpt.clone());
|
|
1947
|
-
|
|
1948
|
-
"
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2062
|
+
let mut failed = serde_json::json!({
|
|
2063
|
+
"message_id": message_id,
|
|
2064
|
+
"recipient": recipient,
|
|
2065
|
+
"attempts": attempts,
|
|
2066
|
+
"max_attempts": SEND_RETRY_MAX_ATTEMPTS,
|
|
2067
|
+
"reason": failure_reason,
|
|
2068
|
+
"verification": verification,
|
|
2069
|
+
"submit_attempts_detail": submit_attempts_detail,
|
|
2070
|
+
"total_elapsed_ms": total_elapsed_ms,
|
|
2071
|
+
"last_matched_literal": last_matched_literal,
|
|
2072
|
+
"last_pane_tail_excerpt": last_pane_tail_excerpt,
|
|
2073
|
+
});
|
|
2074
|
+
if let Some(report) = inject_report {
|
|
2075
|
+
merge_inject_readback_fields(&mut failed, report, None, None);
|
|
2076
|
+
}
|
|
2077
|
+
event_log.write("send.failed", failed)?;
|
|
1962
2078
|
let content = format!(
|
|
1963
2079
|
"send.failed\nerror: send to {recipient} failed with {failure_reason} after {attempts}/{SEND_RETRY_MAX_ATTEMPTS} attempts\naction: inspect the target pane and retry the send\nlog: .team/logs/events.jsonl"
|
|
1964
2080
|
);
|
|
@@ -2866,7 +2982,9 @@ fn project_state_for_owner_team(
|
|
|
2866
2982
|
return Ok(OwnerTeamProjection::Refused(outcome));
|
|
2867
2983
|
}
|
|
2868
2984
|
};
|
|
2869
|
-
if top_level_state_matches_owner_team(fallback, &canonical_team)
|
|
2985
|
+
if top_level_state_matches_owner_team(fallback, &canonical_team)
|
|
2986
|
+
&& state_has_no_team_entries(fallback)
|
|
2987
|
+
{
|
|
2870
2988
|
let mut state = fallback.clone();
|
|
2871
2989
|
carry_top_level_leader_binding(&mut state, &raw);
|
|
2872
2990
|
return Ok(OwnerTeamProjection::Projected {
|
|
@@ -2874,7 +2992,8 @@ fn project_state_for_owner_team(
|
|
|
2874
2992
|
canonical_team,
|
|
2875
2993
|
});
|
|
2876
2994
|
}
|
|
2877
|
-
if top_level_state_matches_owner_team(&raw, &canonical_team)
|
|
2995
|
+
if top_level_state_matches_owner_team(&raw, &canonical_team) && state_has_no_team_entries(&raw)
|
|
2996
|
+
{
|
|
2878
2997
|
return Ok(OwnerTeamProjection::Projected {
|
|
2879
2998
|
state: raw,
|
|
2880
2999
|
canonical_team,
|
|
@@ -2999,16 +3118,31 @@ fn project_state_for_owner_team_value(
|
|
|
2999
3118
|
None
|
|
3000
3119
|
}
|
|
3001
3120
|
|
|
3002
|
-
fn top_level_state_matches_owner_team(state: &serde_json::Value, team: &str) -> bool {
|
|
3003
|
-
state
|
|
3121
|
+
pub(crate) fn top_level_state_matches_owner_team(state: &serde_json::Value, team: &str) -> bool {
|
|
3122
|
+
if !state.is_object() {
|
|
3123
|
+
return false;
|
|
3124
|
+
}
|
|
3125
|
+
if state
|
|
3004
3126
|
.get("active_team_key")
|
|
3005
3127
|
.and_then(serde_json::Value::as_str)
|
|
3006
3128
|
.is_some_and(|value| value == team)
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3129
|
+
{
|
|
3130
|
+
return true;
|
|
3131
|
+
}
|
|
3132
|
+
if state
|
|
3133
|
+
.get("session_name")
|
|
3134
|
+
.and_then(serde_json::Value::as_str)
|
|
3135
|
+
.is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
|
|
3136
|
+
{
|
|
3137
|
+
return true;
|
|
3138
|
+
}
|
|
3139
|
+
let has_explicit_key = ["team_key", "team_dir", "spec_path"].iter().any(|field| {
|
|
3140
|
+
state
|
|
3141
|
+
.get(*field)
|
|
3010
3142
|
.and_then(serde_json::Value::as_str)
|
|
3011
|
-
.is_some_and(|
|
|
3143
|
+
.is_some_and(|value| !value.is_empty())
|
|
3144
|
+
});
|
|
3145
|
+
has_explicit_key && crate::state::projection::team_state_key(state) == team
|
|
3012
3146
|
}
|
|
3013
3147
|
|
|
3014
3148
|
/// `retry_injection_after_trust_auto_answer` (`trust_auto_answer.py`):leader 路径 trust 应答
|
|
@@ -3248,4 +3382,59 @@ mod paste_floor_tests {
|
|
|
3248
3382
|
Duration::ZERO
|
|
3249
3383
|
);
|
|
3250
3384
|
}
|
|
3385
|
+
|
|
3386
|
+
fn empty_report() -> InjectReport {
|
|
3387
|
+
InjectReport {
|
|
3388
|
+
stage_reached: crate::transport::InjectStage::Submit,
|
|
3389
|
+
inject_verification: InjectVerification::CaptureMissingToken,
|
|
3390
|
+
submit_verification: SubmitVerification::SubmitConsumptionUnverified,
|
|
3391
|
+
turn_verification: crate::transport::TurnVerification::NotYetObserved,
|
|
3392
|
+
attempts: 1,
|
|
3393
|
+
submit_diagnostics: Some(crate::transport::SubmitDiagnostics {
|
|
3394
|
+
last_capture_outcome: CaptureSampleOutcome::Failed,
|
|
3395
|
+
capture_err_count: 1,
|
|
3396
|
+
capture_sample_count: 1,
|
|
3397
|
+
pane_command_basename: Some("grok".to_string()),
|
|
3398
|
+
pane_command_query: QueryOutcome::Observed,
|
|
3399
|
+
input_surface: InputSurfaceProbe::Input,
|
|
3400
|
+
target_pane_id: Some("%9".to_string()),
|
|
3401
|
+
target_pane_query_matched: Some(true),
|
|
3402
|
+
token_seen_after_paste: None,
|
|
3403
|
+
..crate::transport::SubmitDiagnostics::default()
|
|
3404
|
+
}),
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
#[test]
|
|
3409
|
+
fn inject_readback_fields_are_safe_scalars_and_keep_attempts_array() {
|
|
3410
|
+
let report = empty_report();
|
|
3411
|
+
let mut event = serde_json::json!({
|
|
3412
|
+
"message_id": "msg_x",
|
|
3413
|
+
"submit_attempts_detail": [{"matched": false}],
|
|
3414
|
+
});
|
|
3415
|
+
merge_inject_readback_fields(
|
|
3416
|
+
&mut event,
|
|
3417
|
+
&report,
|
|
3418
|
+
Some(&Target::Pane(PaneId::new("%9"))),
|
|
3419
|
+
None,
|
|
3420
|
+
);
|
|
3421
|
+
assert_eq!(event["last_capture_outcome"], serde_json::json!("failed"));
|
|
3422
|
+
assert_eq!(event["token_seen_after_paste"], serde_json::Value::Null);
|
|
3423
|
+
assert_eq!(event["pane_command_basename"], serde_json::json!("grok"));
|
|
3424
|
+
assert_eq!(event["input_surface"], serde_json::json!("input"));
|
|
3425
|
+
assert_eq!(event["inject_target_pane_id"], serde_json::json!("%9"));
|
|
3426
|
+
assert!(event["submit_attempts_detail"].is_array());
|
|
3427
|
+
assert!(event.get("pane_tail_excerpt").is_none());
|
|
3428
|
+
let dump = event.to_string();
|
|
3429
|
+
assert!(
|
|
3430
|
+
!dump.contains("--help") && !dump.contains("argv"),
|
|
3431
|
+
"readback fields must not leak argv: {dump}"
|
|
3432
|
+
);
|
|
3433
|
+
assert_eq!(
|
|
3434
|
+
report.inject_verification,
|
|
3435
|
+
InjectVerification::CaptureMissingToken
|
|
3436
|
+
);
|
|
3437
|
+
assert!(!inject_submit_verified(&report));
|
|
3438
|
+
assert!(!pane_readback_verified(&report));
|
|
3439
|
+
}
|
|
3251
3440
|
}
|
|
@@ -7,6 +7,11 @@ use crate::codex_app_server::AppServerBinding;
|
|
|
7
7
|
use crate::model::pane_authority_refusal::PaneWorkspaceMismatchFacts;
|
|
8
8
|
use crate::transport::{PaneInfo, Transport};
|
|
9
9
|
|
|
10
|
+
/// Scope authority written by a fresh quick-start caller binding. It is distinct
|
|
11
|
+
/// from an operator-initiated explicit claim, but uses the same pane-instance
|
|
12
|
+
/// nonce and exact workspace checks at delivery.
|
|
13
|
+
pub(crate) const FRESH_CALLER_SCOPE_AUTHORITY: &str = "fresh_caller";
|
|
14
|
+
|
|
10
15
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
11
16
|
pub enum LiveLeaderChannel {
|
|
12
17
|
DirectTmux(DirectTmuxLeaderChannel),
|
|
@@ -117,7 +122,7 @@ pub fn resolve_live_leader_channel(
|
|
|
117
122
|
};
|
|
118
123
|
if let Some(observed_pane_workspace) = observed.current_path.clone() {
|
|
119
124
|
if !path_is_in_workspace(&observed_pane_workspace, workspace)
|
|
120
|
-
&& !
|
|
125
|
+
&& !scoped_authority_matches(workspace, receiver, &observed)
|
|
121
126
|
{
|
|
122
127
|
return LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch(
|
|
123
128
|
PaneWorkspaceMismatchFacts {
|
|
@@ -138,12 +143,16 @@ pub fn resolve_live_leader_channel(
|
|
|
138
143
|
}))
|
|
139
144
|
}
|
|
140
145
|
|
|
141
|
-
fn
|
|
146
|
+
fn scoped_authority_matches(
|
|
142
147
|
workspace: &Path,
|
|
143
148
|
receiver: &Value,
|
|
144
149
|
observed: &PaneInfo,
|
|
145
150
|
) -> bool {
|
|
146
|
-
|
|
151
|
+
let authority = string_field(receiver, "scope_authority");
|
|
152
|
+
if !matches!(
|
|
153
|
+
authority,
|
|
154
|
+
Some("explicit_claim") | Some(FRESH_CALLER_SCOPE_AUTHORITY)
|
|
155
|
+
) {
|
|
147
156
|
return false;
|
|
148
157
|
}
|
|
149
158
|
let Some(authorized_workspace) = string_field(receiver, "authorized_team_workspace") else {
|