Skip to content

[Epic] Cross-Channel Visibility #162

@raykao

Description

@raykao

Overview

Cross-Channel Visibility gives copilot-bridge agents a structured way to read task state across workspaces, enabling agents to coordinate on shared work without requiring a human to relay information between them. Today, each bot's workspace is fully isolated -- independent Beads/SQLite databases per channel -- and the only shared state is ~/.copilot-bridge/state.db, which tracks sessions and preferences, not task content. This epic covers three coordination patterns in priority order: a readOnly flag for grant_path_access (requires minimal bridge changes), a shared Beads/Dolt instance (ops configuration only), and a coordinator bot pattern that works today with zero bridge changes.

Spec Reference

Full specification and plan: raykao/dark-factory - specs/cross-channel-visibility/

Phases and Tasks

Phase 1: Foundational - DB Migration

Purpose: Additive schema change that blocks all Phase 1 code tasks. Must land first.

CRITICAL: No US1 implementation task can begin until T001 is merged.

  • [T001] Add allow_paths_meta column migration to src/state/store.ts runMigrations() - SQL: ALTER TABLE workspace_overrides ADD COLUMN allow_paths_meta TEXT NOT NULL DEFAULT '[]'; guard with PRAGMA table_info idempotency check; immediately backfill all existing rows converting flat allow_paths entries to { path, readOnly: false } JSON objects; additive-only, WAL-safe (FR-003, NFR-002, AC-108)

Phase 2: User Story 2 - Coordinator Bot Pattern (Docs/Ops Only)

Goal: Document the zero-code cross-channel coordination pattern that works today, making it a first-class supported operator pattern. No bridge code changes.

  • [T002] [P] [US2] Write coordinator bot AGENTS.md template in docs/coordinator-pattern/AGENTS.md - coordinator role preamble, bd list --json filter workflow, max 400-char response cap, deny_tools guidance, structured response schema { "open": [...], "blocked": [...], "completed": [...] } (FR-201, FR-202, AC-301)
  • [T003] [P] [US2] Write ask_agent coordination pattern runbook in docs/coordinator-pattern/README.md - sections: overview, scaling table (1-3 bots OK, 4-8 monitor, 9+ use Phase 2), example calls, deny_tools usage, ephemeral session pool notes, existing 300s timeout behavior, agent_calls table schema (FR-201, FR-202, AC-301, AC-302, AC-303)

Phase 3: User Story 1 - readOnly Flag for grant_path_access

Goal: Close the only security gap requiring a bridge code change -- give admins a way to grant peer-read access without granting write capability.

  • [T004] [P] [US1] Write unit tests for checkPathAccess() in tests/unit/checkPathAccess.test.ts - 7 truth-table cases (workspace path, R/W grant, R/O grant, outside grant, exact match, workspace wins over overlapping R/O, empty grants) plus 1 symlink-escape test using fs.realpathSync (AC-103, AC-104)
  • [T005] [P] [US1] Add PathGrant interface to src/types.ts - export interface PathGrant { path: string; readOnly: boolean; } with JSDoc (FR-001, FR-002)
  • [T006] [US1] Add getWorkspacePathGrants() to src/state/store.ts and update setWorkspaceOverride signature - read allow_paths_meta; fall back to flat allow_paths mapped to { path, readOnly: false } for backward compat; write both fields atomically (FR-002, NFR-004; depends on T005)
  • [T007] [US1] Implement checkPathAccess() helper in src/core/session-manager.ts - signature (realPath, workDir, pathGrants): { allowed, readOnly }; workspace is always R/W; first matching grant wins; callers MUST pass fs.realpathSync(targetPath) to prevent symlink escape (FR-004, NFR-003; depends on T005, T006)
  • [T008] [US1] Update grant_path_access handler in src/core/session-manager.ts (approx. line 2316) - add readOnly?: boolean param (default false); no-op if path+mode unchanged; update in place if mode differs; sensitive-path block list applies even for readOnly: true; new confirm messages for grant/mode-change (FR-001, FR-007, FR-009, FR-010, NFR-001, AC-101, AC-106, AC-107, AC-110; depends on T006, T007)
  • [T009] [US1] Update list_agent_access in src/core/session-manager.ts - load grants via getWorkspacePathGrants(); annotate each extra path with (read-only) or (read-write) suffix; no-grant output unchanged (FR-008, AC-102; depends on T006)
  • [T010] [US1] Update buildCustomTools() write guard in src/core/session-manager.ts - for send_file, show_file_in_chat, and any tool using the isAllowed pattern: resolve real path; call checkPathAccess(); if allowed + readOnly + write op: return ❌ Refused: /path is read-only for this agent.; log refused writes at warn level with bot name, path, and operation type (FR-004, FR-005, NFR-005, AC-103, AC-104, AC-105; depends on T007)
  • [T011] [P] [US1] Write integration tests in tests/integration/readOnly-enforcement.test.ts - 5 end-to-end scenarios: read on R/O path succeeds; write on R/O path returns ❌ Refused; write on own workspace with active R/O grant elsewhere succeeds; revoke removes grant; re-grant as R/W and write succeeds; also: sensitive-path refusal, old DB migration/backfill (depends on T010)

