Skip to content

Cloud: capture remote turns and reconnect owner rewinds durably - #30371

Open
teamleaderleo wants to merge 40 commits into
feat-cmux-nextfrom
lane/cloud-turn-native-bridge
Open

teamleaderleo wants to merge 40 commits into
feat-cmux-nextfrom
lane/cloud-turn-native-bridge

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

An owner Cloud attachment enrolls its existing workspace/carrier through the platform before starting or attaching a chat. Enrollment never allocates another VM. Each native Cloud prompt then reserves its daemon turn, captures the workspace machine using native owner authentication, and dispatches a scoped one-use execution capability. cmux machine turns lists captures; cmux machine rewind restores a selected turn and reconnects the workspace to its verified replacement. Owner-verified bindings persist per account so refresh and relaunch resolve the current carrier.

Request: “snapshot per turn so a user can rewind the whole machine (files + state) to any earlier turn.” This PR connects native owner authentication, the guest daemon transport, and the rewind commands. The platform ledger/carrier enrollment and acpmux execution fence are separate dependencies. Supported app-link targets require an owner-verified logical carrier; raw VM API IDs remain unsupported.

Owner tokens stay native and use the selected Cloud team. Preparation and execution have separate capability purposes bound to one prompt/turn, generation, and attempt. Capture or authorization failure stops dispatch; accepted replays neither capture nor submit twice. Uncertain submission retains its external fence. The attachment pins the owner ticket that opened it and verifies the connected carrier/generation before releasing preparation authorization. A stale attachment cannot borrow a replacement connection's proof: it closes so the pane reattaches. Concurrent attachments share enrollment, and provisioning can force a fresh same-generation connection.

The guest host loads the three non-secret scope variables from /var/lib/cmux/turn-scope.json, a bounded root-owned 0600 regular file in a protected directory. Missing scope preserves existing machines; invalid scope refuses startup. A configured owner attachment checks the actual acpmux capability. If the running acpmux predates that scope, the session daemon uses its existing installed-binary shutdown --keep-agents/start handoff under the start lock, then checks capability again. Concurrent attaches recheck within that lock; an old installed binary or failed handoff gets at most one attempt per daemon lifetime. Enrolled native Cloud sessions refuse uncaptured prompts when support is absent. Ordinary remote sessions and observer permissions retain their existing behavior.

The remote pane host moves into the existing daemon service target; the app keeps authentication and registry wiring. Command selection and guest handoff live in Rust. This coherent capture/rewind/reconnect change spans about 3,150 added lines including tests and the host move: splitting those native seams would leave an unusable command or unsafe stale connection. The App module shrinks by 42 source lines. Prior art read: #28613, #30001, and #30337; the daemon dependency is coordinated with its owner.

At head 62d1382dd74cac27bf9bab584d95c72e2d74fd90, the daemon dependency and its registry/TLS repair are integrated. First enrollment uses generation zero as an absence fence, validates generation one, and uses the returned generation thereafter. Owner origins map to their corresponding platform environment; unknown origins refuse before token mint.

Validation:

  • Exact-head Opus review: SHIP. The owner-connection read was reviewed with its shared validator and allowlist; no blocking findings. Its ownership-refusal test follow-up is tracked in Cover ownership refusal for cloud.turn.connection #30835.
  • Exact-head App compile passed in 80 seconds. The current CloudTurnCommandsTests fleet run and exact-head capture DEV build are in progress.
  • Ten Swift suites passed 46/46 at the dependency-integration head. The subsequent origin change passed CloudWorkspaceImportTests 8/8; the current head carries the independently reviewed daemon registry/TLS repair unchanged.
  • Executed Rust test logs confirm 152 attachment tests and two scope-loader tests passed; those source files are unchanged at this head. The former CLI command targeted the thin binary and ran zero tests; the corrected library-target run passed both CLI tests at the previous head; those Rust files are unchanged (receipt exit zero; 149.7 seconds).

The owner-scoped cloud.turn.connection read exposes only workspace, logical machine, generation, provider, and validated physical VM identity; no provider capabilities or credentials are exported.

The live native check records independent structured command stdout fenced by owner connection reads, mutates the RAM workload/file/SQLite fixture, rewinds the automatic capture, binds the pinned source, fixture, PID/start time/cwd, nonce and exact external capture ledger, and compares actual reads after rewind, forced native inventory refresh, and app relaunch. The capture harness now has its broker-aware installer and fresh-reply matcher. Authenticated fleet-slot relaunch support is being implemented with process-only credential delivery. No native end-to-end success is claimed yet.

HOLD: Finish real native capture, rewind, fresh reply, refresh and relaunch readback.

Agent session: agent session

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentSessionRemoteClient.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentSessionRemoteClient.swift:51">
P3: This message reaches the local app, and "this machine" points the user at their own Mac, not the Cloud VM that needs the update. Say "that machine", matching `settingsUnsupported` in this file.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/RemoteAcpmuxCloudTurnTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/RemoteAcpmuxCloudTurnTests.swift:48">
P2: The suite does not cover the case where `prepareCloudTurn` fails. Add a test whose client throws there, and assert that calls are `["prepare"]` or empty with no `unfenced` or `fenced` entry and an error reply. Also consider a test where the fenced `prompt(cloudTurn:)` throws, which must return an error and never retry unfenced.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/RemoteAcpmuxCloudTurnTests.swift:62">
P3: This wait budget is 200 polls (about 1 s), while the existing `waitUntil` helpers in this suite use 500. Match that budget, or reuse the existing helper, so the test does not flake on slow CI runners.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudTurnOwnerClientTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudTurnOwnerClientTests.swift:6">
P2: The failure path that releases the reserved capture via `/finish` after a capability-mint failure is untested. Add a test where `/capability` returns an error or a mismatched scope, and assert that `/finish` was requested and the error propagates.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudTurnOwnerClientTests.swift:43">
P2: This matcher accepts any `CloudTurnOwnerClient.Failure`, including `.refused(401)`, so the test can pass without proving the machine-binding check fired. Assert the `.scope` case specifically.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

wire.cancel(code: 1000, reason: "test complete")
}

@Test func ownerCaptureFailureNeverFallsBackToAnUnfencedPrompt() async throws {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The suite does not cover the case where prepareCloudTurn fails. Add a test whose client throws there, and assert that calls are ["prepare"] or empty with no unfenced or fenced entry and an error reply. Also consider a test where the fenced prompt(cloudTurn:) throws, which must return an error and never retry unfenced.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/RemoteAcpmuxCloudTurnTests.swift, line 48:

<comment>The suite does not cover the case where `prepareCloudTurn` fails. Add a test whose client throws there, and assert that calls are `["prepare"]` or empty with no `unfenced` or `fenced` entry and an error reply. Also consider a test where the fenced `prompt(cloudTurn:)` throws, which must return an error and never retry unfenced.</comment>

<file context>
@@ -0,0 +1,65 @@
+        wire.cancel(code: 1000, reason: "test complete")
+    }
+
+    @Test func ownerCaptureFailureNeverFallsBackToAnUnfencedPrompt() async throws {
+        let client = Client(), frames = RemoteAcpmuxObserverTests.Frames()
+        let wire = RemoteAcpmuxWire(client: client, session: "session_1", cloudTurn: { _, _, _ in
</file context>

@testable import CmuxNextCloud

@Suite struct CloudTurnOwnerClientTests {
nonisolated final class TurnAPI: URLProtocol {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The failure path that releases the reserved capture via /finish after a capability-mint failure is untested. Add a test where /capability returns an error or a mismatched scope, and assert that /finish was requested and the error propagates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudTurnOwnerClientTests.swift, line 6:

<comment>The failure path that releases the reserved capture via `/finish` after a capability-mint failure is untested. Add a test where `/capability` returns an error or a mismatched scope, and assert that `/finish` was requested and the error propagates.</comment>

<file context>
@@ -0,0 +1,56 @@
+@testable import CmuxNextCloud
+
+@Suite struct CloudTurnOwnerClientTests {
+    nonisolated final class TurnAPI: URLProtocol {
+        override class func canInit(with _: URLRequest) -> Bool { true }
+        override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
</file context>

Comment on lines +43 to +46
await #expect(throws: CloudTurnOwnerClient.Failure.self) {
try await owner("wrong-machine.test").prepare(workspace: "ws_1", machine: "vm_1", session: "session_1", prompt: "prompt_1",
prepared: Data(#"{"session":"session_1","turn":"turn_1"}"#.utf8))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This matcher accepts any CloudTurnOwnerClient.Failure, including .refused(401), so the test can pass without proving the machine-binding check fired. Assert the .scope case specifically.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudTurnOwnerClientTests.swift, line 43:

<comment>This matcher accepts any `CloudTurnOwnerClient.Failure`, including `.refused(401)`, so the test can pass without proving the machine-binding check fired. Assert the `.scope` case specifically.</comment>

<file context>
@@ -0,0 +1,56 @@
+    }
+
+    @Test func differentWorkspaceMachineIsRefused() async {
+        await #expect(throws: CloudTurnOwnerClient.Failure.self) {
+            try await owner("wrong-machine.test").prepare(workspace: "ws_1", machine: "vm_1", session: "session_1", prompt: "prompt_1",
+                                                        prepared: Data(#"{"session":"session_1","turn":"turn_1"}"#.utf8))
</file context>
Suggested change
await #expect(throws: CloudTurnOwnerClient.Failure.self) {
try await owner("wrong-machine.test").prepare(workspace: "ws_1", machine: "vm_1", session: "session_1", prompt: "prompt_1",
prepared: Data(#"{"session":"session_1","turn":"turn_1"}"#.utf8))
}
await #expect {
try await owner("wrong-machine.test").prepare(workspace: "ws_1", machine: "vm_1", session: "session_1", prompt: "prompt_1",
prepared: Data(#"{"session":"session_1","turn":"turn_1"}"#.utf8))
} throws: { error in
guard let failure = error as? CloudTurnOwnerClient.Failure, case .scope = failure else { return false }
return true
}

public func prepareCloudTurn(id: String) async throws -> Data { throw Self.turnUnsupported }
public func prompt(id: String, text: String, cloudTurn: Data) async throws -> Data { throw Self.turnUnsupported }
private static var turnUnsupported: AgentSessionRemoteError {
AgentSessionRemoteError(code: "agent_session.turn_unsupported", message: "Update cmux on this machine to capture Cloud turns.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This message reaches the local app, and "this machine" points the user at their own Mac, not the Cloud VM that needs the update. Say "that machine", matching settingsUnsupported in this file.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentSessionRemoteClient.swift, line 51:

<comment>This message reaches the local app, and "this machine" points the user at their own Mac, not the Cloud VM that needs the update. Say "that machine", matching `settingsUnsupported` in this file.</comment>

<file context>
@@ -41,6 +45,11 @@ public nonisolated protocol AgentSessionRemoteClient: Sendable {
+    public func prepareCloudTurn(id: String) async throws -> Data { throw Self.turnUnsupported }
+    public func prompt(id: String, text: String, cloudTurn: Data) async throws -> Data { throw Self.turnUnsupported }
+    private static var turnUnsupported: AgentSessionRemoteError {
+        AgentSessionRemoteError(code: "agent_session.turn_unsupported", message: "Update cmux on this machine to capture Cloud turns.")
+    }
     /// A daemon without `agent-session-children-v1` lists none.
</file context>
Suggested change
AgentSessionRemoteError(code: "agent_session.turn_unsupported", message: "Update cmux on this machine to capture Cloud turns.")
AgentSessionRemoteError(code: "agent_session.turn_unsupported", message: "update cmux on that machine to capture Cloud turns from here")

}

private func waitForReply(_ frames: RemoteAcpmuxObserverTests.Frames) async throws {
for _ in 0..<200 where frames.reply(7) == nil { try await Task.sleep(for: .milliseconds(5)) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This wait budget is 200 polls (about 1 s), while the existing waitUntil helpers in this suite use 500. Match that budget, or reuse the existing helper, so the test does not flake on slow CI runners.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/RemoteAcpmuxCloudTurnTests.swift, line 62:

<comment>This wait budget is 200 polls (about 1 s), while the existing `waitUntil` helpers in this suite use 500. Match that budget, or reuse the existing helper, so the test does not flake on slow CI runners.</comment>

<file context>
@@ -0,0 +1,65 @@
+    }
+
+    private func waitForReply(_ frames: RemoteAcpmuxObserverTests.Frames) async throws {
+        for _ in 0..<200 where frames.reply(7) == nil { try await Task.sleep(for: .milliseconds(5)) }
+        #expect(frames.reply(7) != nil)
+    }
</file context>

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/RemoteAcpmuxCloudTurnTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/RemoteAcpmuxCloudTurnTests.swift:47">
P2: The no-leak assertion only inspects the JSON-RPC reply for id 7. Assert against every frame the wire emitted, so the capability cannot leak through notifications or the replay stream.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudTurnOwnerClient.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudTurnOwnerClient.swift:40">
P2: Snapshot cleanup only covers capability minting: a successful capture followed by a malformed or mismatched snapshot response throws before `/finish`, leaving the reserved turn fenced. Include snapshot decoding and validation in the same cleanup path before returning the scope error.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudTurnOwnerClient.swift:54">
P2: The best-effort release in this catch runs in the same cancelled task, so the `/finish` request fails on cancellation and the reservation is never released. Run the release outside the caller's cancellation scope, or document that cancellation leaves the reservation for reconciliation.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

#expect(client.calls.withLock { $0 } == ["prepare", "fenced"])
#expect(frames.reply(7)?["result"] != nil)
let reply = try JSONSerialization.data(withJSONObject: frames.reply(7) ?? [:])
#expect(!String(decoding: reply, as: UTF8.self).contains("scoped-capability"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The no-leak assertion only inspects the JSON-RPC reply for id 7. Assert against every frame the wire emitted, so the capability cannot leak through notifications or the replay stream.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/RemoteAcpmuxCloudTurnTests.swift, line 44:

<comment>The no-leak assertion only inspects the JSON-RPC reply for id 7. Assert against every frame the wire emitted, so the capability cannot leak through notifications or the replay stream.</comment>

<file context>
@@ -0,0 +1,65 @@
+        #expect(client.calls.withLock { $0 } == ["prepare", "fenced"])
+        #expect(frames.reply(7)?["result"] != nil)
+        let reply = try JSONSerialization.data(withJSONObject: frames.reply(7) ?? [:])
+        #expect(!String(decoding: reply, as: UTF8.self).contains("scoped-capability"))
+        wire.cancel(code: 1000, reason: "test complete")
+    }
</file context>

} catch {
// No guest has seen the capability yet: releasing this exact reserved capture is safe.
// A failed prompt submission is different and is never released here: it may be running.
_ = try? await request(path + "/\(turn.turn)/finish", body: scope)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The best-effort release in this catch runs in the same cancelled task, so the /finish request fails on cancellation and the reservation is never released. Run the release outside the caller's cancellation scope, or document that cancellation leaves the reservation for reconciliation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudTurnOwnerClient.swift, line 54:

<comment>The best-effort release in this catch runs in the same cancelled task, so the `/finish` request fails on cancellation and the reservation is never released. Run the release outside the caller's cancellation scope, or document that cancellation leaves the reservation for reconciliation.</comment>

<file context>
@@ -0,0 +1,93 @@
+        } catch {
+            // No guest has seen the capability yet: releasing this exact reserved capture is safe.
+            // A failed prompt submission is different and is never released here: it may be running.
+            _ = try? await request(path + "/\(turn.turn)/finish", body: scope)
+            throw error
+        }
</file context>

let state = try JSONDecoder().decode(State.self, from: stateData)
guard state.machineId == machine, state.generation >= 0 else { throw Failure.scope }
let scope: [String: Any] = ["session": session, "expectedMachine": machine, "expectedGeneration": state.generation]
let captureData = try await request(path + "/\(turn.turn)/snapshot", body: scope.merging(["clientKey": prompt]) { _, new in new })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Snapshot cleanup only covers capability minting: a successful capture followed by a malformed or mismatched snapshot response throws before /finish, leaving the reserved turn fenced. Include snapshot decoding and validation in the same cleanup path before returning the scope error.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudTurnOwnerClient.swift, line 40:

<comment>Snapshot cleanup only covers capability minting: a successful capture followed by a malformed or mismatched snapshot response throws before `/finish`, leaving the reserved turn fenced. Include snapshot decoding and validation in the same cleanup path before returning the scope error.</comment>

<file context>
@@ -0,0 +1,93 @@
+        let state = try JSONDecoder().decode(State.self, from: stateData)
+        guard state.machineId == machine, state.generation >= 0 else { throw Failure.scope }
+        let scope: [String: Any] = ["session": session, "expectedMachine": machine, "expectedGeneration": state.generation]
+        let captureData = try await request(path + "/\(turn.turn)/snapshot", body: scope.merging(["clientKey": prompt]) { _, new in new })
+        let capture = try JSONDecoder().decode(Capture.self, from: captureData)
+        guard capture.workspace == workspace, capture.session == session, capture.turn == turn.turn,
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudTurnOwnerClientTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudTurnOwnerClientTests.swift:42">
P2: This test name says workspace, but it only exercises a machine mismatch. Rename it to `differentMachineIsRefused`, and add a case where the snapshot or capability response returns a different workspace.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudTurnOwnerClient.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudTurnOwnerClient.swift:25">
P3: The `prompt` parameter is an idempotency/promptId, not prompt text, and is validated with `isID`. Rename it to `promptID` so callers and the `clientKey` use stay unambiguous.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudTurnOwnerClient.swift:45">
P2: `prepare` returns the raw mint response body rather than the validated capability. Everything the server returns, including `team`, `user` and `expiresAt`, is forwarded to the guest. Re-encode only the validated capability, for example `JSONEncoder().encode(["capability": mint.capability])`.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Wiring.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Wiring.swift:549">
P2: `turnForwarder` can capture against the wrong machine because it uses the first-match cross-machine lookup for a tab that already has a known daemon. Pass the daemon machine ID and resolve the tab on that machine, or capture the exact tab/pane instead.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

#expect(!result.contains("owner-only"))
}

@Test func differentWorkspaceMachineIsRefused() async {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This test name says workspace, but it only exercises a machine mismatch. Rename it to differentMachineIsRefused, and add a case where the snapshot or capability response returns a different workspace.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudTurnOwnerClientTests.swift, line 42:

<comment>This test name says workspace, but it only exercises a machine mismatch. Rename it to `differentMachineIsRefused`, and add a case where the snapshot or capability response returns a different workspace.</comment>

<file context>
@@ -0,0 +1,56 @@
+        #expect(!result.contains("owner-only"))
+    }
+
+    @Test func differentWorkspaceMachineIsRefused() async {
+        await #expect(throws: CloudTurnOwnerClient.Failure.self) {
+            try await owner("wrong-machine.test").prepare(workspace: "ws_1", machine: "vm_1", session: "session_1", prompt: "prompt_1",
</file context>
Suggested change
@Test func differentWorkspaceMachineIsRefused() async {
@Test func differentMachineIsRefused() async {

guard capture.workspace == workspace, capture.session == session, capture.turn == turn.turn,
capture.machineId == machine, capture.machineGeneration == state.generation, capture.status == "ready" else { throw Failure.scope }
do {
let capability = try await request(path + "/\(turn.turn)/capability", body: scope)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: prepare returns the raw mint response body rather than the validated capability. Everything the server returns, including team, user and expiresAt, is forwarded to the guest. Re-encode only the validated capability, for example JSONEncoder().encode(["capability": mint.capability]).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudTurnOwnerClient.swift, line 45:

<comment>`prepare` returns the raw mint response body rather than the validated capability. Everything the server returns, including `team`, `user` and `expiresAt`, is forwarded to the guest. Re-encode only the validated capability, for example `JSONEncoder().encode(["capability": mint.capability])`.</comment>

<file context>
@@ -0,0 +1,93 @@
+        guard capture.workspace == workspace, capture.session == session, capture.turn == turn.turn,
+              capture.machineId == machine, capture.machineGeneration == state.generation, capture.status == "ready" else { throw Failure.scope }
+        do {
+            let capability = try await request(path + "/\(turn.turn)/capability", body: scope)
+            let mint = try JSONDecoder().decode(Mint.self, from: capability)
+            guard mint.workspace == workspace, mint.machine == machine, mint.session == session,
</file context>

return { [weak services] session, prompt, prepared in
let scope = await MainActor.run { () -> (String, String, AgentSessionAttachClient.OwnerLink)? in
guard let services, services.cloud.isSignedIn,
let (tab, pane) = services.locateTabOnAnyMachine(key),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: turnForwarder can capture against the wrong machine because it uses the first-match cross-machine lookup for a tab that already has a known daemon. Pass the daemon machine ID and resolve the tab on that machine, or capture the exact tab/pane instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Wiring.swift, line 549:

<comment>`turnForwarder` can capture against the wrong machine because it uses the first-match cross-machine lookup for a tab that already has a known daemon. Pass the daemon machine ID and resolve the tab on that machine, or capture the exact tab/pane instead.</comment>

<file context>
@@ -530,3 +534,28 @@ extension AgentTabStore {
+        return { [weak services] session, prompt, prepared in
+            let scope = await MainActor.run { () -> (String, String, AgentSessionAttachClient.OwnerLink)? in
+                guard let services, services.cloud.isSignedIn,
+                      let (tab, pane) = services.locateTabOnAnyMachine(key),
+                      let daemon = try? services.machines.agentDaemon(forTab: tab),
+                      let machine = services.machines.session(daemon.machineID), machine.appLink != nil,
</file context>


/// Captures the trusted workspace's machine, then mints its one-use execution capability.
/// `prepared` comes from the owning daemon; neither a workspace nor a machine is taken from it.
public func prepare(workspace: String, machine: String, session: String, prompt: String, prepared: Data) async throws -> Data {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The prompt parameter is an idempotency/promptId, not prompt text, and is validated with isID. Rename it to promptID so callers and the clientKey use stay unambiguous.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudTurnOwnerClient.swift, line 25:

<comment>The `prompt` parameter is an idempotency/promptId, not prompt text, and is validated with `isID`. Rename it to `promptID` so callers and the `clientKey` use stay unambiguous.</comment>

<file context>
@@ -0,0 +1,93 @@
+
+    /// Captures the trusted workspace's machine, then mints its one-use execution capability.
+    /// `prepared` comes from the owning daemon; neither a workspace nor a machine is taken from it.
+    public func prepare(workspace: String, machine: String, session: String, prompt: String, prepared: Data) async throws -> Data {
+        struct Prepared: Decodable { let turn: String; let session: String }
+        struct State: Decodable { let machineId: String; let generation: Int }
</file context>

@teamleaderleo
teamleaderleo force-pushed the lane/cloud-turn-native-bridge branch from c98d4eb to d6ce3e6 Compare November 1, 2026 07:07
@teamleaderleo
teamleaderleo force-pushed the lane/cloud-turn-native-bridge branch 2 times, most recently from 8d9ead9 to 53c78ec Compare November 1, 2026 07:12
@teamleaderleo teamleaderleo changed the title Cloud: capture remote turns through native owner authentication Cloud: capture remote turns and reconnect owner rewinds durably Nov 1, 2026
@cursor

cursor Bot commented Nov 1, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16 issues found across 34 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/cmux-next/machine-rewind.md">

<violation number="1" location="docs/cmux-next/machine-rewind.md:3">
P2: The doc does not say which workspaces support rewind, so users on older or raw-VM workspaces will hit refusals with no guidance. Add the capability and Freestyle app-link requirement to this intro.</violation>

<violation number="2" location="docs/cmux-next/machine-rewind.md:21">
P2: The retry instructions depend on a `--generation` flag and an `expectedGeneration` JSON field that the synopsis above never shows. Document the flag in the rewind synopsis, and confirm the error payload actually carries `expectedGeneration` before advertising it.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudLinkRewindTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudLinkRewindTests.swift:25">
P3: This throws check accepts any `CloudLinkError`, so it would still pass if the stale ticket failed for a different reason. Assert the exact superseded error instead.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/AgentSession/AgentSessionAttachClient.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/AgentSession/AgentSessionAttachClient.swift:304">
P2: `prepareCloudTurn` waits with no timeout, and cancelling the calling task does not release that wait. `ReplySlot.value()` has no cancellation handler, so the abandon in `RemoteAcpmuxWire`'s catch block only runs once the daemon replies or the attachment closes. Either make the wait cancellable (for example, `withTaskCancellationHandler` that calls `abandonCloudTurn` or closes the waiter), or document that cancellation does not end the prepare.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextControl/Routing/ControlError+FeaturePolicy.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextControl/Routing/ControlError+FeaturePolicy.swift:12">
P3: `cloud.turn.state` is added to the Cloud feature map, but nothing checks that name. The shared scope gate in `AppControl+Cloud.swift:54` always checks `"cloud.turn.rewind"`, so a disabled Cloud refuses state with a message naming rewind. Pass the calling method name into the scope gate so each method checks and reports its own name.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudTurnOwnerClientTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudTurnOwnerClientTests.swift:12">
P2: The regression guards in `TurnAPI.startLoading` report through `Issue.record` from the URLProtocol callback, which runs off the test task, so the failure may not be attributed to these tests. Record the requested paths in a lock-protected collector and assert on them with `#expect` after the call returns.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudWorkspaceBindingsTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudWorkspaceBindingsTests.swift:94">
P2: The second case in this loop is labeled as a wrong logical machine, but it is a malformed carrier id that fails the format regex. The `machineId` mismatch branch in `CloudWorkspaceConnection.checked(machine:)` has no coverage. Add a `machineId` parameter to the `connection` helper and pass a different logical id in this loop.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/RemoteAcpmuxWire.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/RemoteAcpmuxWire.swift:471">
P2: The replay decode requires `turn_id` to be a non-empty String, but the daemon's prompt reply sends `turn_id: null` when acpmux omits `turnId`. If a replayed, already-accepted prompt has no turn id, this decode throws and the page gets an error for a prompt that was accepted. Decode `turn_id` as optional and only reject a present but empty value.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudTurnRewindClient.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudTurnRewindClient.swift:61">
P2: This `request` duplicates `CloudTurnOwnerClient.request` line for line, and `valid` duplicates `CloudTurnOwnerClient.isID`. Extract the shared validation and HTTP transport into one internal helper that both clients call, so the owner-auth rules cannot drift.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Wiring.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Wiring.swift:558">
P2: Read the account in the same `MainActor.run` block that resolves the workspace and machine. Otherwise a sign-out or account switch during `await link()` can bind this account's workspace and machine to the new account.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudMachineSession.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudMachineSession.swift:246">
P2: `connectionResolver` can throw `CancellationError` when the workspace scope changes, but this catch treats it as a transport failure and calls `showEnded`. During an account or team transition that leaves the session briefly alive, the stale attempt marks the machine failed and can trigger an erroneous refresh; discard scope cancellation instead of publishing a link failure.

(Based on your team's feedback about discarding cancelled account-scoped refresh results.)</violation>
</file>

<file name="cmux-tui/crates/cmux-tui/src/main.rs">

<violation number="1" location="cmux-tui/crates/cmux-tui/src/main.rs:2643">
P2: `set_cloud_turn_config` runs after the mux is already serving clients, including the Unix socket and WebSocket. A client connecting in that window sees no `agent-session-turn-rewind-v1` capability and can dispatch prompts without an envelope, bypassing capture. Set the config right after the mux is created, before `restoring_server.attach`, so no client is served before the fence is configured.</violation>
</file>

<file name="cmux-tui/crates/cmux-tui/src/cli/machine_rewind.rs">

<violation number="1" location="cmux-tui/crates/cmux-tui/src/cli/machine_rewind.rs:64">
P3: Flag values that start with `-` are accepted, so the next flag name can be swallowed as a value (for example `--workspace --key ...` sets the workspace to `--key`). Reject values beginning with `-` in the value check.

(Based on your team's feedback about rejecting option-like session values.)</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudWorkspaceBindings.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudWorkspaceBindings.swift:62">
P2: This write-back can resurrect a retired binding. If a rewind rebinds the workspace to a new machine during the awaits in `project`, this stale `remember` removes the new binding and re-adds the old one, breaking the next list refresh. Re-check that the snapshot binding is still current before writing it back.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/Link/CloudLinkSession.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/Link/CloudLinkSession.swift:189">
P2: `retarget` closes `previous` even when it equals the new key. On every reconnect of a rewound machine, `cloud.machine.disconnect` tears down the live replacement carrier before `connect` reopens it. Only close when the machine actually changes.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudService.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudService.swift:655">
P2: This guard drops every non-list detail read for a bound workspace because the projected logical row’s ID equals `binding.machine`. Status changes, mutation settlement, and destroy handling therefore never reach `session.listed`; filter retired carrier IDs via `targets` while allowing the logical machine ID through.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

View guided diff | Turn on auto-fix | Re-trigger cubic

@@ -0,0 +1,23 @@
# Rewind a Cloud machine to a turn

Whole-machine rewind restores a captured turn's files, running processes, memory, and database state. It uses the workspace's new Cloud machine and requires its owner to be signed in to the cmux app.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The doc does not say which workspaces support rewind, so users on older or raw-VM workspaces will hit refusals with no guidance. Add the capability and Freestyle app-link requirement to this intro.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/cmux-next/machine-rewind.md, line 3:

<comment>The doc does not say which workspaces support rewind, so users on older or raw-VM workspaces will hit refusals with no guidance. Add the capability and Freestyle app-link requirement to this intro.</comment>

<file context>
@@ -0,0 +1,23 @@
+# Rewind a Cloud machine to a turn
+
+Whole-machine rewind restores a captured turn's files, running processes, memory, and database state. It uses the workspace's new Cloud machine and requires its owner to be signed in to the cmux app.
+
+List captures for the workspace open in the app:
</file context>
Suggested change
Whole-machine rewind restores a captured turn's files, running processes, memory, and database state. It uses the workspace's new Cloud machine and requires its owner to be signed in to the cmux app.
Whole-machine rewind restores a captured turn's files, running processes, memory, and database state. It uses the workspace's new Cloud machine and requires its owner to be signed in to the cmux app. It is available only for new Cloud machines that advertise `agent-session-turn-rewind-v1` on authenticated Freestyle app-link targets; raw VM API targets are not supported.


The app remembers owner-verified workspace bindings for the signed-in account. Refresh and relaunch resolve the workspace's current connection through the owner API before connecting, so a retired source machine cannot replace or remove the restored workspace. An unavailable owner API preserves the last list and refuses an unverified reconnect.

If the connection fails after submission, retry with the same `--key` and original `--generation` printed before submission (also returned as `expectedGeneration` in the JSON result or error). Use the complete retry command printed on stderr. The platform returns the existing result for that key; it does not allocate another replacement machine. Use a new key for a later intentional rewind. An owner token never appears in this command's arguments or output.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The retry instructions depend on a --generation flag and an expectedGeneration JSON field that the synopsis above never shows. Document the flag in the rewind synopsis, and confirm the error payload actually carries expectedGeneration before advertising it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/cmux-next/machine-rewind.md, line 21:

<comment>The retry instructions depend on a `--generation` flag and an `expectedGeneration` JSON field that the synopsis above never shows. Document the flag in the rewind synopsis, and confirm the error payload actually carries `expectedGeneration` before advertising it.</comment>

<file context>
@@ -0,0 +1,23 @@
+
+The app remembers owner-verified workspace bindings for the signed-in account. Refresh and relaunch resolve the workspace's current connection through the owner API before connecting, so a retired source machine cannot replace or remove the restored workspace. An unavailable owner API preserves the last list and refuses an unverified reconnect.
+
+If the connection fails after submission, retry with the same `--key` and original `--generation` printed before submission (also returned as `expectedGeneration` in the JSON result or error). Use the complete retry command printed on stderr. The platform returns the existing result for that key; it does not allocate another replacement machine. Use a new key for a later intentional rewind. An owner token never appears in this command's arguments or output.
+
+The edited-files card's Undo action remains a file revert. Whole-machine rewind is a separate operation.
</file context>


/// Reserves the prompt's immutable turn identity without dispatching the harness. The owning
/// daemon answers when the queued turn can capture; closing this attachment ends the wait.
public func prepareCloudTurn(id: String, scope: Data) async throws -> Data {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: prepareCloudTurn waits with no timeout, and cancelling the calling task does not release that wait. ReplySlot.value() has no cancellation handler, so the abandon in RemoteAcpmuxWire's catch block only runs once the daemon replies or the attachment closes. Either make the wait cancellable (for example, withTaskCancellationHandler that calls abandonCloudTurn or closes the waiter), or document that cancellation does not end the prepare.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/AgentSession/AgentSessionAttachClient.swift, line 304:

<comment>`prepareCloudTurn` waits with no timeout, and cancelling the calling task does not release that wait. `ReplySlot.value()` has no cancellation handler, so the abandon in `RemoteAcpmuxWire`'s catch block only runs once the daemon replies or the attachment closes. Either make the wait cancellable (for example, `withTaskCancellationHandler` that calls `abandonCloudTurn` or closes the waiter), or document that cancellation does not end the prepare.</comment>

<file context>
@@ -299,9 +299,17 @@ public actor AgentSessionAttachClient {
-        try await call("agent-session-turn-prepare", ["prompt_id": id])
+    /// Reserves the prompt's immutable turn identity without dispatching the harness. The owning
+    /// daemon answers when the queued turn can capture; closing this attachment ends the wait.
+    public func prepareCloudTurn(id: String, scope: Data) async throws -> Data {
+        struct Scope: Decodable { let workspace: String; let machine: String; let generation: Int }
+        let scope = try JSONDecoder().decode(Scope.self, from: scope)
</file context>

override func startLoading() {
guard let url = request.url else { return }
if url.host == "original-generation.test", url.path.hasSuffix("/turns") {
Issue.record("Capture must use the owner generation read before guest prepare, without re-reading")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The regression guards in TurnAPI.startLoading report through Issue.record from the URLProtocol callback, which runs off the test task, so the failure may not be attributed to these tests. Record the requested paths in a lock-protected collector and assert on them with #expect after the call returns.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudTurnOwnerClientTests.swift, line 12:

<comment>The regression guards in `TurnAPI.startLoading` report through `Issue.record` from the URLProtocol callback, which runs off the test task, so the failure may not be attributed to these tests. Record the requested paths in a lock-protected collector and assert on them with `#expect` after the call returns.</comment>

<file context>
@@ -8,6 +8,12 @@ import Testing
         override func startLoading() {
             guard let url = request.url else { return }
+            if url.host == "original-generation.test", url.path.hasSuffix("/turns") {
+                Issue.record("Capture must use the owner generation read before guest prepare, without re-reading")
+            }
+            if url.host == "registration-fails.test", !url.path.hasSuffix("/turns") {
</file context>


@Test func rawVMAPIOrWrongLogicalMachineNeverBecomesAnAppLink() async throws {
let binding = try binding(), record = record()
for connection in [connection(provider: "freestyle-vm-api"), connection(machine: "vm_abcdefghijklmnopqrstuvwxyz"), connection(generation: -1)] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The second case in this loop is labeled as a wrong logical machine, but it is a malformed carrier id that fails the format regex. The machineId mismatch branch in CloudWorkspaceConnection.checked(machine:) has no coverage. Add a machineId parameter to the connection helper and pass a different logical id in this loop.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudWorkspaceBindingsTests.swift, line 94:

<comment>The second case in this loop is labeled as a wrong logical machine, but it is a malformed carrier id that fails the format regex. The `machineId` mismatch branch in `CloudWorkspaceConnection.checked(machine:)` has no coverage. Add a `machineId` parameter to the `connection` helper and pass a different logical id in this loop.</comment>

<file context>
@@ -0,0 +1,114 @@
+
+    @Test func rawVMAPIOrWrongLogicalMachineNeverBecomesAnAppLink() async throws {
+        let binding = try binding(), record = record()
+        for connection in [connection(provider: "freestyle-vm-api"), connection(machine: "vm_abcdefghijklmnopqrstuvwxyz"), connection(generation: -1)] {
+            await #expect(throws: CloudTurnRewindClient.Failure.self) {
+                try await CloudWorkspaceProjection.read(binding: binding, machine: { _ in record }, connection: { _ in connection })
</file context>

let previous = connectionKey
connectionKey = CloudLinkKey(machine: machine)
machineGeneration = generation
await resolver.close(previous)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: retarget closes previous even when it equals the new key. On every reconnect of a rewound machine, cloud.machine.disconnect tears down the live replacement carrier before connect reopens it. Only close when the machine actually changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextCloud/Link/CloudLinkSession.swift, line 189:

<comment>`retarget` closes `previous` even when it equals the new key. On every reconnect of a rewound machine, `cloud.machine.disconnect` tears down the live replacement carrier before `connect` reopens it. Only close when the machine actually changes.</comment>

<file context>
@@ -170,7 +173,30 @@ public actor CloudLinkSession {
+        let previous = connectionKey
+        connectionKey = CloudLinkKey(machine: machine)
+        machineGeneration = generation
+        await resolver.close(previous)
+        return machineGeneration == generation && connectionKey.machine == machine
+    }
</file context>
Suggested change
await resolver.close(previous)
if previous != connectionKey { await resolver.close(previous) }

defer { finishRecordRead(read) }
// A provider detail about the retired source must not destroy its logical workspace.
if !workspaceRead, let scope = workspaceBindingScope {
guard let bindings = try? workspaceBindings.bindings(in: scope), !bindings.contains(where: { $0.machine == machine.id }) else { return }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This guard drops every non-list detail read for a bound workspace because the projected logical row’s ID equals binding.machine. Status changes, mutation settlement, and destroy handling therefore never reach session.listed; filter retired carrier IDs via targets while allowing the logical machine ID through.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudService.swift, line 655:

<comment>This guard drops every non-list detail read for a bound workspace because the projected logical row’s ID equals `binding.machine`. Status changes, mutation settlement, and destroy handling therefore never reach `session.listed`; filter retired carrier IDs via `targets` while allowing the logical machine ID through.</comment>

<file context>
@@ -618,9 +647,13 @@ final class CloudService {
         defer { finishRecordRead(read) }
+        // A provider detail about the retired source must not destroy its logical workspace.
+        if !workspaceRead, let scope = workspaceBindingScope {
+            guard let bindings = try? workspaceBindings.bindings(in: scope), !bindings.contains(where: { $0.machine == machine.id }) else { return }
+        }
         guard !policyDisabled, machine.id == session.machineID,
</file context>
Suggested change
guard let bindings = try? workspaceBindings.bindings(in: scope), !bindings.contains(where: { $0.machine == machine.id }) else { return }
guard let bindings = try? workspaceBindings.bindings(in: scope),
!bindings.contains(where: { $0.machine != machine.id && $0.targets.contains(machine.id) }) else { return }

#expect(link.key == logical)
#expect(await link.ownerLink == nil)
#expect(await link.isConnected(machine: "vm_replacement", generation: 1) == false)
await #expect(throws: CloudLinkError.self) { try await link.endpoint(original) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This throws check accepts any CloudLinkError, so it would still pass if the stale ticket failed for a different reason. Assert the exact superseded error instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudLinkRewindTests.swift, line 25:

<comment>This throws check accepts any `CloudLinkError`, so it would still pass if the stale ticket failed for a different reason. Assert the exact superseded error instead.</comment>

<file context>
@@ -0,0 +1,39 @@
+        #expect(link.key == logical)
+        #expect(await link.ownerLink == nil)
+        #expect(await link.isConnected(machine: "vm_replacement", generation: 1) == false)
+        await #expect(throws: CloudLinkError.self) { try await link.endpoint(original) }
+        let replacement = try await link.connect(origin: .user)
+        #expect(try await link.endpoint(replacement) == "/tmp/vm_replacement.sock")
</file context>
Suggested change
await #expect(throws: CloudLinkError.self) { try await link.endpoint(original) }
await #expect(throws: CloudLinkError.disconnected(reason: "superseded")) { try await link.endpoint(original) }

switch method {
// A new sandboxed run is Cloud; waiting on or cancelling one that exists is its cleanup.
case "auth.token", "cloud.machines", "cloud.automation", "debug.cloud.link_ensure", "runs.sandbox.create", "vm.agent_key.create": self = .cloud
case "auth.token", "cloud.machines", "cloud.automation", "cloud.turn.state", "cloud.turn.rewind", "debug.cloud.link_ensure", "runs.sandbox.create", "vm.agent_key.create": self = .cloud

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: cloud.turn.state is added to the Cloud feature map, but nothing checks that name. The shared scope gate in AppControl+Cloud.swift:54 always checks "cloud.turn.rewind", so a disabled Cloud refuses state with a message naming rewind. Pass the calling method name into the scope gate so each method checks and reports its own name.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextControl/Routing/ControlError+FeaturePolicy.swift, line 12:

<comment>`cloud.turn.state` is added to the Cloud feature map, but nothing checks that name. The shared scope gate in `AppControl+Cloud.swift:54` always checks `"cloud.turn.rewind"`, so a disabled Cloud refuses state with a message naming rewind. Pass the calling method name into the scope gate so each method checks and reports its own name.</comment>

<file context>
@@ -9,7 +9,7 @@ extension ActionFeature {
         switch method {
         // A new sandboxed run is Cloud; waiting on or cancelling one that exists is its cleanup.
-        case "auth.token", "cloud.machines", "cloud.automation", "debug.cloud.link_ensure", "runs.sandbox.create", "vm.agent_key.create": self = .cloud
+        case "auth.token", "cloud.machines", "cloud.automation", "cloud.turn.state", "cloud.turn.rewind", "debug.cloud.link_ensure", "runs.sandbox.create", "vm.agent_key.create": self = .cloud
         case "remote.machines": self = .remoteHosts
         default:
</file context>

};
let value =
words.next().ok_or_else(|| UsageError::new(format!("{flag} needs a value")))?;
if target.is_some() || !valid_id(value) || (flag == "--key" && value.len() > 128) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Flag values that start with - are accepted, so the next flag name can be swallowed as a value (for example --workspace --key ... sets the workspace to --key). Reject values beginning with - in the value check.

(Based on your team's feedback about rejecting option-like session values.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/cli/machine_rewind.rs, line 64:

<comment>Flag values that start with `-` are accepted, so the next flag name can be swallowed as a value (for example `--workspace --key ...` sets the workspace to `--key`). Reject values beginning with `-` in the value check.

(Based on your team's feedback about rejecting option-like session values.) </comment>

<file context>
@@ -0,0 +1,265 @@
+            };
+            let value =
+                words.next().ok_or_else(|| UsageError::new(format!("{flag} needs a value")))?;
+            if target.is_some() || !valid_id(value) || (flag == "--key" && value.len() > 128) {
+                return Err(UsageError::new(format!("invalid or repeated {flag}")));
+            }
</file context>
Suggested change
if target.is_some() || !valid_id(value) || (flag == "--key" && value.len() > 128) {
if target.is_some()
|| !valid_id(value)
|| value.starts_with('-')
|| (flag == "--key" && value.len() > 128)
{

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 5602810: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr30371 -f ref=lane/cloud-turn-native-bridge -f platform=macos -f backend_mode=local

web and size checks: green

Default changes

No defaults change at 56028105b54b.

@blacksmith-sh

This comment has been minimized.

…enrollment

The signed Native-owner entry now admits agent-session-turn-prepare and
agent-session-turn-abandon and advertises the turn capability, so native
Cloud chats can prepare their turns; a test drives both through the signed
remote entry. Enrollment reconnects a dropped Cloud link before requiring
its owner answer, then checks the fresh link owner before enrolling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-159afad10a5ab45ea16c
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6-astra high, next-review): BLOCK at 6bc6667

BLOCK at 6bc6667
Visual verdict: no images; Leo tries the build

  1. [P1] [blocking] Preserve chats across multiple workspaces on one machine. CloudService.swift:600 enrolls each tab’s workspace against the same machine, but the guest accepts only one startup workspace (agent_session_turn.rs:160). Two existing workspaces therefore cannot both send through that daemon: rejecting the second enrollment blocks its chat, while replacing the scope breaks the first. Use a consistent machine-wide enrollment identity and cover two workspaces sending concurrently.

  2. [P1] [blocking] Gate enrollment on the actual attachment transport. AgentTabs+Wiring.swift:415 checks the global configuration rather than the session’s appLink. In the default Debug configuration, existing CloudAgentChatTests construct legacy-link sessions but now enter enrollment; ownedTurnScope necessarily rejects their missing appLink, so World.page() throws before the chat tests reach their assertions. Preserve the legacy path and rerun this suite.

  3. [P2] [follow-up] Retarget owner RPCs after rewind. CloudMachineSession.swift:511 updates the daemon transport target, but restartWithApprovals still sends keyed-start and person-holds requests using session.machineID. Those endpoints resolve the original CloudDO row directly, so approval recovery reaches the retired source or fails after replacement. Resolve the verified carrier for these requests while retaining logical identity for local bookkeeping.

Follow-ups:

  • Finding 3: route approval recovery to the verified replacement carrier.

Copy link
Copy Markdown
Collaborator Author

Independent HQ 07 review — uncaptured sends can overlap turn capture (BLOCK; preserve HOLD)

Exact reads (2026-11-03): this native PR head 6bc6667; cmux feat-cmux-next 786f51b; cmux-platform main 794483e7aaee755341c8cbfaf334a7b2148111df. Existing owner remains teamleaderleo / #30371's claimed session. This extends the earlier 07 result and W2-04 with a current source path and a second-session counterexample.

Five code facts:

  1. RemoteAcpmuxWire.swift L232-L259 awaits preparation, then external capture, then sends P1.
  2. acpmux session.rs L574-L596 releases its turn-lock guard when preparation returns; its pending reservation is session-local.
  3. Guest agent_session_turn.rs L222-L255 explicitly accepts an absent Cloud execution envelope on a configured machine; guest tests L115-L140 protect that legacy fallback.
  4. acpmux turns.rs L377-L394 validates preparation only when an execution envelope exists. An ordinary send continues to queue admission L557-L590.
  5. The platform's durable gate fences platform calls but cannot prevent unwrapped guest writes during provider snapshot.

Reproduction to write as a real guest regression: prepare session A / prompt P1, then stall its provider snapshot. Send ordinary unwrapped P2 to A, and P3 to a second chat B on the same VM. Require zero guest turn_started events, zero harness calls and zero machine mutations from P2/P3 until P1's durable snapshot receipt and P1 dispatch admission. Only then drain P2/P3 behind it. Repeat with restart-restored queued prompt, abandonment, uncertain capture/lost reply, and old-generation send. The exact absent-envelope path must be tested; preserving compatibility with old clients outside a pending capture is useful. A session-only pending guard fails the second-chat case.

Recommended first slice: reserve a machine-scoped capture-admission epoch before returning prepared; hold it across external snapshot -> durable identity/coverage receipt -> intended turn dispatch. During the epoch, refuse an unwrapped send with a typed capture-pending result or queue it durably behind P1. On uncertain provider responses, reconcile the original operation key before freeing the barrier. Rewind invalidates queued old-generation work and pending permission approvals. Autonomous jobs, terminals and external services need a separate quiescence or explicit provider-coverage contract; prompt admission alone cannot freeze the entire VM.

Saved experiment: HQ draft PR #6640 includes source-level acceptance review and five-test deterministic Python model. All five offline model tests passed, demonstrating the currently permitted ordering, session-only limitation, proposed machine-wide ordering, remaining autonomous writers and external-effect persistence. Evidence limit: source inspection + synthetic Python only; no actual guest race, Rust/Swift product test, provider VM, live capture, signed app or deployment.

This respects your existing HOLD and the current BLOCK findings. The native PR stays under its owner; I made no changes to that branch, approvals, credentials, VM state or deployment.

Base moved prompts to ordered ACP content blocks (images, file links).
A Cloud turn now sends its prepared capability with those content blocks
instead of text only: `prompt(id:content:cloudTurn:)` replaces the
text-only variant, and the attach client shares one content path (with
the legacy text fallback for daemons that do not serve content) for
both fenced and ordinary prompts. The guest daemon's prompt takes text
or content plus the optional cloud_turn envelope.

Takes base's CloudLinkChange(event) parsing and its CloudLinkAfterUpdate
move; keeps the rewind reconnect and the connection-machine lookup.

Agent-Session: https://cmux.com/deeplink/agent?id=aid-b666f9cd0f6b93c6ec77
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Merged feat-cmux-next (cea84c3) at e034c01. Base moved prompts to ordered ACP content blocks (images, file links), so the re-port follows that shape:

  • A Cloud turn now sends its prepared capability with the content blocks: prompt(id:content:cloudTurn:) replaces the text-only fenced variant in AgentSessionRemoteClient, AgentTabRemoteClient and AgentSessionAttachClient. The attach client shares one content path for fenced and ordinary prompts, including base's text fallback for daemons that do not serve content.
  • RemoteAcpmuxWire keeps the prepare, capture, submit and abandon sequence; the unfenced path is base's (text when the blocks are text, content otherwise).
  • In agent_session_attach.rs, PromptParams takes base's text/content plus the optional cloud_turn envelope, and prompt() uses base's block validation before it inserts the envelope.
  • Takes base's CloudLinkChange(event) parsing and its CloudLinkAfterUpdate move; keeps reconnectAfterRewind and the connection-machine lookup.

Fleet compile (CmuxNext + app, cmux-tui) and the cmux-tui-core attach/turn tests are running at this head.

The HOLD stands: real native capture, rewind, fresh reply, refresh and relaunch readback. The BLOCK findings at 6bc6667 (two workspaces on one machine, gating enrollment on the session's own app link) and the capture-admission review are still open. This merge does not touch them.

teamleaderleo and others added 2 commits November 4, 2026 21:55
…sion epoch

Two workspaces on one machine: the guest serves one startup workspace
scope, so every chat on a machine now enrolls and captures under that
machine's one verified binding (CloudWorkspaceBindings.enrollmentWorkspace).
A tab's own workspace is used only for a first enrollment, and concurrent
tabs in different workspaces join one enrollment keyed by the machine.

Enrollment gate: a tab turns on capture only when its own machine session
is on the app link, not when the build's link setting says app server, so
legacy-link sessions keep their plain prompt path.

Capture admission: the guest daemon holds a machine-wide epoch from a
prepared turn's return until its fenced prompt is recorded by acpmux, or
it is abandoned, expires or its attachment ends. Meanwhile every other
prompt on the machine, plain or another chat's, and every second
preparation is refused with agent_session.capture_pending; a contended
preparation abandons its reservation at once.

Agent-Session: https://cmux.com/deeplink/agent?id=aid-b666f9cd0f6b93c6ec77
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Fixes at 54085bc (feat-cmux-next merged first) for the open findings:

  1. P1, two workspaces on one machine. Every chat on a machine now enrolls and captures under that machine's one verified binding (CloudWorkspaceBindings.enrollmentWorkspace). A tab's own workspace is used only for the machine's first enrollment. CloudWorkspaceEnrollment is keyed by machine, so concurrent tabs in different workspaces join one enrollment and never replace each other's scope. Tests: CloudWorkspaceEnrollmentTests.tabsInDifferentWorkspacesOnOneMachineShareOneEnrollment, CloudWorkspaceBindingsTests.everyWorkspaceOnABoundMachineUsesItsOneEnrollment.
  2. P1, enrollment gate. Capture and enrollment now turn on only when the tab's own machine session is on the app link (AgentTabStore.capturesCloudTurns), not when the global link setting says so. Legacy-link sessions keep their plain prompt path. Test: CloudMachineChangeStateTests.onlyAnAppLinkSessionCapturesCloudTurns; CloudAgentChatTests passes.
  3. Uncaptured sends overlapping capture. The guest daemon now holds a machine-wide capture admission epoch. It starts when a prepared turn returns and lasts until acpmux records that turn's fenced prompt, or until the turn is abandoned, expires or its attachment ends. While it is held, every other prompt on the machine is refused with agent_session.capture_pending: plain prompts, prompts from other chats, and second preparations. A contended preparation abandons its reservation at once. Test: pending_capture_fences_every_other_prompt_on_the_machine (two chats). Not covered: turns already running, autonomous jobs and non-chat writers. Prompt admission cannot freeze those.

Checks at this head: cmux-tui-core agent_session 197 passed, 0 failed; fleet compile of CmuxNext and the app passed; six Swift suites passed (CloudWorkspaceEnrollment, CloudWorkspaceBindings, CloudMachineChangeState, CloudAgentChat, RemoteAcpmuxCloudTurn, CloudTurnAttachment).

The HOLD is unchanged: real native capture, rewind, fresh reply, refresh and relaunch readback.

CloudConfiguration keeps base's relayAPIBaseURL beside this branch's turnAPIBaseURL.

Agent-Session: https://cmux.com/deeplink/agent?id=aid-b666f9cd0f6b93c6ec77
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base moved Cloud origins to the runtime selection Rust validates, so
turnAPIBaseURL now returns that selection's platformOrigin (the same
cmux-platform Worker the old literal mapping named) on the app link.
With no selection, or on the legacy link, it returns no origin and no
bearer is sent. Base's relay, share and owner origins are unchanged.
CloudWorkspaceImportTests now pin the turn origin to the selection.

Agent-Session: https://cmux.com/deeplink/agent?id=aid-b666f9cd0f6b93c6ec77
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

3 inactive deployments
ci-routing-agent — 56028105 Deployed Nov 5, 2026 by github-actions[bot] via cmux-next macOS placement #71574
artifacts — 56028105 Deployed Nov 5, 2026 by teamleaderleo via publish tree-addressed cmux-tui #113979
sentry-dsyms — 56028105 Deployed Nov 5, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #113979
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants