@team-agent/installer 0.5.116 → 0.5.117
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/leader/lease.rs +1 -18
- package/crates/team-agent/src/lifecycle/pane_input_lock.rs +86 -8
- package/crates/team-agent/src/messaging/delivery.rs +132 -27
- package/crates/team-agent/src/messaging/enter_rescue.rs +334 -0
- package/crates/team-agent/src/messaging/enter_rescue_tests.rs +566 -0
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/mod.rs +1 -0
- package/crates/team-agent/src/messaging/scheduler.rs +8 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +2 -0
- package/crates/team-agent/src/messaging/types.rs +1 -0
- package/crates/team-agent/src/state/repository.rs +47 -0
- package/crates/team-agent/src/tmux_backend/native_input.rs +494 -0
- package/crates/team-agent/src/tmux_backend/native_input_tests.rs +781 -0
- package/crates/team-agent/src/tmux_backend.rs +49 -6
- package/crates/team-agent/src/transport/native_input.rs +594 -0
- package/crates/team-agent/src/transport.rs +19 -0
- package/package.json +4 -4
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
//! Guarded legacy native-input effects. Shared contract parsers remain observation-only.
|
|
2
|
+
|
|
3
|
+
use std::time::{Duration, Instant};
|
|
4
|
+
|
|
5
|
+
use sha2::{Digest, Sha256};
|
|
6
|
+
|
|
7
|
+
use super::*;
|
|
8
|
+
use crate::lifecycle::pane_input_lock::{acquire_required, PaneInputLockRequest};
|
|
9
|
+
use crate::model::enums::Provider;
|
|
10
|
+
use crate::transport::native_input::{
|
|
11
|
+
inspect_input, pi_paste_capsule, record_evidence, EnterRescueOutcome, InputFrame,
|
|
12
|
+
NativeInputBinding, NativeInputEvidence, NativeInputPhase, NativeInputRequest,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
#[cfg(test)]
|
|
16
|
+
#[path = "native_input_tests.rs"]
|
|
17
|
+
mod tests;
|
|
18
|
+
|
|
19
|
+
const STABLE: Duration = Duration::from_millis(200);
|
|
20
|
+
const OBSERVE: Duration = Duration::from_secs(10);
|
|
21
|
+
const POLL: Duration = Duration::from_millis(50);
|
|
22
|
+
|
|
23
|
+
fn refused(reason: &str) -> TransportError {
|
|
24
|
+
TransportError::NativeInputNotReady {
|
|
25
|
+
reason: reason.to_string(),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn surface_unknown(error: &TransportError) -> bool {
|
|
30
|
+
matches!(
|
|
31
|
+
error,
|
|
32
|
+
TransportError::NativeInputNotReady { reason } if reason == "native_surface_unknown"
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub(crate) fn digest(text: &str) -> String {
|
|
37
|
+
format!("{:x}", Sha256::digest(text.as_bytes()))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl TmuxBackend {
|
|
41
|
+
fn native_binding(&self, target: &Target) -> Result<NativeInputBinding, TransportError> {
|
|
42
|
+
self.query_native_binding(target, false)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn query_native_binding(
|
|
46
|
+
&self,
|
|
47
|
+
target: &Target,
|
|
48
|
+
allow_unset_nonce: bool,
|
|
49
|
+
) -> Result<NativeInputBinding, TransportError> {
|
|
50
|
+
let pane = pane_from_target(target);
|
|
51
|
+
let argv = self.tmux_argv(&[
|
|
52
|
+
"tmux".into(), "display-message".into(), "-p".into(), "-t".into(), pane.as_str().into(),
|
|
53
|
+
"#{socket_path}|#{pane_id}|#{pane_pid}|#{session_name}|#{window_name}|#{@team_agent_pane_binding_nonce}|#{pane_in_mode}|#{pane_height}".into(),
|
|
54
|
+
]);
|
|
55
|
+
let output = self.runner.run(&argv)?;
|
|
56
|
+
if !output.success || output.stdout.lines().count() != 1 {
|
|
57
|
+
return Err(refused("binding_query"));
|
|
58
|
+
}
|
|
59
|
+
let fields: Vec<_> = output.stdout.trim_end().split('|').collect();
|
|
60
|
+
if fields.len() != 8 {
|
|
61
|
+
return Err(refused("binding_invalid:field_count"));
|
|
62
|
+
}
|
|
63
|
+
for (invalid, field) in [
|
|
64
|
+
(!std::path::Path::new(fields[0]).is_absolute(), "endpoint"),
|
|
65
|
+
(fields[1] != pane.as_str(), "pane"),
|
|
66
|
+
(fields[3].is_empty(), "session_empty"),
|
|
67
|
+
(fields[4].is_empty(), "window_empty"),
|
|
68
|
+
(fields[5].is_empty() && !allow_unset_nonce, "nonce_empty"),
|
|
69
|
+
(fields[6] != "0", "pane_in_mode"),
|
|
70
|
+
] {
|
|
71
|
+
if invalid {
|
|
72
|
+
// Field class only: no argv, environment, business text or raw
|
|
73
|
+
// native metadata is copied into the existing refusal event.
|
|
74
|
+
return Err(refused(&format!("binding_invalid:{field}")));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
let pane_height = fields[7]
|
|
78
|
+
.parse::<u32>()
|
|
79
|
+
.map_err(|_| refused("binding_height"))?;
|
|
80
|
+
if pane_height < 10 {
|
|
81
|
+
return Err(refused("binding_height"));
|
|
82
|
+
}
|
|
83
|
+
let pid = fields[2]
|
|
84
|
+
.parse::<u32>()
|
|
85
|
+
.map_err(|_| refused("binding_pid"))?;
|
|
86
|
+
let birth = team_agent_contract::host::process::process_birth_identity(pid)
|
|
87
|
+
.map_err(|_| refused("carrier_birth"))?;
|
|
88
|
+
Ok(NativeInputBinding {
|
|
89
|
+
endpoint: fields[0].into(),
|
|
90
|
+
pane_id: fields[1].into(),
|
|
91
|
+
pane_pid: pid,
|
|
92
|
+
pane_height,
|
|
93
|
+
carrier_birth: birth,
|
|
94
|
+
session: fields[3].into(),
|
|
95
|
+
window: fields[4].into(),
|
|
96
|
+
binding_nonce: fields[5].into(),
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Called only under the required input lock. A newly launched legacy worker
|
|
101
|
+
// has no Leader lease nonce producer: initialize its exact current carrier,
|
|
102
|
+
// never overwrite an existing nonce or waive the subsequent strict reads.
|
|
103
|
+
fn prepare_native_binding(
|
|
104
|
+
&self,
|
|
105
|
+
target: &Target,
|
|
106
|
+
request: &NativeInputRequest,
|
|
107
|
+
) -> Result<NativeInputBinding, TransportError> {
|
|
108
|
+
let initial = self.query_native_binding(target, true)?;
|
|
109
|
+
if initial.pane_id != request.pane_id
|
|
110
|
+
|| initial.pane_pid != request.pane_pid
|
|
111
|
+
|| initial.session != request.session
|
|
112
|
+
|| initial.window != request.window
|
|
113
|
+
{
|
|
114
|
+
return Err(refused("selected_binding_changed"));
|
|
115
|
+
}
|
|
116
|
+
if !initial.binding_nonce.is_empty() {
|
|
117
|
+
return Ok(initial);
|
|
118
|
+
}
|
|
119
|
+
let pane = PaneId::new(&initial.pane_id);
|
|
120
|
+
let workspace = self
|
|
121
|
+
.event_workspace
|
|
122
|
+
.as_deref()
|
|
123
|
+
.ok_or_else(|| refused("workspace_missing"))?;
|
|
124
|
+
let proposed = next_pane_binding_nonce(workspace, &initial.endpoint, &pane);
|
|
125
|
+
let nonce = self.set_pane_binding_nonce_if_unset(&pane, &proposed)?;
|
|
126
|
+
let expected = NativeInputBinding {
|
|
127
|
+
binding_nonce: nonce,
|
|
128
|
+
..initial
|
|
129
|
+
};
|
|
130
|
+
let observed = self.native_binding(target)?;
|
|
131
|
+
if observed != expected {
|
|
132
|
+
return Err(refused("binding_changed_after_nonce"));
|
|
133
|
+
}
|
|
134
|
+
Ok(observed)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn native_frame(&self, evidence: &NativeInputEvidence) -> Result<InputFrame, TransportError> {
|
|
138
|
+
let target = Target::Pane(PaneId::new(&evidence.binding.pane_id));
|
|
139
|
+
if self.native_binding(&target)? != evidence.binding {
|
|
140
|
+
return Err(refused("binding_changed"));
|
|
141
|
+
}
|
|
142
|
+
let capture = self.capture(&target, CaptureRange::Tail(200))?;
|
|
143
|
+
let frame = inspect_input(
|
|
144
|
+
evidence.request.provider,
|
|
145
|
+
&capture.text,
|
|
146
|
+
evidence.binding.pane_height,
|
|
147
|
+
)
|
|
148
|
+
.ok_or_else(|| refused("native_surface_unknown"))?;
|
|
149
|
+
if self.native_binding(&target)? != evidence.binding {
|
|
150
|
+
return Err(refused("binding_changed"));
|
|
151
|
+
}
|
|
152
|
+
Ok(frame)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fn native_record(&self, evidence: &NativeInputEvidence) -> Result<(), TransportError> {
|
|
156
|
+
record_evidence(evidence);
|
|
157
|
+
let workspace = self
|
|
158
|
+
.event_workspace
|
|
159
|
+
.as_deref()
|
|
160
|
+
.ok_or_else(|| refused("workspace_missing"))?;
|
|
161
|
+
crate::event_log::EventLog::new(workspace)
|
|
162
|
+
.write(
|
|
163
|
+
"native_input.evidence",
|
|
164
|
+
serde_json::to_value(evidence).map_err(std::io::Error::other)?,
|
|
165
|
+
)
|
|
166
|
+
.map(|_| ())
|
|
167
|
+
.map_err(|error| TransportError::Io(std::io::Error::other(error)))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fn stable_frame(
|
|
171
|
+
&self,
|
|
172
|
+
evidence: &NativeInputEvidence,
|
|
173
|
+
empty: bool,
|
|
174
|
+
) -> Result<InputFrame, TransportError> {
|
|
175
|
+
let start = Instant::now();
|
|
176
|
+
let stable_window = if empty {
|
|
177
|
+
STABLE
|
|
178
|
+
} else {
|
|
179
|
+
STABLE.max(current_paste_to_submit_floor())
|
|
180
|
+
};
|
|
181
|
+
let mut eligible: Option<(InputFrame, Instant)> = None;
|
|
182
|
+
while start.elapsed() < OBSERVE {
|
|
183
|
+
let frame = match self.native_frame(evidence) {
|
|
184
|
+
Ok(frame) => frame,
|
|
185
|
+
Err(error) if surface_unknown(&error) => {
|
|
186
|
+
// A partial redraw grants nothing and resets stability.
|
|
187
|
+
// Binding/process/capture failures still refuse immediately.
|
|
188
|
+
eligible = None;
|
|
189
|
+
std::thread::sleep(POLL);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
Err(error) => return Err(error),
|
|
193
|
+
};
|
|
194
|
+
let valid = if empty {
|
|
195
|
+
frame.empty() && !frame.busy
|
|
196
|
+
} else {
|
|
197
|
+
frame.pending(evidence)
|
|
198
|
+
};
|
|
199
|
+
if valid {
|
|
200
|
+
if let Some((previous, since)) = &eligible {
|
|
201
|
+
if previous.composer == frame.composer
|
|
202
|
+
&& previous.identity == frame.identity
|
|
203
|
+
&& previous.capsule == frame.capsule
|
|
204
|
+
&& since.elapsed() >= stable_window
|
|
205
|
+
{
|
|
206
|
+
return Ok(frame);
|
|
207
|
+
}
|
|
208
|
+
if previous.composer != frame.composer
|
|
209
|
+
|| previous.identity != frame.identity
|
|
210
|
+
|| previous.capsule != frame.capsule
|
|
211
|
+
{
|
|
212
|
+
eligible = Some((frame, Instant::now()));
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
eligible = Some((frame, Instant::now()));
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
eligible = None;
|
|
219
|
+
}
|
|
220
|
+
std::thread::sleep(POLL);
|
|
221
|
+
}
|
|
222
|
+
Err(refused(if empty {
|
|
223
|
+
"composer_not_ready"
|
|
224
|
+
} else {
|
|
225
|
+
"owned_paste_not_stable"
|
|
226
|
+
}))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn observe_native_acceptance(
|
|
230
|
+
&self,
|
|
231
|
+
evidence: &NativeInputEvidence,
|
|
232
|
+
) -> Result<bool, TransportError> {
|
|
233
|
+
let start = Instant::now();
|
|
234
|
+
let mut last_unknown = false;
|
|
235
|
+
while start.elapsed() < OBSERVE {
|
|
236
|
+
match self.native_frame(evidence) {
|
|
237
|
+
Ok(frame) => {
|
|
238
|
+
last_unknown = false;
|
|
239
|
+
if frame.accepted(evidence) {
|
|
240
|
+
return Ok(true);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
Err(error) if surface_unknown(&error) => last_unknown = true,
|
|
244
|
+
Err(error) => return Err(error),
|
|
245
|
+
}
|
|
246
|
+
std::thread::sleep(POLL);
|
|
247
|
+
}
|
|
248
|
+
if last_unknown {
|
|
249
|
+
Err(refused("native_surface_unknown"))
|
|
250
|
+
} else {
|
|
251
|
+
Ok(false)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
pub(super) fn inject_native_input(
|
|
256
|
+
&self,
|
|
257
|
+
target: &Target,
|
|
258
|
+
text: &str,
|
|
259
|
+
bracketed: bool,
|
|
260
|
+
request: NativeInputRequest,
|
|
261
|
+
observer: Option<&dyn SubmitObserver>,
|
|
262
|
+
) -> Result<InjectReport, TransportError> {
|
|
263
|
+
let pane = pane_from_target(target);
|
|
264
|
+
let _lock = acquire_required(PaneInputLockRequest {
|
|
265
|
+
workspace: self.event_workspace.as_deref(),
|
|
266
|
+
target_key: pane.as_str(),
|
|
267
|
+
operation: "native_inject",
|
|
268
|
+
})
|
|
269
|
+
.ok_or_else(|| refused("input_lock"))?;
|
|
270
|
+
if !request.supported() {
|
|
271
|
+
return Err(refused("unsupported_profile"));
|
|
272
|
+
}
|
|
273
|
+
let binding = self.prepare_native_binding(target, &request)?;
|
|
274
|
+
let mut evidence = NativeInputEvidence {
|
|
275
|
+
request,
|
|
276
|
+
binding,
|
|
277
|
+
payload_sha256: digest(text),
|
|
278
|
+
ui_identity: String::new(),
|
|
279
|
+
footer_sha256: String::new(),
|
|
280
|
+
baseline_sha256: String::new(),
|
|
281
|
+
baseline_capsules: Vec::new(),
|
|
282
|
+
phase: NativeInputPhase::Ready,
|
|
283
|
+
capsule: None,
|
|
284
|
+
};
|
|
285
|
+
if evidence.binding.pane_id != evidence.request.pane_id
|
|
286
|
+
|| evidence.binding.pane_pid != evidence.request.pane_pid
|
|
287
|
+
|| evidence.binding.session != evidence.request.session
|
|
288
|
+
|| evidence.binding.window != evidence.request.window
|
|
289
|
+
{
|
|
290
|
+
return Err(refused("selected_binding_changed"));
|
|
291
|
+
}
|
|
292
|
+
let baseline = self.stable_frame(&evidence, true)?;
|
|
293
|
+
if baseline.transcript.contains(&evidence.request.marker()) {
|
|
294
|
+
return Err(refused("marker_in_baseline"));
|
|
295
|
+
}
|
|
296
|
+
evidence.ui_identity = baseline.identity;
|
|
297
|
+
evidence.footer_sha256 = digest(&baseline.footer);
|
|
298
|
+
evidence.baseline_sha256 = digest(&baseline.transcript);
|
|
299
|
+
evidence.baseline_capsules =
|
|
300
|
+
capsule_labels(evidence.request.provider, &baseline.transcript);
|
|
301
|
+
self.native_record(&evidence)?;
|
|
302
|
+
let buffer = owned_buffer(&evidence);
|
|
303
|
+
for argv in tmux_inject_text_argv(&pane, &buffer, text, bracketed) {
|
|
304
|
+
let stage = inject_stage_for_argv(&argv);
|
|
305
|
+
if stage == InjectStage::PasteBuffer {
|
|
306
|
+
let frame = self.native_frame(&evidence)?;
|
|
307
|
+
if !frame.empty()
|
|
308
|
+
|| frame.busy
|
|
309
|
+
|| frame.identity != evidence.ui_identity
|
|
310
|
+
|| digest(&frame.footer) != evidence.footer_sha256
|
|
311
|
+
{
|
|
312
|
+
return Err(refused("composer_changed_before_paste"));
|
|
313
|
+
}
|
|
314
|
+
evidence.phase = NativeInputPhase::MayHavePasted;
|
|
315
|
+
self.native_record(&evidence)?;
|
|
316
|
+
}
|
|
317
|
+
if stage == InjectStage::LoadBuffer {
|
|
318
|
+
self.run_inject_stage_with_stdin(&argv, stage, text)?;
|
|
319
|
+
} else {
|
|
320
|
+
self.run_inject_stage(&argv, stage)?;
|
|
321
|
+
}
|
|
322
|
+
if stage == InjectStage::PasteBuffer {
|
|
323
|
+
evidence.phase = NativeInputPhase::Pasted;
|
|
324
|
+
self.native_record(&evidence)?;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// A capsule becomes owned only after this empty-baseline, single-paste transition.
|
|
328
|
+
let start = Instant::now();
|
|
329
|
+
while start.elapsed() < OBSERVE {
|
|
330
|
+
let frame = match self.native_frame(&evidence) {
|
|
331
|
+
Ok(frame) => frame,
|
|
332
|
+
Err(error) if surface_unknown(&error) => {
|
|
333
|
+
std::thread::sleep(POLL);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
Err(error) => return Err(error),
|
|
337
|
+
};
|
|
338
|
+
if frame.identity == evidence.ui_identity
|
|
339
|
+
&& digest(&frame.footer) == evidence.footer_sha256
|
|
340
|
+
&& !frame.busy
|
|
341
|
+
{
|
|
342
|
+
if let Some(capsule) = &frame.capsule {
|
|
343
|
+
if evidence.capsule.is_none() && !evidence.baseline_capsules.contains(capsule) {
|
|
344
|
+
evidence.capsule = Some(capsule.clone());
|
|
345
|
+
// Bind the empty-baseline, single-owned-paste transition
|
|
346
|
+
// before stability waiting can fail. This is still
|
|
347
|
+
// Pasted, not Seen or permission to submit a key.
|
|
348
|
+
self.native_record(&evidence)?;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if frame.pending(&evidence) {
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
std::thread::sleep(POLL);
|
|
356
|
+
}
|
|
357
|
+
self.stable_frame(&evidence, false)?;
|
|
358
|
+
evidence.phase = NativeInputPhase::Seen;
|
|
359
|
+
self.native_record(&evidence)?;
|
|
360
|
+
self.native_submit(&mut evidence, observer)?;
|
|
361
|
+
let accepted = self.observe_native_acceptance(&evidence)?;
|
|
362
|
+
if accepted {
|
|
363
|
+
evidence.phase = NativeInputPhase::Accepted;
|
|
364
|
+
self.native_record(&evidence)?;
|
|
365
|
+
}
|
|
366
|
+
Ok(InjectReport {
|
|
367
|
+
stage_reached: InjectStage::Submit,
|
|
368
|
+
inject_verification: if evidence.capsule.is_some() {
|
|
369
|
+
InjectVerification::CaptureContainsNewPastedContentPrompt
|
|
370
|
+
} else {
|
|
371
|
+
InjectVerification::CaptureContainsToken
|
|
372
|
+
},
|
|
373
|
+
submit_verification: if accepted {
|
|
374
|
+
SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
375
|
+
} else {
|
|
376
|
+
SubmitVerification::SubmitConsumptionUnverified
|
|
377
|
+
},
|
|
378
|
+
turn_verification: TurnVerification::NotRequired,
|
|
379
|
+
attempts: 1,
|
|
380
|
+
submit_diagnostics: None,
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fn native_submit(
|
|
385
|
+
&self,
|
|
386
|
+
evidence: &mut NativeInputEvidence,
|
|
387
|
+
observer: Option<&dyn SubmitObserver>,
|
|
388
|
+
) -> Result<(), TransportError> {
|
|
389
|
+
let frame = self.native_frame(evidence)?;
|
|
390
|
+
if !frame.pending(evidence) {
|
|
391
|
+
return Err(refused("composer_changed_before_enter"));
|
|
392
|
+
}
|
|
393
|
+
evidence.phase = NativeInputPhase::MayHaveSubmitted;
|
|
394
|
+
self.native_record(evidence)?;
|
|
395
|
+
self.run_inject_stage(
|
|
396
|
+
&tmux_send_submit_argv(&PaneId::new(&evidence.binding.pane_id), Key::Enter),
|
|
397
|
+
InjectStage::Submit,
|
|
398
|
+
)?;
|
|
399
|
+
notify_submit_observer(observer);
|
|
400
|
+
evidence.phase = NativeInputPhase::Submitted;
|
|
401
|
+
self.native_record(evidence)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
pub(super) fn rescue_native_input(
|
|
405
|
+
&self,
|
|
406
|
+
evidence: &NativeInputEvidence,
|
|
407
|
+
authorize: &dyn Fn() -> bool,
|
|
408
|
+
observer: Option<&dyn SubmitObserver>,
|
|
409
|
+
) -> Result<EnterRescueOutcome, TransportError> {
|
|
410
|
+
let _lock = match acquire_required(PaneInputLockRequest {
|
|
411
|
+
workspace: self.event_workspace.as_deref(),
|
|
412
|
+
target_key: &evidence.binding.pane_id,
|
|
413
|
+
operation: "enter_rescue",
|
|
414
|
+
}) {
|
|
415
|
+
Some(lock) => lock,
|
|
416
|
+
None => {
|
|
417
|
+
return Ok(EnterRescueOutcome::NoEffect {
|
|
418
|
+
reason: "input_lock",
|
|
419
|
+
reobserve: true,
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
if !evidence.request.supported() || evidence.phase < NativeInputPhase::Pasted {
|
|
424
|
+
return Ok(EnterRescueOutcome::NoEffect {
|
|
425
|
+
reason: "proof_missing",
|
|
426
|
+
reobserve: false,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
let frame = self.native_frame(evidence)?;
|
|
430
|
+
if frame.accepted(evidence) {
|
|
431
|
+
return Ok(EnterRescueOutcome::Consumed);
|
|
432
|
+
}
|
|
433
|
+
if frame.busy {
|
|
434
|
+
return Ok(EnterRescueOutcome::NoEffect {
|
|
435
|
+
reason: "busy",
|
|
436
|
+
reobserve: true,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
if !frame.pending(evidence) {
|
|
440
|
+
return Ok(EnterRescueOutcome::NoEffect {
|
|
441
|
+
reason: "residue_mismatch",
|
|
442
|
+
reobserve: false,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
self.stable_frame(evidence, false)?;
|
|
446
|
+
if !authorize() {
|
|
447
|
+
return Ok(EnterRescueOutcome::NoEffect {
|
|
448
|
+
reason: "canonical_changed",
|
|
449
|
+
reobserve: false,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
let mut current = evidence.clone();
|
|
453
|
+
current.phase = NativeInputPhase::Seen;
|
|
454
|
+
self.native_record(¤t)?;
|
|
455
|
+
self.native_submit(&mut current, observer)?;
|
|
456
|
+
let accepted = self.observe_native_acceptance(¤t)?;
|
|
457
|
+
if accepted {
|
|
458
|
+
current.phase = NativeInputPhase::Accepted;
|
|
459
|
+
self.native_record(¤t)?;
|
|
460
|
+
}
|
|
461
|
+
Ok(EnterRescueOutcome::Entered { accepted })
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
fn owned_buffer(evidence: &NativeInputEvidence) -> String {
|
|
466
|
+
format!(
|
|
467
|
+
"ta-owned-{}-{}",
|
|
468
|
+
evidence.request.message_id,
|
|
469
|
+
digest(&format!(
|
|
470
|
+
"{}:{}:{}",
|
|
471
|
+
evidence.binding.binding_nonce, evidence.binding.pane_id, evidence.payload_sha256
|
|
472
|
+
))
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
fn capsule_labels(provider: Provider, text: &str) -> Vec<String> {
|
|
477
|
+
if provider == Provider::Pi {
|
|
478
|
+
return text
|
|
479
|
+
.match_indices("[paste #")
|
|
480
|
+
.filter_map(|(start, _)| {
|
|
481
|
+
let end = text[start..].find(']')? + start;
|
|
482
|
+
pi_paste_capsule(&text[start..=end]).map(str::to_owned)
|
|
483
|
+
})
|
|
484
|
+
.collect();
|
|
485
|
+
}
|
|
486
|
+
text.lines()
|
|
487
|
+
.filter_map(|line| {
|
|
488
|
+
let start = line.find("[Pasted text #")?;
|
|
489
|
+
let end = line[start..].find(']')? + start;
|
|
490
|
+
let label = &line[start..=end];
|
|
491
|
+
(label.len() <= 80).then(|| label.to_string())
|
|
492
|
+
})
|
|
493
|
+
.collect()
|
|
494
|
+
}
|