feat(cli): per-workspace rclone remotes for Team push/pull#920
Conversation
Route cloud push/pull through the resolved workspace's own tenant-scoped rclone remote so projects in non-default / Team workspaces read and write the correct Tigris bucket. Closes the Codex P1 review note on #917; design in #919. Why - Tigris IAM keys are bucket/tenant-scoped (one tenant = one bucket). The CLI used a single `basic-memory-cloud` remote configured with the default tenant's credentials, and `get_mount_info()` was called without workspace context — so push/pull against a Team-workspace project targeted the wrong bucket. - The cloud mount endpoints already honor `X-Workspace-ID` (resolve workspace, validate membership + subscription, return that tenant's bucket-scoped creds). What - bisync_commands: `get_mount_info(workspace_id=...)` and `generate_mount_credentials(tenant_id)` send `X-Workspace-ID`. - rclone_config: `remote_name_for_workspace` (default tenant keeps `basic-memory-cloud`; others get `basic-memory-cloud-<slug>`), `rclone_remote_exists`, and `configure_rclone_remote(remote_name=...)`. - rclone_commands: `SyncProject.remote_name`; `get_project_remote` uses it. - core_commands: `bm cloud setup --workspace <slug|name|tenant_id>` provisions a per-workspace remote (explicit, once — avoids re-minting legacy-tenant keys). - project_sync: push/pull resolve the project's workspace (config or a new `--workspace` override, with disambiguation by tenant_id/slug), select that workspace's remote, error with the exact `bm cloud setup --workspace` hint if it isn't configured, and scope mount info to the workspace's tenant. Keys stay tenant-scoped (option A). Scope: sync/bisync/check still use the default remote (Personal-only); extending them to per-workspace remotes is a follow-up. Backend note in #919: add the missing FK on tenant_mount_credentials.tenant_id. Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 2m 16s —— View job PR Review: feat(cli): per-workspace rclone remotes for Team push/pullTasks
SummaryThis PR correctly routes Code Quality & Standards✅ Passed
|
| Category | Status |
|---|---|
| Coding conventions / CLAUDE.md | ✅ |
| Python 3.12+ types & async | ✅ |
| FastAPI / Typer conventions | ✅ |
| 100-char line limit | ✅ |
| Unit tests for new functions | ✅ (minor gap: pull --workspace CLI test) |
| 100% coverage | ✅ (appears maintained) |
| Local-first / no DB changes | ✅ (CLI-only change) |
| No hardcoded secrets | ✅ |
| Input validation | |
| Windows compatibility | ✅ |
| No emoji in output | ✅ |
Overall: Approve with suggestions. The slug-sanitization point is the most actionable item before merge; the rest are minor cleanup notes.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b8e4008ec
ℹ️ 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".
| raise typer.Exit(1) | ||
|
|
||
| # Get tenant info for bucket name, scoped to the resolved workspace | ||
| tenant_info = run_with_cleanup(get_mount_info(workspace_id=target_workspace.tenant_id)) |
There was a problem hiding this comment.
Route the project lookup through the selected workspace
When bm cloud pull/push --workspace acme selects a workspace different from the project config or account default, this scopes the bucket/remote to target_workspace, but the subsequent _get_cloud_project(name) still calls get_client(project_name=name) without the selected workspace. Under force_routing(cloud=True), get_client resolves X-Workspace-ID from config/default, so it can fetch the project path from another tenant or report it missing, then transfer against the acme bucket with the wrong/missing project metadata. Pass target_workspace.tenant_id into the cloud project lookup (and ultimately get_client(..., workspace=...)) so all cloud reads use the same workspace selected here.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in c99c3f7 — _get_cloud_project now takes workspace_id and passes it to get_client(..., workspace=...), so the project lookup uses the same tenant the bucket/remote target. Threaded target_workspace.tenant_id through from the resolver.
From the Codex and claude-review automated reviews on PR #920: - Route the cloud project lookup through the resolved workspace (Codex P2): _get_cloud_project now takes workspace_id and passes it to get_client, so the project metadata is read from the same tenant the bucket/remote target. Without this, --workspace (or a non-default config) could read a different tenant's project and transfer against the wrong bucket. - Validate workspace slugs before splicing into a remote name (claude-review, security): remote_name_for_workspace rejects slugs with characters invalid in an rclone remote section, failing fast instead of writing a broken rclone.conf. - Fix the now-stale rclone_commands module docstring (single-remote bullet). - Clarify that generate_mount_credentials' tenant_id is the X-Workspace-ID routing key. - Note (TODO #919) why sync/bisync/check still use default-tenant mount info. - Tests: pull --workspace routing, slug validation rejects unsafe slugs. Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 1m 44s —— View job PR Review: feat(cli): per-workspace rclone remotes for Team push/pullTasks
SummaryThis PR is well-implemented and the latest commit (c99c3f7) addresses the key P2 issue from the Codex review. The workspace isolation for push/pull is correct and complete. A few minor items noted below. Code Quality & Standards✅ Passed
|
| Category | Status |
|---|---|
| Coding conventions / CLAUDE.md | ✅ |
| Python 3.12+ types & async | ✅ |
| Typer conventions | ✅ |
| 100-char line limit | ✅ |
| Literate programming comments | ✅ |
| Unit tests for new functions | ✅ |
| 100% coverage maintained | ✅ |
| Local-first / no DB changes | ✅ (CLI-only) |
| No hardcoded secrets | ✅ |
| Input validation (slug sanitization) | ✅ |
| Codex P2 fix (workspace-scoped project lookup) | ✅ |
| Windows path compatibility | ✅ |
| No emoji in output | ✅ |
Stale module docstring in rclone_config.py |
Overall: Approve with minor suggestion. The stale docstring on rclone_config.py:4 is the only outstanding item — everything else is clean and the Codex P2 fix is correctly threaded through.
Responses to automated review (Codex + claude-review)Thanks both — pushed fixes in Fixed
Acknowledged / not changed
All 114 unit tests + |
Follow-up to the #920 review: rclone_config now owns per-workspace remote naming (remote_name_for_workspace / _SAFE_SLUG), so drop the "single basic-memory-cloud remote for all operations" line. Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 2m 6s —— View job PR Review: feat(cli): per-workspace rclone remotes for Team push/pullTasks
SummaryThis PR correctly wires Code Quality & Standards✅ Passed
|
| Category | Status |
|---|---|
| Coding conventions / CLAUDE.md | ✅ |
| Python 3.12+ types & async | ✅ |
| Typer conventions | ✅ |
| 100-char line limit | ✅ |
| Literate programming comments | ✅ (minor: misleading TODO in check) |
| Unit tests for new functions | ✅ |
| 100% coverage maintained | ✅ |
| Local-first / no DB changes | ✅ (CLI-only) |
| No hardcoded secrets | ✅ |
| Input validation (slug sanitization) | ✅ |
| Codex P2 (workspace-scoped project lookup) | ✅ |
check_project_command workspace gate |
|
| Windows path compatibility | ✅ |
| No emoji/unicode in output | ✅ |
Overall: Approved. The PR is correct and all prior P1/P2 issues are resolved. The one item above is a comment accuracy nit on check_project_command — not a behavioral regression since check is read-only.
Summary
Route
bm cloud push/pullthrough the resolved workspace's own tenant-scoped rclone remote, so projects in non-default / Team workspaces read and write the correct Tigris bucket. Closes the Codex P1 review note on #917. Design + research: #919.Keys stay tenant-scoped (option A); each accessible tenant gets its own remote.
Why
Tigris IAM keys are bucket/tenant-scoped (one tenant = one bucket — confirmed against the cloud DB model and the Tigris IAM docs / Fly Tigris docs). The CLI used a single
basic-memory-cloudremote configured with the default tenant's credentials, andget_mount_info()was called with no workspace context — so push/pull against a Team-workspace project would target the wrong bucket. The cloud mount endpoints already honorX-Workspace-ID(resolve workspace → validate membership + subscription → return that tenant's bucket-scoped creds), so the fix is client-side.What
bisync_commands—get_mount_info(workspace_id=...)andgenerate_mount_credentials(tenant_id)sendX-Workspace-ID(omitted → default tenant, unchanged).rclone_config—remote_name_for_workspace(default tenant keepsbasic-memory-cloud; others getbasic-memory-cloud-<slug>),rclone_remote_exists, andconfigure_rclone_remote(remote_name=...).rclone_commands—SyncProject.remote_name;get_project_remoteroutes through it.core_commands—bm cloud setup --workspace <slug|name|tenant_id>provisions a per-workspace remote. Provisioning is explicit and once — avoids re-minting (and leaking) keys for legacy tenants whose/tenant/mount/credentialsmints a fresh key each call.project_sync— push/pull resolve the project's workspace (config, or a new--workspaceoverride with disambiguation bytenant_id/slug), select that workspace's remote, error with the exactbm cloud setup --workspace …hint if it isn't configured, and scope mount info to the workspace's tenant.Scope / follow-ups
sync/bisync/checkstill use the default remote (they're Personal-only); extending them to per-workspace remotes is a follow-up.--workspaceoverride (not a cross-workspace name search);--workspacecovers same-name disambiguation.tenant_mount_credentials.tenant_idlacks a FK totenant(id)— add it in basic-memory-cloud.Test plan
rclone_config:remote_name_for_workspace, named-remote write,rclone_remote_exists.rclone_commands:get_project_remotehonorsSyncProject.remote_name; default remote unchanged.project_sync: push/pull route a Team workspace throughbasic-memory-cloud-<slug>; error + setup hint when the remote is missing;--workspaceoverride resolution (match / no-match).core_commands:bm cloud setup --workspace acmeconfiguresbasic-memory-cloud-acme.ty+ruffclean.🤖 Generated with Claude Code