Skip to content

feat(pipes): team pipe sharing with versioning, auto-update, and unshare#3971

Merged
louis030195 merged 2 commits into
mainfrom
claude/magical-brown-7527da
Jun 10, 2026
Merged

feat(pipes): team pipe sharing with versioning, auto-update, and unshare#3971
louis030195 merged 2 commits into
mainfrom
claude/magical-brown-7527da

Conversation

@louis030195

@louis030195 louis030195 commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

What

Lets team and enterprise plan users share pipes across their team from the desktop app, with versioned updates and clean unshare. Supersedes the stale-closed #3738 and fixes its gaps.

team pipe sharing flow

How it works

Share. A team admin shares one of their pipes from the pipe row menu. The payload goes through the existing team configs channel as a plaintext row (decision: no E2E for pipe shares, see below). Only raw_content travels. The author's parsed config object is never pushed (it can hold secrets; #3738 shipped it). Share/unshare is admin-only for now because the backend gates team-scope config writes to admins; member-level sharing later is a one-line backend loosening.

Install. Teammates get a local copy under ~/.screenpipe/pipes/<name>/ marked with # team-shared:vN (same pattern as enterprise-managed pipes). It arrives OFF by default, the recipient opts in with the normal toggle. The editor is read-only for marked copies; "fork to edit" strips the marker, renames, and stops auto-updates. No team encryption key is needed to receive shares, members stuck in key-limbo still get them.

Update. Each share carries an integer version. When the author's local content drifts from the shared copy, the menu shows "push update to team (vN)". Recipients' marked copies auto-update on version bumps, preserving each member's own on/off choice (mirrors syncManagedPipes semantics). A 5 minute poll picks up changes while the app is open.

Unshare. Deleting the share disables (never deletes) every marked copy on next sync, with a "no longer shared" badge. The sweep is gated on configsFetched (new flag in use-team) so a failed configs fetch can never look like "everything was unshared" and mass-disable pipes.

Enterprise fix. syncManagedPipes and its policy caller had a known gap: pipes removed from the policy stayed enabled on devices forever (the early-return on empty list plus the length > 0 gate). Policy sync now always runs and disables enterprise-marked pipes dropped from the policy. Pruning only happens when the server response actually contained the managedPipes field, so an older backend that omits it cannot mass-disable a fleet.

Why plaintext (no E2E) for pipe shares

The E2E team-key ceremony (AES key only on desktops, invite links carrying the key in the URL fragment, keys that don't follow accounts, missingKey limbo, unrecoverable data when devices are lost) was the dominant source of team support pain, and it would have silently gated this feature: a member without the key receives nothing. Pipes are workflow prompts, not credentials, and the enterprise managed-pipes path (enterprise_pipes) is already server-readable plaintext. TLS in transit and Supabase at-rest encryption still apply.

Mechanically: the plaintext envelope reuses the same /api/team/configs columns with a sentinel nonce ("plaintext"), and the server already treats value_encrypted as an opaque string, so zero backend changes. fetchConfigs parses plaintext rows without a key and still decrypts encrypted rows (AI presets keep E2E, they carry real API keys).

Safety details

  • share keys become directory names, so they are validated (isSafePipeName) against path traversal before any filesystem write
  • a local pipe with the same name but no marker is never clobbered by an incoming share
  • managed-copy detection is marker-based, not name-based (feat(pipes): share pipes across a team from the desktop #3738 matched by name, which would have locked a user's own colliding pipe read-only)
  • delete is hidden while a share is active (sync would reinstall it) and reappears once unshared

Code layout

  • lib/team-pipes.ts: pure sync logic (plaintext envelope, marker parse/strip, enabled-flag handling scoped to frontmatter, version bump, install/update/skip planner), no React or Tauri imports
  • lib/__tests__/team-pipes.test.ts: 24 unit tests over that logic
  • components/settings/pipes-section.tsx: UI wiring (badges, menu items, read-only editor, sync effect)
  • lib/hooks/use-team.ts: pushConfigPlain, plaintext-aware fetchConfigs, configsFetched flag
  • lib/hooks/use-enterprise-pipes.ts + use-enterprise-policy.ts: removal-on-unlist fix

Test plan

  • bun test lib/__tests__/team-pipes.test.ts lib/__tests__/team-api-contract.test.ts (35 pass)
  • bun run build passes (Next type checking on)
  • verified deployed /api/team/configs PUT accepts the plaintext envelope shape (route stores value_encrypted/nonce as opaque strings; config_type: "pipe" already allowlisted)
  • manual two-account smoke test: share, opt-in, push update, unshare

🤖 Generated with Claude Code

Louis Beaumont and others added 2 commits June 10, 2026 09:18
Share a pipe with your team from the desktop app (E2E-encrypted team
configs). Improves on the stale-closed #3738 design:

- versioned shares: payload carries an integer version; re-sharing bumps
  it ("push update to team (vN)" appears when local content differs)
- recipients' copies carry a `# team-shared:vN` marker (mirrors
  enterprise-managed pipes) and auto-update on version bumps while
  preserving each member's own on/off choice
- new installs arrive OFF by default, read-only in the editor; "fork to
  edit" strips the marker and stops auto-updates
- unshare now propagates: marked copies whose share disappeared are
  disabled (never deleted), gated on a successful configs fetch so a
  failed fetch can never mass-disable
- security: only raw_content travels (the author's parsed config object
  can hold secrets and is no longer pushed); share keys are validated
  against path traversal before any filesystem write
- enterprise managed pipes get the same removal fix: policy sync now
  always runs and disables enterprise-marked pipes dropped from the
  policy (pruning only when the server actually returned the field)

Pure sync logic lives in lib/team-pipes.ts with unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Product decision: nobody asked for E2E on pipe shares, and the team key
management (invite-link fragments, keys that don't follow accounts,
missingKey limbo, unrecoverable data) was the main UX killer. Pipes are
workflow prompts, not credentials, and the enterprise managed-pipes path
is already server-readable. TLS + at-rest encryption still apply. E2E
stays for config types that carry real secrets (AI presets).

- plaintext envelope reuses the same /api/team/configs columns with a
  sentinel nonce ("plaintext") — zero backend changes, the server treats
  value_encrypted as an opaque string either way
- new pushConfigPlain in use-team (no team key required); fetchConfigs
  now parses plaintext rows even when the team key is missing, so
  members in key-limbo still receive shared pipes
- share/unshare UI gated on team admin role — the backend 403s
  team-scope writes from members, so don't show them a dead button
  (member-level sharing later = one-line backend loosening)
- mockup updated to match

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@louis030195 louis030195 merged commit bca12d4 into main Jun 10, 2026
18 of 20 checks passed
louis030195 pushed a commit that referenced this pull request Jun 10, 2026
team-pipes.test.ts imports from bun:test but was missing from both
vitest's exclude list and the test:bun glob, so vitest tried to run it
and failed to resolve bun:test (Frontend Tests red on main). Its two
siblings (team-crypto, team-api-contract) were added in #3971 but this
one was missed. Add it to both, matching the existing pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant