@team-agent/installer 0.3.1 → 0.3.3
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 +34 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +234 -26
- package/crates/team-agent/src/cli/diagnose.rs +144 -10
- package/crates/team-agent/src/cli/emit.rs +289 -54
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +1281 -196
- package/crates/team-agent/src/cli/status_port.rs +195 -46
- package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +59 -3
- package/crates/team-agent/src/cli/types.rs +18 -0
- package/crates/team-agent/src/compiler.rs +15 -5
- package/crates/team-agent/src/coordinator/health.rs +95 -17
- package/crates/team-agent/src/coordinator/mod.rs +4 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
- package/crates/team-agent/src/coordinator/tick.rs +222 -69
- package/crates/team-agent/src/coordinator/types.rs +15 -3
- package/crates/team-agent/src/db/schema.rs +37 -2
- package/crates/team-agent/src/diagnose/comms.rs +226 -0
- package/crates/team-agent/src/diagnose/mod.rs +45 -0
- package/crates/team-agent/src/diagnose/orphans.rs +658 -0
- package/crates/team-agent/src/fake_worker.rs +146 -3
- package/crates/team-agent/src/leader/start.rs +121 -23
- package/crates/team-agent/src/leader/types.rs +44 -1
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +645 -47
- package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
- package/crates/team-agent/src/lifecycle/mod.rs +2 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
- package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
- package/crates/team-agent/src/lifecycle/types.rs +19 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
- package/crates/team-agent/src/mcp_server/mod.rs +3 -74
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
- package/crates/team-agent/src/mcp_server/tools.rs +312 -111
- package/crates/team-agent/src/mcp_server/types.rs +6 -4
- package/crates/team-agent/src/mcp_server/wire.rs +19 -7
- package/crates/team-agent/src/message_store.rs +21 -4
- package/crates/team-agent/src/messaging/delivery.rs +470 -59
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +353 -63
- package/crates/team-agent/src/messaging/selftest.rs +199 -12
- package/crates/team-agent/src/messaging/send.rs +35 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
- package/crates/team-agent/src/messaging/types.rs +11 -3
- package/crates/team-agent/src/os_probe.rs +119 -0
- package/crates/team-agent/src/packaging/migrate.rs +10 -2
- package/crates/team-agent/src/packaging/tests.rs +23 -0
- package/crates/team-agent/src/provider/adapter.rs +564 -63
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
- package/crates/team-agent/src/provider/classify.rs +51 -4
- package/crates/team-agent/src/provider/helpers.rs +10 -1
- package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
- package/crates/team-agent/src/provider/types.rs +47 -0
- package/crates/team-agent/src/session_capture.rs +616 -0
- package/crates/team-agent/src/state/persist.rs +170 -1
- package/crates/team-agent/src/state/projection.rs +141 -8
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +161 -64
- package/crates/team-agent/src/transport/test_support.rs +9 -0
- package/crates/team-agent/src/transport/tests/wire.rs +4 -0
- package/crates/team-agent/src/transport.rs +13 -2
- package/package.json +4 -4
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
use std::path::Path;
|
|
4
4
|
|
|
5
5
|
use crate::model::ids::TeamKey;
|
|
6
|
+
use crate::message_store::MessageStore;
|
|
6
7
|
|
|
7
8
|
use super::helpers::next_run_id;
|
|
8
9
|
use super::{
|
|
9
|
-
CheckEvidence, CheckKind, CheckStatus, IdleEvaluation, MessagingError,
|
|
10
|
-
SelftestCheck, SelftestReport,
|
|
10
|
+
CheckEvidence, CheckKind, CheckStatus, ContractSuiteCheck, IdleEvaluation, MessagingError,
|
|
11
|
+
ProviderSdkCalls, SelftestCheck, SelftestReport,
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
/// selftest driver (`diagnose/comms.py` `CommsSelftestDriver`):**零 token / 零 provider SDK**
|
|
@@ -29,6 +30,7 @@ pub fn run_comms_selftest(
|
|
|
29
30
|
team: Option<&TeamKey>,
|
|
30
31
|
driver: &dyn CommsSelftestDriver,
|
|
31
32
|
) -> Result<SelftestReport, MessagingError> {
|
|
33
|
+
let run_id = driver.run_id().unwrap_or_else(next_run_id);
|
|
32
34
|
let binding = driver.receiver_binding(workspace, team);
|
|
33
35
|
let mismatches = binding
|
|
34
36
|
.get("mismatches")
|
|
@@ -43,29 +45,33 @@ pub fn run_comms_selftest(
|
|
|
43
45
|
let receiver_status = if mismatches.is_empty() { CheckStatus::Pass } else { CheckStatus::Fail };
|
|
44
46
|
let calls = driver.provider_sdk_calls();
|
|
45
47
|
let provider_status = if calls.is_zero() { CheckStatus::Pass } else { CheckStatus::Fail };
|
|
46
|
-
let
|
|
48
|
+
let contract_checks = run_contract_suite(workspace, team, &run_id);
|
|
49
|
+
let contract_status = if contract_checks.iter().all(|check| check.status == CheckStatus::Pass) {
|
|
50
|
+
CheckStatus::Pass
|
|
51
|
+
} else {
|
|
52
|
+
CheckStatus::Fail
|
|
53
|
+
};
|
|
47
54
|
let receiver_binding = SelftestCheck {
|
|
48
55
|
status: receiver_status,
|
|
49
56
|
verifies: CheckKind::ReceiverBinding,
|
|
50
|
-
evidence: CheckEvidence::Binding { mismatches },
|
|
57
|
+
evidence: CheckEvidence::Binding { mismatches, details: binding },
|
|
51
58
|
};
|
|
52
59
|
let contract_suite = SelftestCheck {
|
|
53
|
-
status:
|
|
60
|
+
status: contract_status,
|
|
54
61
|
verifies: CheckKind::ContractSuite,
|
|
55
|
-
evidence: CheckEvidence::
|
|
62
|
+
evidence: CheckEvidence::ContractSuite { checks: contract_checks },
|
|
56
63
|
};
|
|
57
64
|
let provider_sdk_calls = SelftestCheck {
|
|
58
65
|
status: provider_status,
|
|
59
66
|
verifies: CheckKind::NoProviderSdkCalls,
|
|
60
67
|
evidence: CheckEvidence::ProviderSdkCalls(calls),
|
|
61
68
|
};
|
|
69
|
+
let ok = receiver_binding.status == CheckStatus::Pass
|
|
70
|
+
&& contract_suite.status == CheckStatus::Pass
|
|
71
|
+
&& provider_sdk_calls.status == CheckStatus::Pass;
|
|
62
72
|
Ok(SelftestReport {
|
|
63
|
-
ok
|
|
64
|
-
status: if
|
|
65
|
-
CheckStatus::Pass
|
|
66
|
-
} else {
|
|
67
|
-
CheckStatus::Fail
|
|
68
|
-
},
|
|
73
|
+
ok,
|
|
74
|
+
status: if ok { CheckStatus::Pass } else { CheckStatus::Fail },
|
|
69
75
|
run_id,
|
|
70
76
|
scope: "binding_consistency".to_string(),
|
|
71
77
|
boundary: "messaging".to_string(),
|
|
@@ -75,6 +81,187 @@ pub fn run_comms_selftest(
|
|
|
75
81
|
})
|
|
76
82
|
}
|
|
77
83
|
|
|
84
|
+
fn run_contract_suite(
|
|
85
|
+
workspace: &Path,
|
|
86
|
+
team: Option<&TeamKey>,
|
|
87
|
+
run_id: &str,
|
|
88
|
+
) -> Vec<ContractSuiteCheck> {
|
|
89
|
+
let scratch = std::env::temp_dir().join(format!(
|
|
90
|
+
"ta-comms-contract-{run_id}-{}",
|
|
91
|
+
std::process::id()
|
|
92
|
+
));
|
|
93
|
+
let _ = std::fs::remove_dir_all(&scratch);
|
|
94
|
+
let mut checks = Vec::new();
|
|
95
|
+
let mut scratch_store = match MessageStore::open(&scratch) {
|
|
96
|
+
Ok(store) => {
|
|
97
|
+
checks.push(contract_check("message_store_schema", CheckStatus::Pass, None));
|
|
98
|
+
Some(store)
|
|
99
|
+
}
|
|
100
|
+
Err(error) => {
|
|
101
|
+
checks.push(contract_check(
|
|
102
|
+
"message_store_schema",
|
|
103
|
+
CheckStatus::Fail,
|
|
104
|
+
Some(error.to_string()),
|
|
105
|
+
));
|
|
106
|
+
None
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
match scratch_store.as_ref().and_then(|store| {
|
|
111
|
+
store
|
|
112
|
+
.create_message(
|
|
113
|
+
None,
|
|
114
|
+
"doctor",
|
|
115
|
+
"worker",
|
|
116
|
+
"comms contract probe",
|
|
117
|
+
None,
|
|
118
|
+
false,
|
|
119
|
+
Some("contract-team"),
|
|
120
|
+
)
|
|
121
|
+
.ok()
|
|
122
|
+
}) {
|
|
123
|
+
Some(message_id) if message_id.starts_with("msg_") && message_id.len() > 4 => {
|
|
124
|
+
let rendered = format!(
|
|
125
|
+
"Team Agent message from doctor:\n\ncomms contract probe\n\n[team-agent-token:{message_id}]"
|
|
126
|
+
);
|
|
127
|
+
let token = format!("[team-agent-token:{message_id}]");
|
|
128
|
+
checks.push(contract_check(
|
|
129
|
+
"message_token_shape",
|
|
130
|
+
if rendered.ends_with(&token) { CheckStatus::Pass } else { CheckStatus::Fail },
|
|
131
|
+
(!rendered.ends_with(&token)).then(|| "rendered token suffix missing".to_string()),
|
|
132
|
+
));
|
|
133
|
+
}
|
|
134
|
+
Some(_) => checks.push(contract_check(
|
|
135
|
+
"message_token_shape",
|
|
136
|
+
CheckStatus::Fail,
|
|
137
|
+
Some("message id did not use msg_ prefix".to_string()),
|
|
138
|
+
)),
|
|
139
|
+
None => checks.push(contract_check(
|
|
140
|
+
"message_token_shape",
|
|
141
|
+
CheckStatus::Fail,
|
|
142
|
+
Some("could not create scratch message".to_string()),
|
|
143
|
+
)),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let result_id = format!("res-comms-{run_id}");
|
|
147
|
+
let result_content = super::watchers::format_result_watcher_notification(&serde_json::json!({
|
|
148
|
+
"result_id": result_id,
|
|
149
|
+
"task_id": "comms-contract",
|
|
150
|
+
"agent_id": "doctor",
|
|
151
|
+
"status": "success",
|
|
152
|
+
"summary": "contract suite probe",
|
|
153
|
+
}));
|
|
154
|
+
let parsed_result_id = super::watchers::result_id_from_text(&result_content);
|
|
155
|
+
checks.push(contract_check(
|
|
156
|
+
"result_notification_render",
|
|
157
|
+
if parsed_result_id.as_deref() == Some(result_id.as_str()) {
|
|
158
|
+
CheckStatus::Pass
|
|
159
|
+
} else {
|
|
160
|
+
CheckStatus::Fail
|
|
161
|
+
},
|
|
162
|
+
(parsed_result_id.as_deref() != Some(result_id.as_str()))
|
|
163
|
+
.then(|| "result notification did not round-trip result_id".to_string()),
|
|
164
|
+
));
|
|
165
|
+
|
|
166
|
+
let selected = crate::state::selector::resolve_active_team(
|
|
167
|
+
workspace,
|
|
168
|
+
team.map(TeamKey::as_str),
|
|
169
|
+
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
170
|
+
);
|
|
171
|
+
match (scratch_store.take(), selected) {
|
|
172
|
+
(Some(store), Ok(selected)) => {
|
|
173
|
+
let owner_team = selected.team_key;
|
|
174
|
+
let mut state = selected.state;
|
|
175
|
+
if !state.is_object() {
|
|
176
|
+
state = serde_json::json!({});
|
|
177
|
+
}
|
|
178
|
+
if let Some(obj) = state.as_object_mut() {
|
|
179
|
+
obj.insert(
|
|
180
|
+
"active_team_key".to_string(),
|
|
181
|
+
serde_json::json!(owner_team.clone()),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
let event_log = crate::event_log::EventLog::new(&scratch);
|
|
185
|
+
let outcome = super::leader_receiver::send_to_leader_receiver(
|
|
186
|
+
&scratch,
|
|
187
|
+
&state,
|
|
188
|
+
"leader",
|
|
189
|
+
"comms contract leader projection",
|
|
190
|
+
None,
|
|
191
|
+
"doctor",
|
|
192
|
+
false,
|
|
193
|
+
Some("comms-contract-result"),
|
|
194
|
+
&event_log,
|
|
195
|
+
);
|
|
196
|
+
match outcome {
|
|
197
|
+
Ok(outcome) => {
|
|
198
|
+
let actual_owner = outcome
|
|
199
|
+
.message_id
|
|
200
|
+
.as_deref()
|
|
201
|
+
.and_then(|message_id| message_owner_team(store.db_path(), message_id).ok().flatten());
|
|
202
|
+
checks.push(contract_check(
|
|
203
|
+
"leader_projection_owner_team",
|
|
204
|
+
if actual_owner.as_deref() == Some(owner_team.as_str()) {
|
|
205
|
+
CheckStatus::Pass
|
|
206
|
+
} else {
|
|
207
|
+
CheckStatus::Fail
|
|
208
|
+
},
|
|
209
|
+
(actual_owner.as_deref() != Some(owner_team.as_str())).then(|| {
|
|
210
|
+
format!(
|
|
211
|
+
"leader-bound message owner_team_id={:?}, expected={owner_team}",
|
|
212
|
+
actual_owner
|
|
213
|
+
)
|
|
214
|
+
}),
|
|
215
|
+
));
|
|
216
|
+
}
|
|
217
|
+
Err(error) => checks.push(contract_check(
|
|
218
|
+
"leader_projection_owner_team",
|
|
219
|
+
CheckStatus::Fail,
|
|
220
|
+
Some(error.to_string()),
|
|
221
|
+
)),
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
(_, Err(error)) => checks.push(contract_check(
|
|
225
|
+
"leader_projection_owner_team",
|
|
226
|
+
CheckStatus::Fail,
|
|
227
|
+
Some(error.to_string()),
|
|
228
|
+
)),
|
|
229
|
+
(None, _) => checks.push(contract_check(
|
|
230
|
+
"leader_projection_owner_team",
|
|
231
|
+
CheckStatus::Fail,
|
|
232
|
+
Some("message store schema unavailable".to_string()),
|
|
233
|
+
)),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let _ = std::fs::remove_dir_all(&scratch);
|
|
237
|
+
checks
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn contract_check(
|
|
241
|
+
name: &str,
|
|
242
|
+
status: CheckStatus,
|
|
243
|
+
reason: Option<String>,
|
|
244
|
+
) -> ContractSuiteCheck {
|
|
245
|
+
ContractSuiteCheck {
|
|
246
|
+
name: name.to_string(),
|
|
247
|
+
status,
|
|
248
|
+
reason,
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn message_owner_team(
|
|
253
|
+
db_path: &Path,
|
|
254
|
+
message_id: &str,
|
|
255
|
+
) -> Result<Option<String>, MessagingError> {
|
|
256
|
+
let conn = crate::db::schema::open_db(db_path)?;
|
|
257
|
+
let owner = conn.query_row(
|
|
258
|
+
"select owner_team_id from messages where message_id = ?1",
|
|
259
|
+
rusqlite::params![message_id],
|
|
260
|
+
|row| row.get::<_, Option<String>>(0),
|
|
261
|
+
)?;
|
|
262
|
+
Ok(owner)
|
|
263
|
+
}
|
|
264
|
+
|
|
78
265
|
/// `evaluate_idle_behavior` (`diagnose/comms.py:50`):idle 分类准确性评估。零 token,走 driver。
|
|
79
266
|
pub fn evaluate_idle_behavior(
|
|
80
267
|
workspace: &Path,
|
|
@@ -138,9 +138,6 @@ pub fn send_message(
|
|
|
138
138
|
};
|
|
139
139
|
// send.py:259-261 — a non-leader target that is NOT a known team agent is refused
|
|
140
140
|
// (target_not_in_team), NOT persisted. Membership = the runtime state's `agents` map.
|
|
141
|
-
if let Some(outcome) = send_owner_gate_refusal(workspace, &state, &opts.sender)? {
|
|
142
|
-
return Ok(outcome);
|
|
143
|
-
}
|
|
144
141
|
let in_team = state
|
|
145
142
|
.get("agents")
|
|
146
143
|
.and_then(|a| a.as_object())
|
|
@@ -148,6 +145,19 @@ pub fn send_message(
|
|
|
148
145
|
if !in_team {
|
|
149
146
|
return Ok(refused_outcome(DeliveryRefusal::TargetNotInTeam));
|
|
150
147
|
}
|
|
148
|
+
if let Some(outcome) = session_drift_refusal(
|
|
149
|
+
&state,
|
|
150
|
+
recipient,
|
|
151
|
+
"leader",
|
|
152
|
+
&opts.sender,
|
|
153
|
+
opts.task_id.as_ref(),
|
|
154
|
+
&event_log,
|
|
155
|
+
)? {
|
|
156
|
+
return Ok(outcome);
|
|
157
|
+
}
|
|
158
|
+
if let Some(outcome) = send_owner_gate_refusal(workspace, &state, &opts.sender)? {
|
|
159
|
+
return Ok(outcome);
|
|
160
|
+
}
|
|
151
161
|
if opts.route_task_id {
|
|
152
162
|
if let Some(task_id) = opts.task_id.as_ref() {
|
|
153
163
|
if !task_exists(&state, task_id) {
|
|
@@ -367,6 +377,14 @@ fn owner_gate_hint_team_key(state: &serde_json::Value) -> String {
|
|
|
367
377
|
}
|
|
368
378
|
|
|
369
379
|
fn owner_pane_is_dead(state: &serde_json::Value) -> bool {
|
|
380
|
+
if state
|
|
381
|
+
.get("leader_receiver")
|
|
382
|
+
.and_then(|receiver| receiver.get("status"))
|
|
383
|
+
.and_then(serde_json::Value::as_str)
|
|
384
|
+
.is_some_and(|status| status == "unbound")
|
|
385
|
+
{
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
370
388
|
let Some(pane_id) = state
|
|
371
389
|
.get("team_owner")
|
|
372
390
|
.and_then(|owner| owner.get("pane_id"))
|
|
@@ -375,6 +393,9 @@ fn owner_pane_is_dead(state: &serde_json::Value) -> bool {
|
|
|
375
393
|
else {
|
|
376
394
|
return false;
|
|
377
395
|
};
|
|
396
|
+
if pane_id == "__team_agent_unbound__" {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
378
399
|
if pane_id.contains("dead") {
|
|
379
400
|
return true;
|
|
380
401
|
}
|
|
@@ -519,12 +540,15 @@ fn fanout_send(
|
|
|
519
540
|
channel_label: &str,
|
|
520
541
|
) -> Result<DeliveryOutcome, MessagingError> {
|
|
521
542
|
let mut last_message_id: Option<String> = None;
|
|
543
|
+
let mut first_failure: Option<DeliveryOutcome> = None;
|
|
522
544
|
let mut any_failure = false;
|
|
523
545
|
let mut delivered_count = 0usize;
|
|
546
|
+
let mut attempted_count = 0usize;
|
|
524
547
|
for recipient in recipients {
|
|
525
548
|
if recipient.is_empty() || recipient == &opts.sender {
|
|
526
549
|
continue;
|
|
527
550
|
}
|
|
551
|
+
attempted_count = attempted_count.saturating_add(1);
|
|
528
552
|
let outcome = if recipient == "leader" {
|
|
529
553
|
send_to_leader_receiver(
|
|
530
554
|
workspace,
|
|
@@ -556,6 +580,14 @@ fn fanout_send(
|
|
|
556
580
|
}
|
|
557
581
|
} else {
|
|
558
582
|
any_failure = true;
|
|
583
|
+
if first_failure.is_none() {
|
|
584
|
+
first_failure = Some(outcome);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if delivered_count == 0 && attempted_count == 1 {
|
|
589
|
+
if let Some(outcome) = first_failure {
|
|
590
|
+
return Ok(outcome);
|
|
559
591
|
}
|
|
560
592
|
}
|
|
561
593
|
let status = if any_failure {
|
|
@@ -957,16 +957,31 @@ fn run_comms_selftest_nonzero_provider_sdk_fails_gate() {
|
|
|
957
957
|
}
|
|
958
958
|
|
|
959
959
|
#[test]
|
|
960
|
-
fn
|
|
961
|
-
// diagnose/comms.py:132-139 — contract_suite is always deferred (test files
|
|
962
|
-
// not shipped) and counts as a pass for the overall gate.
|
|
960
|
+
fn run_comms_selftest_contract_suite_is_executable_zero_token() {
|
|
963
961
|
let ws = tmp_ws("selftestdefer");
|
|
964
962
|
let driver = ZeroSdkDriver {
|
|
965
963
|
run_id: Some("r3".to_string()),
|
|
966
964
|
calls: ProviderSdkCalls::default(),
|
|
967
965
|
};
|
|
968
966
|
let report = run_comms_selftest(&ws, None, &driver).unwrap();
|
|
969
|
-
assert_eq!(report.contract_suite.status, CheckStatus::
|
|
967
|
+
assert_eq!(report.contract_suite.status, CheckStatus::Pass);
|
|
968
|
+
match &report.contract_suite.evidence {
|
|
969
|
+
CheckEvidence::ContractSuite { checks } => {
|
|
970
|
+
assert!(
|
|
971
|
+
checks.iter().all(|check| check.status == CheckStatus::Pass),
|
|
972
|
+
"contract suite subchecks must all pass: {checks:?}"
|
|
973
|
+
);
|
|
974
|
+
let names: Vec<&str> = checks.iter().map(|check| check.name.as_str()).collect();
|
|
975
|
+
assert!(
|
|
976
|
+
names.contains(&"message_store_schema")
|
|
977
|
+
&& names.contains(&"message_token_shape")
|
|
978
|
+
&& names.contains(&"result_notification_render")
|
|
979
|
+
&& names.contains(&"leader_projection_owner_team"),
|
|
980
|
+
"contract suite must cover schema, token, result notification, and leader projection: {names:?}"
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
other => panic!("expected ContractSuite evidence, got {other:?}"),
|
|
984
|
+
}
|
|
970
985
|
}
|
|
971
986
|
|
|
972
987
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -229,9 +229,17 @@ pub enum CheckEvidence {
|
|
|
229
229
|
/// `no_provider_sdk_calls` 的机械证据 (§84):三 SDK 调用计数。
|
|
230
230
|
ProviderSdkCalls(ProviderSdkCalls),
|
|
231
231
|
/// binding 一致性比对结果 (mismatch 列表)。
|
|
232
|
-
Binding { mismatches: Vec<String
|
|
233
|
-
///
|
|
234
|
-
|
|
232
|
+
Binding { mismatches: Vec<String>, details: serde_json::Value },
|
|
233
|
+
/// executable zero-token comms contract suite evidence.
|
|
234
|
+
ContractSuite { checks: Vec<ContractSuiteCheck> },
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// One executable zero-token comms contract-suite subcheck.
|
|
238
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
239
|
+
pub struct ContractSuiteCheck {
|
|
240
|
+
pub name: String,
|
|
241
|
+
pub status: CheckStatus,
|
|
242
|
+
pub reason: Option<String>,
|
|
235
243
|
}
|
|
236
244
|
|
|
237
245
|
/// **机械门** (§84/MUST-NOT-13;`diagnose/comms.py:142`):selftest 路径 provider SDK 调用
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
use std::cell::RefCell;
|
|
2
|
+
use std::fs::OpenOptions;
|
|
3
|
+
use std::io::{self, Read};
|
|
4
|
+
use std::process::{Command, ExitStatus, Stdio};
|
|
5
|
+
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TIMEOUT: Duration = Duration::from_millis(900);
|
|
8
|
+
|
|
9
|
+
thread_local! {
|
|
10
|
+
static PROBE_TIMEOUT: RefCell<Option<ProbeTimeout>> = const { RefCell::new(None) };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[derive(Clone, Debug)]
|
|
14
|
+
pub(crate) struct ProbeTimeout {
|
|
15
|
+
pub(crate) probe: &'static str,
|
|
16
|
+
pub(crate) pid: Option<u32>,
|
|
17
|
+
pub(crate) timeout_ms: u64,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Debug)]
|
|
21
|
+
pub(crate) struct BoundedCommandOutput {
|
|
22
|
+
pub(crate) status: ExitStatus,
|
|
23
|
+
pub(crate) stdout: Vec<u8>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub(crate) fn clear_probe_timeout() {
|
|
27
|
+
PROBE_TIMEOUT.with(|timeout| *timeout.borrow_mut() = None);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub(crate) fn probe_timed_out() -> bool {
|
|
31
|
+
PROBE_TIMEOUT.with(|timeout| timeout.borrow().is_some())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub(crate) fn probe_timeout() -> Option<ProbeTimeout> {
|
|
35
|
+
PROBE_TIMEOUT.with(|timeout| timeout.borrow().clone())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub(crate) fn bounded_command_output_with_probe(
|
|
39
|
+
command: &mut Command,
|
|
40
|
+
probe: &'static str,
|
|
41
|
+
pid: Option<u32>,
|
|
42
|
+
) -> io::Result<BoundedCommandOutput> {
|
|
43
|
+
bounded_command_output_with_timeout(command, DEFAULT_TIMEOUT, probe, pid)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn bounded_command_output_with_timeout(
|
|
47
|
+
command: &mut Command,
|
|
48
|
+
timeout: Duration,
|
|
49
|
+
probe: &'static str,
|
|
50
|
+
pid: Option<u32>,
|
|
51
|
+
) -> io::Result<BoundedCommandOutput> {
|
|
52
|
+
let stdout_path = temp_output_path("stdout");
|
|
53
|
+
let stdout_file = OpenOptions::new()
|
|
54
|
+
.create_new(true)
|
|
55
|
+
.read(true)
|
|
56
|
+
.write(true)
|
|
57
|
+
.open(&stdout_path)?;
|
|
58
|
+
let child = command
|
|
59
|
+
.stdout(Stdio::from(stdout_file.try_clone()?))
|
|
60
|
+
.stderr(Stdio::null())
|
|
61
|
+
.spawn()?;
|
|
62
|
+
wait_for_bounded_child(child, stdout_file, stdout_path, timeout, probe, pid)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn wait_for_bounded_child(
|
|
66
|
+
mut child: std::process::Child,
|
|
67
|
+
stdout_file: std::fs::File,
|
|
68
|
+
stdout_path: std::path::PathBuf,
|
|
69
|
+
timeout: Duration,
|
|
70
|
+
probe: &'static str,
|
|
71
|
+
pid: Option<u32>,
|
|
72
|
+
) -> io::Result<BoundedCommandOutput> {
|
|
73
|
+
let start = Instant::now();
|
|
74
|
+
loop {
|
|
75
|
+
if let Some(status) = child.try_wait()? {
|
|
76
|
+
drop(stdout_file);
|
|
77
|
+
let stdout = read_and_remove(&stdout_path);
|
|
78
|
+
return Ok(BoundedCommandOutput { status, stdout });
|
|
79
|
+
}
|
|
80
|
+
if start.elapsed() >= timeout {
|
|
81
|
+
PROBE_TIMEOUT.with(|current| {
|
|
82
|
+
let mut current = current.borrow_mut();
|
|
83
|
+
if current.is_none() {
|
|
84
|
+
*current = Some(ProbeTimeout {
|
|
85
|
+
probe,
|
|
86
|
+
pid,
|
|
87
|
+
timeout_ms: timeout.as_millis() as u64,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
let _ = child.kill();
|
|
92
|
+
let status = child.wait()?;
|
|
93
|
+
drop(stdout_file);
|
|
94
|
+
let stdout = read_and_remove(&stdout_path);
|
|
95
|
+
return Ok(BoundedCommandOutput { status, stdout });
|
|
96
|
+
}
|
|
97
|
+
std::thread::sleep(Duration::from_millis(10));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fn temp_output_path(kind: &str) -> std::path::PathBuf {
|
|
102
|
+
let nanos = SystemTime::now()
|
|
103
|
+
.duration_since(UNIX_EPOCH)
|
|
104
|
+
.map(|duration| duration.as_nanos())
|
|
105
|
+
.unwrap_or(0);
|
|
106
|
+
std::env::temp_dir().join(format!(
|
|
107
|
+
"team-agent-os-probe-{}-{nanos}.{kind}",
|
|
108
|
+
std::process::id()
|
|
109
|
+
))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn read_and_remove(path: &std::path::Path) -> Vec<u8> {
|
|
113
|
+
let mut stdout = Vec::new();
|
|
114
|
+
if let Ok(mut file) = std::fs::File::open(path) {
|
|
115
|
+
let _ = file.read_to_end(&mut stdout);
|
|
116
|
+
}
|
|
117
|
+
let _ = std::fs::remove_file(path);
|
|
118
|
+
stdout
|
|
119
|
+
}
|
|
@@ -13,8 +13,16 @@ pub fn doctor(opts: &DoctorOptions) -> Result<DoctorStatus, PackagingError> {
|
|
|
13
13
|
if opts.fix && opts.gate.is_none() {
|
|
14
14
|
return Err(PackagingError::InvalidOptions("--fix requires --gate".to_string()));
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
let gate_blockers = crate::diagnose::doctor_gate_blockers(
|
|
17
|
+
&opts.workspace,
|
|
18
|
+
opts.gate,
|
|
19
|
+
opts.fix,
|
|
20
|
+
opts.confirm,
|
|
21
|
+
)?;
|
|
22
|
+
if !gate_blockers.is_empty() {
|
|
23
|
+
return Ok(DoctorStatus::HasBlockers {
|
|
24
|
+
blockers: gate_blockers,
|
|
25
|
+
});
|
|
18
26
|
}
|
|
19
27
|
let diagnosis = schema_diagnosis_workspace(&opts.workspace)?;
|
|
20
28
|
if diagnosis.layout_diffs.is_empty() {
|
|
@@ -269,6 +269,29 @@ fn doctor_on_clean_workspace_no_drift_is_ok() {
|
|
|
269
269
|
assert_eq!(status, DoctorStatus::Ok);
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
#[test]
|
|
273
|
+
fn doctor_comms_gate_failure_maps_to_typed_blocker() {
|
|
274
|
+
let dir = std::env::temp_dir().join(format!("ta-doctor-comms-{}", std::process::id()));
|
|
275
|
+
let _ = std::fs::create_dir_all(&dir);
|
|
276
|
+
let mut opts = doctor_opts(&dir);
|
|
277
|
+
opts.gate = Some(DoctorGate::Comms);
|
|
278
|
+
let status = doctor(&opts).expect("comms gate should return typed blockers");
|
|
279
|
+
match status {
|
|
280
|
+
DoctorStatus::HasBlockers { blockers } => {
|
|
281
|
+
let blocker = blockers
|
|
282
|
+
.iter()
|
|
283
|
+
.find(|blocker| blocker.source == BlockerSource::CommsGate)
|
|
284
|
+
.expect("must surface CommsGate blocker");
|
|
285
|
+
assert!(
|
|
286
|
+
blocker.detail.contains("receiver_binding"),
|
|
287
|
+
"blocker detail must name failing check: {}",
|
|
288
|
+
blocker.detail
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
DoctorStatus::Ok => panic!("missing receiver binding must not report Ok"),
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
272
295
|
#[test]
|
|
273
296
|
fn doctor_drifted_db_emits_schema_layout_drift_blocker() {
|
|
274
297
|
// STRENGTHENED (gate w59ds828k): the ONLY drift assertion previously
|