@team-agent/installer 0.5.41 → 0.5.43
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 +129 -54
- package/crates/team-agent/src/cli/diagnose.rs +1 -2
- package/crates/team-agent/src/cli/emit.rs +1 -7
- package/crates/team-agent/src/cli/helpers.rs +3 -1
- package/crates/team-agent/src/cli/leader.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +27 -7
- package/crates/team-agent/src/cli/named_address.rs +14 -5
- package/crates/team-agent/src/cli/profile.rs +19 -7
- package/crates/team-agent/src/cli/send.rs +9 -3
- package/crates/team-agent/src/cli/status.rs +8 -30
- package/crates/team-agent/src/cli/status_port.rs +1341 -1272
- package/crates/team-agent/src/cli/tests/base.rs +738 -660
- package/crates/team-agent/src/cli/tests/compile.rs +45 -18
- package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
- package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
- package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
- package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
- package/crates/team-agent/src/cli/tests/mod.rs +17 -7
- package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
- package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
- package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
- package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
- package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
- package/crates/team-agent/src/codex_app_server.rs +2 -5
- package/crates/team-agent/src/compiler/tests.rs +139 -33
- package/crates/team-agent/src/compiler.rs +55 -22
- package/crates/team-agent/src/conpty/backend.rs +23 -33
- package/crates/team-agent/src/coordinator/backoff.rs +2 -7
- package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
- package/crates/team-agent/src/coordinator/health.rs +46 -31
- package/crates/team-agent/src/coordinator/mod.rs +3 -3
- package/crates/team-agent/src/coordinator/orphan.rs +22 -10
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +47 -52
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
- package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
- package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
- package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
- package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
- package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
- package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
- package/crates/team-agent/src/coordinator/tick.rs +19 -1
- package/crates/team-agent/src/db/message_store.rs +138 -30
- package/crates/team-agent/src/db/migration.rs +249 -61
- package/crates/team-agent/src/db/schema.rs +303 -82
- package/crates/team-agent/src/diagnose/comms.rs +9 -2
- package/crates/team-agent/src/diagnose/mod.rs +1 -3
- package/crates/team-agent/src/diagnose/orphans.rs +79 -61
- package/crates/team-agent/src/event_log.rs +70 -16
- package/crates/team-agent/src/layout/manager.rs +15 -4
- package/crates/team-agent/src/layout/mod.rs +4 -4
- package/crates/team-agent/src/layout/overlay.rs +10 -3
- package/crates/team-agent/src/layout/placement.rs +5 -1
- package/crates/team-agent/src/layout/recovery.rs +4 -2
- package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
- package/crates/team-agent/src/layout/sessions.rs +17 -9
- package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
- package/crates/team-agent/src/layout/worker_env.rs +52 -19
- package/crates/team-agent/src/leader/helpers.rs +7 -1
- package/crates/team-agent/src/leader/lease.rs +195 -82
- package/crates/team-agent/src/leader/owner_bind.rs +55 -22
- package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
- package/crates/team-agent/src/leader/rediscover.rs +74 -25
- package/crates/team-agent/src/leader/start.rs +75 -54
- package/crates/team-agent/src/leader/takeover.rs +46 -11
- package/crates/team-agent/src/leader/tests/basics.rs +320 -167
- package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
- package/crates/team-agent/src/leader/tests/identity.rs +428 -356
- package/crates/team-agent/src/leader/tests/idle.rs +285 -254
- package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
- package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
- package/crates/team-agent/src/leader/tests/mod.rs +115 -99
- package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
- package/crates/team-agent/src/lib.rs +4 -4
- package/crates/team-agent/src/lifecycle/display.rs +7 -3
- package/crates/team-agent/src/lifecycle/launch.rs +6 -1
- package/crates/team-agent/src/lifecycle/mod.rs +9 -1
- package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
- package/crates/team-agent/src/lifecycle/tests.rs +2 -2
- package/crates/team-agent/src/main.rs +4 -4
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
- package/crates/team-agent/src/mcp_server/mod.rs +11 -2
- package/crates/team-agent/src/mcp_server/types.rs +14 -3
- package/crates/team-agent/src/mcp_server/wire.rs +230 -68
- package/crates/team-agent/src/messaging/delivery.rs +20 -18
- package/crates/team-agent/src/messaging/helpers.rs +25 -4
- package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
- package/crates/team-agent/src/messaging/mod.rs +2 -3
- package/crates/team-agent/src/messaging/selftest.rs +46 -26
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
- package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
- package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
- package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
- package/crates/team-agent/src/messaging/trust.rs +25 -2
- package/crates/team-agent/src/messaging/watchers.rs +37 -9
- package/crates/team-agent/src/model/enums.rs +65 -16
- package/crates/team-agent/src/model/ids.rs +12 -3
- package/crates/team-agent/src/model/paths.rs +28 -7
- package/crates/team-agent/src/model/permissions.rs +176 -33
- package/crates/team-agent/src/model/routing.rs +66 -20
- package/crates/team-agent/src/model/spec.rs +365 -69
- package/crates/team-agent/src/model/task_graph.rs +36 -9
- package/crates/team-agent/src/model/yaml/tests.rs +24 -6
- package/crates/team-agent/src/model/yaml.rs +7 -6
- package/crates/team-agent/src/packaging/install.rs +23 -9
- package/crates/team-agent/src/packaging/migrate.rs +5 -7
- package/crates/team-agent/src/packaging/mod.rs +9 -1
- package/crates/team-agent/src/packaging/repair.rs +13 -6
- package/crates/team-agent/src/packaging/tests.rs +63 -16
- package/crates/team-agent/src/packaging/types.rs +22 -7
- package/crates/team-agent/src/platform/argv.rs +4 -1
- package/crates/team-agent/src/platform/file_lock.rs +22 -8
- package/crates/team-agent/src/platform/process.rs +21 -22
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
- package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
- package/crates/team-agent/src/provider/classify.rs +127 -42
- package/crates/team-agent/src/provider/faults.rs +17 -5
- package/crates/team-agent/src/provider/helpers.rs +6 -5
- package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
- package/crates/team-agent/src/state/repository.rs +27 -14
- package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
- package/crates/team-agent/src/tmux_backend.rs +80 -44
- package/crates/team-agent/src/topology.rs +27 -20
- package/crates/team-agent/src/transport/test_support.rs +20 -23
- package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
- package/crates/team-agent/src/transport/tests/mod.rs +178 -187
- package/crates/team-agent/src/transport/tests/wire.rs +561 -525
- package/crates/team-agent/src/transport.rs +12 -17
- package/crates/team-agent/src/transport_factory.rs +29 -14
- package/package.json +4 -4
|
@@ -53,9 +53,7 @@ pub enum TerminationOutcome {
|
|
|
53
53
|
/// Windows downgraded `TerminateGraceful` to `TerminateProcess`
|
|
54
54
|
/// (no grace period). Includes a machine-readable reason so
|
|
55
55
|
/// callers can log it in the audit event.
|
|
56
|
-
ForceOnly {
|
|
57
|
-
reason: &'static str,
|
|
58
|
-
},
|
|
56
|
+
ForceOnly { reason: &'static str },
|
|
59
57
|
/// The pid/group was already gone by the time the call resolved.
|
|
60
58
|
/// Not an error.
|
|
61
59
|
AlreadyGone,
|
|
@@ -166,10 +164,7 @@ mod unix_impl {
|
|
|
166
164
|
/// downgrade path); the `AlreadyGone` case is preserved when
|
|
167
165
|
/// `kill()` returns ESRCH (the target was reaped between check
|
|
168
166
|
/// and send).
|
|
169
|
-
pub fn terminate_pid(
|
|
170
|
-
pid: u32,
|
|
171
|
-
kind: SignalKind,
|
|
172
|
-
) -> Result<TerminationOutcome, io::Error> {
|
|
167
|
+
pub fn terminate_pid(pid: u32, kind: SignalKind) -> Result<TerminationOutcome, io::Error> {
|
|
173
168
|
let signal = match kind {
|
|
174
169
|
SignalKind::TerminateGraceful => libc::SIGTERM,
|
|
175
170
|
SignalKind::TerminateForce => libc::SIGKILL,
|
|
@@ -275,12 +270,16 @@ mod windows_impl {
|
|
|
275
270
|
while result.is_ok() {
|
|
276
271
|
if entry.th32ProcessID == my_pid {
|
|
277
272
|
let ppid = entry.th32ParentProcessID;
|
|
278
|
-
unsafe {
|
|
273
|
+
unsafe {
|
|
274
|
+
let _ = windows::Win32::Foundation::CloseHandle(snapshot);
|
|
275
|
+
};
|
|
279
276
|
return Some(ppid);
|
|
280
277
|
}
|
|
281
278
|
result = unsafe { Process32NextW(snapshot, &mut entry) };
|
|
282
279
|
}
|
|
283
|
-
unsafe {
|
|
280
|
+
unsafe {
|
|
281
|
+
let _ = windows::Win32::Foundation::CloseHandle(snapshot);
|
|
282
|
+
};
|
|
284
283
|
None
|
|
285
284
|
}
|
|
286
285
|
|
|
@@ -327,7 +326,9 @@ mod windows_impl {
|
|
|
327
326
|
};
|
|
328
327
|
let mut exit_code: u32 = 0;
|
|
329
328
|
let result = unsafe { GetExitCodeProcess(handle, &mut exit_code) };
|
|
330
|
-
unsafe {
|
|
329
|
+
unsafe {
|
|
330
|
+
let _ = CloseHandle(handle);
|
|
331
|
+
};
|
|
331
332
|
result.map_err(|e| io::Error::from_raw_os_error(e.code().0 as i32))?;
|
|
332
333
|
// `STILL_ACTIVE` (0x103) — the process is still running.
|
|
333
334
|
if exit_code == STILL_ACTIVE.0 as u32 {
|
|
@@ -358,14 +359,9 @@ mod windows_impl {
|
|
|
358
359
|
Ok(Vec::new())
|
|
359
360
|
}
|
|
360
361
|
|
|
361
|
-
pub fn terminate_pid(
|
|
362
|
-
pid: u32,
|
|
363
|
-
kind: SignalKind,
|
|
364
|
-
) -> Result<TerminationOutcome, io::Error> {
|
|
362
|
+
pub fn terminate_pid(pid: u32, kind: SignalKind) -> Result<TerminationOutcome, io::Error> {
|
|
365
363
|
use windows::Win32::Foundation::CloseHandle;
|
|
366
|
-
use windows::Win32::System::Threading::{
|
|
367
|
-
OpenProcess, TerminateProcess, PROCESS_TERMINATE,
|
|
368
|
-
};
|
|
364
|
+
use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
|
|
369
365
|
// CR C-6 anchor: Windows has no SIGTERM analogue for a
|
|
370
366
|
// non-console child. `TerminateGraceful` cannot be honored;
|
|
371
367
|
// we still perform the physical `TerminateProcess` (design
|
|
@@ -394,7 +390,9 @@ mod windows_impl {
|
|
|
394
390
|
}
|
|
395
391
|
};
|
|
396
392
|
let result = unsafe { TerminateProcess(handle, 1) };
|
|
397
|
-
unsafe {
|
|
393
|
+
unsafe {
|
|
394
|
+
let _ = CloseHandle(handle);
|
|
395
|
+
};
|
|
398
396
|
result.map_err(|e| io::Error::from_raw_os_error(e.code().0 as i32))?;
|
|
399
397
|
Ok(match kind {
|
|
400
398
|
SignalKind::TerminateGraceful => TerminationOutcome::ForceOnly {
|
|
@@ -444,7 +442,10 @@ mod tests {
|
|
|
444
442
|
// Windows (Toolhelp32 snapshot) return the process's parent
|
|
445
443
|
// pid. A `None` here on either platform is a regression.
|
|
446
444
|
let ppid = current_parent_pid();
|
|
447
|
-
assert!(
|
|
445
|
+
assert!(
|
|
446
|
+
ppid.is_some(),
|
|
447
|
+
"current_parent_pid must return Some on both unix and windows"
|
|
448
|
+
);
|
|
448
449
|
assert!(ppid.unwrap() > 0);
|
|
449
450
|
}
|
|
450
451
|
|
|
@@ -544,9 +545,7 @@ mod tests {
|
|
|
544
545
|
// pattern-match without cfg.
|
|
545
546
|
assert_ne!(SignalKind::TerminateGraceful, SignalKind::TerminateForce);
|
|
546
547
|
let requested = TerminationOutcome::Requested;
|
|
547
|
-
let force_only = TerminationOutcome::ForceOnly {
|
|
548
|
-
reason: "test",
|
|
549
|
-
};
|
|
548
|
+
let force_only = TerminationOutcome::ForceOnly { reason: "test" };
|
|
550
549
|
let already_gone = TerminationOutcome::AlreadyGone;
|
|
551
550
|
assert_ne!(requested, force_only);
|
|
552
551
|
assert_ne!(requested, already_gone);
|
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
//! helpers now live under `provider/session_scan/claude.rs`.
|
|
9
9
|
|
|
10
10
|
use crate::model::enums::AuthMode;
|
|
11
|
-
use crate::provider::adapter::{
|
|
12
|
-
next_session_token, prompt_needs_native_mcp, BasicProviderAdapter,
|
|
13
|
-
};
|
|
11
|
+
use crate::provider::adapter::{next_session_token, prompt_needs_native_mcp, BasicProviderAdapter};
|
|
14
12
|
use crate::provider::{McpConfig, ProviderAdapter, ProviderError};
|
|
15
13
|
|
|
16
14
|
pub(crate) fn claude_launch_command(
|
|
@@ -49,7 +49,11 @@ pub fn active_approval_control_index(lines: &[&str]) -> Option<usize> {
|
|
|
49
49
|
.rev()
|
|
50
50
|
.find(|(_, line)| is_control_line(&clean_line(line)))
|
|
51
51
|
.map(|(idx, _)| idx)?;
|
|
52
|
-
if lines
|
|
52
|
+
if lines
|
|
53
|
+
.iter()
|
|
54
|
+
.skip(idx + 1)
|
|
55
|
+
.any(|line| !clean_line(line).is_empty())
|
|
56
|
+
{
|
|
53
57
|
return None;
|
|
54
58
|
}
|
|
55
59
|
Some(idx)
|
|
@@ -61,15 +65,39 @@ pub fn extract_approval_prompt(agent_id: &str, capture: &str) -> Option<Approval
|
|
|
61
65
|
|
|
62
66
|
if let Some((matched_idx, prompt, server, tool)) = explicit_mcp_prompt(&lines, control_idx) {
|
|
63
67
|
let choices = approval_choices_between(&lines, matched_idx, control_idx);
|
|
64
|
-
return Some(approval(
|
|
68
|
+
return Some(approval(
|
|
69
|
+
agent_id,
|
|
70
|
+
ApprovalKind::McpTool,
|
|
71
|
+
prompt,
|
|
72
|
+
choices,
|
|
73
|
+
Some(server),
|
|
74
|
+
Some(tool),
|
|
75
|
+
None,
|
|
76
|
+
));
|
|
65
77
|
}
|
|
66
78
|
if let Some((matched_idx, prompt, server, tool)) = short_mcp_prompt(&lines, control_idx) {
|
|
67
79
|
let choices = approval_choices_between(&lines, matched_idx, control_idx);
|
|
68
|
-
return Some(approval(
|
|
80
|
+
return Some(approval(
|
|
81
|
+
agent_id,
|
|
82
|
+
ApprovalKind::McpTool,
|
|
83
|
+
prompt,
|
|
84
|
+
choices,
|
|
85
|
+
Some(server),
|
|
86
|
+
Some(tool),
|
|
87
|
+
None,
|
|
88
|
+
));
|
|
69
89
|
}
|
|
70
90
|
if let Some((matched_idx, prompt, command)) = command_prompt(&lines, control_idx) {
|
|
71
91
|
let choices = approval_choices_between(&lines, matched_idx, control_idx);
|
|
72
|
-
return Some(approval(
|
|
92
|
+
return Some(approval(
|
|
93
|
+
agent_id,
|
|
94
|
+
ApprovalKind::Command,
|
|
95
|
+
prompt,
|
|
96
|
+
choices,
|
|
97
|
+
None,
|
|
98
|
+
None,
|
|
99
|
+
Some(command),
|
|
100
|
+
));
|
|
73
101
|
}
|
|
74
102
|
let choices = approval_choices_between(&lines, 0, control_idx);
|
|
75
103
|
Some(approval(
|
|
@@ -84,7 +112,11 @@ pub fn extract_approval_prompt(agent_id: &str, capture: &str) -> Option<Approval
|
|
|
84
112
|
}
|
|
85
113
|
|
|
86
114
|
pub fn choose_internal_mcp_approval_choice(prompt: &ApprovalPrompt) -> String {
|
|
87
|
-
if prompt
|
|
115
|
+
if prompt
|
|
116
|
+
.choices
|
|
117
|
+
.iter()
|
|
118
|
+
.any(|choice| choice == "Allow for this session")
|
|
119
|
+
{
|
|
88
120
|
return "Allow for this session".to_string();
|
|
89
121
|
}
|
|
90
122
|
for choice in &prompt.choices {
|
|
@@ -100,8 +132,16 @@ pub fn choose_internal_mcp_approval_choice(prompt: &ApprovalPrompt) -> String {
|
|
|
100
132
|
"Allow for this session".to_string()
|
|
101
133
|
}
|
|
102
134
|
|
|
103
|
-
pub fn approval_choice_keys(
|
|
104
|
-
|
|
135
|
+
pub fn approval_choice_keys(
|
|
136
|
+
prompt: &ApprovalPrompt,
|
|
137
|
+
capture: &str,
|
|
138
|
+
target_choice: &str,
|
|
139
|
+
) -> Vec<String> {
|
|
140
|
+
let Some(target_index) = prompt
|
|
141
|
+
.choices
|
|
142
|
+
.iter()
|
|
143
|
+
.position(|choice| choice == target_choice)
|
|
144
|
+
else {
|
|
105
145
|
return vec!["Down".to_string(), "Enter".to_string()];
|
|
106
146
|
};
|
|
107
147
|
let Some(active) = active_choice_index(capture) else {
|
|
@@ -145,7 +185,10 @@ fn is_control_line(line: &str) -> bool {
|
|
|
145
185
|
|| (lower.contains("esc to cancel") && lower.contains("tab to amend"))
|
|
146
186
|
}
|
|
147
187
|
|
|
148
|
-
fn explicit_mcp_prompt(
|
|
188
|
+
fn explicit_mcp_prompt(
|
|
189
|
+
lines: &[&str],
|
|
190
|
+
control_idx: usize,
|
|
191
|
+
) -> Option<(usize, String, String, String)> {
|
|
149
192
|
for (idx, raw) in lines.iter().enumerate().take(control_idx).rev() {
|
|
150
193
|
let line = clean_line(raw);
|
|
151
194
|
let prefix = "Allow the ";
|
|
@@ -171,10 +214,20 @@ fn short_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, String
|
|
|
171
214
|
continue;
|
|
172
215
|
}
|
|
173
216
|
if let Some(tool) = team_orchestrator_tool(&line, '-') {
|
|
174
|
-
return Some((
|
|
217
|
+
return Some((
|
|
218
|
+
idx,
|
|
219
|
+
format!("team_orchestrator - {tool}"),
|
|
220
|
+
"team_orchestrator".to_string(),
|
|
221
|
+
tool,
|
|
222
|
+
));
|
|
175
223
|
}
|
|
176
224
|
if let Some(tool) = team_orchestrator_tool(&line, '.') {
|
|
177
|
-
return Some((
|
|
225
|
+
return Some((
|
|
226
|
+
idx,
|
|
227
|
+
format!("team_orchestrator - {tool}"),
|
|
228
|
+
"team_orchestrator".to_string(),
|
|
229
|
+
tool,
|
|
230
|
+
));
|
|
178
231
|
}
|
|
179
232
|
}
|
|
180
233
|
None
|
|
@@ -192,7 +245,12 @@ fn team_orchestrator_tool(line: &str, separator: char) -> Option<String> {
|
|
|
192
245
|
.chars()
|
|
193
246
|
.take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
|
|
194
247
|
.collect();
|
|
195
|
-
if tool.is_empty()
|
|
248
|
+
if tool.is_empty()
|
|
249
|
+
|| !tool
|
|
250
|
+
.chars()
|
|
251
|
+
.next()
|
|
252
|
+
.is_some_and(|ch| ch.is_ascii_alphabetic() || ch == '_')
|
|
253
|
+
{
|
|
196
254
|
None
|
|
197
255
|
} else {
|
|
198
256
|
Some(tool)
|
|
@@ -300,7 +358,10 @@ mod tests {
|
|
|
300
358
|
assert_eq!(prompt.kind, ApprovalKind::McpTool);
|
|
301
359
|
assert_eq!(prompt.server.as_deref(), Some("team_orchestrator"));
|
|
302
360
|
assert_eq!(prompt.tool.as_deref(), Some("send_message"));
|
|
303
|
-
assert_eq!(
|
|
361
|
+
assert_eq!(
|
|
362
|
+
prompt.prompt,
|
|
363
|
+
"Allow the team_orchestrator MCP server to run tool \"send_message\"?"
|
|
364
|
+
);
|
|
304
365
|
assert_eq!(prompt.choices, vec!["Allow for this session", "Deny"]);
|
|
305
366
|
}
|
|
306
367
|
|
|
@@ -338,7 +399,10 @@ mod tests {
|
|
|
338
399
|
.unwrap();
|
|
339
400
|
assert_eq!(prompt.kind, ApprovalKind::Command);
|
|
340
401
|
assert_eq!(prompt.command.as_deref(), Some("Bash(echo hi)"));
|
|
341
|
-
assert_eq!(
|
|
402
|
+
assert_eq!(
|
|
403
|
+
prompt.prompt,
|
|
404
|
+
"Would you like to run the following command?"
|
|
405
|
+
);
|
|
342
406
|
}
|
|
343
407
|
|
|
344
408
|
#[test]
|
|
@@ -348,7 +412,10 @@ mod tests {
|
|
|
348
412
|
None
|
|
349
413
|
);
|
|
350
414
|
assert_eq!(
|
|
351
|
-
extract_approval_prompt(
|
|
415
|
+
extract_approval_prompt(
|
|
416
|
+
"agent-x",
|
|
417
|
+
"MCP tool: team_orchestrator - send_message requires approval\n 1. Allow\n"
|
|
418
|
+
),
|
|
352
419
|
None
|
|
353
420
|
);
|
|
354
421
|
}
|
|
@@ -381,10 +448,20 @@ mod tests {
|
|
|
381
448
|
None,
|
|
382
449
|
None,
|
|
383
450
|
);
|
|
384
|
-
assert_eq!(
|
|
451
|
+
assert_eq!(
|
|
452
|
+
choose_internal_mcp_approval_choice(&prompt),
|
|
453
|
+
"Allow for this session"
|
|
454
|
+
);
|
|
385
455
|
prompt.choices = vec!["Maybe".to_string()];
|
|
386
|
-
assert_eq!(
|
|
387
|
-
|
|
456
|
+
assert_eq!(
|
|
457
|
+
choose_internal_mcp_approval_choice(&prompt),
|
|
458
|
+
"Allow for this session"
|
|
459
|
+
);
|
|
460
|
+
prompt.choices = vec![
|
|
461
|
+
"Maybe".to_string(),
|
|
462
|
+
"Yes, and don't ask again for this command".to_string(),
|
|
463
|
+
"Allow".to_string(),
|
|
464
|
+
];
|
|
388
465
|
assert_eq!(
|
|
389
466
|
choose_internal_mcp_approval_choice(&prompt),
|
|
390
467
|
"Yes, and don't ask again for this command"
|
|
@@ -402,11 +479,26 @@ mod tests {
|
|
|
402
479
|
None,
|
|
403
480
|
None,
|
|
404
481
|
);
|
|
405
|
-
assert_eq!(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
assert_eq!(
|
|
482
|
+
assert_eq!(
|
|
483
|
+
approval_choice_keys(&prompt, "❯ 1. Allow\n", "Missing"),
|
|
484
|
+
vec!["Down", "Enter"]
|
|
485
|
+
);
|
|
486
|
+
assert_eq!(
|
|
487
|
+
approval_choice_keys(&prompt, " 1. Allow\n 2. Deny\n", "Deny"),
|
|
488
|
+
vec!["2", "Enter"]
|
|
489
|
+
);
|
|
490
|
+
assert_eq!(
|
|
491
|
+
approval_choice_keys(&prompt, "❯ 1. Allow\n 2. Deny\n", "Deny"),
|
|
492
|
+
vec!["Down", "Enter"]
|
|
493
|
+
);
|
|
494
|
+
assert_eq!(
|
|
495
|
+
approval_choice_keys(&prompt, " 1. Allow\n❯ 2. Deny\n", "Allow"),
|
|
496
|
+
vec!["Up", "Enter"]
|
|
497
|
+
);
|
|
498
|
+
assert_eq!(
|
|
499
|
+
approval_choice_keys(&prompt, "❯ 1. Allow\n 2. Deny\n", "Allow"),
|
|
500
|
+
vec!["Enter"]
|
|
501
|
+
);
|
|
410
502
|
}
|
|
411
503
|
|
|
412
504
|
#[test]
|
|
@@ -445,8 +537,16 @@ mod tests {
|
|
|
445
537
|
None,
|
|
446
538
|
)
|
|
447
539
|
.to_ordered_value();
|
|
448
|
-
let keys: Vec<&str> = mcp
|
|
449
|
-
|
|
540
|
+
let keys: Vec<&str> = mcp
|
|
541
|
+
.as_object()
|
|
542
|
+
.unwrap()
|
|
543
|
+
.keys()
|
|
544
|
+
.map(String::as_str)
|
|
545
|
+
.collect();
|
|
546
|
+
assert_eq!(
|
|
547
|
+
keys,
|
|
548
|
+
vec!["agent_id", "state", "kind", "tool", "prompt", "choices"]
|
|
549
|
+
);
|
|
450
550
|
|
|
451
551
|
let command = approval(
|
|
452
552
|
"agent-x",
|
|
@@ -458,8 +558,16 @@ mod tests {
|
|
|
458
558
|
Some("Bash(echo hi)".to_string()),
|
|
459
559
|
)
|
|
460
560
|
.to_ordered_value();
|
|
461
|
-
let keys: Vec<&str> = command
|
|
462
|
-
|
|
561
|
+
let keys: Vec<&str> = command
|
|
562
|
+
.as_object()
|
|
563
|
+
.unwrap()
|
|
564
|
+
.keys()
|
|
565
|
+
.map(String::as_str)
|
|
566
|
+
.collect();
|
|
567
|
+
assert_eq!(
|
|
568
|
+
keys,
|
|
569
|
+
vec!["agent_id", "state", "kind", "command", "prompt", "choices"]
|
|
570
|
+
);
|
|
463
571
|
}
|
|
464
572
|
|
|
465
573
|
#[test]
|
|
@@ -13,7 +13,10 @@ pub fn runtime_mcp_tool_allowlisted(tool: &str) -> bool {
|
|
|
13
13
|
|
|
14
14
|
pub fn runtime_mcp_prompt_allowlisted(prompt: &ApprovalPrompt) -> bool {
|
|
15
15
|
prompt.server.as_deref() == Some(RUNTIME_MCP_APPROVAL_SERVER)
|
|
16
|
-
&& prompt
|
|
16
|
+
&& prompt
|
|
17
|
+
.tool
|
|
18
|
+
.as_deref()
|
|
19
|
+
.is_some_and(runtime_mcp_tool_allowlisted)
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
/// Issue 1 (Round 3b gate review §6): Team-Agent control-plane MCP tools
|
|
@@ -126,7 +129,11 @@ pub fn awaiting_human_confirm_reason(
|
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
pub fn approval_prompt_fingerprint(
|
|
132
|
+
pub fn approval_prompt_fingerprint(
|
|
133
|
+
team: &str,
|
|
134
|
+
agent_id: &str,
|
|
135
|
+
prompt: &ApprovalPrompt,
|
|
136
|
+
) -> ApprovalFingerprint {
|
|
130
137
|
let mut hasher = Sha256::new();
|
|
131
138
|
hasher.update(team.as_bytes());
|
|
132
139
|
hasher.update([0]);
|
|
@@ -161,7 +168,10 @@ pub fn awaiting_human_confirm_dedupe_key(
|
|
|
161
168
|
agent_id: &str,
|
|
162
169
|
fingerprint: &ApprovalFingerprint,
|
|
163
170
|
) -> String {
|
|
164
|
-
format!(
|
|
171
|
+
format!(
|
|
172
|
+
"awaiting_human_confirm:{team}:{agent_id}:{}",
|
|
173
|
+
fingerprint.as_str()
|
|
174
|
+
)
|
|
165
175
|
}
|
|
166
176
|
|
|
167
177
|
pub fn awaiting_human_confirm_fact(
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
use super::helpers::parse_jsonl_records;
|
|
5
5
|
use super::types::{
|
|
6
|
-
ClassifyResult, ClassifySource, FactKind, FaultFact, NoPingReason, ProcessLiveness,
|
|
7
|
-
RemindResult, Signature, TurnId, TurnState,
|
|
6
|
+
ClassifyResult, ClassifySource, FactKind, FaultFact, NoPingReason, ProcessLiveness,
|
|
7
|
+
ProviderError, RemindResult, Signature, TurnId, TurnState,
|
|
8
8
|
};
|
|
9
9
|
use super::Provider;
|
|
10
10
|
|
|
@@ -116,14 +116,20 @@ fn collect_background_open_ids(value: &serde_json::Value, out: &mut Vec<String>)
|
|
|
116
116
|
match value {
|
|
117
117
|
serde_json::Value::String(s) => {
|
|
118
118
|
for piece in s.split(MARKER).skip(1) {
|
|
119
|
-
let id: String = piece
|
|
119
|
+
let id: String = piece
|
|
120
|
+
.chars()
|
|
121
|
+
.take_while(|c| !c.is_whitespace() && *c != '\n')
|
|
122
|
+
.collect();
|
|
120
123
|
if !id.is_empty() {
|
|
121
124
|
out.push(id);
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
serde_json::Value::Object(map) => {
|
|
126
|
-
if let Some(id) = map
|
|
129
|
+
if let Some(id) = map
|
|
130
|
+
.get("backgroundTaskId")
|
|
131
|
+
.and_then(serde_json::Value::as_str)
|
|
132
|
+
{
|
|
127
133
|
if !id.is_empty() {
|
|
128
134
|
out.push(id.to_string());
|
|
129
135
|
}
|
|
@@ -147,9 +153,15 @@ fn collect_background_close_ids(value: &serde_json::Value, out: &mut Vec<String>
|
|
|
147
153
|
// Form B: <task-notification> wrapper in a string with a status tag.
|
|
148
154
|
if let serde_json::Value::Object(map) = value {
|
|
149
155
|
if let Some(bash) = map.get("bashOutput").and_then(serde_json::Value::as_object) {
|
|
150
|
-
let status = bash
|
|
156
|
+
let status = bash
|
|
157
|
+
.get("status")
|
|
158
|
+
.and_then(serde_json::Value::as_str)
|
|
159
|
+
.unwrap_or("");
|
|
151
160
|
if matches!(status, "completed" | "failed" | "killed") {
|
|
152
|
-
if let Some(id) = bash
|
|
161
|
+
if let Some(id) = bash
|
|
162
|
+
.get("backgroundTaskId")
|
|
163
|
+
.and_then(serde_json::Value::as_str)
|
|
164
|
+
{
|
|
153
165
|
if !id.is_empty() {
|
|
154
166
|
out.push(id.to_string());
|
|
155
167
|
}
|
|
@@ -236,37 +248,90 @@ pub fn evaluate_takeover_reminder(
|
|
|
236
248
|
}
|
|
237
249
|
}
|
|
238
250
|
TurnState::Working => {
|
|
239
|
-
return Ok(remind(
|
|
251
|
+
return Ok(remind(
|
|
252
|
+
false,
|
|
253
|
+
NoPingReason::Node(TurnState::Working),
|
|
254
|
+
Vec::new(),
|
|
255
|
+
None,
|
|
256
|
+
));
|
|
240
257
|
}
|
|
241
258
|
TurnState::BlockedOnHuman => {
|
|
242
|
-
return Ok(remind(
|
|
259
|
+
return Ok(remind(
|
|
260
|
+
false,
|
|
261
|
+
NoPingReason::Node(TurnState::BlockedOnHuman),
|
|
262
|
+
Vec::new(),
|
|
263
|
+
None,
|
|
264
|
+
));
|
|
243
265
|
}
|
|
244
266
|
TurnState::Abnormal => {
|
|
245
|
-
return Ok(remind(
|
|
267
|
+
return Ok(remind(
|
|
268
|
+
false,
|
|
269
|
+
NoPingReason::Node(TurnState::Abnormal),
|
|
270
|
+
Vec::new(),
|
|
271
|
+
None,
|
|
272
|
+
));
|
|
246
273
|
}
|
|
247
274
|
TurnState::Unknown => {
|
|
248
|
-
return Ok(remind(
|
|
275
|
+
return Ok(remind(
|
|
276
|
+
false,
|
|
277
|
+
NoPingReason::Node(TurnState::Unknown),
|
|
278
|
+
Vec::new(),
|
|
279
|
+
None,
|
|
280
|
+
));
|
|
249
281
|
}
|
|
250
282
|
}
|
|
251
283
|
}
|
|
252
284
|
|
|
253
285
|
let Some(ms) = monitor_state else {
|
|
254
|
-
return Ok(remind(
|
|
286
|
+
return Ok(remind(
|
|
287
|
+
false,
|
|
288
|
+
NoPingReason::NotArmedNoWorkerTurn,
|
|
289
|
+
Vec::new(),
|
|
290
|
+
None,
|
|
291
|
+
));
|
|
255
292
|
};
|
|
256
|
-
if ms
|
|
257
|
-
|
|
293
|
+
if ms
|
|
294
|
+
.get("opened_worker_turn_since_ack")
|
|
295
|
+
.and_then(serde_json::Value::as_bool)
|
|
296
|
+
!= Some(true)
|
|
297
|
+
{
|
|
298
|
+
return Ok(remind(
|
|
299
|
+
false,
|
|
300
|
+
NoPingReason::NotArmedNoWorkerTurn,
|
|
301
|
+
Vec::new(),
|
|
302
|
+
None,
|
|
303
|
+
));
|
|
258
304
|
}
|
|
259
305
|
if ms.get("suppressed").and_then(serde_json::Value::as_bool) == Some(true) {
|
|
260
306
|
return Ok(remind(false, NoPingReason::Acknowledged, Vec::new(), None));
|
|
261
307
|
}
|
|
262
308
|
let Some(all_idle_since) = ms.get("all_idle_since").and_then(serde_json::Value::as_f64) else {
|
|
263
|
-
return Ok(remind(
|
|
309
|
+
return Ok(remind(
|
|
310
|
+
false,
|
|
311
|
+
NoPingReason::NotArmedNoWorkerTurn,
|
|
312
|
+
Vec::new(),
|
|
313
|
+
None,
|
|
314
|
+
));
|
|
264
315
|
};
|
|
265
|
-
if ms
|
|
266
|
-
|
|
316
|
+
if ms
|
|
317
|
+
.get("pinged_for_episode")
|
|
318
|
+
.and_then(serde_json::Value::as_f64)
|
|
319
|
+
== Some(all_idle_since)
|
|
320
|
+
{
|
|
321
|
+
return Ok(remind(
|
|
322
|
+
false,
|
|
323
|
+
NoPingReason::AlreadyPingedThisEpisode,
|
|
324
|
+
Vec::new(),
|
|
325
|
+
None,
|
|
326
|
+
));
|
|
267
327
|
}
|
|
268
328
|
if now_monotonic - all_idle_since < debounce_seconds {
|
|
269
|
-
return Ok(remind(
|
|
329
|
+
return Ok(remind(
|
|
330
|
+
false,
|
|
331
|
+
NoPingReason::DebounceActive,
|
|
332
|
+
Vec::new(),
|
|
333
|
+
None,
|
|
334
|
+
));
|
|
270
335
|
}
|
|
271
336
|
Ok(remind(
|
|
272
337
|
true,
|
|
@@ -301,7 +366,10 @@ fn classify_result(
|
|
|
301
366
|
}
|
|
302
367
|
}
|
|
303
368
|
|
|
304
|
-
fn extract_lifecycle_facts(
|
|
369
|
+
fn extract_lifecycle_facts(
|
|
370
|
+
provider: Provider,
|
|
371
|
+
records: &[serde_json::Value],
|
|
372
|
+
) -> Vec<LifecycleFact> {
|
|
305
373
|
records
|
|
306
374
|
.iter()
|
|
307
375
|
.filter_map(|record| match provider {
|
|
@@ -328,7 +396,9 @@ fn codex_latest_explicit_error_fact(record: &serde_json::Value) -> Option<FaultF
|
|
|
328
396
|
}
|
|
329
397
|
Some(FaultFact::new(
|
|
330
398
|
Signature::new("turn_failed"),
|
|
331
|
-
turn.get("id")
|
|
399
|
+
turn.get("id")
|
|
400
|
+
.and_then(serde_json::Value::as_str)
|
|
401
|
+
.map(TurnId::new),
|
|
332
402
|
FactKind::Failed,
|
|
333
403
|
))
|
|
334
404
|
}
|
|
@@ -431,29 +501,43 @@ fn claude_lifecycle_fact(record: &serde_json::Value) -> Option<LifecycleFact> {
|
|
|
431
501
|
));
|
|
432
502
|
}
|
|
433
503
|
if record_type == Some("assistant") {
|
|
434
|
-
let turn_id = record
|
|
504
|
+
let turn_id = record
|
|
505
|
+
.get("requestId")
|
|
506
|
+
.and_then(serde_json::Value::as_str)
|
|
507
|
+
.map(TurnId::new);
|
|
435
508
|
let Some(stop_reason) = record
|
|
436
509
|
.get("message")
|
|
437
510
|
.and_then(|m| m.get("stop_reason"))
|
|
438
|
-
.and_then(serde_json::Value::as_str)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
511
|
+
.and_then(serde_json::Value::as_str)
|
|
512
|
+
else {
|
|
513
|
+
if record
|
|
514
|
+
.get("message")
|
|
515
|
+
.and_then(|m| m.get("content"))
|
|
516
|
+
.and_then(serde_json::Value::as_array)
|
|
517
|
+
.is_some()
|
|
518
|
+
{
|
|
519
|
+
return Some(lifecycle(
|
|
520
|
+
FactKind::TurnOpen,
|
|
521
|
+
turn_id,
|
|
522
|
+
"assistant_in_flight",
|
|
523
|
+
vec!["assistant_in_flight".to_string()],
|
|
524
|
+
));
|
|
525
|
+
}
|
|
526
|
+
return None;
|
|
527
|
+
};
|
|
454
528
|
return match stop_reason {
|
|
455
|
-
"tool_use" => Some(lifecycle(
|
|
456
|
-
|
|
529
|
+
"tool_use" => Some(lifecycle(
|
|
530
|
+
FactKind::TurnOpen,
|
|
531
|
+
turn_id,
|
|
532
|
+
"open_turn",
|
|
533
|
+
Vec::new(),
|
|
534
|
+
)),
|
|
535
|
+
"end_turn" => Some(lifecycle(
|
|
536
|
+
FactKind::TurnComplete,
|
|
537
|
+
turn_id,
|
|
538
|
+
"end_turn",
|
|
539
|
+
Vec::new(),
|
|
540
|
+
)),
|
|
457
541
|
"stop_sequence" => Some(lifecycle(
|
|
458
542
|
FactKind::TurnComplete,
|
|
459
543
|
turn_id,
|
|
@@ -466,7 +550,10 @@ fn claude_lifecycle_fact(record: &serde_json::Value) -> Option<LifecycleFact> {
|
|
|
466
550
|
if record_type == Some("user") && claude_user_interrupted(record) {
|
|
467
551
|
return Some(lifecycle(
|
|
468
552
|
FactKind::Interrupted,
|
|
469
|
-
record
|
|
553
|
+
record
|
|
554
|
+
.get("uuid")
|
|
555
|
+
.and_then(serde_json::Value::as_str)
|
|
556
|
+
.map(TurnId::new),
|
|
470
557
|
"user_interrupt",
|
|
471
558
|
vec!["interrupted".to_string()],
|
|
472
559
|
));
|
|
@@ -585,9 +672,7 @@ fn claude_user_interrupted(record: &serde_json::Value) -> bool {
|
|
|
585
672
|
.is_some_and(|items| {
|
|
586
673
|
items.iter().any(|item| {
|
|
587
674
|
item.get("type").and_then(serde_json::Value::as_str) == Some("text")
|
|
588
|
-
&& item
|
|
589
|
-
.get("text")
|
|
590
|
-
.and_then(serde_json::Value::as_str)
|
|
675
|
+
&& item.get("text").and_then(serde_json::Value::as_str)
|
|
591
676
|
== Some("[Request interrupted by user]")
|
|
592
677
|
})
|
|
593
678
|
})
|