-
Notifications
You must be signed in to change notification settings - Fork 6
[Epic] Cross-Channel Visibility #162
Description
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_metacolumn migration tosrc/state/store.tsrunMigrations()- SQL:ALTER TABLE workspace_overrides ADD COLUMN allow_paths_meta TEXT NOT NULL DEFAULT '[]'; guard withPRAGMA table_infoidempotency check; immediately backfill all existing rows converting flatallow_pathsentries 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.mdtemplate indocs/coordinator-pattern/AGENTS.md- coordinator role preamble,bd list --jsonfilter workflow, max 400-char response cap,deny_toolsguidance, structured response schema{ "open": [...], "blocked": [...], "completed": [...] }(FR-201, FR-202, AC-301) - [T003] [P] [US2] Write
ask_agentcoordination pattern runbook indocs/coordinator-pattern/README.md- sections: overview, scaling table (1-3 bots OK, 4-8 monitor, 9+ use Phase 2), example calls,deny_toolsusage, ephemeral session pool notes, existing 300s timeout behavior,agent_callstable 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()intests/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 usingfs.realpathSync(AC-103, AC-104) - [T005] [P] [US1] Add
PathGrantinterface tosrc/types.ts-export interface PathGrant { path: string; readOnly: boolean; }with JSDoc (FR-001, FR-002) - [T006] [US1] Add
getWorkspacePathGrants()tosrc/state/store.tsand updatesetWorkspaceOverridesignature - readallow_paths_meta; fall back to flatallow_pathsmapped to{ path, readOnly: false }for backward compat; write both fields atomically (FR-002, NFR-004; depends on T005) - [T007] [US1] Implement
checkPathAccess()helper insrc/core/session-manager.ts- signature(realPath, workDir, pathGrants): { allowed, readOnly }; workspace is always R/W; first matching grant wins; callers MUST passfs.realpathSync(targetPath)to prevent symlink escape (FR-004, NFR-003; depends on T005, T006) - [T008] [US1] Update
grant_path_accesshandler insrc/core/session-manager.ts(approx. line 2316) - addreadOnly?: booleanparam (defaultfalse); no-op if path+mode unchanged; update in place if mode differs; sensitive-path block list applies even forreadOnly: 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_accessinsrc/core/session-manager.ts- load grants viagetWorkspacePathGrants(); 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 insrc/core/session-manager.ts- forsend_file,show_file_in_chat, and any tool using theisAllowedpattern: resolve real path; callcheckPathAccess(); if allowed + readOnly + write op: return❌ Refused: /path is read-only for this agent.; log refused writes atwarnlevel 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-serversetup, workspace.envconfig, 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_SERVERto bridge.envreference - mark optional; note no hard Dolt dependency; include feature-branch caveat; cross-referencedocs/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 howreadOnly: truelayers bridge-level enforcement on top of the Dolt server; clarify bridge enforcement covers tool-level writes only; cross-reference FR-006 anddocs/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 usesbashdirectly; document the two bypass commands; document four operator mitigations with strength ratings (OS permissions, Docker:romount recommended, KubernetesreadOnlyvolumeMount, dedicated OS user per bot) (FR-006; satisfies "Shell-level enforcement not attempted in bridge") - [T016] [P] Open PR against
ChrisRomp/copilot-bridgewith Phase 1 (Phase 3) changes - link spec and plan; include AC-101 through AC-110 verification checklist; include before/afterlist_agent_accessoutput; note bash escape gap with link todocs/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 flatallow_pathsstring array, preserving backward compatibility. Both are updated atomically insetWorkspaceOverride. The flat array is kept to avoid breaking any caller that reads it directly. - Symlink escape prevention:
checkPathAccess()callers MUST resolve the target path withfs.realpathSyncbefore calling the helper. ThestartsWithprefix 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:robind mounts (kernel-enforced). Documented indocs/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~/.sshwould expose secrets and is refused at the same layer as read-write grants.
Key Risks
bdCLI Dolt support unverified:BEADS_DOLT_SHARED_SERVERis on a Beads feature branch. Phase 4 runbook tasks include a caveat to verify before enabling.allow_paths_metavs. separatepath_grantstable: 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_agentcoordinator 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. TheALTER TABLE ... ADD COLUMNwith 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