@team-agent/installer 0.5.100 → 0.5.102

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.
Files changed (63) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/diagnose.rs +29 -7
  4. package/crates/team-agent/src/cli/mod.rs +24 -8
  5. package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -74
  6. package/crates/team-agent/src/compiler/tests.rs +20 -23
  7. package/crates/team-agent/src/compiler.rs +52 -65
  8. package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +16 -48
  9. package/crates/team-agent/src/leader/start.rs +1 -14
  10. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +211 -16
  11. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +1 -0
  12. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +15 -5
  13. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +89 -6
  14. package/crates/team-agent/src/lifecycle/launch/role_source.rs +168 -32
  15. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  16. package/crates/team-agent/src/lifecycle/launch.rs +11 -3
  17. package/crates/team-agent/src/lifecycle/profile_launch.rs +23 -8
  18. package/crates/team-agent/src/lifecycle/restart/agent.rs +2 -2
  19. package/crates/team-agent/src/lifecycle/restart/avatar.rs +1806 -0
  20. package/crates/team-agent/src/lifecycle/restart/common.rs +115 -2
  21. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +8 -2
  22. package/crates/team-agent/src/lifecycle/restart/selection.rs +14 -2
  23. package/crates/team-agent/src/lifecycle/restart.rs +11 -4
  24. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +0 -28
  25. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +11 -9
  26. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -401
  27. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +2 -2
  28. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1398 -0
  29. package/crates/team-agent/src/lifecycle/tests.rs +1 -0
  30. package/crates/team-agent/src/lifecycle/types.rs +10 -2
  31. package/crates/team-agent/src/lifecycle/worker_command_context.rs +22 -8
  32. package/crates/team-agent/src/mcp_server/helpers.rs +0 -16
  33. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +2 -7
  34. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -127
  35. package/crates/team-agent/src/mcp_server/mod.rs +7 -12
  36. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -132
  37. package/crates/team-agent/src/mcp_server/tests/send.rs +1 -93
  38. package/crates/team-agent/src/mcp_server/tests/tools.rs +0 -61
  39. package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -51
  40. package/crates/team-agent/src/mcp_server/tests.rs +4 -200
  41. package/crates/team-agent/src/mcp_server/tools.rs +5 -396
  42. package/crates/team-agent/src/mcp_server/types.rs +3 -36
  43. package/crates/team-agent/src/mcp_server/wire.rs +2 -223
  44. package/crates/team-agent/src/messaging/leader_receiver.rs +1 -1
  45. package/crates/team-agent/src/messaging/mod.rs +2 -2
  46. package/crates/team-agent/src/messaging/scheduler.rs +2 -205
  47. package/crates/team-agent/src/messaging/tests/basic.rs +0 -31
  48. package/crates/team-agent/src/messaging/tests/main_preserved.rs +0 -73
  49. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -26
  50. package/crates/team-agent/src/messaging/types.rs +0 -20
  51. package/crates/team-agent/src/model/spec.rs +15 -18
  52. package/crates/team-agent/src/provider/types.rs +2 -0
  53. package/crates/team-agent/src/state/persist.rs +58 -2
  54. package/crates/team-agent/src/state/projection.rs +5 -1
  55. package/crates/team-agent/src/state/repository/tests.rs +1 -16
  56. package/crates/team-agent/src/state/repository.rs +126 -37
  57. package/crates/team-agent/src/transport/test_support.rs +23 -3
  58. package/package.json +4 -4
  59. package/skills/team-agent/SKILL.md +18 -47
  60. package/skills/team-agent/command-coverage.json +7 -0
  61. package/skills/team-agent/references/team-agent-operator.md +28 -118
  62. package/skills/team-agent/references/team-in-team.md +4 -19
  63. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +0 -484
@@ -82,6 +82,7 @@ mod pi_command_red;
82
82
  mod pi_compiler_red;
83
83
  mod pi_dual_entry_red;
84
84
  mod pi_executable_mcp_red;
85
+ mod pi_fork_session_inheritance_red;
85
86
  mod pi_lifecycle_doctor_red;
86
87
  mod pi_session_delivery_red;
87
88
  mod quick_start_worker_readiness_red;
