Skip to content

feat(team-mode): remote event-log adapter — Drive + LocalFolder backends (#277)#289

Merged
jinhongkuan merged 3 commits into
devfrom
277-remote-event-log-adapter
May 9, 2026
Merged

feat(team-mode): remote event-log adapter — Drive + LocalFolder backends (#277)#289
jinhongkuan merged 3 commits into
devfrom
277-remote-event-log-adapter

Conversation

@jinhongkuan

Copy link
Copy Markdown
Contributor

Closes #277.

Summary

  • Implements v0 Productization §2: shifts team mode entirely off git as the inter-machine replication substrate, onto a pluggable BackendAdapter with two ship-day implementations: LocalFolderAdapter (NFS / Dropbox / syncthing) and GoogleDriveAdapter (per-team Drive folder + bundled OAuth client). Pull-only sync — no daemons, no webhooks, no Bicameral server in the loop.
  • Setup wizard splits team-mode into Create / Join / LocalFolder branches. Create provisions the Drive folder and prints the literal share-text-to-teammates message. Join verifies access (404 / read-only both block) and confirms the resolved signer (default-No) before persisting. Identity confirmation surfaces the signer_email_fallback policy at the moment it matters.
  • Drive integration uses Bicameral's bundled OAuth client (the same pattern gh / gcloud / cursor use; RFC 8252 native-app pattern). Scope: drive.file only. Token cache at ~/.bicameral/google-drive-token.json mode 0600.
  • Colored security disclosure prints in the terminal before the browser opens, distinguishing CLI-on-machine from Bicameral-the-company. Mirrored on bicameral-ai.com/privacy in BicameralAI/bicameral#111.

Architecture

File Purpose
events/backends/__init__.py BackendAdapter ABC + get_backend(config) factory
events/backends/local_folder.py sha256-idempotent LocalFolderAdapter
events/backends/google_drive.py Drive Files API adapter; bundled client_id + client_secret; FolderNotFoundError / ReadOnlyAccessError for Join verify; create_folder for Create
events/team_adapter.py TeamWriteAdapter accepts backend=, marks _dirty on writes, exposes flush_to_backend()
adapters/ledger.py Refactored _read_collaboration_mode → _read_team_config(repo_path) -> dict; constructs backend, injects
handlers/sync_middleware.py ensure_team_synced (30 s TTL pull) + flush_team_writes (post-handler push); errors swallowed at DEBUG
server.py Wires both into dispatch (pull at top, flush in finally)
setup_wizard.py Create/Join/LocalFolder dispatch + colored security disclosure + identity-confirm prompt at Join

Why bundled OAuth client (not operator-supplied)

Originally drafted with operator-supplied OAuth client (env var or ~/.bicameral/google-drive-client.json) per overcautious audit reading of OWASP A05. UX cost was a 5-step GCP console dance per user, killing onboarding. Pivoted to bundled-client per RFC 8252 native-app pattern (industry standard: gh, gcloud, cursor, every dev-tool OAuth client). Justification + threat-model in docs/google-oauth-verification-submission.md.

The client_secret in source is not a confidentiality boundary — it's a shared identifier. The actual security model is the consent screen + Google's verified-app badge + the open-source CLI auditability. (GitHub secret-scanning push protection flagged the secret on push; deliberately unblocked.)

Security model (mirrored at bicameral-ai.com/privacy)

  • Decision data flows your-CLI ↔ Google directly. Bicameral the company does NOT receive copies. No Bicameral server in the loop.
  • drive.file scope limits the CLI on the user's machine to files it creates in the team folder. Rest of user's Drive is invisible to the CLI; Google enforces server-side.
  • What we DO see (as OAuth app publisher): aggregate API request counts + per-user OAuth consent records. Not contents.
  • Trust dependency: same as any OAuth tool you install. Mitigated by source visibility.

Test plan

  • pytest tests/test_backends_local_folder.py tests/test_backends_google_drive_unit.py tests/test_team_adapter_with_backend.py tests/test_team_round_trip_local_folder.py tests/test_sync_middleware_team.py tests/test_setup_wizard_team_backend.py — 53 pass, 1 platform-skip
  • pytest tests/test_team_event_replay.py tests/test_event_writer.py — adjacent regression, 15 pass + 1 platform-skip
  • ruff check events/ adapters/ledger.py handlers/sync_middleware.py setup_wizard.py tests/test_*team*.py tests/test_backends_*.py tests/test_sync_middleware_team.py — clean
  • Dogfood end-to-end against the real GCP project (test user: jin@bicameral-ai.com): run bicameral-mcp setup → team → Create, complete browser OAuth, verify folder is created in Drive
  • Submit OAuth consent screen for verification per docs/google-oauth-verification-submission.md so non-test-user flows don't see the unverified-app warning
  • (After verification clears) repeat dogfood with a non-test-user account

Out of scope (separate issues)

  • S3 / Dropbox / Box adapters (interface designed for them; bodies are P1 follow-ups)
  • Real-time push, webhooks, polling daemons (explicitly rejected by v0 §2)
  • Multi-folder-per-repo orchestration
  • Encryption at rest beyond what the backend provides

Companion PRs

  • BicameralAI/bicameral#111 — privacy page rewrite mirroring the corrected CLI disclosure

🤖 Generated with Claude Code

…nds (#277)

Closes #277. Implements v0 Productization §2: shifts team mode entirely
off git as the inter-machine replication substrate, onto a pluggable
backend with two ship-day implementations (LocalFolder, GoogleDrive).
Pull-only sync; no daemons, no webhooks, no Bicameral server in the loop.

What changes for users
- Setup wizard team-mode branch now offers Create vs Join vs LocalFolder.
  Create: provisions a Drive folder under the operator's Google account,
  prints the literal share-text-to-teammates message. Join: paste folder
  ID/URL, OAuth, verify access (404 / read-only both block), confirm the
  resolved signer (default-No) before persisting. LocalFolder: single
  prompt for the path.
- Drive integration uses Bicameral's bundled OAuth client (the same
  pattern gh / gcloud / cursor use). Scope: drive.file only — Bicameral's
  CLI can only see files it creates inside the team folder. Token cache
  at ~/.bicameral/google-drive-token.json mode 0600.
- Colored security disclosure renders before the browser opens, walking
  the operator through what flows where, what we do and don't see, and
  the trust dependency. Mirrored on bicameral-ai.com/privacy
  (BicameralAI/bicameral PR #111).

Architecture
- events/backends/__init__.py — BackendAdapter ABC + get_backend factory.
- events/backends/local_folder.py — sha256-idempotent LocalFolderAdapter.
- events/backends/google_drive.py — Drive Files API adapter; bundled
  client_id + client_secret (RFC 8252 native-app pattern, no env override
  per Option A); FolderNotFoundError / ReadOnlyAccessError surface for
  Join verify_access; create_folder helper for Create branch.
- events/team_adapter.py — TeamWriteAdapter accepts backend=, marks
  _dirty on every write, exposes flush_to_backend().
- adapters/ledger.py — _read_collaboration_mode refactored to
  _read_team_config(repo_path) -> dict; constructs backend and injects
  into TeamWriteAdapter.
- handlers/sync_middleware.py — ensure_team_synced (30 s TTL pull) +
  flush_team_writes (post-handler push); errors swallowed at DEBUG.
- server.py — wires both into the dispatch site (pull at top, flush in
  finally).
- setup_wizard.py — Create/Join/LocalFolder dispatch + colored security
  disclosure + identity-confirmation prompt at Join time.

Testing
- 53 new tests, 1 platform-skip (Windows-only path):
  - LocalFolderAdapter: 6 tests (push idempotency, pull peer-files-only,
    list_peers, lock serialization)
  - TeamWriteAdapter ↔ backend: 3 tests (connect-pulls-then-replays,
    write-marks-dirty-then-flush-pushes, no-backend-noop)
  - Two-author round-trip: 2 tests
  - Sync middleware: 5 tests (TTL cache, no-backend-noop, error swallowing)
  - GoogleDriveAdapter: 11 tests (push idempotency on md5, pull
    own-file-skip + max-modifiedTime token, lock create-then-delete +
    cleanup on exception, verify_access 404 / read-only / can-edit,
    create_folder, placeholder-detection auto-skip when bundled client
    is published)
  - Setup wizard Create/Join: 11 tests including identity decline,
    OAuth-disclosure decline, folder-id URL extraction, unwritable-path
    rejection
- All adjacent regression tests still pass (test_team_event_replay,
  test_event_writer).
- Lint clean across events/ adapters/ handlers/sync_middleware.py
  setup_wizard.py + new test files.

Security model (also documented at docs/team-mode-setup.md and on
bicameral-ai.com/privacy)
- Decision data flows your-CLI ↔ Google directly. Bicameral the company
  does NOT receive copies. No Bicameral server in the loop.
- drive.file scope limits the CLI on the user's machine to files it
  creates in the team folder. The rest of the user's Drive is invisible
  to the CLI; Google enforces this server-side.
- As OAuth app publisher, Bicameral receives aggregate API request
  counts and per-user OAuth consent records (which Google accounts
  authenticated, when). Not contents.
- Trust dependency: same as any OAuth tool (gh, gcloud, Notion, Slack
  desktop) — open-source CLI behaves as advertised, mitigated by source
  visibility.

OAuth verification submission text + GCP setup checklist:
docs/google-oauth-verification-submission.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 792becb0-34ff-42f0-af1c-6d05281a356e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 277-remote-event-log-adapter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Pure formatting — `ruff format` against the 10 files touched in #277.
No semantic changes. CI's `ruff format --check .` now passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…endAdapter

Mypy was failing on `events/backends/__init__.py:62,66` — the factory's
return type is `BackendAdapter | None`, but the two concrete adapters
were structurally compatible without declaring inheritance. Added
explicit `BackendAdapter` base.

Both classes already implemented all four abstract methods (push_events,
pull_events, lock, list_peers) — runtime check (issubclass + concrete
instantiation) passes. No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jinhongkuan jinhongkuan had a problem deploying to recording-approval May 9, 2026 06:00 — with GitHub Actions Failure
@jinhongkuan jinhongkuan merged commit e285c45 into dev May 9, 2026
7 of 9 checks passed
jinhongkuan pushed a commit that referenced this pull request May 9, 2026
Triages 25 dev commits onto main (already on dev as of merge time):
  • #289 — team-mode remote event-log adapter (#277)
  • #285, #284, #283 — M2 grounding telemetry, eval harness, precision fix (#280)
  • #275 — README/SECURITY surface
  • plus assorted fixes flowing through dev

Resolved conflicts in CHANGELOG.md (kept dev's [Unreleased] block,
inserted v0.14.2's release entry from main below it, then renamed
[Unreleased] → v0.14.3) and README.md (kept dev's Solo-vs-Team mode
section + extended setup-writes table from #289 — main was missing
both because PR #289 hadn't backflowed yet).

pyproject.toml: 0.14.2 → 0.14.3
RECOMMENDED_VERSION: 0.14.1 → 0.14.3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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