Collaborative document writes in shared chats — implementation plan

Status: In progress · Author: Christophe (with Claude) · Date: 2026-06-30 · Repo: north-os · Related: chat sharing (2026-06-27), document editing (2026-06-10)

Problem

In a shared chat only the thread owner could confirm an agent document/email write (a owner_must_confirm 403 in run.ts). Making it collaborative exposes a second issue: a case_document version row stores one OneDrive pointer (the confirmer's), so other participants can't open the doc — it lives in someone else's drive.

Design

  1. Remove the owner-only gate. Writes already execute under the resumer's identity (configurable.user_id = input.userId), so any participant who confirms acts as themselves — own North Drafts, own mailbox. Identity-safe.
  2. Per-user delivery ledger case_document_delivery (one row per version × user). The confirmer still auto-delivers to their own drive (solo behaviour preserved); every other participant gets a "Save to my OneDrive" action that delivers the R2 bytes to their drive on demand and records their pointer. case_document.onedrive_* stays the confirmer's (load-bearing for the Word doc↔thread R1 lookup) — never overwritten.

Decisions

Deferred (open questions, defaulted)

Build order

  1. case_document_delivery table in packages/db/src/schema/document.ts.
  2. GRANT in packages/db/scripts/provision-roles.ts (app_role: SELECT/INSERT/UPDATE).
  3. db:generate → review → db:migrateprovision (0056).
  4. Store (apps/web/lib/documents/store.ts): relax loadDeliverableVersion to thread-visibility (reuse threadVisible); add findUserDelivery + recordUserDelivery (never touch case_document.onedrive_*).
  5. Deliver route (router.ts): per-user idempotency + record.
  6. Agent auto-deliver (document-write-access.ts / document-write-shared.ts): confirmer also upserts a per-user delivery row.
  7. Card marker attributable: add deliveredBy (additive).
  8. Remove the owner_must_confirm gate (run.ts 958-980 + pendingPayloadKind).
  9. chat-runtime: listMyDocumentDeliveries batched map + export threadVisible.
  10. chat router: return documentDeliveries on /threads/:id/messages.
  11. chat-ui: viewer + deliveries context; LiveChat props.
  12. chat-ui: DocumentResultCard per-viewer Open/Save.
  13. Wire currentUserId + initialDocumentDeliveries through render sites.
  14. Tests (invert §7.4; deliver per-user; card branches; map) + cleanup.

Risks (carry into review)

Ship gate+table together; scope relaxation exact (threadVisible, not org-wide); never write case_document.onedrive_* on a collaborator deliver; migration discipline (db:generate only, GRANT not in migration, no db:push); every new query inside withRlsTransaction; per-user write is ON CONFLICT … DO UPDATE; keep threadCreatedBy + interruptPayloadToolCallId.