@team-agent/installer 0.5.49 → 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 +71 -5
- package/crates/team-agent/src/cli/send.rs +438 -518
- 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 +9 -7
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
- package/crates/team-agent/src/cli/types.rs +4 -8
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- 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,
|
|
@@ -459,7 +459,7 @@ fn resolve_app_server_leader_name_fails_closed_on_transport_conflict() {
|
|
|
459
459
|
|
|
460
460
|
#[cfg(unix)]
|
|
461
461
|
#[test]
|
|
462
|
-
fn
|
|
462
|
+
fn send_to_name_app_server_leader_persists_before_async_delivery() {
|
|
463
463
|
let ws = named_ws("appserver-leader-send");
|
|
464
464
|
let fake = crate::app_server_test_support::FakeAppServer::start(
|
|
465
465
|
"named-send",
|
|
@@ -484,11 +484,13 @@ fn send_to_name_app_server_leader_submits_turn_without_tmux_pane() {
|
|
|
484
484
|
};
|
|
485
485
|
|
|
486
486
|
assert_eq!(value["ok"], json!(true));
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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"
|
|
492
494
|
);
|
|
493
495
|
let _ = std::fs::remove_dir_all(&ws);
|
|
494
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
|
];
|
|
@@ -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
|
|
|
@@ -866,15 +866,8 @@ fn cmd_send_unknown_task_surfaces_golden_error_envelope_not_silent() {
|
|
|
866
866
|
let _ = std::fs::remove_dir_all(&ws);
|
|
867
867
|
}
|
|
868
868
|
|
|
869
|
-
// P0 (b') — the SWALLOW guard: `run()` (the CLI process entry) MUST RENDER the send error, not
|
|
870
|
-
// discard Err(CliError) via unwrap_or (advisor %7 root cause). Proxy: emit_cli_error WRITES a
|
|
871
|
-
// `.team/logs/cli-error-*.log` (and prints the compact envelope) — if run() swallowed, neither
|
|
872
|
-
// happens. So a cli-error log containing the BARE "unknown task id: <id>" (no "validation:" prefix)
|
|
873
|
-
// + ExitCode::Error proves run() rendered. Drives the real argv→(exit,render) path.
|
|
874
|
-
// OLD/NEW: same Bug 1/2 seed sync as cmd_send_unknown_task_*; the render-vs-swallow
|
|
875
|
-
// behavior under test is unchanged.
|
|
876
869
|
#[test]
|
|
877
|
-
fn
|
|
870
|
+
fn run_send_legacy_task_flag_is_internalized_before_persistence() {
|
|
878
871
|
let ws = std::env::temp_dir().join(format!(
|
|
879
872
|
"ta-run-sendunk-{}-{}",
|
|
880
873
|
std::process::id(),
|
|
@@ -906,30 +899,21 @@ fn run_send_unknown_task_renders_error_not_silent_swallow() {
|
|
|
906
899
|
let code = run(&argv, &ws);
|
|
907
900
|
assert_eq!(
|
|
908
901
|
code,
|
|
909
|
-
ExitCode::
|
|
910
|
-
"
|
|
911
|
-
);
|
|
912
|
-
// run() must have RENDERED (emit_cli_error wrote the cli-error log); a swallow leaves none.
|
|
913
|
-
let logs_dir = ws.join(".team").join("logs");
|
|
914
|
-
let mut found = String::new();
|
|
915
|
-
if let Ok(entries) = std::fs::read_dir(&logs_dir) {
|
|
916
|
-
for entry in entries.flatten() {
|
|
917
|
-
let name = entry.file_name().to_string_lossy().to_string();
|
|
918
|
-
if name.starts_with("cli-error-") {
|
|
919
|
-
found = std::fs::read_to_string(entry.path()).unwrap_or_default();
|
|
920
|
-
break;
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
assert!(
|
|
925
|
-
found.contains("unknown task id: t-unknown"),
|
|
926
|
-
"run() must RENDER the send error (cli-error log written with the bare message) — a silent \
|
|
927
|
-
swallow (unwrap_or discards Err) leaves no log. got log body: {found:?}"
|
|
928
|
-
);
|
|
929
|
-
assert!(
|
|
930
|
-
!found.contains("validation:"),
|
|
931
|
-
"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"
|
|
932
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");
|
|
933
917
|
let _ = std::fs::remove_dir_all(&ws);
|
|
934
918
|
}
|
|
935
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
|
|
@@ -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,
|
|
@@ -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()
|
|
@@ -417,21 +417,6 @@ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
|
|
|
417
417
|
string_property("Target agent id, 'leader', or '*' for broadcast."),
|
|
418
418
|
);
|
|
419
419
|
insert_property(&mut properties, "content", string_property("Message body."));
|
|
420
|
-
insert_property(
|
|
421
|
-
&mut properties,
|
|
422
|
-
"task_id",
|
|
423
|
-
string_property("Optional task id to associate with the message."),
|
|
424
|
-
);
|
|
425
|
-
insert_property(
|
|
426
|
-
&mut properties,
|
|
427
|
-
"sender",
|
|
428
|
-
string_property("Optional sender override."),
|
|
429
|
-
);
|
|
430
|
-
insert_property(
|
|
431
|
-
&mut properties,
|
|
432
|
-
"requires_ack",
|
|
433
|
-
boolean_property("Whether the recipient should acknowledge delivery."),
|
|
434
|
-
);
|
|
435
420
|
}
|
|
436
421
|
McpTool::ReportResult => {
|
|
437
422
|
insert_property(
|
|
@@ -609,9 +594,8 @@ pub(crate) fn dispatch_tool(
|
|
|
609
594
|
let outcome = tools.send_message(
|
|
610
595
|
&target,
|
|
611
596
|
content,
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
args.get("requires_ack").and_then(Value::as_bool),
|
|
597
|
+
None,
|
|
598
|
+
None,
|
|
615
599
|
None,
|
|
616
600
|
)?;
|
|
617
601
|
match outcome {
|
|
@@ -96,6 +96,7 @@ pub use scheduler::{detect_stuck_agents, fire_due_scheduled_events, stuck_cancel
|
|
|
96
96
|
pub use selftest::{evaluate_idle_behavior, run_comms_selftest, CommsSelftestDriver};
|
|
97
97
|
pub use send::{
|
|
98
98
|
apply_worker_sender_bypass, send_message, session_drift_refusal, MessageTarget, SendOptions,
|
|
99
|
+
TrustedSender,
|
|
99
100
|
};
|
|
100
101
|
pub use trust::{attempt_trust_auto_answer, TrustAnswerOutcome};
|
|
101
102
|
pub use types::{
|
|
@@ -5,7 +5,7 @@ use std::path::Path;
|
|
|
5
5
|
use crate::coordinator::{CoordinatorHealthStatus, WorkspacePath};
|
|
6
6
|
use crate::event_log::EventLog;
|
|
7
7
|
use crate::model::enums::PaneLiveness;
|
|
8
|
-
use crate::model::ids::{TaskId, TeamKey};
|
|
8
|
+
use crate::model::ids::{AgentId, TaskId, TeamKey};
|
|
9
9
|
use crate::transport::{PaneId, Transport};
|
|
10
10
|
|
|
11
11
|
use super::helpers::{status_wire, MessageStatusShadow};
|
|
@@ -23,6 +23,29 @@ pub enum MessageTarget {
|
|
|
23
23
|
Fanout(Vec<String>),
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/// Sender identity captured from a framework-owned runtime context.
|
|
27
|
+
///
|
|
28
|
+
/// Public CLI/MCP inputs never construct this value from a caller-supplied
|
|
29
|
+
/// string. The wrapper keeps that trust boundary visible throughout delivery
|
|
30
|
+
/// instead of degrading the identity back to an untyped option field.
|
|
31
|
+
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
|
32
|
+
#[serde(transparent)]
|
|
33
|
+
pub struct TrustedSender(AgentId);
|
|
34
|
+
|
|
35
|
+
impl TrustedSender {
|
|
36
|
+
pub fn from_runtime_identity(agent_id: AgentId) -> Self {
|
|
37
|
+
Self(agent_id)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub fn leader() -> Self {
|
|
41
|
+
Self(AgentId::new("leader"))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn as_str(&self) -> &str {
|
|
45
|
+
self.0.as_str()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
/// `send_message` 选项 (`send.py:36`:Python 大量默认参数 → typed 选项 struct)。
|
|
27
50
|
#[derive(Debug, Clone)]
|
|
28
51
|
pub struct SendOptions {
|
|
@@ -31,7 +54,7 @@ pub struct SendOptions {
|
|
|
31
54
|
/// `task_id` 当真任务校验/路由。**投递/fanout/internal/coordinator** 路径传 `false`
|
|
32
55
|
/// (`internal_delivery.py:44`、`send.py:412/481`),此时 `task_id` 只是标签,**不校验 state.tasks**。
|
|
33
56
|
pub route_task_id: bool,
|
|
34
|
-
pub sender:
|
|
57
|
+
pub sender: TrustedSender,
|
|
35
58
|
pub requires_ack: bool,
|
|
36
59
|
pub confirm_human: bool,
|
|
37
60
|
pub wait_visible: bool,
|
|
@@ -52,7 +75,7 @@ impl Default for SendOptions {
|
|
|
52
75
|
Self {
|
|
53
76
|
task_id: None,
|
|
54
77
|
route_task_id: true,
|
|
55
|
-
sender:
|
|
78
|
+
sender: TrustedSender::leader(),
|
|
56
79
|
requires_ack: true,
|
|
57
80
|
confirm_human: false,
|
|
58
81
|
wait_visible: true,
|
|
@@ -95,7 +118,7 @@ pub fn send_message(
|
|
|
95
118
|
"leader",
|
|
96
119
|
content,
|
|
97
120
|
opts.task_id.as_ref(),
|
|
98
|
-
|
|
121
|
+
opts.sender.as_str(),
|
|
99
122
|
opts.requires_ack,
|
|
100
123
|
None,
|
|
101
124
|
opts.message_id.as_deref(),
|
|
@@ -119,7 +142,7 @@ pub fn send_message(
|
|
|
119
142
|
}
|
|
120
143
|
MessageTarget::Single(target) => target,
|
|
121
144
|
MessageTarget::Broadcast => {
|
|
122
|
-
let recipients = broadcast_recipients(&state,
|
|
145
|
+
let recipients = broadcast_recipients(&state, opts.sender.as_str(), opts.team.as_ref());
|
|
123
146
|
return fanout_send(
|
|
124
147
|
workspace,
|
|
125
148
|
&state,
|
|
@@ -163,13 +186,13 @@ pub fn send_message(
|
|
|
163
186
|
&state,
|
|
164
187
|
recipient,
|
|
165
188
|
"leader",
|
|
166
|
-
|
|
189
|
+
opts.sender.as_str(),
|
|
167
190
|
opts.task_id.as_ref(),
|
|
168
191
|
&event_log,
|
|
169
192
|
)? {
|
|
170
193
|
return Ok(outcome);
|
|
171
194
|
}
|
|
172
|
-
if let Some(outcome) = send_owner_gate_refusal(workspace, &state,
|
|
195
|
+
if let Some(outcome) = send_owner_gate_refusal(workspace, &state, opts.sender.as_str())? {
|
|
173
196
|
return Ok(outcome);
|
|
174
197
|
}
|
|
175
198
|
if opts.route_task_id {
|
|
@@ -206,7 +229,7 @@ pub fn send_message(
|
|
|
206
229
|
store.create_message_with_id(
|
|
207
230
|
requested,
|
|
208
231
|
task_id,
|
|
209
|
-
|
|
232
|
+
opts.sender.as_str(),
|
|
210
233
|
recipient,
|
|
211
234
|
content,
|
|
212
235
|
None,
|
|
@@ -216,7 +239,7 @@ pub fn send_message(
|
|
|
216
239
|
} else {
|
|
217
240
|
store.create_message(
|
|
218
241
|
task_id,
|
|
219
|
-
|
|
242
|
+
opts.sender.as_str(),
|
|
220
243
|
recipient,
|
|
221
244
|
content,
|
|
222
245
|
None,
|
|
@@ -690,7 +713,7 @@ fn fanout_send(
|
|
|
690
713
|
let mut delivered_count = 0usize;
|
|
691
714
|
let mut attempted_count = 0usize;
|
|
692
715
|
for recipient in recipients {
|
|
693
|
-
if recipient.is_empty() || recipient ==
|
|
716
|
+
if recipient.is_empty() || recipient == opts.sender.as_str() {
|
|
694
717
|
continue;
|
|
695
718
|
}
|
|
696
719
|
attempted_count = attempted_count.saturating_add(1);
|
|
@@ -701,7 +724,7 @@ fn fanout_send(
|
|
|
701
724
|
recipient,
|
|
702
725
|
content,
|
|
703
726
|
opts.task_id.as_ref(),
|
|
704
|
-
|
|
727
|
+
opts.sender.as_str(),
|
|
705
728
|
opts.requires_ack,
|
|
706
729
|
None,
|
|
707
730
|
event_log,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.50",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.50",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.50",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.50"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|