Phase 4: User Story 3 - Shared Dolt Ops Runbook (Docs/Ops Only)

Goal: Give operators a self-contained runbook to stand up a shared Dolt/Beads instance with per-agent branch isolation. No bridge code changes.

  • [T012] [P] [US3] Write shared Dolt ops runbook in docs/shared-dolt-runbook.md - sections: prerequisites, dolt sql-server setup, workspace .env config, branch-per-agent naming convention (agent/<bot-name>), merge workflow, conflict resolution notes, 4-bot vs 9+-bot scaling guidance, known limitations (FR-101, FR-102, AC-201, AC-202, AC-203)
  • [T013] [P] [US3] Add BEADS_DOLT_SHARED_SERVER to bridge .env reference - mark optional; note no hard Dolt dependency; include feature-branch caveat; cross-reference docs/shared-dolt-runbook.md (FR-101, NFR-101, AC-201)
  • [T014] [US3] Add "Combining Phase 1 + Phase 2" section to docs/shared-dolt-runbook.md - show how readOnly: true layers bridge-level enforcement on top of the Dolt server; clarify bridge enforcement covers tool-level writes only; cross-reference FR-006 and docs/security-known-gaps.md (FR-103; depends on T012)

Phase 5: Polish and Cross-Cutting Concerns

Purpose: Finalize security gap documentation and prepare the upstream PR.

  • [T015] [P] Add bash-tool escape known-gap section to docs/security-known-gaps.md - explain bridge cannot intercept shell-level writes when an agent uses bash directly; document the two bypass commands; document four operator mitigations with strength ratings (OS permissions, Docker :ro mount recommended, Kubernetes readOnly volumeMount, dedicated OS user per bot) (FR-006; satisfies "Shell-level enforcement not attempted in bridge")
  • [T016] [P] Open PR against ChrisRomp/copilot-bridge with Phase 1 (Phase 3) changes - link spec and plan; include AC-101 through AC-110 verification checklist; include before/after list_agent_access output; note bash escape gap with link to docs/security-known-gaps.md (depends on T004-T011 all green)

Acceptance Criteria

Phase 1 (User Story 1 - readOnly Flag)

