Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,9 @@ and start token verify against the live leader, using the same legacy authority
as explicit controls such as `kill`. Missing, unreadable or mismatching tokens
remain refused; `--force` never bypasses identity verification. This does not
write a synthetic `daemonBootId` or upgrade the session's inventory identity.
A legacy record that already has an exit while its daemon is still finishing
is refused the same way, before any restart prompt: a restart waits for proof
that the daemon is gone, and a live legacy daemon cannot give it.

`run` and `restart` are different and deliberately so: from inside a session
`run` runs the command directly and `restart` restarts without attaching.
Expand Down
4 changes: 4 additions & 0 deletions crates/pty/src/cli/attach.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,11 @@ fn attach_session(
};
// Reuse explicit controls' recorded-PID/token policy for pre-boot-ID
// daemons. This does not upgrade inventory identity or rewrite metadata.
// Only while running: a restart needs proof that the daemon is gone, which
// a live legacy daemon can't give, so an exit it is still finishing is
// refused here rather than offered a restart that would then be refused.
let legacy_verified = session.identity == registry::DaemonIdentityCheck::Unknown
&& session.status == SessionStatus::Running
&& session.metadata.as_ref().is_some_and(|metadata| {
metadata.daemon_boot_id.is_none()
&& registry::read_control_signal_target_with(name, Some(metadata)).is_some()
Expand Down
41 changes: 41 additions & 0 deletions crates/pty/tests/cli_attach_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,47 @@ fn legacy_attach_without_matching_token_never_reaches_transport_even_with_force(
}
}

#[test]
fn legacy_exit_with_a_live_daemon_is_refused_before_the_restart_prompt() {
let rig = Rig::new();
let pid = std::process::id() as i32;
let rerun = rig.scratch.join("rerun");
// The exit is written and the verified legacy daemon is still finishing.
rig.write_meta(
"finishing",
json!({
"generation": "063ff7f9-style",
"daemonPid": pid,
"daemonStartToken": pty_core::registry::read_process_start_token(pid).unwrap(),
"command": "sh",
"args": ["-c", "touch \"$1\"", "rerun", rerun],
"exitedAt": "2026-11-04T00:00:00.000Z",
"exitCode": 0,
}),
);
std::fs::write(rig.path("finishing.pid"), pid.to_string()).unwrap();
let before = std::fs::read(rig.path("finishing.json")).unwrap();
// A closed stdin reads as yes to the prompt, and -r never asks.
for out in [
rig.run(&["attach", "finishing"]),
rig.run_stdin(&["attach", "finishing"], "y\n"),
rig.run(&["attach", "-r", "finishing"]),
rig.run(&["attach", "--force", "finishing"]),
] {
assert_eq!(out.code, 1, "{out:?}");
assert!(
out.stderr
.contains("refusing session \"finishing\": daemon identity is unknown"),
"{out:?}"
);
let shown = format!("{}{}", out.stdout, out.stderr);
assert!(!shown.contains("Restart?"), "{out:?}");
assert!(!shown.contains("exited with code"), "{out:?}");
}
assert!(!rerun.exists());
assert_eq!(std::fs::read(rig.path("finishing.json")).unwrap(), before);
}

#[test]
fn auto_restart_revoked_binding_preserves_retained_artifacts_without_running_child() {
for retained_socket in [false, true] {
Expand Down
Loading