@team-agent/installer 0.5.59 → 0.5.61
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 +44 -1
- package/crates/team-agent/src/cli/diagnose.rs +183 -0
- package/crates/team-agent/src/cli/emit.rs +63 -14
- package/crates/team-agent/src/cli/leader.rs +8 -1
- package/crates/team-agent/src/cli/mod.rs +263 -5
- package/crates/team-agent/src/cli/named_address.rs +18 -1
- package/crates/team-agent/src/cli/send/presentation.rs +2 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/status_port/store.rs +7 -3
- package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
- package/crates/team-agent/src/cli/types.rs +33 -0
- package/crates/team-agent/src/communication_mode/mod.rs +53 -0
- package/crates/team-agent/src/compiler/tests.rs +5 -5
- package/crates/team-agent/src/compiler.rs +53 -1
- package/crates/team-agent/src/coordinator/tick.rs +21 -8
- package/crates/team-agent/src/db/message_store.rs +45 -1
- package/crates/team-agent/src/fake_worker.rs +21 -1
- package/crates/team-agent/src/leader/start.rs +732 -94
- package/crates/team-agent/src/leader/tests/identity.rs +64 -57
- package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +58 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +57 -51
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +148 -8
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +35 -1
- package/crates/team-agent/src/lifecycle/types.rs +1 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
- package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +100 -66
- package/crates/team-agent/src/mcp_server/types.rs +5 -0
- package/crates/team-agent/src/mcp_server/wire.rs +27 -21
- package/crates/team-agent/src/messaging/delivery.rs +105 -38
- package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
- package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
- package/crates/team-agent/src/messaging/presentation.rs +109 -0
- package/crates/team-agent/src/messaging/results.rs +165 -17
- package/crates/team-agent/src/messaging/send.rs +94 -81
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
- package/crates/team-agent/src/messaging/types.rs +5 -0
- package/crates/team-agent/src/messaging/watchers.rs +5 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
- package/crates/team-agent/src/model/spec.rs +16 -0
- package/crates/team-agent/src/provider/adapter.rs +57 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +10 -2
- package/crates/team-agent/src/provider/session/capture.rs +69 -34
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +386 -9
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +3 -14
- package/crates/team-agent/src/provider/session/context_fork.rs +7 -2
- package/crates/team-agent/src/provider/session/mod.rs +3 -2
- package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
- package/crates/team-agent/src/provider/session_scan/codex.rs +28 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +34 -8
- package/crates/team-agent/src/provider/session_scan.rs +8 -0
- package/crates/team-agent/src/topology.rs +3 -0
- package/package.json +4 -4
- package/skills/team-agent/command-coverage.json +379 -0
|
@@ -9,7 +9,6 @@ use crate::event_log::EventLog;
|
|
|
9
9
|
use crate::messaging::MessageTarget;
|
|
10
10
|
|
|
11
11
|
use super::helpers::{json_dumps_default, object_fields};
|
|
12
|
-
use super::normalize::normalize_result_status;
|
|
13
12
|
use super::tools::TeamOrchestratorTools;
|
|
14
13
|
use super::types::{
|
|
15
14
|
McpError, McpTool, RpcError, RpcId, RpcMethod, RpcResponse, Scope, SendOutcome,
|
|
@@ -369,7 +368,7 @@ fn python_repr(value: &str) -> String {
|
|
|
369
368
|
fn tool_contract(tool: McpTool) -> Value {
|
|
370
369
|
let (description, required) = match tool {
|
|
371
370
|
McpTool::SendMessage => (
|
|
372
|
-
"Send a message to a teammate, the leader, or '*' for all other team members.
|
|
371
|
+
"Send a message to a teammate, the leader, or '*' for all other team members. mailbox=true stores durably without live injection; the default is live delivery.",
|
|
373
372
|
vec!["to", "content"],
|
|
374
373
|
),
|
|
375
374
|
McpTool::AssignTask => ("Assign or update a task in the team graph and deliver it to its assignee.", vec!["task"]),
|
|
@@ -404,11 +403,7 @@ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
|
|
|
404
403
|
let mut properties = serde_json::Map::new();
|
|
405
404
|
match tool {
|
|
406
405
|
McpTool::AssignTask => {
|
|
407
|
-
insert_property(
|
|
408
|
-
&mut properties,
|
|
409
|
-
"task",
|
|
410
|
-
object_property("Task object to add or update."),
|
|
411
|
-
);
|
|
406
|
+
insert_property(&mut properties, "task", task_property());
|
|
412
407
|
insert_property(
|
|
413
408
|
&mut properties,
|
|
414
409
|
"message",
|
|
@@ -424,8 +419,10 @@ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
|
|
|
424
419
|
insert_property(&mut properties, "content", string_property("Message body."));
|
|
425
420
|
insert_property(
|
|
426
421
|
&mut properties,
|
|
427
|
-
"
|
|
428
|
-
|
|
422
|
+
"mailbox",
|
|
423
|
+
boolean_property(
|
|
424
|
+
"Set true to store durably without live injection; omit for default live delivery.",
|
|
425
|
+
),
|
|
429
426
|
);
|
|
430
427
|
}
|
|
431
428
|
McpTool::ReportResult => {
|
|
@@ -603,6 +600,25 @@ fn object_property(description: &str) -> Value {
|
|
|
603
600
|
serde_json::json!({"type": "object", "description": description, "additionalProperties": true})
|
|
604
601
|
}
|
|
605
602
|
|
|
603
|
+
fn task_property() -> Value {
|
|
604
|
+
serde_json::json!({
|
|
605
|
+
"type": "object",
|
|
606
|
+
"description": "Task object to add or update.",
|
|
607
|
+
"properties": {
|
|
608
|
+
"result_route": {
|
|
609
|
+
"type": "string",
|
|
610
|
+
"enum": crate::messaging::results::ResultRoute::ALL
|
|
611
|
+
.iter()
|
|
612
|
+
.map(|route| route.as_str())
|
|
613
|
+
.collect::<Vec<_>>(),
|
|
614
|
+
"default": crate::messaging::results::ResultRoute::default().as_str(),
|
|
615
|
+
"description": "Result destination chosen by the task assigner."
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
"additionalProperties": true
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
|
|
606
622
|
fn presentation_property(description: &str) -> Value {
|
|
607
623
|
serde_json::json!({
|
|
608
624
|
"type": "object",
|
|
@@ -646,6 +662,7 @@ pub(crate) fn dispatch_tool(
|
|
|
646
662
|
None,
|
|
647
663
|
None,
|
|
648
664
|
None,
|
|
665
|
+
args.get("mailbox"),
|
|
649
666
|
args.get("presentation"),
|
|
650
667
|
)?;
|
|
651
668
|
match outcome {
|
|
@@ -658,18 +675,7 @@ pub(crate) fn dispatch_tool(
|
|
|
658
675
|
McpTool::ReportResult => tools.report_result_with_presentation(
|
|
659
676
|
args.get("envelope"),
|
|
660
677
|
args.get("summary").and_then(Value::as_str),
|
|
661
|
-
|
|
662
|
-
// Partial and is OBSERVABLE at this ingestion boundary, never silent.
|
|
663
|
-
{
|
|
664
|
-
let (status, unknown) =
|
|
665
|
-
crate::mcp_server::normalize::normalize_result_status_observed(
|
|
666
|
-
args.get("status").and_then(Value::as_str),
|
|
667
|
-
);
|
|
668
|
-
if let Some(raw) = unknown {
|
|
669
|
-
tools.note_unknown_result_status(&raw);
|
|
670
|
-
}
|
|
671
|
-
status
|
|
672
|
-
},
|
|
678
|
+
args.get("status").and_then(Value::as_str),
|
|
673
679
|
args.get("changes")
|
|
674
680
|
.and_then(Value::as_array)
|
|
675
681
|
.map(Vec::as_slice),
|
|
@@ -220,7 +220,12 @@ pub fn deliver_pending_message(
|
|
|
220
220
|
}
|
|
221
221
|
_ => state,
|
|
222
222
|
};
|
|
223
|
-
if message.recipient == "leader"
|
|
223
|
+
if message.recipient == "leader"
|
|
224
|
+
&& matches!(
|
|
225
|
+
message.status.as_str(),
|
|
226
|
+
"submitted_pending_acceptance" | "injected_awaiting_receipt"
|
|
227
|
+
)
|
|
228
|
+
{
|
|
224
229
|
return observe_pending_leader_acceptance(store, event_log, state, message_id);
|
|
225
230
|
}
|
|
226
231
|
if message.recipient == "leader" && message.status == "queued_until_leader_attach" {
|
|
@@ -341,6 +346,12 @@ pub fn deliver_pending_message(
|
|
|
341
346
|
.get("status")
|
|
342
347
|
.and_then(serde_json::Value::as_str)
|
|
343
348
|
.unwrap_or("unbound");
|
|
349
|
+
let refusal = match &reason {
|
|
350
|
+
crate::messaging::LeaderChannelUnbound::PaneWorkspaceMismatch(_) => {
|
|
351
|
+
DeliveryRefusal::PaneWorkspaceMismatch
|
|
352
|
+
}
|
|
353
|
+
_ => DeliveryRefusal::LeaderNotAttached,
|
|
354
|
+
};
|
|
344
355
|
store.mark(message_id, "failed", Some("leader_not_attached"))?;
|
|
345
356
|
event_log.write(
|
|
346
357
|
"leader_receiver.delivery_blocked",
|
|
@@ -349,7 +360,7 @@ pub fn deliver_pending_message(
|
|
|
349
360
|
"sender": message.sender,
|
|
350
361
|
"reason": "leader_receiver_not_attached",
|
|
351
362
|
"error": "leader_not_attached",
|
|
352
|
-
"channel_reason":
|
|
363
|
+
"channel_reason": reason.reason_code(),
|
|
353
364
|
"channel": "rebind_required",
|
|
354
365
|
"action": "run team-agent attach-leader or team-agent takeover",
|
|
355
366
|
"status": receiver_status,
|
|
@@ -364,7 +375,7 @@ pub fn deliver_pending_message(
|
|
|
364
375
|
"run team-agent attach-leader or team-agent takeover".to_string(),
|
|
365
376
|
),
|
|
366
377
|
stage: None,
|
|
367
|
-
reason: Some(
|
|
378
|
+
reason: Some(refusal),
|
|
368
379
|
channel: Some("rebind_required".to_string()),
|
|
369
380
|
});
|
|
370
381
|
}
|
|
@@ -711,19 +722,30 @@ pub fn deliver_pending_message(
|
|
|
711
722
|
}
|
|
712
723
|
if is_leader_recipient {
|
|
713
724
|
store.record_delivery_submission(message_id, readback_verified)?;
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
"message_id": message_id,
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
message_id,
|
|
725
|
-
|
|
726
|
-
|
|
725
|
+
match observe_leader_receipt(state, message_id) {
|
|
726
|
+
LeaderReceiptObservation::SourceUnavailable => {
|
|
727
|
+
store.mark_injected_awaiting_receipt(message_id)?;
|
|
728
|
+
event_log.write(
|
|
729
|
+
"leader_receiver.receipt_source_unavailable",
|
|
730
|
+
serde_json::json!({"message_id": message_id}),
|
|
731
|
+
)?;
|
|
732
|
+
return Ok(leader_receipt_source_unavailable_outcome(message_id));
|
|
733
|
+
}
|
|
734
|
+
LeaderReceiptObservation::TokenAbsent => {
|
|
735
|
+
store.mark(message_id, "submitted_pending_acceptance", None)?;
|
|
736
|
+
event_log.write(
|
|
737
|
+
"leader_receiver.acceptance_pending",
|
|
738
|
+
serde_json::json!({
|
|
739
|
+
"message_id": message_id,
|
|
740
|
+
"reason": "provider_receipt_not_observed",
|
|
741
|
+
}),
|
|
742
|
+
)?;
|
|
743
|
+
return Ok(leader_acceptance_pending_outcome(
|
|
744
|
+
message_id,
|
|
745
|
+
"submitted_pending_acceptance",
|
|
746
|
+
));
|
|
747
|
+
}
|
|
748
|
+
LeaderReceiptObservation::TokenObserved => {}
|
|
727
749
|
}
|
|
728
750
|
store.mark_delivered_with_receipt(message_id)?;
|
|
729
751
|
} else {
|
|
@@ -2041,15 +2063,36 @@ fn leader_rollout_path(state: &serde_json::Value) -> Option<std::path::PathBuf>
|
|
|
2041
2063
|
.map(std::path::PathBuf::from)
|
|
2042
2064
|
}
|
|
2043
2065
|
|
|
2044
|
-
|
|
2066
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
2067
|
+
pub(crate) enum LeaderReceiptObservation {
|
|
2068
|
+
SourceUnavailable,
|
|
2069
|
+
TokenAbsent,
|
|
2070
|
+
TokenObserved,
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
pub(crate) fn observe_leader_receipt(
|
|
2074
|
+
state: &serde_json::Value,
|
|
2075
|
+
message_id: &str,
|
|
2076
|
+
) -> LeaderReceiptObservation {
|
|
2045
2077
|
let Some(path) = leader_rollout_path(state) else {
|
|
2046
|
-
return
|
|
2078
|
+
return LeaderReceiptObservation::SourceUnavailable;
|
|
2047
2079
|
};
|
|
2048
|
-
|
|
2080
|
+
let Ok(observed) = rollout_tail_contains_result(
|
|
2049
2081
|
&path,
|
|
2050
2082
|
&format!("[team-agent-token:{message_id}]"),
|
|
2051
2083
|
64 * 1024,
|
|
2052
|
-
)
|
|
2084
|
+
) else {
|
|
2085
|
+
return LeaderReceiptObservation::SourceUnavailable;
|
|
2086
|
+
};
|
|
2087
|
+
if observed {
|
|
2088
|
+
LeaderReceiptObservation::TokenObserved
|
|
2089
|
+
} else {
|
|
2090
|
+
LeaderReceiptObservation::TokenAbsent
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
pub(crate) fn leader_transcript_has_token(state: &serde_json::Value, message_id: &str) -> bool {
|
|
2095
|
+
observe_leader_receipt(state, message_id) == LeaderReceiptObservation::TokenObserved
|
|
2053
2096
|
}
|
|
2054
2097
|
|
|
2055
2098
|
fn leader_acceptance_pending_outcome(message_id: &str, status: &str) -> DeliveryOutcome {
|
|
@@ -2065,17 +2108,41 @@ fn leader_acceptance_pending_outcome(message_id: &str, status: &str) -> Delivery
|
|
|
2065
2108
|
}
|
|
2066
2109
|
}
|
|
2067
2110
|
|
|
2111
|
+
pub(crate) fn leader_receipt_source_unavailable_outcome(message_id: &str) -> DeliveryOutcome {
|
|
2112
|
+
DeliveryOutcome {
|
|
2113
|
+
ok: true,
|
|
2114
|
+
status: DeliveryStatus::Blocked,
|
|
2115
|
+
message_status: MessageStatusShadow("injected_awaiting_receipt".to_string()),
|
|
2116
|
+
message_id: Some(message_id.to_string()),
|
|
2117
|
+
verification: Some("leader_receipt_source_unavailable".to_string()),
|
|
2118
|
+
stage: Some(DeliveryStage::Submit),
|
|
2119
|
+
reason: None,
|
|
2120
|
+
channel: Some("leader_receipt_source_unavailable".to_string()),
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2068
2124
|
fn observe_pending_leader_acceptance(
|
|
2069
2125
|
store: &MessageStore,
|
|
2070
2126
|
event_log: &EventLog,
|
|
2071
2127
|
state: &serde_json::Value,
|
|
2072
2128
|
message_id: &str,
|
|
2073
2129
|
) -> Result<DeliveryOutcome, MessagingError> {
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
message_id
|
|
2077
|
-
|
|
2078
|
-
|
|
2130
|
+
match observe_leader_receipt(state, message_id) {
|
|
2131
|
+
LeaderReceiptObservation::SourceUnavailable => {
|
|
2132
|
+
store.mark_injected_awaiting_receipt(message_id)?;
|
|
2133
|
+
event_log.write(
|
|
2134
|
+
"leader_receiver.receipt_source_unavailable",
|
|
2135
|
+
serde_json::json!({"message_id": message_id}),
|
|
2136
|
+
)?;
|
|
2137
|
+
return Ok(leader_receipt_source_unavailable_outcome(message_id));
|
|
2138
|
+
}
|
|
2139
|
+
LeaderReceiptObservation::TokenAbsent => {
|
|
2140
|
+
return Ok(leader_acceptance_pending_outcome(
|
|
2141
|
+
message_id,
|
|
2142
|
+
"submitted_pending_acceptance",
|
|
2143
|
+
));
|
|
2144
|
+
}
|
|
2145
|
+
LeaderReceiptObservation::TokenObserved => {}
|
|
2079
2146
|
}
|
|
2080
2147
|
store.mark_delivered_with_receipt(message_id)?;
|
|
2081
2148
|
event_log.write(
|
|
@@ -2744,22 +2811,22 @@ fn claude_recipient_rollout(
|
|
|
2744
2811
|
/// Silent on read errors — callers treat missing/unreadable as "token not
|
|
2745
2812
|
/// present" which forces the unverified path.
|
|
2746
2813
|
fn rollout_tail_contains(path: &std::path::Path, needle: &str, tail_bytes: u64) -> bool {
|
|
2814
|
+
rollout_tail_contains_result(path, needle, tail_bytes).unwrap_or(false)
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
fn rollout_tail_contains_result(
|
|
2818
|
+
path: &std::path::Path,
|
|
2819
|
+
needle: &str,
|
|
2820
|
+
tail_bytes: u64,
|
|
2821
|
+
) -> std::io::Result<bool> {
|
|
2747
2822
|
use std::io::{Read, Seek, SeekFrom};
|
|
2748
|
-
let
|
|
2749
|
-
|
|
2750
|
-
};
|
|
2751
|
-
let Ok(metadata) = file.metadata() else {
|
|
2752
|
-
return false;
|
|
2753
|
-
};
|
|
2823
|
+
let mut file = std::fs::File::open(path)?;
|
|
2824
|
+
let metadata = file.metadata()?;
|
|
2754
2825
|
let len = metadata.len();
|
|
2755
2826
|
let start = len.saturating_sub(tail_bytes);
|
|
2756
|
-
|
|
2757
|
-
return false;
|
|
2758
|
-
}
|
|
2827
|
+
file.seek(SeekFrom::Start(start))?;
|
|
2759
2828
|
let mut buf = Vec::with_capacity(tail_bytes.min(len) as usize);
|
|
2760
|
-
|
|
2761
|
-
return false;
|
|
2762
|
-
}
|
|
2829
|
+
file.take(tail_bytes).read_to_end(&mut buf)?;
|
|
2763
2830
|
let haystack = String::from_utf8_lossy(&buf);
|
|
2764
|
-
haystack.contains(needle)
|
|
2831
|
+
Ok(haystack.contains(needle))
|
|
2765
2832
|
}
|
|
@@ -3,6 +3,7 @@ use std::path::Path;
|
|
|
3
3
|
use serde_json::Value;
|
|
4
4
|
|
|
5
5
|
use crate::codex_app_server::AppServerBinding;
|
|
6
|
+
use crate::model::pane_authority_refusal::PaneWorkspaceMismatchFacts;
|
|
6
7
|
use crate::transport::{PaneInfo, Transport};
|
|
7
8
|
|
|
8
9
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -26,7 +27,7 @@ pub enum LeaderChannelResolution {
|
|
|
26
27
|
ProbeFailed(String),
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
#[derive(Debug, Clone,
|
|
30
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
30
31
|
pub enum LeaderChannelUnbound {
|
|
31
32
|
ReceiverNotAttached,
|
|
32
33
|
TransportConflict,
|
|
@@ -34,10 +35,25 @@ pub enum LeaderChannelUnbound {
|
|
|
34
35
|
NonCanonicalTmuxSocket,
|
|
35
36
|
EndpointMismatch,
|
|
36
37
|
PaneNotLive,
|
|
37
|
-
PaneWorkspaceMismatch,
|
|
38
|
+
PaneWorkspaceMismatch(PaneWorkspaceMismatchFacts),
|
|
38
39
|
AppServerBindingInvalid,
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
impl LeaderChannelUnbound {
|
|
43
|
+
pub const fn reason_code(&self) -> &'static str {
|
|
44
|
+
match self {
|
|
45
|
+
Self::ReceiverNotAttached => "ReceiverNotAttached",
|
|
46
|
+
Self::TransportConflict => "TransportConflict",
|
|
47
|
+
Self::MissingPaneId => "MissingPaneId",
|
|
48
|
+
Self::NonCanonicalTmuxSocket => "NonCanonicalTmuxSocket",
|
|
49
|
+
Self::EndpointMismatch => "EndpointMismatch",
|
|
50
|
+
Self::PaneNotLive => "PaneNotLive",
|
|
51
|
+
Self::PaneWorkspaceMismatch(_) => "PaneWorkspaceMismatch",
|
|
52
|
+
Self::AppServerBindingInvalid => "AppServerBindingInvalid",
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
/// Resolve the canonical receiver's live physical channel without mutating
|
|
42
58
|
/// ownership. For direct tmux, `(absolute socket, pane id)` plus an available
|
|
43
59
|
/// pane cwd inside the target workspace is the authority. Session/window/tty/
|
|
@@ -79,11 +95,12 @@ pub fn resolve_live_leader_channel(
|
|
|
79
95
|
.get("tmux_socket")
|
|
80
96
|
.and_then(Value::as_str)
|
|
81
97
|
.filter(|socket| !socket.is_empty());
|
|
98
|
+
let transport_endpoint = transport.tmux_endpoint();
|
|
82
99
|
if tmux_socket.is_some_and(|socket| !std::path::Path::new(socket).is_absolute()) {
|
|
83
100
|
return LeaderChannelResolution::Unbound(LeaderChannelUnbound::NonCanonicalTmuxSocket);
|
|
84
101
|
}
|
|
85
102
|
if let Some(expected) = tmux_socket {
|
|
86
|
-
if
|
|
103
|
+
if transport_endpoint.as_deref() != Some(expected) {
|
|
87
104
|
return LeaderChannelResolution::Unbound(LeaderChannelUnbound::EndpointMismatch);
|
|
88
105
|
}
|
|
89
106
|
}
|
|
@@ -97,13 +114,19 @@ pub fn resolve_live_leader_channel(
|
|
|
97
114
|
else {
|
|
98
115
|
return LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneNotLive);
|
|
99
116
|
};
|
|
100
|
-
if observed
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
if let Some(observed_pane_workspace) = observed.current_path.clone() {
|
|
118
|
+
if !path_is_in_workspace(&observed_pane_workspace, workspace)
|
|
119
|
+
&& !explicit_claim_authority_matches(workspace, receiver, &observed)
|
|
120
|
+
{
|
|
121
|
+
return LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneWorkspaceMismatch(
|
|
122
|
+
PaneWorkspaceMismatchFacts {
|
|
123
|
+
requested_workspace: canonical_path(workspace),
|
|
124
|
+
observed_pane_id: observed.pane_id.as_str().to_string(),
|
|
125
|
+
observed_pane_workspace,
|
|
126
|
+
endpoint: transport_endpoint.unwrap_or_else(|| "default".to_string()),
|
|
127
|
+
},
|
|
128
|
+
));
|
|
129
|
+
}
|
|
107
130
|
}
|
|
108
131
|
let metadata_drift = receiver_metadata_drift(receiver, &observed);
|
|
109
132
|
LeaderChannelResolution::Live(LiveLeaderChannel::DirectTmux(DirectTmuxLeaderChannel {
|
|
@@ -397,29 +397,46 @@ pub fn deliver_to_leader_fallback_pane(
|
|
|
397
397
|
let readback_ok = super::delivery::pane_readback_verified(&report);
|
|
398
398
|
if submit_ok {
|
|
399
399
|
store.record_delivery_submission(message_id, readback_ok)?;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
400
|
+
match super::delivery::observe_leader_receipt(state, message_id) {
|
|
401
|
+
super::delivery::LeaderReceiptObservation::SourceUnavailable => {
|
|
402
|
+
store.mark_injected_awaiting_receipt(message_id)?;
|
|
403
|
+
event_log.write(
|
|
404
|
+
"leader_receiver.receipt_source_unavailable",
|
|
405
|
+
serde_json::json!({
|
|
406
|
+
"message_id": message_id,
|
|
407
|
+
"result_id": result_id,
|
|
408
|
+
"channel": "fallback_pane",
|
|
409
|
+
}),
|
|
410
|
+
)?;
|
|
411
|
+
return Ok(super::delivery::leader_receipt_source_unavailable_outcome(
|
|
412
|
+
message_id,
|
|
413
|
+
));
|
|
414
|
+
}
|
|
415
|
+
super::delivery::LeaderReceiptObservation::TokenAbsent => {
|
|
416
|
+
store.mark(message_id, "submitted_pending_acceptance", None)?;
|
|
417
|
+
event_log.write(
|
|
418
|
+
"leader_receiver.acceptance_pending",
|
|
419
|
+
serde_json::json!({
|
|
420
|
+
"message_id": message_id,
|
|
421
|
+
"result_id": result_id,
|
|
422
|
+
"reason": "provider_receipt_not_observed",
|
|
423
|
+
"channel": "fallback_pane",
|
|
424
|
+
}),
|
|
425
|
+
)?;
|
|
426
|
+
return Ok(DeliveryOutcome {
|
|
427
|
+
ok: true,
|
|
428
|
+
status: DeliveryStatus::RetryScheduled,
|
|
429
|
+
message_status: MessageStatusShadow(
|
|
430
|
+
"submitted_pending_acceptance".to_string(),
|
|
431
|
+
),
|
|
432
|
+
message_id: Some(message_id.to_string()),
|
|
433
|
+
verification: Some("provider_receipt_not_observed".to_string()),
|
|
434
|
+
stage: Some(DeliveryStage::Submit),
|
|
435
|
+
reason: None,
|
|
436
|
+
channel: Some("leader_acceptance_pending".to_string()),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
super::delivery::LeaderReceiptObservation::TokenObserved => {}
|
|
423
440
|
}
|
|
424
441
|
store.mark_delivered_with_receipt(message_id)?;
|
|
425
442
|
event_log.write(
|
|
@@ -98,6 +98,12 @@ pub struct PresentationDecision {
|
|
|
98
98
|
pub policy_version: String,
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
102
|
+
pub struct SendPresentationInput {
|
|
103
|
+
pub request: PresentationRequest,
|
|
104
|
+
pub deprecation: Option<String>,
|
|
105
|
+
}
|
|
106
|
+
|
|
101
107
|
impl Default for PresentationRequest {
|
|
102
108
|
fn default() -> Self {
|
|
103
109
|
Self {
|
|
@@ -177,6 +183,79 @@ pub fn normalize_report_presentation(
|
|
|
177
183
|
(request, None)
|
|
178
184
|
}
|
|
179
185
|
|
|
186
|
+
/// Normalize the one-bit send surface and the temporary S-011 compatibility
|
|
187
|
+
/// input into the existing presentation disposition primitive.
|
|
188
|
+
///
|
|
189
|
+
/// S-011 sensitive: if the compatibility period is ended by a hard-cut
|
|
190
|
+
/// decision, replace this table with one uniform legacy-input refusal.
|
|
191
|
+
pub fn normalize_send_presentation(
|
|
192
|
+
mailbox: Option<&Value>,
|
|
193
|
+
legacy_presentation: Option<&Value>,
|
|
194
|
+
) -> Result<SendPresentationInput, String> {
|
|
195
|
+
if mailbox.is_some() && legacy_presentation.is_some() {
|
|
196
|
+
return Err("mailbox_conflicts_with_deprecated_presentation".to_string());
|
|
197
|
+
}
|
|
198
|
+
if let Some(mailbox) = mailbox {
|
|
199
|
+
let Some(mailbox) = mailbox.as_bool() else {
|
|
200
|
+
return Err("mailbox_must_be_boolean".to_string());
|
|
201
|
+
};
|
|
202
|
+
return Ok(SendPresentationInput {
|
|
203
|
+
request: PresentationRequest {
|
|
204
|
+
sink: if mailbox {
|
|
205
|
+
PresentationSink::Casefile
|
|
206
|
+
} else {
|
|
207
|
+
PresentationSink::Leader
|
|
208
|
+
},
|
|
209
|
+
class: PresentationClass::Message,
|
|
210
|
+
case_id: None,
|
|
211
|
+
},
|
|
212
|
+
deprecation: None,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
let Some(legacy) = legacy_presentation else {
|
|
216
|
+
return Ok(SendPresentationInput {
|
|
217
|
+
request: PresentationRequest::default(),
|
|
218
|
+
deprecation: None,
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
let Some(object) = legacy.as_object() else {
|
|
222
|
+
return Err("malformed_presentation".to_string());
|
|
223
|
+
};
|
|
224
|
+
let Some(class) = object.get("class").and_then(Value::as_str) else {
|
|
225
|
+
return Err("missing_class".to_string());
|
|
226
|
+
};
|
|
227
|
+
let Some(class) = PresentationClass::parse(class) else {
|
|
228
|
+
return Err(format!("unknown_class:{class}"));
|
|
229
|
+
};
|
|
230
|
+
let sink = match class {
|
|
231
|
+
PresentationClass::Message | PresentationClass::Blocking | PresentationClass::Timeout => {
|
|
232
|
+
PresentationSink::Leader
|
|
233
|
+
}
|
|
234
|
+
PresentationClass::Progress => PresentationSink::Casefile,
|
|
235
|
+
PresentationClass::StageResult
|
|
236
|
+
| PresentationClass::StagePass
|
|
237
|
+
| PresentationClass::Bounce
|
|
238
|
+
| PresentationClass::FinalReview => {
|
|
239
|
+
return Err(format!(
|
|
240
|
+
"deprecated_message_class:{}_requires_result_route",
|
|
241
|
+
class.as_str()
|
|
242
|
+
));
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
Ok(SendPresentationInput {
|
|
246
|
+
request: PresentationRequest {
|
|
247
|
+
sink,
|
|
248
|
+
class: PresentationClass::Message,
|
|
249
|
+
case_id: None,
|
|
250
|
+
},
|
|
251
|
+
deprecation: Some(format!(
|
|
252
|
+
"message-class={} is deprecated; use mailbox={} instead",
|
|
253
|
+
class.as_str(),
|
|
254
|
+
sink != PresentationSink::Leader
|
|
255
|
+
)),
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
180
259
|
pub fn normalize_presentation(value: Option<&Value>) -> (PresentationRequest, Option<String>) {
|
|
181
260
|
let Some(value) = value else {
|
|
182
261
|
return (PresentationRequest::default(), None);
|
|
@@ -304,4 +383,34 @@ mod tests {
|
|
|
304
383
|
"send normalization remains unchanged"
|
|
305
384
|
);
|
|
306
385
|
}
|
|
386
|
+
|
|
387
|
+
#[test]
|
|
388
|
+
fn send_mailbox_and_s011_compatibility_map_to_one_bit() {
|
|
389
|
+
let mailbox = normalize_send_presentation(Some(&json!(true)), None).unwrap();
|
|
390
|
+
assert_eq!(mailbox.request.sink, PresentationSink::Casefile);
|
|
391
|
+
assert_eq!(mailbox.deprecation, None);
|
|
392
|
+
|
|
393
|
+
for (class, sink) in [
|
|
394
|
+
("message", PresentationSink::Leader),
|
|
395
|
+
("progress", PresentationSink::Casefile),
|
|
396
|
+
("blocking", PresentationSink::Leader),
|
|
397
|
+
("timeout", PresentationSink::Leader),
|
|
398
|
+
] {
|
|
399
|
+
let legacy = json!({"sink": "silent", "class": class});
|
|
400
|
+
let mapped = normalize_send_presentation(None, Some(&legacy)).unwrap();
|
|
401
|
+
assert_eq!(mapped.request.sink, sink, "{class}");
|
|
402
|
+
assert!(mapped.deprecation.is_some(), "{class}");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#[test]
|
|
407
|
+
fn s011_stage_classes_require_result_route() {
|
|
408
|
+
for class in ["stage_result", "stage_pass", "bounce", "final_review"] {
|
|
409
|
+
let legacy = json!({"sink": "leader", "class": class});
|
|
410
|
+
assert_eq!(
|
|
411
|
+
normalize_send_presentation(None, Some(&legacy)).unwrap_err(),
|
|
412
|
+
format!("deprecated_message_class:{class}_requires_result_route")
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
307
416
|
}
|