-
Notifications
You must be signed in to change notification settings - Fork 1
eval: refuse dead-session attach without restart #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
myobie
merged 2 commits into
main
from
schickling-assistant/2026-07-30-pty-attach-only-dead-refusal
Jul 30, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # pty-attach-only | ||
|
|
||
| **Type:** pty / lifecycle policy · **Ship:** blocked on | ||
| [compoundingtech/pty#122](https://github.com/compoundingtech/pty/issues/122) | ||
|
|
||
| **Capabilities required:** `pty,jq,script`. No model and no bus. The cell uses | ||
| synthetic commands under an eval-owned PTY root. | ||
|
|
||
| **Discriminates:** can a relay request a strict attach-only policy that connects | ||
| to an existing daemon but never evaluates retained launch metadata? A dead | ||
| session must make `pty attach --no-restart <id>` exit nonzero without prompting | ||
| or creating another daemon incarnation. | ||
|
|
||
| ## What it proves | ||
|
|
||
| - **Surface:** `pty attach --help` advertises `--no-restart`, preventing an | ||
| unknown-option failure from masquerading as safe refusal. | ||
| - **Live positive control:** `--no-restart` attaches to a running reader, carries | ||
| terminal input through an `ACK` round-trip, and exits with that process without | ||
| creating another incarnation. | ||
| - **Mutation-valid control:** legacy `pty attach` receives queued future input | ||
| through a real terminal and demonstrably restarts its synthetic dead target. | ||
| - **Noninteractive refusal:** a no-input terminal produces exactly one expected | ||
| dead-session diagnostic and exits nonzero; the queued-input leg permits only | ||
| that diagnostic plus terminal echo. No prompt, command presentation, or other | ||
| interactive output is accepted. | ||
| - **No new incarnation:** the candidate target's marker and `session_start` | ||
| event count both remain exactly one. | ||
| - **State preservation:** retained metadata remains `exited`, and neither its | ||
| original pid nor any pid it still records identifies a live daemon for the | ||
| synthetic root and session. | ||
| - **Isolation and cleanup:** both controls use `$CATALOG/attach-only-pty`, and | ||
| the cell removes their exact synthetic records before grading. | ||
|
|
||
| ## Run it | ||
|
|
||
| ```sh | ||
| st2 eval ./cells/pty-attach-only/ | ||
| ``` | ||
|
|
||
| This cell is intentionally RED until PTY implements the explicit attach-only | ||
| surface tracked in issue #122. The free corpus gate remains deterministic and | ||
| model-free. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| marker="${1:?marker path required}" | ||
| printf 'run\n' >>"$marker" | ||
| printf 'ATTACH-ONLY-LIVE-READY\n' | ||
| IFS= read -r line | ||
| printf 'LIVE-ACK:%s\n' "$line" | ||
| exit 37 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| printf 'run\n' >>"${1:?marker path required}" | ||
| exit 42 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| root="${CATALOG:?CATALOG must be set}/attach-only-pty" | ||
| candidate="attach-only-dead" | ||
| control="legacy-dead" | ||
| live="attach-only-live" | ||
| candidate_marker="$CATALOG/attach-only-runs" | ||
| control_marker="$CATALOG/legacy-runs" | ||
| live_marker="$CATALOG/live-runs" | ||
| candidate_transcript_no_input="$CATALOG/attach-only-no-input.transcript" | ||
| candidate_transcript_future="$CATALOG/attach-only-future.transcript" | ||
| control_transcript="$CATALOG/legacy.transcript" | ||
| live_transcript="$CATALOG/live.transcript" | ||
| once="$PWD/once.sh" | ||
| live_command="$PWD/live.sh" | ||
|
|
||
| mkdir -p "$root" | ||
|
|
||
| pty_at() { | ||
| PTY_ROOT="$root" env -u PTY_SESSION pty "$@" | ||
| } | ||
|
|
||
| cleanup() { | ||
| for id in "$candidate" "$control" "$live"; do | ||
| pty_at kill "$id" >/dev/null 2>&1 || true | ||
| pty_at rm "$id" >/dev/null 2>&1 || true | ||
| done | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| wait_running() { | ||
| id="$1" | ||
| for _ in $(seq 1 100); do | ||
| status="$( | ||
| pty_at list --json | | ||
| jq -r --arg id "$id" '.[] | select(.name == $id) | .status' | ||
| )" | ||
| test "$status" = "running" && return 0 | ||
| sleep 0.05 | ||
| done | ||
| printf 'timed out waiting for %s to run (last status: %s)\n' "$id" "${status:-missing}" >&2 | ||
| return 1 | ||
| } | ||
|
|
||
| wait_exited() { | ||
| id="$1" | ||
| for _ in $(seq 1 100); do | ||
| status="$( | ||
| pty_at list --json | | ||
| jq -r --arg id "$id" '.[] | select(.name == $id) | .status' | ||
| )" | ||
| test "$status" = "exited" && return 0 | ||
| sleep 0.05 | ||
| done | ||
| printf 'timed out waiting for %s to exit (last status: %s)\n' "$id" "${status:-missing}" >&2 | ||
| return 1 | ||
| } | ||
|
|
||
| run_count() { | ||
| marker="$1" | ||
| test -f "$marker" || { | ||
| printf '0\n' | ||
| return | ||
| } | ||
| wc -l <"$marker" | ||
| } | ||
|
|
||
| start_count() { | ||
| id="$1" | ||
| jq -s '[.[] | select(.type == "session_start")] | length' "$root/$id.events.jsonl" | ||
| } | ||
|
|
||
| pid_is_candidate_daemon() { | ||
| pid="$1" | ||
| test -r "/proc/$pid/environ" || return 1 | ||
| tr '\0' '\n' <"/proc/$pid/environ" | grep -Fxq "PTY_ROOT=$root" && | ||
| tr '\0' ' ' <"/proc/$pid/cmdline" | grep -Fq "$candidate" | ||
| } | ||
|
|
||
| if pty attach --help | grep -Eq '(^|[[:space:]])--no-restart([[:space:]]|$)'; then | ||
| echo "ATTACH-ONLY-SURFACE-GREEN-7ca1" | ||
| else | ||
| echo "attach --no-restart is not advertised" >&2 | ||
| fi | ||
|
|
||
| pty_at run -d --id "$live" --tag keep=true -- bash "$live_command" "$live_marker" | ||
| wait_running "$live" | ||
| for _ in $(seq 1 100); do | ||
| pty_at peek --plain "$live" 2>/dev/null | grep -Fq ATTACH-ONLY-LIVE-READY && break | ||
| sleep 0.05 | ||
| done | ||
| pty_at peek --plain "$live" | grep -Fq ATTACH-ONLY-LIVE-READY | ||
| set +e | ||
| printf 'live-input\n' | | ||
| timeout 10 script -qefc \ | ||
| "env -u PTY_SESSION PTY_ROOT='$root' pty attach --no-restart '$live'" \ | ||
| /dev/null >"$live_transcript" 2>&1 | ||
| live_rc=$? | ||
| set -e | ||
| test "$live_rc" -eq 37 | ||
| grep -Fq ATTACH-ONLY-LIVE-READY "$live_transcript" | ||
| grep -Fq LIVE-ACK:live-input "$live_transcript" | ||
| wait_exited "$live" | ||
| test "$(run_count "$live_marker")" -eq 1 | ||
| test "$(start_count "$live")" -eq 1 | ||
| echo "LIVE-ATTACH-ROUNDTRIP-GREEN-7ca1" | ||
|
|
||
| pty_at run -d --id "$control" --tag keep=true -- bash "$once" "$control_marker" | ||
| wait_exited "$control" | ||
| test "$(run_count "$control_marker")" -eq 1 | ||
|
|
||
| # A real terminal and queued future input make the legacy failure deterministic: | ||
| # anything except exactly "n" answers the dead-session restart prompt affirmatively. | ||
| set +e | ||
| printf 'future-input\n' | | ||
| timeout 10 script -qefc \ | ||
| "env -u PTY_SESSION PTY_ROOT='$root' pty attach '$control'" \ | ||
| /dev/null >"$control_transcript" 2>&1 | ||
| control_rc=$? | ||
| set -e | ||
|
|
||
| wait_exited "$control" | ||
| test "$control_rc" -ne 124 | ||
| grep -Fq 'Restart? [Y/n]' "$control_transcript" | ||
| test "$(run_count "$control_marker")" -eq 2 | ||
| echo "LEGACY-RESTART-CONTROL-GREEN-7ca1" | ||
|
|
||
| pty_at run -d --id "$candidate" --tag keep=true -- bash "$once" "$candidate_marker" | ||
| wait_exited "$candidate" | ||
| test "$(run_count "$candidate_marker")" -eq 1 | ||
| test "$(start_count "$candidate")" -eq 1 | ||
| before_pid="$( | ||
| pty_at list --json | | ||
| jq -er --arg id "$candidate" '.[] | select(.name == $id and .status == "exited") | .pid' | ||
| )" | ||
| ! pid_is_candidate_daemon "$before_pid" | ||
|
|
||
| set +e | ||
| timeout 10 script -qefc \ | ||
| "env -u PTY_SESSION PTY_ROOT='$root' pty attach --no-restart '$candidate'" \ | ||
| /dev/null </dev/null >"$candidate_transcript_no_input" 2>&1 | ||
| candidate_rc_no_input=$? | ||
| printf 'future-input\n' | | ||
| timeout 10 script -qefc \ | ||
| "env -u PTY_SESSION PTY_ROOT='$root' pty attach --no-restart '$candidate'" \ | ||
| /dev/null >"$candidate_transcript_future" 2>&1 | ||
| candidate_rc_future=$? | ||
| set -e | ||
|
|
||
| test "$candidate_rc_no_input" -ne 0 | ||
| test "$candidate_rc_no_input" -ne 124 | ||
| test "$candidate_rc_future" -ne 0 | ||
| test "$candidate_rc_future" -ne 124 | ||
| tr -d '\r' <"$candidate_transcript_no_input" >"$candidate_transcript_no_input.normalized" | ||
| tr -d '\r' <"$candidate_transcript_future" >"$candidate_transcript_future.normalized" | ||
| expected_diagnostic="Session \"$candidate\" is not running (status: exited)." | ||
| grep -Fqx "$expected_diagnostic" "$candidate_transcript_no_input.normalized" | ||
| grep -Fqx "$expected_diagnostic" "$candidate_transcript_future.normalized" | ||
| grep -Fqx future-input "$candidate_transcript_future.normalized" | ||
| test "$(wc -l <"$candidate_transcript_no_input.normalized")" -eq 1 | ||
| test "$(wc -l <"$candidate_transcript_future.normalized")" -eq 2 | ||
| echo "DEAD-ATTACH-REFUSAL-GREEN-7ca1" | ||
|
|
||
| test "$(run_count "$candidate_marker")" -eq 1 | ||
| test "$(start_count "$candidate")" -eq 1 | ||
| echo "NO-NEW-INCARNATION-GREEN-7ca1" | ||
|
|
||
| after_status="$( | ||
| pty_at list --json | | ||
| jq -er --arg id "$candidate" '.[] | select(.name == $id) | .status' | ||
| )" | ||
| after_pid="$( | ||
| pty_at list --json | | ||
| jq -r --arg id "$candidate" '.[] | select(.name == $id) | .pid // empty' | ||
| )" | ||
| test "$after_status" = "exited" | ||
| case "$after_pid" in | ||
| "") ;; | ||
| *[!0-9]*) exit 1 ;; | ||
| *) ! pid_is_candidate_daemon "$after_pid" ;; | ||
| esac | ||
| ! pid_is_candidate_daemon "$before_pid" | ||
| echo "DEAD-STATE-UNCHANGED-GREEN-7ca1" | ||
|
|
||
| cleanup | ||
| trap - EXIT | ||
| test "$(pty_at list --json | jq 'length')" -eq 0 | ||
| echo "SYNTHETIC-ROOT-CLEAN-GREEN-7ca1" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // Dead-session attach policy, exercised against an eval-owned PTY root and synthetic one-shot commands. | ||
| eval { | ||
| copy "./fixture" | ||
| max-timeout "90s" | ||
|
|
||
| run "probe" { | ||
| command "bash ./probe.sh" | ||
| } | ||
|
|
||
| judges { | ||
| judge "SURFACE - attach exposes an explicit no-restart policy" { | ||
| exec "grep -Fqx ATTACH-ONLY-SURFACE-GREEN-7ca1 $RUNS_DIR/probe.out" | ||
| } | ||
| judge "LIVE - attach-only connects to a running daemon and carries terminal input" { | ||
| exec "grep -Fqx LIVE-ATTACH-ROUNDTRIP-GREEN-7ca1 $RUNS_DIR/probe.out" | ||
| } | ||
| judge "CONTROL - the same queued input restarts a dead session through legacy attach" { | ||
| exec "grep -Fqx LEGACY-RESTART-CONTROL-GREEN-7ca1 $RUNS_DIR/probe.out" | ||
| } | ||
| judge "REFUSAL - attach-only exits nonzero without presenting a restart prompt" { | ||
| exec "grep -Fqx DEAD-ATTACH-REFUSAL-GREEN-7ca1 $RUNS_DIR/probe.out" | ||
| } | ||
| judge "INCARNATION - attach-only cannot execute retained launch metadata" { | ||
| exec "grep -Fqx NO-NEW-INCARNATION-GREEN-7ca1 $RUNS_DIR/probe.out" | ||
| } | ||
| judge "STATE - the retained record remains exited without a live daemon pid" { | ||
| exec "grep -Fqx DEAD-STATE-UNCHANGED-GREEN-7ca1 $RUNS_DIR/probe.out" | ||
| } | ||
| judge "CLEANUP - both synthetic sessions are removed from the eval root" { | ||
| exec "grep -Fqx SYNTHETIC-ROOT-CLEAN-GREEN-7ca1 $RUNS_DIR/probe.out" | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.