You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.tsrunMigrations() - 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)
[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.
[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
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
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: areadOnlyflag forgrant_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.
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.
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)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 -
readOnlyFlag forgrant_path_accessGoal: Close the only security gap requiring a bridge code change -- give admins a way to grant peer-read access without granting write capability.
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)PathGrantinterface tosrc/types.ts-export interface PathGrant { path: string; readOnly: boolean; }with JSDoc (FR-001, FR-002)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)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)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)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)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)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.
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)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)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.
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")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)
grant_path_accesswithreadOnly: truesucceeds and stores the flagworkspace_overrides.allow_paths_metain SQLitelist_agent_accessshows(read-only)for a read-only grantshow_file_in_chat) can read from a read-only path❌ Refusedresponsewarnlevel~/.ssh,$HOME, etc.) are refused even withreadOnly: truegrant_path_accesscalls withoutreadOnlycontinue to work identicallystate.db(pre-migration) upgrades transparently on bridge restartallow_paths_metacolumn appears and is backfilledrevoke_path_accessremoves a read-only grant identically to a read-write grantlist_agent_accessPhase 2 (User Story 3 - Shared Dolt)
BEADS_DOLT_SHARED_SERVERdocumented in bridge.envguidancedolt sql-serverstartsPhase 0 (User Story 2 - Coordinator Bot)
ask_agentsuccessfully routes a task-state query to a coordinator botagent_callstable records the interactionask_agentcalls to coordinatorNotes
Key Design Decisions
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.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.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.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.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.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