Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to bicameral-mcp are tracked here. Format loosely follows

## [Unreleased]

### Added

- **Team-mode remote event-log adapter (#277, v0 §2 productization).** Team mode now replicates decisions across teammates' machines via a pluggable `BackendAdapter` (`events/backends/__init__.py`), with two ship-day backends: `LocalFolderAdapter` (shared filesystem — NFS/Dropbox/syncthing) and `GoogleDriveAdapter` (per-team folder with a bundled OAuth client). Pull-only sync — no daemons, no webhooks, no Bicameral server in the loop. `TeamWriteAdapter` gains `flush_to_backend()`; `handlers/sync_middleware.py` adds `ensure_team_synced` (30 s TTL pull) + `flush_team_writes` (post-handler push), wired into `server.py` dispatch (pull at top, flush in `finally`). Setup wizard splits team-mode into Create-vs-Join branches: Create makes the Drive folder + prints the literal share-text-to-teammates message; Join verifies access (404/read-only both block) and confirms the operator's resolved signer (default-No) before persisting. Drive scope narrowed to `drive.file` — Bicameral's CLI can only see files it created in the team folder. Colored security disclosure renders before browser open; mirrors the bicameral-ai.com/privacy page. Operator walkthrough at `docs/team-mode-setup.md`; OAuth verification submission text at `docs/google-oauth-verification-submission.md`. 53 new tests (Phase 1 LocalFolder + sync_middleware + TeamWriteAdapter wiring + two-author round-trip; Phase 2 GoogleDrive unit tests with mocked Drive client; Phase 3 wizard Create/Join/LocalFolder branches).

### Fixed

- **`handlers/bind.py`: caller-supplied line range cannot bypass symbol verification (#280, M2 grounding precision regression).** Pre-fix, when a caller supplied `start_line`/`end_line` alongside `symbol_name`, the handler verified only that the file existed at the SHA and accepted any `symbol_name` — silent corruption surface for caller-LLM grounding when the agent hallucinated a wrong symbol on a real file. Branch B now also calls `resolve_symbol_lines` (same tree-sitter path Branch A uses) and rejects two cases: (1) `symbol_name` doesn't resolve at all → `error="symbol '...' not found in <file> at <sha> — caller-supplied line range cannot bypass symbol verification (#280)"`; (2) symbol resolves but the caller-supplied span doesn't overlap the resolved span → `error="span mismatch (#280)"`. Overlap (not exact equality) is the matching rule, so legitimate sub-region binds stay accepted; only hallucinated ranges are rejected.
Expand Down
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,45 @@ bicameral-mcp --smoke-test

---

## Solo or Team mode

Bicameral runs in two modes; the setup wizard asks you which one at install time.

| | Solo | Team |
|---|---|---|
| **Best for** | Individual builders; small projects with one decision-maker | Multiple people writing decisions for the same codebase (PMs, designers, multiple engineers) |
| **Where decisions live** | Local SurrealDB at `.bicameral/ledger.db`; not shared | One append-only `<your-email>.jsonl` per teammate; replicated via a remote substrate you provision |
| **Who can ingest** | You | Anyone on the shared substrate (PM ingests a PRD, dev pulls and surfaces it on `preflight`) |
| **What gets shared** | Nothing leaves your machine | Decision payloads, canonical IDs, signoffs. **No source code** |
| **How replication works** | N/A | Pull-only on tool invocation (~30 s freshness). No daemons, no webhooks, no central server |
| **Failure if remote is down** | N/A | Falls back to local; resync next call. No blocking |

### Team-mode remote substrate

Team mode replicates events through a substrate **you provision and own** —
nothing routes through a Bicameral-operated server, and your decision data
never crosses our infrastructure. The wizard supports two backends:

| Backend | Substrate | Setup |
|---|---|---|
| **Google Drive** (default) | A folder in your team's Google account. Each teammate writes to their own `<email>.jsonl`; everyone reads the rest. | 3-minute one-time OAuth client setup, then Create-or-Join in the wizard. |
| **Local folder** (advanced) | A directory mounted on every teammate's machine (NFS, Dropbox, syncthing). | One prompt for the path. |

S3, Dropbox-native, and Box backends are on the roadmap but not yet shipped — we
deliberately ship Drive first, validate the model with paying teams, then extend.

The Drive integration is scoped to `drive.file` — the Bicameral CLI on your
machine can only touch files it creates inside the team folder; the rest of your
Drive (other folders, Google Docs, shared files) is invisible to the CLI.
Decision data flows your-CLI ↔ Google directly; **Bicameral the company does
not receive copies of your files**. We do see aggregate OAuth telemetry (API
request counts, OAuth consent records — not contents) as the OAuth app
publisher, the same way any OAuth-using tool's vendor does. Token cache lives
at `~/.bicameral/google-drive-token.json`, mode 0600. Full security posture
and operator walkthrough: [`docs/team-mode-setup.md`](docs/team-mode-setup.md).

---

## Slash Commands

After setup, Claude Code gets these slash commands:
Expand All @@ -95,8 +134,10 @@ The agent also fires these automatically — `preflight` before any code change,
| File | What it is |
|---|---|
| `.mcp.json` | MCP server config for Claude Code |
| `.bicameral/config.yaml` | Mode (`solo`/`team`) and guided-mode flag |
| `.bicameral/config.yaml` | Mode (`solo`/`team`), guided-mode flag, and (in team mode) `team.backend` + `team.folder_id`/`team.remote_root` + `team.role` |
| `.bicameral/ledger.db` | Local SurrealDB decision ledger (solo mode) |
| `.bicameral/events/<email>.jsonl` | Append-only event log per teammate (team mode) |
| `~/.bicameral/google-drive-token.json` | Drive OAuth token cache, mode 0600 (team mode + Drive backend only) |
| `.gitignore` entry | Ignores `.bicameral/` in solo mode |
| `.claude/settings.json` | PostToolUse hook (auto-sync after commits) + SessionEnd hook (capture mid-session decisions) |
| `.claude/skills/bicameral-*/SKILL.md` | Slash commands |
Expand Down
40 changes: 29 additions & 11 deletions adapters/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,32 @@
_real_ledger_instance = None


def _read_collaboration_mode(repo_path: str) -> str:
"""Read mode from .bicameral/config.yaml (returns 'solo' or 'team').
def _read_team_config(repo_path: str) -> dict:
"""Read .bicameral/config.yaml as a parsed dict.

Returns ``{"mode": "solo"}`` when the file is absent or unparseable.
Checks BICAMERAL_DATA_PATH first so history stored in a private parent
repo is discovered even when REPO_PATH points to a public submodule.
"""
data_path = os.getenv("BICAMERAL_DATA_PATH", repo_path)
config_path = Path(data_path) / ".bicameral" / "config.yaml"
if not config_path.exists():
return "solo"
return {"mode": "solo"}
try:
import yaml

config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
return config.get("mode", "solo")
cfg = yaml.safe_load(config_path.read_text(encoding="utf-8"))
return cfg if isinstance(cfg, dict) else {"mode": "solo"}
except Exception:
# yaml not installed or bad file — fall back to basic parsing
# yaml not installed or bad file — fall back to mode-only parse
try:
for line in config_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith("mode:"):
return line.split(":", 1)[1].strip().strip("\"'")
return {"mode": line.split(":", 1)[1].strip().strip("\"'")}
except OSError:
pass
return "solo"
return {"mode": "solo"}


def get_ledger():
Expand All @@ -64,9 +65,11 @@ def get_ledger():
)

repo_path = os.getenv("REPO_PATH", ".")
mode = _read_collaboration_mode(repo_path)
cfg = _read_team_config(repo_path)
mode = cfg.get("mode", "solo")

if mode == "team":
from events.backends import get_backend
from events.materializer import EventMaterializer
from events.team_adapter import TeamWriteAdapter
from events.writer import EventFileWriter, _get_git_email
Expand All @@ -83,8 +86,23 @@ def get_ledger():
writer = EventFileWriter(events_dir, author)
materializer = EventMaterializer(events_dir, local_dir)

_real_ledger_instance = TeamWriteAdapter(inner, writer, materializer)
logger.info("[ledger] team mode — events at %s (author: %s)", events_dir, author)
cfg.setdefault("team", {})["author"] = author
try:
backend = get_backend(cfg)
except Exception as exc:
logger.warning(
"[ledger] team backend init failed (%s) — continuing local-only", exc
)
backend = None

_real_ledger_instance = TeamWriteAdapter(inner, writer, materializer, backend=backend)
backend_kind = (cfg.get("team") or {}).get("backend") or "local-only"
logger.info(
"[ledger] team mode — events at %s (author: %s, backend: %s)",
events_dir,
author,
backend_kind,
)
else:
_real_ledger_instance = inner

Expand Down
167 changes: 167 additions & 0 deletions docs/google-oauth-verification-submission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Google OAuth verification submission — Bicameral MCP

This is the operator-facing checklist + ready-to-paste text for getting
Bicameral's Drive OAuth client verified by Google. Run through this once
per project lifetime. Until verification clears, users see a "Google
hasn't verified this app" interstitial — they can click through (advanced
→ "go to Bicameral (unsafe)") but it scares non-technical users.

## Prerequisites (already done)

- [x] GCP project `bicameral-mcp` created
- [x] Linked to billing account `01647A-138E06-EE9A8B`
- [x] Drive API enabled

## Web-console steps

### 1. OAuth consent screen

URL: https://console.cloud.google.com/apis/credentials/consent?project=bicameral-mcp

| Field | Value |
|---|---|
| User Type | **External** |
| App name | `Bicameral` |
| User support email | `support@bicameral-ai.com` (or `jin@bicameral-ai.com` while support@ isn't set up) |
| App logo | Upload `assets/bicameral-icon-120.png` (must be 120×120 PNG, under 1 MB). If we don't have one yet, leave blank — Google won't block submission, but verified apps look more legitimate with a logo |
| Application home page | `https://bicameral-ai.com` |
| Application privacy policy | `https://bicameral-ai.com/privacy` |
| Application terms of service | `https://bicameral-ai.com/terms` |
| Authorized domains | `bicameral-ai.com` |
| Developer contact email | `jin@bicameral-ai.com` |

### 2. Scopes

Add **only** this one scope:

- `https://www.googleapis.com/auth/drive.file` — "View and manage Google Drive files and folders that you have opened or created with this app."

> Critical: do NOT add `drive`, `drive.readonly`, `drive.metadata`, or any
> other Drive scope. `drive.file` is non-sensitive — it skips the "restricted
> scope" review path and is much faster to verify (days vs weeks).

### 3. Test users (during dev / before verification)

Add yourself + any teammates running the unverified flow:

- `jin@bicameral-ai.com`
- (anyone else from the bicameral-ai.com domain)

Once verified, this list is unused — anyone with a Google account can
authenticate.

### 4. OAuth client (Desktop app)

URL: https://console.cloud.google.com/apis/credentials?project=bicameral-mcp

- Click **Create Credentials → OAuth client ID**.
- Application type: **Desktop app**.
- Name: `Bicameral CLI` (Google-internal label, not user-visible).
- Click Create. Download the JSON.

Open the JSON. Copy `client_id` and `client_secret` into
`events/backends/google_drive.py` — replace these constants:

```python
_BUNDLED_CLIENT_ID = "REPLACE_WITH_BICAMERAL_DRIVE_OAUTH_CLIENT_ID.apps.googleusercontent.com"
_BUNDLED_CLIENT_SECRET = "REPLACE_WITH_BICAMERAL_DRIVE_OAUTH_CLIENT_SECRET"
```

Commit. Push. Cut a release.

### 5. Verification submission

Required because we're publishing externally and want the consent screen
without the unverified-app warning. URL:
https://console.cloud.google.com/apis/credentials/consent?project=bicameral-mcp
→ click **Publish App** → submission form opens.

#### Verification justification — paste this

**App functionality**

> Bicameral is an open-source MCP (Model Context Protocol) server for AI
> coding assistants. It maintains a local decision ledger that maps
> meeting decisions to code regions. In team mode, the CLI uses Google
> Drive as a pull-only replication substrate so teammates' decision logs
> sync between machines without operating a central server.

**Why each scope is needed**

> `drive.file` — Bicameral creates one append-only JSONL event log per
> teammate (`<email>.jsonl`) inside a single shared folder the team
> creates. Each user's CLI reads peer files (created by other Bicameral
> instances within the same folder) and writes their own. Bicameral never
> needs access to the user's other Drive files, so the narrow `drive.file`
> scope is sufficient.

**Demo video script (2-3 minutes; record once)**

1. Open Bicameral landing page (`https://bicameral-ai.com`).
2. Show terminal: `bicameral-mcp setup`.
3. When wizard asks "How do you want to set up the shared ledger?", select
"Create a new shared ledger".
4. Cut to the colored security disclosure the wizard prints — pause on
screen for 3 seconds so reviewers see it.
5. Browser opens, OAuth consent screen appears. Click Allow.
6. Cut back to terminal — wizard prints folder ID, instructions to share
with teammates.
7. Open Drive in another tab — show the new `bicameral-<repo>-ledger`
folder. Show the empty folder.
8. In the terminal, ingest a small decision (e.g. `bicameral-mcp ingest
--text "We decided to ship pull-only sync"`). Show that it succeeded.
9. Cut to Drive — refresh the folder. Show the new `<email>.jsonl` file
(one event line). Open it briefly to show the JSON event structure.
10. Cut to the privacy policy page in the browser — pause on the section
that explains what Bicameral can and cannot see.

Upload the unlisted YouTube video link to the verification form.

**Privacy policy must include**

> Bicameral collects no personal data through the Google Drive
> integration. The Drive OAuth flow grants Bicameral the `drive.file`
> scope, which permits the application to read and write only files it
> creates within the shared folder you provision. Bicameral does not
> upload, transmit, or store user data on any Bicameral-operated server;
> all decision data is replicated peer-to-peer through your team's own
> Google Drive folder. As the OAuth application owner, Bicameral receives
> aggregate API usage analytics from Google (e.g. request counts) and
> per-user OAuth consent records (which Google accounts authenticated
> against the Bicameral app). We do not link this telemetry to any
> identifying information beyond the Google account that authenticated
> and we do not share it with third parties.

(Adapt to fit the rest of the bicameral-ai.com privacy policy structure.)

#### Submit

Click **Submit for verification**. Google replies in 1-2 days for
non-sensitive `drive.file` scope; sometimes longer if they ask follow-up
questions. Reply promptly — slow operator response is the #1 reason
verification stalls.

## After verification

- Replace the placeholder constants in `events/backends/google_drive.py`
with the published `client_id` + `client_secret` (if not already done in
step 4).
- Update `tests/test_backends_google_drive_unit.py::test_bundled_client_config_raises_when_placeholders_present`
— it'll auto-skip once placeholders are gone, but you can also rewrite
it to assert the published config dict is well-formed.
- Remove the "Provision an OAuth client" section from
`docs/team-mode-setup.md` (already done — that section was removed when
we pivoted from operator-supplied to bundled client).
- Cut a release; users get the 1-click flow with no unverified-app
warning.

## If verification gets denied

Most common reasons + fixes:

| Reason | Fix |
|---|---|
| Privacy policy doesn't mention Google scopes | Add the boilerplate paragraph above |
| Demo video doesn't show the OAuth flow | Re-record with the consent screen visible |
| Application home page doesn't link to OAuth-using product | Make sure the landing page mentions the team-mode feature |
| Trademark concern with "Bicameral" | Provide trademark documentation if challenged (we own the domain) |
Loading
Loading