@team-agent/installer 0.5.86 → 0.5.91
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 +104 -164
- package/crates/team-agent/src/cli/emit.rs +68 -87
- package/crates/team-agent/src/cli/mod.rs +1 -1
- package/crates/team-agent/src/cli/spec.rs +1 -4
- package/crates/team-agent/src/cli/status_port/brief.rs +187 -7
- package/crates/team-agent/src/cli/status_port/inbox.rs +42 -17
- package/crates/team-agent/src/cli/tests/lane_c.rs +34 -26
- package/crates/team-agent/src/cli/tests/main_preserved.rs +4 -70
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +0 -68
- package/crates/team-agent/src/cli/types.rs +3 -16
- package/crates/team-agent/src/compiler.rs +4 -7
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +5 -6
- package/crates/team-agent/src/coordinator/tick.rs +1 -16
- package/crates/team-agent/src/coordinator/types.rs +1 -1
- package/crates/team-agent/src/db/message_store.rs +10 -5
- package/crates/team-agent/src/fake_worker.rs +4 -4
- package/crates/team-agent/src/lifecycle/launch/identity.rs +39 -21
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -10
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -9
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -26
- package/crates/team-agent/src/main.rs +7 -2
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +234 -386
- package/crates/team-agent/src/messaging/tests/runtime.rs +91 -135
- package/crates/team-agent/src/messaging/watchers.rs +1 -1
- package/crates/team-agent/src/model/enums.rs +17 -3
- package/crates/team-agent/src/model/spec.rs +8 -7
- package/crates/team-agent/src/provider/wire.rs +1 -1
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +3 -3
- package/skills/team-agent/command-coverage.json +2 -9
- package/skills/team-agent/references/team-agent-operator.md +661 -0
|
@@ -76,7 +76,6 @@ pub(crate) enum DispatchKind {
|
|
|
76
76
|
Validate,
|
|
77
77
|
InstallSkill,
|
|
78
78
|
Profile,
|
|
79
|
-
Collect,
|
|
80
79
|
Results,
|
|
81
80
|
Wait,
|
|
82
81
|
Diagnose,
|
|
@@ -123,7 +122,6 @@ pub(crate) const ALL_DISPATCH_KINDS: &[DispatchKind] = &[
|
|
|
123
122
|
DispatchKind::Validate,
|
|
124
123
|
DispatchKind::InstallSkill,
|
|
125
124
|
DispatchKind::Profile,
|
|
126
|
-
DispatchKind::Collect,
|
|
127
125
|
DispatchKind::Results,
|
|
128
126
|
DispatchKind::Wait,
|
|
129
127
|
DispatchKind::Diagnose,
|
|
@@ -170,7 +168,6 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
170
168
|
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for an in-team short name or fully-qualified logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--mailbox] [--json]\nTO forms are co-equal: an in-team short name (for example `team-agent send reviewer \"Review\"`) or `<workspace>::<team>/<agent>`.", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
|
|
171
169
|
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
172
170
|
CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "list exact provider model ids", usage: "usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
173
|
-
CommandSpec { name: "collect", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Collect), summary: "collect reported results", usage: "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
174
171
|
CommandSpec { name: "results", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Results), summary: "read reported results for a case", usage: "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
175
172
|
CommandSpec { name: "wait", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Wait), summary: "block until a task result is stored", usage: "usage: team-agent wait --task TASK [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
176
173
|
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
@@ -208,7 +205,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
208
205
|
CommandSpec { name: "approvals", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Approvals), summary: "inspect provider approval state", usage: "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "approval diagnostics" }) },
|
|
209
206
|
CommandSpec { name: "profile", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Profile), summary: "manage provider profiles", usage: "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--proxy-mode direct|inherit] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "provider/profile setup" }) },
|
|
210
207
|
CommandSpec { name: "install-skill", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::InstallSkill), summary: "install or uninstall Team Agent skills", usage: "usage: team-agent install-skill (--source DIR | --uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "capability setup" }) },
|
|
211
|
-
CommandSpec { name: "inbox", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Inbox), summary: "inspect
|
|
208
|
+
CommandSpec { name: "inbox", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Inbox), summary: "inspect recent message summaries", usage: "usage: team-agent inbox AGENT [-n N|--limit N] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
212
209
|
|
|
213
210
|
CommandSpec { name: "identity", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::Identity), summary: "inspect identity metadata", usage: "usage: team-agent identity [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
214
211
|
CommandSpec { name: "watch", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::Watch), summary: "watch event logs", usage: "usage: team-agent watch [--workspace WORKSPACE] [--team TEAM]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
@@ -1,38 +1,66 @@
|
|
|
1
|
-
//! Seven-field, read-only status projection backed by
|
|
1
|
+
//! Seven-field, read-only status projection backed by native tmux and process samples.
|
|
2
2
|
//!
|
|
3
3
|
//! This intentionally does not reuse RuntimeSnapshot: the legacy snapshot reads
|
|
4
4
|
//! coordinator/db/history and carries diagnostic fields that are outside the
|
|
5
5
|
//! status brief contract.
|
|
6
6
|
|
|
7
|
-
use serde::Deserialize;
|
|
8
7
|
use serde_json::{json, Map, Value};
|
|
9
|
-
use sha2::{Digest, Sha256};
|
|
10
8
|
use std::collections::BTreeMap;
|
|
9
|
+
use std::path::Path;
|
|
10
|
+
use std::process::Command;
|
|
11
|
+
|
|
12
|
+
#[cfg(test)]
|
|
13
|
+
use serde::Deserialize;
|
|
14
|
+
#[cfg(test)]
|
|
15
|
+
use sha2::{Digest, Sha256};
|
|
16
|
+
#[cfg(test)]
|
|
11
17
|
use std::fs::{File, OpenOptions};
|
|
18
|
+
#[cfg(test)]
|
|
12
19
|
use std::io::Read;
|
|
13
|
-
|
|
14
|
-
use std::
|
|
20
|
+
#[cfg(test)]
|
|
21
|
+
use std::path::PathBuf;
|
|
22
|
+
#[cfg(test)]
|
|
23
|
+
use std::process::{Child, ExitStatus, Stdio};
|
|
24
|
+
#[cfg(test)]
|
|
15
25
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
26
|
+
#[cfg(test)]
|
|
16
27
|
use std::sync::mpsc;
|
|
17
28
|
#[cfg(test)]
|
|
18
29
|
use std::sync::Arc;
|
|
30
|
+
#[cfg(test)]
|
|
19
31
|
use std::thread;
|
|
32
|
+
#[cfg(test)]
|
|
20
33
|
use std::time::{Duration, Instant};
|
|
21
34
|
|
|
35
|
+
#[cfg(test)]
|
|
22
36
|
const NODEPROBE_TIMEOUT: Duration = Duration::from_secs(2);
|
|
37
|
+
#[cfg(test)]
|
|
23
38
|
const NODEPROBE_OUTPUT_LIMIT: u64 = 1024 * 1024;
|
|
39
|
+
#[cfg(test)]
|
|
24
40
|
const NODEPROBE_KILL_GRACE: Duration = Duration::from_millis(100);
|
|
41
|
+
#[cfg(test)]
|
|
25
42
|
const NODEPROBE_RECEIPT_LIMIT: u64 = 64 * 1024;
|
|
43
|
+
#[cfg(test)]
|
|
26
44
|
const NODEPROBE_BINARY_LIMIT: u64 = 64 * 1024 * 1024;
|
|
45
|
+
#[cfg(test)]
|
|
27
46
|
const NODEPROBE_READ_CHUNK: usize = 64 * 1024;
|
|
47
|
+
#[cfg(test)]
|
|
28
48
|
const NODEPROBE_NAME: &str = "nodeprobe";
|
|
49
|
+
#[cfg(test)]
|
|
29
50
|
const NODEPROBE_RECEIPT_SUFFIX: &str = ".capability.json";
|
|
51
|
+
#[cfg(test)]
|
|
30
52
|
const NODEPROBE_RECEIPT_SCHEMA: &str = "nodeprobe-capability-v1";
|
|
53
|
+
#[cfg(test)]
|
|
31
54
|
const NODEPROBE_SOURCE_REPO: &str = "Florious95/team-agent-scratch/nodeprobe";
|
|
55
|
+
#[cfg(test)]
|
|
32
56
|
const NODEPROBE_SOURCE_COMMIT: &str = "ff316dc0afe8ab280e61d30934e7624579be6224";
|
|
57
|
+
#[cfg(test)]
|
|
33
58
|
const NODEPROBE_SOURCE_TREE: &str = "5217a41aa914ddcb72c27f39f1b4af9ead68b1b6";
|
|
59
|
+
#[cfg(test)]
|
|
34
60
|
const NODEPROBE_REPORT_SCHEMA: u64 = 1;
|
|
61
|
+
#[cfg(test)]
|
|
35
62
|
const NODEPROBE_CAPABILITIES: &[&str] = &["tmux.list-panes", "ps.pid_ppid_stat_comm"];
|
|
63
|
+
#[cfg(test)]
|
|
36
64
|
const NODEPROBE_FORBIDDEN: &[&str] = &[
|
|
37
65
|
"tmux.capture-pane",
|
|
38
66
|
"tmux.attach",
|
|
@@ -47,6 +75,7 @@ thread_local! {
|
|
|
47
75
|
static TEST_RESOLVER_DELAY: std::cell::RefCell<Option<TestResolverDelay>> = const { std::cell::RefCell::new(None) };
|
|
48
76
|
}
|
|
49
77
|
|
|
78
|
+
#[cfg(test)]
|
|
50
79
|
static NODEPROBE_RESOLVER_ACTIVE: AtomicBool = AtomicBool::new(false);
|
|
51
80
|
|
|
52
81
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
@@ -90,6 +119,7 @@ struct ProbeResult {
|
|
|
90
119
|
}
|
|
91
120
|
|
|
92
121
|
#[derive(Debug, Deserialize)]
|
|
122
|
+
#[cfg(test)]
|
|
93
123
|
struct NodeprobeCapabilityReceipt {
|
|
94
124
|
schema: String,
|
|
95
125
|
binary: String,
|
|
@@ -383,6 +413,126 @@ fn first_string(value: &Value, keys: &[&str]) -> Option<String> {
|
|
|
383
413
|
}
|
|
384
414
|
|
|
385
415
|
fn probe_registered_endpoints(nodes: &[RegisteredNode]) -> BTreeMap<String, Option<ProbeResult>> {
|
|
416
|
+
#[cfg(test)]
|
|
417
|
+
{
|
|
418
|
+
// Keep the legacy fixture seam for existing hermetic tests; production
|
|
419
|
+
// status never searches for or executes nodeprobe.
|
|
420
|
+
if TEST_NODEPROBE.with(|slot| slot.borrow().is_some()) {
|
|
421
|
+
return probe_registered_endpoints_with_test_nodeprobe(nodes);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
let mut endpoints = BTreeMap::new();
|
|
425
|
+
for endpoint in nodes.iter().filter_map(|node| node.endpoint.as_deref()) {
|
|
426
|
+
if endpoints.contains_key(endpoint) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
endpoints.insert(
|
|
430
|
+
endpoint.to_string(),
|
|
431
|
+
sample_native_endpoint(endpoint, nodes),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
endpoints
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
fn sample_native_endpoint(
|
|
438
|
+
endpoint: &str,
|
|
439
|
+
registered: &[RegisteredNode],
|
|
440
|
+
) -> Option<ProbeResult> {
|
|
441
|
+
let output = run_native_tmux_list_panes(endpoint)?;
|
|
442
|
+
let nodes = output
|
|
443
|
+
.lines()
|
|
444
|
+
.filter_map(|line| parse_native_pane(line, endpoint, registered))
|
|
445
|
+
.collect::<Vec<_>>();
|
|
446
|
+
Some(ProbeResult { nodes })
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
fn run_native_tmux_list_panes(endpoint: &str) -> Option<String> {
|
|
450
|
+
const FORMAT: &str = "#{session_name}\t#{window_name}\t#{pane_id}\t#{pane_pid}\t#{pane_current_command}";
|
|
451
|
+
let flag = if Path::new(endpoint).is_absolute() { "-S" } else { "-L" };
|
|
452
|
+
let output = Command::new("tmux")
|
|
453
|
+
.arg(flag)
|
|
454
|
+
.arg(endpoint)
|
|
455
|
+
.args(["list-panes", "-a", "-F", FORMAT])
|
|
456
|
+
.output()
|
|
457
|
+
.ok()?;
|
|
458
|
+
output.status.success().then(|| String::from_utf8(output.stdout).ok())?
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
fn parse_native_pane(
|
|
462
|
+
line: &str,
|
|
463
|
+
endpoint: &str,
|
|
464
|
+
registered: &[RegisteredNode],
|
|
465
|
+
) -> Option<ProbeNode> {
|
|
466
|
+
let mut fields = line.split('\t');
|
|
467
|
+
let session = fields.next()?.to_string();
|
|
468
|
+
let window = fields.next()?.to_string();
|
|
469
|
+
let pane = fields.next()?.to_string();
|
|
470
|
+
let pid = fields.next().filter(|value| !value.is_empty());
|
|
471
|
+
let command = fields.next().unwrap_or_default();
|
|
472
|
+
if session.is_empty() || window.is_empty() || pane.is_empty() {
|
|
473
|
+
return None;
|
|
474
|
+
}
|
|
475
|
+
let matched = registered.iter().find(|node| {
|
|
476
|
+
node.endpoint.as_deref() == Some(endpoint)
|
|
477
|
+
&& node.session.as_deref() == Some(session.as_str())
|
|
478
|
+
&& node.window.as_deref().is_none_or(|value| value == window)
|
|
479
|
+
&& node.pane.as_deref() == Some(pane.as_str())
|
|
480
|
+
});
|
|
481
|
+
if let Some(pid) = pid {
|
|
482
|
+
let snapshot = process_snapshot(pid)?;
|
|
483
|
+
if snapshot.zombie {
|
|
484
|
+
return None;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
let provider = matched
|
|
488
|
+
.map(|node| node.provider.clone())
|
|
489
|
+
.unwrap_or_else(|| "unknown".to_string());
|
|
490
|
+
let activity = if command.is_empty() {
|
|
491
|
+
"unknown"
|
|
492
|
+
} else if matches!(command, "bash" | "fish" | "sh" | "zsh" | "-bash" | "-zsh") {
|
|
493
|
+
"idle"
|
|
494
|
+
} else {
|
|
495
|
+
"working"
|
|
496
|
+
};
|
|
497
|
+
Some(ProbeNode {
|
|
498
|
+
socket: endpoint.to_string(),
|
|
499
|
+
session: session.clone(),
|
|
500
|
+
window: window.clone(),
|
|
501
|
+
pane,
|
|
502
|
+
provider,
|
|
503
|
+
activity: activity.to_string(),
|
|
504
|
+
health: "normal".to_string(),
|
|
505
|
+
session_name: Some(session),
|
|
506
|
+
evidence_method: Some("native_tmux".to_string()),
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
struct ProcessSnapshot {
|
|
511
|
+
zombie: bool,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
fn process_snapshot(pid: &str) -> Option<ProcessSnapshot> {
|
|
515
|
+
let output = Command::new("ps")
|
|
516
|
+
.args(["-o", "pid=,ppid=,stat=,comm=", "-p", pid])
|
|
517
|
+
.output()
|
|
518
|
+
.ok()?;
|
|
519
|
+
if !output.status.success() || String::from_utf8_lossy(&output.stdout).trim().is_empty() {
|
|
520
|
+
return None;
|
|
521
|
+
}
|
|
522
|
+
let output_text = String::from_utf8_lossy(&output.stdout);
|
|
523
|
+
let stat = output_text
|
|
524
|
+
.split_whitespace()
|
|
525
|
+
.nth(2)
|
|
526
|
+
.unwrap_or_default();
|
|
527
|
+
Some(ProcessSnapshot {
|
|
528
|
+
zombie: stat.contains('Z'),
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
#[cfg(test)]
|
|
533
|
+
fn probe_registered_endpoints_with_test_nodeprobe(
|
|
534
|
+
nodes: &[RegisteredNode],
|
|
535
|
+
) -> BTreeMap<String, Option<ProbeResult>> {
|
|
386
536
|
let deadline = Instant::now() + NODEPROBE_TIMEOUT;
|
|
387
537
|
let candidates = nodeprobe_candidates();
|
|
388
538
|
let resolver_options = resolver_options();
|
|
@@ -410,6 +560,7 @@ fn probe_registered_endpoints(nodes: &[RegisteredNode]) -> BTreeMap<String, Opti
|
|
|
410
560
|
endpoints
|
|
411
561
|
}
|
|
412
562
|
|
|
563
|
+
#[cfg(test)]
|
|
413
564
|
fn run_nodeprobe(binary: &Path, endpoint: &str, deadline: Instant) -> Option<Vec<u8>> {
|
|
414
565
|
if Instant::now() >= deadline {
|
|
415
566
|
return None;
|
|
@@ -476,6 +627,7 @@ fn run_nodeprobe(binary: &Path, endpoint: &str, deadline: Instant) -> Option<Vec
|
|
|
476
627
|
}
|
|
477
628
|
}
|
|
478
629
|
|
|
630
|
+
#[cfg(test)]
|
|
479
631
|
fn wait_child_bounded(child: &mut Child, deadline: Instant) -> Option<ExitStatus> {
|
|
480
632
|
loop {
|
|
481
633
|
match child.try_wait() {
|
|
@@ -493,6 +645,7 @@ fn wait_child_bounded(child: &mut Child, deadline: Instant) -> Option<ExitStatus
|
|
|
493
645
|
}
|
|
494
646
|
}
|
|
495
647
|
|
|
648
|
+
#[cfg(test)]
|
|
496
649
|
fn terminate_nodeprobe(child: &mut Child) {
|
|
497
650
|
#[cfg(unix)]
|
|
498
651
|
{
|
|
@@ -509,6 +662,7 @@ fn terminate_nodeprobe(child: &mut Child) {
|
|
|
509
662
|
}
|
|
510
663
|
}
|
|
511
664
|
|
|
665
|
+
#[cfg(test)]
|
|
512
666
|
fn spawn_nodeprobe(binary: &Path, endpoint: &str) -> Option<Child> {
|
|
513
667
|
let flag = if Path::new(endpoint).is_absolute() {
|
|
514
668
|
"-S"
|
|
@@ -522,11 +676,13 @@ fn spawn_nodeprobe(binary: &Path, endpoint: &str) -> Option<Child> {
|
|
|
522
676
|
}
|
|
523
677
|
|
|
524
678
|
#[derive(Clone, Default)]
|
|
679
|
+
#[cfg(test)]
|
|
525
680
|
struct ResolverOptions {
|
|
526
681
|
#[cfg(test)]
|
|
527
682
|
delay_after_selection: Option<TestResolverDelay>,
|
|
528
683
|
}
|
|
529
684
|
|
|
685
|
+
#[cfg(test)]
|
|
530
686
|
fn resolver_options() -> ResolverOptions {
|
|
531
687
|
#[cfg(test)]
|
|
532
688
|
{
|
|
@@ -543,11 +699,13 @@ fn resolver_options() -> ResolverOptions {
|
|
|
543
699
|
// A resolver that is stuck in an uninterruptible filesystem syscall may outlive
|
|
544
700
|
// the caller deadline. It owns only its candidate work; the active gate prevents
|
|
545
701
|
// repeated status calls from accumulating more detached resolver threads.
|
|
702
|
+
#[cfg(test)]
|
|
546
703
|
struct ResolverActiveGuard {
|
|
547
704
|
#[cfg(test)]
|
|
548
705
|
finished_signal: Option<ResolverDelaySignal>,
|
|
549
706
|
}
|
|
550
707
|
|
|
708
|
+
#[cfg(test)]
|
|
551
709
|
impl Drop for ResolverActiveGuard {
|
|
552
710
|
fn drop(&mut self) {
|
|
553
711
|
NODEPROBE_RESOLVER_ACTIVE.store(false, Ordering::Release);
|
|
@@ -558,6 +716,7 @@ impl Drop for ResolverActiveGuard {
|
|
|
558
716
|
}
|
|
559
717
|
}
|
|
560
718
|
|
|
719
|
+
#[cfg(test)]
|
|
561
720
|
fn resolve_nodeprobe_binary(
|
|
562
721
|
candidates: Vec<PathBuf>,
|
|
563
722
|
deadline: Instant,
|
|
@@ -606,6 +765,7 @@ fn resolve_nodeprobe_binary(
|
|
|
606
765
|
}
|
|
607
766
|
}
|
|
608
767
|
|
|
768
|
+
#[cfg(test)]
|
|
609
769
|
fn select_nodeprobe_binary(
|
|
610
770
|
candidates: Vec<PathBuf>,
|
|
611
771
|
deadline: Instant,
|
|
@@ -621,6 +781,7 @@ fn select_nodeprobe_binary(
|
|
|
621
781
|
None
|
|
622
782
|
}
|
|
623
783
|
|
|
784
|
+
#[cfg(test)]
|
|
624
785
|
fn nodeprobe_candidates() -> Vec<PathBuf> {
|
|
625
786
|
#[cfg(test)]
|
|
626
787
|
{
|
|
@@ -642,6 +803,7 @@ fn nodeprobe_candidates() -> Vec<PathBuf> {
|
|
|
642
803
|
candidates
|
|
643
804
|
}
|
|
644
805
|
|
|
806
|
+
#[cfg(test)]
|
|
645
807
|
fn validate_nodeprobe_candidate(path: &Path, deadline: Instant) -> Option<PathBuf> {
|
|
646
808
|
if Instant::now() >= deadline {
|
|
647
809
|
return None;
|
|
@@ -669,6 +831,7 @@ fn validate_nodeprobe_candidate(path: &Path, deadline: Instant) -> Option<PathBu
|
|
|
669
831
|
(digest == receipt.binary_sha256).then_some(path.to_path_buf())
|
|
670
832
|
}
|
|
671
833
|
|
|
834
|
+
#[cfg(test)]
|
|
672
835
|
fn valid_nodeprobe_receipt(receipt: &NodeprobeCapabilityReceipt, binary: &Path) -> bool {
|
|
673
836
|
receipt.schema == NODEPROBE_RECEIPT_SCHEMA
|
|
674
837
|
&& binary.file_name().and_then(|name| name.to_str()) == Some(NODEPROBE_NAME)
|
|
@@ -683,10 +846,12 @@ fn valid_nodeprobe_receipt(receipt: &NodeprobeCapabilityReceipt, binary: &Path)
|
|
|
683
846
|
&& same_string_set(&receipt.forbidden, NODEPROBE_FORBIDDEN)
|
|
684
847
|
}
|
|
685
848
|
|
|
849
|
+
#[cfg(test)]
|
|
686
850
|
fn valid_sha256(value: &str) -> bool {
|
|
687
851
|
value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
|
|
688
852
|
}
|
|
689
853
|
|
|
854
|
+
#[cfg(test)]
|
|
690
855
|
fn same_string_set(actual: &[String], expected: &[&str]) -> bool {
|
|
691
856
|
let mut actual = actual.to_vec();
|
|
692
857
|
actual.sort_unstable();
|
|
@@ -698,6 +863,7 @@ fn same_string_set(actual: &[String], expected: &[&str]) -> bool {
|
|
|
698
863
|
actual == expected
|
|
699
864
|
}
|
|
700
865
|
|
|
866
|
+
#[cfg(test)]
|
|
701
867
|
fn nodeprobe_receipt_path(binary: &Path) -> PathBuf {
|
|
702
868
|
let name = binary
|
|
703
869
|
.file_name()
|
|
@@ -706,6 +872,7 @@ fn nodeprobe_receipt_path(binary: &Path) -> PathBuf {
|
|
|
706
872
|
binary.with_file_name(format!("{name}{NODEPROBE_RECEIPT_SUFFIX}"))
|
|
707
873
|
}
|
|
708
874
|
|
|
875
|
+
#[cfg(test)]
|
|
709
876
|
fn read_bounded_file(path: &Path, limit: u64, deadline: Instant) -> Option<Vec<u8>> {
|
|
710
877
|
let metadata = std::fs::metadata(path).ok()?;
|
|
711
878
|
if !metadata.is_file() || metadata.len() > limit {
|
|
@@ -729,6 +896,7 @@ fn read_bounded_file(path: &Path, limit: u64, deadline: Instant) -> Option<Vec<u
|
|
|
729
896
|
}
|
|
730
897
|
}
|
|
731
898
|
|
|
899
|
+
#[cfg(test)]
|
|
732
900
|
fn sha256_file(path: &Path, deadline: Instant) -> Option<String> {
|
|
733
901
|
let metadata = std::fs::metadata(path).ok()?;
|
|
734
902
|
if !metadata.is_file() || metadata.len() > NODEPROBE_BINARY_LIMIT {
|
|
@@ -754,6 +922,7 @@ fn sha256_file(path: &Path, deadline: Instant) -> Option<String> {
|
|
|
754
922
|
}
|
|
755
923
|
}
|
|
756
924
|
|
|
925
|
+
#[cfg(test)]
|
|
757
926
|
fn open_readonly_nonblocking(path: &Path) -> Option<File> {
|
|
758
927
|
let mut options = OpenOptions::new();
|
|
759
928
|
options.read(true);
|
|
@@ -765,6 +934,7 @@ fn open_readonly_nonblocking(path: &Path) -> Option<File> {
|
|
|
765
934
|
options.open(path).ok()
|
|
766
935
|
}
|
|
767
936
|
|
|
937
|
+
#[cfg(test)]
|
|
768
938
|
fn is_executable(metadata: &std::fs::Metadata) -> bool {
|
|
769
939
|
#[cfg(unix)]
|
|
770
940
|
{
|
|
@@ -778,6 +948,7 @@ fn is_executable(metadata: &std::fs::Metadata) -> bool {
|
|
|
778
948
|
}
|
|
779
949
|
}
|
|
780
950
|
|
|
951
|
+
#[cfg(test)]
|
|
781
952
|
fn current_nodeprobe_target() -> Option<&'static str> {
|
|
782
953
|
match (std::env::consts::OS, std::env::consts::ARCH) {
|
|
783
954
|
("macos", "aarch64") => Some("aarch64-apple-darwin"),
|
|
@@ -789,6 +960,7 @@ fn current_nodeprobe_target() -> Option<&'static str> {
|
|
|
789
960
|
}
|
|
790
961
|
}
|
|
791
962
|
|
|
963
|
+
#[cfg(test)]
|
|
792
964
|
fn configure_nodeprobe_command(command: &mut Command) {
|
|
793
965
|
command
|
|
794
966
|
.stdin(Stdio::null())
|
|
@@ -801,6 +973,7 @@ fn configure_nodeprobe_command(command: &mut Command) {
|
|
|
801
973
|
}
|
|
802
974
|
}
|
|
803
975
|
|
|
976
|
+
#[cfg(test)]
|
|
804
977
|
fn parse_probe(bytes: &[u8], requested_endpoint: &str) -> Result<ProbeResult, ()> {
|
|
805
978
|
let value: Value = serde_json::from_slice(bytes).map_err(|_| ())?;
|
|
806
979
|
if value.get("schema_version").and_then(Value::as_u64) != Some(1) {
|
|
@@ -823,6 +996,7 @@ fn parse_probe(bytes: &[u8], requested_endpoint: &str) -> Result<ProbeResult, ()
|
|
|
823
996
|
Ok(ProbeResult { nodes })
|
|
824
997
|
}
|
|
825
998
|
|
|
999
|
+
#[cfg(test)]
|
|
826
1000
|
fn probe_has_error(value: &Value) -> bool {
|
|
827
1001
|
match value.get("error") {
|
|
828
1002
|
None | Some(Value::Null) => false,
|
|
@@ -833,6 +1007,7 @@ fn probe_has_error(value: &Value) -> bool {
|
|
|
833
1007
|
}
|
|
834
1008
|
}
|
|
835
1009
|
|
|
1010
|
+
#[cfg(test)]
|
|
836
1011
|
fn parse_probe_node(value: &Value, report_socket: &str) -> Option<ProbeNode> {
|
|
837
1012
|
Some(ProbeNode {
|
|
838
1013
|
// The accepted producer binds the socket once at the report envelope
|
|
@@ -857,6 +1032,7 @@ fn parse_probe_node(value: &Value, report_socket: &str) -> Option<ProbeNode> {
|
|
|
857
1032
|
})
|
|
858
1033
|
}
|
|
859
1034
|
|
|
1035
|
+
#[cfg(test)]
|
|
860
1036
|
fn non_empty(value: &Value, key: &str) -> Option<String> {
|
|
861
1037
|
value
|
|
862
1038
|
.get(key)
|
|
@@ -895,12 +1071,16 @@ fn project_node(node: &RegisteredNode, probe: Option<&Option<ProbeResult>>) -> B
|
|
|
895
1071
|
}
|
|
896
1072
|
let pi_channel = observed.provider.eq_ignore_ascii_case("pi")
|
|
897
1073
|
&& observed.evidence_method.as_deref() == Some("pi_activity_channel");
|
|
898
|
-
let
|
|
1074
|
+
let native_tmux = observed.evidence_method.as_deref() == Some("native_tmux");
|
|
1075
|
+
let activity = if observed.provider.eq_ignore_ascii_case("pi")
|
|
1076
|
+
&& !pi_channel
|
|
1077
|
+
&& !native_tmux
|
|
1078
|
+
{
|
|
899
1079
|
"unknown"
|
|
900
1080
|
} else {
|
|
901
1081
|
valid_activity(&observed.activity)
|
|
902
1082
|
};
|
|
903
|
-
let health = if observed.provider.eq_ignore_ascii_case("pi") && !pi_channel {
|
|
1083
|
+
let health = if observed.provider.eq_ignore_ascii_case("pi") && !pi_channel && !native_tmux {
|
|
904
1084
|
"unknown"
|
|
905
1085
|
} else {
|
|
906
1086
|
valid_health(&observed.health)
|
|
@@ -1,37 +1,62 @@
|
|
|
1
1
|
//!
|
|
2
2
|
use super::*;
|
|
3
3
|
|
|
4
|
+
const SUMMARY_LIMIT: usize = 120;
|
|
5
|
+
|
|
6
|
+
fn clean_text(text: &str) -> String {
|
|
7
|
+
text.chars().filter(|ch| !ch.is_control()).collect()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
fn compact_field(value: Option<&Value>) -> Value {
|
|
11
|
+
value
|
|
12
|
+
.and_then(Value::as_str)
|
|
13
|
+
.map(|text| Value::String(clean_text(text)))
|
|
14
|
+
.unwrap_or(Value::Null)
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
pub fn inbox(
|
|
5
18
|
workspace: &Path,
|
|
6
19
|
agent: &str,
|
|
7
20
|
limit: usize,
|
|
8
|
-
since: Option<&str>,
|
|
9
|
-
as_json: bool,
|
|
10
21
|
owner_team_id: Option<&str>,
|
|
11
22
|
) -> Result<Value, CliError> {
|
|
12
|
-
let _ = as_json;
|
|
13
23
|
let store = crate::message_store::MessageStore::open(workspace)
|
|
14
24
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
15
|
-
let
|
|
25
|
+
let messages = store
|
|
16
26
|
.inbox(agent, limit, owner_team_id)
|
|
17
|
-
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.get("created_at")
|
|
22
|
-
.and_then(Value::as_str)
|
|
23
|
-
.and_then(parse_rfc3339)
|
|
24
|
-
.is_some_and(|created| created >= cutoff)
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?
|
|
28
|
+
.into_iter()
|
|
29
|
+
.map(compact_message)
|
|
30
|
+
.collect::<Vec<_>>();
|
|
27
31
|
Ok(json!({
|
|
28
32
|
"ok": true,
|
|
29
33
|
"agent_id": agent,
|
|
30
34
|
"messages": messages,
|
|
31
|
-
"since": since,
|
|
32
35
|
}))
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
fn compact_message(message: Value) -> Value {
|
|
39
|
+
json!({
|
|
40
|
+
"message_id": compact_field(message.get("message_id")),
|
|
41
|
+
"sender": compact_field(message.get("sender")),
|
|
42
|
+
"recipient": compact_field(message.get("recipient")),
|
|
43
|
+
"status": compact_field(message.get("status")),
|
|
44
|
+
"created_at": compact_field(message.get("created_at")),
|
|
45
|
+
"summary": compact_summary(message.get("content").and_then(Value::as_str).unwrap_or("")),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn compact_summary(content: &str) -> String {
|
|
50
|
+
let summary = content
|
|
51
|
+
.lines()
|
|
52
|
+
.map(|line| clean_text(line).trim().to_string())
|
|
53
|
+
.find(|line| !line.is_empty())
|
|
54
|
+
.unwrap_or_default();
|
|
55
|
+
let mut chars = summary.chars();
|
|
56
|
+
let truncated = chars.by_ref().take(SUMMARY_LIMIT).collect::<String>();
|
|
57
|
+
if chars.next().is_some() {
|
|
58
|
+
format!("{truncated}…")
|
|
59
|
+
} else {
|
|
60
|
+
truncated
|
|
61
|
+
}
|
|
37
62
|
}
|
|
@@ -211,36 +211,44 @@ fn approvals_golden_shape_has_waiting_count_and_scan_not_agent() {
|
|
|
211
211
|
let _ = std::fs::remove_dir_all(&ws);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
// ── inbox:
|
|
215
|
-
// `since` (not `agent`/`limit`). RUST mod.rs:136-144 stub {ok,agent,limit,messages}. RED. ──────────
|
|
214
|
+
// ── inbox: compact fallback projection ───────────────────────────────────────────────
|
|
216
215
|
#[test]
|
|
217
|
-
fn
|
|
216
|
+
fn inbox_golden_shape_is_compact_and_scoped_to_agent() {
|
|
218
217
|
let ws = tmp_workspace();
|
|
219
|
-
let v = status_port::inbox(&ws, "alpha",
|
|
218
|
+
let v = status_port::inbox(&ws, "alpha", 3, None).expect("inbox");
|
|
220
219
|
let obj = v.as_object().expect("inbox dict");
|
|
220
|
+
assert_eq!(v["agent_id"], json!("alpha"));
|
|
221
221
|
let order: Vec<&str> = obj.keys().map(String::as_str).collect();
|
|
222
|
-
assert_eq!(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
222
|
+
assert_eq!(order, vec!["ok", "agent_id", "messages"]);
|
|
223
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn inbox_excludes_leader_stage_result_notifications() {
|
|
228
|
+
let ws = tmp_workspace();
|
|
229
|
+
let store = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
230
|
+
let message_id = store
|
|
231
|
+
.create_message(
|
|
232
|
+
None,
|
|
233
|
+
"leader",
|
|
234
|
+
"worker",
|
|
235
|
+
"Task t reported success; Result id: res-1",
|
|
236
|
+
None,
|
|
237
|
+
false,
|
|
238
|
+
None,
|
|
239
|
+
)
|
|
240
|
+
.unwrap();
|
|
241
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
242
|
+
conn.execute(
|
|
243
|
+
"update messages set presentation = ?1 where message_id = ?2",
|
|
244
|
+
rusqlite::params![
|
|
245
|
+
json!({"sink": "leader", "class": "stage_result"}).to_string(),
|
|
246
|
+
message_id,
|
|
247
|
+
],
|
|
248
|
+
)
|
|
249
|
+
.unwrap();
|
|
250
|
+
let value = status_port::inbox(&ws, "leader", 3, None).expect("inbox");
|
|
251
|
+
assert!(value["messages"].as_array().unwrap().is_empty());
|
|
244
252
|
let _ = std::fs::remove_dir_all(&ws);
|
|
245
253
|
}
|
|
246
254
|
|