@@ -605,8 +605,8 @@ pub struct AddAgentReport {
605
605
  pub role_file: PathBuf,
606
606
  }
607
607
 
608
- /// `fork_agent(...)` 结果(`operations.py:402`)。native session fork(provider 须
609
- /// supports_session_fork ∧ auth_mode!=compatible_api)。
608
+ /// `fork_agent(...)` 结果。Pi 的 snapshot fork 附带可审计的 source/target backing 细节;
609
+ /// 已支持的其他 provider 保持既有报告字段。
610
610
  #[derive(Debug, Clone, PartialEq, Eq)]
611
611
  pub struct ForkAgentReport {
612
612
  pub source_agent_id: AgentId,
@@ -614,6 +614,14 @@ pub struct ForkAgentReport {
614
614
  pub env: AgentActionEnvelope,
615
615
  pub session_id: Option<SessionId>,
616
616
  pub backing_state: ForkBackingState,
617
+ pub pi_fork: Option<PiForkDetails>,
618
+ }
619
+
620
+ #[derive(Debug, Clone, PartialEq, Eq)]
621
+ pub struct PiForkDetails {
622
+ pub source_session_id: SessionId,
623
+ pub session_id: SessionId,
624
+ pub backing_path: RolloutPath,
617
625
  }
618
626
 
619
627
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -30,6 +30,9 @@ All communication must go through Team Agent MCP tools.
30
30
  - Teammate: {send_message}(to='<agent_id>', content='...')
31
31
  - Broadcast: {send_message}(to='*', content='...')
32
32
  - Complete: {report_result}(summary='...') — call exactly once
33
+ - Optional roster/status: {get_team_status} — use only when useful; it is not a communication or completion prerequisite.
34
+
35
+ Only these three Team Agent MCP tools are available. Lifecycle management remains on the separately authorized operator CLI.
33
36
 
34
37
  ## Rules:
35
38
 
@@ -55,7 +58,7 @@ pub(crate) struct WorkerCommandAgent {
55
58
  system_prompt_file: Option<String>,
56
59
  output_contract_format: Option<String>,
57
60
  communication_mode: CommunicationMode,
58
- /// 0.5.66 bypass 单源:agent-level `dangerously_skip_permissions`(必填 bool)。
61
+ /// Missing `dangerously_skip_permissions` defaults to true; an explicit false remains respected.
59
62
  dangerously_skip_permissions: bool,
60
63
  }
61
64
 
@@ -107,9 +110,9 @@ impl WorkerCommandAgent {
107
110
  .get("communication_mode")
108
111
  .and_then(crate::model::yaml::Value::as_str),
109
112
  )?,
110
- dangerously_skip_permissions: matches!(
113
+ dangerously_skip_permissions: !matches!(
111
114
  agent.get("dangerously_skip_permissions"),
112
- Some(crate::model::yaml::Value::Bool(true))
115
+ Some(crate::model::yaml::Value::Bool(false))
113
116
  ),
114
117
  })
115
118
  }
@@ -164,7 +167,7 @@ impl WorkerCommandAgent {
164
167
  dangerously_skip_permissions: agent
165
168
  .get("dangerously_skip_permissions")
166
169
  .and_then(serde_json::Value::as_bool)
167
- .unwrap_or(false),
170
+ .unwrap_or(true),
168
171
  })
169
172
  }
170
173
  }
@@ -182,15 +185,20 @@ pub(crate) fn compile_worker_system_prompt(
182
185
  // C-1 cr verdict / B2 灵魂件 — identity 必须 FIRST(MUST-4 行为层守:空白上下文问
183
186
  // "你是谁"必须先答 Team Agent worker 身份)。runtime contract 跟后。
184
187
  let send_message = mcp_tool_name(agent.provider, "team_orchestrator", "send_message");
188
+ let get_team_status = if agent.provider == Provider::Pi {
189
+ r#"mcp({tool:"team_orchestrator_get_team_status", args:{}})"#.to_string()
190
+ } else {
191
+ format!("{}()", mcp_tool_name(agent.provider, "team_orchestrator", "get_team_status"))
192
+ };
185
193
  let report_result = if agent.provider == Provider::Pi {
186
194
  r#"mcp({tool:"team_orchestrator_report_result", args:{summary:"..."}})"#.to_string()
187
195
  } else {
188
196
  mcp_tool_name(agent.provider, "team_orchestrator", "report_result")
189
197
  };
190
198
  let runtime_contract = if agent.provider == Provider::Pi {
191
- pi_runtime_contract_section()
199
+ pi_runtime_contract_section(&get_team_status)
192
200
  } else {
193
- runtime_contract_section(&send_message, &report_result)
201
+ runtime_contract_section(&send_message, &report_result, &get_team_status)
194
202
  };
195
203
  let communication_contract = if agent.provider == Provider::Pi {
196
204
  pi_communication_contract(agent.communication_mode)
@@ -241,13 +249,18 @@ fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
241
249
  }
242
250
  }
243
251
 
244
- fn runtime_contract_section(send_message: &str, report_result: &str) -> String {
252
+ fn runtime_contract_section(
253
+ send_message: &str,
254
+ report_result: &str,
255
+ get_team_status: &str,
256
+ ) -> String {
245
257
  RUNTIME_CONTRACT_SECTION
246
258
  .replace("{send_message}", send_message)
247
259
  .replace("{report_result}", report_result)
260
+ .replace("{get_team_status}", get_team_status)
248
261
  }
249
262
 
250
- fn pi_runtime_contract_section() -> String {
263
+ fn pi_runtime_contract_section(get_team_status: &str) -> String {
251
264
  RUNTIME_CONTRACT_SECTION
252
265
  .replace(
253
266
  "{send_message}(to='<agent_id>', content='...')",
@@ -261,6 +274,7 @@ fn pi_runtime_contract_section() -> String {
261
274
  "{report_result}(summary='...')",
262
275
  r#"mcp({tool:"team_orchestrator_report_result", args:{summary:"..."}})"#,
263
276
  )
277
+ .replace("{get_team_status}", get_team_status)
264
278
  }
265
279
 
266
280
  fn pi_communication_contract(mode: CommunicationMode) -> String {
@@ -40,22 +40,6 @@ pub fn is_worker_recipient(to: &MessageTarget) -> bool {
40
40
  }
41
41
  }
42
42
 
43
- /// `_merge_tasks_by_id` (`tools.py:30-49`): dedupe a task list keyed by `id`,
44
- /// `prefer` entries winning on duplicates (so an earlier `done` is not regressed).
45
- pub fn merge_tasks_by_id(prefer: &[Value], fallback: &[Value]) -> Vec<Value> {
46
- let mut seen = std::collections::BTreeSet::new();
47
- let mut out = Vec::new();
48
- for item in prefer.iter().chain(fallback.iter()) {
49
- let Some(id) = item.get("id").and_then(Value::as_str) else {
50
- continue;
51
- };
52
- if seen.insert(id.to_string()) && item.is_object() {
53
- out.push(item.clone());
54
- }
55
- }
56
- out
57
- }
58
-
59
43
  pub(crate) fn tool_error_reason_wire(reason: ToolErrorReason) -> &'static str {
60
44
  match reason {
61
45
  ToolErrorReason::UnknownTool => "unknown_tool",
@@ -1,11 +1,6 @@
1
1
  //!
2
- //! MCP lifecycle tool facades.
3
- //!
4
- //! S0 keeps the old placeholder behavior behind stable module boundaries so
5
- //! follow-up lanes can implement real lifecycle logic without touching tools.rs.
2
+ //! MCP status query facade.
6
3
 
7
- mod agent_ops;
8
4
  mod state_status;
9
5
 
10
- pub(crate) use agent_ops::{clone_agent, fork_agent, reset_agent, stop_agent};
11
- pub(crate) use state_status::{get_team_status, update_state};
6
+ pub(crate) use state_status::get_team_status;
@@ -5,135 +5,9 @@ use serde_json::Value;
5
5
 
6
6
  use crate::model::ids::TeamKey;
7
7
 
8
- use super::super::helpers::{ensure_object, object_fields, tool_runtime_error};
8
+ use super::super::helpers::{object_fields, tool_runtime_error};
9
9
  use super::super::{ToolOk, ToolResult};
10
10
 
11
- pub(crate) fn update_state(
12
- workspace: &Path,
13
- owner_team: Option<&TeamKey>,
14
- note: &str,
15
- ) -> ToolResult {
16
- let selected = match crate::state::selector::resolve_active_team(
17
- workspace,
18
- owner_team.map(TeamKey::as_str),
19
- crate::state::selector::SelectorMode::RequireSpec,
20
- ) {
21
- Ok(selected) => selected,
22
- Err(err) if is_missing_active_spec(&err) => {
23
- return update_state_without_spec(workspace, owner_team, note);
24
- }
25
- Err(err) => return Err(tool_runtime_error(err)),
26
- };
27
- let state = crate::state::repository::StateRepository::new(&selected.run_workspace).commit(
28
- crate::state::repository::StateWriteIntent::McpUpdateStateNote {
29
- team_key: Some(&selected.team_key),
30
- },
31
- |latest| {
32
- ensure_object(latest);
33
- append_note(latest, note);
34
- },
35
- )
36
- .map_err(tool_runtime_error)?;
37
- let spec_path = selected
38
- .spec_path
39
- .ok_or_else(|| tool_runtime_error("active team spec not found for update_state"))?;
40
- let spec_workspace = spec_path.parent().ok_or_else(|| {
41
- tool_runtime_error(format!(
42
- "active team spec has no parent: {}",
43
- spec_path.display()
44
- ))
45
- })?;
46
- let spec_text = std::fs::read_to_string(&spec_path).map_err(tool_runtime_error)?;
47
- let spec = crate::model::yaml::loads(&spec_text).map_err(tool_runtime_error)?;
48
- let path = crate::lifecycle::restart::write_team_state(spec_workspace, &spec, &state)
49
- .map_err(tool_runtime_error)?;
50
- Ok(update_state_ok(path))
51
- }
52
-
53
- fn update_state_without_spec(
54
- workspace: &Path,
55
- owner_team: Option<&TeamKey>,
56
- note: &str,
57
- ) -> ToolResult {
58
- let selected = crate::state::selector::resolve_active_team(
59
- workspace,
60
- owner_team.map(TeamKey::as_str),
61
- crate::state::selector::SelectorMode::RuntimeOnly,
62
- )
63
- .map_err(tool_runtime_error)?;
64
- let state = crate::state::repository::StateRepository::new(&selected.run_workspace).commit(
65
- crate::state::repository::StateWriteIntent::McpUpdateStateNote {
66
- team_key: Some(&selected.team_key),
67
- },
68
- |latest| {
69
- ensure_object(latest);
70
- seed_legacy_team_key(latest, &selected.run_workspace, &selected.team_key);
71
- append_note(latest, note);
72
- },
73
- )
74
- .map_err(tool_runtime_error)?;
75
- let path = crate::lifecycle::restart::write_team_state(
76
- &selected.run_workspace,
77
- &crate::model::yaml::Value::Null,
78
- &state,
79
- )
80
- .map_err(tool_runtime_error)?;
81
- Ok(update_state_ok(path))
82
- }
83
-
84
- fn append_note(state: &mut Value, note: &str) {
85
- if let Some(obj) = state.as_object_mut() {
86
- let notes = obj
87
- .entry("notes".to_string())
88
- .or_insert_with(|| Value::Array(Vec::new()));
89
- if !notes.is_array() {
90
- *notes = Value::Array(Vec::new());
91
- }
92
- if let Some(items) = notes.as_array_mut() {
93
- items.push(Value::String(note.to_string()));
94
- }
95
- }
96
- }
97
-
98
- fn seed_legacy_team_key(state: &mut Value, run_workspace: &Path, team_key: &str) {
99
- if state.get("team_dir").and_then(Value::as_str).is_some()
100
- || state.get("spec_path").and_then(Value::as_str).is_some()
101
- || state.get("session_name").and_then(Value::as_str).is_some()
102
- {
103
- return;
104
- }
105
- if let Some(obj) = state.as_object_mut() {
106
- obj.insert(
107
- "team_dir".to_string(),
108
- Value::String(
109
- run_workspace
110
- .join(".team")
111
- .join(team_key)
112
- .to_string_lossy()
113
- .to_string(),
114
- ),
115
- );
116
- }
117
- }
118
-
119
- fn update_state_ok(path: std::path::PathBuf) -> ToolOk {
120
- let mut fields = serde_json::Map::new();
121
- fields.insert("ok".to_string(), Value::Bool(true));
122
- fields.insert(
123
- "state_file".to_string(),
124
- Value::String(path.to_string_lossy().to_string()),
125
- );
126
- ToolOk { fields }
127
- }
128
-
129
- fn is_missing_active_spec(err: &crate::state::StateError) -> bool {
130
- matches!(
131
- err,
132
- crate::state::StateError::TeamSelect(message)
133
- if message.starts_with("active team spec not found:")
134
- )
135
- }
136
-
137
11
  pub(crate) fn get_team_status(workspace: &Path, owner_team: Option<&TeamKey>) -> ToolResult {
138
12
  let selected = crate::state::selector::resolve_active_team(
139
13
  workspace,
@@ -4,8 +4,8 @@
4
4
  //! Card: `docs/phase0/subsystems/14-mcp_cli.md` (MCP half).
5
5
  //! Truth source (READ-ONLY) `team-agent-public` @ v0.2.11 / `439bef8`:
6
6
  //! - `mcp_server/server.py` — stdio loop + JSON-RPC route (`dispatch`/`handle_mcp`/`main`)
7
- //! - `mcp_server/tools.py` — `TeamOrchestratorTools`: the 12 typed tool handlers
8
- //! - `mcp_server/contracts.py` — `TOOLS`: name/description/inputSchema (wire single-truth)
7
+ //! - `mcp_server/tools.py` — `TeamOrchestratorTools`: the three retained handlers
8
+ //! - `mcp_server/contracts.py` — retained tool names/descriptions/inputSchema
9
9
  //! - `mcp_server/normalize.py` — result envelope / compact-result regularization
10
10
  //! - `mcp_server/__init__.py` — package re-export surface locked by boundary tests
11
11
  //!
@@ -17,13 +17,9 @@
17
17
  //! `unimplemented!("step14 port: …")`.
18
18
  //!
19
19
  //! REUSE (do NOT redefine):
20
- //! - [`MessageStore`] (step 7) — `request_human` creates the leader message row.
21
- //! - [`EventLog`] (step 4) — `mcp.scope_resolved` / `mcp.send_message_refused` /
22
- //! `mcp.identity_inference_failed` / `mcp.task_inference_failed` audit events.
23
- //! - [`load_runtime_state`] / [`save_runtime_state`] (step 5 persist) — `assign_task`
24
- //! / `update_state` read-modify-write; `get_visible_peers` reads team scope.
25
- //! - [`messaging`] (step 11) — `send_message` / auto-finalizing `report_result` /
26
- //! `stuck_list` / `stuck_cancel` delegated by the tool handlers.
20
+ //! - [`EventLog`] (step 4) — retained message/result audit events.
21
+ //! - [`load_runtime_state`] (step 5 persist) — message scope and peer checks.
22
+ //! - [`messaging`] (step 11) — `send_message` and durable `report_result` delegation.
27
23
  //! - [`crate::model::enums`] (step 2) — [`ResultStatus`] / [`ChangeKind`] /
28
24
  //! [`TestStatus`] / [`RiskSeverity`] are the normalized result-envelope value
29
25
  //! enums; this layer ONLY does string-alias regularization onto them.
@@ -43,9 +39,8 @@
43
39
  //! a frame here would corrupt the stdout JSON-RPC stream.
44
40
  //! - **stdout 是传输通道** (server.py:135): every error is surfaced ON stdout as a
45
41
  //! JSON-RPC frame; logs/warnings MUST go to stderr/file, never stdout.
46
- //! - **worker-recipient 异步 accepted** (tools.py:176-183): a worker recipient with
47
- //! a message_id → [`SendOutcome::WorkerAccepted`] carrying the byte-stable
48
- //! `poll_via = "team-agent inbox <id>"`; leader/`*` → [`SendOutcome::Direct`].
42
+ //! - **worker-recipient 异步 accepted**: a worker recipient with a message_id →
43
+ //! [`SendOutcome::WorkerAccepted`]; leader/`*` → [`SendOutcome::Direct`].
49
44
  //! - **兜底字符串字节级保留** (bug-085): `_infer_task_id` failure → `"manual"` (not
50
45
  //! None); `_infer_agent_id` failure → `None` → caller routes to `"unknown"`.
51
46
  //!
@@ -1,72 +1,4 @@
1
- /// ---
2
- /// purpose: scoped MCP no-compaction update_state filesystem contract
3
- /// contract:
4
- /// provides:
5
- /// - name: update_state_path_contracts
6
- /// what: proves the raw no-compaction response preserves its state-file path in an owned fixture
7
- /// depends:
8
- /// - name: mcp_state_fixture
9
- /// what: fixture-owned root and provenance from tests.rs
10
- /// boundary:
11
- /// - retain the raw {ok,state_file} result shape
12
- /// - the owned selector checks workspace, state-file, and runtime paths remain fixture-owned
13
- /// maturity: wired
14
- /// ---
15
-
16
- fn verify_cleanup_surviving_receipt(
17
- receipt_path: &std::path::Path,
18
- raw_workspace: &std::path::Path,
19
- canonical_workspace: &std::path::Path,
20
- state_file: &std::path::Path,
21
- runtime_state: &std::path::Path,
22
- ) {
23
- let bytes = std::fs::read(receipt_path)
24
- .expect("provenance receipt must survive fixture Drop");
25
- let receipt: Value = serde_json::from_slice(&bytes).expect("provenance receipt must be JSON");
26
- assert_eq!(receipt["schema"], json!("mcp-state-provenance-v1"));
27
- let payload = &receipt["payload"];
28
- let payload_bytes = serde_json::to_vec(payload).unwrap();
29
- assert_eq!(
30
- receipt["payload_sha256"],
31
- json!(format!("{:x}", sha2::Sha256::digest(&payload_bytes)))
32
- );
33
- assert_eq!(
34
- payload["raw_workspace"],
35
- json!(raw_workspace.display().to_string())
36
- );
37
- assert_eq!(
38
- payload["canonical_workspace"],
39
- json!(canonical_workspace.display().to_string())
40
- );
41
- assert_eq!(
42
- payload["resolved_state_file"],
43
- json!(state_file.display().to_string())
44
- );
45
- assert_eq!(
46
- payload["runtime_state_path"],
47
- json!(runtime_state.display().to_string())
48
- );
49
- let facts = payload["facts"].as_array().unwrap();
50
- for name in [
51
- "root",
52
- "raw_workspace",
53
- "canonical_workspace",
54
- "team_dir",
55
- "runtime_dir",
56
- "state_parent",
57
- "runtime_state",
58
- "state_file",
59
- ] {
60
- let fact = facts
61
- .iter()
62
- .find(|fact| fact["name"] == json!(name))
63
- .unwrap_or_else(|| panic!("provenance fact missing: {name}"));
64
- assert!(fact["path"].as_str().is_some());
65
- assert!(fact["uid"].as_u64().is_some());
66
- assert!(fact["mode"].as_u64().is_some());
67
- assert!(fact["device"].as_u64().is_some());
68
- }
69
- }
1
+ /// Scoped retained-tool contracts exercise owner binding and team isolation.
70
2
 
71
3
  #[test]
72
4
  fn dispatch_send_message_worker_accepted_returned_verbatim() {
@@ -100,69 +32,6 @@ fn verify_cleanup_surviving_receipt(
100
32
  assert_eq!(v.get("poll_via"), Some(&json!(format!("team-agent inbox {mid}"))));
101
33
  }
102
34
 
103
- // ── #32/#47 request_human key order ok,message_id,status (no compaction) ────
104
- // GOLDEN (probe_events_red.py REQUEST_HUMAN-KEYS): ['ok','message_id','status'].
105
- // Rust compact_tool_result reorders to ok,status,message_id.
106
- #[test]
107
- fn request_human_key_order_is_ok_message_id_status() {
108
- let tools = TeamOrchestratorTools::with_identity(
109
- &unique_ws("reqhuman-order"),
110
- Some(AgentId::new("worker-3")),
111
- None,
112
- );
113
- let ok = tools.request_human("need approval", Some("task-1"), None).expect("request_human ok");
114
- let v = serde_json::to_value(&ok).unwrap();
115
- assert_eq!(keys(&v), vec!["ok", "message_id", "status"]);
116
- assert_eq!(v.get("status"), Some(&json!("needs_human")));
117
- }
118
-
119
- // ── #32 update_state returns RAW {ok, state_file} (NO compaction) ───────────
120
- // GOLDEN (tools.py:316-325 + probe_passthrough): update_state is NOT compacted;
121
- // state_file survives. Rust runs compact_tool_result whose ok-whitelist DROPS
122
- // state_file (not a golden whitelist key), so the key vanishes.
123
- #[test]
124
- fn update_state_state_file_survives_no_compaction() {
125
- let fixture = McpStateFixture::new("writable");
126
- let expected = fixture.state_file("team_state.md");
127
- let tools = TeamOrchestratorTools::with_identity(
128
- &fixture.workspace,
129
- Some(AgentId::new("leader")),
130
- None,
131
- );
132
- let ok = tools.update_state("note").expect("update_state ok");
133
- let v = serde_json::to_value(&ok).unwrap();
134
- assert!(v.get("state_file").and_then(Value::as_str).is_some(),
135
- "state_file must survive (update_state is not _compact_tool_result'd)");
136
- assert_eq!(keys(&v), vec!["ok", "state_file"]);
137
- let returned_state_file = PathBuf::from(v["state_file"].as_str().unwrap());
138
- assert_eq!(returned_state_file, expected);
139
- let runtime_state = crate::state::persist::runtime_state_path(&fixture.workspace);
140
- assert!(fixture.under_root(&returned_state_file));
141
- assert!(fixture.under_root(&runtime_state));
142
- let canonical_workspace =
143
- crate::model::paths::canonical_run_workspace(&fixture.workspace).unwrap();
144
- let receipt = fixture.record_provenance(&fixture.workspace, &returned_state_file);
145
- let root = fixture.root.clone();
146
- drop(fixture);
147
- assert!(!root.exists(), "fixture root must be removed before receipt verification");
148
- verify_cleanup_surviving_receipt(
149
- &receipt,
150
- &root.join("workspace"),
151
- &canonical_workspace,
152
- &returned_state_file,
153
- &runtime_state,
154
- );
155
- let receipt_bytes = std::fs::read(&receipt).unwrap();
156
- println!(
157
- "F10_PROVENANCE_RECEIPT path={} sha256={:x} payload={}",
158
- receipt.display(),
159
- sha2::Sha256::digest(&receipt_bytes),
160
- String::from_utf8_lossy(&receipt_bytes)
161
- );
162
- std::fs::remove_file(&receipt).unwrap();
163
- assert!(!receipt.exists(), "cleanup must remove only the exact receipt path");
164
- }
165
-
166
35
  // ── #36 report_result identity: neither envelope nor args can override capture ─
167
36
  #[test]
168
37
  fn report_result_rejects_envelope_and_argument_identity_mismatch() {
@@ -43,31 +43,6 @@ fn is_worker_recipient_classification() {
43
43
  ])));
44
44
  }
45
45
 
46
- // ════════════════════════════════════════════════════════════════════════
47
- // merge_tasks_by_id — prefer wins, prefer-first insertion order (tools.py:30)
48
- // Golden: prefer t1(done),t2 + fallback t1(pending),t3,{no id},"notdict"
49
- // → [t1(done), t2, t3] (t1 from prefer wins; non-dict / no-id dropped)
50
- // ════════════════════════════════════════════════════════════════════════
51
- #[test]
52
- fn merge_tasks_by_id_prefer_wins_no_done_regression() {
53
- let prefer = vec![
54
- json!({"id": "t1", "status": "done"}),
55
- json!({"id": "t2", "status": "pending"}),
56
- ];
57
- let fallback = vec![
58
- json!({"id": "t1", "status": "pending"}), // must NOT regress t1
59
- json!({"id": "t3", "status": "ready"}),
60
- json!({"no": "id"}), // dropped (no id)
61
- json!("notdict"), // dropped (not object)
62
- ];
63
- let merged = merge_tasks_by_id(&prefer, &fallback);
64
- assert_eq!(merged.len(), 3);
65
- assert_eq!(merged[0]["id"], json!("t1"));
66
- assert_eq!(merged[0]["status"], json!("done")); // prefer wins → no regression
67
- assert_eq!(merged[1]["id"], json!("t2"));
68
- assert_eq!(merged[2]["id"], json!("t3"));
69
- }
70
-
71
46
  // ════════════════════════════════════════════════════════════════════════
72
47
  // SendOutcome::to_value — worker-accepted async envelope (tools.py:177-182)
73
48
  // byte-stable: {status:"accepted",delivery_pending:true,
@@ -171,7 +146,7 @@ fn send_message_worker_recipient_returns_accepted_with_poll_hint() {
171
146
  }
172
147
 
173
148
  #[test]
174
- fn ordinary_send_assign_shape_has_no_recovery_marker() {
149
+ fn ordinary_send_shape_has_no_recovery_marker() {
175
150
  let ws = seed_current_worker_state("ordinary-no-recovery");
176
151
  let tools = TeamOrchestratorTools::with_identity(
177
152
  &ws,
@@ -198,46 +173,6 @@ fn ordinary_send_assign_shape_has_no_recovery_marker() {
198
173
  "ordinary send must not carry acceptance marker: {sent}"
199
174
  );
200
175
 
201
- let assigned = tools
202
- .assign_task(
203
- &json!({"id": "ordinary-task", "assignee": "worker-1", "title": "ordinary"}),
204
- None,
205
- )
206
- .expect("ordinary assign ok");
207
- let assigned_value = serde_json::to_value(&assigned).expect("serialize ordinary assign");
208
- assert!(
209
- !assigned_value.as_object().unwrap().contains_key("recovery"),
210
- "ordinary assign must not carry recovery marker: {assigned_value}"
211
- );
212
- assert!(
213
- assigned_value.get("acceptance_marker").is_none(),
214
- "ordinary assign must not carry acceptance marker: {assigned_value}"
215
- );
216
-
217
- let recovery_false_assigned = tools
218
- .assign_task(
219
- &json!({
220
- "id": "ordinary-recovery-false-task",
221
- "assignee": "worker-1",
222
- "title": "ordinary recovery false",
223
- "recovery": false,
224
- }),
225
- None,
226
- )
227
- .expect("recovery=false assign ok");
228
- let recovery_false_value =
229
- serde_json::to_value(&recovery_false_assigned).expect("serialize recovery=false assign");
230
- assert!(
231
- !recovery_false_value
232
- .as_object()
233
- .unwrap()
234
- .contains_key("recovery"),
235
- "recovery=false assign must not carry recovery marker: {recovery_false_value}"
236
- );
237
- assert!(
238
- recovery_false_value.get("acceptance_marker").is_none(),
239
- "recovery=false assign must not carry acceptance marker: {recovery_false_value}"
240
- );
241
176
  }
242
177
 
243
178
  #[test]
@@ -256,33 +191,6 @@ fn send_message_without_framework_identity_fails_closed() {
256
191
  assert_eq!(error.reason, ToolErrorReason::McpScopeRefused);
257
192
  }
258
193
 
259
- #[test]
260
- fn recovery_assign_shape_has_structured_marker() {
261
- let ws = seed_current_worker_state("recovery-marker");
262
- let tools = TeamOrchestratorTools::with_identity(
263
- &ws,
264
- Some(AgentId::new("leader")),
265
- Some(TeamKey::new("current")),
266
- );
267
-
268
- let assigned = tools
269
- .assign_task(
270
- &json!({
271
- "id": "recovery-task",
272
- "assignee": "worker-1",
273
- "title": "recover",
274
- "recovery": true,
275
- }),
276
- None,
277
- )
278
- .expect("recovery assign ok");
279
- assert_eq!(assigned.fields.get("recovery"), Some(&json!(true)));
280
- assert_eq!(
281
- assigned.fields.get("acceptance_marker"),
282
- Some(&json!("recovery"))
283
- );
284
- }
285
-
286
194
  #[test]
287
195
  fn send_message_worker_recipient_surfaces_dead_coordinator_warning() {
288
196
  let ws = unique_ws("send-worker-dead-coord");