ace-herdr 0.11.1 → 0.11.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/ace/herdr/native_source/guarded-prompt.patch +475 -7
- data/lib/ace/herdr/native_source/selection.json +4 -4
- data/lib/ace/herdr/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 327da1e07e403516ce5ad886cf4ad3954f71a5688d025a1995f584c8dbc1cd76
|
|
4
|
+
data.tar.gz: 4c444095327d49b82f13f5fccb7cfc2e88d87f625e56acd940c6facb3ce9c1a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2bbb0087de71da48121519600864ae1c43ad555cfc33095016b71bc1c05e6c3a7a4f5f4533110a025d6f613f42f69c79c68ed1c04a8e02074e947cd8edcafdb3
|
|
7
|
+
data.tar.gz: 5cf72643c36c6bd00e670ecfe9a4ed9039dd87476e755b98759818b1e8fe5d45bf4f2912e4903f63e722c2232338b48f33cb222b39092f18c356c982743a7402
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.2] - 2026-11-12
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Pin native handoff schema 2 retaining terminal IDs and guarded process handles with close-on-exec descriptors.
|
|
15
|
+
|
|
16
|
+
|
|
10
17
|
## [0.11.1] - 2026-11-12
|
|
11
18
|
|
|
12
19
|
### Fixed
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
From 7a2e78b4e92d2b694bd8a06c1ef4943915c399b2 Mon Sep 17 00:00:00 2001
|
|
2
2
|
From: Miguel <mc@cs3b.com>
|
|
3
3
|
Date: Wed, 7 Oct 2026 00:43:20 +0100
|
|
4
|
-
Subject: [PATCH 1/
|
|
4
|
+
Subject: [PATCH 1/8] feat: guard and permanently drain original-terminal input
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
docs/next/api/herdr-api.schema.json | 338 ++++++
|
|
@@ -3498,7 +3498,7 @@ index 9171340..a971ef6 100644
|
|
|
3498
3498
|
From 5d98e178453958691f17b58d26fd20b51a985cd7 Mon Sep 17 00:00:00 2001
|
|
3499
3499
|
From: Miguel <mc@cs3b.com>
|
|
3500
3500
|
Date: Wed, 4 Nov 2026 02:57:12 +0000
|
|
3501
|
-
Subject: [PATCH 2/
|
|
3501
|
+
Subject: [PATCH 2/8] fix: validate Pi report acknowledgements before accepting
|
|
3502
3502
|
delivery
|
|
3503
3503
|
|
|
3504
3504
|
---
|
|
@@ -3647,7 +3647,7 @@ index 673225a..546289d 100644
|
|
|
3647
3647
|
From b6d67f318c260b3c0efcbef89bf1034544852776 Mon Sep 17 00:00:00 2001
|
|
3648
3648
|
From: Miguel <mc@cs3b.com>
|
|
3649
3649
|
Date: Wed, 4 Nov 2026 04:17:44 +0000
|
|
3650
|
-
Subject: [PATCH 3/
|
|
3650
|
+
Subject: [PATCH 3/8] fix: resolve agent queries by immutable terminal identity
|
|
3651
3651
|
|
|
3652
3652
|
---
|
|
3653
3653
|
src/app/terminal_targets.rs | 35 +++++++++++++++++++++++++++++++++++
|
|
@@ -3713,7 +3713,7 @@ index 658eeaa..003c30c 100644
|
|
|
3713
3713
|
From 93e768dcacbfd63171957935ae9e2637a91689e6 Mon Sep 17 00:00:00 2001
|
|
3714
3714
|
From: Miguel <mc@cs3b.com>
|
|
3715
3715
|
Date: Tue, 10 Nov 2026 22:59:15 +0000
|
|
3716
|
-
Subject: [PATCH 4/
|
|
3716
|
+
Subject: [PATCH 4/8] feat: launch ordinary agents with private explicit
|
|
3717
3717
|
environments
|
|
3718
3718
|
|
|
3719
3719
|
---
|
|
@@ -4804,7 +4804,7 @@ index 0000000..afe8c3a
|
|
|
4804
4804
|
From 83939be994e8b911140adbb9ba64aa4858cce96b Mon Sep 17 00:00:00 2001
|
|
4805
4805
|
From: Miguel <mc@cs3b.com>
|
|
4806
4806
|
Date: Tue, 10 Nov 2026 23:06:09 +0000
|
|
4807
|
-
Subject: [PATCH 5/
|
|
4807
|
+
Subject: [PATCH 5/8] fix: require fresh environments on cold agent restoration
|
|
4808
4808
|
|
|
4809
4809
|
---
|
|
4810
4810
|
src/app/agents.rs | 3 ++
|
|
@@ -4919,7 +4919,7 @@ index b6d6187..66ae240 100644
|
|
|
4919
4919
|
From f475b86b1fc0a72103fbdf5cc0aba7dfc54cdcb4 Mon Sep 17 00:00:00 2001
|
|
4920
4920
|
From: Miguel <mc@cs3b.com>
|
|
4921
4921
|
Date: Tue, 10 Nov 2026 23:16:46 +0000
|
|
4922
|
-
Subject: [PATCH 6/
|
|
4922
|
+
Subject: [PATCH 6/8] fix: clean ordinary session credentials when agents exit
|
|
4923
4923
|
|
|
4924
4924
|
---
|
|
4925
4925
|
src/persist/restore.rs | 13 +--
|
|
@@ -5271,7 +5271,7 @@ index afe8c3a..9dedb83 100644
|
|
|
5271
5271
|
From 5176d899a567985490682c37eb7f0198eab159b0 Mon Sep 17 00:00:00 2001
|
|
5272
5272
|
From: Miguel <mc@cs3b.com>
|
|
5273
5273
|
Date: Wed, 11 Nov 2026 23:46:22 +0000
|
|
5274
|
-
Subject: [PATCH 7/
|
|
5274
|
+
Subject: [PATCH 7/8] fix: preserve child options in explicit environment
|
|
5275
5275
|
launcher
|
|
5276
5276
|
|
|
5277
5277
|
---
|
|
@@ -5346,3 +5346,471 @@ index 9dedb83..64d4645 100644
|
|
|
5346
5346
|
--
|
|
5347
5347
|
2.54.0 (Apple Git-157)
|
|
5348
5348
|
|
|
5349
|
+
|
|
5350
|
+
From 84753e6b9955fac56a316e1e99d3cbe7219fad9b Mon Sep 17 00:00:00 2001
|
|
5351
|
+
From: Miguel <mc@cs3b.com>
|
|
5352
|
+
Date: Thu, 12 Nov 2026 11:55:39 +0000
|
|
5353
|
+
Subject: [PATCH 8/8] fix: preserve terminal identity and origin handles across
|
|
5354
|
+
live handoff
|
|
5355
|
+
|
|
5356
|
+
---
|
|
5357
|
+
src/handoff_runtime.rs | 4 ++
|
|
5358
|
+
src/pane.rs | 23 +++++++--
|
|
5359
|
+
src/persist/restore.rs | 16 +++++-
|
|
5360
|
+
src/platform/guarded_prompt.rs | 4 ++
|
|
5361
|
+
src/platform/linux.rs | 2 +-
|
|
5362
|
+
src/platform/linux/guarded_origin.rs | 77 ++++++++++++++++++++++++++++
|
|
5363
|
+
src/platform/mod.rs | 9 ++++
|
|
5364
|
+
src/pty/actor/unix.rs | 4 ++
|
|
5365
|
+
src/server/handoff.rs | 22 +++++++-
|
|
5366
|
+
src/server/headless/bootstrap.rs | 11 +++-
|
|
5367
|
+
src/server/headless/lifecycle.rs | 8 ++-
|
|
5368
|
+
src/terminal/runtime.rs | 8 ++-
|
|
5369
|
+
12 files changed, 174 insertions(+), 14 deletions(-)
|
|
5370
|
+
|
|
5371
|
+
diff --git a/src/handoff_runtime.rs b/src/handoff_runtime.rs
|
|
5372
|
+
index 12481f6..c85134d 100644
|
|
5373
|
+
--- a/src/handoff_runtime.rs
|
|
5374
|
+
+++ b/src/handoff_runtime.rs
|
|
5375
|
+
@@ -12,6 +12,8 @@ use serde::{Deserialize, Serialize};
|
|
5376
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
5377
|
+
pub(crate) struct HandoffRuntimeState {
|
|
5378
|
+
pub pane_id: u32,
|
|
5379
|
+
+ pub terminal_id: crate::terminal::TerminalId,
|
|
5380
|
+
+ pub guarded_origin: Option<crate::platform::guarded_prompt::GuardedPromptOrigin>,
|
|
5381
|
+
pub child_pid: u32,
|
|
5382
|
+
pub rows: u16,
|
|
5383
|
+
pub cols: u16,
|
|
5384
|
+
@@ -44,5 +46,7 @@ pub(crate) struct ImportedHandoffRuntime {
|
|
5385
|
+
#[cfg(unix)]
|
|
5386
|
+
pub master_fd: std::os::fd::RawFd,
|
|
5387
|
+
#[cfg(unix)]
|
|
5388
|
+
+ pub origin_fd: Option<std::os::fd::OwnedFd>,
|
|
5389
|
+
+ #[cfg(unix)]
|
|
5390
|
+
pub state: HandoffRuntimeState,
|
|
5391
|
+
}
|
|
5392
|
+
diff --git a/src/pane.rs b/src/pane.rs
|
|
5393
|
+
index eebd3ef..08246c1 100644
|
|
5394
|
+
--- a/src/pane.rs
|
|
5395
|
+
+++ b/src/pane.rs
|
|
5396
|
+
@@ -2148,11 +2148,14 @@ impl PaneRuntime {
|
|
5397
|
+
pub fn handoff_runtime_state(
|
|
5398
|
+
&self,
|
|
5399
|
+
pane_id: u32,
|
|
5400
|
+
+ terminal_id: crate::terminal::TerminalId,
|
|
5401
|
+
) -> crate::handoff_runtime::HandoffRuntimeState {
|
|
5402
|
+
let child_pid = self.child_pid.load(Ordering::Acquire);
|
|
5403
|
+
let (rows, cols, cell_width_px, cell_height_px) = self.current_size.get();
|
|
5404
|
+
crate::handoff_runtime::HandoffRuntimeState {
|
|
5405
|
+
pane_id,
|
|
5406
|
+
+ guarded_origin: self.captured_input_origin(terminal_id.as_str()),
|
|
5407
|
+
+ terminal_id,
|
|
5408
|
+
child_pid,
|
|
5409
|
+
rows,
|
|
5410
|
+
cols,
|
|
5411
|
+
@@ -2364,9 +2367,11 @@ impl PaneRuntime {
|
|
5412
|
+
render_notify: Arc<Notify>,
|
|
5413
|
+
render_dirty: Arc<RenderSignal>,
|
|
5414
|
+
) -> std::io::Result<Self> {
|
|
5415
|
+
- let crate::handoff_runtime::ImportedHandoffRuntime { master_fd, state } = import;
|
|
5416
|
+
+ let crate::handoff_runtime::ImportedHandoffRuntime { master_fd, origin_fd, state } = import;
|
|
5417
|
+
let crate::handoff_runtime::HandoffRuntimeState {
|
|
5418
|
+
pane_id,
|
|
5419
|
+
+ guarded_origin,
|
|
5420
|
+
+ terminal_id,
|
|
5421
|
+
child_pid,
|
|
5422
|
+
rows,
|
|
5423
|
+
cols,
|
|
5424
|
+
@@ -2381,8 +2386,9 @@ impl PaneRuntime {
|
|
5425
|
+
} = state;
|
|
5426
|
+
let pane_id = PaneId::from_raw(pane_id);
|
|
5427
|
+
use std::os::fd::FromRawFd;
|
|
5428
|
+
-
|
|
5429
|
+
let master_fd = unsafe { std::os::fd::OwnedFd::from_raw_fd(master_fd) };
|
|
5430
|
+
+ let origin = crate::platform::import_guarded_child(guarded_origin, origin_fd)?;
|
|
5431
|
+
+ let _ = terminal_id;
|
|
5432
|
+
|
|
5433
|
+
let (response_tx, _response_rx) = mpsc::channel::<Bytes>(1);
|
|
5434
|
+
let mut terminal = crate::ghostty::Terminal::new(cols, rows, scrollback_limit_bytes)
|
|
5435
|
+
@@ -2494,7 +2500,7 @@ impl PaneRuntime {
|
|
5436
|
+
});
|
|
5437
|
+
PaneRuntimeIo::Actor(PtyIoActor::spawn(PtyIoActorConfig {
|
|
5438
|
+
pane_id: pane_id.raw(),
|
|
5439
|
+
- origin: None,
|
|
5440
|
+
+ origin,
|
|
5441
|
+
master_fd,
|
|
5442
|
+
initially_quiesced: true,
|
|
5443
|
+
on_read,
|
|
5444
|
+
@@ -3531,6 +3537,15 @@ impl PaneRuntime {
|
|
5445
|
+
self.io.try_send_bytes(bytes)
|
|
5446
|
+
}
|
|
5447
|
+
|
|
5448
|
+
+ #[cfg(unix)]
|
|
5449
|
+
+ pub(crate) fn duplicate_guarded_origin_fd(&self) -> std::io::Result<Option<std::os::fd::OwnedFd>> {
|
|
5450
|
+
+ match &self.io {
|
|
5451
|
+
+ PaneRuntimeIo::Actor(actor) => actor.duplicate_guarded_origin_fd(),
|
|
5452
|
+
+ #[cfg(test)]
|
|
5453
|
+
+ PaneRuntimeIo::TestChannel { .. } => Ok(None),
|
|
5454
|
+
+ }
|
|
5455
|
+
+ }
|
|
5456
|
+
+
|
|
5457
|
+
pub(crate) fn guarded_prompt_origin(
|
|
5458
|
+
&self,
|
|
5459
|
+
terminal_id: &str,
|
|
5460
|
+
@@ -4962,7 +4977,7 @@ mod tests {
|
|
5461
|
+
runtime.test_process_pty_bytes("\x1b]2;✳ 修复🙂标题\x1b\\".as_bytes());
|
|
5462
|
+
runtime.terminal.clear_agent_osc_state();
|
|
5463
|
+
assert_eq!(runtime.agent_osc_title(), "");
|
|
5464
|
+
- let pane = runtime.handoff_runtime_state(12);
|
|
5465
|
+
+ let pane = runtime.handoff_runtime_state(12, crate::terminal::TerminalId::alloc());
|
|
5466
|
+
|
|
5467
|
+
assert_eq!(pane.keyboard_protocol_flags, 5);
|
|
5468
|
+
assert_eq!(pane.terminal_title.as_deref(), Some("✳ 修复🙂标题"));
|
|
5469
|
+
diff --git a/src/persist/restore.rs b/src/persist/restore.rs
|
|
5470
|
+
index 3b0ca57..1374ff6 100644
|
|
5471
|
+
--- a/src/persist/restore.rs
|
|
5472
|
+
+++ b/src/persist/restore.rs
|
|
5473
|
+
@@ -581,6 +581,10 @@ fn restore_tab(
|
|
5474
|
+
let imported_runtime = old_pane_id.and_then(|old_id| imported_panes.remove(&old_id));
|
|
5475
|
+
let was_imported = imported_runtime.is_some();
|
|
5476
|
+
#[cfg(unix)]
|
|
5477
|
+
+ let imported_terminal_id = imported_runtime.as_ref().map(|imported| imported.state.terminal_id.clone());
|
|
5478
|
+
+ #[cfg(not(unix))]
|
|
5479
|
+
+ let imported_terminal_id: Option<TerminalId> = None;
|
|
5480
|
+
+ #[cfg(unix)]
|
|
5481
|
+
let handoff_agent_state = imported_runtime
|
|
5482
|
+
.as_ref()
|
|
5483
|
+
.and_then(|imported| imported.state.agent_state.clone());
|
|
5484
|
+
@@ -637,6 +641,7 @@ fn restore_tab(
|
|
5485
|
+
TerminalRuntime::from_handoff_fd(
|
|
5486
|
+
crate::handoff_runtime::ImportedHandoffRuntime {
|
|
5487
|
+
master_fd: imported.master_fd,
|
|
5488
|
+
+ origin_fd: imported.origin_fd,
|
|
5489
|
+
state: imported.state.with_pane_id(*id),
|
|
5490
|
+
},
|
|
5491
|
+
runtime_context.scrollback_limit_bytes,
|
|
5492
|
+
@@ -686,7 +691,7 @@ fn restore_tab(
|
|
5493
|
+
|
|
5494
|
+
match runtime_result {
|
|
5495
|
+
Ok(runtime) => {
|
|
5496
|
+
- let terminal_id = TerminalId::alloc();
|
|
5497
|
+
+ let terminal_id = imported_terminal_id.unwrap_or_else(TerminalId::alloc);
|
|
5498
|
+
let mut terminal = TerminalState::new(terminal_id.clone(), cwd.clone());
|
|
5499
|
+
if was_imported {
|
|
5500
|
+
if let Some(argv) = saved_launch_argv {
|
|
5501
|
+
@@ -1957,13 +1962,15 @@ mod tests {
|
|
5502
|
+
runtime
|
|
5503
|
+
.pause_handoff_reader(std::time::Duration::from_secs(2))
|
|
5504
|
+
.unwrap();
|
|
5505
|
+
- let mut state = runtime.handoff_runtime_state(pane_id.raw());
|
|
5506
|
+
+ let original_terminal_id = terminals.keys().next().unwrap().clone();
|
|
5507
|
+
+ let mut state = runtime.handoff_runtime_state(pane_id.raw(), original_terminal_id.clone());
|
|
5508
|
+
state.agent_state = terminals.values().next().unwrap().handoff_agent_state();
|
|
5509
|
+
let state = serde_json::from_value(serde_json::to_value(state).unwrap()).unwrap();
|
|
5510
|
+
let mut imports = HashMap::from([(
|
|
5511
|
+
pane_id.raw(),
|
|
5512
|
+
crate::handoff_runtime::ImportedHandoffRuntime {
|
|
5513
|
+
master_fd: runtime.duplicate_handoff_fd().unwrap(),
|
|
5514
|
+
+ origin_fd: runtime.duplicate_guarded_origin_fd().unwrap(),
|
|
5515
|
+
state,
|
|
5516
|
+
},
|
|
5517
|
+
)]);
|
|
5518
|
+
@@ -1978,6 +1985,11 @@ mod tests {
|
|
5519
|
+
Arc::new(RenderSignal::new()),
|
|
5520
|
+
)
|
|
5521
|
+
.unwrap();
|
|
5522
|
+
+ assert!(restored_terminals.contains_key(&original_terminal_id));
|
|
5523
|
+
+ assert_ne!(TerminalId::alloc(), original_terminal_id);
|
|
5524
|
+
+ let restored_runtime = restored_runtimes.get(&original_terminal_id).unwrap();
|
|
5525
|
+
+ assert_eq!(restored_runtime.guarded_prompt_origin(original_terminal_id.as_str()),
|
|
5526
|
+
+ runtime.guarded_prompt_origin(original_terminal_id.as_str()));
|
|
5527
|
+
drop(restored_runtimes);
|
|
5528
|
+
drop(runtimes);
|
|
5529
|
+
let terminal = restored_terminals.values_mut().next().unwrap();
|
|
5530
|
+
diff --git a/src/platform/guarded_prompt.rs b/src/platform/guarded_prompt.rs
|
|
5531
|
+
index e35245d..ca14377 100644
|
|
5532
|
+
--- a/src/platform/guarded_prompt.rs
|
|
5533
|
+
+++ b/src/platform/guarded_prompt.rs
|
|
5534
|
+
@@ -103,6 +103,10 @@ impl GuardedPromptError {
|
|
5535
|
+
|
|
5536
|
+
pub(crate) trait OriginLiveness: Send + Sync {
|
|
5537
|
+
fn is_live(&self) -> std::io::Result<bool>;
|
|
5538
|
+
+ #[cfg(unix)]
|
|
5539
|
+
+ fn duplicate_fd(&self) -> std::io::Result<std::os::fd::OwnedFd> {
|
|
5540
|
+
+ Err(std::io::Error::other("origin handle cannot be transferred"))
|
|
5541
|
+
+ }
|
|
5542
|
+
}
|
|
5543
|
+
|
|
5544
|
+
pub(crate) struct GuardedChildOrigin {
|
|
5545
|
+
diff --git a/src/platform/linux.rs b/src/platform/linux.rs
|
|
5546
|
+
index 9edf05b..2298846 100644
|
|
5547
|
+
--- a/src/platform/linux.rs
|
|
5548
|
+
+++ b/src/platform/linux.rs
|
|
5549
|
+
@@ -27,7 +27,7 @@ pub(crate) use super::unix_common::{
|
|
5550
|
+
mod config_file_tests;
|
|
5551
|
+
|
|
5552
|
+
mod guarded_origin;
|
|
5553
|
+
-pub(crate) use guarded_origin::capture_guarded_child;
|
|
5554
|
+
+pub(crate) use guarded_origin::{capture_guarded_child, import_guarded_child};
|
|
5555
|
+
|
|
5556
|
+
mod shutdown;
|
|
5557
|
+
pub(crate) use shutdown::monitor_host_shutdown;
|
|
5558
|
+
diff --git a/src/platform/linux/guarded_origin.rs b/src/platform/linux/guarded_origin.rs
|
|
5559
|
+
index 2a44e8f..ffa6b49 100644
|
|
5560
|
+
--- a/src/platform/linux/guarded_origin.rs
|
|
5561
|
+
+++ b/src/platform/linux/guarded_origin.rs
|
|
5562
|
+
@@ -10,6 +10,7 @@ use std::{
|
|
5563
|
+
|
|
5564
|
+
struct LinuxChildHandle(OwnedFd);
|
|
5565
|
+
impl OriginLiveness for LinuxChildHandle {
|
|
5566
|
+
+ fn duplicate_fd(&self) -> io::Result<OwnedFd> { self.0.try_clone() }
|
|
5567
|
+
fn is_live(&self) -> io::Result<bool> {
|
|
5568
|
+
let mut item = libc::pollfd {
|
|
5569
|
+
fd: self.0.as_raw_fd(),
|
|
5570
|
+
@@ -85,3 +86,79 @@ pub(crate) fn capture_guarded_child(pid: u32) -> io::Result<Arc<GuardedChildOrig
|
|
5571
|
+
handle: Arc::new(handle),
|
|
5572
|
+
}))
|
|
5573
|
+
}
|
|
5574
|
+
+
|
|
5575
|
+
+// Import the original kernel handle, never reopen a PID from a snapshot.
|
|
5576
|
+
+pub(crate) fn import_guarded_child(
|
|
5577
|
+
+ expected: Option<crate::platform::guarded_prompt::GuardedPromptOrigin>,
|
|
5578
|
+
+ fd: Option<OwnedFd>,
|
|
5579
|
+
+) -> io::Result<Option<Arc<GuardedChildOrigin>>> {
|
|
5580
|
+
+ match (expected, fd) {
|
|
5581
|
+
+ (None, None) => Ok(None),
|
|
5582
|
+
+ (Some(expected), Some(fd)) => {
|
|
5583
|
+
+ if !expected.valid() { return Err(io::Error::other("invalid handoff origin")); }
|
|
5584
|
+
+ if unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) } == -1 {
|
|
5585
|
+
+ return Err(io::Error::last_os_error());
|
|
5586
|
+
+ }
|
|
5587
|
+
+ let handle = LinuxChildHandle(fd);
|
|
5588
|
+
+ let live = handle.is_live()?;
|
|
5589
|
+
+ let same_process = if live {
|
|
5590
|
+
+ let mut current = identity(expected.child.pid)?;
|
|
5591
|
+
+ // Handoff reparents the surviving child when the old server exits.
|
|
5592
|
+
+ // The original parent remains provenance, not a liveness condition.
|
|
5593
|
+
+ current.parent_pid = expected.child.parent_pid;
|
|
5594
|
+
+ current == expected.child
|
|
5595
|
+
+ } else { true };
|
|
5596
|
+
+ if !same_process {
|
|
5597
|
+
+ return Err(io::Error::other("handoff child identity mismatch"));
|
|
5598
|
+
+ }
|
|
5599
|
+
+ Ok(Some(Arc::new(GuardedChildOrigin {
|
|
5600
|
+
+ child: expected.child,
|
|
5601
|
+
+ runtime_incarnation: expected.runtime_incarnation,
|
|
5602
|
+
+ exited: AtomicBool::new(!live),
|
|
5603
|
+
+ handle: Arc::new(handle),
|
|
5604
|
+
+ })))
|
|
5605
|
+
+ }
|
|
5606
|
+
+ _ => Err(io::Error::other("handoff origin descriptor mismatch")),
|
|
5607
|
+
+ }
|
|
5608
|
+
+}
|
|
5609
|
+
+
|
|
5610
|
+
+#[cfg(test)]
|
|
5611
|
+
+mod tests {
|
|
5612
|
+
+ use super::*;
|
|
5613
|
+
+ use std::process::Command;
|
|
5614
|
+
+
|
|
5615
|
+
+ #[test]
|
|
5616
|
+
+ fn handoff_transfers_original_handle_and_survives_parent_change() {
|
|
5617
|
+
+ let mut child = Command::new("sleep").arg("30").spawn().unwrap();
|
|
5618
|
+
+ let origin = capture_guarded_child(child.id()).unwrap();
|
|
5619
|
+
+ let mut expected = origin.projection("term_ab12");
|
|
5620
|
+
+ // A previous server was the spawn parent; reparenting must not rewrite
|
|
5621
|
+
+ // that provenance or invalidate the still-live original pidfd.
|
|
5622
|
+
+ expected.child.parent_pid = expected.child.parent_pid.saturating_add(1);
|
|
5623
|
+
+ let fd = origin.handle.duplicate_fd().unwrap();
|
|
5624
|
+
+ let imported_fd = fd.as_raw_fd();
|
|
5625
|
+
+ assert_eq!(unsafe { libc::fcntl(imported_fd, libc::F_SETFD, 0) }, 0);
|
|
5626
|
+
+ let imported = import_guarded_child(Some(expected.clone()), Some(fd)).unwrap().unwrap();
|
|
5627
|
+
+ assert_ne!(unsafe { libc::fcntl(imported_fd, libc::F_GETFD) } & libc::FD_CLOEXEC, 0);
|
|
5628
|
+
+ assert_eq!(imported.projection("term_ab12"), expected);
|
|
5629
|
+
+ assert!(imported.check(&expected).is_ok());
|
|
5630
|
+
+ let fd = imported.handle.duplicate_fd().unwrap();
|
|
5631
|
+
+ assert_ne!(unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFD) } & libc::FD_CLOEXEC, 0);
|
|
5632
|
+
+ let again = import_guarded_child(Some(expected.clone()), Some(fd)).unwrap().unwrap();
|
|
5633
|
+
+ child.kill().unwrap();
|
|
5634
|
+
+ child.wait().unwrap();
|
|
5635
|
+
+ assert!(again.check(&expected).is_err());
|
|
5636
|
+
+ }
|
|
5637
|
+
+
|
|
5638
|
+
+ #[test]
|
|
5639
|
+
+ fn handoff_rejects_changed_process_birth() {
|
|
5640
|
+
+ let mut child = Command::new("sleep").arg("30").spawn().unwrap();
|
|
5641
|
+
+ let origin = capture_guarded_child(child.id()).unwrap();
|
|
5642
|
+
+ let mut expected = origin.projection("term_ab12");
|
|
5643
|
+
+ expected.child.started_at.push('1');
|
|
5644
|
+
+ let result = import_guarded_child(Some(expected), Some(origin.handle.duplicate_fd().unwrap()));
|
|
5645
|
+
+ child.kill().unwrap();
|
|
5646
|
+
+ child.wait().unwrap();
|
|
5647
|
+
+ assert!(result.is_err());
|
|
5648
|
+
+ }
|
|
5649
|
+
+}
|
|
5650
|
+
diff --git a/src/platform/mod.rs b/src/platform/mod.rs
|
|
5651
|
+
index 2470ff5..43faa74 100644
|
|
5652
|
+
--- a/src/platform/mod.rs
|
|
5653
|
+
+++ b/src/platform/mod.rs
|
|
5654
|
+
@@ -740,3 +740,12 @@ pub(crate) fn capture_guarded_child(
|
|
5655
|
+
"guarded child origin requires Linux",
|
|
5656
|
+
))
|
|
5657
|
+
}
|
|
5658
|
+
+
|
|
5659
|
+
+#[cfg(all(unix, not(target_os = "linux")))]
|
|
5660
|
+
+pub(crate) fn import_guarded_child(
|
|
5661
|
+
+ origin: Option<guarded_prompt::GuardedPromptOrigin>,
|
|
5662
|
+
+ fd: Option<std::os::fd::OwnedFd>,
|
|
5663
|
+
+) -> std::io::Result<Option<std::sync::Arc<guarded_prompt::GuardedChildOrigin>>> {
|
|
5664
|
+
+ if origin.is_some() || fd.is_some() { return Err(std::io::Error::other("origin import requires Linux")); }
|
|
5665
|
+
+ Ok(None)
|
|
5666
|
+
+}
|
|
5667
|
+
diff --git a/src/pty/actor/unix.rs b/src/pty/actor/unix.rs
|
|
5668
|
+
index 7740265..cbe4203 100644
|
|
5669
|
+
--- a/src/pty/actor/unix.rs
|
|
5670
|
+
+++ b/src/pty/actor/unix.rs
|
|
5671
|
+
@@ -260,6 +260,10 @@ impl PtyIoActorHandle {
|
|
5672
|
+
Ok(result)
|
|
5673
|
+
}
|
|
5674
|
+
|
|
5675
|
+
+ pub(crate) fn duplicate_guarded_origin_fd(&self) -> std::io::Result<Option<std::os::fd::OwnedFd>> {
|
|
5676
|
+
+ self.origin.as_ref().map(|origin| origin.handle.duplicate_fd()).transpose()
|
|
5677
|
+
+ }
|
|
5678
|
+
+
|
|
5679
|
+
pub(crate) fn guarded_origin(&self, terminal_id: &str) -> Option<GuardedPromptOrigin> {
|
|
5680
|
+
let origin = self.origin.as_ref()?;
|
|
5681
|
+
let expected = origin.projection(terminal_id);
|
|
5682
|
+
diff --git a/src/server/handoff.rs b/src/server/handoff.rs
|
|
5683
|
+
index aaa87f3..490fd63 100644
|
|
5684
|
+
--- a/src/server/handoff.rs
|
|
5685
|
+
+++ b/src/server/handoff.rs
|
|
5686
|
+
@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
|
|
5687
|
+
use tracing::{info, warn};
|
|
5688
|
+
|
|
5689
|
+
#[cfg(unix)]
|
|
5690
|
+
-const HANDOFF_VERSION: u32 = 1;
|
|
5691
|
+
+const HANDOFF_VERSION: u32 = 2;
|
|
5692
|
+
#[cfg(unix)]
|
|
5693
|
+
const READY_TIMEOUT: Duration = Duration::from_secs(30);
|
|
5694
|
+
#[cfg(unix)]
|
|
5695
|
+
@@ -262,9 +262,18 @@ pub(crate) fn receive(socket_path: &Path, token: &str) -> io::Result<ReceivedHan
|
|
5696
|
+
crate::build_info::version()
|
|
5697
|
+
)));
|
|
5698
|
+
}
|
|
5699
|
+
+ let mut terminals = std::collections::HashSet::new();
|
|
5700
|
+
+ let mut panes = std::collections::HashSet::new();
|
|
5701
|
+
+ for pane in &manifest.panes {
|
|
5702
|
+
+ if !terminals.insert(&pane.terminal_id) || !panes.insert(pane.pane_id) ||
|
|
5703
|
+
+ pane.guarded_origin.as_ref().is_some_and(|origin| !origin.valid() || origin.terminal_id != pane.terminal_id.as_str() || origin.child.pid != pane.child_pid) {
|
|
5704
|
+
+ return Err(io::Error::other("invalid or duplicate handoff terminal identity"));
|
|
5705
|
+
+ }
|
|
5706
|
+
+ }
|
|
5707
|
+
stream.write_all(b"validated\n")?;
|
|
5708
|
+
stream.flush()?;
|
|
5709
|
+
- let fds = recv_fds(&stream, manifest.panes.len())?;
|
|
5710
|
+
+ let count = manifest.panes.iter().map(|pane| 1 + usize::from(pane.guarded_origin.is_some())).sum();
|
|
5711
|
+
+ let fds = recv_fds(&stream, count)?;
|
|
5712
|
+
Ok(ReceivedHandoff {
|
|
5713
|
+
manifest,
|
|
5714
|
+
fds,
|
|
5715
|
+
@@ -517,6 +526,15 @@ fn recv_fd_batch(stream: &UnixStream, wanted: usize) -> io::Result<Vec<RawFd>> {
|
|
5716
|
+
if out.is_empty() {
|
|
5717
|
+
return Err(io::Error::other("handoff fd message missing SCM_RIGHTS"));
|
|
5718
|
+
}
|
|
5719
|
+
+ // SCM_RIGHTS does not preserve close-on-exec. Protect every received
|
|
5720
|
+
+ // handle before restoration can spawn any replacement shell.
|
|
5721
|
+
+ for fd in &out {
|
|
5722
|
+
+ if unsafe { libc::fcntl(*fd, libc::F_SETFD, libc::FD_CLOEXEC) } == -1 {
|
|
5723
|
+
+ let error = io::Error::last_os_error();
|
|
5724
|
+
+ close_raw_fds(&out);
|
|
5725
|
+
+ return Err(error);
|
|
5726
|
+
+ }
|
|
5727
|
+
+ }
|
|
5728
|
+
Ok(out)
|
|
5729
|
+
}
|
|
5730
|
+
|
|
5731
|
+
diff --git a/src/server/headless/bootstrap.rs b/src/server/headless/bootstrap.rs
|
|
5732
|
+
index e5ff400..183636f 100644
|
|
5733
|
+
--- a/src/server/headless/bootstrap.rs
|
|
5734
|
+
+++ b/src/server/headless/bootstrap.rs
|
|
5735
|
+
@@ -136,12 +136,19 @@ fn run_handoff_import_server(socket_path: &Path, token: &str) -> io::Result<()>
|
|
5736
|
+
let should_quit = Arc::new(AtomicBool::new(false));
|
|
5737
|
+
|
|
5738
|
+
let mut imports = HashMap::new();
|
|
5739
|
+
- for (pane, fd) in received.manifest.panes.into_iter().zip(received.fds) {
|
|
5740
|
+
+ use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd};
|
|
5741
|
+
+ let mut fds = received.fds.into_iter().map(|fd| unsafe { OwnedFd::from_raw_fd(fd) });
|
|
5742
|
+
+ for pane in received.manifest.panes {
|
|
5743
|
+
+ let fd = fds.next().ok_or_else(|| io::Error::other("missing handoff PTY"))?;
|
|
5744
|
+
+ let origin_fd = if pane.guarded_origin.is_some() {
|
|
5745
|
+
+ Some(fds.next().ok_or_else(|| io::Error::other("missing handoff origin"))?)
|
|
5746
|
+
+ } else { None };
|
|
5747
|
+
let pane_id = pane.pane_id;
|
|
5748
|
+
imports.insert(
|
|
5749
|
+
pane_id,
|
|
5750
|
+
crate::handoff_runtime::ImportedHandoffRuntime {
|
|
5751
|
+
- master_fd: fd,
|
|
5752
|
+
+ master_fd: fd.into_raw_fd(),
|
|
5753
|
+
+ origin_fd,
|
|
5754
|
+
state: pane,
|
|
5755
|
+
},
|
|
5756
|
+
);
|
|
5757
|
+
diff --git a/src/server/headless/lifecycle.rs b/src/server/headless/lifecycle.rs
|
|
5758
|
+
index c700a9d..1a04ef9 100644
|
|
5759
|
+
--- a/src/server/headless/lifecycle.rs
|
|
5760
|
+
+++ b/src/server/headless/lifecycle.rs
|
|
5761
|
+
@@ -82,7 +82,7 @@ impl HeadlessServer {
|
|
5762
|
+
let Some(pane_id) = pane_by_terminal.get(terminal_id).copied() else {
|
|
5763
|
+
continue;
|
|
5764
|
+
};
|
|
5765
|
+
- let mut handoff_runtime = runtime.handoff_runtime_state(pane_id);
|
|
5766
|
+
+ let mut handoff_runtime = runtime.handoff_runtime_state(pane_id, terminal_id.clone());
|
|
5767
|
+
handoff_runtime.agent_state = self
|
|
5768
|
+
.app
|
|
5769
|
+
.state
|
|
5770
|
+
@@ -128,11 +128,15 @@ impl HeadlessServer {
|
|
5771
|
+
|
|
5772
|
+
let mut fds = Vec::new();
|
|
5773
|
+
let duplicate_result = (|| {
|
|
5774
|
+
- for (terminal_id, _) in &handoff_entries {
|
|
5775
|
+
+ for (terminal_id, state) in &handoff_entries {
|
|
5776
|
+
let Some(runtime) = self.app.terminal_runtimes.get(terminal_id) else {
|
|
5777
|
+
continue;
|
|
5778
|
+
};
|
|
5779
|
+
fds.push(runtime.duplicate_handoff_fd()?);
|
|
5780
|
+
+ if state.guarded_origin.is_some() {
|
|
5781
|
+
+ use std::os::fd::IntoRawFd;
|
|
5782
|
+
+ fds.push(runtime.duplicate_guarded_origin_fd()?.ok_or_else(|| io::Error::other("handoff origin disappeared"))?.into_raw_fd());
|
|
5783
|
+
+ }
|
|
5784
|
+
}
|
|
5785
|
+
Ok::<(), io::Error>(())
|
|
5786
|
+
})();
|
|
5787
|
+
diff --git a/src/terminal/runtime.rs b/src/terminal/runtime.rs
|
|
5788
|
+
index a971ef6..f80a69b 100644
|
|
5789
|
+
--- a/src/terminal/runtime.rs
|
|
5790
|
+
+++ b/src/terminal/runtime.rs
|
|
5791
|
+
@@ -50,8 +50,9 @@ impl TerminalRuntime {
|
|
5792
|
+
pub fn handoff_runtime_state(
|
|
5793
|
+
&self,
|
|
5794
|
+
pane_id: u32,
|
|
5795
|
+
+ terminal_id: crate::terminal::TerminalId,
|
|
5796
|
+
) -> crate::handoff_runtime::HandoffRuntimeState {
|
|
5797
|
+
- self.0.handoff_runtime_state(pane_id)
|
|
5798
|
+
+ self.0.handoff_runtime_state(pane_id, terminal_id)
|
|
5799
|
+
}
|
|
5800
|
+
|
|
5801
|
+
#[cfg(unix)]
|
|
5802
|
+
@@ -478,6 +479,11 @@ impl TerminalRuntime {
|
|
5803
|
+
self.0.try_send_bytes(bytes)
|
|
5804
|
+
}
|
|
5805
|
+
|
|
5806
|
+
+ #[cfg(unix)]
|
|
5807
|
+
+ pub(crate) fn duplicate_guarded_origin_fd(&self) -> std::io::Result<Option<std::os::fd::OwnedFd>> {
|
|
5808
|
+
+ self.0.duplicate_guarded_origin_fd()
|
|
5809
|
+
+ }
|
|
5810
|
+
+
|
|
5811
|
+
pub(crate) fn guarded_prompt_origin(
|
|
5812
|
+
&self,
|
|
5813
|
+
terminal_id: &str,
|
|
5814
|
+
--
|
|
5815
|
+
2.54.0 (Apple Git-157)
|
|
5816
|
+
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"schema": "ace.herdr.native-source/v1",
|
|
3
3
|
"repository": "https://github.com/herdrdev/herdr.git",
|
|
4
4
|
"baseline_commit": "7b116c05bfda646af39d2524c54e70c751f57ee8",
|
|
5
|
-
"source_commit": "
|
|
6
|
-
"source_tree": "
|
|
5
|
+
"source_commit": "84753e6b9955fac56a316e1e99d3cbe7219fad9b",
|
|
6
|
+
"source_tree": "e948aca926f1511806e03bbea4933c32630122b8",
|
|
7
7
|
"patch_file": "guarded-prompt.patch",
|
|
8
|
-
"patch_sha256": "
|
|
8
|
+
"patch_sha256": "c0cd34f66e90fe98a40a08b127b39172182a03a813e8553fcc7961edc6326936",
|
|
9
9
|
"cargo_lock_sha256": "bb3ab17351af496a2cc316a11c81a97f90959cd37c3d5625f2f8028c0133ffca",
|
|
10
10
|
"rust_toolchain_sha256": "0d9298c075817aba62477d23dcda3534cd836b5d5cf1960bd2de54df675ceffd",
|
|
11
11
|
"rust_version": "1.96.1",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"committer": {
|
|
16
16
|
"name": "Miguel",
|
|
17
17
|
"email": "mc@cs3b.com",
|
|
18
|
-
"date": "2026-11-
|
|
18
|
+
"date": "2026-11-12T11:55:39+00:00"
|
|
19
19
|
},
|
|
20
20
|
"targets": [
|
|
21
21
|
"x86_64-unknown-linux-musl",
|
data/lib/ace/herdr/version.rb
CHANGED