feat(cli): add Team-safe cloud push/pull and gate sync to Personal workspaces#917
Conversation
…rkspaces Adds `bm cloud push` and `bm cloud pull` as git-style, fail-safe transfer primitives that are usable on Team workspaces (issue #858), and restricts the destructive `bm cloud sync` mirror to Personal workspaces. Why - `bm cloud sync` is a destructive local->cloud mirror; on a shared Team bucket it can delete a teammate's files. The only pull path was two-way `bisync`, which is already Personal-only (#849). - Teams need a safe way to fetch teammates' notes and add their own without one stale local tree becoming authoritative for shared cloud state. What - push = `rclone copy` local->cloud, pull = `rclone copy` cloud->local. Both are additive (never delete on the destination), so neither can damage shared state. - Conflicts (a file that differs on both sides) abort by default and list the paths, like git refusing to clobber local changes / rejecting a stale push. `--on-conflict {fail|keep-local|keep-cloud|keep-both}` lets the user decide; no vague --force, no silent winner. - `sync` now requires a Personal workspace; its guard and the bisync guard point Team users at push/pull. Limitations (surfaced in --help and command output, tracked by #862): - No sync baseline yet, so deletions are not propagated and every divergence is treated as a conflict rather than auto-resolved. Real three-way merge needs the per-client manifest + Tigris snapshot baseline designed in #862. Implementation - rclone_commands.py: shared `_build_transfer_cmd`/`_transfer_endpoints`; refactor `project_sync` onto them (no behavior change); add `project_diff` (conflict detection via `rclone check --combined`), `project_copy`, `project_copy_file`, and `project_transfer` (strategy dispatch). - project_sync.py: new `push`/`pull` commands (ungated, Team-safe) with `--on-conflict`/`--dry-run`; gate `sync` to Personal via a per-command guard message. - Tests at the rclone-argv and CLI-command levels; existing sync/bisync tests updated for the new gating and messages. Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 2m 57s —— View job PR Review: feat(cli): add Team-safe cloud push/pull and gate sync to Personal workspacesTodo List
SummaryThis is a well-structured PR that introduces additive Checklist Results
Issues Found1.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e23363606a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| try: | ||
| # Get tenant info for bucket name | ||
| tenant_info = run_with_cleanup(get_mount_info()) |
There was a problem hiding this comment.
Resolve mount info for the project workspace
For projects attached to a non-default Team workspace, this fetches mount info without the same workspace context used by _get_cloud_project below, so the rclone bucket can be the user's default/personal tenant while the project metadata came from the Team tenant. In that configuration bm cloud pull/push --name ... either reads the wrong bucket or uploads the local Team files into the wrong workspace while reporting success; pass the resolved project workspace as an X-Workspace-ID header when obtaining mount info/credentials.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch, and confirmed: get_mount_info() (and generate_mount_credentials) hit /tenant/mount/info with no workspace context, so bucket_name is the account-default tenant's, while _get_cloud_project resolves the project across workspaces. For a project on a non-default Team workspace with its own bucket, the rclone remote could be wrong.
This is a pre-existing pattern (the existing sync/bisync use get_mount_info() identically), but push/pull are the first commands intended to run against a Team bucket, so the PR newly exposes it. The correct fix needs the backend to support a workspace-scoped mount-info/credentials endpoint (e.g. honoring an X-Workspace-ID header), so I don't want to fake it client-side here. Flagging to @phernandez to decide between (a) backend workspace-scoped mount info, or (b) resolving + asserting the project's workspace tenant in _run_directional_transfer until then. Tracking separately rather than blocking this PR.
| result = run(cmd, capture_output=True, text=True) | ||
| return _parse_check_combined(result.stdout) |
There was a problem hiding this comment.
Abort when rclone check itself fails
This ignores rclone check's return code entirely; although differences can produce a non-zero status, rclone's documented CLI contract is still that operational errors are reported via non-zero exits, and fatal failures may have only stderr rather than --combined entries. When the check cannot list/compare the remote (auth, missing remote, network, bad filter, etc.), this returns an empty plan and the caller proceeds to transfer without the conflict-detection gate that makes push/pull safe; treat non-zero with empty/invalid combined output as a RcloneError instead of assuming there are no conflicts.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in c850450 — project_diff now raises RcloneError (with rclone's stderr) when rclone check exits non-zero and produces no combined listing, so a fatal auth/network/missing-remote failure aborts instead of running a no-op transfer that reports success. A normal 'differences found' non-zero exit still parses fine.
Code-review finding: project_copy_file built its `rclone copyto` command inline and omitted `--local-no-preallocate`. On a `pull --on-conflict keep-both` that copyto writes the conflict copy to the local filesystem, which is exactly the case the flag guards (NUL byte padding on virtual filesystems such as Google Drive File Stream). Every other transfer path adds it via _build_transfer_cmd; bring copyto in line. Signed-off-by: phernandez <paul@basicmachines.co>
Code reviewRan a high-recall multi-agent review of the diff (7 finder angles → recall-biased verify). One confirmed finding, now fixed in 9dbe313:
Everything else surfaced (the |
|
Claude finished @phernandez's task in 2m 4s —— View job PR Review: feat(cli): add Team-safe cloud push/pull and gate sync to Personal workspacesTodo List
What the latest commit (9dbe313) fixed
Checklist Results
Issues Remaining1.
|
…t detection Second code-review finding (PLAUSIBLE): project_diff detects conflicts with `rclone check` (content/hash), but project_copy used `rclone copy` with its default size+modtime comparison. On an overwrite strategy (keep-cloud on pull / keep-local on push), copy could skip a file the diff had flagged as a conflict when sizes matched and the destination was not older — silently ignoring the user's explicit choice. Add `--checksum` to the overwrite copy so the transfer decision uses the same content basis as detection. New-only mode is unaffected: `--ignore-existing` skips by existence, so the comparison basis is irrelevant there. project_sync (the Personal-only mirror) is untouched and keeps its default comparison. Signed-off-by: phernandez <paul@basicmachines.co>
Code review — full resultsThe review completed with 5 findings (correction to my earlier comment, which was posted before the full run finished). Resolutions:
Everything else (the All 78 tests, |
|
Claude finished @phernandez's task in 2m 14s —— View job PR Review: feat(cli): add Team-safe cloud push/pull and gate sync to Personal workspacesTodo List
Fixes confirmed in latest commits
Both fixes are correct and targeted. Checklist Results
Issues Remaining1.
|
Update docs/cloud-cli.md for the docs site:
- Add a Team Workspaces push/pull section (additive, git-style transfers,
the --on-conflict {fail|keep-local|keep-cloud|keep-both} resolution, and the
no-baseline limitations tracked by #862).
- Mark `sync` and `bisync` as Personal-workspace-only mirrors and point Team
users at push/pull, with a Personal-vs-Team overview table.
- Fix stale command namespace: `bm project sync|bisync|check|bisync-reset` ->
`bm cloud ...` (these live under `bm cloud`, not `bm project`).
- Add push/pull to the command reference and the summary workflows.
Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 4m 15s —— View job PR Review: feat(cli): add Team-safe cloud push/pull and gate sync to Personal workspacesTodo List
Changes implemented in commit
|
…ests) From the Codex and claude-review automated reviews on PR #917: - project_diff: fail fast on fatal `rclone check` errors. A non-zero exit with no combined listing (auth/network/missing-remote) previously produced an empty plan, so the transfer ran as a no-op and reported success. Now raises RcloneError with rclone's stderr. (Codex P2 / claude-review #2) - sync-setup "Next steps": stop pointing every user at the now-Personal-only `bm cloud sync`; lead with the Team-safe `pull`/`push` and note bisync is Personal-only. (claude-review #1) - Cosmetic: capitalize the push/pull abort headlines for consistency; document the two ConflictStrategy definitions (Typer enum vs engine Literal). (nits) - Tests: cover keep-local+pull and keep-cloud+push (preserve-destination cases), project_transfer aborting on a mid-loop conflict-copy failure, and project_diff raising on a fatal check error vs not raising on a normal differences exit. Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 2m 36s —— View job PR Review: feat(cli): add Team-safe cloud push/pull and gate sync to Personal workspacesTodo List
What commit c850450 fixedAll four issues carried over from the previous reviews are now resolved:
The fatal-error guard in Checklist Results
Remaining Issue: Windows path separator in
|
Responses to automated review (Codex + claude-review)Thanks both. Triage and resolutions (pushed in Fixed
Surfaced, not auto-fixed
Accepted as-is
All 83 unit tests + |
The Windows SQLite unit job failed because two new copyto tests asserted on
raw POSIX dest strings. On Windows `str(Path("/tmp/research"))` renders with
backslashes, so the copyto dest is `\tmp\research/notes/...` (rclone accepts the
mixed separators — the product is fine). Compare the local dest via `Path(...)`
like the other tests, which normalizes separators cross-platform.
Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 1m 38s —— View job PR Review: feat(cli): add Team-safe cloud push/pull and gate sync to Personal workspacesTodo List
SummaryThis is a well-executed PR that has converged through multiple review rounds. All previously raised functional issues are resolved in Checklist Results
Fixes Confirmed from Prior Review Rounds
The fatal-error guard ( Remaining Item (Accepted by Author)Windows path separator in f"{source_root}/{source_rel_path}",
f"{dest_root}/{dest_rel_path}",On Windows What's Working Well
Pre-existing Issue (Not Introduced by This PR)The Codex P1 finding (workspace-scoped mount info) is a real exposure for projects on a non-default Team workspace, but Verdict: Ready to merge. All functional correctness issues are resolved, test coverage is complete, and the design is clean. |
Summary
Adds
bm cloud pushandbm cloud pullas git-style, fail-safe transfer primitives that are usable on Team workspaces, and restricts the destructivebm cloud syncmirror to Personal workspaces.Closes #858. Design discussion: #858 comment. The longer-term three-way reconciler remains tracked separately in #862.
Why
bm cloud syncis a destructive local→cloud mirror; on a shared Team bucket it can delete a teammate's files. The only pull path was two-waybisync, which is already Personal-only (Block rclone sync/bisync commands for Team workspaces with clear errors #849).What
bm cloud pull --name Xcopycloud → localbm cloud push --name Xcopylocal → cloudbm cloud sync --name Xsynclocal → cloud mirrorbm cloud bisync --name Xbisynctwo-wayrclone copy, never delete on the destination), so neither can damage shared cloud state — they're ungated and Team-safe.git pullrefusing to clobber local changes orgit pushbeing rejected when the remote is ahead.--on-conflict {fail|keep-local|keep-cloud|keep-both}lets the user decide — no vague--force, no silently-picked winner.keep-bothwrites the incoming version beside the existing one asname.conflict-<date>.md.syncnow requires a Personal workspace; its guard message (and the bisync guard) point Team users atpush/pull.Limitations (surfaced in
--help+ command output; tracked by #862)Without a sync baseline, deletions are not propagated and every divergence is treated as a conflict rather than auto-resolved. Real three-way merge (auto-resolving unambiguous cases, propagating deletes) needs the per-client manifest + Tigris snapshot baseline designed in #862. Conflict-aware editing continues to flow through MCP/API.
Implementation
rclone_commands.py— shared_build_transfer_cmd/_transfer_endpoints; refactoredproject_synconto them (behavior unchanged); addedproject_diff(conflict detection viarclone check --combined),project_copy,project_copy_file, andproject_transfer(strategy dispatch).project_sync.py— newpush/pullcommands with--on-conflict/--dry-run/--verbose; gatedsyncto Personal via a per-command guard message.AGENTS.md— documented the new commands in the cloud command reference.Test plan
tests/test_rclone_commands.py— argv-level: conflict-output parsing, source/dest ordering per direction,copy/--ignore-existingper strategy,copytoconflict-copy, keep-both ordering.tests/cli/cloud/test_project_sync_command.py— command-level: abort-on-conflict, abort-on-compare-error, each--on-conflictstrategy, push/pull allowed for Team workspaces,syncblocked for Team / allowed for Personal.ruff check+ruff formatclean;ty checkclean on changed source.bm cloud pull/push --helprender correctly.🤖 Generated with Claude Code