Skip to content

fix(workspace): isolate Peer requests and preserve SSH session storage - #3341

Merged
bobleer merged 10 commits into
GCWing:mainfrom
bobleer:bob/peer-ssh-workspace-isolation
Oct 10, 2026
Merged

bobleer merged 10 commits into
GCWing:mainfrom
bobleer:bob/peer-ssh-workspace-isolation

Conversation

@bobleer

@bobleer bobleer commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fix workspace and session ownership when switching Peer devices or opening local and SSH projects with the same path. Asynchronous request preparation could send the previous device's workspace ID to the newly selected host; backend history restore and some persistence paths then resolved the execution path again instead of retaining the session's admitted storage owner.

  • Capture the device activation before request preparation, preserve cancellation through error translation, and fence multi-step requests, streaming searches, catalog events, Git probes, and ACP caches/configuration updates. Returning A → B → A starts fresh pending work. An ESLint rule now rejects await inside api.invoke(...) arguments, the remaining hand-written scope sites use invokePrepared, and invokePrepared honors controller-local commands like ApiClient.
  • Key settled caches by device as well as workspace ID, because same-path local workspaces hash to one ID on every device: Git trust prompts (per activation, re-checked before granting), review platform snapshot/detail caches and launch dedupe, and MCP tool descriptions.
  • Resolve legacy workspace selectors once through the catalog, then use the shared ID resolver. Session restore, turn writes, interruption recovery, deletion, autosave, eviction, and internal queued continuations reuse the committed storage binding and the owning PathManager. Reading a binding never commits one, and readers never follow an uncommitted index entry.
  • Preserve worktree ownership (pre-ID sessions also resolve against recent workspaces), old SSH payload fields, unavailable workspace history, and canonical filesystem aliases. Reject ambiguous selectors without replacing or deleting records.

SSH connections sharing one host and root

The supported SSH storage layout remains host plus remote root, so two saved connections to the same host and root share a workspace record and session mirror. Activation does not reject such records; session identity verification keeps their histories apart, and imported records for each connection stay listed and activatable.

Reopening an existing remote record with a different connection rebinds it only for an allowed reason:

  • the IDs are equivalent (legacy ssh-user@host:port versus current ssh-user@host; one side is canonicalized at a time so bare IPv6 hosts are never merged);
  • the previous owner is no longer a saved SSH connection;
  • the user confirmed, sent as the new optional rebindConnection field on open_remote_workspace (Desktop and CLI peer host).

Otherwise the command fails with remote_workspace_connection_conflict: ... as the whole error string. The interactive Web UI asks the user and retries with rebindConnection; cancelling restores the previous remote workspace. The entire lookup, confirmation, retry, and activation carries the initiating device activation, including A → B → A cancellation. Startup restore checks record ownership before updating the host remote pointer, both for an existing connection and a newly reconnected transport, and defers conflicts with a localized notification (en-US, zh-CN, zh-TW). A device switch abandons stale restore work without publishing into the next surface. Supporting simultaneous distinct endpoints for one host and root still requires a versioned storage-identity migration.

Upgrade compatibility

  • Persisted workspace, session, and connection shapes are unchanged. Legacy port-qualified connection IDs are recognized as the same connection and migrated on reopen.
  • rebindConnection is additive with #[serde(default)]; older hosts accept and ignore it and keep their previous rebind behavior.
  • An older controller talking to a newer host receives the stable conflict error rather than a silent rebind. Its generic error display is loud, not destructive.
  • No record is deleted or reset on conflict.

Type and Areas

Type: bug fix / regression fix

Areas: Rust core workspace/session/coordination, Desktop and CLI open_remote_workspace, Web UI API, Peer, SSH remote and review boundaries, architecture documentation.

Verification

Validated after the final SSH ownership and activation fix (c178f0da2):

  • SSH remote, Peer Device Mode, workspace manager, and prepared-invoke tests: 22 files, 194 tests passed, including 27 SSH provider tests. Regression cases were observed failing before the fix and passing afterward; they cover confirm-time device switches, A → B → A, cancelled profile lookup, delayed workspace responses, restore ownership ordering, and switches during connection probe/reconnect/ownership lookup.
  • pnpm run check:web, focused ESLint for both changed files, and git diff --check passed.

Broader verification recorded on the preceding head (65de9742a):

  • Web UI full suite: 964 files, 10641 tests passed. Local Node 26 needs NODE_OPTIONS=--no-experimental-webstorage so jsdom provides localStorage. New tests cover connection-conflict confirm/cancel/background restore, conflict error parsing, pre-ID worktree sessions, controller-local invokePrepared, cross-device Git trust answers, review cache reuse across devices (observed failing before the fix), and per-device tool info.
  • tsc --noEmit passed; eslint . reports 0 errors (10 pre-existing warnings in untouched files); pnpm run i18n:audit, pnpm run check:core-boundaries, pnpm run check:repo-hygiene, and pnpm run canvas:sdk:check passed. check:web:appearance tests passed; its generated-artifact steps need built design-system packages and are left to CI.
  • Rust openbitfun-core --features product-full --lib: 2558 passed, including the four previously failing CI tests.
  • Owner filters with --no-default-features --features agent-runtime,git,remote-workspace: workspace 45, session 192, coordination 212 passed.
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git,remote-workspace --lib service::workspace::
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git,remote-workspace --lib agentic::session::
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git,remote-workspace --lib agentic::coordination::
  • openbitfun-cli peer_host:: tests (102), openbitfun-legacy-migration-adapters (55), and openbitfun-services-core --features workspace-persistence workspace_identity passed. cargo check passed for openbitfun-desktop --lib and openbitfun-cli --all-targets.
  • pnpm run fmt:rs, git diff --check, and node scripts/check-git-object-sizes.mjs passed. Remaining compiler warnings are in files this PR does not touch.

Reviewer Notes

AI-assisted. Testing level: lightly tested in the contributor-guide terminology; focused automated coverage is extensive, but live remote integration remains unverified.

Remote scenarios exercised: Peer Device Mode with mocked surface activations; remote workspace (SSH) with catalog/storage fixtures and the CLI peer host command path. Remote Control (mobile web, IM bots) does not open remote workspaces, so the conflict cannot reach it. Detached Dispatch: the dispatch-hold paths now return an error while a storage claim is contested instead of following an uncommitted location, covered by unit tests only. No live two-device Peer, SSH transport, mobile Remote Control, or Detached Dispatch end-to-end run was performed.

The only wire change is the optional rebindConnection request field. No command, Cargo feature, dependency, or runtime ownership migration is introduced.

Checklist

  • This PR is focused and does not include secrets, temporary prompts, generated scratch files, or unrelated artifacts.
  • Relevant verification is recorded above, or skipped checks are explained.
  • User-facing strings, docs, and locales are updated where applicable.

Two saved SSH connections to one host and root share a workspace record
and session mirror. Rejecting such records at activation broke imported
catalogs and the same-host multi-account case, so activation no longer
validates storage owners; session identity verification keeps histories
apart.

Reopening an existing remote record with a different connection now
rebinds it only when the ids are equivalent (legacy ssh-user@host:port),
the previous owner is no longer saved, or the user confirmed through the
new optional rebindConnection field on open_remote_workspace. Otherwise
Desktop and the CLI peer host return the stable
remote_workspace_connection_conflict code as the whole error message.
Older hosts ignore the field.

The legacy migration adapter and core now share one canonical SSH
connection id helper. Equivalence canonicalizes one side at a time so
current ids for bare IPv6 hosts are never merged.
When opening a remote folder hits remote_workspace_connection_conflict,
the Web UI asks whether to move the workspace to the selected connection
and retries with rebindConnection only after confirmation. Cancelling
restores the previous remote workspace and keeps the file browser open,
and the record is opened before the host-side workspace is switched so
cancellation leaves no host state behind.

Startup restore never rebinds silently: it defers with a localized
notification and an error status. The conflict is not stored as the
workspace manager's error state.
require_session_storage_path no longer commits a binding while reading.
Before admission it resolves from the session's workspace configuration
and validates against any pending claim, so a reader during a contested
claim fails instead of following an uncommitted location. Turn lookup,
compression transcripts, model exchange traces, reference
materialization, evidence persistence and evicted-session restores read
only committed bindings.

The filesystem alias test now places its user root below the alias,
because product home is derived from the user root's parent.
Legacy session metadata without a workspaceId was matched only against
opened workspaces. A worktree session whose worktree is not opened then
had no owner. Resolution now also considers recent workspace records, and
a regression test covers a pre-ID worktree session listed under its
project without borrowing the project's execution identity.
Add an ESLint rule that rejects await inside api.invoke(...) arguments,
because the device surface is captured only after they resolve. Migrate
the remaining hand-written getActiveSurfaceScope plus assertCurrent sites
in AgentAPI session restore and listing, WorkspaceAPI file operations,
CronAPI, SnapshotAPI, EditorDocument and TerminalService to
invokePrepared, which also checks the activation after the response.

invokePrepared now skips activation checks for controller-local commands
from the generated registry, matching ApiClient.
Same-path local workspaces hash to one workspace ID on every device, so
module-level caches keyed only by workspace ID leaked across Peer
devices.

- Git trust prompts dedupe per activation epoch, re-check the activation
  before granting, and never grant an answer from a departed device. The
  Git scene treats that cancellation quietly.
- Review platform snapshot and detail caches key by the rendered surface
  and clear on activation; review launch dedupe is surface scoped.
- Tool info descriptions are cached per surface.
- In-flight maps delete entries only when they still own them.
Replace the removed remote_workspace_storage_conflict activation rule
with the connection rebind policy, document side-effect free storage
binding reads, the invoke lint guard, controller-local exemption and
surface-scoped caches, and list the coordination owner filter.
@bobleer
bobleer merged commit d79af1a into GCWing:main Oct 10, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant