Repository navigation
Cloud: capture remote turns and reconnect owner rewinds durably - #30371
teamleaderleo wants to merge 40 commits into
Conversation
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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>
| 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)) | ||
| } |
There was a problem hiding this comment.
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>
| 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.") |
There was a problem hiding this comment.
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>
| 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)) } |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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")) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 }) |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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>
| @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) |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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>
c98d4eb to
d6ce3e6
Compare
8d9ead9 to
53c78ec
Compare
53c78ec to
75b6289
Compare
Bugbot needs on-demand usage enabledBugbot 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. |
There was a problem hiding this comment.
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. | |||
There was a problem hiding this comment.
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>
| 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. |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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)] { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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>
| 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 } |
There was a problem hiding this comment.
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>
| 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) } |
There was a problem hiding this comment.
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>
| 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 |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.)
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>
| 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) | |
| { |
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
web and size checks: green Default changesNo defaults change at |
This comment has been minimized.
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
|
Review (codex gpt-6-astra high, next-review): BLOCK at 6bc6667 BLOCK at 6bc6667
Follow-ups:
|
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:
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>
|
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:
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. |
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b666f9cd0f6b93c6ec77 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
|
Fixes at 54085bc (feat-cmux-next merged first) for the open findings:
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>
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 turnslists captures;cmux machine rewindrestores 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-binaryshutdown --keep-agents/starthandoff 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:
The owner-scoped
cloud.turn.connectionread 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