Repository navigation
relay#589: adopt a terminal Claude on an SSH host - #597
Conversation
The host adopt runs as one detached script on the host (check, ladder, start); relay writes the audit intents and a dormant record before the SSH command and reads only its result. A dropped link answers host_link_lost and keeps the record; Resume attaches the started agent or re-runs the script with no_signal. fakerelay serves an adoptable host row and the host_link_lost refusal. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TestAdoptHostSuccess, Busy, Changed and LinkDrop; the host_unsupported case moves to detach_sessions: false (approved behaviour change); SSHLieStart. TestAdoptHostLinkDrop is red: the dormant record is not listed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A host adopt that ends in adopt_link_lost or adopt_launch_failed saves its session so it lists as dormant; the resume no longer takes that session's own hold on the conversation for another's. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…part A per-session adopt.lock keeps two scripts from running at once. A setup failure of the SSH command is its own outcome (nothing touched). A resume answered changed also deletes relay-sessions' saved copy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… host Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oves the record Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…atches the helper by its executable A real Claude writes request blobs and attachments of hundreds of KiB into a turn, so the first turn's prompt lies before the 256 KiB tail. The door then sent child_test "all", and the script read every MCP server, zombie and turn helper as busy. The door now reads the transcript backwards below the tail, 1 MiB per read and 8 MiB at most, until it holds the prompt. ps comm reads "caffeinate" for the turn helper Claude starts by name, so the script now matches the executable path (/proc/<pid>/exe, else lsof's first txt entry), as the local adopt does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TestAdoptHostNoClaude; SSHNoClaude drops this machine's claude from the stub host's PATH so the probe finds none. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The seeded claude-code template stays; only the probe result makes the row host_unsupported. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Evidence after the stack runs and review round 3 (head 0c3ffd8). Stack runs found three real defects, fixed on this branch: a host whose probe found no claude listed as adoptable (now Fix attempts: 5 (dormant listing; review round 1; stack: probe gate; stack: idle test, escalated to the default model; test for the probe gate). Review rounds: 3 plus a confirm. The confirm finding (the new test guarded the template, not the probe) is fixed: removing the probe condition in Full e2e suite at 70ba45c: Review: no blocking findings (round 3 plus confirm; default model for rounds 1–3) |
|
Weakening reviewed: stateNotRun in journey_adopt_host.go is the new journeys' NOTRUN off a stack, which the contract requires (api phase, stack only); no existing journey or test is skipped or loosened |
|
merge-check: merged. approved, target, contract, evidence, owner |
Closes #589
Adopt a terminal Claude on an SSH host. The door reads the transcript tail, writes the intents and a dormant record, then starts one detached transition script on the host (
internal/sshhost/adopt.go) that checks the pid's start time, runs the idle test and the signal ladder, and starts therelay-agent-<id>session. A dropped link answers502 host_link_lostand lists the session as dormant; Resume attaches the started agent or starts one.Non-obvious decisions:
e2e/features/g1_outside_adopt_test.gomoves todetach_sessions: false;fakerelay/prooftest/adopt_test.godrops the "adoptable on a host row" refusal case and takes relay's generichost_unsupportedmessage.TM3f.1claim edit (threat-claim change).adopt_link_lost,adopt_launch_failed) so it lists dormant; a resume that answerschangeddeletes that saved copy, so the outside row is adoptable again.adopt.lockkeeps one transition script per session. A second command answersfailed in_progress(502, record kept).failed setup(relay: 502unavailable, record removed, nothing saved). The new internal codeadopt_setup_failednever reaches eve.session-adopt-hostsendsmodel: "haiku": on a stack the transcript names no Claude family (relay#588 risk 8).TestAdoptHostSuccesscovers the no-model rule.probe.gois not in ownership); the reviewer checked byte equivalence.Fix attempts: 2 (round 0: dormant session not listed after
host_link_lost; review round 1: three blocking findings, fixed). Review rounds: 2, default model.Minor review findings, not fixed (PR notes):
deleteHostSessionignores a non-2xx answer (session_adopt_host.go:387); a failed DELETE would leave the saved copy.interruptedrecords the requested interrupt, not a sent SIGINT.failed in_progress, notfailed setup.docs/ssh-hosts.mdomitsadopt_setup_failedfrom the relay-sessions refusal list./launchbound: only a Claude that ignores SIGTERM passes it; that ends ashost_link_lostwith Resume.Tests:
TestAdoptHostSuccess,TestAdoptHostBusy,TestAdoptHostChanged,TestAdoptHostLinkDrop,TestAdoptReasonsOnRows, prooftestTestAdoptHost. Full e2e suite:ok relaye2e/features 875.291sat ae9d6e6; host testsok 39.251sat HEAD (round-2 reviewer).Review: no blocking findings (round 2, default model)
Break-and-restore: forced the script's lifetime checks to always match (adopt.go lines 130, 159); TestAdoptHostChanged went red ("answered 201, want 409 changed"); restored byte for byte (git diff clean)
Harness weakening: none (two journeys added, nothing removed or loosened)
🤖 Generated with Claude Code