ID Criterion Test Method
AC-101 grant_path_access with readOnly: true succeeds and stores the flag Call tool; inspect workspace_overrides.allow_paths_meta in SQLite
AC-102 list_agent_access shows (read-only) for a read-only grant Call tool; verify output text
AC-103 A bridge-managed file tool (e.g., show_file_in_chat) can read from a read-only path Invoke tool; confirm success
AC-104 No bridge-managed file tool can write to a read-only path Attempt write; confirm ❌ Refused response
AC-105 Write attempt is logged at warn level Check bridge logs after refused write
AC-106 Sensitive paths (~/.ssh, $HOME, etc.) are refused even with readOnly: true Attempt grant; confirm refusal error
AC-107 Existing grant_path_access calls without readOnly continue to work identically Run existing calls; verify no regression
AC-108 Old state.db (pre-migration) upgrades transparently on bridge restart Start bridge with old DB; verify allow_paths_meta column appears and is backfilled
AC-109 revoke_path_access removes a read-only grant identically to a read-write grant Revoke; confirm path absent from list_agent_access
AC-110 Re-granting a path with a different mode updates in place with a confirmation message Grant R/W, then grant R/O for same path; confirm updated response

Phase 2 (User Story 3 - Shared Dolt)

ID Criterion Test Method
AC-201 BEADS_DOLT_SHARED_SERVER documented in bridge .env guidance Review documentation
AC-202 Operator can follow the runbook to stand up a shared Dolt server Execute runbook steps; confirm dolt sql-server starts
AC-203 Two bots connecting to the shared Dolt server can each see the other's tasks after a branch merge Create tasks from both bots; merge branches; query from each bot

Phase 0 (User Story 2 - Coordinator Bot)

ID Criterion Test Method
AC-301 ask_agent successfully routes a task-state query to a coordinator bot Send query; confirm response with task data
AC-302 agent_calls table records the interaction Inspect SQLite after the call
AC-303 No deadlock or session exhaustion with 4 simultaneous ask_agent calls to coordinator Send 4 concurrent calls; confirm all resolve within timeout

Notes

Key Design Decisions

  • Storage format: Extra path grants are stored as JSON objects (allow_paths_meta) alongside the existing flat allow_paths string array, preserving backward compatibility. Both are updated atomically in setWorkspaceOverride. The flat array is kept to avoid breaking any caller that reads it directly.
  • Symlink escape prevention: checkPathAccess() callers MUST resolve the target path with fs.realpathSync before calling the helper. The startsWith prefix check alone is insufficient -- a symlink inside a read-only grant that resolves outside the grant boundary must be refused.
  • bash escape is out of scope: Bridge enforcement operates at the MCP tool invocation layer only. Shell-level writes (e.g., echo "data" > /path/file) cannot be intercepted. The recommended mitigation for same-host multi-bot deployments is Docker :ro bind mounts (kernel-enforced). Documented in docs/security-known-gaps.md.
  • Sensitive-path block list applies equally to read-only grants: FR-007 is not relaxed for readOnly: true. A read-only grant to ~/.ssh would expose secrets and is refused at the same layer as read-write grants.

Key Risks

  • bd CLI Dolt support unverified: BEADS_DOLT_SHARED_SERVER is on a Beads feature branch. Phase 4 runbook tasks include a caveat to verify before enabling.
  • allow_paths_meta vs. separate path_grants table: The current design stores per-path metadata as JSON in one column (simpler, additive migration) rather than a normalized table. If the number of grants per bot grows large, a separate table may be preferable. Deferred as an open question.
  • Coordinator bot scaling limit: The ask_agent coordinator pattern is bounded by the bridge's ephemeral session pool. At 9+ concurrent worker bots the runbook recommends switching to the shared Dolt architecture.
  • WAL mode assumption: The migration and backfill logic assumes SQLite WAL mode is active on state.db. The ALTER TABLE ... ADD COLUMN with a DEFAULT never rewrites existing rows and is safe under WAL, but the backfill loop requires WAL isolation for concurrent readers.

Execution Order

Phase 2 (T002, T003) -- no dependencies, start immediately
Phase 4 (T012, T013, T014) -- no code dependencies, start immediately
Phase 1 (T001) -- land first to unblock Phase 3
  └-► Phase 3 (T004-T011) -- T001 MUST land first
Phase 5: T015 anytime; T016 after Phase 3 complete

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions