@team-agent/installer 0.5.48 → 0.5.50
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 +1 -1
- package/crates/team-agent/src/cli/emit.rs +89 -38
- package/crates/team-agent/src/cli/mod.rs +1 -1
- package/crates/team-agent/src/cli/named_address.rs +127 -25
- package/crates/team-agent/src/cli/send.rs +442 -521
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +32 -7
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -4
- package/crates/team-agent/src/cli/tests/status_send.rs +59 -33
- package/crates/team-agent/src/cli/types.rs +4 -8
- package/crates/team-agent/src/lifecycle/launch.rs +23 -9
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +33 -16
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -2
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +16 -5
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
- package/crates/team-agent/src/mcp_server/tools.rs +12 -10
- package/crates/team-agent/src/mcp_server/wire.rs +2 -18
- package/crates/team-agent/src/messaging/mod.rs +1 -0
- package/crates/team-agent/src/messaging/send.rs +34 -11
- package/package.json +4 -4
|
@@ -153,7 +153,7 @@ pub(crate) struct CommandSpec {
|
|
|
153
153
|
#[rustfmt::skip]
|
|
154
154
|
pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
155
155
|
CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
156
|
-
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "
|
|
156
|
+
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for a logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--json]", 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 },
|
|
157
157
|
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 },
|
|
158
158
|
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 },
|
|
159
159
|
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 },
|
|
@@ -149,7 +149,7 @@ fn named_send_args(
|
|
|
149
149
|
workspace: ws.to_path_buf(),
|
|
150
150
|
team: None,
|
|
151
151
|
task: None,
|
|
152
|
-
sender:
|
|
152
|
+
sender: TrustedSender::leader(),
|
|
153
153
|
no_ack: false,
|
|
154
154
|
no_wait: false,
|
|
155
155
|
watch_result: false,
|
|
@@ -222,6 +222,29 @@ fn resolve_name_team_disambiguated() {
|
|
|
222
222
|
let _ = std::fs::remove_dir_all(&ws);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
#[test]
|
|
226
|
+
fn resolve_qualified_name_canonicalizes_legacy_session_alias() {
|
|
227
|
+
let ws = named_ws("legacy-session-alias");
|
|
228
|
+
seed_state(
|
|
229
|
+
&ws,
|
|
230
|
+
state_with_teams(json!({
|
|
231
|
+
"teamdir": worker_team("team-displayname", "qa", "%1", "qa")
|
|
232
|
+
})),
|
|
233
|
+
);
|
|
234
|
+
let transport = OfflineTransport::new().with_targets(vec![pane(
|
|
235
|
+
"team-displayname",
|
|
236
|
+
"qa",
|
|
237
|
+
"%1",
|
|
238
|
+
)]);
|
|
239
|
+
|
|
240
|
+
let resolved = resolve_name_with_transport(&ws, "displayname/qa", &transport)
|
|
241
|
+
.expect("qualified legacy alias must resolve through the canonical selector");
|
|
242
|
+
assert_eq!(resolved.team_key.as_deref(), Some("teamdir"));
|
|
243
|
+
assert_eq!(resolved.agent_id.as_deref(), Some("qa"));
|
|
244
|
+
assert_eq!(resolved.pane_id, "%1");
|
|
245
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
246
|
+
}
|
|
247
|
+
|
|
225
248
|
#[test]
|
|
226
249
|
fn resolve_name_stale_pane() {
|
|
227
250
|
let ws = named_ws("stale-pane");
|
|
@@ -436,7 +459,7 @@ fn resolve_app_server_leader_name_fails_closed_on_transport_conflict() {
|
|
|
436
459
|
|
|
437
460
|
#[cfg(unix)]
|
|
438
461
|
#[test]
|
|
439
|
-
fn
|
|
462
|
+
fn send_to_name_app_server_leader_persists_before_async_delivery() {
|
|
440
463
|
let ws = named_ws("appserver-leader-send");
|
|
441
464
|
let fake = crate::app_server_test_support::FakeAppServer::start(
|
|
442
465
|
"named-send",
|
|
@@ -461,11 +484,13 @@ fn send_to_name_app_server_leader_submits_turn_without_tmux_pane() {
|
|
|
461
484
|
};
|
|
462
485
|
|
|
463
486
|
assert_eq!(value["ok"], json!(true));
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
487
|
+
let message_id = value["message_id"].as_str().expect("persisted message id");
|
|
488
|
+
assert!(message_id.starts_with("msg_"));
|
|
489
|
+
let store = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
490
|
+
assert!(store.message_exists(message_id).unwrap());
|
|
491
|
+
assert!(
|
|
492
|
+
fake.received_turns().is_empty(),
|
|
493
|
+
"send returns after persistence; coordinator owns physical delivery"
|
|
469
494
|
);
|
|
470
495
|
let _ = std::fs::remove_dir_all(&ws);
|
|
471
496
|
}
|
|
@@ -90,7 +90,7 @@ fn run_dispatches_status_to_handler_returns_ok() {
|
|
|
90
90
|
|
|
91
91
|
#[test]
|
|
92
92
|
fn run_dispatches_send_to_handler_returns_ok() {
|
|
93
|
-
// run([send w1 hello --
|
|
93
|
+
// run([send w1 hello --workspace <seeded>]) -> cmd_send -> messaging::send_message
|
|
94
94
|
// -> ok (w1 is an in-team agent) -> ExitCode::Ok. w1 must be seeded: golden refuses non-team targets.
|
|
95
95
|
//
|
|
96
96
|
// OLD seed: flat `{"agents": {"w1": ...}}`.
|
|
@@ -112,8 +112,6 @@ fn run_dispatches_send_to_handler_returns_ok() {
|
|
|
112
112
|
"send".to_string(),
|
|
113
113
|
"w1".to_string(),
|
|
114
114
|
"hello".to_string(),
|
|
115
|
-
"--sender".to_string(),
|
|
116
|
-
"leader".to_string(),
|
|
117
115
|
"--workspace".to_string(),
|
|
118
116
|
ws.to_string_lossy().to_string(),
|
|
119
117
|
];
|
|
@@ -230,7 +228,7 @@ fn cli_quick_start_invokes_real_lifecycle_compiles_spec() {
|
|
|
230
228
|
};
|
|
231
229
|
let _ = cmd_quick_start(&args); // real quick_start compiles the spec before any coordinator/launch step
|
|
232
230
|
// E5: spec compiled to .team/runtime/<team_key>/, NOT the user team dir.
|
|
233
|
-
let team_key =
|
|
231
|
+
let team_key = "clidelegteam";
|
|
234
232
|
let workspace = crate::model::paths::team_workspace(&team).unwrap();
|
|
235
233
|
let runtime_spec = crate::model::paths::runtime_spec_path(&workspace, &team_key);
|
|
236
234
|
assert!(
|
|
@@ -460,7 +460,7 @@ fn send_args_fixture() -> SendArgs {
|
|
|
460
460
|
workspace: PathBuf::from("."),
|
|
461
461
|
team: Some("teamA".into()),
|
|
462
462
|
task: Some("t-1".into()),
|
|
463
|
-
sender:
|
|
463
|
+
sender: TrustedSender::leader(),
|
|
464
464
|
no_ack: true,
|
|
465
465
|
no_wait: true,
|
|
466
466
|
watch_result: true,
|
|
@@ -515,7 +515,7 @@ fn send_options_negates_no_ack_and_no_wait_and_carries_watch() {
|
|
|
515
515
|
"watch_result flag MUST pass through into SendOptions"
|
|
516
516
|
);
|
|
517
517
|
assert!(!opts.confirm_human);
|
|
518
|
-
assert_eq!(opts.sender, "leader");
|
|
518
|
+
assert_eq!(opts.sender.as_str(), "leader");
|
|
519
519
|
assert_eq!(opts.timeout, 12.5);
|
|
520
520
|
}
|
|
521
521
|
|
|
@@ -706,6 +706,48 @@ fn cmd_send_failed_outcome_yields_error_exit() {
|
|
|
706
706
|
}
|
|
707
707
|
}
|
|
708
708
|
|
|
709
|
+
#[test]
|
|
710
|
+
fn cmd_send_canonicalizes_legacy_session_alias_before_membership_projection() {
|
|
711
|
+
let ws = deleg_uniq_dir("send-team-alias");
|
|
712
|
+
let _ = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
713
|
+
crate::state::persist::save_runtime_state(
|
|
714
|
+
&ws,
|
|
715
|
+
&json!({
|
|
716
|
+
"active_team_key": "teamdir",
|
|
717
|
+
"team_key": "teamdir",
|
|
718
|
+
"teams": {
|
|
719
|
+
"teamdir": {
|
|
720
|
+
"status": "alive",
|
|
721
|
+
"session_name": "team-displayname",
|
|
722
|
+
"agents": {"w1": {"provider": "codex", "status": "running"}}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}),
|
|
726
|
+
)
|
|
727
|
+
.unwrap();
|
|
728
|
+
let args = SendArgs {
|
|
729
|
+
workspace: ws,
|
|
730
|
+
target: Some("w1".into()),
|
|
731
|
+
team: Some("displayname".into()),
|
|
732
|
+
task: None,
|
|
733
|
+
no_wait: true,
|
|
734
|
+
watch_result: false,
|
|
735
|
+
json: true,
|
|
736
|
+
..send_args_fixture()
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
let result = cmd_send(&args).expect("alias selection should reach canonical team send");
|
|
740
|
+
let value = match result.output {
|
|
741
|
+
CmdOutput::Json(value) => value,
|
|
742
|
+
other => panic!("expected JSON send outcome, got {other:?}"),
|
|
743
|
+
};
|
|
744
|
+
assert_ne!(
|
|
745
|
+
value.get("reason").and_then(serde_json::Value::as_str),
|
|
746
|
+
Some("target_not_in_team"),
|
|
747
|
+
"selector accepted the alias, so membership must use its canonical team key: {value}"
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
709
751
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
710
752
|
// coordinator.ok — non-compact status carries the FULL coordinator_health (incl. `ok`); compact
|
|
711
753
|
// strips to {status,pid,metadata_ok,schema_ok} (golden queries.py:77 + compact.py:35; ok =
|
|
@@ -824,15 +866,8 @@ fn cmd_send_unknown_task_surfaces_golden_error_envelope_not_silent() {
|
|
|
824
866
|
let _ = std::fs::remove_dir_all(&ws);
|
|
825
867
|
}
|
|
826
868
|
|
|
827
|
-
// P0 (b') — the SWALLOW guard: `run()` (the CLI process entry) MUST RENDER the send error, not
|
|
828
|
-
// discard Err(CliError) via unwrap_or (advisor %7 root cause). Proxy: emit_cli_error WRITES a
|
|
829
|
-
// `.team/logs/cli-error-*.log` (and prints the compact envelope) — if run() swallowed, neither
|
|
830
|
-
// happens. So a cli-error log containing the BARE "unknown task id: <id>" (no "validation:" prefix)
|
|
831
|
-
// + ExitCode::Error proves run() rendered. Drives the real argv→(exit,render) path.
|
|
832
|
-
// OLD/NEW: same Bug 1/2 seed sync as cmd_send_unknown_task_*; the render-vs-swallow
|
|
833
|
-
// behavior under test is unchanged.
|
|
834
869
|
#[test]
|
|
835
|
-
fn
|
|
870
|
+
fn run_send_legacy_task_flag_is_internalized_before_persistence() {
|
|
836
871
|
let ws = std::env::temp_dir().join(format!(
|
|
837
872
|
"ta-run-sendunk-{}-{}",
|
|
838
873
|
std::process::id(),
|
|
@@ -864,30 +899,21 @@ fn run_send_unknown_task_renders_error_not_silent_swallow() {
|
|
|
864
899
|
let code = run(&argv, &ws);
|
|
865
900
|
assert_eq!(
|
|
866
901
|
code,
|
|
867
|
-
ExitCode::
|
|
868
|
-
"
|
|
869
|
-
);
|
|
870
|
-
// run() must have RENDERED (emit_cli_error wrote the cli-error log); a swallow leaves none.
|
|
871
|
-
let logs_dir = ws.join(".team").join("logs");
|
|
872
|
-
let mut found = String::new();
|
|
873
|
-
if let Ok(entries) = std::fs::read_dir(&logs_dir) {
|
|
874
|
-
for entry in entries.flatten() {
|
|
875
|
-
let name = entry.file_name().to_string_lossy().to_string();
|
|
876
|
-
if name.starts_with("cli-error-") {
|
|
877
|
-
found = std::fs::read_to_string(entry.path()).unwrap_or_default();
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
assert!(
|
|
883
|
-
found.contains("unknown task id: t-unknown"),
|
|
884
|
-
"run() must RENDER the send error (cli-error log written with the bare message) — a silent \
|
|
885
|
-
swallow (unwrap_or discards Err) leaves no log. got log body: {found:?}"
|
|
886
|
-
);
|
|
887
|
-
assert!(
|
|
888
|
-
!found.contains("validation:"),
|
|
889
|
-
"rendered error must be the bare golden message, NO 'validation:' prefix; got {found:?}"
|
|
902
|
+
ExitCode::Ok,
|
|
903
|
+
"legacy --task is sunset-noticed and ignored; public send persists without caller binding"
|
|
890
904
|
);
|
|
905
|
+
let connection = rusqlite::Connection::open(
|
|
906
|
+
ws.join(".team").join("runtime").join("team.db"),
|
|
907
|
+
)
|
|
908
|
+
.unwrap();
|
|
909
|
+
let task_id: Option<String> = connection
|
|
910
|
+
.query_row(
|
|
911
|
+
"SELECT task_id FROM messages WHERE content = 'go' ORDER BY rowid DESC LIMIT 1",
|
|
912
|
+
[],
|
|
913
|
+
|row| row.get(0),
|
|
914
|
+
)
|
|
915
|
+
.unwrap();
|
|
916
|
+
assert_eq!(task_id, None, "caller-supplied task ids must not reach storage");
|
|
891
917
|
let _ = std::fs::remove_dir_all(&ws);
|
|
892
918
|
}
|
|
893
919
|
|
|
@@ -312,7 +312,7 @@ pub struct SendArgs {
|
|
|
312
312
|
pub workspace: PathBuf,
|
|
313
313
|
pub team: Option<String>,
|
|
314
314
|
pub task: Option<String>,
|
|
315
|
-
pub sender:
|
|
315
|
+
pub sender: TrustedSender,
|
|
316
316
|
pub no_ack: bool,
|
|
317
317
|
pub no_wait: bool,
|
|
318
318
|
pub watch_result: bool,
|
|
@@ -323,16 +323,12 @@ pub struct SendArgs {
|
|
|
323
323
|
/// When set, the store insert uses this id verbatim; a repeat with the same
|
|
324
324
|
/// id returns a `Duplicate` refusal instead of creating a second row.
|
|
325
325
|
pub message_id: Option<String>,
|
|
326
|
-
///
|
|
327
|
-
///
|
|
328
|
-
/// directly into the specified tmux pane via `transport.inject`, bypassing
|
|
329
|
-
/// the agent-name → pane-id resolution + team-membership check. This is
|
|
330
|
-
/// the **cross-team communication** primitive: the target pane does not
|
|
331
|
-
/// need to be in the sender's team.
|
|
326
|
+
/// Deprecated compatibility input. Public send refuses pane identity and
|
|
327
|
+
/// requires a logical recipient so persistence always precedes delivery.
|
|
332
328
|
pub pane: Option<String>,
|
|
333
329
|
/// `--to-name <name>` — stable named addressing. Mutually exclusive with
|
|
334
330
|
/// `target` / `targets` / `--pane`; resolves workspace/team/agent or leader
|
|
335
|
-
/// name to the
|
|
331
|
+
/// name before delegating to the persisted message primitive.
|
|
336
332
|
pub to_name: Option<String>,
|
|
337
333
|
/// E7 (0.5.9 host-leader-registry-design): `--to-leader NAME` resolves
|
|
338
334
|
/// NAME through `~/.team-agent/leaders`, canonical-validates, and then
|
|
@@ -3167,10 +3167,18 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3167
3167
|
if !open_display {
|
|
3168
3168
|
override_spec_display_backend(&mut spec, "none");
|
|
3169
3169
|
}
|
|
3170
|
-
let requested_team = quick_start_requested_team_key(team_id, name)
|
|
3171
|
-
.map(str::to_string)
|
|
3172
|
-
.or_else(|| spec_team_id(&spec));
|
|
3173
3170
|
let explicit_team_key = quick_start_requested_team_key(team_id, name).map(str::to_string);
|
|
3171
|
+
let canonical_team_key = explicit_team_key
|
|
3172
|
+
.clone()
|
|
3173
|
+
.or_else(|| spec_team_id(&spec).filter(|team| !team.is_empty()))
|
|
3174
|
+
.unwrap_or_else(|| {
|
|
3175
|
+
runtime_team_key_for_spec(
|
|
3176
|
+
&agents_dir.join("team.spec.yaml"),
|
|
3177
|
+
&spec,
|
|
3178
|
+
&spec_session_name(&spec),
|
|
3179
|
+
)
|
|
3180
|
+
});
|
|
3181
|
+
let requested_team = Some(canonical_team_key.clone());
|
|
3174
3182
|
let team_depth = quick_start_depth_guard(
|
|
3175
3183
|
&workspace,
|
|
3176
3184
|
agents_dir,
|
|
@@ -3248,14 +3256,12 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3248
3256
|
// spec's runtime.session_name with one derived from the REQUESTED team identity
|
|
3249
3257
|
// so launch_with_transport (which reads runtime.session_name) spawns into an
|
|
3250
3258
|
// isolated session per requested team.
|
|
3251
|
-
if let Some(requested) =
|
|
3259
|
+
if let Some(requested) = explicit_team_key.as_deref() {
|
|
3252
3260
|
override_spec_session_name(&mut spec, &format!("team-{requested}"));
|
|
3253
3261
|
}
|
|
3254
3262
|
let session_name = spec_session_name(&spec);
|
|
3255
|
-
// team_key
|
|
3256
|
-
let state_team_key =
|
|
3257
|
-
runtime_team_key_for_spec(&agents_dir.join("team.spec.yaml"), &spec, &session_name)
|
|
3258
|
-
});
|
|
3263
|
+
// team_key 已在入口由显式 id/name 或 compiled spec identity 单次确定。
|
|
3264
|
+
let state_team_key = canonical_team_key;
|
|
3259
3265
|
warn_ignored_owner_team_id(workspace.as_path(), agents_dir, &state_team_key);
|
|
3260
3266
|
// E5 spec 迁移:spec 写到 .team/runtime/<team_key>/(中间产物,绝不落用户目录 agents_dir)。
|
|
3261
3267
|
// Bug2:原子写(tmp+rename),避免半截 spec。
|
|
@@ -3265,7 +3271,13 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3265
3271
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
3266
3272
|
let resolved_spec_path =
|
|
3267
3273
|
std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
3268
|
-
let mut state = initial_runtime_state(
|
|
3274
|
+
let mut state = initial_runtime_state(
|
|
3275
|
+
&spec,
|
|
3276
|
+
&resolved_spec_path,
|
|
3277
|
+
&workspace,
|
|
3278
|
+
agents_dir,
|
|
3279
|
+
&state_team_key,
|
|
3280
|
+
);
|
|
3269
3281
|
// 0.5.x Phase 1d hot-path 接线(裁决1 msg_76e1d98202b8): use the
|
|
3270
3282
|
// generic annotator that writes `state.transport = { kind, source }`
|
|
3271
3283
|
// for every backend AND (for tmux) preserves the existing
|
|
@@ -5104,6 +5116,7 @@ fn initial_runtime_state(
|
|
|
5104
5116
|
spec_path: &Path,
|
|
5105
5117
|
workspace: &Path,
|
|
5106
5118
|
team_dir: &Path,
|
|
5119
|
+
team_key: &str,
|
|
5107
5120
|
) -> serde_json::Value {
|
|
5108
5121
|
let mut agents = serde_json::Map::new();
|
|
5109
5122
|
for agent in spec_agent_values(spec) {
|
|
@@ -5145,6 +5158,7 @@ fn initial_runtime_state(
|
|
|
5145
5158
|
"team_dir".to_string(),
|
|
5146
5159
|
serde_json::json!(team_dir.to_string_lossy().to_string()),
|
|
5147
5160
|
);
|
|
5161
|
+
state.insert("team_key".to_string(), serde_json::json!(team_key));
|
|
5148
5162
|
state.insert(
|
|
5149
5163
|
"session_name".to_string(),
|
|
5150
5164
|
serde_json::json!(spec_session_name(spec).as_str()),
|
|
@@ -80,11 +80,10 @@ fn layout_pane(
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/// E5 spec 迁移:spec 不再落 team_dir,而在 <team_workspace>/.team/runtime/<team_key>/。
|
|
83
|
-
///
|
|
83
|
+
/// quick-start 未显式指定 id 时,team_key 来自 compiled TEAM.md name(`quickteam`)。
|
|
84
84
|
pub(super) fn quick_start_runtime_spec_path(team: &std::path::Path) -> PathBuf {
|
|
85
85
|
let workspace = crate::model::paths::team_workspace(team).unwrap();
|
|
86
|
-
|
|
87
|
-
crate::model::paths::runtime_spec_path(&workspace, &team_key)
|
|
86
|
+
crate::model::paths::runtime_spec_path(&workspace, "quickteam")
|
|
88
87
|
}
|
|
89
88
|
|
|
90
89
|
/// 在 team 自身或其 team_workspace 父下找 .team/runtime/*/team.spec.yaml。run_workspace 解析
|
|
@@ -172,6 +171,26 @@ fn quick_start_compiles_real_spec_to_team_spec_yaml() {
|
|
|
172
171
|
}
|
|
173
172
|
}
|
|
174
173
|
|
|
174
|
+
#[test]
|
|
175
|
+
fn quick_start_without_team_id_uses_compiled_team_name_as_canonical_key() {
|
|
176
|
+
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
177
|
+
let workspace = crate::model::paths::team_workspace(&team).unwrap();
|
|
178
|
+
let transport = OfflineTransport::new();
|
|
179
|
+
|
|
180
|
+
let result = quick_start_with_transport(&team, None, true, None, &transport);
|
|
181
|
+
assert!(result.is_ok(), "quick-start fixture must run: {result:?}");
|
|
182
|
+
|
|
183
|
+
let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
184
|
+
assert_eq!(state["active_team_key"], json!("quickteam"));
|
|
185
|
+
assert_eq!(state["team_key"], json!("quickteam"));
|
|
186
|
+
assert!(state["teams"].get("quickteam").is_some(), "{state}");
|
|
187
|
+
assert!(state["teams"].get("teamdir").is_none(), "{state}");
|
|
188
|
+
assert!(
|
|
189
|
+
crate::model::paths::runtime_spec_path(&workspace, "quickteam").exists(),
|
|
190
|
+
"runtime spec must share the canonical key"
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
175
194
|
#[test]
|
|
176
195
|
fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_collect() {
|
|
177
196
|
let workspace = temp_ws();
|
|
@@ -215,8 +234,8 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
|
|
|
215
234
|
"input={}",
|
|
216
235
|
input.display()
|
|
217
236
|
);
|
|
218
|
-
// E5: selector resolves spec_path to .team/runtime/<
|
|
219
|
-
let expected_spec = crate::model::paths::runtime_spec_path(&workspace, "
|
|
237
|
+
// E5: selector resolves spec_path to .team/runtime/<canonical team_key>/.
|
|
238
|
+
let expected_spec = crate::model::paths::runtime_spec_path(&workspace, "quickteam");
|
|
220
239
|
assert_eq!(
|
|
221
240
|
selected
|
|
222
241
|
.spec_path
|
|
@@ -1319,7 +1338,7 @@ fn quick_start_persists_selected_tmux_endpoint_and_attach_commands() {
|
|
|
1319
1338
|
assert_eq!(state["tmux_socket"], json!(endpoint));
|
|
1320
1339
|
assert_eq!(state["is_external_leader"], json!(false));
|
|
1321
1340
|
assert_eq!(
|
|
1322
|
-
state["teams"]["
|
|
1341
|
+
state["teams"]["quickteam"]["is_external_leader"],
|
|
1323
1342
|
json!(false)
|
|
1324
1343
|
);
|
|
1325
1344
|
}
|
|
@@ -2876,7 +2895,7 @@ fn quick_start_seeds_tasks_key_from_compiled_spec() {
|
|
|
2876
2895
|
}
|
|
2877
2896
|
|
|
2878
2897
|
// Stage A — golden launch/core.py:62-71 seeded top-level runtime state in insertion order:
|
|
2879
|
-
// spec_path, workspace, team_dir, session_name, leader, agents, tasks, display_backend.
|
|
2898
|
+
// spec_path, workspace, team_dir, team_key, session_name, leader, agents, tasks, display_backend.
|
|
2880
2899
|
//
|
|
2881
2900
|
// OLD (Python parity): the top-level shape ended at `display_backend`.
|
|
2882
2901
|
// NEW (Bug 1/2 — team-in-team state scope, see tests/team_in_team_state_scope_red.rs):
|
|
@@ -2889,9 +2908,9 @@ fn quick_start_seeds_tasks_key_from_compiled_spec() {
|
|
|
2889
2908
|
// The post-launch hook drops the top-level owner triple when the launched pane is
|
|
2890
2909
|
// unbound (empty pane_id), and only the per-team entry retains the binding.
|
|
2891
2910
|
// R1: topology is explicit; fresh managed teams carry `is_external_leader=false`
|
|
2892
|
-
// before the team-in-team suffix.
|
|
2893
|
-
//
|
|
2894
|
-
// teams). display_backend stays the resolved backend from display/backend.py:12-29
|
|
2911
|
+
// before the team-in-team suffix. Canonical `team_key` is seeded before owner
|
|
2912
|
+
// binding; the remaining order stays the golden prefix + topology marker + new
|
|
2913
|
+
// suffix (active_team_key, teams). display_backend stays the resolved backend from display/backend.py:12-29
|
|
2895
2914
|
// (default adaptive), not the raw optional spec field.
|
|
2896
2915
|
#[test]
|
|
2897
2916
|
#[serial(env)]
|
|
@@ -2924,6 +2943,9 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
|
|
|
2924
2943
|
"spec_path",
|
|
2925
2944
|
"workspace",
|
|
2926
2945
|
"team_dir",
|
|
2946
|
+
// Seed canonical identity before owner binding so the initial
|
|
2947
|
+
// receiver is written under the same team key as the saved state.
|
|
2948
|
+
"team_key",
|
|
2927
2949
|
"session_name",
|
|
2928
2950
|
"leader",
|
|
2929
2951
|
"agents",
|
|
@@ -2937,17 +2959,12 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
|
|
|
2937
2959
|
// field); source is `"unknown"` until Phase 2 threads
|
|
2938
2960
|
// `ResolvedTransport.source` through the launch call site.
|
|
2939
2961
|
"transport",
|
|
2940
|
-
// 0.4.0 refactor: `team_key` added as top-level topology marker
|
|
2941
|
-
// alongside active_team_key (refactor-modular-architecture). The
|
|
2942
|
-
// owner / leader_receiver / owner_epoch invariants below still
|
|
2943
|
-
// hold — those live only under teams[<active_team_key>].
|
|
2944
|
-
"team_key",
|
|
2945
2962
|
"active_team_key",
|
|
2946
2963
|
"teams",
|
|
2947
2964
|
],
|
|
2948
2965
|
"state.json top-level key order must match golden launch/core.py:62-71 \
|
|
2949
2966
|
plus R1 topology marker and Bug 1/2 team-in-team suffix \
|
|
2950
|
-
(is_external_leader,
|
|
2967
|
+
(is_external_leader, active_team_key, teams). \
|
|
2951
2968
|
Bug 2 owner team-scope (N1/N12/N18/N29) deliberately keeps owner / \
|
|
2952
2969
|
leader_receiver / owner_epoch OFF the top level — they live ONLY under \
|
|
2953
2970
|
teams[<active_team_key>] so per-team isolation has a single source of \
|
|
@@ -451,7 +451,7 @@ fn team_dir_with_roles(role_docs: &[(&str, &str)]) -> PathBuf {
|
|
|
451
451
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
452
452
|
std::fs::write(
|
|
453
453
|
team.join("TEAM.md"),
|
|
454
|
-
"---\nname:
|
|
454
|
+
"---\nname: teamdir\nobjective: Phase B.\nprovider: codex\nsession_name: team-phaseb\n---\n\nPhase B team.\n",
|
|
455
455
|
)
|
|
456
456
|
.unwrap();
|
|
457
457
|
for (file, role_doc) in role_docs {
|
|
@@ -536,7 +536,7 @@ fn add_fixture() -> (PathBuf, PathBuf, PathBuf, OfflineTransport) {
|
|
|
536
536
|
quick_start_with_transport_in_workspace_with_display(
|
|
537
537
|
&workspace,
|
|
538
538
|
&team,
|
|
539
|
-
|
|
539
|
+
Some("teamdir"),
|
|
540
540
|
true,
|
|
541
541
|
None,
|
|
542
542
|
&launch_transport,
|
|
@@ -259,7 +259,7 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
|
|
|
259
259
|
workspace: workspace.clone(),
|
|
260
260
|
team: Some(spec.team_key.to_string()),
|
|
261
261
|
task: None,
|
|
262
|
-
sender:
|
|
262
|
+
sender: crate::messaging::TrustedSender::leader(),
|
|
263
263
|
no_ack: true,
|
|
264
264
|
no_wait: true,
|
|
265
265
|
watch_result: false,
|
|
@@ -367,7 +367,7 @@ fn two_worker_team_dir(hermetic: &HermeticTestEnv) -> PathBuf {
|
|
|
367
367
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
368
368
|
std::fs::write(
|
|
369
369
|
team.join("TEAM.md"),
|
|
370
|
-
"---\nname:
|
|
370
|
+
"---\nname: teamdir\nobjective: Phase golden.\nprovider: codex\nsession_name: team-phasegolden\n---\n\nPhase golden team.\n",
|
|
371
371
|
)
|
|
372
372
|
.unwrap();
|
|
373
373
|
for id in ["w1", "w2"] {
|
|
@@ -88,7 +88,7 @@ use crate::message_store::MessageStore;
|
|
|
88
88
|
use crate::state::persist::{load_runtime_state, save_runtime_state};
|
|
89
89
|
|
|
90
90
|
// ── REUSE: step 11 messaging delegate surface ───────────────────────────────
|
|
91
|
-
use crate::messaging::{self, DeliveryOutcome, MessageTarget, SendOptions};
|
|
91
|
+
use crate::messaging::{self, DeliveryOutcome, MessageTarget, SendOptions, TrustedSender};
|
|
92
92
|
|
|
93
93
|
pub mod helpers;
|
|
94
94
|
pub(crate) mod lifecycle_tools;
|
|
@@ -155,7 +155,6 @@ fn send_message_worker_recipient_returns_accepted_with_poll_hint() {
|
|
|
155
155
|
None,
|
|
156
156
|
None,
|
|
157
157
|
None,
|
|
158
|
-
None,
|
|
159
158
|
);
|
|
160
159
|
match outcome {
|
|
161
160
|
Ok(SendOutcome::WorkerAccepted {
|
|
@@ -185,7 +184,6 @@ fn ordinary_send_assign_shape_has_no_recovery_marker() {
|
|
|
185
184
|
None,
|
|
186
185
|
None,
|
|
187
186
|
None,
|
|
188
|
-
None,
|
|
189
187
|
)
|
|
190
188
|
.expect("ordinary send ok")
|
|
191
189
|
.to_value();
|
|
@@ -240,6 +238,22 @@ fn ordinary_send_assign_shape_has_no_recovery_marker() {
|
|
|
240
238
|
);
|
|
241
239
|
}
|
|
242
240
|
|
|
241
|
+
#[test]
|
|
242
|
+
fn send_message_without_framework_identity_fails_closed() {
|
|
243
|
+
let ws = seed_current_worker_state("missing-sender-identity");
|
|
244
|
+
let tools = TeamOrchestratorTools::with_identity(&ws, None, Some(TeamKey::new("current")));
|
|
245
|
+
let error = tools
|
|
246
|
+
.send_message(
|
|
247
|
+
&MessageTarget::Single("worker-1".to_string()),
|
|
248
|
+
"must not be attributed to unknown",
|
|
249
|
+
None,
|
|
250
|
+
None,
|
|
251
|
+
None,
|
|
252
|
+
)
|
|
253
|
+
.expect_err("missing framework identity must fail before persistence");
|
|
254
|
+
assert_eq!(error.reason, ToolErrorReason::McpScopeRefused);
|
|
255
|
+
}
|
|
256
|
+
|
|
243
257
|
#[test]
|
|
244
258
|
fn recovery_assign_shape_has_structured_marker() {
|
|
245
259
|
let ws = seed_current_worker_state("recovery-marker");
|
|
@@ -304,7 +318,6 @@ fn send_message_worker_recipient_surfaces_dead_coordinator_warning() {
|
|
|
304
318
|
None,
|
|
305
319
|
None,
|
|
306
320
|
None,
|
|
307
|
-
None,
|
|
308
321
|
)
|
|
309
322
|
.expect("send returns degraded warning, not an MCP error");
|
|
310
323
|
let v = outcome.to_value();
|
|
@@ -336,7 +349,6 @@ fn send_message_leader_recipient_is_direct_not_accepted() {
|
|
|
336
349
|
None,
|
|
337
350
|
None,
|
|
338
351
|
None,
|
|
339
|
-
None,
|
|
340
352
|
)
|
|
341
353
|
.expect("leader send ok");
|
|
342
354
|
assert!(
|
|
@@ -435,7 +447,6 @@ fn send_message_cross_team_peer_surfaces_peer_not_in_scope_error() {
|
|
|
435
447
|
None,
|
|
436
448
|
None,
|
|
437
449
|
None,
|
|
438
|
-
None,
|
|
439
450
|
)
|
|
440
451
|
.expect_err("out-of-scope peer must be refused");
|
|
441
452
|
assert_eq!(err.reason, ToolErrorReason::PeerNotInScope);
|
|
@@ -66,6 +66,12 @@
|
|
|
66
66
|
);
|
|
67
67
|
assert_eq!(send["inputSchema"]["additionalProperties"], json!(false));
|
|
68
68
|
assert_eq!(send["inputSchema"]["required"], json!(["to", "content"]));
|
|
69
|
+
for internal in ["sender", "task_id", "requires_ack"] {
|
|
70
|
+
assert!(
|
|
71
|
+
send["inputSchema"]["properties"].get(internal).is_none(),
|
|
72
|
+
"{internal} is framework-owned, not caller-supplied"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
#[test]
|
|
@@ -17,7 +17,7 @@ use crate::state::persist::{
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
// ── REUSE: step 11 messaging delegate surface ───────────────────────────────
|
|
20
|
-
use crate::messaging::{self, MessageTarget, SendOptions};
|
|
20
|
+
use crate::messaging::{self, MessageTarget, SendOptions, TrustedSender};
|
|
21
21
|
|
|
22
22
|
use super::helpers::{
|
|
23
23
|
current_reportable_message_for, delivery_outcome_value, direct_message_attribution_for,
|
|
@@ -140,7 +140,6 @@ impl TeamOrchestratorTools {
|
|
|
140
140
|
Some(task_id),
|
|
141
141
|
None,
|
|
142
142
|
None,
|
|
143
|
-
None,
|
|
144
143
|
)?;
|
|
145
144
|
let mut ok = compact_tool_result(&out.to_value())?;
|
|
146
145
|
if recovery {
|
|
@@ -155,7 +154,7 @@ impl TeamOrchestratorTools {
|
|
|
155
154
|
}
|
|
156
155
|
|
|
157
156
|
/// `send_message` (`tools.py:135-183`): C14/C15/C17 scope resolution.
|
|
158
|
-
/// - sender =
|
|
157
|
+
/// - sender = immutable `TEAM_AGENT_ID` captured when the MCP server starts.
|
|
159
158
|
/// - `requires_ack` defaults from target (`_requires_ack_for_target`).
|
|
160
159
|
/// - C23 cross-team pre-refusal ([`Self::refuse_cross_team_peer`]) before any
|
|
161
160
|
/// runtime call.
|
|
@@ -168,7 +167,6 @@ impl TeamOrchestratorTools {
|
|
|
168
167
|
to: &MessageTarget,
|
|
169
168
|
content: &str,
|
|
170
169
|
task_id: Option<&str>,
|
|
171
|
-
sender: Option<&str>,
|
|
172
170
|
requires_ack: Option<bool>,
|
|
173
171
|
scope_override: Option<Scope>,
|
|
174
172
|
) -> Result<SendOutcome, ToolError> {
|
|
@@ -183,10 +181,14 @@ impl TeamOrchestratorTools {
|
|
|
183
181
|
if let Some(err) = self.refuse_cross_team_peer(to, None) {
|
|
184
182
|
return Err(err);
|
|
185
183
|
}
|
|
186
|
-
let sender =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
let sender = self.agent_id.clone().ok_or_else(|| {
|
|
185
|
+
ToolError::new(
|
|
186
|
+
ToolErrorReason::McpScopeRefused,
|
|
187
|
+
"send_message requires framework-injected TEAM_AGENT_ID",
|
|
188
|
+
"IdentityError",
|
|
189
|
+
)
|
|
190
|
+
})?;
|
|
191
|
+
let sender = TrustedSender::from_runtime_identity(sender);
|
|
190
192
|
let ack = requires_ack.unwrap_or_else(|| requires_ack_for_target(to));
|
|
191
193
|
// C14/C15/C17 scope audit (#230 I-2/I-6 contract): emit mcp.scope_resolved
|
|
192
194
|
// for every worker-origin send before any routing/delivery — the funnel
|
|
@@ -197,7 +199,7 @@ impl TeamOrchestratorTools {
|
|
|
197
199
|
"mcp.scope_resolved",
|
|
198
200
|
serde_json::json!({
|
|
199
201
|
"tool": "send_message",
|
|
200
|
-
"sender": sender,
|
|
202
|
+
"sender": sender.as_str(),
|
|
201
203
|
"owner_team_id": canonical_owner_team.as_ref().map(TeamKey::as_str),
|
|
202
204
|
"to": match to {
|
|
203
205
|
MessageTarget::Single(t) => serde_json::Value::String(t.clone()),
|
|
@@ -213,7 +215,7 @@ impl TeamOrchestratorTools {
|
|
|
213
215
|
let opts = SendOptions {
|
|
214
216
|
task_id: task_id.map(TaskId::new),
|
|
215
217
|
route_task_id: true,
|
|
216
|
-
sender
|
|
218
|
+
sender,
|
|
217
219
|
requires_ack: ack,
|
|
218
220
|
team: canonical_owner_team,
|
|
219
221
|
..SendOptions::default()
|