Skip to content

bm cloud pull/push unusable on Team workspaces; should use the WebDAV transport instead of tenant-wide storage credentials #1262

Description

@groksrc

Summary

bm cloud pull and bm cloud push do not work on organization (Team) workspaces. Both commands are documented as the Team-safe sync path, and their transfer logic is deliberately not role-gated — but they cannot run, because their prerequisite fails for anyone who is not the workspace owner.

Personal workspaces are unaffected.

Reproduction

As a member of an organization workspace holding any role other than owner:

$ bm cloud pull --name <project> --workspace <workspace>
Workspace '<workspace>' is not set up for sync.

Run: bm cloud setup --workspace <workspace>

$ bm cloud setup --workspace <workspace>
Step 1: Installing rclone...
rclone is already installed

Step 2: Getting tenant information...
Found tenant: ...

Step 3: Generating sync credentials...

Setup failed: Failed to generate credentials: API request failed: Client error
'403 Forbidden' for url 'https://cloud.basicmemory.com/tenant/mount/credentials'

The CLI directs the user to a command that can never succeed for them, and offers no alternative.

Why it happens

_run_directional_transfer requires a tenant-scoped rclone remote and stops when one does not exist:

  • src/basic_memory/cli/commands/cloud/project_sync.py:496-502 — the missing-remote check and the bm cloud setup hint.
  • src/basic_memory/cli/commands/cloud/project_sync.py:467 — the transfer itself is intentionally ungated: "Uses additive rclone copy, so it never deletes on the destination — safe for Team workspaces and therefore not gated."
  • src/basic_memory/cli/commands/cloud/project_sync.py:872 — the CLI states the Team-safe commands "work on any workspace."

Provisioning that remote requires object-storage credentials from the cloud service, and that endpoint is restricted to workspace owners. Personal workspaces bypass the restriction because their members are always treated as owners, which is why this went unnoticed.

Proposed direction

Rather than widening who can obtain object-storage credentials, Team pull/push should not use them at all.

Object-storage credentials are scoped to an entire tenant bucket, so they bypass the service's own project-level access control — a member could reach projects they were never granted. No role restriction makes that safe, so loosening it is the wrong fix.

The cloud service already exposes a WebDAV surface that enforces the correct model through the application layer, where per-project access applies:

  • GET / PROPFIND require viewer access — so a viewer can pull.
  • PUT require editor access — so an editor can push.

Routing Team pull/push over that surface means:

  • bm cloud setup drops out of the Team path entirely — no remote to provision, no credentials to mint, and no 403.
  • rclone stays an internal implementation detail of the Personal path and is never exposed through the CLI.
  • There is precedent in this repo already: bm cloud upload performs PUT /webdav/{project}/{path} over httpx with an X-OC-Mtime header (src/basic_memory/cli/commands/cloud/upload.py). It currently has no download counterpart; this work adds the other half.

Scope

CLI semantics do not change. bm cloud pull / bm cloud push keep their existing surface and behavior on Team workspaces, identical to Personal:

  • additive — never deletes on the destination
  • --on-conflict [fail|keep-local|keep-cloud|keep-both], defaulting to fail
  • no flags added, removed, or altered

This is not bidirectional sync. bisync remains gated to Personal workspaces and is explicitly out of scope.

Honoring --on-conflict requires detecting that a file differs on both sides, which needs a comparison primitive the current PROPFIND response does not provide — it returns size only. Size alone cannot distinguish an identical file from two edits that happen to be the same length, which is exactly the case the flag exists to catch.

Prerequisite: a companion change in the cloud service must expose an entity tag and last-modified time on PROPFIND (and ideally on GET, which currently returns no validators either). That work is tracked separately on the cloud side and should land first. It has been verified that the underlying object store returns single-part entity tags that are true content MD5s, including for objects well above the usual multipart threshold, so hash comparison is a viable basis for conflict detection.

Work in this repo

  1. Add the GET / PROPFIND half to the existing WebDAV client in src/basic_memory/cli/commands/cloud/upload.py.
  2. Implement the directional-transfer comparison against entity tag plus size, so all four --on-conflict strategies behave exactly as they do on the Personal path.
  3. Route Team workspaces through that transport; leave the Personal rclone path unchanged.

Treat a missing or multipart-shaped entity tag as "cannot compare by hash" and fall back to last-modified plus size. Silently treating such files as identical would lose an edit; treating them as conflicting would block every transfer.

Notes for implementers

Per CLAUDE.md / AGENTS.md in this repo:

  • PR titles must be semantic and are enforced by .github/workflows/pr-title.yml: type(scope): summary. Use an allowed type (feat, fix, chore, docs, style, refactor, perf, test, build, ci) and an allowed scope — cli or sync fit this work.
  • Sign off commits with git commit -s so the DCO check passes.
  • Run just typecheck alongside targeted ruff and pytest commands when tests change.
  • Add regression tests covering each --on-conflict strategy on the new transport.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcloudBasic Memory Cloud

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions