resume: hookless directory-scoped continue bindings for remote agents (#7989) - #10049
resume: hookless directory-scoped continue bindings for remote agents (#7989)#10049alloevil wants to merge 1 commit into
Conversation
…mote agents (manaflow-ai#7989) An agent launched inside a persistent-SSH workspace without relayed hooks leaves no resume binding, so a restore reattaches the PTY but never resumes the agent once the PTY is genuinely gone. Requiring `cmux hooks setup` on every remote host for every agent CLI is a real adoption cliff for multi-host users. This adds the Tier-1 flavor sketched in the issue discussion: when the snapshot still knows the agent kind and remote working directory, synthesize a directory-scoped continue binding from cmux's own inline templates (claude -> `claude --continue || claude`, codex -> `codex resume --last || codex`; kinds with no trustworthy sessionless continue synthesize nothing). Design points: - New binding source `remote-synthesized`, sharing the trust tier of process-detected bindings: the command is built exclusively from inline templates with no caller-supplied arguments, so it bypasses the signed approval store the same way. The gate for every other source is unchanged. - Reuses .persistentSSH(SurfaceResumeRemoteContext) - no new Codable case, no persistence-format risk. - Liveness-gated through the existing requireExisting attach pipeline: a live remote PTY is attach-only (the synthesized command is only injected once the PTY is confirmed ended), so restore can never race a live agent and create a duplicate writing the same directory. - Precedence: agent-hook / cli / process-detected bindings always win; synthesis only fills the gap, and only when the snapshot recorded a running agent and auto-resume is enabled. Known limitation (documented in code): directory-scoped continue resumes the most recent session in that directory, so a directory shared by multiple agents of the same kind can continue the wrong conversation. Hook-published bindings (Tier 2) remain the precise path and always take precedence. Refs manaflow-ai#7989.
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
📝 WalkthroughWalkthroughThis change adds synthesized remote agent resume bindings for Claude and Codex sessions. Persistent-SSH restoration now creates, trusts, filters, persists, and tests these bindings based on agent state, working directory, auto-resume settings, and PTY liveness. ChangesRemote agent resume
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Workspace
participant RemoteAgentContinueSynthesizer
participant ApprovalCache
participant PersistentSSHPTY
Workspace->>RemoteAgentContinueSynthesizer: synthesize binding for eligible remote agent
RemoteAgentContinueSynthesizer-->>Workspace: return continue command and SSH metadata
Workspace->>ApprovalCache: evaluate synthesized binding trust
Workspace->>PersistentSSHPTY: check PTY liveness
PersistentSSHPTY-->>Workspace: return attach command
Possibly related issues
Possibly related PRs
Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (4 errors, 1 warning, 1 inconclusive)
✅ Passed checks (19 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
Sources/SessionPersistence.swift (1)
380-382: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winInclude remote-synthesized bindings in detected-binding precedence.
shouldYieldToDetectedSurfaceResumeBindingis used during runtime reconciliation. A storedremote-synthesizedbinding does not yield to a laterprocess-detectedbinding, so the less accurate binding remains active. IncludeisRemoteSynthesizedin the yield condition.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/SessionPersistence.swift` around lines 380 - 382, Update shouldYieldToDetectedSurfaceResumeBinding to also treat isRemoteSynthesized as yielding to a detected process binding, while preserving the existing isProcessDetected and isAgentHookBinding checks.Sources/SurfaceResumeApprovalSigningSecretCache.swift (1)
391-416: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftReject reserved trust sources at the public
surface.resume.setboundary.
remote-synthesizedpasses throughpublicResumeSourceand is copied into the binding.trustedBindingthen bypasses signed approval and forces automatic resume. A caller can therefore store an arbitrary command for automatic execution on restore. Accept only internally authenticated provenance values and fail closed for reserved sources.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/SurfaceResumeApprovalSigningSecretCache.swift` around lines 391 - 416, Update trustedBinding to accept only internally authenticated provenance for process-detected and agent-hook bindings, and reject remote-synthesized or other caller-supplied reserved sources at the public surface.resume.set boundary. Ensure untrusted bindings return nil or otherwise fail closed before bypassing signed approval or enabling automatic resume, while preserving automatic handling for genuinely internal observations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@Sources/SessionPersistence.swift`:
- Around line 380-382: Update shouldYieldToDetectedSurfaceResumeBinding to also
treat isRemoteSynthesized as yielding to a detected process binding, while
preserving the existing isProcessDetected and isAgentHookBinding checks.
In `@Sources/SurfaceResumeApprovalSigningSecretCache.swift`:
- Around line 391-416: Update trustedBinding to accept only internally
authenticated provenance for process-detected and agent-hook bindings, and
reject remote-synthesized or other caller-supplied reserved sources at the
public surface.resume.set boundary. Ensure untrusted bindings return nil or
otherwise fail closed before bypassing signed approval or enabling automatic
resume, while preserving automatic handling for genuinely internal observations.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1ba7a8db-534e-40fb-9ff1-94eeab363538
📒 Files selected for processing (9)
Sources/ControlSurfaceResumeTarget.swiftSources/RemoteAgentContinueSynthesizer.swiftSources/SessionPersistence.swiftSources/SurfaceResumeApprovalSigningSecretCache.swiftSources/Workspace+PersistentRemotePTYReattach.swiftSources/Workspace+RemoteSurfaceResumeBinding.swiftSources/Workspace.swiftcmux.xcodeproj/project.pbxprojcmuxTests/RemoteAgentContinueSynthesizerTests.swift
Implements the Tier-1 flavor proposed in #7989's discussion: synthesize a directory-scoped continue binding for remote agents that never published a hook binding, so a persistent-SSH restore can resume the agent after the remote PTY is genuinely gone — without requiring
cmux hooks setupon every remote host.Problem
Local process discovery can't see agents on the SSH host, and hook-published bindings require per-host, per-agent installation — a real adoption cliff for multi-host users. Result: restore reattaches the PTY, but a genuinely-gone PTY leaves the agent unresumed (
resume_binding: null).Approach
When the snapshot still records the agent kind + remote working directory (and
wasAgentRunning),RemoteAgentContinueSynthesizerbuilds a binding from inline templates:cd -- '<dir>' … && claude --continue || claudecd -- '<dir>' … && codex resume --last || codexDesign points, mapped to the issue's acceptance criteria:
requireExistingpipeline; the synthesized command is additionally hard-gated inreattachPersistentRemotePTYPanelsto inject only once the PTY is confirmed ended. A directory-scoped continue has no session checkpoint the remote once-guard could reconcile against, so this gate is stricter than for hook bindings..persistentSSH(SurfaceResumeRemoteContext)— the existing flavor, no new Codable case — and enters through the SSH persistent-session machinery viaremotePTYAttachStartupCommand. No wrapper-resolver tokens (they resolve local Mac paths), mirroringremoteStartupInput()'srepairPortableAgentExecutable: falseconvention.remote-synthesizedshares the process-detected tier (bypasses the signed approval store) because the command is built exclusively from cmux's own inline templates with no caller-supplied arguments — an observation-grade artifact, not a proposal from an arbitrary process. The gate for every other source is unchanged (covered by a regression test).Known limitations (documented in code + tests)
Tests
RemoteAgentContinueSynthesizerTests(15 cases, Swift Testing): exact command form per kind, nil for the 16 uncovered kinds + custom (parameterized), cwd guards, single-quote splice injection safety, non-ASCII printf-octal quoting,isRemoteSynthesizedpredicate + mutual exclusion across all four sources (parameterized), trust-tier resolution with a.pendingsigning secret, cli-source demotion regression guard, reconcile pass-through,remoteStartupInput()verbatim replay, Codable round-trip with quote-bearing cwd.Caveats
main; every call was verified against the callee's source, but I could not compile or run the app locally. Happy to iterate on anything the macOS build or the full restore matrix surfaces.xcodeprojwiring for the 2 new files is included (lint-pbxproj-test-wiring passes).Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Synthesizes directory-scoped resume bindings for hookless remote agents in persistent-SSH workspaces, so restores can resume agents after the remote PTY ends without installing hooks on every host. Previously, restore reattached the PTY but left the agent unresumed when the PTY was gone.
remote-synthesizedbindings forclaude(“claude --continue || claude”) andcodex(“codex resume --last || codex”) only when no binding exists, the snapshot retains agent kind and remote cwd, the workspace is persistent-SSH, and auto-resume is enabled; other kinds synthesize nothing..persistentSSH(...);remoteStartupInput()replays the stored command verbatim; UI maps to.direct(no checkpoint).remote-synthesizedshares the trust tier of process-detected bindings and bypasses the signed approval store; all other sources are unchanged.RemoteAgentContinueSynthesizer.swift; trust tier inSurfaceResumeApprovalSigningSecretCache;isRemoteSynthesizedinSessionPersistence; PTY gating inWorkspace+PersistentRemotePTYReattach; auto-resume gating inWorkspace+RemoteSurfaceResumeBinding; direct-mode mapping inControlSurfaceResumeTarget; synthesis and startup selection inWorkspace; tests and Xcode project wiring.Limitations
Written for commit cb3225c. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes