@team-agent/installer 0.5.117 → 0.5.119
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/mod.rs +20 -0
- package/crates/team-agent/src/db/message_store.rs +10 -3
- package/crates/team-agent/src/leader/start.rs +27 -47
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +2 -7
- package/crates/team-agent/src/messaging/delivery.rs +157 -136
- package/crates/team-agent/src/messaging/delivery_consumption_tests.rs +223 -0
- package/crates/team-agent/src/messaging/enter_rescue.rs +182 -154
- package/crates/team-agent/src/messaging/enter_rescue_tests.rs +344 -277
- package/crates/team-agent/src/messaging/mod.rs +1 -1
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +6 -5
- package/crates/team-agent/src/messaging/tests/spine.rs +27 -28
- package/crates/team-agent/src/tmux_backend/enter_rescue.rs +325 -0
- package/crates/team-agent/src/tmux_backend/enter_rescue_tests.rs +432 -0
- package/crates/team-agent/src/tmux_backend/native_input.rs +19 -0
- package/crates/team-agent/src/tmux_backend/native_input_tests.rs +141 -4
- package/crates/team-agent/src/tmux_backend.rs +27 -0
- package/crates/team-agent/src/transport/enter_rescue.rs +166 -0
- package/crates/team-agent/src/transport/native_input.rs +136 -10
- package/crates/team-agent/src/transport/test_support.rs +25 -0
- package/crates/team-agent/src/transport.rs +34 -0
- package/package.json +4 -4
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
//! Independent watchdog receipts. Registration is unconditional; key authority is not.
|
|
2
|
+
|
|
3
|
+
use serde::{Deserialize, Serialize};
|
|
4
|
+
use sha2::{Digest, Sha256};
|
|
5
|
+
|
|
6
|
+
use crate::model::enums::Provider;
|
|
7
|
+
|
|
8
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
9
|
+
pub struct EnterRescueContext {
|
|
10
|
+
pub message_id: String,
|
|
11
|
+
pub owner_team_id: String,
|
|
12
|
+
pub recipient: String,
|
|
13
|
+
pub payload_sha256: String,
|
|
14
|
+
pub target: Option<EnterRescueTarget>,
|
|
15
|
+
pub proof: Option<EnterRescueProof>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
impl EnterRescueContext {
|
|
19
|
+
pub fn marker(&self) -> String {
|
|
20
|
+
format!("[team-agent-token:{}]", self.message_id)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Binds an opaque-paste receipt to this message and this original carrier.
|
|
24
|
+
pub fn scope_sha256(&self) -> Option<String> {
|
|
25
|
+
let bytes = serde_json::to_vec(&(
|
|
26
|
+
&self.message_id,
|
|
27
|
+
&self.owner_team_id,
|
|
28
|
+
&self.recipient,
|
|
29
|
+
&self.payload_sha256,
|
|
30
|
+
&self.target,
|
|
31
|
+
))
|
|
32
|
+
.ok()?;
|
|
33
|
+
Some(format!("{:x}", Sha256::digest(bytes)))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub fn valid_proof(&self) -> bool {
|
|
37
|
+
self.proof.as_ref().is_some_and(|proof| {
|
|
38
|
+
proof.valid()
|
|
39
|
+
&& proof.owned_capsule.as_ref().is_none_or(|capsule| {
|
|
40
|
+
capsule.valid() && self.scope_sha256().as_ref() == Some(&capsule.scope_sha256)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub fn valid_message(&self) -> bool {
|
|
46
|
+
self.message_id.starts_with("msg_")
|
|
47
|
+
&& self
|
|
48
|
+
.message_id
|
|
49
|
+
.bytes()
|
|
50
|
+
.all(|b| b.is_ascii_alphanumeric() || b == b'_')
|
|
51
|
+
&& !self.owner_team_id.is_empty()
|
|
52
|
+
&& !self.recipient.is_empty()
|
|
53
|
+
&& self.recipient != "leader"
|
|
54
|
+
&& valid_digest(&self.payload_sha256)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// `selected` records canonical original routing, not a late adoption of a pane.
|
|
59
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
60
|
+
pub struct EnterRescueTarget {
|
|
61
|
+
pub provider: Provider,
|
|
62
|
+
pub endpoint: String,
|
|
63
|
+
pub pane_id: String,
|
|
64
|
+
pub pane_pid: u32,
|
|
65
|
+
pub pane_height: u32,
|
|
66
|
+
pub carrier_birth: String,
|
|
67
|
+
pub binding_nonce: String,
|
|
68
|
+
pub session: String,
|
|
69
|
+
pub window: String,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl EnterRescueTarget {
|
|
73
|
+
pub fn selected(
|
|
74
|
+
provider: Provider,
|
|
75
|
+
pane_id: String,
|
|
76
|
+
pane_pid: u32,
|
|
77
|
+
session: String,
|
|
78
|
+
window: String,
|
|
79
|
+
) -> Self {
|
|
80
|
+
Self {
|
|
81
|
+
provider,
|
|
82
|
+
pane_id,
|
|
83
|
+
pane_pid,
|
|
84
|
+
session,
|
|
85
|
+
window,
|
|
86
|
+
endpoint: String::new(),
|
|
87
|
+
pane_height: 0,
|
|
88
|
+
carrier_birth: String::new(),
|
|
89
|
+
binding_nonce: String::new(),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pub fn same_selection(&self, other: &Self) -> bool {
|
|
94
|
+
self.provider == other.provider
|
|
95
|
+
&& self.pane_id == other.pane_id
|
|
96
|
+
&& self.pane_pid == other.pane_pid
|
|
97
|
+
&& self.session == other.session
|
|
98
|
+
&& self.window == other.window
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
pub fn bound(&self) -> bool {
|
|
102
|
+
std::path::Path::new(&self.endpoint).is_absolute()
|
|
103
|
+
&& self.pane_id.starts_with('%')
|
|
104
|
+
&& self.pane_pid > 1
|
|
105
|
+
&& self.pane_height >= 10
|
|
106
|
+
&& !self.carrier_birth.is_empty()
|
|
107
|
+
&& !self.binding_nonce.is_empty()
|
|
108
|
+
&& !self.session.is_empty()
|
|
109
|
+
&& !self.window.is_empty()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
114
|
+
pub struct EnterRescueProof {
|
|
115
|
+
pub ui_identity: String,
|
|
116
|
+
pub footer_sha256: String,
|
|
117
|
+
pub visible_marker: bool,
|
|
118
|
+
pub transcript_marker_absent: bool,
|
|
119
|
+
pub owned_capsule: Option<OwnedCapsule>,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
impl EnterRescueProof {
|
|
123
|
+
pub fn valid(&self) -> bool {
|
|
124
|
+
!self.ui_identity.is_empty()
|
|
125
|
+
&& valid_digest(&self.footer_sha256)
|
|
126
|
+
&& (self.visible_marker || self.owned_capsule.as_ref().is_some_and(OwnedCapsule::valid))
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Only a prior empty-baseline/single-owned-paste transition may create this.
|
|
131
|
+
/// Snapshotting a visible capsule label alone never creates ownership.
|
|
132
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
133
|
+
pub struct OwnedCapsule {
|
|
134
|
+
pub scope_sha256: String,
|
|
135
|
+
pub label: String,
|
|
136
|
+
pub baseline_sha256: String,
|
|
137
|
+
pub baseline_capsules: Vec<String>,
|
|
138
|
+
pub observed_after_owned_paste: bool,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
impl OwnedCapsule {
|
|
142
|
+
pub fn valid(&self) -> bool {
|
|
143
|
+
self.observed_after_owned_paste
|
|
144
|
+
&& valid_digest(&self.scope_sha256)
|
|
145
|
+
&& !self.label.is_empty()
|
|
146
|
+
&& self.label.len() <= 80
|
|
147
|
+
&& valid_digest(&self.baseline_sha256)
|
|
148
|
+
&& !self.baseline_capsules.contains(&self.label)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn valid_digest(value: &str) -> bool {
|
|
153
|
+
value.len() == 64 && value.bytes().all(|b| b.is_ascii_hexdigit())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
157
|
+
pub enum EnterRescueOutcome {
|
|
158
|
+
Consumed,
|
|
159
|
+
NoEffect {
|
|
160
|
+
reason: &'static str,
|
|
161
|
+
reobserve: bool,
|
|
162
|
+
},
|
|
163
|
+
Entered {
|
|
164
|
+
accepted: bool,
|
|
165
|
+
},
|
|
166
|
+
}
|
|
@@ -131,7 +131,7 @@ pub(crate) fn record_evidence(evidence: &NativeInputEvidence) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
134
|
-
pub
|
|
134
|
+
pub struct InputFrame {
|
|
135
135
|
pub composer: String,
|
|
136
136
|
pub transcript: String,
|
|
137
137
|
pub identity: String,
|
|
@@ -195,6 +195,10 @@ fn input_digest(text: &str) -> String {
|
|
|
195
195
|
format!("{:x}", Sha256::digest(text.as_bytes()))
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
pub(crate) fn supports_input_observation(provider: Provider) -> bool {
|
|
199
|
+
matches!(provider, Provider::Pi | Provider::CursorAgent)
|
|
200
|
+
}
|
|
201
|
+
|
|
198
202
|
pub(crate) fn inspect_input(
|
|
199
203
|
provider: Provider,
|
|
200
204
|
text: &str,
|
|
@@ -202,16 +206,27 @@ pub(crate) fn inspect_input(
|
|
|
202
206
|
) -> Option<InputFrame> {
|
|
203
207
|
match provider {
|
|
204
208
|
Provider::Pi => pi_input(text, pane_height),
|
|
205
|
-
Provider::CursorAgent => cursor_input(text),
|
|
209
|
+
Provider::CursorAgent => cursor_input(text, pane_height),
|
|
206
210
|
_ => None,
|
|
207
211
|
}
|
|
208
212
|
}
|
|
209
213
|
|
|
210
|
-
fn cursor_input(text: &str) -> Option<InputFrame> {
|
|
214
|
+
fn cursor_input(text: &str, pane_height: u32) -> Option<InputFrame> {
|
|
211
215
|
use team_agent_contract::contract::delivery::InputSurface;
|
|
216
|
+
if pane_height == 0 {
|
|
217
|
+
return None;
|
|
218
|
+
}
|
|
219
|
+
let text_rows: Vec<_> = text.lines().collect();
|
|
220
|
+
// Only the current viewport authorizes input; retain full history below
|
|
221
|
+
// solely for same-message transcript correlation.
|
|
222
|
+
let viewport_start = text_rows.len().saturating_sub(pane_height as usize);
|
|
223
|
+
let viewport = text_rows[viewport_start..].join("\n");
|
|
212
224
|
let (surface, rows, capsule, busy) =
|
|
213
|
-
team_agent_contract::providers::cursor::interaction::
|
|
214
|
-
let first = rows.first()
|
|
225
|
+
team_agent_contract::providers::cursor::interaction::inspect_legacy_composer(&viewport)?;
|
|
226
|
+
let first = match *rows.first()? {
|
|
227
|
+
"→" => "",
|
|
228
|
+
row => row.strip_prefix("→ ")?,
|
|
229
|
+
};
|
|
215
230
|
let footer = rows.iter().rposition(|row| {
|
|
216
231
|
row.strip_suffix("Run Everything").is_some_and(|prefix| {
|
|
217
232
|
!prefix.trim().is_empty() && !prefix.trim_start().starts_with("→ ")
|
|
@@ -228,9 +243,13 @@ fn cursor_input(text: &str) -> Option<InputFrame> {
|
|
|
228
243
|
if tail.is_empty() {
|
|
229
244
|
return None;
|
|
230
245
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
246
|
+
// Startup history can leave Tail(200) between Ready/Pasted/Accepted. The
|
|
247
|
+
// immutable carrier birth/nonce remains in NativeInputBinding; this UI
|
|
248
|
+
// context is the current native cwd/footer tail, not a version attestation.
|
|
249
|
+
let identity = format!(
|
|
250
|
+
"cursor-current-composer-v1:{}",
|
|
251
|
+
input_digest(&tail.join("\n"))
|
|
252
|
+
);
|
|
234
253
|
let footer_row = rows[footer]
|
|
235
254
|
.split_whitespace()
|
|
236
255
|
.collect::<Vec<_>>()
|
|
@@ -247,10 +266,9 @@ fn cursor_input(text: &str) -> Option<InputFrame> {
|
|
|
247
266
|
.collect::<Vec<_>>()
|
|
248
267
|
.join("\n")
|
|
249
268
|
};
|
|
250
|
-
let text_rows: Vec<_> = text.lines().collect();
|
|
251
269
|
let arrow = text_rows
|
|
252
270
|
.iter()
|
|
253
|
-
.rposition(|line| line.trim().starts_with("→ "))?;
|
|
271
|
+
.rposition(|line| line.trim() == "→" || line.trim().starts_with("→ "))?;
|
|
254
272
|
Some(InputFrame {
|
|
255
273
|
composer,
|
|
256
274
|
transcript: text.lines().take(arrow).collect::<Vec<_>>().join("\n"),
|
|
@@ -544,6 +562,114 @@ mod tests {
|
|
|
544
562
|
}
|
|
545
563
|
}
|
|
546
564
|
|
|
565
|
+
// Native Issue #328 current-composer rows; transcript removed and private
|
|
566
|
+
// cwd replaced with /owned/project. Startup banner is genuinely absent
|
|
567
|
+
// from the owned pane's Tail(200), not a fabricated invalid version.
|
|
568
|
+
const CURSOR_FOLLOWUP: &str =
|
|
569
|
+
include_str!("../../tests/fixtures/cursor-followup-headerless.txt");
|
|
570
|
+
|
|
571
|
+
#[test]
|
|
572
|
+
fn native_input_cursor_clean_followup_does_not_require_scrolled_startup_banner() {
|
|
573
|
+
let ready = inspect_input(Provider::CursorAgent, CURSOR_FOLLOWUP).unwrap();
|
|
574
|
+
assert!(ready.empty());
|
|
575
|
+
assert!(!ready.busy);
|
|
576
|
+
assert!(ready.capsule.is_none());
|
|
577
|
+
let header = format!("Cursor Agent\nv2026.10.14-0f9edde\n{CURSOR_FOLLOWUP}");
|
|
578
|
+
let with_header = inspect_input(Provider::CursorAgent, &header).unwrap();
|
|
579
|
+
assert!(with_header.empty() && !with_header.busy);
|
|
580
|
+
assert_eq!(ready.footer, with_header.footer);
|
|
581
|
+
assert_eq!(
|
|
582
|
+
ready.identity, with_header.identity,
|
|
583
|
+
"startup history is not a new UI identity"
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#[test]
|
|
588
|
+
fn native_input_cursor_headerless_followup_retains_draft_busy_and_footer_fences() {
|
|
589
|
+
let label = "[Pasted text #2 +25 lines]";
|
|
590
|
+
let folded = CURSOR_FOLLOWUP.replace("Add a follow-up", label);
|
|
591
|
+
let draft = inspect_input(Provider::CursorAgent, &folded).unwrap();
|
|
592
|
+
assert!(!draft.empty());
|
|
593
|
+
assert_eq!(draft.capsule.as_deref(), Some(label));
|
|
594
|
+
let dirty = CURSOR_FOLLOWUP.replace("Add a follow-up", "Add a follow-up draft");
|
|
595
|
+
assert!(!inspect_input(Provider::CursorAgent, &dirty)
|
|
596
|
+
.unwrap()
|
|
597
|
+
.empty());
|
|
598
|
+
for dirty in [
|
|
599
|
+
CURSOR_FOLLOWUP.replace("Add a follow-up", "Add a follow-up\nforeign draft"),
|
|
600
|
+
CURSOR_FOLLOWUP.replace("Add a follow-up", &format!("{label}\nforeign draft")),
|
|
601
|
+
] {
|
|
602
|
+
assert!(inspect_input(Provider::CursorAgent, &dirty).is_none());
|
|
603
|
+
}
|
|
604
|
+
let busy = CURSOR_FOLLOWUP.replace("Add a follow-up", "Add a follow-up ctrl+c to stop");
|
|
605
|
+
assert!(inspect_input(Provider::CursorAgent, &busy).unwrap().busy);
|
|
606
|
+
let working = format!("⠘⠆ Working\n{CURSOR_FOLLOWUP}");
|
|
607
|
+
assert!(inspect_input(Provider::CursorAgent, &working).unwrap().busy);
|
|
608
|
+
for unknown in [
|
|
609
|
+
"→ Add a follow-up\n".to_string(),
|
|
610
|
+
CURSOR_FOLLOWUP.replace("Run Everything", "unknown footer"),
|
|
611
|
+
CURSOR_FOLLOWUP.replace(" /owned/project\n", ""),
|
|
612
|
+
] {
|
|
613
|
+
assert!(inspect_input(Provider::CursorAgent, &unknown).is_none());
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
#[test]
|
|
618
|
+
fn native_input_cursor_real_expanded_followup_is_nonempty_current_marker_not_capsule() {
|
|
619
|
+
let expanded = include_str!("../../tests/fixtures/cursor-followup-expanded.txt");
|
|
620
|
+
let marker = "[team-agent-token:msg_one]";
|
|
621
|
+
for prefix in [
|
|
622
|
+
"".to_string(),
|
|
623
|
+
format!("Cursor Agent\nv2026.10.14-0f9edde\n{CURSOR_FOLLOWUP}"),
|
|
624
|
+
] {
|
|
625
|
+
let text = format!("{prefix}{expanded}");
|
|
626
|
+
let frame = inspect_input(Provider::CursorAgent, &text).unwrap();
|
|
627
|
+
assert!(!frame.empty() && !frame.busy);
|
|
628
|
+
assert!(frame.has_marker(marker));
|
|
629
|
+
assert!(frame.capsule.is_none());
|
|
630
|
+
assert!(!frame.transcript.contains(marker));
|
|
631
|
+
let changed = text.replace("/owned/project", "/different/project");
|
|
632
|
+
assert_ne!(
|
|
633
|
+
frame.identity,
|
|
634
|
+
inspect_input(Provider::CursorAgent, &changed)
|
|
635
|
+
.unwrap()
|
|
636
|
+
.identity
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
let duplicate = expanded.replace(marker, &format!("{marker}\n{marker}"));
|
|
640
|
+
assert!(!inspect_input(Provider::CursorAgent, &duplicate)
|
|
641
|
+
.unwrap()
|
|
642
|
+
.has_marker(marker));
|
|
643
|
+
let busy = expanded.replace(" →", "⠘⠆ Working\n →");
|
|
644
|
+
assert!(inspect_input(Provider::CursorAgent, &busy).unwrap().busy);
|
|
645
|
+
for unknown in [
|
|
646
|
+
"→\nGrok 4.7 High Run Everything\n/owned/project\n".to_string(),
|
|
647
|
+
"→\nforeign draft\n→ Add a follow-up\nGrok 4.7 High Run Everything\n/owned/project\n"
|
|
648
|
+
.to_string(),
|
|
649
|
+
expanded.replace("Run Everything", "unknown footer"),
|
|
650
|
+
expanded.replace(" /owned/project\n", ""),
|
|
651
|
+
format!("{expanded}{}", "unknown current row\n".repeat(24)),
|
|
652
|
+
] {
|
|
653
|
+
assert!(super::inspect_input(Provider::CursorAgent, &unknown, 24).is_none());
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#[test]
|
|
658
|
+
fn native_input_cursor_requires_composer_in_current_viewport_not_old_history() {
|
|
659
|
+
for header in ["", "Cursor Agent\nv2026.10.14-0f9edde\n"] {
|
|
660
|
+
let hidden = format!(
|
|
661
|
+
"{header}{CURSOR_FOLLOWUP}{}",
|
|
662
|
+
"unknown current row\n".repeat(24)
|
|
663
|
+
);
|
|
664
|
+
assert!(super::inspect_input(Provider::CursorAgent, &hidden, 24).is_none());
|
|
665
|
+
}
|
|
666
|
+
assert!(super::inspect_input(Provider::CursorAgent, CURSOR_FOLLOWUP, 0).is_none());
|
|
667
|
+
let current = super::inspect_input(Provider::CursorAgent, CURSOR_FOLLOWUP, 24).unwrap();
|
|
668
|
+
let changed = CURSOR_FOLLOWUP.replace("/owned/project", "/different/project");
|
|
669
|
+
let changed = super::inspect_input(Provider::CursorAgent, &changed, 24).unwrap();
|
|
670
|
+
assert_ne!(current.identity, changed.identity);
|
|
671
|
+
}
|
|
672
|
+
|
|
547
673
|
#[test]
|
|
548
674
|
fn native_input_cursor_separates_folded_draft_from_history_and_busy() {
|
|
549
675
|
let capsule = "[Pasted text #1 +25 lines]";
|
|
@@ -57,6 +57,8 @@ struct OfflineState {
|
|
|
57
57
|
/// answer keyed by pane id. Used by leader provider health tests.
|
|
58
58
|
pane_current_commands: BTreeMap<String, String>,
|
|
59
59
|
session_owner: Option<super::SessionOwner>,
|
|
60
|
+
native_input_observation: Option<super::native_input::InputFrame>,
|
|
61
|
+
native_input_error: Option<String>,
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
impl Default for OfflineState {
|
|
@@ -85,6 +87,8 @@ impl Default for OfflineState {
|
|
|
85
87
|
capture_ranges: Vec::new(),
|
|
86
88
|
pane_current_commands: BTreeMap::new(),
|
|
87
89
|
session_owner: None,
|
|
90
|
+
native_input_observation: None,
|
|
91
|
+
native_input_error: None,
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
}
|
|
@@ -95,6 +99,16 @@ pub struct OfflineTransport {
|
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
impl OfflineTransport {
|
|
102
|
+
pub fn with_native_input_observation(self, frame: super::native_input::InputFrame) -> Self {
|
|
103
|
+
self.with_state(|state| state.native_input_observation = Some(frame));
|
|
104
|
+
self
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pub fn with_native_input_error(self, reason: &str) -> Self {
|
|
108
|
+
self.with_state(|state| state.native_input_error = Some(reason.into()));
|
|
109
|
+
self
|
|
110
|
+
}
|
|
111
|
+
|
|
98
112
|
pub fn new() -> Self {
|
|
99
113
|
Self::default()
|
|
100
114
|
}
|
|
@@ -434,6 +448,17 @@ impl OfflineTransport {
|
|
|
434
448
|
}
|
|
435
449
|
|
|
436
450
|
impl Transport for OfflineTransport {
|
|
451
|
+
fn observe_native_input(
|
|
452
|
+
&self,
|
|
453
|
+
_target: &Target,
|
|
454
|
+
_provider: crate::model::enums::Provider,
|
|
455
|
+
) -> Result<Option<super::native_input::InputFrame>, TransportError> {
|
|
456
|
+
self.with_state(|state| match &state.native_input_error {
|
|
457
|
+
Some(reason) => Err(TransportError::NativeInputNotReady { reason: reason.clone() }),
|
|
458
|
+
None => Ok(state.native_input_observation.clone()),
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
437
462
|
fn kind(&self) -> BackendKind {
|
|
438
463
|
BackendKind::Tmux
|
|
439
464
|
}
|
|
@@ -67,6 +67,7 @@ use std::path::{Path, PathBuf};
|
|
|
67
67
|
use serde::{Deserialize, Serialize};
|
|
68
68
|
use thiserror::Error;
|
|
69
69
|
|
|
70
|
+
pub mod enter_rescue;
|
|
70
71
|
pub mod native_input;
|
|
71
72
|
|
|
72
73
|
#[cfg(test)]
|
|
@@ -989,6 +990,39 @@ pub trait Transport: Send + Sync {
|
|
|
989
990
|
Ok(report)
|
|
990
991
|
}
|
|
991
992
|
|
|
993
|
+
/// Observation only; None means this backend/provider has no native parser.
|
|
994
|
+
/// An implemented parser with an incomplete frame returns an error, not absence.
|
|
995
|
+
fn observe_native_input(
|
|
996
|
+
&self,
|
|
997
|
+
_target: &Target,
|
|
998
|
+
_provider: crate::model::enums::Provider,
|
|
999
|
+
) -> Result<Option<native_input::InputFrame>, TransportError> {
|
|
1000
|
+
Ok(None)
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/// Snapshot the original attempted target without submitting or adopting a capsule.
|
|
1004
|
+
fn snapshot_enter_rescue(
|
|
1005
|
+
&self,
|
|
1006
|
+
_target: &Target,
|
|
1007
|
+
context: &enter_rescue::EnterRescueContext,
|
|
1008
|
+
_rendered_payload: &str,
|
|
1009
|
+
) -> Result<enter_rescue::EnterRescueContext, TransportError> {
|
|
1010
|
+
Ok(context.clone())
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/// Independent delayed Enter-only watchdog, not native-input admission.
|
|
1014
|
+
fn rescue_enter_guarded(
|
|
1015
|
+
&self,
|
|
1016
|
+
_context: &enter_rescue::EnterRescueContext,
|
|
1017
|
+
_authorize: &dyn Fn() -> bool,
|
|
1018
|
+
_observer: Option<&dyn SubmitObserver>,
|
|
1019
|
+
) -> Result<enter_rescue::EnterRescueOutcome, TransportError> {
|
|
1020
|
+
Ok(enter_rescue::EnterRescueOutcome::NoEffect {
|
|
1021
|
+
reason: "unsupported_backend",
|
|
1022
|
+
reobserve: false,
|
|
1023
|
+
})
|
|
1024
|
+
}
|
|
1025
|
+
|
|
992
1026
|
/// Enter-only recovery; unsupported backends have an explicit zero-effect refusal.
|
|
993
1027
|
fn rescue_enter(&self, _evidence: &native_input::NativeInputEvidence, _authorize: &dyn Fn() -> bool, _observer: Option<&dyn SubmitObserver>)
|
|
994
1028
|
-> Result<native_input::EnterRescueOutcome, TransportError>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.119",
|
|
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.119",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.119",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.119"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|