@team-agent/installer 0.5.49 → 0.5.51
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 +6 -4
- package/crates/team-agent/src/cli/emit.rs +91 -39
- package/crates/team-agent/src/cli/mod.rs +33 -15
- package/crates/team-agent/src/cli/named_address.rs +82 -53
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +103 -1308
- package/crates/team-agent/src/cli/spec.rs +2 -2
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- 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 +2 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
- package/crates/team-agent/src/cli/types.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
- package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +2 -2
- package/crates/team-agent/src/mcp_server/tests/send.rs +22 -15
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
- package/crates/team-agent/src/mcp_server/tools.rs +26 -15
- package/crates/team-agent/src/mcp_server/wire.rs +2 -18
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +165 -39
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +11 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +133 -58
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +2 -1
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +59 -16
- package/package.json +4 -4
|
@@ -3,6 +3,29 @@ use crate::transport::test_support::OfflineTransport;
|
|
|
3
3
|
use serde_json::json;
|
|
4
4
|
use serial_test::serial;
|
|
5
5
|
|
|
6
|
+
fn launch_source() -> String {
|
|
7
|
+
let src = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/lifecycle");
|
|
8
|
+
let mut files = vec![src.join("launch.rs")];
|
|
9
|
+
let mut siblings = std::fs::read_dir(src.join("launch"))
|
|
10
|
+
.expect("read launch module directory")
|
|
11
|
+
.map(|entry| entry.expect("read launch module entry").path())
|
|
12
|
+
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("rs"))
|
|
13
|
+
.collect::<Vec<_>>();
|
|
14
|
+
siblings.sort();
|
|
15
|
+
files.extend(siblings);
|
|
16
|
+
|
|
17
|
+
files
|
|
18
|
+
.into_iter()
|
|
19
|
+
.map(|path| {
|
|
20
|
+
format!(
|
|
21
|
+
"\n// @source {}\n{}",
|
|
22
|
+
path.strip_prefix(&src).unwrap().display(),
|
|
23
|
+
std::fs::read_to_string(&path).expect("read launch source")
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
.collect()
|
|
27
|
+
}
|
|
28
|
+
|
|
6
29
|
#[allow(dead_code)]
|
|
7
30
|
struct HermeticTestEnv;
|
|
8
31
|
|
|
@@ -968,7 +991,7 @@ fn e5_add_agent_resolves_team_dir_to_role_dir_when_runtime_spec_exists() {
|
|
|
968
991
|
// never write team.spec.yaml into the user team_dir/agents_dir. Pins the spec-demote invariant.
|
|
969
992
|
#[test]
|
|
970
993
|
fn e5_guard_g1_writers_use_runtime_spec_path_not_user_dir() {
|
|
971
|
-
let source =
|
|
994
|
+
let source = launch_source();
|
|
972
995
|
let runtime_writes = source.matches("runtime_spec_path(").count();
|
|
973
996
|
assert!(
|
|
974
997
|
runtime_writes >= 2,
|
|
@@ -989,7 +1012,7 @@ fn e5_guard_g1_writers_use_runtime_spec_path_not_user_dir() {
|
|
|
989
1012
|
// Pins the Bug1 fix: no `fs::copy` of a role file + no `materialize_added_role_file` reborn.
|
|
990
1013
|
#[test]
|
|
991
1014
|
fn e5_guard_g2_no_copy_role_into_platform_dir() {
|
|
992
|
-
let source =
|
|
1015
|
+
let source = launch_source();
|
|
993
1016
|
assert!(
|
|
994
1017
|
!source.contains("materialize_added_role_file"),
|
|
995
1018
|
"G2: materialize_added_role_file (role copy anti-pattern) must stay deleted"
|
|
@@ -3117,6 +3140,36 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
3117
3140
|
assert_eq!(spawn_event["spawned_at"], json!(FIXED_SPAWNED_AT));
|
|
3118
3141
|
assert_eq!(spawn_event["source"], json!("launch"));
|
|
3119
3142
|
assert_eq!(spawn_event["spawn_epoch"], json!(0));
|
|
3143
|
+
let actual_keys = spawn_event
|
|
3144
|
+
.as_object()
|
|
3145
|
+
.expect("spawn event object")
|
|
3146
|
+
.keys()
|
|
3147
|
+
.map(String::as_str)
|
|
3148
|
+
.collect::<std::collections::BTreeSet<_>>();
|
|
3149
|
+
let expected_keys = [
|
|
3150
|
+
"agent_id",
|
|
3151
|
+
"argv",
|
|
3152
|
+
"env_overlay_keys",
|
|
3153
|
+
"env_unset",
|
|
3154
|
+
"event",
|
|
3155
|
+
"expected_session_id",
|
|
3156
|
+
"provider",
|
|
3157
|
+
"session_id_in_argv",
|
|
3158
|
+
"source",
|
|
3159
|
+
"spawn_cwd",
|
|
3160
|
+
"spawn_epoch",
|
|
3161
|
+
"spawned_at",
|
|
3162
|
+
"tmux_endpoint",
|
|
3163
|
+
"tmux_endpoint_source",
|
|
3164
|
+
"tmux_start_mode",
|
|
3165
|
+
"ts",
|
|
3166
|
+
]
|
|
3167
|
+
.into_iter()
|
|
3168
|
+
.collect::<std::collections::BTreeSet<_>>();
|
|
3169
|
+
assert_eq!(
|
|
3170
|
+
actual_keys, expected_keys,
|
|
3171
|
+
"spawn event schema must be canonical"
|
|
3172
|
+
);
|
|
3120
3173
|
}
|
|
3121
3174
|
|
|
3122
3175
|
// Stage B2 — golden launch/core.py:171-173 writes paused workers as exactly
|
|
@@ -233,7 +233,7 @@ fn r2_lifecycle_lock_exists_precondition() {
|
|
|
233
233
|
"remove rollback runs while remove-agent holds the lifecycle lock; it must use lock-free start_agent_at_paths"
|
|
234
234
|
);
|
|
235
235
|
|
|
236
|
-
let launch =
|
|
236
|
+
let launch = launch_source();
|
|
237
237
|
assert_public_operation(&launch, "add-agent");
|
|
238
238
|
assert_public_operation(&launch, "fork-agent");
|
|
239
239
|
assert_body_is_unlocked(&launch, "fn add_agent_with_transport_at_paths");
|
|
@@ -279,6 +279,29 @@ fn read_src(path: &str) -> String {
|
|
|
279
279
|
std::fs::read_to_string(manifest_src().join(path)).unwrap()
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
fn launch_source() -> String {
|
|
283
|
+
let src = manifest_src().join("lifecycle");
|
|
284
|
+
let mut files = vec![src.join("launch.rs")];
|
|
285
|
+
let mut siblings = std::fs::read_dir(src.join("launch"))
|
|
286
|
+
.expect("read launch module directory")
|
|
287
|
+
.map(|entry| entry.expect("read launch module entry").path())
|
|
288
|
+
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("rs"))
|
|
289
|
+
.collect::<Vec<_>>();
|
|
290
|
+
siblings.sort();
|
|
291
|
+
files.extend(siblings);
|
|
292
|
+
|
|
293
|
+
files
|
|
294
|
+
.into_iter()
|
|
295
|
+
.map(|path| {
|
|
296
|
+
format!(
|
|
297
|
+
"\n// @source {}\n{}",
|
|
298
|
+
path.strip_prefix(&src).unwrap().display(),
|
|
299
|
+
std::fs::read_to_string(&path).expect("read launch source")
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
.collect()
|
|
303
|
+
}
|
|
304
|
+
|
|
282
305
|
fn source_files(root: &Path) -> Vec<PathBuf> {
|
|
283
306
|
let mut out = Vec::new();
|
|
284
307
|
collect_rs_files(root, &mut out);
|
|
@@ -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,
|
|
@@ -252,12 +252,28 @@ fn state_spec_workspace_from_entry(state: &Value) -> Option<PathBuf> {
|
|
|
252
252
|
|
|
253
253
|
fn load_local_runtime_state(workspace: &Path) -> Result<Value, super::super::ToolError> {
|
|
254
254
|
let path = crate::state::persist::runtime_state_path(workspace);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
tool_runtime_error(format!(
|
|
260
|
-
|
|
255
|
+
match crate::state::repository::StateRepository::new(workspace)
|
|
256
|
+
.load_workspace_if_exists_without_migrations()
|
|
257
|
+
{
|
|
258
|
+
Ok(Some(state)) => Ok(state),
|
|
259
|
+
Ok(None) => Err(tool_runtime_error(format!(
|
|
260
|
+
"read local runtime state {}: {}",
|
|
261
|
+
path.display(),
|
|
262
|
+
std::io::Error::from(std::io::ErrorKind::NotFound)
|
|
263
|
+
))),
|
|
264
|
+
Err(crate::state::StateError::Json(error)) => Err(tool_runtime_error(format!(
|
|
265
|
+
"parse local runtime state {}: {error}",
|
|
266
|
+
path.display()
|
|
267
|
+
))),
|
|
268
|
+
Err(crate::state::StateError::Io(error)) => Err(tool_runtime_error(format!(
|
|
269
|
+
"read local runtime state {}: {error}",
|
|
270
|
+
path.display()
|
|
271
|
+
))),
|
|
272
|
+
Err(error) => Err(tool_runtime_error(format!(
|
|
273
|
+
"read local runtime state {}: {error}",
|
|
274
|
+
path.display()
|
|
275
|
+
))),
|
|
276
|
+
}
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
fn materialize_mcp_lifecycle_spec(
|
|
@@ -416,7 +432,14 @@ fn prepare_selected_team_state(
|
|
|
416
432
|
}
|
|
417
433
|
}
|
|
418
434
|
}
|
|
419
|
-
crate::state::
|
|
435
|
+
crate::state::repository::StateRepository::new(workspace)
|
|
436
|
+
.save(
|
|
437
|
+
crate::state::repository::StateWriteIntent::McpLifecycleAgentOps {
|
|
438
|
+
team_key: Some(team),
|
|
439
|
+
},
|
|
440
|
+
state,
|
|
441
|
+
)
|
|
442
|
+
.map_err(|e| {
|
|
420
443
|
tool_runtime_error(format!(
|
|
421
444
|
"save MCP lifecycle scoped state {}: {e}",
|
|
422
445
|
workspace.display()
|
|
@@ -26,8 +26,10 @@ pub(crate) fn update_state(
|
|
|
26
26
|
let mut state = selected.state;
|
|
27
27
|
ensure_object(&mut state);
|
|
28
28
|
append_note(&mut state, note);
|
|
29
|
-
crate::state::
|
|
30
|
-
|
|
29
|
+
crate::state::repository::StateRepository::new(&selected.run_workspace).save_reapplying(
|
|
30
|
+
crate::state::repository::StateWriteIntent::McpUpdateStateNote {
|
|
31
|
+
team_key: Some(&selected.team_key),
|
|
32
|
+
},
|
|
31
33
|
&state,
|
|
32
34
|
|latest| {
|
|
33
35
|
ensure_object(latest);
|
|
@@ -66,8 +68,10 @@ fn update_state_without_spec(
|
|
|
66
68
|
ensure_object(&mut state);
|
|
67
69
|
seed_legacy_team_key(&mut state, &selected.run_workspace, &selected.team_key);
|
|
68
70
|
append_note(&mut state, note);
|
|
69
|
-
crate::state::
|
|
70
|
-
|
|
71
|
+
crate::state::repository::StateRepository::new(&selected.run_workspace).save_reapplying(
|
|
72
|
+
crate::state::repository::StateWriteIntent::McpUpdateStateNote {
|
|
73
|
+
team_key: Some(&selected.team_key),
|
|
74
|
+
},
|
|
71
75
|
&state,
|
|
72
76
|
|latest| {
|
|
73
77
|
ensure_object(latest);
|
|
@@ -85,10 +85,10 @@ use crate::event_log::EventLog;
|
|
|
85
85
|
use crate::message_store::MessageStore;
|
|
86
86
|
|
|
87
87
|
// ── REUSE: step 5 state persist / projection ────────────────────────────────
|
|
88
|
-
use crate::state::persist::
|
|
88
|
+
use crate::state::persist::load_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,22 +318,17 @@ fn send_message_worker_recipient_surfaces_dead_coordinator_warning() {
|
|
|
304
318
|
None,
|
|
305
319
|
None,
|
|
306
320
|
None,
|
|
307
|
-
None,
|
|
308
321
|
)
|
|
309
|
-
.expect("send
|
|
322
|
+
.expect("send persists a durable blocker, not an MCP error");
|
|
310
323
|
let v = outcome.to_value();
|
|
311
|
-
assert_eq!(v.get("status"), Some(&json!("
|
|
312
|
-
assert_eq!(v.get("reason"), Some(&json!("coordinator_unavailable")));
|
|
324
|
+
assert_eq!(v.get("status"), Some(&json!("accepted")));
|
|
313
325
|
assert!(
|
|
314
|
-
v.get("
|
|
326
|
+
v.get("message_id")
|
|
315
327
|
.and_then(Value::as_str)
|
|
316
|
-
.is_some_and(|
|
|
317
|
-
"
|
|
318
|
-
);
|
|
319
|
-
assert!(
|
|
320
|
-
v.get("delivery_pending").is_none(),
|
|
321
|
-
"dead coordinator must not return the old accepted async envelope"
|
|
328
|
+
.is_some_and(|id| id.starts_with("msg_")),
|
|
329
|
+
"worker MCP send must expose the durable row id; value={v}"
|
|
322
330
|
);
|
|
331
|
+
assert_eq!(v.get("delivery_pending"), Some(&json!(true)));
|
|
323
332
|
}
|
|
324
333
|
|
|
325
334
|
#[test]
|
|
@@ -336,7 +345,6 @@ fn send_message_leader_recipient_is_direct_not_accepted() {
|
|
|
336
345
|
None,
|
|
337
346
|
None,
|
|
338
347
|
None,
|
|
339
|
-
None,
|
|
340
348
|
)
|
|
341
349
|
.expect("leader send ok");
|
|
342
350
|
assert!(
|
|
@@ -435,7 +443,6 @@ fn send_message_cross_team_peer_surfaces_peer_not_in_scope_error() {
|
|
|
435
443
|
None,
|
|
436
444
|
None,
|
|
437
445
|
None,
|
|
438
|
-
None,
|
|
439
446
|
)
|
|
440
447
|
.expect_err("out-of-scope peer must be refused");
|
|
441
448
|
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,
|
|
@@ -126,11 +126,19 @@ impl TeamOrchestratorTools {
|
|
|
126
126
|
.map(|team| team.as_str().to_string())
|
|
127
127
|
.or_else(|| assignment_team_key(&state));
|
|
128
128
|
reconcile_assigned_task(&mut state, team_key.as_deref(), &task_value);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
crate::state::repository::StateRepository::new(&self.workspace)
|
|
130
|
+
.save_reapplying(
|
|
131
|
+
crate::state::repository::StateWriteIntent::McpAssignTask {
|
|
132
|
+
team_key: team_key.as_deref(),
|
|
133
|
+
task_id,
|
|
134
|
+
},
|
|
135
|
+
&state,
|
|
136
|
+
|latest| {
|
|
137
|
+
ensure_object(latest);
|
|
138
|
+
let latest_team_key = team_key.clone().or_else(|| assignment_team_key(latest));
|
|
139
|
+
reconcile_assigned_task(latest, latest_team_key.as_deref(), &task_value);
|
|
140
|
+
},
|
|
141
|
+
)
|
|
134
142
|
.map_err(tool_runtime_error)?;
|
|
135
143
|
|
|
136
144
|
let content = assignment_message(task, message);
|
|
@@ -140,7 +148,6 @@ impl TeamOrchestratorTools {
|
|
|
140
148
|
Some(task_id),
|
|
141
149
|
None,
|
|
142
150
|
None,
|
|
143
|
-
None,
|
|
144
151
|
)?;
|
|
145
152
|
let mut ok = compact_tool_result(&out.to_value())?;
|
|
146
153
|
if recovery {
|
|
@@ -155,7 +162,7 @@ impl TeamOrchestratorTools {
|
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
/// `send_message` (`tools.py:135-183`): C14/C15/C17 scope resolution.
|
|
158
|
-
/// - sender =
|
|
165
|
+
/// - sender = immutable `TEAM_AGENT_ID` captured when the MCP server starts.
|
|
159
166
|
/// - `requires_ack` defaults from target (`_requires_ack_for_target`).
|
|
160
167
|
/// - C23 cross-team pre-refusal ([`Self::refuse_cross_team_peer`]) before any
|
|
161
168
|
/// runtime call.
|
|
@@ -168,7 +175,6 @@ impl TeamOrchestratorTools {
|
|
|
168
175
|
to: &MessageTarget,
|
|
169
176
|
content: &str,
|
|
170
177
|
task_id: Option<&str>,
|
|
171
|
-
sender: Option<&str>,
|
|
172
178
|
requires_ack: Option<bool>,
|
|
173
179
|
scope_override: Option<Scope>,
|
|
174
180
|
) -> Result<SendOutcome, ToolError> {
|
|
@@ -183,10 +189,14 @@ impl TeamOrchestratorTools {
|
|
|
183
189
|
if let Some(err) = self.refuse_cross_team_peer(to, None) {
|
|
184
190
|
return Err(err);
|
|
185
191
|
}
|
|
186
|
-
let sender =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
let sender = self.agent_id.clone().ok_or_else(|| {
|
|
193
|
+
ToolError::new(
|
|
194
|
+
ToolErrorReason::McpScopeRefused,
|
|
195
|
+
"send_message requires framework-injected TEAM_AGENT_ID",
|
|
196
|
+
"IdentityError",
|
|
197
|
+
)
|
|
198
|
+
})?;
|
|
199
|
+
let sender = TrustedSender::from_runtime_identity(sender);
|
|
190
200
|
let ack = requires_ack.unwrap_or_else(|| requires_ack_for_target(to));
|
|
191
201
|
// C14/C15/C17 scope audit (#230 I-2/I-6 contract): emit mcp.scope_resolved
|
|
192
202
|
// for every worker-origin send before any routing/delivery — the funnel
|
|
@@ -197,7 +207,7 @@ impl TeamOrchestratorTools {
|
|
|
197
207
|
"mcp.scope_resolved",
|
|
198
208
|
serde_json::json!({
|
|
199
209
|
"tool": "send_message",
|
|
200
|
-
"sender": sender,
|
|
210
|
+
"sender": sender.as_str(),
|
|
201
211
|
"owner_team_id": canonical_owner_team.as_ref().map(TeamKey::as_str),
|
|
202
212
|
"to": match to {
|
|
203
213
|
MessageTarget::Single(t) => serde_json::Value::String(t.clone()),
|
|
@@ -211,9 +221,10 @@ impl TeamOrchestratorTools {
|
|
|
211
221
|
)
|
|
212
222
|
.map_err(tool_runtime_error)?;
|
|
213
223
|
let opts = SendOptions {
|
|
224
|
+
origin: crate::messaging::SendOrigin::Mcp,
|
|
214
225
|
task_id: task_id.map(TaskId::new),
|
|
215
226
|
route_task_id: true,
|
|
216
|
-
sender
|
|
227
|
+
sender,
|
|
217
228
|
requires_ack: ack,
|
|
218
229
|
team: canonical_owner_team,
|
|
219
230
|
..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 {
|
|
@@ -80,8 +80,10 @@ pub fn detect_idle_fallbacks(
|
|
|
80
80
|
let suppression_snapshots =
|
|
81
81
|
idle_fallback_suppression_snapshots(&next_state, store, &team, &idle_workers)?;
|
|
82
82
|
register_idle_fallback_suppression(&mut next_state, &team, &now, &suppression_snapshots);
|
|
83
|
-
crate::state::
|
|
84
|
-
|
|
83
|
+
crate::state::repository::StateRepository::new(workspace).save_reapplying(
|
|
84
|
+
crate::state::repository::StateWriteIntent::MessagingTurnArm {
|
|
85
|
+
owner_team_id: Some(&team),
|
|
86
|
+
},
|
|
85
87
|
&next_state,
|
|
86
88
|
|latest| {
|
|
87
89
|
register_idle_fallback_suppression(latest, &team, &now, &suppression_snapshots);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//! Pure logical-address grammar shared by public entry-point adapters.
|
|
2
|
+
|
|
3
|
+
use std::path::PathBuf;
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
6
|
+
pub struct LogicalAddress {
|
|
7
|
+
pub workspace: Option<PathBuf>,
|
|
8
|
+
pub target: LogicalAddressTarget,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
12
|
+
pub enum LogicalAddressTarget {
|
|
13
|
+
Worker(String),
|
|
14
|
+
TeamEntity { team: String, entity: String },
|
|
15
|
+
SessionWindow { session: String, window: String },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn parse_logical_address(raw_name: &str) -> Result<LogicalAddress, &'static str> {
|
|
19
|
+
let raw = raw_name.trim();
|
|
20
|
+
if raw.is_empty() {
|
|
21
|
+
return Err("name is empty");
|
|
22
|
+
}
|
|
23
|
+
let (workspace, name) = if let Some((workspace, rest)) = raw.split_once("::") {
|
|
24
|
+
if workspace.trim().is_empty() || rest.trim().is_empty() {
|
|
25
|
+
return Err("workspace-qualified name must include workspace and target");
|
|
26
|
+
}
|
|
27
|
+
(Some(PathBuf::from(workspace)), rest.trim())
|
|
28
|
+
} else {
|
|
29
|
+
(None, raw)
|
|
30
|
+
};
|
|
31
|
+
if name.contains("//") {
|
|
32
|
+
return Err("name contains an empty path segment");
|
|
33
|
+
}
|
|
34
|
+
let target = if name.contains('/') {
|
|
35
|
+
let parts = name.split('/').collect::<Vec<_>>();
|
|
36
|
+
if parts.len() != 2 || !valid_component(parts[0]) || !valid_component(parts[1]) {
|
|
37
|
+
return Err("expected <team>/<agent> or <team>/leader");
|
|
38
|
+
}
|
|
39
|
+
LogicalAddressTarget::TeamEntity {
|
|
40
|
+
team: parts[0].to_string(),
|
|
41
|
+
entity: parts[1].to_string(),
|
|
42
|
+
}
|
|
43
|
+
} else if name.contains(':') {
|
|
44
|
+
let parts = name.split(':').collect::<Vec<_>>();
|
|
45
|
+
if parts.len() != 2 || parts[0].trim().is_empty() || parts[1].trim().is_empty() {
|
|
46
|
+
return Err("expected <session>:<window>");
|
|
47
|
+
}
|
|
48
|
+
LogicalAddressTarget::SessionWindow {
|
|
49
|
+
session: parts[0].to_string(),
|
|
50
|
+
window: parts[1].to_string(),
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
if !valid_component(name) {
|
|
54
|
+
return Err("expected a non-empty agent id");
|
|
55
|
+
}
|
|
56
|
+
LogicalAddressTarget::Worker(name.to_string())
|
|
57
|
+
};
|
|
58
|
+
Ok(LogicalAddress { workspace, target })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn valid_component(raw: &str) -> bool {
|
|
62
|
+
!raw.trim().is_empty()
|
|
63
|
+
&& !raw.contains(char::is_whitespace)
|
|
64
|
+
&& !raw.contains('/')
|
|
65
|
+
&& !raw.contains(':')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[cfg(test)]
|
|
69
|
+
mod tests {
|
|
70
|
+
use super::*;
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn parses_canonical_send_grammar_without_runtime_state() {
|
|
74
|
+
assert_eq!(
|
|
75
|
+
parse_logical_address("/tmp/ws::team/w1").unwrap(),
|
|
76
|
+
LogicalAddress {
|
|
77
|
+
workspace: Some(PathBuf::from("/tmp/ws")),
|
|
78
|
+
target: LogicalAddressTarget::TeamEntity {
|
|
79
|
+
team: "team".into(),
|
|
80
|
+
entity: "w1".into(),
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
assert!(parse_logical_address("team//w1").is_err());
|
|
85
|
+
}
|
|
86
|
+
}
|