@team-agent/installer 0.5.112 → 0.5.114
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 +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +119 -50
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +173 -109
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/leaders.rs +32 -32
- package/crates/team-agent/src/cli/mod.rs +158 -14
- package/crates/team-agent/src/cli/models.rs +68 -18
- package/crates/team-agent/src/cli/named_address.rs +17 -16
- package/crates/team-agent/src/cli/route.rs +4 -4
- package/crates/team-agent/src/cli/send/presentation.rs +19 -18
- package/crates/team-agent/src/cli/send/resolve.rs +11 -11
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +35 -29
- package/crates/team-agent/src/cli/status.rs +7 -7
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +94 -6
- package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +9 -7
- package/crates/team-agent/src/cli/triage.rs +53 -2
- package/crates/team-agent/src/cli/types.rs +8 -8
- package/crates/team-agent/src/compiler/tests.rs +12 -0
- package/crates/team-agent/src/compiler.rs +65 -26
- package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
- package/crates/team-agent/src/contract_runtime/config.rs +309 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +265 -16
- package/crates/team-agent/src/leader/tests/identity.rs +21 -0
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +760 -365
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
- package/crates/team-agent/src/lifecycle/launch.rs +39 -28
- package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/provider/adapter.rs +8 -59
- package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
- package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
- package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
- package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
- package/crates/team-agent/src/provider/command_helpers.rs +45 -0
- package/crates/team-agent/src/provider/mod.rs +1 -0
- package/crates/team-agent/src/provider/model_catalog.rs +21 -28
- package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
- package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -100,7 +100,7 @@ pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline
|
|
|
100
100
|
// timer + worker timing writer so `lifecycle::launch::launch_with_transport_in_workspace`
|
|
101
101
|
// can emit the same instrumentation event family with `source="launch"`.
|
|
102
102
|
pub(crate) use rebuild::{
|
|
103
|
-
provider_wire_from_state, write_worker_spawn_timing_event, RestartPhaseTimer,
|
|
103
|
+
provider_wire_from_state, write_worker_spawn_timing_event, LifecyclePhase, RestartPhaseTimer,
|
|
104
104
|
};
|
|
105
105
|
pub use rebuild::{restart, restart_with_session_convergence_deadline};
|
|
106
106
|
pub(crate) use rebuild::{
|
|
@@ -165,14 +165,15 @@ fn purge_agent_help_and_dispatch_are_consistent() {
|
|
|
165
165
|
output_text(&case.run(["purge-agent", "ghost", "--workspace", case.ws(), "--json"]));
|
|
166
166
|
if help.contains("purge-agent") {
|
|
167
167
|
assert!(
|
|
168
|
-
!command.contains("invalid choice") && !command.contains("unknown subcommand")
|
|
168
|
+
!command.contains("invalid choice") && !command.contains("unknown subcommand")
|
|
169
|
+
&& !command.contains("Unknown command:") && !command.contains("Commands:"),
|
|
169
170
|
"RED6: help exposes purge-agent, so dispatch must be real or the command must be removed from help; help={help} command={command}"
|
|
170
171
|
);
|
|
171
172
|
} else {
|
|
172
173
|
assert!(
|
|
173
174
|
command.contains("unknown subcommand")
|
|
174
175
|
|| command.contains("invalid choice")
|
|
175
|
-
|| command.contains("
|
|
176
|
+
|| command.contains("Unknown command:"),
|
|
176
177
|
"RED6: if purge-agent is not implemented, help and dispatch must agree it is absent; help={help} command={command}"
|
|
177
178
|
);
|
|
178
179
|
}
|
|
@@ -433,8 +433,11 @@ pub enum LifecycleError {
|
|
|
433
433
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
434
434
|
pub struct LaunchReport {
|
|
435
435
|
pub session_name: SessionName,
|
|
436
|
-
///
|
|
436
|
+
/// Legacy transport starts; their targets belong to the team transport.
|
|
437
437
|
pub started: Vec<StartedAgent>,
|
|
438
|
+
/// Committed contract starts, without fabricating legacy pane/window targets.
|
|
439
|
+
/// Empty on dry-run; populated only after the contract startup returns success.
|
|
440
|
+
pub contract_started: Vec<AgentId>,
|
|
438
441
|
/// 是否 dry-run(只解析路由/权限,不起进程)。
|
|
439
442
|
pub dry_run: bool,
|
|
440
443
|
/// Selected tmux endpoint for the team transport when known.
|
|
@@ -63,16 +63,16 @@ pub(crate) struct WorkerCommandAgent {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
impl WorkerCommandAgent {
|
|
66
|
-
/// ---
|
|
67
|
-
/// purpose: 从 spec 的 YAML agent 节点读出命令上下文
|
|
68
|
-
/// params:
|
|
69
|
-
/// agent: 单个 agent 的 YAML 节点
|
|
70
|
-
/// fallback_id: agent 节点没写 id 时的兜底 id
|
|
71
|
-
/// provider: 已解析的 provider
|
|
72
|
-
/// returns: 填好的 WorkerCommandAgent
|
|
73
|
-
/// errors: communication_mode 取值非法时返回 LifecycleError
|
|
74
|
-
/// contract_id: lifecycle.worker_command_agent.from_source
|
|
75
|
-
/// ---
|
|
66
|
+
/// ---
|
|
67
|
+
/// purpose: 从 spec 的 YAML agent 节点读出命令上下文
|
|
68
|
+
/// params:
|
|
69
|
+
/// agent: 单个 agent 的 YAML 节点
|
|
70
|
+
/// fallback_id: agent 节点没写 id 时的兜底 id
|
|
71
|
+
/// provider: 已解析的 provider
|
|
72
|
+
/// returns: 填好的 WorkerCommandAgent
|
|
73
|
+
/// errors: communication_mode 取值非法时返回 LifecycleError
|
|
74
|
+
/// contract_id: lifecycle.worker_command_agent.from_source
|
|
75
|
+
/// ---
|
|
76
76
|
pub(crate) fn from_yaml(
|
|
77
77
|
agent: &crate::model::yaml::Value,
|
|
78
78
|
fallback_id: Option<&str>,
|
|
@@ -117,16 +117,16 @@ impl WorkerCommandAgent {
|
|
|
117
117
|
})
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
/// ---
|
|
121
|
-
/// purpose: 从 runtime state 的 JSON agent 节点读出命令上下文
|
|
122
|
-
/// params:
|
|
123
|
-
/// agent: 单个 agent 的 JSON 节点
|
|
124
|
-
/// fallback_id: agent 节点没写 id 时的兜底 id
|
|
125
|
-
/// provider: 已解析的 provider
|
|
126
|
-
/// returns: 填好的 WorkerCommandAgent
|
|
127
|
-
/// errors: communication_mode 取值非法时返回 LifecycleError
|
|
128
|
-
/// contract_id: lifecycle.worker_command_agent.from_source
|
|
129
|
-
/// ---
|
|
120
|
+
/// ---
|
|
121
|
+
/// purpose: 从 runtime state 的 JSON agent 节点读出命令上下文
|
|
122
|
+
/// params:
|
|
123
|
+
/// agent: 单个 agent 的 JSON 节点
|
|
124
|
+
/// fallback_id: agent 节点没写 id 时的兜底 id
|
|
125
|
+
/// provider: 已解析的 provider
|
|
126
|
+
/// returns: 填好的 WorkerCommandAgent
|
|
127
|
+
/// errors: communication_mode 取值非法时返回 LifecycleError
|
|
128
|
+
/// contract_id: lifecycle.worker_command_agent.from_source
|
|
129
|
+
/// ---
|
|
130
130
|
pub(crate) fn from_json(
|
|
131
131
|
agent: &serde_json::Value,
|
|
132
132
|
fallback_id: Option<&str>,
|
|
@@ -190,7 +190,8 @@ pub(crate) fn compile_worker_system_prompt(
|
|
|
190
190
|
mcp_tool_name(agent.provider, "team_orchestrator", "get_team_status")
|
|
191
191
|
);
|
|
192
192
|
let report_result = mcp_tool_name(agent.provider, "team_orchestrator", "report_result");
|
|
193
|
-
let runtime_contract =
|
|
193
|
+
let runtime_contract =
|
|
194
|
+
runtime_contract_section(&send_message, &report_result, &get_team_status);
|
|
194
195
|
let communication_contract = agent.communication_mode.runtime_contract(&send_message);
|
|
195
196
|
let mut chunks = vec![
|
|
196
197
|
identity_section(agent),
|
|
@@ -235,7 +236,7 @@ fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
|
|
|
235
236
|
}
|
|
236
237
|
}
|
|
237
238
|
|
|
238
|
-
fn runtime_contract_section(
|
|
239
|
+
pub(crate) fn runtime_contract_section(
|
|
239
240
|
send_message: &str,
|
|
240
241
|
report_result: &str,
|
|
241
242
|
get_team_status: &str,
|
|
@@ -255,10 +256,15 @@ fn communication_mode(value: Option<&str>) -> Result<CommunicationMode, Lifecycl
|
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
fn identity_section(agent: &WorkerCommandAgent) -> String {
|
|
258
|
-
|
|
259
|
-
"You are Team Agent worker `{}` with role `{}`. When asked about your role or identity, answer with this Team Agent worker identity first, not only the generic provider product identity.",
|
|
259
|
+
worker_identity_section(
|
|
260
260
|
agent.id.as_deref().unwrap_or("unknown"),
|
|
261
|
-
agent.role.as_deref().unwrap_or("developer")
|
|
261
|
+
agent.role.as_deref().unwrap_or("developer"),
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
pub(crate) fn worker_identity_section(id: &str, role: &str) -> String {
|
|
266
|
+
format!(
|
|
267
|
+
"You are Team Agent worker `{id}` with role `{role}`. When asked about your role or identity, answer with this Team Agent worker identity first, not only the generic provider product identity."
|
|
262
268
|
)
|
|
263
269
|
}
|
|
264
270
|
|
|
@@ -344,7 +350,9 @@ mod tests {
|
|
|
344
350
|
"Do not reply to pure ACKs, greetings, or unchanged status notices (such as \"paused\" or \"waiting\"); after reporting a blocker once or completing a task, remain silent until a new actionable instruction arrives."
|
|
345
351
|
));
|
|
346
352
|
assert!(!prompt.contains("Silence and resumption (mandatory)"));
|
|
347
|
-
assert!(!prompt.contains(
|
|
353
|
+
assert!(!prompt.contains(
|
|
354
|
+
"When you receive a message from the leader or a teammate, you MUST respond"
|
|
355
|
+
));
|
|
348
356
|
match mode {
|
|
349
357
|
CommunicationMode::LeaderCentric => {
|
|
350
358
|
assert!(prompt.contains(
|
|
@@ -352,7 +360,9 @@ mod tests {
|
|
|
352
360
|
));
|
|
353
361
|
}
|
|
354
362
|
CommunicationMode::Orchestrated => {
|
|
355
|
-
assert!(prompt.contains(
|
|
363
|
+
assert!(prompt.contains(
|
|
364
|
+
"Respond to task-related messages through Team Agent MCP tools."
|
|
365
|
+
));
|
|
356
366
|
assert!(prompt.contains("A pure ACK, unrelated status, or non-task message does not require a response."));
|
|
357
367
|
}
|
|
358
368
|
}
|
|
@@ -5,6 +5,12 @@ fn main() -> anyhow::Result<()> {
|
|
|
5
5
|
.skip(1)
|
|
6
6
|
.map(|arg| arg.to_string_lossy().into_owned());
|
|
7
7
|
let command = args.next();
|
|
8
|
+
#[cfg(unix)]
|
|
9
|
+
if matches!(command.as_deref(), Some("contract-mcp")) {
|
|
10
|
+
team_agent::contract_runtime::mcp::run(&args.collect::<Vec<_>>())
|
|
11
|
+
.map_err(|error| anyhow::anyhow!(error.to_string()))?;
|
|
12
|
+
return Ok(());
|
|
13
|
+
}
|
|
8
14
|
if matches!(command.as_deref(), Some("fake-worker")) {
|
|
9
15
|
let mut workspace = None;
|
|
10
16
|
let mut agent_id = None;
|
|
@@ -1926,7 +1926,11 @@ fn delivery_transport_for_recipient<'a>(
|
|
|
1926
1926
|
}
|
|
1927
1927
|
return DeliveryTransport::Borrowed(product_transport);
|
|
1928
1928
|
};
|
|
1929
|
-
|
|
1929
|
+
// An exact endpoint match can reuse the supplied channel; do not select
|
|
1930
|
+
// another server merely because it has a pane with the same numeric id.
|
|
1931
|
+
if product_transport.tmux_endpoint().as_deref() == Some(socket)
|
|
1932
|
+
|| socket == crate::tmux_backend::socket_name_for_workspace(workspace)
|
|
1933
|
+
{
|
|
1930
1934
|
DeliveryTransport::Borrowed(product_transport)
|
|
1931
1935
|
} else {
|
|
1932
1936
|
// 0.5.4 gate054 cross-team leak fix: when `leader_receiver.tmux_socket`
|
|
@@ -2523,6 +2527,51 @@ pub(crate) fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> b
|
|
|
2523
2527
|
}
|
|
2524
2528
|
|
|
2525
2529
|
fn leader_rollout_path(state: &serde_json::Value) -> Option<std::path::PathBuf> {
|
|
2530
|
+
if let Some(receiver) = leader_receiver_value(state).filter(|receiver| {
|
|
2531
|
+
receiver.get("provider").and_then(serde_json::Value::as_str) == Some("pi")
|
|
2532
|
+
&& receiver.get("native_session").is_some()
|
|
2533
|
+
}) {
|
|
2534
|
+
let native = receiver.get("native_session")?;
|
|
2535
|
+
for key in ["pane_id", "tmux_socket", "leader_session_uuid", "owner_epoch"] {
|
|
2536
|
+
let bound = native.get(key)?;
|
|
2537
|
+
if bound.is_null() || receiver.get(key) != Some(bound) {
|
|
2538
|
+
return None;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
// Managed Pi's native ID is distinct from leader_session_uuid.
|
|
2542
|
+
// The JSONL appears asynchronously. Resolve only inside the recorded
|
|
2543
|
+
// root, by exact id/cwd/spawn time; ambiguity or an unknown
|
|
2544
|
+
// NativeDefault root stays unavailable (no HOME scan).
|
|
2545
|
+
let context = crate::provider::CaptureSessionContext {
|
|
2546
|
+
agent_id: "leader".into(),
|
|
2547
|
+
spawn_cwd: native.get("spawn_cwd")?.as_str()?.into(),
|
|
2548
|
+
pane_id: None,
|
|
2549
|
+
pane_pid: None,
|
|
2550
|
+
spawned_at: Some(native.get("spawned_at")?.as_str()?.into()),
|
|
2551
|
+
expected_session_id: Some(crate::provider::SessionId::new(
|
|
2552
|
+
native.get("expected_session_id")?.as_str()?,
|
|
2553
|
+
)),
|
|
2554
|
+
provider_projects_root: Some(native.get("provider_projects_root")?.as_str()?.into()),
|
|
2555
|
+
};
|
|
2556
|
+
if !context.spawn_cwd.is_absolute()
|
|
2557
|
+
|| !context.provider_projects_root.as_ref()?.is_absolute()
|
|
2558
|
+
{
|
|
2559
|
+
return None;
|
|
2560
|
+
}
|
|
2561
|
+
let candidates = crate::provider::session_scan::scan_session_candidates_once(
|
|
2562
|
+
Provider::Pi,
|
|
2563
|
+
&context,
|
|
2564
|
+
)
|
|
2565
|
+
.ok()?;
|
|
2566
|
+
return match candidates.as_slice() {
|
|
2567
|
+
[candidate] => candidate
|
|
2568
|
+
.captured
|
|
2569
|
+
.rollout_path
|
|
2570
|
+
.as_ref()
|
|
2571
|
+
.map(|path| path.as_path().to_path_buf()),
|
|
2572
|
+
_ => None,
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2526
2575
|
state
|
|
2527
2576
|
.get("leader")
|
|
2528
2577
|
.and_then(|leader| leader.get("rollout_path"))
|
|
@@ -265,6 +265,16 @@ pub fn send_message(
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
|
+
#[cfg(unix)]
|
|
269
|
+
if state
|
|
270
|
+
.get("agents")
|
|
271
|
+
.and_then(|agents| agents.get(recipient))
|
|
272
|
+
.is_some_and(crate::contract_runtime::framework::is_contract)
|
|
273
|
+
{
|
|
274
|
+
return crate::contract_runtime::framework::enqueue(
|
|
275
|
+
workspace, &state, recipient, content, opts,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
268
278
|
let stale_worker_target_missing =
|
|
269
279
|
stale_worker_target_missing_preflight(workspace, &state, recipient)?;
|
|
270
280
|
let coordinator_unavailable = if stale_worker_target_missing {
|
|
@@ -77,6 +77,131 @@ fn tmux_leader_submit_without_provider_receipt_stays_pending_then_accepts_same_r
|
|
|
77
77
|
assert!(case.delivery_consumed_at(&message_id).is_some());
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
fn pi_receipt_case(tag: &str) -> (Case, serde_json::Value) {
|
|
81
|
+
let mut case = Case::new_without_rollout(tag);
|
|
82
|
+
let root = case.workspace.join("pi-sessions");
|
|
83
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
84
|
+
case.rollout = root.join("not-derived-from-the-session-id.jsonl");
|
|
85
|
+
let receiver = &mut case.state["leader_receiver"];
|
|
86
|
+
receiver["provider"] = serde_json::json!("pi");
|
|
87
|
+
receiver["tmux_socket"] = serde_json::json!(case.workspace.join("fixture-tmux.sock"));
|
|
88
|
+
receiver["leader_session_uuid"] = serde_json::json!("team-owner-not-native-id");
|
|
89
|
+
receiver["owner_epoch"] = serde_json::json!(1);
|
|
90
|
+
receiver["native_session"] = serde_json::json!({
|
|
91
|
+
"expected_session_id": "native-pi-session",
|
|
92
|
+
"provider_projects_root": root,
|
|
93
|
+
"spawn_cwd": case.workspace,
|
|
94
|
+
"spawned_at": "2026-10-10T03:59:07Z",
|
|
95
|
+
"pane_id": "%leader",
|
|
96
|
+
"tmux_socket": receiver["tmux_socket"],
|
|
97
|
+
"leader_session_uuid": "team-owner-not-native-id",
|
|
98
|
+
"owner_epoch": 1,
|
|
99
|
+
});
|
|
100
|
+
crate::state::persist::save_runtime_state(&case.workspace, &case.state).unwrap();
|
|
101
|
+
let header = serde_json::json!({
|
|
102
|
+
"type": "session", "version": 3, "id": "native-pi-session",
|
|
103
|
+
"cwd": case.workspace, "timestamp": "2026-10-10T03:59:08Z",
|
|
104
|
+
});
|
|
105
|
+
(case, header)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn pi_leader_receipt_binds_exact_header_and_consumes_without_reinjection() {
|
|
110
|
+
let (case, header) = pi_receipt_case("pi-native-receipt");
|
|
111
|
+
let endpoint = case.state["leader_receiver"]["tmux_socket"].as_str().unwrap();
|
|
112
|
+
assert!(Path::new(endpoint).is_absolute());
|
|
113
|
+
let unbound = crate::messaging::resolve_live_leader_channel(
|
|
114
|
+
&case.workspace,
|
|
115
|
+
&case.state["leader_receiver"],
|
|
116
|
+
&RecordingTransport::new(""),
|
|
117
|
+
);
|
|
118
|
+
assert!(
|
|
119
|
+
matches!(
|
|
120
|
+
unbound,
|
|
121
|
+
crate::messaging::LeaderChannelResolution::Unbound(
|
|
122
|
+
crate::messaging::LeaderChannelUnbound::EndpointMismatch
|
|
123
|
+
)
|
|
124
|
+
),
|
|
125
|
+
"{unbound:?}"
|
|
126
|
+
);
|
|
127
|
+
let transport = RecordingTransport {
|
|
128
|
+
endpoint: Some(endpoint.to_string()),
|
|
129
|
+
..RecordingTransport::new("")
|
|
130
|
+
};
|
|
131
|
+
let message_id = case.seed_message("Pi receipt canary");
|
|
132
|
+
std::fs::write(&case.rollout, format!("{header}\n")).unwrap();
|
|
133
|
+
let first = deliver_pending_message(
|
|
134
|
+
&case.workspace,
|
|
135
|
+
&case.store,
|
|
136
|
+
&transport,
|
|
137
|
+
&message_id,
|
|
138
|
+
&case.event_log,
|
|
139
|
+
&case.state,
|
|
140
|
+
)
|
|
141
|
+
.unwrap();
|
|
142
|
+
assert!(first.ok, "{first:?}");
|
|
143
|
+
assert_eq!(
|
|
144
|
+
case.message_status(&message_id),
|
|
145
|
+
"submitted_pending_acceptance"
|
|
146
|
+
);
|
|
147
|
+
assert_eq!(transport.inject_count(), 1);
|
|
148
|
+
|
|
149
|
+
let received = serde_json::json!({
|
|
150
|
+
"type": "message", "message": {"role": "user", "content": [
|
|
151
|
+
{"type": "text", "text": format!("5535 [team-agent-token:{message_id}]")}
|
|
152
|
+
]}
|
|
153
|
+
});
|
|
154
|
+
std::fs::write(&case.rollout, format!("{header}\n{received}\n")).unwrap();
|
|
155
|
+
let second = deliver_pending_message(
|
|
156
|
+
&case.workspace,
|
|
157
|
+
&case.store,
|
|
158
|
+
&transport,
|
|
159
|
+
&message_id,
|
|
160
|
+
&case.event_log,
|
|
161
|
+
&case.state,
|
|
162
|
+
)
|
|
163
|
+
.unwrap();
|
|
164
|
+
assert!(second.ok, "{second:?}");
|
|
165
|
+
assert_eq!(case.message_status(&message_id), "delivered");
|
|
166
|
+
assert!(case.delivery_consumed_at(&message_id).is_some());
|
|
167
|
+
assert_eq!(transport.inject_count(), 1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[test]
|
|
171
|
+
fn pi_leader_receipt_refuses_missing_ambiguous_stale_and_other_owner_backing() {
|
|
172
|
+
use crate::messaging::delivery::{observe_leader_receipt, LeaderReceiptObservation::*};
|
|
173
|
+
let (mut case, header) = pi_receipt_case("pi-native-receipt-fences");
|
|
174
|
+
let old = case.workspace.join("old-leader.jsonl");
|
|
175
|
+
std::fs::write(&old, "[team-agent-token:canary]").unwrap();
|
|
176
|
+
case.state["leader"] = serde_json::json!({"rollout_path": old});
|
|
177
|
+
// An old root leader receipt cannot substitute for this native session.
|
|
178
|
+
assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
|
|
179
|
+
for (field, value) in [
|
|
180
|
+
("id", serde_json::json!("another-session")),
|
|
181
|
+
("cwd", serde_json::json!(case.workspace.join("other"))),
|
|
182
|
+
("timestamp", serde_json::json!("2026-10-10T03:59:06Z")),
|
|
183
|
+
] {
|
|
184
|
+
let mut wrong = header.clone();
|
|
185
|
+
wrong[field] = value;
|
|
186
|
+
std::fs::write(&case.rollout, format!("{wrong}\n[team-agent-token:canary]\n")).unwrap();
|
|
187
|
+
assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
|
|
188
|
+
}
|
|
189
|
+
std::fs::write(&case.rollout, format!("{header}\n")).unwrap();
|
|
190
|
+
assert_eq!(observe_leader_receipt(&case.state, "canary"), TokenAbsent);
|
|
191
|
+
let duplicate = case.rollout.with_file_name("duplicate.jsonl");
|
|
192
|
+
std::fs::write(&duplicate, format!("{header}\n[team-agent-token:canary]\n")).unwrap();
|
|
193
|
+
assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
|
|
194
|
+
std::fs::remove_file(duplicate).unwrap();
|
|
195
|
+
for key in ["pane_id", "tmux_socket", "leader_session_uuid", "owner_epoch"] {
|
|
196
|
+
let mut moved = case.state.clone();
|
|
197
|
+
moved["leader_receiver"][key] = serde_json::json!("changed");
|
|
198
|
+
assert_eq!(observe_leader_receipt(&moved, "canary"), SourceUnavailable);
|
|
199
|
+
}
|
|
200
|
+
case.state["leader_receiver"]["native_session"]["provider_projects_root"] =
|
|
201
|
+
serde_json::Value::Null;
|
|
202
|
+
assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
|
|
203
|
+
}
|
|
204
|
+
|
|
80
205
|
#[test]
|
|
81
206
|
fn attached_leader_mailbox_is_rechecked_by_the_normal_delivery_tick() {
|
|
82
207
|
let case = Case::new("mailbox");
|
|
@@ -428,6 +553,7 @@ impl Case {
|
|
|
428
553
|
struct RecordingTransport {
|
|
429
554
|
injects: Arc<AtomicUsize>,
|
|
430
555
|
capture: Arc<Mutex<String>>,
|
|
556
|
+
endpoint: Option<String>,
|
|
431
557
|
}
|
|
432
558
|
|
|
433
559
|
impl RecordingTransport {
|
|
@@ -435,6 +561,7 @@ impl RecordingTransport {
|
|
|
435
561
|
Self {
|
|
436
562
|
injects: Arc::new(AtomicUsize::new(0)),
|
|
437
563
|
capture: Arc::new(Mutex::new(capture.to_string())),
|
|
564
|
+
endpoint: None,
|
|
438
565
|
}
|
|
439
566
|
}
|
|
440
567
|
|
|
@@ -444,6 +571,10 @@ impl RecordingTransport {
|
|
|
444
571
|
}
|
|
445
572
|
|
|
446
573
|
impl Transport for RecordingTransport {
|
|
574
|
+
fn tmux_endpoint(&self) -> Option<String> {
|
|
575
|
+
self.endpoint.clone()
|
|
576
|
+
}
|
|
577
|
+
|
|
447
578
|
fn kind(&self) -> BackendKind {
|
|
448
579
|
BackendKind::Tmux
|
|
449
580
|
}
|
|
@@ -16,8 +16,8 @@ use crate::model::enums::TaskStatus;
|
|
|
16
16
|
use crate::model::errors::ModelError;
|
|
17
17
|
use crate::model::ids::TaskId;
|
|
18
18
|
use crate::model::task_graph::{find_dependency_cycle, TaskNode};
|
|
19
|
-
use crate::model::yaml::Value as Yaml;
|
|
20
19
|
use crate::model::yaml;
|
|
20
|
+
use crate::model::yaml::Value as Yaml;
|
|
21
21
|
use crate::provider::wire::parse_canonical_provider;
|
|
22
22
|
|
|
23
23
|
const LEGACY_DISPLAY_KEY: &str = concat!("display", "_backend");
|
|
@@ -733,10 +733,13 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
|
|
|
733
733
|
|
|
734
734
|
for (idx, agent) in agents.iter().enumerate() {
|
|
735
735
|
let provider = agent.get("provider");
|
|
736
|
-
if !provider
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
736
|
+
if !provider.and_then(Yaml::as_str).is_some_and(|p| {
|
|
737
|
+
#[cfg(unix)]
|
|
738
|
+
if crate::contract_runtime::registry::descriptor(p).is_some() {
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
parse_canonical_provider(p).is_some()
|
|
742
|
+
}) {
|
|
740
743
|
e.push(format!(
|
|
741
744
|
"/agents/{idx}/provider: unknown provider {}",
|
|
742
745
|
py_repr(provider)
|
|
@@ -1066,7 +1069,11 @@ mod tests {
|
|
|
1066
1069
|
.iter()
|
|
1067
1070
|
.filter(|m| m.starts_with("/agents/dangerously_skip_permissions: missing"))
|
|
1068
1071
|
.collect();
|
|
1069
|
-
assert_eq!(
|
|
1072
|
+
assert_eq!(
|
|
1073
|
+
missing.len(),
|
|
1074
|
+
1,
|
|
1075
|
+
"exactly one missing-field error, got: {errors:#?}"
|
|
1076
|
+
);
|
|
1070
1077
|
assert!(
|
|
1071
1078
|
missing[0].contains("`codex_implementer`"),
|
|
1072
1079
|
"must name the role id, got: {}",
|
|
@@ -1083,7 +1090,11 @@ mod tests {
|
|
|
1083
1090
|
#[test]
|
|
1084
1091
|
fn test_invalid_dangerously_type_rejected() {
|
|
1085
1092
|
let text = include_str!("testdata/team.spec.yaml");
|
|
1086
|
-
let bad = text.replacen(
|
|
1093
|
+
let bad = text.replacen(
|
|
1094
|
+
" dangerously_skip_permissions: false\n",
|
|
1095
|
+
" dangerously_skip_permissions: \"yes\"\n",
|
|
1096
|
+
1,
|
|
1097
|
+
);
|
|
1087
1098
|
assert_ne!(text, bad, "fixture must contain the field");
|
|
1088
1099
|
let spec = yaml::loads(&bad).unwrap();
|
|
1089
1100
|
let errors = all_spec_errors(&spec);
|
|
@@ -1091,7 +1102,11 @@ mod tests {
|
|
|
1091
1102
|
.iter()
|
|
1092
1103
|
.filter(|m| m.starts_with("/agents/dangerously_skip_permissions: must be a boolean"))
|
|
1093
1104
|
.collect();
|
|
1094
|
-
assert_eq!(
|
|
1105
|
+
assert_eq!(
|
|
1106
|
+
type_errs.len(),
|
|
1107
|
+
1,
|
|
1108
|
+
"exactly one type error, got: {errors:#?}"
|
|
1109
|
+
);
|
|
1095
1110
|
assert!(
|
|
1096
1111
|
type_errs[0].contains("`codex_implementer`"),
|
|
1097
1112
|
"must name the role id, got: {}",
|
|
@@ -1139,10 +1154,7 @@ mod tests {
|
|
|
1139
1154
|
// 缺失 role frontmatter 值时编译为安全默认 false;此处锁定公共编译入口的结果。
|
|
1140
1155
|
#[test]
|
|
1141
1156
|
fn test_all_7_triggers_use_compiled_default_bypass() {
|
|
1142
|
-
let team = std::env::temp_dir().join(format!(
|
|
1143
|
-
"ta-spec-7trigger-{}",
|
|
1144
|
-
std::process::id()
|
|
1145
|
-
));
|
|
1157
|
+
let team = std::env::temp_dir().join(format!("ta-spec-7trigger-{}", std::process::id()));
|
|
1146
1158
|
let _ = std::fs::remove_dir_all(&team);
|
|
1147
1159
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
1148
1160
|
std::fs::write(
|
|
@@ -300,6 +300,10 @@ pub fn get_adapter(p: Provider) -> Box<dyn ProviderAdapter> {
|
|
|
300
300
|
Box::new(BasicProviderAdapter { provider: p })
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
#[cfg(test)]
|
|
304
|
+
#[path = "tests/command_helpers.rs"]
|
|
305
|
+
mod command_helpers_tests;
|
|
306
|
+
|
|
303
307
|
#[derive(Debug, Clone, Copy)]
|
|
304
308
|
pub(crate) struct BasicProviderAdapter {
|
|
305
309
|
provider: Provider,
|
|
@@ -480,22 +484,13 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
480
484
|
Ok(argv)
|
|
481
485
|
}
|
|
482
486
|
Provider::Grok => Ok(grok_launch_command(
|
|
483
|
-
self,
|
|
484
|
-
auth_mode,
|
|
485
|
-
mcp_config,
|
|
486
487
|
system_prompt,
|
|
487
488
|
model,
|
|
488
489
|
dangerously_skip_permissions,
|
|
489
490
|
)?),
|
|
490
491
|
Provider::CursorAgent => Ok(cursor_agent_base_command(
|
|
491
|
-
self,
|
|
492
|
-
auth_mode,
|
|
493
|
-
mcp_config,
|
|
494
|
-
system_prompt,
|
|
495
492
|
model,
|
|
496
493
|
dangerously_skip_permissions,
|
|
497
|
-
false,
|
|
498
|
-
None,
|
|
499
494
|
)?),
|
|
500
495
|
Provider::Pi => Err(ProviderError::Command(
|
|
501
496
|
"Pi commands require the shared lifecycle materializer".to_string(),
|
|
@@ -596,14 +591,9 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
596
591
|
// never written and restart could not resume.
|
|
597
592
|
let expected = next_session_token();
|
|
598
593
|
let mut argv = grok_base_command(
|
|
599
|
-
self,
|
|
600
|
-
ctx.auth_mode,
|
|
601
|
-
ctx.mcp_config,
|
|
602
594
|
ctx.system_prompt,
|
|
603
595
|
ctx.model,
|
|
604
596
|
ctx.dangerously_skip_permissions,
|
|
605
|
-
ctx.profile_launch
|
|
606
|
-
.is_some_and(|profile| profile.managed_mcp_config),
|
|
607
597
|
ctx.effort,
|
|
608
598
|
)?;
|
|
609
599
|
argv.push("--session-id".to_string());
|
|
@@ -637,15 +627,8 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
637
627
|
// U-01 create-chat 无登录超时;禁止发明 --session-id。
|
|
638
628
|
// pending 只能是 CLI chatId,本拍不造假 uuid。
|
|
639
629
|
let argv = cursor_agent_base_command(
|
|
640
|
-
self,
|
|
641
|
-
ctx.auth_mode,
|
|
642
|
-
ctx.mcp_config,
|
|
643
|
-
ctx.system_prompt,
|
|
644
630
|
ctx.model,
|
|
645
631
|
ctx.dangerously_skip_permissions,
|
|
646
|
-
ctx.profile_launch
|
|
647
|
-
.is_some_and(|profile| profile.managed_mcp_config),
|
|
648
|
-
ctx.effort,
|
|
649
632
|
)?;
|
|
650
633
|
Ok(CommandPlan {
|
|
651
634
|
argv,
|
|
@@ -816,7 +799,7 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
816
799
|
// copilot --resume 接受 id|name)。
|
|
817
800
|
Provider::Copilot => {
|
|
818
801
|
let mut argv =
|
|
819
|
-
|
|
802
|
+
copilot_base_command(auth_mode, mcp_config, system_prompt, model, dangerously_skip_permissions);
|
|
820
803
|
argv.push("--resume".to_string());
|
|
821
804
|
argv.push(session_id.as_str().to_string());
|
|
822
805
|
Ok(argv)
|
|
@@ -824,13 +807,9 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
824
807
|
// Grok resume: base + `--resume <id>` (grok -r/--resume 接受 id|title)。
|
|
825
808
|
Provider::Grok => {
|
|
826
809
|
let mut argv = grok_base_command(
|
|
827
|
-
self,
|
|
828
|
-
auth_mode,
|
|
829
|
-
mcp_config,
|
|
830
810
|
system_prompt,
|
|
831
811
|
model,
|
|
832
812
|
dangerously_skip_permissions,
|
|
833
|
-
false,
|
|
834
813
|
None,
|
|
835
814
|
)?;
|
|
836
815
|
argv.push("--resume".to_string());
|
|
@@ -839,16 +818,7 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
839
818
|
}
|
|
840
819
|
// Cursor resume: base + `--resume <chatId>`。
|
|
841
820
|
Provider::CursorAgent => {
|
|
842
|
-
let mut argv = cursor_agent_base_command(
|
|
843
|
-
self,
|
|
844
|
-
auth_mode,
|
|
845
|
-
mcp_config,
|
|
846
|
-
system_prompt,
|
|
847
|
-
model,
|
|
848
|
-
dangerously_skip_permissions,
|
|
849
|
-
false,
|
|
850
|
-
None,
|
|
851
|
-
)?;
|
|
821
|
+
let mut argv = cursor_agent_base_command(model, dangerously_skip_permissions)?;
|
|
852
822
|
// Put --resume immediately after `agent` so the CLI treats it as
|
|
853
823
|
// the session mode rather than a trailing flag after --workspace.
|
|
854
824
|
argv.insert(1, "--resume".to_string());
|
|
@@ -941,14 +911,9 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
941
911
|
));
|
|
942
912
|
};
|
|
943
913
|
let mut argv = grok_base_command(
|
|
944
|
-
self,
|
|
945
|
-
ctx.auth_mode,
|
|
946
|
-
ctx.mcp_config,
|
|
947
914
|
ctx.system_prompt,
|
|
948
915
|
ctx.model,
|
|
949
916
|
ctx.dangerously_skip_permissions,
|
|
950
|
-
ctx.profile_launch
|
|
951
|
-
.is_some_and(|profile| profile.managed_mcp_config),
|
|
952
917
|
ctx.effort,
|
|
953
918
|
)?;
|
|
954
919
|
argv.push("--resume".to_string());
|
|
@@ -968,15 +933,8 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
968
933
|
));
|
|
969
934
|
}
|
|
970
935
|
let mut argv = cursor_agent_base_command(
|
|
971
|
-
self,
|
|
972
|
-
ctx.auth_mode,
|
|
973
|
-
ctx.mcp_config,
|
|
974
|
-
ctx.system_prompt,
|
|
975
936
|
ctx.model,
|
|
976
937
|
ctx.dangerously_skip_permissions,
|
|
977
|
-
ctx.profile_launch
|
|
978
|
-
.is_some_and(|profile| profile.managed_mcp_config),
|
|
979
|
-
ctx.effort,
|
|
980
938
|
)?;
|
|
981
939
|
argv.push("--resume".to_string());
|
|
982
940
|
argv.push(raw.to_string());
|
|
@@ -1067,13 +1025,9 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
1067
1025
|
// (grok help: --fork-session 需配合 --resume/--continue)。
|
|
1068
1026
|
Provider::Grok => {
|
|
1069
1027
|
let mut argv = grok_base_command(
|
|
1070
|
-
self,
|
|
1071
|
-
auth_mode,
|
|
1072
|
-
mcp_config,
|
|
1073
1028
|
system_prompt,
|
|
1074
1029
|
model,
|
|
1075
1030
|
dangerously_skip_permissions,
|
|
1076
|
-
false,
|
|
1077
1031
|
None,
|
|
1078
1032
|
)?;
|
|
1079
1033
|
argv.push("--session-id".to_string());
|
|
@@ -1191,7 +1145,7 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
1191
1145
|
.ok_or_else(|| {
|
|
1192
1146
|
ProviderError::Command("copilot fork root was not materialized".to_string())
|
|
1193
1147
|
})?;
|
|
1194
|
-
let mut argv =
|
|
1148
|
+
let mut argv = copilot_base_command(
|
|
1195
1149
|
ctx.auth_mode,
|
|
1196
1150
|
ctx.mcp_config,
|
|
1197
1151
|
ctx.system_prompt,
|
|
@@ -1214,14 +1168,9 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
1214
1168
|
));
|
|
1215
1169
|
};
|
|
1216
1170
|
let mut argv = grok_base_command(
|
|
1217
|
-
self,
|
|
1218
|
-
ctx.auth_mode,
|
|
1219
|
-
ctx.mcp_config,
|
|
1220
1171
|
ctx.system_prompt,
|
|
1221
1172
|
ctx.model,
|
|
1222
1173
|
ctx.dangerously_skip_permissions,
|
|
1223
|
-
ctx.profile_launch
|
|
1224
|
-
.is_some_and(|profile| profile.managed_mcp_config),
|
|
1225
1174
|
ctx.effort,
|
|
1226
1175
|
)?;
|
|
1227
1176
|
argv.push("--session-id".to_string());
|
|
@@ -1405,7 +1354,7 @@ pub(crate) fn json_inline(value: &serde_json::Value) -> String {
|
|
|
1405
1354
|
// the extracted base-command functions, not directly by this file.
|
|
1406
1355
|
use super::adapters::claude::{claude_base_command, claude_launch_command};
|
|
1407
1356
|
use super::adapters::codex::codex_base_command;
|
|
1408
|
-
use super::adapters::copilot::
|
|
1357
|
+
use super::adapters::copilot::copilot_base_command;
|
|
1409
1358
|
use super::adapters::cursor_agent::cursor_agent_base_command;
|
|
1410
1359
|
use super::adapters::fake::fake_worker_command;
|
|
1411
1360
|
use super::adapters::grok::{grok_base_command, grok_launch_command};
|